Skip to content

Commit

Permalink
Check for JFIF and component IDs when determining decoded colour space (
Browse files Browse the repository at this point in the history
  • Loading branch information
scaramallion committed Mar 31, 2024
1 parent cf472d3 commit 6865c2b
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 3 deletions.
44 changes: 44 additions & 0 deletions src/pydicom/pixels/decoders/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
UID,
JPEG2000TransferSyntaxes,
JPEGLSTransferSyntaxes,
JPEGTransferSyntaxes,
)


Expand Down Expand Up @@ -211,6 +212,45 @@ def __init__(self, tsyntax: UID) -> None:
elif self.transfer_syntax in JPEGLSTransferSyntaxes:
self.set_option("apply_jls_sign_correction", True)

def _conform_jpg_colorspace(self, info: dict[str, Any]) -> None:
"""Conform the photometric interpretation to the JPEG/JPEG-LS codestream.
Parameters
----------
info : dict[str, Any]
A dictionary containing JPEG/JPEG-LS codestream metadata.
"""
if self.samples_per_pixel != 3:
return

pi = self.photometric_interpretation

# Check the component IDs for RGB or rgb (in ASCII)
has_rgb_ids = info.get("component_ids", None) in ([82, 71, 66], [114, 103, 98])
if has_rgb_ids and pi != PI.RGB:
self.set_option("photometric_interpretation", PI.RGB)
warn_and_log(
f"The (0028,0004) 'Photometric Interpretation' value is '{pi}' "
"however the encoded image's codestream uses component IDs that "
"indicate it should be 'RGB'"
)
return

# A JFIF APP marker means the decoded image should be YBR colour space
# https://www.w3.org/Graphics/JPEG/jfif.pdf
cs = (
PI.YBR_FULL_422 if self.transfer_syntax == JPEGBaseline8Bit else PI.YBR_FULL
)
for marker in info.get("app", {}).values():
if marker.startswith(b"JFIF") and "YBR" not in pi:
self.set_option("photometric_interpretation", cs)
warn_and_log(
"The (0028,0004) 'Photometric Interpretation' value is "
f"'{pi}' however the encoded image's codestream contains a "
f"JFIF APP marker which indicates it should be '{cs}'"
)
return

