use std::cmp::{max, min};
use std::collections::HashMap;
#[allow(unused_imports)] use num::Signed;
#[cfg(feature = "img")]
pub mod image;
const DEFAULT_CROP: f32 = 0.05;
const DEFAULT_GRID_SIZE: usize = 10;
pub fn get_buffer_signature(rgba_buffer: &[u8], width: usize) -> Vec<i8> {
let gray = grayscale_buffer(rgba_buffer, width);
let average_square_width_fn = |width, height| {
max(
2_usize,
(0.5 + min(width, height) as f32 / 20.0).floor() as usize,
) / 2
};
compute_from_gray(gray, DEFAULT_CROP, DEFAULT_GRID_SIZE, average_square_width_fn)
}
pub fn get_tuned_buffer_signature(
rgba_buffer: &[u8],
width: usize,
crop: f32,
grid_size: usize,
average_square_width_fn: fn(width: usize, height: usize) -> usize,
) -> Vec<i8> {
let gray = grayscale_buffer(rgba_buffer, width);
compute_from_gray(gray, crop, grid_size, average_square_width_fn)
}
pub fn cosine_similarity(a: &Vec<i8>, b: &Vec<i8>) -> f64 {
assert_eq!(a.len(), b.len(), "Compared vectors must be of equal length");
let a_length = vector_length(a);
let b_length = vector_length(b);
if a_length == 0.0 || b_length == 0.0 {
if a_length == 0.0 && b_length == 0.0 {
1.0
} else {
0.0
}
} else {
let dot_product: f64 = a.iter().zip(b.iter())
.map(|(av, bv)| *av as f64 * *bv as f64)
.sum();
dot_product / (a_length * b_length)
}
}
fn vector_length(v: &[i8]) -> f64 {
v.iter().map(|vi| *vi as i32).map(|vi| (vi * vi) as f64).sum::<f64>().sqrt()
}
fn compute_from_gray(
gray: Vec<Vec<u8>>,
crop: f32,
grid_size: usize,
average_square_width_fn: fn(width: usize, height: usize) -> usize,
) -> Vec<i8> {
let bounds = crop_boundaries(&gray, crop);
let points = grid_points(&bounds, grid_size);
let averages = grid_averages(gray, points, bounds, average_square_width_fn);
compute_signature(averages, grid_size)
}
fn grayscale_buffer(rgba_buffer: &[u8], width: usize) -> Vec<Vec<u8>> {
let height = (rgba_buffer.len() / 4) / width;
let mut result = Vec::with_capacity(height);
let mut idx: usize = 0;
while idx < rgba_buffer.len() {
let mut row = Vec::with_capacity(width);
for _ in 0..width {
let avg = pixel_gray(
rgba_buffer[idx],
rgba_buffer[idx + 1],
rgba_buffer[idx + 2],
rgba_buffer[idx + 3],
);
row.push(avg);
idx += 4;
}
result.push(row);
}
result
}
fn pixel_gray(r: u8, g: u8, b: u8, a: u8) -> u8 {
let rgb_avg = (r as u16 + g as u16 + b as u16) / 3;
((rgb_avg as f32) * (a as f32 / 255.0)) as u8
}
#[derive(Debug, PartialEq)]
struct Bounds {
lower_x: usize,
upper_x: usize,
lower_y: usize,
upper_y: usize,
}
fn crop_boundaries(pixels: &Vec<Vec<u8>>, crop: f32) -> Bounds {
let row_diff_sums: Vec<i32> = (0..pixels.len()).map(|y|
(1..pixels[y].len()).map(|x|
pixels[y][x].abs_diff(pixels[y][x - 1]) as i32).sum()
).collect();
let (top, bottom) = get_bounds(row_diff_sums, crop);
let col_diff_sums: Vec<i32> = (0..pixels[0].len()).map(|x|
(1..pixels.len()).map(|y|
pixels[y][x].abs_diff(pixels[y - 1][x]) as i32).sum()
).collect();
let (left, right) = get_bounds(col_diff_sums, crop);
Bounds {
lower_x: left,
upper_x: right,
lower_y: top,
upper_y: bottom,
}
}
fn get_bounds(diff_sums: Vec<i32>, crop: f32) -> (usize, usize) {
let total_diff_sum: i32 = diff_sums.iter().sum();
let threshold = (total_diff_sum as f32 * crop) as i32;
let mut lower = 0;
let mut upper = diff_sums.len() - 1;
let mut sum = diff_sums[lower];
while sum < threshold {
lower += 1;
sum += diff_sums[lower];
}
sum = diff_sums[upper];
while sum < threshold {
upper -= 1;
sum += diff_sums[upper];
}
(lower, upper)
}
fn grid_points(bounds: &Bounds, grid_size: usize) -> HashMap<(i8, i8), (usize, usize)> {
let x_width = (bounds.upper_x - bounds.lower_x + 1) as f32 / grid_size as f32;
let y_width = (bounds.upper_y - bounds.lower_y + 1) as f32 / grid_size as f32;
let mut points = HashMap::new();
for x in 1..grid_size {
for y in 1..grid_size {
points.insert(
(x as i8, y as i8),
(
bounds.lower_x + (x as f32 * x_width).trunc() as usize,
bounds.lower_y + (y as f32 * y_width).trunc() as usize,
),
);
}
}
points
}
fn grid_averages(
pixels: Vec<Vec<u8>>,
points: HashMap<(i8, i8), (usize, usize)>,
bounds: Bounds,
average_square_width_fn: fn(width: usize, height: usize) -> usize,
) -> HashMap<(i8, i8), u8> {
let width = bounds.upper_x - bounds.lower_x;
let height = bounds.upper_y - bounds.lower_y;
let square_edge = average_square_width_fn(width, height) as i32;
let mut result = HashMap::new();
for (grid_coord, (point_x, point_y)) in points {
let mut sum: f32 = 0.0;
for delta_x in -square_edge..=square_edge {
for delta_y in -square_edge..=square_edge {
let average = pixel_average(
&pixels,
(point_x as i32 + delta_x) as usize,
(point_y as i32 + delta_y) as usize,
);
sum += average;
}
}
let i = sum / ((square_edge * 2 + 1) * (square_edge * 2 + 1)) as f32;
result.insert(grid_coord, i as u8);
}
result
}
const GRID_DELTAS: [(i8, i8); 9] = [
(-1, -1), (0, -1), (1, -1),
(-1, 0), (0, 0), (1, 0),
(-1, 1), (0, 1), (1, 1)
];
fn compute_signature(point_averages: HashMap<(i8, i8), u8>, grid_size: usize) -> Vec<i8> {
let mut raw_diffs = Vec::with_capacity(grid_size * grid_size);
for grid_y in 1..(grid_size as i8) {
for grid_x in 1..(grid_size as i8) {
let gray = *point_averages.get(&(grid_x, grid_y)).unwrap();
let raw_point_diffs: Vec<i16> = GRID_DELTAS.iter()
.filter_map(|(delta_x, delta_y)| {
point_averages.get(&(grid_x + delta_x, grid_y + delta_y))
.map(|other| compute_diff(gray, *other))
}).collect();
raw_diffs.push(raw_point_diffs)
}
}
let (dark_threshold, light_threshold) = get_thresholds(&raw_diffs);
raw_diffs.into_iter().flat_map(|neighbors|
neighbors.into_iter()
.map(|v| {
match v {
v if v > 0 => collapse(v, light_threshold),
v if v < 0 => collapse(v, dark_threshold),
_ => 0
}
})).collect()
}
fn get_thresholds(raw_diffs: &[Vec<i16>]) -> (i16, i16) {
let (dark, light): (Vec<i16>, Vec<i16>) = raw_diffs.iter().flatten()
.filter(|d| **d != 0)
.partition(|d| **d < 0);
let dark_threshold = get_median(dark);
let light_threshold = get_median(light);
(dark_threshold, light_threshold)
}
fn collapse(val: i16, threshold: i16) -> i8 {
if val.abs() >= threshold.abs() {
2 * val.signum() as i8
} else {
val.signum() as i8
}
}
fn get_median(mut vec: Vec<i16>) -> i16 {
vec.sort();
if vec.len() % 2 == 0 {
if vec.is_empty() {
0
} else {
(vec[(vec.len() / 2) - 1] + vec[vec.len() / 2]) / 2
}
} else {
vec[vec.len() / 2]
}
}
fn compute_diff(me: u8, other: u8) -> i16 {
let raw_result = me as i16 - other as i16;
if raw_result.abs() <= 2 {
0
} else {
raw_result
}
}
const PIXEL_DELTAS: [(i32, i32); 9] = [
(-1, -1), (0, -1), (1, -1),
(-1, 0), (0, 0), (1, 0),
(-1, 1), (0, 1), (1, 1)
];
fn pixel_average(pixels: &[Vec<u8>], x: usize, y: usize) -> f32 {
let max_y = pixels.len() as i32 - 1;
let max_x = pixels[0].len() as i32 - 1;
let sum: f32 = PIXEL_DELTAS.iter().map(|(delta_x, delta_y)| {
pixels[(y as i32 + *delta_y).clamp(0, max_y) as usize][(x as i32 + *delta_x).clamp(0, max_x) as usize] as f32
}).sum();
sum / 9.0
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::{assert_eq};
use std::collections::BTreeMap;
macro_rules! assert_map_eq {
( $actual:expr, $expected:expr ) => {
{
let actual: BTreeMap<_, _> = ($actual).into_iter().collect();
let expected: BTreeMap<_, _> = ($expected).into_iter().collect();
assert_eq!(actual, expected)
}
}
}
fn from_dotgrid(grid: &str) -> Vec<Vec<u8>> {
grid.split("\n")
.map(|row| row.replace(" ",""))
.filter(|row| row.len() > 0)
.map(|row| row.chars().map(|c| match c {
'.' => 0,
'o' => 64,
'O' => 128,
'x' => 192,
'X' => 255,
c => panic!("Unexpected dotgrid character '{}'", c)
}).collect()).collect()
}
#[test]
fn test_pixel_gray() {
assert_eq!(pixel_gray(255,255,255,255), 255);
assert_eq!(pixel_gray(0,0,0,0), 0);
assert_eq!(pixel_gray(255,255,255,0), 0);
assert_eq!(pixel_gray(32, 64, 96, 255), 64);
}
#[test]
fn test_grayscale_buffer() {
assert_eq!(grayscale_buffer(&[
255, 255, 255, 255,
128, 128, 128, 128,
0, 0, 0, 0,
0, 128, 255, 128
], 2), [
[255, 64],
[0, 63]
]);
}
#[test]
fn test_get_bounds() {
assert_eq!([
(vec![0,0,50,50,0,0], 0.05),
(vec![0,0,0,50,50,0,0,0], 0.05),
].map(|(v, c)| get_bounds(v, c)),
[(2, 3), (3, 4)]);
}
#[test]
fn test_crop_boundaries() {
let pic = from_dotgrid("
.......
.oooo..
.oXxo..
.oXxo..
.......
.......
");
assert_eq!(crop_boundaries(&pic, 0.05), Bounds {
lower_x: 1,
upper_x: 4,
lower_y: 1,
upper_y: 3,
});
assert_eq!(crop_boundaries(&pic, 0.25), Bounds {
lower_x: 2,
upper_x: 3,
lower_y: 2,
upper_y: 3,
});
assert_eq!(crop_boundaries(&pic, 0.5), Bounds {
lower_x: 2,
upper_x: 2,
lower_y: 2,
upper_y: 2,
});
}
#[test]
fn test_grid_points() {
assert_map_eq!(grid_points(&Bounds {
lower_x: 5,
upper_x: 15,
lower_y: 10,
upper_y: 30,
}, 2), [
((1, 1), (10, 20))
]);
assert_map_eq!(grid_points(&Bounds {
lower_x: 5,
upper_x: 15,
lower_y: 10,
upper_y: 30,
}, 3), [
((1, 1), (8, 17)),
((2, 1), (12, 17)),
((1, 2), (8, 24)),
((2, 2), (12, 24)),
]);
}
#[test]
fn test_grid_points_extreme() {
assert_map_eq!(grid_points(&Bounds {
lower_x: 0,
upper_x: 100,
lower_y: 1,
upper_y: 1,
}, 6), [
((1, 1), (16, 1)),
((2, 1), (33, 1)),
((3, 1), (50, 1)),
((4, 1), (67, 1)),
((5, 1), (84, 1)),
((1, 2), (16, 1)),
((2, 2), (33, 1)),
((3, 2), (50, 1)),
((4, 2), (67, 1)),
((5, 2), (84, 1)),
((1, 3), (16, 1)),
((2, 3), (33, 1)),
((3, 3), (50, 1)),
((4, 3), (67, 1)),
((5, 3), (84, 1)),
((1, 4), (16, 1)),
((2, 4), (33, 1)),
((3, 4), (50, 1)),
((4, 4), (67, 1)),
((5, 4), (84, 1)),
((1, 5), (16, 1)),
((2, 5), (33, 1)),
((3, 5), (50, 1)),
((4, 5), (67, 1)),
((5, 5), (84, 1)),
]);
}
#[test]
fn test_grid_points_tiny() {
assert_map_eq!(grid_points(&Bounds {
lower_x: 0,
upper_x: 1,
lower_y: 0,
upper_y: 1,
}, 3), [
((1,1), (0,0)),
((2,1), (1,0)),
((1,2), (0,1)),
((2,2), (1,1)),
]);
}
}