18、利用深度学习模型进行目标检测

利用深度学习模型进行目标检测

1. 深度学习目标检测简介

传统的级联分类器在目标检测中的召回率和准确率表现,相较于不断发展的深度学习方法较差。OpenCV 库已经开始转向深度学习方法,在 3.x 版本引入了深度神经网络(DNN)模块,到 4.x 版本,它可以加载多种格式的神经网络架构及其预训练权重,并且在最新版本中级联分类器的训练工具已被弃用。

2. OpenCV 支持的 DNN 模型格式

OpenCV 除了支持 TensorFlow 框架训练的 DNN 模型外,还支持来自其他多个框架的多种格式模型:
| 框架 | 模型格式 |
| ---- | ---- |
| Caffe | .caffemodel |
| TensorFlow |
.pb |
| Torch | .t7 或 .net |
| Darknet | .weights |
| DLDT |
.bin |

虽然 OpenCV 支持多种 DNN 模型,但仍有一些流行的深度学习框架未在上述列表中,如 PyTorch、Caffe2、MXNet 和 Microsoft Cognitive Toolkit (CNTK)。不过,有一种名为 Open Neural Network Exchange (ONNX) 的格式,得到了社区的开发和支持,OpenCV 现在也能够加载这种格式的模型。大多数流行的深度学习框架都支持 ONNX 格式,如果你使用的框架不在 OpenCV 支持列表中,可以将模型架构和训练好的权重保存为 ONNX 格式,这样就能在 OpenCV 中使用。

3. 获取可用于 OpenCV 的模型

OpenCV 本身无法构建和训练 DNN 模型,但它可以加载和前向传播 DNN 模型,且依赖项比其他深度学习框架少,是部署 DNN 模型的不错选择。OpenCV 团队和英特尔还创建并维护了一些专注于机器学习模型部署的项目,如 DLDT 和 OpenVINO 项目。

获取可用于 OpenCV 进行目标检测的模型,最简单的方法是寻找预训练模型。大多数流行的深度学习框架都有模型库,你可以在互联网上搜索框架名称加上“model zoo”关键字来找到它们,以下是一些示例:
- TensorFlow: https://github.com/tensorflow/models/blob/master/research/object_detection/g3doc/detection_model_zoo.md
- Caffe: http://caffe.berkeleyvision.org/model_zoo.html
- Caffe2: https://caffe2.ai/docs/zoo.html
- MXNet: https://mxnet.apache.org/model_zoo/index.html

此外,你还可以在 https://modelzoo.co/ 找到许多开源的预训练模型。

4. 深度学习目标检测器类型

一般来说,基于深度学习的目标检测器有三种类型:
- R-CNN 系列检测器,包括 R-CNN、Fast R-CNN 和 Faster R-CNN
- 单阶段检测器(SSD)
- 你只看一次(YOLO)

R-CNN 方法需要先使用某种算法提出可能包含目标的候选边界框,然后将这些候选框送入卷积神经网络(CNN)模型进行分类,因此这类检测器也被称为两阶段检测器。该方法的问题是速度极慢,且不是端到端的深度学习目标检测器。尽管 R-CNN 经过了两次改进(Fast R-CNN 和 Faster R-CNN),但即使在 GPU 上速度仍然不够快。

而 SSD 和 YOLO 方法采用单阶段策略,将目标检测视为回归问题,直接对输入图像进行处理,同时学习边界框坐标和相应的类别标签概率。通常,单阶段检测器的准确性不如两阶段检测器,但速度明显更快。例如,YOLO 在 GPU 上的性能可达到 45 FPS,而两阶段检测器可能只有 5 - 10 FPS。

5. 使用预训练的 YOLOv3 进行目标检测

我们将使用预训练的 YOLOv3 检测器进行目标检测,该模型在 COCO 数据集上进行了训练,可以检测数百种目标类别。要在 OpenCV 中使用该模型,需要先下载以下文件:
- 目标类别名称的文本文件:https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names
- 模型的配置文件:https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg
- 模型的预训练权重:https://pjreddie.com/media/files/yolov3.weights

下载步骤如下:

