OpenCV 中转换 TensorFlow 目标检测模型并使用 Python API 推理:SSD MobileNetV1 完整实战
TensorFlow Object Detection API 产出的检测模型不能直接被 OpenCV 的 cv.dnn 使用,需要先把训练好的模型"冻结"成 .pb 图,再转换为 OpenCV 可读的 .pbtxt 文本图描述。本教程以 COCO 数据集上预训练的 SSD MobileNetV1 为例,完整演示如何从零获取 frozen graph、生成配套文本图,并用 OpenCV Python API(object_detection.py 示例)完成真实图片的目标检测推理与结果后处理。读完本文,你将能独立把任意一个 TensorFlow 检测模型迁移到 OpenCV DNN 模块并调优推理参数。
本文依据 tf_det_model_conversion_tutorial.md 展开,并结合仓库内对应源码逐行印证,帮助读者既会用、又知其所以然。
学习目标
完成本教程后,你将掌握:
- 获取 TensorFlow(TF)检测模型的 frozen graph(冻结图);
- 用 OpenCV Python API 运行转换后的 TensorFlow 检测模型并解析输出。
两条核心能力将围绕 SSD MobileNetV1 这一经典检测模型逐一展开。本教程对应的运行环境要求为 OpenCV >= 4.5(原作者:Anastasia Murzova)。
关键概念:Frozen Graph 与 .pb 文本图
把 TensorFlow 模型迁移到 cv.dnn.Net 的第一步,是得到模型的 frozen graph(冻结图)。冻结图把"模型图结构"与"所需变量(例如权重)的取值"合并为一个整体,去掉了训练所需的 checkpoint、优化器等冗余信息,因此可直接用于部署与推理。冻结图以 protobuf 序列化格式保存在 .pb 文件中。
OpenCV 提供了两个专用于读取 .pb 图的函数:
cv.dnn.readNetFromTensorflow—— 直接读取 TensorFlow 冻结图;cv.dnn.readNet—— 通用读取函数,可通过参数同时指定模型与配置文件(object_detection.py即使用此函数)。
注意:
.pb二进制图描述了网络算子结构,但像 SSD 这类检测模型通常还带有训练配置(anchor、NMS、类别数等),这些信息编码在训练时使用的.config管道文件中。要让 OpenCV 完整重建检测网络,还需要一张"文本图".pbtxt,其生成方法见后文。
环境准备:虚拟环境与依赖安装
为便于隔离依赖,官方推荐使用 Python 3.7+ 的虚拟环境:
virtualenv -p /usr/bin/python3.7 <env_dir_path>
source <env_dir_path>/bin/activate
随后需要在本机源码编译安装 OpenCV-Python(因为要用到较新的 DNN 模块能力),构建步骤见官方 Python 教程的安装章节。
仓库为转换流程预置了依赖清单 requirements.txt,可先按需增删(例如补入 opencv-python),再一次性安装:
pip install -r requirements.txt
模型准备:拉取 SSD MobileNetV1 并抽取冻结图
本阶段的完整代码位于 py_to_py_ssd_mobilenet.py,它属于 samples/dnn/dnn_model_runner 模块。以模块方式执行即可:
python -m dnn_model_runner.dnn_conversion.tf.detection.py_to_py_ssd_mobilenet
主流程只有寥寥几行:指定模型名、下载并抽取冻结图、打印其路径。
tf_model_name = 'ssd_mobilenet_v1_coco_2017_11_17'
graph_extraction_dir = "./"
frozen_graph_path = extract_tf_frozen_graph(tf_model_name, graph_extraction_dir)
print("Frozen graph path for {}: {}".format(tf_model_name, frozen_graph_path))
extract_tf_frozen_graph 的实现逻辑(见 py_to_py_ssd_mobilenet.py)如下:
# define model archive name
tf_model_tar = model_name + '.tar.gz'
# define link to retrieve model archive
model_link = DETECTION_MODELS_URL + tf_model_tar
tf_frozen_graph_name = 'frozen_inference_graph'
try:
urllib.request.urlretrieve(model_link, tf_model_tar)
except Exception:
print("TF {} was not retrieved: {}".format(model_name, model_link))
return
print("TF {} was retrieved.".format(model_name))
tf_model_tar = tarfile.open(tf_model_tar)
frozen_graph_path = ""
for model_tar_elem in tf_model_tar.getmembers():
if tf_frozen_graph_name in os.path.basename(model_tar_elem.name):
tf_model_tar.extract(model_tar_elem, extracted_model_path)
frozen_graph_path = os.path.join(extracted_model_path, model_tar_elem.name)
break
tf_model_tar.close()
代码细节背后有几个值得留意的点:
- 下载地址拼接自模块常量
DETECTION_MODELS_URL = 'http://download.tensorflow.org/models/object_detection/'(源码 py_to_py_ssd_mobilenet.py),与samples/dnn/models.yml中ssd_tf配置块声明的下载源一致; - 官方模型压缩包内部结构为
ssd_mobilenet_v1_coco_2017_11_17/frozen_inference_graph.pb,脚本按文件名关键字frozen_inference_graph在 tar 成员中定位并只解压该文件,避免解出无用的完整训练目录。
成功执行后终端输出应为:
TF ssd_mobilenet_v1_coco_2017_11_17 was retrieved.
Frozen graph path for ssd_mobilenet_v1_coco_2017_11_17: ./ssd_mobilenet_v1_coco_2017_11_17/frozen_inference_graph.pb
准备测试素材
推理使用下面这张 Pexels 协议授权的双层巴士街景照片作为输入(原图位于 images 目录):
从训练配置生成文本图 .pbtxt
仅有 .pb 冻结图还不够。SSD 检测网络带有大量由训练配置决定的超参数(anchor 尺度/宽高比、类别数、NMS 阈值、输入尺寸等),OpenCV 需要一张 .pbtxt 文本图来描述这些结构。这份 .config 来自 TensorFlow Object Detection API 的官方示例 ssd_mobilenet_v1_coco.config,该框架自带的检测模型工具机制可帮助完成这类转换。
仓库提供了 tf_text_graph_ssd.py 脚本来执行这一"二进制图 → 文本图"的转换。运行命令如下:
python tf_text_graph_ssd.py --input ssd_mobilenet_v1_coco_2017_11_17/frozen_inference_graph.pb --config ssd_mobilenet_v1_coco_2017_11_17/ssd_mobilenet_v1_coco.config --output ssd_mobilenet_v1_coco_2017_11_17.pbtxt
三个参数含义(对应脚本 argparse 定义):
| 参数 | 含义 |
|---|---|
--input |
TensorFlow 冻结图路径(.pb) |
--config |
训练时使用的 .config 管道文件路径 |
--output |
输出文本图路径(.pbtxt) |
成功执行后,工作目录下会生成 ssd_mobilenet_v1_coco_2017_11_17.pbtxt。
如果希望了解这一步背后究竟做了什么,可以阅读 createSSDGraph 函数,它的关键动作包括:
- 算子白名单裁剪:只保留
keepOps中列出的算子(Conv2D、BiasAdd、Relu/Relu6、DepthwiseConv2dNative、FusedBatchNorm(V3)等),并移除MultipleGridAnchorGenerator/、Postprocessor/、Preprocessor/等前缀节点,大幅精简网络图; - BatchNorm 融合:
fuse_nodes会把反卷积后的Add/Rsqrt/Mul/Sub子图重写为单个FusedBatchNorm算子并带入epsilon=0.001; - 配置驱动重建检测头:从
.config中读取num_classes、image_resizer的宽高、anchor generator(ssd_anchor_generator或multiscale_anchor_generator)与box_coder的x/y/width/height_scale,据此为每个特征层生成PriorBox,再拼接出ClassPredictor、BoxEncodingPredictor与最终的DetectionOutput层; - 保留标准输出名:将
num_detections、detection_scores、detection_boxes、detection_classes作为网络输出张量名(源码 输出名定义)。
正是这套"解析 config → 重建 PriorBox 与 NMS 头 → 精简算子"的机制,保证了 OpenCV 能拿到与 TensorFlow 原生推理等价的检测语义。
查看 SSD MobileNetV1 的测试默认配置
在运行推理脚本之前,先了解仓库为 SSD MobileNetV1 预设的预处理参数。它们统一维护在 models.yml 的 ssd_tf 键下(见 models.yml 第 158-177 行):
ssd_tf:
load_info:
url: "http://download.tensorflow.org/models/object_detection/ssd_mobilenet_v1_coco_2017_11_17.tar.gz"
sha1: "9e4bcdd98f4c6572747679e4ce570de4f03a70e2"
download_name: "ssd_mobilenet_v1_coco_2017_11_17.tar.gz"
member: "ssd_mobilenet_v1_coco_2017_11_17/frozen_inference_graph.pb"
model: "ssd_mobilenet_v1_coco_2017_11_17.pb"
config: "ssd_mobilenet_v1_coco_2017_11_17.pbtxt"
mean: [0, 0, 0]
scale: 1.0
width: 300
height: 300
rgb: true
labels: "object_detection_classes_coco.txt"
postprocessing: "ssd"
sample: "object_detection"
这些参数中,mean、scale、width、height、rgb 会在运行时通过 common.add_preproc_args(见 samples/dnn/common.py)注入命令行解析器,最终进入 cv.dnn.blobFromImage 完成输入预处理(object_detection.py 中 blob 构造代码)。关键含义:
width/height: 300:SSD MobileNetV1 的固定输入尺寸为 300×300,与.config中fixed_shape_resizer一致;mean: [0, 0, 0]、scale: 1.0:MobileNet 系列训练时使用 [-1, 1] 归一化,OpenCV 侧通过rgb: true(即swapRB)配合scale=1/127.5之外的组合在此处正好抵消,因此保持均值为 0、缩放为 1.0;labels:COCO 80 类名称清单,文件为 object_detection_classes_coco.txt;postprocessing: "ssd":告知示例脚本按 SSD 的1x1xNx7输出格式解码。
运行检测推理
模型的默认配置已就绪,现在用双层巴士图片启动推理。以别名 ssd_tf 引用 models.yml 配置的方式最简洁:
python object_detection.py ssd_tf --input ../data/pexels_double_decker_bus.jpg
这条命令与下面显式指定各参数的完整写法完全等价(ssd_tf 别名会展开成 model/config/width/height/classes 等默认项):
python object_detection.py --model ssd_mobilenet_v1_coco_2017_11_17.pb --config ssd_mobilenet_v1_coco_2017_11_17.pbtxt --input ../data/pexels_double_decker_bus.jpg --width 300 --height 300 --classes ../data/dnn/object_detection_classes_coco.txt
其中 ../data 指向 OpenCV samples 数据目录(对应仓库中的 samples/data)。推理结果如下图所示:
从源码看,示例的运行主链路位于 object_detection.py:
- 加载网络:
net = cv.dnn.readNet(args.model, args.config, "", engine),同时读入.pb与.pbtxt; - 设置后端:
net.setPreferableBackend/net.setPreferableTarget支持切换 CPU、OpenVINO、CUDA、Vulkan 等计算后端; - 前向推理:
net.setInput(blob)后outs = net.forward(outNames); - 解码后处理:
postprocess()解析 SSD 输出并在drawPred()中绘制检测框与类别标签,窗口上还提供 "Confidence threshold" 滑条可实时调节置信度阈值。
SSD 输出解码与 NMS 后处理
SSD 网络的原始输出是一个形状为 1x1xNx7 的 blob,其中 N 是候选检测数量,每个检测是一组 7 元向量:
[batchId, classId, confidence, left, top, right, bottom]
示例脚本在 postprocess 的 ssd 分支中按行解码:保留 confidence > confThreshold 的检测,取 detection[1] - 1 作为类别编号(classId 含背景类 0,因此减 1 跳过背景标签),坐标默认按输入 300×300 归一化,必要时再乘回原图宽高。
由于 ssd_tf 生成文本图时已把 NMS 逻辑内嵌进 DetectionOutput 层(配置 post_processing.batch_non_max_suppression 的 iou_threshold、score_threshold、max_total_detections 等会写入该层属性,见 tf_text_graph_ssd.py),单输出网络在 OpenCV 后端下无需额外 NMS。当后端不提供内置 NMS 或网络存在多输出时,脚本会回退调用 cv.dnn.NMSBoxes 做非极大值抑制(object_detection.py NMS 调用)。
可调参数:threshold 与 NMS 阈值
针对不同场景的检测结果,OpenCV 还提供了两个最常用的"结果修正"参数:
--thr:置信度阈值,默认0.5(见 object_detection.py 参数定义)。调高可过滤更多低置信度误检,调低可召回更弱的目标;--nms:非极大值抑制阈值(IoU),默认0.4(object_detection.py 参数定义)。值越小,重叠框被抑制得越彻底,同一目标越不容易输出多个框。
除 ssd_tf 之外,models.yml 中还维护了 faster_rcnn_tf(Faster-RCNN Inception v2)、YOLO 系列等检测模型条目,它们的文本图可由 tf_text_graph_faster_rcnn.py 等脚本生成——这也说明"TensorFlow 检测模型 → OpenCV DNN"是一套覆盖 SSD、Faster-RCNN 等多个系列的通用方法论,掌握本文的 SSD MobileNetV1 流程即可举一反三。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0627
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00

