1use crate::error::{NdimageError, NdimageResult};
9use std::f64::consts::PI;
10
11#[derive(Debug, Clone)]
19pub struct GaborFilterBank {
20 pub orientations: Vec<f64>,
22 pub frequencies: Vec<f64>,
24 pub sigma: f64,
26 pub kernel_size: usize,
28}
29
30impl GaborFilterBank {
31 pub fn new(n_orientations: usize, frequencies: &[f64], sigma: f64) -> Self {
38 let orientations: Vec<f64> = (0..n_orientations)
39 .map(|i| i as f64 * PI / n_orientations as f64)
40 .collect();
41 let kernel_size = (3.0 * sigma).ceil() as usize;
42 GaborFilterBank {
43 orientations,
44 frequencies: frequencies.to_vec(),
45 sigma,
46 kernel_size,
47 }
48 }
49
50 pub fn kernel(&self, theta: f64, frequency: f64) -> Vec<Vec<f64>> {
54 let half = self.kernel_size as isize;
55 let side = (2 * self.kernel_size + 1) as usize;
56 let sigma_sq = self.sigma * self.sigma;
57 let cos_t = theta.cos();
58 let sin_t = theta.sin();
59
60 let mut kern = vec![vec![0.0f64; side]; side];
61 for (ri, row) in kern.iter_mut().enumerate() {
62 for (ci, cell) in row.iter_mut().enumerate() {
63 let y = ri as isize - half;
64 let x = ci as isize - half;
65 let xp = x as f64 * cos_t + y as f64 * sin_t;
66 let yp = -x as f64 * sin_t + y as f64 * cos_t;
67 let gaussian = (-(xp * xp + yp * yp) / (2.0 * sigma_sq)).exp();
68 let wave = (2.0 * PI * frequency * xp).cos();
69 *cell = gaussian * wave;
70 }
71 }
72 kern
73 }
74
75 pub fn apply(&self, image: &[Vec<f64>]) -> Vec<Vec<Vec<f64>>> {
80 let rows = image.len();
81 if rows == 0 {
82 return Vec::new();
83 }
84 let cols = image[0].len();
85 let mut feature_maps = Vec::new();
86
87 for &theta in &self.orientations {
88 for &freq in &self.frequencies {
89 let kern = self.kernel(theta, freq);
90 let filtered = convolve2d(image, &kern, rows, cols);
91 feature_maps.push(filtered);
92 }
93 }
94 feature_maps
95 }
96
97 pub fn energy_features(&self, image: &[Vec<f64>]) -> Vec<f64> {
101 let maps = self.apply(image);
102 let mut feats = Vec::with_capacity(maps.len() * 2);
103 for map in &maps {
104 let flat: Vec<f64> = map.iter().flat_map(|r| r.iter().map(|v| v.abs())).collect();
105 let n = flat.len() as f64;
106 if n == 0.0 {
107 feats.push(0.0);
108 feats.push(0.0);
109 continue;
110 }
111 let mean = flat.iter().sum::<f64>() / n;
112 let var = flat.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
113 feats.push(mean);
114 feats.push(var.sqrt());
115 }
116 feats
117 }
118}
119
120#[derive(Debug, Clone)]
124pub struct SiftKeypoint {
125 pub x: f64,
127 pub y: f64,
129 pub scale: f64,
131 pub orientation: f64,
133 pub descriptor: Vec<f64>,
135}
136
137pub fn detect_sift_keypoints(
150 image: &[Vec<f64>],
151 n_octaves: usize,
152 n_scales_per_octave: usize,
153 contrast_threshold: f64,
154 edge_threshold: f64,
155) -> Vec<SiftKeypoint> {
156 let rows = image.len();
157 if rows == 0 {
158 return Vec::new();
159 }
160 let cols = image[0].len();
161 if cols == 0 {
162 return Vec::new();
163 }
164
165 let k = 2.0f64.powf(1.0 / n_scales_per_octave as f64);
166 let initial_sigma = 1.6f64;
167 let mut keypoints = Vec::new();
168
169 let mut oct_image: Vec<Vec<f64>> = image.to_vec();
172
173 for _oct in 0..n_octaves {
174 let oct_rows = oct_image.len();
175 let oct_cols = if oct_rows > 0 { oct_image[0].len() } else { 0 };
176 if oct_rows < 4 || oct_cols < 4 {
177 break;
178 }
179
180 let n_gauss = n_scales_per_octave + 3;
182 let mut gauss_stack: Vec<Vec<Vec<f64>>> = Vec::with_capacity(n_gauss);
183 gauss_stack.push(oct_image.clone());
184 for s in 1..n_gauss {
185 let sigma = initial_sigma * k.powi(s as i32);
186 let blurred = gaussian_blur(&gauss_stack[s - 1], sigma);
187 gauss_stack.push(blurred);
188 }
189
190 let mut dog_stack: Vec<Vec<Vec<f64>>> = Vec::with_capacity(n_gauss - 1);
192 for s in 0..(n_gauss - 1) {
193 let dog = subtract_images(&gauss_stack[s + 1], &gauss_stack[s]);
194 dog_stack.push(dog);
195 }
196
197 let n_dog = dog_stack.len();
199 if n_dog < 3 {
200 break;
201 }
202 for s in 1..(n_dog - 1) {
203 for r in 1..(oct_rows.saturating_sub(1)) {
204 for c in 1..(oct_cols.saturating_sub(1)) {
205 let val = dog_stack[s][r][c];
206 if val.abs() < contrast_threshold {
207 continue;
208 }
209 if !is_extremum(&dog_stack, s, r, c) {
210 continue;
211 }
212 if is_edge_point(&dog_stack[s], r, c, edge_threshold) {
214 continue;
215 }
216 let sigma = initial_sigma * k.powi(s as i32);
218 let orient = dominant_orientation(&gauss_stack[s], r, c, sigma);
219 let kp = SiftKeypoint {
220 x: c as f64,
221 y: r as f64,
222 scale: sigma,
223 orientation: orient,
224 descriptor: Vec::new(), };
226 keypoints.push(kp);
227 }
228 }
229 }
230
231 oct_image = downsample2x(&oct_image);
233 }
234
235 keypoints
236}
237
238pub fn compute_sift_descriptor(image: &[Vec<f64>], keypoints: &[SiftKeypoint]) -> Vec<Vec<f64>> {
243 let rows = image.len();
244 if rows == 0 {
245 return Vec::new();
246 }
247 let cols = image[0].len();
248 let (grad_mag, grad_ori) = gradient_images(image);
249
250 keypoints
251 .iter()
252 .map(|kp| {
253 let cx = kp.x as isize;
254 let cy = kp.y as isize;
255 let patch_half = 8isize;
256 let n_bins = 8usize;
257 let n_cells = 4usize;
259 let cell_size = (patch_half * 2 / n_cells as isize).max(1);
260 let mut desc = vec![0.0f64; 128];
261
262 for pr in 0..(patch_half * 2) {
263 for pc in 0..(patch_half * 2) {
264 let r = (cy - patch_half + pr).max(0).min(rows as isize - 1) as usize;
265 let c = (cx - patch_half + pc).max(0).min(cols as isize - 1) as usize;
266 let mag = grad_mag[r][c];
267 let ori = (grad_ori[r][c] - kp.orientation).rem_euclid(2.0 * PI);
268 let cell_r = (pr / cell_size).min((n_cells - 1) as isize) as usize;
269 let cell_c = (pc / cell_size).min((n_cells - 1) as isize) as usize;
270 let bin = ((ori / (2.0 * PI)) * n_bins as f64) as usize % n_bins;
271 let idx = cell_r * n_cells * n_bins + cell_c * n_bins + bin;
272 if idx < 128 {
273 desc[idx] += mag;
274 }
275 }
276 }
277 let norm = desc.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-12);
279 desc.iter_mut().for_each(|v| *v = (*v / norm).min(0.2));
280 let norm2 = desc.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-12);
281 desc.iter_mut().for_each(|v| *v /= norm2);
282 desc
283 })
284 .collect()
285}
286
287#[derive(Debug, Clone)]
294pub struct HogDescriptor {
295 pub orientations: usize,
297 pub pixels_per_cell: (usize, usize),
299 pub cells_per_block: (usize, usize),
301}
302
303impl HogDescriptor {
304 pub fn new(
306 orientations: usize,
307 pixels_per_cell: (usize, usize),
308 cells_per_block: (usize, usize),
309 ) -> Self {
310 HogDescriptor {
311 orientations,
312 pixels_per_cell,
313 cells_per_block,
314 }
315 }
316
317 pub fn compute(&self, image: &[Vec<f64>]) -> Vec<f64> {
319 let rows = image.len();
320 if rows == 0 {
321 return Vec::new();
322 }
323 let cols = image[0].len();
324 let (ph, pw) = self.pixels_per_cell;
325 let (bh, bw) = self.cells_per_block;
326 let n_bins = self.orientations;
327
328 let n_cells_y = rows / ph;
329 let n_cells_x = cols / pw;
330 if n_cells_y == 0 || n_cells_x == 0 {
331 return Vec::new();
332 }
333
334 let mut cell_hists = vec![vec![vec![0.0f64; n_bins]; n_cells_x]; n_cells_y];
336 let (grad_mag, grad_ori) = gradient_images(image);
337
338 for ry in 0..rows {
339 for cx in 0..cols {
340 let cy_idx = ry / ph;
341 let cx_idx = cx / pw;
342 if cy_idx >= n_cells_y || cx_idx >= n_cells_x {
343 continue;
344 }
345 let mag = grad_mag[ry][cx];
346 let ori = grad_ori[ry][cx].rem_euclid(PI);
348 let bin = ((ori / PI) * n_bins as f64) as usize % n_bins;
349 cell_hists[cy_idx][cx_idx][bin] += mag;
350 }
351 }
352
353 let n_blocks_y = n_cells_y.saturating_sub(bh - 1);
355 let n_blocks_x = n_cells_x.saturating_sub(bw - 1);
356 let block_len = bh * bw * n_bins;
357 let mut descriptor = Vec::with_capacity(n_blocks_y * n_blocks_x * block_len);
358
359 for by in 0..n_blocks_y {
360 for bx in 0..n_blocks_x {
361 let mut block = Vec::with_capacity(block_len);
362 for dy in 0..bh {
363 for dx in 0..bw {
364 for &v in &cell_hists[by + dy][bx + dx] {
365 block.push(v);
366 }
367 }
368 }
369 let norm = block.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-10);
371 block.iter_mut().for_each(|v| *v /= norm);
372 block.iter_mut().for_each(|v| *v = v.min(0.2));
374 let norm2 = block.iter().map(|v| v * v).sum::<f64>().sqrt().max(1e-10);
376 block.iter_mut().for_each(|v| *v /= norm2);
377 descriptor.extend(block);
378 }
379 }
380 descriptor
381 }
382
383 pub fn feature_size(&self, image_shape: (usize, usize)) -> usize {
385 let (rows, cols) = image_shape;
386 let (ph, pw) = self.pixels_per_cell;
387 let (bh, bw) = self.cells_per_block;
388 let n_cells_y = rows / ph;
389 let n_cells_x = cols / pw;
390 let n_blocks_y = n_cells_y.saturating_sub(bh - 1);
391 let n_blocks_x = n_cells_x.saturating_sub(bw - 1);
392 n_blocks_y * n_blocks_x * bh * bw * self.orientations
393 }
394}
395
396fn gradient_images(image: &[Vec<f64>]) -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
400 let rows = image.len();
401 let cols = if rows > 0 { image[0].len() } else { 0 };
402 let mut mag = vec![vec![0.0f64; cols]; rows];
403 let mut ori = vec![vec![0.0f64; cols]; rows];
404 for r in 0..rows {
405 for c in 0..cols {
406 let dx = if c + 1 < cols {
407 image[r][c + 1]
408 } else {
409 image[r][c]
410 } - if c > 0 { image[r][c - 1] } else { image[r][c] };
411 let dy = if r + 1 < rows {
412 image[r + 1][c]
413 } else {
414 image[r][c]
415 } - if r > 0 { image[r - 1][c] } else { image[r][c] };
416 mag[r][c] = (dx * dx + dy * dy).sqrt();
417 ori[r][c] = dy.atan2(dx);
418 }
419 }
420 (mag, ori)
421}
422
423fn convolve2d(image: &[Vec<f64>], kernel: &[Vec<f64>], rows: usize, cols: usize) -> Vec<Vec<f64>> {
425 let kh = kernel.len();
426 let kw = if kh > 0 { kernel[0].len() } else { 0 };
427 let khr = (kh / 2) as isize;
428 let kwr = (kw / 2) as isize;
429 let mut out = vec![vec![0.0f64; cols]; rows];
430 for r in 0..rows {
431 for c in 0..cols {
432 let mut acc = 0.0f64;
433 for kr in 0..kh {
434 for kc in 0..kw {
435 let nr = r as isize + kr as isize - khr;
436 let nc = c as isize + kc as isize - kwr;
437 if nr >= 0 && nc >= 0 && (nr as usize) < rows && (nc as usize) < cols {
438 acc += image[nr as usize][nc as usize] * kernel[kr][kc];
439 }
440 }
441 }
442 out[r][c] = acc;
443 }
444 }
445 out
446}
447
448fn gaussian_blur(image: &[Vec<f64>], sigma: f64) -> Vec<Vec<f64>> {
450 let radius = (3.0 * sigma).ceil() as usize;
451 let side = 2 * radius + 1;
452 let mut k1d = vec![0.0f64; side];
453 for i in 0..side {
454 let x = i as f64 - radius as f64;
455 k1d[i] = (-x * x / (2.0 * sigma * sigma)).exp();
456 }
457 let sum: f64 = k1d.iter().sum();
458 k1d.iter_mut().for_each(|v| *v /= sum);
459
460 let rows = image.len();
461 if rows == 0 {
462 return Vec::new();
463 }
464 let cols = image[0].len();
465 let mut tmp = vec![vec![0.0f64; cols]; rows];
467 for r in 0..rows {
468 for c in 0..cols {
469 let mut acc = 0.0f64;
470 for (ki, &kv) in k1d.iter().enumerate() {
471 let nc = c as isize + ki as isize - radius as isize;
472 let nc = nc.max(0).min(cols as isize - 1) as usize;
473 acc += image[r][nc] * kv;
474 }
475 tmp[r][c] = acc;
476 }
477 }
478 let mut out = vec![vec![0.0f64; cols]; rows];
480 for r in 0..rows {
481 for c in 0..cols {
482 let mut acc = 0.0f64;
483 for (ki, &kv) in k1d.iter().enumerate() {
484 let nr = r as isize + ki as isize - radius as isize;
485 let nr = nr.max(0).min(rows as isize - 1) as usize;
486 acc += tmp[nr][c] * kv;
487 }
488 out[r][c] = acc;
489 }
490 }
491 out
492}
493
494fn subtract_images(a: &[Vec<f64>], b: &[Vec<f64>]) -> Vec<Vec<f64>> {
496 a.iter()
497 .zip(b.iter())
498 .map(|(ra, rb)| ra.iter().zip(rb.iter()).map(|(va, vb)| va - vb).collect())
499 .collect()
500}
501
502fn downsample2x(image: &[Vec<f64>]) -> Vec<Vec<f64>> {
504 let rows = image.len();
505 let cols = if rows > 0 { image[0].len() } else { 0 };
506 let new_rows = (rows + 1) / 2;
507 let new_cols = (cols + 1) / 2;
508 let mut out = vec![vec![0.0f64; new_cols]; new_rows];
509 for r in 0..new_rows {
510 for c in 0..new_cols {
511 out[r][c] = image[2 * r][2 * c];
512 }
513 }
514 out
515}
516
517fn is_extremum(dog: &[Vec<Vec<f64>>], s: usize, r: usize, c: usize) -> bool {
519 let val = dog[s][r][c];
520 let is_max = val > 0.0;
521 for ds in 0..3usize {
522 let ss = s + ds - 1;
523 if ss >= dog.len() {
524 continue;
525 }
526 for dr in 0..3usize {
527 let rr = r + dr - 1;
528 if rr >= dog[ss].len() {
529 continue;
530 }
531 for dc in 0..3usize {
532 let cc = c + dc - 1;
533 if cc >= dog[ss][rr].len() {
534 continue;
535 }
536 if ds == 1 && dr == 1 && dc == 1 {
537 continue;
538 }
539 let v = dog[ss][rr][cc];
540 if is_max && v >= val {
541 return false;
542 }
543 if !is_max && v <= val {
544 return false;
545 }
546 }
547 }
548 }
549 true
550}
551
552fn is_edge_point(dog: &[Vec<f64>], r: usize, c: usize, threshold: f64) -> bool {
554 let rows = dog.len();
555 let cols = if rows > 0 { dog[0].len() } else { 0 };
556 if r == 0 || c == 0 || r + 1 >= rows || c + 1 >= cols {
557 return false;
558 }
559 let dxx = dog[r][c + 1] + dog[r][c - 1] - 2.0 * dog[r][c];
560 let dyy = dog[r + 1][c] + dog[r - 1][c] - 2.0 * dog[r][c];
561 let dxy = (dog[r + 1][c + 1] + dog[r - 1][c - 1] - dog[r + 1][c - 1] - dog[r - 1][c + 1]) / 4.0;
562 let trace = dxx + dyy;
563 let det = dxx * dyy - dxy * dxy;
564 if det <= 0.0 {
565 return true;
566 }
567 let ratio = trace * trace / det;
568 let r_thresh = (threshold + 1.0).powi(2) / threshold;
569 ratio >= r_thresh
570}
571
572fn dominant_orientation(image: &[Vec<f64>], r: usize, c: usize, sigma: f64) -> f64 {
574 let rows = image.len();
575 let cols = if rows > 0 { image[0].len() } else { 0 };
576 let radius = (1.5 * sigma).ceil() as isize;
577 let mut hist = vec![0.0f64; 36];
578 for dr in -radius..=radius {
579 for dc in -radius..=radius {
580 let nr = (r as isize + dr).max(0).min(rows as isize - 1) as usize;
581 let nc = (c as isize + dc).max(0).min(cols as isize - 1) as usize;
582 let dy = if nr + 1 < rows {
583 image[nr + 1][nc]
584 } else {
585 image[nr][nc]
586 } - if nr > 0 {
587 image[nr - 1][nc]
588 } else {
589 image[nr][nc]
590 };
591 let dx = if nc + 1 < cols {
592 image[nr][nc + 1]
593 } else {
594 image[nr][nc]
595 } - if nc > 0 {
596 image[nr][nc - 1]
597 } else {
598 image[nr][nc]
599 };
600 let mag = (dx * dx + dy * dy).sqrt();
601 let ori = dy.atan2(dx).rem_euclid(2.0 * PI);
602 let dist_sq = (dr * dr + dc * dc) as f64;
603 let weight = (-dist_sq / (2.0 * (1.5 * sigma).powi(2))).exp();
604 let bin = ((ori / (2.0 * PI)) * 36.0) as usize % 36;
605 hist[bin] += weight * mag;
606 }
607 }
608 let best_bin = hist
609 .iter()
610 .enumerate()
611 .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
612 .map(|(i, _)| i)
613 .unwrap_or(0);
614 best_bin as f64 * 2.0 * PI / 36.0
615}
616
617pub fn compute_hog(
621 image: &[Vec<f64>],
622 orientations: usize,
623 pixels_per_cell: (usize, usize),
624 cells_per_block: (usize, usize),
625) -> NdimageResult<Vec<f64>> {
626 if image.is_empty() {
627 return Err(NdimageError::InvalidInput("Image must not be empty".into()));
628 }
629 if orientations == 0 {
630 return Err(NdimageError::InvalidInput(
631 "orientations must be at least 1".into(),
632 ));
633 }
634 if pixels_per_cell.0 == 0 || pixels_per_cell.1 == 0 {
635 return Err(NdimageError::InvalidInput(
636 "pixels_per_cell dimensions must be at least 1".into(),
637 ));
638 }
639 if cells_per_block.0 == 0 || cells_per_block.1 == 0 {
640 return Err(NdimageError::InvalidInput(
641 "cells_per_block dimensions must be at least 1".into(),
642 ));
643 }
644 let hog = HogDescriptor::new(orientations, pixels_per_cell, cells_per_block);
645 Ok(hog.compute(image))
646}
647
648#[cfg(test)]
651mod tests {
652 use super::*;
653
654 fn make_checkerboard(rows: usize, cols: usize) -> Vec<Vec<f64>> {
655 (0..rows)
656 .map(|r| {
657 (0..cols)
658 .map(|c| if (r / 4 + c / 4) % 2 == 0 { 1.0 } else { 0.0 })
659 .collect()
660 })
661 .collect()
662 }
663
664 #[test]
665 fn test_gabor_bank_construction() {
666 let bank = GaborFilterBank::new(4, &[0.1, 0.2], 1.0);
667 assert_eq!(bank.orientations.len(), 4);
668 assert_eq!(bank.frequencies.len(), 2);
669 let kern = bank.kernel(0.0, 0.1);
670 assert!(!kern.is_empty());
671 }
672
673 #[test]
674 fn test_gabor_apply_shape() {
675 let bank = GaborFilterBank::new(2, &[0.1], 1.0);
676 let img = make_checkerboard(32, 32);
677 let maps = bank.apply(&img);
678 assert_eq!(maps.len(), 2); assert_eq!(maps[0].len(), 32);
680 assert_eq!(maps[0][0].len(), 32);
681 }
682
683 #[test]
684 fn test_gabor_energy_features_length() {
685 let bank = GaborFilterBank::new(3, &[0.1, 0.2], 1.5);
686 let img = make_checkerboard(32, 32);
687 let feats = bank.energy_features(&img);
688 assert_eq!(feats.len(), 12);
690 }
691
692 #[test]
693 fn test_sift_detection_runs() {
694 let img = make_checkerboard(64, 64);
695 let kps = detect_sift_keypoints(&img, 2, 3, 0.03, 10.0);
696 let _ = kps.len();
698 }
699
700 #[test]
701 fn test_sift_descriptor_shape() {
702 let img = make_checkerboard(64, 64);
703 let kps = detect_sift_keypoints(&img, 2, 3, 0.01, 10.0);
704 let descs = compute_sift_descriptor(&img, &kps);
705 assert_eq!(descs.len(), kps.len());
706 for d in &descs {
707 assert_eq!(d.len(), 128);
708 }
709 }
710
711 #[test]
712 fn test_hog_feature_size() {
713 let hog = HogDescriptor::new(9, (8, 8), (2, 2));
714 let img = make_checkerboard(64, 64);
715 let desc = hog.compute(&img);
716 let expected = hog.feature_size((64, 64));
717 assert_eq!(desc.len(), expected);
718 assert!(!desc.is_empty());
719 }
720
721 #[test]
722 fn test_hog_compute_hog_wrapper() {
723 let img = make_checkerboard(32, 32);
724 let result = compute_hog(&img, 9, (8, 8), (2, 2));
725 assert!(result.is_ok());
726 }
727
728 #[test]
729 fn test_hog_invalid_inputs() {
730 let img: Vec<Vec<f64>> = Vec::new();
731 assert!(compute_hog(&img, 9, (8, 8), (2, 2)).is_err());
732 let img2 = make_checkerboard(32, 32);
733 assert!(compute_hog(&img2, 0, (8, 8), (2, 2)).is_err());
734 assert!(compute_hog(&img2, 9, (0, 8), (2, 2)).is_err());
735 }
736}