1pub mod detect;
35pub mod orient;
36pub mod pyramid;
37
38use crate::gray::GrayImage;
39use cumulus3d_core::{Descriptors, Error, Keypoint, Result};
40use detect::{Candidate, DetectParams};
41pub use orient::DescriptorNormalization;
42use orient::{Gradient, OrientParams};
43use pyramid::{BufferPool, ScaleSpace};
44use rayon::prelude::*;
45
46#[derive(Clone, Copy, Debug, PartialEq, Default)]
48pub enum FeatureSelection {
49 #[default]
51 CompatLevels,
52 TopK,
54 SpatialGrid {
57 cells: u32,
59 },
60}
61
62#[derive(Clone, Debug, PartialEq)]
64pub struct SiftOptions {
65 pub max_num_features: usize,
67 pub first_octave: i32,
69 pub num_octaves: Option<usize>,
71 pub octave_resolution: usize,
73 pub peak_threshold: f32,
75 pub edge_threshold: f32,
77 pub max_num_orientations: usize,
79 pub upright: bool,
81 pub normalization: DescriptorNormalization,
83 pub truncate_width_to_4: bool,
85 pub refinement_iterations: usize,
87 pub reject_singular_refinement: bool,
89 pub orientation_bin_interpolation: bool,
91 pub selection: FeatureSelection,
93}
94
95impl Default for SiftOptions {
96 fn default() -> Self {
97 Self {
98 max_num_features: 8192,
99 first_octave: -1,
100 num_octaves: None,
101 octave_resolution: 3,
102 peak_threshold: 0.006667,
103 edge_threshold: 10.0,
104 max_num_orientations: 2,
105 upright: false,
106 normalization: DescriptorNormalization::L1Root,
107 truncate_width_to_4: true,
108 refinement_iterations: 1,
109 reject_singular_refinement: false,
110 orientation_bin_interpolation: false,
111 selection: FeatureSelection::CompatLevels,
112 }
113 }
114}
115
116#[derive(Clone, Copy, Debug, PartialEq)]
118pub struct SiftFeature {
119 pub x: f32,
121 pub y: f32,
123 pub scale: f32,
125 pub orientation: f32,
127 pub octave: i32,
129 pub level: u32,
131 pub response: f32,
133}
134
135impl SiftFeature {
136 pub fn keypoint(&self) -> Keypoint {
138 Keypoint::from_scale_orientation(self.x, self.y, self.scale, self.orientation)
139 }
140}
141
142#[derive(Clone, Debug, Default)]
144pub struct SiftOutput {
145 pub features: Vec<SiftFeature>,
147 pub descriptors: Descriptors,
149}
150
151impl SiftOutput {
152 pub fn keypoints(&self) -> Vec<Keypoint> {
154 self.features.iter().map(|f| f.keypoint()).collect()
155 }
156 pub fn len(&self) -> usize {
158 self.features.len()
159 }
160 pub fn is_empty(&self) -> bool {
162 self.features.is_empty()
163 }
164}
165
166pub trait SiftEngine: Send + Sync {
168 fn name(&self) -> &str;
170 fn extract(&self, image: &GrayImage, options: &SiftOptions) -> Result<SiftOutput>;
172}
173
174#[derive(Default)]
176pub struct CpuSift {
177 pool: BufferPool,
178}
179
180impl CpuSift {
181 pub fn new() -> Self {
183 Self::default()
184 }
185}
186
187struct LevelFeats {
189 oi: usize,
190 j: usize,
191 items: Vec<(Candidate, u16)>,
192}
193
194impl SiftEngine for CpuSift {
195 fn name(&self) -> &str {
196 "cpu"
197 }
198
199 fn extract(&self, image: &GrayImage, opts: &SiftOptions) -> Result<SiftOutput> {
200 if opts.octave_resolution == 0 {
201 return Err(Error::InvalidArgument("octave_resolution 은 1 이상".into()));
202 }
203 if opts.first_octave != -1 && opts.first_octave != 0 {
204 return Err(Error::Unsupported(format!("first_octave {} (−1, 0 만 지원)", opts.first_octave)));
205 }
206 let w = if opts.truncate_width_to_4 { image.width - image.width % 4 } else { image.width };
207 let h = image.height;
208 if w < 8 || h < 8 {
209 return Ok(SiftOutput::default());
210 }
211 let mut base = self.pool.take(w * h);
213 base.par_chunks_mut(w).enumerate().for_each(|(y, row)| {
214 let src = &image.data[y * image.width..y * image.width + w];
215 for (o, &v) in row.iter_mut().zip(src) {
216 *o = v as f32 / 255.0;
217 }
218 });
219 let o_min = opts.first_octave;
220 let (w0, h0, base) = if o_min == -1 {
221 let mut up = self.pool.take(4 * w * h);
222 pyramid::upsample2(&base, w, h, &mut up);
223 self.pool.give(base);
224 (2 * w, 2 * h, up)
225 } else {
226 (w, h, base)
227 };
228 let ss = ScaleSpace::new(opts.octave_resolution);
229 let n_oct = opts.num_octaves.unwrap_or_else(|| pyramid::auto_num_octaves(w0, h0)).max(1);
230 let pyr = pyramid::build_pyramid(base, w0, h0, o_min, n_oct, ss, 0.5, &self.pool);
231
232 let s = ss.s;
233 let k_max = if opts.max_num_features == 0 { usize::MAX } else { opts.max_num_features };
234 let dp = DetectParams {
235 peak_threshold: opts.peak_threshold,
236 edge_threshold: opts.edge_threshold,
237 refinement_iterations: opts.refinement_iterations,
238 reject_singular: opts.reject_singular_refinement,
239 sigma0: ss.sigma0,
240 k: ss.k,
241 };
242 let op = OrientParams { max_num_orientations: opts.max_num_orientations, bin_interpolation: opts.orientation_bin_interpolation };
243
244 let mut levels: Vec<LevelFeats> = Vec::new();
247 let (mut cum_det, mut cum_or) = (0usize, 0usize);
248 'outer: for oi in (0..pyr.len()).rev() {
249 let oct = &pyr[oi];
250 let (ow, oh) = (oct.w, oct.h);
251 let mut dogs: Vec<Vec<f32>> = Vec::new();
252 for j in (0..s).rev() {
253 let stop = match opts.selection {
254 FeatureSelection::CompatLevels => cum_det > k_max,
255 FeatureSelection::TopK => cum_or >= k_max,
256 FeatureSelection::SpatialGrid { .. } => false,
257 };
258 if stop {
259 for d in dogs {
260 self.pool.give(d);
261 }
262 break 'outer;
263 }
264 if dogs.is_empty() {
265 for d in 0..s + 2 {
266 let mut buf = self.pool.take(ow * oh);
267 detect::dog(&oct.gauss[d + 1], &oct.gauss[d], &mut buf);
268 dogs.push(buf);
269 }
270 }
271 let cands = detect::detect_level(&dogs[j], &dogs[j + 1], &dogs[j + 2], ow, oh, j, &dp);
272 cum_det += cands.len();
273 let items: Vec<(Candidate, u16)> = if opts.upright {
274 cands.into_iter().map(|c| (c, 0u16)).collect()
275 } else {
276 let grad = Gradient::lazy(oct.level(j as i32), ow, oh);
278 cands
279 .par_iter()
280 .flat_map_iter(|c| orient::orientations(&grad, c.x, c.y, c.sigma, &op).into_iter().map(move |q| (*c, q)))
281 .collect()
282 };
283 cum_or += items.len();
284 levels.push(LevelFeats { oi, j, items });
285 }
286 for d in dogs {
287 self.pool.give(d);
288 }
289 }
290
291 select(&mut levels, opts.selection, k_max, &pyr);
292
293 levels.sort_by_key(|l| (l.oi, l.j));
295 let total: usize = levels.iter().map(|l| l.items.len()).sum();
296 let mut features = Vec::with_capacity(total);
297 let mut desc = Descriptors::with_capacity(total);
298 for lv in &levels {
299 if lv.items.is_empty() {
300 continue;
301 }
302 let oct = &pyr[lv.oi];
303 let g = oct.level(lv.j as i32);
304 let sig = ss.sigma(lv.j as f32);
305 let work = lv.items.len() as f64 * 225.0 * (sig as f64).powi(2);
306 let grad = if work > (oct.w * oct.h) as f64 { Gradient::precomputed(g, oct.w, oct.h) } else { Gradient::lazy(g, oct.w, oct.h) };
307 let ds: Vec<[u8; 128]> = lv
308 .items
309 .par_iter()
310 .map(|(c, q)| orient::descriptor(&grad, c.x, c.y, c.sigma, orient::dequantize_angle(*q), opts.normalization))
311 .collect();
312 let scale_o = 2f32.powi(oct.o);
313 for ((c, q), d) in lv.items.iter().zip(&ds) {
314 let th = orient::dequantize_angle(*q);
315 let ori = (std::f32::consts::TAU - th).rem_euclid(std::f32::consts::TAU);
316 features.push(SiftFeature {
317 x: scale_o * (c.x - 0.5) + 0.5,
318 y: scale_o * (c.y - 0.5) + 0.5,
319 scale: scale_o * c.sigma,
320 orientation: ori,
321 octave: oct.o,
322 level: lv.j as u32,
323 response: c.response,
324 });
325 desc.push(d);
326 }
327 }
328 for oct in pyr {
329 for g in oct.gauss {
330 self.pool.give(g);
331 }
332 }
333 Ok(SiftOutput { features, descriptors: desc })
334 }
335}
336
337fn select(levels: &mut [LevelFeats], mode: FeatureSelection, k: usize, pyr: &[pyramid::Octave]) {
339 let total: usize = levels.iter().map(|l| l.items.len()).sum();
340 if total <= k {
341 return;
342 }
343 match mode {
344 FeatureSelection::CompatLevels => {
345 let mut total = total;
347 for lv in levels.iter_mut().rev() {
348 if total - lv.items.len() > k {
349 total -= lv.items.len();
350 lv.items.clear();
351 } else {
352 break;
353 }
354 }
355 }
356 FeatureSelection::TopK => {
357 let mut remaining = k;
358 for lv in levels.iter_mut() {
359 if lv.items.len() <= remaining {
360 remaining -= lv.items.len();
361 continue;
362 }
363 let mut idx: Vec<usize> = (0..lv.items.len()).collect();
364 idx.sort_by(|&a, &b| lv.items[b].0.response.abs().total_cmp(&lv.items[a].0.response.abs()).then(a.cmp(&b)));
365 let mut keep = vec![false; lv.items.len()];
366 for &i in idx.iter().take(remaining) {
367 keep[i] = true;
368 }
369 let mut it = keep.iter();
370 lv.items.retain(|_| *it.next().unwrap_or(&false));
371 remaining = 0;
372 }
373 }
374 FeatureSelection::SpatialGrid { cells } => {
375 let cells = cells.max(1) as usize;
376 let o0 = &pyr[0];
378 let sc = 2f32.powi(o0.o);
379 let (iw, ih) = (o0.w as f32 * sc, o0.h as f32 * sc);
380 let mut buckets: Vec<Vec<(f32, usize, usize)>> = vec![Vec::new(); cells * cells];
381 for (li, lv) in levels.iter().enumerate() {
382 let so = 2f32.powi(pyr[lv.oi].o);
383 for (ii, (c, _)) in lv.items.iter().enumerate() {
384 let x = so * (c.x - 0.5) + 0.5;
385 let y = so * (c.y - 0.5) + 0.5;
386 let cx = ((x / iw * cells as f32) as usize).min(cells - 1);
387 let cy = ((y / ih * cells as f32) as usize).min(cells - 1);
388 buckets[cy * cells + cx].push((c.response.abs(), li, ii));
389 }
390 }
391 for b in &mut buckets {
392 b.sort_by(|a, b| b.0.total_cmp(&a.0).then((a.1, a.2).cmp(&(b.1, b.2))));
393 }
394 let mut keep: Vec<Vec<bool>> = levels.iter().map(|l| vec![false; l.items.len()]).collect();
395 let mut taken = 0;
396 let mut round = 0;
397 while taken < k {
398 let mut any = false;
399 for b in &buckets {
400 if let Some(&(_, li, ii)) = b.get(round) {
401 any = true;
402 if taken < k {
403 keep[li][ii] = true;
404 taken += 1;
405 }
406 }
407 }
408 if !any {
409 break;
410 }
411 round += 1;
412 }
413 for (lv, kp) in levels.iter_mut().zip(keep) {
414 let mut it = kp.into_iter();
415 lv.items.retain(|_| it.next().unwrap_or(false));
416 }
417 }
418 }
419}