Skip to main content

iris/features/
matching.rs

1use crate::core::types::{Point, Scalar};
2use crate::error::{IrisError, Result};
3use crate::features::KeyPoint;
4use crate::image::Image;
5use burn::tensor::{Tensor, TensorData, backend::Backend};
6
7/// Represents a descriptor match between two keypoints.
8#[derive(Clone, Copy, Debug, PartialEq)]
9pub struct DMatch {
10    pub query_idx: usize,
11    pub train_idx: usize,
12    pub img_idx: usize,
13    pub distance: f32,
14}
15
16pub struct BFMatcher;
17
18impl BFMatcher {
19    /// Performs brute-force matching between query descriptors [N1, D] and train descriptors [N2, D].
20    pub fn match_descriptors<B: Backend>(
21        &self,
22        query: &Tensor<B, 2>,
23        train: &Tensor<B, 2>,
24    ) -> Result<Vec<DMatch>> {
25        let q_dims = query.dims();
26        let t_dims = train.dims();
27        let n1 = q_dims[0];
28        let _n2 = t_dims[0];
29
30        // Query shape: [N1, 1, D], Train shape: [1, N2, D]
31        let q_unsqueezed = query.clone().unsqueeze_dim::<3>(1);
32        let t_unsqueezed = train.clone().unsqueeze_dim::<3>(0);
33
34        let diff = q_unsqueezed.sub(t_unsqueezed);
35        let squared_diff = diff.powf_scalar(2.0);
36        let dists = squared_diff.sum_dim(2).squeeze::<2>().sqrt(); // [N1, N2]
37
38        let min_dists = dists.clone().min_dim(1).squeeze::<1>(); // [N1]
39        let argmins = dists.argmin(1).squeeze::<1>(); // [N1]
40
41        let min_dists_data = min_dists.into_data();
42        let argmins_data = argmins.into_data();
43
44        let min_dists_vec: Vec<f32> = min_dists_data.iter::<f32>().collect();
45        let argmins_vec: Vec<i32> = argmins_data.iter::<i32>().collect();
46
47        let mut matches = Vec::new();
48        for i in 0..n1 {
49            matches.push(DMatch {
50                query_idx: i,
51                train_idx: argmins_vec[i] as usize,
52                img_idx: 0,
53                distance: min_dists_vec[i],
54            });
55        }
56        Ok(matches)
57    }
58}
59
60// ---------------------------------------------------------------------------
61// FLANN-based KD-tree matcher
62// ---------------------------------------------------------------------------
63
64/// A single node in the KD-tree used by the FLANN matcher.
65struct KdNode {
66    /// Index of the feature vector in the train set.
67    idx: usize,
68    /// Split axis (0 .. D-1).
69    axis: usize,
70    /// Threshold value stored at this node (the coordinate along `axis`).
71    threshold: f32,
72    /// Optional left child index in the nodes vec (None for leaves).
73    left: Option<usize>,
74    /// Optional right child index in the nodes vec.
75    right: Option<usize>,
76}
77
78/// FLANN-inspired approximate nearest-neighbour matcher built on a set of
79/// KD-trees.
80///
81/// Each tree is built by recursively partitioning the descriptor space along
82/// alternating axes. At query time the trees are searched in parallel with a
83/// limited number of checks per tree to trade accuracy for speed.
84pub struct FlannMatcher {
85    /// Number of nearest neighbours to return per query descriptor.
86    k: usize,
87    /// Number of KD-trees in the forest.
88    trees: usize,
89    /// Maximum number of leaf-node checks before the search is terminated
90    /// (the *checks* parameter in FLANN).
91    checks: usize,
92}
93
94impl Default for FlannMatcher {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100impl FlannMatcher {
101    /// Creates a FLANN matcher with reasonable defaults (`k=2`, `trees=5`,
102    /// `checks=32`).
103    #[must_use]
104    pub fn new() -> Self {
105        Self {
106            k: 2,
107            trees: 5,
108            checks: 32,
109        }
110    }
111
112    /// Sets the number of nearest neighbours.
113    #[must_use]
114    pub fn with_k(mut self, k: usize) -> Self {
115        self.k = k;
116        self
117    }
118
119    /// Sets the number of trees in the forest.
120    #[must_use]
121    pub fn with_trees(mut self, trees: usize) -> Self {
122        self.trees = trees;
123        self
124    }
125
126    /// Sets the maximum number of leaf checks during search.
127    #[must_use]
128    pub fn with_checks(mut self, checks: usize) -> Self {
129        self.checks = checks;
130        self
131    }
132
133    // -- internal helpers ---------------------------------------------------
134
135    /// Recursively build a KD-tree over the slice of (index, feature) pairs.
136    fn build_kd_tree(
137        items: &mut [(usize, Vec<f32>)],
138        depth: usize,
139        nodes: &mut Vec<KdNode>,
140    ) -> Option<usize> {
141        if items.is_empty() {
142            return None;
143        }
144        if items.len() == 1 {
145            let idx = items[0].0;
146            let axis = depth % items[0].1.len();
147            let threshold = items[0].1[axis];
148            let node_idx = nodes.len();
149            nodes.push(KdNode {
150                idx,
151                axis,
152                threshold,
153                left: None,
154                right: None,
155            });
156            return Some(node_idx);
157        }
158
159        let dim = items[0].1.len();
160        let axis = depth % dim;
161
162        // Median-of-three pivot selection
163        let mid = items.len() / 2;
164        // Simple nth_element via partial sort
165        items.select_nth_unstable_by(mid, |a, b| {
166            a.1[axis]
167                .partial_cmp(&b.1[axis])
168                .unwrap_or(std::cmp::Ordering::Equal)
169        });
170
171        let threshold = items[mid].1[axis];
172        let split_idx = mid;
173
174        let node = KdNode {
175            idx: items[split_idx].0,
176            axis,
177            threshold,
178            left: None,
179            right: None,
180        };
181        let node_pos = nodes.len();
182        nodes.push(node);
183
184        let (left_items, right_items) = items.split_at_mut(split_idx);
185        let (right_items, _) = right_items.split_at_mut(1); // skip the pivot
186
187        let left_child = Self::build_kd_tree(left_items, depth + 1, nodes);
188        let right_child = Self::build_kd_tree(right_items, depth + 1, nodes);
189
190        nodes[node_pos].left = left_child;
191        nodes[node_pos].right = right_child;
192
193        Some(node_pos)
194    }
195
196    /// Search the KD-tree for the single nearest neighbour of `query`.
197    /// Returns (train_idx, distance).
198    fn search_nn(
199        nodes: &[KdNode],
200        train: &[Vec<f32>],
201        query: &[f32],
202        root: Option<usize>,
203        checks_remaining: &mut usize,
204    ) -> (usize, f32) {
205        let mut best_idx = 0usize;
206        let mut best_dist = f32::MAX;
207        Self::search_nn_recursive(
208            nodes,
209            train,
210            query,
211            root,
212            checks_remaining,
213            &mut best_idx,
214            &mut best_dist,
215        );
216        (best_idx, best_dist)
217    }
218
219    fn search_nn_recursive(
220        nodes: &[KdNode],
221        train: &[Vec<f32>],
222        query: &[f32],
223        node_idx: Option<usize>,
224        checks: &mut usize,
225        best_idx: &mut usize,
226        best_dist: &mut f32,
227    ) {
228        let idx = match node_idx {
229            Some(i) => i,
230            None => return,
231        };
232        if *checks == 0 {
233            return;
234        }
235
236        let node = &nodes[idx];
237
238        // Leaf or we still have budget: evaluate this node
239        let dist = euclidean_dist_sq(query, &train[node.idx]);
240        if dist < *best_dist {
241            *best_dist = dist;
242            *best_idx = node.idx;
243        }
244        *checks = checks.saturating_sub(1);
245
246        let diff = query[node.axis] - node.threshold;
247        let (near, far) = if diff <= 0.0 {
248            (node.left, node.right)
249        } else {
250            (node.right, node.left)
251        };
252
253        Self::search_nn_recursive(nodes, train, query, near, checks, best_idx, best_dist);
254
255        // Only visit the far subtree if the splitting hyperplane is closer
256        // than the current best distance.
257        if diff * diff < *best_dist {
258            Self::search_nn_recursive(nodes, train, query, far, checks, best_idx, best_dist);
259        }
260    }
261
262    // -- public API ---------------------------------------------------------
263
264    /// Finds approximate nearest-neighbour matches between two descriptor
265    /// matrices.
266    ///
267    /// `desc1` is the query matrix of shape `[N1, D]` and `desc2` is the
268    /// train matrix of shape `[N2, D]`. The function returns one
269    /// [`DMatch`] per query descriptor.
270    pub fn find_matches<B: Backend>(
271        &self,
272        desc1: &Tensor<B, 2>,
273        desc2: &Tensor<B, 2>,
274    ) -> Result<Vec<DMatch>> {
275        let q_dims = desc1.dims();
276        let t_dims = desc2.dims();
277        let n1 = q_dims[0];
278        let n2 = t_dims[0];
279        let dim = q_dims[1];
280
281        if dim != t_dims[1] {
282            return Err(IrisError::DimensionMismatch {
283                expected: vec![n1, dim],
284                actual: vec![n2, t_dims[1]],
285            });
286        }
287
288        let q_data = desc1.clone().into_data();
289        let t_data = desc2.clone().into_data();
290        let q_flat: Vec<f32> = q_data.iter::<f32>().collect();
291        let t_flat: Vec<f32> = t_data.iter::<f32>().collect();
292
293        // Materialise into Vec<Vec<f32>> for indexed access
294        let query_vecs: Vec<Vec<f32>> = (0..n1)
295            .map(|i| q_flat[i * dim..(i + 1) * dim].to_vec())
296            .collect();
297        let train_vecs: Vec<Vec<f32>> = (0..n2)
298            .map(|i| t_flat[i * dim..(i + 1) * dim].to_vec())
299            .collect();
300
301        // Build one KD-tree per tree in the forest, each over a random
302        // permutation of the training set to improve diversity.
303        let mut forest: Vec<Vec<KdNode>> = Vec::with_capacity(self.trees);
304        let mut forest_roots: Vec<Option<usize>> = Vec::with_capacity(self.trees);
305
306        for t in 0..self.trees {
307            // Deterministic pseudo-random permutation via LCG seeded by tree index
308            let mut indices: Vec<usize> = (0..n2).collect();
309            let mut seed = (t as u32).wrapping_mul(1103515245).wrapping_add(12345);
310            for i in (1..n2).rev() {
311                seed = seed.wrapping_mul(1103515245).wrapping_add(12345);
312                let j = (seed as usize) % (i + 1);
313                indices.swap(i, j);
314            }
315
316            let mut items: Vec<(usize, Vec<f32>)> = indices
317                .into_iter()
318                .map(|i| (i, train_vecs[i].clone()))
319                .collect();
320            let mut nodes = Vec::new();
321            let root = Self::build_kd_tree(&mut items, 0, &mut nodes);
322            forest.push(nodes);
323            forest_roots.push(root);
324        }
325
326        // Query each tree and aggregate distances via average
327        let mut matches = Vec::with_capacity(n1);
328
329        for qi in 0..n1 {
330            // Accumulate distances from all trees
331            let mut dist_acc = vec![0.0f32; n2];
332            let mut count = vec![0u32; n2];
333
334            for t in 0..self.trees {
335                let mut checks = self.checks;
336                let (best_train_idx, best_dist) = Self::search_nn(
337                    &forest[t],
338                    &train_vecs,
339                    &query_vecs[qi],
340                    forest_roots[t],
341                    &mut checks,
342                );
343                dist_acc[best_train_idx] += best_dist;
344                count[best_train_idx] += 1;
345            }
346
347            // For each train point, compute average distance across the trees
348            // that visited it. Points never visited get MAX distance.
349            let mut best_idx = 0usize;
350            let mut best_dist = f32::MAX;
351            for ti in 0..n2 {
352                if count[ti] > 0 {
353                    let avg = dist_acc[ti] / count[ti] as f32;
354                    if avg < best_dist {
355                        best_dist = avg;
356                        best_idx = ti;
357                    }
358                }
359            }
360
361            // Fallback: brute-force nearest if the forest was too shallow
362            if best_dist.is_infinite() || best_dist >= f32::MAX {
363                for ti in 0..n2 {
364                    let d = euclidean_dist_sq(&query_vecs[qi], &train_vecs[ti]);
365                    if d < best_dist {
366                        best_dist = d;
367                        best_idx = ti;
368                    }
369                }
370            }
371
372            matches.push(DMatch {
373                query_idx: qi,
374                train_idx: best_idx,
375                img_idx: 0,
376                distance: best_dist.sqrt(),
377            });
378        }
379
380        Ok(matches)
381    }
382}
383
384/// Squared Euclidean distance between two equal-length slices.
385fn euclidean_dist_sq(a: &[f32], b: &[f32]) -> f32 {
386    a.iter()
387        .zip(b.iter())
388        .map(|(x, y)| {
389            let d = x - y;
390            d * d
391        })
392        .sum()
393}
394
395/// Helper to draw descriptor matching matches between two images.
396pub struct MatchDrawer;
397
398impl MatchDrawer {
399    /// Combines two images horizontally and draws matching lines between corresponding keypoints.
400    pub fn draw_matches<B: Backend>(
401        img1: &Image<B>,
402        kps1: &[KeyPoint],
403        img2: &Image<B>,
404        kps2: &[KeyPoint],
405        matches: &[DMatch],
406    ) -> Result<Image<B>> {
407        let h1 = img1.height();
408        let w1 = img1.width();
409        let h2 = img2.height();
410        let w2 = img2.width();
411
412        let out_h = h1.max(h2);
413        let out_w = w1 + w2;
414        let c = img1.channels();
415
416        let device = img1.tensor.device();
417
418        // 2. Crop/copy img1 and img2 onto out_flat directly
419        // Simple pixel copy on CPU for canvas assembly
420        let data1 = img1.tensor.clone().into_data();
421        let data2 = img2.tensor.clone().into_data();
422        let flat1: Vec<f32> = data1.iter::<f32>().collect();
423        let flat2: Vec<f32> = data2.iter::<f32>().collect();
424        let mut out_flat = vec![0.0f32; c * out_h * out_w];
425
426        for ch in 0..c {
427            for y in 0..h1 {
428                for x in 0..w1 {
429                    out_flat[ch * out_h * out_w + y * out_w + x] = flat1[ch * h1 * w1 + y * w1 + x];
430                }
431            }
432            for y in 0..h2 {
433                for x in 0..w2 {
434                    out_flat[ch * out_h * out_w + y * out_w + (x + w1)] =
435                        flat2[ch * h2 * w2 + y * w2 + x];
436                }
437            }
438        }
439
440        let assembled_tensor =
441            Tensor::<B, 3>::from_data(TensorData::new(out_flat, [c, out_h, out_w]), &device);
442        let mut assembled = Image::new(assembled_tensor);
443
444        // 3. Draw matching lines
445        for m in matches {
446            let kp1 = &kps1[m.query_idx];
447            let kp2 = &kps2[m.train_idx];
448
449            let p1 = Point::new(kp1.pt.x as usize, kp1.pt.y as usize);
450            let p2 = Point::new(kp2.pt.x as usize + w1, kp2.pt.y as usize);
451
452            assembled = assembled.draw_line(p1, p2, Scalar::new(0.0, 1.0, 0.0, 0.0))?;
453        }
454
455        Ok(assembled)
456    }
457}
458
459#[cfg(test)]
460mod tests {
461    use super::*;
462    use crate::test_helpers::{TestBackend, test_device};
463
464    #[test]
465    fn test_descriptor_matching() {
466        let device = test_device();
467        let query = Tensor::<TestBackend, 2>::from_data(
468            TensorData::new(vec![1.0f32, 0.0, 0.0, 1.0], [2, 2]),
469            &device,
470        );
471        let train = Tensor::<TestBackend, 2>::from_data(
472            TensorData::new(vec![1.0f32, 0.0, 0.0, 1.0], [2, 2]),
473            &device,
474        );
475
476        let matcher = BFMatcher;
477        let matches = matcher.match_descriptors(&query, &train).unwrap();
478        assert_eq!(matches.len(), 2);
479        assert_eq!(matches[0].query_idx, 0);
480        assert_eq!(matches[0].train_idx, 0);
481        assert_eq!(matches[1].query_idx, 1);
482        assert_eq!(matches[1].train_idx, 1);
483
484        let flat_data = vec![0.5f32; 3 * 8 * 8];
485        let t1 = Tensor::<TestBackend, 3>::from_data(
486            TensorData::new(flat_data.clone(), [3, 8, 8]),
487            &device,
488        );
489        let t2 =
490            Tensor::<TestBackend, 3>::from_data(TensorData::new(flat_data, [3, 8, 8]), &device);
491        let img1 = Image::new(t1);
492        let img2 = Image::new(t2);
493
494        let kps1 = vec![KeyPoint::new(1.0, 1.0, 2.0), KeyPoint::new(2.0, 2.0, 2.0)];
495        let kps2 = vec![KeyPoint::new(1.0, 1.0, 2.0), KeyPoint::new(2.0, 2.0, 2.0)];
496
497        let drawn = MatchDrawer::draw_matches(&img1, &kps1, &img2, &kps2, &matches).unwrap();
498        assert_eq!(drawn.shape(), [3, 8, 16]);
499    }
500
501    #[test]
502    fn test_flann_matcher_known_pairs() {
503        let device = test_device();
504        // Two groups of descriptors that are clearly separable:
505        //   group A around [1, 0, 0, 0]
506        //   group B around [0, 1, 0, 0]
507        let query = Tensor::<TestBackend, 2>::from_data(
508            TensorData::new(vec![1.01, 0.01, 0.0, 0.0, 0.01, 0.99, 0.0, 0.0], [2, 4]),
509            &device,
510        );
511        let train = Tensor::<TestBackend, 2>::from_data(
512            TensorData::new(
513                vec![
514                    1.0, 0.0, 0.0, 0.0, // best match for query row 0
515                    0.0, 1.0, 0.0, 0.0, // best match for query row 1
516                    1.05, 0.05, 0.0, 0.0,
517                ],
518                [3, 4],
519            ),
520            &device,
521        );
522
523        let matcher = FlannMatcher::new();
524        let matches = matcher.find_matches(&query, &train).unwrap();
525        assert_eq!(matches.len(), 2);
526
527        // Each query descriptor should match the train descriptor closest to it
528        assert_eq!(matches[0].query_idx, 0);
529        assert_eq!(matches[0].train_idx, 0); // [1,0,0,0] ↔ [1,0,0,0]
530        assert_eq!(matches[1].query_idx, 1);
531        assert_eq!(matches[1].train_idx, 1); // [1,0,0,0] ↔ [0,1,0,0]
532    }
533}