dbdicom 0.3.0__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
 
@@ -24,6 +32,7 @@ def print(path):
24
32
  """
25
33
  dbd = open(path)
26
34
  dbd.print()
35
+ dbd.close()
27
36
 
28
37
 
29
38
  def summary(path) -> dict:
@@ -36,7 +45,24 @@ def summary(path) -> dict:
36
45
  dict: Nested dictionary with summary information on the database.
37
46
  """
38
47
  dbd = open(path)
39
- return dbd.summary()
48
+ s = dbd.summary()
49
+ dbd.close()
50
+ return s
51
+
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
40
66
 
41
67
 
42
68
  def patients(path, name:str=None, contains:str=None, isin:list=None)->list:
@@ -56,7 +82,9 @@ def patients(path, name:str=None, contains:str=None, isin:list=None)->list:
56
82
  list: list of patients fulfilling the criteria.
57
83
  """
58
84
  dbd = open(path)
59
- return dbd.patients(name, contains, isin)
85
+ p = dbd.patients(name, contains, isin)
86
+ dbd.close()
87
+ return p
60
88
 
61
89
 
62
90
  def studies(entity:str | list, name:str=None, contains:str=None, isin:list=None)->list:
@@ -79,10 +107,14 @@ def studies(entity:str | list, name:str=None, contains:str=None, isin:list=None)
79
107
  """
80
108
  if isinstance(entity, str): # path = folder
81
109
  dbd = open(entity)
82
- return dbd.studies(entity, name, contains, isin)
110
+ s = dbd.studies(entity, name, contains, isin)
111
+ dbd.close()
112
+ return s
83
113
  elif len(entity)==2: # path = patient
84
114
  dbd = open(entity[0])
85
- return dbd.studies(entity, name, contains, isin)
115
+ s = dbd.studies(entity, name, contains, isin)
116
+ dbd.close()
117
+ return s
86
118
  else:
