patchworks 2.6.12__tar.gz → 2.7.0__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.
Files changed (116) hide show
  1. {patchworks-2.6.12 → patchworks-2.7.0}/PKG-INFO +2 -1
  2. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/plugins/ome_zarr.md +12 -0
  3. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/snakemake.md +92 -6
  4. {patchworks-2.6.12 → patchworks-2.7.0}/pyproject.toml +10 -1
  5. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_chunks.py +18 -4
  6. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_io.py +15 -4
  7. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/plugins/ome_zarr.py +440 -162
  8. patchworks-2.7.0/tests/ngff_schemas/0.4/image.schema +233 -0
  9. patchworks-2.7.0/tests/ngff_schemas/0.4/label.schema +77 -0
  10. patchworks-2.7.0/tests/ngff_schemas/0.4/ome.schema +17 -0
  11. patchworks-2.7.0/tests/ngff_schemas/0.5/_version.schema +10 -0
  12. patchworks-2.7.0/tests/ngff_schemas/0.5/image.schema +268 -0
  13. patchworks-2.7.0/tests/ngff_schemas/0.5/label.schema +91 -0
  14. patchworks-2.7.0/tests/ngff_schemas/0.5/ome.schema +33 -0
  15. patchworks-2.7.0/tests/ngff_schemas/README.md +16 -0
  16. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_allocation.py +28 -0
  17. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_ome_zarr.py +328 -0
  18. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_pw.py +60 -0
  19. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_run_multi.py +75 -0
  20. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/config/common.yaml +6 -0
  21. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/config/config.yaml +14 -0
  22. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/rules/common.smk +9 -2
  23. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/_pw.py +38 -0
  24. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/build_occupancy.py +5 -1
  25. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/convert.py +8 -2
  26. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/merge.py +28 -1
  27. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/run_multi.py +1 -0
  28. {patchworks-2.6.12 → patchworks-2.7.0}/.github/workflows/docs.yml +0 -0
  29. {patchworks-2.6.12 → patchworks-2.7.0}/.github/workflows/lint.yml +0 -0
  30. {patchworks-2.6.12 → patchworks-2.7.0}/.github/workflows/release.yml +0 -0
  31. {patchworks-2.6.12 → patchworks-2.7.0}/.gitignore +0 -0
  32. {patchworks-2.6.12 → patchworks-2.7.0}/.markdownlint-cli2.yaml +0 -0
  33. {patchworks-2.6.12 → patchworks-2.7.0}/LICENSE +0 -0
  34. {patchworks-2.6.12 → patchworks-2.7.0}/README.md +0 -0
  35. {patchworks-2.6.12 → patchworks-2.7.0}/cliff.toml +0 -0
  36. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/chunks.md +0 -0
  37. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/cluster.md +0 -0
  38. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/io.md +0 -0
  39. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/merge_tile_labels.md +0 -0
  40. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/plugins/cellpose.md +0 -0
  41. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/plugins/dog.md +0 -0
  42. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/plugins/napari.md +0 -0
  43. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/postprocess.md +0 -0
  44. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/relabel.md +0 -0
  45. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/tile_process.md +0 -0
  46. {patchworks-2.6.12 → patchworks-2.7.0}/docs/api/volume_filter.md +0 -0
  47. {patchworks-2.6.12 → patchworks-2.7.0}/docs/assets/logo.png +0 -0
  48. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/cellpose_2d.md +0 -0
  49. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/cellpose_2d.py +0 -0
  50. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/cellpose_3d.md +0 -0
  51. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/cellpose_3d.py +0 -0
  52. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/custom.md +0 -0
  53. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/custom_method.py +0 -0
  54. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/dog.md +0 -0
  55. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/dog.py +0 -0
  56. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/standalone_merge.md +0 -0
  57. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/stardist.md +0 -0
  58. {patchworks-2.6.12 → patchworks-2.7.0}/docs/examples/stardist_2d.py +0 -0
  59. {patchworks-2.6.12 → patchworks-2.7.0}/docs/getting_started.md +0 -0
  60. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/custom_segmentation.md +0 -0
  61. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/gpu_distributed.md +0 -0
  62. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/label_relations.md +0 -0
  63. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/measurements.md +0 -0
  64. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/merging.md +0 -0
  65. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/ome_zarr_napari.md +0 -0
  66. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/performance.md +0 -0
  67. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/pitfalls.md +0 -0
  68. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/skip_empty.md +0 -0
  69. {patchworks-2.6.12 → patchworks-2.7.0}/docs/guide/tiling.md +0 -0
  70. {patchworks-2.6.12 → patchworks-2.7.0}/docs/index.md +0 -0
  71. {patchworks-2.6.12 → patchworks-2.7.0}/mkdocs.yml +0 -0
  72. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/__init__.py +0 -0
  73. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_cluster.py +0 -0
  74. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_core.py +0 -0
  75. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_distributed.py +0 -0
  76. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_gpu.py +0 -0
  77. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_merge.py +0 -0
  78. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_notify.py +0 -0
  79. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_occupancy.py +0 -0
  80. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_postprocess.py +0 -0
  81. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_progress.py +0 -0
  82. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_relabel.py +0 -0
  83. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_relations.py +0 -0
  84. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/_volume_filter.py +0 -0
  85. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/plugins/__init__.py +0 -0
  86. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/plugins/cellpose.py +0 -0
  87. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/plugins/dog.py +0 -0
  88. {patchworks-2.6.12 → patchworks-2.7.0}/src/patchworks/plugins/napari.py +0 -0
  89. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_cellpose.py +0 -0
  90. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_core.py +0 -0
  91. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_distributed.py +0 -0
  92. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_dog.py +0 -0
  93. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_gpu.py +0 -0
  94. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_napari.py +0 -0
  95. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_notify.py +0 -0
  96. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_occupancy.py +0 -0
  97. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_postprocess.py +0 -0
  98. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_progress.py +0 -0
  99. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_relations.py +0 -0
  100. {patchworks-2.6.12 → patchworks-2.7.0}/tests/test_volume_filter.py +0 -0
  101. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/README.md +0 -0
  102. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/Snakefile +0 -0
  103. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/config/config_cilia.yaml +0 -0
  104. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/config/config_cyto.yaml +0 -0
  105. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/config/config_nuclei.yaml +0 -0
  106. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/config/multi.yaml +0 -0
  107. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/pixi.toml +0 -0
  108. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/profile/slurm/config.yaml +0 -0
  109. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/rules/convert.smk +0 -0
  110. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/rules/merge.smk +0 -0
  111. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/rules/segment.smk +0 -0
  112. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/fetch_model.py +0 -0
  113. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/prepare_tiles.py +0 -0
  114. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/relate.py +0 -0
  115. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/segment_tile.py +0 -0
  116. {patchworks-2.6.12 → patchworks-2.7.0}/workflow/scripts/view.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: patchworks
