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

Allow reference of optical path for annotations measurements #194

Open
wants to merge 30 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
4cd7fad
Add warning when empty segmentation is passed with omit_empty_frames
Jul 6, 2022
2c7a629
Change UserWarning to logger.warning
CPBridge Jul 7, 2022
1e4334a
Allow reference of optical path for measurements
hackermd Aug 5, 2022
2609498
Add property to access referenced images
hackermd Aug 5, 2022
da2f5b1
Provide referenced images for measurements
hackermd Aug 5, 2022
366c58a
Remove module level imports of module data (#196)
CPBridge Aug 15, 2022
ed5901f
Minor fixes to segmentation (#195)
CPBridge Aug 15, 2022
bfbd807
Implement TID 1601 Image Library Entry (#83)
seandoyle Aug 19, 2022
d03f4c8
Increase package version
hackermd Aug 19, 2022
6d80f5a
Move pylibjpeg-libjpeg to optional dependency
Oct 10, 2022
3a04c75
Add skips for tests that need libjpeg, restructure segmentation tests
Oct 10, 2022
d1e632d
Remove unnecessary import error
Oct 10, 2022
89d1343
Update installation docs
Oct 10, 2022
2656162
Bump python version in installation docs to match setup.py
Oct 10, 2022
0ca5784
Add libjpeg to CI workflow
Oct 10, 2022
d1a904e
Add workflow with and without libjpeg
Oct 10, 2022
320b4f4
Apply suggestions from code review
CPBridge Oct 11, 2022
1422a25
Update docs/installation.rst
CPBridge Oct 11, 2022
7e08517
remove openjpeg from deps, fix installation guide
Oct 11, 2022
c03598d
Add citation file (#204)
hackermd Oct 28, 2022
2152c19
Use deepcopy for CodedConcept.from_dataset()
Oct 28, 2022
14365c3
Merge pull request #205 from herrmannlab/fix_coded_concept_copy
CPBridge Nov 9, 2022
9026587
Merge pull request #201 from herrmannlab/make_pylibjpeg-libjpeg_optional
CPBridge Nov 9, 2022
4b13338
Merge pull request #181 from herrmannlab/empty_seg_error_message
CPBridge Nov 9, 2022
d36e2a5
Increase package version for release (#206)
CPBridge Nov 9, 2022
91e616d
Allow reference of optical path for measurements
hackermd Aug 5, 2022
615d38c
Add property to access referenced images
hackermd Aug 5, 2022
d32c006
Provide referenced images for measurements
hackermd Aug 5, 2022
bbd1a01
Merge branch 'enhancement/annotations-intensity-measurements' of gith…
hackermd Nov 18, 2022
1ff6985
Update src/highdicom/ann/content.py
hackermd Apr 1, 2023
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
3 changes: 2 additions & 1 deletion .github/workflows/run_unit_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ jobs:
strategy:
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
dependencies: [".", "'.[libjpeg]'"]

steps:
- uses: actions/checkout@v2
Expand All @@ -27,7 +28,7 @@ jobs:
run: |
python -m pip install --upgrade pip setuptools
pip install -r requirements_test.txt
pip install .
pip install ${{ matrix.dependencies }}
- name: Lint with flake8
run: |
flake8 --exclude='bin,build,.eggs,src/highdicom/_*'
Expand Down
42 changes: 42 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
cff-version: 1.2.0
message: "If you use this software, please cite our paper."
authors:
- family-names: "Herrmann"
given-names: "Markus D."
- family-names: "Bridge"
given-names: "Christopher P."
- family-names: "Fedorov"
given-names: "Andriy Y."
- family-names: "Pieper"
given-names: "Steven"
- family-names: "Doyle"
given-names: "Sean W."
- family-names: "Gorman"
given-names: "Chris"
preferred-citation:
type: article
authors:
- family-names: "Bridge"
given-names: "Christopher P."
orcid: "https://orcid.org/0000-0002-2242-351X"
- family-names: "Gorman"
given-names: "Chris"
- family-names: "Pieper"
given-names: "Steven"
- family-names: "Doyle"
given-names: "Sean W."
- family-names: "Lennerz"
given-names: "Jochen K."
- family-names: "Kalpathy-Cramer"
given-names: "Jayashree "
- family-names: "Clunie"
given-names: "David A."
- family-names: "Fedorov"
given-names: "Andriy Y."
- family-names: "Herrmann"
given-names: "Markus D."
orcid: "https://orcid.org/0000-0002-7257-9205"
title: "Highdicom: a Python Library for Standardized Encoding of Image Annotations and Machine Learning Model Outputs in Pathology and Radiology"
journal: "J Digit Imaging"
year: 2022
doi: 10.1007/s10278-022-00683-y
Binary file added data/test_files/dx_image.dcm
Binary file not shown.
19 changes: 17 additions & 2 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Installation guide
Requirements
------------

* `Python <https://www.python.org/>`_ (version 3.5 or higher)
* `Python <https://www.python.org/>`_ (version 3.6 or higher)
* Python package manager `pip <https://pip.pypa.io/en/stable/>`_

.. _installation:
Expand All @@ -22,7 +22,22 @@ Pre-build package available at PyPi:

pip install highdicom

Source code available at Github:
The library relies on the underlying ``pydicom`` package for decoding of pixel
data, which internally delegates the task to either the ``pillow`` or the
``pylibjpeg`` packages. Since ``pillow`` is a dependency of *highdicom* and
will automatically be installed, some transfer syntax can thus be readily
decoded and encoded (baseline JPEG, JPEG-2000, JPEG-LS). Support for additional
transfer syntaxes (e.g., lossless JPEG) requires installation of the
``pylibjpeg`` package as well as the ``pylibjpeg-libjpeg`` and
``pylibjpeg-openjpeg`` packages. Since ``pylibjpeg-libjpeg`` is licensed under
a copyleft GPL v3 license, it is not installed by default when you install
*highdicom*. To install the ``pylibjpeg`` packages along with *highdicom*, use

.. code-block:: none

pip install highdicom[libjpeg]

Install directly from source code (available on Github):

.. code-block:: none

Expand Down
16 changes: 16 additions & 0 deletions docs/release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,19 @@ error.
Similarly, as of highdicom 0.18.0, it is no longer possible to pass datasets
with a Big Endian transfer syntax to the `from_dataset` methods of any of the
:class:`highdicom.SOPClass` subclasses.

.. _update-image-library:

Change in MeasurementReport constructor for TID 1601 enhancement
----------------------------------------------------------------

A breaking change was made after highdicom 0.18.4 in the creation of Image
Library TID 1601 objects.
Previously the Imag Library was constructed by explicitly
passing a `pydicom.sequence.Sequence` of `ImageLibraryEntryDescriptors`
objects to the :class:`highdicom.sr.MeasurementReport` constructor in the `image_library_groups`
argument.
Now a `pydicom.sequence.Sequence` of `pydicom.dataset.Dataset`
objects is passed in the `referenced_images` argument and the
ImageLibrary components are created internally by highdicom.
This standardizes the content of the Image Library subcomponents.
2 changes: 1 addition & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ Derive a Segmentation image from a multi-frame Slide Microscopy (SM) image:
)

# Create the Segmentation instance
seg_dataset = Segmentation(
seg_dataset = hd.seg.Segmentation(
source_images=[image_dataset],
pixel_array=mask,
segmentation_type=hd.seg.SegmentationTypeValues.BINARY,
Expand Down
10 changes: 7 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,12 @@ def get_version():
'numpy>=1.19',
'pillow>=8.3',
'pillow-jpls>=1.0',
'pylibjpeg>=1.4',
'pylibjpeg-libjpeg>=1.3',
'pylibjpeg-openjpeg>=1.2',
],
extras_requires={
'libjpeg': [
'pylibjpeg>=1.4',
'pylibjpeg-libjpeg>=1.3',
'pylibjpeg-openjpeg>=1.2'
],
},
)
17 changes: 10 additions & 7 deletions src/highdicom/_module_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@

from pydicom import Dataset

from highdicom._iods import IOD_MODULE_MAP, SOP_CLASS_UID_IOD_KEY_MAP
from highdicom._modules import MODULE_ATTRIBUTE_MAP
from highdicom._iods import (
IOD_MODULE_MAP,
SOP_CLASS_UID_IOD_KEY_MAP
)


# Allowed values for the type of an attribute
class AttributeTypeValues(Enum):
Expand Down Expand Up @@ -168,6 +161,7 @@ def construct_module_tree(module: str) -> Dict[str, Any]:
dictionary that forms an item in the next level of the tree structure.

"""
from highdicom._modules import MODULE_ATTRIBUTE_MAP
if module not in MODULE_ATTRIBUTE_MAP:
raise AttributeError(f"No such module found: '{module}'.")
tree: Dict[str, Any] = {'attributes': {}}
Expand Down Expand Up @@ -205,6 +199,10 @@ def get_module_usage(


"""
from highdicom._iods import (
IOD_MODULE_MAP,
SOP_CLASS_UID_IOD_KEY_MAP
)
try:
iod_name = SOP_CLASS_UID_IOD_KEY_MAP[sop_class_uid]
except KeyError as e:
Expand Down Expand Up @@ -235,6 +233,11 @@ def is_attribute_in_iod(attribute: str, sop_class_uid: str) -> bool:
specified by the sop_class_uid. False otherwise.

"""
from highdicom._iods import (
IOD_MODULE_MAP,
SOP_CLASS_UID_IOD_KEY_MAP
)
from highdicom._modules import MODULE_ATTRIBUTE_MAP
try:
iod_name = SOP_CLASS_UID_IOD_KEY_MAP[sop_class_uid]
except KeyError as e:
Expand Down
60 changes: 56 additions & 4 deletions src/highdicom/ann/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
AnnotationGroupGenerationTypeValues,
GraphicTypeValues,
)
from highdicom.content import AlgorithmIdentificationSequence
from highdicom.content import (
AlgorithmIdentificationSequence,
ReferencedImageSequence,
)
from highdicom.sr.coding import CodedConcept
from highdicom.uid import UID
from highdicom._module_utils import check_required_attributes
Expand All @@ -25,7 +28,8 @@ def __init__(
self,
name: Union[Code, CodedConcept],
values: np.ndarray,
unit: Union[Code, CodedConcept]
unit: Union[Code, CodedConcept],
referenced_images: Optional[ReferencedImageSequence] = None
) -> None:
"""
Parameters
Expand All @@ -40,6 +44,9 @@ def __init__(
unit: Union[highdicom.sr.CodedConcept, pydicom.sr.coding.Code], optional
Coded units of measurement (see :dcm:`CID 7181 <part16/sect_CID_7181.html>`
"Abstract Multi-dimensional Image Model Component Units")
referenced_images: Union[highdicom.ReferencedImageSequence, None], optional
Referenced image to which the measurement applies. Should only be
provided for intensity measurements.

""" # noqa: E501
super().__init__()
Expand All @@ -61,6 +68,22 @@ def __init__(
item.AnnotationIndexList = stored_indices.tobytes()
self.MeasurementValuesSequence = [item]

if referenced_images is not None:
if len(referenced_images) == 0:
raise ValueError(
'Argument "referenced_images" must contain one item.'
)
elif len(referenced_images) > 1:
raise ValueError(
'Argument "referenced_images" must contain only one item.'
)
if not isinstance(referenced_images, ReferencedImageSequence):
raise TypeError(
'Argument "referenced_images" must have type '
'ReferencedImageSequence.'
)
self.ReferencedImageSequence = referenced_images

@property
def name(self) -> CodedConcept:
"""highdicom.sr.CodedConcept: coded name"""
Expand All @@ -71,6 +94,14 @@ def unit(self) -> CodedConcept:
"""highdicom.sr.CodedConcept: coded unit"""
return self.MeasurementUnitsCodeSequence[0]

@property
def referenced_images(self) -> Union[ReferencedImageSequence, None]:
"""Union[highdicom.ReferencedImageSequence, None]: referenced images"""
if hasattr(self, 'ReferencedImageSequence'):
return ReferencedImageSequence.from_sequence(self.ReferencedImageSequence)
else:
return None

def get_values(self, number_of_annotations: int) -> np.ndarray:
"""Get measured values for annotations.

Expand Down Expand Up @@ -151,6 +182,11 @@ def from_dataset(cls, dataset: Dataset) -> 'Measurements':
measurements.MeasurementUnitsCodeSequence[0]
)
]
if hasattr(measurements, 'ReferencedImageSequence'):
measurements.ReferencedImageSequence = \
ReferencedImageSequence.from_sequence(
measurements.ReferencedImageSequence
)

return cast(Measurements, measurements)

Expand Down Expand Up @@ -520,6 +556,12 @@ def get_graphic_data(
)
else:
if coordinate_type == AnnotationCoordinateTypeValues.SCOORD:
if hasattr(self, 'CommonZCoordinateValue'):
raise ValueError(
'The annotation group contains the '
'"Common Z Coordinate Value" element and therefore '
'cannot have Annotation Coordinate Type "2D".'
)
coordinate_dimensionality = 2
else:
coordinate_dimensionality = 3
Expand Down Expand Up @@ -633,7 +675,10 @@ def get_measurements(
self,
name: Optional[Union[Code, CodedConcept]] = None
) -> Tuple[
List[CodedConcept], np.ndarray, List[CodedConcept]
List[CodedConcept],
np.ndarray,
List[CodedConcept],
List[Union[ReferencedImageSequence, None]]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NB backwards incompatible change

]:
"""Get measurements.

Expand All @@ -654,6 +699,8 @@ def get_measurements(
given annotation.
units: List[highdicom.sr.CodedConcept]
Units of measurements
referenced_images: List[highdicom.ReferencedImageSequence, None]
Referenced images

""" # noqa: E501
number_of_annotations = self.number_of_annotations
Expand All @@ -675,11 +722,16 @@ def get_measurements(
item.unit for item in self.MeasurementsSequence
if name is None or item.name == name
]
referenced_images = [
item.referenced_images for item in self.MeasurementsSequence
if name is None or item.name == name
]
else:
value_array = np.empty((number_of_annotations, 0), np.float32)
names = []
units = []
return (names, value_array, units)
referenced_images = []
return (names, value_array, units, referenced_images)

def _get_coordinate_index(
self,
Expand Down
4 changes: 2 additions & 2 deletions src/highdicom/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@
)
from highdicom.valuerep import check_person_name
from highdicom.version import __version__
from highdicom._iods import IOD_MODULE_MAP, SOP_CLASS_UID_IOD_KEY_MAP
from highdicom._modules import MODULE_ATTRIBUTE_MAP
from highdicom._module_utils import is_attribute_in_iod


Expand Down Expand Up @@ -289,6 +287,8 @@ def _copy_root_attributes_of_module(
DICOM Module (e.g., ``"General Series"`` or ``"Specimen"``)

"""
from highdicom._iods import IOD_MODULE_MAP, SOP_CLASS_UID_IOD_KEY_MAP
from highdicom._modules import MODULE_ATTRIBUTE_MAP
logger.info(
'copy {}-related attributes from dataset "{}"'.format(
ie, dataset.SOPInstanceUID
Expand Down
Loading