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))
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)
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))
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