use crate::error::VisionError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BRIEFPattern {
Uniform,
Gaussian,
}
#[derive(Debug, Clone)]
pub struct BRIEFConfig {
pub n_bits: usize,
pub patch_size: usize,
pub pattern: BRIEFPattern,
}
impl Default for BRIEFConfig {
fn default() -> Self {
Self {
n_bits: 256,
patch_size: 31,
pattern: BRIEFPattern::Gaussian,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BRIEFDescriptor {
pub bits: Vec<u64>,
pub n_bits: usize,
}
fn xorshift64(state: &mut u64) -> u64 {
*state ^= *state << 13;
*state ^= *state >> 7;
*state ^= *state << 17;
*state
}
fn sample_uniform(state: &mut u64, half: i32) -> i32 {
let range = (2 * half + 1) as u64;
let v = xorshift64(state) % range;
v as i32 - half
}
fn sample_gaussian(state: &mut u64, sigma: f64) -> i32 {
const MAX: f64 = u64::MAX as f64;
let u1 = (xorshift64(state) as f64 + 1.0) / (MAX + 2.0);
let u2 = (xorshift64(state) as f64 + 1.0) / (MAX + 2.0);
let z = (-2.0 * u1.ln()).sqrt() * (2.0 * std::f64::consts::PI * u2).cos();
(z * sigma).round() as i32
}
pub fn generate_test_pairs(
n_bits: usize,
patch_size: usize,
pattern: BRIEFPattern,
seed: u64,
) -> Vec<((i32, i32), (i32, i32))> {
let half = (patch_size / 2) as i32;
let sigma = patch_size as f64 / 5.0;
let mut state = if seed == 0 { 0xdeadbeef_u64 } else { seed };
let mut pairs = Vec::with_capacity(n_bits);
for _ in 0..n_bits {
let (py, px, qy, qx) = match pattern {
BRIEFPattern::Uniform => (
sample_uniform(&mut state, half),
sample_uniform(&mut state, half),
sample_uniform(&mut state, half),
sample_uniform(&mut state, half),
),
BRIEFPattern::Gaussian => (
sample_gaussian(&mut state, sigma).clamp(-half, half),
sample_gaussian(&mut state, sigma).clamp(-half, half),
sample_gaussian(&mut state, sigma).clamp(-half, half),
sample_gaussian(&mut state, sigma).clamp(-half, half),
),
};
pairs.push(((py, px), (qy, qx)));
}
pairs
}
#[inline]
fn get_pixel(image: &[Vec<f32>], r: i32, c: i32, rows: usize, cols: usize) -> f32 {
let rr = r.clamp(0, rows as i32 - 1) as usize;
let cc = c.clamp(0, cols as i32 - 1) as usize;
image[rr][cc]
}
#[allow(clippy::type_complexity)]
pub fn compute_brief(
image: &[Vec<f32>],
keypoint: (usize, usize),
pairs: &[((i32, i32), (i32, i32))],
) -> Result<BRIEFDescriptor, VisionError> {
let rows = image.len();
let cols = image.first().map_or(0, |r| r.len());
if rows == 0 || cols == 0 {
return Err(VisionError::InvalidInput("Empty image".into()));
}
let ky = keypoint.0 as i32;
let kx = keypoint.1 as i32;
let n_bits = pairs.len();
let n_words = n_bits.div_ceil(64);
let mut bits = vec![0_u64; n_words];
for (i, &((dy_p, dx_p), (dy_q, dx_q))) in pairs.iter().enumerate() {
let p = get_pixel(image, ky + dy_p, kx + dx_p, rows, cols);
let q = get_pixel(image, ky + dy_q, kx + dx_q, rows, cols);
if p < q {
bits[i / 64] |= 1_u64 << (i % 64);
}
}
Ok(BRIEFDescriptor { bits, n_bits })
}
pub fn hamming_distance(a: &BRIEFDescriptor, b: &BRIEFDescriptor) -> u32 {
let len = a.bits.len().max(b.bits.len());
(0..len)
.map(|i| {
let wa = a.bits.get(i).copied().unwrap_or(0);
let wb = b.bits.get(i).copied().unwrap_or(0);
(wa ^ wb).count_ones()
})
.sum()
}
#[cfg(test)]
mod tests {
use super::*;
fn flat_image(rows: usize, cols: usize, val: f32) -> Vec<Vec<f32>> {
vec![vec![val; cols]; rows]
}
fn gradient_image(rows: usize, cols: usize) -> Vec<Vec<f32>> {
(0..rows)
.map(|r| {
(0..cols)
.map(|c| (r * cols + c) as f32 / (rows * cols) as f32)
.collect()
})
.collect()
}
#[test]
fn test_generate_pairs_count() {
let pairs = generate_test_pairs(256, 31, BRIEFPattern::Gaussian, 42);
assert_eq!(pairs.len(), 256);
}
#[test]
fn test_generate_pairs_bounds_uniform() {
let patch_size = 31_usize;
let half = (patch_size / 2) as i32;
let pairs = generate_test_pairs(256, patch_size, BRIEFPattern::Uniform, 7);
for ((dy_p, dx_p), (dy_q, dx_q)) in &pairs {
assert!((-half..=half).contains(dy_p));
assert!((-half..=half).contains(dx_p));
assert!((-half..=half).contains(dy_q));
assert!((-half..=half).contains(dx_q));
}
}
#[test]
fn test_flat_image_zero_descriptor() {
let img = flat_image(64, 64, 0.5);
let pairs = generate_test_pairs(256, 31, BRIEFPattern::Gaussian, 1);
let desc = compute_brief(&img, (32, 32), &pairs)
.expect("compute_brief should succeed on valid image");
assert!(desc.bits.iter().all(|&w| w == 0));
}
#[test]
fn test_descriptor_word_count() {
let img = gradient_image(64, 64);
let pairs = generate_test_pairs(256, 31, BRIEFPattern::Gaussian, 1);
let desc = compute_brief(&img, (32, 32), &pairs)
.expect("compute_brief should succeed on valid image");
assert_eq!(desc.bits.len(), 4); assert_eq!(desc.n_bits, 256);
}
#[test]
fn test_hamming_distance_self() {
let img = gradient_image(64, 64);
let pairs = generate_test_pairs(128, 31, BRIEFPattern::Gaussian, 99);
let d = compute_brief(&img, (32, 32), &pairs)
.expect("compute_brief should succeed on valid image");
assert_eq!(hamming_distance(&d, &d), 0);
}
#[test]
fn test_hamming_distance_symmetry() {
let img = gradient_image(64, 64);
let pairs = generate_test_pairs(128, 31, BRIEFPattern::Uniform, 55);
let d1 = compute_brief(&img, (20, 20), &pairs)
.expect("compute_brief should succeed at keypoint (20,20)");
let d2 = compute_brief(&img, (40, 40), &pairs)
.expect("compute_brief should succeed at keypoint (40,40)");
assert_eq!(hamming_distance(&d1, &d2), hamming_distance(&d2, &d1));
}
#[test]
fn test_hamming_complement() {
let a = BRIEFDescriptor {
bits: vec![0x0000_0000_0000_0000_u64; 4],
n_bits: 256,
};
let b = BRIEFDescriptor {
bits: vec![0xFFFF_FFFF_FFFF_FFFF_u64; 4],
n_bits: 256,
};
assert_eq!(hamming_distance(&a, &b), 256);
}
}