Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
comment:
layout: "diff, files"
behavior: default
require_changes: false # if true: only post the comment if coverage changes
require_base: yes # [yes :: must have a base report to post]
require_head: yes # [yes :: must have a head report to post]
branches: # branch names that can post comment
- "master"

coverage:
status:
project:
default:
target: auto
threshold: 0.1%
patch:
default:
target: auto
threshold: 0.1%

ignore:
- "rle/tests"
- "rle/src"
- "rle/benchmarks"
15 changes: 13 additions & 2 deletions .github/workflows/pytest-builds.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,18 @@ on:

jobs:
pytest:
runs-on: ubuntu-latest
timeout-minutes: 30
runs-on: ${{ matrix.os }}
timeout-minutes: 10
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8, 3.9]
os: [ubuntu-latest, windows-latest, macos-latest]

steps:
- uses: actions/checkout@v2
with:
fetch-depth: 2

- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
Expand All @@ -39,3 +42,11 @@ jobs:
PYTHON_VERSION: ${{ matrix.python-version }}
run: |
pytest --cov rle

- name: Send coverage results
if: ${{ success() && matrix.os == 'ubuntu-latest' && matrix.python-version == '3.9' }}
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))
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2020 scaramallion
Copyright (c) 2020-2021 scaramallion

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

## pylibjpeg-rle

A fast DICOM RLE decoding plugin for pylibjpeg, written in Rust with a Python 3.6+ wrapper.
A fast DICOM ([PackBits](https://en.wikipedia.org/wiki/PackBits)) RLE plugin for [pylibjpeg](https://github.com/pydicom/pylibjpeg) RLE decoding plugin for pylibjpeg, written in Rust with a Python 3.6+ wrapper.

Linux, OSX and Windows are all supported.

Expand All @@ -22,10 +22,10 @@ python -m setup.py develop
```

### Supported Transfer Syntaxes
#### Decoding
| UID | Description |
| --- | --- |
| 1.2.840.10008.1.2.5 | RLE Lossless |

| UID | Description | Decoding | Encoding |
| --- | --- | --- | --- |
| 1.2.840.10008.1.2.5 | RLE Lossless | Yes | No |

### Benchmarks
#### Decoding
Expand All @@ -48,10 +48,11 @@ Time per 1000 decodes, pydicom's NumPy RLE handler vs. pylibjpeg-rle
| SC_rgb_rle_32bit_2frame.dcm | 20,000 | 240,000 | 1.03 s | 0.28 s |

### Usage
#### With pylibjpeg and pydicom
#### Decoding
##### With pylibjpeg

Because pydicom defaults to the NumPy RLE decoder, you must specify the use
of pylibjpeg when decompressing:
of pylibjpeg when decompressing (**note: requires pydicom v2.2+**):
*Pixel Data*:
```python
from pydicom import dcmread
Expand All @@ -62,6 +63,7 @@ ds.decompress("pylibjpeg")
arr = ds.pixel_array
```

#### Standalone with pydicom
Alternatively you can use the included functions to decode a given dataset:
```python
from rle import pixel_array, generate_frames
Expand Down
11 changes: 11 additions & 0 deletions docs/release_notes/v1.0.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.. _v1.0.0:

1.0.0
=====

Enhancements
............

* Initial release
* Added support for decoding *RLE Lossless* encoded *Pixel Data*
* Added support for use of a plugin with pylibjpeg
2 changes: 1 addition & 1 deletion rle/_version.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import re


__version__ = '1.0.0.dev0'
__version__ = '1.0.0'


VERSION_PATTERN = r"""
Expand Down
39 changes: 5 additions & 34 deletions rle/tests/test_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

try:
from pydicom import dcmread
from pydicom.encaps import generate_pixel_data_frame
from pydicom.uid import RLELossless
HAVE_PYDICOM = True
except ImportError:
Expand All @@ -29,27 +30,12 @@ def test_u8_1s_1f(self):
assert 800 == ds.Columns
assert 1 == getattr(ds, 'NumberOfFrames', 1)

arr = decode_pixel_data(ds.PixelData, ds)
frame = next(generate_pixel_data_frame(ds.PixelData))
arr = decode_pixel_data(frame, ds)
assert (480000, ) == arr.shape
assert arr.flags.writeable
assert 'uint8' == arr.dtype

def test_u8_1s_2f(self):
"""Test plugin decoder for 8 bit, 1 sample, 2 frame data."""
ds = INDEX["OBXXXX1A_rle_2frame.dcm"]['ds']
assert ds.file_meta.TransferSyntaxUID == RLELossless
assert 8 == ds.BitsAllocated
assert 1 == ds.SamplesPerPixel
assert 0 == ds.PixelRepresentation
assert 600 == ds.Rows
assert 800 == ds.Columns
assert 2 == getattr(ds, 'NumberOfFrames', 1)

arr = decode_pixel_data(ds.PixelData, ds)
assert (960000, ) == arr.shape
assert arr.flags.writeable
assert 'uint8' == arr.dtype

def test_u32_3s_1f(self):
"""Test plugin decoder for 32 bit, 3 sample, 1 frame data."""
ds = INDEX["SC_rgb_rle_32bit.dcm"]['ds']
Expand All @@ -61,23 +47,8 @@ def test_u32_3s_1f(self):
assert 100 == ds.Columns
assert 1 == getattr(ds, 'NumberOfFrames', 1)

arr = decode_pixel_data(ds.PixelData, ds)
frame = next(generate_pixel_data_frame(ds.PixelData))
arr = decode_pixel_data(frame, ds)
assert (120000, ) == arr.shape
assert arr.flags.writeable
assert 'uint8' == arr.dtype

def test_u32_3s_2f(self):
"""Test plugin decoder for 32 bit, 3 sample, 2 frame data."""
ds = INDEX["SC_rgb_rle_32bit_2frame.dcm"]['ds']
assert ds.file_meta.TransferSyntaxUID == RLELossless
assert 32 == ds.BitsAllocated
assert 3 == ds.SamplesPerPixel
assert 0 == ds.PixelRepresentation
assert 100 == ds.Rows
assert 100 == ds.Columns
assert 2 == getattr(ds, 'NumberOfFrames', 1)

arr = decode_pixel_data(ds.PixelData, ds)
assert (240000, ) == arr.shape
assert arr.flags.writeable
assert 'uint8' == arr.dtype
29 changes: 6 additions & 23 deletions rle/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,43 +14,26 @@ def decode_pixel_data(stream: bytes, ds: "Dataset") -> "np.ndarray":
Parameters
----------
stream : bytes
The contents of the dataset's *Pixel Data*.
The image frame to be decoded.
ds : pydicom.dataset.Dataset
A :class:`~pydicom.dataset.Dataset` containing the group ``0x0028``
elements corresponding to the *Pixel Data*.

Returns
-------
numpy.ndarray
A 1D array of ``numpy.uint8`` containing the decoded image data,
A 1D array of ``numpy.uint8`` containing the decoded frame data,
with big-endian encoding and planar configuration 1.

Raises
------
ValueError
If the decoding failed.
"""
from pydicom.encaps import generate_pixel_data_frame
from pydicom.pixel_data_handlers.util import get_expected_length
from pydicom.uid import RLELossless

nr_frames = getattr(ds, "NumberOfFrames", 1)
r, c = ds.Rows, ds.Columns
bpp = ds.BitsAllocated

expected_len = get_expected_length(ds, 'bytes')
frame_len = expected_len // getattr(ds, "NumberOfFrames", 1)
# Empty destination array for our decoded pixel data
arr = np.empty(expected_len, dtype='uint8')

generate_frames = generate_pixel_data_frame(ds.PixelData, nr_frames)
generate_offsets = range(0, expected_len, frame_len)
for frame, offset in zip(generate_frames, generate_offsets):
arr[offset:offset + frame_len] = np.frombuffer(
decode_frame(frame, r * c, bpp), dtype='uint8'
)

return arr
return np.frombuffer(
decode_frame(stream, ds.Rows * ds.Columns, ds.BitsAllocated),
dtype='uint8'
)


def generate_frames(
Expand Down
7 changes: 2 additions & 5 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
// https://pyo3.rs/v0.13.2/conversions/tables.html
// bytes -> &[u8] or Vec<u8>
// bytearray -> Vec<u8>
// list[T] -> Vec<T>

use std::error::Error;
use std::convert::TryFrom;
Expand Down Expand Up @@ -105,7 +101,8 @@ fn _decode_frame(
px_per_sample
The number of pixels per sample (rows x columns), maximum (2^32 - 1).
bits_per_px
The number of bits per pixel, should be a multiple of 8 and no larger than 64.
The number of bits per pixel, should be a multiple of 8 and no larger
than 64.
*/

// Pre-define our errors for neatness
Expand Down