Skip to content

Commit

Permalink
Fix handling of pyramid index when creating instance (#161)
Browse files Browse the repository at this point in the history
* Fix handling of pyramid index when creating instance
  • Loading branch information
erikogabrielsson authored Mar 20, 2024
1 parent ec4fd2b commit 46fae89
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 75 deletions.
11 changes: 9 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.20.2] - 2024-02-22
## [0.20.3] - 2024-03-20

### Fixed

- Missing handling of pyramid index when creating `WsiInstance` using `create_instance()`.

## [0.20.2] - 2024-03-18

### Fixed

Expand Down Expand Up @@ -353,7 +359,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.20.2..HEAD
[Unreleased]: https://github.com/imi-bigpicture/wsidicom/compare/0.20.3..HEAD
[0.20.3]: https://github.com/imi-bigpicture/wsidicom/compare/v0.20.2..v0.20.3
[0.20.2]: https://github.com/imi-bigpicture/wsidicom/compare/v0.20.1..v0.20.2
[0.20.1]: https://github.com/imi-bigpicture/wsidicom/compare/v0.20.0..v0.20.1
[0.20.0]: https://github.com/imi-bigpicture/wsidicom/compare/v0.19.1..v0.20.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.20.2"
version = "0.20.3"
description = "Tools for handling DICOM based whole scan images"
authors = ["Erik O Gabrielsson <[email protected]>"]
license = "Apache-2.0"
Expand Down
71 changes: 70 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,23 @@

import json
import os
import random
import sys
from enum import Enum
from io import BufferedReader
from pathlib import Path
from typing import Any, Dict, Iterable, List, Optional, Tuple

import pytest
from dicomweb_client import DICOMfileClient
from pydicom.uid import JPEGBaseline8Bit
from pydicom.uid import UID, JPEGBaseline8Bit
from upath import UPath

from tests.data_gen import create_layer_file
from tests.file.io.test_wsidicom_writer import WsiDicomTestImageData
from wsidicom import WsiDicom
from wsidicom.config import settings
from wsidicom.geometry import Size
from wsidicom.web.wsidicom_web_client import WsiDicomWebClient

SLIDE_FOLDER = Path(os.environ.get("WSIDICOM_TESTDIR", "tests/testdata/slides"))
Expand Down Expand Up @@ -195,3 +199,68 @@ def open_wsi(
wsi.close()
for stream in streams:
stream.close()


@pytest.fixture()
def tiled_size():
yield Size(2, 2)


@pytest.fixture()
def frame_count(tiled_size: Size):
yield tiled_size.area


@pytest.fixture()
def rng():
SEED = 0
yield random.Random(SEED)


@pytest.fixture()
def bits():
yield 8


@pytest.fixture
def samples_per_pixel():
yield 3


@pytest.fixture
def tile_size():
yield Size(10, 10)


@pytest.fixture()
def transfer_syntax():
yield JPEGBaseline8Bit


@pytest.fixture()
def frames(
rng: random.Random,
transfer_syntax: UID,
frame_count: int,
bits: int,
samples_per_pixel: int,
tile_size: Size,
):
if not transfer_syntax.is_encapsulated:
min_frame_length = bits * tile_size.area * samples_per_pixel // 8
max_frame_length = min_frame_length
else:
min_frame_length = 2
max_frame_length = 100
lengths = [
rng.randint(min_frame_length, max_frame_length) for i in range(frame_count)
]
yield [
rng.getrandbits(length * 8).to_bytes(length, sys.byteorder)
for length in lengths
]


@pytest.fixture()
def image_data(frames: List[bytes], tiled_size: Size):
yield WsiDicomTestImageData(frames, tiled_size)
62 changes: 0 additions & 62 deletions tests/file/io/test_wsidicom_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@

import math
import os
import random
import sys
from pathlib import Path
from typing import List, Optional, OrderedDict, Sequence

Expand Down Expand Up @@ -155,66 +153,6 @@ def _get_encoded_tile(self, tile: Point, z: float, path: str) -> bytes:
return self._data[tile.x + tile.y * self.tiled_size.width]


@pytest.fixture()
def tiled_size():
yield Size(2, 2)


@pytest.fixture()
def frame_count(tiled_size: Size):
yield tiled_size.area


@pytest.fixture()
def rng():
SEED = 0
yield random.Random(SEED)


@pytest.fixture()
def bits():
yield 8


@pytest.fixture
def samples_per_pixel():
yield 3


@pytest.fixture
def tile_size():
yield Size(10, 10)


@pytest.fixture()
def frames(
rng: random.Random,
transfer_syntax: UID,
frame_count: int,
bits: int,
samples_per_pixel: int,
tile_size: Size,
):
if not transfer_syntax.is_encapsulated:
min_frame_length = bits * tile_size.area * samples_per_pixel // 8
max_frame_length = min_frame_length
else:
min_frame_length = 2
max_frame_length = 100
lengths = [
rng.randint(min_frame_length, max_frame_length) for i in range(frame_count)
]
yield [
rng.getrandbits(length * 8).to_bytes(length, sys.byteorder)
for length in lengths
]


@pytest.fixture()
def image_data(frames: List[bytes], tiled_size: Size):
yield WsiDicomTestImageData(frames, tiled_size)


@pytest.fixture()
def dataset(image_data: ImageData, frame_count: int):
assert image_data.pixel_spacing is not None
Expand Down
31 changes: 29 additions & 2 deletions tests/test_wsi_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
from pydicom.uid import UID, generate_uid

from wsidicom.geometry import SizeMm
from wsidicom.instance import TileType
from wsidicom.instance.dataset import WsiDataset
from wsidicom.instance import ImageData, TileType
from wsidicom.instance.dataset import ImageType, WsiDataset
from wsidicom.tags import LossyImageCompressionRatioTag


Expand Down Expand Up @@ -265,3 +265,30 @@ def test_spacing_between_slices(

# Assert
assert read_spacing_between_slices == spacing_between_slices

@pytest.mark.parametrize(
["image_type", "pyramid_index", "expected_image_type"],
[
(ImageType.VOLUME, 0, ["ORIGINAL", "PRIMARY", "VOLUME", "NONE"]),
(ImageType.VOLUME, 1, ["ORIGINAL", "PRIMARY", "VOLUME", "RESAMPLED"]),
(ImageType.LABEL, None, ["ORIGINAL", "PRIMARY", "LABEL", "NONE"]),
(ImageType.OVERVIEW, None, ["ORIGINAL", "PRIMARY", "OVERVIEW", "NONE"]),
],
)
def test_create_instance_dataset(
self,
image_data: ImageData,
image_type: ImageType,
pyramid_index: Optional[int],
expected_image_type: Sequence[str],
):
# Arrange
dataset = Dataset()

# Act
instance_dataset = WsiDataset.create_instance_dataset(
dataset, image_type, image_data, pyramid_index
)

# Assert
assert instance_dataset.ImageType == expected_image_type
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.20.2"
__version__ = "0.20.3"

__all__ = [
"settings",
Expand Down
13 changes: 9 additions & 4 deletions wsidicom/instance/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -740,16 +740,21 @@ def create_instance_dataset(
Type of instance ('VOLUME', 'LABEL', 'OVERVIEW)
image_data:
Image data to create dataset for.
pyramid_index: Optional[int] = None
Pyramid index. of image data, if volume image.
Returns
-------
WsiDataset
Dataset for instance.
"""
if image_type == ImageType.VOLUME and pyramid_index == 0:
resampled = "NONE"
else:
resampled = "RESAMPLED"
resampled = "NONE"
if image_type == ImageType.VOLUME:
if pyramid_index is None:
raise ValueError("Pyramid index must be set for volume image.")
if pyramid_index > 0:
resampled = "RESAMPLED"

dataset.ImageType = ["ORIGINAL", "PRIMARY", image_type.value, resampled]
dataset.SOPInstanceUID = generate_uid(prefix=None)
shared_functional_group_sequence = Dataset()
Expand Down
10 changes: 8 additions & 2 deletions wsidicom/instance/instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,11 @@ def create_label(

@classmethod
def create_instance(
cls, image_data: ImageData, base_dataset: Dataset, image_type: ImageType
cls,
image_data: ImageData,
base_dataset: Dataset,
image_type: ImageType,
pyramid_index: Optional[int] = None,
) -> "WsiInstance":
"""Create WsiInstance from ImageData.
Expand All @@ -221,14 +225,16 @@ def create_instance(
Base dataset to include.
image_type: ImageType
Type of instance to create.
pyramid_index: Optional[int] = None
Pyramid index. of image data, if volume image.
Returns
-------
WsiInstance
Created WsiInstance.
"""
instance_dataset = WsiDataset.create_instance_dataset(
base_dataset, image_type, image_data
base_dataset, image_type, image_data, pyramid_index
)

return cls(instance_dataset, image_data)
Expand Down

0 comments on commit 46fae89

Please sign in to comment.