Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
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,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a though, should the default behaviour be to copy the transfer syntax from the input images, and not re-encode the pixels?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I generally agree.

However, this may be a good opportunity to avoid some of the exotic lossless JEPG transfer syntaxes that are not widely supported and thereby facilitate downstream decoding. Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting that if no transfer syntax is explicitly specified, we selectively re-encode some lossless codecs into a more commonly-supported format?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, or just not compress at all by default (which has been the behavior so far)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If any of the source images was lossy compressed, we also shouldn't re-compress the enhanced image with a lossy transfer syntax

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree completely - if the images have been lossy compressed a different compression algorithm can create artifacts; even the same lossy compression technique with different parameters can create a mess. If the original compressed bytes aren't passed through then a lossless algorithm is the only safe mechanism.

I think there is a DICOM field which designates whether the image has been lossy compressed or not. If the images are transmitted decompressed this field should be set to true so that downstream the consumer knows that the images are not the original image data.

I don't know if the FDA wants the lossy compression ratio put into the image or not - there was a big discussion of this 20 years ago but I haven't been following it since then.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, the main thing to avoid would be to decompress lossy data and then recompress losslessly (or store with no compression, as is currently the default) thus giving the impression of lossless compression when in fact information has been lost. That's why I'm suggesting simply leaving the transfer syntax and compression alone by default in those situations unless the user specifically requests otherwise. However I don't see a harm in silently translating between different lossless formats, although it is a bit magical

Copy link
Collaborator Author

@hackermd hackermd Jan 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The user can currently not set a lossy transfer syntax. If the pixel data was lossy compressed in the first place, the attribute LossyImageCompression should already reflect that (and should stay unchanged even if pixel data is subsequently encoded using a lossless transfer syntax).

The behaviour so far has been to decode the pixel data and re-encode it uncompressed. We can (and probably should) change that behavior upon refactoring of the legacy conversion package as part of #34

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