dbdicom 0.3.2__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/api.py CHANGED
@@ -1,3 +1,4 @@
1
+ from typing import Union
1
2
 
2
3
  import vreg
3
4
 
@@ -190,41 +191,36 @@ def move(from_entity:list, to_entity:list):
190
191
  dbd.close()
191
192
 
192
193
 
193
- def volume(series:list, dims:list=None, multislice=False) -> vreg.Volume3D:
194
+ def volume(series:list, dims:list=None) -> vreg.Volume3D:
194
195
  """Read a vreg.Volume3D from a DICOM series
195
196
 
196
197
  Args:
197
198
  series (list): DICOM series to read
198
199
  dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
199
- multislice (bool, optional): Whether the data are to be read
200
- as multislice or not. In multislice data the voxel size
201
- is taken from the slice gap rather thsan the slice thickness. Defaults to False.
202
200
 
203
201
  Returns:
204
202
  vreg.Volume3D: vole read from the series.
205
203
  """
204
+ if isinstance(series, str):
205
+ series = [series]
206
206
  dbd = open(series[0])
207
- vol = dbd.volume(series, dims, multislice)
207
+ vol = dbd.volume(series, dims)
208
208
  dbd.close()
209
209
  return vol
210
210
 
211
- def write_volume(vol:vreg.Volume3D, series:list, ref:list=None,
212
- multislice=False):
211
+ def write_volume(vol:Union[vreg.Volume3D, tuple], series:list, ref:list=None):
213
212
  """Write a vreg.Volume3D to a DICOM series
214
213
 
215
214
  Args:
216
- vol (vreg.Volume3D): Volume to write to the series.
215
+ vol (vreg.Volume3D or tuple): Volume to write to the series.
217
216
  series (list): DICOM series to read
218
217
  dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
219
- multislice (bool, optional): Whether the data are to be read
220
- as multislice or not. In multislice data the voxel size
221
- is taken from the slice gap rather thsan the slice thickness. Defaults to False.
222
218
  """
223
219
  dbd = open(series[0])
224
- dbd.write_volume(vol, series, ref, multislice)
220
+ dbd.write_volume(vol, series, ref)
225
221
  dbd.close()
226
222
 
227
- def to_nifti(series:list, file:str, dims:list=None, multislice=False):
223
+ def to_nifti(series:list, file:str, dims:list=None):
228
224
  """Save a DICOM series in nifti format.
229
225
 
230
226
  Args:
@@ -232,27 +228,21 @@ def to_nifti(series:list, file:str, dims:list=None, multislice=False):
232
228
  file (str): file path of the nifti file.
233
229
  dims (list, optional): Non-spatial dimensions of the volume.
234
230
  Defaults to None.
235
- multislice (bool, optional): Whether the data are to be read
236
- as multislice or not. In multislice data the voxel size
237
- is taken from the slice gap rather thaan the slice thickness. Defaults to False.
238
231
  """
239
232
  dbd = open(series[0])
240
- dbd.to_nifti(series, file, dims, multislice)
233
+ dbd.to_nifti(series, file, dims)
241
234
  dbd.close()
242
235
 
243
- def from_nifti(file:str, series:list, ref:list=None, multislice=False):
236
+ def from_nifti(file:str, series:list, ref:list=None):
244
237
  """Create a DICOM series from a nifti file.
245
238
 
246
239
  Args:
247
240
  file (str): file path of the nifti file.
248
241
  series (list): DICOM series to create
249
242
  ref (list): DICOM series to use as template.
250
- multislice (bool, optional): Whether the data are to be written
251
- as multislice or not. In multislice data the voxel size
252
- is written in the slice gap rather thaan the slice thickness. Defaults to False.
253
243
  """
254
244
  dbd = open(series[0])
255
- dbd.from_nifti(file, series, ref, multislice)
245
+ dbd.from_nifti(file, series, ref)
256
246
  dbd.close()
257
247
 
258
248
  def pixel_data(series:list, dims:list=None, include:list=None) -> tuple:
@@ -270,6 +260,8 @@ def pixel_data(series:list, dims:list=None, include:list=None) -> tuple:
270
260
  is provide these are returned as a dictionary in a third
271
261
  return value.
