dbdicom 0.3.1__py3-none-any.whl → 0.3.2__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 CHANGED
@@ -1,5 +1,4 @@
1
1
 
2
- import numpy as np
3
2
  import vreg
4
3
 
5
4
  from dbdicom.dbd import DataBaseDicom
@@ -16,6 +15,15 @@ def open(path:str) -> DataBaseDicom:
16
15
  """
17
16
  return DataBaseDicom(path)
18
17
 
18
+ def to_json(path):
19
+ """Summarise the contents of the DICOM folder in a json file
20
+
21
+ Args:
22
+ path (str): path to the DICOM folder
23
+ """
24
+ dbd = open(path)
25
+ dbd.close()
26
+
19
27
  def print(path):
20
28
  """Print the contents of the DICOM folder
21
29
 
@@ -42,6 +50,21 @@ def summary(path) -> dict:
42
50
  return s
43
51
 
44
52
 
53
+ def tree(path) -> dict:
54
+ """Return the structure of the database as a dictionary tree.
55
+
56
+ Args:
57
+ path (str): path to the DICOM folder
58
+
59
+ Returns:
60
+ dict: Nested dictionary with summary information on the database.
61
+ """
62
+ dbd = open(path)
63
+ s = dbd.register
64
+ dbd.close()
65
+ return s
66
+
67
+
45
68
  def patients(path, name:str=None, contains:str=None, isin:list=None)->list:
