diff --git a/.github/workflows/run_unit_tests.yml b/.github/workflows/run_unit_tests.yml index 13b92d31..1ee0cc75 100644 --- a/.github/workflows/run_unit_tests.yml +++ b/.github/workflows/run_unit_tests.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] + python-version: ["3.10", "3.11", "3.12"] dependencies: [".", "'.[libjpeg]'"] steps: diff --git a/docs/seg.rst b/docs/seg.rst index 6152f65c..36e33d0a 100644 --- a/docs/seg.rst +++ b/docs/seg.rst @@ -807,22 +807,31 @@ We recommend that if you do this, you specify ``max_fractional_value=1`` to clearly communicate that the segmentation is inherently binary in nature. Why would you want to make this seemingly rather strange choice? Well, -``"FRACTIONAL"`` SEGs tend to compress *much* better than ``"BINARY"`` ones -(see next section). Note however, that this is arguably an misuse of the intent -of the standard, so *caveat emptor*. +``"FRACTIONAL"`` SEGs tend to compress better than ``"BINARY"`` ones (see next +section). Note however, that this is arguably an misuse of the intent of the +standard, so *caveat emptor*. Also note that while this used to be a more +serious issue it is less serious now that ``"JPEG2000Lossless"`` compression is +now supported for ``"BINARY"`` segmentations as of highdicom v0.23.0. Compression ----------- The types of pixel compression available in segmentation images depends on the -segmentation type. Pixels in a ``"BINARY"`` segmentation image are "bit-packed" -such that 8 pixels are grouped into 1 byte in the stored array. If a given frame -contains a number of pixels that is not divisible by 8 exactly, a single byte +segmentation type. + +Pixels in an uncompressed ``"BINARY"`` segmentation image are "bit-packed" such +that 8 pixels are grouped into 1 byte in the stored array. If a given frame +contains a number of pixels that is not divisible by 8 exactly, a single byte will straddle a frame boundary into the next frame if there is one, or the byte will be padded with zeroes of there are no further frames. This means that -retrieving individual frames from segmentation images in which each frame -size is not divisible by 8 becomes problematic. No further compression may be -applied to frames of ``"BINARY"`` segmentation images. +retrieving individual frames from segmentation images in which each frame size +is not divisible by 8 becomes problematic. For this reason, as well as for +space efficiency (sparse segmentations tend to compress very well), we +therefore strongly recommend using ``"JPEG2000Lossless"`` compression with +``"BINRARY"`` segmentations. This is the only compression method currently +supported for ``"BINARY"`` segmentations. However, beware that reading these +single-bit JPEG 2000 images may not be supported by all other tools and +viewers. Pixels in ``"FRACTIONAL"`` segmentation images may be compressed using one of the lossless compression methods available within DICOM. Currently *highdicom* @@ -830,16 +839,6 @@ supports the following compressed transfer syntaxes when creating ``"FRACTIONAL"`` segmentation images: ``"RLELossless"``, ``"JPEG2000Lossless"``, and ``"JPEGLSLossless"``. -Note that there may be advantages to using ``"FRACTIONAL"`` segmentations to -store segmentation images that are binary in nature (i.e. only taking values 0 -and 1): - -- If the segmentation is very simple or sparse, the lossless compression methods - available in ``"FRACTIONAL"`` images may be more effective than the - "bit-packing" method required by ``"BINARY"`` segmentations. -- The clear frame boundaries make retrieving individual frames from - ``"FRACTIONAL"`` image files possible. - Multiprocessing --------------- diff --git a/pyproject.toml b/pyproject.toml index 4d90e18d..e8898135 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "highdicom" dynamic = ["version"] description = "High-level DICOM abstractions." readme = "README.md" -requires-python = ">=3.6" +requires-python = ">=3.10" authors = [ { name = "Markus D. Herrmann" }, ] @@ -24,10 +24,6 @@ classifiers = [ "Operating System :: Microsoft :: Windows", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -36,21 +32,21 @@ classifiers = [ ] dependencies = [ "numpy>=1.19", - "pillow-jpls>=1.0", "pillow>=8.3", - "pydicom>=2.3.0,!=2.4.0", + "pydicom>=3.0.1", + "pyjpegls>=1.0.0", ] [project.optional-dependencies] libjpeg = [ - "pylibjpeg-libjpeg>=1.3", - "pylibjpeg-openjpeg>=1.2", - "pylibjpeg>=1.4", + "pylibjpeg-libjpeg>=2.1", + "pylibjpeg-openjpeg>=2.0.0", + "pylibjpeg>=2.0", ] test = [ "mypy==0.971", - "pytest==7.1.2", - "pytest-cov==3.0.0", + "pytest==7.4.4", + "pytest-cov==4.1.0", "pytest-flake8==1.1.1", "numpy-stubs @ git+https://github.com/numpy/numpy-stubs@201115370a0c011d879d69068b60509accc7f750", ] @@ -68,12 +64,15 @@ documentation = "https://highdicom.readthedocs.io/" repository = "https://github.com/ImagingDataCommons/highdicom.git" [tool.pytest.ini_options] -addopts = "--doctest-modules" +minversion = "7" +addopts = ["--doctest-modules", "-ra", "--strict-config", "--strict-markers"] testpaths = ["tests"] log_cli_level = "INFO" +xfail_strict = true [tool.mypy] warn_unreachable = true +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] [[tool.mypy.overrides]] module = "mypy-pydicom.*" diff --git a/src/highdicom/base.py b/src/highdicom/base.py index 9a80673a..4583537b 100644 --- a/src/highdicom/base.py +++ b/src/highdicom/base.py @@ -139,8 +139,6 @@ def __init__( "Big Endian transfer syntaxes are retired and no longer " "supported by highdicom." ) - self.is_little_endian = True # backwards compatibility - self.is_implicit_VR = transfer_syntax_uid.is_implicit_VR # Include all File Meta Information required for writing SOP instance # to a file in PS3.10 format. @@ -154,7 +152,6 @@ def __init__( '1.2.826.0.1.3680043.9.7433.1.1' ) self.file_meta.ImplementationVersionName = f'highdicom{__version__}' - self.fix_meta_info(enforce_standard=True) with BytesIO() as fp: write_file_meta_info(fp, self.file_meta, enforce_standard=True) self.file_meta.FileMetaInformationGroupLength = len(fp.getvalue()) diff --git a/src/highdicom/content.py b/src/highdicom/content.py index 82b4a690..a4d5daa7 100644 --- a/src/highdicom/content.py +++ b/src/highdicom/content.py @@ -11,7 +11,7 @@ from pydicom.sr.coding import Code from pydicom.sr.codedict import codes from pydicom.valuerep import DS, format_number_as_ds -from pydicom._storage_sopclass_uids import SegmentationStorage +from pydicom.uid import SegmentationStorage from highdicom.enum import ( CoordinateSystemNames, diff --git a/src/highdicom/frame.py b/src/highdicom/frame.py index 6a98fd8b..16bd28b5 100644 --- a/src/highdicom/frame.py +++ b/src/highdicom/frame.py @@ -3,17 +3,20 @@ from typing import Optional, Union import numpy as np +from openjpeg.utils import encode_array from PIL import Image from pydicom.dataset import Dataset, FileMetaDataset from pydicom.encaps import encapsulate -from pydicom.pixel_data_handlers.numpy_handler import pack_bits -from pydicom.pixel_data_handlers.rle_handler import rle_encode_frame +from pydicom.pixels.utils import pack_bits +from pydicom.pixels.encoders.base import get_encoder from pydicom.uid import ( ExplicitVRLittleEndian, ImplicitVRLittleEndian, JPEG2000Lossless, + JPEG2000, JPEGBaseline8Bit, JPEGLSLossless, + JPEGLSNearLossless, UID, RLELossless, ) @@ -51,7 +54,15 @@ def encode_frame( bits_stored: int Number of bits that are required to store a pixel sample photometric_interpretation: Union[PhotometricInterpretationValues, str] - Photometric interpretation + Photometric interpretation that will be used to store data. Usually, + this will match the photometric interpretation of the input pixel + array, however for ``"JPEGBaseline8Bit"``, ``"JPEG2000"``, and + ``"JPEG2000Lossless"`` transfer syntaxes with color images, the pixel + data must be passed in in RGB format and will be converted and stored + as ``"YBR_FULL_422"`` (``"JPEGBaseline8Bit"``), ``"YBR_ICT"`` + (``"JPEG2000"``), or `"YBR_RCT"`` (``"JPEG2000Lossless"``). In these + cases the values of photometric metric passed must match those given + above. pixel_representation: Union[highdicom.PixelRepresentationValues, int, None], optional Whether pixel samples are represented as unsigned integers or 2's complements @@ -111,8 +122,10 @@ def encode_frame( } compressed_transfer_syntaxes = { JPEGBaseline8Bit, + JPEG2000, JPEG2000Lossless, JPEGLSLossless, + JPEGLSNearLossless, RLELossless, } supported_transfer_syntaxes = uncompressed_transfer_syntaxes.union( @@ -132,6 +145,16 @@ def encode_frame( 'Planar configuration must be 0 for color image frames ' 'with native encoding.' ) + allowable_pis = { + 1: ['MONOCHROME1', 'MONOCHROME2', 'PALETTE_COLOR'], + 3: ['RGB', 'YBR_FULL'], + }[samples_per_pixel] + if photometric_interpretation not in allowable_pis: + raise ValueError( + 'Photometric_interpretation of ' + f"'{photometric_interpretation}' " + f'not supported for samples_per_pixel={samples_per_pixel}.' + ) if bits_allocated == 1: if (rows * cols * samples_per_pixel) % 8 != 0: raise ValueError( @@ -142,129 +165,86 @@ def encode_frame( else: return array.flatten().tobytes() - else: - compression_lut = { - JPEGBaseline8Bit: ( - 'jpeg', - { - 'quality': 95 - }, - ), - JPEG2000Lossless: ( - 'jpeg2000', - { - 'tile_size': None, - 'num_resolutions': 1, - 'irreversible': False, - 'no_jp2': True, - }, - ), - JPEGLSLossless: ( - 'JPEG-LS', - { - 'near_lossless': 0, - } - ) - } - - if transfer_syntax_uid == JPEGBaseline8Bit: - if samples_per_pixel == 1: - if planar_configuration is not None: - raise ValueError( - 'Planar configuration must be absent for encoding of ' - 'monochrome image frames with JPEG Baseline codec.' - ) - if photometric_interpretation not in ( - 'MONOCHROME1', 'MONOCHROME2' - ): - raise ValueError( - 'Photometric intpretation must be either "MONOCHROME1" ' - 'or "MONOCHROME2" for encoding of monochrome image ' - 'frames with JPEG Baseline codec.' - ) - elif samples_per_pixel == 3: - if photometric_interpretation != 'YBR_FULL_422': - raise ValueError( - 'Photometric intpretation must be "YBR_FULL_422" for ' - 'encoding of color image frames with ' - 'JPEG Baseline codec.' - ) - if planar_configuration != 0: - raise ValueError( - 'Planar configuration must be 0 for encoding of ' - 'color image frames with JPEG Baseline codec.' - ) - else: + elif transfer_syntax_uid == JPEGBaseline8Bit: + if samples_per_pixel == 1: + if planar_configuration is not None: raise ValueError( - 'Samples per pixel must be 1 or 3 for ' - 'encoding of image frames with JPEG Baseline codec.' + 'Planar configuration must be absent for encoding of ' + 'monochrome image frames with JPEG Baseline codec.' ) - if bits_allocated != 8 or bits_stored != 8: + if photometric_interpretation not in ( + 'MONOCHROME1', 'MONOCHROME2' + ): raise ValueError( - 'Bits allocated and bits stored must be 8 for ' - 'encoding of image frames with JPEG Baseline codec.' + 'Photometric intpretation must be either "MONOCHROME1" ' + 'or "MONOCHROME2" for encoding of monochrome image ' + 'frames with JPEG Baseline codec.' ) - if pixel_representation != 0: + elif samples_per_pixel == 3: + if photometric_interpretation != 'YBR_FULL_422': raise ValueError( - 'Pixel representation must be 0 for ' - 'encoding of image frames with JPEG Baseline codec.' + 'Photometric intpretation must be "YBR_FULL_422" for ' + 'encoding of color image frames with ' + 'JPEG Baseline codec.' ) - - elif transfer_syntax_uid == JPEG2000Lossless: - if samples_per_pixel == 1: - if planar_configuration is not None: - raise ValueError( - 'Planar configuration must be absent for encoding of ' - 'monochrome image frames with Lossless JPEG 2000 codec.' - ) - if photometric_interpretation not in ( - 'MONOCHROME1', 'MONOCHROME2' - ): - raise ValueError( - 'Photometric intpretation must be either "MONOCHROME1" ' - 'or "MONOCHROME2" for encoding of monochrome image ' - 'frames with Lossless JPEG 2000 codec.' - ) - if bits_allocated not in (8, 16): - raise ValueError( - 'Bits Allocated must be 8 or 16 for encoding of ' - 'monochrome image frames with Lossless JPEG 2000 codec.' - ) - elif samples_per_pixel == 3: - if photometric_interpretation != 'YBR_FULL': - raise ValueError( - 'Photometric interpretation must be "YBR_FULL" for ' - 'encoding of color image frames with ' - 'Lossless JPEG 2000 codec.' - ) - if planar_configuration != 0: - raise ValueError( - 'Planar configuration must be 0 for encoding of ' - 'color image frames with Lossless JPEG 2000 codec.' - ) - if bits_allocated != 8: - raise ValueError( - 'Bits Allocated must be 8 for encoding of ' - 'color image frames with Lossless JPEG 2000 codec.' - ) - else: + if planar_configuration != 0: raise ValueError( - 'Samples per pixel must be 1 or 3 for ' - 'encoding of image frames with Lossless JPEG 2000 codec.' + 'Planar configuration must be 0 for encoding of ' + 'color image frames with JPEG Baseline codec.' ) + else: + raise ValueError( + 'Samples per pixel must be 1 or 3 for ' + 'encoding of image frames with JPEG Baseline codec.' + ) + if bits_allocated != 8 or bits_stored != 8: + raise ValueError( + 'Bits allocated and bits stored must be 8 for ' + 'encoding of image frames with JPEG Baseline codec.' + ) + if pixel_representation != 0: + raise ValueError( + 'Pixel representation must be 0 for ' + 'encoding of image frames with JPEG Baseline codec.' + ) + + # Pydicom does not have an encoder for JPEGBaseline8Bit so + # we do this manually + if samples_per_pixel == 3: + image = Image.fromarray(array, mode='RGB') + else: + image = Image.fromarray(array) + with BytesIO() as buf: + image.save(buf, format='jpeg', quality=95) + data = buf.getvalue() + else: + name = { + JPEG2000: "JPEG 2000", + JPEG2000Lossless: "Lossless JPEG 2000", + JPEGLSLossless: "Lossless JPEG-LS", + JPEGLSNearLossless: "Near-Lossless JPEG-LS", + RLELossless: "RLE Lossless", + }[transfer_syntax_uid] + + kwargs = {} + + if samples_per_pixel not in (1, 3): + raise ValueError( + 'Samples per pixel must be 1 or 3 for ' + f'encoding of image frames with {name} codec.' + ) + + if transfer_syntax_uid != RLELossless: if pixel_representation != 0: raise ValueError( 'Pixel representation must be 0 for ' - 'encoding of image frames with Lossless JPEG 2000 codec.' + f'encoding of image frames with {name} codec.' ) - - elif transfer_syntax_uid == JPEGLSLossless: - import pillow_jpls # noqa if samples_per_pixel == 1: if planar_configuration is not None: raise ValueError( 'Planar configuration must be absent for encoding of ' - 'monochrome image frames with Lossless JPEG-LS codec.' + f'monochrome image frames with {name} codec.' ) if photometric_interpretation not in ( 'MONOCHROME1', 'MONOCHROME2' @@ -272,55 +252,87 @@ def encode_frame( raise ValueError( 'Photometric intpretation must be either "MONOCHROME1" ' 'or "MONOCHROME2" for encoding of monochrome image ' - 'frames with Lossless JPEG-LS codec.' - ) - if bits_allocated not in (8, 16): - raise ValueError( - 'Bits Allocated must be 8 or 16 for encoding of ' - 'monochrome image frames with Lossless JPEG-LS codec.' + f'frames with with {name} codec.' ) + if transfer_syntax_uid == JPEG2000Lossless: + if bits_allocated not in (1, 8, 16): + raise ValueError( + 'Bits Allocated must be 1, 8, or 16 for encoding of ' + f'monochrome image frames with with {name} codec.' + ) + else: + if bits_allocated not in (8, 16): + raise ValueError( + 'Bits Allocated must be 8 or 16 for encoding of ' + f'monochrome image frames with with {name} codec.' + ) elif samples_per_pixel == 3: - if photometric_interpretation != 'YBR_FULL': - raise ValueError( - 'Photometric interpretation must be "YBR_FULL" for ' - 'encoding of color image frames with ' - 'Lossless JPEG-LS codec.' - ) if planar_configuration != 0: raise ValueError( 'Planar configuration must be 0 for encoding of ' - 'color image frames with Lossless JPEG-LS codec.' + f'color image frames with {name} codec.' ) - if bits_allocated != 8: + if bits_allocated not in (8, 16): raise ValueError( - 'Bits Allocated must be 8 for encoding of ' - 'color image frames with Lossless JPEG-LS codec.' + 'Bits Allocated must be 8 or 16 for encoding of ' + f'color image frames with {name} codec.' ) - else: - raise ValueError( - 'Samples per pixel must be 1 or 3 for ' - 'encoding of image frames with Lossless JPEG-LS codec.' - ) - if pixel_representation != 0: + + required_pi = { + JPEG2000: PhotometricInterpretationValues.YBR_ICT, + JPEG2000Lossless: PhotometricInterpretationValues.YBR_RCT, + JPEGLSLossless: PhotometricInterpretationValues.RGB, + JPEGLSNearLossless: PhotometricInterpretationValues.RGB, + }[transfer_syntax_uid] + + if photometric_interpretation != required_pi.value: + raise ValueError( + f'Photometric interpretation must be "{required_pi.value}" ' + 'for encoding of color image frames with ' + f'{name} codec.' + ) + + if transfer_syntax_uid == JPEG2000: + kwargs = {'j2k_psnr': [100]} + + if transfer_syntax_uid in (JPEG2000, JPEG2000Lossless): + # This seems to be an openjpeg limitation + if array.shape[0] < 32 or array.shape[1] < 32: raise ValueError( - 'Pixel representation must be 0 for ' - 'encoding of image frames with Lossless JPEG-LS codec.' + 'Images smaller than 32 pixels along both dimensions ' + f'cannot be encoded with {name} codec.' ) - if transfer_syntax_uid in compression_lut: - image_format, kwargs = compression_lut[transfer_syntax_uid] - if samples_per_pixel == 3: - image = Image.fromarray(array, mode='RGB') - else: - image = Image.fromarray(array) - with BytesIO() as buf: - image.save(buf, format=image_format, **kwargs) - data = buf.getvalue() - elif transfer_syntax_uid == RLELossless: - data = rle_encode_frame(array) + if transfer_syntax_uid == JPEG2000Lossless and bits_allocated == 1: + # Single bit JPEG2000 compression. Pydicom doesn't (yet) support + # this case + if array.dtype != bool: + if array.max() > 1: + raise ValueError( + 'Array must contain only 0 and 1 for bits_allocated = 1' + ) + array = array.astype(bool) + data = encode_array( + array, + bits_stored=1, + photometric_interpretation=2, + use_mct=False, + ) else: - raise ValueError( - f'Transfer Syntax "{transfer_syntax_uid}" is not supported.' + encoder = get_encoder(transfer_syntax_uid) + + data = encoder.encode( + array, + rows=array.shape[0], + columns=array.shape[1], + samples_per_pixel=samples_per_pixel, + number_of_frames=1, + bits_allocated=bits_allocated, + bits_stored=bits_stored, + photometric_interpretation=photometric_interpretation, + pixel_representation=pixel_representation, + planar_configuration=planar_configuration, + **kwargs, ) return data @@ -437,16 +449,4 @@ def decode_frame( array = ds.pixel_array - # In case of the JPEG baseline transfer syntax, the pixel_array property - # does not convert the pixel data into the correct (or let's say expected) - # color space after decompression. - if ( - 'YBR' in ds.PhotometricInterpretation and - ds.SamplesPerPixel == 3 and - transfer_syntax_uid == JPEGBaseline8Bit - ): - image = Image.fromarray(array, mode='YCbCr') - image = image.convert(mode='RGB') - array = np.asarray(image) - return array diff --git a/src/highdicom/io.py b/src/highdicom/io.py index 67417f8c..1bccfdef 100644 --- a/src/highdicom/io.py +++ b/src/highdicom/io.py @@ -8,7 +8,7 @@ import numpy as np import pydicom from pydicom.dataset import Dataset -from pydicom.encaps import get_frame_offsets +from pydicom.encaps import parse_basic_offsets from pydicom.filebase import DicomFile, DicomFileLike, DicomBytesIO from pydicom.filereader import ( data_element_offset_to_value, @@ -16,7 +16,7 @@ read_file_meta_info, read_partial ) -from pydicom.pixel_data_handlers.numpy_handler import unpack_bits +from pydicom.pixels.utils import unpack_bits from pydicom.tag import TupleTag, ItemTag, SequenceDelimiterTag from pydicom.uid import UID @@ -120,7 +120,7 @@ def _read_bot(fp: DicomFileLike) -> List[int]: fp.is_implicit_VR, 'OB' ) fp.seek(pixel_data_element_value_offset - 4, 1) - is_empty, offsets = get_frame_offsets(fp) + offsets = parse_basic_offsets(fp) return offsets @@ -249,7 +249,7 @@ def __init__(self, filename: Union[str, Path, DicomFileLike]): DICOM Part10 file containing a dataset of an image SOP Instance """ - if isinstance(filename, DicomFileLike): + if isinstance(filename, (DicomFileLike, DicomBytesIO)): fp = filename self._fp = fp if isinstance(filename, DicomBytesIO): @@ -261,8 +261,8 @@ def __init__(self, filename: Union[str, Path, DicomFileLike]): self._fp = None else: raise TypeError( - 'Argument "filename" must either an open DICOM file object or ' - 'the path to a DICOM file stored on disk.' + 'Argument "filename" must be either an open DICOM file object ' + 'or the path to a DICOM file stored on disk.' ) self._metadata = None diff --git a/src/highdicom/pr/sop.py b/src/highdicom/pr/sop.py index d3b7afca..49b86861 100644 --- a/src/highdicom/pr/sop.py +++ b/src/highdicom/pr/sop.py @@ -8,7 +8,7 @@ from pydicom import Dataset from pydicom.sr.coding import Code from pydicom.uid import ExplicitVRLittleEndian -from pydicom._storage_sopclass_uids import ( +from pydicom.uid import ( AdvancedBlendingPresentationStateStorage, ColorSoftcopyPresentationStateStorage, GrayscaleSoftcopyPresentationStateStorage, diff --git a/src/highdicom/sc/sop.py b/src/highdicom/sc/sop.py index df8affce..0646d4ae 100644 --- a/src/highdicom/sc/sop.py +++ b/src/highdicom/sc/sop.py @@ -16,8 +16,10 @@ ExplicitVRLittleEndian, RLELossless, JPEGBaseline8Bit, + JPEG2000, JPEG2000Lossless, JPEGLSLossless, + JPEGLSNearLossless, ) from highdicom.base import SOPClass @@ -108,9 +110,17 @@ def __init__( image; either a 2D grayscale image or a 3D color image (RGB color space) photometric_interpretation: Union[str, highdicom.PhotometricInterpretationValues] - Interpretation of pixel data; either ``"MONOCHROME1"`` or - ``"MONOCHROME2"`` for 2D grayscale images or ``"RGB"`` or - ``"YBR_FULL"`` for 3D color images + Interpretation with which to store pixel data in the dataset; + either ``"MONOCHROME1"`` or ``"MONOCHROME2"`` for 2D grayscale + images or ``"RGB"`` or ``"YBR_FULL"`` for 3D color images. Note + that this should match the photometric interpretation of the input + pixel array, except in the following cases: if + ``transfer_syntax_uid`` is ``"JPEGBaseline8Bit"``, + ``photometric_interpretation must be ``"YBR_FULL_422"``, if + ``transfer_syntax_uid`` is ``"JPEG2000"``, + ``photometric_interpretation must be ``"YBR_ICT"``, if + ``transfer_syntax_uid`` is ``"JPEG2000Lossless"``, + ``photometric_interpretation must be ``"YBR_RCT"``. bits_allocated: int Number of bits that should be allocated per pixel value coordinate_system: Union[str, highdicom.CoordinateSystemNames] @@ -188,8 +198,10 @@ def __init__( ExplicitVRLittleEndian, RLELossless, JPEGBaseline8Bit, + JPEG2000, JPEG2000Lossless, JPEGLSLossless, + JPEGLSNearLossless, } if transfer_syntax_uid not in supported_transfer_syntaxes: raise ValueError( @@ -326,12 +338,23 @@ def __init__( photometric_interpretation ) if pixel_array.ndim == 3: - accepted_interpretations = { - PhotometricInterpretationValues.RGB.value, - PhotometricInterpretationValues.YBR_FULL.value, - PhotometricInterpretationValues.YBR_FULL_422.value, - PhotometricInterpretationValues.YBR_PARTIAL_420.value, - } + if transfer_syntax_uid == JPEGBaseline8Bit: + accepted_interpretations = { + PhotometricInterpretationValues.YBR_FULL_422.value, + } + elif transfer_syntax_uid == JPEG2000: + accepted_interpretations = { + PhotometricInterpretationValues.YBR_ICT.value, + } + elif transfer_syntax_uid == JPEG2000Lossless: + accepted_interpretations = { + PhotometricInterpretationValues.YBR_RCT.value, + } + else: + accepted_interpretations = { + PhotometricInterpretationValues.RGB.value, + PhotometricInterpretationValues.YBR_FULL.value, + } if photometric_interpretation.value not in accepted_interpretations: raise ValueError( 'Pixel array has an unexpected photometric interpretation.' diff --git a/src/highdicom/seg/sop.py b/src/highdicom/seg/sop.py index 03bf6253..b2ef0c7f 100644 --- a/src/highdicom/seg/sop.py +++ b/src/highdicom/seg/sop.py @@ -29,7 +29,7 @@ from pydicom.datadict import get_entry, keyword_for_tag, tag_for_keyword from pydicom.encaps import encapsulate from pydicom.multival import MultiValue -from pydicom.pixel_data_handlers.numpy_handler import pack_bits +from pydicom.pixels.utils import pack_bits from pydicom.tag import BaseTag, Tag from pydicom.uid import ( ExplicitVRLittleEndian, @@ -95,7 +95,9 @@ _NO_FRAME_REF_VALUE = -1 # These codes are needed many times in loops so we precompute them -_DERIVATION_CODE = CodedConcept.from_code(codes.cid7203.Segmentation) +_DERIVATION_CODE = CodedConcept.from_code( + codes.cid7203.SegmentationImageDerivation +) _PURPOSE_CODE = CodedConcept.from_code( codes.cid7202.SourceImageForImageProcessingOperation ) @@ -1632,7 +1634,10 @@ def __init__( if self.SegmentationType == SegmentationTypeValues.BINARY.value: self.BitsAllocated = 1 self.HighBit = 0 - if self.file_meta.TransferSyntaxUID.is_encapsulated: + if ( + self.file_meta.TransferSyntaxUID != JPEG2000Lossless and + self.file_meta.TransferSyntaxUID.is_encapsulated + ): raise ValueError( 'The chosen transfer syntax ' f'{self.file_meta.TransferSyntaxUID} ' diff --git a/src/highdicom/spatial.py b/src/highdicom/spatial.py index 00e6c530..de4a7557 100644 --- a/src/highdicom/spatial.py +++ b/src/highdicom/spatial.py @@ -742,7 +742,7 @@ def _create_affine_transformation_matrix( rotation[:, 1] *= column_spacing # 4x4 transformation matrix - return np.row_stack( + return np.vstack( [ np.column_stack([ rotation, @@ -828,7 +828,7 @@ def _create_inv_affine_transformation_matrix( inv_rotation = np.linalg.inv(rotation) # 4x4 transformation matrix - return np.row_stack( + return np.vstack( [ np.column_stack([ inv_rotation, @@ -970,7 +970,7 @@ def __call__(self, indices: np.ndarray) -> np.ndarray: 'Argument "indices" must be a two-dimensional array ' 'of integers.' ) - pixel_matrix_coordinates = np.row_stack([ + pixel_matrix_coordinates = np.vstack([ indices.T.astype(float), np.zeros((indices.shape[0], ), dtype=float), np.ones((indices.shape[0], ), dtype=float), @@ -1175,7 +1175,7 @@ def __call__(self, coordinates: np.ndarray) -> np.ndarray: 'Argument "coordinates" must be a two-dimensional array ' 'with shape [n, 3].' ) - reference_coordinates = np.row_stack([ + reference_coordinates = np.vstack([ coordinates.T.astype(float), np.ones((coordinates.shape[0], ), dtype=float) ]) @@ -1424,7 +1424,7 @@ def __call__(self, indices: np.ndarray) -> np.ndarray: 'Argument "indices" must be a two-dimensional array ' 'of integers.' ) - pixel_matrix_coordinates = np.row_stack([ + pixel_matrix_coordinates = np.vstack([ indices.T.astype(float), np.zeros((indices.shape[0], ), dtype=float), np.ones((indices.shape[0], ), dtype=float), @@ -1637,7 +1637,7 @@ def __call__(self, coordinates: np.ndarray) -> np.ndarray: 'Argument "coordinates" must be a two-dimensional array ' 'with shape [n, 2].' ) - image_coordinates = np.row_stack([ + image_coordinates = np.vstack([ coordinates.T.astype(float), np.zeros((coordinates.shape[0], ), dtype=float), np.ones((coordinates.shape[0], ), dtype=float), @@ -1843,7 +1843,7 @@ def __call__(self, coordinates: np.ndarray) -> np.ndarray: 'Argument "coordinates" must be a two-dimensional array ' 'with shape [n, 3].' ) - reference_coordinates = np.row_stack([ + reference_coordinates = np.vstack([ coordinates.T.astype(float), np.ones((coordinates.shape[0], ), dtype=float) ]) @@ -2086,7 +2086,7 @@ def __call__(self, coordinates: np.ndarray) -> np.ndarray: 'Argument "coordinates" must be a two-dimensional array ' 'with shape [n, 2].' ) - image_coordinates = np.row_stack([ + image_coordinates = np.vstack([ coordinates.T.astype(float), np.zeros((coordinates.shape[0], ), dtype=float), np.ones((coordinates.shape[0], ), dtype=float), diff --git a/src/highdicom/sr/templates.py b/src/highdicom/sr/templates.py index f102d357..80c94a60 100644 --- a/src/highdicom/sr/templates.py +++ b/src/highdicom/sr/templates.py @@ -562,7 +562,7 @@ def _get_coded_modality(sop_class_uid: str) -> Code: '1.2.840.10008.5.1.4.1.1.14.2': codes.cid29.IntravascularOpticalCoherenceTomography, # noqa: E501 '1.2.840.10008.5.1.4.1.1.20': codes.cid29.NuclearMedicine, '1.2.840.10008.5.1.4.1.1.66.1': codes.cid32.Registration, - '1.2.840.10008.5.1.4.1.1.66.2': codes.cid32.SpatialFiducials, + '1.2.840.10008.5.1.4.1.1.66.2': codes.cid32.SpatialFiducialsProducer, '1.2.840.10008.5.1.4.1.1.66.3': codes.cid32.Registration, '1.2.840.10008.5.1.4.1.1.66.4': codes.cid32.Segmentation, '1.2.840.10008.5.1.4.1.1.67': codes.cid32.RealWorldValueMap, diff --git a/tests/test_base.py b/tests/test_base.py index 01198392..c3a7f8f2 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -85,8 +85,6 @@ def test_explicit_vr(self): manufacturer='highdicom', transfer_syntax_uid=ExplicitVRLittleEndian, ) - assert not sop_class.is_implicit_VR - assert sop_class.is_little_endian def test_implicit_vr(self): sop_class = SOPClass( @@ -100,8 +98,6 @@ def test_implicit_vr(self): manufacturer='highdicom', transfer_syntax_uid=ImplicitVRLittleEndian, ) - assert sop_class.is_implicit_VR - assert sop_class.is_little_endian class TestEndianCheck(unittest.TestCase): diff --git a/tests/test_frame.py b/tests/test_frame.py index 35477244..1698f1e9 100644 --- a/tests/test_frame.py +++ b/tests/test_frame.py @@ -4,8 +4,10 @@ import numpy as np import pytest from pydicom.uid import ( + JPEG2000, JPEG2000Lossless, JPEGLSLossless, + JPEGLSNearLossless, JPEGBaseline8Bit, ) @@ -160,14 +162,71 @@ def test_jpeg_monochrome(self): def test_jpeg2000_rgb(self): bits_allocated = 8 - frame = np.ones((16, 32, 3), dtype=np.dtype(f'uint{bits_allocated}')) + frame = np.ones((48, 32, 3), dtype=np.dtype(f'uint{bits_allocated}')) + frame[2:4, 5:30, 0] = 7 + compressed_frame = encode_frame( + frame, + transfer_syntax_uid=JPEG2000, + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='YBR_ICT', + pixel_representation=0, + planar_configuration=0 + ) + assert compressed_frame.startswith(b"\xFF\x4F\xFF\x51") + assert compressed_frame.endswith(b'\xFF\xD9') + decoded_frame = decode_frame( + value=compressed_frame, + transfer_syntax_uid=JPEG2000, + rows=frame.shape[0], + columns=frame.shape[1], + samples_per_pixel=frame.shape[2], + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='YBR_ICT', + pixel_representation=0, + planar_configuration=0 + ) + np.testing.assert_allclose(frame, decoded_frame, atol=2) + + def test_jpeg2000_monochrome(self): + bits_allocated = 8 + frame = np.zeros((48, 32), dtype=np.dtype(f'uint{bits_allocated}')) + frame[2:4, 5:30] = 7 + compressed_frame = encode_frame( + frame, + transfer_syntax_uid=JPEG2000, + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='MONOCHROME2', + pixel_representation=0, + ) + assert compressed_frame.startswith(b"\xFF\x4F\xFF\x51") + assert compressed_frame.endswith(b'\xFF\xD9') + decoded_frame = decode_frame( + value=compressed_frame, + transfer_syntax_uid=JPEG2000, + rows=frame.shape[0], + columns=frame.shape[1], + samples_per_pixel=1, + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='MONOCHROME2', + pixel_representation=0, + planar_configuration=0 + ) + np.testing.assert_allclose(frame, decoded_frame, atol=2) + + def test_jpeg2000lossless_rgb(self): + bits_allocated = 8 + frame = np.ones((48, 32, 3), dtype=np.dtype(f'uint{bits_allocated}')) frame *= 255 compressed_frame = encode_frame( frame, transfer_syntax_uid=JPEG2000Lossless, bits_allocated=bits_allocated, bits_stored=bits_allocated, - photometric_interpretation='YBR_FULL', + photometric_interpretation='YBR_RCT', pixel_representation=0, planar_configuration=0 ) @@ -181,15 +240,43 @@ def test_jpeg2000_rgb(self): samples_per_pixel=frame.shape[2], bits_allocated=bits_allocated, bits_stored=bits_allocated, - photometric_interpretation='YBR_FULL', + photometric_interpretation='YBR_RCT', pixel_representation=0, planar_configuration=0 ) np.testing.assert_array_equal(frame, decoded_frame) - def test_jpeg2000_monochrome(self): + def test_jpeg2000lossless_monochrome(self): bits_allocated = 16 - frame = np.zeros((16, 32), dtype=np.dtype(f'uint{bits_allocated}')) + frame = np.zeros((48, 32), dtype=np.dtype(f'uint{bits_allocated}')) + compressed_frame = encode_frame( + frame, + transfer_syntax_uid=JPEG2000Lossless, + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='MONOCHROME2', + pixel_representation=0, + ) + assert compressed_frame.startswith(b"\xFF\x4F\xFF\x51") + assert compressed_frame.endswith(b'\xFF\xD9') + decoded_frame = decode_frame( + value=compressed_frame, + transfer_syntax_uid=JPEG2000Lossless, + rows=frame.shape[0], + columns=frame.shape[1], + samples_per_pixel=1, + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='MONOCHROME2', + pixel_representation=0, + planar_configuration=0 + ) + np.testing.assert_array_equal(frame, decoded_frame) + + def test_jpeg2000lossless_single_bit(self): + bits_allocated = 1 + frame = np.zeros((48, 32), dtype=np.dtype(f'uint8')) + frame[12:45, 3:6] = 1 compressed_frame = encode_frame( frame, transfer_syntax_uid=JPEG2000Lossless, @@ -224,7 +311,7 @@ def test_jpegls_rgb(self): transfer_syntax_uid=JPEGLSLossless, bits_allocated=bits_allocated, bits_stored=bits_allocated, - photometric_interpretation='YBR_FULL', + photometric_interpretation='RGB', pixel_representation=0, planar_configuration=0 ) @@ -238,7 +325,7 @@ def test_jpegls_rgb(self): samples_per_pixel=frame.shape[2], bits_allocated=bits_allocated, bits_stored=bits_allocated, - photometric_interpretation='YBR_FULL', + photometric_interpretation='RGB', pixel_representation=0, planar_configuration=0 ) @@ -272,6 +359,64 @@ def test_jpegls_monochrome(self): ) np.testing.assert_array_equal(frame, decoded_frame) + def test_jpeglsnearlossless_rgb(self): + pytest.importorskip("libjpeg") + bits_allocated = 8 + frame = np.ones((16, 32, 3), dtype=np.dtype(f'uint{bits_allocated}')) + frame *= 255 + compressed_frame = encode_frame( + frame, + transfer_syntax_uid=JPEGLSNearLossless, + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='RGB', + pixel_representation=0, + planar_configuration=0 + ) + assert compressed_frame.startswith(b'\xFF\xD8') + assert compressed_frame.endswith(b'\xFF\xD9') + decoded_frame = decode_frame( + value=compressed_frame, + transfer_syntax_uid=JPEGLSNearLossless, + rows=frame.shape[0], + columns=frame.shape[1], + samples_per_pixel=frame.shape[2], + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='RGB', + pixel_representation=0, + planar_configuration=0 + ) + np.testing.assert_allclose(frame, decoded_frame) + + def test_jpeglsnearlossless_monochrome(self): + pytest.importorskip("libjpeg") + bits_allocated = 16 + frame = np.zeros((16, 32), dtype=np.dtype(f'uint{bits_allocated}')) + compressed_frame = encode_frame( + frame, + transfer_syntax_uid=JPEGLSNearLossless, + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='MONOCHROME2', + pixel_representation=0, + ) + assert compressed_frame.startswith(b'\xFF\xD8') + assert compressed_frame.endswith(b'\xFF\xD9') + decoded_frame = decode_frame( + value=compressed_frame, + transfer_syntax_uid=JPEGLSNearLossless, + rows=frame.shape[0], + columns=frame.shape[1], + samples_per_pixel=1, + bits_allocated=bits_allocated, + bits_stored=bits_allocated, + photometric_interpretation='MONOCHROME2', + pixel_representation=0, + planar_configuration=0 + ) + np.testing.assert_allclose(frame, decoded_frame) + def test_jpeg_rgb_wrong_photometric_interpretation(self): frame = np.ones((16, 32, 3), dtype=np.uint8) with pytest.raises(ValueError): diff --git a/tests/test_pm.py b/tests/test_pm.py index ae18d5ce..ee2a5f49 100644 --- a/tests/test_pm.py +++ b/tests/test_pm.py @@ -6,7 +6,7 @@ import numpy as np import pytest from pydicom import dcmread -from pydicom.data import get_testdata_files +from pydicom.data import get_testdata_file, get_testdata_files from pydicom.sr.codedict import codes from pydicom.sr.coding import Code from pydicom.uid import ( @@ -232,6 +232,10 @@ def setUp(self): str(data_dir.joinpath('test_files', 'ct_image.dcm')) ) + self._ct_multiframe_image = dcmread( + get_testdata_file('eCT_Supplemental.dcm') + ) + self._sm_image = dcmread( str(data_dir.joinpath('test_files', 'sm_image.dcm')) ) @@ -337,6 +341,11 @@ def test_multi_frame_sm_image_single_native(self): window_width=window_width, content_label=content_label ) + + # Work around pydicom 3 decoding issue (should be able to remove this + # soon) + pmap.pixel_array_options(use_v2_backend=True) + assert pmap.SOPClassUID == '1.2.840.10008.5.1.4.1.1.30' assert pmap.SOPInstanceUID == self._sop_instance_uid assert pmap.SeriesInstanceUID == self._series_instance_uid @@ -549,7 +558,7 @@ def test_multi_frame_sm_image_ushort_encapsulated_jpeg2000(self): pixel_array = np.random.randint( low=0, high=2**8, - size=self._sm_image.pixel_array.shape[:3], + size=self._ct_multiframe_image.pixel_array.shape[:3], dtype=np.uint8 ) window_center = 128 @@ -564,7 +573,7 @@ def test_multi_frame_sm_image_ushort_encapsulated_jpeg2000(self): slope=1 ) pmap = ParametricMap( - [self._sm_image], + [self._ct_multiframe_image], pixel_array, self._series_instance_uid, self._series_number, @@ -651,6 +660,11 @@ def test_single_frame_ct_image_double(self): window_width=window_width, ) assert pmap.BitsAllocated == 64 + + # Work around pydicom 3 decoding issue (should be able to remove this + # soon) + pmap.pixel_array_options(use_v2_backend=True) + assert np.array_equal(pmap.pixel_array, pixel_array) def test_single_frame_ct_image_ushort_native(self): @@ -766,6 +780,10 @@ def test_series_single_frame_ct_image_single(self): window_width=window_width, ) + # Work around pydicom 3 decoding issue (should be able to remove this + # soon) + pmap.pixel_array_options(use_v2_backend=True) + assert np.array_equal(pmap.pixel_array, pixel_array) def test_multi_frame_sm_image_with_spatial_positions_not_preserved(self): diff --git a/tests/test_sc.py b/tests/test_sc.py index b2c63b60..c0a05b74 100644 --- a/tests/test_sc.py +++ b/tests/test_sc.py @@ -4,13 +4,15 @@ import numpy as np import pytest -from pydicom.encaps import generate_pixel_data_frame +from pydicom.encaps import generate_fragmented_frames from pydicom.filereader import dcmread from pydicom.uid import ( RLELossless, JPEGBaseline8Bit, + JPEG2000, JPEG2000Lossless, JPEGLSLossless, + JPEGLSNearLossless, ) from pydicom.valuerep import DA, TM @@ -65,7 +67,7 @@ def get_array_after_writing(instance): pixel_representation=ds.PixelRepresentation, planar_configuration=getattr(ds, 'PlanarConfiguration', None) ) - for value in generate_pixel_data_frame(instance.PixelData) + for (value, ) in generate_fragmented_frames(instance.PixelData) ] if len(decoded_frame_arrays) > 1: return np.stack(decoded_frame_arrays) @@ -373,7 +375,7 @@ def test_rgb_jpeg_baseline(self): reread_frame = self.get_array_after_writing(instance) np.testing.assert_allclose(frame, reread_frame, rtol=1.2) - def test_monochrome_jpeg2000(self): + def test_monochrome_jpeg2000lossless(self): bits_allocated = 8 photometric_interpretation = 'MONOCHROME2' coordinate_system = 'PATIENT' @@ -400,9 +402,37 @@ def test_monochrome_jpeg2000(self): frame ) + def test_monochrome_jpeg2000(self): + bits_allocated = 8 + photometric_interpretation = 'MONOCHROME2' + coordinate_system = 'PATIENT' + frame = np.random.randint(0, 256, size=(256, 256), dtype=np.uint8) + instance = SCImage( + pixel_array=frame, + photometric_interpretation=photometric_interpretation, + bits_allocated=bits_allocated, + coordinate_system=coordinate_system, + study_instance_uid=self._study_instance_uid, + series_instance_uid=self._series_instance_uid, + sop_instance_uid=self._sop_instance_uid, + series_number=self._series_number, + instance_number=self._instance_number, + manufacturer=self._manufacturer, + patient_orientation=self._patient_orientation, + transfer_syntax_uid=JPEG2000 + ) + + assert instance.file_meta.TransferSyntaxUID == JPEG2000 + + assert np.allclose( + self.get_array_after_writing(instance), + frame, + atol=2 + ) + def test_rgb_jpeg2000(self): bits_allocated = 8 - photometric_interpretation = 'YBR_FULL' + photometric_interpretation = 'YBR_RCT' coordinate_system = 'PATIENT' frame = np.random.randint(0, 256, size=(256, 256, 3), dtype=np.uint8) instance = SCImage( @@ -455,10 +485,39 @@ def test_monochrome_jpegls(self): frame ) + def test_monochrome_jpegls(self): + pytest.importorskip("libjpeg") + bits_allocated = 16 + photometric_interpretation = 'MONOCHROME2' + coordinate_system = 'PATIENT' + frame = np.random.randint(0, 2**16, size=(256, 256), dtype=np.uint16) + instance = SCImage( + pixel_array=frame, + photometric_interpretation=photometric_interpretation, + bits_allocated=bits_allocated, + coordinate_system=coordinate_system, + study_instance_uid=self._study_instance_uid, + series_instance_uid=self._series_instance_uid, + sop_instance_uid=self._sop_instance_uid, + series_number=self._series_number, + instance_number=self._instance_number, + manufacturer=self._manufacturer, + patient_orientation=self._patient_orientation, + transfer_syntax_uid=JPEGLSNearLossless + ) + + assert instance.file_meta.TransferSyntaxUID == JPEGLSNearLossless + + assert np.allclose( + self.get_array_after_writing(instance), + frame, + atol=2 + ) + def test_rgb_jpegls(self): pytest.importorskip("libjpeg") bits_allocated = 8 - photometric_interpretation = 'YBR_FULL' + photometric_interpretation = 'RGB' coordinate_system = 'PATIENT' frame = np.random.randint(0, 256, size=(256, 256, 3), dtype=np.uint8) instance = SCImage( diff --git a/tests/test_seg.py b/tests/test_seg.py index a546f118..4eb7469b 100644 --- a/tests/test_seg.py +++ b/tests/test_seg.py @@ -657,6 +657,22 @@ def setUp(self): self._sm_image = dcmread( str(data_dir.joinpath('test_files', 'sm_image.dcm')) ) + + # Hack around the openjpeg bug encoding images smaller than 32 pixels + frame = self._sm_image.pixel_array + frame = np.pad(frame, ((0, 0), (12, 12), (12, 12), (0, 0))) + self._sm_image.PixelData = frame.flatten().tobytes() + self._sm_image.TotalPixelMatrixRows = ( + frame.shape[1] * + int(self._sm_image.TotalPixelMatrixRows / self._sm_image.Rows) + ) + self._sm_image.TotalPixelMatrixColumns = ( + frame.shape[2] * + int(self._sm_image.TotalPixelMatrixColumns / self._sm_image.Columns) + ) + self._sm_image.Rows = frame.shape[1] + self._sm_image.Columns = frame.shape[2] + # Override te existing ImageOrientationSlide to make the frame ordering # simpler for the tests self._sm_pixel_array = np.zeros( @@ -697,6 +713,16 @@ def setUp(self): ct_series, key=lambda x: x.ImagePositionPatient[2] ) + + # Hack around the fact that the images are too small to be encoded by + # openjpeg + for im in self._ct_series: + frame = im.pixel_array + frame = np.pad(frame, ((8, 8), (8, 8))) + im.Rows = frame.shape[0] + im.Columns = frame.shape[1] + im.PixelData = frame.flatten().tobytes() + self._ct_series_mask_array = np.zeros( (len(self._ct_series), ) + self._ct_series[0].pixel_array.shape, dtype=bool @@ -725,7 +751,13 @@ def setUp(self): # Fixtures to use to parametrize segmentation creation # Using this fixture mechanism, we can parametrize class methods @staticmethod - @pytest.fixture(params=[ExplicitVRLittleEndian, ImplicitVRLittleEndian]) + @pytest.fixture( + params=[ + ExplicitVRLittleEndian, + ImplicitVRLittleEndian, + JPEG2000Lossless, + ] + ) def binary_transfer_syntax_uid(request): return request.param @@ -1519,10 +1551,8 @@ def segmentation_type(request): @pytest.fixture( params=[ None, - (10, 10), - (10, 25), - (25, 25), - (30, 30), + (32, 32), + (48, 48), ]) def tile_size(request): return request.param @@ -1579,7 +1609,10 @@ def test_construction_autotile( else: omit_empty_frames_values = [False, True] - transfer_syntax_uids = [ExplicitVRLittleEndian] + transfer_syntax_uids = [ + ExplicitVRLittleEndian, + JPEG2000Lossless, + ] if segmentation_type.value == 'FRACTIONAL': try: import libjpeg # noqa: F401 @@ -1587,7 +1620,6 @@ def test_construction_autotile( pass else: transfer_syntax_uids += [ - JPEG2000Lossless, JPEGLSLossless, ] diff --git a/tests/test_sr.py b/tests/test_sr.py index 4f05cabb..e79d4cc1 100644 --- a/tests/test_sr.py +++ b/tests/test_sr.py @@ -5178,7 +5178,7 @@ def test_ct_construction(self): value_item = group[4].MeasuredValueSequence[0] unit_code_item = value_item.MeasurementUnitsCodeSequence[0] assert unit_code_item.CodeValue == 'mm' - assert unit_code_item.CodeMeaning == 'millimeter' + assert unit_code_item.CodeMeaning == 'mm' assert unit_code_item.CodingSchemeDesignator == 'UCUM' assert isinstance(group[5], NumContentItem) assert group[5].name == codes.DCM.VerticalPixelSpacing @@ -5189,7 +5189,7 @@ def test_ct_construction(self): value_item = group[6].MeasuredValueSequence[0] unit_code_item = value_item.MeasurementUnitsCodeSequence[0] assert unit_code_item.CodeValue == 'mm' - assert unit_code_item.CodeMeaning == 'millimeter' + assert unit_code_item.CodeMeaning == 'mm' assert unit_code_item.CodingSchemeDesignator == 'UCUM' assert isinstance(group[7], NumContentItem) assert group[7].name == codes.DCM.SliceThickness diff --git a/tests/utils.py b/tests/utils.py index f1a18c16..f70233a8 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -7,15 +7,17 @@ def write_and_read_dataset(dataset: Dataset): """Write DICOM dataset to buffer and read it back from buffer.""" clone = Dataset(dataset) - clone.is_little_endian = True if hasattr(dataset, 'file_meta'): clone.file_meta = FileMetaDataset(dataset.file_meta) - if dataset.file_meta.TransferSyntaxUID == '1.2.840.10008.1.2': - clone.is_implicit_VR = True - else: - clone.is_implicit_VR = False + little_endian = None + implicit_vr = None else: - clone.is_implicit_VR = False + little_endian = True + implicit_vr = True with BytesIO() as fp: - clone.save_as(fp) + clone.save_as( + fp, + implicit_vr=implicit_vr, + little_endian=little_endian, + ) return dcmread(fp, force=True)