Skip to content

Commit

Permalink
Fix lossy (#185)
Browse files Browse the repository at this point in the history
* Add property transcoder to indicate if image data forces transcoding
  • Loading branch information
erikogabrielsson authored Jan 30, 2025
1 parent a723baa commit 84eab7a
Show file tree
Hide file tree
Showing 6 changed files with 38 additions and 14 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- Setting of `LossyImageCompression`, `LossyImageCompressionRatio`, and `LossyImageCompressionMethod` when image data requires transcoding.

## [0.23.0] - 2025-01-29

### Added
Expand Down
6 changes: 5 additions & 1 deletion tests/file/io/test_wsidicom_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
generate_uid,
)

from wsidicom.codec import LossyCompressionIsoStandard
from wsidicom.codec import Encoder, LossyCompressionIsoStandard
from wsidicom.file.io import (
OffsetTableType,
WsiDicomIO,
Expand Down Expand Up @@ -155,6 +155,10 @@ def lossy_compression(
) -> Optional[List[Tuple[LossyCompressionIsoStandard, float]]]:
return None

@property
def transcoder(self) -> Optional[Encoder]:
return None

@property
def thread_safe(self) -> bool:
return True
Expand Down
21 changes: 13 additions & 8 deletions wsidicom/file/wsidicom_file_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,29 +219,34 @@ def _save_group(
focal_planes, optical_paths, tiled_size, scale
)
if self._transcoder is not None:
transcoder = self._transcoder
elif instances[0].image_data.transcoder is not None:
transcoder = instances[0].image_data.transcoder
else:
transcoder = None
if transcoder is not None:
if (
self._transcoder.bits != instances[0].image_data.bits
or self._transcoder.samples_per_pixel
transcoder.bits != instances[0].image_data.bits
or transcoder.samples_per_pixel
!= instances[0].image_data.samples_per_pixel
):
raise ValueError(
"Transcode settings must match image data bits and "
"photometric interpretation."
)
transfer_syntax = self._transcoder.transfer_syntax
transfer_syntax = transcoder.transfer_syntax
dataset.PhotometricInterpretation = (
self._transcoder.photometric_interpretation
transcoder.photometric_interpretation
)
if self._transcoder.lossy_method:
if transcoder.lossy_method:
dataset.LossyImageCompression = "01"
ratios = dataset.get_multi_value(LossyImageCompressionRatioTag)
# Reserve space for new ratio
ratios.append(" " * MAX_VALUE_LEN["DS"])
methods = dataset.get_multi_value(LossyImageCompressionMethodTag)
methods.append(self._transcoder.lossy_method.value)
methods.append(transcoder.lossy_method.value)
dataset.LossyImageCompressionRatio = ratios
dataset.LossyImageCompressionMethod = methods

else:
transfer_syntax = instances[0].image_data.transfer_syntax
if self._offset_table is not None:
Expand All @@ -261,7 +266,7 @@ def _save_group(
self._chunk_size,
self._instance_number,
scale,
self._transcoder,
transcoder,
)
filepaths.append(filepath)
self._instance_number += 1
Expand Down
7 changes: 7 additions & 0 deletions wsidicom/instance/image_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ def lossy_compression(
ratio is unknown."""
raise NotImplementedError()

@property
@abstractmethod
def transcoder(self) -> Optional[Encoder]:
"""Return transcoder used for image data if image data can't read encoded data
directly. Return None if no transcoder is needed."""
raise NotImplementedError()

@abstractmethod
def _get_decoded_tile(self, tile_point: Point, z: float, path: str) -> Image:
"""
Expand Down
4 changes: 4 additions & 0 deletions wsidicom/instance/pillow_image_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ def lossy_compression(
compressed_size = len(self._get_encoded_tile(Point(0, 0), 0, ""))
return [(iso, uncompressed_size / compressed_size)]

@property
def transcoder(self) -> Optional[Encoder]:
return None

def _get_decoded_tile(self, tile_point: Point, z: float, path: str) -> Image:
if tile_point != Point(0, 0):
raise ValueError("Can only get Point(0, 0) from non-tiled image.")
Expand Down
10 changes: 5 additions & 5 deletions wsidicom/instance/wsidicom_image_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
from PIL.Image import Image

from wsidicom.cache import DecodedFrameCache, EncodedFrameCache
from wsidicom.codec import Codec, Decoder, LossyCompressionIsoStandard
from wsidicom.codec import Codec, Decoder, Encoder, LossyCompressionIsoStandard
from wsidicom.errors import WsiDicomOutOfBoundsError
from wsidicom.geometry import Point, Region, Size, SizeMm
from wsidicom.instance.dataset import TileType, WsiDataset
Expand Down Expand Up @@ -116,10 +116,6 @@ def samples_per_pixel(self) -> int:
"""Return samples per pixel (1 or 3)."""
return self._datasets[0].samples_per_pixel

# @property
# def lossy_compressed(self) -> bool:
# return self._datasets[0].lossy_compressed

@cached_property
def image_coordinate_system(self) -> Optional[ImageCoordinateSystem]:
"""Return the image origin of the image data."""
Expand Down Expand Up @@ -153,6 +149,10 @@ def lossy_compression(
]
return list(zip(methods, ratios))

@property
def transcoder(self) -> Optional[Encoder]:
return None

@property
def decoder(self) -> Decoder:
return self._decoder
Expand Down

0 comments on commit 84eab7a

Please sign in to comment.