3
- Version: 2.6.12
3
+ Version: 2.7.0
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
@@ -57,6 +57,7 @@ Requires-Dist: cellpose<4,>=3.0; extra == 'cellpose3'
57
57
  Provides-Extra: cellpose4
58
58
  Requires-Dist: cellpose>=4; extra == 'cellpose4'
59
59
  Provides-Extra: dev
60
+ Requires-Dist: jsonschema>=4.18; extra == 'dev'
60
61
  Requires-Dist: psutil; extra == 'dev'
61
62
  Requires-Dist: pytest; extra == 'dev'
62
63
  Requires-Dist: pytest-cov; extra == 'dev'
@@ -25,6 +25,10 @@ resolution, matching anisotropic microscopy stacks.
25
25
 
26
26
  ::: patchworks.plugins.ome_zarr.register_labels
27
27
 
28
+ ## reshard_level
29
+
30
+ ::: patchworks.plugins.ome_zarr.reshard_level
31
+
28
32
  ## read_pixel_size
29
33
 
30
34
  ::: patchworks.plugins.ome_zarr.read_pixel_size
@@ -37,6 +41,14 @@ matches the store, and reads both.
37
41
 
38
42
  ::: patchworks.plugins.ome_zarr.ngff_version
39
43
 
44
+ Every writer takes an `ngff_version=` keyword: `"auto"` (default) follows the
45
+ installed zarr — 0.5 on v3, 0.4 on v2 — and `"0.4"` pins the older, zarr-v2
46
+ layout. Writing into an existing store always follows *that store's* format,
47
+ so a label pyramid added later can never disagree with the image it sits in.
48
+ NGFF 0.6 is released but not written: RFC-5 replaces `axes` with
49
+ `coordinateSystems` and requires `input`/`output` on every coordinate
50
+ transformation, and no reader supports it yet.
51
+
40
52
  ::: patchworks.plugins.ome_zarr.read_ngff_attr
41
53
 
42
54
  ::: patchworks.plugins.ome_zarr.write_ngff_attrs
