无人驾驶技术——YOLO目标检测
文章目录
-
为什么要检测物体?
-
YOLO简介
-
Yolov3工作流程
-
- 步骤1:初始化参数
- 步骤2:准备模型
- 步骤3:从输入图像生成4维 blob
- 步骤4:通过网络向前运行
- 步骤5:网络输出后处理
-
完整实现代码
-
- 数据结构定义
- YOLO_目标检测
为什么要检测物体?
我们需要一种方法来检测图像中的车辆,这样我们就可以隔离匹配的关键点以及投影的激光雷达点,并将它们与特定的对象相关联。让我们看一看程序流程图,我们已经在关于设计碰撞检测系统的课程中讨论过。

根据在上一课中学习的内容,现在可以使用各种检测器和描述符来检测和匹配关键点。然而,为了计算特定车辆的碰撞时间,我们需要隔离该车辆上的关键点,以便TTC估计不会由于包含匹配而失真,例如路面、静止物体或现场其他车辆。实现这一点的一种方法是使用自动检测识别场景中的车辆。这种算法的输出(理想情况下)是围绕场景中所有对象的一组二维边界框。基于这些边界框,我们可以很容易地将关键点匹配与对象关联起来,并实现一个稳定的TTC估计。
对于激光雷达测量,同样的原理也适用。正如您在前面关于LiDAR的课程中所学到的,您已经知道,可以通过使用点云库(PCL)中的算法,将3D点成功地聚集到单个对象中,这可以被视为与用于3D应用程序的OpenCV库等效。在本节中,让我们来看一下将激光雷达点分组为对象的另一种方法。根据在上一节中学习的内容,您现在知道如何将激光雷达点投影到图像平面上。给定一组来自对象检测的边界框,我们可以通过简单地检查3D激光雷达点在投射到相机中时是否被边界框包围,从而轻松地将其关联到场景中的特定目标。
我们将在本节后面详细介绍这两种方法,但现在,让我们来看一种检测摄像机图像中目标的方法——这是对关键点匹配和激光雷达点进行分组的先决条件。在上面所示的示意图中,当前课程的内容以蓝色矩形突出显示,在本节中,我们将重点讨论“目标的检测和分类”。
YOLO简介
本节的目的是使您能够快速利用一个强大的、最先进的对象检测工具。它的目的不是对这种算法的内部工作进行理论上的深入研究,而是使您能够快速、无缝地将对象检测集成到本课程的代码框架中。下图显示了我们将在本节中开发的代码的一个示例输出。

在过去,目标检测方法通常是基于定向梯度(HOG)和支持向量机(SVM)的直方图。在深入学习之前,HOG/SVM方法一直被认为是最先进的检测方法。虽然hog/svm的结果对于各种各样的问题仍然是可以接受的,但是它在处理速度有限的平台上的使用是有限的。与SIFT一样,一个主要的问题是依赖强度梯度的方法——这是一个昂贵的操作。
另一种目标检测方法是使用深度学习框架,如TensorFlow或Caffe。然而,两者的学习曲线都是非常陡峭的,这将保证一个完整的课程本身。一个简单易用的替代方案是yolo,它是一个非常快速的检测框架,随opencv库一起提供。YOLO是由康奈尔大学的Joseph Redmon, Santosh Divvala, Ross Girshick, and Ali Farhadi 开发的,他使用的方法与大多数其他方法不同:在这里,一个单一的神经网络应用于整个图像。该网络将图像划分为多个区域,并预测每个区域的边界框和概率。这些边界框由预测的概率加权。下图说明了原理:

