Skip to content

Port CorbeauSplat to Windows / NVIDIA CUDA#8

Draft
marmotte5 wants to merge 46 commits into
freddewitt:mainfrom
marmotte5:claude/splatter-windows-cuda-1q8sh8
Draft

Port CorbeauSplat to Windows / NVIDIA CUDA#8
marmotte5 wants to merge 46 commits into
freddewitt:mainfrom
marmotte5:claude/splatter-windows-cuda-1q8sh8

Conversation

@marmotte5

Copy link
Copy Markdown

Convert the macOS/Apple Silicon Gaussian Splatting tool into a Windows-only fork focused on the video -> frames -> COLMAP -> Brush splat training -> view pipeline, accelerated by NVIDIA CUDA.

Platform core

  • system.py: replace Apple Silicon/MPS detection with CUDA detection via nvidia-smi (get_device -> cuda/cpu); Windows memory via GlobalMemoryStatusEx; resolve_binary handles .exe/COLMAP.bat and auto-detects C:\COLMAP.
  • FFmpeg uses -hwaccel cuda (NVDEC); COLMAP SIFT extraction/matching pass --SiftExtraction.use_gpu / --SiftMatching.use_gpu when CUDA is present; Brush runs on wgpu with WGPU_BACKEND=dx12.

Launcher & installers

  • New run.bat replaces run.command.
  • tools.py: drop Homebrew/Xcode; FFmpeg via winget, COLMAP CUDA detected/guided, Node/CMake/Ninja via winget, Rust via rustup-init.exe.
  • mapping.py: ColmapEngineDep (PATH/known-dir detection) replaces brew-based dep; Windows-aware Glomap build.
  • upscayl + brush installers: Windows asset selection and .exe binaries; checksums.json gains windows_* keys.
  • Drop the macOS-only pyobjc dependency.

Removed Apple ML Sharp

  • MLX/Apple-only image/video->3D feature removed: engine, installer, GUI tab, CLI sharp subcommand, workers and related tests.

Docs & tests

  • README.md / manifest.md / CHANGELOG.md rewritten for Windows/CUDA.
  • Test suite updated for the new platform; 189/189 pass.

Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3

claude added 2 commits June 26, 2026 10:59
Convert the macOS/Apple Silicon Gaussian Splatting tool into a Windows-only
fork focused on the video -> frames -> COLMAP -> Brush splat training -> view
pipeline, accelerated by NVIDIA CUDA.

Platform core
- system.py: replace Apple Silicon/MPS detection with CUDA detection via
  nvidia-smi (get_device -> cuda/cpu); Windows memory via GlobalMemoryStatusEx;
  resolve_binary handles .exe/COLMAP.bat and auto-detects C:\COLMAP.
- FFmpeg uses -hwaccel cuda (NVDEC); COLMAP SIFT extraction/matching pass
  --SiftExtraction.use_gpu / --SiftMatching.use_gpu when CUDA is present;
  Brush runs on wgpu with WGPU_BACKEND=dx12.

Launcher & installers
- New run.bat replaces run.command.
- tools.py: drop Homebrew/Xcode; FFmpeg via winget, COLMAP CUDA detected/guided,
  Node/CMake/Ninja via winget, Rust via rustup-init.exe.
- mapping.py: ColmapEngineDep (PATH/known-dir detection) replaces brew-based dep;
  Windows-aware Glomap build.
- upscayl + brush installers: Windows asset selection and .exe binaries;
  checksums.json gains windows_* keys.
- Drop the macOS-only pyobjc dependency.

Removed Apple ML Sharp
- MLX/Apple-only image/video->3D feature removed: engine, installer, GUI tab,
  CLI sharp subcommand, workers and related tests.

Docs & tests
- README.md / manifest.md / CHANGELOG.md rewritten for Windows/CUDA.
- Test suite updated for the new platform; 189/189 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
@marmotte5 marmotte5 marked this pull request as draft June 26, 2026 11:02
claude added 10 commits June 26, 2026 11:12
Remove the manual COLMAP/FFmpeg setup step. On first launch the dependency
manager now downloads them automatically:

- ColmapEngineDep fetches the latest COLMAP release and extracts the
  colmap-x64-windows-cuda.zip asset into engines/colmap (non-CUDA build when
  no NVIDIA GPU is detected).
- FfmpegEngineDep installs FFmpeg via winget when available, else extracts a
  static build into engines/ffmpeg.
- resolve_binary() searches engines/colmap and engines/ffmpeg subtrees and
  prefers .exe over .bat so binaries are launchable via subprocess.
- ColmapEngine.run_command adds COLMAP's bin/ and lib/ dirs to PATH so the
  bundled DLLs load without COLMAP.bat.
- Shared download_and_extract_zip helper with path-traversal-safe extraction.

Docs updated; added unit tests for the COLMAP asset selector. 193 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Real-machine testing surfaced several Windows-specific bugs:

- download_and_extract_zip leaked the mkstemp file descriptor, so Windows
  held an exclusive lock and ZipFile failed with WinError 32 — this blocked
  both COLMAP and FFmpeg extraction. Now closes the fd (also fixed the same
  pattern in install_rust_toolchain).
