Skip to content

Instantly share code, notes, and snippets.

@polyfjord
Created August 3, 2025 10:51
Show Gist options
  • Select an option

  • Save polyfjord/4ed7e8988bdb9674145f1c270440200d to your computer and use it in GitHub Desktop.

Select an option

Save polyfjord/4ed7e8988bdb9674145f1c270440200d to your computer and use it in GitHub Desktop.
Batch script for automated photogrammetry tracking workflow
:: ================================================================
:: BATCH SCRIPT FOR AUTOMATED PHOTOGRAMMETRY TRACKING WORKFLOW
:: By polyfjord - https://youtube.com/polyfjord
:: ================================================================
:: USAGE
:: • Double-click this .bat or run it from a command prompt.
:: • Frames are extracted, features matched, and a sparse
:: reconstruction is produced automatically.
:: • Videos that have already been processed are skipped on
:: subsequent runs.
::
:: PURPOSE
:: This is a fully automated photogrammetry tracker for turning
:: videos into COLMAP sparse models with robust error handling,
:: clean directory setup, and clear ✖ / ✔ logging.
::
:: FOLDER LAYOUT (all folders sit side-by-side):
:: 01 COLMAP – Download the latest release from
:: https://github.com/colmap/colmap
:: and place colmap.bat (plus its dlls) here.
::
:: 02 VIDEOS – Put your input video files (.mp4, .mov, …) here.
:: All framerates and aspect ratios are supported.
::
:: 03 FFMPEG – Drop a **static build** of FFmpeg
:: (either ffmpeg.exe or bin\ffmpeg.exe) here.
::
:: 04 SCENES – The script creates one sub-folder per video
:: containing extracted frames, the COLMAP
:: database, sparse model, and TXT export.
::
:: 05 SCRIPTS – This batch file lives here.
::
:: ================================================================
@echo off
:: ---------- Resolve top-level folder (one up from this .bat) -----
pushd "%~dp0\.." >nul
set "TOP=%cd%"
popd >nul
:: ---------- Key paths -------------------------------------------
set "COLMAP_DIR=%TOP%\01 COLMAP"
set "VIDEOS_DIR=%TOP%\02 VIDEOS"
set "FFMPEG_DIR=%TOP%\03 FFMPEG"
set "SCENES_DIR=%TOP%\04 SCENES"
:: ---------- Locate ffmpeg.exe -----------------------------------
if exist "%FFMPEG_DIR%\ffmpeg.exe" (
set "FFMPEG=%FFMPEG_DIR%\ffmpeg.exe"
) else if exist "%FFMPEG_DIR%\bin\ffmpeg.exe" (
set "FFMPEG=%FFMPEG_DIR%\bin\ffmpeg.exe"
) else (
echo [ERROR] ffmpeg.exe not found inside "%FFMPEG_DIR%".
pause & goto :eof
)
:: ---------- Locate colmap.exe (skip the .bat) --------------------
if exist "%COLMAP_DIR%\colmap.exe" (
set "COLMAP=%COLMAP_DIR%\colmap.exe"
) else if exist "%COLMAP_DIR%\bin\colmap.exe" (
set "COLMAP=%COLMAP_DIR%\bin\colmap.exe"
) else (
echo [ERROR] colmap.exe not found inside "%COLMAP_DIR%".
pause & goto :eof
)
:: ---------- Put COLMAP’s dll folder(s) on PATH -------------------
set "PATH=%COLMAP_DIR%;%COLMAP_DIR%\bin;%PATH%"
:: ---------- Ensure required folders exist ------------------------
if not exist "%VIDEOS_DIR%" (
echo [ERROR] Input folder "%VIDEOS_DIR%" missing.
pause & goto :eof
)
if not exist "%SCENES_DIR%" mkdir "%SCENES_DIR%"
:: ---------- Count videos for progress bar ------------------------
for /f %%C in ('dir /b /a-d "%VIDEOS_DIR%\*" ^| find /c /v ""') do set "TOTAL=%%C"
if "%TOTAL%"=="0" (
echo [INFO] No video files found in "%VIDEOS_DIR%".
pause & goto :eof
)
echo ==============================================================
echo Starting COLMAP on %TOTAL% video(s) …
echo ==============================================================
setlocal EnableDelayedExpansion
set /a IDX=0
for %%V in ("%VIDEOS_DIR%\*.*") do (
if exist "%%~fV" (
set /a IDX+=1
call :PROCESS_VIDEO "%%~fV" "!IDX!" "%TOTAL%"
)
)
echo --------------------------------------------------------------
echo All jobs finished – results are in "%SCENES_DIR%".
echo --------------------------------------------------------------
pause
goto :eof
:PROCESS_VIDEO
:: ----------------------------------------------------------------
:: %1 = full path to video %2 = current index %3 = total
:: ----------------------------------------------------------------
setlocal
set "VIDEO=%~1"
set "NUM=%~2"
set "TOT=%~3"
for %%I in ("%VIDEO%") do (
set "BASE=%%~nI"
set "EXT=%%~xI"
)
echo.
echo [!NUM!/!TOT!] === Processing "!BASE!!EXT!" ===
:: -------- Directory layout for this scene -----------------------
set "SCENE=%SCENES_DIR%\!BASE!"
set "IMG_DIR=!SCENE!\images"
set "SPARSE_DIR=!SCENE!\sparse"
:: -------- Skip if already reconstructed -------------------------
if exist "!SCENE!" (
echo ↻ Skipping "!BASE!" – already reconstructed.
goto :END
)
:: Clean slate ----------------------------------------------------
mkdir "!IMG_DIR!" >nul
mkdir "!SPARSE_DIR!" >nul
:: -------- 1) Extract every frame --------------------------------
echo [1/4] Extracting frames …
"%FFMPEG%" -loglevel error -stats -i "!VIDEO!" -qscale:v 2 ^
"!IMG_DIR!\frame_%%06d.jpg"
if errorlevel 1 (
echo ✖ FFmpeg failed – skipping "!BASE!".
goto :END
)
:: Check at least one frame exists
dir /b "!IMG_DIR!\*.jpg" >nul 2>&1 || (
echo ✖ No frames extracted – skipping "!BASE!".
goto :END
)
:: -------- 2) Feature extraction ---------------------------------
echo [2/4] COLMAP feature_extractor …
"%COLMAP%" feature_extractor ^
--database_path "!SCENE!\database.db" ^
--image_path "!IMG_DIR!" ^
--ImageReader.single_camera 1 ^
--SiftExtraction.use_gpu 1 ^
--SiftExtraction.max_image_size 4096
if errorlevel 1 (
echo ✖ feature_extractor failed – skipping "!BASE!".
goto :END
)
:: -------- 3) Sequential matching --------------------------------
echo [3/4] COLMAP sequential_matcher …
"%COLMAP%" sequential_matcher ^
--database_path "!SCENE!\database.db" ^
--SequentialMatching.overlap 15
if errorlevel 1 (
echo ✖ sequential_matcher failed – skipping "!BASE!".
goto :END
)
:: -------- 4) Sparse reconstruction ------------------------------
echo [4/4] COLMAP mapper …
"%COLMAP%" mapper ^
--database_path "!SCENE!\database.db" ^
--image_path "!IMG_DIR!" ^
--output_path "!SPARSE_DIR!" ^
--Mapper.num_threads %NUMBER_OF_PROCESSORS%
if errorlevel 1 (
echo ✖ mapper failed – skipping "!BASE!".
goto :END
)
:: -------- Export best model to TXT ------------------------------
if exist "!SPARSE_DIR!\0" (
"%COLMAP%" model_converter ^
--input_path "!SPARSE_DIR!\0" ^
--output_path "!SPARSE_DIR!" ^
--output_type TXT >nul
)
echo ✔ Finished "!BASE!" (!NUM!/!TOT!)
:END
endlocal & goto :eof
@Fahadenheit
Copy link

