Skip to content

Commit d7776db

Browse files
Fix closed surface creation for older vtk
Closes OpenwaterHealth#237 The workaround here is just to manually apply the transform of the vtkImageData to the vtkPolyData that is created, only if the vtk version is below 9.3.0.
1 parent 4938fa6 commit d7776db

2 files changed

Lines changed: 63 additions & 14 deletions

File tree

src/openlifu/seg/skinseg.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,28 @@
1919
import skimage.filters
2020
import skimage.measure
2121
import vtk
22+
from packaging.version import parse
2223
from scipy.interpolate import LinearNDInterpolator
2324
from scipy.ndimage import distance_transform_edt
2425
from vtk.util.numpy_support import numpy_to_vtk
2526

2627
from 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+
2944
def 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+
148180
def 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

228279
def spherical_interpolator_from_mesh(

tests/test_skinseg.py

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
from scipy.stats import skew
1010

1111
from openlifu.seg.skinseg import (
12+
affine_from_vtk_image_data,
13+
apply_affine_to_polydata,
1214
cartesian_to_spherical,
1315
compute_foreground_mask,
1416
create_closed_surface_from_labelmap,
@@ -37,20 +39,6 @@ def add_ball(
3739

3840
volume[dist_sq <= radius**2] = value
3941

40-
def apply_affine_to_polydata(affine:np.ndarray, polydata:vtk.vtkPolyData) -> vtk.vtkPolyData:
41-
"""Apply an affine transform to a vtkPolyData"""
42-
affine_vtkmat = vtk.vtkMatrix4x4()
43-
for i in range(4):
44-
for j in range(4):
45-
affine_vtkmat.SetElement(i, j, affine[i, j])
46-
affine_vtktransform = vtk.vtkTransform()
47-
affine_vtktransform.SetMatrix(affine_vtkmat)
48-
transform_filter = vtk.vtkTransformPolyDataFilter()
49-
transform_filter.SetTransform(affine_vtktransform)
50-
transform_filter.SetInputData(polydata)
51-
transform_filter.Update()
52-
return transform_filter.GetOutput()
53-
5442
def test_take_largest_connected_component():
5543
vol_array = np.zeros((20,20,20))
5644
add_ball(vol_array, (5,5,5), 4) # ball of radius 4 at (5,5,5)
@@ -84,6 +72,16 @@ def test_vtk_img_from_array_and_affine():
8472

8573
assert vtk_img.GetPointData().GetScalars().GetTuple1(point_id) == pytest.approx(vol_array[i,j,k])
8674

75+
def test_affine_from_vtk_image_data():
76+
rng = np.random.default_rng(716)
77+
vol_array = rng.random((5,4,3))
78+
affine = np.eye(4)
79+
affine[:3,:3] = expm((lambda A: (A - A.T)/2)(rng.normal(size=(3,3)))) # generate a random orthogonal matrix
80+
affine[:3,3] = rng.random(3) # generate a random origin
81+
vtk_img = vtk_img_from_array_and_affine(vol_array, affine)
82+
affine_reconstructed = affine_from_vtk_image_data(vtk_img)
83+
assert np.allclose(affine, affine_reconstructed)
84+
8785
def test_create_closed_surface_from_labelmap():
8886
# create a ball of radius 7 for a labelmap
8987
labelmap = np.zeros((20,20,20))

0 commit comments

Comments
 (0)