コード例 #1
0
ファイル: mask_to_graph.py プロジェクト: jonschwenk/RivGraph
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
コード例 #2
0
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
コード例 #3
0
ファイル: test_im_utils.py プロジェクト: vojta-curin/RivGraph
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
コード例 #4
0
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
コード例 #5
0
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
コード例 #6
0
ファイル: mask_to_graph.py プロジェクト: jonschwenk/RivGraph
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
コード例 #7
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