Fahadenheit commented Mar 17, 2026

I think I've fixed it, apparently related to a colmap update. If anyone else has this open batch_reconstruct.bat in any editing program, and change

:: -------- 2) Feature extraction ---------------------------------
echo        [2/4] COLMAP feature_extractor …
"%COLMAP%" feature_extractor ^
    --database_path "!SCENE!\database.db" ^
    --image_path    "!IMG_DIR!" ^
    --ImageReader.single_camera 1 ^
    --SiftExtraction.use_gpu 1 ^
    --SiftExtraction.max_image_size 4096
if errorlevel 1 (
    echo        ✖ feature_extractor failed – skipping "!BASE!".
    goto :END
)

to

:: -------- 2) Feature extraction ---------------------------------
echo        [2/4] COLMAP feature_extractor …
"%COLMAP%" feature_extractor ^
    --database_path "!SCENE!\database.db" ^
    --image_path    "!IMG_DIR!" ^
    --ImageReader.single_camera 1 ^
    --FeatureExtraction.use_gpu 1 ^
    --SiftExtraction.max_image_size 4096
if errorlevel 1 (
    echo        ✖ feature_extractor failed – skipping "!BASE!".
    goto :END
)

SiftExtraction still exists for the image size i presume as if i switch that to FeatureExtraction aswell it breaks again. I'll update if that was the solution.

