DJI drones record GPS telemetry into a subtitle track inside the video file. Pull still frames out of that video and the location data does not come with them, which leaves photogrammetry software with nothing to work from.
This project takes the telemetry back out, matches it to the extracted frames, and writes real GPS EXIF tags. From there a web app carries the same footage through coverage review, COLMAP reconstruction, gaussian-splat training, and export.
DJI video ──ffmpeg──> frames ──CLI──> geotagged JPGs ──import──> map/review/plan
│
COLMAP SfM ──> gsplat training ──> splat viewer,
LAS/mesh/GeoJSON export
Every stage also produces WebODM/OpenDroneMap-ready output, so you can stop at any point and take the frames elsewhere.
New here? Start with the end-to-end workflow tutorial.
pip install -e ".[backend,dev]"ffmpeg and exiftool must be on your PATH (or passed with --ffmpeg / --exiftool). COLMAP
and a CUDA GPU are needed only for reconstruction. Full setup, including per-platform binaries and
GPU training, is in docs/INSTALL.md.
Extract frames, then geotag them:
ffmpeg -i flight.mp4 -vf fps=8 extracted/frame_%05d.jpg
drone-video-geotagger --video flight.mp4 --frames extracted --takeoff-altitude 236.94That writes geotagged copies to extracted_geotagged/, ready to upload to WebODM.
To use the web app instead:
./run.sh # macOS / Linux
run.bat # WindowsThen open http://localhost:5173. (dev.sh / dev.bat do the same, but also create the virtualenv
and install dependencies on first run.)
drone-video-geotagger reads the telemetry, interpolates a position for each frame, and writes the
EXIF tags.
| Flag | Description |
|---|---|
--video |
Source DJI video (MP4). Required. |
--frames |
Folder of extracted JPG frames. Required. |
--takeoff-altitude |
Takeoff altitude in metres above sea level. Required. |
--output |
Folder for the geotagged copies. Defaults to <frames>_geotagged. |
--srt |
Existing DJI SRT file. Extracted from the video when omitted. |
--frame-rate |
Frame extraction rate. Estimated from the SRT when omitted. |
--ffmpeg |
Path to the ffmpeg binary, if it is not on PATH. |
--exiftool |
Path to the exiftool binary, if it is not on PATH. |
--in-place |
Write tags into the source folder instead of making copies. |
Frame numbering uses the last number in each filename, so frame_00042.jpg and
DJI_0081_frame_42.jpg both resolve to frame 42. Files with no digits are skipped.
Alongside the geotagged images it writes frame_geotags.csv — per-frame index, time offset,
coordinates, altitudes, timestamp — for checking the alignment, plus exiftool_geotags.args, the
generated ExifTool argument file.
dvg-pipeline runs a whole geotag → ingest → coverage → reconstruct → export sequence from a YAML
job spec, with no web UI involved:
dvg-pipeline job.yml --dry-run # print the step plan, change nothing
dvg-pipeline job.yml--output-root and --log-dir override the matching spec keys, and -v turns on debug logging.
docs/examples/pipeline-job-spec.yml is an annotated spec.
The backend is a FastAPI service — 28 routers, self-documenting at /docs while running — covering
import, quality scoring, footprint geometry, coverage analysis, mission planning, the reconstruction
job pipeline, and the export formats. The frontend is a React app whose tabs follow the flight
itself: import and review frames, check coverage on a map, plan the next flight, run a
reconstruction, then explore the resulting splat and export it.
docs/USER-MANUAL.md is the full reference for both.
src/ CLI package (drone-video-geotagger, dvg-pipeline)
backend/ FastAPI app (API server, DB models, services)
frontend/ Vite + React frontend
tests/ pytest suite (tests/cli/ and tests/backend/)
packaging/ Windows installer build (PyInstaller + Inno Setup)
docs/ Documentation
data/, imports/, processed/, and exports/ are created at runtime and gitignored.
| Doc | What it covers |
|---|---|
| WORKFLOW.md | End-to-end tutorial: video → geotag → import → reconstruct → export |
| USER-MANUAL.md | Full reference for the backend, the frontend, and the splat trainer |
| INSTALL.md | System requirements, per-platform setup, deployment and auth |
| SETUP.md | GPU / CUDA / gsplat training setup |
| TROUBLESHOOTING.md | Exact error messages → causes → fixes |
| ARCHITECTURE.md | Component map, reconstruction state machine, design rules |
| WEBODM.md | WebODM round trip: upload, poll, download results |
| CESIUM-ION.md | Cesium ion tileset upload |
| VENDOR-PROJECT-IMPORT.md | Importing Pix4D / DroneDeploy projects |
| WINDOWS-INSTALLER.md | Building the distributable Windows installer |
| CHANGELOG.md | Release history |
A CPU-only image serves the backend and the built frontend from one container:
docker build -t telemetry-frame-mapper .
docker run --rm -p 8000:8000 \
-v "$PWD/data:/app/data" \
-v "$PWD/imports:/app/imports" \
-v "$PWD/processed:/app/processed" \
-v "$PWD/exports:/app/exports" \
telemetry-frame-mapperOpen http://localhost:8000. The image bundles ffmpeg, exiftool, and COLMAP. GPU training is
out of scope for it — use the manual setup in docs/SETUP.md.
pytest # CLI + backend
ruff check . # linter
cd frontend && npm test -- --run # frontend (vitest)Tests use inline fixture data and temporary paths, and every external binary is mocked — no real ffmpeg, exiftool, COLMAP, or GPU required.
See CONTRIBUTING.md. Report security issues through private advisories rather than public issues — see SECURITY.md.
Do not commit real drone videos, FlightRecord files, extracted frames, SRT files, or geotagged
images. .gitignore blocks them by default; run git status --short before pushing.
MIT — see LICENSE.