dbdicom 0.3.1__py3-none-any.whl → 0.3.3__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,5 +1,7 @@
1
1
  import os
2
2
  from datetime import datetime
3
+ import json
4
+ from typing import Union
3
5
 
4
6
  from tqdm import tqdm
5
7
  import numpy as np
@@ -8,9 +10,9 @@ import vreg
8
10
  from pydicom.dataset import Dataset
9
11
 
10
12
  import dbdicom.utils.arrays
11
- import dbdicom.utils.files as filetools
12
- import dbdicom.utils.dcm4che as dcm4che
13
+
13
14
  import dbdicom.dataset as dbdataset
15
+ import dbdicom.database as dbdatabase
14
16
  import dbdicom.register as register
15
17
  import dbdicom.const as const
16
18
 
@@ -32,7 +34,8 @@ class DataBaseDicom():
32
34
  file = self._register_file()
33
35
  if os.path.exists(file):
34
36
  try:
35
- self.register = pd.read_pickle(file)
37
+ with open(file, 'r') as f:
38
+ self.register = json.load(f)
36
39
  except:
37
40
  # If the file is corrupted, delete it and load again
38
41
  os.remove(file)
@@ -44,75 +47,45 @@ class DataBaseDicom():
44
47
  def read(self):
45
48
  """Read the DICOM folder again
46
49
  """
47
-
48
- files = filetools.all_files(self.path)
49
- self.register = dbdataset.read_dataframe(
50
- files,
51
- register.COLUMNS + ['NumberOfFrames','SOPClassUID'],
52
- path=self.path,
53
- images_only = True)
54
- self.register['removed'] = False
55
- self.register['created'] = False
56
- # No support for multiframe data at the moment
57
- self._multiframe_to_singleframe()
50
+ self.register = dbdatabase.read(self.path)
58
51
  # For now ensure all series have just a single CIOD
59
- self._split_series()
52
+ # Leaving this out for now until the issue occurs again
53
+ # self._split_series()
60
54
  return self
61
-
62
55
 
63
- def close(self):
64
- """Close the DICOM folder
65
-
66
- This also saves changes in the header file to disk.
67
- """
56
+
68
57
 
69
- created = self.register.created & (self.register.removed==False)
70
- removed = self.register.removed
71
- created = created[created].index
72
- removed = removed[removed].index
58
+ def delete(self, entity):
59
+ """Delete a DICOM entity from the database
73
60
 
61
+ Args:
62
+ entity (list): entity to delete
63
+ """
64
+ removed = register.index(self.register, entity)
74
65
  # delete datasets marked for removal
75
66
  for index in removed.tolist():
76
67
  file = os.path.join(self.path, index)
77
68
  if os.path.exists(file):
78
69
  os.remove(file)
79
70
  # and drop then from the register
80
- self.register.drop(index=removed, inplace=True)
81
-
82
- # for new or edited data, mark as saved.
83
- self.register.loc[created, 'created'] = False
84
-
85
- # save register
86
- file = self._register_file()
87
- self.register.to_pickle(file)
71
+ self.register = register.drop(removed)
88
72
  return self
89
73
 
90
74
 
91
- def restore(self):
92
- """Restore the DICOM folder to the last saved state."""
93
-
94
- created = self.register.created
95
- removed = self.register.removed & (self.register.created==False)
96
- created = created[created].index
97
- removed = removed[removed].index
98
-
99
- # permanently delete newly created datasets
100
- for index in created.tolist():
101
- file = os.path.join(self.path, index)
102
- if os.path.exists(file):
103
- os.remove(file)
104
-
105
- # and drop then from the register
106
- self.register.drop(index=created, inplace=True)
107
-
108
- # Restore those that were marked for removal
109
- self.register.loc[removed, 'removed'] = False
110
-
111
- # save register
75
+ def close(self):
76
+ """Close the DICOM folder
77
+
78
+ This also saves changes in the header file to disk.
79
+ """
80
+ # Save df as pkl
112
81
  file = self._register_file()
113
- self.register.to_pickle(file)
114
- return self
82
+ with open(file, 'w') as f:
83
+ json.dump(self.register, f, indent=4)
84
+ return self
115
85
 
