第四章:图像分类模型部署——用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这些模型都会上场,敬请期待。