use crate::densewrap::{make_match_backend, make_pm_backend, make_sift_backend, parse_profile, DenseConfig};
use crate::pipeline::Pipeline;
use crate::session::{pairs_for, zones_upto, FrameImage, FrameSet, Input, Session, SessionConfig};
use crate::sinks::{DefaultSinks, SinkOptions};
use cumulus3d_core::io::{read_gps_file, GpsRecord};
use cumulus3d_dense::DepthMapCache;
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
pub const CAMS: [&str; 3] = ["camF", "camR", "camL"];
#[derive(Clone, Debug)]
pub struct StreamConfig {
pub src: PathBuf,
pub out: PathBuf,
pub span: usize,
pub overlap: usize,
pub stride: usize,
pub incremental_triangulation: bool,
pub depth_cache: bool,
pub fixed_enu_origin: bool,
pub pm_backend: String,
pub mvs_profile: String,
pub sift_backend: String,
pub match_backend: String,
pub threads: usize,
pub max_positions: Option<usize>,
pub save_models: bool,
pub serialize_dense: bool,
pub dense_max_image_size: i64,
pub number_views: usize,
pub max_num_features: usize,
pub sift_max_image_size: usize,
pub echo: bool,
pub no_dense: bool,
pub seed: Option<u64>,
}
impl StreamConfig {
pub fn new(src: PathBuf, out: PathBuf) -> Self {
Self {
src,
out,
span: 12,
overlap: 2,
stride: 3,
incremental_triangulation: false,
depth_cache: false,
fixed_enu_origin: false,
pm_backend: "cuda".into(),
mvs_profile: "fast".into(),
sift_backend: "cpu".into(),
match_backend: "cpu".into(),
threads: 0,
max_positions: None,
save_models: false,
serialize_dense: false,
dense_max_image_size: 960,
number_views: 10,
max_num_features: 8192,
sift_max_image_size: 3200,
echo: true,
no_dense: false,
seed: None,
}
}
}
#[derive(Clone, Debug)]
pub struct Layout {
pub frames: Vec<u32>,
pub pos_of: HashMap<String, usize>,
}
impl Layout {
pub fn discover(src: &Path, stride: usize) -> Result<Self, String> {
let dir = src.join("images").join(CAMS[0]);
let rd = std::fs::read_dir(&dir).map_err(|e| format!("{}: {e}", dir.display()))?;
let mut frames = Vec::new();
for e in rd.flatten() {
let n = e.file_name().to_string_lossy().to_string();
let Some(stem) = n.strip_prefix(&format!("{}_", CAMS[0])).and_then(|s| s.strip_suffix(".jpg")) else { continue };
if let Ok(i) = stem.parse::<u32>() {
if (i as usize).is_multiple_of(stride.max(1)) {
frames.push(i);
}
}
}
frames.sort_unstable();
frames.dedup();
let mut l = Self { frames, pos_of: HashMap::new() };
for p in 0..l.frames.len() {
for c in CAMS {
let n = l.name(c, p);
l.pos_of.insert(n, p);
}
}
Ok(l)
}
pub fn name(&self, cam: &str, p: usize) -> String {
format!("{cam}/{cam}_{:04}.jpg", self.frames[p])
}
pub fn position(&self, name: &str) -> Option<usize> {
self.pos_of.get(name).copied()
}
}
pub fn frame_set(layout: &Layout, p: usize) -> FrameSet {
FrameSet::new(CAMS.iter().map(|c| (c.to_string(), layout.name(c, p))))
}
pub fn pairs_for_position(layout: &Layout, p: usize) -> Vec<(String, String)> {
let frames: Vec<Vec<FrameImage>> = (0..=p).map(|q| frame_set(layout, q).images).collect();
pairs_for(&frames, p)
}
pub fn regions(npos: usize, span: usize, ovl: usize) -> Vec<(usize, usize, usize)> {
zones_upto(npos, span, ovl).into_iter().map(|z| (z.zone, z.lo, z.hi)).collect()
}
pub fn session_config(cfg: &StreamConfig, npos: usize, gps: Vec<GpsRecord>, dense: Option<DenseConfig>) -> Result<SessionConfig, String> {
let mut s = SessionConfig::new(cfg.src.join("images"));
s.gps = gps;
s.span = cfg.span;
s.overlap = cfg.overlap;
s.total_positions = Some(npos);
s.incremental_triangulation = cfg.incremental_triangulation;
s.fixed_enu_origin = cfg.fixed_enu_origin;
s.seed = cfg.seed;
s.extraction.sift.max_num_features = cfg.max_num_features;
s.extraction.reader.max_image_size = cfg.sift_max_image_size;
s.sift = make_sift_backend(&cfg.sift_backend)?;
s.matcher = Arc::from(make_match_backend(&cfg.match_backend)?);
s.dense = dense;
s.history = 0;
Ok(s)
}
pub fn run_stream(cfg: StreamConfig) -> Result<(), String> {
if cfg.threads > 0 {
let _ = rayon::ThreadPoolBuilder::new().num_threads(cfg.threads).build_global();
}
let backend = make_pm_backend(&cfg.pm_backend);
let profile = parse_profile(&cfg.mvs_profile)?;
let out = cfg.out.clone();
if out.exists() {
std::fs::remove_dir_all(&out).map_err(|e| format!("{}: {e}", out.display()))?;
}
let sinks = DefaultSinks::new(&out, &SinkOptions { echo: cfg.echo, save_models: cfg.save_models }).map_err(|e| e.to_string())?;
let mut layout = Layout::discover(&cfg.src, cfg.stride)?;
if let Some(m) = cfg.max_positions {
layout.frames.truncate(m);
let frames = layout.frames.len();
layout.pos_of.retain(|_, p| *p < frames);
}
let npos = layout.frames.len();
let gps_path = cfg.src.join("gps_ref.txt");
let gps_all: Vec<GpsRecord> = read_gps_file(&gps_path)
.map_err(|e| format!("{}: {e}", gps_path.display()))?
.into_iter()
.filter(|g| layout.pos_of.contains_key(&g.name))
.collect();
let dense = (!cfg.no_dense).then(|| {
let mut dense = DenseConfig::new(backend, profile);
dense.undistort.max_image_size = cfg.dense_max_image_size;
dense.densify.neighbors.num_views = cfg.number_views;
if cfg.serialize_dense {
dense.lock = Some(Arc::new(Mutex::new(())));
}
if cfg.depth_cache {
dense.depth_cache = Some(Arc::new(DepthMapCache::new()));
}
dense
});
let session = Session::new(session_config(&cfg, npos, gps_all, dense)?);
let mut pl = Pipeline::new(session).on_any(sinks.into_hook());
let (span, ovl) = (cfg.span, cfg.overlap);
let mut reg_end: BTreeMap<usize, Vec<(usize, usize, usize)>> = BTreeMap::new();
for (k, lo, hi) in regions(npos, span, ovl) {
reg_end.entry(hi - 1).or_default().push((k, lo, hi));
}
pl.push(Input::Log(format!(
"regions: {}",
reg_end
.iter()
.map(|(e, v)| format!("{e} ->{}", v.iter().map(|(k, lo, hi)| format!(" {k}:{lo}:{hi}")).collect::<String>()))
.collect::<Vec<_>>()
.join(", ")
)));
pl.push(Input::Log(format!(
"설정: span {span} overlap {ovl} stride {} 증분삼각측량 {} 깊이캐시 {} 고정원점 {} pm {}({})/sift {}/match {} 직렬조밀화 {} 왜곡보정최대 {} 뷰 {} 스레드 {}",
cfg.stride,
cfg.incremental_triangulation,
cfg.depth_cache,
cfg.fixed_enu_origin,
cfg.pm_backend,
cfg.mvs_profile,
cfg.sift_backend,
cfg.match_backend,
cfg.serialize_dense,
cfg.dense_max_image_size,
cfg.number_views,
rayon::current_num_threads()
)));
for p in 0..npos {
pl.push(frame_set(&layout, p).into());
if let Some(e) = pl.reducer().failed() {
return Err(e.to_string());
}
}
let _ = pl.finish();
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn regions_match_script() {
assert_eq!(regions(26, 12, 2), vec![(0, 0, 14), (1, 10, 26), (2, 22, 26)]);
assert_eq!(regions(40, 12, 2), vec![(0, 0, 14), (1, 10, 26), (2, 22, 38), (3, 34, 40)]);
}
#[test]
fn pairs_rule() {
let frames: Vec<u32> = (0..20).map(|i| i * 3).collect();
let mut l = Layout { frames, pos_of: HashMap::new() };
l.pos_of.insert("x".into(), 0);
let p0 = pairs_for_position(&l, 0);
assert_eq!(p0.len(), 3);
let p17 = pairs_for_position(&l, 17);
assert_eq!(p17.len(), 21 + 27);
assert!(p17.contains(&("camF/camF_0003.jpg".to_string(), "camF/camF_0051.jpg".to_string())));
}
}