272
262
  """
263
+ if isinstance(series, str):
264
+ series = [series]
273
265
  dbd = open(series[0])
274
266
  array = dbd.pixel_data(series, dims, include)
275
267
  dbd.close()
dbdicom/database.py CHANGED
@@ -20,6 +20,7 @@ COLUMNS = [
20
20
  'PatientName',
21
21
  'StudyDescription',
22
22
  'StudyDate',
23
+ 'StudyID',
23
24
  'SeriesDescription',
24
25
  'SeriesNumber',
25
26
  'InstanceNumber',
@@ -77,6 +78,7 @@ def _multiframe_to_singleframe(path, df):
77
78
 
78
79
  def _tree(df):
79
80
  # A human-readable summary tree
81
+ # TODO: Add version number
80
82
 
81
83
  df.sort_values(['PatientID','StudyInstanceUID','SeriesNumber'], inplace=True)
82
84
  df = df.fillna('None')
@@ -94,10 +96,12 @@ def _tree(df):
94
96
  for uid_study in df_patient.StudyInstanceUID.unique():
95
97
  df_study = df_patient[df_patient.StudyInstanceUID == uid_study]
96
98
  study_desc = df_study.StudyDescription.values[0]
99
+ study_id = df_study.StudyID.values[0]
97
100
  study_date = df_study.StudyDate.values[0]
98
101
  study = {
99
102
  'StudyDescription': study_desc,
100
103
  'StudyDate': study_date,
104
+ 'StudyID': study_id,
101
105
  'StudyInstanceUID': uid_study,
102
106
  'series': [],
103
107
  }
dbdicom/dataset.py CHANGED
@@ -229,7 +229,8 @@ def write(ds, file, status=None):
229
229
  dir = os.path.dirname(file)
230
230
  if not os.path.exists(dir):
231
231
  os.makedirs(dir)
232
- ds.save_as(file, write_like_original=False)
232
+ #ds.save_as(file, write_like_original=False) # deprecated
233
+ ds.save_as(file, enforce_file_format=True)
233
234
 
234
235
 
235
236
  def codify(source_file, save_file, **kwargs):
@@ -513,32 +514,24 @@ def set_lut(ds, RGB):
513
514
 
514
515
 
515
516
 
516
- def affine(ds, multislice=False):
517
- if multislice:
518
- return image.affine_matrix(
519
- get_values(ds, 'ImageOrientationPatient'),
520
- get_values(ds, 'ImagePositionPatient'),
521
- get_values(ds, 'PixelSpacing'),
522
- get_values(ds, 'SpacingBetweenSlices'),
523
- )
524
- else:
525
- return image.affine_matrix(
526
- get_values(ds, 'ImageOrientationPatient'),
527
- get_values(ds, 'ImagePositionPatient'),
528
- get_values(ds, 'PixelSpacing'),
529
- get_values(ds, 'SliceThickness'),
530
- )
517
+ def affine(ds):
518
+ # Spacing Between Slices is not required so can be absent
519
+ slice_spacing = ds.get("SpacingBetweenSlices")
520
+ if slice_spacing is None:
521
+ slice_spacing = ds.get("SliceThickness")
522
+ return image.affine_matrix(
523
+ get_values(ds, 'ImageOrientationPatient'),
524
+ get_values(ds, 'ImagePositionPatient'),
525
+ get_values(ds, 'PixelSpacing'),
526
+ slice_spacing,
527
+ )
531
528
 
532
-
533
- def set_affine(ds, affine, multislice=False):
529
+ def set_affine(ds, affine):
534
530
  if affine is None:
535
531
  raise ValueError('The affine cannot be set to an empty value')
536
532
  v = image.dismantle_affine_matrix(affine)
537
533
  set_values(ds, 'PixelSpacing', v['PixelSpacing'])
538
- if multislice:
539
- set_values(ds, 'SpacingBetweenSlices', v['SliceThickness'])
540
- else:
541
- set_values(ds, 'SliceThickness', v['SliceThickness'])
534
+ set_values(ds, 'SpacingBetweenSlices', v['SpacingBetweenSlices'])
542
535
  set_values(ds, 'ImageOrientationPatient', v['ImageOrientationPatient'])
543
536
  set_values(ds, 'ImagePositionPatient', v['ImagePositionPatient'])
544
537
  set_values(ds, 'SliceLocation', np.dot(v['ImagePositionPatient'], v['slice_cosine']))
@@ -602,10 +595,10 @@ def set_pixel_data(ds, array):
602
595
  ds.PixelData = array.tobytes()
603
596
 
604
597
 
605
- def volume(ds, multislice=False):
606
- return vreg.volume(pixel_data(ds), affine(ds, multislice))
598
+ def volume(ds):
599
+ return vreg.volume(pixel_data(ds), affine(ds))
607
600
 
608
- def set_volume(ds, volume:vreg.Volume3D, multislice=False):
601
+ def set_volume(ds, volume:vreg.Volume3D):
609
602
  if volume is None:
610
603
  raise ValueError('The volume cannot be set to an empty value.')
611
604
  try:
@@ -621,7 +614,7 @@ def set_volume(ds, volume:vreg.Volume3D, multislice=False):
621
614
  if image.ndim != 2:
622
615
  raise ValueError("Can only write 2D images to a dataset.")
623
616
  set_pixel_data(ds, image)
624
- set_affine(ds, volume.affine, multislice)
617
+ set_affine(ds, volume.affine)
625
618
  if volume.coords is not None:
626
619
  # All other dimensions should have size 1
627
620
  coords = volume.coords.reshape((volume.coords.shape[0], -1))
dbdicom/dbd.py CHANGED
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  from datetime import datetime
3
3
  import json
4
+ from typing import Union
4
5
 
5
6
  from tqdm import tqdm
6
7
  import numpy as np
@@ -143,6 +144,11 @@ class DataBaseDicom():
143
144
  for patient in self.patients():
144
145
  studies += self.studies(patient, name, contains, isin)
145
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
146
152
  else:
147
153
  return register.studies(self.register, entity, name, contains, isin)
148
154
 
@@ -172,6 +178,11 @@ class DataBaseDicom():
172
178
  for study in self.studies(entity):
173
179
  series += self.series(study, name, contains, isin)
174
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
175
186
  elif len(entity)==2:
176
187
  series = []
177
188
  for study in self.studies(entity):
@@ -181,20 +192,20 @@ class DataBaseDicom():
181
192
  return register.series(self.register, entity, name, contains, isin)
182
193
 
183
194
 
184
- def volume(self, series:list, dims:list=None, multislice=False) -> vreg.Volume3D:
195
+ def volume(self, series:list, dims:list=None) -> vreg.Volume3D:
185
196
  """Read a vreg.Volume3D from a DICOM series
