dbdicom 0.3.2__py3-none-any.whl → 0.3.3__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 +14 -22
- dbdicom/database.py +4 -0
- dbdicom/dataset.py +19 -26
- dbdicom/dbd.py +134 -39
- dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
- dbdicom/register.py +2 -1
- dbdicom/sop_classes/mr_image.py +34 -13
- dbdicom/utils/image.py +3 -4
- {dbdicom-0.3.2.dist-info → dbdicom-0.3.3.dist-info}/METADATA +1 -1
- {dbdicom-0.3.2.dist-info → dbdicom-0.3.3.dist-info}/RECORD +15 -15
- {dbdicom-0.3.2.dist-info → dbdicom-0.3.3.dist-info}/WHEEL +0 -0
- {dbdicom-0.3.2.dist-info → dbdicom-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {dbdicom-0.3.2.dist-info → dbdicom-0.3.3.dist-info}/top_level.txt +0 -0
dbdicom/api.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from typing import Union
|
|
1
2
|
|
|
2
3
|
import vreg
|
|
3
4
|
|
|
@@ -190,41 +191,36 @@ def move(from_entity:list, to_entity:list):
|
|
|
190
191
|
dbd.close()
|
|
191
192
|
|
|
192
193
|
|
|
193
|
-
def volume(series:list, dims:list=None
|
|
194
|
+
def volume(series:list, dims:list=None) -> vreg.Volume3D:
|
|
194
195
|
"""Read a vreg.Volume3D from a DICOM series
|
|
195
196
|
|
|
196
197
|
Args:
|
|
197
198
|
series (list): DICOM series to read
|
|
198
199
|
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
199
|
-
multislice (bool, optional): Whether the data are to be read
|
|
200
|
-
as multislice or not. In multislice data the voxel size
|
|
201
|
-
is taken from the slice gap rather thsan the slice thickness. Defaults to False.
|
|
202
200
|
|
|
203
201
|
Returns:
|
|
204
202
|
vreg.Volume3D: vole read from the series.
|
|
205
203
|
"""
|
|
204
|
+
if isinstance(series, str):
|
|
205
|
+
series = [series]
|
|
206
206
|
dbd = open(series[0])
|
|
207
|
-
vol = dbd.volume(series, dims
|
|
207
|
+
vol = dbd.volume(series, dims)
|
|
208
208
|
dbd.close()
|
|
209
209
|
return vol
|
|
210
210
|
|
|
211
|
-
def write_volume(vol:vreg.Volume3D, series:list, ref:list=None
|
|
212
|
-
multislice=False):
|
|
211
|
+
def write_volume(vol:Union[vreg.Volume3D, tuple], series:list, ref:list=None):
|
|
213
212
|
"""Write a vreg.Volume3D to a DICOM series
|
|
214
213
|
|
|
215
214
|
Args:
|
|
216
|
-
vol (vreg.Volume3D): Volume to write to the series.
|
|
215
|
+
vol (vreg.Volume3D or tuple): Volume to write to the series.
|
|
217
216
|
series (list): DICOM series to read
|
|
218
217
|
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
219
|
-
multislice (bool, optional): Whether the data are to be read
|
|
220
|
-
as multislice or not. In multislice data the voxel size
|
|
221
|
-
is taken from the slice gap rather thsan the slice thickness. Defaults to False.
|
|
222
218
|
"""
|
|
223
219
|
dbd = open(series[0])
|
|
224
|
-
dbd.write_volume(vol, series, ref
|
|
220
|
+
dbd.write_volume(vol, series, ref)
|
|
225
221
|
dbd.close()
|
|
226
222
|
|
|
227
|
-
def to_nifti(series:list, file:str, dims:list=None
|
|
223
|
+
def to_nifti(series:list, file:str, dims:list=None):
|
|
228
224
|
"""Save a DICOM series in nifti format.
|
|
229
225
|
|
|
230
226
|
Args:
|
|
@@ -232,27 +228,21 @@ def to_nifti(series:list, file:str, dims:list=None, multislice=False):
|
|
|
232
228
|
file (str): file path of the nifti file.
|
|
233
229
|
dims (list, optional): Non-spatial dimensions of the volume.
|
|
234
230
|
Defaults to None.
|
|
235
|
-
multislice (bool, optional): Whether the data are to be read
|
|
236
|
-
as multislice or not. In multislice data the voxel size
|
|
237
|
-
is taken from the slice gap rather thaan the slice thickness. Defaults to False.
|
|
238
231
|
"""
|
|
239
232
|
dbd = open(series[0])
|
|
240
|
-
dbd.to_nifti(series, file, dims
|
|
233
|
+
dbd.to_nifti(series, file, dims)
|
|
241
234
|
dbd.close()
|
|
242
235
|
|
|
243
|
-
def from_nifti(file:str, series:list, ref:list=None
|
|
236
|
+
def from_nifti(file:str, series:list, ref:list=None):
|
|
244
237
|
"""Create a DICOM series from a nifti file.
|
|
245
238
|
|
|
246
239
|
Args:
|
|
247
240
|
file (str): file path of the nifti file.
|
|
248
241
|
series (list): DICOM series to create
|
|
249
242
|
ref (list): DICOM series to use as template.
|
|
250
|
-
multislice (bool, optional): Whether the data are to be written
|
|
251
|
-
as multislice or not. In multislice data the voxel size
|
|
252
|
-
is written in the slice gap rather thaan the slice thickness. Defaults to False.
|
|
253
243
|
"""
|
|
254
244
|
dbd = open(series[0])
|
|
255
|
-
dbd.from_nifti(file, series, ref
|
|
245
|
+
dbd.from_nifti(file, series, ref)
|
|
256
246
|
dbd.close()
|
|
257
247
|
|
|
258
248
|
def pixel_data(series:list, dims:list=None, include:list=None) -> tuple:
|
|
@@ -270,6 +260,8 @@ def pixel_data(series:list, dims:list=None, include:list=None) -> tuple:
|
|
|
270
260
|
is provide these are returned as a dictionary in a third
|
|
271
261
|
return value.
|
|
272
262
|
"""
|
|
263
|
+
if isinstance(series, str):
|
|
264
|
+
series = [series]
|
|
273
265
|
dbd = open(series[0])
|
|
274
266
|
array = dbd.pixel_data(series, dims, include)
|
|
275
267
|
dbd.close()
|
dbdicom/database.py
CHANGED
|
@@ -20,6 +20,7 @@ COLUMNS = [
|
|
|
20
20
|
'PatientName',
|
|
21
21
|
'StudyDescription',
|
|
22
22
|
'StudyDate',
|
|
23
|
+
'StudyID',
|
|
23
24
|
'SeriesDescription',
|
|
24
25
|
'SeriesNumber',
|
|
25
26
|
'InstanceNumber',
|
|
@@ -77,6 +78,7 @@ def _multiframe_to_singleframe(path, df):
|
|
|
77
78
|
|
|
78
79
|
def _tree(df):
|
|
79
80
|
# A human-readable summary tree
|
|
81
|
+
# TODO: Add version number
|
|
80
82
|
|
|
81
83
|
df.sort_values(['PatientID','StudyInstanceUID','SeriesNumber'], inplace=True)
|
|
82
84
|
df = df.fillna('None')
|
|
@@ -94,10 +96,12 @@ def _tree(df):
|
|
|
94
96
|
for uid_study in df_patient.StudyInstanceUID.unique():
|
|
95
97
|
df_study = df_patient[df_patient.StudyInstanceUID == uid_study]
|
|
96
98
|
study_desc = df_study.StudyDescription.values[0]
|
|
99
|
+
study_id = df_study.StudyID.values[0]
|
|
97
100
|
study_date = df_study.StudyDate.values[0]
|
|
98
101
|
study = {
|
|
99
102
|
'StudyDescription': study_desc,
|
|
100
103
|
'StudyDate': study_date,
|
|
104
|
+
'StudyID': study_id,
|
|
101
105
|
'StudyInstanceUID': uid_study,
|
|
102
106
|
'series': [],
|
|
103
107
|
}
|
dbdicom/dataset.py
CHANGED
|
@@ -229,7 +229,8 @@ def write(ds, file, status=None):
|
|
|
229
229
|
dir = os.path.dirname(file)
|
|
230
230
|
if not os.path.exists(dir):
|
|
231
231
|
os.makedirs(dir)
|
|
232
|
-
ds.save_as(file, write_like_original=False)
|
|
232
|
+
#ds.save_as(file, write_like_original=False) # deprecated
|
|
233
|
+
ds.save_as(file, enforce_file_format=True)
|
|
233
234
|
|
|
234
235
|
|
|
235
236
|
def codify(source_file, save_file, **kwargs):
|
|
@@ -513,32 +514,24 @@ def set_lut(ds, RGB):
|
|
|
513
514
|
|
|
514
515
|
|
|
515
516
|
|
|
516
|
-
def affine(ds
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
)
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
get_values(ds, 'ImagePositionPatient'),
|
|
528
|
-
get_values(ds, 'PixelSpacing'),
|
|
529
|
-
get_values(ds, 'SliceThickness'),
|
|
530
|
-
)
|
|
517
|
+
def affine(ds):
|
|
518
|
+
# Spacing Between Slices is not required so can be absent
|
|
519
|
+
slice_spacing = ds.get("SpacingBetweenSlices")
|
|
520
|
+
if slice_spacing is None:
|
|
521
|
+
slice_spacing = ds.get("SliceThickness")
|
|
522
|
+
return image.affine_matrix(
|
|
523
|
+
get_values(ds, 'ImageOrientationPatient'),
|
|
524
|
+
get_values(ds, 'ImagePositionPatient'),
|
|
525
|
+
get_values(ds, 'PixelSpacing'),
|
|
526
|
+
slice_spacing,
|
|
527
|
+
)
|
|
531
528
|
|
|
532
|
-
|
|
533
|
-
def set_affine(ds, affine, multislice=False):
|
|
529
|
+
def set_affine(ds, affine):
|
|
534
530
|
if affine is None:
|
|
535
531
|
raise ValueError('The affine cannot be set to an empty value')
|
|
536
532
|
v = image.dismantle_affine_matrix(affine)
|
|
537
533
|
set_values(ds, 'PixelSpacing', v['PixelSpacing'])
|
|
538
|
-
|
|
539
|
-
set_values(ds, 'SpacingBetweenSlices', v['SliceThickness'])
|
|
540
|
-
else:
|
|
541
|
-
set_values(ds, 'SliceThickness', v['SliceThickness'])
|
|
534
|
+
set_values(ds, 'SpacingBetweenSlices', v['SpacingBetweenSlices'])
|
|
542
535
|
set_values(ds, 'ImageOrientationPatient', v['ImageOrientationPatient'])
|
|
543
536
|
set_values(ds, 'ImagePositionPatient', v['ImagePositionPatient'])
|
|
544
537
|
set_values(ds, 'SliceLocation', np.dot(v['ImagePositionPatient'], v['slice_cosine']))
|
|
@@ -602,10 +595,10 @@ def set_pixel_data(ds, array):
|
|
|
602
595
|
ds.PixelData = array.tobytes()
|
|
603
596
|
|
|
604
597
|
|
|
605
|
-
def volume(ds
|
|
606
|
-
return vreg.volume(pixel_data(ds), affine(ds
|
|
598
|
+
def volume(ds):
|
|
599
|
+
return vreg.volume(pixel_data(ds), affine(ds))
|
|
607
600
|
|
|
608
|
-
def set_volume(ds, volume:vreg.Volume3D
|
|
601
|
+
def set_volume(ds, volume:vreg.Volume3D):
|
|
609
602
|
if volume is None:
|
|
610
603
|
raise ValueError('The volume cannot be set to an empty value.')
|
|
611
604
|
try:
|
|
@@ -621,7 +614,7 @@ def set_volume(ds, volume:vreg.Volume3D, multislice=False):
|
|
|
621
614
|
if image.ndim != 2:
|
|
622
615
|
raise ValueError("Can only write 2D images to a dataset.")
|
|
623
616
|
set_pixel_data(ds, image)
|
|
624
|
-
set_affine(ds, volume.affine
|
|
617
|
+
set_affine(ds, volume.affine)
|
|
625
618
|
if volume.coords is not None:
|
|
626
619
|
# All other dimensions should have size 1
|
|
627
620
|
coords = volume.coords.reshape((volume.coords.shape[0], -1))
|
dbdicom/dbd.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
import json
|
|
4
|
+
from typing import Union
|
|
4
5
|
|
|
5
6
|
from tqdm import tqdm
|
|
6
7
|
import numpy as np
|
|
@@ -143,6 +144,11 @@ class DataBaseDicom():
|
|
|
143
144
|
for patient in self.patients():
|
|
144
145
|
studies += self.studies(patient, name, contains, isin)
|
|
145
146
|
return studies
|
|
147
|
+
elif len(entity)==1:
|
|
148
|
+
studies = []
|
|
149
|
+
for patient in self.patients():
|
|
150
|
+
studies += self.studies(patient, name, contains, isin)
|
|
151
|
+
return studies
|
|
146
152
|
else:
|
|
147
153
|
return register.studies(self.register, entity, name, contains, isin)
|
|
148
154
|
|
|
@@ -172,6 +178,11 @@ class DataBaseDicom():
|
|
|
172
178
|
for study in self.studies(entity):
|
|
173
179
|
series += self.series(study, name, contains, isin)
|
|
174
180
|
return series
|
|
181
|
+
elif len(entity)==1:
|
|
182
|
+
series = []
|
|
183
|
+
for study in self.studies(entity):
|
|
184
|
+
series += self.series(study, name, contains, isin)
|
|
185
|
+
return series
|
|
175
186
|
elif len(entity)==2:
|
|
176
187
|
series = []
|
|
177
188
|
for study in self.studies(entity):
|
|
@@ -181,20 +192,20 @@ class DataBaseDicom():
|
|
|
181
192
|
return register.series(self.register, entity, name, contains, isin)
|
|
182
193
|
|
|
183
194
|
|
|
184
|
-
def volume(self, series:list, dims:list=None
|
|
195
|
+
def volume(self, series:list, dims:list=None) -> vreg.Volume3D:
|
|
185
196
|
"""Read a vreg.Volume3D from a DICOM series
|
|
186
197
|
|
|
187
198
|
Args:
|
|
188
199
|
series (list): DICOM series to read
|
|
189
200
|
dims (list, optional): Non-spatial dimensions of the volume. Defaults to None.
|
|
190
|
-
multislice (bool, optional): Whether the data are to be read
|
|
191
|
-
as multislice or not. In multislice data the voxel size
|
|
192
|
-
is taken from the slice gap rather thsan the slice thickness. Defaults to False.
|
|
193
201
|
|
|
194
202
|
Returns:
|
|
195
203
|
vreg.Volume3D: vole read from the series.
|
|
196
204
|
"""
|
|
197
|
-
|
|
205
|
+
if isinstance(series, str): # path to folder
|
|
206
|
+
return [self.volume(s, dims) for s in self.series(series)]
|
|
207
|
+
if len(series) < 4: # folder, patient or study
|
|
208
|
+
return [self.volume(s, dims) for s in self.series(series)]
|
|
198
209
|
if dims is None:
|
|
199
210
|
dims = []
|
|
200
211
|
elif isinstance(dims, str):
|
|
@@ -211,7 +222,7 @@ class DataBaseDicom():
|
|
|
211
222
|
for f in tqdm(files, desc='Reading volume..'):
|
|
212
223
|
ds = dbdataset.read_dataset(f)
|
|
213
224
|
values.append(dbdataset.get_values(ds, dims))
|
|
214
|
-
volumes.append(dbdataset.volume(ds
|
|
225
|
+
volumes.append(dbdataset.volume(ds))
|
|
215
226
|
|
|
216
227
|
# Format as mesh
|
|
217
228
|
coords = np.stack(values, axis=-1)
|
|
@@ -229,9 +240,21 @@ class DataBaseDicom():
|
|
|
229
240
|
"firstslice=True, the coordinates of the lowest "
|
|
230
241
|
"slice will be assigned to the whole volume."
|
|
231
242
|
)
|
|
243
|
+
|
|
244
|
+
# Infer spacing between slices from slice locations
|
|
245
|
+
# Technically only necessary if SpacingBetweenSlices not set or incorrect
|
|
246
|
+
vols = infer_slice_spacing(vols)
|
|
232
247
|
|
|
233
248
|
# Join 2D volumes into 3D volumes
|
|
234
|
-
|
|
249
|
+
try:
|
|
250
|
+
vol = vreg.join(vols)
|
|
251
|
+
except ValueError:
|
|
252
|
+
# some vendors define the slice vector as -cross product
|
|
253
|
+
# of row and column vector. Check if that solves the issue.
|
|
254
|
+
for v in vols.reshape(-1):
|
|
255
|
+
v.affine[:3,2] = -v.affine[:3,2]
|
|
256
|
+
# Then try again
|
|
257
|
+
vol = vreg.join(vols)
|
|
235
258
|
if vol.ndim > 3:
|
|
236
259
|
vol.set_coords(c0)
|
|
237
260
|
vol.set_dims(dims[1:])
|
|
@@ -239,19 +262,18 @@ class DataBaseDicom():
|
|
|
239
262
|
|
|
240
263
|
|
|
241
264
|
def write_volume(
|
|
242
|
-
self, vol:vreg.Volume3D, series:list,
|
|
243
|
-
ref:list=None,
|
|
265
|
+
self, vol:Union[vreg.Volume3D, tuple], series:list,
|
|
266
|
+
ref:list=None,
|
|
244
267
|
):
|
|
245
268
|
"""Write a vreg.Volume3D to a DICOM series
|
|
246
269
|
|
|
247
270
|
Args:
|
|
248
271
|
vol (vreg.Volume3D): Volume to write to the series.
|
|
249
272
|
series (list): DICOM series to read
|
|
250
|
-
|
|
251
|
-
multislice (bool, optional): Whether the data are to be read
|
|
252
|
-
as multislice or not. In multislice data the voxel size
|
|
253
|
-
is taken from the slice gap rather than the slice thickness. Defaults to False.
|
|
273
|
+
ref (list): Reference series
|
|
254
274
|
"""
|
|
275
|
+
if isinstance(vol, tuple):
|
|
276
|
+
vol = vreg.volume(vol[0], vol[1])
|
|
255
277
|
if ref is None:
|
|
256
278
|
ds = dbdataset.new_dataset('MRImage')
|
|
257
279
|
#ds = dbdataset.new_dataset('ParametricMap')
|
|
@@ -270,21 +292,21 @@ class DataBaseDicom():
|
|
|
270
292
|
if vol.ndim==3:
|
|
271
293
|
slices = vol.split()
|
|
272
294
|
for i, sl in tqdm(enumerate(slices), desc='Writing volume..'):
|
|
273
|
-
dbdataset.set_volume(ds, sl
|
|
295
|
+
dbdataset.set_volume(ds, sl)
|
|
274
296
|
self._write_dataset(ds, attr, n + 1 + i)
|
|
275
297
|
else:
|
|
276
298
|
i=0
|
|
277
299
|
vols = vol.separate().reshape(-1)
|
|
278
300
|
for vt in tqdm(vols, desc='Writing volume..'):
|
|
279
301
|
for sl in vt.split():
|
|
280
|
-
dbdataset.set_volume(ds, sl
|
|
302
|
+
dbdataset.set_volume(ds, sl)
|
|
281
303
|
dbdataset.set_value(ds, sl.dims, sl.coords[:,...])
|
|
282
304
|
self._write_dataset(ds, attr, n + 1 + i)
|
|
283
305
|
i+=1
|
|
284
306
|
return self
|
|
285
307
|
|
|
286
308
|
|
|
287
|
-
def to_nifti(self, series:list, file:str, dims=None
|
|
309
|
+
def to_nifti(self, series:list, file:str, dims=None):
|
|
288
310
|
"""Save a DICOM series in nifti format.
|
|
289
311
|
|
|
290
312
|
Args:
|
|
@@ -292,34 +314,31 @@ class DataBaseDicom():
|
|
|
292
314
|
file (str): file path of the nifti file.
|
|
293
315
|
dims (list, optional): Non-spatial dimensions of the volume.
|
|
294
316
|
Defaults to None.
|
|
295
|
-
multislice (bool, optional): Whether the data are to be read
|
|
296
|
-
as multislice or not. In multislice data the voxel size
|
|
297
|
-
is taken from the slice gap rather thaan the slice thickness. Defaults to False.
|
|
298
317
|
"""
|
|
299
|
-
vol = self.volume(series, dims
|
|
318
|
+
vol = self.volume(series, dims)
|
|
300
319
|
vreg.write_nifti(vol, file)
|
|
301
320
|
return self
|
|
302
321
|
|
|
303
|
-
def from_nifti(self, file:str, series:list, ref:list=None
|
|
322
|
+
def from_nifti(self, file:str, series:list, ref:list=None):
|
|
304
323
|
"""Create a DICOM series from a nifti file.
|
|
305
324
|
|
|
306
325
|
Args:
|
|
307
326
|
file (str): file path of the nifti file.
|
|
308
327
|
series (list): DICOM series to create
|
|
309
328
|
ref (list): DICOM series to use as template.
|
|
310
|
-
multislice (bool, optional): Whether the data are to be written
|
|
311
|
-
as multislice or not. In multislice data the voxel size
|
|
312
|
-
is written in the slice gap rather thaan the slice thickness. Defaults to False.
|
|
313
329
|
"""
|
|
314
330
|
vol = vreg.read_nifti(file)
|
|
315
|
-
self.write_volume(vol, series, ref
|
|
331
|
+
self.write_volume(vol, series, ref)
|
|
316
332
|
return self
|
|
317
333
|
|
|
318
334
|
def pixel_data(self, series:list, dims:list=None, coords=False, include=None) -> np.ndarray:
|
|
319
335
|
"""Read the pixel data from a DICOM series
|
|
320
336
|
|
|
321
337
|
Args:
|
|
322
|
-
series (list): DICOM series to read
|
|
338
|
+
series (list or str): DICOM series to read. This can also
|
|
339
|
+
be a path to a folder containing DICOM files, or a
|
|
340
|
+
patient or study to read all series in that patient or
|
|
341
|
+
study. In those cases a list is returned.
|
|
323
342
|
dims (list, optional): Dimensions of the array.
|
|
324
343
|
coords (bool): If set to Trye, the coordinates of the
|
|
325
344
|
arrays are returned alongside the pixel data
|
|
@@ -332,8 +351,12 @@ class DataBaseDicom():
|
|
|
332
351
|
coords is set these are returned too as an array with
|
|
333
352
|
coordinates of the slices according to dims. If include
|
|
334
353
|
is provide the values are returned as a dictionary in the last
|
|
335
|
-
return value.
|
|
354
|
+
return value.
|
|
336
355
|
"""
|
|
356
|
+
if isinstance(series, str): # path to folder
|
|
357
|
+
return [self.pixel_data(s, dims, coords, include) for s in self.series(series)]
|
|
358
|
+
if len(series) < 4: # folder, patient or study
|
|
359
|
+
return [self.pixel_data(s, dims, coords, include) for s in self.series(series)]
|
|
337
360
|
if coords:
|
|
338
361
|
if dims is None:
|
|
339
362
|
raise ValueError(
|
|
@@ -529,13 +552,19 @@ class DataBaseDicom():
|
|
|
529
552
|
ds = dbdataset.read_dataset(f)
|
|
530
553
|
self._write_dataset(ds, attr, n + 1 + i)
|
|
531
554
|
|
|
532
|
-
|
|
555
|
+
def _max_study_id(self, patient_id):
|
|
556
|
+
for pt in self.register:
|
|
557
|
+
if pt['PatientID'] == patient_id:
|
|
558
|
+
n = [int(st['StudyID']) for st in pt['studies']]
|
|
559
|
+
return int(np.amax(n))
|
|
560
|
+
return 0
|
|
561
|
+
|
|
533
562
|
def _max_series_number(self, study_uid):
|
|
534
563
|
for pt in self.register:
|
|
535
564
|
for st in pt['studies']:
|
|
536
565
|
if st['StudyInstanceUID'] == study_uid:
|
|
537
|
-
n = [sr['SeriesNumber'] for sr in st['
|
|
538
|
-
return np.amax(n)
|
|
566
|
+
n = [sr['SeriesNumber'] for sr in st['series']]
|
|
567
|
+
return int(np.amax(n))
|
|
539
568
|
return 0
|
|
540
569
|
|
|
541
570
|
def _max_instance_number(self, series_uid):
|
|
@@ -544,7 +573,7 @@ class DataBaseDicom():
|
|
|
544
573
|
for sr in st['series']:
|
|
545
574
|
if sr['SeriesInstanceUID'] == series_uid:
|
|
546
575
|
n = list(sr['instances'].keys())
|
|
547
|
-
return np.amax([int(i) for i in n])
|
|
576
|
+
return int(np.amax([int(i) for i in n]))
|
|
548
577
|
return 0
|
|
549
578
|
|
|
550
579
|
def _attributes(self, entity):
|
|
@@ -566,7 +595,8 @@ class DataBaseDicom():
|
|
|
566
595
|
except:
|
|
567
596
|
# If the patient does not exist, generate values
|
|
568
597
|
attr = ['PatientID', 'PatientName']
|
|
569
|
-
patient_id = dbdataset.new_uid()
|
|
598
|
+
#patient_id = dbdataset.new_uid()
|
|
599
|
+
patient_id = patient[-1] if isinstance(patient[-1], str) else f"{patient[-1][0]}_{patient[-1][1]}"
|
|
570
600
|
patient_name = patient[-1] if isinstance(patient[-1], str) else patient[-1][0]
|
|
571
601
|
vals = [patient_id, patient_name]
|
|
572
602
|
return {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
|
|
@@ -581,12 +611,18 @@ class DataBaseDicom():
|
|
|
581
611
|
ds = dbdataset.read_dataset(files[0])
|
|
582
612
|
vals = dbdataset.get_values(ds, attr)
|
|
583
613
|
except:
|
|
584
|
-
# If the study does not exist, generate values
|
|
585
|
-
|
|
586
|
-
|
|
614
|
+
# If the study does not exist or is empty, generate values
|
|
615
|
+
try:
|
|
616
|
+
patient_id = register.uid(self.register, study[:-1])
|
|
617
|
+
except:
|
|
618
|
+
study_id = 1
|
|
619
|
+
else:
|
|
620
|
+
study_id = 1 + self._max_study_id(patient_id)
|
|
621
|
+
attr = ['StudyInstanceUID', 'StudyDescription', 'StudyDate', 'StudyID']
|
|
622
|
+
study_uid = dbdataset.new_uid()
|
|
587
623
|
study_desc = study[-1] if isinstance(study[-1], str) else study[-1][0]
|
|
588
624
|
study_date = datetime.today().strftime('%Y%m%d')
|
|
589
|
-
vals = [
|
|
625
|
+
vals = [study_uid, study_desc, study_date, str(study_id)]
|
|
590
626
|
return patient_attr | {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
|
|
591
627
|
|
|
592
628
|
|
|
@@ -607,9 +643,9 @@ class DataBaseDicom():
|
|
|
607
643
|
else:
|
|
608
644
|
series_number = 1 + self._max_series_number(study_uid)
|
|
609
645
|
attr = ['SeriesInstanceUID', 'SeriesDescription', 'SeriesNumber']
|
|
610
|
-
|
|
646
|
+
series_uid = dbdataset.new_uid()
|
|
611
647
|
series_desc = series[-1] if isinstance(series[-1], str) else series[-1][0]
|
|
612
|
-
vals = [
|
|
648
|
+
vals = [series_uid, series_desc, int(series_number)]
|
|
613
649
|
return study_attr | {attr[i]:vals[i] for i in range(len(attr)) if vals[i] is not None}
|
|
614
650
|
|
|
615
651
|
|
|
@@ -619,7 +655,13 @@ class DataBaseDicom():
|
|
|
619
655
|
attr['InstanceNumber'] = str(instance_nr)
|
|
620
656
|
dbdataset.set_values(ds, list(attr.keys()), list(attr.values()))
|
|
621
657
|
# Save results in a new file
|
|
622
|
-
|
|
658
|
+
rel_dir = os.path.join(
|
|
659
|
+
f"patient_{attr['PatientID']}",
|
|
660
|
+
f"study_[{attr['StudyID']}]_{attr['StudyDescription']}",
|
|
661
|
+
f"series_[{attr['SeriesNumber']}]_{attr['SeriesDescription']}",
|
|
662
|
+
)
|
|
663
|
+
os.makedirs(os.path.join(self.path, rel_dir), exist_ok=True)
|
|
664
|
+
rel_path = os.path.join(rel_dir, dbdataset.new_uid() + '.dcm')
|
|
623
665
|
dbdataset.write(ds, os.path.join(self.path, rel_path))
|
|
624
666
|
# Add an entry in the register
|
|
625
667
|
register.add_instance(self.register, attr, rel_path)
|
|
@@ -656,6 +698,59 @@ class DataBaseDicom():
|
|
|
656
698
|
# self.register.drop('SOPClassUID', axis=1, inplace=True)
|
|
657
699
|
|
|
658
700
|
|
|
701
|
+
def infer_slice_spacing(vols):
|
|
702
|
+
# In case spacing between slices is not (correctly) encoded in
|
|
703
|
+
# DICOM it can be inferred from the slice locations.
|
|
704
|
+
|
|
705
|
+
shape = vols.shape
|
|
706
|
+
vols = vols.reshape((shape[0], -1))
|
|
707
|
+
slice_spacing = np.zeros(vols.shape[-1])
|
|
708
|
+
|
|
709
|
+
for d in range(vols.shape[-1]):
|
|
710
|
+
|
|
711
|
+
# For single slice volumes there is nothing to do
|
|
712
|
+
if vols[:,d].shape[0]==1:
|
|
713
|
+
continue
|
|
714
|
+
|
|
715
|
+
# Get a normal slice vector from the first volume.
|
|
716
|
+
mat = vols[0,d].affine[:3,:3]
|
|
717
|
+
normal = mat[:,2]/np.linalg.norm(mat[:,2])
|
|
718
|
+
|
|
719
|
+
# Get slice locations by projection on the normal.
|
|
720
|
+
pos = [v.affine[:3,3] for v in vols[:,d]]
|
|
721
|
+
slice_loc = [np.dot(p, normal) for p in pos]
|
|
722
|
+
|
|
723
|
+
# Sort slice locations and take consecutive differences.
|
|
724
|
+
slice_loc = np.sort(slice_loc)
|
|
725
|
+
distances = slice_loc[1:] - slice_loc[:-1]
|
|
726
|
+
|
|
727
|
+
# Round to micrometer and check if unique
|
|
728
|
+
distances = np.around(distances, 3)
|
|
729
|
+
slice_spacing_d = np.unique(distances)
|
|
730
|
+
|
|
731
|
+
# Check if unique - otherwise this is not a volume
|
|
732
|
+
if len(slice_spacing_d) > 1:
|
|
733
|
+
raise ValueError(
|
|
734
|
+
'Cannot build a volume - spacings between slices are not unique.'
|
|
735
|
+
)
|
|
736
|
+
else:
|
|
737
|
+
slice_spacing_d= slice_spacing_d[0]
|
|
738
|
+
|
|
739
|
+
# Set correct slice spacing in all volumes
|
|
740
|
+
for v in vols[:,d]:
|
|
741
|
+
v.affine[:3,2] = normal * abs(slice_spacing_d)
|
|
742
|
+
|
|
743
|
+
slice_spacing[d] = slice_spacing_d
|
|
744
|
+
|
|
745
|
+
# Check slice_spacing is the same across dimensions
|
|
746
|
+
slice_spacing = np.unique(slice_spacing)
|
|
747
|
+
if len(slice_spacing) > 1:
|
|
748
|
+
raise ValueError(
|
|
749
|
+
'Cannot build a volume - spacings between slices are not unique.'
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
return vols.reshape(shape)
|
|
753
|
+
|
|
659
754
|
|
|
660
755
|
|
|
661
756
|
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
dbdicom/register.py
CHANGED
|
@@ -21,6 +21,7 @@ def add_instance(dbtree:list, instance, rel_path):
|
|
|
21
21
|
st = {
|
|
22
22
|
'StudyDescription': instance['StudyDescription'],
|
|
23
23
|
'StudyDate': instance['StudyDate'],
|
|
24
|
+
'StudyID': instance['StudyID'],
|
|
24
25
|
'StudyInstanceUID': instance['StudyInstanceUID'],
|
|
25
26
|
'series': [],
|
|
26
27
|
}
|
|
@@ -186,7 +187,7 @@ def _patient_uid(dbtree, patient):
|
|
|
186
187
|
f"Please specify the index in the call to patient_uid(). "
|
|
187
188
|
f"For instance ({patient}, {len(patients)-1})'. "
|
|
188
189
|
)
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
|
|
191
192
|
|
|
192
193
|
def _study_uid(dbtree, study):
|
dbdicom/sop_classes/mr_image.py
CHANGED
|
@@ -23,33 +23,54 @@ def pixel_data(ds):
|
|
|
23
23
|
if [0x2005, 0x100E] in ds: # 'Philips Rescale Slope'
|
|
24
24
|
slope = ds[(0x2005, 0x100E)].value
|
|
25
25
|
intercept = ds[(0x2005, 0x100D)].value
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
if (intercept == 0) and (slope == 1):
|
|
27
|
+
array = array.astype(np.int16)
|
|
28
|
+
else:
|
|
29
|
+
array = array.astype(np.float32)
|
|
30
|
+
array -= intercept
|
|
31
|
+
array /= slope
|
|
28
32
|
else:
|
|
29
33
|
slope = float(getattr(ds, 'RescaleSlope', 1))
|
|
30
34
|
intercept = float(getattr(ds, 'RescaleIntercept', 0))
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
if (intercept == 0) and (slope == 1):
|
|
36
|
+
array = array.astype(np.int16)
|
|
37
|
+
else:
|
|
38
|
+
array = array.astype(np.float32)
|
|
39
|
+
array *= slope
|
|
40
|
+
array += intercept
|
|
33
41
|
return np.transpose(array)
|
|
34
42
|
|
|
35
43
|
|
|
36
44
|
def set_pixel_data(ds, array):
|
|
37
45
|
|
|
46
|
+
# Delete 'Philips Rescale Slope'
|
|
38
47
|
if (0x2005, 0x100E) in ds:
|
|
39
|
-
del ds[0x2005, 0x100E]
|
|
48
|
+
del ds[0x2005, 0x100E]
|
|
40
49
|
if (0x2005, 0x100D) in ds:
|
|
41
50
|
del ds[0x2005, 0x100D]
|
|
42
51
|
|
|
52
|
+
ds.BitsAllocated = 16
|
|
53
|
+
ds.BitsStored = 16
|
|
54
|
+
ds.HighBit = 15
|
|
55
|
+
|
|
43
56
|
# clipping may slow down a lot
|
|
44
|
-
array = image.clip(array.astype(np.float32))
|
|
45
|
-
array
|
|
46
|
-
array
|
|
57
|
+
#array = image.clip(array.astype(np.float32))
|
|
58
|
+
array = image.clip(array) # remove nan and infs
|
|
59
|
+
if array.dtype==np.int16:
|
|
60
|
+
ds.PixelRepresentation = 1
|
|
61
|
+
ds.RescaleSlope = 1
|
|
62
|
+
ds.RescaleIntercept = 0
|
|
63
|
+
elif array.dtype==np.uint16:
|
|
64
|
+
ds.PixelRepresentation = 0
|
|
65
|
+
ds.RescaleSlope = 1
|
|
66
|
+
ds.RescaleIntercept = 0
|
|
67
|
+
else:
|
|
68
|
+
array, slope, intercept = image.scale_to_range(array, ds.BitsStored)
|
|
69
|
+
ds.PixelRepresentation = 0
|
|
70
|
+
ds.RescaleSlope = 1 / slope
|
|
71
|
+
ds.RescaleIntercept = - intercept / slope
|
|
47
72
|
|
|
48
|
-
|
|
49
|
-
ds.RescaleSlope = 1 / slope
|
|
50
|
-
ds.RescaleIntercept = - intercept / slope
|
|
51
|
-
# ds.WindowCenter = (maximum + minimum) / 2
|
|
52
|
-
# ds.WindowWidth = maximum - minimum
|
|
73
|
+
array = np.transpose(array)
|
|
53
74
|
ds.Rows = array.shape[0]
|
|
54
75
|
ds.Columns = array.shape[1]
|
|
55
76
|
ds.PixelData = array.tobytes()
|
dbdicom/utils/image.py
CHANGED
|
@@ -54,14 +54,13 @@ def dismantle_affine_matrix(affine):
|
|
|
54
54
|
# ImagePositionPatient_i = ImagePositionPatient + i * SpacingBetweenSlices * slice_cosine
|
|
55
55
|
column_spacing = np.linalg.norm(affine[:3, 0])
|
|
56
56
|
row_spacing = np.linalg.norm(affine[:3, 1])
|
|
57
|
-
|
|
57
|
+
slice_spacing = np.linalg.norm(affine[:3, 2])
|
|
58
58
|
row_cosine = affine[:3, 0] / column_spacing
|
|
59
59
|
column_cosine = affine[:3, 1] / row_spacing
|
|
60
|
-
slice_cosine = affine[:3, 2] /
|
|
60
|
+
slice_cosine = affine[:3, 2] / slice_spacing
|
|
61
61
|
return {
|
|
62
62
|
'PixelSpacing': [row_spacing, column_spacing],
|
|
63
|
-
|
|
64
|
-
'SliceThickness': slice_thickness,
|
|
63
|
+
'SpacingBetweenSlices': slice_spacing,
|
|
65
64
|
'ImageOrientationPatient': row_cosine.tolist() + column_cosine.tolist(),
|
|
66
65
|
'ImagePositionPatient': affine[:3, 3].tolist(), # first slice for a volume
|
|
67
66
|
'slice_cosine': slice_cosine.tolist()}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dbdicom
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
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,21 +1,21 @@
|
|
|
1
1
|
dbdicom/__init__.py,sha256=DyogeTraV6o-FgWdBCbtVEaMmdkMQHkYkraDIE0t8OA,25
|
|
2
|
-
dbdicom/api.py,sha256=
|
|
2
|
+
dbdicom/api.py,sha256=MHJBHY_ikUj93T8Vt0GAjvjLFJL-2IdIFKhr8T-bvVI,8871
|
|
3
3
|
dbdicom/const.py,sha256=BqBiRRjeiSqDr1W6YvaayD8WKCjG4Cny2NT0GeLM6bI,4269
|
|
4
|
-
dbdicom/database.py,sha256=
|
|
5
|
-
dbdicom/dataset.py,sha256=
|
|
6
|
-
dbdicom/dbd.py,sha256=
|
|
7
|
-
dbdicom/register.py,sha256=
|
|
4
|
+
dbdicom/database.py,sha256=_LUbH7gc9l7j_63AC71DjwxgTUwbEjHSy5kuvRw75Hw,4764
|
|
5
|
+
dbdicom/dataset.py,sha256=hLAyFlN7zQ-dOzI9V67aHfTq3VtpvCI7_83tnBqXObE,21880
|
|
6
|
+
dbdicom/dbd.py,sha256=ZUXmLcQ2C6L9UTanGzTSte1XIJAcQoNNsEpweXW8N50,29921
|
|
7
|
+
dbdicom/register.py,sha256=VPxS2oTlONxx5eNdGRbvFeEfQo69jo5YDp7L_Vb4x28,23676
|
|
8
8
|
dbdicom/external/__init__.py,sha256=XNQqfspyf6vFGedXlRKZsUB8k8E-0W19Uamwn8Aioxo,316
|
|
9
|
-
dbdicom/external/__pycache__/__init__.cpython-311.pyc,sha256=
|
|
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
|
|
11
11
|
dbdicom/external/dcm4che/__init__.py,sha256=YwpeMCLrxffGOkchsGjgAuB6ia3VX_tx9Y7ru9EWtoY,35
|
|
12
|
-
dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc,sha256=
|
|
12
|
+
dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc,sha256=FB8wyWqXDUt_1P-QmE4yt9uD6dDm5YqYWjqVuRwGdSo,256
|
|
13
13
|
dbdicom/external/dcm4che/bin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
dbdicom/external/dcm4che/bin/deidentify,sha256=64MNIEpp-CWzFSb6TV0KtyCBvD7XyEsovRjBeyxDqSc,1698
|
|
15
15
|
dbdicom/external/dcm4che/bin/deidentify.bat,sha256=kVXUkcy1C4Y3KjC2NJwmmR0pufSJWmaof_LR5CTAxMg,1455
|
|
16
16
|
dbdicom/external/dcm4che/bin/emf2sf,sha256=svCzkZ-QhdVTV0NNHOpBiwNBMODVWZHJIFA7cWaN2bM,1622
|
|
17
17
|
dbdicom/external/dcm4che/bin/emf2sf.bat,sha256=Vh0ry9KNJX_WXcyCrLSxbJ_6Crot9rjmwi__u2GZqLY,1375
|
|
18
|
-
dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc,sha256=
|
|
18
|
+
dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc,sha256=GYcm47ETjYvRUN5RPTe5R-c0prd14GP8gm96eJcy0uQ,203
|
|
19
19
|
dbdicom/external/dcm4che/etc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
20
|
dbdicom/external/dcm4che/etc/emf2sf/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
21
|
dbdicom/external/dcm4che/etc/emf2sf/log4j.properties,sha256=3hHcBFt2oNRjvHtix5bfuEsnKfdv5IYOkbsyoY9g7cM,223
|
|
@@ -36,7 +36,7 @@ dbdicom/external/dcm4che/lib/windows-x86/opencv_java.dll,sha256=QanyzLy0Cd79-aOV
|
|
|
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
38
|
dbdicom/sop_classes/enhanced_mr_image.py,sha256=13j4EGXniBpJxpzzL3Xa4y3g5OKhMd5Ct7cjPGOYQY4,35496
|
|
39
|
-
dbdicom/sop_classes/mr_image.py,sha256=
|
|
39
|
+
dbdicom/sop_classes/mr_image.py,sha256=kNcrWXZ3VC3hhfqjMRjrlZOVqZH3Q5KfWXYLfLD-bEY,10913
|
|
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
|
|
@@ -45,10 +45,10 @@ dbdicom/sop_classes/xray_angiographic_image.py,sha256=nWysCGaEWKVNItnOgyJfcGMpS3
|
|
|
45
45
|
dbdicom/utils/arrays.py,sha256=wiqCczLXlNl0qIePVOwCYvbAJhPveNorplkhtGleS48,1121
|
|
46
46
|
dbdicom/utils/dcm4che.py,sha256=Vxq8NYWWK3BuqJkzhBQ89oMqzJlnxqTxgsgTo_Frznc,2317
|
|
47
47
|
dbdicom/utils/files.py,sha256=qhWNJqeWnRjDNbERpC6Mz962_TW9mFdvd2lnBbK3xt4,2259
|
|
48
|
-
dbdicom/utils/image.py,sha256=
|
|
48
|
+
dbdicom/utils/image.py,sha256=D46CD_ezpp2uq8VMqug5Z09fAyoJ9U6VwuxIFNJK8zg,4048
|
|
49
49
|
dbdicom/utils/variables.py,sha256=vUh5cDnmCft5hoXDYXUvfkg5Cy5WlgMAogU38Y_BKRo,5753
|
|
50
|
-
dbdicom-0.3.
|
|
51
|
-
dbdicom-0.3.
|
|
52
|
-
dbdicom-0.3.
|
|
53
|
-
dbdicom-0.3.
|
|
54
|
-
dbdicom-0.3.
|
|
50
|
+
dbdicom-0.3.3.dist-info/licenses/LICENSE,sha256=HrhfyXIkWY2tGFK11kg7vPCqhgh5DcxleloqdhrpyMY,11558
|
|
51
|
+
dbdicom-0.3.3.dist-info/METADATA,sha256=aMYhKPS907fOj0pgSkQh7vbFeYspNvJP5-ZYNZ6LpMs,1030
|
|
52
|
+
dbdicom-0.3.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
53
|
+
dbdicom-0.3.3.dist-info/top_level.txt,sha256=nJWxXg4YjD6QblfmhrzTMXcr8FSKNc0Yk-CAIDUsYkQ,8
|
|
54
|
+
dbdicom-0.3.3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|