86
+ def _register_file(self):
87
+ return os.path.join(self.path, 'dbtree.json')
88
+
116
89
 
117
90
  def summary(self):
118
91
  """Return a summary of the contents of the database.
@@ -122,6 +95,7 @@ class DataBaseDicom():
122
95
  """
123
96
  return register.summary(self.register)
124
97
 
98
+
125
99
  def print(self):
126
100
  """Print the contents of the DICOM folder
127
101
  """
@@ -170,6 +144,11 @@ class DataBaseDicom():
170
144
  for patient in self.patients():
171
145
  studies += self.studies(patient, name, contains, isin)
172
146
  return studies
147
+ elif len(entity)==1:
148
+ studies = []
149
+ for patient in self.patients():
150
+ studies += self.studies(patient, name, contains, isin)
151
+ return studies
173
152
  else:
174
153
  return register.studies(self.register, entity, name, contains, isin)
175
154
 
@@ -199,6 +178,11 @@ class DataBaseDicom():
199
178
  for study in self.studies(entity):
200
179
  series += self.series(study, name, contains, isin)
201
180
  return series
181
+ elif len(entity)==1:
182
+ series = []
183
+ for study in self.studies(entity):
184
+ series += self.series(study, name, contains, isin)
185
+ return series
202
186
  elif len(entity)==2:
203
187
  series = []
204
188
  for study in self.studies(entity):
@@ -208,20 +192,20 @@ class DataBaseDicom():
208
192
  return register.series(self.register, entity, name, contains, isin)
209
193
 
210
194
 
211
- def volume(self, series:list, dims:list=None, multislice=False) -> vreg.Volume3D:
195
+ def volume(self, series:list, dims:list=None) -> vreg.Volume3D:
212
196
  """Read a vreg.Volume3D from a DICOM series
213
197
 
214
198
  Args:
215
199
  series (list): DICOM series to read
216
200
  dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
217
- multislice (bool, optional): Whether the data are to be read
218
- as multislice or not. In multislice data the voxel size
219
- is taken from the slice gap rather thsan the slice thickness. Defaults to False.
220
201
 
221
202
  Returns:
222
203
  vreg.Volume3D: vole read from the series.
223
204
  """
224
-
205
+ if isinstance(series, str): # path to folder
206
+ return [self.volume(s, dims) for s in self.series(series)]
207
+ if len(series) < 4: # folder, patient or study
208
+ return [self.volume(s, dims) for s in self.series(series)]
225
209
  if dims is None:
226
210
  dims = []
227
211
  elif isinstance(dims, str):
@@ -238,7 +222,7 @@ class DataBaseDicom():
238
222
  for f in tqdm(files, desc='Reading volume..'):
239
223
  ds = dbdataset.read_dataset(f)
240
224
  values.append(dbdataset.get_values(ds, dims))
241
- volumes.append(dbdataset.volume(ds, multislice))
225
+ volumes.append(dbdataset.volume(ds))
242
226
 
243
227
  # Format as mesh
244
228
  coords = np.stack(values, axis=-1)
@@ -256,9 +240,21 @@ class DataBaseDicom():
256
240
  "firstslice=True, the coordinates of the lowest "
257
241
  "slice will be assigned to the whole volume."
258
242
  )
243
+
244
+ # Infer spacing between slices from slice locations
245
+ # Technically only necessary if SpacingBetweenSlices not set or incorrect
246
+ vols = infer_slice_spacing(vols)
259
247
 
260
248
  # Join 2D volumes into 3D volumes
261
- vol = vreg.join(vols)
249
+ try:
250
+ vol = vreg.join(vols)
251
+ except ValueError:
252
+ # some vendors define the slice vector as -cross product
253
+ # of row and column vector. Check if that solves the issue.
254
+ for v in vols.reshape(-1):
255
+ v.affine[:3,2] = -v.affine[:3,2]
256
+ # Then try again
257
+ vol = vreg.join(vols)
262
258
  if vol.ndim > 3:
263
259
  vol.set_coords(c0)
