霍夫变换是一种常用的在图像中查找直线的方法(当前也被推广到查找圆等其他几何形状)。
其基本原理是将原图像空间上的每个点变为新空间上的一条曲线,然后在新空间上检测多条曲线的焦点从而得到原图像空间上的直线方程。
本文主要以直线为例介绍霍夫直线变换的细节和原理。
对于霍夫直线变换首先需要明确的是变换之后的空间并不是传统意义上的极坐标系,而是 Hesse 仿射坐标系
对于图像空间上的一个点 而言,对应 Hesse 仿射坐标系下的曲线方程为
。
具体曲线绘制方法就是遍历 空间,根据
求出 r。
def get_hough_space(self, edge):
self.edge = edge
self.rmax = int(math.hypot(edge.shape[0], edge.shape[1])) # the maximum value rho can get.
self.hough_space = np.zeros((len(self.thetas), 2 * self.rmax + 1),
dtype=np.uint8) # This is the hough space that we will vote.
h, w = self.edge.shape
for x in range(w): # the X and Y coordinates in an image is different thats why x == img.shape[1]
for y in range(h):
if self.edge[y, x] != 0:
for i, theta in enumerate(self.thetas): # rotate the line
th = math.radians(theta)
ro = round(x * math.cos(th) + y * math.sin(th)) + self.rmax # we add r_max to get rid of negative values for indexing.
if ro <= 2 * self.rmax:
self.hough_space[i, ro] += 1 # vote
return self.hough_space
这样原始图像中一个点(一般输入提取得到的图像边缘信息)就转换为了 Hesse 仿射坐标系下的一条曲线。而 Hesse 仿射坐标系曲线上的每个点都表示了原始图像空间上经过转换点的一条直线。
因此可以通过分析 Hesse 仿射坐标系下曲线的相交情况对原始图像空间中的直线进行检测。
在检测到 Hesse 仿射坐标系下的若干峰值后,根据之前的分析每一个峰值都对应了原始图像空间上的一条直线。比如某一峰值 ,图像空间对应直线方程为
。
这里的 的绝对值就是原始图像坐标系下坐标原点到直线的垂直距离;
为图像空间上直线与 y 轴的夹角。在图像坐标系 x 轴正向水平向右,y轴正向竖直向下定义下,
表示与 y 轴平行,顺时针旋转为正,逆时针旋转为负,取值范围为
。
对于所求的方程 ,可以发现这个直线一定通过图像空间上的
点,具体推导可以代入公式利用
得到。同时由于直线法向量为
,因此该直线可以很简单的确定。
def get_lines(self, hough_space):
self.lines = dict()
# modify hough_space by yourself, then input
y, x = np.where(hough_space)
for i, j in zip(y, x):
th = math.radians(self.thetas[i])
ro = j - self.rmax # we extract rmax since we added it on the preprocessing phase.
a = math.cos(th)
b = math.sin(th)
x0 = a * ro
y0 = b * ro
x1 = int(round(x0 + self.rmax * (-b)))
y1 = int(round(y0 + self.rmax * (a)))
x2 = int(round(x0 - self.rmax * (-b)))
y2 = int(round(y0 - self.rmax * (a)))
if i in self.lines:
self.lines[i].append(((x1, y1), (x2, y2)))
else:
self.lines[i] = [((x1, y1), (x2, y2))]
return self.lines
最后给出完整的程序,程序参考了 GitHub - sercanamac/Line-Detection-Using-Hough-Space
需要特别说明的是绘制的 hough space 图的横向为 r,纵向为 。而且纵坐标数值并不是实际的
,实际的
需要通过纵坐标数值代入 self.thetas 数组得到;同时为了避免 r 向上存在负值,转换时 r 上统一叠加了 self.rmax。
#!usr/bin/env python
# -*- coding: utf-8 -*-
import cv2
import math
import numpy as np
import matplotlib.pyplot as plt
import copy
class HoughTransformer(object):
def __init__(self, theta_step=1):
self.thetas = [i for i in range(-90, 90, theta_step)]
def get_hough_space(self, edge):
self.edge = edge
self.rmax = int(math.hypot(edge.shape[0], edge.shape[1])) # the maximum value rho can get.
self.hough_space = np.zeros((len(self.thetas), 2 * self.rmax + 1),
dtype=np.uint8) # This is the hough space that we will vote.
h, w = self.edge.shape
for x in range(w): # the X and Y coordinates in an image is different thats why x == img.shape[1]
for y in range(h):
if self.edge[y, x] != 0:
for i, theta in enumerate(self.thetas): # rotate the line
th = math.radians(theta)
ro = round(x * math.cos(th) + y * math.sin(th)) + self.rmax # we add r_max to get rid of negative values for indexing.
if ro <= 2 * self.rmax:
self.hough_space[i, ro] += 1 # vote
return self.hough_space
def topk(self, hough_space, k):
idx = np.argpartition(hough_space.ravel(), hough_space.size - k)[-k:]
return np.column_stack(np.unravel_index(idx, hough_space.shape))
def get_lines(self, hough_space):
self.lines = dict()
# modify hough_space by yourself, then input
y, x = np.where(hough_space)
for i, j in zip(y, x):
th = math.radians(self.thetas[i])
ro = j - self.rmax # we extract rmax since we added it on the preprocessing phase.
a = math.cos(th)
b = math.sin(th)
# line equation: ax + by = r
x0 = a * ro
y0 = b * ro
x1 = int(round(x0 + self.rmax * (-b)))
y1 = int(round(y0 + self.rmax * (a)))
x2 = int(round(x0 - self.rmax * (-b)))
y2 = int(round(y0 - self.rmax * (a)))
if i in self.lines:
self.lines[i].append(((x1, y1), (x2, y2)))
else:
self.lines[i] = [((x1, y1), (x2, y2))]
return self.lines
def draw_lines(self, edge, lines):
img = copy.deepcopy(edge)
for degree, points in lines.items():
for p1, p2 in points:
cv2.line(img, p1, p2, 255, 1)
plt.imshow(img)
plt.show()
def main():
edge = np.zeros((600, 600), dtype=np.uint8)
cv2.line(edge, (0, 0), (200, 400), 255, 1)
plt.imshow(edge)
plt.show()
ht = HoughTransformer()
hough_space = ht.get_hough_space(edge)
plt.imshow(hough_space)
plt.show()
peak = ht.topk(hough_space, 1)
print("peak: {}(theta),{}(r)".format(ht.thetas[peak[0][0]], peak[0][1]-ht.rmax))
hough_space[hough_space < hough_space[peak[0][0], peak[0][1]]] = 0
lines = ht.get_lines(hough_space)
ht.draw_lines(edge, lines)
if __name__ == '__main__':
main()
最后需要说明的是霍夫直线变换只能得到直线,而不能得到线段,即无法获取图像中线段的端点信息。
Opencv 中提供了 cv2.HoughLines 利用霍夫变换进行直线检测,而且还提供了能够检测线段的 cv2.HoughLinesP。
def HoughLinesP(image, rho, theta, threshold, lines=..., minLineLength=..., maxLineGap=...)
输入参数:
其检测逻辑: