def refine_worm(image, initial_area, candidate_edges): # find strong worm edges (roughly equivalent to the edges found by find_initial_worm, # which are in candidate_edges): smooth the image, do canny edge-finding, and # then keep only those edges near candidate_edges smooth_image = restoration.denoise_tv_bregman(image, 140).astype(numpy.float32) smoothed, gradient, sobel = canny.prepare_canny(smooth_image, 8, initial_area) local_maxima = canny.canny_local_maxima(gradient, sobel) candidate_edge_region = ndimage.binary_dilation(candidate_edges, iterations=4) strong_edges = local_maxima & candidate_edge_region # Now threshold the image to find dark blobs as our initial worm region # First, find areas in the initial region unlikely to be worm pixels mean, std = mcd.robust_mean_std(smooth_image[initial_area][::4], 0.85) non_worm = (smooth_image > mean - std) & initial_area # now fit a smoothly varying polynomial to the non-worm pixels in the initial # region of interest, and subtract that from the actual image to generate # an image with a flat illumination field background = polyfit.fit_polynomial(smooth_image, mask=non_worm, degree=2) minus_bg = smooth_image - background # now recalculate a threshold from the background-subtracted pixels mean, std = mcd.robust_mean_std(minus_bg[initial_area][::4], 0.85) initial_worm = (minus_bg < mean - std) & initial_area # Add any pixels near the strong edges to our candidate worm position initial_worm |= ndimage.binary_dilation(strong_edges, iterations=3) initial_worm = mask.fill_small_radius_holes(initial_worm, 5) # Now grow/shrink the initial_worm region so that as many of the strong # edges from the canny filter are in contact with the region edges as possible. ac = active_contour.EdgeClaimingAdvection(initial_worm, strong_edges, max_region_mask=initial_area) stopper = active_contour.StoppingCondition(ac, max_iterations=100) while stopper.should_continue(): ac.advect(iters=1) ac.smooth(iters=1, depth=2) worm_mask = mask.fill_small_radius_holes(ac.mask, 7) # Now, get edges from the image at a finer scale smoothed, gradient, sobel = canny.prepare_canny(smooth_image, 0.3, initial_area) local_maxima = canny.canny_local_maxima(gradient, sobel) strong_sum = strong_edges.sum() highp = 100 * (1 - 1.5*strong_sum/local_maxima.sum()) lowp = max(100 * (1 - 3*strong_sum/local_maxima.sum()), 0) low_worm, high_worm = numpy.percentile(gradient[local_maxima], [lowp, highp]) fine_edges = canny.canny_hysteresis(local_maxima, gradient, low_worm, high_worm) # Expand out the identified worm area to include any of these finer edges closed_edges = ndimage.binary_closing(fine_edges, structure=S) worm = ndimage.binary_propagation(worm_mask, mask=worm_mask|closed_edges, structure=S) worm = ndimage.binary_closing(worm, structure=S, iterations=2) worm = mask.fill_small_radius_holes(worm, 5) worm = ndimage.binary_opening(worm) worm = mask.get_largest_object(worm) # Last, smooth the shape a bit to reduce sharp corners, but not too much to # sand off the tail ac = active_contour.CurvatureMorphology(worm, max_region_mask=initial_area) ac.smooth(depth=2, iters=2) return strong_edges, ac.mask
def find_initial_worm(small_image, well_mask): # plan here is to find known good worm edges with Canny using a stringent threshold, then # relax the threshold in the vicinity of the good edges. # back off another pixel from the well edge to avoid gradient from the edge shrunk_mask = ndimage.binary_erosion(well_mask, structure=S) smoothed, gradient, sobel = canny.prepare_canny(small_image, 2, shrunk_mask) local_maxima = canny.canny_local_maxima(gradient, sobel) # Calculate stringent and medium-stringent thresholds. The stringent threshold # is the 200th-brightest edge pixel, and the medium is the 450th-brightest pixel highp = 100 * (1-200/local_maxima.sum()) highp = max(highp, 94) mediump = 100 * (1-450/local_maxima.sum()) mediump = max(mediump, 94) low_worm, medium_worm, high_worm = numpy.percentile(gradient[local_maxima], [94, mediump, highp]) stringent_worm = canny.canny_hysteresis(local_maxima, gradient, low_worm, high_worm) # Expand out 20 pixels from the stringent worm edges to make our search space stringent_area = ndimage.binary_dilation(stringent_worm, mask=well_mask, iterations=20) # now use the relaxed threshold but only in the stringent area relaxed_worm = canny.canny_hysteresis(local_maxima, gradient, low_worm, medium_worm) & stringent_area # join very close-by objects, and remove remaining small objects candidate_worm = ndimage.binary_dilation(relaxed_worm, structure=S) candidate_worm = ndimage.binary_erosion(candidate_worm) candidate_worm = mask.remove_small_area_objects(candidate_worm, 30, structure=S) # Now figure out the biggest blob of nearby edges, and call that the worm region glommed_candidate = ndimage.binary_dilation(candidate_worm, structure=S, iterations=2) glommed_candidate = ndimage.binary_erosion(glommed_candidate, iterations=2) # get just outline, not any regions filled-in due to closing glommed_candidate ^= ndimage.binary_erosion(glommed_candidate) glommed_candidate = mask.get_largest_object(glommed_candidate, structure=S) worm_area = ndimage.binary_dilation(glommed_candidate, mask=well_mask, structure=S, iterations=12) worm_area = mask.fill_small_radius_holes(worm_area, max_radius=15) candidate_edges = relaxed_worm & candidate_worm & worm_area return candidate_edges, worm_area
def get_well_mask(image): small_image = pyramid.pyr_down(image, 4) smoothed, gradient, sobel = canny.prepare_canny(small_image, 2) local_maxima = canny.canny_local_maxima(gradient, sobel) # well outline has ~6000 px full-size = 1500 px at 4x downsampled # So find the intensity value of the 2000th-brightest pixel, via percentile: highp = 100 * (1-2000/local_maxima.sum()) highp = max(highp, 90) low_edge, high_edge = numpy.percentile(gradient[local_maxima], [90, highp]) # Do canny edge-finding starting with gradient pixels as bright or brighter # than the 2000th-brightest pixel, and spread out to the 90th percentile # intensity: well_edge = canny.canny_hysteresis(local_maxima, gradient, low_edge, high_edge) # connect nearby edges and remove small unconnected bits well_edge = ndimage.binary_closing(well_edge, structure=S) well_edge = mask.remove_small_area_objects(well_edge, 300, structure=S) # Get map of distances and directions to nearest edge to use for contour-fitting distances, nearest_edge = active_contour.edge_direction(well_edge) # initial curve is the whole image less one pixel on the outside initial = numpy.ones(well_edge.shape, dtype=bool) initial[:,[0,-1]] = 0 initial[[0,-1],:] = 0 # Now evolve the curve inward until it contacts the canny well edges. gac = active_contour.GAC(initial, nearest_edge, advection_mask=(distances < 10), balloon_direction=-1) stopper = active_contour.StoppingCondition(gac, max_iterations=200) while stopper.should_continue(): # otherwise evolve the curve by shrinking, advecting toward edges, and smoothing gac.balloon_force(iters=3) gac.advect(iters=2) gac.smooth() gac.smooth(depth=3) # now erode everywhere the contour edge is right on a canny edge: gac.move_to_outside(well_edge[tuple(gac.inside_border_indices.T)]) gac.smooth() well_mask = gac.mask if well_mask.sum() / well_mask.size < 0.25: # if the well mask is too small, something went very wrong well_mask = None return small_image, well_mask
def find_centerline_pixels(dv_coords, mask, sigma=1, worm_width=50, low_threshold=0.25, high_threshold=0.6): """ Find pixels along the centerline of the worm given a DV-coordinate image. Parameters: dv_coords: floating-point image of dorsal-ventral worm coordinates with midline values high and edge values low. Should NOT be masked. mask: segmented mask (approximate is ok, as ridgeline may extend out of the masked region if the dv_coords image supports it. sigma: gaussian blur applied to smooth the dv_coords image worm_width: approximate width of worms at their widest at this magnification. 50 is a reasonable value for adult worms at 5x magnification. low_threshold: lowest dv-coordinate value that could possibly be part of the centerline. high_threshold: lowest dv-coordinate value that would almost certainly be part of the centerline. Returns: all_ridges, extended_ridges, neighboring_ridges all_ridges: all local ridglines in the image, regardless of mask. Mainly useful for diagnostics. extended_ridges: all low-confidence-or-better ridgelines extending from high-confidence regions within the mask. Strictest set of ridges. neighboring_ridges: all low-confidence-or-better ridgelines extending from locations within three pixels of the extended_ridges. Most inclusive set of ridges. """ # A horizontal ridge of dv_coords will have a positive y gradient on the low-y side of the ridge # and a negative gradient on the other, with a zero-gradient patch right along the center of the # ridgeline: # y^ # | # |-------- horizontal ridgeline of high-value dv-coordinates on a low-value background # | # --------->x # # Gradient in y direction: # y^ # |-------- # |00000000 # |++++++++ # --------->x # # A vertical ridge will have a positive x gradient on the low-x side and negative on the other. # A diagonal ridge will be a combination of the above, and the orientation of the diagonal # can be determined by whether the low-x and low-y sides of the ridge are the same. # I.e. if the ridge is +45 degrees, the low-x and low-y sides are opposite: # x-gradient: y-gradient: # y^ y^ y^ # | / ridge |++0- |--0+ # | / |+0-- |-0++ # |/ |0--- |0+++ # ------>x ------>x ------>x # In the above the low-x side of the diagonal ridgeline is above the diagonal and the # low-y side is below. So in this case, the x and y gradients on each side of the ridge # are of opposite sign. # However, if the ridge is -45 degrees, the low-x and low-y sides are both below # the ridge, and so the gradients have the same sign. # x-gradient: y-gradient: # ------>x ------>x ------>x # |\ |0--- |0--- # | \ |+0-- |+0-- # | \ ridge |++0- |++0- # yv yv yv # # To find the ridgeline pixels (0 gradient surrounded by high + and - gradients on # either side), we could look for maxima of the second derivative, # but that is noise-prone in practice. Instead, we will take a local average of # the absolute value of the x- and y-gradients nearby each pixel. The 0-valued ridge # pixels will have high averages. This, however, discards the orientation information. # To recover the degree to which the y-gradient has an + or - diagonal angle, # we compare the local average of the product of the x and y gradients to # the local average of the product of the absolute values of the gradients. # This number ranges from -1 indicating that the x- and y-gradients are on # opposite sides of the ridgeline, to +1 indicating that they're on the same sides. # We then weight the local magnitude in the y direction by this number, to produce # an (x, y) vector that captures the direction normal to the ridge. # # Given these normals, we can then locate the exact center pixels of the ridge # by choosing pixels from the original dv_coordinate image that are local maxima # along the ridge normal. Even if the ridgeline itself is slowly climbing or # descending, the saddle points along the ridge will be maximal normal to the # ridge. This technique comes from the Canny edge-finding method. # # Finally, we also use Canny hysteresis thresholding to extend an initial seed # of high-confidence ridge pixels into potentially lower confidence regions: # If a low-confidence region of ridge is connected to a high-confidence region # (or in this implementation, within a few pixels therof), extend the ridge # into it. smooth_dv = ndimage.gaussian_filter(dv_coords, sigma) ridge = smooth_dv**3 dx, dy = numpy.gradient(ridge) adx, ady = numpy.abs(dx), numpy.abs(dy) # average over a region about the worm's width, so choose a smoothing # gaussian with a standard deviation about 1/4 of the width, and truncate # at two standard deivations (1/2th of the width) on either side of the ridge gradient_sigma = worm_width / 4 local_magnitude_x = ndimage.gaussian_filter(adx, gradient_sigma, truncate=2) local_magnitude_y = ndimage.gaussian_filter(ady, gradient_sigma, truncate=2) orientation = ndimage.gaussian_filter(dx * dy, gradient_sigma, truncate=2) max_orientation = ndimage.gaussian_filter(adx * ady, gradient_sigma, truncate=2) orientation /= max_orientation all_ridges = canny.canny_local_maxima( ridge, [local_magnitude_x, local_magnitude_y * orientation]) # Hysteresis threshold: high_mask below is regions of the local_maxima image # that are within the overall mask region and also have a dv_coord above # the high-confidence value. Then we extend those regions into any connected # low-confidence regions (the low_mask regions), ignoring low-confidence # regions not contiguous with a high-confidence region. high_mask = mask & all_ridges & (smooth_dv >= high_threshold) low_mask = all_ridges & (smooth_dv >= low_threshold) extended_ridges = ndimage.binary_propagation(high_mask, mask=low_mask, structure=FULLY_CONNECTED) # Now add regions of the maxima that are just a couple pixels away from the # extended regions too. # nearby_pixels below is regions of the low_mask that are both within the overall # mask and near to regions in extended_ridges. Then expand out such pixels to # all connected pixels within low_mask nearby_pixels = mask & low_mask & ndimage.binary_dilation( extended_ridges, iterations=3, structure=FULLY_CONNECTED) neighboring_ridges = ndimage.binary_propagation(nearby_pixels, mask=low_mask, structure=FULLY_CONNECTED) neighboring_ridges = morphology.thin(neighboring_ridges) return all_ridges, extended_ridges, neighboring_ridges