霍夫变换:用极坐标系表示直线
在直角坐标系(x,y)中的直线通常用:
y = k x + b y=k x+b y=kx+b
表示。k为斜率,b为截距。
而在霍夫变换中,使用极坐标系(r,θ)来表示直线。
直角坐标系中的直线与x轴和y轴围成一个直角三角形,我们作出这个三角形的高(即从原点出发到直线的垂线),则高的长度就是r,r称为该直线到原点的距离;θ就是高与x轴的夹角,θ称为该直线的垂线与x轴的夹角。
对于每一条在直线坐标系中的直线,我们都能将其转化成极坐标系(r,θ)中的一个点(r0,θ0)。
现在我们再从直角坐标系中直线上的某一个点来考虑:
直角坐标系上某一点(x,y)转换成极坐标时,有:
x = r cos θ , y = r sin θ x=r \cos \theta, y=r \sin \theta x=rcosθ,y=rsinθ
r 2 = x 2 + y 2 r^{2}=x^{2}+y^{2} r2=x2+y2
tan θ = y x \tan \theta=\frac{y}{x} tanθ=xy
在直角坐标系上表示某条经过(x,y)的直线可以写成(假设(x,y)是原点出发到该直线的垂线与这条直线的交点):
y = k x + b y=k x+b y=kx+b
用极坐标系来表示该直线(假设(x,y)是原点出发到该直线的垂线与这条直线的交点):
y = ( − cos θ sin θ ) x + ( r sin θ ) y=\left(-\frac{\cos \theta}{\sin \theta}\right) x+\left(\frac{r}{\sin \theta}\right) y=(−sinθcosθ)x+(sinθr)
那么通过点(x,y)的直线可以在极坐标系中统一定义为:
r = x cos θ + y sin θ r=x \cos \theta+y \sin \theta r=xcosθ+ysinθ
我们可以发现一个点经过极坐标系变换后在极坐标系中是一条曲线,而在直角坐标系中在同一条直线上的多个点经过极坐标系变换形成的多条曲线都会在极坐标系中相交于某一点。
一条在直角坐标系上的直线上的所有点经过极坐标系变换后得到的曲线都会在极坐标系中通过同一个点,这个点就是该直线转换成极坐标系中的点(r0,θ0)。
霍夫变换就是追踪图像中每一个像素点对应曲线间的交点。越多曲线交于一点也就意味着这个交点表示的直线由更多的点组成(越亮)。如果交于一点的曲线的数量超过了阈值,那么可以认为这个交点所代表的参数对在原图像中为一条直线。再用下面的公式反向计算直线即可。
y = ( − cos θ sin θ ) x + ( r sin θ ) y=\left(-\frac{\cos \theta}{\sin \theta}\right) x+\left(\frac{r}{\sin \theta}\right) y=(−sinθcosθ)x+(sinθr)
函数原型:
void HoughLines(InputArray image, OutputArray lines, double rho, double theta, int threshold, double srn=0, double stn=0)
函数参数:
image:边缘检测的输出图像。它应该是个灰度图 (但事实上是个二值化图),这是霍夫变换的输入图像。
lines:储存着检测到的直线的参数对(r,θ)的容器。这是最终输出,是极坐标系形式的直线。
rho:参数极径,即以像素值为单位的分辨率,我们使用1像素。
theta:参数极角,即以弧度为单位的分辨率,我们使用1度(即CV_PI/180)。
threshold:要”检测” 一条直线所需最少的的曲线交点个数。
srn and stn:如果srn和stn同时为0,就表示使用经典的霍夫变换。否则,这两个参数应该都为正数。
使用HoughLines函数需要子集将极坐标系形式的直线反转换成直角坐标系形式的直线。
这是执行起来效率更高的概率霍夫线变换,它输出检测到的直线的端点 。
函数原型:
void HoughLinesP(InputArray image, OutputArray lines, double rho, double theta, int threshold,double minLineLength=0, double maxLineGap=0)
函数参数:
image:必须是二值图像,推荐使用canny边缘检测的结果图像。
rho:线段以像素为单位的距离精度,double类型的,推荐用1.0。
theta:线段以弧度为单位的角度精度,推荐用numpy.pi/180。
threshod:累加平面的阈值参数,int类型,超过设定阈值才被检测出线段,值越大,基本上意味着检出的线段越长,检出的线段个数越少。根据情况推荐先用100试试。
lines:储存着检测到的直线的端点的容器。
minLineLength:线段以像素为单位的最小长度,根据应用场景设置。
maxLineGap:同一方向上两条线段判定为一条线段的最大允许间隔(断裂),超过了设定值,则把两条线段当成一条线段,值越大,允许线段上的断裂越大,越有可能检出潜在的直线段。
彩色图像转化为灰度图像;
使用高斯滤波平滑图像;
使用膨胀填充内部空洞,使车道线变粗和更加连贯;
使用canny算子提取图片中物体边缘;
截取感兴趣区域(Region of Interest,ROI)比如截取一个三角形区域,遍历每个像素点坐标,如果发现当前点的坐标不在三角区域内,则将该点像素值置为0。然后只保留图片的ROI区域中的物体边缘;
使用霍夫变换来提取图片ROI区域中的直线(段);
过滤掉一些角度不太符合的直线(段);
对剩余直线(段)按斜率归类为左车道直线集合和右车道直线集合,然后求取其斜率和截距的平均值,计算出平均值直线的两个端点,这样我们就得到了1条左车道直线和1条右车道直线;
在全黑背景的图片上单独绘制车道线,如果两条车道线都存在,则填充两条车道线之间的区域;
在原始彩色图像与我们刚绘制的车道线图像进行比例融合。
该算法在路面车道标志比较完整的情况下效果较好,且经过上述优化后在路面一般颠簸的情况下(比如高速路上过减速带时)仍能准确识别车道线,但如果标志不清晰或路面严重不平导致摄像头画面抖动非常厉害时,该算法的效果就会变差。此外,该算法对于转弯车道的识别较差。
import numpy as np
import cv2
import time
def region_of_interest(img, vertices):
"""
生成ROI区域
:param img: 原始图像,是提取了物体边缘的图像
:param vertices: 多边形坐标
:return: 返回只保留了ROI区域内的物体边缘的图像
"""
# 生成和img大小一致的图像矩阵,全部填充0(黑色)
roi = np.zeros_like(img)
# vertices即三角形区域顶点,填充三角形内部区域为白色
ignore_mask_color = 255
# 填充函数,将vertices多边形区域填充为指定的灰度值
cv2.fillPoly(roi, vertices, ignore_mask_color)
# 显示ROI区域
# cv2.imshow("ROI", roi)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
# 两张图片上的像素进行与操作,ROI区域中已经填充白色,其他是黑色
# img中是canny算子提取的物体边缘是白色,其他区域是黑色
# 黑色与黑色与运算还是黑色,白色与白色与运算还是白色,白色与黑色与运算是黑色
# bitwise_and即两张图片上相同位置像素点上的灰度值进行与运算
masked_image = cv2.bitwise_and(img, roi)
# cv2.imshow("masked_image", masked_image)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
return masked_image
def bypass_angle_filter(lines, low_thres, high_thres):
"""
角度滤波器
:param lines: 概率霍夫变换得到的直线的端点对集合
:param low_thres:低阈值
:param high_thres:高阈值
:return:得到过滤后的直线端点对集合
"""
filtered_lines = []
if lines is None:
return filtered_lines
for line in lines:
for x1, y1, x2, y2 in line:
# 过滤掉角度0或90度的直线
if x1 == x2 or y1 == y2:
continue
# 保留角度在low_thres到high_thres之间的直线,角度按360度的标度来算
angle = abs(np.arctan((y2 - y1) / (x2 - x1)) * 180 / np.pi)
if low_thres < angle < high_thres:
filtered_lines.append([[x1, y1, x2, y2]])
return filtered_lines
def average_lines(lines, y_min, y_max):
"""
求霍夫变换得到的直线检测出的线的平均值,以平均值作为左右车道的线,这样左右车道各只有一条线,左右车道根据直线斜率判断
m为斜率,b为截距,norm为长度
:param lines:概率霍夫变换得到的直线两个端点
:param y_min:
:param y_max:
:return:
"""
# 直线y=mx+b
# 左右车道的候选直线存储集合,以斜率/截距/线段长度形式分别存储
hough_pts = {'m_left': [], 'b_left': [], 'norm_left': [], 'm_right': [], 'b_right': [], 'norm_right': []}
if lines is not None:
for line in lines:
for x1, y1, x2, y2 in line:
# poly_coef保存了多项式系数,对于一次项,第0个元素是k,第1个元素是b
poly_coef = np.polyfit([x2, x1], [y2, y1], 1)
norm = ((x2 - x1) ** 2 + (y2 - y1) ** 2) ** 0.5
if poly_coef[0] > 0:
# 如果是右车道线
hough_pts['m_right'].append(poly_coef[0])
hough_pts['b_right'].append(poly_coef[1])
hough_pts['norm_right'].append(norm)
if poly_coef[0] < 0:
# 如果是左车道线
hough_pts['m_left'].append(poly_coef[0])
hough_pts['b_left'].append(poly_coef[1])
hough_pts['norm_left'].append(norm)
if len(hough_pts['b_left']) != 0 or len(hough_pts['m_left']) != 0 or len(hough_pts['norm_left']) != 0:
# 如果集合中左车道直线集合中有直线
b_avg_left = np.mean(np.array(hough_pts['b_left']))
m_avg_left = np.mean(np.array(hough_pts['m_left']))
xmin_left = int((y_min - b_avg_left) / m_avg_left)
xmax_left = int((y_max - b_avg_left) / m_avg_left)
left_lane = [[xmin_left, y_min, xmax_left, y_max]]
else:
left_lane = [[0, 0, 0, 0]]
if len(hough_pts['b_right']) != 0 or len(hough_pts['m_right']) != 0 or len(hough_pts['norm_right']) != 0:
# 如果集合中右车道直线集合中有直线
b_avg_right = np.mean(np.array(hough_pts['b_right']))
m_avg_right = np.mean(np.array(hough_pts['m_right']))
xmin_right = int((y_min - b_avg_right) / m_avg_right)
xmax_right = int((y_max - b_avg_right) / m_avg_right)
right_lane = [[xmin_right, y_min, xmax_right, y_max]]
else:
right_lane = [[0, 0, 0, 0]]
return [left_lane, right_lane]
def draw_lines(img, lines, color, thickness=2):
"""
最后处理好的左右车道线集合输入,根据线两点坐标画直线
:param img: 输入背景图像(全黑)
:param lines: 输入左右车道线集合
:param color: 指定直线颜色
:param thickness: 指定直线宽度
:return:
"""
for line in lines:
for x1, y1, x2, y2 in line:
cv2.line(img, (x1, y1), (x2, y2), color, thickness)
# 如果左右两条车道线都存在,那么填充两条车道线之间的区域(高亮)
if lines[0] != [0, 0, 0, 0] and lines[1] != [0, 0, 0, 0]:
lines_np = np.array(lines)
lines_np_del = np.squeeze(lines_np)
polys = []
poly_1 = [(lines_np_del[0][0], lines_np_del[0][1]), (lines_np_del[0][2], lines_np_del[0][3]),
(lines_np_del[1][2], lines_np_del[1][3]), (lines_np_del[1][0], lines_np_del[1][1])]
polys.append(poly_1)
cv2.fillPoly(img, np.array(polys), (0, 255, 0))
def process_image(img):
"""
对输入图像进行灰度化、高斯平滑、canny算子提取物体边缘、只保留ROI区域内物体边缘、霍夫变换求取图片内直线、将直线画到原图图像上
:param img: 原始彩色图像
:return: 标注了车道线的彩色图像
"""
# 霍夫像素单位,取1
rho = 1
# 霍夫角度移动步长,使用1度
theta = np.pi / 180
# 霍夫平面累加阈值,即检测一条直线所需最少的曲线交点个数
hof_threshold = 20
# 线段最小长度
min_line_len = 30
# 线段最大允许断裂长度
max_line_gap = 60
# 高斯滤波核大小size
kernel_size = 5
# canny算子边缘检测低阈值
canny_low_threshold = 75
# canny算子边缘检测高阈值
canny_high_threshold = canny_low_threshold * 3
# 原图像权重
alpha = 1
# 车道线图像权重
beta = 0.5
gamma = 0.
# 获取图像大小,注意shape[0]是高,shape[1]是宽
img_shape = img.shape
# print(img_shape)
# 彩色图转换成灰度图,使用cv2.imread()读到的img的数据排列为BGR,因此这里的参数为BGR2GRAY
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
# 高斯滤波平滑图像
blur_gray = cv2.GaussianBlur(gray, (kernel_size, kernel_size), 0)
# 平滑后的图像做一下膨胀,使物体边缘更容易检测到
dilate_gray = cv2.dilate(blur_gray, (3, 3), iterations=10)
# Canny算子边缘检测
edge_image = cv2.Canny(dilate_gray, canny_low_threshold, canny_high_threshold)
# 显示canny算子提取的边缘
# cv2.imshow("canny_edge_image", edge_image)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
# 确定ROI多边形区域,两层的np数组是因为可以输入多个区域,但我们这里其实只有一个区域,内层每一个np数组记录了一个多边形的坐标
# shape[0]是高,shape[1]是宽,图像坐标系以左上角为原点,向下为y轴正向,向右为x轴正向
# 下面是一个三角形,底下两点是图像的左下角和右下角
vertices = np.array([[(0, img_shape[0]), (img_shape[1] / 2, 23 * img_shape[0] / 40), (img_shape[1], img_shape[0])]],
dtype=np.int32)
masked_edges = region_of_interest(edge_image, vertices)
# 基于概率霍夫变换的直线检测
lines = cv2.HoughLinesP(masked_edges, rho, theta, hof_threshold, np.array([]), minLineLength=min_line_len,
maxLineGap=max_line_gap)
# img: 输入只保留了ROI区域内的物体边缘的图像
# rho: 线段以像素为单位的距离精度, 一般取1
# theta: 像素以弧度为单位的角度精度, 一般取1度(np.pi / 180)
# threshold: 霍夫平面累加的阈值, 即检测一条直线所需最少的的曲线交点个数
# min_line_len: 线段最小长度(像素级)
# max_line_gap: 最大允许断裂长度
# 返回检测到的直线的两个端点对的集合
# 过滤掉一些角度不符合的直线
low_thres, high_thres = 30, 80
angle_filter_lines = bypass_angle_filter(lines, low_thres, high_thres)
# 求取左右车道直线集合的平均值作为左右车道线
y_min, y_max = int(img.shape[0] * 0.65), int(img.shape[0] * 1.0)
avg_hlines = average_lines(angle_filter_lines, y_min, y_max)
# 创建一个新黑色背景图片,在图片上绘制检测到的车道线
lines_image = np.zeros_like(img)
lines_color = [0, 0, 255]
# 绘制车道线线段
draw_lines(lines_image, avg_hlines, lines_color, thickness=15)
# cv2.imshow("lines_image", lines_image)
# cv2.waitKey(0)
# cv2.destroyAllWindows()
# 原彩色图像与车道线图像融合,alpha为第一个数组的权重,beta为第二个数组的权重,gamma为一个加到权重总和上的标量值
lines_edges = cv2.addWeighted(img, alpha, lines_image, beta, gamma)
return lines_edges
if __name__ == "__main__":
# 检测视频流中的车道线
# ./test_image_and_video/1.avi
cap = cv2.VideoCapture("./test_image_and_video/video_1.mp4")
cap_width, cap_height = int(cap.get(3)), int(cap.get(4))
fps = 0
while cap.isOpened():
return_value, frame = cap.read()
if return_value == 0:
break
start_time = time.time()
processed_image = process_image(frame)
# cv2.waitKey(1)
fps = int(1.0 / (time.time() - start_time))
fps_str = "FPS: " + str(fps)
cv2.putText(processed_image, text=fps_str, org=(cap_width - 80, 15), fontFace=cv2.FONT_HERSHEY_SIMPLEX,
fontScale=0.50, color=(255, 0, 0), thickness=2)
cv2.imshow("image", processed_image)
# 按q可以退出
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
cv2.destroyAllWindows()
# 检测图片中车道线
image = cv2.imread("./test_image_and_video/0.jpg")
line_image = process_image(image)
cv2.imshow("image", line_image)
cv2.waitKey(0)
cv2.destroyAllWindows()