Commit 6eb3d420 authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: Add v88_to_colmap_dense.sh — full pipeline from v88 to fused.ply

parent 978388ba
#!/usr/bin/env bash
# =============================================================================
# v88_to_colmap_dense.sh — Elphel LWIR-16 v88 folder → COLMAP dense 3D model
#
# Copyright (C) 2026 Elphel, Inc.
# SPDX-License-Identifier: GPL-3.0-or-later
# =============================================================================
#
# Full pipeline from a v88 processing folder to a dense point cloud:
#
# Step 1 Python: inverse-shift TERRAIN-DISP-MERGED frames using ELEV_GND
# → COLMAP text sparse model (cameras.txt / images.txt / PNGs)
#
# Step 2 colmap model_converter: text → binary sparse model
#
# Step 3 colmap image_undistorter: prepare dense workspace
#
# Step 4 Python: write sequential patch-match.cfg (±N temporal neighbours)
# (required because we skip COLMAP feature matching)
#
# Step 5 colmap patch_match_stereo: GPU PatchMatch depth maps
# (with filter pass so stereo_fusion accepts the result)
#
# Step 6 colmap stereo_fusion: merge depth maps → fused.ply point cloud
#
# Usage:
# bash scripts/v88_to_colmap_dense.sh \
# --v88 /path/to/.../1763232146_700470/v88 \
# --out /tmp/colmap_run
#
# Optional:
# --neighbours N source frames each side for PatchMatch (default: 10)
# --depth_min M minimum depth in metres (default: auto)
# --depth_max M maximum depth in metres (default: auto)
# --hfov deg camera horizontal FoV in degrees (default: 32)
#
# Prerequisites:
# • conda-forge COLMAP with CUDA in PATH:
# export PATH="/home/elphel/miniforge3/bin:$PATH"
# • Python 3 with numpy, scipy, Pillow (system or conda env)
# • foliage repo checked out; script must be run from its root:
# bash scripts/v88_to_colmap_dense.sh ...
# =============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
PYTHON="${PYTHON:-/usr/bin/python3}"
# ── Defaults ─────────────────────────────────────────────────────────────────
V88=""
OUT=""
NEIGHBOURS=10
DEPTH_MIN=""
DEPTH_MAX=""
HFOV=32.0
# ── Argument parsing ──────────────────────────────────────────────────────────
usage() {
echo "Usage: $0 --v88 <path> --out <dir> [--neighbours N] [--depth_min M] [--depth_max M] [--hfov deg]" >&2
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--v88) V88="$2"; shift 2 ;;
--out) OUT="$2"; shift 2 ;;
--neighbours) NEIGHBOURS="$2"; shift 2 ;;
--depth_min) DEPTH_MIN="$2"; shift 2 ;;
--depth_max) DEPTH_MAX="$2"; shift 2 ;;
--hfov) HFOV="$2"; shift 2 ;;
*) usage ;;
esac
done
[[ -n "$V88" && -n "$OUT" ]] || usage
# ── Locate input files ────────────────────────────────────────────────────────
# Timestamp = name of the parent directory of v88
TS="$(basename "$(dirname "$V88")")"
TIFF="$V88/${TS}-TERRAIN-DISP-MERGED.tiff"
XML="$V88/${TS}-INTERFRAME.corr-xml"
ELEV="$V88/${TS}-ELEV_GND.tiff"
for f in "$TIFF" "$XML" "$ELEV"; do
[[ -f "$f" ]] || { echo "ERROR: missing $f" >&2; exit 1; }
done
echo "=== Input files verified ==="
echo " TIFF: $TIFF"
echo " XML: $XML"
echo " ELEV_GND: $ELEV"
# ── Auto-detect depth range from ELEV_GND if not specified ───────────────────
if [[ -z "$DEPTH_MIN" || -z "$DEPTH_MAX" ]]; then
read DEPTH_MIN DEPTH_MAX < <($PYTHON - <<PYEOF
import numpy as np
from PIL import Image
elev = np.array(Image.open("$ELEV"), dtype=np.float32)
z = -elev # positive AGL depth
# Add 40 % margin around the observed range
lo = z.min() * 0.6
hi = z.max() * 1.4
print(f"{lo:.0f} {hi:.0f}")
PYEOF
)
echo " Auto depth range: ${DEPTH_MIN}${DEPTH_MAX} m AGL"
fi
mkdir -p "$OUT"
# ── Step 1: Perspective remap → COLMAP text sparse model ─────────────────────
COLMAP_TEXT="$OUT/colmap_text"
echo ""
echo "=== Step 1: Perspective remap ==="
$PYTHON "$REPO_ROOT/scripts/elphel_to_colmap_perspective.py" \
--tiff "$TIFF" \
--xml "$XML" \
--elev_gnd "$ELEV" \
--out "$COLMAP_TEXT" \
--hfov "$HFOV"
# ── Step 2: Convert text sparse model → binary ───────────────────────────────
DENSE_ROOT="$OUT/dense_workspace"
echo ""
echo "=== Step 2: Convert sparse model text → binary ==="
mkdir -p "$DENSE_ROOT/sparse"
colmap model_converter \
--input_path "$COLMAP_TEXT/sparse/0" \
--output_path "$DENSE_ROOT/sparse" \
--output_type BIN
# ── Step 3: Undistort images (prepares dense/ workspace structure) ────────────
echo ""
echo "=== Step 3: Undistort images ==="
colmap image_undistorter \
--image_path "$COLMAP_TEXT/images" \
--input_path "$DENSE_ROOT/sparse" \
--output_path "$DENSE_ROOT/dense" \
--output_type COLMAP
# ── Step 4: Write sequential patch-match.cfg ─────────────────────────────────
echo ""
echo "=== Step 4: Write patch-match.cfg (neighbours = ±${NEIGHBOURS}) ==="
$PYTHON - <<PYEOF
import os
from pathlib import Path
images_dir = Path("$DENSE_ROOT/dense/images")
images = sorted(os.listdir(images_dir))
N = $NEIGHBOURS
cfg_lines = []
for i, img in enumerate(images):
sources = [images[j] for j in range(max(0, i-N), min(len(images), i+N+1)) if j != i]
cfg_lines.append(img)
cfg_lines.append(', '.join(sources))
cfg_path = Path("$DENSE_ROOT/dense/stereo/patch-match.cfg")
cfg_path.write_text('\n'.join(cfg_lines) + '\n')
print(f" Wrote {len(images)} entries to {cfg_path}")
PYEOF
# ── Step 5: PatchMatch stereo (GPU) ──────────────────────────────────────────
echo ""
echo "=== Step 5: PatchMatch stereo depth=[${DEPTH_MIN}, ${DEPTH_MAX}] m ==="
echo " (this takes ~20-30 min on RTX 5060 Ti for 185 frames)"
colmap patch_match_stereo \
--workspace_path "$DENSE_ROOT/dense" \
--workspace_format COLMAP \
--PatchMatchStereo.depth_min "$DEPTH_MIN" \
--PatchMatchStereo.depth_max "$DEPTH_MAX" \
--PatchMatchStereo.geom_consistency true \
--PatchMatchStereo.filter true
# ── Step 6: Stereo fusion → dense point cloud ─────────────────────────────────
echo ""
echo "=== Step 6: Stereo fusion ==="
colmap stereo_fusion \
--workspace_path "$DENSE_ROOT/dense" \
--workspace_format COLMAP \
--input_type geometric \
--output_path "$OUT/fused.ply" \
--StereoFusion.min_num_pixels 2 \
--StereoFusion.max_depth_error 0.05 \
--StereoFusion.max_reproj_error 4
echo ""
echo "=== Done ==="
echo "Dense point cloud: $OUT/fused.ply"
echo "Size: $(du -sh "$OUT/fused.ply" 2>/dev/null | cut -f1)"
echo ""
echo "Open in Blender:"
echo " DISPLAY=:0 blender --python-expr \"import bpy; bpy.ops.wm.ply_import(filepath='$OUT/fused.ply')\" &"
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment