def test_trace_boundary(self): # test moore neighbor algorithm m_neighbor = np.array( [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], dtype=np.bool) # refenece neighbors for isbf rx_isbf = [ 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 8, 7, 7, 7, 7, 6, 6, 5, 5, 5, 4, 4, 3, 3, 3, 3, 2, 1 ] ry_isbf = [ 7, 8, 8, 7, 6, 6, 6, 6, 6, 7, 8, 8, 7, 7, 6, 5, 4, 3, 3, 2, 2, 1, 2, 2, 3, 3, 4, 5, 6, 7, 7 ] # refenece neighbors for moore rx_moore = [ 1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 8, 7, 7, 7, 7, 6, 5, 4, 3, 3, 3, 3, 2, 1 ] ry_moore = [ 7, 8, 8, 7, 6, 6, 6, 6, 6, 7, 8, 8, 7, 7, 6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6, 7, 7 ] output_isbf = trace_boundaries(m_neighbor) np.testing.assert_allclose(rx_isbf, output_isbf[0][1]) np.testing.assert_allclose(ry_isbf, output_isbf[0][0]) output_moore = trace_boundaries(m_neighbor, 8) np.testing.assert_allclose(rx_moore, output_moore[0][1]) np.testing.assert_allclose(ry_moore, output_moore[0][0])
def test_trace_boundary(self): # test moore neighbor algorithm m_neighbor = np.array( [ [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], [0, 1, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 1, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 1, 1, 1, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 1, 1, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], ], dtype=np.bool, ) m_neighbor = np.ascontiguousarray(m_neighbor, dtype=np.int) # refenece neighbors for isbf rx_isbf = [1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 8, 7, 7, 7, 7, 6, 6, 5, 5, 5, 4, 4, 3, 3, 3, 3, 2, 1] ry_isbf = [7, 8, 8, 7, 6, 6, 6, 6, 6, 7, 8, 8, 7, 7, 6, 5, 4, 3, 3, 2, 2, 1, 2, 2, 3, 3, 4, 5, 6, 7, 7] # refenece neighbors for moore rx_moore = [1, 1, 2, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 8, 7, 7, 7, 7, 6, 5, 4, 3, 3, 3, 3, 2, 1] ry_moore = [7, 8, 8, 7, 6, 6, 6, 6, 6, 7, 8, 8, 7, 7, 6, 5, 4, 3, 2, 1, 2, 3, 4, 5, 6, 7, 7] output_isbf = trace_boundaries(m_neighbor) np.testing.assert_allclose(rx_isbf, output_isbf[0][1]) np.testing.assert_allclose(ry_isbf, output_isbf[0][0]) output_moore = trace_boundaries(m_neighbor, 8) np.testing.assert_allclose(rx_moore, output_moore[0][1]) np.testing.assert_allclose(ry_moore, output_moore[0][0])
def split_concavities(Label, MinDepth=4, MinConcavity=np.inf): # noqa: C901 """Performs splitting of objects in a label image using geometric scoring of concavities. Attempts to perform splits at narrow regions that are perpendicular to the object's convex hull boundaries. Parameters: ----------- Label : array_like A uint32 label image. MinDepth : float Minimum depth of concavities to consider during geometric splitting. Default value = 2. MinConcavity : float Minimum concavity score to consider when performing for geometric splitting. Default value = np.inf. Notes: ------ Can produce a large number of thin "halo" objects surrouding the objects with higher scores. These can be removed by filtering object width in the resulting label image. Returns: -------- Label : array_like A uint32 label image. See Also: --------- label_contours, min_model References: ----------- .. [1] S. Weinert et al "Detection and Segmentation of Cell Nuclei in Virtual Microscopy Images: A Minimum-Model Approach" in Nature Scientific Reports,vol.2,no.503, doi:10.1038/srep00503, 2012. """ # use shape profiles to split objects with concavities # copy input label image Convex = Label.copy() # condense label image if np.unique(Convex).size-1 != Convex.max(): Convex = label.condense(Convex) # get locations of objects in initial image Locations = ms.find_objects(Convex) # initialize number of labeled objects and record initial count Total = Label.max() # initialize loop counter i = 1 while i <= Total: # get object window from label image if i < len(Locations): W = Convex[Locations[i-1]] else: Locations = ms.find_objects(Convex) W = Convex[Locations[i-1]] # embed masked object in padded boolean array Mask = np.zeros((W.shape[0]+2, W.shape[1]+2), dtype=np.bool) Mask[1:-1, 1:-1] = W == i # generate convex hull of object Hull = mo.convex_hull_image(Mask) # generate boundary coordinates, trim duplicate point X, Y = label.trace_boundaries(Mask, Connectivity=8) X = np.array(X[:-1], dtype=np.uint32) Y = np.array(Y[:-1], dtype=np.uint32) # calculate distance transform of object boundary pixels to convex hull Distance = mp.distance_transform_edt(Hull) D = Distance[Y, X] - 1 # generate linear index of positions Linear = np.arange(X.size) # rotate boundary counter-clockwise until start position is on hull while(D[0] != 0): X = np.roll(X, -1) Y = np.roll(Y, -1) D = np.roll(D, -1) Linear = np.roll(Linear, -1) # find runs of concave pixels with length > 1 Concave = (D > 0).astype(np.int) Start = np.where((Concave[1:] - Concave[0:-1]) == 1)[0] Stop = np.where((Concave[1:] - Concave[0:-1]) == -1)[0] + 1 if(Stop.size == Start.size - 1): Stop = np.append(Stop, 0) # extract depth profiles, indices, distances for each run iX = [] iY = [] Depths = [] Length = np.zeros((Start.size)) MaxDepth = np.zeros((Start.size)) for j in np.arange(Start.size): if(Start[j] < Stop[j]): iX.append(X[Start[j]:Stop[j]+1]) iY.append(Y[Start[j]:Stop[j]+1]) Depths.append(D[Start[j]:Stop[j]+1]) else: # run terminates at beginning of sequence iX.append(np.append(X[Start[j]:], X[0])) iY.append(np.append(Y[Start[j]:], Y[0])) Depths.append(np.append(D[Start[j]:], D[0])) Length[j] = iX[j].size MaxDepth[j] = np.max(Depths[j]) # filter based on concave contour length and max depth Keep = np.where((Length > 1) & (MaxDepth >= MinDepth))[0] Start = Start[Keep] Stop = Stop[Keep] iX = [iX[Ind].astype(dtype=np.float) for Ind in Keep] iY = [iY[Ind].astype(dtype=np.float) for Ind in Keep] Depths = [Depths[Ind] for Ind in Keep] # attempt cutting if more than 1 sequence is found if Start.size > 1: # initialize containers to hold cut scores, optimal cut locations Scores = np.inf * np.ones((Start.size, Start.size)) Xcut1 = np.zeros((Start.size, Start.size), dtype=np.uint32) Ycut1 = np.zeros((Start.size, Start.size), dtype=np.uint32) Xcut2 = np.zeros((Start.size, Start.size), dtype=np.uint32) Ycut2 = np.zeros((Start.size, Start.size), dtype=np.uint32) # compare candidates pairwise between all runs and score for j in np.arange(Start.size): # get list of 'j' candidates that pass depth threshold jCandidates = np.where(Depths[j] >= MinDepth)[0] for k in np.arange(j+1, Start.size): # get list of 'k' candidates that pass depth threshold kCandidates = np.where(Depths[k] >= MinDepth)[0] # initialize minimum score and cut locations minScore = np.inf minj = -1 mink = -1 # loop over each coordinate pair for concavities j,k for a in np.arange(jCandidates.size): for b in np.arange(kCandidates.size): # calculate length score Ls = length_score(iX[j][jCandidates[a]], iY[j][jCandidates[a]], iX[k][kCandidates[b]], iY[k][kCandidates[b]], Depths[j][jCandidates[a]], Depths[k][kCandidates[b]]) # calculate angle score As = angle_score(iX[j][0], iY[j][0], iX[j][-1], iY[j][-1], iX[k][0], iY[k][0], iX[k][-1], iY[k][-1], iX[j][jCandidates[a]], iY[j][jCandidates[a]], iX[k][kCandidates[b]], iY[k][kCandidates[b]]) # combine scores Score = (Ls + As) / 2 # replace if improvement if Score < minScore: minScore = Score Scores[j, k] = minScore minj = jCandidates[a] mink = kCandidates[b] # record best cut location Xcut1[j, k] = iX[j][minj] Ycut1[j, k] = iY[j][minj] Xcut2[j, k] = iX[k][mink] Ycut2[j, k] = iY[k][mink] # pick the best scoring candidates and cut if needed ArgMin = np.unravel_index(Scores.argmin(), Scores.shape) if Scores[ArgMin[0], ArgMin[1]] <= MinConcavity: # perform cut SplitMask = cut(Mask, Xcut1[ArgMin[0], ArgMin[1]].astype(np.float), Ycut1[ArgMin[0], ArgMin[1]].astype(np.float), Xcut2[ArgMin[0], ArgMin[1]].astype(np.float), Ycut2[ArgMin[0], ArgMin[1]].astype(np.float)) # re-label cut image SplitLabel = ms.label(SplitMask)[0] # increment object count, and label new object at end SplitLabel[SplitLabel > 1] = Total + 1 Total += 1 # label object '1' with current object value SplitLabel[SplitLabel == 1] = i # trim padding from corrected label image Mask = Mask[1:-1, 1:-1] SplitLabel = SplitLabel[1:-1, 1:-1] # update label image W[Mask] = SplitLabel[Mask] else: # no cut made, move to next object i = i + 1 else: # no cuts to attempt, move to next object i = i + 1 return Convex
def trace_contours(I, X, Y, Min, Max, MaxLength=255): """Performs contour tracing of seed pixels in an intensity image using gradient information. Parameters: ----------- I : array_like An intensity image used for analyzing local minima/maxima and gradients. Dimensions M x N. X : array_like A 1D array of horizontal coordinates of contour seed pixels for tracing. Y : array_like A 1D array of the vertical coordinates of seed pixels for tracing. Min : array_like A 1D array of the corresponding minimum values for contour tracing of seed point X, Y. Max : array_like A 1D array of the corresponding maximum values for contour tracing of seed point X, Y. MaxLength : int Maximum allowable contour length. Default value = 255. Notes: ------ Can be computationally expensive for large numbers of contours. Use smoothing and delta thresholding when seeding contours to reduce burden. Returns: -------- cXs : list A list of 1D numpy arrays defining the horizontal coordinates of object boundaries. cYs : list A list of 1D numpy arrays defining the vertical coordinates of object boundaries. See Also: --------- SeedContours, ScoreContours, MinimumModel References: ----------- .. [1] S. Weinert et al "Detection and Segmentation of Cell Nuclei in Virtual Microscopy Images: A Minimum-Model Approach" in Nature Scientific Reports,vol.2,no.503, doi:10.1038/srep00503, 2012. """ # initialize list of lists containing contours cXs = [] cYs = [] # process each seed pixel sequentially for i in np.arange(X.size): # capture window surrounding (X[i], Y[i]) W = I[max(0, Y[i]-np.ceil(MaxLength/2.0)): min(I.shape[0]+1, Y[i]+np.ceil(MaxLength/2.0)+1), max(0, X[i]-np.ceil(MaxLength/2.0)): min(I.shape[1]+1, X[i]+np.ceil(MaxLength/2.0))+1] # binary threshold corresponding to seed pixel 'i' W = (W <= Max[i]) & (W >= Min[i]) # embed with center pixel in middle of padded window Embed = np.zeros((W.shape[0]+2, W.shape[1]+2), dtype=np.bool) Embed[1:-1, 1:-1] = W # calculate location of (X[i], Y[i]) in 'Embed' pX = X[i] - max(0, X[i]-np.ceil(MaxLength/2.0)) + 1 pY = Y[i] - max(0, Y[i]-np.ceil(MaxLength/2.0)) + 1 # trace boundary, check stopping condition, append to list of contours cX, cY = label.trace_boundaries(Embed, Connectivity=4, XStart=pX, YStart=pY, MaxLength=MaxLength) if(cX[0] == cX[-1] and cY[0] == cY[-1] and len(cX) <= MaxLength): # add window offset to contour coordinates cX = [x + max(0, X[i]-np.ceil(MaxLength/2.0)) - 1 for x in cX] cY = [y + max(0, Y[i]-np.ceil(MaxLength/2.0)) - 1 for y in cY] # append to list of candidate contours cXs.append(np.array(cX, dtype=np.uint32)) cYs.append(np.array(cY, dtype=np.uint32)) return cXs, cYs
def split_concavities(Label, MinDepth=4, MinConcavity=np.inf): # noqa: C901 """Performs splitting of objects in a label image using geometric scoring of concavities. Attempts to perform splits at narrow regions that are perpendicular to the object's convex hull boundaries. Parameters ---------- Label : array_like A uint32 label image. MinDepth : float Minimum depth of concavities to consider during geometric splitting. Default value = 2. MinConcavity : float Minimum concavity score to consider when performing for geometric splitting. Default value = np.inf. Notes ----- Can produce a large number of thin "halo" objects surrouding the objects with higher scores. These can be removed by filtering object width in the resulting label image. Returns ------- Label : array_like A uint32 label image. See Also -------- label_contours, min_model References ---------- .. [1] S. Weinert et al "Detection and Segmentation of Cell Nuclei in Virtual Microscopy Images: A Minimum-Model Approach" in Nature Scientific Reports,vol.2,no.503, doi:10.1038/srep00503, 2012. """ # use shape profiles to split objects with concavities # copy input label image Convex = Label.copy() # condense label image if np.unique(Convex).size - 1 != Convex.max(): Convex = label.condense(Convex) # get locations of objects in initial image Locations = ms.find_objects(Convex) # initialize number of labeled objects and record initial count Total = Label.max() # initialize loop counter i = 1 while i <= Total: # get object window from label image if i < len(Locations): W = Convex[Locations[i - 1]] else: Locations = ms.find_objects(Convex) W = Convex[Locations[i - 1]] # embed masked object in padded boolean array Mask = np.zeros((W.shape[0] + 2, W.shape[1] + 2), dtype=np.bool) Mask[1:-1, 1:-1] = W == i # generate convex hull of object Hull = mo.convex_hull_image(Mask) # generate boundary coordinates, trim duplicate point X, Y = label.trace_boundaries(Mask, conn=8) X = np.array(X[:-1], dtype=np.uint32) Y = np.array(Y[:-1], dtype=np.uint32) # calculate distance transform of object boundary pixels to convex hull Distance = mp.distance_transform_edt(Hull) D = Distance[Y, X] - 1 # generate linear index of positions Linear = np.arange(X.size) # rotate boundary counter-clockwise until start position is on hull while (D[0] != 0): X = np.roll(X, -1) Y = np.roll(Y, -1) D = np.roll(D, -1) Linear = np.roll(Linear, -1) # find runs of concave pixels with length > 1 Concave = (D > 0).astype(np.int) Start = np.where((Concave[1:] - Concave[0:-1]) == 1)[0] Stop = np.where((Concave[1:] - Concave[0:-1]) == -1)[0] + 1 if (Stop.size == Start.size - 1): Stop = np.append(Stop, 0) # extract depth profiles, indices, distances for each run iX = [] iY = [] Depths = [] Length = np.zeros((Start.size)) MaxDepth = np.zeros((Start.size)) for j in np.arange(Start.size): if (Start[j] < Stop[j]): iX.append(X[Start[j]:Stop[j] + 1]) iY.append(Y[Start[j]:Stop[j] + 1]) Depths.append(D[Start[j]:Stop[j] + 1]) else: # run terminates at beginning of sequence iX.append(np.append(X[Start[j]:], X[0])) iY.append(np.append(Y[Start[j]:], Y[0])) Depths.append(np.append(D[Start[j]:], D[0])) Length[j] = iX[j].size MaxDepth[j] = np.max(Depths[j]) # filter based on concave contour length and max depth Keep = np.where((Length > 1) & (MaxDepth >= MinDepth))[0] Start = Start[Keep] Stop = Stop[Keep] iX = [iX[Ind].astype(dtype=np.float) for Ind in Keep] iY = [iY[Ind].astype(dtype=np.float) for Ind in Keep] Depths = [Depths[Ind] for Ind in Keep] # attempt cutting if more than 1 sequence is found if Start.size > 1: # initialize containers to hold cut scores, optimal cut locations Scores = np.inf * np.ones((Start.size, Start.size)) Xcut1 = np.zeros((Start.size, Start.size), dtype=np.uint32) Ycut1 = np.zeros((Start.size, Start.size), dtype=np.uint32) Xcut2 = np.zeros((Start.size, Start.size), dtype=np.uint32) Ycut2 = np.zeros((Start.size, Start.size), dtype=np.uint32) # compare candidates pairwise between all runs and score for j in np.arange(Start.size): # get list of 'j' candidates that pass depth threshold jCandidates = np.where(Depths[j] >= MinDepth)[0] for k in np.arange(j + 1, Start.size): # get list of 'k' candidates that pass depth threshold kCandidates = np.where(Depths[k] >= MinDepth)[0] # initialize minimum score and cut locations minScore = np.inf minj = -1 mink = -1 # loop over each coordinate pair for concavities j,k for a in np.arange(jCandidates.size): for b in np.arange(kCandidates.size): # calculate length score Ls = length_score(iX[j][jCandidates[a]], iY[j][jCandidates[a]], iX[k][kCandidates[b]], iY[k][kCandidates[b]], Depths[j][jCandidates[a]], Depths[k][kCandidates[b]]) # calculate angle score As = angle_score( iX[j][0], iY[j][0], iX[j][-1], iY[j][-1], iX[k][0], iY[k][0], iX[k][-1], iY[k][-1], iX[j][jCandidates[a]], iY[j][jCandidates[a]], iX[k][kCandidates[b]], iY[k][kCandidates[b]]) # combine scores Score = (Ls + As) / 2 # replace if improvement if Score < minScore: minScore = Score Scores[j, k] = minScore minj = jCandidates[a] mink = kCandidates[b] # record best cut location Xcut1[j, k] = iX[j][minj] Ycut1[j, k] = iY[j][minj] Xcut2[j, k] = iX[k][mink] Ycut2[j, k] = iY[k][mink] # pick the best scoring candidates and cut if needed ArgMin = np.unravel_index(Scores.argmin(), Scores.shape) if Scores[ArgMin[0], ArgMin[1]] <= MinConcavity: # perform cut SplitMask = cut(Mask, Xcut1[ArgMin[0], ArgMin[1]].astype(np.float), Ycut1[ArgMin[0], ArgMin[1]].astype(np.float), Xcut2[ArgMin[0], ArgMin[1]].astype(np.float), Ycut2[ArgMin[0], ArgMin[1]].astype(np.float)) # re-label cut image SplitLabel = ms.label(SplitMask)[0] # increment object count, and label new object at end SplitLabel[SplitLabel > 1] = Total + 1 Total += 1 # label object '1' with current object value SplitLabel[SplitLabel == 1] = i # trim padding from corrected label image Mask = Mask[1:-1, 1:-1] SplitLabel = SplitLabel[1:-1, 1:-1] # update label image W[Mask] = SplitLabel[Mask] else: # no cut made, move to next object i = i + 1 else: # no cuts to attempt, move to next object i = i + 1 return Convex
def trace_contours(I, X, Y, Min, Max, MaxLength=255): """Performs contour tracing of seed pixels in an intensity image using gradient information. Parameters ---------- I : array_like An intensity image used for analyzing local minima/maxima and gradients. Dimensions M x N. X : array_like A 1D array of horizontal coordinates of contour seed pixels for tracing. Y : array_like A 1D array of the vertical coordinates of seed pixels for tracing. Min : array_like A 1D array of the corresponding minimum values for contour tracing of seed point X, Y. Max : array_like A 1D array of the corresponding maximum values for contour tracing of seed point X, Y. MaxLength : int Maximum allowable contour length. Default value = 255. Notes ----- Can be computationally expensive for large numbers of contours. Use smoothing and delta thresholding when seeding contours to reduce burden. Returns ------- cXs : list A list of 1D numpy arrays defining the horizontal coordinates of object boundaries. cYs : list A list of 1D numpy arrays defining the vertical coordinates of object boundaries. See Also -------- SeedContours, ScoreContours, MinimumModel References ---------- .. [1] S. Weinert et al "Detection and Segmentation of Cell Nuclei in Virtual Microscopy Images: A Minimum-Model Approach" in Nature Scientific Reports,vol.2,no.503, doi:10.1038/srep00503, 2012. """ # initialize list of lists containing contours cXs = [] cYs = [] # process each seed pixel sequentially for i in np.arange(X.size): # capture window surrounding (X[i], Y[i]) W = I[max(0, Y[i] - np.ceil(MaxLength / 2.0)):min(I.shape[0] + 1, Y[i] + np.ceil(MaxLength / 2.0) + 1), max(0, X[i] - np.ceil(MaxLength / 2.0) ):min(I.shape[1] + 1, X[i] + np.ceil(MaxLength / 2.0)) + 1] # binary threshold corresponding to seed pixel 'i' W = (W <= Max[i]) & (W >= Min[i]) # embed with center pixel in middle of padded window Embed = np.zeros((W.shape[0] + 2, W.shape[1] + 2), dtype=np.bool) Embed[1:-1, 1:-1] = W # calculate location of (X[i], Y[i]) in 'Embed' pX = X[i] - max(0, X[i] - np.ceil(MaxLength / 2.0)) + 1 pY = Y[i] - max(0, Y[i] - np.ceil(MaxLength / 2.0)) + 1 # trace boundary, check stopping condition, append to list of contours cX, cY = label.trace_boundaries(Embed, conn=4, x_start=pX, y_start=pY, MaxLength=MaxLength) if (cX[0] == cX[-1] and cY[0] == cY[-1] and len(cX) <= MaxLength): # add window offset to contour coordinates cX = [x + max(0, X[i] - np.ceil(MaxLength / 2.0)) - 1 for x in cX] cY = [y + max(0, Y[i] - np.ceil(MaxLength / 2.0)) - 1 for y in cY] # append to list of candidate contours cXs.append(np.array(cX, dtype=np.uint32)) cYs.append(np.array(cY, dtype=np.uint32)) return cXs, cYs