46
69
  """Return a list of patients in the DICOM folder.
47
70
 
dbdicom/database.py ADDED
@@ -0,0 +1,122 @@
1
+ import os
2
+ from tqdm import tqdm
3
+
4
+ import numpy as np
5
+ import pandas as pd
6
+ import pydicom
7
+
8
+ import dbdicom.utils.dcm4che as dcm4che
9
+ import dbdicom.utils.files as filetools
10
+ import dbdicom.dataset as dbdataset
11
+
12
+
13
+ COLUMNS = [
14
+ # Identifiers (unique)
15
+ 'PatientID',
16
+ 'StudyInstanceUID',
17
+ 'SeriesInstanceUID',
18
+ 'SOPInstanceUID',
19
+ # Human-readable identifiers (not unique)
20
+ 'PatientName',
21
+ 'StudyDescription',
22
+ 'StudyDate',
23
+ 'SeriesDescription',
24
+ 'SeriesNumber',
25
+ 'InstanceNumber',
26
+ ]
27
+
28
+ def read(path):
29
+ files = filetools.all_files(path)
30
+ tags = COLUMNS + ['NumberOfFrames'] # + ['SOPClassUID']
31
+ array = []
32
+ dicom_files = []
33
+ for i, file in tqdm(enumerate(files), total=len(files), desc='Reading DICOM folder'):
34
+ try:
35
+ ds = pydicom.dcmread(file, force=True, specific_tags=tags+['Rows'])
36
+ except:
37
+ pass
38
+ else:
39
+ if isinstance(ds, pydicom.dataset.FileDataset):
40
+ if 'TransferSyntaxUID' in ds.file_meta:
41
+ if not 'Rows' in ds: # Image only
42
+ continue
43
+ row = dbdataset.get_values(ds, tags)
44
+ array.append(row)
45
+ index = os.path.relpath(file, path)
46
+ dicom_files.append(index)
47
+ df = pd.DataFrame(array, index = dicom_files, columns = tags)
48
+ df = _multiframe_to_singleframe(path, df)
49
+ dbtree = _tree(df)
50
+ return dbtree
51
+
52
+
53
+ def _multiframe_to_singleframe(path, df):
54
+ """Converts all multiframe files in the folder into single-frame files.
55
+
56
+ Reads all the multi-frame files in the folder,
57
+ converts them to singleframe files, and delete the original multiframe file.
58
+ """
59
+ singleframe = df.NumberOfFrames.isnull()
60
+ multiframe = singleframe == False
61
+ nr_multiframe = multiframe.sum()
62
+ if nr_multiframe != 0:
63
+ for relpath in tqdm(df[multiframe].index.values, desc="Converting multiframe file " + relpath):
64
+ filepath = os.path.join(path, relpath)
65
+ singleframe_files = dcm4che.split_multiframe(filepath)
66
+ if singleframe_files != []:
67
+ # add the single frame files to the dataframe
68
+ dfnew = read(singleframe_files, df.columns, path)
69
+ df = pd.concat([df, dfnew])
70
+ # delete the original multiframe
71
+ os.remove(filepath)
72
+ # drop the file also if the conversion has failed
73
+ df.drop(index=relpath, inplace=True)
74
+ df.drop('NumberOfFrames', axis=1, inplace=True)
75
+ return df
76
+
77
+
78
+ def _tree(df):
79
+ # A human-readable summary tree
80
+
81
+ df.sort_values(['PatientID','StudyInstanceUID','SeriesNumber'], inplace=True)
82
+ df = df.fillna('None')
83
+ summary = []
84
+
85
+ for uid_patient in df.PatientID.unique():
86
+ df_patient = df[df.PatientID == uid_patient]
87
+ patient_name = df_patient.PatientName.values[0]
88
+ patient = {
89
+ 'PatientName': patient_name,
90
+ 'PatientID': uid_patient,
91
+ 'studies': [],
92
+ }
93
+ summary.append(patient)
94
+ for uid_study in df_patient.StudyInstanceUID.unique():
95
+ df_study = df_patient[df_patient.StudyInstanceUID == uid_study]
96
+ study_desc = df_study.StudyDescription.values[0]
97
+ study_date = df_study.StudyDate.values[0]
98
+ study = {
99
+ 'StudyDescription': study_desc,
100
+ 'StudyDate': study_date,
101
+ 'StudyInstanceUID': uid_study,
102
+ 'series': [],
103
+ }
104
+ patient['studies'].append(study)
105
+ for uid_sery in df_study.SeriesInstanceUID.unique():
106
+ df_series = df_study[df_study.SeriesInstanceUID == uid_sery]
107
+ series_desc = df_series.SeriesDescription.values[0]
108
+ series_nr = int(df_series.SeriesNumber.values[0])
109
+ series = {
110
+ 'SeriesNumber': series_nr,
111
+ 'SeriesDescription': series_desc,
112
+ 'SeriesInstanceUID': uid_sery,
113
+ 'instances': {},
114
+ }
115
+ study['series'].append(series)
116
+ for uid_instance in df_series.SOPInstanceUID.unique():
117
+ df_instance = df_series[df_series.SOPInstanceUID == uid_instance]
118
+ instance_nr = int(df_instance.InstanceNumber.values[0])
119
+ relpath = df_instance.index[0]
120
+ series['instances'][instance_nr]=relpath
121
+
122
+ return summary
dbdicom/dataset.py CHANGED
@@ -7,13 +7,12 @@ import struct
7
7
  from tqdm import tqdm
8
8
 
9
9
  import numpy as np
10
- import pandas as pd
11
10
  import pydicom
12
11
  from pydicom.util.codify import code_file
13
12
  import pydicom.config
14
- from pydicom.dataset import Dataset
15
13
  import vreg
16
14
 
15
+
17
16
  import dbdicom.utils.image as image
18
17
  import dbdicom.utils.variables as variables
19
18
  from dbdicom.sop_classes import (
@@ -74,6 +73,8 @@ def new_dataset(sop_class):
74
73
  return xray_angiographic_image.default()
75
74
  if sop_class == 'UltrasoundMultiFrameImage':
76
75
  return ultrasound_multiframe_image.default()
76
+ if sop_class == 'ParametricMap':
77
+ return parametric_map.default()
77
78
  else:
78
79
  raise ValueError(
79
80
  f"DICOM class {sop_class} is not currently supported"
@@ -238,7 +239,7 @@ def codify(source_file, save_file, **kwargs):
238
239
  file.close()
239
240
 
240
241
 
241
- def read_data(files, tags, path=None, images_only=False):
242
+ def read_data(files, tags, path=None, images_only=False): # obsolete??
242
243
 
243
244
  if np.isscalar(files):
244
245
  files = [files]
@@ -266,34 +267,6 @@ def read_data(files, tags, path=None, images_only=False):
266
267
 
267
268
 
268
269
 
269
- def read_dataframe(files, tags, path=None, images_only=False):
270
- if np.isscalar(files):
271
- files = [files]
272
- if np.isscalar(tags):
273
- tags = [tags]
274
- array = []
275
- dicom_files = []
276
- for i, file in tqdm(enumerate(files), desc='Reading DICOM folder'):
277
- try:
278
- ds = pydicom.dcmread(file, force=True, specific_tags=tags+['Rows'])
279
- except:
280
- pass
281
- else:
282
- if isinstance(ds, pydicom.dataset.FileDataset):
283
- if 'TransferSyntaxUID' in ds.file_meta:
284
- if images_only:
285
- if not 'Rows' in ds:
286
- continue
287
- row = get_values(ds, tags)
288
- array.append(row)
289
- if path is None:
290
- index = file
291
- else:
292
- index = os.path.relpath(file, path)
293
- dicom_files.append(index)
294
- df = pd.DataFrame(array, index = dicom_files, columns = tags)
295
- return df
296
-
297
270
 
298
271
  def _add_new(ds, tag, value, VR='OW'):
299
272
  if not isinstance(tag, pydicom.tag.BaseTag):
@@ -586,7 +559,7 @@ def pixel_data(ds):
586
559
  try:
587
560
  array = ds.pixel_array
588
561
  except:
589
- return None
562
+ raise ValueError("Dataset has no pixel data.")
590
563
  array = array.astype(np.float32)
591
564
  slope = float(getattr(ds, 'RescaleSlope', 1))
592
565
  intercept = float(getattr(ds, 'RescaleIntercept', 0))
@@ -595,7 +568,7 @@ def pixel_data(ds):
595
568
  return np.transpose(array)
596
569
 
597
570
 
598
- def set_pixel_data(ds, array, value_range=None):
571
+ def set_pixel_data(ds, array):
599
572
  if array is None:
600
573
  raise ValueError('The pixel array cannot be set to an empty value.')
601
574
 
@@ -611,7 +584,7 @@ def set_pixel_data(ds, array, value_range=None):
611
584
  # if array.ndim >= 3: # remove spurious dimensions of 1
612
585
  # array = np.squeeze(array)
613
586
 
614
- array = image.clip(array.astype(np.float32), value_range=value_range)
587
+ array = image.clip(array.astype(np.float32))
615
588
  array, slope, intercept = image.scale_to_range(array, ds.BitsAllocated)
616
589
  array = np.transpose(array)
617
590
 
@@ -635,6 +608,15 @@ def volume(ds, multislice=False):
635
608
  def set_volume(ds, volume:vreg.Volume3D, multislice=False):
636
609
  if volume is None:
637
610
  raise ValueError('The volume cannot be set to an empty value.')
611
+ try:
612
+ mod = SOPCLASSMODULE[ds.SOPClassUID]
613
+ except KeyError:
614
+ raise ValueError(
615
+ f"DICOM class {ds.SOPClassUID} is not currently supported."
616
+ )
617
+ if hasattr(mod, 'set_volume'):
618
+ return getattr(mod, 'set_volume')(ds, volume)
619
+
638
620
  image = np.squeeze(volume.values)
639
621
  if image.ndim != 2:
640
622
  raise ValueError("Can only write 2D images to a dataset.")
dbdicom/dbd.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  from datetime import datetime
3
+ import json
3
4
 
4
5
  from tqdm import tqdm
5
6
  import numpy as np
@@ -8,9 +9,9 @@ import vreg
8
9
  from pydicom.dataset import Dataset
9
10
 
10
11
  import dbdicom.utils.arrays
11
- import dbdicom.utils.files as filetools
12
- import dbdicom.utils.dcm4che as dcm4che
12
+
13
13
  import dbdicom.dataset as dbdataset
14
+ import dbdicom.database as dbdatabase
14
15
  import dbdicom.register as register
15
16
  import dbdicom.const as const
16
17
 
@@ -32,7 +33,8 @@ class DataBaseDicom():
32
33
  file = self._register_file()
33
34
  if os.path.exists(file):
34
35
  try:
35
- self.register = pd.read_pickle(file)
36
+ with open(file, 'r') as f:
37
+ self.register = json.load(f)
36
38
  except:
37
39
  # If the file is corrupted, delete it and load again
38
40
  os.remove(file)
@@ -44,75 +46,45 @@ class DataBaseDicom():
44
46
  def read(self):
45
47
  """Read the DICOM folder again
