dbdicom 0.3.11__tar.gz → 0.3.12__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.11/src/dbdicom.egg-info → dbdicom-0.3.12}/PKG-INFO +1 -1
- {dbdicom-0.3.11 → dbdicom-0.3.12}/pyproject.toml +1 -1
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/api.py +38 -22
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/dataset.py +17 -8
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/dbd.py +136 -110
- {dbdicom-0.3.11 → dbdicom-0.3.12/src/dbdicom.egg-info}/PKG-INFO +1 -1
- {dbdicom-0.3.11 → dbdicom-0.3.12}/tests/test_api.py +80 -1
- {dbdicom-0.3.11 → dbdicom-0.3.12}/LICENSE +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/MANIFEST.in +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/README.rst +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/setup.cfg +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/__init__.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/const.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/database.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/__init__.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/README.md +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/__init__.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/bin/__init__.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/bin/deidentify +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/bin/deidentify.bat +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/bin/emf2sf +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/bin/emf2sf.bat +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/etc/__init__.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/etc/emf2sf/__init__.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/etc/emf2sf/log4j.properties +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/__init__.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/commons-cli-1.4.jar +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/dcm4che-core-5.23.1.jar +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/dcm4che-emf-5.23.1.jar +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-common-5.23.1.jar +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-emf2sf-5.23.1.jar +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/log4j-1.2.17.jar +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/macosx-x86-64/libopencv_java.jnilib +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/slf4j-api-1.7.30.jar +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/slf4j-log4j12-1.7.30.jar +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio.dll +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_sse2.dll +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_util.dll +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/windows-x86-64/opencv_java.dll +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/register.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/sop_classes/ct_image.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/sop_classes/enhanced_mr_image.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/sop_classes/mr_image.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/sop_classes/parametric_map.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/sop_classes/secondary_capture.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/sop_classes/segmentation.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/sop_classes/ultrasound_multiframe_image.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/sop_classes/xray_angiographic_image.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/utils/arrays.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/utils/dcm4che.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/utils/files.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/utils/image.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/utils/pydicom_dataset.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom.egg-info/SOURCES.txt +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom.egg-info/dependency_links.txt +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom.egg-info/requires.txt +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom.egg-info/top_level.txt +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/tests/test_dcm4che.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/tests/test_sop_classes.py +0 -0
- {dbdicom-0.3.11 → dbdicom-0.3.12}/tests/test_utils.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.12
|
|
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/
|
|
@@ -240,6 +240,23 @@ def volume(series:list, dims:list=None, verbose=1) -> vreg.Volume3D:
|
|
|
240
240
|
return vol
|
|
241
241
|
|
|
242
242
|
|
|
243
|
+
def volumes_2d(series:list, dims:list=None, verbose=1) -> vreg.Volume3D:
|
|
244
|
+
"""Read 2D volumes from the series
|
|
245
|
+
|
|
246
|
+
Args:
|
|
247
|
+
entity (list, str): DICOM series to read
|
|
248
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
249
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
list of vreg.Volume3D
|
|
253
|
+
"""
|
|
254
|
+
dbd = open(series[0])
|
|
255
|
+
vol = dbd.volumes_2d(series, dims, verbose)
|
|
256
|
+
dbd.close()
|
|
257
|
+
return vol
|
|
258
|
+
|
|
259
|
+
|
|
243
260
|
def values(series:list, *attr, dims:list=None, verbose=1) -> Union[np.ndarray, list]:
|
|
244
261
|
"""Read the values of some attributes from a DICOM series
|
|
245
262
|
|
|
@@ -259,16 +276,23 @@ def values(series:list, *attr, dims:list=None, verbose=1) -> Union[np.ndarray, l
|
|
|
259
276
|
return values
|
|
260
277
|
|
|
261
278
|
|
|
262
|
-
|
|
279
|
+
|
|
280
|
+
def write_volume(vol:Union[vreg.Volume3D, tuple], series:list,
|
|
281
|
+
ref:list=None, append=False, verbose=1):
|
|
263
282
|
"""Write a vreg.Volume3D to a DICOM series
|
|
264
283
|
|
|
265
284
|
Args:
|
|
266
285
|
vol (vreg.Volume3D or tuple): Volume to write to the series.
|
|
267
286
|
series (list): DICOM series to read
|
|
268
287
|
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
288
|
+
append (bool): by default write_volume will only write to a new series,
|
|
289
|
+
and raise an error when attempting to write to an existing series.
|
|
290
|
+
To overrule this behaviour and add the volume to an existing series, set append to True.
|
|
291
|
+
Default is False.
|
|
292
|
+
verbose (bool): if set to 1, a progress bar is shown. verbose=0 does not show updates.
|
|
269
293
|
"""
|
|
270
294
|
dbd = open(series[0])
|
|
271
|
-
dbd.write_volume(vol, series, ref)
|
|
295
|
+
dbd.write_volume(vol, series, ref, append, verbose)
|
|
272
296
|
dbd.close()
|
|
273
297
|
|
|
274
298
|
|
|
@@ -337,39 +361,27 @@ def files(entity:list) -> list:
|
|
|
337
361
|
return files
|
|
338
362
|
|
|
339
363
|
|
|
340
|
-
def pixel_data(series:list, dims:list=None,
|
|
364
|
+
def pixel_data(series:list, dims:list=None, verbose=1) -> tuple:
|
|
341
365
|
"""Read the pixel data from a DICOM series
|
|
342
366
|
|
|
343
367
|
Args:
|
|
344
|
-
series (list): DICOM series to read
|
|
368
|
+
series (list or str): DICOM series to read. This can also
|
|
369
|
+
be a path to a folder containing DICOM files, or a
|
|
370
|
+
patient or study to read all series in that patient or
|
|
371
|
+
study. In those cases a list is returned.
|
|
345
372
|
dims (list, optional): Dimensions of the array.
|
|
346
|
-
coords (bool): If set to True, the coordinates of the
|
|
347
|
-
slices are returned alongside the pixel data.
|
|
348
|
-
attr (list, optional): list of DICOM attributes that are
|
|
349
|
-
read on the fly to avoid reading the data twice.
|
|
350
373
|
|
|
351
374
|
Returns:
|
|
352
|
-
tuple: numpy array with pixel values
|
|
353
|
-
|
|
354
|
-
is provide these are returned as a dictionary in a third
|
|
355
|
-
return value.
|
|
375
|
+
numpy.ndarray or tuple: numpy array with pixel values, with
|
|
376
|
+
at least 3 dimensions (x,y,z).
|
|
356
377
|
"""
|
|
357
378
|
if isinstance(series, str):
|
|
358
379
|
series = [series]
|
|
359
380
|
dbd = open(series[0])
|
|
360
|
-
array = dbd.pixel_data(series, dims,
|
|
381
|
+
array = dbd.pixel_data(series, dims, verbose)
|
|
361
382
|
dbd.close()
|
|
362
383
|
return array
|
|
363
384
|
|
|
364
|
-
# write_pixel_data()
|
|
365
|
-
# values()
|
|
366
|
-
# write_values()
|
|
367
|
-
# to_png(series, folder, dims)
|
|
368
|
-
# to_npy(series, folder, dims)
|
|
369
|
-
# split(series, attribute)
|
|
370
|
-
# extract(series, *kwargs) # subseries
|
|
371
|
-
|
|
372
|
-
# zeros(series, shape, dims)
|
|
373
385
|
|
|
374
386
|
def unique(pars:list, entity:list) -> dict:
|
|
375
387
|
"""Return a list of unique values for a DICOM entity
|
|
@@ -420,6 +432,8 @@ def _copy_and_extract_zips(src_folder, dest_folder):
|
|
|
420
432
|
if file.lower().endswith('.zip'):
|
|
421
433
|
try:
|
|
422
434
|
zip_dest_folder = dest_file_path[:-4]
|
|
435
|
+
if os.path.exists(zip_dest_folder):
|
|
436
|
+
continue
|
|
423
437
|
with zipfile.ZipFile(src_file_path, 'r') as zip_ref:
|
|
424
438
|
zip_ref.extractall(zip_dest_folder)
|
|
425
439
|
#tqdm.write(f"Extracted ZIP: {src_file_path}")
|
|
@@ -427,6 +441,8 @@ def _copy_and_extract_zips(src_folder, dest_folder):
|
|
|
427
441
|
except zipfile.BadZipFile:
|
|
428
442
|
tqdm.write(f"Bad ZIP file skipped: {src_file_path}")
|
|
429
443
|
else:
|
|
444
|
+
if os.path.exists(dest_file_path):
|
|
445
|
+
continue
|
|
430
446
|
shutil.copy2(src_file_path, dest_file_path)
|
|
431
447
|
|
|
432
448
|
pbar.update(1)
|
|
@@ -85,8 +85,8 @@ def write(ds, file, status=None):
|
|
|
85
85
|
dir = os.path.dirname(file)
|
|
86
86
|
if not os.path.exists(dir):
|
|
87
87
|
os.makedirs(dir)
|
|
88
|
-
|
|
89
|
-
ds.save_as(file, enforce_file_format=True)
|
|
88
|
+
ds.save_as(file, write_like_original=False) # deprecated
|
|
89
|
+
# ds.save_as(file, enforce_file_format=True)
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
def codify(source_file, save_file, **kwargs):
|
|
@@ -232,11 +232,20 @@ def set_lut(ds, RGB):
|
|
|
232
232
|
|
|
233
233
|
|
|
234
234
|
|
|
235
|
-
def affine(ds):
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
235
|
+
def affine(ds, multislice=False):
|
|
236
|
+
|
|
237
|
+
if multislice:
|
|
238
|
+
# For 2D scans the slice_spacing is the slice thickness
|
|
239
239
|
slice_spacing = ds.get("SliceThickness")
|
|
240
|
+
else:
|
|
241
|
+
# For 3D scans the slice spacing is the SpacingBetweenSlices
|
|
242
|
+
# Spacing Between Slices is not required so can be absent
|
|
243
|
+
# This is less critical because when reading a 3D volume the
|
|
244
|
+
# definitive slice_spacing is inferred from the slice positions.
|
|
245
|
+
slice_spacing = ds.get("SpacingBetweenSlices")
|
|
246
|
+
if slice_spacing is None:
|
|
247
|
+
slice_spacing = ds.get("SliceThickness")
|
|
248
|
+
|
|
240
249
|
return image.affine_matrix(
|
|
241
250
|
get_values(ds, 'ImageOrientationPatient'),
|
|
242
251
|
get_values(ds, 'ImagePositionPatient'),
|
|
@@ -339,8 +348,8 @@ def set_pixel_data(ds, array):
|
|
|
339
348
|
# ds.PixelData = array.tobytes()
|
|
340
349
|
|
|
341
350
|
|
|
342
|
-
def volume(ds):
|
|
343
|
-
return vreg.volume(pixel_data(ds), affine(ds))
|
|
351
|
+
def volume(ds, multislice=False):
|
|
352
|
+
return vreg.volume(pixel_data(ds), affine(ds, multislice=multislice))
|
|
344
353
|
|
|
345
354
|
|
|
346
355
|
|
|
@@ -292,6 +292,8 @@ class DataBaseDicom():
|
|
|
292
292
|
v.affine[:3,2] = -v.affine[:3,2]
|
|
293
293
|
# Then try again
|
|
294
294
|
vol = vreg.join(vols)
|
|
295
|
+
|
|
296
|
+
# For multi-dimensional volumes, set dimensions and coordinates
|
|
295
297
|
if vol.ndim > 3:
|
|
296
298
|
# Coordinates of slice 0
|
|
297
299
|
c0 = [c[0,...] for c in coords[1:]]
|
|
@@ -300,6 +302,128 @@ class DataBaseDicom():
|
|
|
300
302
|
return vol
|
|
301
303
|
|
|
302
304
|
|
|
305
|
+
def volumes_2d(self, entity:Union[list, str], dims:list=None, verbose=1) -> list:
|
|
306
|
+
"""Read 2D volumes from the series
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
entity (list, str): DICOM series to read
|
|
310
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
311
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
list of vreg.Volume3D
|
|
315
|
+
"""
|
|
316
|
+
# if isinstance(entity, str): # path to folder
|
|
317
|
+
# return [self.volume(s, dims) for s in self.series(entity)]
|
|
318
|
+
# if len(entity) < 4: # folder, patient or study
|
|
319
|
+
# return [self.volume(s, dims) for s in self.series(entity)]
|
|
320
|
+
|
|
321
|
+
if dims is None:
|
|
322
|
+
dims = []
|
|
323
|
+
elif isinstance(dims, str):
|
|
324
|
+
dims = [dims]
|
|
325
|
+
else:
|
|
326
|
+
dims = list(dims)
|
|
327
|
+
dims = ['SliceLocation'] + dims
|
|
328
|
+
|
|
329
|
+
# Read dicom files
|
|
330
|
+
values = {}
|
|
331
|
+
volumes = {}
|
|
332
|
+
|
|
333
|
+
files = register.files(self.register, entity)
|
|
334
|
+
for f in tqdm(files, desc='Reading volume..', disable=(verbose==0)):
|
|
335
|
+
ds = pydicom.dcmread(f)
|
|
336
|
+
values_f = get_values(ds, dims)
|
|
337
|
+
vol = dbdataset.volume(ds, multislice=True)
|
|
338
|
+
slice_loc = values_f[0]
|
|
339
|
+
if slice_loc in volumes:
|
|
340
|
+
volumes[slice_loc].append(vol)
|
|
341
|
+
for d in range(len(dims)):
|
|
342
|
+
values[slice_loc][d].append(values_f[d])
|
|
343
|
+
else:
|
|
344
|
+
volumes[slice_loc] = [vol]
|
|
345
|
+
values[slice_loc] = [[values_f[d]] for d in range(len(dims))]
|
|
346
|
+
|
|
347
|
+
# Build a volume for each slice location
|
|
348
|
+
volumes_2d = []
|
|
349
|
+
for slice_loc in volumes.keys():
|
|
350
|
+
vols_list = volumes[slice_loc]
|
|
351
|
+
|
|
352
|
+
if values == {}:
|
|
353
|
+
if len(vols_list) > 1:
|
|
354
|
+
raise ValueError(
|
|
355
|
+
"Cannot return a 2D volume - multiple slices at the same "
|
|
356
|
+
"location. \n Use InstanceNumber or another suitable DICOM "
|
|
357
|
+
"attribute as dimension to sort them.")
|
|
358
|
+
volumes_2d.append(vols_list[0])
|
|
359
|
+
continue
|
|
360
|
+
|
|
361
|
+
# Sort by coordinata values
|
|
362
|
+
vals_list = values[slice_loc]
|
|
363
|
+
|
|
364
|
+
# Format coordinates as mesh
|
|
365
|
+
coords = [np.array(v) for v in vals_list]
|
|
366
|
+
coords, inds = dbdicom.utils.arrays.meshvals(coords)
|
|
367
|
+
|
|
368
|
+
# Check that all slices have the same coordinates
|
|
369
|
+
if len(dims) > 1:
|
|
370
|
+
# Loop over all coordinates after slice location
|
|
371
|
+
for c in coords[1:]:
|
|
372
|
+
# Loop over all slice locations
|
|
373
|
+
for k in range(1, c.shape[0]):
|
|
374
|
+
# Coordinate c of slice k
|
|
375
|
+
if not np.array_equal(c[k,...], c[0,...]):
|
|
376
|
+
raise ValueError(
|
|
377
|
+
"Cannot build a single volume. Not all slices "
|
|
378
|
+
"have the same coordinates."
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
# Build volumes, sort and reshape along the coordinates
|
|
382
|
+
vols = np.array(vols_list)
|
|
383
|
+
vols = vols[inds].reshape(coords[0].shape)
|
|
384
|
+
|
|
385
|
+
# Join 2D volumes along the extra dimensions
|
|
386
|
+
vol = vreg.join(vols[0,...].reshape((1,) + vols.shape[1:]))
|
|
387
|
+
|
|
388
|
+
# For multi-dimensional volumes, set dimensions and coordinates
|
|
389
|
+
if vol.ndim > 3:
|
|
390
|
+
# Coordinates of slice 0
|
|
391
|
+
c0 = [c[0,...] for c in coords[1:]]
|
|
392
|
+
vol.set_coords(c0)
|
|
393
|
+
vol.set_dims(dims[1:])
|
|
394
|
+
|
|
395
|
+
volumes_2d.append(vol)
|
|
396
|
+
|
|
397
|
+
return volumes_2d
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def pixel_data(self, series:list, dims:list=None, verbose=1) -> np.ndarray:
|
|
401
|
+
"""Read the pixel data from a DICOM series
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
series (list or str): DICOM series to read. This can also
|
|
405
|
+
be a path to a folder containing DICOM files, or a
|
|
406
|
+
patient or study to read all series in that patient or
|
|
407
|
+
study. In those cases a list is returned.
|
|
408
|
+
dims (list, optional): Dimensions of the array.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
numpy.ndarray or tuple: numpy array with pixel values, with
|
|
412
|
+
at least 3 dimensions (x,y,z).
|
|
413
|
+
"""
|
|
414
|
+
vols = self.volumes_2d(series, dims, verbose)
|
|
415
|
+
for v in vols[1:]:
|
|
416
|
+
if v.shape != vols[0].shape:
|
|
417
|
+
raise ValueError(
|
|
418
|
+
"Cannot return a pixel array because slices have different shapes." \
|
|
419
|
+
"Instead try using volumes_2d to return a list of 2D volumes."
|
|
420
|
+
)
|
|
421
|
+
slices = [v.values for v in vols]
|
|
422
|
+
pixel_array = np.concatenate(slices, axis=2)
|
|
423
|
+
return pixel_array
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
|
|
303
427
|
def values(self, series:list, *attr, dims:list=None, verbose=1) -> Union[dict, tuple]:
|
|
304
428
|
"""Read the values of some attributes from a DICOM series
|
|
305
429
|
|
|
@@ -355,7 +479,7 @@ class DataBaseDicom():
|
|
|
355
479
|
|
|
356
480
|
def write_volume(
|
|
357
481
|
self, vol:Union[vreg.Volume3D, tuple], series:list,
|
|
358
|
-
ref:list=None,
|
|
482
|
+
ref:list=None, append=False, verbose=1,
|
|
359
483
|
):
|
|
360
484
|
"""Write a vreg.Volume3D to a DICOM series
|
|
361
485
|
|
|
@@ -363,10 +487,16 @@ class DataBaseDicom():
|
|
|
363
487
|
vol (vreg.Volume3D): Volume to write to the series.
|
|
364
488
|
series (list): DICOM series to read
|
|
365
489
|
ref (list): Reference series
|
|
490
|
+
append (bool): by default write_volume will only write to a new series,
|
|
491
|
+
and raise an error when attempting to write to an existing series.
|
|
492
|
+
To overrule this behaviour and add the volume to an existing series, set append to True.
|
|
493
|
+
Default is False.
|
|
494
|
+
verbose (bool): if set to 1, a progress bar is shown
|
|
366
495
|
"""
|
|
367
496
|
series_full_name = full_name(series)
|
|
368
497
|
if series_full_name in self.series():
|
|
369
|
-
|
|
498
|
+
if not append:
|
|
499
|
+
raise ValueError(f"Series {series_full_name[-1]} already exists in study {series_full_name[-2]}.")
|
|
370
500
|
|
|
371
501
|
if isinstance(vol, tuple):
|
|
372
502
|
vol = vreg.volume(vol[0], vol[1])
|
|
@@ -388,13 +518,13 @@ class DataBaseDicom():
|
|
|
388
518
|
|
|
389
519
|
if vol.ndim==3:
|
|
390
520
|
slices = vol.split()
|
|
391
|
-
for i, sl in tqdm(enumerate(slices), desc='Writing volume..'):
|
|
521
|
+
for i, sl in tqdm(enumerate(slices), desc='Writing volume..', disable=verbose==0):
|
|
392
522
|
dbdataset.set_volume(ds, sl)
|
|
393
523
|
self._write_dataset(ds, attr, n + 1 + i)
|
|
394
524
|
else:
|
|
395
525
|
i=0
|
|
396
526
|
vols = vol.separate().reshape(-1)
|
|
397
|
-
for vt in tqdm(vols, desc='Writing volume..'):
|
|
527
|
+
for vt in tqdm(vols, desc='Writing volume..', disable=verbose==0):
|
|
398
528
|
slices = vt.split()
|
|
399
529
|
for sl in slices:
|
|
400
530
|
dbdataset.set_volume(ds, sl)
|
|
@@ -506,108 +636,7 @@ class DataBaseDicom():
|
|
|
506
636
|
self.write_volume(vol, series, ref)
|
|
507
637
|
return self
|
|
508
638
|
|
|
509
|
-
def pixel_data(self, series:list, dims:list=None, coords=False, attr=None) -> np.ndarray:
|
|
510
|
-
"""Read the pixel data from a DICOM series
|
|
511
|
-
|
|
512
|
-
Args:
|
|
513
|
-
series (list or str): DICOM series to read. This can also
|
|
514
|
-
be a path to a folder containing DICOM files, or a
|
|
515
|
-
patient or study to read all series in that patient or
|
|
516
|
-
study. In those cases a list is returned.
|
|
517
|
-
dims (list, optional): Dimensions of the array.
|
|
518
|
-
coords (bool): If set to True, the coordinates of the
|
|
519
|
-
arrays are returned alongside the pixel data
|
|
520
|
-
attr (list, optional): list of DICOM attributes that are
|
|
521
|
-
read on the fly to avoid reading the data twice.
|
|
522
|
-
|
|
523
|
-
Returns:
|
|
524
|
-
numpy.ndarray or tuple: numpy array with pixel values, with
|
|
525
|
-
at least 3 dimensions (x,y,z). If
|
|
526
|
-
coords is set these are returned too as an array with
|
|
527
|
-
coordinates of the slices according to dims. If include
|
|
528
|
-
is provided the values are returned as a dictionary in the last
|
|
529
|
-
return value.
|
|
530
|
-
"""
|
|
531
|
-
if isinstance(series, str): # path to folder
|
|
532
|
-
return [self.pixel_data(s, dims, coords, attr) for s in self.series(series)]
|
|
533
|
-
if len(series) < 4: # folder, patient or study
|
|
534
|
-
return [self.pixel_data(s, dims, coords, attr) for s in self.series(series)]
|
|
535
639
|
|
|
536
|
-
if dims is None:
|
|
537
|
-
dims = ['InstanceNumber']
|
|
538
|
-
elif np.isscalar(dims):
|
|
539
|
-
dims = [dims]
|
|
540
|
-
else:
|
|
541
|
-
dims = list(dims)
|
|
542
|
-
|
|
543
|
-
# Ensure return_vals is a list
|
|
544
|
-
if attr is None:
|
|
545
|
-
params = []
|
|
546
|
-
elif np.isscalar(attr):
|
|
547
|
-
params = [attr]
|
|
548
|
-
else:
|
|
549
|
-
params = list(attr)
|
|
550
|
-
|
|
551
|
-
files = register.files(self.register, series)
|
|
552
|
-
|
|
553
|
-
# Read dicom files
|
|
554
|
-
coords_array = []
|
|
555
|
-
arrays = np.empty(len(files), dtype=dict)
|
|
556
|
-
if attr is not None:
|
|
557
|
-
values = np.empty(len(files), dtype=dict)
|
|
558
|
-
for i, f in tqdm(enumerate(files), desc='Reading pixel data..'):
|
|
559
|
-
ds = pydicom.dcmread(f)
|
|
560
|
-
coords_array.append(get_values(ds, dims))
|
|
561
|
-
# save as dict so numpy does not stack as arrays
|
|
562
|
-
arrays[i] = {'pixel_data': dbdataset.pixel_data(ds)}
|
|
563
|
-
if attr is not None:
|
|
564
|
-
values[i] = {'values': get_values(ds, params)}
|
|
565
|
-
|
|
566
|
-
# Format as mesh
|
|
567
|
-
coords_array = np.stack([v for v in coords_array], axis=-1)
|
|
568
|
-
coords_array, inds = dbdicom.utils.arrays.meshvals(coords_array)
|
|
569
|
-
|
|
570
|
-
arrays = arrays[inds].reshape(coords_array.shape[1:])
|
|
571
|
-
arrays = np.stack([a['pixel_data'] for a in arrays.reshape(-1)], axis=-1)
|
|
572
|
-
arrays = arrays.reshape(arrays.shape[:2] + coords_array.shape[1:])
|
|
573
|
-
|
|
574
|
-
if attr is None:
|
|
575
|
-
if coords:
|
|
576
|
-
return arrays, coords_array
|
|
577
|
-
else:
|
|
578
|
-
return arrays
|
|
579
|
-
|
|
580
|
-
# Return values as a dictionary
|
|
581
|
-
values = values[inds].reshape(-1)
|
|
582
|
-
values_dict = {}
|
|
583
|
-
for p in range(len(params)):
|
|
584
|
-
# Get the type from the first value
|
|
585
|
-
vp0 = values[0]['values'][p]
|
|
586
|
-
# Build an array of the right type
|
|
587
|
-
vp = np.zeros(values.size, dtype=type(vp0))
|
|
588
|
-
# Populate the array with values for parameter p
|
|
589
|
-
for i, v in enumerate(values):
|
|
590
|
-
vp[i] = v['values'][p]
|
|
591
|
-
# Reshape values for parameter p
|
|
592
|
-
vp = vp.reshape(coords_array.shape[1:])
|
|
593
|
-
# Eneter in the dictionary
|
|
594
|
-
values_dict[params[p]] = vp
|
|
595
|
-
|
|
596
|
-
# If only one, return as value
|
|
597
|
-
if len(params) == 1:
|
|
598
|
-
values_return = values_dict[attr[0]]
|
|
599
|
-
else:
|
|
600
|
-
values_return = values_dict
|
|
601
|
-
|
|
602
|
-
# problem if the values are a list. Needs an array with a prespeficied dtype
|
|
603
|
-
# values = values[inds].reshape(coords_array.shape[1:])
|
|
604
|
-
# values = np.stack([a['values'] for a in values.reshape(-1)], axis=-1)
|
|
605
|
-
# values = values.reshape((len(params), ) + coords_array.shape[1:])
|
|
606
|
-
|
|
607
|
-
if coords:
|
|
608
|
-
return arrays, coords_array, values_return
|
|
609
|
-
else:
|
|
610
|
-
return arrays, values_return
|
|
611
640
|
|
|
612
641
|
|
|
613
642
|
|
|
@@ -1120,7 +1149,7 @@ def infer_slice_spacing(vols):
|
|
|
1120
1149
|
distances = np.around(distances, 2)
|
|
1121
1150
|
slice_spacing_d = np.unique(distances)
|
|
1122
1151
|
|
|
1123
|
-
# Check if unique - otherwise this is not a volume
|
|
1152
|
+
# Check if slice spacings are unique - otherwise this is not a volume
|
|
1124
1153
|
if len(slice_spacing_d) > 1:
|
|
1125
1154
|
raise ValueError(
|
|
1126
1155
|
'Cannot build a volume - spacings between slices are not unique.'
|
|
@@ -1135,6 +1164,7 @@ def infer_slice_spacing(vols):
|
|
|
1135
1164
|
slice_spacing[d] = slice_spacing_d
|
|
1136
1165
|
|
|
1137
1166
|
# Check slice_spacing is the same across dimensions
|
|
1167
|
+
# Not sure if this is possible as volumes are sorted by slice location
|
|
1138
1168
|
slice_spacing = np.unique(slice_spacing)
|
|
1139
1169
|
if len(slice_spacing) > 1:
|
|
1140
1170
|
raise ValueError(
|
|
@@ -1143,7 +1173,3 @@ def infer_slice_spacing(vols):
|
|
|
1143
1173
|
|
|
1144
1174
|
return vols.reshape(shape)
|
|
1145
1175
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbdicom
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.12
|
|
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/
|
|
@@ -11,6 +11,8 @@ shutil.rmtree(tmp)
|
|
|
11
11
|
os.makedirs(tmp, exist_ok=True)
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
|
|
15
|
+
|
|
14
16
|
def test_write_volume():
|
|
15
17
|
|
|
16
18
|
values = 100*np.random.rand(128, 192, 20).astype(np.float32)
|
|
@@ -24,11 +26,84 @@ def test_write_volume():
|
|
|
24
26
|
series = [tmp, '007', 'dbdicom_test', 'dixon']
|
|
25
27
|
db.write_volume(vol, series)
|
|
26
28
|
|
|
29
|
+
# Writing to an existing series returns an error by default
|
|
30
|
+
try:
|
|
31
|
+
db.write_volume(vol, series)
|
|
32
|
+
except:
|
|
33
|
+
assert True
|
|
34
|
+
else:
|
|
35
|
+
assert False
|
|
36
|
+
|
|
37
|
+
# Translate the volume in the z-direction over 10mm and append to the series
|
|
38
|
+
# This creates a series with two volumes separated by a gap of 5 mm
|
|
39
|
+
vol2 = vol.translate([0,0,20], coords='volume')
|
|
40
|
+
db.write_volume(vol2, series, append=True)
|
|
41
|
+
|
|
42
|
+
# Reading now throws an error as there are multiple volumes in the series
|
|
43
|
+
try:
|
|
44
|
+
db.volume(series, dims=['ImageType'])
|
|
45
|
+
except:
|
|
46
|
+
assert True
|
|
47
|
+
else:
|
|
48
|
+
assert False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
shutil.rmtree(tmp)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_volumes_2d():
|
|
55
|
+
|
|
56
|
+
# Write one volume
|
|
57
|
+
values = 100*np.random.rand(128, 192, 5).astype(np.float32)
|
|
58
|
+
vol = vreg.volume(values)
|
|
59
|
+
series = [tmp, '007', 'dbdicom_test', 'ax']
|
|
60
|
+
db.write_volume(vol, series)
|
|
61
|
+
|
|
62
|
+
# Shift it up to leave a gap and write to the same series
|
|
63
|
+
vol2 = vol.translate([0,0,10], coords='volume')
|
|
64
|
+
db.write_volume(vol2, series, append=True)
|
|
65
|
+
|
|
66
|
+
# Trying to read as a single volume throws an error because of the gap
|
|
67
|
+
try:
|
|
68
|
+
db.volume(series)
|
|
69
|
+
except:
|
|
70
|
+
assert True
|
|
71
|
+
else:
|
|
72
|
+
assert False
|
|
73
|
+
|
|
74
|
+
# But we can read them as 2D volumes, returning 10 2D volumes
|
|
75
|
+
vols = db.volumes_2d(series)
|
|
76
|
+
assert len(vols) == 10
|
|
77
|
+
|
|
78
|
+
# Now 4D
|
|
79
|
+
values = np.zeros((256, 256, 5, 2))
|
|
80
|
+
affine = np.eye(4)
|
|
81
|
+
vol = vreg.volume(values, affine, coords=(['INPHASE', 'OUTPHASE'], ), dims=['ImageType'])
|
|
82
|
+
series = [tmp, '007', 'dbdicom_test', 'dixon']
|
|
83
|
+
db.write_volume(vol, series)
|
|
84
|
+
|
|
85
|
+
vol2 = vol.translate([0,0,10], coords='volume')
|
|
86
|
+
db.write_volume(vol2, series, append=True)
|
|
87
|
+
|
|
88
|
+
vols = db.volumes_2d(series, dims=['ImageType'])
|
|
89
|
+
assert len(vols) == 10
|
|
90
|
+
assert vols[-1].shape == (256, 256, 1, 2)
|
|
91
|
+
|
|
27
92
|
shutil.rmtree(tmp)
|
|
28
93
|
|
|
29
94
|
|
|
30
95
|
def test_volume():
|
|
31
96
|
|
|
97
|
+
# One slice
|
|
98
|
+
values = 100*np.random.rand(128, 192, 1).astype(np.float32)
|
|
99
|
+
vol = vreg.volume(values)
|
|
100
|
+
series = [tmp, '007', 'test', 'slice']
|
|
101
|
+
db.write_volume(vol, series)
|
|
102
|
+
vol2 = db.volume(series)
|
|
103
|
+
assert np.linalg.norm(vol2.values-vol.values) < 0.0001*np.linalg.norm(vol.values)
|
|
104
|
+
assert np.linalg.norm(vol2.affine-vol.affine) == 0
|
|
105
|
+
|
|
106
|
+
# 3D volume
|
|
32
107
|
values = 100*np.random.rand(128, 192, 20).astype(np.float32)
|
|
33
108
|
vol = vreg.volume(values)
|
|
34
109
|
series = [tmp, '007', 'test', 'ax']
|
|
@@ -37,6 +112,7 @@ def test_volume():
|
|
|
37
112
|
assert np.linalg.norm(vol2.values-vol.values) < 0.0001*np.linalg.norm(vol.values)
|
|
38
113
|
assert np.linalg.norm(vol2.affine-vol.affine) == 0
|
|
39
114
|
|
|
115
|
+
# 4D volume
|
|
40
116
|
values = 100*np.random.rand(256, 256, 3, 2).astype(np.float32)
|
|
41
117
|
vol = vreg.volume(values, dims=['ImageType'], coords=(['INPHASE', 'OUTPHASE'], ), orient='coronal')
|
|
42
118
|
series = [tmp, '007', 'dbdicom_test', 'dixon']
|
|
@@ -120,6 +196,8 @@ def test_edit():
|
|
|
120
196
|
assert np.array_equal(tr, new_tr)
|
|
121
197
|
assert np.array_equal(pn, new_pn)
|
|
122
198
|
|
|
199
|
+
shutil.rmtree(tmp)
|
|
200
|
+
|
|
123
201
|
|
|
124
202
|
def test_write_database():
|
|
125
203
|
values = 100*np.random.rand(16, 16, 4).astype(np.float32)
|
|
@@ -215,9 +293,10 @@ def test_copy():
|
|
|
215
293
|
|
|
216
294
|
if __name__ == '__main__':
|
|
217
295
|
|
|
296
|
+
test_write_volume()
|
|
297
|
+
test_volumes_2d()
|
|
218
298
|
test_values()
|
|
219
299
|
test_edit()
|
|
220
|
-
test_write_volume()
|
|
221
300
|
test_volume()
|
|
222
301
|
test_write_database()
|
|
223
302
|
test_copy()
|
|
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.11 → dbdicom-0.3.12}/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.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-common-5.23.1.jar
RENAMED
|
File without changes
|
{dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/dcm4che-tool-emf2sf-5.23.1.jar
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio.dll
RENAMED
|
File without changes
|
{dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_sse2.dll
RENAMED
|
File without changes
|
{dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_util.dll
RENAMED
|
File without changes
|
{dbdicom-0.3.11 → dbdicom-0.3.12}/src/dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll
RENAMED
|
File without changes
|
{dbdicom-0.3.11 → dbdicom-0.3.12}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|