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/dbd.py CHANGED
@@ -1,20 +1,27 @@
1
1
  import os
2
- from datetime import datetime
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
- # and drop then from the register
81
- self.register = register.drop(self.register, removed)
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) -> Union[vreg.Volume3D, list]:
205
- """Read volume or volumes.
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 entity to read
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 | list: If the entity is a series this returns
213
- a volume, else a list of volumes.
222
+ vreg.Volume3D:
214
223
  """
215
- if isinstance(entity, str): # path to folder
216
- return [self.volume(s, dims) for s in self.series(entity)]
217
- if len(entity) < 4: # folder, patient or study
218
- return [self.volume(s, dims) for s in self.series(entity)]
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
- for f in tqdm(files, desc='Reading volume..'):
233
- ds = dbdataset.read_dataset(f)
234
- values.append(dbdataset.get_values(ds, dims))
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.stack(values, axis=-1)
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
- c0 = coords[1:,0,...]
245
- for k in range(coords.shape[1]-1):
246
- if not np.array_equal(coords[1:,k+1,...], c0):
247
- raise ValueError(
248
- "Cannot build a single volume. Not all slices "
249
- "have the same coordinates."
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 = dbdataset.read_dataset(files[0])
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
- for sl in vt.split():
387
+ slices = vt.split()
388
+ for sl in slices:
311
389
  dbdataset.set_volume(ds, sl)
312
- dbdataset.set_value(ds, sl.dims, sl.coords[:,...])
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 = dbdataset.read_dataset(f)
394
- coords_array.append(dbdataset.get_values(ds, dims))
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': dbdataset.get_values(ds, params)}
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
- return self._copy_series(from_entity, to_entity)
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
- return self._copy_study(from_entity, to_entity)
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
- return self._copy_patient(from_entity, to_entity)
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 = dbdataset.read_dataset(f)
674
- v = dbdataset.get_values(ds, attr)
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 = dbdataset.read_dataset(f)
705
- v[i,:] = dbdataset.get_values(ds, attributes)
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 = dbdataset.read_dataset(f)
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 = dbdataset.read_dataset(files[0])
811
- vals = dbdataset.get_values(ds, attr)
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 = dbdataset.read_dataset(files[0])
831
- vals = dbdataset.get_values(ds, attr)
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[-1])
930
+ study_id = 1 + self._max_study_id(study[1])
840
931
  attr = ['StudyInstanceUID', 'StudyDescription', 'StudyID']
841
- study_uid = dbdataset.new_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 = dbdataset.read_dataset(files[0])
855
- vals = dbdataset.get_values(ds, attr)
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 = dbdataset.new_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'] = dbdataset.new_uid()
966
+ attr['SOPInstanceUID'] = pydicom.uid.generate_uid()
876
967
  attr['InstanceNumber'] = str(instance_nr)
877
- dbdataset.set_values(ds, list(attr.keys()), list(attr.values()))
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, dbdataset.new_uid() + '.dcm')
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
+