spatialdata-io 0.7.0__tar.gz → 0.7.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.
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/PKG-INFO +1 -1
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/_version.py +2 -2
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/__main__.py +4 -4
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/macsima.py +121 -24
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/xenium.py +2 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/test_macsima.py +250 -31
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/test_xenium.py +3 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.bumpversion.cfg +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.codecov.yaml +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.editorconfig +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.github/codecov.yml +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.github/release.yml +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.github/workflows/prepare_test_data.yaml +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.github/workflows/release.yaml +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.github/workflows/test.yaml +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.gitignore +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.mypy.ini +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.pre-commit-config.yaml +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/.readthedocs.yaml +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/LICENSE +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/README.md +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/asv.conf.json +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/benchmarks/__init__.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/benchmarks/benchmark_image.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/benchmarks/benchmark_imports.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/benchmarks/benchmark_xenium.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/Makefile +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/_static/.gitkeep +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/_templates/.gitkeep +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/_templates/autosummary/class.rst +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/api.md +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/changelog.md +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/cli.md +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/conf.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/contributing.md +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/extensions/.gitkeep +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/extensions/typed_returns.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/index.md +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/make.bat +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/references.bib +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/docs/references.md +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/pyproject.toml +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/__init__.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/_constants/__init__.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/_constants/_constants.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/_constants/_enum.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/_docs.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/_utils.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/converters/__init__.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/converters/generic_to_zarr.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/converters/legacy_anndata.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/experimental/__init__.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/__init__.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/_utils/__init__.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/_utils/_image.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/_utils/_utils.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/codex.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/cosmx.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/curio.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/dbit.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/generic.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/iss.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/mcmicro.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/merscope.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/seqfish.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/steinbock.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/stereoseq.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/visium.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/readers/visium_hd.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/__init__.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/_utils.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/conftest.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/converters/__init__.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/converters/test_legacy_anndata.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/readers/test_utils_image.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/test_cli_alignment.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/test_generic.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/test_init.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/test_seqfish.py +0 -0
- {spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/tests/test_visium_hd.py +0 -0
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.7.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 7,
|
|
21
|
+
__version__ = version = '0.7.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 7, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -794,9 +794,9 @@ def xenium_wrapper(
|
|
|
794
794
|
@_input_output_click_options
|
|
795
795
|
@click.option(
|
|
796
796
|
"--parsing-style",
|
|
797
|
-
type=click.Choice(["
|
|
798
|
-
default="
|
|
799
|
-
help="Parsing style for MACSima data. [default:
|
|
797
|
+
type=click.Choice(["processed_single_folder", "processed_multiple_folders", "raw"]),
|
|
798
|
+
default="processed_single_folder",
|
|
799
|
+
help="Parsing style for MACSima data. [default: processed_single_folder]",
|
|
800
800
|
)
|
|
801
801
|
@click.option(
|
|
802
802
|
"--filter-folder-names",
|
|
@@ -870,7 +870,7 @@ def macsima_wrapper(
|
|
|
870
870
|
input: str,
|
|
871
871
|
output: str,
|
|
872
872
|
*,
|
|
873
|
-
parsing_style: str = "
|
|
873
|
+
parsing_style: str = "processed_single_folder",
|
|
874
874
|
filter_folder_names: list[str] | None = None,
|
|
875
875
|
subset: int | None = None,
|
|
876
876
|
c_subset: int | None = None,
|
|
@@ -48,7 +48,6 @@ class MACSimaParsingStyle(ModeEnum):
|
|
|
48
48
|
PROCESSED_SINGLE_FOLDER = "processed_single_folder"
|
|
49
49
|
PROCESSED_MULTIPLE_FOLDERS = "processed_multiple_folders"
|
|
50
50
|
RAW = "raw"
|
|
51
|
-
AUTO = "auto"
|
|
52
51
|
|
|
53
52
|
|
|
54
53
|
@dataclass
|
|
@@ -62,6 +61,8 @@ class ChannelMetadata:
|
|
|
62
61
|
roi: int
|
|
63
62
|
fluorophore: str
|
|
64
63
|
exposure: float
|
|
64
|
+
translation_x: int
|
|
65
|
+
translation_y: int
|
|
65
66
|
clone: str | None = None # For example DAPI doesnt have a clone
|
|
66
67
|
|
|
67
68
|
|
|
@@ -104,6 +105,8 @@ class MultiChannelImage:
|
|
|
104
105
|
fluorophore=metadata["fluorophore"],
|
|
105
106
|
clone=metadata["clone"],
|
|
106
107
|
exposure=metadata["exposure"],
|
|
108
|
+
translation_x=metadata["translation_x"],
|
|
109
|
+
translation_y=metadata["translation_y"],
|
|
107
110
|
)
|
|
108
111
|
)
|
|
109
112
|
|
|
@@ -126,12 +129,11 @@ class MultiChannelImage:
|
|
|
126
129
|
),
|
|
127
130
|
)
|
|
128
131
|
imgs = [imread(img, **imread_kwargs) for img in valid_files]
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
)
|
|
132
|
+
|
|
133
|
+
# Pad images to same dimensions if necessary
|
|
134
|
+
if cls._check_for_differing_xy_dimensions(imgs):
|
|
135
|
+
imgs = cls._pad_images(imgs, channel_metadata)
|
|
136
|
+
|
|
135
137
|
# create MultiChannelImage object with imgs and metadata
|
|
136
138
|
output = cls(data=imgs, metadata=channel_metadata)
|
|
137
139
|
return output
|
|
@@ -221,10 +223,81 @@ class MultiChannelImage:
|
|
|
221
223
|
def get_stack(self) -> da.Array:
|
|
222
224
|
return da.stack(self.data, axis=0).squeeze(axis=1)
|
|
223
225
|
|
|
226
|
+
@staticmethod
|
|
227
|
+
def _check_for_differing_xy_dimensions(imgs: list[da.Array]) -> bool:
|
|
228
|
+
"""Checks whether any of the images have differing extent in dimensions X and Y."""
|
|
229
|
+
# Shape has order CYX
|
|
230
|
+
dims_x = [x.shape[2] for x in imgs]
|
|
231
|
+
dims_y = [x.shape[1] for x in imgs]
|
|
232
|
+
|
|
233
|
+
dims_x_different = len(set(dims_x)) != 1
|
|
234
|
+
dims_y_different = len(set(dims_y)) != 1
|
|
235
|
+
|
|
236
|
+
different_dimensions = any([dims_x_different, dims_y_different])
|
|
237
|
+
|
|
238
|
+
warnings.warn(
|
|
239
|
+
"Supplied images have different dimensions!",
|
|
240
|
+
UserWarning,
|
|
241
|
+
stacklevel=2,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return different_dimensions
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def _pad_images(imgs: list[da.Array], channel_metadata: list[ChannelMetadata]) -> list[da.Array]:
|
|
248
|
+
"""Pad all images to the same dimensions in X and Y with 0s.
|
|
249
|
+
|
|
250
|
+
Padding obeys translations stored in ome metadata.
|
|
251
|
+
If no translations are found, padding is added only away from the origin:
|
|
252
|
+
on the right side for X and at the
|
|
253
|
+
bottom for Y, so the top-left corner of each image stays aligned.
|
|
254
|
+
"""
|
|
255
|
+
min_translation_x = min(metadata.translation_x for metadata in channel_metadata)
|
|
256
|
+
min_translation_y = min(metadata.translation_y for metadata in channel_metadata)
|
|
257
|
+
normalized_translations_x = [metadata.translation_x - min_translation_x for metadata in channel_metadata]
|
|
258
|
+
normalized_translations_y = [metadata.translation_y - min_translation_y for metadata in channel_metadata]
|
|
259
|
+
|
|
260
|
+
dims_x_max = max(
|
|
261
|
+
img.shape[2] + translation_x for img, translation_x in zip(imgs, normalized_translations_x, strict=True)
|
|
262
|
+
)
|
|
263
|
+
dims_y_max = max(
|
|
264
|
+
img.shape[1] + translation_y for img, translation_y in zip(imgs, normalized_translations_y, strict=True)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
warnings.warn(
|
|
268
|
+
f"Padding images with 0s to same size of ({dims_y_max}, {dims_x_max})",
|
|
269
|
+
UserWarning,
|
|
270
|
+
stacklevel=2,
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
padded_imgs = []
|
|
274
|
+
for img, translation_x, translation_y in zip(
|
|
275
|
+
imgs, normalized_translations_x, normalized_translations_y, strict=True
|
|
276
|
+
):
|
|
277
|
+
pad_y_prepend = translation_y
|
|
278
|
+
pad_x_prepend = translation_x
|
|
279
|
+
# For appending, we check how much space is already covered by the original image, and we take into account how many pixels were prepended
|
|
280
|
+
# If there still is a difference, we append 0 to get to the same image size
|
|
281
|
+
pad_y_append = dims_y_max - img.shape[1] - pad_y_prepend
|
|
282
|
+
pad_x_append = dims_x_max - img.shape[2] - pad_x_prepend
|
|
283
|
+
# Only pad if necessary
|
|
284
|
+
if (pad_x_prepend, pad_x_append, pad_y_prepend, pad_y_append) != (0, 0, 0, 0):
|
|
285
|
+
pad_width = (
|
|
286
|
+
# c axis: no pad
|
|
287
|
+
(0, 0),
|
|
288
|
+
(pad_y_prepend, pad_y_append),
|
|
289
|
+
(pad_x_prepend, pad_x_append),
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
img = da.pad(img, pad_width, mode="constant", constant_values=0)
|
|
293
|
+
padded_imgs.append(img)
|
|
294
|
+
|
|
295
|
+
return padded_imgs
|
|
296
|
+
|
|
224
297
|
|
|
225
298
|
def macsima(
|
|
226
299
|
path: str | Path,
|
|
227
|
-
parsing_style: MACSimaParsingStyle | str = MACSimaParsingStyle.
|
|
300
|
+
parsing_style: MACSimaParsingStyle | str = MACSimaParsingStyle.PROCESSED_SINGLE_FOLDER,
|
|
228
301
|
filter_folder_names: list[str] | None = None,
|
|
229
302
|
imread_kwargs: Mapping[str, Any] = MappingProxyType({}),
|
|
230
303
|
subset: int | None = None,
|
|
@@ -245,6 +318,8 @@ def macsima(
|
|
|
245
318
|
This function reads images from a MACSima cyclic imaging experiment. MACSima data follows the OME-TIFF specificiation.
|
|
246
319
|
All metadata is parsed from the OME metadata. The exact metadata schema can change between software versions of MACSiQView.
|
|
247
320
|
As there is no public specification of the metadata fields used, please consider the provided test data sets as ground truth to guide development.
|
|
321
|
+
If images from different cycles differ in spatial dimensions, they are zero-padded on the right (X) and bottom (Y) to match
|
|
322
|
+
the largest dimensions, keeping the top-left origin aligned; a warning is emitted in that case.
|
|
248
323
|
|
|
249
324
|
.. seealso::
|
|
250
325
|
|
|
@@ -255,7 +330,8 @@ def macsima(
|
|
|
255
330
|
path
|
|
256
331
|
Path to the directory containing the data.
|
|
257
332
|
parsing_style
|
|
258
|
-
Parsing style to use. If ``
|
|
333
|
+
Parsing style to use. If ``processed_single_folder``, all subfolders of ``path`` are combined into a stack.
|
|
334
|
+
If ``processed_multiple_folders``, a stack is created for each folder directly beneath ``path``.
|
|
259
335
|
filter_folder_names
|
|
260
336
|
List of folder names to filter out when parsing multiple folders.
|
|
261
337
|
imread_kwargs
|
|
@@ -295,19 +371,13 @@ def macsima(
|
|
|
295
371
|
if not isinstance(parsing_style, MACSimaParsingStyle):
|
|
296
372
|
parsing_style = MACSimaParsingStyle(parsing_style)
|
|
297
373
|
|
|
298
|
-
if parsing_style == MACSimaParsingStyle.AUTO:
|
|
299
|
-
assert path.is_dir(), f"Path {path} is not a directory."
|
|
300
|
-
|
|
301
|
-
if any(p.suffix in [".tif", ".tiff"] for p in path.iterdir()):
|
|
302
|
-
# if path contains tifs, do parse_processed_folder on path
|
|
303
|
-
parsing_style = MACSimaParsingStyle.PROCESSED_SINGLE_FOLDER
|
|
304
|
-
elif all(p.is_dir() for p in path.iterdir() if not p.name.startswith(".")):
|
|
305
|
-
# if path contains only folders or hidden files, do parse_processed_folder on each folder
|
|
306
|
-
parsing_style = MACSimaParsingStyle.PROCESSED_MULTIPLE_FOLDERS
|
|
307
|
-
else:
|
|
308
|
-
raise ValueError(f"Cannot determine parsing style for path {path}. Please specify the parsing style.")
|
|
309
|
-
|
|
310
374
|
if parsing_style == MACSimaParsingStyle.PROCESSED_SINGLE_FOLDER:
|
|
375
|
+
if filter_folder_names:
|
|
376
|
+
warnings.warn(
|
|
377
|
+
"single_processed_folder was requested but filter_folder_names was specified. Note that it is ignored here, filtering only happens for processed_multi_folders!",
|
|
378
|
+
UserWarning,
|
|
379
|
+
stacklevel=2,
|
|
380
|
+
)
|
|
311
381
|
return parse_processed_folder(
|
|
312
382
|
path=path,
|
|
313
383
|
imread_kwargs=imread_kwargs,
|
|
@@ -332,6 +402,9 @@ def macsima(
|
|
|
332
402
|
for p in path.iterdir()
|
|
333
403
|
if p.is_dir() and (not filter_folder_names or not any(f in p.name for f in filter_folder_names))
|
|
334
404
|
]:
|
|
405
|
+
if not len(list(p.glob("*.tif*"))):
|
|
406
|
+
warnings.warn(f"No tif files found in {p}, skipping it!", UserWarning, stacklevel=2)
|
|
407
|
+
continue
|
|
335
408
|
sdatas[p.stem] = parse_processed_folder(
|
|
336
409
|
path=p,
|
|
337
410
|
imread_kwargs=imread_kwargs,
|
|
@@ -415,6 +488,25 @@ def _get_software_major_version(version: str) -> int:
|
|
|
415
488
|
return major
|
|
416
489
|
|
|
417
490
|
|
|
491
|
+
def _get_translations(ome: OME) -> dict[str, int]:
|
|
492
|
+
try:
|
|
493
|
+
translations = {
|
|
494
|
+
"translation_x": ome.images[0].pixels.planes[0].position_x,
|
|
495
|
+
"translation_y": ome.images[0].pixels.planes[0].position_y,
|
|
496
|
+
}
|
|
497
|
+
# If the position attributes are not present the values will be None and we default to (0,0)
|
|
498
|
+
if any(v is None for v in translations.values()):
|
|
499
|
+
logger.debug(f"No translation found for {ome.images[0].name}, defaulting to (0, 0)")
|
|
500
|
+
translations = {"translation_x": 0, "translation_y": 0}
|
|
501
|
+
|
|
502
|
+
# In case the ome is faulty, also default to (0,0)
|
|
503
|
+
except AttributeError:
|
|
504
|
+
logger.debug(f"No translation found for {ome.images[0].name}, defaulting to (0, 0)")
|
|
505
|
+
translations = {"translation_x": 0, "translation_y": 0}
|
|
506
|
+
|
|
507
|
+
return translations
|
|
508
|
+
|
|
509
|
+
|
|
418
510
|
def _parse_v0_ome_metadata(ome: OME) -> dict[str, Any]:
|
|
419
511
|
"""Parse Legacy Format of OME Metadata (software version 0.x.x)."""
|
|
420
512
|
logger.debug("Parsing OME metadata expecting version 0 format")
|
|
@@ -592,12 +684,16 @@ def _parse_ome_metadata(ome: OME) -> dict[str, Any]:
|
|
|
592
684
|
major = _get_software_major_version(version_str)
|
|
593
685
|
|
|
594
686
|
if major == 0:
|
|
595
|
-
|
|
687
|
+
metadata = _parse_v0_ome_metadata(ome)
|
|
596
688
|
elif major == 1:
|
|
597
|
-
|
|
689
|
+
metadata = _parse_v1_ome_metadata(ome)
|
|
598
690
|
else:
|
|
599
691
|
raise ValueError("Unknown software version, cannot determine parser")
|
|
600
692
|
|
|
693
|
+
translations = _get_translations(ome)
|
|
694
|
+
metadata.update(translations)
|
|
695
|
+
return metadata
|
|
696
|
+
|
|
601
697
|
|
|
602
698
|
def parse_metadata(path: Path) -> dict[str, Any]:
|
|
603
699
|
"""Parse metadata for a file.
|
|
@@ -625,7 +721,7 @@ def parse_processed_folder(
|
|
|
625
721
|
nuclei_channel_name: str = "DAPI",
|
|
626
722
|
split_threshold_nuclei_channel: int | None = 2,
|
|
627
723
|
skip_rounds: list[int] | None = None,
|
|
628
|
-
file_pattern: str = "
|
|
724
|
+
file_pattern: str = "**/*.tif*",
|
|
629
725
|
include_cycle_in_channel_name: bool = False,
|
|
630
726
|
) -> SpatialData:
|
|
631
727
|
"""Parse a single folder containing images from a cyclical imaging platform."""
|
|
@@ -760,6 +856,7 @@ def create_table(mci: MultiChannelImage) -> ad.AnnData:
|
|
|
760
856
|
clones = mci.get_clones()
|
|
761
857
|
exposures = mci.get_exposures()
|
|
762
858
|
|
|
859
|
+
# We dont add the translations, because they are usually not interesting for the user
|
|
763
860
|
df = pd.DataFrame(
|
|
764
861
|
{
|
|
765
862
|
"name": names,
|
|
@@ -605,6 +605,7 @@ def _get_polygons(
|
|
|
605
605
|
else:
|
|
606
606
|
# Use cell_id (str) as GeoDataFrame index.
|
|
607
607
|
geo_df = GeoDataFrame({"geometry": geoms}, index=indices_mapping["cell_id"].values)
|
|
608
|
+
geo_df.index.name = "cell_id"
|
|
608
609
|
else:
|
|
609
610
|
# Fall back to extracting unique cell IDs from parquet (slow for large_string columns).
|
|
610
611
|
# Triggered when indices_mapping is None: v < 1.3.0 (both nuclei and cells, because
|
|
@@ -617,6 +618,7 @@ def _get_polygons(
|
|
|
617
618
|
unique_ids = id_col.filter(np.concatenate([[True], change_mask])).to_pylist()
|
|
618
619
|
index = _decode_cell_id_column(pd.Series(unique_ids))
|
|
619
620
|
geo_df = GeoDataFrame({"geometry": geoms}, index=index.values)
|
|
621
|
+
geo_df.index.name = "cell_id"
|
|
620
622
|
|
|
621
623
|
scale = Scale([1.0 / specs["pixel_size"], 1.0 / specs["pixel_size"]], axes=("x", "y"))
|
|
622
624
|
return ShapesModel.parse(geo_df, transformations={"global": scale})
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import contextlib
|
|
1
2
|
import math
|
|
3
|
+
import os
|
|
2
4
|
import shutil
|
|
3
5
|
from copy import deepcopy
|
|
4
6
|
from pathlib import Path
|
|
@@ -12,7 +14,12 @@ import pytest
|
|
|
12
14
|
from click.testing import CliRunner
|
|
13
15
|
from ome_types import OME
|
|
14
16
|
from ome_types.model import (
|
|
17
|
+
Image,
|
|
15
18
|
MapAnnotation,
|
|
19
|
+
Pixels,
|
|
20
|
+
Pixels_DimensionOrder,
|
|
21
|
+
PixelType,
|
|
22
|
+
Plane,
|
|
16
23
|
Plate,
|
|
17
24
|
Reagent,
|
|
18
25
|
Screen,
|
|
@@ -30,12 +37,12 @@ from spatialdata_io.readers.macsima import (
|
|
|
30
37
|
_collect_map_annotation_values,
|
|
31
38
|
_get_software_major_version,
|
|
32
39
|
_get_software_version,
|
|
40
|
+
_get_translations,
|
|
33
41
|
_parse_ome_metadata,
|
|
34
42
|
_parse_v0_ome_metadata,
|
|
35
43
|
_parse_v1_ome_metadata,
|
|
36
44
|
macsima,
|
|
37
45
|
)
|
|
38
|
-
from tests._utils import skip_if_below_python_version
|
|
39
46
|
|
|
40
47
|
RNG = da.random.default_rng(seed=0)
|
|
41
48
|
|
|
@@ -52,21 +59,25 @@ if not (Path("./data/OMAP10_small").exists() or Path("./data/OMAP23_small").exis
|
|
|
52
59
|
def make_ChannelMetadata(
|
|
53
60
|
name: str,
|
|
54
61
|
cycle: int,
|
|
55
|
-
fluorophore: str
|
|
56
|
-
exposure: float
|
|
57
|
-
imagetype: str
|
|
58
|
-
well: str
|
|
59
|
-
roi: int
|
|
62
|
+
fluorophore: str = "",
|
|
63
|
+
exposure: float = 0.0,
|
|
64
|
+
imagetype: str = "StainCycle",
|
|
65
|
+
well: str = "A01",
|
|
66
|
+
roi: int = 0,
|
|
67
|
+
translation_x: int = 0,
|
|
68
|
+
translation_y: int = 0,
|
|
60
69
|
) -> ChannelMetadata:
|
|
61
70
|
"""Helper to construct ChannelMetadata with required defaults."""
|
|
62
71
|
return ChannelMetadata(
|
|
63
72
|
name=name,
|
|
64
73
|
cycle=cycle,
|
|
65
|
-
fluorophore=fluorophore
|
|
66
|
-
exposure=exposure
|
|
67
|
-
imagetype=imagetype
|
|
68
|
-
well=well
|
|
69
|
-
|
|
74
|
+
fluorophore=fluorophore,
|
|
75
|
+
exposure=exposure,
|
|
76
|
+
imagetype=imagetype,
|
|
77
|
+
well=well,
|
|
78
|
+
translation_x=translation_x,
|
|
79
|
+
translation_y=translation_y,
|
|
80
|
+
roi=roi,
|
|
70
81
|
)
|
|
71
82
|
|
|
72
83
|
|
|
@@ -93,7 +104,7 @@ def test_exception_on_no_valid_files(tmp_path: Path) -> None:
|
|
|
93
104
|
# Write a tiff file without metadata
|
|
94
105
|
height = 10
|
|
95
106
|
width = 10
|
|
96
|
-
arr = np.zeros((height, width
|
|
107
|
+
arr = np.zeros((1, height, width), dtype=np.uint16)
|
|
97
108
|
path_no_metadata = Path(tmp_path) / "tiff_no_metadata.tiff"
|
|
98
109
|
imwrite(path_no_metadata, arr, metadata=None, description=None, software=None, datetime=None)
|
|
99
110
|
|
|
@@ -101,7 +112,114 @@ def test_exception_on_no_valid_files(tmp_path: Path) -> None:
|
|
|
101
112
|
macsima(tmp_path)
|
|
102
113
|
|
|
103
114
|
|
|
104
|
-
|
|
115
|
+
def test_multiple_subfolder_parsing_skips_emtpy_folders(tmp_path: Path) -> None:
|
|
116
|
+
parent_folder = tmp_path / "test_folder"
|
|
117
|
+
shutil.copytree("./data/OMAP23_small", parent_folder / "OMAP23_small")
|
|
118
|
+
os.makedirs(parent_folder / "empty_folder")
|
|
119
|
+
|
|
120
|
+
with pytest.warns(UserWarning, match="No tif files found in .* skipping it"):
|
|
121
|
+
sdata = macsima(parent_folder, parsing_style="processed_multiple_folders")
|
|
122
|
+
assert len(sdata.images.keys()) == 1
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
@pytest.mark.parametrize(
|
|
126
|
+
"dimensions,expected",
|
|
127
|
+
[
|
|
128
|
+
(((10, 10), (10, 10)), False),
|
|
129
|
+
(((10, 10), (15, 10)), True),
|
|
130
|
+
(((10, 10), (10, 15)), True),
|
|
131
|
+
(((15, 10), (10, 15)), True),
|
|
132
|
+
],
|
|
133
|
+
)
|
|
134
|
+
def test_check_differing_dimensions_works(dimensions: tuple[tuple[int, int], tuple[int, int]], expected: bool) -> None:
|
|
135
|
+
imgs = []
|
|
136
|
+
for img_dim in dimensions:
|
|
137
|
+
arr = da.from_array(np.ones((1, img_dim[0], img_dim[1]), dtype=np.uint16))
|
|
138
|
+
imgs.append(arr)
|
|
139
|
+
|
|
140
|
+
ctx = (
|
|
141
|
+
pytest.warns(UserWarning, match="Supplied images have different dimensions!")
|
|
142
|
+
if expected
|
|
143
|
+
else contextlib.nullcontext()
|
|
144
|
+
)
|
|
145
|
+
with ctx:
|
|
146
|
+
assert MultiChannelImage._check_for_differing_xy_dimensions(imgs) == expected
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_padding_on_differing_dimensions() -> None:
|
|
150
|
+
# Simple test where all translations are 0
|
|
151
|
+
# Here we expect to pad to the largest element.
|
|
152
|
+
heights = [10, 10, 15, 20]
|
|
153
|
+
widths = [10, 15, 10, 20]
|
|
154
|
+
|
|
155
|
+
imgs = []
|
|
156
|
+
for height, width in zip(heights, widths, strict=True):
|
|
157
|
+
arr = da.from_array(np.ones((1, height, width), dtype=np.uint16))
|
|
158
|
+
imgs.append(arr)
|
|
159
|
+
|
|
160
|
+
channel_metadata = [make_ChannelMetadata(name="test", cycle=1)] * 4
|
|
161
|
+
with pytest.warns(UserWarning, match="Padding images with 0s to same size of \\(20, 20\\)"):
|
|
162
|
+
imgs_padded = MultiChannelImage._pad_images(imgs, channel_metadata)
|
|
163
|
+
for img in imgs_padded:
|
|
164
|
+
assert img.shape == (1, 20, 20)
|
|
165
|
+
|
|
166
|
+
# More complex with non-zero translations
|
|
167
|
+
# First test that padding does the minimal padding necessary.
|
|
168
|
+
# To do this create images with very large, but identical translations. Since all of these should be normalized out we expect size 20x20 again.
|
|
169
|
+
heights = [10, 10, 15, 20]
|
|
170
|
+
widths = [10, 15, 10, 20]
|
|
171
|
+
|
|
172
|
+
imgs = []
|
|
173
|
+
for height, width in zip(heights, widths, strict=True):
|
|
174
|
+
arr = da.from_array(np.ones((1, height, width), dtype=np.uint16))
|
|
175
|
+
imgs.append(arr)
|
|
176
|
+
channel_metadata = channel_metadata = [
|
|
177
|
+
make_ChannelMetadata(name="test", cycle=1, translation_x=100, translation_y=100)
|
|
178
|
+
] * 4
|
|
179
|
+
with pytest.warns(UserWarning, match="Padding images with 0s to same size of \\(20, 20\\)"):
|
|
180
|
+
imgs_padded = MultiChannelImage._pad_images(imgs, channel_metadata)
|
|
181
|
+
for img in imgs_padded:
|
|
182
|
+
assert img.shape == (1, 20, 20)
|
|
183
|
+
|
|
184
|
+
# Test with differing translations but same size.
|
|
185
|
+
# As we translate the first image by 2 in x and 3 in y, we expect a 13x12 image
|
|
186
|
+
heights = [10, 10]
|
|
187
|
+
widths = [10, 10]
|
|
188
|
+
|
|
189
|
+
imgs = []
|
|
190
|
+
for height, width in zip(heights, widths, strict=True):
|
|
191
|
+
arr = da.from_array(np.ones((1, height, width), dtype=np.uint16))
|
|
192
|
+
imgs.append(arr)
|
|
193
|
+
channel_metadata = channel_metadata = [
|
|
194
|
+
make_ChannelMetadata(name="test", cycle=1, translation_x=2, translation_y=3),
|
|
195
|
+
make_ChannelMetadata(name="test", cycle=1, translation_x=0, translation_y=0),
|
|
196
|
+
]
|
|
197
|
+
with pytest.warns(UserWarning, match="Padding images with 0s to same size of \\(13, 12\\)"):
|
|
198
|
+
imgs_padded = MultiChannelImage._pad_images(imgs, channel_metadata)
|
|
199
|
+
for img in imgs_padded:
|
|
200
|
+
assert img.shape == (1, 13, 12)
|
|
201
|
+
|
|
202
|
+
# Final test with differing image sizes, and translations that need to be normalized
|
|
203
|
+
# For the total size, we need to check the sum of each image dimension + normalized translation
|
|
204
|
+
# Here that would be image 2, with y = 15 + 5 - 3 = 17 (normalized to other image!) and x = 15 + 5 - 2 = 18
|
|
205
|
+
|
|
206
|
+
heights = [10, 15]
|
|
207
|
+
widths = [10, 15]
|
|
208
|
+
|
|
209
|
+
imgs = []
|
|
210
|
+
for height, width in zip(heights, widths, strict=True):
|
|
211
|
+
arr = da.from_array(np.ones((1, height, width), dtype=np.uint16))
|
|
212
|
+
imgs.append(arr)
|
|
213
|
+
channel_metadata = channel_metadata = [
|
|
214
|
+
make_ChannelMetadata(name="test", cycle=1, translation_x=2, translation_y=3),
|
|
215
|
+
make_ChannelMetadata(name="test", cycle=1, translation_x=5, translation_y=5),
|
|
216
|
+
]
|
|
217
|
+
with pytest.warns(UserWarning, match="Padding images with 0s to same size of \\(17, 18\\)"):
|
|
218
|
+
imgs_padded = MultiChannelImage._pad_images(imgs, channel_metadata)
|
|
219
|
+
for img in imgs_padded:
|
|
220
|
+
assert img.shape == (1, 17, 18)
|
|
221
|
+
|
|
222
|
+
|
|
105
223
|
@pytest.mark.parametrize(
|
|
106
224
|
"dataset,expected",
|
|
107
225
|
[
|
|
@@ -123,7 +241,6 @@ def test_image_size(dataset: str, expected: dict[str, Any]) -> None:
|
|
|
123
241
|
assert extent == expected
|
|
124
242
|
|
|
125
243
|
|
|
126
|
-
@skip_if_below_python_version()
|
|
127
244
|
@pytest.mark.parametrize(
|
|
128
245
|
"dataset,expected",
|
|
129
246
|
[("OMAP10_small", 4), ("OMAP23_small", 5)],
|
|
@@ -139,7 +256,6 @@ def test_total_channels(dataset: str, expected: int) -> None:
|
|
|
139
256
|
assert channels == expected
|
|
140
257
|
|
|
141
258
|
|
|
142
|
-
@skip_if_below_python_version()
|
|
143
259
|
@pytest.mark.parametrize(
|
|
144
260
|
"dataset,expected",
|
|
145
261
|
[
|
|
@@ -161,7 +277,6 @@ def test_channel_names_with_cycle_in_name(dataset: str, expected: list[str]) ->
|
|
|
161
277
|
assert list(channels) == expected
|
|
162
278
|
|
|
163
279
|
|
|
164
|
-
@skip_if_below_python_version()
|
|
165
280
|
@pytest.mark.parametrize(
|
|
166
281
|
"dataset,expected",
|
|
167
282
|
[
|
|
@@ -178,7 +293,6 @@ def test_total_rounds(dataset: str, expected: list[int]) -> None:
|
|
|
178
293
|
assert max_cycle == expected
|
|
179
294
|
|
|
180
295
|
|
|
181
|
-
@skip_if_below_python_version()
|
|
182
296
|
@pytest.mark.parametrize(
|
|
183
297
|
"dataset,skip_rounds,expected",
|
|
184
298
|
[
|
|
@@ -201,6 +315,57 @@ def test_skip_rounds(dataset: str, skip_rounds: list[int], expected: list[str])
|
|
|
201
315
|
assert list(channels) == expected, f"Expected {expected}, got {list(channels)}"
|
|
202
316
|
|
|
203
317
|
|
|
318
|
+
def test_unsupported_parsing_styles() -> None:
|
|
319
|
+
with pytest.raises(ValueError, match="Invalid option `not_a_parsing_style` for `MACSimaParsingStyle`."):
|
|
320
|
+
macsima(Path(), parsing_style="not_a_parsing_style")
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_processed_single_folder_parsing_returns_a_single_image_stack(tmp_path: Path) -> None:
|
|
324
|
+
omap10_path = Path("./data/OMAP10_small")
|
|
325
|
+
shutil.copytree(omap10_path, tmp_path / "OMAP10_small_1")
|
|
326
|
+
shutil.copytree(omap10_path, tmp_path / "OMAP10_small_2")
|
|
327
|
+
|
|
328
|
+
sdata = macsima(tmp_path, parsing_style="processed_single_folder")
|
|
329
|
+
|
|
330
|
+
assert len(sdata.images) == 1
|
|
331
|
+
# omap10_small has 4 channels, so we expect 8 here
|
|
332
|
+
el = sdata[list(sdata.images.keys())[0]]
|
|
333
|
+
assert len(get_channel_names(el)) == 8
|
|
334
|
+
assert len(sdata.tables) == 1
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def test_processed_single_folder_parsing_warns_when_specifying_filtered_folders(tmp_path: Path) -> None:
|
|
338
|
+
omap10_path = Path("./data/OMAP10_small")
|
|
339
|
+
shutil.copytree(omap10_path, tmp_path / "OMAP10_small_1")
|
|
340
|
+
shutil.copytree(omap10_path, tmp_path / "OMAP10_small_2")
|
|
341
|
+
with pytest.warns(UserWarning, match="filtering only happens for processed_multi_folders"):
|
|
342
|
+
macsima(tmp_path, parsing_style="processed_single_folder", filter_folder_names=["OMAP10_small_2"])
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_processed_multiple_folders_returns_an_image_stack_per_subfolder(tmp_path: Path) -> None:
|
|
346
|
+
omap10_path = Path("./data/OMAP10_small")
|
|
347
|
+
shutil.copytree(omap10_path, tmp_path / "OMAP10_small_1")
|
|
348
|
+
shutil.copytree(omap10_path, tmp_path / "OMAP10_small_2")
|
|
349
|
+
|
|
350
|
+
sdata = macsima(tmp_path, parsing_style="processed_multiple_folders")
|
|
351
|
+
|
|
352
|
+
assert len(sdata.images) == 2
|
|
353
|
+
for el in sdata.images.keys():
|
|
354
|
+
assert len(get_channel_names(sdata[el])) == 4
|
|
355
|
+
assert len(sdata.tables) == 2
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
def test_processed_multiple_folders_skips_filtered_folder_names(tmp_path: Path) -> None:
|
|
359
|
+
shutil.copytree(Path("./data/OMAP10_small"), tmp_path / "OMAP10_small")
|
|
360
|
+
shutil.copytree(Path("./data/OMAP23_small"), tmp_path / "OMAP23_small")
|
|
361
|
+
|
|
362
|
+
sdata = macsima(tmp_path, parsing_style="processed_multiple_folders", filter_folder_names=["OMAP10_small"])
|
|
363
|
+
assert len(sdata.images) == 1
|
|
364
|
+
assert list(sdata.images.keys()) == ["OMAP23_small_image"]
|
|
365
|
+
assert len(sdata.tables) == 1
|
|
366
|
+
assert list(sdata.tables.keys()) == ["OMAP23_small_table"]
|
|
367
|
+
|
|
368
|
+
|
|
204
369
|
METADATA_COLUMN_ORDER = [
|
|
205
370
|
"cycle",
|
|
206
371
|
"imagetype",
|
|
@@ -242,7 +407,6 @@ EXPECTED_METADATA_OMAP23 = pd.DataFrame(
|
|
|
242
407
|
)
|
|
243
408
|
|
|
244
409
|
|
|
245
|
-
@skip_if_below_python_version()
|
|
246
410
|
@pytest.mark.parametrize(
|
|
247
411
|
"dataset,expected_df",
|
|
248
412
|
[
|
|
@@ -262,11 +426,6 @@ def test_metadata_table(dataset: str, expected_df: pd.DataFrame) -> None:
|
|
|
262
426
|
pd.testing.assert_frame_equal(actual, expected_df)
|
|
263
427
|
|
|
264
428
|
|
|
265
|
-
def test_parsing_style() -> None:
|
|
266
|
-
with pytest.raises(ValueError):
|
|
267
|
-
macsima(Path(), parsing_style="not_a_parsing_style")
|
|
268
|
-
|
|
269
|
-
|
|
270
429
|
def test_mci_sort_by_channel() -> None:
|
|
271
430
|
sizes = [100, 200, 300]
|
|
272
431
|
c_names = ["test11", "test3", "test2"]
|
|
@@ -315,7 +474,6 @@ def test_mci_array_reference() -> None:
|
|
|
315
474
|
assert da.all(mci.data[0] == orig_arr1)
|
|
316
475
|
|
|
317
476
|
|
|
318
|
-
@skip_if_below_python_version()
|
|
319
477
|
@pytest.mark.parametrize("dataset", ["OMAP10_small", "OMAP23_small"])
|
|
320
478
|
def test_cli_macsima(runner: CliRunner, dataset: str) -> None:
|
|
321
479
|
f = Path("./data") / dataset
|
|
@@ -645,15 +803,77 @@ def test_parse_v1_ome_metadata_handles_unknown_imagetypes() -> None:
|
|
|
645
803
|
assert md["imagetype"] == "NOT A VALID TYPE"
|
|
646
804
|
|
|
647
805
|
|
|
648
|
-
def
|
|
649
|
-
|
|
806
|
+
def test_get_translations_returns_correct_values() -> None:
|
|
807
|
+
ome = OME(
|
|
808
|
+
images=[
|
|
809
|
+
Image(
|
|
810
|
+
pixels=Pixels(
|
|
811
|
+
dimension_order=Pixels_DimensionOrder("XYZCT"),
|
|
812
|
+
type=PixelType.UINT16,
|
|
813
|
+
size_x=1,
|
|
814
|
+
size_y=1,
|
|
815
|
+
size_z=1,
|
|
816
|
+
size_c=1,
|
|
817
|
+
size_t=1,
|
|
818
|
+
planes=[Plane(position_x=1, position_y=2, the_z=0, the_t=0, the_c=0)],
|
|
819
|
+
)
|
|
820
|
+
)
|
|
821
|
+
]
|
|
822
|
+
)
|
|
823
|
+
expected = {"translation_x": 1, "translation_y": 2}
|
|
824
|
+
|
|
825
|
+
translations = _get_translations(ome)
|
|
826
|
+
assert translations == expected
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
def test_get_translations_defaults_to_0_on_missing_data() -> None:
|
|
830
|
+
ome = OME(
|
|
831
|
+
images=[
|
|
832
|
+
Image(
|
|
833
|
+
pixels=Pixels(
|
|
834
|
+
dimension_order=Pixels_DimensionOrder("XYZCT"),
|
|
835
|
+
type=PixelType.UINT16,
|
|
836
|
+
size_x=1,
|
|
837
|
+
size_y=1,
|
|
838
|
+
size_z=1,
|
|
839
|
+
size_c=1,
|
|
840
|
+
size_t=1,
|
|
841
|
+
planes=[Plane(the_z=0, the_t=0, the_c=0)],
|
|
842
|
+
)
|
|
843
|
+
)
|
|
844
|
+
],
|
|
845
|
+
)
|
|
846
|
+
expected = {"translation_x": 0, "translation_y": 0}
|
|
847
|
+
|
|
848
|
+
translations = _get_translations(ome)
|
|
849
|
+
assert translations == expected
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def make_ome(extra_ma: dict[str, Any] | None = None) -> OME:
|
|
853
|
+
base = {}
|
|
650
854
|
if extra_ma:
|
|
651
855
|
base.update(extra_ma)
|
|
652
|
-
return OME(
|
|
856
|
+
return OME(
|
|
857
|
+
images=[
|
|
858
|
+
Image(
|
|
859
|
+
pixels=Pixels(
|
|
860
|
+
dimension_order=Pixels_DimensionOrder("XYZCT"),
|
|
861
|
+
type=PixelType.UINT16,
|
|
862
|
+
size_x=1,
|
|
863
|
+
size_y=1,
|
|
864
|
+
size_z=1,
|
|
865
|
+
size_c=1,
|
|
866
|
+
size_t=1,
|
|
867
|
+
planes=[Plane(the_z=0, the_t=0, the_c=0)],
|
|
868
|
+
)
|
|
869
|
+
)
|
|
870
|
+
],
|
|
871
|
+
structured_annotations=StructuredAnnotations(map_annotations=[MapAnnotation(value=base)]),
|
|
872
|
+
)
|
|
653
873
|
|
|
654
874
|
|
|
655
875
|
def test_parse_ome_metadata_dispatches_to_v0() -> None:
|
|
656
|
-
ome =
|
|
876
|
+
ome = make_ome(extra_ma={"SoftwareVersion": "0.9.0"})
|
|
657
877
|
# enrich some so v0 parser has something to see
|
|
658
878
|
ome.screens = [Screen(reagents=[Reagent(name="Marker0")])]
|
|
659
879
|
|
|
@@ -666,15 +886,14 @@ def test_parse_ome_metadata_dispatches_to_v0() -> None:
|
|
|
666
886
|
|
|
667
887
|
|
|
668
888
|
def test_parse_ome_metadata_dispatches_to_v1() -> None:
|
|
669
|
-
ome =
|
|
670
|
-
|
|
889
|
+
ome = make_ome(extra_ma={"SoftwareVersion": "1.0.0", "Biomarker": "CD3"})
|
|
671
890
|
md = _parse_ome_metadata(ome)
|
|
672
891
|
|
|
673
892
|
assert md["name"] == "CD3"
|
|
674
893
|
|
|
675
894
|
|
|
676
895
|
def test_parse_ome_metadata_unknown_major_raises() -> None:
|
|
677
|
-
ome =
|
|
896
|
+
ome = make_ome(extra_ma={"SoftwareVersion": "2.0.0"})
|
|
678
897
|
|
|
679
898
|
with pytest.raises(ValueError, match="Unknown software version"):
|
|
680
899
|
_parse_ome_metadata(ome)
|
|
@@ -114,6 +114,7 @@ def test_example_data_index_integrity(dataset: str) -> None:
|
|
|
114
114
|
assert sdata["nucleus_labels"]["scale0"]["image"].sel(y=3515.5, x=4618.5).data.compute() == 6392
|
|
115
115
|
assert np.allclose(sdata['transcripts'].compute().loc[[0, 10000, 1113949]]['x'], [2.608911, 194.917831, 1227.499268])
|
|
116
116
|
assert np.isclose(sdata['cell_boundaries'].loc['oipggjko-1'].geometry.centroid.x,736.4864931162789)
|
|
117
|
+
assert sdata['cell_boundaries'].index.name == 'cell_id'
|
|
117
118
|
index = sdata['nucleus_boundaries']['cell_id'].index[sdata['nucleus_boundaries']['cell_id'].eq('oipggjko-1')][0]
|
|
118
119
|
assert np.isclose(sdata['nucleus_boundaries'].loc[index].geometry.centroid.x,736.4931256878282)
|
|
119
120
|
assert np.array_equal(sdata['table'].X.indices[:3], [1, 3, 34])
|
|
@@ -140,6 +141,7 @@ def test_example_data_index_integrity(dataset: str) -> None:
|
|
|
140
141
|
assert sdata["nucleus_labels"]["scale0"]["image"].sel(y=18.5, x=3015.5).data.compute() == 2764
|
|
141
142
|
assert np.allclose(sdata['transcripts'].compute().loc[[0, 10000, 20000]]['x'], [174.258392, 12.210024, 214.759186])
|
|
142
143
|
assert np.isclose(sdata['cell_boundaries'].loc['aaanbaof-1'].geometry.centroid.x, 43.96894317275074)
|
|
144
|
+
assert sdata['cell_boundaries'].index.name == 'cell_id'
|
|
143
145
|
index = sdata['nucleus_boundaries']['cell_id'].index[sdata['nucleus_boundaries']['cell_id'].eq('aaanbaof-1')][0]
|
|
144
146
|
assert np.isclose(sdata['nucleus_boundaries'].loc[index].geometry.centroid.x,43.31874577809517)
|
|
145
147
|
assert np.array_equal(sdata['table'].X.indices[:3], [1, 8, 19])
|
|
@@ -167,6 +169,7 @@ def test_example_data_index_integrity(dataset: str) -> None:
|
|
|
167
169
|
assert sdata["nucleus_labels"]["scale0"]["image"].sel(y=4039.5, x=93.5).data.compute() == 274
|
|
168
170
|
assert np.allclose(sdata['transcripts'].compute().loc[[0, 10000, 20000]]['x'], [43.296875, 62.484375, 93.125])
|
|
169
171
|
assert np.isclose(sdata['cell_boundaries'].loc['aadmbfof-1'].geometry.centroid.x, 64.54541104696033)
|
|
172
|
+
assert sdata['cell_boundaries'].index.name == 'cell_id'
|
|
170
173
|
index = sdata['nucleus_boundaries']['cell_id'].index[sdata['nucleus_boundaries']['cell_id'].eq('aadmbfof-1')][0]
|
|
171
174
|
assert np.isclose(sdata['nucleus_boundaries'].loc[index].geometry.centroid.x, 65.43305896114295)
|
|
172
175
|
assert np.array_equal(sdata['table'].X.indices[:3], [3, 49, 53])
|
|
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
|
{spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/converters/generic_to_zarr.py
RENAMED
|
File without changes
|
{spatialdata_io-0.7.0 → spatialdata_io-0.7.1}/src/spatialdata_io/converters/legacy_anndata.py
RENAMED
|
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
|