dbdicom 0.3.5__tar.gz → 0.3.7__tar.gz

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.

Files changed (63) hide show
  1. {dbdicom-0.3.5/src/dbdicom.egg-info → dbdicom-0.3.7}/PKG-INFO +1 -1
  2. {dbdicom-0.3.5 → dbdicom-0.3.7}/pyproject.toml +1 -1
  3. dbdicom-0.3.7/src/dbdicom/__init__.py +4 -0
  4. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/api.py +128 -4
  5. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/dataset.py +40 -14
  6. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/dbd.py +210 -35
  7. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/mr_image.py +5 -3
  8. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/arrays.py +6 -2
  9. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/image.py +4 -9
  10. {dbdicom-0.3.5 → dbdicom-0.3.7/src/dbdicom.egg-info}/PKG-INFO +1 -1
  11. dbdicom-0.3.5/src/dbdicom/__init__.py +0 -1
  12. {dbdicom-0.3.5 → dbdicom-0.3.7}/LICENSE +0 -0
  13. {dbdicom-0.3.5 → dbdicom-0.3.7}/MANIFEST.in +0 -0
  14. {dbdicom-0.3.5 → dbdicom-0.3.7}/README.rst +0 -0
  15. {dbdicom-0.3.5 → dbdicom-0.3.7}/setup.cfg +0 -0
  16. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/const.py +0 -0
  17. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/database.py +0 -0
  18. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/__init__.py +0 -0
  19. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
  20. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/README.md +0 -0
  21. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/__init__.py +0 -0
  22. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
  23. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/__init__.py +0 -0
  24. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
  25. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/deidentify +0 -0
  26. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/deidentify.bat +0 -0
  27. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/emf2sf +0 -0
  28. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/emf2sf.bat +0 -0
  29. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/etc/__init__.py +0 -0
  30. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/etc/emf2sf/__init__.py +0 -0
  31. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/etc/emf2sf/log4j.properties +0 -0
  32. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/__init__.py +0 -0
  33. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/commons-cli-1.4.jar +0 -0
  34. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-core-5.23.1.jar +0 -0
  35. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-emf-5.23.1.jar +0 -0
  36. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-common-5.23.1.jar +0 -0
  37. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-emf2sf-5.23.1.jar +0 -0
  38. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/log4j-1.2.17.jar +0 -0
  39. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/macosx-x86-64/libopencv_java.jnilib +0 -0
  40. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/slf4j-api-1.7.30.jar +0 -0
  41. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/slf4j-log4j12-1.7.30.jar +0 -0
  42. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio.dll +0 -0
  43. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_sse2.dll +0 -0
  44. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_util.dll +0 -0
  45. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll +0 -0
  46. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86-64/opencv_java.dll +0 -0
  47. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/register.py +0 -0
  48. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/ct_image.py +0 -0
  49. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/enhanced_mr_image.py +0 -0
  50. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/parametric_map.py +0 -0
  51. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/secondary_capture.py +0 -0
  52. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/segmentation.py +0 -0
  53. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/ultrasound_multiframe_image.py +0 -0
  54. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/xray_angiographic_image.py +0 -0
  55. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/dcm4che.py +0 -0
  56. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/files.py +0 -0
  57. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/variables.py +0 -0
  58. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom.egg-info/SOURCES.txt +0 -0
  59. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom.egg-info/dependency_links.txt +0 -0
  60. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom.egg-info/requires.txt +0 -0
  61. {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom.egg-info/top_level.txt +0 -0
  62. {dbdicom-0.3.5 → dbdicom-0.3.7}/tests/test_api.py +0 -0
  63. {dbdicom-0.3.5 → dbdicom-0.3.7}/tests/test_dcm4che.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbdicom
3
- Version: 0.3.5
3
+ Version: 0.3.7
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/
@@ -7,7 +7,7 @@ requires = ['setuptools>=61.2']
7
7
 
8
8
  [project]
9
9
  name = "dbdicom"
10
- version = "0.3.5"
10
+ version = "0.3.7"
11
11
  dependencies = [
12
12
  "tqdm",
13
13
  "importlib-resources",
@@ -0,0 +1,4 @@
1
+ from dbdicom.api import *
2
+
3
+ # Utilities
4
+ from dbdicom.utils.image import affine_matrix
@@ -1,10 +1,18 @@
1
+ import os
2
+ import shutil
3
+ import zipfile
4
+ from pathlib import Path
1
5
  from typing import Union
6
+ from tqdm import tqdm
7
+
2
8
 
3
9
  import vreg
4
10
 
5
11
  from dbdicom.dbd import DataBaseDicom
6
12
 
7
13
 
14
+
15
+
8
16
  def open(path:str) -> DataBaseDicom:
9
17
  """Open a DICOM database
10
18
 
@@ -262,13 +270,65 @@ def from_nifti(file:str, series:list, ref:list=None):
262
270
  dbd.from_nifti(file, series, ref)
263
271
  dbd.close()
264
272
 
265
- def pixel_data(series:list, dims:list=None, include:list=None) -> tuple:
273
+
274
+ def values(series:list, attr=None, dims:list=None, coords=False) -> Union[dict, tuple]:
275
+ """Read the values of some or all attributes from a DICOM series
276
+
277
+ Args:
278
+ series (list or str): DICOM series to read. This can also
279
+ be a path to a folder containing DICOM files, or a
280
+ patient or study to read all series in that patient or
281
+ study. In those cases a list is returned.
282
+ attr (list, optional): list of DICOM attributes to read.
283
+ dims (list, optional): Dimensions to sort the attributes.
284
+ If dims is not provided, values are sorted by
285
+ InstanceNumber.
286
+ coords (bool): If set to True, the coordinates of the
287
+ attributes are returned alongside the values
288
+
289
+ Returns:
290
+ dict or tuple: values as a dictionary in the last
291
+ return value, where each value is a numpy array with
292
+ the required dimensions. If coords is set to True,
293
+ these are returned too.
294
+ """
295
+ if isinstance(series, str):
296
+ series = [series]
297
+ dbd = open(series[0])
298
+ array = dbd.values(series, attr, dims, coords)
299
+ dbd.close()
300
+ return array
301
+
302
+
303
+ def files(entity:list) -> list:
304
+ """Read the files in a DICOM entity
305
+
306
+ Args:
307
+ entity (list or str): DICOM entity to read. This can
308
+ be a path to a folder containing DICOM files, or a
309
+ patient or study to read all series in that patient or
310
+ study.
311
+
312
+ Returns:
313
+ list: list of valid dicom files.
314
+ """
315
+ if isinstance(entity, str):
316
+ entity = [entity]
317
+ dbd = open(entity[0])
318
+ files = dbd.files(entity)
319
+ dbd.close()
320
+ return files
321
+
322
+
323
+ def pixel_data(series:list, dims:list=None, coords=False, attr:list=None) -> tuple:
266
324
  """Read the pixel data from a DICOM series
267
325
 
268
326
  Args:
269
327
  series (list): DICOM series to read
270
328
  dims (list, optional): Dimensions of the array.
271
- include (list, optional): list of DICOM attributes that are
329
+ coords (bool): If set to True, the coordinates of the
330
+ slices are returned alongside the pixel data.
331
+ attr (list, optional): list of DICOM attributes that are
272
332
  read on the fly to avoid reading the data twice.
273
333
 
274
334
  Returns:
@@ -280,7 +340,7 @@ def pixel_data(series:list, dims:list=None, include:list=None) -> tuple:
280
340
  if isinstance(series, str):
281
341
  series = [series]
282
342
  dbd = open(series[0])
283
- array = dbd.pixel_data(series, dims, include)
343
+ array = dbd.pixel_data(series, dims, coords, attr)
284
344
  dbd.close()
285
345
  return array
286
346
 
@@ -311,10 +371,74 @@ def unique(pars:list, entity:list) -> dict:
311
371
  return u
312
372
 
313
373
 
374
+ def archive(path, archive_path):
375
+ dbd = open(path)
376
+ dbd.archive(archive_path)
377
+ dbd.close()
314
378
 
315
379
 
380
+ def restore(archive_path, path):
381
+ _copy_and_extract_zips(archive_path, path)
382
+ dbd = open(path)
383
+ dbd.close()
316
384
 
317
385
 
318
- if __name__=='__main__':
386
+ def _copy_and_extract_zips(src_folder, dest_folder):
387
+ if not os.path.exists(dest_folder):
388
+ os.makedirs(dest_folder)
389
+
390
+ # First pass: count total files
391
+ total_files = sum(len(files) for _, _, files in os.walk(src_folder))
319
392
 
393
+ with tqdm(total=total_files, desc="Copying and extracting") as pbar:
394
+ for root, dirs, files in os.walk(src_folder):
395
+ rel_path = os.path.relpath(root, src_folder)
396
+ dest_path = os.path.join(dest_folder, rel_path)
397
+ os.makedirs(dest_path, exist_ok=True)
398
+
399
+ for file in files:
400
+ src_file_path = os.path.join(root, file)
401
+ dest_file_path = os.path.join(dest_path, file)
402
+
403
+ if file.lower().endswith('.zip'):
404
+ try:
405
+ zip_dest_folder = dest_file_path[:-4]
406
+ with zipfile.ZipFile(src_file_path, 'r') as zip_ref:
407
+ zip_ref.extractall(zip_dest_folder)
408
+ #tqdm.write(f"Extracted ZIP: {src_file_path}")
409
+ #_flatten_folder(zip_dest_folder) # still needed?
410
+ except zipfile.BadZipFile:
411
+ tqdm.write(f"Bad ZIP file skipped: {src_file_path}")
412
+ else:
413
+ shutil.copy2(src_file_path, dest_file_path)
414
+
415
+ pbar.update(1)
416
+
417
+
418
+ def _flatten_folder(root_folder):
419
+ for dirpath, dirnames, filenames in os.walk(root_folder, topdown=False):
420
+ for filename in filenames:
421
+ src_path = os.path.join(dirpath, filename)
422
+ dst_path = os.path.join(root_folder, filename)
423
+
424
+ # If file with same name exists, optionally rename or skip
425
+ if os.path.exists(dst_path):
426
+ base, ext = os.path.splitext(filename)
427
+ counter = 1
428
+ while os.path.exists(dst_path):
429
+ dst_path = os.path.join(root_folder, f"{base}_{counter}{ext}")
430
+ counter += 1
431
+
432
+ shutil.move(src_path, dst_path)
433
+
434
+ # Remove empty subdirectories (but skip the root folder)
435
+ if dirpath != root_folder:
436
+ try:
437
+ os.rmdir(dirpath)
438
+ except OSError:
439
+ print(f"Could not remove {dirpath} — not empty or in use.")
440
+
441
+
442
+
443
+ if __name__=='__main__':
320
444
  pass
@@ -574,25 +574,51 @@ def set_pixel_data(ds, array):
574
574
  if hasattr(mod, 'set_pixel_data'):
575
575
  return getattr(mod, 'set_pixel_data')(ds, array)
576
576
 
577
- # if array.ndim >= 3: # remove spurious dimensions of 1
578
- # array = np.squeeze(array)
577
+ ds.BitsAllocated = 16
578
+ ds.BitsStored = 16
579
+ ds.HighBit = 15
580
+
581
+ if array.dtype==np.int16:
582
+ array = image.clip(array) # remove nan and infs
583
+ ds.PixelRepresentation = 1
584
+ ds.RescaleSlope = 1
585
+ ds.RescaleIntercept = 0
586
+ elif array.dtype==np.uint16:
587
+ array = image.clip(array) # remove nan and infs
588
+ ds.PixelRepresentation = 0
589
+ ds.RescaleSlope = 1
590
+ ds.RescaleIntercept = 0
591
+ else:
592
+ array = image.clip(array) # remove nan and infs
593
+ array, slope, intercept = image.scale_to_range(array, ds.BitsStored)
594
+ ds.PixelRepresentation = 0
595
+ ds.RescaleSlope = 1 / slope
596
+ ds.RescaleIntercept = - intercept / slope
579
597
 
580
- array = image.clip(array.astype(np.float32))
581
- array, slope, intercept = image.scale_to_range(array, ds.BitsAllocated)
582
598
  array = np.transpose(array)
583
-
584
- ds.PixelRepresentation = 0
585
- #ds.SmallestImagePixelValue = int(0)
586
- #ds.LargestImagePixelValue = int(2**ds.BitsAllocated - 1)
587
- #ds.set_values('SmallestImagePixelValue', int(0))
588
- #ds.set_values('LargestImagePixelValue', int(2**ds.BitsAllocated - 1))
589
- ds.RescaleSlope = 1 / slope
590
- ds.RescaleIntercept = - intercept / slope
591
- # ds.WindowCenter = (maximum + minimum) / 2
592
- # ds.WindowWidth = maximum - minimum
593
599
  ds.Rows = array.shape[0]
594
600
  ds.Columns = array.shape[1]
595
601
  ds.PixelData = array.tobytes()
602
+
603
+ # # if array.ndim >= 3: # remove spurious dimensions of 1
604
+ # # array = np.squeeze(array)
605
+
606
+ # array = image.clip(array.astype(np.float32))
607
+ # array, slope, intercept = image.scale_to_range(array, ds.BitsAllocated)
608
+ # array = np.transpose(array)
609
+
610
+ # ds.PixelRepresentation = 0
611
+ # #ds.SmallestImagePixelValue = int(0)
612
+ # #ds.LargestImagePixelValue = int(2**ds.BitsAllocated - 1)
613
+ # #ds.set_values('SmallestImagePixelValue', int(0))
614
+ # #ds.set_values('LargestImagePixelValue', int(2**ds.BitsAllocated - 1))
615
+ # ds.RescaleSlope = 1 / slope
616
+ # ds.RescaleIntercept = - intercept / slope
617
+ # # ds.WindowCenter = (maximum + minimum) / 2
618
+ # # ds.WindowWidth = maximum - minimum
619
+ # ds.Rows = array.shape[0]
620
+ # ds.Columns = array.shape[1]
621
+ # ds.PixelData = array.tobytes()
596
622
 
597
623
 
598
624
  def volume(ds):
@@ -2,6 +2,7 @@ import os
2
2
  from datetime import datetime
3
3
  import json
4
4
  from typing import Union
5
+ import zipfile
5
6
 
6
7
  from tqdm import tqdm
7
8
  import numpy as np
@@ -39,7 +40,12 @@ class DataBaseDicom():
39
40
  # have been made which are not reflected in the json
40
41
  # file on disk
41
42
  # os.remove(file)
42
- except:
43
+ except Exception as e:
44
+ # raise ValueError(
45
+ # f'Cannot open {file}. Please close any programs that are '
46
+ # f'using it and try again. Alternatively you can delete the file '
47
+ # f'manually and try again.'
48
+ # )
43
49
  # If the file can't be read, delete it and load again
44
50
  os.remove(file)
45
51
  self.read()
@@ -239,9 +245,7 @@ class DataBaseDicom():
239
245
  if not np.array_equal(coords[1:,k+1,...], c0):
240
246
  raise ValueError(
241
247
  "Cannot build a single volume. Not all slices "
242
- "have the same coordinates. \nIf you set "
243
- "firstslice=True, the coordinates of the lowest "
244
- "slice will be assigned to the whole volume."
248
+ "have the same coordinates."
245
249
  )
246
250
 
247
251
  # Infer spacing between slices from slice locations
@@ -335,7 +339,7 @@ class DataBaseDicom():
335
339
  self.write_volume(vol, series, ref)
336
340
  return self
337
341
 
338
- def pixel_data(self, series:list, dims:list=None, coords=False, include=None) -> np.ndarray:
342
+ def pixel_data(self, series:list, dims:list=None, coords=False, attr=None) -> np.ndarray:
339
343
  """Read the pixel data from a DICOM series
340
344
 
341
345
  Args:
@@ -344,9 +348,9 @@ class DataBaseDicom():
344
348
  patient or study to read all series in that patient or
345
349
  study. In those cases a list is returned.
346
350
  dims (list, optional): Dimensions of the array.
347
- coords (bool): If set to Trye, the coordinates of the
351
+ coords (bool): If set to True, the coordinates of the
348
352
  arrays are returned alongside the pixel data
349
- include (list, optional): list of DICOM attributes that are
353
+ attr (list, optional): list of DICOM attributes that are
350
354
  read on the fly to avoid reading the data twice.
351
355
 
352
356
  Returns:
@@ -354,48 +358,42 @@ class DataBaseDicom():
354
358
  at least 3 dimensions (x,y,z). If
355
359
  coords is set these are returned too as an array with
356
360
  coordinates of the slices according to dims. If include
357
- is provide the values are returned as a dictionary in the last
361
+ is provided the values are returned as a dictionary in the last
358
362
  return value.
359
363
  """
360
364
  if isinstance(series, str): # path to folder
361
- return [self.pixel_data(s, dims, coords, include) for s in self.series(series)]
365
+ return [self.pixel_data(s, dims, coords, attr) for s in self.series(series)]
362
366
  if len(series) < 4: # folder, patient or study
363
- return [self.pixel_data(s, dims, coords, include) for s in self.series(series)]
364
- if coords:
365
- if dims is None:
366
- raise ValueError(
367
- "Coordinates can only be returned if dimensions are specified."
368
- )
367
+ return [self.pixel_data(s, dims, coords, attr) for s in self.series(series)]
369
368
 
370
369
  if dims is None:
371
- dims = []
370
+ dims = ['InstanceNumber']
372
371
  elif np.isscalar(dims):
373
372
  dims = [dims]
374
373
  else:
375
374
  dims = list(dims)
376
- dims = ['SliceLocation'] + dims
377
375
 
378
376
  # Ensure return_vals is a list
379
- if include is None:
377
+ if attr is None:
380
378
  params = []
381
- elif np.isscalar(include):
382
- params = [include]
379
+ elif np.isscalar(attr):
380
+ params = [attr]
383
381
  else:
384
- params = list(include)
382
+ params = list(attr)
385
383
 
386
384
  files = register.files(self.register, series)
387
385
 
388
386
  # Read dicom files
389
387
  coords_array = []
390
388
  arrays = np.empty(len(files), dtype=dict)
391
- if include is not None:
389
+ if attr is not None:
392
390
  values = np.empty(len(files), dtype=dict)
393
391
  for i, f in tqdm(enumerate(files), desc='Reading pixel data..'):
394
392
  ds = dbdataset.read_dataset(f)
395
393
  coords_array.append(dbdataset.get_values(ds, dims))
396
394
  # save as dict so numpy does not stack as arrays
397
395
  arrays[i] = {'pixel_data': dbdataset.pixel_data(ds)}
398
- if include is not None:
396
+ if attr is not None:
399
397
  values[i] = {'values': dbdataset.get_values(ds, params)}
400
398
 
401
399
  # Format as mesh
@@ -406,20 +404,169 @@ class DataBaseDicom():
406
404
  arrays = np.stack([a['pixel_data'] for a in arrays.reshape(-1)], axis=-1)
407
405
  arrays = arrays.reshape(arrays.shape[:2] + coords_array.shape[1:])
408
406
 
409
- if include is None:
407
+ if attr is None:
410
408
  if coords:
411
- return arrays, coords_array[1:,...]
409
+ return arrays, coords_array
412
410
  else:
413
411
  return arrays
412
+
413
+ # Return values as a dictionary
414
+ values = values[inds].reshape(-1)
415
+ values_dict = {}
416
+ for p in range(len(params)):
417
+ # Get the type from the first value
418
+ vp0 = values[0]['values'][p]
419
+ # Build an array of the right type
420
+ vp = np.zeros(values.size, dtype=type(vp0))
421
+ # Populate the array with values for parameter p
422
+ for i, v in enumerate(values):
423
+ vp[i] = v['values'][p]
424
+ # Reshape values for parameter p
425
+ vp = vp.reshape(coords_array.shape[1:])
426
+ # Eneter in the dictionary
427
+ values_dict[params[p]] = vp
428
+
429
+ # If only one, return as value
430
+ if len(params) == 1:
431
+ values_return = values_dict[attr[0]]
432
+ else:
433
+ values_return = values_dict
414
434
 
415
- values = values[inds].reshape(coords_array.shape[1:])
416
- values = np.stack([a['values'] for a in values.reshape(-1)], axis=-1)
417
- values = values.reshape((len(params), ) + coords_array.shape[1:])
435
+ # problem if the values are a list. Needs an array with a prespeficied dtype
436
+ # values = values[inds].reshape(coords_array.shape[1:])
437
+ # values = np.stack([a['values'] for a in values.reshape(-1)], axis=-1)
438
+ # values = values.reshape((len(params), ) + coords_array.shape[1:])
418
439
 
419
440
  if coords:
420
- return arrays, coords_array[1:,...], values
441
+ return arrays, coords_array, values_return
421
442
  else:
422
- return arrays, values
443
+ return arrays, values_return
444
+
445
+
446
+ def values(self, series:list, attr=None, dims:list=None, coords=False) -> Union[dict, tuple]:
447
+ """Read the values of some or all attributes from a DICOM series
448
+
449
+ Args:
450
+ series (list or str): DICOM series to read. This can also
451
+ be a path to a folder containing DICOM files, or a
452
+ patient or study to read all series in that patient or
453
+ study. In those cases a list is returned.
454
+ attr (list, optional): list of DICOM attributes to read.
455
+ dims (list, optional): Dimensions to sort the attributes.
456
+ If dims is not provided, values are sorted by
457
+ InstanceNumber.
458
+ coords (bool): If set to True, the coordinates of the
459
+ attributes are returned alongside the values
460
+
461
+ Returns:
462
+ dict or tuple: values as a dictionary in the last
463
+ return value, where each value is a numpy array with
464
+ the required dimensions. If coords is set to True,
465
+ these are returned too.
466
+ """
467
+ if isinstance(series, str): # path to folder
468
+ return [self.values(s, attr, dims, coords) for s in self.series(series)]
469
+ if len(series) < 4: # folder, patient or study
470
+ return [self.values(s, attr, dims, coords) for s in self.series(series)]
471
+
472
+ if dims is None:
473
+ dims = ['InstanceNumber']
474
+ elif np.isscalar(dims):
475
+ dims = [dims]
476
+ else:
477
+ dims = list(dims)
478
+
479
+ files = register.files(self.register, series)
480
+
481
+ # Ensure return_vals is a list
482
+ if attr is None:
483
+ # If attributes are not provided, read all
484
+ # attributes from the first file
485
+ ds = dbdataset.read_dataset(files[0])
486
+ exclude = ['PixelData', 'FloatPixelData', 'DoubleFloatPixelData']
487
+ params = []
488
+ param_labels = []
489
+ for elem in ds:
490
+ if elem.keyword not in exclude:
491
+ params.append(elem.tag)
492
+ # For known tags use the keyword as label
493
+ label = elem.tag if len(elem.keyword)==0 else elem.keyword
494
+ param_labels.append(label)
495
+ elif np.isscalar(attr):
496
+ params = [attr]
497
+ param_labels = params[:]
498
+ else:
499
+ params = list(attr)
500
+ param_labels = params[:]
501
+
502
+ # Read dicom files
503
+ coords_array = []
504
+ values = np.empty(len(files), dtype=dict)
505
+ for i, f in tqdm(enumerate(files), desc='Reading values..'):
506
+ ds = dbdataset.read_dataset(f)
507
+ coords_array.append(dbdataset.get_values(ds, dims))
508
+ # save as dict so numpy does not stack as arrays
509
+ values[i] = {'values': dbdataset.get_values(ds, params)}
510
+
511
+ # Format as mesh
512
+ coords_array = np.stack([v for v in coords_array], axis=-1)
513
+ coords_array, inds = dbdicom.utils.arrays.meshvals(coords_array)
514
+
515
+ # Sort values accordingly
516
+ values = values[inds].reshape(-1)
517
+
518
+ # Return values as a dictionary
519
+ values_dict = {}
520
+ for p in range(len(params)):
521
+ # Get the type from the first value
522
+ vp0 = values[0]['values'][p]
523
+ # Build an array of the right type
524
+ vp = np.zeros(values.size, dtype=type(vp0))
525
+ # Populate the arrate with values for parameter p
526
+ for i, v in enumerate(values):
527
+ vp[i] = v['values'][p]
528
+ # Reshape values for parameter p
529
+ vp = vp.reshape(coords_array.shape[1:])
530
+ # Eneter in the dictionary
531
+ values_dict[param_labels[p]] = vp
532
+
533
+ # If only one, return as value
534
+ if len(params) == 1:
535
+ values_return = values_dict[params[0]]
536
+ else:
537
+ values_return = values_dict
538
+
539
+ if coords:
540
+ return values_return, coords_array
541
+ else:
542
+ return values_return
543
+
544
+
545
+ def files(self, entity:list) -> list:
546
+ """Read the files in a DICOM entity
547
+
548
+ Args:
549
+ entity (list or str): DICOM entity to read. This can
550
+ be a path to a folder containing DICOM files, or a
551
+ patient or study to read all series in that patient or
552
+ study.
553
+
554
+ Returns:
555
+ list: list of valid dicom files.
556
+ """
557
+ if isinstance(entity, str): # path to folder
558
+ files = []
559
+ for s in self.series(entity):
560
+ files += self.files(s)
561
+ return files
562
+ if len(entity) < 4: # folder, patient or study
563
+ files = []
564
+ for s in self.series(entity):
565
+ files += self.files(s)
566
+ return files
567
+
568
+ return register.files(self.register, entity)
569
+
423
570
 
424
571
 
425
572
  def unique(self, pars:list, entity:list) -> dict:
@@ -723,9 +870,9 @@ class DataBaseDicom():
723
870
  dbdataset.set_values(ds, list(attr.keys()), list(attr.values()))
724
871
  # Save results in a new file
725
872
  rel_dir = os.path.join(
726
- f"patient_{attr['PatientID']}",
727
- f"study_[{attr['StudyID']}]_{attr['StudyDescription']}",
728
- f"series_[{attr['SeriesNumber']}]_{attr['SeriesDescription']}",
873
+ f"Patient__{attr['PatientID']}",
874
+ f"Study__{attr['StudyID']}__{attr['StudyDescription']}",
875
+ f"Series__{attr['SeriesNumber']}__{attr['SeriesDescription']}",
729
876
  )
730
877
  os.makedirs(os.path.join(self.path, rel_dir), exist_ok=True)
731
878
  rel_path = os.path.join(rel_dir, dbdataset.new_uid() + '.dcm')
@@ -734,6 +881,34 @@ class DataBaseDicom():
734
881
  register.add_instance(self.register, attr, rel_path)
735
882
 
736
883
 
884
+ def archive(self, archive_path):
885
+ # TODO add flat=True option for zipping at patient level
886
+ for pt in tqdm(self.register, desc='Archiving '):
887
+ for st in pt['studies']:
888
+ zip_dir = os.path.join(
889
+ archive_path,
890
+ f"Patient__{pt['PatientID']}",
891
+ f"Study__{st['StudyID']}__{st['StudyDescription']}",
892
+ )
893
+ os.makedirs(zip_dir, exist_ok=True)
894
+ for sr in st['series']:
895
+ try:
896
+ zip_file = os.path.join(
897
+ zip_dir,
898
+ f"Series__{sr['SeriesNumber']}__{sr['SeriesDescription']}.zip",
899
+ )
900
+ with zipfile.ZipFile(zip_file, 'w') as zipf:
901
+ for rel_path in sr['instances'].values():
902
+ file = os.path.join(self.path, rel_path)
903
+ zipf.write(file, arcname=os.path.basename(file))
904
+ except Exception as e:
905
+ raise RuntimeError(
906
+ f"Error extracting series {sr['SeriesDescription']} "
907
+ f"in study {st['StudyDescription']} of patient {pt['PatientID']}."
908
+ )
909
+
910
+
911
+
737
912
 
738
913
  def infer_slice_spacing(vols):
739
914
  # In case spacing between slices is not (correctly) encoded in
@@ -761,8 +936,8 @@ def infer_slice_spacing(vols):
761
936
  slice_loc = np.sort(slice_loc)
762
937
  distances = slice_loc[1:] - slice_loc[:-1]
763
938
 
764
- # Round to micrometer and check if unique
765
- distances = np.around(distances, 3)
939
+ # Round to 10 micrometer and check if unique
940
+ distances = np.around(distances, 2)
766
941
  slice_spacing_d = np.unique(distances)
767
942
 
768
943
  # Check if unique - otherwise this is not a volume
@@ -1,6 +1,7 @@
1
1
  import os
2
2
 
3
3
  import numpy as np
4
+ import vreg
4
5
  import pydicom
5
6
  from pydicom.dataset import FileMetaDataset, Dataset, FileDataset
6
7
  from pydicom.sequence import Sequence
@@ -53,18 +54,18 @@ def set_pixel_data(ds, array):
53
54
  ds.BitsStored = 16
54
55
  ds.HighBit = 15
55
56
 
56
- # clipping may slow down a lot
57
- #array = image.clip(array.astype(np.float32))
58
- array = image.clip(array) # remove nan and infs
59
57
  if array.dtype==np.int16:
58
+ array = image.clip(array) # remove nan and infs
60
59
  ds.PixelRepresentation = 1
61
60
  ds.RescaleSlope = 1
62
61
  ds.RescaleIntercept = 0
63
62
  elif array.dtype==np.uint16:
63
+ array = image.clip(array) # remove nan and infs
64
64
  ds.PixelRepresentation = 0
65
65
  ds.RescaleSlope = 1
66
66
  ds.RescaleIntercept = 0
67
67
  else:
68
+ array = image.clip(array) # remove nan and infs
68
69
  array, slope, intercept = image.scale_to_range(array, ds.BitsStored)
69
70
  ds.PixelRepresentation = 0
70
71
  ds.RescaleSlope = 1 / slope
@@ -76,6 +77,7 @@ def set_pixel_data(ds, array):
76
77
  ds.PixelData = array.tobytes()
77
78
 
78
79
 
80
+
79
81
  def default(): # from the RIDER dataset
80
82
 
81
83
  # File meta info data elements
@@ -29,8 +29,12 @@ def _mesh_shape(sorted_array):
29
29
 
30
30
  if np.prod(shape) != sorted_array.size:
31
31
  raise ValueError(
32
- 'These are not mesh coordinates.'
33
- 'Make sure to specify dimensions for a multidimensional series.'
32
+ 'Improper dimensions for the series. This usually means '
33
+ 'that there are multiple images at the same location, \n or that '
34
+ 'there are no images at one or more locations. \n\n'
35
+ 'Make sure to specify proper dimensions when reading a pixel array or volume. \n'
36
+ 'If the default dimensions of pixel_array (InstanceNumber) generate this error, '
37
+ 'the DICOM data may be corrupted.'
34
38
  )
35
39
 
36
40
  return shape
@@ -7,7 +7,7 @@ def affine_matrix( # single slice function
7
7
  image_orientation, # ImageOrientationPatient
8
8
  image_position, # ImagePositionPatient
9
9
  pixel_spacing, # PixelSpacing
10
- slice_thickness): # SliceThickness
10
+ slice_spacing): # SpacingBetweenSlices
11
11
 
12
12
  row_spacing = pixel_spacing[0]
13
13
  column_spacing = pixel_spacing[1]
@@ -16,15 +16,10 @@ def affine_matrix( # single slice function
16
16
  column_cosine = np.array(image_orientation[3:])
17
17
  slice_cosine = np.cross(row_cosine, column_cosine)
18
18
 
19
- # This should not be addressed here
20
- # # The coronal orientation has a left-handed reference frame
21
- # if np.array_equal(np.around(image_orientation, 3), [1,0,0,0,0,-1]):
22
- # slice_cosine = -slice_cosine
23
-
24
19
  affine = np.identity(4, dtype=np.float32)
25
20
  affine[:3, 0] = row_cosine * column_spacing
26
21
  affine[:3, 1] = column_cosine * row_spacing
27
- affine[:3, 2] = slice_cosine * slice_thickness
22
+ affine[:3, 2] = slice_cosine * slice_spacing
28
23
  affine[:3, 3] = image_position
29
24
 
30
25
  return affine
@@ -91,8 +86,8 @@ def scale_to_range(array, bits_allocated, signed=False):
91
86
  else:
92
87
  slope = range / (maximum - minimum)
93
88
  intercept = -slope * minimum + minval
94
- array *= slope
95
- array += intercept
89
+ array = array * slope
90
+ array = array + intercept
96
91
 
97
92
  if bits_allocated == 8:
98
93
  if signed:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbdicom
3
- Version: 0.3.5
3
+ Version: 0.3.7
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 +0,0 @@
1
- from dbdicom.api import *
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes