dbdicom 0.3.6__tar.gz → 0.3.8__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 (62) hide show
  1. {dbdicom-0.3.6/src/dbdicom.egg-info → dbdicom-0.3.8}/PKG-INFO +1 -1
  2. {dbdicom-0.3.6 → dbdicom-0.3.8}/pyproject.toml +1 -1
  3. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/api.py +79 -6
  4. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/dataset.py +14 -0
  5. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/dbd.py +72 -13
  6. {dbdicom-0.3.6 → dbdicom-0.3.8/src/dbdicom.egg-info}/PKG-INFO +1 -1
  7. {dbdicom-0.3.6 → dbdicom-0.3.8}/LICENSE +0 -0
  8. {dbdicom-0.3.6 → dbdicom-0.3.8}/MANIFEST.in +0 -0
  9. {dbdicom-0.3.6 → dbdicom-0.3.8}/README.rst +0 -0
  10. {dbdicom-0.3.6 → dbdicom-0.3.8}/setup.cfg +0 -0
  11. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/__init__.py +0 -0
  12. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/const.py +0 -0
  13. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/database.py +0 -0
  14. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/__init__.py +0 -0
  15. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
  16. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/README.md +0 -0
  17. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/__init__.py +0 -0
  18. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
  19. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/bin/__init__.py +0 -0
  20. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
  21. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/bin/deidentify +0 -0
  22. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/bin/deidentify.bat +0 -0
  23. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/bin/emf2sf +0 -0
  24. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/bin/emf2sf.bat +0 -0
  25. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/etc/__init__.py +0 -0
  26. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/etc/emf2sf/__init__.py +0 -0
  27. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/etc/emf2sf/log4j.properties +0 -0
  28. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/__init__.py +0 -0
  29. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/commons-cli-1.4.jar +0 -0
  30. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/dcm4che-core-5.23.1.jar +0 -0
  31. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/dcm4che-emf-5.23.1.jar +0 -0
  32. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-common-5.23.1.jar +0 -0
  33. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-emf2sf-5.23.1.jar +0 -0
  34. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/log4j-1.2.17.jar +0 -0
  35. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/macosx-x86-64/libopencv_java.jnilib +0 -0
  36. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/slf4j-api-1.7.30.jar +0 -0
  37. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/slf4j-log4j12-1.7.30.jar +0 -0
  38. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio.dll +0 -0
  39. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_sse2.dll +0 -0
  40. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_util.dll +0 -0
  41. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll +0 -0
  42. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/external/dcm4che/lib/windows-x86-64/opencv_java.dll +0 -0
  43. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/register.py +0 -0
  44. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/sop_classes/ct_image.py +0 -0
  45. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/sop_classes/enhanced_mr_image.py +0 -0
  46. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/sop_classes/mr_image.py +0 -0
  47. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/sop_classes/parametric_map.py +0 -0
  48. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/sop_classes/secondary_capture.py +0 -0
  49. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/sop_classes/segmentation.py +0 -0
  50. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/sop_classes/ultrasound_multiframe_image.py +0 -0
  51. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/sop_classes/xray_angiographic_image.py +0 -0
  52. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/utils/arrays.py +0 -0
  53. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/utils/dcm4che.py +0 -0
  54. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/utils/files.py +0 -0
  55. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/utils/image.py +0 -0
  56. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom/utils/variables.py +0 -0
  57. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom.egg-info/SOURCES.txt +0 -0
  58. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom.egg-info/dependency_links.txt +0 -0
  59. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom.egg-info/requires.txt +0 -0
  60. {dbdicom-0.3.6 → dbdicom-0.3.8}/src/dbdicom.egg-info/top_level.txt +0 -0
  61. {dbdicom-0.3.6 → dbdicom-0.3.8}/tests/test_api.py +0 -0
  62. {dbdicom-0.3.6 → dbdicom-0.3.8}/tests/test_dcm4che.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbdicom
3
- Version: 0.3.6
3
+ Version: 0.3.8
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.6"
10
+ version = "0.3.8"
11
11
  dependencies = [
12
12
  "tqdm",
13
13
  "importlib-resources",
@@ -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
 
@@ -190,19 +198,20 @@ def move(from_entity:list, to_entity:list):
190
198
  dbd.delete(from_entity)
191
199
  dbd.close()
192
200
 
193
- def split_series(series:list, attr:Union[str, tuple])->dict:
201
+ def split_series(series:list, attr:Union[str, tuple], key=None)->list:
194
202
  """
195
203
  Split a series into multiple series
196
204
 
197
205
  Args:
198
206
  series (list): series to split.
199
- attr (str or tuple): dicom attribute to split the series by.
207
+ attr (str or tuple): dicom attribute to split the series by.
208
+ key (function): split by by key(attr)
200
209
  Returns:
201
- dict: dictionary with keys the unique values found (ascending)
202
- and as values the series corresponding to that value.
210
+ list: list of two-element tuples, where the first element is
211
+ is the value and the second element is the series corresponding to that value.
203
212
  """
204
213
  dbd = open(series[0])
205
- split_series = dbd.split_series(series, attr)
214
+ split_series = dbd.split_series(series, attr, key)
206
215
  dbd.close()
207
216
  return split_series
208
217
 
@@ -363,10 +372,74 @@ def unique(pars:list, entity:list) -> dict:
363
372
  return u
364
373
 
365
374
 
375
+ def archive(path, archive_path):
376
+ dbd = open(path)
377
+ dbd.archive(archive_path)
378
+ dbd.close()
379
+
380
+
381
+ def restore(archive_path, path):
382
+ _copy_and_extract_zips(archive_path, path)
383
+ dbd = open(path)
384
+ dbd.close()
366
385
 
367
386
 
387
+ def _copy_and_extract_zips(src_folder, dest_folder):
388
+ if not os.path.exists(dest_folder):
389
+ os.makedirs(dest_folder)
368
390
 
391
+ # First pass: count total files
392
+ total_files = sum(len(files) for _, _, files in os.walk(src_folder))
369
393
 
370
- if __name__=='__main__':
394
+ with tqdm(total=total_files, desc="Copying and extracting") as pbar:
395
+ for root, dirs, files in os.walk(src_folder):
396
+ rel_path = os.path.relpath(root, src_folder)
397
+ dest_path = os.path.join(dest_folder, rel_path)
398
+ os.makedirs(dest_path, exist_ok=True)
371
399
 
400
+ for file in files:
401
+ src_file_path = os.path.join(root, file)
402
+ dest_file_path = os.path.join(dest_path, file)
403
+
404
+ if file.lower().endswith('.zip'):
405
+ try:
406
+ zip_dest_folder = dest_file_path[:-4]
407
+ with zipfile.ZipFile(src_file_path, 'r') as zip_ref:
408
+ zip_ref.extractall(zip_dest_folder)
409
+ #tqdm.write(f"Extracted ZIP: {src_file_path}")
410
+ #_flatten_folder(zip_dest_folder) # still needed?
411
+ except zipfile.BadZipFile:
412
+ tqdm.write(f"Bad ZIP file skipped: {src_file_path}")
413
+ else:
414
+ shutil.copy2(src_file_path, dest_file_path)
415
+
416
+ pbar.update(1)
417
+
418
+
419
+ def _flatten_folder(root_folder):
420
+ for dirpath, dirnames, filenames in os.walk(root_folder, topdown=False):
421
+ for filename in filenames:
422
+ src_path = os.path.join(dirpath, filename)
423
+ dst_path = os.path.join(root_folder, filename)
424
+
425
+ # If file with same name exists, optionally rename or skip
426
+ if os.path.exists(dst_path):
427
+ base, ext = os.path.splitext(filename)
428
+ counter = 1
429
+ while os.path.exists(dst_path):
430
+ dst_path = os.path.join(root_folder, f"{base}_{counter}{ext}")
431
+ counter += 1
432
+
433
+ shutil.move(src_path, dst_path)
434
+
435
+ # Remove empty subdirectories (but skip the root folder)
436
+ if dirpath != root_folder:
437
+ try:
438
+ os.rmdir(dirpath)
439
+ except OSError:
440
+ print(f"Could not remove {dirpath} — not empty or in use.")
441
+
442
+
443
+
444
+ if __name__=='__main__':
372
445
  pass
@@ -344,10 +344,24 @@ def format_value(value, VR=None, tag=None):
344
344
  #return value[:64]
345
345
  if VR == 'TM':
346
346
  return variables.seconds_to_str(value)
347
+ if VR == 'DA':
348
+ if not is_valid_dicom_date(value):
349
+ return '99991231'
347
350
 
348
351
  return value
349
352
 
350
353
 
354
+
355
+ def is_valid_dicom_date(da_str: str) -> bool:
356
+ if not isinstance(da_str, str) or len(da_str) != 8 or not da_str.isdigit():
357
+ return False
358
+ try:
359
+ datetime.strptime(da_str, "%Y%m%d")
360
+ return True
361
+ except ValueError:
362
+ return False
363
+
364
+
351
365
  def check_value(value, tag):
352
366
 
353
367
  # If the change below is made (TM, DA, DT) then this needs to
@@ -2,6 +2,8 @@ import os
2
2
  from datetime import datetime
3
3
  import json
4
4
  from typing import Union
5
+ import zipfile
6
+ import re
5
7
 
6
8
  from tqdm import tqdm
7
9
  import numpy as np
@@ -650,37 +652,43 @@ class DataBaseDicom():
650
652
  self.delete(from_entity)
651
653
  return self
652
654
 
653
- def split_series(self, series:list, attr:Union[str, tuple]) -> dict:
655
+ def split_series(self, series:list, attr:Union[str, tuple], key=None) -> list:
654
656
  """
655
657
  Split a series into multiple series
656
658
 
657
659
  Args:
658
660
  series (list): series to split.
659
661
  attr (str or tuple): dicom attribute to split the series by.
662
+ key (function): split by by key(attr)
660
663
  Returns:
661
- dict: dictionary with keys the unique values found (ascending)
662
- and as values the series corresponding to that value.
664
+ list: list of two-element tuples, where the first element is
665
+ is the value and the second element is the series corresponding to that value.
663
666
  """
664
667
 
665
668
  # Find all values of the attr and list files per value
666
669
  all_files = register.files(self.register, series)
667
- files = {}
670
+ files = []
671
+ values = []
668
672
  for f in tqdm(all_files, desc=f'Reading {attr}'):
669
673
  ds = dbdataset.read_dataset(f)
670
674
  v = dbdataset.get_values(ds, attr)
671
- if v in files:
672
- files[v].append(f)
675
+ if key is not None:
676
+ v = key(v)
677
+ if v in values:
678
+ index = values.index(v)
679
+ files[index].append(f)
673
680
  else:
674
- files[v] = [f]
681
+ values.append(v)
682
+ files.append([f])
675
683
 
676
684
  # Copy the files for each value (sorted) to new series
677
- values = sorted(list(files.keys()))
678
- split_series = {}
679
- for v in tqdm(values, desc='Writing new series'):
685
+ split_series = []
686
+ for index, v in tqdm(enumerate(values), desc='Writing new series'):
680
687
  series_desc = series[-1] if isinstance(series, str) else series[-1][0]
681
- series_v = series[:3] + [f'{series_desc}_{attr}_{v}']
682
- self._files_to_series(files[v], series_v)
683
- split_series[v] = series_v
688
+ series_desc = clean_folder_name(f'{series_desc}_{attr}_{v}')
689
+ series_v = series[:3] + [(series_desc, 0)]
690
+ self._files_to_series(files[index], series_v)
691
+ split_series.append((v, series_v))
684
692
  return split_series
685
693
 
686
694
 
@@ -880,6 +888,57 @@ class DataBaseDicom():
880
888
  register.add_instance(self.register, attr, rel_path)
881
889
 
882
890
 
891
+ def archive(self, archive_path):
892
+ # TODO add flat=True option for zipping at patient level
893
+ for pt in tqdm(self.register, desc='Archiving '):
894
+ for st in pt['studies']:
895
+ zip_dir = os.path.join(
896
+ archive_path,
897
+ f"Patient__{pt['PatientID']}",
898
+ f"Study__{st['StudyID']}__{st['StudyDescription']}",
899
+ )
900
+ os.makedirs(zip_dir, exist_ok=True)
901
+ for sr in st['series']:
902
+ try:
903
+ zip_file = os.path.join(
904
+ zip_dir,
905
+ f"Series__{sr['SeriesNumber']}__{sr['SeriesDescription']}.zip",
906
+ )
907
+ with zipfile.ZipFile(zip_file, 'w') as zipf:
908
+ for rel_path in sr['instances'].values():
909
+ file = os.path.join(self.path, rel_path)
910
+ zipf.write(file, arcname=os.path.basename(file))
911
+ except Exception as e:
912
+ raise RuntimeError(
913
+ f"Error extracting series {sr['SeriesDescription']} "
914
+ f"in study {st['StudyDescription']} of patient {pt['PatientID']}."
915
+ )
916
+
917
+
918
+
919
+
920
+ def clean_folder_name(name, replacement="", max_length=255):
921
+ # Strip leading/trailing whitespace
922
+ name = name.strip()
923
+
924
+ # Replace invalid characters (Windows, macOS, Linux-safe)
925
+ illegal_chars = r'[<>:"/\\|?*\[\]\x00-\x1F\x7F]'
926
+ name = re.sub(illegal_chars, replacement, name)
927
+
928
+ # Replace reserved Windows names
929
+ reserved = {
930
+ "CON", "PRN", "AUX", "NUL",
931
+ *(f"COM{i}" for i in range(1, 10)),
932
+ *(f"LPT{i}" for i in range(1, 10))
933
+ }
934
+ name_upper = name.upper().split(".")[0] # Just base name
935
+ if name_upper in reserved:
936
+ name = f"{name}_folder"
937
+
938
+ # Truncate to max length (common max: 255 bytes)
939
+ return name[:max_length] or "folder"
940
+
941
+
883
942
 
884
943
  def infer_slice_spacing(vols):
885
944
  # In case spacing between slices is not (correctly) encoded in
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dbdicom
3
- Version: 0.3.6
3
+ Version: 0.3.8
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/
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
File without changes