scope-camera-video / scope /evaluation.py
iggy101's picture
Upload folder using huggingface_hub (part 2)
2dc3625 verified
Raw
History Blame Contribute Delete
5.2 kB
"""Camera-trajectory metrics used to evaluate SCoPE generations."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import TypedDict
import numpy as np
class TrajectoryMetrics(TypedDict):
rotation_error_degrees: float
translation_error: float
ate: float
cammc: float
scale_normalized_ate: float
scale_normalized_cammc: float
away_response: float
return_translation_ratio: float
return_rotation_degrees: float
control_success: bool
def _as_homogeneous(poses: np.ndarray) -> np.ndarray:
poses = np.asarray(poses, dtype=np.float64)
if poses.ndim != 3 or poses.shape[1:] not in ((3, 4), (4, 4)):
raise ValueError(f"Expected poses [T,3,4] or [T,4,4], got {poses.shape}")
if poses.shape[1:] == (4, 4):
return poses.copy()
bottom = np.broadcast_to(np.array([0.0, 0.0, 0.0, 1.0]), (len(poses), 1, 4))
return np.concatenate((poses, bottom), axis=1)
def _relative(poses: np.ndarray) -> np.ndarray:
poses_44 = _as_homogeneous(poses)
return np.linalg.inv(poses_44[0]) @ poses_44
def _normalize_translation(poses: np.ndarray) -> np.ndarray:
output = poses.copy()
scale = np.linalg.norm(output[:, :3, 3], axis=1).max()
if scale > 1e-8:
output[:, :3, 3] /= scale
return output
def _rotation_angles_degrees(rotations: np.ndarray) -> np.ndarray:
trace = np.trace(rotations, axis1=1, axis2=2)
return np.degrees(np.arccos(np.clip((trace - 1.0) / 2.0, -1.0, 1.0)))
def evaluate_trajectory(
target_c2w: np.ndarray,
predicted_c2w: np.ndarray,
*,
away_window: slice = slice(36, 41),
return_window: slice = slice(76, 81),
) -> TrajectoryMetrics:
"""Evaluate one predicted camera path against its conditioning trajectory.
The inputs must use the OpenCV camera-to-world convention. Both raw and
independently scale-normalized translation metrics are returned.
"""
target = _relative(target_c2w)
predicted = _relative(predicted_c2w)
if len(target) != len(predicted):
raise ValueError(f"Pose lengths differ: target={len(target)}, predicted={len(predicted)}")
if len(target) < max(away_window.stop or 0, return_window.stop or 0):
raise ValueError("The default Revisit windows require at least 81 poses")
target_normalized = _normalize_translation(target)
predicted_normalized = _normalize_translation(predicted)
relative_rotation = (
np.swapaxes(target_normalized[:, :3, :3], 1, 2) @ predicted_normalized[:, :3, :3]
)
rotation_error = float(_rotation_angles_degrees(relative_rotation).mean())
raw_translation_delta = predicted[:, :3, 3] - target[:, :3, 3]
translation_error = float(np.linalg.norm(raw_translation_delta, axis=1).mean())
raw_ate = float(np.sqrt(np.square(raw_translation_delta).sum(axis=1).mean()))
raw_cammc = float(np.linalg.norm(predicted[:, :3, :4] - target[:, :3, :4], axis=(1, 2)).mean())
normalized_translation_delta = predicted_normalized[:, :3, 3] - target_normalized[:, :3, 3]
normalized_ate = float(np.sqrt(np.square(normalized_translation_delta).sum(axis=1).mean()))
normalized_cammc = float(
np.linalg.norm(
predicted_normalized[:, :3, :4] - target_normalized[:, :3, :4],
axis=(1, 2),
).mean()
)
predicted_distance = np.linalg.norm(predicted[:, :3, 3], axis=1)
max_distance = float(predicted_distance.max())
denominator = max(max_distance, 1e-8)
away_response = float(predicted_distance[away_window].mean() / denominator)
return_ratio = float(predicted_distance[return_window].mean() / denominator)
return_rotation = float(_rotation_angles_degrees(predicted[return_window, :3, :3]).mean())
success = away_response >= 0.5 and return_ratio <= 0.25 and return_rotation <= 5.0
return {
"rotation_error_degrees": rotation_error,
"translation_error": translation_error,
"ate": raw_ate,
"cammc": raw_cammc,
"scale_normalized_ate": normalized_ate,
"scale_normalized_cammc": normalized_cammc,
"away_response": away_response,
"return_translation_ratio": return_ratio,
"return_rotation_degrees": return_rotation,
"control_success": bool(success),
}
def main() -> None:
parser = argparse.ArgumentParser(description="Evaluate one SCoPE camera trajectory")
parser.add_argument("--target-pose", type=Path, required=True)
parser.add_argument("--predicted-pose", type=Path, required=True)
parser.add_argument("--output", type=Path, default=None)
args = parser.parse_args()
metrics = evaluate_trajectory(
np.load(args.target_pose, allow_pickle=False),
np.load(args.predicted_pose, allow_pickle=False),
)
rendered = json.dumps(metrics, indent=2)
print(rendered)
if args.output is not None:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered + "\n", encoding="utf-8")
if __name__ == "__main__":
main()