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/extensions/vreg.py
DELETED
|
@@ -1,1390 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import pandas as pd
|
|
3
|
-
import scipy
|
|
4
|
-
import dbdicom
|
|
5
|
-
import vreg
|
|
6
|
-
from dbdicom import Series
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
def fill_slice_gaps(series, ref, slice_thickness=None, mask=None):
|
|
10
|
-
# slice_thickness - make thin slices for smoother interpolation
|
|
11
|
-
z,t = 'SliceLocation','AcquisitionTime'
|
|
12
|
-
if slice_thickness is not None:
|
|
13
|
-
thick = series.values('SliceThickness')
|
|
14
|
-
series.set_values(slice_thickness, 'SliceThickness')
|
|
15
|
-
input_array = pixel_values(series, dims=(z,t), on=ref)
|
|
16
|
-
input_geom, _ = mask_array(series, on=ref, dim=t, geom=True)
|
|
17
|
-
if slice_thickness is not None:
|
|
18
|
-
series.set_values(thick, 'SliceThickness')
|
|
19
|
-
if mask is not None:
|
|
20
|
-
mask, _ = mask_array(mask, on=ref, dim=t)
|
|
21
|
-
mask = mask[...,0]
|
|
22
|
-
series.message('Interpolating slice gaps..')
|
|
23
|
-
output_array = vreg.fill_gaps(input_array[...,0], input_geom[...,0], mask=mask)
|
|
24
|
-
output_series = ref.copy(SeriesDescription = series.instance().SeriesDescription + '_fill')
|
|
25
|
-
output_series.set_pixel_values(output_array, dims=z)
|
|
26
|
-
return output_series
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
def _equal_geometry(affine1, affine2):
|
|
30
|
-
# Check if both are the same,
|
|
31
|
-
# ignoring the order in the list
|
|
32
|
-
if not isinstance(affine2, list):
|
|
33
|
-
affine2 = [affine2]
|
|
34
|
-
if not isinstance(affine1, list):
|
|
35
|
-
affine1 = [affine1]
|
|
36
|
-
if len(affine1) != len(affine2):
|
|
37
|
-
return False
|
|
38
|
-
unmatched = list(range(len(affine2)))
|
|
39
|
-
for a1 in affine1:
|
|
40
|
-
imatch = None
|
|
41
|
-
for i in unmatched:
|
|
42
|
-
if np.array_equal(a1[0], affine2[i][0]):
|
|
43
|
-
# If a slice group with the same affine is found,
|
|
44
|
-
# check if the image dimensions are the same too.
|
|
45
|
-
dim1 = a1[1][0].array().shape
|
|
46
|
-
dim2 = affine2[i][1][0].array().shape
|
|
47
|
-
if dim1 == dim2:
|
|
48
|
-
imatch = i
|
|
49
|
-
break
|
|
50
|
-
if imatch is not None:
|
|
51
|
-
unmatched.remove(imatch)
|
|
52
|
-
return unmatched == []
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def map_to(source:Series, target:Series, **kwargs):
|
|
56
|
-
"""Map non-zero pixels onto another series"""
|
|
57
|
-
|
|
58
|
-
# Get transformation matrix
|
|
59
|
-
source.status.message('Loading transformation matrices..')
|
|
60
|
-
affine_source = source.affine_matrix()
|
|
61
|
-
affine_target = target.affine_matrix()
|
|
62
|
-
if _equal_geometry(affine_source, affine_target):
|
|
63
|
-
source.status.hide()
|
|
64
|
-
return source
|
|
65
|
-
|
|
66
|
-
if isinstance(affine_target, list):
|
|
67
|
-
mapped_series = []
|
|
68
|
-
for affine_slice_group in affine_target:
|
|
69
|
-
slice_group_target = target.new_sibling()
|
|
70
|
-
slice_group_target.adopt(affine_slice_group[1])
|
|
71
|
-
mapped = _map_series_to_slice_group(source, slice_group_target, affine_source, affine_slice_group[0], **kwargs)
|
|
72
|
-
mapped_series.append(mapped)
|
|
73
|
-
slice_group_target.remove()
|
|
74
|
-
desc = source.instance().SeriesDescription + ' [overlay]'
|
|
75
|
-
mapped_series = dbdicom.merge(mapped_series, inplace=True)
|
|
76
|
-
mapped_series.SeriesDescription = desc
|
|
77
|
-
else:
|
|
78
|
-
mapped_series = _map_series_to_slice_group(source, target, affine_source, affine_target[0], **kwargs)
|
|
79
|
-
return mapped_series
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
def _map_series_to_slice_group(source, target, affine_source, affine_target, **kwargs):
|
|
83
|
-
|
|
84
|
-
if isinstance(affine_source, list):
|
|
85
|
-
array_target, headers_target = target.array(['SliceLocation','AcquisitionTime'], pixels_first=True)
|
|
86
|
-
array = None
|
|
87
|
-
for affine_slice_group in affine_source:
|
|
88
|
-
slice_group_source = source.new_sibling()
|
|
89
|
-
slice_group_source.adopt(affine_slice_group[1])
|
|
90
|
-
array_sg, weight_sg = _map_slice_group_to_slice_group_array(slice_group_source, affine_slice_group[0], target, affine_target, array_target.shape[:3], **kwargs)
|
|
91
|
-
slice_group_source.remove()
|
|
92
|
-
if array is None:
|
|
93
|
-
array = array_sg
|
|
94
|
-
weight = weight_sg
|
|
95
|
-
else:
|
|
96
|
-
array += weight_sg*array_sg
|
|
97
|
-
weight += weight_sg
|
|
98
|
-
nozero = np.where(weight > 0)
|
|
99
|
-
array[nozero] = array[nozero]/weight[nozero]
|
|
100
|
-
|
|
101
|
-
# Create new series
|
|
102
|
-
mapped_series = source.new_sibling(suffix='overlay')
|
|
103
|
-
ns, nt, nk = array.shape[2], array.shape[3], array.shape[4]
|
|
104
|
-
cnt=0
|
|
105
|
-
for t in range(nt):
|
|
106
|
-
for k in range(nk):
|
|
107
|
-
for s in range(ns):
|
|
108
|
-
cnt+=1
|
|
109
|
-
source.progress(cnt, ns*nt*nk, 'Saving results..')
|
|
110
|
-
image = headers_target[s,0,0].copy_to(mapped_series)
|
|
111
|
-
image.AcquisitionTime = t
|
|
112
|
-
image.set_array(array[:,:,s,t,k])
|
|
113
|
-
return mapped_series
|
|
114
|
-
else:
|
|
115
|
-
return _map_slice_group_to_slice_group(source, affine_source[0], target, affine_target, **kwargs)
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _map_slice_group_to_slice_group_array(source, affine_source, target, output_affine, target_shape, **kwargs):
|
|
119
|
-
|
|
120
|
-
# Get source arrays
|
|
121
|
-
array_source, headers_source = source.array(['SliceLocation','AcquisitionTime'], pixels_first=True)
|
|
122
|
-
|
|
123
|
-
# Get message status updates
|
|
124
|
-
source_desc = source.instance().SeriesDescription
|
|
125
|
-
target_desc = target.instance().SeriesDescription
|
|
126
|
-
message = 'Mapping ' + source_desc + ' onto ' + target_desc
|
|
127
|
-
source.message(message)
|
|
128
|
-
|
|
129
|
-
array_mapped = np.empty(target_shape + array_source.shape[3:])
|
|
130
|
-
weights_mapped = np.empty(target_shape + array_source.shape[3:])
|
|
131
|
-
slice_thickness = headers_source[0,0,0].SliceThickness
|
|
132
|
-
|
|
133
|
-
for t in range(array_source.shape[3]):
|
|
134
|
-
for k in range(array_source.shape[4]):
|
|
135
|
-
array_mapped[:,:,:,t,k], _ = vreg.affine_reslice_slice_by_slice(
|
|
136
|
-
array_source[:,:,:,t,k], affine_source,
|
|
137
|
-
output_affine, output_shape=target_shape,
|
|
138
|
-
slice_thickness = slice_thickness,
|
|
139
|
-
**kwargs,
|
|
140
|
-
)
|
|
141
|
-
weights_mapped[:,:,:,t,k], _ = vreg.affine_reslice_slice_by_slice(
|
|
142
|
-
np.ones(array_source.shape[:3]), affine_source,
|
|
143
|
-
output_affine, output_shape=target_shape,
|
|
144
|
-
slice_thickness = slice_thickness,
|
|
145
|
-
**kwargs,
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
return array_mapped, weights_mapped
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _map_slice_group_to_slice_group(source, affine_source, target, output_affine, **kwargs):
|
|
152
|
-
|
|
153
|
-
# Get source arrays
|
|
154
|
-
array_source, headers_source = source.array(['SliceLocation','AcquisitionTime'], pixels_first=True)
|
|
155
|
-
array_target, headers_target = target.array(['SliceLocation','AcquisitionTime'], pixels_first=True)
|
|
156
|
-
|
|
157
|
-
# Get message status updates
|
|
158
|
-
source_desc = source.instance().SeriesDescription
|
|
159
|
-
target_desc = target.instance().SeriesDescription
|
|
160
|
-
message = 'Mapping ' + source_desc + ' onto ' + target_desc
|
|
161
|
-
source.message(message)
|
|
162
|
-
|
|
163
|
-
# Create new series
|
|
164
|
-
# Retain source acquisition times
|
|
165
|
-
# Assign acquisition time of slice=0 to all slices
|
|
166
|
-
mapped_series = source.new_sibling(suffix='overlay')
|
|
167
|
-
nt, nk = array_source.shape[3], array_source.shape[4]
|
|
168
|
-
ns = headers_target.shape[0]
|
|
169
|
-
acq_times = [headers_source[0,t,0].AcquisitionTime for t in range(nt)]
|
|
170
|
-
slice_thickness = headers_source[0,0,0].SliceThickness
|
|
171
|
-
cnt=0
|
|
172
|
-
for t in range(nt):
|
|
173
|
-
for k in range(nk):
|
|
174
|
-
array_mapped, _ = vreg.affine_reslice_slice_by_slice(
|
|
175
|
-
array_source[:,:,:,t,k],
|
|
176
|
-
affine_source,
|
|
177
|
-
output_affine,
|
|
178
|
-
output_shape=array_target.shape[:3],
|
|
179
|
-
slice_thickness = slice_thickness, **kwargs)
|
|
180
|
-
for s in range(ns):
|
|
181
|
-
cnt+=1
|
|
182
|
-
source.progress(cnt, ns*nt*nk, 'Saving results..')
|
|
183
|
-
image = headers_target[s,0,0].copy_to(mapped_series)
|
|
184
|
-
image.AcquisitionTime = acq_times[t]
|
|
185
|
-
image.set_array(array_mapped[:,:,s])
|
|
186
|
-
return mapped_series
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def mask_array(mask, on=None, dim='InstanceNumber', geom=False):
|
|
190
|
-
|
|
191
|
-
if on is None:
|
|
192
|
-
# geom keyword not yet implemented
|
|
193
|
-
return dbdicom.array(mask, sortby=['SliceLocation', dim], mask=True, pixels_first=True, first_volume=True)
|
|
194
|
-
|
|
195
|
-
# Get transformation matrix
|
|
196
|
-
mask.message('Loading transformation matrices..')
|
|
197
|
-
affine_source = mask.affine_matrix()
|
|
198
|
-
affine_target = on.affine_matrix()
|
|
199
|
-
|
|
200
|
-
if isinstance(affine_target, list):
|
|
201
|
-
mapped_arrays = []
|
|
202
|
-
mapped_headers = []
|
|
203
|
-
for affine_slice_group_target in affine_target:
|
|
204
|
-
mapped, headers = _map_mask_series_to_slice_group(
|
|
205
|
-
mask,
|
|
206
|
-
affine_slice_group_target[1],
|
|
207
|
-
affine_source,
|
|
208
|
-
affine_slice_group_target[0],
|
|
209
|
-
dim=dim, geom=geom,
|
|
210
|
-
)
|
|
211
|
-
mapped_arrays.append(mapped)
|
|
212
|
-
mapped_headers.append(headers)
|
|
213
|
-
else:
|
|
214
|
-
mapped_arrays, mapped_headers = _map_mask_series_to_slice_group(
|
|
215
|
-
mask, on, affine_source, affine_target[0], dim=dim, geom=geom)
|
|
216
|
-
return mapped_arrays, mapped_headers
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def _map_mask_series_to_slice_group(source, target, affine_source, affine_target, **kwargs):
|
|
220
|
-
|
|
221
|
-
if isinstance(affine_source, list):
|
|
222
|
-
mapped_arrays = []
|
|
223
|
-
for affine_slice_group in affine_source:
|
|
224
|
-
mapped, headers = _map_mask_slice_group_to_slice_group(
|
|
225
|
-
affine_slice_group[1],
|
|
226
|
-
target,
|
|
227
|
-
affine_slice_group[0],
|
|
228
|
-
affine_target,
|
|
229
|
-
**kwargs,
|
|
230
|
-
)
|
|
231
|
-
mapped_arrays.append(mapped)
|
|
232
|
-
array = np.logical_or(mapped_arrays[0], mapped_arrays[1])
|
|
233
|
-
for a in mapped_arrays[2:]:
|
|
234
|
-
array = np.logical_or(array, a)
|
|
235
|
-
return array, headers
|
|
236
|
-
else:
|
|
237
|
-
return _map_mask_slice_group_to_slice_group(source, target, affine_source[0], affine_target, **kwargs)
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
def _map_mask_slice_group_to_slice_group(source, target, affine_source, affine_target, dim='InstanceNumber', geom=False):
|
|
241
|
-
|
|
242
|
-
if isinstance(source, list):
|
|
243
|
-
status = source[0].status
|
|
244
|
-
else:
|
|
245
|
-
status = source.status
|
|
246
|
-
|
|
247
|
-
# Get arrays
|
|
248
|
-
array_source, headers_source = dbdicom.array(source, sortby=['SliceLocation',dim], pixels_first=True, first_volume=True)
|
|
249
|
-
array_target, headers_target = dbdicom.array(target, sortby=['SliceLocation',dim], pixels_first=True, first_volume=True)
|
|
250
|
-
|
|
251
|
-
if geom:
|
|
252
|
-
# mask shows geometry of source
|
|
253
|
-
array_source = np.full(array_source.shape, 1)
|
|
254
|
-
|
|
255
|
-
# For mapping mask onto series, the time dimensions must be the same.
|
|
256
|
-
# If they are not, the mask is extruded on to the series time dimensions.
|
|
257
|
-
nk = array_target.shape[3]
|
|
258
|
-
if array_source.shape[3] != nk:
|
|
259
|
-
status.message('Extruding ROI on time series..')
|
|
260
|
-
array_source = np.amax(array_source, axis=-1)
|
|
261
|
-
array_source = np.repeat(array_source[:,:,:,np.newaxis], nk, axis=3)
|
|
262
|
-
|
|
263
|
-
# If the dimensions and affines are equal there is nothing to do
|
|
264
|
-
if np.array_equal(affine_source, affine_target):
|
|
265
|
-
if array_source.shape == array_target.shape:
|
|
266
|
-
# Make sure the result is a mask
|
|
267
|
-
array_source[array_source > 0.5] = 1
|
|
268
|
-
array_source[array_source <= 0.5] = 0
|
|
269
|
-
return array_source, headers_target
|
|
270
|
-
|
|
271
|
-
slice_thickness = headers_source[0,0].SliceThickness
|
|
272
|
-
array_target = np.empty(array_target.shape[:3] + (array_source.shape[3],))
|
|
273
|
-
for t in range(array_source.shape[3]):
|
|
274
|
-
array_target[:,:,:,t], _ = vreg.affine_reslice_slice_by_slice(
|
|
275
|
-
array_source[:,:,:,t],
|
|
276
|
-
affine_source,
|
|
277
|
-
affine_target,
|
|
278
|
-
output_shape = array_target.shape[:3],
|
|
279
|
-
slice_thickness = slice_thickness,
|
|
280
|
-
mask=True)
|
|
281
|
-
|
|
282
|
-
return array_target, headers_target
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def mask_statistics(masks, images):
|
|
286
|
-
if not isinstance(masks, list):
|
|
287
|
-
masks = [masks]
|
|
288
|
-
if not isinstance(images, list):
|
|
289
|
-
images = [images]
|
|
290
|
-
df_all_masks = None
|
|
291
|
-
for mask in masks:
|
|
292
|
-
df_mask = None
|
|
293
|
-
for img in images:
|
|
294
|
-
data = mask_values(mask, img)
|
|
295
|
-
df_img = mask_data_statistics(data, mask, img)
|
|
296
|
-
if df_mask is None:
|
|
297
|
-
df_mask = df_img
|
|
298
|
-
else:
|
|
299
|
-
df_mask = pd.concat([df_mask, df_img], ignore_index=True)
|
|
300
|
-
if df_all_masks is None:
|
|
301
|
-
df_all_masks = df_mask
|
|
302
|
-
else:
|
|
303
|
-
df_all_masks = pd.concat([df_all_masks, df_mask], ignore_index=True)
|
|
304
|
-
return df_all_masks
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
def mask_values(mask, image):
|
|
308
|
-
msk_arr, img_hdrs = mask_array(mask, on=image)
|
|
309
|
-
values = _mask_data_slice_groups(msk_arr, img_hdrs)
|
|
310
|
-
return values
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
def mask_data_statistics(data, mask, image):
|
|
314
|
-
# Get mask array
|
|
315
|
-
#msk_arr, img_hdrs = mask_array(mask, on=image)
|
|
316
|
-
#data = _mask_data_slice_groups(msk_arr, img_hdrs)
|
|
317
|
-
props = _summary_stats(data)
|
|
318
|
-
instance = image.instance()
|
|
319
|
-
columns = ['PatientID', 'StudyDescription', 'SeriesDescription', 'Region of Interest', 'Parameter', 'Value', 'Unit']
|
|
320
|
-
ids = [instance.PatientID, instance.StudyDescription, instance.SeriesDescription, mask.instance().SeriesDescription]
|
|
321
|
-
data = []
|
|
322
|
-
for par, val in props.items():
|
|
323
|
-
row = ids + [par, val, '']
|
|
324
|
-
data.append(row)
|
|
325
|
-
return pd.DataFrame(data, columns=columns)
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def _mask_data_slice_groups(msk_arr, img_hdrs):
|
|
329
|
-
if isinstance(msk_arr, list):
|
|
330
|
-
# Loop over slice groups
|
|
331
|
-
data = [_mask_data(arr, img_hdrs[m]) for m, arr in enumerate(msk_arr)]
|
|
332
|
-
data = [d for d in data if d is not None]
|
|
333
|
-
if data == []:
|
|
334
|
-
data = None
|
|
335
|
-
else:
|
|
336
|
-
data = np.concatenate(data)
|
|
337
|
-
else:
|
|
338
|
-
# single slice group
|
|
339
|
-
data = _mask_data(msk_arr, img_hdrs)
|
|
340
|
-
return data
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
def _mask_data(msk_arr, imgs):
|
|
344
|
-
data = []
|
|
345
|
-
for i, image in np.ndenumerate(imgs):
|
|
346
|
-
if image is not None:
|
|
347
|
-
if len(i) == 1:
|
|
348
|
-
mask = msk_arr[:,:,i[0]]
|
|
349
|
-
elif len(i) == 2:
|
|
350
|
-
mask = msk_arr[:,:,i[0],i[1]]
|
|
351
|
-
if np.count_nonzero(mask) > 0:
|
|
352
|
-
array = image.array()
|
|
353
|
-
array = array[mask > 0.5]
|
|
354
|
-
data.append(array.ravel())
|
|
355
|
-
if data == []:
|
|
356
|
-
return None
|
|
357
|
-
else:
|
|
358
|
-
return np.concatenate(data)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def _summary_stats(data):
|
|
362
|
-
if data is None:
|
|
363
|
-
return {}
|
|
364
|
-
return {
|
|
365
|
-
'Mean': np.mean(data),
|
|
366
|
-
'Standard deviation': np.std(data),
|
|
367
|
-
'Maximum': np.amax(data),
|
|
368
|
-
'Minimum': np.amin(data),
|
|
369
|
-
'2.5% percentile': np.percentile(data, 2.5),
|
|
370
|
-
'5% percentile': np.percentile(data, 5),
|
|
371
|
-
'10% percentile': np.percentile(data, 10),
|
|
372
|
-
'25% percentile': np.percentile(data, 25),
|
|
373
|
-
'Median': np.percentile(data, 50),
|
|
374
|
-
'75% percentile': np.percentile(data, 75),
|
|
375
|
-
'90% percentile': np.percentile(data, 90),
|
|
376
|
-
'95% percentile': np.percentile(data, 95),
|
|
377
|
-
'97.5% percentile': np.percentile(data, 97.5),
|
|
378
|
-
'Range': np.amax(data) - np.amin(data),
|
|
379
|
-
'Interquartile range':np.percentile(data, 75) - np.percentile(data, 25),
|
|
380
|
-
'90 percent range': np.percentile(data, 95) - np.percentile(data, 5),
|
|
381
|
-
'Coefficient of variation': np.std(data)/np.mean(data),
|
|
382
|
-
'Heterogeneity': (np.percentile(data, 95) - np.percentile(data, 5))/np.percentile(data, 50),
|
|
383
|
-
'Kurtosis': scipy.stats.kurtosis(data),
|
|
384
|
-
'Skewness': scipy.stats.skew(data),
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
# no longer public - replace by vreg.pixel_values()
|
|
388
|
-
# Needs an approach that does no create a DICOM series first
|
|
389
|
-
def array(series, on=None, **kwargs):
|
|
390
|
-
"""Return the array overlaid on another series"""
|
|
391
|
-
|
|
392
|
-
if on is None:
|
|
393
|
-
array, _ = series.array(**kwargs)
|
|
394
|
-
else:
|
|
395
|
-
series_map = map_to(series, on)
|
|
396
|
-
array, _ = series_map.array(**kwargs)
|
|
397
|
-
if series_map != series:
|
|
398
|
-
series_map.remove()
|
|
399
|
-
return array
|
|
400
|
-
|
|
401
|
-
def pixel_values(series, dims=('InstanceNumber',), on=None):
|
|
402
|
-
# Wrapper for array following new API
|
|
403
|
-
|
|
404
|
-
if np.isscalar(dims):
|
|
405
|
-
dims = (dims,)
|
|
406
|
-
|
|
407
|
-
return array(series, on=on, sortby=list(dims), pixels_first=True, first_volume=True)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
def print_current(vk):
|
|
413
|
-
print(vk)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def _get_input_volume(series:Series):
|
|
417
|
-
if series is None:
|
|
418
|
-
return None, None
|
|
419
|
-
desc = series.instance().SeriesDescription
|
|
420
|
-
affine = series.unique_affines()
|
|
421
|
-
if affine.shape[0] > 1:
|
|
422
|
-
msg = 'This function only works for series with a single slice group. \n'
|
|
423
|
-
msg += 'Multiple slice groups detected in ' + desc + ' - please split the series first.'
|
|
424
|
-
raise ValueError(msg)
|
|
425
|
-
else:
|
|
426
|
-
affine = affine[0,:,:]
|
|
427
|
-
#array, headers = series.array('SliceLocation', pixels_first=True, first_volume=True)
|
|
428
|
-
array = series.pixel_values(dims=('SliceLocation',))
|
|
429
|
-
if array is None:
|
|
430
|
-
msg = 'Series ' + desc + ' is empty - cannot perform alignment.'
|
|
431
|
-
raise ValueError(msg)
|
|
432
|
-
return array, affine
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
def _get_input(moving, static, region=None, margin=0):
|
|
436
|
-
|
|
437
|
-
array_moving, affine_moving = _get_input_volume(moving)
|
|
438
|
-
array_static, affine_static = _get_input_volume(static)
|
|
439
|
-
|
|
440
|
-
moving.message('Performing coregistration. Please be patient. Its hard work and I need to concentrate..')
|
|
441
|
-
|
|
442
|
-
# If a region is provided, use it extract a bounding box around the static array
|
|
443
|
-
if region is not None:
|
|
444
|
-
array_region, affine_region = _get_input_volume(region)
|
|
445
|
-
array_static, affine_static = vreg.mask_volume(array_static, affine_static, array_region, affine_region, margin)
|
|
446
|
-
|
|
447
|
-
return array_static, affine_static, array_moving, affine_moving
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
def find_translation(moving:Series, static:Series, tolerance=0.1, metric='mutual information', region:Series=None, margin:float=0)->np.ndarray:
|
|
452
|
-
"""Find the translation that maps a moving volume onto a static volume.
|
|
453
|
-
|
|
454
|
-
Args:
|
|
455
|
-
moving (dbdicom.Series): Series with the moving volume.
|
|
456
|
-
static (dbdicom.Series): Series with the static volume
|
|
457
|
-
tolerance (float, optional): Positive tolerance parameter to decide convergence of the gradient descent. A smaller value means a more accurate solution but also a lomger computation time. Defaults to 0.1.
|
|
458
|
-
metric (str, option): Determines which metric to use in the optimization. Current options are 'mutual information' (default) or 'sum of squares'.
|
|
459
|
-
region (dbdicom.Series, optional): Series with region of interest to restrict the alignment. The translation will be chosen based on the goodness of the alignment in the bounding box of this region. If none is provided, the entire volume is used. Defaults to None.
|
|
460
|
-
margin (float, optional): in case a region is provided, this specifies a margin (in mm) to take around the region. Default is 0 (no margin)
|
|
461
|
-
|
|
462
|
-
Returns:
|
|
463
|
-
np.ndarray: 3-element numpy array with values of the translation that maps the moving volume on to the static volume.
|
|
464
|
-
"""
|
|
465
|
-
|
|
466
|
-
array_static, affine_static, array_moving, affine_moving = _get_input(moving, static, region=region, margin=margin)
|
|
467
|
-
|
|
468
|
-
# Define initial values and optimization
|
|
469
|
-
_, _, static_pixel_spacing = vreg.affine_components(affine_static)
|
|
470
|
-
optimization = {
|
|
471
|
-
'method': 'GD',
|
|
472
|
-
'options': {'gradient step': static_pixel_spacing, 'tolerance': tolerance},
|
|
473
|
-
'callback': lambda vk: moving.status.message('Current parameter: ' + str(vk)),
|
|
474
|
-
}
|
|
475
|
-
func = {
|
|
476
|
-
'sum of squares': vreg.sum_of_squares,
|
|
477
|
-
'mutual information': vreg.mutual_information,
|
|
478
|
-
'interaction': vreg.interaction,
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
# Align volumes
|
|
482
|
-
try:
|
|
483
|
-
translation_estimate = vreg.align(
|
|
484
|
-
moving = array_moving,
|
|
485
|
-
moving_affine = affine_moving,
|
|
486
|
-
static = array_static,
|
|
487
|
-
static_affine = affine_static,
|
|
488
|
-
parameters = np.zeros(3, dtype=np.float32),
|
|
489
|
-
resolutions = [4,2,1],
|
|
490
|
-
transformation = vreg.translate,
|
|
491
|
-
metric = func[metric],
|
|
492
|
-
optimization = optimization,
|
|
493
|
-
)
|
|
494
|
-
except:
|
|
495
|
-
print('Failed to align volumes..')
|
|
496
|
-
translation_estimate = None
|
|
497
|
-
|
|
498
|
-
return translation_estimate
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
def apply_translation(series_moving:Series, parameters:np.ndarray, target:Series=None, description:str=None)->Series:
|
|
502
|
-
"""Apply active translation of an image volume.
|
|
503
|
-
|
|
504
|
-
Args:
|
|
505
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
506
|
-
parameters (np.ndarray): three-element numpy array with coordinates of the translation in the absolute reference frame (mm).
|
|
507
|
-
target (dbdicom.Series, optional): If provided, the result is mapped onto the geometry of this series. If none is provided, the result has the same geometry of the moving series. Defaults to None.
|
|
508
|
-
|
|
509
|
-
Raises:
|
|
510
|
-
ValueError: If the moving series contains multiple slice groups with different orientations.
|
|
511
|
-
ValueError: If the array to be moved is empty.
|
|
512
|
-
|
|
513
|
-
Returns:
|
|
514
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the translated volume.
|
|
515
|
-
"""
|
|
516
|
-
|
|
517
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
518
|
-
affine_moving = series_moving.unique_affines()
|
|
519
|
-
if affine_moving.shape[0] > 1:
|
|
520
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
521
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
522
|
-
msg += 'Please split the series first.'
|
|
523
|
-
raise ValueError(msg)
|
|
524
|
-
else:
|
|
525
|
-
affine_moving = affine_moving[0,:,:]
|
|
526
|
-
|
|
527
|
-
array_moving = series_moving.pixel_values(dims=('SliceLocation',))
|
|
528
|
-
if array_moving.size == 0:
|
|
529
|
-
msg = desc_moving + ' is empty - cannot perform alignment.'
|
|
530
|
-
raise ValueError(msg)
|
|
531
|
-
|
|
532
|
-
if target is None:
|
|
533
|
-
shape_moved = array_moving.shape
|
|
534
|
-
affine_moved = affine_moving
|
|
535
|
-
else:
|
|
536
|
-
array_moved = target.pixel_values(dims=('SliceLocation',))
|
|
537
|
-
shape_moved = array_moved.shape
|
|
538
|
-
affine_moved = target.affine()
|
|
539
|
-
|
|
540
|
-
series_moving.message('Applying translation..')
|
|
541
|
-
if description is None:
|
|
542
|
-
description = desc_moving + ' [translation]'
|
|
543
|
-
array_moved = vreg.translate(array_moving, affine_moving, shape_moved, affine_moved, parameters)
|
|
544
|
-
series_moved = series_moving.new_sibling(SeriesDescription=description)
|
|
545
|
-
series_moved.set_pixel_values(array_moved, coords={'SliceLocation': np.arange(array_moved.shape[-1])})
|
|
546
|
-
series_moved.set_affine(affine_moved)
|
|
547
|
-
return series_moved
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
def apply_passive_translation(series_moving:Series, parameters:np.ndarray)->Series:
|
|
551
|
-
"""Apply passive translation of an image volume.
|
|
552
|
-
|
|
553
|
-
Args:
|
|
554
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
555
|
-
parameters (np.ndarray): 6-element numpy array with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The vectors are defined in an absolute reference frame in units of mm.
|
|
556
|
-
|
|
557
|
-
Raises:
|
|
558
|
-
ValueError: If the moving series contains multiple slice groups with different orentations.
|
|
559
|
-
ValueError: If the array to be moved is empty.
|
|
560
|
-
|
|
561
|
-
Returns:
|
|
562
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the transformed volume.
|
|
563
|
-
"""
|
|
564
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
565
|
-
affine_moving = series_moving.unique_affines()
|
|
566
|
-
if affine_moving.shape[0] > 1:
|
|
567
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
568
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
569
|
-
msg += 'Please split the series first.'
|
|
570
|
-
raise ValueError(msg)
|
|
571
|
-
else:
|
|
572
|
-
affine_moving = affine_moving[0,:,:]
|
|
573
|
-
|
|
574
|
-
series_moving.message('Applying passive rigid transformation..')
|
|
575
|
-
output_affine = vreg.passive_translation(affine_moving, parameters)
|
|
576
|
-
series_moved = series_moving.copy(SeriesDescription = desc_moving + ' [passive translation]')
|
|
577
|
-
series_moved.set_affine(output_affine, dims=('SliceLocation',), multislice=True)
|
|
578
|
-
return series_moved
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
def find_rigid_transformation(moving:Series, static:Series, tolerance=0.1, metric='mutual information', region:Series=None, margin:float=0, prereg=False)->np.ndarray:
|
|
582
|
-
"""Find the rigid transform that maps a moving volume onto a static volume.
|
|
583
|
-
|
|
584
|
-
Args:
|
|
585
|
-
moving (dbdicom.Series): Series with the moving volume.
|
|
586
|
-
static (dbdicom.Series): Series with the static volume
|
|
587
|
-
tolerance (float, optional): Positive tolerance parameter to decide convergence of the gradient descent. A smaller value means a more accurate solution but also a lomger computation time. Defaults to 0.1.
|
|
588
|
-
metric (str, option): Determines which metric to use in the optimization. Current options are 'mutual information' (default) or 'sum of squares'.
|
|
589
|
-
region (dbdicom.Series, optional): Series with region of interest to restrict the alignment. The rigid transform will be chosen based on the goodness of the alignment in the bounding box of this region. If none is provided, the entire volume is used. Defaults to None.
|
|
590
|
-
margin (float, optional): in case a region is provided, this specifies a margin (in mm) to take around the region. Default is 0 (no margin).
|
|
591
|
-
|
|
592
|
-
Returns:
|
|
593
|
-
np.ndarray: 6-element numpy array with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The vectors are defined in an absolute reference frame in units of mm.
|
|
594
|
-
"""
|
|
595
|
-
|
|
596
|
-
if prereg:
|
|
597
|
-
translation = find_translation(moving, static, tolerance=tolerance, metric=metric, region=region, margin=margin)
|
|
598
|
-
rigid_estimate = np.concatenate([np.zeros(3, dtype=np.float32), translation])
|
|
599
|
-
else:
|
|
600
|
-
rigid_estimate = np.zeros(6, dtype=np.float32)
|
|
601
|
-
|
|
602
|
-
array_static, affine_static, array_moving, affine_moving = _get_input(moving, static, region=region, margin=margin)
|
|
603
|
-
|
|
604
|
-
# Define initial values and optimization
|
|
605
|
-
_, _, static_pixel_spacing = vreg.affine_components(affine_static)
|
|
606
|
-
rot_gradient_step, translation_gradient_step, _ = vreg.affine_resolution(array_static.shape, static_pixel_spacing)
|
|
607
|
-
gradient_step = np.concatenate((1.0*rot_gradient_step, 0.5*translation_gradient_step))
|
|
608
|
-
optimization = {
|
|
609
|
-
'method': 'GD',
|
|
610
|
-
'options': {'gradient step': gradient_step, 'tolerance': tolerance},
|
|
611
|
-
'callback': lambda vk: moving.status.message('Current parameter: ' + str(vk)),
|
|
612
|
-
}
|
|
613
|
-
func = {
|
|
614
|
-
'sum of squares': vreg.sum_of_squares,
|
|
615
|
-
'mutual information': vreg.mutual_information,
|
|
616
|
-
'interaction': vreg.interaction,
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
# Align volumes
|
|
620
|
-
try:
|
|
621
|
-
rigid_estimate = vreg.align(
|
|
622
|
-
moving = array_moving,
|
|
623
|
-
moving_affine = affine_moving,
|
|
624
|
-
static = array_static,
|
|
625
|
-
static_affine = affine_static,
|
|
626
|
-
parameters = rigid_estimate,
|
|
627
|
-
resolutions = [4,2,1],
|
|
628
|
-
transformation = vreg.rigid,
|
|
629
|
-
metric = func[metric],
|
|
630
|
-
optimization = optimization,
|
|
631
|
-
)
|
|
632
|
-
except:
|
|
633
|
-
print('Failed to align volumes..')
|
|
634
|
-
rigid_estimate = None
|
|
635
|
-
|
|
636
|
-
return rigid_estimate
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
def apply_rigid_transformation(series_moving:Series, parameters:np.ndarray, target:Series=None, description:str=None)->Series:
|
|
640
|
-
"""Apply rigid transformation of an image volume.
|
|
641
|
-
|
|
642
|
-
Args:
|
|
643
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
644
|
-
parameters (np.ndarray): 6-element numpy array with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The vectors are defined in an absolute reference frame in units of mm.
|
|
645
|
-
target (dbdicom.Series, optional): If provided, the result is mapped onto the geometry of this series. If none is provided, the result has the same geometry of the moving series. Defaults to None.
|
|
646
|
-
description (str, optional): Series description of the resulting series. Defaults to None.
|
|
647
|
-
|
|
648
|
-
Raises:
|
|
649
|
-
ValueError: If the moving series contains multiple slice groups with different orentations.
|
|
650
|
-
ValueError: If the array to be moved is empty.
|
|
651
|
-
|
|
652
|
-
Returns:
|
|
653
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the transformed volume.
|
|
654
|
-
"""
|
|
655
|
-
# TODO: target is not the right word. geometry?
|
|
656
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
657
|
-
affine_moving = series_moving.unique_affines()
|
|
658
|
-
if affine_moving.shape[0] > 1:
|
|
659
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
660
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
661
|
-
msg += 'Please split the series first.'
|
|
662
|
-
raise ValueError(msg)
|
|
663
|
-
else:
|
|
664
|
-
affine_moving = affine_moving[0,:,:]
|
|
665
|
-
|
|
666
|
-
array_moving = series_moving.pixel_values(dims=('SliceLocation',))
|
|
667
|
-
if array_moving.size == 0:
|
|
668
|
-
msg = desc_moving + ' is empty - cannot perform alignment.'
|
|
669
|
-
raise ValueError(msg)
|
|
670
|
-
|
|
671
|
-
if target is None:
|
|
672
|
-
shape_moved = array_moving.shape
|
|
673
|
-
affine_moved = affine_moving
|
|
674
|
-
else:
|
|
675
|
-
array_moved = target.pixel_values(dims=('SliceLocation',))
|
|
676
|
-
shape_moved = array_moved.shape
|
|
677
|
-
affine_moved = target.affine()
|
|
678
|
-
|
|
679
|
-
series_moving.message('Applying rigid transformation..')
|
|
680
|
-
array_moved = vreg.rigid(array_moving, affine_moving, shape_moved, affine_moved, parameters)
|
|
681
|
-
if description is None:
|
|
682
|
-
description = desc_moving + ' [rigid]'
|
|
683
|
-
series_moved = series_moving.new_sibling(SeriesDescription = description)
|
|
684
|
-
series_moved.set_pixel_values(array_moved, slice={'SliceLocation': np.arange(array_moved.shape[-1])})
|
|
685
|
-
series_moved.set_affine(affine_moved)
|
|
686
|
-
return series_moved
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
def apply_passive_rigid_transformation(series_moving:Series, parameters:np.ndarray,description:str=None)->Series:
|
|
690
|
-
"""Apply passive rigid transformation of an image volume.
|
|
691
|
-
|
|
692
|
-
Args:
|
|
693
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
694
|
-
parameters (np.ndarray): 6-element numpy array with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The vectors are defined in an absolute reference frame in units of mm.
|
|
695
|
-
|
|
696
|
-
Raises:
|
|
697
|
-
ValueError: If the moving series contains multiple slice groups with different orentations.
|
|
698
|
-
ValueError: If the array to be moved is empty.
|
|
699
|
-
|
|
700
|
-
Returns:
|
|
701
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the transformed volume.
|
|
702
|
-
"""
|
|
703
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
704
|
-
affine_moving = series_moving.unique_affines()
|
|
705
|
-
if affine_moving.shape[0] > 1:
|
|
706
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
707
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
708
|
-
msg += 'Please split the series first.'
|
|
709
|
-
raise ValueError(msg)
|
|
710
|
-
else:
|
|
711
|
-
affine_moving = affine_moving[0,:,:]
|
|
712
|
-
|
|
713
|
-
series_moving.message('Applying passive rigid transformation..')
|
|
714
|
-
output_affine = vreg.passive_rigid_transform(affine_moving, parameters)
|
|
715
|
-
if description is None:
|
|
716
|
-
description = desc_moving + ' [passive rigid]'
|
|
717
|
-
series_moved = series_moving.copy(SeriesDescription = description)
|
|
718
|
-
series_moved.set_affine(output_affine, dims=('SliceLocation',), multislice=True)
|
|
719
|
-
return series_moved
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
def find_sbs_inslice_translation(moving:Series, static:Series, tolerance=0.1, metric='mutual information', region:Series=None, margin:float=0)->np.ndarray:
|
|
723
|
-
"""Find the slice-by-slice inslice translation that maps a moving volume onto a static volume.
|
|
724
|
-
|
|
725
|
-
Args:
|
|
726
|
-
moving (dbdicom.Series): Series with the moving volume.
|
|
727
|
-
static (dbdicom.Series): Series with the static volume
|
|
728
|
-
tolerance (float, optional): Positive tolerance parameter to decide convergence of the gradient descent. A smaller value means a more accurate solution but also a lomger computation time. Defaults to 0.1.
|
|
729
|
-
metric (str, option): Determines which metric to use in the optimization. Current options are 'mutual information' (default) or 'sum of squares'.
|
|
730
|
-
region (dbdicom.Series, optional): Series with region of interest to restrict the alignment. The translation will be chosen based on the goodness of the alignment in the bounding box of this region. If none is provided, the entire volume is used. Defaults to None.
|
|
731
|
-
margin (float, optional): in case a region is provided, this specifies a margin (in mm) to take around the region. Default is 0 (no margin).
|
|
732
|
-
|
|
733
|
-
Returns:
|
|
734
|
-
np.ndarray: list of 3-element numpy arrays with values of the translation that maps the moving volume onto the static volume. The list has one entry per slice of the volume.
|
|
735
|
-
"""
|
|
736
|
-
array_static, affine_static, array_moving, affine_moving = _get_input(moving, static, region=region, margin=margin)
|
|
737
|
-
func = {
|
|
738
|
-
'sum of squares': vreg.sum_of_squares,
|
|
739
|
-
'mutual information': vreg.mutual_information,
|
|
740
|
-
'interaction': vreg.interaction,
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
# Perform coregistration
|
|
744
|
-
translation_estimate = np.zeros(2, dtype=np.float32)
|
|
745
|
-
_, _, static_pixel_spacing = vreg.affine_components(affine_static)
|
|
746
|
-
optimization = {
|
|
747
|
-
'method': 'GD',
|
|
748
|
-
'options': {'gradient step': 0.5*static_pixel_spacing[:2], 'tolerance': tolerance},
|
|
749
|
-
# 'callback': lambda vk: moving.message('Current parameter: ' + str(vk)),
|
|
750
|
-
}
|
|
751
|
-
try:
|
|
752
|
-
translation_estimate = vreg.align_slice_by_slice(
|
|
753
|
-
moving = array_moving,
|
|
754
|
-
moving_affine = affine_moving,
|
|
755
|
-
static = array_static,
|
|
756
|
-
static_affine = affine_static,
|
|
757
|
-
parameters = translation_estimate,
|
|
758
|
-
resolutions = [1],
|
|
759
|
-
transformation = vreg.translate_inslice,
|
|
760
|
-
metric = func[metric],
|
|
761
|
-
optimization = optimization,
|
|
762
|
-
slice_thickness = list(moving.values('SliceThickness', dims=('SliceLocation',))),
|
|
763
|
-
progress = lambda z, nz: moving.progress(z+1, nz, 'Coregistering slice-by-slice using translations'),
|
|
764
|
-
)
|
|
765
|
-
except:
|
|
766
|
-
print('Failed to align volumes..')
|
|
767
|
-
translation_estimate = np.zeros(2, dtype=np.float32)
|
|
768
|
-
|
|
769
|
-
return translation_estimate
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
def apply_sbs_inslice_translation(series_moving:Series, parameters:np.ndarray, target:Series=None)->Series:
|
|
773
|
-
"""Apply slice-by-slice inslice translation of an image volume.
|
|
774
|
-
|
|
775
|
-
Args:
|
|
776
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
777
|
-
parameters (np.ndarray): list of 3-element numpy arrays with values of the translation that maps the moving volume onto the static volume. The list has one entry per slice of the volume.
|
|
778
|
-
target (dbdicom.Series, optional): If provided, the result is mapped onto the geometry of this series. If none is provided, the result has the same geometry of the moving series. Defaults to None.
|
|
779
|
-
|
|
780
|
-
Raises:
|
|
781
|
-
ValueError: If the moving series contains multiple slice groups with different orientations.
|
|
782
|
-
ValueError: If the array to be moved is empty.
|
|
783
|
-
|
|
784
|
-
Returns:
|
|
785
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the translated volume.
|
|
786
|
-
"""
|
|
787
|
-
affine_moving = series_moving.unique_affines()
|
|
788
|
-
if affine_moving.shape[0] > 1:
|
|
789
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
790
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
791
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
792
|
-
msg += 'Please split the series first.'
|
|
793
|
-
raise ValueError(msg)
|
|
794
|
-
else:
|
|
795
|
-
affine_moving = affine_moving[0,:,:]
|
|
796
|
-
|
|
797
|
-
translation = [vreg.inslice_translation(affine_moving, par) for par in parameters]
|
|
798
|
-
return apply_sbs_translation(series_moving, translation, target=target)
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
def apply_sbs_passive_inslice_translation(series_moving:Series, parameters:np.ndarray)->Series:
|
|
802
|
-
"""Apply slice-by-slice passive translation of an image volume.
|
|
803
|
-
|
|
804
|
-
Passive in this context means that the coordinates are transformed rather than the image array itself.
|
|
805
|
-
|
|
806
|
-
Args:
|
|
807
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
808
|
-
parameters (np.ndarray): 6-element numpy array with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The list contains one entry per slice, ordered by slice location. The vectors are defined in an absolute reference frame in units of mm.
|
|
809
|
-
|
|
810
|
-
Raises:
|
|
811
|
-
ValueError: If the moving series contains multiple slice groups with different orientations.
|
|
812
|
-
ValueError: If the array to be moved is empty.
|
|
813
|
-
|
|
814
|
-
Returns:
|
|
815
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the translated volume.
|
|
816
|
-
"""
|
|
817
|
-
affine_moving = series_moving.unique_affines()
|
|
818
|
-
if affine_moving.shape[0] > 1:
|
|
819
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
820
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
821
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
822
|
-
msg += 'Please split the series first.'
|
|
823
|
-
raise ValueError(msg)
|
|
824
|
-
else:
|
|
825
|
-
affine_moving = affine_moving[0,:,:]
|
|
826
|
-
translation = [vreg.inslice_translation(affine_moving, par) for par in parameters]
|
|
827
|
-
return apply_sbs_passive_translation(series_moving, translation)
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
def find_sbs_translation(moving:Series, static:Series, tolerance=0.1, metric='mutual information', region:Series=None, margin:float=0, prereg=False)->np.ndarray:
|
|
831
|
-
"""Find the slice-by-slice translation that maps a moving volume onto a static volume.
|
|
832
|
-
|
|
833
|
-
Args:
|
|
834
|
-
moving (dbdicom.Series): Series with the moving volume.
|
|
835
|
-
static (dbdicom.Series): Series with the static volume
|
|
836
|
-
tolerance (float, optional): Positive tolerance parameter to decide convergence of the gradient descent. A smaller value means a more accurate solution but also a lomger computation time. Defaults to 0.1.
|
|
837
|
-
metric (str, option): Determines which metric to use in the optimization. Current options are 'mutual information' (default) or 'sum of squares'.
|
|
838
|
-
region (dbdicom.Series, optional): Series with region of interest to restrict the alignment. The translation will be chosen based on the goodness of the alignment in the bounding box of this region. If none is provided, the entire volume is used. Defaults to None.
|
|
839
|
-
margin (float, optional): in case a region is provided, this specifies a margin (in mm) to take around the region. Default is 0 (no margin).
|
|
840
|
-
|
|
841
|
-
Returns:
|
|
842
|
-
np.ndarray: list of 3-element numpy arrays with values of the translation that maps the moving volume onto the static volume. The list has one entry per slice of the volume.
|
|
843
|
-
"""
|
|
844
|
-
|
|
845
|
-
if prereg:
|
|
846
|
-
translation = find_translation(moving, static, tolerance=tolerance, metric=metric, region=region, margin=margin)
|
|
847
|
-
else:
|
|
848
|
-
translation = np.zeros(3, dtype=np.float32)
|
|
849
|
-
|
|
850
|
-
array_static, affine_static, array_moving, affine_moving = _get_input(moving, static, region=region, margin=margin)
|
|
851
|
-
|
|
852
|
-
func = {
|
|
853
|
-
'sum of squares': vreg.sum_of_squares,
|
|
854
|
-
'mutual information': vreg.mutual_information,
|
|
855
|
-
'interaction': vreg.interaction,
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
# # Find an initial value with a brute force
|
|
859
|
-
# optimization = {
|
|
860
|
-
# 'method': 'brute',
|
|
861
|
-
# 'options': {'grid':[[-10,10,10], [-10,10,10], [-10,10,10]]},
|
|
862
|
-
# }
|
|
863
|
-
# translation = vreg.align_slice_by_slice(
|
|
864
|
-
# moving = array_moving,
|
|
865
|
-
# moving_affine = affine_moving,
|
|
866
|
-
# static = array_static,
|
|
867
|
-
# static_affine = affine_static,
|
|
868
|
-
# transformation = vreg.translate,
|
|
869
|
-
# metric = func[metric],
|
|
870
|
-
# optimization = optimization,
|
|
871
|
-
# slice_thickness = list(moving.values('SliceThickness', dims=('SliceLocation',))),
|
|
872
|
-
# progress = lambda z, nz: moving.progress(z+1, nz, 'Performing brute force pre-search'),
|
|
873
|
-
# )
|
|
874
|
-
|
|
875
|
-
# Define initial values and optimization
|
|
876
|
-
_, _, static_pixel_spacing = vreg.affine_components(affine_static)
|
|
877
|
-
optimization = {
|
|
878
|
-
'method': 'GD',
|
|
879
|
-
'options': {'gradient step': 0.1*static_pixel_spacing, 'tolerance': tolerance},
|
|
880
|
-
# 'callback': lambda vk: moving.message('Current parameter: ' + str(vk)),
|
|
881
|
-
}
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
# Perform coregistration
|
|
885
|
-
try:
|
|
886
|
-
translation_estimate = vreg.align_slice_by_slice(
|
|
887
|
-
moving = array_moving,
|
|
888
|
-
moving_affine = affine_moving,
|
|
889
|
-
static = array_static,
|
|
890
|
-
static_affine = affine_static,
|
|
891
|
-
parameters = translation,
|
|
892
|
-
resolutions = [4,2,1],
|
|
893
|
-
transformation = vreg.translate,
|
|
894
|
-
metric = func[metric],
|
|
895
|
-
optimization = optimization,
|
|
896
|
-
slice_thickness = list(moving.values('SliceThickness', dims=('SliceLocation',))),
|
|
897
|
-
progress = lambda z, nz: moving.progress(z+1, nz, 'Coregistering slice-by-slice using translations'),
|
|
898
|
-
)
|
|
899
|
-
except:
|
|
900
|
-
print('Failed to align volumes..')
|
|
901
|
-
translation_estimate = None
|
|
902
|
-
|
|
903
|
-
return translation_estimate
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
def apply_sbs_translation(series_moving:Series, parameters:np.ndarray, target:Series=None)->Series:
|
|
907
|
-
"""Apply slice-by-slice translation of an image volume.
|
|
908
|
-
|
|
909
|
-
Args:
|
|
910
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
911
|
-
parameters (np.ndarray): list of 3-element numpy arrays with values of the translation that maps the moving volume onto the static volume. The list has one entry per slice of the volume.
|
|
912
|
-
target (dbdicom.Series, optional): If provided, the result is mapped onto the geometry of this series. If none is provided, the result has the same geometry of the moving series. Defaults to None.
|
|
913
|
-
|
|
914
|
-
Raises:
|
|
915
|
-
ValueError: If the moving series contains multiple slice groups with different orientations.
|
|
916
|
-
ValueError: If the array to be moved is empty.
|
|
917
|
-
|
|
918
|
-
Returns:
|
|
919
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the translated volume.
|
|
920
|
-
"""
|
|
921
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
922
|
-
affine_moving = series_moving.unique_affines()
|
|
923
|
-
if affine_moving.shape[0] > 1:
|
|
924
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
925
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
926
|
-
msg += 'Please split the series first.'
|
|
927
|
-
raise ValueError(msg)
|
|
928
|
-
else:
|
|
929
|
-
affine_moving = affine_moving[0,:,:]
|
|
930
|
-
|
|
931
|
-
array_moving = series_moving.pixel_values(dims=('SliceLocation',))
|
|
932
|
-
if array_moving.size == 0:
|
|
933
|
-
msg = desc_moving + ' is empty - cannot perform alignment.'
|
|
934
|
-
raise ValueError(msg)
|
|
935
|
-
slice_thickness = series_moving.values('SliceThickness', dims=('SliceLocation',))
|
|
936
|
-
|
|
937
|
-
if target is None:
|
|
938
|
-
shape_moved = array_moving.shape
|
|
939
|
-
affine_moved = affine_moving
|
|
940
|
-
else:
|
|
941
|
-
array_moved = target.pixel_values(dims=('SliceLocation',))
|
|
942
|
-
shape_moved = array_moved.shape
|
|
943
|
-
affine_moved = target.affine()
|
|
944
|
-
|
|
945
|
-
series_moving.message('Applying slice-by-slice translation..')
|
|
946
|
-
array_moved = vreg.transform_slice_by_slice(array_moving, affine_moving, shape_moved, affine_moved, parameters, vreg.translate, slice_thickness)
|
|
947
|
-
series_moved = series_moving.new_sibling(SeriesDescription = desc_moving + ' [sbs translation]')
|
|
948
|
-
series_moved.set_pixel_values(array_moved, slice={'SliceLocation': np.arange(array_moved.shape[-1])})
|
|
949
|
-
series_moved.set_affine(affine_moved)
|
|
950
|
-
return series_moved
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
def apply_sbs_passive_translation(series_moving:Series, parameters:np.ndarray)->Series:
|
|
954
|
-
"""Apply slice-by-slice passive translation of an image volume.
|
|
955
|
-
|
|
956
|
-
Passive in this context means that the coordinates are transformed rather than the image array itself.
|
|
957
|
-
|
|
958
|
-
Args:
|
|
959
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
960
|
-
parameters (np.ndarray): 6-element numpy array with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The list contains one entry per slice, ordered by slice location. The vectors are defined in an absolute reference frame in units of mm.
|
|
961
|
-
|
|
962
|
-
Raises:
|
|
963
|
-
ValueError: If the moving series contains multiple slice groups with different orientations.
|
|
964
|
-
ValueError: If the array to be moved is empty.
|
|
965
|
-
|
|
966
|
-
Returns:
|
|
967
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the translated volume.
|
|
968
|
-
"""
|
|
969
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
970
|
-
affine_moving = series_moving.unique_affines()
|
|
971
|
-
if affine_moving.shape[0] > 1:
|
|
972
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
973
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
974
|
-
msg += 'Please split the series first.'
|
|
975
|
-
raise ValueError(msg)
|
|
976
|
-
else:
|
|
977
|
-
affine_moving = affine_moving[0,:,:]
|
|
978
|
-
|
|
979
|
-
series_moving.message('Applying slice-by-slice passive translation..')
|
|
980
|
-
output_affine = vreg.passive_translation_slice_by_slice(affine_moving, parameters)
|
|
981
|
-
series_moved = series_moving.new_sibling(SeriesDescription = desc_moving + ' [sbs passive translation]')
|
|
982
|
-
frames = series_moving.frames(dims=('SliceLocation',))
|
|
983
|
-
for z in range(frames.size):
|
|
984
|
-
imz = frames[z].copy_to(series_moved)
|
|
985
|
-
#affine_z = output_affine[z]
|
|
986
|
-
#affine_z[:3,2] *= imz.SliceThickness/np.linalg.norm(affine_z[:3,2])
|
|
987
|
-
affine_z = vreg.multislice_to_singleslice_affine(output_affine[z], imz.SliceThickness)
|
|
988
|
-
imz.set_affine(affine_z)
|
|
989
|
-
return series_moved
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
def find_sbs_rigid_transformation_with_prealign(moving:Series, static:Series, tolerance=0.1, metric='mutual information', region:Series=None, margin:float=0, moving_mask:Series=None, static_mask:Series=None, resolutions=[4,2,1])->np.ndarray:
|
|
993
|
-
"""Find the rigid transform that maps a moving volume onto a static volume.
|
|
994
|
-
|
|
995
|
-
Args:
|
|
996
|
-
moving (dbdicom.Series): Series with the moving volume.
|
|
997
|
-
static (dbdicom.Series): Series with the static volume
|
|
998
|
-
tolerance (float, optional): Positive tolerance parameter to decide convergence of the gradient descent. A smaller value means a more accurate solution but also a lomger computation time. Defaults to 0.1.
|
|
999
|
-
metric (str, option): Determines which metric to use in the optimization. Current options are 'mutual information' (default) or 'sum of squares'.
|
|
1000
|
-
region (dbdicom.Series, optional): Series with region of interest to restrict the alignment. The rigid transform will be chosen based on the goodness of the alignment in the bounding box of this region. If none is provided, the entire volume is used. Defaults to None.
|
|
1001
|
-
margin (float, optional): in case a region is provided, this specifies a margin (in mm) to take around the region. Default is 0 (no margin).
|
|
1002
|
-
|
|
1003
|
-
Returns:
|
|
1004
|
-
np.ndarray: 6-element numpy array with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The vectors are defined in an absolute reference frame in units of mm.
|
|
1005
|
-
"""
|
|
1006
|
-
|
|
1007
|
-
array_static, affine_static, array_moving, affine_moving = _get_input(moving, static, region=region, margin=margin)
|
|
1008
|
-
array_moving_mask, affine_moving_mask = _get_input_volume(moving_mask)
|
|
1009
|
-
array_static_mask, affine_static_mask = _get_input_volume(static_mask)
|
|
1010
|
-
|
|
1011
|
-
# Define initial values and optimization
|
|
1012
|
-
_, _, static_pixel_spacing = vreg.affine_components(affine_static)
|
|
1013
|
-
rot_gradient_step, translation_gradient_step, _ = vreg.affine_resolution(array_static.shape, static_pixel_spacing)
|
|
1014
|
-
gradient_step = np.concatenate((1.0*rot_gradient_step, 0.5*translation_gradient_step))
|
|
1015
|
-
optimization = {
|
|
1016
|
-
'method': 'GD',
|
|
1017
|
-
'options': {'gradient step': gradient_step, 'tolerance': tolerance},
|
|
1018
|
-
'callback': lambda vk: moving.message('Current parameter: ' + str(vk)),
|
|
1019
|
-
}
|
|
1020
|
-
func = {
|
|
1021
|
-
'sum of squares': vreg.sum_of_squares,
|
|
1022
|
-
'mutual information': vreg.mutual_information,
|
|
1023
|
-
'interaction': vreg.interaction,
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
# Align volumes
|
|
1027
|
-
try:
|
|
1028
|
-
rigid_estimate = vreg.align(
|
|
1029
|
-
moving = array_moving,
|
|
1030
|
-
moving_affine = affine_moving,
|
|
1031
|
-
static = array_static,
|
|
1032
|
-
static_affine = affine_static,
|
|
1033
|
-
#parameters = np.array([0, 0, 0, 0, 0, 0], dtype=np.float32),
|
|
1034
|
-
parameters = np.zeros(6, dtype=np.float32),
|
|
1035
|
-
resolutions = [4,2,1],
|
|
1036
|
-
transformation = vreg.rigid,
|
|
1037
|
-
metric = func[metric],
|
|
1038
|
-
optimization = optimization,
|
|
1039
|
-
static_mask = array_static_mask,
|
|
1040
|
-
static_mask_affine = affine_static_mask,
|
|
1041
|
-
moving_mask = array_moving_mask,
|
|
1042
|
-
moving_mask_affine = affine_moving_mask,
|
|
1043
|
-
)
|
|
1044
|
-
except:
|
|
1045
|
-
print('Failed to align volumes..')
|
|
1046
|
-
#rigid_estimate = np.array([0, 0, 0, 0, 0, 0], dtype=np.float32)
|
|
1047
|
-
rigid_estimate = np.zeros(6, dtype=np.float32)
|
|
1048
|
-
|
|
1049
|
-
del optimization['callback']
|
|
1050
|
-
try:
|
|
1051
|
-
parameters = vreg.align_slice_by_slice(
|
|
1052
|
-
moving = array_moving,
|
|
1053
|
-
moving_affine = affine_moving,
|
|
1054
|
-
static = array_static,
|
|
1055
|
-
static_affine = affine_static,
|
|
1056
|
-
parameters = rigid_estimate,
|
|
1057
|
-
resolutions = resolutions,
|
|
1058
|
-
transformation = vreg.rigid,
|
|
1059
|
-
metric = func[metric],
|
|
1060
|
-
optimization = optimization,
|
|
1061
|
-
slice_thickness = list(moving.values('SliceThickness', dims=('SliceLocation',))),
|
|
1062
|
-
progress = lambda z, nz: moving.progress(z+1, nz, 'Coregistering slice-by-slice using rigid transformations'),
|
|
1063
|
-
static_mask = array_static_mask,
|
|
1064
|
-
static_mask_affine = affine_static_mask,
|
|
1065
|
-
moving_mask = array_moving_mask,
|
|
1066
|
-
moving_mask_affine = affine_moving_mask,
|
|
1067
|
-
)
|
|
1068
|
-
except:
|
|
1069
|
-
print('Failed to align slice-by-slice..')
|
|
1070
|
-
parameters = None
|
|
1071
|
-
|
|
1072
|
-
return parameters
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
def find_sbs_rigid_transformation(moving:Series, static:Series, tolerance=0.1, metric='mutual information', region:Series=None, margin:float=0, moving_mask:Series=None, static_mask:Series=None, resolutions=[4,2,1])->np.ndarray:
|
|
1076
|
-
"""Find the slice-by-slice rigid transformation that maps a moving volume onto a static volume.
|
|
1077
|
-
|
|
1078
|
-
Args:
|
|
1079
|
-
moving (dbdicom.Series): Series with the moving volume.
|
|
1080
|
-
static (dbdicom.Series): Series with the static volume
|
|
1081
|
-
tolerance (float, optional): Positive tolerance parameter to decide convergence of the gradient descent. A smaller value means a more accurate solution but also a lomger computation time. Defaults to 0.1.
|
|
1082
|
-
metric (str, option): Determines which metric to use in the optimization. Current options are 'mutual information' (default) or 'sum of squares'.
|
|
1083
|
-
region (dbdicom.Series, optional): Series with region of interest to restrict the alignment. The translation will be chosen based on the goodness of the alignment in the bounding box of this region. If none is provided, the entire volume is used. Defaults to None.
|
|
1084
|
-
margin (float, optional): in case a region is provided, this specifies a margin (in mm) to take around the region. Default is 0 (no margin).
|
|
1085
|
-
moving_mask (dbdicom.Series): Series for masking the moving volume.
|
|
1086
|
-
static_mask (dbdicom.Series): Series for masking the static volume.
|
|
1087
|
-
|
|
1088
|
-
Returns:
|
|
1089
|
-
np.ndarray: list of 6-element numpy arrays with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The list contains one entry per slice, ordered by slice location. The vectors are defined in an absolute reference frame in units of mm.
|
|
1090
|
-
"""
|
|
1091
|
-
|
|
1092
|
-
array_static, affine_static, array_moving, affine_moving = _get_input(moving, static, region=region, margin=margin)
|
|
1093
|
-
array_moving_mask, affine_moving_mask = _get_input_volume(moving_mask)
|
|
1094
|
-
array_static_mask, affine_static_mask = _get_input_volume(static_mask)
|
|
1095
|
-
|
|
1096
|
-
# Define initial values and optimization
|
|
1097
|
-
_, _, static_pixel_spacing = vreg.affine_components(affine_static)
|
|
1098
|
-
rot_gradient_step, translation_gradient_step, _ = vreg.affine_resolution(array_static.shape, static_pixel_spacing)
|
|
1099
|
-
gradient_step = np.concatenate((1.0*rot_gradient_step, 0.5*translation_gradient_step))
|
|
1100
|
-
optimization = {
|
|
1101
|
-
'method': 'GD',
|
|
1102
|
-
'options': {'gradient step': gradient_step, 'tolerance': tolerance},
|
|
1103
|
-
}
|
|
1104
|
-
func = {
|
|
1105
|
-
'sum of squares': vreg.sum_of_squares,
|
|
1106
|
-
'mutual information': vreg.mutual_information,
|
|
1107
|
-
'interaction': vreg.interaction,
|
|
1108
|
-
}
|
|
1109
|
-
|
|
1110
|
-
# Perform coregistration
|
|
1111
|
-
try:
|
|
1112
|
-
parameters = vreg.align_slice_by_slice(
|
|
1113
|
-
moving = array_moving,
|
|
1114
|
-
moving_affine = affine_moving,
|
|
1115
|
-
static = array_static,
|
|
1116
|
-
static_affine = affine_static,
|
|
1117
|
-
parameters = np.zeros(6, dtype=np.float32),
|
|
1118
|
-
resolutions = resolutions,
|
|
1119
|
-
transformation = vreg.rigid,
|
|
1120
|
-
metric = func[metric],
|
|
1121
|
-
optimization = optimization,
|
|
1122
|
-
slice_thickness = list(moving.values('SliceThickness', dims=('SliceLocation',))),
|
|
1123
|
-
progress = lambda z, nz: moving.progress(z+1, nz, 'Coregistering slice-by-slice using rigid transformations'),
|
|
1124
|
-
static_mask = array_static_mask,
|
|
1125
|
-
static_mask_affine = affine_static_mask,
|
|
1126
|
-
moving_mask = array_moving_mask,
|
|
1127
|
-
moving_mask_affine = affine_moving_mask,
|
|
1128
|
-
)
|
|
1129
|
-
except:
|
|
1130
|
-
print('Failed to align volumes..')
|
|
1131
|
-
parameters = None
|
|
1132
|
-
|
|
1133
|
-
return parameters
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
def apply_sbs_passive_rigid_transformation(series_moving:Series, parameters:np.ndarray, description=None)->Series:
|
|
1137
|
-
"""Apply slice-by-slice passive rigid transformation of an image volume.
|
|
1138
|
-
|
|
1139
|
-
Passive in this context means that the coordinates are transformed rather than the image array itself.
|
|
1140
|
-
|
|
1141
|
-
Args:
|
|
1142
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
1143
|
-
parameters (np.ndarray): 6-element numpy array with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The list contains one entry per slice, ordered by slice location. The vectors are defined in an absolute reference frame in units of mm.
|
|
1144
|
-
|
|
1145
|
-
Raises:
|
|
1146
|
-
ValueError: If the moving series contains multiple slice groups with different orientations.
|
|
1147
|
-
ValueError: If the array to be moved is empty.
|
|
1148
|
-
|
|
1149
|
-
Returns:
|
|
1150
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the translated volume.
|
|
1151
|
-
"""
|
|
1152
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
1153
|
-
affine_moving = series_moving.unique_affines()
|
|
1154
|
-
if affine_moving.shape[0] > 1:
|
|
1155
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
1156
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
1157
|
-
msg += 'Please split the series first.'
|
|
1158
|
-
raise ValueError(msg)
|
|
1159
|
-
else:
|
|
1160
|
-
affine_moving = affine_moving[0,:,:]
|
|
1161
|
-
|
|
1162
|
-
if description is None:
|
|
1163
|
-
description = desc_moving + ' [sbs passive rigid]'
|
|
1164
|
-
output_affine = vreg.passive_rigid_transform_slice_by_slice(affine_moving, parameters)
|
|
1165
|
-
|
|
1166
|
-
series_moved = series_moving.new_sibling(SeriesDescription = description)
|
|
1167
|
-
frames = series_moving.frames(('SliceLocation','InstanceNumber'))
|
|
1168
|
-
cnt=0
|
|
1169
|
-
for z in range(frames.shape[0]):
|
|
1170
|
-
for t in range(frames.shape[1]):
|
|
1171
|
-
cnt+=1
|
|
1172
|
-
series_moving.progress(cnt, frames.size, 'Applying transformation to ' + desc_moving)
|
|
1173
|
-
affine_zt = vreg.multislice_to_singleslice_affine(output_affine[z], frames[z,t].SliceThickness)
|
|
1174
|
-
frames[z,t].copy_to(series_moved).set_affine(affine_zt)
|
|
1175
|
-
|
|
1176
|
-
return series_moved
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
def apply_sbs_rigid_transformation(series_moving:Series, parameters:np.ndarray, target:Series=None)->Series:
|
|
1181
|
-
"""Apply slice-by-slice rigid transformation of an image volume.
|
|
1182
|
-
|
|
1183
|
-
Args:
|
|
1184
|
-
series_moving (dbdicom.Series): Series containing the volune to be moved.
|
|
1185
|
-
parameters (np.ndarray): 6-element numpy array with values of the translation (first 3 elements) and rotation vector (last 3 elements) that map the moving volume on to the static volume. The list contains one entry per slice, ordered by slice location. The vectors are defined in an absolute reference frame in units of mm.
|
|
1186
|
-
target (dbdicom.Series, optional): If provided, the result is mapped onto the geometry of this series. If none is provided, the result has the same geometry as the moving series. Defaults to None.
|
|
1187
|
-
|
|
1188
|
-
Raises:
|
|
1189
|
-
ValueError: If the moving series contains multiple slice groups with different orientations.
|
|
1190
|
-
ValueError: If the array to be moved is empty.
|
|
1191
|
-
|
|
1192
|
-
Returns:
|
|
1193
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the translated volume.
|
|
1194
|
-
"""
|
|
1195
|
-
desc_moving = series_moving.instance().SeriesDescription
|
|
1196
|
-
affine_moving = series_moving.affine()
|
|
1197
|
-
if len(affine_moving) > 1:
|
|
1198
|
-
msg = 'Multiple slice groups detected in ' + desc_moving + '\n'
|
|
1199
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
1200
|
-
msg += 'Please split the series first.'
|
|
1201
|
-
raise ValueError(msg)
|
|
1202
|
-
else:
|
|
1203
|
-
affine_moving = affine_moving[0]
|
|
1204
|
-
|
|
1205
|
-
array_moving = series_moving.pixel_values(dims=('SliceLocation',))
|
|
1206
|
-
if array_moving.size == 0:
|
|
1207
|
-
msg = desc_moving + ' is empty - cannot perform alignment.'
|
|
1208
|
-
raise ValueError(msg)
|
|
1209
|
-
slice_thickness = list(series_moving.values('SliceThickness', dims=('SliceLocation',)))
|
|
1210
|
-
|
|
1211
|
-
if target is None:
|
|
1212
|
-
shape_moved = array_moving.shape
|
|
1213
|
-
affine_moved = affine_moving
|
|
1214
|
-
else:
|
|
1215
|
-
array_moved = target.pixel_values(dims=('SliceLocation',))
|
|
1216
|
-
shape_moved = array_moved.shape
|
|
1217
|
-
affine_moved = target.affine()
|
|
1218
|
-
|
|
1219
|
-
series_moving.message('Applying slice-by-slice rigid transformation..')
|
|
1220
|
-
array_moved = vreg.transform_slice_by_slice(array_moving, affine_moving, shape_moved, affine_moved, parameters, vreg.rigid, slice_thickness)
|
|
1221
|
-
series_moved = series_moving.new_sibling(SeriesDescription = desc_moving + ' [sbs rigid]')
|
|
1222
|
-
series_moved.set_pixel_values(array_moved, slice={'SliceLocation': np.arange(array_moved.shape[-1])})
|
|
1223
|
-
series_moved.set_affine(affine_moved)
|
|
1224
|
-
return series_moved
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
def rigid_around_com_sos(moving, static, tolerance=0.1):
|
|
1228
|
-
|
|
1229
|
-
array_static, affine_static, array_moving, affine_moving = _get_input(moving, static)
|
|
1230
|
-
|
|
1231
|
-
# Define initial values and optimization
|
|
1232
|
-
_, _, static_pixel_spacing = vreg.affine_components(affine_static)
|
|
1233
|
-
rot_gradient_step, translation_gradient_step, _ = vreg.affine_resolution(array_static.shape, static_pixel_spacing)
|
|
1234
|
-
gradient_step = np.concatenate((1.0*rot_gradient_step, 0.5*translation_gradient_step))
|
|
1235
|
-
optimization = {
|
|
1236
|
-
'method': 'GD',
|
|
1237
|
-
'options': {'gradient step': gradient_step, 'tolerance': tolerance},
|
|
1238
|
-
'callback': lambda vk: moving.message('Current parameter: ' + str(vk)),
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
# Align volumes
|
|
1242
|
-
try:
|
|
1243
|
-
rigid_estimate = vreg.align(
|
|
1244
|
-
moving = array_moving,
|
|
1245
|
-
moving_affine = affine_moving,
|
|
1246
|
-
static = array_static,
|
|
1247
|
-
static_affine = affine_static,
|
|
1248
|
-
parameters = np.array([0, 0, 0, 0, 0, 0], dtype=np.float32),
|
|
1249
|
-
resolutions = [4,2,1],
|
|
1250
|
-
transformation = vreg.rigid_around_com,
|
|
1251
|
-
metric = vreg.sum_of_squares,
|
|
1252
|
-
optimization = optimization,
|
|
1253
|
-
)
|
|
1254
|
-
except:
|
|
1255
|
-
print('Failed to align volumes..')
|
|
1256
|
-
return None
|
|
1257
|
-
|
|
1258
|
-
coregistered = vreg.rigid_around_com(array_moving, affine_moving, array_static.shape, affine_static, rigid_estimate)
|
|
1259
|
-
|
|
1260
|
-
moving.message('Writing coregistered series to database..')
|
|
1261
|
-
|
|
1262
|
-
# Save results as new dicom series
|
|
1263
|
-
desc = moving.instance().SeriesDescription
|
|
1264
|
-
coreg = moving.new_sibling(SeriesDescription = desc + ' [rigid com]')
|
|
1265
|
-
coreg.set_pixel_values(coregistered, slice={'SliceLocation': np.arange(coregistered.shape[-1])})
|
|
1266
|
-
return coreg
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
def sbs_rigid_around_com_sos(moving, static, tolerance=0.1):
|
|
1270
|
-
|
|
1271
|
-
array_static, affine_static, array_moving, affine_moving = _get_input(moving, static)
|
|
1272
|
-
slice_thickness = list(moving.values('SliceThickness', dims=('SliceLocation',)))
|
|
1273
|
-
|
|
1274
|
-
# Define initial values and optimization
|
|
1275
|
-
_, _, static_pixel_spacing = vreg.affine_components(affine_static)
|
|
1276
|
-
rot_gradient_step, translation_gradient_step, _ = vreg.affine_resolution(array_static.shape, static_pixel_spacing)
|
|
1277
|
-
gradient_step = np.concatenate((1.0*rot_gradient_step, 0.5*translation_gradient_step))
|
|
1278
|
-
optimization = {
|
|
1279
|
-
'method': 'GD',
|
|
1280
|
-
'options': {'gradient step': gradient_step, 'tolerance': tolerance},
|
|
1281
|
-
#'callback': lambda vk: moving.message('Current parameter: ' + str(vk)),
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
# Perform coregistration
|
|
1285
|
-
estimate = vreg.align_slice_by_slice(
|
|
1286
|
-
moving = array_moving,
|
|
1287
|
-
static = array_static,
|
|
1288
|
-
parameters = np.array([0, 0, 0, 0, 0, 0], dtype=np.float32),
|
|
1289
|
-
moving_affine = affine_moving,
|
|
1290
|
-
static_affine = affine_static,
|
|
1291
|
-
transformation = vreg.rigid_around_com,
|
|
1292
|
-
metric = vreg.sum_of_squares,
|
|
1293
|
-
optimization = optimization,
|
|
1294
|
-
resolutions = [4,2,1],
|
|
1295
|
-
slice_thickness = slice_thickness,
|
|
1296
|
-
progress = lambda z, nz: moving.progress(z, nz, 'Coregistering slice-by-slice using rigid transformations'),
|
|
1297
|
-
)
|
|
1298
|
-
|
|
1299
|
-
# The generic slice-by-slice transform does not work for center of mass rotations.
|
|
1300
|
-
# Calculate rotation center and use rigid rotation around given center instead.
|
|
1301
|
-
estimate_center = []
|
|
1302
|
-
for z in range(len(estimate)):
|
|
1303
|
-
array_moving_z, affine_moving_z = vreg.extract_slice(array_moving, affine_moving, z, slice_thickness)
|
|
1304
|
-
center = estimate[z][3:] + vreg.center_of_mass(vreg.to_3d(array_moving_z), affine_moving_z)
|
|
1305
|
-
pars = np.concatenate((estimate[z][:3], center, estimate[z][3:]))
|
|
1306
|
-
estimate_center.append(pars)
|
|
1307
|
-
|
|
1308
|
-
# Calculate coregistered (using rigid around known center)
|
|
1309
|
-
coregistered = vreg.transform_slice_by_slice(array_moving, affine_moving, array_static.shape, affine_static, estimate_center, vreg.rigid_around, slice_thickness)
|
|
1310
|
-
|
|
1311
|
-
# Save results as new dicom series
|
|
1312
|
-
moving.message('Writing coregistered series to database..')
|
|
1313
|
-
desc = moving.instance().SeriesDescription
|
|
1314
|
-
coreg = moving.new_sibling(SeriesDescription = desc + ' [sbs rigid com]')
|
|
1315
|
-
coreg.set_pixel_values(coregistered, slice={'SliceLocation': np.arange(coregistered.shape[-1])})
|
|
1316
|
-
|
|
1317
|
-
return coreg
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
def rotate(series:Series, parameters:np.ndarray, reshape=False, output_shape:tuple=None, output_affine:np.ndarray=None, mode='constant', **kwargs)->Series:
|
|
1321
|
-
"""Rotate a series in 3D
|
|
1322
|
-
|
|
1323
|
-
Args:
|
|
1324
|
-
series (dbdicom.Series): Series containing the volume to be rotated.
|
|
1325
|
-
parameters (np.ndarray): 3-element numpy array with values of the rotation vector. The vectors are defined in the absolute (scanner) reference frame in units of mm.
|
|
1326
|
-
reshape (bool, optional): if True, the array size and affine will be adjusted to contain the complete rotate data. If False, the original array size and affine is retained. Defaults to True.
|
|
1327
|
-
output_shape (tuple, optional): determines the shape of the result. If not provided, the shape of the original (reshape=False) or reshaped array (reshape=True) is used. Defaults to False.
|
|
1328
|
-
output_affine (ndarray, optional): determines the affine of the result. If not provided, the affine of the original (reshape=False) or reshaped array (reshape=True) is used. Defaults to None.
|
|
1329
|
-
mode (str, optional): Determines how the input array is extended beyond its boundaries. See `scipy.ndimage.map_coordinates <https://docs.scipy.org/doc/scipy/reference/generated/scipy.ndimage.map_coordinates.html>`_ for more detail. Defaults to constant = 0.
|
|
1330
|
-
kwargs: List of optionional arguments specifying valid DICOM (keyword = value) pairs.
|
|
1331
|
-
|
|
1332
|
-
Raises:
|
|
1333
|
-
ValueError: If the moving series contains multiple slice groups with different orentations.
|
|
1334
|
-
ValueError: If the array to be moved is empty.
|
|
1335
|
-
|
|
1336
|
-
Returns:
|
|
1337
|
-
dbdicom.Series: Sibling dbdicom series in the same study, containing the rotated volume.
|
|
1338
|
-
"""
|
|
1339
|
-
|
|
1340
|
-
# Check that the series has a single slice group
|
|
1341
|
-
affine = series.unique_affines()
|
|
1342
|
-
if affine.shape[0] > 1:
|
|
1343
|
-
desc = series.instance().SeriesDescription
|
|
1344
|
-
msg = 'Multiple slice groups detected in ' + desc + '\n'
|
|
1345
|
-
msg += 'This function only works for series with a single slice group. \n'
|
|
1346
|
-
msg += 'Please split the series first.'
|
|
1347
|
-
raise ValueError(msg)
|
|
1348
|
-
else:
|
|
1349
|
-
affine = affine[0,:,:]
|
|
1350
|
-
|
|
1351
|
-
# Check that the array is not empty
|
|
1352
|
-
array = series.pixel_values(dims=('SliceLocation',))
|
|
1353
|
-
if array.size == 0:
|
|
1354
|
-
desc = series.instance().SeriesDescription
|
|
1355
|
-
msg = desc + ' is empty - cannot perform alignment.'
|
|
1356
|
-
raise ValueError(msg)
|
|
1357
|
-
|
|
1358
|
-
# Perform rotation
|
|
1359
|
-
series.message('Applying rotation..')
|
|
1360
|
-
if reshape:
|
|
1361
|
-
|
|
1362
|
-
# Perform rotation and reshape
|
|
1363
|
-
output_arr, output_aff = vreg.rotate_reshape(array, affine, parameters, mode=mode)
|
|
1364
|
-
|
|
1365
|
-
# If no output geometry is specified, return the results as they are.
|
|
1366
|
-
if output_shape is None and output_affine is None:
|
|
1367
|
-
output_array, output_affine = output_arr, output_aff
|
|
1368
|
-
|
|
1369
|
-
# If an output geometry is specified, reslice the result to this geometry.
|
|
1370
|
-
else:
|
|
1371
|
-
if output_shape is None:
|
|
1372
|
-
output_shape = output_arr.shape
|
|
1373
|
-
if output_affine is None:
|
|
1374
|
-
output_affine = output_aff
|
|
1375
|
-
output_array, output_affine = vreg.affine_reslice(output_arr, output_aff, output_affine, output_shape)
|
|
1376
|
-
|
|
1377
|
-
else:
|
|
1378
|
-
# If not provided, use default values for array shape and affine
|
|
1379
|
-
if output_shape is None:
|
|
1380
|
-
output_shape = array.shape
|
|
1381
|
-
if output_affine is None:
|
|
1382
|
-
output_affine = affine
|
|
1383
|
-
output_array = vreg.rotate(array, affine, output_shape, output_affine, parameters, mode=mode)
|
|
1384
|
-
|
|
1385
|
-
# Save results in a new series
|
|
1386
|
-
output_series = series.new_sibling(WindowCenter=series.WindowCenter, WindowWidth=series.WindowWidth, **kwargs)
|
|
1387
|
-
output_series.set_pixel_values(output_array, slice={'SliceLocation': np.arange(output_array.shape[-1])})
|
|
1388
|
-
output_series.set_affine(output_affine)
|
|
1389
|
-
|
|
1390
|
-
return output_series
|