Reconstruct a surface from registered spherical views

Use this route when camera poses and physical scale are already known. It does not estimate registration. Start with at least three central 2:1 ERPs for the candidate reviewed profile.

1. Build the registered scene

import numpy as np

from panorai import EquirectangularImage
from panorai.reconstruction import (
    RegisteredSphericalScene,
    RegisteredSphericalView,
    SphericalCameraPose,
)

views = []
for panorama_id, path, R_world_to_camera, center_world in camera_records:
    views.append(
        RegisteredSphericalView(
            image=EquirectangularImage.open(path),
            pose=SphericalCameraPose(
                panorama_id=panorama_id,
                rotation_world_to_panorama=np.asarray(R_world_to_camera),
                center_world=np.asarray(center_world),
            ),
        )
    )

scene = RegisteredSphericalScene(views=tuple(views), units="m")

validity= can add a boolean HW mask to a view. It is intersected with the image container’s own support and validity. Do not encode missing range or image samples as zero.

2. Run the candidate reviewed profile

from panorai.applications import SphericalDenseReconstruction

method = SphericalDenseReconstruction.reviewed_profile(
    voxel_size=0.05,
    bounds=None,
)
result = method(scene)

if result.accepted:
    mesh = result.require_success().surface
    print(mesh.vertices_xyz.shape, mesh.faces.shape)
else:
    print(result.reasons)

With bounds=None, bounds come from accepted range points and expand by the TSDF truncation distance. The method fails rather than silently increasing the voxel size when the derived volume exceeds max_voxels.

The name reviewed_profile identifies frozen parameters for future promotion. The application still has Experimental maturity until its external scientific and installed-wheel gates pass.

3. Inspect or alter the composition

pipeline = method.as_pipeline()
print(pipeline.describe())

run = pipeline.run(
    scene,
    retain=("estimate_multi_view_ranges", "fuse_tsdf"),
)
surface = run.output.require_success().surface

The stages are public:

from panorai.reconstruction import (
    BuildRegisteredReferenceSets,
    EstimateMultiViewRanges,
    FuseTSDF,
    ExtractSurface,
    ColorizeSurface,
    ValidateDenseReconstruction,
)

FuseTSDF also accepts an external MultiViewRangeSet. That is the extension point for another range estimator; it does not change the TSDF, frame, support or validity contracts.

4. Evaluate against a reference surface

from panorai.evaluation import evaluate_surface_reconstruction

metrics = evaluate_surface_reconstruction(
    result.require_success().surface,
    reference_mesh,
    distance_threshold=2 * method.voxel_size,
)
print(metrics.accuracy_median, metrics.recall, metrics.fscore)

The evaluator uses bidirectional vertex-to-triangle distances. Freeze the distance threshold and held-out scene list before inference. Dataset download, split selection and experiment orchestration stay outside the library.

Aggregate only after every scene has been evaluated with that same frozen threshold:

from panorai.evaluation import aggregate_surface_reconstruction

report = aggregate_surface_reconstruction(
    {"scene-001": metrics_001, "scene-002": metrics_002}
)
print(report.per_scene["scene-001"].fscore, report.aggregate["fscore"])

The aggregate is the unweighted median of scene-level values, so a finely tessellated scene cannot dominate a corpus result.

5. Optional Open3D conversion

from panorai.pcd import surface_mesh_to_open3d

o3d_mesh = surface_mesh_to_open3d(mesh)

Open3D is imported only by this adapter. It is not the canonical surface contract and is not required to import reconstruction or geometry.