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.
Drag to look · scroll to zoom
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.

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.

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.

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.

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.
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.
smoothedraw
smoothedraw
smoothedraw
smoothedraw
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:
| Angle | Raw wobble | Smoothed | |
|---|---|---|---|
| Pitch | 0.085° | 0.006° | 13.5× quieter |
| Roll | 0.123° | 0.024° | 5.2× quieter |
| Yaw | 0.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.
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