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
79 changes: 57 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,33 +25,13 @@ python -m setup.py develop

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

### Benchmarks
#### Decoding

Time per 1000 decodes, pydicom's default RLE handler vs. pylibjpeg-rle

| Dataset | Pixels | Bytes | pydicom | pylibjpeg-rle |
| --- | --- | --- | --- | --- |
| OBXXXX1A_rle.dcm | 480,000 | 480,000 | 4.89 s | 0.79 s |
| OBXXXX1A_rle_2frame.dcm | 960,000 | 960,000 | 9.89 s | 1.65 s |
| SC_rgb_rle.dcm | 10,000 | 30,000 | 0.20 s | 0.15 s |
| SC_rgb_rle_2frame.dcm | 20,000 | 60,000 | 0.32 s | 0.18 s |
| MR_small_RLE.dcm | 4,096 | 8,192 | 0.35 s | 0.13 s |
| emri_small_RLE.dcm | 40,960 | 81,920 | 1.13 s | 0.28 s |
| SC_rgb_rle_16bit.dcm | 10,000 | 60,000 | 0.33 s | 0.17 s |
| SC_rgb_rle_16bit_2frame.dcm | 20,000 | 120,000 | 0.56 s | 0.21 s |
| rtdose_rle_1frame.dcm | 100 | 400 | 0.12 s | 0.13 s |
| rtdose_rle.dcm | 1,500 | 6,000 | 0.53 s | 0.26 s |
| SC_rgb_rle_32bit.dcm | 10,000 | 120,000 | 0.56 s | 0.19 s |
| SC_rgb_rle_32bit_2frame.dcm | 20,000 | 240,000 | 1.03 s | 0.28 s |
| 1.2.840.10008.1.2.5 | RLE Lossless | Yes | Yes |

### Usage
#### Decoding
##### With pylibjpeg

Because pydicom defaults to its own RLE decoder, you must specify the use
Because pydicom defaults to its own RLE decoder you must specify the use
of pylibjpeg when decompressing:
```python
from pydicom import dcmread
Expand All @@ -75,3 +55,58 @@ arr = pixel_array(ds)
for arr in generate_frames(ds):
print(arr.shape)
```

#### Encoding
##### Standalone with pydicom

Convert uncompressed pixel data to RLE encoding and save:
```python
from pydicom import dcmread
from pydicom.data import get_testdata_file
from pydicom.uid import RLELossless

from rle import pixel_data

# Get the uncompressed pixel data
ds = dcmread(get_testdata_file("OBXXXX1A.dcm"))
arr = ds.pixel_array

# RLE encode and encapsulate `arr`
ds.PixelData = pixel_data(arr, ds)
# Set the correct *Transfer Syntax UID*
ds.file_meta.TransferSyntaxUID = RLELossless
ds.save_as('as_rle.dcm')
```

### Benchmarks
#### Decoding

Time per 1000 decodes, pydicom's default RLE handler vs. pylibjpeg-rle

| Dataset | Pixels | Bytes | pydicom | pylibjpeg-rle |
| --- | --- | --- | --- | --- |
| OBXXXX1A_rle.dcm | 480,000 | 480,000 | 4.89 s | 0.79 s |
| OBXXXX1A_rle_2frame.dcm | 960,000 | 960,000 | 9.89 s | 1.65 s |
| SC_rgb_rle.dcm | 10,000 | 30,000 | 0.20 s | 0.15 s |
| SC_rgb_rle_2frame.dcm | 20,000 | 60,000 | 0.32 s | 0.18 s |
| MR_small_RLE.dcm | 4,096 | 8,192 | 0.35 s | 0.13 s |
| emri_small_RLE.dcm | 40,960 | 81,920 | 1.13 s | 0.28 s |
| SC_rgb_rle_16bit.dcm | 10,000 | 60,000 | 0.33 s | 0.17 s |
| SC_rgb_rle_16bit_2frame.dcm | 20,000 | 120,000 | 0.56 s | 0.21 s |
| rtdose_rle_1frame.dcm | 100 | 400 | 0.12 s | 0.13 s |
| rtdose_rle.dcm | 1,500 | 6,000 | 0.53 s | 0.26 s |
| SC_rgb_rle_32bit.dcm | 10,000 | 120,000 | 0.56 s | 0.19 s |
| SC_rgb_rle_32bit_2frame.dcm | 20,000 | 240,000 | 1.03 s | 0.28 s |

#### Encoding

Time per 1000 encodes, pydicom's default RLE handler vs. pylibjpeg-rle

| Dataset | Pixels | Bytes | NumPy | pylibjpeg-rle |
| --- | --- | --- | --- | --- |
| OBXXXX1A.dcm | 480,000 | 480,000 | 30.7 s | 1.36 s |
| SC_rgb.dcm | 10,000 | 30,000 | 1.80 s | 0.09 s |
| MR_small.dcm | 4,096 | 8,192 | 2.29 s | 0.04 s |
| SC_rgb_16bit.dcm | 10,000 | 60,000 | 3.57 s | 0.17 s |
| rtdose_1frame.dcm | 100 | 400 | 0.19 s | 0.003 s |
| SC_rgb_32bit.dcm | 10,000 | 120,000 | 7.20 s | 0.33 s |
22 changes: 22 additions & 0 deletions docs/release_notes/v1.1.0.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
.. _v1.1.0:

1.1.0
=====

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

* Added support for *RLE Lossless* encoding of *Pixel Data*
* Added :func:`~rle.utils.encode_array` generator for standalone encoding
* Added :func:`~rle.utils.pixel_data` function for encoding and encapsulating
a numpy ndarray
* Added :func:`~rle.utils.encode_pixel_data` entry point for encoding
* Added the ability to return decoded data in either little or big endian
ordering

