【亲测可用】YOLOv3 + OpenCV 实现目标检测(Python / C ++)

本文译自 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教程和示例,以及计算机视觉和机器学习算法和新闻。

现在订阅

参考文献:

YOLOv3 Tech Report

我们使用了以下来源的视频片段:

Pixabay: [1] , [2] , [3] , [4] , [5] , [6]

Pexels: [2]

猜你喜欢

转载自blog.csdn.net/haoqimao_hard/article/details/82081285