Skip to content

System Architecture

High-level data flow

graph LR
    subgraph Challenge Platform
        SIM[Simulator]
        ROS[ROS 2 Topics]
    end

    subgraph ai_module Container
        SUB[Subscribers]
        CACHE[LatestCache]
        TICK[2 Hz Timer]
        RESP[Responder]
        PUB[VLMOutputPublisher]
    end

    subgraph perception Container
        PERCS[YOLO-World + SAM 2.1<br/>FastAPI]
    end

    SIM --> ROS
    ROS -->|challenge topics| SUB
    SUB -->|overwrite latest| CACHE
    TICK -->|snapshot| CACHE
    CACHE -->|VLMInput| RESP
    RESP -->|HTTP /detect| PERCS
    PERCS -->|detections + masks| RESP
    RESP -->|VLMOutput| PUB
    PUB -->|ROS publish| ROS
    ROS --> SIM

Container topology

graph TB
    subgraph Docker Compose
        SYS[iros2026_system<br/>Challenge simulator + ROS]
        AI[xiao_hei_ai_module<br/>Python responder + ROS node]
        PERC["xiao_hei_perception<br/>YOLO-World + SAM 2.1<br/>(profile: perception)"]
        GEM["Gemini API<br/>(external, HTTPS)"]
    end

    SYS <-->|ROS 2 DDS<br/>network_mode: host| AI
    AI -.->|HTTP :8001| PERC
    AI -.->|HTTPS| GEM

system and ai_module always start. The perception sidecar is profile-gated — the wrapper maps XIAO_HEI_RESPONDER=perception to --profile perception, so it only runs when a responder needs it. The submission stack (scene_gemini) brings up the sim, the sidecar and our node together via docker/compose_scene_gemini.yml, and reaches Gemini over the network rather than running a local inference server. See Docker setup and Perception Sidecar.

All containers share network_mode: host so ROS 2 DDS discovery and the sidecar HTTP APIs work without port mapping.

Input topics

ROS topic Rate Python type Description
/camera/image ~10 Hz ImageFrame 1920x640 BGR8 panoramic
/registered_scan ~5 Hz LidarScan (x,y,z,intensity) in map frame
/sensor_scan ~5 Hz LidarScan (x,y,z) in sensor frame
/terrain_map ~5 Hz TerrainMap Local 5m traversability
/terrain_map_ext ~5 Hz TerrainMap Extended 20m traversability
/state_estimation 100–200 Hz OdomPose Robot pose in map frame
/challenge_question 1 Hz ChallengeQuestion Natural language question
/way_point_reached continuous Float32 Nav stack distance to current waypoint (exploration only)

Output topics

Question type Python class ROS topic
Numerical NumericalResponse /numerical_response
Object reference ObjectReferenceResponse /selected_object_marker
Instruction following WaypointPathResponse /way_point_with_heading

Module map

src/xiao_hei_vln/
├── messages/      # Pydantic models for all I/O types
├── sync/          # LatestCache — thread-safe sensor buffer
├── adapters/      # ROS 2 subscribers + publishers
├── app/           # rclpy entry point, tick loop, explorer + responder factory
├── exploration/   # Exploration strategies (FrontierExplorer + protocol)
├── logger.py      # VLM tick logger (model-agnostic)
├── image_utils.py # Shared image conversion helpers
├── dummy/         # Reference responder (no GPU)
├── gemini/        # Gemini engine, prompts, offline batch evaluator, tracer
├── scene_gemini/  # Submission responder: exploration + scene graph + Gemini
├── scene/         # Three-level scene graph (Room/Viewpoint/Object) + renderer
├── trajectory/    # Offline coverage-trajectory planner (Task 7)
├── perception/    # PerceptionResponder + HTTP client + lifter + vocabulary
├── evaluator/     # Offline metrics (numerical, object reference)
├── eval_sampler/  # Ground-truth ↔ prediction pairing
└── eval_pipeline/ # End-to-end evaluation CLI

Tick lifecycle

The tick loop runs in two phases. Exploration runs first; the responder only runs once exploration is complete or a question is active.

sequenceDiagram
    participant Timer as 2 Hz Timer
    participant Cache as LatestCache
    participant Exp as Explorer
    participant Resp as Responder
    participant Engine as GeminiEngine
    participant Gemini as Gemini API

    Timer->>Cache: snapshot(tick_id, timestamp)
    Cache-->>Timer: VLMInput

    alt exploration active (no question + not complete)
        Timer->>Exp: update(VLMInput)
        Exp-->>Timer: Waypoint | None
        Timer->>Timer: publisher.publish(WaypointPathResponse)
    else question active or exploration done
        Timer->>Resp: respond(VLMInput)
        Resp->>Resp: build prompts (system + user)
        Resp->>Engine: infer_multimodal(system, user_text, images)
        Engine->>Gemini: generate_content
        Gemini-->>Engine: JSON response
        Engine-->>Resp: VLMOutput
        Resp-->>Timer: VLMOutput
        Timer->>Timer: publisher.publish(output)
    end