264
260
  vol.set_dims(dims[1:])
@@ -266,21 +262,21 @@ class DataBaseDicom():
266
262
 
267
263
 
268
264
  def write_volume(
269
- self, vol:vreg.Volume3D, series:list,
270
- ref:list=None, multislice=False,
265
+ self, vol:Union[vreg.Volume3D, tuple], series:list,
266
+ ref:list=None,
271
267
  ):
272
268
  """Write a vreg.Volume3D to a DICOM series
273
269
 
274
270
  Args:
275
271
  vol (vreg.Volume3D): Volume to write to the series.
276
272
  series (list): DICOM series to read
277
- dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
278
- multislice (bool, optional): Whether the data are to be read
279
- as multislice or not. In multislice data the voxel size
280
- is taken from the slice gap rather thsan the slice thickness. Defaults to False.
273
+ ref (list): Reference series
281
274
  """
275
+ if isinstance(vol, tuple):
276
+ vol = vreg.volume(vol[0], vol[1])
282
277
  if ref is None:
283
278
  ds = dbdataset.new_dataset('MRImage')
279
+ #ds = dbdataset.new_dataset('ParametricMap')
284
280
  else:
285
281
  if ref[0] == series[0]:
286
282
  ref_mgr = self
@@ -293,26 +289,24 @@ class DataBaseDicom():
293
289
  attr = self._attributes(series)
294
290
  n = self._max_instance_number(attr['SeriesInstanceUID'])
295
291
 
296
- new_instances = {}
297
292
  if vol.ndim==3:
298
293
  slices = vol.split()
299
294
  for i, sl in tqdm(enumerate(slices), desc='Writing volume..'):
300
- dbdataset.set_volume(ds, sl, multislice)
301
- self._write_dataset(ds, attr, n + 1 + i, new_instances)
295
+ dbdataset.set_volume(ds, sl)
296
+ self._write_dataset(ds, attr, n + 1 + i)
302
297
  else:
303
298
  i=0
304
299
  vols = vol.separate().reshape(-1)
305
300
  for vt in tqdm(vols, desc='Writing volume..'):
306
301
  for sl in vt.split():
307
- dbdataset.set_volume(ds, sl, multislice)
302
+ dbdataset.set_volume(ds, sl)
308
303
  dbdataset.set_value(ds, sl.dims, sl.coords[:,...])
309
- self._write_dataset(ds, attr, n + 1 + i, new_instances)
304
+ self._write_dataset(ds, attr, n + 1 + i)
310
305
  i+=1
311
- return self
312
- self._update_register(new_instances)
306
+ return self
313
307
 
314
308
 
315
- def to_nifti(self, series:list, file:str, dims=None, multislice=False):
309
+ def to_nifti(self, series:list, file:str, dims=None):
316
310
  """Save a DICOM series in nifti format.
317
311
 
318
312
  Args:
@@ -320,34 +314,31 @@ class DataBaseDicom():
320
314
  file (str): file path of the nifti file.
321
315
  dims (list, optional): Non-spatial dimensions of the volume.
322
316
  Defaults to None.
323
- multislice (bool, optional): Whether the data are to be read
324
- as multislice or not. In multislice data the voxel size
325
- is taken from the slice gap rather thaan the slice thickness. Defaults to False.
326
317
  """
327
- vol = self.volume(series, dims, multislice)
318
+ vol = self.volume(series, dims)
328
319
  vreg.write_nifti(vol, file)
329
320
  return self
330
321
 
331
- def from_nifti(self, file:str, series:list, ref:list=None, multislice=False):
322
+ def from_nifti(self, file:str, series:list, ref:list=None):
332
323
  """Create a DICOM series from a nifti file.
333
324
 
334
325
  Args:
335
326
  file (str): file path of the nifti file.
336
327
  series (list): DICOM series to create
337
328
  ref (list): DICOM series to use as template.
338
- multislice (bool, optional): Whether the data are to be written
339
- as multislice or not. In multislice data the voxel size
340
- is written in the slice gap rather thaan the slice thickness. Defaults to False.
341
329
  """
