Skip to content

Commit 1ebb7d7

Browse files
authored
Changes to genericise the plugin interface (#44)
1 parent a23c75a commit 1ebb7d7

File tree

12 files changed

+300
-146
lines changed

12 files changed

+300
-146
lines changed
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: build
2+
3+
on:
4+
push:
5+
branches: [ master ]
6+
pull_request:
7+
branches: [ master ]
8+
9+
jobs:
10+
ubuntu:
11+
runs-on: ubuntu-latest
12+
timeout-minutes: 10
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
python-version: [3.6, 3.7, 3.8]
17+
18+
steps:
19+
- uses: actions/checkout@v2
20+
21+
- name: Set up Python ${{ matrix.python-version }}
22+
uses: actions/setup-python@v2
23+
with:
24+
python-version: ${{ matrix.python-version }}
25+
26+
- name: Install package and dependencies
27+
run: |
28+
python -m pip install -U pip
29+
python -m pip install .
30+
python -m pip install pytest coverage pytest-cov
31+
python -m pip install git+https://github.com/pydicom/pylibjpeg-data
32+
33+
- name: Run pytest
34+
run: |
35+
pytest --cov openjpeg --ignore=openjpeg/src/openjpeg
36+
37+
- name: Install pydicom release and rerun pytest
38+
run: |
39+
pip install pydicom
40+
pytest --cov pylibjpeg
41+
42+
- name: Install -libjpeg plugin rerun pytest
43+
run: |
44+
pip install git+https://github.com/pydicom/pylibjpeg-libjpeg
45+
pytest --cov pylibjpeg
46+
47+
- name: Install -openjpeg plugin rerun pytest
48+
run: |
49+
pip install git+https://github.com/pydicom/pylibjpeg-openjpeg
50+
pytest --cov pylibjpeg
51+
52+
- name: Send coverage results
53+
if: ${{ success() }}
54+
run: |
55+
bash <(curl --connect-timeout 10 --retry 10 --retry-max-time \
56+
0 https://codecov.io/bash) || (sleep 30 && bash <(curl \
57+
--connect-timeout 10 --retry 10 --retry-max-time \
58+
0 https://codecov.io/bash))

.travis.yml

Lines changed: 0 additions & 93 deletions
This file was deleted.

README.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,12 @@ plugins come in. To support a given JPEG format or DICOM Transfer Syntax
3030
you first have to install the corresponding package:
3131

3232
#### JPEG Format
33-
| Format | Decode? | Encode? | Plugin | Based on |
34-
|---|------|---|---|---|
35-
| JPEG, JPEG-LS and JPEG XT | Yes | No | [pylibjpeg-libjpeg][1] | [libjpeg][2] |
33+
| Format | Decode? | Encode? | Plugin | Based on | Included? |
34+
|---|------|---|---|---|---|
35+
| JPEG, JPEG-LS and JPEG XT | Yes | No | [pylibjpeg-libjpeg][1] | [libjpeg][2] | No |
36+
| JPEG 2000 | Yes | No | [pylibjpeg-openjpeg][3] | [openjpeg][4] | Yes |
3637

37-
#### Transfer Syntax
38+
#### DICOM Transfer Syntax
3839

3940
| UID | Description | Plugin |
4041
|---|---|----|
@@ -44,8 +45,9 @@ you first have to install the corresponding package:
4445
| 1.2.840.10008.1.2.4.70 | JPEG Lossless, Non-Hierarchical, First-Order Prediction</br>(Process 14, Selection Value 1) | [pylibjpeg-libjpeg][1]|
4546
| 1.2.840.10008.1.2.4.80 | JPEG-LS Lossless | [pylibjpeg-libjpeg][1]|
4647
| 1.2.840.10008.1.2.4.81 | JPEG-LS Lossy (Near-Lossless) Image Compression | [pylibjpeg-libjpeg][1]|
47-
| 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | Not yet supported |
48-
| 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | Not yet supported |
48+
| 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | [pylibjpeg-openjpeg][4] |
49+
| 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | [pylibjpeg-openjpeg][4] |
50+
| 1.2.840.10008.1.2.5 | RLE Lossless | Not yet supported |
4951

5052
If you're not sure what the dataset's *Transfer Syntax UID* is, it can be
5153
determined with:
@@ -57,21 +59,28 @@ determined with:
5759

5860
[1]: https://github.com/pydicom/pylibjpeg-libjpeg
5961
[2]: https://github.com/thorfdbg/libjpeg
62+
[3]: https://github.com/pydicom/pylibjpeg-openjpeg
63+
[4]: https://github.com/uclouvain/openjpeg
6064

6165

6266
### Usage
6367
#### With pydicom
64-
Assuming you already have *pydicom* v1.4+ installed:
68+
Assuming you already have *pydicom* v1.4+ and suitable plugins installed:
6569

6670
```python
6771
from pydicom import dcmread
6872
from pydicom.data import get_testdata_file
6973

70-
# With the pylibjpeg-libjpeg plugin installed
74+
# Importing the package adds the pixel data handler to pydicom
7175
import pylibjpeg
7276

77+
# With the pylibjpeg-libjpeg plugin
7378
ds = dcmread(get_testdata_file('JPEG-LL.dcm'))
74-
arr = ds.pixel_array
79+
jpg_arr = ds.pixel_array
80+
81+
# With the pylibjpeg-openjpeg plugin
82+
ds = dcmread(get_testdata_file('JPEG2000.dcm'))
83+
j2k_arr = ds.pixel_array
7584
```
7685

7786
For datasets with multiple frames you can reduce your memory usage by
@@ -93,7 +102,7 @@ You can also just use *pylibjpeg* to decode JPEG images to a [numpy ndarray](htt
93102
```python
94103
from pylibjpeg import decode
95104

96-
# Can decode using the path to a JPG file as str or pathlike
105+
# Can decode using the path to a JPG file as str or path-like
97106
arr = decode('filename.jpg')
98107

99108
# Or a file-like...

build_tools/travis/install.sh

Lines changed: 0 additions & 28 deletions
This file was deleted.

docs/plugins.md

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
2+
## Plugins
3+
4+
Plugins should register their entry points via the *entry_points* kwarg for [setuptools.setup()](https://setuptools.readthedocs.io/en/latest/setuptools.html#dynamic-discovery-of-services-and-plugins) in their `setup.py` file.
5+
6+
### DICOM Pixel Data decoders
7+
#### Decoder plugin registration
8+
9+
Plugins that decode DICOM *Pixel Data* should register their decoding functions using the corresponding *Transfer Syntax UID* as the entry point name. For example, if the `my_plugin` plugin supported *JPEG Baseline* (1.2.840.10008.1.2.4.50) with the decoding function `decode_jpeg_baseline()` and *JPEG-LS Lossless* (1.2.840.10008.1.2.4.80) with the decoding function `decode_jls_lossless()` then it should include the following in its `setup.py`:
10+
11+
```python
12+
from setuptools import setup
13+
14+
setup(
15+
...,
16+
entry_points={
17+
"pylibjpeg.pixel_data_decoders": [
18+
"1.2.840.10008.1.2.4.50 = my_plugin:decode_jpeg_baseline",
19+
"1.2.840.10008.1.2.4.80 = my_plugin:decode_jls_lossless",
20+
],
21+
}
22+
)
23+
```
24+
25+
#### Decoder function signature
26+
27+
The pixel data decoding function will be passed two arguments; a single encoded
28+
image frame as [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) and a *pydicom* [Dataset](https://pydicom.github.io/pydicom/stable/reference/generated/pydicom.dataset.Dataset.html) object containing the (0028,eeee) elements corresponding to the pixel data. The function should return the decoded pixel data as a one-dimensional numpy [ndarray](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html) of `'uint8'`:
29+
30+
```python
31+
def my_pixel_data_decoder(data, ds):
32+
"""Return the encoded `data` as an unshaped numpy ndarray of uint8.
33+
34+
Parameters
35+
----------
36+
data : bytes
37+
A single frame of the encoded *Pixel Data*.
38+
ds : pydicom.dataset.Dataset
39+
A dataset containing the group ``0x0028`` elements corresponding to
40+
the *Pixel Data*.
41+
42+
Returns
43+
-------
44+
numpy.ndarray
45+
A 1-dimensional ndarray of 'uint8' containing the decoded pixel data.
46+
"""
47+
# Decoding happens here
48+
```
49+
50+
### JPEG decoders
51+
#### Decoder plugin registration
52+
53+
Plugins that decoder JPEG data should register their decoding functions uding
54+
the name of the plugin as the entry point name. For example, if the `my_plugin`
55+
plugin supports decoding JPEG images via the `decode_jpeg()` function then
56+
it should include the following in its `setup.py`:
57+
58+
```python
59+
from setuptools import setup
60+
61+
setup(
62+
...,
63+
entry_points={
64+
"pylibjpeg.jpeg_decoders": "my_plugin = my_plugin:decode_jpeg",
65+
}
66+
)
67+
```
68+
69+
Possible entry points for JPEG decoding are:
70+
71+
| JPEG Format | ISO/IEC Standard | Entry Point |
72+
| --- | --- | --- |
73+
| JPEG | [10918](https://www.iso.org/standard/18902.html) | `"pylibjpeg.jpeg_decoders"` |
74+
| JPEG XT | [18477](https://www.iso.org/standard/62552.html) | `"pylibjpeg.jpeg_xt_decoders"` |
75+
| JPEG-LS | [14495](https://www.iso.org/standard/22397.html) | `"pylibjpeg.jpeg_ls_decoders"` |
76+
| JPEG 2000 | [15444](https://www.iso.org/standard/78321.html) | `"pylibjpeg.jpeg_2000_decoders"` |
77+
78+
79+
#### Decoder function signature
80+
81+
The JPEG decoding function will be passed the encoded JPEG *data* as
82+
[bytes](https://docs.python.org/3/library/stdtypes.html#bytes) and a
83+
[dict](https://docs.python.org/3/library/stdtypes.html#dict) containing keyword arguments passed to the function. The function should return the decoded image data as a numpy [ndarray](https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html) with a dtype and shape matching the image format and dimensions:
84+
85+
```python
86+
def my_jpeg_decoder(data, **kwarg):
87+
"""Return the encoded JPEG `data` as an numpy ndarray.
88+
89+
Parameters
90+
----------
91+
data : bytes
92+
The encoded JPEG data.
93+
kwarg
94+
Keyword arguments passed to the decoder.
95+
96+
Returns
97+
-------
98+
numpy.ndarray
99+
An ndarray containing the decoded pixel data.
100+
"""
101+
# Decoding happens here
102+
```

docs/release_notes/v1.1.0.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,5 @@
77
* Removed the ``plugins`` and ``_config`` modules
88
* Added ``utils.get_decoders()`` function
99
* Added ``pydicom.utils.get_pixel_data_decoders()`` function
10+
* Changed arguments passed to pixel data decoding functions to
11+
``func(bytes, Dataset)``

pylibjpeg/pydicom/pixel_data_handler.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def get_pixeldata(ds):
142142
# Note: this does NOT include the trailing null byte for odd length data
143143
expected_len = get_expected_length(ds)
144144
if ds.PhotometricInterpretation == 'YBR_FULL_422':
145+
# JPEG Transfer Syntaxes
145146
# Plugin should have already resampled the pixel data
146147
# see PS3.3 C.7.6.3.1.2
147148
expected_len = expected_len // 2 * 3
@@ -158,12 +159,13 @@ def get_pixeldata(ds):
158159
decoder = _DECODERS[tsyntax]
159160
LOGGER.debug("Decoding {} Pixel Data using {}".format(tsyntax, decoder))
160161

161-
# Generators for the encoded JPG image frame(s) and insertion offsets
162+
# Generators for the encoded JPEG image frame(s) and insertion offsets
162163
generate_frames = generate_pixel_data_frame(ds.PixelData, nr_frames)
163164
generate_offsets = range(0, expected_len, frame_len)
164165
for frame, offset in zip(generate_frames, generate_offsets):
165-
# Encoded JPG data to be sent to the decoder
166-
frame = np.frombuffer(frame, np.uint8)
167-
arr[offset:offset + frame_len] = decoder(frame, p_interp)
166+
# Encoded JPEG data to be sent to the decoder
167+
arr[offset:offset + frame_len] = decoder(
168+
frame, ds.group_dataset(0x0028)
169+
)
168170

169171
return arr.view(pixel_dtype(ds))

pylibjpeg/pydicom/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def generate_frames(ds):
2525
p_interp = ds.PhotometricInterpretation
2626
nr_frames = getattr(ds, 'NumberOfFrames', 1)
2727
for frame in generate_pixel_data_frame(ds.PixelData, nr_frames):
28-
arr = decode(frame, p_interp).view(pixel_dtype(ds))
28+
arr = decode(frame, ds.group_dataset(0x0028)).view(pixel_dtype(ds))
2929
yield reshape_frame(ds, arr)
3030

3131

0 commit comments

Comments
 (0)