- winget installs (cmake/ninja/node) didn't update the current process PATH,
  so the next cmake/npm call failed with WinError 2. Added
  refresh_windows_path() (reads PATH from the registry) and call it after
  every winget install.
- Glomap (MSVC+CUDA source build) and the 360 Extractor no longer auto-build
  at startup — added install_on_startup=False so heavy/optional engines only
  install when their feature is enabled.
- FFmpeg install simplified to download a static build into engines/ffmpeg
  (no winget round-trip / PATH-refresh dependency); a system ffmpeg is still
  detected and reused.
- Brush is now saved as brush.exe on Windows (was 'brush', not launchable).
- npm/npx are .cmd shims that CreateProcess can't launch directly; route them
  through cmd.exe for SuperSplat install and the viewer launch.
- validate_path: relaxed home/project containment that rejected legitimate
  output folders on other drives (e.g. I:\projects). User-selected absolute
  paths are now allowed; '..' parent-traversal is still rejected. Dropped the
  matching GUI drop-target restriction.

193 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
- COLMAP 4.x rejects --SiftExtraction.use_gpu / --SiftMatching.use_gpu with
  "unrecognised option", aborting feature extraction. Removed the explicit
  flags: a CUDA-enabled COLMAP uses the GPU for SIFT by default, so GPU
  acceleration is preserved while staying compatible across COLMAP versions.
- When the selected input contains no images (images mode), the pipeline used
  to return success and run COLMAP on an empty folder. It now stops with a
  clear message (unless the working folder already has images from a re-run).

193 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Add a fast-forward `git pull` step to run.bat so launching the app always
picks up the latest fixes. Skips cleanly when git is unavailable, the repo
has local changes, or the machine is offline.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
The upscale step checked only for the upscayl binary, not for a downloaded
model. With no model, it moved the originals to images_src and then crashed
upscayl (missing .param), leaving the working folder broken and aborting the
whole dataset creation.

Now:
- verify a downloaded model exists before touching the images; if none, log a
  clear message and skip upscale (originals untouched);
- on any upscale failure/exception, restore the originals from images_src and
  continue the pipeline instead of aborting (upscale is an optional step).

193 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Discards out-of-focus / motion-blurred frames (common with video-extracted
images) before reconstruction, which improves splat sharpness and speeds up
COLMAP by dropping low-information frames.

- Sharpness = variance of the Laplacian (OpenCV). A frame is rejected when its
  score is below blur_factor x the median sharpness; rejected frames are moved
  to a sibling 'images_blurry' folder (not deleted) so they can be inspected.
- Relative/median-based threshold with a 50% safety cap so it adapts to content
  and never guts the dataset. Runs on the raw frames, before upscaling. Never
  fatal — skips cleanly if OpenCV is missing or there are too few images.
- GUI: "Filtrer les images floues" checkbox + Léger/Moyen/Fort severity in the
  Config tab Options group, persisted in config.json.
- CLI: --filter_blur / --blur_strength {light,medium,strong} on colmap+pipeline.
- Added opencv-python-headless to requirements (also enables the previously
  skipped resolution-normalization step).
- Pure selection logic factored into select_blurry_files() with unit tests.

197 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
New "Nettoyage / Cleaner" tab to clean a trained Gaussian Splat .ply without
touching the original:

- app/core/ply_cleaner.py: removes near-transparent splats (low opacity),
  oversized gaussians (sky shells / big floaters) and spatial outliers
  (floaters far from the cloud), via opacity + scale-percentile + distance-
  percentile filtering. Survivors are kept byte-for-byte (colour/SH/rotation
  preserved). Pure compute_keep_mask() is unit-tested; clean_ply() does the I/O.
- GUI tab: load a .ply, choose severity (Léger/Moyen/Fort) or fine-tune the
  three thresholds, run cleanup in a worker thread, see before/after stats and
  per-filter breakdown, preview the result in SuperSplat, then "Save as".
- CLI: python main.py clean -i in.ply -o out.ply --strength medium.
- CleanerWorker added; tab wired into the main window + session persistence.

Note: cleanup removes noise (sky/floaters), it does not fix reconstruction
drift — that needs loop closure at the COLMAP stage.

204 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
…filter

Large reconstructions (thousands of images) can crash COLMAP's global bundle
adjustment with "Linear solver failure. Failed to compute a finite step.".
The new robust mode stabilises it using only known COLMAP options:

- camera model PINHOLE (no distortion params to diverge),
- ba_refine_extra_params = off, ba_refine_principal_point = off,
- multiple_models = on (a degenerate region spawns a sub-model instead of
  killing the whole BA),
- forces blur filtering on so blurry frames aren't registered.

Exposed as a "Mode robuste (grandes scènes)" checkbox in the Config tab
(enabling it also ticks the blur filter), and as --robust on the colmap and
pipeline CLI commands. Persisted in config.json.

206 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
The blur filter analysed every frame at full resolution with no feedback, so on
large sets (6000+ images) it looked frozen for minutes. Now it:

- logs progress (status every 100 images, log line every 500),
- downscales images to ≤640px before the Laplacian — the relative sharpness
  ranking is preserved and it runs ~10x faster.

206 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
cv2.imread/resize/Laplacian release the GIL, so scoring the frames in a
ThreadPoolExecutor (num_threads workers) gives a large speedup on big sets
(e.g. 16x on a 16-thread machine). Progress is reported as futures complete;
cancellation shuts the pool down promptly.

