Skip to content

Commit

Permalink
Visualize tracked cells and their features (#13)
Browse files Browse the repository at this point in the history
* remove unused import

* configurable logging levels

* metadata options in reader functions

* add tracking labels viewer

* fix ambiguous variable name

* only use labels layer for annotation

* clean up arguments

* todo comment

* improve log message

* format

* fix exports

* use unprojected embeddings

* configurable feature files

* test add widget

* use default napari dependencies for dev

* add optional dependencies
also redo packaging with pyproject.toml

* relax napari pin after napari-clusters-plotter update

* format

* wip: use xarray output zarr format for embeddings

* construct index from xarray

* fix index naming

* bump iohub to 0.2.0a1

* add fake z axis for tracking

* ci: use x86_64 runner for macos since higra is not available on arm64

* disable coverage

* use precomputed umap

* aggregate feature loading

* update widget name in test

* add 3.12 to test runners

* make features optional
this allows the widget to be used as a tracks viewer

* fix fov name filtering

* pin ncp to commit
  • Loading branch information
ziw-liu authored Oct 18, 2024
1 parent 44590b8 commit 28c49c2
Show file tree
Hide file tree
Showing 9 changed files with 211 additions and 104 deletions.
7 changes: 2 additions & 5 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ jobs:
runs-on: ${{ matrix.platform }}
strategy:
matrix:
platform: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.10', '3.11']
platform: [ubuntu-latest, windows-latest, macos-13]
python-version: ['3.10', '3.11', '3.12']

steps:
- uses: actions/checkout@v3
Expand Down Expand Up @@ -60,9 +60,6 @@ jobs:
env:
PLATFORM: ${{ matrix.platform }}

- name: Coverage
uses: codecov/codecov-action@v2

deploy:
# this will run when you have tagged a commit, starting with "v*"
# and requires that you have put your twine API key in your
Expand Down
64 changes: 60 additions & 4 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,12 +1,68 @@
[build-system]
requires = ["setuptools>=42.0.0", "wheel"]
requires = ["setuptools>=42.0.0", "setuptools-scm[toml]"]
build-backend = "setuptools.build_meta"

[project]
name = "napari-iohub"
description = "OME-Zarr viewer for napari with iohub as the I/O backend"
readme = "README.md"
requires-python = ">=3.10"
license = { file = "LICENSE" }
authors = [
{ name = "CZ Biohub SF and napari-iohub contributors", email = "[email protected]" },
]
dependencies = ["iohub>=0.1.0", "magicgui", "qtpy"]
dynamic = ["version"]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Framework :: napari",
"Intended Audience :: Developers",
"License :: OSI Approved :: BSD License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering :: Image Processing",
]

[project.optional-dependencies]
clustering = [
# https://github.com/BiAPoL/napari-clusters-plotter/pull/345
"napari-clusters-plotter@git+https://github.com/BiAPoL/napari-clusters-plotter.git@43f5f8297f41927eb5993b95092aaf9f90bb1583",
"ultrack",
"iohub==0.2.0a1",
]
all = ["napari[all]", "napari-iohub[clustering]"]
dev = [
"tox",
"pytest",
"pytest-cov",
"pytest-qt",
"black",
"ruff",
"napari-iohub[all]",
]

[project.entry-points."napari.manifest"]
"napari-iohub" = "napari_iohub:napari.yaml"

[tool.setuptools_scm]
write_to = "src/napari_iohub/_version.py"

[tool.setuptools]
include-package-data = true

[tool.setuptools.packages.find]
where = ["src"]

[tool.setuptools.package-data]
"*" = ["*.yaml"]

[tool.black]
line-length = 79

[tool.isort]
profile = "black"
line_length = 79
[tool.ruff]
src = ["src"]
lint.extend-select = ["I001"]
lint.isort.known-first-party = ["napari_iohub"]
59 changes: 0 additions & 59 deletions setup.cfg

This file was deleted.

11 changes: 9 additions & 2 deletions src/napari_iohub/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
__version__ = "0.0.1"
import logging
import os

from ._edit_labels import EditLabelsWidget
from ._reader import napari_get_reader
from ._view_tracks import open_image_and_tracks
from ._widget import MainWidget

__all__ = (
"napari_get_reader",
"MainWidget",
"example_magic_widget",
"EditLabelsWidget",
"open_image_and_tracks",
)

_logger = logging.getLogger(__name__)
_logger.setLevel(os.getenv("NAPARI_IOHUB_LOGGING_LEVEL", logging.DEBUG))
4 changes: 1 addition & 3 deletions src/napari_iohub/_edit_labels.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@
import logging
import os
import warnings
from os.path import isdir, isfile
from pathlib import Path
from typing import TYPE_CHECKING

from napari import Viewer
import numpy as np
from iohub.ngff.nodes import Plate, Position, Row, Well, open_ome_zarr
from napari import Viewer
from qtpy.QtWidgets import (
QFileDialog,
QFormLayout,
Expand All @@ -27,7 +26,6 @@
import napari
from napari.types import LayerData
_logger = logging.getLogger(__name__)
_logger.setLevel(logging.DEBUG)


class _LoadFOV(QWidget):
Expand Down
53 changes: 22 additions & 31 deletions src/napari_iohub/_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
MultiScaleMeta,
NGFFNode,
OMEROMeta,
Plate,
Position,
Well,
Plate,
_open_store,
open_ome_zarr,
)
Expand Down Expand Up @@ -69,9 +69,7 @@ def _get_node(path: StrOrBytesPath):
zgroup.store.close()
node = open_ome_zarr(store_path=path, layout="hcs", mode="r")
else:
raise KeyError(
f"NGFF plate or well metadata not found under '{zgroup.name}'"
)
raise KeyError(f"NGFF plate or well metadata not found under '{zgroup.name}'")
return node


