OpenCV实战(33)——OpenCV与深度学习的碰撞

本文介绍了如何使用OpenCV结合深度学习进行人脸检测,重点讲解了SSD算法的工作原理,并提供了使用预训练模型进行人脸检测的步骤,包括下载模型、创建网络输入、加载模型和在图像上绘制检测结果。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

0. 前言

深度学习是机器学习的一个子领域,基于传统的神经网络和卷积神经网络,在语音识别、文本识别和图像分类等领域能够获得接近甚至超越人类水平的准确率。OpenCV 在其核心算法中添加了深度学习模块作为基础模块,并借助 CPUGPU 来提高其性能。

1. 深度学习和卷积神经网络

将机器学习算法应用于现实世界问题时的出色表现使它们为相关应用程序提供了新思路。深度学习基于神经网络理论,深度学习的快速发展主要是由于以下原因,首先是可用的算力允许部署大规模的神经网络,使其能够解决具有挑战性的问题,虽然初代神经网络(感知器)只有一层并仅有很少的权重参数需要调整,但今天的网络可以有数百层和数千万个参数需要优化(因此称为深度网络);其次海量的数据使神经网络的训练成为可能,为了获得优异性能,深度网络需要数千甚至数百万个带标签的样本(这是因为需要优化的参数数量非常多)。
深度网络中最重要的一个分支是卷积神经网络 (Convolutional Neural Networks, CNN),其基于卷积操作,要学习的参数是构成网络的所有滤波器核中的值。将这些滤波器组织成多个网络层,早期的网络层可以提取对象基本形状,例如线条和角等,而后期的层可以逐渐检测更复杂的模式,例如,眼睛、嘴巴和头发等。
OpenCV 中包含一个深度神经网络模块,主要用于导入使用其他机器学习库(例如 TensorFlowCaffeTorch) 训练过的深度网络。

2. 使用深度学习进行人脸检测

在本节中,我们将学习如何在 OpenCV 中使用预训练的深度学习执行进行人脸检测。我们需要下载预训练的人脸检测模型并使用 OpenCV 方法导入模型,并了解如何将输入图像或图像帧转换为深度学习模型所需的结构。
OpenCV 中使用深度学习模型非常简单,仅需要加载预训练模型文件并了解其基本配置。我们首先需要下载预训练的深度神经网络模型,接下来我们以人脸检测为例,讲解如何使用在 OpenCV 中使用深度神经网络模型。

2.1 SSD 简介

本节使用单次检测器 (Single-Shot Detector, SSD) DNN 算法检测图像中的人脸,SSD 算法在处理图像时同时预测边界框和类别。SSD DNN 结构如下:

  • 使用尺寸为 300x300 的输入图像
  • 输入图像经过多个卷积层,得到不同尺度的不同特征
  • 对于每个特征图,使用 3x3 卷积滤波器评估一组默认边界框
  • 评估每个默认边界框时,预测边界框偏移量和类别概率

模型架构如下所示:

SSD 网络架构
SSD 是一种 DNN 算法,可用于对多个类别进行分类,我们可以使用修改后的网络执行人脸检测。在 OpenCV 中,定义和使用 DNN 模型最重要的函数是 blobFomImagereadNetFromsetInputforward
使用 blobFromImage 函数可以将输入图像转换为 blob,调用方法如下:

blobFromImage(image, scaleFactor, size, mean, swapRB, crop);

blobFromImage 函数中的每个参数含义如下:

  • image:输入图像
  • size:输出图像的尺寸大小
  • mean:将在图像中减去的标量,如果使用均值减法,在 swapRB = True 时,结果为 (mean-R, mean-G, mean-B)
  • scalefactor:图像值缩放因子
  • swapRB:标志位,表示是否需要交换 3 通道图像中的第一个和最后一个通道
  • crop:标志位,表示图像在调整大小后是否需要裁剪

要加载模型,我们可以使用 readFrom[type] 导入器导入使用以下机器学习库训练的模型:

  • Caffe
  • Tensorflow
  • PyTorch
  • Keras