206 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
@freddewitt

Copy link
Copy Markdown
Owner

It's great idea. I can't merge to my project, but we can do another repo ?

claude added 11 commits June 26, 2026 20:11
The resolution check full-decoded every image via cv2.imread just to read
width/height — minutes of work on large sets (10k+ frames), and pointless for
video frames that all share one resolution.

Now it reads dimensions from the file header via PIL (no pixel decode),
parallelized across CPU threads with progress, and exits instantly when the
resolution is uniform. cv2 is only used for the actual resize, which only runs
when resolutions genuinely differ.

206 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
…ath log

The biggest hidden cost: "Domain Size Pooling" was ON by default, which
disables COLMAP's GPU SIFT and silently falls back to slow CPU extraction —
defeating the CUDA port. Flipped it OFF by default across the dataclass, the
Params tab and the CLI (now an opt-in --domain_size_pooling). Affine Shape stays
off, Cross Check / Single Camera stay on — the settings we recommended.

Also: feature_extraction now logs which SIFT path runs (GPU vs CPU) and warns
when Affine/Domain Pooling is forcing CPU, so a misconfiguration that loses the
GPU is obvious.

206 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
On shaky video, the median-relative sharpness threshold could flag a large
fraction of frames, and the 50% safety cap then moved up to half the dataset
into images_blurry — users reported "my whole dataset is in blurry". Lower the
default cap to 20% so at least 80% of frames are always kept, and the log now
explains how to restore (move files back from images_blurry to images).

206 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Robust mode set params.filter_blurry = True in get_current_params, overriding
the user's choice — so unchecking "Filtrer les images floues" did nothing while
Mode robuste was on. Robust mode now only pre-ticks the blur checkbox (via
config_tab._on_robust_toggled); the actual checkbox state is what's used, so
unchecking it is respected.

206 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
…ion)

When the input is an image folder, the pipeline copied every image into
project/images, doubling disk usage. It now hardlinks them (same data on
disk, zero extra space), falling back to a copy only when the source is on a
different volume. The one in-place writer (resolution resize) unlinks first so
it writes a fresh file and never modifies the user's originals; blur-filter and
upscale only move/replace project entries, so originals are always safe.

206 tests pass; verified hardlink (same inode) + resize-safety locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
…oads

- ply_utils.parse_ply_manual: numpy.void rows have no .get(); replaced with a
  field-name guarded _field() helper (was AttributeError on every binary PLY
  with scale/rot/opacity).
- upscale_engine.load_model: scale heuristic produced "realesrgan-2plus"
  instead of "realesrgan-x2plus" (replace "x4" with f"x{scale}").
- upscayl_manager: guard os.path.relpath ValueError when binary and models
  live on different Windows drives; guard tarfile.extractfile() returning None;
  stream the release archive to disk instead of buffering in RAM.
- installers/tools.py and installers/brush.py: stream large COLMAP/ffmpeg/Brush
  downloads with shutil.copyfileobj instead of resp.read().
- ruff --fix: style cleanup across the project.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Two features from COLMAP 4.1.0:

GPU bundle adjustment ("Caspar"):
- New ba_use_gpu / ba_gpu_index params + "Bundle adjustment GPU" checkbox in
  the COLMAP tab. Runs BA on the GPU, which fixes the "Linear solver failure"
  CPU crashes on large scenes.
- engine.mapper passes --Mapper.ba_use_gpu only after probing `colmap mapper -h`
  (cached) for the option, so an older COLMAP never aborts on an unknown flag —
  it logs a clear fallback message instead.

Native 360 reconstruction:
- EQUIRECTANGULAR added to the camera-model dropdown plus a convenience
  "360 natif" checkbox that drives it (single source of truth = camera_model,
  synced both ways). feature_extraction logs the 4.1.0 requirement.

Auto-upgrade:
- ColmapEngineDep now defines REQUIRED_MIN=4.1.0 and re-downloads when the
  local build is older, so existing users get the GPU-BA/360 build on next
  run.bat without manually deleting engines/colmap.

- New i18n keys (check_native_360, check_ba_use_gpu) in all 9 locales.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
…dencies

The ruff --fix pass deleted `from app.core.system import resolve_project_root`
from setup_dependencies.py, but the module still calls resolve_project_root()
in get_venv_360_python() AND re-exports it for app.core.extractor_360_engine.

That re-export is imported at GUI startup, so the missing name raised
ImportError before the window could open — the launcher terminal closed after
a few seconds with no visible error.

- Restored the import (now genuinely used at module level, so ruff's
  unused-import autofix will not strip it again) with a comment explaining why.
- Removed the stale "# resolve_project_root is imported from app.core.system"
  placeholder comment left behind by the autofix.

Verified by importing all 34 app.* modules and constructing/showing ColmapGUI
headlessly — startup now completes and the event loop runs cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Defaults tuned for the video→frames→COLMAP→splat workflow (fast, no quality
loss, blur filter off):
- matcher_type default exhaustive → sequential: O(n) instead of O(n²) and the
  correct matcher for ordered video frames. Big speedup on large datasets.
