patchworks 1.3.1__tar.gz → 1.3.2__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.3.1 → patchworks-1.3.2}/PKG-INFO +1 -1
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/dog.md +3 -3
- patchworks-1.3.2/docs/guide/custom_segmentation.md +154 -0
- patchworks-1.3.2/docs/guide/label_relations.md +44 -0
- patchworks-1.3.2/docs/guide/measurements.md +48 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/ome_zarr_napari.md +1 -1
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/snakemake.md +19 -223
- {patchworks-1.3.1 → patchworks-1.3.2}/mkdocs.yml +4 -1
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/dog.py +1 -1
- {patchworks-1.3.1 → patchworks-1.3.2}/.github/workflows/docs.yml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/.github/workflows/lint.yml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/.github/workflows/release.yml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/.gitignore +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/.markdownlint-cli2.yaml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/LICENSE +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/README.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/cliff.toml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/chunks.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/cluster.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/io.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/merge_tile_labels.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/plugins/cellpose.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/plugins/dog.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/plugins/napari.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/plugins/ome_zarr.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/postprocess.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/relabel.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/api/tile_process.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/assets/logo.png +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/cellpose_2d.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/cellpose_2d.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/cellpose_3d.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/cellpose_3d.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/custom.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/custom_method.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/dog.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/standalone_merge.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/stardist.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/examples/stardist_2d.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/getting_started.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/gpu_distributed.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/merging.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/performance.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/pitfalls.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/skip_empty.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/guide/tiling.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/docs/index.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/pyproject.toml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/__init__.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_chunks.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_cluster.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_core.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_distributed.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_io.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_merge.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_postprocess.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_relabel.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/_relations.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/__init__.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/cellpose.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/napari.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/src/patchworks/plugins/ome_zarr.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_core.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_distributed.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_dog.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_napari.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_ome_zarr.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_postprocess.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/tests/test_relations.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/README.md +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/Snakefile +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/config.yaml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/config_cilia.yaml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/config_cyto.yaml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/config_nuclei.yaml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/config/multi.yaml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/pixi.toml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/profile/slurm/config.yaml +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/rules/common.smk +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/rules/convert.smk +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/rules/merge.smk +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/rules/segment.smk +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/_pw.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/convert.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/fetch_model.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/merge.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/prepare_tiles.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/workflow/scripts/run_multi.py +0 -0
- {patchworks-1.3.1 → patchworks-1.3.2}/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.
|
|
3
|
+
Version: 1.3.2
|
|
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
|
|
@@ -97,12 +97,12 @@ tile_process(IMAGE, fn, tile_shape=(1, 1024, 1024), overlap=8, write_to=OUTPUT)
|
|
|
97
97
|
|
|
98
98
|
On the cluster, set `dilate: 2` in the YAML config instead — it applies to
|
|
99
99
|
`method: "custom"` (this plugin) the same way it does for `cellpose`/
|
|
100
|
-
`threshold`, see [Growing labels
|
|
100
|
+
`threshold`, see [Growing labels afterwards](../guide/custom_segmentation.md#growing-labels-afterwards-dilation).
|
|
101
101
|
|
|
102
102
|
## Using it in the Snakemake workflow
|
|
103
103
|
|
|
104
104
|
No dedicated wiring needed — `patchworks.plugins.dog` exposes a `segment(tile, **kwargs)`
|
|
105
|
-
adapter for the documented [`"custom"` method](../guide/
|
|
105
|
+
adapter for the documented [`"custom"` method](../guide/custom_segmentation.md):
|
|
106
106
|
|
|
107
107
|
```yaml
|
|
108
108
|
method: "custom"
|
|
@@ -185,6 +185,6 @@ Checklist specific to this config:
|
|
|
185
185
|
|
|
186
186
|
Segment the cell body with Cellpose and the cilia with `dog_label_fn` as two
|
|
187
187
|
separate `tile_process` runs (same image, same `tile_shape`), then use
|
|
188
|
-
[`label_relations`](../guide/
|
|
188
|
+
[`label_relations`](../guide/label_relations.md)
|
|
189
189
|
to map each cilium to the cell it belongs to — see
|
|
190
190
|
`workflow/config/multi.yaml` for the same thing wired up as a cluster job.
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Custom segmentation function
|
|
2
|
+
|
|
3
|
+
Not using Cellpose? Run **your own** per-tile function — no need to edit the
|
|
4
|
+
package. You write one function; patchworks handles everything around it
|
|
5
|
+
(tiling, halos, skipping empty tiles, the zarr-native merge, global
|
|
6
|
+
relabelling, resume, logs).
|
|
7
|
+
|
|
8
|
+
This applies the same whether you call the API directly (`tile_process`) or
|
|
9
|
+
run via the [Snakemake cluster workflow](snakemake.md) — see [Wiring it into
|
|
10
|
+
the cluster workflow](#wiring-it-into-the-cluster-workflow) below for the
|
|
11
|
+
config side.
|
|
12
|
+
|
|
13
|
+
## The contract
|
|
14
|
+
|
|
15
|
+
Your function is called **once per tile**:
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
labels = segment(tile) # plus any kwargs you configure
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| | What you get / must return |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| **Input `tile`** | A NumPy array of **one** tile, with the overlap halo already included. The channel and pyramid level are already selected, so it is purely spatial: `(z, y, x)` for a 3-D run, `(y, x)` for 2-D. Dtype is the image's (e.g. `uint16`). |
|
|
24
|
+
| **Return** | An integer **label** array (not a boolean mask), **same shape** as `tile`. `0` = background; each object a distinct positive integer. |
|
|
25
|
+
| **Labels** | Only need to be unique **within the tile**. Don't try to make them globally unique — the merge step stitches objects across tile borders and renumbers everything to a contiguous `1..N` (`sequential_labels: true`). |
|
|
26
|
+
| **Shape** | Must match the input exactly — patchworks trims the halo off your output, so a wrong shape is an error. Don't crop or resize inside the function. |
|
|
27
|
+
|
|
28
|
+
That is the whole interface. Anything that turns an image tile into a label
|
|
29
|
+
image works: classic image processing, StarDist, a trained model, an external
|
|
30
|
+
binary you shell out to, …
|
|
31
|
+
|
|
32
|
+
## Minimal example (no GPU, no deps beyond scikit-image)
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
# my_seg.py
|
|
36
|
+
import numpy as np
|
|
37
|
+
from skimage.measure import label
|
|
38
|
+
|
|
39
|
+
def segment(tile: np.ndarray, sigma: float = 2.0) -> np.ndarray:
|
|
40
|
+
"""Threshold + connected components. Returns int32 labels (0 = bg)."""
|
|
41
|
+
from skimage.filters import gaussian, threshold_otsu
|
|
42
|
+
|
|
43
|
+
smooth = gaussian(tile, sigma=sigma, preserve_range=True)
|
|
44
|
+
thr = threshold_otsu(smooth) if smooth.max() > smooth.min() else np.inf
|
|
45
|
+
return label(smooth > thr).astype("int32")
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
```python
|
|
49
|
+
from patchworks import tile_process
|
|
50
|
+
from my_seg import segment
|
|
51
|
+
|
|
52
|
+
tile_process("image.zarr", segment, write_to="labels.zarr")
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Growing labels afterwards (dilation)
|
|
56
|
+
|
|
57
|
+
To grow every label by a few pixels after segmentation, wrap your function
|
|
58
|
+
with [`patchworks.dilate_labels`](../api/postprocess.md):
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from patchworks import tile_process, dilate_labels
|
|
62
|
+
from patchworks.plugins.dog import dog_label_fn
|
|
63
|
+
|
|
64
|
+
fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
|
|
65
|
+
fn = dilate_labels(fn, iterations=2) # grow each label by 2 px, then run
|
|
66
|
+
result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048),
|
|
67
|
+
overlap=8, write_to="labels.zarr")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
`dilate_labels` wraps any `(tile) -> labels` function — the same contract
|
|
71
|
+
above — so it works with `dog_label_fn`, `cellpose_fn`, or your own
|
|
72
|
+
`segment`. It dilates each tile's labels before the halo is trimmed and
|
|
73
|
+
tiles are merged, so `overlap` must still cover the dilation amount. On the
|
|
74
|
+
cluster, set `dilate: N` in the config instead — see [Configure the
|
|
75
|
+
run](snakemake.md#3-configure-the-run).
|
|
76
|
+
|
|
77
|
+
## Real example: StarDist 3-D, with model caching
|
|
78
|
+
|
|
79
|
+
Heavy models must be loaded **once**, not per tile. On SLURM each tile is its
|
|
80
|
+
own process so this matters less, but for local runs one process segments many
|
|
81
|
+
tiles — cache the model at module level (or with `functools.lru_cache`):
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
# stardist_seg.py
|
|
85
|
+
import numpy as np
|
|
86
|
+
|
|
87
|
+
_MODEL = None
|
|
88
|
+
|
|
89
|
+
def _model():
|
|
90
|
+
global _MODEL
|
|
91
|
+
if _MODEL is None: # loaded once per worker process
|
|
92
|
+
from stardist.models import StarDist3D
|
|
93
|
+
_MODEL = StarDist3D.from_pretrained("3D_demo")
|
|
94
|
+
return _MODEL
|
|
95
|
+
|
|
96
|
+
def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
|
|
97
|
+
from csbdeep.utils import normalize
|
|
98
|
+
|
|
99
|
+
labels, _ = _model().predict_instances(
|
|
100
|
+
normalize(tile), prob_thresh=prob_thresh
|
|
101
|
+
)
|
|
102
|
+
return labels.astype("int32")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Using a GPU? Just let your framework see it — nothing extra needed.
|
|
106
|
+
|
|
107
|
+
## Test it before you submit
|
|
108
|
+
|
|
109
|
+
Run your function on one real tile first — it catches shape/dtype bugs in
|
|
110
|
+
seconds instead of after a queue wait (or a long local run). Output must be
|
|
111
|
+
integer, same shape, `0` for background:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from patchworks import load_ome_zarr
|
|
115
|
+
from my_seg import segment
|
|
116
|
+
|
|
117
|
+
img = load_ome_zarr("results/image.zarr", channel=0, level=0)
|
|
118
|
+
tile = img[:, :512, :512].compute() # a small spatial block
|
|
119
|
+
out = segment(tile)
|
|
120
|
+
|
|
121
|
+
assert out.shape == tile.shape, (out.shape, tile.shape)
|
|
122
|
+
assert out.dtype.kind in "iu" # integer labels, not a float mask
|
|
123
|
+
print("objects in tile:", int(out.max()))
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Wiring it into the cluster workflow
|
|
127
|
+
|
|
128
|
+
Point the config at your module and function:
|
|
129
|
+
|
|
130
|
+
```yaml
|
|
131
|
+
method: "custom"
|
|
132
|
+
label_name: "my_labels"
|
|
133
|
+
custom:
|
|
134
|
+
module: "my_seg" # import name
|
|
135
|
+
function: "segment" # default is "segment"
|
|
136
|
+
kwargs: # optional — forwarded as segment(tile, **kwargs)
|
|
137
|
+
sigma: 1.5
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Same for the StarDist example above:
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
method: "custom"
|
|
144
|
+
label_name: "stardist"
|
|
145
|
+
custom:
|
|
146
|
+
module: "stardist_seg"
|
|
147
|
+
function: "segment"
|
|
148
|
+
kwargs:
|
|
149
|
+
prob_thresh: 0.5
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
For getting the module importable on a compute node, dependency/GPU
|
|
153
|
+
checklist, and troubleshooting, see [Custom functions on the
|
|
154
|
+
cluster](snakemake.md#custom-segmentation-function) in the cluster guide.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Relating labels across segmentations
|
|
2
|
+
|
|
3
|
+
Once you have two segmentations of the same image (e.g. nuclei inside cells),
|
|
4
|
+
`label_relations()` maps each label in one to the label it overlaps most in
|
|
5
|
+
the other — by streaming both arrays chunk by chunk, so it scales to
|
|
6
|
+
hundreds of thousands of objects without loading anything fully into RAM.
|
|
7
|
+
|
|
8
|
+
Both label arrays must share the exact same chunk layout — same
|
|
9
|
+
`tile_shape`/pyramid `level` when they were produced.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
import dask.array as da
|
|
13
|
+
from patchworks import label_relations
|
|
14
|
+
|
|
15
|
+
nuclei = da.from_zarr("results/image.zarr", component="labels/nuclei_labels/0")
|
|
16
|
+
cells = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
|
|
17
|
+
|
|
18
|
+
table = label_relations(nuclei, cells)
|
|
19
|
+
table[2]
|
|
20
|
+
# {'match': 3, 'overlap_voxels': 4821, 'overlap_fraction': 0.94}
|
|
21
|
+
# -> nucleus 2 belongs to cell 3, 94% of its voxels fall inside it
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`table` only contains matched `a` labels (nuclei with at least one
|
|
25
|
+
overlapping voxel in `cells`) — unmatched labels and full per-`b` coverage
|
|
26
|
+
need a bit more bookkeeping (the [cluster workflow's `run_multi.py`
|
|
27
|
+
script](snakemake.md#one-command-multiple-segmentations--relations) does
|
|
28
|
+
this for you and writes it as a two-sheet workbook).
|
|
29
|
+
|
|
30
|
+
Save it as a table yourself:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import csv
|
|
34
|
+
|
|
35
|
+
with open("nuclei_to_cell.csv", "w", newline="") as f:
|
|
36
|
+
w = csv.writer(f)
|
|
37
|
+
w.writerow(["nucleus_id", "cell_id", "overlap_voxels", "overlap_fraction"])
|
|
38
|
+
for nucleus_id, m in table.items():
|
|
39
|
+
w.writerow([nucleus_id, m["match"], m["overlap_voxels"], m["overlap_fraction"]])
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
On the cluster, producing the two label stores in the first place is a
|
|
43
|
+
matter of running the workflow twice against the same `work_dir` — see
|
|
44
|
+
[Running two segmentations](snakemake.md#running-two-segmentations-eg-nuclei--cytoplasm).
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Measurements (fast, whole-volume regionprops)
|
|
2
|
+
|
|
3
|
+
`skimage.measure.regionprops` needs the full labelled + intensity array in
|
|
4
|
+
RAM — fine for one tile, not for a hundred-thousand-object OME-ZARR.
|
|
5
|
+
|
|
6
|
+
## Interactively, in napari
|
|
7
|
+
|
|
8
|
+
[napari-chunked-regionprops](https://github.com/imcf/napari-chunked-regionprops)
|
|
9
|
+
is built for this — its "Measure" dock widget computes area/centroid/intensity
|
|
10
|
+
stats directly off a Labels layer's dask/zarr-backed array, out-of-core, and
|
|
11
|
+
scales with chunk count rather than object count. It's the best fit for
|
|
12
|
+
measuring *every* object in a store this size, not just a cropped region —
|
|
13
|
+
see [View image + labels in napari](ome_zarr_napari.md#view-image--labels-in-napari).
|
|
14
|
+
Bundled in `patchworks[napari]`.
|
|
15
|
+
|
|
16
|
+
For interactively inspecting individual cells by clicking in the viewer (not
|
|
17
|
+
all objects at once), the
|
|
18
|
+
[napari-skimage-regionprops](https://github.com/haesleinhuepf/napari-skimage-regionprops)
|
|
19
|
+
plugin's table widget also works well — point it at a cropped region rather
|
|
20
|
+
than the full volume, since it loads its input fully into memory.
|
|
21
|
+
|
|
22
|
+
## Headless / scripted
|
|
23
|
+
|
|
24
|
+
Use [`dask-image`](https://image.dask.org)'s `ndmeasure`, which computes
|
|
25
|
+
directly on the dask/zarr-backed arrays, chunk-parallel, without
|
|
26
|
+
materializing the volume:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install dask-image
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
import dask.array as da
|
|
34
|
+
from dask_image.ndmeasure import area, center_of_mass, mean, standard_deviation
|
|
35
|
+
|
|
36
|
+
labels = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
|
|
37
|
+
image = da.from_zarr("results/image.zarr", component="0")[0] # channel 0, level 0
|
|
38
|
+
|
|
39
|
+
ids = da.unique(labels[labels > 0]).compute()
|
|
40
|
+
areas = area(image, labels, ids).compute() # voxel counts
|
|
41
|
+
means = mean(image, labels, ids).compute() # mean intensity
|
|
42
|
+
stds = standard_deviation(image, labels, ids).compute()
|
|
43
|
+
centroids = center_of_mass(image, labels, ids).compute() # voxel coords (z, y, x)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Multiply `areas` by the voxel's physical volume and `centroids` by the pixel
|
|
47
|
+
size (both read straight from the OME-ZARR's own `multiscales` metadata) to
|
|
48
|
+
get µm-scale measurements.
|
|
@@ -159,7 +159,7 @@ just one channel instead: `view_in_napari("scan.zarr", channel=0)`.
|
|
|
159
159
|
out-of-core straight off the Labels layer's backing dask/zarr array, so
|
|
160
160
|
it scales to the same huge label images `tile_process` writes, unlike
|
|
161
161
|
plain `skimage.measure.regionprops`. Bundled in `patchworks[napari]`. See
|
|
162
|
-
[Measurements](
|
|
162
|
+
[Measurements](measurements.md)
|
|
163
163
|
for the non-interactive/headless equivalent.
|
|
164
164
|
|
|
165
165
|
## End-to-end
|
|
@@ -80,15 +80,9 @@ sequential_labels: true # renumber labels to a contiguous 1..N
|
|
|
80
80
|
|
|
81
81
|
!!! tip "Growing labels after segmentation"
|
|
82
82
|
`dilate: N` grows every label by `N` pixels once segmentation finishes,
|
|
83
|
-
regardless of `method` (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
sure `overlap` covers the dilation amount plus the usual object-diameter
|
|
87
|
-
halo. `0` (default) disables it. Under the hood this wraps whatever
|
|
88
|
-
segmentation function `method` builds with
|
|
89
|
-
[`patchworks.dilate_labels`](../api/postprocess.md); see [Custom
|
|
90
|
-
segmentation function](#custom-segmentation-function) below for using it
|
|
91
|
-
directly from Python instead of via config.
|
|
83
|
+
regardless of `method`. `0` (default) disables it. See [Growing labels
|
|
84
|
+
afterwards](custom_segmentation.md#growing-labels-afterwards-dilation)
|
|
85
|
+
for how it works and the equivalent direct-API call.
|
|
92
86
|
|
|
93
87
|
!!! tip "Tile size vs runtime"
|
|
94
88
|
`tile_shape: "auto"` sizes each tile to your GPU's VRAM. Smaller tiles =
|
|
@@ -266,39 +260,11 @@ results/image.zarr/labels/cyto_labels/
|
|
|
266
260
|
Different segmentations of the same image can use different `channel` and
|
|
267
261
|
`cellpose:` settings freely, but keep `tile_shape`/`level` the same across
|
|
268
262
|
configs — the label arrays then share the exact same chunk layout, which
|
|
269
|
-
`label_relations()`
|
|
263
|
+
[`label_relations()`](label_relations.md) requires.
|
|
270
264
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
`label_relations()` maps each label in one to the label it overlaps most in
|
|
275
|
-
the other — by streaming both arrays chunk by chunk, so it scales to
|
|
276
|
-
hundreds of thousands of objects without loading anything fully into RAM:
|
|
277
|
-
|
|
278
|
-
```python
|
|
279
|
-
import dask.array as da
|
|
280
|
-
from patchworks import label_relations
|
|
281
|
-
|
|
282
|
-
nuclei = da.from_zarr("results/image.zarr", component="labels/nuclei_labels/0")
|
|
283
|
-
cells = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
|
|
284
|
-
|
|
285
|
-
table = label_relations(nuclei, cells)
|
|
286
|
-
table[2]
|
|
287
|
-
# {'match': 3, 'overlap_voxels': 4821, 'overlap_fraction': 0.94}
|
|
288
|
-
# -> nucleus 2 belongs to cell 3, 94% of its voxels fall inside it
|
|
289
|
-
```
|
|
290
|
-
|
|
291
|
-
Save it as a table:
|
|
292
|
-
|
|
293
|
-
```python
|
|
294
|
-
import csv
|
|
295
|
-
|
|
296
|
-
with open("nuclei_to_cell.csv", "w", newline="") as f:
|
|
297
|
-
w = csv.writer(f)
|
|
298
|
-
w.writerow(["nucleus_id", "cell_id", "overlap_voxels", "overlap_fraction"])
|
|
299
|
-
for nucleus_id, m in table.items():
|
|
300
|
-
w.writerow([nucleus_id, m["match"], m["overlap_voxels"], m["overlap_fraction"]])
|
|
301
|
-
```
|
|
265
|
+
See [Relating labels across segmentations](label_relations.md) for what
|
|
266
|
+
`label_relations()` returns and how to save it yourself — the cluster
|
|
267
|
+
workflow's own automation is below.
|
|
302
268
|
|
|
303
269
|
### One command: multiple segmentations + relations
|
|
304
270
|
|
|
@@ -338,9 +304,9 @@ extra) with two sheets:
|
|
|
338
304
|
| `<a>` | every non-background `a` label, **including unmatched ones** | `<a>_id`, `<b>_id` (blank if unmatched), `overlap_voxels`, `overlap_fraction` (0 if unmatched) |
|
|
339
305
|
| `<b>` | every non-background `b` label, **including ones with zero matches** | `<b>_id`, `<a>_count`, `total_overlap_voxels` |
|
|
340
306
|
|
|
341
|
-
Unlike calling `label_relations()` directly (which
|
|
342
|
-
|
|
343
|
-
segmentations, so counts (e.g. "how many nuclei have no matching cell",
|
|
307
|
+
Unlike calling [`label_relations()`](label_relations.md) directly (which
|
|
308
|
+
only returns matched `a` labels), the workbook always covers every object in
|
|
309
|
+
both segmentations, so counts (e.g. "how many nuclei have no matching cell",
|
|
344
310
|
"how many cells have zero cilia") aren't silently dropped.
|
|
345
311
|
|
|
346
312
|
Both lists are ordinary lists, so 3+ segmentations work the same way — add
|
|
@@ -358,189 +324,19 @@ cyto_labels` and `cilia_labels -> nuclei_labels`) so you can use whichever
|
|
|
358
324
|
fits a given dataset. See `config/config_cilia.yaml`. Its deconvolution step
|
|
359
325
|
needs `pip install "patchworks[dog]"` in the segment jobs' environment.
|
|
360
326
|
|
|
361
|
-
## Measurements
|
|
362
|
-
|
|
363
|
-
`skimage.measure.regionprops` needs the full labelled + intensity array in
|
|
364
|
-
RAM — fine for one tile, not for a hundred-thousand-object OME-ZARR.
|
|
365
|
-
|
|
366
|
-
**Interactively, in napari**, this is what
|
|
367
|
-
[napari-chunked-regionprops](https://github.com/imcf/napari-chunked-regionprops)
|
|
368
|
-
is for — its "Measure" dock widget computes area/centroid/intensity stats
|
|
369
|
-
directly off a Labels layer's dask/zarr-backed array, out-of-core, and scales
|
|
370
|
-
with chunk count rather than object count. It's the best fit for measuring
|
|
371
|
-
*every* object in a store this size, not just a cropped region — see
|
|
372
|
-
[View image + labels in napari](ome_zarr_napari.md#view-image--labels-in-napari).
|
|
373
|
-
Bundled in `patchworks[napari]`.
|
|
327
|
+
## Measurements
|
|
374
328
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
```bash
|
|
380
|
-
pip install dask-image
|
|
381
|
-
```
|
|
382
|
-
|
|
383
|
-
```python
|
|
384
|
-
import dask.array as da
|
|
385
|
-
from dask_image.ndmeasure import area, center_of_mass, mean, standard_deviation
|
|
386
|
-
|
|
387
|
-
labels = da.from_zarr("results/image.zarr", component="labels/cyto_labels/0")
|
|
388
|
-
image = da.from_zarr("results/image.zarr", component="0")[0] # channel 0, level 0
|
|
389
|
-
|
|
390
|
-
ids = da.unique(labels[labels > 0]).compute()
|
|
391
|
-
areas = area(image, labels, ids).compute() # voxel counts
|
|
392
|
-
means = mean(image, labels, ids).compute() # mean intensity
|
|
393
|
-
stds = standard_deviation(image, labels, ids).compute()
|
|
394
|
-
centroids = center_of_mass(image, labels, ids).compute() # voxel coords (z, y, x)
|
|
395
|
-
```
|
|
396
|
-
|
|
397
|
-
Multiply `areas` by the voxel's physical volume and `centroids` by the pixel
|
|
398
|
-
size (both read straight from the OME-ZARR's own `multiscales` metadata) to
|
|
399
|
-
get µm-scale measurements.
|
|
400
|
-
|
|
401
|
-
For interactively inspecting individual cells by clicking in the viewer
|
|
402
|
-
(not all objects at once), the
|
|
403
|
-
[napari-skimage-regionprops](https://github.com/haesleinhuepf/napari-skimage-regionprops)
|
|
404
|
-
plugin's table widget works well — point it at a cropped region rather than
|
|
405
|
-
the full volume, since it loads its input fully into memory. For measuring
|
|
406
|
-
*all* objects at once, use
|
|
407
|
-
[napari-chunked-regionprops](https://github.com/imcf/napari-chunked-regionprops)
|
|
408
|
-
instead (see above) — it doesn't have this in-memory limitation.
|
|
329
|
+
See [Measurements](measurements.md) for computing area/centroid/intensity
|
|
330
|
+
stats on a whole label store, interactively in napari or headless/scripted —
|
|
331
|
+
`skimage.measure.regionprops` alone doesn't scale to a store this size.
|
|
409
332
|
|
|
410
333
|
## Custom segmentation function
|
|
411
334
|
|
|
412
|
-
Not using Cellpose?
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
### The contract
|
|
418
|
-
|
|
419
|
-
Your function is called **once per tile**:
|
|
420
|
-
|
|
421
|
-
```python
|
|
422
|
-
labels = segment(tile) # plus any kwargs you configure
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
| | What you get / must return |
|
|
426
|
-
| --- | --- |
|
|
427
|
-
| **Input `tile`** | A NumPy array of **one** tile, with the overlap halo already included. The channel and pyramid level from the config are already selected, so it is purely spatial: `(z, y, x)` for a 3-D run, `(y, x)` for 2-D. Dtype is the image's (e.g. `uint16`). |
|
|
428
|
-
| **Return** | An integer **label** array (not a boolean mask), **same shape** as `tile`. `0` = background; each object a distinct positive integer. |
|
|
429
|
-
| **Labels** | Only need to be unique **within the tile**. Don't try to make them globally unique — the merge step stitches objects across tile borders and renumbers everything to a contiguous `1..N` (`sequential_labels: true`). |
|
|
430
|
-
| **Shape** | Must match the input exactly — patchworks trims the halo off your output, so a wrong shape is an error. Don't crop or resize inside the function. |
|
|
431
|
-
|
|
432
|
-
That is the whole interface. Anything that turns an image tile into a label
|
|
433
|
-
image works: classic image processing, StarDist, a trained model, an external
|
|
434
|
-
binary you shell out to, …
|
|
435
|
-
|
|
436
|
-
### Minimal example (no GPU, no deps beyond scikit-image)
|
|
437
|
-
|
|
438
|
-
```python
|
|
439
|
-
# my_seg.py
|
|
440
|
-
import numpy as np
|
|
441
|
-
from skimage.measure import label
|
|
442
|
-
|
|
443
|
-
def segment(tile: np.ndarray, sigma: float = 2.0) -> np.ndarray:
|
|
444
|
-
"""Threshold + connected components. Returns int32 labels (0 = bg)."""
|
|
445
|
-
from skimage.filters import gaussian, threshold_otsu
|
|
446
|
-
|
|
447
|
-
smooth = gaussian(tile, sigma=sigma, preserve_range=True)
|
|
448
|
-
thr = threshold_otsu(smooth) if smooth.max() > smooth.min() else np.inf
|
|
449
|
-
return label(smooth > thr).astype("int32")
|
|
450
|
-
```
|
|
451
|
-
|
|
452
|
-
```yaml
|
|
453
|
-
method: "custom"
|
|
454
|
-
label_name: "my_labels"
|
|
455
|
-
custom:
|
|
456
|
-
module: "my_seg" # import name (see "Make it importable")
|
|
457
|
-
function: "segment" # default is "segment"
|
|
458
|
-
kwargs: # optional — forwarded as segment(tile, **kwargs)
|
|
459
|
-
sigma: 1.5
|
|
460
|
-
```
|
|
461
|
-
|
|
462
|
-
### Growing labels afterwards (dilation)
|
|
463
|
-
|
|
464
|
-
To grow every label by a few pixels after segmentation — any method, not
|
|
465
|
-
just `custom` — set `dilate: N` in the config (see the tip above), or wrap
|
|
466
|
-
your function directly with
|
|
467
|
-
[`patchworks.dilate_labels`](../api/postprocess.md) when calling the API
|
|
468
|
-
yourself:
|
|
469
|
-
|
|
470
|
-
```python
|
|
471
|
-
from patchworks import tile_process, dilate_labels
|
|
472
|
-
from patchworks.plugins.dog import dog_label_fn
|
|
473
|
-
|
|
474
|
-
fn = dog_label_fn(low_sigma=1.0, high_sigma=3.0, threshold=0.02)
|
|
475
|
-
fn = dilate_labels(fn, iterations=2) # grow each label by 2 px, then run
|
|
476
|
-
result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048),
|
|
477
|
-
overlap=8, write_to="labels.zarr")
|
|
478
|
-
```
|
|
479
|
-
|
|
480
|
-
`dilate_labels` wraps any `(tile) -> labels` function — the same contract
|
|
481
|
-
described above — so it works with `dog_label_fn`, `cellpose_fn`, or your
|
|
482
|
-
own `segment`. It dilates each tile's labels before the halo is trimmed and
|
|
483
|
-
tiles are merged, so `overlap` must still cover the dilation amount.
|
|
484
|
-
|
|
485
|
-
### Real example: StarDist 3-D, with model caching
|
|
486
|
-
|
|
487
|
-
Heavy models must be loaded **once**, not per tile. On SLURM each tile is its
|
|
488
|
-
own process so this matters less, but for local runs one process segments many
|
|
489
|
-
tiles — cache the model at module level (or with `functools.lru_cache`):
|
|
490
|
-
|
|
491
|
-
```python
|
|
492
|
-
# stardist_seg.py
|
|
493
|
-
import numpy as np
|
|
494
|
-
|
|
495
|
-
_MODEL = None
|
|
496
|
-
|
|
497
|
-
def _model():
|
|
498
|
-
global _MODEL
|
|
499
|
-
if _MODEL is None: # loaded once per worker process
|
|
500
|
-
from stardist.models import StarDist3D
|
|
501
|
-
_MODEL = StarDist3D.from_pretrained("3D_demo")
|
|
502
|
-
return _MODEL
|
|
503
|
-
|
|
504
|
-
def segment(tile: np.ndarray, prob_thresh: float = 0.5) -> np.ndarray:
|
|
505
|
-
from csbdeep.utils import normalize
|
|
506
|
-
|
|
507
|
-
labels, _ = _model().predict_instances(
|
|
508
|
-
normalize(tile), prob_thresh=prob_thresh
|
|
509
|
-
)
|
|
510
|
-
return labels.astype("int32")
|
|
511
|
-
```
|
|
512
|
-
|
|
513
|
-
```yaml
|
|
514
|
-
method: "custom"
|
|
515
|
-
label_name: "stardist"
|
|
516
|
-
custom:
|
|
517
|
-
module: "stardist_seg"
|
|
518
|
-
function: "segment"
|
|
519
|
-
kwargs:
|
|
520
|
-
prob_thresh: 0.5
|
|
521
|
-
```
|
|
522
|
-
|
|
523
|
-
Using a GPU? The segment jobs already hold one (the `gres: "gpu:1"` request),
|
|
524
|
-
so just let your framework see it — nothing extra in the config.
|
|
525
|
-
|
|
526
|
-
### Test it before you submit
|
|
527
|
-
|
|
528
|
-
Run your function on one real tile first — it catches shape/dtype bugs in
|
|
529
|
-
seconds instead of after a queue wait. Output must be integer, same shape, `0`
|
|
530
|
-
for background:
|
|
531
|
-
|
|
532
|
-
```python
|
|
533
|
-
from patchworks import load_ome_zarr
|
|
534
|
-
from my_seg import segment
|
|
535
|
-
|
|
536
|
-
img = load_ome_zarr("results/image.zarr", channel=0, level=0)
|
|
537
|
-
tile = img[:, :512, :512].compute() # a small spatial block
|
|
538
|
-
out = segment(tile)
|
|
539
|
-
|
|
540
|
-
assert out.shape == tile.shape, (out.shape, tile.shape)
|
|
541
|
-
assert out.dtype.kind in "iu" # integer labels, not a float mask
|
|
542
|
-
print("objects in tile:", int(out.max()))
|
|
543
|
-
```
|
|
335
|
+
Not using Cellpose? See [Custom segmentation function](custom_segmentation.md)
|
|
336
|
+
for the function contract, examples (including label dilation), and how to
|
|
337
|
+
test it before submitting. The rest of this section covers what's specific to
|
|
338
|
+
running it **on the cluster**: getting the module importable and the
|
|
339
|
+
checklist below.
|
|
544
340
|
|
|
545
341
|
### Make it importable on the cluster
|
|
546
342
|
|
|
@@ -41,8 +41,11 @@ nav:
|
|
|
41
41
|
- Empty tile skipping: guide/skip_empty.md
|
|
42
42
|
- GPU & distributed: guide/gpu_distributed.md
|
|
43
43
|
- Performance & memory: guide/performance.md
|
|
44
|
-
-
|
|
44
|
+
- Custom segmentation function: guide/custom_segmentation.md
|
|
45
|
+
- Relating labels across segmentations: guide/label_relations.md
|
|
46
|
+
- Measurements: guide/measurements.md
|
|
45
47
|
- OME-ZARR & napari: guide/ome_zarr_napari.md
|
|
48
|
+
- Cluster workflow (Snakemake): guide/snakemake.md
|
|
46
49
|
- Pitfalls: guide/pitfalls.md
|
|
47
50
|
- Examples:
|
|
48
51
|
- Cellpose 2-D: examples/cellpose_2d.md
|
|
@@ -25,7 +25,7 @@ With deconvolution first (widen ``overlap`` to cover the PSF support):
|
|
|
25
25
|
>>> result = tile_process("image.zarr", fn, tile_shape=(1, 2048, 2048), overlap=32)
|
|
26
26
|
|
|
27
27
|
For the Snakemake workflow's ``method: "custom"`` (see
|
|
28
|
-
docs/guide/
|
|
28
|
+
docs/guide/custom_segmentation.md), use the
|
|
29
29
|
:func:`segment` adapter instead of the factory directly:
|
|
30
30
|
|
|
31
31
|
>>> # custom: {module: "patchworks.plugins.dog", function: "segment",
|
|
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
|
|
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
|