本笔记gitee源代码:
https://gitee.com/hongtao-jiang/opencv_lanedetect.git
2023.8.5
文章目录
- 1、OpenCV安装
- 2、图片的读入、保存
- 3、Canny算法边缘检测
- 4、ROI mask
- 5、霍夫变换
- 6、离群值过滤
- 7、最小二乘拟合
- 8、直线绘制
- 9、视频流读写
1、OpenCV安装
conda管理虚拟环境与否看自己
pip install opencv
import cv2
print(cv2.__version__)
查看该库是否安装成功
2、图片的读入、保存
import cv2
img=cv2.imread('img.jpg',cv2.IMREAD_GRAYSCALE) #此为以灰度图格式读入,彩色读入则shape是3通道
print(type(img)) #<class 'numpy.ndarray'>
print(img.shape) #(368, 640)
cv2.imshow('image',img)#一闪而逝
#cv2.waitKey(0)#延时阻塞 0一直阻直到键入 单位毫秒
#k=cv2.waitKey(0)
#print(k) #输出键入的ASCII值
cv2.imwrite('img_gray.jpg',img)#灰度图重组到文件里,图片要指定后缀,cv以此确定压缩格式
3、Canny算法边缘检测
求取每个像素点周边梯度,对比变化,以确定是否为边缘
对于B,C点,不处于边缘,求梯度无明显变化,但是处在边缘的A来说,梯度变化会很大,就可能是边缘点
以上是理想例子,沿着边缘法向求梯度求,实际上情况更复杂,例如:
1是左右边缘,3上下左右都有,360°计算运算成本高,Canny算法则是选取4个梯度方向(有正负所以说4个),
(1)应用高斯滤波器,平滑图像,滤除噪声。
边缘检测易受噪声影响,所以平滑图像,降低噪声点
(2)计算图像每个像素点的梯度大小和方向。
(3)非极大值抑制,消除边缘检测带来的不利影响
遍历图像中所有的像素点,判断当前像素点是否是周围像素点中具有相同方向梯度的最大值
保留黄色背景像素点,其他的归0
(4)应用双阈值检测确定真实和潜在的边缘
先设置高、低两个阈值(一般高阈值是低阈值的2~3倍),遍历整个灰度矩阵,若某点的梯度高于高阈值,则在结果中置1,若该点的梯度值低于低阈值,则在结果中置0,若该点的梯度值介于高低阈值之间,则需要进行如下判断:检查该点(将其视为中心点)的8邻域点,看是否存在梯度值高于高阈值的点,若存在,则说明该中心点和确定的边缘点相连接,故在结果中置1,否则置0。
(5)抑制孤立的弱边缘完成边缘检测
双阈值检测确定真是和潜在的边缘
原图:
边缘轮廓:
阈值:edge_img=cv2.Canny(img,50,100)
阈值:edge_img=cv2.Canny(img,70,120)
相比前面面的图,边缘线少了
import cv2
img=cv2.imread('img.jpg',cv2.IMREAD_GRAYSCALE)
edge_img=cv2.Canny(img,70,120)
cv2.imshow('edges',edge_img)
cv2.waitKey(0)
cv2.imwrite('edge_img.jpg',edge_img)
附一段可动态调节阈值的实时检测效果代码:
import cv2
cv2.namedWindow('edge_detection')
cv2.createTrackbar('minThreshold','edge_detection',50,1000,lambda x: x)
cv2.createTrackbar('maxThreshold','edge_detection',100,1000,lambda x: x)
img=cv2.imread('img.jpg',cv2.IMREAD_GRAYSCALE)
while True:
minThreshold=cv2.getTrackbarPos('minThreshold', 'edge_detection')
maxThreshold = cv2.getTrackbarPos('maxThreshold', 'edge_detection')
edges = cv2.Canny(img, minThreshold, maxThreshold)
cv2.imshow('edge_detection', edges)
cv2.waitKey(10)
效果如下:
用以选择合适阈值
4、ROI mask
region of interest 感兴趣区域
· 数组切片
· 布尔运算(AND与运算)
原图以矩阵np.array形式存储在内存
zeros_like生成一个大小一致的全0矩阵
fillpoly填充掩码部分,留作与操作
cv2.fillPoly(mask,np.array([[[0,368],[240,210],[300,210],[640,368]]]),color=255)
这部分是将四个顶点包住的梯形填充,255为灰度值,纯白色
import cv2
import numpy as np
edge_img=cv2.imread('edge_img.jpg',cv2.IMREAD_GRAYSCALE)
mask=np.zeros_like(edge_img)
cv2.fillPoly(mask,np.array([[[0,368],[240,210],[300,210],[640,368]]]),color=255)
cv2.imshow('mask',mask)
cv2.waitKey(0)
import cv2
import numpy as np
edge_img=cv2.imread('edge_img.jpg',cv2.IMREAD_GRAYSCALE)
mask=np.zeros_like(edge_img)
mask=cv2.fillPoly(mask,np.array([[[0,368],[240,210],[300,210],[640,368]]]),color=255)
#cv2.imshow('mask',mask)
#cv2.waitKey(0)
mask_edge_img=cv2.bitwise_and(edge_img,mask)
cv2.imshow('mask_edge_img.jpg',mask_edge_img)
#cv2.imwrite('mask_edge_img.jpg',mask_edge_img)
cv2.waitKey(0)
有点偏,多了杂点,往右挪一挪
np.array([[[0, 368], [300, 210], [340, 210], [640, 368]]]),
color=255)
5、霍夫变换
找直线用
笛卡尔坐标系中的一条线,在霍夫空间中是个点;
霍夫空间中的一条线,代表笛卡尔系中所有经过某点的直线;
!!!
所以,经过这样一个对应变换的思路,霍夫空间中的一个点作为参数可以确定笛卡尔空间中多数点确定的一条直线
右图是极坐标系对应霍夫空间交线图
如图,两条直线,右图两个两点,说明相交次数多,根据这两个点的坐标参数,确定直线
api:
返回值是一个列表,代表线段起点和终点的坐标值
opencv中霍夫变换要在灰度图中进行,读取图片必须注意格式
在控制台逐行输入
import cv2
import numpy as np
img=cv2.imread('lines.jpg',cv2.IMREAD_GRAYSCALE)
lines = cv2.HoughLinesP(img, 1, np.pi / 180, 15, minLineLength=40,
maxLineGap=20)
lines
len(lines)
我们直观感受到的2条线,发现输出会有32条直线
其实是因为我们所处理的原图,直线有宽度,粗了
看参数,其中有很多线起点终点坐标相差不大,就是一条线段
为了先验获得两条,我们得进行分类,按斜率分为两组
import cv2
import numpy as np
def calculate_slope(line):
"""
计算线段line的斜率
:param line: np.array([[x_1, y_1, x_2, y_2]])
:return:
"""
x_1, y_1, x_2, y_2 = line[0]
return (y_2 - y_1) / (x_2 - x_1)
edge_img = cv2.imread('mask_edge_img.jpg', cv2.IMREAD_GRAYSCALE)
# 获取所有线段
lines = cv2.HoughLinesP(edge_img, 1, np.pi / 180, 15, minLineLength=40,
maxLineGap=20)
# 按照斜率正负分成车道线
left_lines = [line for line in lines if calculate_slope(line) > 0]
right_lines = [line for line in lines if calculate_slope(line) < 0]
注意,opencv中,坐标原点在左上角
6、离群值过滤
上节根据斜率分为左右两组线,但实际因为误差,噪点,会误识别为车道线,我们要进一步剔除一部分,我们知道,落在车道线上的线占大多数,与之斜率差别大的我们过滤掉
import cv2
import numpy as np
def calculate_slope(line):
"""
计算线段line的斜率
:param line: np.array([[x_1, y_1, x_2, y_2]])
:return:
"""
x_1, y_1, x_2, y_2 = line[0]
return (y_2 - y_1) / (x_2 - x_1)
edge_img = cv2.imread('mask_edge_img.jpg', cv2.IMREAD_GRAYSCALE)
# 获取所有线段
lines = cv2.HoughLinesP(edge_img, 1, np.pi / 180, 15, minLineLength=40, maxLineGap=20)
# 按照斜率分成车道线
left_lines = [line for line in lines if calculate_slope(line) > 0]
right_lines = [line for line in lines if calculate_slope(line) < 0]
def reject_abnormal_lines(lines, threshold):
"""
剔除斜率不一致的线段
:param lines: 线段集合, [np.array([[x_1, y_1, x_2, y_2]]),np.array([[x_1, y_1, x_2, y_2]]),...,np.array([[x_1, y_1, x_2, y_2]])]
"""
slopes = [calculate_slope(line) for line in lines]#建个斜率列表
while len(lines) > 0:
mean = np.mean(slopes)#计算斜率平均值
diff = [abs(s - mean) for s in slopes]#计算每个斜率和平均值的差值
idx = np.argmax(diff)# 获取array中数值最大的 找差值最大线段的索引
if diff[idx] > threshold:#和阈值比一下,差得多不,超标就滚蛋
slopes.pop(idx)#斜率列表中删掉
lines.pop(idx)#从线段列表中删掉
else:
break
return lines
print('before filter:')
print('left lines number=')
print(len(left_lines))
print('right lines number=')
print(len(right_lines))
reject_abnormal_lines(left_lines, threshold=0.2)
reject_abnormal_lines(right_lines, threshold=0.2)
print('after filter:')
print('left lines number=')
print(len(left_lines))
print('right lines number=')
print(len(right_lines))
7、最小二乘拟合
将所有认为是左或右车道的线拟合为一条
np.ravel :将高维数组拉成一维数组
np.polyfit: 多项式拟合
np.polyval:多项式求值,第一个参数是多项式系数,第二参数是任意X
def least_squares_fit(lines):
"""
将lines中的线段拟合成一条线段
:param lines: 线段集合, [np.array([[x_1, y_1, x_2, y_2]]),np.array([[x_1, y_1, x_2, y_2]]),...,np.array([[x_1, y_1, x_2, y_2]])]
:return: 线段上的两点,np.array([[xmin, ymin], [xmax, ymax]])
"""
# 1. 取出所有坐标点
x_coords = np.ravel([[line[0][0], line[0][2]] for line in lines])
y_coords = np.ravel([[line[0][1], line[0][3]] for line in lines])
# 2. 进行直线拟合.得到多项式系数
poly = np.polyfit(x_coords, y_coords, deg=1)
# 3. 根据多项式系数,计算两个直线上的点,用于唯一确定这条直线
point_min = (np.min(x_coords), np.polyval(poly, np.min(x_coords)))
point_max = (np.max(x_coords), np.polyval(poly, np.max(x_coords)))
return np.array([point_min, point_max], dtype=int)
print("left lane")
print(least_squares_fit(left_lines))
print("right lane")
print(least_squares_fit(right_lines))
在上节代码后加入这段处理
两点练成一条线
8、直线绘制
cv2.line
添加如下代码
left_line = least_squares_fit(left_lines)
right_line = least_squares_fit(right_lines)
img = cv2.imread('img.jpg', cv2.IMREAD_COLOR)
cv2.line(img, tuple(left_line[0]), tuple(left_line[1]), color=(0, 255, 255), thickness=5)
cv2.line(img, tuple(right_line[0]), tuple(right_line[1]), color=(0, 255, 255), thickness=5)
cv2.imshow('lane', img)
cv2.waitKey(0)
为什么左侧车道线肉眼可见的偏移大?
因为在Canny边缘检测阈值调整,和前面ROI mask处掩码区域没有设置好,如下图,左车道线处有车辆的边缘,我没有细心调节阈值,后面掩码区域也没有掩盖掉
将梯形块右移,得到新的图取参与绘图:
所以调整需要看情况
9、视频流读写
capture = cv2.VideoCapture('video.mp4')
while True:
ret, frame = capture.read()#ret视频流状况,是否关闭
frame = show_lane(frame)
cv2.imshow('frame', frame)
cv2.waitKey(100)