dbdicom 0.2.6__py3-none-any.whl → 0.3.0__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/__init__.py +1 -28
- dbdicom/api.py +267 -0
- dbdicom/const.py +144 -0
- dbdicom/dataset.py +752 -0
- dbdicom/dbd.py +719 -0
- 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 +527 -0
- dbdicom/{ds/types → sop_classes}/ct_image.py +2 -16
- dbdicom/{ds/types → sop_classes}/enhanced_mr_image.py +153 -26
- dbdicom/{ds/types → sop_classes}/mr_image.py +185 -140
- dbdicom/sop_classes/parametric_map.py +307 -0
- dbdicom/sop_classes/secondary_capture.py +140 -0
- dbdicom/sop_classes/segmentation.py +311 -0
- dbdicom/{ds/types → sop_classes}/ultrasound_multiframe_image.py +1 -15
- dbdicom/{ds/types → sop_classes}/xray_angiographic_image.py +2 -17
- dbdicom/utils/arrays.py +36 -0
- dbdicom/utils/files.py +0 -20
- dbdicom/utils/image.py +10 -629
- dbdicom-0.3.0.dist-info/METADATA +28 -0
- dbdicom-0.3.0.dist-info/RECORD +53 -0
- dbdicom/create.py +0 -457
- dbdicom/dro.py +0 -174
- dbdicom/ds/__init__.py +0 -10
- dbdicom/ds/create.py +0 -63
- dbdicom/ds/dataset.py +0 -869
- dbdicom/ds/dictionaries.py +0 -620
- dbdicom/ds/types/parametric_map.py +0 -226
- dbdicom/extensions/__init__.py +0 -9
- dbdicom/extensions/dipy.py +0 -448
- dbdicom/extensions/elastix.py +0 -503
- dbdicom/extensions/matplotlib.py +0 -107
- dbdicom/extensions/numpy.py +0 -271
- dbdicom/extensions/scipy.py +0 -1512
- dbdicom/extensions/skimage.py +0 -1030
- dbdicom/extensions/sklearn.py +0 -243
- dbdicom/extensions/vreg.py +0 -1390
- dbdicom/manager.py +0 -2132
- dbdicom/message.py +0 -119
- dbdicom/pipelines.py +0 -66
- dbdicom/record.py +0 -1893
- dbdicom/types/database.py +0 -107
- dbdicom/types/instance.py +0 -231
- dbdicom/types/patient.py +0 -40
- dbdicom/types/series.py +0 -2874
- dbdicom/types/study.py +0 -58
- dbdicom-0.2.6.dist-info/METADATA +0 -72
- dbdicom-0.2.6.dist-info/RECORD +0 -66
- {dbdicom-0.2.6.dist-info → dbdicom-0.3.0.dist-info}/WHEEL +0 -0
- {dbdicom-0.2.6.dist-info → dbdicom-0.3.0.dist-info}/licenses/LICENSE +0 -0
- {dbdicom-0.2.6.dist-info → dbdicom-0.3.0.dist-info}/top_level.txt +0 -0
dbdicom/utils/image.py
CHANGED
|
@@ -1,415 +1,6 @@
|
|
|
1
|
-
import math
|
|
2
|
-
import numpy as np
|
|
3
|
-
from scipy.interpolate import interpn
|
|
4
|
-
from scipy.ndimage import affine_transform
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
def as_mosaic(array, rows=None):
|
|
8
|
-
"""Reformat a 3D array (x,y,z) into a 2D mosaic"""
|
|
9
|
-
|
|
10
|
-
nz = array.shape[2]
|
|
11
|
-
if rows is None:
|
|
12
|
-
rows = math.ceil(math.sqrt(nz))
|
|
13
|
-
cols = math.ceil(nz/rows)
|
|
14
|
-
mosaic = np.zeros((array.shape[0]*cols, array.shape[1]*rows))
|
|
15
|
-
for k in range(nz):
|
|
16
|
-
j = math.floor(k/cols)
|
|
17
|
-
i = k-j*cols
|
|
18
|
-
mosaic[
|
|
19
|
-
i*array.shape[0]:(i+1)*array.shape[0],
|
|
20
|
-
j*array.shape[1]:(j+1)*array.shape[1],
|
|
21
|
-
] = array[:,:,k]
|
|
22
|
-
return mosaic
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def ellipsoid(a, b, c, spacing=(1., 1., 1.), levelset=False):
|
|
28
|
-
"""
|
|
29
|
-
Generates ellipsoid with semimajor axes aligned with grid dimensions
|
|
30
|
-
on grid with specified `spacing`.
|
|
31
|
-
|
|
32
|
-
Parameters
|
|
33
|
-
----------
|
|
34
|
-
a : float
|
|
35
|
-
Length of semimajor axis aligned with x-axis.
|
|
36
|
-
b : float
|
|
37
|
-
Length of semimajor axis aligned with y-axis.
|
|
38
|
-
c : float
|
|
39
|
-
Length of semimajor axis aligned with z-axis.
|
|
40
|
-
spacing : tuple of floats, length 3
|
|
41
|
-
Spacing in (x, y, z) spatial dimensions.
|
|
42
|
-
levelset : bool
|
|
43
|
-
If True, returns the level set for this ellipsoid (signed level
|
|
44
|
-
set about zero, with positive denoting interior) as np.float64.
|
|
45
|
-
False returns a binarized version of said level set.
|
|
46
|
-
|
|
47
|
-
Returns
|
|
48
|
-
-------
|
|
49
|
-
ellip : (N, M, P) array
|
|
50
|
-
Ellipsoid centered in a correctly sized array for given `spacing`.
|
|
51
|
-
Boolean dtype unless `levelset=True`, in which case a float array is
|
|
52
|
-
returned with the level set above 0.0 representing the ellipsoid.
|
|
53
|
-
|
|
54
|
-
Note
|
|
55
|
-
----
|
|
56
|
-
This function is copy-pasted directly from skimage source code without modification - this to avoid bringing in skimage as an essential dependency.
|
|
57
|
-
|
|
58
|
-
"""
|
|
59
|
-
if (a <= 0) or (b <= 0) or (c <= 0):
|
|
60
|
-
raise ValueError('Parameters a, b, and c must all be > 0')
|
|
61
|
-
|
|
62
|
-
offset = np.r_[1, 1, 1] * np.r_[spacing]
|
|
63
|
-
|
|
64
|
-
# Calculate limits, and ensure output volume is odd & symmetric
|
|
65
|
-
low = np.ceil(- np.r_[a, b, c] - offset)
|
|
66
|
-
high = np.floor(np.r_[a, b, c] + offset + 1)
|
|
67
|
-
|
|
68
|
-
for dim in range(3):
|
|
69
|
-
if (high[dim] - low[dim]) % 2 == 0:
|
|
70
|
-
low[dim] -= 1
|
|
71
|
-
num = np.arange(low[dim], high[dim], spacing[dim])
|
|
72
|
-
if 0 not in num:
|
|
73
|
-
low[dim] -= np.max(num[num < 0])
|
|
74
|
-
|
|
75
|
-
# Generate (anisotropic) spatial grid
|
|
76
|
-
x, y, z = np.mgrid[low[0]:high[0]:spacing[0],
|
|
77
|
-
low[1]:high[1]:spacing[1],
|
|
78
|
-
low[2]:high[2]:spacing[2]]
|
|
79
|
-
|
|
80
|
-
if not levelset:
|
|
81
|
-
arr = ((x / float(a)) ** 2 +
|
|
82
|
-
(y / float(b)) ** 2 +
|
|
83
|
-
(z / float(c)) ** 2) <= 1
|
|
84
|
-
else:
|
|
85
|
-
arr = ((x / float(a)) ** 2 +
|
|
86
|
-
(y / float(b)) ** 2 +
|
|
87
|
-
(z / float(c)) ** 2) - 1
|
|
88
|
-
|
|
89
|
-
return arr
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def multislice_affine_transform(array_source, affine_source, output_affine, slice_thickness=None, **kwargs):
|
|
93
|
-
"""Generalization of scipy's affine transform.
|
|
94
|
-
|
|
95
|
-
This version also works when the source array is 2D and when it is multislice 2D (ie. slice thickness < slice spacing).
|
|
96
|
-
In these scenarios each slice is first reshaped into a volume with provided slice thickness and mapped separately.
|
|
97
|
-
"""
|
|
98
|
-
|
|
99
|
-
slice_spacing = np.linalg.norm(affine_source[:3,2])
|
|
100
|
-
|
|
101
|
-
# Single-slice 2D sequence
|
|
102
|
-
if array_source.shape[2] == 1:
|
|
103
|
-
return _map_multislice_array(array_source, affine_source, output_affine, **kwargs)
|
|
104
|
-
|
|
105
|
-
# Multi-slice 2D sequence
|
|
106
|
-
elif slice_spacing != slice_thickness:
|
|
107
|
-
return _map_multislice_array(array_source, affine_source, output_affine, slice_thickness=slice_thickness, **kwargs)
|
|
108
|
-
|
|
109
|
-
# 3D volume sequence
|
|
110
|
-
else:
|
|
111
|
-
return _map_volume_array(array_source, affine_source, output_affine, **kwargs)
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _map_multislice_array(array, affine, output_affine, output_shape=None, slice_thickness=None, mask=False, label=False, cval=0):
|
|
116
|
-
|
|
117
|
-
# Turn each slice into a volume and map as volume.
|
|
118
|
-
array_mapped = None
|
|
119
|
-
for z in range(array.shape[2]):
|
|
120
|
-
array_z, affine_z = slice_to_volume(array, affine, z, slice_thickness=slice_thickness)
|
|
121
|
-
array_mapped_z = _map_volume_array(array_z, affine_z, output_affine, output_shape=output_shape, cval=cval)
|
|
122
|
-
if array_mapped is None:
|
|
123
|
-
array_mapped = array_mapped_z
|
|
124
|
-
else:
|
|
125
|
-
array_mapped += array_mapped_z
|
|
126
|
-
|
|
127
|
-
# If source is a mask array, set values to [0,1].
|
|
128
|
-
if mask:
|
|
129
|
-
array_mapped[array_mapped > 0.5] = 1
|
|
130
|
-
array_mapped[array_mapped <= 0.5] = 0
|
|
131
|
-
elif label:
|
|
132
|
-
array_mapped = np.around(array_mapped)
|
|
133
|
-
|
|
134
|
-
return array_mapped
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def slice_to_volume(array, affine, z=0, slice_thickness=None):
|
|
138
|
-
|
|
139
|
-
# Reshape array to 4D (x,y,z + remainder)
|
|
140
|
-
shape = array.shape
|
|
141
|
-
if len(shape) > 3:
|
|
142
|
-
nk = np.prod(shape[3:])
|
|
143
|
-
else:
|
|
144
|
-
nk = 1
|
|
145
|
-
array = array.reshape(shape[:3] + (nk,))
|
|
146
|
-
|
|
147
|
-
# Extract a 2D array
|
|
148
|
-
array_z = array[:,:,z,:]
|
|
149
|
-
array_z = array_z[:,:,np.newaxis,:]
|
|
150
|
-
|
|
151
|
-
# Duplicate the array in the z-direction to create 2 slices.
|
|
152
|
-
nz = 2
|
|
153
|
-
array_z = np.repeat(array_z, nz, axis=2)
|
|
154
|
-
|
|
155
|
-
# Reshape to original nr of dimensions
|
|
156
|
-
if len(shape) > 3:
|
|
157
|
-
dim = shape[:2] + (nz,) + shape[3:]
|
|
158
|
-
else:
|
|
159
|
-
dim = shape[:2] + (nz,)
|
|
160
|
-
array_z = array_z.reshape(dim)
|
|
161
|
-
|
|
162
|
-
# Offset the slice position accordingly
|
|
163
|
-
affine_z = affine.copy()
|
|
164
|
-
affine_z[:3,3] += z*affine_z[:3,2]
|
|
165
|
-
|
|
166
|
-
# Set the slice spacing to equal the slice thickness
|
|
167
|
-
if slice_thickness is not None:
|
|
168
|
-
slice_spacing = np.linalg.norm(affine_z[:3,2])
|
|
169
|
-
affine_z[:3,2] *= slice_thickness/slice_spacing
|
|
170
|
-
|
|
171
|
-
# Offset the slice position by half of the slice thickness.
|
|
172
|
-
affine_z[:3,3] -= affine_z[:3,2]/2
|
|
173
|
-
|
|
174
|
-
return array_z, affine_z
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def _map_volume_array(array, affine, output_affine, output_shape=None, mask=False, label=False, cval=0):
|
|
178
|
-
|
|
179
|
-
shape = array.shape
|
|
180
|
-
if shape[2] == 1:
|
|
181
|
-
msg = 'This function only works for an array with at least 2 slices'
|
|
182
|
-
raise ValueError(msg)
|
|
183
|
-
|
|
184
|
-
# Get transformation matrix
|
|
185
|
-
source_to_target = np.linalg.inv(affine).dot(output_affine)
|
|
186
|
-
source_to_target = np.around(source_to_target, 3) # remove round-off errors in the inversion
|
|
187
|
-
|
|
188
|
-
# Reshape array to 4D (x,y,z + remainder)
|
|
189
|
-
if output_shape is None:
|
|
190
|
-
output_shape = shape[:3]
|
|
191
|
-
nk = np.prod(shape[3:])
|
|
192
|
-
output = np.empty(output_shape + (nk,))
|
|
193
|
-
array = array.reshape(shape[:3] + (nk,))
|
|
194
|
-
|
|
195
|
-
#Perform transformation
|
|
196
|
-
for k in range(nk):
|
|
197
|
-
output[:,:,:,k] = affine_transform(
|
|
198
|
-
array[:,:,:,k],
|
|
199
|
-
matrix = source_to_target[:3,:3],
|
|
200
|
-
offset = source_to_target[:3,3],
|
|
201
|
-
output_shape = output_shape,
|
|
202
|
-
cval = cval,
|
|
203
|
-
order = 0 if mask else 3,
|
|
204
|
-
)
|
|
205
|
-
|
|
206
|
-
# If source is a mask array, set values to [0,1]
|
|
207
|
-
if mask:
|
|
208
|
-
output[output > 0.5] = 1
|
|
209
|
-
output[output <= 0.5] = 0
|
|
210
|
-
|
|
211
|
-
# If source is a label array, round to integers
|
|
212
|
-
elif label:
|
|
213
|
-
output = np.around(output)
|
|
214
|
-
|
|
215
|
-
return output.reshape(output_shape + shape[3:])
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
# https://discovery.ucl.ac.uk/id/eprint/10146893/1/geometry_medim.pdf
|
|
220
|
-
|
|
221
|
-
def interpolate3d_scale(array, scale=2):
|
|
222
|
-
|
|
223
|
-
array, _ = interpolate3d_isotropic(array, [1,1,1], isotropic_spacing=1/scale)
|
|
224
|
-
return array
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
def interpolate3d_isotropic(array, spacing, isotropic_spacing=None):
|
|
228
|
-
|
|
229
|
-
if isotropic_spacing is None:
|
|
230
|
-
isotropic_spacing = np.amin(spacing)
|
|
231
|
-
|
|
232
|
-
# Get x, y, z coordinates for array
|
|
233
|
-
nx = array.shape[0]
|
|
234
|
-
ny = array.shape[1]
|
|
235
|
-
nz = array.shape[2]
|
|
236
|
-
Lx = (nx-1)*spacing[0]
|
|
237
|
-
Ly = (ny-1)*spacing[1]
|
|
238
|
-
Lz = (nz-1)*spacing[2]
|
|
239
|
-
x = np.linspace(0, Lx, nx)
|
|
240
|
-
y = np.linspace(0, Ly, ny)
|
|
241
|
-
z = np.linspace(0, Lz, nz)
|
|
242
|
-
|
|
243
|
-
# Get x, y, z coordinates for isotropic array
|
|
244
|
-
nxi = 1 + np.floor(Lx/isotropic_spacing)
|
|
245
|
-
nyi = 1 + np.floor(Ly/isotropic_spacing)
|
|
246
|
-
nzi = 1 + np.floor(Lz/isotropic_spacing)
|
|
247
|
-
Lxi = (nxi-1)*isotropic_spacing
|
|
248
|
-
Lyi = (nyi-1)*isotropic_spacing
|
|
249
|
-
Lzi = (nzi-1)*isotropic_spacing
|
|
250
|
-
xi = np.linspace(0, Lxi, nxi.astype(int))
|
|
251
|
-
yi = np.linspace(0, Lyi, nyi.astype(int))
|
|
252
|
-
zi = np.linspace(0, Lzi, nzi.astype(int))
|
|
253
|
-
|
|
254
|
-
# Interpolate to isotropic
|
|
255
|
-
ri = np.meshgrid(xi,yi,zi, indexing='ij')
|
|
256
|
-
array = interpn((x,y,z), array, np.stack(ri, axis=-1))
|
|
257
|
-
return array, isotropic_spacing
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
def bounding_box(
|
|
261
|
-
image_orientation, # ImageOrientationPatient (assume same for all slices)
|
|
262
|
-
image_positions, # ImagePositionPatient for all slices
|
|
263
|
-
pixel_spacing, # PixelSpacing (assume same for all slices)
|
|
264
|
-
rows, # Number of rows
|
|
265
|
-
columns): # Number of columns
|
|
266
|
-
|
|
267
|
-
"""
|
|
268
|
-
Calculate the bounding box of an 3D image stored in slices in the DICOM file format.
|
|
269
|
-
|
|
270
|
-
Parameters:
|
|
271
|
-
image_orientation (list):
|
|
272
|
-
a list of 6 elements representing the ImageOrientationPatient DICOM tag for the image.
|
|
273
|
-
This specifies the orientation of the image slices in 3D space.
|
|
274
|
-
image_positions (list):
|
|
275
|
-
a list of 3-element lists representing the ImagePositionPatient DICOM tag for each slice in the image.
|
|
276
|
-
This specifies the position of each slice in 3D space.
|
|
277
|
-
pixel_spacing (list):
|
|
278
|
-
a list of 2 elements representing the PixelSpacing DICOM tag for the image.
|
|
279
|
-
This specifies the spacing between pixels in the rows and columns of each slice.
|
|
280
|
-
rows (int):
|
|
281
|
-
an integer representing the number of rows in each slice.
|
|
282
|
-
columns (int):
|
|
283
|
-
an integer representing the number of columns in each slice.
|
|
284
|
-
|
|
285
|
-
Returns:
|
|
286
|
-
dict: a dictionary with keys 'RPF', 'LPF', 'LPH', 'RPH', 'RAF', 'LAF', 'LAH', and 'RAH',
|
|
287
|
-
representing the Right Posterior Foot, Left Posterior Foot, Left Posterior Head,
|
|
288
|
-
Right Posterior Head, Right Anterior Foot, Left Anterior Foot,
|
|
289
|
-
Left Anterior Head, and Right Anterior Head, respectively.
|
|
290
|
-
Each key maps to a list of 3 elements representing the x, y, and z coordinates
|
|
291
|
-
of the corresponding corner of the bounding box.
|
|
292
|
-
|
|
293
|
-
"""
|
|
294
|
-
|
|
295
|
-
row_spacing = pixel_spacing[0]
|
|
296
|
-
column_spacing = pixel_spacing[1]
|
|
297
|
-
|
|
298
|
-
row_cosine = np.array(image_orientation[:3])
|
|
299
|
-
column_cosine = np.array(image_orientation[3:])
|
|
300
|
-
slice_cosine = np.cross(row_cosine, column_cosine)
|
|
301
|
-
|
|
302
|
-
number_of_slices = len(image_positions)
|
|
303
|
-
image_locations = [np.dot(np.array(pos), slice_cosine) for pos in image_positions]
|
|
304
|
-
slab_thickness = max(image_locations) - min(image_locations)
|
|
305
|
-
slice_spacing = slab_thickness / (number_of_slices - 1)
|
|
306
|
-
image_position_first_slice = image_positions[image_locations.index(min(image_locations))]
|
|
307
|
-
|
|
308
|
-
# ul = Upper Left corner of a slice
|
|
309
|
-
# ur = Upper Right corner of a slice
|
|
310
|
-
# bl = Bottom Left corner of a slice
|
|
311
|
-
# br = Bottom Right corner of a slice
|
|
312
|
-
|
|
313
|
-
# Initialize with the first slice
|
|
314
|
-
ul = image_position_first_slice
|
|
315
|
-
ur = ul + row_cosine * (columns-1) * column_spacing
|
|
316
|
-
br = ur + column_cosine * (rows-1) * row_spacing
|
|
317
|
-
bl = ul + column_cosine * (rows-1) * row_spacing
|
|
318
|
-
corners = np.array([ul, ur, br, bl])
|
|
319
|
-
amin = np.amax(corners, axis=0)
|
|
320
|
-
amax = np.amax(corners, axis=0)
|
|
321
|
-
box = {
|
|
322
|
-
'RPF': [amin[0],amax[1],amin[2]], # Right Posterior Foot
|
|
323
|
-
'LPF': [amax[0],amax[1],amin[2]], # Left Posterior Foot
|
|
324
|
-
'LPH': [amax[0],amax[1],amax[2]], # Left Posterior Head
|
|
325
|
-
'RPH': [amin[0],amax[1],amax[2]], # Right Posterior Head
|
|
326
|
-
'RAF': [amin[0],amin[1],amin[2]], # Right Anterior Foot
|
|
327
|
-
'LAF': [amax[0],amin[1],amin[2]], # Left Anterior Foot
|
|
328
|
-
'LAH': [amax[0],amin[1],amax[2]], # Left Anterior Head
|
|
329
|
-
'RAH': [amin[0],amin[1],amax[2]], # Right Anterior Head
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
# Update with all other slices
|
|
333
|
-
# PROBABLY SUFFICIENT TO USE ONLY THE OUTER SLICES!!
|
|
334
|
-
for _ in range(1, number_of_slices):
|
|
335
|
-
|
|
336
|
-
ul += slice_cosine * slice_spacing
|
|
337
|
-
ur = ul + row_cosine * (columns-1) * column_spacing
|
|
338
|
-
br = ur + column_cosine * (rows-1) * row_spacing
|
|
339
|
-
bl = ul + column_cosine * (rows-1) * row_spacing
|
|
340
|
-
|
|
341
|
-
corners = np.array([ul, ur, br, bl])
|
|
342
|
-
amin = np.amin(corners, axis=0)
|
|
343
|
-
amax = np.amax(corners, axis=0)
|
|
344
|
-
|
|
345
|
-
box['RPF'][0] = min([box['RPF'][0], amin[0]])
|
|
346
|
-
box['RPF'][1] = max([box['RPF'][1], amax[1]])
|
|
347
|
-
box['RPF'][2] = min([box['RPF'][2], amin[2]])
|
|
348
|
-
|
|
349
|
-
box['LPF'][0] = max([box['LPF'][0], amax[0]])
|
|
350
|
-
box['LPF'][1] = max([box['LPF'][1], amax[1]])
|
|
351
|
-
box['LPF'][2] = min([box['LPF'][2], amin[2]])
|
|
352
|
-
|
|
353
|
-
box['LPH'][0] = max([box['LPH'][0], amax[0]])
|
|
354
|
-
box['LPH'][1] = max([box['LPH'][1], amax[1]])
|
|
355
|
-
box['LPH'][2] = max([box['LPH'][2], amax[2]])
|
|
356
|
-
|
|
357
|
-
box['RPH'][0] = min([box['RPH'][0], amin[0]])
|
|
358
|
-
box['RPH'][1] = max([box['RPH'][1], amax[1]])
|
|
359
|
-
box['RPH'][2] = max([box['RPH'][2], amax[2]])
|
|
360
1
|
|
|
361
|
-
|
|
362
|
-
box['RAF'][1] = min([box['RAF'][1], amin[1]])
|
|
363
|
-
box['RAF'][2] = min([box['RAF'][2], amin[2]])
|
|
364
|
-
|
|
365
|
-
box['LAF'][0] = max([box['LAF'][0], amax[0]])
|
|
366
|
-
box['LAF'][1] = min([box['LAF'][1], amin[1]])
|
|
367
|
-
box['LAF'][2] = min([box['LAF'][2], amin[2]])
|
|
368
|
-
|
|
369
|
-
box['LAH'][0] = max([box['LAH'][0], amax[0]])
|
|
370
|
-
box['LAH'][1] = min([box['LAH'][1], amin[1]])
|
|
371
|
-
box['LAH'][2] = max([box['LAH'][2], amax[2]])
|
|
372
|
-
|
|
373
|
-
box['RAH'][0] = min([box['RAH'][0], amin[0]])
|
|
374
|
-
box['RAH'][1] = min([box['RAH'][1], amin[1]])
|
|
375
|
-
box['RAH'][2] = max([box['RAH'][2], amax[2]])
|
|
376
|
-
|
|
377
|
-
return box
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
def standard_affine_matrix(
|
|
382
|
-
bounding_box,
|
|
383
|
-
pixel_spacing,
|
|
384
|
-
slice_spacing,
|
|
385
|
-
orientation = 'axial'):
|
|
386
|
-
|
|
387
|
-
row_spacing = pixel_spacing[0]
|
|
388
|
-
column_spacing = pixel_spacing[1]
|
|
389
|
-
|
|
390
|
-
if orientation == 'axial':
|
|
391
|
-
image_position = bounding_box['RAF']
|
|
392
|
-
row_cosine = np.array([1,0,0])
|
|
393
|
-
column_cosine = np.array([0,1,0])
|
|
394
|
-
slice_cosine = np.array([0,0,1])
|
|
395
|
-
elif orientation == 'coronal':
|
|
396
|
-
image_position = bounding_box['RAH']
|
|
397
|
-
row_cosine = np.array([1,0,0])
|
|
398
|
-
column_cosine = np.array([0,0,-1])
|
|
399
|
-
slice_cosine = np.array([0,1,0])
|
|
400
|
-
elif orientation == 'sagittal':
|
|
401
|
-
image_position = bounding_box['LAH']
|
|
402
|
-
row_cosine = np.array([0,1,0])
|
|
403
|
-
column_cosine = np.array([0,0,-1])
|
|
404
|
-
slice_cosine = np.array([-1,0,0])
|
|
2
|
+
import numpy as np
|
|
405
3
|
|
|
406
|
-
affine = np.identity(4, dtype=np.float32)
|
|
407
|
-
affine[:3, 0] = row_cosine * column_spacing
|
|
408
|
-
affine[:3, 1] = column_cosine * row_spacing
|
|
409
|
-
affine[:3, 2] = slice_cosine * slice_spacing
|
|
410
|
-
affine[:3, 3] = image_position
|
|
411
|
-
|
|
412
|
-
return affine
|
|
413
4
|
|
|
414
5
|
|
|
415
6
|
def affine_matrix( # single slice function
|
|
@@ -425,6 +16,10 @@ def affine_matrix( # single slice function
|
|
|
425
16
|
column_cosine = np.array(image_orientation[3:])
|
|
426
17
|
slice_cosine = np.cross(row_cosine, column_cosine)
|
|
427
18
|
|
|
19
|
+
# The coronal orientation has a left-handed reference frame
|
|
20
|
+
if np.array_equal(np.around(image_orientation, 3), [1,0,0,0,0,-1]):
|
|
21
|
+
slice_cosine = -slice_cosine
|
|
22
|
+
|
|
428
23
|
affine = np.identity(4, dtype=np.float32)
|
|
429
24
|
affine[:3, 0] = row_cosine * column_spacing
|
|
430
25
|
affine[:3, 1] = column_cosine * row_spacing
|
|
@@ -444,61 +39,11 @@ def slice_location(
|
|
|
444
39
|
column_cosine = np.array(image_orientation[3:])
|
|
445
40
|
slice_cosine = np.cross(row_cosine, column_cosine)
|
|
446
41
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
def image_position_from_slice_location(slice_location:float, affine=np.eye(4))->list:
|
|
451
|
-
v = dismantle_affine_matrix(affine)
|
|
452
|
-
return list(affine[:3, 3] + slice_location * np.array(v['slice_cosine']))
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
def image_position_patient(affine, number_of_slices):
|
|
456
|
-
slab = dismantle_affine_matrix(affine)
|
|
457
|
-
image_positions = []
|
|
458
|
-
image_locations = []
|
|
459
|
-
for s in range(number_of_slices):
|
|
460
|
-
pos = [
|
|
461
|
-
slab['ImagePositionPatient'][i]
|
|
462
|
-
+ s*slab['SpacingBetweenSlices']*slab['slice_cosine'][i]
|
|
463
|
-
for i in range(3)
|
|
464
|
-
]
|
|
465
|
-
loc = np.dot(np.array(pos), np.array(slab['slice_cosine']))
|
|
466
|
-
image_positions.append(pos)
|
|
467
|
-
image_locations.append(loc)
|
|
468
|
-
return image_positions, image_locations
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
def affine_matrix_multislice(
|
|
472
|
-
image_orientation, # ImageOrientationPatient (assume same for all slices)
|
|
473
|
-
image_positions, # ImagePositionPatient for all slices
|
|
474
|
-
pixel_spacing): # PixelSpacing (assume same for all slices)
|
|
475
|
-
|
|
476
|
-
row_spacing = pixel_spacing[0]
|
|
477
|
-
column_spacing = pixel_spacing[1]
|
|
478
|
-
|
|
479
|
-
row_cosine = np.array(image_orientation[:3])
|
|
480
|
-
column_cosine = np.array(image_orientation[3:])
|
|
481
|
-
slice_cosine = np.cross(row_cosine, column_cosine)
|
|
482
|
-
|
|
483
|
-
image_locations = [np.dot(np.array(pos), slice_cosine) for pos in image_positions]
|
|
484
|
-
#number_of_slices = len(image_positions)
|
|
485
|
-
number_of_slices = np.unique(image_locations).size
|
|
486
|
-
if number_of_slices == 1:
|
|
487
|
-
msg = 'Cannot calculate affine matrix for the slice group. \n'
|
|
488
|
-
msg += 'All slices have the same location. \n'
|
|
489
|
-
msg += 'Use the single-slice affine formula instead.'
|
|
490
|
-
raise ValueError(msg)
|
|
491
|
-
slab_thickness = np.amax(image_locations) - np.amin(image_locations)
|
|
492
|
-
slice_spacing = slab_thickness / (number_of_slices - 1)
|
|
493
|
-
image_position_first_slice = image_positions[image_locations.index(np.amin(image_locations))]
|
|
494
|
-
|
|
495
|
-
affine = np.identity(4, dtype=np.float32)
|
|
496
|
-
affine[:3, 0] = row_cosine * column_spacing
|
|
497
|
-
affine[:3, 1] = column_cosine * row_spacing
|
|
498
|
-
affine[:3, 2] = slice_cosine * slice_spacing
|
|
499
|
-
affine[:3, 3] = image_position_first_slice
|
|
42
|
+
# The coronal orientation has a left-handed reference frame
|
|
43
|
+
if np.array_equal(np.around(image_orientation, 3), [1,0,0,0,0,-1]):
|
|
44
|
+
slice_cosine = -slice_cosine
|
|
500
45
|
|
|
501
|
-
return
|
|
46
|
+
return np.dot(np.array(image_position), slice_cosine)
|
|
502
47
|
|
|
503
48
|
|
|
504
49
|
def dismantle_affine_matrix(affine):
|
|
@@ -514,116 +59,13 @@ def dismantle_affine_matrix(affine):
|
|
|
514
59
|
slice_cosine = affine[:3, 2] / slice_thickness
|
|
515
60
|
return {
|
|
516
61
|
'PixelSpacing': [row_spacing, column_spacing],
|
|
517
|
-
'SpacingBetweenSlices': slice_thickness, # Obsolete
|
|
62
|
+
# 'SpacingBetweenSlices': slice_thickness, # Obsolete
|
|
518
63
|
'SliceThickness': slice_thickness,
|
|
519
64
|
'ImageOrientationPatient': row_cosine.tolist() + column_cosine.tolist(),
|
|
520
65
|
'ImagePositionPatient': affine[:3, 3].tolist(), # first slice for a volume
|
|
521
66
|
'slice_cosine': slice_cosine.tolist()}
|
|
522
67
|
|
|
523
68
|
|
|
524
|
-
def unstack_affine(affine, nz):
|
|
525
|
-
|
|
526
|
-
pos0 = affine[:3, 3]
|
|
527
|
-
slice_vec = affine[:3, 2]
|
|
528
|
-
|
|
529
|
-
affines = []
|
|
530
|
-
for z in range(nz):
|
|
531
|
-
affine_z = affine.copy()
|
|
532
|
-
affine_z[:3, 3] = pos0 + z*slice_vec
|
|
533
|
-
affines.append(affine_z)
|
|
534
|
-
|
|
535
|
-
return affines
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
def stack_affines(affines):
|
|
539
|
-
|
|
540
|
-
aff = [dismantle_affine_matrix(a) for a in affines]
|
|
541
|
-
|
|
542
|
-
# Check that all affines have the same orientation
|
|
543
|
-
orient = [a['ImageOrientationPatient'] for a in aff]
|
|
544
|
-
orient = [x for i, x in enumerate(orient) if i==orient.index(x)]
|
|
545
|
-
if len(orient) > 1:
|
|
546
|
-
raise ValueError(
|
|
547
|
-
"Slices have different orientations and cannot be stacked")
|
|
548
|
-
orient = orient[0]
|
|
549
|
-
|
|
550
|
-
# Check that all affines have the same slice_cosine
|
|
551
|
-
slice_cosine = [a['slice_cosine'] for a in aff]
|
|
552
|
-
slice_cosine = [x for i, x in enumerate(slice_cosine) if i==slice_cosine.index(x)]
|
|
553
|
-
if len(slice_cosine) > 1:
|
|
554
|
-
raise ValueError(
|
|
555
|
-
"Slices have different slice cosines and cannot be stacked")
|
|
556
|
-
slice_cosine = slice_cosine[0]
|
|
557
|
-
|
|
558
|
-
# Check all slices have the same thickness
|
|
559
|
-
thick = [a['SpacingBetweenSlices'] for a in aff] # note incorrectly named
|
|
560
|
-
thick = np.unique(thick)
|
|
561
|
-
if len(thick)>1:
|
|
562
|
-
raise ValueError(
|
|
563
|
-
"Slices have different slice thickness and cannot be stacked")
|
|
564
|
-
thick = thick[0]
|
|
565
|
-
|
|
566
|
-
# Check all slices have the same pixel spacing
|
|
567
|
-
pix_space = [a['PixelSpacing'] for a in aff]
|
|
568
|
-
pix_space = [x for i, x in enumerate(pix_space) if i==pix_space.index(x)]
|
|
569
|
-
if len(pix_space)>1:
|
|
570
|
-
raise ValueError(
|
|
571
|
-
"Slices have different pixel sizes and cannot be stacked. ")
|
|
572
|
-
pix_space = pix_space[0]
|
|
573
|
-
|
|
574
|
-
# Get orientations (orthogonal assumed here)
|
|
575
|
-
row_vec = np.array(orient[:3])
|
|
576
|
-
column_vec = np.array(orient[3:])
|
|
577
|
-
slice_vec = np.array(slice_cosine)
|
|
578
|
-
|
|
579
|
-
# Check that all slice spacings are equal
|
|
580
|
-
pos = [a['ImagePositionPatient'] for a in aff]
|
|
581
|
-
loc = np.array([np.dot(p, slice_vec) for p in pos])
|
|
582
|
-
# Get unique slice spacing (to micrometer precision)
|
|
583
|
-
slice_spacing = np.unique(np.around(loc[1:]-loc[:-1], 3))
|
|
584
|
-
# If there is more than 1 slice spacing, the series is multislice
|
|
585
|
-
if slice_spacing.size != 1:
|
|
586
|
-
raise ValueError(
|
|
587
|
-
"There are different spacings between consecutive slices. "
|
|
588
|
-
"The slices cannot be stacked.")
|
|
589
|
-
slice_spacing = slice_spacing[0]
|
|
590
|
-
|
|
591
|
-
# Check the slice spacing is equal to the slice thickness
|
|
592
|
-
if np.around(thick-slice_spacing, 3) != 0:
|
|
593
|
-
raise ValueError(
|
|
594
|
-
"This is a multi-slice sequence, i.e. the slice spacing is "
|
|
595
|
-
"different from the slice thickness. If you want to stack the "
|
|
596
|
-
"slices, set the slice thickness equal to the slice spacing "
|
|
597
|
-
"first (" + str(slice_spacing) + " mm).")
|
|
598
|
-
|
|
599
|
-
# Check that all positions are on the slice vector
|
|
600
|
-
for p in pos[1:]:
|
|
601
|
-
# Position relative to first slice position
|
|
602
|
-
prel = np.array(p)-np.array(pos[0])
|
|
603
|
-
# Parallel means cross product has length zero
|
|
604
|
-
norm = np.linalg.norm(np.cross(slice_vec, prel))
|
|
605
|
-
# Round to micrometers to avoid numerical error
|
|
606
|
-
if np.round(norm, 3) != 0:
|
|
607
|
-
raise ValueError(
|
|
608
|
-
"Slices are not aligned and cannot be stacked")
|
|
609
|
-
|
|
610
|
-
# Build affine for the stack
|
|
611
|
-
affine = np.identity(4, dtype=np.float32)
|
|
612
|
-
affine[:3, 0] = row_vec * pix_space[1]
|
|
613
|
-
affine[:3, 1] = column_vec * pix_space[0]
|
|
614
|
-
affine[:3, 2] = slice_vec * slice_spacing
|
|
615
|
-
affine[:3, 3] = pos[0]
|
|
616
|
-
|
|
617
|
-
return affine
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
def affine_to_RAH(affine):
|
|
622
|
-
"""Convert to the coordinate system used in NifTi"""
|
|
623
|
-
|
|
624
|
-
rot_180 = np.identity(4, dtype=np.float32)
|
|
625
|
-
rot_180[:2,:2] = [[-1,0],[0,-1]]
|
|
626
|
-
return np.matmul(rot_180, affine)
|
|
627
69
|
|
|
628
70
|
|
|
629
71
|
def clip(array, value_range = None):
|
|
@@ -673,64 +115,3 @@ def scale_to_range(array, bits_allocated, signed=False):
|
|
|
673
115
|
else:
|
|
674
116
|
return array.astype(np.uint64), slope, intercept
|
|
675
117
|
|
|
676
|
-
|
|
677
|
-
def _scale_to_range(array, bits_allocated):
|
|
678
|
-
# Obsolete - generalized as above
|
|
679
|
-
|
|
680
|
-
range = 2.0**bits_allocated - 1
|
|
681
|
-
maximum = np.amax(array)
|
|
682
|
-
minimum = np.amin(array)
|
|
683
|
-
if maximum == minimum:
|
|
684
|
-
slope = 1
|
|
685
|
-
else:
|
|
686
|
-
slope = range / (maximum - minimum)
|
|
687
|
-
intercept = -slope * minimum
|
|
688
|
-
array *= slope
|
|
689
|
-
array += intercept
|
|
690
|
-
|
|
691
|
-
if bits_allocated == 8:
|
|
692
|
-
return array.astype(np.uint8), slope, intercept
|
|
693
|
-
if bits_allocated == 16:
|
|
694
|
-
return array.astype(np.uint16), slope, intercept
|
|
695
|
-
if bits_allocated == 32:
|
|
696
|
-
return array.astype(np.uint32), slope, intercept
|
|
697
|
-
if bits_allocated == 64:
|
|
698
|
-
return array.astype(np.uint64), slope, intercept
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
def BGRA(array, RGBlut=None, width=None, center=None):
|
|
702
|
-
|
|
703
|
-
if (width is None) or (center is None):
|
|
704
|
-
max = np.amax(array)
|
|
705
|
-
min = np.amin(array)
|
|
706
|
-
else:
|
|
707
|
-
max = center+width/2
|
|
708
|
-
min = center-width/2
|
|
709
|
-
|
|
710
|
-
# Scale pixel array into byte range
|
|
711
|
-
array = np.clip(array, min, max)
|
|
712
|
-
array -= min
|
|
713
|
-
if max > min:
|
|
714
|
-
array *= 255/(max-min)
|
|
715
|
-
array = array.astype(np.ubyte)
|
|
716
|
-
|
|
717
|
-
BGRA = np.empty(array.shape[:2]+(4,), dtype=np.ubyte)
|
|
718
|
-
BGRA[:,:,3] = 255 # Alpha channel
|
|
719
|
-
|
|
720
|
-
if RGBlut is None:
|
|
721
|
-
# Greyscale image
|
|
722
|
-
for c in range(3):
|
|
723
|
-
BGRA[:,:,c] = array
|
|
724
|
-
else:
|
|
725
|
-
# Scale LUT into byte range
|
|
726
|
-
RGBlut *= 255
|
|
727
|
-
RGBlut = RGBlut.astype(np.ubyte)
|
|
728
|
-
# Create RGB array by indexing LUT with pixel array
|
|
729
|
-
for c in range(3):
|
|
730
|
-
BGRA[:,:,c] = RGBlut[array,2-c]
|
|
731
|
-
|
|
732
|
-
return BGRA
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dbdicom
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A pythonic interface for reading and writing DICOM databases
|
|
5
|
+
Author-email: Steven Sourbron <s.sourbron@sheffield.ac.uk>, Ebony Gunwhy <e.gunwhy@sheffield.ac.uk>
|
|
6
|
+
Project-URL: Homepage, https://openmiblab.github.io/dbdicom/
|
|
7
|
+
Keywords: python,medical imaging,DICOM
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Topic :: Scientific/Engineering
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Requires-Python: >=3.6
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Requires-Dist: tqdm
|
|
21
|
+
Requires-Dist: importlib-resources
|
|
22
|
+
Requires-Dist: numpy
|
|
23
|
+
Requires-Dist: pandas
|
|
24
|
+
Requires-Dist: vreg
|
|
25
|
+
Requires-Dist: pydicom
|
|
26
|
+
Requires-Dist: python-gdcm
|
|
27
|
+
Requires-Dist: pylibjpeg-libjpeg
|
|
28
|
+
Dynamic: license-file
|