dbdicom 0.3.8__py3-none-any.whl → 0.3.10__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.
Potentially problematic release.
This version of dbdicom might be problematic. Click here for more details.
- dbdicom/api.py +60 -46
- dbdicom/database.py +2 -2
- dbdicom/dataset.py +30 -316
- dbdicom/dbd.py +302 -162
- dbdicom/register.py +21 -0
- dbdicom/sop_classes/enhanced_mr_image.py +190 -271
- dbdicom/utils/arrays.py +124 -36
- dbdicom/utils/image.py +13 -13
- dbdicom/utils/pydicom_dataset.py +386 -0
- {dbdicom-0.3.8.dist-info → dbdicom-0.3.10.dist-info}/METADATA +1 -1
- {dbdicom-0.3.8.dist-info → dbdicom-0.3.10.dist-info}/RECORD +14 -14
- dbdicom/utils/variables.py +0 -161
- {dbdicom-0.3.8.dist-info → dbdicom-0.3.10.dist-info}/WHEEL +0 -0
- {dbdicom-0.3.8.dist-info → dbdicom-0.3.10.dist-info}/licenses/LICENSE +0 -0
- {dbdicom-0.3.8.dist-info → dbdicom-0.3.10.dist-info}/top_level.txt +0 -0
dbdicom/dbd.py
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
2
|
+
import shutil
|
|
3
3
|
import json
|
|
4
4
|
from typing import Union
|
|
5
5
|
import zipfile
|
|
6
6
|
import re
|
|
7
|
+
from copy import deepcopy
|
|
7
8
|
|
|
8
9
|
from tqdm import tqdm
|
|
9
10
|
import numpy as np
|
|
10
11
|
import vreg
|
|
11
12
|
from pydicom.dataset import Dataset
|
|
13
|
+
import pydicom
|
|
12
14
|
|
|
13
15
|
import dbdicom.utils.arrays
|
|
14
16
|
import dbdicom.dataset as dbdataset
|
|
15
17
|
import dbdicom.database as dbdatabase
|
|
16
18
|
import dbdicom.register as register
|
|
17
19
|
import dbdicom.const as const
|
|
20
|
+
from dbdicom.utils.pydicom_dataset import (
|
|
21
|
+
get_values,
|
|
22
|
+
set_values,
|
|
23
|
+
set_value,
|
|
24
|
+
)
|
|
18
25
|
|
|
19
26
|
|
|
20
27
|
|
|
@@ -71,14 +78,16 @@ class DataBaseDicom():
|
|
|
71
78
|
Args:
|
|
72
79
|
entity (list): entity to delete
|
|
73
80
|
"""
|
|
81
|
+
# delete datasets on disk
|
|
74
82
|
removed = register.index(self.register, entity)
|
|
75
|
-
# delete datasets marked for removal
|
|
76
83
|
for index in removed:
|
|
77
84
|
file = os.path.join(self.path, index)
|
|
78
85
|
if os.path.exists(file):
|
|
79
86
|
os.remove(file)
|
|
80
|
-
#
|
|
81
|
-
|
|
87
|
+
# drop the entity from the register
|
|
88
|
+
register.remove(self.register, entity)
|
|
89
|
+
# cleanup empty folders
|
|
90
|
+
remove_empty_folders(entity[0])
|
|
82
91
|
return self
|
|
83
92
|
|
|
84
93
|
|
|
@@ -201,21 +210,22 @@ class DataBaseDicom():
|
|
|
201
210
|
return register.series(self.register, entity, desc, contains, isin)
|
|
202
211
|
|
|
203
212
|
|
|
204
|
-
def volume(self, entity:Union[list, str], dims:list=None) ->
|
|
205
|
-
"""Read volume
|
|
213
|
+
def volume(self, entity:Union[list, str], dims:list=None, verbose=1) -> vreg.Volume3D:
|
|
214
|
+
"""Read volume.
|
|
206
215
|
|
|
207
216
|
Args:
|
|
208
|
-
entity (list, str): DICOM
|
|
217
|
+
entity (list, str): DICOM series to read
|
|
209
218
|
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
219
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
210
220
|
|
|
211
221
|
Returns:
|
|
212
|
-
vreg.Volume3D
|
|
213
|
-
a volume, else a list of volumes.
|
|
222
|
+
vreg.Volume3D:
|
|
214
223
|
"""
|
|
215
|
-
if isinstance(entity, str): # path to folder
|
|
216
|
-
|
|
217
|
-
if len(entity) < 4: # folder, patient or study
|
|
218
|
-
|
|
224
|
+
# if isinstance(entity, str): # path to folder
|
|
225
|
+
# return [self.volume(s, dims) for s in self.series(entity)]
|
|
226
|
+
# if len(entity) < 4: # folder, patient or study
|
|
227
|
+
# return [self.volume(s, dims) for s in self.series(entity)]
|
|
228
|
+
|
|
219
229
|
if dims is None:
|
|
220
230
|
dims = []
|
|
221
231
|
elif isinstance(dims, str):
|
|
@@ -224,31 +234,39 @@ class DataBaseDicom():
|
|
|
224
234
|
dims = list(dims)
|
|
225
235
|
dims = ['SliceLocation'] + dims
|
|
226
236
|
|
|
227
|
-
files = register.files(self.register, entity)
|
|
228
|
-
|
|
229
237
|
# Read dicom files
|
|
230
|
-
values = []
|
|
238
|
+
values = [[] for _ in dims]
|
|
231
239
|
volumes = []
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
240
|
+
|
|
241
|
+
files = register.files(self.register, entity)
|
|
242
|
+
for f in tqdm(files, desc='Reading volume..', disable=(verbose==0)):
|
|
243
|
+
ds = pydicom.dcmread(f)
|
|
244
|
+
values_f = get_values(ds, dims)
|
|
245
|
+
for d in range(len(dims)):
|
|
246
|
+
values[d].append(values_f[d])
|
|
235
247
|
volumes.append(dbdataset.volume(ds))
|
|
236
248
|
|
|
237
|
-
# Format as mesh
|
|
238
|
-
coords = np.
|
|
249
|
+
# Format coordinates as mesh
|
|
250
|
+
coords = [np.array(v) for v in values]
|
|
239
251
|
coords, inds = dbdicom.utils.arrays.meshvals(coords)
|
|
240
|
-
vols = np.array(volumes)
|
|
241
|
-
vols = vols[inds].reshape(coords.shape[1:])
|
|
242
252
|
|
|
243
253
|
# Check that all slices have the same coordinates
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
254
|
+
if len(dims) > 1:
|
|
255
|
+
# Loop over all coordinates after slice location
|
|
256
|
+
for c in coords[1:]:
|
|
257
|
+
# Loop over all slice locations
|
|
258
|
+
for k in range(1, c.shape[0]):
|
|
259
|
+
# Coordinate c of slice k
|
|
260
|
+
if not np.array_equal(c[k,...], c[0,...]):
|
|
261
|
+
raise ValueError(
|
|
262
|
+
"Cannot build a single volume. Not all slices "
|
|
263
|
+
"have the same coordinates."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Build volumes
|
|
267
|
+
vols = np.array(volumes)
|
|
268
|
+
vols = vols[inds].reshape(coords[0].shape)
|
|
269
|
+
|
|
252
270
|
# Infer spacing between slices from slice locations
|
|
253
271
|
# Technically only necessary if SpacingBetweenSlices not set or incorrect
|
|
254
272
|
vols = infer_slice_spacing(vols)
|
|
@@ -264,11 +282,66 @@ class DataBaseDicom():
|
|
|
264
282
|
# Then try again
|
|
265
283
|
vol = vreg.join(vols)
|
|
266
284
|
if vol.ndim > 3:
|
|
285
|
+
# Coordinates of slice 0
|
|
286
|
+
c0 = [c[0,...] for c in coords[1:]]
|
|
267
287
|
vol.set_coords(c0)
|
|
268
288
|
vol.set_dims(dims[1:])
|
|
269
289
|
return vol
|
|
270
290
|
|
|
271
|
-
|
|
291
|
+
|
|
292
|
+
def values(self, series:list, *attr, dims:list=None, verbose=1) -> Union[dict, tuple]:
|
|
293
|
+
"""Read the values of some attributes from a DICOM series
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
series (list): DICOM series to read.
|
|
297
|
+
attr (tuple, optional): DICOM attributes to read.
|
|
298
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
299
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
tuple: arrays with values for the attributes.
|
|
303
|
+
"""
|
|
304
|
+
# if isinstance(series, str): # path to folder
|
|
305
|
+
# return [self.values(s, attr, dims) for s in self.series(series)]
|
|
306
|
+
# if len(series) < 4: # folder, patient or study
|
|
307
|
+
# return [self.values(s, attr, dims) for s in self.series(series)]
|
|
308
|
+
|
|
309
|
+
if dims is None:
|
|
310
|
+
dims = ['InstanceNumber']
|
|
311
|
+
elif np.isscalar(dims):
|
|
312
|
+
dims = [dims]
|
|
313
|
+
else:
|
|
314
|
+
dims = list(dims)
|
|
315
|
+
|
|
316
|
+
# Read dicom files
|
|
317
|
+
coord_values = [[] for _ in dims]
|
|
318
|
+
attr_values = [[] for _ in attr]
|
|
319
|
+
|
|
320
|
+
files = register.files(self.register, series)
|
|
321
|
+
for f in tqdm(files, desc='Reading values..', disable=(verbose==0)):
|
|
322
|
+
ds = pydicom.dcmread(f)
|
|
323
|
+
coord_values_f = get_values(ds, dims)
|
|
324
|
+
for d in range(len(dims)):
|
|
325
|
+
coord_values[d].append(coord_values_f[d])
|
|
326
|
+
attr_values_f = get_values(ds, attr)
|
|
327
|
+
for a in range(len(attr)):
|
|
328
|
+
attr_values[a].append(attr_values_f[a])
|
|
329
|
+
|
|
330
|
+
# Format coordinates as mesh
|
|
331
|
+
coords = [np.array(v) for v in coord_values]
|
|
332
|
+
coords, inds = dbdicom.utils.arrays.meshvals(coords)
|
|
333
|
+
|
|
334
|
+
# Sort values accordingly
|
|
335
|
+
values = [np.array(v) for v in attr_values]
|
|
336
|
+
values = [v[inds].reshape(coords[0].shape) for v in values]
|
|
337
|
+
|
|
338
|
+
# Return values
|
|
339
|
+
if len(values) == 1:
|
|
340
|
+
return values[0]
|
|
341
|
+
else:
|
|
342
|
+
return tuple(values)
|
|
343
|
+
|
|
344
|
+
|
|
272
345
|
def write_volume(
|
|
273
346
|
self, vol:Union[vreg.Volume3D, tuple], series:list,
|
|
274
347
|
ref:list=None,
|
|
@@ -280,6 +353,10 @@ class DataBaseDicom():
|
|
|
280
353
|
series (list): DICOM series to read
|
|
281
354
|
ref (list): Reference series
|
|
282
355
|
"""
|
|
356
|
+
series_full_name = full_name(series)
|
|
357
|
+
if series_full_name in self.series():
|
|
358
|
+
raise ValueError(f"Series {series_full_name[-1]} already exists in study {series_full_name[-2]}.")
|
|
359
|
+
|
|
283
360
|
if isinstance(vol, tuple):
|
|
284
361
|
vol = vreg.volume(vol[0], vol[1])
|
|
285
362
|
if ref is None:
|
|
@@ -292,7 +369,7 @@ class DataBaseDicom():
|
|
|
292
369
|
ref_mgr = DataBaseDicom(ref[0])
|
|
293
370
|
files = register.files(ref_mgr.register, ref)
|
|
294
371
|
ref_mgr.close()
|
|
295
|
-
ds =
|
|
372
|
+
ds = pydicom.dcmread(files[0])
|
|
296
373
|
|
|
297
374
|
# Get the attributes of the destination series
|
|
298
375
|
attr = self._series_attributes(series)
|
|
@@ -307,15 +384,91 @@ class DataBaseDicom():
|
|
|
307
384
|
i=0
|
|
308
385
|
vols = vol.separate().reshape(-1)
|
|
309
386
|
for vt in tqdm(vols, desc='Writing volume..'):
|
|
310
|
-
|
|
387
|
+
slices = vt.split()
|
|
388
|
+
for sl in slices:
|
|
311
389
|
dbdataset.set_volume(ds, sl)
|
|
312
|
-
|
|
390
|
+
sl_coords = [c.ravel()[0] for c in sl.coords]
|
|
391
|
+
set_value(ds, sl.dims, sl_coords)
|
|
313
392
|
self._write_dataset(ds, attr, n + 1 + i)
|
|
314
393
|
i+=1
|
|
315
394
|
return self
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def edit(
|
|
398
|
+
self, series:list, new_values:dict, dims:list=None, verbose=1,
|
|
399
|
+
):
|
|
400
|
+
"""Edit attribute values in a new DICOM series
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
series (list): DICOM series to edit
|
|
404
|
+
new_values (dict): dictionary with attribute: value pairs to write to the series
|
|
405
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
406
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
if dims is None:
|
|
410
|
+
dims = ['InstanceNumber']
|
|
411
|
+
elif np.isscalar(dims):
|
|
412
|
+
dims = [dims]
|
|
413
|
+
else:
|
|
414
|
+
dims = list(dims)
|
|
415
|
+
|
|
416
|
+
# Check that all values have the correct nr of elements
|
|
417
|
+
files = register.files(self.register, series)
|
|
418
|
+
for a in new_values.values():
|
|
419
|
+
if np.isscalar(a):
|
|
420
|
+
pass
|
|
421
|
+
elif np.array(a).size != len(files):
|
|
422
|
+
raise ValueError(
|
|
423
|
+
f"Incorrect value lengths. All values need to have {len(files)} elements"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Read dicom files to sort them
|
|
427
|
+
coord_values = [[] for _ in dims]
|
|
428
|
+
for f in tqdm(files, desc='Sorting series..', disable=(verbose==0)):
|
|
429
|
+
ds = pydicom.dcmread(f)
|
|
430
|
+
coord_values_f = get_values(ds, dims)
|
|
431
|
+
for d in range(len(dims)):
|
|
432
|
+
coord_values[d].append(coord_values_f[d])
|
|
433
|
+
|
|
434
|
+
# Format coordinates as mesh
|
|
435
|
+
coords = [np.array(v) for v in coord_values]
|
|
436
|
+
coords, inds = dbdicom.utils.arrays.meshvals(coords)
|
|
437
|
+
|
|
438
|
+
# Sort files accordingly
|
|
439
|
+
files = np.array(files)[inds]
|
|
440
|
+
|
|
441
|
+
# Now edit and write the files
|
|
442
|
+
attr = self._series_attributes(series)
|
|
443
|
+
n = self._max_instance_number(attr['SeriesInstanceUID'])
|
|
444
|
+
|
|
445
|
+
# Drop existing attributes if they are edited
|
|
446
|
+
attr = {a:attr[a] for a in attr if a not in new_values}
|
|
447
|
+
|
|
448
|
+
# List instances to be edited
|
|
449
|
+
to_drop = register.index(self.register, series)
|
|
450
|
+
|
|
451
|
+
# Write the instances
|
|
452
|
+
tags = list(new_values.keys())
|
|
453
|
+
for i, f in tqdm(enumerate(files), desc='Writing values..', disable=(verbose==0)):
|
|
454
|
+
ds = pydicom.dcmread(f)
|
|
455
|
+
values = []
|
|
456
|
+
for a in new_values.values():
|
|
457
|
+
if np.isscalar(a):
|
|
458
|
+
values.append(a)
|
|
459
|
+
else:
|
|
460
|
+
values.append(np.array(a).reshape(-1)[i])
|
|
461
|
+
set_values(ds, tags, values)
|
|
462
|
+
self._write_dataset(ds, attr, n + 1 + i)
|
|
463
|
+
|
|
464
|
+
# Delete the originals files
|
|
465
|
+
register.drop(self.register, to_drop)
|
|
466
|
+
[os.remove(os.path.join(self.path, idx)) for idx in to_drop]
|
|
467
|
+
|
|
468
|
+
return self
|
|
316
469
|
|
|
317
470
|
|
|
318
|
-
def to_nifti(self, series:list, file:str, dims=None):
|
|
471
|
+
def to_nifti(self, series:list, file:str, dims=None, verbose=1):
|
|
319
472
|
"""Save a DICOM series in nifti format.
|
|
320
473
|
|
|
321
474
|
Args:
|
|
@@ -323,8 +476,10 @@ class DataBaseDicom():
|
|
|
323
476
|
file (str): file path of the nifti file.
|
|
324
477
|
dims (list, optional): Non-spatial dimensions of the volume.
|
|
325
478
|
Defaults to None.
|
|
479
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
480
|
+
|
|
326
481
|
"""
|
|
327
|
-
vol = self.volume(series, dims)
|
|
482
|
+
vol = self.volume(series, dims, verbose)
|
|
328
483
|
vreg.write_nifti(vol, file)
|
|
329
484
|
return self
|
|
330
485
|
|
|
@@ -390,12 +545,12 @@ class DataBaseDicom():
|
|
|
390
545
|
if attr is not None:
|
|
391
546
|
values = np.empty(len(files), dtype=dict)
|
|
392
547
|
for i, f in tqdm(enumerate(files), desc='Reading pixel data..'):
|
|
393
|
-
ds =
|
|
394
|
-
coords_array.append(
|
|
548
|
+
ds = pydicom.dcmread(f)
|
|
549
|
+
coords_array.append(get_values(ds, dims))
|
|
395
550
|
# save as dict so numpy does not stack as arrays
|
|
396
551
|
arrays[i] = {'pixel_data': dbdataset.pixel_data(ds)}
|
|
397
552
|
if attr is not None:
|
|
398
|
-
values[i] = {'values':
|
|
553
|
+
values[i] = {'values': get_values(ds, params)}
|
|
399
554
|
|
|
400
555
|
# Format as mesh
|
|
401
556
|
coords_array = np.stack([v for v in coords_array], axis=-1)
|
|
@@ -444,103 +599,7 @@ class DataBaseDicom():
|
|
|
444
599
|
return arrays, values_return
|
|
445
600
|
|
|
446
601
|
|
|
447
|
-
def values(self, series:list, attr=None, dims:list=None, coords=False) -> Union[dict, tuple]:
|
|
448
|
-
"""Read the values of some or all attributes from a DICOM series
|
|
449
|
-
|
|
450
|
-
Args:
|
|
451
|
-
series (list or str): DICOM series to read. This can also
|
|
452
|
-
be a path to a folder containing DICOM files, or a
|
|
453
|
-
patient or study to read all series in that patient or
|
|
454
|
-
study. In those cases a list is returned.
|
|
455
|
-
attr (list, optional): list of DICOM attributes to read.
|
|
456
|
-
dims (list, optional): Dimensions to sort the attributes.
|
|
457
|
-
If dims is not provided, values are sorted by
|
|
458
|
-
InstanceNumber.
|
|
459
|
-
coords (bool): If set to True, the coordinates of the
|
|
460
|
-
attributes are returned alongside the values
|
|
461
|
-
|
|
462
|
-
Returns:
|
|
463
|
-
dict or tuple: values as a dictionary in the last
|
|
464
|
-
return value, where each value is a numpy array with
|
|
465
|
-
the required dimensions. If coords is set to True,
|
|
466
|
-
these are returned too.
|
|
467
|
-
"""
|
|
468
|
-
if isinstance(series, str): # path to folder
|
|
469
|
-
return [self.values(s, attr, dims, coords) for s in self.series(series)]
|
|
470
|
-
if len(series) < 4: # folder, patient or study
|
|
471
|
-
return [self.values(s, attr, dims, coords) for s in self.series(series)]
|
|
472
602
|
|
|
473
|
-
if dims is None:
|
|
474
|
-
dims = ['InstanceNumber']
|
|
475
|
-
elif np.isscalar(dims):
|
|
476
|
-
dims = [dims]
|
|
477
|
-
else:
|
|
478
|
-
dims = list(dims)
|
|
479
|
-
|
|
480
|
-
files = register.files(self.register, series)
|
|
481
|
-
|
|
482
|
-
# Ensure return_vals is a list
|
|
483
|
-
if attr is None:
|
|
484
|
-
# If attributes are not provided, read all
|
|
485
|
-
# attributes from the first file
|
|
486
|
-
ds = dbdataset.read_dataset(files[0])
|
|
487
|
-
exclude = ['PixelData', 'FloatPixelData', 'DoubleFloatPixelData']
|
|
488
|
-
params = []
|
|
489
|
-
param_labels = []
|
|
490
|
-
for elem in ds:
|
|
491
|
-
if elem.keyword not in exclude:
|
|
492
|
-
params.append(elem.tag)
|
|
493
|
-
# For known tags use the keyword as label
|
|
494
|
-
label = elem.tag if len(elem.keyword)==0 else elem.keyword
|
|
495
|
-
param_labels.append(label)
|
|
496
|
-
elif np.isscalar(attr):
|
|
497
|
-
params = [attr]
|
|
498
|
-
param_labels = params[:]
|
|
499
|
-
else:
|
|
500
|
-
params = list(attr)
|
|
501
|
-
param_labels = params[:]
|
|
502
|
-
|
|
503
|
-
# Read dicom files
|
|
504
|
-
coords_array = []
|
|
505
|
-
values = np.empty(len(files), dtype=dict)
|
|
506
|
-
for i, f in tqdm(enumerate(files), desc='Reading values..'):
|
|
507
|
-
ds = dbdataset.read_dataset(f)
|
|
508
|
-
coords_array.append(dbdataset.get_values(ds, dims))
|
|
509
|
-
# save as dict so numpy does not stack as arrays
|
|
510
|
-
values[i] = {'values': dbdataset.get_values(ds, params)}
|
|
511
|
-
|
|
512
|
-
# Format as mesh
|
|
513
|
-
coords_array = np.stack([v for v in coords_array], axis=-1)
|
|
514
|
-
coords_array, inds = dbdicom.utils.arrays.meshvals(coords_array)
|
|
515
|
-
|
|
516
|
-
# Sort values accordingly
|
|
517
|
-
values = values[inds].reshape(-1)
|
|
518
|
-
|
|
519
|
-
# Return values as a dictionary
|
|
520
|
-
values_dict = {}
|
|
521
|
-
for p in range(len(params)):
|
|
522
|
-
# Get the type from the first value
|
|
523
|
-
vp0 = values[0]['values'][p]
|
|
524
|
-
# Build an array of the right type
|
|
525
|
-
vp = np.zeros(values.size, dtype=type(vp0))
|
|
526
|
-
# Populate the arrate with values for parameter p
|
|
527
|
-
for i, v in enumerate(values):
|
|
528
|
-
vp[i] = v['values'][p]
|
|
529
|
-
# Reshape values for parameter p
|
|
530
|
-
vp = vp.reshape(coords_array.shape[1:])
|
|
531
|
-
# Eneter in the dictionary
|
|
532
|
-
values_dict[param_labels[p]] = vp
|
|
533
|
-
|
|
534
|
-
# If only one, return as value
|
|
535
|
-
if len(params) == 1:
|
|
536
|
-
values_return = values_dict[params[0]]
|
|
537
|
-
else:
|
|
538
|
-
values_return = values_dict
|
|
539
|
-
|
|
540
|
-
if coords:
|
|
541
|
-
return values_return, coords_array
|
|
542
|
-
else:
|
|
543
|
-
return values_return
|
|
544
603
|
|
|
545
604
|
|
|
546
605
|
def files(self, entity:list) -> list:
|
|
@@ -610,34 +669,66 @@ class DataBaseDicom():
|
|
|
610
669
|
else:
|
|
611
670
|
return {p: values[i] for i, p in enumerate(pars)}
|
|
612
671
|
|
|
613
|
-
def copy(self, from_entity, to_entity):
|
|
672
|
+
def copy(self, from_entity, to_entity=None):
|
|
614
673
|
"""Copy a DICOM entity (patient, study or series)
|
|
615
674
|
|
|
616
675
|
Args:
|
|
617
676
|
from_entity (list): entity to copy
|
|
618
|
-
to_entity (list): entity after copying.
|
|
677
|
+
to_entity (list, optional): entity after copying. If this is not
|
|
678
|
+
provided, a copy will be made in the same study and returned
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
entity: the copied entity. If th to_entity is provided, this is
|
|
682
|
+
returned.
|
|
619
683
|
"""
|
|
620
684
|
if len(from_entity) == 4:
|
|
685
|
+
if to_entity is None:
|
|
686
|
+
to_entity = deepcopy(from_entity)
|
|
687
|
+
if isinstance(to_entity[-1], tuple):
|
|
688
|
+
to_entity[-1] = (to_entity[-1][0] + '_copy', 0)
|
|
689
|
+
else:
|
|
690
|
+
to_entity[-1] = (to_entity[-1] + '_copy', 0)
|
|
691
|
+
while to_entity in self.series():
|
|
692
|
+
to_entity[-1][1] += 1
|
|
621
693
|
if len(to_entity) != 4:
|
|
622
694
|
raise ValueError(
|
|
623
695
|
f"Cannot copy series {from_entity} to series {to_entity}. "
|
|
624
696
|
f"{to_entity} is not a series (needs 4 elements)."
|
|
625
697
|
)
|
|
626
|
-
|
|
698
|
+
self._copy_series(from_entity, to_entity)
|
|
699
|
+
return to_entity
|
|
700
|
+
|
|
627
701
|
if len(from_entity) == 3:
|
|
702
|
+
if to_entity is None:
|
|
703
|
+
to_entity = deepcopy(from_entity)
|
|
704
|
+
if isinstance(to_entity[-1], tuple):
|
|
705
|
+
to_entity[-1] = (to_entity[-1][0] + '_copy', 0)
|
|
706
|
+
else:
|
|
707
|
+
to_entity[-1] = (to_entity[-1] + '_copy', 0)
|
|
708
|
+
while to_entity in self.studies():
|
|
709
|
+
to_entity[-1][1] += 1
|
|
628
710
|
if len(to_entity) != 3:
|
|
629
711
|
raise ValueError(
|
|
630
712
|
f"Cannot copy study {from_entity} to study {to_entity}. "
|
|
631
713
|
f"{to_entity} is not a study (needs 3 elements)."
|
|
632
714
|
)
|
|
633
|
-
|
|
715
|
+
self._copy_study(from_entity, to_entity)
|
|
716
|
+
return to_entity
|
|
717
|
+
|
|
634
718
|
if len(from_entity) == 2:
|
|
719
|
+
if to_entity is None:
|
|
720
|
+
to_entity = deepcopy(from_entity)
|
|
721
|
+
to_entity[-1] += '_copy'
|
|
722
|
+
while to_entity in self.patients():
|
|
723
|
+
to_entity[-1] += '_copy'
|
|
635
724
|
if len(to_entity) != 2:
|
|
636
725
|
raise ValueError(
|
|
637
726
|
f"Cannot copy patient {from_entity} to patient {to_entity}. "
|
|
638
727
|
f"{to_entity} is not a patient (needs 2 elements)."
|
|
639
728
|
)
|
|
640
|
-
|
|
729
|
+
self._copy_patient(from_entity, to_entity)
|
|
730
|
+
return to_entity
|
|
731
|
+
|
|
641
732
|
raise ValueError(
|
|
642
733
|
f"Cannot copy {from_entity} to {to_entity}. "
|
|
643
734
|
)
|
|
@@ -670,8 +761,8 @@ class DataBaseDicom():
|
|
|
670
761
|
files = []
|
|
671
762
|
values = []
|
|
672
763
|
for f in tqdm(all_files, desc=f'Reading {attr}'):
|
|
673
|
-
ds =
|
|
674
|
-
v =
|
|
764
|
+
ds = pydicom.dcmread(f)
|
|
765
|
+
v = get_values(ds, attr)
|
|
675
766
|
if key is not None:
|
|
676
767
|
v = key(v)
|
|
677
768
|
if v in values:
|
|
@@ -701,8 +792,8 @@ class DataBaseDicom():
|
|
|
701
792
|
files = register.files(self.register, entity)
|
|
702
793
|
v = np.empty((len(files), len(attributes)), dtype=object)
|
|
703
794
|
for i, f in enumerate(files):
|
|
704
|
-
ds =
|
|
705
|
-
v[i,:] =
|
|
795
|
+
ds = pydicom.dcmread(f)
|
|
796
|
+
v[i,:] = get_values(ds, attributes)
|
|
706
797
|
return v
|
|
707
798
|
|
|
708
799
|
def _copy_patient(self, from_patient, to_patient):
|
|
@@ -757,7 +848,7 @@ class DataBaseDicom():
|
|
|
757
848
|
# Copy the files to the new series
|
|
758
849
|
for i, f in tqdm(enumerate(files), total=len(files), desc=f'Copying series {to_series[1:]}'):
|
|
759
850
|
# Read dataset and assign new properties
|
|
760
|
-
ds =
|
|
851
|
+
ds = pydicom.dcmread(f)
|
|
761
852
|
self._write_dataset(ds, attr, n + 1 + i)
|
|
762
853
|
|
|
763
854
|
def _max_study_id(self, patient_id):
|
|
@@ -807,8 +898,8 @@ class DataBaseDicom():
|
|
|
807
898
|
# If the patient exists and has files, read from file
|
|
808
899
|
files = register.files(self.register, patient)
|
|
809
900
|
attr = const.PATIENT_MODULE
|
|
810
|
-
ds =
|
|
811
|
-
vals =
|
|
901
|
+
ds = pydicom.dcmread(files[0])
|
|
902
|
+
vals = get_values(ds, attr)
|
|
812
903
|
except:
|
|
813
904
|
# If the patient does not exist, generate values
|
|
814
905
|
if patient in self.patients():
|
|
@@ -827,8 +918,8 @@ class DataBaseDicom():
|
|
|
827
918
|
# If the study exists and has files, read from file
|
|
828
919
|
files = register.files(self.register, study)
|
|
829
920
|
attr = const.STUDY_MODULE
|
|
830
|
-
ds =
|
|
831
|
-
vals =
|
|
921
|
+
ds = pydicom.dcmread(files[0])
|
|
922
|
+
vals = get_values(ds, attr)
|
|
832
923
|
except register.AmbiguousError as e:
|
|
833
924
|
raise register.AmbiguousError(e)
|
|
834
925
|
except:
|
|
@@ -836,9 +927,9 @@ class DataBaseDicom():
|
|
|
836
927
|
if study[:-1] not in self.patients():
|
|
837
928
|
study_id = 1
|
|
838
929
|
else:
|
|
839
|
-
study_id = 1 + self._max_study_id(study[
|
|
930
|
+
study_id = 1 + self._max_study_id(study[1])
|
|
840
931
|
attr = ['StudyInstanceUID', 'StudyDescription', 'StudyID']
|
|
841
|
-
study_uid =
|
|
932
|
+
study_uid = pydicom.uid.generate_uid()
|
|
842
933
|
study_desc = study[-1] if isinstance(study[-1], str) else study[-1][0]
|
|
843
934
|
#study_date = datetime.today().strftime('%Y%m%d')
|
|
844
935
|
vals = [study_uid, study_desc, str(study_id)]
|
|
@@ -851,8 +942,8 @@ class DataBaseDicom():
|
|
|
851
942
|
# If the series exists and has files, read from file
|
|
852
943
|
files = register.files(self.register, series)
|
|
853
944
|
attr = const.SERIES_MODULE
|
|
854
|
-
ds =
|
|
855
|
-
vals =
|
|
945
|
+
ds = pydicom.dcmread(files[0])
|
|
946
|
+
vals = get_values(ds, attr)
|
|
856
947
|
except register.AmbiguousError as e:
|
|
857
948
|
raise register.AmbiguousError(e)
|
|
858
949
|
except:
|
|
@@ -864,7 +955,7 @@ class DataBaseDicom():
|
|
|
864
955
|
else:
|
|
865
956
|
series_number = 1 + self._max_series_number(study_uid)
|
|
866
957
|
attr = ['SeriesInstanceUID', 'SeriesDescription', 'SeriesNumber']
|
|
867
|
-
series_uid =
|
|
958
|
+
series_uid = pydicom.uid.generate_uid()
|
|
868
959
|
series_desc = series[-1] if isinstance(series[-1], str) else series[-1][0]
|
|
869
960
|
vals = [series_uid, series_desc, int(series_number)]
|
|
870
961
|
return study_attr | {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
|
|
@@ -872,9 +963,9 @@ class DataBaseDicom():
|
|
|
872
963
|
|
|
873
964
|
def _write_dataset(self, ds:Dataset, attr:dict, instance_nr:int):
|
|
874
965
|
# Set new attributes
|
|
875
|
-
attr['SOPInstanceUID'] =
|
|
966
|
+
attr['SOPInstanceUID'] = pydicom.uid.generate_uid()
|
|
876
967
|
attr['InstanceNumber'] = str(instance_nr)
|
|
877
|
-
|
|
968
|
+
set_values(ds, list(attr.keys()), list(attr.values()))
|
|
878
969
|
# Save results in a new file
|
|
879
970
|
rel_dir = os.path.join(
|
|
880
971
|
f"Patient__{attr['PatientID']}",
|
|
@@ -882,7 +973,7 @@ class DataBaseDicom():
|
|
|
882
973
|
f"Series__{attr['SeriesNumber']}__{attr['SeriesDescription']}",
|
|
883
974
|
)
|
|
884
975
|
os.makedirs(os.path.join(self.path, rel_dir), exist_ok=True)
|
|
885
|
-
rel_path = os.path.join(rel_dir,
|
|
976
|
+
rel_path = os.path.join(rel_dir, pydicom.uid.generate_uid() + '.dcm')
|
|
886
977
|
dbdataset.write(ds, os.path.join(self.path, rel_path))
|
|
887
978
|
# Add an entry in the register
|
|
888
979
|
register.add_instance(self.register, attr, rel_path)
|
|
@@ -899,11 +990,13 @@ class DataBaseDicom():
|
|
|
899
990
|
)
|
|
900
991
|
os.makedirs(zip_dir, exist_ok=True)
|
|
901
992
|
for sr in st['series']:
|
|
993
|
+
zip_file = os.path.join(
|
|
994
|
+
zip_dir,
|
|
995
|
+
f"Series__{sr['SeriesNumber']}__{sr['SeriesDescription']}.zip",
|
|
996
|
+
)
|
|
997
|
+
if os.path.exists(zip_file):
|
|
998
|
+
continue
|
|
902
999
|
try:
|
|
903
|
-
zip_file = os.path.join(
|
|
904
|
-
zip_dir,
|
|
905
|
-
f"Series__{sr['SeriesNumber']}__{sr['SeriesDescription']}.zip",
|
|
906
|
-
)
|
|
907
1000
|
with zipfile.ZipFile(zip_file, 'w') as zipf:
|
|
908
1001
|
for rel_path in sr['instances'].values():
|
|
909
1002
|
file = os.path.join(self.path, rel_path)
|
|
@@ -916,6 +1009,28 @@ class DataBaseDicom():
|
|
|
916
1009
|
|
|
917
1010
|
|
|
918
1011
|
|
|
1012
|
+
def full_name(entity):
|
|
1013
|
+
|
|
1014
|
+
if len(entity)==3: # study
|
|
1015
|
+
if isinstance(entity[-1], tuple):
|
|
1016
|
+
return entity
|
|
1017
|
+
else:
|
|
1018
|
+
full_name_study = deepcopy(entity)
|
|
1019
|
+
full_name_study[-1] = (full_name_study[-1], 0)
|
|
1020
|
+
return full_name_study
|
|
1021
|
+
|
|
1022
|
+
elif len(entity)==4: # series
|
|
1023
|
+
full_name_study = full_name(entity[:3])
|
|
1024
|
+
series = full_name_study + [entity[-1]]
|
|
1025
|
+
if isinstance(series[-1], tuple):
|
|
1026
|
+
return series
|
|
1027
|
+
else:
|
|
1028
|
+
full_name_series = deepcopy(series)
|
|
1029
|
+
full_name_series[-1] = (full_name_series[-1], 0)
|
|
1030
|
+
return full_name_series
|
|
1031
|
+
else:
|
|
1032
|
+
return entity
|
|
1033
|
+
|
|
919
1034
|
|
|
920
1035
|
def clean_folder_name(name, replacement="", max_length=255):
|
|
921
1036
|
# Strip leading/trailing whitespace
|
|
@@ -940,6 +1055,30 @@ def clean_folder_name(name, replacement="", max_length=255):
|
|
|
940
1055
|
|
|
941
1056
|
|
|
942
1057
|
|
|
1058
|
+
def remove_empty_folders(path):
|
|
1059
|
+
"""
|
|
1060
|
+
Removes all empty subfolders from a given directory.
|
|
1061
|
+
|
|
1062
|
+
This function walks through the directory tree from the bottom up.
|
|
1063
|
+
This is crucial because it allows child directories to be removed before
|
|
1064
|
+
their parents, potentially making the parent directory empty and
|
|
1065
|
+
eligible for removal in the same pass.
|
|
1066
|
+
|
|
1067
|
+
Args:
|
|
1068
|
+
path (str): The absolute or relative path to the directory to scan.
|
|
1069
|
+
"""
|
|
1070
|
+
# Walk the directory tree in a bottom-up manner (topdown=False)
|
|
1071
|
+
for dirpath, dirnames, filenames in os.walk(path, topdown=False):
|
|
1072
|
+
# A directory is considered empty if it has no subdirectories and no files
|
|
1073
|
+
if not dirnames and not filenames:
|
|
1074
|
+
try:
|
|
1075
|
+
shutil.rmtree(dirpath)
|
|
1076
|
+
except OSError as e:
|
|
1077
|
+
# This might happen due to permissions issues
|
|
1078
|
+
print(f"Error removing {dirpath}: {e}")
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
|
|
943
1082
|
def infer_slice_spacing(vols):
|
|
944
1083
|
# In case spacing between slices is not (correctly) encoded in
|
|
945
1084
|
# DICOM it can be inferred from the slice locations.
|
|
@@ -996,3 +1135,4 @@ def infer_slice_spacing(vols):
|
|
|
996
1135
|
|
|
997
1136
|
|
|
998
1137
|
|
|
1138
|
+
|