ngio 0.1.4__tar.gz → 0.1.6__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 (82) hide show
  1. {ngio-0.1.4 → ngio-0.1.6}/PKG-INFO +2 -1
  2. {ngio-0.1.4 → ngio-0.1.6}/docs/notebooks/basic_usage.ipynb +6 -4
  3. {ngio-0.1.4 → ngio-0.1.6}/docs/notebooks/image.ipynb +28 -16
  4. {ngio-0.1.4 → ngio-0.1.6}/docs/notebooks/processing.ipynb +18 -14
  5. {ngio-0.1.4 → ngio-0.1.6}/pyproject.toml +1 -2
  6. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/core/ngff_image.py +2 -2
  7. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/core/utils.py +3 -3
  8. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/io/_zarr_group_utils.py +1 -2
  9. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/ngff_meta/v04/zarr_utils.py +8 -1
  10. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/pipes/_zoom_utils.py +46 -4
  11. {ngio-0.1.4 → ngio-0.1.6}/tests/core/test_ngff_image.py +28 -0
  12. {ngio-0.1.4 → ngio-0.1.6}/tests/ngff_meta/test_utils.py +1 -0
  13. {ngio-0.1.4 → ngio-0.1.6}/tests/ngff_meta/test_v04.py +30 -0
  14. {ngio-0.1.4 → ngio-0.1.6}/tests/pipes/test_zoom.py +25 -0
  15. {ngio-0.1.4 → ngio-0.1.6}/.copier-answers.yml +0 -0
  16. {ngio-0.1.4 → ngio-0.1.6}/.gitattributes +0 -0
  17. {ngio-0.1.4 → ngio-0.1.6}/.github/ISSUE_TEMPLATE.md +0 -0
  18. {ngio-0.1.4 → ngio-0.1.6}/.github/TEST_FAIL_TEMPLATE.md +0 -0
  19. {ngio-0.1.4 → ngio-0.1.6}/.github/dependabot.yml +0 -0
  20. {ngio-0.1.4 → ngio-0.1.6}/.github/workflows/build_docs.yml +0 -0
  21. {ngio-0.1.4 → ngio-0.1.6}/.github/workflows/ci.yml +0 -0
  22. {ngio-0.1.4 → ngio-0.1.6}/.gitignore +0 -0
  23. {ngio-0.1.4 → ngio-0.1.6}/.pre-commit-config.yaml +0 -0
  24. {ngio-0.1.4 → ngio-0.1.6}/LICENSE +0 -0
  25. {ngio-0.1.4 → ngio-0.1.6}/README.md +0 -0
  26. {ngio-0.1.4 → ngio-0.1.6}/_typos.toml +0 -0
  27. {ngio-0.1.4 → ngio-0.1.6}/docs/api/core.md +0 -0
  28. {ngio-0.1.4 → ngio-0.1.6}/docs/getting-started.md +0 -0
  29. {ngio-0.1.4 → ngio-0.1.6}/docs/index.md +0 -0
  30. {ngio-0.1.4 → ngio-0.1.6}/mkdocs.yml +0 -0
  31. {ngio-0.1.4 → ngio-0.1.6}/setup_data.sh +0 -0
  32. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/__init__.py +0 -0
  33. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/core/__init__.py +0 -0
  34. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/core/dimensions.py +0 -0
  35. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/core/image_handler.py +0 -0
  36. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/core/image_like_handler.py +0 -0
  37. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/core/label_handler.py +0 -0
  38. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/core/roi.py +0 -0
  39. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/io/__init__.py +0 -0
  40. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/io/_zarr.py +0 -0
  41. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/io/_zarr_array_utils.py +0 -0
  42. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/iterators/__init__.py +0 -0
  43. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/ngff_meta/__init__.py +0 -0
  44. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/ngff_meta/fractal_image_meta.py +0 -0
  45. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/ngff_meta/meta_handler.py +0 -0
  46. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/ngff_meta/utils.py +0 -0
  47. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/ngff_meta/v04/__init__.py +0 -0
  48. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/ngff_meta/v04/specs.py +0 -0
  49. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/pipes/__init__.py +0 -0
  50. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/pipes/_slicer_transforms.py +0 -0
  51. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/pipes/_transforms.py +0 -0
  52. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/pipes/data_pipe.py +0 -0
  53. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/tables/__init__.py +0 -0
  54. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/tables/_ad_reader.py +0 -0
  55. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/tables/_utils.py +0 -0
  56. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/tables/tables_group.py +0 -0
  57. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/tables/v1/__init__.py +0 -0
  58. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/tables/v1/_generic_table.py +0 -0
  59. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/tables/v1/feature_tables.py +0 -0
  60. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/tables/v1/masking_roi_tables.py +0 -0
  61. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/tables/v1/roi_tables.py +0 -0
  62. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/utils/__init__.py +0 -0
  63. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/utils/_common_types.py +0 -0
  64. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/utils/_errors.py +0 -0
  65. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/utils/_logger.py +0 -0
  66. {ngio-0.1.4 → ngio-0.1.6}/src/ngio/utils/_pydantic_utils.py +0 -0
  67. {ngio-0.1.4 → ngio-0.1.6}/tests/core/conftest.py +0 -0
  68. {ngio-0.1.4 → ngio-0.1.6}/tests/core/test_image_handler.py +0 -0
  69. {ngio-0.1.4 → ngio-0.1.6}/tests/core/test_image_like_handler.py +0 -0
  70. {ngio-0.1.4 → ngio-0.1.6}/tests/core/test_label_handler.py +0 -0
  71. {ngio-0.1.4 → ngio-0.1.6}/tests/core/test_roi.py +0 -0
  72. {ngio-0.1.4 → ngio-0.1.6}/tests/io/conftest.py +0 -0
  73. {ngio-0.1.4 → ngio-0.1.6}/tests/io/test_zarr_group_utils.py +0 -0
  74. {ngio-0.1.4 → ngio-0.1.6}/tests/ngff_meta/conftest.py +0 -0
  75. {ngio-0.1.4 → ngio-0.1.6}/tests/ngff_meta/test_fractal_image_meta.py +0 -0
  76. {ngio-0.1.4 → ngio-0.1.6}/tests/ngff_meta/test_pixel_size.py +0 -0
  77. {ngio-0.1.4 → ngio-0.1.6}/tests/pipes/conftest.py +0 -0
  78. {ngio-0.1.4 → ngio-0.1.6}/tests/tables/conftest.py +0 -0
  79. {ngio-0.1.4 → ngio-0.1.6}/tests/tables/test_table_conversion.py +0 -0
  80. {ngio-0.1.4 → ngio-0.1.6}/tests/tables/test_table_group.py +0 -0
  81. {ngio-0.1.4 → ngio-0.1.6}/tests/tables/test_v1_tables.py +0 -0
  82. {ngio-0.1.4 → ngio-0.1.6}/tests/tables/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ngio
3
- Version: 0.1.4
3
+ Version: 0.1.6
4
4
  Summary: Next Generation file format IO
5
5
  Project-URL: homepage, https://github.com/lorenzocerrone/ngio
6
6
  Project-URL: repository, https://github.com/lorenzocerrone/ngio
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3
13
13
  Classifier: Programming Language :: Python :: 3.10
14
14
  Classifier: Programming Language :: Python :: 3.11
15
15
  Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
16
17
  Classifier: Typing :: Typed
17
18
  Requires-Python: >=3.10
18
19
  Requires-Dist: aiohttp
@@ -194,9 +194,9 @@
194
194
  "outputs": [],
195
195
  "source": [
196
196
  "print(\"List of Tables: \", ngff_image.tables.list())\n",
197
- "print(\" - Feature tables: \", ngff_image.tables.list(table_type='feature_table'))\n",
198
- "print(\" - Roi tables: \", ngff_image.tables.list(table_type='roi_table'))\n",
199
- "print(\" - Masking Roi tables: \", ngff_image.tables.list(table_type='masking_roi_table'))"
197
+ "print(\" - Feature tables: \", ngff_image.tables.list(table_type=\"feature_table\"))\n",
198
+ "print(\" - Roi tables: \", ngff_image.tables.list(table_type=\"roi_table\"))\n",
199
+ "print(\" - Masking Roi tables: \", ngff_image.tables.list(table_type=\"masking_roi_table\"))"
200
200
  ]
