├── .coveragerc ├── codecov.yml ├── openjpeg ├── py.typed ├── _version.py ├── tests │ ├── __init__.py │ ├── README.md │ ├── test_misc.py │ ├── test_parameters.py │ ├── test_decode.py │ └── test_handler.py ├── __init__.py ├── _openjpeg.pyx └── utils.py ├── .gitmodules ├── docs └── changes │ ├── v1.1.1.rst │ ├── v1.3.0.rst │ ├── v1.3.1.rst │ ├── v1.3.2.rst │ ├── v2.0.0.rst │ ├── v2.5.0.rst │ ├── v1.2.0.rst │ ├── v2.4.0.rst │ ├── v2.3.0.rst │ ├── v2.2.0.rst │ ├── v2.1.0.rst │ └── v1.1.0.rst ├── .codecov.yml ├── .github ├── dependabot.yml └── workflows │ ├── pytest-builds.yml │ └── release-wheels.yml ├── lib └── interface │ ├── utils.h │ ├── utils.c │ ├── decode.c │ └── encode.c ├── .gitignore ├── pyproject.toml ├── LICENSE ├── README.md └── poetry.lock /.coveragerc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /openjpeg/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | 2 | [submodule "lib/openjpeg"] 3 | path = lib/openjpeg 4 | url = https://github.com/uclouvain/openjpeg 5 | -------------------------------------------------------------------------------- /docs/changes/v1.1.1.rst: -------------------------------------------------------------------------------- 1 | .. _v1.1.1: 2 | 3 | 1.1.1 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Re-added support for Python 3.6 10 | -------------------------------------------------------------------------------- /docs/changes/v1.3.0.rst: -------------------------------------------------------------------------------- 1 | .. _v1.3.0: 2 | 3 | 1.3.0 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Updated OpenJPEG version to 2.5.0 10 | -------------------------------------------------------------------------------- /docs/changes/v1.3.1.rst: -------------------------------------------------------------------------------- 1 | .. _v1.3.1: 2 | 3 | 1.3.1 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Added wheels for Linux aarch64 architecture 10 | -------------------------------------------------------------------------------- /docs/changes/v1.3.2.rst: -------------------------------------------------------------------------------- 1 | .. _v1.3.2: 2 | 3 | 1.3.2 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Added wheels for Python 3.11 (contributed by James Meakin) 10 | -------------------------------------------------------------------------------- /openjpeg/_version.py: -------------------------------------------------------------------------------- 1 | """Version information based on PEP396 and 440.""" 2 | 3 | from importlib.metadata import version 4 | 5 | __version__: str = version("pylibjpeg-openjpeg") 6 | -------------------------------------------------------------------------------- /docs/changes/v2.0.0.rst: -------------------------------------------------------------------------------- 1 | .. _v2.0.0: 2 | 3 | 2.0.0 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * OpenJPEG version updated to v2.5.0 10 | * Supported Python versions are 3.8, 3.9, 3.10, 3.11 and 3.12 11 | * Added type hints 12 | -------------------------------------------------------------------------------- /docs/changes/v2.5.0.rst: -------------------------------------------------------------------------------- 1 | .. _v2.5.0: 2 | 3 | 2.5.0 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Bits above the precision are now ignored when encoding (:issue:`104`) 10 | * Supported Python versions are 3.9 to 3.14. 11 | * Update to openjpeg v2.5.3 12 | -------------------------------------------------------------------------------- /docs/changes/v1.2.0.rst: -------------------------------------------------------------------------------- 1 | .. _v1.2.0: 2 | 3 | 1.2.0 4 | ===== 5 | 6 | Fixes 7 | ..... 8 | 9 | * Fixed being unable to build from source using the PyPI package 10 | 11 | Changes 12 | ....... 13 | 14 | * Dropped support for Python 3.6 15 | * Added support for Python 3.10 16 | -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | comment: off 2 | 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | threshold: 0.1% 9 | patch: 10 | default: 11 | target: auto 12 | threshold: 0.1% 13 | 14 | ignore: 15 | - "openjpeg/tests" 16 | -------------------------------------------------------------------------------- /docs/changes/v2.4.0.rst: -------------------------------------------------------------------------------- 1 | .. _v2.4.0: 2 | 3 | 2.4.0 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Fixed decoding and encoding failures when running on big endian systems. 10 | * Supported Python versions are 3.9 to 3.13. 11 | * NumPy < 2.0 is no longer supported. 12 | * Switched to OpenJpeg v2.5.2 13 | -------------------------------------------------------------------------------- /openjpeg/tests/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | # Add the testing data to openjpeg (if available) 4 | try: 5 | import ljdata as _data 6 | 7 | globals()["data"] = _data 8 | # Add to cache - needed for pytest 9 | sys.modules["openjpeg.data"] = _data 10 | except ImportError: 11 | pass 12 | -------------------------------------------------------------------------------- /docs/changes/v2.3.0.rst: -------------------------------------------------------------------------------- 1 | .. _v2.3.0: 2 | 3 | 2.3.0 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Fixed using MCT with RGB in ``encode_pixel_data()`` 10 | * Removed using :class:`bytearray` as an image data source in ``encode_pixel_data()`` 11 | and ``encode_buffer()`` 12 | * Added compatibility for NumPy > 2.0 with Python 3.9+ 13 | -------------------------------------------------------------------------------- /openjpeg/tests/README.md: -------------------------------------------------------------------------------- 1 | Unit tests for pylibjpeg-openjpeg 2 | 3 | Dependencies 4 | ------------ 5 | 6 | Required 7 | ........ 8 | [pytest](https://docs.pytest.org/) 9 | [pylibjpeg-data](https://github.com/pydicom/pylibjpeg-data) 10 | 11 | Optional 12 | ........ 13 | [pylibjpeg](https://github.com/pydicom/pylibjpeg) 14 | [pydicom](https://github.com/pydicom/pydicom) 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot#example-dependabotyml-file-for-github-actions 2 | 3 | version: 2 4 | updates: 5 | 6 | - package-ecosystem: "github-actions" 7 | directory: "/" 8 | schedule: 9 | # Check for updates to GitHub Actions every month 10 | interval: "monthly" 11 | -------------------------------------------------------------------------------- /docs/changes/v2.2.0.rst: -------------------------------------------------------------------------------- 1 | .. _v2.2.0: 2 | 3 | 2.2.0 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Added support for encoding using :class:`bytes` or :class:`bytearray` 10 | * Fixed encoding with the JP2 format 11 | * Changed ``nr_components`` parameter to ``samples_per_pixel`` to be more in line 12 | with DICOM 13 | * Encoding with ``compression_ratios=[1]`` or ``signal_noise_ratios=[0]`` should now 14 | result in lossless encoding. 15 | -------------------------------------------------------------------------------- /docs/changes/v2.1.0.rst: -------------------------------------------------------------------------------- 1 | .. _v2.1.0: 2 | 3 | 2.1.0 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Added support for encoding a numpy ndarray using JPEG2000 lossless and lossy 10 | * Supported array shapes are (rows, columns) and (rows, columns, planes) 11 | * Supported number of planes is 1, 3 and 4 12 | * Supported dtypes are bool, u1, i1, u2, i2 for bit-depths 1-16 13 | * Also supported are u4 and i4 for bit-depths 1-24 14 | * Added support for decoding JPEG2000 data with precision up to 24-bits 15 | -------------------------------------------------------------------------------- /docs/changes/v1.1.0.rst: -------------------------------------------------------------------------------- 1 | .. _v1.1.0: 2 | 3 | 1.1.0 4 | ===== 5 | 6 | Changes 7 | ....... 8 | 9 | * Removed support for Python 3.6 10 | * Added support for Python 3.9 11 | 12 | 13 | Enhancements 14 | ............ 15 | 16 | * Add support for passing the path to the JPEG 2000 file to 17 | :func:`~openjpeg.utils.decode` and :func:`~openjpeg.utils.get_parameters` 18 | as either :class:`str` or :class:`pathlib.Path` 19 | 20 | 21 | Fixes 22 | ..... 23 | 24 | * Fixed handling subsampled images (:issues:`36`) 25 | -------------------------------------------------------------------------------- /openjpeg/tests/test_misc.py: -------------------------------------------------------------------------------- 1 | """Tests for standalone decoding.""" 2 | 3 | import logging 4 | 5 | from openjpeg import debug_logger 6 | 7 | 8 | def test_debug_logger(): 9 | """Test __init__.debug_logger().""" 10 | logger = logging.getLogger("openjpeg") 11 | assert len(logger.handlers) == 1 12 | assert isinstance(logger.handlers[0], logging.NullHandler) 13 | 14 | debug_logger() 15 | 16 | assert len(logger.handlers) == 1 17 | assert isinstance(logger.handlers[0], logging.StreamHandler) 18 | 19 | debug_logger() 20 | 21 | assert len(logger.handlers) == 1 22 | assert isinstance(logger.handlers[0], logging.StreamHandler) 23 | 24 | logger.handlers = [] 25 | -------------------------------------------------------------------------------- /openjpeg/__init__.py: -------------------------------------------------------------------------------- 1 | """Set package shortcuts.""" 2 | 3 | import logging 4 | 5 | from ._version import __version__ # noqa: F401 6 | from .utils import ( 7 | decode, # noqa: F401 8 | decode_pixel_data, # noqa: F401 9 | encode, # noqa: F401 10 | encode_pixel_data, # noqa: F401 11 | get_parameters, # noqa: F401 12 | ) 13 | 14 | 15 | # Setup default logging 16 | _logger = logging.getLogger(__name__) 17 | _logger.addHandler(logging.NullHandler()) 18 | _logger.debug(f"pylibjpeg-openjpeg v{__version__}") 19 | 20 | 21 | def debug_logger() -> None: 22 | """Setup the logging for debugging.""" 23 | logger = logging.getLogger(__name__) 24 | logger.handlers = [] 25 | handler = logging.StreamHandler() 26 | logger.setLevel(logging.DEBUG) 27 | formatter = logging.Formatter("%(levelname).1s: %(message)s") 28 | handler.setFormatter(formatter) 29 | logger.addHandler(handler) 30 | -------------------------------------------------------------------------------- /lib/interface/utils.h: -------------------------------------------------------------------------------- 1 | 2 | #include <../openjpeg/src/lib/openjp2/openjpeg.h> 3 | 4 | // Size of the buffer for the input/output streams 5 | #define BUFFER_SIZE OPJ_J2K_STREAM_CHUNK_SIZE 6 | 7 | #ifndef _PYOPJ_UTILS_H_ 8 | #define _PYOPJ_UTILS_H_ 9 | 10 | // BinaryIO.tell() 11 | extern Py_ssize_t py_tell(PyObject *stream); 12 | 13 | // BinaryIO.read() 14 | extern OPJ_SIZE_T py_read(void *destination, OPJ_SIZE_T nr_bytes, void *fd); 15 | 16 | // BinaryIO.seek() 17 | extern OPJ_BOOL py_seek(Py_ssize_t offset, void *stream, int whence); 18 | 19 | // BinaryIO.seek(offset, SEEK_SET) 20 | extern OPJ_BOOL py_seek_set(OPJ_OFF_T offset, void *stream); 21 | 22 | // BinaryIO.seek(offset, SEEK_CUR) 23 | extern OPJ_OFF_T py_skip(OPJ_OFF_T offset, void *stream); 24 | 25 | // len(BinaryIO) 26 | extern OPJ_UINT64 py_length(PyObject *stream); 27 | 28 | // BinaryIO.write() 29 | extern OPJ_SIZE_T py_write(void *src, OPJ_SIZE_T nr_bytes, void *dst); 30 | 31 | // Log a message to the Python logger 32 | extern void py_log(char *name, char *log_level, const char *log_msg); 33 | 34 | 35 | #endif 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | #*.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | #build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | parts/ 18 | sdist/ 19 | var/ 20 | wheels/ 21 | pip-wheel-metadata/ 22 | share/python-wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .nox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | *.py,cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | db.sqlite3-journal 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # IPython 78 | profile_default/ 79 | ipython_config.py 80 | 81 | # pyenv 82 | .python-version 83 | 84 | # pipenv 85 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 86 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 87 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 88 | # install all needed dependencies. 89 | #Pipfile.lock 90 | 91 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 92 | __pypackages__/ 93 | 94 | # Celery stuff 95 | celerybeat-schedule 96 | celerybeat.pid 97 | 98 | # SageMath parsed files 99 | *.sage.py 100 | 101 | # Environments 102 | .env 103 | .venv 104 | env/ 105 | venv/ 106 | ENV/ 107 | env.bak/ 108 | venv.bak/ 109 | 110 | # Spyder project settings 111 | .spyderproject 112 | .spyproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | 117 | # mkdocs documentation 118 | /site 119 | 120 | # mypy 121 | .mypy_cache/ 122 | .dmypy.json 123 | dmypy.json 124 | 125 | # Pyre type checker 126 | .pyre/ 127 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "poetry-core >=1.8,<2", 4 | "numpy >= 1.24", 5 | "cython >= 3.0", 6 | "setuptools", 7 | ] 8 | build-backend = "poetry.core.masonry.api" 9 | 10 | [tool.poetry.build] 11 | script = "build_package.py" 12 | generate-setup-file = true 13 | 14 | [tool.poetry] 15 | authors = ["pylibjpeg-openjpeg contributors"] 16 | classifiers=[ 17 | "License :: OSI Approved :: MIT License", 18 | "Intended Audience :: Developers", 19 | "Intended Audience :: Healthcare Industry", 20 | "Intended Audience :: Science/Research", 21 | "Development Status :: 5 - Production/Stable", 22 | "Natural Language :: English", 23 | "Programming Language :: Python :: 3.9", 24 | "Programming Language :: Python :: 3.10", 25 | "Programming Language :: Python :: 3.11", 26 | "Programming Language :: Python :: 3.12", 27 | "Programming Language :: Python :: 3.13", 28 | "Programming Language :: Python :: 3.14", 29 | "Operating System :: OS Independent", 30 | "Topic :: Scientific/Engineering :: Medical Science Apps.", 31 | "Topic :: Software Development :: Libraries", 32 | ] 33 | description = """\ 34 | A Python wrapper for openjpeg, with a focus on use as a plugin for \ 35 | for pylibjpeg\ 36 | """ 37 | homepage = "https://github.com/pydicom/pylibjpeg-openjpeg" 38 | keywords = ["dicom pydicom python imaging jpg jpeg jpeg2k jpeg2000 pylibjpeg openjpeg"] 39 | license = "MIT" 40 | maintainers = ["scaramallion "] 41 | name = "pylibjpeg-openjpeg" 42 | # We want to be able to build from sdist, so include required openjpeg src 43 | # But don't include any openjpeg src in the built wheels 44 | include = [ 45 | { path = "lib", format="sdist" }, 46 | { path = "build_tools", format="sdist" }, 47 | ] 48 | packages = [ 49 | {include = "openjpeg" }, 50 | ] 51 | readme = "README.md" 52 | version = "2.5.0" 53 | 54 | 55 | [tool.poetry.dependencies] 56 | python = "^3.9" 57 | numpy = "^2.0" 58 | 59 | [tool.poetry.plugins."pylibjpeg.jpeg_2000_decoders"] 60 | openjpeg = "openjpeg:decode" 61 | 62 | [tool.poetry.plugins."pylibjpeg.pixel_data_decoders"] 63 | "1.2.840.10008.1.2.4.90" = "openjpeg:decode_pixel_data" 64 | "1.2.840.10008.1.2.4.91" = "openjpeg:decode_pixel_data" 65 | "1.2.840.10008.1.2.4.201" = "openjpeg:decode_pixel_data" 66 | "1.2.840.10008.1.2.4.202" = "openjpeg:decode_pixel_data" 67 | "1.2.840.10008.1.2.4.203" = "openjpeg:decode_pixel_data" 68 | 69 | [tool.poetry.plugins."pylibjpeg.pixel_data_encoders"] 70 | "1.2.840.10008.1.2.4.90" = "openjpeg:encode_pixel_data" 71 | "1.2.840.10008.1.2.4.91" = "openjpeg:encode_pixel_data" 72 | 73 | [tool.coverage.run] 74 | omit = [ 75 | "openjpeg/tests/*", 76 | ] 77 | 78 | [tool.mypy] 79 | python_version = "3.9" 80 | files = "openjpeg" 81 | exclude = ["openjpeg/tests"] 82 | show_error_codes = true 83 | warn_redundant_casts = true 84 | warn_unused_ignores = true 85 | warn_return_any = true 86 | warn_unreachable = false 87 | ignore_missing_imports = true 88 | disallow_untyped_calls = true 89 | disallow_untyped_defs = true 90 | disallow_incomplete_defs = true 91 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | pylibjpeg-openjpeg 2 | ------------------ 3 | 4 | MIT License 5 | 6 | Copyright (c) 2020-2024 scaramallion 7 | 8 | Permission is hereby granted, free of charge, to any person obtaining a copy 9 | of this software and associated documentation files (the "Software"), to deal 10 | in the Software without restriction, including without limitation the rights 11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | copies of the Software, and to permit persons to whom the Software is 13 | furnished to do so, subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice shall be included in all 16 | copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 24 | SOFTWARE. 25 | 26 | 27 | openjpeg 28 | -------- 29 | 30 | BSD License 31 | 32 | The copyright in this software is being made available under the 2-clauses 33 | BSD License, included below. This software may be subject to other third 34 | party and contributor rights, including patent rights, and no such rights 35 | are granted under this license. 36 | 37 | Copyright (c) 2002-2014, Universite catholique de Louvain (UCL), Belgium 38 | Copyright (c) 2002-2014, Professor Benoit Macq 39 | Copyright (c) 2003-2014, Antonin Descampe 40 | Copyright (c) 2003-2009, Francois-Olivier Devaux 41 | Copyright (c) 2005, Herve Drolon, FreeImage Team 42 | Copyright (c) 2002-2003, Yannick Verschueren 43 | Copyright (c) 2001-2003, David Janssens 44 | Copyright (c) 2011-2012, Centre National d'Etudes Spatiales (CNES), France 45 | Copyright (c) 2012, CS Systemes d'Information, France 46 | 47 | All rights reserved. 48 | 49 | Redistribution and use in source and binary forms, with or without 50 | modification, are permitted provided that the following conditions 51 | are met: 52 | 1. Redistributions of source code must retain the above copyright 53 | notice, this list of conditions and the following disclaimer. 54 | 2. Redistributions in binary form must reproduce the above copyright 55 | notice, this list of conditions and the following disclaimer in the 56 | documentation and/or other materials provided with the distribution. 57 | 58 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS `AS IS' 59 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 60 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 61 | ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 62 | LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 63 | CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 64 | SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 65 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 66 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 67 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 68 | POSSIBILITY OF SUCH DAMAGE. 69 | -------------------------------------------------------------------------------- /.github/workflows/pytest-builds.yml: -------------------------------------------------------------------------------- 1 | name: unit-tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | jobs: 9 | windows: 10 | runs-on: windows-latest 11 | timeout-minutes: 30 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 16 | arch: ['x64', 'x86'] 17 | 18 | steps: 19 | - uses: actions/checkout@v6 20 | with: 21 | submodules: true 22 | 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v6 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | architecture: ${{ matrix.arch }} 28 | allow-prereleases: true 29 | 30 | - name: Install package and dependencies 31 | run: | 32 | python -m pip install -U pip 33 | python -m pip install -U pytest coverage pytest-cov 34 | python -m pip install git+https://github.com/pydicom/pylibjpeg-data 35 | python -m pip install . 36 | 37 | - name: Run pytest 38 | run: | 39 | pytest --cov openjpeg openjpeg/tests 40 | 41 | - name: Install pydicom release and rerun pytest 42 | run: | 43 | pip install pydicom pylibjpeg 44 | pytest --cov openjpeg openjpeg/tests 45 | 46 | osx: 47 | runs-on: macos-latest 48 | timeout-minutes: 30 49 | strategy: 50 | fail-fast: false 51 | matrix: 52 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 53 | 54 | steps: 55 | - uses: actions/checkout@v6 56 | with: 57 | submodules: true 58 | 59 | - name: Set up Python ${{ matrix.python-version }} 60 | uses: actions/setup-python@v6 61 | with: 62 | python-version: ${{ matrix.python-version }} 63 | allow-prereleases: true 64 | 65 | - name: Install package and dependencies 66 | run: | 67 | python -m pip install -U pip 68 | python -m pip install pytest coverage pytest-cov 69 | python -m pip install git+https://github.com/pydicom/pylibjpeg-data 70 | python -m pip install . 71 | 72 | - name: Run pytest 73 | run: | 74 | pytest --cov openjpeg openjpeg/tests 75 | 76 | - name: Install pydicom release and rerun pytest 77 | run: | 78 | pip install pydicom pylibjpeg 79 | pytest --cov openjpeg openjpeg/tests 80 | 81 | ubuntu: 82 | runs-on: ubuntu-latest 83 | timeout-minutes: 30 84 | strategy: 85 | fail-fast: false 86 | matrix: 87 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14'] 88 | 89 | steps: 90 | - uses: actions/checkout@v6 91 | with: 92 | submodules: true 93 | 94 | - name: Set up Python ${{ matrix.python-version }} 95 | uses: actions/setup-python@v6 96 | with: 97 | python-version: ${{ matrix.python-version }} 98 | allow-prereleases: true 99 | 100 | - name: Install package and dependencies 101 | run: | 102 | python -m pip install -U pip 103 | python -m pip install pytest coverage pytest-cov 104 | python -m pip install git+https://github.com/pydicom/pylibjpeg-data 105 | python -m pip install . 106 | 107 | - name: Run pytest 108 | run: | 109 | pytest --cov openjpeg openjpeg/tests 110 | 111 | - name: Install pydicom dev and rerun pytest (3.10+) 112 | if: ${{ contains('3.10 3.11 3.12 3.13 3.14', matrix.python-version) }} 113 | run: | 114 | pip install pylibjpeg 115 | pip install git+https://github.com/pydicom/pydicom 116 | pytest --cov openjpeg openjpeg/tests 117 | 118 | - name: Switch to current pydicom release and rerun pytest 119 | if: ${{ contains('3.10 3.11 3.12 3.13', matrix.python-version) }} 120 | run: | 121 | pip uninstall -y pydicom 122 | pip install pydicom pylibjpeg 123 | pytest --cov openjpeg openjpeg/tests 124 | 125 | - name: Send coverage results 126 | if: ${{ success() }} 127 | uses: codecov/codecov-action@v5 128 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Build status 3 | Test coverage 4 | PyPI versions 5 | Python versions 6 | Code style: black 7 |

