Skip to content

Commit

Permalink
Add encoding interface for plugins (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
scaramallion authored May 2, 2021
1 parent d4c803d commit a539187
Show file tree
Hide file tree
Showing 12 changed files with 267 additions and 44 deletions.
63 changes: 42 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

## pylibjpeg

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).
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).


### Installation
Expand All @@ -25,29 +25,29 @@ python -m pip install pylibjpeg

### Plugins

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
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
you first have to install the corresponding package:

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

#### DICOM Transfer Syntax

| UID | Description | Plugin |
|---|---|----|
| 1.2.840.10008.1.2.4.50 | JPEG Baseline (Process 1) | [pylibjpeg-libjpeg][1] |
| 1.2.840.10008.1.2.4.51 | JPEG Extended (Process 2 and 4) | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.57 | JPEG Lossless, Non-Hierarchical (Process 14) | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.70 | JPEG Lossless, Non-Hierarchical, First-Order Prediction</br>(Process 14, Selection Value 1) | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.80 | JPEG-LS Lossless | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.81 | JPEG-LS Lossy (Near-Lossless) Image Compression | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | [pylibjpeg-openjpeg][4] |
| 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | [pylibjpeg-openjpeg][4] |
| 1.2.840.10008.1.2.5 | RLE Lossless | [pylibjpeg-rle][5] |
|UID | Description | Plugin |
|--- |--- |---- |
|1.2.840.10008.1.2.4.50|JPEG Baseline (Process 1) |[pylibjpeg-libjpeg][1] |
|1.2.840.10008.1.2.4.51|JPEG Extended (Process 2 and 4) |[pylibjpeg-libjpeg][1] |
|1.2.840.10008.1.2.4.57|JPEG Lossless, Non-Hierarchical (Process 14) |[pylibjpeg-libjpeg][1] |
|1.2.840.10008.1.2.4.70|JPEG Lossless, Non-Hierarchical, First-Order Prediction</br>(Process 14, Selection Value 1) | [pylibjpeg-libjpeg][1]|
|1.2.840.10008.1.2.4.80|JPEG-LS Lossless |[pylibjpeg-libjpeg][1] |
|1.2.840.10008.1.2.4.81|JPEG-LS Lossy (Near-Lossless) Image Compression |[pylibjpeg-libjpeg][1] |
|1.2.840.10008.1.2.4.90|JPEG 2000 Image Compression (Lossless Only) |[pylibjpeg-openjpeg][4]|
|1.2.840.10008.1.2.4.91|JPEG 2000 Image Compression |[pylibjpeg-openjpeg][4]|
|1.2.840.10008.1.2.5 |RLE Lossless |[pylibjpeg-rle][5] |

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


### Usage
#### With pydicom
Assuming you already have *pydicom* v2.1+ and suitable plugins installed:
#### Decoding
##### With pydicom
Assuming you have *pydicom* v2.1+ and suitable plugins installed:

```python
from pydicom import dcmread
Expand Down Expand Up @@ -101,7 +102,7 @@ frames = generate_frames(ds)
arr = next(frames)
```

#### Standalone JPEG decoding
##### Standalone JPEG decoding
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:
```python
from pylibjpeg import decode
Expand All @@ -117,3 +118,23 @@ with open('filename.jpg', 'rb') as f:
with open('filename.jpg', 'rb') as f:
arr = decode(f.read())
```

#### Encoding
##### With pydicom

Assuming you have *pydicom* v2.2+ and suitable plugins installed:

