Skip to content

Conversation

@tbirdso
Copy link
Contributor

@tbirdso tbirdso commented May 23, 2025

Addresses rows (32) and (33) in I4H v0.2 Execution Plan

Description

Integrates the CloudXR and Isaac teams' OpenXR hand tracking support in Isaac for Healthcare.

  1. Adds support for Isaac Lab's OpenXR-based Se3HandTracking in the Robotic Ultrasound teleoperation workflow sample.
  2. Adds tutorial for using Isaac Lab + CloudXR OpenXR components to run the robotic_ultrasound teleoperation sample with XR hand tracking inputs.

xr_teleop_avp_sim

Notes:

  • Changes are in draft stage. Feedback and suggestions for improvements are welcome.
  • Isaac Lab OpenXR setup is largely informed by the Isaac Lab OpenXR tutorial, streamlined for Isaac for Healthcare and limited to v2.0.2 feature availability and API.
  • Tested locally with Apple Vision Pro.
    • Last tested based on 929ed75. Rebased on main for PR submission.
  • Observed limitations are noted in the tutorial, including:
    • Some XR features present in the v2.1.0 API are missing from the robotic_ultrasound pinned Isaac Lab version v2.0.2 (retargeting), limiting ease of use
    • Observed application error when room camera and XR device are rendering at the same time. External data limited to wrist camera + joint position during teleop

@tbirdso tbirdso requested review from KumoLiu and binliunls May 23, 2025 17:40

Please ensure that your machine meets the [recommended specifications](#mixed-reality-device) for mixed reality teleoperation.

- **Why is Room Camera output not published over DDS?**
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @KumoLiu , @mingxin-zheng , could you please help take a look at this DDS issue?

Thanks.

@Nic-Ma Nic-Ma requested a review from mingxin-zheng May 24, 2025 00:41
KumoLiu and others added 22 commits May 28, 2025 13:53
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Signed-off-by: YunLiu <[email protected]>
Adds support for Isaac Lab's OpenXR-based `Se3HandTracking` in the
Robotic Ultrasound teleoperation workflow sample.

Changes:
- Update sample with hand tracking default properties based on Isaac Lab
  v2.0.2 sample:
https://github.com/isaac-sim/IsaacLab/blob/b5fa0eb031a2413c182eeb54fa3a9295e8fd867c/scripts/environments/teleoperation/teleop_se3_agent.py
- Disable room camera polling to work around observed application crash
  when running XR teleop in the scene

Tested on local network with Apple Vision Pro.
Adds tutorial for using Isaac Lab + CloudXR OpenXR components to run the
`robotic_ultrasound` teleoperation sample with XR hand tracking inputs.

Instructions are ported from the Isaac Lab XR teleoperation tutorial
with specific focus on Isaac for Healthcare specs and integration.
https://isaac-sim.github.io/IsaacLab/main/source/how-to/cloudxr_teleoperation.html#setting-up-cloudxr-teleoperation

Includes discussion on observed limitations and troubleshooting based on
local testing experience.

Tested locally with Apple Vision Pro (headset and sim).
Changes:
- Update to leverage Isaac Lab 2.1.0 API
- Document known limitations and workarounds
@tbirdso tbirdso force-pushed the tbirdsong/xr-teleop-tutorial branch from 20462a4 to 20ba3e0 Compare May 30, 2025 04:10
@tbirdso tbirdso changed the base branch from main to yunl/update-lab-21 May 30, 2025 04:11
@tbirdso tbirdso marked this pull request as ready for review May 30, 2025 04:13
Copy link
Contributor

@KumoLiu KumoLiu left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the update! I've tried the latest code and overall it looks good and works.
There are some remain issues related to rotation and the field of view as displayed on the screen. I've left a few comments inline for your reference. Additionally, after starting the AR feature, the initial pose appears awkward. Do you know how we can adjust it to better display all the assets within the scene?

I haven't had the chance to review the DDS-related aspects yet, but I plan to look into them later.

)
elif args_cli.teleop_device.lower() == "handtracking":
retargeter_device = Se3RelRetargeter(
bound_hand=OpenXRDevice.TrackingTarget.HAND_RIGHT, zero_out_xy_rotation=True
Copy link
Contributor

@KumoLiu KumoLiu May 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting zero_out_xy_rotation=True may prevent the probe from correctly orienting downward if it is not initially pointing in that direction. To address this, I developed a custom Retargeter that primarily utilizes the 6DOF from the palm. This adjustment resulted in significantly improved teleoperation of the probe within these degrees of freedom. Additionally, I observed that the Se3RelRetargeter offers options such as use_wrist_position and use_wrist_rotation. Do you think we might be able to apply this approach here? If you could test these settings—use_wrist_position=True, use_wrist_rotation=True, zero_out_xy_rotation=False—and see how they perform, I’d appreciate it. Otherwise, I'm also available to share the customized retargeter for your evaluation.

I am also happy to help verify when I back office.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @KumoLiu , good point. I've updated the teleop sample to use wrist translation and rotation, in my experience I see only a minor difference in ease of control. However I've also reduced the rotation scale factor from the default value of 10.0 and the probe is now easier to control in 6DOF.

Please try out the latest update, if you are seeing significant improvement with your custom retargeter then please feel free to contribute the custom retargeter, thanks!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, it works better now.
Let's use wrist pos and rot for now.

Also put my customized regarter here if you are interested in, The main difference is that in this regarter, I'm trying to use hand_data.get("palm") pos an rot.

class PalmRelRetargeter(RetargeterBase):
    """Retargets OpenXR palm tracking data to end-effector commands using relative positioning."""
    def __init__(
        self,
        bound_hand: OpenXRDevice.TrackingTarget,
        zero_out_xy_rotation: bool = False,
        delta_pos_scale_factor: float = 10.0,
        delta_rot_scale_factor: float = 10.0,
        alpha_pos: float = 0.5,
        alpha_rot: float = 0.5,
    ):
        """Initialize the palm relative motion retargeter.

        Args:
            bound_hand: The hand to track (OpenXRDevice.TrackingTarget.HAND_LEFT or OpenXRDevice.TrackingTarget.HAND_RIGHT)
            zero_out_xy_rotation: If True, ignore rotations around x and y axes, allowing only z-axis rotation.
            delta_pos_scale_factor: Amplification factor for position changes.
            delta_rot_scale_factor: Amplification factor for rotation changes.
            alpha_pos: Position smoothing parameter (0-1); higher values track input closely, lower values smooth more.
            alpha_rot: Rotation smoothing parameter (0-1); higher values track input closely, lower values smooth more.
        """
        if bound_hand not in [OpenXRDevice.TrackingTarget.HAND_LEFT, OpenXRDevice.TrackingTarget.HAND_RIGHT]:
            raise ValueError(
                "bound_hand must be either OpenXRDevice.TrackingTarget.HAND_LEFT or"
                " OpenXRDevice.TrackingTarget.HAND_RIGHT"
            )
        self.bound_hand = bound_hand

        self._zero_out_xy_rotation = zero_out_xy_rotation
        self._delta_pos_scale_factor = delta_pos_scale_factor
        self._delta_rot_scale_factor = delta_rot_scale_factor
        self._alpha_pos = alpha_pos
        self._alpha_rot = alpha_rot

        self._smoothed_delta_pos = np.zeros(3)
        self._smoothed_delta_rot_vec = np.zeros(3)

        self._position_threshold = 0.002  # Meters
        self._rotation_threshold = 0.02  # Radians

        self._previous_palm_data: np.ndarray | None = None # Stores [px, py, pz, qw, qx, qy, qz]


    def retarget(self, data: dict) -> np.ndarray:
        """Convert palm pose to robot end-effector command.

        Args:
            data: Dictionary mapping tracking targets to joint data.
                  The joint data for "wrist" is expected as [pos_x, pos_y, pos_z, quat_w, quat_x, quat_y, quat_z].

        Returns:
            np.ndarray: 6D array containing position delta (xyz) and rotation delta vector (rx,ry,rz)
                        for the robot end-effector.
        """
        hand_data = data.get(self.bound_hand)
        if hand_data is None:
            return np.zeros(6) # No data for the bound hand

        current_palm_data = hand_data.get("palm")
        if current_palm_data is None:
            # Wrist data not available, return zero command
            return np.zeros(6)

        if self._previous_palm_data is None:
            # First frame with valid wrist data, store and return zero command
            self._previous_palm_data = current_palm_data.copy()
            return np.zeros(6)

        # Extract current pose
        current_pos = current_palm_data[:3]
        # Scipy expects quaternion as [x, y, z, w]
        current_rot_scipy = Rotation.from_quat([current_palm_data[4], current_palm_data[5], current_palm_data[6], current_palm_data[3]])

        # Extract previous pose
        previous_pos = self._previous_palm_data[:3]
        previous_rot_scipy = Rotation.from_quat([self._previous_palm_data[4], self._previous_palm_data[5], self._previous_palm_data[6], self._previous_palm_data[3]])

        # Calculate delta pose
        delta_pos = current_pos - previous_pos
        delta_rot_obj_scipy = current_rot_scipy * previous_rot_scipy.inv()
        delta_rot_vec = delta_rot_obj_scipy.as_rotvec()

        # Store current pose for next iteration
        self._previous_palm_data = current_palm_data.copy()

        # Process deltas for command generation
        command_pos = delta_pos
        command_rot_vec = delta_rot_vec

        if self._zero_out_xy_rotation:
            command_rot_vec[0] = 0.0  # Zero out x-axis rotation
            command_rot_vec[1] = 0.0  # Zero out y-axis rotation

        # Smooth position and rotation
        self._smoothed_delta_pos = self._alpha_pos * command_pos + (1 - self._alpha_pos) * self._smoothed_delta_pos
        self._smoothed_delta_rot_vec = self._alpha_rot * command_rot_vec + (1 - self._alpha_rot) * self._smoothed_delta_rot_vec

        # Apply thresholds to ignore small movements
        if np.linalg.norm(self._smoothed_delta_pos) < self._position_threshold:
            self._smoothed_delta_pos = np.zeros(3)
        if np.linalg.norm(self._smoothed_delta_rot_vec) < self._rotation_threshold:
            self._smoothed_delta_rot_vec = np.zeros(3)

        # Scale to get final command
        final_pos_command = self._smoothed_delta_pos * self._delta_pos_scale_factor
        final_rot_command_vec = self._smoothed_delta_rot_vec * self._delta_rot_scale_factor


        return np.concatenate([final_pos_command, final_rot_command_vec])

Comment on lines +285 to +286
XR_INITIAL_POS = [0.2, 0, -0.2]
XR_INITIAL_ROT = (0.7, 0.0, 0, -0.7)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you clarify how we obtain the initial position and rotation? Does this pertain to the initial position of the headset? I observed that what I see on the Vision Pro does not accurately reflect on IsaacLab. Is this discrepancy expected, or do you have any suggestions on how we can adjust the settings so that what we view through the headset is accurately displayed on the screen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These settings were experimentally chosen based on observation so that the initial headset position is adjacent to the sample workbench. We can adjust them to maximize user comfort. To my knowledge we do not have control over the screen display viewport during AR mode.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my knowledge we do not have control over the screen display viewport during AR mode.

Got it, thanks for the explaination.

delta_pos_4d = torch.matmul(
delta_pos_4d,
torch.tensor(
[[0, 0, 1, 0], [0, 1, 0, 0], [-1, 0, 0, 0], [0, 0, 0, 1]],
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why we need to matmul this transform matrix here? Does it related to the initial state of the probe?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observed discrepancy between the hand tracking reference frame and the probe frame. Without the transformation applied, translation in one hand tracking relative axis moves the probe in a different, unexpected direction along its relative axis.

In the latest update I've included an additional mapping workaround so that wrist rotation deltas are also mapped to the expected probe direction in the probe's relative coordinate frame.

These are experimental workarounds based on observation. I am not certain at the moment why the hand tracking and probe coordinate frames appear to differ.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the update, I update the transform matrix in this PR: #161
Let me know if it make more sense to you. Thanks!

Base automatically changed from yunl/update-lab-21 to main June 3, 2025 01:53
Copy link
Contributor

@mingxin-zheng mingxin-zheng left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR.

Need to merge for rc1 tagging.

If there are any topic unresolved, feel free to open another PR to address.

@mingxin-zheng mingxin-zheng merged commit 6535ebe into main Jun 3, 2025
3 checks passed
@mingxin-zheng mingxin-zheng deleted the tbirdsong/xr-teleop-tutorial branch June 3, 2025 08:14

- **Spawning cameras fails at app startup**

The error below may appear if cameras have not been enabled in your app configurations:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hello @tbirdso,I discovered that this might be a known issue in Isaaclab: link. It seems that the camera and XR cannot function together. I have just joined their thread to see if the issue has been resolved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants