thyra 4.3.2__tar.gz → 4.3.3__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.
- {thyra-4.3.2 → thyra-4.3.3}/PKG-INFO +1 -1
- {thyra-4.3.2 → thyra-4.3.3}/pyproject.toml +1 -1
- {thyra-4.3.2 → thyra-4.3.3}/thyra/__init__.py +1 -1
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/optical_image.py +38 -12
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/base_reader.py +42 -1
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/extractors/imzml_extractor.py +6 -3
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/extractors/mzpeak_extractor.py +1 -1
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/mzpeak/mzpeak_reader.py +165 -18
- {thyra-4.3.2 → thyra-4.3.3}/thyra/utils/imzml_coordinate_base.py +85 -1
- {thyra-4.3.2 → thyra-4.3.3}/.gitignore +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/LICENSE +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/README.md +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/__main__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/alignment/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/alignment/affine.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/alignment/teaching_points.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/config.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/convert.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/_chunking.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/base_spatialdata_converter.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/converter.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/csc_assembly.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/fused_passes.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/mobility_heatmap.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/mobility_table.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/msms_table.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/root_attrs.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/sibling_tables.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/streaming_converter.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/converters/spatialdata/uns_assembler.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/base_converter.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/base_extractor.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/conversion_state.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/drop_tally.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/frames.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/mass_axis.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/mobility.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/msms.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/core/registry.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/errors.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/constants.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/document.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/extractors/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/extractors/bruker_extractor.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/extractors/imzml_header_extractor.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/extractors/phi_extractor.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/extractors/solarix_extractor.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/extractors/waters_extractor.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/ontology/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/ontology/_ims.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/ontology/_ms.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/ontology/_uo.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/ontology/cache.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/ontology/dump_obo_files.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/personal_data.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/builder.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/cli.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/generate.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/metaspace.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/models.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/msi_metadata.linkml.yaml +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/msi_metadata_schema_v0_10.json +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/store_io.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/validate.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/schema/vocab.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/types.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/uns_compat.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/metadata/validator.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/preview.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/py.typed +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/base_bruker_reader.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/folder_structure.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/mis_parser.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/rapiflex/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/rapiflex/rapiflex_reader.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/solarix/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/solarix/solarix_reader.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll/LICENCE-BRUKER.txt +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll/README.md +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll/timsdata.dll +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll/timsdata.so +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/dll_manager.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/platform_detector.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/sdk/sdk_functions.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/timstof/timstof_reader.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/bruker/vendor_db.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/imzml/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/imzml/_pyimzml_compat.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/imzml/header.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/imzml/imzml_reader.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/imzml/mobility_array.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/mzpeak/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/mzpeak/chunk_decoding.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/phi/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/phi/event_stream.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/phi/mass_axis.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/phi/phi_header.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/phi/phi_reader.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/waters/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/waters/imaging_grid.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/waters/instrument.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/waters/lib/MLReader.dll +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/waters/lib/MassLynxRaw.dll +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/waters/lib/libMLReader.so +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/waters/lib/libMassLynxRaw.so +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/waters/masslynx_lib.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/readers/waters/waters_reader.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/axis_planner.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/binning.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/common_axis.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/constants.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/data_characteristics.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/decision_tree.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/gaps.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/instrument_detectors.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/interpolation.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/mass_axis/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/mass_axis/base_generator.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/mass_axis/fticr_generator.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/mass_axis/linear_generator.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/mass_axis/linear_tof_generator.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/mass_axis/orbitrap_generator.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/mass_axis/reflector_tof_generator.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/mass_axis/tof_generator.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/mobility_grid.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/strategies/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/strategies/base.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/strategies/nearest_neighbor.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/strategies/tic_preserving.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/tic.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/resampling/types.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/tools/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/tools/check_ontology.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/tools/make_example_data.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/utils/__init__.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/utils/bruker_exceptions.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/utils/logging_config.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/utils/pyimzml_direct.py +0 -0
- {thyra-4.3.2 → thyra-4.3.3}/thyra/utils/windows_paths.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: thyra
|
|
3
|
-
Version: 4.3.
|
|
3
|
+
Version: 4.3.3
|
|
4
4
|
Summary: A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics
|
|
5
5
|
Project-URL: Homepage, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra
|
|
6
6
|
Project-URL: Repository, https://github.com/M4i-Imaging-Mass-Spectrometry/thyra
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "thyra"
|
|
7
|
-
version = "4.3.
|
|
7
|
+
version = "4.3.3"
|
|
8
8
|
description = "A modern Python library for converting Mass Spectrometry Imaging (MSI) data into SpatialData/Zarr format - your portal to spatial omics"
|
|
9
9
|
authors = [{ name = "Theodoros Visvikis", email = "t.visvikis@maastrichtuniversity.nl" }]
|
|
10
10
|
maintainers = [{ name = "Theodoros Visvikis", email = "t.visvikis@maastrichtuniversity.nl" }]
|
|
@@ -126,7 +126,7 @@ from ._chunking import image_chunks
|
|
|
126
126
|
|
|
127
127
|
if TYPE_CHECKING: # pragma: no cover - typing only
|
|
128
128
|
from ...alignment.teaching_points import RegionMapping
|
|
129
|
-
from ...core.base_reader import BaseMSIReader
|
|
129
|
+
from ...core.base_reader import BaseMSIReader, OpticalImageLabel
|
|
130
130
|
|
|
131
131
|
logger = logging.getLogger(__name__)
|
|
132
132
|
|
|
@@ -944,6 +944,11 @@ def _calc_optical_scale_factors(
|
|
|
944
944
|
return factors
|
|
945
945
|
|
|
946
946
|
|
|
947
|
+
def _clean_stem(stem: str) -> str:
|
|
948
|
+
"""A file stem made fit for an element name: lowercase, at most 30 long."""
|
|
949
|
+
return stem.lower().replace(" ", "_").replace("-", "_")[:30]
|
|
950
|
+
|
|
951
|
+
|
|
947
952
|
class OpticalImages:
|
|
948
953
|
"""The optical images of one conversion: alignment, declaration, streaming.
|
|
949
954
|
|
|
@@ -1459,9 +1464,11 @@ class OpticalImages:
|
|
|
1459
1464
|
images: The store's images, to add the placeholder to
|
|
1460
1465
|
"""
|
|
1461
1466
|
# Generate a clean name for the image layer
|
|
1462
|
-
|
|
1467
|
+
label = self._reader_label(image_path)
|
|
1468
|
+
image_name = self._element_name(image_path, label)
|
|
1469
|
+
source_file = label.source_file if label is not None else image_path.name
|
|
1463
1470
|
|
|
1464
|
-
logger.info(f"Loading optical image: {
|
|
1471
|
+
logger.info(f"Loading optical image: {source_file} as '{image_name}'")
|
|
1465
1472
|
|
|
1466
1473
|
# Only the page header is read here. The pixels never enter this
|
|
1467
1474
|
# process whole: the element is declared to SpatialData as a lazy
|
|
@@ -1584,8 +1591,10 @@ class OpticalImages:
|
|
|
1584
1591
|
# rewrites the stem (_0000 -> highres, and a stem over 30
|
|
1585
1592
|
# characters is truncated), and the alignment image is only
|
|
1586
1593
|
# distinguishable by its transform, and only when the alignment
|
|
1587
|
-
# was applied. Recorded here, written by the root attrs.
|
|
1588
|
-
|
|
1594
|
+
# was applied. Recorded here, written by the root attrs. For an
|
|
1595
|
+
# image a reader copied out of a container, the member it came
|
|
1596
|
+
# from, not the copy.
|
|
1597
|
+
self._sources[image_name] = source_file
|
|
1589
1598
|
if is_primary:
|
|
1590
1599
|
self._alignment_element = image_name
|
|
1591
1600
|
|
|
@@ -1688,18 +1697,39 @@ class OpticalImages:
|
|
|
1688
1697
|
"global": {**systems["global"], "reference_element": None},
|
|
1689
1698
|
}
|
|
1690
1699
|
|
|
1691
|
-
def
|
|
1700
|
+
def _reader_label(self, image_path: Path) -> Optional[OpticalImageLabel]:
|
|
1701
|
+
"""The reader's name for an image it copied out of its source.
|
|
1702
|
+
|
|
1703
|
+
Asked for with ``getattr`` for the reason
|
|
1704
|
+
:meth:`_adopt_reader_primary_optical` gives: a reader here need not
|
|
1705
|
+
inherit from ``BaseMSIReader``.
|
|
1706
|
+
"""
|
|
1707
|
+
resolve = getattr(self.reader, "get_optical_image_label", None)
|
|
1708
|
+
if not callable(resolve):
|
|
1709
|
+
return None
|
|
1710
|
+
label: Optional[OpticalImageLabel] = resolve(image_path)
|
|
1711
|
+
return label
|
|
1712
|
+
|
|
1713
|
+
def _element_name(
|
|
1714
|
+
self, image_path: Path, label: Optional[OpticalImageLabel] = None
|
|
1715
|
+
) -> str:
|
|
1692
1716
|
"""Generate a clean name for an optical image layer.
|
|
1693
1717
|
|
|
1694
1718
|
The suffix is dropped, so the same acquisition exported as a .tif or
|
|
1695
|
-
a .jpg lands under the same element name.
|
|
1719
|
+
a .jpg lands under the same element name. The ``_0000`` / ``_0001``
|
|
1720
|
+
rule is for files named by the acquisition software; an image a
|
|
1721
|
+
reader copied out of a container is named by the reader's label.
|
|
1696
1722
|
|
|
1697
1723
|
Args:
|
|
1698
1724
|
image_path: Path to the optical image
|
|
1725
|
+
label: The reader's name for the image, if it gave one.
|
|
1699
1726
|
|
|
1700
1727
|
Returns:
|
|
1701
1728
|
Clean name for the image layer (e.g., 'optical_0000', 'optical_deriv')
|
|
1702
1729
|
"""
|
|
1730
|
+
if label is not None:
|
|
1731
|
+
return f"{self.dataset_id}_optical_{_clean_stem(label.name)}"
|
|
1732
|
+
|
|
1703
1733
|
stem = image_path.stem.lower()
|
|
1704
1734
|
|
|
1705
1735
|
# Extract meaningful suffix from filename
|
|
@@ -1710,11 +1740,7 @@ class OpticalImages:
|
|
|
1710
1740
|
elif "deriv" in stem:
|
|
1711
1741
|
suffix = "overview"
|
|
1712
1742
|
else:
|
|
1713
|
-
|
|
1714
|
-
suffix = stem.replace(" ", "_").replace("-", "_")
|
|
1715
|
-
# Truncate if too long
|
|
1716
|
-
if len(suffix) > 30:
|
|
1717
|
-
suffix = suffix[:30]
|
|
1743
|
+
suffix = _clean_stem(stem)
|
|
1718
1744
|
|
|
1719
1745
|
return f"{self.dataset_id}_optical_{suffix}"
|
|
1720
1746
|
|
|
@@ -52,7 +52,16 @@ for all seven readers at once, so the next change to it is a visible one.
|
|
|
52
52
|
import logging
|
|
53
53
|
from abc import ABC, abstractmethod
|
|
54
54
|
from pathlib import Path
|
|
55
|
-
from typing import
|
|
55
|
+
from typing import (
|
|
56
|
+
TYPE_CHECKING,
|
|
57
|
+
Dict,
|
|
58
|
+
Generator,
|
|
59
|
+
List,
|
|
60
|
+
NamedTuple,
|
|
61
|
+
Optional,
|
|
62
|
+
Tuple,
|
|
63
|
+
Union,
|
|
64
|
+
)
|
|
56
65
|
|
|
57
66
|
import numpy as np
|
|
58
67
|
from numpy.typing import NDArray
|
|
@@ -70,6 +79,22 @@ from .mass_axis import validate_max_mass_axis_length
|
|
|
70
79
|
logger = logging.getLogger(__name__)
|
|
71
80
|
|
|
72
81
|
|
|
82
|
+
class OpticalImageLabel(NamedTuple):
|
|
83
|
+
"""What a store calls an optical image whose file name is not the source's.
|
|
84
|
+
|
|
85
|
+
A reader that copies images out of a container hands the converter files
|
|
86
|
+
it named itself. Their names say nothing, and the name rule written for
|
|
87
|
+
vendor folders (``_0000`` is the high resolution scan) would read meaning
|
|
88
|
+
into them.
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
name: str
|
|
92
|
+
"""The element name after ``<dataset_id>_optical_``."""
|
|
93
|
+
|
|
94
|
+
source_file: str
|
|
95
|
+
"""What the store records the element came from."""
|
|
96
|
+
|
|
97
|
+
|
|
73
98
|
class BaseMSIReader(ABC):
|
|
74
99
|
"""Abstract base class for reading MSI data formats."""
|
|
75
100
|
|
|
@@ -169,6 +194,22 @@ class BaseMSIReader(ABC):
|
|
|
169
194
|
"""
|
|
170
195
|
return None
|
|
171
196
|
|
|
197
|
+
def get_optical_image_label(self, path: Path) -> Optional[OpticalImageLabel]:
|
|
198
|
+
"""Name an optical image this reader copied out of its source.
|
|
199
|
+
|
|
200
|
+
Default implementation returns None: the file is the source's own,
|
|
201
|
+
and its name is what the store is named after. Override alongside
|
|
202
|
+
:meth:`get_optical_image_paths` when the reader names the files.
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
path: One of the paths :meth:`get_optical_image_paths` returned.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
The element name and the source file to record, or None to name
|
|
209
|
+
the element after the file.
|
|
210
|
+
"""
|
|
211
|
+
return None
|
|
212
|
+
|
|
172
213
|
@abstractmethod
|
|
173
214
|
def get_common_mass_axis(self) -> NDArray[np.float64]:
|
|
174
215
|
"""Return the common mass axis for all spectra.
|
|
@@ -10,7 +10,7 @@ from pyimzml.ImzMLParser import ImzMLParser
|
|
|
10
10
|
|
|
11
11
|
from ...core.base_extractor import MetadataExtractor
|
|
12
12
|
from ...errors import ConversionRefused
|
|
13
|
-
from ...utils.imzml_coordinate_base import coordinate_bases
|
|
13
|
+
from ...utils.imzml_coordinate_base import coordinate_bases, recorded_offsets
|
|
14
14
|
from ...utils.pyimzml_direct import read_spectrum_mzs_only
|
|
15
15
|
from ..constants import (
|
|
16
16
|
BinaryDataType,
|
|
@@ -352,8 +352,11 @@ class ImzMLMetadataExtractor(MetadataExtractor):
|
|
|
352
352
|
# store can say where its origin came from: the converter
|
|
353
353
|
# writes it to coordinate_systems.global.coordinate_offsets_px.
|
|
354
354
|
# (1, 1, 1) for an ordinary 1-based file, (0, 0, ...) for a
|
|
355
|
-
# 0-based one -- see _coordinate_bases() (issue #244).
|
|
356
|
-
|
|
355
|
+
# 0-based one -- see _coordinate_bases() (issue #244). z is 0
|
|
356
|
+
# when the file states none (D30).
|
|
357
|
+
coordinate_offsets=recorded_offsets(
|
|
358
|
+
self._coordinate_bases(coords), coords[:, 2], self.imzml_path
|
|
359
|
+
),
|
|
357
360
|
spectrum_type=spectrum_type,
|
|
358
361
|
)
|
|
359
362
|
|
|
@@ -118,7 +118,7 @@ class MzPeakMetadataExtractor(MetadataExtractor):
|
|
|
118
118
|
n_spectra=n_spectra,
|
|
119
119
|
total_peaks=total_peaks,
|
|
120
120
|
source_path=str(self.data_path),
|
|
121
|
-
coordinate_offsets=(index.offsets[0], index.offsets[1],
|
|
121
|
+
coordinate_offsets=(index.offsets[0], index.offsets[1], index.z_offset),
|
|
122
122
|
spectrum_type=self._spectrum_type(),
|
|
123
123
|
)
|
|
124
124
|
|
|
@@ -65,13 +65,14 @@ from typing import (
|
|
|
65
65
|
List,
|
|
66
66
|
NamedTuple,
|
|
67
67
|
Optional,
|
|
68
|
+
Set,
|
|
68
69
|
Tuple,
|
|
69
70
|
)
|
|
70
71
|
|
|
71
72
|
import numpy as np
|
|
72
73
|
from numpy.typing import NDArray
|
|
73
74
|
|
|
74
|
-
from ...core.base_reader import BaseMSIReader
|
|
75
|
+
from ...core.base_reader import BaseMSIReader, OpticalImageLabel
|
|
75
76
|
from ...core.mass_axis import MassAxisAccumulator
|
|
76
77
|
from ...core.registry import register_reader
|
|
77
78
|
from ...errors import ConversionRefused
|
|
@@ -104,11 +105,13 @@ ZIP_MAGIC = b"PK\x03\x04"
|
|
|
104
105
|
# one axis and misses the other.
|
|
105
106
|
IMS_POSITION_X = "IMS:1000050"
|
|
106
107
|
IMS_POSITION_Y = "IMS:1000051"
|
|
108
|
+
IMS_POSITION_Z = "IMS:1000052"
|
|
107
109
|
|
|
108
110
|
#: Conventional column names for the position terms. Used only as a fallback
|
|
109
111
|
#: when the index carries no column mapping, which the schema permits.
|
|
110
112
|
DEFAULT_POSITION_X_COLUMN = "opt_IMS_1000050_position_x"
|
|
111
113
|
DEFAULT_POSITION_Y_COLUMN = "opt_IMS_1000051_position_y"
|
|
114
|
+
DEFAULT_POSITION_Z_COLUMN = "opt_IMS_1000052_position_z"
|
|
112
115
|
|
|
113
116
|
#: Base of the positions when the archive does not declare one in
|
|
114
117
|
#: ``imaging.coordinate_base``: the imzML convention, which every archive
|
|
@@ -132,6 +135,17 @@ CHUNK_BATCH_ROWS = (16, 65536)
|
|
|
132
135
|
#: Image formats the optical image loader reads.
|
|
133
136
|
OPTICAL_IMAGE_SUFFIXES = (".tif", ".tiff", ".jpg", ".jpeg", ".png", ".bmp")
|
|
134
137
|
|
|
138
|
+
#: The suffix an embedded image is copied out under, by the media type the
|
|
139
|
+
#: archive declares for it. The loader picks its decoder by suffix, and a
|
|
140
|
+
#: slide scanner's TIFF is a TIFF under a name of its own: the glioma example
|
|
141
|
+
#: embeds an Aperio ``.svs`` declared ``image/tiff``.
|
|
142
|
+
MEDIA_TYPE_SUFFIXES = {
|
|
143
|
+
"image/tiff": ".tif",
|
|
144
|
+
"image/jpeg": ".jpg",
|
|
145
|
+
"image/png": ".png",
|
|
146
|
+
"image/bmp": ".bmp",
|
|
147
|
+
}
|
|
148
|
+
|
|
135
149
|
#: Metadata column giving each spectrum's MS level.
|
|
136
150
|
MS_LEVEL_COLUMN = "ms_level"
|
|
137
151
|
|
|
@@ -281,6 +295,8 @@ class MzPeakSpatialIndex(NamedTuple):
|
|
|
281
295
|
complete: Whether every spectrum the archive lists is kept. When
|
|
282
296
|
one is left out (no position, or an MSn spectrum beside MS1),
|
|
283
297
|
the axis, the mass range and the peak count skip it too.
|
|
298
|
+
z_offset: The position z the archive states for its one plane, 0
|
|
299
|
+
when it states none (D30).
|
|
284
300
|
"""
|
|
285
301
|
|
|
286
302
|
spectrum_indices: NDArray[np.int64]
|
|
@@ -289,6 +305,7 @@ class MzPeakSpatialIndex(NamedTuple):
|
|
|
289
305
|
point_counts: NDArray[np.int64]
|
|
290
306
|
offsets: Tuple[int, int]
|
|
291
307
|
complete: bool = True
|
|
308
|
+
z_offset: int = 0
|
|
292
309
|
|
|
293
310
|
|
|
294
311
|
class MzPeakArchive:
|
|
@@ -696,6 +713,25 @@ class MzPeakArchive:
|
|
|
696
713
|
return None
|
|
697
714
|
return (x_column, y_column)
|
|
698
715
|
|
|
716
|
+
def position_z_column(self) -> Optional[str]:
|
|
717
|
+
"""Resolve the scan column holding position z, the way x and y are.
|
|
718
|
+
|
|
719
|
+
mzpeak-convert writes one when its source states z (an imzML with
|
|
720
|
+
``IMS:1000052`` on its spectra) and none otherwise.
|
|
721
|
+
|
|
722
|
+
Returns:
|
|
723
|
+
The column name, or ``None`` when the archive has none.
|
|
724
|
+
"""
|
|
725
|
+
entry = self.entry("spectrum", "scans")
|
|
726
|
+
if entry is None:
|
|
727
|
+
return None
|
|
728
|
+
names = set(self.parquet("spectrum", "scans").schema_arrow.names)
|
|
729
|
+
z_column = (
|
|
730
|
+
self.column_for_accession(entry, IMS_POSITION_Z)
|
|
731
|
+
or DEFAULT_POSITION_Z_COLUMN
|
|
732
|
+
)
|
|
733
|
+
return z_column if z_column in names else None
|
|
734
|
+
|
|
699
735
|
def spatial_index(self) -> MzPeakSpatialIndex:
|
|
700
736
|
"""Read positions and per-spectrum point counts.
|
|
701
737
|
|
|
@@ -723,9 +759,14 @@ class MzPeakArchive:
|
|
|
723
759
|
f"position columns. Thyra converts imaging acquisitions only."
|
|
724
760
|
)
|
|
725
761
|
x_column, y_column = columns
|
|
762
|
+
z_column = self.position_z_column()
|
|
726
763
|
|
|
727
764
|
scans_member = self.parquet("spectrum", "scans")
|
|
728
|
-
scans = scans_member.read(
|
|
765
|
+
scans = scans_member.read(
|
|
766
|
+
columns=[
|
|
767
|
+
name for name in ("source_index", x_column, y_column, z_column) if name
|
|
768
|
+
]
|
|
769
|
+
)
|
|
729
770
|
self._report_ion_mobility(scans_member)
|
|
730
771
|
source = np.asarray(scans.column("source_index").to_numpy(), dtype=np.int64)
|
|
731
772
|
xs, has_x = _positions(scans.column(x_column))
|
|
@@ -743,6 +784,10 @@ class MzPeakArchive:
|
|
|
743
784
|
f"both, and a scan on no pixel has neither."
|
|
744
785
|
)
|
|
745
786
|
placed = has_x & has_y
|
|
787
|
+
# Which row of the scans member each entry came from, carried through
|
|
788
|
+
# the reordering below so that position z can be read for the scans
|
|
789
|
+
# that end up on a pixel.
|
|
790
|
+
scan_row = np.arange(source.size, dtype=np.int64)
|
|
746
791
|
|
|
747
792
|
# One spectrum may own several scans; imaging acquisitions write one.
|
|
748
793
|
# Keeping the first per spectrum means a multi-scan file resolves to a
|
|
@@ -750,14 +795,21 @@ class MzPeakArchive:
|
|
|
750
795
|
# position goes before one without, so a spectrum is only unplaced
|
|
751
796
|
# when none of its scans is on a pixel.
|
|
752
797
|
first_placed = np.argsort(~placed, kind="stable")
|
|
753
|
-
source, xs, ys, placed = (
|
|
798
|
+
source, xs, ys, placed, scan_row = (
|
|
754
799
|
source[first_placed],
|
|
755
800
|
xs[first_placed],
|
|
756
801
|
ys[first_placed],
|
|
757
802
|
placed[first_placed],
|
|
803
|
+
scan_row[first_placed],
|
|
758
804
|
)
|
|
759
805
|
_, first = np.unique(source, return_index=True)
|
|
760
|
-
source, xs, ys, placed
|
|
806
|
+
source, xs, ys, placed, scan_row = (
|
|
807
|
+
source[first],
|
|
808
|
+
xs[first],
|
|
809
|
+
ys[first],
|
|
810
|
+
placed[first],
|
|
811
|
+
scan_row[first],
|
|
812
|
+
)
|
|
761
813
|
|
|
762
814
|
spectrum_index, counts, levels = self._read_point_counts()
|
|
763
815
|
order = np.argsort(spectrum_index, kind="stable")
|
|
@@ -819,6 +871,7 @@ class MzPeakArchive:
|
|
|
819
871
|
complete = spectrum_index.size == n_listed
|
|
820
872
|
|
|
821
873
|
raw = np.stack([xs[rows], ys[rows]], axis=1)
|
|
874
|
+
z_offset = self._z_offset(scans, z_column, scan_row[rows])
|
|
822
875
|
self._report_shared_pixels(raw)
|
|
823
876
|
bases = self._position_bases(raw)
|
|
824
877
|
coordinates = raw - np.array(bases, dtype=np.int64)
|
|
@@ -831,9 +884,52 @@ class MzPeakArchive:
|
|
|
831
884
|
point_counts=counts,
|
|
832
885
|
offsets=(bases[0] + shift[0], bases[1] + shift[1]),
|
|
833
886
|
complete=complete,
|
|
887
|
+
z_offset=z_offset,
|
|
834
888
|
)
|
|
835
889
|
return self._spatial_index
|
|
836
890
|
|
|
891
|
+
def _z_offset(
|
|
892
|
+
self, scans: Any, z_column: Optional[str], placed_rows: NDArray[np.int64]
|
|
893
|
+
) -> int:
|
|
894
|
+
"""The position z of the archive's plane, 0 when it states none.
|
|
895
|
+
|
|
896
|
+
Recorded as the z offset, the way D14 records the smallest z of an
|
|
897
|
+
imzML. An archive without z records 0, as every format without z
|
|
898
|
+
does (D30). The spectra on a pixel all sit on index 0 of z either
|
|
899
|
+
way: this reader yields one plane.
|
|
900
|
+
|
|
901
|
+
Args:
|
|
902
|
+
scans: The scans member as read, with the z column when there is
|
|
903
|
+
one.
|
|
904
|
+
z_column: The column holding position z, or ``None``.
|
|
905
|
+
placed_rows: The rows of ``scans`` whose spectra are on a pixel.
|
|
906
|
+
|
|
907
|
+
Raises:
|
|
908
|
+
ConversionRefused: If some of those spectra state z and the
|
|
909
|
+
others do not, or if they state more than one z. Spectra on
|
|
910
|
+
several planes would otherwise be summed into one.
|
|
911
|
+
"""
|
|
912
|
+
if z_column is None:
|
|
913
|
+
return 0
|
|
914
|
+
zs, has_z = _positions(scans.column(z_column))
|
|
915
|
+
zs, has_z = zs[placed_rows], has_z[placed_rows]
|
|
916
|
+
if not has_z.any():
|
|
917
|
+
return 0
|
|
918
|
+
if not has_z.all():
|
|
919
|
+
raise ConversionRefused(
|
|
920
|
+
f"{self.path} gives {int(has_z.sum())} of {has_z.size} spectra "
|
|
921
|
+
f"on a pixel a position z ({IMS_POSITION_Z}) and the others "
|
|
922
|
+
f"none. A plane is stated for every spectrum or for none."
|
|
923
|
+
)
|
|
924
|
+
planes = np.unique(zs)
|
|
925
|
+
if planes.size > 1:
|
|
926
|
+
raise ConversionRefused(
|
|
927
|
+
f"{self.path} places its spectra on {planes.size} planes "
|
|
928
|
+
f"(position z from {int(planes[0])} to {int(planes[-1])}). "
|
|
929
|
+
f"Thyra reads one plane of an mzPeak archive."
|
|
930
|
+
)
|
|
931
|
+
return int(planes[0])
|
|
932
|
+
|
|
837
933
|
def _report_shared_pixels(self, raw: NDArray[np.int64]) -> None:
|
|
838
934
|
"""Warn when several spectra sit on one pixel; they are summed there."""
|
|
839
935
|
pixels = np.unique(raw, axis=0).shape[0]
|
|
@@ -877,7 +973,7 @@ class MzPeakArchive:
|
|
|
877
973
|
self.path.name,
|
|
878
974
|
)
|
|
879
975
|
|
|
880
|
-
def
|
|
976
|
+
def imaging_metadata(self) -> Dict[str, Any]:
|
|
881
977
|
"""The archive's ``imaging`` block, or an empty one."""
|
|
882
978
|
imaging = self.file_level_metadata().get("imaging")
|
|
883
979
|
return imaging if isinstance(imaging, dict) else {}
|
|
@@ -892,7 +988,7 @@ class MzPeakArchive:
|
|
|
892
988
|
smallest position instead moved it to the corner and cut the grid
|
|
893
989
|
to its bounding box.
|
|
894
990
|
"""
|
|
895
|
-
declared = self.
|
|
991
|
+
declared = self.imaging_metadata().get("coordinate_base")
|
|
896
992
|
if not isinstance(declared, int) or isinstance(declared, bool):
|
|
897
993
|
declared = SPEC_BASE
|
|
898
994
|
smallest = (int(raw[:, 0].min()), int(raw[:, 1].min()))
|
|
@@ -917,7 +1013,7 @@ class MzPeakArchive:
|
|
|
917
1013
|
frame of the instrument's raster, where a conversion of the same
|
|
918
1014
|
run from its ``.d`` puts it.
|
|
919
1015
|
"""
|
|
920
|
-
offset = self.
|
|
1016
|
+
offset = self.imaging_metadata().get("position_offset")
|
|
921
1017
|
if not isinstance(offset, dict):
|
|
922
1018
|
return (0, 0)
|
|
923
1019
|
values = (offset.get("x", 0), offset.get("y", 0))
|
|
@@ -1090,6 +1186,8 @@ class MzPeakReader(BaseMSIReader):
|
|
|
1090
1186
|
#: Folder the embedded images are copied to, made on first use.
|
|
1091
1187
|
self._image_dir: Optional[Path] = None
|
|
1092
1188
|
self._image_paths: Optional[List[Path]] = None
|
|
1189
|
+
#: What the store calls each copied image, by the copy's path.
|
|
1190
|
+
self._image_labels: Dict[Path, OpticalImageLabel] = {}
|
|
1093
1191
|
self._regions_read = False
|
|
1094
1192
|
self._region_cache: Optional[
|
|
1095
1193
|
Tuple[Dict[Tuple[int, int], int], List[Dict[str, Any]]]
|
|
@@ -1626,8 +1724,10 @@ class MzPeakReader(BaseMSIReader):
|
|
|
1626
1724
|
|
|
1627
1725
|
mzpeak-convert stores an image as an ``image`` member, verbatim.
|
|
1628
1726
|
The optical image loader reads files, so each one is copied to a
|
|
1629
|
-
folder of this reader's own, which :meth:`close` removes.
|
|
1630
|
-
|
|
1727
|
+
folder of this reader's own, which :meth:`close` removes. The copy
|
|
1728
|
+
takes the suffix of the media type the archive declares, so a slide
|
|
1729
|
+
scanner's TIFF is read as the TIFF it is. A member in a format the
|
|
1730
|
+
loader does not read is skipped with a warning.
|
|
1631
1731
|
|
|
1632
1732
|
The images are not aligned to the pixels. The archive gives an
|
|
1633
1733
|
affine for each, but the ones seen so far are marked
|
|
@@ -1639,6 +1739,16 @@ class MzPeakReader(BaseMSIReader):
|
|
|
1639
1739
|
self._image_paths = self._extract_images()
|
|
1640
1740
|
return list(self._image_paths)
|
|
1641
1741
|
|
|
1742
|
+
def get_optical_image_label(self, path: Path) -> Optional[OpticalImageLabel]:
|
|
1743
|
+
"""Name a copied image after its member, not by the vendor rule.
|
|
1744
|
+
|
|
1745
|
+
mzpeak-convert numbers its members ``images/image_0000.<ext>``, and
|
|
1746
|
+
the rule for vendor folders would call the first one the high
|
|
1747
|
+
resolution scan and the second the derived image, whatever they
|
|
1748
|
+
show.
|
|
1749
|
+
"""
|
|
1750
|
+
return self._image_labels.get(Path(path))
|
|
1751
|
+
|
|
1642
1752
|
def _image_members(self) -> List[str]:
|
|
1643
1753
|
"""Names of the members the index lists as images, in index order."""
|
|
1644
1754
|
entries = self.archive.index.get("files")
|
|
@@ -1653,34 +1763,70 @@ class MzPeakReader(BaseMSIReader):
|
|
|
1653
1763
|
names.append(name)
|
|
1654
1764
|
return names
|
|
1655
1765
|
|
|
1766
|
+
def _image_media_types(self) -> Dict[str, str]:
|
|
1767
|
+
"""The media type the archive declares for each image member.
|
|
1768
|
+
|
|
1769
|
+
From ``imaging.images``, where each entry names its member in
|
|
1770
|
+
``archive_path``.
|
|
1771
|
+
"""
|
|
1772
|
+
entries = self.archive.imaging_metadata().get("images")
|
|
1773
|
+
media_types: Dict[str, str] = {}
|
|
1774
|
+
for entry in entries if isinstance(entries, list) else []:
|
|
1775
|
+
if not isinstance(entry, dict):
|
|
1776
|
+
continue
|
|
1777
|
+
path, media_type = entry.get("archive_path"), entry.get("media_type")
|
|
1778
|
+
if isinstance(path, str) and isinstance(media_type, str):
|
|
1779
|
+
media_types[path] = media_type.strip().lower()
|
|
1780
|
+
return media_types
|
|
1781
|
+
|
|
1782
|
+
@staticmethod
|
|
1783
|
+
def _copy_suffix(name: str, media_type: Optional[str]) -> Optional[str]:
|
|
1784
|
+
"""The suffix the loader reads a member under, or ``None`` for none.
|
|
1785
|
+
|
|
1786
|
+
The declared media type decides, so a TIFF named ``.svs`` is read as
|
|
1787
|
+
a TIFF. Without one, the member's own suffix does.
|
|
1788
|
+
"""
|
|
1789
|
+
if media_type in MEDIA_TYPE_SUFFIXES:
|
|
1790
|
+
return MEDIA_TYPE_SUFFIXES[media_type]
|
|
1791
|
+
suffix = Path(name).suffix.lower()
|
|
1792
|
+
return suffix if suffix in OPTICAL_IMAGE_SUFFIXES else None
|
|
1793
|
+
|
|
1656
1794
|
def _extract_images(self) -> List[Path]:
|
|
1657
1795
|
"""Copy every readable image member out of the archive."""
|
|
1658
1796
|
members = self._image_members()
|
|
1659
1797
|
if not members:
|
|
1660
1798
|
return []
|
|
1799
|
+
media_types = self._image_media_types()
|
|
1661
1800
|
paths: List[Path] = []
|
|
1801
|
+
taken: Set[str] = set()
|
|
1662
1802
|
for name in members:
|
|
1663
|
-
suffix =
|
|
1664
|
-
if suffix
|
|
1803
|
+
suffix = self._copy_suffix(name, media_types.get(name))
|
|
1804
|
+
if suffix is None:
|
|
1665
1805
|
logger.warning(
|
|
1666
|
-
"%s embeds the image %s, in a format the optical
|
|
1667
|
-
"loader does not read; it is not carried over.",
|
|
1806
|
+
"%s embeds the image %s (%s), in a format the optical "
|
|
1807
|
+
"image loader does not read; it is not carried over.",
|
|
1668
1808
|
self.data_path.name,
|
|
1669
1809
|
name,
|
|
1810
|
+
media_types.get(name, "no media type declared"),
|
|
1670
1811
|
)
|
|
1671
1812
|
continue
|
|
1672
1813
|
if self._image_dir is None:
|
|
1673
1814
|
self._image_dir = Path(tempfile.mkdtemp(prefix="thyra-mzpeak-images-"))
|
|
1674
1815
|
# The base name only: a member name is a path inside the ZIP
|
|
1675
|
-
# and must not choose where on this disk the copy goes.
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1816
|
+
# and must not choose where on this disk the copy goes. Unique
|
|
1817
|
+
# whatever the case, because the store names the element after
|
|
1818
|
+
# it, lowercased.
|
|
1819
|
+
stem = candidate = Path(name).stem
|
|
1820
|
+
number = 1
|
|
1821
|
+
while candidate.lower() in taken:
|
|
1822
|
+
candidate = f"{stem}_{number}"
|
|
1680
1823
|
number += 1
|
|
1824
|
+
taken.add(candidate.lower())
|
|
1825
|
+
target = self._image_dir / f"{candidate}{suffix}"
|
|
1681
1826
|
with self.archive.open_member(name) as source, target.open("wb") as sink:
|
|
1682
1827
|
shutil.copyfileobj(source, sink)
|
|
1683
1828
|
paths.append(target)
|
|
1829
|
+
self._image_labels[target] = OpticalImageLabel(candidate, name)
|
|
1684
1830
|
if paths:
|
|
1685
1831
|
logger.info(
|
|
1686
1832
|
"%s embeds %d optical image(s); they are carried over without "
|
|
@@ -1699,3 +1845,4 @@ class MzPeakReader(BaseMSIReader):
|
|
|
1699
1845
|
shutil.rmtree(self._image_dir, ignore_errors=True)
|
|
1700
1846
|
self._image_dir = None
|
|
1701
1847
|
self._image_paths = None
|
|
1848
|
+
self._image_labels = {}
|