【多传感融合】优达学城多传感融合学习笔记(五)——利用YOLO进行图像目标识别

利用YOLO进行图像目标识别

目录

  • 利用YOLO进行图像目标识别
    • 为什么需要目标检测?
    • YOLO简介
    • YOLOv3目标识别工作流程

为什么需要目标检测?

在上一课将激光雷达3D点云映射到相机图像中我们已经实现了将激光雷达3D点云映射到相机图像中(效果如下图所示),但是要想让我们得到的激光和相机融合后的结果更好地服务于自动驾驶车辆,例如,应用到常见的碰转时间(TTC)估计中,我们还需要一种技术来检测图像中的车辆目标,这样我们就能分离出图像中匹配的关键点(matched keypoints)以及映射在图像中的激光雷达点云,并能进一步将它们与特定的对象进行关联。
【多传感融合】优达学城多传感融合学习笔记(五)——利用YOLO进行图像目标识别_第1张图片
我们知道,利用图像处理技术,我们可以使用各种检测器(detectors )和描述子(descriptors)来检测和匹配图像中的关键点(keypoints )。然而,在TTC估计的应用场景中,为了计算车辆的碰撞时间,我们需要分离出车辆目标上的关键点,这样TTC估计就不会因为包含了前方场景中路面、静止物体或其他车辆上的匹配信息而失真。实现这一目的的一种方法是使用目标检测来自动识别场景中的车辆。这种算法的输出(理想情况下)是场景中所有对象周围的一组2D边界框(bounding boxes)。基于这些边界框,我们可以轻松地将匹配关键点与目标对象关联起来,从而获得一个稳定的TTC估计值。

对于激光雷达的测量结果,可以应用相同的原理。我们知道,激光雷达的3D点云可以通过聚类形成单独的对象。例如,我们可以通过使用点云库(PCL)中的算法,进行激光雷达点云的聚类处理,而PCL则可以被近似看作是3D应用程序的OpenCV库。在本节中,让我们来看看另一种将激光雷达点分聚类成目标的方法。根据上一节的课程,我们现在已经掌握了如何将激光雷达点投射到图像平面上,那么给定一组图像中来自目标检测的边界框,我们这只需检查它在映射到相机图像后是否在某个边界框中,就可以很容易地将一个三维激光雷达点与场景中的特定目标关联起来。

我们将在本节后面详细介绍这两种方法,但是现在,让我们先来了解一种检测相机图像中目标的方法——这是分离出匹配关键点和激光雷达点云的先决条件。下面就让我们学习一种“检测和分类对象”的方法。

YOLO简介

本节的目的是使你能够快速地利用一个强大和先进的工具进行目标检测。本文的目的不是在理论上深入研究这些算法的内部工作原理,而是让您能够快速将目标检测集成到本课程的代码框架中。下面的图像显示了我们将在本节中开发的代码的示例输出。

YOLO官网


注:上面的图片改编自开源项目源代码

不同于HOG/SVM这样基于分类器的系统,YOLO针对整个图像,因此其预测值依赖于整个图像的全部内容。它的预测只用了单层网络模型,而不像R-CNN系统那样依赖上千层网络模型。这使得YOLO在输出类似预测结果的条件下,具有极快的预测速度。
YOLO的创造者也提供了一系列基于COCO数据集进行预训练得到的权重,使当前版本YOLOv3能够识别图像和图像中的80种不同目标。这意味着,我们可以利用YOLOv3作为一个“开箱即用的”分类器,来相对准确地检测图像中的汽车及其他目标。

YOLOv3目标识别工作流程

本节,我们将看一下针对本课程的图像数据集,执行YOLO所需的步骤。下面提到的参数是课程作者推荐的。下面是主要算法流程:

  1. 首先,图像被分成1313像素的栅格单元。基于输入图像的尺寸,这些栅格单元的尺寸也应该相应变化。在下面的代码中,一个416416像素的图像,对应的栅格单元尺寸为32*32像素。
  2. 如下方原理图所示,每个栅格单元用于预测一系列的bounding box。对于每个bounding box,网络也会预测该box包含某个特定目标的置信度,以及该目标属于哪种种类的概率(依据COCO数据集)。
  3. 最后,进行非极大值抑制 (non-maximum suppression),用于排除具有低置信度的bounding box以及包含了同一目标的多余box。

