本文译自 Deep Learning based Object Detection using YOLOv3 with OpenCV ( Python / C++ ) ,根据自己的实现情况补充了一些小细节,用红色字体标出。
在这篇文章中,我们将结合OpenCV,学习如何使用YOLOv3(一种最先进的目标检测算法)。
YOLOv3是流行的目标检测算法YOLO的最新变种 - You Only Look Once。已发布的模型可识别图像和视频中的80个不同对象,最重要的是其速度很快,且几乎与Single Shot MultiBox(SSD)一样准确。
从OpenCV 3.4.2开始,您可以在自己的OpenCV应用程序中轻松使用YOLOv3模型。
How does YOLO work ?
我们可以将目标检测器(object detector)视为对象定位器(object locator)和对象识别器(object recognizer)的组合。
在传统的计算机视觉方法中,使用滑动窗口来寻找不同位置和尺度的物体。由于代价太大,通常假设物体的纵横比是固定的。
早期,基于深度学习的目标检测算法(如 R-CNN 和 Fast R-CNN)使用称为“选择性搜索”(Selective Search)的方法来减少算法必须测试的边界框数量。
另一种称为Overfeat的方法使用类似滑动窗口的机制(sliding windows-like mechanisms)从多个尺度扫描图像。
紧随其后的Faster R-CNN,使用区域提议网络(RPN)来识别需要测试的边界框。RPN也通过巧妙地设计、提取用于识别对象的特征,以提出潜在的 bounding boxes,从而节省了大量的计算。
然而,YOLO用完全不同的方式处理目标检测问题。它只通过网络 forward 整个图像一次。 SSD是另一种目标检测算法,它通过深度学习网络将图像 forward 一次,但是YOLOv3比SSD快得多,同时实现了非常具有可比性的精度。 YOLOv3在M40,TitanX或1080 Ti GPU上可以提供比实时结果更快的速度。
让我们看看YOLO如何检测给定图像中的对象:
首先,它将图像划分为13×13网格的单元格。这169个单元格的大小取决于输入图像的大小。对于我们在实验中使用的416×416输入,每个单元格尺寸为32×32。每个单元格负责预测图像中的多个框。
对于每个边界框,网络还预测边界框实际包围对象的置信度,以及对象属于特定类的概率。
如果 bounding box 的置信度很低,或者它们与另一个具有非常高置信度得分的 bounding box 包围着相同的对象,那么这些 bounding boxes 就会被消除。该技术称为非极大值抑制(non-maximum suppression)。
YOLOv3的作者(Joseph Redmon 和 Ali Farhadi)更新了YOLOv3,比以前的YOLOv2更快、更准确。 YOLOv3可以更好地处理多个尺度。他们还通过加深网络来改进网络,并通过添加 shortcut connections 将网络向 residual network 扩展。
Why use OpenCV for YOLO ?
以下可能是将OpenCV用于YOLO的几个原因:
1. 与OpenCV应用轻松集成:如果您的应用基于OpenCV,同时又想尝试YOLOv3,则无需担心编译和构建额外的Darknet代码。
2. OpenCV CPU版本快9倍:OpenCV的DNN模块在CPU的实现速度惊人。例如,与OpenMP一起使用时,Darknet在CPU上药花费大约2s来实现对单个图像的推理。相比之下,OpenCV只需0.22秒!详见下表。
3. Python支持:Darknet是用C语言编写的,虽然Darknet有可用的python端口,但官方并不支持Python。相比之下,OpenCV却可以。
在Darknet和OpenCV上对YOLOv3进行速度测试
下表展示了 YOLOv3在 Darknet 与 OpenCV上的性能。输入大小均416×416。毫无疑问,Darknet 的GPU版本胜过其他。 OpenMP 可以使用多个处理器,所以使用 OpenMP 的 Darknet 有更好的工作性能。
令人惊讶的是,OpenCV的DNN模块在 CPU的实现速度比使用 OpenML 的 Darknet 快9倍。
表1:YOLOv3 在 Darknet 与 OpenCV 的速度测试
注意:我们使用OpenCV的DNN模块在 GPU实现时遇到了问题。文档表明它仅使用了英特尔的GPU进行测试,因此如果您没有英特尔GPU,代码会将您切换回CPU版本。
亲测确实是这样的:
使用C ++ / Python进行YOLOv3的目标检测
现在让我们看看如何在OpenCV中使用YOLOv3来执行目标检测。
第1步:下载模型
我们将从命令行使用脚本文件getModels.sh下载模型
sudo chmod a + x getModels.sh
./getModels.sh
这一步将会下载yolov3.weights文件(包含预先训练的网络权重),yolov3.cfg文件(包含网络配置)和coco.names文件,其中包含COCO数据集中使用的80个不同的类名。
已经下载好的就把自己下载的放到文件夹下即可。
第2步:初始化参数
YOLOv3算法生成 bounding boxes 作为预测的检测输出。每个预测框都与置信度得分相关联。在第一阶段,忽略掉低于置信度阈值参数的所有框。对其余框执行非极大值抑制,这会消除多余的重叠边界框。非极大值抑制由参数 nmsThreshold 控制。您可以尝试更改这些值,并查看输出预测框的数量如何变化。
接下来,设置网络输入图像的宽度(inpWidth)和高度(inpHeight)的默认值。我们设置为416,这样可以将我们的运行结果与YOLOv3作者给出的Darknet C代码进行比较。您也可以将它们更改为320以获得更快的结果,或更改为608以获得更准确的结果。
python
# Initialize the parameters
confThreshold = 0.5 #Confidence threshold
nmsThreshold = 0.4 #Non-maximum suppression threshold
inpWidth = 416 #Width of network's input image
inpHeight = 416 #Height of network's input image
C++
// Initialize the parameters
float confThreshold = 0.5; // Confidence threshold
float nmsThreshold = 0.4; // Non-maximum suppression threshold
int inpWidth = 416; // Width of network's input image
int inpHeight = 416; // Height of network's input image
第3步:加载模型和类
文件coco.names包含训练模型的所有对象, 我们读取了类的名字。
接下来,我们加载两个部分的网络 ——
yolov3.weights:预训练的权重。
yolov3.cfg:配置文件。
我们在这里将DNN后端设置为OpenCV,将目标设置为CPU。 您可以尝试将首选目标设置为cv.dnn.DNN_TARGET_OPENCL以在GPU上运行它。 但请记住,目前的OpenCV版本仅使用英特尔的GPU进行测试,如果您没有英特尔GPU,它会自动切换到CPU。
python
# Load names of classes
classesFile = "coco.names";
classes = None
with open(classesFile, 'rt') as f:
classes = f.read().rstrip('\n').split('\n')
# Give the configuration and weight files for the model and load the network using them.
modelConfiguration = "yolov3.cfg";
modelWeights = "yolov3.weights";
net = cv.dnn.readNetFromDarknet(modelConfiguration, modelWeights)
net.setPreferableBackend(cv.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv.dnn.DNN_TARGET_CPU)
C++
// Load names of classes
string classesFile = "coco.names";
ifstream ifs(classesFile.c_str());
string line;
while (getline(ifs, line)) classes.push_back(line);
// Give the configuration and weight files for the model
String modelConfiguration = "yolov3.cfg";
String modelWeights = "yolov3.weights";
// Load the network
Net net = readNetFromDarknet(modelConfiguration, modelWeights);
net.setPreferableBackend(DNN_BACKEND_OPENCV);
net.setPreferableTarget(DNN_TARGET_CPU);
第4步:读取输入
在此步骤中,我们将读取图像/视频/摄像头。 此外,我们还打开 video writer 以保存含有检测到的输出边界框的帧。
python
outputFile = "yolo_out_py.avi"
if (args.image):
# Open the image file
if not os.path.isfile(args.image):
print("Input image file ", args.image, " doesn't exist")
sys.exit(1)
cap = cv.VideoCapture(args.image)
outputFile = args.image[:-4]+'_yolo_out_py.jpg'
elif (args.video):
# Open the video file
if not os.path.isfile(args.video):
print("Input video file ", args.video, " doesn't exist")
sys.exit(1)
cap = cv.VideoCapture(args.video)
outputFile = args.video[:-4]+'_yolo_out_py.avi'
else:
# Webcam input
cap = cv.VideoCapture(0)
# Get the video writer initialized to save the output video
if (not args.image):
vid_writer = cv.VideoWriter(outputFile, cv.VideoWriter_fourcc('M','J','P','G'), 30, (round(cap.get(cv.CAP_PROP_FRAME_WIDTH)),round(cap.get(cv.CAP_PROP_FRAME_HEIGHT))))
C++
outputFile = "yolo_out_cpp.avi";
if (parser.has("image"))
{
// Open the image file
str = parser.get<String>("image");
ifstream ifile(str);
if (!ifile) throw("error");
cap.open(str);
str.replace(str.end()-4, str.end(), "_yolo_out.jpg");
outputFile = str;
}
else if (parser.has("video"))
{
// Open the video file
str = parser.get<String>("video");
ifstream ifile(str);
if (!ifile) throw("error");
cap.open(str);
str.replace(str.end()-4, str.end(), "_yolo_out.avi");
outputFile = str;
}
// Open the webcaom
else cap.open(parser.get<int>("device"));
// Get the video writer initialized to save the output video
if (!parser.has("image")) {
video.open(outputFile, VideoWriter::fourcc('M','J','P','G'), 28, Size(cap.get(CAP_PROP_FRAME_WIDTH), cap.get(CAP_PROP_FRAME_HEIGHT)));
}
我在跑python版本检测图片正常,但是检测视频时遇到了这样的问题:
需要float格式的输入,但我查看过输入都是float的,不知道为什么报错,最后把程序改成下面这样就好了:
给摄像头实时读取添加了输出视频名字,还把输出视频 fps 和 size 都转换成 int 型了。
第4步:处理每一帧
神经网络的输入图像需要采用称为blob的特定格式。
从输入图像或视频流中读取帧后,将通过 blobFromImage 函数将其转换为神经网络的输入blob。在此过程中,它使用比例因子1/255 将图像像素值缩放到0到1的目标范围。它还将图像的大小缩放为给定的大小(416,416)而不进行裁剪。请注意,我们不在此处执行任何均值减法,因此将[0,0,0]传递给函数的mean参数,并将swapRB参数保持为其默认值1。
然后输出blob作为输入传递到网络,并运行前向传递以获得预测边界框列表作为网络的输出。这些框经过后处理步骤,以滤除具有低置信度分数的那些框。我们将在下一节中更详细地介绍后处理步骤。我们会在左上角打印出每帧的推理时间。然后将含有最终边界框的图像保存下来。
python
while cv.waitKey(1) < 0:
# get frame from the video
hasFrame, frame = cap.read()
# Stop the program if reached end of video
if not hasFrame:
print("Done processing !!!")
print("Output file is stored as ", outputFile)
cv.waitKey(3000)
break
# Create a 4D blob from a frame.
blob = cv.dnn.blobFromImage(frame, 1/255, (inpWidth, inpHeight), [0,0,0], 1, crop=False)
# Sets the input to the network
net.setInput(blob)
# Runs the forward pass to get output of the output layers
outs = net.forward(getOutputsNames(net))
# Remove the bounding boxes with low confidence
postprocess(frame, outs)
# Put efficiency information. The function getPerfProfile returns the
# overall time for inference(t) and the timings for each of the layers(in layersTimes)
t, _ = net.getPerfProfile()
label = 'Inference time: %.2f ms' % (t * 1000.0 / cv.getTickFrequency())
cv.putText(frame, label, (0, 15), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255))
# Write the frame with the detection boxes
if (args.image):
cv.imwrite(outputFile, frame.astype(np.uint8));
else:
vid_writer.write(frame.astype(np.uint8))
C++
// Process frames.
while (waitKey(1) < 0)
{
// get frame from the video
cap >> frame;
// Stop the program if reached end of video
if (frame.empty()) {
cout << "Done processing !!!" << endl;
cout << "Output file is stored as " << outputFile << endl;
waitKey(3000);
break;
}
// Create a 4D blob from a frame.
blobFromImage(frame, blob, 1/255.0, cvSize(inpWidth, inpHeight), Scalar(0,0,0), true, false);
//Sets the input to the network
net.setInput(blob);
// Runs the forward pass to get output of the output layers
vector<Mat> outs;
net.forward(outs, getOutputsNames(net));
// Remove the bounding boxes with low confidence
postprocess(frame, outs);
// Put efficiency information. The function getPerfProfile returns the
// overall time for inference(t) and the timings for each of the layers(in layersTimes)
vector<double> layersTimes;
double freq = getTickFrequency() / 1000;
double t = net.getPerfProfile(layersTimes) / freq;
string label = format("Inference time for a frame : %.2f ms", t);
putText(frame, label, Point(0, 15), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0, 0, 255));
// Write the frame with the detection boxes
Mat detectedFrame;
frame.convertTo(detectedFrame, CV_8U);
if (parser.has("image")) imwrite(outputFile, detectedFrame);
else video.write(detectedFrame);
}
现在让我们详细了解上面调用的一些函数。
步骤4a:获取输出层名称
OpenCV 的 Net 类中的 forward 函数需要知道它的结束层。 想要遍历整个网络就需要识别网络的最后一层。 我们通过使用函数getUnconnectedOutLayers()来实现,该函数给出了未连接的输出层名称,这些输出层基本上是网络的最后一层。 然后我们运行网络的前向传递以从输出层获得输出,如前面的代码片段(net.forward(getOutputsNames(net)))。
python
# Get the names of the output layers
def getOutputsNames(net):
# Get the names of all the layers in the network
layersNames = net.getLayerNames()
# Get the names of the output layers, i.e. the layers with unconnected outputs
return [layersNames[i[0] - 1] for i in net.getUnconnectedOutLayers()]
C++
// Get the names of the output layers
vector<String> getOutputsNames(const Net& net)
{
static vector<String> names;
if (names.empty())
{
//Get the indices of the output layers, i.e. the layers with unconnected outputs
vector<int> outLayers = net.getUnconnectedOutLayers();
//get the names of all the layers in the network
vector<String> layersNames = net.getLayerNames();
// Get the names of the output layers in names
names.resize(outLayers.size());
for (size_t i = 0; i < outLayers.size(); ++i)
names[i] = layersNames[outLayers[i] - 1];
}
return names;
}
步骤4b:后处理网络的输出
网络 bounding boxes 每个输出都由一组 类数目+5 个元素的向量表示。前4个元素代表center_x,center_y,width和height。 第五个元素表示边界框包围对象的置信度。其余元素是与每个类相关的置信度(即对象类型)。 该框被分配到与该框的最高分相对应的那一类。
box 最高分也被称为置信度(confidence)。 如果框的置信度小于给定阈值,则删除该边界框并且不考虑进行进一步处理。然后对其置信度等于或大于阈值的框进行非极大值抑制。 这将会减少重叠框的数量。
python
# Remove the bounding boxes with low confidence using non-maxima suppression
def postprocess(frame, outs):
frameHeight = frame.shape[0]
frameWidth = frame.shape[1]
classIds = []
confidences = []
boxes = []
# Scan through all the bounding boxes output from the network and keep only the
# ones with high confidence scores. Assign the box's class label as the class with the highest score.
classIds = []
confidences = []
boxes = []
for out in outs:
for detection in out:
scores = detection[5:]
classId = np.argmax(scores)
confidence = scores[classId]
if confidence > confThreshold:
center_x = int(detection[0] * frameWidth)
center_y = int(detection[1] * frameHeight)
width = int(detection[2] * frameWidth)
height = int(detection[3] * frameHeight)
left = int(center_x - width / 2)
top = int(center_y - height / 2)
classIds.append(classId)
confidences.append(float(confidence))
boxes.append([left, top, width, height])
# Perform non maximum suppression to eliminate redundant overlapping boxes with
# lower confidences.
indices = cv.dnn.NMSBoxes(boxes, confidences, confThreshold, nmsThreshold)
for i in indices:
i = i[0]
box = boxes[i]
left = box[0]
top = box[1]
width = box[2]
height = box[3]
drawPred(classIds[i], confidences[i], left, top, left + width, top + height)
C++
// Remove the bounding boxes with low confidence using non-maxima suppression
void postprocess(Mat& frame, const vector<Mat>& outs)
{
vector<int> classIds;
vector<float> confidences;
vector<Rect> boxes;
for (size_t i = 0; i < outs.size(); ++i)
{
// Scan through all the bounding boxes output from the network and keep only the
// ones with high confidence scores. Assign the box's class label as the class
// with the highest score for the box.
float* data = (float*)outs[i].data;
for (int j = 0; j < outs[i].rows; ++j, data += outs[i].cols)
{
Mat scores = outs[i].row(j).colRange(5, outs[i].cols);
Point classIdPoint;
double confidence;
// Get the value and location of the maximum score
minMaxLoc(scores, 0, &confidence, 0, &classIdPoint);
if (confidence > confThreshold)
{
int centerX = (int)(data[0] * frame.cols);
int centerY = (int)(data[1] * frame.rows);
int width = (int)(data[2] * frame.cols);
int height = (int)(data[3] * frame.rows);
int left = centerX - width / 2;
int top = centerY - height / 2;
classIds.push_back(classIdPoint.x);
confidences.push_back((float)confidence);
boxes.push_back(Rect(left, top, width, height));
}
}
}
// Perform non maximum suppression to eliminate redundant overlapping boxes with
// lower confidences
vector<int> indices;
NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
for (size_t i = 0; i < indices.size(); ++i)
{
int idx = indices[i];
Rect box = boxes[idx];
drawPred(classIds[idx], confidences[idx], box.x, box.y,
box.x + box.width, box.y + box.height, frame);
}
}
非极大值抑制由nmsThreshold参数控制。 如果nmsThreshold设置得太低(例如 0.1),我们可能无法检测到相同或不同类的重叠对象。 但如果设置得太高(例如 1),然后我们会得到同一个对象的多个框。 所以我们在上面的代码中使用了0.4的中间值。 下面的gif显示了改变NMS阈值的效果。
图1:使用YOLOv3和OpenCV进行基于深度学习的目标检测(Python / C ++)
步骤4c:绘制预测框
最后,我们在输入框架上绘制通过非极大值抑制过滤的框,并指定其类别标签和置信度分数。
python
# Draw the predicted bounding box
def drawPred(classId, conf, left, top, right, bottom):
# Draw a bounding box.
cv.rectangle(frame, (left, top), (right, bottom), (0, 0, 255))
label = '%.2f' % conf
# Get the label for the class name and its confidence
if classes:
assert(classId < len(classes))
label = '%s:%s' % (classes[classId], label)
#Display the label at the top of the bounding box
labelSize, baseLine = cv.getTextSize(label, cv.FONT_HERSHEY_SIMPLEX, 0.5, 1)
top = max(top, labelSize[1])
cv.putText(frame, label, (left, top), cv.FONT_HERSHEY_SIMPLEX, 0.5, (255,255,255))
C++
// Draw the predicted bounding box
void drawPred(int classId, float conf, int left, int top, int right, int bottom, Mat& frame)
{
//Draw a rectangle displaying the bounding box
rectangle(frame, Point(left, top), Point(right, bottom), Scalar(0, 0, 255));
//Get the label for the class name and its confidence
string label = format("%.2f", conf);
if (!classes.empty())
{
CV_Assert(classId < (int)classes.size());
label = classes[classId] + ":" + label;
}
//Display the label at the top of the bounding box
int baseLine;
Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
top = max(top, labelSize.height);
putText(frame, label, Point(left, top), FONT_HERSHEY_SIMPLEX, 0.5, Scalar(255,255,255));
}
最后倒是跑成功了,但我的没有画出检测目标框,只有左上角的信息,自己把时间换成了帧率,看起来方便,等问题解决了会继续更新哒~
订阅和下载代码
如果您喜欢这篇文章并想下载此帖子中使用的代码(C ++和Python)和示例图片,请订阅我们的新闻通讯。 您还将收到免费的计算机视觉资源指南。 在我们的时事通讯中,我们分享了用C ++ / Python编写的OpenCV教程和示例,以及计算机视觉和机器学习算法和新闻。
参考文献:
我们使用了以下来源的视频片段:
Pixabay: [1] , [2] , [3] , [4] , [5] , [6]
Pexels: [2]