Skip to content

Commit fdc6e98

Browse files
authored
Convert unsigned J2K data to signed (#53)
1 parent 444ad8a commit fdc6e98

File tree

3 files changed

+117
-5
lines changed

3 files changed

+117
-5
lines changed

pylibjpeg/pydicom/pixel_data_handler.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,9 @@
4242

4343
from pydicom.encaps import generate_pixel_data_frame
4444
from pydicom.pixel_data_handlers.util import pixel_dtype, get_expected_length
45+
from pydicom.uid import JPEG2000, JPEG2000Lossless
4546

46-
from .utils import get_pixel_data_decoders
47+
from .utils import get_pixel_data_decoders, get_j2k_parameters
4748

4849

4950
LOGGER = logging.getLogger(__name__)
@@ -54,6 +55,8 @@
5455
'numpy': ('http://www.numpy.org/', 'NumPy'),
5556
}
5657

58+
APPLY_J2K_CORRECTIONS = True
59+
5760
_DECODERS = get_pixel_data_decoders()
5861
SUPPORTED_TRANSFER_SYNTAXES = list(_DECODERS.keys())
5962

@@ -162,10 +165,28 @@ def get_pixeldata(ds):
162165
# Generators for the encoded JPEG image frame(s) and insertion offsets
163166
generate_frames = generate_pixel_data_frame(ds.PixelData, nr_frames)
164167
generate_offsets = range(0, expected_len, frame_len)
168+
pixel_module = ds.group_dataset(0x0028)
165169
for frame, offset in zip(generate_frames, generate_offsets):
166170
# Encoded JPEG data to be sent to the decoder
167-
arr[offset:offset + frame_len] = decoder(
168-
frame, ds.group_dataset(0x0028)
169-
)
171+
arr[offset:offset + frame_len] = decoder(frame, pixel_module)
172+
173+
if tsyntax in [JPEG2000, JPEG2000Lossless] and APPLY_J2K_CORRECTIONS:
174+
j2k_parameters = get_j2k_parameters(frame)
175+
if j2k_parameters:
176+
shift = ds.BitsAllocated - j2k_parameters['precision']
177+
if (
178+
shift
179+
and not j2k_parameters['is_signed']
180+
and bool(ds.PixelRepresentation)
181+
):
182+
# Correct for a mismatch between J2K and Pixel Representation
183+
# by converting unsigned data to signed (2's complement)
184+
pixel_module.PixelRepresentation = 0
185+
# This probably isn't very efficient
186+
arr = arr.view(pixel_dtype(pixel_module))
187+
np.left_shift(arr, shift, out=arr)
188+
arr = arr.astype(pixel_dtype(ds))
189+
190+
return np.right_shift(arr, shift)
170191

171192
return arr.view(pixel_dtype(ds))

pylibjpeg/pydicom/utils.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,3 +149,43 @@ def reshape_frame(ds, arr):
149149
arr = arr.transpose(1, 2, 0)
150150

151151
return arr
152+
153+
154+
def get_j2k_parameters(codestream):
155+
"""Return some of the JPEG 2000 component sample's parameters in `stream`.
156+
157+
Parameters
158+
----------
159+
codestream : bytes
160+
The JPEG 2000 (ISO/IEC 15444-1) codestream data to be parsed.
161+
162+
Returns
163+
-------
164+
dict
165+
A dict containing the JPEG 2000 parameters for the first component
166+
sample, will be empty if `codestream` doesn't contain JPEG 2000 data or
167+
if unable to parse the data.
168+
"""
169+
try:
170+
# First 2 bytes must be the SOC marker - if not then wrong format
171+
if codestream[0:2] != b'\xff\x4f':
172+
return {}
173+
174+
# SIZ is required to be the second marker - Figure A-3 in 15444-1
175+
if codestream[2:4] != b'\xff\x51':
176+
return {}
177+
178+
# See 15444-1 A.5.1 for format of the SIZ box and contents
179+
ssiz = ord(codestream[42:43])
180+
parameters = {}
181+
if ssiz & 0x80:
182+
parameters['precision'] = (ssiz & 0x7F) + 1
183+
parameters['is_signed'] = True
184+
else:
185+
parameters['precision'] = ssiz + 1
186+
parameters['is_signed'] = False
187+
188+
return parameters
189+
190+
except (IndexError, TypeError):
191+
return {}

pylibjpeg/tests/test_pydicom.py

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
from pylibjpeg.data import get_indexed_datasets
1919
from pylibjpeg.pydicom.utils import (
20-
generate_frames, reshape_frame, get_pixel_data_decoders
20+
generate_frames, reshape_frame, get_pixel_data_decoders, get_j2k_parameters
2121
)
2222
from pylibjpeg.utils import add_handler, remove_handler
2323

@@ -219,6 +219,24 @@ def test_pixel_array(self):
219219
[165, 11, 0],
220220
[175, 17, 0]] == arr[175:195, 28, :].tolist()
221221

222+
def test_pixel_representation_mismatch(self):
223+
"""Test mismatch between Pixel Representation and the J2K data."""
224+
index = get_indexed_datasets(self.uid)
225+
ds = index['J2K_pixelrep_mismatch.dcm']['ds']
226+
227+
arr = ds.pixel_array
228+
assert arr.flags.writeable
229+
assert 'int16' == arr.dtype
230+
assert (512, 512) == arr.shape
231+
232+
assert -2000 == arr[0, 0]
233+
assert [621, 412, 138, -193, -520, -767, -907, -966, -988, -995] == (
234+
arr[47:57, 279].tolist()
235+
)
236+
assert [-377, -121, 141, 383, 633, 910, 1198, 1455, 1638, 1732] == (
237+
arr[328:338, 106].tolist()
238+
)
239+
222240

223241
class TestUtils(object):
224242
"""Test the pydicom.utils functions."""
@@ -272,3 +290,36 @@ def test_generate_frames_3s_0p(self):
272290
assert 'uint8' == arr.dtype
273291
assert (ds.Rows, ds.Columns, 3) == arr.shape
274292
assert [48, 128, 128] == arr[159, 290, :].tolist()
293+
294+
295+
class TestGetJ2KParameters:
296+
"""Tests for get_j2k_parameters."""
297+
def test_parameters(self):
298+
"""Test getting the parameters for a JPEG2K codestream."""
299+
base = b'\xff\x4f\xff\x51' + b'\x00' * 38
300+
# Signed
301+
for ii in range(135, 143):
302+
params = get_j2k_parameters(base + bytes([ii]))
303+
assert ii - 127 == params['precision']
304+
assert params['is_signed']
305+
306+
# Unsigned
307+
for ii in range(7, 17):
308+
params = get_j2k_parameters(base + bytes([ii]))
309+
assert ii + 1 == params['precision']
310+
assert not params['is_signed']
311+
312+
def test_not_j2k(self):
313+
"""Test result when no JPEG2K SOF marker present"""
314+
base = b'\xff\x4e\xff\x51' + b'\x00' * 38
315+
assert {} == get_j2k_parameters(base + b'\x8F')
316+
317+
def test_no_siz(self):
318+
"""Test result when no SIZ box present"""
319+
base = b'\xff\x4f\xff\x52' + b'\x00' * 38
320+
assert {} == get_j2k_parameters(base + b'\x8F')
321+
322+
def test_short_bytestream(self):
323+
"""Test result when no SIZ box present"""
324+
assert {} == get_j2k_parameters(b'')
325+
assert {} == get_j2k_parameters(b'\xff\x4f\xff\x51' + b'\x00' * 20)

0 commit comments

Comments
 (0)