trace_visualizer / trajectory_viz.py
Anthony Liang
add prediction app and script for running inference on trained model
7c21061
"""
Trajectory Visualization Utilities for Trace Model
Extracts trajectory coordinates from model output text and overlays them on images.
Supports both pixel coordinates and normalized (0-1) coordinates.
"""
import os
import re
from typing import List, Tuple, Optional, Union
import numpy as np
from PIL import Image, ImageDraw
def extract_trajectory_from_text(text: str) -> List[List[float]]:
"""
Extract trajectory coordinates from model output text.
Handles both pixel coordinates [[100, 200], [150, 250]] and
normalized coordinates [[0.5, 0.3], [0.7, 0.4]].
Args:
text: The text output from the model containing trajectory information
Returns:
List of [x, y] coordinate pairs as floats
"""
# Look for coordinate pairs [x, y] - supports ints and floats
coord_pattern = r"\[\s*(-?\d+(?:\.\d+)?)\s*,\s*(-?\d+(?:\.\d+)?)\s*\]"
coord_matches = re.findall(coord_pattern, text)
if not coord_matches:
return []
trajectory = []
for x_str, y_str in coord_matches:
try:
x = float(x_str.strip())
y = float(y_str.strip())
trajectory.append([x, y])
except (ValueError, IndexError):
continue
return trajectory
def _to_pixel_coords(
trajectory: List[List[float]],
img_width: int,
img_height: int,
normalized: bool = True,
) -> List[List[int]]:
"""Convert trajectory to pixel coordinates."""
pixel_traj = []
for x, y in trajectory:
if normalized:
x = int(x * img_width)
y = int(y * img_height)
else:
x, y = int(x), int(y)
pixel_traj.append([x, y])
return pixel_traj
def visualize_trajectory_on_image(
trajectory: List[List[float]],
image_path: Optional[str] = None,
output_path: Optional[str] = None,
pil_image: Optional[Image.Image] = None,
normalized: bool = True,
start_color: Tuple[int, int, int] = (0, 255, 0),
end_color: Tuple[int, int, int] = (255, 0, 0),
line_thickness: int = 4,
) -> Optional[np.ndarray]:
"""
Overlay trajectory on an image with gradient coloring (green start -> red end).
Args:
trajectory: List of [x, y] coordinate pairs (pixel or normalized)
image_path: Path to input image (used if pil_image is None)
output_path: Where to save the output image
pil_image: PIL Image to draw on (overrides image_path)
normalized: If True, coordinates are 0-1 and will be scaled to image size
start_color: RGB for trajectory start
end_color: RGB for trajectory end
line_thickness: Line width in pixels
Returns:
numpy array of the output image, or None if trajectory too short
"""
if not trajectory or len(trajectory) < 2:
return None
if pil_image is not None:
img = pil_image.convert("RGB").copy()
elif image_path and os.path.exists(image_path):
img = Image.open(image_path).convert("RGB").copy()
else:
return None
w, h = img.size
pixel_traj = _to_pixel_coords(trajectory, w, h, normalized=normalized)
# Clamp to image bounds
pixel_traj = [
[max(0, min(w - 1, x)), max(0, min(h - 1, y))]
for x, y in pixel_traj
]
draw = ImageDraw.Draw(img)
# Draw gradient line segments
num_segments = len(pixel_traj) - 1
for i in range(num_segments):
progress = i / max(1, num_segments - 1)
r = int(start_color[0] * (1 - progress) + end_color[0] * progress)
g = int(start_color[1] * (1 - progress) + end_color[1] * progress)
b = int(start_color[2] * (1 - progress) + end_color[2] * progress)
segment_color = (r, g, b)
start_pt = tuple(pixel_traj[i])
end_pt = tuple(pixel_traj[i + 1])
draw.line([start_pt, end_pt], fill=segment_color, width=line_thickness)
# Draw start marker
if pixel_traj:
sx, sy = pixel_traj[0]
r = max(3, line_thickness)
bbox = [sx - r, sy - r, sx + r, sy + r]
draw.ellipse(bbox, fill=start_color, outline=(255, 255, 255), width=2)
if output_path:
img.save(output_path)
return np.array(img)