From bbb46fb0bd8284c104375a3e78d2b57c5e5c40fb Mon Sep 17 00:00:00 2001 From: senselessdev1 Date: Tue, 10 Oct 2023 00:12:42 -0400 Subject: [PATCH 1/5] add relative rotation metric --- .../rotation/rotation_averaging_base.py | 22 +++++++--- gtsfm/utils/geometry_comparisons.py | 44 ++++++++++++++----- gtsfm/utils/metrics.py | 23 +++++++++- 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/gtsfm/averaging/rotation/rotation_averaging_base.py b/gtsfm/averaging/rotation/rotation_averaging_base.py index 7fec94a9a..cc697a539 100644 --- a/gtsfm/averaging/rotation/rotation_averaging_base.py +++ b/gtsfm/averaging/rotation/rotation_averaging_base.py @@ -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 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..aa0c56f3a 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 = [ @@ -263,17 +263,41 @@ def compare_global_poses( return rotations_equal and translations_equal +def compute_rotation_angle_measurement_consistency( + i2Ri1: Optional[Unit3], wRi2: Optional[Pose3], wRi1: Optional[Pose3] +) -> Optional[float]: + """Compute angle between a [ TODO ] between 2 poses. + + Given a relative rotation measurement from i2 to i1, and the estimated global rotations of + i1 and i2, returns the angular difference between the relative vs. synthetic measurements. + + Args: + i2Ri1: Relative rotation measurement. + wRi2: Global rotation of camera i2. + wRi1: Global rotation of camera i1. + + Returns: + Angle between two-view measurement and synthetic relative rotation in degrees. + """ + if i2Ri1 is None or wRi2 is None or wRi1 is None: + return None + + # Synthetic measurement. + i2Ri1_synthetic = wRi2.between(wRi1) + return compute_relative_rotation_angle(i2Ri1, i2Ri1_synthetic) + + def compute_relative_rotation_angle(R_1: Optional[Rot3], R_2: Optional[Rot3]) -> Optional[float]: """Compute the angle between two rotations. 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 +316,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..11c765cd7 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,27 @@ 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. + 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)] + angles.append( + comp_utils.compute_rotation_angle_measurement_consistency(i2Ri1, wRi2=wRi_list[i2], wRi1=wRi_list[i1]) + ) + 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: From 4e12a9cc04de0d801e909949e9e365a69a59fddd Mon Sep 17 00:00:00 2001 From: senselessdev1 Date: Wed, 11 Oct 2023 12:04:51 -0400 Subject: [PATCH 2/5] remove glorified wrapped of .between() --- gtsfm/utils/geometry_comparisons.py | 36 +++++------------------------ gtsfm/utils/metrics.py | 17 +++++++++++--- 2 files changed, 20 insertions(+), 33 deletions(-) diff --git a/gtsfm/utils/geometry_comparisons.py b/gtsfm/utils/geometry_comparisons.py index aa0c56f3a..1a30c98ff 100644 --- a/gtsfm/utils/geometry_comparisons.py +++ b/gtsfm/utils/geometry_comparisons.py @@ -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] @@ -263,30 +263,6 @@ def compare_global_poses( return rotations_equal and translations_equal -def compute_rotation_angle_measurement_consistency( - i2Ri1: Optional[Unit3], wRi2: Optional[Pose3], wRi1: Optional[Pose3] -) -> Optional[float]: - """Compute angle between a [ TODO ] between 2 poses. - - Given a relative rotation measurement from i2 to i1, and the estimated global rotations of - i1 and i2, returns the angular difference between the relative vs. synthetic measurements. - - Args: - i2Ri1: Relative rotation measurement. - wRi2: Global rotation of camera i2. - wRi1: Global rotation of camera i1. - - Returns: - Angle between two-view measurement and synthetic relative rotation in degrees. - """ - if i2Ri1 is None or wRi2 is None or wRi1 is None: - return None - - # Synthetic measurement. - i2Ri1_synthetic = wRi2.between(wRi1) - return compute_relative_rotation_angle(i2Ri1, i2Ri1_synthetic) - - def compute_relative_rotation_angle(R_1: Optional[Rot3], R_2: Optional[Rot3]) -> Optional[float]: """Compute the angle between two rotations. diff --git a/gtsfm/utils/metrics.py b/gtsfm/utils/metrics.py index 11c765cd7..f083c6a06 100644 --- a/gtsfm/utils/metrics.py +++ b/gtsfm/utils/metrics.py @@ -248,9 +248,20 @@ def compute_relative_rotation_angle_metric( angles: List[Optional[float]] = [] for i1, i2 in i2Ri1_dict: i2Ri1 = i2Ri1_dict[(i1, i2)] - angles.append( - comp_utils.compute_rotation_angle_measurement_consistency(i2Ri1, wRi2=wRi_list[i2], wRi1=wRi_list[i1]) - ) + + 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 vs. synthetic measurements. + # (angle between two-view measurement and synthetic relative rotation in degrees). + i2Ri1_synthetic = wRi2.between(wRi1) + angle_deg = comp_utils.compute_relative_rotation_angle(i2Ri1, i2Ri1_synthetic) + angles.append(angle_deg) + return GtsfmMetric("relative_rotation_angle_consistency_error_deg", np.array(angles, dtype=np.float32)) From 929008fb8b13d21002f62e912b0cd41341057424 Mon Sep 17 00:00:00 2001 From: senselessdev1 Date: Fri, 13 Oct 2023 00:14:38 -0700 Subject: [PATCH 3/5] improve docs --- gtsfm/averaging/rotation/rotation_averaging_base.py | 4 ++-- gtsfm/utils/metrics.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gtsfm/averaging/rotation/rotation_averaging_base.py b/gtsfm/averaging/rotation/rotation_averaging_base.py index cc697a539..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. @@ -96,7 +96,7 @@ def evaluate( Args: wRi_computed: List of global rotations computed. wTi_gt: Ground truth global rotations to compare against. - i2Ri1_dict: Relative rotations as dictionary (i1, i2): i2Ri1. + i2Ri1_dict: Relative rotations generated by front-end as dictionary (i1, i2): i2Ri1. Returns: Metrics on global rotations. diff --git a/gtsfm/utils/metrics.py b/gtsfm/utils/metrics.py index f083c6a06..94bd8412b 100644 --- a/gtsfm/utils/metrics.py +++ b/gtsfm/utils/metrics.py @@ -239,7 +239,7 @@ def compute_relative_rotation_angle_metric( """Computes consistency statistics between estimated global rotations and relative rotation measurements. Args: - i2Ri1_dict: List of relative rotation measurements. + i2Ri1_dict: List of relative rotation measurements, generated by front-end. wRi_list: List of estimated camera global rotations. Returns: From fe99fa54ea3b92384b8a9ba06c9f79023fee2192 Mon Sep 17 00:00:00 2001 From: senselessdev1 Date: Thu, 19 Oct 2023 00:16:41 -0400 Subject: [PATCH 4/5] address comments --- gtsfm/utils/metrics.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gtsfm/utils/metrics.py b/gtsfm/utils/metrics.py index 94bd8412b..216bbf497 100644 --- a/gtsfm/utils/metrics.py +++ b/gtsfm/utils/metrics.py @@ -256,10 +256,10 @@ def compute_relative_rotation_angle_metric( 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 vs. synthetic measurements. - # (angle between two-view measurement and synthetic relative rotation in degrees). - i2Ri1_synthetic = wRi2.between(wRi1) - angle_deg = comp_utils.compute_relative_rotation_angle(i2Ri1, i2Ri1_synthetic) + # 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)) From 1351ba396e531e869969912daed88cb80106f3c7 Mon Sep 17 00:00:00 2001 From: senselessdev1 Date: Thu, 19 Oct 2023 22:56:49 -0400 Subject: [PATCH 5/5] use 3.9 --- environment_linux.yml | 2 +- environment_linux_cpuonly.yml | 2 +- environment_mac.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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