dbdicom 0.3.9__py3-none-any.whl → 0.3.10__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dbdicom might be problematic. Click here for more details.
- dbdicom/api.py +56 -44
- dbdicom/dataset.py +2 -2
- dbdicom/dbd.py +259 -133
- dbdicom/register.py +21 -0
- dbdicom/sop_classes/enhanced_mr_image.py +1 -1
- dbdicom/utils/arrays.py +124 -36
- {dbdicom-0.3.9.dist-info → dbdicom-0.3.10.dist-info}/METADATA +1 -1
- {dbdicom-0.3.9.dist-info → dbdicom-0.3.10.dist-info}/RECORD +11 -11
- {dbdicom-0.3.9.dist-info → dbdicom-0.3.10.dist-info}/WHEEL +0 -0
- {dbdicom-0.3.9.dist-info → dbdicom-0.3.10.dist-info}/licenses/LICENSE +0 -0
- {dbdicom-0.3.9.dist-info → dbdicom-0.3.10.dist-info}/top_level.txt +0 -0
dbdicom/api.py
CHANGED
|
@@ -4,8 +4,7 @@ import zipfile
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
from typing import Union
|
|
6
6
|
from tqdm import tqdm
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import numpy as np
|
|
9
8
|
import vreg
|
|
10
9
|
|
|
11
10
|
from dbdicom.dbd import DataBaseDicom
|
|
@@ -164,16 +163,22 @@ def series(entity:str | list, desc:str=None, contains:str=None, isin:list=None)-
|
|
|
164
163
|
"To retrieve a series, the entity must be a database, patient or study."
|
|
165
164
|
)
|
|
166
165
|
|
|
167
|
-
def copy(from_entity:list, to_entity
|
|
168
|
-
"""Copy a DICOM
|
|
166
|
+
def copy(from_entity:list, to_entity=None):
|
|
167
|
+
"""Copy a DICOM entity (patient, study or series)
|
|
169
168
|
|
|
170
169
|
Args:
|
|
171
170
|
from_entity (list): entity to copy
|
|
172
|
-
to_entity (list): entity after copying.
|
|
171
|
+
to_entity (list, optional): entity after copying. If this is not
|
|
172
|
+
provided, a copy will be made in the same study and returned.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
entity: the copied entity. If th to_entity is provided, this is
|
|
176
|
+
returned.
|
|
173
177
|
"""
|
|
174
178
|
dbd = open(from_entity[0])
|
|
175
|
-
dbd.copy(from_entity, to_entity)
|
|
179
|
+
from_entity_copy = dbd.copy(from_entity, to_entity)
|
|
176
180
|
dbd.close()
|
|
181
|
+
return from_entity_copy
|
|
177
182
|
|
|
178
183
|
|
|
179
184
|
def delete(entity:list):
|
|
@@ -216,25 +221,42 @@ def split_series(series:list, attr:Union[str, tuple], key=None)->list:
|
|
|
216
221
|
return split_series
|
|
217
222
|
|
|
218
223
|
|
|
219
|
-
def volume(
|
|
220
|
-
"""Read volume
|
|
224
|
+
def volume(series:list, dims:list=None, verbose=1) -> vreg.Volume3D:
|
|
225
|
+
"""Read volume from a series.
|
|
221
226
|
|
|
222
227
|
Args:
|
|
223
|
-
|
|
228
|
+
series (list, str): DICOM entity to read
|
|
224
229
|
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
225
230
|
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
226
231
|
|
|
227
232
|
Returns:
|
|
228
|
-
vreg.Volume3D
|
|
229
|
-
a volume, else a list of volumes.
|
|
233
|
+
vreg.Volume3D.
|
|
230
234
|
"""
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
dbd = open(entity[0])
|
|
234
|
-
vol = dbd.volume(entity, dims, verbose)
|
|
235
|
+
dbd = open(series[0])
|
|
236
|
+
vol = dbd.volume(series, dims, verbose)
|
|
235
237
|
dbd.close()
|
|
236
238
|
return vol
|
|
237
239
|
|
|
240
|
+
|
|
241
|
+
def values(series:list, *attr, dims:list=None, verbose=1) -> Union[np.ndarray, list]:
|
|
242
|
+
"""Read the values of some attributes from a DICOM series
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
series (list): DICOM series to read.
|
|
246
|
+
attr (tuple, optional): DICOM attributes to read.
|
|
247
|
+
dims (list, optional): Dimensions to sort the values.
|
|
248
|
+
If dims is not provided, values are sorted by
|
|
249
|
+
InstanceNumber.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
tuple: arrays with values for the attributes.
|
|
253
|
+
"""
|
|
254
|
+
dbd = open(series[0])
|
|
255
|
+
values = dbd.values(series, *attr, dims=dims, verbose=verbose)
|
|
256
|
+
dbd.close()
|
|
257
|
+
return values
|
|
258
|
+
|
|
259
|
+
|
|
238
260
|
def write_volume(vol:Union[vreg.Volume3D, tuple], series:list, ref:list=None):
|
|
239
261
|
"""Write a vreg.Volume3D to a DICOM series
|
|
240
262
|
|
|
@@ -247,6 +269,25 @@ def write_volume(vol:Union[vreg.Volume3D, tuple], series:list, ref:list=None):
|
|
|
247
269
|
dbd.write_volume(vol, series, ref)
|
|
248
270
|
dbd.close()
|
|
249
271
|
|
|
272
|
+
|
|
273
|
+
def edit(series:list, new_values:dict, dims:list=None, verbose=1):
|
|
274
|
+
"""Edit attribute values in a DICOM series
|
|
275
|
+
|
|
276
|
+
Warning: this function edits all values as requested. Please take care
|
|
277
|
+
when editing attributes that affect the DICOM file organisation, such as
|
|
278
|
+
UIDs, as this could corrupt the database.
|
|
279
|
+
|
|
280
|
+
Args:
|
|
281
|
+
series (list): DICOM series to edit
|
|
282
|
+
new_values (dict): dictionary with attribute: value pairs to write to the series
|
|
283
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
284
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
285
|
+
|
|
286
|
+
"""
|
|
287
|
+
dbd = open(series[0])
|
|
288
|
+
dbd.edit(series, new_values, dims=dims, verbose=verbose)
|
|
289
|
+
dbd.close()
|
|
290
|
+
|
|
250
291
|
def to_nifti(series:list, file:str, dims:list=None, verbose=1):
|
|
251
292
|
"""Save a DICOM series in nifti format.
|
|
252
293
|
|
|
@@ -274,35 +315,6 @@ def from_nifti(file:str, series:list, ref:list=None):
|
|
|
274
315
|
dbd.close()
|
|
275
316
|
|
|
276
317
|
|
|
277
|
-
def values(series:list, attr=None, dims:list=None, coords=False) -> Union[dict, tuple]:
|
|
278
|
-
"""Read the values of some or all attributes from a DICOM series
|
|
279
|
-
|
|
280
|
-
Args:
|
|
281
|
-
series (list or str): DICOM series to read. This can also
|
|
282
|
-
be a path to a folder containing DICOM files, or a
|
|
283
|
-
patient or study to read all series in that patient or
|
|
284
|
-
study. In those cases a list is returned.
|
|
285
|
-
attr (list, optional): list of DICOM attributes to read.
|
|
286
|
-
dims (list, optional): Dimensions to sort the attributes.
|
|
287
|
-
If dims is not provided, values are sorted by
|
|
288
|
-
InstanceNumber.
|
|
289
|
-
coords (bool): If set to True, the coordinates of the
|
|
290
|
-
attributes are returned alongside the values
|
|
291
|
-
|
|
292
|
-
Returns:
|
|
293
|
-
dict or tuple: values as a dictionary in the last
|
|
294
|
-
return value, where each value is a numpy array with
|
|
295
|
-
the required dimensions. If coords is set to True,
|
|
296
|
-
these are returned too.
|
|
297
|
-
"""
|
|
298
|
-
if isinstance(series, str):
|
|
299
|
-
series = [series]
|
|
300
|
-
dbd = open(series[0])
|
|
301
|
-
array = dbd.values(series, attr, dims, coords)
|
|
302
|
-
dbd.close()
|
|
303
|
-
return array
|
|
304
|
-
|
|
305
|
-
|
|
306
318
|
def files(entity:list) -> list:
|
|
307
319
|
"""Read the files in a DICOM entity
|
|
308
320
|
|
dbdicom/dataset.py
CHANGED
|
@@ -370,7 +370,7 @@ def set_volume(ds, volume:vreg.Volume3D):
|
|
|
370
370
|
set_affine(ds, volume.affine)
|
|
371
371
|
if volume.coords is not None:
|
|
372
372
|
# All other dimensions should have size 1
|
|
373
|
-
coords =
|
|
373
|
+
coords = [c.reshape(-1) for c in volume.coords]
|
|
374
374
|
for i, d in enumerate(volume.dims):
|
|
375
375
|
if not is_valid_dicom_tag(d):
|
|
376
376
|
raise ValueError(
|
|
@@ -380,7 +380,7 @@ def set_volume(ds, volume:vreg.Volume3D):
|
|
|
380
380
|
"tags to change the dimensions."
|
|
381
381
|
)
|
|
382
382
|
else:
|
|
383
|
-
set_values(ds, d, coords[i
|
|
383
|
+
set_values(ds, d, coords[i][0])
|
|
384
384
|
|
|
385
385
|
|
|
386
386
|
|
dbdicom/dbd.py
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import shutil
|
|
2
3
|
import json
|
|
3
4
|
from typing import Union
|
|
4
5
|
import zipfile
|
|
5
6
|
import re
|
|
7
|
+
from copy import deepcopy
|
|
6
8
|
|
|
7
9
|
from tqdm import tqdm
|
|
8
10
|
import numpy as np
|
|
@@ -76,14 +78,16 @@ class DataBaseDicom():
|
|
|
76
78
|
Args:
|
|
77
79
|
entity (list): entity to delete
|
|
78
80
|
"""
|
|
81
|
+
# delete datasets on disk
|
|
79
82
|
removed = register.index(self.register, entity)
|
|
80
|
-
# delete datasets marked for removal
|
|
81
83
|
for index in removed:
|
|
82
84
|
file = os.path.join(self.path, index)
|
|
83
85
|
if os.path.exists(file):
|
|
84
86
|
os.remove(file)
|
|
85
|
-
#
|
|
86
|
-
|
|
87
|
+
# drop the entity from the register
|
|
88
|
+
register.remove(self.register, entity)
|
|
89
|
+
# cleanup empty folders
|
|
90
|
+
remove_empty_folders(entity[0])
|
|
87
91
|
return self
|
|
88
92
|
|
|
89
93
|
|
|
@@ -206,22 +210,22 @@ class DataBaseDicom():
|
|
|
206
210
|
return register.series(self.register, entity, desc, contains, isin)
|
|
207
211
|
|
|
208
212
|
|
|
209
|
-
def volume(self, entity:Union[list, str], dims:list=None, verbose=1) ->
|
|
210
|
-
"""Read volume
|
|
213
|
+
def volume(self, entity:Union[list, str], dims:list=None, verbose=1) -> vreg.Volume3D:
|
|
214
|
+
"""Read volume.
|
|
211
215
|
|
|
212
216
|
Args:
|
|
213
|
-
entity (list, str): DICOM
|
|
217
|
+
entity (list, str): DICOM series to read
|
|
214
218
|
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
215
219
|
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
216
220
|
|
|
217
221
|
Returns:
|
|
218
|
-
vreg.Volume3D
|
|
219
|
-
a volume, else a list of volumes.
|
|
222
|
+
vreg.Volume3D:
|
|
220
223
|
"""
|
|
221
|
-
if isinstance(entity, str): # path to folder
|
|
222
|
-
|
|
223
|
-
if len(entity) < 4: # folder, patient or study
|
|
224
|
-
|
|
224
|
+
# if isinstance(entity, str): # path to folder
|
|
225
|
+
# return [self.volume(s, dims) for s in self.series(entity)]
|
|
226
|
+
# if len(entity) < 4: # folder, patient or study
|
|
227
|
+
# return [self.volume(s, dims) for s in self.series(entity)]
|
|
228
|
+
|
|
225
229
|
if dims is None:
|
|
226
230
|
dims = []
|
|
227
231
|
elif isinstance(dims, str):
|
|
@@ -230,33 +234,39 @@ class DataBaseDicom():
|
|
|
230
234
|
dims = list(dims)
|
|
231
235
|
dims = ['SliceLocation'] + dims
|
|
232
236
|
|
|
233
|
-
files = register.files(self.register, entity)
|
|
234
|
-
|
|
235
237
|
# Read dicom files
|
|
236
|
-
values = []
|
|
238
|
+
values = [[] for _ in dims]
|
|
237
239
|
volumes = []
|
|
240
|
+
|
|
241
|
+
files = register.files(self.register, entity)
|
|
238
242
|
for f in tqdm(files, desc='Reading volume..', disable=(verbose==0)):
|
|
239
243
|
ds = pydicom.dcmread(f)
|
|
240
|
-
|
|
244
|
+
values_f = get_values(ds, dims)
|
|
245
|
+
for d in range(len(dims)):
|
|
246
|
+
values[d].append(values_f[d])
|
|
241
247
|
volumes.append(dbdataset.volume(ds))
|
|
242
248
|
|
|
243
|
-
# Format as mesh
|
|
244
|
-
|
|
245
|
-
values = [np.array(v, dtype=object) for v in values] # object array to allow for mixed types
|
|
246
|
-
coords = np.stack(values, axis=-1)
|
|
249
|
+
# Format coordinates as mesh
|
|
250
|
+
coords = [np.array(v) for v in values]
|
|
247
251
|
coords, inds = dbdicom.utils.arrays.meshvals(coords)
|
|
248
|
-
vols = np.array(volumes)
|
|
249
|
-
vols = vols[inds].reshape(coords.shape[1:])
|
|
250
252
|
|
|
251
253
|
# Check that all slices have the same coordinates
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
254
|
+
if len(dims) > 1:
|
|
255
|
+
# Loop over all coordinates after slice location
|
|
256
|
+
for c in coords[1:]:
|
|
257
|
+
# Loop over all slice locations
|
|
258
|
+
for k in range(1, c.shape[0]):
|
|
259
|
+
# Coordinate c of slice k
|
|
260
|
+
if not np.array_equal(c[k,...], c[0,...]):
|
|
261
|
+
raise ValueError(
|
|
262
|
+
"Cannot build a single volume. Not all slices "
|
|
263
|
+
"have the same coordinates."
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# Build volumes
|
|
267
|
+
vols = np.array(volumes)
|
|
268
|
+
vols = vols[inds].reshape(coords[0].shape)
|
|
269
|
+
|
|
260
270
|
# Infer spacing between slices from slice locations
|
|
261
271
|
# Technically only necessary if SpacingBetweenSlices not set or incorrect
|
|
262
272
|
vols = infer_slice_spacing(vols)
|
|
@@ -272,11 +282,66 @@ class DataBaseDicom():
|
|
|
272
282
|
# Then try again
|
|
273
283
|
vol = vreg.join(vols)
|
|
274
284
|
if vol.ndim > 3:
|
|
285
|
+
# Coordinates of slice 0
|
|
286
|
+
c0 = [c[0,...] for c in coords[1:]]
|
|
275
287
|
vol.set_coords(c0)
|
|
276
288
|
vol.set_dims(dims[1:])
|
|
277
289
|
return vol
|
|
278
290
|
|
|
279
|
-
|
|
291
|
+
|
|
292
|
+
def values(self, series:list, *attr, dims:list=None, verbose=1) -> Union[dict, tuple]:
|
|
293
|
+
"""Read the values of some attributes from a DICOM series
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
series (list): DICOM series to read.
|
|
297
|
+
attr (tuple, optional): DICOM attributes to read.
|
|
298
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
299
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
300
|
+
|
|
301
|
+
Returns:
|
|
302
|
+
tuple: arrays with values for the attributes.
|
|
303
|
+
"""
|
|
304
|
+
# if isinstance(series, str): # path to folder
|
|
305
|
+
# return [self.values(s, attr, dims) for s in self.series(series)]
|
|
306
|
+
# if len(series) < 4: # folder, patient or study
|
|
307
|
+
# return [self.values(s, attr, dims) for s in self.series(series)]
|
|
308
|
+
|
|
309
|
+
if dims is None:
|
|
310
|
+
dims = ['InstanceNumber']
|
|
311
|
+
elif np.isscalar(dims):
|
|
312
|
+
dims = [dims]
|
|
313
|
+
else:
|
|
314
|
+
dims = list(dims)
|
|
315
|
+
|
|
316
|
+
# Read dicom files
|
|
317
|
+
coord_values = [[] for _ in dims]
|
|
318
|
+
attr_values = [[] for _ in attr]
|
|
319
|
+
|
|
320
|
+
files = register.files(self.register, series)
|
|
321
|
+
for f in tqdm(files, desc='Reading values..', disable=(verbose==0)):
|
|
322
|
+
ds = pydicom.dcmread(f)
|
|
323
|
+
coord_values_f = get_values(ds, dims)
|
|
324
|
+
for d in range(len(dims)):
|
|
325
|
+
coord_values[d].append(coord_values_f[d])
|
|
326
|
+
attr_values_f = get_values(ds, attr)
|
|
327
|
+
for a in range(len(attr)):
|
|
328
|
+
attr_values[a].append(attr_values_f[a])
|
|
329
|
+
|
|
330
|
+
# Format coordinates as mesh
|
|
331
|
+
coords = [np.array(v) for v in coord_values]
|
|
332
|
+
coords, inds = dbdicom.utils.arrays.meshvals(coords)
|
|
333
|
+
|
|
334
|
+
# Sort values accordingly
|
|
335
|
+
values = [np.array(v) for v in attr_values]
|
|
336
|
+
values = [v[inds].reshape(coords[0].shape) for v in values]
|
|
337
|
+
|
|
338
|
+
# Return values
|
|
339
|
+
if len(values) == 1:
|
|
340
|
+
return values[0]
|
|
341
|
+
else:
|
|
342
|
+
return tuple(values)
|
|
343
|
+
|
|
344
|
+
|
|
280
345
|
def write_volume(
|
|
281
346
|
self, vol:Union[vreg.Volume3D, tuple], series:list,
|
|
282
347
|
ref:list=None,
|
|
@@ -288,6 +353,10 @@ class DataBaseDicom():
|
|
|
288
353
|
series (list): DICOM series to read
|
|
289
354
|
ref (list): Reference series
|
|
290
355
|
"""
|
|
356
|
+
series_full_name = full_name(series)
|
|
357
|
+
if series_full_name in self.series():
|
|
358
|
+
raise ValueError(f"Series {series_full_name[-1]} already exists in study {series_full_name[-2]}.")
|
|
359
|
+
|
|
291
360
|
if isinstance(vol, tuple):
|
|
292
361
|
vol = vreg.volume(vol[0], vol[1])
|
|
293
362
|
if ref is None:
|
|
@@ -318,11 +387,85 @@ class DataBaseDicom():
|
|
|
318
387
|
slices = vt.split()
|
|
319
388
|
for sl in slices:
|
|
320
389
|
dbdataset.set_volume(ds, sl)
|
|
321
|
-
sl_coords = [
|
|
390
|
+
sl_coords = [c.ravel()[0] for c in sl.coords]
|
|
322
391
|
set_value(ds, sl.dims, sl_coords)
|
|
323
392
|
self._write_dataset(ds, attr, n + 1 + i)
|
|
324
393
|
i+=1
|
|
325
394
|
return self
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def edit(
|
|
398
|
+
self, series:list, new_values:dict, dims:list=None, verbose=1,
|
|
399
|
+
):
|
|
400
|
+
"""Edit attribute values in a new DICOM series
|
|
401
|
+
|
|
402
|
+
Args:
|
|
403
|
+
series (list): DICOM series to edit
|
|
404
|
+
new_values (dict): dictionary with attribute: value pairs to write to the series
|
|
405
|
+
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
406
|
+
verbose (bool, optional): If set to 1, shows progress bar. Defaults to 1.
|
|
407
|
+
"""
|
|
408
|
+
|
|
409
|
+
if dims is None:
|
|
410
|
+
dims = ['InstanceNumber']
|
|
411
|
+
elif np.isscalar(dims):
|
|
412
|
+
dims = [dims]
|
|
413
|
+
else:
|
|
414
|
+
dims = list(dims)
|
|
415
|
+
|
|
416
|
+
# Check that all values have the correct nr of elements
|
|
417
|
+
files = register.files(self.register, series)
|
|
418
|
+
for a in new_values.values():
|
|
419
|
+
if np.isscalar(a):
|
|
420
|
+
pass
|
|
421
|
+
elif np.array(a).size != len(files):
|
|
422
|
+
raise ValueError(
|
|
423
|
+
f"Incorrect value lengths. All values need to have {len(files)} elements"
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
# Read dicom files to sort them
|
|
427
|
+
coord_values = [[] for _ in dims]
|
|
428
|
+
for f in tqdm(files, desc='Sorting series..', disable=(verbose==0)):
|
|
429
|
+
ds = pydicom.dcmread(f)
|
|
430
|
+
coord_values_f = get_values(ds, dims)
|
|
431
|
+
for d in range(len(dims)):
|
|
432
|
+
coord_values[d].append(coord_values_f[d])
|
|
433
|
+
|
|
434
|
+
# Format coordinates as mesh
|
|
435
|
+
coords = [np.array(v) for v in coord_values]
|
|
436
|
+
coords, inds = dbdicom.utils.arrays.meshvals(coords)
|
|
437
|
+
|
|
438
|
+
# Sort files accordingly
|
|
439
|
+
files = np.array(files)[inds]
|
|
440
|
+
|
|
441
|
+
# Now edit and write the files
|
|
442
|
+
attr = self._series_attributes(series)
|
|
443
|
+
n = self._max_instance_number(attr['SeriesInstanceUID'])
|
|
444
|
+
|
|
445
|
+
# Drop existing attributes if they are edited
|
|
446
|
+
attr = {a:attr[a] for a in attr if a not in new_values}
|
|
447
|
+
|
|
448
|
+
# List instances to be edited
|
|
449
|
+
to_drop = register.index(self.register, series)
|
|
450
|
+
|
|
451
|
+
# Write the instances
|
|
452
|
+
tags = list(new_values.keys())
|
|
453
|
+
for i, f in tqdm(enumerate(files), desc='Writing values..', disable=(verbose==0)):
|
|
454
|
+
ds = pydicom.dcmread(f)
|
|
455
|
+
values = []
|
|
456
|
+
for a in new_values.values():
|
|
457
|
+
if np.isscalar(a):
|
|
458
|
+
values.append(a)
|
|
459
|
+
else:
|
|
460
|
+
values.append(np.array(a).reshape(-1)[i])
|
|
461
|
+
set_values(ds, tags, values)
|
|
462
|
+
self._write_dataset(ds, attr, n + 1 + i)
|
|
463
|
+
|
|
464
|
+
# Delete the originals files
|
|
465
|
+
register.drop(self.register, to_drop)
|
|
466
|
+
[os.remove(os.path.join(self.path, idx)) for idx in to_drop]
|
|
467
|
+
|
|
468
|
+
return self
|
|
326
469
|
|
|
327
470
|
|
|
328
471
|
def to_nifti(self, series:list, file:str, dims=None, verbose=1):
|
|
@@ -456,103 +599,7 @@ class DataBaseDicom():
|
|
|
456
599
|
return arrays, values_return
|
|
457
600
|
|
|
458
601
|
|
|
459
|
-
def values(self, series:list, attr=None, dims:list=None, coords=False) -> Union[dict, tuple]:
|
|
460
|
-
"""Read the values of some or all attributes from a DICOM series
|
|
461
602
|
|
|
462
|
-
Args:
|
|
463
|
-
series (list or str): DICOM series to read. This can also
|
|
464
|
-
be a path to a folder containing DICOM files, or a
|
|
465
|
-
patient or study to read all series in that patient or
|
|
466
|
-
study. In those cases a list is returned.
|
|
467
|
-
attr (list, optional): list of DICOM attributes to read.
|
|
468
|
-
dims (list, optional): Dimensions to sort the attributes.
|
|
469
|
-
If dims is not provided, values are sorted by
|
|
470
|
-
InstanceNumber.
|
|
471
|
-
coords (bool): If set to True, the coordinates of the
|
|
472
|
-
attributes are returned alongside the values
|
|
473
|
-
|
|
474
|
-
Returns:
|
|
475
|
-
dict or tuple: values as a dictionary in the last
|
|
476
|
-
return value, where each value is a numpy array with
|
|
477
|
-
the required dimensions. If coords is set to True,
|
|
478
|
-
these are returned too.
|
|
479
|
-
"""
|
|
480
|
-
if isinstance(series, str): # path to folder
|
|
481
|
-
return [self.values(s, attr, dims, coords) for s in self.series(series)]
|
|
482
|
-
if len(series) < 4: # folder, patient or study
|
|
483
|
-
return [self.values(s, attr, dims, coords) for s in self.series(series)]
|
|
484
|
-
|
|
485
|
-
if dims is None:
|
|
486
|
-
dims = ['InstanceNumber']
|
|
487
|
-
elif np.isscalar(dims):
|
|
488
|
-
dims = [dims]
|
|
489
|
-
else:
|
|
490
|
-
dims = list(dims)
|
|
491
|
-
|
|
492
|
-
files = register.files(self.register, series)
|
|
493
|
-
|
|
494
|
-
# Ensure return_vals is a list
|
|
495
|
-
if attr is None:
|
|
496
|
-
# If attributes are not provided, read all
|
|
497
|
-
# attributes from the first file
|
|
498
|
-
ds = pydicom.dcmread(files[0])
|
|
499
|
-
exclude = ['PixelData', 'FloatPixelData', 'DoubleFloatPixelData']
|
|
500
|
-
params = []
|
|
501
|
-
param_labels = []
|
|
502
|
-
for elem in ds:
|
|
503
|
-
if elem.keyword not in exclude:
|
|
504
|
-
params.append(elem.tag)
|
|
505
|
-
# For known tags use the keyword as label
|
|
506
|
-
label = elem.tag if len(elem.keyword)==0 else elem.keyword
|
|
507
|
-
param_labels.append(label)
|
|
508
|
-
elif np.isscalar(attr):
|
|
509
|
-
params = [attr]
|
|
510
|
-
param_labels = params[:]
|
|
511
|
-
else:
|
|
512
|
-
params = list(attr)
|
|
513
|
-
param_labels = params[:]
|
|
514
|
-
|
|
515
|
-
# Read dicom files
|
|
516
|
-
coords_array = []
|
|
517
|
-
values = np.empty(len(files), dtype=dict)
|
|
518
|
-
for i, f in tqdm(enumerate(files), desc='Reading values..'):
|
|
519
|
-
ds = pydicom.dcmread(f)
|
|
520
|
-
coords_array.append(get_values(ds, dims))
|
|
521
|
-
# save as dict so numpy does not stack as arrays
|
|
522
|
-
values[i] = {'values': get_values(ds, params)}
|
|
523
|
-
|
|
524
|
-
# Format as mesh
|
|
525
|
-
coords_array = np.stack([v for v in coords_array], axis=-1)
|
|
526
|
-
coords_array, inds = dbdicom.utils.arrays.meshvals(coords_array)
|
|
527
|
-
|
|
528
|
-
# Sort values accordingly
|
|
529
|
-
values = values[inds].reshape(-1)
|
|
530
|
-
|
|
531
|
-
# Return values as a dictionary
|
|
532
|
-
values_dict = {}
|
|
533
|
-
for p in range(len(params)):
|
|
534
|
-
# Get the type from the first value
|
|
535
|
-
vp0 = values[0]['values'][p]
|
|
536
|
-
# Build an array of the right type
|
|
537
|
-
vp = np.zeros(values.size, dtype=type(vp0))
|
|
538
|
-
# Populate the arrate with values for parameter p
|
|
539
|
-
for i, v in enumerate(values):
|
|
540
|
-
vp[i] = v['values'][p]
|
|
541
|
-
# Reshape values for parameter p
|
|
542
|
-
vp = vp.reshape(coords_array.shape[1:])
|
|
543
|
-
# Eneter in the dictionary
|
|
544
|
-
values_dict[param_labels[p]] = vp
|
|
545
|
-
|
|
546
|
-
# If only one, return as value
|
|
547
|
-
if len(params) == 1:
|
|
548
|
-
values_return = values_dict[params[0]]
|
|
549
|
-
else:
|
|
550
|
-
values_return = values_dict
|
|
551
|
-
|
|
552
|
-
if coords:
|
|
553
|
-
return values_return, coords_array
|
|
554
|
-
else:
|
|
555
|
-
return values_return
|
|
556
603
|
|
|
557
604
|
|
|
558
605
|
def files(self, entity:list) -> list:
|
|
@@ -622,34 +669,66 @@ class DataBaseDicom():
|
|
|
622
669
|
else:
|
|
623
670
|
return {p: values[i] for i, p in enumerate(pars)}
|
|
624
671
|
|
|
625
|
-
def copy(self, from_entity, to_entity):
|
|
672
|
+
def copy(self, from_entity, to_entity=None):
|
|
626
673
|
"""Copy a DICOM entity (patient, study or series)
|
|
627
674
|
|
|
628
675
|
Args:
|
|
629
676
|
from_entity (list): entity to copy
|
|
630
|
-
to_entity (list): entity after copying.
|
|
677
|
+
to_entity (list, optional): entity after copying. If this is not
|
|
678
|
+
provided, a copy will be made in the same study and returned
|
|
679
|
+
|
|
680
|
+
Returns:
|
|
681
|
+
entity: the copied entity. If th to_entity is provided, this is
|
|
682
|
+
returned.
|
|
631
683
|
"""
|
|
632
684
|
if len(from_entity) == 4:
|
|
685
|
+
if to_entity is None:
|
|
686
|
+
to_entity = deepcopy(from_entity)
|
|
687
|
+
if isinstance(to_entity[-1], tuple):
|
|
688
|
+
to_entity[-1] = (to_entity[-1][0] + '_copy', 0)
|
|
689
|
+
else:
|
|
690
|
+
to_entity[-1] = (to_entity[-1] + '_copy', 0)
|
|
691
|
+
while to_entity in self.series():
|
|
692
|
+
to_entity[-1][1] += 1
|
|
633
693
|
if len(to_entity) != 4:
|
|
634
694
|
raise ValueError(
|
|
635
695
|
f"Cannot copy series {from_entity} to series {to_entity}. "
|
|
636
696
|
f"{to_entity} is not a series (needs 4 elements)."
|
|
637
697
|
)
|
|
638
|
-
|
|
698
|
+
self._copy_series(from_entity, to_entity)
|
|
699
|
+
return to_entity
|
|
700
|
+
|
|
639
701
|
if len(from_entity) == 3:
|
|
702
|
+
if to_entity is None:
|
|
703
|
+
to_entity = deepcopy(from_entity)
|
|
704
|
+
if isinstance(to_entity[-1], tuple):
|
|
705
|
+
to_entity[-1] = (to_entity[-1][0] + '_copy', 0)
|
|
706
|
+
else:
|
|
707
|
+
to_entity[-1] = (to_entity[-1] + '_copy', 0)
|
|
708
|
+
while to_entity in self.studies():
|
|
709
|
+
to_entity[-1][1] += 1
|
|
640
710
|
if len(to_entity) != 3:
|
|
641
711
|
raise ValueError(
|
|
642
712
|
f"Cannot copy study {from_entity} to study {to_entity}. "
|
|
643
713
|
f"{to_entity} is not a study (needs 3 elements)."
|
|
644
714
|
)
|
|
645
|
-
|
|
715
|
+
self._copy_study(from_entity, to_entity)
|
|
716
|
+
return to_entity
|
|
717
|
+
|
|
646
718
|
if len(from_entity) == 2:
|
|
719
|
+
if to_entity is None:
|
|
720
|
+
to_entity = deepcopy(from_entity)
|
|
721
|
+
to_entity[-1] += '_copy'
|
|
722
|
+
while to_entity in self.patients():
|
|
723
|
+
to_entity[-1] += '_copy'
|
|
647
724
|
if len(to_entity) != 2:
|
|
648
725
|
raise ValueError(
|
|
649
726
|
f"Cannot copy patient {from_entity} to patient {to_entity}. "
|
|
650
727
|
f"{to_entity} is not a patient (needs 2 elements)."
|
|
651
728
|
)
|
|
652
|
-
|
|
729
|
+
self._copy_patient(from_entity, to_entity)
|
|
730
|
+
return to_entity
|
|
731
|
+
|
|
653
732
|
raise ValueError(
|
|
654
733
|
f"Cannot copy {from_entity} to {to_entity}. "
|
|
655
734
|
)
|
|
@@ -930,6 +1009,28 @@ class DataBaseDicom():
|
|
|
930
1009
|
|
|
931
1010
|
|
|
932
1011
|
|
|
1012
|
+
def full_name(entity):
|
|
1013
|
+
|
|
1014
|
+
if len(entity)==3: # study
|
|
1015
|
+
if isinstance(entity[-1], tuple):
|
|
1016
|
+
return entity
|
|
1017
|
+
else:
|
|
1018
|
+
full_name_study = deepcopy(entity)
|
|
1019
|
+
full_name_study[-1] = (full_name_study[-1], 0)
|
|
1020
|
+
return full_name_study
|
|
1021
|
+
|
|
1022
|
+
elif len(entity)==4: # series
|
|
1023
|
+
full_name_study = full_name(entity[:3])
|
|
1024
|
+
series = full_name_study + [entity[-1]]
|
|
1025
|
+
if isinstance(series[-1], tuple):
|
|
1026
|
+
return series
|
|
1027
|
+
else:
|
|
1028
|
+
full_name_series = deepcopy(series)
|
|
1029
|
+
full_name_series[-1] = (full_name_series[-1], 0)
|
|
1030
|
+
return full_name_series
|
|
1031
|
+
else:
|
|
1032
|
+
return entity
|
|
1033
|
+
|
|
933
1034
|
|
|
934
1035
|
def clean_folder_name(name, replacement="", max_length=255):
|
|
935
1036
|
# Strip leading/trailing whitespace
|
|
@@ -954,6 +1055,30 @@ def clean_folder_name(name, replacement="", max_length=255):
|
|
|
954
1055
|
|
|
955
1056
|
|
|
956
1057
|
|
|
1058
|
+
def remove_empty_folders(path):
|
|
1059
|
+
"""
|
|
1060
|
+
Removes all empty subfolders from a given directory.
|
|
1061
|
+
|
|
1062
|
+
This function walks through the directory tree from the bottom up.
|
|
1063
|
+
This is crucial because it allows child directories to be removed before
|
|
1064
|
+
their parents, potentially making the parent directory empty and
|
|
1065
|
+
eligible for removal in the same pass.
|
|
1066
|
+
|
|
1067
|
+
Args:
|
|
1068
|
+
path (str): The absolute or relative path to the directory to scan.
|
|
1069
|
+
"""
|
|
1070
|
+
# Walk the directory tree in a bottom-up manner (topdown=False)
|
|
1071
|
+
for dirpath, dirnames, filenames in os.walk(path, topdown=False):
|
|
1072
|
+
# A directory is considered empty if it has no subdirectories and no files
|
|
1073
|
+
if not dirnames and not filenames:
|
|
1074
|
+
try:
|
|
1075
|
+
shutil.rmtree(dirpath)
|
|
1076
|
+
except OSError as e:
|
|
1077
|
+
# This might happen due to permissions issues
|
|
1078
|
+
print(f"Error removing {dirpath}: {e}")
|
|
1079
|
+
|
|
1080
|
+
|
|
1081
|
+
|
|
957
1082
|
def infer_slice_spacing(vols):
|
|
958
1083
|
# In case spacing between slices is not (correctly) encoded in
|
|
959
1084
|
# DICOM it can be inferred from the slice locations.
|
|
@@ -1010,3 +1135,4 @@ def infer_slice_spacing(vols):
|
|
|
1010
1135
|
|
|
1011
1136
|
|
|
1012
1137
|
|
|
1138
|
+
|
dbdicom/register.py
CHANGED
|
@@ -96,6 +96,27 @@ def index(dbtree, entity):
|
|
|
96
96
|
if sr['SeriesInstanceUID'] == series_uid:
|
|
97
97
|
return list(sr['instances'].values())
|
|
98
98
|
|
|
99
|
+
def remove(dbtree, entity):
|
|
100
|
+
if len(entity)==2:
|
|
101
|
+
patient_id = entity[1]
|
|
102
|
+
for pt in sorted(dbtree, key=lambda pt: pt['PatientID']):
|
|
103
|
+
if pt['PatientID'] == patient_id:
|
|
104
|
+
dbtree.remove(pt)
|
|
105
|
+
elif len(entity)==3:
|
|
106
|
+
study_uid = uid(dbtree, entity)
|
|
107
|
+
for pt in sorted(dbtree, key=lambda pt: pt['PatientID']):
|
|
108
|
+
for st in sorted(pt['studies'], key=lambda st: st['StudyInstanceUID']):
|
|
109
|
+
if st['StudyInstanceUID'] == study_uid:
|
|
110
|
+
pt['studies'].remove(st)
|
|
111
|
+
elif len(entity)==4:
|
|
112
|
+
series_uid = uid(dbtree, entity)
|
|
113
|
+
for pt in sorted(dbtree, key=lambda pt: pt['PatientID']):
|
|
114
|
+
for st in sorted(pt['studies'], key=lambda st: st['StudyInstanceUID']):
|
|
115
|
+
for sr in sorted(st['series'], key=lambda sr: sr['SeriesNumber']):
|
|
116
|
+
if sr['SeriesInstanceUID'] == series_uid:
|
|
117
|
+
st['series'].remove(sr)
|
|
118
|
+
return dbtree
|
|
119
|
+
|
|
99
120
|
|
|
100
121
|
def drop(dbtree, relpaths):
|
|
101
122
|
for pt in sorted(dbtree[:], key=lambda pt: pt['PatientID']):
|
|
@@ -110,7 +110,7 @@ def from_volume(vol:vreg.Volume3D):
|
|
|
110
110
|
|
|
111
111
|
# Assign parameters using dims as DICOM keywords
|
|
112
112
|
for ax_i, axis in enumerate(vol.dims):
|
|
113
|
-
val = vol.coords[
|
|
113
|
+
val = vol.coords[ax_i][indices]
|
|
114
114
|
|
|
115
115
|
sequence, attr = axis.split("/")
|
|
116
116
|
if not hasattr(frame_ds, sequence):
|
dbdicom/utils/arrays.py
CHANGED
|
@@ -1,40 +1,128 @@
|
|
|
1
1
|
import numpy as np
|
|
2
2
|
|
|
3
|
+
from typing import List, Tuple
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
for
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
5
|
+
|
|
6
|
+
def meshvals(arrays) -> Tuple[List[np.ndarray], np.ndarray]:
|
|
7
|
+
"""
|
|
8
|
+
Lexicographically sort flattened N coordinate arrays and reshape back to inferred grid shape,
|
|
9
|
+
preserving original type of each input array.
|
|
10
|
+
|
|
11
|
+
Parameters
|
|
12
|
+
----------
|
|
13
|
+
*arrays : array-like
|
|
14
|
+
Flattened coordinate arrays of the same length. Can be numbers, strings, or list objects.
|
|
15
|
+
|
|
16
|
+
Returns
|
|
17
|
+
-------
|
|
18
|
+
sorted_arrays : list[np.ndarray]
|
|
19
|
+
Coordinate arrays reshaped to inferred N-D grid shape, dtype/type preserved.
|
|
20
|
+
indices : np.ndarray
|
|
21
|
+
Permutation indices applied to the flattened arrays.
|
|
22
|
+
shape : tuple[int, ...]
|
|
23
|
+
Inferred grid shape (number of unique values per axis).
|
|
24
|
+
"""
|
|
25
|
+
# Remember original type/dtype for each array
|
|
26
|
+
orig_types = [a.dtype if isinstance(a[0], np.ndarray) else type(a[0]) for a in arrays]
|
|
27
|
+
|
|
28
|
+
# Convert non arrays to object arrays
|
|
29
|
+
arrs = []
|
|
30
|
+
for a in arrays:
|
|
31
|
+
arrs_a = np.empty(len(a), dtype=object)
|
|
32
|
+
arrs_a[:] = a
|
|
33
|
+
arrs.append(arrs_a)
|
|
34
|
+
|
|
35
|
+
# Stack arrays as columns (M x N)
|
|
36
|
+
coords = np.stack(arrs, axis=1)
|
|
37
|
+
|
|
38
|
+
# Lexicographic sort using structured array
|
|
39
|
+
indices = np.lexsort(coords.T[::-1])
|
|
40
|
+
sorted_coords = coords[indices]
|
|
41
|
+
|
|
42
|
+
# Check that all coordinates are unique
|
|
43
|
+
points = [tuple(col) for col in sorted_coords]
|
|
44
|
+
if not all_elements_unique(points):
|
|
31
45
|
raise ValueError(
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
46
|
+
f"Improper coordinates. Coordinate values are not unique."
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Infer shape from unique values per axis
|
|
50
|
+
shape = tuple(len(np.unique(sorted_coords[:, i])) for i in range(sorted_coords.shape[1]))
|
|
51
|
+
|
|
52
|
+
# Check perfect grid
|
|
53
|
+
if np.prod(shape) != sorted_coords.shape[0]:
|
|
54
|
+
raise ValueError(
|
|
55
|
+
f"Coordinates do not form a perfect Cartesian grid: inferred shape {shape} "
|
|
56
|
+
f"does not match number of points {sorted_coords.shape[0]}"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Split back into individual arrays and cast to original type
|
|
60
|
+
sorted_arrays = []
|
|
61
|
+
for i, orig_type in enumerate(orig_types):
|
|
62
|
+
arr = sorted_coords[:, i]
|
|
63
|
+
arr = arr.astype(orig_type).reshape(shape)
|
|
64
|
+
sorted_arrays.append(arr)
|
|
65
|
+
|
|
66
|
+
return sorted_arrays, indices
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def all_elements_unique(items):
|
|
70
|
+
"""
|
|
71
|
+
The most general uniqueness check, but also the slowest (O(n^2)).
|
|
72
|
+
|
|
73
|
+
It works for ANY type that supports equality checking (==), including
|
|
74
|
+
lists, dicts, and custom objects, without requiring them to be hashable.
|
|
75
|
+
"""
|
|
76
|
+
for i in range(len(items)):
|
|
77
|
+
for j in range(i + 1, len(items)):
|
|
78
|
+
if items[i] == items[j]:
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# def NEWmeshvals(coords):
|
|
85
|
+
# stack_coords = [np.array(c, dtype=object) for c in coords]
|
|
86
|
+
# stack_coords = np.stack(stack_coords)
|
|
87
|
+
# mesh_coords, sorted_indices = _meshvals(stack_coords)
|
|
88
|
+
# mesh_coords = [mesh_coords[d,...] for d in range(mesh_coords.shape[0])]
|
|
89
|
+
# return mesh_coords, sorted_indices
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# def _meshvals(coords):
|
|
93
|
+
# # Input array shape: (d, f) with d = nr of dims and f = nr of frames
|
|
94
|
+
# # Output array shape: (d, f1,..., fd)
|
|
95
|
+
# if coords.size == 0:
|
|
96
|
+
# return np.array([])
|
|
97
|
+
# # Sort by column
|
|
98
|
+
# sorted_indices = np.lexsort(coords[::-1])
|
|
99
|
+
# sorted_array = coords[:, sorted_indices]
|
|
100
|
+
# # Find shape
|
|
101
|
+
# shape = _mesh_shape(sorted_array)
|
|
102
|
+
# # Reshape
|
|
103
|
+
# mesh_array = sorted_array.reshape(shape)
|
|
104
|
+
# return mesh_array, sorted_indices
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# def _mesh_shape(sorted_array):
|
|
108
|
+
|
|
109
|
+
# nd = np.unique(sorted_array[0,:]).size
|
|
110
|
+
# shape = (sorted_array.shape[0], nd)
|
|
111
|
+
|
|
112
|
+
# for dim in range(1,shape[0]):
|
|
113
|
+
# shape_dim = (shape[0], np.prod(shape[1:]), -1)
|
|
114
|
+
# sorted_array = sorted_array.reshape(shape_dim)
|
|
115
|
+
# nd = [np.unique(sorted_array[dim,d,:]).size for d in range(shape_dim[1])]
|
|
116
|
+
# shape = shape + (max(nd),)
|
|
117
|
+
|
|
118
|
+
# if np.prod(shape) != sorted_array.size:
|
|
119
|
+
# raise ValueError(
|
|
120
|
+
# 'Improper dimensions for the series. This usually means '
|
|
121
|
+
# 'that there are multiple images at the same location, \n or that '
|
|
122
|
+
# 'there are no images at one or more locations. \n\n'
|
|
123
|
+
# 'Make sure to specify proper dimensions when reading a pixel array or volume. \n'
|
|
124
|
+
# 'If the default dimensions of pixel_array (InstanceNumber) generate this error, '
|
|
125
|
+
# 'the DICOM data may be corrupted.'
|
|
126
|
+
# )
|
|
127
|
+
|
|
128
|
+
# return shape
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbdicom
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.10
|
|
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,10 @@
|
|
|
1
1
|
dbdicom/__init__.py,sha256=dW5aezonmMc_41Dp1PuYmXQlr307RkyJxsJuetkpWso,87
|
|
2
|
-
dbdicom/api.py,sha256=
|
|
2
|
+
dbdicom/api.py,sha256=5jOJjwOahDrj97Hu0XEJbryvhihpv4iuPSXdX8INpsE,14731
|
|
3
3
|
dbdicom/const.py,sha256=BqBiRRjeiSqDr1W6YvaayD8WKCjG4Cny2NT0GeLM6bI,4269
|
|
4
4
|
dbdicom/database.py,sha256=mkYQAAf9fETzUhSQflQFp7RQUBdPlSlDty9nn6KY1jQ,4771
|
|
5
|
-
dbdicom/dataset.py,sha256=
|
|
6
|
-
dbdicom/dbd.py,sha256=
|
|
7
|
-
dbdicom/register.py,sha256=
|
|
5
|
+
dbdicom/dataset.py,sha256=pokXlcXLM33OdqMKGfVcWHlVE9ez4iBfpoefVWw1ob8,14421
|
|
6
|
+
dbdicom/dbd.py,sha256=isqBzCE5bxfixZTH5ShNmJ123vCAhueGxV7pCTrqBF8,44035
|
|
7
|
+
dbdicom/register.py,sha256=5yXnTbRUu8rYJqeIbSv5SiRf2E4BZ0JZyvKm_xvaXZQ,14944
|
|
8
8
|
dbdicom/external/__init__.py,sha256=XNQqfspyf6vFGedXlRKZsUB8k8E-0W19Uamwn8Aioxo,316
|
|
9
9
|
dbdicom/external/__pycache__/__init__.cpython-311.pyc,sha256=pXAQ35ixd92fm6YcuHgzR1t6RcASQ-cHhU1wOA5b8sw,542
|
|
10
10
|
dbdicom/external/dcm4che/README.md,sha256=0aAGRs36W3_0s5LzWHRGf_tqariS_JP4iJggaxnD4Xw,8987
|
|
@@ -35,20 +35,20 @@ dbdicom/external/dcm4che/lib/windows-x86/clib_jiio_util.dll,sha256=wi4yyrI1gTRo_
|
|
|
35
35
|
dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll,sha256=QanyzLy0Cd79-aOVPwOcXwikUYeutne0Au-Um91_B4M,8505856
|
|
36
36
|
dbdicom/external/dcm4che/lib/windows-x86-64/opencv_java.dll,sha256=TmjW2SbG4MR3GQ95T8xCVVDLgsdKukgaHBPUvWkfXp8,11039232
|
|
37
37
|
dbdicom/sop_classes/ct_image.py,sha256=16PNv_0e1_7cfxE12JWlx5YQeaTAQVzwtXTjxs3aonk,2812
|
|
38
|
-
dbdicom/sop_classes/enhanced_mr_image.py,sha256=
|
|
38
|
+
dbdicom/sop_classes/enhanced_mr_image.py,sha256=6x4CEd982i64e90ZlFDKNSc83XHC2k2DVit1iyjXjCU,33368
|
|
39
39
|
dbdicom/sop_classes/mr_image.py,sha256=1biIw7R26Fc38FAeSlWxd29VO17e8cEQdDIdLbeXTzw,10959
|
|
40
40
|
dbdicom/sop_classes/parametric_map.py,sha256=2OKBuC2bo03OEpKqimQS-nVGFp1cKRPYwVgmDGVf1JU,12288
|
|
41
41
|
dbdicom/sop_classes/secondary_capture.py,sha256=wgNRX8qyhV7HR7Jq2tQWPPuGpiRzYl6qPOgK6qFbPUc,4541
|
|
42
42
|
dbdicom/sop_classes/segmentation.py,sha256=I8-PciIoIz27_-dZ4esBZSw0TBBbO8KbNYTiTmVe62g,11465
|
|
43
43
|
dbdicom/sop_classes/ultrasound_multiframe_image.py,sha256=j3KN5R90j6WwPMy01hAN2_XSum5TvksF2MYoNGfX_yE,2797
|
|
44
44
|
dbdicom/sop_classes/xray_angiographic_image.py,sha256=nWysCGaEWKVNItnOgyJfcGMpS3oEK1T0_uNR2D7p0Ls,3270
|
|
45
|
-
dbdicom/utils/arrays.py,sha256=
|
|
45
|
+
dbdicom/utils/arrays.py,sha256=_dJGFQPVRfchIRN6vra08RBYnEezobclHv5rEndQ3OA,4588
|
|
46
46
|
dbdicom/utils/dcm4che.py,sha256=Vxq8NYWWK3BuqJkzhBQ89oMqzJlnxqTxgsgTo_Frznc,2317
|
|
47
47
|
dbdicom/utils/files.py,sha256=qhWNJqeWnRjDNbERpC6Mz962_TW9mFdvd2lnBbK3xt4,2259
|
|
48
48
|
dbdicom/utils/image.py,sha256=zRM1O0bxPp-qpf3Iv_GRS1omKaMN1SgSkAwufWLJ0Fk,3863
|
|
49
49
|
dbdicom/utils/pydicom_dataset.py,sha256=XM3EERsCWPlEaUzVaFQSbPNiNbEGwxIbf-sUKKf_YxA,12755
|
|
50
|
-
dbdicom-0.3.
|
|
51
|
-
dbdicom-0.3.
|
|
52
|
-
dbdicom-0.3.
|
|
53
|
-
dbdicom-0.3.
|
|
54
|
-
dbdicom-0.3.
|
|
50
|
+
dbdicom-0.3.10.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
51
|
+
dbdicom-0.3.10.dist-info/METADATA,sha256=0BoheAKLRKF5pClyGgZqc1Y07-AH3I7IbvB-Mh9tQQA,1031
|
|
52
|
+
dbdicom-0.3.10.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
53
|
+
dbdicom-0.3.10.dist-info/top_level.txt,sha256=nJWxXg4YjD6QblfmhrzTMXcr8FSKNc0Yk-CAIDUsYkQ,8
|
|
54
|
+
dbdicom-0.3.10.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|