Expand All @@ -80,8 +78,8 @@ def stitch_well_by_channel(well: Well, row_wrap: int):
levels = []
pyramids: list[list] = []
for i, (_, pos) in enumerate(well.positions()):
l, ims = _get_multiscales(pos)
levels.append(l)
lv, ims = _get_multiscales(pos)
levels.append(lv)
pyramids.append(ims)
if i == 0:
layers_kwargs = _ome_to_napari_by_channel(pos.metadata)
Expand All @@ -98,8 +96,8 @@ def stack_well_by_position(well: Well):
levels = []
pyramids: list[list] = []
for i, (_, pos) in enumerate(well.positions()):
l, ims = _get_multiscales(pos)
levels.append(l)
lv, ims = _get_multiscales(pos)
levels.append(lv)
pyramids.append(ims)
if i == 0:
layers_kwargs = _ome_to_napari_by_channel(pos.metadata)
Expand All @@ -118,9 +116,7 @@ def _get_multiscales(pos: Position):
try:
multiscales.append(da.from_zarr(pos[im]))
except Exception as e:
logging.warning(
f"Skipped array '{im}' at position {pos.zgroup.name}: {e}"
)
logging.warning(f"Skipped array '{im}' at position {pos.zgroup.name}: {e}")
return len(multiscales), multiscales


Expand All @@ -136,12 +132,12 @@ def _make_grid(elements: list[da.Array], cols: int):
return grid


def _ome_to_napari_by_channel(metadata):
def _ome_to_napari_by_channel(metadata, parse_colormap: bool = True):
omero: OMEROMeta = metadata.omero
layers_kwargs = []
for channel in omero.channels:
metadata = {"name": channel.label}
if channel.color:
if channel.color and parse_colormap:
# alpha channel is optional
rgb = Color(channel.color).as_rgb_tuple(alpha=None)
start = [0.0] * 3
Expand All @@ -153,6 +149,7 @@ def _ome_to_napari_by_channel(metadata):
[v / np.iinfo(np.uint8).max for v in rgb],
]
)
metadata["blending"] = "additive"
layers_kwargs.append(metadata)
return layers_kwargs

Expand Down Expand Up @@ -190,20 +187,20 @@ def layers_from_arrays(
return layers


def fov_to_layers(fov: Position):
layers_kwargs = _ome_to_napari_by_channel(fov.metadata)
def fov_to_layers(fov: Position, layer_type: str = "image"):
layers_kwargs = _ome_to_napari_by_channel(
fov.metadata, parse_colormap=(layer_type == "image")
)
ch_axis = _find_ch_axis(fov)
arrays = [arr for _, arr in fov.images()]
return layers_from_arrays(layers_kwargs, ch_axis, arrays, mode="stitch")
return layers_from_arrays(
layers_kwargs, ch_axis, arrays, mode="stitch", layer_type=layer_type
)


def well_to_layers(
well: Well, mode: Literal["stitch", "stack"], layer_type: str
):
def well_to_layers(well: Well, mode: Literal["stitch", "stack"], layer_type: str):
if mode == "stitch":
layers_kwargs, ch_axis, arrays = stitch_well_by_channel(
well, row_wrap=4
)
layers_kwargs, ch_axis, arrays = stitch_well_by_channel(well, row_wrap=4)
elif mode == "stack":
layers_kwargs, ch_axis, arrays = stack_well_by_position(well)
return layers_from_arrays(
Expand Down Expand Up @@ -234,9 +231,7 @@ def make_bbox(bbox_extents):
maxr = bbox_extents[2]
maxc = bbox_extents[3]

bbox_rect = np.array(
[[minr, minc], [maxr, minc], [maxr, maxc], [minr, maxc]]
)
bbox_rect = np.array([[minr, minc], [maxr, minc], [maxr, maxc], [minr, maxc]])
bbox_rect = np.moveaxis(bbox_rect, 2, 0)

return bbox_rect
Expand Down Expand Up @@ -276,9 +271,7 @@ def plate_to_layers(
]
for k in range(len(boxes)):
boxes[k].append(box_extents[k] - 0.5)
properties["fov"].append(
well_path + "/" + next(well.positions())[0]
)
properties["fov"].append(well_path + "/" + next(well.positions())[0])
else:
row_arrays.append(None)
plate_arrays.append(row_arrays)
Expand All @@ -291,9 +284,7 @@ def plate_to_layers(
row_level = []
for c in r:
if c is None:
arr = da.zeros(
shape=fill_args[level][0], dtype=fill_args[level][1]
)
arr = da.zeros(shape=fill_args[level][0], dtype=fill_args[level][1])
else:
arr = c[level]
row_level.append(arr)
Expand Down
3 changes: 3 additions & 0 deletions src/napari_iohub/_tests/test_view_tracks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
def test_view_tracks_widget(make_napari_viewer):
viewer = make_napari_viewer()
_ = viewer.window.add_plugin_dock_widget("napari-iohub", "Single-cell features")
Loading

0 comments on commit 28c49c2

Please sign in to comment.