186
197
 
187
198
  Args:
188
199
  series (list): DICOM series to read
189
200
  dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
190
- multislice (bool, optional): Whether the data are to be read
191
- as multislice or not. In multislice data the voxel size
192
- is taken from the slice gap rather thsan the slice thickness. Defaults to False.
193
201
 
194
202
  Returns:
195
203
  vreg.Volume3D: vole read from the series.
196
204
  """
197
-
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)]
198
209
  if dims is None:
199
210
  dims = []
200
211
  elif isinstance(dims, str):
@@ -211,7 +222,7 @@ class DataBaseDicom():
211
222
  for f in tqdm(files, desc='Reading volume..'):
212
223
  ds = dbdataset.read_dataset(f)
213
224
  values.append(dbdataset.get_values(ds, dims))
214
- volumes.append(dbdataset.volume(ds, multislice))
225
+ volumes.append(dbdataset.volume(ds))
215
226
 
216
227
  # Format as mesh
217
228
  coords = np.stack(values, axis=-1)
@@ -229,9 +240,21 @@ class DataBaseDicom():
229
240
  "firstslice=True, the coordinates of the lowest "
230
241
  "slice will be assigned to the whole volume."
231
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)
232
247
 
233
248
  # Join 2D volumes into 3D volumes
234
- 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)
235
258
  if vol.ndim > 3:
236
259
  vol.set_coords(c0)
237
260
  vol.set_dims(dims[1:])
@@ -239,19 +262,18 @@ class DataBaseDicom():
239
262
 
240
263
 
241
264
  def write_volume(
242
- self, vol:vreg.Volume3D, series:list,
243
- ref:list=None, multislice=False,
265
+ self, vol:Union[vreg.Volume3D, tuple], series:list,
266
+ ref:list=None,
244
267
  ):
245
268
  """Write a vreg.Volume3D to a DICOM series
246
269
 
247
270
  Args:
248
271
  vol (vreg.Volume3D): Volume to write to the series.
249
272
  series (list): DICOM series to read
