dbdicom 0.2.0__py3-none-any.whl → 0.3.16__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.
- dbdicom/__init__.py +3 -25
- dbdicom/api.py +496 -0
- dbdicom/const.py +144 -0
- dbdicom/database.py +133 -0
- dbdicom/dataset.py +471 -0
- dbdicom/dbd.py +1290 -0
- dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/bin/emf2sf +57 -57
- dbdicom/register.py +402 -0
- dbdicom/{ds/types → sop_classes}/ct_image.py +2 -16
- dbdicom/{ds/types → sop_classes}/enhanced_mr_image.py +206 -160
- dbdicom/sop_classes/mr_image.py +338 -0
- dbdicom/sop_classes/parametric_map.py +381 -0
- dbdicom/sop_classes/secondary_capture.py +140 -0
- dbdicom/sop_classes/segmentation.py +311 -0
- dbdicom/{ds/types → sop_classes}/ultrasound_multiframe_image.py +1 -15
- dbdicom/{ds/types → sop_classes}/xray_angiographic_image.py +2 -17
- dbdicom/utils/arrays.py +142 -0
- dbdicom/utils/files.py +0 -20
- dbdicom/utils/image.py +43 -466
- dbdicom/utils/pydicom_dataset.py +386 -0
- dbdicom-0.3.16.dist-info/METADATA +26 -0
- dbdicom-0.3.16.dist-info/RECORD +54 -0
- {dbdicom-0.2.0.dist-info → dbdicom-0.3.16.dist-info}/WHEEL +1 -1
- dbdicom/create.py +0 -450
- dbdicom/ds/__init__.py +0 -10
- dbdicom/ds/create.py +0 -63
- dbdicom/ds/dataset.py +0 -841
- dbdicom/ds/dictionaries.py +0 -620
- dbdicom/ds/types/mr_image.py +0 -267
- dbdicom/ds/types/parametric_map.py +0 -226
- dbdicom/external/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/lib/linux-x86/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/linux-x86-64/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/linux-x86-64/libopencv_java.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis2.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis2.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-x86/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-x86-64/libclib_jiio.so +0 -0
- dbdicom/manager.py +0 -2077
- dbdicom/message.py +0 -119
- dbdicom/record.py +0 -1526
- dbdicom/types/database.py +0 -107
- dbdicom/types/instance.py +0 -184
- dbdicom/types/patient.py +0 -40
- dbdicom/types/series.py +0 -816
- dbdicom/types/study.py +0 -58
- dbdicom/utils/variables.py +0 -155
- dbdicom/utils/vreg.py +0 -2626
- dbdicom/wrappers/__init__.py +0 -7
- dbdicom/wrappers/dipy.py +0 -462
- dbdicom/wrappers/elastix.py +0 -855
- dbdicom/wrappers/numpy.py +0 -119
- dbdicom/wrappers/scipy.py +0 -1413
- dbdicom/wrappers/skimage.py +0 -1030
- dbdicom/wrappers/sklearn.py +0 -151
- dbdicom/wrappers/vreg.py +0 -273
- dbdicom-0.2.0.dist-info/METADATA +0 -276
- dbdicom-0.2.0.dist-info/RECORD +0 -81
- {dbdicom-0.2.0.dist-info → dbdicom-0.3.16.dist-info/licenses}/LICENSE +0 -0
- {dbdicom-0.2.0.dist-info → dbdicom-0.3.16.dist-info}/top_level.txt +0 -0
dbdicom/__init__.py
CHANGED
|
@@ -1,26 +1,4 @@
|
|
|
1
|
+
from dbdicom.api import *
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
database_hollywood,
|
|
5
|
-
patient,
|
|
6
|
-
study,
|
|
7
|
-
series,
|
|
8
|
-
as_series,
|
|
9
|
-
zeros,
|
|
10
|
-
)
|
|
11
|
-
from .record import (
|
|
12
|
-
copy_to,
|
|
13
|
-
move_to,
|
|
14
|
-
group,
|
|
15
|
-
merge,
|
|
16
|
-
)
|
|
17
|
-
from .types.series import (
|
|
18
|
-
array
|
|
19
|
-
)
|
|
20
|
-
from .record import Record
|
|
21
|
-
from .types.database import Database
|
|
22
|
-
from .types.patient import Patient
|
|
23
|
-
from .types.study import Study
|
|
24
|
-
from .types.series import Series
|
|
25
|
-
|
|
26
|
-
from .utils import image
|
|
3
|
+
# Utilities
|
|
4
|
+
from dbdicom.utils.image import affine_matrix
|
dbdicom/api.py
ADDED
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import zipfile
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Union
|
|
6
|
+
from tqdm import tqdm
|
|
7
|
+
import numpy as np
|
|
8
|
+
import vreg
|
|
9
|
+
|
|
10
|
+
from dbdicom.dbd import DataBaseDicom
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def open(path:str) -> DataBaseDicom:
|
|
16
|
+
"""Open a DICOM database
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
path (str): path to the DICOM folder
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
DataBaseDicom: database instance.
|
|
23
|
+
"""
|
|
24
|
+
return DataBaseDicom(path)
|
|
25
|
+
|
|
26
|
+
def to_json(path):
|
|
27
|
+
"""Summarise the contents of the DICOM folder in a json file
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
path (str): path to the DICOM folder
|
|
31
|
+
"""
|
|
32
|
+
dbd = open(path)
|
|
33
|
+
dbd.close()
|
|
34
|
+
|
|
35
|
+
def print(path):
|
|
36
|
+
"""Print the contents of the DICOM folder
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
path (str): path to the DICOM folder
|
|
40
|
+
"""
|
|
41
|
+
dbd = open(path)
|
|
42
|
+
dbd.print()
|
|
43
|
+
dbd.close()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def summary(path) -> dict:
|
|
47
|
+
"""Return a summary of the contents of the database.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
path (str): path to the DICOM folder
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
dict: Nested dictionary with summary information on the database.
|
|
54
|
+
"""
|
|
55
|
+
dbd = open(path)
|
|
56
|
+
s = dbd.summary()
|
|
57
|
+
dbd.close()
|
|
58
|
+
return s
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def tree(path) -> dict:
|
|
62
|
+
"""Return the structure of the database as a dictionary tree.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
path (str): path to the DICOM folder
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
dict: Nested dictionary with summary information on the database.
|
|
69
|
+
"""
|
|
70
|
+
dbd = open(path)
|
|
71
|
+
s = dbd.register
|
|
72
|
+
dbd.close()
|
|
73
|
+
return s
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def to_csv(path, csv_file) -> dict:
|
|
77
|
+
"""Write a summary of the contents of the database to csv.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
path (str): path to the DICOM folder
|
|
81
|
+
csv_file (str): path to the csv file
|
|
82
|
+
"""
|
|
83
|
+
dbd = open(path)
|
|
84
|
+
dbd.to_csv(csv_file)
|
|
85
|
+
dbd.close()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def patients(path, name:str=None, contains:str=None, isin:list=None)->list:
|
|
89
|
+
"""Return a list of patients in the DICOM folder.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
path (str): path to the DICOM folder
|
|
93
|
+
name (str, optional): value of PatientName, to search for
|
|
94
|
+
individuals with a given name. Defaults to None.
|
|
95
|
+
contains (str, optional): substring of PatientName, to
|
|
96
|
+
search for individuals based on part of their name.
|
|
97
|
+
Defaults to None.
|
|
98
|
+
isin (list, optional): List of PatientName values, to search
|
|
99
|
+
for patients whose name is in the list. Defaults to None.
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
list: list of patients fulfilling the criteria.
|
|
103
|
+
"""
|
|
104
|
+
dbd = open(path)
|
|
105
|
+
p = dbd.patients(name, contains, isin)
|
|
106
|
+
dbd.close()
|
|
107
|
+
return p
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def studies(entity:str | list, desc:str=None, contains:str=None, isin:list=None)->list:
|
|
111
|
+
"""Return a list of studies in the DICOM folder.
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
entity (str or list): path to a DICOM folder (to search in
|
|
115
|
+
the whole folder), or a two-element list identifying a
|
|
116
|
+
patient (to search studies of a given patient).
|
|
117
|
+
desc (str, optional): value of StudyDescription, to search for
|
|
118
|
+
studies with a given description. Defaults to None.
|
|
119
|
+
contains (str, optional): substring of StudyDescription, to
|
|
120
|
+
search for studies based on part of their description.
|
|
121
|
+
Defaults to None.
|
|
122
|
+
isin (list, optional): List of StudyDescription values, to search
|
|
123
|
+
for studies whose description is in a list. Defaults to None.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
list: list of studies fulfilling the criteria.
|
|
127
|
+
"""
|
|
128
|
+
if isinstance(entity, str): # path = folder
|
|
129
|
+
dbd = open(entity)
|
|
130
|
+
s = dbd.studies(entity, desc, contains, isin)
|
|
131
|
+
dbd.close()
|
|
132
|
+
return s
|
|
133
|
+
elif len(entity)==2: # path = patient
|
|
134
|
+
dbd = open(entity[0])
|
|
135
|
+
s = dbd.studies(entity, desc, contains, isin)
|
|
136
|
+
dbd.close()
|
|
137
|
+
return s
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
"The path must be a folder or a 2-element list "
|
|
141
|
+
"with a folder and a patient name."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def series(entity:str | list, desc:str=None, contains:str=None, isin:list=None)->list:
|
|
145
|
+
"""Return a list of series in the DICOM folder.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
entity (str or list): path to a DICOM folder (to search in
|
|
149
|
+
the whole folder), or a list identifying a
|
|
150
|
+
patient or a study (to search series of a given patient
|
|
151
|
+
or study).
|
|
152
|
+
desc (str, optional): value of SeriesDescription, to search for
|
|
153
|
+
series with a given description. Defaults to None.
|
|
154
|
+
contains (str, optional): substring of SeriesDescription, to
|
|
155
|
+
search for series based on part of their description.
|
|
156
|
+
Defaults to None.
|
|
157
|
+
isin (list, optional): List of SeriesDescription values, to search
|
|
158
|
+
for series whose description is in a list. Defaults to None.
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
list: list of series fulfilling the criteria.
|
|
162
|
+
"""
|
|
163
|
+
if isinstance(entity, str): # path = folder
|
|
164
|
+
dbd = open(entity)
|
|
165
|
+
s = dbd.series(entity, desc, contains, isin)
|
|
166
|
+
dbd.close()
|
|
167
|
+
return s
|
|
168
|
+
elif len(entity) in [2,3]:
|
|
169
|
+
dbd = open(entity[0])
|
|
170
|
+
s = dbd.series(entity, desc, contains, isin)
|
|
171
|
+
dbd.close()
|
|
172
|
+
return s
|
|
173
|
+
else:
|
|
174
|
+
raise ValueError(
|
|
175
|
+
"To retrieve a series, the entity must be a database, patient or study."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def copy(from_entity:list, to_entity=None):
|
|
179
|
+
"""Copy a DICOM entity (patient, study or series)
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
from_entity (list): entity to copy
|
|
183
|
+
to_entity (list, optional): entity after copying. If this is not
|
|
184
|
+
provided, a copy will be made in the same study and returned.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
entity: the copied entity. If th to_entity is provided, this is
|
|
188
|
+
returned.
|
|
189
|
+
"""
|
|
190
|
+
dbd = open(from_entity[0])
|
|
191
|
+
from_entity_copy = dbd.copy(from_entity, to_entity)
|
|
192
|
+
dbd.close()
|
|
193
|
+
return from_entity_copy
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def delete(entity:list, not_exists_ok=False):
|
|
197
|
+
"""Delete a DICOM entity
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
entity (list): entity to delete
|
|
201
|
+
not_exists_ok (bool): By default, an exception is raised when attempting
|
|
202
|
+
to delete an entity that does not exist. Set this to True to pass over this silently.
|
|
203
|
+
"""
|
|
204
|
+
dbd = open(entity[0])
|
|
205
|
+
dbd.delete(entity, not_exists_ok)
|
|
206
|
+
dbd.close()
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def move(from_entity:list, to_entity:list):
|
|
210
|
+
"""Move a DICOM entity
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
entity (list): entity to move
|
|
214
|
+
"""
|
|
215
|
+
dbd = open(from_entity[0])
|
|
216
|
+
dbd.copy(from_entity, to_entity)
|
|
217
|
+
dbd.delete(from_entity)
|
|
218
|
+
dbd.close()
|
|
219
|
+
|
|
220
|
+
def split_series(series:list, attr:Union[str, tuple], key=None)->list:
|
|
221
|
+
"""
|
|
222
|
+
Split a series into multiple series
|
|
223
|
+
|
|
224
|
+
Args:
|
|
225
|
+
series (list): series to split.
|
|
226
|
+
attr (str or tuple): dicom attribute to split the series by.
|
|
227
|
+
key (function): split by by key(attr)
|
|
228
|
+
Returns:
|
|
229
|
+
list: list of two-element tuples, where the first element is
|
|
230
|
+
is the value and the second element is the series corresponding to that value.
|
|
231
|
+
"""
|
|
232
|
+
dbd = open(series[0])
|
|
233
|
+
split_series = dbd.split_series(series, attr, key)
|
|
234
|
+
dbd.close()
|
|
235
|
+
return split_series
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def volume(series:list, dims:list=None, verbose=1, **kwargs) -> vreg.Volume3D:
|
|
239
|
+
"""Read volume from a series.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
series (list, str): DICOM entity to read
|
|
243
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
244
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
245
|
+
kwargs (dict, optional): keywords to filter the series.
|
|
246
|
+
|
|
247
|
+
Returns:
|
|
248
|
+
vreg.Volume3D.
|
|
249
|
+
"""
|
|
250
|
+
dbd = open(series[0])
|
|
251
|
+
vol = dbd.volume(series, dims, verbose, **kwargs)
|
|
252
|
+
dbd.close()
|
|
253
|
+
return vol
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def slices(series:list, dims:list=None, verbose=1) -> vreg.Volume3D:
|
|
257
|
+
"""Read 2D volumes from the series
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
entity (list, str): DICOM series to read
|
|
261
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
262
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
list of vreg.Volume3D
|
|
266
|
+
"""
|
|
267
|
+
dbd = open(series[0])
|
|
268
|
+
vol = dbd.slices(series, dims, verbose)
|
|
269
|
+
dbd.close()
|
|
270
|
+
return vol
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
# Obsolete API - phase out
|
|
274
|
+
def volumes_2d(*args, **kwargs):
|
|
275
|
+
return slices(*args, **kwargs)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def values(series:list, *attr, dims:list=None, verbose=1) -> Union[np.ndarray, list]:
|
|
279
|
+
"""Read the values of some attributes from a DICOM series
|
|
280
|
+
|
|
281
|
+
Args:
|
|
282
|
+
series (list): DICOM series to read.
|
|
283
|
+
attr (tuple, optional): DICOM attributes to read.
|
|
284
|
+
dims (list, optional): Dimensions to sort the values.
|
|
285
|
+
If dims is not provided, values are sorted by
|
|
286
|
+
InstanceNumber.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
tuple: arrays with values for the attributes.
|
|
290
|
+
"""
|
|
291
|
+
dbd = open(series[0])
|
|
292
|
+
values = dbd.values(series, *attr, dims=dims, verbose=verbose)
|
|
293
|
+
dbd.close()
|
|
294
|
+
return values
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def write_volume(vol:Union[vreg.Volume3D, tuple], series:list,
|
|
299
|
+
ref:list=None, append=False, verbose=1, **kwargs):
|
|
300
|
+
"""Write a vreg.Volume3D to a DICOM series
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
vol (vreg.Volume3D or tuple): Volume to write to the series.
|
|
304
|
+
series (list): DICOM series to read
|
|
305
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
306
|
+
append (bool): by default write_volume will only write to a new series,
|
|
307
|
+
and raise an error when attempting to write to an existing series.
|
|
308
|
+
To overrule this behaviour and add the volume to an existing series, set append to True.
|
|
309
|
+
Default is False.
|
|
310
|
+
verbose (bool): if set to 1, a progress bar is shown. verbose=0 does not show updates.
|
|
311
|
+
kwargs: Keyword-value pairs to be set on the fly
|
|
312
|
+
"""
|
|
313
|
+
dbd = open(series[0])
|
|
314
|
+
dbd.write_volume(vol, series, ref=ref, append=append, verbose=verbose, **kwargs)
|
|
315
|
+
dbd.close()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def edit(series:list, new_values:dict, dims:list=None, verbose=1):
|
|
319
|
+
"""Edit attribute values in a DICOM series
|
|
320
|
+
|
|
321
|
+
Warning: this function edits all values as requested. Please take care
|
|
322
|
+
when editing attributes that affect the DICOM file organisation, such as
|
|
323
|
+
UIDs, as this could corrupt the database.
|
|
324
|
+
|
|
325
|
+
Args:
|
|
326
|
+
series (list): DICOM series to edit
|
|
327
|
+
new_values (dict): dictionary with attribute: value pairs to write to the series
|
|
328
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
329
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
330
|
+
|
|
331
|
+
"""
|
|
332
|
+
dbd = open(series[0])
|
|
333
|
+
dbd.edit(series, new_values, dims=dims, verbose=verbose)
|
|
334
|
+
dbd.close()
|
|
335
|
+
|
|
336
|
+
def to_nifti(series:list, file:str, dims:list=None, verbose=1):
|
|
337
|
+
"""Save a DICOM series in nifti format.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
series (list): DICOM series to read
|
|
341
|
+
file (str): file path of the nifti file.
|
|
342
|
+
dims (list, optional): Non-spatial dimensions of the volume.
|
|
343
|
+
Defaults to None.
|
|
344
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
345
|
+
"""
|
|
346
|
+
dbd = open(series[0])
|
|
347
|
+
dbd.to_nifti(series, file, dims, verbose)
|
|
348
|
+
dbd.close()
|
|
349
|
+
|
|
350
|
+
def from_nifti(file:str, series:list, ref:list=None):
|
|
351
|
+
"""Create a DICOM series from a nifti file.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
file (str): file path of the nifti file.
|
|
355
|
+
series (list): DICOM series to create
|
|
356
|
+
ref (list): DICOM series to use as template.
|
|
357
|
+
"""
|
|
358
|
+
dbd = open(series[0])
|
|
359
|
+
dbd.from_nifti(file, series, ref)
|
|
360
|
+
dbd.close()
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def files(entity:list) -> list:
|
|
364
|
+
"""Read the files in a DICOM entity
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
entity (list or str): DICOM entity to read. This can
|
|
368
|
+
be a path to a folder containing DICOM files, or a
|
|
369
|
+
patient or study to read all series in that patient or
|
|
370
|
+
study.
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
list: list of valid dicom files.
|
|
374
|
+
"""
|
|
375
|
+
if isinstance(entity, str):
|
|
376
|
+
entity = [entity]
|
|
377
|
+
dbd = open(entity[0])
|
|
378
|
+
files = dbd.files(entity)
|
|
379
|
+
dbd.close()
|
|
380
|
+
return files
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def pixel_data(series:list, dims:list=None, verbose=1) -> tuple:
|
|
384
|
+
"""Read the pixel data from a DICOM series
|
|
385
|
+
|
|
386
|
+
Args:
|
|
387
|
+
series (list or str): DICOM series to read. This can also
|
|
388
|
+
be a path to a folder containing DICOM files, or a
|
|
389
|
+
patient or study to read all series in that patient or
|
|
390
|
+
study. In those cases a list is returned.
|
|
391
|
+
dims (list, optional): Dimensions of the array.
|
|
392
|
+
|
|
393
|
+
Returns:
|
|
394
|
+
numpy.ndarray or tuple: numpy array with pixel values, with
|
|
395
|
+
at least 3 dimensions (x,y,z).
|
|
396
|
+
"""
|
|
397
|
+
if isinstance(series, str):
|
|
398
|
+
series = [series]
|
|
399
|
+
dbd = open(series[0])
|
|
400
|
+
array = dbd.pixel_data(series, dims, verbose)
|
|
401
|
+
dbd.close()
|
|
402
|
+
return array
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def unique(pars:list, entity:list) -> dict:
|
|
406
|
+
"""Return a list of unique values for a DICOM entity
|
|
407
|
+
|
|
408
|
+
Args:
|
|
409
|
+
pars (list, str/tuple): attribute or attributes to return.
|
|
410
|
+
entity (list): DICOM entity to search (Patient, Study or Series)
|
|
411
|
+
|
|
412
|
+
Returns:
|
|
413
|
+
dict: if a pars is a list, this returns a dictionary with
|
|
414
|
+
unique values for each attribute. If pars is a scalar this returnes a list of values
|
|
415
|
+
"""
|
|
416
|
+
dbd = open(entity[0])
|
|
417
|
+
u = dbd.unique(pars, entity)
|
|
418
|
+
dbd.close()
|
|
419
|
+
return u
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
def archive(path, archive_path):
|
|
423
|
+
dbd = open(path)
|
|
424
|
+
dbd.archive(archive_path)
|
|
425
|
+
dbd.close()
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def restore(archive_path, path):
|
|
429
|
+
_copy_and_extract_zips(archive_path, path)
|
|
430
|
+
dbd = open(path)
|
|
431
|
+
dbd.close()
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def _copy_and_extract_zips(src_folder, dest_folder):
|
|
435
|
+
if not os.path.exists(dest_folder):
|
|
436
|
+
os.makedirs(dest_folder)
|
|
437
|
+
|
|
438
|
+
# First pass: count total files
|
|
439
|
+
total_files = sum(len(files) for _, _, files in os.walk(src_folder))
|
|
440
|
+
|
|
441
|
+
with tqdm(total=total_files, desc="Copying and extracting") as pbar:
|
|
442
|
+
for root, dirs, files in os.walk(src_folder):
|
|
443
|
+
rel_path = os.path.relpath(root, src_folder)
|
|
444
|
+
dest_path = os.path.join(dest_folder, rel_path)
|
|
445
|
+
os.makedirs(dest_path, exist_ok=True)
|
|
446
|
+
|
|
447
|
+
for file in files:
|
|
448
|
+
src_file_path = os.path.join(root, file)
|
|
449
|
+
dest_file_path = os.path.join(dest_path, file)
|
|
450
|
+
|
|
451
|
+
if file.lower().endswith('.zip'):
|
|
452
|
+
try:
|
|
453
|
+
zip_dest_folder = dest_file_path[:-4]
|
|
454
|
+
if os.path.exists(zip_dest_folder):
|
|
455
|
+
continue
|
|
456
|
+
with zipfile.ZipFile(src_file_path, 'r') as zip_ref:
|
|
457
|
+
zip_ref.extractall(zip_dest_folder)
|
|
458
|
+
#tqdm.write(f"Extracted ZIP: {src_file_path}")
|
|
459
|
+
#_flatten_folder(zip_dest_folder) # still needed?
|
|
460
|
+
except zipfile.BadZipFile:
|
|
461
|
+
tqdm.write(f"Bad ZIP file skipped: {src_file_path}")
|
|
462
|
+
else:
|
|
463
|
+
if os.path.exists(dest_file_path):
|
|
464
|
+
continue
|
|
465
|
+
shutil.copy2(src_file_path, dest_file_path)
|
|
466
|
+
|
|
467
|
+
pbar.update(1)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _flatten_folder(root_folder):
|
|
471
|
+
for dirpath, dirnames, filenames in os.walk(root_folder, topdown=False):
|
|
472
|
+
for filename in filenames:
|
|
473
|
+
src_path = os.path.join(dirpath, filename)
|
|
474
|
+
dst_path = os.path.join(root_folder, filename)
|
|
475
|
+
|
|
476
|
+
# If file with same name exists, optionally rename or skip
|
|
477
|
+
if os.path.exists(dst_path):
|
|
478
|
+
base, ext = os.path.splitext(filename)
|
|
479
|
+
counter = 1
|
|
480
|
+
while os.path.exists(dst_path):
|
|
481
|
+
dst_path = os.path.join(root_folder, f"{base}_{counter}{ext}")
|
|
482
|
+
counter += 1
|
|
483
|
+
|
|
484
|
+
shutil.move(src_path, dst_path)
|
|
485
|
+
|
|
486
|
+
# Remove empty subdirectories (but skip the root folder)
|
|
487
|
+
if dirpath != root_folder:
|
|
488
|
+
try:
|
|
489
|
+
os.rmdir(dirpath)
|
|
490
|
+
except OSError:
|
|
491
|
+
print(f"Could not remove {dirpath} — not empty or in use.")
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
if __name__=='__main__':
|
|
496
|
+
pass
|
dbdicom/const.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
|
|
2
|
+
|
|
3
|
+
PATIENT_MODULE = [
|
|
4
|
+
'ReferencedPatientSequence',
|
|
5
|
+
'PatientName',
|
|
6
|
+
'PatientID',
|
|
7
|
+
'IssuerOfPatientID',
|
|
8
|
+
'TypeOfPatientID',
|
|
9
|
+
'IssuerOfPatientIDQualifiersSequence',
|
|
10
|
+
'SourcePatientGroupIdentificationSequence',
|
|
11
|
+
'GroupOfPatientsIdentificationSequence',
|
|
12
|
+
'PatientBirthDate',
|
|
13
|
+
'PatientBirthTime',
|
|
14
|
+
'PatientBirthDateInAlternativeCalendar',
|
|
15
|
+
'PatientDeathDateInAlternativeCalendar',
|
|
16
|
+
'PatientAlternativeCalendar',
|
|
17
|
+
'PatientSex',
|
|
18
|
+
'QualityControlSubject',
|
|
19
|
+
'StrainDescription',
|
|
20
|
+
'StrainNomenclature',
|
|
21
|
+
'StrainStockSequence',
|
|
22
|
+
'StrainAdditionalInformation',
|
|
23
|
+
'StrainCodeSequence',
|
|
24
|
+
'GeneticModificationsSequence',
|
|
25
|
+
'OtherPatientNames',
|
|
26
|
+
'OtherPatientIDsSequence',
|
|
27
|
+
'ReferencedPatientPhotoSequence',
|
|
28
|
+
'EthnicGroup',
|
|
29
|
+
'PatientSpeciesDescription',
|
|
30
|
+
'PatientSpeciesCodeSequence',
|
|
31
|
+
'PatientBreedDescription',
|
|
32
|
+
'PatientBreedCodeSequence',
|
|
33
|
+
'BreedRegistrationSequence',
|
|
34
|
+
'ResponsiblePerson',
|
|
35
|
+
'ResponsiblePersonRole',
|
|
36
|
+
'ResponsibleOrganization',
|
|
37
|
+
'PatientComments',
|
|
38
|
+
'PatientIdentityRemoved',
|
|
39
|
+
'DeidentificationMethod',
|
|
40
|
+
'DeidentificationMethodCodeSequence',
|
|
41
|
+
'ClinicalTrialSponsorName',
|
|
42
|
+
'ClinicalTrialProtocolID',
|
|
43
|
+
'ClinicalTrialProtocolName',
|
|
44
|
+
'ClinicalTrialSiteID',
|
|
45
|
+
'ClinicalTrialSiteName',
|
|
46
|
+
'ClinicalTrialSubjectID',
|
|
47
|
+
'ClinicalTrialSubjectReadingID',
|
|
48
|
+
'ClinicalTrialProtocolEthicsCommitteeName',
|
|
49
|
+
'ClinicalTrialProtocolEthicsCommitteeApprovalNumber',
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
STUDY_MODULE = [
|
|
55
|
+
'StudyDate',
|
|
56
|
+
'StudyTime',
|
|
57
|
+
'AccessionNumber',
|
|
58
|
+
'IssuerOfAccessionNumberSequence',
|
|
59
|
+
'ReferringPhysicianName',
|
|
60
|
+
'ReferringPhysicianIdentificationSequence',
|
|
61
|
+
'ConsultingPhysicianName',
|
|
62
|
+
'ConsultingPhysicianIdentificationSequence',
|
|
63
|
+
'StudyDescription',
|
|
64
|
+
'ProcedureCodeSequence',
|
|
65
|
+
'PhysiciansOfRecord',
|
|
66
|
+
'PhysiciansOfRecordIdentificationSequence',
|
|
67
|
+
'NameOfPhysiciansReadingStudy',
|
|
68
|
+
'PhysiciansReadingStudyIdentificationSequence',
|
|
69
|
+
'ReferencedStudySequence',
|
|
70
|
+
'StudyInstanceUID',
|
|
71
|
+
'StudyID',
|
|
72
|
+
'RequestingService',
|
|
73
|
+
'RequestingServiceCodeSequence',
|
|
74
|
+
'ReasonForPerformedProcedureCodeSequence',
|
|
75
|
+
'AdmittingDiagnosesDescription',
|
|
76
|
+
'AdmittingDiagnosesCodeSequence',
|
|
77
|
+
'PatientAge',
|
|
78
|
+
'PatientSize',
|
|
79
|
+
'PatientSizeCodeSequence',
|
|
80
|
+
'PatientBodyMassIndex',
|
|
81
|
+
'MeasuredAPDimension',
|
|
82
|
+
'MeasuredLateralDimension',
|
|
83
|
+
'PatientWeight',
|
|
84
|
+
'MedicalAlerts',
|
|
85
|
+
'Allergies',
|
|
86
|
+
'Occupation',
|
|
87
|
+
'SmokingStatus',
|
|
88
|
+
'AdditionalPatientHistory',
|
|
89
|
+
'PregnancyStatus',
|
|
90
|
+
'LastMenstrualDate',
|
|
91
|
+
'PatientSexNeutered',
|
|
92
|
+
'ReasonForVisit',
|
|
93
|
+
'ReasonForVisitCodeSequence',
|
|
94
|
+
'AdmissionID',
|
|
95
|
+
'IssuerOfAdmissionIDSequence',
|
|
96
|
+
'ServiceEpisodeID',
|
|
97
|
+
'ServiceEpisodeDescription',
|
|
98
|
+
'IssuerOfServiceEpisodeIDSequence',
|
|
99
|
+
'PatientState',
|
|
100
|
+
'ClinicalTrialTimePointID',
|
|
101
|
+
'ClinicalTrialTimePointDescription',
|
|
102
|
+
'LongitudinalTemporalOffsetFromEvent',
|
|
103
|
+
'LongitudinalTemporalEventType',
|
|
104
|
+
'ConsentForClinicalTrialUseSequence',
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
SERIES_MODULE = [
|
|
110
|
+
'SeriesDate',
|
|
111
|
+
'SeriesTime',
|
|
112
|
+
'Modality',
|
|
113
|
+
'SeriesDescription',
|
|
114
|
+
'SeriesDescriptionCodeSequence',
|
|
115
|
+
'PerformingPhysicianName',
|
|
116
|
+
'PerformingPhysicianIdentificationSequence',
|
|
117
|
+
'OperatorsName',
|
|
118
|
+
'OperatorIdentificationSequence',
|
|
119
|
+
'ReferencedPerformedProcedureStepSequence',
|
|
120
|
+
'RelatedSeriesSequence',
|
|
121
|
+
'AnatomicalOrientationType',
|
|
122
|
+
'BodyPartExamined',
|
|
123
|
+
'ProtocolName',
|
|
124
|
+
'PatientPosition',
|
|
125
|
+
'ReferencedDefinedProtocolSequence',
|
|
126
|
+
'ReferencedPerformedProtocolSequence',
|
|
127
|
+
'SeriesInstanceUID',
|
|
128
|
+
'SeriesNumber',
|
|
129
|
+
'Laterality',
|
|
130
|
+
'SmallestPixelValueInSeries',
|
|
131
|
+
'LargestPixelValueInSeries',
|
|
132
|
+
'PerformedProcedureStepStartDate',
|
|
133
|
+
'PerformedProcedureStepStartTime',
|
|
134
|
+
'PerformedProcedureStepEndDate',
|
|
135
|
+
'PerformedProcedureStepEndTime',
|
|
136
|
+
'PerformedProcedureStepID',
|
|
137
|
+
'PerformedProcedureStepDescription',
|
|
138
|
+
'PerformedProtocolCodeSequence',
|
|
139
|
+
'RequestAttributesSequence',
|
|
140
|
+
'CommentsOnThePerformedProcedureStep',
|
|
141
|
+
'ClinicalTrialCoordinatingCenterName',
|
|
142
|
+
'ClinicalTrialSeriesID',
|
|
143
|
+
'ClinicalTrialSeriesDescription',
|
|
144
|
+
]
|