|
5 | 5 | import math |
6 | 6 | import pathlib |
7 | 7 | import sqlite3 |
| 8 | +import xml.etree.ElementTree as ET |
8 | 9 | from enum import Enum |
9 | 10 | from typing import Any, Optional, Tuple |
10 | 11 |
|
@@ -1099,3 +1100,175 @@ def _vadere_shape_to_point_list( |
1099 | 1100 | for p in points |
1100 | 1101 | ] |
1101 | 1102 | 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) |
0 commit comments