342
330
  vol = vreg.read_nifti(file)
343
- self.write_volume(vol, series, ref, multislice)
331
+ self.write_volume(vol, series, ref)
344
332
  return self
345
333
 
346
334
  def pixel_data(self, series:list, dims:list=None, coords=False, include=None) -> np.ndarray:
347
335
  """Read the pixel data from a DICOM series
348
336
 
349
337
  Args:
350
- series (list): DICOM series to read
338
+ series (list or str): DICOM series to read. This can also
339
+ be a path to a folder containing DICOM files, or a
340
+ patient or study to read all series in that patient or
341
+ study. In those cases a list is returned.
351
342
  dims (list, optional): Dimensions of the array.
352
343
  coords (bool): If set to Trye, the coordinates of the
353
344
  arrays are returned alongside the pixel data
@@ -360,8 +351,12 @@ class DataBaseDicom():
360
351
  coords is set these are returned too as an array with
361
352
  coordinates of the slices according to dims. If include
362
353
  is provide the values are returned as a dictionary in the last
363
- return value.
354
+ return value.
364
355
  """
356
+ if isinstance(series, str): # path to folder
357
+ return [self.pixel_data(s, dims, coords, include) for s in self.series(series)]
358
+ if len(series) < 4: # folder, patient or study
359
+ return [self.pixel_data(s, dims, coords, include) for s in self.series(series)]
365
360
  if coords:
366
361
  if dims is None:
367
362
  raise ValueError(
@@ -488,16 +483,6 @@ class DataBaseDicom():
488
483
  f"Cannot copy {from_entity} to {to_entity}. "
489
484
  )
490
485
 
491
- def delete(self, entity):
492
- """Delete a DICOM entity from the database
493
-
494
- Args:
495
- entity (list): entity to delete
496
- """
497
- index = register.index(self.register, entity)
498
- self.register.loc[index,'removed'] = True
499
- return self
500
-
501
486
  def move(self, from_entity, to_entity):