- ba_use_gpu default off → on: GPU bundle adjustment ("Caspar"). Only applied
  when COLMAP ≥ 4.1.0 supports it (probed + cached), and COLMAP auto-falls back
  to CPU for small scenes, so it's safe as a default.
- Blur filter already off by default (unchanged) — left off per request.

Persistence:
- The COLMAP params were only saved on a clean window close. Now switching
  tabs triggers a debounced session save, so a choice like the matcher is
  persisted as soon as the user navigates away — connected after load() so it
  never overwrites the just-loaded session at startup.

Verified headlessly: fresh GUI shows sequential + GPU BA on + blur off; a
matcher change followed by a tab switch is written to config.json.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
On a re-run, the working images/ folder already holds (hardlinked or copied)
images from the previous run. The prepare loop saw target.exists() and made a
RENAMED duplicate (images_1_frame_0001.png), so each run grew the dataset.

Now each source image is compared against what's already in images/ via a new
_same_image() check — same inode (hardlink from a prior run) or byte-identical
content (size first, then filecmp). Already-present images are skipped; only
genuinely different images that share a filename are disambiguated. The final
log reports how many were skipped.

Verified: _same_image handles hardlink / identical-copy / different /
same-size-different-content; three consecutive _prepare_images runs keep the
image count flat instead of multiplying it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Two related "settings change by themselves" bugs:

1. Wheel-over-widget: the params live in a QScrollArea, and Qt combo/spin
   widgets change value when the wheel passes over them. Scrolling the COLMAP
   params page silently flipped the matcher (down to the last item =
   vocab_tree), and auto-save then persisted the accident. Added a reusable
   install_wheel_guard() (app/gui/widgets/wheel_guard.py) that makes combos,
   spin boxes and sliders ignore the wheel unless focused, so the page scrolls
   instead. Applied to params, brush, upscale and config tabs.

2. Blur re-checked on startup: ConfigTab.set_state restored blur_filter and
   THEN robust_mode, but set_robust_mode(True) pre-ticks blur via
   _on_robust_toggled — overriding the saved value. Reordered so robust is
   applied before blur, letting the saved blur value win.

Verified headlessly: a wheel scroll over the unfocused matcher no longer
changes it; restoring {robust:true, blur:false} keeps blur off.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
@marmotte5

marmotte5 commented Jun 27, 2026 via email

Copy link
Copy Markdown
Author

claude added 4 commits June 27, 2026 08:39
…lags

- Lead the README by crediting the upstream project
  (freddewitt/CorbeauSplat) that this Windows/CUDA edition is forked from.
- Drop the obsolete --SiftExtraction.use_gpu / --SiftMatching.use_gpu mention
  (COLMAP 4.x rejects those flags; the CUDA build uses the GPU automatically)
  and note the video-tuned defaults (sequential matching, GPU bundle adjustment).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
After "Extraction des features terminé", two steps run with no output and make
the pipeline look frozen on large datasets:
- the sequential-matcher DB sort (image_id → temporal order), and
- the matcher loading every image's descriptors into RAM before it prints its
  first "Matching block" line (several minutes on a big 4K set).

Added log + status lines around both so the user knows it's working and not to
close the program. No behavior change to the actual reconstruction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
During mapping, COLMAP can print "Linear solver failure ... dense Cholesky
factorization" as a glog W-level warning when one bundle-adjustment step
fails — the solver retries and registration continues. It reads like a crash
to users. Annotate it once in the log with a clear "non bloquant" note.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Repeated "Linear solver failure" during the Global bundle adjustment signals a
numerically hard scene. Extend the one-time note to point users at the GPU
bundle adjustment (COLMAP 4.1.0) or robust mode to stabilise it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
claude added 19 commits June 27, 2026 09:51
The mapper has no fixed total, so there's no clean %, but num_reg_frames is the
real progress counter (images successfully placed). Surface it in the status
bar ("Reconstruction 3D : 404 images placées") instead of the raw image id, so
the user can see where they are without reading the log.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Multi-dimension performance pass. All changes are pure speedups or tunable
defaults — no reconstruction-quality regression.

Startup (time-to-window):
- app/cli/__init__.py: lazy-import the heavy CLI engine stack (.commands →
  ColmapEngine/BrushEngine/app.core.engine) only on the CLI branch; the GUI
  default path no longer pays for it. check_dependencies stays eager (cheap).
- ply_cleaner.py: import numpy lazily inside the compute functions (cleaner_tab
  pulls this in at startup via resolve_params, which needs no numpy).
- engine.py: lazy-import send2trash inside the delete path (off the startup chain).
  Confirmed: `import app.cli` no longer loads numpy/send2trash/cv2/plyfile.

Image preparation:
- Single-video fast path in resolution check: frames from one ffmpeg run share
  a resolution, so sample the first frame instead of header-reading every file
  (O(n) → O(1)). Multi-video/image-folder inputs keep the full scan.
- Parallelize the resolution-resize pass (cv2 releases the GIL).
- _same_image: short-circuit on matching size+mtime (copy2 preserves mtime) so
  no-op re-runs skip the full byte compare; filecmp remains the fallback.

COLMAP mapper (the 10-15h bottleneck):
- Pin OMP/OpenBLAS/MKL to 1 thread for COLMAP children. COLMAP already
  parallelizes via --*.num_threads; letting BLAS also spawn N threads caused
  N×N oversubscription that thrashes the CPU and worsens the "Linear solver
  failure" churn during global BA. One source of parallelism, not nested.
- Bound bundle adjustment via new tunable ColmapParams fields (faster than
  COLMAP defaults, safe for 3DGS): ba_global_max_num_iterations=30,
  ba_global_function_tolerance=1e-6, ba_global_images_ratio/points_ratio=1.2,
  ba_local_max_num_iterations=20 — caps global-BA iterations and how often it
  re-runs. Tunable via config.json back toward COLMAP defaults if needed.

Misc:
- base_engine: line-buffer subprocess stdout (bufsize=1) so BA progress flushes
  promptly instead of looking frozen.

Tests:
- Repoint CLI dispatch patches to app.cli.commands.DISPATCH (now lazy).
- Repoint send2trash patches to the real module (now a lazy import).
- Fix pre-existing test_managers fixture gap: mock cleaner_tab (it was added to
  the save mapping earlier but never mocked).

Verified: full app.* import sweep clean; 130 core + 89 affected tests pass;
GUI constructs/runs headlessly; mapper builds the new BA flags; run_command
pins BLAS env to 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
A focused correctness review of engine.py (heavily churned) surfaced three real
bugs in the video-frame / resolution-resize path:

1. _imwrite_unicode lossy/failing re-encode on resize. IMREAD_UNCHANGED can
   return a 4-channel image; cv2.imencode(".jpg", rgba) FAILS → the frame was
   left at its original size, leaking mixed resolutions into COLMAP. JPEG also
   re-encoded at the default quality 95 (a second lossy generation). Fix: drop
   the alpha channel before JPEG encode and encode at quality 100. Verified
   RGBA→JPEG now succeeds and PNG alpha is preserved.

2. Video prefix collision. Two source videos whose stems sanitize to the same
   string (e.g. "a.b" and "ab") — or to an empty string — produced the same
   frame prefix, so the second extraction overwrote the first's frames and the
   per-prefix count was wrong. Fix: prefix the enumeration index
   (f"{i:03d}_{sanitized}", or f"{i:03d}" when empty) → distinct, non-empty.

3. Stale frames on video re-run. extract_frames_from_video never cleared old
   frames, so re-running with a changed fps/source left higher-numbered stale
   frames that COLMAP then ingested. Fix: unlink this prefix's existing
   *.jpg before extracting (only this prefix is touched).

The review verified the recently-added perf code (mapper ba_* flags, run_command
BLAS pinning, _same_image, _is_single_video_input, DB sort) is correct.

Verified: prefix uniqueness, per-prefix frame clearing, and imwrite alpha/quality
all tested; 89 affected tests pass; full app.* import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Frame extraction used bare '-hwaccel cuda', which frequently still decodes on
the CPU. Now the input codec is probed (ffprobe) and the matching NVDEC cuvid
decoder is selected (-c:v h264_cuvid / hevc_cuvid / …) when available, moving
decode onto the GPU — 2-5x faster extraction on long/high-res video.

Safe by construction: the standard CPU decode is always the last attempt, so a
missing/incompatible cuvid decoder can never break extraction. The cuvid path is
only chosen when both (a) ffprobe identifies a mappable codec and (b) the decoder
is present in `ffmpeg -decoders` (probed once, cached). If the GPU attempt yields
a non-zero return or zero frames, it clears partial frames and retries on CPU.

Also drops audio (-an) and keeps the per-prefix frame cleanup from the previous
fix (now run before each attempt).