87
119
  raise ValueError(
88
120
  "The path must be a folder or a 2-element list "
@@ -110,10 +142,14 @@ def series(entity:str | list, name:str=None, contains:str=None, isin:list=None)-
110
142
  """
111
143
  if isinstance(entity, str): # path = folder
112
144
  dbd = open(entity)
113
- return dbd.series(entity, name, contains, isin)
145
+ s = dbd.series(entity, name, contains, isin)
146
+ dbd.close()
147
+ return s
114
148
  elif len(entity) in [2,3]:
115
149
  dbd = open(entity[0])
116
- return dbd.series(entity, name, contains, isin)
150
+ s = dbd.series(entity, name, contains, isin)
151
+ dbd.close()
152
+ return s
117
153
  else:
118
154
  raise ValueError(
119
155
  "To retrieve a series, the entity must be a database, patient or study."
@@ -168,7 +204,9 @@ def volume(series:list, dims:list=None, multislice=False) -> vreg.Volume3D:
168
204
  vreg.Volume3D: vole read from the series.
169
205
  """
170
206
  dbd = open(series[0])
171
- return dbd.volume(series, dims, multislice)
207
+ vol = dbd.volume(series, dims, multislice)
208
+ dbd.close()
209
+ return vol
172
210
 
173
211
  def write_volume(vol:vreg.Volume3D, series:list, ref:list=None,
174
212
  multislice=False):
@@ -200,6 +238,7 @@ def to_nifti(series:list, file:str, dims:list=None, multislice=False):
200
238
  """
201
239
  dbd = open(series[0])
202
240
  dbd.to_nifti(series, file, dims, multislice)
241
+ dbd.close()
203
242
 
204
243
  def from_nifti(file:str, series:list, ref:list=None, multislice=False):
205
244
  """Create a DICOM series from a nifti file.
@@ -232,7 +271,9 @@ def pixel_data(series:list, dims:list=None, include:list=None) -> tuple:
232
271
  return value.
233
272
  """
234
273
  dbd = open(series[0])
235
- return dbd.pixel_data(series, dims, include)
274
+ array = dbd.pixel_data(series, dims, include)
275
+ dbd.close()
276
+ return array
236
277
 
237
278
  # write_pixel_data()
238
279
  # values()
@@ -255,7 +296,9 @@ def unique(pars:list, entity:list) -> dict:
255
296
  dict: dictionary with unique values for each attribute.
256
297
  """
257
298
  dbd = open(entity[0])
258
- return dbd.unique(pars, entity)
299
+ u = dbd.unique(pars, entity)
300
+ dbd.close()
301
+ return u
259
302
 
260
303
 
261
304
 
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
@@ -1,16 +1,18 @@
1
+ # Test data
2
+ # https://www.aliza-dicom-viewer.com/download/datasets
3
+
1
4
  import os
2
5
  from datetime import datetime
3
6
  import struct
4
7
  from tqdm import tqdm
5
8
 
6
9
  import numpy as np
7
- import pandas as pd
8
10
  import pydicom
9
11
  from pydicom.util.codify import code_file
10
12
  import pydicom.config
11
- from pydicom.dataset import Dataset
12
13
  import vreg
13
14
 
15
+
14
16
  import dbdicom.utils.image as image
15
17
  import dbdicom.utils.variables as variables
16
18
  from dbdicom.sop_classes import (
@@ -71,6 +73,8 @@ def new_dataset(sop_class):
71
73
  return xray_angiographic_image.default()
72
74
  if sop_class == 'UltrasoundMultiFrameImage':
73
75
  return ultrasound_multiframe_image.default()
76
+ if sop_class == 'ParametricMap':
77
+ return parametric_map.default()
74
78
  else:
75
79
  raise ValueError(
76
80
  f"DICOM class {sop_class} is not currently supported"
@@ -235,7 +239,7 @@ def codify(source_file, save_file, **kwargs):
235
239
  file.close()
236
240
 
237
241
 
238
- def read_data(files, tags, path=None, images_only=False):
242
+ def read_data(files, tags, path=None, images_only=False): # obsolete??
239
243
 
240
244
  if np.isscalar(files):
241
245
  files = [files]
@@ -263,34 +267,6 @@ def read_data(files, tags, path=None, images_only=False):
263
267
 
264
268
 
265
269
 
266
- def read_dataframe(files, tags, path=None, images_only=False):
267
- if np.isscalar(files):
268
- files = [files]
269
- if np.isscalar(tags):
270
- tags = [tags]
271
- array = []
272
- dicom_files = []
273
- for i, file in tqdm(enumerate(files), desc='Reading DICOM folder'):
274
- try:
275
- ds = pydicom.dcmread(file, force=True, specific_tags=tags+['Rows'])
276
- except:
277
- pass
278
- else:
279
- if isinstance(ds, pydicom.dataset.FileDataset):
280
- if 'TransferSyntaxUID' in ds.file_meta:
281
- if images_only:
282
- if not 'Rows' in ds:
283
- continue
284
- row = get_values(ds, tags)
285
- array.append(row)
286
- if path is None:
287
- index = file
288
- else:
289
- index = os.path.relpath(file, path)
290
- dicom_files.append(index)
291
- df = pd.DataFrame(array, index = dicom_files, columns = tags)
292
- return df
293
-
294
270
 
295
271
  def _add_new(ds, tag, value, VR='OW'):
296
272
  if not isinstance(tag, pydicom.tag.BaseTag):
@@ -583,7 +559,7 @@ def pixel_data(ds):
583
559
  try:
584
560
  array = ds.pixel_array
585
561
  except:
586
- return None
562
+ raise ValueError("Dataset has no pixel data.")
587
563
  array = array.astype(np.float32)
588
564
  slope = float(getattr(ds, 'RescaleSlope', 1))
589
565
  intercept = float(getattr(ds, 'RescaleIntercept', 0))
@@ -592,7 +568,7 @@ def pixel_data(ds):
592
568
  return np.transpose(array)
593
569
 
594
570
 
595
- def set_pixel_data(ds, array, value_range=None):
571
+ def set_pixel_data(ds, array):
596
572
  if array is None:
597
573
  raise ValueError('The pixel array cannot be set to an empty value.')
598
574
 
@@ -608,7 +584,7 @@ def set_pixel_data(ds, array, value_range=None):
608
584
  # if array.ndim >= 3: # remove spurious dimensions of 1
609
585
  # array = np.squeeze(array)
610
586
 
611
- array = image.clip(array.astype(np.float32), value_range=value_range)
587
+ array = image.clip(array.astype(np.float32))
612
588
  array, slope, intercept = image.scale_to_range(array, ds.BitsAllocated)
613
589
  array = np.transpose(array)
614
590
 
@@ -632,6 +608,15 @@ def volume(ds, multislice=False):
632
608
  def set_volume(ds, volume:vreg.Volume3D, multislice=False):
633
609
  if volume is None:
634
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
+
635
620
  image = np.squeeze(volume.values)
636
621
  if image.ndim != 2:
637
622
  raise ValueError("Can only write 2D images to a dataset.")
@@ -712,41 +697,7 @@ def set_signal_type(ds, value):
712
697
 
713
698
 
714
699
 
700
+ if __name__=='__main__':
715
701
 
716
- # def _initialize(ds, UID=None, ref=None): # ds is pydicom dataset
717
-
718
- # # Date and Time of Creation
719
- # dt = datetime.now()
720
- # timeStr = dt.strftime('%H%M%S') # long format with micro seconds
721
-
722
- # ds.ContentDate = dt.strftime('%Y%m%d')
723
- # ds.ContentTime = timeStr
724
- # ds.AcquisitionDate = dt.strftime('%Y%m%d')
725
- # ds.AcquisitionTime = timeStr
726
- # ds.SeriesDate = dt.strftime('%Y%m%d')
727
- # ds.SeriesTime = timeStr
728
- # ds.InstanceCreationDate = dt.strftime('%Y%m%d')
729
- # ds.InstanceCreationTime = timeStr
730
-
731
- # if UID is not None:
732
-
733
- # # overwrite UIDs
734
- # ds.PatientID = UID[0]
735
- # ds.StudyInstanceUID = UID[1]
736
- # ds.SeriesInstanceUID = UID[2]
737
- # ds.SOPInstanceUID = UID[3]
738
-
739
- # if ref is not None:
740
-
741
- # # Series, Instance and Class for Reference
742
- # refd_instance = Dataset()
743
- # refd_instance.ReferencedSOPClassUID = ref.SOPClassUID
744
- # refd_instance.ReferencedSOPInstanceUID = ref.SOPInstanceUID
745
-
746
- # refd_series = Dataset()
747
- # refd_series.ReferencedInstanceSequence = Sequence([refd_instance])
748
- # refd_series.SeriesInstanceUID = ds.SeriesInstanceUID
749
-
750
- # ds.ReferencedSeriesSequence = Sequence([refd_series])
751
-
752
- # return ds
702
+ pass
703
+ #codify('C:\\Users\\md1spsx\\Documents\\f32bit.dcm', 'C:\\Users\\md1spsx\\Documents\\f32bit.py')