opensfm/synthetic_data/synthetic_scene.py (425 lines of code) (raw):

import functools import math from typing import Dict, Optional, List, Any, Union, Tuple, Callable import numpy as np import opensfm.synthetic_data.synthetic_dataset as sd import opensfm.synthetic_data.synthetic_generator as sg import opensfm.synthetic_data.synthetic_metrics as sm from opensfm import pygeometry, types, pymap, geo def get_camera( type: str, id: str, focal: float, k1: float, k2: float ) -> pygeometry.Camera: camera: Optional[pygeometry.Camera] = None if type == "perspective": camera = pygeometry.Camera.create_perspective(focal, k1, k2) elif type == "fisheye": camera = pygeometry.Camera.create_fisheye(focal, k1, k2) elif type == "spherical": camera = pygeometry.Camera.create_spherical() else: raise RuntimeError(f"Invalid camera type {type}") camera.id = id camera.height = 1600 camera.width = 2000 return camera def get_scene_generator(type: str, length: float, **kwargs) -> functools.partial: generator = None if type == "circle": generator = functools.partial(sg.ellipse_generator, length, length) if type == "ellipse": ellipse_ratio = 2 generator = functools.partial( sg.ellipse_generator, length, length / ellipse_ratio ) if type == "line": center_x = kwargs.get("center_x", 0) center_y = kwargs.get("center_y", 0) transpose = kwargs.get("transpose", True) generator = functools.partial( sg.line_generator, length, center_x, center_y, transpose ) assert generator return generator def camera_pose( position: np.ndarray, lookat: np.ndarray, up: np.ndarray ) -> pygeometry.Pose: """ Pose from position and look at direction >>> position = [1.0, 2.0, 3.0] >>> lookat = [0., 10.0, 2.0] >>> up = [0.0, 0.0, 1.0] >>> pose = camera_pose(position, lookat, up) >>> np.allclose(pose.get_origin(), position) True """ def normalized(x: np.ndarray) -> np.ndarray: return x / np.linalg.norm(x) ez = normalized(np.array(lookat) - np.array(position)) ex = normalized(np.cross(ez, up)) ey = normalized(np.cross(ez, ex)) pose = pygeometry.Pose() pose.set_rotation_matrix(np.array([ex, ey, ez])) pose.set_origin(position) return pose class SyntheticScene(object): def get_reconstruction(self) -> types.Reconstruction: raise NotImplementedError() class SyntheticCubeScene(SyntheticScene): """Scene consisting of cameras looking at point in a cube.""" def __init__(self, num_cameras: int, num_points: int, noise: float) -> None: self.reconstruction = types.Reconstruction() self.cameras = {} for i in range(num_cameras): camera = camera = pygeometry.Camera.create_perspective(0.9, -0.1, 0.01) camera.id = "camera%04d" % i camera.height = 600 camera.width = 800 self.cameras[camera.id] = camera self.reconstruction.cameras = self.cameras r = 2.0 for i in range(num_cameras): phi = np.random.rand() * math.pi theta = np.random.rand() * 2.0 * math.pi x = r * np.sin(theta) * np.cos(phi) y = r * np.sin(theta) * np.sin(phi) z = r * np.cos(theta) position = np.array([x, y, z]) alpha = np.random.rand() lookat = np.array([0.0, 0, 0]) up = np.array([alpha * 0.2, alpha * 0.2, 1.0]) shot_id = "shot%04d" % i camera_id = "camera%04d" % i pose = camera_pose(position, lookat, up) self.reconstruction.create_shot( shot_id, camera_id, pose, rig_camera_id=None, rig_instance_id=None ) points = np.random.rand(num_points, 3) - [0.5, 0.5, 0.5] for i, p in enumerate(points): point_id = "point" + str(i) pt = self.reconstruction.create_point(point_id, p) pt.color = np.array([100, 100, 20]) def get_reconstruction(self) -> types.Reconstruction: reconstruction = types.Reconstruction() # Copy our original reconstruction # since we do not want to modify the reference reconstruction.cameras = self.cameras for shot in self.reconstruction.shots.values(): reconstruction.create_shot( shot.id, shot.camera.id, shot.pose, rig_camera_id=None, rig_instance_id=None, ) for point in self.reconstruction.points.values(): pt = reconstruction.create_point(point.id, point.coordinates) pt.color = point.color return reconstruction class SyntheticStreetScene(SyntheticScene): """Scene consisting in a virtual street extruded along some parametric shape (line, ellipse), with camera placed along the shape. """ generator: Optional[Callable] wall_points: Optional[np.ndarray] floor_points: Optional[np.ndarray] shot_ids: List[List[str]] cameras: List[List[pygeometry.Camera]] instances_positions: List[np.ndarray] instances_rotations: List[np.ndarray] rig_instances: List[List[List[Tuple[str, str]]]] rig_cameras: List[List[pymap.RigCamera]] width: float def __init__( self, generator: Optional[Callable], reference: Optional[geo.TopocentricConverter] = None, ) -> None: self.generator = generator self.reference = reference self.wall_points = None self.floor_points = None self.shot_ids = [] self.cameras = [] self.instances_positions = [] self.instances_rotations = [] self.rig_instances = [] self.rig_cameras = [] self.width = 0.0 def combine(self, other_scene: "SyntheticStreetScene") -> "SyntheticStreetScene": combined_scene = SyntheticStreetScene(None) combined_scene.wall_points = np.concatenate( (self.wall_points, other_scene.wall_points) ) combined_scene.floor_points = np.concatenate( (self.floor_points, other_scene.floor_points) ) combined_scene.cameras = self.cameras + other_scene.cameras combined_scene.instances_positions = ( self.instances_positions + other_scene.instances_positions ) combined_scene.instances_rotations = ( self.instances_rotations + other_scene.instances_rotations ) combined_scene.rig_instances = self.rig_instances + other_scene.rig_instances combined_scene.rig_cameras = self.rig_cameras + other_scene.rig_cameras combined_scene.shot_ids = self.shot_ids + other_scene.shot_ids shift = 0 for subshots in combined_scene.shot_ids: for i in range(len(subshots)): subshots[i] = f"Shot {i+shift:04d}" shift += len(subshots) return combined_scene def add_street( self, points_count: int, height: float, width: float ) -> "SyntheticStreetScene": generator = self.generator assert generator self.wall_points, self.floor_points = sg.generate_street( sg.samples_generator_random_count(int(points_count // 3)), generator, height, width, ) self.width = width return self def perturb_walls(self, walls_pertubation: List[float]) -> "SyntheticStreetScene": wall_points = self.wall_points assert wall_points is not None sg.perturb_points(wall_points, walls_pertubation) return self def perturb_floor(self, floor_pertubation: List[float]) -> "SyntheticStreetScene": floor_points = self.floor_points assert floor_points is not None sg.perturb_points(floor_points, floor_pertubation) return self def set_terrain_hill( self, height: float, radius: float, repeated: bool ) -> "SyntheticStreetScene": if not repeated: self._set_terrain_hill_single(height, radius) else: self._set_terrain_hill_repeated(height, radius) return self def _set_terrain_hill_single(self, height: float, radius: float) -> None: wall_points, floor_points = self.wall_points, self.floor_points assert wall_points is not None and floor_points is not None wall_points[:, 2] += height * np.exp( -0.5 * np.linalg.norm(wall_points[:, :2], axis=1) ** 2 / radius ** 2 ) floor_points[:, 2] += height * np.exp( -0.5 * np.linalg.norm(floor_points[:, :2], axis=1) ** 2 / radius ** 2 ) for positions in self.instances_positions: for position in positions: position[2] += height * np.exp( -0.5 * np.linalg.norm( (position[0] ** 2 + position[1] ** 2) / radius ** 2 ) ) def _set_terrain_hill_repeated(self, height: float, radius: float) -> None: wall_points, floor_points = self.wall_points, self.floor_points assert wall_points is not None and floor_points is not None wall_points[:, 2] += height * np.sin( np.linalg.norm(wall_points[:, :2], axis=1) / radius ) floor_points[:, 2] += height * np.sin( np.linalg.norm(floor_points[:, :2], axis=1) / radius ) for positions in self.instances_positions: for position in positions: position[2] += height * np.sin( math.sqrt(position[0] ** 2 + position[1] ** 2) / radius ) def add_camera_sequence( self, camera: pygeometry.Camera, length: float, height: float, interval: float, position_noise: List[float], rotation_noise: float, positions_shift: Optional[List[float]] = None, end: Optional[float] = None, ) -> "SyntheticStreetScene": default_noise_interval = 0.25 * interval actual_end = length if end is None else end generator = self.generator assert generator positions, rotations = sg.generate_cameras( sg.samples_generator_interval( length, actual_end, interval, default_noise_interval ), generator, height, ) sg.perturb_points(positions, position_noise) sg.perturb_rotations(rotations, rotation_noise) if positions_shift: positions += np.array(positions_shift) shift = 0 if len(self.shot_ids) == 0 else sum(len(s) for s in self.shot_ids) new_shot_ids = [f"Shot {shift+i:04d}" for i in range(len(positions))] self.shot_ids.append(new_shot_ids) self.cameras.append([camera]) rig_camera = pymap.RigCamera(pygeometry.Pose(), camera.id) self.rig_cameras.append([rig_camera]) rig_instances = [] for shot_id in new_shot_ids: rig_instances.append([(shot_id, camera.id)]) self.rig_instances.append(rig_instances) self.instances_positions.append(positions) self.instances_rotations.append(rotations) return self def add_rig_camera_sequence( self, cameras: List[pygeometry.Camera], relative_positions: List[List[float]], relative_rotations: List[List[float]], length: float, height: float, interval: float, position_noise: List[float], rotation_noise: float, end: Optional[float] = None, ) -> "SyntheticStreetScene": default_noise_interval = 0.25 * interval actual_end = length if end is None else end generator = self.generator assert generator instances_positions, instances_rotations = sg.generate_cameras( sg.samples_generator_interval( length, actual_end, interval, default_noise_interval ), generator, height, ) sg.perturb_points(instances_positions, position_noise) sg.perturb_rotations(instances_rotations, rotation_noise) shift = sum(len(s) for s in self.shot_ids) shots_ids_per_camera = [] for j, (rig_camera_p, rig_camera_r) in enumerate( zip(relative_positions, relative_rotations) ): pose_rig_camera = pygeometry.Pose(rig_camera_r) pose_rig_camera.set_origin(rig_camera_p) rotations = [] positions = [] for instance_p, instance_r in zip(instances_positions, instances_rotations): pose_instance = pygeometry.Pose(instance_r) pose_instance.set_origin(instance_p) composed = pose_rig_camera.compose(pose_instance) rotations.append(composed.rotation) positions.append(composed.get_origin()) camera_shot_ids = [] for i in range(len(positions)): shot_index = i * len(relative_positions) + j camera_shot_ids.append(f"Shot {shift+shot_index:04d}") shots_ids_per_camera.append(camera_shot_ids) self.cameras.append(cameras) self.shot_ids += shots_ids_per_camera rig_camera_ids = [] rig_cameras = [] rig_camera_id_shift = sum(len(s) for s in self.rig_cameras) for i, (rig_camera_p, rig_camera_r) in enumerate( zip(relative_positions, relative_rotations) ): pose_rig_camera = pygeometry.Pose(rig_camera_r) pose_rig_camera.set_origin(rig_camera_p) rig_camera_id = f"RigCamera {rig_camera_id_shift + i}" rig_camera = pymap.RigCamera(pose_rig_camera, rig_camera_id) rig_camera_ids.append(rig_camera_id) rig_cameras.append(rig_camera) self.rig_cameras.append(rig_cameras) rig_instances: List[List[Tuple[str, str]]] = [] for i in range(len(instances_positions)): instance = [] for j in range(len(shots_ids_per_camera)): instance.append((shots_ids_per_camera[j][i], rig_camera_ids[j])) rig_instances.append(instance) self.rig_instances.append(rig_instances) self.instances_positions.append(instances_positions) self.instances_rotations.append(instances_rotations) return self def get_reconstruction(self) -> types.Reconstruction: floor_color = [120, 90, 10] wall_color = [10, 90, 130] return sg.create_reconstruction( points=np.asarray([self.floor_points, self.wall_points]), colors=np.asarray([floor_color, wall_color]), cameras=self.cameras, shot_ids=self.shot_ids, rig_shots=self.rig_instances, rig_positions=self.instances_positions, rig_rotations=self.instances_rotations, rig_cameras=self.rig_cameras, reference=self.reference, ) class SyntheticInputData: """Class that generate all data necessary to run SfM processes end-to-end based on some input Reconstruction (synthetic or not), by re-creating inputs (GPS, projections) based on some required amount of noise. """ reconstruction: types.Reconstruction exifs: Dict[str, Any] features: sd.SyntheticFeatures tracks_manager: pymap.TracksManager gcps: Dict[str, pymap.GroundControlPoint] def __init__( self, reconstruction: types.Reconstruction, reference: geo.TopocentricConverter, projection_max_depth: float, projection_noise: float, gps_noise: Union[Dict[str, float], float], imu_noise: float, gcp_noise: Tuple[float, float], causal_gps_noise: bool, gcps_count: Optional[int] = None, gcps_shift: Optional[np.ndarray] = None, on_disk_features_filename: Optional[str] = None, generate_projections: bool = True, ) -> None: self.reconstruction = reconstruction self.exifs = sg.generate_exifs( reconstruction, reference, gps_noise, imu_noise, causal_gps_noise=causal_gps_noise, ) if generate_projections: (self.features, self.tracks_manager, self.gcps) = sg.generate_track_data( reconstruction, projection_max_depth, projection_noise, gcp_noise, gcps_count, gcps_shift, on_disk_features_filename, ) else: self.features = sd.SyntheticFeatures(None) self.tracks_manager = pymap.TracksManager() def compare( reference: types.Reconstruction, gcps: Dict[str, pymap.GroundControlPoint], reconstruction: types.Reconstruction, ) -> Dict[str, float]: """Compare a reconstruction with reference groundtruth.""" completeness = sm.completeness_errors(reference, reconstruction) absolute_position = sm.position_errors(reference, reconstruction) absolute_rotation = sm.rotation_errors(reference, reconstruction) absolute_points = sm.points_errors(reference, reconstruction) absolute_gps = sm.gps_errors(reconstruction) absolute_gcp = sm.gcp_errors(reconstruction, gcps) aligned = sm.aligned_to_reference(reference, reconstruction) aligned_position = sm.position_errors(reference, aligned) aligned_rotation = sm.rotation_errors(reference, aligned) aligned_points = sm.points_errors(reference, aligned) aligned_gps = sm.gps_errors(aligned) return { "ratio_cameras": completeness[0], "ratio_points": completeness[1], "absolute_position_rmse": sm.rmse(absolute_position), "absolute_position_mad": sm.mad(absolute_position), "absolute_rotation_rmse": sm.rmse(absolute_rotation), "absolute_rotation_median": np.median(absolute_rotation), "absolute_points_rmse": sm.rmse(absolute_points), "absolute_points_mad": sm.mad(absolute_points), "absolute_gps_rmse": sm.rmse(absolute_gps), "absolute_gps_mad": sm.mad(absolute_gps), "absolute_gcp_rmse_horizontal": sm.rmse(absolute_gcp[:, :2]) if len(absolute_gcp.shape) > 1 else 0.0, "absolute_gcp_rmse_vertical": sm.rmse(absolute_gcp[:, 2]) if len(absolute_gcp.shape) > 1 else 0.0, "aligned_position_rmse": sm.rmse(aligned_position), "aligned_position_mad": sm.mad(aligned_position), "aligned_rotation_rmse": sm.rmse(aligned_rotation), "aligned_rotation_median": np.median(aligned_rotation), "aligned_gps_rmse": sm.rmse(aligned_gps), "aligned_gps_mad": sm.mad(aligned_gps), "aligned_points_rmse": sm.rmse(aligned_points), "aligned_points_mad": sm.mad(aligned_points), }