$ pwd
/home/kdr2/Work/Books/Qt-5-and-OpenCV-4-Computer-Vision-Projects/Chapter-06/Detective
$ mkdir data
$ cd data/
$ curl -L -O https://raw.githubusercontent.com/pjreddie/darknet/master/data/coco.names
# output omitted
$ curl -L -O https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3.cfg
# output omitted
$ curl -L -O https://pjreddie.com/media/files/yolov3.weights
# output omitted
$ ls -l
total 242216
-rw-r--r-- 1 kdr2 kdr2 625 Apr 9 15:23 coco.names
-rw-r--r-- 1 kdr2 kdr2 8342 Apr 9 15:24 yolov3.cfg
-rw-r--r-- 1 kdr2 kdr2 248007048 Apr 9 15:49 yolov3.weights

接下来,我们需要在代码中进行一些修改。首先,打开 capture_thread.h 头文件,添加一些方法和字段:

// ...
#include "opencv2/dnn.hpp"
// ...
class CaptureThread : public QThread
{
    // ...
private:
    // ...
    void detectObjectsDNN(cv::Mat &frame);
private:
    // ...
    cv::dnn::Net net;
    vector<string> objectClasses;
};

这里添加了 opencv2/dnn.hpp 头文件,因为要使用 DNN 模块。 detectObjectsDNN 方法用于使用 DNN 模型在帧中检测目标, cv::dnn::Net net 是 DNN 模型实例, vector<string> objectClasses 用于存储 COCO 数据集中的目标类别名称。

然后,打开 capture_thread.cpp 文件,实现 detectObjectsDNN 方法:

void CaptureThread::detectObjectsDNN(cv::Mat &frame)
{
    int inputWidth = 416;
    int inputHeight = 416;
    if (net.empty()) {
        // give the configuration and weight files for the model
        string modelConfig = "data/yolov3.cfg";
        string modelWeights = "data/yolov3.weights";
        net = cv::dnn::readNetFromDarknet(modelConfig, modelWeights);
        objectClasses.clear();
        string name;
        string namesFile = "data/coco.names";
        ifstream ifs(namesFile.c_str());
        while(getline(ifs, name)) objectClasses.push_back(name);
    }
    // more code here ...
}

在这个方法开始时,定义了 YOLO 模型输入图像的宽度和高度,这里选择了 416 x 416。检查 net 字段是否为空,如果为空则使用 cv::dnn::readNetFromDarknet 函数加载模型,并读取 data/coco.names 文件中的类别名称存储到 objectClasses 中。

6. 图像转换与模型前向传播

模型和类别名称加载完成后,需要将输入图像进行转换并传递给 DNN 模型进行前向传播以获取输出:

cv::Mat blob;
cv::dnn::blobFromImage(
    frame, blob, 1 / 255.0,
    cv::Size(inputWidth, inputHeight),
    cv::Scalar(0, 0, 0), true, false);
net.setInput(blob);
// forward
vector<cv::Mat> outs;
net.forward(outs, getOutputsNames(net));

cv::dnn::blobFromImage 方法用于图像转换,其参数含义如下:
- 第一个参数是输入图像。
- 第二个参数是输出图像。
- 第三个参数是每个像素值的缩放因子,这里使用 1 / 255.0,因为模型要求像素值为 0 到 1 之间的浮点数。
- 第四个参数是输出图像的空间大小,这里使用之前定义的 416 x 416。
- 第五个参数是均值,由于 YOLO 训练时未进行均值减法,所以这里使用零。
- 下一个参数是是否交换 R 和 B 通道,因为 OpenCV 使用 BGR 格式,而 YOLO 使用 RGB 格式,所以需要交换。
- 最后一个参数是是否裁剪图像并取中心裁剪,这里指定为 false。

在进行前向传播时,需要知道要获取哪些层的输出,这通过 getOutputsNames 函数实现:

vector<string> getOutputsNames(const cv::dnn::Net& net)
{
    static vector<string> names;
    vector<int> outLayers = net.getUnconnectedOutLayers();
    vector<string> layersNames = net.getLayerNames();
    names.resize(outLayers.size());
    for (size_t i = 0; i < outLayers.size(); ++i)
        names[i] = layersNames[outLayers[i] - 1];
    return names;
}
7. 解码输出层数据

前向传播完成后,输出层的数据存储在 vector<cv::Mat> outs 变量中,需要解码该向量以获取所需信息,通过 decodeOutLayers 函数实现:

void decodeOutLayers(
    cv::Mat &frame, const vector<cv::Mat> &outs,
    vector<int> &outClassIds,
    vector<float> &outConfidences,
    vector<cv::Rect> &outBoxes
)
{
    float confThreshold = 0.5; // confidence threshold
    float nmsThreshold = 0.4; // non-maximum suppression threshold
    vector<int> classIds;
    vector<float> confidences;
    vector<cv::Rect> boxes;
    // not finished, more code here ...
    for (size_t i = 0; i < outs.size(); ++i) {
        float* data = (float*)outs[i].data;
        for (int j = 0; j < outs[i].rows; ++j, data += outs[i].cols)
        {
            cv::Mat scores = outs[i].row(j).colRange(5, outs[i].cols);
            cv::Point classIdPoint;
            double confidence;
            // get the value and location of the maximum score
            cv::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(cv::Rect(left, top, width, height));
            }
        }
    }
    // non maximum suppression
    vector<int> indices;
    cv::dnn::NMSBoxes(boxes, confidences, confThreshold, nmsThreshold, indices);
    for (size_t i = 0; i < indices.size(); ++i) {
        int idx = indices[i];
        outClassIds.push_back(classIds[idx]);
        outBoxes.push_back(boxes[idx]);
        outConfidences.push_back(confidences[idx]);
    }
}

该函数首先定义了置信度和非极大值抑制的阈值,然后遍历输出层的矩阵,对于每个矩阵的每一行,获取最大得分及其位置,若得分大于置信度阈值,则解码出边界框信息。最后使用非极大值抑制减少重叠框,将保留的框及其类别索引和置信度存储在输出参数中。

8. 在原始帧上绘制检测结果

调用 decodeOutLayers 函数获取检测到的目标信息后,在原始帧上绘制边界框和类别标签:

for(size_t i = 0; i < outClassIds.size(); i ++) {
    cv::rectangle(frame, outBoxes[i], cv::Scalar(0, 0, 255));
    // get the label for the class name and its confidence
    string label = objectClasses[outClassIds[i]];
    label += cv::format(":%.2f", outConfidences[i]);
    // display the label at the top of the bounding box
    int baseLine;
    cv::Size labelSize = cv::getTextSize(label,
        cv::FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
    int left = outBoxes[i].x, top = outBoxes[i].y;
    top = max(top, labelSize.height);
    cv::putText(frame, label, cv::Point(left, top),
        cv::FONT_HERSHEY_SIMPLEX, 0.5, cv::Scalar(255,255,255));
}
9. 编译和运行前的准备

在编译和运行应用程序之前,还需要进行一些修改:
- 在 CaptureThread::run() 方法中,将使用级联分类器检测目标的 detectObjects 方法调用改为新添加的 detectObjectsDNN 方法调用:

// detectObjects(tmp_frame);
detectObjectsDNN(tmp_frame);
  • Detective.pro 项目文件的 LIBS 配置末尾添加 opencv_dnn 模块:
unix: !mac {
    INCLUDEPATH += /home/kdr2/programs/opencv/include/opencv4
    LIBS += -L/home/kdr2/programs/opencv/lib -lopencv_core -lopencv_imgproc -lopencv_imgco
}
10. YOLO 检测效果与局限性

YOLO 在目标检测中表现出色,但仍存在一些误判情况。例如,可能会将 iPad 识别为笔记本电脑,将足球场识别为电视监视器。为了减少误判,可以适当提高置信度阈值,如设置为 0.70。此外,YOLO 模型也有其局限性,它对小目标和紧密排列的目标处理效果不佳。如果处理的是小目标或紧密排列目标的视频或图像,YOLO 可能不是最佳选择。

11. 自定义模型训练

如果没有适合你需求的预训练模型,比如要检测的目标不在 COCO 数据集中,就需要自己构建和训练 DNN 模型。每个深度学习框架都有关于在 MNIST 或 CIFAR - 10/100 数据集上构建和训练模型的教程,按照这些教程学习如何为自己的用例训练 DNN 模型。此外,Keras 框架提供了用于构建、训练和运行 DNN 模型的高级 API,它使用 TensorFlow、CNTK 和 Theano 作为底层框架,对于初学者来说是个不错的选择。

下面是整个目标检测流程的 mermaid 流程图:

graph TD;
    A[下载 YOLOv3 文件] --> B[加载模型和类别名称];
    B --> C[图像转换];
    C --> D[模型前向传播];
    D --> E[解码输出层数据];
    E --> F[绘制检测结果];
    F --> G[编译和运行应用程序];

综上所述,通过使用 OpenCV 和预训练的 YOLOv3 模型,我们可以方便地进行目标检测,但在实际应用中需要根据具体情况考虑模型的局限性,并在必要时进行自定义模型训练。

利用深度学习模型进行目标检测

12. 不同目标检测器的性能对比

为了更好地理解不同类型的深度学习目标检测器的特点,下面对 R - CNN 系列、SSD 和 YOLO 进行详细的性能对比,如下表所示:
| 检测器类型 | 检测阶段 | 准确性 | 速度 | 适用场景 |
| ---- | ---- | ---- | ---- | ---- |
| R - CNN 系列(R - CNN、Fast R - CNN、Faster R - CNN) | 两阶段 | 较高 | 较慢,即使在 GPU 上也不够快 | 对准确性要求极高,对速度要求不苛刻的场景,如静态图像中的复杂目标检测 |
| SSD | 单阶段 | 中等 | 较快 | 对速度有一定要求,同时对准确性也有一定要求的场景,如实时视频流中的目标检测 |
| YOLO | 单阶段 | 中等,但有一定误判 | 快,在 GPU 上可达 45 FPS | 对速度要求极高的场景,如实时监控、自动驾驶中的快速目标检测 |

通过这个对比表格,我们可以根据具体的应用场景选择合适的目标检测器。

13. 优化 YOLO 检测效果的其他方法

除了提高置信度阈值来减少误判外,还可以尝试以下方法来优化 YOLO 的检测效果:
- 数据增强 :在训练自定义模型时,对训练数据进行旋转、翻转、缩放等操作,增加数据的多样性,提高模型的泛化能力。
- 模型微调 :如果有少量的自定义数据,可以在预训练的 YOLO 模型基础上进行微调,使模型更适应特定的检测任务。
- 多模型融合 :结合多个不同的目标检测器,综合它们的检测结果,提高检测的准确性和鲁棒性。

14. 拓展阅读与实践建议

对于想要深入学习目标检测和深度学习的读者,以下是一些拓展阅读和实践建议:
- 阅读经典论文 :深入研究 R - CNN、SSD、YOLO 等相关的经典论文,了解其原理和算法细节。
- 参与开源项目 :在 GitHub 等平台上参与目标检测相关的开源项目,学习他人的代码和经验。
- 实践自定义数据集 :收集自己的数据集,尝试训练自定义的目标检测模型,加深对整个流程的理解。

15. 总结

本文详细介绍了利用深度学习模型进行目标检测的相关知识,主要内容总结如下:
1. OpenCV 支持的 DNN 模型格式 :包括 Caffe、TensorFlow、Torch、Darknet、DLDT 等框架的模型格式,以及 ONNX 格式的通用性。
2. 获取模型的途径 :通过各框架的模型库和开源模型网站获取预训练模型。
3. 目标检测器类型 :R - CNN 系列、SSD 和 YOLO,并对比了它们的性能特点。
4. YOLOv3 目标检测流程 :从下载文件、加载模型、图像转换、前向传播、解码输出到绘制检测结果,以及编译和运行应用程序的详细步骤。
5. YOLO 的局限性和优化方法 :指出 YOLO 在小目标和紧密排列目标检测上的不足,并提供了减少误判和优化检测效果的方法。

整个目标检测的步骤可以用以下列表概括:
1. 选择合适的目标检测器。
2. 获取或训练适合的模型。
3. 准备必要的文件(配置文件、权重文件、类别名称文件等)。
4. 进行图像转换和模型前向传播。
5. 解码输出层数据,减少重叠框。
6. 在原始帧上绘制检测结果。
7. 编译和运行应用程序。

通过本文的学习,希望读者能够掌握利用深度学习模型进行目标检测的基本方法,并在实际应用中根据具体需求做出合理的选择和优化。

下面是一个简单的决策流程图,帮助读者选择合适的目标检测器:

graph TD;
    A[对速度要求高吗?] -->|是| B[选择单阶段检测器];
    A -->|否| C[对准确性要求极高吗?];
    C -->|是| D[选择 R - CNN 系列];
    C -->|否| B;
    B --> E{更倾向于准确性还是速度};
    E -->|速度优先| F[选择 YOLO];
    E -->|准确性和速度平衡| G[选择 SSD];

在实际应用中,我们可以根据这个流程图快速确定适合自己场景的目标检测器。同时,不断学习和实践,探索更多优化目标检测效果的方法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值