502
487
  """Move a DICOM entity
503
488
 
@@ -510,15 +495,15 @@ class DataBaseDicom():
510
495
 
511
496
  def _values(self, attributes:list, entity:list):
512
497
  # Create a np array v with values for each instance and attribute
513
- if set(attributes) <= set(self.register.columns):
514
- index = register.index(self.register, entity)
515
- v = self.register.loc[index, attributes].values
516
- else:
517
- files = register.files(self.register, entity)
518
- v = np.empty((len(files), len(attributes)), dtype=object)
519
- for i, f in enumerate(files):
520
- ds = dbdataset.read_dataset(f)
521
- v[i,:] = dbdataset.get_values(ds, attributes)
498
+ # if set(attributes) <= set(dbdatabase.COLUMNS):
499
+ # index = register.index(self.register, entity)
500
+ # v = self.register.loc[index, attributes].values
501
+ # else:
502
+ files = register.files(self.register, entity)
503
+ v = np.empty((len(files), len(attributes)), dtype=object)
504
+ for i, f in enumerate(files):
505
+ ds = dbdataset.read_dataset(f)
506
+ v[i,:] = dbdataset.get_values(ds, attributes)
522
507
  return v
523
508
 
524
509
  def _copy_patient(self, from_patient, to_patient):
@@ -562,30 +547,34 @@ class DataBaseDicom():
562
547
  n = self._max_instance_number(attr['SeriesInstanceUID'])
563
548
 
564
549
  # Copy the files to the new series
565
- new_instances = {}
566
550
  for i, f in tqdm(enumerate(files), total=len(files), desc=f'Copying series {to_series[1:]}'):
567
551
  # Read dataset and assign new properties
568
552
  ds = dbdataset.read_dataset(f)
569
- self._write_dataset(ds, attr, n + 1 + i, new_instances)
570
- self._update_register(new_instances)
571
-
572
-
553
+ self._write_dataset(ds, attr, n + 1 + i)
554
+
555
+ def _max_study_id(self, patient_id):
556
+ for pt in self.register:
557
+ if pt['PatientID'] == patient_id:
558
+ n = [int(st['StudyID']) for st in pt['studies']]
559
+ return int(np.amax(n))
560
+ return 0
561
+
573
562
  def _max_series_number(self, study_uid):
574
- df = self.register
575
- df = df[(df.StudyInstanceUID==study_uid) & (df.removed==False)]
576
- n = df['SeriesNumber'].values
577
- n = n[n != -1]
578
- max_number=0 if n.size==0 else np.amax(n)
579
- return max_number
563
+ for pt in self.register:
564
+ for st in pt['studies']:
565
+ if st['StudyInstanceUID'] == study_uid:
566
+ n = [sr['SeriesNumber'] for sr in st['series']]
567
+ return int(np.amax(n))
568
+ return 0
580
569
 
581
570
  def _max_instance_number(self, series_uid):
582
- df = self.register
583
- df = df[(df.SeriesInstanceUID==series_uid) & (df.removed==False)]
584
- n = df['InstanceNumber'].values
585
- n = n[n != -1]
586
- max_number=0 if n.size==0 else np.amax(n)
587
- return max_number
588
-
571
+ for pt in self.register:
572
+ for st in pt['studies']:
573
+ for sr in st['series']:
574
+ if sr['SeriesInstanceUID'] == series_uid:
575
+ n = list(sr['instances'].keys())
576
+ return int(np.amax([int(i) for i in n]))
577
+ return 0
589
578
 
590
579
  def _attributes(self, entity):
591
580
  if len(entity)==4:
@@ -606,7 +595,8 @@ class DataBaseDicom():
606
595
  except:
607
596
  # If the patient does not exist, generate values
608
597
  attr = ['PatientID', 'PatientName']
609
- patient_id = dbdataset.new_uid()
598
+ #patient_id = dbdataset.new_uid()
599
+ patient_id = patient[-1] if isinstance(patient[-1], str) else f"{patient[-1][0]}_{patient[-1][1]}"
610
600
  patient_name = patient[-1] if isinstance(patient[-1], str) else patient[-1][0]
611
601
  vals = [patient_id, patient_name]
612
602
  return {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
@@ -621,12 +611,18 @@ class DataBaseDicom():
621
611
  ds = dbdataset.read_dataset(files[0])
622
612
  vals = dbdataset.get_values(ds, attr)
623
613
  except:
624
- # If the study does not exist, generate values
625
- attr = ['StudyInstanceUID', 'StudyDescription', 'StudyDate']
626
- study_id = dbdataset.new_uid()
614
+ # If the study does not exist or is empty, generate values
615
+ try:
616
+ patient_id = register.uid(self.register, study[:-1])
617
+ except:
618
+ study_id = 1
619
+ else:
620
+ study_id = 1 + self._max_study_id(patient_id)
621
+ attr = ['StudyInstanceUID', 'StudyDescription', 'StudyDate', 'StudyID']
622
+ study_uid = dbdataset.new_uid()
627
623
  study_desc = study[-1] if isinstance(study[-1], str) else study[-1][0]
628
624
  study_date = datetime.today().strftime('%Y%m%d')
629
- vals = [study_id, study_desc, study_date]
625
+ vals = [study_uid, study_desc, study_date, str(study_id)]
630
626
  return patient_attr | {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
631
627
 
632
628
 
@@ -647,90 +643,114 @@ class DataBaseDicom():
647
643
  else:
648
644
  series_number = 1 + self._max_series_number(study_uid)
649
645
  attr = ['SeriesInstanceUID', 'SeriesDescription', 'SeriesNumber']
650
- series_id = dbdataset.new_uid()
646
+ series_uid = dbdataset.new_uid()
651
647
  series_desc = series[-1] if isinstance(series[-1], str) else series[-1][0]
652
- vals = [series_id, series_desc, series_number]
648
+ vals = [series_uid, series_desc, int(series_number)]
653
649
  return study_attr | {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
654
650
 
655
651
 
656
- def _write_dataset(self, ds:Dataset, attr:dict, instance_nr:int, register:dict):
652
+ def _write_dataset(self, ds:Dataset, attr:dict, instance_nr:int):
657
653
  # Set new attributes
658
654
  attr['SOPInstanceUID'] = dbdataset.new_uid()
659
- attr['InstanceNumber'] = instance_nr
655
+ attr['InstanceNumber'] = str(instance_nr)
660
656
  dbdataset.set_values(ds, list(attr.keys()), list(attr.values()))
661
657
  # Save results in a new file
662
- rel_path = os.path.join('dbdicom', dbdataset.new_uid() + '.dcm')
658
+ rel_dir = os.path.join(
659
+ f"patient_{attr['PatientID']}",
660
+ f"study_[{attr['StudyID']}]_{attr['StudyDescription']}",
661
+ f"series_[{attr['SeriesNumber']}]_{attr['SeriesDescription']}",
662
+ )
663
+ os.makedirs(os.path.join(self.path, rel_dir), exist_ok=True)
664
+ rel_path = os.path.join(rel_dir, dbdataset.new_uid() + '.dcm')
663
665
  dbdataset.write(ds, os.path.join(self.path, rel_path))
664
- # Add a row to the register
665
- register[rel_path] = dbdataset.get_values(ds, self.register.columns)
666
-
666
+ # Add an entry in the register
667
+ register.add_instance(self.register, attr, rel_path)
668
+
669
+
670
+
671
+ # def _split_series(self):
672
+ # """
673
+ # Split series with multiple SOP Classes.
674
+
675
+ # If a series contain instances from different SOP Classes,
676
+ # these are separated out into multiple series with identical SOP Classes.
677
+ # """
678
+ # # For each series, check if there are multiple
679
+ # # SOP Classes in the series and split them if yes.
680
+ # for series in self.series():
681
+ # series_index = register.index(self.register, series)
682
+ # df_series = self.register.loc[series_index]
683
+ # sop_classes = df_series.SOPClassUID.unique()
684
+ # if len(sop_classes) > 1:
685
+ # # For each sop_class, create a new series and move all
686
+ # # instances of that sop_class to the new series
687
+ # desc = series[-1] if isinstance(series, str) else series[0]
688
+ # for i, sop_class in tqdm(enumerate(sop_classes[1:]), desc='Splitting series with multiple SOP Classes.'):
689
+ # df_sop_class = df_series[df_series.SOPClassUID == sop_class]
690
+ # relpaths = df_sop_class.index.tolist()
691
+ # sop_class_files = [os.path.join(self.path, p) for p in relpaths]
692
+ # sop_class_series = series[:-1] + [desc + f' [{i+1}]']
693
+ # self._files_to_series(sop_class_files, sop_class_series)
694
+ # # Delete original files permanently
695
+ # self.register.drop(relpaths)
696
+ # for f in sop_class_files:
697
+ # os.remove(f)
698
+ # self.register.drop('SOPClassUID', axis=1, inplace=True)
699
+
700
+
701
+ def infer_slice_spacing(vols):
702
+ # In case spacing between slices is not (correctly) encoded in
703
+ # DICOM it can be inferred from the slice locations.
704
+
705
+ shape = vols.shape
706
+ vols = vols.reshape((shape[0], -1))
707
+ slice_spacing = np.zeros(vols.shape[-1])
708
+
709
+ for d in range(vols.shape[-1]):
710
+
711
+ # For single slice volumes there is nothing to do
712
+ if vols[:,d].shape[0]==1:
713
+ continue
714
+
715
+ # Get a normal slice vector from the first volume.
716
+ mat = vols[0,d].affine[:3,:3]
717
+ normal = mat[:,2]/np.linalg.norm(mat[:,2])
718
+
719
+ # Get slice locations by projection on the normal.
720
+ pos = [v.affine[:3,3] for v in vols[:,d]]
721
+ slice_loc = [np.dot(p, normal) for p in pos]
722
+
723
+ # Sort slice locations and take consecutive differences.
724
+ slice_loc = np.sort(slice_loc)
725
+ distances = slice_loc[1:] - slice_loc[:-1]
726
+
727
+ # Round to micrometer and check if unique
728
+ distances = np.around(distances, 3)
729
+ slice_spacing_d = np.unique(distances)
730
+
731
+ # Check if unique - otherwise this is not a volume
732
+ if len(slice_spacing_d) > 1:
733
+ raise ValueError(
734
+ 'Cannot build a volume - spacings between slices are not unique.'
735
+ )
736
+ else:
737
+ slice_spacing_d= slice_spacing_d[0]
738
+
739
+ # Set correct slice spacing in all volumes
740
+ for v in vols[:,d]:
741
+ v.affine[:3,2] = normal * abs(slice_spacing_d)
667
742
 
