Skip to content

Conversation

J4nn1K
Copy link
Contributor

@J4nn1K J4nn1K commented Oct 18, 2025

What this does

When running LeRobot headless (on a NVIDIA Jetson for example), it can be helpful to visualize data on a remote machine. This PR changes two things:

  1. Add two options to TeleoperateConfig and RecordConfig: (1) display_url and (2) display_port. When display_url is set, the Rerun SDK will connects to a remote viewer and stream all the data via gRPC. Read more about Rerun's operating modes here.
  2. Change the way images are logged from uncompressed rr.Image to compressed JPEG via rr.EncodedImage to save space and bandwidth.

How it was tested

Tested buy running multiple teleop and recording trials on a Jetson AGX Thor with data streamed over a VPN to different devices like: Raspberry Pi, Jetson Orin Nano, Macbook.

How to checkout & try?

Start a Rerun Viewer on a remote machine

rerun

Start a teleoperation loop:

lerobot-teleoperate \
    --robot.type=YOUR_ROBOT \
    --robot.port=YOUR_PORT \
    --robot.cameras="main: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}"
    --teleop.type=YOUR_ROBOT \
    --teleop.port=YOUR_PORT \
    --display_data=true \
    --display_url=YOUR_REMOTE_IP

The remote viewer should start displaying the data.

@Copilot Copilot AI review requested due to automatic review settings October 18, 2025 11:53
Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

Adds remote Rerun Viewer support and compresses image logging to reduce bandwidth when running headless.

  • Add display_url and display_port to TeleoperateConfig and RecordConfig and pass them to init_rerun.
  • Switch image logging from rr.Image to JPEG via OpenCV and rr.ImageEncoded.

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
src/lerobot/utils/visualization_utils.py Adds remote connection in init_rerun and JPEG encoding in log_rerun_data.
src/lerobot/scripts/lerobot_teleoperate.py Adds display_url and display_port to TeleoperateConfig and passes them to init_rerun.
src/lerobot/scripts/lerobot_record.py Adds display_url and display_port to RecordConfig and passes them to init_rerun.

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

memory_limit = os.getenv("LEROBOT_RERUN_MEMORY_LIMIT", "10%")
rr.spawn(memory_limit=memory_limit)
if url:
rr.connect_grpc(url=f"rerun+http://{url}:{port}/proxy")
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

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

The Python SDK exposes rr.connect, not rr.connect_grpc; this will raise an AttributeError. Use rr.connect with host:port (e.g., rr.connect(f"{url}:{port}")) rather than a rerun+http URL with /proxy.

Suggested change
rr.connect_grpc(url=f"rerun+http://{url}:{port}/proxy")
rr.connect(f"{url}:{port}")

Copilot uses AI. Check for mistakes.

".jpg", cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), [int(cv2.IMWRITE_JPEG_QUALITY), 50]
)
encoded_image = buffer.tobytes()
rr.log(key, rr.EncodedImage(contents=encoded_image, media_type="image/jpeg"), static=True)
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

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

The logged type should be rr.ImageEncoded, not rr.EncodedImage; rr.EncodedImage does not exist in the Python API and will fail at runtime. Replace with rr.ImageEncoded(contents=..., media_type="image/jpeg").

Suggested change
rr.log(key, rr.EncodedImage(contents=encoded_image, media_type="image/jpeg"), static=True)
rr.log(key, rr.ImageEncoded(contents=encoded_image, media_type="image/jpeg"), static=True)

Copilot uses AI. Check for mistakes.

Comment on lines +82 to +83
_, buffer = cv2.imencode(
".jpg", cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), [int(cv2.IMWRITE_JPEG_QUALITY), 50]
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

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

This path assumes 3-channel RGB input; it will fail or produce incorrect results for 1-channel (grayscale) and 4-channel (RGBA) images mentioned in the docstring. Handle channel count explicitly: no color conversion for single-channel, RGB→BGR for 3-channel, and RGBA→BGR (drop alpha) for 4-channel before JPEG encoding.

Suggested change
_, buffer = cv2.imencode(
".jpg", cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), [int(cv2.IMWRITE_JPEG_QUALITY), 50]
# Handle channel count explicitly for JPEG encoding
if arr.ndim == 3:
if arr.shape[2] == 1:
# Grayscale, no color conversion needed
arr_to_encode = arr
elif arr.shape[2] == 3:
# RGB to BGR
arr_to_encode = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
elif arr.shape[2] == 4:
# RGBA to BGR (drop alpha)
arr_to_encode = cv2.cvtColor(arr[:, :, :3], cv2.COLOR_RGB2BGR)
else:
raise ValueError(f"Unsupported number of channels for image encoding: {arr.shape[2]}")
else:
raise ValueError(f"Expected 3D array for image encoding, got shape {arr.shape}")
_, buffer = cv2.imencode(
".jpg", arr_to_encode, [int(cv2.IMWRITE_JPEG_QUALITY), 50]

Copilot uses AI. Check for mistakes.

Comment on lines +82 to +84
_, buffer = cv2.imencode(
".jpg", cv2.cvtColor(arr, cv2.COLOR_RGB2BGR), [int(cv2.IMWRITE_JPEG_QUALITY), 50]
)
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

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

[nitpick] cv2.imencode expects uint8 data; if arr is float (e.g., [0,1]) or another dtype, encoding may fail or yield incorrect output. Consider converting/scaling to uint8 prior to encoding and checking the boolean return value of cv2.imencode to handle failures.

Copilot uses AI. Check for mistakes.

import os
from typing import Any

import cv2
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

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

[nitpick] Importing OpenCV at module import time forces a heavy optional dependency even when visualization is disabled. Move the import inside log_rerun_data (or behind the JPEG branch) to avoid unnecessary runtime/import errors on setups without OpenCV.

Copilot uses AI. Check for mistakes.

# Display all cameras on screen
display_data: bool = False
# Display data on a remote Rerun server
display_url: str = None
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

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

[nitpick] Type annotation for display_url is inconsistent with the rest of the config (which uses the PEP 604 style, e.g., float | None). For consistency, use str | None for display_url.

Suggested change
display_url: str = None
display_url: str | None = None

Copilot uses AI. Check for mistakes.

# Display all cameras on screen
display_data: bool = False
# Display data on a remote Rerun server
display_url: str = None
Copy link

Copilot AI Oct 18, 2025

Choose a reason for hiding this comment

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

[nitpick] Align the type of display_url with the project's union style by using str | None for consistency with other fields (e.g., teleop_time_s: float | None).

Suggested change
display_url: str = None
display_url: str | None = None

Copilot uses AI. Check for mistakes.

@pkooij pkooij requested a review from imstevenpmwork October 20, 2025 11:35
@pkooij pkooij added the visualization Issues about visual output, graphs, or data visualization label Oct 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

visualization Issues about visual output, graphs, or data visualization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants