Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
install_requires=[
'pydicom>=2.2.2',
'numpy>=1.19',
'pillow>=8.3'
'pillow>=8.3',
'pillow-jpls>=1.0',
'pylibjpeg>=1.3',
'pylibjpeg-libjpeg>=1.2',
'pylibjpeg-openjpeg>=1.1',
],
)
61 changes: 59 additions & 2 deletions src/highdicom/frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from typing import Optional, Union

import numpy as np
import pillow_jpls # noqa
from PIL import Image
from pydicom.dataset import Dataset, FileMetaDataset
from pydicom.encaps import encapsulate
Expand All @@ -13,6 +14,7 @@
ImplicitVRLittleEndian,
JPEG2000Lossless,
JPEGBaseline8Bit,
JPEGLSLossless,
UID,
RLELossless,
)
Expand All @@ -23,7 +25,6 @@
PlanarConfigurationValues,
)


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -101,6 +102,7 @@ def encode_frame(
compressed_transfer_syntaxes = {
JPEGBaseline8Bit,
JPEG2000Lossless,
JPEGLSLossless,
RLELossless,
}
supported_transfer_syntaxes = uncompressed_transfer_syntaxes.union(
Expand All @@ -114,6 +116,12 @@ def encode_frame(
)
)
if transfer_syntax_uid in uncompressed_transfer_syntaxes:
if samples_per_pixel > 1:
if planar_configuration != 0:
raise ValueError(
'Planar configuration must be 0 for color image frames '
'with native encoding.'
)
if bits_allocated == 1:
if (rows * cols * samples_per_pixel) % 8 != 0:
raise ValueError(
Expand All @@ -140,6 +148,12 @@ def encode_frame(
'irreversible': False,
},
),
JPEGLSLossless: (
'JPEG-LS',
{
'near_lossless': 0,
}
)
}

if transfer_syntax_uid == JPEGBaseline8Bit:
Expand Down Expand Up @@ -185,7 +199,7 @@ def encode_frame(
'encoding of image frames with JPEG Baseline codec.'
)

if transfer_syntax_uid == JPEG2000Lossless:
elif transfer_syntax_uid == JPEG2000Lossless:
if samples_per_pixel == 1:
if planar_configuration is not None:
raise ValueError(
Expand Down Expand Up @@ -223,6 +237,49 @@ def encode_frame(
'encoding of image frames with Lossless JPEG2000 codec.'
)

elif transfer_syntax_uid == JPEGLSLossless:
if samples_per_pixel == 1:
if planar_configuration is not None:
raise ValueError(
'Planar configuration must be absent for encoding of '
'monochrome image frames with Lossless JPEG-LS codec.'
)
if photometric_interpretation not in (
'MONOCHROME1', 'MONOCHROME2'
):
raise ValueError(
'Photometric intpretation must be either "MONOCHROME1" '
'or "MONOCHROME2" for encoding of monochrome image '
'frames with Lossless JPEG-LS codec.'
)
elif samples_per_pixel == 3:
if photometric_interpretation != 'YBR_FULL':
raise ValueError(
'Photometric interpretation must be "YBR_FULL" for '
'encoding of color image frames with '
'Lossless JPEG-LS codec.'
)
if planar_configuration != 0:
raise ValueError(
'Planar configuration must be 0 for encoding of '
'color image frames with Lossless JPEG-LS codec.'
)
if bits_allocated != 8:
raise ValueError(
'Bits Allocated must be 8 for encoding of '
'color image frames with Lossless JPEG-LS codec.'
)
else:
raise ValueError(
'Samples per pixel must be 1 or 3 for '
'encoding of image frames with Lossless JPEG-LS codec.'
)
if pixel_representation != 0:
raise ValueError(
'Pixel representation must be 0 for '
'encoding of image frames with Lossless JPEG-LS codec.'
)

if transfer_syntax_uid in compression_lut.keys():
image_format, kwargs = compression_lut[transfer_syntax_uid]
if samples_per_pixel == 3:
Expand Down
61 changes: 59 additions & 2 deletions tests/test_frame.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import pytest
from pydicom.uid import (
JPEG2000Lossless,
JPEGLSLossless,
JPEGBaseline8Bit,
)

Expand Down Expand Up @@ -148,7 +149,7 @@ def test_jpeg2000_monochrome(self):
transfer_syntax_uid=JPEG2000Lossless,
bits_allocated=bits_allocated,
bits_stored=bits_allocated,
photometric_interpretation='MONOCHROME1',
photometric_interpretation='MONOCHROME2',
pixel_representation=0,
)
assert compressed_frame.startswith(b'\x00\x00\x00\x0C\x6A\x50\x20')
Expand All @@ -161,7 +162,63 @@ def test_jpeg2000_monochrome(self):
samples_per_pixel=1,
bits_allocated=bits_allocated,
bits_stored=bits_allocated,
photometric_interpretation='MONOCHROME1',
photometric_interpretation='MONOCHROME2',
pixel_representation=0,
planar_configuration=0
)
np.testing.assert_array_equal(frame, decoded_frame)

def test_jpegls_rgb(self):
bits_allocated = 8
frame = np.ones((16, 32, 3), dtype=np.dtype(f'uint{bits_allocated}'))
frame *= 255
compressed_frame = encode_frame(
frame,
transfer_syntax_uid=JPEGLSLossless,
bits_allocated=bits_allocated,
bits_stored=bits_allocated,
photometric_interpretation='YBR_FULL',
pixel_representation=0,
planar_configuration=0
)
assert compressed_frame.startswith(b'\xFF\xD8')
assert compressed_frame.endswith(b'\xFF\xD9')
decoded_frame = decode_frame(
value=compressed_frame,
transfer_syntax_uid=JPEGLSLossless,
rows=frame.shape[0],
columns=frame.shape[1],
samples_per_pixel=frame.shape[2],
bits_allocated=bits_allocated,
bits_stored=bits_allocated,
photometric_interpretation='YBR_FULL',
pixel_representation=0,
planar_configuration=0
)
np.testing.assert_array_equal(frame, decoded_frame)

def test_jpegls_monochrome(self):
bits_allocated = 16
frame = np.zeros((16, 32), dtype=np.dtype(f'uint{bits_allocated}'))
compressed_frame = encode_frame(
frame,
transfer_syntax_uid=JPEGLSLossless,
bits_allocated=bits_allocated,
bits_stored=bits_allocated,
photometric_interpretation='MONOCHROME2',
pixel_representation=0,
)
assert compressed_frame.startswith(b'\xFF\xD8')
assert compressed_frame.endswith(b'\xFF\xD9')
decoded_frame = decode_frame(
value=compressed_frame,
transfer_syntax_uid=JPEG2000Lossless,
rows=frame.shape[0],
columns=frame.shape[1],
samples_per_pixel=1,
bits_allocated=bits_allocated,
bits_stored=bits_allocated,
photometric_interpretation='MONOCHROME2',
pixel_representation=0,
planar_configuration=0
)
Expand Down