Skip to content

Commit a539187

Browse files
authored
Add encoding interface for plugins (#62)
1 parent d4c803d commit a539187

File tree

12 files changed

+267
-44
lines changed

12 files changed

+267
-44
lines changed

README.md

Lines changed: 42 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
## pylibjpeg
77

8-
A Python 3.6+ framework for decoding JPEG images and RLE datasets, with a focus on providing support for [pydicom](https://github.com/pydicom/pydicom).
8+
A Python 3.6+ framework for decoding JPEG images and decoding/encoding RLE datasets, with a focus on providing support for [pydicom](https://github.com/pydicom/pydicom).
99

1010

1111
### Installation
@@ -25,29 +25,29 @@ python -m pip install pylibjpeg
2525

2626
### Plugins
2727

28-
One or more plugins are required before *pylibjpeg* is able to decode JPEG images or RLE datasets. To decode a given format or DICOM Transfer Syntax
28+
One or more plugins are required before *pylibjpeg* is able to handle JPEG images or RLE datasets. To handle a given format or DICOM Transfer Syntax
2929
you first have to install the corresponding package:
3030

3131
#### Supported Formats
32-
| Format | Decode? | Encode? | Plugin | Based on |
33-
|---|------|---|---|---|
34-
| JPEG, JPEG-LS and JPEG XT | Yes | No | [pylibjpeg-libjpeg][1] | [libjpeg][2] |
35-
| JPEG 2000 | Yes | No | [pylibjpeg-openjpeg][3] | [openjpeg][4] |
36-
| RLE (PackBits) | Yes | No | [pylibjpeg-rle][5] | - |
32+
|Format |Decode?|Encode?|Plugin |Based on |
33+
|--- |------ |--- |--- |--- |
34+
|JPEG, JPEG-LS and JPEG XT|Yes |No |[pylibjpeg-libjpeg][1] |[libjpeg][2] |
35+
|JPEG 2000 |Yes |No |[pylibjpeg-openjpeg][3]|[openjpeg][4]|
36+
|RLE Lossless (PackBits) |Yes |Yes |[pylibjpeg-rle][5] |- |
3737

3838
#### DICOM Transfer Syntax
3939

40-
| UID | Description | Plugin |
41-
|---|---|----|
42-
| 1.2.840.10008.1.2.4.50 | JPEG Baseline (Process 1) | [pylibjpeg-libjpeg][1] |
43-
| 1.2.840.10008.1.2.4.51 | JPEG Extended (Process 2 and 4) | [pylibjpeg-libjpeg][1]|
44-
| 1.2.840.10008.1.2.4.57 | JPEG Lossless, Non-Hierarchical (Process 14) | [pylibjpeg-libjpeg][1]|
45-
| 1.2.840.10008.1.2.4.70 | JPEG Lossless, Non-Hierarchical, First-Order Prediction</br>(Process 14, Selection Value 1) | [pylibjpeg-libjpeg][1]|
46-
| 1.2.840.10008.1.2.4.80 | JPEG-LS Lossless | [pylibjpeg-libjpeg][1]|
47-
| 1.2.840.10008.1.2.4.81 | JPEG-LS Lossy (Near-Lossless) Image Compression | [pylibjpeg-libjpeg][1]|
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 | [pylibjpeg-rle][5] |
40+
|UID | Description | Plugin |
41+
|--- |--- |---- |
42+
|1.2.840.10008.1.2.4.50|JPEG Baseline (Process 1) |[pylibjpeg-libjpeg][1] |
43+
|1.2.840.10008.1.2.4.51|JPEG Extended (Process 2 and 4) |[pylibjpeg-libjpeg][1] |
44+
|1.2.840.10008.1.2.4.57|JPEG Lossless, Non-Hierarchical (Process 14) |[pylibjpeg-libjpeg][1] |
45+
|1.2.840.10008.1.2.4.70|JPEG Lossless, Non-Hierarchical, First-Order Prediction</br>(Process 14, Selection Value 1) | [pylibjpeg-libjpeg][1]|
46+
|1.2.840.10008.1.2.4.80|JPEG-LS Lossless |[pylibjpeg-libjpeg][1] |
47+
|1.2.840.10008.1.2.4.81|JPEG-LS Lossy (Near-Lossless) Image Compression |[pylibjpeg-libjpeg][1] |
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 |[pylibjpeg-rle][5] |
5151

5252
If you're not sure what the dataset's *Transfer Syntax UID* is, it can be
5353
determined with:
@@ -65,8 +65,9 @@ determined with:
6565

6666

6767
### Usage
68-
#### With pydicom
69-
Assuming you already have *pydicom* v2.1+ and suitable plugins installed:
68+
#### Decoding
69+
##### With pydicom
70+
Assuming you have *pydicom* v2.1+ and suitable plugins installed:
7071

7172
```python
7273
from pydicom import dcmread
@@ -101,7 +102,7 @@ frames = generate_frames(ds)
101102
arr = next(frames)
102103
```
103104

104-
#### Standalone JPEG decoding
105+
##### Standalone JPEG decoding
105106
You can also just use *pylibjpeg* to decode JPEG images to a [numpy ndarray](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html), provided you have a suitable plugin installed:
106107
```python
107108
from pylibjpeg import decode
@@ -117,3 +118,23 @@ with open('filename.jpg', 'rb') as f:
117118
with open('filename.jpg', 'rb') as f:
118119
arr = decode(f.read())
119120
```
121+
122+
#### Encoding
123+
##### With pydicom
124+
125+
Assuming you have *pydicom* v2.2+ and suitable plugins installed:
126+
127+
```python
128+
from pydicom import dcmread
129+
from pydicom.data import get_testdata_file
130+
from pydicom.uid import RLELossless
131+
132+
ds = dcmread(get_testdata_file("CT_small.dcm"))
133+
134+
# Encode in-place using RLE Lossless and update the dataset
135+
# Updates the Pixel Data, Transfer Syntax UID and Planar Configuration
136+
ds.compress(uid)
137+
138+
# Save compressed
139+
ds.save_as("CT_small_rle.dcm")
140+
```

docs/plugins.md

Lines changed: 95 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,38 @@ setup(
2424

2525
#### Decoder function signature
2626

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'`:
27+
The pixel data decoding function will be passed two required parameters:
28+
29+
* *src*: a single encoded image frame as [bytes](https://docs.python.org/3/library/stdtypes.html#bytes)
30+
* *ds*: 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
31+
32+
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 little-endian ordered `'uint8'`, with the data ordered from left-to-right, top-to-bottom (i.e. the first byte corresponds to the upper left pixel and the last byte corresponds to the lower-right pixel) and a planar configuration that matches
33+
the requirements of the transfer syntax:
2934

3035
```python
31-
def my_pixel_data_decoder(data, ds):
32-
"""Return the encoded `data` as an unshaped numpy ndarray of uint8.
36+
def my_pixel_data_decoder(
37+
src: bytes, ds: pydicom.dataset.Dataset, **kwargs
38+
) -> numpy.ndarray:
39+
"""Return the encoded `src` as an unshaped numpy ndarray of uint8.
40+
41+
.. versionchanged:: 1.3
42+
43+
Added requirement to return little-endian ordered data by default.
3344
3445
Parameters
3546
----------
36-
data : bytes
47+
src : bytes
3748
A single frame of the encoded *Pixel Data*.
3849
ds : pydicom.dataset.Dataset
3950
A dataset containing the group ``0x0028`` elements corresponding to
4051
the *Pixel Data*.
52+
kwargs
53+
Optional keyword parameters for the decoder.
4154
4255
Returns
4356
-------
4457
numpy.ndarray
45-
A 1-dimensional ndarray of 'uint8' containing the decoded pixel data.
58+
A 1-dimensional ndarray of 'uint8' containing the little-endian ordered decoded pixel data.
4659
"""
4760
# Decoding happens here
4861
```
@@ -78,19 +91,18 @@ Possible entry points for JPEG decoding are:
7891

7992
#### Decoder function signature
8093

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
94+
The JPEG decoding function will be passed the encoded JPEG *data* as [bytes](https://docs.python.org/3/library/stdtypes.html#bytes) and a
8395
[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:
8496

8597
```python
86-
def my_jpeg_decoder(data, **kwarg):
87-
"""Return the encoded JPEG `data` as an numpy ndarray.
98+
def my_jpeg_decoder(src, **kwargs):
99+
"""Return the encoded JPEG `src` as an numpy ndarray.
88100
89101
Parameters
90102
----------
91-
data : bytes
103+
src : bytes
92104
The encoded JPEG data.
93-
kwarg
105+
kwargs
94106
Keyword arguments passed to the decoder.
95107
96108
Returns
@@ -100,3 +112,74 @@ def my_jpeg_decoder(data, **kwarg):
100112
"""
101113
# Decoding happens here
102114
```
115+
116+
### DICOM Pixel Data encoders
117+
#### Encoder plugin registration
118+
119+
Plugins that encode DICOM *Pixel Data* should register their encoding functions using the corresponding *Transfer Syntax UID* as the entry point name. For example, if the `my_plugin` plugin supported encoding *RLE Lossless* (1.2.840.10008.1.2.5) with the encoding function `encode_rle_lossless()` then it should include the following in its `setup.py`:
120+
121+
```python
122+
from setuptools import setup
123+
124+
setup(
125+
...,
126+
entry_points={
127+
"pylibjpeg.pixel_data_encoders": [
128+
"1.2.840.10008.1.2.5 = my_plugin:encode_rle_lossless",
129+
],
130+
}
131+
)
132+
```
133+
134+
#### Encoder function signature
135+
136+
The pixel data encoding function will be passed two required parameters:
137+
138+
* *src*: a single unencoded image frame as `bytes`, with the data ordered from
139+
left-to-right, top-to-bottom (i.e. the first byte corresponds to the upper
140+
left pixel and the last byte corresponds to the lower-right pixel) and a
141+
planar configuration of 0 if more than 1 sample per pixel is used
142+
* *kwargs*: a dict with at least the following keys
143+
144+
* `'transfer_syntax_uid': pydicom.uid.UID` - the intended
145+
*Transfer Syntax UID* of the encoded data.
146+
* `'byteorder': str` - the byte ordering used by *src*, `'<'`
147+
for little-endian (the default), `'>'` for big-endian.
148+
* `'rows': int` - the number of rows of pixels in the *src*.
149+
* `'columns': int` - the number of columns of pixels in the
150+
*src*.
151+
* `'samples_per_pixel': int` - the number of samples used per
152+
pixel, e.g. `1` for grayscale images or `3` for RGB.
153+
* `'number_of_frames': int` - the number of image frames
154+
contained in *src*.
155+
* `'bits_allocated': int` - the number of bits used to contain
156+
each pixel in *src*, should be 8, 16, 32 or 64.
157+
* `'bits_stored': int` - the number of bits actually used by
158+
each pixel in *src*, e.g. 12-bit pixel data (range 0 to 4095) will be
159+
contained by 16-bits (range 0 to 65535).
160+
* `'pixel_representation': int` - the type of data in *src*,
161+
`0` for unsigned integers, `1` for 2's complement (signed)
162+
integers.
163+
* `'photometric_interpretation: str` - the intended colorspace
164+
of the encoded data, such as `'YBR'`.
165+
166+
The function should return the encoded pixel data as `bytes`.
167+
168+
```python
169+
def my_pixel_data_encoder(src: bytes, **kwargs) -> bytes:
170+
"""Return `src` as encoded bytes.
171+
172+
Parameters
173+
----------
174+
src : bytes
175+
A single frame of the encoded *Pixel Data*.
176+
**kwargs
177+
Required and optional parameters for the encoder.
178+
179+
Returns
180+
-------
181+
bytes
182+
The encoded image data.
183+
"""
184+
# Encoding happens here
185+
```
File renamed without changes.
File renamed without changes.
File renamed without changes.

docs/release_notes/v1.3.0.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
.. _v1.3.0:
2+
3+
1.3.0
4+
=====
5+
6+
* Added interface for encoding pixel data
7+
* Added :func:`~pylibjpeg.utils.get_encoders` and :func:`~pylibjpeg.utils.get_pixel_data_encoders`
8+
* Updated the requirements for plugins to be more explicit about the format of the
9+
data sent to and received from decoding functions

pylibjpeg/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44

55

6-
__version__ = '1.2.0'
6+
__version__ = '1.3.0'
77

88

99
VERSION_PATTERN = r"""

pylibjpeg/tests/test_encode.py

Whitespace-only changes.

pylibjpeg/tests/test_encode_pydicom.py

Whitespace-only changes.

0 commit comments

Comments
 (0)