在2D坐标系中实现Hough变换线检测

时间:2016-11-08 05:57:11

标签: algorithm math rust ascii-art

我想在简单的坐标系中实现线检测。我粗略地讲了一篇关于如何实现the Hough Transform的文章,但我得到的结果与我想要的结果相去甚远。

给出像这样的3 x 3矩阵:

X X X
X X X
- - -

我想检测从0,0开始到2,0的行。我将坐标系表示为一个简单的元组数组,元组中的第一项是x,第二项是y,第三项是点的类型(画布或线)。

我认为使用Hough检测线是相对容易的,因为边缘检测基本上只是一个二元决策:元组是否为类型行。

我在Rust中实现了以下程序:

use std::f32;

extern crate nalgebra as na;
use na::DMatrix;

#[derive(Debug, PartialEq, Clone)]
enum Representation {
   Canvas,
   Line,
}

fn main () {
    let image_width = 3;
    let image_height = 3;

    let grid = vec![
        (0, 0, Representation::Line), (1, 0, Representation::Line), (2, 0, Representation::Line),
        (0, 1, Representation::Canvas), (1, 1, Representation::Canvas), (2, 1, Representation::Canvas),
        (0, 2, Representation::Canvas), (1, 2, Representation::Canvas), (2, 2, Representation::Canvas),
    ];

    //let tmp:f32 = (image_width as f32 * image_width as f32) + (image_height as f32 * image_height as f32);
    let max_line_length = 3;
    let mut accumulator = DMatrix::from_element(180, max_line_length as usize, 0);

    for y in 0..image_height {
        for x in 0..image_width {
            let coords_index = (y * image_width) + x;
            let coords = grid.get(coords_index as usize).unwrap();

            // check if coords is an edge
            if coords.2 == Representation::Line {
                for angle in 0..180 {
                    let r = (x as f32) * (angle as f32).cos() + (y as f32) * (angle as f32).sin();
                    let r_scaled = scale_between(r, 0.0, 2.0, -2.0, 2.0).round() as u32;

                    accumulator[(angle as usize, r_scaled as usize)] += 1;
                }
            }
        }
    }

    let threshold = 3;

    // z = angle
    for z in 0..180 {
        for r in 0..3 {
            let val = accumulator[(z as usize, r as usize)];

            if val < threshold {
                continue;
            }

            let px = (r as f32) * (z as f32).cos();
            let py = (r as f32) * (z as f32).sin();

            let p1_px = px + (max_line_length as f32) * (z as f32).cos();
            let p1_py = py + (max_line_length as f32) * (z as f32).sin();

            let p2_px = px - (max_line_length as f32) * (z as f32).cos();
            let p2_py = px - (max_line_length as f32) * (z as f32).cos();

            println!("Found lines from {}/{} to {}/{}", p1_px.ceil(), p1_py.ceil(), p2_px.ceil(), p2_py.ceil());
        }
    }
}

fn scale_between(unscaled_num: f32, min_allowed: f32, max_allowed: f32, min: f32, max: f32) -> f32 {
    (max_allowed - min_allowed) * (unscaled_num - min) / (max - min) + min_allowed
}

结果如下:

Found lines from -1/4 to 1/1
Found lines from 2/4 to 0/0
Found lines from 2/-3 to 0/0
Found lines from -1/4 to 1/1
Found lines from 1/-3 to 0/0
Found lines from 0/4 to 1/1
...

实际上这是非常多的,因为我只想检测一行。我的实现显然是错误的,但我不知道在哪里看,我的数学不足以进一步调试。

我认为第一部分,即实际的Hough变换,似乎是正确的,因为链接的文章说:

for each image point p 
{
  if (p is part of an edge)
  {
    for each possible angle
    {
     r = x * cos(angle) + y * sin(angle);
     houghMatrix[angle][r]++;
    }
  }
}

我坚持使用映射和过滤,这是根据文章:

  
      
  1. 霍夫空间中的每个点由角度a和距离r给出。使用这些值,可以通过计算线的单个点p(x,y)   px = r * cos(角度)   py = r * sin(角度)。

  2.   
  3. 行的最大长度受sqrt(imagewidth2 + imageheight2)限制。

  4.   
  5. 点p,线的角度a和最大线长度&lt; maxLength&#39;可用于计算线的另外两个点。这里的最大长度确保了要计算的两个点都位于实际图像之外,从而导致如果在这两个点之间绘制一条线,则该线在任何情况下从图像边界到图像边界并且永远不会被裁剪在图像中的某个地方。

  6.   
  7. 这两点p1和p2的计算公式如下:   p1_x = px + maxLength * cos(angle);   p1_y = py + maxLength * sin(角度);   p2_x = px - maxLength * cos(angle);   p2_y = py - maxLength * sin(角度);

  8.   
  9. ...

  10.   

修改

根据@RaymoAisla

的建议更新了考虑图像大小的版本
use std::f32;

extern crate nalgebra as na;
use na::DMatrix;

