Exemplo n.º 1
0
def skel_to_graph(Iskel):
    """
    Resolves a skeleton into its consitutent links and nodes.
    This function finds a starting point to walk along a skeleton, then begins
    the walk. Rules are in place to ensure the network is fully resolved. One
    of the key algorithms called by this function involves the identfication of
    branchpoints in a way that eliminates unnecessary ones to create a parsimonious
    network. Rules are baked in for how to walk along the skeleton in cases
    where multiple branchpoints are clustered or there are multiple possible
    links to walk along.

    Note that some minor adjustments to the skeleton may be made in order to
    reduce the complexity of the network. For example, in the case of a "+"
    with a missing center pixel in the skeleton, this function will add the
    pixel to the center to enable the use of a single branchpoint as opposed to
    four.

    The takeaway is that there is no guarantee that the input skeleton will
    be perfectly preserved when network-ifying. One possible workaround, if
    perfect preservation is required, is to resample the skeleton to double the
    resolution.

    Parameters
    ----------
    Iskel : np.ndarray
        Binary image of a skeleton.

    Returns
    -------
    links : dict
        Links of the network with four properties:

        1. 'id' - a list of unique ids for each link in the network

        2. 'idx' - a list containing the pixel indices within Iskel that defines the link. These are ordered.

        3. 'conn' - a list of 2-element lists containing the node ids that the link is connected to.

        4. 'n_networks' - the number of disconnected networks found in the skeleton

    nodes : dict
        Nodes of the network with four properties:

        1. 'id' - a list of unique ids for each node in the network

        2. 'idx' - the index within Iskel of the node location

        3. 'conn' - a list of lists containing the link ids connected to this node

    """
    def check_startpoint(spidx, Iskel):
        """
        Returns True if a skeleton pixel's first neighbor is not a branchpoint
        (i.e. the start pixel is valid for a walk), else returns False.


        Parameters
        ----------
        spidx : int
            Index within Iskel of the point to check.
        Iskel : np.array
            Image of skeletonized mask.

        Returns
        -------
        chk_sp : bool
            True if the startpoint is valid; else False.

        """
        neighs = walk.walkable_neighbors([spidx], Iskel)
        isbp = walk.is_bp(neighs.pop(), Iskel)

        if isbp == 0:
            chk_sp = True
        else:
            chk_sp = False

        return chk_sp

    def find_starting_pixels(Iskel):
        """
        Finds an endpoint pixel to begin walking to resolve network.

        Parameters
        ----------
        Iskel : np.array
            Image of skeletonized mask.

        Returns
        -------
        startpoints : list
            Possible starting points for the walk.

        """
        # Get skeleton connectivity
        eps = imu.skel_endpoints(Iskel)
        eps = set(np.ravel_multi_index(eps, Iskel.shape))

        # Get one endpoint per connected component in network
        rp, _ = imu.regionprops(Iskel, ['coords'])
        startpoints = []
        for ni in rp['coords']:
            idcs = set(np.ravel_multi_index((ni[:, 0], ni[:, 1]), Iskel.shape))
            # Find a valid endpoint for each connected component network
            poss_id = idcs.intersection(eps)
            if len(poss_id) > 0:
                for pid in poss_id:
                    if check_startpoint(pid, Iskel) is True:
                        startpoints.append(pid)
                        break

        return startpoints

    # Pad the skeleton image to avoid edge problems when walking along skeleton
    initial_dims = Iskel.shape
    npad = 20
    Iskel = np.pad(Iskel, npad, mode='constant', constant_values=0)
    dims = Iskel.shape

    # Find starting points of all the networks in Iskel
    startpoints = find_starting_pixels(Iskel)

    # Initialize topology storage vars
    nodes = dict()
    nodes['idx'] = OrderedSet([])
    nodes['conn'] = []

    links = dict()
    links['idx'] = [[]]
    links['conn'] = [[]]
    links['id'] = OrderedSet([])

    # Initialize first links emanting from all starting points
    for i, sp in enumerate(startpoints):
        links = lnu.link_updater(links, len(links['id']), sp, i)
        nodes = lnu.node_updater(nodes, sp, i)
        first_step = walk.walkable_neighbors(links['idx'][i], Iskel)
        links = lnu.link_updater(links, i, first_step.pop())

    links['n_networks'] = i + 1

    # Initialize set of links to be resolved
    links2do = OrderedSet(links['id'])

    while links2do:

        linkid = next(iter(links2do))
        linkidx = links['id'].index(linkid)

        walking = 1
        cantwalk = walk.cant_walk(links, linkidx, nodes, Iskel)

        while walking:

            # Get next possible steps
            poss_steps = walk.walkable_neighbors(links['idx'][linkidx], Iskel)

            # Now we have a few possible cases:
            # 1) endpoint reached,
            # 2) only one pixel to walk to: must check if it's a branchpoint so walk can terminate
            # 3) two pixels to walk to: if neither is branchpoint, problem in skeleton. If one is branchpoint, walk to it and terminate link. If both are branchpoints, walk to the one that is 4-connected.

            if len(poss_steps
                   ) == 0:  # endpoint reached, update node, link connectivity
                nodes = lnu.node_updater(nodes, links['idx'][linkidx][-1],
                                         linkid)
                links = lnu.link_updater(links,
                                         linkid,
                                         conn=nodes['idx'].index(
                                             links['idx'][linkidx][-1]))
                links2do.remove(linkid)
                break  # must break rather than set walking to 0 as we don't want to execute the rest of the code

            if len(links['idx'][linkidx]) < 4:
                poss_steps = list(poss_steps - cantwalk)
            else:
                poss_steps = list(poss_steps)

            if len(
                    poss_steps
            ) == 0:  # We have reached an emanating link, so delete the current one we're working on
                links, nodes = walk.delete_link(linkid, links, nodes)
                links2do.remove(linkid)
                walking = 0

            elif len(poss_steps
                     ) == 1:  # Only one option, so we'll take the step
                links = lnu.link_updater(links, linkid, poss_steps)

                # But check if it's a branchpoint, and if so, stop marching along this link and resolve all the branchpoint links
                if walk.is_bp(poss_steps[0], Iskel) == 1:
                    links, nodes, links2do = walk.handle_bp(
                        linkid, poss_steps[0], nodes, links, links2do, Iskel)
                    links, nodes, links2do = walk.check_dup_links(
                        linkid, links, nodes, links2do)
                    walking = 0  # on to next link

            elif len(
                    poss_steps
            ) > 1:  # Check to see if either/both/none are branchpoints
                isbp = []
                for n in poss_steps:
                    isbp.append(walk.is_bp(n, Iskel))

                if sum(isbp) == 0:

                    # Compute 4-connected neighbors
                    isfourconn = []
                    for ps in poss_steps:
                        checkfour = links['idx'][linkidx][-1] - ps
                        if checkfour in [-1, 1, -dims[1], dims[1]]:
                            isfourconn.append(1)
                        else:
                            isfourconn.append(0)

                    # Compute noturn neighbors
                    noturn = walk.idcs_no_turnaround(
                        links['idx'][linkidx][-2:], Iskel)
                    noturnidx = [n for n in noturn if n in poss_steps]

                    # If we can walk to a 4-connected pixel, we will
                    if sum(isfourconn) == 1:
                        links = lnu.link_updater(
                            links, linkid, poss_steps[isfourconn.index(1)])
                    # If we can't walk to a 4-connected, try to walk in a direction that does not turn us around
                    elif len(noturnidx) == 1:
                        links = lnu.link_updater(links, linkid, noturnidx)
                    # Else, shit. You've found a critical flaw in the algorithm.
                    else:
                        logger.info('idx: {}, poss_steps: {}'.format(
                            links['idx'][linkidx][-1], poss_steps))
                        raise RuntimeError(
                            'Ambiguous which step to take next :( Please raise issue at https://github.com/jonschwenk/RivGraph/issues.'
                        )

                elif sum(isbp) == 1:
                    # If we've already accounted for this branchpoint, delete the link and halt
                    links = lnu.link_updater(links, linkid,
                                             poss_steps[isbp.index(1)])
                    links, nodes, links2do = walk.handle_bp(
                        linkid, poss_steps[isbp.index(1)], nodes, links,
                        links2do, Iskel)
                    links, nodes, links2do = walk.check_dup_links(
                        linkid, links, nodes, links2do)
                    walking = 0

                elif sum(isbp) > 1:
                    # In the case where we can walk to more than one branchpoint, choose the
                    # one that is 4-connected, as this is how we've designed branchpoint
                    # assignment for complete network resolution.
                    isfourconn = []
                    for ps in poss_steps:
                        checkfour = links['idx'][linkidx][-1] - ps
                        if checkfour in [-1, 1, -dims[1], dims[1]]:
                            isfourconn.append(1)
                        else:
                            isfourconn.append(0)

                    # Find poss_step(s) that is both 4-connected and a branchpoint
                    isbp_and_fourconn_idx = [
                        i for i in range(0, len(isbp))
                        if isbp[i] == 1 and isfourconn[i] == 1
                    ]

                    # If we don't have exactly one, shit.
                    if len(isbp_and_fourconn_idx) != 1:
                        logger.info('idx: {}, poss_steps: {}'.format(
                            links['idx'][linkidx][-1], poss_steps))
                        raise RuntimeError(
                            'There is not a unique branchpoint to step to.')
                    else:
                        links = lnu.link_updater(
                            links, linkid,
                            poss_steps[isbp_and_fourconn_idx[0]])
                        links, nodes, links2do = walk.handle_bp(
                            linkid, poss_steps[isbp_and_fourconn_idx[0]],
                            nodes, links, links2do, Iskel)
                        links, nodes, links2do = walk.check_dup_links(
                            linkid, links, nodes, links2do)
                        walking = 0

    # Put the link and node coordinates back into the unpadded
    links, nodes = lnu.adjust_for_padding(links, nodes, npad, dims,
                                          initial_dims)

    # Add indices to nodes--this probably should've been done in network extraction
    # but since nodes have unique idx it was unnecessary.
    nodes['id'] = OrderedSet(range(0, len(nodes['idx'])))

    # Remove duplicate links if they exist; for some single-pixel links,
    # duplicates are formed. Ideally the walking code should ensure that this
    # doesn't happen, but for now removing duplicates suffices.
    links, nodes = lnu.remove_duplicate_links(links, nodes)

    return links, nodes
