def test_largest_blob_removal(self): """Test 2.""" I = np.zeros((5, 5)) I[1:4, 1:4] = 1 Ic = im_utils.largest_blobs(I, nlargest=1, action='remove') # make assertion assert np.all(Ic) == 0
def test_largest_blob_error(self): """Test 3.""" I = np.zeros((5, 5)) I[1:4, 1:4] = 1 Ic = im_utils.largest_blobs(I, nlargest=1, action='invalid') # make assertion assert np.all(Ic == I)
def max_valley_width(Imask): """ Computes the maximum valley width of the input mask. Finds the single largest blob in the mask, fills its holes, then uses the distance transform to find the largest width. Parameters ---------- Imask : np.array Binary mask from which the centerline was computed. Returns ------- max_valley_width : float Maximum width of the channel belt, useful for computing a mesh. Units are pixels, so be careful to re-convert. """ Imask = iu.largest_blobs(Imask, nlargest=1, action='keep') Imask = iu.fill_holes(Imask) Idist = distance_transform_edt(Imask) max_valley_width = np.max(Idist) * 2 return max_valley_width
def skeletonize_river_mask(I, es, padscale=2): """ Skeletonizes a binary mask of a river channel network. Differs from skeletonize mask above by using knowledge of the exit sides of the river with respect to the mask (I) to avoid edge effects of skeletonization by mirroring the mask at its ends, then trimming it after processing. As with skeletonize_mask, skeleton simplification is performed. Parameters ---------- I : np.array Binary river mask to skeletonize. es : str A two-character string (from N, E, S, or W) that denotes which sides of the image the river intersects (upstream first) -- e.g. 'NS', 'EW', 'NW', etc. padscale : int, optional Pad multiplier that sets the size of the padding. Multplies the blob size along the axis of the image that the blob intersect to determine the padding distance. The default is 2. Returns ------- Iskel : np.array The skeletonization of I. """ # Crop image Ic, crop_pads = imu.crop_binary_im(I) # Pad image (reflects channels at image edges) Ip, pads = pad_river_im(Ic, es, pm=padscale) # Skeletonize padded image Iskel = morphology.skeletonize(Ip) # Remove padding Iskel = Iskel[pads[0]:Iskel.shape[0] - pads[1], pads[3]:Iskel.shape[1] - pads[2]] # Add back what was cropped so skeleton image is original size crop_pads_add = ((crop_pads[1], crop_pads[3]), (crop_pads[0], crop_pads[2])) Iskel = np.pad(Iskel, crop_pads_add, mode='constant', constant_values=(0, 0)) # Ensure skeleton is prepared for analysis by RivGraph # Simplify the skeleton (i.e. remove pixels that don't alter connectivity) Iskel = simplify_skel(Iskel) # Fill small skeleton holes, re-skeletonize, and re-simplify Iskel = imu.fill_holes(Iskel, maxholesize=4) Iskel = morphology.skeletonize(Iskel) Iskel = simplify_skel(Iskel) # Fill single pixel holes Iskel = imu.fill_holes(Iskel, maxholesize=1) # The handling of edges can leave pieces of the skeleton stranded (i.e. # disconnected from the main skeleton). Remove those here by keeping the # largest blob. Iskel = imu.largest_blobs(Iskel, nlargest=1, action='keep') return Iskel
def is_bp(idx, Iskel): """ Determine if an index is a branchpoint. Determines if the index given by idx is a branchpoint. Branchpoints are not simply pixels in the skeleton with more than two neighbors; they are pruned through a somewhat complicated procedure that minimizes the number of required branchpoints to preserve the skeleton topology. Parameters ---------- idx : np.int Index within Iskel to determine if it is a branchpoint. Iskel : np.ndarray Image of the skeletonized mask, but can be any image array. Returns ------- isbp : int 1 if idx is a branchpoint, else 0. """ # TODO: change to return True/False rather than 1/0. # Trivial case, only one or two neighbors is not bp neighs = get_neighbors(idx, Iskel) if len(neighs) < 3: return 0 # Pull out the neighborhood big_enough = 0 size = (7, 7) # Loop to ensure the domain is large enough to capture all connected # nconn>2 pixels while big_enough == 0: centidx = (int((size[0] - 1) / 2), int((size[1] - 1) / 2)) I, roffset, coffset = iu.get_array(idx, Iskel, size) # Find 4-connected pixels with connectivity > 2 Ic = iu.im_connectivity(I) Ict = np.zeros_like(I) Ict[Ic > 2] = 1 Ilab = measure.label(Ict, background=0, connectivity=1) cpy, cpx = np.where(Ilab == Ilab[centidx]) big_enough = 1 if 1 in cpx or size[0] - 2 in cpx: size = (size[0] + 4, size[1]) big_enough = 0 if 1 in cpy or size[1] - 2 in cpy: size = (size[0], size[1] + 4) big_enough = 0 # Reduce image to subset of connected conn > 2 pixels with a 1 pixel # buffer by zeroing out values outside the domain I[:np.min(cpy) - 1, :] = 0 I[np.max(cpy) + 2:, :] = 0 I[:, :np.min(cpx) - 1] = 0 I[:, np.max(cpx) + 2:] = 0 # Take only the largest blob in case there are border stragglers I = iu.largest_blobs(I, 1, 'keep') # Zero out everything outside our region of interest Ic[np.bitwise_and( Ilab != Ilab[centidx], Ic > 2)] = 1 # set edge pixel connectivity to 1 (even if not true) Ic[I != 1] = 0 # Trivial case where idx is the only possible branchpoint if np.sum(Ic > 2) == 1: return 1 # Compute number of axes and four-connectivity Ina = naxes_connectivity(I) Inf = iu.nfour_connectivity(I) # Ravel everything Icr = np.ravel(Ic) Inar = np.ravel(Ina) Infr = np.ravel(Inf) bps = isbp_parsimonious(Ic, Icr, Inar, Infr) # Return branchpoints to global, flat coordinates bps = iu.reglobalize_flat_idx(bps, Ic.shape, roffset, coffset, Iskel.shape) # Check input idx for being a branchpoint if idx in bps: isbp = 1 else: isbp = 0 return isbp
def is_bp(idx, Iskel): """ Returns 1 if a pixel is a branchpoint in a skeleton given by vrtpath; else 0 """ # Trivial case, only one or two neighbors is not bp neighs = get_neighbors(idx, Iskel) if len(neighs) < 3: return 0 # Pull out the neighborhood big_enough = 0 size = (7, 7) # Loop to ensure our size is large enough to capture all connected nconn>2 pixels while big_enough == 0: centidx = (int((size[0] - 1) / 2), int((size[1] - 1) / 2)) I, roffset, coffset = iu.get_array(idx, Iskel, size) # Find 4-connected pixels with connectivity > 2 Ic = iu.im_connectivity(I) Ict = np.zeros_like(I) Ict[Ic > 2] = 1 Ilab = measure.label(Ict, background=0, connectivity=1) cpy, cpx = np.where(Ilab == Ilab[centidx]) big_enough = 1 if 1 in cpx or size[0] - 2 in cpx: size = (size[0] + 4, size[1]) big_enough = 0 if 1 in cpy or size[1] - 2 in cpy: size = (size[0], size[1] + 4) big_enough = 0 # Reduce image to subset of connected conn > 2 pixels with a 1 pixel buffer by zeroing out values outside the domain I[:np.min(cpy) - 1, :] = 0 I[np.max(cpy) + 2:, :] = 0 I[:, :np.min(cpx) - 1] = 0 I[:, np.max(cpx) + 2:] = 0 # Take only the largest blob in case there are border stragglers I = iu.largest_blobs(I, 1, 'keep') # Zero out everything outside our region of interest Ic[np.bitwise_and( Ilab != Ilab[centidx], Ic > 2)] = 1 # set edge pixel connectivity to 1 (even if not true) Ic[I != 1] = 0 # Trivial case where idx is the only possible branchpoint if np.sum(Ic > 2) == 1: return 1 # Compute number of axes and four-connectivity Ina = naxes_connectivity(I) Inf = iu.nfour_connectivity(I) # Ravel everything Icr = np.ravel(Ic) Inar = np.ravel(Ina) Infr = np.ravel(Inf) bps = isbp_parsimonious(Ic, Icr, Inar, Infr) # Return branchpoints to global, flat coordinates bps = iu.reglobalize_flat_idx(bps, Ic.shape, roffset, coffset, Iskel.shape) # Check input idx for being a branchpoint if idx in bps: return 1 else: return 0
def mask_to_centerline(Imask, es): """ Extract centerline from a river mask. This function takes an input binary mask of a river and extracts its centerline. If there are multiple channels (and therefore islands) in the river, they will be filled before the centerline is computed. .. note:: The input mask should have the following properties: 1) There should be only one "blob" (connected component) 2) Where the blob intersects the image edges, there should be only one channel. This avoids ambiguity in identifying inlet/outlet links Parameters ---------- Imask : ndarray the mask image (numpy array) es : str two-character string comprinsed of "n", "e", "s", or "w". Exit sides correspond to the sides of the image that the river intersects. Upstream should be first, followed by downstream. Returns ------- dt.tif : geotiff geotiff of the distance transform of the binary mask skel.tif : geotiff geotiff of the skeletonized binary mask centerline.shp : shp shapefile of the centerline, arranged upstream to downstream cl.pkl : pkl pickle file containing centerline coords, EPSG, and paths dictionary """ # Lowercase the exit sides es = es.lower() # Keep only largest connected blob I = iu.largest_blobs(Imask, nlargest=1, action='keep') # Fill holes in mask Ihf = iu.fill_holes(I) # Skeletonize holes-filled river image Ihf_skel = m2g.skeletonize_river_mask(Ihf, es) # In some cases, skeleton spurs can prevent the creation of an endpoint # at the edge of the image. This next block of code tries to condition # the skeleton to prevent this from happening. # Find skeleton border pixels skel_rows, skel_cols = np.where(Ihf_skel) idcs_top = np.where(skel_rows == 0) idcs_bottom = np.where(skel_rows == Ihf_skel.shape[0] - 1) idcs_right = np.where(skel_cols == Ihf_skel.shape[1] - 1) idcs_left = np.where(skel_cols == 0) # Remove skeleton border pixels Ihf_skel[skel_rows[idcs_top], skel_cols[idcs_top]] = 0 Ihf_skel[skel_rows[idcs_bottom], skel_cols[idcs_bottom]] = 0 Ihf_skel[skel_rows[idcs_right], skel_cols[idcs_right]] = 0 Ihf_skel[skel_rows[idcs_left], skel_cols[idcs_left]] = 0 # Remove all pixels now disconnected from the main skeleton Ihf_skel = iu.largest_blobs(Ihf_skel, nlargest=1, action='keep') # Add the border pixels back Ihf_skel[skel_rows[idcs_top], skel_cols[idcs_top]] = 1 Ihf_skel[skel_rows[idcs_bottom], skel_cols[idcs_bottom]] = 1 Ihf_skel[skel_rows[idcs_right], skel_cols[idcs_right]] = 1 Ihf_skel[skel_rows[idcs_left], skel_cols[idcs_left]] = 1 # Keep only the largest connected skeleton Ihf_skel = iu.largest_blobs(Ihf_skel, nlargest=1, action='keep') # Convert skeleton to graph hf_links, hf_nodes = m2g.skel_to_graph(Ihf_skel) # Compute holes-filled distance transform Ihf_dist = distance_transform_edt(Ihf) # distance transform # Append link widths and lengths hf_links = lnu.link_widths_and_lengths(hf_links, Ihf_dist) """ Find shortest path between inlet/outlet centerline nodes""" # Put skeleton into networkX graph object G = nx.Graph() G.add_nodes_from(hf_nodes['id']) for lc, wt in zip(hf_links['conn'], hf_links['len']): G.add_edge(lc[0], lc[1], weight=wt) # Get endpoints of graph endpoints = [ nid for nid, nconn in zip(hf_nodes['id'], hf_nodes['conn']) if len(nconn) == 1 ] # Filter endpoints if we have too many--shortest path compute time scales as a power of len(endpoints) while len(endpoints) > 100: ep_r, ep_c = np.unravel_index( [hf_nodes['idx'][hf_nodes['id'].index(ep)] for ep in endpoints], Ihf_skel.shape) pct = 10 ep_keep = set() for esi in [0, 1]: if es[esi] == 'n': n_pct = int(np.percentile(ep_r, pct)) ep_keep.update(np.where(ep_r <= n_pct)[0]) elif es[esi] == 's': s_pct = int(np.percentile(ep_r, 100 - pct)) ep_keep.update(np.where(ep_r >= s_pct)[0]) elif es[esi] == 'e': e_pct = int(np.percentile(ep_c, 100 - pct)) ep_keep.update(np.where(ep_c > e_pct)[0]) elif es[esi] == 'w': w_pct = int(np.percentile(ep_c, pct)) ep_keep.update(np.where(ep_c < w_pct)[0]) endpoints = [endpoints[ek] for ek in ep_keep] # Get all paths from inlet(s) to outlets longest_shortest_paths = [] for inl in endpoints: temp_lens = [] for o in endpoints: temp_lens.append( nx.dijkstra_path_length(G, inl, o, weight='weight')) longest_shortest_paths.append(max(temp_lens)) # The two end nodes with the longest shortest path are the centerline's # endnodes end_nodes_idx = np.where( np.isclose(np.max(longest_shortest_paths), longest_shortest_paths))[0] end_nodes = [endpoints[i] for i in end_nodes_idx] # It is possible that more than two endnodes were identified; in these # cases, choose the nodes that are farthest apart in Euclidean space en_r, en_c = np.unravel_index( [hf_nodes['idx'][hf_nodes['id'].index(en)] for en in end_nodes], Ihf_skel.shape) ep_coords = np.r_['1,2,0', en_r, en_c] ep_dists = cdist(ep_coords, ep_coords, 'euclidean') en_idcs_to_use = np.unravel_index(np.argmax(ep_dists), ep_dists.shape) end_nodes = [end_nodes[eitu] for eitu in en_idcs_to_use] # Ensure that exactly two end nodes are identified if len(end_nodes) != 2: raise RuntimeError( '{} endpoints were found for the centerline. (Need exactly two).'. format(len(end_nodes))) # Find upstream node en_r, en_c = np.unravel_index( [hf_nodes['idx'][hf_nodes['id'].index(n)] for n in end_nodes], Ihf_skel.shape) # Compute error for each end node given the exit sides errors = [] for orientation in [0, 1]: if orientation == 0: er = en_r ec = en_c elif orientation == 1: er = en_r[::-1] ec = en_c[::-1] err = 0 for ot in [0, 1]: if es[ot].lower() == 'n': err = err + er[ot] elif es[ot].lower() == 's': err = err + Ihf_dist.shape[0] - er[ot] elif es[ot].lower() == 'w': err = err + ec[ot] elif es[ot].lower() == 'e': err = err + Ihf_dist.shape[1] - ec[ot] errors.append(err) # Flip end node orientation to get US->DS arrangement if errors[0] > errors[1]: end_nodes = end_nodes[::-1] # Create centerline from links along shortest path nodespath = nx.dijkstra_path(G, end_nodes[0], end_nodes[1]) # nodes shortest path # Find the links along the shortest node path cl_link_ids = [] for u, v in zip(nodespath[0:-1], nodespath[1:]): ulinks = hf_nodes['conn'][hf_nodes['id'].index(u)] vlinks = hf_nodes['conn'][hf_nodes['id'].index(v)] cl_link_ids.append([ul for ul in ulinks if ul in vlinks][0]) # Create a shortest-path links dict cl_links = dict.fromkeys(hf_links.keys()) dokeys = list(hf_links.keys()) dokeys.remove('n_networks') # Don't need n_networks for clid in cl_link_ids: for k in dokeys: if cl_links[k] is None: cl_links[k] = [] cl_links[k].append(hf_links[k][hf_links['id'].index(clid)]) # Save centerline as shapefile # lnu.links_to_shapefile(cl_links, igd, rmh.get_EPSG(paths['skel']), paths['cl_temp_shp']) # Get and save coordinates of centerline cl = [] for ic, cll in enumerate(cl_link_ids): if ic == 0: if hf_links['idx'][hf_links['id'].index( cll)][0] != hf_nodes['idx'][hf_nodes['id'].index( end_nodes[0])]: hf_links['idx'][hf_links['id'].index(cll)] = hf_links['idx'][ hf_links['id'].index(cll)][::-1] else: if hf_links['idx'][hf_links['id'].index(cll)][0] != cl[-1]: hf_links['idx'][hf_links['id'].index(cll)] = hf_links['idx'][ hf_links['id'].index(cll)][::-1] cl.extend(hf_links['idx'][hf_links['id'].index(cll)][:]) # Uniquify points, preserving order cl = list(OrderedSet(cl)) # Convert back to coordinates cly, clx = np.unravel_index(cl, Ihf_skel.shape) # Get width at each pixel of centerline pix_width = [Ihf_dist[y, x] * 2 for x, y in zip(clx, cly)] coords = np.transpose(np.vstack((clx, cly))) return coords, pix_width