1use std::cmp::{max, min};
2use std::collections::HashMap;
3
4#[allow(unused_imports)] use num::Signed;
6
7#[cfg(feature = "img")]
8pub mod image;
9
10const DEFAULT_CROP: f32 = 0.05;
11const DEFAULT_GRID_SIZE: usize = 10;
12
13pub fn get_buffer_signature(rgba_buffer: &[u8], width: usize) -> Vec<i8> {
17 let gray = grayscale_buffer(rgba_buffer, width);
18
19 let average_square_width_fn = |width, height| {
20 max(
21 2_usize,
22 (0.5 + min(width, height) as f32 / 20.0).floor() as usize,
23 ) / 2
24 };
25
26 compute_from_gray(gray, DEFAULT_CROP, DEFAULT_GRID_SIZE, average_square_width_fn)
27}
28
29pub fn get_tuned_buffer_signature(
42 rgba_buffer: &[u8],
43 width: usize,
44 crop: f32,
45 grid_size: usize,
46 average_square_width_fn: fn(width: usize, height: usize) -> usize,
47) -> Vec<i8> {
48 let gray = grayscale_buffer(rgba_buffer, width);
49 compute_from_gray(gray, crop, grid_size, average_square_width_fn)
50}
51
52pub fn cosine_similarity(a: &Vec<i8>, b: &Vec<i8>) -> f64 {
57 assert_eq!(a.len(), b.len(), "Compared vectors must be of equal length");
60
61 let dot_product: f64 = a.iter().zip(b.iter())
62 .map(|(av, bv)| *av as f64 * *bv as f64)
63 .sum();
64
65 dot_product / (vector_length(a) * vector_length(b))
66}
67
68fn vector_length(v: &[i8]) -> f64 {
69 v.iter().map(|vi| *vi as i32).map(|vi| (vi * vi) as f64).sum::<f64>().sqrt()
70}
71
72fn compute_from_gray(
75 gray: Vec<Vec<u8>>,
76 crop: f32,
77 grid_size: usize,
78 average_square_width_fn: fn(width: usize, height: usize) -> usize,
79) -> Vec<i8> {
80 let bounds = crop_boundaries(&gray, crop);
81 let points = grid_points(&bounds, grid_size);
82 let averages = grid_averages(gray, points, bounds, average_square_width_fn);
83 compute_signature(averages, grid_size)
84}
85
86fn grayscale_buffer(rgba_buffer: &[u8], width: usize) -> Vec<Vec<u8>> {
92 let height = (rgba_buffer.len() / 4) / width;
93 let mut result = Vec::with_capacity(height);
94 let mut idx: usize = 0;
95 while idx < rgba_buffer.len() {
96 let mut row = Vec::with_capacity(width);
97 for _ in 0..width {
98 let avg = pixel_gray(
99 rgba_buffer[idx],
100 rgba_buffer[idx + 1],
101 rgba_buffer[idx + 2],
102 rgba_buffer[idx + 3],
103 );
104
105 row.push(avg);
106 idx += 4;
107 }
108 result.push(row);
109 }
110
111 result
112}
113
114fn pixel_gray(r: u8, g: u8, b: u8, a: u8) -> u8 {
115 let rgb_avg = (r as u16 + g as u16 + b as u16) / 3;
116 ((rgb_avg as f32) * (a as f32 / 255.0)) as u8
117}
118
119#[derive(Debug)]
120struct Bounds {
121 lower_x: usize,
122 upper_x: usize,
123 lower_y: usize,
124 upper_y: usize,
125}
126
127fn crop_boundaries(pixels: &Vec<Vec<u8>>, crop: f32) -> Bounds {
145 let row_diff_sums: Vec<i32> = (0..pixels.len()).map(|y|
146 (1..pixels[y].len()).map(|x|
147 pixels[y][x].abs_diff(pixels[y][x - 1]) as i32).sum()
148 ).collect();
149
150 let (top, bottom) = get_bounds(row_diff_sums, crop);
151
152 let col_diff_sums: Vec<i32> = (0..pixels[0].len()).map(|x|
153 (1..pixels.len()).map(|y|
154 pixels[y][x].abs_diff(pixels[y - 1][x]) as i32).sum()
155 ).collect();
156
157 let (left, right) = get_bounds(col_diff_sums, crop);
158
159 Bounds {
160 lower_x: left,
161 upper_x: right,
162 lower_y: top,
163 upper_y: bottom,
164 }
165}
166
167fn get_bounds(diff_sums: Vec<i32>, crop: f32) -> (usize, usize) {
168 let total_diff_sum: i32 = diff_sums.iter().sum();
169 let threshold = (total_diff_sum as f32 * crop) as i32;
170 let mut lower = 0;
171 let mut upper = diff_sums.len() - 1;
172 let mut sum = 0;
173
174 while sum < threshold {
175 sum += diff_sums[lower];
176 lower += 1;
177 }
178 sum = 0;
179 while sum < threshold {
180 sum += diff_sums[upper];
181 upper -= 1;
182 }
183 (lower, upper)
184}
185
186fn grid_points(bounds: &Bounds, grid_size: usize) -> HashMap<(i8, i8), (usize, usize)> {
196 let x_width = (bounds.upper_x - bounds.lower_x) / grid_size;
197 let y_width = (bounds.upper_y - bounds.lower_y) / grid_size;
198
199 let mut points = HashMap::new();
200 for x in 1..grid_size {
201 for y in 1..grid_size {
202 points.insert((x as i8, y as i8), (x * x_width, y * y_width));
203 }
204 }
205
206 points
207}
208
209fn grid_averages(
217 pixels: Vec<Vec<u8>>,
218 points: HashMap<(i8, i8), (usize, usize)>,
219 bounds: Bounds,
220 average_square_width_fn: fn(width: usize, height: usize) -> usize,
221) -> HashMap<(i8, i8), u8> {
222 let width = bounds.upper_x - bounds.lower_x;
223 let height = bounds.upper_y - bounds.lower_y;
224 let square_edge = average_square_width_fn(width, height) as i32;
225
226 let mut result = HashMap::new();
227 for (grid_coord, (point_x, point_y)) in points {
228 let mut sum: f32 = 0.0;
229 for delta_x in -square_edge..=square_edge {
230 for delta_y in -square_edge..=square_edge {
231 let average = pixel_average(
232 &pixels,
233 (point_x as i32 + delta_x) as usize,
234 (point_y as i32 + delta_y) as usize,
235 );
236 sum += average;
237 }
238 }
239
240 let i = sum / ((square_edge * 2 + 1) * (square_edge * 2 + 1)) as f32;
241 result.insert(grid_coord, i as u8);
242 }
243
244 result
245}
246
247const GRID_DELTAS: [(i8, i8); 9] = [
266 (-1, -1), (0, -1), (1, -1),
267 (-1, 0), (0, 0), (1, 0),
268 (-1, 1), (0, 1), (1, 1)
269];
270
271fn compute_signature(point_averages: HashMap<(i8, i8), u8>, grid_size: usize) -> Vec<i8> {
272 let mut raw_diffs = Vec::with_capacity(grid_size * grid_size);
273 for grid_y in 1..(grid_size as i8) {
274 for grid_x in 1..(grid_size as i8) {
275 let gray = *point_averages.get(&(grid_x, grid_y)).unwrap();
276 let raw_point_diffs: Vec<i16> = GRID_DELTAS.iter()
277 .filter_map(|(delta_x, delta_y)| {
278 point_averages.get(&(grid_x + delta_x, grid_y + delta_y))
279 .map(|other| compute_diff(gray, *other))
280 }).collect();
281 raw_diffs.push(raw_point_diffs)
282 }
283 }
284
285 let (dark_threshold, light_threshold) = get_thresholds(&raw_diffs);
286 raw_diffs.into_iter().flat_map(|neighbors|
287 neighbors.into_iter()
288 .map(|v| {
289 match v {
290 v if v > 0 => collapse(v, light_threshold),
291 v if v < 0 => collapse(v, dark_threshold),
292 _ => 0
293 }
294 })).collect()
295}
296
297
298fn get_thresholds(raw_diffs: &[Vec<i16>]) -> (i16, i16) {
299 let (dark, light): (Vec<i16>, Vec<i16>) = raw_diffs.iter().flatten()
300 .filter(|d| **d != 0)
301 .partition(|d| **d < 0);
302
303 let dark_threshold = get_median(dark);
304 let light_threshold = get_median(light);
305
306 (dark_threshold, light_threshold)
307}
308
309fn collapse(val: i16, threshold: i16) -> i8 {
310 if val.abs() >= threshold.abs() {
311 2 * val.signum() as i8
312 } else {
313 val.signum() as i8
314 }
315}
316
317fn get_median(mut vec: Vec<i16>) -> i16 {
318 vec.sort();
319 if vec.len() % 2 == 0 {
320 if vec.is_empty() {
321 0
322 } else {
323 (vec[(vec.len() / 2) - 1] + vec[vec.len() / 2]) / 2
324 }
325 } else {
326 vec[vec.len() / 2]
327 }
328}
329
330fn compute_diff(me: u8, other: u8) -> i16 {
331 let raw_result = me as i16 - other as i16;
332 if raw_result.abs() <= 2 {
333 0
334 } else {
335 raw_result
336 }
337}
338
339const PIXEL_DELTAS: [(i32, i32); 9] = [
340 (-1, -1), (0, -1), (1, -1),
341 (-1, 0), (0, 0), (1, 0),
342 (-1, 1), (0, 1), (1, 1)
343];
344
345fn pixel_average(pixels: &[Vec<u8>], x: usize, y: usize) -> f32 {
346 let sum: f32 = PIXEL_DELTAS.iter().map(|(delta_x, delta_y)| {
347 pixels[(y as i32 + *delta_y) as usize][(x as i32 + *delta_x) as usize] as f32
348 }).sum();
349
350 sum / 9.0
351}