250
- dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
251
- multislice (bool, optional): Whether the data are to be read
252
- as multislice or not. In multislice data the voxel size
253
- is taken from the slice gap rather than the slice thickness. Defaults to False.
273
+ ref (list): Reference series
254
274
  """
275
+ if isinstance(vol, tuple):
276
+ vol = vreg.volume(vol[0], vol[1])
255
277
  if ref is None:
256
278
  ds = dbdataset.new_dataset('MRImage')
257
279
  #ds = dbdataset.new_dataset('ParametricMap')
@@ -270,21 +292,21 @@ class DataBaseDicom():
270
292
  if vol.ndim==3:
271
293
  slices = vol.split()
272
294
  for i, sl in tqdm(enumerate(slices), desc='Writing volume..'):
273
- dbdataset.set_volume(ds, sl, multislice)
295
+ dbdataset.set_volume(ds, sl)
274
296
  self._write_dataset(ds, attr, n + 1 + i)
275
297
  else:
276
298
  i=0
277
299
  vols = vol.separate().reshape(-1)
278
300
  for vt in tqdm(vols, desc='Writing volume..'):
279
301
  for sl in vt.split():
280
- dbdataset.set_volume(ds, sl, multislice)
302
+ dbdataset.set_volume(ds, sl)
281
303
  dbdataset.set_value(ds, sl.dims, sl.coords[:,...])
282
304
  self._write_dataset(ds, attr, n + 1 + i)
283
305
  i+=1
284
306
  return self
285
307
 
286
308
 
287
- def to_nifti(self, series:list, file:str, dims=None, multislice=False):
309
+ def to_nifti(self, series:list, file:str, dims=None):
288
310
  """Save a DICOM series in nifti format.
289
311
 
290
312
  Args:
@@ -292,34 +314,31 @@ class DataBaseDicom():
292
314
  file (str): file path of the nifti file.
293
315
  dims (list, optional): Non-spatial dimensions of the volume.
294
316
  Defaults to None.
295
- multislice (bool, optional): Whether the data are to be read
296
- as multislice or not. In multislice data the voxel size
297
- is taken from the slice gap rather thaan the slice thickness. Defaults to False.
298
317
  """
299
- vol = self.volume(series, dims, multislice)
318
+ vol = self.volume(series, dims)
300
319
  vreg.write_nifti(vol, file)
301
320
  return self
302
321
 
303
- 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):
304
323
  """Create a DICOM series from a nifti file.
305
324
 
306
325
  Args:
307
326
  file (str): file path of the nifti file.
308
327
  series (list): DICOM series to create
309
328
  ref (list): DICOM series to use as template.
310
- multislice (bool, optional): Whether the data are to be written
311
- as multislice or not. In multislice data the voxel size
312
- is written in the slice gap rather thaan the slice thickness. Defaults to False.
313
329
  """
314
330
  vol = vreg.read_nifti(file)
315
- self.write_volume(vol, series, ref, multislice)
331
+ self.write_volume(vol, series, ref)
316
332
  return self
317
333
 
318
334
  def pixel_data(self, series:list, dims:list=None, coords=False, include=None) -> np.ndarray:
319
335
  """Read the pixel data from a DICOM series
320
336
 
321
337
  Args:
322
- 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.
323
342
  dims (list, optional): Dimensions of the array.
324
343
  coords (bool): If set to Trye, the coordinates of the
325
344
  arrays are returned alongside the pixel data
@@ -332,8 +351,12 @@ class DataBaseDicom():
332
351
  coords is set these are returned too as an array with
333
352
  coordinates of the slices according to dims. If include
334
353
  is provide the values are returned as a dictionary in the last
335
- return value.
354
+ return value.
336
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)]
337
360
  if coords:
338
361
  if dims is None:
