pico-detect 0.3.0

Pixel Intensity Comparison-based Object (PICO) detection library.
Documentation
use std::cmp::Ordering;
use std::io;

use image::{GenericImageView, GrayImage};
use na::Point2;

use rand::SeedableRng;
use rand_xorshift::XorShiftRng;

#[derive(Debug, PartialEq)]
pub struct ComparisonNode {
    pub left: Point2<i8>,
    pub right: Point2<i8>,
}

impl ComparisonNode {
    pub fn new(data: [i8; 4]) -> Self {
        let [y0, x0, y1, x1] = data;
        Self {
            left: Point2::new(x0, y0),
            right: Point2::new(x1, y1),
        }
    }

    pub fn from_buffer(buf: &[u8; 4]) -> Self {
        let mut data = [0i8; 4];
        for (value, byte) in data.iter_mut().zip(buf.iter()) {
            *value = i8::from_le_bytes(byte.to_le_bytes());
        }
        Self::new(data)
    }
}

pub struct ThresholdNode {
    pub idx: (usize, usize),
    pub threshold: f32,
}

impl ThresholdNode {
    pub fn from_readable(mut readable: impl io::Read) -> io::Result<Self> {
        let mut buf = [0u8; 4];

        readable.read_exact(&mut buf)?;
        let idx0 = u32::from_be_bytes(buf) as usize;

        readable.read_exact(&mut buf)?;
        let idx1 = u32::from_be_bytes(buf) as usize;

        readable.read_exact(&mut buf)?;
        let threshold = f32::from_be_bytes(buf);

        Ok(Self {
            idx: (idx0, idx1),
            threshold,
        })
    }

    pub fn bintest(&self, feautures: &[u8]) -> bool {
        let diff = feautures[self.idx.0] as i16 - feautures[self.idx.1] as i16;
        self.threshold > (diff as f32)
    }
}

pub trait Bintest<T> {
    fn find_point(transform: &T, point: &Point2<i8>) -> Point2<u32>;

    fn find_lum(image: &GrayImage, transform: &T, point: &Point2<i8>) -> u8;

    fn bintest(&self, image: &GrayImage, transform: &T) -> bool;
}

pub trait SaturatedGet: GenericImageView {
    #[inline]
    fn saturate_bound(value: u32, bound: u32) -> u32 {
        match value.cmp(&bound) {
            Ordering::Less => value,
            _ => bound - 1,
        }
    }

    fn saturated_get_lum(&self, x: u32, y: u32) -> u8;
}

impl SaturatedGet for GrayImage {
    #[inline]
    fn saturated_get_lum(&self, x: u32, y: u32) -> u8 {
        let x = Self::saturate_bound(x, self.width());
        let y = Self::saturate_bound(y, self.height());
        unsafe { self.unsafe_get_pixel(x, y) }.0[0]
    }
}

pub trait SafeGet: GenericImageView {
    fn safe_get_lum(&self, x: u32, y: u32, fallback: u8) -> u8;
}

impl SafeGet for GrayImage {
    #[inline]
    fn safe_get_lum(&self, x: u32, y: u32, fallback: u8) -> u8 {
        if self.in_bounds(x, y) {
            unsafe { self.unsafe_get_pixel(x, y) }.0[0]
        } else {
            fallback
        }
    }
}

pub fn create_xorshift_rng(seed: u64) -> XorShiftRng {
    XorShiftRng::seed_from_u64(seed)
}

#[cfg(test)]
mod tests {
    use super::*;
    use image::Luma;

    #[test]
    fn get_luminance_in_and_out_of_image_bounds() {
        let (width, height) = (640, 480);
        let mut image = GrayImage::new(width, height);
        image.put_pixel(0, 0, Luma::from([42u8]));
        image.put_pixel(width - 1, height - 1, Luma::from([255u8]));

        let tests = vec![
            (Point2::new(0f32, 0f32), 42u8),
            (Point2::new(-10f32, -10f32), 42u8),
            (Point2::new((width - 1) as f32, (height - 1) as f32), 255u8),
            (Point2::new(width as f32, height as f32), 255u8),
        ];

        for (point, test_lum) in tests {
            let lum = image.saturated_get_lum(point.x as u32, point.y as u32);
            assert_eq!(lum, test_lum);
        }
    }

    #[test]
    fn compare_node_from_buffer_and_new() {
        let (y0, x0, y1, x1) = (-128i8, 42i8, -34i8, 127i8);
        let node1 = ComparisonNode::new([y0, x0, y1, x1]);
        let node2 = ComparisonNode::from_buffer(&[
            y0.to_le_bytes()[0],
            x0.to_le_bytes()[0],
            y1.to_le_bytes()[0],
            x1.to_le_bytes()[0],
        ]);

        assert_eq!(node1, node2);
    }
}