简介:上一节中我们计算得到了前视图到俯视图的投影变换矩阵,在这一节中我们要完成车道线的检测。
当车辆摄像头获取到一张图像,我们大致需要经过以下步骤来得到车道线数据:
1、预设参数
在整体过程中,我们需要预设以下变量参数
#!/usr/bin/env python3
import cv2
import numpy as np
import math
import time
from matplotlib import pyplot as plt
class LineDetect():
def __init__(self):
#图像缩小比例,减少运算量
self.scale = [160,120]
#边缘检测参数设置
self.canny_thresholds = [80,200]
self.canny_aperture_size = 3
self.dilation_kernel_size = 3
#霍夫变换参数设置
self.hough_threshold = 2
self.hough_min_line_length = 3
self.hough_max_line_gap = 1
#色彩平衡参数设置
self.lower_thresholds = [53, 57, 59]
self.higher_thresholds = [189, 189, 190]
#投影变换参数设置
self.M = np.array([-1.02032622e+00,-2.48368883e+00,6.44725345e+02,2.54332970e-16,-4.61731493e+00,9.22218319e+02,-8.37904170e-19,-7.77917189e-03,1.00000000e+00])
self.M = np.reshape(self.M, (3, 3))
2、图像预处理:转俯视图,等比例缩小,色彩平衡
#图像预处理
def prev_cb_image(self, image):
#转俯视图
fs_img = cv2.warpPerspective(image,self.M,(640,480))
#等比例缩小
scale_img = cv2.resize(fs_img, (self.scale[0],self.scale[1]), interpolation=cv2.INTER_NEAREST)
#色彩平衡调整
color_banlance_img = self.apply_color_balance(self.lower_thresholds, self.higher_thresholds, scale_img)
cv2.imshow("fs_img",fs_img)
cv2.imshow("scale_img",scale_img)
cv2.imshow("color_banlance_img",color_banlance_img)
cv2.waitKey(0)
return color_banlance_img
def apply_color_balance(self, lower_threshold, higher_threshold, image, scale=1):
if lower_threshold is None:
return None
resized_image = cv2.resize(image, (0, 0), fx=scale, fy=scale)
channels = cv2.split(resized_image)
out_channels = []
for idx, channel in enumerate(channels):
thresholded = self.apply_threshold(channel, lower_threshold[idx], higher_threshold[idx])
normalized = cv2.normalize(thresholded, thresholded.copy(), 0, 255, cv2.NORM_MINMAX)
out_channels.append(normalized)
return cv2.merge(out_channels)
def apply_threshold(self, matrix, low_value, high_value):
low_mask = matrix < low_value
matrix = self.apply_mask(matrix, low_mask, low_value)
high_mask = matrix > high_value
matrix = self.apply_mask(matrix, high_mask, high_value)
return matrix
def apply_mask(self, matrix, mask, fill_value):
masked = np.ma.array(matrix, mask=mask, fill_value=fill_value)
return masked.filled()
if __name__=='__main__':
node = LineDetect()
image = cv2.imread("images/line.png")
color_banlance_img = node.prev_cb_image(image)
cv2.destroyAllWindows()
3、车道线边缘检测
def getEdges(self, image):
#图像转化为HSV格式
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
#检测图像中的边缘
edges = cv2.Canny(image,self.canny_thresholds[0],self.canny_thresholds[1],apertureSize=self.canny_aperture_size)
cv2.imshow('edges', edges)
#检测图像中白色、黄色、红色的区域
white_map = cv2.inRange(hsv, np.array([0,0,150]),np.array([180,30,255]))
yellow_map = cv2.inRange(hsv, np.array([26,43,100]),np.array([34,255,255]))
red_map = cv2.inRange(hsv, np.array([0,43,46]),np.array([15,255,255]))
red_map |= cv2.inRange(hsv, np.array([156,43,100]),np.array([180,255,255]))
#膨胀腐蚀内核设置,单倍内核
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (self.dilation_kernel_size, self.dilation_kernel_size))
#白色、黄色区域进行膨胀处理
white_map = cv2.dilate(white_map, kernel)
yellow_map = cv2.dilate(yellow_map, kernel)
#对红色区域特殊处理,先单核腐蚀,在双核膨胀,减少干扰
red_map = cv2.erode(red_map, kernel)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (self.dilation_kernel_size*2, self.dilation_kernel_size*2))
red_map = cv2.dilate(red_map, kernel)
#边缘与各颜色区域求交集,即为各颜色区域的边缘数据
edge_white = cv2.bitwise_and(white_map, edges)
edge_yellow = cv2.bitwise_and(yellow_map, edges)
edge_red = cv2.bitwise_and(red_map, edges)
cv2.imshow('edge_white', edge_white)
cv2.imshow('edge_yellow', edge_yellow)
cv2.imshow('edge_red', edge_red)
cv2.waitKey(0)
return edge_white,edge_yellow,edge_red
主函数
if __name__=='__main__':
node = LineDetect()
image = cv2.imread("images/line.png")
color_banlance_img = node.prev_cb_image(image)
cv2.destroyAllWindows()
edge_white,edge_yellow,edge_red = node.getEdges(color_banlance_img)
cv2.destroyAllWindows()
注:由于途中没有红色区域,所以检测不到红色边缘。
4、检测边缘中的的直线
def getLines(self, edges):
#检测边缘中的线段,线段以起点终点坐标体现
lines = cv2.HoughLinesP(edges,rho=1,theta=np.pi / 180,threshold=self.hough_threshold,minLineLength=self.hough_min_line_length,maxLineGap=self.hough_max_line_gap)
if lines is not None:
#重新整理数据格式
lines = lines.reshape((-1,4))
return lines
else:
return None
if __name__=='__main__':
node = LineDetect()
image = cv2.imread("images/line.png")
color_banlance_img = node.prev_cb_image(image)
cv2.destroyAllWindows()
edge_white,edge_yellow,edge_red = node.getEdges(color_banlance_img)
cv2.destroyAllWindows()
white_lines = node.getLines(edge_white)
yellow_lines = node.getLines(edge_yellow)
red_lines = node.getLines(edge_red)
fig = plt.figure(figsize=(20, 20))
ax1 = fig.add_subplot(1, 2, 1)
ax1.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
ax1.set_title('Input image'), ax1.set_xticks([]), ax1.set_yticks([])
ax2 = fig.add_subplot(1, 2, 2)
ax2.set_title('lines')
if white_lines is not None:
for line in white_lines:
[x1, y1, x2, y2] = line
X = np.array([x1, x2])
Y = np.array([y1, y2])
c = 'b-'
ax2.plot(X, Y, c)
if yellow_lines is not None:
for line in yellow_lines:
[x1, y1, x2, y2] = line
X = np.array([x1, x2])
Y = np.array([y1, y2])
c = 'g-'
ax2.plot(X, Y, c)
if red_lines is not None:
for line in red_lines:
[x1, y1, x2, y2] = line
X = np.array([x1, x2])
Y = np.array([y1, y2])
c = 'r-'
ax2.plot(X, Y, c)
plt.show()
5、坐标转换
上文计算出的坐标参数是以图像的坐标做为参考,原点为左上角向右为x轴正,向下为y轴正,而我们计算车辆位置时,是以车辆所在位置为原点计算的,向前为x轴正,向左为y轴正,同时计算单位也要换算成米,在虚拟仿真环境中,每块地图块尺寸为0.585米,转化为俯视图后为640像素,然后图像又缩小4倍,所以像素坐标转化为尺寸坐标需要经过以下转变:
V = v*4/640*0.585
图像坐标转观察坐标(观察坐标原点为图像坐标中的(320,480),等比例缩小后为(80,120)):
X = 120-y
Y = 80-x
所以:
观察坐标(米):
X = (120-y)*4/640*0.585
Y = (80-x)*4/640*0.585
编程实现坐标转化:
def lines_to_coors(self, white_lines,yellow_lines,red_lines):
white_coors = []
yellow_coors = []
red_coors = []
#计算黄色线段x轴平均值
yellow_mean_x = np.mean(np.mean(yellow_lines, 0).reshape((2,2)), 0)[0] if yellow_lines is not None else 0
if yellow_lines is not None:
for line in yellow_lines:
[x1, y1, x2, y2] = line
yellow_coors.append(self.calcRealCoor(x1,y1,x2,y2))
white_lines_use = None
white_mean_x = 0
if white_lines is not None:
white_lines_use = []
for line in white_lines:
[x1, y1, x2, y2] = line
#如果白色线段x坐标小于黄线均值,说明这条白线在黄线左侧,无参考价值,跳过删除
if (x1+x2)/2<yellow_mean_x:
continue
else:
white_coors.append(self.calcRealCoor(x1,y1,x2,y2))
white_lines_use.append([x1, y1, x2, y2])
white_lines_use = np.array(white_lines_use)
if len(white_lines_use)>0:
white_mean_x = np.mean(np.mean(white_lines_use, 0).reshape((2,2)), 0)[0]
if red_lines is not None:
for line in red_lines:
[x1, y1, x2, y2] = line
#如果红色线段不在白线和黄线之间,无参考价值,跳过删除
if (yellow_mean_x!=0 and (x1+x2)/2<yellow_mean_x) or (white_mean_x!=0 and (x1+x2)/2>white_mean_x):
continue
else:
red_coors.append(self.calcRealCoor(x1,y1,x2,y2))
return white_coors, yellow_coors, red_coors
def calcRealCoor(self, x1, y1, x2, y2):
X1 = (120-y1)*4/640*0.585
Y1 = (80-x1)*4/640*0.585
X2 = (120-y2)*4/640*0.585
Y2 = (80-x2)*4/640*0.585
return [X1,Y1,X2,Y2]
显示转化后的坐标:
white_coors, yellow_coors, red_coors = node.lines_to_coors(white_lines,yellow_lines,red_lines)
if white_coors is not None:
for line in white_coors:
[x1, y1, x2, y2] = line
X = np.array([x1, x2])
Y = np.array([y1, y2])
c = 'b-'
ax2.plot(X, Y, c)
if yellow_coors is not None:
for line in yellow_coors:
[x1, y1, x2, y2] = line
X = np.array([x1, x2])
Y = np.array([y1, y2])
c = 'g-'
ax2.plot(X, Y, c)
if red_coors is not None:
for line in red_coors:
[x1, y1, x2, y2] = line
X = np.array([x1, x2])
Y = np.array([y1, y2])
c = 'r-'
ax2.plot(X, Y, c)
plt.show()
由图可以大致观察出黄线和白线之间的距离大约为0.22米左右,与实际情况相符。
到此为止,我们得到了车道线边缘上的线段坐标,在实际应用中,需要转化为ROS系统中的消息格式,这一步需要引入duckietown_msgs中的Segment、以及SegmentList,具体转化我们在下一节中介绍。
若有收获,就点个赞吧