339
362
  raise ValueError(
@@ -529,13 +552,19 @@ class DataBaseDicom():
529
552
  ds = dbdataset.read_dataset(f)
530
553
  self._write_dataset(ds, attr, n + 1 + i)
531
554
 
532
-
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
+
533
562
  def _max_series_number(self, study_uid):
534
563
  for pt in self.register:
535
564
  for st in pt['studies']:
536
565
  if st['StudyInstanceUID'] == study_uid:
537
- n = [sr['SeriesNumber'] for sr in st['studies']]
538
- return np.amax(n)
566
+ n = [sr['SeriesNumber'] for sr in st['series']]
567
+ return int(np.amax(n))
539
568
  return 0
540
569
 
541
570
  def _max_instance_number(self, series_uid):
@@ -544,7 +573,7 @@ class DataBaseDicom():
544
573
  for sr in st['series']:
545
574
  if sr['SeriesInstanceUID'] == series_uid:
546
575
  n = list(sr['instances'].keys())
547
- return np.amax([int(i) for i in n])
576
+ return int(np.amax([int(i) for i in n]))
548
577
  return 0
549
578
 
550
579
  def _attributes(self, entity):
@@ -566,7 +595,8 @@ class DataBaseDicom():
566
595
  except:
567
596
  # If the patient does not exist, generate values
568
597
  attr = ['PatientID', 'PatientName']
569
- 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]}"
570
600
  patient_name = patient[-1] if isinstance(patient[-1], str) else patient[-1][0]
571
601
  vals = [patient_id, patient_name]
572
602
  return {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
@@ -581,12 +611,18 @@ class DataBaseDicom():
581
611
  ds = dbdataset.read_dataset(files[0])
582
612
  vals = dbdataset.get_values(ds, attr)
583
613
  except:
584
- # If the study does not exist, generate values
585
- attr = ['StudyInstanceUID', 'StudyDescription', 'StudyDate']
586
- 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()
587
623
  study_desc = study[-1] if isinstance(study[-1], str) else study[-1][0]
588
624
  study_date = datetime.today().strftime('%Y%m%d')
589
- vals = [study_id, study_desc, study_date]
625
+ vals = [study_uid, study_desc, study_date, str(study_id)]
590
626
  return patient_attr | {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
591
627
 
592
628
 
@@ -607,9 +643,9 @@ class DataBaseDicom():
607
643
  else:
608
644
  series_number = 1 + self._max_series_number(study_uid)
609
645
  attr = ['SeriesInstanceUID', 'SeriesDescription', 'SeriesNumber']
610
- series_id = dbdataset.new_uid()
646
+ series_uid = dbdataset.new_uid()
611
647
  series_desc = series[-1] if isinstance(series[-1], str) else series[-1][0]
612
- vals = [series_id, series_desc, series_number]
648
+ vals = [series_uid, series_desc, int(series_number)]
613
649
  return study_attr | {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
614
650
 
615
651
 
@@ -619,7 +655,13 @@ class DataBaseDicom():
619
655
  attr['InstanceNumber'] = str(instance_nr)
620
656
  dbdataset.set_values(ds, list(attr.keys()), list(attr.values()))
621
657
  # Save results in a new file
622
- 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')
623
665
  dbdataset.write(ds, os.path.join(self.path, rel_path))
624
666
  # Add an entry in the register
625
667
  register.add_instance(self.register, attr, rel_path)
@@ -656,6 +698,59 @@ class DataBaseDicom():
656
698
  # self.register.drop('SOPClassUID', axis=1, inplace=True)
657
699
 
658
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)
742
+
743
+ slice_spacing[d] = slice_spacing_d
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
+ )
751
+
752
+ return vols.reshape(shape)
753
+
659
754
 
660
755
 
661
756
 
dbdicom/register.py CHANGED
@@ -21,6 +21,7 @@ def add_instance(dbtree:list, instance, rel_path):
21
21
  st = {
22
22
  'StudyDescription': instance['StudyDescription'],
23
23
  'StudyDate': instance['StudyDate'],
24
+ 'StudyID': instance['StudyID'],
24
25
  'StudyInstanceUID': instance['StudyInstanceUID'],
25
26
  'series': [],
26
27
  }
@@ -186,7 +187,7 @@ def _patient_uid(dbtree, patient):
186
187
  f"Please specify the index in the call to patient_uid(). "
187
188
  f"For instance ({patient}, {len(patients)-1})'. "
188
189
  )
189
- raise ValueError(f"Patient {patient} not found in database.")
190
+
190
191
 
191
192
 
192
193
  def _study_uid(dbtree, study):
@@ -23,33 +23,54 @@ def pixel_data(ds):
23
23
  if [0x2005, 0x100E] in ds: # 'Philips Rescale Slope'
24
24
  slope = ds[(0x2005, 0x100E)].value
25
25
  intercept = ds[(0x2005, 0x100D)].value
