Skip to content

Commit

Permalink
Implemented writer to save results and improved docs
Browse files Browse the repository at this point in the history
  • Loading branch information
rhoitink committed Nov 7, 2023
1 parent 295d9ee commit 891ee1b
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 45 deletions.
33 changes: 9 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,24 @@
[![codecov](https://codecov.io/gh/rhoitink/napari-trackpy/branch/main/graph/badge.svg)](https://codecov.io/gh/rhoitink/napari-trackpy)
[![napari hub](https://img.shields.io/endpoint?url=https://api.napari-hub.org/shields/napari-trackpy)](https://napari-hub.org/plugins/napari-trackpy)

Plugin to do trackpy particle tracking on 3D microscopy data within napari

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

This [napari] plugin was generated with [Cookiecutter] using [@napari]'s [cookiecutter-napari-plugin] template.

<!--
Don't miss the full getting started guide to set up your new package:
https://github.com/napari/cookiecutter-napari-plugin#getting-started
and review the napari docs for plugin developers:
https://napari.org/stable/plugins/index.html
-->
Plugin to do [trackpy] particle tracking on 3D microscopy data within [napari]. Currently only tracking of XYZ data is implemented.

## Installation

You can install `napari-trackpy` via [pip]:

pip install napari-trackpy



To install latest development version :

pip install git+https://github.com/rhoitink/napari-trackpy.git

## How to use this plugin?
* Load your XYZ data (using [napari-aicsimageio])
* Make sure to split channels into different layers, such that the layer only contains 3D (XYZ) data
* Open the widget for the tracking plugin via `Plugins` > `XYZ particle tracking`
* Optimize the tracking settings for your dataset, for an extensive description of the settings, visit [this tutorial](http://soft-matter.github.io/trackpy/dev/tutorial/tracking-3d.html)
* Save your tracking data into the `.xyz` file format using `Ctrl`+`S` (on the points layer) or via the menu `File` > `Save Selected Layer(s)...`

## Contributing

Expand All @@ -49,19 +41,12 @@ Distributed under the terms of the [MIT] license,
If you encounter any problems, please [file an issue] along with a detailed description.

[napari]: https://github.com/napari/napari
[Cookiecutter]: https://github.com/audreyr/cookiecutter
[@napari]: https://github.com/napari
[trackpy]: https://github.com/soft-matter/trackpy
[napari-aicsimageio]: https://github.com/AllenCellModeling/napari-aicsimageio
[MIT]: http://opensource.org/licenses/MIT
[BSD-3]: http://opensource.org/licenses/BSD-3-Clause
[GNU GPL v3.0]: http://www.gnu.org/licenses/gpl-3.0.txt
[GNU LGPL v3.0]: http://www.gnu.org/licenses/lgpl-3.0.txt
[Apache Software License 2.0]: http://www.apache.org/licenses/LICENSE-2.0
[Mozilla Public License 2.0]: https://www.mozilla.org/media/MPL/2.0/index.txt
[cookiecutter-napari-plugin]: https://github.com/napari/cookiecutter-napari-plugin

[file an issue]: https://github.com/rhoitink/napari-trackpy/issues

[napari]: https://github.com/napari/napari
[tox]: https://tox.readthedocs.io/en/latest/
[pip]: https://pypi.org/project/pip/
[PyPI]: https://pypi.org/
74 changes: 55 additions & 19 deletions src/napari_trackpy/_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@

class XYZWidget(Container):
def __init__(self, viewer: napari.viewer.Viewer):
"""Class to perform particle tracking using `trackpy` on an xyz dataset
Args:
viewer (napari.viewer.Viewer): Napari viewer instance
"""
self.viewer = viewer
self.img_layer = None
self.img_layer_name = cast(
Expand Down Expand Up @@ -68,9 +73,9 @@ def __init__(self, viewer: napari.viewer.Viewer):
enabled=False, label="Save coordinates"
)
self.run_btn = PushButton(label="Run")
self.run_btn.clicked.connect(self._on_run_clicked)
self.run_btn.clicked.connect(self.run_tracking)
self.reset_btn.clicked.connect(self.reset)
self.img_layer_name.changed.connect(self._on_label_layer_changed)
self.img_layer_name.changed.connect(self._on_image_layer_changed)

self.last_added_points_layer = None
self.fig, self.ax = None, None
Expand All @@ -92,7 +97,8 @@ def __init__(self, viewer: napari.viewer.Viewer):
)

@thread_worker
def do_particle_tracking(self):
def do_particle_tracking(self) -> None:
"""Thread that performs the particle tracking"""
img = self.img_layer.metadata["aicsimage"]

# tracking code implementation based on `sp8_xyz_tracking_lif.py` by Maarten Bransen
Expand All @@ -105,7 +111,6 @@ def do_particle_tracking(self):
)

# convert feature size and min_separation to pixel units

feature_sizes = np.array(
[
np.ceil(
Expand Down Expand Up @@ -145,7 +150,7 @@ def do_particle_tracking(self):
]
)

# disallow equal sizes in all dimensions
# disallow equal sizes in all dimensions (trackpy requirement)
if (
feature_sizes[2] == feature_sizes[1]
and feature_sizes[2] == feature_sizes[0]
Expand Down Expand Up @@ -185,12 +190,14 @@ def do_particle_tracking(self):
f"{np.shape(self.coords)[0]} features found, took {time()-t:.2f} s"
)

return (self.coords, self.pixel_sizes)
return

def _on_label_layer_changed(self, new_value: napari.layers.Image):
def _on_image_layer_changed(self, new_value: napari.layers.Image):
"""set self.img_layer to an image layer object"""
self.img_layer = new_value

def _on_run_clicked(self):
def run_tracking(self) -> None:
"""Run some checks and start particle tracking if those succeeed"""
if self.img_layer is None:
notifications.show_error("No image selected")
return
Expand All @@ -202,23 +209,29 @@ def _on_run_clicked(self):
return

if self.fig is None:
# initialise figure for mass histogram
self.fig, self.ax = plt.subplots(1, 1)
self.native.layout().addWidget(FigureCanvas(self.fig))

self.run_btn.enabled = False
self.run_btn.enabled = False # disable run button
tracking_thread = self.do_particle_tracking()

# when tracking is finished, process results
tracking_thread.finished.connect(lambda: self.process_tracking())
tracking_thread.start()

def process_tracking(self):
tracking_thread.start() # start thread

def process_tracking(self) -> None:
"""Process results from particle tracking
Basically calls few other functions and resets buttons
"""
self.add_points_to_viewer()
self.show_mass_histogram()
self.run_btn.enabled = True
self.reset_btn.enabled = True
# self.save_params_btn.enabled = True
# self.save_tracking_btn.enabled = True

def add_points_to_viewer(self):
def add_points_to_viewer(self) -> None:
"""Add coordinates as points layer to viewer"""
# @todo: fix size of points
self.last_added_points_layer = self.viewer.add_points(
np.array(self.coords[["z", "y", "x"]]),
Expand All @@ -228,27 +241,50 @@ def add_points_to_viewer(self):
face_color="transparent",
name=f"{self.img_layer.name}_coords",
out_of_slice_display=True,
metadata={
"particle_tracking_pixel_sizes": self.pixel_sizes,
"particle_tracking_settings": self._get_tracking_settings(),
},
)

def show_mass_histogram(self):
def show_mass_histogram(self) -> None:
"""Plot histogram of particle mass"""
self.reset_histogram()
self.ax.hist(self.coords["mass"], "auto")
self.ax.figure.tight_layout()
self.ax.figure.canvas.draw()

def reset_histogram(self):
def reset_histogram(self) -> None:
"""Clear data from histogram"""
self.ax.cla()
self.ax.set_xlabel("mass (a.u.)")
self.ax.set_ylabel("occurence")
self.ax.figure.tight_layout()
self.ax.figure.canvas.draw()

def reset(self):
def reset(self) -> None:
"""Reset histogram and remove data"""
self.reset_histogram()
self.coords = None
self.pixel_sizes = None
if self.last_added_points_layer is not None:
self.viewer.layers.remove(self.last_added_points_layer.name)
self.reset_btn.enabled = False
self.save_params_btn.enabled = False
self.save_tracking_btn.enabled = False

def _get_tracking_settings(self) -> dict:
"""Get dictionary with settings for the particle tracking,
useful for saving parameters into a file.
Returns:
dict: dictionary with the values for each of the parameters
"""

return {
"image_layer": self.img_layer_name.value,
"feature_size_xy_µm": self.feature_size_xy_μm.value,
"feature_size_z_μm": self.feature_size_z_μm.value,
"min_separation_xy_μm": self.min_separation_xy_μm.value,
"min_separation_z_μm": self.min_separation_z_μm.value,
"min_mass": self.min_mass.value,
"max_mass": self.max_mass.value,
}
31 changes: 31 additions & 0 deletions src/napari_trackpy/_writer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Any, Dict, List

import numpy as np


def xyz_file_writer(
path: str, layer_data: Any, attributes: Dict[str, Any]
) -> List[str]:
coordinates = np.array(layer_data)
header = f"{len(coordinates)}\nProperties=Pos:R:3"

if "particle_tracking_pixel_sizes" in attributes["metadata"]:
# scale coordinates with pixel size
coordinates *= np.array(
attributes["metadata"]["particle_tracking_pixel_sizes"]
)
header += ' Unit="micrometer"'

if "particle_tracking_settings" in attributes["metadata"]:
# save particle tracking settings to txt file
with open(
path.replace(".xyz", "_params.txt"), "w", encoding="utf-8"
) as f:
for key, val in attributes["metadata"][
"particle_tracking_settings"
].items():
f.write(f"{key} = {val}\n")

np.savetxt(path, coordinates, comments="", header=header) # save .xyz file

return [path]
14 changes: 12 additions & 2 deletions src/napari_trackpy/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ name: napari-trackpy
display_name: Particle tracking
contributions:
commands:
- id: napari-trackpy.xyz_particle_tracking_settings_widget
- id: napari-trackpy.xyz_tracking
python_name: napari_trackpy._widget:XYZWidget
title: XYZ particle tracking
- id: napari-trackpy.write_points_xyzfile
title: Save points layer to xyz file
python_name: napari_trackpy._writer:xyz_file_writer
widgets:
- command: napari-trackpy.xyz_particle_tracking_settings_widget
- command: napari-trackpy.xyz_tracking
display_name: XYZ particle tracking
writers:
- command: napari-trackpy.write_points_xyzfile
display_name: XYZ format
layer_types:
- points
filename_extensions:
- .xyz

0 comments on commit 891ee1b

Please sign in to comment.