Skip to content

Commit

Permalink
Metadata fixes (#170)
Browse files Browse the repository at this point in the history
* Fix use of ZOffsetInSlideCoordinateSystem

* Fix skip serialization of empty PrimaryAnatomicStructureSequence

* Fix skip serialization of empty ContainerComponentSequence

* Fix missing WholeSlideMicroscopyImageFrameTypeSequence for some image types

* Fix skip setting SpacingBetweenSlices if no focal planes

* Use defined slice_thickness

* Test for serializing full json metadata

* Update version
  • Loading branch information
erikogabrielsson authored Oct 28, 2024
1 parent e88c91c commit 08045d4
Show file tree
Hide file tree
Showing 17 changed files with 230 additions and 124 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.21.3] - 2024-10-28

### Fixed

- Missing `WholeSlideMicroscopyImageFrameTypeSequence` in produced DICOM dataset for some image types.
- Do not insert empty `ContainerComponentSequence` and `PrimaryAnatomicStructureSequence` into produced DICOM dataset.
- Only insert `SpacingBetweenSlices` into produced DICOM dataset if multiple focal planes.
- Prefer use of `ZOffsetInSlideCoordinateSystem` in main DICOM dataset to attribute in `SharedFunctionalGroupsSequence`/`PlanePositionSlideSequence`.

## [0.21.2] - 2024-10-21

### Fixed
Expand Down Expand Up @@ -405,7 +414,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Initial release of wsidicom

[Unreleased]: https://github.com/imi-bigpicture/wsidicom/compare/0.21.2..HEAD
[Unreleased]: https://github.com/imi-bigpicture/wsidicom/compare/v0.21.3..HEAD
[0.21.3]: https://github.com/imi-bigpicture/wsidicom/compare/v0.21.2..v0.21.3
[0.21.2]: https://github.com/imi-bigpicture/wsidicom/compare/v0.21.1..v0.21.2
[0.21.1]: https://github.com/imi-bigpicture/wsidicom/compare/v0.21.0..v0.21.1
[0.21.0]: https://github.com/imi-bigpicture/wsidicom/compare/v0.20.6..v0.21.0
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "wsidicom"
version = "0.21.2"
version = "0.21.3"
description = "Tools for handling DICOM based whole scan images"
authors = ["Erik O Gabrielsson <[email protected]>"]
license = "Apache-2.0"
Expand Down
48 changes: 47 additions & 1 deletion tests/metadata/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@
SpecimenSamplingProcedureCode,
SpecimenStainsCode,
)
from wsidicom.geometry import SizeMm
from wsidicom.geometry import PointMm, SizeMm
from wsidicom.instance.dataset import ImageType
from wsidicom.metadata import (
Equipment,
ExtendedDepthOfField,
Expand Down Expand Up @@ -536,6 +537,51 @@ def study():
)


@pytest.fixture()
def illumination():
yield IlluminationColorCode("Full Spectrum")


@pytest.fixture()
def acquisition_datetime():
yield datetime.datetime(2023, 8, 5)


@pytest.fixture()
def focus_method():
yield FocusMethod.AUTO


@pytest.fixture()
def extended_depth_of_field():
yield ExtendedDepthOfField(5, 0.5)


@pytest.fixture()
def image_coordinate_system():
yield ImageCoordinateSystem(PointMm(20.0, 30.0), 90.0)


@pytest.fixture()
def pixel_spacing():
yield None


@pytest.fixture()
def focal_plane_spacing():
yield None


@pytest.fixture()
def depth_of_field():
yield None


@pytest.fixture()
def image_type():
yield ImageType.VOLUME


@pytest.fixture()
def wsi_metadata(
study: Study,
Expand Down
56 changes: 4 additions & 52 deletions tests/metadata/dicom_schema/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from datetime import datetime

import pytest
from pydicom import Dataset
Expand All @@ -24,7 +23,6 @@
code_to_code_dataset,
)
from wsidicom.conceptcode import IlluminationColorCode
from wsidicom.geometry import PointMm
from wsidicom.instance import ImageType
from wsidicom.metadata import (
Equipment,
Expand All @@ -37,11 +35,6 @@
Study,
WsiMetadata,
)
from wsidicom.metadata.image import (
ExtendedDepthOfField,
FocusMethod,
ImageCoordinateSystem,
)
from wsidicom.metadata.schema.dicom.optical_path import LutDicomFormatter
from wsidicom.metadata.schema.dicom.slide import SlideDicomSchema

