简介:这是一份基于OpenCV与SSD模型的人脸检测项目压缩包,面向计算机视觉初学者与开发者,解决图像及视频流中的人脸定位需求。包内共10个文件,约6.36MB,核心包含两个Python脚本(单图检测与视频检测)、预训练的SSD caffemodel权重、模型描述文件、XML工程配置以及多张JPG测试样图,无需额外准备模型即可直接运行并观察检测效果。资源已有417人学习下载,适合希望快速上手深度学习目标检测、了解OpenCV调用SSD流程的读者。借助清晰的函数模块划分与处理流程,可以掌握图像预处理、模型加载、置信度过滤、边界框绘制等关键环节;同时测试图片和模型权重分离存放,便于替换自己的图像进行验证,为后续扩展人脸识别、实时监控或嵌入式部署等项目实践打下基础。
1. 一个 facedetection.zip 压缩包,到底能救你什么?
一个叫facedetection.zip的压缩包到你手里,通常意味着一个人脸检测方案的最小可复用资产:里面大概率有训练好的模型权重、推理脚本、依赖清单和一小撮标注数据,能让你不用从零训练就在图片或视频里把人脸框出来。对做安防、考勤、客流统计的团队来说,它是典型的“拿到就能先验证,跑通再谈改造”。但压缩包不是承诺,解压后能不能用,取决于三个问题:模型是什么框架导出的、输入输出是什么格式、代码和当前环境是否兼容。这篇笔记就按拿到zip后的实际操作顺序写:解压、识别结构、跑通推理、微调训练、排坑,最后用评估指标判断这个方案值不值得继续投入。
2. 用 unzip 安全解压 facedetection.zip:先看清单而不是直接运行
很多人拿到压缩包第一反应是双击解压,然后拖进 IDE。这个习惯会给你之后的工作埋两颗雷:一是压缩包可能带目录层级,直接解压会让一堆文件铺满当前目录,后面找模型找不到;二是如果包本身损坏或内容被篡改,你到训练时才发现白跑了一轮。所以我拿到任何facedetection.zip,第一件事永远是建一个干净的目录,先校验再解压。这个过程不涉及任何业务逻辑,但能帮你省掉后面百分之三十的排错时间。
2.1 解压前先做两件事:文件大小和 MD5 校验
压缩包可能只有几 KB,也可能有几 GB,这个差异直接决定了它是代码骨架还是带了完整权重。先看大小,再算校验值,是成本最低的两步。
# 1. 看文件大小,判断由模型权重主导还是由代码主导 ls -lh facedetection.zip # 2. 计算 MD5,并保存下来,方便后续核对传输结果 md5sum facedetection.zip | tee facedetection.zip.md5 # 3. 创建专用目录,解压到里面,避免文件散落到当前目录 mkdir -p facedetection unzip -q facedetection.zip -d facedetection这三条命令里,ls -lh和unzip -q都好理解,关键是中间的md5sum。很多工程事故不是代码写错,而是压缩包在拷贝过程中丢了一个字节,导致模型加载到一半报“Unexpected end of file”。你把md5sum算出来的值和来源方给的对不上,就别继续浪费时间解压,直接重传。如果来源方没给值,算完存起来,至少自己能追查。Windows 环境可以把md5sum换成certutil -hashfile facedetection.zip MD5,逻辑一样。
解压到facedetection/目录后,我的习惯是立刻看一眼解压出来的顶层文件,不要急着跑脚本。还有一个容易忽略的细节:如果压缩包是在 Windows 上打的,解压后文件权限可能都是 644,脚本没有执行权限。这时候先chmod +x *.sh,再用ls -R确认目录树的完整度。很多人在这一步直接跳过,后面bash train.sh时报 permission denied,还以为是代码问题。
2.2 一个典型人脸检测项目的目录结构与文件作用
虽然facedetection.zip内部长得各不相同,但人脸检测项目翻来覆去就那几类文件。理解的职责如下表:
| 文件/目录 | 常见名称举例 | 作用 |
|---|---|---|
| 模型文件 | models/face.onnx、weights/face.pt、model.pth | 人脸检测推理的核心,可能是权重也可能是完整图 |
| 推理脚本 | detect.py、run.py、infer.py | 读取模型,对图片/视频/摄像头做预测 |
| 数据样例 | data/sample.jpg、test.mp4 | 让你不依赖真实业务数据就能快速复现结果 |
| 配置文件 | config.yaml、face.yaml | 模型路径、输入尺寸、置信度阈值、类别名 |
| 依赖清单 | requirements.txt、environment.yml | 锁定运行环境的第三方库版本 |
| 训练脚本 | train.py、train.sh | 训练或微调模型 |
| 标注数据 | annotations/、labels/、JPEGImages/ | 训练或验证用的人脸框标签 |
| 说明文档 | README.md、INSTALL.md | 作者的使用说明,通常包含环境配置和运行命令 |
这里要提醒一句:不要迷信README.md。我见过不少压缩包里的 README 是自动生成的,写满了“TODO”,真正有价值的信息在代码里。所以下一步我会打开detect.py或train.py的头部,看它 import 了什么库、读取了哪个路径。这两分钟能帮你少踩很多坑。另外,如果发现包里有requirements.txt,我会先cat它,而不是直接pip install -r。因为有些依赖列表里写着pytorch==1.8.1,但你机器上的 CUDA 是 11.7,装了也不一定能跑。先看再装,比装完再卸强。
2.3 怎么判断这个压缩包是推理包、训练包还是数据集包
三者混淆是常态,但根据关键文件可以快速归类。
如果解压后有models/face.onnx加一个detect.py,这是推理包,目标是把现有模型跑起来,重点看模型输入输出和依赖。如果出现train.py、datasets/、face.yaml,这是训练包,说明作者把训练配置也放进来了,你可以用它微调。如果全是images/和xml/或json/标注,那它只是个数据集包,并不带模型,你需要另外找训练代码或直接用标注格式去训练。
我的判断方法是:先看有没有模型文件,再看有没有 train.py,最后看标注数据的格式。最省事的是推理包,因为它不需要你配训练环境;最容易被误判的是数据集包,有人拿到一堆图片和标注,以为缺了模型文件,其实是这包本来就没打算给你模型。所以别一上来就骂对方“少发了一个文件”,先按这个思路排一遍。
这里还有一个实际场景:有些包既有.onnx,又有.pt,还有onnx2trt.py。这种通常是作者的交付模板,包含了从 PyTorch 到 ONNX 再到 TensorRT 的完整转换链。这时你不必全部跑通,只需要根据部署目标选一条路。如果目标是验证算法效果,直接用.pt或.onnx;如果目标是上线 GPU 服务器,再看转换脚本。不要贪心把链路都跑一遍,因为每一步都要和版本对齐,时间成本很高。
2.4 如果压缩包里的 README 是空白的,怎么把结构补全
我遇到过一次,整个包只有一个detect.py和一个.onnx,没有说明。这种情况下,我会先用 grep 看代码里引用了哪些入口文件和资源路径。
# 在解压目录内搜索代码中出现的模型、输入路径和依赖关键字 grep -R "load\|weight\|onnx\|yaml\|video" --include="*.py" .这条命令能把代码里所有涉及模型加载和输入输出的行翻出来。之后看代码开头 import 的库,就能逆推出环境依赖。如果代码里写了torch.load("face.pt"),那模型就是 PyTorch 格式;写了ort.InferenceSession("face.onnx"),就是 ONNX 格式。再配合requirements.txt(如果有)装依赖,没有就手动装几个最常见的库,比如opencv-python、numpy、torch或onnxruntime。这套反推流程基本能把一个“裸包”盘活。
如果连代码都没有,只有一个模型文件,那就只能靠模型本身去推断输入输出。这时可以用torch.jit加载.pt,或者用onnx库查看计算图。常见做法是:
python -c "import onnx; m = onnx.load('models/face.onnx'); print(m.graph.input[0]); print(m.graph.output[0])"这样能看到输入张量的 shape 和输出张量的 shape,从而确定输入尺寸。没有 README 并不可怕,可怕的是不检查就盲跑,跑了一天发现模型路径写的是绝对路径,根本不是你机器上的目录。到这一步,包的结构和类型已经清楚了。下一章我们进入最关键的事件:把推理真正跑通。
3. 把 facedetection 的推理跑通:从模型文件识别框架到最小脚本
推理是判断一个facedetection.zip有没有价值的试金石。很多模型文件在理论上有 99% 的 mAP,可一旦放到你的机器上,加载失败、输出乱码、框画错,都是推理阶段暴露的。所以不要先去纠结训练,先把最小推理验证跑通。
3.1 先看后缀名判断框架:ONNX、TensorRT 还是 PyTorch
模型文件的后缀基本决定了你的技术路线。下面是我常用的判断表和初次运行时需要的运行时库:
| 文件后缀 | 大概率框架 | 运行时选择 | 一句话注意点 |
|---|---|---|---|
.pt/.pth | PyTorch | torch | 要小心它是state_dict还是完整模型,加载方式不同 |
.onnx | ONNX | onnxruntime | 跨平台最稳,可转 TensorRT/OpenVINO |
.engine/.trt | TensorRT | TensorRT 运行时 | 只适配特定GPU型号,换卡要重新导出 |
.xml+.bin | OpenVINO | openvino | 适合 Intel CPU 部署,但需要转换工具链 |
这里最容易翻车的是.engine文件。TensorRT 的推理引擎和 CUDA、显卡架构强绑定,你在 A 卡上导出的 engine,放到 B 卡上大概率加载失败。所以见到.engine,先问清楚来源环境的显卡型号和 TensorRT 版本。如果没有.engine,只有.onnx,那是最好的一条路,因为onnxruntime在 CPU 上也能跑,能先把逻辑验证跑通再谈加速。
3.2 最小推理脚本:加载模型、人脸预处理、后处理过滤
假设解压目录里有一个models/face.onnx和一段说明文字,我们就用 ONNX Runtime 写一个最小推理脚本。选择 ONNX 是因为它不需要搭建复杂的 PyTorch 环境,用 CPU 就能在大多数机器上跑。
# detect_min.py # 以 ONNX 模型为例,读取一张图片并输出人脸框 import cv2 import numpy as np import onnxruntime as ort # ---------- 1. 加载模型 ---------- session = ort.InferenceSession( "models/face.onnx", providers=["CPUExecutionProvider"] # CPU 先调通,之后可按需切 GPU ) input_name = session.get_inputs()[0].name print("模型输入:", session.get_inputs()[0].shape, session.get_inputs()[0].type) # ---------- 2. 预处理:等比缩放 + letterbox 填充 ---------- def letterbox(img, new_size=640): h, w = img.shape[:2] scale = min(new_size / h, new_size / w) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(img, (new_w, new_h)) canvas = np.full((new_size, new_size, 3), 114, dtype=np.uint8) canvas[:new_h, :new_w] = resized return canvas, scale, (new_w, new_h) img = cv2.imread("test.jpg") canvas, scale, (new_w, new_h) = letterbox(img, 640) # 转为 CHW 并归一化到 0~1 blob = cv2.dnn.blobFromImage(canvas, 1/255.0, (640, 640), swapRB=True) # ---------- 3. 推理 ---------- outputs = session.run(None, {input_name: blob}) pred = outputs[0][0] # 通常第一个维度是 batch,第二维是候选框 print("原始输出形状:", pred.shape) # ---------- 4. 后处理:过滤低置信度 + NMS ---------- # 假设输出格式为 [x_center, y_center, w, h, objectness, class_score] conf = pred[:, 4] * pred[:, 5] keep = conf >= 0.5 boxes = pred[keep] conf = conf[keep] # 把中心点坐标转成左上角和右下角 x1 = boxes[:, 0] - boxes[:, 2] / 2 y1 = boxes[:, 1] - boxes[:, 3] / 2 x2 = boxes[:, 0] + boxes[:, 2] / 2 y2 = boxes[:, 1] + boxes[:, 3] / 2 # 还原 letterbox:先减 padding,再除 scale pad_x = (640 - new_w) / 2 pad_y = (640 - new_h) / 2 x1 = (x1 - pad_x) / scale y1 = (y1 - pad_y) / scale x2 = (x2 - pad_x) / scale y2 = (y2 - pad_y) / scale # NMS 去重 indices = cv2.dnn.NMSBoxes( [(float(a), float(b), float(c-a), float(d-b)) for a, b, c, d in zip(x1, y1, x2, y2)], [float(c) for c in conf], score_threshold=0.5, nms_threshold=0.45 ) for i in indices: i = i[0] if isinstance(i[0], list) or isinstance(i[0], np.ndarray) else i cv2.rectangle(img, (int(x1[i]), int(y1[i])), (int(x2[i]), int(y2[i])), (0, 255, 0), 2) cv2.imwrite("result.jpg", img) print("检测完成,结果已写入 result.jpg")这段代码有三个关键点你要注意。
第一,letterbox函数里填充用的是114,这是 YOLO 系列常用的填充灰度值,不是随便填的。如果你拿的模型来自 MTCNN 或 RetinaFace,可能根本不期望 letterbox,而是直接 resize 到固定尺寸,那时这段预处理就要改。第二,pred.shape能帮你确定输出格式:如果第二维是25200,多半是 YOLO 风格的解码前输出;如果是N且每行大于 5,就可能是x1, y1, x2, y2, conf, ...的直接回归格式。需要按模型自定义。第三,坐标还原那步“减 padding,再除 scale”是最容易被漏掉的,后面避坑章节我会再展开。
3.3 三个必调参数:conf_thres、nms_thres、input_size
不管模型来源是什么,你在推理阶段一定会遇到这三个参数,它们直接决定检测效果。
置信度阈值conf_thres控制“多像人脸才算检测到”。默认 0.5,但距离远、模糊的人脸置信度往往只有 0.3~0.4。我一般先用 0.3 看召回,再用 0.5 看精度,最后根据业务场景选一个平衡点。NMS 阈值nms_thres控制“两个重叠框合并的松紧度”。设 0.4 会保留较少的框,适合密集人群;设 0.6 会合并更多相近的框,但可能导致多人贴脸时漏检。最微妙的是input_size:模型训练时的输入尺寸决定了它的感受野。如果你用 320 寸输入,一张包含无数小脸的集体照可能全部漏检,换 640 就能好很多,但推理速度也会相应下降。这个参数不是越大越好,要配合业务里人脸最小的像素数来定。
3.4 用摄像头实时跑一遍,验证不是只能跑图片
图片能跑通只说明模型的一部分。我会把它改成摄像头输入,因为人脸检测最终大多会落在视频流上。
# video_demo.py # 对摄像头每一帧做人脸检测,断掉边界情况验证稳定性 import cv2 from detect_min import letterbox import onnxruntime as ort import numpy as np session = ort.InferenceSession("models/face.onnx", providers=["CPUExecutionProvider"]) cap = cv2.VideoCapture(0) while True: ret, frame = cap.read() if not ret: break canvas, scale, (new_w, new_h) = letterbox(frame, 640) blob = cv2.dnn.blobFromImage(canvas, 1/255.0, (640, 640), swapRB=True) pred = session.run(None, {session.get_inputs()[0].name: blob})[0][0] conf = pred[:, 4] * pred[:, 5] keep = conf >= 0.3 # 实时场景适当降低阈值,容忍更多误检 # ... 同前面 NMS 和坐标还原逻辑 ... cv2.imshow("face", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()实时跑最怕的不是速度慢,而是画面翻转、摄像头采集格式异常这类基础问题。如果画面一直打不开,优先检查cap.isOpened(),很多环境里 OpenCV 需要sudo或者换采集后端。实时推理时我会把conf_thres降到 0.3,因为视频帧有运动模糊,阈值太高框会一闪一闪。后面要做的训练微调,也会以这个视频 demo 的结果作为基线对比。
4. 用训练包微调 facedetection 模型:数据集格式、命令与超参
推理跑通只说明模型可用,但不说明它对你的业务场景好用。人脸检测最常见的痛点是:原模型在开源基准上不错,但对你的俯拍角度、密集人群、昏暗光线效果一塌糊涂。这时就需要用训练包做微调。很多facedetection.zip里虽然带训练脚本,但你要先解决数据格式问题。
4.1 三种主流人脸检测标签格式:VOC、COCO、YOLO
你的标注数据可能是对方的格式,也可能是你自己用标注工具导出的格式。最常见的是这三种:
| 格式 | 典型文件 | 坐标形式 | 适合的训练代码 |
|---|---|---|---|
| VOC | Annotations/*.xml | 左上 + 右下绝对像素 | 传统检测框架、部分 PyTorch 项目 |
| COCO | annotations/train.json | 左上 + 宽高绝对像素 | 通用检测项目,pycocotools评估方便 |
| YOLO | labels/*.txt | 归一化的cx, cy, w, h | YOLO 系列仓库,读取最快 |
我经常收到只有 VOC 标注的包,但对方训练代码只吃 YOLO 格式。转换脚本不复杂,但有两个边界坑:一是 VOC 里类别名是中文,需要先映射成names里的索引;二是 XML 中有旋转或未剪裁的情况,坐标可能超出图像边界,转换时要 clip。另一个常见问题是 COCO 的语义:有人把bbox写成[x1, y1, w, h],有人写成[x1, y1, x2, y2],对不上会直接导致训练 loss 不收敛。我的建议是转换后用一个小脚本可视化三张图,确认框落到人脸再开始训练。不要怕这一步费时间,训练一次几小时,可视化确认只要三分钟,省下的返工时间很可观。
4.2 最小可用的训练命令与参数设置
假设你已经把标注整理成face.yaml,且代码基于常见的 YOLO 系列训练器,那么最小命令是:
python train.py \ --data face.yaml \ --weights models/face.pt \ --batch 16 \ --epochs 30 \ --img 640 \ --device 0--weights models/face.pt表示在压缩包里那个预训练模型的基础上微调,而不是从随机初始化开始;--data face.yaml指向数据配置;--batch 16表示每轮用 16 张图;--img 640与推理时的输入尺寸保持一致。face.yaml最少需要四行:
train: ./data/train val: ./data/val nc: 1 names: ["face"]这里的nc是类别数量,人脸检测通常就是 1。如果原模型是从 COCO 预训练的,它可能有 80 类,你把它改成 1 类后,检测头最后一层权重会初始化成随机值。这没问题,微调会把它学回来。但要注意:--weights加载时如果报 shape mismatch,是正常的,说明检测头不匹配,而骨干网络权重已经加载上了。看到这个错误不要慌,先检查打印日志里加载权重是否 skip 了最后一层,如果 skip 的只是检测头,就继续跑;如果整个权重都没加载,那才是路径或 key 的问题。
4.3 超参调整:epoch、batch size、学习率怎么配不会翻车
拿到一个压缩包里的预训练模型,我不建议直接照搬作者的训练参数。数据量不同,超参完全不同。如果你的标注只有几百张,epochs设 30 就够了,再多大概率过拟合;如果数据量有上万张,30 只是个起点。判断标准很简单:看验证集 loss 是否在最后一个 epoch 还在下降,如果还在降,就继续加。
batch size主要受显存限制。16 是常规值,如果你 8GB 显存跑不动,就降到 8,同时学习率也要降。一个我可以给你的血泪经验是:batch 从 16 改成 8 时,学习率最好减半,否则训练初期 loss 容易飞。学习率一般用0.001起步,配合 cosine 衰减。如果训练时 NaN,最常见原因是学习率太高或数据里有脏标注,先把 lr 降到0.0001试,别再调网络结构。另一个容易忽略的是workers参数,它控制数据加载线程数。在 Windows 上workers设太大会死锁,在 Linux 上设 8 通常没问题。如果你在训练时发现 GPU 利用率忽高忽低,多半是workers太少,数据供给跟不上,而不是模型问题。
4.4 迁移学习时要不要冻结骨干网络
微调时冻结骨干可以显著加快训练,但也会限制模型适应新场景的能力。我的做法是:当新数据量少且与预训练分布差距不大时,冻结前 10 层;当新场景反差很大(比如全是监控俯拍角度),则不冻结,让全部参数参与更新。冻结操作在不同仓库里写法不同,通常是一个--freeze 10参数,含义是冻结前 10 层。如果你用自研训练脚本,就要在加载权重后遍历模型参数并设置requires_grad=False。这里有个判断技巧:先不冻结跑 5 个 epoch,看验证集效果,如果已经不错,再开冻结重新训练,做对比。冻结不是省事的手段,是当你没有足够算力时的妥协方案。
如果压缩包里没有训练脚本,只有推理代码,那最省力的路径是把你整理好的数据集移到通用训练框架里跑,而不是自己写训练循环。通用框架对数据采样、增强、NMS 后处理都调试过,比自己从零写稳定得多。你只需要把模型结构和权重迁移过去。迁移时注意保留原来的归一化参数,也就是数据集里的均值方差,如果框架默认用 COCO 的均值方差,而你的模型是在自建数据上训练的,结果会偏差很大。
5. facedetection 实战避坑:从解压到部署的 5 个典型翻车点
人脸检测项目真正做到生产环境,你会发现一半时间在排环境冲突。下面这 5 个坑我基本每个都踩过,按“现象 → 原因 → 解决”写清楚,你遇到时可以直接照做。
5.1 依赖版本冲突:OpenCV 和 PyTorch 的 CUDA 版本对不上
现象:跑detect.py,到torch.load之后崩溃,终端报一堆undefined symbol: cudnn_BatchNormalizationForwardInference,或者onnxruntime直接提示找不到 CUDA 库。 原因:机器上装了多个 CUDA 运行时,PyTorch 自带的libcudnn和系统全局的 OpenCV 动态库版本不一致,运行时加载顺序互相污染。这属于环境问题,不是代码问题。 解决:先别调代码,用python -c "import torch; print(torch.__version__, torch.version.cuda)"和python -c "import onnxruntime as ort; print(ort.__version__)"查看版本。如果两者要求的 CUDA 版本不一致,最简单的办法是推理阶段只用CPUExecutionProvider,先保证流程通;要上 GPU 时,把 OpenCV 换成opencv-python-headless,减少动态库冲突面。另一个操作是启动脚本前加export LD_LIBRARY_PATH=$TORCH_HOME/lib:$LD_LIBRARY_PATH,给 PyTorch 的库更高的优先级,但这是后手,治标不治本。
5.2 小脸检测不到:可能不是模型问题,是预处理 resize
现象:单人照检测很好,一到五六人的会议室合照就直接漏掉后排人脸。 原因:大多数 YOLO 训练使用 640×640 输入,如果图片直接 resize 到 640,原图里 20×20 像素的小脸在输入图上只有 5×5,特征完全丢失。这不是模型能力不行,而是预处理方式有问题。 解决:先把输入尺寸提高到 1280,看小脸召回率是否明显上升。如果还不行,使用分块推理(tiling):把原图按 640×640 的窗口切块,每块单独检测,再把结果合并。注意相邻块要有重叠区,否则若人脸在切割边界会被截断。分块推理会增加耗时,但对密集小脸场景是有效的。另外也可以在微调阶段加入“随机裁剪缩放”增强,让模型见过更多小脸尺度的样本。有一种更激进的做法是训练专门的小脸分支,但那是重活,先通过调大输入尺寸和 tiling 解决,别一上来就改模型结构。
5.3 推理速度不达标:可能只是没开半精度或线程池
现象:模型在 GPU 上跑,但每帧耗时 80ms,明显比作者说的慢,或 CPU 推理时 CPU 占用率只有 20%。 原因:默认加载模型是 FP32,并且 ONNX Runtime 在 CPU 上只用一个线程,GPU 也可能没启用 TensorRT 或 FP16。 解决:CPU 场景,设置os.environ["OMP_NUM_THREADS"] = "8",并在InferenceSession里指定sess_options.intra_op_num_threads = 8;GPU 场景,如果模型是 ONNX,试着用 TensorRT provider,开启 FP16。这些改动不动模型结构,就能翻几倍速度。如果模型本来是 PyTorch,也可以在推理时加一句model.half()并使用torch.cuda.amp。不过半精度对某些老 GPU 不友好,需要实测。另一个容易被忽视的点是分辨率:把输入从 640 降到 480,速度提升明显,精度损失通常可控,要看业务是否接受。
5.4 检测框和原图错位:letterbox 坐标还原漏了
现象:检测框能画出来,但位置明显偏左/偏上,或者框比人脸大了一圈。 原因:预处理用 letterbox 把原图缩放并填充到 640×640,后处理时直接用了模型输出的坐标,没有做“减 padding 再除 scale”的逆运算。 解决:回到 3.2 节那一步,严格把模型输出坐标还原。常见错误是只除了 scale,忘了减 padding。可以用下面这段代码自我检查:
# 纠正 letterbox 坐标还原 x1_orig = (x1 - pad_x) / scale y1_orig = (y1 - pad_y) / scale然后在原图上画框,和人眼位置对比。如果仍偏,打印pad_x和scale的数值,多半是取值用了全局变量而没从返回值拿。这类 bug 玄学就在于:它在静态图上看着还行,一到视频里框就会抖。更隐蔽的情况是训练时做了 mosaic 增强,模型输出坐标里的 padding 不是预处理里的那一套,这时需要回到配置文件里看模型作者是怎么算坐标的。
5.5 压缩包里的权重和代码版本不匹配:加载就报错
现象:用torch.load加载.pt时,报KeyError: 'model.0.conv1.weight'或权重文件能加载,但model.forward()输出形状和预期不同。 原因:压缩包里的权重是用旧版网络结构训练的,而你手上的推理或训练代码是另一个版本。比如 YOLOv5 的权重被拿去给 YOLOv8 代码加载,层名字匹配不上。 解决:先诊断,打印权重文件里的 key:
import torch ckpt = torch.load("models/face.pt", map_location="cpu") if isinstance(ckpt, dict) and "model" in ckpt: print(list(ckpt["model"].state_dict().keys())[:5]) else: print(type(ckpt), list(ckpt.keys())[:5])拿到 key 之后和当前代码里网络定义的层名对照,确定是哪个版本的权重。如果版本差异大,最好的选择不是手动改权重,而是找对应版本的推理代码。避免以后出现这种问题的做法是在解压后立刻给压缩包内的代码和权重文件的 MD5 建一个“配套清单”,记录谁和谁是一套。
6. 让 facedetection 更可信:先算 mAP,再谈优化
推理通了、微调也跑了,但你还是没回答“这个方案到底行不行”。只靠肉眼在几张图上数框,很容易被个别好结果迷惑。我的习惯是任何 face 检测模型到手,先算一次 mAP,用数据决定下一步。
6.1 用测试集算一次 mAP,10 分钟看懂模型真实水平
你不用重写评估框架,如果压缩包内代码有val.py就用它;没有的话,在训练包基础上写一个评估脚本,逻辑很简单:对每张测试图推理,和真值框算 IoU,IoU 大于 0.5 记为 true positive,再按置信度排序画 PR 曲线。用 WIDER FACE 的公开验证集也可以,但更推荐用你自己的业务数据,因为 mAP 高只代表在特定分布上强,不代表在你场景上强。这个数字可以直接用来决策:如果 mAP 在 0.9 以上,放心交付;如果在 0.7 左右,先补小脸场景的标注再微调;如果低于 0.5,别在这个模型上死磕,换模型比调参更划算。
6.2 一个高性价比改动:让检测头顺带输出两个关键点
如果业务下游还需要做人脸对齐、活体判断或裁剪归一化,我通常会选择在检测头里顺带回归双眼中心点,而不是另起一个新模型。检测框的两个顶点外加双眼两个点,只需要给最后一层多加四个输出通道,训练时多一次损失计算。这样做的好处是:下游任务复用同一个特征图,完全省掉一次前向推理。方案落地时注意给这两个关键点也做同样的坐标归一化,并在微调数据里补齐人脸框内双眼标注;没有标注就别硬改,否则会拖累检测精度。
最后说一点我的习惯:任何拿到手的facedetection.zip,我都会先重复一遍“解压——跑通——建测试集——算 mAP”这四步,再决定要不要改结构、换框架。之前有次为了省时间,跳过 mAP 直接上摄像头调参数,结果小脸漏检问题在灰度环境里被放大,返工了一整周。从那以后我再也不信“看起来还行”,只信测试集上的数字。希望帮到你。
本文还有配套的精品资源,点击获取