use crate::error::{GeekedError, Result};
use image::{DynamicImage, GrayImage, Luma};
use imageproc::template_matching::{find_extremes, match_template, MatchTemplateMethod};
pub struct SlideSolver {
puzzle_piece: DynamicImage,
background: DynamicImage,
}
impl SlideSolver {
pub fn from_bytes(puzzle_piece: &[u8], background: &[u8]) -> Result<Self> {
let puzzle_piece = image::load_from_memory(puzzle_piece).map_err(|e| {
GeekedError::ImageProcessing(format!("Failed to load puzzle piece: {}", e))
})?;
let background = image::load_from_memory(background).map_err(|e| {
GeekedError::ImageProcessing(format!("Failed to load background: {}", e))
})?;
Ok(Self {
puzzle_piece,
background,
})
}
pub fn new(puzzle_piece: DynamicImage, background: DynamicImage) -> Self {
Self {
puzzle_piece,
background,
}
}
pub fn find_position(&self) -> f64 {
let piece_gray = self.puzzle_piece.to_luma8();
let bg_gray = self.background.to_luma8();
let piece_edges = canny_edge_detection(&piece_gray, 100.0, 200.0);
let bg_edges = canny_edge_detection(&bg_gray, 100.0, 200.0);
let result = match_template(
&bg_edges,
&piece_edges,
MatchTemplateMethod::CrossCorrelationNormalized,
);
let extremes = find_extremes(&result);
let (max_x, _max_y) = extremes.max_value_location;
let piece_width = self.puzzle_piece.width() as f64;
let center_x = max_x as f64 + piece_width / 2.0;
center_x - 41.0
}
}
fn canny_edge_detection(image: &GrayImage, low_threshold: f64, high_threshold: f64) -> GrayImage {
let (width, height) = image.dimensions();
let blurred = gaussian_blur(image);
let (gx, gy) = sobel_gradients(&blurred);
let mut magnitude = vec![vec![0.0f64; height as usize]; width as usize];
let mut direction = vec![vec![0.0f64; height as usize]; width as usize];
for x in 0..width as usize {
for y in 0..height as usize {
let gx_val = gx[x][y];
let gy_val = gy[x][y];
magnitude[x][y] = (gx_val * gx_val + gy_val * gy_val).sqrt();
direction[x][y] = gy_val.atan2(gx_val);
}
}
let suppressed =
non_maximum_suppression(&magnitude, &direction, width as usize, height as usize);
let result = double_threshold_hysteresis(
&suppressed,
low_threshold,
high_threshold,
width as usize,
height as usize,
);
let mut output = GrayImage::new(width, height);
for x in 0..width {
for y in 0..height {
let val = if result[x as usize][y as usize] {
255
} else {
0
};
output.put_pixel(x, y, Luma([val]));
}
}
output
}
fn gaussian_blur(image: &GrayImage) -> GrayImage {
let kernel = [
[1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0],
[2.0 / 16.0, 4.0 / 16.0, 2.0 / 16.0],
[1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0],
];
let (width, height) = image.dimensions();
let mut output = GrayImage::new(width, height);
for x in 1..width - 1 {
for y in 1..height - 1 {
let mut sum = 0.0;
for (kx, kernel_row) in kernel.iter().enumerate() {
for (ky, kernel_val) in kernel_row.iter().enumerate() {
let px = (x as i32 + kx as i32 - 1) as u32;
let py = (y as i32 + ky as i32 - 1) as u32;
sum += image.get_pixel(px, py)[0] as f64 * kernel_val;
}
}
output.put_pixel(x, y, Luma([sum.clamp(0.0, 255.0) as u8]));
}
}
output
}
fn sobel_gradients(image: &GrayImage) -> (Vec<Vec<f64>>, Vec<Vec<f64>>) {
let (width, height) = image.dimensions();
let mut gx = vec![vec![0.0f64; height as usize]; width as usize];
let mut gy = vec![vec![0.0f64; height as usize]; width as usize];
let sobel_x = [[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]];
let sobel_y = [[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]];
for x in 1..width as usize - 1 {
for y in 1..height as usize - 1 {
let mut sum_x = 0.0;
let mut sum_y = 0.0;
for kx in 0..3 {
for ky in 0..3 {
let px = (x as i32 + kx as i32 - 1) as u32;
let py = (y as i32 + ky as i32 - 1) as u32;
let val = image.get_pixel(px, py)[0] as f64;
sum_x += val * sobel_x[kx][ky];
sum_y += val * sobel_y[kx][ky];
}
}
gx[x][y] = sum_x;
gy[x][y] = sum_y;
}
}
(gx, gy)
}
fn non_maximum_suppression(
magnitude: &[Vec<f64>],
direction: &[Vec<f64>],
width: usize,
height: usize,
) -> Vec<Vec<f64>> {
let mut suppressed = vec![vec![0.0f64; height]; width];
for x in 1..width - 1 {
for y in 1..height - 1 {
let angle = direction[x][y].to_degrees();
let angle = if angle < 0.0 { angle + 180.0 } else { angle };
let (neighbor1, neighbor2) =
if (0.0..22.5).contains(&angle) || (157.5..180.0).contains(&angle) {
(magnitude[x + 1][y], magnitude[x - 1][y])
} else if (22.5..67.5).contains(&angle) {
(magnitude[x + 1][y - 1], magnitude[x - 1][y + 1])
} else if (67.5..112.5).contains(&angle) {
(magnitude[x][y + 1], magnitude[x][y - 1])
} else {
(magnitude[x - 1][y - 1], magnitude[x + 1][y + 1])
};
if magnitude[x][y] >= neighbor1 && magnitude[x][y] >= neighbor2 {
suppressed[x][y] = magnitude[x][y];
}
}
}
suppressed
}
fn double_threshold_hysteresis(
image: &[Vec<f64>],
low: f64,
high: f64,
width: usize,
height: usize,
) -> Vec<Vec<bool>> {
let mut result = vec![vec![false; height]; width];
let mut strong = vec![vec![false; height]; width];
for x in 0..width {
for y in 0..height {
if image[x][y] >= high {
strong[x][y] = true;
result[x][y] = true;
} else if image[x][y] >= low {
}
}
}
for x in 1..width - 1 {
for y in 1..height - 1 {
if image[x][y] >= low && !result[x][y] {
let has_strong_neighbor = strong[x - 1][y - 1]
|| strong[x][y - 1]
|| strong[x + 1][y - 1]
|| strong[x - 1][y]
|| strong[x + 1][y]
|| strong[x - 1][y + 1]
|| strong[x][y + 1]
|| strong[x + 1][y + 1];
if has_strong_neighbor {
result[x][y] = true;
}
}
}
}
result
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_slide_solver_creation() {
let piece = DynamicImage::new_rgb8(50, 50);
let bg = DynamicImage::new_rgb8(300, 200);
let solver = SlideSolver::new(piece, bg);
let position = solver.find_position();
assert!(position >= -50.0 && position <= 300.0);
}
}