Skip to content

Commit ad498b2

Browse files
Copy subject context (#298)
* Add SubjectContextSpecimen.from_image() method * Import related fixes; remove fix_meta_info, now unnecessary * Drop support for python<3.10 * Fixes to codes and RLE encoding * Remove fix for resolved pydicom bug in decode_frame * Fix frame encoding and decoding, add further transfer syntaxes * Various fixes * Enforce repo-review rules (#296) * Enforce repo-review rule PP302 PP302: Sets a minimum pytest to at least 6 Must have a `minversion=`, and must be at least 6 (first version to support `pyproject.toml` configuration). * Enforce repo-review rule PP305 PP305: Specifies xfail_strict `xfail_strict` should be set. You can manually specify if a check should be strict when setting each xfail. * Enforce repo-review rule PP306 PP306: Specifies strict config `--strict-config` should be in `addopts = [...]`. This forces an error if a config setting is misspelled. * Enforce repo-review rule PP307 PP307: Specifies strict markers `--strict-markers` should be in `addopts = [...]`. This forces all markers to be specified in config, avoiding misspellings. * Enforce repo-review rule PP308 PP308: Specifies useful pytest summary An explicit summary flag like `-ra` should be in `addopts = [...]` (print summary of all fails/errors). * Enforce pytest ≥ 7.3.2 This is the first version to support Python 3.12: https://docs.pytest.org/en/stable/changelog.html#pytest-7-3-2-2023-06-10 * Enforce repo-review rule MY104 MY104: MyPy enables ignore-without-code Must have `"ignore-without-code"` in `enable_error_code = [...]`. This will force all skips in your project to include the error code, which makes them more readable, and avoids skipping something unintended. * Enforce repo-review rule MY105 MY105: MyPy enables redundant-expr Must have `"redundant-expr"` in `enable_error_code = [...]`. This helps catch useless lines of code, like checking the same condition twice. * Enforce repo-review rule MY106 MY106: MyPy enables truthy-bool Must have `"truthy-bool"` in `enable_error_code = []`. This catches mistakes in using a value as truthy if it cannot be falsy. * Remove pillow-jpls dependency as functionality now comes from pylibjpeg * Add details of single bit JPEG2000 to the docs * Better workaround for floating point pixel decoding * Adjust dependencies * Deprecate row_stack for vstack * Bump pydicom version to 3.0.1 * Error message typo * Add SubjectContext.for_image() method --------- Co-authored-by: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com>
1 parent 10f153d commit ad498b2

2 files changed

Lines changed: 186 additions & 1 deletion

File tree

src/highdicom/sr/templates.py

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1703,7 +1703,7 @@ def __init__(
17031703
Identifier of the observed specimen (may have limited scope,
17041704
e.g., only relevant with respect to the corresponding container)
17051705
container_identifier: Union[str, None], optional
1706-
Identifier of the container holding the speciment (e.g., a glass
1706+
Identifier of the container holding the specimen (e.g., a glass
17071707
slide)
17081708
specimen_type: Union[pydicom.sr.coding.Code, highdicom.sr.CodedConcept, None], optional
17091709
Type of the specimen (see
@@ -1809,6 +1809,49 @@ def specimen_type(self) -> Union[CodedConcept, None]:
18091809
return matches[0].value
18101810
return None
18111811

1812+
@classmethod
1813+
def from_image(
1814+
cls,
1815+
image: Dataset,
1816+
) -> 'SubjectContextSpecimen':
1817+
"""Deduce specimen information from an existing image.
1818+
1819+
This is appropriate, for example, when copying the specimen information
1820+
from a source image into a derived SR or similar object.
1821+
1822+
Parameters
1823+
----------
1824+
image: pydicom.Dataset
1825+
An image from which to infer specimen information. There is no
1826+
limitation on the type of image, however it must have the Specimen
1827+
module included.
1828+
1829+
Raises
1830+
------
1831+
ValueError:
1832+
If the input image does not contain specimen information.
1833+
1834+
"""
1835+
if not hasattr(image, 'ContainerIdentifier'):
1836+
raise ValueError("Image does not contain specimen information.")
1837+
1838+
description = image.SpecimenDescriptionSequence[0]
1839+
1840+
# Specimen type code sequence is optional
1841+
if hasattr(description, 'SpecimenTypeCodeSequence'):
1842+
specimen_type: Optional[CodedConcept] = CodedConcept.from_dataset(
1843+
description.SpecimenTypeCodeSequence[0]
1844+
)
1845+
else:
1846+
specimen_type = None
1847+
1848+
return cls(
1849+
container_identifier=image.ContainerIdentifier,
1850+
identifier=description.SpecimenIdentifier,
1851+
uid=description.SpecimenUID,
1852+
specimen_type=specimen_type,
1853+
)
1854+
18121855
@classmethod
18131856
def from_sequence(
18141857
cls,
@@ -2124,6 +2167,41 @@ def __init__(
21242167
raise TypeError('Unexpected subject class specific context.')
21252168
self.extend(subject_class_specific_context)
21262169

2170+
@classmethod
2171+
def from_image(cls, image: Dataset) -> 'Optional[SubjectContext]':
2172+
"""Get a subject context inferred from an existing image.
2173+
2174+
Currently this is only supported for subjects that are specimens.
2175+
2176+
Parameters
2177+
----------
2178+
image: pydicom.Dataset
2179+
Dataset of an existing DICOM image object
2180+
containing metadata on the imaging subject. Highdicom will attempt
2181+
to infer the subject context from this image. If successful, it
2182+
will be returned as a ``SubjectContext``, otherwise ``None``.
2183+
2184+
Returns
2185+
-------
2186+
Optional[highdicom.sr.SubjectContext]:
2187+
SubjectContext, if it can be inferred from the image. Otherwise,
2188+
``None``.
2189+
2190+
"""
2191+
try:
2192+
subject_context_specimen = SubjectContextSpecimen.from_image(
2193+
image
2194+
)
2195+
except ValueError:
2196+
pass
2197+
else:
2198+
return cls(
2199+
subject_class=codes.DCM.Specimen,
2200+
subject_class_specific_context=subject_context_specimen,
2201+
)
2202+
2203+
return None
2204+
21272205
@property
21282206
def subject_class(self) -> CodedConcept:
21292207
"""highdicom.sr.CodedConcept: type of subject"""

tests/test_sr.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1398,6 +1398,113 @@ def test_construction_all(self):
13981398
assert context[5].TextValue == self._physical_location
13991399

14001400

1401+
class TestSubjectContextSpecimen(unittest.TestCase):
1402+
1403+
def setUp(self):
1404+
super().setUp()
1405+
file_path = Path(__file__)
1406+
data_dir = file_path.parent.parent.joinpath('data')
1407+
self._sm_image = dcmread(
1408+
str(data_dir.joinpath('test_files', 'sm_image.dcm'))
1409+
)
1410+
1411+
def test_from_image(self):
1412+
specimen_context = SubjectContextSpecimen.from_image(
1413+
self._sm_image
1414+
)
1415+
1416+
assert (
1417+
specimen_context.container_identifier ==
1418+
self._sm_image.ContainerIdentifier
1419+
)
1420+
description = self._sm_image.SpecimenDescriptionSequence[0]
1421+
assert (
1422+
specimen_context.specimen_identifier ==
1423+
description.SpecimenIdentifier
1424+
)
1425+
assert (
1426+
specimen_context.specimen_uid == description.SpecimenUID
1427+
)
1428+
assert specimen_context.specimen_type == codes.SCT.TissueSection
1429+
1430+
def test_from_image_no_specimen_type(self):
1431+
# Specimen type is optional, for_image method should cope corectly if
1432+
# it is missing
1433+
image = deepcopy(self._sm_image)
1434+
delattr(
1435+
image.SpecimenDescriptionSequence[0],
1436+
'SpecimenTypeCodeSequence'
1437+
)
1438+
specimen_context = SubjectContextSpecimen.from_image(image)
1439+
1440+
assert (
1441+
specimen_context.container_identifier ==
1442+
self._sm_image.ContainerIdentifier
1443+
)
1444+
description = self._sm_image.SpecimenDescriptionSequence[0]
1445+
assert (
1446+
specimen_context.specimen_identifier ==
1447+
description.SpecimenIdentifier
1448+
)
1449+
assert (
1450+
specimen_context.specimen_uid == description.SpecimenUID
1451+
)
1452+
assert specimen_context.specimen_type is None
1453+
1454+
1455+
class TestSubjectContext(unittest.TestCase):
1456+
1457+
def setUp(self):
1458+
super().setUp()
1459+
file_path = Path(__file__)
1460+
data_dir = file_path.parent.parent.joinpath('data')
1461+
self._sm_image = dcmread(
1462+
str(data_dir.joinpath('test_files', 'sm_image.dcm'))
1463+
)
1464+
self._ct_image = dcmread(
1465+
str(data_dir.joinpath('test_files', 'ct_image.dcm'))
1466+
)
1467+
1468+
def test_from_image(self):
1469+
subject_context = SubjectContext.from_image(
1470+
self._sm_image
1471+
)
1472+
1473+
assert subject_context is not None
1474+
1475+
has_specimen_uid = False
1476+
has_specimen_id = False
1477+
has_container_id = False
1478+
1479+
for item in subject_context:
1480+
1481+
# SpecimenUID
1482+
if item.ConceptNameCodeSequence[0].CodeValue == '121039':
1483+
assert item.UID == '2.25.281821656492584880365678271074145532563'
1484+
has_specimen_uid = True
1485+
1486+
# Specimen Identifier
1487+
elif item.ConceptNameCodeSequence[0].CodeValue == '121041':
1488+
assert item.TextValue == 'S19-1_A_1_1'
1489+
has_specimen_id = True
1490+
1491+
# Specimen Container Identifier
1492+
elif item.ConceptNameCodeSequence[0].CodeValue == '111700':
1493+
assert item.TextValue == 'S19-1_A_1_1'
1494+
has_container_id = True
1495+
1496+
assert has_specimen_uid
1497+
assert has_specimen_id
1498+
assert has_container_id
1499+
1500+
def test_from_image_no_subject_info(self):
1501+
subject_context = SubjectContext.from_image(
1502+
self._ct_image
1503+
)
1504+
1505+
assert subject_context is None
1506+
1507+
14011508
class TestObservationContext(unittest.TestCase):
14021509

14031510
def setUp(self):

0 commit comments

Comments
 (0)