opensfm/reconstruction_helpers.py (158 lines of code) (raw):

import logging import math from typing import Optional, List, Dict, Any, Iterable import numpy as np from opensfm import ( multiview, pygeometry, pymap, geometry, types, exif as oexif, rig, ) from opensfm.dataset_base import DataSetBase logger = logging.getLogger(__name__) def guess_acceleration_from_orientation_tag(orientation: int) -> List[float]: """Guess upward vector in camera coordinates given the orientation tag. Assumes camera is looking towards the horizon and horizon is horizontal on the image when taking in to account the orientation tag. """ # See http://sylvana.net/jpegcrop/exif_orientation.html if orientation == 1: return [0, -1, 0] if orientation == 2: return [0, -1, 0] if orientation == 3: return [0, 1, 0] if orientation == 4: return [0, 1, 0] if orientation == 5: return [-1, 0, 0] if orientation == 6: return [-1, 0, 0] if orientation == 7: return [1, 0, 0] if orientation == 8: return [1, 0, 0] raise RuntimeError(f"Error: Unknown orientation tag: {orientation}") def orientation_from_acceleration_in_image_axis(x: float, y: float) -> int: """Return the orientation tag corresponding to an acceleration""" if y <= -(np.fabs(x)): return 1 elif y >= np.fabs(x): return 3 elif x <= -(np.fabs(y)): return 6 elif x >= np.fabs(y): return 8 else: raise RuntimeError(f"Error: Invalid acceleration {x}, {y}!") def transform_acceleration_from_phone_to_image_axis( x: float, y: float, z: float, orientation: int ) -> List[float]: """Compute acceleration in image axis. Orientation tag is used to ensure that the resulting acceleration points downwards in the image. This validation is not needed if the orientation tag is the one from the original picture. It is only required when the image has been rotated with respect to the original and the orientation tag modified accordingly. """ assert orientation in [1, 3, 6, 8] # Orientation in image axis assuming image has not been transformed length = np.sqrt(x * x + y * y + z * z) if length < 3: # Assume IOS device since gravity is 1 in there ix, iy, iz = y, -x, z else: # Assume Android device since gravity is 9.8 in there ix, iy, iz = -y, -x, -z for _ in range(4): if orientation == orientation_from_acceleration_in_image_axis(ix, iy): break else: ix, iy = -iy, ix return [ix, iy, iz] def shot_acceleration_in_image_axis(shot: pymap.Shot) -> List[float]: """Get or guess shot's acceleration.""" orientation = shot.metadata.orientation.value if not 1 <= orientation <= 8: logger.error( "Unknown orientation tag {} for image {}".format(orientation, shot.id) ) orientation = 1 if shot.metadata.accelerometer.has_value: x, y, z = shot.metadata.accelerometer.value if x != 0 or y != 0 or z != 0: return transform_acceleration_from_phone_to_image_axis(x, y, z, orientation) return guess_acceleration_from_orientation_tag(orientation) def rotation_from_shot_metadata(shot: pymap.Shot) -> np.ndarray: rotation = rotation_from_angles(shot) if rotation is None: rotation = rotation_from_orientation_compass(shot) return rotation def rotation_from_orientation_compass(shot: pymap.Shot) -> np.ndarray: up_vector = shot_acceleration_in_image_axis(shot) if shot.metadata.compass_angle.has_value: angle = shot.metadata.compass_angle.value else: angle = 0.0 return multiview.rotation_matrix_from_up_vector_and_compass(up_vector, angle) def rotation_from_angles(shot: pymap.Shot) -> Optional[np.ndarray]: if not shot.metadata.opk_angles.has_value: return None opk_degrees = shot.metadata.opk_angles.value opk_rad = map(math.radians, opk_degrees) return geometry.rotation_from_opk(*opk_rad) def reconstruction_from_metadata( data: DataSetBase, images: Iterable[str] ) -> types.Reconstruction: """Initialize a reconstruction by using EXIF data for constructing shot poses and cameras.""" data.init_reference() rig_assignments = rig.rig_assignments_per_image(data.load_rig_assignments()) reconstruction = types.Reconstruction() reconstruction.reference = data.load_reference() reconstruction.cameras = data.load_camera_models() for image in images: camera_id = data.load_exif(image)["camera"] if image in rig_assignments: rig_instance_id, rig_camera_id, _ = rig_assignments[image] else: rig_instance_id = image rig_camera_id = camera_id reconstruction.add_rig_camera(pymap.RigCamera(pygeometry.Pose(), rig_camera_id)) reconstruction.add_rig_instance(pymap.RigInstance(rig_instance_id)) shot = reconstruction.create_shot( shot_id=image, camera_id=camera_id, rig_camera_id=rig_camera_id, rig_instance_id=rig_instance_id, ) shot.metadata = get_image_metadata(data, image) if not shot.metadata.gps_position.has_value: reconstruction.remove_shot(image) continue gps_pos = shot.metadata.gps_position.value shot.pose.set_rotation_matrix(rotation_from_shot_metadata(shot)) shot.pose.set_origin(gps_pos) shot.scale = 1.0 return reconstruction def exif_to_metadata( exif: Dict[str, Any], use_altitude: bool, reference: types.TopocentricConverter ) -> pymap.ShotMeasurements: """Construct a metadata object from raw EXIF tags (as a dict).""" metadata = pymap.ShotMeasurements() gps = exif.get("gps") if gps and "latitude" in gps and "longitude" in gps: lat, lon = gps["latitude"], gps["longitude"] if use_altitude: alt = min([oexif.maximum_altitude, gps.get("altitude", 2.0)]) else: alt = 2.0 # Arbitrary value used to align the reconstruction x, y, z = reference.to_topocentric(lat, lon, alt) metadata.gps_position.value = np.array([x, y, z]) metadata.gps_accuracy.value = gps.get("dop", 15.0) if metadata.gps_accuracy.value == 0.0: metadata.gps_accuracy.value = 15.0 opk = exif.get("opk") if opk and "omega" in opk and "phi" in opk and "kappa" in opk: omega, phi, kappa = opk["omega"], opk["phi"], opk["kappa"] metadata.opk_angles.value = np.array([omega, phi, kappa]) metadata.opk_accuracy.value = opk.get("accuracy", 1.0) metadata.orientation.value = exif.get("orientation", 1) if "accelerometer" in exif: metadata.accelerometer.value = exif["accelerometer"] if "compass" in exif: metadata.compass_angle.value = exif["compass"]["angle"] if "accuracy" in exif["compass"]: metadata.compass_accuracy.value = exif["compass"]["accuracy"] if "capture_time" in exif: metadata.capture_time.value = exif["capture_time"] if "skey" in exif: metadata.sequence_key.value = exif["skey"] return metadata def get_image_metadata(data: DataSetBase, image: str) -> pymap.ShotMeasurements: """Get image metadata as a ShotMetadata object.""" exif = data.load_exif(image) reference = data.load_reference() return exif_to_metadata(exif, data.config["use_altitude_tag"], reference)