Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 67 additions & 35 deletions OpenLIFUData/OpenLIFUData.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,15 @@
BusyCursor,
)

from OpenLIFULib.guided_mode_util import get_guided_mode_state
from OpenLIFULib.virtual_fit_results import (
clear_virtual_fit_results,
add_virtual_fit_results_from_openlifu_session_format,
)

if TYPE_CHECKING:
import openlifu # This import is deferred at runtime using openlifu_lz, but it is done here for IDE and static analysis purposes
import openlifu.db
from OpenLIFUPrePlanning.OpenLIFUPrePlanning import OpenLIFUPrePlanningWidget

#
# OpenLIFUData
Expand Down Expand Up @@ -1031,8 +1035,13 @@ def updateSessionStatus(self):

# Build the additional info message here; this is status text that conditionally displays.
additional_info_messages : List[str] = []
if session_openlifu.virtual_fit_approval_for_target_id is not None:
additional_info_messages.append(f"Virtual fit approved for \"{session_openlifu.virtual_fit_approval_for_target_id}\"")
approved_vf_targets = self.logic.get_virtual_fit_approvals_in_session()
num_approved = len(approved_vf_targets)
if num_approved > 0:
additional_info_messages.append(
"Virtual fit approved for "
+ (f"{num_approved} targets" if num_approved > 1 else f"target \"{approved_vf_targets[0]}\"")
)
if loaded_session.transducer_tracking_is_approved():
additional_info_messages.append(f"Transducer tracking approved")
self.ui.sessionStatusAdditionalInfoLabel.setText('\n'.join(additional_info_messages))
Expand Down Expand Up @@ -1218,7 +1227,8 @@ def clear_session(self, clean_up_scene:bool = True) -> None:
self.getParameterNode().loaded_solution is not None
and loaded_session.last_generated_solution_id == self.getParameterNode().loaded_solution.solution.solution.id
):
self.clear_solution(clean_up_scene=True)
self.clear_solution(clean_up_scene=True)
clear_virtual_fit_results(session_id = loaded_session.get_session_id(), target_id=None)

def save_session(self) -> None:
"""Save the current session to the openlifu database.
Expand All @@ -1232,15 +1242,25 @@ def save_session(self) -> None:
if not self.validate_session():
raise RuntimeError("Cannot save session because there is no active session, or the active session was invalid.")

parameter_node = self.getParameterNode()
session : SlicerOpenLIFUSession = parameter_node.loaded_session
targets = get_target_candidates() # future TODO: ask the user which targets they want to include in the session
session_openlifu = session.update_underlying_openlifu_session(targets)
parameter_node.loaded_session = session # remember to write the updated session to the parameter node
session_openlifu = self.update_underlying_openlifu_session()

OnConflictOpts : "openlifu.db.database.OnConflictOpts" = openlifu_lz().db.database.OnConflictOpts
self.db.write_session(self._subjects[session_openlifu.subject_id],session_openlifu,on_conflict=OnConflictOpts.OVERWRITE)

def update_underlying_openlifu_session(self) -> "openlifu.db.Session":
"""Update the underlying openlifu session of the currently loaded session, if there is one.
Returns the newly updated openlifu Session object."""
parameter_node = self.getParameterNode()
session : SlicerOpenLIFUSession = parameter_node.loaded_session
if session is not None:
targets = get_target_candidates()
# TODO: I think instead of getting all 1-point fiducial nodes as targets, we should attribute-tag targets with
# the session ID, and have a tool that adds and retrieves targets by session ID similar to what we do for virtual fit results
session_openlifu = session.update_underlying_openlifu_session(targets)
parameter_node.loaded_session = session # remember to write the updated session into the parameter node
return session_openlifu


def validate_session(self) -> bool:
"""Check to ensure that the currently active session is in a valid state, clearing out the session
if it is not and returning whether there is an active valid session.
Expand Down Expand Up @@ -1472,10 +1492,7 @@ def load_session(self, subject_id, session_id) -> None:
# === Load transducer ===

transducer_openlifu = self.db.load_transducer(session_openlifu.transducer_id)
if transducer_openlifu.registration_surface_filename or transducer_openlifu.transducer_body_filename:
transducer_abspaths_info = self.db.get_transducer_absolute_filepaths(session_openlifu.transducer_id)
else:
transducer_abspaths_info = {}
transducer_abspaths_info = self.db.get_transducer_absolute_filepaths(session_openlifu.transducer_id)
newly_loaded_transducer = self.load_transducer_from_openlifu(
transducer = transducer_openlifu,
transducer_abspaths_info = transducer_abspaths_info,
Expand All @@ -1492,6 +1509,25 @@ def load_session(self, subject_id, session_id) -> None:
replace_confirmed = True,
)

# === Load virtual fit results ===

newly_added_vf_result_nodes = add_virtual_fit_results_from_openlifu_session_format(
vf_results_openlifu = session_openlifu.virtual_fit_results,
session_id = session_openlifu.id,
transducer = newly_loaded_transducer.transducer.transducer,
replace=True, # If there happen to already be some virtual fit result nodes that clash, loading a session will silently overwrite them.
)

shNode = slicer.mrmlScene.GetSubjectHierarchyNode()
transducer_parent_folder_id = shNode.GetItemParent(shNode.GetItemByDataNode(newly_loaded_transducer.transform_node))

for vf_node in newly_added_vf_result_nodes:
preplanning_widget : OpenLIFUPrePlanningWidget = slicer.modules.OpenLIFUPrePlanningWidget
preplanning_widget.watchVirtualFit(vf_node)

# Place virtual fit results under the transducer folder
shNode.SetItemParent(shNode.GetItemByDataNode(vf_node), transducer_parent_folder_id)

# === Toggle slice visibility and center slices on first target ===

slices_center_point = new_session.get_initial_center_point()
Expand Down Expand Up @@ -1524,19 +1560,6 @@ def _on_transducer_transform_modified(self, transducer: SlicerOpenLIFUTransducer
session.toggle_transducer_tracking_approval() # revoke approval
self.getParameterNode().loaded_session = session # remember to write the updated session object into the parameter node

# Revoke any possible virtual fit approval if the transducer whose transform was just modified
# belongs to an active session
if (
session.session.session.virtual_fit_approval_for_target_id is not None
and session.get_transducer_id() == transducer.transducer.transducer.id
):
slicer.util.infoDisplay(
text= "Virtual fit approval has been revoked because the transducer was moved.",
windowTitle="Approval revoked"
)
session.approve_virtual_fit_for_target(None) # revoke approval
self.getParameterNode().loaded_session = session # remember to write the updated session object into the parameter node

def load_protocol_from_file(self, filepath:str) -> None:
protocol = openlifu_lz().Protocol.from_file(filepath)
self.load_protocol_from_openlifu(protocol)
Expand Down Expand Up @@ -1570,12 +1593,13 @@ def load_transducer_from_file(self, filepath:str) -> None:
transducer = openlifu_lz().Transducer.from_file(filepath)
transducer_parent_dir = Path(filepath).parent

transducer_abspaths_info = {key: transducer_parent_dir.joinpath(filename)
for key, filename in {
'transducer_body_abspath': transducer.transducer_body_filename,
'registration_surface_abspath': transducer.registration_surface_filename
}.items() if filename
}
transducer_abspaths_info = {
key: transducer_parent_dir.joinpath(filename) if filename else None
for key, filename in [
('transducer_body_abspath', transducer.transducer_body_filename),
('registration_surface_abspath', transducer.registration_surface_filename),
]
}

self.load_transducer_from_openlifu(transducer, transducer_abspaths_info)

Expand Down Expand Up @@ -1803,14 +1827,22 @@ def add_subject_to_database(self, subject_name, subject_id):

self.db.write_subject(newOpenLIFUSubject, on_conflict = openlifu_lz().db.database.OnConflictOpts.OVERWRITE)

def get_virtual_fit_approval_state(self) -> Optional[str]:
"""Get the virtual fit approval state in the current session, i.e. the value of virtual_fit_approval_for_target_id.
def get_virtual_fit_approvals_in_session(self) -> List[str]:
"""Get the virtual fit approval state in the current session object, a list of target IDs for which virtual fit
is approved.
This does not first check whether there is an active session; make sure that one exists before using this.
"""
session = self.getParameterNode().loaded_session
if session is None:
raise RuntimeError("No active session.")
return session.session.session.virtual_fit_approval_for_target_id
session_openlifu : "openlifu.db.Session" = session.session.session
approved_vf_targets = []
for target in session_openlifu.targets:
if target.id not in session_openlifu.virtual_fit_results:
continue
if session_openlifu.virtual_fit_results[target.id][0]:
approved_vf_targets.append(target.id)
return approved_vf_targets

def load_volume_from_openlifu(self, volume_dir: Path, volume_metadata: Dict):
""" Load a volume based on openlifu metadata and check for duplicate volumes in the scene.
Expand Down
2 changes: 2 additions & 0 deletions OpenLIFULib/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ set(MODULE_PYTHON_SCRIPTS
OpenLIFULib/algorithm_input_widget.py
OpenLIFULib/coordinate_system_utils.py
OpenLIFULib/photoscan.py
OpenLIFULib/virtual_fit_results.py
OpenLIFULib//transform_conversion.py
)

set(MODULE_PYTHON_RESOURCES
Expand Down
2 changes: 1 addition & 1 deletion OpenLIFULib/OpenLIFULib/Resources/python-requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
git+https://github.com/OpenwaterHealth/OpenLIFU-python.git@f9f94cf91e8dc18540a6afc2077aa88611cdc16f
git+https://github.com/OpenwaterHealth/OpenLIFU-python.git@0db543a787fc61b59958f8689987a9308c6fe183
6 changes: 5 additions & 1 deletion OpenLIFULib/OpenLIFULib/algorithm_input_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class AlgorithmInput:
combo_box : qt.QComboBox
most_recent_selection : Any = None

def disable_with_tooltip(self, tooltip_message:str) -> None:
self.combo_box.setDisabled(True)
self.combo_box.setToolTip(tooltip_message)

def indicate_no_options(self):
"""Disable and set a message indicating that there are no objects"""
self.combo_box.addItem(f"No {self.name} objects")
Expand All @@ -35,7 +39,7 @@ def __init__(self, algorithm_input_names : List[str], parent=None):
layout = qt.QFormLayout(self)
self.setLayout(layout)

self.inputs_dict = {}
self.inputs_dict : Dict[str,AlgorithmInput] = {}
for input_name in algorithm_input_names:
if input_name not in ["Protocol", "Transducer", "Volume", "Target", "Photoscan"]:
raise ValueError("Invalid algorithm input specified.")
Expand Down
34 changes: 8 additions & 26 deletions OpenLIFULib/OpenLIFULib/session.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from typing import List, TYPE_CHECKING, Optional, Tuple, Dict
from pathlib import Path
import numpy as np
import slicer
from slicer import (
Expand All @@ -14,13 +13,9 @@
from OpenLIFULib.targets import (
openlifu_point_to_fiducial,
fiducial_to_openlifu_point,
fiducial_to_openlifu_point_id,
)
from OpenLIFULib.coordinate_system_utils import (
get_xx2mm_scale_factor,
get_xxx2ras_matrix,
linear_to_affine,
)
from OpenLIFULib.transform_conversion import transform_node_to_openlifu
from OpenLIFULib.virtual_fit_results import get_virtual_fit_results_in_openlifu_session_format

if TYPE_CHECKING:
import openlifu
Expand Down Expand Up @@ -148,7 +143,7 @@ def initialize_from_openlifu_session(

# Load targets
target_nodes = [openlifu_point_to_fiducial(target) for target in session.targets]

return SlicerOpenLIFUSession(SlicerOpenLIFUSessionWrapper(session), volume_node, target_nodes)

def set_affiliated_photoscans(self, affiliated_photoscans : Dict[str, "openlifu.Photoscan"]):
Expand Down Expand Up @@ -180,29 +175,16 @@ def update_underlying_openlifu_session(self, targets : List[vtkMRMLMarkupsFiduci
transducer = get_openlifu_data_parameter_node().loaded_transducers[self.get_transducer_id()]
transducer_openlifu = transducer.transducer.transducer
transducer_transform_node : vtkMRMLTransformNode = transducer.transform_node
transducer_transform_array = slicer.util.arrayFromTransformMatrix(transducer_transform_node, toWorld=True)
openlifu2slicer_matrix = linear_to_affine(
get_xxx2ras_matrix('LPS') * get_xx2mm_scale_factor(transducer_openlifu.units)
)
self.session.session.array_transform = openlifu_lz().db.session.ArrayTransform(
matrix = np.linalg.inv(openlifu2slicer_matrix) @ transducer_transform_array,
self.session.session.array_transform = transform_node_to_openlifu(transducer_transform_node, transducer_openlifu.units)

# Update virtual fit results
self.session.session.virtual_fit_results = get_virtual_fit_results_in_openlifu_session_format(
session_id=self.get_session_id(),
units = transducer_openlifu.units,
)

return self.session.session

def approve_virtual_fit_for_target(self, target : Optional[vtkMRMLMarkupsFiducialNode] = None):
"""Apply approval for the virtual fit of the given target. If no target is provided, then
any existing approval is revoked."""
target_id = None
if target is not None:
target_id = fiducial_to_openlifu_point_id(target)
self.session.session.virtual_fit_approval_for_target_id = target_id # apply the approval or lack thereof

def virtual_fit_is_approved_for_target(self, target : vtkMRMLMarkupsFiducialNode) -> bool:
"""Return whether there is a virtual fit approval for the given target"""
return self.session.session.virtual_fit_approval_for_target_id == fiducial_to_openlifu_point_id(target)

def toggle_transducer_tracking_approval(self) -> None:
"""Approve transducer tracking if it was not approved. Revoke approval if it was approved."""
self.session.session.transducer_tracking_approved = not self.session.session.transducer_tracking_approved
Expand Down
Loading