dbdicom 0.2.0__py3-none-any.whl → 0.3.16__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.
- dbdicom/__init__.py +3 -25
- dbdicom/api.py +496 -0
- dbdicom/const.py +144 -0
- dbdicom/database.py +133 -0
- dbdicom/dataset.py +471 -0
- dbdicom/dbd.py +1290 -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/external/dcm4che/bin/emf2sf +57 -57
- dbdicom/register.py +402 -0
- dbdicom/{ds/types → sop_classes}/ct_image.py +2 -16
- dbdicom/{ds/types → sop_classes}/enhanced_mr_image.py +206 -160
- dbdicom/sop_classes/mr_image.py +338 -0
- dbdicom/sop_classes/parametric_map.py +381 -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 +142 -0
- dbdicom/utils/files.py +0 -20
- dbdicom/utils/image.py +43 -466
- dbdicom/utils/pydicom_dataset.py +386 -0
- dbdicom-0.3.16.dist-info/METADATA +26 -0
- dbdicom-0.3.16.dist-info/RECORD +54 -0
- {dbdicom-0.2.0.dist-info → dbdicom-0.3.16.dist-info}/WHEEL +1 -1
- dbdicom/create.py +0 -450
- dbdicom/ds/__init__.py +0 -10
- dbdicom/ds/create.py +0 -63
- dbdicom/ds/dataset.py +0 -841
- dbdicom/ds/dictionaries.py +0 -620
- dbdicom/ds/types/mr_image.py +0 -267
- dbdicom/ds/types/parametric_map.py +0 -226
- dbdicom/external/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/lib/linux-x86/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/linux-x86-64/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/linux-x86-64/libopencv_java.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis2.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis2.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-x86/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-x86-64/libclib_jiio.so +0 -0
- dbdicom/manager.py +0 -2077
- dbdicom/message.py +0 -119
- dbdicom/record.py +0 -1526
- dbdicom/types/database.py +0 -107
- dbdicom/types/instance.py +0 -184
- dbdicom/types/patient.py +0 -40
- dbdicom/types/series.py +0 -816
- dbdicom/types/study.py +0 -58
- dbdicom/utils/variables.py +0 -155
- dbdicom/utils/vreg.py +0 -2626
- dbdicom/wrappers/__init__.py +0 -7
- dbdicom/wrappers/dipy.py +0 -462
- dbdicom/wrappers/elastix.py +0 -855
- dbdicom/wrappers/numpy.py +0 -119
- dbdicom/wrappers/scipy.py +0 -1413
- dbdicom/wrappers/skimage.py +0 -1030
- dbdicom/wrappers/sklearn.py +0 -151
- dbdicom/wrappers/vreg.py +0 -273
- dbdicom-0.2.0.dist-info/METADATA +0 -276
- dbdicom-0.2.0.dist-info/RECORD +0 -81
- {dbdicom-0.2.0.dist-info → dbdicom-0.3.16.dist-info/licenses}/LICENSE +0 -0
- {dbdicom-0.2.0.dist-info → dbdicom-0.3.16.dist-info}/top_level.txt +0 -0
dbdicom/utils/image.py
CHANGED
|
@@ -1,359 +1,15 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
from scipy.interpolate import interpn
|
|
3
|
-
from scipy.ndimage import affine_transform
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
def multislice_affine_transform(array_source, affine_source, output_affine, slice_thickness=None, **kwargs):
|
|
7
|
-
"""Generalization of scipy's affine transform.
|
|
8
|
-
|
|
9
|
-
This version also works when the source array is 2D and when it is multislice 2D (ie. slice thickness < slice spacing).
|
|
10
|
-
In these scenarios each slice is first reshaped into a volume with provided slice thickness and mapped separately.
|
|
11
|
-
"""
|
|
12
|
-
|
|
13
|
-
slice_spacing = np.linalg.norm(affine_source[:3,2])
|
|
14
|
-
|
|
15
|
-
# Single-slice 2D sequence
|
|
16
|
-
if array_source.shape[2] == 1:
|
|
17
|
-
return _map_multislice_array(array_source, affine_source, output_affine, **kwargs)
|
|
18
|
-
|
|
19
|
-
# Multi-slice 2D sequence
|
|
20
|
-
elif slice_spacing != slice_thickness:
|
|
21
|
-
return _map_multislice_array(array_source, affine_source, output_affine, slice_thickness=slice_thickness, **kwargs)
|
|
22
|
-
|
|
23
|
-
# 3D volume sequence
|
|
24
|
-
else:
|
|
25
|
-
return _map_volume_array(array_source, affine_source, output_affine, **kwargs)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _map_multislice_array(array, affine, output_affine, output_shape=None, slice_thickness=None, mask=False, label=False, cval=0):
|
|
30
|
-
|
|
31
|
-
# Turn each slice into a volume and map as volume.
|
|
32
|
-
array_mapped = None
|
|
33
|
-
for z in range(array.shape[2]):
|
|
34
|
-
array_z, affine_z = slice_to_volume(array, affine, z, slice_thickness=slice_thickness)
|
|
35
|
-
array_mapped_z = _map_volume_array(array_z, affine_z, output_affine, output_shape=output_shape, cval=cval)
|
|
36
|
-
if array_mapped is None:
|
|
37
|
-
array_mapped = array_mapped_z
|
|
38
|
-
else:
|
|
39
|
-
array_mapped += array_mapped_z
|
|
40
|
-
|
|
41
|
-
# If source is a mask array, set values to [0,1].
|
|
42
|
-
if mask:
|
|
43
|
-
array_mapped[array_mapped > 0.5] = 1
|
|
44
|
-
array_mapped[array_mapped <= 0.5] = 0
|
|
45
|
-
elif label:
|
|
46
|
-
array_mapped = np.around(array_mapped)
|
|
47
|
-
|
|
48
|
-
return array_mapped
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def slice_to_volume(array, affine, z=0, slice_thickness=None):
|
|
52
|
-
|
|
53
|
-
# Reshape array to 4D (x,y,z + remainder)
|
|
54
|
-
shape = array.shape
|
|
55
|
-
if len(shape) > 3:
|
|
56
|
-
nk = np.prod(shape[3:])
|
|
57
|
-
else:
|
|
58
|
-
nk = 1
|
|
59
|
-
array = array.reshape(shape[:3] + (nk,))
|
|
60
|
-
|
|
61
|
-
# Extract a 2D array
|
|
62
|
-
array_z = array[:,:,z,:]
|
|
63
|
-
array_z = array_z[:,:,np.newaxis,:]
|
|
64
|
-
|
|
65
|
-
# Duplicate the array in the z-direction to create 2 slices.
|
|
66
|
-
nz = 2
|
|
67
|
-
array_z = np.repeat(array_z, nz, axis=2)
|
|
68
|
-
|
|
69
|
-
# Reshape to original nr of dimensions
|
|
70
|
-
if len(shape) > 3:
|
|
71
|
-
dim = shape[:2] + (nz,) + shape[3:]
|
|
72
|
-
else:
|
|
73
|
-
dim = shape[:2] + (nz,)
|
|
74
|
-
array_z = array_z.reshape(dim)
|
|
75
|
-
|
|
76
|
-
# Offset the slice position accordingly
|
|
77
|
-
affine_z = affine.copy()
|
|
78
|
-
affine_z[:3,3] += z*affine_z[:3,2]
|
|
79
|
-
|
|
80
|
-
# Set the slice spacing to equal the slice thickness
|
|
81
|
-
if slice_thickness is not None:
|
|
82
|
-
slice_spacing = np.linalg.norm(affine_z[:3,2])
|
|
83
|
-
affine_z[:3,2] *= slice_thickness/slice_spacing
|
|
84
|
-
|
|
85
|
-
# Offset the slice position by half of the slice thickness.
|
|
86
|
-
affine_z[:3,3] -= affine_z[:3,2]/2
|
|
87
|
-
|
|
88
|
-
return array_z, affine_z
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def _map_volume_array(array, affine, output_affine, output_shape=None, mask=False, label=False, cval=0):
|
|
92
|
-
|
|
93
|
-
shape = array.shape
|
|
94
|
-
if shape[2] == 1:
|
|
95
|
-
msg = 'This function only works for an array with at least 2 slices'
|
|
96
|
-
raise ValueError(msg)
|
|
97
|
-
|
|
98
|
-
# Get transformation matrix
|
|
99
|
-
source_to_target = np.linalg.inv(affine).dot(output_affine)
|
|
100
|
-
source_to_target = np.around(source_to_target, 3) # remove round-off errors in the inversion
|
|
101
|
-
|
|
102
|
-
# Reshape array to 4D (x,y,z + remainder)
|
|
103
|
-
if output_shape is None:
|
|
104
|
-
output_shape = shape[:3]
|
|
105
|
-
nk = np.prod(shape[3:])
|
|
106
|
-
output = np.empty(output_shape + (nk,))
|
|
107
|
-
array = array.reshape(shape[:3] + (nk,))
|
|
108
|
-
|
|
109
|
-
#Perform transformation
|
|
110
|
-
for k in range(nk):
|
|
111
|
-
output[:,:,:,k] = affine_transform(
|
|
112
|
-
array[:,:,:,k],
|
|
113
|
-
matrix = source_to_target[:3,:3],
|
|
114
|
-
offset = source_to_target[:3,3],
|
|
115
|
-
output_shape = output_shape,
|
|
116
|
-
cval = cval,
|
|
117
|
-
order = 0 if mask else 3,
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
# If source is a mask array, set values to [0,1]
|
|
121
|
-
if mask:
|
|
122
|
-
output[output > 0.5] = 1
|
|
123
|
-
output[output <= 0.5] = 0
|
|
124
|
-
|
|
125
|
-
# If source is a label array, round to integers
|
|
126
|
-
elif label:
|
|
127
|
-
output = np.around(output)
|
|
128
|
-
|
|
129
|
-
return output.reshape(output_shape + shape[3:])
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
# https://discovery.ucl.ac.uk/id/eprint/10146893/1/geometry_medim.pdf
|
|
134
|
-
|
|
135
|
-
def interpolate3d_scale(array, scale=2):
|
|
136
|
-
|
|
137
|
-
array, _ = interpolate3d_isotropic(array, [1,1,1], isotropic_spacing=1/scale)
|
|
138
|
-
return array
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def interpolate3d_isotropic(array, spacing, isotropic_spacing=None):
|
|
142
|
-
|
|
143
|
-
if isotropic_spacing is None:
|
|
144
|
-
isotropic_spacing = np.amin(spacing)
|
|
145
|
-
|
|
146
|
-
# Get x, y, z coordinates for array
|
|
147
|
-
nx = array.shape[0]
|
|
148
|
-
ny = array.shape[1]
|
|
149
|
-
nz = array.shape[2]
|
|
150
|
-
Lx = (nx-1)*spacing[0]
|
|
151
|
-
Ly = (ny-1)*spacing[1]
|
|
152
|
-
Lz = (nz-1)*spacing[2]
|
|
153
|
-
x = np.linspace(0, Lx, nx)
|
|
154
|
-
y = np.linspace(0, Ly, ny)
|
|
155
|
-
z = np.linspace(0, Lz, nz)
|
|
156
|
-
|
|
157
|
-
# Get x, y, z coordinates for isotropic array
|
|
158
|
-
nxi = 1 + np.floor(Lx/isotropic_spacing)
|
|
159
|
-
nyi = 1 + np.floor(Ly/isotropic_spacing)
|
|
160
|
-
nzi = 1 + np.floor(Lz/isotropic_spacing)
|
|
161
|
-
Lxi = (nxi-1)*isotropic_spacing
|
|
162
|
-
Lyi = (nyi-1)*isotropic_spacing
|
|
163
|
-
Lzi = (nzi-1)*isotropic_spacing
|
|
164
|
-
xi = np.linspace(0, Lxi, nxi.astype(int))
|
|
165
|
-
yi = np.linspace(0, Lyi, nyi.astype(int))
|
|
166
|
-
zi = np.linspace(0, Lzi, nzi.astype(int))
|
|
167
|
-
|
|
168
|
-
# Interpolate to isotropic
|
|
169
|
-
ri = np.meshgrid(xi,yi,zi, indexing='ij')
|
|
170
|
-
array = interpn((x,y,z), array, np.stack(ri, axis=-1))
|
|
171
|
-
return array, isotropic_spacing
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
def bounding_box(
|
|
175
|
-
image_orientation, # ImageOrientationPatient (assume same for all slices)
|
|
176
|
-
image_positions, # ImagePositionPatient for all slices
|
|
177
|
-
pixel_spacing, # PixelSpacing (assume same for all slices)
|
|
178
|
-
rows, # Number of rows
|
|
179
|
-
columns): # Number of columns
|
|
180
|
-
|
|
181
|
-
"""
|
|
182
|
-
Calculate the bounding box of an 3D image stored in slices in the DICOM file format.
|
|
183
|
-
|
|
184
|
-
Parameters:
|
|
185
|
-
image_orientation (list):
|
|
186
|
-
a list of 6 elements representing the ImageOrientationPatient DICOM tag for the image.
|
|
187
|
-
This specifies the orientation of the image slices in 3D space.
|
|
188
|
-
image_positions (list):
|
|
189
|
-
a list of 3-element lists representing the ImagePositionPatient DICOM tag for each slice in the image.
|
|
190
|
-
This specifies the position of each slice in 3D space.
|
|
191
|
-
pixel_spacing (list):
|
|
192
|
-
a list of 2 elements representing the PixelSpacing DICOM tag for the image.
|
|
193
|
-
This specifies the spacing between pixels in the rows and columns of each slice.
|
|
194
|
-
rows (int):
|
|
195
|
-
an integer representing the number of rows in each slice.
|
|
196
|
-
columns (int):
|
|
197
|
-
an integer representing the number of columns in each slice.
|
|
198
|
-
|
|
199
|
-
Returns:
|
|
200
|
-
dict: a dictionary with keys 'RPF', 'LPF', 'LPH', 'RPH', 'RAF', 'LAF', 'LAH', and 'RAH',
|
|
201
|
-
representing the Right Posterior Foot, Left Posterior Foot, Left Posterior Head,
|
|
202
|
-
Right Posterior Head, Right Anterior Foot, Left Anterior Foot,
|
|
203
|
-
Left Anterior Head, and Right Anterior Head, respectively.
|
|
204
|
-
Each key maps to a list of 3 elements representing the x, y, and z coordinates
|
|
205
|
-
of the corresponding corner of the bounding box.
|
|
206
|
-
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
row_spacing = pixel_spacing[0]
|
|
210
|
-
column_spacing = pixel_spacing[1]
|
|
211
|
-
|
|
212
|
-
row_cosine = np.array(image_orientation[:3])
|
|
213
|
-
column_cosine = np.array(image_orientation[3:])
|
|
214
|
-
slice_cosine = np.cross(row_cosine, column_cosine)
|
|
215
|
-
|
|
216
|
-
number_of_slices = len(image_positions)
|
|
217
|
-
image_locations = [np.dot(np.array(pos), slice_cosine) for pos in image_positions]
|
|
218
|
-
slab_thickness = max(image_locations) - min(image_locations)
|
|
219
|
-
slice_spacing = slab_thickness / (number_of_slices - 1)
|
|
220
|
-
image_position_first_slice = image_positions[image_locations.index(min(image_locations))]
|
|
221
|
-
|
|
222
|
-
# ul = Upper Left corner of a slice
|
|
223
|
-
# ur = Upper Right corner of a slice
|
|
224
|
-
# bl = Bottom Left corner of a slice
|
|
225
|
-
# br = Bottom Right corner of a slice
|
|
226
|
-
|
|
227
|
-
# Initialize with the first slice
|
|
228
|
-
ul = image_position_first_slice
|
|
229
|
-
ur = ul + row_cosine * (columns-1) * column_spacing
|
|
230
|
-
br = ur + column_cosine * (rows-1) * row_spacing
|
|
231
|
-
bl = ul + column_cosine * (rows-1) * row_spacing
|
|
232
|
-
corners = np.array([ul, ur, br, bl])
|
|
233
|
-
amin = np.amax(corners, axis=0)
|
|
234
|
-
amax = np.amax(corners, axis=0)
|
|
235
|
-
box = {
|
|
236
|
-
'RPF': [amin[0],amax[1],amin[2]], # Right Posterior Foot
|
|
237
|
-
'LPF': [amax[0],amax[1],amin[2]], # Left Posterior Foot
|
|
238
|
-
'LPH': [amax[0],amax[1],amax[2]], # Left Posterior Head
|
|
239
|
-
'RPH': [amin[0],amax[1],amax[2]], # Right Posterior Head
|
|
240
|
-
'RAF': [amin[0],amin[1],amin[2]], # Right Anterior Foot
|
|
241
|
-
'LAF': [amax[0],amin[1],amin[2]], # Left Anterior Foot
|
|
242
|
-
'LAH': [amax[0],amin[1],amax[2]], # Left Anterior Head
|
|
243
|
-
'RAH': [amin[0],amin[1],amax[2]], # Right Anterior Head
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
# Update with all other slices
|
|
247
|
-
# PROBABLY SUFFICIENT TO USE ONLY THE OUTER SLICES!!
|
|
248
|
-
for _ in range(1, number_of_slices):
|
|
249
|
-
|
|
250
|
-
ul += slice_cosine * slice_spacing
|
|
251
|
-
ur = ul + row_cosine * (columns-1) * column_spacing
|
|
252
|
-
br = ur + column_cosine * (rows-1) * row_spacing
|
|
253
|
-
bl = ul + column_cosine * (rows-1) * row_spacing
|
|
254
|
-
|
|
255
|
-
corners = np.array([ul, ur, br, bl])
|
|
256
|
-
amin = np.amin(corners, axis=0)
|
|
257
|
-
amax = np.amax(corners, axis=0)
|
|
258
|
-
|
|
259
|
-
box['RPF'][0] = min([box['RPF'][0], amin[0]])
|
|
260
|
-
box['RPF'][1] = max([box['RPF'][1], amax[1]])
|
|
261
|
-
box['RPF'][2] = min([box['RPF'][2], amin[2]])
|
|
262
|
-
|
|
263
|
-
box['LPF'][0] = max([box['LPF'][0], amax[0]])
|
|
264
|
-
box['LPF'][1] = max([box['LPF'][1], amax[1]])
|
|
265
|
-
box['LPF'][2] = min([box['LPF'][2], amin[2]])
|
|
266
|
-
|
|
267
|
-
box['LPH'][0] = max([box['LPH'][0], amax[0]])
|
|
268
|
-
box['LPH'][1] = max([box['LPH'][1], amax[1]])
|
|
269
|
-
box['LPH'][2] = max([box['LPH'][2], amax[2]])
|
|
270
|
-
|
|
271
|
-
box['RPH'][0] = min([box['RPH'][0], amin[0]])
|
|
272
|
-
box['RPH'][1] = max([box['RPH'][1], amax[1]])
|
|
273
|
-
box['RPH'][2] = max([box['RPH'][2], amax[2]])
|
|
274
|
-
|
|
275
|
-
box['RAF'][0] = min([box['RAF'][0], amin[0]])
|
|
276
|
-
box['RAF'][1] = min([box['RAF'][1], amin[1]])
|
|
277
|
-
box['RAF'][2] = min([box['RAF'][2], amin[2]])
|
|
278
|
-
|
|
279
|
-
box['LAF'][0] = max([box['LAF'][0], amax[0]])
|
|
280
|
-
box['LAF'][1] = min([box['LAF'][1], amin[1]])
|
|
281
|
-
box['LAF'][2] = min([box['LAF'][2], amin[2]])
|
|
282
|
-
|
|
283
|
-
box['LAH'][0] = max([box['LAH'][0], amax[0]])
|
|
284
|
-
box['LAH'][1] = min([box['LAH'][1], amin[1]])
|
|
285
|
-
box['LAH'][2] = max([box['LAH'][2], amax[2]])
|
|
286
|
-
|
|
287
|
-
box['RAH'][0] = min([box['RAH'][0], amin[0]])
|
|
288
|
-
box['RAH'][1] = min([box['RAH'][1], amin[1]])
|
|
289
|
-
box['RAH'][2] = max([box['RAH'][2], amax[2]])
|
|
290
|
-
|
|
291
|
-
return box
|
|
292
|
-
|
|
293
1
|
|
|
2
|
+
import numpy as np
|
|
294
3
|
|
|
295
|
-
def standard_affine_matrix(
|
|
296
|
-
bounding_box,
|
|
297
|
-
pixel_spacing,
|
|
298
|
-
slice_spacing,
|
|
299
|
-
orientation = 'axial'):
|
|
300
|
-
|
|
301
|
-
row_spacing = pixel_spacing[0]
|
|
302
|
-
column_spacing = pixel_spacing[1]
|
|
303
|
-
|
|
304
|
-
if orientation == 'axial':
|
|
305
|
-
image_position = bounding_box['RAF']
|
|
306
|
-
row_cosine = np.array([1,0,0])
|
|
307
|
-
column_cosine = np.array([0,1,0])
|
|
308
|
-
slice_cosine = np.array([0,0,1])
|
|
309
|
-
elif orientation == 'coronal':
|
|
310
|
-
image_position = bounding_box['RAH']
|
|
311
|
-
row_cosine = np.array([1,0,0])
|
|
312
|
-
column_cosine = np.array([0,0,-1])
|
|
313
|
-
slice_cosine = np.array([0,1,0])
|
|
314
|
-
elif orientation == 'sagittal':
|
|
315
|
-
image_position = bounding_box['LAH']
|
|
316
|
-
row_cosine = np.array([0,1,0])
|
|
317
|
-
column_cosine = np.array([0,0,-1])
|
|
318
|
-
slice_cosine = np.array([-1,0,0])
|
|
319
|
-
|
|
320
|
-
affine = np.identity(4, dtype=np.float32)
|
|
321
|
-
affine[:3, 0] = row_cosine * column_spacing
|
|
322
|
-
affine[:3, 1] = column_cosine * row_spacing
|
|
323
|
-
affine[:3, 2] = slice_cosine * slice_spacing
|
|
324
|
-
affine[:3, 3] = image_position
|
|
325
|
-
|
|
326
|
-
return affine
|
|
327
4
|
|
|
328
5
|
|
|
329
6
|
def affine_matrix( # single slice function
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
The affine transformation matrix can be used to transform the image from its original coordinates
|
|
337
|
-
to a new set of coordinates.
|
|
338
|
-
|
|
339
|
-
Parameters:
|
|
340
|
-
image_orientation (list): a list of 6 elements representing the ImageOrientationPatient
|
|
341
|
-
DICOM tag for the image. This specifies the orientation of the
|
|
342
|
-
image slices in 3D space.
|
|
343
|
-
image_position (list): a list of 3 elements representing the ImagePositionPatient DICOM
|
|
344
|
-
tag for the slice. This specifies the position of the slice in 3D space.
|
|
345
|
-
pixel_spacing (list): a list of 2 elements representing the PixelSpacing DICOM tag for the
|
|
346
|
-
image. This specifies the spacing between pixels in the rows and columns
|
|
347
|
-
of each slice.
|
|
348
|
-
slice_spacing (float): a float representing the SpacingBetweenSlices DICOM tag for the image. This
|
|
349
|
-
specifies the spacing between slices in the image.
|
|
350
|
-
|
|
351
|
-
Returns:
|
|
352
|
-
np.ndarray: an affine transformation matrix represented as a 4x4 NumPy array with dtype `float32`.
|
|
353
|
-
The matrix can be used to transform the image from its original coordinates to a new set
|
|
354
|
-
of coordinates.
|
|
355
|
-
"""
|
|
356
|
-
|
|
7
|
+
image_orientation, # ImageOrientationPatient
|
|
8
|
+
image_position, # ImagePositionPatient
|
|
9
|
+
pixel_spacing, # PixelSpacing
|
|
10
|
+
slice_spacing, # SpacingBetweenSlices
|
|
11
|
+
# slice_location=None,
|
|
12
|
+
):
|
|
357
13
|
row_spacing = pixel_spacing[0]
|
|
358
14
|
column_spacing = pixel_spacing[1]
|
|
359
15
|
|
|
@@ -366,54 +22,16 @@ def affine_matrix( # single slice function
|
|
|
366
22
|
affine[:3, 1] = column_cosine * row_spacing
|
|
367
23
|
affine[:3, 2] = slice_cosine * slice_spacing
|
|
368
24
|
affine[:3, 3] = image_position
|
|
369
|
-
|
|
370
|
-
return affine
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
def slice_location(
|
|
374
|
-
image_orientation, # ImageOrientationPatient
|
|
375
|
-
image_position, # ImagePositionPatient
|
|
376
|
-
):
|
|
377
|
-
"""Calculate Slice Location"""
|
|
378
|
-
|
|
379
|
-
row_cosine = np.array(image_orientation[:3])
|
|
380
|
-
column_cosine = np.array(image_orientation[3:])
|
|
381
|
-
slice_cosine = np.cross(row_cosine, column_cosine)
|
|
382
|
-
|
|
383
|
-
return np.dot(np.array(image_position), slice_cosine)
|
|
384
25
|
|
|
26
|
+
# if slice_location is not None:
|
|
27
|
+
# # Use slice location to resolve ambiguity in slice cosine
|
|
28
|
+
# loc = np.dot(image_position, slice_cosine)
|
|
29
|
+
# # If loc = -slice_location then flip the slice cosine
|
|
30
|
+
# # Using np.abs() here to avoid effect of small numerical error
|
|
31
|
+
# if np.abs(loc + slice_location) < np.abs(loc - slice_location):
|
|
32
|
+
# affine[:3, 2] *= -1
|
|
385
33
|
|
|
386
|
-
|
|
387
|
-
image_orientation, # ImageOrientationPatient (assume same for all slices)
|
|
388
|
-
image_positions, # ImagePositionPatient for all slices
|
|
389
|
-
pixel_spacing): # PixelSpacing (assume same for all slices)
|
|
390
|
-
|
|
391
|
-
row_spacing = pixel_spacing[0]
|
|
392
|
-
column_spacing = pixel_spacing[1]
|
|
393
|
-
|
|
394
|
-
row_cosine = np.array(image_orientation[:3])
|
|
395
|
-
column_cosine = np.array(image_orientation[3:])
|
|
396
|
-
slice_cosine = np.cross(row_cosine, column_cosine)
|
|
397
|
-
|
|
398
|
-
image_locations = [np.dot(np.array(pos), slice_cosine) for pos in image_positions]
|
|
399
|
-
#number_of_slices = len(image_positions)
|
|
400
|
-
number_of_slices = np.unique(image_locations).size
|
|
401
|
-
if number_of_slices == 1:
|
|
402
|
-
msg = 'Cannot calculate affine matrix for the slice group. \n'
|
|
403
|
-
msg += 'All slices have the same location. \n'
|
|
404
|
-
msg += 'Use the single-slice affine formula instead.'
|
|
405
|
-
raise ValueError(msg)
|
|
406
|
-
slab_thickness = np.amax(image_locations) - np.amin(image_locations)
|
|
407
|
-
slice_spacing = slab_thickness / (number_of_slices - 1)
|
|
408
|
-
image_position_first_slice = image_positions[image_locations.index(np.amin(image_locations))]
|
|
409
|
-
|
|
410
|
-
affine = np.identity(4, dtype=np.float32)
|
|
411
|
-
affine[:3, 0] = row_cosine * column_spacing
|
|
412
|
-
affine[:3, 1] = column_cosine * row_spacing
|
|
413
|
-
affine[:3, 2] = slice_cosine * slice_spacing
|
|
414
|
-
affine[:3, 3] = image_position_first_slice
|
|
415
|
-
|
|
416
|
-
return affine
|
|
34
|
+
return affine
|
|
417
35
|
|
|
418
36
|
|
|
419
37
|
def dismantle_affine_matrix(affine):
|
|
@@ -427,36 +45,16 @@ def dismantle_affine_matrix(affine):
|
|
|
427
45
|
row_cosine = affine[:3, 0] / column_spacing
|
|
428
46
|
column_cosine = affine[:3, 1] / row_spacing
|
|
429
47
|
slice_cosine = affine[:3, 2] / slice_spacing
|
|
48
|
+
# Change to the most common definition - affine[:3, 2] is not even guaranteed to be perp.
|
|
49
|
+
# It is also consistent with def affine_matrix
|
|
50
|
+
# slice_cosine = np.cross(row_cosine, column_cosine)
|
|
430
51
|
return {
|
|
431
52
|
'PixelSpacing': [row_spacing, column_spacing],
|
|
432
|
-
'SpacingBetweenSlices': slice_spacing,
|
|
53
|
+
'SpacingBetweenSlices': slice_spacing,
|
|
433
54
|
'ImageOrientationPatient': row_cosine.tolist() + column_cosine.tolist(),
|
|
434
55
|
'ImagePositionPatient': affine[:3, 3].tolist(), # first slice for a volume
|
|
435
56
|
'slice_cosine': slice_cosine.tolist()}
|
|
436
57
|
|
|
437
|
-
def affine_to_RAH(affine):
|
|
438
|
-
"""Convert to the coordinate system used in NifTi"""
|
|
439
|
-
|
|
440
|
-
rot_180 = np.identity(4, dtype=np.float32)
|
|
441
|
-
rot_180[:2,:2] = [[-1,0],[0,-1]]
|
|
442
|
-
return np.matmul(rot_180, affine)
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
def image_position_patient(affine, number_of_slices):
|
|
446
|
-
slab = dismantle_affine_matrix(affine)
|
|
447
|
-
image_positions = []
|
|
448
|
-
image_locations = []
|
|
449
|
-
for s in range(number_of_slices):
|
|
450
|
-
pos = [
|
|
451
|
-
slab['ImagePositionPatient'][i]
|
|
452
|
-
+ s*slab['SpacingBetweenSlices']*slab['slice_cosine'][i]
|
|
453
|
-
for i in range(3)
|
|
454
|
-
]
|
|
455
|
-
loc = np.dot(np.array(pos), np.array(slab['slice_cosine']))
|
|
456
|
-
image_positions.append(pos)
|
|
457
|
-
image_locations.append(loc)
|
|
458
|
-
return image_positions, image_locations
|
|
459
|
-
|
|
460
58
|
|
|
461
59
|
def clip(array, value_range = None):
|
|
462
60
|
|
|
@@ -467,62 +65,41 @@ def clip(array, value_range = None):
|
|
|
467
65
|
return np.clip(array, value_range[0], value_range[1])
|
|
468
66
|
|
|
469
67
|
|
|
470
|
-
def scale_to_range(array, bits_allocated):
|
|
68
|
+
def scale_to_range(array, bits_allocated, signed=False):
|
|
471
69
|
|
|
472
70
|
range = 2.0**bits_allocated - 1
|
|
71
|
+
if signed:
|
|
72
|
+
minval = -2.0**(bits_allocated-1)
|
|
73
|
+
else:
|
|
74
|
+
minval = 0
|
|
473
75
|
maximum = np.amax(array)
|
|
474
76
|
minimum = np.amin(array)
|
|
475
77
|
if maximum == minimum:
|
|
476
78
|
slope = 1
|
|
477
79
|
else:
|
|
478
80
|
slope = range / (maximum - minimum)
|
|
479
|
-
intercept = -slope * minimum
|
|
480
|
-
array
|
|
481
|
-
array
|
|
81
|
+
intercept = -slope * minimum + minval
|
|
82
|
+
array = array * slope
|
|
83
|
+
array = array + intercept
|
|
482
84
|
|
|
483
85
|
if bits_allocated == 8:
|
|
484
|
-
|
|
86
|
+
if signed:
|
|
87
|
+
return array.astype(np.int8), slope, intercept
|
|
88
|
+
else:
|
|
89
|
+
return array.astype(np.uint8), slope, intercept
|
|
485
90
|
if bits_allocated == 16:
|
|
486
|
-
|
|
91
|
+
if signed:
|
|
92
|
+
return array.astype(np.int16), slope, intercept
|
|
93
|
+
else:
|
|
94
|
+
return array.astype(np.uint16), slope, intercept
|
|
487
95
|
if bits_allocated == 32:
|
|
488
|
-
|
|
96
|
+
if signed:
|
|
97
|
+
return array.astype(np.int32), slope, intercept
|
|
98
|
+
else:
|
|
99
|
+
return array.astype(np.uint32), slope, intercept
|
|
489
100
|
if bits_allocated == 64:
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
if (width is None) or (center is None):
|
|
496
|
-
max = np.amax(array)
|
|
497
|
-
min = np.amin(array)
|
|
498
|
-
else:
|
|
499
|
-
max = center+width/2
|
|
500
|
-
min = center-width/2
|
|
501
|
-
|
|
502
|
-
# Scale pixel array into byte range
|
|
503
|
-
array = np.clip(array, min, max)
|
|
504
|
-
array -= min
|
|
505
|
-
if max > min:
|
|
506
|
-
array *= 255/(max-min)
|
|
507
|
-
array = array.astype(np.ubyte)
|
|
508
|
-
|
|
509
|
-
BGRA = np.empty(array.shape[:2]+(4,), dtype=np.ubyte)
|
|
510
|
-
BGRA[:,:,3] = 255 # Alpha channel
|
|
511
|
-
|
|
512
|
-
if RGBlut is None:
|
|
513
|
-
# Greyscale image
|
|
514
|
-
for c in range(3):
|
|
515
|
-
BGRA[:,:,c] = array
|
|
516
|
-
else:
|
|
517
|
-
# Scale LUT into byte range
|
|
518
|
-
RGBlut *= 255
|
|
519
|
-
RGBlut = RGBlut.astype(np.ubyte)
|
|
520
|
-
# Create RGB array by indexing LUT with pixel array
|
|
521
|
-
for c in range(3):
|
|
522
|
-
BGRA[:,:,c] = RGBlut[array,2-c]
|
|
523
|
-
|
|
524
|
-
return BGRA
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
101
|
+
if signed:
|
|
102
|
+
return array.astype(np.int64), slope, intercept
|
|
103
|
+
else:
|
|
104
|
+
return array.astype(np.uint64), slope, intercept
|
|
528
105
|
|