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

Alternative SCImage Constructor #97

Merged
merged 3 commits into from
Aug 10, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 src/highdicom/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ def __init__(
# Patient
self.PatientID = patient_id
self.PatientName = patient_name
self.PatientBirthDate = patient_birth_date
self.PatientBirthDate = DA(patient_birth_date)
self.PatientSex = patient_sex

# Study
Expand Down
155 changes: 155 additions & 0 deletions src/highdicom/sc/sop.py
Original file line number Diff line number Diff line change
Expand Up @@ -367,3 +367,158 @@ def __init__(
self.PixelData = encapsulate([encoded_frame])
else:
self.PixelData = encoded_frame

@classmethod
def from_study_dataset(
Copy link
Collaborator

Choose a reason for hiding this comment

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

I find the name study_dataset confusing, since a data set encodes an instance. Can we call it reference_dataset or ref_dataset instead?

Copy link
Collaborator Author

@CPBridge CPBridge Aug 10, 2021

Choose a reason for hiding this comment

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

Totally agree. I knew study_dataset wasn't great but was drawing a blank on something better and was hoping you could suggest one. Thanks! Changed the method and its parameter to ref_dataset as suggested in 5cce71d.

cls,
study_dataset: Dataset,
pixel_array: np.ndarray,
photometric_interpretation: Union[
str,
PhotometricInterpretationValues
],
bits_allocated: int,
coordinate_system: Union[str, CoordinateSystemNames],
series_instance_uid: str,
series_number: int,
sop_instance_uid: str,
instance_number: int,
manufacturer: str,
pixel_spacing: Optional[Tuple[int, int]] = None,
laterality: Optional[Union[str, LateralityValues]] = None,
patient_orientation: Optional[
Union[
Tuple[str, str],
Tuple[
PatientOrientationValuesBiped,
PatientOrientationValuesBiped,
],
Tuple[
PatientOrientationValuesQuadruped,
PatientOrientationValuesQuadruped,
]
]
] = None,
anatomical_orientation_type: Optional[
Union[str, AnatomicalOrientationTypeValues]
] = None,
container_identifier: Optional[str] = None,
issuer_of_container_identifier: Optional[IssuerOfIdentifier] = None,
specimen_descriptions: Optional[
Sequence[SpecimenDescription]
] = None,
transfer_syntax_uid: str = ImplicitVRLittleEndian,
**kwargs: Any
) -> 'SCImage':
"""Constructor that copies patient and study from an existing dataset.

This provides a more concise way to construct an SCImage when an
existing dataset from the study is available. All patient- and study-
related attributes required by the main constructor are copied from the
``study_dataset``, if present.

The ``study_dataset`` may be any dataset
from the study to which the resulting SC image should belong, and
contain all the relevant patient and study metadata. It does not need to
be specifically related to the contents of the SCImage.

Parameters
----------
study_dataset: pydicom.dataset.Dataset
An existing dataset from the study to which the SCImage should
belong. Patient- and study-related metadata will be copied from
this dataset.
pixel_array: numpy.ndarray
Array of unsigned integer pixel values representing a single-frame
image; either a 2D grayscale image or a 3D color image
(RGB color space)
photometric_interpretation: Union[str, highdicom.enum.PhotometricInterpretationValues]
Interpretation of pixel data; either ``"MONOCHROME1"`` or
``"MONOCHROME2"`` for 2D grayscale images or ``"RGB"`` or
``"YBR_FULL"`` for 3D color images
bits_allocated: int
Number of bits that should be allocated per pixel value
coordinate_system: Union[str, highdicom.enum.CoordinateSystemNames]
Subject (``"PATIENT"`` or ``"SLIDE"``) that was the target of
imaging
series_instance_uid: str
Series Instance UID of the SC image series
series_number: Union[int, None]
Series Number of the SC image series
sop_instance_uid: str
SOP instance UID that should be assigned to the SC image instance
instance_number: int
Number that should be assigned to this SC image instance
manufacturer: str
Name of the manufacturer of the device that creates the SC image
instance (in a research setting this is typically the same
as `institution_name`)
pixel_spacing: Union[Tuple[int, int]], optional
Physical spacing in millimeter between pixels along the row and
column dimension
laterality: Union[str, highdicom.enum.LateralityValues, None], optional
Laterality of the examined body part
patient_orientation:
Union[Tuple[str, str], Tuple[highdicom.enum.PatientOrientationValuesBiped, highdicom.enum.PatientOrientationValuesBiped], Tuple[highdicom.enum.PatientOrientationValuesQuadruped, highdicom.enum.PatientOrientationValuesQuadruped], None], optional
Orientation of the patient along the row and column axes of the
image (required if `coordinate_system` is ``"PATIENT"``)
anatomical_orientation_type: Union[str, highdicom.enum.AnatomicalOrientationTypeValues, None], optional
Type of anatomical orientation of patient relative to image (may be
provide if `coordinate_system` is ``"PATIENT"`` and patient is
an animal)
container_identifier: Union[str], optional
Identifier of the container holding the specimen (required if
`coordinate_system` is ``"SLIDE"``)
issuer_of_container_identifier: Union[highdicom.IssuerOfIdentifier, None], optional
Issuer of `container_identifier`
specimen_descriptions: Union[Sequence[highdicom.SpecimenDescriptions], None], optional
Description of each examined specimen (required if
`coordinate_system` is ``"SLIDE"``)
transfer_syntax_uid: str, optional
UID of transfer syntax that should be used for encoding of
data elements. The following lossless compressed transfer syntaxes
are supported: RLE Lossless (``"1.2.840.10008.1.2.5"``).
**kwargs: Any, optional
Additional keyword arguments that will be passed to the constructor
of `highdicom.base.SOPClass`

Returns
-------
SCImage
Secondary capture image.

""" # noqa: E501
return cls(
pixel_array=pixel_array,
photometric_interpretation=photometric_interpretation,
bits_allocated=bits_allocated,
coordinate_system=coordinate_system,
study_instance_uid=study_dataset.StudyInstanceUID,
series_instance_uid=series_instance_uid,
series_number=series_number,
sop_instance_uid=sop_instance_uid,
instance_number=instance_number,
manufacturer=manufacturer,
patient_id=getattr(study_dataset, 'PatientID', None),
patient_name=getattr(study_dataset, 'PatientName', None),
patient_birth_date=getattr(study_dataset, 'PatientBirthDate', None),
patient_sex=getattr(study_dataset, 'PatientSex', None),
accession_number=getattr(study_dataset, 'AccessionNumber', None),
study_id=getattr(study_dataset, 'StudyID', None),
study_date=getattr(study_dataset, 'StudyDate', None),
study_time=getattr(study_dataset, 'StudyTime', None),
referring_physician_name=getattr(
study_dataset,
'ReferringPhysicianName',
None
),
pixel_spacing=pixel_spacing,
laterality=laterality,
patient_orientation=patient_orientation,
anatomical_orientation_type=anatomical_orientation_type,
container_identifier=container_identifier,
issuer_of_container_identifier=issuer_of_container_identifier,
specimen_descriptions=specimen_descriptions,
transfer_syntax_uid=transfer_syntax_uid,
**kwargs
)
2 changes: 1 addition & 1 deletion src/highdicom/valuerep.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def check_person_name(person_name: Union[str, PersonName]) -> None:
'http://dicom.nema.org/dicom/2013/output/chtml/part05/'
'sect_6.2.html#sect_6.2.1.2'
)
if '^' not in person_name:
if '^' not in person_name and person_name != '': # empty string is allowed
raise ValueError(
f'The string "{person_name}" is unlikely to represent the '
'intended person name since it contains only a single component. '
Expand Down
47 changes: 46 additions & 1 deletion tests/test_sc.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from pathlib import Path
from io import BytesIO
import unittest

import numpy as np
import pytest
from pydicom import dcmread
from pydicom.uid import generate_uid, RLELossless
from pydicom.valuerep import DA, TM

from highdicom import SpecimenDescription
from highdicom.sc import SCImage
Expand All @@ -27,6 +29,11 @@ def setUp(self):
self._container_identifier = str(np.random.choice(100))
self._specimen_identifier = str(np.random.choice(100))
self._specimen_uid = generate_uid()
file_path = Path(__file__)
data_dir = file_path.parent.parent.joinpath('data')
self._study_dataset = dcmread(
str(data_dir.joinpath('test_files', 'ct_image.dcm'))
)

@staticmethod
def get_array_after_writing(instance):
Expand Down Expand Up @@ -220,7 +227,7 @@ def test_construct_monochrome_patient(self):
assert instance.PatientName is None
assert instance.PatientSex is None
assert instance.StudyTime is None
assert instance.StudyTime is None
assert instance.StudyDate is None
assert instance.PixelData == self._monochrome_pixel_array.tobytes()
with pytest.raises(AttributeError):
instance.ContainerIdentifier
Expand Down Expand Up @@ -281,3 +288,41 @@ def test_rgb_rle(self):
self.get_array_after_writing(instance),
frame
)

def test_construct_rgb_from_study_dataset(self):
bits_allocated = 8
photometric_interpretation = 'RGB'
coordinate_system = 'PATIENT'
instance = SCImage.from_study_dataset(
study_dataset=self._study_dataset,
pixel_array=self._rgb_pixel_array,
photometric_interpretation=photometric_interpretation,
bits_allocated=bits_allocated,
coordinate_system=coordinate_system,
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,
laterality=self._laterality
)
assert instance.BitsAllocated == bits_allocated
assert instance.SamplesPerPixel == 3
assert instance.PlanarConfiguration == 0
assert instance.PhotometricInterpretation == photometric_interpretation
assert instance.StudyInstanceUID == self._study_dataset.StudyInstanceUID
assert instance.SeriesInstanceUID == self._series_instance_uid
assert instance.SOPInstanceUID == self._sop_instance_uid
assert instance.SeriesNumber == self._series_number
assert instance.InstanceNumber == self._instance_number
assert instance.Manufacturer == self._manufacturer
assert instance.Laterality == self._laterality
assert instance.PatientOrientation == self._patient_orientation
assert instance.AccessionNumber == self._study_dataset.AccessionNumber
assert instance.PatientName == self._study_dataset.PatientName
assert instance.PatientSex == self._study_dataset.PatientSex
assert instance.StudyTime == TM(self._study_dataset.StudyTime)
assert instance.StudyDate == DA(self._study_dataset.StudyDate)
assert instance.StudyID == self._study_dataset.StudyID
assert instance.PixelData == self._rgb_pixel_array.tobytes()