1919import skimage .filters
2020import skimage .measure
2121import vtk
22+ from packaging .version import parse
2223from scipy .interpolate import LinearNDInterpolator
2324from scipy .ndimage import distance_transform_edt
2425from vtk .util .numpy_support import numpy_to_vtk
2526
2627from openlifu .geo import cartesian_to_spherical
2728
2829
30+ def apply_affine_to_polydata (affine :np .ndarray , polydata :vtk .vtkPolyData ) -> vtk .vtkPolyData :
31+ """Apply an affine transform to a vtkPolyData."""
32+ affine_vtkmat = vtk .vtkMatrix4x4 ()
33+ for i in range (4 ):
34+ for j in range (4 ):
35+ affine_vtkmat .SetElement (i , j , affine [i , j ])
36+ affine_vtktransform = vtk .vtkTransform ()
37+ affine_vtktransform .SetMatrix (affine_vtkmat )
38+ transform_filter = vtk .vtkTransformPolyDataFilter ()
39+ transform_filter .SetTransform (affine_vtktransform )
40+ transform_filter .SetInputData (polydata )
41+ transform_filter .Update ()
42+ return transform_filter .GetOutput ()
43+
2944def take_largest_connected_component (mask : np .ndarray ) -> np .ndarray :
3045 """Given a boolean image array (or any integer numpy array), return a mask of the largest connected component."""
3146 mask_labeled = skimage .measure .label (mask )
@@ -145,6 +160,23 @@ def vtk_img_from_array_and_affine(vol_array:np.ndarray, affine:np.ndarray) -> vt
145160
146161 return vtk_img
147162
163+ def affine_from_vtk_image_data (vtk_img :vtk .vtkImageData ) -> np .ndarray :
164+ """Get a 4x4 affine matrix out of a vtkImageData, a partial reverse to `vtk_img_from_array_and_affine`"""
165+ origin = np .array (vtk_img .GetOrigin ())
166+ spacing = np .array (vtk_img .GetSpacing ())
167+
168+ direction_vtk = vtk_img .GetDirectionMatrix ()
169+ direction = np .eye (3 )
170+ for i in range (3 ):
171+ for j in range (3 ):
172+ direction [i , j ] = direction_vtk .GetElement (i , j )
173+
174+ affine = np .eye (4 , dtype = float )
175+ affine [:3 , :3 ] = direction @ np .diag (spacing )
176+ affine [:3 , 3 ] = origin
177+
178+ return affine
179+
148180def create_closed_surface_from_labelmap (
149181 binary_labelmap :vtk .vtkImageData ,
150182 decimation_factor :float = 0. ,
@@ -164,6 +196,17 @@ def create_closed_surface_from_labelmap(
164196 https://github.com/Slicer/Slicer/blob/677932127c73a6c78654d4afd9458a655a4eef63/Libs/vtkSegmentationCore/vtkBinaryLabelmapToClosedSurfaceConversionRule.cxx#L246-L476
165197 """
166198
199+ affine = None # Only needed if vtk version is less than 9.3.0
200+ if parse (vtk .__version__ ) < parse ("9.3.0" ):
201+ # In these older versions of vtk, the labelmap would not work.
202+ affine = affine_from_vtk_image_data (binary_labelmap )
203+ binary_labelmap .SetOrigin ([0 ,0 ,0 ])
204+ binary_labelmap .SetSpacing ([1 ,1 ,1 ])
205+ direction_matrix_vtk = vtk .vtkMatrix3x3 ()
206+ direction_matrix_vtk .Identity ()
207+ binary_labelmap .SetDirectionMatrix (direction_matrix_vtk )
208+
209+
167210 # step 1: pad by 1 pixel all around with 0s, to ensure that the surface is still closed
168211 # even if the labelmap runs up against the image boundary.
169212 padder = vtk .vtkImageConstantPad ()
@@ -223,6 +266,14 @@ def create_closed_surface_from_labelmap(
223266 normals .Update ()
224267 surface_mesh = normals .GetOutput ()
225268
269+ if parse (vtk .__version__ ) < parse ("9.3.0" ):
270+ # In these older versions of vtk, the labelmap internal affine transform is not used correctly,
271+ # so we manually apply the transform after the fact
272+ surface_mesh = apply_affine_to_polydata (
273+ affine ,
274+ surface_mesh ,
275+ )
276+
226277 return surface_mesh
227278
228279def spherical_interpolator_from_mesh (
0 commit comments