# YAM — calibration

Live exo-aruco recal + wrist calibration. **Always live, never load from
JSON** (except `T_lfr` which is static and trustworthy after one
calibration session).

## Exo aruco — scene cam pose

The right arm's base plate carries a large aruco board (`YAM_BASE_ONLY_CONFIG`
"larger_coarse_board" in `raiden_fork/third_party/exo_redo/`). Live solve
finds `T_scene_in_right_arm_base` at startup.

```python
import sys
sys.path.insert(0, "raiden_fork/third_party/exo_redo")
from ExoConfigs.yam_exo import YAM_BASE_ONLY_CONFIG
from exo_utils import do_est_aruco_pose, ARUCO_DICT

exo_cfg = YAM_BASE_ONLY_CONFIG
link_cfg = exo_cfg.links["larger_coarse_board"]
aruco_board = exo_cfg.aruco_board_objects["larger_coarse_board"]
board_length = link_cfg.board_length

# K from the live ZED; dist all-zeros (raiden rectifies internally).
T_scene_in_rbase = do_est_aruco_pose(
    bgr, ARUCO_DICT, aruco_board, board_length,
    cameraMatrix=K, distCoeffs=np.zeros(5),
)
```

`do_est_aruco_pose` returns `-1` on detection failure.

Convert to world (= left_arm_base):

```python
T_lfr = np.linalg.inv(json_data["right_base_to_left_base"])  # misnamed key!
T_scene_in_world = T_lfr @ T_scene_in_rbase
```

## Pool-until-N detections

A single `do_est_aruco_pose` call uses one frame and is noisy. Pool 8+
detections, then average / SVD-orthogonalize:

```python
detections = []
t0 = time.time()
last_id = None
while len(detections) < min_detections:
    if time.time() - t0 > max_wait_s:
        break
    scene = env.latest_obs[args.scene_cam_name]   # cached by obs stream
    cur_id = id(scene.image)
    if cur_id == last_id:
        time.sleep(0.02)
        continue
    last_id = cur_id
    T = do_est_aruco_pose(scene.image, ARUCO_DICT, aruco_board, board_length,
                          cameraMatrix=K, distCoeffs=np.zeros(5))
    if T is not -1:
        detections.append(T)

T_avg = average_transforms(detections)   # SVD on R, mean on t
```

Defaults: `min_detections=8`, `max_wait_s=20.0`. Older code used
`hold_s=3` — abandoned because at slow chiral round-trip times you only
get 1-2 detections in 3 s. Pool-until-N is the right pattern.

**Critical**: use `env.latest_obs` (cached by the obs stream), not
`env.get_obs()` (blocking RPC competing with the stream for the lock).
Dedup by `id(scene.image)` because `scene.timestamp` is always `0.0`.

## Scene cam HD1080 — name-conditional patch

Both files in `raiden_fork` need the same conditional (one is used by
the recorder, the other by `rd serve`):

```python
# raiden/cameras/zed.py:57
# raiden/server.py:1213
params.camera_resolution = (
    sl.RESOLUTION.HD1080 if name == "scene_camera"
    else sl.RESOLUTION.HD720
)
```

Wrist cams stay HD720 (ZED Mini, no native HD1080).

K at HD1080 is approximately:

```
fx = fy ≈ 1122.83
cx ≈ 946.81, cy ≈ 571.31
```

(Verify by reading `K` off the live `scene.intrinsics` at startup.)

## Wrist calibration — yellow-button state machine

Run on russet with leader-arm teleop active:

```bash
cd ~/cameron/raiden_fork && rd calibrate-wrist
```

Yellow button flow (see also `memory.md`):

1. **#1 FREEZE**: stop teleop, start accumulating aruco detections of
   the wrist board (held in the gripper view).
2. **#2 UNFREEZE**: resume teleop; reposition for a second viewpoint.
3. **#3 FREEZE 2**: snapshot wrist + scene + state for verification.
4. **#4 UNFREEZE**: teleop to a safe end pose.
5. **#5 END**: solve `T_ee_to_cam` from FREEZE 1's accumulated history.

Writes to `calibration_results.json` next to the script. **The named
key `right_base_to_left_base` is misnamed** — see `memory.md`.

## `wrist_verify_rerun.py` — visual sanity check

Always run this after a fresh calibration:

```bash
cd ~/yam_para/yam_control && python ../raiden_fork/raiden/calibration/wrist_verify_rerun.py
```

Opens a rerun viewer showing the wrist cam's projection of known
fiducial points overlaid on the live frame. Misalignment > a few px =
re-calibrate.

Has an always-live scene-cam recal at startup (no JSON load) — same
pattern as deploy.

## Wrist cam pose capture (for joint-state mapping)

For collecting wrist cam pose at specific joint states (used in arm
self-localization debugging):

```bash
rd capture-wrist-pose
```

Press YELLOW when the wrist board is visible to capture the current joint
state + decoded wrist pose. Writes to a JSON next to the script.

## `T_lfr` — the static frame

`T_left_from_right` rarely changes (only on physical rig reconfiguration).
After a fresh exo calibration session, snapshot it to a known PKL and
reuse for inference + dataset:

```python
T_lfr_path = "~/yam_para/data/place_mug_wrist_cam/0000/lowdim/0000000000.pkl"
with open(T_lfr_path, "rb") as f:
    fd = pickle.load(f)
T_lfr = fd["T_left_from_right"]
```

Sanity check: `T_lfr[:3, 3]` should be ~`[0, -0.6, 0]` meters (right arm
is ~60 cm to the +Y of left arm).

## Workspace mask sanity

After loading the extrinsic at deploy startup, the workspace mask should
report ~92k / 100k valid voxels. Far fewer (e.g. 30k) means the
extrinsic is wildly wrong — usually a missed `T_lfr` application or a
mis-stored extrinsic. Investigate before continuing.
