Skip to content

Commit 1ed8de6

Browse files
authored
fix: Refactor DICOM utilities and enhance documentation (#216)
Remove unnecessary sanitization functionality, improve DICOM exception handling, and introduce new utilities for loading and extracting ROI metadata. Update documentation to include references for the new DICOM utilities. - **New Features** - Enhanced support for processing various DICOM file formats with improved metadata extraction and robust error handling. - Introduced cross-platform support for determining optimal file path and filename lengths, along with secure filename sanitization. - **Refactor** - Streamlined DICOM processing by removing legacy parameters and simplifying function interfaces for consistent performance. - **Tests** - Updated the testing suite for improved type safety and reliability, ensuring smoother interactions when handling DICOM files.
1 parent 5f38f49 commit 1ed8de6

19 files changed

+687
-159
lines changed

docs/.pages

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ nav:
22
- Home: index.md
33
- Usage: usage
44
- CLI Reference: cli
5-
- API Reference: reference
5+
- API Reference: reference

docs/reference/.pages

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
nav:
2+
- DICOM Utilities: dicom-utils
3+
- DICOM Sorting: dicomsort
4+
- ...

docs/reference/dicom-utils/.pages

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
nav:
2+
- load-dicom.md
3+
- find-dicoms.md
4+
- ...
+4-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
# Find DICOMs
22

3-
::: imgtools.dicom.utils.find_dicoms
3+
::: imgtools.dicom.find_dicoms
4+
options:
5+
show_root_full_path: true
6+
show_docstring_raises: true
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Load DICOM
2+
3+
::: imgtools.dicom.load_dicom
4+
options:
5+
show_root_full_path: true
6+
show_docstring_raises: true
7+
8+
9+
::: imgtools.dicom.load_rtstruct_dcm
10+
options:
11+
show_root_full_path: true
12+
show_docstring_raises: true
13+
14+
::: imgtools.dicom.load_seg_dcm
15+
options:
16+
show_root_full_path: true
17+
show_docstring_raises: true
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Lookup Tag
2+
3+
::: imgtools.dicom.lookup_tag
4+
options:
5+
show_root_full_path: true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Similar Tags
2+
3+
::: imgtools.dicom.similar_tags
4+
options:
5+
show_root_full_path: true
+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# Tag Exists
2+
3+
::: imgtools.dicom.tag_exists
4+
options:
5+
show_root_full_path: true

mkdocs.yml

+15-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
site_name: Med-ImageTools Documentation
22
site_url: https://bhklab.github.io/med-imagetools
3+
repo_url: https://github.com/bhklab/med-imagetools
34
watch: [docs, src, mkdocs.yml]
45
dev_addr: "127.0.0.1:8001"
56

@@ -13,7 +14,20 @@ markdown_extensions:
1314
- pymdownx.tabbed:
1415
alternate_style: true
1516
- mkdocs-click
16-
17+
18+
extra:
19+
homepage: https://bhklab.github.io/med-imagetools
20+
social:
21+
#######################################################################################
22+
# https://squidfunk.github.io/mkdocs-material/setup/setting-up-the-footer/#social-links
23+
- icon: fontawesome/brands/github
24+
link: https://github.com/bhklab/
25+
name: Check out our GitHub!
26+
- icon: fontawesome/brands/linkedin
27+
link: https://www.linkedin.com/in/bhklab/
28+
name: Connect with us on LinkedIn!
29+
generator: false # disable 'built with MkDocs' footer
30+
1731
theme:
1832
name: material
1933
features:

src/imgtools/dicom/__init__.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,23 @@
1+
from .input import (
2+
extract_roi_meta,
3+
extract_roi_names,
4+
load_dicom,
5+
load_rtstruct_dcm,
6+
load_seg_dcm,
7+
rtstruct_reference_uids,
8+
)
19
from .utils import find_dicoms, lookup_tag, similar_tags, tag_exists
210

311
__all__ = [
412
"find_dicoms",
513
"lookup_tag",
614
"similar_tags",
715
"tag_exists",
16+
# input
17+
"load_dicom",
18+
"load_rtstruct_dcm",
19+
"load_seg_dcm",
20+
"extract_roi_meta",
21+
"extract_roi_names",
22+
"rtstruct_reference_uids",
823
]

src/imgtools/dicom/input/__init__.py

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from .dicom_reader import (
2+
load_dicom,
3+
load_rtstruct_dcm,
4+
load_seg_dcm,
5+
path_from_pathlike,
6+
)
7+
from .rtstruct_utils import (
8+
extract_roi_meta,
9+
extract_roi_names,
10+
rtstruct_reference_uids,
11+
)
12+
13+
__all__ = [
14+
"load_dicom",
15+
"path_from_pathlike",
16+
"load_rtstruct_dcm",
17+
"load_seg_dcm",
18+
# rtstruct_utils
19+
"extract_roi_meta",
20+
"extract_roi_names",
21+
"rtstruct_reference_uids",
22+
]
+174
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import os
2+
from io import BytesIO
3+
from pathlib import Path
4+
from typing import BinaryIO, TypeAlias, cast
5+
6+
from pydicom import dcmread
7+
from pydicom.dataset import FileDataset
8+
9+
from imgtools.exceptions import (
10+
InvalidDicomError,
11+
NotRTSTRUCTError,
12+
NotSEGError,
13+
)
14+
15+
# Define a type alias for DICOM input types
16+
DicomInput: TypeAlias = FileDataset | str | Path | bytes | BinaryIO
17+
18+
19+
def path_from_pathlike(file_object: str | Path | BinaryIO) -> str | BinaryIO:
20+
"""Return the string representation if file_object is path-like,
21+
otherwise return the object itself.
22+
23+
Parameters
24+
----------
25+
file_object : str | Path | BinaryIO
26+
File path or file-like object.
27+
28+
Returns
29+
-------
30+
str | BinaryIO
31+
String representation of the path or the original file-like object.
32+
"""
33+
try:
34+
return os.fspath(file_object) # type: ignore[arg-type]
35+
except TypeError:
36+
return cast(BinaryIO, file_object)
37+
38+
39+
def load_dicom(
40+
dicom_input: DicomInput,
41+
force: bool = True,
42+
stop_before_pixels: bool = True,
43+
) -> FileDataset:
44+
"""Load a DICOM file and return the parsed FileDataset object.
45+
46+
This function supports various input types including file paths, byte streams,
47+
and file-like objects. It uses the `pydicom.dcmread` function to read the DICOM file.
48+
49+
Notes
50+
-----
51+
- If `dicom_input` is already a `FileDataset`, it is returned as is.
52+
- If `dicom_input` is a file path or file-like object, it is read using `pydicom.dcmread`.
53+
- If `dicom_input` is a byte stream, it is wrapped in a `BytesIO` object and then read.
54+
- An `InvalidDicomError` is raised if the input type is unsupported.
55+
56+
Parameters
57+
----------
58+
dicom_input : FileDataset | str | Path | bytes | BinaryIO
59+
Input DICOM file as a `pydicom.FileDataset`, file path, byte stream, or file-like object.
60+
force : bool, optional
61+
Whether to allow reading DICOM files missing the *File Meta Information*
62+
header, by default True.
63+
stop_before_pixels : bool, optional
64+
Whether to stop reading the DICOM file before loading pixel data, by default True.
65+
66+
Returns
67+
-------
68+
FileDataset
69+
Parsed DICOM dataset.
70+
71+
Raises
72+
------
73+
InvalidDicomError
74+
If the input is of an unsupported type or cannot be read as a DICOM file.
75+
"""
76+
match dicom_input:
77+
case FileDataset():
78+
return dicom_input
79+
case str() | Path() | BinaryIO():
80+
dicom_source = path_from_pathlike(dicom_input)
81+
return dcmread(
82+
dicom_source,
83+
force=force,
84+
stop_before_pixels=stop_before_pixels,
85+
)
86+
case bytes():
87+
return dcmread(
88+
BytesIO(dicom_input),
89+
force=force,
90+
stop_before_pixels=stop_before_pixels,
91+
)
92+
case _:
93+
msg = (
94+
f"Invalid input type for 'dicom_input': {type(dicom_input)}. "
95+
"Must be a FileDataset, str, Path, bytes, or BinaryIO object."
96+
)
97+
raise InvalidDicomError(msg)
98+
99+
100+
def load_rtstruct_dcm(
101+
rtstruct_input: DicomInput,
102+
force: bool = True,
103+
stop_before_pixels: bool = True,
104+
) -> FileDataset:
105+
"""Load an RTSTRUCT DICOM file and return the parsed FileDataset object.
106+
107+
Parameters
108+
----------
109+
rtstruct_input : FileDataset | str | Path | bytes
110+
Input DICOM file as a `pydicom.FileDataset`, file path, or byte stream.
111+
force : bool, optional
112+
Whether to allow reading DICOM files missing the *File Meta Information*
113+
header, by default True.
114+
stop_before_pixels : bool, optional
115+
Whether to stop reading the DICOM file before loading pixel data, by default True.
116+
117+
Returns
118+
-------
119+
FileDataset
120+
Parsed RTSTRUCT DICOM dataset.
121+
122+
Raises
123+
------
124+
InvalidDicomError
125+
If the input is of an unsupported type or cannot be read as a DICOM file.
126+
NotRTSTRUCTError
127+
If the input file is not an RTSTRUCT (i.e., `Modality` field is not "RTSTRUCT").
128+
"""
129+
130+
dicom = load_dicom(rtstruct_input, force, stop_before_pixels)
131+
132+
if dicom.Modality != "RTSTRUCT":
133+
msg = f"The provided DICOM is not an RTSTRUCT file. Found Modality: {dicom.Modality}"
134+
raise NotRTSTRUCTError(msg)
135+
136+
return dicom
137+
138+
139+
def load_seg_dcm(
140+
seg_input: DicomInput,
141+
force: bool = True,
142+
stop_before_pixels: bool = True,
143+
) -> FileDataset:
144+
"""Load a SEG DICOM file and return the parsed FileDataset object.
145+
146+
Parameters
147+
----------
148+
seg_input : FileDataset | str | Path | bytes
149+
Input DICOM file as a `pydicom.FileDataset`, file path, or byte stream.
150+
force : bool, optional
151+
Whether to allow reading DICOM files missing the *File Meta Information*
152+
header, by default True.
153+
stop_before_pixels : bool, optional
154+
Whether to stop reading the DICOM file before loading pixel data, by default True.
155+
156+
Returns
157+
-------
158+
FileDataset
159+
Parsed SEG DICOM dataset.
160+
161+
Raises
162+
------
163+
InvalidDicomError
164+
If the input is of an unsupported type or cannot be read as a DICOM file.
165+
NotSEGError
166+
If the input file is not a SEG (i.e., `Modality` field is not "SEG").
167+
"""
168+
dicom = load_dicom(seg_input, force, stop_before_pixels)
169+
170+
if dicom.Modality != "SEG":
171+
msg = f"The provided DICOM is not a SEG file. Found Modality: {dicom.Modality}"
172+
raise NotSEGError(msg)
173+
174+
return dicom

0 commit comments

Comments
 (0)