From 891ee1be7bd771b015c3214be3e7a9804ae4e079 Mon Sep 17 00:00:00 2001 From: Roy Hoitink Date: Tue, 7 Nov 2023 15:51:43 +0100 Subject: [PATCH] Implemented writer to save results and improved docs --- README.md | 33 +++++---------- src/napari_trackpy/_widget.py | 74 +++++++++++++++++++++++++--------- src/napari_trackpy/_writer.py | 31 ++++++++++++++ src/napari_trackpy/napari.yaml | 14 ++++++- 4 files changed, 107 insertions(+), 45 deletions(-) create mode 100644 src/napari_trackpy/_writer.py diff --git a/README.md b/README.md index 13afd7f..3cc357a 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,7 @@ [![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. - - +Plugin to do [trackpy] particle tracking on 3D microscopy data within [napari]. Currently only tracking of XYZ data is implemented. ## Installation @@ -27,12 +15,16 @@ 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 @@ -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/ diff --git a/src/napari_trackpy/_widget.py b/src/napari_trackpy/_widget.py index 023aacb..4369b35 100644 --- a/src/napari_trackpy/_widget.py +++ b/src/napari_trackpy/_widget.py @@ -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( @@ -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 @@ -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 @@ -105,7 +111,6 @@ def do_particle_tracking(self): ) # convert feature size and min_separation to pixel units - feature_sizes = np.array( [ np.ceil( @@ -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] @@ -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 @@ -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"]]), @@ -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, + } diff --git a/src/napari_trackpy/_writer.py b/src/napari_trackpy/_writer.py new file mode 100644 index 0000000..294b04c --- /dev/null +++ b/src/napari_trackpy/_writer.py @@ -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] diff --git a/src/napari_trackpy/napari.yaml b/src/napari_trackpy/napari.yaml index c240387..ce11c95 100644 --- a/src/napari_trackpy/napari.yaml +++ b/src/napari_trackpy/napari.yaml @@ -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