use crate::camera_init::init_camera;
use crate::exif_info::ExifInfo;
use crate::gray::{self, GrayImage};
use crate::sift::{CpuSift, SiftEngine, SiftOptions, SiftOutput};
use cumulus3d_core::store::PosePrior;
use cumulus3d_core::{CameraId, CameraModelKind, Error, FeatureStore, ImageId, Keypoint, Result, Vec3};
use rayon::prelude::*;
use std::collections::HashMap;
use std::path::Path;
use std::sync::Arc;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum CameraMode {
PerImage,
Single,
#[default]
PerFolder,
Existing(CameraId),
}
#[derive(Clone, Debug, PartialEq)]
pub struct ReaderOptions {
pub camera_model: CameraModelKind,
pub camera_mode: CameraMode,
pub camera_params: Option<Vec<f64>>,
pub default_focal_length_factor: f64,
pub max_image_size: usize,
pub strict_existing_camera_size: bool,
pub read_pose_priors: bool,
}
impl Default for ReaderOptions {
fn default() -> Self {
Self {
camera_model: CameraModelKind::OpenCv,
camera_mode: CameraMode::PerFolder,
camera_params: None,
default_focal_length_factor: 1.2,
max_image_size: 3200,
strict_existing_camera_size: false,
read_pose_priors: true,
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct ExtractionOptions {
pub reader: ReaderOptions,
pub sift: SiftOptions,
pub sequential_images: bool,
}
#[derive(Clone, Debug)]
pub struct ImageSource {
pub name: String,
pub gray: GrayImage,
pub exif: ExifInfo,
}
#[derive(Clone, Debug, PartialEq)]
pub enum ImageStatus {
Extracted {
image_id: ImageId,
camera_id: CameraId,
num_features: usize,
},
AlreadyExists {
image_id: ImageId,
},
Failed {
error: String,
},
}
#[derive(Clone, Debug, PartialEq)]
pub struct ImageReport {
pub name: String,
pub status: ImageStatus,
}
pub fn read_image_list(path: &Path) -> Result<Vec<String>> {
let s = std::fs::read_to_string(path)?;
Ok(s.lines().map(str::trim).filter(|l| !l.is_empty()).map(str::to_string).collect())
}
pub fn image_folder(name: &str) -> &str {
match name.rfind('/') {
Some(i) => &name[..i],
None => "",
}
}
pub fn extract_for_camera(
backend: &dyn SiftEngine,
gray: &GrayImage,
orientation: Option<u32>,
cam_w: u64,
cam_h: u64,
max_image_size: usize,
sift: &SiftOptions,
) -> Result<(Vec<Keypoint>, SiftOutput)> {
let (rw, rh) = gray::limited_size(gray.width, gray.height, max_image_size);
let resized;
let img = if (rw, rh) != (gray.width, gray.height) {
resized = gray.resized(rw, rh);
&resized
} else {
gray
};
let k = gray::orientation_to_rotation(orientation);
let rotated;
let input = if k != 0 {
rotated = img.rotate_ccw(k);
&rotated
} else {
img
};
let out = backend.extract(input, sift)?;
let (iw, ih) = (input.width as f32, input.height as f32);
let sx = cam_w as f32 / rw as f32;
let sy = cam_h as f32 / rh as f32;
let kps = out
.features
.iter()
.map(|f| {
let mut kp = f.keypoint();
if k != 0 {
kp = gray::rotate_keypoint_ccw(&kp, 4 - k, iw, ih);
}
if (rw as u64, rh as u64) != (cam_w, cam_h) {
kp.rescale(sx, sy);
}
kp
})
.collect();
Ok((kps, out))
}
#[derive(Clone)]
pub struct FeatureExtractor {
backend: Arc<dyn SiftEngine>,
}
impl Default for FeatureExtractor {
fn default() -> Self {
Self::new()
}
}
struct Job {
idx: usize,
image_id: ImageId,
camera_id: CameraId,
}
impl FeatureExtractor {
pub fn new() -> Self {
Self { backend: Arc::new(CpuSift::new()) }
}
pub fn with_backend(backend: Arc<dyn SiftEngine>) -> Self {
Self { backend }
}
pub fn backend(&self) -> &dyn SiftEngine {
self.backend.as_ref()
}
pub fn extract_files(&self, store: &FeatureStore, root: &Path, names: &[String], opts: &ExtractionOptions) -> Result<Vec<ImageReport>> {
let mut names: Vec<String> = names.iter().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
names.sort();
names.dedup();
check_existing_camera(store, &opts.reader)?;
let mut reports: Vec<Option<ImageReport>> = vec![None; names.len()];
let mut todo = Vec::new();
for (i, n) in names.iter().enumerate() {
match already_done(store, n) {
Some(id) => reports[i] = Some(ImageReport { name: n.clone(), status: ImageStatus::AlreadyExists { image_id: id } }),
None => todo.push(i),
}
}
let decode = |i: &usize| -> std::result::Result<ImageSource, String> {
let n = &names[*i];
let path = root.join(n);
let bytes = std::fs::read(&path).map_err(|e| format!("{}: {e}", path.display()))?;
let exif = ExifInfo::from_bytes(&bytes);
let img = image::load_from_memory(&bytes).map_err(|e| format!("{}: 디코딩 실패: {e}", path.display()))?;
let gray = gray::to_gray(&img).map_err(|e| e.to_string())?;
Ok(ImageSource { name: n.clone(), gray, exif })
};
let decoded: Vec<std::result::Result<ImageSource, String>> =
if opts.sequential_images { todo.iter().map(decode).collect() } else { todo.par_iter().map(decode).collect() };
let mut inputs = Vec::new();
let mut input_slot = Vec::new();
for (i, d) in todo.iter().zip(decoded) {
match d {
Ok(inp) => {
inputs.push(inp);
input_slot.push(*i);
}
Err(e) => reports[*i] = Some(ImageReport { name: names[*i].clone(), status: ImageStatus::Failed { error: e } }),
}
}
let sub = self.extract_inputs(store, inputs, opts)?;
for (slot, r) in input_slot.into_iter().zip(sub) {
reports[slot] = Some(r);
}
Ok(reports.into_iter().flatten().collect())
}
pub fn extract_inputs(&self, store: &FeatureStore, mut inputs: Vec<ImageSource>, opts: &ExtractionOptions) -> Result<Vec<ImageReport>> {
inputs.sort_by(|a, b| a.name.cmp(&b.name));
let ro = &opts.reader;
check_existing_camera(store, ro)?;
let mut reports: Vec<ImageReport> =
inputs.iter().map(|i| ImageReport { name: i.name.clone(), status: ImageStatus::Failed { error: String::new() } }).collect();
let mut jobs = Vec::new();
let mut folder_cam: HashMap<String, CameraId> = HashMap::new();
let mut single_cam: Option<CameraId> = None;
for (idx, inp) in inputs.iter().enumerate() {
if let Some(id) = already_done(store, &inp.name) {
reports[idx].status = ImageStatus::AlreadyExists { image_id: id };
continue;
}
let (w, h) = (inp.gray.width as u64, inp.gray.height as u64);
let assigned: std::result::Result<(ImageId, CameraId), String> = (|| {
if let Some(img) = store.image_by_name(&inp.name) {
return Ok((img.image_id, img.camera_id));
}
let new_cam = |store: &FeatureStore| -> std::result::Result<CameraId, String> {
let cam = init_camera(ro.camera_model, w, h, &inp.exif, ro.camera_params.as_deref(), ro.default_focal_length_factor)
.map_err(|e| e.to_string())?;
store.add_camera(cam).map_err(|e| e.to_string())
};
let check_size = |cid: CameraId| -> std::result::Result<(), String> {
let c = store.camera(cid).ok_or_else(|| format!("카메라 {cid} 없음"))?;
if (c.width, c.height) != (w, h) {
return Err(format!("크기 불일치: 영상 {w}x{h}, 카메라 {cid} {}x{}", c.width, c.height));
}
Ok(())
};
let cam = match ro.camera_mode {
CameraMode::PerImage => new_cam(store)?,
CameraMode::Single => match single_cam {
Some(c) => {
check_size(c)?;
c
}
None => {
let c = new_cam(store)?;
single_cam = Some(c);
c
}
},
CameraMode::PerFolder => {
let folder = image_folder(&inp.name).to_string();
match folder_cam.get(&folder) {
Some(&c) => {
check_size(c)?;
c
}
None => {
let c = new_cam(store)?;
folder_cam.insert(folder, c);
c
}
}
}
CameraMode::Existing(n) => {
if ro.strict_existing_camera_size {
check_size(n)?;
}
n
}
};
let id = store.add_image(&inp.name, cam).map_err(|e| e.to_string())?;
if ro.read_pose_priors {
if let Some((lat, lon, alt)) = inp.exif.gps {
let gravity = gray::orientation_gravity(inp.exif.orientation).map(|(gx, gy)| Vec3::new(gx, gy, 0.0));
store.set_pose_prior(id, PosePrior { position: Vec3::new(lat, lon, alt), coordinate_system: 0, gravity });
}
}
Ok((id, cam))
})();
match assigned {
Ok((image_id, camera_id)) => jobs.push(Job { idx, image_id, camera_id }),
Err(e) => reports[idx].status = ImageStatus::Failed { error: e },
}
}
let run = |job: &Job| -> (usize, ImageStatus) {
let inp = &inputs[job.idx];
let Some(cam) = store.camera(job.camera_id) else {
return (job.idx, ImageStatus::Failed { error: format!("카메라 {} 없음", job.camera_id) });
};
match extract_for_camera(
self.backend.as_ref(),
&inp.gray,
inp.exif.orientation,
cam.width,
cam.height,
ro.max_image_size,
&opts.sift,
) {
Ok((kps, out)) => {
let n = kps.len();
store.set_keypoints(job.image_id, kps);
store.set_descriptors(job.image_id, out.descriptors);
(job.idx, ImageStatus::Extracted { image_id: job.image_id, camera_id: job.camera_id, num_features: n })
}
Err(e) => (job.idx, ImageStatus::Failed { error: e.to_string() }),
}
};
let results: Vec<(usize, ImageStatus)> =
if opts.sequential_images { jobs.iter().map(run).collect() } else { jobs.par_iter().map(run).collect() };
for (idx, st) in results {
reports[idx].status = st;
}
Ok(reports)
}
}
fn already_done(store: &FeatureStore, name: &str) -> Option<ImageId> {
let id = store.image_id_by_name(name)?;
(store.exists_keypoints(id) && store.exists_descriptors(id)).then_some(id)
}
fn check_existing_camera(store: &FeatureStore, ro: &ReaderOptions) -> Result<Option<CameraId>> {
match ro.camera_mode {
CameraMode::Existing(n) => {
if store.camera(n).is_none() {
return Err(Error::NotFound(format!("existing_camera_id {n}")));
}
Ok(Some(n))
}
_ => Ok(None),
}
}