Реализация обнаружения линий преобразования Хафа в 2D-системе координат

Я хочу реализовать обнаружение линий в простой системе координат. Я примерно следил за статьей о том, как реализовать преобразование Хафа, но результаты, которые я получаю, весьма далеки от того, что я хочу.

Учитывая такую ​​матрицу 3 x 3:

X X X
X X X
- - -

Я хочу обнаружить строку, начинающуюся с 0,0, до 2,0. Я представляю систему координат как простой массив кортежей, первый элемент в кортеже - x, второй - y, третий - тип точки (холст или линия).

Я думал, что с помощью Hough обнаружить линию будет относительно легко, потому что определение границ - это, по сути, просто двоичное решение: либо кортеж имеет тип line, либо нет.

Я реализовал на 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
...

На самом деле это довольно много, учитывая, что я хочу обнаружить только одну строку. Моя реализация явно неверна, но я не знаю, где искать, мой maths-fu недостаточно высок для дальнейшей отладки.

Я думаю, что первая часть, собственно преобразование Хафа, кажется правильной, потому что в связанной статье говорится:

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. Максимальная длина строки ограничена sqrt (imagewidth2 + imageheight2).

  3. Точка p, угол a линии и максимальная длина линии maxLength могут использоваться для вычисления двух других точек линии. Максимальная длина здесь гарантирует, что обе вычисляемые точки лежат за пределами фактического изображения, в результате чего, если линия проведена между этими двумя точками, линия в любом случае идет от границы изображения к границе изображения и никогда не обрезается. где-то внутри изображения.

  4. Эти две точки p1 и p2 вычисляются по формуле: p1_x = px + maxLength * cos (угол); p1_y = py + maxLength * sin (угол); p2_x = px - maxLength * cos (угол); p2_y = py - maxLength * sin (угол);

  5. ...

ИЗМЕНИТЬ

Обновленная версия, которая учитывает размер изображения, как предложено @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 

Я нарисовал линии в системе координат, они очень далеко от линии, которую я ожидал. Интересно, конвертация обратно в баллы все еще отключена.


person Max    schedule 08.11.2016    source источник
comment
При использовании преобразования Хафа многое зависит от реального изображения и от того, насколько резкими являются края. Чем насыщеннее изображение, тем сложнее будут результаты преобразования. Что бы я сделал, так это сгенерировал бы изображение вывода, чтобы проверить, что именно создается. Может помочь добавление изображений ввода и вывода к вопросу.   -  person Salix alba    schedule 08.11.2016
comment
Одна мысль, которая кажется подозрительной, заключается в том, что вы довольно сильно округляете значения r, всего на 4 уровня. Это будет означать, что вы в основном делаете чек с очень широкой линией, и множество точек будет составлять одну и ту же линию.   -  person Salix alba    schedule 08.11.2016
comment
Если вы на самом деле построите линии на выходе, вы обнаружите, что они очень похожи, некоторые из них параллельны, с разницей в пикселе. Рассматривая небольшое изображение, вы усложняете себе жизнь. Попробуйте что-то большее, например изображение 100 x 100, и вы увидите, что результаты станут более четкими. Попробуйте изменить степень детализации r и угла, чтобы увидеть, что произойдет.   -  person Salix alba    schedule 08.11.2016
comment
@Salixalba Я также пробовал с матрицей 30x30, результаты даже с отредактированной версией, к сожалению, довольно далеки. :(   -  person Max    schedule 09.11.2016


Ответы (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

Вам нужно преобразовать все углы в радианы перед вызовом cos и sin.

В первом цикле у меня

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 ржавый (на самом деле не существует), поэтому есть более удобные способы выполнить преобразование, и где-то должна быть константа с точным значением пи.

person Salix alba    schedule 09.11.2016
comment
Пример. - person Shepmaster; 09.11.2016
comment
Спасибо за ответ, я этого не знал. Результаты все еще не такие, как я хотел, поэтому я думаю, что мне нужно начинать заново, реализация в целом кажется неправильной. Принимая этот ответ, поскольку он дал мне некоторые рекомендации по работе с математическими функциями в целом и с ржавчиной в частности, также благодаря @Shepmaster - person Max; 10.11.2016
comment
Я запустил версию модифицированного кода, которая дает более правдоподобные ответы. Есть вторая проблема с вашими значениями r. Иногда они становятся отрицательными, но функция масштабирования округляет все от <1 до 1. - person Salix alba; 10.11.2016
comment
@Salixalba хм, я думал, что реализую функцию масштабирования, чтобы использовать значения в качестве смещения в матрице аккумулятора. - person Max; 10.11.2016

Принцип преобразования Хафа заключается в поиске всех линий, проходящих через каждую рассматриваемую точку, и подсчете их появления благодаря аккумулятору.

Однако мы не можем определить все эти линии, потому что их количество бесконечно. К тому же изображение дискретизировано, поэтому вычислять все линии не имеет смысла.

И проблема в этой дискретности. Дискретизация угла должна соответствовать размеру изображения. Здесь вычисление радиуса для 180 углов является перерасчетом, потому что изображение составляет всего 9 пикселей, а возможные углы для любой линии на этом изображении ограничены дюжиной значений.

Итак, здесь для первой точки (0,0) для каждого угла соответствующий радиус равен r = 0.

Для второго (1,0) соответствующий радиус равен r = cos (угол)

Для третьего (2,0) соответствующий радиус равен r = 2 cos (угол)

При скруглении многочисленные значения будут иметь связанный радиус 0 для одного и того же угла, и это вызовет чрезмерное обнаружение. Дискретность вызывает распространение Hough Accumulator.

Поэтому радиус и угловую дискретизацию необходимо рассчитывать в зависимости от размера изображения. Здесь шаг 30 °, поэтому для обнаружения линии будет достаточно аккумулятора 7 * 3.

person RaymoAisla    schedule 08.11.2016
comment
В этом есть смысл, спасибо. Я пробовал обновленную версию, теперь она находит только 2 строки, что для меня имеет больше смысла. Однако проблема в том, что обе строки действительно далеки от того, что я ожидал, программа сообщает строки от 3,4 до -1,-1 и от -3,1 до 2,2. Я обновил вопрос своим новым кодом, вероятно, обратное преобразование в баллы все еще отключено. - person Max; 09.11.2016