New helpers: _cuvid_decoder_for, _probe_video_codec, _ffprobe_bin (derived from
ffmpeg's path), _available_cuvid_decoders (cached).

Verified: decoder selection (codec→cuvid, availability gating), GPU→CPU fallback
(2 attempts, CPU succeeds), and the no-CUDA single-attempt path are unit-tested;
64 affected tests pass; full app.* import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
…ions

PR #26 added --Mapper.ba_global_* / ba_local_* flags unconditionally. COLMAP
4.1.0 renamed/removed some of them (ba_global_images_ratio / ba_global_points_ratio),
so the mapper aborted with "unrecognised option '--Mapper.ba_global_images_ratio'"
right after a successful match — losing the whole run.

Now each optional --Mapper.* flag is gated on `colmap mapper -h` advertising it,
using a single cached help-text probe shared with the GPU-BA check:
- supported flags are passed (e.g. ba_global_max_num_iterations, ba_use_gpu),
- unsupported ones are skipped with an info log,
- if the help probe fails/returns nothing, all optional flags are skipped and the
  mapper runs with COLMAP defaults (correct, just unbounded).

Verified across three simulated builds: 4.1.0 (skips *_ratio, keeps the rest +
GPU BA), empty-help (skips all optional flags, base mapper intact), and 3.x
(keeps *_ratio, omits GPU BA). 51 affected tests pass; import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
… run

A failed/cancelled mapper used to force redoing feature extraction + matching
from scratch (the DB was wiped at the start of every run). Now the pipeline
reuses an existing database.db and jumps straight to the mapper when it is
provably safe to do so.

Reuse happens only when ALL hold (any mismatch → full rebuild):
- a sidecar signature (database.db.sig.json) matches the current
  extraction/matching params AND image count — so changing any COLMAP setting
  (max_image_size, features, matcher, overlap, …) still forces a fresh run;
- the DB's image names equal exactly the current image set;
- every image has descriptors (extraction complete);
- two_view_geometries (verified matches) is non-empty (matching complete).

The signature is written after a successful match; the sparse model is still
always rebuilt fresh. On any DB/SQLite/JSON error the guard returns False, so a
stale or corrupt DB can never be silently reused.

This directly helps the just-fixed mapper-abort case: re-running now skips the
(already-done) extraction + matching and resumes at the mapper.

Verified with a synthetic COLMAP-schema DB across 7 cases (valid→reuse;
image mismatch, param change, no matches, partial features, no signature,
no DB → all redo). 64 affected tests pass; import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Re-running an image-folder project created duplicate files like
"Photos-3-001_1_IMG_xxx.jpg" alongside "IMG_xxx.jpg", compounding every run.

Root cause: the first run resizes mismatched-resolution images in place
(_check_and_normalize_resolution), changing their bytes/size. On a re-run the
old content-based dedup (_same_image) then saw source≠target and fell into the
"<folder>_1_<name>" disambiguation path — re-importing every image under an
alias. COLMAP then ingested both copies.

Fix: import provenance is now tracked by SOURCE PATH in a manifest
(project/import_manifest.json), not by content:
- a source already imported (even after an in-place resize) is recognised by
  its absolute path and adopted, never re-imported under an alias;
- target names are claimed deterministically; two genuinely different sources
  sharing a basename are still disambiguated;
- self-heal: stale top-level images that no current source claims (old
  "<folder>_N_<name>" duplicates, or frames of a removed source) are deleted,
  so an already-messy project cleans itself on the next run. Subfolders
  (images_src, blurry) and mask files are left untouched.

Removed the now-unused _same_image helper.

Verified across 5 filesystem scenarios: fresh import; re-run after in-place
resize (no duplicates); existing messy project (orphan duplicate removed); two
distinct sources sharing a basename (both kept, re-run stable); source removed
(its target dropped). 64 affected tests pass; import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
….1.0

The "Retriangulation and Global bundle adjustment" step is dominated by how many
BA+filter refinement passes each global-BA trigger runs, and how often global BA
is triggered. Two improvements (both probe-gated, version-safe):

1. Cap refinements — the single biggest in-mapper lever:
   - ba_global_max_refinements 5 → 3 (global BA+filter passes per trigger)
   - ba_local_max_refinements  2 → 1 (local BA runs after every image)

2. Fix global-BA frequency control on COLMAP 4.1.0: COLMAP 4.1.0 renamed
   --Mapper.ba_global_images_ratio → ba_global_frames_ratio (rig/frame
   terminology). We only sent the old name, which 4.1.0 rejects → it was
   silently dropped, leaving global BA at the slow default cadence (1.1). Now we
   pass BOTH names; the probe sends whichever the build advertises, so the
   "run global BA less often" tuning (1.2) actually applies on 4.1.0 and 3.x.

Confirmed option names + defaults against COLMAP source
(controllers/incremental_pipeline.h): ba_global_max_refinements=5,
ba_local_max_refinements=2, ba_global_frames_ratio=1.1.

Verified: new params round-trip; simulated 4.1.0 help → frames_ratio used
(freq restored), refinements applied, images_ratio skipped; simulated 3.x →
images_ratio used, frames_ratio skipped. 51 affected tests pass; import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Brush is built on burn-fusion, which can panic mid-training with
"Ordering is bigger than operations" (a known burn stream-ordering / tensor-
handle bug). There is no runtime flag to disable fusion, but switching the wgpu
backend (DX12 ↔ Vulkan) reschedules the op stream and usually dodges it.

train() now detects the panic in Brush's output and, on a fusion crash, retries
once on the alternate GPU backend (DX12 default → Vulkan). Non-fusion failures
are NOT retried. The backend is also overridable via the `wgpu_backend` param
(config.json) for users who want to pin Vulkan from the start.

build_command gains an optional backend_override (backward compatible; default
stays DX12).

Verified: existing 17 brush tests pass; new tests cover fusion-panic→Vulkan
retry, non-fusion failure→no retry, and success→single attempt. Import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
…fragment)

With multiple_models on (robust mode), COLMAP writes sub-models sparse/0,
sparse/1, … in no size order. Brush, the undistorter, and create_brush_config
all use sparse/0, so when the scene fragments, training could land on a tiny
fragment (observed: 11 of ~3000 images → a spiky, useless splat).

After mapping, _promote_largest_sparse_model() now reads each sub-model's
registered-image count (the uint64 header of images.bin, with an images.txt
fallback) and swaps the largest model into the sparse/0 slot, so Brush trains on
the full reconstruction. No sub-model is deleted — only reordered.

Verified: fragment promotion (sparse/1=2000 → sparse/0, all models preserved),
already-largest unchanged, single-model no-op, images.txt fallback count. 27
colmap tests pass; import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
The user's log showed the real trigger of the burn-fusion panic: on DX12 the
device reports maxComputeInvocationsPerWorkgroup=768, but Brush's reduce kernels
need 1024 → wgpu "Validation Error" in create_compute_pipeline → burn-fusion
panic. Vulkan exposes the full 1024, so it trains fine.