8 | 9 | 10 | ## pylibjpeg-openjpeg 11 | 12 | A Python wrapper for 13 | [openjpeg](https://github.com/uclouvain/openjpeg), with a focus on use as a plugin for [pylibjpeg](http://github.com/pydicom/pylibjpeg). 14 | 15 | Linux, OSX and Windows are all supported. 16 | 17 | ### Installation 18 | #### Dependencies 19 | [NumPy](http://numpy.org) 20 | 21 | #### Installing the current release 22 | ```bash 23 | python -m pip install -U pylibjpeg-openjpeg 24 | ``` 25 | 26 | #### Installing the development version 27 | 28 | Make sure [Python](https://www.python.org/), [Git](https://git-scm.com/) and [CMake](https://cmake.org/) are installed. For Windows, you also need to install 29 | [Microsoft's C++ Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools&rel=16). 30 | ```bash 31 | git clone --recurse-submodules https://github.com/pydicom/pylibjpeg-openjpeg 32 | python -m pip install pylibjpeg-openjpeg 33 | ``` 34 | 35 | 36 | ### Supported JPEG Formats 37 | #### Decoding 38 | 39 | | ISO/IEC Standard | ITU Equivalent | JPEG Format | 40 | | --- | --- | --- | 41 | | [15444-1](https://www.iso.org/standard/78321.html) | [T.800](https://www.itu.int/rec/T-REC-T.800/en) | [JPEG 2000](https://jpeg.org/jpeg2000/) | 42 | 43 | #### Encoding 44 | 45 | Encoding of NumPy ndarrays is supported for the following: 46 | 47 | * Array dtype: bool, uint8, int8, uint16, int16, uint32 and int32 (1-24 bit-depth only) 48 | * Array shape: (rows, columns) and (rows, columns, planes) 49 | * Number of rows/columns: up to 65535 50 | * Number of planes: 1, 3 or 4 51 | 52 | ### Transfer Syntaxes 53 | | UID | Description | 54 | | --- | --- | 55 | | 1.2.840.10008.1.2.4.90 | JPEG 2000 Image Compression (Lossless Only) | 56 | | 1.2.840.10008.1.2.4.91 | JPEG 2000 Image Compression | 57 | | 1.2.840.10008.1.2.4.201 | High-Throughput JPEG 2000 Image Compression (Lossless Only) | 58 | | 1.2.840.10008.1.2.4.202 | High-Throughput JPEG 2000 with RPCL Options Image Compression (Lossless Only) | 59 | | 1.2.840.10008.1.2.4.203 | High-Throughput JPEG 2000 Image Compression | 60 | 61 | 62 | ### Usage 63 | #### With pylibjpeg and pydicom 64 | 65 | ```python 66 | from pydicom import dcmread 67 | from pydicom.data import get_testdata_file 68 | 69 | ds = dcmread(get_testdata_file('JPEG2000.dcm')) 70 | arr = ds.pixel_array 71 | ``` 72 | 73 | #### Standalone JPEG decoding 74 | 75 | You can also decode JPEG 2000 images to a [numpy ndarray][1]: 76 | 77 | [1]: https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html 78 | 79 | ```python 80 | from openjpeg import decode 81 | 82 | with open('filename.j2k', 'rb') as f: 83 | # Returns a numpy array 84 | arr = decode(f) 85 | 86 | # Or simply... 87 | arr = decode('filename.j2k') 88 | ``` 89 | 90 | #### Standalone JPEG encoding 91 | 92 | Lossless encoding of RGB with multiple-component transformation: 93 | 94 | ```python 95 | 96 | import numpy as np 97 | from openjpeg import encode_array 98 | 99 | arr = np.random.randint(low=0, high=65536, size=(100, 100, 3), dtype="uint8") 100 | encode_array(arr, photometric_interpretation=1) # 1: sRGB 101 | ``` 102 | 103 | Lossy encoding of a monochrome image using compression ratios: 104 | 105 | ```python 106 | 107 | import numpy as np 108 | from openjpeg import encode_array 109 | 110 | arr = np.random.randint(low=-2**15, high=2**15, size=(100, 100), dtype="int8") 111 | # You must determine your own values for `compression_ratios` 112 | # as these are for illustration purposes only 113 | encode_array(arr, compression_ratios=[5, 2]) 114 | ``` 115 | 116 | Lossy encoding of a monochrome image using peak signal-to-noise ratios: 117 | 118 | ```python 119 | 120 | import numpy as np 121 | from openjpeg import encode_array 122 | 123 | arr = np.random.randint(low=-2**15, high=2**15, size=(100, 100), dtype="int8") 124 | # You must determine your own values for `signal_noise_ratios` 125 | # as these are for illustration purposes only 126 | encode_array(arr, signal_noise_ratios=[50, 80, 100]) 127 | ``` 128 | 129 | See the docstring for the [encode_array() function][2] for full details. 130 | 131 | [2]: https://github.com/pydicom/pylibjpeg-openjpeg/blob/main/openjpeg/utils.py#L429 132 | -------------------------------------------------------------------------------- /openjpeg/tests/test_parameters.py: -------------------------------------------------------------------------------- 1 | """Tests for get_parameters().""" 2 | 3 | import pytest 4 | 5 | try: 6 | from pydicom.encaps import generate_frames 7 | 8 | HAS_PYDICOM = True 9 | except ImportError: 10 | HAS_PYDICOM = False 11 | 12 | from openjpeg import get_parameters 13 | from openjpeg.data import get_indexed_datasets, JPEG_DIRECTORY 14 | 15 | 16 | DIR_15444 = JPEG_DIRECTORY / "15444" 17 | 18 | 19 | REF_DCM = { 20 | "1.2.840.10008.1.2.4.90": [ 21 | # filename, (rows, columns, samples/px, bits/sample, signed?) 22 | ("693_J2KR.dcm", (512, 512, 1, 14, True)), 23 | ("966_fixed.dcm", (2128, 2000, 1, 12, False)), 24 | ("emri_small_jpeg_2k_lossless.dcm", (64, 64, 1, 16, False)), 25 | ("explicit_VR-UN.dcm", (512, 512, 1, 16, True)), 26 | ("GDCMJ2K_TextGBR.dcm", (400, 400, 3, 8, False)), 27 | ("JPEG2KLossless_1s_1f_u_16_16.dcm", (1416, 1420, 1, 16, False)), 28 | ("MR_small_jp2klossless.dcm", (64, 64, 1, 16, True)), 29 | ("MR2_J2KR.dcm", (1024, 1024, 1, 12, False)), 30 | ("NM_Kakadu44_SOTmarkerincons.dcm", (2500, 2048, 1, 16, False)), 31 | ("RG1_J2KR.dcm", (1955, 1841, 1, 15, False)), 32 | ("RG3_J2KR.dcm", (1760, 1760, 1, 10, False)), 33 | ("TOSHIBA_J2K_OpenJPEGv2Regression.dcm", (512, 512, 1, 16, False)), 34 | ("TOSHIBA_J2K_SIZ0_PixRep1.dcm", (512, 512, 1, 16, False)), 35 | ("TOSHIBA_J2K_SIZ1_PixRep0.dcm", (512, 512, 1, 16, True)), 36 | ("US1_J2KR.dcm", (480, 640, 3, 8, False)), 37 | ], 38 | "1.2.840.10008.1.2.4.91": [ 39 | ("693_J2KI.dcm", (512, 512, 1, 16, True)), 40 | ("ELSCINT1_JP2vsJ2K.dcm", (512, 512, 1, 12, False)), 41 | ("JPEG2000.dcm", (1024, 256, 1, 16, True)), 42 | ("MAROTECH_CT_JP2Lossy.dcm", (716, 512, 1, 12, False)), 43 | ("MR2_J2KI.dcm", (1024, 1024, 1, 12, False)), 44 | ("OsirixFake16BitsStoredFakeSpacing.dcm", (224, 176, 1, 11, False)), 45 | ("RG1_J2KI.dcm", (1955, 1841, 1, 15, False)), 46 | ("RG3_J2KI.dcm", (1760, 1760, 1, 10, False)), 47 | ("SC_rgb_gdcm_KY.dcm", (100, 100, 3, 8, False)), 48 | ("US1_J2KI.dcm", (480, 640, 3, 8, False)), 49 | ], 50 | } 51 | 52 | 53 | def get_frame_generator(ds): 54 | """Return a frame generator for DICOM datasets.""" 55 | nr_frames = ds.get("NumberOfFrames", 1) 56 | return generate_frames(ds.PixelData, number_of_frames=nr_frames) 57 | 58 | 59 | def test_bad_decode(): 60 | """Test trying to decode bad data.""" 61 | stream = b"\xff\x4f\xff\x51\x00\x00\x01" 62 | msg = r"Error decoding the J2K data: failed to read the header" 63 | with pytest.raises(RuntimeError, match=msg): 64 | get_parameters(stream) 65 | 66 | 67 | def test_subsampling(): 68 | """Test parameters with subsampled data (see #36).""" 69 | jpg = DIR_15444 / "2KLS" / "oj36.j2k" 70 | params = get_parameters(jpg) 71 | assert params["rows"] == 256 72 | assert params["columns"] == 256 73 | assert params["colourspace"] == "unspecified" 74 | assert params["samples_per_pixel"] == 3 75 | assert params["precision"] == 8 76 | assert params["is_signed"] is False 77 | assert params["nr_tiles"] == 0 78 | 79 | 80 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No pydicom") 81 | class TestGetParametersDCM: 82 | """Tests for get_parameters() using DICOM datasets.""" 83 | 84 | @pytest.mark.parametrize("fname, info", REF_DCM["1.2.840.10008.1.2.4.90"]) 85 | def test_jpeg2000r(self, fname, info): 86 | """Test get_parameters() for the baseline datasets.""" 87 | index = get_indexed_datasets("1.2.840.10008.1.2.4.90") 88 | ds = index[fname]["ds"] 89 | 90 | frame = next(get_frame_generator(ds)) 91 | params = get_parameters(frame) 92 | 93 | assert (info[0], info[1]) == (params["rows"], params["columns"]) 94 | assert info[2] == params["samples_per_pixel"] 95 | assert info[3] == params["precision"] 96 | assert info[4] == params["is_signed"] 97 | 98 | @pytest.mark.parametrize("fname, info", REF_DCM["1.2.840.10008.1.2.4.91"]) 99 | def test_jpeg2000i(self, fname, info): 100 | """Test get_parameters() for the baseline datasets.""" 101 | index = get_indexed_datasets("1.2.840.10008.1.2.4.91") 102 | ds = index[fname]["ds"] 103 | 104 | frame = next(get_frame_generator(ds)) 105 | params = get_parameters(frame) 106 | 107 | assert (info[0], info[1]) == (params["rows"], params["columns"]) 108 | assert info[2] == params["samples_per_pixel"] 109 | assert info[3] == params["precision"] 110 | assert info[4] == params["is_signed"] 111 | 112 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No pydicom") 113 | def test_decode_bad_type_raises(self): 114 | """Test decoding using invalid type raises.""" 115 | index = get_indexed_datasets("1.2.840.10008.1.2.4.90") 116 | ds = index["MR_small_jp2klossless.dcm"]["ds"] 117 | frame = tuple(next(get_frame_generator(ds))) 118 | assert not hasattr(frame, "tell") and not isinstance(frame, bytes) 119 | 120 | msg = ( 121 | r"The Python object containing the encoded JPEG 2000 data must " 122 | r"either be bytes or have read\(\), tell\(\) and seek\(\) methods." 123 | ) 124 | with pytest.raises(TypeError, match=msg): 125 | get_parameters(frame) 126 | 127 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No pydicom") 128 | def test_decode_format_raises(self): 129 | """Test decoding using invalid format raises.""" 130 | index = get_indexed_datasets("1.2.840.10008.1.2.4.90") 131 | ds = index["693_J2KR.dcm"]["ds"] 132 | frame = next(get_frame_generator(ds)) 133 | msg = r"Unsupported 'j2k_format' value: 3" 134 | with pytest.raises(ValueError, match=msg): 135 | get_parameters(frame, j2k_format=3) 136 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "numpy" 5 | version = "2.0.2" 6 | description = "Fundamental package for array computing in Python" 7 | optional = false 8 | python-versions = ">=3.9" 9 | files = [ 10 | {file = "numpy-2.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:51129a29dbe56f9ca83438b706e2e69a39892b5eda6cedcb6b0c9fdc9b0d3ece"}, 11 | {file = "numpy-2.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f15975dfec0cf2239224d80e32c3170b1d168335eaedee69da84fbe9f1f9cd04"}, 12 | {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:8c5713284ce4e282544c68d1c3b2c7161d38c256d2eefc93c1d683cf47683e66"}, 13 | {file = "numpy-2.0.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:becfae3ddd30736fe1889a37f1f580e245ba79a5855bff5f2a29cb3ccc22dd7b"}, 14 | {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2da5960c3cf0df7eafefd806d4e612c5e19358de82cb3c343631188991566ccd"}, 15 | {file = "numpy-2.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:496f71341824ed9f3d2fd36cf3ac57ae2e0165c143b55c3a035ee219413f3318"}, 16 | {file = "numpy-2.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a61ec659f68ae254e4d237816e33171497e978140353c0c2038d46e63282d0c8"}, 17 | {file = "numpy-2.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d731a1c6116ba289c1e9ee714b08a8ff882944d4ad631fd411106a30f083c326"}, 18 | {file = "numpy-2.0.2-cp310-cp310-win32.whl", hash = "sha256:984d96121c9f9616cd33fbd0618b7f08e0cfc9600a7ee1d6fd9b239186d19d97"}, 19 | {file = "numpy-2.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:c7b0be4ef08607dd04da4092faee0b86607f111d5ae68036f16cc787e250a131"}, 20 | {file = "numpy-2.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:49ca4decb342d66018b01932139c0961a8f9ddc7589611158cb3c27cbcf76448"}, 21 | {file = "numpy-2.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:11a76c372d1d37437857280aa142086476136a8c0f373b2e648ab2c8f18fb195"}, 22 | {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:807ec44583fd708a21d4a11d94aedf2f4f3c3719035c76a2bbe1fe8e217bdc57"}, 23 | {file = "numpy-2.0.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8cafab480740e22f8d833acefed5cc87ce276f4ece12fdaa2e8903db2f82897a"}, 24 | {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a15f476a45e6e5a3a79d8a14e62161d27ad897381fecfa4a09ed5322f2085669"}, 25 | {file = "numpy-2.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13e689d772146140a252c3a28501da66dfecd77490b498b168b501835041f951"}, 26 | {file = "numpy-2.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9ea91dfb7c3d1c56a0e55657c0afb38cf1eeae4544c208dc465c3c9f3a7c09f9"}, 27 | {file = "numpy-2.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c1c9307701fec8f3f7a1e6711f9089c06e6284b3afbbcd259f7791282d660a15"}, 28 | {file = "numpy-2.0.2-cp311-cp311-win32.whl", hash = "sha256:a392a68bd329eafac5817e5aefeb39038c48b671afd242710b451e76090e81f4"}, 29 | {file = "numpy-2.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:286cd40ce2b7d652a6f22efdfc6d1edf879440e53e76a75955bc0c826c7e64dc"}, 30 | {file = "numpy-2.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:df55d490dea7934f330006d0f81e8551ba6010a5bf035a249ef61a94f21c500b"}, 31 | {file = "numpy-2.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8df823f570d9adf0978347d1f926b2a867d5608f434a7cff7f7908c6570dcf5e"}, 32 | {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9a92ae5c14811e390f3767053ff54eaee3bf84576d99a2456391401323f4ec2c"}, 33 | {file = "numpy-2.0.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:a842d573724391493a97a62ebbb8e731f8a5dcc5d285dfc99141ca15a3302d0c"}, 34 | {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05e238064fc0610c840d1cf6a13bf63d7e391717d247f1bf0318172e759e692"}, 35 | {file = "numpy-2.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0123ffdaa88fa4ab64835dcbde75dcdf89c453c922f18dced6e27c90d1d0ec5a"}, 36 | {file = "numpy-2.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:96a55f64139912d61de9137f11bf39a55ec8faec288c75a54f93dfd39f7eb40c"}, 37 | {file = "numpy-2.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ec9852fb39354b5a45a80bdab5ac02dd02b15f44b3804e9f00c556bf24b4bded"}, 38 | {file = "numpy-2.0.2-cp312-cp312-win32.whl", hash = "sha256:671bec6496f83202ed2d3c8fdc486a8fc86942f2e69ff0e986140339a63bcbe5"}, 39 | {file = "numpy-2.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:cfd41e13fdc257aa5778496b8caa5e856dc4896d4ccf01841daee1d96465467a"}, 40 | {file = "numpy-2.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9059e10581ce4093f735ed23f3b9d283b9d517ff46009ddd485f1747eb22653c"}, 41 | {file = "numpy-2.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:423e89b23490805d2a5a96fe40ec507407b8ee786d66f7328be214f9679df6dd"}, 42 | {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:2b2955fa6f11907cf7a70dab0d0755159bca87755e831e47932367fc8f2f2d0b"}, 43 | {file = "numpy-2.0.2-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:97032a27bd9d8988b9a97a8c4d2c9f2c15a81f61e2f21404d7e8ef00cb5be729"}, 44 | {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e795a8be3ddbac43274f18588329c72939870a16cae810c2b73461c40718ab1"}, 45 | {file = "numpy-2.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b258c385842546006213344c50655ff1555a9338e2e5e02a0756dc3e803dd"}, 46 | {file = "numpy-2.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5fec9451a7789926bcf7c2b8d187292c9f93ea30284802a0ab3f5be8ab36865d"}, 47 | {file = "numpy-2.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9189427407d88ff25ecf8f12469d4d39d35bee1db5d39fc5c168c6f088a6956d"}, 48 | {file = "numpy-2.0.2-cp39-cp39-win32.whl", hash = "sha256:905d16e0c60200656500c95b6b8dca5d109e23cb24abc701d41c02d74c6b3afa"}, 49 | {file = "numpy-2.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:a3f4ab0caa7f053f6797fcd4e1e25caee367db3112ef2b6ef82d749530768c73"}, 50 | {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7f0a0c6f12e07fa94133c8a67404322845220c06a9e80e85999afe727f7438b8"}, 51 | {file = "numpy-2.0.2-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:312950fdd060354350ed123c0e25a71327d3711584beaef30cdaa93320c392d4"}, 52 | {file = "numpy-2.0.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26df23238872200f63518dd2aa984cfca675d82469535dc7162dc2ee52d9dd5c"}, 53 | {file = "numpy-2.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a46288ec55ebbd58947d31d72be2c63cbf839f0a63b49cb755022310792a3385"}, 54 | {file = "numpy-2.0.2.tar.gz", hash = "sha256:883c987dee1880e2a864ab0dc9892292582510604156762362d9326444636e78"}, 55 | ] 56 | 57 | [metadata] 58 | lock-version = "2.0" 59 | python-versions = "^3.9" 60 | content-hash = "7d3e1f75d485bfd3cd24ea778f58b117e55115d79d6ff42411add2939484a14d" 61 | -------------------------------------------------------------------------------- /lib/interface/utils.c: -------------------------------------------------------------------------------- 1 | 2 | #include "Python.h" 3 | #include "utils.h" 4 | #include <../openjpeg/src/lib/openjp2/openjpeg.h> 5 | 6 | 7 | // Python logging 8 | void py_log(char *name, char *log_level, const char *log_msg) 9 | { 10 | /* Log `log_msg` to the Python logger `name`. 11 | 12 | Parameters 13 | ---------- 14 | name 15 | The name of the logger (i.e. logger.getLogger(name)) 16 | log_level 17 | The log level to use DEBUG, INFO, WARNING, ERROR, CRITICAL 18 | log_msg 19 | The message to use. 20 | */ 21 | static PyObject *module = NULL; 22 | static PyObject *logger = NULL; 23 | static PyObject *msg = NULL; 24 | static PyObject *lib_name = NULL; 25 | 26 | // import logging 27 | module = PyImport_ImportModuleNoBlock("logging"); 28 | if (module == NULL) { 29 | return; 30 | } 31 | 32 | // logger = logging.getLogger(lib_name) 33 | lib_name = Py_BuildValue("s", name); 34 | logger = PyObject_CallMethod(module, "getLogger", "O", lib_name); 35 | 36 | // Create Python str and remove trailing newline 37 | msg = Py_BuildValue("s", log_msg); 38 | msg = PyObject_CallMethod(msg, "strip", NULL); 39 | 40 | // logger.debug(msg), etc 41 | if (strcmp(log_level, "DEBUG") == 0) { 42 | PyObject_CallMethod(logger, "debug", "O", msg); 43 | } else if (strcmp(log_level, "INFO") == 0) { 44 | PyObject_CallMethod(logger, "info", "O", msg); 45 | } else if (strcmp(log_level, "WARNING") == 0) { 46 | PyObject_CallMethod(logger, "warning", "O", msg); 47 | } else if (strcmp(log_level, "ERROR") == 0) { 48 | PyObject_CallMethod(logger, "error", "O", msg); 49 | } else if (strcmp(log_level, "CRITICAL") == 0) { 50 | PyObject_CallMethod(logger, "critical", "O", msg); 51 | } 52 | 53 | Py_DECREF(msg); 54 | Py_DECREF(lib_name); 55 | } 56 | 57 | // Python BinaryIO methods 58 | Py_ssize_t py_tell(PyObject *stream) 59 | { 60 | /* Return the current position of the `stream`. 61 | 62 | Parameters 63 | ---------- 64 | stream : PyObject * 65 | The Python stream object to use (must have a ``tell()`` method). 66 | 67 | Returns 68 | ------- 69 | Py_ssize_t 70 | The current position in the `stream`. 71 | */ 72 | PyObject *result; 73 | Py_ssize_t location; 74 | 75 | result = PyObject_CallMethod(stream, "tell", NULL); 76 | location = PyLong_AsSsize_t(result); 77 | 78 | Py_DECREF(result); 79 | 80 | //printf("py_tell(): %u\n", location); 81 | return location; 82 | } 83 | 84 | 85 | OPJ_SIZE_T py_read(void *destination, OPJ_SIZE_T nr_bytes, void *fd) 86 | { 87 | /* Read `nr_bytes` from Python object `fd` and copy it to `destination`. 88 | 89 | Parameters 90 | ---------- 91 | destination : void * 92 | The object where the read data will be copied. 93 | nr_bytes : OPJ_SIZE_T 94 | The number of bytes to be read. 95 | fd : PyObject * 96 | The Python file-like to read the data from (must have a ``read()`` 97 | method). 98 | 99 | Returns 100 | ------- 101 | OPJ_SIZE_T 102 | The number of bytes read or -1 if reading failed or if trying to read 103 | while at the end of the data. 104 | */ 105 | PyObject* result; 106 | char* buffer; 107 | Py_ssize_t length; 108 | int bytes_result; 109 | 110 | // Py_ssize_t: signed int samed size as size_t 111 | // fd.read(nr_bytes), "k" => C unsigned long int to Python int 112 | result = PyObject_CallMethod(fd, "read", "n", nr_bytes); 113 | // Returns the null-terminated contents of `result` 114 | // `length` is Py_ssize_t * 115 | // `buffer` is char ** 116 | // If `length` is NULL, returns -1 117 | bytes_result = PyBytes_AsStringAndSize(result, &buffer, &length); 118 | 119 | // `length` is NULL 120 | if (bytes_result == -1) 121 | goto error; 122 | 123 | // More bytes read then asked for 124 | if (length > (long)(nr_bytes)) 125 | goto error; 126 | 127 | // Convert `length` to OPJ_SIZE_T - shouldn't have negative lengths 128 | if (length < 0) 129 | goto error; 130 | 131 | OPJ_SIZE_T len_size_t = (OPJ_SIZE_T)(length); 132 | 133 | // memcpy(void *dest, const void *src, size_t n) 134 | memcpy(destination, buffer, len_size_t); 135 | 136 | //printf("py_read(): %u bytes asked, %u bytes read\n", nr_bytes, len_size_t); 137 | 138 | Py_DECREF(result); 139 | return len_size_t ? len_size_t: (OPJ_SIZE_T)-1; 140 | 141 | error: 142 | Py_DECREF(result); 143 | return -1; 144 | } 145 | 146 | 147 | OPJ_BOOL py_seek(Py_ssize_t offset, void *stream, int whence) 148 | { 149 | /* Change the `stream` position to the given `offset` from `whence`. 150 | 151 | Parameters 152 | ---------- 153 | offset : OPJ_OFF_T 154 | The offset relative to `whence`. 155 | stream : PyObject * 156 | The Python stream object to seek (must have a ``seek()`` method). 157 | whence : int 158 | 0 for SEEK_SET, 1 for SEEK_CUR, 2 for SEEK_END 159 | 160 | Returns 161 | ------- 162 | OPJ_TRUE : OBJ_BOOL 163 | */ 164 | // Python and C; SEEK_SET is 0, SEEK_CUR is 1 and SEEK_END is 2 165 | // stream.seek(nr_bytes, whence), 166 | // k: convert C unsigned long int to Python int 167 | // i: convert C int to a Python integer 168 | PyObject *result; 169 | result = PyObject_CallMethod(stream, "seek", "ni", offset, whence); 170 | Py_DECREF(result); 171 | 172 | //printf("py_seek(): offset %u bytes from %u\n", offset, whence); 173 | 174 | return OPJ_TRUE; 175 | } 176 | 177 | 178 | OPJ_BOOL py_seek_set(OPJ_OFF_T offset, void *stream) 179 | { 180 | /* Change the `stream` position to the given `offset` from SEEK_SET. 181 | 182 | Parameters 183 | ---------- 184 | offset : OPJ_OFF_T 185 | The offset relative to SEEK_SET. 186 | stream : PyObject * 187 | The Python stream object to seek (must have a ``seek()`` method). 188 | 189 | Returns 190 | ------- 191 | OPJ_TRUE : OBJ_BOOL 192 | */ 193 | return py_seek(offset, stream, SEEK_SET); 194 | } 195 | 196 | 197 | OPJ_OFF_T py_skip(OPJ_OFF_T offset, void *stream) 198 | { 199 | /* Change the `stream` position by `offset` from SEEK_CUR and return the 200 | number of skipped bytes. 201 | 202 | Parameters 203 | ---------- 204 | offset : OPJ_OFF_T 205 | The offset relative to SEEK_CUR. 206 | stream : PyObject * 207 | The Python stream object to seek (must have a ``seek()`` method). 208 | 209 | Returns 210 | ------- 211 | int 212 | The number of bytes skipped 213 | */ 214 | off_t initial; 215 | initial = py_tell(stream); 216 | 217 | py_seek(offset, stream, SEEK_CUR); 218 | 219 | off_t current; 220 | current = py_tell(stream); 221 | 222 | return current - initial; 223 | } 224 | 225 | 226 | OPJ_UINT64 py_length(PyObject * stream) 227 | { 228 | /* Return the total length of the `stream`. 229 | 230 | Parameters 231 | ---------- 232 | stream : PyObject * 233 | The Python stream object (must have ``seek()`` and ``tell()`` methods). 234 | 235 | Returns 236 | ------- 237 | OPJ_UINT64 238 | The total length of the `stream`. 239 | */ 240 | OPJ_OFF_T input_length = 0; 241 | 242 | py_seek(0, stream, SEEK_END); 243 | input_length = (OPJ_OFF_T)py_tell(stream); 244 | py_seek(0, stream, SEEK_SET); 245 | 246 | return (OPJ_UINT64)input_length; 247 | } 248 | 249 | 250 | OPJ_SIZE_T py_write(void *src, OPJ_SIZE_T nr_bytes, void *dst) 251 | { 252 | /* Write `nr_bytes` from `src` to the Python object `dst` 253 | 254 | Parameters 255 | ---------- 256 | src : void * 257 | The source of data. 258 | nr_bytes : OPJ_SIZE_T 259 | The length of the data to be written. 260 | dst : void * 261 | Where the data should be written to. Should be BinaryIO. 262 | 263 | Returns 264 | ------- 265 | OBJ_SIZE_T 266 | The number of bytes written to dst. 267 | */ 268 | PyObject *result; 269 | PyObject *bytes_object; 270 | 271 | // Create bytes object from `src` (of length `bytes`) 272 | bytes_object = PyBytes_FromStringAndSize(src, nr_bytes); 273 | // Use the bytes object to extend our dst using write(bytes_object) 274 | // 'O' means pass the Python object untouched 275 | result = PyObject_CallMethod(dst, "write", "O", bytes_object); 276 | 277 | Py_DECREF(bytes_object); 278 | Py_DECREF(result); 279 | 280 | return nr_bytes; 281 | } 282 | -------------------------------------------------------------------------------- /.github/workflows/release-wheels.yml: -------------------------------------------------------------------------------- 1 | name: release-deploy 2 | 3 | on: 4 | release: 5 | types: [ published ] 6 | # push: 7 | # branches: [ main ] 8 | # pull_request: 9 | 10 | jobs: 11 | build-sdist: 12 | name: Build source distribution 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 10 15 | steps: 16 | - uses: actions/checkout@v6 17 | with: 18 | submodules: true 19 | 20 | - uses: actions/setup-python@v6 21 | name: Install Python 22 | with: 23 | python-version: '3.13' 24 | 25 | - name: Build sdist 26 | run: | 27 | python -m pip install -U pip 28 | python -m pip install poetry 29 | poetry build -f sdist 30 | 31 | - name: Store artifacts 32 | uses: actions/upload-artifact@v5 33 | with: 34 | name: sdist 35 | path: ./dist 36 | 37 | build-wheels: 38 | name: Build wheel for cp${{ matrix.python }}-${{ matrix.platform_id }}-${{ matrix.manylinux_image }} 39 | runs-on: ${{ matrix.os }} 40 | strategy: 41 | fail-fast: false 42 | matrix: 43 | include: 44 | # Windows 32 bit 45 | - os: windows-latest 46 | python: 39 47 | platform_id: win32 48 | - os: windows-latest 49 | python: 310 50 | platform_id: win32 51 | - os: windows-latest 52 | python: 311 53 | platform_id: win32 54 | - os: windows-latest 55 | python: 312 56 | platform_id: win32 57 | - os: windows-latest 58 | python: 313 59 | platform_id: win32 60 | - os: windows-latest 61 | python: 314 62 | platform_id: win32 63 | 64 | # Windows 64 bit 65 | - os: windows-latest 66 | python: 39 67 | platform_id: win_amd64 68 | - os: windows-latest 69 | python: 310 70 | platform_id: win_amd64 71 | - os: windows-latest 72 | python: 311 73 | platform_id: win_amd64 74 | - os: windows-latest 75 | python: 312 76 | platform_id: win_amd64 77 | - os: windows-latest 78 | python: 313 79 | platform_id: win_amd64 80 | - os: windows-latest 81 | python: 314 82 | platform_id: win_amd64 83 | 84 | # Linux 64 bit manylinux2014 85 | - os: ubuntu-latest 86 | python: 39 87 | platform_id: manylinux_x86_64 88 | manylinux_image: manylinux2014 89 | - os: ubuntu-latest 90 | python: 310 91 | platform_id: manylinux_x86_64 92 | manylinux_image: manylinux2014 93 | - os: ubuntu-latest 94 | python: 311 95 | platform_id: manylinux_x86_64 96 | manylinux_image: manylinux2014 97 | - os: ubuntu-latest 98 | python: 312 99 | platform_id: manylinux_x86_64 100 | manylinux_image: manylinux2014 101 | - os: ubuntu-latest 102 | python: 313 103 | platform_id: manylinux_x86_64 104 | manylinux_image: manylinux2014 105 | - os: ubuntu-latest 106 | python: 314 107 | platform_id: manylinux_x86_64 108 | manylinux_image: manylinux2014 109 | 110 | # Linux aarch64 111 | - os: ubuntu-latest 112 | python: 39 113 | platform_id: manylinux_aarch64 114 | - os: ubuntu-latest 115 | python: 310 116 | platform_id: manylinux_aarch64 117 | - os: ubuntu-latest 118 | python: 311 119 | platform_id: manylinux_aarch64 120 | - os: ubuntu-latest 121 | python: 312 122 | platform_id: manylinux_aarch64 123 | - os: ubuntu-latest 124 | python: 313 125 | platform_id: manylinux_aarch64 126 | - os: ubuntu-latest 127 | python: 314 128 | platform_id: manylinux_aarch64 129 | 130 | # MacOS 13 x86_64 131 | - os: macos-13 132 | python: 39 133 | platform_id: macosx_x86_64 134 | - os: macos-13 135 | python: 310 136 | platform_id: macosx_x86_64 137 | - os: macos-13 138 | python: 311 139 | platform_id: macosx_x86_64 140 | - os: macos-13 141 | python: 312 142 | platform_id: macosx_x86_64 143 | - os: macos-13 144 | python: 313 145 | platform_id: macosx_x86_64 146 | - os: macos-13 147 | python: 314 148 | platform_id: macosx_x86_64 149 | 150 | steps: 151 | - uses: actions/checkout@v6 152 | with: 153 | submodules: true 154 | 155 | - name: Set up QEMU 156 | if: ${{ matrix.platform_id == 'manylinux_aarch64' }} 157 | uses: docker/setup-qemu-action@v3 158 | with: 159 | platforms: arm64 160 | 161 | - uses: actions/setup-python@v6 162 | name: Install Python 163 | with: 164 | python-version: '3.13' 165 | 166 | - name: Install cibuildwheel 167 | run: | 168 | python -m pip install -U pip 169 | python -m pip install cibuildwheel>=3.1.3 170 | 171 | - name: Build wheels (non-MacOS arm64) 172 | env: 173 | CIBW_BUILD: cp${{ matrix.python }}-${{ matrix.platform_id }} 174 | CIBW_ARCHS: all 175 | CIBW_MANYLINUX_X86_64_IMAGE: ${{ matrix.manylinux_image }} 176 | CIBW_MANYLINUX_I686_IMAGE: ${{ matrix.manylinux_image }} 177 | CIBW_MANYLINUX_AARCH64_IMAGE: ${{ matrix.manylinux_image }} 178 | CIBW_ARCHS_MACOS: x86_64 179 | CIBW_BUILD_VERBOSITY: 1 180 | run: | 181 | python --version 182 | python -m cibuildwheel --output-dir dist 183 | 184 | - name: Store artifacts 185 | uses: actions/upload-artifact@v5 186 | with: 187 | name: wheel-${{ matrix.python }}-${{ matrix.platform_id }} 188 | path: ./dist 189 | 190 | build-wheels-macos-arm64: 191 | name: Build wheel for cp${{ matrix.python }}-${{ matrix.platform_id }} 192 | runs-on: ${{ matrix.os }} 193 | strategy: 194 | fail-fast: false 195 | matrix: 196 | include: 197 | # MacOS 14 arm64 198 | - os: macos-14 199 | python: 39 200 | platform_id: macosx_arm64 201 | - os: macos-14 202 | python: 310 203 | platform_id: macosx_arm64 204 | - os: macos-14 205 | python: 311 206 | platform_id: macosx_arm64 207 | - os: macos-14 208 | python: 312 209 | platform_id: macosx_arm64 210 | - os: macos-14 211 | python: 313 212 | platform_id: macosx_arm64 213 | - os: macos-14 214 | python: 314 215 | platform_id: macosx_arm64 216 | 217 | steps: 218 | - uses: actions/checkout@v6 219 | with: 220 | submodules: true 221 | 222 | - uses: actions/setup-python@v6 223 | name: Install Python 224 | with: 225 | python-version: '3.13' 226 | 227 | - name: Install cibuildwheel 228 | run: python -m pip install cibuildwheel>=3.1.3 229 | 230 | - name: Build wheels 231 | env: 232 | CIBW_BUILD: cp${{ matrix.python }}-* 233 | CIBW_ARCHS_MACOS: arm64 234 | CIBW_BUILD_VERBOSITY: 1 235 | run: | 236 | python -m cibuildwheel --output-dir dist 237 | 238 | - name: Store artifacts 239 | uses: actions/upload-artifact@v5 240 | with: 241 | name: wheel-${{ matrix.python }}-${{ matrix.platform_id }} 242 | path: ./dist/*.whl 243 | 244 | # test-package: 245 | # name: Test built package 246 | # needs: [ build-wheels, build-sdist, build-wheels-macos-arm64 ] 247 | # runs-on: ubuntu-latest 248 | # timeout-minutes: 30 249 | # strategy: 250 | # fail-fast: false 251 | # matrix: 252 | # python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] 253 | # 254 | # steps: 255 | # - name: Set up Python ${{ matrix.python-version }} 256 | # uses: actions/setup-python@v6 257 | # with: 258 | # python-version: ${{ matrix.python-version }} 259 | # 260 | # - name: Download the wheels 261 | # uses: actions/download-artifact@v6 262 | # with: 263 | # path: dist/ 264 | # merge-multiple: true 265 | # 266 | # - name: Install from package wheels and test 267 | # run: | 268 | # python -m venv testwhl 269 | # source testwhl/bin/activate 270 | # python -m pip install -U pip 271 | # python -m pip install pytest pydicom pylibjpeg 272 | # python -m pip uninstall -y pylibjpeg-openjpeg 273 | # python -m pip install git+https://github.com/pydicom/pylibjpeg-data 274 | # python -m pip install -U --pre --find-links dist/ pylibjpeg-openjpeg 275 | # python -m pytest --pyargs openjpeg.tests 276 | # deactivate 277 | # 278 | # - name: Install from package tarball and test 279 | # run: | 280 | # python -m venv testsrc 281 | # source testsrc/bin/activate 282 | # python -m pip install -U pip 283 | # python -m pip install pytest pydicom pylibjpeg 284 | # python -m pip uninstall -y pylibjpeg-openjpeg 285 | # python -m pip install git+https://github.com/pydicom/pylibjpeg-data 286 | # python -m pip install -U dist/pylibjpeg*openjpeg-*.tar.gz 287 | # python -m pytest --pyargs openjpeg.tests 288 | # deactivate 289 | 290 | # The pypi upload fails with non-linux containers, so grab the uploaded 291 | # artifacts and run using those 292 | # See: https://github.com/pypa/gh-action-pypi-publish/discussions/15 293 | deploy: 294 | name: Upload wheels to PyPI 295 | # needs: [ test-package ] 296 | needs: [ build-wheels, build-sdist, build-wheels-macos-arm64 ] 297 | runs-on: ubuntu-latest 298 | environment: 299 | name: pypi 300 | url: https://pypi.org/project/pylibjpeg-openjpeg/ 301 | permissions: 302 | id-token: write 303 | 304 | steps: 305 | - name: Download the wheels 306 | uses: actions/download-artifact@v6 307 | with: 308 | path: dist/ 309 | merge-multiple: true 310 | 311 | - name: Publish package to PyPi 312 | uses: pypa/gh-action-pypi-publish@release/v1 313 | -------------------------------------------------------------------------------- /openjpeg/tests/test_decode.py: -------------------------------------------------------------------------------- 1 | """Unit tests for openjpeg.""" 2 | 3 | from io import BytesIO 4 | 5 | try: 6 | from pydicom.encaps import generate_frames 7 | from pydicom.pixels.utils import ( 8 | reshape_pixel_array, 9 | pixel_dtype, 10 | ) 11 | 12 | HAS_PYDICOM = True 13 | except ImportError: 14 | HAS_PYDICOM = False 15 | 16 | import numpy as np 17 | import pytest 18 | 19 | from openjpeg.data import get_indexed_datasets, JPEG_DIRECTORY 20 | from openjpeg.utils import ( 21 | get_openjpeg_version, 22 | decode, 23 | decode_pixel_data, 24 | _get_format, 25 | ) 26 | 27 | 28 | DIR_15444 = JPEG_DIRECTORY / "15444" 29 | 30 | 31 | REF_DCM = { 32 | "1.2.840.10008.1.2.4.90": [ 33 | # filename, (rows, columns, samples/px, bits/sample, signed?) 34 | ("693_J2KR.dcm", (512, 512, 1, 14, True)), 35 | ("966_fixed.dcm", (2128, 2000, 1, 12, False)), 36 | ("emri_small_jpeg_2k_lossless.dcm", (64, 64, 1, 16, False)), 37 | ("explicit_VR-UN.dcm", (512, 512, 1, 16, True)), 38 | ("GDCMJ2K_TextGBR.dcm", (400, 400, 3, 8, False)), 39 | ("JPEG2KLossless_1s_1f_u_16_16.dcm", (1416, 1420, 1, 16, False)), 40 | ("MR_small_jp2klossless.dcm", (64, 64, 1, 16, True)), 41 | ("MR2_J2KR.dcm", (1024, 1024, 1, 12, False)), 42 | ("NM_Kakadu44_SOTmarkerincons.dcm", (2500, 2048, 1, 16, False)), 43 | ("RG1_J2KR.dcm", (1955, 1841, 1, 15, False)), 44 | ("RG3_J2KR.dcm", (1760, 1760, 1, 10, False)), 45 | ("TOSHIBA_J2K_OpenJPEGv2Regression.dcm", (512, 512, 1, 16, True)), 46 | ("TOSHIBA_J2K_SIZ0_PixRep1.dcm", (512, 512, 1, 16, True)), 47 | ("TOSHIBA_J2K_SIZ1_PixRep0.dcm", (512, 512, 1, 16, False)), 48 | ("US1_J2KR.dcm", (480, 640, 3, 8, False)), 49 | ], 50 | "1.2.840.10008.1.2.4.91": [ 51 | ("693_J2KI.dcm", (512, 512, 1, 16, True)), 52 | ("ELSCINT1_JP2vsJ2K.dcm", (512, 512, 1, 12, False)), 53 | ("JPEG2000.dcm", (1024, 256, 1, 16, True)), 54 | ("MAROTECH_CT_JP2Lossy.dcm", (716, 512, 1, 12, False)), 55 | ("MR2_J2KI.dcm", (1024, 1024, 1, 12, False)), 56 | ("OsirixFake16BitsStoredFakeSpacing.dcm", (224, 176, 1, 16, False)), 57 | ("RG1_J2KI.dcm", (1955, 1841, 1, 15, False)), 58 | ("RG3_J2KI.dcm", (1760, 1760, 1, 10, False)), 59 | ("SC_rgb_gdcm_KY.dcm", (100, 100, 3, 8, False)), 60 | ("US1_J2KI.dcm", (480, 640, 3, 8, False)), 61 | ], 62 | } 63 | 64 | 65 | def test_version(): 66 | """Test that the openjpeg version can be retrieved.""" 67 | version = get_openjpeg_version() 68 | assert isinstance(version, tuple) 69 | assert isinstance(version[0], int) 70 | assert 3 == len(version) 71 | assert 2 == version[0] 72 | assert 5 == version[1] 73 | 74 | 75 | def get_frame_generator(ds): 76 | """Return a frame generator for DICOM datasets.""" 77 | nr_frames = ds.get("NumberOfFrames", 1) 78 | return generate_frames(ds.PixelData, number_of_frames=nr_frames) 79 | 80 | 81 | def test_get_format_raises(): 82 | """Test get_format() raises for an unknown magic number""" 83 | buffer = BytesIO(b"\x00" * 20) 84 | msg = "No matching JPEG 2000 format found" 85 | with pytest.raises(ValueError, match=msg): 86 | _get_format(buffer) 87 | 88 | 89 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No pydicom") 90 | def test_bad_decode(): 91 | """Test trying to decode bad data.""" 92 | index = get_indexed_datasets("1.2.840.10008.1.2.4.90") 93 | ds = index["966.dcm"]["ds"] 94 | frame = next(get_frame_generator(ds)) 95 | msg = r"Error decoding the J2K data: failed to decode image" 96 | with pytest.raises(RuntimeError, match=msg): 97 | decode(frame) 98 | 99 | with pytest.raises(RuntimeError, match=msg): 100 | decode_pixel_data(frame, version=2) 101 | 102 | 103 | class TestDecode: 104 | """General tests for decode.""" 105 | 106 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No pydicom") 107 | def test_decode_bytes(self): 108 | """Test decoding using bytes.""" 109 | index = get_indexed_datasets("1.2.840.10008.1.2.4.90") 110 | ds = index["MR_small_jp2klossless.dcm"]["ds"] 111 | frame = next(get_frame_generator(ds)) 112 | assert isinstance(frame, bytes) 113 | arr = decode(frame) 114 | assert arr.flags.writeable 115 | assert " bytes: 66 | """Return the openjpeg version as bytes.""" 67 | cdef char *version = OpenJpegVersion() 68 | 69 | return version 70 | 71 | 72 | def decode( 73 | fp: BinaryIO, 74 | codec: int = 0, 75 | as_array: bool = False 76 | ) -> Union[np.ndarray, bytearray]: 77 | """Return the decoded JPEG 2000 data from Python file-like `fp`. 78 | 79 | Parameters 80 | ---------- 81 | fp : file-like 82 | A Python file-like containing the encoded JPEG 2000 data. Must have 83 | ``tell()``, ``seek()`` and ``read()`` methods. 84 | codec : int, optional 85 | The codec to use for decoding, one of: 86 | 87 | * ``0``: JPEG-2000 codestream 88 | * ``1``: JPT-stream (JPEG 2000, JPIP) 89 | * ``2``: JP2 file format 90 | as_array : bool, optional 91 | If ``True`` then return the decoded image data as a :class:`numpy.ndarray` 92 | otherwise return the data as a :class:`bytearray` (default). 93 | 94 | Returns 95 | ------- 96 | bytearray | numpy.ndarray 97 | If `as_array` is False (default) then returns the decoded image data 98 | as a :class:`bytearray`, otherwise returns the image data as a 99 | :class:`numpy.ndarray`. 100 | 101 | Raises 102 | ------ 103 | RuntimeError 104 | If unable to decode the JPEG 2000 data. 105 | """ 106 | param = get_parameters(fp, codec) 107 | bpp = ceil(param['precision'] / 8) 108 | if bpp == 3: 109 | bpp = 4 110 | nr_bytes = param['rows'] * param['columns'] * param['samples_per_pixel'] * bpp 111 | 112 | cdef PyObject* p_in = fp 113 | cdef unsigned char *p_out 114 | if as_array: 115 | out = np.zeros(nr_bytes, dtype=np.uint8) 116 | p_out = cnp.PyArray_DATA(out) 117 | else: 118 | out = bytearray(nr_bytes) 119 | p_out = out 120 | 121 | return_code = Decode(p_in, p_out, codec) 122 | 123 | return return_code, out 124 | 125 | 126 | def get_parameters(fp: BinaryIO, codec: int = 0) -> Dict[str, Union[str, int, bool]]: 127 | """Return a :class:`dict` containing the JPEG 2000 image parameters. 128 | 129 | Parameters 130 | ---------- 131 | fp : file-like 132 | A Python file-like containing the encoded JPEG 2000 data. 133 | codec : int, optional 134 | The codec to use for decoding, one of: 135 | 136 | * ``0``: JPEG-2000 codestream 137 | * ``1``: JPT-stream (JPEG 2000, JPIP) 138 | * ``2``: JP2 file format 139 | 140 | Returns 141 | ------- 142 | dict 143 | A :class:`dict` containing the J2K image parameters: 144 | ``{'columns': int, 'rows': int, 'colourspace': str, 145 | 'samples_per_pixel: int, 'precision': int, `is_signed`: bool, 146 | 'nr_tiles: int'}``. Possible colour spaces are "unknown", 147 | "unspecified", "sRGB", "monochrome", "YUV", "e-YCC" and "CYMK". 148 | 149 | Raises 150 | ------ 151 | RuntimeError 152 | If unable to decode the JPEG 2000 data. 153 | """ 154 | cdef JPEG2000Parameters param 155 | param.columns = 0 156 | param.rows = 0 157 | param.colourspace = 0 158 | param.samples_per_pixel = 0 159 | param.precision = 0 160 | param.is_signed = 0 161 | param.nr_tiles = 0 162 | 163 | # Pointer to the JPEGParameters object 164 | cdef JPEG2000Parameters *p_param = ¶m 165 | 166 | # Pointer to J2K data 167 | cdef PyObject* ptr = fp 168 | 169 | # Decode the data - output is written to output_buffer 170 | result = GetParameters(ptr, codec, p_param) 171 | if result != 0: 172 | try: 173 | msg = f": {ERRORS[result]}" 174 | except KeyError: 175 | pass 176 | 177 | raise RuntimeError("Error decoding the J2K data" + msg) 178 | 179 | # From openjpeg.h#L309 180 | colours = { 181 | -1: "unknown", 182 | 0: "unspecified", 183 | 1: "sRGB", 184 | 2: "monochrome", 185 | 3: "YUV", 186 | 4: "e-YCC", 187 | 5: "CYMK", 188 | } 189 | 190 | try: 191 | colourspace = colours[param.colourspace] 192 | except KeyError: 193 | colourspace = "unknown" 194 | 195 | parameters = { 196 | 'rows' : param.rows, 197 | 'columns' : param.columns, 198 | 'colourspace' : colourspace, 199 | 'samples_per_pixel' : param.samples_per_pixel, 200 | 'precision' : param.precision, 201 | 'is_signed' : bool(param.is_signed), 202 | 'nr_tiles' : param.nr_tiles, 203 | } 204 | 205 | return parameters 206 | 207 | 208 | def encode_array( 209 | cnp.ndarray arr, 210 | int bits_stored, 211 | int photometric_interpretation, 212 | bint use_mct, 213 | List[float] compression_ratios, 214 | List[float] signal_noise_ratios, 215 | int codec_format, 216 | ) -> Tuple[int, bytes]: 217 | """Return the JPEG 2000 compressed `arr`. 218 | 219 | Parameters 220 | ---------- 221 | arr : numpy.ndarray 222 | The array containing the image data to be encoded. 223 | bits_stored : int, optional 224 | The number of bits used per pixel. 225 | photometric_interpretation : int 226 | The colour space of the unencoded image data that will be set in the 227 | JP2 metadata (if `codec_format` is ``1``). 228 | use_mct : bool 229 | If ``True`` then apply multi-component transformation (MCT) to RGB 230 | images. 231 | compression_ratios : list[float] 232 | Required for lossy encoding, this is the compression ratio to use 233 | for each quality layer. Cannot be used with `signal_noise_ratios`. 234 | signal_noise_ratios : list[float] 235 | Required for lossy encoding, this is the PSNR to use for each quality 236 | layer. Cannot be used with `compression_ratios`. 237 | codec_format : int 238 | The codec to used when encoding: 239 | 240 | * ``0``: JPEG 2000 codestream only (default) (J2K/J2C format) 241 | * ``1``: A boxed JPEG 2000 codestream (JP2 format) 242 | 243 | Returns 244 | ------- 245 | tuple[int, bytes] 246 | The return code of the encoding and the encoded image data. The return 247 | code will be ``0`` for success, otherwise the encoding failed. 248 | """ 249 | if not (1 <= bits_stored <= arr.dtype.itemsize * 8): 250 | raise ValueError( 251 | "Invalid value for the 'bits_stored' parameter, the value must be " 252 | f"in the range (1, {arr.dtype.itemsize * 8})" 253 | ) 254 | 255 | kind, itemsize = arr.dtype.kind, arr.dtype.itemsize 256 | if kind not in ("b", "i", "u") or itemsize not in (1, 2, 4): 257 | raise ValueError( 258 | f"The input array has an unsupported dtype '{arr.dtype}', only bool, " 259 | "u1, u2, u4, i1, i2 and i4 are supported" 260 | ) 261 | 262 | # It seems like OpenJPEG can only encode up to 24 bits, although theoretically 263 | # based on their use of OPJ_INT32 for pixel values, it should be 32-bit for 264 | # signed and 31 bit for unsigned. Maybe I've made a mistake somewhere? 265 | arr_max = arr.max() 266 | arr_min = arr.min() 267 | if ( 268 | (kind == "u" and itemsize == 4 and arr_max > 2**24 - 1) 269 | or (kind == "i" and itemsize == 4 and (arr_max > 2**23 - 1 or arr_min < -2**23)) 270 | ): 271 | raise ValueError( 272 | "The input array contains values outside the range of the maximum " 273 | "supported bit-depth of 24" 274 | ) 275 | 276 | # Check the array matches bits_stored 277 | if kind == "u" and itemsize in (1, 2, 4) and arr_max > 2**bits_stored - 1: 278 | raise ValueError( 279 | f"A 'bits_stored' value of {bits_stored} is incompatible with " 280 | f"the range of pixel data in the input array: ({arr_min}, {arr_max})" 281 | ) 282 | 283 | if ( 284 | kind == "i" and itemsize in (1, 2, 4) 285 | and (arr_max > 2**(bits_stored - 1) - 1 or arr_min < -2**(bits_stored - 1)) 286 | ): 287 | raise ValueError( 288 | f"A 'bits_stored' value of {bits_stored} is incompatible with " 289 | f"the range of pixel data in the input array: ({arr_min}, {arr_max})" 290 | ) 291 | 292 | # MCT may be used with RGB in both lossy and lossless modes 293 | use_mct = 1 if use_mct else 0 294 | 295 | if codec_format not in (0, 1): 296 | raise ValueError( 297 | f"Invalid 'codec_format' value '{codec_format}', must be 0 or 1" 298 | ) 299 | 300 | compression_ratios = [float(x) for x in compression_ratios] 301 | signal_noise_ratios = [float(x) for x in signal_noise_ratios] 302 | if compression_ratios and signal_noise_ratios: 303 | raise ValueError( 304 | "Only one of 'compression_ratios' or 'signal_noise_ratios' is " 305 | "allowed when performing lossy compression" 306 | ) 307 | if len(compression_ratios) > 100 or len(signal_noise_ratios) > 100: 308 | raise ValueError("More than 100 compression layers is not supported") 309 | 310 | # The destination for the encoded J2K codestream, needs to support BinaryIO 311 | dst = BytesIO() 312 | return_code = EncodeArray( 313 | arr, 314 | dst, 315 | bits_stored, 316 | photometric_interpretation, 317 | use_mct, 318 | compression_ratios, 319 | signal_noise_ratios, 320 | codec_format, 321 | ) 322 | return return_code, dst.getvalue() 323 | 324 | 325 | def encode_buffer( 326 | src, 327 | int columns, 328 | int rows, 329 | int samples_per_pixel, 330 | int bits_stored, 331 | int is_signed, 332 | int photometric_interpretation, 333 | int use_mct, 334 | List[float] compression_ratios, 335 | List[float] signal_noise_ratios, 336 | int codec_format, 337 | ) -> Tuple[int, bytes]: 338 | """Return the JPEG 2000 compressed `src`. 339 | 340 | If performing lossy encoding then either `compression_ratios` or 341 | `signal_noise_ratios` must be set to a non-empty list, otherwise lossless 342 | encoding will be used. 343 | 344 | Parameters 345 | ---------- 346 | src : bytes | bytearray 347 | A bytes or bytearray containing the image data to be encoded, ordered as 348 | little endian and colour-by-pixel. 349 | columns : int 350 | The number of columns in the image, should be in the range [1, 65535]. 351 | rows : int 352 | The number of rows in the image, should be in the range [1, 65535]. 353 | samples_per_pixel : int 354 | The number of samples per pixel, should be 1, 3 or 4. 355 | bits_stored : int 356 | The number of bits used per pixel (i.e. the sample precision), should be 357 | in the range [1, 24]. 358 | is_signed: int 359 | ``0`` if the image uses unsigned pixels, ``1`` for signed. 360 | photometric_interpretation : int 361 | The colour space of the unencoded image data that will be set in the 362 | JP2 metadata (if `codec_format` is ``1``). 363 | use_mct : bool 364 | If ``1`` then apply multi-component transformation (MCT) to RGB 365 | images. Requires a `photometric_interpretation` of ``1`` and a 366 | `samples_per_pixel` value of ``3``, otherwise no MCT will be used. 367 | compression_ratios : list[float] 368 | Required for lossy encoding, this is the compression ratio to use 369 | for each quality layer. Cannot be used with `signal_noise_ratios`. 370 | signal_noise_ratios : list[float] 371 | Required for lossy encoding, this is the PSNR to use for each quality 372 | layer. Cannot be used with `compression_ratios`. 373 | codec_format : int, optional 374 | The codec to used when encoding: 375 | 376 | * ``0``: JPEG 2000 codestream only (default) (J2K/J2C format) 377 | * ``1``: A boxed JPEG 2000 codestream (JP2 format) 378 | 379 | Returns 380 | ------- 381 | tuple[int, bytes] 382 | The return code of the encoding and the JPEG 2000 encoded image data. 383 | The return code will be ``0`` for success, otherwise the encoding 384 | failed. 385 | """ 386 | # Checks 387 | if not isinstance(src, (bytes, bytearray)): 388 | raise TypeError( 389 | f"'src' must be bytes or bytearray, not {type(src).__name__}" 390 | ) 391 | 392 | if not 1 <= columns <= 65535: 393 | raise ValueError( 394 | f"Invalid 'columns' value '{columns}', must be in the range [1, 65535]" 395 | ) 396 | 397 | if not 1 <= rows <= 65535: 398 | raise ValueError( 399 | f"Invalid 'rows' value '{rows}', must be in the range [1, 65535]" 400 | ) 401 | 402 | if samples_per_pixel not in (1, 3, 4): 403 | raise ValueError( 404 | f"Invalid 'samples_per_pixel' value '{samples_per_pixel}', must be 1, 3 " 405 | "or 4" 406 | ) 407 | 408 | if 0 < bits_stored <= 8: 409 | bytes_allocated = 1 410 | elif 8 < bits_stored <= 16: 411 | bytes_allocated = 2 412 | elif 16 < bits_stored <= 24: 413 | bytes_allocated = 4 414 | else: 415 | raise ValueError( 416 | f"Invalid 'bits_stored' value '{bits_stored}', must be in the " 417 | "range [1, 24]" 418 | ) 419 | 420 | actual_length = len(src) 421 | expected_length = rows * columns * samples_per_pixel * bytes_allocated 422 | if actual_length != expected_length: 423 | raise ValueError( 424 | f"The length of 'src' is {actual_length} bytes which doesn't " 425 | f"match the expected length of {expected_length} bytes" 426 | ) 427 | 428 | if is_signed not in (0, 1): 429 | raise ValueError(f"Invalid 'is_signed' value '{is_signed}'") 430 | 431 | if photometric_interpretation not in (0, 1, 2, 3, 4, 5): 432 | raise ValueError( 433 | "Invalid 'photometric_interpretation' value " 434 | f"'{photometric_interpretation}', must be in the range [0, 5]" 435 | ) 436 | 437 | if use_mct not in (0, 1): 438 | raise ValueError(f"Invalid 'use_mct' value '{use_mct}'") 439 | 440 | if codec_format not in (0, 1): 441 | raise ValueError( 442 | f"Invalid 'codec_format' value '{codec_format}', must be 0 or 1" 443 | ) 444 | 445 | compression_ratios = [float(x) for x in compression_ratios] 446 | signal_noise_ratios = [float(x) for x in signal_noise_ratios] 447 | if compression_ratios and signal_noise_ratios: 448 | raise ValueError( 449 | "Only one of 'compression_ratios' or 'signal_noise_ratios' is " 450 | "allowed when performing lossy compression" 451 | ) 452 | if len(compression_ratios) > 100 or len(signal_noise_ratios) > 100: 453 | raise ValueError("More than 100 compression layers is not supported") 454 | 455 | dst = BytesIO() 456 | return_code = EncodeBuffer( 457 | src, 458 | columns, 459 | rows, 460 | samples_per_pixel, 461 | bits_stored, 462 | is_signed, 463 | photometric_interpretation, 464 | dst, 465 | use_mct, 466 | compression_ratios, 467 | signal_noise_ratios, 468 | codec_format, 469 | ) 470 | return return_code, dst.getvalue() 471 | -------------------------------------------------------------------------------- /lib/interface/decode.c: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | py_read, py_seek, py_skip and py_tell are adapted from 4 | Pillow/src/libimaging.codec_fd.c which is licensed under the PIL Software 5 | License: 6 | 7 | The Python Imaging Library (PIL) is 8 | 9 | Copyright © 1997-2011 by Secret Labs AB 10 | Copyright © 1995-2011 by Fredrik Lundh 11 | 12 | Pillow is the friendly PIL fork. It is 13 | 14 | Copyright © 2010-2020 by Alex Clark and contributors 15 | 16 | Like PIL, Pillow is licensed under the open source PIL Software License: 17 | 18 | By obtaining, using, and/or copying this software and/or its associated 19 | documentation, you agree that you have read, understood, and will comply 20 | with the following terms and conditions: 21 | 22 | Permission to use, copy, modify, and distribute this software and its 23 | associated documentation for any purpose and without fee is hereby granted, 24 | provided that the above copyright notice appears in all copies, and that 25 | both that copyright notice and this permission notice appear in supporting 26 | documentation, and that the name of Secret Labs AB or the author not be 27 | used in advertising or publicity pertaining to distribution of the software 28 | without specific, written prior permission. 29 | 30 | SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS 31 | SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. 32 | IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR BE LIABLE FOR ANY SPECIAL, 33 | INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 34 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE 35 | OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 36 | PERFORMANCE OF THIS SOFTWARE. 37 | 38 | ------------------------------------------------------------------------------ 39 | 40 | `upsample_image_components` and bits and pieces of the rest are taken/adapted 41 | from openjpeg/src/bin/jp2/opj_decompress.c which is licensed under the 2-clause 42 | BSD license (see the main LICENSE file). 43 | */ 44 | 45 | #include "Python.h" 46 | #include 47 | #include 48 | #include <../openjpeg/src/lib/openjp2/openjpeg.h> 49 | #include "color.h" 50 | #include "utils.h" 51 | 52 | 53 | static void py_error(const char *msg) { 54 | py_log("openjpeg.decode", "ERROR", msg); 55 | } 56 | 57 | static void info_callback(const char *msg, void *callback) { 58 | py_log("openjpeg.decode", "INFO", msg); 59 | } 60 | 61 | static void warning_callback(const char *msg, void *callback) { 62 | py_log("openjpeg.decode", "WARNING", msg); 63 | } 64 | 65 | static void error_callback(const char *msg, void *callback) { 66 | py_error(msg); 67 | } 68 | 69 | 70 | const char * OpenJpegVersion(void) 71 | { 72 | /* Return the openjpeg version as char array 73 | 74 | Returns 75 | ------- 76 | char * 77 | The openjpeg version as MAJOR.MINOR.PATCH 78 | */ 79 | return opj_version(); 80 | } 81 | 82 | 83 | // parameters for decoding 84 | typedef struct opj_decompress_params { 85 | /** core library parameters */ 86 | opj_dparameters_t core; 87 | 88 | /** input file format 0: J2K, 1: JP2, 2: JPT */ 89 | int decod_format; 90 | 91 | /** Decoding area left boundary */ 92 | OPJ_UINT32 DA_x0; 93 | /** Decoding area right boundary */ 94 | OPJ_UINT32 DA_x1; 95 | /** Decoding area up boundary */ 96 | OPJ_UINT32 DA_y0; 97 | /** Decoding area bottom boundary */ 98 | OPJ_UINT32 DA_y1; 99 | /** Verbose mode */ 100 | OPJ_BOOL m_verbose; 101 | 102 | /** tile number of the decoded tile */ 103 | OPJ_UINT32 tile_index; 104 | /** Nb of tile to decode */ 105 | OPJ_UINT32 nb_tile_to_decode; 106 | 107 | /* force output colorspace to RGB */ 108 | //int force_rgb; 109 | /** number of threads */ 110 | //int num_threads; 111 | /* Quiet */ 112 | //int quiet; 113 | /** number of components to decode */ 114 | OPJ_UINT32 numcomps; 115 | /** indices of components to decode */ 116 | OPJ_UINT32* comps_indices; 117 | } opj_decompress_parameters; 118 | 119 | 120 | // Decoding stuff 121 | static void set_default_parameters(opj_decompress_parameters* parameters) 122 | { 123 | if (parameters) 124 | { 125 | memset(parameters, 0, sizeof(opj_decompress_parameters)); 126 | 127 | /* default decoding parameters (command line specific) */ 128 | parameters->decod_format = -1; 129 | /* default decoding parameters (core) */ 130 | opj_set_default_decoder_parameters(&(parameters->core)); 131 | } 132 | } 133 | 134 | 135 | static void destroy_parameters(opj_decompress_parameters* parameters) 136 | { 137 | if (parameters) 138 | { 139 | free(parameters->comps_indices); 140 | parameters->comps_indices = NULL; 141 | } 142 | } 143 | 144 | 145 | typedef struct JPEG2000Parameters { 146 | OPJ_UINT32 columns; // width in pixels 147 | OPJ_UINT32 rows; // height in pixels 148 | OPJ_COLOR_SPACE colourspace; // the colour space 149 | OPJ_UINT32 nr_components; // number of components 150 | OPJ_UINT32 precision; // precision of the components (in bits) 151 | unsigned int is_signed; // 0 for unsigned, 1 for signed 152 | OPJ_UINT32 nr_tiles; // number of tiles 153 | } j2k_parameters_t; 154 | 155 | 156 | extern int GetParameters(PyObject* fd, int codec_format, j2k_parameters_t *output) 157 | { 158 | /* Decode a JPEG 2000 header for the image meta data. 159 | 160 | Parameters 161 | ---------- 162 | fd : PyObject * 163 | The Python stream object containing the JPEG 2000 data to be decoded. 164 | codec_format : int 165 | The format of the JPEG 2000 data, one of: 166 | * ``0`` - OPJ_CODEC_J2K : JPEG-2000 codestream 167 | * ``1`` - OPJ_CODEC_JPT : JPT-stream (JPEG 2000, JPIP) 168 | * ``2`` - OPJ_CODEC_JP2 : JP2 file format 169 | output : j2k_parameters_t * 170 | The struct where the parameters will be stored. 171 | 172 | Returns 173 | ------- 174 | int 175 | The exit status, 0 for success, failure otherwise. 176 | */ 177 | // J2K stream 178 | opj_stream_t *stream = NULL; 179 | // struct defining image data and characteristics 180 | opj_image_t *image = NULL; 181 | // J2K codec 182 | opj_codec_t *codec = NULL; 183 | // Setup decompression parameters 184 | opj_decompress_parameters parameters; 185 | set_default_parameters(¶meters); 186 | 187 | int return_code = EXIT_FAILURE; 188 | 189 | // Creates an abstract input stream; allocates memory 190 | stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE); 191 | 192 | if (!stream) 193 | { 194 | // Failed to create the input stream 195 | return_code = 1; 196 | goto failure; 197 | } 198 | 199 | // Functions for the stream 200 | opj_stream_set_read_function(stream, py_read); 201 | opj_stream_set_skip_function(stream, py_skip); 202 | opj_stream_set_seek_function(stream, py_seek_set); 203 | opj_stream_set_user_data(stream, fd, NULL); 204 | opj_stream_set_user_data_length(stream, py_length(fd)); 205 | 206 | codec = opj_create_decompress(codec_format); 207 | 208 | /* Setup the decoder parameters */ 209 | if (!opj_setup_decoder(codec, &(parameters.core))) 210 | { 211 | // failed to setup the decoder 212 | return_code = 2; 213 | goto failure; 214 | } 215 | 216 | /* Read the main header of the codestream and if necessary the JP2 boxes*/ 217 | if (!opj_read_header(stream, codec, &image)) 218 | { 219 | // failed to read the header 220 | return_code = 3; 221 | goto failure; 222 | } 223 | 224 | output->colourspace = image->color_space; 225 | output->columns = image->x1; 226 | output->rows = image->y1; 227 | output->nr_components = image->numcomps; 228 | output->precision = (int)image->comps[0].prec; 229 | output->is_signed = (int)image->comps[0].sgnd; 230 | output->nr_tiles = parameters.nb_tile_to_decode; 231 | 232 | destroy_parameters(¶meters); 233 | opj_destroy_codec(codec); 234 | opj_image_destroy(image); 235 | opj_stream_destroy(stream); 236 | 237 | return EXIT_SUCCESS; 238 | 239 | failure: 240 | destroy_parameters(¶meters); 241 | if (codec) 242 | opj_destroy_codec(codec); 243 | if (image) 244 | opj_image_destroy(image); 245 | if (stream) 246 | opj_stream_destroy(stream); 247 | 248 | return return_code; 249 | } 250 | 251 | 252 | static opj_image_t* upsample_image_components(opj_image_t* original) 253 | { 254 | // Basically a straight copy from opj_decompress.c 255 | opj_image_t* l_new_image = NULL; 256 | opj_image_cmptparm_t* l_new_components = NULL; 257 | OPJ_BOOL upsample = OPJ_FALSE; 258 | OPJ_UINT32 ii; 259 | 260 | for (ii = 0U; ii < original->numcomps; ++ii) 261 | { 262 | if ((original->comps[ii].dx > 1U) || (original->comps[ii].dy > 1U)) 263 | { 264 | upsample = OPJ_TRUE; 265 | break; 266 | } 267 | } 268 | 269 | // No upsampling required 270 | if (!upsample) 271 | { 272 | return original; 273 | } 274 | 275 | l_new_components = (opj_image_cmptparm_t*)malloc( 276 | original->numcomps * sizeof(opj_image_cmptparm_t) 277 | ); 278 | if (l_new_components == NULL) { 279 | // Failed to allocate memory for component parameters 280 | opj_image_destroy(original); 281 | return NULL; 282 | } 283 | 284 | for (ii = 0U; ii < original->numcomps; ++ii) 285 | { 286 | opj_image_cmptparm_t* l_new_cmp = &(l_new_components[ii]); 287 | opj_image_comp_t* l_org_cmp = &(original->comps[ii]); 288 | 289 | l_new_cmp->prec = l_org_cmp->prec; 290 | l_new_cmp->sgnd = l_org_cmp->sgnd; 291 | l_new_cmp->x0 = original->x0; 292 | l_new_cmp->y0 = original->y0; 293 | l_new_cmp->dx = 1; 294 | l_new_cmp->dy = 1; 295 | // should be original->x1 - original->x0 for dx==1 296 | l_new_cmp->w = l_org_cmp->w; 297 | // should be original->y1 - original->y0 for dy==0 298 | l_new_cmp->h = l_org_cmp->h; 299 | 300 | if (l_org_cmp->dx > 1U) 301 | { 302 | l_new_cmp->w = original->x1 - original->x0; 303 | } 304 | 305 | if (l_org_cmp->dy > 1U) { 306 | l_new_cmp->h = original->y1 - original->y0; 307 | } 308 | } 309 | 310 | l_new_image = opj_image_create( 311 | original->numcomps, l_new_components, original->color_space 312 | ); 313 | free(l_new_components); 314 | 315 | if (l_new_image == NULL) { 316 | // Failed to allocate memory for image 317 | opj_image_destroy(original); 318 | return NULL; 319 | } 320 | 321 | l_new_image->x0 = original->x0; 322 | l_new_image->x1 = original->x1; 323 | l_new_image->y0 = original->y0; 324 | l_new_image->y1 = original->y1; 325 | 326 | for (ii = 0U; ii < original->numcomps; ++ii) 327 | { 328 | opj_image_comp_t* l_new_cmp = &(l_new_image->comps[ii]); 329 | opj_image_comp_t* l_org_cmp = &(original->comps[ii]); 330 | 331 | l_new_cmp->alpha = l_org_cmp->alpha; 332 | l_new_cmp->resno_decoded = l_org_cmp->resno_decoded; 333 | 334 | if ((l_org_cmp->dx > 1U) || (l_org_cmp->dy > 1U)) 335 | { 336 | const OPJ_INT32* l_src = l_org_cmp->data; 337 | OPJ_INT32* l_dst = l_new_cmp->data; 338 | OPJ_UINT32 y; 339 | OPJ_UINT32 xoff, yoff; 340 | 341 | // need to take into account dx & dy 342 | xoff = l_org_cmp->dx * l_org_cmp->x0 - original->x0; 343 | yoff = l_org_cmp->dy * l_org_cmp->y0 - original->y0; 344 | 345 | // Invalid components found 346 | if ((xoff >= l_org_cmp->dx) || (yoff >= l_org_cmp->dy)) 347 | { 348 | opj_image_destroy(original); 349 | opj_image_destroy(l_new_image); 350 | return NULL; 351 | } 352 | 353 | for (y = 0U; y < yoff; ++y) { 354 | memset(l_dst, 0U, l_new_cmp->w * sizeof(OPJ_INT32)); 355 | l_dst += l_new_cmp->w; 356 | } 357 | 358 | if (l_new_cmp->h > (l_org_cmp->dy - 1U)) 359 | { /* check subtraction overflow for really small images */ 360 | for (; y < l_new_cmp->h - (l_org_cmp->dy - 1U); y += l_org_cmp->dy) 361 | { 362 | OPJ_UINT32 x, dy; 363 | OPJ_UINT32 xorg; 364 | 365 | xorg = 0U; 366 | for (x = 0U; x < xoff; ++x) 367 | { 368 | l_dst[x] = 0; 369 | } 370 | if (l_new_cmp->w > (l_org_cmp->dx - 1U)) 371 | { /* check subtraction overflow for really small images */ 372 | for (; x < l_new_cmp->w - (l_org_cmp->dx - 1U); x += l_org_cmp->dx, ++xorg) 373 | { 374 | OPJ_UINT32 dx; 375 | for (dx = 0U; dx < l_org_cmp->dx; ++dx) 376 | { 377 | l_dst[x + dx] = l_src[xorg]; 378 | } 379 | } 380 | } 381 | for (; x < l_new_cmp->w; ++x) 382 | { 383 | l_dst[x] = l_src[xorg]; 384 | } 385 | l_dst += l_new_cmp->w; 386 | 387 | for (dy = 1U; dy < l_org_cmp->dy; ++dy) 388 | { 389 | memcpy( 390 | l_dst, 391 | l_dst - l_new_cmp->w, 392 | l_new_cmp->w * sizeof(OPJ_INT32) 393 | ); 394 | l_dst += l_new_cmp->w; 395 | } 396 | l_src += l_org_cmp->w; 397 | } 398 | } 399 | 400 | if (y < l_new_cmp->h) 401 | { 402 | OPJ_UINT32 x; 403 | OPJ_UINT32 xorg; 404 | 405 | xorg = 0U; 406 | for (x = 0U; x < xoff; ++x) { 407 | l_dst[x] = 0; 408 | } 409 | 410 | if (l_new_cmp->w > (l_org_cmp->dx - 1U)) 411 | { /* check subtraction overflow for really small images */ 412 | for (; x < l_new_cmp->w - (l_org_cmp->dx - 1U); x += l_org_cmp->dx, ++xorg) 413 | { 414 | OPJ_UINT32 dx; 415 | for (dx = 0U; dx < l_org_cmp->dx; ++dx) 416 | { 417 | l_dst[x + dx] = l_src[xorg]; 418 | } 419 | } 420 | } 421 | 422 | for (; x < l_new_cmp->w; ++x) 423 | { 424 | l_dst[x] = l_src[xorg]; 425 | } 426 | l_dst += l_new_cmp->w; 427 | ++y; 428 | 429 | for (; y < l_new_cmp->h; ++y) { 430 | memcpy( 431 | l_dst, 432 | l_dst - l_new_cmp->w, 433 | l_new_cmp->w * sizeof(OPJ_INT32) 434 | ); 435 | l_dst += l_new_cmp->w; 436 | } 437 | } 438 | } else { // dx == dy == 1 439 | memcpy( 440 | l_new_cmp->data, 441 | l_org_cmp->data, 442 | sizeof(OPJ_INT32) * l_org_cmp->w * l_org_cmp->h 443 | ); 444 | } 445 | } 446 | 447 | opj_image_destroy(original); 448 | return l_new_image; 449 | } 450 | 451 | 452 | extern int Decode(PyObject* fd, unsigned char *out, int codec_format) 453 | { 454 | /* Decode JPEG 2000 data. 455 | 456 | Parameters 457 | ---------- 458 | fd : PyObject * 459 | The Python stream object containing the JPEG 2000 data to be decoded. 460 | out : unsigned char * 461 | Either a Python bytearray object or a numpy ndarray of uint8 to write the 462 | decoded image data to. Multi-byte decoded data will be written using little 463 | endian byte ordering. 464 | codec_format : int 465 | The format of the JPEG 2000 data, one of: 466 | * ``0`` - OPJ_CODEC_J2K : JPEG-2000 codestream 467 | * ``1`` - OPJ_CODEC_JPT : JPT-stream (JPEG 2000, JPIP) 468 | * ``2`` - OPJ_CODEC_JP2 : JP2 file format 469 | 470 | Returns 471 | ------- 472 | int 473 | The exit status, 0 for success, failure otherwise. 474 | */ 475 | // J2K stream 476 | opj_stream_t *stream = NULL; 477 | // struct defining image data and characteristics 478 | opj_image_t *image = NULL; 479 | // J2K codec 480 | opj_codec_t *codec = NULL; 481 | // Setup decompression parameters 482 | opj_decompress_parameters parameters; 483 | set_default_parameters(¶meters); 484 | // Array of pointers to the first element of each component 485 | int **p_component = NULL; 486 | 487 | int return_code = EXIT_FAILURE; 488 | 489 | /* Send info, warning, error message to Python logging */ 490 | opj_set_info_handler(codec, info_callback, NULL); 491 | opj_set_warning_handler(codec, warning_callback, NULL); 492 | opj_set_error_handler(codec, error_callback, NULL); 493 | 494 | // Creates an abstract input stream; allocates memory 495 | stream = opj_stream_create(BUFFER_SIZE, OPJ_TRUE); 496 | if (!stream) 497 | { 498 | // Failed to create the input stream 499 | return_code = 1; 500 | goto failure; 501 | } 502 | 503 | // Functions for the stream 504 | opj_stream_set_read_function(stream, py_read); 505 | opj_stream_set_skip_function(stream, py_skip); 506 | opj_stream_set_seek_function(stream, py_seek_set); 507 | opj_stream_set_user_data(stream, fd, NULL); 508 | opj_stream_set_user_data_length(stream, py_length(fd)); 509 | 510 | //opj_set_error_handler(codec, j2k_error, 00); 511 | 512 | codec = opj_create_decompress(codec_format); 513 | 514 | /* Setup the decoder parameters */ 515 | if (!opj_setup_decoder(codec, &(parameters.core))) 516 | { 517 | // failed to setup the decoder 518 | return_code = 2; 519 | goto failure; 520 | } 521 | 522 | /* Read the main header of the codestream and if necessary the JP2 boxes*/ 523 | if (! opj_read_header(stream, codec, &image)) 524 | { 525 | // failed to read the header 526 | return_code = 3; 527 | goto failure; 528 | } 529 | 530 | // TODO: add check that all components match 531 | 532 | if (parameters.numcomps) 533 | { 534 | if (!opj_set_decoded_components( 535 | codec, parameters.numcomps, 536 | parameters.comps_indices, OPJ_FALSE) 537 | ) 538 | { 539 | // failed to set the component indices 540 | return_code = 4; 541 | goto failure; 542 | } 543 | } 544 | 545 | if (!opj_set_decode_area( 546 | codec, image, 547 | (OPJ_INT32)parameters.DA_x0, 548 | (OPJ_INT32)parameters.DA_y0, 549 | (OPJ_INT32)parameters.DA_x1, 550 | (OPJ_INT32)parameters.DA_y1) 551 | ) 552 | { 553 | // failed to set the decoded area 554 | return_code = 5; 555 | goto failure; 556 | } 557 | 558 | /* Get the decoded image */ 559 | if (!(opj_decode(codec, stream, image) && opj_end_decompress(codec, stream))) 560 | { 561 | // failed to decode image 562 | return_code = 6; 563 | goto failure; 564 | } 565 | 566 | // Convert colour space (if required) 567 | // Colour space information is only available with JP2 568 | if ( 569 | image->color_space != OPJ_CLRSPC_SYCC 570 | && image->numcomps == 3 571 | && image->comps[0].dx == image->comps[0].dy 572 | && image->comps[1].dx != 1 573 | ) { 574 | image->color_space = OPJ_CLRSPC_SYCC; 575 | } 576 | 577 | if (image->color_space == OPJ_CLRSPC_SYCC) 578 | { 579 | color_sycc_to_rgb(image); 580 | } 581 | 582 | /* Upsample components (if required) */ 583 | image = upsample_image_components(image); 584 | if (image == NULL) { 585 | // failed to upsample image 586 | return_code = 8; 587 | goto failure; 588 | } 589 | 590 | // Set our component pointers 591 | const unsigned int NR_COMPONENTS = image->numcomps; // 15444-1 A.5.1 592 | p_component = malloc(NR_COMPONENTS * sizeof(int *)); 593 | for (unsigned int ii = 0; ii < NR_COMPONENTS; ii++) 594 | { 595 | p_component[ii] = image->comps[ii].data; 596 | //printf("%u, %u, %u\n", ii, image->comps[ii].dx, image->comps[ii].dy); 597 | } 598 | 599 | int width = (int)image->comps[0].w; 600 | int height = (int)image->comps[0].h; 601 | int precision = (int)image->comps[0].prec; 602 | 603 | // Our output should have planar configuration of 0, i.e. for RGB data 604 | // we have R1, B1, G1 | R2, G2, B2 | ..., where 1 is the first pixel, 605 | // 2 the second, etc 606 | // See DICOM Standard, Part 3, Annex C.7.6.3.1.3 607 | int row, col; 608 | unsigned int ii; 609 | if (precision <= 8) { 610 | // 8-bit signed/unsigned 611 | for (row = 0; row < height; row++) 612 | { 613 | for (col = 0; col < width; col++) 614 | { 615 | for (ii = 0; ii < NR_COMPONENTS; ii++) 616 | { 617 | *out = (unsigned char)(*p_component[ii]); 618 | out++; 619 | p_component[ii]++; 620 | } 621 | } 622 | } 623 | } else if (precision <= 16) { 624 | union { 625 | unsigned short val; 626 | unsigned char vals[2]; 627 | } u16; 628 | 629 | // 16-bit signed/unsigned 630 | for (row = 0; row < height; row++) 631 | { 632 | for (col = 0; col < width; col++) 633 | { 634 | for (ii = 0; ii < NR_COMPONENTS; ii++) 635 | { 636 | u16.val = (unsigned short)(*p_component[ii]); 637 | // Ensure little endian output 638 | #ifdef PYOJ_BIG_ENDIAN 639 | *out = u16.vals[1]; 640 | out++; 641 | *out = u16.vals[0]; 642 | out++; 643 | #else 644 | *out = u16.vals[0]; 645 | out++; 646 | *out = u16.vals[1]; 647 | out++; 648 | #endif 649 | 650 | p_component[ii]++; 651 | } 652 | } 653 | } 654 | } else if (precision <= 32) { 655 | union { 656 | OPJ_INT32 val; 657 | unsigned char vals[4]; 658 | } u32; 659 | 660 | // 32-bit signed/unsigned 661 | for (row = 0; row < height; row++) 662 | { 663 | for (col = 0; col < width; col++) 664 | { 665 | for (ii = 0; ii < NR_COMPONENTS; ii++) 666 | { 667 | u32.val = (OPJ_INT32)(*p_component[ii]); 668 | // Ensure little endian output 669 | #ifdef PYOJ_BIG_ENDIAN 670 | *out = u32.vals[3]; 671 | out++; 672 | *out = u32.vals[2]; 673 | out++; 674 | *out = u32.vals[1]; 675 | out++; 676 | *out = u32.vals[0]; 677 | out++; 678 | #else 679 | *out = u32.vals[0]; 680 | out++; 681 | *out = u32.vals[1]; 682 | out++; 683 | *out = u32.vals[2]; 684 | out++; 685 | *out = u32.vals[3]; 686 | out++; 687 | #endif 688 | 689 | p_component[ii]++; 690 | } 691 | } 692 | } 693 | } else { 694 | // Support for more than 32-bits per component is not implemented 695 | return_code = 7; 696 | goto failure; 697 | } 698 | 699 | if (p_component) 700 | { 701 | free(p_component); 702 | p_component = NULL; 703 | } 704 | destroy_parameters(¶meters); 705 | opj_destroy_codec(codec); 706 | opj_image_destroy(image); 707 | opj_stream_destroy(stream); 708 | 709 | return EXIT_SUCCESS; 710 | 711 | failure: 712 | if (p_component) 713 | { 714 | free(p_component); 715 | p_component = NULL; 716 | } 717 | destroy_parameters(¶meters); 718 | if (codec) 719 | opj_destroy_codec(codec); 720 | if (image) 721 | opj_image_destroy(image); 722 | if (stream) 723 | opj_stream_destroy(stream); 724 | 725 | return return_code; 726 | } 727 | -------------------------------------------------------------------------------- /openjpeg/utils.py: -------------------------------------------------------------------------------- 1 | from enum import IntEnum 2 | from io import BytesIO 3 | import logging 4 | from math import ceil, log 5 | import os 6 | from pathlib import Path 7 | from typing import BinaryIO, Tuple, Union, TYPE_CHECKING, Any, Dict, cast, List 8 | import warnings 9 | 10 | import numpy as np 11 | 12 | import _openjpeg 13 | 14 | 15 | if TYPE_CHECKING: # pragma: no cover 16 | from pydicom.dataset import Dataset 17 | 18 | 19 | LOGGER = logging.getLogger(__name__) 20 | 21 | 22 | class Version(IntEnum): 23 | v1 = 1 24 | v2 = 2 25 | 26 | 27 | class PhotometricInterpretation(IntEnum): 28 | MONOCHROME1 = 2 29 | MONOCHROME2 = 2 30 | PALETTE_COLOR = 2 31 | RGB = 1 32 | YBR_FULL = 3 33 | YBR_FULL_422 = 3 34 | 35 | 36 | MAGIC_NUMBERS = { 37 | # JPEG 2000 codestream, has no header, .j2k, .jpc, .j2c 38 | b"\xff\x4f\xff\x51": 0, 39 | # JP2 and JP2 RFC3745, .jp2 40 | b"\x0d\x0a\x87\x0a": 2, 41 | b"\x00\x00\x00\x0c\x6a\x50\x20\x20\x0d\x0a\x87\x0a": 2, 42 | # JPT, .jpt - shrug 43 | } 44 | DECODING_ERRORS = { 45 | 1: "failed to create the input stream", 46 | 2: "failed to setup the decoder", 47 | 3: "failed to read the header", 48 | 4: "failed to set the component indices", 49 | 5: "failed to set the decoded area", 50 | 6: "failed to decode image", 51 | 7: "support for more than 32-bits per component is not implemented", 52 | 8: "failed to upscale subsampled components", 53 | } 54 | ENCODING_ERRORS = { 55 | # Validation errors 56 | 1: ( 57 | "the input array has an unsupported number of samples per pixel, " 58 | "must be 1, 3 or 4" 59 | ), 60 | 2: ( 61 | "the input array has an invalid shape, must be (rows, columns) or " 62 | "(rows, columns, planes)" 63 | ), 64 | 3: ("the input array has an unsupported number of rows, must be in [1, 65535]"), 65 | 4: ("the input array has an unsupported number of columns, must be in [1, 65535]"), 66 | 5: ( 67 | "the input array has an unsupported dtype, only bool, u1, u2, u4, i1, i2" 68 | " and i4 are supported" 69 | ), 70 | 6: "the input array must use little endian byte ordering", 71 | 7: "the input array must be C-style, contiguous and aligned", 72 | 8: ( 73 | "the image precision given by bits stored must be in [1, itemsize of " 74 | "the input array's dtype]" 75 | ), 76 | 9: ( 77 | "the value of the 'photometric_interpretation' parameter is not valid " 78 | "for the number of samples per pixel" 79 | ), 80 | 10: "the valid of the 'codec_format' paramter is invalid", 81 | 11: "more than 100 'compression_ratios' is not supported", 82 | 12: "invalid item in the 'compression_ratios' value", 83 | 13: "invalid compression ratio, lowest value must be at least 1", 84 | 14: "more than 100 'signal_noise_ratios' is not supported", 85 | 15: "invalid item in the 'signal_noise_ratios' value", 86 | 16: "invalid signal-to-noise ratio, lowest value must be at least 0", 87 | # Encoding errors 88 | 20: "failed to assign the image component parameters", 89 | 21: "failed to create an empty image object", 90 | 22: "failed to set the encoding handler", 91 | 23: "failed to set up the encoder", 92 | 24: "failed to create the output stream", 93 | 25: "failure result from 'opj_start_compress()'", 94 | 26: "failure result from 'opj_encode()'", 95 | 27: "failure result from 'opj_endt_compress()'", 96 | 50: "the value of the 'bits_stored' parameter is invalid", 97 | 51: "the value of the 'samples_per_pixel' parameter is invalid", 98 | 52: "the value of the 'rows' is invalid, must be in [1, 65535]", 99 | 53: "the value of the 'columns' is invalid, must be in [1, 65535]", 100 | 54: "the value of the 'is_signed' is invalid, must be 0 or 1", 101 | 55: "the length of 'src' doesn't match the expected length", 102 | } 103 | 104 | 105 | def _get_format(stream: BinaryIO) -> int: 106 | """Return the JPEG 2000 format for the encoded data in `stream`. 107 | 108 | Parameters 109 | ---------- 110 | stream : file-like 111 | A Python object containing the encoded JPEG 2000 data. If not 112 | :class:`bytes` then the object must have ``tell()``, ``seek()`` and 113 | ``read()`` methods. 114 | 115 | Returns 116 | ------- 117 | int 118 | The format of the encoded data, one of: 119 | 120 | * ``0``: JPEG-2000 codestream 121 | * ``1``: JP2 file format 122 | 123 | Raises 124 | ------ 125 | ValueError 126 | If no matching JPEG 2000 file format found for the data. 127 | """ 128 | data = stream.read(20) 129 | stream.seek(0) 130 | 131 | try: 132 | return MAGIC_NUMBERS[data[:4]] 133 | except KeyError: 134 | pass 135 | 136 | try: 137 | return MAGIC_NUMBERS[data[:12]] 138 | except KeyError: 139 | pass 140 | 141 | raise ValueError("No matching JPEG 2000 format found") 142 | 143 | 144 | def get_openjpeg_version() -> Tuple[int, ...]: 145 | """Return the openjpeg version as tuple of int.""" 146 | version = _openjpeg.get_version().decode("ascii").split(".") 147 | return tuple([int(ii) for ii in version]) 148 | 149 | 150 | def decode( 151 | stream: Union[str, os.PathLike, bytes, bytearray, BinaryIO], 152 | j2k_format: Union[int, None] = None, 153 | reshape: bool = True, 154 | ) -> np.ndarray: 155 | """Return the decoded JPEG2000 data from `stream` as a :class:`numpy.ndarray`. 156 | 157 | .. versionchanged:: 1.1 158 | 159 | `stream` can now also be :class:`str` or :class:`pathlib.Path` 160 | 161 | Parameters 162 | ---------- 163 | stream : str, pathlib.Path, bytes, bytearray or file-like 164 | The path to the JPEG 2000 file or a Python object containing the 165 | encoded JPEG 2000 data. If using a file-like then the object must have 166 | ``tell()``, ``seek()`` and ``read()`` methods. 167 | j2k_format : int, optional 168 | The JPEG 2000 format to use for decoding, one of: 169 | 170 | * ``0``: JPEG-2000 codestream (such as from DICOM *Pixel Data*) 171 | * ``1``: JPT-stream (JPEG 2000, JPIP) 172 | * ``2``: JP2 file format 173 | reshape : bool, optional 174 | Reshape and re-view the output array so it matches the image data 175 | (default), otherwise return a 1D array of ``np.uint8``. 176 | 177 | Returns 178 | ------- 179 | numpy.ndarray 180 | An array of containing the decoded image data. 181 | 182 | Raises 183 | ------ 184 | RuntimeError 185 | If the decoding failed. 186 | """ 187 | if isinstance(stream, (str, Path)): 188 | with open(stream, "rb") as f: 189 | buffer: BinaryIO = BytesIO(f.read()) 190 | buffer.seek(0) 191 | elif isinstance(stream, (bytes, bytearray)): 192 | buffer = BytesIO(stream) 193 | else: 194 | # BinaryIO 195 | required_methods = ["read", "tell", "seek"] 196 | if not all([hasattr(stream, meth) for meth in required_methods]): 197 | raise TypeError( 198 | "The Python object containing the encoded JPEG 2000 data must " 199 | "either be bytes or have read(), tell() and seek() methods." 200 | ) 201 | buffer = cast(BinaryIO, stream) 202 | 203 | if j2k_format is None: 204 | j2k_format = _get_format(buffer) 205 | 206 | if j2k_format not in [0, 1, 2]: 207 | raise ValueError(f"Unsupported 'j2k_format' value: {j2k_format}") 208 | 209 | return_code, arr = _openjpeg.decode(buffer, j2k_format, as_array=True) 210 | if return_code != 0: 211 | raise RuntimeError( 212 | f"Error decoding the J2K data: {DECODING_ERRORS.get(return_code, return_code)}" 213 | ) 214 | 215 | if not reshape: 216 | return cast(np.ndarray, arr) 217 | 218 | meta = get_parameters(buffer, j2k_format) 219 | precision = cast(int, meta["precision"]) 220 | rows = cast(int, meta["rows"]) 221 | columns = cast(int, meta["columns"]) 222 | pixels_per_sample = cast(int, meta["samples_per_pixel"]) 223 | pixel_representation = cast(bool, meta["is_signed"]) 224 | bpp = ceil(precision / 8) 225 | 226 | bpp = 4 if bpp == 3 else bpp 227 | dtype = f" 1: 232 | shape.append(pixels_per_sample) 233 | 234 | return cast(np.ndarray, arr.reshape(*shape)) 235 | 236 | 237 | def decode_pixel_data( 238 | src: Union[bytes, bytearray], 239 | ds: Union["Dataset", Dict[str, Any], None] = None, 240 | version: int = Version.v1, 241 | **kwargs: Any, 242 | ) -> Union[np.ndarray, bytearray]: 243 | """Return the decoded JPEG 2000 data as a :class:`numpy.ndarray`. 244 | 245 | Intended for use with *pydicom* ``Dataset`` objects. 246 | 247 | Parameters 248 | ---------- 249 | src : bytes | bytearray 250 | The encoded JPEG 2000 data as :class:`bytes`, :class:`bytearray`. 251 | ds : pydicom.dataset.Dataset, optional 252 | A :class:`~pydicom.dataset.Dataset` containing the group ``0x0028`` 253 | elements corresponding to the *Pixel data*. If used then the 254 | *Samples per Pixel*, *Bits Stored* and *Pixel Representation* values 255 | will be checked against the JPEG 2000 data and warnings issued if 256 | different. Not used if `version` is ``2``. 257 | version : int, optional 258 | 259 | * If ``1`` (default) then return the image data as an :class:`numpy.ndarray` 260 | * If ``2`` then return the image data as :class:`bytearray` 261 | 262 | Returns 263 | ------- 264 | bytearray | numpy.ndarray 265 | The image data as either a bytearray or ndarray. 266 | 267 | Raises 268 | ------ 269 | RuntimeError 270 | If the decoding failed. 271 | """ 272 | buffer = BytesIO(src) 273 | j2k_format = _get_format(buffer) 274 | 275 | # Version 1 276 | if version == Version.v1: 277 | if j2k_format != 0: 278 | warnings.warn( 279 | "The (7FE0,0010) Pixel Data contains a JPEG 2000 codestream " 280 | "with the optional JP2 file format header, which is " 281 | "non-conformant to the DICOM Standard (Part 5, Annex A.4.4)" 282 | ) 283 | 284 | return_code, arr = _openjpeg.decode(buffer, j2k_format, as_array=True) 285 | if return_code != 0: 286 | raise RuntimeError( 287 | f"Error decoding the J2K data: {DECODING_ERRORS.get(return_code, return_code)}" 288 | ) 289 | 290 | samples_per_pixel = kwargs.get("samples_per_pixel") 291 | bits_stored = kwargs.get("bits_stored") 292 | pixel_representation = kwargs.get("pixel_representation") 293 | no_kwargs = None in (samples_per_pixel, bits_stored, pixel_representation) 294 | 295 | if not ds and no_kwargs: 296 | return cast(np.ndarray, arr) 297 | 298 | ds = cast("Dataset", ds) 299 | samples_per_pixel = ds.get("SamplesPerPixel", samples_per_pixel) 300 | bits_stored = ds.get("BitsStored", bits_stored) 301 | pixel_representation = ds.get("PixelRepresentation", pixel_representation) 302 | 303 | meta = get_parameters(buffer, j2k_format) 304 | if samples_per_pixel != meta["samples_per_pixel"]: 305 | warnings.warn( 306 | f"The (0028,0002) Samples per Pixel value '{samples_per_pixel}' " 307 | f"in the dataset does not match the number of components " 308 | f"'{meta['samples_per_pixel']}' found in the JPEG 2000 data. " 309 | f"It's recommended that you change the Samples per Pixel value " 310 | f"to produce the correct output" 311 | ) 312 | 313 | if bits_stored != meta["precision"]: 314 | warnings.warn( 315 | f"The (0028,0101) Bits Stored value '{bits_stored}' in the " 316 | f"dataset does not match the component precision value " 317 | f"'{meta['precision']}' found in the JPEG 2000 data. " 318 | f"It's recommended that you change the Bits Stored value to " 319 | f"produce the correct output" 320 | ) 321 | 322 | if bool(pixel_representation) != meta["is_signed"]: 323 | val = "signed" if meta["is_signed"] else "unsigned" 324 | ds_val = "signed" if bool(pixel_representation) else "unsigned" 325 | ds_val = f"'{pixel_representation}' ({ds_val})" 326 | warnings.warn( 327 | f"The (0028,0103) Pixel Representation value {ds_val} in the " 328 | f"dataset does not match the format of the values found in the " 329 | f"JPEG 2000 data '{val}'" 330 | ) 331 | 332 | return cast(np.ndarray, arr) 333 | 334 | # Version 2 335 | return_code, buffer = _openjpeg.decode(buffer, j2k_format, as_array=False) 336 | if return_code != 0: 337 | raise RuntimeError( 338 | f"Error decoding the J2K data: {DECODING_ERRORS.get(return_code, return_code)}" 339 | ) 340 | 341 | return cast(bytearray, buffer) 342 | 343 | 344 | def get_parameters( 345 | stream: Union[str, os.PathLike, bytes, bytearray, BinaryIO], 346 | j2k_format: Union[int, None] = None, 347 | ) -> Dict[str, Union[int, str, bool]]: 348 | """Return a :class:`dict` containing the JPEG2000 image parameters. 349 | 350 | .. versionchanged:: 1.1 351 | 352 | `stream` can now also be :class:`str` or :class:`pathlib.Path` 353 | 354 | Parameters 355 | ---------- 356 | stream : str, pathlib.Path, bytes or file-like 357 | The path to the JPEG 2000 file or a Python object containing the 358 | encoded JPEG 2000 data. If using a file-like then the object must have 359 | ``tell()``, ``seek()`` and ``read()`` methods. 360 | j2k_format : int, optional 361 | The JPEG 2000 format to use for decoding, one of: 362 | 363 | * ``0``: JPEG-2000 codestream (such as from DICOM *Pixel Data*) 364 | * ``1``: JPT-stream (JPEG 2000, JPIP) 365 | * ``2``: JP2 file format 366 | 367 | Returns 368 | ------- 369 | dict 370 | A :class:`dict` containing the J2K image parameters: 371 | ``{'columns': int, 'rows': int, 'colourspace': str, 372 | 'samples_per_pixel: int, 'precision': int, `is_signed`: bool}``. Possible 373 | colour spaces are "unknown", "unspecified", "sRGB", "monochrome", 374 | "YUV", "e-YCC" and "CYMK". 375 | 376 | Raises 377 | ------ 378 | RuntimeError 379 | If reading the image parameters failed. 380 | """ 381 | if isinstance(stream, (str, Path)): 382 | with open(stream, "rb") as f: 383 | buffer: BinaryIO = BytesIO(f.read()) 384 | buffer.seek(0) 385 | elif isinstance(stream, (bytes, bytearray)): 386 | buffer = BytesIO(stream) 387 | else: 388 | # BinaryIO 389 | required_methods = ["read", "tell", "seek"] 390 | if not all([hasattr(stream, meth) for meth in required_methods]): 391 | raise TypeError( 392 | "The Python object containing the encoded JPEG 2000 data must " 393 | "either be bytes or have read(), tell() and seek() methods." 394 | ) 395 | buffer = cast(BinaryIO, stream) 396 | 397 | if j2k_format is None: 398 | j2k_format = _get_format(buffer) 399 | 400 | if j2k_format not in [0, 1, 2]: 401 | raise ValueError(f"Unsupported 'j2k_format' value: {j2k_format}") 402 | 403 | return cast( 404 | Dict[str, Union[str, int, bool]], 405 | _openjpeg.get_parameters(buffer, j2k_format), 406 | ) 407 | 408 | 409 | def _get_bits_stored(arr: np.ndarray) -> int: 410 | """Return a 'bits_stored' appropriate for `arr`.""" 411 | if arr.dtype.kind == "b": 412 | return 1 413 | 414 | maximum = arr.max() 415 | if arr.dtype.kind == "u": 416 | if maximum == 0: 417 | return 1 418 | 419 | return int(log(maximum, 2) + 1) 420 | 421 | minimum = arr.min() 422 | for bit_depth in range(1, arr.dtype.itemsize * 8): 423 | if maximum <= 2 ** (bit_depth - 1) - 1 and minimum >= -(2 ** (bit_depth - 1)): 424 | return bit_depth 425 | 426 | return cast(int, arr.dtype.itemsize * 8) 427 | 428 | 429 | def encode_array( 430 | arr: np.ndarray, 431 | bits_stored: Union[int, None] = None, 432 | photometric_interpretation: int = 0, 433 | use_mct: bool = True, 434 | compression_ratios: Union[List[float], None] = None, 435 | signal_noise_ratios: Union[List[float], None] = None, 436 | codec_format: int = 0, 437 | **kwargs: Any, 438 | ) -> bytes: 439 | """Return the JPEG 2000 compressed `arr`. 440 | 441 | Encoding of the input array will use lossless compression by default, to 442 | use lossy compression either `compression_ratios` or `signal_noise_ratios` 443 | must be supplied. 444 | 445 | The following encoding parameters are always used: 446 | 447 | * No sub-sampling 448 | * LRCP progression order 449 | * 64 x 64 code blocks 450 | * 6 DWT resolutions 451 | * 2^15 x 2^15 precincts 452 | * 1 tile 453 | * No SOP or EPH markers 454 | * MCT will be used by default for 3 samples per pixel if 455 | `photometric_interpretation` is ``1`` (RGB) 456 | 457 | Lossless compression will use the following: 458 | 459 | * DWT 5-3 with reversible component transformation 460 | * 1 quality layer 461 | 462 | Lossy compression will use the following: 463 | 464 | * DWT 9-7 with irreversible component transformation 465 | * 1 or more quality layers 466 | 467 | Parameters 468 | ---------- 469 | arr : numpy.ndarray 470 | The array containing the image data to be encoded. For 1-bit DICOM 471 | *Pixel Data*, the data should be unpacked (if packed) and stored as a 472 | bool or u1 dtype. 473 | bits_stored : int, optional 474 | The bit-depth (precision) of the pixels in the image, defaults to the 475 | minimum bit-depth required to fully cover the range of pixel data in 476 | `arr`. 477 | photometric_interpretation : int, optional 478 | The colour space of the unencoded image data, used to help determine 479 | if MCT may be applied. If `codec_format` is ``1`` then this will also 480 | be the colour space set in the JP2 metadata. One of: 481 | 482 | * ``0``: Unspecified (default) 483 | * ``1``: sRGB 484 | * ``2``: Greyscale 485 | * ``3``: sYCC (YCbCr) 486 | * ``4``: eYCC 487 | * ``5``: CMYK 488 | use_mct : bool, optional 489 | Apply multi-component transformation (MCT) prior to encoding the image 490 | data. Defaults to ``True`` when the `photometric_interpretation` is 491 | ``1`` as it is intended for use with RGB data and should result in 492 | smaller file sizes. For all other values of `photometric_interpretation` 493 | the value of `use_mct` will be ignored and MCT not applied. 494 | 495 | If MCT is applied then the corresponding value of (0028,0004) 496 | *Photometric Interpretation* is: 497 | 498 | * ``"YBR_RCT"`` for lossless encoding 499 | * ``"YBR_ICT"`` for lossy encoding 500 | 501 | If MCT is not applied then *Photometric Intrepretation* should be the 502 | value corresponding to the unencoded dataset. 503 | compression_ratios : list[float], optional 504 | Required for lossy encoding, this is the compression ratio to use 505 | for each quality layer. Each item in the list is the factor of 506 | compression for a quality layer, so a value of ``[5]`` means 1 quality 507 | layer with 5x compression and a value of ``[5, 2]`` means 2 quality 508 | layers, one with 5x compression and one with 2x compression. If using 509 | multiple quality layers then the list should be in ordered with 510 | decreasing compression value and the lowest value must be at least 1. 511 | **Cannot be used with** `signal_noise_ratios`. 512 | signal_noise_ratios : list[float], optional 513 | Required for lossy encoding, this is a list of the desired peak 514 | signal-to-noise ratio (PSNR) to use for each layer. Each item in the 515 | list is the PSNR for a quality layer, so a value of ``[30]`` means 1 516 | quality layer with a PSNR of 30, and a value of ``[30, 50]`` means 2 517 | quality layers, one with a PSNR of 30 and one with a PSNR of 50. If 518 | using multiple quality layers then the list should be in ordered with 519 | increasing PSNR value and the lowest value must be greater than 0. 520 | **Cannot be used with** `compression_ratios`. 521 | codec_format : int, optional 522 | The codec to used when encoding: 523 | 524 | * ``0``: JPEG 2000 codestream only (default) (J2K/J2C format) 525 | * ``1``: A boxed JPEG 2000 codestream (JP2 format) 526 | 527 | Returns 528 | ------- 529 | bytes 530 | A JPEG 2000 or JP2 (with `codec_format=1`) codestream. 531 | """ 532 | if compression_ratios is None: 533 | compression_ratios = [] 534 | 535 | if signal_noise_ratios is None: 536 | signal_noise_ratios = [] 537 | 538 | if arr.dtype.kind not in ("b", "i", "u"): 539 | raise ValueError( 540 | f"The input array has an unsupported dtype '{arr.dtype}', only " 541 | "bool, u1, u2, u4, i1, i2 and i4 are supported" 542 | ) 543 | 544 | if bits_stored is None: 545 | bits_stored = _get_bits_stored(arr) 546 | 547 | # The destination for the encoded data, must support BinaryIO 548 | return_code, buffer = _openjpeg.encode_array( 549 | arr, 550 | bits_stored, 551 | photometric_interpretation, 552 | use_mct, 553 | compression_ratios, 554 | signal_noise_ratios, 555 | codec_format, 556 | ) 557 | 558 | if return_code != 0: 559 | raise RuntimeError( 560 | f"Error encoding the data: {ENCODING_ERRORS.get(return_code, return_code)}" 561 | ) 562 | 563 | return cast(bytes, buffer) 564 | 565 | 566 | encode = encode_array 567 | 568 | 569 | def encode_buffer( 570 | src: bytes, 571 | columns: int, 572 | rows: int, 573 | samples_per_pixel: int, 574 | bits_stored: int, 575 | is_signed: bool, 576 | *, 577 | photometric_interpretation: int = 0, 578 | use_mct: bool = True, 579 | compression_ratios: Union[List[float], None] = None, 580 | signal_noise_ratios: Union[List[float], None] = None, 581 | codec_format: int = 0, 582 | **kwargs: Any, 583 | ) -> bytes: 584 | """Return the JPEG 2000 compressed `src`. 585 | 586 | .. versionadded:: 2.2 587 | 588 | The following encoding parameters are always used: 589 | 590 | * No sub-sampling 591 | * LRCP progression order 592 | * 64 x 64 code blocks 593 | * 6 DWT resolutions 594 | * 2^15 x 2^15 precincts 595 | * 1 tile 596 | * No SOP or EPH markers 597 | * MCT will be used by default for 3 samples per pixel if 598 | `photometric_interpretation` is ``1`` (RGB) 599 | 600 | Lossless compression will use the following: 601 | 602 | * DWT 5-3 with reversible component transformation 603 | * 1 quality layer 604 | 605 | Lossy compression will use the following: 606 | 607 | * DWT 9-7 with irreversible component transformation 608 | * 1 or more quality layers 609 | 610 | Parameters 611 | ---------- 612 | src : bytes 613 | A single frame of little endian, colour-by-pixel ordered image data to 614 | be JPEG 2000 encoded. Each pixel should be encoded using the following 615 | (each pixel has 1 or more samples): 616 | 617 | * For 0 < bits per sample <= 8: 1 byte per sample 618 | * For 8 < bits per sample <= 16: 2 bytes per sample 619 | * For 16 < bits per sample <= 24: 4 bytes per sample 620 | columns : int 621 | The number of columns in the image, must be in the range [1, 2**16 - 1]. 622 | rows : int 623 | The number of rows in the image, must be in the range [1, 2**16 - 1]. 624 | samples_per_pixel : int 625 | The number of samples per pixel, must be 1, 3 or 4. 626 | bits_stored : int 627 | The number of bits per sample for each pixel, must be in the range 628 | (1, 24). 629 | is_signed : bool 630 | If ``True`` then the image uses signed integers, ``False`` otherwise. 631 | photometric_interpretation : int, optional 632 | The colour space of the unencoded image data, used to help determine 633 | if MCT may be applied. If `codec_format` is ``1`` then this will also 634 | be the colour space set in the JP2 metadata. One of: 635 | 636 | * ``0``: Unspecified (default) 637 | * ``1``: sRGB 638 | * ``2``: Greyscale 639 | * ``3``: sYCC (YCbCr) 640 | * ``4``: eYCC 641 | * ``5``: CMYK 642 | use_mct : bool, optional 643 | Apply multi-component transformation (MCT) prior to encoding the image 644 | data. Defaults to ``True`` when the `photometric_interpretation` is 645 | ``1`` as it is intended for use with RGB data and should result in 646 | smaller file sizes. For all other values of `photometric_interpretation` 647 | the value of `use_mct` will be ignored and MCT not applied. 648 | 649 | If MCT is applied then the corresponding value of (0028,0004) 650 | *Photometric Interpretation* is: 651 | 652 | * ``"YBR_RCT"`` for lossless encoding 653 | * ``"YBR_ICT"`` for lossy encoding 654 | 655 | If MCT is not applied then *Photometric Intrepretation* should be the 656 | value corresponding to the unencoded dataset. 657 | compression_ratios : list[float], optional 658 | Required for lossy encoding, this is the compression ratio to use 659 | for each quality layer. Each item in the list is the factor of 660 | compression for a quality layer, so a value of ``[5]`` means 1 quality 661 | layer with 5x compression and a value of ``[5, 2]`` means 2 quality 662 | layers, one with 5x compression and one with 2x compression. If using 663 | multiple quality layers then the list should be in ordered with 664 | decreasing compression value and the lowest value must be at least 1. 665 | **Cannot be used with** `signal_noise_ratios`. 666 | signal_noise_ratios : list[float], optional 667 | Required for lossy encoding, this is a list of the desired peak 668 | signal-to-noise ratio (PSNR) to use for each layer. Each item in the 669 | list is the PSNR for a quality layer, so a value of ``[30]`` means 1 670 | quality layer with a PSNR of 30, and a value of ``[30, 50]`` means 2 671 | quality layers, one with a PSNR of 30 and one with a PSNR of 50. If 672 | using multiple quality layers then the list should be in ordered with 673 | increasing PSNR value and the lowest value must be greater than 0. 674 | **Cannot be used with** `compression_ratios`. 675 | codec_format : int, optional 676 | The codec to used when encoding: 677 | 678 | * ``0``: JPEG 2000 codestream only (default) (J2K/J2C format) 679 | * ``1``: A boxed JPEG 2000 codestream (JP2 format) 680 | 681 | Returns 682 | ------- 683 | bytes 684 | A JPEG 2000 or JP2 (with `codec_format=1`) codestream. 685 | """ 686 | 687 | if compression_ratios is None: 688 | compression_ratios = [] 689 | 690 | if signal_noise_ratios is None: 691 | signal_noise_ratios = [] 692 | 693 | return_code, buffer = _openjpeg.encode_buffer( 694 | src, 695 | columns, 696 | rows, 697 | samples_per_pixel, 698 | bits_stored, 699 | 1 if is_signed else 0, 700 | photometric_interpretation, 701 | 1 if use_mct else 0, 702 | compression_ratios, 703 | signal_noise_ratios, 704 | codec_format, 705 | ) 706 | 707 | if return_code != 0: 708 | raise RuntimeError( 709 | f"Error encoding the data: {ENCODING_ERRORS.get(return_code, return_code)}" 710 | ) 711 | 712 | return cast(bytes, buffer) 713 | 714 | 715 | def encode_pixel_data(src: bytes, **kwargs: Any) -> bytes: 716 | """Return the JPEG 2000 compressed `src`. 717 | 718 | .. versionadded:: 2.2 719 | 720 | Parameters 721 | ---------- 722 | src : bytes 723 | A single frame of little endian, colour-by-pixel ordered image data to 724 | be JPEG2000 encoded. Each pixel should be encoded using the following: 725 | 726 | * For 0 < bits per sample <= 8: 1 byte per sample 727 | * For 8 < bits per sample <= 16: 2 bytes per sample 728 | * For 16 < bits per sample <= 24: 4 bytes per sample 729 | **kwargs 730 | The following keyword arguments are required: 731 | 732 | * ``'rows'``: int - the number of rows in the image (1, 65535) 733 | * ``'columns'``: int - the number of columns in the image (1, 65535) 734 | * ``'samples_per_pixel': int - the number of samples per pixel, 1 or 3. 735 | * ``'bits_stored'``: int - the number of bits per sample for pixels in 736 | the image (1, 24) 737 | * ``'photometric_interpretation'``: str - the colour space of the 738 | image in `src`. 739 | 740 | The following keyword arguments are optional: 741 | 742 | * ``'use_mct'``: bool: ``True`` to use MCT with RGB images (default) 743 | ``False`` otherwise. Will be ignored if `photometric_interpretation` 744 | is not YBR_RCT or YBR_ICT. 745 | * ''`compression_ratios'``: list[float] - required for lossy encoding if 746 | `signal_noise_ratios` is not used. The desired compression ratio to 747 | use for each quality layer. 748 | * ``'signal_noise_ratios'``: list[float] - required for lossy encoding 749 | if `compression_ratios` is not used. The desired peak 750 | signal-to-noise ratio (PSNR) to use for each quality layer. 751 | 752 | Returns 753 | ------- 754 | bytes | bytearray 755 | A JPEG 2000 codestream. 756 | """ 757 | # A J2K codestream doesn't track the colour space, so the photometric 758 | # interpretation is only used to help with setting MCT 759 | pi = kwargs["photometric_interpretation"] 760 | if pi in ("YBR_ICT", "YBR_RCT"): 761 | kwargs["photometric_interpretation"] = 1 762 | else: 763 | kwargs["photometric_interpretation"] = 0 764 | 765 | kwargs["is_signed"] = kwargs["pixel_representation"] 766 | kwargs["codec_format"] = 0 767 | 768 | return encode_buffer(src, **kwargs) 769 | -------------------------------------------------------------------------------- /lib/interface/encode.c: -------------------------------------------------------------------------------- 1 | 2 | 3 | #include "Python.h" 4 | #include "numpy/ndarrayobject.h" 5 | #include 6 | #include 7 | #include <../openjpeg/src/lib/openjp2/openjpeg.h> 8 | #include "utils.h" 9 | 10 | 11 | static void py_debug(const char *msg) { 12 | py_log("openjpeg.encode", "DEBUG", msg); 13 | } 14 | 15 | static void py_error(const char *msg) { 16 | py_log("openjpeg.encode", "ERROR", msg); 17 | } 18 | 19 | static void info_callback(const char *msg, void *callback) { 20 | py_log("openjpeg.encode", "INFO", msg); 21 | } 22 | 23 | static void warning_callback(const char *msg, void *callback) { 24 | py_log("openjpeg.encode", "WARNING", msg); 25 | } 26 | 27 | static void error_callback(const char *msg, void *callback) { 28 | py_error(msg); 29 | } 30 | 31 | 32 | extern int EncodeArray( 33 | PyArrayObject *arr, 34 | PyObject *dst, 35 | int bits_stored, 36 | int photometric_interpretation, 37 | int use_mct, 38 | PyObject *compression_ratios, 39 | PyObject *signal_noise_ratios, 40 | int codec_format 41 | ) 42 | { 43 | /* Encode a numpy ndarray using JPEG 2000. 44 | 45 | Parameters 46 | ---------- 47 | arr : PyArrayObject * 48 | The numpy ndarray containing the image data to be encoded. 49 | dst : PyObject * 50 | The destination for the encoded codestream, should be a BinaryIO. 51 | bits_stored : int 52 | Supported values: 1-24 53 | photometric_interpretation : int 54 | Supported values: 0-5 55 | use_mct : int 56 | Supported values 0-1, can't be used with subsampling 57 | compression_ratios : list[float] 58 | Encode lossy with the specified compression ratio for each quality 59 | layer. The ratio should be decreasing with increasing layer. 60 | signal_noise_ratios : list[float] 61 | Encode lossy with the specified peak signal-to-noise ratio for each 62 | quality layer. The ratio should be increasing with increasing layer. 63 | codec_format : int 64 | The format of the encoded JPEG 2000 data, one of: 65 | * ``0`` - OPJ_CODEC_J2K : JPEG-2000 codestream 66 | * ``1`` - OPJ_CODEC_JP2 : JP2 file format 67 | 68 | Returns 69 | ------- 70 | int 71 | The exit status, 0 for success, failure otherwise. 72 | */ 73 | 74 | // Check input 75 | // Determine the number of dimensions in the array, should be 2 or 3 76 | int nd = PyArray_NDIM(arr); 77 | 78 | // Can return NULL for 0-dimension arrays 79 | npy_intp *shape = PyArray_DIMS(arr); 80 | OPJ_UINT32 rows = 0; 81 | OPJ_UINT32 columns = 0; 82 | unsigned int samples_per_pixel = 0; 83 | switch (nd) { 84 | case 2: { 85 | // (rows, columns) 86 | samples_per_pixel = 1; 87 | rows = (OPJ_UINT32) shape[0]; 88 | columns = (OPJ_UINT32) shape[1]; 89 | break; 90 | } 91 | case 3: { 92 | // (rows, columns, planes) 93 | // Only allow 3 or 4 samples per pixel 94 | if ( shape[2] != 3 && shape[2] != 4 ) { 95 | py_error( 96 | "The input array has an unsupported number of samples per pixel" 97 | ); 98 | return 1; 99 | } 100 | rows = (OPJ_UINT32) shape[0]; 101 | columns = (OPJ_UINT32) shape[1]; 102 | samples_per_pixel = (unsigned int) shape[2]; 103 | break; 104 | } 105 | default: { 106 | py_error("An input array with the given dimensions is not supported"); 107 | return 2; 108 | } 109 | } 110 | 111 | // Check number of rows and columns is in (1, 2^16 - 1) 112 | if (rows < 1 || rows > 0xFFFF) { 113 | py_error("The input array has an unsupported number of rows"); 114 | return 3; 115 | } 116 | if (columns < 1 || columns > 0xFFFF) { 117 | py_error("The input array has an unsupported number of columns"); 118 | return 4; 119 | } 120 | 121 | // Check the dtype is supported 122 | PyArray_Descr *dtype = PyArray_DTYPE(arr); 123 | int type_enum = dtype->type_num; 124 | switch (type_enum) { 125 | case NPY_BOOL: // bool 126 | case NPY_INT8: // i1 127 | case NPY_UINT8: // u1 128 | case NPY_INT16: // i2 129 | case NPY_UINT16: // u2 130 | case NPY_INT32: // i4 131 | case NPY_UINT32: // u4 132 | break; 133 | default: 134 | py_error("The input array has an unsupported dtype"); 135 | return 5; 136 | } 137 | 138 | // Check array is C-style, contiguous and aligned 139 | if (PyArray_ISCARRAY_RO(arr) != 1) { 140 | py_error("The input array must be C-style, contiguous, aligned and in machine byte-order"); 141 | return 7; 142 | }; 143 | 144 | int bits_allocated; 145 | if (type_enum == NPY_BOOL || type_enum == NPY_INT8 || type_enum == NPY_UINT8) { 146 | bits_allocated = 8; // bool, i1, u1 147 | } else if (type_enum == NPY_INT16 || type_enum == NPY_UINT16 ) { 148 | bits_allocated = 16; // i2, u2 149 | } else { 150 | bits_allocated = 32; // i4, u4 151 | } 152 | 153 | // Check `photometric_interpretation` is valid 154 | if ( 155 | samples_per_pixel == 1 156 | && ( 157 | photometric_interpretation != 0 // OPJ_CLRSPC_UNSPECIFIED 158 | && photometric_interpretation != 2 // OPJ_CLRSPC_GRAY 159 | ) 160 | ) { 161 | py_error( 162 | "The value of the 'photometric_interpretation' parameter is not " 163 | "valid for the number of samples per pixel" 164 | ); 165 | return 9; 166 | } 167 | 168 | if ( 169 | samples_per_pixel == 3 170 | && ( 171 | photometric_interpretation != 0 // OPJ_CLRSPC_UNSPECIFIED 172 | && photometric_interpretation != 1 // OPJ_CLRSPC_SRGB 173 | && photometric_interpretation != 3 // OPJ_CLRSPC_SYCC 174 | && photometric_interpretation != 4 // OPJ_CLRSPC_EYCC 175 | ) 176 | ) { 177 | py_error( 178 | "The value of the 'photometric_interpretation' parameter is not " 179 | "valid for the number of samples per pixel" 180 | ); 181 | return 9; 182 | } 183 | 184 | if ( 185 | samples_per_pixel == 4 186 | && ( 187 | photometric_interpretation != 0 // OPJ_CLRSPC_UNSPECIFIED 188 | && photometric_interpretation != 5 // OPJ_CLRSPC_CMYK 189 | ) 190 | ) { 191 | py_error( 192 | "The value of the 'photometric_interpretation' parameter is not " 193 | "valid for the number of samples per pixel" 194 | ); 195 | return 9; 196 | } 197 | 198 | // Disable MCT if the input is not RGB 199 | if (samples_per_pixel != 3 || photometric_interpretation != 1) { 200 | use_mct = 0; 201 | } 202 | 203 | // Check the encoding format 204 | if (codec_format != 0 && codec_format != 1) { 205 | py_error("The value of the 'codec_format' parameter is invalid"); 206 | return 10; 207 | } 208 | 209 | unsigned int is_signed; 210 | if (type_enum == NPY_INT8 || type_enum == NPY_INT16 || type_enum == NPY_INT32) { 211 | is_signed = 1; 212 | } else { 213 | is_signed = 0; 214 | } 215 | 216 | // Encoding parameters 217 | unsigned int return_code; 218 | opj_cparameters_t parameters; 219 | opj_stream_t *stream = 00; 220 | opj_codec_t *codec = 00; 221 | opj_image_t *image = NULL; 222 | 223 | // subsampling_dx 1 224 | // subsampling_dy 1 225 | // tcp_numlayers = 0 226 | // tcp_rates[0] = 0 227 | // prog_order = OPJ_LRCP 228 | // cblockw_init = 64 229 | // cblockh_init = 64 230 | // numresolution = 6 231 | opj_set_default_encoder_parameters(¶meters); 232 | 233 | // Set MCT and codec 234 | parameters.tcp_mct = use_mct; 235 | parameters.cod_format = codec_format; 236 | 237 | // Set up for lossy (if applicable) 238 | Py_ssize_t nr_cr_layers = PyObject_Length(compression_ratios); 239 | Py_ssize_t nr_snr_layers = PyObject_Length(signal_noise_ratios); 240 | if (nr_cr_layers > 0 || nr_snr_layers > 0) { 241 | // Lossy compression using compression ratios 242 | // For 1 quality layer we use reversible if CR is 1 or PSNR is 0 243 | parameters.irreversible = 1; // use DWT 9-7 244 | if (nr_cr_layers > 0) { 245 | if (nr_cr_layers > 100) { 246 | return_code = 11; 247 | goto failure; 248 | } 249 | 250 | parameters.cp_disto_alloc = 1; // Allocation by rate/distortion 251 | parameters.tcp_numlayers = nr_cr_layers; 252 | for (int idx = 0; idx < nr_cr_layers; idx++) { 253 | PyObject *item = PyList_GetItem(compression_ratios, idx); 254 | if (item == NULL || !PyFloat_Check(item)) { 255 | return_code = 12; 256 | goto failure; 257 | } 258 | double value = PyFloat_AsDouble(item); 259 | if (value < 1) { 260 | return_code = 13; 261 | goto failure; 262 | } 263 | // Maximum 100 rates 264 | parameters.tcp_rates[idx] = value; 265 | 266 | if (nr_cr_layers == 1 && value == 1) { 267 | parameters.irreversible = 0; // use DWT 5-3 268 | } 269 | } 270 | py_debug("Encoding using lossy compression based on compression ratios"); 271 | 272 | } else { 273 | // Lossy compression using peak signal-to-noise ratios 274 | if (nr_snr_layers > 100) { 275 | return_code = 14; 276 | goto failure; 277 | } 278 | 279 | parameters.cp_fixed_quality = 1; 280 | parameters.tcp_numlayers = nr_snr_layers; 281 | for (int idx = 0; idx < nr_snr_layers; idx++) { 282 | PyObject *item = PyList_GetItem(signal_noise_ratios, idx); 283 | if (item == NULL || !PyFloat_Check(item)) { 284 | return_code = 15; 285 | goto failure; 286 | } 287 | double value = PyFloat_AsDouble(item); 288 | if (value < 0) { 289 | return_code = 16; 290 | goto failure; 291 | } 292 | // Maximum 100 ratios 293 | parameters.tcp_distoratio[idx] = value; 294 | 295 | if (nr_snr_layers == 1 && value == 0) { 296 | parameters.irreversible = 0; // use DWT 5-3 297 | } 298 | } 299 | py_debug( 300 | "Encoding using lossy compression based on peak signal-to-noise ratios" 301 | ); 302 | } 303 | } 304 | 305 | py_debug("Input validation complete, setting up for encoding"); 306 | 307 | // Create the input image and configure it 308 | // Setup the parameters for each image component 309 | opj_image_cmptparm_t *cmptparm; 310 | cmptparm = (opj_image_cmptparm_t*) calloc( 311 | (OPJ_UINT32) samples_per_pixel, 312 | sizeof(opj_image_cmptparm_t) 313 | ); 314 | if (!cmptparm) { 315 | py_error("Failed to assign the image component parameters"); 316 | return_code = 20; 317 | goto failure; 318 | } 319 | unsigned int i; 320 | for (i = 0; i < samples_per_pixel; i++) { 321 | cmptparm[i].prec = (OPJ_UINT32) bits_stored; 322 | cmptparm[i].sgnd = (OPJ_UINT32) is_signed; 323 | // Sub-sampling: none 324 | cmptparm[i].dx = 1; 325 | cmptparm[i].dy = 1; 326 | cmptparm[i].w = columns; 327 | cmptparm[i].h = rows; 328 | } 329 | 330 | // Create the input image object 331 | image = opj_image_create( 332 | (OPJ_UINT32) samples_per_pixel, 333 | &cmptparm[0], 334 | photometric_interpretation 335 | ); 336 | 337 | free(cmptparm); 338 | if (!image) { 339 | py_error("Failed to create an empty image object"); 340 | return_code = 21; 341 | goto failure; 342 | } 343 | 344 | /* set image offset and reference grid */ 345 | image->x0 = (OPJ_UINT32)parameters.image_offset_x0; 346 | image->y0 = (OPJ_UINT32)parameters.image_offset_y0; 347 | image->x1 = (OPJ_UINT32)parameters.image_offset_x0 + (OPJ_UINT32)columns; 348 | image->y1 = (OPJ_UINT32)parameters.image_offset_y0 + (OPJ_UINT32)rows; 349 | 350 | // Add the image data 351 | void *ptr; 352 | unsigned int p, r, c; 353 | if (bits_allocated == 8) { // bool, u1, i1 354 | for (p = 0; p < samples_per_pixel; p++) 355 | { 356 | for (r = 0; r < rows; r++) 357 | { 358 | for (c = 0; c < columns; c++) 359 | { 360 | ptr = PyArray_GETPTR3(arr, r, c, p); 361 | image->comps[p].data[c + columns * r] = is_signed ? *(npy_int8 *) ptr : *(npy_uint8 *) ptr; 362 | } 363 | } 364 | } 365 | } else if (bits_allocated == 16) { // u2, i2 366 | for (p = 0; p < samples_per_pixel; p++) 367 | { 368 | for (r = 0; r < rows; r++) 369 | { 370 | for (c = 0; c < columns; c++) 371 | { 372 | ptr = PyArray_GETPTR3(arr, r, c, p); 373 | image->comps[p].data[c + columns * r] = is_signed ? *(npy_int16 *) ptr : *(npy_uint16 *) ptr; 374 | } 375 | } 376 | } 377 | } else if (bits_allocated == 32) { // u4, i4 378 | for (p = 0; p < samples_per_pixel; p++) 379 | { 380 | for (r = 0; r < rows; r++) 381 | { 382 | for (c = 0; c < columns; c++) 383 | { 384 | ptr = PyArray_GETPTR3(arr, r, c, p); 385 | // `data[...]` is OPJ_INT32, so *may* support range i (1, 32), u (1, 31) 386 | if (is_signed) { 387 | image->comps[p].data[c + columns * r] = *(npy_int32 *) ptr; 388 | } else { 389 | image->comps[p].data[c + columns * r] = *(npy_uint32 *) ptr; 390 | 391 | } 392 | } 393 | } 394 | } 395 | } 396 | py_debug("Input image configured and populated with data"); 397 | 398 | /* Get an encoder handle */ 399 | switch (parameters.cod_format) { 400 | case 0: { // J2K codestream only 401 | codec = opj_create_compress(OPJ_CODEC_J2K); 402 | break; 403 | } 404 | case 1: { // JP2 codestream 405 | codec = opj_create_compress(OPJ_CODEC_JP2); 406 | break; 407 | } 408 | default: 409 | py_error("Failed to set the encoding handler"); 410 | return_code = 22; 411 | goto failure; 412 | } 413 | 414 | /* Send info, warning, error message to Python logging */ 415 | opj_set_info_handler(codec, info_callback, NULL); 416 | opj_set_warning_handler(codec, warning_callback, NULL); 417 | opj_set_error_handler(codec, error_callback, NULL); 418 | 419 | if (! opj_setup_encoder(codec, ¶meters, image)) { 420 | py_error("Failed to set up the encoder"); 421 | return_code = 23; 422 | goto failure; 423 | } 424 | 425 | // Creates an abstract output stream; allocates memory 426 | stream = opj_stream_create(BUFFER_SIZE, OPJ_FALSE); 427 | 428 | if (!stream) { 429 | py_error("Failed to create the output stream"); 430 | return_code = 24; 431 | goto failure; 432 | } 433 | 434 | // Functions for the stream 435 | opj_stream_set_write_function(stream, py_write); 436 | opj_stream_set_skip_function(stream, py_skip); 437 | opj_stream_set_seek_function(stream, py_seek_set); 438 | opj_stream_set_user_data(stream, dst, NULL); 439 | 440 | OPJ_BOOL result; 441 | 442 | // Encode `image` using `codec` and put the output in `stream` 443 | py_debug("Encoding started"); 444 | result = opj_start_compress(codec, image, stream); 445 | if (!result) { 446 | py_error("Failure result from 'opj_start_compress()'"); 447 | return_code = 25; 448 | goto failure; 449 | } 450 | 451 | result = result && opj_encode(codec, stream); 452 | if (!result) { 453 | py_error("Failure result from 'opj_encode()'"); 454 | return_code = 26; 455 | goto failure; 456 | } 457 | 458 | result = result && opj_end_compress(codec, stream); 459 | if (!result) { 460 | py_error("Failure result from 'opj_end_compress()'"); 461 | return_code = 27; 462 | goto failure; 463 | } 464 | 465 | py_debug("Encoding completed"); 466 | 467 | opj_stream_destroy(stream); 468 | opj_destroy_codec(codec); 469 | opj_image_destroy(image); 470 | 471 | return 0; 472 | 473 | failure: 474 | opj_stream_destroy(stream); 475 | opj_destroy_codec(codec); 476 | opj_image_destroy(image); 477 | return return_code; 478 | } 479 | 480 | 481 | extern int EncodeBuffer( 482 | PyObject *src, 483 | unsigned int columns, 484 | unsigned int rows, 485 | unsigned int samples_per_pixel, 486 | unsigned int bits_stored, 487 | unsigned int is_signed, 488 | unsigned int photometric_interpretation, 489 | PyObject *dst, 490 | unsigned int use_mct, 491 | PyObject *compression_ratios, 492 | PyObject *signal_noise_ratios, 493 | int codec_format 494 | ) 495 | { 496 | /* Encode image data using JPEG 2000. 497 | 498 | Parameters 499 | ---------- 500 | src : PyObject * 501 | The image data to be encoded, as a little endian and colour-by-pixel 502 | ordered bytes or bytearray. 503 | columns : int 504 | Supported values: 1-2^16 - 1 505 | rows : int 506 | Supported values: 1-2^16 - 1 507 | samples_per_pixel : int 508 | Supported values: 1, 3, 4 509 | bits_stored : int 510 | Supported values: 1-24 511 | is_signed : int 512 | 0 for unsigned, 1 for signed 513 | photometric_interpretation : int 514 | Supported values: 0-5 515 | dst : PyObject * 516 | The destination for the encoded codestream, should be a BinaryIO or 517 | an object with write(), tell() and seek(). 518 | use_mct : int 519 | Supported values 0-1, can't be used with subsampling 520 | compression_ratios : list[float] 521 | Encode lossy with the specified compression ratio for each quality 522 | layer. The ratio should be decreasing with increasing layer. 523 | signal_noise_ratios : list[float] 524 | Encode lossy with the specified peak signal-to-noise ratio for each 525 | quality layer. The ratio should be increasing with increasing layer. 526 | codec_format : int 527 | The format of the encoded JPEG 2000 data, one of: 528 | * ``0`` - OPJ_CODEC_J2K : JPEG-2000 codestream 529 | * ``1`` - OPJ_CODEC_JP2 : JP2 file format 530 | 531 | Returns 532 | ------- 533 | int 534 | The exit status, 0 for success, failure otherwise. 535 | */ 536 | 537 | // Check input 538 | // Check bits_stored is in (1, 24) 539 | unsigned int bytes_per_pixel; 540 | if (bits_stored > 0 && bits_stored <= 8) { 541 | bytes_per_pixel = 1; 542 | } else if (bits_stored > 8 && bits_stored <= 16) { 543 | bytes_per_pixel = 2; 544 | } else if (bits_stored > 16 && bits_stored <= 24) { 545 | bytes_per_pixel = 4; 546 | } else { 547 | py_error("The value of the 'bits_stored' parameter is invalid"); 548 | return 50; 549 | } 550 | 551 | // Check samples_per_pixel is 1, 3 or 4 552 | switch (samples_per_pixel) { 553 | case 1: break; 554 | case 3: break; 555 | case 4: break; 556 | default: { 557 | py_error("The number of samples per pixel is not supported"); 558 | return 51; 559 | } 560 | } 561 | 562 | // Check number of rows and columns is in (1, 2^16 - 1) 563 | // The J2K standard supports up to 32-bit rows and columns 564 | if (rows < 1 || rows > 0xFFFF) { 565 | py_error("The number of rows is invalid"); 566 | return 52; 567 | } 568 | if (columns < 1 || columns > 0xFFFF) { 569 | py_error("The number of columns is invalid"); 570 | return 53; 571 | } 572 | 573 | // Check is_signed is 0 or 1 574 | if (is_signed != 0 && is_signed != 1) { 575 | py_error("The value of the 'is_signed' parameter is invalid"); 576 | return 54; 577 | } 578 | 579 | // Check length of `src` matches expected dimensions 580 | Py_ssize_t actual_length = PyObject_Length(src); 581 | // (2**24 - 1) x (2**24 - 1) * 4 * 4 -> requires 52-bits 582 | // OPJ_INT64 covers from (-2**63, 2**63 - 1) which is sufficient 583 | OPJ_INT64 expected_length = rows * columns * samples_per_pixel * bytes_per_pixel; 584 | if (actual_length != expected_length) { 585 | py_error("The length of `src` does not match the expected length"); 586 | return 55; 587 | } 588 | 589 | // Check `photometric_interpretation` is valid 590 | if ( 591 | samples_per_pixel == 1 592 | && ( 593 | photometric_interpretation != 0 // OPJ_CLRSPC_UNSPECIFIED 594 | && photometric_interpretation != 2 // OPJ_CLRSPC_GRAY 595 | ) 596 | ) { 597 | py_error( 598 | "The value of the 'photometric_interpretation' parameter is not " 599 | "valid for the number of samples per pixel" 600 | ); 601 | return 9; 602 | } 603 | 604 | if ( 605 | samples_per_pixel == 3 606 | && ( 607 | photometric_interpretation != 0 // OPJ_CLRSPC_UNSPECIFIED 608 | && photometric_interpretation != 1 // OPJ_CLRSPC_SRGB 609 | && photometric_interpretation != 3 // OPJ_CLRSPC_SYCC 610 | && photometric_interpretation != 4 // OPJ_CLRSPC_EYCC 611 | ) 612 | ) { 613 | py_error( 614 | "The value of the 'photometric_interpretation' parameter is not " 615 | "valid for the number of samples per pixel" 616 | ); 617 | return 9; 618 | } 619 | 620 | if ( 621 | samples_per_pixel == 4 622 | && ( 623 | photometric_interpretation != 0 // OPJ_CLRSPC_UNSPECIFIED 624 | && photometric_interpretation != 5 // OPJ_CLRSPC_CMYK 625 | ) 626 | ) { 627 | py_error( 628 | "The value of the 'photometric_interpretation' parameter is not " 629 | "valid for the number of samples per pixel" 630 | ); 631 | return 9; 632 | } 633 | 634 | // Check the encoding format 635 | if (codec_format != 0 && codec_format != 1) { 636 | py_error("The value of the 'codec_format' parameter is invalid"); 637 | return 10; 638 | } 639 | 640 | // Disable MCT if the input is not RGB 641 | if (samples_per_pixel != 3 || photometric_interpretation != 1) { 642 | use_mct = 0; 643 | } 644 | 645 | // Encoding parameters 646 | unsigned int return_code; 647 | opj_cparameters_t parameters; 648 | opj_stream_t *stream = 00; 649 | opj_codec_t *codec = 00; 650 | opj_image_t *image = NULL; 651 | 652 | // subsampling_dx 1 653 | // subsampling_dy 1 654 | // tcp_numlayers = 0 655 | // tcp_rates[0] = 0 656 | // prog_order = OPJ_LRCP 657 | // cblockw_init = 64 658 | // cblockh_init = 64 659 | // numresolution = 6 660 | opj_set_default_encoder_parameters(¶meters); 661 | 662 | // Set MCT and codec 663 | parameters.tcp_mct = use_mct; 664 | parameters.cod_format = codec_format; 665 | 666 | // Set up for lossy (if applicable) 667 | Py_ssize_t nr_cr_layers = PyObject_Length(compression_ratios); 668 | Py_ssize_t nr_snr_layers = PyObject_Length(signal_noise_ratios); 669 | if (nr_cr_layers > 0 || nr_snr_layers > 0) { 670 | // Lossy compression using compression ratios 671 | parameters.irreversible = 1; // use DWT 9-7 672 | if (nr_cr_layers > 0) { 673 | if (nr_cr_layers > 100) { 674 | return_code = 11; 675 | goto failure; 676 | } 677 | 678 | parameters.cp_disto_alloc = 1; // Allocation by rate/distortion 679 | parameters.tcp_numlayers = nr_cr_layers; 680 | for (int idx = 0; idx < nr_cr_layers; idx++) { 681 | PyObject *item = PyList_GetItem(compression_ratios, idx); 682 | if (item == NULL || !PyFloat_Check(item)) { 683 | return_code = 12; 684 | goto failure; 685 | } 686 | double value = PyFloat_AsDouble(item); 687 | if (value < 1) { 688 | return_code = 13; 689 | goto failure; 690 | } 691 | // Maximum 100 rates 692 | parameters.tcp_rates[idx] = value; 693 | 694 | if (nr_cr_layers == 1 && value == 1) { 695 | parameters.irreversible = 0; // use DWT 5-3 696 | } 697 | } 698 | py_debug("Encoding using lossy compression based on compression ratios"); 699 | 700 | } else { 701 | // Lossy compression using peak signal-to-noise ratios 702 | if (nr_snr_layers > 100) { 703 | return_code = 14; 704 | goto failure; 705 | } 706 | 707 | parameters.cp_fixed_quality = 1; 708 | parameters.tcp_numlayers = nr_snr_layers; 709 | for (int idx = 0; idx < nr_snr_layers; idx++) { 710 | PyObject *item = PyList_GetItem(signal_noise_ratios, idx); 711 | if (item == NULL || !PyFloat_Check(item)) { 712 | return_code = 15; 713 | goto failure; 714 | } 715 | double value = PyFloat_AsDouble(item); 716 | if (value < 0) { 717 | return_code = 16; 718 | goto failure; 719 | } 720 | // Maximum 100 ratios 721 | parameters.tcp_distoratio[idx] = value; 722 | 723 | if (nr_snr_layers == 1 && value == 0) { 724 | parameters.irreversible = 0; // use DWT 5-3 725 | } 726 | } 727 | py_debug( 728 | "Encoding using lossy compression based on peak signal-to-noise ratios" 729 | ); 730 | } 731 | } 732 | 733 | py_debug("Input validation complete, setting up for encoding"); 734 | 735 | // Create the input image and configure it 736 | // Setup the parameters for each image component 737 | opj_image_cmptparm_t *cmptparm; 738 | cmptparm = (opj_image_cmptparm_t*) calloc( 739 | (OPJ_UINT32) samples_per_pixel, 740 | sizeof(opj_image_cmptparm_t) 741 | ); 742 | if (!cmptparm) { 743 | py_error("Failed to assign the image component parameters"); 744 | return_code = 20; 745 | goto failure; 746 | } 747 | unsigned int i; 748 | for (i = 0; i < samples_per_pixel; i++) { 749 | cmptparm[i].prec = (OPJ_UINT32) bits_stored; 750 | cmptparm[i].sgnd = (OPJ_UINT32) is_signed; 751 | // Sub-sampling: none 752 | cmptparm[i].dx = (OPJ_UINT32) 1; 753 | cmptparm[i].dy = (OPJ_UINT32) 1; 754 | cmptparm[i].w = (OPJ_UINT32) columns; 755 | cmptparm[i].h = (OPJ_UINT32) rows; 756 | } 757 | 758 | // Create the input image object 759 | image = opj_image_create( 760 | (OPJ_UINT32) samples_per_pixel, 761 | &cmptparm[0], 762 | photometric_interpretation 763 | ); 764 | 765 | free(cmptparm); 766 | if (!image) { 767 | py_error("Failed to create an empty image object"); 768 | return_code = 21; 769 | goto failure; 770 | } 771 | 772 | /* set image offset and reference grid */ 773 | image->x0 = (OPJ_UINT32)parameters.image_offset_x0; 774 | image->y0 = (OPJ_UINT32)parameters.image_offset_y0; 775 | image->x1 = (OPJ_UINT32)parameters.image_offset_x0 + (OPJ_UINT32) columns; 776 | image->y1 = (OPJ_UINT32)parameters.image_offset_y0 + (OPJ_UINT32) rows; 777 | 778 | // Add the image data 779 | // src is ordered as colour-by-pixel 780 | unsigned int p; 781 | OPJ_UINT64 nr_pixels = rows * columns; 782 | char *data = PyBytes_AsString(src); 783 | if (bytes_per_pixel == 1) { 784 | unsigned char value; 785 | unsigned char unsigned_mask = 0xFF >> (8 - bits_stored); 786 | unsigned char signed_mask = 0xFF << bits_stored; 787 | unsigned char bit_flag = 1 << (bits_stored - 1); 788 | unsigned short do_masking = bits_stored < 8; 789 | for (OPJ_UINT64 ii = 0; ii < nr_pixels; ii++) 790 | { 791 | for (p = 0; p < samples_per_pixel; p++) 792 | { 793 | // comps[...].data[...] is OPJ_INT32 -> int32_t 794 | value = (unsigned char) *data; 795 | data++; 796 | 797 | if (do_masking) { 798 | // Unsigned: zero out bits above `precision` 799 | // Signed: zero out bits above `precision` if value >= 0, otherwise 800 | // set them to one 801 | if (is_signed && (bit_flag & value)) { 802 | value = value | signed_mask; 803 | } else { 804 | value = value & unsigned_mask; 805 | } 806 | } 807 | 808 | image->comps[p].data[ii] = is_signed ? (signed char) value : value; 809 | } 810 | } 811 | } else if (bytes_per_pixel == 2) { 812 | unsigned short value; 813 | unsigned char temp1; 814 | unsigned char temp2; 815 | unsigned short unsigned_mask = 0xFFFF >> (16 - bits_stored); 816 | unsigned short signed_mask = 0xFFFF << bits_stored; 817 | unsigned short bit_flag = 1 << (bits_stored - 1); 818 | unsigned short do_masking = bits_stored < 16; 819 | for (OPJ_UINT64 ii = 0; ii < nr_pixels; ii++) 820 | { 821 | for (p = 0; p < samples_per_pixel; p++) 822 | { 823 | temp1 = (unsigned char) *data; 824 | data++; 825 | temp2 = (unsigned char) *data; 826 | data++; 827 | 828 | value = (unsigned short) ((temp2 << 8) + temp1); 829 | if (do_masking) { 830 | if (is_signed && (bit_flag & value)) { 831 | value = value | signed_mask; 832 | } else { 833 | value = value & unsigned_mask; 834 | } 835 | } 836 | 837 | image->comps[p].data[ii] = is_signed ? (signed short) value : value; 838 | } 839 | } 840 | } else if (bytes_per_pixel == 4) { 841 | unsigned long value; 842 | unsigned char temp1; 843 | unsigned char temp2; 844 | unsigned char temp3; 845 | unsigned char temp4; 846 | unsigned long unsigned_mask = 0xFFFFFFFF >> (32 - bits_stored); 847 | unsigned long signed_mask = 0xFFFFFFFF << bits_stored; 848 | unsigned long bit_flag = 1 << (bits_stored - 1); 849 | unsigned short do_masking = bits_stored < 32; 850 | for (OPJ_UINT64 ii = 0; ii < nr_pixels; ii++) 851 | { 852 | for (p = 0; p < samples_per_pixel; p++) 853 | { 854 | temp1 = (unsigned char) * data; 855 | data++; 856 | temp2 = (unsigned char) * data; 857 | data++; 858 | temp3 = (unsigned char) * data; 859 | data++; 860 | temp4 = (unsigned char) * data; 861 | data++; 862 | 863 | value = (unsigned long) ((temp4 << 24) + (temp3 << 16) + (temp2 << 8) + temp1); 864 | if (do_masking) { 865 | if (is_signed && (bit_flag & value)) { 866 | value = value | signed_mask; 867 | } else { 868 | value = value & unsigned_mask; 869 | } 870 | } 871 | 872 | image->comps[p].data[ii] = is_signed ? (long) value : value; 873 | } 874 | } 875 | } 876 | py_debug("Input image configured and populated with data"); 877 | 878 | /* Get an encoder handle */ 879 | switch (parameters.cod_format) { 880 | case 0: { // J2K codestream only 881 | codec = opj_create_compress(OPJ_CODEC_J2K); 882 | break; 883 | } 884 | case 1: { // JP2 codestream 885 | codec = opj_create_compress(OPJ_CODEC_JP2); 886 | break; 887 | } 888 | default: 889 | py_error("Failed to set the encoding handler"); 890 | return_code = 22; 891 | goto failure; 892 | } 893 | 894 | /* Send info, warning, error message to Python logging */ 895 | opj_set_info_handler(codec, info_callback, NULL); 896 | opj_set_warning_handler(codec, warning_callback, NULL); 897 | opj_set_error_handler(codec, error_callback, NULL); 898 | 899 | if (! opj_setup_encoder(codec, ¶meters, image)) { 900 | py_error("Failed to set up the encoder"); 901 | return_code = 23; 902 | goto failure; 903 | } 904 | 905 | // Creates an abstract output stream; allocates memory 906 | // cio::opj_stream_create(buffer size, is_input) 907 | stream = opj_stream_create(BUFFER_SIZE, OPJ_FALSE); 908 | 909 | if (!stream) { 910 | py_error("Failed to create the output stream"); 911 | return_code = 24; 912 | goto failure; 913 | } 914 | 915 | // Functions for the stream 916 | opj_stream_set_user_data(stream, dst, NULL); 917 | opj_stream_set_write_function(stream, py_write); 918 | opj_stream_set_skip_function(stream, py_skip); 919 | opj_stream_set_seek_function(stream, py_seek_set); 920 | 921 | OPJ_BOOL result; 922 | 923 | // Encode `image` using `codec` and put the output in `stream` 924 | py_debug("Encoding started"); 925 | result = opj_start_compress(codec, image, stream); 926 | if (!result) { 927 | py_error("Failure result from 'opj_start_compress()'"); 928 | return_code = 25; 929 | goto failure; 930 | } 931 | 932 | result = result && opj_encode(codec, stream); 933 | if (!result) { 934 | py_error("Failure result from 'opj_encode()'"); 935 | return_code = 26; 936 | goto failure; 937 | } 938 | 939 | result = result && opj_end_compress(codec, stream); 940 | if (!result) { 941 | py_error("Failure result from 'opj_end_compress()'"); 942 | return_code = 27; 943 | goto failure; 944 | } 945 | 946 | py_debug("Encoding completed"); 947 | 948 | opj_stream_destroy(stream); 949 | opj_destroy_codec(codec); 950 | opj_image_destroy(image); 951 | 952 | return 0; 953 | 954 | failure: 955 | opj_stream_destroy(stream); 956 | opj_destroy_codec(codec); 957 | opj_image_destroy(image); 958 | return return_code; 959 | } 960 | -------------------------------------------------------------------------------- /openjpeg/tests/test_handler.py: -------------------------------------------------------------------------------- 1 | """Tests for the pylibjpeg pixel data handler.""" 2 | 3 | import pytest 4 | 5 | try: 6 | from pydicom import __version__ 7 | from pydicom.encaps import generate_frames 8 | from pydicom.pixels.utils import ( 9 | reshape_pixel_array, 10 | pixel_dtype, 11 | ) 12 | 13 | HAS_PYDICOM = True 14 | except ImportError: 15 | HAS_PYDICOM = False 16 | 17 | from openjpeg import get_parameters, decode_pixel_data 18 | from openjpeg.data import get_indexed_datasets 19 | 20 | if HAS_PYDICOM: 21 | PYD_VERSION = int(__version__.split(".")[0]) 22 | 23 | 24 | def get_frame_generator(ds): 25 | """Return a frame generator for DICOM datasets.""" 26 | nr_frames = ds.get("NumberOfFrames", 1) 27 | return generate_frames(ds.PixelData, number_of_frames=nr_frames) 28 | 29 | 30 | @pytest.mark.skipif(not HAS_PYDICOM, reason="pydicom unavailable") 31 | class TestHandler: 32 | """Tests for the pixel data handler.""" 33 | 34 | def test_invalid_type_raises(self): 35 | """Test decoding using invalid type raises.""" 36 | index = get_indexed_datasets("1.2.840.10008.1.2.4.90") 37 | ds = index["MR_small_jp2klossless.dcm"]["ds"] 38 | frame = tuple(next(get_frame_generator(ds))) 39 | assert not hasattr(frame, "tell") and not isinstance(frame, bytes) 40 | 41 | msg = "a bytes-like object is required, not 'tuple'" 42 | with pytest.raises(TypeError, match=msg): 43 | decode_pixel_data(frame) 44 | 45 | def test_no_dataset(self): 46 | index = get_indexed_datasets("1.2.840.10008.1.2.4.90") 47 | ds = index["MR_small_jp2klossless.dcm"]["ds"] 48 | frame = next(get_frame_generator(ds)) 49 | arr = decode_pixel_data(frame) 50 | assert arr.flags.writeable 51 | assert "uint8" == arr.dtype 52 | length = ds.Rows * ds.Columns * ds.SamplesPerPixel * ds.BitsAllocated / 8 53 | assert (length,) == arr.shape 54 | 55 | 56 | class HandlerTestBase: 57 | """Baseclass for handler tests.""" 58 | 59 | uid = None 60 | 61 | def setup_method(self): 62 | self.ds = get_indexed_datasets(self.uid) 63 | 64 | def plot(self, arr, index=None, cmap=None): 65 | import matplotlib.pyplot as plt 66 | 67 | if index is not None: 68 | if cmap: 69 | plt.imshow(arr[index], cmap=cmap) 70 | else: 71 | plt.imshow(arr[index]) 72 | else: 73 | if cmap: 74 | plt.imshow(arr, cmap=cmap) 75 | else: 76 | plt.imshow(arr) 77 | 78 | plt.show() 79 | 80 | 81 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies") 82 | class TestLibrary: 83 | """Tests for libjpeg itself.""" 84 | 85 | def test_non_conformant_raises(self): 86 | """Test that a non-conformant JPEG image raises an exception.""" 87 | ds_list = get_indexed_datasets("1.2.840.10008.1.2.4.90") 88 | # Image has invalid Se value in the SOS marker segment 89 | item = ds_list["966.dcm"] 90 | assert 0xC000 == item["Status"][1] 91 | msg = r"Error decoding the J2K data: failed to decode image" 92 | with pytest.raises(RuntimeError, match=msg): 93 | item["ds"].pixel_array 94 | 95 | def test_valid_no_warning(self, recwarn): 96 | """Test no warning issued when dataset matches JPEG data.""" 97 | index = get_indexed_datasets("1.2.840.10008.1.2.4.90") 98 | ds = index["966_fixed.dcm"]["ds"] 99 | ds.pixel_array 100 | 101 | assert len(recwarn) == 0 102 | 103 | 104 | # ISO/IEC 10918 JPEG - Expected fail 105 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies") 106 | class TestJPEGBaseline(HandlerTestBase): 107 | """Test the handler with ISO 10918 JPEG images. 108 | 109 | 1.2.840.10008.1.2.4.50 : JPEG Baseline (Process 1) 110 | """ 111 | 112 | uid = "1.2.840.10008.1.2.4.50" 113 | 114 | def test_raises(self): 115 | """Test greyscale.""" 116 | ds = self.ds["JPEGBaseline_1s_1f_u_08_08.dcm"]["ds"] 117 | assert self.uid == ds.file_meta.TransferSyntaxUID 118 | assert 1 == ds.SamplesPerPixel 119 | assert 1 == getattr(ds, "NumberOfFrames", 1) 120 | assert "MONOCHROME" in ds.PhotometricInterpretation 121 | assert 8 == ds.BitsAllocated == ds.BitsStored 122 | assert 0 == ds.PixelRepresentation 123 | 124 | if PYD_VERSION < 3: 125 | msg = ( 126 | "Unable to convert the Pixel Data as the 'pylibjpeg-libjpeg' plugin is " 127 | "not installed" 128 | ) 129 | else: 130 | msg = ( 131 | r"Unable to decompress 'JPEG Baseline \(Process 1\)' pixel data because " 132 | "all plugins are missing dependencies:" 133 | ) 134 | 135 | with pytest.raises(RuntimeError, match=msg): 136 | ds.pixel_array 137 | 138 | 139 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies") 140 | class TestJPEGExtended(HandlerTestBase): 141 | """Test the handler with ISO 10918 JPEG images. 142 | 143 | 1.2.840.10008.1.2.4.51 : JPEG Extended (Process 2 and 4) 144 | """ 145 | 146 | uid = "1.2.840.10008.1.2.4.51" 147 | 148 | # Process 4 149 | def test_raises(self): 150 | """Test process 4 greyscale.""" 151 | ds = self.ds["RG2_JPLY_fixed.dcm"]["ds"] 152 | assert self.uid == ds.file_meta.TransferSyntaxUID 153 | assert 1 == ds.SamplesPerPixel 154 | assert 1 == getattr(ds, "NumberOfFrames", 1) 155 | assert "MONOCHROME" in ds.PhotometricInterpretation 156 | assert 16 == ds.BitsAllocated 157 | # Input precision is 12, not 10 158 | assert 10 == ds.BitsStored 159 | assert 0 == ds.PixelRepresentation 160 | 161 | if PYD_VERSION < 3: 162 | msg = ( 163 | "Unable to convert the Pixel Data as the 'pylibjpeg-libjpeg' plugin is " 164 | "not installed" 165 | ) 166 | else: 167 | msg = ( 168 | r"Unable to decompress 'JPEG Extended \(Process 2 and 4\)' pixel data because " 169 | "all plugins are missing dependencies:" 170 | ) 171 | 172 | with pytest.raises(RuntimeError, match=msg): 173 | ds.pixel_array 174 | 175 | 176 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies") 177 | class TestJPEGLossless(HandlerTestBase): 178 | """Test the handler with ISO 10918 JPEG images. 179 | 180 | 1.2.840.10008.1.2.4.57 : JPEG Lossless, Non-Hierarchical (Process 14) 181 | """ 182 | 183 | uid = "1.2.840.10008.1.2.4.57" 184 | 185 | def test_raises(self): 186 | """Test process 2 greyscale.""" 187 | ds = self.ds["JPEGLossless_1s_1f_u_16_12.dcm"]["ds"] 188 | assert self.uid == ds.file_meta.TransferSyntaxUID 189 | assert 1 == ds.SamplesPerPixel 190 | assert 1 == getattr(ds, "NumberOfFrames", 1) 191 | assert "MONOCHROME" in ds.PhotometricInterpretation 192 | assert 16 == ds.BitsAllocated 193 | assert 12 == ds.BitsStored 194 | assert 0 == ds.PixelRepresentation 195 | 196 | if PYD_VERSION < 3: 197 | msg = ( 198 | "Unable to convert the Pixel Data as the 'pylibjpeg-libjpeg' plugin is " 199 | "not installed" 200 | ) 201 | else: 202 | msg = ( 203 | r"Unable to decompress 'JPEG Lossless, Non-Hierarchical \(Process " 204 | r"14\)' pixel data because all plugins are missing dependencies:" 205 | ) 206 | 207 | with pytest.raises(RuntimeError, match=msg): 208 | ds.pixel_array 209 | 210 | 211 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies") 212 | class TestJPEGLosslessSV1(HandlerTestBase): 213 | """Test the handler with ISO 10918 JPEG images. 214 | 215 | 1.2.840.10008.1.2.4.70 : JPEG Lossless, Non-Hierarchical, First-Order 216 | Prediction (Process 14 [Selection Value 1] 217 | """ 218 | 219 | uid = "1.2.840.10008.1.2.4.70" 220 | 221 | def test_raises(self): 222 | """Test process 2 greyscale.""" 223 | ds = self.ds["JPEGLosslessP14SV1_1s_1f_u_08_08.dcm"]["ds"] 224 | assert self.uid == ds.file_meta.TransferSyntaxUID 225 | assert 1 == ds.SamplesPerPixel 226 | assert 1 == getattr(ds, "NumberOfFrames", 1) 227 | assert "MONOCHROME" in ds.PhotometricInterpretation 228 | assert 8 == ds.BitsAllocated 229 | assert 8 == ds.BitsStored 230 | assert 0 == ds.PixelRepresentation 231 | 232 | if PYD_VERSION < 3: 233 | msg = ( 234 | "Unable to convert the Pixel Data as the 'pylibjpeg-libjpeg' plugin is " 235 | "not installed" 236 | ) 237 | else: 238 | msg = ( 239 | "Unable to decompress 'JPEG Lossless, Non-Hierarchical, First-Order " 240 | r"Prediction \(Process 14 \[Selection Value 1\]\)' " 241 | "pixel data because all plugins are missing dependencies:" 242 | ) 243 | 244 | with pytest.raises(RuntimeError, match=msg): 245 | ds.pixel_array 246 | 247 | 248 | # ISO/IEC 14495 JPEG-LS - Expected fail 249 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies") 250 | class TestJPEGLSLossless(HandlerTestBase): 251 | """Test the handler with ISO 14495 JPEG-LS images. 252 | 253 | 1.2.840.10008.1.2.4.80 : JPEG-LS Lossless Image Compression 254 | """ 255 | 256 | uid = "1.2.840.10008.1.2.4.80" 257 | 258 | def test_raises(self): 259 | """Test process 2 greyscale.""" 260 | ds = self.ds["MR_small_jpeg_ls_lossless.dcm"]["ds"] 261 | assert self.uid == ds.file_meta.TransferSyntaxUID 262 | assert 1 == ds.SamplesPerPixel 263 | assert 1 == getattr(ds, "NumberOfFrames", 1) 264 | assert "MONOCHROME" in ds.PhotometricInterpretation 265 | assert 16 == ds.BitsAllocated 266 | assert 16 == ds.BitsStored 267 | assert 1 == ds.PixelRepresentation 268 | 269 | if PYD_VERSION < 3: 270 | msg = ( 271 | "Unable to convert the Pixel Data as the 'pylibjpeg-libjpeg' plugin is " 272 | "not installed" 273 | ) 274 | else: 275 | msg = ( 276 | r"Unable to decompress 'JPEG-LS Lossless Image Compression' " 277 | "pixel data because all plugins are missing dependencies:" 278 | ) 279 | 280 | with pytest.raises(RuntimeError, match=msg): 281 | ds.pixel_array 282 | 283 | 284 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies") 285 | class TestJPEGLS(HandlerTestBase): 286 | """Test the handler with ISO 14495 JPEG-LS images. 287 | 288 | 1.2.840.10008.1.2.4.81 : JPEG-LS Lossy (Near-Lossless) Image Compression 289 | """ 290 | 291 | uid = "1.2.840.10008.1.2.4.81" 292 | 293 | def test_raises(self): 294 | """Test process 2 greyscale.""" 295 | ds = self.ds["CT1_JLSN.dcm"]["ds"] 296 | assert self.uid == ds.file_meta.TransferSyntaxUID 297 | assert 1 == ds.SamplesPerPixel 298 | assert 1 == getattr(ds, "NumberOfFrames", 1) 299 | assert "MONOCHROME" in ds.PhotometricInterpretation 300 | assert 16 == ds.BitsAllocated 301 | assert 16 == ds.BitsStored 302 | assert 1 == ds.PixelRepresentation 303 | 304 | if PYD_VERSION < 3: 305 | msg = ( 306 | "Unable to convert the Pixel Data as the 'pylibjpeg-libjpeg' plugin is " 307 | "not installed" 308 | ) 309 | else: 310 | msg = ( 311 | r"Unable to decompress 'JPEG-LS Lossy \(Near-Lossless\) Image Compression' " 312 | "pixel data because all plugins are missing dependencies:" 313 | ) 314 | 315 | with pytest.raises(RuntimeError, match=msg): 316 | ds.pixel_array 317 | 318 | 319 | # ISO/IEC 15444 JPEG 2000 320 | @pytest.mark.skipif(not HAS_PYDICOM, reason="No dependencies") 321 | class TestJPEG2000Lossless(HandlerTestBase): 322 | """Test the handler with ISO 15444 JPEG2000 images. 323 | 324 | 1.2.840.10008.1.2.4.90 : JPEG 2000 Image Compression (Lossless Only) 325 | """ 326 | 327 | uid = "1.2.840.10008.1.2.4.90" 328 | 329 | @pytest.mark.skip("No suitable dataset") 330 | def test_1s_1f_i_08_08(self): 331 | """Test 1 component, 1 frame, signed 8-bit.""" 332 | ds = self.ds[".dcm"]["ds"] 333 | assert self.uid == ds.file_meta.TransferSyntaxUID 334 | assert 1 == ds.SamplesPerPixel 335 | assert 1 == getattr(ds, "NumberOfFrames", 1) 336 | assert "MONOCHROME" in ds.PhotometricInterpretation 337 | assert 8 == ds.BitsAllocated 338 | assert 8 == ds.BitsStored 339 | assert 1 == ds.PixelRepresentation 340 | 341 | arr = ds.pixel_array 342 | assert arr.flags.writeable 343 | assert "int8" == arr.dtype 344 | assert (ds.Rows, ds.Columns) == arr.shape 345 | 346 | @pytest.mark.skip("No suitable dataset") 347 | def test_1s_1f_u_08_08(self): 348 | """Test 1 component, 1 frame, unsigned 8-bit.""" 349 | ds = self.ds[".dcm"]["ds"] 350 | assert self.uid == ds.file_meta.TransferSyntaxUID 351 | assert 1 == ds.SamplesPerPixel 352 | assert 1 == getattr(ds, "NumberOfFrames", 1) 353 | assert "MONOCHROME" in ds.PhotometricInterpretation 354 | assert 8 == ds.BitsAllocated 355 | assert 8 == ds.BitsStored 356 | assert 0 == ds.PixelRepresentation 357 | 358 | arr = ds.pixel_array 359 | assert arr.flags.writeable 360 | assert "int8" == arr.dtype 361 | assert (ds.Rows, ds.Columns) == arr.shape 362 | 363 | @pytest.mark.skip("No suitable dataset") 364 | def test_1s_2f_i_08_08(self): 365 | """Test 1 component, 2 frame, signed 8-bit.""" 366 | ds = self.ds[".dcm"]["ds"] 367 | assert self.uid == ds.file_meta.TransferSyntaxUID 368 | assert 1 == ds.SamplesPerPixel 369 | assert 2 == getattr(ds, "NumberOfFrames", 1) 370 | assert "MONOCHROME" in ds.PhotometricInterpretation 371 | assert 8 == ds.BitsAllocated 372 | assert 8 == ds.BitsStored 373 | assert 1 == ds.PixelRepresentation 374 | 375 | arr = ds.pixel_array 376 | assert arr.flags.writeable 377 | assert "int8" == arr.dtype 378 | assert (ds.Rows, ds.Columns) == arr.shape 379 | 380 | def test_3s_1f_u_08_08(self): 381 | """Test 3 component, 1 frame, unsigned 8-bit.""" 382 | ds = self.ds["US1_J2KR.dcm"]["ds"] 383 | assert self.uid == ds.file_meta.TransferSyntaxUID 384 | assert 3 == ds.SamplesPerPixel 385 | assert 1 == getattr(ds, "NumberOfFrames", 1) 386 | assert "YBR_RCT" in ds.PhotometricInterpretation 387 | assert 8 == ds.BitsAllocated 388 | assert 8 == ds.BitsStored 389 | assert 0 == ds.PixelRepresentation 390 | 391 | arr = ds.pixel_array 392 | assert arr.flags.writeable 393 | assert "uint8" == arr.dtype 394 | assert (ds.Rows, ds.Columns, ds.SamplesPerPixel) == arr.shape 395 | 396 | # Values checked against GDCM 397 | assert [ 398 | [180, 26, 0], 399 | [172, 15, 0], 400 | [162, 9, 0], 401 | [152, 4, 0], 402 | [145, 0, 0], 403 | [132, 0, 0], 404 | [119, 0, 0], 405 | [106, 0, 0], 406 | [87, 0, 0], 407 | [37, 0, 0], 408 | [0, 0, 0], 409 | [50, 0, 0], 410 | [100, 0, 0], 411 | [109, 0, 0], 412 | [122, 0, 0], 413 | [135, 0, 0], 414 | [145, 0, 0], 415 | [155, 5, 0], 416 | [165, 11, 0], 417 | [175, 17, 0], 418 | ] == arr[175:195, 28, :].tolist() 419 | 420 | @pytest.mark.skip("No suitable dataset") 421 | def test_3s_2f_i_08_08(self): 422 | """Test 3 component, 2 frame, signed 8-bit.""" 423 | ds = self.ds[".dcm"]["ds"] 424 | assert self.uid == ds.file_meta.TransferSyntaxUID 425 | assert 1 == ds.SamplesPerPixel 426 | assert 1 == getattr(ds, "NumberOfFrames", 1) 427 | assert "RGB" in ds.PhotometricInterpretation 428 | assert 8 == ds.BitsAllocated 429 | assert 8 == ds.BitsStored 430 | assert 1 == ds.PixelRepresentation 431 | 432 | arr = ds.pixel_array 433 | assert arr.flags.writeable 434 | assert "uint8" == arr.dtype 435 | assert (ds.Rows, ds.Columns, ds.SamplesPerPixel) == arr.shape 436 | 437 | def test_1s_1f_i_16_14(self): 438 | """Test 1 component, 1 frame, signed 16/14-bit.""" 439 | ds = self.ds["693_J2KR.dcm"]["ds"] 440 | assert self.uid == ds.file_meta.TransferSyntaxUID 441 | assert 1 == ds.SamplesPerPixel 442 | assert 1 == getattr(ds, "NumberOfFrames", 1) 443 | assert "MONOCHROME" in ds.PhotometricInterpretation 444 | assert 16 == ds.BitsAllocated 445 | # assert 14 == ds.BitsStored # wrong bits stored value - should warn? 446 | assert 1 == ds.PixelRepresentation 447 | 448 | arr = ds.pixel_array 449 | assert arr.flags.writeable 450 | assert "