Skip to main content

sklears_svm/
computer_vision_kernels.rs

1//! Computer Vision Kernels for SVM
2//!
3//! This module implements specialized kernels for computer vision tasks including:
4//! - Histogram Intersection Kernel
5//! - Spatial Pyramid Kernels
6//! - HOG (Histogram of Oriented Gradients) Feature Kernels
7//! - Local Binary Pattern (LBP) Kernels
8//! - Chi-Square Kernels for histograms
9//! - Earth Mover's Distance (EMD) Kernels
10
11use scirs2_core::ndarray::Array2;
12use thiserror::Error;
13
14/// Errors for computer vision kernels
15#[derive(Error, Debug)]
16pub enum CVKernelError {
17    #[error("Dimension mismatch: expected {expected}, got {actual}")]
18    DimensionMismatch { expected: usize, actual: usize },
19    #[error("Invalid histogram: negative values not allowed")]
20    InvalidHistogram,
21    #[error("Empty histogram")]
22    EmptyHistogram,
23    #[error("Invalid kernel parameters: {message}")]
24    InvalidParameters { message: String },
25}
26
27/// Computer Vision Kernel Types
28#[derive(Debug, Clone, PartialEq)]
29pub enum CVKernelType {
30    /// Histogram Intersection Kernel
31    HistogramIntersection,
32    /// Chi-Square Kernel with gamma parameter
33    ChiSquare { gamma: f64 },
34    /// Spatial Pyramid Kernel with levels and weights
35    SpatialPyramid { levels: usize, weights: Vec<f64> },
36    /// HOG Feature Kernel
37    HOG { bins: usize, cell_size: usize },
38    /// Local Binary Pattern Kernel
39    LBP { radius: f64, neighbors: usize },
40    /// Earth Mover's Distance Kernel
41    EMD { distance_matrix: Array2<f64> },
42    /// Additive Chi-Square Kernel
43    AdditiveChiSquare,
44    /// Jensen-Shannon Kernel
45    JensenShannon,
46    /// Bhattacharyya Kernel
47    Bhattacharyya,
48    /// Hellinger Kernel
49    Hellinger,
50}
51
52/// Computer Vision Kernel Function
53#[derive(Debug, Clone)]
54pub struct CVKernelFunction {
55    pub kernel_type: CVKernelType,
56}
57
58impl CVKernelFunction {
59    /// Create a new computer vision kernel function
60    pub fn new(kernel_type: CVKernelType) -> Self {
61        Self { kernel_type }
62    }
63
64    /// Compute kernel value between two feature vectors
65    pub fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, CVKernelError> {
66        if x.len() != y.len() {
67            return Err(CVKernelError::DimensionMismatch {
68                expected: x.len(),
69                actual: y.len(),
70            });
71        }
72
73        match &self.kernel_type {
74            CVKernelType::HistogramIntersection => {
75                self.validate_histogram(x)?;
76                self.validate_histogram(y)?;
77                Ok(self.histogram_intersection(x, y))
78            }
79            CVKernelType::ChiSquare { gamma } => {
80                self.validate_histogram(x)?;
81                self.validate_histogram(y)?;
82                Ok(self.chi_square_kernel(x, y, *gamma))
83            }
84            CVKernelType::SpatialPyramid { levels, weights } => {
85                self.spatial_pyramid_kernel(x, y, *levels, weights)
86            }
87            CVKernelType::HOG { bins, cell_size } => self.hog_kernel(x, y, *bins, *cell_size),
88            CVKernelType::LBP { radius, neighbors } => self.lbp_kernel(x, y, *radius, *neighbors),
89            CVKernelType::EMD { distance_matrix } => self.emd_kernel(x, y, distance_matrix),
90            CVKernelType::AdditiveChiSquare => {
91                self.validate_histogram(x)?;
92                self.validate_histogram(y)?;
93                Ok(self.additive_chi_square_kernel(x, y))
94            }
95            CVKernelType::JensenShannon => {
96                self.validate_histogram(x)?;
97                self.validate_histogram(y)?;
98                Ok(self.jensen_shannon_kernel(x, y))
99            }
100            CVKernelType::Bhattacharyya => {
101                self.validate_histogram(x)?;
102                self.validate_histogram(y)?;
103                Ok(self.bhattacharyya_kernel(x, y))
104            }
105            CVKernelType::Hellinger => {
106                self.validate_histogram(x)?;
107                self.validate_histogram(y)?;
108                Ok(self.hellinger_kernel(x, y))
109            }
110        }
111    }
112
113    /// Compute kernel matrix for datasets
114    pub fn compute_matrix(
115        &self,
116        x: &Array2<f64>,
117        y: &Array2<f64>,
118    ) -> Result<Array2<f64>, CVKernelError> {
119        let (n_x, n_features_x) = x.dim();
120        let (n_y, n_features_y) = y.dim();
121
122        if n_features_x != n_features_y {
123            return Err(CVKernelError::DimensionMismatch {
124                expected: n_features_x,
125                actual: n_features_y,
126            });
127        }
128
129        let mut kernel_matrix = Array2::zeros((n_x, n_y));
130
131        for i in 0..n_x {
132            for j in 0..n_y {
133                let x_row = x.row(i).to_vec();
134                let y_row = y.row(j).to_vec();
135                kernel_matrix[[i, j]] = self.compute(&x_row, &y_row)?;
136            }
137        }
138
139        Ok(kernel_matrix)
140    }
141
142    /// Validate histogram (non-negative values)
143    fn validate_histogram(&self, hist: &[f64]) -> Result<(), CVKernelError> {
144        if hist.is_empty() {
145            return Err(CVKernelError::EmptyHistogram);
146        }
147
148        for &value in hist {
149            if value < 0.0 {
150                return Err(CVKernelError::InvalidHistogram);
151            }
152        }
153
154        Ok(())
155    }
156
157    /// Histogram Intersection Kernel
158    fn histogram_intersection(&self, x: &[f64], y: &[f64]) -> f64 {
159        x.iter().zip(y.iter()).map(|(a, b)| a.min(*b)).sum()
160    }
161
162    /// Chi-Square Kernel
163    fn chi_square_kernel(&self, x: &[f64], y: &[f64], gamma: f64) -> f64 {
164        let chi_square_distance = x
165            .iter()
166            .zip(y.iter())
167            .map(|(a, b)| {
168                if a + b > 0.0 {
169                    (a - b).powi(2) / (a + b)
170                } else {
171                    0.0
172                }
173            })
174            .sum::<f64>();
175
176        (-gamma * chi_square_distance).exp()
177    }
178
179    /// Spatial Pyramid Kernel
180    fn spatial_pyramid_kernel(
181        &self,
182        x: &[f64],
183        y: &[f64],
184        levels: usize,
185        weights: &[f64],
186    ) -> Result<f64, CVKernelError> {
187        if weights.len() != levels + 1 {
188            return Err(CVKernelError::InvalidParameters {
189                message: format!("Expected {} weights for {} levels", levels + 1, levels),
190            });
191        }
192
193        let total_cells: usize = (0..=levels).map(|lvl| 1usize << (2 * lvl)).sum();
194
195        if x.len() != y.len() {
196            return Err(CVKernelError::DimensionMismatch {
197                expected: x.len(),
198                actual: y.len(),
199            });
200        }
201
202        if !x.len().is_multiple_of(total_cells) {
203            return Err(CVKernelError::InvalidParameters {
204                message: format!(
205                    "Feature vector length {} is not divisible by spatial pyramid cell count {}",
206                    x.len(),
207                    total_cells
208                ),
209            });
210        }
211
212        let cell_hist_size = x.len() / total_cells;
213
214        let mut kernel_value = 0.0;
215        let mut offset = 0;
216
217        for (level, weight) in weights.iter().enumerate().take(levels + 1) {
218            let grid_size = 1 << (2 * level); // 4^level cells
219            let level_span = grid_size * cell_hist_size;
220            debug_assert!(offset + level_span <= x.len());
221
222            let mut level_intersection = 0.0;
223            for cell in 0..grid_size {
224                let start = offset + cell * cell_hist_size;
225                let end = start + cell_hist_size;
226
227                let x_cell = &x[start..end];
228                let y_cell = &y[start..end];
229
230                level_intersection += self.histogram_intersection(x_cell, y_cell);
231            }
232
233            kernel_value += weight * level_intersection;
234            offset += level_span;
235        }
236
237        Ok(kernel_value)
238    }
239
240    /// HOG Feature Kernel
241    fn hog_kernel(
242        &self,
243        x: &[f64],
244        y: &[f64],
245        bins: usize,
246        cell_size: usize,
247    ) -> Result<f64, CVKernelError> {
248        if !x.len().is_multiple_of(bins * cell_size) {
249            return Err(CVKernelError::InvalidParameters {
250                message: "Feature vector length must be divisible by bins * cell_size".to_string(),
251            });
252        }
253
254        let num_cells = x.len() / (bins * cell_size);
255        let mut total_intersection = 0.0;
256
257        for cell in 0..num_cells {
258            let start = cell * bins * cell_size;
259            let end = start + bins * cell_size;
260
261            let x_cell = &x[start..end];
262            let y_cell = &y[start..end];
263
264            total_intersection += self.histogram_intersection(x_cell, y_cell);
265        }
266
267        Ok(total_intersection / num_cells as f64)
268    }
269
270    /// Local Binary Pattern Kernel
271    fn lbp_kernel(
272        &self,
273        x: &[f64],
274        y: &[f64],
275        _radius: f64,
276        neighbors: usize,
277    ) -> Result<f64, CVKernelError> {
278        // LBP features are typically histograms of local binary patterns
279        // We use histogram intersection as the base kernel
280        let expected_bins = 2_usize.pow(neighbors as u32);
281
282        if x.len() != expected_bins || y.len() != expected_bins {
283            return Err(CVKernelError::InvalidParameters {
284                message: format!(
285                    "Expected {} bins for {} neighbors",
286                    expected_bins, neighbors
287                ),
288            });
289        }
290
291        // Normalize histograms
292        let x_sum: f64 = x.iter().sum();
293        let y_sum: f64 = y.iter().sum();
294
295        if x_sum == 0.0 || y_sum == 0.0 {
296            return Ok(0.0);
297        }
298
299        let x_normalized: Vec<f64> = x.iter().map(|v| v / x_sum).collect();
300        let y_normalized: Vec<f64> = y.iter().map(|v| v / y_sum).collect();
301
302        Ok(self.histogram_intersection(&x_normalized, &y_normalized))
303    }
304
305    /// Earth Mover's Distance Kernel
306    fn emd_kernel(
307        &self,
308        x: &[f64],
309        y: &[f64],
310        distance_matrix: &Array2<f64>,
311    ) -> Result<f64, CVKernelError> {
312        if distance_matrix.nrows() != x.len() || distance_matrix.ncols() != y.len() {
313            return Err(CVKernelError::InvalidParameters {
314                message: "Distance matrix dimensions don't match feature vectors".to_string(),
315            });
316        }
317
318        // Simplified EMD calculation (optimal transport)
319        // For full EMD, we would need a linear programming solver
320        let mut emd_distance = 0.0;
321        let x_sum: f64 = x.iter().sum();
322        let y_sum: f64 = y.iter().sum();
323
324        if x_sum == 0.0 || y_sum == 0.0 {
325            return Ok(0.0);
326        }
327
328        // Normalize to make them probability distributions
329        let x_normalized: Vec<f64> = x.iter().map(|v| v / x_sum).collect();
330        let y_normalized: Vec<f64> = y.iter().map(|v| v / y_sum).collect();
331
332        // Approximate EMD using minimum cost flow
333        for i in 0..x.len() {
334            for j in 0..y.len() {
335                let flow = x_normalized[i].min(y_normalized[j]);
336                emd_distance += flow * distance_matrix[[i, j]];
337            }
338        }
339
340        // Convert distance to kernel value
341        Ok((-emd_distance).exp())
342    }
343
344    /// Additive Chi-Square Kernel
345    fn additive_chi_square_kernel(&self, x: &[f64], y: &[f64]) -> f64 {
346        x.iter()
347            .zip(y.iter())
348            .map(|(a, b)| {
349                if a + b > 0.0 {
350                    2.0 * a * b / (a + b)
351                } else {
352                    0.0
353                }
354            })
355            .sum()
356    }
357
358    /// Jensen-Shannon Kernel
359    fn jensen_shannon_kernel(&self, x: &[f64], y: &[f64]) -> f64 {
360        let x_sum: f64 = x.iter().sum();
361        let y_sum: f64 = y.iter().sum();
362
363        if x_sum == 0.0 || y_sum == 0.0 {
364            return 0.0;
365        }
366
367        let x_normalized: Vec<f64> = x.iter().map(|v| v / x_sum).collect();
368        let y_normalized: Vec<f64> = y.iter().map(|v| v / y_sum).collect();
369
370        let mut js_divergence = 0.0;
371
372        for i in 0..x.len() {
373            let p = x_normalized[i];
374            let q = y_normalized[i];
375            let m = (p + q) / 2.0;
376
377            if p > 0.0 && m > 0.0 {
378                js_divergence += p * (p / m).ln();
379            }
380            if q > 0.0 && m > 0.0 {
381                js_divergence += q * (q / m).ln();
382            }
383        }
384
385        js_divergence /= 2.0;
386
387        // Convert divergence to kernel value
388        (-js_divergence).exp()
389    }
390
391    /// Bhattacharyya Kernel
392    fn bhattacharyya_kernel(&self, x: &[f64], y: &[f64]) -> f64 {
393        let x_sum: f64 = x.iter().sum();
394        let y_sum: f64 = y.iter().sum();
395
396        if x_sum == 0.0 || y_sum == 0.0 {
397            return 0.0;
398        }
399
400        let x_normalized: Vec<f64> = x.iter().map(|v| v / x_sum).collect();
401        let y_normalized: Vec<f64> = y.iter().map(|v| v / y_sum).collect();
402
403        x_normalized
404            .iter()
405            .zip(y_normalized.iter())
406            .map(|(a, b)| (a * b).sqrt())
407            .sum()
408    }
409
410    /// Hellinger Kernel
411    fn hellinger_kernel(&self, x: &[f64], y: &[f64]) -> f64 {
412        let bhattacharyya = self.bhattacharyya_kernel(x, y);
413        bhattacharyya.sqrt()
414    }
415}
416
417/// Utilities for computer vision kernels
418pub mod cv_utils {
419    use super::*;
420
421    /// Create spatial pyramid weights (decreasing with level)
422    pub fn create_pyramid_weights(levels: usize) -> Vec<f64> {
423        let mut weights = Vec::with_capacity(levels + 1);
424
425        for level in 0..=levels {
426            if level == 0 {
427                weights.push(1.0);
428            } else {
429                weights.push(0.5 / (1 << (level - 1)) as f64);
430            }
431        }
432
433        weights
434    }
435
436    /// Create distance matrix for Earth Mover's Distance
437    pub fn create_distance_matrix(size: usize, distance_type: &str) -> Array2<f64> {
438        let mut matrix = Array2::zeros((size, size));
439
440        match distance_type {
441            "euclidean" => {
442                for i in 0..size {
443                    for j in 0..size {
444                        matrix[[i, j]] = ((i as f64 - j as f64).powi(2)).sqrt();
445                    }
446                }
447            }
448            "manhattan" => {
449                for i in 0..size {
450                    for j in 0..size {
451                        matrix[[i, j]] = (i as f64 - j as f64).abs();
452                    }
453                }
454            }
455            "grid" => {
456                // For 2D grid distances
457                let grid_size = (size as f64).sqrt() as usize;
458                for i in 0..size {
459                    for j in 0..size {
460                        let i_x = i % grid_size;
461                        let i_y = i / grid_size;
462                        let j_x = j % grid_size;
463                        let j_y = j / grid_size;
464
465                        matrix[[i, j]] = ((i_x as f64 - j_x as f64).powi(2)
466                            + (i_y as f64 - j_y as f64).powi(2))
467                        .sqrt();
468                    }
469                }
470            }
471            _ => {
472                // Default to identity matrix
473                for i in 0..size {
474                    matrix[[i, i]] = 1.0;
475                }
476            }
477        }
478
479        matrix
480    }
481
482    /// Normalize histogram to probability distribution
483    pub fn normalize_histogram(hist: &[f64]) -> Vec<f64> {
484        let sum: f64 = hist.iter().sum();
485        if sum == 0.0 {
486            return vec![0.0; hist.len()];
487        }
488        hist.iter().map(|v| v / sum).collect()
489    }
490
491    /// Compute histogram intersection efficiently
492    pub fn fast_histogram_intersection(x: &[f64], y: &[f64]) -> f64 {
493        x.iter().zip(y.iter()).map(|(a, b)| a.min(*b)).sum()
494    }
495
496    /// Compute chi-square distance between histograms
497    pub fn chi_square_distance(x: &[f64], y: &[f64]) -> f64 {
498        x.iter()
499            .zip(y.iter())
500            .map(|(a, b)| {
501                if a + b > 0.0 {
502                    (a - b).powi(2) / (a + b)
503                } else {
504                    0.0
505                }
506            })
507            .sum()
508    }
509}
510
511/// Specialized kernels for different computer vision tasks
512pub mod specialized_cv_kernels {
513    use super::*;
514
515    /// SIFT Descriptor Kernel with advanced matching
516    pub struct SIFTKernel {
517        pub sigma: f64,
518        pub use_normalization: bool,
519        pub matching_method: DescriptorMatchingMethod,
520    }
521
522    /// Descriptor matching methods
523    #[derive(Debug, Clone, PartialEq)]
524    pub enum DescriptorMatchingMethod {
525        /// Euclidean distance with RBF kernel
526        RBF,
527        /// Cosine similarity
528        CosineSimilarity,
529        /// L1 distance with Laplacian kernel
530        Laplacian,
531        /// Chi-square distance
532        ChiSquare,
533    }
534
535    impl SIFTKernel {
536        pub fn new(sigma: f64, use_normalization: bool) -> Self {
537            Self {
538                sigma,
539                use_normalization,
540                matching_method: DescriptorMatchingMethod::RBF,
541            }
542        }
543
544        pub fn with_matching_method(mut self, method: DescriptorMatchingMethod) -> Self {
545            self.matching_method = method;
546            self
547        }
548
549        pub fn compute(&self, x: &[f64], y: &[f64]) -> f64 {
550            if x.len() != y.len() || x.len() != 128 {
551                return 0.0; // SIFT descriptors are 128-dimensional
552            }
553
554            let mut x_norm = x.to_vec();
555            let mut y_norm = y.to_vec();
556
557            if self.use_normalization {
558                let x_magnitude = x.iter().map(|v| v * v).sum::<f64>().sqrt();
559                let y_magnitude = y.iter().map(|v| v * v).sum::<f64>().sqrt();
560
561                if x_magnitude > 0.0 {
562                    x_norm.iter_mut().for_each(|v| *v /= x_magnitude);
563                }
564                if y_magnitude > 0.0 {
565                    y_norm.iter_mut().for_each(|v| *v /= y_magnitude);
566                }
567            }
568
569            match self.matching_method {
570                DescriptorMatchingMethod::RBF => {
571                    // RBF kernel on normalized descriptors
572                    let squared_distance: f64 = x_norm
573                        .iter()
574                        .zip(y_norm.iter())
575                        .map(|(a, b)| (a - b).powi(2))
576                        .sum();
577                    (-squared_distance / (2.0 * self.sigma.powi(2))).exp()
578                }
579                DescriptorMatchingMethod::CosineSimilarity => {
580                    let dot_product: f64 =
581                        x_norm.iter().zip(y_norm.iter()).map(|(a, b)| a * b).sum();
582                    let x_norm_sq: f64 = x_norm.iter().map(|v| v * v).sum::<f64>().sqrt();
583                    let y_norm_sq: f64 = y_norm.iter().map(|v| v * v).sum::<f64>().sqrt();
584                    if x_norm_sq > 0.0 && y_norm_sq > 0.0 {
585                        dot_product / (x_norm_sq * y_norm_sq)
586                    } else {
587                        0.0
588                    }
589                }
590                DescriptorMatchingMethod::Laplacian => {
591                    let l1_distance: f64 = x_norm
592                        .iter()
593                        .zip(y_norm.iter())
594                        .map(|(a, b)| (a - b).abs())
595                        .sum();
596                    (-l1_distance / self.sigma).exp()
597                }
598                DescriptorMatchingMethod::ChiSquare => {
599                    let chi_square: f64 = x_norm
600                        .iter()
601                        .zip(y_norm.iter())
602                        .map(|(a, b)| {
603                            if a + b > 0.0 {
604                                (a - b).powi(2) / (a + b)
605                            } else {
606                                0.0
607                            }
608                        })
609                        .sum();
610                    (-chi_square / (2.0 * self.sigma)).exp()
611                }
612            }
613        }
614    }
615
616    /// SURF Descriptor Kernel (64 or 128 dimensional)
617    pub struct SURFKernel {
618        pub sigma: f64,
619        pub use_normalization: bool,
620        pub extended: bool, // 128-dimensional if true, 64 if false
621    }
622
623    impl SURFKernel {
624        pub fn new(sigma: f64, use_normalization: bool, extended: bool) -> Self {
625            Self {
626                sigma,
627                use_normalization,
628                extended,
629            }
630        }
631
632        pub fn compute(&self, x: &[f64], y: &[f64]) -> f64 {
633            let expected_dim = if self.extended { 128 } else { 64 };
634            if x.len() != y.len() || x.len() != expected_dim {
635                return 0.0;
636            }
637
638            let mut x_norm = x.to_vec();
639            let mut y_norm = y.to_vec();
640
641            if self.use_normalization {
642                let x_magnitude = x.iter().map(|v| v * v).sum::<f64>().sqrt();
643                let y_magnitude = y.iter().map(|v| v * v).sum::<f64>().sqrt();
644
645                if x_magnitude > 0.0 {
646                    x_norm.iter_mut().for_each(|v| *v /= x_magnitude);
647                }
648                if y_magnitude > 0.0 {
649                    y_norm.iter_mut().for_each(|v| *v /= y_magnitude);
650                }
651            }
652
653            // RBF kernel on normalized SURF descriptors
654            let squared_distance: f64 = x_norm
655                .iter()
656                .zip(y_norm.iter())
657                .map(|(a, b)| (a - b).powi(2))
658                .sum();
659
660            (-squared_distance / (2.0 * self.sigma.powi(2))).exp()
661        }
662    }
663
664    /// Color Histogram Kernel
665    pub struct ColorHistogramKernel {
666        pub bins_per_channel: usize,
667        pub num_channels: usize,
668        pub kernel_type: CVKernelType,
669    }
670
671    impl ColorHistogramKernel {
672        pub fn new(
673            bins_per_channel: usize,
674            num_channels: usize,
675            kernel_type: CVKernelType,
676        ) -> Self {
677            Self {
678                bins_per_channel,
679                num_channels,
680                kernel_type,
681            }
682        }
683
684        pub fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, CVKernelError> {
685            let expected_size = self.bins_per_channel * self.num_channels;
686            if x.len() != expected_size || y.len() != expected_size {
687                return Err(CVKernelError::DimensionMismatch {
688                    expected: expected_size,
689                    actual: x.len(),
690                });
691            }
692
693            let cv_kernel = CVKernelFunction::new(self.kernel_type.clone());
694            cv_kernel.compute(x, y)
695        }
696    }
697
698    /// Texture Kernel using Local Binary Patterns
699    pub struct TextureKernel {
700        pub radius: f64,
701        pub neighbors: usize,
702        pub uniform_patterns_only: bool,
703    }
704
705    impl TextureKernel {
706        pub fn new(radius: f64, neighbors: usize, uniform_patterns_only: bool) -> Self {
707            Self {
708                radius,
709                neighbors,
710                uniform_patterns_only,
711            }
712        }
713
714        pub fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, CVKernelError> {
715            let expected_bins = if self.uniform_patterns_only {
716                self.neighbors + 2 // Uniform patterns + 1 for non-uniform
717            } else {
718                2_usize.pow(self.neighbors as u32)
719            };
720
721            if x.len() != expected_bins || y.len() != expected_bins {
722                return Err(CVKernelError::DimensionMismatch {
723                    expected: expected_bins,
724                    actual: x.len(),
725                });
726            }
727
728            let cv_kernel = CVKernelFunction::new(CVKernelType::LBP {
729                radius: self.radius,
730                neighbors: self.neighbors,
731            });
732            cv_kernel.compute(x, y)
733        }
734    }
735
736    /// Deep Feature Extraction Kernel
737    /// Supports features from pre-trained deep learning models
738    pub struct DeepFeatureKernel {
739        pub feature_dimension: usize,
740        pub kernel_type: DeepFeatureKernelType,
741        pub normalize: bool,
742    }
743
744    /// Types of kernels for deep features
745    #[derive(Debug, Clone, PartialEq)]
746    pub enum DeepFeatureKernelType {
747        /// Linear kernel on deep features
748        Linear,
749        /// RBF kernel with specified gamma
750        RBF { gamma: f64 },
751        /// Cosine similarity kernel
752        Cosine,
753        /// Polynomial kernel
754        Polynomial { degree: f64, gamma: f64, coef0: f64 },
755    }
756
757    impl DeepFeatureKernel {
758        pub fn new(feature_dimension: usize, kernel_type: DeepFeatureKernelType) -> Self {
759            Self {
760                feature_dimension,
761                kernel_type,
762                normalize: true,
763            }
764        }
765
766        pub fn with_normalization(mut self, normalize: bool) -> Self {
767            self.normalize = normalize;
768            self
769        }
770
771        pub fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, CVKernelError> {
772            if x.len() != self.feature_dimension || y.len() != self.feature_dimension {
773                return Err(CVKernelError::DimensionMismatch {
774                    expected: self.feature_dimension,
775                    actual: x.len(),
776                });
777            }
778
779            let mut x_proc = x.to_vec();
780            let mut y_proc = y.to_vec();
781
782            if self.normalize {
783                let x_norm = x.iter().map(|v| v * v).sum::<f64>().sqrt();
784                let y_norm = y.iter().map(|v| v * v).sum::<f64>().sqrt();
785
786                if x_norm > 0.0 {
787                    x_proc.iter_mut().for_each(|v| *v /= x_norm);
788                }
789                if y_norm > 0.0 {
790                    y_proc.iter_mut().for_each(|v| *v /= y_norm);
791                }
792            }
793
794            let kernel_value = match &self.kernel_type {
795                DeepFeatureKernelType::Linear => {
796                    x_proc.iter().zip(y_proc.iter()).map(|(a, b)| a * b).sum()
797                }
798                DeepFeatureKernelType::RBF { gamma } => {
799                    let squared_dist: f64 = x_proc
800                        .iter()
801                        .zip(y_proc.iter())
802                        .map(|(a, b)| (a - b).powi(2))
803                        .sum();
804                    (-gamma * squared_dist).exp()
805                }
806                DeepFeatureKernelType::Cosine => {
807                    let dot_product: f64 =
808                        x_proc.iter().zip(y_proc.iter()).map(|(a, b)| a * b).sum();
809                    let x_magnitude = x_proc.iter().map(|v| v * v).sum::<f64>().sqrt();
810                    let y_magnitude = y_proc.iter().map(|v| v * v).sum::<f64>().sqrt();
811                    if x_magnitude > 0.0 && y_magnitude > 0.0 {
812                        dot_product / (x_magnitude * y_magnitude)
813                    } else {
814                        0.0
815                    }
816                }
817                DeepFeatureKernelType::Polynomial {
818                    degree,
819                    gamma,
820                    coef0,
821                } => {
822                    let dot_product: f64 =
823                        x_proc.iter().zip(y_proc.iter()).map(|(a, b)| a * b).sum();
824                    (gamma * dot_product + coef0).powf(*degree)
825                }
826            };
827
828            Ok(kernel_value)
829        }
830    }
831
832    /// Bag-of-Features Kernel for image classification
833    /// Uses visual vocabulary from clustered local descriptors
834    pub struct BagOfFeaturesKernel {
835        pub vocabulary_size: usize,
836        pub kernel_type: CVKernelType,
837    }
838
839    impl BagOfFeaturesKernel {
840        pub fn new(vocabulary_size: usize, kernel_type: CVKernelType) -> Self {
841            Self {
842                vocabulary_size,
843                kernel_type,
844            }
845        }
846
847        /// Compute kernel between bag-of-features histograms
848        pub fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, CVKernelError> {
849            if x.len() != self.vocabulary_size || y.len() != self.vocabulary_size {
850                return Err(CVKernelError::DimensionMismatch {
851                    expected: self.vocabulary_size,
852                    actual: x.len(),
853                });
854            }
855
856            let cv_kernel = CVKernelFunction::new(self.kernel_type.clone());
857            cv_kernel.compute(x, y)
858        }
859    }
860
861    /// Fisher Vector Kernel for image classification
862    /// Uses Gaussian Mixture Model encoding of local descriptors
863    pub struct FisherVectorKernel {
864        pub n_components: usize,
865        pub descriptor_dim: usize,
866        pub sigma: f64,
867    }
868
869    impl FisherVectorKernel {
870        pub fn new(n_components: usize, descriptor_dim: usize, sigma: f64) -> Self {
871            Self {
872                n_components,
873                descriptor_dim,
874                sigma,
875            }
876        }
877
878        /// Fisher vector dimension is 2 * n_components * descriptor_dim
879        pub fn fisher_vector_dim(&self) -> usize {
880            2 * self.n_components * self.descriptor_dim
881        }
882
883        pub fn compute(&self, x: &[f64], y: &[f64]) -> Result<f64, CVKernelError> {
884            let expected_dim = self.fisher_vector_dim();
885            if x.len() != expected_dim || y.len() != expected_dim {
886                return Err(CVKernelError::DimensionMismatch {
887                    expected: expected_dim,
888                    actual: x.len(),
889                });
890            }
891
892            // Normalize Fisher vectors (power normalization + L2 normalization)
893            let x_power: Vec<f64> = x.iter().map(|v| v.signum() * v.abs().sqrt()).collect();
894            let y_power: Vec<f64> = y.iter().map(|v| v.signum() * v.abs().sqrt()).collect();
895
896            let x_norm = x_power.iter().map(|v| v * v).sum::<f64>().sqrt();
897            let y_norm = y_power.iter().map(|v| v * v).sum::<f64>().sqrt();
898
899            let x_normalized: Vec<f64> = if x_norm > 0.0 {
900                x_power.iter().map(|v| v / x_norm).collect()
901            } else {
902                x_power
903            };
904
905            let y_normalized: Vec<f64> = if y_norm > 0.0 {
906                y_power.iter().map(|v| v / y_norm).collect()
907            } else {
908                y_power
909            };
910
911            // Linear kernel on normalized Fisher vectors
912            let kernel_value: f64 = x_normalized
913                .iter()
914                .zip(y_normalized.iter())
915                .map(|(a, b)| a * b)
916                .sum();
917
918            Ok(kernel_value)
919        }
920    }
921}
922
923#[allow(non_snake_case)]
924#[cfg(test)]
925mod tests {
926    use super::*;
927    use approx::assert_abs_diff_eq;
928
929    #[test]
930    fn test_histogram_intersection_kernel() {
931        let kernel = CVKernelFunction::new(CVKernelType::HistogramIntersection);
932
933        let x = vec![1.0, 2.0, 3.0, 4.0];
934        let y = vec![2.0, 1.0, 4.0, 3.0];
935
936        let result = kernel.compute(&x, &y).expect("computation should succeed");
937        assert_abs_diff_eq!(result, 8.0, epsilon = 1e-10);
938    }
939
940    #[test]
941    fn test_chi_square_kernel() {
942        let kernel = CVKernelFunction::new(CVKernelType::ChiSquare { gamma: 1.0 });
943
944        let x = vec![1.0, 2.0, 3.0, 4.0];
945        let y = vec![1.0, 2.0, 3.0, 4.0];
946
947        let result = kernel.compute(&x, &y).expect("computation should succeed");
948        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
949    }
950
951    #[test]
952    fn test_bhattacharyya_kernel() {
953        let kernel = CVKernelFunction::new(CVKernelType::Bhattacharyya);
954
955        let x = vec![1.0, 2.0, 3.0, 4.0];
956        let y = vec![1.0, 2.0, 3.0, 4.0];
957
958        let result = kernel.compute(&x, &y).expect("computation should succeed");
959        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
960    }
961
962    #[test]
963    fn test_additive_chi_square_kernel() {
964        let kernel = CVKernelFunction::new(CVKernelType::AdditiveChiSquare);
965
966        let x = vec![1.0, 2.0, 3.0, 4.0];
967        let y = vec![2.0, 1.0, 4.0, 3.0];
968
969        let result = kernel.compute(&x, &y).expect("computation should succeed");
970        assert!(result > 0.0);
971    }
972
973    #[test]
974    fn test_spatial_pyramid_kernel() {
975        let weights = cv_utils::create_pyramid_weights(2);
976        let kernel = CVKernelFunction::new(CVKernelType::SpatialPyramid { levels: 2, weights });
977
978        // Create feature vectors that represent spatial pyramid
979        let x = vec![
980            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
981            17.0, 18.0, 19.0, 20.0, 21.0,
982        ];
983        let y = vec![
984            1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0,
985            17.0, 18.0, 19.0, 20.0, 21.0,
986        ];
987
988        let result = kernel.compute(&x, &y).expect("computation should succeed");
989        assert!(result > 0.0);
990    }
991
992    #[test]
993    fn test_kernel_matrix_computation() {
994        let kernel = CVKernelFunction::new(CVKernelType::HistogramIntersection);
995
996        let X_var = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
997            .expect("array shape mismatch");
998        let Y_var = Array2::from_shape_vec((2, 3), vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
999            .expect("array shape mismatch");
1000
1001        let result = kernel
1002            .compute_matrix(&X_var, &Y_var)
1003            .expect("matrix computation should succeed");
1004        assert_eq!(result.dim(), (2, 2));
1005    }
1006
1007    #[test]
1008    fn test_cv_utils() {
1009        let weights = cv_utils::create_pyramid_weights(2);
1010        assert_eq!(weights.len(), 3);
1011        assert_eq!(weights[0], 1.0);
1012
1013        let distance_matrix = cv_utils::create_distance_matrix(3, "euclidean");
1014        assert_eq!(distance_matrix.dim(), (3, 3));
1015
1016        let hist = vec![1.0, 2.0, 3.0, 4.0];
1017        let normalized = cv_utils::normalize_histogram(&hist);
1018        let sum: f64 = normalized.iter().sum();
1019        assert_abs_diff_eq!(sum, 1.0, epsilon = 1e-10);
1020    }
1021
1022    #[test]
1023    fn test_specialized_kernels() {
1024        let sift_kernel = specialized_cv_kernels::SIFTKernel::new(1.0, true);
1025        let x = vec![1.0; 128];
1026        let y = vec![1.0; 128];
1027        let result = sift_kernel.compute(&x, &y);
1028        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1029
1030        let color_kernel = specialized_cv_kernels::ColorHistogramKernel::new(
1031            8,
1032            3,
1033            CVKernelType::HistogramIntersection,
1034        );
1035        let x = vec![1.0; 24];
1036        let y = vec![1.0; 24];
1037        let result = color_kernel
1038            .compute(&x, &y)
1039            .expect("computation should succeed");
1040        assert_eq!(result, 24.0);
1041    }
1042
1043    #[test]
1044    fn test_sift_kernel_advanced() {
1045        use specialized_cv_kernels::{DescriptorMatchingMethod, SIFTKernel};
1046
1047        // Test RBF matching
1048        let sift_rbf = SIFTKernel::new(1.0, true);
1049        let x = vec![1.0; 128];
1050        let y = vec![1.0; 128];
1051        let result = sift_rbf.compute(&x, &y);
1052        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1053
1054        // Test cosine similarity
1055        let sift_cosine = SIFTKernel::new(1.0, true)
1056            .with_matching_method(DescriptorMatchingMethod::CosineSimilarity);
1057        let result = sift_cosine.compute(&x, &y);
1058        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1059
1060        // Test Laplacian
1061        let sift_laplacian =
1062            SIFTKernel::new(1.0, true).with_matching_method(DescriptorMatchingMethod::Laplacian);
1063        let result = sift_laplacian.compute(&x, &y);
1064        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1065
1066        // Test different descriptors
1067        let x = vec![1.0; 128];
1068        let mut y = vec![1.0; 128];
1069        y[0] = 0.5; // Make them different
1070        let result = sift_rbf.compute(&x, &y);
1071        assert!(result < 1.0 && result > 0.0);
1072    }
1073
1074    #[test]
1075    fn test_surf_kernel() {
1076        use specialized_cv_kernels::SURFKernel;
1077
1078        // Test standard SURF (64 dimensions)
1079        let surf_64 = SURFKernel::new(1.0, true, false);
1080        let x = vec![1.0; 64];
1081        let y = vec![1.0; 64];
1082        let result = surf_64.compute(&x, &y);
1083        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1084
1085        // Test extended SURF (128 dimensions)
1086        let surf_128 = SURFKernel::new(1.0, true, true);
1087        let x = vec![1.0; 128];
1088        let y = vec![1.0; 128];
1089        let result = surf_128.compute(&x, &y);
1090        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1091
1092        // Test wrong dimension returns 0
1093        let x = vec![1.0; 32];
1094        let result = surf_64.compute(&x, &x);
1095        assert_eq!(result, 0.0);
1096    }
1097
1098    #[test]
1099    fn test_deep_feature_kernel() {
1100        use specialized_cv_kernels::{DeepFeatureKernel, DeepFeatureKernelType};
1101
1102        // Test linear kernel
1103        let deep_linear = DeepFeatureKernel::new(512, DeepFeatureKernelType::Linear);
1104        let x = vec![1.0; 512];
1105        let y = vec![1.0; 512];
1106        let result = deep_linear
1107            .compute(&x, &y)
1108            .expect("computation should succeed");
1109        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1110
1111        // Test RBF kernel
1112        let deep_rbf = DeepFeatureKernel::new(512, DeepFeatureKernelType::RBF { gamma: 0.5 });
1113        let result = deep_rbf
1114            .compute(&x, &y)
1115            .expect("computation should succeed");
1116        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1117
1118        // Test cosine similarity
1119        let deep_cosine = DeepFeatureKernel::new(512, DeepFeatureKernelType::Cosine);
1120        let result = deep_cosine
1121            .compute(&x, &y)
1122            .expect("computation should succeed");
1123        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1124
1125        // Test polynomial kernel
1126        let deep_poly = DeepFeatureKernel::new(
1127            512,
1128            DeepFeatureKernelType::Polynomial {
1129                degree: 2.0,
1130                gamma: 1.0,
1131                coef0: 1.0,
1132            },
1133        );
1134        let result = deep_poly
1135            .compute(&x, &y)
1136            .expect("computation should succeed");
1137        assert!(result > 1.0); // (1*1 + 1)^2 = 4
1138
1139        // Test dimension mismatch
1140        let x = vec![1.0; 256];
1141        let result = deep_linear.compute(&x, &y);
1142        assert!(result.is_err());
1143    }
1144
1145    #[test]
1146    fn test_bag_of_features_kernel() {
1147        use specialized_cv_kernels::BagOfFeaturesKernel;
1148
1149        let bof_kernel = BagOfFeaturesKernel::new(100, CVKernelType::HistogramIntersection);
1150        let x = vec![1.0; 100];
1151        let y = vec![1.0; 100];
1152        let result = bof_kernel
1153            .compute(&x, &y)
1154            .expect("computation should succeed");
1155        assert_eq!(result, 100.0);
1156
1157        // Test with chi-square kernel
1158        let bof_chi = BagOfFeaturesKernel::new(100, CVKernelType::ChiSquare { gamma: 1.0 });
1159        let result = bof_chi.compute(&x, &y).expect("computation should succeed");
1160        assert_abs_diff_eq!(result, 1.0, epsilon = 1e-10);
1161    }
1162
1163    #[test]
1164    fn test_fisher_vector_kernel() {
1165        use specialized_cv_kernels::FisherVectorKernel;
1166
1167        let fisher_kernel = FisherVectorKernel::new(16, 64, 1.0);
1168        let dim = fisher_kernel.fisher_vector_dim();
1169        assert_eq!(dim, 2 * 16 * 64);
1170
1171        let x = vec![1.0; dim];
1172        let y = vec![1.0; dim];
1173        let result = fisher_kernel
1174            .compute(&x, &y)
1175            .expect("computation should succeed");
1176        assert!(result > 0.0 && result <= 1.0);
1177
1178        // Test power normalization effect
1179        let mut x_varied = vec![1.0; dim];
1180        x_varied[0] = 0.5; // Make it different
1181        let result_varied = fisher_kernel
1182            .compute(&x_varied, &y)
1183            .expect("computation should succeed");
1184        assert!(result_varied < 1.0 && result_varied > 0.0);
1185    }
1186
1187    #[test]
1188    fn test_error_handling() {
1189        let kernel = CVKernelFunction::new(CVKernelType::HistogramIntersection);
1190
1191        // Test dimension mismatch
1192        let x = vec![1.0, 2.0, 3.0];
1193        let y = vec![1.0, 2.0];
1194
1195        let result = kernel.compute(&x, &y);
1196        assert!(result.is_err());
1197
1198        // Test invalid histogram (negative values)
1199        let x = vec![-1.0, 2.0, 3.0];
1200        let y = vec![1.0, 2.0, 3.0];
1201
1202        let result = kernel.compute(&x, &y);
1203        assert!(result.is_err());
1204    }
1205}