EDIT: this was the solution

did ur fix , still the same problem , did u do anything else ?

EDIT : the video title was in arabic , changed to english and it worked for me

@rodrigopolo
Copy link

For macOS using either the native Terminal app or Ghostty, a different approach, a script that can receive one or several videos and outputs the result in a folder with the same name as the input:

#!/usr/bin/env bash
# ================================================================
#  AUTOMATED PHOTOGRAMMETRY TRACKING WORKFLOW
# ================================================================
#  USAGE
#    chmod +x colmap_photogrammetry.sh
#    ./colmap_photogrammetry.sh video1.mp4 video2.mov …
#
#  OUTPUT
#    Each input file produces a sibling directory named after the
#    file (without extension), containing:
#      images/      – extracted frames
#      database.db  – COLMAP feature database
#      sparse/      – sparse reconstruction + TXT export
#
#  DEPENDENCIES (must be on $PATH)
#    ffmpeg   – brew install ffmpeg
#    colmap   – brew install colmap
# ================================================================

set -euo pipefail

# ----------------------------------------------------------------
# Helpers
# ----------------------------------------------------------------
log()  { echo "       $*"; }
die()  { echo "[ERROR] $*" >&2; exit 1; }

# ----------------------------------------------------------------
# Dependency check
# ----------------------------------------------------------------
for cmd in ffmpeg colmap; do
    command -v "$cmd" &>/dev/null || die "'$cmd' not found on \$PATH. Install via: brew install $cmd"
done

