Skip to content

Commit f11e6ce

Browse files
authored
Add standalone JPEG decode (#35)
1 parent 6e0ff61 commit f11e6ce

File tree

16 files changed

+897
-442
lines changed

16 files changed

+897
-442
lines changed

.travis.yml

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,49 +7,71 @@ language: python
77
matrix:
88
include:
99
# No plugins + pydicom
10-
- name: "Python 3.6, Ubuntu"
10+
- name: "Python 3.6, Ubuntu + pydicom"
1111
os: linux
1212
dist: bionic
1313
python: "3.6"
1414
env:
1515
- INSTALL_PYDICOM=true
1616
- INSTALL_LIBJPEG=false
17-
- name: "Python 3.7, Ubuntu"
17+
- name: "Python 3.7, Ubuntu + pydicom"
1818
os: linux
1919
dist: bionic
2020
python: "3.7"
2121
env:
2222
- INSTALL_PYDICOM=true
2323
- INSTALL_LIBJPEG=false
24-
- name: "Python 3.8, Ubuntu"
24+
- name: "Python 3.8, Ubuntu + pydicom"
2525
os: linux
2626
dist: bionic
2727
python: "3.8"
2828
env:
2929
- INSTALL_PYDICOM=true
3030
- INSTALL_LIBJPEG=false
3131
# libjpeg + pydicom
32-
- name: "Python 3.6, Ubuntu + libjpeg"
32+
- name: "Python 3.6, Ubuntu + libjpeg + pydicom"
3333
os: linux
3434
dist: bionic
3535
python: "3.6"
3636
env:
3737
- INSTALL_PYDICOM=true
3838
- INSTALL_LIBJPEG=true
39-
- name: "Python 3.7, Ubuntu + libjpeg"
39+
- name: "Python 3.7, Ubuntu + libjpeg + pydicom"
4040
os: linux
4141
dist: bionic
4242
python: "3.7"
4343
env:
4444
- INSTALL_PYDICOM=true
4545
- INSTALL_LIBJPEG=true
46-
- name: "Python 3.8, Ubuntu + libjpeg"
46+
- name: "Python 3.8, Ubuntu + libjpeg + pydicom"
4747
os: linux
4848
dist: bionic
4949
python: "3.8"
5050
env:
5151
- INSTALL_PYDICOM=true
5252
- INSTALL_LIBJPEG=true
53+
# libjpeg standalone
54+
- name: "Python 3.6, Ubuntu + libjpeg"
55+
os: linux
56+
dist: bionic
57+
python: "3.6"
58+
env:
59+
- INSTALL_PYDICOM=false
60+
- INSTALL_LIBJPEG=true
61+
- name: "Python 3.7, Ubuntu + libjpeg"
62+
os: linux
63+
dist: bionic
64+
python: "3.7"
65+
env:
66+
- INSTALL_PYDICOM=false
67+
- INSTALL_LIBJPEG=true
68+
- name: "Python 3.8, Ubuntu + libjpeg"
69+
os: linux
70+
dist: bionic
71+
python: "3.8"
72+
env:
73+
- INSTALL_PYDICOM=false
74+
- INSTALL_LIBJPEG=true
5375

5476

5577
# Install dependencies and package

README.md

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,19 @@
33

44
## pylibjpeg
55

6-
A Python 3.6+ framework for decoding JPEG images, with a focus on providing
7-
support for [pydicom](https://github.com/pydicom/pydicom).
8-
9-
Linux, OSX and Windows are all supported.
6+
A Python 3.6+ framework for decoding JPEG images, with a focus on providing JPEG support for [pydicom](https://github.com/pydicom/pydicom).
107

118

129
### Installation
10+
#### Installing the current release
11+
12+
```
13+
pip install pylibjpeg
14+
```
15+
1316
#### Installing the development version
1417

15-
Make sure [Python](https://www.python.org/) and [Git](https://git-scm.com/) are installed.
18+
Make sure [Git](https://git-scm.com/) is installed, then
1619
```bash
1720
git clone https://github.com/pydicom/pylibjpeg
1821
python -m pip install pylibjpeg
@@ -21,11 +24,38 @@ python -m pip install pylibjpeg
2124
### Plugins
2225

2326
By itself *pylibjpeg* is unable to decode any JPEG images, which is where the
24-
plugins come in. To support the given JPEG format you'll first have to install
25-
the corresponding package:
27+
plugins come in. To support a given JPEG format or DICOM Transfer Syntax
28+
you first have to install the corresponding package:
29+
30+
#### JPEG Format
31+
| Format | Decode? | Encode? | Plugin | Based on |
32+
|---|------|---|---|---|
33+
| JPEG, JPEG-LS and JPEG XT | Yes | No | [pylibjpeg-libjpeg][1] | [libjpeg][2] |
34+
35+
#### Transfer Syntax
36+
37+
| UID | Description | Plugin |
38+
|---|---|----|
39+
| 1.2.840.10008.1.2.4.50 | JPEG Baseline (Process 1) | [pylibjpeg-libjpeg][1] |
40+
| 1.2.840.10008.1.2.4.51 | JPEG Extended (Process 2 and 4) | [pylibjpeg-libjpeg][1]|
41+
| 1.2.840.10008.1.2.4.57 | JPEG Lossless, Non-Hierarchical (Process 14) | [pylibjpeg-libjpeg][1]|
42+
| 1.2.840.10008.1.2.4.70 | JPEG Lossless, Non-Hierarchical, First-Order Prediction</br>(Process 14, Selection Value 1) | [pylibjpeg-libjpeg][1]|
43+
| 1.2.840.10008.1.2.4.80 | JPEG-LS Lossless | [pylibjpeg-libjpeg][1]|
44+
| 1.2.840.10008.1.2.4.81 | JPEG-LS Lossy (Near-Lossless) Image Compression | [pylibjpeg-libjpeg][1]|
45+
| 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | Not yet supported |
46+
| 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | Not yet supported |
47+
48+
If you're not sure what the dataset's *Transfer Syntax UID* is, it can be
49+
determined with:
50+
```python
51+
>>> from pydicom import dcmread
52+
>>> ds = dcmread('path/to/dicom_file')
53+
>>> ds.file_meta.TransferSyntaxUID.name
54+
```
55+
56+
[1]: https://github.com/pydicom/pylibjpeg-libjpeg
57+
[2]: https://github.com/thorfdbg/libjpeg
2658

27-
* JPEG, JPEG-LS and JPEG XT: [pylibjpeg-libjpeg](https://github.com/pydicom/pylibjpeg-libjpeg)
28-
* JPEG2000: To be implemented
2959

3060
### Usage
3161
#### With pydicom
@@ -35,19 +65,40 @@ Assuming you already have *pydicom* installed:
3565
from pydicom import dcmread
3666
from pydicom.data import get_testdata_file
3767

38-
# With the pylibjpeg-libjpeg plugin
68+
# With the pylibjpeg-libjpeg plugin installed
3969
import pylibjpeg
4070

4171
ds = dcmread(get_testdata_file('JPEG-LL.dcm'))
4272
arr = ds.pixel_array
4373
```
4474

45-
#### Standalone
75+
For datasets with multiple frames you can reduce your memory usage by
76+
processing each frame separately using the ``generate_frames()`` generator
77+
function:
78+
```python
79+
from pydicom import dcmread
80+
from pydicom.data import get_testdata_file
81+
82+
from pylibjpeg import generate_frames
83+
84+
ds = dcmread(get_testdata_file('color3d_jpeg_baseline.dcm'))
85+
frames = generate_frames(ds)
86+
arr = next(frames)
87+
```
4688

89+
#### Standalone JPEG decoding
90+
You can also just use *pylibjpeg* to decode JPEG images to a [numpy ndarray](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html), provided you have a suitable plugin installed:
4791
```python
4892
from pylibjpeg import decode
4993

94+
# Can decode using the path to a JPG file as str or pathlike
95+
arr = decode('filename.jpg')
96+
97+
# Or a file-like...
98+
with open('filename.jpg', 'rb') as f:
99+
arr = decode(f)
100+
101+
# Or bytes...
50102
with open('filename.jpg', 'rb') as f:
51-
# Returns a numpy array
52-
arr = decode(f.read())
103+
arr = decode(f.read())
53104
```

pylibjpeg/__init__.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
from ._version import __version__
88
from ._config import PLUGINS
99
from .plugins import load_plugins
10+
from .utils import decode
1011

1112

1213
# Setup default logging
13-
LOGGER = logging.getLogger('pynetdicom')
14-
LOGGER.addHandler(logging.NullHandler())
15-
LOGGER.debug("pylibjpeg v{}".format(__version__))
14+
_logger = logging.getLogger('pynetdicom')
15+
_logger.addHandler(logging.NullHandler())
16+
_logger.debug("pylibjpeg v{}".format(__version__))
1617

1718

1819
def debug_logger():
@@ -26,26 +27,16 @@ def debug_logger():
2627
logger.addHandler(handler)
2728

2829

29-
# TODO: remove this later
30-
debug_logger()
31-
32-
33-
try:
34-
import data as _data
35-
globals()['data'] = _data
36-
# Add to cache - needed for pytest
37-
sys.modules['pylibjpeg.data'] = _data
38-
LOGGER.debug('pylibjpeg-data module loaded')
39-
except ImportError:
40-
pass
41-
4230
load_plugins(PLUGINS)
4331

32+
33+
# Must be after loading the plugins
4434
from .utils import add_handler
4535

4636
try:
4737
import pydicom
4838
add_handler()
49-
LOGGER.debug('pydicom module loaded')
39+
_logger.debug('pydicom module loaded')
40+
from pylibjpeg.pydicom.utils import generate_frames
5041
except ImportError:
5142
pass

pylibjpeg/plugins.py

Lines changed: 27 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -10,40 +10,31 @@
1010
LOGGER = logging.getLogger(__name__)
1111

1212

13-
def load_plugins(plugins):
14-
"""Load the `plugins` and add them to the namespace."""
15-
for plugin in plugins:
13+
def get_decoders():
14+
"""Return a :class:`dict` of {plugin: decoder func}."""
15+
decoders = {}
16+
for name in get_plugins():
1617
try:
17-
LOGGER.debug("Importing {}".format(plugin))
18-
module = import_module(plugin)
19-
except ImportError as exc:
20-
LOGGER.debug("Failed to import {}".format(plugin))
21-
continue
22-
23-
# Add successful imported modules to the namespace
24-
globals()[plugin] = module
25-
sys.modules['pylibjpeg.plugins.{}'.format(plugin)] = module
18+
decoders[name] = getattr(globals()[name], 'decode')
19+
except AttributeError:
20+
pass
2621

22+
return decoders
2723

28-
def get_plugin_coders():
29-
"""Return the available plugin decoders and encoders.
3024

31-
Returns
32-
-------
33-
dict, dict
34-
A ``dict`` containing the available (decoders, encoders) as
35-
``{plugin name : {UID : callable}}``.
36-
"""
37-
decoders = {}
25+
def get_encoders():
26+
"""Return a :class:`dict` of {plugin: encoder func}."""
3827
encoders = {}
3928
for name in get_plugins():
40-
decoders[name] = getattr(globals()[name], 'DICOM_DECODERS')
41-
encoders[name] = getattr(globals()[name], 'DICOM_ENCODERS')
29+
try:
30+
encoders[name] = getattr(globals()[name], 'encode')
31+
except AttributeError:
32+
pass
4233

43-
return decoders, encoders
34+
return encoders
4435

4536

46-
def get_plugins(as_objects=False):
37+
def get_plugins():
4738
"""Return the available plugins.
4839
4940
Returns
@@ -54,78 +45,16 @@ def get_plugins(as_objects=False):
5445
return [nn for nn in PLUGINS if nn in globals()]
5546

5647

57-
def get_transfer_syntaxes(decodable=False, encodable=False):
58-
"""Return a list of decodable or encodable *Transfer Syntax UIDs*.
59-
60-
Parameters
61-
----------
62-
decodable : bool, optional
63-
Return a list of decodable *Transfer Syntax UIDs*.
64-
encodable : bool, optional
65-
Return a list of encodable *Transfer Syntax UIDs*.
66-
67-
Returns
68-
-------
69-
list of str
70-
A list containing unique *Transfer Syntax UIDs*.
71-
"""
72-
if not decodable and not encodable:
73-
raise ValueError("Either 'decodable' or 'encodable' must be True")
74-
75-
dec, enc = get_plugin_coders()
76-
if decodable:
77-
obj = dec
78-
else:
79-
obj = enc
80-
81-
uids = []
82-
for name, uid_coder in obj.items():
83-
uids += uid_coder.keys()
84-
85-
return list(set(uids))
86-
87-
88-
def get_decoder(uid):
89-
"""Return a callable function that can decode pixel data encoding using
90-
the *Transfer Syntax UID* `uid`.
91-
"""
92-
decoders, _ = get_plugin_coders()
93-
for name in decoders:
94-
try:
95-
return decoders[name][uid]
96-
except KeyError:
97-
pass
98-
99-
msg = (
100-
"No decoder is available for the Transfer Syntax UID - '{}'"
101-
.format(uid)
102-
)
103-
raise NotImplementedError(msg)
104-
105-
106-
def get_decoders():
107-
uids = get_transfer_syntaxes(decodable=True)
108-
decoders = {}
109-
dec, _ = get_plugin_coders()
110-
for name, uid_coder in dec.items():
111-
decoders.update(uid_coder)
112-
113-
return decoders
114-
115-
116-
def get_encoder(uid):
117-
"""Return a callable function that can encode pixel data using
118-
the *Transfer Syntax UID* `uid`.
119-
"""
120-
_, encoders = get_plugin_coders()
121-
for name in encoders:
48+
def load_plugins(plugins):
49+
"""Load the `plugins` and add them to the namespace."""
50+
for plugin in plugins:
12251
try:
123-
return encoders[name][uid]
124-
except KeyError:
125-
pass
52+
LOGGER.debug("Importing {}".format(plugin))
53+
module = import_module(plugin)
54+
except ImportError as exc:
55+
LOGGER.debug("Failed to import {}".format(plugin))
56+
continue
12657

127-
msg = (
128-
"No encoder is available for the Transfer Syntax UID - '{}'"
129-
.format(uid)
130-
)
131-
raise NotImplementedError(msg)
58+
# Add successful imported modules to the namespace
59+
globals()[plugin] = module
60+
sys.modules['pylibjpeg.plugins.{}'.format(plugin)] = module

0 commit comments

Comments
 (0)