Expand Down Expand Up @@ -74,6 +67,10 @@ def dicom_image(image: Image, valid_dicom: bool):
origin = Dataset()
origin.XOffsetInSlideCoordinateSystem = image.image_coordinate_system.origin.x
origin.YOffsetInSlideCoordinateSystem = image.image_coordinate_system.origin.y
if image.image_coordinate_system.z_offset is not None:
origin.ZOffsetInSlideCoordinateSystem = (
image.image_coordinate_system.z_offset
)

dataset.TotalPixelMatrixOriginSequence = [origin]
elif not valid_dicom:
Expand Down Expand Up @@ -313,48 +310,3 @@ def dicom_wsi_metadata(
dataset.DimensionOrganizationSequence = dimension_organization_sequence
dataset.FrameOfReferenceUID = wsi_metadata.default_frame_of_reference_uid
yield dataset


@pytest.fixture()
def illumination():
yield IlluminationColorCode("Full Spectrum")


@pytest.fixture()
def acquisition_datetime():
yield datetime(2023, 8, 5)


@pytest.fixture()
def focus_method():
yield FocusMethod.AUTO


@pytest.fixture()
def extended_depth_of_field():
yield ExtendedDepthOfField(5, 0.5)


@pytest.fixture()
def image_coordinate_system():
yield ImageCoordinateSystem(PointMm(20.0, 30.0), 90.0)


@pytest.fixture()
def pixel_spacing():
yield None


@pytest.fixture()
def focal_plane_spacing():
yield None


@pytest.fixture()
def depth_of_field():
yield None


@pytest.fixture()
def image_type():
yield ImageType.VOLUME
9 changes: 8 additions & 1 deletion tests/metadata/dicom_schema/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,13 @@ def assert_dicom_image_equals_image(dicom_image: Dataset, image: Image):
assert dicom_image.ImageOrientationSlide == list(
image.image_coordinate_system.orientation.values
)
if image.image_coordinate_system.z_offset is not None:
assert (
dicom_image.TotalPixelMatrixOriginSequence[
0
].ZOffsetInSlideCoordinateSystem
== image.image_coordinate_system.z_offset
)
if any(
item is not None
for item in [
Expand Down Expand Up @@ -762,7 +769,7 @@ def create_description_dataset(
issuer_of_identifier_dataset
]

if primary_anatomic_structures is not None:
if primary_anatomic_structures is not None and len(primary_anatomic_structures) > 0:
description.PrimaryAnatomicStructureSequence = [
create_code_dataset(item) for item in primary_anatomic_structures
]
Expand Down
47 changes: 28 additions & 19 deletions tests/metadata/dicom_schema/sample/test_dicom_sample.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ class TestSampleDicom:
],
],
)
@pytest.mark.parametrize(
"primary_anatomic_structures", ([], [Code("value", "schema", "meaning")])
)
def test_slide_sample_from_dataset(
self,
slide_sample_ids: Sequence[str],
Expand Down Expand Up @@ -269,6 +272,9 @@ def test_slide_sample_from_dataset(
],
],
)
@pytest.mark.parametrize(
"primary_anatomic_structures", ([], [Code("value", "schema", "meaning")])
)
def test_slide_sample_to_dataset(
self,
slide_sample_ids: Sequence[str],
Expand Down Expand Up @@ -361,26 +367,29 @@ def test_slide_sample_to_dataset(
assert description.SpecimenDetailedDescription == detailed_description
else:
assert "SpecimenDetailedDescription" not in description
assert len(description.PrimaryAnatomicStructureSequence) == len(
primary_anatomic_structures
)
for index, primary_anatomic_structure in enumerate(
primary_anatomic_structures
):
assert (
description.PrimaryAnatomicStructureSequence[index].CodeValue
== primary_anatomic_structure.value
)
assert (
description.PrimaryAnatomicStructureSequence[
index
].CodingSchemeDesignator
== primary_anatomic_structure.scheme_designator
)
assert (
description.PrimaryAnatomicStructureSequence[index].CodeMeaning
== primary_anatomic_structure.meaning
if len(primary_anatomic_structures) > 0:
assert len(description.PrimaryAnatomicStructureSequence) == len(
primary_anatomic_structures
)
for index, primary_anatomic_structure in enumerate(
primary_anatomic_structures
):
assert (
description.PrimaryAnatomicStructureSequence[index].CodeValue
== primary_anatomic_structure.value
)
assert (
description.PrimaryAnatomicStructureSequence[
index
].CodingSchemeDesignator
== primary_anatomic_structure.scheme_designator
)
assert (
description.PrimaryAnatomicStructureSequence[index].CodeMeaning
== primary_anatomic_structure.meaning
)
else:
assert "PrimaryAnatomicStructureSequence" not in description

step_iterator = iter(description.SpecimenPreparationSequence)

Expand Down
4 changes: 2 additions & 2 deletions tests/metadata/dicom_schema/test_dicom_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ def test_deserialize_equipment(
datetime(2023, 8, 5, 12, 13, 14, 150),
FocusMethod.MANUAL,
ExtendedDepthOfField(15, 0.5),
ImageCoordinateSystem(PointMm(50.0, 20.0), 180.0),
ImageCoordinateSystem(PointMm(50.0, 20.0), 180.0, 1.0),
SizeMm(0.5, 0.5),
0.25,
2.5,
Expand Down Expand Up @@ -226,7 +226,7 @@ def test_serialize_default_image(self):
datetime(2023, 8, 5, 12, 13, 14, 150),
FocusMethod.MANUAL,
ExtendedDepthOfField(15, 0.5),
ImageCoordinateSystem(PointMm(50.0, 20.0), 180.0),
ImageCoordinateSystem(PointMm(50.0, 20.0), 180.0, 1.0),
SizeMm(0.5, 0.5),
0.25,
2.5,
Expand Down
35 changes: 35 additions & 0 deletions tests/metadata/json_schema/test_json_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@
SeriesJsonSchema,
StudyJsonSchema,
)
from wsidicom.metadata.schema.json.wsi import WsiMetadataJsonSchema
from wsidicom.metadata.wsi import WsiMetadata


class TestJsonSchema:
Expand Down Expand Up @@ -181,6 +183,14 @@ def test_image_serialize(self, image: Image):
dumped["image_coordinate_system"]["rotation"]
== image.image_coordinate_system.rotation
)
if image.image_coordinate_system.z_offset is None:
assert dumped["image_coordinate_system"]["z_offset"] is None
else:
assert (
dumped["image_coordinate_system"]["z_offset"]
== image.image_coordinate_system.z_offset
)

if image.pixel_spacing is None:
assert dumped["pixel_spacing"] is None
else:
Expand Down Expand Up @@ -216,6 +226,7 @@ def test_image_deserialize(self):
"image_coordinate_system": {
"origin": {"x": 20.0, "y": 30.0},
"rotation": 90.0,
"z_offset": 1.0,
},
"pixel_spacing": {"width": 0.01, "height": 0.01},
"focal_plane_spacing": 0.001,
Expand Down Expand Up @@ -261,6 +272,10 @@ def test_image_deserialize(self):
loaded.image_coordinate_system.rotation
== dumped["image_coordinate_system"]["rotation"]
)
assert (
loaded.image_coordinate_system.z_offset
== dumped["image_coordinate_system"]["z_offset"]
)
assert loaded.pixel_spacing.width == dumped["pixel_spacing"]["width"]
assert loaded.pixel_spacing.height == dumped["pixel_spacing"]["height"]
assert loaded.focal_plane_spacing == dumped["focal_plane_spacing"]
Expand Down Expand Up @@ -709,3 +724,23 @@ def test_study_deserialize(self):
assert loaded.time == time.fromisoformat(dumped["time"])
assert loaded.accession_number == dumped["accession_number"]
assert loaded.referring_physician_name == dumped["referring_physician_name"]

def test_metadata_serialize(
self,
wsi_metadata: WsiMetadata,
):
# Arrange

# Act
dumped = WsiMetadataJsonSchema().dump(wsi_metadata)

# Assert
assert isinstance(dumped, dict)
assert "study" in dumped
assert "series" in dumped
assert "patient" in dumped
assert "equipment" in dumped
assert "optical_paths" in dumped
assert "slide" in dumped
assert "label" in dumped
assert "image" in dumped
2 changes: 1 addition & 1 deletion wsidicom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
from wsidicom.web import WsiDicomWebClient
from wsidicom.wsidicom import WsiDicom

__version__ = "0.21.2"
__version__ = "0.21.3"

__all__ = [
"settings",
Expand Down
Loading

0 comments on commit 08045d4

Please sign in to comment.