patchworks 1.1.1__tar.gz → 1.2.1__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.
- {patchworks-1.1.1 → patchworks-1.2.1}/PKG-INFO +6 -1
- {patchworks-1.1.1 → patchworks-1.2.1}/cliff.toml +6 -3
- patchworks-1.2.1/docs/api/plugins/dog.md +3 -0
- patchworks-1.2.1/docs/examples/dog.md +110 -0
- patchworks-1.2.1/docs/examples/dog.py +24 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/guide/snakemake.md +22 -1
- {patchworks-1.1.1 → patchworks-1.2.1}/mkdocs.yml +2 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/pyproject.toml +14 -1
- patchworks-1.2.1/src/patchworks/plugins/dog.py +202 -0
- patchworks-1.2.1/tests/test_dog.py +49 -0
- patchworks-1.2.1/workflow/config/config_cilia.yaml +50 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/config/multi.yaml +11 -2
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/pixi.toml +3 -1
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/scripts/run_multi.py +50 -16
- {patchworks-1.1.1 → patchworks-1.2.1}/.github/workflows/docs.yml +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/.github/workflows/lint.yml +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/.github/workflows/release.yml +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/.gitignore +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/.markdownlint-cli2.yaml +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/LICENSE +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/README.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/api/chunks.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/api/cluster.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/api/io.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/api/plugins/napari.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/api/relabel.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/api/tile_process.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/assets/logo.png +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/examples/custom.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/examples/custom_method.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/examples/standalone_merge.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/examples/stardist.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/examples/stardist_2d.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/getting_started.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/guide/merging.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/guide/ome_zarr_napari.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/guide/performance.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/guide/pitfalls.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/guide/skip_empty.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/guide/tiling.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/docs/index.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/__init__.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/_chunks.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/_cluster.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/_core.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/_distributed.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/_io.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/_merge.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/_relabel.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/_relations.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/tests/test_core.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/tests/test_distributed.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/tests/test_napari.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/tests/test_ome_zarr.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/tests/test_relations.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/README.md +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/Snakefile +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/config/config.yaml +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/rules/common.smk +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/rules/convert.smk +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/rules/merge.smk +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/rules/segment.smk +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/scripts/_pw.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/scripts/convert.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/scripts/merge.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-1.1.1 → patchworks-1.2.1}/workflow/scripts/segment_tile.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: patchworks
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.1
|
|
4
4
|
Summary: Tiled processing of arbitrarily large images with globally consistent labels
|
|
5
5
|
Project-URL: Homepage, https://github.com/imcf/patchworks
|
|
6
6
|
Project-URL: Issues, https://github.com/imcf/patchworks/issues
|
|
@@ -39,6 +39,7 @@ Requires-Dist: napari[all]>=0.7.1; extra == 'all'
|
|
|
39
39
|
Requires-Dist: numpy<2.5; extra == 'all'
|
|
40
40
|
Requires-Dist: nvidia-ml-py; extra == 'all'
|
|
41
41
|
Requires-Dist: psutil; extra == 'all'
|
|
42
|
+
Requires-Dist: pyqt6<6.10; extra == 'all'
|
|
42
43
|
Requires-Dist: scikit-image; extra == 'all'
|
|
43
44
|
Requires-Dist: tqdm; extra == 'all'
|
|
44
45
|
Provides-Extra: bioio
|
|
@@ -64,6 +65,8 @@ Requires-Dist: tqdm; extra == 'dev'
|
|
|
64
65
|
Provides-Extra: docs
|
|
65
66
|
Requires-Dist: mkdocs-material>=9.0; extra == 'docs'
|
|
66
67
|
Requires-Dist: mkdocstrings[python]>=0.24; extra == 'docs'
|
|
68
|
+
Provides-Extra: dog
|
|
69
|
+
Requires-Dist: pycudadecon; extra == 'dog'
|
|
67
70
|
Provides-Extra: gpu
|
|
68
71
|
Requires-Dist: nvidia-ml-py; extra == 'gpu'
|
|
69
72
|
Provides-Extra: imaris
|
|
@@ -78,7 +81,9 @@ Requires-Dist: lxml-html-clean; extra == 'napari'
|
|
|
78
81
|
Requires-Dist: napari-chunked-regionprops>=0.1.0; extra == 'napari'
|
|
79
82
|
Requires-Dist: napari[all]>=0.7.1; extra == 'napari'
|
|
80
83
|
Requires-Dist: numpy<2.5; extra == 'napari'
|
|
84
|
+
Requires-Dist: pyqt6<6.10; extra == 'napari'
|
|
81
85
|
Provides-Extra: workflow
|
|
86
|
+
Requires-Dist: openpyxl; extra == 'workflow'
|
|
82
87
|
Requires-Dist: snakemake-executor-plugin-slurm; extra == 'workflow'
|
|
83
88
|
Requires-Dist: snakemake>=8; extra == 'workflow'
|
|
84
89
|
Description-Content-Type: text/markdown
|
|
@@ -8,11 +8,14 @@ body = """
|
|
|
8
8
|
- {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | split(pat="\n") | first }}
|
|
9
9
|
{% endfor %}
|
|
10
10
|
{% endfor %}\
|
|
11
|
-
{%
|
|
11
|
+
{% set contributor_commits = commits | unique(attribute="author.name") %}
|
|
12
|
+
{% if contributor_commits | length > 0 %}
|
|
12
13
|
### 👥 Contributors
|
|
13
14
|
|
|
14
|
-
{% for
|
|
15
|
-
* @{{
|
|
15
|
+
{% for c in contributor_commits %}\
|
|
16
|
+
{% if c.github.username %}* @{{ c.github.username }}
|
|
17
|
+
{% else %}* {{ c.author.name }}
|
|
18
|
+
{% endif %}\
|
|
16
19
|
{% endfor %}
|
|
17
20
|
{% endif %}\
|
|
18
21
|
"""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Difference of Gaussians (blobs, threads, cilia, …)
|
|
2
|
+
|
|
3
|
+
A lightweight blob/thread detector for structures Cellpose isn't shaped for
|
|
4
|
+
(cilia, spots, fibres): blur twice at different sigmas, subtract, threshold,
|
|
5
|
+
label the connected components. CPU (scipy) by default, GPU (cupy) optional.
|
|
6
|
+
Optionally deconvolve each tile first with
|
|
7
|
+
[pycudadecon](https://github.com/tlambert03/pycudadecon).
|
|
8
|
+
|
|
9
|
+
## Installation
|
|
10
|
+
|
|
11
|
+
`dog_label_fn` itself only needs patchworks' core deps (scipy). The
|
|
12
|
+
deconvolution step needs pycudadecon:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install "patchworks[dog]"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
GPU blur/label (`use_gpu=True`) needs `cupy` too, matching your CUDA version
|
|
19
|
+
(e.g. `pip install cupy-cuda12x`) — not bundled in the `dog` extra since it's
|
|
20
|
+
CUDA-version-specific.
|
|
21
|
+
|
|
22
|
+
## Code
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
import numpy as np
|
|
26
|
+
from patchworks import tile_process
|
|
27
|
+
from patchworks.plugins.dog import dog_label_fn
|
|
28
|
+
|
|
29
|
+
IMAGE = "image.zarr"
|
|
30
|
+
OUTPUT = "labels_dog.zarr"
|
|
31
|
+
|
|
32
|
+
fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
|
|
33
|
+
|
|
34
|
+
tile_process(
|
|
35
|
+
IMAGE,
|
|
36
|
+
fn,
|
|
37
|
+
channel=1,
|
|
38
|
+
tile_shape=(1, 1024, 1024),
|
|
39
|
+
overlap=8, # just needs to cover one object + high_sigma
|
|
40
|
+
write_to=OUTPUT,
|
|
41
|
+
progress=True,
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Picking `low_sigma` / `high_sigma` / `threshold`
|
|
46
|
+
|
|
47
|
+
`dog = blur(low_sigma) - blur(high_sigma)`. `low_sigma` should be about the
|
|
48
|
+
object's radius (denoises without erasing it); `high_sigma` a few times
|
|
49
|
+
larger (models the background to subtract out). `threshold` is applied
|
|
50
|
+
directly to the DoG image — start near the DoG's typical peak value on a
|
|
51
|
+
known-positive region and adjust from there; there's no auto (Otsu-style)
|
|
52
|
+
option, since the DoG image isn't bimodal the way a raw intensity image is.
|
|
53
|
+
|
|
54
|
+
## GPU
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02, use_gpu=True)
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Requires `cupy` (matching your CUDA version, e.g. `pip install cupy-cuda12x`)
|
|
61
|
+
— not a patchworks dependency, install it separately.
|
|
62
|
+
|
|
63
|
+
## With deconvolution first
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
fn = dog_label_fn(
|
|
67
|
+
low_sigma=1.0, high_sigma=3.0, threshold=0.02,
|
|
68
|
+
decon_kwargs=dict(
|
|
69
|
+
psf=psf, dxpsf=xy_scale, dxdata=xy_scale,
|
|
70
|
+
dzpsf=z_scale, dzdata=z_scale,
|
|
71
|
+
wavelength=525, na=1.4, nimm=1.515,
|
|
72
|
+
),
|
|
73
|
+
)
|
|
74
|
+
result = tile_process(IMAGE, fn, tile_shape=(1, 1024, 1024), overlap=32)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
!!! note "Deconvolution always needs a GPU"
|
|
78
|
+
`pycudadecon` is CUDA-only, independent of `dog_label_fn`'s own `use_gpu`
|
|
79
|
+
flag (which only picks the backend for the blur/label steps). A SLURM job
|
|
80
|
+
running this needs a GPU allocated. Widen `overlap` past the PSF support
|
|
81
|
+
so edge tiles keep enough context (a plain intensity/threshold halo is
|
|
82
|
+
too thin).
|
|
83
|
+
|
|
84
|
+
## Using it in the Snakemake workflow
|
|
85
|
+
|
|
86
|
+
No dedicated wiring needed — `patchworks.plugins.dog` exposes a `segment(tile, **kwargs)`
|
|
87
|
+
adapter for the documented [`"custom"` method](../guide/snakemake.md#custom-segmentation-function):
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
method: "custom"
|
|
91
|
+
label_name: "cilia_labels"
|
|
92
|
+
custom:
|
|
93
|
+
module: "patchworks.plugins.dog"
|
|
94
|
+
function: "segment"
|
|
95
|
+
kwargs:
|
|
96
|
+
low_sigma: 1.0
|
|
97
|
+
high_sigma: 3.0
|
|
98
|
+
threshold: 0.02
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
See `workflow/config/config_cilia.yaml` for a full example, including
|
|
102
|
+
deconvolution.
|
|
103
|
+
|
|
104
|
+
## Relating cilia to their cell
|
|
105
|
+
|
|
106
|
+
Segment the cell body with Cellpose and the cilia with `dog_label_fn` as two
|
|
107
|
+
separate `tile_process` runs (same image, same `tile_shape`), then use
|
|
108
|
+
[`label_relations`](../guide/snakemake.md#relating-labels-across-segmentations)
|
|
109
|
+
to map each cilium to the cell it belongs to — see
|
|
110
|
+
`workflow/config/multi.yaml` for the same thing wired up as a cluster job.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Difference-of-Gaussians blob/thread segmentation (e.g. cilia).
|
|
2
|
+
|
|
3
|
+
Blur twice at different sigmas, subtract, threshold, label the connected
|
|
4
|
+
components. CPU (scipy) by default.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from patchworks import tile_process
|
|
8
|
+
from patchworks.plugins.dog import dog_label_fn
|
|
9
|
+
|
|
10
|
+
IMAGE = "image.zarr"
|
|
11
|
+
OUTPUT = "labels_dog.zarr"
|
|
12
|
+
CHANNEL = 1
|
|
13
|
+
|
|
14
|
+
fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
|
|
15
|
+
|
|
16
|
+
tile_process(
|
|
17
|
+
IMAGE,
|
|
18
|
+
fn,
|
|
19
|
+
channel=CHANNEL,
|
|
20
|
+
tile_shape=(1, 1024, 1024),
|
|
21
|
+
overlap=8,
|
|
22
|
+
write_to=OUTPUT,
|
|
23
|
+
progress=True,
|
|
24
|
+
)
|
|
@@ -303,7 +303,7 @@ segmentations:
|
|
|
303
303
|
relations:
|
|
304
304
|
- a: nuclei_labels
|
|
305
305
|
b: cyto_labels
|
|
306
|
-
output: nuclei_to_cyto.
|
|
306
|
+
output: nuclei_to_cyto.xlsx # written into work_dir
|
|
307
307
|
```
|
|
308
308
|
|
|
309
309
|
```bash
|
|
@@ -317,6 +317,19 @@ Every listed segmentation config must share the same `work_dir` (so
|
|
|
317
317
|
script checks this and errors out otherwise. `relations` is optional; omit it
|
|
318
318
|
to just chain segmentations without a relation step.
|
|
319
319
|
|
|
320
|
+
Each `output:` is an Excel workbook (`openpyxl`, part of the `workflow`
|
|
321
|
+
extra) with two sheets:
|
|
322
|
+
|
|
323
|
+
| Sheet | One row per | Columns |
|
|
324
|
+
| --- | --- | --- |
|
|
325
|
+
| `<a>` | every non-background `a` label, **including unmatched ones** | `<a>_id`, `<b>_id` (blank if unmatched), `overlap_voxels`, `overlap_fraction` (0 if unmatched) |
|
|
326
|
+
| `<b>` | every non-background `b` label, **including ones with zero matches** | `<b>_id`, `<a>_count`, `total_overlap_voxels` |
|
|
327
|
+
|
|
328
|
+
Unlike calling `label_relations()` directly (which only returns matched `a`
|
|
329
|
+
labels — see below), the workbook always covers every object in both
|
|
330
|
+
segmentations, so counts (e.g. "how many nuclei have no matching cell",
|
|
331
|
+
"how many cells have zero cilia") aren't silently dropped.
|
|
332
|
+
|
|
320
333
|
Both lists are ordinary lists, so 3+ segmentations work the same way — add
|
|
321
334
|
more entries to `segmentations`, then list whichever pairs to relate. There's
|
|
322
335
|
no automatic "chain": list every pair explicitly, e.g. for nuclei + cyto +
|
|
@@ -324,6 +337,14 @@ membrane you'd add `nuclei_labels -> cyto_labels`, `nuclei_labels ->
|
|
|
324
337
|
membrane_labels`, and `cyto_labels -> membrane_labels` as three separate
|
|
325
338
|
entries under `relations`.
|
|
326
339
|
|
|
340
|
+
The shipped `config/multi.yaml` is actually a three-way example: nuclei +
|
|
341
|
+
cytoplasm (Cellpose) plus cilia (`method: "custom"` ->
|
|
342
|
+
[`patchworks.plugins.dog`](../examples/dog.md), deconvolution + a
|
|
343
|
+
difference-of-Gaussians detector), related both ways (`cilia_labels ->
|
|
344
|
+
cyto_labels` and `cilia_labels -> nuclei_labels`) so you can use whichever
|
|
345
|
+
fits a given dataset. See `config/config_cilia.yaml`. Its deconvolution step
|
|
346
|
+
needs `pip install "patchworks[dog]"` in the segment jobs' environment.
|
|
347
|
+
|
|
327
348
|
## Measurements (fast, whole-volume regionprops)
|
|
328
349
|
|
|
329
350
|
`skimage.measure.regionprops` needs the full labelled + intensity array in
|
|
@@ -48,6 +48,7 @@ nav:
|
|
|
48
48
|
- Cellpose 2-D: examples/cellpose_2d.md
|
|
49
49
|
- Cellpose 3-D: examples/cellpose_3d.md
|
|
50
50
|
- StarDist: examples/stardist.md
|
|
51
|
+
- Difference of Gaussians: examples/dog.md
|
|
51
52
|
- Custom function: examples/custom.md
|
|
52
53
|
- Standalone merge: examples/standalone_merge.md
|
|
53
54
|
- API Reference:
|
|
@@ -59,6 +60,7 @@ nav:
|
|
|
59
60
|
- Cluster helpers: api/cluster.md
|
|
60
61
|
- Plugins:
|
|
61
62
|
- Cellpose: api/plugins/cellpose.md
|
|
63
|
+
- Difference of Gaussians: api/plugins/dog.md
|
|
62
64
|
- OME-ZARR conversion: api/plugins/ome_zarr.md
|
|
63
65
|
- napari viewer: api/plugins/napari.md
|
|
64
66
|
|
|
@@ -43,6 +43,11 @@ io = ["psutil", "tqdm"]
|
|
|
43
43
|
cellpose = ["cellpose>=3.0"]
|
|
44
44
|
cellpose3 = ["cellpose>=3.0,<4"]
|
|
45
45
|
cellpose4 = ["cellpose>=4"]
|
|
46
|
+
# dog enables the deconvolution step in patchworks.plugins.dog (method:
|
|
47
|
+
# "custom" -> segment). cupy, for that plugin's use_gpu=True blur/label
|
|
48
|
+
# path, isn't included here — it's CUDA-version-specific (e.g.
|
|
49
|
+
# cupy-cuda12x), install it separately to match your CUDA version.
|
|
50
|
+
dog = ["pycudadecon"]
|
|
46
51
|
gpu = ["nvidia-ml-py"]
|
|
47
52
|
# bioio enables converting any image format (CZI, LIF, ND2, OME-TIFF, …) to
|
|
48
53
|
# OME-ZARR. Ships the common native readers (faster than going through Java)
|
|
@@ -72,6 +77,11 @@ imaris = ["imaris-ims-file-reader"]
|
|
|
72
77
|
# their backing dask/zarr arrays — the "Measure" dock widget. Formerly
|
|
73
78
|
# napari-dask-ndmeasure; renamed when its engine dropped dask_image.ndmeasure
|
|
74
79
|
# for a chunk-local map/merge that scales with chunk count, not object count.
|
|
80
|
+
# - pyqt6 < 6.10: PyQt6-Qt6 6.10.2's Windows wheel fails to import (`DLL load
|
|
81
|
+
# failed while importing QtWidgets: The specified procedure could not be
|
|
82
|
+
# found`) — confirmed unrelated to napari/qtpy, reproduces on a bare
|
|
83
|
+
# `from PyQt6.QtWidgets import QApplication`. 6.9.1 works; excluding the
|
|
84
|
+
# whole 6.10.x line until a fixed release is confirmed.
|
|
75
85
|
napari = [
|
|
76
86
|
"napari[all]>=0.7.1",
|
|
77
87
|
"numpy<2.5",
|
|
@@ -79,9 +89,12 @@ napari = [
|
|
|
79
89
|
"lxml-html-clean",
|
|
80
90
|
"glasbey",
|
|
81
91
|
"napari-chunked-regionprops>=0.1.0",
|
|
92
|
+
"pyqt6<6.10",
|
|
82
93
|
]
|
|
83
94
|
# workflow runs the Snakemake pipeline (per-tile SLURM jobs across GPUs).
|
|
84
|
-
|
|
95
|
+
# openpyxl -> scripts/run_multi.py writes label_relations() output as an
|
|
96
|
+
# Excel workbook (per-object + per-container sheets), not a plain CSV.
|
|
97
|
+
workflow = ["snakemake>=8", "snakemake-executor-plugin-slurm", "openpyxl"]
|
|
85
98
|
dev = ["pytest", "pytest-cov", "scikit-image", "psutil", "tqdm"]
|
|
86
99
|
docs = ["mkdocs-material>=9.0", "mkdocstrings[python]>=0.24"]
|
|
87
100
|
all = [
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Difference-of-Gaussians plugin for patchworks.
|
|
2
|
+
|
|
3
|
+
Optional deconvolution (pycudadecon) followed by a DoG blob/spot detector:
|
|
4
|
+
threshold the difference of two Gaussian blurs and label connected
|
|
5
|
+
components. CPU (scipy) or GPU (cupy) backed.
|
|
6
|
+
|
|
7
|
+
Usage
|
|
8
|
+
-----
|
|
9
|
+
>>> from patchworks.plugins.dog import dog_label_fn
|
|
10
|
+
>>> from patchworks import tile_process
|
|
11
|
+
>>>
|
|
12
|
+
>>> fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
|
|
13
|
+
>>> result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048),
|
|
14
|
+
... overlap=8, write_to="labels.zarr", progress=True)
|
|
15
|
+
|
|
16
|
+
With deconvolution first (widen ``overlap`` to cover the PSF support):
|
|
17
|
+
|
|
18
|
+
>>> fn = dog_label_fn(
|
|
19
|
+
... low_sigma=1.0, high_sigma=3.0, threshold=0.02,
|
|
20
|
+
... decon_kwargs=dict(psf=psf, dxpsf=xy_scale, dxdata=xy_scale,
|
|
21
|
+
... dzpsf=z_scale, dzdata=z_scale,
|
|
22
|
+
... wavelength=wavelength, na=numerical_aperture,
|
|
23
|
+
... nimm=refractive_index),
|
|
24
|
+
... )
|
|
25
|
+
>>> result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048), overlap=32)
|
|
26
|
+
|
|
27
|
+
For the Snakemake workflow's ``method: "custom"`` (see
|
|
28
|
+
docs/guide/snakemake.md "Custom segmentation function"), use the
|
|
29
|
+
:func:`segment` adapter instead of the factory directly:
|
|
30
|
+
|
|
31
|
+
>>> # custom: {module: "patchworks.plugins.dog", function: "segment",
|
|
32
|
+
>>> # kwargs: {low_sigma: 1.0, high_sigma: 3.0, threshold: 0.02}}
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from functools import partial
|
|
38
|
+
from typing import Any, Callable
|
|
39
|
+
|
|
40
|
+
import numpy as np
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _require_cupy():
|
|
44
|
+
"""Raise an actionable ImportError if cupy is not installed.
|
|
45
|
+
|
|
46
|
+
Returns
|
|
47
|
+
-------
|
|
48
|
+
None
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
import cupy # noqa: F401
|
|
52
|
+
import cupyx.scipy.ndimage # noqa: F401
|
|
53
|
+
except ImportError as exc:
|
|
54
|
+
raise ImportError(
|
|
55
|
+
"cupy is not installed. Install a build matching your CUDA "
|
|
56
|
+
"version (e.g. pip install cupy-cuda12x), or use use_gpu=False "
|
|
57
|
+
"for the CPU (scipy) path."
|
|
58
|
+
) from exc
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _require_pycudadecon():
|
|
62
|
+
"""Raise an actionable ImportError if pycudadecon is not installed.
|
|
63
|
+
|
|
64
|
+
Returns
|
|
65
|
+
-------
|
|
66
|
+
None
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
import pycudadecon # noqa: F401
|
|
70
|
+
except ImportError as exc:
|
|
71
|
+
raise ImportError(
|
|
72
|
+
"pycudadecon is not installed. Install it with:\n"
|
|
73
|
+
" pip install pycudadecon\n"
|
|
74
|
+
"or drop decon_kwargs to skip deconvolution."
|
|
75
|
+
) from exc
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def dog_label_fn(
|
|
79
|
+
low_sigma: float | tuple[float, ...],
|
|
80
|
+
high_sigma: float | tuple[float, ...],
|
|
81
|
+
threshold: float,
|
|
82
|
+
*,
|
|
83
|
+
use_gpu: bool = False,
|
|
84
|
+
decon_kwargs: dict[str, Any] | None = None,
|
|
85
|
+
) -> Callable[[np.ndarray], np.ndarray]:
|
|
86
|
+
"""Return a ready-to-use DoG labeler for ``tile_process``.
|
|
87
|
+
|
|
88
|
+
Parameters
|
|
89
|
+
----------
|
|
90
|
+
low_sigma, high_sigma:
|
|
91
|
+
Gaussian sigmas (pixels) for the narrow/wide blur.
|
|
92
|
+
``dog = blur(low_sigma) - blur(high_sigma)``.
|
|
93
|
+
threshold:
|
|
94
|
+
Binary threshold applied to the DoG image (``dog > threshold``).
|
|
95
|
+
use_gpu:
|
|
96
|
+
Run gaussian_filter + label on GPU via cupy/cupyx instead of scipy.
|
|
97
|
+
Independent of ``decon_kwargs`` — pycudadecon always needs a CUDA
|
|
98
|
+
GPU regardless of this flag (it takes/returns plain NumPy), and this
|
|
99
|
+
flag only picks the backend for the blur/label steps that follow.
|
|
100
|
+
decon_kwargs:
|
|
101
|
+
If given, each tile is first deconvolved via
|
|
102
|
+
``pycudadecon.decon(block, **decon_kwargs)`` before the DoG step.
|
|
103
|
+
``None`` (default) skips deconvolution. pycudadecon is CUDA-only, so
|
|
104
|
+
a SLURM job running this needs a GPU allocated regardless of
|
|
105
|
+
``use_gpu`` above. Widen ``tile_process``'s ``overlap`` to cover the
|
|
106
|
+
PSF support when deconvolving, so edge tiles don't get truncated
|
|
107
|
+
context.
|
|
108
|
+
|
|
109
|
+
Returns
|
|
110
|
+
-------
|
|
111
|
+
Callable[[ndarray], ndarray]
|
|
112
|
+
Picklable function ready for ``tile_process``.
|
|
113
|
+
"""
|
|
114
|
+
if use_gpu:
|
|
115
|
+
_require_cupy()
|
|
116
|
+
if decon_kwargs is not None:
|
|
117
|
+
_require_pycudadecon()
|
|
118
|
+
cfg = {
|
|
119
|
+
"low_sigma": low_sigma,
|
|
120
|
+
"high_sigma": high_sigma,
|
|
121
|
+
"threshold": threshold,
|
|
122
|
+
"use_gpu": use_gpu,
|
|
123
|
+
"decon_kwargs": decon_kwargs,
|
|
124
|
+
}
|
|
125
|
+
return partial(_run, dog_dict=cfg)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _run(block: np.ndarray, dog_dict: dict[str, Any]) -> np.ndarray:
|
|
129
|
+
"""Deconvolve (optional), then DoG-threshold-label one tile.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
block : np.ndarray
|
|
134
|
+
One image tile.
|
|
135
|
+
dog_dict : dict
|
|
136
|
+
Configuration from :func:`dog_label_fn`.
|
|
137
|
+
|
|
138
|
+
Returns
|
|
139
|
+
-------
|
|
140
|
+
np.ndarray
|
|
141
|
+
Integer (``int32``) label array of the same shape.
|
|
142
|
+
"""
|
|
143
|
+
img = block.astype("float32")
|
|
144
|
+
|
|
145
|
+
decon_kwargs = dog_dict["decon_kwargs"]
|
|
146
|
+
if decon_kwargs is not None:
|
|
147
|
+
# ponytail: re-inits the GPU/OTF context on every tile via the
|
|
148
|
+
# one-shot decon() API. Ceiling: per-tile setup cost dominates on
|
|
149
|
+
# many small tiles. Upgrade: cache a pycudadecon.RLContext per
|
|
150
|
+
# worker process (see cellpose.py's _model_cache) if that shows up
|
|
151
|
+
# in the per-tile timing that tile_process logs.
|
|
152
|
+
from pycudadecon import decon
|
|
153
|
+
|
|
154
|
+
img = decon(images=img, **decon_kwargs)
|
|
155
|
+
|
|
156
|
+
use_gpu = dog_dict["use_gpu"]
|
|
157
|
+
if use_gpu:
|
|
158
|
+
import cupy as cp
|
|
159
|
+
from cupyx.scipy.ndimage import gaussian_filter, label
|
|
160
|
+
|
|
161
|
+
img = cp.asarray(img)
|
|
162
|
+
else:
|
|
163
|
+
from scipy.ndimage import gaussian_filter, label
|
|
164
|
+
|
|
165
|
+
low_blur = gaussian_filter(img, sigma=dog_dict["low_sigma"])
|
|
166
|
+
high_blur = gaussian_filter(img, sigma=dog_dict["high_sigma"])
|
|
167
|
+
dog_image = low_blur - high_blur
|
|
168
|
+
mask = dog_image > dog_dict["threshold"]
|
|
169
|
+
labels, _ = label(mask)
|
|
170
|
+
labels = labels.astype("int32")
|
|
171
|
+
|
|
172
|
+
return cp.asnumpy(labels) if use_gpu else labels
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def segment(tile: np.ndarray, **kwargs: Any) -> np.ndarray:
|
|
176
|
+
"""``dog_label_fn`` as a direct-call function for ``method: "custom"``.
|
|
177
|
+
|
|
178
|
+
Snakemake's ``custom`` method (see docs/guide/snakemake.md) calls
|
|
179
|
+
``segment(tile, **kwargs)`` directly rather than building a factory
|
|
180
|
+
first. This adapts :func:`dog_label_fn` to that contract, so this
|
|
181
|
+
plugin needs no dedicated wiring in the workflow's ``build_fn`` —
|
|
182
|
+
``method: "custom"`` with ``module: "patchworks.plugins.dog"`` already
|
|
183
|
+
covers it.
|
|
184
|
+
|
|
185
|
+
Parameters
|
|
186
|
+
----------
|
|
187
|
+
tile : np.ndarray
|
|
188
|
+
One image tile.
|
|
189
|
+
**kwargs : Any
|
|
190
|
+
Forwarded to :func:`dog_label_fn` (``low_sigma``, ``high_sigma``,
|
|
191
|
+
``threshold``, ``use_gpu``, ``decon_kwargs``).
|
|
192
|
+
|
|
193
|
+
Returns
|
|
194
|
+
-------
|
|
195
|
+
np.ndarray
|
|
196
|
+
Integer (``int32``) label array of the same shape.
|
|
197
|
+
"""
|
|
198
|
+
return dog_label_fn(**kwargs)(tile)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
# Keep the lower-level name available for advanced users
|
|
202
|
+
run_dog_label = _run
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Self-contained tests for the dog plugin. No frameworks, no fixtures."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _make_blob_image(shape=(1, 64, 64)):
|
|
7
|
+
img = np.zeros(shape, dtype="float32")
|
|
8
|
+
img[0, 28:36, 28:36] = 1.0
|
|
9
|
+
return img
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def test_dog_label_fn_cpu_finds_blob():
|
|
13
|
+
from patchworks.plugins.dog import dog_label_fn
|
|
14
|
+
|
|
15
|
+
fn = dog_label_fn(low_sigma=1.0, high_sigma=4.0, threshold=0.01)
|
|
16
|
+
labels = fn(_make_blob_image())
|
|
17
|
+
|
|
18
|
+
assert labels.shape == (1, 64, 64)
|
|
19
|
+
assert labels.dtype == np.int32
|
|
20
|
+
assert labels.max() >= 1 # the blob was detected
|
|
21
|
+
assert labels[0, 0, 0] == 0 # background stays unlabeled
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_segment_adapter_matches_factory():
|
|
25
|
+
# method: "custom" calls segment(tile, **kwargs) directly — must match
|
|
26
|
+
# dog_label_fn(**kwargs)(tile) exactly.
|
|
27
|
+
from patchworks.plugins.dog import dog_label_fn, segment
|
|
28
|
+
|
|
29
|
+
kwargs = dict(low_sigma=1.0, high_sigma=4.0, threshold=0.01)
|
|
30
|
+
img = _make_blob_image()
|
|
31
|
+
|
|
32
|
+
via_adapter = segment(img, **kwargs)
|
|
33
|
+
via_factory = dog_label_fn(**kwargs)(img)
|
|
34
|
+
|
|
35
|
+
np.testing.assert_array_equal(via_adapter, via_factory)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_dog_label_fn_with_tile_process():
|
|
39
|
+
import dask.array as da
|
|
40
|
+
|
|
41
|
+
from patchworks import tile_process
|
|
42
|
+
from patchworks.plugins.dog import dog_label_fn
|
|
43
|
+
|
|
44
|
+
arr = da.from_array(_make_blob_image((1, 64, 64)), chunks=(1, 64, 64))
|
|
45
|
+
fn = dog_label_fn(low_sigma=1.0, high_sigma=4.0, threshold=0.01)
|
|
46
|
+
result = tile_process(arr, fn, overlap=4).compute()
|
|
47
|
+
|
|
48
|
+
assert result.shape == (1, 64, 64)
|
|
49
|
+
assert result.max() >= 1
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Example segmentation config: cilia channel, via method: "custom" ->
|
|
2
|
+
# patchworks.plugins.dog.segment (deconvolution + difference-of-Gaussians).
|
|
3
|
+
# Cilia are thin/small structures a cell-body model like Cellpose isn't
|
|
4
|
+
# shaped for. Paired with config_cyto.yaml / config_nuclei.yaml via
|
|
5
|
+
# config/multi.yaml to relate cilia -> containing cell / nucleus. Same
|
|
6
|
+
# work_dir and tile_shape as those two so patchworks.label_relations() can
|
|
7
|
+
# compare the label arrays chunk-for-chunk.
|
|
8
|
+
#
|
|
9
|
+
# pycudadecon is CUDA-only, so this segment job needs a GPU too — same
|
|
10
|
+
# profile/slurm as the Cellpose configs already covers it.
|
|
11
|
+
|
|
12
|
+
input: "/path/to/scan.ims"
|
|
13
|
+
work_dir: "/path/to/results"
|
|
14
|
+
|
|
15
|
+
reuse_pyramid: false
|
|
16
|
+
convert_chunks: null
|
|
17
|
+
shard: false
|
|
18
|
+
|
|
19
|
+
channel: 2 # cilia marker channel
|
|
20
|
+
level: 0
|
|
21
|
+
tile_shape: [16, 1024, 1024] # keep identical to config_cyto.yaml/config_nuclei.yaml
|
|
22
|
+
gpu_memory_gb: null
|
|
23
|
+
overlap: 30 # cover the PSF support (decon) + the DoG's high_sigma
|
|
24
|
+
skip_empty: true
|
|
25
|
+
empty_threshold: null
|
|
26
|
+
|
|
27
|
+
method: "custom"
|
|
28
|
+
label_name: "cilia_labels"
|
|
29
|
+
custom:
|
|
30
|
+
module: "patchworks.plugins.dog"
|
|
31
|
+
function: "segment"
|
|
32
|
+
kwargs:
|
|
33
|
+
low_sigma: 1.0
|
|
34
|
+
high_sigma: 3.0
|
|
35
|
+
threshold: 0.02
|
|
36
|
+
# use_gpu: true # optional: also run the blur/label step on GPU
|
|
37
|
+
decon_kwargs:
|
|
38
|
+
psf: "/path/to/psf.tif"
|
|
39
|
+
dxpsf: 0.1
|
|
40
|
+
dxdata: 0.1
|
|
41
|
+
dzpsf: 0.2
|
|
42
|
+
dzdata: 0.2
|
|
43
|
+
wavelength: 525
|
|
44
|
+
na: 1.4
|
|
45
|
+
nimm: 1.515
|
|
46
|
+
|
|
47
|
+
pyramid_levels: 5
|
|
48
|
+
pyramid_downscale: 2
|
|
49
|
+
sequential_labels: true
|
|
50
|
+
merge_workers: null
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# Drives `pixi run multi` / `scripts/run_multi.py`: runs several segmentation
|
|
2
2
|
# configs (in order) against the same work_dir, then relates their labels by
|
|
3
|
-
# voxel overlap (patchworks.label_relations) and writes
|
|
3
|
+
# voxel overlap (patchworks.label_relations) and writes an Excel workbook per
|
|
4
|
+
# pair (two sheets: one row per a-object incl. unmatched ones, one row per
|
|
5
|
+
# b-object incl. ones with zero matches — see docs/guide/snakemake.md).
|
|
4
6
|
#
|
|
5
7
|
# All listed segmentation configs must share the same work_dir/image.zarr and
|
|
6
8
|
# the same tile_shape/level, so the resulting label arrays are directly
|
|
@@ -14,8 +16,15 @@
|
|
|
14
16
|
segmentations:
|
|
15
17
|
- config/config_nuclei.yaml
|
|
16
18
|
- config/config_cyto.yaml
|
|
19
|
+
- config/config_cilia.yaml
|
|
17
20
|
|
|
18
21
|
relations:
|
|
19
22
|
- a: nuclei_labels
|
|
20
23
|
b: cyto_labels
|
|
21
|
-
output: nuclei_to_cyto.
|
|
24
|
+
output: nuclei_to_cyto.xlsx
|
|
25
|
+
- a: cilia_labels
|
|
26
|
+
b: cyto_labels
|
|
27
|
+
output: cilia_to_cell.xlsx
|
|
28
|
+
- a: cilia_labels
|
|
29
|
+
b: nuclei_labels
|
|
30
|
+
output: cilia_to_nucleus.xlsx
|
|
@@ -19,8 +19,10 @@ platforms = ["linux-64"]
|
|
|
19
19
|
python = "3.12.*"
|
|
20
20
|
|
|
21
21
|
# Default env uses patchworks[cellpose] (>=3.0, resolves to latest compatible).
|
|
22
|
+
# "dog" -> pycudadecon, for config_cilia.yaml's deconvolution step (part of
|
|
23
|
+
# the shipped config/multi.yaml example).
|
|
22
24
|
[pypi-dependencies]
|
|
23
|
-
patchworks = { version = "*", extras = ["workflow", "cellpose", "imaris", "bioio"] }
|
|
25
|
+
patchworks = { version = "*", extras = ["workflow", "cellpose", "imaris", "bioio", "dog"] }
|
|
24
26
|
|
|
25
27
|
# Cellpose-version-pinned features for explicit environments below.
|
|
26
28
|
[feature.cp3.pypi-dependencies]
|
|
@@ -12,13 +12,15 @@ a Snakemake rule — the segmentations already namespace their own paths under
|
|
|
12
12
|
work_dir/<label_name>/, so running them one after another here is exactly
|
|
13
13
|
equivalent to running each snakemake command by hand). Once all segmentations
|
|
14
14
|
finish, each configured relation pair is computed via
|
|
15
|
-
patchworks.label_relations and written as
|
|
15
|
+
patchworks.label_relations and written as an Excel workbook in work_dir,
|
|
16
|
+
with two sheets: one row per a-object (unmatched ones included, with an
|
|
17
|
+
empty b-id and zeros) and one row per b-object (a-object count + total
|
|
18
|
+
overlap, including b-objects with zero matches).
|
|
16
19
|
"""
|
|
17
20
|
|
|
18
21
|
from __future__ import annotations
|
|
19
22
|
|
|
20
23
|
import argparse
|
|
21
|
-
import csv
|
|
22
24
|
import subprocess
|
|
23
25
|
import sys
|
|
24
26
|
from pathlib import Path
|
|
@@ -121,31 +123,51 @@ def main() -> None:
|
|
|
121
123
|
image_store = f"{work_dir}/image.zarr"
|
|
122
124
|
|
|
123
125
|
import dask.array as da
|
|
126
|
+
import openpyxl
|
|
124
127
|
|
|
125
128
|
from patchworks import label_relations
|
|
126
129
|
|
|
127
130
|
for rel in relations:
|
|
128
131
|
a_name, b_name = rel["a"], rel["b"]
|
|
129
132
|
out_path = Path(work_dir) / rel.get(
|
|
130
|
-
"output", f"{a_name}_to_{b_name}.
|
|
133
|
+
"output", f"{a_name}_to_{b_name}.xlsx"
|
|
131
134
|
)
|
|
132
135
|
print(f"[run_multi] relating {a_name} -> {b_name} …", flush=True)
|
|
133
136
|
a = da.from_zarr(image_store, component=f"labels/{a_name}/0")
|
|
134
137
|
b = da.from_zarr(image_store, component=f"labels/{b_name}/0")
|
|
135
138
|
table = label_relations(a, b)
|
|
136
139
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
|
|
140
|
+
# label_relations() only returns a-objects that touch a b-object.
|
|
141
|
+
# Pull the full id sets so unmatched a-objects (zero overlap) and
|
|
142
|
+
# b-objects with no matches at all still get a row -- otherwise
|
|
143
|
+
# they'd silently vanish instead of counting as zero.
|
|
144
|
+
a_ids = sorted(int(x) for x in da.unique(a[a > 0]).compute())
|
|
145
|
+
b_ids = sorted(int(x) for x in da.unique(b[b > 0]).compute())
|
|
146
|
+
|
|
147
|
+
per_b = {b_id: {"count": 0, "overlap_voxels": 0} for b_id in b_ids}
|
|
148
|
+
for m in table.values():
|
|
149
|
+
agg = per_b.get(m["match"])
|
|
150
|
+
if agg is not None:
|
|
151
|
+
agg["count"] += 1
|
|
152
|
+
agg["overlap_voxels"] += m["overlap_voxels"]
|
|
153
|
+
|
|
154
|
+
wb = openpyxl.Workbook()
|
|
155
|
+
ws_a = wb.active
|
|
156
|
+
ws_a.title = a_name[:31] # Excel sheet-name length limit
|
|
157
|
+
ws_a.append(
|
|
158
|
+
[
|
|
159
|
+
f"{a_name}_id",
|
|
160
|
+
f"{b_name}_id",
|
|
161
|
+
"overlap_voxels",
|
|
162
|
+
"overlap_fraction",
|
|
163
|
+
]
|
|
164
|
+
)
|
|
165
|
+
for a_id in a_ids:
|
|
166
|
+
m = table.get(a_id)
|
|
167
|
+
if m is None:
|
|
168
|
+
ws_a.append([a_id, None, 0, 0]) # no overlap -- still counted
|
|
169
|
+
else:
|
|
170
|
+
ws_a.append(
|
|
149
171
|
[
|
|
150
172
|
a_id,
|
|
151
173
|
m["match"],
|
|
@@ -153,7 +175,19 @@ def main() -> None:
|
|
|
153
175
|
m["overlap_fraction"],
|
|
154
176
|
]
|
|
155
177
|
)
|
|
156
|
-
|
|
178
|
+
|
|
179
|
+
ws_b = wb.create_sheet(title=b_name[:31])
|
|
180
|
+
ws_b.append([f"{b_name}_id", f"{a_name}_count", "total_overlap_voxels"])
|
|
181
|
+
for b_id in b_ids:
|
|
182
|
+
agg = per_b[b_id]
|
|
183
|
+
ws_b.append([b_id, agg["count"], agg["overlap_voxels"]])
|
|
184
|
+
|
|
185
|
+
wb.save(out_path)
|
|
186
|
+
print(
|
|
187
|
+
f"[run_multi] wrote {out_path} "
|
|
188
|
+
f"({len(a_ids)} {a_name}, {len(b_ids)} {b_name})",
|
|
189
|
+
flush=True,
|
|
190
|
+
)
|
|
157
191
|
|
|
158
192
|
|
|
159
193
|
if __name__ == "__main__":
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|