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.
- {dbdicom-0.3.5/src/dbdicom.egg-info → dbdicom-0.3.7}/PKG-INFO +1 -1
- {dbdicom-0.3.5 → dbdicom-0.3.7}/pyproject.toml +1 -1
- dbdicom-0.3.7/src/dbdicom/__init__.py +4 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/api.py +128 -4
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/dataset.py +40 -14
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/dbd.py +210 -35
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/mr_image.py +5 -3
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/arrays.py +6 -2
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/image.py +4 -9
- {dbdicom-0.3.5 → dbdicom-0.3.7/src/dbdicom.egg-info}/PKG-INFO +1 -1
- dbdicom-0.3.5/src/dbdicom/__init__.py +0 -1
- {dbdicom-0.3.5 → dbdicom-0.3.7}/LICENSE +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/MANIFEST.in +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/README.rst +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/setup.cfg +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/const.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/database.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/__init__.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/README.md +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/__init__.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/__init__.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/deidentify +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/deidentify.bat +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/emf2sf +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/bin/emf2sf.bat +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/etc/__init__.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/etc/emf2sf/__init__.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/etc/emf2sf/log4j.properties +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/__init__.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/commons-cli-1.4.jar +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-core-5.23.1.jar +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-emf-5.23.1.jar +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-common-5.23.1.jar +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-emf2sf-5.23.1.jar +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/log4j-1.2.17.jar +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/macosx-x86-64/libopencv_java.jnilib +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/slf4j-api-1.7.30.jar +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/slf4j-log4j12-1.7.30.jar +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio.dll +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_sse2.dll +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_util.dll +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86-64/opencv_java.dll +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/register.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/ct_image.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/enhanced_mr_image.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/parametric_map.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/secondary_capture.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/segmentation.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/ultrasound_multiframe_image.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/sop_classes/xray_angiographic_image.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/dcm4che.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/files.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/utils/variables.py +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom.egg-info/SOURCES.txt +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom.egg-info/dependency_links.txt +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom.egg-info/requires.txt +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom.egg-info/top_level.txt +0 -0
- {dbdicom-0.3.5 → dbdicom-0.3.7}/tests/test_api.py +0 -0
- {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.
|
|
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,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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
|
|
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.
|
|
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,
|
|
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
|
|
351
|
+
coords (bool): If set to True, the coordinates of the
|
|
348
352
|
arrays are returned alongside the pixel data
|
|
349
|
-
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
377
|
+
if attr is None:
|
|
380
378
|
params = []
|
|
381
|
-
elif np.isscalar(
|
|
382
|
-
params = [
|
|
379
|
+
elif np.isscalar(attr):
|
|
380
|
+
params = [attr]
|
|
383
381
|
else:
|
|
384
|
-
params = list(
|
|
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
|
|
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
|
|
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
|
|
407
|
+
if attr is None:
|
|
410
408
|
if coords:
|
|
411
|
-
return arrays, coords_array
|
|
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
|
|
416
|
-
values =
|
|
417
|
-
values = values.reshape(
|
|
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
|
|
441
|
+
return arrays, coords_array, values_return
|
|
421
442
|
else:
|
|
422
|
-
return arrays,
|
|
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"
|
|
727
|
-
f"
|
|
728
|
-
f"
|
|
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,
|
|
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
|
-
'
|
|
33
|
-
'
|
|
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
|
-
|
|
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 *
|
|
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
|
|
95
|
-
array
|
|
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.
|
|
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
|
|
File without changes
|
{dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-common-5.23.1.jar
RENAMED
|
File without changes
|
{dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-emf2sf-5.23.1.jar
RENAMED
|
File without changes
|
|
File without changes
|
{dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/macosx-x86-64/libopencv_java.jnilib
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_sse2.dll
RENAMED
|
File without changes
|
{dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_util.dll
RENAMED
|
File without changes
|
{dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll
RENAMED
|
File without changes
|
{dbdicom-0.3.5 → dbdicom-0.3.7}/src/dbdicom/external/dcm4che/lib/windows-x86-64/opencv_java.dll
RENAMED
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|