mhsnar's picture
Add files using upload-large-folder tool
0ea5594 verified
Raw History Blame Contribute Delete
19.4 kB
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from .arrays import to_np
from .video import save_video
from obstacles import PILLARS, NOVEL_CIRCULAR, NOVEL_PLANAR, CONSTRAINT_SETS
def save_single_trajectory_image(
trajectory, filepath, constraint="", obstacle_margin=0.0, draw_obstacle_margin=False
):
plotter = AvoidingTrajectoryPlotter(
constraint=constraint,
obstacle_margin=obstacle_margin,
draw_obstacle_margin=draw_obstacle_margin,
)
fig, ax = plotter.setup_figure()
plotter.plot_single_trajectory(trajectory, ax, style="multiple_real")
plotter.add_environment_elements(ax)
plotter.apply_legend(ax)
plotter.save_figure(fig, filepath)
def save_trajectory_sequence(
trajectories,
base_path,
folder_name,
constraint="",
obstacle_margin=0.0,
draw_obstacle_margin=False,
):
import os
img_path = os.path.join(base_path, folder_name)
os.makedirs(img_path, exist_ok=True)
for step_i, trajectory in enumerate(trajectories):
imgpath = os.path.join(img_path, f"{step_i}.png")
save_single_trajectory_image(
trajectory, imgpath, constraint, obstacle_margin, draw_obstacle_margin
)
def save_prediction_overlay_plot(
predicted_trajectories,
real_trajectory,
filepath,
title=None,
constraint="",
obstacle_margin=0.0,
draw_obstacle_margin=False,
):
plotter = AvoidingTrajectoryPlotter(
constraint=constraint,
obstacle_margin=obstacle_margin,
draw_obstacle_margin=draw_obstacle_margin,
)
fig, ax = plotter.plot_overlaid_predictions_and_real(
predicted_trajectories,
real_trajectory,
title=title,
)
plotter.save_figure(fig, filepath)
def plot2img(fig, remove_margins=True):
from matplotlib.backends.backend_agg import FigureCanvasAgg
if remove_margins:
fig.subplots_adjust(left=0, bottom=0, right=1, top=1, wspace=0, hspace=0)
canvas = FigureCanvasAgg(fig)
canvas.draw()
img_as_string, (width, height) = canvas.print_to_buffer()
return np.fromstring(img_as_string, dtype="uint8").reshape((height, width, 4))
class AvoidingRenderer:
def __init__(
self,
env_name,
env,
constraint="",
obstacle_margin=0.0,
draw_obstacle_margin=False,
):
self.env_name = env_name
self.env = env
self.constraint = constraint
self.obstacle_margin = obstacle_margin
self.draw_obstacle_margin = draw_obstacle_margin
def renders(self, observations):
plotter = AvoidingTrajectoryPlotter(
constraint=self.constraint,
obstacle_margin=self.obstacle_margin,
draw_obstacle_margin=self.draw_obstacle_margin,
)
fig, ax = plotter.setup_figure()
plotter.plot_single_trajectory(observations, ax, style="multiple_real")
plotter.add_environment_elements(ax)
plotter.apply_legend(ax)
img = plot2img(fig, remove_margins=False)
plt.close(fig)
return img
def render_diffusion(self, savepath, diffusion_path, **video_kwargs):
diffusion_path = to_np(diffusion_path)
n_diffusion_steps = diffusion_path.shape[0]
frame = []
for t in range(n_diffusion_steps):
print(f"[ utils/renderer ] Diffusion: {t} / {n_diffusion_steps}")
img = self.renders(diffusion_path[t])
frame.append(img)
frame = np.stack(frame, axis=0)
save_video(savepath, frame, **video_kwargs)
class AvoidingTrajectoryPlotter:
def __init__(self, constraint="", obstacle_margin=0.0, draw_obstacle_margin=False):
self.constraint = constraint
self.obstacle_margin = obstacle_margin
self.draw_obstacle_margin = draw_obstacle_margin
self.ax_limits = [[0.2, 0.8], [-0.3, 0.4]]
self.figsize = (9, 9)
self.fontsize_label = 16
self.fontsize_ticks = 14
self.fontsize_legend = 16
self._legend_flags = {
"target": False,
"obstacle": False,
"start": False,
"end": False,
}
def setup_figure(self):
fig, ax = plt.subplots(figsize=self.figsize)
ax.set_xlim(self.ax_limits[0])
ax.set_ylim(self.ax_limits[1])
ax.set_facecolor([1, 1, 0.9])
ax.set_aspect("equal", adjustable="box")
fig.subplots_adjust(left=0.06, right=0.985, bottom=0.06, top=0.985)
ax.xaxis.labelpad = 2
ax.yaxis.labelpad = 2
ax.tick_params(axis="both", which="major", pad=1, labelsize=self.fontsize_ticks)
ax.set_xlabel("X Position", fontsize=self.fontsize_label)
ax.set_ylabel("Y Position", fontsize=self.fontsize_label)
ax.grid(True, alpha=0.3)
return fig, ax
def add_environment_elements(self, ax):
target_color = [0.4, 1, 0.4]
x_min, x_max = self.ax_limits[0]
y_min_target = 0.35
y_max = self.ax_limits[1][1]
if y_max > y_min_target:
target_patch = patches.Rectangle(
(x_min, y_min_target),
x_max - x_min,
y_max - y_min_target,
facecolor=target_color,
alpha=0.35,
label="Target" if not self._legend_flags["target"] else "",
zorder=0.5,
)
ax.add_patch(target_patch)
self._legend_flags["target"] = True
polytopic_constraints = self._get_polytopic_constraints(ax)
self._add_polytopic_constraints(ax, polytopic_constraints)
self._add_target_circles(ax)
def _get_polytopic_constraints(self, ax):
if self.constraint == "":
return []
elif self.constraint not in CONSTRAINT_SETS:
raise ValueError(f"Unsupported constraint: {self.constraint}")
for obs in NOVEL_CIRCULAR:
center = list(obs.center)
ax.add_patch(
patches.Circle(
center,
obs.radius,
color="b",
alpha=0.3,
label="Obstacle" if not self._legend_flags["obstacle"] else "",
)
)
self._legend_flags["obstacle"] = True
if self.obstacle_margin > 0 and self.draw_obstacle_margin:
self._add_margin_ring(ax, center, obs.radius, self.obstacle_margin, "b")
return [
[list(obs.point_a), list(obs.point_b), "below"]
for obs in NOVEL_PLANAR
]
def _add_polytopic_constraints(self, ax, polytopic_constraints):
if self.constraint == "":
return
for i, constraint in enumerate(polytopic_constraints):
mat = np.vstack((constraint[:2], np.zeros(2)))
slope = (constraint[1][1] - constraint[0][1]) / (
constraint[1][0] - constraint[0][0]
)
if slope > 0 and constraint[2] == "below":
mat[2] = np.array([self.ax_limits[0][0], self.ax_limits[1][1]])
elif slope < 0 and constraint[2] == "below":
mat[2] = np.array([self.ax_limits[0][1], self.ax_limits[1][1]])
elif slope > 0 and constraint[2] == "above":
mat[2] = np.array([self.ax_limits[0][1], self.ax_limits[1][0]])
elif slope < 0 and constraint[2] == "above":
mat[2] = np.array([self.ax_limits[0][0], self.ax_limits[1][0]])
clipped = self._clip_polygon_below_y(mat, 0.35)
if len(clipped) >= 3:
ax.add_patch(
patches.Polygon(
np.array(clipped),
color="b",
alpha=0.2,
label="Obstacle" if not self._legend_flags["obstacle"] else "",
)
)
self._legend_flags["obstacle"] = True
if self.obstacle_margin > 0 and self.draw_obstacle_margin:
self._add_margin_parallelogram(ax, constraint, "b")
def _add_target_circles(self, ax):
for idx, pillar in enumerate(PILLARS):
center = list(pillar.center)
radius = pillar.radius
ax.add_patch(
patches.Circle(
center,
radius,
color="r",
alpha=0.7,
label="Obstacle" if not self._legend_flags["obstacle"] else "",
)
)
self._legend_flags["obstacle"] = True
if self.obstacle_margin > 0 and self.draw_obstacle_margin:
if not self._is_pillar_covered_by_obstacle(center):
self._add_margin_ring(ax, center, radius, self.obstacle_margin, "r")
def _is_pillar_covered_by_obstacle(self, pillar_center):
if self.constraint not in CONSTRAINT_SETS:
return False
for obs in NOVEL_CIRCULAR:
distance = np.sqrt(
(pillar_center[0] - obs.center[0]) ** 2
+ (pillar_center[1] - obs.center[1]) ** 2
)
if distance < obs.radius:
return True
return False
def _add_margin_ring(self, ax, center, inner_radius, margin, color):
outer_radius = inner_radius + margin
outer_circle = patches.Circle(
center, outer_radius, color=color, alpha=0.15, zorder=1
)
ax.add_patch(outer_circle)
border_circle = patches.Circle(
center,
outer_radius,
fill=False,
edgecolor=color,
alpha=0.7,
linewidth=1.5,
linestyle="--",
zorder=3,
)
ax.add_patch(border_circle)
def _add_margin_parallelogram(self, ax, constraint, color):
p1, p2 = constraint[:2]
slope = (p2[1] - p1[1]) / (p2[0] - p1[0])
intercept = p2[1] - slope * p2[0]
normal_x = -slope / np.sqrt(1 + slope**2)
normal_y = 1 / np.sqrt(1 + slope**2)
if constraint[2] == "below":
margin_offset_x = -normal_x * self.obstacle_margin
margin_offset_y = -normal_y * self.obstacle_margin
else:
margin_offset_x = normal_x * self.obstacle_margin
margin_offset_y = normal_y * self.obstacle_margin
original_mat = np.vstack((constraint[:2], np.zeros(2)))
if slope > 0 and constraint[2] == "below":
original_mat[2] = np.array([self.ax_limits[0][0], self.ax_limits[1][1]])
elif slope < 0 and constraint[2] == "below":
original_mat[2] = np.array([self.ax_limits[0][1], self.ax_limits[1][1]])
elif slope > 0 and constraint[2] == "above":
original_mat[2] = np.array([self.ax_limits[0][1], self.ax_limits[1][0]])
elif slope < 0 and constraint[2] == "above":
original_mat[2] = np.array([self.ax_limits[0][0], self.ax_limits[1][0]])
margin_mat = original_mat.copy()
margin_mat[0] += [margin_offset_x, margin_offset_y]
margin_mat[1] += [margin_offset_x, margin_offset_y]
margin_vertices = np.vstack([original_mat[:2], margin_mat[:2][::-1]])
ax.add_patch(
patches.Polygon(
margin_vertices,
color=color,
alpha=0.1,
)
)
def _clip_polygon_below_y(self, vertices, y_max):
verts = [np.array(v, dtype=float).tolist() for v in vertices]
if len(verts) == 0:
return []
output = []
prev = verts[-1]
prev_inside = prev[1] <= y_max
for curr in verts:
curr_inside = curr[1] <= y_max
if curr_inside:
if not prev_inside:
denom = curr[1] - prev[1]
if denom != 0:
t = (y_max - prev[1]) / denom
x_int = prev[0] + t * (curr[0] - prev[0])
output.append([x_int, y_max])
output.append(curr)
else:
if prev_inside:
denom = curr[1] - prev[1]
if denom != 0:
t = (y_max - prev[1]) / denom
x_int = prev[0] + t * (curr[0] - prev[0])
output.append([x_int, y_max])
prev, prev_inside = curr, curr_inside
return output
def plot_single_trajectory(
self, trajectory, ax, style="actual", label_prefix="", show_labels=True
):
x_coords = trajectory[:, 2]
y_coords = trajectory[:, 3]
if style == "actual":
ax.plot(
x_coords,
y_coords,
"k-",
linewidth=3,
label=f"{label_prefix}Actual Trajectory" if show_labels else "",
)
ax.plot(
x_coords[0],
y_coords[0],
"go",
markersize=10,
markeredgecolor="darkgreen",
markeredgewidth=1,
label="Start" if not self._legend_flags["start"] else "",
)
self._legend_flags["start"] = True
ax.plot(
x_coords[-1],
y_coords[-1],
"bo",
markersize=10,
markeredgecolor="navy",
markeredgewidth=1,
label="End" if not self._legend_flags["end"] else "",
)
self._legend_flags["end"] = True
if len(x_coords) > 2:
ax.plot(
x_coords[1:-1],
y_coords[1:-1],
"ko",
markersize=3,
alpha=0.7,
)
elif style == "predicted":
ax.plot(
x_coords,
y_coords,
"m-",
linewidth=1.5,
alpha=0.4,
label=f"{label_prefix}Predicted Trajectories" if show_labels else "",
)
ax.plot(
x_coords[0],
y_coords[0],
"ms",
markersize=5,
alpha=0.5,
)
ax.plot(
x_coords[-1],
y_coords[-1],
"m^",
markersize=5,
alpha=0.5,
)
if len(x_coords) > 2:
ax.plot(
x_coords[1:-1],
y_coords[1:-1],
"mx",
markersize=3,
alpha=0.4,
)
elif style == "multiple_real":
ax.plot(
x_coords,
y_coords,
"k-",
linewidth=2,
label=f"{label_prefix}Trajectory" if show_labels else "",
)
ax.plot(
x_coords[0],
y_coords[0],
"go",
markersize=8,
label="Start" if not self._legend_flags["start"] else "",
)
self._legend_flags["start"] = True
ax.plot(
x_coords[-1],
y_coords[-1],
"bo",
markersize=8,
label="End" if not self._legend_flags["end"] else "",
)
self._legend_flags["end"] = True
if len(x_coords) > 2:
ax.plot(
x_coords[1:-1],
y_coords[1:-1],
"co",
markersize=4,
alpha=0.6,
)
def plot_overlaid_predictions_and_real(
self,
predicted_trajectories,
real_trajectory,
title=None,
):
fig, ax = self.setup_figure()
for i, trajectory in enumerate(predicted_trajectories):
self.plot_single_trajectory(
trajectory, ax, style="predicted", show_labels=(i == 0)
)
self.plot_single_trajectory(real_trajectory, ax, style="actual")
if title:
ax.set_title(title, fontsize=self.fontsize_label)
self.add_environment_elements(ax)
self.apply_legend(ax)
return fig, ax
def _trajectory_violates(self, trajectory):
"""Check if any point in the trajectory violates constraints."""
if self.constraint == "":
return False
from obstacles import check_violation
for t in range(len(trajectory)):
if check_violation(trajectory[t], self.constraint) > 0:
return True
return False
def plot_multiple_real_trajectories(self, trajectories, title=None):
fig, ax = self.setup_figure()
for i, trajectory in enumerate(trajectories):
x_coords = trajectory[:, 2]
y_coords = trajectory[:, 3]
violated = self._trajectory_violates(trajectory)
color = "gray" if violated else "k"
ax.plot(
x_coords,
y_coords,
color=color,
linestyle="-",
linewidth=2,
alpha=0.7,
label="Trajectory" if i == 0 else "",
)
ax.plot(
x_coords[0],
y_coords[0],
"go",
markersize=8,
label="Start" if not self._legend_flags["start"] else "",
)
self._legend_flags["start"] = True
ax.plot(
x_coords[-1],
y_coords[-1],
"bo",
markersize=8,
label="End" if not self._legend_flags["end"] else "",
)
self._legend_flags["end"] = True
if len(x_coords) > 2:
ax.plot(x_coords[1:-1], y_coords[1:-1], "co", markersize=4, alpha=0.6)
self.add_environment_elements(ax)
self.apply_legend(ax)
return fig, ax
def save_figure(self, fig, filepath, dpi=150, bbox_inches=None):
fig.savefig(filepath, dpi=dpi, bbox_inches=bbox_inches)
plt.close(fig)
print(f"Saved trajectory plot to: {filepath}")
def apply_legend(self, ax):
from matplotlib.legend_handler import HandlerTuple
handles, labels = ax.get_legend_handles_labels()
new_handles = []
new_labels = []
seen = set()
for h, l in zip(handles, labels):
if not l:
continue
if l.lower() == "obstacle":
continue
if l not in seen:
seen.add(l)
new_handles.append(h)
new_labels.append(l)
purple_proxy = patches.Rectangle((0, 0), 1, 1, facecolor="b", alpha=0.2)
red_proxy = patches.Circle((0.5, 0.5), 0.5, facecolor="r", alpha=0.7)
composite = (purple_proxy, red_proxy)
new_handles.append(composite)
new_labels.append("Obstacle")
ax.legend(
new_handles,
new_labels,
handler_map={tuple: HandlerTuple(ndivide=None)},
loc="lower left",
fontsize=self.fontsize_legend,
)