Changes
.......

* :func:`~rle.utils.pixel_array`, :func:`~rle.utils.generate_frames` and
:func:`~rle.utils.decode_pixel_data` now return little-endian ordered
ndarrays by default
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'
__version__ = '1.1.0'


VERSION_PATTERN = r"""
Expand Down
156 changes: 156 additions & 0 deletions rle/benchmarks/bench_encode.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@

import asv
import timeit

from pydicom import dcmread
from pydicom.data import get_testdata_file
from pydicom.encaps import generate_pixel_data_frame
from pydicom.pixel_data_handlers.rle_handler import (
get_pixeldata, _rle_decode_frame, _rle_encode_row, rle_encode_frame
)
from pydicom.pixel_data_handlers.util import reshape_pixel_array
from pydicom.uid import RLELossless

from ljdata import get_indexed_datasets
from rle.utils import pixel_array, decode_frame
from rle._rle import encode_row, encode_frame

INDEX = get_indexed_datasets(RLELossless)
# 8/8-bit, 1 sample/pixel, 1 frame
EXPL_8_1_1F = get_testdata_file("OBXXXX1A.dcm")
# 8/8-bit, 3 sample/pixel, 1 frame
EXPL_8_3_1F = get_testdata_file("SC_rgb.dcm")
# 16/16-bit, 1 sample/pixel, 1 frame
EXPL_16_1_1F = get_testdata_file("MR_small.dcm")
# 16/16-bit, 3 sample/pixel, 1 frame
EXPL_16_3_1F = get_testdata_file("SC_rgb_16bit.dcm")
# 32/32-bit, 1 sample/pixel, 1 frame
EXPL_32_1_1F = get_testdata_file("rtdose_1frame.dcm")
# 32/32-bit, 3 sample/pixel, 1 frame
EXPL_32_3_1F = get_testdata_file("SC_rgb_32bit.dcm")


class TimeEncodeRow:
def setup(self):
self.no_runs = 100000
ds = u8_1s_1f_rle
arr = ds.pixel_array
self.row_arr = arr[0, :].ravel()
self.row = self.row_arr.tobytes()

def time_rle(self):
for _ in range(self.no_runs):
encode_row(self.row)

def time_default(self):
for _ in range(self.no_runs):
_rle_encode_row(self.row_arr)


class TimePYDEncodeFrame:
"""Time tests for rle_handler.rle_encode_frame."""
def setup(self):
ds = dcmread(EXPL_8_1_1F)
self.arr8_1 = ds.pixel_array
ds = dcmread(EXPL_8_3_1F)
self.arr8_3 = ds.pixel_array
ds = dcmread(EXPL_16_1_1F)
self.arr16_1 = ds.pixel_array
ds = dcmread(EXPL_16_3_1F)
self.arr16_3 = ds.pixel_array
ds = dcmread(EXPL_32_1_1F)
self.arr32_1 = ds.pixel_array
ds = dcmread(EXPL_32_3_1F)
self.arr32_3 = ds.pixel_array

self.no_runs = 1000

def time_08_1(self):
"""Time encoding 8 bit 1 sample/pixel."""
for ii in range(self.no_runs):
rle_encode_frame(self.arr8_1)

def time_08_3(self):
"""Time encoding 8 bit 3 sample/pixel."""
for ii in range(self.no_runs):
rle_encode_frame(self.arr8_3)

def time_16_1(self):
"""Time encoding 16 bit 1 sample/pixel."""
for ii in range(self.no_runs):
rle_encode_frame(self.arr16_1)

def time_16_3(self):
"""Time encoding 16 bit 3 sample/pixel."""
for ii in range(self.no_runs):
rle_encode_frame(self.arr16_3)

def time_32_1(self):
"""Time encoding 32 bit 1 sample/pixel."""
for ii in range(self.no_runs):
rle_encode_frame(self.arr32_1)

def time_32_3(self):
"""Time encoding 32 bit 3 sample/pixel."""
for ii in range(self.no_runs):
rle_encode_frame(self.arr32_3)


class TimeRLEEncodeFrame:
def setup(self):
ds = dcmread(EXPL_8_1_1F)
self.ds8_1 = (
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
)
ds = dcmread(EXPL_8_3_1F)
self.ds8_3 = (
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
)
ds = dcmread(EXPL_16_1_1F)
self.ds16_1 = (
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
)
ds = dcmread(EXPL_16_3_1F)
self.ds16_3 = (
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
)
ds = dcmread(EXPL_32_1_1F)
self.ds32_1 = (
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
)
ds = dcmread(EXPL_32_3_1F)
self.ds32_3 = (
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
)

self.no_runs = 1000

def time_08_1(self):
"""Time encoding 8 bit 1 sample/pixel."""
for ii in range(self.no_runs):
encode_frame(*self.ds8_1, '<')

def time_08_3(self):
"""Time encoding 8 bit 3 sample/pixel."""
for ii in range(self.no_runs):
encode_frame(*self.ds8_3, '<')

def time_16_1(self):
"""Time encoding 16 bit 1 sample/pixel."""
for ii in range(self.no_runs):
encode_frame(*self.ds16_1, '<')

def time_16_3(self):
"""Time encoding 16 bit 3 sample/pixel."""
for ii in range(self.no_runs):
encode_frame(*self.ds16_3, '<')

def time_32_1(self):
"""Time encoding 32 bit 1 sample/pixel."""
for ii in range(self.no_runs):
encode_frame(*self.ds32_1, '<')

def time_32_3(self):
"""Time encoding 32 bit 3 sample/pixel."""
for ii in range(self.no_runs):
encode_frame(*self.ds32_3, '<')
Loading