Codync
무료 오픈소스 1:1 대안, Grok Bot과 Muse 대신
CPU 전용 파이썬·OpenCV 파이프라인으로 핸드폰 영상을 구면 파노라마와 Three.js 뷰어로 변환
A CPU-only Python and OpenCV pipeline that turns handheld phone video into an equirectangular panorama and a self-contained Three.js viewer.
개발 도구Python · HTMLGitHub @Kronbii온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기CPU-only Python and OpenCV pipeline that turns a handheld phone sweep into a 360° equirectangular panorama with a Three.js viewer. ORB + RANSAC, pure-rotation model, temporal smoothing. Live demo: 360.ramikronbi.com
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗I built a CPU-only Python and OpenCV pipeline that turns a handheld phone sweep into an equirectangular 360° panorama, plus a browser viewer to look around in it. Every geometric decision is a module you can read, not a single "stitch" call.
Open the live viewer: 360.ramikronbi.com
309 frames of a phone video, one handheld sweep, 4096 × 2048 out.
A panorama looks simple only after it works. Before that it is a chain of small geometric errors: a few bad matches tilt the camera estimate, a fraction of a degree of pitch turns a ceiling edge into a staircase, and a step sideways adds parallax that no homography can explain. General-purpose stitchers hide those decisions behind one call, which makes it hard to say why a panorama tilts, ghosts or drifts. I wanted each one out in the open. No gyroscope, no tripod, no stitching app: the geometry comes from image features and a pure-rotation camera model.
A 309-frame handheld sweep of a living room, every second frame of a phone video, matched at full resolution on a Ryzen 7 5800H, CPU only:
| Frames in | 309 |
| Recovered sweep | 333° |
| Median RANSAC inliers per pair | 921 |
| Pairs recovered by interpolation | 15 of 308 |
| Sphere actually imaged | 36.9% |
| Output | 4096 × 2048, equirectangular |
| End-to-end time | ~2 minutes (19 s matching, 96 s warping) |
| Smoothing | pitch wobble 13.5× lower, roll 5.2× lower |
flowchart LR
A[Extract<br/>frames + intrinsics] --> B[Match<br/>ORB, ratio test, RANSAC]
B --> C[Rotate<br/>R = K⁻¹HK, SVD]
C --> D[Smooth<br/>moving average]
D --> E[Warp<br/>to equirectangular]
E --> F[Blend and fill]
F --> G[View<br/>Three.js]
Extract. Frames come out of the video by interval, count, frame rate or measured motion (video_utils.py). Focal length comes from EXIF when it's there, otherwise from a stated field of view or a calibration file (intrinsics.py).
Match. ORB features between neighbouring frames, Lowe's ratio test, then RANSAC. If a pair fails, the matcher tries across the gap and interpolates the missing step instead of silently dropping the frame (features.py).
Rotate. Under a pure-rotation model the relative rotation is R = K⁻¹HK. Noise means that matrix is rarely a true rotation, so SVD projects it back onto one before the rotations are chained into global orientations (rotation.py).
Smooth. Independent pairwise estimates leave a few tenths of a degree of pitch and roll error on every frame, enough to turn long straight edges into a staircase. A moving average over the chained rotations damps it. It is deliberately simple: not bundle adjustment, not loop closure (TEMPORAL_SMOOTHING.md).
Warp. Each output pixel is a world direction; rotate it into the camera, project it onto the image plane, sample (warp_sphere.py).
Blend and fill. Overlaps are blended (multiband, feather, sharp or none) and whatever the sweep never saw is inpainted from its surroundings (blend.py). Filling is not reconstruction.
The full method, with the maths, is in TECHNICAL.md.
git clone https://github.com/Kronbii/360-spherical-stitching.git
cd 360-spherical-stitching
pip install -r requirements.txt
Everything is driven by one YAML file. config.yaml is a working example; point video (or input_dir for a folder of stills) at your capture and set output_dir, then:
python run.py config.yaml
The run prints a rotation summary as it goes and writes panorama.jpg, the extracted frames, the resolved intrinsics.json, the exact config.json it used, and viewer/index.html to output_dir. Some browsers won't load the panorama over file://, so serve the folder:
cd <output_dir> && python -m http.server 8000 # then open localhost:8000/viewer/
Every option and its default is in USAGE.md. Tests:
python run_tests.py # or: pytest -q
The first version has no bundle adjustment and no loop closure, so drift is damped rather than corrected. The second version, on the imu-bundle-adjustment branch, adds both. A gyro, integrated between frames, predicts each camera's orientation. That prediction narrows where a feature can land in the next frame, so matching only searches a small window, and it says which frames a full turn apart should overlap, which is how loop closures are found without comparing every image with every other. Each pair is fitted with a rotation-only model, then bundle adjustment solves every orientation and one shared focal length together, with a robust loss, starting from the gyro.
So far it has only run on simulated data: frames rendered from a known panorama as a camera turns 396°, and a 200 Hz gyro with bias, drift and noise. On a simulated 73-frame sweep, against ground truth:
| Orientation source | Mean rotation error |
|---|---|
| Gyro only | 2.01° |
| Pairwise chaining | 1.35° |
| Bundle adjusted | 0.04° |
Bundle adjustment also recovers a focal length that started 3.5% short to within 0.02%. Without the global solve the rotation-only chain is worse than the first version's, so the bundle adjustment is what earns the improvement.
Simulated: the gyro drifts to 4.3° by the last frame and pairwise chaining ends 2.1° off; bundle adjustment stays under 0.14° throughout.
Simulated: where the sweep meets its start, chaining leaves a 2.09° break; after bundle adjustment it is 0.03°.
Simulated sweep, still frame. The animation is on the project page.
The simulation leaves out lens distortion, rolling shutter, parallax and the camera-to-gyro time offset, and a real capture would need a synced sensor log. run.py on main still uses the first version.
run.py entry point: load the config, extract frames, run the pipeline
config.yaml example configuration
src/ one module per stage; src/pipeline.py runs them in order
src/viewer/ the Three.js viewer template
tests/ pytest suite (186 tests)
docs/ the demo site at 360.ramikronbi.com
showcase/ sample panorama and capture video
TECHNICAL.md the method
TEMPORAL_SMOOTHING.md why and how the rotations are smoothed
USAGE.md every config option
Rotation only. The camera is assumed to turn about its optical centre. Move it sideways past a nearby object and the scene no longer has a single homography.
A phone-height sweep doesn't cover a sphere. One horizontal pass sees a band around you, 36.9% of the sphere on the sample run. The poles are filled, not captured.
Texture drives the estimate. Blank walls and repeating patterns give few usable matches and can force interpolation.
No bundle adjustment or loop closure on main. Drift is damped, not corrected. The second version adds both, but has only been validated in simulation.
No exposure matching. Brightness changes stay visible across seams.
Known issues. The smoothing window clamps at the ends of the sequence, so on a clip that finishes mid-motion the last frame can snap forward (5.4° on the sample run); reflecting the sequence at the edges would fix it. A few tests are stale: they pass CalibrationData(source=...), a keyword the dataclass no longer takes.
imu-bundle-adjustment branchMIT License, see LICENSE. Contributions are welcome, see CONTRIBUTING.md.
Built by Rami Kronbi — ramikronbi.com
View. A self-contained Three.js page is written next to the panorama.