diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..856cdf6 --- /dev/null +++ b/.codecov.yml @@ -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" diff --git a/.github/workflows/pytest-builds.yml b/.github/workflows/pytest-builds.yml index 71ece02..0d10e57 100644 --- a/.github/workflows/pytest-builds.yml +++ b/.github/workflows/pytest-builds.yml @@ -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 @@ -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)) diff --git a/LICENSE b/LICENSE index a49252d..62f3da2 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index 160c35c..5544937 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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 @@ -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 diff --git a/docs/release_notes/v1.0.0.rst b/docs/release_notes/v1.0.0.rst index e69de29..0cdfe7a 100644 --- a/docs/release_notes/v1.0.0.rst +++ b/docs/release_notes/v1.0.0.rst @@ -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 diff --git a/rle/_version.py b/rle/_version.py index b58a61c..266adfa 100644 --- a/rle/_version.py +++ b/rle/_version.py @@ -3,7 +3,7 @@ import re -__version__ = '1.0.0.dev0' +__version__ = '1.0.0' VERSION_PATTERN = r""" diff --git a/rle/tests/test_handler.py b/rle/tests/test_handler.py index 4bf14cb..826e1ff 100644 --- a/rle/tests/test_handler.py +++ b/rle/tests/test_handler.py @@ -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: @@ -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'] @@ -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 diff --git a/rle/utils.py b/rle/utils.py index 66492fc..69b37d3 100644 --- a/rle/utils.py +++ b/rle/utils.py @@ -14,7 +14,7 @@ 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*. @@ -22,7 +22,7 @@ def decode_pixel_data(stream: bytes, ds: "Dataset") -> "np.ndarray": 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 @@ -30,27 +30,10 @@ def decode_pixel_data(stream: bytes, ds: "Dataset") -> "np.ndarray": 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( diff --git a/src/lib.rs b/src/lib.rs index 6bd9779..44b36f2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,3 @@ -// https://pyo3.rs/v0.13.2/conversions/tables.html -// bytes -> &[u8] or Vec -// bytearray -> Vec -// list[T] -> Vec use std::error::Error; use std::convert::TryFrom; @@ -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