Skip to content

Commit

Permalink
Update interface, workflows (#68)
Browse files Browse the repository at this point in the history
  • Loading branch information
scaramallion authored Jan 22, 2022
1 parent 68455e3 commit bc25035
Show file tree
Hide file tree
Showing 20 changed files with 754 additions and 628 deletions.
27 changes: 13 additions & 14 deletions .github/workflows/pytest-builds.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: build
name: unit-tests

on:
push:
Expand All @@ -12,7 +12,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
python-version: ['3.7', '3.8', '3.9', '3.10']

steps:
- uses: actions/checkout@v2
Expand All @@ -25,11 +25,9 @@ jobs:
- name: Install package and dependencies
run: |
python -m pip install -U pip
python -m pip install wheel
python -m pip install .
python -m pip uninstall -y pylibjpeg-openjpeg
python -m pip install -U wheel pytest coverage pytest-cov
python -m pip install git+https://github.com/pydicom/pylibjpeg-data
python -m pip install pytest coverage pytest-cov
python -m pip install .
- name: Run pytest with no plugins
run: |
Expand All @@ -44,7 +42,6 @@ jobs:
run: |
pip install git+https://github.com/pydicom/pylibjpeg-libjpeg
pip install git+https://github.com/pydicom/pylibjpeg-openjpeg
pytest --cov=pylibjpeg --cov-append
- name: Rerun pytest with -oj plugin
Expand All @@ -53,15 +50,17 @@ jobs:
pip install git+https://github.com/pydicom/pylibjpeg-openjpeg
pytest --cov=pylibjpeg --cov-append
- name: Rerun pytest with -oj and -lj plugins
- name: Rerun pytest with -rle plugin
run: |
pip install git+https://github.com/pydicom/pylibjpeg-libjpeg
pip uninstall -y pylibjpeg-libjpeg pylibjpeg-openjpeg
pip install git+https://github.com/pydicom/pylibjpeg-rle
pytest --cov=pylibjpeg --cov-append
- name: Rerun pytest with all plugins
run: |
pip install .[all]
pytest --cov=pylibjpeg --cov-append
- name: Send coverage results
if: ${{ success() }}
run: |
bash <(curl --connect-timeout 10 --retry 10 --retry-max-time \
0 https://codecov.io/bash) || (sleep 30 && bash <(curl \
--connect-timeout 10 --retry 10 --retry-max-time \
0 https://codecov.io/bash))
uses: codecov/codecov-action@v1
2 changes: 1 addition & 1 deletion .github/workflows/release-deploy.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build package and deploy to PyPI
name: release-deploy

on:
release:
Expand Down
12 changes: 9 additions & 3 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 decoding/encoding RLE datasets, with a focus on providing support for [pydicom](https://github.com/pydicom/pydicom).
A Python 3.7+ 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 @@ -17,10 +17,16 @@ pip install pylibjpeg

##### Installing extra requirements

The package can be installed with extra requirements `openjpeg` or `rle` to enable support for JPEG-2000 and Run-Length Encoding (RLE), respectively:
The package can be installed with extra requirements to enable support for JPEG (with `libjpeg`), JPEG 2000 (with `openjpeg`) and Run-Length Encoding (RLE) (with `rle`), respectively:

```
pip install pylibjpeg[openjpeg,rle]
pip install pylibjpeg[libjpeg,openjpeg,rle]
```

Or alternatively with just `all`:

```
pip install pylibjpeg[all]
```

#### Installing the development version
Expand Down
54 changes: 47 additions & 7 deletions docs/plugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,43 @@ setup(

#### Decoder function signature

The pixel data decoding function will be passed two required parameters:
The pixel data decoding function will be passed one required parameter:

* *src*: a single encoded image frame as [bytes](https://docs.python.org/3/library/stdtypes.html#bytes)

And at least one of:
* *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
* *kwargs*: a dict with at least the following keys:
* `"transfer_syntax_uid": pydicom.uid.UID` - the *Transfer Syntax UID* of
the encoded data.
* `'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.
* `'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*.
* `'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 color space
of the encoded data, such as `'YBR_FULL'`.

Other decoder-specific optional keyword parameters may also be present.

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(
src: bytes, ds: pydicom.dataset.Dataset, **kwargs
src: bytes, ds: Optional[pydicom.dataset.Dataset] = None, **kwargs: Any
) -> numpy.ndarray:
"""Return the encoded `src` as an unshaped numpy ndarray of uint8.
"""Return the encoded *src* as an unshaped numpy ndarray of uint8.
.. versionchanged:: 1.3
Expand All @@ -46,11 +70,27 @@ def my_pixel_data_decoder(
----------
src : bytes
A single frame of the encoded *Pixel Data*.
ds : pydicom.dataset.Dataset
ds : pydicom.dataset.Dataset, optional
A dataset containing the group ``0x0028`` elements corresponding to
the *Pixel Data*.
kwargs
Optional keyword parameters for the decoder.
the *Pixel Data*. If not used then *kwargs* must be supplied.
kwargs : Dict[str, Any]
A dict containing relevant image pixel module elements:
* "rows": int - the number of rows of pixels in *src*, maximum 65535.
* "columns": int - the number of columns of pixels in *src*, maximum
65535.
* "number_of_frames": int - the number of frames in *src*.
* "samples_per_pixel": int - the number of samples per pixel in *src*,
should be 1 or 3.
* "bits_allocated": int - the number of bits used to contain each
pixel, should be a multiple of 8.
* "bits_stored": int - the number of bits actually used per pixel.
* "pixel_representation": int - the type of data being decoded, 0 for
unsigned, 1 for 2's complement (signed)
* "photometric_interpretation": the color space of the *encoded* pixel
data, such as "YBR_FULL".
And optional keyword parameters for the decoder.
Returns
-------
Expand Down
6 changes: 3 additions & 3 deletions pylibjpeg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,17 @@


# Setup default logging
_logger = logging.getLogger('pylibjpeg')
_logger = logging.getLogger("pylibjpeg")
_logger.addHandler(logging.NullHandler())
_logger.debug("pylibjpeg v{}".format(__version__))


def debug_logger():
"""Setup the logging for debugging."""
logger = logging.getLogger('pylibjpeg')
logger = logging.getLogger("pylibjpeg")
logger.handlers = []
handler = logging.StreamHandler()
logger.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(levelname).1s: %(message)s')
formatter = logging.Formatter("%(levelname).1s: %(message)s")
handler.setFormatter(formatter)
logger.addHandler(handler)
14 changes: 8 additions & 6 deletions pylibjpeg/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re


__version__ = '1.4.0'
__version__ = "1.4.0"


VERSION_PATTERN = r"""
Expand Down Expand Up @@ -41,11 +41,13 @@
def is_canonical(version):
"""Return True if `version` is a PEP440 conformant version."""
match = re.match(
r'^([1-9]\d*!)?(0|[1-9]\d*)'
r'(\.(0|[1-9]\d*))'
r'*((a|b|rc)(0|[1-9]\d*))'
r'?(\.post(0|[1-9]\d*))'
r'?(\.dev(0|[1-9]\d*))?$', version)
r"^([1-9]\d*!)?(0|[1-9]\d*)"
r"(\.(0|[1-9]\d*))"
r"*((a|b|rc)(0|[1-9]\d*))"
r"?(\.post(0|[1-9]\d*))"
r"?(\.dev(0|[1-9]\d*))?$",
version,
)

return match is not None

Expand Down
37 changes: 18 additions & 19 deletions pylibjpeg/pydicom/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ def generate_frames(ds):
try:
import pydicom
except ImportError:
raise RuntimeError(
"'generate_frames' requires the pydicom package"
)
raise RuntimeError("'generate_frames' requires the pydicom package")

from pydicom.encaps import generate_pixel_data_frame
from pydicom.pixel_data_handlers.util import pixel_dtype
Expand All @@ -41,7 +39,7 @@ def generate_frames(ds):
decode = decoders[ds.file_meta.TransferSyntaxUID]

p_interp = ds.PhotometricInterpretation
nr_frames = getattr(ds, 'NumberOfFrames', 1)
nr_frames = getattr(ds, "NumberOfFrames", 1)
for frame in generate_pixel_data_frame(ds.PixelData, nr_frames):
arr = decode(frame, ds.group_dataset(0x0028)).view(pixel_dtype(ds))
yield reshape_frame(ds, arr)
Expand Down Expand Up @@ -121,16 +119,16 @@ def reshape_frame(ds, arr):
"""
# Transfer Syntax UIDs that are always Planar Configuration 0
conf_zero = [
'1.2.840.10008.1.2.4.50',
'1.2.840.10008.1.2.4.57',
'1.2.840.10008.1.2.4.70',
'1.2.840.10008.1.2.4.90',
'1.2.840.10008.1.2.4.91'
"1.2.840.10008.1.2.4.50",
"1.2.840.10008.1.2.4.57",
"1.2.840.10008.1.2.4.70",
"1.2.840.10008.1.2.4.90",
"1.2.840.10008.1.2.4.91",
]
# Transfer Syntax UIDs that are always Planar Configuration 1
conf_one = [
'1.2.840.10008.1.2.4.80',
'1.2.840.10008.1.2.4.81',
"1.2.840.10008.1.2.4.80",
"1.2.840.10008.1.2.4.81",
]

# Valid values for Planar Configuration are dependent on transfer syntax
Expand All @@ -147,8 +145,9 @@ def reshape_frame(ds, arr):
if planar_configuration not in [0, 1]:
raise ValueError(
"Unable to reshape the pixel array as a value of {} for "
"(0028,0006) 'Planar Configuration' is invalid."
.format(planar_configuration)
"(0028,0006) 'Planar Configuration' is invalid.".format(
planar_configuration
)
)

if nr_samples == 1:
Expand Down Expand Up @@ -187,22 +186,22 @@ def get_j2k_parameters(codestream):
"""
try:
# First 2 bytes must be the SOC marker - if not then wrong format
if codestream[0:2] != b'\xff\x4f':
if codestream[0:2] != b"\xff\x4f":
return {}

# SIZ is required to be the second marker - Figure A-3 in 15444-1
if codestream[2:4] != b'\xff\x51':
if codestream[2:4] != b"\xff\x51":
return {}

# See 15444-1 A.5.1 for format of the SIZ box and contents
ssiz = ord(codestream[42:43])
parameters = {}
if ssiz & 0x80:
parameters['precision'] = (ssiz & 0x7F) + 1
parameters['is_signed'] = True
parameters["precision"] = (ssiz & 0x7F) + 1
parameters["is_signed"] = True
else:
parameters['precision'] = ssiz + 1
parameters['is_signed'] = False
parameters["precision"] = ssiz + 1
parameters["is_signed"] = False

return parameters

Expand Down
8 changes: 4 additions & 4 deletions pylibjpeg/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@

import logging
import sys

_logger = logging.getLogger(__name__)

try:
import ljdata as _data
globals()['data'] = _data

globals()["data"] = _data
# Add to cache - needed for pytest
sys.modules['pylibjpeg.data'] = _data
_logger.debug('pylibjpeg-data module loaded')
sys.modules["pylibjpeg.data"] = _data
_logger.debug("pylibjpeg-data module loaded")
except ImportError:
pass
Loading

0 comments on commit bc25035

Please sign in to comment.