一、原理
引用
hough变换利用点、线对偶的思想,把提取图像空间中直线的问题转换成在参数空间/hough空间中计算点的峰值的问题。
在x − y坐标系中,假设有一条直线过点( x0 , y0 ),那么我们可以把这条直线的方程记为
那么易得
,把公式3,4代入公式1,整理可得
二、语法
2.1 enumerate
2.1.1 基本用法
s = [2,5,8,3,6,9]
for i in enumerate(s):
print i
结果为:
(0, 2)
(1, 5)
(2, 8)
(3, 3)
(4, 6)
(5, 9)
按照一般的序列迭代方法,可以看出enumerate()函数的返回值是一个元组,元组构成为(index, value),也就是序列索引和值的元组。
2.1.2 第二可选参数,解决索引从0开始输出的问题
enumerate()有两个参数序列和起始索引。
即enumerate(sequence , index)
s = [2,5,8,3,6,9]
for i in enumerate(s, 1):
print i
结果为:
(1, 2)
(2, 5)
(3, 8)
(4, 3)
(5, 6)
(6, 9)
2.2 round
2.2.1 round()是python自带的一个函数,用于数字的四舍五入。
2.2.2使用方法(环境python3)
参数:
digits>0,四舍五入到指定的小数位
digits=0, 四舍五入到最接近的整数
digits<0 ,在小数点左侧进行四舍五入
如果round()函数只有number这个参数,等同于digits=0
四舍五入规则:
- 要求保留位数的后一位<=4,则进位,如round(5.214,2)保留小数点后两位,结果是 5.21
- 要求保留位数的后一位“=5”,且该位数后面没有数字,则不进位,如round(5.215,2),结果为5.21
- 要求保留位数的后一位“=5”,且该位数后面有数字,则进位,如round(5.2151,2),结果为5.22
- 要求保留位数的后一位“>=6”,则进位。如round(5.216,2),结果为5.22
2.3 argpartition
引用
找到最大值
2.4 column_stack
三、代码
3.1 调用opencv库
调用代码如下:
import cv2
import numpy as np
import matplotlib.pyplot as plt
# 读取图片
img = cv2.imread('D:/house.png')
# 彩色图片灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 执行边缘检测
edges = cv2.Canny(gray, 80, 200)
# 显示原始结果
cv2.imwrite('edges.png', edges)
cv2.imshow('edge', edges)
# plt.subplot(121)
# plt.imshow(edges)
# 执行Hough直线检测
lines = cv2.HoughLines(edges, 1, np.pi/180, 130) #cv2.HoughLines 的返回参数 line == [\rho ,\Theta ] ,
# 其中,第一个参数表示图像原点距离直线的长度,第二个参数表示沿着x轴的角度大小。
lines1 = lines[:, 0, :]
print('lines', lines)
print('lines1', lines1)
for rho, theta in lines1:
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b)) # 延长直线的长度,保证在整幅图像上绘制直线
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
cv2.imwrite('line.png', img)
cv2.imshow('line', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
通过调整各项参数,边缘检测图和直线检测图如下图所示:
可以看到检测效果还不错。
通过打印我们发现cv2.HoughLines()函数返回值为弧长和弧度的数组:
3.2 不调用函数
(一)
在参考了这位博主和这位博主的帖子之后,我将代码改为如下形式(此处先只修改了Hough变换的部分,边缘检测依然采用库函数):
import cv2
import numpy as np
# 读取图片
img = cv2.imread('D:/house.png')
# 彩色图片灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 执行边缘检测
edges = cv2.Canny(gray, 80, 200)
# 显示原始结果
cv2.imwrite('edges.png', edges)
# cv2.imshow('edge', edges)
# def Hough(img, origin, delta=1):
def Hough(img):
thetas = np.deg2rad(np.arange(0.0, 181.0, 1))
row, col = img.shape
print('row, col =', row, col)
rhomax = int(round(np.sqrt(row*row+col*col)))
print('rhomax', rhomax)
# 因为ρ会存在负值,所以要先右移,后面减去即可
A = np.zeros((2*rhomax, len(thetas)), np.int32)
for x in range(row):
for y in range(col):
if img[x, y] > 0: # 寻找边缘点
for i in range(len(thetas)): # 找出过边缘点的所有直线
rho = int(round(x*np.cos(thetas[i])+y*np.sin(thetas[i]))) # round四舍五入至接近的整数,计算弧长
rho += rhomax
A[rho, i] += 1 # 统计参数空间中过点的曲线数目
# 统计大于阈值的点数
m = 0
for x in range(len(thetas)):
for y in range(rhomax):
if A[y, x] >= 80: # 设置阈值
m += 1
print('m=', m)
rho_list = np.zeros(m, np.int32) # 存每个点的弧长
theta_list = np.zeros(m, np.int32) # 存每个点的弧度
print('rho_list =', rho_list)
print('theta_list =', theta_list)
for i in range(m):
rho_list[i] = rho
theta_list[i] = i
lines = [list(t) for t in zip(rho_list, theta_list)] # 返回弧长和弧度的数组
return lines
lines = Hough(edges)
print('lines=', lines)
# 画线
for rho, theta in lines:
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b)) # 延长直线的长度,保证在整幅图像上绘制直线
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
cv2.imwrite('line.png', img)
cv2.imshow('line', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
检测结果如下图所示:
由于图片灰度化和边缘检测都是调用的库函数,所以边缘检测图是没有问题的,然而直线检测显然是错的。。。。。。且用自己编写的函数明显运行速度变慢。
(二)
经过对上述程序的分析,我推测可能是在寻找过边缘点直线的过程中,将弧长四舍五入产生了较大的误差,于是对程序做了如下修改:
import cv2
import numpy as np
# 读取图片
img = cv2.imread('D:/house.png')
# 彩色图片灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 执行边缘检测
edges = cv2.Canny(gray, 80, 200)
# 显示原始结果
cv2.imwrite('edges.png', edges)
# cv2.imshow('edge', edges)
# def Hough(img, origin, delta=1):
def Hough(img):
thetas = np.deg2rad(np.arange(0.0, 181.0, 1))
row, col = img.shape
print('row, col =', row, col)
rhomax = int(round(np.sqrt(row*row+col*col)))
print('rhomax', rhomax)
# 因为ρ会存在负值,所以要先右移,后面减去即可
A = np.zeros((2*rhomax, len(thetas)), np.int32)
for x in range(row):
for y in range(col):
if img[x, y] > 0: # 寻找边缘点
for i in range(len(thetas)): # 找出过边缘点的所有直线
rho = int(round((x*np.cos(thetas[i])+y*np.sin(thetas[i])), 1)) # round四舍五入至小数点后1位,计算弧长
rho += rhomax
A[rho, i] += 1 # 统计参数空间中过点的曲线数目
# 将弧长误差为1的点近似为同一个点
for x in range(len(thetas)):
for y in range(rhomax):
for i in range(rhomax-y):
if abs(A[y, x]-A[y+i, x]) <= 1: # 设置阈值,小于等于1判为同一个点
A[y + i, x] = A[y, x]
# 统计大于阈值的点数
m = 0
for x in range(len(thetas)):
for y in range(rhomax):
if A[y, x] >= 80: # 设置阈值
m += 1
print('m=', m)
rho_list = np.zeros(m, np.int32) # 存每个点的弧长
theta_list = np.zeros(m, np.int32) # 存每个点的弧度
print('rho_list =', rho_list)
print('theta_list =', theta_list)
for i in range(m):
rho_list[i] = rho
theta_list[i] = i
lines = [list(t) for t in zip(rho_list, theta_list)] # 返回弧长和弧度的数组
return lines
lines = Hough(edges)
print('lines=', lines)
# 画线
for rho, theta in lines:
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b)) # 延长直线的长度,保证在整幅图像上绘制直线
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
cv2.imwrite('line.png', img)
cv2.imshow('line', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
运行速度又慢了许多。。。。。。检测结果如下:
显然没有任何改进。
此时我发现输出lines如下:
与直接调用函数返回的值大相径庭,根本就是错的。。。。。。
(三)
分析发现是rho_list和theta_list赋值赋错了,然后将程序改为如下形式:
i = 0
for x in range(len(thetas)):
for y in range(rhomax):
if A[y, x] >= delta: # 设置阈值
rho_list[i] = y-rhomax
theta_list[i] = thetas[x]
i += 1
print('rho_list =', rho_list)
print('theta_list =', theta_list)
lines = [list(t) for t in zip(rho_list, theta_list)] # 返回弧长和弧度的数组
输出值如下图所示:
检测结果如是:
还是错的。
由于调用的canny算子阈值没有变,都是80,200,输入边缘图是一样的,我将Hough变换的阈值也变为和调用的函数一样的值130,然后发现大于阈值的只有三条线,原本应该有七条线,且这三条线都是错的。判断边缘点这一步应该不会出错,那么就是说在存入Hough空间数组A这一步就出错了。
(四)
实在写不出来,去翻了一下原函数。请见这位博主和这位博主的帖子以及他们的评论区。非常感谢大家的分享。
我仿照原函数写了一个代码如下:
import cv2
import numpy as np
# 读取图片
img = cv2.imread('D:/house.png')
# 彩色图片灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 执行边缘检测
edges = cv2.Canny(gray, 80, 200)
# 显示原始结果
cv2.imwrite('edges.png', edges)
# cv2.imshow('edge', edges)
# def Hough(img, origin, delta=1):
def Hough(img, delta = 130):
row, col = img.shape # 获取图片长宽
rho = 1.0 #定义以像素为单位的距离精度为1 float型
theta = 1*np.pi/180 #定义以弧度为单位的角度精度为1 float型
print('rho=', rho)
print('theta=', theta)
# 由角度和距离的分辨率得到角度和距离的数量,即霍夫变换后角度和距离的个数
numangle = round(np.pi / theta)
numrho = int(((row + col) * 2 + 1) / rho)
# thetas = np.deg2rad(np.arange(0.0, 180.0, theta)) # 弧度数组
# 定义累加器即霍夫空间,它是用一维数组表示二维空间
A = np.zeros(((numrho + 2), (numangle + 2)), np.int32) # int型数组
# 做好tabSin、tabCos数组
angle = 0.0
tabSin = np.zeros(numangle, np.float)
tabCos = np.zeros(numangle, np.float)
for n in range(numangle):
tabSin[n] = float(np.sin((angle)/rho))
tabCos[n] = float(np.cos((angle)/rho))
angle += theta
# 逐点Hough变换,将结果存入累加器数组内
for x in range(row):
for y in range(col):
if img[x, y] > 0: # 寻找边缘点
for i in range(numangle):
r = int(round(x * tabCos[i] + y * tabSin[i])) # round四舍五入至接近的整数,r表示距离
r = int(r+(numrho - 1) / 2)
A[r+1, i+1] += 1 # 在累加器内找到它们所对应的位置(即霍夫空间内的位置),其值加1
# 找到局部极大值,即非极大值抑制, 记录个数
m = 0
rho_list = np.zeros(numrho, np.float) # 存每个点的弧长
theta_list = np.zeros(numangle, np.float)
for x in range(numrho):
for y in range(numangle):
if A[x+1, y+1] >= delta: # 设置阈值
if A[x+1, y+1] > A[x+1, y] and A[x+1, y+1] >= A[x+1, y+2] and A[x+1, y+1] > A[x, y+1] and A[x+1, y+1] >= A[x+2, y+1]: # 在四邻域内进行非极大值抑制
rho_list[m] = (x - (numrho - 1) / 2) * rho
theta_list[m] = y * theta
m += 1
print('rho_list =', rho_list)
print('theta_list =', theta_list)
rho_list = rho_list[rho_list != 0]
theta_list = theta_list[theta_list != 0]
print('rho_list =', rho_list)
print('theta_list =', theta_list)
lines = [list(t) for t in zip(rho_list, theta_list)] # 返回弧长和弧度的数组
return lines
lines = Hough(edges)
print('lines=', lines)
# 画线
for rho, theta in lines:
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b)) # 延长直线的长度,保证在整幅图像上绘制直线
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
cv2.imwrite('line.png', img)
cv2.imshow('line', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
检测结果如下图所示:
这回总算是检测出了八个点,比之前的三个点好一些,但还是不对。
通过与运行原函数的结果相比对,发现弧长差不多能对上,但是角度对不上,也就是说还是公式的计算出了问题。
(五)
重新分析一遍代码思路:
- 构建Hough空间
(1)首先获取图片宽度和高度。
(2)定义距离精度和角度精度。在这里我定义的都是1,距离精度是1个像素,角度精度是1°,换成弧度也就是代码中表示的那样.
(3)定义角度的个数和距离的个数。这里角度的个数没有异议,肯定是180个。至于的个数,个人认为不用纠结为什么是(周长+1)/距离精度,前面提到的博主在帖子和评论区讲的都比较清楚,距离长度最小值肯定是图片对角线距离的最大值,只要构建的数组空间存放距离的那一栏大于对角线长度就可以,周长显然大于对角线长度,所以是可以的,而+1是为了防止“0”效应,这里到了后面Hough变换那一步就会明白。当然如果用弧度最小值来构建空间我认为也是没有任何问题的。
(4)最后是构建sin和cos数组。 - 进行Hough变换
(1)遍历边缘图,找出所有边缘点。
(2)画出所有过边缘点的直线,判断原点到每一条直线的距离,将距离和角度存入Hough空间相应位置。问题就出在这里。这里row是指图片的高,col是指图片的宽,我的所有循环中x都是取的row(高)的范围,y取得是col(宽)的范围,也就是说我这里的横轴其实是y,纵轴是x,因此我的距离公式应该是:
而不是:
将这里改了之后就对了。。。。。。
继续记录一下我的心路历程。
在图像中每个像素代表的是一个小方格,然而我们在计算距离的时候应该是把每个小方格看成了一个像素点的。如下图所示:
举个例子。假设图中阴影部分为边缘点像素格。最开始我把它看成一个格子,那么我理所当然的将原点距离它的长度想成了原点与①的距离,这个格子的坐标为(3,2),但它是横着的第四个格子,竖着的第三个格子,由勾股定理显然r应该是5,即应该是:
但如果是这样的话,那么假如(0,0)这个点是边缘点的话,它的距离显然不为0。但实际上这个点就是原点,它离原点的距离应该就是0。所以我们应该计算的是原点离②的距离。所以公式中出现的是:
而不是:
因为计算出的r可能为负值,所以我们要再加距离偏移的一半即1024,使r的值最后都能落在[0,2048]这个区间。
然后我们将这组距离、角度存入Hough数组。还是以刚才的例子为例,在存入的过程中,假设距离为5,角度为37°,那么我们应该在(6,38)这个格子+1,这是为了将hough空间的边缘空出来,方便我们进行下一步的非极大值抑制。
- 进行非极大值抑制
(1)这里我们采用的是四邻域比较法。遍历Hough空间除边缘外的每一个点。这里我们表示Hough空间的数组A的范围是横轴[0 ~ 181]共182个格子,纵轴[0 ~ 2050]共2051个格子,我们需要比较y=[1 ~ 180],x=[1 ~ 2049]这些个格子,如图阴影部分所示:
每个格子与它上下左右的四个格子相比较。
(2)找到最大值后我们就存入新的距离、弧度数组,并将其作为返回值进行下一步的画线。
最后放一下代码:
import cv2
import numpy as np
# 读取图片
img = cv2.imread('D:/house.png')
# 彩色图片灰度化
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 执行边缘检测
edges = cv2.Canny(gray, 80, 200)
# 显示原始结果
cv2.imwrite('edges.png', edges)
# cv2.imshow('edge', edges)
# def Hough(img, origin, delta=1):
def Hough(img, delta = 130):
row, col = img.shape # 获取图片长宽
rho = 1.0 #定义以像素为单位的距离精度为1 float型
theta = 1*np.pi/180 #定义以弧度为单位的角度精度为1 float型
print('rho=', rho)
print('theta=', theta)
# 由角度和距离的分辨率得到角度和距离的数量,即霍夫变换后角度和距离的个数
numangle = round(np.pi / theta)
numrho = int(((row + col) * 2 + 1) / rho)
# thetas = np.deg2rad(np.arange(0.0, 180.0, theta)) # 弧度数组
# 定义累加器即霍夫空间,它是用一维数组表示二维空间
A = np.zeros(((numrho + 2), (numangle + 2)), np.int32) # int型数组
# 做好tabSin、tabCos数组
angle = 0.0
tabSin = np.zeros(numangle, np.float)
tabCos = np.zeros(numangle, np.float)
for n in range(numangle):
tabSin[n] = float(np.sin((angle)/rho))
tabCos[n] = float(np.cos((angle)/rho))
angle += theta
# 逐点Hough变换,将结果存入累加器数组内
for x in range(row):
for y in range(col):
if img[x, y] > 0: # 寻找边缘点
for i in range(numangle):
r = int(round(y * tabCos[i] + x * tabSin[i])) # round四舍五入至小数点后1位,r表示距离
r += int((numrho - 1) / 2)
A[r+1, i+1] += 1 # 在累加器内找到它们所对应的位置(即霍夫空间内的位置),其值加1,实际距离r就是r
# 找到局部极大值,即非极大值抑制, 记录个数
m = 0
rho_list = np.zeros(numrho, np.float) # 存每个点的弧长
theta_list = np.zeros(numangle, np.float)
for x in range(numrho): # 0~2048
for y in range(numangle): # 0~179
if A[x+1, y+1] >= delta: # 设置阈值 x+1=[1,2049], y+1=[1,180]
if A[x+1, y+1] > A[x+1, y] and A[x+1, y+1] >= A[x+1, y+2] and A[x+1, y+1] > A[x, y+1] and A[x+1, y+1] >= A[x+2, y+1]: # 在四邻域内进行非极大值抑制
rho_list[m] = (x - (numrho - 1) / 2) * rho
theta_list[m] = y * theta
m += 1
print('m=', m)
rho_list = rho_list[rho_list != 0]
theta_list = theta_list[theta_list != 0]
print('rho_list =', rho_list)
print('theta_list =', theta_list)
lines = [list(t) for t in zip(rho_list, theta_list)] # 返回弧长和弧度的数组
return lines
lines = Hough(edges)
print('lines=', lines)
# 画线
for rho, theta in lines:
a = np.cos(theta)
b = np.sin(theta)
x0 = a*rho
y0 = b*rho
x1 = int(x0 + 1000*(-b)) # 延长直线的长度,保证在整幅图像上绘制直线
y1 = int(y0 + 1000*(a))
x2 = int(x0 - 1000*(-b))
y2 = int(y0 - 1000*(a))
cv2.line(img, (x1, y1), (x2, y2), (0, 0, 255), 2)
cv2.imwrite('line.png', img)
cv2.imshow('line', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
检测结果如图所示:
(六)
将图片灰度化以及canny等所有程序都换成自己编写的之后,程序运行很慢,效果如下图所示:
通过更改阈值能得到不同的检测效果。
四、错误
在调试代码的过程中,遇到了许多问题,在这里记录一下。
(一)、数据类型与数组类型不匹配
这个问题出现了许多次,错误如下:
IndexError: only integers, slices (`:`), ellipsis (`...`), numpy.newaxis (`None`) and integer or boolean arrays are valid indices
分析发现应该是说数组的索引必须是整数,不能是浮点数、字符串等其他类型。
分步运行时发现r是一个浮点数,如下图所示:
但我的程序是这样写的:
r = int(round(x * tabCos[i] + y * tabSin[i])) # round四舍五入至小数点后1位,r表示距离
r += (numrho - 1) / 2
然后我发现第二行的
r += (numrho - 1) / 2
输出为浮点数。但是如果改成如下形式输出就为整数:
r += (numrho - 1) * 2
也就是除法的结果会变成一个浮点数。所以我将第二行也进行了一个int转换:
r = int(round(x * tabCos[i] + y * tabSin[i])) # round四舍五入至接近的整数,r表示距离
r = int(r+(numrho - 1) / 2)
这回总算是对了。
(二)opencv中的cvRound与c++中的round
c++中的round为四舍五入,而opencv中的cvRound为五舍六入。请见这位博主的帖子。
此处应该不用在意这些微小的差别。