dbdicom 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of dbdicom might be problematic. Click here for more details.
- dbdicom/__init__.py +4 -3
- dbdicom/create.py +34 -97
- dbdicom/dro.py +174 -0
- dbdicom/ds/dataset.py +29 -3
- dbdicom/ds/types/mr_image.py +18 -7
- dbdicom/extensions/__init__.py +10 -0
- dbdicom/{wrappers → extensions}/dipy.py +191 -205
- dbdicom/extensions/elastix.py +503 -0
- dbdicom/extensions/matplotlib.py +107 -0
- dbdicom/extensions/numpy.py +271 -0
- dbdicom/{wrappers → extensions}/scipy.py +130 -31
- dbdicom/{wrappers → extensions}/skimage.py +1 -1
- dbdicom/extensions/sklearn.py +243 -0
- dbdicom/extensions/vreg.py +1390 -0
- dbdicom/external/dcm4che/bin/emf2sf +57 -57
- dbdicom/manager.py +70 -36
- dbdicom/pipelines.py +66 -0
- dbdicom/record.py +266 -43
- dbdicom/types/instance.py +17 -3
- dbdicom/types/series.py +1900 -404
- dbdicom/utils/image.py +152 -21
- dbdicom/utils/vreg.py +327 -135
- dbdicom-0.2.3.dist-info/METADATA +88 -0
- {dbdicom-0.2.1.dist-info → dbdicom-0.2.3.dist-info}/RECORD +27 -41
- {dbdicom-0.2.1.dist-info → dbdicom-0.2.3.dist-info}/WHEEL +1 -1
- dbdicom/external/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/dcm4che/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-310.pyc +0 -0
- dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-37.pyc +0 -0
- dbdicom/external/dcm4che/lib/linux-x86/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/linux-x86-64/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/linux-x86-64/libopencv_java.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis2.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis2.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-x86/libclib_jiio.so +0 -0
- dbdicom/external/dcm4che/lib/solaris-x86-64/libclib_jiio.so +0 -0
- dbdicom/wrappers/__init__.py +0 -7
- dbdicom/wrappers/elastix.py +0 -855
- dbdicom/wrappers/numpy.py +0 -119
- dbdicom/wrappers/sklearn.py +0 -151
- dbdicom/wrappers/vreg.py +0 -273
- dbdicom-0.2.1.dist-info/METADATA +0 -276
- {dbdicom-0.2.1.dist-info → dbdicom-0.2.3.dist-info}/LICENSE +0 -0
- {dbdicom-0.2.1.dist-info → dbdicom-0.2.3.dist-info}/top_level.txt +0 -0
dbdicom/utils/vreg.py
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import time
|
|
2
2
|
import numpy as np
|
|
3
|
-
import scipy
|
|
3
|
+
import scipy
|
|
4
4
|
import scipy.optimize as opt
|
|
5
5
|
import scipy.ndimage as ndi
|
|
6
|
-
from scipy.interpolate import interpn
|
|
6
|
+
from scipy.interpolate import interpn, griddata
|
|
7
7
|
from scipy.spatial.transform import Rotation
|
|
8
|
-
import sklearn
|
|
9
8
|
|
|
10
9
|
import pyvista as pv
|
|
11
10
|
|
|
@@ -17,6 +16,34 @@ from skimage.draw import ellipsoid
|
|
|
17
16
|
##########################
|
|
18
17
|
|
|
19
18
|
|
|
19
|
+
def fill_gaps(data, loc, mask=None):
|
|
20
|
+
# Fill gaps in data by interpolation
|
|
21
|
+
# data is an array with values
|
|
22
|
+
# loc is a mask array defining where to interpolate (0=interpolate, 1=value)
|
|
23
|
+
# mask is a mask array to restrict the interpolation to certain regions.
|
|
24
|
+
|
|
25
|
+
x, y, z = np.indices(data.shape)
|
|
26
|
+
|
|
27
|
+
# Get locations and values of non-zero pixels
|
|
28
|
+
i = loc>0.5
|
|
29
|
+
if mask is not None:
|
|
30
|
+
i = i & (mask==1)
|
|
31
|
+
points = np.column_stack([x[i], y[i], z[i]])
|
|
32
|
+
values = data[i]
|
|
33
|
+
|
|
34
|
+
# Interpolate using griddata
|
|
35
|
+
k = loc<0.5
|
|
36
|
+
if mask is not None:
|
|
37
|
+
k = k & (mask==1)
|
|
38
|
+
filled = data.copy()
|
|
39
|
+
filled[k] = griddata(points, values, (x[k], y[k], z[k]), method='linear', fill_value=0)
|
|
40
|
+
|
|
41
|
+
if mask is not None:
|
|
42
|
+
filled *= mask
|
|
43
|
+
|
|
44
|
+
return filled
|
|
45
|
+
|
|
46
|
+
|
|
20
47
|
def volume_coordinates(shape, position=[0,0,0]):
|
|
21
48
|
|
|
22
49
|
# data are defined at the middle of the voxels - use p+0.5 offset.
|
|
@@ -116,7 +143,6 @@ def interpolator_displacement(displacement, shape, interpolator):
|
|
|
116
143
|
return displacement_interp
|
|
117
144
|
|
|
118
145
|
|
|
119
|
-
|
|
120
146
|
def surface_coordinates(shape):
|
|
121
147
|
|
|
122
148
|
# data are defined at the edge of volume - extend shape with 1.
|
|
@@ -158,7 +184,7 @@ def pv_contour(values, data, affine, surface=False):
|
|
|
158
184
|
surf_data = np.reshape(surf_data, surf_shape)
|
|
159
185
|
|
|
160
186
|
rotation, translation, pixel_spacing = affine_components(affine)
|
|
161
|
-
grid = pv.
|
|
187
|
+
grid = pv.ImageData(dimensions=surf_shape, spacing=pixel_spacing)
|
|
162
188
|
surf = grid.contour(values, surf_data.flatten(order="F"), method='marching_cubes')
|
|
163
189
|
surf = surf.rotate_vector(rotation, np.linalg.norm(rotation)*180/np.pi, inplace=False)
|
|
164
190
|
surf = surf.translate(translation, inplace=False)
|
|
@@ -299,6 +325,63 @@ def envelope(d, affine):
|
|
|
299
325
|
return output_shape, output_pos
|
|
300
326
|
|
|
301
327
|
|
|
328
|
+
def bounding_box(array:np.ndarray, affine:np.ndarray, margin:float=0)->tuple:
|
|
329
|
+
"""Return a bounding box around a region of interest.
|
|
330
|
+
|
|
331
|
+
For a given mask array with know affine this finds the bounding box around the mask, and returns its shape and affine.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
array (np.ndarray): 3d binary array with mask data (1=inside, 0=outside).
|
|
335
|
+
affine (np.ndarray): affine of the 3d volume as a 4x4 numpy array.
|
|
336
|
+
margin (float): margin (in mm) to include around the bounding box. (default=0)
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
tuple: shape and affine of the bounding box, where shape is a tuple of 3 values and affine is a 4x4 numpy array.
|
|
340
|
+
"""
|
|
341
|
+
|
|
342
|
+
_, _, pixel_spacing = affine_components(affine)
|
|
343
|
+
xmargin = np.around(margin/pixel_spacing[0]).astype(np.int16)
|
|
344
|
+
ymargin = np.around(margin/pixel_spacing[1]).astype(np.int16)
|
|
345
|
+
zmargin = np.around(margin/pixel_spacing[2]).astype(np.int16)
|
|
346
|
+
|
|
347
|
+
# Find shape and location of the box in array coordinates and get array.
|
|
348
|
+
x, y, z = np.where(array != 0)
|
|
349
|
+
x0, x1 = np.amin(x)-xmargin, np.amax(x)+xmargin
|
|
350
|
+
y0, y1 = np.amin(y)-ymargin, np.amax(y)+ymargin
|
|
351
|
+
z0, z1 = np.amin(z)-zmargin, np.amax(z)+zmargin
|
|
352
|
+
x0, x1 = np.amax([0, x0]), np.amin([x1, array.shape[0]-1])
|
|
353
|
+
y0, y1 = np.amax([0, y0]), np.amin([y1, array.shape[1]-1])
|
|
354
|
+
z0, z1 = np.amax([0, z0]), np.amin([z1, array.shape[2]-1])
|
|
355
|
+
nx = 1 + np.ceil(x1-x0).astype(np.int16)
|
|
356
|
+
ny = 1 + np.ceil(y1-y0).astype(np.int16)
|
|
357
|
+
nz = 1 + np.ceil(z1-z0).astype(np.int16)
|
|
358
|
+
box_array = array[x0:x0+nx, y0:y0+ny, z0:z0+nz]
|
|
359
|
+
|
|
360
|
+
# Get the corner in absolute coordinates and offset the affine.
|
|
361
|
+
nd = 3
|
|
362
|
+
matrix = affine[:nd,:nd]
|
|
363
|
+
offset = affine[:nd, nd]
|
|
364
|
+
r0 = np.array([x0,y0,z0])
|
|
365
|
+
r0 = np.dot(r0, matrix.T) + offset
|
|
366
|
+
box_affine = affine.copy()
|
|
367
|
+
box_affine[:nd, nd] = r0
|
|
368
|
+
|
|
369
|
+
return box_array, box_affine
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def mask_volume(array, affine, array_mask, affine_mask, margin:float=0):
|
|
373
|
+
|
|
374
|
+
# Overlay the mask on the array.
|
|
375
|
+
array_mask, _ = affine_reslice(array_mask, affine_mask, affine, array.shape)
|
|
376
|
+
|
|
377
|
+
# Mask out array pixels outside of region.
|
|
378
|
+
array *= array_mask
|
|
379
|
+
|
|
380
|
+
# Extract bounding box around non-zero pixels in masked array.
|
|
381
|
+
array, affine = bounding_box(array, affine, margin)
|
|
382
|
+
|
|
383
|
+
return array, affine
|
|
384
|
+
|
|
302
385
|
|
|
303
386
|
def apply_affine(affine, coord):
|
|
304
387
|
"""Apply affine transformation to an array of coordinates"""
|
|
@@ -446,6 +529,7 @@ def affine_output_geometry(input_shape, input_affine, transformation):
|
|
|
446
529
|
return output_shape, output_affine
|
|
447
530
|
|
|
448
531
|
|
|
532
|
+
|
|
449
533
|
####################################
|
|
450
534
|
## Affine transformation and reslice
|
|
451
535
|
####################################
|
|
@@ -471,7 +555,38 @@ def affine_transform(input_data, input_affine, transformation, reshape=False, **
|
|
|
471
555
|
return output_data, output_affine
|
|
472
556
|
|
|
473
557
|
|
|
474
|
-
|
|
558
|
+
def affine_reslice_slice_by_slice(input_data, input_affine, output_affine, output_shape=None, slice_thickness=None, mask=False, label=False, **kwargs):
|
|
559
|
+
# generalizes affine_reslice - also works with multislice volumes where slice thickness is less than slice gap
|
|
560
|
+
|
|
561
|
+
# If 3D volume - do normal affine_reslice
|
|
562
|
+
if slice_thickness is None:
|
|
563
|
+
output_data, output_affine = affine_reslice(input_data, input_affine, output_affine, output_shape=output_shape, **kwargs)
|
|
564
|
+
# If slice thickness equals slice spacing:
|
|
565
|
+
# then its a 3D volume - do normal affine_reslice
|
|
566
|
+
elif slice_thickness == np.linalg.norm(input_affine[:3,2]):
|
|
567
|
+
output_data, output_affine = affine_reslice(input_data, input_affine, output_affine, output_shape=output_shape, **kwargs)
|
|
568
|
+
# If multislice - perform affine slice by slice
|
|
569
|
+
else:
|
|
570
|
+
output_data = None
|
|
571
|
+
for z in range(input_data.shape[2]):
|
|
572
|
+
input_data_z, input_affine_z = extract_slice(input_data, input_affine, z, slice_thickness=slice_thickness)
|
|
573
|
+
output_data_z, output_affine = affine_reslice(input_data_z, input_affine_z, output_affine, output_shape=output_shape, **kwargs)
|
|
574
|
+
if output_data is None:
|
|
575
|
+
output_data = output_data_z
|
|
576
|
+
else:
|
|
577
|
+
output_data += output_data_z
|
|
578
|
+
# If source is a mask array, convert to binary:
|
|
579
|
+
if mask:
|
|
580
|
+
output_data[output_data > 0.5] = 1
|
|
581
|
+
output_data[output_data <= 0.5] = 0
|
|
582
|
+
# If source is a label array, convert to integers:
|
|
583
|
+
elif label:
|
|
584
|
+
output_data = np.around(output_data)
|
|
585
|
+
|
|
586
|
+
return output_data, output_affine
|
|
587
|
+
|
|
588
|
+
|
|
589
|
+
|
|
475
590
|
def affine_reslice(input_data, input_affine, output_affine, output_shape=None, **kwargs):
|
|
476
591
|
|
|
477
592
|
# If 2d array, add a 3d dimension of size 1
|
|
@@ -646,60 +761,41 @@ def freeform_deformation_align(input_data, displacement, output_shape=None, outp
|
|
|
646
761
|
return output_data
|
|
647
762
|
|
|
648
763
|
|
|
649
|
-
def
|
|
764
|
+
def affine_slice(affine, z, slice_thickness=None):
|
|
650
765
|
|
|
651
766
|
# Get the slice and its affine
|
|
652
|
-
array_z = array[:,:,z]
|
|
653
767
|
affine_z = affine.copy()
|
|
654
768
|
affine_z[:3,3] += z*affine[:3,2]
|
|
655
769
|
# Set the slice spacing to equal the slice thickness.
|
|
656
770
|
# Note: both are equal for 3D array but different for 2D multislice
|
|
657
771
|
if slice_thickness is not None:
|
|
658
772
|
slice_spacing = np.linalg.norm(affine[:3,2])
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
# def _transform_slice_by_slice(
|
|
665
|
-
# moving = None,
|
|
666
|
-
# moving_affine = None,
|
|
667
|
-
# static_shape = None,
|
|
668
|
-
# static_affine = None,
|
|
669
|
-
# parameters = None,
|
|
670
|
-
# transformation = translate,
|
|
671
|
-
# slice_thickness = None):
|
|
672
|
-
|
|
673
|
-
# # Note this does not work for center of mass rotation because weight array has different center of mass.
|
|
674
|
-
|
|
675
|
-
# nz = moving.shape[2]
|
|
676
|
-
# if slice_thickness is not None:
|
|
677
|
-
# if not isinstance(slice_thickness, list):
|
|
678
|
-
# slice_thickness = [slice_thickness]*nz
|
|
679
|
-
|
|
680
|
-
# weight = np.zeros(static_shape + (nz,))
|
|
681
|
-
# coregistered = np.zeros(static_shape + (nz,))
|
|
773
|
+
if np.isscalar(slice_thickness):
|
|
774
|
+
affine_z[:3,2] *= slice_thickness/slice_spacing
|
|
775
|
+
else:
|
|
776
|
+
affine_z[:3,2] *= slice_thickness[z]/slice_spacing
|
|
777
|
+
return affine_z
|
|
682
778
|
|
|
683
|
-
# for z in range(nz):
|
|
684
|
-
# moving_z, moving_affine_z = extract_slice(moving, moving_affine, z, slice_thickness)
|
|
685
|
-
# weight[...,z] = transformation(np.ones(moving.shape[:2]), moving_affine_z, static_shape, static_affine, parameters[z])
|
|
686
|
-
# coregistered[...,z] = transformation(moving_z, moving_affine_z, static_shape, static_affine, parameters[z])
|
|
687
779
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
# coregistered[nozero] = coregistered[nozero]/weight[nozero]
|
|
780
|
+
def extract_slice(array, affine, z, slice_thickness=None):
|
|
781
|
+
array_z = array[:,:,z]
|
|
782
|
+
affine_z = affine_slice(affine, z, slice_thickness=slice_thickness)
|
|
783
|
+
return array_z, affine_z
|
|
693
784
|
|
|
694
|
-
|
|
785
|
+
def inslice_translation(affine, translation):
|
|
786
|
+
row_cosine = affine[:3,0]/np.linalg.norm(affine[:3,0])
|
|
787
|
+
column_cosine = affine[:3,1]/np.linalg.norm(affine[:3,1])
|
|
788
|
+
return translation[0]*row_cosine + translation[1]*column_cosine
|
|
695
789
|
|
|
696
790
|
|
|
697
791
|
|
|
698
792
|
####################################
|
|
699
|
-
#
|
|
793
|
+
# extensions for use in align function
|
|
700
794
|
####################################
|
|
701
795
|
|
|
702
|
-
|
|
796
|
+
def translate_inslice(input_data, input_affine, output_shape, output_affine, translation, **kwargs):
|
|
797
|
+
transformation = affine_matrix(translation=inslice_translation(input_affine, translation))
|
|
798
|
+
return affine_transform_and_reslice(input_data, input_affine, output_shape, output_affine, transformation, **kwargs)
|
|
703
799
|
|
|
704
800
|
def translate(input_data, input_affine, output_shape, output_affine, translation, **kwargs):
|
|
705
801
|
transformation = affine_matrix(translation=translation)
|
|
@@ -776,7 +872,6 @@ def freeform_align(input_data, input_affine, output_shape, output_affine, parame
|
|
|
776
872
|
def transform_slice_by_slice(input_data, input_affine, output_shape, output_affine, parameters, transformation=translate, slice_thickness=None):
|
|
777
873
|
|
|
778
874
|
# Note this does not work for center of mass rotation because weight array has different center of mass.
|
|
779
|
-
|
|
780
875
|
nz = input_data.shape[2]
|
|
781
876
|
if slice_thickness is not None:
|
|
782
877
|
if not isinstance(slice_thickness, list):
|
|
@@ -795,93 +890,97 @@ def transform_slice_by_slice(input_data, input_affine, output_shape, output_affi
|
|
|
795
890
|
# Average each pixel value over all slices that have sampled it
|
|
796
891
|
nozero = np.where(weight > 0)
|
|
797
892
|
coregistered[nozero] = coregistered[nozero]/weight[nozero]
|
|
798
|
-
|
|
799
893
|
return coregistered
|
|
800
894
|
|
|
895
|
+
def passive_translation_slice_by_slice(input_affine, parameters):
|
|
896
|
+
output_affine = []
|
|
897
|
+
for z, pz in enumerate(parameters):
|
|
898
|
+
input_affine_z = affine_slice(input_affine, z)
|
|
899
|
+
rigid_transform = affine_matrix(translation=pz)
|
|
900
|
+
transformed_input_affine = rigid_transform.dot(input_affine_z)
|
|
901
|
+
output_affine.append(transformed_input_affine)
|
|
902
|
+
return output_affine
|
|
903
|
+
|
|
904
|
+
def passive_rigid_transform_slice_by_slice(input_affine, parameters):
|
|
905
|
+
output_affine = []
|
|
906
|
+
for z, pz in enumerate(parameters):
|
|
907
|
+
input_affine_z = affine_slice(input_affine, z)
|
|
908
|
+
rigid_transform = affine_matrix(rotation=pz[:3], translation=pz[3:])
|
|
909
|
+
#rigid_transform = affine_matrix(rotation=z*(np.pi/180)*np.array([1,0,0])) # for debugging
|
|
910
|
+
transformed_input_affine = rigid_transform.dot(input_affine_z)
|
|
911
|
+
output_affine.append(transformed_input_affine)
|
|
912
|
+
return output_affine
|
|
913
|
+
|
|
914
|
+
def passive_translation(input_affine, parameters):
|
|
915
|
+
rigid_transform = affine_matrix(translation=parameters)
|
|
916
|
+
output_affine = rigid_transform.dot(input_affine)
|
|
917
|
+
return output_affine
|
|
918
|
+
|
|
919
|
+
def passive_rigid_transform(input_affine, parameters):
|
|
920
|
+
rigid_transform = affine_matrix(rotation=parameters[:3], translation=parameters[3:])
|
|
921
|
+
output_affine = rigid_transform.dot(input_affine)
|
|
922
|
+
return output_affine
|
|
923
|
+
|
|
924
|
+
def multislice_to_singleslice_affine(affine_ms, slice_thickness):
|
|
925
|
+
# In a multi-slice affine, the z-spacing is the slice spacing (distance between slice centers)
|
|
926
|
+
# In a single-slice affine, the z-spacing is the slice thickness
|
|
927
|
+
affine_ss = affine_ms.copy()
|
|
928
|
+
affine_ss[:3,2] *= slice_thickness/np.linalg.norm(affine_ss[:3,2])
|
|
929
|
+
return affine_ss
|
|
930
|
+
|
|
931
|
+
|
|
801
932
|
|
|
802
933
|
# default metrics
|
|
803
934
|
# ---------------
|
|
804
935
|
|
|
805
|
-
def
|
|
936
|
+
def interaction(static, transformed, nan=None):
|
|
937
|
+
# static is here a mask image.
|
|
806
938
|
if nan is not None:
|
|
807
939
|
i = np.where(transformed != nan)
|
|
808
|
-
|
|
940
|
+
msk, img = static[i], transformed[i]
|
|
809
941
|
else:
|
|
810
|
-
|
|
942
|
+
msk, img = static, transformed
|
|
943
|
+
return np.std(img[np.where(msk>0.5)])
|
|
944
|
+
return 1/np.mean(np.abs(msk*img))
|
|
945
|
+
return np.exp(-np.mean(np.abs(msk*img)))
|
|
946
|
+
i = img[np.where(msk>0.5)]
|
|
947
|
+
return np.exp(-np.mean(i**2))
|
|
811
948
|
|
|
812
949
|
|
|
813
|
-
def
|
|
950
|
+
def sum_of_squares(static, transformed, nan=None):
|
|
814
951
|
if nan is not None:
|
|
815
952
|
i = np.where(transformed != nan)
|
|
816
|
-
|
|
953
|
+
st, tr = static[i], transformed[i]
|
|
817
954
|
else:
|
|
818
|
-
|
|
955
|
+
st, tr = static, transformed
|
|
956
|
+
return np.sum(np.square(st-tr))
|
|
819
957
|
|
|
820
958
|
|
|
821
|
-
def
|
|
959
|
+
def mutual_information(static, transformed, nan=None):
|
|
960
|
+
|
|
961
|
+
# Mask if needed
|
|
822
962
|
if nan is not None:
|
|
823
963
|
i = np.where(transformed != nan)
|
|
824
|
-
|
|
825
|
-
else:
|
|
826
|
-
return sklearn.metrics.normalized_mutual_info_score(static, transformed)
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
# Generic objective function
|
|
831
|
-
def _objective_function(
|
|
832
|
-
params,
|
|
833
|
-
moving: np.ndarray,
|
|
834
|
-
static: np.ndarray,
|
|
835
|
-
transformation,
|
|
836
|
-
metric,
|
|
837
|
-
moving_affine,
|
|
838
|
-
static_affine,
|
|
839
|
-
moving_mask,
|
|
840
|
-
static_mask,
|
|
841
|
-
transformation_args,
|
|
842
|
-
metric_args):
|
|
843
|
-
|
|
844
|
-
# # Get static size (TODO: use shape instead of size in transformations)
|
|
845
|
-
# _, _, static_pixel_spacing = affine_components(static_affine)
|
|
846
|
-
# static_size = np.multiply(np.array(static.shape)-1, static_pixel_spacing)
|
|
847
|
-
|
|
848
|
-
# Transform the moving image
|
|
849
|
-
nan = 0
|
|
850
|
-
if transformation_args is None:
|
|
851
|
-
transformed = transformation(moving, moving_affine, static.shape, static_affine, params, cval=nan)
|
|
852
|
-
else:
|
|
853
|
-
transformed = transformation(moving, moving_affine, static.shape, static_affine, params, transformation_args, cval=nan)
|
|
854
|
-
|
|
855
|
-
# If a moving mask is provided, transform this as well
|
|
856
|
-
if moving_mask is not None:
|
|
857
|
-
if transformation_args is None:
|
|
858
|
-
transformed_mask = transformation(moving_mask, moving_affine, static.shape, static_affine, params)
|
|
859
|
-
else:
|
|
860
|
-
transformed_mask = transformation(moving_mask, moving_affine, static.shape, static_affine, params, transformation_args)
|
|
861
|
-
transformed_mask[transformed_mask > 0.5] = 1
|
|
862
|
-
transformed_mask[transformed_mask <= 0.5] = 0
|
|
863
|
-
|
|
864
|
-
# Get the indices in the static data where the mask is non-zero
|
|
865
|
-
if static_mask is not None and moving_mask is not None:
|
|
866
|
-
ind = np.where(transformed!=nan and static_mask==1 and transformed_mask==1)
|
|
867
|
-
elif static_mask is not None:
|
|
868
|
-
ind = np.where(transformed!=nan and static_mask==1)
|
|
869
|
-
elif moving_mask is not None:
|
|
870
|
-
ind = np.where(transformed!=nan and transformed_mask==1)
|
|
871
|
-
else:
|
|
872
|
-
ind = np.where(transformed!=nan)
|
|
873
|
-
|
|
874
|
-
# Calculate the cost function
|
|
875
|
-
if metric_args is None:
|
|
876
|
-
return metric(static[ind], transformed[ind])
|
|
964
|
+
st, tr = static[i], transformed[i]
|
|
877
965
|
else:
|
|
878
|
-
|
|
966
|
+
st, tr = static, transformed
|
|
967
|
+
# Calculate 2d histogram
|
|
968
|
+
hist_2d, _, _ = np.histogram2d(st.ravel(), tr.ravel(), bins=20)
|
|
969
|
+
# Convert bins counts to probability values
|
|
970
|
+
pxy = hist_2d / float(np.sum(hist_2d))
|
|
971
|
+
px = np.sum(pxy, axis=1) # marginal for x over y
|
|
972
|
+
py = np.sum(pxy, axis=0) # marginal for y over x
|
|
973
|
+
px_py = px[:, None] * py[None, :] # Broadcast to multiply marginals
|
|
974
|
+
# Now we can do the calculation using the pxy, px_py 2D arrays
|
|
975
|
+
nzs = pxy > 0 # Only non-zero pxy values contribute to the sum
|
|
976
|
+
return -np.sum(pxy[nzs] * np.log(pxy[nzs] / px_py[nzs]))
|
|
879
977
|
|
|
880
978
|
|
|
881
979
|
def print_current(xk):
|
|
882
980
|
print('Current parameter estimates: ' , xk)
|
|
883
981
|
return False
|
|
884
982
|
|
|
983
|
+
|
|
885
984
|
def print_current_norm(xk):
|
|
886
985
|
print('Norm of current parameter estimates: ' , np.linalg.norm(xk))
|
|
887
986
|
return False
|
|
@@ -892,10 +991,28 @@ def minimize(*args, method='GD', **kwargs):
|
|
|
892
991
|
|
|
893
992
|
if method == 'GD':
|
|
894
993
|
return minimize_gd(*args, **kwargs)
|
|
994
|
+
elif method=='brute':
|
|
995
|
+
return minimize_brute(*args, **kwargs)
|
|
895
996
|
else:
|
|
896
997
|
res = opt.minimize(*args, method=method, **kwargs)
|
|
897
998
|
return res.x
|
|
898
|
-
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def minimize_brute(cost_function, parameters, args=None, callback=None, options={}):
|
|
1002
|
+
#options = {'grid':[[start, stop, num], [start, stop, num], ...]}
|
|
1003
|
+
x = [np.linspace(p[0], p[1], p[2]) for p in options['grid']]
|
|
1004
|
+
x = np.meshgrid(*tuple(x), indexing='ij')
|
|
1005
|
+
for i in range(x[0].size):
|
|
1006
|
+
parameters = np.array([xp.ravel()[i] for xp in x])
|
|
1007
|
+
cost = cost_function(parameters, *args)
|
|
1008
|
+
if i==0:
|
|
1009
|
+
minimum = cost
|
|
1010
|
+
par_opt = parameters
|
|
1011
|
+
elif cost<minimum:
|
|
1012
|
+
minimum = cost
|
|
1013
|
+
par_opt = parameters
|
|
1014
|
+
return par_opt
|
|
1015
|
+
|
|
899
1016
|
|
|
900
1017
|
def minimize_gd(cost_function, parameters, args=None, callback=None, options={}, bounds=None):
|
|
901
1018
|
|
|
@@ -931,7 +1048,6 @@ def minimize_gd(cost_function, parameters, args=None, callback=None, options={},
|
|
|
931
1048
|
return parameters
|
|
932
1049
|
|
|
933
1050
|
|
|
934
|
-
|
|
935
1051
|
def gradient(cost_function, parameters, f0, step, bounds, *args):
|
|
936
1052
|
grad = np.empty(parameters.shape)
|
|
937
1053
|
for i in range(parameters.size):
|
|
@@ -952,8 +1068,7 @@ def gradient(cost_function, parameters, f0, step, bounds, *args):
|
|
|
952
1068
|
# Normalize the gradient
|
|
953
1069
|
grad_norm = np.linalg.norm(grad)
|
|
954
1070
|
if grad_norm == 0:
|
|
955
|
-
|
|
956
|
-
raise ValueError(msg)
|
|
1071
|
+
return grad
|
|
957
1072
|
grad /= grad_norm
|
|
958
1073
|
grad = np.multiply(step, grad)
|
|
959
1074
|
|
|
@@ -1044,18 +1159,31 @@ def line_search(cost_function, grad, p0, stepsize0, f0, bounds, *args, tolerance
|
|
|
1044
1159
|
raise ValueError(msg)
|
|
1045
1160
|
|
|
1046
1161
|
|
|
1047
|
-
def goodness_of_alignment(params, transformation, metric, moving, moving_affine, static, static_affine, coord):
|
|
1162
|
+
def goodness_of_alignment(params, transformation, metric, moving, moving_affine, static, static_affine, coord, moving_mask, static_mask_ind):
|
|
1048
1163
|
|
|
1049
1164
|
# Transform the moving image
|
|
1050
1165
|
nan = 2**16-2 #np.nan does not work
|
|
1051
1166
|
transformed = transformation(moving, moving_affine, static.shape, static_affine, params, output_coordinates=coord, cval=nan)
|
|
1052
|
-
|
|
1053
|
-
# Calculate the cost function
|
|
1054
|
-
ls = metric(static, transformed, nan=nan)
|
|
1055
1167
|
|
|
1056
|
-
|
|
1168
|
+
# If a moving mask is provided, this needs to be transformed in the same way
|
|
1169
|
+
if moving_mask is None:
|
|
1170
|
+
moving_mask_ind = None
|
|
1171
|
+
else:
|
|
1172
|
+
mask_transformed = transformation(moving_mask, moving_affine, static.shape, static_affine, params, output_coordinates=coord, cval=nan)
|
|
1173
|
+
moving_mask_ind = np.where(mask_transformed >= 0.5)
|
|
1174
|
+
|
|
1175
|
+
# Calculate matric in indices exposed by the mask(s)
|
|
1176
|
+
if static_mask_ind is None and moving_mask_ind is None:
|
|
1177
|
+
return metric(static, transformed, nan=nan)
|
|
1178
|
+
if static_mask_ind is None and moving_mask_ind is not None:
|
|
1179
|
+
return metric(static[moving_mask_ind], transformed[moving_mask_ind], nan=nan)
|
|
1180
|
+
if static_mask_ind is not None and moving_mask_ind is None:
|
|
1181
|
+
return metric(static[static_mask_ind], transformed[static_mask_ind], nan=nan)
|
|
1182
|
+
if static_mask_ind is not None and moving_mask_ind is not None:
|
|
1183
|
+
ind = static_mask_ind or moving_mask_ind
|
|
1184
|
+
return metric(static[ind], transformed[ind], nan=nan)
|
|
1185
|
+
|
|
1057
1186
|
|
|
1058
|
-
|
|
1059
1187
|
def align(
|
|
1060
1188
|
moving = None,
|
|
1061
1189
|
static = None,
|
|
@@ -1065,7 +1193,11 @@ def align(
|
|
|
1065
1193
|
transformation = translate,
|
|
1066
1194
|
metric = sum_of_squares,
|
|
1067
1195
|
optimization = {'method':'GD', 'options':{}},
|
|
1068
|
-
resolutions = [1]
|
|
1196
|
+
resolutions = [1],
|
|
1197
|
+
static_mask = None,
|
|
1198
|
+
static_mask_affine = None,
|
|
1199
|
+
moving_mask = None,
|
|
1200
|
+
moving_mask_affine = None):
|
|
1069
1201
|
|
|
1070
1202
|
# Set defaults
|
|
1071
1203
|
if moving is None:
|
|
@@ -1074,9 +1206,6 @@ def align(
|
|
|
1074
1206
|
if static is None:
|
|
1075
1207
|
msg = 'The static volume is a required argument for alignment'
|
|
1076
1208
|
raise ValueError(msg)
|
|
1077
|
-
if parameters is None:
|
|
1078
|
-
msg = 'Initial values for alignment must be provided'
|
|
1079
|
-
raise ValueError(msg)
|
|
1080
1209
|
if moving.ndim == 2: # If 2d array, add a 3d dimension of size 1
|
|
1081
1210
|
moving = np.expand_dims(moving, axis=-1)
|
|
1082
1211
|
if static.ndim == 2: # If 2d array, add a 3d dimension of size 1
|
|
@@ -1085,7 +1214,12 @@ def align(
|
|
|
1085
1214
|
moving_affine = np.eye(1 + moving.ndim)
|
|
1086
1215
|
if static_affine is None:
|
|
1087
1216
|
static_affine = np.eye(1 + static.ndim)
|
|
1088
|
-
|
|
1217
|
+
if moving_mask is not None:
|
|
1218
|
+
if moving_mask_affine is None:
|
|
1219
|
+
moving_mask_affine = moving_affine
|
|
1220
|
+
if static_mask is not None:
|
|
1221
|
+
if static_mask_affine is None:
|
|
1222
|
+
static_mask_affine = static_affine
|
|
1089
1223
|
|
|
1090
1224
|
# Perform multi-resolution loop
|
|
1091
1225
|
for res in resolutions:
|
|
@@ -1106,19 +1240,27 @@ def align(
|
|
|
1106
1240
|
static_resampled_affine = affine_matrix(rotation=r, translation=t, pixel_spacing=p*res)
|
|
1107
1241
|
static_resampled, static_resampled_affine = affine_reslice(static, static_affine, static_resampled_affine)
|
|
1108
1242
|
|
|
1243
|
+
# resample the masks on the geometry of the target volumes
|
|
1244
|
+
if moving_mask is None:
|
|
1245
|
+
moving_mask_resampled = None
|
|
1246
|
+
else:
|
|
1247
|
+
moving_mask_resampled, _ = affine_reslice(moving_mask, moving_mask_affine, moving_resampled_affine, moving_resampled.shape)
|
|
1248
|
+
if static_mask is None:
|
|
1249
|
+
static_mask_resampled_ind = None
|
|
1250
|
+
else:
|
|
1251
|
+
static_mask_resampled, _ = affine_reslice(static_mask, static_mask_affine, static_resampled_affine, static_resampled.shape)
|
|
1252
|
+
static_mask_resampled_ind = np.where(static_mask_resampled >= 0.5)
|
|
1253
|
+
|
|
1109
1254
|
coord = volume_coordinates(static_resampled.shape)
|
|
1110
1255
|
# Here we need a generic precomputation step:
|
|
1111
1256
|
# prec = precompute(moving_resampled, moving_resampled_affine, static_resampled, static_resampled_affine)
|
|
1112
1257
|
# args = (transformation, metric, moving_resampled, moving_resampled_affine, static_resampled, static_resampled_affine, coord, prec)
|
|
1113
|
-
args = (transformation, metric, moving_resampled, moving_resampled_affine, static_resampled, static_resampled_affine, coord)
|
|
1258
|
+
args = (transformation, metric, moving_resampled, moving_resampled_affine, static_resampled, static_resampled_affine, coord, moving_mask_resampled, static_mask_resampled_ind)
|
|
1114
1259
|
parameters = minimize(goodness_of_alignment, parameters, args=args, **optimization)
|
|
1115
1260
|
|
|
1116
1261
|
return parameters
|
|
1117
1262
|
|
|
1118
1263
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
1264
|
def align_slice_by_slice(
|
|
1123
1265
|
moving = None,
|
|
1124
1266
|
static = None,
|
|
@@ -1130,22 +1272,34 @@ def align_slice_by_slice(
|
|
|
1130
1272
|
optimization = {'method':'GD', 'options':{}},
|
|
1131
1273
|
resolutions = [1],
|
|
1132
1274
|
slice_thickness = None,
|
|
1133
|
-
progress = None
|
|
1275
|
+
progress = None,
|
|
1276
|
+
static_mask = None,
|
|
1277
|
+
static_mask_affine = None,
|
|
1278
|
+
moving_mask = None,
|
|
1279
|
+
moving_mask_affine = None):
|
|
1134
1280
|
|
|
1135
1281
|
# If a single slice thickness is provided, turn it into a list.
|
|
1136
1282
|
nz = moving.shape[2]
|
|
1137
1283
|
if slice_thickness is not None:
|
|
1138
1284
|
if not isinstance(slice_thickness, list):
|
|
1139
1285
|
slice_thickness = [slice_thickness]*nz
|
|
1286
|
+
if not isinstance(parameters, list):
|
|
1287
|
+
parameters = [parameters]*nz
|
|
1140
1288
|
|
|
1141
1289
|
estimate = []
|
|
1142
1290
|
for z in range(nz):
|
|
1143
1291
|
|
|
1292
|
+
print('SLICE: ', z)
|
|
1293
|
+
|
|
1144
1294
|
if progress is not None:
|
|
1145
1295
|
progress(z, nz)
|
|
1146
1296
|
|
|
1147
1297
|
# Get the slice and its affine
|
|
1148
1298
|
moving_z, moving_affine_z = extract_slice(moving, moving_affine, z, slice_thickness)
|
|
1299
|
+
if moving_mask is None:
|
|
1300
|
+
moving_mask_z, moving_mask_affine_z = None, None
|
|
1301
|
+
else:
|
|
1302
|
+
moving_mask_z, moving_mask_affine_z = extract_slice(moving_mask, moving_mask_affine, z, slice_thickness)
|
|
1149
1303
|
|
|
1150
1304
|
# Align volumes
|
|
1151
1305
|
try:
|
|
@@ -1154,14 +1308,18 @@ def align_slice_by_slice(
|
|
|
1154
1308
|
moving_affine = moving_affine_z,
|
|
1155
1309
|
static = static,
|
|
1156
1310
|
static_affine = static_affine,
|
|
1157
|
-
parameters = parameters,
|
|
1311
|
+
parameters = parameters[z],
|
|
1158
1312
|
resolutions = resolutions,
|
|
1159
1313
|
transformation = transformation,
|
|
1160
1314
|
metric = metric,
|
|
1161
1315
|
optimization = optimization,
|
|
1316
|
+
static_mask = static_mask,
|
|
1317
|
+
static_mask_affine = static_mask_affine,
|
|
1318
|
+
moving_mask = moving_mask_z,
|
|
1319
|
+
moving_mask_affine = moving_mask_affine_z,
|
|
1162
1320
|
)
|
|
1163
1321
|
except:
|
|
1164
|
-
estimate_z = parameters
|
|
1322
|
+
estimate_z = parameters[z]
|
|
1165
1323
|
|
|
1166
1324
|
estimate.append(estimate_z)
|
|
1167
1325
|
|
|
@@ -1169,8 +1327,6 @@ def align_slice_by_slice(
|
|
|
1169
1327
|
|
|
1170
1328
|
|
|
1171
1329
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
1330
|
#############################
|
|
1175
1331
|
# Plot results
|
|
1176
1332
|
#############################
|
|
@@ -1272,6 +1428,21 @@ def plot_affine_transformed(input_data, input_affine, output_data, output_affine
|
|
|
1272
1428
|
pl.show()
|
|
1273
1429
|
|
|
1274
1430
|
|
|
1431
|
+
def plot_bounding_box(input_data, input_affine, output_shape, output_affine):
|
|
1432
|
+
|
|
1433
|
+
pl = plot_volume(input_data, input_affine)
|
|
1434
|
+
|
|
1435
|
+
# Plot wireframe around edges of resliced volume
|
|
1436
|
+
vertices, faces = parallellepid(output_shape, affine=output_affine)
|
|
1437
|
+
box = pv.PolyData(vertices, faces)
|
|
1438
|
+
pl.add_mesh(box,
|
|
1439
|
+
style='wireframe',
|
|
1440
|
+
opacity=1.0,
|
|
1441
|
+
color=(255,0,0),
|
|
1442
|
+
)
|
|
1443
|
+
pl.show()
|
|
1444
|
+
|
|
1445
|
+
|
|
1275
1446
|
def plot_freeform_transformed(input_data, input_affine, output_data, output_affine):
|
|
1276
1447
|
|
|
1277
1448
|
pl = plot_affine_resliced(input_data, input_affine, output_data, output_affine)
|
|
@@ -1711,6 +1882,29 @@ def test_plot(n=1):
|
|
|
1711
1882
|
pl.show()
|
|
1712
1883
|
|
|
1713
1884
|
|
|
1885
|
+
def test_bounding_box():
|
|
1886
|
+
|
|
1887
|
+
# Define geometry of source data
|
|
1888
|
+
input_shape = np.array([300, 250, 12]) # mm
|
|
1889
|
+
pixel_spacing = np.array([1.25, 1.25, 10]) # mm
|
|
1890
|
+
rotation_angle = 0.5 * (np.pi/2) # radians
|
|
1891
|
+
rotation_axis = [1,0,0]
|
|
1892
|
+
translation = np.array([0, -40, 180]) # mm
|
|
1893
|
+
margin = 10 # mm
|
|
1894
|
+
|
|
1895
|
+
# Generate reference volume
|
|
1896
|
+
rotation = rotation_angle * np.array(rotation_axis)/np.linalg.norm(rotation_axis)
|
|
1897
|
+
input_affine = affine_matrix(rotation=rotation, translation=translation, pixel_spacing=pixel_spacing)
|
|
1898
|
+
#input_data, input_affine = generate('double ellipsoid', shape=input_shape, affine=input_affine, markers=False)
|
|
1899
|
+
input_data, input_affine = generate('triple ellipsoid', shape=input_shape, affine=input_affine, markers=False)
|
|
1900
|
+
|
|
1901
|
+
# Perform translation with reshaping
|
|
1902
|
+
output_data, output_affine = bounding_box(input_data, input_affine, margin=margin)
|
|
1903
|
+
|
|
1904
|
+
# Display results
|
|
1905
|
+
plot_bounding_box(input_data, input_affine, output_data.shape, output_affine)
|
|
1906
|
+
|
|
1907
|
+
|
|
1714
1908
|
def test_affine_reslice(n=1):
|
|
1715
1909
|
|
|
1716
1910
|
if n==1:
|
|
@@ -2528,7 +2722,6 @@ def test_align_freeform(n=1):
|
|
|
2528
2722
|
pl.show()
|
|
2529
2723
|
|
|
2530
2724
|
|
|
2531
|
-
|
|
2532
2725
|
def test_align_freeform_pyramid(n=1):
|
|
2533
2726
|
|
|
2534
2727
|
if n==1:
|
|
@@ -2580,14 +2773,13 @@ def test_align_freeform_pyramid(n=1):
|
|
|
2580
2773
|
|
|
2581
2774
|
|
|
2582
2775
|
|
|
2583
|
-
|
|
2584
2776
|
if __name__ == "__main__":
|
|
2585
|
-
|
|
2586
|
-
dataset=2
|
|
2777
|
+
pass
|
|
2587
2778
|
|
|
2588
2779
|
# Test plotting
|
|
2589
2780
|
# -------------
|
|
2590
2781
|
# test_plot(1)
|
|
2782
|
+
# test_bounding_box()
|
|
2591
2783
|
|
|
2592
2784
|
|
|
2593
2785
|
# Test affine transformations
|
|
@@ -2621,6 +2813,6 @@ if __name__ == "__main__":
|
|
|
2621
2813
|
# test_align_stretch(n=2)
|
|
2622
2814
|
# test_align_rigid(n=1)
|
|
2623
2815
|
# test_align_affine(n=1)
|
|
2624
|
-
test_align_freeform(n=3)
|
|
2816
|
+
# test_align_freeform(n=3)
|
|
2625
2817
|
|
|
2626
2818
|
|