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_crop_binary_im(): """Test crop_binary_im().""" I = np.zeros((5, 5)) I[0, 0] = 1 I[1, 2:5] = 1 Icrop, pads = im_utils.crop_binary_im(I) # assertions assert np.shape(Icrop) == (2, 5) assert np.all(pads == [0, 0, 0, 3])
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