导入深度学习模型并创建输入 blob 后,就可以使用 Net 类的 setInput 函数将输入 blob 输入到神经网络中,其中第一个参数是 blob 输入,第二个参数是输入层的名称(如果存在多个输入层,需要指定输入层名)。最后调用函数 forward,为输入 blob 执行前向计算并以 cv::Mat 格式返回预测结果。
在人脸检测算法中,返回的 cv::Mat 具有以下结构,detection.size[2] 是检测到的物体的数量,detect.size[3] 是每次检测的结果数据(边界框数据和置信度),其结构如下:

  • Column 0:对象存在的置信度
  • Column 1:边界框的置信度
  • Column 2:检测到的人脸的置信度
  • Column 3:左下边界框 X 坐标
  • Column 4:左下边界框 Y 坐标
  • Column 5:右上角边界框 X 坐标
  • Column 6:右上角边界框 Y 坐标

边界框与图像大小相关,当我们要在图像中绘制边界框矩形时,我们需要乘以图像大小。

2.2 使用 SSD 执行人脸检测

(1) 下载人脸检测器的模型并保存在 data 文件夹中,通常需要两个文件:权重文件 deploy.prototxt 和网络结构文件 res10_300x300_ssd_iter_140000.caffemodel。为了使用人脸检测算法中,我们下载定义网络结构的 deploy.prototxt 文件和包含网络权重的 res10_300x300_ssd_iter_140000.caffemodel 文件。

(2) 使用预训练深度神经网络 (Deep Neural Network, DNN),创建 face_detection.cpp 文件并导入所需的库:

#include <opencv2/dnn.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;
using namespace cv::dnn;

(3) 声明需要在 DNN 算法中使用的全局变量,用于定义了输入网络、预处理数据和要加载的文件名:

float confidenceThreshold = 0.5;
String modelConfiguration = "deploy.prototxt";
String modelBinary = "res10_300x300_ssd_iter_140000.caffemodel";
const size_t inWidth = 300;
const size_t inHeight = 300;
const double inScaleFactor = 1.0;
const Scalar meanVal(104.0, 177.0, 123.0);

(4) 创建 main 函数并在 OpenCV dnn::Net 类中加载模型:

