Skip to content

Commit e8fbb60

Browse files
authored
Add RLE encoding (#5)
1 parent 006052e commit e8fbb60

File tree

10 files changed

+1720
-330
lines changed

10 files changed

+1720
-330
lines changed

README.md

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,33 +25,13 @@ python -m setup.py develop
2525

2626
| UID | Description | Decoding | Encoding |
2727
| --- | --- | --- | --- |
28-
| 1.2.840.10008.1.2.5 | RLE Lossless | Yes | No |
29-
30-
### Benchmarks
31-
#### Decoding
32-
33-
Time per 1000 decodes, pydicom's default RLE handler vs. pylibjpeg-rle
34-
35-
| Dataset | Pixels | Bytes | pydicom | pylibjpeg-rle |
36-
| --- | --- | --- | --- | --- |
37-
| OBXXXX1A_rle.dcm | 480,000 | 480,000 | 4.89 s | 0.79 s |
38-
| OBXXXX1A_rle_2frame.dcm | 960,000 | 960,000 | 9.89 s | 1.65 s |
39-
| SC_rgb_rle.dcm | 10,000 | 30,000 | 0.20 s | 0.15 s |
40-
| SC_rgb_rle_2frame.dcm | 20,000 | 60,000 | 0.32 s | 0.18 s |
41-
| MR_small_RLE.dcm | 4,096 | 8,192 | 0.35 s | 0.13 s |
42-
| emri_small_RLE.dcm | 40,960 | 81,920 | 1.13 s | 0.28 s |
43-
| SC_rgb_rle_16bit.dcm | 10,000 | 60,000 | 0.33 s | 0.17 s |
44-
| SC_rgb_rle_16bit_2frame.dcm | 20,000 | 120,000 | 0.56 s | 0.21 s |
45-
| rtdose_rle_1frame.dcm | 100 | 400 | 0.12 s | 0.13 s |
46-
| rtdose_rle.dcm | 1,500 | 6,000 | 0.53 s | 0.26 s |
47-
| SC_rgb_rle_32bit.dcm | 10,000 | 120,000 | 0.56 s | 0.19 s |
48-
| SC_rgb_rle_32bit_2frame.dcm | 20,000 | 240,000 | 1.03 s | 0.28 s |
28+
| 1.2.840.10008.1.2.5 | RLE Lossless | Yes | Yes |
4929

5030
### Usage
5131
#### Decoding
5232
##### With pylibjpeg
5333

54-
Because pydicom defaults to its own RLE decoder, you must specify the use
34+
Because pydicom defaults to its own RLE decoder you must specify the use
5535
of pylibjpeg when decompressing:
5636
```python
5737
from pydicom import dcmread
@@ -75,3 +55,58 @@ arr = pixel_array(ds)
7555
for arr in generate_frames(ds):
7656
print(arr.shape)
7757
```
58+
59+
#### Encoding
60+
##### Standalone with pydicom
61+
62+
Convert uncompressed pixel data to RLE encoding and save:
63+
```python
64+
from pydicom import dcmread
65+
from pydicom.data import get_testdata_file
66+
from pydicom.uid import RLELossless
67+
68+
from rle import pixel_data
69+
70+
# Get the uncompressed pixel data
71+
ds = dcmread(get_testdata_file("OBXXXX1A.dcm"))
72+
arr = ds.pixel_array
73+
74+
# RLE encode and encapsulate `arr`
75+
ds.PixelData = pixel_data(arr, ds)
76+
# Set the correct *Transfer Syntax UID*
77+
ds.file_meta.TransferSyntaxUID = RLELossless
78+
ds.save_as('as_rle.dcm')
79+
```
80+
81+
### Benchmarks
82+
#### Decoding
83+
84+
Time per 1000 decodes, pydicom's default RLE handler vs. pylibjpeg-rle
85+
86+
| Dataset | Pixels | Bytes | pydicom | pylibjpeg-rle |
87+
| --- | --- | --- | --- | --- |
88+
| OBXXXX1A_rle.dcm | 480,000 | 480,000 | 4.89 s | 0.79 s |
89+
| OBXXXX1A_rle_2frame.dcm | 960,000 | 960,000 | 9.89 s | 1.65 s |
90+
| SC_rgb_rle.dcm | 10,000 | 30,000 | 0.20 s | 0.15 s |
91+
| SC_rgb_rle_2frame.dcm | 20,000 | 60,000 | 0.32 s | 0.18 s |
92+
| MR_small_RLE.dcm | 4,096 | 8,192 | 0.35 s | 0.13 s |
93+
| emri_small_RLE.dcm | 40,960 | 81,920 | 1.13 s | 0.28 s |
94+
| SC_rgb_rle_16bit.dcm | 10,000 | 60,000 | 0.33 s | 0.17 s |
95+
| SC_rgb_rle_16bit_2frame.dcm | 20,000 | 120,000 | 0.56 s | 0.21 s |
96+
| rtdose_rle_1frame.dcm | 100 | 400 | 0.12 s | 0.13 s |
97+
| rtdose_rle.dcm | 1,500 | 6,000 | 0.53 s | 0.26 s |
98+
| SC_rgb_rle_32bit.dcm | 10,000 | 120,000 | 0.56 s | 0.19 s |
99+
| SC_rgb_rle_32bit_2frame.dcm | 20,000 | 240,000 | 1.03 s | 0.28 s |
100+
101+
#### Encoding
102+
103+
Time per 1000 encodes, pydicom's default RLE handler vs. pylibjpeg-rle
104+
105+
| Dataset | Pixels | Bytes | NumPy | pylibjpeg-rle |
106+
| --- | --- | --- | --- | --- |
107+
| OBXXXX1A.dcm | 480,000 | 480,000 | 30.7 s | 1.36 s |
108+
| SC_rgb.dcm | 10,000 | 30,000 | 1.80 s | 0.09 s |
109+
| MR_small.dcm | 4,096 | 8,192 | 2.29 s | 0.04 s |
110+
| SC_rgb_16bit.dcm | 10,000 | 60,000 | 3.57 s | 0.17 s |
111+
| rtdose_1frame.dcm | 100 | 400 | 0.19 s | 0.003 s |
112+
| SC_rgb_32bit.dcm | 10,000 | 120,000 | 7.20 s | 0.33 s |

docs/release_notes/v1.1.0.rst

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
.. _v1.1.0:
2+
3+
1.1.0
4+
=====
5+
6+
Enhancements
7+
............
8+
9+
* Added support for *RLE Lossless* encoding of *Pixel Data*
10+
* Added :func:`~rle.utils.encode_array` generator for standalone encoding
11+
* Added :func:`~rle.utils.pixel_data` function for encoding and encapsulating
12+
a numpy ndarray
13+
* Added :func:`~rle.utils.encode_pixel_data` entry point for encoding
14+
* Added the ability to return decoded data in either little or big endian
15+
ordering
16+
17+
Changes
18+
.......
19+
20+
* :func:`~rle.utils.pixel_array`, :func:`~rle.utils.generate_frames` and
21+
:func:`~rle.utils.decode_pixel_data` now return little-endian ordered
22+
ndarrays by default

rle/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import re
44

55

6-
__version__ = '1.0.0'
6+
__version__ = '1.1.0'
77

88

99
VERSION_PATTERN = r"""

rle/benchmarks/bench_encode.py

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
2+
import asv
3+
import timeit
4+
5+
from pydicom import dcmread
6+
from pydicom.data import get_testdata_file
7+
from pydicom.encaps import generate_pixel_data_frame
8+
from pydicom.pixel_data_handlers.rle_handler import (
9+
get_pixeldata, _rle_decode_frame, _rle_encode_row, rle_encode_frame
10+
)
11+
from pydicom.pixel_data_handlers.util import reshape_pixel_array
12+
from pydicom.uid import RLELossless
13+
14+
from ljdata import get_indexed_datasets
15+
from rle.utils import pixel_array, decode_frame
16+
from rle._rle import encode_row, encode_frame
17+
18+
INDEX = get_indexed_datasets(RLELossless)
19+
# 8/8-bit, 1 sample/pixel, 1 frame
20+
EXPL_8_1_1F = get_testdata_file("OBXXXX1A.dcm")
21+
# 8/8-bit, 3 sample/pixel, 1 frame
22+
EXPL_8_3_1F = get_testdata_file("SC_rgb.dcm")
23+
# 16/16-bit, 1 sample/pixel, 1 frame
24+
EXPL_16_1_1F = get_testdata_file("MR_small.dcm")
25+
# 16/16-bit, 3 sample/pixel, 1 frame
26+
EXPL_16_3_1F = get_testdata_file("SC_rgb_16bit.dcm")
27+
# 32/32-bit, 1 sample/pixel, 1 frame
28+
EXPL_32_1_1F = get_testdata_file("rtdose_1frame.dcm")
29+
# 32/32-bit, 3 sample/pixel, 1 frame
30+
EXPL_32_3_1F = get_testdata_file("SC_rgb_32bit.dcm")
31+
32+
33+
class TimeEncodeRow:
34+
def setup(self):
35+
self.no_runs = 100000
36+
ds = u8_1s_1f_rle
37+
arr = ds.pixel_array
38+
self.row_arr = arr[0, :].ravel()
39+
self.row = self.row_arr.tobytes()
40+
41+
def time_rle(self):
42+
for _ in range(self.no_runs):
43+
encode_row(self.row)
44+
45+
def time_default(self):
46+
for _ in range(self.no_runs):
47+
_rle_encode_row(self.row_arr)
48+
49+
50+
class TimePYDEncodeFrame:
51+
"""Time tests for rle_handler.rle_encode_frame."""
52+
def setup(self):
53+
ds = dcmread(EXPL_8_1_1F)
54+
self.arr8_1 = ds.pixel_array
55+
ds = dcmread(EXPL_8_3_1F)
56+
self.arr8_3 = ds.pixel_array
57+
ds = dcmread(EXPL_16_1_1F)
58+
self.arr16_1 = ds.pixel_array
59+
ds = dcmread(EXPL_16_3_1F)
60+
self.arr16_3 = ds.pixel_array
61+
ds = dcmread(EXPL_32_1_1F)
62+
self.arr32_1 = ds.pixel_array
63+
ds = dcmread(EXPL_32_3_1F)
64+
self.arr32_3 = ds.pixel_array
65+
66+
self.no_runs = 1000
67+
68+
def time_08_1(self):
69+
"""Time encoding 8 bit 1 sample/pixel."""
70+
for ii in range(self.no_runs):
71+
rle_encode_frame(self.arr8_1)
72+
73+
def time_08_3(self):
74+
"""Time encoding 8 bit 3 sample/pixel."""
75+
for ii in range(self.no_runs):
76+
rle_encode_frame(self.arr8_3)
77+
78+
def time_16_1(self):
79+
"""Time encoding 16 bit 1 sample/pixel."""
80+
for ii in range(self.no_runs):
81+
rle_encode_frame(self.arr16_1)
82+
83+
def time_16_3(self):
84+
"""Time encoding 16 bit 3 sample/pixel."""
85+
for ii in range(self.no_runs):
86+
rle_encode_frame(self.arr16_3)
87+
88+
def time_32_1(self):
89+
"""Time encoding 32 bit 1 sample/pixel."""
90+
for ii in range(self.no_runs):
91+
rle_encode_frame(self.arr32_1)
92+
93+
def time_32_3(self):
94+
"""Time encoding 32 bit 3 sample/pixel."""
95+
for ii in range(self.no_runs):
96+
rle_encode_frame(self.arr32_3)
97+
98+
99+
class TimeRLEEncodeFrame:
100+
def setup(self):
101+
ds = dcmread(EXPL_8_1_1F)
102+
self.ds8_1 = (
103+
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
104+
)
105+
ds = dcmread(EXPL_8_3_1F)
106+
self.ds8_3 = (
107+
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
108+
)
109+
ds = dcmread(EXPL_16_1_1F)
110+
self.ds16_1 = (
111+
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
112+
)
113+
ds = dcmread(EXPL_16_3_1F)
114+
self.ds16_3 = (
115+
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
116+
)
117+
ds = dcmread(EXPL_32_1_1F)
118+
self.ds32_1 = (
119+
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
120+
)
121+
ds = dcmread(EXPL_32_3_1F)
122+
self.ds32_3 = (
123+
ds.PixelData, ds.Rows, ds.Columns, ds.SamplesPerPixel, ds.BitsAllocated,
124+
)
125+
126+
self.no_runs = 1000
127+
128+
def time_08_1(self):
129+
"""Time encoding 8 bit 1 sample/pixel."""
130+
for ii in range(self.no_runs):
131+
encode_frame(*self.ds8_1, '<')
132+
133+
def time_08_3(self):
134+
"""Time encoding 8 bit 3 sample/pixel."""
135+
for ii in range(self.no_runs):
136+
encode_frame(*self.ds8_3, '<')
137+
138+
def time_16_1(self):
139+
"""Time encoding 16 bit 1 sample/pixel."""
140+
for ii in range(self.no_runs):
141+
encode_frame(*self.ds16_1, '<')
142+
143+
def time_16_3(self):
144+
"""Time encoding 16 bit 3 sample/pixel."""
145+
for ii in range(self.no_runs):
146+
encode_frame(*self.ds16_3, '<')
147+
148+
def time_32_1(self):
149+
"""Time encoding 32 bit 1 sample/pixel."""
150+
for ii in range(self.no_runs):
151+
encode_frame(*self.ds32_1, '<')
152+
153+
def time_32_3(self):
154+
"""Time encoding 32 bit 3 sample/pixel."""
155+
for ii in range(self.no_runs):
156+
encode_frame(*self.ds32_3, '<')

0 commit comments

Comments
 (0)