use crate::declare::{Fusion, Plan, Recon};
use crate::densewrap::{make_match_backend, make_sift_backend, DenseConfig};
use crate::intrinsics::IntrinsicsPrior;
use crate::session::{pairs_for, zones_upto, FrameImage, FrameSet, SessionConfig};
use cumulus3d_core::io::GpsRecord;
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::sync::Arc;
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 undistort_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 stable_display: bool,
pub settle_tol: f64,
pub settle_max_zone: usize,
pub intrinsics_prior: Option<PathBuf>,
pub intrinsics_prior_sigma: f64,
pub intrinsics_prior_dist_sigma: f64,
pub seed: Option<u64>,
pub max_levels: Option<usize>,
pub fusion: Fusion,
pub live_stride: usize,
}
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(),
undistort_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,
stable_display: false,
settle_tol: 0.01,
settle_max_zone: 3,
intrinsics_prior: None,
intrinsics_prior_sigma: crate::intrinsics::DEFAULT_FOCAL_SIGMA,
intrinsics_prior_dist_sigma: crate::intrinsics::DEFAULT_DIST_SIGMA,
seed: None,
max_levels: None,
fusion: Fusion::default(),
live_stride: crate::post::LiveOptions::default().stride,
}
}
}
#[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> {
Self::discover_in(&src.join("images"), &CAMS, stride)
}
pub fn discover_in<S: AsRef<str>>(images: &Path, cams: &[S], stride: usize) -> Result<Self, String> {
let first = cams.first().ok_or("카메라 폴더 목록이 비어 있음")?.as_ref();
let dir = images.join(first);
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!("{first}_")).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.as_ref(), p);
l.pos_of.insert(n, p);
}
}
Ok(l)
}
pub fn truncate(&mut self, n: usize) {
self.frames.truncate(n);
let frames = self.frames.len();
self.pos_of.retain(|_, p| *p < frames);
}
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 {
frame_set_of(layout, &CAMS, p)
}
pub fn frame_set_of<S: AsRef<str>>(layout: &Layout, cams: &[S], p: usize) -> FrameSet {
FrameSet::new(cams.iter().map(|c| (c.as_ref().to_string(), layout.name(c.as_ref(), 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.stable_display = cfg.stable_display;
s.settle_tol = cfg.settle_tol;
s.settle_max_zone = cfg.settle_max_zone;
if let Some(p) = &cfg.intrinsics_prior {
s.intrinsics_prior = Some(Arc::new(IntrinsicsPrior::load(p, cfg.intrinsics_prior_sigma, cfg.intrinsics_prior_dist_sigma)?));
}
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> {
let recon = Recon::from_plan(Plan::from_stream(&cfg)).build().map_err(|e| e.to_string())?;
recon.run().map(|_| ())
}
#[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())));
}
}