Skip to content

Commit 4f03a24

Browse files
authored
Test standalone j2k decoding (#46)
1 parent c19ef3e commit 4f03a24

File tree

6 files changed

+194
-34
lines changed

6 files changed

+194
-34
lines changed

.github/workflows/pytest-builds.yml

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,19 +27,31 @@ jobs:
2727
run: |
2828
python -m pip install -U pip
2929
python -m pip install .
30+
python -m pip uninstall -y pylibjpeg-openjpeg
3031
python -m pip install git+https://github.com/pydicom/pylibjpeg-data
31-
python -m pip install pytest coverage pytest-cov
32+
python -m pip install pytest coverage pytest-cov pydicom
3233
33-
- name: Run pytest
34+
- name: Run pytest with no plugins
3435
run: |
35-
pytest --cov pylibjpeg
36+
pytest --cov=pylibjpeg --cov-append
3637
37-
- name: Install -libjpeg and -openjpeg plugins and rerun pytest
38+
- name: Rerun pytest with -lj plugin
3839
run: |
39-
pip install pydicom
40+
4041
pip install git+https://github.com/pydicom/pylibjpeg-libjpeg
4142
pip install git+https://github.com/pydicom/pylibjpeg-openjpeg
42-
pytest --cov pylibjpeg
43+
pytest --cov=pylibjpeg --cov-append
44+
45+
- name: Rerun pytest with -oj plugin
46+
run: |
47+
pip uninstall -y pylibjpeg-libjpeg
48+
pip install git+https://github.com/pydicom/pylibjpeg-openjpeg
49+
pytest --cov=pylibjpeg --cov-append
50+
51+
- name: Rerun pytest with -oj and -lj plugins
52+
run: |
53+
pip install git+https://github.com/pydicom/pylibjpeg-libjpeg
54+
pytest --cov=pylibjpeg --cov-append
4355
4456
- name: Send coverage results
4557
if: ${{ success() }}

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[![codecov](https://codecov.io/gh/pydicom/pylibjpeg/branch/master/graph/badge.svg)](https://codecov.io/gh/pydicom/pylibjpeg)
2-
[![Build Status](https://travis-ci.org/pydicom/pylibjpeg.svg?branch=master)](https://travis-ci.org/pydicom/pylibjpeg)
2+
[![Build Status](https://github.com/pydicom/pylibjpeg/workflows/build/badge.svg)](https://github.com/pydicom/pylibjpeg/actions?query=workflow%3Abuild)
33
[![PyPI version](https://badge.fury.io/py/pylibjpeg.svg)](https://badge.fury.io/py/pylibjpeg)
44
[![Python versions](https://img.shields.io/pypi/pyversions/pylibjpeg.svg)](https://img.shields.io/pypi/pyversions/pylibjpeg.svg)
55

@@ -25,13 +25,12 @@ python -m pip install pylibjpeg
2525

2626
### Plugins
2727

28-
By itself *pylibjpeg* is unable to decode any JPEG images, which is where the
29-
plugins come in. To support a given JPEG format or DICOM Transfer Syntax
28+
One or more plugins are required before *pylibjpeg* is able to decode JPEG images. To decode a given JPEG format or DICOM Transfer Syntax
3029
you first have to install the corresponding package:
3130

3231
#### JPEG Format
3332
| Format | Decode? | Encode? | Plugin | Based on |
34-
|---|------|---|---|---|---|
33+
|---|------|---|---|---|
3534
| JPEG, JPEG-LS and JPEG XT | Yes | No | [pylibjpeg-libjpeg][1] | [libjpeg][2] |
3635
| JPEG 2000 | Yes | No | [pylibjpeg-openjpeg][3] | [openjpeg][4] |
3736

pylibjpeg/tests/test_decode.py

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515

1616
HAS_DECODERS = bool(get_decoders())
17+
RUN_JPEG = bool(get_decoders("JPEG"))
18+
RUN_JPEGLS = bool(get_decoders("JPEG-LS"))
19+
RUN_JPEG2K = bool(get_decoders("JPEG 2000"))
1720

1821

1922
@pytest.mark.skipif(HAS_DECODERS, reason="Decoders available")
@@ -59,8 +62,8 @@ def test_unknown_decoder_type(self):
5962
get_decoders(decoder_type='TEST')
6063

6164

62-
@pytest.mark.skipif(not HAS_DECODERS, reason="Decoders unavailable")
63-
class TestDecoders(object):
65+
@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG decoders available")
66+
class TestJPEGDecoders(object):
6467
"""Test decoding."""
6568
def test_decode_str(self):
6669
"""Test passing a str to decode."""
@@ -109,9 +112,120 @@ def test_specify_decoder(self):
109112
assert isinstance(fpath, str)
110113
arr = decode(fpath, decoder='libjpeg')
111114

115+
@pytest.mark.skipif("openjpeg" in get_decoders(), reason="Have openjpeg")
112116
def test_specify_unknown_decoder(self):
113117
"""Test specifying an unknown decoder."""
114118
fpath = os.path.join(JPEG_DIRECTORY, '10918', 'p1', 'A1.JPG')
115119
assert isinstance(fpath, str)
116120
with pytest.raises(ValueError, match=r"The 'openjpeg' decoder"):
117121
decode(fpath, decoder='openjpeg')
122+
123+
124+
@pytest.mark.skipif(not RUN_JPEGLS, reason="No JPEG-LS decoders available")
125+
class TestJPEGLSDecoders(object):
126+
"""Test decoding JPEG-LS files."""
127+
def setup(self):
128+
self.basedir = os.path.join(JPEG_DIRECTORY, '14495', 'JLS')
129+
130+
def test_decode_str(self):
131+
"""Test passing a str to decode."""
132+
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
133+
assert isinstance(fpath, str)
134+
arr = decode(fpath)
135+
136+
def test_decode_pathlike(self):
137+
"""Test passing a pathlike to decode."""
138+
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
139+
p = Path(fpath)
140+
assert isinstance(p, os.PathLike)
141+
arr = decode(p)
142+
143+
def test_decode_filelike(self):
144+
"""Test passing a filelike to decode."""
145+
bs = BytesIO()
146+
147+
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
148+
with open(fpath, 'rb') as f:
149+
arr = decode(f)
150+
151+
with open(fpath, 'rb') as f:
152+
bs.write(f.read())
153+
154+
bs.seek(0)
155+
arr = decode(bs)
156+
157+
def test_decode_bytes(self):
158+
"""Test passing bytes to decode."""
159+
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
160+
with open(fpath, 'rb') as f:
161+
data = f.read()
162+
163+
assert isinstance(data, bytes)
164+
arr = decode(data)
165+
166+
def test_specify_decoder(self):
167+
"""Test specifying the decoder."""
168+
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
169+
arr = decode(fpath, decoder='libjpeg')
170+
171+
@pytest.mark.skipif("openjpeg" in get_decoders(), reason="Have openjpeg")
172+
def test_specify_unknown_decoder(self):
173+
"""Test specifying an unknown decoder."""
174+
fpath = os.path.join(self.basedir, 'T8C0E0.JLS')
175+
with pytest.raises(ValueError, match=r"The 'openjpeg' decoder"):
176+
decode(fpath, decoder='openjpeg')
177+
178+
179+
@pytest.mark.skipif(not RUN_JPEG2K, reason="No JPEG 2000 decoders available")
180+
class TestJPEG2KDecoders(object):
181+
"""Test decoding JPEG 2000 files."""
182+
def setup(self):
183+
self.basedir = os.path.join(JPEG_DIRECTORY, '15444', '2KLS')
184+
185+
def test_decode_str(self):
186+
"""Test passing a str to decode."""
187+
fpath = os.path.join(self.basedir, '693.j2k')
188+
assert isinstance(fpath, str)
189+
arr = decode(fpath)
190+
191+
def test_decode_pathlike(self):
192+
"""Test passing a pathlike to decode."""
193+
fpath = os.path.join(self.basedir, '693.j2k')
194+
p = Path(fpath)
195+
assert isinstance(p, os.PathLike)
196+
arr = decode(p)
197+
198+
def test_decode_filelike(self):
199+
"""Test passing a filelike to decode."""
200+
bs = BytesIO()
201+
202+
fpath = os.path.join(self.basedir, '693.j2k')
203+
with open(fpath, 'rb') as f:
204+
arr = decode(f)
205+
206+
with open(fpath, 'rb') as f:
207+
bs.write(f.read())
208+
209+
bs.seek(0)
210+
arr = decode(bs)
211+
212+
def test_decode_bytes(self):
213+
"""Test passing bytes to decode."""
214+
fpath = os.path.join(self.basedir, '693.j2k')
215+
with open(fpath, 'rb') as f:
216+
data = f.read()
217+
218+
assert isinstance(data, bytes)
219+
arr = decode(data)
220+
221+
def test_specify_decoder(self):
222+
"""Test specifying the decoder."""
223+
fpath = os.path.join(self.basedir, '693.j2k')
224+
arr = decode(fpath, decoder='openjpeg')
225+
226+
@pytest.mark.skipif("libjpeg" in get_decoders(), reason="Have libjpeg")
227+
def test_specify_unknown_decoder(self):
228+
"""Test specifying an unknown decoder."""
229+
fpath = os.path.join(self.basedir, '693.j2k')
230+
with pytest.raises(ValueError, match=r"The 'libjpeg' decoder"):
231+
decode(fpath, decoder='libjpeg')

pylibjpeg/tests/test_pydicom.py

Lines changed: 25 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
HAS_JPEG_LS_PLUGIN = '1.2.840.10008.1.2.4.80' in decoders
2929
HAS_JPEG_2K_PLUGIN = '1.2.840.10008.1.2.4.90' in decoders
3030

31+
RUN_JPEG = HAS_JPEG_PLUGIN and HAS_PYDICOM
32+
RUN_JPEGLS = HAS_JPEG_LS_PLUGIN and HAS_PYDICOM
33+
RUN_JPEG2K = HAS_JPEG_2K_PLUGIN and HAS_PYDICOM
34+
3135

3236
@pytest.mark.skipif(not HAS_PYDICOM or HAS_PLUGINS, reason="Plugins available")
3337
class TestNoPlugins(object):
@@ -68,9 +72,9 @@ def test_get_pixeldata_no_lj_syntax(self):
6872
handler.get_pixeldata(ds)
6973

7074

71-
@pytest.mark.skipif(not HAS_PYDICOM or not HAS_PLUGINS, reason="No plugins")
7275
class TestPlugins(object):
7376
"""Test interaction with plugins."""
77+
@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
7478
def test_pixel_array(self):
7579
# Should basically just not mess up the usual pydicom behaviour
7680
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
@@ -106,6 +110,7 @@ def test_should_change_PI(self):
106110
result = handler.should_change_PhotometricInterpretation_to_RGB(None)
107111
assert result is False
108112

113+
@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
109114
def test_missing_required(self):
110115
"""Test missing required element raises."""
111116
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
@@ -118,6 +123,7 @@ def test_missing_required(self):
118123
with pytest.raises(AttributeError, match=msg):
119124
ds.pixel_array
120125

126+
@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
121127
def test_ybr_full_422(self):
122128
"""Test YBR_FULL_422 data decoded."""
123129
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
@@ -126,7 +132,7 @@ def test_ybr_full_422(self):
126132
arr = ds.pixel_array
127133

128134

129-
@pytest.mark.skipif(not HAS_PYDICOM or not HAS_JPEG_PLUGIN, reason="No plugin")
135+
@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
130136
class TestJPEGPlugin(object):
131137
"""Test interaction with plugins that support JPEG."""
132138
uid = '1.2.840.10008.1.2.4.50'
@@ -154,7 +160,7 @@ def test_pixel_array(self):
154160
assert 255 == arr[95, 50]
155161

156162

157-
@pytest.mark.skipif(not HAS_PYDICOM or not HAS_JPEG_LS_PLUGIN, reason="No plugin")
163+
@pytest.mark.skipif(not RUN_JPEGLS, reason="No JPEG-LS plugin")
158164
class TestJPEGLSPlugin(object):
159165
"""Test interaction with plugins that support JPEG-LS."""
160166
uid = '1.2.840.10008.1.2.4.80'
@@ -175,7 +181,7 @@ def test_pixel_array(self):
175181
)
176182

177183

178-
@pytest.mark.skipif(not HAS_PYDICOM or not HAS_JPEG_2K_PLUGIN, reason="No plugin")
184+
@pytest.mark.skipif(not RUN_JPEG2K, reason="No JPEG 2000 plugin")
179185
class TestJPEG2KPlugin(object):
180186
"""Test interaction with plugins that support JPEG 2000."""
181187
uid = '1.2.840.10008.1.2.4.90'
@@ -213,29 +219,33 @@ def test_pixel_array(self):
213219
[175, 17, 0]] == arr[175:195, 28, :].tolist()
214220

215221

216-
@pytest.mark.skipif(not HAS_PYDICOM or not HAS_PLUGINS, reason="No plugins")
217222
class TestUtils(object):
218223
"""Test the pydicom.utils functions."""
224+
@pytest.mark.skipif(not RUN_JPEG2K, reason="No JPEG 2000 plugin")
219225
def test_generate_frames_single_1s(self):
220226
"""Test with single frame, 1 sample/px."""
221-
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
222-
ds = index['JPEGBaseline_1s_1f_u_08_08.dcm']['ds']
227+
index = get_indexed_datasets('1.2.840.10008.1.2.4.90')
228+
ds = index['693_J2KR.dcm']['ds']
223229
assert 1 == getattr(ds, 'NumberOfFrames', 1)
224230
assert 1 == ds.SamplesPerPixel
225-
frames = generate_frames(ds)
226-
arr = next(frames)
231+
frame_gen = generate_frames(ds)
232+
arr = next(frame_gen)
227233
with pytest.raises(StopIteration):
228-
next(frames)
234+
next(frame_gen)
229235

230236
assert arr.flags.writeable
231-
assert 'uint8' == arr.dtype
237+
assert 'int16' == arr.dtype
232238
assert (ds.Rows, ds.Columns) == arr.shape
233-
assert 64 == arr[76, 22]
239+
assert (
240+
[1022, 1051, 1165, 1442, 1835, 2096, 2074, 1868, 1685, 1603] ==
241+
arr[290, 135:145].tolist()
242+
)
234243

244+
@pytest.mark.skipif(not RUN_JPEG2K, reason="No JPEG 2000 plugin")
235245
def test_generate_frames_1s(self):
236246
"""Test with multiple frames, 1 sample/px."""
237-
index = get_indexed_datasets('1.2.840.10008.1.2.4.80')
238-
ds = index['emri_small_jpeg_ls_lossless.dcm']['ds']
247+
index = get_indexed_datasets('1.2.840.10008.1.2.4.90')
248+
ds = index['emri_small_jpeg_2k_lossless.dcm']['ds']
239249
assert ds.NumberOfFrames > 1
240250
assert 1 == ds.SamplesPerPixel
241251
frames = generate_frames(ds)
@@ -246,6 +256,7 @@ def test_generate_frames_1s(self):
246256
assert (ds.Rows, ds.Columns) == arr.shape
247257
assert 163 == arr[12, 23]
248258

259+
@pytest.mark.skipif(not RUN_JPEG, reason="No JPEG plugin")
249260
def test_generate_frames_3s_0p(self):
250261
"""Test with multiple frames, 3 sample/px, 0 planar conf."""
251262
index = get_indexed_datasets('1.2.840.10008.1.2.4.50')
@@ -260,9 +271,3 @@ def test_generate_frames_3s_0p(self):
260271
assert 'uint8' == arr.dtype
261272
assert (ds.Rows, ds.Columns, 3) == arr.shape
262273
assert [48, 128, 128] == arr[159, 290, :].tolist()
263-
264-
@pytest.mark.skip()
265-
def test_generate_frames_3s_1p(self):
266-
"""Test 3 sample/px, 1 planar conf."""
267-
# No data
268-
pass

pylibjpeg/utils.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,29 @@ def decode(data, decoder=None, kwargs=None):
8585
raise ValueError("Unable to decode the data")
8686

8787

88-
def get_decoders(decoder_type='JPEG'):
89-
"""Return a :class:`dict` of JPEG decoders as {package: callable}."""
88+
def get_decoders(decoder_type=None):
89+
"""Return a :class:`dict` of JPEG decoders as {package: callable}.
90+
91+
Parameters
92+
----------
93+
decoder_type : str, optional
94+
The class of decoders to return, one of:
95+
96+
* ``"JPEG"`` - ISO/IEC 10918 JPEG decoders
97+
* ``"JPEG XT"`` - ISO/IEC 18477 JPEG decoders
98+
* ``"JPEG-LS"`` - ISO/IEC 14495 JPEG decoders
99+
* ``"JPEG 2000"`` - ISO/IEC 15444 JPEG decoders
100+
* ``"JPEG XS"`` - ISO/IEC 21122 JPEG decoders
101+
* ``"JPEG XL"`` - ISO/IEC 18181 JPEG decoders
102+
103+
If no `decoder_type` is used then all available decoders will be
104+
returned.
105+
106+
Returns
107+
-------
108+
dict
109+
A dict of ``{'package_name': <decoder function>}``.
110+
"""
90111
entry_points = {
91112
"JPEG" : "pylibjpeg.jpeg_decoders",
92113
"JPEG XT" : "pylibjpeg.jpeg_xt_decoders",
@@ -96,6 +117,14 @@ def get_decoders(decoder_type='JPEG'):
96117
"JPEG XS" : "pylibjpeg.jpeg_xs_decoders",
97118
"JPEG XL" : "pylibjpeg.jpeg_xl_decoders",
98119
}
120+
if decoder_type is None:
121+
decoders = {}
122+
for entry_point in entry_points.values():
123+
decoders.update({
124+
val.name: val.load() for val in iter_entry_points(entry_point)
125+
})
126+
return decoders
127+
99128
try:
100129
return {
101130
val.name: val.load()

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pylibjpeg-openjpeg

0 commit comments

Comments
 (0)