201
201
  },
202
202
  {
@@ -267,7 +267,9 @@
267
267
  "metadata": {},
268
268
  "outputs": [],
269
269
  "source": [
270
- "new_ngff_image = ngff_image.derive_new_image(\"../../data/new_ome.zarr\", name=\"new_image\")\n",
270
+ "new_ngff_image = ngff_image.derive_new_image(\n",
271
+ " \"../../data/new_ome.zarr\", name=\"new_image\"\n",
272
+ ")\n",
271
273
  "print(new_ngff_image)"
272
274
  ]
273
275
  },
@@ -19,7 +19,9 @@
19
19
  "\n",
20
20
  "from ngio.core.ngff_image import NgffImage\n",
21
21
  "\n",
22
- "ngff_image = NgffImage(\"../../data/20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr/B/03/0\")"
22
+ "ngff_image = NgffImage(\n",
23
+ " \"../../data/20200812-CardiomyocyteDifferentiation14-Cycle1_mip.zarr/B/03/0\"\n",
24
+ ")"
23
25
  ]
24
26
  },
25
27
  {
@@ -88,7 +90,9 @@
88
90
  "metadata": {},
89
91
  "outputs": [],
90
92
  "source": [
91
- "image_numpy = image.get_array(c=0, x=slice(0, 250), y=slice(0, 250), preserve_dimensions=False, mode=\"numpy\")\n",
93
+ "image_numpy = image.get_array(\n",
94
+ " c=0, x=slice(0, 250), y=slice(0, 250), preserve_dimensions=False, mode=\"numpy\"\n",
95
+ ")\n",
92
96
  "\n",
93
97
  "print(f\"{image_numpy.shape=}\")"
94
98
  ]
@@ -112,7 +116,9 @@
112
116
  "roi = roi_table.get_roi(\"FOV_1\")\n",
113
117
  "print(f\"{roi=}\")\n",
114
118
  "\n",
115
- "image_roi_1 = image.get_array_from_roi(roi=roi, c=0, preserve_dimensions=True, mode=\"dask\")\n",
119
+ "image_roi_1 = image.get_array_from_roi(\n",
120
+ " roi=roi, c=0, preserve_dimensions=True, mode=\"dask\"\n",
121
+ ")\n",
116
122
  "image_roi_1"
117
123
  ]
118
124
  },
@@ -277,29 +283,35 @@
277
283
  "print(f\"List of feature table: {ngff_image.tables.list(table_type='feature_table')}\")\n",
278
284
  "\n",
279
285
  "\n",
280
- "nuclei = ngff_image.labels.get_label('nuclei')\n",
286
+ "nuclei = ngff_image.labels.get_label(\"nuclei\")\n",
281
287
  "\n",
282
288
  "# Create a table with random features for each nuclei in each ROI\n",
283
289
  "list_of_records = []\n",
284
290
  "for roi in roi_table.rois:\n",
285
- " nuclei_in_roi = nuclei.get_array_from_roi(roi, mode='numpy')\n",
291
+ " nuclei_in_roi = nuclei.get_array_from_roi(roi, mode=\"numpy\")\n",
286
292
  " for nuclei_id in np.unique(nuclei_in_roi)[1:]:\n",
287
293
  " list_of_records.append(\n",
288
- " {\"label\": nuclei_id,\n",
289
- " \"feat1\": np.random.rand(),\n",
290
- " \"feat2\": np.random.rand(),\n",
291
- " \"ROI\": roi.infos.get(\"FieldIndex\")}\n",
292
- " )\n",
294
+ " {\n",
295
+ " \"label\": nuclei_id,\n",
296
+ " \"feat1\": np.random.rand(),\n",
297
+ " \"feat2\": np.random.rand(),\n",
298
+ " \"ROI\": roi.infos.get(\"FieldIndex\"),\n",
299
+ " }\n",
300
+ " )\n",
293
301
  "\n",
