napari-ome-arrow 0.0.4__tar.gz → 0.0.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/workflows/run-tests.yml +7 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.pre-commit-config.yaml +4 -4
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/CITATION.cff +26 -0
- {napari_ome_arrow-0.0.4/src/napari_ome_arrow.egg-info → napari_ome_arrow-0.0.5}/PKG-INFO +24 -5
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/README.md +20 -3
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/pyproject.toml +5 -4
- napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader.py +340 -0
- napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_infer.py +233 -0
- napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_napari.py +107 -0
- napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_omearrow.py +474 -0
- napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_stack.py +711 -0
- napari_ome_arrow-0.0.5/src/napari_ome_arrow/_reader_types.py +11 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow/_version.py +3 -3
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow/napari.yaml +3 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5/src/napari_ome_arrow.egg-info}/PKG-INFO +24 -5
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/SOURCES.txt +5 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/requires.txt +4 -1
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/uv.lock +205 -318
- napari_ome_arrow-0.0.4/src/napari_ome_arrow/_reader.py +0 -456
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/ISSUE_TEMPLATE/issue.yml +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/dependabot.yml +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/release-drafter.yml +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/workflows/draft-release.yml +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.github/workflows/publish-pypi.yml +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.gitignore +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/.napari-hub/config.yml +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/CODE_OF_CONDUCT.md +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/CONTRIBUTING.md +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/LICENSE +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/MANIFEST.in +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/setup.cfg +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow/__init__.py +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/dependency_links.txt +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/entry_points.txt +0 -0
- {napari_ome_arrow-0.0.4 → napari_ome_arrow-0.0.5}/src/napari_ome_arrow.egg-info/top_level.txt +0 -0
|
@@ -52,5 +52,12 @@ jobs:
|
|
|
52
52
|
with:
|
|
53
53
|
qt: true
|
|
54
54
|
wm: herbstluftwm
|
|
55
|
+
- name: Install test dependencies
|
|
56
|
+
run: >
|
|
57
|
+
uv sync --frozen
|
|
58
|
+
--python ${{ matrix.python_version }}
|
|
59
|
+
--extra all
|
|
60
|
+
--extra pyside6
|
|
61
|
+
--extra vortex
|
|
55
62
|
- name: Run pytest
|
|
56
63
|
run: uv run --python ${{ matrix.python_version }} --frozen pytest
|
|
@@ -9,7 +9,7 @@ repos:
|
|
|
9
9
|
- id: check-yaml
|
|
10
10
|
- id: detect-private-key
|
|
11
11
|
- repo: https://github.com/tox-dev/pyproject-fmt
|
|
12
|
-
rev: "v2.
|
|
12
|
+
rev: "v2.12.1"
|
|
13
13
|
hooks:
|
|
14
14
|
- id: pyproject-fmt
|
|
15
15
|
- repo: https://github.com/citation-file-format/cffconvert
|
|
@@ -34,17 +34,17 @@ repos:
|
|
|
34
34
|
additional_dependencies:
|
|
35
35
|
- mdformat-gfm
|
|
36
36
|
- repo: https://github.com/adrienverge/yamllint
|
|
37
|
-
rev: v1.
|
|
37
|
+
rev: v1.38.0
|
|
38
38
|
hooks:
|
|
39
39
|
- id: yamllint
|
|
40
40
|
exclude: pre-commit-config.yaml
|
|
41
41
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
42
|
-
rev: "v0.
|
|
42
|
+
rev: "v0.15.0"
|
|
43
43
|
hooks:
|
|
44
44
|
- id: ruff-format
|
|
45
45
|
- id: ruff-check
|
|
46
46
|
- repo: https://github.com/rhysd/actionlint
|
|
47
|
-
rev: v1.7.
|
|
47
|
+
rev: v1.7.10
|
|
48
48
|
hooks:
|
|
49
49
|
- id: actionlint
|
|
50
50
|
- repo: https://gitlab.com/vojko.pribudic.foss/pre-commit-update
|
|
@@ -129,3 +129,29 @@ references:
|
|
|
129
129
|
JUMP (cpg0000-jump-pilot) was used to help demonstrate napari-ome-arrow's
|
|
130
130
|
ability to parse various image data (specifically, plate BR00117006).
|
|
131
131
|
https://github.com/broadinstitute/cellpainting-gallery
|
|
132
|
+
- type: article
|
|
133
|
+
authors:
|
|
134
|
+
- family-names: Blin
|
|
135
|
+
given-names: Guillaume
|
|
136
|
+
- family-names: Sadurska
|
|
137
|
+
given-names: Dominika
|
|
138
|
+
- family-names: Portero Migueles
|
|
139
|
+
given-names: Rafael
|
|
140
|
+
- family-names: Chen
|
|
141
|
+
given-names: Ni
|
|
142
|
+
- family-names: Watson
|
|
143
|
+
given-names: James A.
|
|
144
|
+
- family-names: Lowell
|
|
145
|
+
given-names: Sally
|
|
146
|
+
title: "Nessys: A new set of tools for the automated detection of nuclei within intact tissues and dense 3D cultures"
|
|
147
|
+
journal: PLoS Biology
|
|
148
|
+
volume: "17"
|
|
149
|
+
issue: "8"
|
|
150
|
+
pages: e3000388
|
|
151
|
+
year: 2019
|
|
152
|
+
doi: "10.1371/journal.pbio.3000388"
|
|
153
|
+
url: "https://doi.org/10.1371/journal.pbio.3000388"
|
|
154
|
+
notes: >
|
|
155
|
+
This work used the file "6001240_labels.zarr" from the DISCEPTS imaging
|
|
156
|
+
dataset, available through the Image Data Resource (IDR) under accession
|
|
157
|
+
number idr0062.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: napari-ome-arrow
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: A Napari plugin for OME-Arrow and OME-Parquet bioimage data
|
|
5
5
|
License:
|
|
6
6
|
Copyright (c) 2025, Dave Bunten
|
|
@@ -51,7 +51,7 @@ Description-Content-Type: text/markdown
|
|
|
51
51
|
License-File: LICENSE
|
|
52
52
|
Requires-Dist: magicgui
|
|
53
53
|
Requires-Dist: numpy
|
|
54
|
-
Requires-Dist: ome-arrow>=0.0.
|
|
54
|
+
Requires-Dist: ome-arrow>=0.0.3
|
|
55
55
|
Requires-Dist: qtpy>=2.4
|
|
56
56
|
Requires-Dist: scikit-image
|
|
57
57
|
Provides-Extra: all
|
|
@@ -65,6 +65,8 @@ Provides-Extra: pyside6
|
|
|
65
65
|
Requires-Dist: napari[pyside6]; extra == "pyside6"
|
|
66
66
|
Requires-Dist: pyside6>=6.6; extra == "pyside6"
|
|
67
67
|
Requires-Dist: qtpy>=2.4; extra == "pyside6"
|
|
68
|
+
Provides-Extra: vortex
|
|
69
|
+
Requires-Dist: vortex-data; extra == "vortex"
|
|
68
70
|
Dynamic: license-file
|
|
69
71
|
|
|
70
72
|
# napari-ome-arrow
|
|
@@ -83,6 +85,7 @@ It provides a single, explicit pathway for loading OME-style bioimage data:
|
|
|
83
85
|
- **OME-TIFF** (`.ome.tif`, `.ome.tiff`, `.tif`, `.tiff`)
|
|
84
86
|
- **OME-Zarr** (`.ome.zarr`, `.zarr` stores and URLs)
|
|
85
87
|
- **OME-Parquet** (`.ome.parquet`, `.parquet`, `.pq`)
|
|
88
|
+
- **OME-Vortex** (`.ome.vortex`, `.vortex`)
|
|
86
89
|
- **Bio-Formats–style stack patterns** (paths containing `<`, `>`, or `*`)
|
|
87
90
|
- A simple **`.npy` fallback** for quick testing / ad-hoc arrays
|
|
88
91
|
|
|
@@ -127,8 +130,11 @@ It provides a single, explicit pathway for loading OME-style bioimage data:
|
|
|
127
130
|
- respects `NAPARI_OME_ARROW_LAYER_TYPE`, and
|
|
128
131
|
- defaults to `"image"` if the variable is not set.
|
|
129
132
|
|
|
130
|
-
- ✅ **Grid view for multi-row OME-Parquet**
|
|
131
|
-
When a Parquet file contains multiple OME-Arrow rows, each row is loaded as its own layer and the viewer is switched to napari’s grid mode. Set `NAPARI_OME_ARROW_PARQUET_COLUMN` to pick which image column to visualize.
|
|
133
|
+
- ✅ **Grid view for multi-row OME-Parquet / OME-Vortex**
|
|
134
|
+
When a Parquet or Vortex file contains multiple OME-Arrow rows, each row is loaded as its own layer and the viewer is switched to napari’s grid mode. Set `NAPARI_OME_ARROW_PARQUET_COLUMN` or `NAPARI_OME_ARROW_VORTEX_COLUMN` to pick which image column to visualize.
|
|
135
|
+
|
|
136
|
+
- ✅ **Stack scale prompt + override**
|
|
137
|
+
When loading image stacks (stack patterns), napari prompts for voxel spacing only if no scale metadata or `NAPARI_OME_ARROW_STACK_SCALE` override is present and a Qt UI is available (headless runs skip the prompt). To avoid the prompt, set `NAPARI_OME_ARROW_STACK_SCALE` using `Z,Y,X` or `T,C,Z,Y,X`.
|
|
132
138
|
|
|
133
139
|
______________________________________________________________________
|
|
134
140
|
|
|
@@ -154,13 +160,19 @@ To install latest development version :
|
|
|
154
160
|
pip install git+https://github.com/wayscience/napari-ome-arrow.git
|
|
155
161
|
```
|
|
156
162
|
|
|
163
|
+
To enable OME-Vortex support via OME-Arrow, install the optional extra:
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
pip install "napari-ome-arrow[vortex]"
|
|
167
|
+
```
|
|
168
|
+
|
|
157
169
|
## Usage
|
|
158
170
|
|
|
159
171
|
### From the napari GUI
|
|
160
172
|
|
|
161
173
|
1. Install the plugin (see above).
|
|
162
174
|
1. Start napari.
|
|
163
|
-
1. Drag and drop an OME-TIFF, OME-Zarr, OME-Parquet file,
|
|
175
|
+
1. Drag and drop an OME-TIFF, OME-Zarr, OME-Parquet, OME-Vortex file, a stack pattern, or a multi-select stack (e.g. `img_000.tif` ... `img_123.tif`) into the viewer.
|
|
164
176
|
1. When prompted, choose **Image** or **Labels**.
|
|
165
177
|
|
|
166
178
|
The plugin will:
|
|
@@ -168,6 +180,7 @@ The plugin will:
|
|
|
168
180
|
- load the data through `OMEArrow`,
|
|
169
181
|
- map channels and axes appropriately, and
|
|
170
182
|
- automatically switch to 3D if there is a Z-stack.
|
|
183
|
+
- When multiple files look like a numbered stack, treat them as a single stack rather than independent layers.
|
|
171
184
|
|
|
172
185
|
### From the command line
|
|
173
186
|
|
|
@@ -184,6 +197,12 @@ NAPARI_OME_ARROW_LAYER_TYPE=labels napari my_labels.ome.parquet
|
|
|
184
197
|
NAPARI_OME_ARROW_LAYER_TYPE=image \\
|
|
185
198
|
NAPARI_OME_ARROW_PARQUET_COLUMN=Image_FileName_OrigDNA_OMEArrow_ORIG \\
|
|
186
199
|
napari tests/data/cytodataframe/BR00117006.ome.parquet
|
|
200
|
+
|
|
201
|
+
# Prefill stack voxel spacing for stack patterns (Z,Y,X or T,C,Z,Y,X)
|
|
202
|
+
NAPARI_OME_ARROW_STACK_SCALE=1.0,0.108,0.108 napari "stack/z<000-120>.tif"
|
|
203
|
+
|
|
204
|
+
# Prefill stack voxel spacing for multi-file stacks (use a pattern or glob on CLI)
|
|
205
|
+
NAPARI_OME_ARROW_STACK_SCALE=1.0,0.108,0.108 napari "stack/img_<000-120>.tif"
|
|
187
206
|
```
|
|
188
207
|
|
|
189
208
|
## Contributing
|
|
@@ -14,6 +14,7 @@ It provides a single, explicit pathway for loading OME-style bioimage data:
|
|
|
14
14
|
- **OME-TIFF** (`.ome.tif`, `.ome.tiff`, `.tif`, `.tiff`)
|
|
15
15
|
- **OME-Zarr** (`.ome.zarr`, `.zarr` stores and URLs)
|
|
16
16
|
- **OME-Parquet** (`.ome.parquet`, `.parquet`, `.pq`)
|
|
17
|
+
- **OME-Vortex** (`.ome.vortex`, `.vortex`)
|
|
17
18
|
- **Bio-Formats–style stack patterns** (paths containing `<`, `>`, or `*`)
|
|
18
19
|
- A simple **`.npy` fallback** for quick testing / ad-hoc arrays
|
|
19
20
|
|
|
@@ -58,8 +59,11 @@ It provides a single, explicit pathway for loading OME-style bioimage data:
|
|
|
58
59
|
- respects `NAPARI_OME_ARROW_LAYER_TYPE`, and
|
|
59
60
|
- defaults to `"image"` if the variable is not set.
|
|
60
61
|
|
|
61
|
-
- ✅ **Grid view for multi-row OME-Parquet**
|
|
62
|
-
When a Parquet file contains multiple OME-Arrow rows, each row is loaded as its own layer and the viewer is switched to napari’s grid mode. Set `NAPARI_OME_ARROW_PARQUET_COLUMN` to pick which image column to visualize.
|
|
62
|
+
- ✅ **Grid view for multi-row OME-Parquet / OME-Vortex**
|
|
63
|
+
When a Parquet or Vortex file contains multiple OME-Arrow rows, each row is loaded as its own layer and the viewer is switched to napari’s grid mode. Set `NAPARI_OME_ARROW_PARQUET_COLUMN` or `NAPARI_OME_ARROW_VORTEX_COLUMN` to pick which image column to visualize.
|
|
64
|
+
|
|
65
|
+
- ✅ **Stack scale prompt + override**
|
|
66
|
+
When loading image stacks (stack patterns), napari prompts for voxel spacing only if no scale metadata or `NAPARI_OME_ARROW_STACK_SCALE` override is present and a Qt UI is available (headless runs skip the prompt). To avoid the prompt, set `NAPARI_OME_ARROW_STACK_SCALE` using `Z,Y,X` or `T,C,Z,Y,X`.
|
|
63
67
|
|
|
64
68
|
______________________________________________________________________
|
|
65
69
|
|
|
@@ -85,13 +89,19 @@ To install latest development version :
|
|
|
85
89
|
pip install git+https://github.com/wayscience/napari-ome-arrow.git
|
|
86
90
|
```
|
|
87
91
|
|
|
92
|
+
To enable OME-Vortex support via OME-Arrow, install the optional extra:
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pip install "napari-ome-arrow[vortex]"
|
|
96
|
+
```
|
|
97
|
+
|
|
88
98
|
## Usage
|
|
89
99
|
|
|
90
100
|
### From the napari GUI
|
|
91
101
|
|
|
92
102
|
1. Install the plugin (see above).
|
|
93
103
|
1. Start napari.
|
|
94
|
-
1. Drag and drop an OME-TIFF, OME-Zarr, OME-Parquet file,
|
|
104
|
+
1. Drag and drop an OME-TIFF, OME-Zarr, OME-Parquet, OME-Vortex file, a stack pattern, or a multi-select stack (e.g. `img_000.tif` ... `img_123.tif`) into the viewer.
|
|
95
105
|
1. When prompted, choose **Image** or **Labels**.
|
|
96
106
|
|
|
97
107
|
The plugin will:
|
|
@@ -99,6 +109,7 @@ The plugin will:
|
|
|
99
109
|
- load the data through `OMEArrow`,
|
|
100
110
|
- map channels and axes appropriately, and
|
|
101
111
|
- automatically switch to 3D if there is a Z-stack.
|
|
112
|
+
- When multiple files look like a numbered stack, treat them as a single stack rather than independent layers.
|
|
102
113
|
|
|
103
114
|
### From the command line
|
|
104
115
|
|
|
@@ -115,6 +126,12 @@ NAPARI_OME_ARROW_LAYER_TYPE=labels napari my_labels.ome.parquet
|
|
|
115
126
|
NAPARI_OME_ARROW_LAYER_TYPE=image \\
|
|
116
127
|
NAPARI_OME_ARROW_PARQUET_COLUMN=Image_FileName_OrigDNA_OMEArrow_ORIG \\
|
|
117
128
|
napari tests/data/cytodataframe/BR00117006.ome.parquet
|
|
129
|
+
|
|
130
|
+
# Prefill stack voxel spacing for stack patterns (Z,Y,X or T,C,Z,Y,X)
|
|
131
|
+
NAPARI_OME_ARROW_STACK_SCALE=1.0,0.108,0.108 napari "stack/z<000-120>.tif"
|
|
132
|
+
|
|
133
|
+
# Prefill stack voxel spacing for multi-file stacks (use a pattern or glob on CLI)
|
|
134
|
+
NAPARI_OME_ARROW_STACK_SCALE=1.0,0.108,0.108 napari "stack/img_<000-120>.tif"
|
|
118
135
|
```
|
|
119
136
|
|
|
120
137
|
## Contributing
|
|
@@ -29,7 +29,7 @@ dynamic = [ "version" ]
|
|
|
29
29
|
dependencies = [
|
|
30
30
|
"magicgui",
|
|
31
31
|
"numpy",
|
|
32
|
-
"ome-arrow>=0.0.
|
|
32
|
+
"ome-arrow>=0.0.3",
|
|
33
33
|
"qtpy>=2.4",
|
|
34
34
|
"scikit-image",
|
|
35
35
|
]
|
|
@@ -39,6 +39,7 @@ optional-dependencies.all = [ "napari", "qtpy>=2.4" ]
|
|
|
39
39
|
optional-dependencies.pyqt6 = [ "napari[pyqt6]", "pyqt6>=6.6", "qtpy>=2.4" ]
|
|
40
40
|
# Let users choose their Qt backend explicitly:
|
|
41
41
|
optional-dependencies.pyside6 = [ "napari[pyside6]", "pyside6>=6.6", "qtpy>=2.4" ]
|
|
42
|
+
optional-dependencies.vortex = [ "vortex-data" ]
|
|
42
43
|
urls."Bug Tracker" = "https://github.com/wayscience/napari-ome-arrow/issues"
|
|
43
44
|
urls."Documentation" = "https://github.com/wayscience/napari-ome-arrow#README.md"
|
|
44
45
|
urls."Source Code" = "https://github.com/wayscience/napari-ome-arrow"
|
|
@@ -58,13 +59,13 @@ dev = [
|
|
|
58
59
|
[tool.setuptools]
|
|
59
60
|
include-package-data = true
|
|
60
61
|
|
|
62
|
+
[tool.setuptools.package-data]
|
|
63
|
+
"*" = [ "*.yaml" ]
|
|
64
|
+
|
|
61
65
|
[tool.setuptools.packages.find]
|
|
62
66
|
where = [ "src" ]
|
|
63
67
|
exclude = [ "*.tests", "*.tests.*", "tests*" ]
|
|
64
68
|
|
|
65
|
-
[tool.setuptools.package-data]
|
|
66
|
-
"*" = [ "*.yaml" ]
|
|
67
|
-
|
|
68
69
|
[tool.setuptools_scm]
|
|
69
70
|
write_to = "src/napari_ome_arrow/_version.py"
|
|
70
71
|
fallback_version = "0.0.1+nogit"
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Minimal napari reader for OME-Arrow sources (stack patterns, OME-Zarr, OME-Parquet,
|
|
3
|
+
OME-Vortex, OME-TIFF) plus a fallback .npy example.
|
|
4
|
+
|
|
5
|
+
Behavior:
|
|
6
|
+
* If NAPARI_OME_ARROW_LAYER_TYPE is set to "image" or "labels",
|
|
7
|
+
that choice is used.
|
|
8
|
+
* Otherwise, in a GUI/Qt context, the user is prompted with a modal
|
|
9
|
+
dialog asking whether to load as image or labels.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import warnings
|
|
16
|
+
from collections.abc import Callable, Sequence
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from ._reader_infer import ( # noqa: F401
|
|
20
|
+
_infer_layer_mode_from_record,
|
|
21
|
+
_infer_layer_mode_from_source,
|
|
22
|
+
_looks_like_ome_source,
|
|
23
|
+
_normalize_image_type,
|
|
24
|
+
)
|
|
25
|
+
from ._reader_napari import ( # noqa: F401
|
|
26
|
+
_maybe_set_viewer_3d,
|
|
27
|
+
_strip_channel_axis,
|
|
28
|
+
)
|
|
29
|
+
from ._reader_omearrow import ( # noqa: F401
|
|
30
|
+
_read_one,
|
|
31
|
+
_read_parquet_rows,
|
|
32
|
+
_read_vortex_rows,
|
|
33
|
+
)
|
|
34
|
+
from ._reader_stack import ( # noqa: F401
|
|
35
|
+
_channel_names_from_pattern,
|
|
36
|
+
_collect_stack_files,
|
|
37
|
+
_infer_stack_scale_from_pattern,
|
|
38
|
+
_parse_stack_scale,
|
|
39
|
+
_prompt_stack_pattern,
|
|
40
|
+
_prompt_stack_scale,
|
|
41
|
+
_read_rgb_stack_pattern,
|
|
42
|
+
_replace_channel_placeholder,
|
|
43
|
+
_stack_default_dim_for_pattern,
|
|
44
|
+
_suggest_stack_pattern,
|
|
45
|
+
)
|
|
46
|
+
from ._reader_types import LayerData, PathLike
|
|
47
|
+
|
|
48
|
+
__all__ = [
|
|
49
|
+
"napari_get_reader",
|
|
50
|
+
"reader_function",
|
|
51
|
+
"_infer_layer_mode_from_record",
|
|
52
|
+
"_infer_layer_mode_from_source",
|
|
53
|
+
"_looks_like_ome_source",
|
|
54
|
+
"_normalize_image_type",
|
|
55
|
+
"_maybe_set_viewer_3d",
|
|
56
|
+
"_strip_channel_axis",
|
|
57
|
+
"_read_one",
|
|
58
|
+
"_read_parquet_rows",
|
|
59
|
+
"_read_vortex_rows",
|
|
60
|
+
"_channel_names_from_pattern",
|
|
61
|
+
"_collect_stack_files",
|
|
62
|
+
"_infer_stack_scale_from_pattern",
|
|
63
|
+
"_parse_stack_scale",
|
|
64
|
+
"_prompt_stack_pattern",
|
|
65
|
+
"_prompt_stack_scale",
|
|
66
|
+
"_read_rgb_stack_pattern",
|
|
67
|
+
"_replace_channel_placeholder",
|
|
68
|
+
"_stack_default_dim_for_pattern",
|
|
69
|
+
"_suggest_stack_pattern",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
# --------------------------------------------------------------------- #
|
|
73
|
+
# Mode selection (env var + GUI prompt)
|
|
74
|
+
# --------------------------------------------------------------------- #
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _get_layer_mode(
|
|
78
|
+
sample_path: str, *, image_type_hint: str | None = None
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Decide whether to load as "image" or "labels".
|
|
81
|
+
|
|
82
|
+
Priority:
|
|
83
|
+
1. `NAPARI_OME_ARROW_LAYER_TYPE` env var (image/labels).
|
|
84
|
+
2. If in a Qt GUI context, show a modal dialog asking the user.
|
|
85
|
+
3. Otherwise, default to "image".
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
sample_path: Path used for prompt labeling.
|
|
89
|
+
image_type_hint: Optional inferred mode from metadata.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
The selected mode, "image" or "labels".
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
RuntimeError: If the environment variable is invalid or the user
|
|
96
|
+
cancels the dialog.
|
|
97
|
+
"""
|
|
98
|
+
# Env var override (used in headless/batch workflows).
|
|
99
|
+
mode = os.environ.get("NAPARI_OME_ARROW_LAYER_TYPE")
|
|
100
|
+
if mode is not None:
|
|
101
|
+
mode = mode.lower()
|
|
102
|
+
if mode in {"image", "labels"}:
|
|
103
|
+
return mode
|
|
104
|
+
raise RuntimeError(
|
|
105
|
+
f"Invalid NAPARI_OME_ARROW_LAYER_TYPE={mode!r}; expected 'image' or 'labels'."
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Metadata hint (best effort).
|
|
109
|
+
if image_type_hint in {"image", "labels"}:
|
|
110
|
+
return image_type_hint
|
|
111
|
+
|
|
112
|
+
# No env var → try to prompt in GUI context
|
|
113
|
+
try:
|
|
114
|
+
from qtpy import QtWidgets
|
|
115
|
+
except Exception:
|
|
116
|
+
# no Qt, probably headless: default to image
|
|
117
|
+
warnings.warn(
|
|
118
|
+
"NAPARI_OME_ARROW_LAYER_TYPE not set and Qt not available; "
|
|
119
|
+
"defaulting to 'image'.",
|
|
120
|
+
stacklevel=2,
|
|
121
|
+
)
|
|
122
|
+
return "image"
|
|
123
|
+
|
|
124
|
+
app = QtWidgets.QApplication.instance()
|
|
125
|
+
if app is None:
|
|
126
|
+
# Again, likely headless or non-Qt usage
|
|
127
|
+
warnings.warn(
|
|
128
|
+
"NAPARI_OME_ARROW_LAYER_TYPE not set and no QApplication instance; "
|
|
129
|
+
"defaulting to 'image'.",
|
|
130
|
+
stacklevel=2,
|
|
131
|
+
)
|
|
132
|
+
return "image"
|
|
133
|
+
|
|
134
|
+
# Build a simple modal choice dialog
|
|
135
|
+
msg = QtWidgets.QMessageBox()
|
|
136
|
+
msg.setWindowTitle("napari-ome-arrow: choose layer type")
|
|
137
|
+
msg.setText(
|
|
138
|
+
f"<p align='left'>How should '{Path(sample_path).name}' be loaded?<br><br>"
|
|
139
|
+
"You can also set NAPARI_OME_ARROW_LAYER_TYPE=image or labels to skip this prompt.</p>"
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# Use ActionRole for ALL buttons so Qt does NOT reorder them
|
|
143
|
+
image_button = msg.addButton("Image", QtWidgets.QMessageBox.ActionRole)
|
|
144
|
+
labels_button = msg.addButton("Labels", QtWidgets.QMessageBox.ActionRole)
|
|
145
|
+
cancel_button = msg.addButton("Cancel", QtWidgets.QMessageBox.ActionRole)
|
|
146
|
+
|
|
147
|
+
# If you want Esc to behave as Cancel:
|
|
148
|
+
msg.setEscapeButton(cancel_button)
|
|
149
|
+
|
|
150
|
+
msg.exec_()
|
|
151
|
+
clicked = msg.clickedButton()
|
|
152
|
+
|
|
153
|
+
if clicked is labels_button:
|
|
154
|
+
return "labels"
|
|
155
|
+
if clicked is image_button:
|
|
156
|
+
return "image"
|
|
157
|
+
|
|
158
|
+
raise RuntimeError("User cancelled napari-ome-arrow load dialog.")
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# --------------------------------------------------------------------- #
|
|
162
|
+
# napari entry point: napari_get_reader
|
|
163
|
+
# --------------------------------------------------------------------- #
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def napari_get_reader(
|
|
167
|
+
path: PathLike | Sequence[PathLike],
|
|
168
|
+
) -> Callable[[PathLike | Sequence[PathLike]], list[LayerData]] | None:
|
|
169
|
+
"""Return a reader callable for napari if the path is supported.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
path: A single path or a sequence of paths provided by napari.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Reader callable if the path is supported, otherwise None.
|
|
176
|
+
"""
|
|
177
|
+
# napari may pass a list/tuple or a single path
|
|
178
|
+
first = str(path[0] if isinstance(path, (list, tuple)) else path).strip()
|
|
179
|
+
|
|
180
|
+
if _looks_like_ome_source(first):
|
|
181
|
+
return reader_function
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
# --------------------------------------------------------------------- #
|
|
186
|
+
# Reader implementation: reader_function
|
|
187
|
+
# --------------------------------------------------------------------- #
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def reader_function(
|
|
191
|
+
path: PathLike | Sequence[PathLike],
|
|
192
|
+
) -> list[LayerData]:
|
|
193
|
+
"""Read one or more paths into napari layer data.
|
|
194
|
+
|
|
195
|
+
The user may be prompted (or an env var used) to decide "image" vs
|
|
196
|
+
"labels".
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
path: A single path or a sequence of paths provided by napari.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of layer tuples for napari.
|
|
203
|
+
|
|
204
|
+
Raises:
|
|
205
|
+
ValueError: If no readable inputs are found.
|
|
206
|
+
"""
|
|
207
|
+
paths: list[str] = [
|
|
208
|
+
str(p) for p in (path if isinstance(path, (list, tuple)) else [path])
|
|
209
|
+
]
|
|
210
|
+
layers: list[LayerData] = []
|
|
211
|
+
|
|
212
|
+
# Offer a stack pattern prompt when multiple compatible files are present.
|
|
213
|
+
stack_selection = _collect_stack_files(paths)
|
|
214
|
+
stack_pattern = None
|
|
215
|
+
if stack_selection is not None:
|
|
216
|
+
stack_pattern = _prompt_stack_pattern(*stack_selection)
|
|
217
|
+
|
|
218
|
+
# Use the original first path as context for the dialog label
|
|
219
|
+
mode_hint = None
|
|
220
|
+
if stack_pattern is not None:
|
|
221
|
+
stack_default_dim = _stack_default_dim_for_pattern(stack_pattern)
|
|
222
|
+
mode_hint = _infer_layer_mode_from_source(
|
|
223
|
+
stack_pattern, stack_default_dim=stack_default_dim
|
|
224
|
+
)
|
|
225
|
+
elif paths:
|
|
226
|
+
mode_hint = _infer_layer_mode_from_source(paths[0])
|
|
227
|
+
try:
|
|
228
|
+
mode = _get_layer_mode(
|
|
229
|
+
sample_path=paths[0], image_type_hint=mode_hint
|
|
230
|
+
) # 'image' or 'labels'
|
|
231
|
+
except RuntimeError as e:
|
|
232
|
+
# If user canceled the dialog, propagate a clean error for napari
|
|
233
|
+
raise ValueError(str(e)) from e
|
|
234
|
+
|
|
235
|
+
if stack_pattern is not None:
|
|
236
|
+
try:
|
|
237
|
+
channel_names = _channel_names_from_pattern(
|
|
238
|
+
stack_pattern, stack_default_dim
|
|
239
|
+
)
|
|
240
|
+
resolved_stack_scale = None
|
|
241
|
+
if mode == "image" and channel_names and len(channel_names) > 1:
|
|
242
|
+
# Split each channel into separate layers for stack patterns.
|
|
243
|
+
inferred_stack_scale = _infer_stack_scale_from_pattern(
|
|
244
|
+
stack_pattern, stack_default_dim
|
|
245
|
+
)
|
|
246
|
+
env_scale = os.environ.get("NAPARI_OME_ARROW_STACK_SCALE")
|
|
247
|
+
if env_scale:
|
|
248
|
+
try:
|
|
249
|
+
resolved_stack_scale = _parse_stack_scale(env_scale)
|
|
250
|
+
except ValueError as exc:
|
|
251
|
+
warnings.warn(
|
|
252
|
+
f"Invalid NAPARI_OME_ARROW_STACK_SCALE '{env_scale}': {exc}.",
|
|
253
|
+
stacklevel=2,
|
|
254
|
+
)
|
|
255
|
+
resolved_stack_scale = None
|
|
256
|
+
if (
|
|
257
|
+
resolved_stack_scale is None
|
|
258
|
+
and inferred_stack_scale is None
|
|
259
|
+
):
|
|
260
|
+
resolved_stack_scale = _prompt_stack_scale(
|
|
261
|
+
sample_path=stack_pattern, default_scale=None
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
for label in channel_names:
|
|
265
|
+
channel_pattern = _replace_channel_placeholder(
|
|
266
|
+
stack_pattern, label, stack_default_dim
|
|
267
|
+
)
|
|
268
|
+
channel_default_dim = _stack_default_dim_for_pattern(
|
|
269
|
+
channel_pattern
|
|
270
|
+
)
|
|
271
|
+
try:
|
|
272
|
+
arr, add_kwargs, layer_type = _read_one(
|
|
273
|
+
channel_pattern,
|
|
274
|
+
mode=mode,
|
|
275
|
+
stack_default_dim=channel_default_dim,
|
|
276
|
+
stack_scale_override=resolved_stack_scale,
|
|
277
|
+
)
|
|
278
|
+
arr, add_kwargs = _strip_channel_axis(arr, add_kwargs)
|
|
279
|
+
except Exception as exc:
|
|
280
|
+
try:
|
|
281
|
+
arr, is_rgb = _read_rgb_stack_pattern(
|
|
282
|
+
channel_pattern
|
|
283
|
+
)
|
|
284
|
+
except Exception:
|
|
285
|
+
warnings.warn(
|
|
286
|
+
f"Failed to read channel '{label}' from stack "
|
|
287
|
+
f"pattern '{stack_pattern}': {exc}",
|
|
288
|
+
stacklevel=2,
|
|
289
|
+
)
|
|
290
|
+
continue
|
|
291
|
+
add_kwargs = {"name": Path(channel_pattern).name}
|
|
292
|
+
if is_rgb:
|
|
293
|
+
add_kwargs["rgb"] = True
|
|
294
|
+
layer_type = "image"
|
|
295
|
+
|
|
296
|
+
add_kwargs["name"] = (
|
|
297
|
+
f"{add_kwargs.get('name', label)}[{label}]"
|
|
298
|
+
)
|
|
299
|
+
if not add_kwargs.get("rgb"):
|
|
300
|
+
_maybe_set_viewer_3d(arr)
|
|
301
|
+
layers.append((arr, add_kwargs, layer_type))
|
|
302
|
+
else:
|
|
303
|
+
# Single-layer stack read.
|
|
304
|
+
arr, add_kwargs, layer_type = _read_one(
|
|
305
|
+
stack_pattern,
|
|
306
|
+
mode=mode,
|
|
307
|
+
stack_default_dim=stack_default_dim,
|
|
308
|
+
)
|
|
309
|
+
layers.append((arr, add_kwargs, layer_type))
|
|
310
|
+
except Exception as e:
|
|
311
|
+
warnings.warn(
|
|
312
|
+
f"Failed to read stack pattern '{stack_pattern}': {e}. "
|
|
313
|
+
"Loading files individually instead.",
|
|
314
|
+
stacklevel=2,
|
|
315
|
+
)
|
|
316
|
+
else:
|
|
317
|
+
return layers
|
|
318
|
+
elif stack_selection is not None and len(paths) == 1:
|
|
319
|
+
paths = [str(p) for p in stack_selection[0]]
|
|
320
|
+
|
|
321
|
+
for src in paths:
|
|
322
|
+
try:
|
|
323
|
+
parquet_layers = _read_parquet_rows(src, mode)
|
|
324
|
+
if parquet_layers is not None:
|
|
325
|
+
layers.extend(parquet_layers)
|
|
326
|
+
continue
|
|
327
|
+
vortex_layers = _read_vortex_rows(src, mode)
|
|
328
|
+
if vortex_layers is not None:
|
|
329
|
+
layers.extend(vortex_layers)
|
|
330
|
+
continue
|
|
331
|
+
layers.append(_read_one(src, mode=mode))
|
|
332
|
+
except Exception as e:
|
|
333
|
+
warnings.warn(
|
|
334
|
+
f"Failed to read '{src}' with napari-ome-arrow: {e}",
|
|
335
|
+
stacklevel=2,
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
if not layers:
|
|
339
|
+
raise ValueError("No readable inputs found for given path(s).")
|
|
340
|
+
return layers
|