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.

Files changed (50) hide show
  1. dbdicom/__init__.py +4 -3
  2. dbdicom/create.py +34 -97
  3. dbdicom/dro.py +174 -0
  4. dbdicom/ds/dataset.py +29 -3
  5. dbdicom/ds/types/mr_image.py +18 -7
  6. dbdicom/extensions/__init__.py +10 -0
  7. dbdicom/{wrappers → extensions}/dipy.py +191 -205
  8. dbdicom/extensions/elastix.py +503 -0
  9. dbdicom/extensions/matplotlib.py +107 -0
  10. dbdicom/extensions/numpy.py +271 -0
  11. dbdicom/{wrappers → extensions}/scipy.py +130 -31
  12. dbdicom/{wrappers → extensions}/skimage.py +1 -1
  13. dbdicom/extensions/sklearn.py +243 -0
  14. dbdicom/extensions/vreg.py +1390 -0
  15. dbdicom/external/dcm4che/bin/emf2sf +57 -57
  16. dbdicom/manager.py +70 -36
  17. dbdicom/pipelines.py +66 -0
  18. dbdicom/record.py +266 -43
  19. dbdicom/types/instance.py +17 -3
  20. dbdicom/types/series.py +1900 -404
  21. dbdicom/utils/image.py +152 -21
  22. dbdicom/utils/vreg.py +327 -135
  23. dbdicom-0.2.3.dist-info/METADATA +88 -0
  24. {dbdicom-0.2.1.dist-info → dbdicom-0.2.3.dist-info}/RECORD +27 -41
  25. {dbdicom-0.2.1.dist-info → dbdicom-0.2.3.dist-info}/WHEEL +1 -1
  26. dbdicom/external/__pycache__/__init__.cpython-310.pyc +0 -0
  27. dbdicom/external/__pycache__/__init__.cpython-37.pyc +0 -0
  28. dbdicom/external/dcm4che/__pycache__/__init__.cpython-310.pyc +0 -0
  29. dbdicom/external/dcm4che/__pycache__/__init__.cpython-37.pyc +0 -0
  30. dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-310.pyc +0 -0
  31. dbdicom/external/dcm4che/bin/__pycache__/__init__.cpython-37.pyc +0 -0
  32. dbdicom/external/dcm4che/lib/linux-x86/libclib_jiio.so +0 -0
  33. dbdicom/external/dcm4che/lib/linux-x86-64/libclib_jiio.so +0 -0
  34. dbdicom/external/dcm4che/lib/linux-x86-64/libopencv_java.so +0 -0
  35. dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio.so +0 -0
  36. dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis.so +0 -0
  37. dbdicom/external/dcm4che/lib/solaris-sparc/libclib_jiio_vis2.so +0 -0
  38. dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio.so +0 -0
  39. dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis.so +0 -0
  40. dbdicom/external/dcm4che/lib/solaris-sparcv9/libclib_jiio_vis2.so +0 -0
  41. dbdicom/external/dcm4che/lib/solaris-x86/libclib_jiio.so +0 -0
  42. dbdicom/external/dcm4che/lib/solaris-x86-64/libclib_jiio.so +0 -0
  43. dbdicom/wrappers/__init__.py +0 -7
  44. dbdicom/wrappers/elastix.py +0 -855
  45. dbdicom/wrappers/numpy.py +0 -119
  46. dbdicom/wrappers/sklearn.py +0 -151
  47. dbdicom/wrappers/vreg.py +0 -273
  48. dbdicom-0.2.1.dist-info/METADATA +0 -276
  49. {dbdicom-0.2.1.dist-info → dbdicom-0.2.3.dist-info}/LICENSE +0 -0
  50. {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.stats as stats
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.UniformGrid(dimensions=surf_shape, spacing=pixel_spacing)
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
- # TODO This needs to become a private helper function
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 extract_slice(array, affine, z, slice_thickness=None):
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
- affine_z[:3,2] *= slice_thickness[z]/slice_spacing
660
-
661
- return array_z, affine_z
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
- # # Average each pixel value over all slices that have sampled it
689
- # coregistered = np.sum(weight*coregistered, axis=-1)
690
- # weight = np.sum(weight, axis=-1)
691
- # nozero = np.where(weight > 0)
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
- # return coregistered
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
- # wrappers for use in align function
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 sum_of_squares(static, transformed, nan=None):
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
- return np.sum(np.square(static[i]-transformed[i]))
940
+ msk, img = static[i], transformed[i]
809
941
  else:
810
- return np.sum(np.square(static-transformed))
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 mutual_information(static, transformed, nan=None):
950
+ def sum_of_squares(static, transformed, nan=None):
814
951
  if nan is not None:
815
952
  i = np.where(transformed != nan)
816
- return sklearn.metrics.mutual_info_score(static[i], transformed[i])
953
+ st, tr = static[i], transformed[i]
817
954
  else:
818
- return sklearn.metrics.mutual_info_score(static, transformed)
955
+ st, tr = static, transformed
956
+ return np.sum(np.square(st-tr))
819
957
 
820
958
 
821
- def normalized_mutual_information(static, transformed, nan=None):
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
- return sklearn.metrics.normalized_mutual_info_score(static[i], transformed[i])
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
- return metric(static[ind], transformed[ind], metric_args)
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
- msg = 'Gradient has length zero - cannot perform gradient descent'
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
- return ls
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