Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Preparation for 0.23.0 release #305

Merged
merged 19 commits into from
Sep 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/run_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Binary file modified data/test_files/seg_image_sm_control.dcm
Binary file not shown.
Binary file modified data/test_files/seg_image_sm_dots.dcm
Binary file not shown.
Binary file modified data/test_files/seg_image_sm_dots_tiled_full.dcm
Binary file not shown.
Binary file modified data/test_files/seg_image_sm_numbers.dcm
Binary file not shown.
37 changes: 18 additions & 19 deletions docs/seg.rst
Original file line number Diff line number Diff line change
Expand Up @@ -807,39 +807,38 @@ 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
``"BINARY"`` 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*
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
---------------

Expand Down
26 changes: 13 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,15 @@ 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" },
]
maintainers = [
{ name = "Markus D. Herrmann" },
{ name = "Christopher P. Bridge" },
]
license = { text = "LICENSE" }
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Science/Research",
Expand All @@ -23,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",
Expand All @@ -35,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",
]
Expand All @@ -67,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.*"
Expand Down
22 changes: 22 additions & 0 deletions src/highdicom/_module_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,3 +281,25 @@ def does_iod_have_pixel_data(sop_class_uid: str) -> bool:
return any(
is_attribute_in_iod(attr, sop_class_uid) for attr in pixel_attrs
)


def is_multiframe_image(dataset: Dataset):
"""Determine whether an image is a multiframe image.
The definition used is whether the IOD allows for multiple frames, not
whether this particular instance has more than one frame.

Parameters
----------
dataset: pydicom.Dataset
A dataset to check.

Returns
-------
bool:
Whether the image belongs to a multiframe IOD.

"""
return is_attribute_in_iod(
'PerFrameFunctionalGroupsSequence',
dataset.SOPClassUID,
)
3 changes: 0 additions & 3 deletions src/highdicom/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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())
Expand Down
24 changes: 17 additions & 7 deletions src/highdicom/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@

import numpy as np
from pydicom.dataset import Dataset
from pydicom import DataElement
from pydicom.sequence import Sequence as DataElementSequence
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,
Expand Down Expand Up @@ -469,18 +470,27 @@ def __init__(
'Position in Pixel Matrix must be specified for '
'slide coordinate system.'
)
col_position, row_position = pixel_matrix_position
x, y, z = image_position
item.XOffsetInSlideCoordinateSystem = DS(x, auto_format=True)
item.YOffsetInSlideCoordinateSystem = DS(y, auto_format=True)
item.ZOffsetInSlideCoordinateSystem = DS(z, auto_format=True)
col_position, row_position = pixel_matrix_position
if row_position < 0 or col_position < 0:
raise ValueError(
'Both items in "pixel_matrix_position" must be positive '
'integers.'
)
item.RowPositionInTotalImagePixelMatrix = row_position
item.ColumnPositionInTotalImagePixelMatrix = col_position

# Use hard-coded tags to avoid the keyword dictionary lookup
# (this constructor is called a large number of times in large
# multiframe images, so some optimization makes sense)
x_tag = 0x0040072a # XOffsetInSlideCoordinateSystem
y_tag = 0x0040073a # YOffsetInSlideCoordinateSystem
z_tag = 0x0040074a # ZOffsetInSlideCoordinateSystem
row_tag = 0x0048021f # RowPositionInTotalImagePixelMatrix
column_tag = 0x0048021e # ColumnPositionInTotalImagePixelMatrix
item.add(DataElement(x_tag, 'DS', DS(x, auto_format=True)))
item.add(DataElement(y_tag, 'DS', DS(y, auto_format=True)))
item.add(DataElement(z_tag, 'DS', DS(z, auto_format=True)))
item.add(DataElement(row_tag, 'SL', int(row_position)))
item.add(DataElement(column_tag, 'SL', int(col_position)))
elif coordinate_system == CoordinateSystemNames.PATIENT:
item.ImagePositionPatient = [
DS(ip, auto_format=True) for ip in image_position
Expand Down
Loading
Loading