def decode(self, index: int) -> bytes | bytearray:
"""Decode the frame of pixel data at `index`.
Expand Down Expand Up @@ -261,6 +301,10 @@ def _decode_frame(self, src: bytes) -> bytes | bytearray:
self.set_option(
"jls_precision", jls_info.get("precision", self.bits_stored)
)
self._conform_jpg_colorspace(jls_info)
elif self.transfer_syntax in JPEGTransferSyntaxes:
jpg_info = _get_jpg_parameters(src)
self._conform_jpg_colorspace(jpg_info)

# If self._previous is not set then this is the first frame being decoded
# If self._previous is set, then the previously successful decoder
Expand Down
2 changes: 2 additions & 0 deletions tests/pixels/pixels_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -1305,6 +1305,7 @@ def test(ref, arr, **kwargs):

# tsyntax, (bits allocated, stored), (frames, rows, cols, planes), VR, PI, pixel repr.
# 0: JPGB, (8, 8), (1, 3, 3, 3), OB, YBR_FULL, 0
# Uses a JFIF APP marker
def test(ref, arr, **kwargs):
# Pillow, pylibjpeg
assert tuple(arr[0, 0, :]) == (138, 78, 147)
Expand Down Expand Up @@ -1636,6 +1637,7 @@ def test(ref, arr, **kwargs):


# JPGB, (8, 8), (1, 100, 100, 3), OB, RGB, 0
# Uses RGB component IDs
def test(ref, arr, **kwargs):
assert tuple(arr[5, 50, :]) == (255, 0, 0)
assert tuple(arr[15, 50, :]) == (255, 128, 128)
Expand Down
40 changes: 40 additions & 0 deletions tests/pixels/test_decoder_gdcm.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
J2KR_16_13_1_1_1F_M2_MISMATCH,
JLSN_08_01_1_0_1F,
JLSL_08_07_1_0_1F,
JPGB_08_08_3_0_1F_RGB, # has RGB component IDs
JPGB_08_08_3_0_1F_YBR_FULL, # has JFIF APP marker
)


Expand Down Expand Up @@ -194,6 +196,44 @@ def test_jls_lossy_signed_raises(self):
pixel_representation=1,
)

def test_rgb_component_ids(self):
"""Test decoding an incorrect photometric interpretation using cIDs."""
decoder = get_decoder(JPEGBaseline8Bit)
reference = JPGB_08_08_3_0_1F_RGB
msg = (
r"The \(0028,0004\) 'Photometric Interpretation' value is "
"'YBR_FULL_422' however the encoded image's codestream uses "
"component IDs that indicate it should be 'RGB'"
)
ds = reference.ds
ds.PhotometricInterpretation = "YBR_FULL_422"
with pytest.warns(UserWarning, match=msg):
arr = decoder.as_array(ds, raw=True, decoding_plugin="gdcm")

reference.test(arr, plugin="pylibjpeg")
assert arr.shape == reference.shape
assert arr.dtype == reference.dtype
assert arr.flags.writeable

def test_jfif(self):
"""Test decoding an incorrect photometric interpretation using JFIF."""
decoder = get_decoder(JPEGBaseline8Bit)
reference = JPGB_08_08_3_0_1F_YBR_FULL
msg = (
r"The \(0028,0004\) 'Photometric Interpretation' value is "
"'RGB' however the encoded image's codestream contains a JFIF APP "
"marker which indicates it should be 'YBR_FULL_422'"
)
ds = reference.ds
ds.PhotometricInterpretation = "RGB"
with pytest.warns(UserWarning, match=msg):
arr = decoder.as_array(ds, raw=True, decoding_plugin="gdcm")

reference.test(arr, plugin="pylibjpeg")
assert arr.shape == reference.shape
assert arr.dtype == reference.dtype
assert arr.flags.writeable


@pytest.mark.skipif(SKIP_TEST, reason="Test is missing dependencies")
def test_version_check(caplog):
Expand Down
48 changes: 46 additions & 2 deletions tests/pixels/test_decoder_pillow.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
from .pixels_reference import (
PIXEL_REFERENCE,
J2KR_08_08_3_0_1F_YBR_RCT,
JPGB_08_08_3_0_1F_RGB,
JPGB_08_08_3_0_1F_RGB, # has RGB component IDs
JPGB_08_08_3_0_1F_YBR_FULL, # has JFIF APP marker
)


Expand All @@ -56,7 +57,12 @@ class TestLibJpegDecoder:
def test_jpg_baseline(self, reference):
"""Test the decoder with JPEGBaseline8Bit."""
decoder = get_decoder(JPEGBaseline8Bit)
arr = decoder.as_array(reference.ds, raw=True, decoding_plugin="pillow")
if reference in (JPGB_08_08_3_0_1F_RGB, JPGB_08_08_3_0_1F_YBR_FULL):
with pytest.warns(UserWarning):
arr = decoder.as_array(reference.ds, raw=True, decoding_plugin="pillow")
else:
arr = decoder.as_array(reference.ds, raw=True, decoding_plugin="pillow")

reference.test(arr, plugin="pillow")
assert arr.shape == reference.shape
assert arr.dtype == reference.dtype
Expand All @@ -81,6 +87,44 @@ def test_jpg_extended(self, reference):
assert arr.dtype == reference.dtype
assert arr.flags.writeable

def test_rgb_component_ids(self):
"""Test decoding an incorrect photometric interpretation using cIDs."""
decoder = get_decoder(JPEGBaseline8Bit)
reference = JPGB_08_08_3_0_1F_RGB
msg = (
r"The \(0028,0004\) 'Photometric Interpretation' value is "
"'YBR_FULL_422' however the encoded image's codestream uses "
"component IDs that indicate it should be 'RGB'"
)
ds = reference.ds
ds.PhotometricInterpretation = "YBR_FULL_422"
with pytest.warns(UserWarning, match=msg):
arr = decoder.as_array(ds, raw=True, decoding_plugin="pillow")

reference.test(arr, plugin="pylibjpeg")
assert arr.shape == reference.shape
assert arr.dtype == reference.dtype
assert arr.flags.writeable

def test_jfif(self):
"""Test decoding an incorrect photometric interpretation using JFIF."""
decoder = get_decoder(JPEGBaseline8Bit)
reference = JPGB_08_08_3_0_1F_YBR_FULL
msg = (
r"The \(0028,0004\) 'Photometric Interpretation' value is "
"'RGB' however the encoded image's codestream contains a JFIF APP "
"marker which indicates it should be 'YBR_FULL_422'"
)
ds = reference.ds
ds.PhotometricInterpretation = "RGB"
with pytest.warns(UserWarning, match=msg):
arr = decoder.as_array(ds, raw=True, decoding_plugin="pillow")

reference.test(arr, plugin="pylibjpeg")
assert arr.shape == reference.shape
assert arr.dtype == reference.dtype
assert arr.flags.writeable


@pytest.mark.skipif(SKIP_OJ, reason="Test is missing dependencies")
class TestOpenJpegDecoder:
Expand Down
50 changes: 49 additions & 1 deletion tests/pixels/test_decoder_pylibjpeg.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
JPGE_BAD,
J2KR_16_13_1_1_1F_M2_MISMATCH,
JLSN_08_01_1_0_1F,
JPGB_08_08_3_0_1F_RGB, # has RGB component IDs
JPGB_08_08_3_0_1F_YBR_FULL, # has JFIF APP marker
)


Expand Down Expand Up @@ -63,7 +65,15 @@ class TestLibJpegDecoder:
def test_jpg_baseline(self, reference):
"""Test the decoder with JPEGBaseline8Bit."""
decoder = get_decoder(JPEGBaseline8Bit)
arr = decoder.as_array(reference.ds, raw=True, decoding_plugin="pylibjpeg")

if reference in (JPGB_08_08_3_0_1F_RGB, JPGB_08_08_3_0_1F_YBR_FULL):
with pytest.warns(UserWarning):
arr = decoder.as_array(
reference.ds, raw=True, decoding_plugin="pylibjpeg"
)
else:
arr = decoder.as_array(reference.ds, raw=True, decoding_plugin="pylibjpeg")

reference.test(arr, plugin="pylibjpeg")
assert arr.shape == reference.shape
assert arr.dtype == reference.dtype
Expand Down Expand Up @@ -169,6 +179,44 @@ def test_bits_allocated_mismatch_as_buffer(self):
JLSN_08_01_1_0_1F.test(arr)
assert arr.shape == JLSN_08_01_1_0_1F.shape

def test_rgb_component_ids(self):
"""Test decoding an incorrect photometric interpretation using cIDs."""
decoder = get_decoder(JPEGBaseline8Bit)
reference = JPGB_08_08_3_0_1F_RGB
msg = (
r"The \(0028,0004\) 'Photometric Interpretation' value is "
"'YBR_FULL_422' however the encoded image's codestream uses "
"component IDs that indicate it should be 'RGB'"
)
ds = reference.ds
ds.PhotometricInterpretation = "YBR_FULL_422"
with pytest.warns(UserWarning, match=msg):
arr = decoder.as_array(ds, raw=True, decoding_plugin="pylibjpeg")

reference.test(arr, plugin="pylibjpeg")
assert arr.shape == reference.shape
assert arr.dtype == reference.dtype
assert arr.flags.writeable

def test_jfif(self):
"""Test decoding an incorrect photometric interpretation using JFIF."""
decoder = get_decoder(JPEGBaseline8Bit)
reference = JPGB_08_08_3_0_1F_YBR_FULL
msg = (
r"The \(0028,0004\) 'Photometric Interpretation' value is "
"'RGB' however the encoded image's codestream contains a JFIF APP "
"marker which indicates it should be 'YBR_FULL_422'"
)
ds = reference.ds
ds.PhotometricInterpretation = "RGB"
with pytest.warns(UserWarning, match=msg):
arr = decoder.as_array(ds, raw=True, decoding_plugin="pylibjpeg")

reference.test(arr, plugin="pylibjpeg")
assert arr.shape == reference.shape
assert arr.dtype == reference.dtype
assert arr.flags.writeable


@pytest.mark.skipif(SKIP_OJ, reason="Test is missing dependencies")
class TestOpenJpegDecoder:
Expand Down

0 comments on commit 6865c2b

Please sign in to comment.