Skip to main content

scirs2_vision/feature/
descriptors.rs

1//! Unified feature descriptor API
2//!
3//! Provides a single entry point for computing feature descriptors using
4//! multiple methods (BRIEF, ORB, HOG). The descriptors are returned in a
5//! unified format that supports both binary and float descriptors.
6//!
7//! - **BRIEF**: Fast binary descriptor for keypoint matching
8//! - **ORB**: Oriented FAST + Rotated BRIEF (rotation-invariant binary descriptor)
9//! - **HOG**: Histogram of Oriented Gradients (dense float descriptor for detection)
10
11use crate::error::{Result, VisionError};
12use crate::feature::KeyPoint;
13use image::DynamicImage;
14
15/// Descriptor method selection
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum DescriptorMethod {
18    /// BRIEF binary descriptor (fast, not rotation invariant)
19    Brief,
20    /// ORB descriptor (oriented FAST + rotated BRIEF, rotation invariant)
21    Orb,
22    /// HOG descriptor (dense float descriptor, good for object detection)
23    Hog,
24}
25
26/// A computed feature descriptor in unified format
27#[derive(Debug, Clone)]
28pub struct UnifiedDescriptor {
29    /// Associated keypoint (position, scale, orientation)
30    pub keypoint: KeyPoint,
31    /// Float descriptor vector (for SIFT-like, HOG descriptors)
32    /// For binary descriptors, each bit is expanded to 0.0 or 1.0
33    pub float_vector: Vec<f32>,
34    /// Binary descriptor (only populated for BRIEF/ORB)
35    pub binary_vector: Option<Vec<u32>>,
36    /// Method used to compute this descriptor
37    pub method: DescriptorMethod,
38}
39
40/// Parameters for unified descriptor computation
41#[derive(Debug, Clone)]
42pub struct DescriptorParams {
43    /// Descriptor method to use
44    pub method: DescriptorMethod,
45    /// BRIEF descriptor size in bits (128, 256, or 512)
46    pub brief_size: usize,
47    /// BRIEF/ORB patch size
48    pub patch_size: usize,
49    /// ORB number of features
50    pub orb_num_features: usize,
51    /// HOG cell size
52    pub hog_cell_size: usize,
53    /// HOG number of orientation bins
54    pub hog_num_bins: usize,
55}
56
57impl Default for DescriptorParams {
58    fn default() -> Self {
59        Self {
60            method: DescriptorMethod::Brief,
61            brief_size: 256,
62            patch_size: 48,
63            orb_num_features: 500,
64            hog_cell_size: 8,
65            hog_num_bins: 9,
66        }
67    }
68}
69
70/// Compute descriptors for given keypoints using the specified method
71///
72/// # Arguments
73///
74/// * `img` - Input image
75/// * `keypoints` - Detected keypoints to compute descriptors for
76/// * `params` - Descriptor computation parameters
77///
78/// # Returns
79///
80/// * Vector of unified descriptors
81///
82/// # Example
83///
84/// ```rust,no_run
85/// use scirs2_vision::feature::descriptors::{compute_descriptors, DescriptorMethod, DescriptorParams};
86/// use scirs2_vision::feature::KeyPoint;
87/// use image::DynamicImage;
88///
89/// # fn main() -> scirs2_vision::error::Result<()> {
90/// let img = image::open("test.jpg").expect("Failed to open");
91/// let keypoints = vec![
92///     KeyPoint { x: 50.0, y: 50.0, scale: 1.0, orientation: 0.0, response: 1.0 },
93/// ];
94/// let params = DescriptorParams {
95///     method: DescriptorMethod::Brief,
96///     ..DescriptorParams::default()
97/// };
98/// let descriptors = compute_descriptors(&img, &keypoints, &params)?;
99/// # Ok(())
100/// # }
101/// ```
102pub fn compute_descriptors(
103    img: &DynamicImage,
104    keypoints: &[KeyPoint],
105    params: &DescriptorParams,
106) -> Result<Vec<UnifiedDescriptor>> {
107    match params.method {
108        DescriptorMethod::Brief => compute_brief_unified(img, keypoints, params),
109        DescriptorMethod::Orb => compute_orb_unified(img, keypoints, params),
110        DescriptorMethod::Hog => compute_hog_unified(img, keypoints, params),
111    }
112}
113
114/// Compute BRIEF descriptors and convert to unified format
115fn compute_brief_unified(
116    img: &DynamicImage,
117    keypoints: &[KeyPoint],
118    params: &DescriptorParams,
119) -> Result<Vec<UnifiedDescriptor>> {
120    let config = crate::feature::brief::BriefConfig {
121        descriptor_size: params.brief_size,
122        patch_size: params.patch_size,
123        use_smoothing: true,
124        smoothing_sigma: 2.0,
125    };
126
127    let brief_descs =
128        crate::feature::brief::compute_brief_descriptors(img, keypoints.to_vec(), &config)?;
129
130    let mut unified = Vec::with_capacity(brief_descs.len());
131    for desc in brief_descs {
132        let float_vector = binary_to_float(&desc.descriptor, params.brief_size);
133        unified.push(UnifiedDescriptor {
134            keypoint: desc.keypoint,
135            float_vector,
136            binary_vector: Some(desc.descriptor),
137            method: DescriptorMethod::Brief,
138        });
139    }
140
141    Ok(unified)
142}
143
144/// Compute ORB descriptors and convert to unified format
145fn compute_orb_unified(
146    img: &DynamicImage,
147    _keypoints: &[KeyPoint],
148    params: &DescriptorParams,
149) -> Result<Vec<UnifiedDescriptor>> {
150    let config = crate::feature::orb::OrbConfig {
151        num_features: params.orb_num_features,
152        patch_size: params.patch_size.min(31),
153        ..crate::feature::orb::OrbConfig::default()
154    };
155
156    // ORB detects its own keypoints and computes descriptors
157    let orb_descs = crate::feature::orb::detect_and_compute_orb(img, &config)?;
158
159    let mut unified = Vec::with_capacity(orb_descs.len());
160    for desc in orb_descs {
161        let float_vector = binary_to_float(&desc.descriptor, 256);
162        unified.push(UnifiedDescriptor {
163            keypoint: desc.keypoint,
164            float_vector,
165            binary_vector: Some(desc.descriptor),
166            method: DescriptorMethod::Orb,
167        });
168    }
169
170    Ok(unified)
171}
172
173/// Compute HOG descriptor and create unified descriptors
174///
175/// HOG computes a dense descriptor for the entire image. We create one
176/// UnifiedDescriptor per input keypoint, extracting the local HOG features
177/// around each keypoint's cell.
178fn compute_hog_unified(
179    img: &DynamicImage,
180    keypoints: &[KeyPoint],
181    params: &DescriptorParams,
182) -> Result<Vec<UnifiedDescriptor>> {
183    let config = crate::feature::hog::HogConfig {
184        cell_size: params.hog_cell_size,
185        num_bins: params.hog_num_bins,
186        ..crate::feature::hog::HogConfig::default()
187    };
188
189    let hog_desc = crate::feature::hog::compute_hog(img, &config)?;
190
191    // For each keypoint, extract the local cell's histogram as its descriptor
192    let cell_size = params.hog_cell_size;
193    let bins = params.hog_num_bins;
194    let cells_x = hog_desc.cells_x;
195
196    let mut unified = Vec::with_capacity(keypoints.len());
197
198    for kp in keypoints {
199        let cell_x = (kp.x as usize) / cell_size;
200        let cell_y = (kp.y as usize) / cell_size;
201
202        if cell_x >= hog_desc.cells_x || cell_y >= hog_desc.cells_y {
203            continue;
204        }
205
206        // Extract multi-cell neighborhood (3x3 cells around keypoint)
207        let mut float_vector = Vec::new();
208        for dy in -1i32..=1 {
209            for dx in -1i32..=1 {
210                let cy = cell_y as i32 + dy;
211                let cx = cell_x as i32 + dx;
212
213                if cy >= 0
214                    && cy < hog_desc.cells_y as i32
215                    && cx >= 0
216                    && cx < hog_desc.cells_x as i32
217                {
218                    let offset = (cy as usize * cells_x + cx as usize) * bins;
219                    if offset + bins <= hog_desc.features.len() {
220                        float_vector.extend_from_slice(&hog_desc.features[offset..offset + bins]);
221                    }
222                } else {
223                    // Pad with zeros for border cells
224                    float_vector.extend(std::iter::repeat_n(0.0f32, bins));
225                }
226            }
227        }
228
229        unified.push(UnifiedDescriptor {
230            keypoint: kp.clone(),
231            float_vector,
232            binary_vector: None,
233            method: DescriptorMethod::Hog,
234        });
235    }
236
237    Ok(unified)
238}
239
240/// Convert binary descriptor words to float vector
241fn binary_to_float(binary: &[u32], num_bits: usize) -> Vec<f32> {
242    let mut float_vec = Vec::with_capacity(num_bits);
243
244    for (word_idx, &word) in binary.iter().enumerate() {
245        for bit in 0..32 {
246            if word_idx * 32 + bit >= num_bits {
247                break;
248            }
249            let val = if word & (1 << bit) != 0 {
250                1.0f32
251            } else {
252                0.0f32
253            };
254            float_vec.push(val);
255        }
256    }
257
258    float_vec
259}
260
261/// Match two sets of unified descriptors
262///
263/// For binary descriptors, uses Hamming distance.
264/// For float descriptors, uses Euclidean distance.
265///
266/// # Arguments
267///
268/// * `desc1` - First set of descriptors
269/// * `desc2` - Second set of descriptors
270/// * `max_distance` - Maximum distance for a valid match (normalized 0-1)
271///
272/// # Returns
273///
274/// * Vector of (index1, index2, distance) tuples for matched descriptors
275pub fn match_unified_descriptors(
276    desc1: &[UnifiedDescriptor],
277    desc2: &[UnifiedDescriptor],
278    max_distance: f32,
279) -> Vec<(usize, usize, f32)> {
280    if desc1.is_empty() || desc2.is_empty() {
281        return Vec::new();
282    }
283
284    let use_binary = desc1[0].binary_vector.is_some() && desc2[0].binary_vector.is_some();
285    let mut matches = Vec::new();
286
287    for (i, d1) in desc1.iter().enumerate() {
288        let mut best_dist = f32::MAX;
289        let mut best_idx = 0;
290        let mut second_best = f32::MAX;
291
292        for (j, d2) in desc2.iter().enumerate() {
293            let dist = if use_binary {
294                match (d1.binary_vector.as_ref(), d2.binary_vector.as_ref()) {
295                    (Some(b1), Some(b2)) => {
296                        let hamming: u32 = b1
297                            .iter()
298                            .zip(b2.iter())
299                            .map(|(&a, &b)| (a ^ b).count_ones())
300                            .sum();
301                        let max_bits = (b1.len() * 32) as f32;
302                        hamming as f32 / max_bits
303                    }
304                    _ => euclidean_distance(&d1.float_vector, &d2.float_vector),
305                }
306            } else {
307                euclidean_distance(&d1.float_vector, &d2.float_vector)
308            };
309
310            if dist < best_dist {
311                second_best = best_dist;
312                best_dist = dist;
313                best_idx = j;
314            } else if dist < second_best {
315                second_best = dist;
316            }
317        }
318
319        // Apply distance threshold and Lowe's ratio test
320        if best_dist < max_distance && best_dist < second_best * 0.75 {
321            matches.push((i, best_idx, best_dist));
322        }
323    }
324
325    matches.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
326    matches
327}
328
329/// Compute normalized Euclidean distance between two float vectors
330fn euclidean_distance(a: &[f32], b: &[f32]) -> f32 {
331    let len = a.len().min(b.len());
332    if len == 0 {
333        return f32::MAX;
334    }
335
336    let sum_sq: f32 = a
337        .iter()
338        .take(len)
339        .zip(b.iter().take(len))
340        .map(|(x, y)| {
341            let d = x - y;
342            d * d
343        })
344        .sum();
345
346    sum_sq.sqrt() / (len as f32).sqrt()
347}
348
349#[cfg(test)]
350mod tests {
351    use super::*;
352
353    #[test]
354    fn test_binary_to_float() {
355        let binary = vec![0b00001111u32]; // bits 0-3 set
356        let float_vec = binary_to_float(&binary, 8);
357        assert_eq!(float_vec.len(), 8);
358        assert_eq!(float_vec[0], 1.0); // bit 0 set
359        assert_eq!(float_vec[3], 1.0); // bit 3 set
360        assert_eq!(float_vec[4], 0.0); // bit 4 not set
361        assert_eq!(float_vec[7], 0.0); // bit 7 not set
362    }
363
364    #[test]
365    fn test_euclidean_distance_identical() {
366        let a = vec![1.0, 2.0, 3.0];
367        let dist = euclidean_distance(&a, &a);
368        assert!(dist.abs() < 1e-6);
369    }
370
371    #[test]
372    fn test_euclidean_distance_different() {
373        let a = vec![0.0, 0.0, 0.0];
374        let b = vec![1.0, 0.0, 0.0];
375        let dist = euclidean_distance(&a, &b);
376        // sqrt(1) / sqrt(3) ~= 0.577
377        assert!((dist - 1.0 / 3.0f32.sqrt()).abs() < 0.01);
378    }
379
380    #[test]
381    fn test_match_unified_empty() {
382        let matches = match_unified_descriptors(&[], &[], 1.0);
383        assert!(matches.is_empty());
384    }
385
386    #[test]
387    fn test_match_unified_float_descriptors() {
388        let desc1 = vec![UnifiedDescriptor {
389            keypoint: KeyPoint {
390                x: 10.0,
391                y: 10.0,
392                scale: 1.0,
393                orientation: 0.0,
394                response: 1.0,
395            },
396            float_vector: vec![1.0, 0.0, 0.0, 0.0],
397            binary_vector: None,
398            method: DescriptorMethod::Hog,
399        }];
400
401        let desc2 = vec![
402            UnifiedDescriptor {
403                keypoint: KeyPoint {
404                    x: 20.0,
405                    y: 20.0,
406                    scale: 1.0,
407                    orientation: 0.0,
408                    response: 1.0,
409                },
410                float_vector: vec![1.0, 0.0, 0.0, 0.0],
411                binary_vector: None,
412                method: DescriptorMethod::Hog,
413            },
414            UnifiedDescriptor {
415                keypoint: KeyPoint {
416                    x: 30.0,
417                    y: 30.0,
418                    scale: 1.0,
419                    orientation: 0.0,
420                    response: 1.0,
421                },
422                float_vector: vec![0.0, 1.0, 0.0, 0.0],
423                binary_vector: None,
424                method: DescriptorMethod::Hog,
425            },
426        ];
427
428        // Same descriptor should match well
429        let matches = match_unified_descriptors(&desc1, &desc2, 1.0);
430        // With only 2 candidates and ratio test, may or may not match
431        // Just verify it runs without errors
432        assert!(matches.len() <= 1);
433    }
434
435    #[test]
436    fn test_descriptor_params_default() {
437        let params = DescriptorParams::default();
438        assert_eq!(params.method, DescriptorMethod::Brief);
439        assert_eq!(params.brief_size, 256);
440        assert!(params.patch_size > 0);
441    }
442
443    #[test]
444    fn test_compute_descriptors_brief_no_keypoints() {
445        let img = DynamicImage::new_luma8(64, 64);
446        let params = DescriptorParams {
447            method: DescriptorMethod::Brief,
448            ..DescriptorParams::default()
449        };
450
451        let descs =
452            compute_descriptors(&img, &[], &params).expect("BRIEF with empty keypoints failed");
453        assert!(descs.is_empty());
454    }
455
456    #[test]
457    fn test_compute_descriptors_hog_empty_keypoints() {
458        let img = DynamicImage::new_luma8(64, 64);
459        let params = DescriptorParams {
460            method: DescriptorMethod::Hog,
461            ..DescriptorParams::default()
462        };
463
464        let descs =
465            compute_descriptors(&img, &[], &params).expect("HOG with empty keypoints failed");
466        assert!(descs.is_empty());
467    }
468
469    #[test]
470    fn test_compute_hog_descriptor_has_values() {
471        // Create an image with some gradient
472        let mut buf = image::GrayImage::new(64, 64);
473        for y in 0..64u32 {
474            for x in 0..64u32 {
475                buf.put_pixel(x, y, image::Luma([(x * 4) as u8]));
476            }
477        }
478        let img = DynamicImage::ImageLuma8(buf);
479
480        let keypoints = vec![KeyPoint {
481            x: 32.0,
482            y: 32.0,
483            scale: 1.0,
484            orientation: 0.0,
485            response: 1.0,
486        }];
487
488        let params = DescriptorParams {
489            method: DescriptorMethod::Hog,
490            hog_cell_size: 8,
491            hog_num_bins: 9,
492            ..DescriptorParams::default()
493        };
494
495        let descs = compute_descriptors(&img, &keypoints, &params)
496            .expect("HOG descriptor computation failed");
497        assert_eq!(descs.len(), 1);
498        // 3x3 neighborhood x 9 bins = 81 features
499        assert_eq!(descs[0].float_vector.len(), 81);
500        assert!(descs[0].binary_vector.is_none());
501    }
502}