示例#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
示例#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
示例#3
0
 def test_six(self):
     """Test sixth if."""
     Iskel = np.zeros((5, 5))
     idcs = [5, 0]
     poss_walk_idcs = walk.idcs_no_turnaround(idcs, Iskel)
     assert np.all(poss_walk_idcs == [-6, -5, -4])
示例#4
0
 def test_seven(self):
     """Test seventh if."""
     Iskel = np.zeros((5, 5))
     idcs = [6, 0]
     poss_walk_idcs = walk.idcs_no_turnaround(idcs, Iskel)
     assert np.all(poss_walk_idcs == [-1, -6, -5])
示例#5
0
 def test_five(self):
     """Test fifth if."""
     Iskel = np.zeros((5, 5))
     idcs = [4, 0]
     poss_walk_idcs = walk.idcs_no_turnaround(idcs, Iskel)
     assert np.all(poss_walk_idcs == [-5, -4, 1])
示例#6
0
 def test_four(self):
     """Test fourth if."""
     Iskel = np.zeros((5, 5))
     idcs = [3, 4]
     poss_walk_idcs = walk.idcs_no_turnaround(idcs, Iskel)
     assert np.all(poss_walk_idcs == [0, 5, 10])
示例#7
0
 def test_three(self):
     """Test third if."""
     Iskel = np.zeros((5, 5))
     idcs = [0, 4]
     poss_walk_idcs = walk.idcs_no_turnaround(idcs, Iskel)
     assert np.all(poss_walk_idcs == [3, 8, 9])
示例#8
0
 def test_two(self):
     """Test second if."""
     Iskel = np.zeros((5, 5))
     idcs = [0, 5]
     poss_walk_idcs = walk.idcs_no_turnaround(idcs, Iskel)
     assert np.all(poss_walk_idcs == [9, 10, 11])
示例#9
0
 def test_one(self):
     """Test first if."""
     Iskel = np.zeros((5, 5))
     idcs = [0, 6]
     poss_walk_idcs = walk.idcs_no_turnaround(idcs, Iskel)
     assert np.all(poss_walk_idcs == [7, 11, 12])