本節分析一個國人開發的簡單的車道檢測程式(不涉及跟蹤)
- simple_lane_tracking 源碼位址
- 作者首頁
概述
- 采用opencv2編寫 C++
- 算法核心步驟
- 提取車道标記特征,封裝在laneExtraction類中
- 車道模組化(兩條邊,單車道),封裝在laneModeling類中
- 對于斷斷續續的斑點車道标記(虛線)使用RANSAC比對直線,對每幅圖像,檢測結果可能是感興趣的左右車道都檢測到或者全都沒檢測到
主程式架構
track.cpp
主程式依次讀入源檔案夾下的圖檔,進行處理後輸出到目标檔案夾。由于圖檔上有固定的遮擋物,是以添加了mask進行去除。
建構laneExtraction類的執行個體,設定參數路寬,添加mask去除固定遮擋物的幹擾,然後調用getLaneCandidates得到備選車道線。
/*
* extract line segments
*/
aps::laneExtraction le;
le.setLaneWidth();
le.compute(img_org, carmask / );
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[].getEndPoint1(), lanes[].getEndPoint2(),
cv::Scalar(, , ), , CV_AA);
cv::line(img_org, lanes[].getEndPoint1(), lanes[].getEndPoint2(),
cv::Scalar(, , ), , CV_AA);
int x_left =
(lanes[].getEndPoint2().x < lanes[].getEndPoint2().x) ?
lanes[].getEndPoint2().x :
lanes[].getEndPoint2().x;
int x_right =
(lanes[].getEndPoint2().x > lanes[].getEndPoint2().x) ?
lanes[].getEndPoint2().x :
lanes[].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();
int aux = ;
for (int j = ; j < img.rows; j++)
{
for (int i = tau; i < img.cols - tau; i++)
{
if (img.at<uchar>(j, i) != )
{
aux = * 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 < ) ? () : (aux);
aux = (aux > ) ? () : (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, , , CV_THRESH_BINARY);
std::vector<cv::Vec4i> lines;
cv::HoughLinesP(m_IMMarker, lines, , CV_PI / , , , );
for (size_t i = ; i < lines.size(); i++)
{
cv::Vec4i l = lines[i];
aps::line seg;
seg.set(cv::Point(l[], l[]), cv::Point(l[], l[]));
if ((seg.getOrientation() > && seg.getOrientation() < )
|| (seg.getOrientation() > && seg.getOrientation() < ))
{
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 = ;
if (lines.empty() || lines.size() < clusterCount)
{
m_success = false;
return;
}
cv::Mat points(lines.size(), , CV_32FC1);
cv::Mat labels;
cv::Mat centers(clusterCount, , CV_32FC1);
for (size_t i = ; i < lines.size(); i++)
{
points.at<float>(i, ) = 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, , ),
, cv::KMEANS_PP_CENTERS, centers);
for (int k = ; k < clusterCount; k++)
{
std::vector<cv::Point> candid_points;
for (size_t i = ; i < lines.size(); i++) // 周遊所有備選直線
{
if (labels.at<int>(i, ) == k) // 如果該直線屬于目前的類别K
{
std::vector<cv::Point> pts = lines[i].interpolatePoints();
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();
Ransac.setRequiredInliers();
Ransac.setTreshold();
Ransac.computeModel();
Ransac.getBestModel(model);
double m = model.mSlope, b = model.mIntercept;
if (fabs(m) <= && fabs(b) <= )
continue;
cv::Point p1(( - b) / (m + ), );
cv::Point p2(( - b) / (m + ), );
aps::line laneSide;
laneSide.set(p1, p2);
lanes.push_back(laneSide);
}
if (lanes.size() < )
{
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,
/ 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年提出。
這裡不再展開。
總結
該程式的結構較好,對象設計合理,提供了一個适合擴充的車道檢測架構。