fn main () {
    let image_width = 3;
    let image_height = 3;

    let mut grid = DMatrix::from_element(image_width as usize, image_height as usize, 0);
    grid[(0, 0)] = 1;
    grid[(1, 0)] = 1;
    grid[(2, 0)] = 1;

    let accu_width = 7;
    let accu_height = 3;
    let max_line_length = 3;

    let mut accumulator = DMatrix::from_element(accu_width as usize, accu_height as usize, 0);


    for y in 0..image_height {
        for x in 0..image_width {
            let coords = (x, y);
            let is_edge = grid[coords] == 1;

            if !is_edge {
                continue;
            }

            for i in 0..7 {
                let angle = i * 30;

                let r = (x as f32) * (angle as f32).cos() + (y as f32) * (angle as f32).sin();
                let r_scaled = scale_between(r, 0.0, 2.0, -2.0, 2.0).round() as u32;

                accumulator[(i as usize, r_scaled as usize)] += 1;

                println!("angle: {}, r: {}, r_scaled: {}", angle, r, r_scaled);
            }
        }
    }

    let threshold = 3;

    // z = angle index
    for z in 0..7 {
        for r in 0..3 {
            let val = accumulator[(z as usize, r as usize)];

            if val < threshold {
                continue;
            }

            let px = (r as f32) * (z as f32).cos();
            let py = (r as f32) * (z as f32).sin();

            let p1_px = px + (max_line_length as f32) * (z as f32).cos();
            let p1_py = py + (max_line_length as f32) * (z as f32).sin();

            let p2_px = px - (max_line_length as f32) * (z as f32).cos();
            let p2_py = px - (max_line_length as f32) * (z as f32).cos();

            println!("Found lines from {}/{} to {}/{} - val: {}", p1_px.ceil(), p1_py.ceil(), p2_px.ceil(), p2_py.ceil(), val);
        }
    }
}

fn scale_between(unscaled_num: f32, min_allowed: f32, max_allowed: f32, min: f32, max: f32) -> f32 {
    (max_allowed - min_allowed) * (unscaled_num - min) / (max - min) + min_allowed
}

现在报告的输出是:

angle: 0, r: 0, r_scaled: 1
angle: 30, r: 0, r_scaled: 1
angle: 60, r: 0, r_scaled: 1
angle: 90, r: 0, r_scaled: 1
angle: 120, r: 0, r_scaled: 1
angle: 150, r: 0, r_scaled: 1
angle: 180, r: 0, r_scaled: 1
...
Found lines from 3/4 to -1/-1
Found lines from -3/1 to 2/2 

我在坐标系上绘制了线条,线条与我期望的线条相差很远。我想知道转换回积分的转换是否仍然存在。

2 个答案:

答案 0 :(得分:2)

你的角度是度数而不是弧度!

Rust与所有其他编程语言一样,使用弧度作为其三角函数。运行

let ang_d = 30.0;
let ang_r = ang_d * 3.1415926 / 180.0;
println!("sin(30) {} sin(30*pi/180) {}", (ang_d as f32).sin(), (ang_r as f32).sin());

给出结果

sin(30) -0.9880316 sin(30*pi/180) 0.5

在调用cossin之前,您需要将所有角度转换为弧度。

在第一个循环中,我有

let angle = (i as f32) * 30.0 * 3.1415926 / 180.0;
let r = (x as f32) * (angle as f32).cos() + (y as f32) * (angle as f32).sin();

并在第二行计算行上的点数

let ang = (z as f32) * 30.0 * 3.1415926 / 180.0;
let px = (r as f32) * (ang as f32).cos();
let py = (r as f32) * (ang as f32).sin();
let p1_px = px + (max_line_length as f32) * (ang as f32).cos();          
let p1_py = py + (max_line_length as f32) * (ang as f32).sin();
let p2_px = px - (max_line_length as f32) * (ang as f32).cos();
let p2_py = px - (max_line_length as f32) * (ang as f32).cos();

我的Rust生锈了(实际上并不存在)所以有更好的方法进行转换,并且应该有一个常量与pi的确切值。

答案 1 :(得分:1)

霍夫变换原理是搜索通过每个考虑点的所有线,并通过累加器计算这些线的出现次数。

但是,我们无法确定所有这些行,因为它们的数量是无限的。此外,图像是离散的,因此计算所有线条没有意义。

问题来自于这种离散化。角度离散化需要与图像大小相关。在这里,计算180个角度的半径是过度计算,因为图像只有9个像素,并且该图像中任何线条的可能角度都限制在十几个值。

所以在这里,对于第一个点(0,0),对于每个角度,相关的半径是r = 0

对于第二个(1,0),相关的半径是r = cos(角度)

对于第三个(2,0),相关半径为r = 2 cos(角度)

通过舍入,对于相同的角度,许多值将具有相关的半径0,并且导致过度检测。离散化导致Hough Accumulator的扩散。

因此需要根据图像大小计算半径和角度离散度。这里是一个30°的步长,因此一个7 * 3的累加器就足以检测到一条线。