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 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 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
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