Skip to content

Commit

Permalink
Merge branch 'main' into refactor_for_editing
Browse files Browse the repository at this point in the history
  • Loading branch information
cmalinmayor committed Oct 18, 2024
2 parents 40b128f + 4a6d058 commit 62c0f9d
Show file tree
Hide file tree
Showing 11 changed files with 8,741 additions and 11 deletions.
3 changes: 0 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,6 @@ by framing the task as an Integer Linear Program (ILP).
See the motile [documentation](https://funkelab.github.io/motile)
for more details on the concepts and method.

Browsing tracking data with interactive lineage tree
![](docs/images/motile_napari_tree_view.gif)

----------------------------------

## Installation
Expand Down
Binary file removed docs/images/motile_napari_tree_view.gif
Binary file not shown.
3 changes: 3 additions & 0 deletions docs/source/getting_started.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ times with different inputs or parameters, you can click back and forth
between the results here. Here you can also save any runs that you want to store for later.
Deleting runs you do not want to keep viewing is a good idea, since these are stored in memory.
Runs that were saved in previous sessions do not appear here until you load them from disk with the ``Load Tracks`` button.
The tracking results can also be visualized as a lineage tree.
You can open the lineage tree widget via ``Plugins`` > ``Motile`` > ``Lineage View``.
For more details, go to the :doc:`Tree View <tree_view>` documentation.

.. _Issue #48: https://github.com/funkelab/motile_napari_plugin/issues/48
.. _Cell Tracking Challenge: https://celltrackingchallenge.net/
Expand Down
8,602 changes: 8,602 additions & 0 deletions scripts/hela_example_tracks.csv

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions scripts/view_external_tracks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import napari
from motile_plugin.example_data import Fluo_N2DL_HeLa
from motile_plugin.utils.load_tracks import tracks_from_csv
from motile_plugin.widgets import TreeWidget

if __name__ == "__main__":
# load the example data
raw_layer_info, labels_layer_info = Fluo_N2DL_HeLa()
segmentation_arr = labels_layer_info[0]
# the segmentation ids in this file correspond to the segmentation ids in the
# example segmentation data, loaded above
csvfile = "hela_example_tracks.csv"
tracks = tracks_from_csv(csvfile, segmentation_arr)

viewer = napari.Viewer()
raw_data, raw_kwargs, _ = raw_layer_info
viewer.add_image(raw_data, **raw_kwargs)
labels_data, labels_kwargs, _ = labels_layer_info
viewer.add_labels(labels_data, **labels_kwargs)
widget = TreeWidget(viewer)
widget.tracks_viewer.view_external_tracks(tracks, "example")
viewer.window.add_dock_widget(widget, name="Lineage View", area="bottom")

# Start the Napari GUI event loop
napari.run()
10 changes: 7 additions & 3 deletions src/motile_plugin/data_views/views/tree_view/tree_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ def set_view(
Args:
view_direction (str): "horizontal" or "vertical"
feature (str): the feature being displayed, it can be either 'tree' or 'area'
feature (str): the feature being displayed, it can be 'tree' or 'area'
"""

if view_direction == self.view_direction and feature == self.feature:
Expand Down Expand Up @@ -576,8 +576,11 @@ def _update_track_data(self, reset_view: bool | None = None) -> None:
)
self.graph = self.tracks_viewer.tracks.graph

# check whether we have area measurements and therefore should activate the area button
# check whether we have area measurements and therefore should activate the area
# button
if "area" not in self.track_df.columns:
if self.feature_widget.feature == "area":
self.feature_widget._toggle_feature_mode()
self.feature_widget.show_area_radio.setEnabled(False)
else:
self.feature_widget.show_area_radio.setEnabled(True)
Expand Down Expand Up @@ -642,7 +645,8 @@ def _set_mode(self, mode: str) -> None:
)

def _set_feature(self, feature: str) -> None:
"""Set the feature mode to 'tree' or 'area'. For this the view is always horizontal.
"""Set the feature mode to 'tree' or 'area'. For this the view is always
horizontal.
Args:
feature (str): The feature to plot. Options are "tree" or "area"
Expand Down
16 changes: 16 additions & 0 deletions src/motile_plugin/data_views/views_coordinator/tracks_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import napari
import numpy as np
from motile_toolbox.candidate_graph.graph_attributes import NodeAttr
from motile_toolbox.visualization.napari_utils import assign_tracklet_ids
from psygnal import Signal

from motile_plugin.data_model import NodeType, Tracks
Expand All @@ -13,6 +14,7 @@
from motile_plugin.data_views.views.tree_view.tree_widget_utils import (
extract_lineage_tree,
)
from motile_plugin.utils.relabel_segmentation import relabel_segmentation

from .node_selection_list import NodeSelectionList
from .tracks_list import TracksList
Expand Down Expand Up @@ -189,6 +191,20 @@ def update_selection(self) -> None:
visible = self.filter_visible_nodes()
self.tracking_layers.update_visible(visible)

def view_external_tracks(self, tracks: Tracks, name: str) -> None:
"""View tracks created externally. Assigns tracklet ids, adds a hypothesis
dimension to the segmentation, and relabels the segmentation based on the
assigned track ids. Then calls update_tracks.
Args:
tracks (Tracks): A tracks object to view, created externally from the plugin
name (str): The name to display in napari layers
"""
tracks.graph, _ = assign_tracklet_ids(tracks.graph)
tracks.segmentation = np.expand_dims(tracks.segmentation, axis=1)
tracks.segmentation = relabel_segmentation(tracks.graph, tracks.segmentation)
self.update_tracks(tracks, name)

def set_napari_view(self) -> None:
"""Adjust the current_step of the viewer to jump to the last item of the selected_nodes list"""
if len(self.selected_nodes) > 0:
Expand Down
6 changes: 3 additions & 3 deletions src/motile_plugin/motile/menus/motile_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ def edit_run(self, run: MotileRun | None):
self.edit_run_widget.new_run(run)

def _generate_tracks(self, run: MotileRun) -> None:
"""Called when we start solving a new run. Switches from run editor to run viewer
and starts solving of the new run in a separate thread to avoid blocking
"""Called when we start solving a new run. Switches from run editor to run
viewer and starts solving of the new run in a separate thread to avoid blocking
Args:
run (MotileRun): Start solving this motile run.
Expand Down Expand Up @@ -220,7 +220,7 @@ def _title_widget(self) -> QWidget:
<a href="https://funkelab.github.io/motile/"><font color=yellow>motile</font></a> library to
track objects with global optimization. See the
<a href="https://funkelab.github.io/motile_napari_plugin/"><font color=yellow>user guide</font></a>
for a tutorial to the plugin functionality."""
for a tutorial to the plugin functionality.""" # noqa
label = QLabel(richtext)
label.setWordWrap(True)
label.setOpenExternalLinks(True)
Expand Down
5 changes: 3 additions & 2 deletions src/motile_plugin/motile/menus/run_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,10 +183,11 @@ def save_run(self):

def export_tracks(self):
"""Export the tracks from this run to a csv with the following columns:
t,[z],y,x,id,parent_id
t,[z],y,x,id,parent_id,[seg_id]
Cells without a parent_id will have an empty string for the parent_id.
Whether or not to include z is inferred from the length of an
arbitrary node's position attribute.
arbitrary node's position attribute. If the nodes have a "seg_id" attribute,
the "seg_id" column is included.
"""
default_name = self.run._make_id()
default_name = f"{default_name}_tracks.csv"
Expand Down
46 changes: 46 additions & 0 deletions src/motile_plugin/utils/load_tracks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
from csv import DictReader

import networkx as nx
import numpy as np
from motile_plugin.core import Tracks


def tracks_from_csv(csvfile: str, segmentation: np.ndarray | None = None) -> Tracks:
"""Assumes a csv similar to that created from "export tracks to csv" with columns:
t,[z],y,x,id,parent_id,[seg_id]
Cells without a parent_id will have an empty string or a -1 for the parent_id.
Args:
csvfile (str):
path to the csv to load
segmentation (np.ndarray | None, optional):
An optional accompanying segmentation.
If provided, assumes that the seg_id column in the csv file exists and
corresponds to the label ids in the segmentation array
Returns:
Tracks: a tracks object ready to be visualized with
TracksViewer.view_external_tracks
"""
graph = nx.DiGraph()
with open(csvfile) as f:
reader = DictReader(f)
for row in reader:
_id = row["id"]
attrs = {
"pos": [float(row["y"]), float(row["x"])],
"time": int(row["t"]),
}
if "seg_id" in row:
attrs["seg_id"] = int(row["seg_id"])
graph.add_node(_id, **attrs)
parent_id = row["parent_id"].strip()
if parent_id != "":
parent_id = parent_id
if parent_id != -1:
assert parent_id in graph.nodes, f"{_id} {parent_id}"
graph.add_edge(parent_id, _id)
tracks = Tracks(
graph=graph, segmentation=segmentation, pos_attr=("pos"), time_attr="time"
)
return tracks
36 changes: 36 additions & 0 deletions src/motile_plugin/utils/relabel_segmentation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import networkx as nx
import numpy as np
from motile_toolbox.candidate_graph.graph_attributes import NodeAttr


def relabel_segmentation(
solution_nx_graph: nx.DiGraph,
segmentation: np.ndarray,
) -> np.ndarray:
"""Relabel a segmentation based on tracking results so that nodes in same
track share the same id. IDs do change at division.
Args:
solution_nx_graph (nx.DiGraph): Networkx graph with the solution to use
for relabeling. Nodes not in graph will be removed from seg. Original
segmentation ids and hypothesis ids have to be stored in the graph so we
can map them back. Assumes tracket ids have already been assigned.
segmentation (np.ndarray): Original (potentially multi-hypothesis)
segmentation with dimensions (t,h,[z],y,x), where h is 1 for single
input segmentation.
Returns:
np.ndarray: Relabeled segmentation array where nodes in same track share same
id with shape (t,1,[z],y,x)
"""
output_shape = (segmentation.shape[0], 1, *segmentation.shape[2:])
tracked_masks = np.zeros_like(segmentation, shape=output_shape)
for node, _data in solution_nx_graph.nodes(data=True):
time_frame = solution_nx_graph.nodes[node][NodeAttr.TIME.value]
previous_seg_id = solution_nx_graph.nodes[node][NodeAttr.SEG_ID.value]
assert previous_seg_id != 0
tracklet_id = solution_nx_graph.nodes[node]["tracklet_id"]
hypothesis_id = solution_nx_graph.nodes[node].get(NodeAttr.SEG_HYPO.value, 0)
previous_seg_mask = segmentation[time_frame, hypothesis_id] == previous_seg_id
tracked_masks[time_frame, 0][previous_seg_mask] = tracklet_id
return tracked_masks

0 comments on commit 62c0f9d

Please sign in to comment.