Skip to content

Commit 493df54

Browse files
authored
Add crowdit loader (#474)
Add crowdit loader with reference data
1 parent 2688aed commit 493df54

5 files changed

Lines changed: 727 additions & 0 deletions

File tree

pedpy/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,11 +63,13 @@
6363
from .io.trajectory_loader import (
6464
TrajectoryUnit,
6565
load_trajectory,
66+
load_trajectory_from_crowdit,
6667
load_trajectory_from_jupedsim_sqlite,
6768
load_trajectory_from_ped_data_archive_hdf5,
6869
load_trajectory_from_txt,
6970
load_trajectory_from_vadere,
7071
load_trajectory_from_viswalk,
72+
load_walkable_area_from_crowdit,
7173
load_walkable_area_from_jupedsim_sqlite,
7274
load_walkable_area_from_ped_data_archive_hdf5,
7375
load_walkable_area_from_vadere_scenario,
@@ -164,6 +166,8 @@
164166
"load_walkable_area_from_jupedsim_sqlite",
165167
"load_walkable_area_from_ped_data_archive_hdf5",
166168
"load_walkable_area_from_vadere_scenario",
169+
"load_walkable_area_from_crowdit",
170+
"load_trajectory_from_crowdit",
167171
"compute_classic_density",
168172
"compute_line_density",
169173
"compute_passing_density",

pedpy/io/trajectory_loader.py

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import math
66
import pathlib
77
import sqlite3
8+
import xml.etree.ElementTree as ET
89
from enum import Enum
910
from typing import Any, Optional, Tuple
1011

@@ -1099,3 +1100,175 @@ def _vadere_shape_to_point_list(
10991100
for p in points
11001101
]
11011102
return points
1103+
1104+
1105+
def load_trajectory_from_crowdit(
1106+
*,
1107+
trajectory_file: pathlib.Path,
1108+
) -> TrajectoryData:
1109+
"""Loads data from Crowdit file as :class:`~trajectory_data.TrajectoryData`.
1110+
1111+
This function reads a CSV file containing trajectory data from Crowdit
1112+
simulations and converts it into a :class:`~trajectory_data.TrajectoryData`
1113+
object.
1114+
1115+
Args:
1116+
trajectory_file: The full path of the CSV file containing the Crowdit
1117+
trajectory data. The expected format is a CSV file with comma
1118+
as delimiter, and it should contain at least the following
1119+
columns: pedID, time, posX, posY.
1120+
1121+
Returns:
1122+
TrajectoryData: :class:`~trajectory_data.TrajectoryData` representation
1123+
of the file data
1124+
1125+
Raises:
1126+
LoadTrajectoryError: If the provided path does not exist or is not a
1127+
file.
1128+
"""
1129+
_validate_is_file(trajectory_file)
1130+
1131+
traj_dataframe = _load_trajectory_data_from_crowdit(
1132+
trajectory_file=trajectory_file
1133+
)
1134+
traj_dataframe["frame"], traj_frame_rate = _calculate_frames_and_fps(
1135+
traj_dataframe
1136+
)
1137+
1138+
return TrajectoryData(
1139+
data=traj_dataframe[[ID_COL, FRAME_COL, X_COL, Y_COL]],
1140+
frame_rate=traj_frame_rate,
1141+
)
1142+
1143+
1144+
def _load_trajectory_data_from_crowdit(
1145+
*, trajectory_file: pathlib.Path
1146+
) -> pd.DataFrame:
1147+
"""Parse the Crowdit trajectory file for trajectory data.
1148+
1149+
Args:
1150+
trajectory_file: The file containing the trajectory data.
1151+
The expected format is a CSV file with comma as delimiter, and it
1152+
should contain at least the following columns:
1153+
pedID, time, posX, posY.
1154+
1155+
Returns:
1156+
The trajectory data as :class:`DataFrame`, the coordinates are
1157+
in meter (m).
1158+
"""
1159+
columns_to_keep = ["pedID", "time", "posX", "posY"]
1160+
rename_mapping = {
1161+
"pedID": ID_COL,
1162+
"time": TIME_COL,
1163+
"posX": X_COL,
1164+
"posY": Y_COL,
1165+
}
1166+
column_types = {"id": int, "time": float, "x": float, "y": float}
1167+
1168+
common_error_message = (
1169+
"The given trajectory file seems to be incorrect or empty. "
1170+
"It should contain at least the following columns: "
1171+
"pedID, time, posX, posY, separated by comma. "
1172+
f"Please check your trajectory file: {trajectory_file}."
1173+
)
1174+
1175+
try:
1176+
data = pd.read_csv(
1177+
trajectory_file,
1178+
encoding="utf-8-sig",
1179+
).dropna()
1180+
except Exception as e:
1181+
raise LoadTrajectoryError(
1182+
f"{common_error_message}\nOriginal error: {e}"
1183+
) from e
1184+
1185+
missing_columns = set(columns_to_keep) - set(data.columns)
1186+
if missing_columns:
1187+
raise LoadTrajectoryError(
1188+
f"{common_error_message}"
1189+
f"Missing columns: {', '.join(missing_columns)}."
1190+
)
1191+
1192+
try:
1193+
data = data[columns_to_keep]
1194+
data = data.rename(columns=rename_mapping)
1195+
data = data.astype(column_types)
1196+
except Exception as e:
1197+
raise LoadTrajectoryError(
1198+
f"{common_error_message}\nOriginal error: {e}"
1199+
) from e
1200+
1201+
if data.empty:
1202+
raise LoadTrajectoryError(common_error_message)
1203+
1204+
return data
1205+
1206+
1207+
def load_walkable_area_from_crowdit(
1208+
*, geometry_file: pathlib.Path, buffer: float = 1e-3
1209+
) -> WalkableArea:
1210+
"""Load walkable area from a Crowdit XML geometry file.
1211+
1212+
Args:
1213+
geometry_file: Path to the XML geometry file.
1214+
buffer: Optional padding around the bounding box to avoid
1215+
overlap with obstacles.
1216+
1217+
Returns:
1218+
WalkableArea: representation of the walkable area.
1219+
"""
1220+
_validate_is_file(geometry_file)
1221+
1222+
try:
1223+
tree = ET.parse(geometry_file)
1224+
root = tree.getroot()
1225+
except Exception as e:
1226+
raise LoadTrajectoryError(
1227+
f"Could not parse Crowdit geometry file: {geometry_file}\n"
1228+
f"Original error: {e}"
1229+
) from e
1230+
1231+
all_points = []
1232+
walls = []
1233+
1234+
# Get walls from all layers, ignore WunderZone
1235+
for layer in root.findall("layer"):
1236+
for geom in layer.findall("wall"):
1237+
points = []
1238+
for pt in geom.findall("point"):
1239+
x = pt.get("x")
1240+
y = pt.get("y")
1241+
if x is None or y is None:
1242+
raise LoadTrajectoryError(
1243+
f"Invalid point found in {geometry_file}."
1244+
"missing x or y attribute"
1245+
)
1246+
points.append((float(x), float(y)))
1247+
if points:
1248+
if points[0] != points[-1]:
1249+
points.append(points[0])
1250+
all_points.extend(points)
1251+
walls.append(points)
1252+
1253+
if not all_points:
1254+
raise LoadTrajectoryError(
1255+
f"No wall polygons found in Crowdit geometry file: {geometry_file}"
1256+
)
1257+
1258+
# Exception for single wall → directly as WalkableArea
1259+
if len(walls) == 1:
1260+
return WalkableArea(polygon=walls[0])
1261+
1262+
# Normal case: Bounding Box + all walls as obstacles
1263+
xs, ys = zip(*all_points, strict=True)
1264+
minx, maxx = min(xs), max(xs)
1265+
miny, maxy = min(ys), max(ys)
1266+
outer = [
1267+
(minx - buffer, miny - buffer),
1268+
(maxx + buffer, miny - buffer),
1269+
(maxx + buffer, maxy + buffer),
1270+
(minx - buffer, maxy + buffer),
1271+
(minx - buffer, miny - buffer),
1272+
]
1273+
1274+
return WalkableArea(polygon=outer, obstacles=walls)
5.26 KB
Binary file not shown.

0 commit comments

Comments
 (0)