26
- array -= intercept
27
- array /= slope
26
+ if (intercept == 0) and (slope == 1):
27
+ array = array.astype(np.int16)
28
+ else:
29
+ array = array.astype(np.float32)
30
+ array -= intercept
31
+ array /= slope
28
32
  else:
29
33
  slope = float(getattr(ds, 'RescaleSlope', 1))
30
34
  intercept = float(getattr(ds, 'RescaleIntercept', 0))
31
- array *= slope
32
- array += intercept
35
+ if (intercept == 0) and (slope == 1):
36
+ array = array.astype(np.int16)
37
+ else:
38
+ array = array.astype(np.float32)
39
+ array *= slope
40
+ array += intercept
33
41
  return np.transpose(array)
34
42
 
35
43
 
36
44
  def set_pixel_data(ds, array):
37
45
 
46
+ # Delete 'Philips Rescale Slope'
38
47
  if (0x2005, 0x100E) in ds:
39
- del ds[0x2005, 0x100E] # Delete 'Philips Rescale Slope'
48
+ del ds[0x2005, 0x100E]
40
49
  if (0x2005, 0x100D) in ds:
41
50
  del ds[0x2005, 0x100D]
42
51
 
52
+ ds.BitsAllocated = 16
53
+ ds.BitsStored = 16
54
+ ds.HighBit = 15
55
+
43
56
  # clipping may slow down a lot
44
- array = image.clip(array.astype(np.float32))
45
- array, slope, intercept = image.scale_to_range(array, ds.BitsAllocated)
46
- array = np.transpose(array)
57
+ #array = image.clip(array.astype(np.float32))
58
+ array = image.clip(array) # remove nan and infs
59
+ if array.dtype==np.int16:
60
+ ds.PixelRepresentation = 1
61
+ ds.RescaleSlope = 1
62
+ ds.RescaleIntercept = 0
63
+ elif array.dtype==np.uint16:
64
+ ds.PixelRepresentation = 0
65
+ ds.RescaleSlope = 1
66
+ ds.RescaleIntercept = 0
67
+ else:
68
+ array, slope, intercept = image.scale_to_range(array, ds.BitsStored)
69
+ ds.PixelRepresentation = 0
70
+ ds.RescaleSlope = 1 / slope
71
+ ds.RescaleIntercept = - intercept / slope
47
72
 
48
- ds.PixelRepresentation = 0
49
- ds.RescaleSlope = 1 / slope
50
- ds.RescaleIntercept = - intercept / slope
51
- # ds.WindowCenter = (maximum + minimum) / 2
52
- # ds.WindowWidth = maximum - minimum
73
+ array = np.transpose(array)
53
74
  ds.Rows = array.shape[0]
54
75
  ds.Columns = array.shape[1]
55
76
  ds.PixelData = array.tobytes()
dbdicom/utils/image.py CHANGED
@@ -54,14 +54,13 @@ def dismantle_affine_matrix(affine):
54
54
  # ImagePositionPatient_i = ImagePositionPatient + i * SpacingBetweenSlices * slice_cosine
55
55
  column_spacing = np.linalg.norm(affine[:3, 0])
56
56
  row_spacing = np.linalg.norm(affine[:3, 1])
57
- slice_thickness = np.linalg.norm(affine[:3, 2])
57
+ slice_spacing = np.linalg.norm(affine[:3, 2])
58
58
  row_cosine = affine[:3, 0] / column_spacing
59
59
  column_cosine = affine[:3, 1] / row_spacing
60
- slice_cosine = affine[:3, 2] / slice_thickness
60
+ slice_cosine = affine[:3, 2] / slice_spacing
61
61
  return {
62
62
  'PixelSpacing': [row_spacing, column_spacing],
63
- # 'SpacingBetweenSlices': slice_thickness, # Obsolete
64
- 'SliceThickness': slice_thickness,
63
+ 'SpacingBetweenSlices': slice_spacing,
65
64
  'ImageOrientationPatient': row_cosine.tolist() + column_cosine.tolist(),
66
65
  'ImagePositionPatient': affine[:3, 3].tolist(), # first slice for a volume
67
66
  'slice_cosine': slice_cosine.tolist()}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbdicom
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: A pythonic interface for reading and writing DICOM databases
5
5
  Author-email: Steven Sourbron <s.sourbron@sheffield.ac.uk>, Ebony Gunwhy <e.gunwhy@sheffield.ac.uk>