除了基于分类器的系统(如hog/svm)之外,Yolo查看整个图像,以便通过图像中的全局上下文来通知其预测。它还可以通过单一网络进行预测,而不像R-CNN这样的系统,它需要对单个图像进行数千次预测。这使得它非常快,同时产生类似于其他最先进的方法,如单点多盒探测器(SSD)的结果。
Yolo的开发者还提供了一组预先训练的权重,使当前版本的Yolov3能够识别基于Coco(http://coco dataset.org/home)的图像和视频中的80个不同对象,这是一个大规模的对象检测、分割和字幕数据集。在本课程的背景下,这意味着我们可以使用yolov3作为开箱分类器,它能够以合理的精度检测车辆(和许多其他障碍物)。
Yolov3工作流程
在本节中,我们将了解在课程图像集上执行yolo所涉及的不同步骤。下面使用的参数主要是作者建议的参数。下面简要介绍了主要算法步骤:
首先,图像被划分为13x13网格单元。根据输入图像的大小,这些单元格的大小(以像素为单位)会有所不同。在下面的代码中,使用416 x 416像素的大小,导致32 x 32像素的单元大小。
2.如上图所示,每个单元随后用于预测一组边界框。对于每个边界框,网络还预测边界框包含特定对象的置信度以及属于特定类的对象的概率(取自COCO数据集)。
3.最后,使用非最大抑制来消除置信度较低的边界框以及包围同一对象的冗余边界框。
下面介绍了主要的工作流程以及相应的代码。
步骤1:初始化参数
Yolov3预测的每个边界框都与置信度得分相关。参数“confThreshold”用于删除分数较低的所有边界框。
然后,对剩余的边界框应用非最大抑制。NMS过程由参数“nmsthreshold”控制。
输入图像的大小由参数“inpWidth”和“inpHeight”控制,该参数由Yolo作者建议设置为416。其他值可以是320(更快)或608(更准确)。
步骤2:准备模型
文件“yolov3.weights”包含预先培训过的网络的权重,这里由yolo的作者提供。
包含网络配置的文件“yolov3.cfg”在这里可用,包含coco数据集中使用的80个不同类名的coco.names文件可以在这里下载。
以下代码显示了如何加载模型权重以及关联的模型配置:
// 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后端设置为DNN_BACKEND_OPENCV。如果opencv是用intel的推理引擎构建的,那么应该使用dnn_后端推理引擎。目标在代码中设置为CPU,相反,如果如果(Intel)GPU可用,可以选择使用DNN_TARGET_OPENCL。
步骤3:从输入图像生成4维 blob
当数据流经网络时,yolo将信息存储、通信和操作为“blobs”:blob是许多框架(包括caffe)的标准数组和统一内存接口。BLOB是对正在处理和传递的实际数据的包装,还提供CPU和GPU之间的同步功能。在数学上,BLOB是以C-连续方式存储的N维数组。成批图像数据的传统斑点尺寸是数字n x通道c x高度h x宽度w。在这个术语中,n是数据的成批大小。批处理为通信和设备处理实现了更好的吞吐量。对于一批256个图像的训练,n将是256。参数c代表特征尺寸,例如,对于RGB图像c=3。在opencv中,blob存储为具有NCHW 维度顺序的4维cv::mat数组。有关blob的更多详细信息,请访问:http://caffe.berkeleyvision.org/tutorial/net_layer_blob.html
下面的示例说明了具有n=2、c=16个通道和height h=5, width w=4的blob的内存结构。

在BLOB数据结构中,可以使用以下公式访问位置(n、c、h、w)的值(如果需要):b(n、c、h、w)=((n c+c)h+h)*w+w。
下面的代码显示了如何通过blobFromImage 函数将从文件加载的图像转换为神经网络的输入块。像素值的缩放比例为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将作为输入传递到网络。然后,将执行正向传递,以获取作为网络输出的预测边界框列表。这些框通过后处理步骤过滤掉那些低置信值的框。让我们更详细地看看这些步骤。
步骤4:通过网络向前运行
作为下一步,我们必须将刚刚创建的blob作为输入传递给网络。然后,我们运行opencv的forward函数,通过网络执行一次前向传递。为了做到这一点,我们需要识别网络的最后一层,并为函数提供相关的内部名称。这可以通过使用opencv函数“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类的数目)的向量,每个类中的前四个元素表示x中的中心、y中的中心以及相关边界框的宽度和高度。第五个元素表示各个边界框实际包含对象的信任或信心。矩阵的其余元素是与coco.cfg文件中包含的每个类相关联的置信度。在代码中,每个框都被分配给对应于最高置信度的类。
下面的代码显示了如何扫描网络结果,并将具有足够高的置信度的边界框组装成一个向量。函数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);
}
}
}
将yolov3算法应用到我们的公路图像中可以得到以下结果:

如图所示,左车道上的汽车由两个大小相似的边界框覆盖。为了避免冗余框的出现,最后一步执行一个非最大抑制,目的是只保留置信度得分最高的边界框。
步骤5:网络输出后处理
OpenCv库提供了一个现成的功能来抑制重叠的边界框。此函数称为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期间已被删除。

完整实现代码
数据结构定义
#ifndef dataStructures_h
#define dataStructures_h
#include <vector>
#include <opencv2/core.hpp>
struct LidarPoint { // single lidar point in space
double x,y,z,r; // x,y,z in [m], r is point reflectivity
};
struct BoundingBox { // bounding box around a classified object (contains both 2D and 3D data)
int boxID; // unique identifier for this bounding box
int trackID; // unique identifier for the track to which this bounding box belongs
cv::Rect roi; // 2D region-of-interest in image coordinates
int classID; // ID based on class file provided to YOLO framework
double confidence; // classification trust
std::vector<LidarPoint> lidarPoints; // Lidar 3D points which project into 2D image roi
std::vector<cv::KeyPoint> keypoints; // keypoints enclosed by 2D roi
std::vector<cv::DMatch> kptMatches; // keypoint matches enclosed by 2D roi
};
#endif /* dataStructures_h */
YOLO_目标检测
#include <iostream>
#include <numeric>
#include <fstream>
#include <opencv2/core.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/dnn.hpp>
#include "dataStructures.h"
using namespace std;
void detectObjects2()
{
// load image from file
cv::Mat img = cv::imread("../images/horses.jpg");
// 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);
// 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);
// 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);
// 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);
}
}
}
// perform non-maxima suppression
float nmsThreshold = 0.4; // Non-maximum suppression threshold
vector<int> indices;
cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
std::vector<BoundingBox> bBoxes;
for (auto it = indices.begin(); it != indices.end(); ++it)
{
BoundingBox bBox;
bBox.roi = boxes[*it];
bBox.classID = classIds[*it];
bBox.confidence = confidences[*it];
bBox.boxID = (int)bBoxes.size(); // zero-based unique identifier for this bounding box
bBoxes.push_back(bBox);
}
// show results
cv::Mat visImg = img.clone();
for (auto it = bBoxes.begin(); it != bBoxes.end(); ++it)
{
// Draw rectangle displaying the bounding box
int top, left, width, height;
top = (*it).roi.y;
left = (*it).roi.x;
width = (*it).roi.width;
height = (*it).roi.height;
cv::rectangle(visImg, cv::Point(left, top), cv::Point(left + width, top + height), cv::Scalar(0, 255, 0), 2);
string label = cv::format("%.2f", (*it).confidence);
label = classes[((*it).classID)] + ":" + label;
// Display label at the top of the bounding box
int baseLine;
cv::Size labelSize = getTextSize(label, cv::FONT_ITALIC, 0.5, 1, &baseLine);
top = max(top, labelSize.height);
rectangle(visImg, cv::Point(left, top - round(1.5 * labelSize.height)), cv::Point(left + round(1.5 * labelSize.width), top + baseLine), cv::Scalar(255, 255, 255), cv::FILLED);
cv::putText(visImg, label, cv::Point(left, top), cv::FONT_ITALIC, 0.75, cv::Scalar(0, 0, 0), 1);
}
string windowName = "Object classification";
cv::namedWindow( windowName, 1 );
cv::imshow( windowName, visImg );
cv::waitKey(0); // wait for key to be pressed
}
int main()
{
detectObjects2();
}