下面较为详细地展示了程序工作流程对应的代码。
步骤1:参数初始化
YOLOv3预测的每个bounding box都有一个相应的置信度。参数 confThreshold用于移除低置信度的bounding box。
然后,非极大值抑制(NMS)用那个与保留bounding box,NMS过程受参数nmsThreshold控制。
输入图像的而大小受参数inpWidthinpWidth控制,YOLO作者将其均设置为416。这里我们也可以将其改为320(更快)或608(更慢)。
步骤2:准备模型
文件yolov3.weights包含YOLO作者提供的预训练网络的权重,可以通过此链接下载。

下面的代码展示了如何加载模型权重以及相关的模型配置。

    // load image from file
    cv::Mat img = cv::imread("./images/img1.png");

    // load class names from file
    string yoloBasePath = "./dat/yolo/";
    string yoloClassesFile = yoloBasePath + "coco.names";
    string yoloModelConfiguration = yoloBasePath + "yolov3.cfg";
    string yoloModelWeights = yoloBasePath + "yolov3.weights"; 

    vector<string> classes;
    ifstream ifs(yoloClassesFile.c_str());
    string line;
    while (getline(ifs, line)) classes.push_back(line);

    // load neural network
    cv::dnn::Net net = cv::dnn::readNetFromDarknet(yoloModelConfiguration, yoloModelWeights);
    net.setPreferableBackend(cv::dnn::DNN_BACKEND_OPENCV);
    net.setPreferableTarget(cv::dnn::DNN_TARGET_CPU);

加载了网络之后,DNN backend被设置为DNN_BACKEND_OPENCV。如果OpenCV已经经过Intel的Inference引擎编译,则应该将其设置为DNN_BACKEND_INFERENCE_ENGINE。 在代码中,目标被设置为了CPU,如果要使用(Intel)GPU,则应该将DNN_TARGET_CPU设置为DNN_TARGET_OPENCL
**步骤3:从输入图像中生成4D Blob **

当数据流进入网络模型,YOLO以“Blobs”作为保存、通信、操作信息的基本单元:blob是用于许多框架(如Caffe)的标准序列(array)和统一存储接口。一个Blob是一个提供实际数据处理、交互,以及CPU和GPU之间数据同步的包装器 (wrapper)。数学上,一个blob是以C-contiguous方式存储的一个N维序列,N是数据的batch size。常见的用于图像数据batches的blob维度是 数量 N * 通道C * 高度H * 宽度W。在这种情况下,N是数据的batch size。利用Batch进行处理可以使数据通信和设备处理获得更好的数据吞吐量。对于一个256个图像的训练batch,N应为256. C表示特征维度,例如对于RGB图像,C=3。在OpenCV中,blob被存储为4维cv::Mat 序列(array),维度顺序为NCHW。更多关于blob的信息可以参考:http://caffe.berkeleyvision.org/tutorial/net_layer_blob.html
下面的例子展示了N=2,C=16通道,H=5, W=4时的blob数据情况。
【多传感融合】优达学城多传感融合学习笔记(五)——利用YOLO进行图像目标识别_第2张图片
注:图片来源

在一个blob数据结构中,位置值(n,c,h,w)可以通过下面的公式进行访问:
b (n, c, h, w) = ((n * C + c) * H + h) * W + w

下方的代码展示了从文件中加载的图像数据如何传递给blobFromImage函数从而转换为用于神经网络的输入block。像素值可以通过乘以一个缩放因子1/255,来将像素值缩放到[0,1]范围。而且代码中还调整了图像尺寸为(416,416,416),从而使图像不会被裁剪。

    // generate 4D blob from input image
    cv::Mat blob;
    double scalefactor = 1/255.0;
    cv::Size size = cv::Size(416, 416);
    cv::Scalar mean = cv::Scalar(0,0,0);
    bool swapRB = false;
    bool crop = false;
    cv::dnn::blobFromImage(img, blob, scalefactor, size, mean, swapRB, crop);