- Default wgpu backend on cuda/auto is now Vulkan (was DX12); DX12 becomes the
  automatic fallback (order reversed in train()).
- Crash detection also matches "wgpu error" / "validation error", so even a
  non-panicking wgpu compute-limit error triggers the backend retry.
- Still overridable via wgpu_backend / backend_override.

Updated the two tests that asserted the DX12 default; added an override test.
18 brush tests pass; vulkan→dx12 fallback on a wgpu validation error verified;
import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
A user's COLMAP 4.1.0 accepts --Mapper.ba_use_gpu but its Ceres was compiled
without CUDA/cuDSS, so bundle adjustment silently falls back to CPU:
  "Requested to use GPU ... but Ceres was compiled without CUDA support."
Our pre-run log claimed "Bundle adjustment : GPU (CUDA) ✅", which was wrong and
hid the real cause of the slow mapper.

- Soften the pre-run message: GPU BA is only effective if the build has
  Ceres+CUDA/cuDSS (confirmed at runtime).
- Detect the runtime "compiled without CUDA/cuDSS" warning once and explain that
  BA is actually on the CPU, and that GLOMAP (or a cuDSS-enabled COLMAP) is the
  way to actually accelerate it.

27 colmap tests pass; import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
The user wants faster reconstruction (global SfM does one global BA instead of
the repeated per-image one). The old path built GLOMAP from source (MSVC + CUDA +
CMake/Ninja) — heavy and likely to fail. COLMAP 4.0+ merged GLOMAP in as the
built-in `colmap global_mapper`, so the colmap.exe already installed can do it
with no extra build.

- mapper() now dispatches to _global_mapper (use_glomap) or _incremental_mapper.
- _global_mapper prefers `colmap global_mapper` (probe-gated via `colmap help`),
  running `view_graph_calibrator` first to improve intrinsics (global SfM relies
  on them). Falls back to a standalone glomap binary, then to incremental — each
  gated so nothing aborts on an unsupported build.
- GlomapEngineDep.is_installed() now also returns True when colmap advertises
  global_mapper, so enabling GLOMAP no longer triggers a source build.

Drive-by (pre-existing breakage surfaced by the test run):
- Re-export relax_requirements from setup_dependencies (the test imported it but
  it lived only in installers/tools) and add __all__ so ruff --fix never strips
  the compat re-exports again (a stripped re-export previously broke startup).

Verified: global_mapper(+calibrator) used when available; fallbacks to standalone
glomap then incremental; mapper() dispatch on use_glomap; relax_requirements test
passes; 45 engine tests pass; import sweep clean; ruff clean on the shim.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
The user's reconstruction placed the same building in two different 3D spots — a
loop-closure failure. Sequential matching only matches frames that are close in
TIME, so when the camera revisits a place (common in video walkthroughs) the two
visits aren't linked and the location gets reconstructed twice. No mapper (incl.
GLOMAP) can fix matches that were never made.

- New ColmapParams.loop_detection (default True) added to the DB-resume
  signature, so enabling it forces a fresh match.
- feature_matching (sequential) now passes --SequentialMatching.loop_detection 1
  with a COLMAP vocabulary tree, gated on: the param, the option existing in
  `colmap sequential_matcher -h`, and the vocab tree being available.
- _ensure_vocab_tree() downloads vocab_tree_flickr100K_words32K.bin into engines/
  once (streamed, atomic via .part rename); returns None on failure so matching
  proceeds without loop detection — never blocks.
- _colmap_cmd_help() caches per-subcommand help for option probing.

Verified: flags added only when supported + vocab present; skipped gracefully on
missing vocab, unsupported option, or param off; loop_detection in the resume
signature. 27 colmap tests pass; import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
The COLMAP params tab header read "Apple Silicon detected - N threads" — a
leftover from the macOS origin, shown on Windows/NVIDIA machines. Replaced the
info_cpu string in all 9 locales with an accurate, platform-neutral
"CPU: N threads detected".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
COLMAP switched its vocabulary-tree index from FLANN to FAISS in May 2025.
The auto-downloaded flickr100K/32K tree was the legacy FLANN format, so
COLMAP 4.1.0 aborted feature matching with "Failed to read faiss index"
whenever loop detection was enabled — breaking the whole pipeline.

- _ensure_vocab_tree() now fetches a FAISS-format tree (engines/
  vocab_tree_flickr100K_words32K_faiss.bin) and validates the download size.
- feature_matching() is now failure-proof: if the sequential matcher with
  loop detection fails, it automatically retries WITHOUT loop detection, so
  matching always produces correspondences instead of aborting.
- matcher_type='vocab_tree' now runs the real vocab_tree_matcher (was
  silently falling through to exhaustive), with a graceful fallback to
  exhaustive matching if the tree is unreadable or unavailable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Multi-agent adversarial audit of the whole video→COLMAP→Brush pipeline.
Confirmed defects fixed (false positives discarded after verifying against
the actual code, e.g. quadratic_overlap is valid; the sparse-swap rename is
already guarded by except OSError):

- Frame extraction quality: ffmpeg -qscale:v 2 → 1 (highest JPEG quality)
  so frames feeding COLMAP/Brush keep maximum detail.
- Mixed-resolution unification now resizes to the LARGEST resolution
  (INTER_CUBIC up / INTER_AREA down) instead of the smallest — the previous
  behaviour permanently discarded detail from the sharpest cameras.
- WAL→DELETE DB conversion (GLOMAP path) now closes the SQLite connection
  explicitly before deleting -wal/-shm, fixing a Windows file-lock failure;
  checkpoint runs before the journal-mode switch and is committed.
- _execute_command terminates the child process if streaming/parsing throws,
  so a COLMAP/Brush process is never orphaned (zombie holding GPU/port).
- Glomap install verifies the binary copy actually landed before recording
  the version (a failed copy no longer marks it "installed" forever).
- CLI matcher_type default aligned with the engine/GUI: exhaustive → sequential
  (fast and correct for ordered video frames); added --sequential_overlap and
  --no_loop_detection and wired them into run_colmap / run_pipeline.
- GUI: added a "Détection de boucles" checkbox (loop_detection) to the COLMAP
  params tab so it can be seen/toggled, wired through get_params/set_params.

All tests pass; compile + import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
… CLI ply_name

Second adversarial audit pass over the subsystems the first didn't cover
(export/PLY, viewer, upscale, GUI threading, param flow). Confirmed defects
fixed (dangerous/incorrect agent suggestions discarded after verifying against
the code, e.g. blocking empty-checksum downloads would break all installs;
adding a --refine-mode flag Brush may not accept could break training):

- SPZ export was 100% broken: export_engine.py used math.sqrt without
  importing math (NameError) and called .get() on a numpy.void structured
  scalar (AttributeError). Added the import; index fields directly (the
  enclosing `if 'scale_0' in data.dtype.names` guards guarantee presence).
- Manual ASCII PLY parser returned zero points: it iterated the already-
  consumed header instead of the file body. Now reads the vertex rows from
  the remaining file stream (verified: 3-vertex ASCII PLY now parses).
- SuperSplat data server reported success even when the port bind failed
  (error was only logged in the worker thread). It now waits on the bind and
  returns a real error; the Cleaner preview checks that return.
- CLI --ply_name was silently ignored (only the GUI renamed the output). Added
  a shared rename_latest_ply() helper, wired into run_brush and run_pipeline.

Targeted tests pass (78); compile + import sweep clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
…, phantom Brush flags

Focused audit of camera placement (SfM) and Brush training. Fixes:

MAPPING (camera placement):
- GLOMAP/global_mapper was throttled to a SINGLE thread: run_command pins
  OMP_NUM_THREADS=1 (anti-oversubscription for the incremental mapper), but
  global_mapper has no --num_threads flag and derives its thread count from
  OMP_NUM_THREADS. run_command now takes omp_threads and the global-mapper
  path (view_graph_calibrator included) gets the full core count.
- global_mapper/glomap received ZERO options — GUI settings (GPU BA,
  intrinsics refinement) were silently dropped. New probe-gated
  _global_mapper_opts() forwards BundleAdjustment/GlobalPositioning use_gpu +
  gpu_index, optimize_intrinsics/principal_point, and skips the view-graph
  calibration inside global_mapper when we already ran it as a separate step.
  ba_global_max_num_iterations is deliberately NOT mapped (tuned low for the
  repeated incremental BA; the single global BA keeps GLOMAP's own budget).
- vocab_tree matching: raise retrieval breadth (num_images 150, nearest
  neighbors 8, probe-gated) and add a sequential TOP-UP pass after a
  successful vocab-tree pass — matches accumulate in the same database, and
  temporally-adjacent frames reconnect views the retrieval missed (the
  user's run left 88/2031 images unconnected). DB image-id sort now also
  runs for vocab_tree (required for sequential adjacency; the sort wipes
  matches, so it stays strictly before matching).
- _sort_colmap_database_images now sorts NATURALLY (frame_9999 < frame_10000)
  instead of SQL lexicographic order.

TRAINING (Brush):
- ALLOWED_FLAGS contained flags that DO NOT EXIST in Brush v0.3.0 and make
  it abort via clap: --refine-pose, --test-split, --save-iterations,
  --log-level (the GUI placeholder even advertised --refine_pose). Verified
  against the v0.3.0 TrainConfig/LoadDataseConfig/ProcessConfig sources.
  Replaced with the real flag set: --eval-split-every, --ssim-weight,
  --lpips-loss-weight, --opac-loss-weight, lr-* etc. Tests updated (phantom
  flags now asserted rejected).
- Resolution hint corrected in all locales: Brush's built-in default cap is
  1920 px (not 1080) — with the spinbox at 0 the flag is omitted and Brush
  silently trains at 1920.
- Undistortion was DEAD WEIGHT for training: dense/ was written but Brush
  always trained on the original distorted images (brush_config.json has no
  reader). The undistorted model is now normalized to dense/sparse/0 and the
  GUI + CLI pipeline route Brush to dense/ when it exists.
- Robust mode's silent camera-model override (→ PINHOLE, no distortion
  modelling) is now surfaced in the logs with guidance.

95 targeted tests pass; locales JSON validated; natural sort verified.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GCByXMmnQgpLN6LgSLhEZ3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants