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
34 changes: 28 additions & 6 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,49 +7,71 @@ language: python
matrix:
include:
# No plugins + pydicom
- name: "Python 3.6, Ubuntu"
- name: "Python 3.6, Ubuntu + pydicom"
os: linux
dist: bionic
python: "3.6"
env:
- INSTALL_PYDICOM=true
- INSTALL_LIBJPEG=false
- name: "Python 3.7, Ubuntu"
- name: "Python 3.7, Ubuntu + pydicom"
os: linux
dist: bionic
python: "3.7"
env:
- INSTALL_PYDICOM=true
- INSTALL_LIBJPEG=false
- name: "Python 3.8, Ubuntu"
- name: "Python 3.8, Ubuntu + pydicom"
os: linux
dist: bionic
python: "3.8"
env:
- INSTALL_PYDICOM=true
- INSTALL_LIBJPEG=false
# libjpeg + pydicom
- name: "Python 3.6, Ubuntu + libjpeg"
- name: "Python 3.6, Ubuntu + libjpeg + pydicom"
os: linux
dist: bionic
python: "3.6"
env:
- INSTALL_PYDICOM=true
- INSTALL_LIBJPEG=true
- name: "Python 3.7, Ubuntu + libjpeg"
- name: "Python 3.7, Ubuntu + libjpeg + pydicom"
os: linux
dist: bionic
python: "3.7"
env:
- INSTALL_PYDICOM=true
- INSTALL_LIBJPEG=true
- name: "Python 3.8, Ubuntu + libjpeg"
- name: "Python 3.8, Ubuntu + libjpeg + pydicom"
os: linux
dist: bionic
python: "3.8"
env:
- INSTALL_PYDICOM=true
- INSTALL_LIBJPEG=true
# libjpeg standalone
- name: "Python 3.6, Ubuntu + libjpeg"
os: linux
dist: bionic
python: "3.6"
env:
- INSTALL_PYDICOM=false
- INSTALL_LIBJPEG=true
- name: "Python 3.7, Ubuntu + libjpeg"
os: linux
dist: bionic
python: "3.7"
env:
- INSTALL_PYDICOM=false
- INSTALL_LIBJPEG=true
- name: "Python 3.8, Ubuntu + libjpeg"
os: linux
dist: bionic
python: "3.8"
env:
- INSTALL_PYDICOM=false
- INSTALL_LIBJPEG=true


# Install dependencies and package
Expand Down
77 changes: 64 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,19 @@

## pylibjpeg