@@ -50,7 +50,9 @@ work_dir: "/scratch/results" # everything is written here
50
50
  # conversion (input → pyramidal OME-ZARR)
51
51
  reuse_pyramid: true # .ims: copy its own pyramid (fast)
52
52
  convert_chunks: null # null → bounded auto chunks; or [c,z,y,x]
53
- shard: false # true → pack chunks into shards (fewer files)
53
+ shard: false # true → pack chunks into shards (fewer files);
54
+ # covers the image and the label pyramids
55
+ ngff_version: "auto" # OME-ZARR version: "auto" (0.5), or "0.4"
54
56
 
55
57
  # tiling
56
58
  channel: 0 # channel to segment, 0-based (null = keep all)
@@ -83,6 +85,8 @@ cellpose:
83
85
  pyramid_levels: 5
84
86
  pyramid_downscale: 2
85
87
  sequential_labels: true # renumber labels to a contiguous 1..N
88
+ shard_labels: false # true → also reshard label level 0 after the
89
+ # merge (one extra pass; see tip below)
86
90
  ```
87
91
 
88
92
  !!! tip "Growing labels after segmentation"
@@ -98,6 +102,77 @@ sequential_labels: true # renumber labels to a contiguous 1..N
98
102
  labels afterwards](custom_segmentation.md#growing-labels-afterwards-dilation)
99
103
  for how it works and the equivalent direct-API call.
100
104
 
105
+ !!! tip "What `shard: true` covers, and `shard_labels` for label level 0"
106
+ Without sharding, one chunk is one file, and a fine-chunked level 0 can
107
+ run to ~950k of them — painful on a shared filesystem at write time and
108
+ on every read after. `shard: true` packs chunks into far fewer files
109
+ without changing the chunking or the memory profile.
110
+
111
+ `shard: true` applies to **the converted image (all levels)** and to
112
+ **the label pyramid levels (1..N)**. It cannot apply to **label level
113
+ 0** *while that level is being written*: a shard has to be written whole
114
+ by a single writer, while level 0 is filled one chunk at a time by
115
+ concurrent `segment` jobs (or by the merge's own worker pool). Two
116
+ writers doing a read-modify-write on the same shard file silently lose
117
+ each other's chunks.
118
+
119
+ Once the merge has finished, though, nothing is writing it anymore, so a
120
+ single-threaded pass *can* rewrite it sharded. That is `shard_labels`:
121
+
122
+ ```yaml
123
+ shard: true
124
+ shard_labels: true # or an explicit shape, e.g. [16, 512, 512]
125
+ ```
126
+
127
+ `true` reuses whatever spec `shard` carries; a list overrides it.
128
+ `shard: false` does not veto it — an unsharded image with sharded labels
129
+ is a valid combination. It is opt-in because it costs one extra full
130
+ read+write of level 0, and because it is the level with the most chunks
131
+ it is also the one worth paying for. The array's attributes (including
132
+ the merge's own completion marker) are carried across, so a later rerun
133
+ still sees the merge as done.
134
+
135
+ Check whether the file count is actually a problem for your data first —
136
+ a `(126, 34000, 28500)` image at the `(16, 1024, 1024)` label chunk cap
137
+ is 7,616 files per label group, well under the 200,000 at which the
138
+ conversion starts warning. On scicore that is fine; on a filesystem with
139
+ a tighter inode or per-directory budget it may not be.
140
+
141
+ !!! tip "Which OME-ZARR version gets written (`ngff_version`)"
142
+ OME-ZARR is versioned, and the version decides the **zarr format** as
143
+ well as the metadata layout — the two are not separate choices. NGFF 0.4
144
+ is specified over zarr v2 and puts `multiscales`, `labels` and
145
+ `image-label` at the top level of the store's attributes; 0.5 is the
146
+ zarr-v3 revision and nests them under an `ome` key.
147
+
148
+ `ngff_version: "auto"` (the default) follows the installed zarr, which on
149
+ any current environment means **0.5**. Pin `"0.4"` only when a downstream
150
+ tool still cannot read 0.5:
151
+
152
+ ```yaml
153
+ ngff_version: "0.4"
154
+ shard: false # required: zarr v2 has no sharding codec
155
+ ```
156
+
157
+ That is a real trade-off, not a formality — 0.4 means a zarr-v2 store, and
158
+ sharding is a zarr-v3 feature, so `shard`/`shard_labels` stop doing
159
+ anything. The workflow refuses the combination rather than silently
160
+ ignoring it. The store's own root file changes too (`.zgroup` instead of
161
+ `zarr.json`), which the rules account for.
162
+
163
+ Writing into an **existing** store always follows that store's format,
164
+ whatever this key says: the label pyramid is written by `merge`, long
165
+ after `convert` made the image, and a store with v2 arrays and v3
166
+ metadata is one no reader can open.
167
+
168
+ **What about 0.6?** It was released on 14 September 2026 and patchworks
169
+ does not write it. It is not a version bump but a different metadata
170
+ document: RFC-5 replaces a multiscale's `axes` with `coordinateSystems`
171
+ and requires an `input`/`output` pair on every coordinate transformation.
172
+ Nothing reads it yet either — ome-zarr-py, and so napari, still default
173
+ to 0.5. Setting `ngff_version: "0.6"` is rejected with that explanation
174
+ rather than writing a store you could not open.
175
+
101
176
  !!! tip "Dropping objects by size with `min_volume`/`max_volume`"
102
177
  `min_volume: N` drops any object smaller than `N` µm³; `max_volume: N`
103
178
  drops any object larger than `N` µm³ (e.g. several objects merged into
@@ -362,6 +437,7 @@ input: "/data/scan.ims"
362
437
  work_dir: "/scratch/results"
363
438
  tile_shape: [16, 1024, 1024]
364
439
  shard: false # true → far fewer files, same chunks
440
+ ngff_version: "auto" # convert reads it, so it belongs here
365
441
  tiles_per_job: 4
366
442
  ```
367
443
 
@@ -447,11 +523,21 @@ snakemake --workflow-profile profile/slurm --configfile config/common.yaml confi
447
523
  ```
448
524
 
449
525
  !!! warning "Conversion settings belong in the shared file"
450
- `convert` runs **once**, from the first config only. A `shard`, `input` or
451
- `pyramid_levels` set on the second config is therefore never read, and
452
- nothing logs that it was dropped. `run_multi` refuses to start when those
453
- keys disagree across configs and tells you which one — but if you drive
454
- the configs by hand, keep them in `common.yaml`.
526
+ `convert` runs **once**, from the first config only. A `shard`, `input`,
527
+ `convert_chunks`, `sequence_pattern` or `reuse_pyramid` set on the second
528
+ config is therefore never read, and nothing logs that it was dropped.
529
+ `run_multi` refuses to start when those keys disagree across configs and
530
+ tells you which one — but if you drive the configs by hand, keep them in
531
+ `common.yaml`.
532
+
533
+ `ngff_version` is in that list: it decides the store's zarr format, so a
534
+ second config disagreeing about it would describe a store that is not the
535
+ one on disk.
536
+
537
+ `merge` runs once **per config**, so the keys it reads —
538
+ `pyramid_levels`, `pyramid_downscale`, `sequential_labels`,
539
+ `min_volume`/`max_volume` and `shard_labels` — may legitimately differ
540
+ between them, and are only in `common.yaml` for convenience.
455
541
 
456
542
  Splitting the configs is optional: a self-contained config still works,
457
543
  and `common:` can simply be left out of `multi.yaml`.
@@ -96,7 +96,16 @@ napari = [
96
96
  # openpyxl -> scripts/run_multi.py writes label_relations() output as an
97
97
  # Excel workbook (per-object + per-container sheets), not a plain CSV.
98
98
  workflow = ["snakemake>=8", "snakemake-executor-plugin-slurm", "openpyxl"]
99
- dev = ["pytest", "pytest-cov", "scikit-image", "psutil", "tqdm"]
99
+ # jsonschema validates what we write against the vendored official
100
+ # OME-NGFF schemas (tests/ngff_schemas/); without it that one test skips.
101
+ dev = [
102
+ "pytest",
103
+ "pytest-cov",
104
+ "scikit-image",
105
+ "psutil",
106
+ "tqdm",
107
+ "jsonschema>=4.18",
108
+ ]
100
109
  docs = ["mkdocs-material>=9.0", "mkdocstrings[python]>=0.24"]
101
110
  all = [
102
111
  "patchworks[io,gpu,bioio,imaris,napari]",
@@ -449,6 +449,18 @@ def auto_tile_shape_cellpose(
449
449
  (1, 2048, 2048)
450
450
  """
451
451
  n_workers = n_workers or cpu_allocation()
452
+ # Cellpose resizes the tile before the net runs -- by `rescale`
453
+ # (= 30 / diameter) on every axis, and by `anisotropy` on z as well -- so
454
+ # the array it actually holds is bigger than the one it was handed, and a
455
+ # budget computed from the unresized tile under-counts by that factor. A
456
+ # diameter half the model's 30 px means a 2x upsample per axis: 8x the
457
+ # voxels, enough to turn a comfortable tile into an OOM.
458
+ #
459
+ # Both only ever *shrink* the tile. A predicted downsample (diameter > 30)
460
+ # would license a bigger one, but these are a safety margin against a
461
+ # rough memory model, not a measurement to spend headroom on.
462
+ rescale = max(1.0, 30.0 / diameter) if diameter else 1.0
463
+ z_resize = max(1.0, anisotropy or 1.0)
452
464
  # A tile holds n_channels planes per voxel (e.g. Cellpose's
453
465
  # cyto+nuclei pair), so the per-voxel cost -- and every budget
454
466
  # derived from it below -- scales with them.
@@ -475,7 +487,8 @@ def auto_tile_shape_cellpose(
475
487
  min_tile = int(4 * diameter) if diameter is not None else 1
476
488
 
477
489
  if n_spatial == 2 or not do_3D:
478
- max_pixels_2d = max(1, max_raw_bytes // itemsize)
490
+ # Two axes resized, so the cost per configured pixel is rescale**2.
491
+ max_pixels_2d = max(1, int(max_raw_bytes // (itemsize * rescale**2)))
479
492
  tile_side = max(min_tile, int(max_pixels_2d**0.5))
480
493
  if n_spatial == 2:
481
494
  y, x = shape[-2], shape[-1]
@@ -485,9 +498,10 @@ def auto_tile_shape_cellpose(
485
498
  chunk_spatial = [1, min(y, tile_side), min(x, tile_side)]
486
499
  else:
487
500
  z, y, x = shape[-3], shape[-2], shape[-1]
488
- # Cellpose resizes z by `anisotropy` before the net runs, so the tile
489
- # it actually holds is that much deeper than the one handed to it.
490
- effective_z = z * max(1.0, anisotropy or 1.0)
501
+ # All three axes are resized: z by anisotropy * rescale, y/x by
502
+ # rescale each -- so one configured voxel costs
503
+ # anisotropy * rescale**3 of them.
504
+ effective_z = z * z_resize * rescale**3
491
505
  max_pixels_per_slice = max(
492
506
  1, int((max_raw_bytes // 3) // (effective_z * itemsize))
493
507
  )
@@ -18,27 +18,38 @@ _ZARR_V3 = int(zarr.__version__.split(".")[0]) >= 3
18
18
  _ZARR_V3 = int(zarr.__version__.split(".")[0]) >= 3
19
19
 
20
20
 
21
- def zarr_compressor_kwargs() -> dict:
21
+ def zarr_compressor_kwargs(zarr_format: int = 3) -> dict:
22
22
  """Keyword arguments pinning the compression codec for a new array.
23
23
 
24
24
  zstd is already zarr v3's default, but relying on a library default means
25
25
  the stores patchworks writes change silently if that default ever moves.
26
26
  Labels in particular are highly compressible, so this is worth stating.
27
27
 
28
+ The codec *object* depends on the format of the array being written, not
29
+ on the installed zarr: zarr-python 3 can write a zarr-v2 array (which is
30
+ what NGFF 0.4 needs), and a v2 array rejects ``zarr.codecs.ZstdCodec`` --
31
+ it wants the numcodecs one.
32
+
33
+ Parameters
34
+ ----------
35
+ zarr_format : int, optional
36
+ Format of the array about to be created, 2 or 3 (default 3).
37
+
28
38
  Returns
29
39
  -------
30
40
  dict
31
- ``compressors=``/``compressor=`` as the installed zarr expects, or
41
+ ``compressors=``/``compressor=`` as that combination expects, or
32
42
  empty if the codec cannot be built (then the default applies).
33
43
  """
34
44
  try:
35
- if _ZARR_V3:
45
+ if _ZARR_V3 and zarr_format != 2:
36
46
  from zarr.codecs import ZstdCodec
37
47
 
38
48
  return {"compressors": (ZstdCodec(level=1),)}
39
49
  import numcodecs
40
50
 
41
- return {"compressor": numcodecs.Zstd(level=1)}
51
+ codec = numcodecs.Zstd(level=1)
52
+ return {"compressors": (codec,)} if _ZARR_V3 else {"compressor": codec}
42
53
  except Exception: # pragma: no cover - depends on the installed zarr
43
54
  logger.debug("could not pin a compressor; using zarr's default")
44
55
  return {}