# ----------------------------------------------------------------
# Usage check
# ----------------------------------------------------------------
if [[ $# -eq 0 ]]; then
    echo "Usage: $(basename "$0") <video1> [video2 …]"
    exit 1
fi

NUM_THREADS="$(sysctl -n hw.logicalcpu 2>/dev/null || echo 4)"
TOTAL=$#
IDX=0

# ----------------------------------------------------------------
# Process one video
# ----------------------------------------------------------------
process_video() {
    local VIDEO="$1"

    [[ -f "$VIDEO" ]] || { log "✖ Not a file: \"$VIDEO\" – skipping."; return 0; }

    local BASE="${VIDEO%.*}"          # path minus extension
    local NAME="$(basename "$BASE")"  # filename only, for labels

    IDX=$(( IDX + 1 ))
    echo ""
    echo "[$IDX/$TOTAL] === Processing \"$(basename "$VIDEO")\" ==="

    # -------- Output directories ---------------------------------
    local SCENE="$BASE"
    local IMG_DIR="$SCENE/images"
    local SPARSE_DIR="$SCENE/sparse"

    # Overwrite: wipe and recreate
    rm -rf "$SCENE"
    mkdir -p "$IMG_DIR" "$SPARSE_DIR"

    # -------- 1) Extract every frame -----------------------------
    log "[1/4] Extracting frames …"
    if ! ffmpeg -loglevel error -stats -i "$VIDEO" -qscale:v 2 \
            "$IMG_DIR/frame_%06d.jpg"; then
        log "✖ ffmpeg failed – skipping \"$NAME\"."
        rm -rf "$SCENE"; return 0
    fi

    ls "$IMG_DIR"/*.jpg &>/dev/null || {
        log "✖ No frames extracted – skipping \"$NAME\"."
        rm -rf "$SCENE"; return 0
    }

    # -------- 2) Feature extraction ------------------------------
    log "[2/4] COLMAP feature_extractor …"
    if ! colmap feature_extractor \
            --database_path "$SCENE/database.db" \
            --image_path    "$IMG_DIR" \
            --ImageReader.single_camera 1 \
            --SiftExtraction.use_gpu 0 \
            --SiftExtraction.max_image_size 4096; then
        log "✖ feature_extractor failed – skipping \"$NAME\"."
        rm -rf "$SCENE"; return 0
    fi

    # -------- 3) Sequential matching -----------------------------
    log "[3/4] COLMAP sequential_matcher …"
    if ! colmap sequential_matcher \
            --database_path "$SCENE/database.db" \
            --SequentialMatching.overlap 15; then
        log "✖ sequential_matcher failed – skipping \"$NAME\"."
        rm -rf "$SCENE"; return 0
    fi

    # -------- 4) Sparse reconstruction ---------------------------
    log "[4/4] COLMAP mapper …"
    if ! colmap mapper \
            --database_path "$SCENE/database.db" \
            --image_path    "$IMG_DIR" \
            --output_path   "$SPARSE_DIR" \
            --Mapper.num_threads "$NUM_THREADS"; then
        log "✖ mapper failed – skipping \"$NAME\"."
        rm -rf "$SCENE"; return 0
    fi

    # -------- Export best model to TXT ---------------------------
    if [[ -d "$SPARSE_DIR/0" ]]; then
        colmap model_converter \
            --input_path  "$SPARSE_DIR/0" \
            --output_path "$SPARSE_DIR" \
            --output_type TXT &>/dev/null
    fi

    log "✔ Finished \"$NAME\"  ($IDX/$TOTAL)"
}

# ----------------------------------------------------------------
# Main loop
# ----------------------------------------------------------------
echo "=============================================================="
echo " Starting COLMAP on $TOTAL video(s) …"
echo "=============================================================="

for VIDEO in "$@"; do
    process_video "$VIDEO"
done

echo ""
echo "=============================================================="
echo " All jobs finished."
echo "=============================================================="

Similar scripts:
https://github.com/rodrigopolo/clis/tree/main/GaussianSplatting

@xamatic
Copy link

xamatic commented Mar 25, 2026

My configuration: [Colmap Nocuda Version: 4.0.2] [FFmpeg Essentials Version: 8.1]

After the latest update, my computer which does not have a supported GPU did not function as intended.
So after 2 hours of debugging, I have found a fix to the following errors:
[(Warn) No images with matches -> (Error) Failed to create any sparse model Γ£û mapper failed ΓÇô skipping "IMG"]
And also:
[qt.qpa.plugin: Could not find the Qt platform plugin "windows" in ""
This application failed to start because no Qt platform plugin could be initialized.
]

Fixing Qt platform plugin issues:

  1. Locate qwindows.dll inside your COLMAP install folder — it will be somewhere like \plugins\platforms\qwindows.dll
  2. Open System Properties → Environment Variables
  3. Under System Variables click New and add:
  4. Variable name: QT_QPA_PLATFORM_PLUGIN_PATH
  5. Variable value: the full path to the platforms folder containing qwindows.dll (e.g. C:\COLMAP\plugins\platforms)
  6. Click OK, then close and reopen any terminal windows for the variable to take effect

To fix issues revolving the non-cuda version (CPU Version):

  • :: -------- 2) Feature Extraction --------------------------------
    Change "--SiftExtraction.use_gpu 1 ^" to "--FeatureExtraction.use_gpu 0 ^"
    Change "--SiftExtraction.max_image_size 4096" to "--FeatureExtraction.max_image_size 4096"

  • :: -------- 3) Sequential matching --------------------------------
    add: "--FeatureMatching.use_gpu false ^"
    (optional) add: "--FeatureMatching.max_num_matches 16384" - Not 100% needed but just incase it gives a "gpu vram size too low" error

(I hope this helps someone)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment