1use crate::camera_init::init_camera;
35use crate::exif_info::ExifInfo;
36use crate::gray::{self, GrayImage};
37use crate::sift::{CpuSift, SiftEngine, SiftOptions, SiftOutput};
38use cumulus3d_core::store::PosePrior;
39use cumulus3d_core::{CameraId, CameraModelKind, Error, FeatureStore, ImageId, Keypoint, Result, Vec3};
40use rayon::prelude::*;
41use std::collections::HashMap;
42use std::path::Path;
43use std::sync::Arc;
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
47pub enum CameraMode {
48 PerImage,
50 Single,
52 #[default]
54 PerFolder,
55 Existing(CameraId),
57}
58
59#[derive(Clone, Debug, PartialEq)]
61pub struct ReaderOptions {
62 pub camera_model: CameraModelKind,
64 pub camera_mode: CameraMode,
66 pub camera_params: Option<Vec<f64>>,
68 pub default_focal_length_factor: f64,
70 pub max_image_size: usize,
72 pub strict_existing_camera_size: bool,
74 pub read_pose_priors: bool,
76}
77
78impl Default for ReaderOptions {
79 fn default() -> Self {
80 Self {
81 camera_model: CameraModelKind::OpenCv,
82 camera_mode: CameraMode::PerFolder,
83 camera_params: None,
84 default_focal_length_factor: 1.2,
85 max_image_size: 3200,
86 strict_existing_camera_size: false,
87 read_pose_priors: true,
88 }
89 }
90}
91
92#[derive(Clone, Debug, PartialEq, Default)]
94pub struct ExtractionOptions {
95 pub reader: ReaderOptions,
97 pub sift: SiftOptions,
99 pub sequential_images: bool,
101}
102
103#[derive(Clone, Debug)]
105pub struct ImageSource {
106 pub name: String,
108 pub gray: GrayImage,
110 pub exif: ExifInfo,
112}
113
114#[derive(Clone, Debug, PartialEq)]
116pub enum ImageStatus {
117 Extracted {
119 image_id: ImageId,
121 camera_id: CameraId,
123 num_features: usize,
125 },
126 AlreadyExists {
128 image_id: ImageId,
130 },
131 Failed {
133 error: String,
135 },
136}
137
138#[derive(Clone, Debug, PartialEq)]
140pub struct ImageReport {
141 pub name: String,
143 pub status: ImageStatus,
145}
146
147pub fn read_image_list(path: &Path) -> Result<Vec<String>> {
149 let s = std::fs::read_to_string(path)?;
150 Ok(s.lines().map(str::trim).filter(|l| !l.is_empty()).map(str::to_string).collect())
151}
152
153pub fn image_folder(name: &str) -> &str {
155 match name.rfind('/') {
156 Some(i) => &name[..i],
157 None => "",
158 }
159}
160
161pub fn extract_for_camera(
163 backend: &dyn SiftEngine,
164 gray: &GrayImage,
165 orientation: Option<u32>,
166 cam_w: u64,
167 cam_h: u64,
168 max_image_size: usize,
169 sift: &SiftOptions,
170) -> Result<(Vec<Keypoint>, SiftOutput)> {
171 let (rw, rh) = gray::limited_size(gray.width, gray.height, max_image_size);
172 let resized;
173 let img = if (rw, rh) != (gray.width, gray.height) {
174 resized = gray.resized(rw, rh);
175 &resized
176 } else {
177 gray
178 };
179 let k = gray::orientation_to_rotation(orientation);
180 let rotated;
181 let input = if k != 0 {
182 rotated = img.rotate_ccw(k);
183 &rotated
184 } else {
185 img
186 };
187 let out = backend.extract(input, sift)?;
188 let (iw, ih) = (input.width as f32, input.height as f32);
189 let sx = cam_w as f32 / rw as f32;
190 let sy = cam_h as f32 / rh as f32;
191 let kps = out
192 .features
193 .iter()
194 .map(|f| {
195 let mut kp = f.keypoint();
196 if k != 0 {
197 kp = gray::rotate_keypoint_ccw(&kp, 4 - k, iw, ih);
198 }
199 if (rw as u64, rh as u64) != (cam_w, cam_h) {
200 kp.rescale(sx, sy);
201 }
202 kp
203 })
204 .collect();
205 Ok((kps, out))
206}
207
208#[derive(Clone)]
210pub struct FeatureExtractor {
211 backend: Arc<dyn SiftEngine>,
212}
213
214impl Default for FeatureExtractor {
215 fn default() -> Self {
216 Self::new()
217 }
218}
219
220struct Job {
222 idx: usize,
223 image_id: ImageId,
224 camera_id: CameraId,
225}
226
227impl FeatureExtractor {
228 pub fn new() -> Self {
230 Self { backend: Arc::new(CpuSift::new()) }
231 }
232
233 pub fn with_backend(backend: Arc<dyn SiftEngine>) -> Self {
235 Self { backend }
236 }
237
238 pub fn backend(&self) -> &dyn SiftEngine {
240 self.backend.as_ref()
241 }
242
243 pub fn extract_files(&self, store: &FeatureStore, root: &Path, names: &[String], opts: &ExtractionOptions) -> Result<Vec<ImageReport>> {
246 let mut names: Vec<String> = names.iter().map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect();
247 names.sort();
248 names.dedup();
249 check_existing_camera(store, &opts.reader)?;
250 let mut reports: Vec<Option<ImageReport>> = vec![None; names.len()];
251 let mut todo = Vec::new();
252 for (i, n) in names.iter().enumerate() {
253 match already_done(store, n) {
254 Some(id) => reports[i] = Some(ImageReport { name: n.clone(), status: ImageStatus::AlreadyExists { image_id: id } }),
255 None => todo.push(i),
256 }
257 }
258 let decode = |i: &usize| -> std::result::Result<ImageSource, String> {
259 let n = &names[*i];
260 let path = root.join(n);
261 let bytes = std::fs::read(&path).map_err(|e| format!("{}: {e}", path.display()))?;
262 let exif = ExifInfo::from_bytes(&bytes);
263 let img = image::load_from_memory(&bytes).map_err(|e| format!("{}: 디코딩 실패: {e}", path.display()))?;
264 let gray = gray::to_gray(&img).map_err(|e| e.to_string())?;
265 Ok(ImageSource { name: n.clone(), gray, exif })
266 };
267 let decoded: Vec<std::result::Result<ImageSource, String>> =
268 if opts.sequential_images { todo.iter().map(decode).collect() } else { todo.par_iter().map(decode).collect() };
269 let mut inputs = Vec::new();
270 let mut input_slot = Vec::new();
271 for (i, d) in todo.iter().zip(decoded) {
272 match d {
273 Ok(inp) => {
274 inputs.push(inp);
275 input_slot.push(*i);
276 }
277 Err(e) => reports[*i] = Some(ImageReport { name: names[*i].clone(), status: ImageStatus::Failed { error: e } }),
278 }
279 }
280 let sub = self.extract_inputs(store, inputs, opts)?;
281 for (slot, r) in input_slot.into_iter().zip(sub) {
283 reports[slot] = Some(r);
284 }
285 Ok(reports.into_iter().flatten().collect())
286 }
287
288 pub fn extract_ordered(
293 &self,
294 store: &FeatureStore,
295 root: &Path,
296 items: &[(String, CameraMode)],
297 opts: &ExtractionOptions,
298 ) -> Result<Vec<ImageReport>> {
299 let mut seen = std::collections::HashSet::new();
300 let items: Vec<(String, CameraMode)> =
301 items.iter().map(|(n, m)| (n.trim().to_string(), *m)).filter(|(n, _)| !n.is_empty() && seen.insert(n.clone())).collect();
302 for (_, m) in &items {
303 check_existing_mode(store, *m)?;
304 }
305 let mut reports: Vec<Option<ImageReport>> = vec![None; items.len()];
306 let mut todo = Vec::new();
307 for (i, (n, _)) in items.iter().enumerate() {
308 match already_done(store, n) {
309 Some(id) => reports[i] = Some(ImageReport { name: n.clone(), status: ImageStatus::AlreadyExists { image_id: id } }),
310 None => todo.push(i),
311 }
312 }
313 let decode = |i: &usize| -> std::result::Result<ImageSource, String> {
314 let n = &items[*i].0;
315 let path = root.join(n);
316 let bytes = std::fs::read(&path).map_err(|e| format!("{}: {e}", path.display()))?;
317 let exif = ExifInfo::from_bytes(&bytes);
318 let img = image::load_from_memory(&bytes).map_err(|e| format!("{}: 디코딩 실패: {e}", path.display()))?;
319 let gray = gray::to_gray(&img).map_err(|e| e.to_string())?;
320 Ok(ImageSource { name: n.clone(), gray, exif })
321 };
322 let decoded: Vec<std::result::Result<ImageSource, String>> =
323 if opts.sequential_images { todo.iter().map(decode).collect() } else { todo.par_iter().map(decode).collect() };
324 let (mut inputs, mut modes, mut slot) = (Vec::new(), Vec::new(), Vec::new());
325 for (i, d) in todo.iter().zip(decoded) {
326 match d {
327 Ok(inp) => {
328 inputs.push(inp);
329 modes.push(items[*i].1);
330 slot.push(*i);
331 }
332 Err(e) => reports[*i] = Some(ImageReport { name: items[*i].0.clone(), status: ImageStatus::Failed { error: e } }),
333 }
334 }
335 let sub = self.extract_inputs_impl(store, inputs, Some(modes), opts)?;
336 for (s, r) in slot.into_iter().zip(sub) {
337 reports[s] = Some(r);
338 }
339 Ok(reports.into_iter().flatten().collect())
340 }
341
342 pub fn extract_inputs(&self, store: &FeatureStore, mut inputs: Vec<ImageSource>, opts: &ExtractionOptions) -> Result<Vec<ImageReport>> {
344 inputs.sort_by(|a, b| a.name.cmp(&b.name));
345 self.extract_inputs_impl(store, inputs, None, opts)
346 }
347
348 fn extract_inputs_impl(
351 &self,
352 store: &FeatureStore,
353 inputs: Vec<ImageSource>,
354 modes: Option<Vec<CameraMode>>,
355 opts: &ExtractionOptions,
356 ) -> Result<Vec<ImageReport>> {
357 let ro = &opts.reader;
358 if modes.is_none() {
359 check_existing_camera(store, ro)?;
360 }
361 let mut reports: Vec<ImageReport> =
362 inputs.iter().map(|i| ImageReport { name: i.name.clone(), status: ImageStatus::Failed { error: String::new() } }).collect();
363
364 let mut jobs = Vec::new();
366 let mut folder_cam: HashMap<String, CameraId> = HashMap::new();
367 let mut single_cam: Option<CameraId> = None;
368 for (idx, inp) in inputs.iter().enumerate() {
369 if let Some(id) = already_done(store, &inp.name) {
370 reports[idx].status = ImageStatus::AlreadyExists { image_id: id };
371 continue;
372 }
373 let (w, h) = (inp.gray.width as u64, inp.gray.height as u64);
374 let assigned: std::result::Result<(ImageId, CameraId), String> = (|| {
375 if let Some(img) = store.image_by_name(&inp.name) {
377 return Ok((img.image_id, img.camera_id));
378 }
379 let new_cam = |store: &FeatureStore| -> std::result::Result<CameraId, String> {
380 let cam = init_camera(ro.camera_model, w, h, &inp.exif, ro.camera_params.as_deref(), ro.default_focal_length_factor)
381 .map_err(|e| e.to_string())?;
382 store.add_camera(cam).map_err(|e| e.to_string())
383 };
384 let check_size = |cid: CameraId| -> std::result::Result<(), String> {
385 let c = store.camera(cid).ok_or_else(|| format!("카메라 {cid} 없음"))?;
386 if (c.width, c.height) != (w, h) {
387 return Err(format!("크기 불일치: 영상 {w}x{h}, 카메라 {cid} {}x{}", c.width, c.height));
388 }
389 Ok(())
390 };
391 let mode = modes.as_ref().map_or(ro.camera_mode, |m| m[idx]);
392 let cam = match mode {
393 CameraMode::PerImage => new_cam(store)?,
394 CameraMode::Single => match single_cam {
395 Some(c) => {
396 check_size(c)?;
397 c
398 }
399 None => {
400 let c = new_cam(store)?;
401 single_cam = Some(c);
402 c
403 }
404 },
405 CameraMode::PerFolder => {
408 let folder = image_folder(&inp.name).to_string();
409 match folder_cam.get(&folder) {
410 Some(&c) => {
411 check_size(c)?;
412 c
413 }
414 None => {
415 let c = new_cam(store)?;
416 folder_cam.insert(folder, c);
417 c
418 }
419 }
420 }
421 CameraMode::Existing(n) => {
422 if ro.strict_existing_camera_size {
423 check_size(n)?;
424 }
425 n
426 }
427 };
428 let id = store.add_image(&inp.name, cam).map_err(|e| e.to_string())?;
429 if ro.read_pose_priors {
430 if let Some((lat, lon, alt)) = inp.exif.gps {
431 let gravity = gray::orientation_gravity(inp.exif.orientation).map(|(gx, gy)| Vec3::new(gx, gy, 0.0));
432 store.set_pose_prior(id, PosePrior { position: Vec3::new(lat, lon, alt), coordinate_system: 0, gravity });
433 }
434 }
435 Ok((id, cam))
436 })();
437 match assigned {
438 Ok((image_id, camera_id)) => jobs.push(Job { idx, image_id, camera_id }),
439 Err(e) => reports[idx].status = ImageStatus::Failed { error: e },
440 }
441 }
442
443 let run = |job: &Job| -> (usize, ImageStatus) {
445 let inp = &inputs[job.idx];
446 let Some(cam) = store.camera(job.camera_id) else {
447 return (job.idx, ImageStatus::Failed { error: format!("카메라 {} 없음", job.camera_id) });
448 };
449 match extract_for_camera(
450 self.backend.as_ref(),
451 &inp.gray,
452 inp.exif.orientation,
453 cam.width,
454 cam.height,
455 ro.max_image_size,
456 &opts.sift,
457 ) {
458 Ok((kps, out)) => {
459 let n = kps.len();
460 store.set_keypoints(job.image_id, kps);
461 store.set_descriptors(job.image_id, out.descriptors);
462 (job.idx, ImageStatus::Extracted { image_id: job.image_id, camera_id: job.camera_id, num_features: n })
463 }
464 Err(e) => (job.idx, ImageStatus::Failed { error: e.to_string() }),
465 }
466 };
467 let results: Vec<(usize, ImageStatus)> =
468 if opts.sequential_images { jobs.iter().map(run).collect() } else { jobs.par_iter().map(run).collect() };
469 for (idx, st) in results {
470 reports[idx].status = st;
471 }
472 Ok(reports)
473 }
474}
475
476fn already_done(store: &FeatureStore, name: &str) -> Option<ImageId> {
478 let id = store.image_id_by_name(name)?;
479 (store.exists_keypoints(id) && store.exists_descriptors(id)).then_some(id)
480}
481
482fn check_existing_camera(store: &FeatureStore, ro: &ReaderOptions) -> Result<Option<CameraId>> {
483 check_existing_mode(store, ro.camera_mode)
484}
485
486fn check_existing_mode(store: &FeatureStore, mode: CameraMode) -> Result<Option<CameraId>> {
487 match mode {
488 CameraMode::Existing(n) => {
489 if store.camera(n).is_none() {
490 return Err(Error::NotFound(format!("existing_camera_id {n}")));
491 }
492 Ok(Some(n))
493 }
494 _ => Ok(None),
495 }
496}