Hello image

Open the camera the robot already has, and read frames as they arrive.

cargo run -p vrobots-examples --bin ex03_hello_image
./target/cpp-build/ex03_hello_image
python examples/python/ex03_hello_image.py

Opening a camera

Every vrobot ships with front_left and front_right mounted, at 720p rgba8. Reading images does not start with creating a camera: it starts with attaching to one of those. open_camera opens the iceoryx2 subscriber and touches the simulator not at all. The name, resolution and format are constants, as they are in every example.

From examples/rust/src/bin/ex03_hello_image.rs:

const SYS_ID: u32 = 1; // the multirotor in the test scene
const CAMERA: &str = "front_left"; // every vrobot ships front_left and front_right
const RESOLUTION: &str = "720p";
const FORMAT: &str = "rgba8"; // Unity's native readback -- four channels, NOT rgb8
const FRAMES: u64 = 120; // then exit
const HZ: f64 = 100.0;

fn main() -> Result<(), VrError> {
    // ===== setup =====
    vrobots_sdk::init_logging("info");
    let robot = VirtualRobot::connect(RobotType::Multirotor, Some(SYS_ID))?;

    // open_camera SUBSCRIBES to a camera the robot already has, without mutating
    // the sim. The name, resolution and format must match the publisher exactly
    // -- on iceoryx2 those three strings are the stream identity -- so a mismatch
    // surfaces as VrError::Timeout (ex13 shows that path).
    let cam = robot.open_camera(CAMERA, RESOLUTION, FORMAT)?;
    println!("camera stream: {}", cam.service_name());

    let mut seen = 0u64;
The same in C++ (examples/cpp/ex03_hello_image.cpp)
constexpr std::uint32_t SYS_ID = 1;  // the multirotor in the test scene
constexpr const char* CAMERA = "front_left";  // every vrobot ships front_left and front_right
constexpr const char* RESOLUTION = "720p";
constexpr const char* FORMAT = "rgba8";  // Unity's native readback -- four channels, NOT rgb8
constexpr std::uint64_t FRAMES = 120;    // then exit
constexpr double HZ = 100.0;

int main() {
    try {
        // ===== setup =====
        vrsdk::check_version();
        vrsdk::VirtualRobot robot(vrsdk::RobotType::Multirotor, SYS_ID);
        robot.connect();

        // open_camera SUBSCRIBES to a camera the robot already has, without
        // mutating the sim. The name, resolution and format must match the
        // publisher exactly -- on iceoryx2 those three strings are the stream
        // identity -- so a mismatch surfaces as a timeout (ex13 shows that path).
        vrsdk::CameraStream cam = robot.open_camera(CAMERA, RESOLUTION, FORMAT);
        std::printf("camera stream: %s\n", cam.service_name().c_str());

        std::uint64_t seen = 0;
The same in Python (examples/python/ex03_hello_image.py)
SYS_ID = 1  # the multirotor in the test scene
CAMERA = "front_left"  # every vrobot ships front_left and front_right
RESOLUTION = "720p"
FORMAT = "rgba8"  # Unity's native readback -- four channels, NOT rgb8
FRAMES = 300  # then exit
HZ = 100


def main() -> None:
    # ===== setup =====
    vrsdk.init_logging("info")
    mr = VirtualRobot(RobotType.MULTIROTOR, sys_id=SYS_ID)
    mr.connect()

    # open_camera SUBSCRIBES to a camera the robot already has, without
    # mutating the sim. The name, resolution and format must match the
    # publisher exactly -- on iceoryx2 those three strings are the stream
    # identity -- so a mismatch surfaces as a TIMEOUT (ex13 shows that path).
    cam = mr.open_camera(CAMERA, RESOLUTION, FORMAT)
    print(f"camera stream: {cam.service_name}")

    seen = 0

The printed name is the iceoryx2 service, and it matches a row of vrobots topic list character for character:

camera stream: vrobots/1/i/cam/front_left/720p_rgba8

Two processes can open the same stream and neither disturbs the other, because neither owns the camera. mount_camera is the other verb: it creates a camera of your choosing, and it is what page Lens and mount pose covers. You need it only when the pair the robot ships cannot serve you.

The loop, and what fresh() means

Images and states are two independent streams. The image half of the loop runs once per frame while the state half runs every iteration, and the code says which by branching on fresh().

#![allow(unused)]
fn main() {
    // ===== loop =====
    while seen < FRAMES {
        let s = robot.states();

        // Images are a separate stream with their own timestamps -- never assume
        // they match the state's. Compare t_ns explicitly when fusing.
        if let Some(frame) = cam.fresh() {
            // Some only if new since the last read
            seen += 1;
            println!(
                "Image {} t={:.3} size=({}x{}) seq={} lag_vs_state={:.1} ms",
                frame.camera_name,
                frame.elapsed,
                frame.width,
                frame.height,
                frame.seq,
                (s.t_ns - frame.t_ns) as f64 / 1e6
            );
}
The same in C++ (examples/cpp/ex03_hello_image.cpp)
        // ===== loop =====
        while (seen < FRAMES) {
            const vrsdk::State s = robot.states();

            // A value only if new since the last read.
            if (auto frame = cam.fresh()) {
                ++seen;
                std::printf("Image %s t=%.3f size=(%ux%u) seq=%llu lag_vs_state=%.1f ms\n",
                            frame->camera_name.c_str(), frame->elapsed(), frame->width(),
                            frame->height(),
                            static_cast<unsigned long long>(frame->seq()),
                            static_cast<double>(s.t_ns - frame->t_ns()) / 1e6);
The same in Python (examples/python/ex03_hello_image.py)
    # ===== loop =====
    while seen < FRAMES:
        s = mr.states

        # Images are a separate stream with their own timestamps -- never assume
        # they match the state's. Compare t_ns explicitly when fusing.
        if cam.fresh:
            frame = cam.frame  # metadata for the image we are about to read
            img = cam.image  # numpy (h, w, c) uint8, top-down, RGB(A)
            seen += 1

            print(
                f"Image {frame.camera_name} t={frame.elapsed:.3f} "
                f"size=({frame.width}x{frame.height}) seq={frame.seq} "
                f"lag_vs_state={(s.t_ns - frame.t_ns) / 1e6:.1f} ms"
            )

Rust and C++ ask and receive in one move, so the frame arrives inside an Option that the if unwraps. Python splits it: cam.fresh is a boolean property and cam.image is the numpy array, and reading the image is what consumes the freshness.

State t=0.410 (no new frame)
Image front_left t=0.412 size=(1280x720) seq=17 lag_vs_state=12.4 ms
      sky-ness (B-R) top=+98 bottom=-25 (top-down: sky above ground), fov_y=61.9 deg
State t=0.420 (no new frame)

fresh() returns Some only when a frame has arrived since the last call, and it hands each frame out exactly once. That makes it the right read for work that must not run twice on one image. Its counterpart latest() returns the current frame regardless and does not consume freshness.

The lag_vs_state figure is why the two streams are never paired by the SDK. Frames arrive at the render rate and states at 25 Hz, so no frame belongs to any state. Both t_ns values are on the same clock, so subtracting them is meaningful, and doing that subtraction explicitly is what fusion looks like here.

Gotcha. Camera frames ride iceoryx2 shared memory, so they are same-host only. zenoh will happily reach a simulator on another machine and deliver states, and not one frame will follow. There is no error: open_camera times out after ConnectOptions::camera_timeout, five seconds by default.

Rows in frame.data are row-major and top-down: row 0 is the top of the picture. The wire is bottom-up, following Unity's render order, and the SDK flips while copying. The sky-ness figures are the check on that, measuring blue minus red on the first and last rows: outdoors the top row is sky and the bottom is ground. Channel order is the renderer's own RGBA, never swapped, so OpenCV users convert to BGR at the call site.

Nothing to clean up

There is no unmount at the end, and that is the point of opening rather than mounting. This handle never created a camera, so it has nothing to remove; letting the stream go ends this subscription and front_left keeps rendering and publishing for everyone else.

#![allow(unused)]
fn main() {
    // Nothing to unmount: this handle never created a camera. Dropping the stream
    // ends this subscription only -- front_left keeps rendering and publishing for
    // everyone else.
    let stats = cam.stats();
    println!(
        "{seen} frame(s), received={} decode_errors={} seq_gaps={}",
        stats.received, stats.decode_errors, stats.seq_gaps
    );
    Ok(())
}
}
The same in C++ (examples/cpp/ex03_hello_image.cpp)
        // Nothing to unmount: this handle never created a camera. Letting the
        // stream go ends this subscription only -- front_left keeps rendering and
        // publishing for everyone else.
        const vrsdk_camera_stats_t st = cam.stats();
        std::printf("%llu frame(s), received=%llu decode_errors=%llu seq_gaps=%llu\n",
                    static_cast<unsigned long long>(seen),
                    static_cast<unsigned long long>(st.received),
                    static_cast<unsigned long long>(st.decode_errors),
                    static_cast<unsigned long long>(st.seq_gaps));
        return 0;
The same in Python (examples/python/ex03_hello_image.py)
    # Nothing to unmount: this handle never created a camera. Letting the stream
    # be collected ends this subscription only -- front_left keeps rendering and
    # publishing for everyone else.
    st = cam.stats
    print(
        f"{seen} frame(s), received={st.received} "
        f"decode_errors={st.decode_errors} seq_gaps={st.seq_gaps}"
    )
120 frame(s), received=120 decode_errors=0 seq_gaps=0

That count is FRAMES, so the Python run prints 300 rather than 120: it opens a window and wants more of them. Because nothing was mutated, the loop bound is a convenience rather than a cleanup deadline, and Ctrl-C is as safe an exit as running to the end. The one example that does have a cleanup step is ex17_camera_pose, which mounts a camera of its own.

Next: Hello service

See also: Mount, open and unmount, Freshness, Inside a frame, Showing frames in a window