def skeletonize_mask(Imask): """ Skeletonizes an input binary image, typically a mask. Also performs some skeleton simplification by (1) removing pixels that don't alter connectivity, and (2) filling small skeleton holes and reskeletonizing. Parameters ---------- Imask : np.array Binary image to be skeletonized. Returns ------- Iskel : np.array The skeletonization of Imask. """ # Create copy of mask to skeletonize Iskel = np.array(Imask, copy=True, dtype='bool') # Perform skeletonization Iskel = morphology.skeletonize(Iskel) # 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) return Iskel
def skeletonize_river_mask(I, es, npad=20): """ Skeletonize a binary river mask. Crops mask to river extents, reflects the mask at the appropriate borders, skeletonizes, then un-crops the mask. INPUTS: I - binary river mask to skeletonize es - NSEW "exit sides" corresponding to the upstream and downstream sides of the image that intersect the river. e.g. 'NS', 'EN', 'WS' npad - (Optional) number of pixels to reflect I before skeletonization to remove edge effects of skeletonization """ Ic, crop_pads = iu.crop_binary_im(I) # Number of pixels to mirror - maximum of 2% of each dimension or npad pixels n_vert = max(int(I.shape[0] * 0.02 / 2), npad) n_horiz = max(int(I.shape[1] * 0.02 / 2), npad) pads = [[0, 0], [0, 0]] if 'N' in es: pads[0][0] = n_vert if 'E' in es: pads[1][1] = n_horiz if 'S' in es: pads[0][1] = n_vert if 'W' in es: pads[1][0] = n_horiz pads = tuple([tuple(pads[0]), tuple(pads[1])]) # Generate padded image Ip = np.pad(Ic, pads, mode='reflect') # Skeletonize padded image Iskel = morphology.skeletonize(Ip) # Remove padding Iskel = Iskel[pads[0][0]:Iskel.shape[0] - pads[0][1], pads[1][0]:Iskel.shape[1] - pads[1][1]] # 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 = skel_simplify(Iskel) # Fill small skeleton holes, re-skeletonize, and re-simplify Iskel = iu.fill_holes(Iskel, maxholesize=4) Iskel = morphology.skeletonize(Iskel) Iskel = skel_simplify(Iskel) # Fill single pixel holes Iskel = iu.fill_holes(Iskel, maxholesize=1) return Iskel
def test_fill_holes(): """Test fill_holes().""" I = np.ones((3, 3)) I[1, 1] = 0. I_filled = im_utils.fill_holes(I, maxholesize=0) # assertion that hole was filled assert I_filled[1, 1] == 1
def skeletonize_mask(Imask, skelpath=None): """ Skeletonize any input binary mask. """ # Create copy of mask to skeletonize Iskel = np.array(Imask, copy=True, dtype='bool') # Perform skeletonization Iskel = morphology.skeletonize(Iskel) # Simplify the skeleton (i.e. remove pixels that don't alter connectivity) Iskel = skel_simplify(Iskel) # Fill small skeleton holes, re-skeletonize, and re-simplify Iskel = iu.fill_holes(Iskel, maxholesize=4) Iskel = morphology.skeletonize(Iskel) Iskel = skel_simplify(Iskel) # Fill single pixel holes Iskel = iu.fill_holes(Iskel, maxholesize=1) return Iskel
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 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