dbdicom 0.2.6__py3-none-any.whl → 0.3.1__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.

Files changed (52) hide show
  1. dbdicom/__init__.py +1 -28
  2. dbdicom/api.py +287 -0
  3. dbdicom/const.py +144 -0
  4. dbdicom/dataset.py +721 -0
  5. dbdicom/dbd.py +736 -0
  6. dbdicom/external/__pycache__/__init__.cpython-311.pyc +0 -0
  7. dbdicom/external/dcm4che/__pycache__/__init__.cpython-311.pyc +0 -0
  8. dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-311.pyc +0 -0
  9. dbdicom/register.py +527 -0
  10. dbdicom/{ds/types → sop_classes}/ct_image.py +2 -16
  11. dbdicom/{ds/types → sop_classes}/enhanced_mr_image.py +153 -26
  12. dbdicom/{ds/types → sop_classes}/mr_image.py +185 -140
  13. dbdicom/sop_classes/parametric_map.py +310 -0
  14. dbdicom/sop_classes/secondary_capture.py +140 -0
  15. dbdicom/sop_classes/segmentation.py +311 -0
  16. dbdicom/{ds/types → sop_classes}/ultrasound_multiframe_image.py +1 -15
  17. dbdicom/{ds/types → sop_classes}/xray_angiographic_image.py +2 -17
  18. dbdicom/utils/arrays.py +36 -0
  19. dbdicom/utils/files.py +0 -20
  20. dbdicom/utils/image.py +10 -629
  21. dbdicom-0.3.1.dist-info/METADATA +28 -0
  22. dbdicom-0.3.1.dist-info/RECORD +53 -0
  23. dbdicom/create.py +0 -457
  24. dbdicom/dro.py +0 -174
  25. dbdicom/ds/__init__.py +0 -10
  26. dbdicom/ds/create.py +0 -63
  27. dbdicom/ds/dataset.py +0 -869
  28. dbdicom/ds/dictionaries.py +0 -620
  29. dbdicom/ds/types/parametric_map.py +0 -226
  30. dbdicom/extensions/__init__.py +0 -9
  31. dbdicom/extensions/dipy.py +0 -448
  32. dbdicom/extensions/elastix.py +0 -503
  33. dbdicom/extensions/matplotlib.py +0 -107
  34. dbdicom/extensions/numpy.py +0 -271
  35. dbdicom/extensions/scipy.py +0 -1512
  36. dbdicom/extensions/skimage.py +0 -1030
  37. dbdicom/extensions/sklearn.py +0 -243
  38. dbdicom/extensions/vreg.py +0 -1390
  39. dbdicom/manager.py +0 -2132
  40. dbdicom/message.py +0 -119
  41. dbdicom/pipelines.py +0 -66
  42. dbdicom/record.py +0 -1893
  43. dbdicom/types/database.py +0 -107
  44. dbdicom/types/instance.py +0 -231
  45. dbdicom/types/patient.py +0 -40
  46. dbdicom/types/series.py +0 -2874
  47. dbdicom/types/study.py +0 -58
  48. dbdicom-0.2.6.dist-info/METADATA +0 -72
  49. dbdicom-0.2.6.dist-info/RECORD +0 -66
  50. {dbdicom-0.2.6.dist-info → dbdicom-0.3.1.dist-info}/WHEEL +0 -0
  51. {dbdicom-0.2.6.dist-info → dbdicom-0.3.1.dist-info}/licenses/LICENSE +0 -0
  52. {dbdicom-0.2.6.dist-info → dbdicom-0.3.1.dist-info}/top_level.txt +0 -0
@@ -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