Skip to content

Commit 053bb64

Browse files
authored
Merge pull request #305 from ImagingDataCommons/v0.23.0dev
Preparation for 0.23.0 release
2 parents c97b3fb + 1ac75ce commit 053bb64

31 files changed

Lines changed: 3598 additions & 789 deletions

.github/workflows/run_unit_tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616
strategy:
1717
matrix:
18-
python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"]
18+
python-version: ["3.10", "3.11", "3.12"]
1919
dependencies: [".", "'.[libjpeg]'"]
2020

2121
steps:
54 Bytes
Binary file not shown.
70 Bytes
Binary file not shown.
12 Bytes
Binary file not shown.
70 Bytes
Binary file not shown.

docs/seg.rst

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -807,39 +807,38 @@ We recommend that if you do this, you specify ``max_fractional_value=1`` to
807807
clearly communicate that the segmentation is inherently binary in nature.
808808

809809
Why would you want to make this seemingly rather strange choice? Well,
810-
``"FRACTIONAL"`` SEGs tend to compress *much* better than ``"BINARY"`` ones
811-
(see next section). Note however, that this is arguably an misuse of the intent
812-
of the standard, so *caveat emptor*.
810+
``"FRACTIONAL"`` SEGs tend to compress better than ``"BINARY"`` ones (see next
811+
section). Note however, that this is arguably an misuse of the intent of the
812+
standard, so *caveat emptor*. Also note that while this used to be a more
813+
serious issue it is less serious now that ``"JPEG2000Lossless"`` compression is
814+
now supported for ``"BINARY"`` segmentations as of highdicom v0.23.0.
813815

814816
Compression
815817
-----------
816818

817819
The types of pixel compression available in segmentation images depends on the
818-
segmentation type. Pixels in a ``"BINARY"`` segmentation image are "bit-packed"
819-
such that 8 pixels are grouped into 1 byte in the stored array. If a given frame
820-
contains a number of pixels that is not divisible by 8 exactly, a single byte
820+
segmentation type.
821+
822+
Pixels in an uncompressed ``"BINARY"`` segmentation image are "bit-packed" such
823+
that 8 pixels are grouped into 1 byte in the stored array. If a given frame
824+
contains a number of pixels that is not divisible by 8 exactly, a single byte
821825
will straddle a frame boundary into the next frame if there is one, or the byte
822826
will be padded with zeroes of there are no further frames. This means that
823-
retrieving individual frames from segmentation images in which each frame
824-
size is not divisible by 8 becomes problematic. No further compression may be
825-
applied to frames of ``"BINARY"`` segmentation images.
827+
retrieving individual frames from segmentation images in which each frame size
828+
is not divisible by 8 becomes problematic. For this reason, as well as for
829+
space efficiency (sparse segmentations tend to compress very well), we
830+
therefore strongly recommend using ``"JPEG2000Lossless"`` compression with
831+
``"BINARY"`` segmentations. This is the only compression method currently
832+
supported for ``"BINARY"`` segmentations. However, beware that reading these
833+
single-bit JPEG 2000 images may not be supported by all other tools and
834+
viewers.
826835

827836
Pixels in ``"FRACTIONAL"`` segmentation images may be compressed using one of
828837
the lossless compression methods available within DICOM. Currently *highdicom*
829838
supports the following compressed transfer syntaxes when creating
830839
``"FRACTIONAL"`` segmentation images: ``"RLELossless"``,
831840
``"JPEG2000Lossless"``, and ``"JPEGLSLossless"``.
832841

833-
Note that there may be advantages to using ``"FRACTIONAL"`` segmentations to
834-
store segmentation images that are binary in nature (i.e. only taking values 0
835-
and 1):
836-
837-
- If the segmentation is very simple or sparse, the lossless compression methods
838-
available in ``"FRACTIONAL"`` images may be more effective than the
839-
"bit-packing" method required by ``"BINARY"`` segmentations.
840-
- The clear frame boundaries make retrieving individual frames from
841-
``"FRACTIONAL"`` image files possible.
842-
843842
Multiprocessing
844843
---------------
845844

