Initial commit
This commit is contained in:
26
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__init__.py
vendored
Executable file
26
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__init__.py
vendored
Executable file
@@ -0,0 +1,26 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
|
||||
from pydicom.pixels.decoders.base import get_decoder
|
||||
from pydicom.pixels.encoders.base import get_encoder
|
||||
from pydicom.pixels.processing import (
|
||||
apply_color_lut,
|
||||
apply_icc_profile,
|
||||
apply_modality_lut,
|
||||
apply_presentation_lut,
|
||||
apply_rescale,
|
||||
apply_voi_lut,
|
||||
apply_voi,
|
||||
apply_windowing,
|
||||
convert_color_space,
|
||||
create_icc_transform,
|
||||
)
|
||||
from pydicom.pixels.utils import (
|
||||
as_pixel_options,
|
||||
compress,
|
||||
decompress,
|
||||
iter_pixels,
|
||||
pack_bits,
|
||||
pixel_array,
|
||||
set_pixel_data,
|
||||
unpack_bits,
|
||||
)
|
||||
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__pycache__/__init__.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__pycache__/__init__.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__pycache__/common.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__pycache__/common.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__pycache__/processing.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__pycache__/processing.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__pycache__/utils.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/__pycache__/utils.cpython-313.pyc
vendored
Executable file
Binary file not shown.
689
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/common.py
vendored
Executable file
689
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/common.py
vendored
Executable file
@@ -0,0 +1,689 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Common objects for pixel data handling."""
|
||||
|
||||
from enum import Enum, unique
|
||||
from importlib import import_module
|
||||
from typing import TYPE_CHECKING, Any, TypedDict
|
||||
|
||||
from pydicom.misc import warn_and_log
|
||||
from pydicom.pixels.utils import as_pixel_options
|
||||
from pydicom.uid import UID
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from collections.abc import Callable
|
||||
from pydicom.dataset import Dataset
|
||||
from pydicom.pixels.decoders.base import DecodeOptions, DecodeFunction
|
||||
from pydicom.pixels.encoders.base import EncodeOptions, EncodeFunction
|
||||
|
||||
|
||||
Buffer = bytes | bytearray | memoryview
|
||||
|
||||
|
||||
class CoderBase:
|
||||
"""Base class for Decoder and Encoder."""
|
||||
|
||||
def __init__(self, uid: UID, decoder: bool) -> None:
|
||||
"""Create a new data decoder or encoder.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
uid : pydicom.uid.UID
|
||||
The supported *Transfer Syntax UID*.
|
||||
decoder : bool
|
||||
``True`` for a decoder subclass, ``False`` for an encoder subclass.
|
||||
"""
|
||||
# The *Transfer Syntax UID* data will be encoded to
|
||||
self._uid = uid
|
||||
# Available plugins
|
||||
self._available: dict[str, Callable] = {}
|
||||
# Unavailable plugins - missing dependencies or other reason
|
||||
self._unavailable: dict[str, tuple[str, ...]] = {}
|
||||
# True for a Decoder class, False for an Encoder class
|
||||
self._decoder = decoder
|
||||
|
||||
def add_plugin(self, label: str, import_path: tuple[str, str]) -> None:
|
||||
"""Add a plugin to the class instance.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method is not thread-safe.
|
||||
|
||||
The requirements for encoding plugins are available
|
||||
:doc:`here</guides/encoding/encoder_plugins>`, while the requirements
|
||||
for decoding plugins are available :doc:`here
|
||||
</guides/decoding/decoder_plugins>`.
|
||||
|
||||
Only encoding plugins should be added to
|
||||
:class:`~pydicom.pixels.encoders.base.Encoder` class instances
|
||||
and only decoding plugins should be added to
|
||||
:class:`~pydicom.pixels.decoders.base.Decoder` class instances.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The label to use for the plugin, should be unique.
|
||||
import_path : Tuple[str, str]
|
||||
The module import path and the function's name (e.g.
|
||||
``('pydicom.pixels.encoders.pylibjpeg', 'encode_pixel_data')`` or
|
||||
``('pydicom.pixels.decoders.pylibjpeg', 'decode_pixel_data')``).
|
||||
|
||||
Raises
|
||||
------
|
||||
ModuleNotFoundError
|
||||
If the module import path is incorrect or unavailable.
|
||||
AttributeError
|
||||
If the plugin's required functions and attributes aren't found in
|
||||
the module.
|
||||
"""
|
||||
if label in self._available or label in self._unavailable:
|
||||
raise ValueError(
|
||||
f"'{type(self).__name__}' already has a plugin named '{label}'"
|
||||
)
|
||||
|
||||
module = import_module(import_path[0])
|
||||
|
||||
# `is_available(UID)` is required for plugins
|
||||
if module.is_available(self.UID):
|
||||
self._available[label] = getattr(module, import_path[1])
|
||||
else:
|
||||
# `DE/ENCODER_DEPENDENCIES[UID]` is required for plugins
|
||||
if self._decoder:
|
||||
deps = module.DECODER_DEPENDENCIES
|
||||
else:
|
||||
deps = module.ENCODER_DEPENDENCIES
|
||||
|
||||
if self.UID not in deps:
|
||||
raise ValueError(
|
||||
f"The '{label}' plugin doesn't support '{self.UID.name}'"
|
||||
)
|
||||
|
||||
self._unavailable[label] = deps[self.UID]
|
||||
|
||||
def add_plugins(self, plugins: list[tuple[str, tuple[str, str]]]) -> None:
|
||||
"""Add multiple plugins to the class instance.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method is not thread-safe.
|
||||
|
||||
The requirements for encoding plugins are available
|
||||
:doc:`here</guides/encoding/encoder_plugins>`, while the requirements
|
||||
for decoding plugins are available :doc:`here
|
||||
</guides/decoding/decoder_plugins>`.
|
||||
|
||||
Only encoding plugins should be added to
|
||||
:class:`~pydicom.pixels.encoders.base.Encoder` class instances
|
||||
and only decoding plugins should be added to
|
||||
:class:`~pydicom.pixels.decoders.base.Decoder` class instances.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
plugins : list[tuple[str, tuple[str, str]]]
|
||||
A list of [label, import path] for the plugins, where:
|
||||
|
||||
* `label` is the label to use for the plugin, which should be unique.
|
||||
* `import path` is the module import path and the function's
|
||||
name (e.g. ``('pydicom.pixels.encoders.pylibjpeg', 'encode_pixel_data')``
|
||||
or ``('pydicom.pixels.decoders.pylibjpeg', 'decode_pixel_data')``).
|
||||
"""
|
||||
for label, import_path in plugins:
|
||||
self.add_plugin(label, import_path)
|
||||
|
||||
@property
|
||||
def available_plugins(self) -> tuple[str, ...]:
|
||||
"""Return a tuple containing available plugins."""
|
||||
return tuple(sorted(self._available.keys()))
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
"""Return ``True`` if plugins are available that can be used to encode or
|
||||
decode data, ``False`` otherwise.
|
||||
"""
|
||||
if self._decoder and not self.UID.is_encapsulated:
|
||||
return True
|
||||
|
||||
return bool(self._available)
|
||||
|
||||
@property
|
||||
def is_encapsulated(self) -> bool:
|
||||
"""Return ``True`` if the decoder is for an encapsulated transfer
|
||||
syntax, ``False`` otherwise.
|
||||
"""
|
||||
return self.UID.is_encapsulated
|
||||
|
||||
@property
|
||||
def is_native(self) -> bool:
|
||||
"""Return ``True`` if the decoder is for an native transfer
|
||||
syntax, ``False`` otherwise.
|
||||
"""
|
||||
return not self.is_encapsulated
|
||||
|
||||
@property
|
||||
def missing_dependencies(self) -> list[str]:
|
||||
"""Return nice strings for plugins with missing dependencies."""
|
||||
s = []
|
||||
for label, deps in self._unavailable.items():
|
||||
if not deps:
|
||||
# A plugin might have no dependencies and be unavailable for
|
||||
# other reasons
|
||||
s.append(f"{label} - plugin indicating it is unavailable")
|
||||
elif len(deps) > 1:
|
||||
s.append(f"{label} - requires {', '.join(deps[:-1])} and {deps[-1]}")
|
||||
else:
|
||||
s.append(f"{label} - requires {deps[0]}")
|
||||
|
||||
return s
|
||||
|
||||
def remove_plugin(self, label: str) -> None:
|
||||
"""Remove a plugin.
|
||||
|
||||
.. warning::
|
||||
|
||||
This method is not thread-safe.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
label : str
|
||||
The label of the plugin to remove.
|
||||
"""
|
||||
if label in self._available:
|
||||
del self._available[label]
|
||||
elif label in self._unavailable:
|
||||
del self._unavailable[label]
|
||||
else:
|
||||
raise ValueError(f"Unable to remove '{label}', no such plugin'")
|
||||
|
||||
@property
|
||||
def UID(self) -> UID:
|
||||
"""Return the corresponding *Transfer Syntax UID* as :class:`~pydicom.uid.UID`."""
|
||||
return self._uid
|
||||
|
||||
def _validate_plugins(
|
||||
self, plugin: str = ""
|
||||
) -> dict[str, "DecodeFunction"] | dict[str, "EncodeFunction"]:
|
||||
"""Return available plugins.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
plugin : str, optional
|
||||
If not used (default) then return all available plugins, otherwise
|
||||
only return the plugin with a matching name (if it's available).
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, DecodeFunction] | dict[str, EncodeFunction]
|
||||
A dict of available {plugin name: decode/encode function} that can
|
||||
be used to decode/encode the corresponding pixel data.
|
||||
"""
|
||||
if self._decoder and not self.UID.is_encapsulated:
|
||||
return {} # type: ignore[return-value]
|
||||
|
||||
if plugin:
|
||||
if plugin in self.available_plugins:
|
||||
return {plugin: self._available[plugin]}
|
||||
|
||||
if deps := self._unavailable.get(plugin, None):
|
||||
missing = deps[0]
|
||||
if len(deps) > 1:
|
||||
missing = f"{', '.join(deps[:-1])} and {deps[-1]}"
|
||||
|
||||
if self._decoder:
|
||||
raise RuntimeError(
|
||||
f"Unable to decompress '{self.UID.name}' pixel data because "
|
||||
f"the specified plugin is missing dependencies:\n\t{plugin} "
|
||||
f"- requires {missing}"
|
||||
)
|
||||
|
||||
raise RuntimeError(
|
||||
f"Unable to compress the pixel data using '{self.UID.name}' because "
|
||||
f"the specified plugin is missing dependencies:\n\t{plugin} "
|
||||
f"- requires {missing}"
|
||||
)
|
||||
|
||||
msg = (
|
||||
f"No plugin named '{plugin}' has been added to '{self.UID.keyword}"
|
||||
f"{type(self).__name__}'"
|
||||
)
|
||||
if self._available:
|
||||
msg += f", available plugins are: {', '.join(self.available_plugins)}"
|
||||
|
||||
raise ValueError(msg)
|
||||
|
||||
if self._available:
|
||||
return self._available.copy()
|
||||
|
||||
missing = "\n".join([f"\t{s}" for s in self.missing_dependencies])
|
||||
if self._decoder:
|
||||
raise RuntimeError(
|
||||
f"Unable to decompress '{self.UID.name}' pixel data because all "
|
||||
f"plugins are missing dependencies:\n{missing}"
|
||||
)
|
||||
|
||||
raise RuntimeError(
|
||||
f"Unable to compress the pixel data using '{self.UID.name}' because all "
|
||||
f"plugins are missing dependencies:\n{missing}"
|
||||
)
|
||||
|
||||
|
||||
# TODO: Python 3.11 switch to StrEnum
|
||||
@unique
|
||||
class PhotometricInterpretation(str, Enum):
|
||||
"""Values for (0028,0004) *Photometric Interpretation*"""
|
||||
|
||||
# Standard Photometric Interpretations from C.7.6.3.1.2 in Part 3
|
||||
MONOCHROME1 = "MONOCHROME1"
|
||||
MONOCHROME2 = "MONOCHROME2"
|
||||
PALETTE_COLOR = "PALETTE COLOR"
|
||||
RGB = "RGB"
|
||||
YBR_FULL = "YBR_FULL"
|
||||
YBR_FULL_422 = "YBR_FULL_422"
|
||||
YBR_ICT = "YBR_ICT"
|
||||
YBR_RCT = "YBR_RCT"
|
||||
HSV = "HSV" # Retired
|
||||
ARGB = "ARGB" # Retired
|
||||
CMYK = "CMYK" # Retired
|
||||
YBR_PARTIAL_422 = "YBR_PARTIAL_422" # Retired
|
||||
YBR_PARTIAL_420 = "YBR_PARTIAL_420" # Retired
|
||||
|
||||
# TODO: no longer needed if StrEnum
|
||||
def __str__(self) -> str:
|
||||
return str.__str__(self)
|
||||
|
||||
|
||||
class RunnerBase:
|
||||
"""Base class for the pixel data decoding/encoding process managers."""
|
||||
|
||||
def __init__(self, tsyntax: UID) -> None:
|
||||
"""Create a new runner for encoding/decoding data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tsyntax : pydicom.uid.UID
|
||||
The transfer syntax UID corresponding to the pixel data to be
|
||||
decoded.
|
||||
"""
|
||||
# Runner options
|
||||
self._opts: DecodeOptions | EncodeOptions = {}
|
||||
self.set_option("transfer_syntax_uid", tsyntax)
|
||||
# Runner options that cannot be deleted, only modified
|
||||
self._undeletable: tuple[str, ...] = ("transfer_syntax_uid",)
|
||||
|
||||
# The source type, one of "Dataset", "Buffer", "Array" or "BinaryIO"
|
||||
self._src_type = "UNDEFINED"
|
||||
|
||||
@property
|
||||
def bits_allocated(self) -> int:
|
||||
"""Return the expected number of bits allocated used by the data."""
|
||||
if (value := self._opts.get("bits_allocated", None)) is not None:
|
||||
return value
|
||||
|
||||
raise AttributeError("No value for 'bits_allocated' has been set")
|
||||
|
||||
@property
|
||||
def bits_stored(self) -> int:
|
||||
"""Return the expected number of bits stored used by the data."""
|
||||
if (value := self._opts.get("bits_stored", None)) is not None:
|
||||
return value
|
||||
|
||||
raise AttributeError("No value for 'bits_stored' has been set")
|
||||
|
||||
@property
|
||||
def columns(self) -> int:
|
||||
"""Return the expected number of columns in the data."""
|
||||
if (value := self._opts.get("columns", None)) is not None:
|
||||
return value
|
||||
|
||||
raise AttributeError("No value for 'columns' has been set")
|
||||
|
||||
def del_option(self, name: str) -> None:
|
||||
"""Delete option `name` from the runner."""
|
||||
if name in self._undeletable:
|
||||
raise ValueError(f"Deleting '{name}' is not allowed")
|
||||
|
||||
self._opts.pop(name, None) # type: ignore[misc]
|
||||
|
||||
@property
|
||||
def extended_offsets(
|
||||
self,
|
||||
) -> tuple[list[int], list[int]] | tuple[bytes, bytes] | None:
|
||||
"""Return the extended offsets table and lengths
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[list[int], list[int]] | tuple[bytes, bytes] | None
|
||||
Returns the extended offsets and lengths as either lists of int
|
||||
or their equivalent encoded values, or ``None`` if no extended
|
||||
offsets have been set.
|
||||
"""
|
||||
return self._opts.get("extended_offsets", None)
|
||||
|
||||
def frame_length(self, unit: str = "bytes") -> int | float:
|
||||
"""Return the expected length (in number of bytes or pixels) of each
|
||||
frame of pixel data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
unit : str, optional
|
||||
If ``"bytes"`` then returns the expected length of the pixel data
|
||||
in whole bytes and NOT including an odd length trailing NULL
|
||||
padding byte. If ``"pixels"`` then returns the expected length of
|
||||
the pixel data in terms of the total number of pixels (default
|
||||
``"bytes"``).
|
||||
|
||||
Returns
|
||||
-------
|
||||
int | float
|
||||
The expected length of a single frame of pixel data in either whole
|
||||
bytes or pixels, excluding the NULL trailing padding byte for odd
|
||||
length data. For "pixels", an integer will always be returned. For
|
||||
"bytes", a float will be returned for images with BitsAllocated of
|
||||
1 whose frames do not consist of a whole number of bytes.
|
||||
"""
|
||||
length: int | float = self.rows * self.columns * self.samples_per_pixel
|
||||
|
||||
if unit == "pixels":
|
||||
return length
|
||||
|
||||
# Correct for the number of bytes per pixel
|
||||
if self.bits_allocated == 1:
|
||||
if self.transfer_syntax.is_encapsulated:
|
||||
# Determine the nearest whole number of bytes needed to contain
|
||||
# 1-bit pixel data. e.g. 10 x 10 1-bit pixels is 100 bits,
|
||||
# which are packed into 12.5 -> 13 bytes
|
||||
length = length // 8 + (length % 8 > 0)
|
||||
else:
|
||||
# For native, "bit-packed" pixel data, frames are not padded so
|
||||
# this may not be a whole number of bytes e.g. 10x10 = 100
|
||||
# pixels images are packed into 12.5 bytes
|
||||
length = length / 8
|
||||
if length.is_integer():
|
||||
length = int(length)
|
||||
else:
|
||||
length *= self.bits_allocated // 8
|
||||
|
||||
# DICOM Standard, Part 4, Annex C.7.6.3.1.2 - native only
|
||||
if (
|
||||
self.photometric_interpretation == PhotometricInterpretation.YBR_FULL_422
|
||||
and not self.transfer_syntax.is_encapsulated
|
||||
):
|
||||
length = length // 3 * 2
|
||||
|
||||
return length
|
||||
|
||||
def get_option(self, name: str, default: Any = None) -> Any:
|
||||
"""Return the value of the option `name`."""
|
||||
return self._opts.get(name, default)
|
||||
|
||||
@property
|
||||
def is_array(self) -> bool:
|
||||
"""Return ``True`` if the pixel data source is an :class:`~numpy.ndarray`"""
|
||||
return self._src_type == "Array"
|
||||
|
||||
@property
|
||||
def is_binary(self) -> bool:
|
||||
"""Return ``True`` if the pixel data source is BinaryIO"""
|
||||
return self._src_type == "BinaryIO"
|
||||
|
||||
@property
|
||||
def is_buffer(self) -> bool:
|
||||
"""Return ``True`` if the pixel data source is a buffer-like"""
|
||||
return self._src_type == "Buffer"
|
||||
|
||||
@property
|
||||
def is_dataset(self) -> bool:
|
||||
"""Return ``True`` if the pixel data source is a :class:`~pydicom.dataset.Dataset`"""
|
||||
return self._src_type == "Dataset"
|
||||
|
||||
@property
|
||||
def number_of_frames(self) -> int:
|
||||
"""Return the expected number of frames in the data."""
|
||||
if (value := self._opts.get("number_of_frames", None)) is not None:
|
||||
return value
|
||||
|
||||
raise AttributeError("No value for 'number_of_frames' has been set")
|
||||
|
||||
@property
|
||||
def options(self) -> "DecodeOptions | EncodeOptions":
|
||||
"""Return a reference to the runner's options dict."""
|
||||
return self._opts
|
||||
|
||||
@property
|
||||
def photometric_interpretation(self) -> str:
|
||||
"""Return the expected photometric interpretation of the data."""
|
||||
if (value := self._opts.get("photometric_interpretation", None)) is not None:
|
||||
return value
|
||||
|
||||
raise AttributeError("No value for 'photometric_interpretation' has been set")
|
||||
|
||||
@property
|
||||
def pixel_keyword(self) -> str:
|
||||
"""Return the expected pixel keyword of the data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
One of ``"PixelData"``, ``"FloatPixelData"``, ``"DoubleFloatPixelData"``
|
||||
"""
|
||||
if (value := self._opts.get("pixel_keyword", None)) is not None:
|
||||
return value
|
||||
|
||||
raise AttributeError("No value for 'pixel_keyword' has been set")
|
||||
|
||||
@property
|
||||
def pixel_representation(self) -> int:
|
||||
"""Return the expected pixel representation of the data."""
|
||||
if (value := self._opts.get("pixel_representation", None)) is not None:
|
||||
return value
|
||||
|
||||
raise AttributeError("No value for 'pixel_representation' has been set")
|
||||
|
||||
@property
|
||||
def planar_configuration(self) -> int:
|
||||
"""Return the expected planar configuration of the data."""
|
||||
# Only required when number of samples is more than 1
|
||||
# Uncompressed may be either 0 or 1
|
||||
if (value := self._opts.get("planar_configuration", None)) is not None:
|
||||
return value
|
||||
|
||||
# Planar configuration is not relevant for compressed syntaxes
|
||||
if self.transfer_syntax.is_compressed:
|
||||
return 0
|
||||
|
||||
raise AttributeError("No value for 'planar_configuration' has been set")
|
||||
|
||||
@property
|
||||
def rows(self) -> int:
|
||||
"""Return the expected number of rows in the data."""
|
||||
if (value := self._opts.get("rows", None)) is not None:
|
||||
return value
|
||||
|
||||
raise AttributeError("No value for 'rows' has been set")
|
||||
|
||||
@property
|
||||
def samples_per_pixel(self) -> int:
|
||||
"""Return the expected number of samples per pixel in the data."""
|
||||
if (value := self._opts.get("samples_per_pixel", None)) is not None:
|
||||
return value
|
||||
|
||||
raise AttributeError("No value for 'samples_per_pixel' has been set")
|
||||
|
||||
def set_option(self, name: str, value: Any) -> None:
|
||||
"""Set a runner option.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name : str
|
||||
The name of the option to be set.
|
||||
value : Any
|
||||
The value of the option.
|
||||
"""
|
||||
if name == "number_of_frames":
|
||||
value = int(value) if isinstance(value, str) else value
|
||||
if value in (None, 0):
|
||||
warn_and_log(
|
||||
f"A value of '{value}' for (0028,0008) 'Number of Frames' is "
|
||||
"invalid, assuming 1 frame"
|
||||
)
|
||||
value = 1
|
||||
elif name == "photometric_interpretation":
|
||||
if value == "PALETTE COLOR":
|
||||
value = PhotometricInterpretation.PALETTE_COLOR
|
||||
try:
|
||||
value = PhotometricInterpretation[value]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
self._opts[name] = value # type: ignore[literal-required]
|
||||
|
||||
def set_options(self, **kwargs: "DecodeOptions | EncodeOptions") -> None:
|
||||
"""Set multiple runner options.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
kwargs : dict[str, Any]
|
||||
A dictionary containing the options as ``{name: value}``, where
|
||||
`name` is the name of the option and `value` is it's value.
|
||||
"""
|
||||
for name, value in kwargs.items():
|
||||
self.set_option(name, value)
|
||||
|
||||
def _set_options_ds(self, ds: "Dataset") -> None:
|
||||
"""Set options using a dataset.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ds : pydicom.dataset.Dataset
|
||||
The dataset to use.
|
||||
"""
|
||||
self.set_options(**as_pixel_options(ds))
|
||||
|
||||
@property
|
||||
def transfer_syntax(self) -> UID:
|
||||
"""Return the expected transfer syntax corresponding to the data."""
|
||||
return self._opts["transfer_syntax_uid"]
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate the runner options and source data (if any)."""
|
||||
raise NotImplementedError(
|
||||
f"{type(self).__name__}.validate() has not been implemented"
|
||||
)
|
||||
|
||||
def _validate_options(self) -> None:
|
||||
"""Validate the supplied options to ensure they meet requirements."""
|
||||
prefix = "Missing required element: (0028"
|
||||
if self._opts.get("bits_allocated") is None:
|
||||
raise AttributeError(f"{prefix},0100) 'Bits Allocated'")
|
||||
|
||||
if not 1 <= self.bits_allocated <= 64 or (
|
||||
self.bits_allocated != 1 and self.bits_allocated % 8
|
||||
):
|
||||
raise ValueError(
|
||||
f"A (0028,0100) 'Bits Allocated' value of '{self.bits_allocated}' "
|
||||
"is invalid, it must be 1 or a multiple of 8 and in the range (1, 64)"
|
||||
)
|
||||
|
||||
if "Float" not in self.pixel_keyword:
|
||||
if self._opts.get("bits_stored") is None:
|
||||
raise AttributeError(f"{prefix},0101) 'Bits Stored'")
|
||||
|
||||
if not 1 <= self.bits_stored <= self.bits_allocated <= 64:
|
||||
raise ValueError(
|
||||
f"A (0028,0101) 'Bits Stored' value of '{self.bits_stored}' is "
|
||||
"invalid, it must be in the range (1, 64) and no greater than "
|
||||
"the (0028,0100) 'Bits Allocated' value of "
|
||||
f"'{self.bits_allocated}'"
|
||||
)
|
||||
|
||||
if self._opts.get("columns") is None:
|
||||
raise AttributeError(f"{prefix},0011) 'Columns'")
|
||||
|
||||
if not 0 < self.columns <= 2**16 - 1:
|
||||
raise ValueError(
|
||||
f"A (0028,0011) 'Columns' value of '{self.columns}' is invalid, "
|
||||
"it must be in the range (1, 65535)"
|
||||
)
|
||||
|
||||
# Number of Frames is conditionally required
|
||||
if self._opts.get("number_of_frames") is not None and self.number_of_frames < 1:
|
||||
raise ValueError(
|
||||
f"A (0028,0008) 'Number of Frames' value of '{self.number_of_frames}' "
|
||||
"is invalid, it must be greater than or equal to 1"
|
||||
)
|
||||
|
||||
if self._opts.get("photometric_interpretation") is None:
|
||||
raise AttributeError(f"{prefix},0004) 'Photometric Interpretation'")
|
||||
|
||||
try:
|
||||
PhotometricInterpretation[self.photometric_interpretation]
|
||||
except KeyError:
|
||||
if self.photometric_interpretation != "PALETTE COLOR":
|
||||
raise ValueError(
|
||||
"Unknown (0028,0004) 'Photometric Interpretation' value "
|
||||
f"'{self.photometric_interpretation}'"
|
||||
)
|
||||
|
||||
kw = ("PixelData", "FloatPixelData", "DoubleFloatPixelData")
|
||||
if self.pixel_keyword not in kw:
|
||||
raise ValueError(f"Unknown 'pixel_keyword' value '{self.pixel_keyword}'")
|
||||
|
||||
if self.pixel_keyword == "PixelData":
|
||||
if self._opts.get("pixel_representation") is None:
|
||||
raise AttributeError(f"{prefix},0103) 'Pixel Representation'")
|
||||
|
||||
if self.pixel_representation not in (0, 1):
|
||||
raise ValueError(
|
||||
"A (0028,0103) 'Pixel Representation' value of "
|
||||
f"'{self.pixel_representation}' is invalid, it must be 0 or 1"
|
||||
)
|
||||
|
||||
if self._opts.get("rows") is None:
|
||||
raise AttributeError(f"{prefix},0010) 'Rows'")
|
||||
|
||||
if not 0 < self.rows <= 2**16 - 1:
|
||||
raise ValueError(
|
||||
f"A (0028,0010) 'Rows' value of '{self.rows}' is invalid, it "
|
||||
"must be in the range (1, 65535)"
|
||||
)
|
||||
|
||||
if self._opts.get("samples_per_pixel") is None:
|
||||
raise AttributeError(f"{prefix},0002) 'Samples per Pixel'")
|
||||
|
||||
if self.samples_per_pixel not in (1, 3):
|
||||
raise ValueError(
|
||||
f"A (0028,0002) 'Samples per Pixel' value of '{self.samples_per_pixel}' "
|
||||
"is invalid, it must be 1 or 3"
|
||||
)
|
||||
|
||||
if self.samples_per_pixel == 3:
|
||||
if self._opts.get("planar_configuration") is None:
|
||||
raise AttributeError(f"{prefix},0006) 'Planar Configuration'")
|
||||
|
||||
if self.planar_configuration not in (0, 1):
|
||||
raise ValueError(
|
||||
"A (0028,0006) 'Planar Configuration' value of "
|
||||
f"'{self.planar_configuration}' is invalid, it must be 0 or 1"
|
||||
)
|
||||
|
||||
|
||||
class RunnerOptions(TypedDict, total=False):
|
||||
"""Options accepted by RunnerBase"""
|
||||
|
||||
## Pixel data description options
|
||||
# Required
|
||||
bits_allocated: int
|
||||
bits_stored: int
|
||||
columns: int
|
||||
number_of_frames: int
|
||||
photometric_interpretation: str
|
||||
pixel_keyword: str
|
||||
pixel_representation: int
|
||||
rows: int
|
||||
samples_per_pixel: int
|
||||
transfer_syntax_uid: UID
|
||||
|
||||
# Conditionally required if samples_per_pixel > 1
|
||||
planar_configuration: int
|
||||
|
||||
# Optional
|
||||
# The Extended Offset Table values
|
||||
extended_offsets: tuple[bytes, bytes] | tuple[list[int], list[int]]
|
||||
20
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__init__.py
vendored
Executable file
20
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__init__.py
vendored
Executable file
@@ -0,0 +1,20 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
|
||||
from pydicom.pixels.decoders.base import (
|
||||
ImplicitVRLittleEndianDecoder,
|
||||
ExplicitVRLittleEndianDecoder,
|
||||
ExplicitVRBigEndianDecoder,
|
||||
DeflatedExplicitVRLittleEndianDecoder,
|
||||
JPEGBaseline8BitDecoder,
|
||||
JPEGExtended12BitDecoder,
|
||||
JPEGLosslessDecoder,
|
||||
JPEGLosslessSV1Decoder,
|
||||
JPEGLSLosslessDecoder,
|
||||
JPEGLSNearLosslessDecoder,
|
||||
JPEG2000LosslessDecoder,
|
||||
JPEG2000Decoder,
|
||||
HTJ2KLosslessDecoder,
|
||||
HTJ2KLosslessRPCLDecoder,
|
||||
HTJ2KDecoder,
|
||||
RLELosslessDecoder,
|
||||
)
|
||||
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/__init__.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/__init__.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/base.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/base.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/gdcm.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/gdcm.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/pillow.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/pillow.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/pyjpegls.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/pyjpegls.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/pylibjpeg.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/pylibjpeg.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/rle.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/__pycache__/rle.cpython-313.pyc
vendored
Executable file
Binary file not shown.
2039
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/base.py
vendored
Executable file
2039
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/base.py
vendored
Executable file
File diff suppressed because it is too large
Load Diff
144
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/gdcm.py
vendored
Executable file
144
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/gdcm.py
vendored
Executable file
@@ -0,0 +1,144 @@
|
||||
# Copyright 2024 pydicom authors. See LICENSE file for details.
|
||||
"""Use GDCM <https://github.com/malaterre/GDCM> to decompress encoded
|
||||
*Pixel Data*.
|
||||
|
||||
This module is not intended to be used directly.
|
||||
"""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from pydicom import uid
|
||||
from pydicom.pixels.decoders.base import DecodeRunner
|
||||
from pydicom.pixels.common import PhotometricInterpretation as PI
|
||||
|
||||
try:
|
||||
import gdcm
|
||||
|
||||
GDCM_VERSION = tuple(int(x) for x in gdcm.Version.GetVersion().split("."))
|
||||
HAVE_GDCM = True
|
||||
except ImportError:
|
||||
HAVE_GDCM = False
|
||||
|
||||
|
||||
DECODER_DEPENDENCIES = {
|
||||
uid.JPEGBaseline8Bit: ("gdcm>=3.0.10",),
|
||||
uid.JPEGExtended12Bit: ("gdcm>=3.0.10",),
|
||||
uid.JPEGLossless: ("gdcm>=3.0.10",),
|
||||
uid.JPEGLosslessSV1: ("gdcm>=3.0.10",),
|
||||
uid.JPEGLSLossless: ("gdcm>=3.0.10",),
|
||||
uid.JPEGLSNearLossless: ("gdcm>=3.0.10",),
|
||||
uid.JPEG2000Lossless: ("gdcm>=3.0.10",),
|
||||
uid.JPEG2000: ("gdcm>=3.0.10",),
|
||||
}
|
||||
|
||||
|
||||
def is_available(uid: str) -> bool:
|
||||
"""Return ``True`` if a pixel data decoder for `uid` is available for use,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
if not HAVE_GDCM or GDCM_VERSION < (3, 0):
|
||||
return False
|
||||
|
||||
return uid in DECODER_DEPENDENCIES
|
||||
|
||||
|
||||
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytes:
|
||||
"""Return the decoded `src` as :class:`bytes`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes
|
||||
An encoded pixel data frame.
|
||||
runner : pydicom.pixels.decoders.base.DecodeRunner
|
||||
The runner managing the decoding.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The decoded pixel data frame.
|
||||
"""
|
||||
tsyntax = runner.transfer_syntax
|
||||
photometric_interpretation = runner.photometric_interpretation
|
||||
bits_stored = runner.bits_stored
|
||||
if tsyntax == uid.JPEGExtended12Bit and bits_stored != 8:
|
||||
raise NotImplementedError(
|
||||
"GDCM does not support 'JPEG Extended' for samples with 12-bit precision"
|
||||
)
|
||||
|
||||
if (
|
||||
tsyntax == uid.JPEGLSNearLossless
|
||||
and runner.pixel_representation == 1
|
||||
and bits_stored < 8
|
||||
):
|
||||
raise ValueError(
|
||||
"Unable to decode signed lossy JPEG-LS pixel data with a sample "
|
||||
"precision less than 8 bits"
|
||||
)
|
||||
|
||||
if tsyntax in uid.JPEGLSTransferSyntaxes and bits_stored in (6, 7):
|
||||
raise ValueError(
|
||||
"Unable to decode unsigned JPEG-LS pixel data with a sample "
|
||||
"precision of 6 or 7 bits"
|
||||
)
|
||||
|
||||
fragment = gdcm.Fragment()
|
||||
fragment.SetByteStringValue(src)
|
||||
|
||||
fragments = gdcm.SequenceOfFragments.New()
|
||||
fragments.AddFragment(fragment)
|
||||
|
||||
elem = gdcm.DataElement(gdcm.Tag(0x7FE0, 0x0010))
|
||||
elem.SetValue(fragments.__ref__())
|
||||
|
||||
img = gdcm.Image()
|
||||
img.SetNumberOfDimensions(2)
|
||||
img.SetDimensions((runner.columns, runner.rows, 1))
|
||||
img.SetDataElement(elem)
|
||||
|
||||
pi_type = gdcm.PhotometricInterpretation.GetPIType(photometric_interpretation)
|
||||
img.SetPhotometricInterpretation(gdcm.PhotometricInterpretation(pi_type))
|
||||
if runner.samples_per_pixel > 1:
|
||||
img.SetPlanarConfiguration(runner.planar_configuration)
|
||||
|
||||
ts_type = gdcm.TransferSyntax.GetTSType(str.__str__(tsyntax))
|
||||
img.SetTransferSyntax(gdcm.TransferSyntax(ts_type))
|
||||
|
||||
if tsyntax in uid.JPEGLSTransferSyntaxes:
|
||||
# GDCM always returns JPEG-LS data as color-by-pixel
|
||||
runner.set_option("planar_configuration", 0)
|
||||
bits_stored = runner.get_option("jls_precision", bits_stored)
|
||||
if 0 < bits_stored <= 8:
|
||||
runner.set_option("bits_allocated", 8)
|
||||
elif 8 < bits_stored <= 16:
|
||||
runner.set_option("bits_allocated", 16)
|
||||
|
||||
if tsyntax in uid.JPEG2000TransferSyntaxes:
|
||||
# GDCM pixel container size is based on precision
|
||||
bits_stored = runner.get_option("j2k_precision", bits_stored)
|
||||
if 0 < bits_stored <= 8:
|
||||
runner.set_option("bits_allocated", 8)
|
||||
elif 8 < bits_stored <= 16:
|
||||
runner.set_option("bits_allocated", 16)
|
||||
elif 16 < bits_stored <= 32:
|
||||
runner.set_option("bits_allocated", 32)
|
||||
|
||||
pixel_format = gdcm.PixelFormat(
|
||||
runner.samples_per_pixel,
|
||||
runner.bits_allocated,
|
||||
bits_stored,
|
||||
bits_stored - 1,
|
||||
runner.pixel_representation,
|
||||
)
|
||||
img.SetPixelFormat(pixel_format)
|
||||
|
||||
# GDCM returns char* as str, so re-encode it to bytes
|
||||
frame = img.GetBuffer().encode("utf-8", "surrogateescape")
|
||||
|
||||
# GDCM returns YBR_ICT and YBR_RCT as RGB
|
||||
if tsyntax in uid.JPEG2000TransferSyntaxes and photometric_interpretation in (
|
||||
PI.YBR_ICT,
|
||||
PI.YBR_RCT,
|
||||
):
|
||||
runner.set_option("photometric_interpretation", PI.RGB)
|
||||
|
||||
return cast(bytes, frame)
|
||||
127
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/pillow.py
vendored
Executable file
127
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/pillow.py
vendored
Executable file
@@ -0,0 +1,127 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Use Pillow <https://github.com/python-pillow/Pillow> to decompress encoded
|
||||
*Pixel Data*.
|
||||
|
||||
This module is not intended to be used directly.
|
||||
"""
|
||||
|
||||
from io import BytesIO
|
||||
from typing import cast
|
||||
|
||||
from pydicom import uid
|
||||
from pydicom.pixels.utils import _passes_version_check
|
||||
from pydicom.pixels.common import PhotometricInterpretation as PI
|
||||
from pydicom.pixels.decoders.base import DecodeRunner
|
||||
|
||||
try:
|
||||
from PIL import Image, features
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
HAVE_NP = True
|
||||
except ImportError:
|
||||
HAVE_NP = False
|
||||
|
||||
|
||||
DECODER_DEPENDENCIES = {
|
||||
uid.JPEGBaseline8Bit: ("pillow>=10.0",),
|
||||
uid.JPEGExtended12Bit: ("pillow>=10.0",),
|
||||
uid.JPEG2000Lossless: ("numpy", "pillow>=10.0"),
|
||||
uid.JPEG2000: ("numpy", "pillow>=10.0"),
|
||||
}
|
||||
|
||||
_LIBJPEG_SYNTAXES = [uid.JPEGBaseline8Bit, uid.JPEGExtended12Bit]
|
||||
_OPENJPEG_SYNTAXES = [uid.JPEG2000Lossless, uid.JPEG2000]
|
||||
|
||||
|
||||
def is_available(uid: str) -> bool:
|
||||
"""Return ``True`` if a pixel data decoder for `uid` is available for use,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
if not _passes_version_check("PIL", (10, 0)):
|
||||
return False
|
||||
|
||||
if uid in _LIBJPEG_SYNTAXES:
|
||||
return bool(features.check_codec("jpg")) # type: ignore[no-untyped-call]
|
||||
|
||||
if uid in _OPENJPEG_SYNTAXES:
|
||||
return bool(features.check_codec("jpg_2000")) and HAVE_NP # type: ignore[no-untyped-call]
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytes:
|
||||
"""Return the decoded image data in `src` as a :class:`bytes`."""
|
||||
tsyntax = runner.transfer_syntax
|
||||
|
||||
# libjpeg only supports 8-bit JPEG Extended (can be 8 or 12 in the JPEG standard)
|
||||
if tsyntax == uid.JPEGExtended12Bit and runner.bits_stored != 8:
|
||||
raise NotImplementedError(
|
||||
"Pillow does not support 'JPEG Extended' for samples with 12-bit precision"
|
||||
)
|
||||
|
||||
image = Image.open(BytesIO(src), formats=("JPEG", "JPEG2000"))
|
||||
if tsyntax in _LIBJPEG_SYNTAXES:
|
||||
if runner.samples_per_pixel != 1:
|
||||
# If the Adobe APP14 marker is not present then Pillow assumes
|
||||
# that JPEG images were transformed into YCbCr color space prior
|
||||
# to compression, so setting the image mode to YCbCr signals we
|
||||
# don't want any color transformations.
|
||||
# Any color transformations would be inconsistent with the
|
||||
# behavior required by the `raw` flag
|
||||
if "adobe_transform" not in image.info:
|
||||
image.draft("YCbCr", image.size) # type: ignore[no-untyped-call]
|
||||
|
||||
return cast(bytes, image.tobytes())
|
||||
|
||||
# JPEG 2000
|
||||
# The precision from the J2K codestream is more appropriate because the
|
||||
# decoder will use it to create the output integers
|
||||
precision = runner.get_option("j2k_precision", runner.bits_stored)
|
||||
# pillow's pixel container size is based on precision
|
||||
if 0 < precision <= 8:
|
||||
runner.set_option("bits_allocated", 8)
|
||||
elif 8 < precision <= 16:
|
||||
# Pillow converts >= 9-bit RGB/YCbCr data to 8-bit
|
||||
if runner.samples_per_pixel > 1:
|
||||
raise ValueError(
|
||||
f"Pillow cannot decode {precision}-bit multi-sample data correctly"
|
||||
)
|
||||
|
||||
runner.set_option("bits_allocated", 16)
|
||||
else:
|
||||
raise ValueError(
|
||||
"only (0028,0101) 'Bits Stored' values of up to 16 are supported"
|
||||
)
|
||||
|
||||
# Pillow converts N-bit signed/unsigned data to 8- or 16-bit unsigned data
|
||||
# See Pillow src/libImaging/Jpeg2KDecode.c::j2ku_gray_i
|
||||
buffer = bytearray(image.tobytes()) # so the array is writeable
|
||||
del image
|
||||
dtype = runner.pixel_dtype
|
||||
arr = np.frombuffer(buffer, dtype=f"u{dtype.itemsize}")
|
||||
|
||||
is_signed = runner.pixel_representation
|
||||
if runner.get_option("apply_j2k_sign_correction", False):
|
||||
is_signed = runner.get_option("j2k_is_signed", is_signed)
|
||||
|
||||
if is_signed and runner.pixel_representation == 1:
|
||||
# Re-view the unsigned integers as signed
|
||||
# e.g. [0, 127, 128, 255] -> [0, 127, -128, -1]
|
||||
arr = arr.view(dtype)
|
||||
# Level-shift to match the unsigned integers range
|
||||
# e.g. [0, 127, -128, -1] -> [-128, -1, 0, 127]
|
||||
arr -= np.int32(2 ** (runner.bits_allocated - 1))
|
||||
|
||||
if bit_shift := (runner.bits_allocated - precision):
|
||||
# Bit shift to undo the upscaling of N-bit to 8- or 16-bit
|
||||
np.right_shift(arr, bit_shift, out=arr)
|
||||
|
||||
# pillow returns YBR_ICT and YBR_RCT as RGB
|
||||
if runner.photometric_interpretation in (PI.YBR_ICT, PI.YBR_RCT):
|
||||
runner.set_option("photometric_interpretation", PI.RGB)
|
||||
|
||||
return cast(bytes, arr.tobytes())
|
||||
47
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/pyjpegls.py
vendored
Executable file
47
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/pyjpegls.py
vendored
Executable file
@@ -0,0 +1,47 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Use pyjpegls <https://github.com/pydicom/pyjpegls> to decompress encoded
|
||||
*Pixel Data*.
|
||||
|
||||
This module is not intended to be used directly.
|
||||
"""
|
||||
from typing import cast
|
||||
|
||||
from pydicom import uid
|
||||
from pydicom.pixels.utils import _passes_version_check
|
||||
from pydicom.pixels.decoders.base import DecodeRunner
|
||||
|
||||
try:
|
||||
import jpeg_ls
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
DECODER_DEPENDENCIES = {
|
||||
uid.JPEGLSLossless: ("numpy", "pyjpegls>=1.2"),
|
||||
uid.JPEGLSNearLossless: ("numpy", "pyjpegls>=1.2"),
|
||||
}
|
||||
|
||||
|
||||
def is_available(uid: str) -> bool:
|
||||
"""Return ``True`` if the decoder has its dependencies met, ``False`` otherwise"""
|
||||
return _passes_version_check("jpeg_ls", (1, 2))
|
||||
|
||||
|
||||
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray:
|
||||
"""Return the decoded image data in `src` as a :class:`bytearray`."""
|
||||
buffer, info = jpeg_ls.decode_pixel_data(src)
|
||||
# Interleave mode 0 is colour-by-plane, 1 and 2 are colour-by-pixel
|
||||
if info["components"] > 1:
|
||||
if info["interleave_mode"] == 0:
|
||||
runner.set_option("planar_configuration", 1)
|
||||
else:
|
||||
runner.set_option("planar_configuration", 0)
|
||||
|
||||
precision = info["bits_per_sample"]
|
||||
if 0 < precision <= 8:
|
||||
runner.set_option("bits_allocated", 8)
|
||||
elif 8 < precision <= 16:
|
||||
runner.set_option("bits_allocated", 16)
|
||||
|
||||
return cast(bytearray, buffer)
|
||||
115
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/pylibjpeg.py
vendored
Executable file
115
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/pylibjpeg.py
vendored
Executable file
@@ -0,0 +1,115 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Use pylibjpeg <https://github.com/pydicom/pylibjpeg> to decompress encoded
|
||||
*Pixel Data*.
|
||||
|
||||
This module is not intended to be used directly.
|
||||
"""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from pydicom import uid
|
||||
from pydicom.pixels.decoders.base import DecodeRunner
|
||||
from pydicom.pixels.utils import _passes_version_check
|
||||
from pydicom.pixels.common import PhotometricInterpretation as PI
|
||||
|
||||
try:
|
||||
from pylibjpeg.utils import get_pixel_data_decoders, Decoder
|
||||
|
||||
# {UID: {plugin name: function}}
|
||||
_DECODERS = cast(
|
||||
dict[uid.UID, dict[str, "Decoder"]], get_pixel_data_decoders(version=2)
|
||||
)
|
||||
except ImportError:
|
||||
_DECODERS = {}
|
||||
|
||||
|
||||
DECODER_DEPENDENCIES = {
|
||||
uid.JPEGBaseline8Bit: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
|
||||
uid.JPEGExtended12Bit: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
|
||||
uid.JPEGLossless: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
|
||||
uid.JPEGLosslessSV1: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
|
||||
uid.JPEGLSLossless: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
|
||||
uid.JPEGLSNearLossless: ("pylibjpeg>=2.0", "pylibjpeg-libjpeg>=2.1"),
|
||||
uid.JPEG2000Lossless: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
|
||||
uid.JPEG2000: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
|
||||
uid.HTJ2KLossless: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
|
||||
uid.HTJ2KLosslessRPCL: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
|
||||
uid.HTJ2K: ("pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.0"),
|
||||
uid.RLELossless: ("pylibjpeg>=2.0", "pylibjpeg-rle>=2.0"),
|
||||
}
|
||||
|
||||
_LIBJPEG_SYNTAXES = [
|
||||
uid.JPEGBaseline8Bit,
|
||||
uid.JPEGExtended12Bit,
|
||||
uid.JPEGLossless,
|
||||
uid.JPEGLosslessSV1,
|
||||
uid.JPEGLSLossless,
|
||||
uid.JPEGLSNearLossless,
|
||||
]
|
||||
_OPENJPEG_SYNTAXES = [
|
||||
uid.JPEG2000Lossless,
|
||||
uid.JPEG2000,
|
||||
uid.HTJ2KLossless,
|
||||
uid.HTJ2KLosslessRPCL,
|
||||
uid.HTJ2K,
|
||||
]
|
||||
_RLE_SYNTAXES = [uid.RLELossless]
|
||||
|
||||
|
||||
def is_available(uid: str) -> bool:
|
||||
"""Return ``True`` if the decoder has its dependencies met, ``False`` otherwise"""
|
||||
if not _passes_version_check("pylibjpeg", (2, 0)):
|
||||
return False
|
||||
|
||||
if uid in _LIBJPEG_SYNTAXES:
|
||||
return _passes_version_check("libjpeg", (2, 0, 2))
|
||||
|
||||
if uid in _OPENJPEG_SYNTAXES:
|
||||
return _passes_version_check("openjpeg", (2, 0))
|
||||
|
||||
if uid in _RLE_SYNTAXES:
|
||||
return _passes_version_check("rle", (2, 0))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray: # type: ignore[return]
|
||||
"""Return the decoded image data in `src` as a :class:`bytearray`."""
|
||||
tsyntax = runner.transfer_syntax
|
||||
|
||||
# Currently only one pylibjpeg plugin is available per UID
|
||||
# so decode using the first available decoder
|
||||
for _, func in sorted(_DECODERS[tsyntax].items()):
|
||||
# `version=2` to return frame as bytearray
|
||||
frame = cast(bytearray, func(src, version=2, **runner.options))
|
||||
|
||||
# pylibjpeg-rle returns decoded data as planar configuration 1
|
||||
if tsyntax == uid.RLELossless:
|
||||
runner.set_option("planar_configuration", 1)
|
||||
|
||||
if tsyntax in _OPENJPEG_SYNTAXES:
|
||||
# pylibjpeg-openjpeg returns YBR_ICT and YBR_RCT as RGB
|
||||
if runner.photometric_interpretation in (PI.YBR_ICT, PI.YBR_RCT):
|
||||
runner.set_option("photometric_interpretation", PI.RGB)
|
||||
|
||||
# pylibjpeg-openjpeg pixel container size is based on J2K precision
|
||||
precision = runner.get_option("j2k_precision", runner.bits_stored)
|
||||
if 0 < precision <= 8:
|
||||
runner.set_option("bits_allocated", 8)
|
||||
elif 8 < precision <= 16:
|
||||
runner.set_option("bits_allocated", 16)
|
||||
elif 16 < precision <= 32:
|
||||
runner.set_option("bits_allocated", 32)
|
||||
|
||||
if tsyntax in uid.JPEGLSTransferSyntaxes:
|
||||
# pylibjpeg-libjpeg always returns JPEG-LS data as color-by-pixel
|
||||
runner.set_option("planar_configuration", 0)
|
||||
|
||||
# pylibjpeg-libjpeg pixel container size is based on JPEG-LS precision
|
||||
precision = runner.get_option("jls_precision", runner.bits_stored)
|
||||
if 0 < precision <= 8:
|
||||
runner.set_option("bits_allocated", 8)
|
||||
elif 8 < precision <= 16:
|
||||
runner.set_option("bits_allocated", 16)
|
||||
|
||||
return frame
|
||||
278
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/rle.py
vendored
Executable file
278
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/decoders/rle.py
vendored
Executable file
@@ -0,0 +1,278 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Use Python to decode RLE Lossless encoded *Pixel Data*.
|
||||
|
||||
This module is not intended to be used directly.
|
||||
"""
|
||||
|
||||
from struct import unpack
|
||||
|
||||
from pydicom.misc import warn_and_log
|
||||
from pydicom.pixels.decoders.base import DecodeRunner
|
||||
from pydicom.uid import RLELossless
|
||||
|
||||
|
||||
DECODER_DEPENDENCIES = {RLELossless: ()}
|
||||
|
||||
|
||||
def is_available(uid: str) -> bool:
|
||||
"""Return ``True`` if a pixel data decoder for `uid` is available for use,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
return uid in DECODER_DEPENDENCIES
|
||||
|
||||
|
||||
def _decode_frame(src: bytes, runner: DecodeRunner) -> bytearray:
|
||||
"""Wrapper for use with the decoder interface.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes
|
||||
A single frame of RLE encoded data.
|
||||
runner : pydicom.pixels.decoders.base.DecodeRunner
|
||||
|
||||
|
||||
Required parameters:
|
||||
|
||||
* `rows`: int
|
||||
* `columns`: int
|
||||
* `samples_per_pixel`: int
|
||||
* `bits_allocated`: int
|
||||
|
||||
Optional parameters:
|
||||
|
||||
* `rle_segment_order`: str, "<" for little endian segment order, or
|
||||
">" for big endian (default)
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytearray
|
||||
The decoded frame, ordered as planar configuration 1.
|
||||
"""
|
||||
frame = _rle_decode_frame(
|
||||
src,
|
||||
runner.rows,
|
||||
runner.columns,
|
||||
runner.samples_per_pixel,
|
||||
runner.bits_allocated,
|
||||
runner.get_option("rle_segment_order", ">"),
|
||||
)
|
||||
# Update the runner options to ensure the reshaping is correct
|
||||
# Only do this if we successfully decoded the frame
|
||||
runner.set_option("planar_configuration", 1)
|
||||
|
||||
return frame
|
||||
|
||||
|
||||
def _rle_decode_frame(
|
||||
src: bytes,
|
||||
rows: int,
|
||||
columns: int,
|
||||
nr_samples: int,
|
||||
nr_bits: int,
|
||||
segment_order: str = ">",
|
||||
) -> bytearray:
|
||||
"""Decodes a single frame of RLE encoded data.
|
||||
|
||||
Each frame may contain up to 15 segments of encoded data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes
|
||||
The RLE frame data
|
||||
rows : int
|
||||
The number of output rows
|
||||
columns : int
|
||||
The number of output columns
|
||||
nr_samples : int
|
||||
Number of samples per pixel (e.g. 3 for RGB data).
|
||||
nr_bits : int
|
||||
Number of bits per sample - must be a multiple of 8
|
||||
segment_order : str
|
||||
The segment order of the `data`, '>' for big endian (default),
|
||||
'<' for little endian (non-conformant).
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytearray
|
||||
The frame's decoded data in little endian and planar configuration 1
|
||||
byte ordering (i.e. for RGB data this is all red pixels then all
|
||||
green then all blue, with the bytes for each pixel ordered from
|
||||
MSB to LSB when reading left to right).
|
||||
"""
|
||||
if nr_bits % 8:
|
||||
raise NotImplementedError(
|
||||
f"Unable to decode RLE encoded pixel data with {nr_bits} bits allocated"
|
||||
)
|
||||
|
||||
# Parse the RLE Header
|
||||
offsets = _rle_parse_header(src[:64])
|
||||
nr_segments = len(offsets)
|
||||
|
||||
# Check that the actual number of segments is as expected
|
||||
bytes_per_sample = nr_bits // 8
|
||||
if nr_segments != nr_samples * bytes_per_sample:
|
||||
raise ValueError(
|
||||
"The number of RLE segments in the pixel data doesn't match the "
|
||||
f"expected amount ({nr_segments} vs. {nr_samples * bytes_per_sample} "
|
||||
"segments)"
|
||||
)
|
||||
|
||||
# Ensure the last segment gets decoded
|
||||
offsets.append(len(src))
|
||||
|
||||
# Preallocate with null bytes
|
||||
decoded = bytearray(rows * columns * nr_samples * bytes_per_sample)
|
||||
|
||||
# Example:
|
||||
# RLE encoded data is ordered like this (for 16-bit, 3 sample):
|
||||
# Segment: 0 | 1 | 2 | 3 | 4 | 5
|
||||
# R MSB | R LSB | G MSB | G LSB | B MSB | B LSB
|
||||
# A segment contains only the MSB or LSB parts of all the sample pixels
|
||||
|
||||
# To minimise the amount of array manipulation later, and to make things
|
||||
# faster we interleave each segment in a manner consistent with a planar
|
||||
# configuration of 1 (and use little endian byte ordering):
|
||||
# All red samples | All green samples | All blue
|
||||
# Pxl 1 Pxl 2 ... Pxl N | Pxl 1 Pxl 2 ... Pxl N | ...
|
||||
# LSB MSB LSB MSB ... LSB MSB | LSB MSB LSB MSB ... LSB MSB | ...
|
||||
|
||||
# `stride` is the total number of bytes of each sample plane
|
||||
stride = bytes_per_sample * rows * columns
|
||||
for sample_number in range(nr_samples):
|
||||
le_gen = range(bytes_per_sample)
|
||||
byte_offsets = le_gen if segment_order == "<" else reversed(le_gen)
|
||||
for byte_offset in byte_offsets:
|
||||
# Decode the segment
|
||||
ii = sample_number * bytes_per_sample + byte_offset
|
||||
# ii is 1, 0, 3, 2, 5, 4 for the example above
|
||||
# This is where the segment order correction occurs
|
||||
segment = _rle_decode_segment(src[offsets[ii] : offsets[ii + 1]])
|
||||
|
||||
# Check that the number of decoded bytes is correct
|
||||
actual_length = len(segment)
|
||||
if actual_length < rows * columns:
|
||||
raise ValueError(
|
||||
"The amount of decoded RLE segment data doesn't match the "
|
||||
f"expected amount ({actual_length} vs. {rows * columns} bytes)"
|
||||
)
|
||||
elif actual_length != rows * columns:
|
||||
warn_and_log(
|
||||
"The decoded RLE segment contains non-conformant padding "
|
||||
f"- {actual_length} vs. {rows * columns} bytes expected"
|
||||
)
|
||||
|
||||
if segment_order == ">":
|
||||
byte_offset = bytes_per_sample - byte_offset - 1
|
||||
|
||||
# For 100 pixel/plane, 32-bit, 3 sample data, `start` will be
|
||||
# 0, 1, 2, 3, 400, 401, 402, 403, 800, 801, 802, 803
|
||||
start = byte_offset + (sample_number * stride)
|
||||
decoded[start : start + stride : bytes_per_sample] = segment[
|
||||
: rows * columns
|
||||
]
|
||||
|
||||
return decoded
|
||||
|
||||
|
||||
def _rle_decode_segment(src: bytes) -> bytearray:
|
||||
"""Return a single segment of decoded RLE data as bytearray.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
buffer : bytes
|
||||
The segment data to be decoded.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytearray
|
||||
The decoded segment.
|
||||
"""
|
||||
result = bytearray()
|
||||
pos = 0
|
||||
result_extend = result.extend
|
||||
|
||||
try:
|
||||
while True:
|
||||
# header_byte is N + 1
|
||||
header_byte = src[pos] + 1
|
||||
pos += 1
|
||||
if header_byte > 129:
|
||||
# Extend by copying the next byte (-N + 1) times
|
||||
# however since using uint8 instead of int8 this will be
|
||||
# (256 - N + 1) times
|
||||
result_extend(src[pos : pos + 1] * (258 - header_byte))
|
||||
pos += 1
|
||||
elif header_byte < 129:
|
||||
# Extend by literally copying the next (N + 1) bytes
|
||||
result_extend(src[pos : pos + header_byte])
|
||||
pos += header_byte
|
||||
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _rle_parse_header(header: bytes) -> list[int]:
|
||||
"""Return a list of byte offsets for the segments in RLE data.
|
||||
|
||||
**RLE Header Format**
|
||||
|
||||
The RLE Header contains the number of segments for the image and the
|
||||
starting offset of each segment. Each of these numbers is represented as
|
||||
an unsigned long stored in little-endian. The RLE Header is 16 long words
|
||||
in length (i.e. 64 bytes) which allows it to describe a compressed image
|
||||
with up to 15 segments. All unused segment offsets shall be set to zero.
|
||||
|
||||
As an example, the table below describes an RLE Header with 3 segments as
|
||||
would typically be used with 8-bit RGB or YCbCr data (with 1 segment per
|
||||
channel).
|
||||
|
||||
+--------------+---------------------------------+------------+
|
||||
| Byte offset | Description | Value |
|
||||
+==============+=================================+============+
|
||||
| 0 | Number of segments | 3 |
|
||||
+--------------+---------------------------------+------------+
|
||||
| 4 | Offset of segment 1, N bytes | 64 |
|
||||
+--------------+---------------------------------+------------+
|
||||
| 8 | Offset of segment 2, M bytes | 64 + N |
|
||||
+--------------+---------------------------------+------------+
|
||||
| 12 | Offset of segment 3 | 64 + N + M |
|
||||
+--------------+---------------------------------+------------+
|
||||
| 16 | Offset of segment 4 (not used) | 0 |
|
||||
+--------------+---------------------------------+------------+
|
||||
| ... | ... | 0 |
|
||||
+--------------+---------------------------------+------------+
|
||||
| 60 | Offset of segment 15 (not used) | 0 |
|
||||
+--------------+---------------------------------+------------+
|
||||
|
||||
Parameters
|
||||
----------
|
||||
header : bytes
|
||||
The RLE header data (i.e. the first 64 bytes of an RLE frame).
|
||||
|
||||
Returns
|
||||
-------
|
||||
list of int
|
||||
The byte offsets for each segment in the RLE data.
|
||||
|
||||
Raises
|
||||
------
|
||||
ValueError
|
||||
If there are more than 15 segments or if the header is not 64 bytes
|
||||
long.
|
||||
|
||||
References
|
||||
----------
|
||||
DICOM Standard, Part 5, :dcm:`Annex G<part05/chapter_G.html>`
|
||||
"""
|
||||
if len(header) != 64:
|
||||
raise ValueError("The RLE header can only be 64 bytes long")
|
||||
|
||||
nr_segments = unpack("<L", header[:4])[0]
|
||||
if nr_segments > 15:
|
||||
raise ValueError(
|
||||
f"The RLE header specifies an invalid number of segments ({nr_segments})"
|
||||
)
|
||||
|
||||
return list(unpack(f"<{nr_segments}L", header[4 : 4 * (nr_segments + 1)]))
|
||||
9
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__init__.py
vendored
Executable file
9
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__init__.py
vendored
Executable file
@@ -0,0 +1,9 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
|
||||
from pydicom.pixels.encoders.base import (
|
||||
RLELosslessEncoder,
|
||||
JPEGLSLosslessEncoder,
|
||||
JPEGLSNearLosslessEncoder,
|
||||
JPEG2000LosslessEncoder,
|
||||
JPEG2000Encoder,
|
||||
)
|
||||
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/__init__.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/__init__.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/base.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/base.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/gdcm.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/gdcm.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/native.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/native.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/pyjpegls.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/pyjpegls.cpython-313.pyc
vendored
Executable file
Binary file not shown.
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/pylibjpeg.cpython-313.pyc
vendored
Executable file
BIN
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/__pycache__/pylibjpeg.cpython-313.pyc
vendored
Executable file
Binary file not shown.
874
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/base.py
vendored
Executable file
874
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/base.py
vendored
Executable file
@@ -0,0 +1,874 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Pixel data encoding."""
|
||||
|
||||
from collections.abc import Callable, Iterator, Iterable
|
||||
import logging
|
||||
import math
|
||||
import sys
|
||||
from typing import Any, cast, TYPE_CHECKING
|
||||
|
||||
try:
|
||||
import numpy as np
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
from pydicom import config
|
||||
from pydicom.pixels.common import Buffer, RunnerBase, CoderBase, RunnerOptions
|
||||
from pydicom.uid import (
|
||||
UID,
|
||||
JPEGBaseline8Bit,
|
||||
JPEGExtended12Bit,
|
||||
JPEGLossless,
|
||||
JPEGLosslessSV1,
|
||||
JPEGLSLossless,
|
||||
JPEGLSNearLossless,
|
||||
JPEG2000Lossless,
|
||||
JPEG2000,
|
||||
RLELossless,
|
||||
JPEGLSTransferSyntaxes,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from pydicom.dataset import Dataset
|
||||
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
EncodeFunction = Callable[[bytes, "EncodeRunner"], bytes | bytearray]
|
||||
|
||||
|
||||
class EncodeOptions(RunnerOptions, total=False):
|
||||
"""Options accepted by EncodeRunner and encoding plugins"""
|
||||
|
||||
# The byte order used by the raw pixel data sent to the encoder
|
||||
# ">" for big endian, "<" for little endian (default)
|
||||
byteorder: str
|
||||
|
||||
## Transfer Syntax specific options
|
||||
# JPEG-LS Near Lossless
|
||||
# The maximum allowable error in *unsigned* pixel intensity
|
||||
jls_error: int
|
||||
|
||||
# JPEG 2000
|
||||
# Either j2k_cr or j2k_psnr is required
|
||||
# The compression ratio for each quality layer, should be in decreasing order
|
||||
j2k_cr: list[float]
|
||||
# The peak signal-to-noise ratio for each layer, should be in increasing order
|
||||
j2k_psnr: list[float]
|
||||
|
||||
|
||||
class EncodeRunner(RunnerBase):
|
||||
"""Class for managing the pixel data encoding process.
|
||||
|
||||
.. versionadded:: 3.0
|
||||
|
||||
This class is not intended to be used directly. For encoding pixel data
|
||||
use the :class:`~pydicom.pixels.encoders.base.Encoder` instance
|
||||
corresponding to the transfer syntax of the pixel data.
|
||||
"""
|
||||
|
||||
def __init__(self, tsyntax: UID) -> None:
|
||||
"""Create a new runner for encoding data as `tsyntax`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
tsyntax : pydicom.uid.UID
|
||||
The transfer syntax UID corresponding to the pixel data to be
|
||||
encoded.
|
||||
"""
|
||||
self._src: Buffer | np.ndarray
|
||||
self._src_type: str
|
||||
self._opts: EncodeOptions = {
|
||||
"transfer_syntax_uid": tsyntax,
|
||||
"byteorder": "<",
|
||||
"pixel_keyword": "PixelData",
|
||||
}
|
||||
self._undeletable = ("transfer_syntax_uid", "pixel_keyword", "byteorder")
|
||||
self._encoders: dict[str, EncodeFunction] = {}
|
||||
|
||||
def encode(self, index: int | None) -> bytes:
|
||||
"""Return an encoded frame of pixel data as :class:`bytes`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
index : int | None
|
||||
If `index` is ``None`` then the pixel data only contains one frame,
|
||||
otherwise `index` is the frame number to be encoded.
|
||||
|
||||
Returns
|
||||
------
|
||||
bytes
|
||||
The encoded pixel data frame.
|
||||
"""
|
||||
failure_messages = []
|
||||
for name, func in self._encoders.items():
|
||||
try:
|
||||
return func(self.get_frame(index), self)
|
||||
except Exception as exc:
|
||||
LOGGER.exception(exc)
|
||||
failure_messages.append(f"{name}: {exc}")
|
||||
|
||||
messages = "\n ".join(failure_messages)
|
||||
raise RuntimeError(
|
||||
"Unable to encode as exceptions were raised by all available "
|
||||
f"plugins:\n {messages}"
|
||||
)
|
||||
|
||||
def get_frame(self, index: int | None) -> bytes:
|
||||
"""Return a frame's worth of uncompressed pixel data as :class:`bytes`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
index : int | None
|
||||
If the pixel data only has one from then use ``None``, otherwise
|
||||
`index` is the index of the frame to be returned.
|
||||
"""
|
||||
if self.is_array:
|
||||
return self._get_frame_array(index)
|
||||
|
||||
frame = self._get_frame_buffer(index)
|
||||
return bytes(frame) if not isinstance(frame, bytes) else frame
|
||||
|
||||
def _get_frame_array(self, index: int | None) -> bytes:
|
||||
"""Return a frame's worth of uncompressed pixel data from an ndarray."""
|
||||
# Grab the frame so that subsequent array manipulations minimize
|
||||
# the memory usage
|
||||
arr = cast(np.ndarray, self.src[index] if index is not None else self.src)
|
||||
|
||||
# The ndarray containing the pixel data may use a larger container
|
||||
# than is strictly needed: e.g. 32 bits allocated with 7 bits stored.
|
||||
# However the encoders expect data to be sized appropriately
|
||||
# for the sample precision, so we may need to downscale
|
||||
if self.bits_stored <= 8:
|
||||
itemsize = 1
|
||||
elif 8 < self.bits_stored <= 16:
|
||||
itemsize = 2
|
||||
elif 16 < self.bits_stored <= 32:
|
||||
itemsize = 4
|
||||
elif 32 < self.bits_stored <= 64:
|
||||
itemsize = 8
|
||||
|
||||
if arr.dtype.itemsize != itemsize:
|
||||
arr = arr.astype(f"{arr.dtype.kind}{itemsize}")
|
||||
|
||||
# JPEG-LS allows different ordering of the input image data via the
|
||||
# interleave mode (ILV) parameter. ILV 0 matches a planar configuration
|
||||
# of 1 and requires shape (samples, rows, columns). ILV 1 and 2 match
|
||||
# planar configuration 0 so no further action is needed
|
||||
if (
|
||||
self.transfer_syntax in JPEGLSTransferSyntaxes
|
||||
and self.samples_per_pixel == 3
|
||||
and self.planar_configuration == 1
|
||||
):
|
||||
arr = arr.transpose(2, 0, 1)
|
||||
|
||||
return cast(bytes, arr.tobytes())
|
||||
|
||||
def _get_frame_buffer(self, index: int | None) -> bytes | bytearray:
|
||||
"""Return a frame's worth of uncompressed pixel data from buffer-like."""
|
||||
# The encoded pixel data may use a larger container than is strictly
|
||||
# needed: e.g. 32 bits allocated with 7 bits stored
|
||||
# However the encoders typically expect data to be sized appropriately
|
||||
# for the sample precision, so we need to downscale to:
|
||||
# 0 < precision <= 8: an 8-bit container (char)
|
||||
# 8 < precision <= 16: a 16-bit container (short)
|
||||
# 16 < precision <= 32: a 32-bit container (int/long)
|
||||
# 32 < precision <= 64: a 64-bit container (long long)
|
||||
bytes_per_frame = cast(int, self.frame_length(unit="bytes"))
|
||||
start = 0 if index is None else index * bytes_per_frame
|
||||
src = cast(bytes, self.src[start : start + bytes_per_frame])
|
||||
|
||||
# Resize the data to fit the appropriate container
|
||||
expected_length = cast(int, self.frame_length(unit="pixels"))
|
||||
bytes_per_pixel = len(src) // expected_length
|
||||
|
||||
# 1 byte/px actual
|
||||
if self.bits_stored <= 8:
|
||||
# If not 1 byte/px then must be 2, 3, 4, 5, 6, 7 or 8
|
||||
# but only the first byte is relevant
|
||||
return src if bytes_per_pixel == 1 else src[::bytes_per_pixel]
|
||||
|
||||
# 2 bytes/px actual
|
||||
if 8 < self.bits_stored <= 16:
|
||||
if bytes_per_pixel == 2:
|
||||
return src
|
||||
|
||||
# If not 2 bytes/px then must be 3, 4, 5, 6, 7 or 8
|
||||
# but only the first 2 bytes are relevant
|
||||
out = bytearray(expected_length * 2)
|
||||
out[::2] = src[::bytes_per_pixel]
|
||||
out[1::2] = src[1::bytes_per_pixel]
|
||||
return out
|
||||
|
||||
# 3 or 4 bytes/px actual
|
||||
if 16 < self.bits_stored <= 32:
|
||||
if bytes_per_pixel == 4:
|
||||
return src
|
||||
|
||||
# If not 4 bytes/px then must be 3, 5, 6, 7 or 8
|
||||
# but only the first 3 or 4 bytes are relevant
|
||||
out = bytearray(expected_length * 4)
|
||||
out[::4] = src[::bytes_per_pixel]
|
||||
out[1::4] = src[1::bytes_per_pixel]
|
||||
out[2::4] = src[2::bytes_per_pixel]
|
||||
if bytes_per_pixel > 3:
|
||||
out[3::4] = src[3::bytes_per_pixel]
|
||||
|
||||
return out
|
||||
|
||||
# 32 < bits_stored <= 64 (maximum allowed)
|
||||
# 5, 6, 7 or 8 bytes/px actual
|
||||
if bytes_per_pixel == 8:
|
||||
return src
|
||||
|
||||
# If not 8 bytes/px then must be 5, 6 or 7
|
||||
out = bytearray(expected_length * 8)
|
||||
out[::8] = src[::bytes_per_pixel]
|
||||
out[1::8] = src[1::bytes_per_pixel]
|
||||
out[2::8] = src[2::bytes_per_pixel]
|
||||
out[3::8] = src[3::bytes_per_pixel]
|
||||
out[4::8] = src[4::bytes_per_pixel]
|
||||
if bytes_per_pixel == 5:
|
||||
return out
|
||||
|
||||
out[5::8] = src[5::bytes_per_pixel]
|
||||
if bytes_per_pixel == 6:
|
||||
return out
|
||||
|
||||
# 7 bytes/px
|
||||
out[6::8] = src[6::bytes_per_pixel]
|
||||
return out
|
||||
|
||||
def set_encoders(self, encoders: dict[str, EncodeFunction]) -> None:
|
||||
"""Set the encoders use for encoding compressed pixel data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
encoders : dict[str, EncodeFunction]
|
||||
A dict of {name: encoder function}.
|
||||
"""
|
||||
self._encoders = encoders
|
||||
|
||||
def set_source(self, src: "np.ndarray | Dataset | Buffer") -> None:
|
||||
"""Set the pixel data to be encoded.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes | bytearray | memoryview | pydicom.dataset.Dataset | numpy.ndarray
|
||||
|
||||
* If a buffer-like then the encoded pixel data
|
||||
* If a :class:`~pydicom.dataset.Dataset` then a dataset containing
|
||||
the pixel data and associated group ``0x0028`` elements.
|
||||
* If a :class:`numpy.ndarray` then an array containing the image data.
|
||||
"""
|
||||
from pydicom.dataset import Dataset
|
||||
|
||||
if isinstance(src, Dataset):
|
||||
self._set_options_ds(src)
|
||||
self._src = src.PixelData
|
||||
self._src_type = "Dataset"
|
||||
elif isinstance(src, (bytes | bytearray | memoryview)):
|
||||
self._src = src
|
||||
self._src_type = "Buffer"
|
||||
elif isinstance(src, np.ndarray):
|
||||
# Ensure the array is in the required byte order (little-endian)
|
||||
sys_endianness = "<" if sys.byteorder == "little" else ">"
|
||||
# `byteorder` may be
|
||||
# '|': none available, such as for 8 bit -> ignore
|
||||
# '=': native system endianness -> change to '<' or '>'
|
||||
# '<' or '>': little or big
|
||||
byteorder = src.dtype.byteorder
|
||||
byteorder = sys_endianness if byteorder == "=" else byteorder
|
||||
if byteorder == ">":
|
||||
src = src.astype(src.dtype.newbyteorder("<"))
|
||||
|
||||
self._src = src
|
||||
self._src_type = "Array"
|
||||
else:
|
||||
raise TypeError(
|
||||
"'src' must be bytes, numpy.ndarray or pydicom.dataset.Dataset, "
|
||||
f"not '{src.__class__.__name__}'"
|
||||
)
|
||||
|
||||
@property
|
||||
def src(self) -> "Buffer | np.ndarray":
|
||||
"""Return the buffer-like or :class:`numpy.ndarray` containing the pixel data."""
|
||||
return self._src
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Return nice string output for the runner."""
|
||||
s = [f"EncodeRunner for '{self.transfer_syntax.name}'"]
|
||||
s.append("Options")
|
||||
s.extend([f" {name}: {value}" for name, value in self.options.items()])
|
||||
if self._encoders:
|
||||
s.append("Encoders")
|
||||
s.extend([f" {name}" for name in self._encoders])
|
||||
|
||||
return "\n".join(s)
|
||||
|
||||
def validate(self) -> None:
|
||||
"""Validate the encoding options and source pixel data."""
|
||||
self._validate_options()
|
||||
if self.is_dataset or self.is_buffer:
|
||||
self._validate_buffer()
|
||||
else:
|
||||
self._validate_array()
|
||||
|
||||
# UID specific validation based on Section 8 of Part 5
|
||||
self._validate_encoding_profile()
|
||||
|
||||
def _validate_array(self) -> None:
|
||||
"""Check that the ndarray matches the supplied options."""
|
||||
arr = cast(np.ndarray, self.src)
|
||||
shape = arr.shape
|
||||
dtype = arr.dtype
|
||||
|
||||
if len(shape) not in (2, 3, 4):
|
||||
raise ValueError(f"Unable to encode {len(shape)}D ndarrays")
|
||||
|
||||
# `arr` may be (for planar configuration 0):
|
||||
# (rows, columns)
|
||||
# (rows, columns, planes)
|
||||
# (frames, rows, columns)
|
||||
# (frames, rows, columns, planes)
|
||||
expected = [
|
||||
self.number_of_frames,
|
||||
self.rows,
|
||||
self.columns,
|
||||
self.samples_per_pixel,
|
||||
]
|
||||
expected = expected[1:] if expected[0] == 1 else expected
|
||||
expected = expected[:-1] if expected[-1] in (None, 1) else expected
|
||||
|
||||
if shape != tuple(expected):
|
||||
raise ValueError(
|
||||
f"Mismatch between the expected ndarray shape {tuple(expected)} "
|
||||
f"and the actual shape {shape}"
|
||||
)
|
||||
|
||||
# Check dtype is int or uint
|
||||
ui = [
|
||||
np.issubdtype(dtype, np.unsignedinteger),
|
||||
np.issubdtype(dtype, np.signedinteger),
|
||||
]
|
||||
if not any(ui):
|
||||
raise ValueError(
|
||||
f"The ndarray's dtype '{dtype}' is not supported, must be a "
|
||||
"signed or unsigned integer type"
|
||||
)
|
||||
|
||||
# Check dtype is consistent with the *Pixel Representation*
|
||||
is_signed = self.pixel_representation == 1
|
||||
if not ui[is_signed]:
|
||||
s = ["unsigned", "signed"][is_signed]
|
||||
raise ValueError(
|
||||
f"The ndarray's dtype '{dtype}' is not consistent with a (0028,0103) "
|
||||
f"'Pixel Representation' of '{self.pixel_representation}' ({s} integers)"
|
||||
)
|
||||
|
||||
# Check the dtype's itemsize is at least as large as *Bits Allocated*
|
||||
if dtype.itemsize < math.ceil(self.bits_allocated / 8):
|
||||
raise ValueError(
|
||||
f"The ndarray's dtype '{dtype}' is not consistent with a (0028,0100) "
|
||||
f"'Bits Allocated' value of '{self.bits_allocated}'"
|
||||
)
|
||||
|
||||
# Check the pixel data values are consistent with *Bits Stored*
|
||||
amax, amin = arr.max(), arr.min()
|
||||
if is_signed:
|
||||
minimum = -(2 ** (self.bits_stored - 1))
|
||||
maximum = 2 ** (self.bits_stored - 1) - 1
|
||||
else:
|
||||
minimum, maximum = 0, 2**self.bits_stored - 1
|
||||
|
||||
if amax > maximum or amin < minimum:
|
||||
raise ValueError(
|
||||
"The ndarray contains values that are outside the allowable "
|
||||
f"range of ({minimum}, {maximum}) for a (0028,0101) 'Bits "
|
||||
f"Stored' value of '{self.bits_stored}'"
|
||||
)
|
||||
|
||||
if self.transfer_syntax == JPEGLSNearLossless and is_signed:
|
||||
# JPEG-LS doesn't track signedness, so with lossy encoding of
|
||||
# signed data it's possible to flip from a negative to a positive
|
||||
# value (or vice versa) due to the introduced error.
|
||||
# The only way to avoid this is to limit pixel values to the
|
||||
# range (minimum + jls_error, maximum - jls_error), where
|
||||
# `jls_error` is the JPEG-LS 'NEAR' parameter and `minimum`
|
||||
# and `maximum` are the min/max possible values for a given
|
||||
# sample precision
|
||||
error = self.get_option("jls_error", 0)
|
||||
within = amax <= (maximum - error) and amin >= (minimum + error)
|
||||
if error and not within:
|
||||
raise ValueError(
|
||||
"The supported range of pixel values when performing lossy "
|
||||
"JPEG-LS encoding of signed integers with a (0028,0103) 'Bits "
|
||||
f"Stored' value of '{self.bits_stored}' and a 'jls_error' "
|
||||
f"of '{error}' is ({minimum + error}, {maximum - error})"
|
||||
)
|
||||
|
||||
def _validate_buffer(self) -> None:
|
||||
"""Validate the supplied pixel data buffer."""
|
||||
# Check the length is at least as long as required
|
||||
length_bytes = self.frame_length(unit="bytes")
|
||||
expected = length_bytes * self.number_of_frames
|
||||
if (actual := len(self.src)) < expected:
|
||||
raise ValueError(
|
||||
"The length of the uncompressed pixel data doesn't match the "
|
||||
f"expected length - {actual} bytes actual vs. {expected} expected"
|
||||
)
|
||||
|
||||
def _validate_encoding_profile(self) -> None:
|
||||
"""Perform UID specific validation of encoding parameters based on
|
||||
Part 5, Section 8 of the DICOM Standard.
|
||||
|
||||
Encoding profiles should be:
|
||||
|
||||
Tuple[str, int, Iterable[int], Iterable[int], Iterable[int]] as
|
||||
(
|
||||
PhotometricInterpretation, SamplesPerPixel, PixelRepresentation,
|
||||
BitsAllocated, BitsStored
|
||||
)
|
||||
"""
|
||||
if self.transfer_syntax not in ENCODING_PROFILES:
|
||||
return
|
||||
|
||||
# Test each profile and see if it matches source parameters
|
||||
profile = ENCODING_PROFILES[self.transfer_syntax]
|
||||
for pi, spp, px_repr, bits_a, bits_s in profile:
|
||||
try:
|
||||
assert self.photometric_interpretation == pi
|
||||
assert self.samples_per_pixel == spp
|
||||
assert self.pixel_representation in px_repr
|
||||
assert self.bits_allocated in bits_a
|
||||
assert self.bits_stored in bits_s
|
||||
except AssertionError:
|
||||
continue
|
||||
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
"One or more of the following values is not valid for pixel data "
|
||||
f"encoded with '{self.transfer_syntax.name}':\n"
|
||||
f" (0028,0002) Samples per Pixel: {self.samples_per_pixel}\n"
|
||||
" (0028,0006) Photometric Interpretation: "
|
||||
f"{self.photometric_interpretation}\n"
|
||||
f" (0028,0100) Bits Allocated: {self.bits_allocated}\n"
|
||||
f" (0028,0101) Bits Stored: {self.bits_stored}\n"
|
||||
f" (0028,0103) Pixel Representation: {self.pixel_representation}\n"
|
||||
"See Part 5, Section 8.2 of the DICOM Standard for more information"
|
||||
)
|
||||
|
||||
|
||||
class Encoder(CoderBase):
|
||||
"""Factory class for data encoders.
|
||||
|
||||
Every available ``Encoder`` instance in *pydicom* corresponds directly
|
||||
to a single DICOM *Transfer Syntax UID*, and provides a mechanism for
|
||||
converting raw unencoded source data to meet the requirements of that
|
||||
transfer syntax using one or more :doc:`encoding plugins
|
||||
</guides/encoding/encoder_plugins>`.
|
||||
|
||||
.. versionadded:: 2.2
|
||||
"""
|
||||
|
||||
def __init__(self, uid: UID) -> None:
|
||||
"""Create a new data encoder.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
uid : pydicom.uid.UID
|
||||
The *Transfer Syntax UID* that the encoder supports.
|
||||
"""
|
||||
super().__init__(uid, decoder=False)
|
||||
|
||||
def encode(
|
||||
self,
|
||||
src: "bytes | np.ndarray | Dataset",
|
||||
*,
|
||||
index: int | None = None,
|
||||
validate: bool = True,
|
||||
encoding_plugin: str = "",
|
||||
**kwargs: Any,
|
||||
) -> bytes:
|
||||
"""Return an encoded frame of the pixel data in `src` as
|
||||
:class:`bytes`.
|
||||
|
||||
.. warning::
|
||||
|
||||
With the exception of *RLE Lossless*, this method requires the
|
||||
installation of additional packages to perform the actual pixel
|
||||
data encoding. See the :doc:`encoding documentation
|
||||
</guides/user/image_data_compression>` for more information.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes, numpy.ndarray or pydicom.dataset.Dataset
|
||||
Single or multi-frame pixel data as one of the following:
|
||||
|
||||
* :class:`~numpy.ndarray`: the uncompressed pixel data, should be
|
||||
:attr:`shaped<numpy.ndarray.shape>` as:
|
||||
|
||||
* (rows, columns) for single frame, single sample data.
|
||||
* (rows, columns, planes) for single frame, multi-sample data.
|
||||
* (frames, rows, columns) for multi-frame, single sample data.
|
||||
* (frames, rows, columns, planes) for multi-frame and
|
||||
multi-sample data.
|
||||
|
||||
* :class:`~pydicom.dataset.Dataset`: the dataset containing
|
||||
the uncompressed *Pixel Data* to be encoded.
|
||||
* :class:`bytes`: the uncompressed little-endian ordered pixel
|
||||
data. `src` should use 1, 2, 4 or 8 bytes per pixel, whichever
|
||||
of these is sufficient for the (0028,0103) *Bits Stored* value.
|
||||
index : int, optional
|
||||
Required when `src` contains multiple frames, this is the index
|
||||
of the frame to be encoded.
|
||||
validate : bool, optional
|
||||
If ``True`` (default) then validate the supplied encoding options
|
||||
and pixel data prior to encoding, otherwise if ``False`` no
|
||||
validation will be performed.
|
||||
encoding_plugin : str, optional
|
||||
The name of the pixel data encoding plugin to use. If
|
||||
`encoding_plugin` is not specified then all available
|
||||
plugins will be tried (default). For information on the available
|
||||
plugins for each encoder see the
|
||||
:mod:`API documentation<pydicom.pixels.encoders>`.
|
||||
**kwargs
|
||||
The following keyword parameters are required when `src` is
|
||||
:class:`bytes` or :class:`~numpy.ndarray`:
|
||||
|
||||
* ``'rows'``: :class:`int` - the number of rows of pixels in `src`,
|
||||
maximum 65535.
|
||||
* ``'columns'``: :class:`int` - the number of columns of pixels in
|
||||
`src`, maximum 65535.
|
||||
* ``'number_of_frames'``: :class:`int` - the number of frames
|
||||
in `src`.
|
||||
* ``'samples_per_pixel'``: :class:`int` - the number of samples
|
||||
per pixel in `src`, should be 1 or 3.
|
||||
* ``'bits_allocated'``: :class:`int` - the number of bits used
|
||||
to contain each pixel, should be a multiple of 8.
|
||||
* ``'bits_stored'``: :class:`int` - the number of bits actually
|
||||
used per pixel. For example, an ``ndarray`` `src` might have a
|
||||
:class:`~numpy.dtype` of ``'uint16'`` (range 0 to 65535) but
|
||||
only contain 12-bit pixel values (range 0 to 4095).
|
||||
* ``'pixel_representation'``: :class:`int` - the type of data
|
||||
being encoded, ``0`` for unsigned, ``1`` for 2's complement
|
||||
(signed)
|
||||
* ``'photometric_interpretation'``: :class:`str` - the intended
|
||||
color space of the *encoded* pixel data, such as ``'YBR_FULL'``.
|
||||
|
||||
Optional keyword parameters for the encoding plugin may also be
|
||||
present. See the :doc:`encoding plugin options
|
||||
</guides/encoding/encoder_plugin_options>` for more information.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The encoded pixel data.
|
||||
"""
|
||||
if index is not None and index < 0:
|
||||
raise ValueError("'index' must be greater than or equal to 0")
|
||||
|
||||
runner = EncodeRunner(self.UID)
|
||||
runner.set_source(src)
|
||||
runner.set_options(**kwargs)
|
||||
runner.set_encoders(
|
||||
cast(
|
||||
dict[str, "EncodeFunction"],
|
||||
self._validate_plugins(encoding_plugin),
|
||||
),
|
||||
)
|
||||
|
||||
if config.debugging:
|
||||
LOGGER.debug(runner)
|
||||
|
||||
if validate:
|
||||
runner.validate()
|
||||
|
||||
if runner.number_of_frames > 1 and index is None:
|
||||
raise ValueError(
|
||||
"The 'index' of the frame to be encoded is required for "
|
||||
"multi-frame pixel data"
|
||||
)
|
||||
|
||||
return runner.encode(index)
|
||||
|
||||
def iter_encode(
|
||||
self,
|
||||
src: "bytes | np.ndarray | Dataset",
|
||||
*,
|
||||
validate: bool = True,
|
||||
encoding_plugin: str = "",
|
||||
**kwargs: Any,
|
||||
) -> Iterator[bytes]:
|
||||
"""Yield encoded frames of the pixel data in `src` as :class:`bytes`.
|
||||
|
||||
.. warning::
|
||||
|
||||
With the exception of *RLE Lossless*, this method requires the
|
||||
installation of additional packages to perform the actual pixel
|
||||
data encoding. See the :doc:`encoding documentation
|
||||
</guides/user/image_data_compression>` for more information.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes, numpy.ndarray or pydicom.dataset.Dataset
|
||||
Single or multi-frame pixel data as one of the following:
|
||||
|
||||
* :class:`~numpy.ndarray`: the uncompressed pixel data, should be
|
||||
:attr:`shaped<numpy.ndarray.shape>` as:
|
||||
|
||||
* (rows, columns) for single frame, single sample data.
|
||||
* (rows, columns, planes) for single frame, multi-sample data.
|
||||
* (frames, rows, columns) for multi-frame, single sample data.
|
||||
* (frames, rows, columns, planes) for multi-frame and
|
||||
multi-sample data.
|
||||
|
||||
* :class:`~pydicom.dataset.Dataset`: the dataset containing
|
||||
the uncompressed *Pixel Data* to be encoded.
|
||||
* :class:`bytes`: the uncompressed little-endian ordered pixel
|
||||
data. `src` should use 1, 2, 4 or 8 bytes per pixel, whichever
|
||||
of these is sufficient for the (0028,0103) *Bits Stored* value.
|
||||
validate : bool, optional
|
||||
If ``True`` (default) then validate the supplied encoding options
|
||||
and pixel data prior to encoding, otherwise if ``False`` no
|
||||
validation will be performed.
|
||||
encoding_plugin : str, optional
|
||||
The name of the pixel data encoding plugin to use. If
|
||||
`encoding_plugin` is not specified then all available
|
||||
plugins will be tried (default). For information on the available
|
||||
plugins for each encoder see the
|
||||
:mod:`API documentation<pydicom.pixels.encoders>`.
|
||||
**kwargs
|
||||
The following keyword parameters are required when `src` is
|
||||
:class:`bytes` or :class:`~numpy.ndarray`:
|
||||
|
||||
* ``'rows'``: :class:`int` - the number of rows of pixels in `src`,
|
||||
maximum 65535.
|
||||
* ``'columns'``: :class:`int` - the number of columns of pixels in
|
||||
`src`, maximum 65535.
|
||||
* ``'number_of_frames'``: :class:`int` - the number of frames
|
||||
in `src`.
|
||||
* ``'samples_per_pixel'``: :class:`int` - the number of samples
|
||||
per pixel in `src`, should be 1 or 3.
|
||||
* ``'bits_allocated'``: :class:`int` - the number of bits used
|
||||
to contain each pixel, should be a multiple of 8.
|
||||
* ``'bits_stored'``: :class:`int` - the number of bits actually
|
||||
used per pixel. For example, an ``ndarray`` `src` might have a
|
||||
:class:`~numpy.dtype` of ``'uint16'`` (range 0 to 65535) but
|
||||
only contain 12-bit pixel values (range 0 to 4095).
|
||||
* ``'pixel_representation'``: :class:`int` - the type of data
|
||||
being encoded, ``0`` for unsigned, ``1`` for 2's complement
|
||||
(signed)
|
||||
* ``'photometric_interpretation'``: :class:`str` - the intended
|
||||
color space of the encoded pixel data, such as ``'YBR_FULL'``.
|
||||
|
||||
Optional keyword parameters for the encoding plugin may also be
|
||||
present. See the :doc:`encoding plugin options
|
||||
</guides/encoding/encoder_plugin_options>` for more information.
|
||||
|
||||
Yields
|
||||
------
|
||||
bytes
|
||||
An encoded frame of pixel data.
|
||||
"""
|
||||
runner = EncodeRunner(self.UID)
|
||||
runner.set_source(src)
|
||||
runner.set_options(**kwargs)
|
||||
runner.set_encoders(
|
||||
cast(
|
||||
dict[str, "EncodeFunction"],
|
||||
self._validate_plugins(encoding_plugin),
|
||||
),
|
||||
)
|
||||
|
||||
if config.debugging:
|
||||
LOGGER.debug(runner)
|
||||
|
||||
if validate:
|
||||
runner.validate()
|
||||
|
||||
if runner.number_of_frames == 1:
|
||||
yield runner.encode(None)
|
||||
return
|
||||
|
||||
for index in range(runner.number_of_frames):
|
||||
yield runner.encode(index)
|
||||
|
||||
|
||||
# UID: [
|
||||
# Photometric Interpretation (the intended value *after* encoding),
|
||||
# Samples per Pixel,
|
||||
# Pixel Representation,
|
||||
# Bits Allocated,
|
||||
# Bits Stored,
|
||||
# ]
|
||||
ProfileType = tuple[str, int, Iterable[int], Iterable[int], Iterable[int]]
|
||||
ENCODING_PROFILES: dict[UID, list[ProfileType]] = {
|
||||
JPEGBaseline8Bit: [ # 1.2.840.10008.1.2.4.50: Table 8.2.1-1 in PS3.5
|
||||
("MONOCHROME1", 1, (0,), (8,), (8,)),
|
||||
("MONOCHROME2", 1, (0,), (8,), (8,)),
|
||||
("YBR_FULL_422", 3, (0,), (8,), (8,)),
|
||||
("RGB", 3, (0,), (8,), (8,)),
|
||||
],
|
||||
JPEGExtended12Bit: [ # 1.2.840.10008.1.2.4.51: Table 8.2.1-1 in PS3.5
|
||||
("MONOCHROME1", 1, (0,), (8,), (8,)),
|
||||
("MONOCHROME1", 1, (0,), (16,), (12,)),
|
||||
("MONOCHROME2", 1, (0,), (8,), (8,)),
|
||||
("MONOCHROME2", 1, (0,), (16,), (12,)),
|
||||
],
|
||||
JPEGLossless: [ # 1.2.840.10008.1.2.4.57: Table 8.2.1-2 in PS3.5
|
||||
("MONOCHROME1", 1, (0, 1), (8, 16), range(1, 17)),
|
||||
("MONOCHROME2", 1, (0, 1), (8, 16), range(1, 17)),
|
||||
("PALETTE COLOR", 1, (0,), (8, 16), range(1, 17)),
|
||||
("YBR_FULL", 3, (0,), (8, 16), range(1, 17)),
|
||||
("RGB", 3, (0,), (8, 16), range(1, 17)),
|
||||
],
|
||||
JPEGLosslessSV1: [ # 1.2.840.10008.1.2.4.70: Table 8.2.1-2 in PS3.5
|
||||
("MONOCHROME1", 1, (0, 1), (8, 16), range(1, 17)),
|
||||
("MONOCHROME2", 1, (0, 1), (8, 16), range(1, 17)),
|
||||
("PALETTE COLOR", 1, (0,), (8, 16), range(1, 17)),
|
||||
("YBR_FULL", 3, (0,), (8, 16), range(1, 17)),
|
||||
("RGB", 3, (0,), (8, 16), range(1, 17)),
|
||||
],
|
||||
JPEGLSLossless: [ # 1.2.840.10008.1.2.4.80: Table 8.2.3-1 in PS3.5
|
||||
("MONOCHROME1", 1, (0, 1), (8, 16), range(2, 17)),
|
||||
("MONOCHROME2", 1, (0, 1), (8, 16), range(2, 17)),
|
||||
("PALETTE COLOR", 1, (0,), (8, 16), range(2, 17)),
|
||||
("YBR_FULL", 3, (0,), (8,), range(2, 9)),
|
||||
("RGB", 3, (0,), (8, 16), range(2, 17)),
|
||||
],
|
||||
JPEGLSNearLossless: [ # 1.2.840.10008.1.2.4.81: Table 8.2.3-1 in PS3.5
|
||||
("MONOCHROME1", 1, (0, 1), (8, 16), range(2, 17)),
|
||||
("MONOCHROME2", 1, (0, 1), (8, 16), range(2, 17)),
|
||||
("YBR_FULL", 3, (0,), (8,), range(2, 9)),
|
||||
("RGB", 3, (0,), (8, 16), range(2, 17)),
|
||||
],
|
||||
JPEG2000Lossless: [ # 1.2.840.10008.1.2.4.90: Table 8.2.4-1 in PS3.5
|
||||
("MONOCHROME1", 1, (0, 1), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
("MONOCHROME2", 1, (0, 1), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
("PALETTE COLOR", 1, (0,), (8, 16), range(1, 17)),
|
||||
("YBR_RCT", 3, (0,), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
("RGB", 3, (0,), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
("YBR_FULL", 3, (0,), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
],
|
||||
JPEG2000: [ # 1.2.840.10008.1.2.4.91: Table 8.2.4-1 in PS3.5
|
||||
("MONOCHROME1", 1, (0, 1), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
("MONOCHROME2", 1, (0, 1), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
("YBR_ICT", 3, (0,), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
("RGB", 3, (0,), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
("YBR_FULL", 3, (0,), (8, 16, 24, 32, 40), range(1, 39)),
|
||||
],
|
||||
RLELossless: [ # 1.2.840.10008.1.2.5: Table 8.2.2-1 in PS3.5
|
||||
("MONOCHROME1", 1, (0, 1), (8, 16), range(1, 17)),
|
||||
("MONOCHROME2", 1, (0, 1), (8, 16), range(1, 17)),
|
||||
("PALETTE COLOR", 1, (0,), (8, 16), range(1, 17)),
|
||||
("YBR_FULL", 3, (0,), (8,), range(1, 9)),
|
||||
("RGB", 3, (0,), (8, 16), range(1, 17)),
|
||||
],
|
||||
}
|
||||
|
||||
# Encoder names should be f"{UID.keyword}Encoder"
|
||||
RLELosslessEncoder = Encoder(RLELossless)
|
||||
RLELosslessEncoder.add_plugins(
|
||||
[
|
||||
("gdcm", ("pydicom.pixels.encoders.gdcm", "encode_pixel_data")),
|
||||
("pylibjpeg", ("pydicom.pixels.encoders.pylibjpeg", "_encode_frame")),
|
||||
("pydicom", ("pydicom.pixels.encoders.native", "_encode_frame")),
|
||||
],
|
||||
)
|
||||
|
||||
JPEGLSLosslessEncoder = Encoder(JPEGLSLossless)
|
||||
JPEGLSLosslessEncoder.add_plugin(
|
||||
"pyjpegls", ("pydicom.pixels.encoders.pyjpegls", "_encode_frame")
|
||||
)
|
||||
|
||||
JPEGLSNearLosslessEncoder = Encoder(JPEGLSNearLossless)
|
||||
JPEGLSNearLosslessEncoder.add_plugin(
|
||||
"pyjpegls", ("pydicom.pixels.encoders.pyjpegls", "_encode_frame")
|
||||
)
|
||||
|
||||
JPEG2000LosslessEncoder = Encoder(JPEG2000Lossless)
|
||||
JPEG2000LosslessEncoder.add_plugin(
|
||||
"pylibjpeg", ("pydicom.pixels.encoders.pylibjpeg", "_encode_frame")
|
||||
)
|
||||
|
||||
JPEG2000Encoder = Encoder(JPEG2000)
|
||||
JPEG2000Encoder.add_plugin(
|
||||
"pylibjpeg", ("pydicom.pixels.encoders.pylibjpeg", "_encode_frame")
|
||||
)
|
||||
|
||||
|
||||
# Available pixel data encoders
|
||||
_PIXEL_DATA_ENCODERS = {
|
||||
# UID: (encoder, 'versionadded')
|
||||
RLELossless: (RLELosslessEncoder, "2.2"),
|
||||
JPEGLSLossless: (JPEGLSLosslessEncoder, "3.0"),
|
||||
JPEGLSNearLossless: (JPEGLSNearLosslessEncoder, "3.0"),
|
||||
JPEG2000Lossless: (JPEG2000LosslessEncoder, "3.0"),
|
||||
JPEG2000: (JPEG2000Encoder, "3.0"),
|
||||
}
|
||||
|
||||
|
||||
def _build_encoder_docstrings() -> None:
|
||||
"""Override the default Encoder docstring."""
|
||||
plugin_doc_links = {
|
||||
"pydicom": ":ref:`pydicom <encoder_plugin_pydicom>`",
|
||||
"pylibjpeg": ":ref:`pylibjpeg <encoder_plugin_pylibjpeg>`",
|
||||
"gdcm": ":ref:`gdcm <encoder_plugin_gdcm>`",
|
||||
"pyjpegls": ":ref:`pyjpegls <encoder_plugin_pyjpegls>`",
|
||||
}
|
||||
|
||||
for enc, versionadded in _PIXEL_DATA_ENCODERS.values():
|
||||
uid = enc.UID
|
||||
available = enc._available.keys()
|
||||
unavailable = enc._unavailable.keys()
|
||||
plugins = list(available) + list(unavailable)
|
||||
|
||||
plugins = [plugin_doc_links[name] for name in sorted(plugins)]
|
||||
|
||||
s = [f"A *Pixel Data* encoder for *{uid.name}* - ``{uid}``"]
|
||||
s.append("")
|
||||
s.append(f".. versionadded:: {versionadded}")
|
||||
s.append("")
|
||||
s.append(f"Encoding plugins: {', '.join(plugins)}")
|
||||
s.append("")
|
||||
s.append(
|
||||
"See the :class:`~pydicom.pixels.encoders.base.Encoder` "
|
||||
"reference for instance methods and attributes."
|
||||
)
|
||||
enc.__doc__ = "\n".join(s)
|
||||
|
||||
|
||||
_build_encoder_docstrings()
|
||||
|
||||
|
||||
def get_encoder(uid: str) -> Encoder:
|
||||
"""Return the pixel data encoder corresponding to `uid`.
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
+--------------------------------------------------+----------------+
|
||||
| Transfer Syntax | Version added |
|
||||
+-------------------------+------------------------+ +
|
||||
| Name | UID | |
|
||||
+=========================+========================+================+
|
||||
| *JPEG-LS Lossless* | 1.2.840.10008.1.2.4.80 | 3.0 |
|
||||
+-------------------------+------------------------+----------------+
|
||||
| *JPEG-LS Near Lossless* | 1.2.840.10008.1.2.4.81 | 3.0 |
|
||||
+-------------------------+------------------------+----------------+
|
||||
| *JPEG 2000 Lossless* | 1.2.840.10008.1.2.4.90 | 3.0 |
|
||||
+-------------------------+------------------------+----------------+
|
||||
| *JPEG 2000* | 1.2.840.10008.1.2.4.91 | 3.0 |
|
||||
+-------------------------+------------------------+----------------+
|
||||
| *RLE Lossless* | 1.2.840.10008.1.2.5 | 2.2 |
|
||||
+-------------------------+------------------------+----------------+
|
||||
"""
|
||||
uid = UID(uid)
|
||||
try:
|
||||
return _PIXEL_DATA_ENCODERS[uid][0]
|
||||
except KeyError:
|
||||
raise NotImplementedError(
|
||||
f"No pixel data encoders have been implemented for '{uid.name}'"
|
||||
)
|
||||
143
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/gdcm.py
vendored
Executable file
143
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/gdcm.py
vendored
Executable file
@@ -0,0 +1,143 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Interface for *Pixel Data* encoding, not intended to be used directly."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from pydicom.pixels.encoders.base import EncodeRunner
|
||||
from pydicom.uid import RLELossless
|
||||
|
||||
try:
|
||||
import gdcm
|
||||
|
||||
GDCM_VERSION = tuple(int(x) for x in gdcm.Version.GetVersion().split("."))
|
||||
HAVE_GDCM = True
|
||||
except ImportError:
|
||||
HAVE_GDCM = False
|
||||
|
||||
|
||||
ENCODER_DEPENDENCIES = {
|
||||
RLELossless: ("gdcm>=3.0.10",),
|
||||
}
|
||||
|
||||
|
||||
def is_available(uid: str) -> bool:
|
||||
"""Return ``True`` if a pixel data encoder for `uid` is available for use,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
if not HAVE_GDCM or GDCM_VERSION < (3, 0, 10):
|
||||
return False
|
||||
|
||||
return uid in ENCODER_DEPENDENCIES
|
||||
|
||||
|
||||
def encode_pixel_data(src: bytes, runner: EncodeRunner) -> bytes:
|
||||
"""Return the encoded image data in `src`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes
|
||||
The raw image frame data to be encoded.
|
||||
runner : pydicom.pixels.encoders.base.EncodeRunner
|
||||
The runner managing the encoding process.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The encoded image data.
|
||||
"""
|
||||
byteorder = runner.get_option("byteorder", "<")
|
||||
if byteorder == ">":
|
||||
raise ValueError("Unsupported option \"byteorder = '>'\"")
|
||||
|
||||
return _ENCODERS[runner.transfer_syntax](src, runner)
|
||||
|
||||
|
||||
def _rle_encode(src: bytes, runner: EncodeRunner) -> bytes:
|
||||
"""Return RLE encoded image data from `src`.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes
|
||||
The raw image frame data to be encoded.
|
||||
runner : pydicom.pixels.encoders.base.EncodeRunner
|
||||
The runner managing the encoding process.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The encoded image data.
|
||||
"""
|
||||
if runner.bits_allocated > 32:
|
||||
raise ValueError("Unable to encode more than 32-bit data")
|
||||
|
||||
# Create a gdcm.Image with the uncompressed `src` data
|
||||
pi = gdcm.PhotometricInterpretation.GetPIType(runner.photometric_interpretation)
|
||||
|
||||
# GDCM's null photometric interpretation gets used for invalid values
|
||||
if pi == gdcm.PhotometricInterpretation.PI_END:
|
||||
raise ValueError(
|
||||
f"Invalid photometric interpretation '{runner.photometric_interpretation}'"
|
||||
)
|
||||
|
||||
# `src` uses little-endian byte ordering
|
||||
ts = gdcm.TransferSyntax.ImplicitVRLittleEndian
|
||||
|
||||
# Must use ImageWriter().GetImage() to create a gdcmImage
|
||||
# also have to make sure `writer` doesn't go out of scope
|
||||
writer = gdcm.ImageWriter()
|
||||
image = writer.GetImage()
|
||||
image.SetNumberOfDimensions(2)
|
||||
image.SetDimensions((runner.columns, runner.rows, 1))
|
||||
image.SetPhotometricInterpretation(gdcm.PhotometricInterpretation(pi))
|
||||
image.SetTransferSyntax(gdcm.TransferSyntax(ts))
|
||||
|
||||
pixel_format = gdcm.PixelFormat(
|
||||
runner.samples_per_pixel,
|
||||
runner.bits_allocated,
|
||||
runner.bits_stored,
|
||||
runner.bits_stored - 1,
|
||||
runner.pixel_representation,
|
||||
)
|
||||
image.SetPixelFormat(pixel_format)
|
||||
if runner.samples_per_pixel > 1:
|
||||
# Default `src` is planar configuration 0 (i.e. R1 G1 B1 R2 G2 B2)
|
||||
image.SetPlanarConfiguration(0)
|
||||
|
||||
# Add the Pixel Data element and set the value to `src`
|
||||
elem = gdcm.DataElement(gdcm.Tag(0x7FE0, 0x0010))
|
||||
elem.SetByteStringValue(src)
|
||||
image.SetDataElement(elem)
|
||||
|
||||
# Converts an image to match the set transfer syntax
|
||||
converter = gdcm.ImageChangeTransferSyntax()
|
||||
|
||||
# Set up the converter with the intended transfer syntax...
|
||||
rle = gdcm.TransferSyntax.GetTSType(runner.transfer_syntax)
|
||||
converter.SetTransferSyntax(gdcm.TransferSyntax(rle))
|
||||
# ...and image to be converted
|
||||
converter.SetInput(image)
|
||||
|
||||
# Perform the conversion, returns bool
|
||||
# 'PALETTE COLOR' and a lossy transfer syntax will return False
|
||||
result = converter.Change()
|
||||
if not result:
|
||||
raise RuntimeError(
|
||||
"ImageChangeTransferSyntax.Change() returned a failure result"
|
||||
)
|
||||
|
||||
# A new gdcmImage with the converted pixel data element
|
||||
image = converter.GetOutput()
|
||||
|
||||
# The element's value is the encapsulated encoded pixel data
|
||||
seq = image.GetDataElement().GetSequenceOfFragments()
|
||||
|
||||
# RLECodec::Code() uses only 1 fragment per frame
|
||||
if seq is None or seq.GetNumberOfFragments() != 1:
|
||||
# Covers both no sequence and unexpected number of fragments
|
||||
raise RuntimeError("Unexpected number of fragments found in the 'Pixel Data'")
|
||||
|
||||
fragment = seq.GetFragment(0).GetByteValue().GetBuffer()
|
||||
return cast(bytes, fragment.encode("utf-8", "surrogateescape"))
|
||||
|
||||
|
||||
_ENCODERS = {RLELossless: _rle_encode}
|
||||
168
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/native.py
vendored
Executable file
168
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/native.py
vendored
Executable file
@@ -0,0 +1,168 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Interface for *Pixel Data* encoding, not intended to be used directly."""
|
||||
|
||||
from itertools import groupby
|
||||
import math
|
||||
from struct import pack
|
||||
|
||||
from pydicom.pixels.encoders.base import EncodeRunner
|
||||
from pydicom.uid import RLELossless
|
||||
|
||||
|
||||
ENCODER_DEPENDENCIES = {RLELossless: ()}
|
||||
|
||||
|
||||
def is_available(uid: str) -> bool:
|
||||
"""Return ``True`` if a pixel data encoder for `uid` is available for use,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
return True
|
||||
|
||||
|
||||
def _encode_frame(src: bytes, runner: EncodeRunner) -> bytes:
|
||||
"""Wrapper for use with the encoder interface.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes
|
||||
A single frame of little-endian ordered image data to be RLE encoded.
|
||||
runner : pydicom.pixels.encoders.base.EncodeRunner
|
||||
The runner managing the encoding process.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
An RLE encoded frame.
|
||||
"""
|
||||
if runner.get_option("byteorder", "<") == ">":
|
||||
raise ValueError("Unsupported option \"byteorder = '>'\"")
|
||||
|
||||
bytes_allocated = math.ceil(runner.bits_allocated / 8)
|
||||
|
||||
nr_segments = bytes_allocated * runner.samples_per_pixel
|
||||
if nr_segments > 15:
|
||||
raise ValueError(
|
||||
"Unable to encode as the DICOM standard only allows "
|
||||
"a maximum of 15 segments in RLE encoded data"
|
||||
)
|
||||
|
||||
rle_data = bytearray()
|
||||
seg_lengths = []
|
||||
|
||||
columns = runner.columns
|
||||
for sample_nr in range(runner.samples_per_pixel):
|
||||
for byte_offset in reversed(range(bytes_allocated)):
|
||||
idx = byte_offset + bytes_allocated * sample_nr
|
||||
segment = _encode_segment(src[idx::nr_segments], columns)
|
||||
rle_data.extend(segment)
|
||||
seg_lengths.append(len(segment))
|
||||
|
||||
# Add the number of segments to the header
|
||||
rle_header = bytearray(pack("<L", len(seg_lengths)))
|
||||
|
||||
# Add the segment offsets, starting at 64 for the first segment
|
||||
# We don't need an offset to any data at the end of the last segment
|
||||
offsets = [64]
|
||||
for ii, length in enumerate(seg_lengths[:-1]):
|
||||
offsets.append(offsets[ii] + length)
|
||||
rle_header.extend(pack(f"<{len(offsets)}L", *offsets))
|
||||
|
||||
# Add trailing padding to make up the rest of the header (if required)
|
||||
rle_header.extend(b"\x00" * (64 - len(rle_header)))
|
||||
|
||||
return bytes(rle_header + rle_data)
|
||||
|
||||
|
||||
def _encode_segment(src: bytes, columns: int) -> bytearray:
|
||||
"""Return `src` as an RLE encoded bytearray.
|
||||
|
||||
Each row of the image is encoded separately as required by the DICOM
|
||||
Standard.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes
|
||||
The little-endian ordered data to be encoded, representing a Byte
|
||||
Segment as in the DICOM Standard, Part 5,
|
||||
:dcm:`Annex G.2<part05/sect_G.2.html>`.
|
||||
columns : int
|
||||
The number of columns in the image.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytearray
|
||||
The RLE encoded segment, following the format specified by the DICOM
|
||||
Standard. Odd length encoded segments are padded by a trailing ``0x00``
|
||||
to be even length.
|
||||
"""
|
||||
out = bytearray()
|
||||
for idx in range(0, len(src), columns):
|
||||
out.extend(_encode_row(src[idx : idx + columns]))
|
||||
|
||||
# Pad odd length data with a trailing 0x00 byte
|
||||
out.extend(b"\x00" * (len(out) % 2))
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _encode_row(src: bytes) -> bytes:
|
||||
"""Return `src` as RLE encoded bytes.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
src : bytes
|
||||
The little-endian ordered data to be encoded.
|
||||
|
||||
Returns
|
||||
-------
|
||||
bytes
|
||||
The RLE encoded row, following the format specified by the DICOM
|
||||
Standard, Part 5, :dcm:`Annex G<part05/chapter_G.html>`
|
||||
|
||||
Notes
|
||||
-----
|
||||
* 2-byte repeat runs are always encoded as Replicate Runs rather than
|
||||
only when not preceded by a Literal Run as suggested by the Standard.
|
||||
"""
|
||||
out: list[int] = []
|
||||
out_append = out.append
|
||||
out_extend = out.extend
|
||||
|
||||
literal = []
|
||||
for _, iter_group in groupby(src):
|
||||
group = list(iter_group)
|
||||
if len(group) == 1:
|
||||
literal.append(group[0])
|
||||
else:
|
||||
if literal:
|
||||
# Literal runs
|
||||
nr_full_runs, len_partial_run = divmod(len(literal), 128)
|
||||
for idx in range(nr_full_runs):
|
||||
idx *= 128
|
||||
out_append(127)
|
||||
out_extend(literal[idx : idx + 128])
|
||||
|
||||
if len_partial_run:
|
||||
out_append(len_partial_run - 1)
|
||||
out_extend(literal[-len_partial_run:])
|
||||
|
||||
literal = []
|
||||
|
||||
# Replicate runs
|
||||
nr_full_runs, len_partial_run = divmod(len(group), 128)
|
||||
if nr_full_runs:
|
||||
out_extend((129, group[0]) * nr_full_runs)
|
||||
|
||||
if len_partial_run > 1:
|
||||
out_extend((257 - len_partial_run, group[0]))
|
||||
elif len_partial_run == 1:
|
||||
# Literal run - only if last replicate part is length 1
|
||||
out_extend((0, group[0]))
|
||||
|
||||
# Final literal run if literal isn't followed by a replicate run
|
||||
for ii in range(0, len(literal), 128):
|
||||
_run = literal[ii : ii + 128]
|
||||
out_append(len(_run) - 1)
|
||||
out_extend(_run)
|
||||
|
||||
return pack(f"{len(out)}B", *out)
|
||||
53
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/pyjpegls.py
vendored
Executable file
53
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/pyjpegls.py
vendored
Executable file
@@ -0,0 +1,53 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Use pyjpegls <https://github.com/pydicom/pyjpegls> to compress *Pixel Data*.
|
||||
|
||||
This module is not intended to be used directly.
|
||||
"""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from pydicom import uid
|
||||
from pydicom.pixels.encoders.base import EncodeRunner
|
||||
from pydicom.pixels.utils import _passes_version_check
|
||||
|
||||
try:
|
||||
import jpeg_ls
|
||||
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
ENCODER_DEPENDENCIES = {
|
||||
uid.JPEGLSLossless: ("numpy", "pyjpegls>=1.3"),
|
||||
uid.JPEGLSNearLossless: ("numpy", "pyjpegls>=1.3"),
|
||||
}
|
||||
|
||||
|
||||
def is_available(uid: str) -> bool:
|
||||
"""Return ``True`` if the decoder has its dependencies met, ``False`` otherwise"""
|
||||
return _passes_version_check("jpeg_ls", (1, 3))
|
||||
|
||||
|
||||
def _encode_frame(src: bytes, runner: EncodeRunner) -> bytearray:
|
||||
"""Return the image data in `src` as a JPEG-LS encoded codestream."""
|
||||
lossy_error = runner.get_option("jls_error", 0)
|
||||
if lossy_error and runner.transfer_syntax == uid.JPEGLSLossless:
|
||||
raise ValueError(
|
||||
f"A 'jls_error' value of '{lossy_error}' is being used with a "
|
||||
"transfer syntax of 'JPEG-LS Lossless' - did you mean to use "
|
||||
"'JPEG-LS Near Lossless' instead?"
|
||||
)
|
||||
|
||||
opts = {
|
||||
"rows": runner.rows,
|
||||
"columns": runner.columns,
|
||||
"samples_per_pixel": runner.samples_per_pixel,
|
||||
"bits_stored": runner.bits_stored,
|
||||
}
|
||||
|
||||
if runner.samples_per_pixel > 1:
|
||||
opts["planar_configuration"] = runner.planar_configuration
|
||||
|
||||
return cast(
|
||||
bytearray, jpeg_ls.encode_pixel_data(src, lossy_error=lossy_error, **opts)
|
||||
)
|
||||
82
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/pylibjpeg.py
vendored
Executable file
82
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/encoders/pylibjpeg.py
vendored
Executable file
@@ -0,0 +1,82 @@
|
||||
# Copyright 2008-2024 pydicom authors. See LICENSE file for details.
|
||||
"""Interface for *Pixel Data* encoding, not intended to be used directly."""
|
||||
|
||||
from typing import cast
|
||||
|
||||
from pydicom.pixels.encoders.base import EncodeRunner
|
||||
from pydicom.pixels.common import PhotometricInterpretation as PI
|
||||
from pydicom.pixels.utils import _passes_version_check
|
||||
from pydicom import uid
|
||||
|
||||
try:
|
||||
from pylibjpeg.utils import get_pixel_data_encoders, Encoder
|
||||
|
||||
_ENCODERS = get_pixel_data_encoders()
|
||||
except ImportError:
|
||||
_ENCODERS = {}
|
||||
|
||||
|
||||
ENCODER_DEPENDENCIES = {
|
||||
uid.JPEG2000Lossless: ("numpy", "pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.2"),
|
||||
uid.JPEG2000: ("numpy", "pylibjpeg>=2.0", "pylibjpeg-openjpeg>=2.2"),
|
||||
uid.RLELossless: ("numpy", "pylibjpeg>=2.0", "pylibjpeg-rle>=2.0"),
|
||||
}
|
||||
_OPENJPEG_SYNTAXES = [uid.JPEG2000Lossless, uid.JPEG2000]
|
||||
_RLE_SYNTAXES = [uid.RLELossless]
|
||||
|
||||
|
||||
def is_available(uid: str) -> bool:
|
||||
"""Return ``True`` if a pixel data encoder for `uid` is available for use,
|
||||
``False`` otherwise.
|
||||
"""
|
||||
if not _passes_version_check("pylibjpeg", (2, 0)):
|
||||
return False
|
||||
|
||||
if uid in _OPENJPEG_SYNTAXES:
|
||||
return _passes_version_check("openjpeg", (2, 2))
|
||||
|
||||
if uid in _RLE_SYNTAXES:
|
||||
return _passes_version_check("rle", (2, 0))
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def _encode_frame(src: bytes, runner: EncodeRunner) -> bytes | bytearray:
|
||||
"""Return `src` as an encoded codestream."""
|
||||
encoder = cast(Encoder, _ENCODERS[runner.transfer_syntax])
|
||||
|
||||
tsyntax = runner.transfer_syntax
|
||||
if tsyntax == uid.RLELossless:
|
||||
return cast(bytes, encoder(src, **runner.options))
|
||||
|
||||
opts = dict(runner.options)
|
||||
if runner.photometric_interpretation == PI.RGB:
|
||||
opts["use_mct"] = False
|
||||
|
||||
cr = opts.pop("compression_ratios", opts.get("j2k_cr", None))
|
||||
psnr = opts.pop("signal_noise_ratios", opts.get("j2k_psnr", None))
|
||||
if tsyntax == uid.JPEG2000Lossless:
|
||||
if cr or psnr:
|
||||
raise ValueError(
|
||||
"A lossy configuration option is being used with a transfer "
|
||||
"syntax of 'JPEG 2000 Lossless' - did you mean to use 'JPEG "
|
||||
"2000' instead?"
|
||||
)
|
||||
|
||||
return cast(bytes, encoder(src, **opts))
|
||||
|
||||
if not cr and not psnr:
|
||||
raise ValueError(
|
||||
"The 'JPEG 2000' transfer syntax requires a lossy configuration "
|
||||
"option such as 'j2k_cr' or 'j2k_psnr'"
|
||||
)
|
||||
|
||||
if cr and psnr:
|
||||
raise ValueError(
|
||||
"Multiple lossy configuration options are being used with the "
|
||||
"'JPEG 2000' transfer syntax, please specify only one"
|
||||
)
|
||||
|
||||
cs = encoder(src, **opts, compression_ratios=cr, signal_noise_ratios=psnr)
|
||||
|
||||
return cast(bytes, cs)
|
||||
1191
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/processing.py
vendored
Executable file
1191
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/processing.py
vendored
Executable file
File diff suppressed because it is too large
Load Diff
1994
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/utils.py
vendored
Executable file
1994
dist/dicom2pacs.app/Contents/Resources/lib/python3.13/pydicom/pixels/utils.py
vendored
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user