Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add relative rotation metric on Rotation Averaging module #731

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion environment_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ channels:
- conda-forge
dependencies:
# python essentials
- python=3.8
- python=3.9
- pip
# formatting and dev environment
- black
Expand Down
2 changes: 1 addition & 1 deletion environment_linux_cpuonly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ channels:
- conda-forge
dependencies:
# python essentials
- python=3.8
- python=3.9
- pip
# formatting and dev environment
- black
Expand Down
2 changes: 1 addition & 1 deletion environment_mac.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ channels:
- conda-forge
dependencies:
# python essentials
- python=3.8
- python=3.9
- pip
# formatting and dev environment
- black
Expand Down
24 changes: 18 additions & 6 deletions gtsfm/averaging/rotation/rotation_averaging_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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]

Expand All @@ -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(
Expand Down
32 changes: 16 additions & 16 deletions gtsfm/utils/geometry_comparisons.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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:
Expand All @@ -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]

Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down
34 changes: 33 additions & 1 deletion gtsfm/utils/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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:
Expand Down
Loading