本节分析一个国人开发的简单的车道检测程序(不涉及跟踪)
概述
- 采用opencv2编写 C++
- 算法核心步骤
- 提取车道标记特征,封装在laneExtraction类中
- 车道建模(两条边,单车道),封装在laneModeling类中
- 对于断断续续的斑点车道标记(虚线)使用RANSAC匹配直线,对每幅图像,检测结果可能是感兴趣的左右车道都检测到或者全都没检测到
主程序框架
track.cpp
主程序依次读入源文件夹下的图片,进行处理后输出到目标文件夹。由于图片上有固定的遮挡物,所以添加了mask进行去除。
构建laneExtraction类的实例,设置参数路宽,添加mask去除固定遮挡物的干扰,然后调用getLaneCandidates得到备选车道线。
/*
* extract line segments
*/
aps::laneExtraction le;
le.setLaneWidth(15);
le.compute(img_org, carmask / 255);
std::vector<aps::line> laneSegCandidates = le.getLaneCandidates();
构建laneModeling类的实例,对备选车道线参数化后,验证左右车道是否检测到,如果检测到则标记线段。
/*
* model the lane
*/
aps::laneModeling lm;
lm.compute(laneSegCandidates);
std::vector<aps::line> lanes = lm.getLanes();
cv::cvtColor(img_org, img_org, CV_GRAY2BGR);
ofs << fname << ",";
if (lm.verifyLanes())
{
cv::line(img_org, lanes[0].getEndPoint1(), lanes[0].getEndPoint2(),
cv::Scalar(0, 255, 255), 5, CV_AA);
cv::line(img_org, lanes[1].getEndPoint1(), lanes[1].getEndPoint2(),
cv::Scalar(0, 255, 255), 5, CV_AA);
int x_left =
(lanes[0].getEndPoint2().x < lanes[1].getEndPoint2().x) ?
lanes[0].getEndPoint2().x :
lanes[1].getEndPoint2().x;
int x_right =
(lanes[0].getEndPoint2().x > lanes[1].getEndPoint2().x) ?
lanes[0].getEndPoint2().x :
lanes[1].getEndPoint2().x;
ofs << x_left << "," << x_right << std::endl;
}
else
{
ofs << "NONE,NONE\n";
}
车道标记特征提取
laneExtraction.cpp
先看类的内部方法检测车道标记,输出结果是一个特征图:关键就是一个源图像点到特征图点的计算公式。
void
laneExtraction::detectMarkers(const cv::Mat img, int tau)
{
m_IMMarker.create(img.rows, img.cols, CV_8UC1);
m_IMMarker.setTo(0);
int aux = 0;
for (int j = 0; j < img.rows; j++)
{
for (int i = tau; i < img.cols - tau; i++)
{
if (img.at<uchar>(j, i) != 0)
{
aux = 2 * img.at<uchar>(j, i);
aux += -img.at<uchar>(j, i - tau);
aux += -img.at<uchar>(j, i + tau);
aux += -abs(
(int) (img.at<uchar>(j, i - tau)
- img.at<uchar>(j, i + tau)));
aux = (aux < 0) ? (0) : (aux);
aux = (aux > 255) ? (255) : (aux);
m_IMMarker.at<uchar>(j, i) = (unsigned char) aux;
}
}
}
return;
}
得到特征图后,去掉mask固定遮挡物部分,然后通过固定阈值进行二值化,得到二值图,再通过概率hough变换检测直线,最后遍历检测到的直线,如果检测到的直线偏角在某个固定范围内,就算作备选车道线。
void
laneExtraction::compute(const cv::Mat img, const cv::Mat mask)
{
detectMarkers(img, m_laneWidth);
m_IMMarker = m_IMMarker.mul(mask);
cv::threshold(m_IMMarker, m_IMMarker, 25, 255, CV_THRESH_BINARY);
std::vector<cv::Vec4i> lines;
cv::HoughLinesP(m_IMMarker, lines, 1, CV_PI / 180, 40, 20, 10);
for (size_t i = 0; i < lines.size(); i++)
{
cv::Vec4i l = lines[i];
aps::line seg;
seg.set(cv::Point(l[0], l[1]), cv::Point(l[2], l[3]));
if ((seg.getOrientation() > 10 && seg.getOrientation() < 80)
|| (seg.getOrientation() > 100 && seg.getOrientation() < 170))
{
m_laneSegCandid.push_back(seg);
}
}
return;
}
车道建模
下面看建模部分:laneModeling类
如果检测到的直线不够2条,则返回失败。对检测到的直线通过kmeans分类方法分成两类,分类依据是方向信息。
lines–>lanes
收集各个类别的直线上的点并用RANSAC拟合成直线,得到最终的车道线,如果得到的lanes少于2条,则失败。
void
laneModeling::compute(const std::vector<aps::line>& lines)
{
int clusterCount = 2;
if (lines.empty() || lines.size() < clusterCount)
{
m_success = false;
return;
}
cv::Mat points(lines.size(), 1, CV_32FC1);
cv::Mat labels;
cv::Mat centers(clusterCount, 1, CV_32FC1);
for (size_t i = 0; i < lines.size(); i++)
{
points.at<float>(i, 0) = lines[i].getOrientation();
}
/*
* put all line candidates into two clusters based on their orientations
*/
cv::kmeans(points, clusterCount, labels,
cv::TermCriteria(CV_TERMCRIT_EPS + CV_TERMCRIT_ITER, 10, 1.0),
3, cv::KMEANS_PP_CENTERS, centers);
for (int k = 0; k < clusterCount; k++)
{
std::vector<cv::Point> candid_points;
for (size_t i = 0; i < lines.size(); i++) // 遍历所有备选直线
{
if (labels.at<int>(i, 0) == k) // 如果该直线属于当前的类别K
{
std::vector<cv::Point> pts = lines[i].interpolatePoints(10);
candid_points.insert(candid_points.end(), pts.begin(),
pts.end()); // 将直线上的点收集起来,用于RANSAC
}
}
if (candid_points.empty())
continue; // 如果没有收集到,则继续看下一类
/*
* fit the perfect line
*/
aps::LineModel model;
aps::RansacLine2D Ransac;
Ransac.setObservationSet(candid_points);
Ransac.setIterations(500);
Ransac.setRequiredInliers(2);
Ransac.setTreshold(1);
Ransac.computeModel();
Ransac.getBestModel(model);
double m = model.mSlope, b = model.mIntercept;
if (fabs(m) <= 1e-10 && fabs(b) <= 1e-10)
continue;
cv::Point p1((700 - b) / (m + 1e-5), 700);
cv::Point p2((1200 - b) / (m + 1e-5), 1200);
aps::line laneSide;
laneSide.set(p1, p2);
lanes.push_back(laneSide);
}
if (lanes.size() < 2)
{
m_success = false;
}
else
{
m_success = true;
}
return;
}
对车道最后的验证步骤比较简单,如果得到的两条车道线方向近似,则失败;如果相交,则失败。laneModeling::verifyLanes()部分代码略。
线段操作的封装
封装在line类里
- set 根据两个点的坐标指定一条线段
- is_Set 返回线段是否已经通过set设定
- getOrientation 返回线段方向
- getEndPoint1,getEndPoint2返回两个端点
- getLength 返回线段长度
- is_Headup 端点1的纵坐标比端点2的纵坐标小?
- getDistFromLine 线段和线段的距离
- point2Line 点到线段的距离
- interpolatePoints 返回线段上的点
抛物线掩码
parabolicMask用作去除图像上不必要的噪声,包括固定遮挡物和抛物线划分的ROI
/*
* create the mask that can help remove unwanted noise
*/
cv::Mat carmask = cv::imread("carmask.png", CV_LOAD_IMAGE_GRAYSCALE);
aps::parabolicMask pmask(carmask.cols, carmask.rows,
1.0 / carmask.rows);
cv::Mat M = pmask.mkMask();
M.convertTo(M, CV_8UC1);
carmask.convertTo(carmask, CV_8UC1);
carmask = carmask.mul(M);
RANSAC拟合直线
封装在LineModel类里面,
关于RANSAC的讲解见 随机抽样一致性算法(RANSAC)
RANSAC是“RANdom SAmple Consensus(随机抽样一致)”的缩写。它可以从一组包含“局外点”的观测数据集中,通过迭代方式估计数学模型的参数。它是一种不确定的算法——它有一定的概率得出一个合理的结果;为了提高概率必须提高迭代次数。该算法最早由Fischler和Bolles于1981年提出。
这里不再展开。
总结
该程序的结构较好,对象设计合理,提供了一个适合扩展的车道检测框架。