Hough(霍夫)直线检测


Hough(霍夫)直线检测

直线检测:对于边界上有 n 个点的点集,找出共线的点集和直线方程。

Hough 变换的原理

对于直线$y = a*x + b$,从原点做其垂线,如图中的$CD$。如果将其看做极坐标系,则$CD$可以用$(\rho, \theta)$表示。

怎么表达AB与CD的关系呢?

任取AB上一点E(x,y),做CD的垂线GH,AB的垂线GI。从而我们可以得到角度关系$\angle{IGE} = \angle {GHC} = \angle{DCB} = \theta$。
所以,$CH = y sin\theta, HD = GI = x cos\theta$,从而$CD = CH+HD = xcos\theta + ysin\theta$,即$\rho = xcos\theta + ysin\theta$。可以证明对于直线AB上的任意一点,都满足上式,即$\rho = xcos\theta + ysin\theta$是直线AB的表达式。

再看$\rho = xcos\theta + ysin\theta$,如果我们固定$x,y$,将$\theta$作为自变量,可以得到一个$\rho - \theta$的一个函数关系式。当$\theta$变化时(CD绕原点旋转时),AB也会随之变化(转动)。由于固定了$x,y$,所以直线AB一定会经过点$(x,y)$,所以它表达了经过点$(x,y)$的所有直线。

例如:$\rho = 3cos\theta + 2sin\theta$,曲线上每一个点$(\rho, \theta)$都可以表示一条经过点$(3,2)$的直线。

下图分别是$\rho = 3cos\theta + 2sin\theta$,$\rho = 2cos\theta + sin\theta$,$\rho = cos\theta$的图像,则分别表示所有经过点$(3,2)$,$(2,1)$,$(1,0)$的所有直线。那么图中的交点A表示什么?
经过点$(3,2)$,$(2,1)$,$(1,0)$的直线是同一条线。Amazing!这不就是说明$(3,2)$,$(2,1)$,$(1,0)$三点共线吗,感觉忽然发现了新大陆。

现在,我们已经找到了证明多个点共线的方法了。让我们重新梳理一下:

  1. 设有一个点集$R$,可得到$R$中任意点$(x_i,y_i)$对应的曲线$\rho_i = x_icos\theta + y_isin\theta$,$\theta \in [0, 2\pi)$。它表示了经过该点的所有直线。
  2. 设$(x_i, y_i),(x_j,y_j) \in R,i \ne j$ ,如果$\rho_i = x_icos\theta + y_isin\theta$与$\rho_j = x_jcos\theta + y_jsin\theta$有交点$A_{ij}(p_{ij}, \theta_{ij}$),就说明$(x_i, y_i),(x_j,y_j) $两点共线。
  3. 由$A_{ij}(p_{ij}, \theta_{ij})$可得到$(x_i, y_i),(x_j,y_j) $所在的直线$\rho_{ij} = xcos\theta_{ij} + ysin\theta_{ij}$。

刚才分析的都是基于连续的函数,如果要用算法实现,则需要将其离散化
因为$\theta \in [0, 2\pi)$,但是如果用程序实现只能将$\theta$离散化,如令theta = range(0, 360) 取360个离散的值(具体的精度根据实际情况决定)。

算法实现

  1. 输入一幅RGB图
  2. 检测图中的边缘,可以使用Canny边缘检测得到一幅二值图像
  3. 开辟一个二维数组 array ,初始值全部为0,用于存储共线点的个数;确定$\theta$的精度。
  4. 对于每一个边缘点$(x_i,y_i)$,使用公式$\rho_i = x_icos\theta + y_isin\theta$计算每一个$\theta$对应的,将作为数组 array 的索引,将对应的数组元素值加 1
  5. 设定一个阈值,过滤出共线点较多的直线(也可以选取共线点最多的几条直线)。
  6. 将检测出的直线绘制到原图上。

Code[Python]

  • hough_without_canny 中直接使用的是经过Canny检测后的二值图像。
  • hough_with_canny 中调用了之前写过的Canny算法。(有时间会把Canny检测的代码也贴出来)

效果如下:

原图


直线检测

from PIL import Image
import matplotlib.pyplot as plt
import numpy as np
import canny
import math


def to_gray(img):
    height, width, _ = img.shape
    gray = np.zeros((height, width), dtype=np.uint8)
    k = np.array([0.299, 0.587, 0.114])

    for i in range(height):
        for j in range(width):
            gray[i][j] = (k * img[i][j]).sum()

    return gray


def to_rgb(img):
    height, width = img.shape
    rgb = np.zeros((height, width, 3), dtype=np.uint8)

    for i in range(height):
        for j in range(width):
            rgb[i][j] = [img[i][j]] * 3

    return rgb


def plot_line(img, expression):
    if len(img.shape) != 3 or img.shape[2] != 3:
        print('The image shape must be (height, width, 3)')
        return img

    height = img.shape[0]
    width = img.shape[1]

    def func(x, a, b):
        return int(a * x + b)

    for exp in expression:
        a, b = exp
        point = [(x, func(x, a, b)) for x in range(width)
                 if 0 <= func(x, a, b) < height]

        for x, y in point:
            img[y][x] = [255, 0, 0]

    return img


def hough_check(img, precision=360, threshold=50, number=-1):
    height, width = img.shape
    theta = range(0, precision)
    factor = 360 / precision

    tab_height = len(theta)
    tab_width = (height + width) << 1
    tab = [[0] * tab_width for i in range(tab_height)]

    for i in range(height):
        for j in range(width):
            if img[i][j] == 255:
                for k in theta:
                    arc = k * factor * math.pi / 180
                    rho = int(j * math.cos(arc) + i * math.sin(arc))
                    tab[k][rho + height + width] += 1

    line = []
    for i in range(tab_height):
        for j in range(tab_width):
            if tab[i][j] > threshold:
                # append theta (0-360) and rho to line
                line.append((i * factor, j - height - width))

    if number == -1 or len(line) <= number:
        return line
    else:
        line.sort(key=lambda x: tab[int(x[0] / factor)][x[1] + height + width])
        return line[0:number]


def get_expression(pair):
    theta, rho = pair
    theta = theta * math.pi / 180
    k = -math.cos(theta) / (math.sin(theta) + 1e-8)
    b = rho / (math.sin(theta) + 1e-8)
    return k, b


def hough_with_canny():
    filename = 'hf.jpg'
    rgb_img = Image.open(filename)
    gray_img = rgb_img.convert('L')
    gray_array = np.array(gray_img)
    # img = to_gray(img_array)
    gray_array = canny.canny(gray_array, (3, 3))

    line = hough_check(gray_array, number=5)
    print('The number of lines is &#123;&#125;'.format(len(line)))
    expression = [get_expression(x) for x in line]
    rgb_array = to_rgb(gray_array)
    rgb_array = plot_line(rgb_array, expression)

    plt.figure()
    plt.imshow(rgb_array)
    plt.show()


def hough_without_canny():
    filename = 'gray.jpg'
    rgb_img = Image.open(filename)
    gray_img = rgb_img.convert('L')

    gray_array = np.array(gray_img)
    rgb_array = to_rgb(gray_array)

    line = hough_check(gray_array, number=5)
    print('The number of lines is &#123;&#125;'.format(len(line)))
    expression = [get_expression(x) for x in line]
    rgb_array = plot_line(rgb_array, expression)

    plt.figure()
    plt.imshow(rgb_array)
    plt.show()


if __name__ == '__main__':
    # main()
    hough_without_canny()

文章作者: Xu Yuan
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Xu Yuan !
评论
 上一篇
傅里叶变换 傅里叶变换
傅里叶变换是一种线性积分变换,用于信号在时域和频域之间的变换。由法国学者约瑟夫·傅里叶系统地提出,所以以其名字来命名以示纪念。本文主要讲解傅里叶变换的公式推导和快速傅里叶变换算法的原理及实现。 笔者并没有在课程中学习过傅里叶变换,最初的
2020-04-30
下一篇 
书单 书单
在读 经济学原理-微观经济学 北欧神话 浩荡两千年 经济类 经济学原理-微观经济学 经济学原理-宏观经济学 浩荡两千年 跌荡一百年 激荡三十年 计算机类 人月神话 摩托车维修艺术 设计模式之禅 浪潮之巅 AI未来
2020-03-05
  目录