360° Spherical Stitching

ORB features · pure-rotation model · equirectangular output

Sweep a room.Get a sphere.

Point your phone at the scene, rotate once, and this pipeline turns the frames into a seamless 360° panorama — geometry recovered from features alone, no gyroscope, no tripod, no stitching app.

309frames in
333°recovered sweep
4096×2048equirect out
921inliers / pair
Equirectangular panorama of a living room, stitched from 309 phone frames

Drag to look · scroll to zoom

The output of a single handheld sweep, projected back to a sphere in the browser. The rulers read true yaw and pitch — the same angles the pipeline solved for. Every pixel here came out of panorama.jpg.

01 — Pipeline

Four stages from frames to sphere

The run below is real: 309 frames pulled from a phone video, matched pair by pair, chained into global rotations, then inverse-mapped onto an equirectangular canvas. Numbers are from that run, not a demo.

A single frame extracted from the source video: a living room with a teal sofa

One frame off the phone

Frames come straight out of the video — every 2nd frame here, which lands about 1.1° apart. Focal length comes from EXIF when it's there and from hfov_deg when it isn't; this clip runs on a 42° horizontal field of view, fx = fy = 1475 px.

sourceIMG_1480_2.MOV
methodinterval, every 2nd
frame size1080×1920
step1.08° / frame
Two consecutive frames side by side with teal lines linking matched ORB features and amber lines showing rejected outliers

Features, then a homography

ORB finds 3,000 keypoints per frame; Lowe's ratio test keeps 1,574 of the pairings, and RANSAC certifies 1,530 of those as one consistent homography — the teal lines. Amber lines are the rejects it threw out. Notice the blank roller blind contributes almost nothing: texture carries the estimate, flat walls freeload.

pairframe 150 → 151
keypoints3,000
ratio test1,574 kept
ransac inliers1,530
Partial equirectangular canvas with a horizontal band of the room filled in and the rest black

Rotation only, projected out

Each homography becomes a pure rotation via R = K⁻¹HK, orthonormalized with SVD so it stays a real rotation, then chained frame to frame. Every frame is inverse-mapped onto the sphere at its own angle. One frame alone covers 4.0% of the canvas; this partial sweep is 26 of them.

modelH = K·R·K⁻¹
one frame4.0% of sphere
shown here26 frames, 14.2%
full run36.9% of sphere
The finished equirectangular panorama, a full 360 degree view of the living room

All 309 placed

The rest of the sweep fills the band, overlaps resolve by the chosen blend mode — sharp, feather, multiband, or none — and the poles a phone-height sweep never saw get filled from their surroundings. That's the image in the viewer above.

output4096×2048 jpg
coverage36.9% imaged
recovered pairs15 interpolated
blendconfigurable

02 — The hard part

Small angle errors, loud results

Each pair's rotation is solved independently, so every frame arrives with its own few tenths of a degree of pitch and roll error. Across a 42° field of view at 4096 px wide that's several pixels of vertical offset per frame — and since neighbours overlap heavily, every seam leaves a step. Drag the dividers: left is smoothed, right is the raw chain.

Panorama band built from raw rotations Panorama band built from smoothed rotations smoothedraw
Same 309 frames, same intrinsics, same matcher — the only difference is whether a moving window runs over the chained rotations before warping. Cropped to the band the sweep actually imaged, +31° down to −37° elevation.
Curtain rail crop, raw rotations, edges broken into steps Curtain rail crop, smoothed rotations, edges continuous smoothedraw
The rail and the sofa's gold trim break into rungs, one per seam.
Doorway crop, raw rotations Doorway crop, smoothed rotations smoothedraw
A dark lintel shears into blocks where frames disagree.
Ceiling coving crop, raw rotations Ceiling coving crop, smoothed rotations smoothedraw
Even low-texture coving arrives serrated.

What the window fixes

A moving average over the chained rotations — window 17 on this run — collapses the frame-to-frame wobble while leaving the sweep itself alone. Measured on the run above:

AngleRaw wobbleSmoothed
Pitch0.085°0.006°13.5× quieter
Roll0.123°0.024°5.2× quieter
Yaw0.209°0.278°left alone

Wobble = std. dev. of the second difference, sweep removed

And what it costs

Averaging isn't free. Failed pairs — 15 of them here — get filled from their neighbours before the window ever runs, so a bad estimate spreads a little before it's damped. And at the ends of the sequence the window has nothing to average against: on this clip the last frames get dragged back and the final one snaps forward by 5.4°, against a largest honest step of 1.9°.

Fixes worth making: reflect the sequence at the edges instead of clamping the window, and taper the endpoints instead of exempting them.

03 — The sweep

Every frame knows where it points

These are real frames from the run, evenly sampled across the sweep, each labelled with the yaw the pipeline solved for it. Pick one and the viewer above turns to face it.

frame solved yaw pitch click to aim the viewer

Handheld, and it shows

The sweep starts almost stationary — 0.03° per frame for the first dozen — then settles to a steady 1.08° per frame and finishes mid-motion at about 1.2°. Pitch wanders over 7.2° across the sweep because an arm is not a tripod; that wander is exactly what the rotation chain has to absorb.

When matching fails

15 of 308 pairs never reached the 200-inlier threshold — blank wall, blown highlight, too fast a turn. Rather than dropping those frames, the pipeline borrows the rotation from a neighbour, or averages the two around it, and carries on.

04 — Run it

Your own sweep, four commands

# 1 — get it
git clone https://github.com/Kronbii/360-spherical-stitching.git
cd 360-spherical-stitching
pip install -r requirements.txt

# 2 — point config.yaml at your video or photos
# 3 — stitch
python run.py config.yaml

# 4 — look around
open output/<name>/viewer/index.html

A video gets its frames pulled automatically; a folder of stills works just as well. Every knob lives in one YAML file, and the run prints a rotation summary as it goes so you can see the sweep add up.

Docs
USAGE.md — every option, with defaults
Method
TECHNICAL.md — the geometry, written out
Smoothing
TEMPORAL_SMOOTHING.md — window sizing
Needs
Python 3.12, OpenCV, NumPy. No GPU.
video: ./IMG_1480_2.MOV
output_dir: ./output/livingroom

video_extraction:
  method: interval     # uniform · fps · motion
  frame_interval: 2

matching:
  match_full_res: true
  min_inliers: 200
  rotation_smoothing_window: 17

intrinsics:
  hfov_deg: 42

output:
  pano_width: 4096