A Python 3.6+ framework for decoding JPEG images, with a focus on providing
support for [pydicom](https://github.com/pydicom/pydicom).

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


### Installation
#### Installing the current release

```
pip install pylibjpeg
```

#### Installing the development version

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

By itself *pylibjpeg* is unable to decode any JPEG images, which is where the
plugins come in. To support the given JPEG format you'll first have to install
the corresponding package:
plugins come in. To support a given JPEG format or DICOM Transfer Syntax
you first have to install the corresponding package:

#### JPEG Format
| Format | Decode? | Encode? | Plugin | Based on |
|---|------|---|---|---|
| JPEG, JPEG-LS and JPEG XT | Yes | No | [pylibjpeg-libjpeg][1] | [libjpeg][2] |

#### Transfer Syntax

| UID | Description | Plugin |
|---|---|----|
| 1.2.840.10008.1.2.4.50 | JPEG Baseline (Process 1) | [pylibjpeg-libjpeg][1] |
| 1.2.840.10008.1.2.4.51 | JPEG Extended (Process 2 and 4) | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.57 | JPEG Lossless, Non-Hierarchical (Process 14) | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.70 | JPEG Lossless, Non-Hierarchical, First-Order Prediction</br>(Process 14, Selection Value 1) | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.80 | JPEG-LS Lossless | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.81 | JPEG-LS Lossy (Near-Lossless) Image Compression | [pylibjpeg-libjpeg][1]|
| 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | Not yet supported |
| 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | Not yet supported |

If you're not sure what the dataset's *Transfer Syntax UID* is, it can be
determined with:
```python
>>> from pydicom import dcmread
>>> ds = dcmread('path/to/dicom_file')
>>> ds.file_meta.TransferSyntaxUID.name
```

[1]: https://github.com/pydicom/pylibjpeg-libjpeg
[2]: https://github.com/thorfdbg/libjpeg

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

### Usage
#### With pydicom
Expand All @@ -35,19 +65,40 @@ Assuming you already have *pydicom* installed:
from pydicom import dcmread
from pydicom.data import get_testdata_file

# With the pylibjpeg-libjpeg plugin
# With the pylibjpeg-libjpeg plugin installed
import pylibjpeg

ds = dcmread(get_testdata_file('JPEG-LL.dcm'))
arr = ds.pixel_array
```

#### Standalone
For datasets with multiple frames you can reduce your memory usage by
processing each frame separately using the ``generate_frames()`` generator
function:
```python
from pydicom import dcmread
from pydicom.data import get_testdata_file

from pylibjpeg import generate_frames

ds = dcmread(get_testdata_file('color3d_jpeg_baseline.dcm'))
frames = generate_frames(ds)
arr = next(frames)
```

#### Standalone JPEG decoding
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:
```python
from pylibjpeg import decode

# Can decode using the path to a JPG file as str or pathlike
arr = decode('filename.jpg')

# Or a file-like...
with open('filename.jpg', 'rb') as f:
arr = decode(f)

# Or bytes...
with open('filename.jpg', 'rb') as f:
# Returns a numpy array
arr = decode(f.read())
arr = decode(f.read())
```
25 changes: 8 additions & 17 deletions pylibjpeg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from ._version import __version__
from ._config import PLUGINS
from .plugins import load_plugins
from .utils import decode


# Setup default logging
LOGGER = logging.getLogger('pynetdicom')
LOGGER.addHandler(logging.NullHandler())
LOGGER.debug("pylibjpeg v{}".format(__version__))
_logger = logging.getLogger('pynetdicom')
_logger.addHandler(logging.NullHandler())
_logger.debug("pylibjpeg v{}".format(__version__))


def debug_logger():
Expand All @@ -26,26 +27,16 @@ def debug_logger():
logger.addHandler(handler)


# TODO: remove this later
debug_logger()


try:
import data as _data
globals()['data'] = _data
# Add to cache - needed for pytest
sys.modules['pylibjpeg.data'] = _data
LOGGER.debug('pylibjpeg-data module loaded')
except ImportError:
pass

load_plugins(PLUGINS)


# Must be after loading the plugins
from .utils import add_handler

try:
import pydicom
add_handler()
LOGGER.debug('pydicom module loaded')
_logger.debug('pydicom module loaded')
from pylibjpeg.pydicom.utils import generate_frames
except ImportError:
pass
125 changes: 27 additions & 98 deletions pylibjpeg/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,40 +10,31 @@
LOGGER = logging.getLogger(__name__)


def load_plugins(plugins):
"""Load the `plugins` and add them to the namespace."""
for plugin in plugins:
def get_decoders():
"""Return a :class:`dict` of {plugin: decoder func}."""
decoders = {}
for name in get_plugins():
try:
LOGGER.debug("Importing {}".format(plugin))
module = import_module(plugin)
except ImportError as exc:
LOGGER.debug("Failed to import {}".format(plugin))
continue

# Add successful imported modules to the namespace
globals()[plugin] = module
sys.modules['pylibjpeg.plugins.{}'.format(plugin)] = module
decoders[name] = getattr(globals()[name], 'decode')
except AttributeError:
pass

return decoders

def get_plugin_coders():
"""Return the available plugin decoders and encoders.

Returns
-------
dict, dict
A ``dict`` containing the available (decoders, encoders) as
``{plugin name : {UID : callable}}``.
"""
decoders = {}
def get_encoders():
"""Return a :class:`dict` of {plugin: encoder func}."""
encoders = {}
for name in get_plugins():
decoders[name] = getattr(globals()[name], 'DICOM_DECODERS')
encoders[name] = getattr(globals()[name], 'DICOM_ENCODERS')
try:
encoders[name] = getattr(globals()[name], 'encode')
except AttributeError:
pass

return decoders, encoders
return encoders


def get_plugins(as_objects=False):
def get_plugins():
"""Return the available plugins.

Returns
Expand All @@ -54,78 +45,16 @@ def get_plugins(as_objects=False):
return [nn for nn in PLUGINS if nn in globals()]


def get_transfer_syntaxes(decodable=False, encodable=False):
"""Return a list of decodable or encodable *Transfer Syntax UIDs*.

Parameters
----------
decodable : bool, optional
Return a list of decodable *Transfer Syntax UIDs*.
encodable : bool, optional
Return a list of encodable *Transfer Syntax UIDs*.

Returns
-------
list of str
A list containing unique *Transfer Syntax UIDs*.
"""
if not decodable and not encodable:
raise ValueError("Either 'decodable' or 'encodable' must be True")

dec, enc = get_plugin_coders()
if decodable:
obj = dec
else:
obj = enc

uids = []
for name, uid_coder in obj.items():
uids += uid_coder.keys()

return list(set(uids))


def get_decoder(uid):
"""Return a callable function that can decode pixel data encoding using
the *Transfer Syntax UID* `uid`.
"""
decoders, _ = get_plugin_coders()
for name in decoders:
try:
return decoders[name][uid]
except KeyError:
pass

msg = (
"No decoder is available for the Transfer Syntax UID - '{}'"
.format(uid)
)
raise NotImplementedError(msg)


def get_decoders():
uids = get_transfer_syntaxes(decodable=True)
decoders = {}
dec, _ = get_plugin_coders()
for name, uid_coder in dec.items():
decoders.update(uid_coder)

return decoders


def get_encoder(uid):
"""Return a callable function that can encode pixel data using
the *Transfer Syntax UID* `uid`.
"""
_, encoders = get_plugin_coders()
for name in encoders:
def load_plugins(plugins):
"""Load the `plugins` and add them to the namespace."""
for plugin in plugins:
try:
return encoders[name][uid]
except KeyError:
pass
LOGGER.debug("Importing {}".format(plugin))
module = import_module(plugin)
except ImportError as exc:
LOGGER.debug("Failed to import {}".format(plugin))
continue

msg = (
"No encoder is available for the Transfer Syntax UID - '{}'"
.format(uid)
)
raise NotImplementedError(msg)
# Add successful imported modules to the namespace
globals()[plugin] = module
sys.modules['pylibjpeg.plugins.{}'.format(plugin)] = module
Loading