摘要
本文深入讲解 OpenCV DNN(Deep Neural Network)模块的应用,涵盖模型加载、图像预处理、网络推理、结果后处理等完整流程。详细介绍如何使用 OpenCV 部署 YOLO、SSD、Faster R-CNN 等主流目标检测模型,以及 ResNet、VGG 等分类网络。通过丰富的实践案例,帮助读者掌握基于 OpenCV 的深度学习模型部署技术,实现高效的计算机视觉应用。
关键词:OpenCV、DNN、深度学习、目标检测、YOLO、模型部署、Python
目录
- [一、DNN 模块概述](#一 dnn 模块概述)
- 二、环境配置与模型准备
- [三、DNN 模块基础使用](#三 dnn 模块基础使用)
- 四、图像分类实战
- 五、目标检测实战
- 六、人脸检测与识别
- 七、语义分割实战
- 八、性能优化与部署
- 九、常见问题解答
- 十、总结与扩展
- 参考资料
一、DNN 模块概述
1.1 什么是 OpenCV DNN
OpenCV DNN 模块是一个高效的深度学习推理引擎,支持多种主流深度学习框架训练的模型。
#mermaid-svg-I7gvyk5cSOIVhLeK{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-I7gvyk5cSOIVhLeK .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-I7gvyk5cSOIVhLeK .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-I7gvyk5cSOIVhLeK .error-icon{fill:#552222;}#mermaid-svg-I7gvyk5cSOIVhLeK .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-I7gvyk5cSOIVhLeK .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-I7gvyk5cSOIVhLeK .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-I7gvyk5cSOIVhLeK .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-I7gvyk5cSOIVhLeK .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-I7gvyk5cSOIVhLeK .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-I7gvyk5cSOIVhLeK .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-I7gvyk5cSOIVhLeK .marker{fill:#333333;stroke:#333333;}#mermaid-svg-I7gvyk5cSOIVhLeK .marker.cross{stroke:#333333;}#mermaid-svg-I7gvyk5cSOIVhLeK svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-I7gvyk5cSOIVhLeK p{margin:0;}#mermaid-svg-I7gvyk5cSOIVhLeK .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-I7gvyk5cSOIVhLeK .cluster-label text{fill:#333;}#mermaid-svg-I7gvyk5cSOIVhLeK .cluster-label span{color:#333;}#mermaid-svg-I7gvyk5cSOIVhLeK .cluster-label span p{background-color:transparent;}#mermaid-svg-I7gvyk5cSOIVhLeK .label text,#mermaid-svg-I7gvyk5cSOIVhLeK span{fill:#333;color:#333;}#mermaid-svg-I7gvyk5cSOIVhLeK .node rect,#mermaid-svg-I7gvyk5cSOIVhLeK .node circle,#mermaid-svg-I7gvyk5cSOIVhLeK .node ellipse,#mermaid-svg-I7gvyk5cSOIVhLeK .node polygon,#mermaid-svg-I7gvyk5cSOIVhLeK .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-I7gvyk5cSOIVhLeK .rough-node .label text,#mermaid-svg-I7gvyk5cSOIVhLeK .node .label text,#mermaid-svg-I7gvyk5cSOIVhLeK .image-shape .label,#mermaid-svg-I7gvyk5cSOIVhLeK .icon-shape .label{text-anchor:middle;}#mermaid-svg-I7gvyk5cSOIVhLeK .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-I7gvyk5cSOIVhLeK .rough-node .label,#mermaid-svg-I7gvyk5cSOIVhLeK .node .label,#mermaid-svg-I7gvyk5cSOIVhLeK .image-shape .label,#mermaid-svg-I7gvyk5cSOIVhLeK .icon-shape .label{text-align:center;}#mermaid-svg-I7gvyk5cSOIVhLeK .node.clickable{cursor:pointer;}#mermaid-svg-I7gvyk5cSOIVhLeK .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-I7gvyk5cSOIVhLeK .arrowheadPath{fill:#333333;}#mermaid-svg-I7gvyk5cSOIVhLeK .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-I7gvyk5cSOIVhLeK .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-I7gvyk5cSOIVhLeK .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I7gvyk5cSOIVhLeK .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-I7gvyk5cSOIVhLeK .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I7gvyk5cSOIVhLeK .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-I7gvyk5cSOIVhLeK .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-I7gvyk5cSOIVhLeK .cluster text{fill:#333;}#mermaid-svg-I7gvyk5cSOIVhLeK .cluster span{color:#333;}#mermaid-svg-I7gvyk5cSOIVhLeK div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-I7gvyk5cSOIVhLeK .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-I7gvyk5cSOIVhLeK rect.text{fill:none;stroke-width:0;}#mermaid-svg-I7gvyk5cSOIVhLeK .icon-shape,#mermaid-svg-I7gvyk5cSOIVhLeK .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-I7gvyk5cSOIVhLeK .icon-shape p,#mermaid-svg-I7gvyk5cSOIVhLeK .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-I7gvyk5cSOIVhLeK .icon-shape rect,#mermaid-svg-I7gvyk5cSOIVhLeK .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-I7gvyk5cSOIVhLeK .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-I7gvyk5cSOIVhLeK .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-I7gvyk5cSOIVhLeK :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
深度学习框架
模型导出
Caffe
TensorFlow
PyTorch/ONNX
Darknet
MXNet
OpenCV DNN
推理引擎
CPU/GPU 加速
应用层
目标检测
图像分类
语义分割
姿态估计
1.2 DNN 模块优势
为什么选择 OpenCV DNN?
- CPU(默认)
- CUDA(NVIDIA GPU)
- OpenCL(异构计算)
- Intel Inference Engine
- Vulkan
1.3 支持的模型格式
| Caffe | .caffemodel + .prototxt | 需要网络和权重文件 |
| TensorFlow | .pb | Frozen Graph 格式 |
| PyTorch | .onnx | 需导出为 ONNX 格式 |
| Darknet | .weights + .cfg | YOLO 系列使用 |
| MXNet | .params + .json | 较少使用 |
二、环境配置与模型准备
2.1 安装 OpenCV
# 方法 1:pip 安装(推荐)
pip install opencv-contrib-python
# 方法 2:从源码编译(支持 CUDA 等高级特性)
git clone https://github.com/opencv/opencv.git
cd opencv
mkdir build && cd build
cmake -D CMAKE_BUILD_TYPE=RELEASE \\
-D CMAKE_INSTALL_PREFIX=/usr/local \\
-D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib/modules \\
-D WITH_CUDA=ON \\
-D ENABLE_FAST_MATH=1 \\
-D CUDA_FAST_MATH=1 \\
-D WITH_CUBLAS=1 \\
..
make -j$(nproc)
sudo make install
2.2 验证 DNN 模块
import cv2 as cv
import numpy as np
print(f"OpenCV 版本:{cv.__version__}")
print(f"DNN 后端:{cv.getBuildInformation()}")
# 检查可用的 DNN 后端
backends = {
cv.dnn.DNN_BACKEND_DEFAULT: "默认",
cv.dnn.DNN_BACKEND_HALIDE: "Halide",
cv.dnn.DNN_BACKEND_INFERENCE_ENGINE: "OpenVINO",
cv.dnn.DNN_BACKEND_OPENCV: "OpenCV",
cv.dnn.DNN_BACKEND_VKCOM: "Vulkan",
cv.dnn.DNN_BACKEND_CUDA: "CUDA",
}
targets = {
cv.dnn.DNN_TARGET_CPU: "CPU",
cv.dnn.DNN_TARGET_OPENCL: "OpenCL",
cv.dnn.DNN_TARGET_OPENCL_FP16: "OpenCL FP16",
cv.dnn.DNN_TARGET_MYRIAD: "Myriad",
cv.dnn.DNN_TARGET_HDDL: "HDDL",
cv.dnn.DNN_TARGET_VULKAN: "Vulkan",
cv.dnn.DNN_TARGET_CUDA: "CUDA",
cv.dnn.DNN_TARGET_CUDA_FP16: "CUDA FP16",
}
print("\\n可用的 DNN 后端:")
for backend_id, name in backends.items():
try:
cv.setUseOptimized(True)
print(f" ✓ {name}")
except:
print(f" ✗ {name} (不可用)")
print("\\n可用的计算目标:")
for target_id, name in targets.items():
print(f" – {name}")
2.3 下载预训练模型
import os
from pathlib import Path
def download_models():
"""下载常用预训练模型"""
models_dir = Path("models")
models_dir.mkdir(exist_ok=True)
# 图像分类模型
classification_models = {
"ResNet-50": {
"url": "https://raw.githubusercontent.com/opencv/opencv_3rdparty/dnn_samples_face/deploy.prototxt",
"weights": "https://raw.githubusercontent.com/opencv/opencv_3rdparty/dnn_samples_face/res10_300x300_ssd_iter_140000.caffemodel"
},
"MobileNet-SSD": {
"url": "https://raw.githubusercontent.com/chuanqi305/MobileNet-SSD/master/MobileNetSSD_deploy.prototxt",
"weights": "https://github.com/chuanqi305/MobileNet-SSD/raw/master/MobileNetSSD_deploy.caffemodel"
}
}
# YOLO 目标检测模型
yolo_models = {
"YOLOv3-tiny": {
"cfg": "https://raw.githubusercontent.com/pjreddie/darknet/master/cfg/yolov3-tiny.cfg",
"weights": "https://pjreddie.com/media/files/yolov3-tiny.weights"
},
"YOLOv4-tiny": {
"cfg": "https://raw.githubusercontent.com/AlexeyAB/darknet/master/cfg/yolov4-tiny.cfg",
"weights": "https://github.com/AlexeyAB/darknet/releases/download/yolov4/yolov4-tiny.weights"
}
}
print("请手动下载模型文件到 models/ 目录")
print("或者使用以下命令自动下载:")
print("\\n# 分类模型")
print("wget -P models/ <URL>")
print("\\n# YOLO 模型")
print("wget -P models/ <URL>")
return models_dir
models_directory = download_models()
三、DNN 模块基础使用
3.1 读取网络的基本流程
import cv2 as cv
def load_network_basic():
"""加载网络基础示例"""
# 1. 读取网络
net = cv.dnn.readNetFromCaffe(
"deploy.prototxt", # 网络配置文件
"model.caffemodel" # 权重文件
)
# 或使用通用接口
net = cv.dnn.readNet(
"model.pb", # 模型文件
"config.pbtxt", # 配置文件(可选)
"tensorflow" # 框架名称
)
# 2. 设置后端和目标设备
net.setPreferableBackend(cv.dnn.DNN_BACKEND_CUDA) # 使用 CUDA
net.setPreferableTarget(cv.dnn.DNN_TARGET_CUDA) # GPU 推理
# 3. 准备输入
image = cv.imread("image.jpg")
blob = cv.dnn.blobFromImage(
image, # 输入图像
scalefactor=1.0, # 缩放因子
size=(224, 224), # 输入尺寸
mean=(104.0, 177.0, 123.0), # 均值减法
swapRB=False, # 是否交换 RGB 通道
crop=False # 是否裁剪
)
# 4. 设置输入
net.setInput(blob)
# 5. 前向传播
output = net.forward()
return output
print("DNN 基本使用流程已展示")
3.2 blobFromImage 详解
def blob_from_image_demo():
"""blobFromImage 参数详解"""
image = cv.imread("lena.jpg")
# 参数详细说明
blob = cv.dnn.blobFromImage(
image=image, # 输入图像(NumPy 数组)
scalefactor=1/255.0, # 归一化因子(0-1)
size=(224, 224), # 输出尺寸(宽,高)
mean=(0, 0, 0), # 各通道均值
swapRB=True, # BGR <-> RGB 转换
crop=False, # 中心裁剪
borderType=cv.BORDER_CONSTANT, # 边界填充类型
value=None # 填充值
)
print(f"原始图像形状:{image.shape}")
print(f"Blob 形状:{blob.shape}") # (batch_size, channels, height, width)
print(f"Blob 数据类型:{blob.dtype}")
print(f"Blob 数值范围:[{blob.min():.3f}, {blob.max():.3f}]")
return blob
blob_example = blob_from_image_demo()
3.3 获取网络信息
def inspect_network(net):
"""检查网络结构和层信息"""
# 获取网络层名称
layer_names = net.getLayerNames()
print(f"网络总层数:{len(layer_names)}")
# 获取未连接的输出层
output_layers = net.getUnconnectedOutLayersNames()
print(f"输出层:{output_layers}")
# 获取特定层信息
for i, layer_name in enumerate(layer_names[:5]): # 只显示前 5 层
layer = net.getLayer(i)
print(f"\\n第{i}层:{layer_name}")
print(f" 类型:{layer.type}")
print(f" 输入形状:{layer.inputNameToIndex('')}")
# 获取网络参数
params = net.params()
if params:
print(f"\\n网络参数数量:{len(params)}")
return layer_names, output_layers
四、图像分类实战
4.1 ResNet 图像分类
import cv2 as cv
import numpy as np
class ImageClassifier:
"""图像分类器"""
def __init__(self, model_path, config_path, classes_path):
"""
初始化分类器
Args:
model_path: 模型权重文件路径
config_path: 网络配置文件路径
classes_path: 类别标签文件路径
"""
# 加载网络
self.net = cv.dnn.readNet(model_path, config_path, 'caffe')
# 设置后端(可修改)
self.net.setPreferableBackend(cv.dnn.DNN_BACKEND_OPENCV)
self.net.setPreferableTarget(cv.dnn.DNN_TARGET_CPU)
# 加载类别标签
with open(classes_path, 'r') as f:
self.classes = [line.strip() for line in f.readlines()]
print(f"成功加载 {len(self.classes)} 个类别")
def predict(self, image_path, top_k=5):
"""
预测图像类别
Args:
image_path: 图像路径
top_k: 返回概率最高的 K 个类别
Returns:
预测结果列表 [(类别名,概率), …]
"""
# 读取图像
image = cv.imread(image_path)
if image is None:
raise FileNotFoundError(f"无法读取图像:{image_path}")
# 预处理
blob = cv.dnn.blobFromImage(
image,
scalefactor=1.0,
size=(224, 224),
mean=(104.0, 177.0, 123.0), # ImageNet 均值
swapRB=False,
crop=False
)
# 前向传播
self.net.setInput(blob)
outputs = self.net.forward()
# 获取预测结果
probabilities = outputs[0]
# 排序获取 top-k
top_indices = np.argsort(probabilities)[::–1][:top_k]
results = []
for idx in top_indices:
class_name = self.classes[idx]
probability = probabilities[idx] * 100
results.append((class_name, probability))
return results
def predict_batch(self, image_paths, batch_size=8):
"""批量预测"""
results = []
for i in range(0, len(image_paths), batch_size):
batch_paths = image_paths[i:i+batch_size]
# 读取并预处理批次图像
images = []
for path in batch_paths:
img = cv.imread(path)
if img is not None:
images.append(img)
if not images:
continue
# 创建 blob
blob = cv.dnn.blobFromImages(
images,
scalefactor=1.0,
size=(224, 224),
mean=(104.0, 177.0, 123.0),
swapRB=False
)
# 前向传播
self.net.setInput(blob)
outputs = self.net.forward()
# 处理结果
for j, output in enumerate(outputs):
top_idx = np.argmax(output)
results.append({
'image': batch_paths[j],
'class': self.classes[top_idx],
'probability': float(output[top_idx])
})
return results
def main():
"""主函数示例"""
# 初始化分类器
classifier = ImageClassifier(
model_path='models/resnet50.caffemodel',
config_path='models/resnet50.prototxt',
classes_path='models/imagenet_classes.txt'
)
# 单张图像预测
results = classifier.predict('test_image.jpg', top_k=5)
print("\\n预测结果:")
print("-" * 50)
for i, (class_name, prob) in enumerate(results, 1):
print(f"{i}. {class_name}: {prob:.2f}%")
# 可视化结果
image = cv.imread('test_image.jpg')
# 在图像上显示预测结果
y_offset = 30
for i, (class_name, prob) in enumerate(results[:3], 1):
text = f"{i}. {class_name}: {prob:.2f}%"
cv.putText(image, text, (10, y_offset + i*30),
cv.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
cv.imshow('Classification Result', image)
cv.waitKey(0)
cv.destroyAllWindows()
if __name__ == '__main__':
main()
4.2 MobileNet 轻量级分类
class MobileNetClassifier(ImageClassifier):
"""MobileNet 轻量级分类器"""
def __init__(self):
super().__init__(
model_path='models/MobileNetSSD_deploy.caffemodel',
config_path='models/MobileNetSSD_deploy.prototxt',
classes_path='models/mobilenet_classes.txt'
)
# MobileNet 专用参数
self.input_size = 224
self.mean = (127.5, 127.5, 127.5) # 不同的均值
self.scale = 0.007843 # 归一化到 [-1, 1]
def predict(self, image_path, confidence_threshold=0.5):
"""MobileNet 预测(针对 SSD 优化)"""
image = cv.imread(image_path)
# 预处理
blob = cv.dnn.blobFromImage(
image,
scalefactor=self.scale,
size=(self.input_size, self.input_size),
mean=self.mean,
swapRB=True
)
self.net.setInput(blob)
detections = self.net.forward()
# 解析检测结果
results = []
h, w = image.shape[:2]
for i in range(detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > confidence_threshold:
class_id = int(detections[0, 0, i, 1])
class_name = self.classes[class_id]
# 获取边界框
left = int(detections[0, 0, i, 3] * w)
top = int(detections[0, 0, i, 4] * h)
right = int(detections[0, 0, i, 5] * w)
bottom = int(detections[0, 0, i, 6] * h)
results.append({
'class': class_name,
'confidence': float(confidence),
'bbox': (left, top, right, bottom)
})
return results
4.3 自定义模型训练与部署
def train_and_export_model():
"""训练自定义模型并导出为 ONNX 格式"""
# PyTorch 示例代码
pytorch_code = """
import torch
import torch.nn as nn
import torchvision.models as models
# 1. 加载预训练模型
model = models.resnet18(pretrained=True)
# 2. 修改最后的全连接层(自定义类别数)
num_classes = 10 # 你的类别数
model.fc = nn.Linear(model.fc.in_features, num_classes)
# 3. 训练模型(省略训练代码)
# … training code here …
# 4. 导出为 ONNX
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(
model,
dummy_input,
"custom_model.onnx",
export_params=True,
opset_version=11,
do_constant_folding=True,
input_names=['input'],
output_names=['output'],
dynamic_axes={
'input': {0: 'batch_size'},
'output': {0: 'batch_size'}
}
)
"""
print("PyTorch 模型导出为 ONNX 示例:")
print(pytorch_code)
# OpenCV 加载 ONNX 模型
onnx_net = cv.dnn.readNetFromONNX('custom_model.onnx')
print("ONNX 模型加载成功!")
return onnx_net
五、目标检测实战
5.1 YOLOv3 目标检测
class YOLODetector:
"""YOLO 目标检测器"""
def __init__(self, cfg_path, weights_path, names_path,
confidence_threshold=0.5, nms_threshold=0.4):
"""
初始化 YOLO 检测器
Args:
cfg_path: YOLO 配置文件路径
weights_path: YOLO 权重文件路径
names_path: 类别名称文件路径
confidence_threshold: 置信度阈值
nms_threshold: NMS 阈值
"""
# 加载网络
self.net = cv.dnn.readNetFromDarknet(cfg_path, weights_path)
# 设置后端
self.net.setPreferableBackend(cv.dnn.DNN_BACKEND_OPENCV)
self.net.setPreferableTarget(cv.dnn.DNN_TARGET_CPU)
# 加载类别名称
with open(names_path, 'r') as f:
self.classes = [line.strip() for line in f.readlines()]
# 获取输出层名称
self.layer_names = self.net.getLayerNames()
self.output_layers = [
self.layer_names[i[0] – 1]
for i in self.net.getUnconnectedOutLayers()
]
# 生成颜色(用于可视化)
np.random.seed(42)
self.colors = np.random.randint(
0, 255, size=(len(self.classes), 3), dtype='uint8'
)
self.conf_thresh = confidence_threshold
self.nms_thresh = nms_threshold
print(f"YOLO 检测器初始化成功,共{len(self.classes)}个类别")
def detect(self, image_path):
"""
执行目标检测
Args:
image_path: 图像路径或 NumPy 数组
Returns:
检测结果列表
"""
# 读取图像
if isinstance(image_path, str):
image = cv.imread(image_path)
else:
image = image_path.copy()
if image is None:
raise ValueError("无法读取图像")
height, width = image.shape[:2]
# 创建 blob
blob = cv.dnn.blobFromImage(
image,
1/255.0,
(416, 416),
swapRB=True,
crop=False
)
# 前向传播
self.net.setInput(blob)
outputs = self.net.forward(self.output_layers)
# 合并多个尺度的输出
boxes = []
confidences = []
class_ids = []
for output in outputs:
for detection in output:
scores = detection[5:]
class_id = np.argmax(scores)
confidence = scores[class_id]
if confidence > self.conf_thresh:
# 检测框坐标
center_x = int(detection[0] * width)
center_y = int(detection[1] * height)
w = int(detection[2] * width)
h = int(detection[3] * height)
# 转换为左上角坐标
x = int(center_x – w / 2)
y = int(center_y – h / 2)
boxes.append([x, y, w, h])
confidences.append(float(confidence))
class_ids.append(class_id)
# 应用 NMS
indices = cv.dnn.NMSBoxes(
boxes, confidences,
self.conf_thresh,
self.nms_thresh
)
# 整理结果
results = []
if len(indices) > 0:
for i in indices.flatten():
x, y, w, h = boxes[i]
results.append({
'class': self.classes[class_ids[i]],
'class_id': class_ids[i],
'confidence': confidences[i],
'bbox': (x, y, w, h)
})
return results
def draw_detections(self, image, results):
"""绘制检测结果"""
for result in results:
x, y, w, h = result['bbox']
class_id = result['class_id']
color = [int(c) for c in self.colors[class_id]]
# 绘制边界框
cv.rectangle(image, (x, y), (x + w, y + h), color, 2)
# 绘制标签
label = f"{result['class']}: {result['confidence']:.2f}"
label_size, base_line = cv.getTextSize(
label, cv.FONT_HERSHEY_SIMPLEX, 0.5, 1
)
y_label = max(y, label_size[1])
cv.rectangle(
image,
(x, y_label – label_size[1]),
(x + label_size[0], y_label + base_line),
color,
cv.FILLED
)
cv.putText(
image, label, (x, y_label),
cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1
)
return image
def detect_and_show(self, image_path):
"""检测并显示结果"""
image = cv.imread(image_path)
results = self.detect(image)
# 绘制结果
result_image = self.draw_detections(image, results)
# 显示统计信息
print(f"\\n检测到 {len(results)} 个目标:")
for i, result in enumerate(results, 1):
print(f"{i}. {result['class']} ({result['confidence']:.2f})")
# 显示图像
cv.imshow('YOLO Detection', result_image)
cv.waitKey(0)
cv.destroyAllWindows()
return result_image, results
def main_yolo():
"""YOLO 检测主函数"""
detector = YOLODetector(
cfg_path='models/yolov3.cfg',
weights_path='models/yolov3.weights',
names_path='models/coco.names',
confidence_threshold=0.5,
nms_threshold=0.4
)
result_image, results = detector.detect_and_show('street.jpg')
# 保存结果
cv.imwrite('detection_result.jpg', result_image)
print(f"结果已保存到 detection_result.jpg")
if __name__ == '__main__':
main_yolo()
5.2 YOLOv8 实时检测
class YOLOv8Detector:
"""YOLOv8 检测器(使用 ONNX 模型)"""
def __init__(self, onnx_model_path, confidence_threshold=0.4):
"""加载 YOLOv8 ONNX 模型"""
self.net = cv.dnn.readNetFromONNX(onnx_model_path)
self.conf_thresh = confidence_threshold
# COCO 数据集类别
self.classes = [
'person', 'bicycle', 'car', 'motorcycle', 'airplane', 'bus',
'train', 'truck', 'boat', 'traffic light', 'fire hydrant',
# … 更多类别
]
# 生成颜色
np.random.seed(42)
self.colors = np.random.randint(0, 255, size=(len(self.classes), 3))
def detect(self, image):
"""YOLOv8 检测"""
h, w = image.shape[:2]
# 预处理
blob = cv.dnn.blobFromImage(
image, 1/255.0, (640, 640),
swapRB=True, crop=False
)
self.net.setInput(blob)
outputs = self.net.forward()
# YOLOv8 输出格式处理
outputs = outputs.transpose(0, 2, 1)
boxes = []
confidences = []
class_ids = []
for detection in outputs[0]:
scores = detection[4:]
class_id = np.argmax(scores)
confidence = scores[class_id]
if confidence > self.conf_thresh:
cx, cy, bw, bh = detection[:4]
# 转换为像素坐标
x = int((cx – bw/2) * w)
y = int((cy – bh/2) * h)
box_w = int(bw * w)
box_h = int(bh * h)
boxes.append([x, y, box_w, box_h])
confidences.append(float(confidence))
class_ids.append(class_id)
# NMS
indices = cv.dnn.NMSBoxes(
boxes, confidences,
self.conf_thresh, 0.45
)
results = []
if len(indices) > 0:
for i in indices.flatten():
results.append({
'class': self.classes[class_ids[i]],
'confidence': confidences[i],
'bbox': tuple(boxes[i])
})
return results
def process_video(self, video_source=0):
"""处理视频流"""
cap = cv.VideoCapture(video_source)
if not cap.isOpened():
print("无法打开视频源")
return
frame_count = 0
fps_counter = []
while True:
ret, frame = cap.read()
if not ret:
break
# 检测
start_time = cv.getTickCount()
results = self.detect(frame)
end_time = cv.getTickCount()
# 计算 FPS
fps = cv.getTickFrequency() / (end_time – start_time)
fps_counter.append(fps)
avg_fps = np.mean(fps_counter[–30:]) # 最近 30 帧平均
# 绘制结果
for result in results:
x, y, bw, bh = result['bbox']
color = [int(c) for c in self.colors[self.classes.index(result['class'])]]
cv.rectangle(frame, (x, y), (x+bw, y+bh), color, 2)
cv.putText(frame, f"{result['class']} {result['confidence']:.2f}",
(x, y–10), cv.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
# 显示 FPS
cv.putText(frame, f"FPS: {avg_fps:.1f}", (10, 30),
cv.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
cv.imshow('YOLOv8 Detection', frame)
if cv.waitKey(1) & 0xFF == ord('q'):
break
frame_count += 1
cap.release()
cv.destroyAllWindows()
print(f"处理完成,平均 FPS: {avg_fps:.1f}")
5.3 SSD 目标检测
class SSDDetector:
"""SSD 目标检测器"""
def __init__(self, prototxt_path, caffe_model_path):
"""加载 SSD 模型"""
self.net = cv.dnn.readNetFromCaffe(prototxt_path, caffe_model_path)
# SSD 类别映射
self.CLASSES = {
0: '__background__',
1: 'aeroplane', 2: 'bicycle', 3: 'bird', 4: 'boat',
5: 'bottle', 6: 'bus', 7: 'car', 8: 'cat', 9: 'chair',
10: 'cow', 11: 'diningtable', 12: 'dog', 13: 'horse',
14: 'motorbike', 15: 'person', 16: 'pottedplant',
17: 'sheep', 18: 'sofa', 19: 'train', 20: 'tvmonitor'
}
self.confidence_threshold = 0.5
def detect(self, image):
"""SSD 检测"""
h, w = image.shape[:2]
# 预处理
blob = cv.dnn.blobFromImage(
cv.resize(image, (300, 300)),
1.0, (300, 300),
(104.0, 177.0, 123.0)
)
self.net.setInput(blob)
detections = self.net.forward()
results = []
# 解析检测结果
for i in range(detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > self.confidence_threshold:
class_id = int(detections[0, 0, i, 1])
# 恢复边界框坐标
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
(startX, startY, endX, endY) = box.astype("int")
results.append({
'class': self.CLASSES[class_id],
'confidence': float(confidence),
'bbox': (startX, startY, endX – startX, endY – startY)
})
return results
六、人脸检测与识别
6.1 DNN 人脸检测
class FaceDetector:
"""基于 DNN 的人脸检测器"""
def __init__(self):
"""加载人脸检测模型"""
# 使用 ResNet-10 SSD 人脸检测模型
self.net = cv.dnn.readNetFromCaffe(
'models/deploy.prototxt',
'models/res10_300x300_ssd_iter_140000.caffemodel'
)
self.confidence_threshold = 0.7
def detect_faces(self, image):
"""
检测人脸
Returns:
faces: 人脸列表 [(x, y, w, h, confidence), …]
"""
h, w = image.shape[:2]
# 预处理
blob = cv.dnn.blobFromImage(
cv.resize(image, (300, 300)),
1.0, (300, 300),
(104.0, 177.0, 123.0),
swapRB=False
)
self.net.setInput(blob)
detections = self.net.forward()
faces = []
for i in range(detections.shape[2]):
confidence = detections[0, 0, i, 2]
if confidence > self.confidence_threshold:
box = detections[0, 0, i, 3:7] * np.array([w, h, w, h])
(startX, startY, endX, endY) = box.astype("int")
faces.append({
'bbox': (startX, startY, endX – startX, endY – startY),
'confidence': float(confidence)
})
return faces
def draw_faces(self, image, faces):
"""绘制人脸检测框"""
for face in faces:
x, y, bw, bh = face['bbox']
confidence = face['confidence']
# 绘制边界框
cv.rectangle(image, (x, y), (x+bw, y+bh), (0, 255, 0), 2)
# 绘制标签
label = f"Face: {confidence:.2f}"
cv.putText(image, label, (x, y–10),
cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2)
return image
6.2 人脸识别系统
class FaceRecognitionSystem:
"""人脸识别系统"""
def __init__(self):
"""初始化人脸识别系统"""
self.face_detector = FaceDetector()
self.recognition_net = cv.dnn.readNet(
'models/openface.nn4.small2.v1.t7',
'models/openface.nn4.small2.v1.prototxt',
'torch'
)
self.known_faces = {} # 存储已知人脸特征
def register_face(self, name, image_path):
"""注册人脸"""
image = cv.imread(image_path)
faces = self.face_detector.detect_faces(image)
if not faces:
print("未检测到人脸")
return False
# 提取最大人脸的特征
largest_face = max(faces, key=lambda f: f['bbox'][2] * f['bbox'][3])
embedding = self.get_embedding(image, largest_face['bbox'])
self.known_faces[name] = embedding
print(f"成功注册:{name}")
return True
def get_embedding(self, image, bbox):
"""获取人脸特征向量"""
x, y, w, h = bbox
face_roi = image[y:y+h, x:x+w]
# 预处理
blob = cv.dnn.blobFromImage(
face_roi,
1.0/255,
(96, 96),
(0, 0, 0),
swapRB=True
)
self.recognition_net.setInput(blob)
embedding = self.recognition_net.forward()
return embedding.flatten()
def recognize(self, image_path):
"""识别人脸"""
image = cv.imread(image_path)
faces = self.face_detector.detect_faces(image)
results = []
for face in faces:
embedding = self.get_embedding(image, face['bbox'])
# 计算与已知人脸的距离
min_distance = float('inf')
recognized_name = "Unknown"
for name, known_embedding in self.known_faces.items():
distance = np.linalg.norm(embedding – known_embedding)
if distance < min_distance:
min_distance = distance
recognized_name = name
# 判断是否认识
if min_distance < 0.6: # 阈值
label = f"{recognized_name} ({min_distance:.2f})"
color = (0, 255, 0)
else:
label = "Unknown"
color = (0, 0, 255)
results.append({
'name': recognized_name,
'distance': min_distance,
'bbox': face['bbox']
})
# 绘制结果
x, y, w, h = face['bbox']
cv.rectangle(image, (x, y), (x+w, y+h), color, 2)
cv.putText(image, label, (x, y–10),
cv.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)
cv.imshow('Face Recognition', image)
cv.waitKey(0)
cv.destroyAllWindows()
return results
七、语义分割实战
7.1 DeepLabv3+ 语义分割
class SemanticSegmenter:
"""语义分割器"""
def __init__(self, model_path, config_path, classes_path):
"""初始化分割网络"""
self.net = cv.dnn.readNet(model_path, config_path, 'tensorflow')
# 加载类别
with open(classes_path, 'r') as f:
self.classes = [line.strip() for line in f.readlines()]
# 生成颜色
np.random.seed(42)
self.colors = np.random.randint(0, 255, size=(len(self.classes), 3))
def segment(self, image_path):
"""执行语义分割"""
image = cv.imread(image_path)
h, w = image.shape[:2]
# 预处理
blob = cv.dnn.blobFromImage(
image,
1.0,
(513, 513),
(123.68, 116.78, 103.94),
swapRB=False,
crop=False
)
self.net.setInput(blob)
output = self.net.forward()
# 调整输出尺寸
output = output[0, :, :, :]
output = cv.resize(output, (w, h), interpolation=cv.INTER_LINEAR)
# 获取每个像素的类别
segmentation_map = np.argmax(output, axis=2)
# 创建彩色掩码
colored_mask = np.zeros_like(image)
for class_id in range(len(self.classes)):
mask = segmentation_map == class_id
colored_mask[mask] = self.colors[class_id]
# 融合原图和分割结果
result = cv.addWeighted(image, 0.6, colored_mask, 0.4, 0)
return result, segmentation_map
def create_overlay(self, image, segmentation_map, alpha=0.5):
"""创建半透明叠加效果"""
colored_mask = np.zeros_like(image)
for class_id in range(len(self.classes)):
mask = segmentation_map == class_id
colored_mask[mask] = self.colors[class_id]
overlay = cv.addWeighted(image, 1–alpha, colored_mask, alpha, 0)
return overlay
八、性能优化与部署
8.1 后端性能对比
def benchmark_backends(model_path, image_path):
"""对比不同后端性能"""
image = cv.imread(image_path)
blob = cv.dnn.blobFromImage(image, 1/255.0, (224, 224))
backends_configs = [
(cv.dnn.DNN_BACKEND_OPENCV, cv.dnn.DNN_TARGET_CPU, "CPU"),
(cv.dnn.DNN_BACKEND_CUDA, cv.dnn.DNN_TARGET_CUDA, "CUDA"),
(cv.dnn.DNN_BACKEND_OPENCL, cv.dnn.DNN_TARGET_OPENCL, "OpenCL"),
]
results = []
for backend, target, name in backends_configs:
try:
net = cv.dnn.readNet(model_path)
net.setPreferableBackend(backend)
net.setPreferableTarget(target)
# 预热
net.setInput(blob)
net.forward()
# 测试 10 次
times = []
for _ in range(10):
start = cv.getTickCount()
net.setInput(blob)
net.forward()
elapsed = (cv.getTickCount() – start) / cv.getTickFrequency() * 1000
times.append(elapsed)
avg_time = np.mean(times)
fps = 1000 / avg_time
results.append({
'backend': name,
'avg_time_ms': avg_time,
'fps': fps
})
print(f"{name}: {avg_time:.2f}ms ({fps:.1f} FPS)")
except Exception as e:
print(f"{name}: 不可用 – {e}")
return results
8.2 模型量化优化
def optimize_model_fp16(model_path):
"""FP16 量化优化"""
# 加载模型
net = cv.dnn.readNet(model_path)
# 启用 FP16
net.setPreferableTarget(cv.dnn.DNN_TARGET_CUDA_FP16)
print("FP16 量化完成")
print("注意:需要 CUDA 支持且 GPU 支持 FP16 运算")
return net
九、常见问题解答
Q1: 如何选择合适的 DNN 后端?
答:根据硬件条件选择:
# 有 NVIDIA GPU -> CUDA
net.setPreferableBackend(cv.dnn.DNN_BACKEND_CUDA)
net.setPreferableTarget(cv.dnn.DNN_TARGET_CUDA)
# 有 Intel CPU -> OpenVINO
net.setPreferableBackend(cv.dnn.DNN_BACKEND_INFERENCE_ENGINE)
# 通用 -> CPU
net.setPreferableBackend(cv.dnn.DNN_BACKEND_OPENCV)
net.setPreferableTarget(cv.dnn.DNN_TARGET_CPU)
Q2: 如何处理不同尺寸的输入图像?
答:使用动态输入或固定尺寸:
# 方法 1:固定尺寸(推荐)
blob = cv.dnn.blobFromImage(image, 1/255.0, (224, 224))
# 方法 2:保持宽高比
h, w = image.shape[:2]
scale = 224 / max(h, w)
new_h, new_w = int(h * scale), int(w * scale)
blob = cv.dnn.blobFromImage(image, 1/255.0, (new_w, new_h))
Q3: 如何提高检测精度?
答:调整阈值和数据增强:
# 降低置信度阈值
detector = YOLODetector(confidence_threshold=0.3)
# 多尺度检测
scales = [416, 512, 608]
for scale in scales:
blob = cv.dnn.blobFromImage(image, 1/255.0, (scale, scale))
# … 检测逻辑
Q4: 如何实现实时检测?
答:使用轻量模型和跳帧策略:
# 使用 YOLOv4-tiny 或 YOLOv8n
detector = YOLOv8Detector('yolov8n.onnx')
# 跳帧处理
frame_skip = 2
for i in range(total_frames):
if i % frame_skip == 0:
results = detector.detect(frame)
# 使用上一帧结果
Q5: 如何处理显存不足?
答:减小 batch size 和输入尺寸:
# 减小输入尺寸
blob = cv.dnn.blobFromImage(image, 1/255.0, (320, 320)) # 而不是 640
# 分批处理
batch_size = 4
for i in range(0, len(images), batch_size):
batch = images[i:i+batch_size]
# 处理批次
十、总结与扩展
10.1 本章要点
本文详细介绍了 OpenCV DNN 模块的应用:
10.2 最佳实践建议
- 模型选择:根据应用场景选择合适精度的模型
- 后端优化:充分利用硬件加速(CUDA、OpenCL)
- 预处理:严格按照模型要求预处理输入
- 批处理:批量处理提高吞吐量
- 内存管理:及时释放不用的中间结果
10.3 下一步学习建议
建议深入学习:
- 模型转换:ONNX、TensorRT
- 自定义算子:扩展 DNN 模块功能
- 边缘设备部署:树莓派、Jetson Nano
- 模型压缩:剪枝、量化、知识蒸馏



