Skip to content

Commit

Permalink
Add support for JPEG-LS encoding/decoding (#142)
Browse files Browse the repository at this point in the history
* Add support for JPEG-LS encoding/decoding

* Allow JPEG-LS encoding for SEG, PM, and SC images
  • Loading branch information
hackermd authored Jan 3, 2022
1 parent 5064694 commit 6db292f
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 35 deletions.
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
96 changes: 77 additions & 19 deletions src/highdicom/legacy/sop.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@

from pydicom.datadict import tag_for_keyword
from pydicom.dataset import Dataset
from pydicom.encaps import encapsulate
from pydicom.uid import (
ImplicitVRLittleEndian,
ExplicitVRLittleEndian,
JPEG2000Lossless,
JPEGLSLossless,
)

from highdicom.base import SOPClass
from highdicom.frame import encode_frame
from highdicom._iods import IOD_MODULE_MAP, SOP_CLASS_UID_IOD_KEY_MAP
from highdicom._modules import MODULE_ATTRIBUTE_MAP

Expand All @@ -26,9 +34,9 @@


def _convert_legacy_to_enhanced(
sf_datasets: Sequence[Dataset],
mf_dataset: Optional[Dataset] = None
) -> Dataset:
sf_datasets: Sequence[Dataset],
mf_dataset: Optional[Dataset] = None
) -> Dataset:
"""Converts one or more MR, CT or PET Image instances into one
Legacy Converted Enhanced MR/CT/PET Image instance by copying information
from `sf_datasets` into `mf_dataset`.
Expand Down Expand Up @@ -383,15 +391,25 @@ def _convert_legacy_to_enhanced(

mf_dataset.AcquisitionContextSequence = []

# TODO: Encapsulated Pixel Data with compressed frame items.

# Create the Pixel Data element of the mulit-frame image instance using
# native encoding (simply concatenating pixels of individual frames)
# Sometimes there may be numpy types such as ">i2". The (* 1) hack
# ensures that pixel values have the correct integer type.
mf_dataset.PixelData = b''.join([
(ds.pixel_array * 1).data for ds in sf_datasets
])
encoded_frames = [
encode_frame(
ds.pixel_array * 1,
transfer_syntax_uid=mf_dataset.file_meta.TransferSyntaxUID,
bits_allocated=ds.BitsAllocated,
bits_stored=ds.BitsStored,
photometric_interpretation=ds.PhotometricInterpretation,
pixel_representation=ds.PixelRepresentation
)
for ds in sf_datasets
]
if mf_dataset.file_meta.TransferSyntaxUID.is_encapsulated:
mf_dataset.PixelData = encapsulate(encoded_frames)
else:
mf_dataset.PixelData = b''.join(encoded_frames)

return mf_dataset

Expand All @@ -407,6 +425,7 @@ def __init__(
series_number: int,
sop_instance_uid: str,
instance_number: int,
transfer_syntax_uid: str = ExplicitVRLittleEndian,
**kwargs: Any
) -> None:
"""
Expand All @@ -423,6 +442,11 @@ def __init__(
UID that should be assigned to the instance
instance_number: int
Number that should be assigned to the instance
transfer_syntax_uid: str, optional
UID of transfer syntax that should be used for encoding of
data elements. The following compressed transfer syntaxes
are supported: JPEG 2000 Lossless (``"1.2.840.10008.1.2.4.90"``)
and JPEG-LS Lossless (``"1.2.840.10008.1.2.4.80"``).
**kwargs: Any, optional
Additional keyword arguments that will be passed to the constructor
of `highdicom.base.SOPClass`
Expand All @@ -445,6 +469,17 @@ def __init__(

sop_class_uid = LEGACY_ENHANCED_SOP_CLASS_UID_MAP[ref_ds.SOPClassUID]

supported_transfer_syntaxes = {
ImplicitVRLittleEndian,
ExplicitVRLittleEndian,
JPEG2000Lossless,
JPEGLSLossless,
}
if transfer_syntax_uid not in supported_transfer_syntaxes:
raise ValueError(
f'Transfer syntax "{transfer_syntax_uid}" is not supported'
)

super().__init__(
study_instance_uid=ref_ds.StudyInstanceUID,
series_instance_uid=series_instance_uid,
Expand All @@ -454,7 +489,7 @@ def __init__(
instance_number=instance_number,
manufacturer=ref_ds.Manufacturer,
modality=ref_ds.Modality,
transfer_syntax_uid=None, # FIXME: frame encoding
transfer_syntax_uid=transfer_syntax_uid,
patient_id=ref_ds.PatientID,
patient_name=ref_ds.PatientName,
patient_birth_date=ref_ds.PatientBirthDate,
Expand Down Expand Up @@ -483,6 +518,7 @@ def __init__(
series_number: int,
sop_instance_uid: str,
instance_number: int,
transfer_syntax_uid: str = ExplicitVRLittleEndian,
**kwargs: Any
) -> None:
"""
Expand All @@ -499,6 +535,11 @@ def __init__(
UID that should be assigned to the instance
instance_number: int
Number that should be assigned to the instance
transfer_syntax_uid: str, optional
UID of transfer syntax that should be used for encoding of
data elements. The following compressed transfer syntaxes
are supported: JPEG 2000 Lossless (``"1.2.840.10008.1.2.4.90"``)
and JPEG-LS Lossless (``"1.2.840.10008.1.2.4.80"``).
**kwargs: Any, optional
Additional keyword arguments that will be passed to the constructor
of `highdicom.base.SOPClass`
Expand Down Expand Up @@ -530,7 +571,7 @@ def __init__(
instance_number=instance_number,
manufacturer=ref_ds.Manufacturer,
modality=ref_ds.Modality,
transfer_syntax_uid=None, # FIXME: frame encoding
transfer_syntax_uid=transfer_syntax_uid,
patient_id=ref_ds.PatientID,
patient_name=ref_ds.PatientName,
patient_birth_date=ref_ds.PatientBirthDate,
Expand All @@ -550,14 +591,15 @@ class LegacyConvertedEnhancedPETImage(SOPClass):
"""SOP class for Legacy Converted Enhanced PET Image instances."""

def __init__(
self,
legacy_datasets: Sequence[Dataset],
series_instance_uid: str,
series_number: int,
sop_instance_uid: str,
instance_number: int,
**kwargs: Any
) -> None:
self,
legacy_datasets: Sequence[Dataset],
series_instance_uid: str,
series_number: int,
sop_instance_uid: str,
instance_number: int,
transfer_syntax_uid: str = ExplicitVRLittleEndian,
**kwargs: Any
) -> None:
"""
Parameters
----------
Expand All @@ -572,6 +614,11 @@ def __init__(
UID that should be assigned to the instance
instance_number: int
Number that should be assigned to the instance
transfer_syntax_uid: str, optional
UID of transfer syntax that should be used for encoding of
data elements. The following compressed transfer syntaxes
are supported: JPEG 2000 Lossless (``"1.2.840.10008.1.2.4.90"``)
and JPEG-LS Lossless (``"1.2.840.10008.1.2.4.80"``).
**kwargs: Any, optional
Additional keyword arguments that will be passed to the constructor
of `highdicom.base.SOPClass`
Expand All @@ -594,6 +641,17 @@ def __init__(

sop_class_uid = LEGACY_ENHANCED_SOP_CLASS_UID_MAP[ref_ds.SOPClassUID]

supported_transfer_syntaxes = {
ImplicitVRLittleEndian,
ExplicitVRLittleEndian,
JPEG2000Lossless,
JPEGLSLossless,
}
if transfer_syntax_uid not in supported_transfer_syntaxes:
raise ValueError(
f'Transfer syntax "{transfer_syntax_uid}" is not supported'
)

super().__init__(
study_instance_uid=ref_ds.StudyInstanceUID,
series_instance_uid=series_instance_uid,
Expand All @@ -603,7 +661,7 @@ def __init__(
instance_number=instance_number,
manufacturer=ref_ds.Manufacturer,
modality=ref_ds.Modality,
transfer_syntax_uid=None, # FIXME: frame encoding
transfer_syntax_uid=transfer_syntax_uid,
patient_id=ref_ds.PatientID,
patient_name=ref_ds.PatientName,
patient_birth_date=ref_ds.PatientBirthDate,
Expand Down
8 changes: 4 additions & 4 deletions src/highdicom/pm/sop.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
ExplicitVRLittleEndian,
ImplicitVRLittleEndian,
JPEG2000Lossless,
JPEGLSLossless,
RLELossless,
)
from pydicom.valuerep import format_number_as_ds
Expand Down Expand Up @@ -175,8 +176,8 @@ def __init__(
stored values to be displayed on 8-bit monitors.
transfer_syntax_uid: Union[str, None], optional
UID of transfer syntax that should be used for encoding of
data elements. Defaults to Implicit VR Little Endian
(UID ``"1.2.840.10008.1.2"``)
data elements. Defaults to Explicit VR Little Endian
(UID ``"1.2.840.10008.1.2.1"``)
content_description: Union[str, None], optional
Brief description of the parametric map image
content_creator_name: Union[str, None], optional
Expand Down Expand Up @@ -275,11 +276,10 @@ def __init__(
# If pixel data has unsigned or signed integer data type, then it
# can be lossless compressed. The standard does not specify any
# compression codecs for floating-point data types.
# In case of signed integer data type, values will be rescaled to
# a signed integer range prior to compression.
supported_transfer_syntaxes.update(
{
JPEG2000Lossless,
JPEGLSLossless,
RLELossless,
}
)
Expand Down
7 changes: 5 additions & 2 deletions src/highdicom/sc/sop.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
RLELossless,
JPEGBaseline8Bit,
JPEG2000Lossless,
JPEGLSLossless,
)

from highdicom.base import SOPClass
Expand Down Expand Up @@ -95,7 +96,7 @@ def __init__(
specimen_descriptions: Optional[
Sequence[SpecimenDescription]
] = None,
transfer_syntax_uid: str = ImplicitVRLittleEndian,
transfer_syntax_uid: str = ExplicitVRLittleEndian,
**kwargs: Any
):
"""
Expand Down Expand Up @@ -172,7 +173,8 @@ def __init__(
UID of transfer syntax that should be used for encoding of
data elements. The following compressed transfer syntaxes
are supported: RLE Lossless (``"1.2.840.10008.1.2.5"``), JPEG
2000 Lossless (``"1.2.840.10008.1.2.4.90"``), JPEG Baseline
2000 Lossless (``"1.2.840.10008.1.2.4.90"``), JPEG-LS Lossless
(``"1.2.840.10008.1.2.4.80"``), and JPEG Baseline
(``"1.2.840.10008.1.2.4.50"``). Note that JPEG Baseline is a
lossy compression method that will lead to a loss of detail in
the image.
Expand All @@ -187,6 +189,7 @@ def __init__(
RLELossless,
JPEGBaseline8Bit,
JPEG2000Lossless,
JPEGLSLossless,
}
if transfer_syntax_uid not in supported_transfer_syntaxes:
raise ValueError(
Expand Down
Loading

0 comments on commit 6db292f

Please sign in to comment.