use crate::error::Result;
use image::{DynamicImage, GrayImage, Luma, Rgb, RgbImage};
use scirs2_core::ndarray::Array2;
use std::collections::HashMap;
use std::f32::consts::PI;
#[derive(Debug, Clone, Copy)]
pub struct HoughLine {
pub rho: f32,
pub theta: f32,
pub votes: u32,
}
#[derive(Debug, Clone)]
pub struct HoughParams {
pub rho_resolution: f32,
pub theta_resolution: f32,
pub threshold: u32,
pub min_line_length: f32,
pub max_line_gap: f32,
}
impl Default for HoughParams {
fn default() -> Self {
Self {
rho_resolution: 1.0,
theta_resolution: PI / 180.0, threshold: 100,
min_line_length: 50.0,
max_line_gap: 10.0,
}
}
}
#[allow(dead_code)]
pub fn hough_lines(edges: &GrayImage, params: &HoughParams) -> Result<Vec<HoughLine>> {
let (width, height) = edges.dimensions();
let max_rho = ((width * width + height * height) as f32).sqrt();
let n_rho = (2.0 * max_rho / params.rho_resolution).ceil() as usize;
let n_theta = (PI / params.theta_resolution).ceil() as usize;
let mut accumulator = Array2::zeros((n_rho, n_theta));
for y in 0..height {
for x in 0..width {
if edges.get_pixel(x, y)[0] > 0 {
for theta_idx in 0..n_theta {
let theta = theta_idx as f32 * params.theta_resolution;
let rho = x as f32 * theta.cos() + y as f32 * theta.sin();
let rho_idx = ((rho + max_rho) / params.rho_resolution) as usize;
if rho_idx < n_rho {
accumulator[[rho_idx, theta_idx]] += 1;
}
}
}
}
}
let mut lines = Vec::new();
for rho_idx in 1..n_rho - 1 {
for theta_idx in 1..n_theta - 1 {
let votes = accumulator[[rho_idx, theta_idx]];
if votes >= params.threshold {
let mut is_peak = true;
for dr in -1..=1 {
for dt in -1..=1 {
if dr == 0 && dt == 0 {
continue;
}
let r = (rho_idx as i32 + dr) as usize;
let t = (theta_idx as i32 + dt) as usize;
if r < n_rho && t < n_theta && accumulator[[r, t]] > votes {
is_peak = false;
break;
}
}
if !is_peak {
break;
}
}
if is_peak {
let rho = (rho_idx as f32 * params.rho_resolution) - max_rho;
let theta = theta_idx as f32 * params.theta_resolution;
lines.push(HoughLine { rho, theta, votes });
}
}
}
}
lines.sort_by_key(|l| std::cmp::Reverse(l.votes));
Ok(lines)
}
#[derive(Debug, Clone, Copy)]
pub struct LineSegment {
pub x1: f32,
pub y1: f32,
pub x2: f32,
pub y2: f32,
pub strength: f32,
}
#[allow(dead_code)]
pub fn hough_lines_p(edges: &GrayImage, params: &HoughParams) -> Result<Vec<LineSegment>> {
let (width, height) = edges.dimensions();
let mut segments = Vec::new();
let mut edge_map = edges.clone();
let mut edge_points = Vec::new();
for y in 0..height {
for x in 0..width {
if edge_map.get_pixel(x, y)[0] > 0 {
edge_points.push((x, y));
}
}
}
use scirs2_core::random::seq::SliceRandom;
let mut rng = scirs2_core::random::rng();
edge_points.shuffle(&mut rng);
for &(x, y) in &edge_points {
if edge_map.get_pixel(x, y)[0] == 0 {
continue; }
let max_rho = ((width * width + height * height) as f32).sqrt();
let n_theta = (PI / params.theta_resolution).ceil() as usize;
let mut line_votes: HashMap<(i32, usize), u32> = HashMap::new();
let search_radius = 10;
for dy in -search_radius..=search_radius {
for dx in -search_radius..=search_radius {
let nx = (x as i32 + dx).max(0).min(width as i32 - 1) as u32;
let ny = (y as i32 + dy).max(0).min(height as i32 - 1) as u32;
if edge_map.get_pixel(nx, ny)[0] > 0 {
for theta_idx in 0..n_theta {
let theta = theta_idx as f32 * params.theta_resolution;
let rho = nx as f32 * theta.cos() + ny as f32 * theta.sin();
let rho_idx = ((rho + max_rho) / params.rho_resolution) as i32;
*line_votes.entry((rho_idx, theta_idx)).or_insert(0) += 1;
}
}
}
}
if let Some((&(rho_idx, theta_idx), &votes)) = line_votes.iter().max_by_key(|&(_, &v)| v) {
if votes >= params.threshold / 10 {
let theta = theta_idx as f32 * params.theta_resolution;
let rho = (rho_idx as f32 * params.rho_resolution) - max_rho;
if let Some(segment) = extract_line_segment(&mut edge_map, rho, theta, params) {
if segment_length(&segment) >= params.min_line_length {
segments.push(segment);
}
}
}
}
}
Ok(segments)
}
#[allow(dead_code)]
fn extract_line_segment(
edge_map: &mut GrayImage,
rho: f32,
theta: f32,
params: &HoughParams,
) -> Option<LineSegment> {
let (width, height) = edge_map.dimensions();
let cos_theta = theta.cos();
let sin_theta = theta.sin();
let mut points = Vec::new();
if sin_theta.abs() > cos_theta.abs() {
for x in 0..width {
let y = ((rho - x as f32 * cos_theta) / sin_theta).round() as i32;
if y >= 0 && y < height as i32 && edge_map.get_pixel(x, y as u32)[0] > 0 {
points.push((x as f32, y as f32));
edge_map.put_pixel(x, y as u32, Luma([0])); }
}
} else {
for y in 0..height {
let x = ((rho - y as f32 * sin_theta) / cos_theta).round() as i32;
if x >= 0 && x < width as i32 && edge_map.get_pixel(x as u32, y)[0] > 0 {
points.push((x as f32, y as f32));
edge_map.put_pixel(x as u32, y, Luma([0])); }
}
}
if points.len() < 2 {
return None;
}
points.sort_by(|a, b| {
let dist_a = a.0 * a.0 + a.1 * a.1;
let dist_b = b.0 * b.0 + b.1 * b.1;
dist_a.partial_cmp(&dist_b).expect("Operation failed")
});
let mut segments = Vec::new();
let mut start = points[0];
let mut end = points[0];
for &point in points.iter().skip(1) {
let dist = ((point.0 - end.0).powi(2) + (point.1 - end.1).powi(2)).sqrt();
if dist <= params.max_line_gap {
end = point;
} else {
let length = ((end.0 - start.0).powi(2) + (end.1 - start.1).powi(2)).sqrt();
if length >= params.min_line_length {
segments.push(LineSegment {
x1: start.0,
y1: start.1,
x2: end.0,
y2: end.1,
strength: length,
});
}
start = point;
end = point;
}
}
let length = ((end.0 - start.0).powi(2) + (end.1 - start.1).powi(2)).sqrt();
if length >= params.min_line_length {
segments.push(LineSegment {
x1: start.0,
y1: start.1,
x2: end.0,
y2: end.1,
strength: length,
});
}
segments.into_iter().max_by(|a, b| {
a.strength
.partial_cmp(&b.strength)
.expect("Operation failed")
})
}
#[allow(dead_code)]
fn segment_length(segment: &LineSegment) -> f32 {
((segment.x2 - segment.x1).powi(2) + (segment.y2 - segment.y1).powi(2)).sqrt()
}
#[allow(dead_code)]
pub fn draw_lines(img: &DynamicImage, lines: &[HoughLine], color: [u8; 3]) -> RgbImage {
let mut result = img.to_rgb8();
let (width, height) = result.dimensions();
for line in lines {
draw_hough_line(&mut result, line.rho, line.theta, width, height, color);
}
result
}
#[allow(dead_code)]
pub fn draw_line_segments(
img: &DynamicImage,
segments: &[LineSegment],
color: [u8; 3],
) -> RgbImage {
let mut result = img.to_rgb8();
for segment in segments {
draw_line_segment(&mut result, segment, color);
}
result
}
#[allow(dead_code)]
fn draw_hough_line(
img: &mut RgbImage,
rho: f32,
theta: f32,
width: u32,
height: u32,
color: [u8; 3],
) {
let cos_theta = theta.cos();
let sin_theta = theta.sin();
let rgb_color = Rgb(color);
if sin_theta.abs() > 0.001 {
for x in 0..width {
let y = ((rho - x as f32 * cos_theta) / sin_theta).round() as i32;
if y >= 0 && y < height as i32 {
img.put_pixel(x, y as u32, rgb_color);
}
}
}
if cos_theta.abs() > 0.001 {
for y in 0..height {
let x = ((rho - y as f32 * sin_theta) / cos_theta).round() as i32;
if x >= 0 && x < width as i32 {
img.put_pixel(x as u32, y, rgb_color);
}
}
}
}
#[allow(dead_code)]
fn draw_line_segment(img: &mut RgbImage, segment: &LineSegment, color: [u8; 3]) {
let rgb_color = Rgb(color);
let mut x0 = segment.x1.round() as i32;
let mut y0 = segment.y1.round() as i32;
let x1 = segment.x2.round() as i32;
let y1 = segment.y2.round() as i32;
let dx = (x1 - x0).abs();
let dy = (y1 - y0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx - dy;
let (width, height) = img.dimensions();
loop {
if x0 >= 0 && x0 < width as i32 && y0 >= 0 && y0 < height as i32 {
img.put_pixel(x0 as u32, y0 as u32, rgb_color);
}
if x0 == x1 && y0 == y1 {
break;
}
let e2 = 2 * err;
if e2 > -dy {
err -= dy;
x0 += sx;
}
if e2 < dx {
err += dx;
y0 += sy;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_hough_lines() {
let mut edges = GrayImage::new(50, 50);
for i in 0..50 {
edges.put_pixel(i, i, Luma([255]));
}
let params = HoughParams {
threshold: 30,
..Default::default()
};
let lines = hough_lines(&edges, ¶ms).expect("Operation failed");
assert!(!lines.is_empty());
let best_line = &lines[0];
let angle_diff_1 = (best_line.theta - PI / 4.0).abs();
let angle_diff_2 = (best_line.theta - 3.0 * PI / 4.0).abs();
assert!(
angle_diff_1 < 0.1 || angle_diff_2 < 0.1,
"Expected angle near π/4 or 3π/4, got {}",
best_line.theta
);
}
#[test]
fn test_line_segments() {
let segment = LineSegment {
x1: 0.0,
y1: 0.0,
x2: 3.0,
y2: 4.0,
strength: 1.0,
};
assert_eq!(segment_length(&segment), 5.0);
}
}