46
48
  """
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()
49
+ self.register = dbdatabase.read(self.path)
58
50
  # For now ensure all series have just a single CIOD
59
- self._split_series()
51
+ # Leaving this out for now until the issue occurs again
52
+ # self._split_series()
60
53
  return self
61
-
62
54
 
63
- def close(self):
64
- """Close the DICOM folder
65
-
66
- This also saves changes in the header file to disk.
67
- """
55
+
68
56
 
69
- created = self.register.created & (self.register.removed==False)
70
- removed = self.register.removed
71
- created = created[created].index
72
- removed = removed[removed].index
57
+ def delete(self, entity):
58
+ """Delete a DICOM entity from the database
73
59
 
60
+ Args:
61
+ entity (list): entity to delete
62
+ """
63
+ removed = register.index(self.register, entity)
74
64
  # delete datasets marked for removal
75
65
  for index in removed.tolist():
76
66
  file = os.path.join(self.path, index)
77
67
  if os.path.exists(file):
78
68
  os.remove(file)
79
69
  # 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)
70
+ self.register = register.drop(removed)
88
71
  return self
89
72
 
90
73
 
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
74
+ def close(self):
75
+ """Close the DICOM folder
76
+
77
+ This also saves changes in the header file to disk.
78
+ """
79
+ # Save df as pkl
112
80
  file = self._register_file()
113
- self.register.to_pickle(file)
114
- return self
81
+ with open(file, 'w') as f:
82
+ json.dump(self.register, f, indent=4)
83
+ return self
115
84
 
85
+ def _register_file(self):
86
+ return os.path.join(self.path, 'dbtree.json')
87
+
116
88
 
117
89
  def summary(self):
118
90
  """Return a summary of the contents of the database.
@@ -122,6 +94,7 @@ class DataBaseDicom():
122
94
  """
123
95
  return register.summary(self.register)
124
96
 
97
+
125
98
  def print(self):
126
99
  """Print the contents of the DICOM folder
127
100
  """
@@ -277,10 +250,11 @@ class DataBaseDicom():
277
250
  dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
278
251
  multislice (bool, optional): Whether the data are to be read
279
252
  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.
253
+ is taken from the slice gap rather than the slice thickness. Defaults to False.
281
254
  """
282
255
  if ref is None:
283
256
  ds = dbdataset.new_dataset('MRImage')
257
+ #ds = dbdataset.new_dataset('ParametricMap')
284
258
  else:
285
259
  if ref[0] == series[0]:
286
260
  ref_mgr = self
@@ -293,12 +267,11 @@ class DataBaseDicom():
293
267
  attr = self._attributes(series)
294
268
  n = self._max_instance_number(attr['SeriesInstanceUID'])
295
269
 
296
- new_instances = {}
297
270
  if vol.ndim==3:
298
271
  slices = vol.split()
299
272
  for i, sl in tqdm(enumerate(slices), desc='Writing volume..'):
300
273
  dbdataset.set_volume(ds, sl, multislice)
301
- self._write_dataset(ds, attr, n + 1 + i, new_instances)
274
+ self._write_dataset(ds, attr, n + 1 + i)
302
275
  else:
303
276
  i=0
304
277
  vols = vol.separate().reshape(-1)
@@ -306,10 +279,9 @@ class DataBaseDicom():
306
279
  for sl in vt.split():
307
280
  dbdataset.set_volume(ds, sl, multislice)
308
281
  dbdataset.set_value(ds, sl.dims, sl.coords[:,...])
309
- self._write_dataset(ds, attr, n + 1 + i, new_instances)
282
+ self._write_dataset(ds, attr, n + 1 + i)
310
283
  i+=1
311
- return self
312
- self._update_register(new_instances)
284
+ return self
313
285
 
314
286
 
315
287
  def to_nifti(self, series:list, file:str, dims=None, multislice=False):
@@ -488,16 +460,6 @@ class DataBaseDicom():
488
460
  f"Cannot copy {from_entity} to {to_entity}. "
489
461
  )
490
462
 
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
463
  def move(self, from_entity, to_entity):
502
464
  """Move a DICOM entity
503
465
 
@@ -510,15 +472,15 @@ class DataBaseDicom():
510
472
 
511
473
  def _values(self, attributes:list, entity:list):
512
474
  # 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)
475
+ # if set(attributes) <= set(dbdatabase.COLUMNS):
476
+ # index = register.index(self.register, entity)
477
+ # v = self.register.loc[index, attributes].values
478
+ # else:
479
+ files = register.files(self.register, entity)
480
+ v = np.empty((len(files), len(attributes)), dtype=object)
481
+ for i, f in enumerate(files):
482
+ ds = dbdataset.read_dataset(f)
483
+ v[i,:] = dbdataset.get_values(ds, attributes)
522
484
  return v
523
485
 
524
486
  def _copy_patient(self, from_patient, to_patient):
@@ -562,30 +524,28 @@ class DataBaseDicom():
562
524
  n = self._max_instance_number(attr['SeriesInstanceUID'])
563
525
 
564
526
  # Copy the files to the new series
565
- new_instances = {}
566
527
  for i, f in tqdm(enumerate(files), total=len(files), desc=f'Copying series {to_series[1:]}'):
567
528
  # Read dataset and assign new properties
568
529
  ds = dbdataset.read_dataset(f)
569
- self._write_dataset(ds, attr, n + 1 + i, new_instances)
570
- self._update_register(new_instances)
530
+ self._write_dataset(ds, attr, n + 1 + i)
571
531
 
572
532
 
573
533
  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
534
+ for pt in self.register:
535
+ for st in pt['studies']:
536
+ if st['StudyInstanceUID'] == study_uid:
537
+ n = [sr['SeriesNumber'] for sr in st['studies']]
538
+ return np.amax(n)
539
+ return 0
580
540
 
581
541
  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
-
542
+ for pt in self.register:
543
+ for st in pt['studies']:
544
+ for sr in st['series']:
545
+ if sr['SeriesInstanceUID'] == series_uid:
546
+ n = list(sr['instances'].keys())
547
+ return np.amax([int(i) for i in n])
548
+ return 0
589
549
 
590
550
  def _attributes(self, entity):
591
551
  if len(entity)==4:
@@ -653,84 +613,49 @@ class DataBaseDicom():
653
613
  return study_attr | {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
654
614
 
655
615
 
656
- def _write_dataset(self, ds:Dataset, attr:dict, instance_nr:int, register:dict):
616
+ def _write_dataset(self, ds:Dataset, attr:dict, instance_nr:int):
657
617
  # Set new attributes
658
618
  attr['SOPInstanceUID'] = dbdataset.new_uid()
659
- attr['InstanceNumber'] = instance_nr
619
+ attr['InstanceNumber'] = str(instance_nr)
660
620
  dbdataset.set_values(ds, list(attr.keys()), list(attr.values()))
661
621
  # Save results in a new file
662
622
  rel_path = os.path.join('dbdicom', dbdataset.new_uid() + '.dcm')
663
623
  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
-
667
-
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])
624
+ # Add an entry in the register
625
+ register.add_instance(self.register, attr, rel_path)
626
+
627
+
628
+
629
+ # def _split_series(self):
630
+ # """
631
+ # Split series with multiple SOP Classes.
632
+
633
+ # If a series contain instances from different SOP Classes,
634
+ # these are separated out into multiple series with identical SOP Classes.
635
+ # """
636
+ # # For each series, check if there are multiple
637
+ # # SOP Classes in the series and split them if yes.
638
+ # for series in self.series():
639
+ # series_index = register.index(self.register, series)
640
+ # df_series = self.register.loc[series_index]
641
+ # sop_classes = df_series.SOPClassUID.unique()
642
+ # if len(sop_classes) > 1:
643
+ # # For each sop_class, create a new series and move all
644
+ # # instances of that sop_class to the new series
645
+ # desc = series[-1] if isinstance(series, str) else series[0]
646
+ # for i, sop_class in tqdm(enumerate(sop_classes[1:]), desc='Splitting series with multiple SOP Classes.'):
647
+ # df_sop_class = df_series[df_series.SOPClassUID == sop_class]
648
+ # relpaths = df_sop_class.index.tolist()
649
+ # sop_class_files = [os.path.join(self.path, p) for p in relpaths]
650
+ # sop_class_series = series[:-1] + [desc + f' [{i+1}]']
651
+ # self._files_to_series(sop_class_files, sop_class_series)
652
+ # # Delete original files permanently
653
+ # self.register.drop(relpaths)
654
+ # for f in sop_class_files:
655
+ # os.remove(f)
656
+ # self.register.drop('SOPClassUID', axis=1, inplace=True)
674
657
 
675
658
 
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
-
680
659
 
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
-
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
660
 
736
661