pyproject.toml

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@ name = "highdicom"
77
dynamic = ["version"]
88
description = "High-level DICOM abstractions."
99
readme = "README.md"
10-
requires-python = ">=3.6"
10+
requires-python = ">=3.10"
1111
authors = [
1212
{ name = "Markus D. Herrmann" },
1313
]
1414
maintainers = [
1515
{ name = "Markus D. Herrmann" },
1616
{ name = "Christopher P. Bridge" },
1717
]
18+
license = { text = "LICENSE" }
1819
classifiers = [
1920
"Development Status :: 4 - Beta",
2021
"Intended Audience :: Science/Research",
@@ -23,10 +24,6 @@ classifiers = [
2324
"Operating System :: Microsoft :: Windows",
2425
"Operating System :: POSIX :: Linux",
2526
"Programming Language :: Python :: 3",
26-
"Programming Language :: Python :: 3.6",
27-
"Programming Language :: Python :: 3.7",
28-
"Programming Language :: Python :: 3.8",
29-
"Programming Language :: Python :: 3.9",
3027
"Programming Language :: Python :: 3.10",
3128
"Programming Language :: Python :: 3.11",
3229
"Programming Language :: Python :: 3.12",
@@ -35,16 +32,16 @@ classifiers = [
3532
]
3633
dependencies = [
3734
"numpy>=1.19",
38-
"pillow-jpls>=1.0",
3935
"pillow>=8.3",
40-
"pydicom>=2.3.0,!=2.4.0",
36+
"pydicom>=3.0.1",
37+
"pyjpegls>=1.0.0",
4138
]
4239

4340
[project.optional-dependencies]
4441
libjpeg = [
45-
"pylibjpeg-libjpeg>=1.3",
46-
"pylibjpeg-openjpeg>=1.2",
47-
"pylibjpeg>=1.4",
42+
"pylibjpeg-libjpeg>=2.1",
43+
"pylibjpeg-openjpeg>=2.0.0",
44+
"pylibjpeg>=2.0",
4845
]
4946
test = [
5047
"mypy==0.971",

src/highdicom/_module_utils.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,3 +281,25 @@ def does_iod_have_pixel_data(sop_class_uid: str) -> bool:
281281
return any(
282282
is_attribute_in_iod(attr, sop_class_uid) for attr in pixel_attrs
283283
)
284+
285+
286+
def is_multiframe_image(dataset: Dataset):
287+
"""Determine whether an image is a multiframe image.
288+
The definition used is whether the IOD allows for multiple frames, not
289+
whether this particular instance has more than one frame.
290+
291+
Parameters
292+
----------
293+
dataset: pydicom.Dataset
294+
A dataset to check.
295+
296+
Returns
297+
-------
298+
bool:
299+
Whether the image belongs to a multiframe IOD.
300+
301+
"""
302+
return is_attribute_in_iod(
303+
'PerFrameFunctionalGroupsSequence',
304+
dataset.SOPClassUID,
305+
)

src/highdicom/base.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,6 @@ def __init__(
139139
"Big Endian transfer syntaxes are retired and no longer "
140140
"supported by highdicom."
141141
)
142-
self.is_little_endian = True # backwards compatibility
143-
self.is_implicit_VR = transfer_syntax_uid.is_implicit_VR
144142

145143
# Include all File Meta Information required for writing SOP instance
146144
# to a file in PS3.10 format.
@@ -154,7 +152,6 @@ def __init__(
154152
'1.2.826.0.1.3680043.9.7433.1.1'
155153
)
156154
self.file_meta.ImplementationVersionName = f'highdicom{__version__}'
157-
self.fix_meta_info(enforce_standard=True)
158155
with BytesIO() as fp:
159156
write_file_meta_info(fp, self.file_meta, enforce_standard=True)
160157
self.file_meta.FileMetaInformationGroupLength = len(fp.getvalue())

src/highdicom/content.py

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
import numpy as np
88
from pydicom.dataset import Dataset
9+
from pydicom import DataElement
910
from pydicom.sequence import Sequence as DataElementSequence
1011
from pydicom.sr.coding import Code
1112
from pydicom.sr.codedict import codes
1213
from pydicom.valuerep import DS, format_number_as_ds
13-
from pydicom._storage_sopclass_uids import SegmentationStorage
14+
from pydicom.uid import SegmentationStorage
1415

1516
from highdicom.enum import (
1617
CoordinateSystemNames,
@@ -469,18 +470,27 @@ def __init__(
469470
'Position in Pixel Matrix must be specified for '
470471
'slide coordinate system.'
471472
)
472-
col_position, row_position = pixel_matrix_position
473473
x, y, z = image_position
474-
item.XOffsetInSlideCoordinateSystem = DS(x, auto_format=True)
475-
item.YOffsetInSlideCoordinateSystem = DS(y, auto_format=True)
476-
item.ZOffsetInSlideCoordinateSystem = DS(z, auto_format=True)
474+
col_position, row_position = pixel_matrix_position
477475
if row_position < 0 or col_position < 0:
478476
raise ValueError(
479477
'Both items in "pixel_matrix_position" must be positive '
480478
'integers.'
481479
)
482-
item.RowPositionInTotalImagePixelMatrix = row_position
483-
item.ColumnPositionInTotalImagePixelMatrix = col_position
480+
481+
# Use hard-coded tags to avoid the keyword dictionary lookup
482+
# (this constructor is called a large number of times in large
483+
# multiframe images, so some optimization makes sense)
484+
x_tag = 0x0040072a # XOffsetInSlideCoordinateSystem
485+
y_tag = 0x0040073a # YOffsetInSlideCoordinateSystem
486+
z_tag = 0x0040074a # ZOffsetInSlideCoordinateSystem
487+
row_tag = 0x0048021f # RowPositionInTotalImagePixelMatrix
488+
column_tag = 0x0048021e # ColumnPositionInTotalImagePixelMatrix
489+
item.add(DataElement(x_tag, 'DS', DS(x, auto_format=True)))
490+
item.add(DataElement(y_tag, 'DS', DS(y, auto_format=True)))
491+
item.add(DataElement(z_tag, 'DS', DS(z, auto_format=True)))
492+
item.add(DataElement(row_tag, 'SL', int(row_position)))
493+
item.add(DataElement(column_tag, 'SL', int(col_position)))
484494
elif coordinate_system == CoordinateSystemNames.PATIENT:
485495
item.ImagePositionPatient = [
486496
DS(ip, auto_format=True) for ip in image_position

0 commit comments

Comments
 (0)