Example #1
0
    def test_warnings(self):
        with self.assertWarns(Warning) as w:
            crop_points(self.edge['triangle'], [0, 100, 100, 0])

        self.assertIn('Check the order', str(w.warning))

        with self.assertWarns(Warning) as w:
            crop_points(self.edge['triangle'], [-100, 100, 0, 100])

        self.assertIn('All bounds must', str(w.warning))
Example #2
0
    def test_crop_size(self):
        edge = self.edge

        triangle = edge['triangle']
        circle = edge['circle']

        t_crop = crop_points(triangle, [200, 400, 200, 400])
        self.assertTrue(crop_points(circle, [200, 400, 200, 400]).size
                        == 0)
        self.assertFalse(199 in t_crop)
        self.assertFalse(0 in t_crop)
        self.assertTrue(200 in t_crop)
        self.assertTrue(t_crop.size == 604)
Example #3
0
    def test_crop_full(self):
        edge = self.edge

        triangle_size = self.image['triangle'].shape
        circle_size = self.image['circle'].shape

        triangle = edge['triangle']
        circle = edge['circle']

        t_crop = crop_points(triangle, [0, triangle_size[1],
                                             0, triangle_size[0]])
        c_crop = crop_points(circle, [0, circle_size[1],
                                           0, circle_size[0]])

        self.assertTrue(np.array_equal(t_crop, triangle))
        self.assertTrue(np.array_equal(c_crop, circle))
Example #4
0
    def test_crop_above_line(self):
        edge_c = self.edge['circle']

        line = {}
        line[L] = lambda x, y: x
        line[R] = lambda x, y: x
        line[T] = lambda x, y: y
        line[B] = lambda x, y: y - x
        c_crop = crop_points(edge_c, [0, 600, 0, 0])

        self.assertTrue(all([y <= x for x, y in c_crop]))
def generate_droplet_width(crop, bounds=None, f=None):
    # Look for the greatest distance between points on the baseline
    # by calculating points that are in the circle within the linear
    # threshold

    if bounds is not None:
        just_inside = crop_points(crop, bounds, f=f)
    else:
        just_inside = crop

    limits = {L: np.amin(just_inside[:, 0]), R: np.amax(just_inside[:, 0])}
    return limits