668
- def _update_register(self, new_instances:dict):
669
- # A new instances to the register
670
- df = pd.DataFrame.from_dict(new_instances, orient='index', columns=self.register.columns)
671
- df['removed'] = False
672
- df['created'] = True
673
- self.register = pd.concat([self.register, df])
743
+ slice_spacing[d] = slice_spacing_d
674
744
 
745
+ # Check slice_spacing is the same across dimensions
746
+ slice_spacing = np.unique(slice_spacing)
747
+ if len(slice_spacing) > 1:
748
+ raise ValueError(
749
+ 'Cannot build a volume - spacings between slices are not unique.'
750
+ )
675
751
 
676
- def _register_file(self):
677
- filename = os.path.basename(os.path.normpath(self.path)) + ".pkl"
678
- return os.path.join(self.path, filename)
679
-
752
+ return vols.reshape(shape)
680
753
 
681
- def _multiframe_to_singleframe(self):
682
- """Converts all multiframe files in the folder into single-frame files.
683
-
684
- Reads all the multi-frame files in the folder,
685
- converts them to singleframe files, and delete the original multiframe file.
686
- """
687
- singleframe = self.register.NumberOfFrames.isnull()
688
- multiframe = singleframe == False
689
- nr_multiframe = multiframe.sum()
690
- if nr_multiframe != 0:
691
- for relpath in tqdm(self.register[multiframe].index.values, desc="Converting multiframe file " + relpath):
692
- filepath = os.path.join(self.path, relpath)
693
- singleframe_files = dcm4che.split_multiframe(filepath)
694
- if singleframe_files != []:
695
- # add the single frame files to the dataframe
696
- df = dbdataset.read_dataframe(singleframe_files, self.register.columns, path=self.path)
697
- df['removed'] = False
698
- df['created'] = False
699
- self.register = pd.concat([self.register, df])
700
- # delete the original multiframe
701
- os.remove(filepath)
702
- # drop the file also if the conversion has failed
703
- self.register.drop(index=relpath, inplace=True)
704
- self.register.drop('NumberOfFrames', axis=1, inplace=True)
705
-
706
-
707
- def _split_series(self):
708
- """
709
- Split series with multiple SOP Classes.
710
754
 
711
- If a series contain instances from different SOP Classes,
712
- these are separated out into multiple series with identical SOP Classes.
713
- """
714
- # For each series, check if there are multiple
715
- # SOP Classes in the series and split them if yes.
716
- for series in self.series():
717
- series_index = register.index(self.register, series)
718
- df_series = self.register.loc[series_index]
719
- sop_classes = df_series.SOPClassUID.unique()
720
- if len(sop_classes) > 1:
721
- # For each sop_class, create a new series and move all
722
- # instances of that sop_class to the new series
723
- desc = series[-1] if isinstance(series, str) else series[0]
724
- for i, sop_class in tqdm(enumerate(sop_classes[1:]), desc='Splitting series with multiple SOP Classes.'):
725
- df_sop_class = df_series[df_series.SOPClassUID == sop_class]
726
- relpaths = df_sop_class.index.tolist()
727
- sop_class_files = [os.path.join(self.path, p) for p in relpaths]
728
- sop_class_series = series[:-1] + [desc + f' [{i+1}]']
729
- self._files_to_series(sop_class_files, sop_class_series)
730
- # Delete original files permanently
731
- self.register.drop(relpaths)
732
- for f in sop_class_files:
733
- os.remove(f)
734
- self.register.drop('SOPClassUID', axis=1, inplace=True)
735
755
 
736
756