Local-first desktop app for picking individual still frames from drone videos, aligning them against GPX or FIT tracks, and exporting the selected images with timestamp and GPS metadata.
The main workflow is visual:
- load a video
- optionally load a GPX or FIT track
- scrub to the exact frames you want
- align video time against GPX time when needed
- export only the selected photos
No uploads or cloud processing are required.
This repository is an MVP with a working desktop UI and CLI.
What already works:
- local video loading
- optional GPX/FIT loading
- visual frame selection in a PySide6 UI
- play/pause, scrubbing, and
1/5frame stepping VideoandGPXtimestamp authority modes- derived sync offset and export-only time shift controls
- GPX/FIT cursor alignment on a zoomable OpenStreetMap view
- JPG export for standard footage
- TIFF export for wide-gamut / HDR-like footage
- EXIF timestamp writing and GPS tagging through
exiftool - JSON or CSV manifest export
- remembered last output folder in the UI
Current limitations:
- playback performance depends heavily on Qt Multimedia codec support on the local machine
- GPX matching uses nearest-point matching, not interpolation
- macOS is the main tested platform right now
- the map baselayer uses online OpenStreetMap tiles, so internet access is needed for the basemap
- Local-first workflow
- no uploads
- no cloud dependency
- local file selection and local export
- Visual still selection
- video scrubber
- play / pause toggle
-1,+1,-5,+5frame stepping- marker-based selection of individual photos
- Flexible sync workflow
offsetrelative-startabsolute-video- choose whether
VideoorGPXis the authoritative timeline - second-level sync offset
- separate export-only time shift from
-5 hto+5 h
- Map-based positioning
- zoomable OpenStreetMap-based track view
- current video position on the track
- track cursor scrubbing
- align current frame to the track cursor
- Export pipeline
- automatic export format selection
jpgfor standard footagetifffor detected wide-gamut / HDR-like footage- EXIF timestamp writing
- optional GPS metadata when a track is loaded
- JSON or CSV manifest
- custom filename middle segment
- Python 3.11+
- PySide6
- ffmpeg / ffprobe
- fitparse
- gpxpy
- exiftool
- typer
- rich
On macOS with Homebrew:
brew install ffmpeg exiftoolgit clone https://github.com/derfrankie/drone-frame-exporter.git
cd drone-frame-exporterpython3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -e ".[dev]"drone-frame-extractor --help
.venv/bin/pytestAfter pip install -e ., the easiest way to launch the GUI is:
drone-frame-extractor uiYou can also launch it directly through Python:
python -m app.main uiTo open the UI with files preloaded:
drone-frame-extractor ui \
--video /path/to/video.mp4 \
--gpx /path/to/track.gpx \
--out /path/to/output--gpx accepts .gpx and .fit tracks and is optional. If no track is loaded, exports still work and use the resolved video timestamp plus the configured sync offset. Export Time Shift still affects only the written export time, not the map matching.
- choose a video
- optionally choose a GPX or FIT file
- choose an output folder
The UI remembers the last output folder you used.
The Sync panel exposes:
Modeoffsetrelative-startabsolute-video
Timestamp AuthorityVideoGPX
Derived Offset- fine adjustment in seconds
- this is the computed or manually refined time delta between the two timelines
- this controls sync, not export-only clock correction
Export Time Shift- optional export-only correction from
-5 hto+5 h - default is
0 h - does not move the GPX mapping on the map
- optional export-only correction from
Relative Start- used only in
relative-startmode
- used only in
- drag the playhead
- use
Play/Pause - use
-1 Frame,+1 Frame,-5 Frames,+5 Frames - click
Add Current Frameto mark a photo candidate
Only marked frames are exported.
When a GPX or FIT file is loaded, the map shows:
- the track on a real OpenStreetMap basemap
- the current resolved video position
- the GPX cursor position
- marker locations for selected photos
You can scrub on the track and use Sync Current Video Frame To GPX Cursor to align the current frame against the GPX cursor.
The intended semantics are:
Timestamp Authority = GPX- GPX is treated as the correct timeline
- syncing derives the video start relative to the chosen GPX point
Timestamp Authority = Video- the video timestamp is treated as the correct timeline
- syncing derives the required offset against the GPX track
The Export panel supports:
JPG Quality- default
10
- default
Export Format- automatically switches to
tifffor detected wide-gamut / HDR-like sources - otherwise defaults to
jpg
- automatically switches to
Manifestjsonorcsv
Filename Middle- inserted between the original filename stem and the export timestamp
Example output filename:
HOVER_X1PROMAX_0080_hero_2024-12-31_13-40-55.tiff
Example manifest filename:
HOVER_X1PROMAX_0080_hero_export.json
The CLI is useful for inspection, scripted export, and debugging the sync pipeline.
drone-frame-extractor inspect-video --video /path/to/video.mp4drone-frame-extractor inspect-track --track /path/to/track.gpxinspect-gpx --gpx /path/to/track.gpx remains available as a compatibility alias and also accepts .fit files.
drone-frame-extractor export \
--video /path/to/video.mp4 \
--gpx /path/to/track.fit \
--out /path/to/output \
--times 12.5,44.2,91.0 \
--sync-mode offset \
--reference-mode video-first \
--shift-hours 0 \
--offset-seconds 37 \
--export-format jpg \
--jpg-quality 10 \
--manifest-format jsondrone-frame-extractor export \
--video /path/to/video.mp4 \
--out /path/to/output \
--times 12.5,44.2 \
--sync-mode offset \
--shift-hours 0 \
--offset-seconds 37Without a GPX file, the exported image still receives DateTimeOriginal, but no GPS fields are written.
drone-frame-extractor export \
--video /path/to/video.mp4 \
--gpx /path/to/track.gpx \
--out /path/to/output \
--times 8.0,12.0 \
--sync-mode relative-start \
--reference-mode gpx-first \
--shift-hours 0 \
--start-time 2025-06-01T08:30:00Zdrone-frame-extractor preview-map \
--video /path/to/video.mp4 \
--gpx /path/to/track.gpx \
--out preview/video-placement.html \
--times 4.0,18.5,33.2 \
--sync-mode offset \
--reference-mode video-first \
--shift-hours 0 \
--offset-seconds 37This project is designed for the reality that drone-camera timestamps can be wrong.
The important rule is:
- with GPX loaded, the sync result determines the map-matched time
- without GPX, the sync result falls back to the resolved video time plus sync offset
Export Time Shiftis applied afterward only to the written export timestamp
Video metadata such as creation_time, encoded_date, or tagged_date is mainly used as an initial reference, not as absolute truth.
The app inspects the video stream and automatically prefers tiff when footage looks like wide-gamut / HDR-like material, for example:
10-bitvideoBT.2020primariesHLG/arib-std-b67PQ/smpte2084
For standard footage such as typical 8-bit AVC / BT.601 or BT.709 files, jpg remains the default.
For each exported still, the app can write:
DateTimeOriginal- GPS latitude / longitude
- optional altitude
The manifest also includes values such as:
- source video filename
- frame time in the source video
- resolved export timestamp
- GPX timestamp, if available
- latitude / longitude / altitude, if available
- sync mode
- offset seconds
- export time shift
drone-frame-exporter/
README.md
pyproject.toml
src/
app/
main.py
services/
ui/
core/
export.py
gpx.py
metadata.py
sync.py
video.py
tests/
Run tests:
.venv/bin/pytestRun the UI directly from source:
PYTHONPATH=src python -m app.main uiBuild a local .app bundle with py2app:
source .venv/bin/activate
python -m pip install -e ".[macos-app]"
python setup.py py2appThe built app will be placed in:
dist/Drone Frame Extractor.app
Notes:
- the bundle uses
src/app/macos_bundle.pyas the GUI entrypoint - Qt WebEngine helper/resource paths are configured at runtime in
src/app/ui/launcher.py - the basemap still requires internet access for OpenStreetMap or satellite tiles
ffmpegandexiftoolare still expected on the target Mac unless you bundle them separately
- better playback performance for difficult codecs
- timeline thumbnails
- GPX interpolation between track points
- packaging as a distributable macOS app bundle
- more visible timestamp-source diagnostics in the UI