6
6
  Project-URL: Homepage, https://openmiblab.github.io/dbdicom/
@@ -1,21 +1,21 @@
1
1
  dbdicom/__init__.py,sha256=DyogeTraV6o-FgWdBCbtVEaMmdkMQHkYkraDIE0t8OA,25
2
- dbdicom/api.py,sha256=Xcur6TstwvSD8QMDkFn97NDnuVymjCtHWldimG8CDP4,9783
2
+ dbdicom/api.py,sha256=MHJBHY_ikUj93T8Vt0GAjvjLFJL-2IdIFKhr8T-bvVI,8871
3
3
  dbdicom/const.py,sha256=BqBiRRjeiSqDr1W6YvaayD8WKCjG4Cny2NT0GeLM6bI,4269
4
- dbdicom/database.py,sha256=w835qQHThZsIhoTXlQbZykcKnVrEADgp2mRYBTOKz4g,4627
5
- dbdicom/dataset.py,sha256=dm0bmBjqb_3uZLYV87gKszlqKMMLrSbaymVROafB5ac,22143
6
- dbdicom/dbd.py,sha256=xpDiWzlMUmXzYgRCR5jwvvHENbTsq3B89HvK2JxAo3c,26450
7
- dbdicom/register.py,sha256=5wBQsf4PB6ITHN3clUOMStUy5iYjMnIYUBfhVS2Drio,23692
4
+ dbdicom/database.py,sha256=_LUbH7gc9l7j_63AC71DjwxgTUwbEjHSy5kuvRw75Hw,4764
5
+ dbdicom/dataset.py,sha256=hLAyFlN7zQ-dOzI9V67aHfTq3VtpvCI7_83tnBqXObE,21880
6
+ dbdicom/dbd.py,sha256=ZUXmLcQ2C6L9UTanGzTSte1XIJAcQoNNsEpweXW8N50,29921
7
+ dbdicom/register.py,sha256=VPxS2oTlONxx5eNdGRbvFeEfQo69jo5YDp7L_Vb4x28,23676
8
8
  dbdicom/external/__init__.py,sha256=XNQqfspyf6vFGedXlRKZsUB8k8E-0W19Uamwn8Aioxo,316
9
- dbdicom/external/__pycache__/__init__.cpython-311.pyc,sha256=cIySrImYKo1fJP3-0ZMV-5ZKcZEEDX-uwfZ7I4U-jRs,534
9
+ dbdicom/external/__pycache__/__init__.cpython-311.pyc,sha256=pXAQ35ixd92fm6YcuHgzR1t6RcASQ-cHhU1wOA5b8sw,542
10
10
  dbdicom/external/dcm4che/README.md,sha256=0aAGRs36W3_0s5LzWHRGf_tqariS_JP4iJggaxnD4Xw,8987
11
11
  dbdicom/external/dcm4che/__init__.py,sha256=YwpeMCLrxffGOkchsGjgAuB6ia3VX_tx9Y7ru9EWtoY,35
12
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc,sha256=QsnVh0zH1ZSOoNwtTqiirWH8CB0b2eJ0DEvgVoVhZ0Y,248
12
+ dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc,sha256=FB8wyWqXDUt_1P-QmE4yt9uD6dDm5YqYWjqVuRwGdSo,256
13
13
  dbdicom/external/dcm4che/bin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
14
  dbdicom/external/dcm4che/bin/deidentify,sha256=64MNIEpp-CWzFSb6TV0KtyCBvD7XyEsovRjBeyxDqSc,1698
15
15
  dbdicom/external/dcm4che/bin/deidentify.bat,sha256=kVXUkcy1C4Y3KjC2NJwmmR0pufSJWmaof_LR5CTAxMg,1455
16
16
  dbdicom/external/dcm4che/bin/emf2sf,sha256=svCzkZ-QhdVTV0NNHOpBiwNBMODVWZHJIFA7cWaN2bM,1622
17
17
  dbdicom/external/dcm4che/bin/emf2sf.bat,sha256=Vh0ry9KNJX_WXcyCrLSxbJ_6Crot9rjmwi__u2GZqLY,1375
