第四章:图像分类模型部署——用OpenCV跑ResNet、MobileNet、EfficientNet
图像分类,说白了就是让计算机回答「这张图里是什么」。你手机相册里的人物归类、电商平台的以图搜图,背后都是分类模型在干活。
这一章,我们聚焦一个很实际的问题:怎么用OpenCV把ResNet、MobileNet、EfficientNet这些主流分类模型跑起来。不依赖PyTorch、不依赖TensorFlow,就靠OpenCV的DNN模块搞定推理。我当年第一次在树莓派上跑通MobileNet时,那种感觉——嗯,就像给一台老电脑装上了新显卡。
4.1 为什么选OpenCV做分类部署?
很多人问我:「直接用PyTorch推理不香吗?」香,但分场景。
我个人的习惯是:原型验证用PyTorch,产品部署用OpenCV。原因有三:
- 零依赖:OpenCV是C++写的,编译后一个so文件搞定。你不需要在客户机器上装Python、装CUDA、装PyTorch那一整套。
- 跨平台:Windows、Linux、ARM、Android,OpenCV全平台支持。我在项目中遇到过客户要求部署在国产龙芯上,PyTorch当时还没适配,OpenCV直接搞定。
- 推理速度快:OpenCV的DNN模块底层调用了ONNX Runtime或OpenVINO,推理效率不输原生框架。
核心结论:如果你要做边缘端部署、嵌入式部署、或者需要给客户交付一个「双击就能跑」的exe,OpenCV DNN是首选。
4.2 模型准备:从PyTorch到OpenCV能吃的格式
OpenCV的DNN模块支持两种模型格式:
- Caffe模型:.prototxt + .caffemodel
- ONNX模型:.onnx(推荐,通用性最强)
我个人强烈建议用ONNX。为什么?因为几乎所有主流框架都能导出ONNX,而且OpenCV对ONNX的支持越来越完善。
这里给一个PyTorch转ONNX的示例代码:
import torch
import torchvision.models as models
# 加载预训练模型
model = models.resnet18(pretrained=True)
model.eval()
# 构造一个虚拟输入
dummy_input = torch.randn(1, 3, 224, 224)
# 导出ONNX
torch.onnx.export(
model,
dummy_input,
"resnet18.onnx",
input_names=['input'],
output_names=['output'],
opset_version=11
)
print("ONNX模型导出成功!")
小技巧:opset_version建议用11或更高。我遇到过用opset=9导出的模型在OpenCV里某些算子不支持,折腾了半天才发现是版本问题。
4.3 用OpenCV加载并推理分类模型
模型准备好了,接下来就是加载和推理。代码其实很简洁:
import cv2
import numpy as np
# 1. 加载ONNX模型
net = cv2.dnn.readNetFromONNX("resnet18.onnx")
# 2. 读取并预处理图像
image = cv2.imread("cat.jpg")
blob = cv2.dnn.blobFromImage(
image,
scalefactor=1.0/255.0, # 归一化到[0,1]
size=(224, 224), # ResNet输入尺寸
mean=(0.485, 0.456, 0.406), # ImageNet均值
swapRB=True, # OpenCV是BGR,需要转RGB
crop=False
)
# 3. 设置输入并推理
net.setInput(blob)
outputs = net.forward() # 形状: (1, 1000)
# 4. 获取预测结果
scores = outputs[0]
predicted_class = np.argmax(scores)
confidence = scores[predicted_class]
print(f"预测类别ID: {predicted_class}, 置信度: {confidence:.4f}")
这里有个坑,我必须要说:预处理参数一定要和训练时保持一致。ResNet训练时用的是ImageNet的均值和标准差,你如果随便换个均值,结果可能惨不忍睹。我曾经有个项目,模型在服务器上跑得好好的,部署到客户机器上准确率直接掉到30%,查了半天发现是预处理时忘了除以255。
4.4 ImageNet标签映射:从类别ID到人类能看懂的名字
上面代码输出的predicted_class是一个数字,比如281。281是什么?猫?狗?还是汽车?
我们需要一个标签映射文件。ImageNet有1000个类别,官方提供了一个映射文件imagenet_labels.txt,每行一个类别名。你可以从网上下载,也可以自己写一个简单的映射字典。
加载标签的代码:
# 加载ImageNet标签
with open("imagenet_labels.txt", "r") as f:
labels = [line.strip() for line in f.readlines()]
# 获取类别名
class_name = labels[predicted_class]
print(f"预测结果: {class_name}, 置信度: {confidence:.4f}")
注意:ImageNet的标签文件有两种常见格式:一种是纯类别名(如"tabby cat"),另一种是带编号的(如"n02123045 tabby cat")。我建议用纯类别名的版本,解析起来更省事。
4.5 Top-1与Top-5准确率计算
在图像分类中,我们通常关注两个指标:
- Top-1准确率:模型预测的最高分类别是否就是真实类别。
- Top-5准确率:真实类别是否在模型预测的前5个最高分类别中。
为什么要有Top-5?你想想看,有些类别本身就很相似,比如「波斯猫」和「布偶猫」,模型分不清也情有可原。Top-5给了模型一定的容错空间。
计算Top-1和Top-5的代码:
def compute_top_k_accuracy(scores, true_label, k=5):
"""
计算Top-1和Top-5准确率
scores: 模型输出的1000维向量
true_label: 真实类别ID
"""
# 获取得分最高的k个类别索引
top_k_indices = np.argsort(scores)[::-1][:k]
# Top-1
top1_correct = (top_k_indices[0] == true_label)
# Top-5
top5_correct = (true_label in top_k_indices)
return top1_correct, top5_correct
# 示例:假设真实类别是281(猫)
true_label = 281
top1, top5 = compute_top_k_accuracy(scores, true_label)
print(f"Top-1正确: {top1}")
print(f"Top-5正确: {top5}")
在实际项目中,我一般会同时输出Top-1和Top-5的结果。如果Top-1不对但Top-5对了,说明模型「接近正确」,可能只是类别边界模糊。这时候我会考虑是否要合并相似类别,或者增加训练数据。
4.6 不同模型的部署对比
我整理了一张表,方便你对比三种主流模型在OpenCV下的表现:
| 模型 | 输入尺寸 | 参数量 | 推理时间(CPU) | Top-1准确率 | 适用场景 |
|---|---|---|---|---|---|
| ResNet-18 | 224x224 | 11.7M | ~30ms | 69.8% | 通用场景,平衡性好 |
| ResNet-50 | 224x224 | 25.6M | ~60ms | 76.2% | 高精度要求 |
| MobileNetV2 | 224x224 | 3.5M | ~15ms | 71.8% | 移动端、嵌入式 |
| MobileNetV3 | 224x224 | 5.4M | ~12ms | 75.2% | 移动端,精度提升 |
| EfficientNet-B0 | 224x224 | 5.3M | ~25ms | 77.1% | 精度与效率的平衡 |
| EfficientNet-B4 | 380x380 | 19.3M | ~100ms | 82.6% | 高精度,算力充足 |
推理时间是在我的笔记本(i7-11800H)上测的,仅供参考。你实际部署时,性能会因硬件不同而有差异。
我的建议:
- 如果部署在手机或树莓派上,首选MobileNetV3,速度快、精度也不错。
- 如果是服务器端,用EfficientNet-B0或ResNet-50,精度更高。
- 如果对精度有极致要求且算力充足,上EfficientNet-B4甚至B7。
4.7 完整部署示例:批量图片分类
最后,给一个完整的批量分类脚本。这个脚本我在多个项目中复用,改改路径就能用:
import cv2
import numpy as np
import os
class ImageClassifier:
def __init__(self, model_path, label_path, input_size=(224, 224)):
self.net = cv2.dnn.readNetFromONNX(model_path)
self.input_size = input_size
with open(label_path, "r") as f:
self.labels = [line.strip() for line in f.readlines()]
def preprocess(self, image):
blob = cv2.dnn.blobFromImage(
image, 1.0/255.0, self.input_size,
(0.485, 0.456, 0.406), swapRB=True, crop=False
)
return blob
def predict(self, image):
blob = self.preprocess(image)
self.net.setInput(blob)
scores = self.net.forward()[0]
# Top-5结果
top5_idx = np.argsort(scores)[::-1][:5]
results = []
for idx in top5_idx:
results.append({
"class_id": idx,
"class_name": self.labels[idx],
"confidence": float(scores[idx])
})
return results
def predict_batch(self, image_dir):
for img_name in os.listdir(image_dir):
img_path = os.path.join(image_dir, img_name)
image = cv2.imread(img_path)
if image is None:
continue
results = self.predict(image)
print(f"\n图片: {img_name}")
for i, res in enumerate(results):
print(f" Top-{i+1}: {res['class_name']} ({res['confidence']:.4f})")
# 使用示例
classifier = ImageClassifier(
model_path="resnet18.onnx",
label_path="imagenet_labels.txt"
)
classifier.predict_batch("test_images/")
4.8 避坑指南
做分类模型部署,有几个坑我踩过,分享给你:
- 输入尺寸不匹配:ResNet用224x224,EfficientNet-B4用380x380。搞错了尺寸,模型不会报错,但结果完全不对。
- 通道顺序:OpenCV读图是BGR,模型训练时用的是RGB。别忘了
swapRB=True。 - 归一化方式:有的模型用[0,1]归一化,有的用[-1,1]。检查一下你用的模型训练时的预处理方式。
- ONNX算子兼容性:某些高级算子(如Resize、Upsample)在不同版本的ONNX Runtime中表现不同。我建议导出ONNX时用
opset_version=11,兼容性最好。
曾经有一次,我把一个EfficientNet模型部署到客户的Windows服务器上,结果推理结果全是乱码。查了两天,发现是OpenCV版本问题——客户机器上装的是OpenCV 4.5.0,而我用的是4.6.0,ONNX解析器有差异。升级OpenCV后问题解决。所以,部署时一定要确认OpenCV版本一致。
好了,这一章的内容就到这里。下一章我们会聊目标检测模型的部署,到时候YOLO、SSD这些模型都会上场,敬请期待。