Estimate spherical pose and reconstruct multiple panoramas¶
This guide connects the Stable spherical-feature core to two Experimental public surfaces without exposing OpenCV objects:
central ERP arrays
→ panorai.features spherical matches
→ panorai.estimators pairwise R and unit t direction
→ panorai.reconstruction arbitrary-scale cameras and points
Every input panorama must represent a calibrated central camera. Translation scale is not observable from monocular bearings. A numerical result is not an automatic acceptance decision: inspect the pairwise quality report and the global reconstruction diagnostics.
Pairwise pose¶
Start with two decoded ERP arrays named panorama_a and panorama_b.
They may be NumPy HW/HWC arrays or supported Torch layouts; OpenCV
feature work is performed on CPU and the public feature results are NumPy.
from panorai.estimators import SphericalRelativePoseEstimator
from panorai.features import SphericalFeaturePipeline
pipeline = SphericalFeaturePipeline.for_relative_pose()
matches = pipeline.extract_and_match(
panorama_a,
panorama_b,
panorama_id_a="pano-a",
panorama_id_b="pano-b",
validity_mask_a=valid_a,
validity_mask_b=valid_b,
)
estimator = SphericalRelativePoseEstimator()
pose = estimator.estimate(matches.to_bearing_correspondences())
if pose is None or not pose.quality_report.accepted:
raise RuntimeError("pairwise geometry was not trustworthy")
R_b_from_a = pose.R
t_b_from_a_direction = pose.t
inliers = pose.inlier_mask
The convention is
pose.t has unit norm and no metric magnitude. Low parallax, poor angular
coverage, unstable consensus, rotation-only evidence, or ambiguous translation
orientation remain visible in pose.quality_report and
pose.degeneracy_reasons.
Multiview reconstruction¶
For three or more panoramas, extract the pairs that are expected to overlap. The IDs supplied during extraction identify observations when tracks are built.
from panorai.reconstruction import SphericalGlobalMapper
panoramas = {"p0": pano0, "p1": pano1, "p2": pano2}
pairs = (("p0", "p1"), ("p1", "p2"), ("p0", "p2"))
pairwise_matches = [
pipeline.extract_and_match(
panoramas[a],
panoramas[b],
panorama_id_a=a,
panorama_id_b=b,
)
for a, b in pairs
]
mapper = SphericalGlobalMapper()
result = mapper.reconstruct(matches=pairwise_matches)
if not result.success:
raise RuntimeError(result.failure_reasons)
pose_p1 = result.pose("p1")
R_world_to_p1 = pose_p1.R
center_p1_world = pose_p1.center
points_world = result.points_xyz
assert result.scale == "arbitrary"
The mapper estimates each pair once, admits quality-accepted edges, averages
rotations, builds conflict-free tracks, positions cameras and points, runs
spherical bundle adjustment, filters weak geometry, and independently checks
tracks observed by at least three panoramas. Insufficient geometry returns
success=False with explicit reasons and no fabricated partial poses.
If pairwise estimation is expensive and must be reused, make that stage explicit:
edges = mapper.estimate_pairwise(pairwise_matches)
result = mapper.reconstruct(edges=edges)
Exactly one of matches= and edges= is accepted.
Executable contract checks¶
The documentation runner executes the estimator boundary against real
SphericalFeatureMatches produced by the feature façade. A rejected or
degenerate pair may correctly return None; a returned pose must satisfy
the public rotation, translation-direction, and interface contracts:
from panorai.estimators import RelativePoseOptions, SphericalRelativePoseEstimator
pose_estimator = SphericalRelativePoseEstimator(
RelativePoseOptions(
min_num_trials=8,
max_num_trials=8,
local_optimization_steps=0,
minimal_solver_starts=8,
minimal_solver_max_nfev=10,
refinement_max_nfev=10,
stability_trials=0,
model_competition_trials=8,
random_seed=7,
)
)
pairwise_pose = pose_estimator.estimate(
feature_matches.to_bearing_correspondences()
)
if pairwise_pose is not None:
assert pairwise_pose.R.shape == (3, 3)
assert pairwise_pose.t.shape == (3,)
assert pairwise_pose.describe()["translation"] == "unit-direction-only"
It also verifies that insufficient multiview geometry is an explicit result with no fabricated points:
from panorai.reconstruction import SphericalGlobalMapper
mapper = SphericalGlobalMapper(relative_pose_estimator=pose_estimator)
insufficient = mapper.reconstruct(
matches=(), panorama_ids=("pano-a", "pano-b", "pano-c")
)
assert not insufficient.success
assert insufficient.points_xyz.shape == (0, 3)
assert insufficient.failure_reasons
What to inspect¶
Before consuming a map, inspect at least:
pairwise
quality_report.acceptedand rejection reasons;admitted, rotation-filtered, and translation-filtered edges;
active tracks per panorama and multiview corroboration;
angular reprojection percentiles and triangulation support;
failure_reasonsand the arbitrary-scale contract.
See Spherical relative-pose API for pairwise policy and Global spherical reconstruction for frames, gauges, numerical stages, native acceleration, and current validation limits. For online processing, continue with Run spherical visual SLAM.