AVM环视拼接自标定中车道线检测
VM环视系统自标定算法分为两个部分:1. 车道线检测 2. 相机外参自标定。这篇帖的主要内容是基于hough变换的传统车道线检测方法,包含基础的图像处理算法原理,以及调参的trick和策略。
为了能够针对自己的使用场景合理地进行调参,需要理解canny角点、hough空间这些基础算法的原理,并添加一些策略。笔者在理解hough空间如何应用在车道线检测上花了一些时间,有些是参考源码的理解,在这里记录下来。不准确的地方大家可以一起探讨下。
关键字:车道线检测、hough变换、笛卡尔空间、极坐标、策略
附赠自动驾驶学习资料和量产经验:链接
1.图像预处理
1.1 去畸变和灰度处理
由于相机外参计算、单应变换这些都是基于理想相机模型,因此需要先对图像做去畸变处理。
void RoadCalibrate::dataProcess(const cv::Mat& imgs,
cv::Mat& gray) {
cv::remap(imgs, gray, m_mapx, m_mapy, cv::INTER_LINEAR);
if (gray[0].channels() == 3) {
cv::cvtColor(gray, gray, cv::COLOR_BGR2GRAY);
}
}
1.2 canny算子
1.2.1 算法原理
canny算子概括起来就是用sobel算子计算梯度,然后做极大值抑制,最后用阈值卡掉一些梯度比较弱的角点:
- 高斯滤波(滤除噪点)
这个太简单不说了,目的是滤掉噪点。
- sobel算子计算像素梯度
这一步要计算每一个像素点的梯度方向和梯度强度
- 极大值抑制
如图所示为图像上某相邻的9个像素点,在前面的过程中我们已经算出了每个像素的梯度方向和梯度强度值。对于某一个像素点,极大值抑制就是将它的梯度强度值与周围8个像素点的梯度值进行比较,如果它大于或小于周围每一个像素的梯度强度值,那么认为这是一个边缘角点。否则,认为它的特征不够明显。
例如对于上图中的o点,沿着它的梯度正负方向分别与其相邻点的连线相交于c和d,c点的强度值可通过线性插值的方式分别计算a和b的权重,加权求和得到c点强度值。d点同理。比较O点与c、d梯度强度值的大小,然后进行非极大值抑制。
如果不想搞得太复杂,可以简单理解成是把每个像素与它周围其他像素的梯度强度值大小作比较,如果它是极大值或极小值,就认为它是个突出的边缘角点。
- 阈值处理
高阈值:梯度强度高于高阈值的,一定是边缘点,强边缘
低阈值:梯度强度低于低阈值的,一定不是边缘点
介于高低阈值之间的:弱边缘,等待进一步处理。
- 弱边缘处理
看弱边缘点是否与强边缘连接,如果有连接到强边缘,那么把这个若边缘点也当成强边缘点。
1.2.2 参数调节
@param image 8-bit input image.
@param edges output edge map; single channels 8-bit image, which has the same size as image .
@param threshold1 first threshold for the hysteresis procedure.
@param threshold2 second threshold for the hysteresis procedure.
@param apertureSize aperture size for the Sobel operator.
@param L2gradient a flag, indicating whether a more accurate \f$L_2\f$ norm
\f$=\sqrt{(dI/dx)^2 + (dI/dy)^2}\f$ should be used to calculate the image gradient magnitude (
L2gradient=true ), or whether the default \f$L_1\f$ norm \f$=|dI/dx|+|dI/dy|\f$ is enough (
L2gradient=false ).
*/
CV_EXPORTS_W void Canny( InputArray image, OutputArray edges,
double threshold1, double threshold2,
int apertureSize = 3, bool L2gradient = false );
Canny(img, img_canny, 300, 20, 3, false);
函数入参中高、低阈值对车道线检测结果影响极大,是主要调节的参数:
左侧高阈值为300 右侧高阈值为30
上图中可看出阈值对提取出角点的数量影响较大,相比之下左侧这种车道线明显,且非车道线区域的角点较少的二值图更有利于后续基于Hough变换的车道线检测。
另外,需要注意进入算子的图像质量。例如左侧图像为车机上yuv图像直接转成bgr的jpg图像。右侧是视频采集工具采集到的mp4视频中的图像帧,视频采集工具是将yuv数据通过h264编码成mp4的格式,这个过程有损失,因此可以看到右侧图像明显没有左侧图像清晰。因此右侧图像提取出来的角点会比左侧图像稀疏。
1.2.3 mask预处理
从上面的canny算子提取的边缘图像中可以看到,车道线的轮廓提取的比较清晰,但是在尤其分布于图像的下半部分车道线中间有大量的干扰角点;在图像的上方也有大量的边缘,这些边缘通常是远处的直立建筑。
这些边缘都不利于后续的车道线检测。因此在这里可以添加一步mask的预处理,超参crop_t和crop_b与相机的俯仰角相关,主要看你感兴趣的区域在哪里,可根据自己的实验调整合适的crop范围。
比如当摄像头俯仰角比较小的时候,图像的上半区域大部分都是远处的建筑,此时可以将上半部分全都用mask罩住;当俯仰角比较大的时候,地面的物体清晰度较高,会引入很多非车道线的角点,这时候就可以把图像的上下1/4罩住。
// mask 1/4
void RoadCalibrate::maskCal() {
m_mask = cv::Mat::zeros(m_fish_height, m_fish_width, CV_8U);
for (int i = m_fish_height * crop_t; i < m_fish_height * crop_b; i++) {
for (int j = 0; j < m_fish_width; j++) {
m_mask.at<uchar>(i, j) = 1;
}
}
}
2. 车道线检测
2.1 原理
2.1.1 笛卡尔坐标系与hough空间
笛卡尔坐标系是一种常见的图像坐标系,它与hough空间的转换如下:
笛卡尔坐标系与hough空间
笛卡尔坐标系与hough空间
如图所示,hough空间的A,B直线交于点h。
进一步地,笛卡尔坐标系中AB直线上还有很多C,D,E,F等等无数个点,它们在hough空间上全都交于点h。
笛卡尔坐标系与hough空间
因此我们得到一个结论:hough空间上的如果有多条直线交于h点,那么说明对应在笛卡尔空间上的这个方向上有许多角点。
canny角点图
在这个canny角点图中(图像空间是笛卡尔坐标系),车道线的角点连成一条线。假设这条线上有100个角点,把这些角点转换到hough空间得到100条直线,它们全部交于同一点h(参考上面的笛卡尔坐标系与hough空间的示意图)。
基于hough变换的车道线检测算法流程如下:
算法步骤 | 具体实现 |
---|---|
hough空间转换 | 将canny算子提取的角点图像进行hough变换,全部转换到hough空间。即对每一个角点进行霍夫变换,图像坐标系上的每一个角点在hough空间都对应一条直线。 |
累加 | 对hough空间每条直线经过的位置做++1累加 |
阈值筛选(hough空间) | 如果hough空间的某个位置累加的次数>thresh,说明对应在笛卡尔坐标系上的该方向上有>thresh个角点,那么这个方向上可能存在直线。 |
阈值筛选(图像空间) | 选择连续的直线 |
hough空间的阈值筛选需要注意:它只能限制在图像(笛卡尔)中某个方向上确实有>thresh个canny角点,但是这些角点是否连续,还需要其他方法来限制。
例如下图中,上述hough空间阈值可能会把图中标出方向上判定成存在一条直线,因为在这个方向上确实存在很多的canny角点。不过这显然不是一条连续的车道线。因此还需要在笛卡尔坐标系上用mask方法进行筛选。
图像空间的阈值筛选:
- maxLineGap
前面已经根据hough空间的阈值筛选了可能存在直线的方向,现在图像空间的该方向上通过maxLineGap筛选连续的角点,当该方向上相邻两canny角点之间的距离<maxLineGap时,认为这两个角点是连续的,如果>maxLineGap则认为它们不连续。
- minLineLength
当图像空间上某方向上存在>minLineLength个连续角点时,认为这是一个条直线。
2.1.2 极坐标与hough空间
然而,由于笛卡尔坐标系中当直线垂直于x轴时斜率为无穷大,这给计算带来不便,因此可以使用极坐标代替。极坐标系中过点A的直线可以用如下公式表示:
它对应的hough空间如下,极坐标系过点A的无数条直线在hough空间连接成一条类似三角函数的曲线。
与笛卡尔坐标-霍夫空间的结论类似:hough空间上的如果有多条直线交于h点,那么说明对应在笛卡尔空间上的这个方向上有许多角点,如下图:
于是算法实现流程如下:
算法步骤 | 具体实现 |
---|---|
hough空间转换 | 将canny算子提取的角点图像进行hough变换,全部转换到hough空间。即对每一个角点进行霍夫变换,图像坐标系上的每一个角点在hough空间都对应一条三角函数曲线。 |
累加 | 对hough空间每条曲线经过的位置做++1累加 |
阈值筛选(hough空间) | 如果hough空间的某个位置累加的次数>thresh,说明其对应在笛卡尔坐标系上的该方向上有>thresh个角点,那么这个方向上可能存在直线。 |
阈值筛选(图像空间) | 选择连续的直线 |
2.2 调参
CV_EXPORTS_W void HoughLinesP( InputArray image, OutputArray lines,
double rho, double theta, int threshold,
double minLineLength = 0, double maxLineGap = 0 );
HoughLinesP(img_canny, lines1, 1, 3.14 / 180, 200, 150, 5);
- theta
在对极坐标系上的某canny边缘点A进行霍夫变换时,要取到很多经过A点的直线然后将其映射到hough空间上,映射到空间上的这些点可以构成一个类似三角函数的曲线(右图),那么左侧的这些直线如何取,采样粒度是怎样的?theta=1表示的就是每旋转1°,选取一条直线,将其映射到hough空间上。映射到hough空间后,这些点的位置++1累计。超参theta一般都设成1。
- threshold(hough空间)
当极坐标系(图像上)全部的canny边缘点都转到hough空间后,如果hough空间上某点的累计次数很多,说明这个hough空间上的点对应极坐标系的该方向上存在很多的canny边缘点,当累计的次数大于这个threshold阈值,那么说明该方向上可能存在一条直线。
- maxLineGap(图像空间)
图中的两条线段在图像坐标系上在同一条直线上,它们会在hough空间的同一个位置做累加,但它们在图像坐标系上不是同一条车道线。maxLineGap的意义就是设置一个最小的gap,如果line1和line2之间的距离小于这个gap,则认为它们是同一条直线。代码实现就是只返回一个Vec4f类型的数据,里面包含的line1的头和line2的尾。如果line1和line2之间的距离大于这个gap,则返回两个Vec4f类型的数据,分别包含line1和lin2的首尾坐标。line1和line2之间计算距离的实现利用了mask,就是这个canny边缘图,它是一个0,1的二值化图。
- minLineLength
使用threshhold和maxLineGap选出了直线之后,最后使用minLineLength筛掉比较短的直线。
3. 策略
3.1 检测车道线的斜率限制
检测到的直线有些横七竖八的,需要用斜率来进行筛选。
斜率控制
std::vector<cv::Vec4f> lines1;
HoughLinesP(img_canny, lines1, 1, 3.14 / 180, 100, 150, 10);
for (unsigned j = 0; j < lines1.size(); j++) {
cv::Vec4f hline = lines1[j];
float k = fabs((hline[3] - hline[1]) / (hline[2] - hline[0]));
if (k > 5) {
continue;
}
if (k < 0.2) {
continue;
}
cv::Vec4i lines_int{ lines1[j][0], lines1[j][1], lines1[j][2], (lines1[j][3] };
detect_line.push_back(lines_int);
}
3.2 车道线分布限制
车道线检测的目的是计算消失点,用到的方法如图:
消失点计算原理
如果检测到的车道线全都分布在图像中的某一侧,这些车道线斜率相近,缺少另外一侧直线方程的约束,直觉上就是有问题的。实际上相当于你只拟合了局部的数据,并没有全局拟合。
车道线分布于图像的同一侧
因此需要做个判断:是否在图像的左右两个区域都存在检测到的车道线。
//line strategy(for better vanish pts)
bool line_flag_l = false;
bool line_flag_r = false;
for (size_t i = 0; i < detect_line.size();i++) {
int center_x = (detect_line[i][0] + detect_line[i][2]) / 2;
if (center_x < (m_mapx.cols/ 2)) {
line_flag_l = true;
} else {
line_flag_r = true;
}
}
if (line_flag_l && line_flag_r) {
//go on
} else {
return CalibrationResultType::kLineDetectFailed;
}
3.3 消失点误差
在自标定算法中,我们要求车身与车道线平行,这样消失点的Z方向坐标才能远大于X和Y,算法的公式是基于这样的假设进行推导的。
在这种情况下,图像上消失点的x方向坐标,应该在width/2的位置上。因此,如果消失点计算的结果与图像x方向的中心位置偏差较多,说明车没有平行于车道线行驶,或者一切其他的原因。总之,要排除掉这种情况。
int vanish_error = static_cast<int>(abs(pts.x - (m_mapx.cols / 2)));
if (vanish_error > m_vanish_error_thresh) {
return CalibrationResultType::kLineDetectFailed;
}
3.4 BEV直线斜率
我们可以通过外参计算出图像投影到鸟瞰图的单应矩阵H,因此可以将去畸变图上的车道线投影到鸟瞰图上。当鸟瞰图上车道线几乎垂直于图像x轴的时候,我们认为外参标定的比较准确。
for (size_t i = 0; i < detect_line.size(); i++) {
cv::Mat pts1 =
(cv::Mat_<float>(3, 1) << detect_line[i][0], detect_line[i][1], 1);
cv::Mat pts2 =
(cv::Mat_<float>(3, 1) << detect_line[i][2], detect_line[i][3], 1);
cv::Mat pts1_bev = Homo * pts1;
pts1_bev = pts1_bev / pts1_bev.at<float>(2, 0);
cv::Mat pts2_bev = Homo * pts2;
pts2_bev = pts2_bev / pts2_bev.at<float>(2, 0);
float slope = fabs((pts2_bev.at<float>(0, 0) - pts1_bev.at<float>(0, 0)) /
(pts2_bev.at<float>(1, 0) - pts1_bev.at<float>(1, 0)));
//std::cout << "slope:" << slope << std::endl;
if (slope > 0.03) {
return CalibrationResultType::kLineDetectFailed;
}
frame_slope_average += slope;
}
3.5 标定成功条件
-
迭代次数
-
前后两次差异太小退出
总结
本篇记录了基于Hough车道线检测的算法原理,包含canny算子、hough空间转换、车道线检测原理,以及在此基础上给检测算法添加的策略。
这两篇关于avm环视系统相机自标定的内容可以初步计算出前后相机的外参,如果想要做的更好可以将其当作初始解,使用类似BA的方法进行非线性优化。以及在工厂标定中,也可以在标定结束后加一层BA优化。关于非线性优化的内容后续会慢慢更新。