Skip to content

Commit 3081310

Browse files
committed
Fix loading of fisheye mei camera and add to viser.
1 parent 1a184aa commit 3081310

File tree

9 files changed

+181
-11
lines changed

9 files changed

+181
-11
lines changed

src/py123d/conversion/datasets/kitti360/kitti360_converter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ def _get_kitti360_fisheye_mei_camera_metadata(
387387
camera_type=fcam_type,
388388
width=fisheye_result[fcam_name]["image_width"],
389389
height=fisheye_result[fcam_name]["image_height"],
390-
mirror_parameter=fisheye_result[fcam_name]["mirror_parameters"],
390+
mirror_parameter=float(fisheye_result[fcam_name]["mirror_parameters"]["xi"]),
391391
distortion=distortion,
392392
projection=projection,
393393
)

src/py123d/datatypes/scene/arrow/arrow_scene.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ def get_fisheye_mei_camera_at_iteration(
149149
self, iteration: int, camera_type: FisheyeMEICameraType
150150
) -> Optional[FisheyeMEICamera]:
151151
fisheye_mei_camera: Optional[FisheyeMEICamera] = None
152-
if camera_type in self.available_pinhole_camera_types:
152+
if camera_type in self.available_fisheye_mei_camera_types:
153153
fisheye_mei_camera = get_camera_from_arrow_table(
154154
self._get_recording_table(),
155155
self._get_table_index(iteration),

src/py123d/datatypes/scene/arrow/utils/arrow_getters.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,14 +141,15 @@ def get_camera_from_arrow_table(
141141
else:
142142
raise NotImplementedError("Only string file paths for camera data are supported.")
143143

144-
camera_metadata = log_metadata.pinhole_camera_metadata[camera_type]
145-
if hasattr(camera_metadata, "mirror_parameter") and camera_metadata.mirror_parameter is not None:
144+
if camera_name.startswith("fcam"):
145+
camera_metadata = log_metadata.fisheye_mei_camera_metadata[camera_type]
146146
return FisheyeMEICamera(
147147
metadata=camera_metadata,
148148
image=image,
149149
extrinsic=extrinsic,
150150
)
151151
else:
152+
camera_metadata = log_metadata.pinhole_camera_metadata[camera_type]
152153
return PinholeCamera(
153154
metadata=camera_metadata,
154155
image=image,

src/py123d/datatypes/sensors/fisheye_mei_camera.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ def from_dict(cls, data_dict: Dict[str, Any]) -> FisheyeMEICameraMetadata:
150150
)
151151
return FisheyeMEICameraMetadata(**data_dict)
152152

153+
@property
154+
def aspect_ratio(self) -> float:
155+
return self.width / self.height
156+
153157
def to_dict(self) -> Dict[str, Any]:
154158
data_dict = asdict(self)
155159
data_dict["camera_type"] = int(self.camera_type)

src/py123d/script/config/conversion/datasets/kitti360_dataset.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ kitti360_dataset:
3434
pinhole_camera_store_option: "path"
3535

3636
# Fisheye Cameras
37-
include_fisheye_mei_cameras: false
37+
include_fisheye_mei_cameras: true
3838
fisheye_mei_camera_store_option: "path"
3939

4040
# LiDARs

src/py123d/visualization/viser/elements/sensor_elements.py

Lines changed: 136 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import viser
88

99
from py123d.datatypes.scene.abstract_scene import AbstractScene
10+
from py123d.datatypes.sensors.fisheye_mei_camera import FisheyeMEICamera, FisheyeMEICameraMetadata, FisheyeMEICameraType
1011
from py123d.datatypes.sensors.lidar import LiDARType
1112
from py123d.datatypes.sensors.pinhole_camera import PinholeCamera, PinholeCameraType
1213
from py123d.datatypes.vehicle_state.ego_state import EgoStateSE3
@@ -50,7 +51,7 @@ def _add_camera_frustums_to_viser_server(camera_type: PinholeCameraType) -> None
5051
f"camera_frustums/{camera_type.serialize()}",
5152
fov=camera.metadata.fov_y,
5253
aspect=camera.metadata.aspect_ratio,
53-
scale=viser_config.camera_frustum_frustum_scale,
54+
scale=viser_config.camera_frustum_scale,
5455
image=camera_image,
5556
position=camera_position,
5657
wxyz=camera_quaternion,
@@ -74,6 +75,60 @@ def _add_camera_frustums_to_viser_server(camera_type: PinholeCameraType) -> None
7475
return None
7576

7677

78+
def add_fisheye_frustums_to_viser_server(
79+
scene: AbstractScene,
80+
scene_interation: int,
81+
initial_ego_state: EgoStateSE3,
82+
viser_server: viser.ViserServer,
83+
viser_config: ViserConfig,
84+
fisheye_frustum_handles: Dict[FisheyeMEICameraType, viser.CameraFrustumHandle],
85+
) -> None:
86+
if viser_config.fisheye_frustum_visible:
87+
scene_center_array = initial_ego_state.center.point_3d.array
88+
ego_pose = scene.get_ego_state_at_iteration(scene_interation).rear_axle_se3.array
89+
ego_pose[StateSE3Index.XYZ] -= scene_center_array
90+
91+
def _add_fisheye_frustums_to_viser_server(fisheye_camera_type: FisheyeMEICameraType) -> None:
92+
camera = scene.get_fisheye_mei_camera_at_iteration(scene_interation, fisheye_camera_type)
93+
if camera is not None:
94+
fcam_position, fcam_quaternion, fcam_image = _get_fisheye_camera_values(
95+
camera,
96+
ego_pose.copy(),
97+
viser_config.fisheye_frustum_image_scale,
98+
)
99+
if fisheye_camera_type in fisheye_frustum_handles:
100+
fisheye_frustum_handles[fisheye_camera_type].position = fcam_position
101+
fisheye_frustum_handles[fisheye_camera_type].wxyz = fcam_quaternion
102+
fisheye_frustum_handles[fisheye_camera_type].image = fcam_image
103+
else:
104+
# NOTE @DanielDauner: The FOV is just taking as a static value here.
105+
# The function se
106+
fisheye_frustum_handles[fisheye_camera_type] = viser_server.scene.add_camera_frustum(
107+
f"camera_frustums/{fisheye_camera_type.serialize()}",
108+
fov=185, # vertical fov
109+
aspect=camera.metadata.aspect_ratio,
110+
scale=viser_config.fisheye_frustum_scale,
111+
image=fcam_image,
112+
position=fcam_position,
113+
wxyz=fcam_quaternion,
114+
)
115+
116+
return None
117+
118+
# NOTE; In order to speed up adding camera frustums, we use multithreading and resize the images.
119+
with concurrent.futures.ThreadPoolExecutor(
120+
max_workers=len(viser_config.fisheye_mei_camera_frustum_types)
121+
) as executor:
122+
future_to_camera = {
123+
executor.submit(_add_fisheye_frustums_to_viser_server, fcam_type): fcam_type
124+
for fcam_type in viser_config.fisheye_mei_camera_frustum_types
125+
}
126+
for future in concurrent.futures.as_completed(future_to_camera):
127+
_ = future.result()
128+
129+
return None
130+
131+
77132
def add_camera_gui_to_viser_server(
78133
scene: AbstractScene,
79134
scene_interation: int,
@@ -183,10 +238,90 @@ def _get_camera_values(
183238
return camera_position, camera_rotation, camera_image
184239

185240

241+
def _get_fisheye_camera_values(
242+
camera: FisheyeMEICamera,
243+
ego_pose: npt.NDArray[np.float64],
244+
resize_factor: Optional[float] = None,
245+
) -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], npt.NDArray[np.uint8]]:
246+
assert ego_pose.ndim == 1 and len(ego_pose) == len(StateSE3Index)
247+
248+
rel_camera_pose = camera.extrinsic.array
249+
abs_camera_pose = convert_relative_to_absolute_se3_array(origin=ego_pose, se3_array=rel_camera_pose)
250+
251+
camera_position = abs_camera_pose[StateSE3Index.XYZ]
252+
camera_rotation = abs_camera_pose[StateSE3Index.QUATERNION]
253+
254+
camera_image = _rescale_image(camera.image, resize_factor)
255+
return camera_position, camera_rotation, camera_image
256+
257+
186258
def _rescale_image(image: npt.NDArray[np.uint8], scale: float) -> npt.NDArray[np.uint8]:
187259
if scale == 1.0:
188260
return image
189261
new_width = int(image.shape[1] * scale)
190262
new_height = int(image.shape[0] * scale)
191263
downscaled_image = cv2.resize(image, (new_width, new_height), interpolation=cv2.INTER_LINEAR)
192264
return downscaled_image
265+
266+
267+
import numpy as np
268+
269+
270+
def calculate_fov(metadata: FisheyeMEICameraMetadata) -> tuple[float, float]:
271+
"""
272+
Calculate horizontal and vertical FOV in degrees.
273+
274+
Returns:
275+
(horizontal_fov, vertical_fov) in degrees
276+
"""
277+
xi = metadata.mirror_parameter
278+
gamma1 = metadata.projection.gamma1
279+
gamma2 = metadata.projection.gamma2
280+
u0 = metadata.projection.u0
281+
v0 = metadata.projection.v0
282+
283+
width = metadata.width
284+
height = metadata.height
285+
286+
# Calculate corner positions (furthest from principal point)
287+
corners = np.array([[0, 0], [width, 0], [0, height], [width, height]])
288+
289+
# Convert to normalized coordinates
290+
x_norm = (corners[:, 0] - u0) / gamma1
291+
y_norm = (corners[:, 1] - v0) / gamma2
292+
293+
# For MEI model, inverse projection (ignoring distortion for FOV estimate):
294+
# r² = x² + y²
295+
# θ = arctan(r / (1 - ξ·√(1 + r²)))
296+
297+
r_squared = x_norm**2 + y_norm**2
298+
r = np.sqrt(r_squared)
299+
300+
# Calculate incident angle for each corner
301+
# From MEI model: r = (X/Z_s) where Z_s = Z + ξ·√(X² + Y² + Z²)
302+
# This gives: θ = arctan(r·√(1 + (1-ξ²)r²) / (1 - ξ²·r²))
303+
# Simplified approximation:
304+
305+
if xi < 1e-6: # Perspective camera
306+
theta = np.arctan(r)
307+
else:
308+
# For small angles or as approximation
309+
denominator = 1 - xi * np.sqrt(1 + r_squared)
310+
theta = np.arctan2(r, denominator)
311+
312+
np.max(np.abs(theta))
313+
314+
# Calculate horizontal and vertical FOV separately
315+
x_max = np.max(np.abs(x_norm))
316+
y_max = np.max(np.abs(y_norm))
317+
318+
if xi < 1e-6:
319+
h_fov = 2 * np.arctan(x_max)
320+
v_fov = 2 * np.arctan(y_max)
321+
else:
322+
denom_h = 1 - xi * np.sqrt(1 + x_max**2)
323+
denom_v = 1 - xi * np.sqrt(1 + y_max**2)
324+
h_fov = 2 * np.arctan2(x_max, denom_h)
325+
v_fov = 2 * np.arctan2(y_max, denom_v)
326+
327+
return h_fov, v_fov

src/py123d/visualization/viser/viser_config.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from dataclasses import dataclass, field
22
from typing import List, Literal, Optional, Tuple
33

4+
from py123d.datatypes.sensors.fisheye_mei_camera import FisheyeMEICameraType
45
from py123d.datatypes.sensors.lidar import LiDARType
56
from py123d.datatypes.sensors.pinhole_camera import PinholeCameraType
67
from py123d.visualization.color.color import ELLIS_5
@@ -52,7 +53,7 @@ class ViserConfig:
5253

5354
# Map
5455
map_visible: bool = True
55-
map_radius: float = 500.0 # [m]
56+
map_radius: float = 200.0 # [m]
5657
map_non_road_z_offset: float = 0.1 # small z-translation to place crosswalks, parking, etc. on top of the road
5758
map_requery: bool = True # Re-query map when ego vehicle moves out of current map bounds
5859

@@ -61,18 +62,28 @@ class ViserConfig:
6162
bounding_box_type: Literal["mesh", "lines"] = "mesh"
6263
bounding_box_line_width: float = 4.0
6364

64-
# Cameras
65+
# Pinhole Cameras
6566
# -> Frustum
6667
camera_frustum_visible: bool = True
6768
camera_frustum_types: List[PinholeCameraType] = field(default_factory=lambda: all_camera_types.copy())
68-
camera_frustum_frustum_scale: float = 1.0
69+
camera_frustum_scale: float = 1.0
6970
camera_frustum_image_scale: float = 0.25 # Resize factor for the camera image shown on the frustum (<1.0 for speed)
7071

7172
# -> GUI
7273
camera_gui_visible: bool = True
7374
camera_gui_types: List[PinholeCameraType] = field(default_factory=lambda: [PinholeCameraType.PCAM_F0].copy())
7475
camera_gui_image_scale: float = 0.25 # Resize factor for the camera image shown in the GUI (<1.0 for speed)
7576

77+
# Fisheye MEI Cameras
78+
# -> Frustum
79+
fisheye_frustum_visible: bool = True
80+
fisheye_mei_camera_frustum_visible: bool = True
81+
fisheye_mei_camera_frustum_types: List[PinholeCameraType] = field(
82+
default_factory=lambda: [fcam for fcam in FisheyeMEICameraType]
83+
)
84+
fisheye_frustum_scale: float = 1.0
85+
fisheye_frustum_image_scale: float = 0.25 # Resize factor for the camera image shown on the frustum
86+
7687
# LiDAR
7788
lidar_visible: bool = True
7889
lidar_types: List[LiDARType] = field(default_factory=lambda: all_lidar_types.copy())

src/py123d/visualization/viser/viser_viewer.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from py123d.datatypes.maps.map_datatypes import MapLayer
1212
from py123d.datatypes.scene.abstract_scene import AbstractScene
13+
from py123d.datatypes.sensors.fisheye_mei_camera import FisheyeMEICameraType
1314
from py123d.datatypes.sensors.lidar import LiDARType
1415
from py123d.datatypes.sensors.pinhole_camera import PinholeCameraType
1516
from py123d.datatypes.vehicle_state.ego_state import EgoStateSE3
@@ -24,6 +25,7 @@
2425
get_ego_3rd_person_view_position,
2526
get_ego_bev_view_position,
2627
)
28+
from py123d.visualization.viser.elements.sensor_elements import add_fisheye_frustums_to_viser_server
2729
from py123d.visualization.viser.viser_config import ViserConfig
2830

2931
logger = logging.getLogger(__name__)
@@ -249,6 +251,14 @@ def _(_) -> None:
249251
self._viser_config,
250252
camera_gui_handles,
251253
)
254+
add_fisheye_frustums_to_viser_server(
255+
scene,
256+
gui_timestep.value,
257+
initial_ego_state,
258+
self._viser_server,
259+
self._viser_config,
260+
fisheye_frustum_handles,
261+
)
252262
add_lidar_pc_to_viser_server(
253263
scene,
254264
gui_timestep.value,
@@ -315,6 +325,7 @@ def _(event: viser.GuiEvent) -> None:
315325
"lines": None,
316326
}
317327
camera_frustum_handles: Dict[PinholeCameraType, viser.CameraFrustumHandle] = {}
328+
fisheye_frustum_handles: Dict[FisheyeMEICameraType, viser.CameraFrustumHandle] = {}
318329
camera_gui_handles: Dict[PinholeCameraType, viser.GuiImageHandle] = {}
319330
lidar_pc_handles: Dict[LiDARType, Optional[viser.PointCloudHandle]] = {LiDARType.LIDAR_MERGED: None}
320331
map_handles: Dict[MapLayer, viser.MeshHandle] = {}
@@ -342,6 +353,14 @@ def _(event: viser.GuiEvent) -> None:
342353
self._viser_config,
343354
camera_gui_handles,
344355
)
356+
add_fisheye_frustums_to_viser_server(
357+
scene,
358+
gui_timestep.value,
359+
initial_ego_state,
360+
self._viser_server,
361+
self._viser_config,
362+
fisheye_frustum_handles,
363+
)
345364
add_lidar_pc_to_viser_server(
346365
scene,
347366
gui_timestep.value,

test_viser.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77

88
if __name__ == "__main__":
99
# splits = ["kitti360_train"]
10-
# splits = ["nuscenes-mini_val", "nuscenes-mini_train"]
11-
splits = ["nuplan-mini_test", "nuplan-mini_train", "nuplan-mini_val"]
10+
splits = ["nuscenes-mini_val", "nuscenes-mini_train"]
11+
# splits = ["nuplan-mini_test", "nuplan-mini_train", "nuplan-mini_val"]
1212
# splits = ["nuplan_private_test"]
1313
# splits = ["carla_test"]
1414
# splits = ["wopd_val"]

0 commit comments

Comments
 (0)