Exemplo n.º 2
0
def skel_to_graph(Iskel):
    """
    Breaks a skeletonized image into links and nodes; exports if desired.
    """
    def check_startpoint(spidx, Iskel):
        """
        Returns True if a skeleton pixel's first neighbor is not a branchpoint (i.e.
        the start pixel is valid), else returns False.
        """

        neighs = walk.walkable_neighbors([spidx], Iskel)
        isbp = walk.is_bp(neighs.pop(), Iskel)

        if isbp == 0:
            return True
        else:
            return False

    def find_starting_pixels(Iskel):
        """
        Finds an endpoint pixel to begin network resolution
        """

        # Get skeleton connectivity
        eps = iu.skel_endpoints(Iskel)
        eps = set(np.ravel_multi_index(eps, Iskel.shape))

        # Get one endpoint per connected component in network
        rp = iu.regionprops(Iskel, ['coords'])
        startpoints = []
        for ni in rp['coords']:
            idcs = set(np.ravel_multi_index((ni[:, 0], ni[:, 1]), Iskel.shape))
            # Find a valid endpoint for each connected component network
            poss_id = idcs.intersection(eps)
            if len(poss_id) > 0:
                for pid in poss_id:
                    if check_startpoint(pid, Iskel) is True:
                        startpoints.append(pid)
                        break

        return startpoints

    # Pad the skeleton image to avoid edge problems when walking along skeleton
    initial_dims = Iskel.shape
    npad = 10
    Iskel = np.pad(Iskel, npad, mode='constant', constant_values=0)

    dims = Iskel.shape

    # Find starting points of all the networks in Iskel
    startpoints = find_starting_pixels(Iskel)

    # Initialize topology storage vars
    nodes = dict()
    nodes['idx'] = OrderedSet([])
    nodes['conn'] = []  #[[] for i in range(3)]

    links = dict()
    links['idx'] = [[]]
    links['conn'] = [[]]
    links['id'] = OrderedSet([])

    # Initialize first links emanting from all starting points
    for i, sp in enumerate(startpoints):
        links = lnu.link_updater(links, len(links['id']), sp, i)
        nodes = lnu.node_updater(nodes, sp, i)
        first_step = walk.walkable_neighbors(links['idx'][i], Iskel)
        links = lnu.link_updater(links, i, first_step.pop())

    links['n_networks'] = i + 1

    # Initialize set of links to be resolved
    links2do = OrderedSet(links['id'])

    while links2do:

        linkid = next(iter(links2do))
        linkidx = links['id'].index(linkid)

        walking = 1
        cantwalk = walk.cant_walk(links, linkidx, nodes, Iskel)

        while walking:

            # Get next possible steps
            poss_steps = walk.walkable_neighbors(links['idx'][linkidx], Iskel)

            # Now we have a few possible cases:
            # 1) endpoint reached,
            # 2) only one pixel to walk to: must check if it's a branchpoint so walk can terminate
            # 3) two pixels to walk to: if neither is branchpoint, problem in skeleton. If one is branchpoint, walk to it and terminate link. If both are branchpoints, walk to the one that is 4-connected.

            if len(poss_steps
                   ) == 0:  # endpoint reached, update node, link connectivity
                nodes = lnu.node_updater(nodes, links['idx'][linkidx][-1],
                                         linkid)
                links = lnu.link_updater(links,
                                         linkid,
                                         conn=nodes['idx'].index(
                                             links['idx'][linkidx][-1]))
                links2do.remove(linkid)
                break  # must break rather than set walking to 0 as we don't want to execute the rest of the code

            if len(links['idx'][linkidx]) < 4:
                poss_steps = list(poss_steps - cantwalk)
            else:
                poss_steps = list(poss_steps)

            if len(
                    poss_steps
            ) == 0:  # We have reached an emanating link, so delete the current one we're working on
                links, nodes = walk.delete_link(linkid, links, nodes)
                links2do.remove(linkid)
                walking = 0

            elif len(poss_steps
                     ) == 1:  # Only one option, so we'll take the step
                links = lnu.link_updater(links, linkid, poss_steps)

                # But check if it's a branchpoint, and if so, stop marching along this link and resolve all the branchpoint links
                if walk.is_bp(poss_steps[0], Iskel) == 1:
                    links, nodes, links2do = walk.handle_bp(
                        linkid, poss_steps[0], nodes, links, links2do, Iskel)
                    links, nodes, links2do = walk.check_dup_links(
                        linkid, links, nodes, links2do)
                    walking = 0  # on to next link

            elif len(
                    poss_steps
            ) > 1:  # Check to see if either/both/none are branchpoints
                isbp = []
                for n in poss_steps:
                    isbp.append(walk.is_bp(n, Iskel))

                if sum(isbp) == 0:

                    # Compute 4-connected neighbors
                    isfourconn = []
                    for ps in poss_steps:
                        checkfour = links['idx'][linkidx][-1] - ps
                        if checkfour in [-1, 1, -dims[1], dims[1]]:
                            isfourconn.append(1)
                        else:
                            isfourconn.append(0)

                    # Compute noturn neighbors
                    noturn = walk.idcs_no_turnaround(
                        links['idx'][linkidx][-2:], Iskel)
                    noturnidx = [n for n in noturn if n in poss_steps]

                    # If we can walk to a 4-connected pixel, we will
                    if sum(isfourconn) == 1:
                        links = lnu.link_updater(
                            links, linkid, poss_steps[isfourconn.index(1)])
                    # If we can't walk to a 4-connected, try to walk in a direction that does not turn us around
                    elif len(noturnidx) == 1:
                        links = lnu.link_updater(links, linkid, noturnidx)
                    # Else, f**k. You've found a critical flaw in the algorithm.
                    else:
                        print('idx: {}, poss_steps: {}'.format(
                            links['idx'][linkidx][-1], poss_steps))
                        raise RuntimeError(
                            'Ambiguous which step to take next :(')

                elif sum(isbp) == 1:
                    # If we've already accounted for this branchpoint, delete the link and halt
                    links = lnu.link_updater(links, linkid,
                                             poss_steps[isbp.index(1)])
                    links, nodes, links2do = walk.handle_bp(
                        linkid, poss_steps[isbp.index(1)], nodes, links,
                        links2do, Iskel)
                    links, nodes, links2do = walk.check_dup_links(
                        linkid, links, nodes, links2do)
                    walking = 0

                elif sum(isbp) > 1:
                    # In the case where we can walk to more than one branchpoint, choose the
                    # one that is 4-connected, as this is how we've designed branchpoint
                    # assignment for complete network resolution.
                    isfourconn = []
                    for ps in poss_steps:
                        checkfour = links['idx'][linkidx][-1] - ps
                        if checkfour in [-1, 1, -dims[1], dims[1]]:
                            isfourconn.append(1)
                        else:
                            isfourconn.append(0)

                    # Find poss_step(s) that is both 4-connected and a branchpoint
                    isbp_and_fourconn_idx = [
                        i for i in range(0, len(isbp))
                        if isbp[i] == 1 and isfourconn[i] == 1
                    ]

                    # If we don't have exactly one, f**k.
                    if len(isbp_and_fourconn_idx) != 1:
                        print('idx: {}, poss_steps: {}'.format(
                            links['idx'][linkidx][-1], poss_steps))
                        raise RuntimeError(
                            'There is not a unique branchpoint to step to.')
                    else:
                        links = lnu.link_updater(
                            links, linkid,
                            poss_steps[isbp_and_fourconn_idx[0]])
                        links, nodes, links2do = walk.handle_bp(
                            linkid, poss_steps[isbp_and_fourconn_idx[0]],
                            nodes, links, links2do, Iskel)
                        links, nodes, links2do = walk.check_dup_links(
                            linkid, links, nodes, links2do)
                        walking = 0

    # Put the link and node coordinates back into the unpadded
    links, nodes = lnu.adjust_for_padding(links, nodes, npad, dims,
                                          initial_dims)

    # Add indices to nodes--this probably should've been done in network extraction
    # but since nodes have unique idx it was unnecessary. IDs may be required
    # for further processing, though.
    nodes['id'] = OrderedSet(range(0, len(nodes['idx'])))

    return links, nodes
Exemplo n.º 3
0
def handle_bp(linkid, bpnode, nodes, links, links2do, Iskel):
    """
    Handle branchpoints.

    When walking along a skeleton and encountering a branchpoint, we want to
    initialize all un-done links emanating from the branchpoint. Each new link
    contains the branchpoint as the first index, and this function also takes
    the first step of the link.

    Parameters
    ----------
    linkid : int
        Link id of the link walking from.
    bpnode : np.int
        Node id of the branchpoint to be resolved.
    nodes : dict
        Network nodes and associated properties.
    links : dict
        Network links and associated properties.
    links2do : OrderedSet
        This set keeps track of all the links that still need to be resolved
        in the full skeleton. It is generated and populated in mask_to_graph.
    Iskel : np.ndarray
        Image of the local skeletonized mask.

    Returns
    -------
    links : TYPE
        Network links and associated properties with the branchpoint links
        added.
    nodes : TYPE
        Network nodes and associated properties with the branchpoint-related
        nodes added.
    links2do : OrderedSet
        Same as input, but with new link emanators added and linkid removed.

    """
    links2do.remove(linkid)

    # If the branchpoint has already been visited, we don't need to
    # re-initialize emanating links
    if bpnode in nodes['idx']:
        doneflag = 1
    else:
        doneflag = 0

    linkidx = links['id'].index(linkid)

    # Add the branchpoint to nodes dict
    nodes = lnu.node_updater(nodes, bpnode, linkid)

    # Update link connectivity
    links = lnu.link_updater(links, linkid, conn=nodes['idx'].index(bpnode))

    if doneflag == 1:
        return links, nodes, links2do

    # We must initialize new branchpoints. If branchpoints are connected,
    # their links must be assigned to preserve 4-connectivity to avoid
    # problems when walking.

    # Resolve the branchpoint cluster (or single branchpoint)
    bp = bp_cluster([bpnode], Iskel)

    # Find the pixels emanating from the cluster
    emanators = np.array(
        list(
            find_emanators(bpnode, Iskel) - set(bp) -
            set(links['idx'][linkidx])))

    # For each branchpoint, separate emanators into 4-connected neighbors and
    # diagonally-connected neighbors
    fourconn = []
    emremove = []
    for b in bp:
        abdif = abs(b - emanators)
        for i, a in enumerate(abdif):
            if a == 1 or a == Iskel.shape[1]:
                fourconn.append([b, emanators[i]])
                emremove.append(emanators[i])
    # Remove the 4-connected emanators we've just assigned from emanators list
    emanators = np.array([e for e in emanators if e not in emremove])

    # Make diagonal links
    diagconn = []
    for b in bp:
        abdif = abs(b - emanators)
        for i, a in enumerate(abdif):
            if a == Iskel.shape[1] + 1 or a == Iskel.shape[1] - 1:
                diagconn.append([b, emanators[i]])

    # Make links connecting adjacent branchpoints
    bp_pairs = []
    for b in bp:
        bn = walkable_neighbors([b], Iskel)
        for bb in bp:
            if bb in bn:
                bp_pairs.append(set([b, bb]))

    # Get the unique links - ordering is lost
    bp_pairs = [list(i) for i in set(tuple(i) for i in bp_pairs)]

    # Update links and nodes
    # Create links between adjacent branchpoints
    for b in bp_pairs:
        linkid = max(links['id']) + 1
        nodes = lnu.node_updater(nodes, b[0], linkid)
        nodes = lnu.node_updater(nodes, b[1], linkid)
        links = lnu.link_updater(links,
                                 linkid,
                                 b,
                                 conn=nodes['idx'].index(b[0]))
        links = lnu.link_updater(links, linkid, conn=nodes['idx'].index(b[1]))

    # Finally, initialize new links to be walked
    # Before issuing new links, ensure that the link has not
    # already been walked

    # Initialize the fourconn first so they will be walked first
    for p in fourconn:
        # Check if link to issue has already been resolved
        nodeidx = nodes['idx'].index(p[0])
        donelinks = nodes['conn'][nodeidx]
        isdone = 0
        for dl in donelinks:
            if set(links['idx'][links['id'].index(dl)][-2:]) == set(p):
                isdone = 1
        if isdone == 0:
            linkid = max(links['id']) + 1
            links2do.add(linkid)
            nodes = lnu.node_updater(nodes, p[0], linkid)
            links = lnu.link_updater(links, linkid, p,
                                     nodes['idx'].index(p[0]))

    # Then initialize the diagonals
    for p in diagconn:
        # Check if link to issue has already been resolved
        nodeidx = nodes['idx'].index(p[0])
        donelinks = nodes['conn'][nodeidx]
        isdone = 0
        for dl in donelinks:
            if set(links['idx'][links['id'].index(dl)][-2:]) == set(p):
                isdone = 1
        if isdone == 0:
            linkid = max(links['id']) + 1
            links2do.add(linkid)
            nodes = lnu.node_updater(nodes, p[0], linkid)
            links = lnu.link_updater(links, linkid, p,
                                     nodes['idx'].index(p[0]))

    return links, nodes, links2do