```python
from pydicom import dcmread
from pydicom.data import get_testdata_file
from pydicom.uid import RLELossless

ds = dcmread(get_testdata_file("CT_small.dcm"))

# Encode in-place using RLE Lossless and update the dataset
# Updates the Pixel Data, Transfer Syntax UID and Planar Configuration
ds.compress(uid)

# Save compressed
ds.save_as("CT_small_rle.dcm")
```
107 changes: 95 additions & 12 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,38 @@ setup(

#### Decoder function signature

The pixel data decoding function will be passed two arguments; a single encoded
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'`:
The pixel data decoding function will be passed two required parameters:

* *src*: a single encoded image frame as [bytes](https://docs.python.org/3/library/stdtypes.html#bytes)
* *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

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
the requirements of the transfer syntax:

```python
def my_pixel_data_decoder(data, ds):
"""Return the encoded `data` as an unshaped numpy ndarray of uint8.
def my_pixel_data_decoder(
src: bytes, ds: pydicom.dataset.Dataset, **kwargs
) -> numpy.ndarray:
"""Return the encoded `src` as an unshaped numpy ndarray of uint8.
.. versionchanged:: 1.3
Added requirement to return little-endian ordered data by default.
Parameters
----------
data : bytes
src : bytes
A single frame of the encoded *Pixel Data*.
ds : pydicom.dataset.Dataset
A dataset containing the group ``0x0028`` elements corresponding to
the *Pixel Data*.
kwargs
Optional keyword parameters for the decoder.
Returns
-------
numpy.ndarray
A 1-dimensional ndarray of 'uint8' containing the decoded pixel data.
A 1-dimensional ndarray of 'uint8' containing the little-endian ordered decoded pixel data.
"""
# Decoding happens here
```
Expand Down Expand Up @@ -78,19 +91,18 @@ Possible entry points for JPEG decoding are:

#### Decoder function signature

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

```python
def my_jpeg_decoder(data, **kwarg):
"""Return the encoded JPEG `data` as an numpy ndarray.
def my_jpeg_decoder(src, **kwargs):
"""Return the encoded JPEG `src` as an numpy ndarray.
Parameters
----------
data : bytes
src : bytes
The encoded JPEG data.
kwarg
kwargs
Keyword arguments passed to the decoder.
Returns
Expand All @@ -100,3 +112,74 @@ def my_jpeg_decoder(data, **kwarg):
"""
# Decoding happens here
```

### DICOM Pixel Data encoders
#### Encoder plugin registration

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`:

```python
from setuptools import setup

setup(
...,
entry_points={
"pylibjpeg.pixel_data_encoders": [
"1.2.840.10008.1.2.5 = my_plugin:encode_rle_lossless",
],
}
)
```

#### Encoder function signature

The pixel data encoding function will be passed two required parameters:

* *src*: a single unencoded image frame as `bytes`, 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 of 0 if more than 1 sample per pixel is used
* *kwargs*: a dict with at least the following keys

* `'transfer_syntax_uid': pydicom.uid.UID` - the intended
*Transfer Syntax UID* of the encoded data.
* `'byteorder': str` - the byte ordering used by *src*, `'<'`
for little-endian (the default), `'>'` for big-endian.
* `'rows': int` - the number of rows of pixels in the *src*.
* `'columns': int` - the number of columns of pixels in the
*src*.
* `'samples_per_pixel': int` - the number of samples used per
pixel, e.g. `1` for grayscale images or `3` for RGB.
* `'number_of_frames': int` - the number of image frames
contained in *src*.
* `'bits_allocated': int` - the number of bits used to contain
each pixel in *src*, should be 8, 16, 32 or 64.
* `'bits_stored': int` - the number of bits actually used by
each pixel in *src*, e.g. 12-bit pixel data (range 0 to 4095) will be
contained by 16-bits (range 0 to 65535).
* `'pixel_representation': int` - the type of data in *src*,
`0` for unsigned integers, `1` for 2's complement (signed)
integers.
* `'photometric_interpretation: str` - the intended colorspace
of the encoded data, such as `'YBR'`.

The function should return the encoded pixel data as `bytes`.

```python
def my_pixel_data_encoder(src: bytes, **kwargs) -> bytes:
"""Return `src` as encoded bytes.
Parameters
----------
src : bytes
A single frame of the encoded *Pixel Data*.
**kwargs
Required and optional parameters for the encoder.
Returns
-------
bytes
The encoded image data.
"""
# Encoding happens here
```
File renamed without changes.
File renamed without changes.
File renamed without changes.
9 changes: 9 additions & 0 deletions docs/release_notes/v1.3.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.. _v1.3.0:

1.3.0
=====

* Added interface for encoding pixel data
* Added :func:`~pylibjpeg.utils.get_encoders` and :func:`~pylibjpeg.utils.get_pixel_data_encoders`
* Updated the requirements for plugins to be more explicit about the format of the
data sent to and received from decoding functions
2 changes: 1 addition & 1 deletion pylibjpeg/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re


__version__ = '1.2.0'
__version__ = '1.3.0'


VERSION_PATTERN = r"""
Expand Down
File renamed without changes.
Empty file added pylibjpeg/tests/test_encode.py
Empty file.
Empty file.
Loading

0 comments on commit a539187

Please sign in to comment.