Skip to content

instance segmentation pipeline - graph post processing#69

Open
stmartineau99 wants to merge 5 commits into
computational-cell-analytics:mainfrom
stmartineau99:skeleton-postprocessing
Open

instance segmentation pipeline - graph post processing#69
stmartineau99 wants to merge 5 commits into
computational-cell-analytics:mainfrom
stmartineau99:skeleton-postprocessing

Conversation

@stmartineau99

Copy link
Copy Markdown

graph postprocessing (postprocessing.py)

  • draw_instances function - rasterize the instances or skeleton graph for easy viewing in napari
  • clean_graph has 4 steps
  1. remove_ticks reimplemented function from kimimaro, prune ticks (spurs) below a certain tick_length
  2. split 3 degree junctions - separate the odd arm when it diverges from the through pair by at least min_branch_angle.
  3. split 4 degree junctions, separating its two through pairs when they are collinear to within min_through_angle.
  4. join close components at their endpoints - inspired by similar postporocessing approach in TARDIS library, search all endpoints for nearby endpoints that can be joined if they are within the join_dist and if they are sufficiently colinear, as determined by min_join_angle

tests/skeleton/test_postprocessing.py

  • tests all stages of clean_graph

examples/skeleton/instance_segmentation.py

  • script with CLI for the whole pipeline
    • starting from a binary mask, run teasar to get the skeleton, then convert to a graph and postprocess with clean_graph
    • convert to labels with connected_components - input to draw_instances
    • option to --view, open teasar skeleton and instances
  • importantly, increase direction_span to get better tangent measurements, I found this helps best at around 8-10 to minimize aliasing artifacts

@constantinpape constantinpape left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this all looks good. There are only a few cosmetic things to address.

Some of the things could be rewritten more readable and efficient by using better graph primitives. But I don't know if we have all of the ones needed, especially a contraction graph, so this is work for a follow-up.

Comment thread src/bioimage_cpp/skeleton/postprocessing.py Outdated
return indptr, dst, eid, degrees


def remove_ticks(vertices, edges, tick_length, radii=None):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I skimmed the implementation. The algorithm makes sense high-level:

  • Build a supergraph of distances between junctions (degree >= 3) and endpoints (degree == 1)
  • Drop the edges between junctions and endpoints that are shorter than tick-length

There are two possible things to address:

  • Update the distances during graph contraction (second step) --- this seems to be missing (Never mind, this actually happens)
  • Use more graph primitives (adjacency and a contraction graph) --- I have to check what we have in our graph impl already and we can do this in a follow-up PR

for x in (far1, far2, node):
incident[x].discard(s1)
incident[x].discard(s2)
supers[sid] = [far1, far2, l1 + l2, p1 + p2]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The length update happens here.

sid += 1

dropped = set()
while True:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be more readable if we used a graph structure again.

return volume


__all__ = ["clean_graph", "draw_instances", "join_close_components", "remove_ticks"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typically goes to the top of the file.

return [int(edge_ids[odd])]


def split_degree4(v, graph, vertices, direction_span=1, min_through_angle=160.0):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should either be a proper public function and have a doc-string and type annotations or be private (= _split_degree4). I would make it private.

return vertices[used], remap[edges], radii


def split_degree3(v, graph, vertices, direction_span=1, min_branch_angle=30.0):

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either public function with doc-string and type annotations or ptivate (_split_degree3)

return [int(edge_ids[k]) for k in pair_b]


def clean_graph(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should have a more distinct name --- we can have skeleton graphs for all kinds of things not necessarily only filament graphs. In other cases having junctions may be fine. I would go with clean_filament_graph.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants