diff --git a/CHANGELOG.md b/CHANGELOG.md index 827c223..1114f76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.16.0] - 2025-01-29 + ### Added - Support for reading isyntax files using pyisyntax. @@ -15,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Reading correct image size in pyramid series in Bioformats reader. - Skip non-dyadic pyramid series in Bioformats reader. +- Missing values in `LossyImageCompressionRatio` and `LossyImageCompressionMethod` when converting without re-encoding. +- Pin zarr to <3.0 to fix import exception. ## [0.15.1] - 2025-01-07 @@ -285,7 +289,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initial release of wsidicomizer -[Unreleased]: https://github.com/imi-bigpicture/wsidicomizer/compare/0.15.1..HEAD +[Unreleased]: https://github.com/imi-bigpicture/wsidicomizer/compare/0.16.0..HEAD +[0.16.0]: https://github.com/imi-bigpicture/wsidicomizer/compare/0.16.0..0.16.0 [0.15.1]: https://github.com/imi-bigpicture/wsidicomizer/compare/0.15.0..0.15.1 [0.15.0]: https://github.com/imi-bigpicture/wsidicomizer/compare/0.14.2..0.15.0 [0.14.2]: https://github.com/imi-bigpicture/wsidicomizer/compare/0.14.1..0.14.2 diff --git a/poetry.lock b/poetry.lock index ed32d18..9f3881a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1017,13 +1017,13 @@ Pillow = "*" [[package]] name = "opentile" -version = "0.13.4" +version = "0.14.0" description = "Read tiles from wsi-TIFF files" optional = false python-versions = "<4.0,>=3.9" files = [ - {file = "opentile-0.13.4-py3-none-any.whl", hash = "sha256:f89de52524c231bcbef5119ca0d9901d57a569c365a2e2619ff20a0b4f6693a5"}, - {file = "opentile-0.13.4.tar.gz", hash = "sha256:4ae56512ae40a39c55ff115abdaf6f3ca5052393f102646f5bd37768c7376303"}, + {file = "opentile-0.14.0-py3-none-any.whl", hash = "sha256:b205caf2c71d1cc7975f84c07b4919c3f1aa6545970ea311cf35fdaa5e90d6cb"}, + {file = "opentile-0.14.0.tar.gz", hash = "sha256:fc5f10f7cb5b8e64a2a42c58390781ac55570e2c940c0dc542bcc207a36d4808"}, ] [package.dependencies] @@ -1729,13 +1729,13 @@ watchmedo = ["PyYAML (>=3.10)"] [[package]] name = "wsidicom" -version = "0.22.0" +version = "0.23.0" description = "Tools for handling DICOM based whole scan images" optional = false python-versions = "<4.0,>=3.10" files = [ - {file = "wsidicom-0.22.0-py3-none-any.whl", hash = "sha256:9e364b3cc4f15e29f8c49bc35e57de60682ea03c00fcb379ab72c9fbb934da4e"}, - {file = "wsidicom-0.22.0.tar.gz", hash = "sha256:576b352369171a535faff957cbaa8aa58368ddc1f564dddba175df2fd44085df"}, + {file = "wsidicom-0.23.0-py3-none-any.whl", hash = "sha256:e5c3fa9d354f56a653d69a679d4efdbe152dea2d071e71ae24fb38d2d903f66d"}, + {file = "wsidicom-0.23.0.tar.gz", hash = "sha256:3eb33becb4c82821cb3127e6e92ed933eab8cf0e25c5955ab426febeda5bacd2"}, ] [package.dependencies] @@ -1803,4 +1803,4 @@ openslide = ["openslide-python"] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "d9a687c54711c24ea428a6d6345a0c985e0af8966bbcbbede54088c36034ce9b" +content-hash = "785b3a19fed13992780966d3270bba6e00928156dd2584bca085985201d34868" diff --git a/pyproject.toml b/pyproject.toml index 47723bd..38dc311 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wsidicomizer" -version = "0.15.1" +version = "0.16.0" description = "Tool for reading WSI files from proprietary formats and optionally convert them to to DICOM" authors = ["Erik O Gabrielsson "] license = "Apache-2.0" @@ -18,8 +18,8 @@ classifiers = [ [tool.poetry.dependencies] python = "^3.10" -wsidicom = "^0.22.0" -opentile = "^0.13.1" +wsidicom = "^0.23.0" +opentile = "^0.14.0" numpy = ">=1.22.0" pydicom = ">=3.0.0" czifile = "^2019.7.2" @@ -29,6 +29,7 @@ scyjava = { version = "^1.8.1", optional = true } ome-types = {version = "^0.5.0", optional = true } pyisyntax = {version = "^0.1.2", optional = true } imagecodecs = { version = "^2024.12.30", optional = true } +zarr = ">=2.11.0, <3.0" [tool.poetry.extras] openslide = ["openslide-python"] diff --git a/tests/conftest.py b/tests/conftest.py index 19312ce..0b8b8d1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,31 +157,18 @@ "image_coordinate_system": {"x": 2.3061675, "y": 20.79015}, "icc_profile": False, "read_region": [ - # OpenSlide produces different results across platforms - # { - # "location": { - # "x": 50, - # "y": 100 - # }, - # "level": 6, - # "size": { - # "width": 500, - # "height": 500 - # }, - # "md5": "fe29e76f5904d65253d8eb742b244789" - # }, - # { - # "location": { - # "x": 400, - # "y": 500 - # }, - # "level": 4, - # "size": { - # "width": 500, - # "height": 500 - # }, - # "md5": "4f4c904ed9257e385fc8f0818337d9e7" - # } + { + "location": {"x": 50, "y": 100}, + "level": 6, + "size": {"width": 500, "height": 500}, + "md5": "b9052fc1ebbe99582906c575511fe944", + }, + { + "location": {"x": 400, "y": 500}, + "level": 4, + "size": {"width": 500, "height": 500}, + "md5": "a11881460a4fae78ddfef1100339cac0", + }, ], "read_region_openslide": [ { @@ -196,6 +183,7 @@ }, ], "read_thumbnail": [], + "skip_hash_test_platforms": ["Darwin"], } }, "ndpi": { diff --git a/wsidicomizer/__init__.py b/wsidicomizer/__init__.py index 7cc0391..9d0296d 100644 --- a/wsidicomizer/__init__.py +++ b/wsidicomizer/__init__.py @@ -14,6 +14,6 @@ from wsidicomizer.wsidicomizer import WsiDicomizer -__version__ = "0.15.1" +__version__ = "0.16.0" __all__ = ["WsiDicomizer"] diff --git a/wsidicomizer/extras/bioformats/bioformats_image_data.py b/wsidicomizer/extras/bioformats/bioformats_image_data.py index 57e03f2..45ea433 100644 --- a/wsidicomizer/extras/bioformats/bioformats_image_data.py +++ b/wsidicomizer/extras/bioformats/bioformats_image_data.py @@ -94,6 +94,10 @@ def photometric_interpretation(self) -> str: data.""" return self.encoder.photometric_interpretation + @property + def thread_safe(self) -> bool: + return True + def _get_tile( self, tile_point: Point, z: float, path: str ) -> ContextManager[np.ndarray]: diff --git a/wsidicomizer/extras/isyntax/isyntax_image_data.py b/wsidicomizer/extras/isyntax/isyntax_image_data.py index 36589f1..0cb010c 100644 --- a/wsidicomizer/extras/isyntax/isyntax_image_data.py +++ b/wsidicomizer/extras/isyntax/isyntax_image_data.py @@ -116,6 +116,10 @@ def blank_color(self) -> Union[int, Tuple[int, int, int]]: def image_coordinate_system(self) -> ImageCoordinateSystem: return ImageCoordinateSystem(PointMm(0, 0), 0) + @property + def thread_safe(selt) -> bool: + return False + def stitch_tiles(self, region: Region, path: str, z: float, threads: int) -> Image: """Overrides ImageData stitch_tiles() to read reagion directly from ISyntax object. @@ -302,6 +306,10 @@ def pyramid_index(self) -> int: def image_coordinate_system(self) -> ImageCoordinateSystem: return ImageCoordinateSystem(PointMm(0, 0), 0) + @property + def thread_safe(self) -> bool: + return True + def _get_encoded_tile(self, tile: Point, z: float, path: str) -> bytes: if z not in self.focal_planes or path not in self.optical_paths: raise WsiDicomNotFoundError( diff --git a/wsidicomizer/extras/openslide/openslide_image_data.py b/wsidicomizer/extras/openslide/openslide_image_data.py index e1e1550..3046a0a 100644 --- a/wsidicomizer/extras/openslide/openslide_image_data.py +++ b/wsidicomizer/extras/openslide/openslide_image_data.py @@ -98,6 +98,10 @@ def optical_paths(self) -> List[str]: def blank_color(self) -> Union[int, Tuple[int, int, int]]: return self._blank_color + @property + def thread_safe(self) -> bool: + return True + def _get_blank_color( self, photometric_interpretation: str ) -> Union[int, Tuple[int, int, int]]: diff --git a/wsidicomizer/image_data.py b/wsidicomizer/image_data.py index 3d91231..62ad78d 100644 --- a/wsidicomizer/image_data.py +++ b/wsidicomizer/image_data.py @@ -16,11 +16,13 @@ files.""" from abc import ABCMeta, abstractmethod +from typing import List, Optional, Tuple import numpy as np from PIL import Image as Pillow from PIL.Image import Image from wsidicom import ImageData +from wsidicom.codec import LossyCompressionIsoStandard from wsidicom.geometry import PointMm, Size from wsidicom.metadata import ImageCoordinateSystem @@ -49,9 +51,11 @@ def bits(self) -> int: return self.encoder.bits @property - def lossy_compressed(self) -> bool: - # TODO: This should be set from encoder and base file. - return True + def lossy_compression( + self, + ) -> Optional[List[Tuple[LossyCompressionIsoStandard, float]]]: + """Return None as image compression is for most format not known.""" + return None def _encode(self, image_data: np.ndarray) -> bytes: """Return image data encoded in jpeg using set quality and subsample diff --git a/wsidicomizer/sources/czi/czi_image_data.py b/wsidicomizer/sources/czi/czi_image_data.py index 3148bce..b791e32 100644 --- a/wsidicomizer/sources/czi/czi_image_data.py +++ b/wsidicomizer/sources/czi/czi_image_data.py @@ -103,6 +103,10 @@ def photometric_interpretation(self) -> str: def pixel_spacing(self) -> SizeMm: return self._pixel_spacing + @property + def thread_safe(self) -> bool: + return True + @cached_property def image_size(self) -> Size: """The pixel size of the image.""" diff --git a/wsidicomizer/sources/opentile/opentile_image_data.py b/wsidicomizer/sources/opentile/opentile_image_data.py index 301261e..f9342b2 100644 --- a/wsidicomizer/sources/opentile/opentile_image_data.py +++ b/wsidicomizer/sources/opentile/opentile_image_data.py @@ -14,14 +14,14 @@ """Image data for opentile compatible file.""" -from typing import Iterable, Iterator, List, Optional +from typing import Iterable, Iterator, List, Optional, Tuple from opentile.tiff_image import TiffImage from PIL import Image as Pillow from PIL.Image import Image from pydicom.uid import JPEG2000, UID, JPEG2000Lossless, JPEGBaseline8Bit from tifffile import COMPRESSION, PHOTOMETRIC -from wsidicom.codec import Encoder +from wsidicom.codec import Encoder, LossyCompressionIsoStandard from wsidicom.geometry import Point, Size, SizeMm from wsidicom.metadata import Image as ImageMetadata from wsidicom.metadata import ImageCoordinateSystem @@ -138,6 +138,25 @@ def photometric_interpretation(self) -> str: def samples_per_pixel(self) -> int: return self._tiff_image.samples_per_pixel + @property + def thread_safe(self) -> bool: + return True + + @property + def lossy_compression( + self, + ) -> Optional[List[Tuple[LossyCompressionIsoStandard, float]]]: + """Return lossy compression method and compression ratio if lossy compressed.""" + iso = LossyCompressionIsoStandard.transfer_syntax_to_iso(self.transfer_syntax) + if iso is None: + return None + uncompressed_size = ( + self.image_size.area * self.samples_per_pixel * self.bits // 8 + ) + compressed_size = self._tiff_image.compressed_size + compression_ratio = round(compressed_size / uncompressed_size, 2) + return [(iso, compression_ratio)] + def _get_encoded_tile(self, tile: Point, z: float, path: str) -> bytes: """Return image bytes for tile. Returns transcoded tile if non-supported encoding. diff --git a/wsidicomizer/sources/tiffslide/tiffslide_image_data.py b/wsidicomizer/sources/tiffslide/tiffslide_image_data.py index a55b12d..6719f03 100644 --- a/wsidicomizer/sources/tiffslide/tiffslide_image_data.py +++ b/wsidicomizer/sources/tiffslide/tiffslide_image_data.py @@ -86,6 +86,10 @@ def optical_paths(self) -> List[str]: def blank_color(self) -> Union[int, Tuple[int, int, int]]: return self._blank_color + @property + def thread_safe(self) -> bool: + return True + def _get_blank_color( self, photometric_interpretation: str ) -> Union[int, Tuple[int, int, int]]: