Skip to content

Commit 5150e24

Browse files
Integrate virtual fitting algorithm (#139)
1 parent ee345e7 commit 5150e24

File tree

6 files changed

+86
-42
lines changed

6 files changed

+86
-42
lines changed

OpenLIFULib/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ set(MODULE_PYTHON_SCRIPTS
1919
OpenLIFULib/photoscan.py
2020
OpenLIFULib/virtual_fit_results.py
2121
OpenLIFULib//transform_conversion.py
22+
OpenLIFULib/skinseg.py
2223
)
2324

2425
set(MODULE_PYTHON_RESOURCES

OpenLIFULib/OpenLIFULib/coordinate_system_utils.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,15 +48,15 @@ def linear_to_affine(matrix, translation=None):
4848
axis=0,
4949
)
5050

51-
def get_RAS2IJK(volume_node: vtkMRMLScalarVolumeNode):
52-
"""Get the _world_ RAS to volume IJK affine matrix for a given volume node.
51+
def get_IJK2RAS(volume_node: vtkMRMLScalarVolumeNode):
52+
"""Get the trasnfrom from IJK to the _world_ RAS for a given volume node.
5353
5454
This takes into account any transforms that the volume node may be subject to.
5555
5656
Returns a numpy array of shape (4,4).
5757
"""
5858
IJK_to_volumeRAS_vtk = vtk.vtkMatrix4x4()
59-
volume_node.GetRASToIJKMatrix(IJK_to_volumeRAS_vtk)
59+
volume_node.GetIJKToRASMatrix(IJK_to_volumeRAS_vtk)
6060
IJK_to_volumeRAS = slicer.util.arrayFromVTKMatrix(IJK_to_volumeRAS_vtk)
6161
if volume_node.GetParentTransformNode():
6262
volumeRAS_to_worldRAS_vtk = vtk.vtkMatrix4x4()

OpenLIFULib/OpenLIFULib/simulation.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from vtk.util import numpy_support
66
import slicer
77
from slicer import vtkMRMLScalarVolumeNode
8-
from OpenLIFULib.coordinate_system_utils import get_RAS2IJK
8+
from OpenLIFULib.coordinate_system_utils import get_IJK2RAS
99
from OpenLIFULib.lazyimport import xarray_lz
1010

1111
if TYPE_CHECKING:
@@ -60,7 +60,7 @@ def make_xarray_in_transducer_coords_from_volume(volume_node:vtkMRMLScalarVolume
6060
# IJK : the volume node's underlying data array indices
6161
ijk2xyz = np.concatenate([np.concatenate([np.diag(spacing),origin.reshape(3,1)], axis=1), np.array([0,0,0,1],dtype=origin.dtype).reshape(1,4)])
6262
xyz2ras = slicer.util.arrayFromTransformMatrix(transducer.transform_node)
63-
ras2IJK = get_RAS2IJK(volume_node)
63+
ras2IJK = np.linalg.inv(get_IJK2RAS(volume_node))
6464
ijk2IJK = ras2IJK @ xyz2ras @ ijk2xyz
6565
volume_resampled_array = affine_transform(
6666
slicer.util.arrayFromVolume(volume_node).transpose((2,1,0)), # the array indices come in KJI rather than IJK so we permute them

OpenLIFULib/OpenLIFULib/skinseg.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Skin segmentation tools useful mainly for debugging"""
2+
3+
from OpenLIFULib.lazyimport import openlifu_lz
4+
from slicer import vtkMRMLScalarVolumeNode, vtkMRMLModelNode
5+
from OpenLIFULib.coordinate_system_utils import get_IJK2RAS
6+
import slicer
7+
8+
def generate_skin_mesh(volume_node:vtkMRMLScalarVolumeNode) -> vtkMRMLModelNode:
9+
volume_array = slicer.util.arrayFromVolume(volume_node).transpose((2,1,0)) # the array indices come in KJI rather than IJK so we permute them
10+
volume_affine_RAS = get_IJK2RAS(volume_node)
11+
foreground_mask_array = openlifu_lz().seg.skinseg.compute_foreground_mask(volume_array)
12+
foreground_mask_vtk_image = openlifu_lz().seg.skinseg.vtk_img_from_array_and_affine(foreground_mask_array, volume_affine_RAS)
13+
skin_mesh = openlifu_lz().seg.skinseg.create_closed_surface_from_labelmap(foreground_mask_vtk_image)
14+
skin_node = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode")
15+
skin_node.SetAndObservePolyData(skin_mesh)
16+
return skin_node

OpenLIFULib/OpenLIFULib/transducer.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,3 +159,11 @@ def observe_transform_modified(self, callback : "Callable[[SlicerOpenLIFUTransdu
159159

160160
def stop_observing_transform_modified(self, tag:int) -> None:
161161
self.transform_node.RemoveObserver(tag)
162+
163+
def set_current_transform_to_match_transform_node(self, transform_node : vtkMRMLTransformNode) -> None:
164+
"""Set the matrix on the current transform node of this transducer to match the matrix of a given transform node.
165+
(This is done by a copy not reference, so it's a one-time update -- the tranforms do not become linked in any way.)"""
166+
167+
transform_matrix = vtk.vtkMatrix4x4()
168+
transform_node.GetMatrixTransformToParent(transform_matrix)
169+
self.transform_node.SetMatrixTransformToParent(transform_matrix)

OpenLIFUPrePlanning/OpenLIFUPrePlanning.py

Lines changed: 56 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import qt
66
import vtk
7+
import numpy as np
78

89
import slicer
910
from slicer.i18n import tr as _
@@ -14,13 +15,14 @@
1415
from slicer import vtkMRMLMarkupsFiducialNode, vtkMRMLScalarVolumeNode, vtkMRMLTransformNode
1516

1617
from OpenLIFULib import (
18+
openlifu_lz,
1719
get_target_candidates,
1820
get_openlifu_data_parameter_node,
1921
OpenLIFUAlgorithmInputWidget,
2022
SlicerOpenLIFUProtocol,
2123
SlicerOpenLIFUTransducer,
2224
)
23-
from OpenLIFULib.util import replace_widget
25+
from OpenLIFULib.util import replace_widget, BusyCursor
2426
from OpenLIFULib.virtual_fit_results import (
2527
add_virtual_fit_result,
2628
clear_virtual_fit_results,
@@ -32,9 +34,13 @@
3234
get_target_id_from_virtual_fit_result_node,
3335
)
3436
from OpenLIFULib.targets import fiducial_to_openlifu_point_id
37+
from OpenLIFULib.coordinate_system_utils import get_IJK2RAS
38+
from OpenLIFULib.transform_conversion import transform_node_from_openlifu
3539

3640
if TYPE_CHECKING:
3741
from OpenLIFUData.OpenLIFUData import OpenLIFUDataLogic
42+
import openlifu
43+
import openlifu.geo
3844

3945
PLACE_INTERACTION_MODE_ENUM_VALUE = slicer.vtkMRMLInteractionNode().Place
4046

@@ -458,19 +464,19 @@ def updateApprovalStatusLabel(self):
458464

459465
def onVirtualfitClicked(self):
460466
activeData = self.algorithm_input_widget.get_current_data()
461-
virtual_fit_result : Optional[vtkMRMLTransformNode] = self.logic.virtual_fit(
462-
activeData["Protocol"],activeData["Transducer"], activeData["Volume"], activeData["Target"]
463-
)
467+
with BusyCursor():
468+
virtual_fit_result : Optional[vtkMRMLTransformNode] = self.logic.virtual_fit(
469+
activeData["Protocol"],activeData["Transducer"], activeData["Volume"], activeData["Target"]
470+
)
464471

465-
if virtual_fit_result is None: # Temporary behavior!
466-
# None indicates for now that the user activated the transform handles to do the placeholder manual virtual fitting.
467-
# It will not be possible to get None once the algorithm is implemented, or at least it wouldn't mean the same thing.
468-
target_id = fiducial_to_openlifu_point_id(activeData["Target"])
469-
self.algorithm_input_widget.inputs_dict["Target"].disable_with_tooltip(f"VF for {target_id} in progress...") # Disable target selector during manual VF
472+
if virtual_fit_result is None:
473+
slicer.util.errorDisplay("Virtual fit failed. No viable transducer positions found.")
470474
return
471-
self.algorithm_input_widget.update() # Re-enable target-selector now that manual VF is completed. Again, temporary behavior.
475+
else:
476+
# TODO: Make the virtual fit button both update the transducer transform and populate in the virtual fit results
477+
activeData["Transducer"].set_current_transform_to_match_transform_node(virtual_fit_result)
478+
self.watchVirtualFit(virtual_fit_result)
472479

473-
self.watchVirtualFit(virtual_fit_result)
474480
self.updateApproveButton()
475481
self.updateApprovalStatusLabel()
476482

@@ -549,39 +555,52 @@ def virtual_fit(
549555
volume: vtkMRMLScalarVolumeNode,
550556
target: vtkMRMLMarkupsFiducialNode,
551557
) -> Optional[vtkMRMLTransformNode]:
552-
# Temporary measure of "manual" virtual fitting. See https://github.com/OpenwaterHealth/SlicerOpenLIFU/issues/153
553-
transducer.transform_node.CreateDefaultDisplayNodes()
554-
if not transducer.transform_node.GetDisplayNode().GetEditorVisibility():
555-
slicer.util.infoDisplay(
556-
text=(
557-
"The automatic virtual fitting algorithm is not yet implemented."
558-
" Use the interaction handles on the transducer to manually fit it."
559-
" You can click the Virtual fit button again to remove the interaction handles,"
560-
" completing the manual virtual fit and recording the virtual fit transform."
561-
),
562-
windowTitle="Not implemented"
563-
)
564-
transducer.transform_node.GetDisplayNode().SetEditorVisibility(True)
565-
return None # we would also return this in the event of failure to do virtual fitting
566-
else:
567-
# "Complete" the virtual fit
568-
transducer.transform_node.GetDisplayNode().SetEditorVisibility(False)
569558

570-
session = get_openlifu_data_parameter_node().loaded_session
571-
session_id : Optional[str] = session.get_session_id() if session is not None else None
559+
# TODO: Many quantities are hard-coded here will not have to be when these two issues are done:
560+
# https://github.com/OpenwaterHealth/OpenLIFU-python/issues/166
561+
# https://github.com/OpenwaterHealth/OpenLIFU-python/issues/165
562+
vf_transforms = openlifu_lz().virtual_fit(
563+
standoff_transform = openlifu_lz().geo.create_standoff_transform(
564+
z_offset = 13.55,
565+
dzdy = 0.15
566+
),
567+
volume_array = slicer.util.arrayFromVolume(volume),
568+
volume_affine_RAS = get_IJK2RAS(volume),
569+
target_RAS = target.GetNthControlPointPosition(0),
570+
pitch_range = (-10,150),
571+
pitch_step = 5,
572+
yaw_range = (-65, 65),
573+
yaw_step = 5,
574+
transducer_steering_center_distance = 50,
575+
steering_limits = (
576+
(-50, 50), # lat
577+
(-50, 50), # ele
578+
(-50, 50), # ax
579+
),
580+
)
581+
# TODO: add log handler for this
572582

573-
target_id = fiducial_to_openlifu_point_id(target)
574-
clear_virtual_fit_results(target_id=target_id,session_id=session_id)
583+
session = get_openlifu_data_parameter_node().loaded_session
584+
session_id : Optional[str] = session.get_session_id() if session is not None else None
575585

576-
# When actually running the real virtual fit algorithm, there will be more virtual fit results to add
577-
# but we would only return the best one.
578-
return add_virtual_fit_result(
579-
transform_node = transducer.transform_node,
586+
target_id = fiducial_to_openlifu_point_id(target)
587+
clear_virtual_fit_results(target_id=target_id,session_id=session_id)
588+
589+
vf_result_nodes = []
590+
591+
for i,vf_transform in zip(range(10), vf_transforms): # We only add the top 10 virtual fit nodes, to not put so many transforms into the scene.
592+
node = add_virtual_fit_result(
593+
transform_node = transform_node_from_openlifu(vf_transform, transducer.transducer.transducer, "mm"),
580594
target_id = target_id,
581595
session_id = session_id,
582596
approval_status = False,
583-
clone_node=True,
597+
clone_node=False,
598+
rank = i+1,
584599
)
600+
vf_result_nodes.append(node)
601+
if len(vf_result_nodes)==0:
602+
return None
603+
return vf_result_nodes[0]
585604

586605
#
587606
# OpenLIFUPrePlanningTest

0 commit comments

Comments
 (0)