diff --git a/environment_linux.yml b/environment_linux.yml index 6c0f837bd..521dd430d 100644 --- a/environment_linux.yml +++ b/environment_linux.yml @@ -8,7 +8,7 @@ channels: - conda-forge dependencies: # python essentials - - python=3.8 + - python=3.9 - pip # formatting and dev environment - black diff --git a/environment_linux_cpuonly.yml b/environment_linux_cpuonly.yml index 9de9d997a..db2eebde5 100644 --- a/environment_linux_cpuonly.yml +++ b/environment_linux_cpuonly.yml @@ -8,7 +8,7 @@ channels: - conda-forge dependencies: # python essentials - - python=3.8 + - python=3.9 - pip # formatting and dev environment - black diff --git a/environment_mac.yml b/environment_mac.yml index 1d173f0c2..874b37cb2 100644 --- a/environment_mac.yml +++ b/environment_mac.yml @@ -12,7 +12,7 @@ channels: - conda-forge dependencies: # python essentials - - python=3.8 + - python=3.9 - pip # formatting and dev environment - black diff --git a/gtsfm/averaging/rotation/rotation_averaging_base.py b/gtsfm/averaging/rotation/rotation_averaging_base.py index 7fec94a9a..37d9e197d 100644 --- a/gtsfm/averaging/rotation/rotation_averaging_base.py +++ b/gtsfm/averaging/rotation/rotation_averaging_base.py @@ -66,7 +66,7 @@ def _run_rotation_averaging_base( Args: num_images: Number of poses. - i2Ri1_dict: Relative rotations as dictionary (i1, i2): i2Ri1. + i2Ri1_dict: Relative rotations generated by front-end as dictionary (i1, i2): i2Ri1. i1Ti2_priors: Priors on relative poses as dictionary(i1, i2): PosePrior on i1Ti2. wTi_gt: Ground truth global rotations to compare against. @@ -80,23 +80,29 @@ def _run_rotation_averaging_base( wRis = self.run_rotation_averaging(num_images, i2Ri1_dict, i1Ti2_priors) run_time = time.time() - start_time - metrics = self.evaluate(wRis, wTi_gt) + metrics = self.evaluate(wRis, wTi_gt, i2Ri1_dict) metrics.add_metric(GtsfmMetric("total_duration_sec", run_time)) return wRis, metrics - def evaluate(self, wRi_computed: List[Optional[Rot3]], wTi_gt: List[Optional[Pose3]]) -> GtsfmMetricsGroup: + def evaluate( + self, + wRi_computed: List[Optional[Rot3]], + wTi_gt: List[Optional[Pose3]], + i2Ri1_dict: Dict[Tuple[int, int], Optional[Rot3]], + ) -> GtsfmMetricsGroup: """Evaluates the global rotations computed by the rotation averaging implementation. Args: wRi_computed: List of global rotations computed. wTi_gt: Ground truth global rotations to compare against. - - Raises: - ValueError: If the length of the computed and GT list differ. + i2Ri1_dict: Relative rotations generated by front-end as dictionary (i1, i2): i2Ri1. Returns: Metrics on global rotations. + + Raises: + ValueError: If the length of the computed and GT list differ. """ wRi_gt = [wTi.rotation() if wTi is not None else None for wTi in wTi_gt] @@ -108,6 +114,12 @@ def evaluate(self, wRi_computed: List[Optional[Rot3]], wTi_gt: List[Optional[Pos metrics = [] metrics.append(GtsfmMetric(name="num_rotations_computed", data=len([x for x in wRi_computed if x is not None]))) metrics.append(metric_utils.compute_rotation_angle_metric(wRi_aligned, wRi_gt)) + metrics.append( + metric_utils.compute_relative_rotation_angle_metric( + i2Ri1_dict=i2Ri1_dict, + wRi_list=wRi_computed, + ) + ) return GtsfmMetricsGroup(name="rotation_averaging_metrics", metrics=metrics) def create_computation_graph( diff --git a/gtsfm/utils/geometry_comparisons.py b/gtsfm/utils/geometry_comparisons.py index 67f0cee6c..1a30c98ff 100644 --- a/gtsfm/utils/geometry_comparisons.py +++ b/gtsfm/utils/geometry_comparisons.py @@ -19,11 +19,11 @@ def align_rotations(aRi_list: List[Optional[Rot3]], bRi_list: List[Optional[Rot3 """Aligns the list of rotations to the reference list by using Karcher mean. Args: - aRi_list: reference rotations in frame "a" which are the targets for alignment - bRi_list: input rotations which need to be aligned to frame "a" + aRi_list: Reference rotations in frame "a" which are the targets for alignment + bRi_list: Input rotations which need to be aligned to frame "a" Returns: - aRi_list_: transformed input rotations previously "bRi_list" but now which + aRi_list_: Transformed input rotations previously "bRi_list" but now which have the same origin as reference (now living in "a" frame) """ aRb_list = [ @@ -207,18 +207,18 @@ def compare_global_poses( aTi_list: 1st list of poses. bTi_list: 2nd list of poses. rot_angular_error_threshold_degrees (optional): angular error threshold for rotations. Defaults to 2. - trans_err_atol (optional): absolute error threshold for translation. Defaults to 1e-2. - trans_err_rtol (optional): relative error threshold for translation. Defaults to 1e-1. + trans_err_atol (optional): Absolute error threshold for translation. Defaults to 1e-2. + trans_err_rtol (optional): Relative error threshold for translation. Defaults to 1e-1. Returns: - result of the comparison. + Result of the comparison. """ - # check the length of the input lists + # Check the length of the input lists if len(aTi_list) != len(bTi_list): return False - # check the presense of valid Pose3 objects in the same location + # Check the presence of valid Pose3 objects in the same location. aTi_valid = [i for (i, aTi) in enumerate(aTi_list) if aTi is not None] bTi_valid = [i for (i, bTi) in enumerate(bTi_list) if bTi is not None] if aTi_valid != bTi_valid: @@ -228,7 +228,7 @@ def compare_global_poses( # we need >= two entries going forward for meaningful comparisons return False - # align the remaining poses + # Align the remaining poses. aTi_list = [aTi_list[i] for i in aTi_valid] bTi_list = [bTi_list[i] for i in bTi_valid] @@ -269,11 +269,11 @@ def compute_relative_rotation_angle(R_1: Optional[Rot3], R_2: Optional[Rot3]) -> Note: the angle is the norm of the angle-axis representation. Args: - R_1: the first rotation. - R_2: the second rotation. + R_1: The first rotation. + R_2: The second rotation. Returns: - the angle between two rotations, in degrees + The angle between two rotations, in degrees. """ if R_1 is None or R_2 is None: @@ -292,16 +292,16 @@ def compute_relative_unit_translation_angle(U_1: Optional[Unit3], U_2: Optional[ """Compute the angle between two unit-translations. Args: - U_1: the first unit-translation. - U_2: the second unit-translation. + U_1: The first unit-translation. + U_2: The second unit-translation. Returns: - the angle between the two unit-vectors, in degrees + The angle between the two unit-vectors, in degrees. """ if U_1 is None or U_2 is None: return None - # TODO: expose Unit3's dot function and use it directly + # TODO: expose Unit3's dot function and use it directly. dot_product = np.dot(U_1.point3(), U_2.point3()) dot_product = np.clip(dot_product, -1, 1) angle_rad = np.arccos(dot_product) diff --git a/gtsfm/utils/metrics.py b/gtsfm/utils/metrics.py index 23898b8f7..216bbf497 100644 --- a/gtsfm/utils/metrics.py +++ b/gtsfm/utils/metrics.py @@ -212,7 +212,7 @@ def compute_keypoint_intersections( def compute_rotation_angle_metric(wRi_list: List[Optional[Rot3]], gt_wRi_list: List[Optional[Pose3]]) -> GtsfmMetric: - """Computes statistics for the angle between estimated and GT rotations. + """Computes statistics for the angle between estimated and GT global rotations. Assumes that the estimated and GT rotations have been aligned and do not have a gauge freedom. @@ -233,6 +233,38 @@ def compute_rotation_angle_metric(wRi_list: List[Optional[Rot3]], gt_wRi_list: L return GtsfmMetric("rotation_angle_error_deg", errors) +def compute_relative_rotation_angle_metric( + i2Ri1_dict: Dict[Tuple[int, int], Optional[Unit3]], wRi_list: List[Optional[Pose3]] +) -> GtsfmMetric: + """Computes consistency statistics between estimated global rotations and relative rotation measurements. + + Args: + i2Ri1_dict: List of relative rotation measurements, generated by front-end. + wRi_list: List of estimated camera global rotations. + + Returns: + A GtsfmMetric for the relative rotation angle errors, in degrees. + """ + angles: List[Optional[float]] = [] + for i1, i2 in i2Ri1_dict: + i2Ri1 = i2Ri1_dict[(i1, i2)] + + wRi2 = wRi_list[i2] + wRi1 = wRi_list[i1] + + if i2Ri1 is None or wRi2 is None or wRi1 is None: + return None + + # Given a relative rotation measurement from i2 to i1, and the estimated global rotations of + # i1 and i2, compute the angular difference between the relative measurement vs. derived relative + # rotation. + i2Ri1_derived = wRi2.between(wRi1) + angle_deg = comp_utils.compute_relative_rotation_angle(i2Ri1, i2Ri1_derived) + angles.append(angle_deg) + + return GtsfmMetric("relative_rotation_angle_consistency_error_deg", np.array(angles, dtype=np.float32)) + + def compute_translation_distance_metric( wti_list: List[Optional[Point3]], gt_wti_list: List[Optional[Point3]] ) -> GtsfmMetric: