pico-detect 0.7.0

Pixel Intensity Comparison-based Object (PICO) detection library.
Documentation
use image::Luma;
use nalgebra::Point2;

use pixelutil_image::ExtendedImageView;

#[derive(Debug, PartialEq, Clone, Copy, Default)]
pub struct ComparisonNode(pub Point2<i8>, pub Point2<i8>);

impl From<[i8; 4]> for ComparisonNode {
    #[inline]
    fn from(data: [i8; 4]) -> Self {
        let [y0, x0, y1, x1] = data;
        Self(Point2::new(x0, y0), Point2::new(x1, y1))
    }
}

impl From<[u8; 4]> for ComparisonNode {
    #[inline]
    fn from(data: [u8; 4]) -> Self {
        data.map(|value| i8::from_le_bytes(value.to_le_bytes()))
            .into()
    }
}

impl From<ComparisonNode> for [i8; 4] {
    #[inline]
    fn from(node: ComparisonNode) -> [i8; 4] {
        [node.0.y, node.0.x, node.1.y, node.1.x]
    }
}

impl From<ComparisonNode> for [u8; 4] {
    #[inline]
    fn from(node: ComparisonNode) -> [u8; 4] {
        [
            node.0.y.to_le_bytes()[0],
            node.0.x.to_le_bytes()[0],
            node.1.y.to_le_bytes()[0],
            node.1.x.to_le_bytes()[0],
        ]
    }
}

impl ComparisonNode {
    #[inline]
    pub fn bintest<I: ExtendedImageView<Pixel = Luma<u8>>>(
        &self,
        image: &I,
        point: Point2<i32>,
        size: u32,
    ) -> bool {
        let p0 = transform(point, size, self.0.cast());
        let p1 = transform(point, size, self.1.cast());

        let Luma([lum0]) = image.get_pixel_clamped(p0);
        let Luma([lum1]) = image.get_pixel_clamped(p1);

        lum0 > lum1
    }
}

#[allow(dead_code)]
const SCALE: i32 = u8::MAX as i32 + 1;
const SHIFT: i32 = 8;

#[allow(dead_code)]
#[inline]
fn na_transform(i: Point2<i32>, s: u32, n: Point2<i32>) -> Point2<i32> {
    (i * SCALE + n.coords * (s as i32)) / SCALE
}

#[inline]
fn transform(i: Point2<i32>, s: u32, n: Point2<i32>) -> Point2<i32> {
    let (x, y) = original_transform(i.x, i.y, s as i32, n.x, n.y);
    Point2::new(x, y)
}

#[allow(dead_code)]
#[inline]
fn original_transform(ix: i32, iy: i32, s: i32, nx: i32, ny: i32) -> (i32, i32) {
    let x = ((ix << SHIFT) + nx * s) >> SHIFT;
    let y = ((iy << SHIFT) + ny * s) >> SHIFT;
    (x, y)
}

#[cfg(test)]
mod tests {
    use image::{GrayImage, Luma};

    use super::*;

    #[test]
    fn test_comparison_node_from_into() {
        let data: [i8; 4] = [-128, 42, -34, 127];
        let buf: [u8; 4] = ComparisonNode::from(data.clone()).into();
        let out: [i8; 4] = ComparisonNode::from(buf).into();
        assert_eq!(data, out);
    }

    #[test]
    fn test_original_transform() {
        let (ix, iy, s) = (100, 150, 50);
        let (nx, ny) = (42, -34);
        assert_eq!(original_transform(ix, iy, s, nx, ny), (108, 143));
    }

    #[test]
    fn test_na_transform() {
        let i = Point2::new(100, 150);
        let p = Point2::new(42, -34);
        let s = 50;
        assert_eq!(na_transform(i, s, p), transform(i, s, p));
    }

    #[test]
    fn test_comparison_node_bintest() {
        let node = ComparisonNode::from([i8::MAX, i8::MAX, i8::MIN, i8::MIN]);

        let size = 255;
        let mut image = GrayImage::new(size, size);
        image.put_pixel(0, 0, Luma::from([42u8]));
        image.put_pixel(size - 1, size - 1, Luma::from([255u8]));

        let point = Point2::new(size / 2 + 1, size / 2 + 1);
        assert!(node.bintest(&image, point.cast(), size));
    }
}