From 269e8f85aa955ddd154157cecec253967d0bec41 Mon Sep 17 00:00:00 2001 From: Erik O Gabrielsson <83275777+erikogabrielsson@users.noreply.github.com> Date: Fri, 22 Nov 2024 21:54:59 +0100 Subject: [PATCH] Skip lossy jpeg 2000 on macos (#179) --- tests/conftest.py | 67 ++++++++++++------ tests/test_wsidicom_integration.py | 94 +++++++++++++++++--------- tests/testdata/region_definitions.json | 27 +++++++- 3 files changed, 134 insertions(+), 54 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index f6c2d66..ae921d2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -92,47 +92,76 @@ def wsi_names(cls, tiling: Optional[str] = None) -> Iterable[str]: ) @classmethod - def read_region(cls) -> Iterable[Tuple[str, Dict[str, Any]]]: - return cls._get_dict("read_region") + def read_region(cls) -> Iterable[Tuple[str, UID, Dict[str, Any]]]: + return cls._get_test_values_and_transfer_syntax( + "read_region", "level_transfer_syntax" + ) @classmethod - def read_region_mm(cls) -> Iterable[Tuple[str, Dict[str, Any]]]: - return cls._get_dict("read_region_mm") + def read_region_mm(cls) -> Iterable[Tuple[str, UID, Dict[str, Any]]]: + return cls._get_test_values_and_transfer_syntax( + "read_region_mm", "level_transfer_syntax" + ) @classmethod - def read_region_mpp(cls) -> Iterable[Tuple[str, Dict[str, Any]]]: - return cls._get_dict("read_region_mpp") + def read_region_mpp(cls) -> Iterable[Tuple[str, UID, Dict[str, Any]]]: + return cls._get_test_values_and_transfer_syntax( + "read_region_mpp", "level_transfer_syntax" + ) @classmethod - def read_tile(cls) -> Iterable[Tuple[str, Dict[str, Any]]]: - return cls._get_dict("read_tile") + def read_tile(cls) -> Iterable[Tuple[str, UID, Dict[str, Any]]]: + return cls._get_test_values_and_transfer_syntax( + "read_tile", "level_transfer_syntax" + ) @classmethod - def read_encoded_tile(cls) -> Iterable[Tuple[str, Dict[str, Any]]]: - return cls._get_dict("read_encoded_tile") + def read_encoded_tile(cls) -> Iterable[Tuple[str, UID, Dict[str, Any]]]: + return cls._get_test_values_and_transfer_syntax( + "read_encoded_tile", "level_transfer_syntax" + ) @classmethod - def read_thumbnail(cls) -> Iterable[Tuple[str, Dict[str, Any]]]: - return cls._get_dict("read_thumbnail") + def read_thumbnail(cls) -> Iterable[Tuple[str, UID, Dict[str, Any]]]: + return cls._get_test_values_and_transfer_syntax( + "read_thumbnail", "level_transfer_syntax" + ) @classmethod def levels(cls) -> Iterable[Tuple[str, int]]: return cls._get_parameter("levels") @classmethod - def label_hash(cls) -> Iterable[Tuple[str, Optional[str]]]: - return cls._get_parameter("label") + def label_hash(cls) -> Iterable[Tuple[str, UID, Optional[str]]]: + return cls._get_parameter_and_transfer_syntax("label", "label_transfer_syntax") + + @classmethod + def overview_hash(cls) -> Iterable[Tuple[str, UID, Optional[str]]]: + return cls._get_parameter_and_transfer_syntax( + "overview", "overview_transfer_syntax" + ) @classmethod - def overview_hash(cls) -> Iterable[Tuple[str, Optional[str]]]: - return cls._get_parameter("overview") + def _get_test_values_and_transfer_syntax( + cls, test_value_name: str, transfer_syntax_name: str + ) -> Iterable[Tuple[str, UID, Dict[str, Any]]]: + return [ + (wsi_name, UID(wsi_definition[transfer_syntax_name]), region) + for wsi_name, wsi_definition in cls.test_definitions.items() + for region in wsi_definition[test_value_name] + ] @classmethod - def _get_dict(cls, region_name: str) -> Iterable[Tuple[str, Dict[str, Any]]]: + def _get_parameter_and_transfer_syntax( + cls, parameter_name: str, transfer_syntax_name: str + ) -> Iterable[Tuple[str, UID, Any]]: return [ - (wsi_name, region) + ( + wsi_name, + UID(wsi_definition[transfer_syntax_name]), + wsi_definition[parameter_name], + ) for wsi_name, wsi_definition in cls.test_definitions.items() - for region in wsi_definition[region_name] ] @classmethod diff --git a/tests/test_wsidicom_integration.py b/tests/test_wsidicom_integration.py index 4c9d9b7..4efd3ce 100644 --- a/tests/test_wsidicom_integration.py +++ b/tests/test_wsidicom_integration.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import platform from hashlib import md5 from pathlib import Path from typing import Any, Callable, Dict, Optional @@ -19,6 +20,7 @@ import numpy as np import pytest from PIL import Image as Pillow +from pydicom.uid import JPEG2000, UID from tests.conftest import WsiInputType, WsiTestDefinitions from wsidicom import WsiDicom @@ -35,15 +37,19 @@ ], ) class TestWsiDicomIntegration: - @pytest.mark.parametrize(["wsi_name", "region"], WsiTestDefinitions.read_region()) + @pytest.mark.parametrize( + ["wsi_name", "transfer_syntax", "region"], WsiTestDefinitions.read_region() + ) def test_read_region( self, - wsi_name: Path, + wsi_name: str, + transfer_syntax: UID, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], region: Dict[str, Any], ): # Arrange + self._skip_lossy_jpeg2000_tests_on_macos(transfer_syntax) wsi = wsi_factory(wsi_name, input_type) # Act @@ -58,16 +64,18 @@ def test_read_region( assert checksum == region["md5"], (region, checksum) @pytest.mark.parametrize( - ["wsi_name", "region"], WsiTestDefinitions.read_region_mm() + ["wsi_name", "transfer_syntax", "region"], WsiTestDefinitions.read_region_mm() ) def test_read_region_mm( self, - wsi_name: Path, + wsi_name: str, + transfer_syntax: UID, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], region: Dict[str, Any], ): # Arrange + self._skip_lossy_jpeg2000_tests_on_macos(transfer_syntax) wsi = wsi_factory(wsi_name, input_type) # Act @@ -82,16 +90,18 @@ def test_read_region_mm( assert checksum == region["md5"], (region, checksum) @pytest.mark.parametrize( - ["wsi_name", "region"], WsiTestDefinitions.read_region_mpp() + ["wsi_name", "transfer_syntax", "region"], WsiTestDefinitions.read_region_mpp() ) def test_read_region_mpp( self, - wsi_name: Path, + wsi_name: str, + transfer_syntax: UID, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], region: Dict[str, Any], ): # Arrange + self._skip_lossy_jpeg2000_tests_on_macos(transfer_syntax) wsi = wsi_factory(wsi_name, input_type) # Act @@ -105,15 +115,19 @@ def test_read_region_mpp( checksum = md5(im.tobytes()).hexdigest() assert checksum == region["md5"], (region, checksum) - @pytest.mark.parametrize(["wsi_name", "region"], WsiTestDefinitions.read_tile()) + @pytest.mark.parametrize( + ["wsi_name", "transfer_syntax", "region"], WsiTestDefinitions.read_tile() + ) def test_read_tile( self, - wsi_name: Path, + wsi_name: str, + transfer_syntax: UID, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], region: Dict[str, Any], ): # Arrange + self._skip_lossy_jpeg2000_tests_on_macos(transfer_syntax) wsi = wsi_factory(wsi_name, input_type) # Act @@ -127,16 +141,19 @@ def test_read_tile( assert checksum == region["md5"], (region, checksum) @pytest.mark.parametrize( - ["wsi_name", "region"], WsiTestDefinitions.read_encoded_tile() + ["wsi_name", "transfer_syntax", "region"], + WsiTestDefinitions.read_encoded_tile(), ) def test_read_encoded_tile( self, - wsi_name: Path, + wsi_name: str, + transfer_syntax: UID, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], region: Dict[str, Any], ): # Arrange + self._skip_lossy_jpeg2000_tests_on_macos(transfer_syntax) wsi = wsi_factory(wsi_name, input_type) # Act @@ -150,16 +167,18 @@ def test_read_encoded_tile( assert checksum == region["md5"], (region, checksum) @pytest.mark.parametrize( - ["wsi_name", "region"], WsiTestDefinitions.read_thumbnail() + ["wsi_name", "transfer_syntax", "region"], WsiTestDefinitions.read_thumbnail() ) def test_read_thumbnail( self, - wsi_name: Path, + wsi_name: str, + transfer_syntax: UID, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], region: Dict[str, Any], ): # Arrange + self._skip_lossy_jpeg2000_tests_on_macos(transfer_syntax) wsi = wsi_factory(wsi_name, input_type) # Act @@ -174,9 +193,9 @@ def test_read_thumbnail( ) def test_number_of_levels( self, - wsi_name: Path, + wsi_name: str, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], expected_level_count: int, ): # Arrange @@ -189,16 +208,19 @@ def test_number_of_levels( assert levels_count == expected_level_count @pytest.mark.parametrize( - ["wsi_name", "expected_label_hash"], WsiTestDefinitions.label_hash() + ["wsi_name", "transfer_syntax", "expected_label_hash"], + WsiTestDefinitions.label_hash(), ) - def test_has_label( + def test_read_label( self, - wsi_name: Path, + wsi_name: str, + transfer_syntax: UID, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], expected_label_hash: Optional[bool], ): # Arrange + self._skip_lossy_jpeg2000_tests_on_macos(transfer_syntax) wsi = wsi_factory(wsi_name, input_type) # Act @@ -211,16 +233,19 @@ def test_has_label( assert checksum == expected_label_hash @pytest.mark.parametrize( - ["wsi_name", "expected_overview_hash"], WsiTestDefinitions.overview_hash() + ["wsi_name", "transfer_syntax", "expected_overview_hash"], + WsiTestDefinitions.overview_hash(), ) - def test_has_overview( + def test_read_overview( self, - wsi_name: Path, + wsi_name: str, + transfer_syntax: UID, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], expected_overview_hash: Optional[str], ): # Arrange + self._skip_lossy_jpeg2000_tests_on_macos(transfer_syntax) wsi = wsi_factory(wsi_name, input_type) # Act @@ -253,9 +278,9 @@ def test_save_replace_label( @pytest.mark.parametrize("wsi_name", WsiTestDefinitions.wsi_names("full")) def test_levels_returns_selected_pyramid( self, - wsi_name: Path, + wsi_name: str, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], ): # Arrange wsi = wsi_factory(wsi_name, input_type) @@ -270,9 +295,9 @@ def test_levels_returns_selected_pyramid( @pytest.mark.parametrize("wsi_name", WsiTestDefinitions.wsi_names("full")) def test_pyramid_returns_selected_pyramid( self, - wsi_name: Path, + wsi_name: str, input_type: WsiInputType, - wsi_factory: Callable[[Path, WsiInputType], WsiDicom], + wsi_factory: Callable[[str, WsiInputType], WsiDicom], ): # Arrange wsi = wsi_factory(wsi_name, input_type) @@ -283,3 +308,8 @@ def test_pyramid_returns_selected_pyramid( # Assert assert isinstance(pyramid, Pyramid) assert pyramid == wsi.pyramids[wsi.selected_pyramid] + + def _skip_lossy_jpeg2000_tests_on_macos(self, transfer_syntax: UID): + """Lossy Jpeg2000 does not produce same output on macOS.""" + if platform.system() == "Darwin" and transfer_syntax == JPEG2000: + pytest.skip("Lossy JPEG2000 does not produce same output on macOS.") diff --git a/tests/testdata/region_definitions.json b/tests/testdata/region_definitions.json index 77a8357..54d80f7 100644 --- a/tests/testdata/region_definitions.json +++ b/tests/testdata/region_definitions.json @@ -8,6 +8,9 @@ "label": "991552d66ac9b3a82ce61d4af47824e6", "overview": "06147bec99dc9d6852f281f317fbb96c", "tiled": "full", + "level_transfer_syntax": "1.2.840.10008.1.2.4.50", + "label_transfer_syntax": "1.2.840.10008.1.2.4.50", + "overview_transfer_syntax": "1.2.840.10008.1.2.4.50", "read_region": [ { "location": { @@ -193,6 +196,9 @@ "label": null, "overview": null, "tiled": "sparse", + "level_transfer_syntax": "1.2.840.10008.1.2.4.50", + "label_transfer_syntax": "1.2.840.10008.1.2.4.50", + "overview_transfer_syntax": "1.2.840.10008.1.2.4.50", "read_region": [ { "location": { @@ -378,6 +384,9 @@ "label": "f93c9845d9c7454125a55230a8202a9a", "overview": "aa1d544afebabd89a30c46de332cb11a", "tiled": "sparse", + "level_transfer_syntax": "1.2.840.10008.1.2.4.50", + "label_transfer_syntax": "1.2.840.10008.1.2.4.50", + "overview_transfer_syntax": "1.2.840.10008.1.2.4.50", "read_region": [ { "location": { @@ -458,6 +467,9 @@ "levels": 3, "label": "6634ad9dbe8c8f074266e21ef8eb6c12", "overview": "3d792eb3441c58c5d881cb0cf21a397e", + "level_transfer_syntax": " 1.2.840.10008.1.2.4.91", + "label_transfer_syntax": "1.2.840.10008.1.2.1", + "overview_transfer_syntax": "1.2.840.10008.1.2.1", "tiled": "sparse", "read_region": [ { @@ -540,6 +552,9 @@ "label": "5a9a991e350f4fe29af08b7e3bcc56df", "overview": "c3d8f41772dd26a19121f4f87d9f520c", "tiled": "sparse", + "level_transfer_syntax": " 1.2.840.10008.1.2.4.91", + "label_transfer_syntax": "1.2.840.10008.1.2.1", + "overview_transfer_syntax": "1.2.840.10008.1.2.1", "read_region": [ { "location": { @@ -621,6 +636,9 @@ "label": "692c84fcb25130abcb65169ec4f4d37f", "overview": "2274f918d7ca4b3f0951de77d1c48d04", "tiled": "sparse", + "level_transfer_syntax": "1.2.840.10008.1.2.4.50", + "label_transfer_syntax": "1.2.840.10008.1.2.4.50", + "overview_transfer_syntax": "1.2.840.10008.1.2.4.50", "read_region": [ { "location": { @@ -633,7 +651,8 @@ "height": 300 }, "md5": "de274a86b15ed4fca984f6619c6e5547" - }], + } + ], "read_region_mm": [ { "location": { @@ -646,7 +665,8 @@ "height": 2.0 }, "md5": "ec4e7d038f1de1021fcb3e0027f0105b" - }], + } + ], "read_region_mpp": [ { "location": { @@ -659,7 +679,8 @@ "height": 2.0 }, "md5": "439021d08a2e2fb755b1b65519eb2836" - }], + } + ], "read_tile": [ { "location": {