车道检测源码分析系列(二)

本节分析一个国人开发的简单的车道检测程序(不涉及跟踪)

概述

  • 采用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年提出。

这里不再展开。

总结

该程序的结构较好,对象设计合理,提供了一个适合扩展的车道检测框架。

时间: 2024-08-09 14:37:42

车道检测源码分析系列(二)的相关文章

车道检测源码分析系列(三)

下面看一个出自国外某教授的基于OpenCVSharp的车辆视觉框架,虽然包含车道检测和车辆识别,但源程序仅100行左右,非常精巧. 算法描述 检测部分的步骤很经典,是比较通用的步骤: 将图像上半部分无关内容截掉,设定ROI 转为灰度图 Canny算子提取边缘 Hough检测直线 通过直线角度位置信息筛选出车道线,标记到图上 效果如下图: 源程序 有博客将其中车道检测部分转为了OpenCV2,以下的源码转自该博客.原文链接 另外,国外有基于此程序进行扩展的,后续会介绍. OpenCVSharp版本

车道检测源码分析系列(一)

车道线检测是辅助驾驶和自动驾驶的重要研究内容,相关的算法研究已经延续了20多年,本系列文章旨在帮助初学者在阅读文献之余快速上手构建自己的项目.OpenCV极大地方便了机器视觉应用的编写,为了能在刚起步时就看得远些,我们需要站在OpenCV巨人的肩膀上. 先看一个比较简单的程序,Github地址 框架 class LaneDetect封装核心算法 void makeFromVid(string path) 根据视频文件目录读入,对视频中的图像帧进行处理 主函数 调用makeFromVid 核心算法

Cordova Android源码分析系列二(CordovaWebView相关类分析)

本篇文章是Cordova Android源码分析系列文章的第二篇,主要分析CordovaWebView和CordovaWebViewClient类,通过分析代码可以知道Web网页加载的过程,错误出来,多线程处理等. CordovaWebView类分析 CordovaWebView类继承了Android WebView类,这是一个很自然的实现,共1000多行代码.包含了PluginManager pluginManager,BroadcastReceiver receiver,CordovaInt

Redis java客户端 jedis 源码分析系列二:单实例 jedis

在使用Jedis的过程中最简单的用法就是单实例单连接的jedis,如下代码所示: public void testJedis(){ Jedis jedis = new Jedis("127.0.0.1"); jedis.set("key", "value"); jedis.get("key"); jedis.close(); } 让我们深入到内部去看一看其结构,如下图所示: 此处请先忽略 JedisPool 类和 Pool&l

Cordova Android源码分析系列一(项目总览和CordovaActivity分析)

PhoneGap/Cordova是一个专业的移动应用开发框架,是一个全面的WEB APP开发的框架,提供了以WEB形式来访问终端设备的API的功能.这对于采用WEB APP进行开发者来说是个福音,这可以避免了原生开发的某些功能.Cordova 只是个原生外壳,app的内核是一个完整的webapp,需要调用的原生功能将以原生插件的形式实现,以暴露js接口的方式调用. Cordova Android项目是Cordova Android原生部分的Java代码实现,提供了Android原生代码和上层We

jQuery源码分析系列(36) : Ajax - 类型转化器

什么是类型转化器? jQuery支持不同格式的数据返回形式,比如dataType为 xml, json,jsonp,script, or html 但是浏览器的XMLHttpRequest对象对数据的响应只有 responseText与responseXML 二种 所以现在我要定义dataType为jsonp,那么所得的最终数据是一个json的键值对,所以jQuery内部就会默认帮你完成这个转化工作 jQuery为了处理这种执行后数据的转化,就引入了类型转化器,如果没有指定类型就依据响应头Con

jquery2源码分析系列目录

学习jquery的源码对于提高前端的能力很有帮助,下面的系列是我在网上看到的对jquery2的源码的分析.等有时间了好好研究下.我们知道jquery2开始就不支持IE6-8了,从jquery2的源码中可以学到很多w3c新的标准( 如html5,css3,ECMAScript).原文地址是:http://www.cnblogs.com/aaronjs/p/3279314.html 关于1.x.x版的jquery源码分析系列,本博客也转载了一个地址http://www.cnblogs.com/jav

jQuery-1.9.1源码分析系列完毕目录整理

jQuery 1.9.1源码分析已经完毕.目录如下 jQuery-1.9.1源码分析系列(一)整体架构 jQuery-1.9.1源码分析系列(一)整体架构续 jQuery-1.9.1源码分析系列(二)jQuery选择器 jQuery-1.9.1源码分析系列(二)jQuery选择器续1 jQuery-1.9.1源码分析系列(二)jQuery选择器续2——筛选 jQuery-1.9.1源码分析系列(三) Sizzle选择器引擎——词法解析 jQuery-1.9.1源码分析系列(三) Sizzle选择

Dubbo源码分析系列-服务的发布

RPC简化类图 RPC模块核心接口和抽象实现 默认实现Dubbo协议的接口和抽象实现 服务发布过程 调用过程 上图是服务提供者暴露服务的主过程: 首先ServiceConfig类拿到对外提供服务的实际类ref(如:HelloWorldImpl),然后通过ProxyFactory类的getInvoker方法使用ref生成一个AbstractProxyInvoker实例,到这一步就完成具体服务到Invoker的转化.接下来就是Invoker转换到Exporter的过程. Dubbo处理服务暴露的关键