def analyze_frame(im, time, bounds, circ_thresh, lin_thresh, σ, low, high, ε,
                  lim, fit_type):
    '''
    Report the main findings for a single contact angle image

    Takes the provided image and fits it with the specified method ['linear',
    'circular', 'bashforth-adams'] to calculate the droplet contact angle,
    baseline width, and volume.

    Its main use lies within the DropPy main script, but it can also be used
    externally for debugging individual frames of a video file.

    :param im: 2D numpy array of a grayscale image
    :param time: float value of the movie time after burn-in
    :param bounds: edges of the box which crop the image
    :param circ_thresh: height above which the baseline does not exist
    :param lin_thresh: distance that preserves a set of linear points on the
                       droplet
    :param σ: value of the Gaussian filter used in the Canny algorithm
    :param low: value of the weak pixels used in dual-thresholding
    :param high: value of the strong pixels used in dual-thresholding
    :param ε: size of finite difference step to take in approximating baseline
              slope
    :param lim: maximum number of iterations to take during circle fitting
    :param fit_type: specified method for fitting the droplet profile
    :return: 5-tuple of (L, R) contact angles, contact area diameter,
             calculated droplet volume, fitted (x, y) points on droplet, and
             fitted (x,y) points on baseline
    '''
    coords = extract_edges(im, σ=σ, low=low, high=high)

    if bounds is None:
        bounds = auto_crop(im, σ=σ, low=low, high=high)

    crop = crop_points(coords, bounds)

    cropped_edges = np.zeros((np.max(crop[:, 1]) + 1, np.max(crop[:, 0]) + 1),
                             dtype=bool)

    for pt in crop:
        cropped_edges[pt[1], pt[0]] = True

    # Get the baseline from the linear Hough transform
    accums, angles, dists = hough_line_peaks(*hough_line(cropped_edges),
                                             num_peaks=5)

    # Change parameterization from (r, θ) to (m, b) for standard form of line
    a = [dists[0] / np.sin(angles[0]), -np.cos(angles[0]) / np.sin(angles[0])]

    f = {i: lambda x, y: x for i in [L, R]}
    f[B] = lambda x, y: y - (np.dot(a, np.power(x, range(len(a)))))
    f[T] = lambda x, y: y

    b = np.copy(bounds)
    b[3] = -circ_thresh
    circle = crop_points(crop, b, f=f)

    # Make sure that flat droplets (wetted) are ignored
    # (i.e. assign angle to NaN and continue)
    if circle.shape[0] < 5:
        return (np.nan, np.nan), np.nan, np.nan, np.array(
            ((np.nan, np.nan), (np.nan, np.nan))), np.array(
                ((np.nan, np.nan), (np.nan, np.nan)))

    # Baseline
    x = np.linspace(0, im.shape[1])
    y = np.dot(a, np.power(x, [[po] * len(x) for po in range(2)]))

    baseline = np.array([x, y]).T

    if fit_type == 'linear':
        b = np.copy(bounds)
        b[3] = -(circ_thresh + lin_thresh)
        limits = generate_droplet_width(crop, b, f)

        # Get linear points
        f[T] = f[B]
        linear_points = {
            L:
            crop_points(crop, [
                int(limits[L] - 2 * lin_thresh),
                int(limits[L] + 2 * lin_thresh), -(circ_thresh + lin_thresh),
                -circ_thresh
            ],
                        f=f),
            R:
            crop_points(crop, [
                int(limits[R] - 2 * lin_thresh),
                int(limits[R] + 2 * lin_thresh), -(circ_thresh + lin_thresh),
                -circ_thresh
            ],
                        f=f)
        }

        if linear_points[L].size == 0 or linear_points[R].size == 0:
            raise IndexError('We could not identify linear points, '
                             f'try changing lin_thresh from {lin_thresh}')

        v, b, m, bv, vertical = generate_vectors(linear_points, limits, ε, a)

        # Calculate the angle between these two vectors defining the
        # base-line and tangent-line
        ϕ = {i: calculate_angle(bv[i], v[i]) for i in [L, R]}

        fit = {}
        # Plot lines
        for side in [L, R]:
            x = np.linspace(0, im.shape[1])
            if not vertical[side]:
                y = m[side] * x + b[side]
            else:
                y = np.linspace(0, im.shape[0])
                x = m[side] * y + b[side]
            fit[side] = np.array([x, y]).T

        baseline_width = limits[R] - limits[L]

        volume = np.NaN
        # TODO:// Add the actual volume calculation here!

    elif fit_type == 'circular' or fit_type == 'bashforth-adams':
        # Get the cropped image width
        width = bounds[1] - bounds[0]

        res = fit_circle(circle, width, start=True)
        *z, r = res['x']

        theta = np.linspace(0, 2 * np.pi, num=500)
        x = z[0] + r * np.cos(theta)
        y = z[1] + r * np.sin(theta)

        iters = 0

        # Keep retrying the fitting while the function value is
        # large, as this indicates that we probably have 2 circles
        # (e.g. there's something light in the middle of the image)
        while res['fun'] >= circle.shape[0] and iters < lim:

            # Extract and fit only those points outside
            # the previously fit circle
            points = np.array([(x, y) for x, y in circle
                               if (x - z[0])**2 + (y - z[1])**2 >= r**2])
            res = fit_circle(points, width)
            *z, r = res['x']
            iters += 1

        x_t, y_t = find_intersection(a, res['x'])

        v1, v2 = generate_circle_vectors([x_t, y_t])

        ϕ = {i: calculate_angle(v1, v2) for i in [L, R]}
        if fit_type == 'circular':
            baseline_width = 2 * x_t

            volume = (2 / 3 * np.pi * r**3 + np.pi * r**2 * y_t -
                      np.pi * y_t**3 / 3)

            # Fitted circle
            theta = np.linspace(0, 2 * np.pi, num=100)
            x = z[0] + r * np.cos(theta)
            y = z[1] + r * np.sin(theta)
            fit = np.array([x, y]).T
        else:
            # Get points within 10 pixels of the circle edge
            points = np.array([(x, y) for x, y in circle
                               if (x - z[0])**2 + (y - z[1])**2 >= (r - 10)**2
                               ])

            points[:, 1] = -np.array([
                y - np.dot(a, np.power(y, range(len(a)))) for y in points[:, 1]
            ])
            center = (np.max(points[:, 0]) + np.min(points[:, 0])) / 2
            points[:, 0] = points[:, 0] - center
            h = np.max(points[:, 1])
            points = np.vstack([points[:, 0], h - points[:, 1]]).T

            cap_length, curv = fit_bashforth_adams(points).x
            θs, pred = sim_bashforth_adams(h, cap_length, curv)
            ϕ[L] = -np.min(θs)
            ϕ[R] = np.max(θs)

            θ = (ϕ[L] + ϕ[R]) / 2

            R0 = pred[np.argmax(θs), 0] - pred[np.argmin(θs), 0]
            baseline_width = R0

            P = 2 * cap_length / curv
            volume = np.pi * R0 * (R0 * h + R0 * P - 2 * np.sin(θ))
            x = pred[:, 0] + center
            y = np.array([
                np.dot(a, np.power(y, range(len(a)))) + y
                for y in (pred[:, 1] - h)
            ])
            fit = np.array([x, y]).T

    else:
        raise Exception('Unknown fit type! Try another.')

    # FI FITTYPE
    output_text(time, ϕ, baseline_width, volume)

    return (ϕ[L], ϕ[R]), baseline_width, volume, fit, baseline