dicom-info 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {dicom_info-0.1.0.dist-info → dicom_info-0.2.0.dist-info}/METADATA +26 -5
- dicom_info-0.2.0.dist-info/RECORD +7 -0
- {dicom_info-0.1.0.dist-info → dicom_info-0.2.0.dist-info}/WHEEL +1 -1
- dicominfo/__init__.py +151 -1
- dicom_info-0.1.0.dist-info/RECORD +0 -7
- {dicom_info-0.1.0.dist-info → dicom_info-0.2.0.dist-info}/entry_points.txt +0 -0
- {dicom_info-0.1.0.dist-info → dicom_info-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {dicom_info-0.1.0.dist-info → dicom_info-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dicom-info
|
|
3
|
-
Version: 0.
|
|
4
|
-
Summary: Prints information about
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Prints information about DICOM files.
|
|
5
5
|
Requires-Python: >=3.13
|
|
6
6
|
Description-Content-Type: text/markdown
|
|
7
7
|
License-File: LICENSE
|
|
8
8
|
Requires-Dist: pydicom>=3.0.1
|
|
9
|
+
Requires-Dist: matplotlib>=3.7.0
|
|
10
|
+
Requires-Dist: numpy>=1.24.0
|
|
9
11
|
Dynamic: license-file
|
|
10
12
|
|
|
11
13
|
# dicom-info
|
|
@@ -14,12 +16,14 @@ A command-line tool for displaying DICOM file information.
|
|
|
14
16
|
|
|
15
17
|
## Description
|
|
16
18
|
|
|
17
|
-
dicom-info is a simple Python utility that reads and displays information from DICOM (Digital Imaging and Communications in Medicine) files. It provides a straightforward way to inspect DICOM file metadata from the command line.
|
|
19
|
+
dicom-info is a simple Python utility that reads and displays information from DICOM (Digital Imaging and Communications in Medicine) files. It provides a straightforward way to inspect DICOM file metadata from the command line, and can also display DICOM images interactively.
|
|
18
20
|
|
|
19
21
|
## Requirements
|
|
20
22
|
|
|
21
23
|
- Python >= 3.13
|
|
22
24
|
- pydicom >= 3.0.1
|
|
25
|
+
- matplotlib >= 3.7.0 (for image display)
|
|
26
|
+
- numpy >= 1.24.0 (for image display)
|
|
23
27
|
|
|
24
28
|
## Installation
|
|
25
29
|
|
|
@@ -43,20 +47,37 @@ The basic syntax is:
|
|
|
43
47
|
dicom-info FILE [FILE ...]
|
|
44
48
|
```
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
### Displaying Metadata
|
|
47
51
|
|
|
48
|
-
|
|
52
|
+
To display DICOM file metadata:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
49
55
|
dicom-info path/to/file.dcm
|
|
50
56
|
dicom-info file1.dcm file2.dcm file3.dcm
|
|
51
57
|
```
|
|
52
58
|
|
|
53
59
|
The tool will display information for each DICOM file, including all available metadata and attributes.
|
|
54
60
|
|
|
61
|
+
### Displaying Images
|
|
62
|
+
|
|
63
|
+
To display DICOM images interactively, use the `--display` or `-d` flag:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
dicom-info --display path/to/file.dcm
|
|
67
|
+
dicom-info -d file1.dcm file2.dcm file3.dcm
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Image display features:
|
|
71
|
+
- **2D images**: Displayed as grayscale plots with a colorbar
|
|
72
|
+
- **3D images**: Displayed with an interactive slider to navigate through slices
|
|
73
|
+
- **Multiple files**: Each image is shown in its own subplot with the filename as the title
|
|
74
|
+
|
|
55
75
|
## Error Handling
|
|
56
76
|
|
|
57
77
|
The tool will exit with status code 1 if:
|
|
58
78
|
- Files are not found
|
|
59
79
|
- Files are not valid DICOM files
|
|
80
|
+
- When using `--display`, if no files contain pixel data
|
|
60
81
|
|
|
61
82
|
## License
|
|
62
83
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
dicom_info-0.2.0.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
2
|
+
dicominfo/__init__.py,sha256=MwdwA_dnMxEdk8mNT8PHcKCQMIzkdwxLOvyC-Qf_CtQ,5670
|
|
3
|
+
dicom_info-0.2.0.dist-info/METADATA,sha256=X-NKpFoNL6H08SPWBZ5OE_iFb6Ev3M-nrkopdkYPsco,2041
|
|
4
|
+
dicom_info-0.2.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
5
|
+
dicom_info-0.2.0.dist-info/entry_points.txt,sha256=tCOO5Vkl04B0doIO8msDBl5suKLYAbQwjanb9XxtgKw,46
|
|
6
|
+
dicom_info-0.2.0.dist-info/top_level.txt,sha256=Sb5T-FcQW-zE4FXaF23fa5YaZ1bVGFtaKGm-FT45rHs,10
|
|
7
|
+
dicom_info-0.2.0.dist-info/RECORD,,
|
dicominfo/__init__.py
CHANGED
|
@@ -4,12 +4,30 @@
|
|
|
4
4
|
|
|
5
5
|
from __future__ import annotations
|
|
6
6
|
|
|
7
|
+
# Typing
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from matplotlib.axes import Axes
|
|
12
|
+
from matplotlib.image import AxesImage
|
|
13
|
+
from numpy import ndarray
|
|
14
|
+
|
|
15
|
+
|
|
7
16
|
# Python imports
|
|
8
17
|
import argparse
|
|
18
|
+
import logging
|
|
9
19
|
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Callable
|
|
10
22
|
|
|
11
23
|
# Module imports
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
12
25
|
import pydicom
|
|
26
|
+
import pydicom.errors
|
|
27
|
+
from matplotlib.widgets import Slider
|
|
28
|
+
from mpl_toolkits.axes_grid1 import make_axes_locatable
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
13
31
|
|
|
14
32
|
|
|
15
33
|
def print_stats(files: list[str]) -> None:
|
|
@@ -26,6 +44,120 @@ def print_stats(files: list[str]) -> None:
|
|
|
26
44
|
print(banner, dcm, sep="\n")
|
|
27
45
|
|
|
28
46
|
|
|
47
|
+
def display_images(
|
|
48
|
+
files: list[str],
|
|
49
|
+
max_cols: int | None = None,
|
|
50
|
+
) -> None:
|
|
51
|
+
"""Display DICOM images with interactive controls."""
|
|
52
|
+
try:
|
|
53
|
+
dcms = [pydicom.dcmread(f) for f in files]
|
|
54
|
+
|
|
55
|
+
except (FileNotFoundError, pydicom.errors.InvalidDicomError) as err:
|
|
56
|
+
print(f"Files could not be read due to {err}")
|
|
57
|
+
sys.exit(1)
|
|
58
|
+
|
|
59
|
+
# Check if any files have pixel data
|
|
60
|
+
files_with_pixels = [
|
|
61
|
+
(f, dcm) for f, dcm in zip(files, dcms, strict=True)
|
|
62
|
+
if hasattr(dcm, "pixel_array")
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
if not files_with_pixels:
|
|
66
|
+
print("No DICOM files with pixel data found.")
|
|
67
|
+
sys.exit(1)
|
|
68
|
+
|
|
69
|
+
# Create figure with subplots for each image
|
|
70
|
+
num_images = len(files_with_pixels)
|
|
71
|
+
max_cols = int(num_images ** .5) if max_cols is None else max_cols
|
|
72
|
+
cols = min(num_images, max_cols)
|
|
73
|
+
rows = (num_images - 1) // cols + 1
|
|
74
|
+
fig = plt.figure(figsize=(5 * cols, 4 * rows))
|
|
75
|
+
|
|
76
|
+
# Store references to manage 3D sliders
|
|
77
|
+
sliders = []
|
|
78
|
+
axes_images: list[
|
|
79
|
+
tuple[Axes, AxesImage, Slider | None, ndarray | None]
|
|
80
|
+
] = []
|
|
81
|
+
|
|
82
|
+
for idx, (filepath, dcm) in enumerate(files_with_pixels, start=1):
|
|
83
|
+
pixel_array = dcm.pixel_array
|
|
84
|
+
filename = Path(filepath).name
|
|
85
|
+
|
|
86
|
+
# Determine if 2D or 3D
|
|
87
|
+
if len(pixel_array.shape) == 2: # noqa: PLR2004
|
|
88
|
+
# 2D image - simple display
|
|
89
|
+
ax = fig.add_subplot(rows, cols, idx)
|
|
90
|
+
im = ax.imshow(pixel_array, cmap="gray")
|
|
91
|
+
ax.set_title(filename)
|
|
92
|
+
ax.axis("off")
|
|
93
|
+
plt.colorbar(im, ax=ax, fraction=0.046, pad=0.04)
|
|
94
|
+
axes_images.append((ax, im, None, None))
|
|
95
|
+
|
|
96
|
+
elif len(pixel_array.shape) == 3: # noqa: PLR2004
|
|
97
|
+
# 3D image - display with slider
|
|
98
|
+
ax = fig.add_subplot(rows, cols, idx)
|
|
99
|
+
|
|
100
|
+
# Start with the first slice
|
|
101
|
+
initial_slice = 0
|
|
102
|
+
im = ax.imshow(pixel_array[initial_slice], cmap="gray")
|
|
103
|
+
ax.set_title(
|
|
104
|
+
f"{filename}\nSlice {initial_slice + 1}/{pixel_array.shape[0]}",
|
|
105
|
+
)
|
|
106
|
+
ax.axis("off")
|
|
107
|
+
|
|
108
|
+
# Create slider axes to the right of the image
|
|
109
|
+
divider = make_axes_locatable(ax)
|
|
110
|
+
slider_ax = divider.append_axes("right", size="5%", pad=0.1)
|
|
111
|
+
slider = Slider(
|
|
112
|
+
slider_ax,
|
|
113
|
+
"Slice",
|
|
114
|
+
0,
|
|
115
|
+
pixel_array.shape[0] - 1,
|
|
116
|
+
valinit=initial_slice,
|
|
117
|
+
valstep=1,
|
|
118
|
+
orientation="vertical",
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
# Update function for slider
|
|
122
|
+
def make_update(
|
|
123
|
+
image_obj: AxesImage,
|
|
124
|
+
axis: Axes,
|
|
125
|
+
data: ndarray,
|
|
126
|
+
fname: str,
|
|
127
|
+
sldr: Slider,
|
|
128
|
+
) -> Callable[[float], None]:
|
|
129
|
+
def update(val: float) -> None:
|
|
130
|
+
slice_idx = int(sldr.val)
|
|
131
|
+
image_obj.set_data(data[slice_idx])
|
|
132
|
+
axis.set_title(
|
|
133
|
+
f"{fname}\nSlice {slice_idx + 1}/{data.shape[0]}",
|
|
134
|
+
)
|
|
135
|
+
fig.canvas.draw_idle()
|
|
136
|
+
logger.debug("Slider at slice %f for %s", val, fname)
|
|
137
|
+
|
|
138
|
+
return update
|
|
139
|
+
|
|
140
|
+
slider.on_changed(
|
|
141
|
+
make_update(im, ax, pixel_array, filename, slider),
|
|
142
|
+
)
|
|
143
|
+
sliders.append(slider)
|
|
144
|
+
axes_images.append((ax, im, slider, pixel_array))
|
|
145
|
+
|
|
146
|
+
else:
|
|
147
|
+
logger.warning(
|
|
148
|
+
"%s has unsupported dimensions: %s",
|
|
149
|
+
filename,
|
|
150
|
+
pixel_array.shape,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
plt.tight_layout()
|
|
154
|
+
if len(files_with_pixels) == 1 and axes_images[0][2] is not None:
|
|
155
|
+
# Adjust layout for single 3D image with slider
|
|
156
|
+
plt.subplots_adjust(bottom=0.15)
|
|
157
|
+
|
|
158
|
+
plt.show()
|
|
159
|
+
|
|
160
|
+
|
|
29
161
|
def main() -> None:
|
|
30
162
|
"""Entry point for the CLI."""
|
|
31
163
|
parser = argparse.ArgumentParser(
|
|
@@ -39,11 +171,29 @@ def main() -> None:
|
|
|
39
171
|
type=str,
|
|
40
172
|
)
|
|
41
173
|
|
|
174
|
+
parser.add_argument(
|
|
175
|
+
"-d",
|
|
176
|
+
"--display",
|
|
177
|
+
help="Display DICOM images instead of printing metadata.",
|
|
178
|
+
action="store_true",
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"-c",
|
|
183
|
+
"--columns",
|
|
184
|
+
help="Maximum number of columns for image display.",
|
|
185
|
+
type=int,
|
|
186
|
+
default=None,
|
|
187
|
+
)
|
|
188
|
+
|
|
42
189
|
args = parser.parse_args()
|
|
43
190
|
|
|
44
|
-
# Print dicom info
|
|
45
191
|
print_stats(args.file)
|
|
46
192
|
|
|
193
|
+
# Display images
|
|
194
|
+
if args.display:
|
|
195
|
+
display_images(args.file, max_cols=args.columns)
|
|
196
|
+
|
|
47
197
|
|
|
48
198
|
if __name__ == "__main__":
|
|
49
199
|
main()
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
dicom_info-0.1.0.dist-info/licenses/LICENSE,sha256=ixuiBLtpoK3iv89l7ylKkg9rs2GzF9ukPH7ynZYzK5s,35148
|
|
2
|
-
dicominfo/__init__.py,sha256=d4lS1hVqMRghlmhuzgi743x1pMDyNgQVEcEsXw4pPWc,1024
|
|
3
|
-
dicom_info-0.1.0.dist-info/METADATA,sha256=74grN25OiNkTQ2n3QylEclvKDQBwFcewWNr8FyhYDJA,1289
|
|
4
|
-
dicom_info-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
5
|
-
dicom_info-0.1.0.dist-info/entry_points.txt,sha256=tCOO5Vkl04B0doIO8msDBl5suKLYAbQwjanb9XxtgKw,46
|
|
6
|
-
dicom_info-0.1.0.dist-info/top_level.txt,sha256=Sb5T-FcQW-zE4FXaF23fa5YaZ1bVGFtaKGm-FT45rHs,10
|
|
7
|
-
dicom_info-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|