Robot Documentations/Guides

Visualize Targets on Map

This guide shows how to fetch the robot's map and all defined targets, then render them together in a single matplotlib window using Python. When you run the script you will see the occupancy-grid map as the background with every target drawn as a labeled marker on top.

Prerequisites:

  • The robot API is reachable, e.g., http://192.168.1.100:7242
  • Python 3.8+ is installed
  • Install dependencies once: pip install requests matplotlib pillow
#!/usr/bin/env python3
"""
Visualize robot targets on the map.

Requirements:
    pip install requests matplotlib pillow

Usage:
    python visualize_targets.py
    ROBOT_URL=http://192.168.1.100:7242 python visualize_targets.py
"""

import base64
import io
import os
import sys

import matplotlib.patches as mpatches
import matplotlib.pyplot as plt
import requests
from PIL import Image

# ── configuration ────────────────────────────────────────────────────────────
ROBOT_URL = os.getenv("ROBOT_URL", "http://192.168.1.100:7242")
# ─────────────────────────────────────────────────────────────────────────────


def fetch_map() -> dict:
    resp = requests.get(f"{ROBOT_URL}/api/v1/mapping/map", timeout=10)
    resp.raise_for_status()
    return resp.json()


def fetch_targets() -> list:
    resp = requests.get(f"{ROBOT_URL}/api/v1/targets", timeout=10)
    resp.raise_for_status()
    return resp.json()


def world_to_pixel(wx: float, wy: float, origin: dict, resolution: float, height: int):
    """Convert world coordinates (meters) to image pixel coordinates."""
    px = (wx - origin["x"]) / resolution
    py = height - (wy - origin["y"]) / resolution  # flip y because image origin is top-left
    return px, py


def main():
    print(f"Robot URL : {ROBOT_URL}")

    # 1 ── fetch map ──────────────────────────────────────────────────────────
    print("[1/2] Fetching map...")
    try:
        map_data = fetch_map()
    except Exception as exc:
        print(f"  ✗ Could not fetch map: {exc}")
        sys.exit(1)

    resolution  = map_data["resolution"]                            # meters / pixel
    img_height  = map_data["height"]                               # pixels
    origin      = map_data.get("origin", {"x": 0.0, "y": 0.0, "z": 0.0})
    image_bytes = base64.b64decode(map_data["map_png_base64"])
    map_image   = Image.open(io.BytesIO(image_bytes))
    print(f"  ✓ Map loaded  ({map_data['width']} × {img_height} px, {resolution} m/px)")

    # 2 ── fetch targets ──────────────────────────────────────────────────────
    print("[2/2] Fetching targets...")
    try:
        targets = fetch_targets()
    except Exception as exc:
        print(f"  ✗ Could not fetch targets: {exc}")
        sys.exit(1)
    print(f"  ✓ {len(targets)} target(s) found")

    # 3 ── plot ───────────────────────────────────────────────────────────────
    colors = plt.cm.tab10.colors

    DPI = 100
    MAX_INCHES = 22
    MIN_INCHES = 16
    img_w = map_data["width"]

    # derive figure size from actual image pixels, capped to screen-safe range
    aspect = img_height / img_w if img_w else 1
    fw = max(MIN_INCHES, min(img_w / DPI, MAX_INCHES))
    fh = max(MIN_INCHES * aspect, min(fw * aspect, MAX_INCHES))

    fig, ax = plt.subplots(figsize=(fw, fh), dpi=DPI)
    ax.imshow(map_image, cmap="gray", origin="upper")

    # lock axes to image bounds so out-of-range targets don't shrink the map
    ax.set_xlim(0, img_w)
    ax.set_ylim(img_height, 0)  # imshow origin=upper → y increases downward

    ax.set_title(f"Robot Targets on Map  ({len(targets)} targets)", fontsize=14, pad=12)
    ax.set_xlabel("X (pixels)")
    ax.set_ylabel("Y (pixels)")

    for i, target in enumerate(targets):
        wx    = float(target.get("px") or 0)
        wy    = float(target.get("py") or 0)
        name  = target.get("name", "?")
        ttype = target.get("type", "default")

        px, py = world_to_pixel(wx, wy, origin, resolution, img_height)
        color  = colors[i % len(colors)]

        ax.plot(
            px, py,
            "o",
            markersize=10,
            color=color,
            markeredgecolor="white",
            markeredgewidth=1.5,
        )
        ax.annotate(
            f"{name}\n({ttype})",
            xy=(px, py),
            xytext=(8, 8),
            textcoords="offset points",
            fontsize=8,
            color=color,
            bbox=dict(boxstyle="round,pad=0.2", fc="white", alpha=0.7),
        )

    if targets:
        legend_handles = [
            mpatches.Patch(color=colors[i % len(colors)], label=t.get("name", "?"))
            for i, t in enumerate(targets)
        ]
        ax.legend(handles=legend_handles, loc="upper right", fontsize=7, title="Targets")

    plt.tight_layout()
    plt.show()


if __name__ == "__main__":
    main()

What the script does step by step:

  1. Fetch map — calls GET /api/v1/mapping/map, decodes the base64 PNG and reads the resolution (m/px) and origin (world position of the bottom-left corner).
  2. Fetch targets — calls GET /api/v1/targets to get all targets with their world position (px, py in meters).
  3. Convert coordinates — converts each target's world position to pixel coordinates using the map's origin and resolution.
  4. Render — displays the occupancy map as a grayscale background and draws every target as a coloured dot with a name + type label. A legend is shown in the top-right corner.