車道檢測源碼分析系列(二)

本節分析一個國人開發的簡單的車道檢測程序(不涉及跟蹤)

概述

  • 採用opencv2編寫 C++
  • 算法核心步驟
    1. 提取車道標記特徵,封裝在laneExtraction類中
    2. 車道建模(兩條邊,單車道),封裝在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年提出。

這裏不再展開。

總結

該程序的結構較好,對象設計合理,提供了一個適合擴展的車道檢測框架。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章