@ -95,6 +95,47 @@ 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 )
class ThreadedWebcam :
""" Non-blocking webcam reader. Captures frames in a background thread
so cv2 . VideoCapture . read ( ) never blocks the control loop . """
def __init__ ( self , device_index , width , height ) :
import cv2
self . cap = cv2 . VideoCapture ( device_index )
if not self . cap . isOpened ( ) :
raise RuntimeError ( f " Cannot open /dev/video{device_index} " )
self . cap . set ( cv2 . CAP_PROP_FRAME_WIDTH , width )
self . cap . set ( cv2 . CAP_PROP_FRAME_HEIGHT , height )
# Minimize internal buffer so we always get the latest frame
self . cap . set ( cv2 . CAP_PROP_BUFFERSIZE , 1 )
self . actual_w = int ( self . cap . get ( cv2 . CAP_PROP_FRAME_WIDTH ) )
self . actual_h = int ( self . cap . get ( cv2 . CAP_PROP_FRAME_HEIGHT ) )
self . _frame = None
self . _lock = threading . Lock ( )
self . _stop = threading . Event ( )
self . _thread = threading . Thread ( target = self . _capture_loop , daemon = True )
self . _thread . start ( )
def _capture_loop ( self ) :
while not self . _stop . is_set ( ) :
ret , frame = self . cap . read ( )
if ret :
with self . _lock :
self . _frame = frame
def read ( self ) :
""" Return (True, frame) or (False, None). Never blocks. """
with self . _lock :
if self . _frame is not None :
frame = self . _frame
self . _frame = None # consume it
return True , frame
return False , None
def release ( self ) :
self . _stop . set ( )
self . _thread . join ( timeout = 2.0 )
self . cap . release ( )
# Previous button state for edge detection
# Previous button state for edge detection
_r3_prev_buttons = { }
_r3_prev_buttons = { }
@ -161,8 +202,10 @@ if __name__ == '__main__':
# 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. ' )
parser . add_argument ( ' --webcam-res ' , type = str , default = ' 720p ' , choices = [ ' 480p ' , ' 720p ' , ' 1080p ' ] ,
help = ' Webcam resolution (default: 720p) ' )
parser . add_argument ( ' --webcam-res ' , type = str , default = ' 480p ' , choices = [ ' 480p ' , ' 720p ' , ' 1080p ' ] ,
help = ' Webcam resolution (default: 480p) ' )
parser . add_argument ( ' --webcam-fps ' , type = int , default = 15 ,
help = ' Max webcam send rate in fps (default: 15, saves bandwidth for arm tracking) ' )
# record mode and task info
# record mode and task info
parser . add_argument ( ' --record ' , action = ' store_true ' , help = ' Enable data recording mode ' )
parser . add_argument ( ' --record ' , action = ' store_true ' , help = ' Enable data recording mode ' )
parser . add_argument ( ' --task-dir ' , type = str , default = ' ./utils/data/ ' , help = ' path to save data ' )
parser . add_argument ( ' --task-dir ' , type = str , default = ' ./utils/data/ ' , help = ' path to save data ' )
@ -210,23 +253,17 @@ if __name__ == '__main__':
# image source: USB webcam or teleimager
# image source: USB webcam or teleimager
webcam_cap = None
webcam_cap = None
img_client = None
img_client = None
webcam_last_send = 0.0
webcam_send_interval = 1.0 / args . webcam_fps # Cap video fps to save bandwidth for DDS
if args . webcam is not None :
if args . webcam is not None :
import cv2 as _cv2
webcam_cap = _cv2 . VideoCapture ( args . webcam )
if not webcam_cap . isOpened ( ) :
logger_mp . error ( f " [webcam] Cannot open /dev/video{args.webcam} " )
exit ( 1 )
res_map = { ' 480p ' : ( 640 , 480 ) , ' 720p ' : ( 1280 , 720 ) , ' 1080p ' : ( 1920 , 1080 ) }
res_map = { ' 480p ' : ( 640 , 480 ) , ' 720p ' : ( 1280 , 720 ) , ' 1080p ' : ( 1920 , 1080 ) }
cam_w , cam_h = res_map [ args . webcam_res ]
cam_w , cam_h = res_map [ args . webcam_res ]
webcam_cap . set ( _cv2 . CAP_PROP_FRAME_WIDTH , cam_w )
webcam_cap . set ( _cv2 . CAP_PROP_FRAME_HEIGHT , cam_h )
actual_w = int ( webcam_cap . get ( _cv2 . CAP_PROP_FRAME_WIDTH ) )
actual_h = int ( webcam_cap . get ( _cv2 . CAP_PROP_FRAME_HEIGHT ) )
logger_mp . info ( f " [webcam] Opened /dev/video{args.webcam} at {actual_w}x{actual_h} " )
webcam_cap = ThreadedWebcam ( args . webcam , cam_w , cam_h )
logger_mp . info ( f " [webcam] Opened /dev/video{args.webcam} at {webcam_cap.actual_w}x{webcam_cap.actual_h} (threaded, {args.webcam_fps}fps cap) " )
camera_config = {
camera_config = {
' head_camera ' : {
' head_camera ' : {
' enable_zmq ' : True , ' enable_webrtc ' : False ,
' enable_zmq ' : True , ' enable_webrtc ' : False ,
' binocular ' : False , ' image_shape ' : ( actual_h , actual_w ) ,
' binocular ' : False , ' image_shape ' : ( webcam_cap . actual_h , webcam_cap . actual_w ) ,
' fps ' : 30 , ' webrtc_port ' : 0 ,
' fps ' : 30 , ' webrtc_port ' : 0 ,
} ,
} ,
' left_wrist_camera ' : { ' enable_zmq ' : False } ,
' left_wrist_camera ' : { ' enable_zmq ' : False } ,
@ -240,12 +277,12 @@ if __name__ == '__main__':
logger_mp . debug ( f " Camera config: {camera_config} " )
logger_mp . debug ( f " Camera config: {camera_config} " )
# televuer_wrapper: obtain hand pose data from the XR device and transmit the robot's head camera image to the XR device.
# televuer_wrapper: obtain hand pose data from the XR device and transmit the robot's head camera image to the XR device.
# Match display_fps to webcam send rate to avoid re-sending stale frames
vuer_display_fps = float ( args . webcam_fps ) if args . webcam is not None else 30.0
tv_wrapper = TeleVuerWrapper ( use_hand_tracking = args . input_mode == " hand " ,
tv_wrapper = TeleVuerWrapper ( use_hand_tracking = args . input_mode == " hand " ,
binocular = camera_config [ ' head_camera ' ] [ ' binocular ' ] ,
binocular = camera_config [ ' head_camera ' ] [ ' binocular ' ] ,
img_shape = camera_config [ ' head_camera ' ] [ ' image_shape ' ] ,
img_shape = camera_config [ ' head_camera ' ] [ ' image_shape ' ] ,
# maybe should decrease fps for better performance?
# https://github.com/unitreerobotics/xr_teleoperate/issues/172
# display_fps=camera_config['head_camera']['fps'] ? args.frequency? 30.0?
display_fps = vuer_display_fps ,
display_mode = args . display_mode ,
display_mode = args . display_mode ,
zmq = camera_config [ ' head_camera ' ] [ ' enable_zmq ' ] ,
zmq = camera_config [ ' head_camera ' ] [ ' enable_zmq ' ] ,
webrtc = camera_config [ ' head_camera ' ] [ ' enable_webrtc ' ] ,
webrtc = camera_config [ ' head_camera ' ] [ ' enable_webrtc ' ] ,
@ -382,10 +419,13 @@ if __name__ == '__main__':
logger_mp . info ( " [R3] A pressed → START tracking " )
logger_mp . info ( " [R3] A pressed → START tracking " )
_r3_prev_buttons = r3_btns
_r3_prev_buttons = r3_btns
if xr_need_local_img :
if xr_need_local_img :
now = time . time ( )
if webcam_cap is not None :
if webcam_cap is not None :
ret , frame = webcam_cap . read ( )
if ret :
tv_wrapper . render_to_xr ( frame )
if now - webcam_last_send > = webcam_send_interval :
ret , frame = webcam_cap . read ( )
if ret :
tv_wrapper . render_to_xr ( frame )
webcam_last_send = now
elif camera_config [ ' head_camera ' ] [ ' enable_zmq ' ] :
elif camera_config [ ' head_camera ' ] [ ' enable_zmq ' ] :
head_img = img_client . get_head_frame ( )
head_img = img_client . get_head_frame ( )
tv_wrapper . render_to_xr ( head_img )
tv_wrapper . render_to_xr ( head_img )
@ -502,12 +542,14 @@ if __name__ == '__main__':
time . sleep ( sleep_time )
time . sleep ( sleep_time )
continue
continue
# get image
# get image (webcam: non-blocking, rate-limited to save bandwidth)
if webcam_cap is not None :
if webcam_cap is not None :
ret , webcam_frame = webcam_cap . read ( )
if ret :
head_img = webcam_frame
tv_wrapper . render_to_xr ( webcam_frame )
if current_time - webcam_last_send > = webcam_send_interval :
ret , webcam_frame = webcam_cap . read ( )
if ret :
head_img = webcam_frame
tv_wrapper . render_to_xr ( webcam_frame )
webcam_last_send = current_time
else :
else :
if camera_config [ ' head_camera ' ] [ ' enable_zmq ' ] :
if camera_config [ ' head_camera ' ] [ ' enable_zmq ' ] :
if args . record or xr_need_local_img :
if args . record or xr_need_local_img :