NuPlan¶
About¶
NuPlan is the world's first large-scale planning benchmark for autonomous driving. The data is recorded over 4 cities - Boston, Pittsburgh, Singapore and Las Vegas. The nuPlan v1.0 dataset consists of over 15,000 logs and 1300+ hours of driving data. The nuPlan v1.1 dataset brings multiple improvements over the v1.0 dataset. Meanwhile, some log files are removed to guarantee the accuracy and reliability of the dataset.
Structure of the Database¶
How to Obtain¶
Citation¶
Data Analysis¶
This part is independently conducted by Tactics2D.
import os
import sys
sys.path.append(".")
import warnings
warnings.filterwarnings("ignore")
import logging
logging.basicConfig(level=logging.WARNING)
import geopandas as gpd
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import sqlite3
import seaborn as sns
# Setting up parameters for matplotlib
mpl.rcParams.update(
{
"figure.dpi": 200, # 200 for high quality
"font.family": "DejaVu Sans Mono",
"font.size": 6,
"font.stretch": "semi-expanded",
"animation.html": "jshtml",
"animation.embed_limit": 5000,
"axes.edgecolor": "black",
"axes.linewidth": 0.8,
"axes.grid": False,
"axes.facecolor": "white",
}
)
sns.set_palette("Set2")
data_path = "../../data/nuplan/data/cache/"
trajectory_folders = ["train_boston", "train_pittsburgh", "train_singapore", "train_vegas_1"]
test_folder = "test"
val_folder = "val"
map_path = "../../data/nuplan/maps"
map_files = [
"us-ma-boston/9.12.1817/map.gpkg",
"us-pa-pittsburgh-hazelwood/9.17.1937/map.gpkg",
"sg-one-north/9.17.1964/map.gpkg",
"us-nv-las-vegas-strip/9.15.1915/map.gpkg",
]
trajectory_files = []
for folder in trajectory_folders:
file_list = os.listdir(data_path + folder)
trajectory_files.append(file_list)
print(folder, len(file_list))
test_file_list = os.listdir(data_path + test_folder)
val_file_list = os.listdir(data_path + val_folder)
print(test_folder, len(test_file_list))
print(val_folder, len(val_file_list))
train_boston 1648 train_pittsburgh 1560 train_singapore 2396 train_vegas_1 1750 test 1349 val 1381
Trajectory Types in NuPlan¶
The dynamic objects recorded in NuPlan include
vehicle: Includes all four or more wheeled vehicles, as well as trailers.bicycle: Includes bicycles, motorcycles and tricycles.pedestrian: All types of pedestrians, incl. strollers and wheelchairs.generic object: Animals, debris, pushable/pullable objects, permanent poles.
The static objects recorded in NuPlan include
traffic_cone: Cones that are temporarily placed to control the flow of traffic.barrier: Solid barriers that can be either temporary or permanent.czone_sign: Temporary signs that indicate construction zones.
sample_db = sqlite3.connect(data_path + trajectory_folders[0] + "/" + trajectory_files[0][0])
df_category = pd.read_sql_query("SELECT * FROM category;", sample_db)
categories = {}
for row in df_category.iterrows():
categories[row[1]["token"]] = row[1]["name"]
print("%s: %s" % (row[1]["name"], row[1]["description"]))
vehicle: Includes all four or more wheeled vehicles, as well as trailers. bicycle: Includes bicycles, motorcycles and tricycles. pedestrian: All types of pedestrians, incl. strollers and wheelchairs. traffic_cone: Cones that are temporarily placed to control the flow of traffic. barrier: Solid barriers that can be either temporary or permanent. czone_sign: Temporary signs that indicate construction zones. generic_object: Animals, debris, pushable/pullable objects, permanent poles.
Scenario Types in NuPlan¶
NuPlan has its own taxonomy in scenarios. There are in total 75 types of scenarios in NuPlan dataset. The scenario types included in train_boston, train_pittsburgh, train_singapore, train_vegas_1 are shown as follows.
scenario_types = set()
for i, trajectory_folder in enumerate(trajectory_folders):
for trajectory_file in trajectory_files[i]:
if trajectory_file[-3:] != ".db":
continue
file_path = data_path + trajectory_folder + "/" + trajectory_file
with sqlite3.connect(data_path + trajectory_folder + "/" + trajectory_file) as db:
df_scenario_tag = pd.read_sql_query("SELECT * FROM scenario_tag;", db)
scenario_type_subset = set(df_scenario_tag["type"].unique())
scenario_types = scenario_types.union(scenario_type_subset)
db.close()
print(len(scenario_types), sorted(scenario_types))
73 ['accelerating_at_crosswalk', 'accelerating_at_stop_sign', 'accelerating_at_stop_sign_no_crosswalk', 'accelerating_at_traffic_light', 'accelerating_at_traffic_light_with_lead', 'accelerating_at_traffic_light_without_lead', 'behind_bike', 'behind_long_vehicle', 'behind_pedestrian_on_driveable', 'behind_pedestrian_on_pickup_dropoff', 'changing_lane', 'changing_lane_to_left', 'changing_lane_to_right', 'changing_lane_with_lead', 'changing_lane_with_trail', 'crossed_by_bike', 'crossed_by_vehicle', 'following_lane_with_lead', 'following_lane_with_slow_lead', 'following_lane_without_lead', 'high_lateral_acceleration', 'high_magnitude_jerk', 'high_magnitude_speed', 'low_magnitude_speed', 'medium_magnitude_speed', 'near_barrier_on_driveable', 'near_construction_zone_sign', 'near_high_speed_vehicle', 'near_long_vehicle', 'near_multiple_bikes', 'near_multiple_pedestrians', 'near_multiple_vehicles', 'near_pedestrian_at_pickup_dropoff', 'near_pedestrian_on_crosswalk', 'near_pedestrian_on_crosswalk_with_ego', 'near_trafficcone_on_driveable', 'on_all_way_stop_intersection', 'on_carpark', 'on_intersection', 'on_pickup_dropoff', 'on_stopline_crosswalk', 'on_stopline_stop_sign', 'on_stopline_traffic_light', 'on_traffic_light_intersection', 'starting_high_speed_turn', 'starting_left_turn', 'starting_low_speed_turn', 'starting_protected_cross_turn', 'starting_protected_noncross_turn', 'starting_right_turn', 'starting_straight_stop_sign_intersection_traversal', 'starting_straight_traffic_light_intersection_traversal', 'starting_u_turn', 'starting_unprotected_cross_turn', 'starting_unprotected_noncross_turn', 'stationary', 'stationary_at_crosswalk', 'stationary_at_traffic_light_with_lead', 'stationary_at_traffic_light_without_lead', 'stationary_in_traffic', 'stopping_at_crosswalk', 'stopping_at_stop_sign_no_crosswalk', 'stopping_at_stop_sign_with_lead', 'stopping_at_stop_sign_without_lead', 'stopping_at_traffic_light_with_lead', 'stopping_at_traffic_light_without_lead', 'stopping_with_lead', 'traversing_crosswalk', 'traversing_intersection', 'traversing_narrow_lane', 'traversing_pickup_dropoff', 'traversing_traffic_light_intersection', 'waiting_for_pedestrian_to_cross']
Map Visualization¶
def plot_map(data_path, map_file):
map_path = data_path + "/" + map_file
fig, ax = plt.subplots()
fig.set_figwidth(6)
generic_drivable_areas = gpd.read_file(map_path, layer="generic_drivable_areas")
generic_drivable_areas.plot(ax=ax, color="lightgreen")
lanes_polygons = gpd.read_file(map_path, layer="lanes_polygons")
lanes_polygons.plot(ax=ax, color="gray")
intersections = gpd.read_file(map_path, layer="intersections")
intersections.plot(ax=ax, color="gray")
walkways = gpd.read_file(map_path, layer="walkways")
walkways.plot(ax=ax, color="lightgray")
carpark_areas = gpd.read_file(map_path, layer="carpark_areas")
carpark_areas.plot(ax=ax, color="lightgray")
stop_polygons = gpd.read_file(map_path, layer="stop_polygons")
stop_polygons.plot(ax=ax, color="red")
crosswalks = gpd.read_file(map_path, layer="crosswalks")
crosswalks.plot(ax=ax, color="lightgray")
traffic_lights = gpd.read_file(map_path, layer="traffic_lights")
traffic_lights.plot(ax=ax, marker="*", color="orange", markersize=0.01)
plt.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
ax.set_title(map_file.split("/")[0])
plt.show()
Proportion of Trajectory Categories¶
In this analysis, our primary emphasis is on scrutinizing the distribution of dynamic objects, specifically focusing on vehicle, bicycle, pedestrian, and generic_object. Given the vast volume of trajectory databases at our disposal, we calculate proportions based on the location-level rather than the file-level. It's noteworthy that the distribution of dynamic objects exhibits significant variability across different locations.
Within the NuPlan dataset, the track data has a especially high proportion of pedestrians. This influx of pedestrian data has the potential to increase complexity to the traffic scenarios, thereby presenting more challenging dynamics for analysis and understanding.
dynamic_category = ["vehicle", "bicycle", "pedestrian", "generic_object"]
all_trajectory_folders = trajectory_folders + [val_folder] + [test_folder]
all_trajectory_files = trajectory_files + [val_file_list] + [test_file_list]
def plot_class_proportion(data_path, trajectory_folders, trajectory_files, proportion=None):
df_proportion = pd.DataFrame(columns=dynamic_category, index=trajectory_folders)
for i, trajectory_folder in enumerate(trajectory_folders):
if proportion is not None:
np.random.shuffle(trajectory_files[i])
trajectory_files_subset = trajectory_files[i][
: int(len(trajectory_files[i]) * proportion)
]
else:
trajectory_files_subset = trajectory_files[i]
for trajectory_file in trajectory_files_subset:
if trajectory_file[-3:] != ".db":
continue
file_path = data_path + trajectory_folder + "/" + trajectory_file
with sqlite3.connect(file_path) as motion_db:
df_category = pd.read_sql_query(
"SELECT * FROM category;", motion_db, index_col="token"
)
df_track = pd.read_sql_query("SELECT * FROM track;", motion_db, index_col="token")
dict_category = dict(zip(df_category["name"], df_category.index))
for category in dynamic_category:
df_proportion.loc[trajectory_folder][category] = len(
df_track[df_track["category_token"] == dict_category[category]]
)
motion_db.close()
if proportion is None:
print(df_proportion)
df_proportion = df_proportion.div(df_proportion.sum(axis=1), axis=0)
df_proportion.plot.barh(stacked=True, colormap="Set2", rot=1)
plt.gcf().set_figwidth(6)
plt.legend(bbox_to_anchor=(1.0, 1.0), loc="upper left")
plot_class_proportion(data_path, all_trajectory_folders, all_trajectory_files)
Distribution of Average Speed on Map¶
def plot_speed_distribution(
map_boundary, data_path, trajectory_folder, trajectory_files, proportion=None
):
x_min, x_max, y_min, y_max = map_boundary
matrix_x = int((x_max - x_min))
matrix_y = int((y_max - y_min))
speed_map = np.zeros((matrix_y, matrix_x, 2))
np.random.shuffle(trajectory_files)
cnt = 0
if proportion is not None:
max_iter = int(len(trajectory_files) * proportion)
for trajectory_file in trajectory_files:
if not trajectory_file.endswith(".db"):
continue
file_path = os.path.join(data_path, trajectory_folder, trajectory_file)
with sqlite3.connect(file_path) as motion_db:
df_category = pd.read_sql_query("SELECT * FROM category;", motion_db, index_col="token")
df_track = pd.read_sql_query("SELECT * FROM track;", motion_db, index_col="token")
df_lidar_box = pd.read_sql_query("SELECT * FROM lidar_box;", motion_db)
# 1. track_token -> category_token
df_lidar_box["category_token"] = df_lidar_box["track_token"].map(df_track["category_token"])
# 2. category_token -> category_name
df_lidar_box["category"] = df_lidar_box["category_token"].map(df_category["name"])
df_vehicle = df_lidar_box[df_lidar_box["category"] == "vehicle"]
df_vehicle["speed"] = np.sqrt(
df_vehicle["vx"] ** 2 + df_vehicle["vy"] ** 2 + df_vehicle["vz"] ** 2
)
df_vehicle["x_idx"] = (df_vehicle["x"] - x_min).astype(int)
df_vehicle["y_idx"] = (df_vehicle["y"] - y_min).astype(int)
valid = (
(df_vehicle["x_idx"] >= 0) & (df_vehicle["x_idx"] < matrix_x) &
(df_vehicle["y_idx"] >= 0) & (df_vehicle["y_idx"] < matrix_y)
)
df_vehicle = df_vehicle[valid]
for (y, x), group in df_vehicle.groupby(["y_idx", "x_idx"]):
speed_map[y, x, 0] += group["speed"].sum()
speed_map[y, x, 1] += len(group)
if proportion is not None:
cnt += 1
if cnt >= max_iter:
break
with np.errstate(divide='ignore', invalid='ignore'):
speed_map[:, :, 0] = np.divide(
speed_map[:, :, 0], speed_map[:, :, 1], out=np.zeros_like(speed_map[:, :, 0]),
where=speed_map[:, :, 1] > 0
)
speed_map[:, :, 0][speed_map[:, :, 1] == 0] = np.nan
speed_map = np.flip(speed_map, axis=0)
im = plt.imshow(speed_map[:, :, 0], cmap="cool", vmin=0)
plt.tick_params(left=False, bottom=False, labelleft=False, labelbottom=False)
plt.gcf().set_figwidth(6)
cax = plt.gcf().add_axes(
[
plt.gca().get_position().x1 + 0.01,
plt.gca().get_position().y0,
0.02,
plt.gca().get_position().height,
]
)
plt.gca().set_title(trajectory_folder)
plt.colorbar(im, cax=cax)
plt.show()
map_boundary_boston = [330500, 333500, 4689100, 4691400]
plot_speed_distribution(
map_boundary_boston, data_path, trajectory_folders[0], trajectory_files[0]
)
map_boundary_pittsburgh = [587000, 589500, 4473750, 4476250]
plot_speed_distribution(
map_boundary_pittsburgh, data_path, trajectory_folders[1], trajectory_files[1]
)
map_boundary_pittsburgh = [364750, 366250, 142700, 144400]
plot_speed_distribution(
map_boundary_pittsburgh, data_path, trajectory_folders[2], trajectory_files[2]
)
map_boundary_pittsburgh = [664000, 667000, 3996000, 4000000]
plot_speed_distribution(
map_boundary_pittsburgh, data_path, trajectory_folders[3], trajectory_files[3], proportion=0.6
)
Distribution of Speed by Trajectory Type¶
The speed distribution indicates that vehicles in the NuPlan dataset are frequently stationary, largely due to the dataset’s focus on urban scenarios with numerous traffic lights. The speeds of vehicles and bicycles exhibit greater variation depending on the location. In contrast, the average walking speed of pedestrians remains relatively consistent across locations, as human walking speed tends not to vary significantly.
def plot_speed_by_type(data_path, trajectory_folders, trajectory_files, max_files_per_folder=100):
speeds = []
for i, trajectory_folder in enumerate(trajectory_folders):
files = trajectory_files[i]
if max_files_per_folder is not None:
np.random.shuffle(files)
files = files[:max_files_per_folder]
for trajectory_file in files:
if not trajectory_file.endswith(".db"):
continue
file_path = os.path.join(data_path, trajectory_folder, trajectory_file)
with sqlite3.connect(file_path) as motion_db:
df_category = pd.read_sql_query("SELECT * FROM category;", motion_db, index_col="token")
df_track = pd.read_sql_query("SELECT * FROM track;", motion_db, index_col="token")
df_lidar_box = pd.read_sql_query("SELECT * FROM lidar_box;", motion_db)
df_lidar_box["category_token"] = df_lidar_box["track_token"].map(df_track["category_token"])
df_lidar_box["class"] = df_lidar_box["category_token"].map(df_category["name"])
df_lidar_box["speed"] = np.sqrt(
df_lidar_box["vx"] ** 2 + df_lidar_box["vy"] ** 2 + df_lidar_box["vz"] ** 2
)
df_lidar_box["location"] = trajectory_folder
speeds.append(df_lidar_box[["location", "class", "speed"]])
df = pd.concat(speeds, ignore_index=True)
plot = sns.FacetGrid(
df,
col="location",
col_wrap=2,
sharex=True,
sharey=False,
hue="class",
palette="Set2",
hue_order=dynamic_category,
)
plot.map(sns.histplot, "speed", stat="percent", element="step", kde=False, bins=50)
plot.set(xlim=(0, 25))
legend_ax = plot.axes.flat[1]
handles, labels = legend_ax.get_legend_handles_labels()
legend_ax.legend(handles, labels, loc="upper right", title="class")
plt.show()
Tactics2D Integration¶
Dataset Preparation¶
You can place the NuPlan dataset in any directory of your choice. However, it is suggested to maintain the original folder structure for compatibility.
nuplan
├── data/cache
│ ├── train_boston
│ │ ├── 2021.08.18.18.32.06_veh-28_00049_00111.db
│ │ ├── ...
│ ├── train_pittsburgh
│ ├── train_singapore
│ ├── train_vegas_1
│ ├── val
│ ├── test
├── maps
│ ├── sg-one-north/9.17.1964
│ │ ├── map.gpkg
│ ├── us-ma-boston/9.12.1817
│ │ ├── map.gpkg
│ ├── us-nv-las-vegas-strip/9.15.1915
│ │ ├── drivable_area.npy.npz
│ │ ├── intensity.npy.npz
│ │ ├── map.gpkg
│ ├── us-pa-pittsburgh-hazelwood/9.17.1937
│ │ ├── map.gpkg
│ ├── nuplan-maps-v1.0.json
Class Mapping¶
The table below illustrates how classes from the highD dataset are mapped to corresponding traffic participants in NuPlan.
| NuPlan Class | Tactics2D Class |
|---|---|
| vehicle | tactics2d.participant.element.Vehicle |
| bicycle | tactics2d.participant.element.Cyclist |
| pedestrian | tactics2d.participant.element.Pedestrian |
| traffic_cone | tactics2d.participant.element.Other |
| barrier | tactics2d.participant.element.Other |
| czone | tactics2d.participant.element.Other |
| generic_object | tactics2d.participant.element.Other |
Map Parsing¶
NuPlan records four locations for its dataset in the form of geopackage file (.gkpg). The map file includes the following layers:
- baseline_paths*
- boundaries
- carpark_areas
- crosswalks
- dubins_nodes*
- generic_drivable_areas*
- gen_lane_connectors_scaled_width_polygons*
- intersections*
- lane_connectors
- lane_group_connectors*
- lane_groups_polygons*
- lanes_polygons
- meta
- road_segments
- stop_polygons*
- traffic_lights
- walkways
To align with Tactics2D's data structure, we use those layers without * for data parsing.
Parse and Replay Logs¶
To parse, replay, and visualize the NuPlan dataset using Tactics2D, you can use the following code snippet.
%matplotlib notebook
import os
import numpy as np
import matplotlib as mpl
try:
from imageio_ffmpeg import get_ffmpeg_exe
except ImportError:
get_ffmpeg_exe = None
from matplotlib.animation import FuncAnimation
from shapely.geometry import Point, Polygon
from IPython.display import HTML
from tactics2d.dataset_parser import NuPlanParser
from tactics2d.sensor import BEVCamera
from tactics2d.renderer import MatplotlibRenderer
from tactics2d.map.map_config import NUPLAN_MAP_CONFIG
from tactics2d.participant.trajectory import State
if get_ffmpeg_exe is not None:
mpl.rcParams["animation.ffmpeg_path"] = get_ffmpeg_exe()
D:\envs\tactics2d\lib\site-packages\sklearn\base.py:380: InconsistentVersionWarning: Trying to unpickle estimator SVC from version 1.3.0 when using version 1.6.1. This might lead to breaking code or invalid results. Use at your own risk. For more info please refer to: https://scikit-learn.org/stable/model_persistence.html#security-maintainability-limitations warnings.warn(
dataset_path = "../../tactics2d/data/trajectory_sample/NuPlan/data/cache" # Replace with your dataset path
map_path = "../../tactics2d/data/map/NuPlan/maps"
dataset_parser = NuPlanParser()
def render_nuplan(
map_,
participants,
time_range,
overlap_threshold=0.6,
max_gap_frames=2
):
x_min = np.inf
x_max = -np.inf
y_min = np.inf
y_max = -np.inf
frame_list = time_range
if isinstance(time_range, tuple):
frame_list = sorted({
frame
for participant in participants.values()
for frame in participant.trajectory.history_states
if time_range[0] <= frame <= time_range[1]
})
frame_index = {frame: index for index, frame in enumerate(frame_list)}
def interpolate_heading(heading_0, heading_1, ratio):
delta = np.arctan2(np.sin(heading_1 - heading_0), np.cos(heading_1 - heading_0))
return float(heading_0 + ratio * delta)
def get_replay_state(participant, frame):
if participant.trajectory.has_state(frame):
return participant.trajectory.get_state(frame)
frames = participant.trajectory.frames
insert_at = np.searchsorted(frames, frame)
if insert_at == 0 or insert_at == len(frames):
return None
prev_frame = frames[insert_at - 1]
next_frame = frames[insert_at]
if prev_frame not in frame_index or next_frame not in frame_index:
return None
missing_frames = frame_index[next_frame] - frame_index[prev_frame] - 1
if missing_frames <= 0 or missing_frames > max_gap_frames:
return None
prev_state = participant.trajectory.get_state(prev_frame)
next_state = participant.trajectory.get_state(next_frame)
ratio = (frame - prev_frame) / (next_frame - prev_frame)
return State(
frame=frame,
x=(1 - ratio) * prev_state.x + ratio * next_state.x,
y=(1 - ratio) * prev_state.y + ratio * next_state.y,
heading=interpolate_heading(prev_state.heading, next_state.heading, ratio),
vx=(1 - ratio) * prev_state.vx + ratio * next_state.vx,
vy=(1 - ratio) * prev_state.vy + ratio * next_state.vy,
)
for participant in participants.values():
trace = np.array(
participant.trajectory.get_trace(frame_range=(frame_list[0], frame_list[-1]))
)
if len(trace) == 0:
continue
x_min = min(x_min, np.min(trace[:, 0]))
x_max = max(x_max, np.max(trace[:, 0]))
y_min = min(y_min, np.min(trace[:, 1]))
y_max = max(y_max, np.max(trace[:, 1]))
x = x_max - x_min
y = y_max - y_min
padding = max(10.0, 0.1 * max(x, y))
x_min -= padding
x_max += padding
y_min -= padding
y_max += padding
x = x_max - x_min
y = y_max - y_min
if x > y:
y_padding = (x - y) / 2
y_min -= y_padding
y_max += y_padding
else:
x_padding = (y - x) / 2
x_min -= x_padding
x_max += x_padding
camera_position = np.array([(x_min + x_max) / 2, (y_min + y_max) / 2])
perception_range = max(x_max - x_min, y_max - y_min) / 2
camera = BEVCamera(id_=0, map_=map_, perception_range=perception_range)
renderer = MatplotlibRenderer(
xlim=(x_min, x_max), ylim=(y_min, y_max), resolution=(1000, 1000), auto_scale=False
)
fig = renderer.fig # Use the one already created
prev_road_id_set = set()
prev_participant_id_set = set()
def get_box_geometry(participant, state):
length = participant.length or getattr(state, "length", None)
width = participant.width or getattr(state, "width", None)
if None in [length, width]:
return None, None
points = np.array([
[0.5 * length, -0.5 * width],
[0.5 * length, 0.5 * width],
[-0.5 * length, 0.5 * width],
[-0.5 * length, -0.5 * width],
])
rotation = np.array([
[np.cos(state.heading), -np.sin(state.heading)],
[np.sin(state.heading), np.cos(state.heading)],
])
polygon = Polygon(points @ rotation.T + np.array(state.location))
return points, polygon
def has_high_overlap(polygon, selected_polygons):
for selected_polygon in selected_polygons:
if not polygon.intersects(selected_polygon):
continue
min_area = min(polygon.area, selected_polygon.area)
if min_area > 0 and polygon.intersection(selected_polygon).area / min_area > overlap_threshold:
return True
return False
def select_participants(frame):
candidates = []
for participant_id, participant in participants.items():
state = get_replay_state(participant, frame)
if state is None:
continue
box_points, polygon = get_box_geometry(participant, state)
track_length = len(participant.trajectory)
candidates.append((track_length, participant_id, participant, state, box_points, polygon))
selected = []
selected_polygons = []
for candidate in sorted(candidates, key=lambda item: (-item[0], item[1])):
polygon = candidate[-1]
if polygon is not None and has_high_overlap(polygon, selected_polygons):
continue
selected.append(candidate)
if polygon is not None:
selected_polygons.append(polygon)
return selected
def build_participant_data(frame):
participant_list = []
participant_id_list = []
for _, participant_id, participant, state, box_points, _ in select_participants(frame):
id_ = abs(int(participant_id))
type_ = participant.__class__.__name__.lower()
if type_ in {"vehicle", "cyclist"} and box_points is not None:
triangle = [
((box_points[0] + box_points[1]) / 2).tolist(),
((box_points[1] + box_points[2]) / 2).tolist(),
((box_points[3] + box_points[0]) / 2).tolist(),
]
participant_list.append({
"id": id_,
"shape": "polygon",
"geometry": box_points.tolist(),
"position": list(state.location),
"rotation": state.heading,
"color": participant.color,
"type": type_,
"line_width": 1,
})
participant_id_list.append(id_)
participant_list.append({
"id": id_ + 0.5,
"shape": "polygon",
"geometry": triangle,
"position": list(state.location),
"rotation": state.heading,
"color": "black",
"type": "heading_arrow",
"line_width": 0,
})
participant_id_list.append(id_ + 0.5)
elif type_ == "pedestrian":
radius = participant.width or getattr(state, "width", None) or 0
participant_list.append({
"id": id_,
"shape": "circle",
"position": list(state.location),
"radius": radius / 2,
"color": participant.color,
"type": type_,
"line_width": 1,
})
participant_id_list.append(id_)
participant_id_set = set(participant_id_list)
return {
"participant_id_to_create": list(participant_id_set - prev_participant_id_set),
"participant_id_to_remove": list(prev_participant_id_set - participant_id_set),
"participants": participant_list,
}, participant_id_set
def update(frame):
nonlocal prev_road_id_set, prev_participant_id_set
geometry_data, prev_road_id_set, _ = camera.update(
frame,
participants,
[],
prev_road_id_set,
set(),
Point(camera_position),
0.0,
)
participant_data, prev_participant_id_set = build_participant_data(frame)
geometry_data["participant_data"] = participant_data
renderer.update(geometry_data)
renderer.ax.set_title(f"Frame: {frame}")
ani = FuncAnimation(fig, update, frames=frame_list, interval=50, repeat=True)
return ani
For visualization, the replay uses NuPlan track-level participant dimensions and fills short internal trajectory gaps for display continuity. Frame-level box metadata is still parsed and kept on each participant trajectory, but it is not used to hide objects in the default replay.
The runnable examples below use the NuPlan mini logs shipped in this repository. For a full NuPlan download, keep the same data/cache and maps layout and replace data_file and the split folder accordingly. The map is parsed through NuPlanParser.parse_map, so the replay uses the common Tactics2D Map, Lane, Area, RoadLine, and Regulatory objects.
By examining extended segments of the NuPlan dataset, several key characteristics become evident:
- The dataset is centered around the data-collecting (ego) vehicle and only includes trajectories of surrounding vehicles within the ego vehicle’s perception range at each time frame.
- Caution is needed when interpreting the perception results. Object positions may fluctuate slightly around their actual locations, and vehicle headings may occasionally be misestimated in some frames.
data_file = "2021.08.09.17.55.59_veh-28_00021_00307.db"
data_path = os.path.join(dataset_path, "mini")
location = dataset_parser.get_location(data_file, data_path)
map_config = NUPLAN_MAP_CONFIG[location]
map_file = map_config["folder"] + "/" + map_config["gpkg_file"]
print(f"Displaying log replay at {location}")
participants, actual_time_range = dataset_parser.parse_trajectory(data_file, data_path)
frame_list = sorted({
frame
for participant in participants.values()
for frame in participant.trajectory.history_states
})
map_ = dataset_parser.parse_map(map_file, map_path)
print(len(map_.lanes), len(map_.areas), len(map_.roadlines), len(map_.regulations))
ani = render_nuplan(map_, participants, frame_list[:80])
HTML(ani.to_html5_video())
Displaying log replay at us-ma-boston
3019 2248 11176 967
data_file = "2021.08.17.16.57.11_veh-08_01200_01636.db"
data_path = os.path.join(dataset_path, "mini")
location = dataset_parser.get_location(data_file, data_path)
map_config = NUPLAN_MAP_CONFIG[location]
map_file = map_config["folder"] + "/" + map_config["gpkg_file"]
print(f"Displaying log replay at {location}")
participants, actual_time_range = dataset_parser.parse_trajectory(data_file, data_path)
frame_list = sorted({
frame
for participant in participants.values()
for frame in participant.trajectory.history_states
})
map_ = dataset_parser.parse_map(map_file, map_path)
print(len(map_.lanes), len(map_.areas), len(map_.roadlines), len(map_.regulations))
ani = render_nuplan(map_, participants, frame_list[:80])
HTML(ani.to_html5_video())
Displaying log replay at us-pa-pittsburgh-hazelwood
962 814 3780 392
data_file = "2021.10.05.07.10.04_veh-52_01442_01802.db"
data_path = os.path.join(dataset_path, "mini")
location = dataset_parser.get_location(data_file, data_path)
map_config = NUPLAN_MAP_CONFIG[location]
map_file = map_config["folder"] + "/" + map_config["gpkg_file"]
print(f"Displaying log replay at {location}")
participants, actual_time_range = dataset_parser.parse_trajectory(data_file, data_path)
frame_list = sorted({
frame
for participant in participants.values()
for frame in participant.trajectory.history_states
})
map_ = dataset_parser.parse_map(map_file, map_path)
print(len(map_.lanes), len(map_.areas), len(map_.roadlines), len(map_.regulations))
ani = render_nuplan(map_, participants, frame_list[200:280])
HTML(ani.to_html5_video())
Displaying log replay at sg-one-north
2736 1826 9525 673
data_file = "2021.05.12.22.00.38_veh-35_01008_01518.db"
data_path = os.path.join(dataset_path, "mini")
location = dataset_parser.get_location(data_file, data_path)
map_config = NUPLAN_MAP_CONFIG[location]
map_file = map_config["folder"] + "/" + map_config["gpkg_file"]
print(f"Displaying log replay at {location}")
participants, actual_time_range = dataset_parser.parse_trajectory(data_file, data_path)
frame_list = sorted({
frame
for participant in participants.values()
for frame in participant.trajectory.history_states
})
map_ = dataset_parser.parse_map(map_file, map_path)
print(len(map_.lanes), len(map_.areas), len(map_.roadlines), len(map_.regulations))
ani = render_nuplan(map_, participants, frame_list[400:480])
HTML(ani.to_html5_video())
Displaying log replay at las_vegas
4583 1370 12092 1164