18
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc,sha256=M2mla4vNW1gSUWQps3dx4g8NRd4BaQJLP1OKQHmi6Sk,195
18
+ dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc,sha256=GYcm47ETjYvRUN5RPTe5R-c0prd14GP8gm96eJcy0uQ,203
19
19
  dbdicom/external/dcm4che/etc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  dbdicom/external/dcm4che/etc/emf2sf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  dbdicom/external/dcm4che/etc/emf2sf/log4j.properties,sha256=3hHcBFt2oNRjvHtix5bfuEsnKfdv5IYOkbsyoY9g7cM,223
@@ -36,7 +36,7 @@ dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll,sha256=QanyzLy0Cd79-aOV
36
36
  dbdicom/external/dcm4che/lib/windows-x86-64/opencv_java.dll,sha256=TmjW2SbG4MR3GQ95T8xCVVDLgsdKukgaHBPUvWkfXp8,11039232
37
37
  dbdicom/sop_classes/ct_image.py,sha256=16PNv_0e1_7cfxE12JWlx5YQeaTAQVzwtXTjxs3aonk,2812
38
38
  dbdicom/sop_classes/enhanced_mr_image.py,sha256=13j4EGXniBpJxpzzL3Xa4y3g5OKhMd5Ct7cjPGOYQY4,35496
39
- dbdicom/sop_classes/mr_image.py,sha256=nHSQIh_zpdmZ0VhwKCVr9iqkluHxl1ASQ75Fr9OeNXI,10270
39
+ dbdicom/sop_classes/mr_image.py,sha256=kNcrWXZ3VC3hhfqjMRjrlZOVqZH3Q5KfWXYLfLD-bEY,10913
40
40
  dbdicom/sop_classes/parametric_map.py,sha256=2OKBuC2bo03OEpKqimQS-nVGFp1cKRPYwVgmDGVf1JU,12288
41
41
  dbdicom/sop_classes/secondary_capture.py,sha256=wgNRX8qyhV7HR7Jq2tQWPPuGpiRzYl6qPOgK6qFbPUc,4541
42
42
  dbdicom/sop_classes/segmentation.py,sha256=I8-PciIoIz27_-dZ4esBZSw0TBBbO8KbNYTiTmVe62g,11465
@@ -45,10 +45,10 @@ dbdicom/sop_classes/xray_angiographic_image.py,sha256=nWysCGaEWKVNItnOgyJfcGMpS3
45
45
  dbdicom/utils/arrays.py,sha256=wiqCczLXlNl0qIePVOwCYvbAJhPveNorplkhtGleS48,1121
46
46
  dbdicom/utils/dcm4che.py,sha256=Vxq8NYWWK3BuqJkzhBQ89oMqzJlnxqTxgsgTo_Frznc,2317
47
47
  dbdicom/utils/files.py,sha256=qhWNJqeWnRjDNbERpC6Mz962_TW9mFdvd2lnBbK3xt4,2259
48
- dbdicom/utils/image.py,sha256=dCjukz9Zvpl4cP3KR1Y4hmKkox5KaFiEhdxDWDEdNec,4110
48
+ dbdicom/utils/image.py,sha256=D46CD_ezpp2uq8VMqug5Z09fAyoJ9U6VwuxIFNJK8zg,4048
49
49
  dbdicom/utils/variables.py,sha256=vUh5cDnmCft5hoXDYXUvfkg5Cy5WlgMAogU38Y_BKRo,5753
50
- dbdicom-0.3.2.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
51
- dbdicom-0.3.2.dist-info/METADATA,sha256=zqs5ghuuW4o1UR_QkGhiM9afOqmKYWMg0d54ThWRV0k,1030
52
- dbdicom-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
- dbdicom-0.3.2.dist-info/top_level.txt,sha256=nJWxXg4YjD6QblfmhrzTMXcr8FSKNc0Yk-CAIDUsYkQ,8
54
- dbdicom-0.3.2.dist-info/RECORD,,
50
+ dbdicom-0.3.3.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
51
+ dbdicom-0.3.3.dist-info/METADATA,sha256=aMYhKPS907fOj0pgSkQh7vbFeYspNvJP5-ZYNZ6LpMs,1030
52
+ dbdicom-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
53
+ dbdicom-0.3.3.dist-info/top_level.txt,sha256=nJWxXg4YjD6QblfmhrzTMXcr8FSKNc0Yk-CAIDUsYkQ,8
54
+ dbdicom-0.3.3.dist-info/RECORD,,