接下来的代码中,输出的blob将会作为神经网络的输入。然后,会执行前向传递从而获得一个预测的bounding box作为网络的输出。这些box经过后处理步骤,滤掉了其中具有低置信度的部分。详细内容请见下一步骤。
步骤4:执行网络的前向传递
接下来,我们将上一步中输出的blob作为神经网络的输入。然后,我们运行forward-function函数执行网络的前向传播过程。为了实现这一步,我们需要确定网络的最后一层,并提供相关的函数内部名称。这一步通过getUnconnectedOutLayers函数完成,它会给未连接的网络输出层提供名字,下面的代码展示了具体实现:

    // Get names of output layers
    vector<cv::String> names;
    vector<int> outLayers = net.getUnconnectedOutLayers(); // get indices of output layers, i.e. layers with unconnected outputs
    vector<cv::String> layersNames = net.getLayerNames(); // get names of all layers in the network

    names.resize(outLayers.size());
    for (size_t i = 0; i < outLayers.size(); ++i) // Get the names of the output layers in names
    {
        names[i] = layersNames[outLayers[i] - 1];
    }

    // invoke forward propagation through network
    vector<cv::Mat> netOutput;
    net.setInput(blob);
    net.forward(netOutput, names);

因此,前向传播的结果,也就是神经网络的输出是一个长度为C(blob类别个数)的数组,每个类别的前四个值分别为bounding box的x中心、y中心、宽度、高度;第五个值表示该bounding box包含一个目标的置信度;剩余的矩阵元素是对应coco.cfg文件中每个目标类别的置信度。随后,在代码中,我们将每个box都关联到了置信度最高的目标类别上。

下面的代码展示了如何遍历网络结果并且将具有足够高置信度的bounding box聚合为一个向量。函数cv::minMaxLoc勇于找到元素中的最小值和最大值,以及它们在向量中对应的位置。

  // Scan through all bounding boxes and keep only the ones with high confidence
    float confThreshold = 0.20;
    vector<int> classIds;
    vector<float> confidences;
    vector<cv::Rect> boxes;
    for (size_t i = 0; i < netOutput.size(); ++i)
    {
        float* data = (float*)netOutput[i].data;
        for (int j = 0; j < netOutput[i].rows; ++j, data += netOutput[i].cols)
        {
            cv::Mat scores = netOutput[i].row(j).colRange(5, netOutput[i].cols);
            cv::Point classId;
            double confidence;

            // Get the value and location of the maximum score
            cv::minMaxLoc(scores, 0, &confidence, 0, &classId);
            if (confidence > confThreshold)
            {
                cv::Rect box; int cx, cy;
                cx = (int)(data[0] * img.cols);
                cy = (int)(data[1] * img.rows);
                box.width = (int)(data[2] * img.cols);
                box.height = (int)(data[3] * img.rows);
                box.x = cx - box.width/2; // left
                box.y = cy - box.height/2; // top

                boxes.push_back(box);
                classIds.push_back(classId.x);
                confidences.push_back((float)confidence);
            }
        }
    }

步骤5:网络输出的后处理

通常,为了避免多个box实际包含了同一目标,我们会利用非极大值抑制算法(NMS)来去除掉多余的bounding box,并只保留具有最高置信度的box。OpenCV库 提供了一个现成的函数用于实现重叠bounding box的NMS。该函数为NMSBoxes ,其调用方式如下:

    // perform non-maxima suppression
    float nmsThreshold = 0.4;  // Non-maximum suppression threshold
    vector<int> indices;
    cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);

应用了NMS操作之后,多余的bounding box将被成功移除。下图展示了最后利用YOLOv3进行图像目标识别的最终效果:

你可能感兴趣的:(多传感融合,c++编程)