Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merge for v0.3.0 #4

Merged
merged 3 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
strategy:
matrix:
platform: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.8', '3.9', '3.10']
python-version: ['3.8', '3.9', '3.10', '3.11']

steps:
- uses: actions/checkout@v3
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ write_to = "src/napari_trackpy/_version.py"

[tool.black]
line-length = 79
target-version = ['py38', 'py39', 'py310']
target-version = ['py38', 'py39', 'py310', 'py311']


[tool.ruff]
Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ classifiers =
Programming Language :: Python :: 3.8
Programming Language :: Python :: 3.9
Programming Language :: Python :: 3.10
Programming Language :: Python :: 3.11
Topic :: Scientific/Engineering :: Image Processing
project_urls =
Bug Tracker = https://github.com/rhoitink/napari-trackpy/issues
Expand Down
67 changes: 45 additions & 22 deletions src/napari_trackpy/_widget.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from time import time
from typing import cast
from typing import Union, cast

import matplotlib.pyplot as plt
import napari
Expand All @@ -10,7 +10,6 @@
from magicgui.widgets import (
ComboBox,
Container,
FloatSlider,
FloatSpinBox,
PushButton,
create_widget,
Expand All @@ -28,8 +27,7 @@ def __init__(self, viewer: napari.viewer.Viewer):
viewer (napari.viewer.Viewer): Napari viewer instance
"""
self.viewer = viewer
self.img_layer = None
self.img_layer_name = cast(
self.img_layer = cast(
ComboBox,
create_widget(annotation=napari.layers.Image, label="Image"),
)
Expand All @@ -49,34 +47,45 @@ def __init__(self, viewer: napari.viewer.Viewer):
max=1e3,
step=0.05,
label="Min. separation xy(µm)",
tooltip="Minimial separation between two features along the x/y axis",
)
self.min_separation_z_µm = FloatSpinBox(
value=0.3,
min=0.0,
max=1e3,
step=0.05,
label="Max. separation z(µm)",
label="Min. separation z(µm)",
tooltip="Minimial separation between two features along the z axis",
)
self.min_mass = FloatSlider(
value=1e2, min=0, max=1e9, label="Min. mass"
self.min_mass = FloatSpinBox(
value=1.0,
min=0.0,
max=1e6,
step=0.1,
label="Min. mass",
tooltip="Per unit of volume (based on defined feature size)",
)
self.max_mass = FloatSlider(
value=1e8, min=1, max=1e9, label="Max. mass"
self.max_mass = FloatSpinBox(
value=1e4,
min=1.0,
max=1e6,
step=0.1,
label="Max. mass",
tooltip="Per unit of volume (based on defined feature size)",
)

self.run_btn = PushButton(label="Run")
self.reset_btn = PushButton(enabled=False, label="Reset")
self.run_btn = PushButton(label="Run")
self.run_btn.clicked.connect(self.run_tracking)
self.reset_btn.clicked.connect(self.reset)
self.img_layer_name.changed.connect(self._on_image_layer_changed)

self.last_added_points_layer = None
self.fig, self.ax = None, None

super().__init__(
widgets=[
self.img_layer_name,
self.img_layer,
self.feature_size_xy_µm,
self.feature_size_z_μm,
self.min_separation_xy_μm,
Expand All @@ -91,10 +100,10 @@ def __init__(self, viewer: napari.viewer.Viewer):
@thread_worker
def do_particle_tracking(self) -> None:
"""Thread that performs the particle tracking"""
img = self.img_layer.metadata["aicsimage"]
img = self.img_layer.value.metadata["aicsimage"]

stack = np.squeeze(
self.img_layer.data_raw
self.img_layer.value.data_raw
) # squeeze out dimensions with length 1
nz, ny, nx = stack.shape
self.pixel_sizes = np.array(
Expand Down Expand Up @@ -151,17 +160,25 @@ def do_particle_tracking(self) -> None:
f"Increasing z-size to {feature_sizes[0] * np.abs(self.pixel_sizes[0])}"
)

self.feature_volume = np.prod(feature_sizes)

t = time()

# trackpy particle tracking with set parameters
# trackpy needs the mass to be not normalized
coords = tp.locate(
stack,
diameter=feature_sizes,
minmass=self.min_mass.value,
minmass=self.mass_denormalize(
self.min_mass.value, self.feature_volume
),
separation=min_separations,
characterize=False,
)
coords = coords.dropna(subset=["x", "y", "z", "mass"])
coords["mass"] = self.mass_normalize(
coords["mass"], self.feature_volume
)
coords = coords.loc[(coords["mass"] < self.max_mass.value)]
coords = coords.loc[
(
Expand All @@ -183,17 +200,13 @@ def do_particle_tracking(self) -> None:

return

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 run_tracking(self) -> None:
"""Run some checks and start particle tracking if those succeeed"""
if self.img_layer is None:
if self.img_layer.value is None:
notifications.show_error("No image selected")
return

if "aicsimage" not in self.img_layer.metadata:
if "aicsimage" not in self.img_layer.value.metadata:
notifications.show_error(
"Data not loaded via aicsimageio plugin, cannot extract metadata"
)
Expand Down Expand Up @@ -221,6 +234,16 @@ def process_tracking(self) -> None:
self.run_btn.enabled = True
self.reset_btn.enabled = True

def mass_normalize(
self, mass: Union[float, np.ndarray], feature_volume: float
):
return mass / feature_volume

def mass_denormalize(
self, normalized_mass: Union[float, np.ndarray], feature_volume: float
):
return normalized_mass * feature_volume

def add_points_to_viewer(self) -> None:
"""Add coordinates as points layer to viewer"""
# @todo: fix size of points
Expand All @@ -230,7 +253,7 @@ def add_points_to_viewer(self) -> None:
scale=self.pixel_sizes,
edge_color="red",
face_color="transparent",
name=f"{self.img_layer.name}_coords",
name=f"{self.img_layer.value.name}_coords",
out_of_slice_display=True,
metadata={
"particle_tracking_pixel_sizes": self.pixel_sizes,
Expand Down Expand Up @@ -271,7 +294,7 @@ def _get_tracking_settings(self) -> dict:
"""

return {
"image_layer": self.img_layer_name.value,
"image_layer": self.img_layer.value.name,
"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,
Expand Down
3 changes: 2 additions & 1 deletion tox.ini
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
# For more information about tox, see https://tox.readthedocs.io/en/latest/
[tox]
envlist = py{38,39,310}-{linux,macos,windows}
envlist = py{38,39,310,311}-{linux,macos,windows}
isolated_build=true

[gh-actions]
python =
3.8: py38
3.9: py39
3.10: py310
3.11: py311

[gh-actions:env]
PLATFORM =
Expand Down