pub mod gradients;
use crate::error::Result;
use crate::image::Image;
use burn::tensor::{Tensor, TensorData, backend::Backend};
pub type LineSegment = ((usize, usize), (usize, usize));
impl<B: Backend> Image<B> {
pub fn sobel(&self) -> Result<Self> {
let gray = self.grayscale()?;
let dims = gray.tensor.dims();
let _c = dims[0]; let h = dims[1];
let w = dims[2];
let device = gray.tensor.device();
let tensor_data = gray.tensor.clone().into_data();
let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
let mut mag_vals = vec![0.0f32; h * w];
let kx = [[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]];
let ky = [[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]];
for y in 1..(h - 1) {
for x in 1..(w - 1) {
let mut gx = 0.0f32;
let mut gy = 0.0f32;
for dy in -1..=1 {
for dx in -1..=1 {
let val =
flat_vals[(y as isize + dy) as usize * w + (x as isize + dx) as usize];
gx += val * kx[(dy + 1) as usize][(dx + 1) as usize] as f32;
gy += val * ky[(dy + 1) as usize][(dx + 1) as usize] as f32;
}
}
mag_vals[y * w + x] = (gx * gx + gy * gy).sqrt();
}
}
let new_data = TensorData::new(mag_vals, [1, h, w]);
let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
Ok(Image::new(new_tensor))
}
pub fn canny(&self, low_threshold: f32, high_threshold: f32) -> Result<Self> {
let blurred = self.grayscale()?.gaussian_blur(5, 1.4)?;
let dims = blurred.tensor.dims();
let h = dims[1];
let w = dims[2];
let device = blurred.tensor.device();
let tensor_data = blurred.tensor.into_data();
let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
let mut gx_vals = vec![0.0f32; h * w];
let mut gy_vals = vec![0.0f32; h * w];
let mut mag_vals = vec![0.0f32; h * w];
let mut angle_vals = vec![0.0f32; h * w];
let kx = [[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]];
let ky = [[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]];
for y in 1..(h - 1) {
for x in 1..(w - 1) {
let mut gx = 0.0f32;
let mut gy = 0.0f32;
for dy in -1..=1 {
for dx in -1..=1 {
let val =
flat_vals[(y as isize + dy) as usize * w + (x as isize + dx) as usize];
gx += val * kx[(dy + 1) as usize][(dx + 1) as usize] as f32;
gy += val * ky[(dy + 1) as usize][(dx + 1) as usize] as f32;
}
}
let idx = y * w + x;
gx_vals[idx] = gx;
gy_vals[idx] = gy;
mag_vals[idx] = (gx * gx + gy * gy).sqrt();
let mut angle = gy.atan2(gx).to_degrees();
if angle < 0.0 {
angle += 180.0;
}
angle_vals[idx] = angle;
}
}
let mut nms_vals = vec![0.0f32; h * w];
for y in 1..(h - 1) {
for x in 1..(w - 1) {
let idx = y * w + x;
let angle = angle_vals[idx];
let mag = mag_vals[idx];
let (mag1, mag2) =
if (0.0..22.5).contains(&angle) || (157.5..=180.0).contains(&angle) {
(mag_vals[y * w + (x + 1)], mag_vals[y * w + (x - 1)])
} else if (22.5..67.5).contains(&angle) {
(
mag_vals[(y - 1) * w + (x + 1)],
mag_vals[(y + 1) * w + (x - 1)],
)
} else if (67.5..112.5).contains(&angle) {
(mag_vals[(y - 1) * w + x], mag_vals[(y + 1) * w + x])
} else {
(
mag_vals[(y - 1) * w + (x - 1)],
mag_vals[(y + 1) * w + (x + 1)],
)
};
if mag >= mag1 && mag >= mag2 {
nms_vals[idx] = mag;
} else {
nms_vals[idx] = 0.0;
}
}
}
let mut final_vals = vec![0.0f32; h * w];
let mut strong_edges = Vec::new();
let mut labels = vec![0u8; h * w];
for y in 1..(h - 1) {
for x in 1..(w - 1) {
let idx = y * w + x;
let val = nms_vals[idx];
if val >= high_threshold {
labels[idx] = 2;
strong_edges.push((x, y));
final_vals[idx] = 1.0; } else if val >= low_threshold {
labels[idx] = 1;
}
}
}
while let Some((x, y)) = strong_edges.pop() {
for dy in -1..=1 {
for dx in -1..=1 {
let nx = x as isize + dx;
let ny = y as isize + dy;
if nx >= 0 && nx < w as isize && ny >= 0 && ny < h as isize {
let nidx = (ny as usize) * w + (nx as usize);
if labels[nidx] == 1 {
labels[nidx] = 2;
final_vals[nidx] = 1.0;
strong_edges.push((nx as usize, ny as usize));
}
}
}
}
}
let new_data = TensorData::new(final_vals, [1, h, w]);
let new_tensor = Tensor::<B, 3>::from_data(new_data, &device);
Ok(Image::new(new_tensor))
}
pub fn hough_lines_p(
&self,
rho: f32,
theta: f32,
threshold: u32,
min_line_length: u32,
max_line_gap: u32,
) -> Result<Vec<LineSegment>> {
let dims = self.tensor.dims();
let h = dims[1];
let w = dims[2];
let tensor_data = self.tensor.clone().into_data();
let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
let mut edge_points = Vec::new();
for y in 1..(h - 1) {
for x in 1..(w - 1) {
if flat_vals[y * w + x] > 0.5 {
edge_points.push((x, y));
}
}
}
if edge_points.is_empty() {
return Ok(Vec::new());
}
let num_theta = (std::f64::consts::PI / theta as f64) as usize;
let diag = ((h * h + w * w) as f64).sqrt();
let num_rho = (2.0 * diag / rho as f64) as usize;
let mut accumulator = vec![vec![0u32; num_theta]; num_rho];
for &(x, y) in &edge_points {
for t_idx in 0..num_theta {
let angle = t_idx as f64 * theta as f64;
let r = x as f64 * angle.cos() + y as f64 * angle.sin();
let r_idx = ((r / rho as f64 + diag / rho as f64) as usize).min(num_rho - 1);
accumulator[r_idx][t_idx] += 1;
}
}
let mut lines = Vec::new();
let mut visited = vec![vec![false; num_theta]; num_rho];
for r_idx in 1..(num_rho - 1) {
for t_idx in 1..(num_theta - 1) {
if accumulator[r_idx][t_idx] >= threshold
&& !visited[r_idx][t_idx]
&& accumulator[r_idx][t_idx] >= accumulator[r_idx - 1][t_idx]
&& accumulator[r_idx][t_idx] >= accumulator[r_idx + 1][t_idx]
&& accumulator[r_idx][t_idx] >= accumulator[r_idx][t_idx - 1]
&& accumulator[r_idx][t_idx] >= accumulator[r_idx][t_idx + 1]
{
visited[r_idx][t_idx] = true;
let angle = t_idx as f64 * theta as f64;
let r_val = (r_idx as f64 - diag / rho as f64) * rho as f64;
let cos_a = angle.cos();
let sin_a = angle.sin();
let mut line_pts: Vec<(usize, usize)> = edge_points
.iter()
.filter(|&&(x, y)| {
let proj = x as f64 * cos_a + y as f64 * sin_a;
(proj - r_val).abs() < rho as f64 * 1.5
})
.copied()
.collect();
line_pts.sort_by_key(|&(x, y)| (x as i64 - y as i64).abs());
if line_pts.len() >= min_line_length as usize {
let mut segments = Vec::new();
let mut seg_start = 0;
for j in 1..line_pts.len() {
let dx = line_pts[j].0 as i64 - line_pts[j - 1].0 as i64;
let dy = line_pts[j].1 as i64 - line_pts[j - 1].1 as i64;
let dist = ((dx * dx + dy * dy) as f64).sqrt() as u32;
if dist > max_line_gap {
if j - seg_start >= min_line_length as usize {
segments.push((line_pts[seg_start], line_pts[j - 1]));
}
seg_start = j;
}
}
if line_pts.len() - seg_start >= min_line_length as usize {
segments.push((line_pts[seg_start], *line_pts.last().unwrap()));
}
lines.extend(segments);
}
}
}
}
Ok(lines)
}
pub fn hough_circles(
&self,
dp: f32,
min_dist: f32,
param1: f32,
param2: f32,
min_radius: usize,
max_radius: usize,
) -> Result<Vec<(usize, usize, usize)>> {
let gray = self.grayscale()?;
let dims = gray.tensor.dims();
let h = dims[1];
let w = dims[2];
let blurred = gray.gaussian_blur(5, 1.4)?;
let tensor_data = blurred.tensor.clone().into_data();
let flat_vals: Vec<f32> = tensor_data.iter::<f32>().collect();
let mut gx_vals = vec![0.0f32; h * w];
let mut gy_vals = vec![0.0f32; h * w];
let kx = [[-1.0, 0.0, 1.0], [-2.0, 0.0, 2.0], [-1.0, 0.0, 1.0]];
let ky = [[-1.0, -2.0, -1.0], [0.0, 0.0, 0.0], [1.0, 2.0, 1.0]];
for y in 1..(h - 1) {
for x in 1..(w - 1) {
let mut gxx = 0.0f32;
let mut gyy = 0.0f32;
for dy in -1..=1 {
for dx in -1..=1 {
let val =
flat_vals[(y as isize + dy) as usize * w + (x as isize + dx) as usize];
gxx += val * kx[(dy + 1) as usize][(dx + 1) as usize];
gyy += val * ky[(dy + 1) as usize][(dx + 1) as usize];
}
}
gx_vals[y * w + x] = gxx;
gy_vals[y * w + x] = gyy;
}
}
let mut circles = Vec::new();
let step = (dp as usize).max(1);
let max_r = max_radius.min(h / 2).min(w / 2);
for r in min_radius..=max_r {
let r = r as f32;
let edge_thresh = param1 * 0.5;
let mut acc = vec![0u32; h * w];
for y in (1..(h - 1)).step_by(step) {
for x in (1..(w - 1)).step_by(step) {
let gx = gx_vals[y * w + x];
let gy = gy_vals[y * w + x];
let mag = (gx * gx + gy * gy).sqrt();
if mag < edge_thresh {
continue;
}
let angle = gy.atan2(gx);
for sign in [-1.0, 1.0] {
let vx = (x as f32 + sign * r * angle.cos()).round() as isize;
let vy = (y as f32 + sign * r * angle.sin()).round() as isize;
if vx >= 0 && vx < w as isize && vy >= 0 && vy < h as isize {
acc[vy as usize * w + vx as usize] += 1;
}
}
}
}
for y in ((r as usize)..(h - r as usize)).step_by(step) {
for x in ((r as usize)..(w - r as usize)).step_by(step) {
if acc[y * w + x] >= param2 as u32 {
let mut is_max = true;
for dy in -(step as isize)..=(step as isize) {
for dx in -(step as isize)..=(step as isize) {
let ny = y as isize + dy;
let nx = x as isize + dx;
if ny >= 0
&& ny < h as isize
&& nx >= 0
&& nx < w as isize
&& (ny as usize != y || nx as usize != x)
&& acc[ny as usize * w + nx as usize] > acc[y * w + x]
{
is_max = false;
}
}
}
if is_max {
let too_close =
circles.iter().any(|&(cx, cy, _): &(usize, usize, usize)| {
let dx = cx as f32 - x as f32;
let dy = cy as f32 - y as f32;
(dx * dx + dy * dy).sqrt() < min_dist
});
if !too_close {
circles.push((x, y, r as usize));
}
}
}
}
}
}
Ok(circles)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_helpers::{TestBackend, test_device};
#[test]
fn test_sobel_and_canny() {
let device = test_device();
let data = TensorData::new(vec![0.3f32; 3 * 16 * 16], [3, 16, 16]);
let img = Image::new(Tensor::<TestBackend, 3>::from_data(data, &device));
let sobel = img.sobel().unwrap();
assert_eq!(sobel.shape(), [1, 16, 16]);
let canny = img.canny(0.1, 0.3).unwrap();
assert_eq!(canny.shape(), [1, 16, 16]);
}
#[test]
fn test_hough_lines_p() {
let device = test_device();
let mut data = vec![0.0f32; 32 * 32];
for x in 5..27 {
data[16 * 32 + x] = 1.0;
}
let tensor =
Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [1, 32, 32]), &device);
let img = Image::new(tensor);
let lines = img
.hough_lines_p(1.0, std::f32::consts::PI / 180.0, 10, 5, 5)
.unwrap();
assert!(!lines.is_empty());
}
#[test]
fn test_hough_circles() {
let device = test_device();
let mut data = vec![0.0f32; 60 * 60];
let cx = 30.0_f32;
let cy = 30.0_f32;
let radius = 15.0_f32;
for angle_deg in 0..360 {
let angle = angle_deg as f32 * std::f32::consts::PI / 180.0;
for dr in -1..=1 {
let r = radius + dr as f32;
let x = (cx + r * angle.cos()).round() as usize;
let y = (cy + r * angle.sin()).round() as usize;
if x < 60 && y < 60 {
data[y * 60 + x] = 1.0;
}
}
}
let tensor =
Tensor::<TestBackend, 3>::from_data(TensorData::new(data, [1, 60, 60]), &device);
let img = Image::new(tensor);
let circles = img.hough_circles(1.0, 5.0, 2.0, 2.0, 8, 20).unwrap();
let _ = circles;
}
}