int main(int argc, char **argv) {
    dnn::Net net = readNetFromCaffe(modelConfiguration, modelBinary);

(5) 调用 empty() 函数检查 DNN 是否正确加载:

    if (net.empty()) {
        cerr << "Can't load network by using the following files: " << endl;
        cerr << "prototxt: " << modelConfiguration << endl;
        cerr << "caffemodel: " << modelBinary << endl;
        cerr << "Models are available here:" << endl;
        cerr << "<OPENCV_SRC_DIR>/samples/dnn/face_detector" << endl;
        cerr << "or here:" << endl;
        cerr << "https://github.com/opencv/opencv/tree/master/samples/dnn/face_detector" << endl;
        exit(-1);
    }

(6) 如果 DNN 正确加载,我们就可以开始捕获图像帧。检查应用程序中输入参数的数量以确定需要加载默认值或要处理的视频文件:

    VideoCapture cap;
    if (argc==1) {
        cap = VideoCapture(0);
        if(!cap.isOpened()) {
            cout << "Couldn't find  default camera" << endl;
            return -1;
        }
    } else {
        cap.open(argv[1]);
        if(!cap.isOpened()) {
            cout << "Couldn't open image or video: " << argv[1] << endl;
            return -1;
        }
    }

(7) 如果视频捕获对象正确打开,就可以开始主循环来获取每一视频帧:

    for(;;)
    {
        Mat frame;
        cap >> frame; // 获取新帧
        if (frame.empty()) {
            waitKey();
            break;
        }

(8)DNN 算法中处理图像。准备要输入到 DNN 算法的图像,需要使用 blobFromImage 函数将 OpenCV Mat 结构转换为 DNN 结构 blobOpenCV 中使用 cv::Mat 类来存储 blob

        //! [Prepare blob]
        Mat inputBlob = blobFromImage(frame, inScaleFactor,
                                      Size(inWidth, inHeight), meanVal, false, false); //Convert Mat to batch of images

(9) 将视频帧转换为 blob 后,输入到 DNN 中并使用前向传播函数 forward 进行检测:

        //! [Set input blob]
        net.setInput(inputBlob, "data"); // 设定网络输入
        //! [Make forward pass]
        Mat detection = net.forward("detection_out"); // 计算输出
        Mat detectionMat(detection.size[2], detection.size[3], CV_32F, detection.ptr<float>());

(10) 为图像中每个检测到的人脸绘制一个矩形框并给出其置信度:

        for(int i = 0; i < detectionMat.rows; i++)
        {
            float confidence = detectionMat.at<float>(i, 2);
            if(confidence > confidenceThreshold)
            {
                int xLeftBottom = static_cast<int>(detectionMat.at<float>(i, 3) * frame.cols);
                int yLeftBottom = static_cast<int>(detectionMat.at<float>(i, 4) * frame.rows);
                int xRightTop = static_cast<int>(detectionMat.at<float>(i, 5) * frame.cols);
                int yRightTop = static_cast<int>(detectionMat.at<float>(i, 6) * frame.rows);
                Rect object((int)xLeftBottom, (int)yLeftBottom,
                            (int)(xRightTop - xLeftBottom),
                            (int)(yRightTop - yLeftBottom));
                rectangle(frame, object, Scalar(0, 255, 0));
                stringstream ss;
                ss.str("");
                ss << confidence;
                String conf(ss.str());
                String label = "Face: " + conf;
                int baseLine = 0;
                Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
                rectangle(frame, Rect(Point(xLeftBottom, yLeftBottom - labelSize.height),
                                      Size(labelSize.width, labelSize.height + baseLine)),
                          Scalar(255, 255, 255), FILLED);
                putText(frame, label, Point(xLeftBottom, yLeftBottom),
                        FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0,0,0));
            }
        }

执行以上代码,得到的检测结果如下所示:

人脸检测结果

3. 完整代码

完整代码 face_detection.cpp 如下所示:

#include <opencv2/dnn.hpp>
#include <opencv2/imgproc/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <iostream>

using namespace cv;
using namespace std;
using namespace cv::dnn;

float confidenceThreshold = 0.5;
String modelConfiguration = "deploy.prototxt";
String modelBinary = "res10_300x300_ssd_iter_140000.caffemodel";
const size_t inWidth = 300;
const size_t inHeight = 300;
const double inScaleFactor = 1.0;
const Scalar meanVal(104.0, 177.0, 123.0);

int main(int argc, char **argv) {
    dnn::Net net = readNetFromCaffe(modelConfiguration, modelBinary);
    if (net.empty()) {
        cerr << "Can't load network by using the following files: " << endl;
        cerr << "prototxt: " << modelConfiguration << endl;
        cerr << "caffemodel: " << modelBinary << endl;
        cerr << "Models are available here:" << endl;
        cerr << "<OPENCV_SRC_DIR>/samples/dnn/face_detector" << endl;
        cerr << "or here:" << endl;
        cerr << "https://github.com/opencv/opencv/tree/master/samples/dnn/face_detector" << endl;
        exit(-1);
    }
    VideoCapture cap;
    if (argc==1) {
        cap = VideoCapture(0);
        if(!cap.isOpened()) {
            cout << "Couldn't find  default camera" << endl;
            return -1;
        }
    } else {
        cap.open(argv[1]);
        if(!cap.isOpened()) {
            cout << "Couldn't open image or video: " << argv[1] << endl;
            return -1;
        }
    }
    for(;;)
    {
        Mat frame;
        cap >> frame; // 获取新帧
        if (frame.empty()) {
            waitKey();
            break;
        }
        //! [Prepare blob]
        Mat inputBlob = blobFromImage(frame, inScaleFactor,
                                      Size(inWidth, inHeight), meanVal, false, false); //Convert Mat to batch of images
        
        //! [Set input blob]
        net.setInput(inputBlob, "data"); // 设定网络输入
        //! [Make forward pass]
        Mat detection = net.forward("detection_out"); // 计算输出
        Mat detectionMat(detection.size[2], detection.size[3], CV_32F, detection.ptr<float>());
        for(int i = 0; i < detectionMat.rows; i++)
        {
            float confidence = detectionMat.at<float>(i, 2);
            if(confidence > confidenceThreshold)
            {
                int xLeftBottom = static_cast<int>(detectionMat.at<float>(i, 3) * frame.cols);
                int yLeftBottom = static_cast<int>(detectionMat.at<float>(i, 4) * frame.rows);
                int xRightTop = static_cast<int>(detectionMat.at<float>(i, 5) * frame.cols);
                int yRightTop = static_cast<int>(detectionMat.at<float>(i, 6) * frame.rows);
                Rect object((int)xLeftBottom, (int)yLeftBottom,
                            (int)(xRightTop - xLeftBottom),
                            (int)(yRightTop - yLeftBottom));
                rectangle(frame, object, Scalar(0, 255, 0));
                stringstream ss;
                ss.str("");
                ss << confidence;
                String conf(ss.str());
                String label = "Face: " + conf;
                int baseLine = 0;
                Size labelSize = getTextSize(label, FONT_HERSHEY_SIMPLEX, 0.5, 1, &baseLine);
                rectangle(frame, Rect(Point(xLeftBottom, yLeftBottom - labelSize.height),
                                      Size(labelSize.width, labelSize.height + baseLine)),
                          Scalar(255, 255, 255), FILLED);
                putText(frame, label, Point(xLeftBottom, yLeftBottom),
                        FONT_HERSHEY_SIMPLEX, 0.5, Scalar(0,0,0));
            }
        }
        imshow("detections", frame);
        if (waitKey(1) >= 0) break;
    }
    return 0;
}

小结

在本文中,我们首先通过 cv2::dnn::blobFromImage()cv2::dnn::blobFromImages() 函数了解了如何在 OpenCV 中构建网络输入 blob,然后通过实战学习将流行的深度学习模型架构应用于目标检测任务中,构建 OpenCV 计算机视觉项目。

系列链接

OpenCV实战(1)——OpenCV与图像处理基础
OpenCV实战(2)——OpenCV核心数据结构
OpenCV实战(3)——图像感兴趣区域
OpenCV实战(4)——像素操作
OpenCV实战(5)——图像运算详解
OpenCV实战(6)——OpenCV策略设计模式
OpenCV实战(7)——OpenCV色彩空间转换
OpenCV实战(8)——直方图详解
OpenCV实战(9)——基于反向投影直方图检测图像内容
OpenCV实战(10)——积分图像详解
OpenCV实战(11)——形态学变换详解
OpenCV实战(12)——图像滤波详解
OpenCV实战(13)——高通滤波器及其应用
OpenCV实战(14)——图像线条提取
OpenCV实战(15)——轮廓检测详解
OpenCV实战(16)——角点检测详解
OpenCV实战(17)——FAST特征点检测
OpenCV实战(18)——特征匹配
OpenCV实战(19)——特征描述符
OpenCV实战(20)——图像投影关系
OpenCV实战(21)——基于随机样本一致匹配图像
OpenCV实战(22)——单应性及其应用
OpenCV实战(23)——相机标定
OpenCV实战(24)——相机姿态估计
OpenCV实战(25)——3D场景重建
OpenCV实战(26)——视频序列处理
OpenCV实战(27)——追踪视频中的特征点
OpenCV实战(28)——光流估计
OpenCV实战(29)——视频对象追踪
OpenCV实战(30)——OpenCV与机器学习的碰撞
OpenCV实战(31)——基于级联Haar特征的目标检测
OpenCV实战(32)——使用SVM和定向梯度直方图执行目标检测
OpenCV实战(33)——OpenCV与深度学习的碰撞

<think>好的,我现在需要帮助用户了解Java中的构造函数,并展示具体的例子说明用法。首先,我得回顾用户之前的问题和之前的回答,确保内容连贯。 用户之前询问了构造函数的基本定义,现在转向Java的具体示例。之前的回答中,用户提供了C++的例子,所以现在需要转换到Java,同时保持结构清晰。用户提供的引用中有几个关键点:默认构造函数、带参数构造函数、构造函数链(使用this)、以及一些注意事项,比如构造函数的命名、关键字限制等。 首先,我需要明确Java构造函数的基本概念,C++的不同之处。例如,Java的构造函数必须类同名,没有返回类型,可以重载。根据引用[1],用户提到默认构造函数、带参数的构造函数和构造函数链,这些都需要涵盖。 接下来,需要组织内容结构。用户希望看到具体的例子,所以每个部分都应该有代码示例。例如,默认构造函数和带参数的构造函数,可以分别举例,并展示它们的调用和效果。引用[3]中的例子已经有一个Person类,可以借用并扩展,或者创建新的例子以增加多样性。 然后,构造函数链的部分,即在一个构造函数中调用另一个构造函数,使用this关键字。需要展示如何通过this()实现,减少代码重复,确保参数的正确传递。例如,可以设计一个Employee类,其中有一个构造函数接受姓名和年龄,另一个只接受姓名,默认年龄为0,通过this调用前者。 另外,用户提供的引用[2]中有选择题,其中提到构造方法上不能使用的关键字,比如final、static等。需要解释为什么这些关键字不能用于构造函数,可能的话在注意事项中提到。 还要注意用户提到的“每个类都有一个默认构造函数”,但实际上只有当没有定义任何构造函数时,编译器才会自动生成默认构造函数。如果用户定义了带参数的构造函数,就必须显式定义默认构造函数,否则无法使用无参构造。这一点需要澄清,避免误解。 然后,实际应用示例部分,可以设计一个DatabaseConnector类,类似于之前的C++例子,但用Java实现,展示带参数的构造函数和默认参数的效果。不过Java不支持默认参数,但可以通过方法重载来模拟,比如定义多个构造函数,其中一个调用另一个并提供默认值。 注意事项部分需要包括初始化顺序、this和super的使用限制、访问修饰符的影响等。例如,构造函数可以是public、protected或private,不同的修饰符影响对象的创建方式,单例模式常用private构造函数。 最后,相关问题部分需要根据内容生成,可能涉及构造函数的重载、私有构造函数的作用、构造函数静态工厂方法的区别等。 现在需要确保所有代码示例语法正确,符合Java规范。例如,在构造函数链中使用this()必须在第一行,成员变量初始化构造函数体内的赋值区别,以及使用this区分成员变量和参数。 可能还需要指出Java中的构造函数没有返回类型,包括void,这普通方法不同。同时,不能在构造函数上使用final,因为构造函数不能被继承或覆盖,static也不适用,因为构造函数用于实例化对象。 总结结构: 1. 基本概念:定义、特点 2. 核心用法:默认、带参、构造函数链 3. 示例代码:每个用法对应示例 4. 注意事项:关键字限制、初始化顺序、访问控制 5. 相关问题 需要确保每个部分都有明确的示例和解释,引用提供的资料,但根据用户当前需求调整,突出Java的特性。检查是否有遗漏点,如构造函数类同名、没有返回类型、自动调用等,确保覆盖所有用户可能关心的问题。</think>### Java构造函数详解示例 #### 一、构造函数基本概念 **构造函数**是Java中用于初始化对象的特殊方法,具有以下核心特性: 1. **类同名**:例如类名为`Person`,构造函数名称也为`Person` 2. **无返回类型**(包括`void`) 3. **自动调用**:对象通过`new`创建时自动执行 4. **初始化控制**:为成员变量赋初值,确保对象合法状态[^1] #### 二、核心用法示例 ##### 1. 默认构造函数(无参构造) 当未定义任何构造函数时,编译器自动生成空实现的默认构造函数。若已定义其他构造函数,需显式声明默认构造: ```java public class Book { private String title; // 显式定义的默认构造函数 public Book() { this.title = "Untitled"; // 初始化默认值 System.out.println("默认构造函数调用"); } } ``` ##### 2. 带参数构造函数 通过参数传递实现灵活初始化: ```java public class Student { private String name; private int score; // 带参数的构造函数 public Student(String name, int score) { this.name = name; // 使用this区分成员变量参数 this.score = score; System.out.println("带参构造调用:" + name); } } ``` ##### 3. 构造函数链(Constructor Chaining) 使用`this()`调用同类其他构造函数,减少代码重复: ```java public class Employee { private String id; private String department; // 三参数构造函数(完整初始化) public Employee(String id, String name, String dept) { this.id = id; this.name = name; this.department = dept; } // 两参数构造函数(默认部门为"General") public Employee(String id, String name) { this(id, name, "General"); // 调用三参构造 } // 单参数构造函数(默认部门+自动生成ID) public Employee(String name) { this(generateID(), name); // 调用两参构造 } private static String generateID() { return "EMP" + System.currentTimeMillis(); } } ``` #### 三、实际应用示例 ```java public class BankAccount { private String accountNumber; private double balance; // 默认构造函数(生成随机账号) public BankAccount() { this.accountNumber = generateAccountNumber(); this.balance = 0.0; } // 带初始余额的构造函数 public BankAccount(double initialBalance) { this(); // 调用默认构造生成账号 this.balance = initialBalance; } private String generateAccountNumber() { return "ACC-" + UUID.randomUUID().toString().substring(0, 8); } public void display() { System.out.println("Account: " + accountNumber + ", Balance: $" + balance); } public static void main(String[] args) { BankAccount acc1 = new BankAccount(); // 使用默认构造 acc1.display(); // 输出示例:Account: ACC-3f4a1b, Balance: $0.0 BankAccount acc2 = new BankAccount(500.0); acc2.display(); // 输出示例:Account: ACC-7c82d9, Balance: $500.0 } } ``` #### 四、关键注意事项 1. **初始化顺序** - 静态初始化块 → 实例初始化块 → 构造函数 - 成员变量初始化优先于构造函数体执行[^3] 2. **关键字限制** - 构造函数**不能**使用`final`、`static`、`abstract`等修饰符 - `synchronized`和`native`虽然语法允许,但实际极少使用[^2] 3. **访问控制** - `private`构造函数用于单例模式: ```java public class Singleton { private static Singleton instance; private Singleton() {} // 禁止外部实例化 public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } } ``` 4. **继承关系** - 子类构造函数默认调用父类无参构造(`super()`) - 需显式调用父类构造时,`super(...)`必须是子类构造的第一条语句: ```java public class Manager extends Employee { private int teamSize; public Manager(String name, int teamSize) { super(name, "Management"); // 先调用父类构造 this.teamSize = teamSize; } } ``` #### 五、常见错误示例 ```java // 错误1:尝试返回类型 public class ErrorExample1 { public void ErrorExample1() { // 实际是普通方法,非构造函数! System.out.println("这不是构造函数"); } } // 错误2:误用static public class ErrorExample2 { public static ErrorExample2() { // 编译错误:构造函数不能是static } } // 错误3:构造函数递归调用 public class ErrorExample3 { public ErrorExample3() { this(); // 编译错误:递归调用自身 } } ``` --相关问题-- 1. 如何在子类构造函数中正确调用父类构造函数? 2. 单例模式为什么要用private构造函数? 3. 构造函数实例初始化块有何区别? 4. 为什么Java不允许构造函数定义返回类型?
评论 87
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

盼小辉丶

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值