Browse Source

Add --diag-log CSV diagnostic for head rotation compensation

Writes per-frame CSV to /tmp/teleop_diag.csv with 73 columns capturing
the full compensation pipeline: head pose, calibration reference,
R_head_delta, raw/compensated wrist positions, IK inputs/outputs, and
robot joint positions. Enabled with --diag-log flag.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
main
Joe DiPrima 1 month ago
parent
commit
e9d37880d3
  1. 69
      teleop/teleop_hand_and_arm.py

69
teleop/teleop_hand_and_arm.py

@ -1,10 +1,12 @@
import time import time
import argparse import argparse
import struct import struct
import csv
import numpy as np import numpy as np
from multiprocessing import Value, Array, Lock from multiprocessing import Value, Array, Lock
import threading import threading
import logging_mp import logging_mp
from scipy.spatial.transform import Rotation as ScipyR
logging_mp.basicConfig(level=logging_mp.INFO) logging_mp.basicConfig(level=logging_mp.INFO)
logger_mp = logging_mp.getLogger(__name__) logger_mp = logging_mp.getLogger(__name__)
@ -95,6 +97,10 @@ def scale_rotation(R, alpha):
rv = rotation_to_rotvec(R) rv = rotation_to_rotvec(R)
return rotvec_to_rotation(rv * alpha) return rotvec_to_rotation(rv * alpha)
def mat_to_euler(R):
"""Extract yaw, pitch, roll in degrees from a 3x3 rotation matrix."""
return ScipyR.from_matrix(R).as_euler('ZYX', degrees=True)
class ThreadedWebcam: class ThreadedWebcam:
"""Non-blocking webcam reader. Captures frames in a background thread """Non-blocking webcam reader. Captures frames in a background thread
so cv2.VideoCapture.read() never blocks the control loop.""" so cv2.VideoCapture.read() never blocks the control loop."""
@ -202,6 +208,8 @@ if __name__ == '__main__':
help='Rotation tracking blend (0=position-only, 1=full rotation, default: 0.7)') help='Rotation tracking blend (0=position-only, 1=full rotation, default: 0.7)')
parser.add_argument('--head-rot-comp', type=float, default=1.0, parser.add_argument('--head-rot-comp', type=float, default=1.0,
help='Head rotation compensation blend (0=none, 1=full, default: 1.0)') help='Head rotation compensation blend (0=none, 1=full, default: 1.0)')
parser.add_argument('--diag-log', action='store_true', default=False,
help='Write per-frame diagnostic CSV to /tmp/teleop_diag.csv')
# webcam # webcam
parser.add_argument('--webcam', type=int, default=None, parser.add_argument('--webcam', type=int, default=None,
help='USB webcam device index (e.g. 0 for /dev/video0). Bypasses teleimager.') help='USB webcam device index (e.g. 0 for /dev/video0). Bypasses teleimager.')
@ -468,6 +476,29 @@ if __name__ == '__main__':
logger_mp.info(f"[calibration] Initial reference captured. " logger_mp.info(f"[calibration] Initial reference captured. "
f"Robot L={robot_ref_left[:3,3]}, R={robot_ref_right[:3,3]}") f"Robot L={robot_ref_left[:3,3]}, R={robot_ref_right[:3,3]}")
# Diagnostic CSV logger
diag_file = None
diag_writer = None
diag_frame = 0
if args.diag_log:
diag_file = open('/tmp/teleop_diag.csv', 'w', newline='')
diag_writer = csv.writer(diag_file)
diag_writer.writerow([
'frame', 'time',
'head_x', 'head_y', 'head_z', 'head_yaw', 'head_pitch', 'head_roll',
'cal_head_yaw', 'cal_head_pitch', 'cal_head_roll',
'delta_yaw', 'delta_pitch', 'delta_roll',
'raw_L_x', 'raw_L_y', 'raw_L_z', 'raw_R_x', 'raw_R_y', 'raw_R_z',
'comp_L_x', 'comp_L_y', 'comp_L_z', 'comp_R_x', 'comp_R_y', 'comp_R_z',
'delta_L_x', 'delta_L_y', 'delta_L_z', 'delta_R_x', 'delta_R_y', 'delta_R_z',
'ref_L_x', 'ref_L_y', 'ref_L_z', 'ref_R_x', 'ref_R_y', 'ref_R_z',
'ik_L_x', 'ik_L_y', 'ik_L_z', 'ik_R_x', 'ik_R_y', 'ik_R_z',
'q0','q1','q2','q3','q4','q5','q6','q7','q8','q9','q10','q11','q12','q13',
'sq0','sq1','sq2','sq3','sq4','sq5','sq6','sq7','sq8','sq9','sq10','sq11','sq12','sq13',
'settle_alpha',
])
logger_mp.info("[diag] CSV logging to /tmp/teleop_diag.csv")
# main loop. robot start to follow VR user's motion # main loop. robot start to follow VR user's motion
while not STOP: while not STOP:
start_time = time.time() start_time = time.time()
@ -638,6 +669,7 @@ if __name__ == '__main__':
R_head_delta = tele_data.head_pose[:3, :3] @ head_R_at_cal.T R_head_delta = tele_data.head_pose[:3, :3] @ head_R_at_cal.T
R_comp = scale_rotation(R_head_delta.T, args.head_rot_comp) R_comp = scale_rotation(R_head_delta.T, args.head_rot_comp)
else: else:
R_head_delta = np.eye(3)
R_comp = np.eye(3) R_comp = np.eye(3)
# De-rotate wrist positions, then compute delta from calibration. # De-rotate wrist positions, then compute delta from calibration.
@ -741,6 +773,36 @@ if __name__ == '__main__':
sol_q, sol_tauff = arm_ik.solve_ik(left_wrist_adjusted, right_wrist_adjusted, current_lr_arm_q, current_lr_arm_dq) sol_q, sol_tauff = arm_ik.solve_ik(left_wrist_adjusted, right_wrist_adjusted, current_lr_arm_q, current_lr_arm_dq)
time_ik_end = time.time() time_ik_end = time.time()
logger_mp.debug(f"ik:\t{round(time_ik_end - time_ik_start, 6)}") logger_mp.debug(f"ik:\t{round(time_ik_end - time_ik_start, 6)}")
# Diagnostic CSV: write one row per frame
if diag_writer is not None and calibrated:
he = mat_to_euler(tele_data.head_pose[:3, :3])
ce = mat_to_euler(head_R_at_cal) if head_R_at_cal is not None else [0,0,0]
de = mat_to_euler(R_head_delta) if head_R_at_cal is not None else [0,0,0]
diag_writer.writerow([
diag_frame, f'{now - calibration_time:.3f}',
*[f'{v:.4f}' for v in tele_data.head_pose[:3, 3]],
*[f'{v:.2f}' for v in he],
*[f'{v:.2f}' for v in ce],
*[f'{v:.2f}' for v in de],
*[f'{v:.4f}' for v in tele_data.left_wrist_pose[:3, 3]],
*[f'{v:.4f}' for v in tele_data.right_wrist_pose[:3, 3]],
*[f'{v:.4f}' for v in left_pos_comp],
*[f'{v:.4f}' for v in right_pos_comp],
*[f'{v:.4f}' for v in left_delta_pos],
*[f'{v:.4f}' for v in right_delta_pos],
*[f'{v:.4f}' for v in vp_ref_left[:3, 3]],
*[f'{v:.4f}' for v in vp_ref_right[:3, 3]],
*[f'{v:.4f}' for v in left_wrist_adjusted[:3, 3]],
*[f'{v:.4f}' for v in right_wrist_adjusted[:3, 3]],
*[f'{v:.4f}' for v in current_lr_arm_q],
*[f'{v:.4f}' for v in sol_q],
f'{settle_alpha:.3f}',
])
diag_frame += 1
if diag_frame % 150 == 0:
diag_file.flush()
arm_ctrl.ctrl_dual_arm(sol_q, sol_tauff) arm_ctrl.ctrl_dual_arm(sol_q, sol_tauff)
# record data # record data
@ -927,6 +989,13 @@ if __name__ == '__main__':
except Exception as e: except Exception as e:
logger_mp.error(f"Failed to stop keyboard listener or ipc server: {e}") logger_mp.error(f"Failed to stop keyboard listener or ipc server: {e}")
if diag_file is not None:
try:
diag_file.close()
logger_mp.info(f"[diag] Wrote {diag_frame} frames to /tmp/teleop_diag.csv")
except:
pass
if webcam_cap is not None: if webcam_cap is not None:
try: try:
webcam_cap.release() webcam_cap.release()

Loading…
Cancel
Save