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)
Exemple #3
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
Exemple #4
0
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
Exemple #5
0
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
Exemple #6
0
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
Exemple #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