Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,6 @@ engines/brush/**
!engines/brush/README.md
engines/linux/brush/**
engines/macos/arm64/**

# Collected Windows installer artifacts (large) are never committed.
dist-artifacts/
5 changes: 4 additions & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
| 优先级 | 事项 | 目标 | GitHub Issue |
| --- | --- | --- | --- |
| P0 | 失败或暂停后支持断点续跑 | 保留可复用的阶段结果,使中断任务能够从合适的处理阶段继续。 | 待创建 |
| P1 | 支持输入图片序列 | 允许用户以有序图片集作为重建输入,而不必先制作视频文件。 | 待创建 |
| P1 | 提升重建性能(GLOMAP / 词汇树环路) | 用 GLOMAP 全局建图替代增量 mapper 以提速并提高注册率;用词汇树/环路检测提升环绕视频首尾闭环、降低“注册率<50%”失败。依赖:内置 COLMAP 需含 `global_mapper`;词汇树需 faiss 兼容树(当前公开预训练树为 flann,与内置 faiss 版 COLMAP 不兼容)。 | 待创建 |
| P3 | 全景视频支持 | 探索将全景视频作为输入并生成可用 Gaussian Splatting 结果的工作流。 | 待创建 |

## 已完成
Expand All @@ -31,6 +31,9 @@
| 已完成 | Apple Silicon macOS Alpha | 为 macOS 15+ arm64 提供随应用交付的 FFmpeg、FFprobe、CPU COLMAP 和 Brush 工作流。 | [#4 Add macOS support](https://github.com/ooolabdev/ooosplat/issues/4),由 [PR #8](https://github.com/ooolabdev/ooosplat/pull/8) 交付 |
| 已完成 | 内嵌高斯泼溅预览 | 已支持加载 `.ply`、相机浏览、整体 Transform、撤销 / 重做、动画预览,以及非破坏式 Gaussian 和竖屏视频导出。 | [#3 关于集成查看功能](https://github.com/ooolabdev/ooosplat/issues/3) |
| 已完成 | COLMAP CUDA 自动加速 | 已支持检测 NVIDIA 驱动和 Compute Capability,满足要求时自动启用 GPU 特征提取与匹配,否则无中断地回退 CPU。 | [#6 Add CUDA-accelerated pipeline for NVIDIA GPUs](https://github.com/ooolabdev/ooosplat/issues/6);相关用户反馈 [#2](https://github.com/ooolabdev/ooosplat/issues/2) |
| 已完成 | 支持输入图片序列 | 新增“图片文件夹 / 图片序列”输入入口:前端可选视频或图片文件夹;后端按输入类型自动选择匹配器(图片→穷举匹配,视频→顺序匹配)并复用整条 COLMAP + Brush 链路,无需先制作视频。 | 本次实现(待建 Issue) |
| 已完成 | 重建性能基础优化 | 特征提取 `max_num_features=8192` 提升特征密度;图片序列默认 `exhaustive_matcher` 提升无序图集注册率;新增 GLOMAP 与词汇树/环路匹配的代码接入(运行时检测 + 自动回退,避免旧引擎/缺资产时破坏流程)。 | 本次实现(待建 Issue) |
| 已完成 | Windows NSIS 客户端打包 | 补齐构建环境(Rust MSVC + VS2022 BuildTools + SDK),使用国内镜像下载并校验内置引擎(FFmpeg/COLMAP/Brush),产出 `OOOSplat-0.3.0-x64-setup.exe`(含内置引擎,本地未签名)。 | 本次实现(待建 Issue) |

## 跟踪与贡献

Expand Down
22 changes: 18 additions & 4 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ use crate::{
},
reconstruction::{ply::inspect_gaussian_ply, splat_transform::export_transformed_ply},
telemetry::{PipelineTelemetrySession, TelemetryPreferences, TelemetryService},
video::{FramePlan, FrameSelectionStrategy, UniformRatioFrameSelection, VideoInfo},
video::{create_image_plan, list_images, FramePlan, FrameSelectionStrategy, UniformRatioFrameSelection, VideoInfo},
};

#[derive(Default)]
Expand Down Expand Up @@ -105,7 +105,7 @@ pub struct GaussianVideoExportResult {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProbeAndPlan {
video: VideoInfo,
video: Option<VideoInfo>,
plan: FramePlan,
}

Expand All @@ -129,9 +129,23 @@ pub async fn probe_and_plan(
path: String,
quality: Quality,
) -> std::result::Result<ProbeAndPlan, SplatError> {
let video = probe_video(&paths_for_app(&app).ffprobe, &PathBuf::from(path), None).await?;
let input = PathBuf::from(&path);
if input.is_dir() {
let images = list_images(&input)?;
if images.is_empty() {
return Err(SplatError::InvalidVideo(
"图片序列为空,未找到支持的图片文件".into(),
));
}
let plan = create_image_plan(images.len() as u64, &quality.preset());
return Ok(ProbeAndPlan { video: None, plan });
}
let video = probe_video(&paths_for_app(&app).ffprobe, &input, None).await?;
let plan = UniformRatioFrameSelection.create_plan(&video, &quality.preset());
Ok(ProbeAndPlan { video, plan })
Ok(ProbeAndPlan {
video: Some(video),
plan,
})
}

#[tauri::command]
Expand Down
239 changes: 239 additions & 0 deletions src-tauri/src/engines/colmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,167 @@ pub async fn match_sequential(
.await
}

/// Pairwise exhaustive matching.
///
/// Used for image-sequence inputs: unlike sequential matching it connects every
/// image pair, so an unordered photo set (and the closing loop of an orbit set)
/// is much more likely to register. It does not need a vocabulary tree, and the
/// `exhaustive_matcher` command exists on both the legacy (3.x) and modern
/// (4.x) engine families, so it is safe to use with the bundled COLMAP.
pub async fn match_exhaustive(
executable: &Path,
database: &Path,
log: PathBuf,
manager: &ProcessManager,
observer: Option<ProcessObserver>,
gpu_index: Option<u32>,
) -> Result<()> {
let (use_gpu_option, gpu_index_option) = matching_gpu_options(executable, manager).await?;
run_colmap(
executable,
exhaustive_matching_args(database, gpu_index, use_gpu_option, gpu_index_option),
database.parent().unwrap_or(Path::new(".")),
log,
manager,
observer,
)
.await
}

fn exhaustive_matching_args(
database: &Path,
gpu_index: Option<u32>,
use_gpu_option: &str,
gpu_index_option: &str,
) -> Vec<OsString> {
let mut args = vec![
"exhaustive_matcher".into(),
"--database_path".into(),
database.into(),
use_gpu_option.into(),
(if gpu_index.is_some() { "1" } else { "0" }).into(),
];
if let Some(index) = gpu_index {
args.push(gpu_index_option.into());
args.push(index.to_string().into());
}
args
}

/// Vocabulary-tree matching: robust and much faster than full pairwise
/// matching for larger un-ordered image sets. Requires a pre-trained
/// vocabulary tree (`--VocabTreeMatching.vocab_tree_path`).
#[allow(clippy::too_many_arguments)]
pub async fn match_vocab_tree(
executable: &Path,
database: &Path,
vocab_tree: &Path,
log: PathBuf,
manager: &ProcessManager,
observer: Option<ProcessObserver>,
gpu_index: Option<u32>,
) -> Result<()> {
let (use_gpu_option, gpu_index_option) = matching_gpu_options(executable, manager).await?;
run_colmap(
executable,
vocab_tree_matching_args(
database,
vocab_tree,
gpu_index,
use_gpu_option,
gpu_index_option,
),
database.parent().unwrap_or(Path::new(".")),
log,
manager,
observer,
)
.await
}

fn vocab_tree_matching_args(
database: &Path,
vocab_tree: &Path,
gpu_index: Option<u32>,
use_gpu_option: &str,
gpu_index_option: &str,
) -> Vec<OsString> {
let mut args = vec![
"vocab_tree_matcher".into(),
"--database_path".into(),
database.into(),
"--VocabTreeMatching.vocab_tree_path".into(),
vocab_tree.as_os_str().to_os_string(),
use_gpu_option.into(),
(if gpu_index.is_some() { "1" } else { "0" }).into(),
];
if let Some(index) = gpu_index {
args.push(gpu_index_option.into());
args.push(index.to_string().into());
}
args
}

/// Sequential matching with loop detection, which closes the first/last-frame
/// cycle of an orbit video (the most common cause of a sub-50% registration
/// ratio). Requires a vocabulary tree for the retrieval step.
#[allow(clippy::too_many_arguments)]
pub async fn match_sequential_loop(
executable: &Path,
database: &Path,
vocab_tree: &Path,
log: PathBuf,
manager: &ProcessManager,
observer: Option<ProcessObserver>,
gpu_index: Option<u32>,
) -> Result<()> {
let (use_gpu_option, gpu_index_option) = matching_gpu_options(executable, manager).await?;
run_colmap(
executable,
sequential_loop_args(
database,
vocab_tree,
gpu_index,
use_gpu_option,
gpu_index_option,
),
database.parent().unwrap_or(Path::new(".")),
log,
manager,
observer,
)
.await
}

fn sequential_loop_args(
database: &Path,
vocab_tree: &Path,
gpu_index: Option<u32>,
use_gpu_option: &str,
gpu_index_option: &str,
) -> Vec<OsString> {
let mut args = vec![
"sequential_matcher".into(),
"--database_path".into(),
database.into(),
use_gpu_option.into(),
(if gpu_index.is_some() { "1" } else { "0" }).into(),
];
if let Some(index) = gpu_index {
args.push(gpu_index_option.into());
args.push(index.to_string().into());
}
args.extend([
OsString::from("--SequentialMatching.overlap"),
OsString::from("10"),
OsString::from("--SequentialMatching.loop_detection"),
OsString::from("1"),
OsString::from("--SequentialMatching.vocab_tree_path"),
vocab_tree.as_os_str().to_os_string(),
]);
args
}

fn feature_extraction_args(
database: &Path,
images: &Path,
Expand All @@ -202,6 +363,14 @@ fn feature_extraction_args(
args.push(gpu_index_option.into());
args.push(index.to_string().into());
}
// More SIFT features -> richer matches -> higher registration and a more
// stable initial model. The option prefix follows the detected CLI family
// (--FeatureExtraction.* on COLMAP 4.x, --SiftExtraction.* on 3.x).
let prefix = use_gpu_option
.strip_suffix(".use_gpu")
.unwrap_or(use_gpu_option);
args.push(format!("{prefix}.max_num_features").into());
args.push("8192".into());
args
}

Expand Down Expand Up @@ -258,6 +427,76 @@ pub async fn map(
.await
}

/// Detect whether the installed COLMAP exposes the `global_mapper` command
/// (GLOMAP's global SfM pipeline). Older builds (for example the Ubuntu 3.9
/// system package) do not ship it, so callers should fall back to the
/// incremental `mapper`.
pub async fn detect_global_mapper(
executable: &Path,
manager: &ProcessManager,
) -> Result<bool> {
match command_help(executable, "help", manager).await {
Ok(help) => Ok(help.contains("global_mapper")),
Err(_) => Ok(false),
}
}

/// Run the global mapper (GLOMAP).
///
/// As COLMAP recommends, a view-graph calibrator pass runs first on a copy of
/// the database so the global solver has reasonable focal-length priors; the
/// original database is left untouched. A calibration failure is tolerated so
/// the global mapper can still run with the default focal-length priors.
pub async fn map_global(
executable: &Path,
database: &Path,
images: &Path,
output: &Path,
log: PathBuf,
manager: &ProcessManager,
observer: Option<ProcessObserver>,
) -> Result<()> {
tokio::fs::create_dir_all(output).await?;
let global_db = database.with_file_name("database_global.db");
tokio::fs::copy(database, &global_db).await?;
let working_directory = database.parent().unwrap_or(output);
let calibration = run_colmap(
executable,
vec![
"view_graph_calibrator".into(),
"--database_path".into(),
global_db.clone().into(),
],
working_directory,
log.clone(),
manager,
None,
)
.await;
if let Err(calibration_error) = calibration {
// Non-fatal: fall back to the default focal-length priors. The full
// calibration output is already captured in the COLMAP log.
let _ = calibration_error;
}
run_colmap(
executable,
vec![
"global_mapper".into(),
"--database_path".into(),
global_db.into(),
"--image_path".into(),
images.into(),
"--output_path".into(),
output.into(),
],
working_directory,
log,
manager,
observer,
)
.await
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
7 changes: 7 additions & 0 deletions src-tauri/src/engines/health.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ impl EnginePaths {
paths
}

/// Optional COLMAP vocabulary tree used for `vocab_tree_matcher` and for
/// `sequential_matcher` loop detection. When absent those features are
/// skipped and OOOSplat falls back to the existing matchers.
pub fn vocab_tree(&self) -> PathBuf {
self.root.join("vocab_tree.bin")
}

fn from_candidates(root: PathBuf) -> Self {
#[cfg(windows)]
let paths = {
Expand Down
Loading