Hi @becketps,
Yes, your reasoning is correct, and the two recommendations are fully consistent, they just live at different layers.
What’s actually happening
calibrateHandEye() returns a transform in whatever camera frame produced the marker observations. Since Isaac ROS AprilTag publishes poses in zed2i_left_camera_frame_optical, the OpenCV solver gives you T_tcp_optical. That’s the “physically meaningful” calibration result; it’s the one that, when chained with the AprilTag detector output, gives consistent geometry.
The URDF is a separate concern: it describes the physical mounting of the camera, and the only rigid link the ZED driver allows you to attach to is zed2i_camera_link. The driver itself publishes the internal sub-tree camera_link → left_camera_frame → left_camera_optical_frame as static transforms (defined by zed_macro.urdf.xacro). You cannot, and should not, try to override those.
So your conversion is exactly right:
T_tcp_camera_link = T_tcp_optical · inv(T_camera_link_optical)
where T_camera_link_optical is the static transform published by the ZED driver and read with:
ros2 run tf2_ros tf2_echo zed2i_camera_link zed2i_left_camera_frame_optical
That value is constant per model, for the ZED 2i it’s essentially a fixed ~−90° pitch plus a small lateral offset to the left optical center, so you only need to compute it once.
A small sanity check
After you publish your URDF with the converted T_tcp_camera_link, verify the round-trip:
ros2 run tf2_ros tf2_echo wpg_tcp zed2i_left_camera_frame_optical
That should reproduce, within numerical noise, the original T_tcp_optical returned by calibrateHandEye(). If it doesn’t, the conversion has a sign or order issue somewhere.
Closing the loop on the residual error
Coming back to your previous test (post #26), 8 mm to the right and 2 mm too high after all of this is consistent and stable in one direction, which is exactly the signature of an angular residual in the hand-eye calibration, not a frame-conversion mistake. A 0.6–0.7° error around the optical axis projects to ~8 mm at 70 cm, right in the range you’re seeing.
The question I asked in #25 that’s still open: what’s the angular residual reported by the hand-eye solver, and how diverse were the 40 calibration poses in rotation? If they were mostly translations of the TCP with similar orientations, the rotation part of the calibration is essentially underdetermined and you can have several tenths of a degree of error with a very small reprojection number. Re-running with poses that include ≥30° rotations around each TCP axis usually closes the lateral residual.
If you can share the solver’s per-axis residual breakdown (or even just the angular RMS), we can confirm this is where the remaining 8 mm comes from.