294
302
  "feat_df = pd.DataFrame.from_records(list_of_records)\n",
295
303
  "\n",
296
304
  "# Create a new feature table\n",
297
- "feat_table = ngff_image.tables.new(name='new_feature_table',\n",
298
- " label_image='../nuclei',\n",
299
- " table_type='feature_table',\n",
300
- " overwrite=True)\n",
301
- "\n",
302
- "print(f\"New list of feature table: {ngff_image.tables.list(table_type='feature_table')}\")\n",
305
+ "feat_table = ngff_image.tables.new(\n",
306
+ " name=\"new_feature_table\",\n",
307
+ " label_image=\"../nuclei\",\n",
308
+ " table_type=\"feature_table\",\n",
309
+ " overwrite=True,\n",
310
+ ")\n",
311
+ "\n",
312
+ "print(\n",
313
+ " f\"New list of feature table: {ngff_image.tables.list(table_type='feature_table')}\"\n",
314
+ ")\n",
303
315
  "feat_table.set_table(feat_df)\n",
304
316
  "feat_table.consolidate()\n",
305
317
  "\n",
@@ -37,7 +37,9 @@
37
37
  "\n",
38
38
  "from ngio.core import NgffImage\n",
39
39
  "\n",
40
- "ngff_image = NgffImage(\"../../data/20200812-CardiomyocyteDifferentiation14-Cycle1.zarr/B/03/0\")"
40
+ "ngff_image = NgffImage(\n",
41
+ " \"../../data/20200812-CardiomyocyteDifferentiation14-Cycle1.zarr/B/03/0\"\n",
42
+ ")"
41
43
  ]
42
44
  },
43
45
  {
@@ -53,9 +55,11 @@
53
55
  "metadata": {},
54
56
  "outputs": [],
55
57
  "source": [
56
- "mip_ngff = ngff_image.derive_new_image(\"../../data/20200812-CardiomyocyteDifferentiation14-Cycle1.zarr/B/03/0_mip\",\n",
57
- " name=\"MIP\",\n",
58
- " on_disk_shape=(1, 1, 2160, 5120))"
58
+ "mip_ngff = ngff_image.derive_new_image(\n",
59
+ " \"../../data/20200812-CardiomyocyteDifferentiation14-Cycle1.zarr/B/03/0_mip\",\n",
60
+ " name=\"MIP\",\n",
61
+ " on_disk_shape=(1, 1, 2160, 5120),\n",
62
+ ")"
59
63
  ]
60
64
  },
61
65
  {
@@ -93,15 +97,15 @@
93
97
  " patch = source_image.get_array_from_roi(roi)\n",
94
98
  " mip_patch = patch.max(axis=1, keepdims=True)\n",
95
99
  " mip_image.set_array_from_roi(patch=mip_patch, roi=roi)\n",
96
- " \n",
100
+ "\n",
97
101
  "print(\"MIP image saved\")\n",
98
102
  "\n",
99
103
  "plt.figure(figsize=(5, 5))\n",
100
104
  "plt.title(\"Mip\")\n",
101
105
  "plt.imshow(mip_image.on_disk_array[0, 0, :, :], cmap=\"gray\")\n",
102
- "plt.axis('off')\n",
106
+ "plt.axis(\"off\")\n",
103
107
  "plt.tight_layout()\n",
104
- "plt.show()\n"
108
+ "plt.show()"
105
109
  ]
106
110
  },
107
111
  {
@@ -136,9 +140,9 @@
136
140
  "axs[1].set_title(\"After consolidation\")\n",
137
141
  "axs[1].imshow(image_after_consolidation, cmap=\"gray\")\n",
138
142
  "for ax in axs:\n",
139
- " ax.axis('off')\n",
143
+ " ax.axis(\"off\")\n",
140
144
  "plt.tight_layout()\n",
141
- "plt.show()\n"
145
+ "plt.show()"
142
146
  ]
143
147
  },
144
148
  {
@@ -162,7 +166,7 @@
162
166
  "roi_list = []\n",
163
167
  "for roi in roi_table.rois:\n",
164
168
  " print(f\" - Processing ROI {roi.infos.get('field_index')}\")\n",
165
- " roi.z_length = 1 # In the MIP image, the z dimension is 1\n",
169
+ " roi.z_length = 1 # In the MIP image, the z dimension is 1\n",
166
170
  " roi_list.append(roi)\n",
167
171
  "\n",
168
172
  "mip_roi_table.set_rois(roi_list, overwrite=True)\n",
@@ -198,7 +202,7 @@
198
202
  "rand_cmap = ListedColormap(rand_cmap)\n",
199
203
  "\n",
200
204
  "\n",
201
- "def otsu_threshold_segmentation(image: np.ndarray, max_label:int) -> np.ndarray:\n",
205
+ "def otsu_threshold_segmentation(image: np.ndarray, max_label: int) -> np.ndarray:\n",
202
206
  " \"\"\"Simple segmentation using Otsu thresholding.\"\"\"\n",
203
207
  " threshold = threshold_otsu(image)\n",
204
208
  " binary = image > threshold\n",
@@ -271,11 +275,11 @@
271
275
  "axs[0].set_title(\"MIP\")\n",
272
276
  "axs[0].imshow(source_image.on_disk_array[0, 0], cmap=\"gray\")\n",
273
277
  "axs[1].set_title(\"Nuclei segmentation\")\n",
274
- "axs[1].imshow(nuclei_image.on_disk_array[0], cmap=rand_cmap, interpolation='nearest')\n",
278
+ "axs[1].imshow(nuclei_image.on_disk_array[0], cmap=rand_cmap, interpolation=\"nearest\")\n",
275
279
  "for ax in axs:\n",
276
- " ax.axis('off')\n",
280
+ " ax.axis(\"off\")\n",
277
281
  "plt.tight_layout()\n",
278
- "plt.show()\n"
282
+ "plt.show()"
279
283
  ]
280
284
  },
281
285
  {
@@ -29,6 +29,7 @@ classifiers = [
29
29
  "Programming Language :: Python :: 3.10",
30
30
  "Programming Language :: Python :: 3.11",
31
31
  "Programming Language :: Python :: 3.12",
32
+ "Programming Language :: Python :: 3.13",
32
33
  "Typing :: Typed",
33
34
  ]
34
35
  # add your package dependencies here
@@ -199,8 +200,6 @@ channels = ["conda-forge"]
199
200
  platforms = ["osx-arm64", "linux-64", "win-64"]
200
201
 
201
202
  [tool.pixi.pypi-dependencies]
202
- # zarr = { path = "../zarr-python/", editable = true }
203
- # anndata = { path = "../anndata/", editable = true }
204
203
  ngio = { path = ".", editable = true }
205
204
 
206
205
  [tool.pixi.feature.py310.dependencies]
@@ -152,7 +152,7 @@ class NgffImage:
152
152
 
153
153
  if meta.omero is None:
154
154
  raise NotImplementedError(
155
- "OMERO metadata not found. " " Please add OMERO metadata to the image."
155
+ "OMERO metadata not found. Please add OMERO metadata to the image."
156
156
  )
157
157
 
158
158
  channel_list = meta.omero.channels
@@ -269,7 +269,7 @@ class NgffImage:
269
269
 
270
270
  if meta.omero is None:
271
271
  raise NotImplementedError(
272
- "OMERO metadata not found. " " Please add OMERO metadata to the image."
272
+ "OMERO metadata not found. Please add OMERO metadata to the image."
273
273
  )
274
274
 
275
275
  channel_list = meta.omero.channels
@@ -67,17 +67,17 @@ def _build_empty_pyramid(
67
67
 
68
68
  if chunks is not None and len(on_disk_shape) != len(chunks):
69
69
  raise ValueError(
70
- "The shape and chunks must have the same number " "of dimensions."
70
+ "The shape and chunks must have the same number of dimensions."
71
71
  )
72
72
 
73
73
  if len(on_disk_shape) != len(scaling_factor):
74
74
  raise ValueError(
75
- "The shape and scaling factor must have the same number " "of dimensions."
75
+ "The shape and scaling factor must have the same number of dimensions."
76
76
  )
77
77
 
78
78
  if len(on_disk_shape) != len(on_disk_axis):
79
79
  raise ValueError(
80
- "The shape and on-disk axis must have the same number " "of dimensions."
80
+ "The shape and on-disk axis must have the same number of dimensions."
81
81
  )
82
82
 
83
83
  for dataset in image_meta.datasets:
@@ -50,8 +50,7 @@ def open_group_wrapper(
50
50
  if isinstance(store, zarr.Group):
51
51
  _group = _pass_through_group(store, mode=mode, zarr_format=zarr_format)
52
52
  ngio_logger.debug(
53
- f"Passing through group: {_group}, "
54
- f"located in store: {_group.store.path}"
53
+ f"Passing through group: {_group}, located in store: {_group.store.path}"
55
54
  )
56
55
  return _group
57
56
 
@@ -110,7 +110,13 @@ def vanilla_omero_v04_to_fractal(omero04: Omero04) -> Omero:
110
110
  for channel04 in omero04.channels:
111
111
  # Convert the window to a dictionary
112
112
  label = channel04.label
113
- wavelength_id = channel04.extra_fields.get("wavelength_id", label)
113
+
114
+ if "wavelength_id" in channel04.extra_fields:
115
+ # If the wavelength_id is present, pop it from the extra fields
116
+ # so that it is not added to the channel_visualisation
117
+ wavelength_id = channel04.extra_fields.pop("wavelength_id")
118
+ else:
119
+ wavelength_id = label
114
120
 
115
121
  if channel04.window is None:
116
122
  window04 = Window04(
@@ -161,6 +167,7 @@ def fractal_omero_to_vanilla_v04(omero: Omero) -> Omero04:
161
167
  color=channel.channel_visualisation.color,
162
168
  active=channel.channel_visualisation.active,
163
169
  window=window04,
170
+ wavelength_id=channel.wavelength_id,
164
171
  **channel.channel_visualisation.extra_fields,
165
172
  )
166
173
  list_channels04.append(channel04)
@@ -4,7 +4,48 @@ from typing import Literal
4
4
  import dask.array as da
5
5
  import numpy as np
6
6
  import zarr
7
- from scipy.ndimage import zoom
7
+ from scipy.ndimage import zoom as scipy_zoom
8
+
9
+
10
+ def _stacked_zoom(x, zoom_y, zoom_x, order=1, mode="grid-constant", grid_mode=True):
11
+ *rest, yshape, xshape = x.shape
12
+ x = x.reshape(-1, yshape, xshape)
13
+ scale_xy = (zoom_y, zoom_x)
14
+ x_out = np.stack(
15
+ [
16
+ scipy_zoom(x[i], scale_xy, order=order, mode=mode, grid_mode=True)
17
+ for i in range(x.shape[0])
18
+ ]
19
+ )
20
+ return x_out.reshape(*rest, *x_out.shape[1:])
21
+
22
+
23
+ def fast_zoom(x, zoom, order=1, mode="grid-constant", grid_mode=True, auto_stack=True):
24
+ """Fast zoom function.
25
+
26
+ Scipy zoom function that can handle singleton dimensions
27
+ but the performance degrades with the number of dimensions.
28
+
29
+ This function has two small optimizations:
30
+ - it removes singleton dimensions before calling zoom
31
+ - if it detects that the zoom is only on the last two dimensions
32
+ it stacks the first dimensions to call zoom only on the last two.
33
+ """
34
+ mask = np.isclose(x.shape, 1)
35
+ zoom = np.array(zoom)
36
+ singletons = tuple(np.where(mask)[0])
37
+ xs = np.squeeze(x, axis=singletons)
38
+ new_zoom = zoom[~mask]
39
+
40
+ *zoom_rest, zoom_y, zoom_x = new_zoom
41
+ if auto_stack and np.allclose(zoom_rest, 1):
42
+ xs = _stacked_zoom(
43
+ xs, zoom_y, zoom_x, order=order, mode=mode, grid_mode=grid_mode
44
+ )
45
+ else:
46
+ xs = scipy_zoom(xs, new_zoom, order=order, mode=mode, grid_mode=grid_mode)
47
+ x = np.expand_dims(xs, axis=singletons)
48
+ return x
8
49
 
9
50
 
10
51
  def _zoom_inputs_check(
@@ -73,7 +114,7 @@ def _dask_zoom(
73
114
  block_output_shape = tuple(np.ceil(better_source_chunks * _scale).astype(int))
74
115
 
75
116
  zoom_wrapper = partial(
76
- zoom, zoom=_scale, order=order, mode="grid-constant", grid_mode=True
117
+ fast_zoom, zoom=_scale, order=order, mode="grid-constant", grid_mode=True
77
118
  )
78
119
 
79
120
  out_array = da.map_blocks(
@@ -109,7 +150,7 @@ def _numpy_zoom(
109
150
  source_array=source_array, scale=scale, target_shape=target_shape
110
151
  )
111
152
 
112
- out_array = zoom(
153
+ out_array = fast_zoom(
113
154
  source_array, zoom=_scale, order=order, mode="grid-constant", grid_mode=True
114
155
  )
115
156
  assert isinstance(out_array, np.ndarray)
@@ -149,6 +190,7 @@ def on_disk_zoom(
149
190
  target_array = _dask_zoom(source_array, target_shape=target.shape, order=order)
150
191
 
151
192
  target_array = target_array.rechunk(target.chunks)
193
+ target_array.compute_chunk_sizes()
152
194
  target_array.to_zarr(target)
153
195
 
154
196
 
@@ -180,7 +222,7 @@ def on_disk_coarsen(
180
222
  coarsening_setup[i] = int(factor)
181
223
  else:
182
224
  raise ValueError(
183
- "Coarsening factor must be an integer, got " f"{factor} on axis {i}"
225
+ f"Coarsening factor must be an integer, got {factor} on axis {i}"
184
226
  )
185
227
 
186
228
  out_target = da.coarsen(
@@ -1,5 +1,7 @@
1
1
  from pathlib import Path
2
2
 
3
+ import pytest
4
+
3
5
 
4
6
  class TestNgffImage:
5
7
  def test_ngff_image(self, ome_zarr_image_v04_path: Path) -> None:
@@ -57,3 +59,29 @@ class TestNgffImage:
57
59
 
58
60
  assert ngff_image.tables.list() == new_ngff_image.tables.list()
59
61
  assert ngff_image.labels.list() == new_ngff_image.labels.list()
62
+
63
+ @pytest.mark.parametrize(
64
+ "shape, axis, chunks",
65
+ [
66
+ ((1, 4, 1, 1945, 1945), ("t", "c", "z", "y", "x"), (1, 1, 1, 1000, 1000)),
67
+ ((1, 4, 1, 1945, 1945), ("t", "c", "z", "y", "x"), None),
68
+ ((1, 4, 1, 1945, 2000), ("t", "c", "z", "y", "x"), None),
69
+ ((1, 1, 1000, 1000), ("c", "z", "y", "x"), (1, 1, 1000, 1000)),
70
+ ((1, 1, 1000, 1000), ("c", "z", "y", "x"), None),
71
+ ((739, 1033), ("y", "x"), (53, 173)),
72
+ ],
73
+ )
74
+ def test_ngff_image_consolidate(self, tmp_path, shape, axis, chunks) -> None:
75
+ from ngio import NgffImage
76
+ from ngio.core.utils import create_empty_ome_zarr_image
77
+
78
+ ome_zarr = tmp_path / "test_consolidate.zarr"
79
+ create_empty_ome_zarr_image(
80
+ ome_zarr,
81
+ on_disk_shape=shape,
82
+ on_disk_axis=axis,
83
+ chunks=chunks,
84
+ )
85
+
86
+ image = NgffImage(ome_zarr).get_image()
87
+ image.consolidate()
@@ -22,6 +22,7 @@ class TestUtils:
22
22
  )
23
23
 
24
24
  assert meta.channel_labels == ["DAPI", "nanog", "Lamin B1"]
25
+ assert meta.channel_wavelength_ids == ["A01_C01", "A02_C02", "A03_C03"]
25
26
  np.testing.assert_array_equal(meta.pixel_size(idx=0).zyx, [1.0, 1.0, 1.0])
26
27
  np.testing.assert_array_equal(meta.scale(idx=0), [1.0, 1.0, 1.0, 1.0, 1.0])
27
28
  np.testing.assert_array_equal(meta.pixel_size(path="2").zyx, [1.0, 4.0, 4.0])
@@ -4,6 +4,36 @@ import pytest
4
4
 
5
5
 
6
6
  class TestOMEZarrHandlerV04:
7
+ def test_create_image_meta(self, tmp_path):
8
+ from ngio.ngff_meta import create_image_metadata
9
+ from ngio.ngff_meta.v04.zarr_utils import (
10
+ fractal_ngff_image_meta_to_vanilla_v04,
11
+ vanilla_ngff_image_meta_v04_to_fractal,
12
+ )
13
+
14
+ meta = create_image_metadata(
15
+ on_disk_axis=("t", "c", "z", "y", "x"),
16
+ pixel_sizes=None,
17
+ xy_scaling_factor=2.0,
18
+ z_scaling_factor=1.0,
19
+ time_spacing=1.0,
20
+ time_units="s",
21
+ levels=5,
22
+ name="test",
23
+ channel_labels=["DAPI", "nanog", "Lamin B1"],
24
+ channel_wavelengths=["A01_C01", "A02_C02", "A03_C03"],
25
+ channel_visualization=None,
26
+ omero_kwargs=None,
27
+ version="0.4",
28
+ )
29
+
30
+ meta04 = fractal_ngff_image_meta_to_vanilla_v04(meta)
31
+ meta2 = vanilla_ngff_image_meta_v04_to_fractal(meta04)
32
+ assert meta.channel_labels == meta2.channel_labels
33
+ assert meta.channel_wavelength_ids == meta2.channel_wavelength_ids
34
+ assert meta.axes_names == meta2.axes_names
35
+ assert meta.scale(path="2") == meta2.scale(path="2")
36
+
7
37
  def test_basic_workflow(self, ome_zarr_image_v04_path):
8
38
  from ngio.ngff_meta import get_ngff_image_meta_handler
9
39
  from ngio.ngff_meta.v04.zarr_utils import NgffImageMeta04
@@ -75,3 +75,28 @@ class TestZoom:
75
75
  source, target = zarr_zoom_2d_array_not_int
76
76
  with pytest.raises(ValueError):
77
77
  self._test_coarsen(source, target)
78
+
79
+ @pytest.mark.parametrize(
80
+ "shape, zoom_factor",
81
+ [
82
+ ((10, 1, 1, 30, 30), (1, 1, 1, 0.5, 0.5)), # with time
83
+ ((1, 1, 1, 300, 300), (1, 1, 1, 0.5, 0.5)), # with singletons
84
+ ((1, 4, 10, 30, 30), (1, 1, 1, 0.5, 0.5)), # with channels and z
85
+ ((1, 4, 10, 30, 30), (1, 1, 0.3234, 0.5, 0.5)), # with channels and z
86
+ ((5, 4, 10, 30, 30), (0.5, 1, 0.3234, 0.5, 0.5)), # with channels and z
87
+ ((1, 4, 10, 30, 30), (1, 1, 1, 0.5323, 0.5231)), # with channels and z
88
+ ((4, 300, 300), (1, 0.5, 0.5)), # without time and channels
89
+ ((3000, 3000), (0.5, 0.5)), # without time and channels
90
+ ],
91
+ )
92
+ def test_fast_zoom(self, shape, zoom_factor) -> None:
93
+ from scipy.ndimage import zoom
94
+
95
+ from ngio.pipes._zoom_utils import fast_zoom
96
+
97
+ x = np.random.rand(*shape)
98
+ out1 = fast_zoom(
99
+ x, zoom=zoom_factor, order=1, mode="grid-constant", grid_mode=True
100
+ )
101
+ out2 = zoom(x, zoom=zoom_factor, order=1, mode="grid-constant", grid_mode=True)
102
+ np.testing.assert_allclose(out1, out2)
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