车道线检测
传统车道线检测的具体步骤为:
- 校准镜头
- 去除图像的畸变
- 使用颜色和梯度阈值
- 使用透视变换
- 辨别哪些像素属于车道线
- 确定车道线的形状和位置
1. 阈值
可以尝试不同的颜色和梯度的阈值组合来生成一个车道线清晰可见的二进制图像,在这里不会展开讨论,但是图像应该差不多像这样:
![](https://i-blog.csdnimg.cn/blog_migrate/bdc6ee6d18653da9de4a3ceec68a69c0.jpeg)
![](https://i-blog.csdnimg.cn/blog_migrate/7b1a4b81fc6baf07b6fd9c3ab0a4e4b5.jpeg)
2. 透视变换
需要识别四个源点来做透视变换。在这种情况下,可以假设道路是一个平面。这不是严格正确的,但是它可以在项目中,被看成是近似值。可以在一个梯形形状(类似于区域蒙版)中选择四个点,当从上面向下看道路时,它将表示为一个矩形:
使用上面所使用的相同四个源点,可以得到弯曲的线:
3. 辨别车道线
在应用了上述步骤后,应该得到一个车道线清晰突出的二值图像。但是,仍需要明确地决定哪些像素是线的一部分,哪些像素属于左行,哪些属于右行。
3.1 直方图寻找车道线
3.1.1 画直方图
histogram = np.sum(img[img.shape[0]//2:,:], axis=0)
将图像中每一列的像素值相加,就这样,上面的二进制图像所得到的结果如下:
3.1.2 滑动窗口
上面直方图中,最突出的两个峰值是车道线基底
x
x
x 位置的良好指示器,可以用它作为起点来搜索整个车道线。我们可以使用一个滑动窗口,放置在线的中心,找到并跟随线到框架的顶部。
3.1.2.1 直方图分离两条线
第一步要做的就是将直方图分成两边,每一边是一个车道线:
midpoint = np.int(histogram.shape[0]//2)
leftx_base = np.argmax(histogram[:midpoint])
rightx_base = np.argmax(histogram[midpoint:]) + midpoint
3.1.2.2 通过窗口迭代跟踪曲率
- 循环遍历每个窗口 nwindows
- 找到当前窗口边界----基于窗口的左当前点 leftx_current 和右当前点 rightx_current,还有 margin----为窗口长度的一半
- 已知窗口边界,寻找哪些点落入窗口内
- 如果在上一步中找到的像素总数大于一个阈值 minpix,基于这些像素点的中心位置重新确定窗口中心
# 步骤一:Step through the windows one by one
for window in range(nwindows):
# 步骤二:Identify window boundaries in x and y (and right and left)
win_y_low = binary_warped.shape[0] - (window+1)*window_height
win_y_high = binary_warped.shape[0] - window*window_height
win_xleft_low = leftx_current - margin
win_xleft_high = leftx_current + margin
win_xright_low = rightx_current - margin
win_xright_high = rightx_current + margin
# 步骤三:Identify the nonzero pixels in x and y within the window #
good_left_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xleft_low) & (nonzerox < win_xleft_high)).nonzero()[0]
good_right_inds = ((nonzeroy >= win_y_low) & (nonzeroy < win_y_high) &
(nonzerox >= win_xright_low) & (nonzerox < win_xright_high)).nonzero()[0]
# Append these indices to the lists
left_lane_inds.append(good_left_inds)
right_lane_inds.append(good_right_inds)
# 步骤四:If you found > minpix pixels, recenter next window on their mean position
if len(good_left_inds) > minpix:
leftx_current = np.int(np.mean(nonzerox[good_left_inds]))
if len(good_right_inds) > minpix:
rightx_current = np.int(np.mean(nonzerox[good_right_inds]))
3.1.2.3 拟合多项式
现在我们已经通过滑动窗口方法找到了属于每条线的所有像素,现在是时候为这条线拟合一个多项式了。首先,我们有几个小步骤来准备我们的像素。
# Concatenate the arrays of indices (previously was a list of lists of pixels)
left_lane_inds = np.concatenate(left_lane_inds)
right_lane_inds = np.concatenate(right_lane_inds)
# Extract left and right line pixel positions
leftx = nonzerox[left_lane_inds]
lefty = nonzeroy[left_lane_inds]
rightx = nonzerox[right_lane_inds]
righty = nonzeroy[right_lane_inds]
拟合得二次函数:
# Fit a second order polynomial to each using `np.polyfit`
left_fit = np.polyfit(lefty, leftx, 2)
right_fit = np.polyfit(righty, rightx, 2)
# Generate x and y values for plotting
ploty = np.linspace(0, binary_warped.shape[0]-1, binary_warped.shape[0] )
try:
left_fitx = left_fit[0]*ploty**2 + left_fit[1]*ploty + left_fit[2]
right_fitx = right_fit[0]*ploty**2 + right_fit[1]*ploty + right_fit[2]
在这里拟合得多项式: f ( y ) = A y 2 + B y + C f(y)=Ay^2+By+C f(y)=Ay2+By+C,在这里拟合 f ( y ) f(y) f(y) 而不是 f ( x ) f(x) f(x),因为弯曲图像中车道线接近垂直,这会让多个 y y y 值有相同的 x x x 值。
3.1.2.4 跳过之后的滑动窗口步骤
上面已经构建了一个使用滑动窗口跟踪车道线到远处的算法。然而,使用之前的完成算法在每一帧上重新开始似乎效率不太高,因为车道线在帧与帧之间不一定会移动很多。
在下一帧视频中,不需要再进行盲搜索,只需要在之前的车道线位置周围的边距(margin)进行搜索,就像下图一样,绿色阴影区域显示了我们这次搜索车道线的地方。所以,一旦知道了这些车道线在视频一帧中的位置,就可以在在下一帧中对他们进行高度有针对性的搜索:
这相当于为每一帧视频使用定制的 ROI,可以帮助在尖锐的曲线和棘手的条件下跟踪车道线。如果丢失了这些车道线,请返回滑动窗口搜索或使用其他方法重新发现它们。
3.2 卷积寻找车道线
另一种滑动窗口法是应用卷积,会找到每个窗口中白像素的个数最大值的位置。卷积是两个独立信号的乘积的总和,在我们的例子中是窗口模板和像素图像的垂直切片。
窗口模板从左到右滑动,任何重叠的值都将相加,从而创建卷积信号。卷积信号的峰值是像素重叠最大的地方,也是车道标记最可能的位置。
4. 计算曲率
在获取了像素位置的多项式后,可以计算曲率的半径。
4.1 曲率半径(Theory)
我们可以画一个圆,它与曲线局部部分上的邻近点非常吻合,曲线和圆紧密相连,因为两条曲线在相交点有相同的切线和曲率。
曲线在某一点上的曲率半径被定义为近似圆的半径。这个半径随着我们沿着曲线移动而变化。
当曲线为 y = f ( x ) y=f(x) y=f(x) 时,在任意点 x x x 的曲率半径公式为:
R c u r v e = [ 1 + ( d y d x ) 2 ] 3 / 2 [ d 2 y d x 2 ] R_{curve} =\frac{[1+(\frac{dy}{dx})^2]^{3/2}}{[\frac{d^2y}{dx^2}]} Rcurve=[dx2d2y][1+(dxdy)2]3/2
例,求
y
=
2
x
3
−
x
+
3
y = 2x^3 − x + 3
y=2x3−x+3 在点
x
=
1
x=1
x=1 上的曲率半径:
求得
d
y
d
x
=
6
x
2
−
1
(
d
y
d
x
)
2
=
(
6
x
2
−
1
)
2
=
36
x
4
−
12
x
2
+
1
d
2
y
d
x
2
=
12
x
\frac{dy}{dx}=6x^2-1\\(\frac{dy}{dx})^2=(6x^2-1)^2=36x^4-12x^2+1\\\frac{d^2y}{dx^2}=12x
dxdy=6x2−1(dxdy)2=(6x2−1)2=36x4−12x2+1dx2d2y=12x
代入公式,得:
R
=
[
36
x
4
−
12
x
2
+
2
]
3
/
2
[
12
x
]
R=\frac{[36x^4-12x^2+2]^{3/2}}{[12x]}
R=[12x][36x4−12x2+2]3/2
将所需点
x
=
1
x=1
x=1 代入公式,得
R
=
11.04787562
R = 11.04787562
R=11.04787562,如图所示,此时的圆心为
(
−
9.8
,
6.17
)
(−9.8,6.17)
(−9.8,6.17):
4.2 Radius of Curvature
在车道线中函数为: x = f ( y ) x=f(y) x=f(y),此时的曲率半径为:
R c u r v e = [ 1 + ( d x d y ) 2 ] 3 / 2 [ d 2 x d y 2 ] R_{curve} =\frac{[1+(\frac{dx}{dy})^2]^{3/2}}{[\frac{d^2x}{dy^2}]} Rcurve=[dy2d2x][1+(dydx)2]3/2
已知二项式:
f
(
y
)
=
A
y
2
+
B
y
+
C
f(y)=Ay^2+By+C
f(y)=Ay2+By+C, 一阶导数和二阶导数分别为:
f
′
(
y
)
=
d
x
d
y
=
2
A
y
+
B
f
′
′
(
y
)
=
d
2
x
d
y
2
=
2
A
f'(y)=\frac{dx}{dy}=2Ay+B \\ f''(y)=\frac{d^2x}{dy^2}=2A
f′(y)=dydx=2Ay+Bf′′(y)=dy2d2x=2A
此时方程的曲率半径变成了:
R c u r v e = ( 1 + ( 2 A y + B ) 2 ) 3 / 2 ∣ 2 A ∣ R_{curve} =\frac{(1+(2Ay+B)^2)^{3/2}}{|2A|} Rcurve=∣2A∣(1+(2Ay+B)2)3/2
# Define y-value where we want radius of curvature
# We'll choose the maximum y-value, corresponding to the bottom of the image
y_eval = np.max(ploty)
# Calculation of R_curve (radius of curvature)
left_curverad = ((1 + (2*left_fit[0]*y_eval + left_fit[1])**2)**1.5) / np.absolute(2*left_fit[0])
right_curverad = ((1 + (2*right_fit[0]*y_eval + right_fit[1])**2)**1.5) / np.absolute(2*right_fit[0])
4.3 从像素坐标系转换到世界坐标系
现在计算的曲率半径是基于像素值的,而不是真实世界空间的,所以我们应该将上述计算的 x x x 和 y y y 值转变到真实世界空间中。
这涉及到透视变换后,车道截面的长和宽,可以通过现实测量物理车道线等方式来获得。
在该例程中,假设车道线长 30 m 30m 30m,宽 3.7 m 3.7m 3.7m,多项式的拟合以及曲率半径计算方式变为了:
# Define conversions in x and y from pixels space to meters
ym_per_pix = 30/720 # meters per pixel in y dimension
xm_per_pix = 3.7/700 # meters per pixel in x dimension
# Fit a second order polynomial to pixel positions in each fake lane line
# Fit new polynomials to x,y in world space
left_fit_cr = np.polyfit(ploty*ym_per_pix, leftx*xm_per_pix, 2)
right_fit_cr = np.polyfit(ploty*ym_per_pix, rightx*xm_per_pix, 2)
# Calculation of R_curve (radius of curvature)
left_curverad = ((1 + (2*left_fit_cr[0]*y_eval*ym_per_pix + left_fit_cr[1])**2)**1.5) / np.absolute(2*left_fit_cr[0])
right_curverad = ((1 + (2*right_fit_cr[0]*y_eval*ym_per_pix + right_fit_cr[1])**2)**1.5) / np.absolute(2*right_fit_cr[0])