news 2026/10/25 10:47:04

MTCNN人脸检测:P-Net/R-Net/O-Net三级级联与工程落地

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MTCNN人脸检测:P-Net/R-Net/O-Net三级级联与工程落地

简介:基于MTCNN的人脸检测完整代码包面向深度学习初学者与计算机视觉开发者,围绕P-Net、R-Net、O-Net三级级联网络展开,覆盖候选框生成、边框回归、关键点定位与非极大值抑制等核心流程,可用于人脸检测实验复现、课程项目与工程原型验证。压缩包共80个文件、约23.13MB,以48个py脚本为主体,配套jpg示意图、prototxt与caffemodel网络配置及权重、npy数据、md说明、ipynb分步教程和mp4演示,便于按模块查阅。代码中包含模型定义、训练与测试脚本、Caffe模型转换、视频检测及人脸对齐示例;数据集需按说明自行下载和预处理。已有990人学习,适合希望理解级联检测实现细节并在此基础上做二次开发的中高级读者。

1. 深度学习人脸检测里,MTCNN 凭什么还在被反复翻出来用

一张 3000×2000 的团队合照,站后排的人脸只有 40 像素宽,侧脸、逆光,还有人被前面的肩膀挡掉半张。拿固定输入尺寸的检测器直接跑,前排大脸框得好好的,后排小脸基本全漏。MTCNN 就是冲着这个场景设计的:先做图像金字塔把原图缩成十几层,最上面那层让 12×12 的滑窗也能覆盖 40 像素的远脸;再用 P-Net、R-Net、O-Net 三级级联一路筛,最后一层不光给框,还顺手输出 5 个关键点。

这套模型整体只有几十万参数量级,在普通 CPU 上跑一张 1080p 图不到一秒,做门禁、相册聚类、直播推流的预处理环节都够用。深度学习做人脸检测的方案不少,但 MTCNN 是那种"想在自己机器上跑通、想改两行就见到效果"的项目:结构清晰、模块独立、单文件就能推理。适合两类人:一类是要在本地或边缘设备上做人脸预处理的开发者,一类是想拿一个完整的检测 pipeline 练手、搞懂级联网络到底怎么串起来的人。

2. 三级级联网络拆解:P-Net、R-Net、O-Net 各管哪一段

MTCNN 的结构说穿了就是三句话:P-Net 负责撒网,R-Net 负责筛掉大部分误检,O-Net 负责最终定框和打点。难的是三级之间的数据怎么传、坐标怎么还原、阈值怎么配合。这一章把每一级的输入输出、卷积配置和任务头拆开看。

2.1 图像金字塔:让 12×12 的滑窗看得见大脸也看得见小脸

P-Net 的固定输入是 12×12,如果不做缩放,它在整图上只能检测到 12 像素左右的人脸。图像金字塔的作用就是把原图按比例一层层缩小,小到一定程度之后,原本 200 像素的大脸在新图里就变成 12 像素了,于是同一个 12×12 的检测器就能覆盖所有尺寸。

缩放因子一般取 0.709,约等于 √2/2,含义是每缩两层的面积减半,这样金字塔层数与尺度覆盖比较均衡。终止条件是缩放后图像的短边小于 12,因为再小 P-Net 就没法滑窗了。

import cv2 import numpy as np def build_pyramid(img, min_size=12, factor=0.709): """生成图像金字塔的缩放系数列表(不是生成图片,省内存)""" h, w = img.shape[:2] # 最短边缩到 min_size 时对应的缩放比,这是金字塔的第一层(最大尺度) m = min_size / min(h, w) scales = [] while m <= 1.0: scales.append(m) m *= factor # 每层继续缩小 return scales img = cv2.imread("group.jpg") scales = build_pyramid(img, min_size=12, factor=0.709) print(len(scales), scales[:5])

这段代码只算缩放系数、不实际生成图,是为了把内存留给后续批处理。min_size是 P-Net 的输入边长,改成 20 就能少检一些小脸、换来速度;factor从 0.709 调到 0.5,金字塔层数会明显减少,大脸和小脸之间的尺度跨度变大,检测的召回会掉一点。实际推理时是在循环里按scale逐个cv2.resize,同一个金字塔只在一次检测里用一次。

2.2 P-Net 的全卷积滑窗与候选框生成

P-Net 名字里有"Proposal",它的任务就是用最便宜的卷积网络在整图上扫一遍。网络结构很浅:三层3×3卷积(stride 1,带 padding)加上三路1×1卷积输出头,分别是 2 通道的人脸/非人脸分类、4 通道的框偏移、10 通道的关键点坐标。

关键在于它是全卷积的。如果输入 12×12,最后得到的是 1×1 的输出;如果直接把整张金字塔层级的图喂进去,输出就是一个H'×W'的特征图,特征图上每一个点就对应原图上一个 12×12 的窗口。因为经过三次 stride 为 1 的 3×3 卷积没有下采样(前两层带 padding),所以特征图的空间尺寸和输入一致,但等效感受野是 12×12,窗口之间的步长是 2。

def pnet_forward(sess, pnet_input, pnet_cls, pnet_reg, pnet_landmark, img, scale): """单个金字塔层级的 P-Net 前向,返回候选框与分数(框架无关写法,换成 PyTorch 同理)""" h, w = img.shape[:2] hs, ws = int(h * scale), int(w * scale) resized = cv2.resize(img, (ws, hs)) # 归一化:MTCNN 训练时就用的 (x - 127.5) / 128,推理必须严格对齐 inp = (resized.astype(np.float32) - 127.5) / 128.0 inp = np.expand_dims(inp, 0) # NCHW cls_map, reg_map, lm_map = sess.run( [pnet_cls, pnet_reg, pnet_landmark], feed_dict={pnet_input: inp}) # 分类头 softmax 后取人脸概率 score_map = cls_map[0, :, :, 1] # 特征图上每个点对应原图 (stride=2) 的偏移 stride = 2 ys, xs = np.where(score_map > 0.6) scores = score_map[ys, xs] # 特征图坐标还原到缩放图上的 12x12 框 x1 = xs * stride y1 = ys * stride x2 = x1 + 12 y2 = y1 + 12 boxes = np.stack([x1, y1, x2, y2], axis=1).astype(np.float32) # 回归量在缩放图尺度上做还原 reg = reg_map[0, ys, xs, :] w_box, h_box = 12.0, 12.0 boxes[:, 0] += reg[:, 0] * w_box boxes[:, 1] += reg[:, 1] * h_box boxes[:, 2] += reg[:, 2] * w_box boxes[:, 3] += reg[:, 3] * h_box # 再除以 scale,统一回原图坐标系 boxes /= scale return boxes, scores

几个参数必须对齐,错一个就全军覆没。第一是归一化系数127.5/128,训练时用的什么推理就得用什么,BGR 还是 RGB 也要跟训练时一致(这里按 BGR 读图,训练端如果用的是 RGB 就会平移)。第二是偏移还原的尺度w_box/h_box:网络回归的不是绝对坐标,是相对于 12×12 锚框的归一化偏移,所以还原时乘的是框宽高而不是整图宽高。第三是score_map > 0.6这个阈值,P-Net 阶段故意放低,宁可多放进来一些,靠后面两级去筛。

2.3 R-Net 与 O-Net:裁到 24×24 和 48×48 再判一次

P-Net 的问题是它只看 12×12 的局部,很容易把眼睛、鼻子、纹理丰富的角落判成人脸。R-Net 的作用就是收窄:把 P-Net 剩下的候选框在原图上裁出来,统一cv2.resize到 24×24,送进一个带全连接的网络里,输出同样是分类 + 回归,但没有关键点。

O-Net 再走一遍同样的流程,输入尺寸升到 48×48,网络更深,输出多了 10 个通道的关键点坐标。三级之间的差别主要就三点:输入尺寸、网络深度、是否输出关键点。

def crop_and_resize(img, boxes, size): """按候选框裁剪并缩放到统一尺寸,MTCNN 三级之间靠这个函数衔接""" h, w = img.shape[:2] # 四舍五入 + 截断,注意必须夹到图像范围内,否则 cv2.resize 会报错 x1 = np.round(boxes[:, 0]).astype(np.int32).clip(0, w - 1) y1 = np.round(boxes[:, 1]).astype(np.int32).clip(0, h - 1) x2 = np.round(boxes[:, 2]).astype(np.int32).clip(0, w) y2 = np.round(boxes[:, 3]).astype(np.int32).clip(0, h) crops = [] for i in range(len(boxes)): patch = img[y1[i]:y2[i], x1[i]:x2[i]] if patch.size == 0: # 退化框,给个黑图占位 patch = np.zeros((size, size, 3), dtype=np.uint8) crops.append(cv2.resize(patch, (size, size), interpolation=cv2.INTER_LINEAR)) batch = (np.stack(crops).astype(np.float32) - 127.5) / 128.0 return batch

这个函数被 R-Net 和 O-Net 共用,size传 24 或 48。clip那两行是血泪经验:P-Net 的回归会把框推到图像边界之外,负数坐标切出来是空数组,cv2.resize会直接抛异常。很多人调 MTCNN 报!ssize.empty(),八成就是这里没夹。

另外裁剪时用的是原图坐标,不是缩放图坐标。P-Net 出来之后已经除过 scale 还原到原图了,如果忘了这一步、直接拿缩放图的框去原图上裁,得到的就是一堆错位的小图,R-Net 会全部判成背景。

2.4 三个任务头怎么联合:分类、框回归、关键点的损失配比

训练时三个任务是一起算损失的,公式很朴素:

L = L_cls + α · L_box + β · L_landmark

分类用交叉熵,框回归和关键点用平方损失。α 通常取 0.5,β 取 0.5,这样三路梯度量级差不多,不至于让回归项把分类项压死。剩下两个细节容易踩坑:一是框回归只对正样本和部分脸样本算,负样本没有框可回归,如果全算进去,网络会学会把所有框压到零;二是关键点损失只在标注了关键点的正样本上算,其余样本直接置零 mask。

def compute_loss(pred_cls, pred_box, pred_lm, gt_label, gt_box, gt_lm, alpha=0.5, beta=0.5): """三任务联合损失,label: 1 正样本 0 负样本 -1 部分脸 -2 关键点样本""" cls_loss = softmax_cross_entropy(pred_cls, gt_label) # 只有正样本和部分脸参与框回归 box_mask = (gt_label == 1) | (gt_label == -1) box_loss = mse(pred_box, gt_box) * box_mask # 只有关键点样本参与 landmark 回归 lm_mask = (gt_label == -2) lm_loss = mse(pred_lm, gt_lm) * lm_mask n = box_mask.sum() + lm_mask.sum() + 1e-6 return cls_loss + alpha * (box_loss.sum() / n) + beta * (lm_loss.sum() / n)

把 mask 写成布尔数组再与损失相乘,是这套代码里最常见的写法,比 if 分支好读。注意归一化用的是有效样本数n而不是 batch size,否则 α、β 的含义会随 batch 里正负样本比例漂移,训练日志上看起来就是损失忽高忽低,很难判断到底收敛没收敛。

3. 工程落地:从目录结构到推理主流程

原理讲完,接下来是最能抄作业的部分:一个能跑起来的 MTCNN 工程长什么样,前向、NMS、坐标还原这几块代码怎么串。

3.1 目录结构与依赖清单

这类项目通常不会太大,两三十个文件就能闭环。核心是把"网络定义""工具函数""推理入口""模型权重"分开,别全塞一个文件里。

目录 / 文件作用
nets/pnet.py、rnet.py、onet.py三级网络定义,各自独立,方便单独训练
utils/box_utils.py框偏移还原、IOU 计算、NMS、坐标裁剪
utils/pyramid.py图像金字塔缩放系数生成
train/三阶段训练脚本与数据加载
detect.py推理主流程,命令行入口
weights/三个阶段的权重文件(通常为.npy或框架权重)

依赖只有四个:数值计算、图像读写、深度学习框架、可选的可视化。如果只用推理,甚至可以不要框架——权重是固定形状的话,纯 numpy 手写前向也能跑,只是慢。

pip install numpy opencv-python pip install torch torchvision # 或 tensorflow,二选一

提示:三条路径(P-Net / R-Net / O-Net)的权重文件名要和加载代码严格对应。建议在加载后打印每个权重的 shape 做一次自检,免得出现"模型加载成功但输出全是 0"这种黑匣子式问题。

3.2 单阶段前向:批处理与偏移还原

三级网络里,P-Net 是唯一在整图(金字塔层)上做全卷积的,R-Net、O-Net 都是对候选框批处理。批处理有两个直接好处:GPU 利用率高、Python 循环次数少。但批大小要控制,候选框动辄上万个,一次全塞进去显存直接爆。

def rnet_forward(sess, rnet_input, rnet_cls, rnet_box, img, boxes, batch_size=256, thresh=0.7): """R-Net 精修:按批裁图送网络,返回过阈值的框与分数""" out_boxes, out_scores = [], [] for i in range(0, len(boxes), batch_size): chunk = boxes[i:i + batch_size] batch = crop_and_resize(img, chunk, size=24) # 24x24 归一化后输入 cls, reg = sess.run([rnet_cls, rnet_box], feed_dict={rnet_input: batch}) score = cls[:, 1] keep = np.where(score > thresh)[0] if len(keep) == 0: continue # 在这批候选框自己的坐标系里做偏移还原 b = chunk[keep].copy() w_ = b[:, 2] - b[:, 0] h_ = b[:, 3] - b[:, 1] b[:, 0] += reg[keep, 0] * w_ b[:, 1] += reg[keep, 1] * h_ b[:, 2] += reg[keep, 2] * w_ b[:, 3] += reg[keep, 3] * h_ out_boxes.append(b) out_scores.append(score[keep]) return np.concatenate(out_boxes), np.concatenate(out_scores)

batch_size取 256 是个折中值,显存小可以降到 64,对结果没有影响,只是慢一点。thresh在 R-Net 阶段建议提到 0.7,比 P-Net 的 0.6 严,因为这时候候选框数量已经降了一个数量级,筛掉误检的收益更大。reg[keep, 0] * w_这一段是偏移还原的核心:网络输出的是相对于当前框宽高的归一化偏移量,训练和推理必须用完全一致的公式,否则框会整体缩水或膨胀。

3.3 NMS 与框合并:阈值定生死

每一级输出都有一堆互相重叠的框,最后要靠非极大值抑制(NMS)合并。MTCNN 里的 NMS 有个变体叫"加权平均",把重叠框的分数量和坐标按分数加权求平均,而不是简单丢掉低分框,框的位置会更贴合。

def nms(boxes, scores, thresh=0.7, mode="union"): """标准 NMS,返回保留下来的索引""" x1, y1, x2, y2 = boxes[:, 0], boxes[:, 1], boxes[:, 2], boxes[:, 3] areas = (x2 - x1 + 1) * (y2 - y1 + 1) order = scores.argsort()[::-1] keep = [] while order.size > 0: i = order[0] keep.append(i) xx1 = np.maximum(x1[i], x1[order[1:]]) yy1 = np.maximum(y1[i], y1[order[1:]]) xx2 = np.minimum(x2[i], x2[order[1:]]) yy2 = np.minimum(y2[i], y2[order[1:]]) inter = np.maximum(0.0, xx2 - xx1 + 1) * np.maximum(0.0, yy2 - yy1 + 1) if mode == "min": # MTCNN 常用 min 模式,对小框更宽容,减少小脸被大框吞掉 union = np.minimum(areas[i] + areas[order[1:]] - inter, areas[i]) else: union = areas[i] + areas[order[1:]] - inter iou = inter / (union + 1e-6) inds = np.where(iou <= thresh)[0] order = order[inds + 1] return np.array(keep)

阈值 0.7 是常用起点。调到 0.5 会更激进地合并,侧脸和紧挨着的两张脸容易被并成一个;调到 0.9 会留下很多互相叠着的框,调用方还得自己再处理一次。mode="min"这一支是很多人忽略的地方,用标准 union 时大框对小框的 IOU 会偏小,导致同一个人的大小两个框都活下来。

三级网络之后通常要做一次regress加权平均:

def box_regress(boxes, scores, thresh=0.7): """NMS + 坐标加权平均,让框往高分框方向收敛""" keep = nms(boxes, scores, thresh) kept, ks = boxes[keep], scores[keep] result = kept.copy() for i in range(len(kept)): # 找出与当前框 IOU 超过阈值的邻居,按分数加权 iou = iou_of(kept[i], kept) idx = np.where(iou > thresh)[0] w = ks[idx] / ks[idx].sum() result[i] = (kept[idx] * w[:, None]).sum(axis=0) return result

这一步不改变存活框的数量,只微调坐标。实测对视频流特别有用,框不会随帧抖动得太厉害。

3.4 推理主流程串联

把上面所有模块按顺序拼起来,就是detect.py里的主函数。逻辑是线性的:金字塔逐层 P-Net → 汇总候选框 → NMS → R-Net 精修 → NMS → O-Net 精修 → 最终 NMS 和关键点输出。

def detect_face(img, pnet, rnet, onet, min_size=20, thresholds=(0.6, 0.7, 0.7), nms_thresh=0.7): # 1) 生成金字塔缩放系数 scales = build_pyramid(img, min_size=min_size, factor=0.709) # 2) 逐层跑 P-Net,汇总候选 all_boxes, all_scores = [], [] for s in scales: b, sc = pnet_forward(pnet, img, s) all_boxes.append(b); all_scores.append(sc) boxes = np.vstack(all_boxes); scores = np.hstack(all_scores) # 3) NMS + 框回归微调,再做一次阈值过滤 keep = nms(boxes, scores, nms_thresh, mode="union") boxes, scores = boxes[keep], scores[keep] keep = np.where(scores > thresholds[1])[0] if False else keep # 示例占位 # 4) R-Net 精修 boxes, scores = rnet_forward(img, boxes, scores, batch_size=256, thresh=thresholds[1]) keep = nms(boxes, scores, nms_thresh, mode="min") boxes, scores = boxes[keep], scores[keep] # 5) O-Net 精修,同时输出关键点 boxes, scores, landmarks = onet_forward(img, boxes, scores, batch_size=128, thresh=thresholds[2]) return boxes, scores, landmarks

调用侧的参数最值得关注的是min_size。默认 20 是速度和召回的折中;如果只需要检测画面中央的大脸,可以调到 40,金字塔层数锐减,速度提升非常明显。thresholds是一个三元组,分别对应三级的分数门槛,前低后高是原则,反着设会导致大量误检漏过 R-Net 直接进 O-Net。

关键点的输出是 10 个值,即 5 个点的(x, y):左眼、右眼、鼻尖、左嘴角、右嘴角。它们是在 48×48 的裁剪图上回归出来的,用之前要先还原:point / 48 * 框宽 + 框左上角,忘了这一步就会得到一堆 0~48 之间的小数,画在原图上什么都看不见。

4. 训练侧:样本构造、难例挖掘与三阶段顺序

推理跑通之后,如果想换成自己的人脸数据或者修复某个场景下的漏检,就得动训练。MTCNN 的训练是分三个网络单独训的,数据构造方式完全一样,只是裁剪尺寸不同。

4.1 按 IOU 划四档:正样本、负样本、部分脸、关键点

一张训练图的标注里有人脸框和关键点,对每个预生成的候选框计算它与所有人脸框的 IOU,然后按区间打标签:

标签值含义IOU 区间参与哪些损失
1正样本(人脸)≥ 0.65分类 + 框回归
-1部分脸0.4 ~ 0.65框回归,分类标 0
0负样本(背景)< 0.3只参与分类
-2关键点样本对某张脸 IOU ≥ 0.65 且有 5 点标注框回归 + 关键点回归

打标签这步的坑在于比例。正负样本要保持 1:3 左右,如果直接随机采样,背景样本会占九成以上,分类器学会"全部判负"就能拿到 90% 准确率。常见做法是先采足正样本和部分脸,再按比例采负样本。

def sample_by_iou(box, gt_boxes, gt_landmarks, pos=0.65, part=0.4, neg=0.3): """根据一个候选框与所有 GT 的 IOU,决定它的标签""" ious = iou_of(box, gt_boxes) max_idx = np.argmax(ious) max_iou = ious[max_idx] if max_iou >= pos: return 1, gt_boxes[max_idx], gt_landmarks[max_idx] if max_iou >= part: return -1, gt_boxes[max_idx], None if max_iou < neg: return 0, None, None return None, None, None # 0.3 ~ 0.4 的灰区,丢弃

灰区为什么要丢:0.3 到 0.4 之间的框,到底是人脸还是背景人工也说不清,留着会让分类边界模糊。丢掉这一部分样本,分类器的决策面反而更干净。

4.2 OHEM 难例挖掘与损失权重

MTCNN 训练里最关键的技巧之一是 online hard example mining。每一批数据前向之后,把每个样本的损失算出来,只保留损失最大的前 70% 参与反向传播。

def ohem(loss_per_sample, keep_ratio=0.7): """只保留损失最大的前 keep_ratio 样本,返回布尔 mask""" n = int(len(loss_per_sample) * keep_ratio) if n == 0: return np.zeros(len(loss_per_sample), dtype=bool) thresh = np.sort(loss_per_sample)[::-1][n - 1] return loss_per_sample >= thresh

为什么要这么做:正样本里大多数是容易样本,梯度贡献很小;真正能把网络推着走的是那些被误判的难例,比如侧脸、遮挡脸、低光照脸。不做 OHEM 的话,训练前期损失降得很快,后期就开始原地打转, práctico 上表现为"训练集准确率 99%,实际图片一堆漏检"。

keep_ratio取 0.7 是原设定,调到 0.5 会更激进,但样本量小时容易过拟合。样本少的时候我一般把它提到 0.8,先保证梯度稳定。

4.3 三阶段训练顺序与验证口径

三个网络是按 P-Net → R-Net → O-Net 的顺序单独训的,不存在端到端联合训练。原因是每一级的训练数据都依赖前一级的输出(R-Net 的候选来自 P-Net),如果联合训,前一级一动后一级的数据分布就变了,收敛非常困难。

具体顺序:

  1. 训 P-Net,输入 12×12,数据用随机裁剪 + 缩放构造,训完之后在验证集上跑一遍,把漏检的图找出来。
  2. 用训好的 P-Net 在训练集上生成候选框,与 GT 做 IOU 分配,构造 24×24 的 R-Net 数据,训练并验证。
  3. 同理用 P-Net + R-Net 生成候选,构造 48×48 的 O-Net 数据,这一级额外要有关键点标注。
  4. 三级都训完之后做端到端评估,重点看两个指标:在固定误检率下的人脸召回率,以及关键点归一化误差。

验证集必须与训练集在场景上分开(比如训练用室内、验证用室外)。如果同源划分,P-Net 会在验证集上过拟合得非常好看,实际一换场景就崩。

5. 排查与避坑:跑不起来、框漂移、关键点乱飞的现场记录

这套代码最容易出问题的地方,集中体现在预处理、坐标系统和阈值设置上。下面几条是按踩坑频率排的。

5.1 输入与预处理相关的坑

现象:模型能加载、能前向,但输出全都是背景,分数最高不到 0.1。原因:输入归一化或通道顺序和训练时不一致。训练用的是 RGB、(x-127.5)/128,推理用 OpenCV 读进来是 BGR,或者忘了归一化直接喂 0~255,网络看到的分布完全变了。解决:在预处理函数里固定两步——cv2.cvtColor(img, cv2.COLOR_BGR2RGB)和(x - 127.5) / 128.0,并且把这个函数在训练和推理两端共用同一份,别各写一遍。

现象:cv2.resize报错!ssize.empty()。原因:候选框坐标越界或者宽高为 0,切出来是空数组。解决:裁剪前统一x1 = max(0, x1)、x2 = min(w, x2),并且检查x2 > x1 and y2 > y1,不满足的直接跳过。这一条在检测贴边人脸时几乎必现。

现象:大图(4000 像素以上)直接卡死或内存爆掉。原因:图像金字塔的层数是按短边除以 12 算的,4000 像素的图会有 20 多层,P-Net 的中间特征图非常大。解决:先把长边限制到 1000~1600 再检测,检测完把坐标按比例放大回去。人脸检测本来就不需要原始分辨率,缩小之后召回几乎不掉。

5.2 检测结果层面的坑

现象:框都在,但整体往右下偏了十几像素。原因:P-Net 特征图坐标还原时忘了乘 stride,或者偏移还原时把 12 当成了整图尺寸。解决:打日志打印前 5 个候选框,跟肉眼标注的位置对比;确认x1 = xs * 2、偏移乘的是框宽高12。

现象:同一个人的脸出了三个框,怎么调 NMS 都合不掉。原因:NMS 的 IOU 模式用的是 union,大框套小框时 IOU 上不去。解决:把 NMS 的mode换成min,对小框更宽容,同一目标的多个框基本都能合并。

现象:关键点画出来挤在框的左上角一堆。原因:关键点坐标是 48×48 裁剪图上的相对坐标,直接当成原图坐标用了。解决:按px = lm[0] / 48.0 * box_w + box_x1还原,还原完再画。

注意:调阈值的时候一次只动一个。三级阈值同时改,出了问题根本不知道是哪一级导致的,最后只能全量回滚重来。

6. 二次开发:换骨干、加通道、接视频流的改造路径

如果想把这份代码用到自己的场景里,最省力的三条改造路线是这样的。

6.1 换轻量骨干与导出固定输入

P-Net 的卷积层很浅,直接换骨干意义不大;真正值得改的是 R-Net 和 O-Net,把里面的全连接层换成全局平均池化 + 小全连接,参数量能降一半以上,对精度几乎没影响。改完之后导出成固定输入(24×24、48×48)的推理格式,部署端就只需要一个前向接口,不用带整个框架。

class RNetLite(nn.Module): """把 R-Net 的全连接压缩掉,换取更小的体积""" def __init__(self): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 28, 3), nn.PReLU(), nn.MaxPool2d(3, 2, 1), nn.Conv2d(28, 48, 3), nn.PReLU(), nn.MaxPool2d(3, 2, 1), ) self.gap = nn.AdaptiveAvgPool2d(1) # 替代原来的大全连接 self.cls = nn.Linear(48, 2) self.box = nn.Linear(48, 4) def forward(self, x): f = self.gap(self.features(x)).flatten(1) return self.cls(f), self.box(f)

改完必须重新训练,不能直接加载原权重——层结构变了,参数对不上。验证方法是拿同一批图片,对比改之前和改之后的框坐标,IOU 平均掉超过 0.03 就说明改坏了,回去查池化层的位置。

6.2 接视频流的两个技巧

视频流和单张图最大的区别是帧间相关性。第一个技巧是跟踪代替检测:不是每一帧都跑完整三级网络,而是每 5 帧做一次全量检测,中间帧用 IOU 匹配做位置预测,只有检测结果和跟踪结果偏差过大时才触发全量检测。第二个技巧是自适应金字塔:先用上一帧的人脸尺寸估算这一帧需要的缩放范围,只跑 3~4 层金字塔而不是 15 层。

这两个改动在 1080p 视频上的实测收益是帧率从个位数提到 20 以上,代价是快速运动时可能有 1~2 帧的框滞后。要不要用取决于场景,做实时预览够了,做精确统计就得全量跑。

从那以后我每次动 MTCNN 的预处理或者阈值,都强制走一遍"固定图片集 + 打印前 5 个框坐标"的对照流程,不再凭肉眼扫一眼觉得没问题就打包发出去——人脸检测这种多阶段串联的东西,前面错一点,后面放大一百倍。希望这些记录能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/25 10:46:37

基于知识图谱和生成式AI的食谱推荐系统:从Neo4j建模到Prompt工程实战

简介&#xff1a;面向计算机专业毕业设计及项目实战学习者&#xff0c;内含一套基于知识图谱与生成式AI的智能食谱推荐系统完整源码。项目以知识图谱组织食材、菜谱与营养标签关系&#xff0c;结合生成式模型输出个性化推荐方案&#xff0c;覆盖从需求分析、数据库设计到前后端…

作者头像 李华
网站建设 2026/10/25 10:45:10

从零构建可落地的中文GPT-2文本生成系统

简介&#xff1a;本资源是一份面向NLP初学者与进阶开发者的GPT-2中文文本生成实战项目&#xff0c;聚焦Python环境下预训练模型微调、中文数据适配与端到端生成部署全流程。项目完整复现了从数据预处理、模型加载与微调&#xff08;基于Hugging Face TransformersPyTorch&#…

作者头像 李华
网站建设 2026/10/25 10:43:49

华为昇腾GPUStack部署与优化实战指南

1. GPUStack与昇腾IA服务器概述GPUStack是华为面向昇腾AI处理器开发的容器化加速引擎&#xff0c;专门针对Atlas系列服务器的异构计算架构优化。它通过容器化封装将昇腾处理器的算力资源池化&#xff0c;实现多任务并行调度和资源隔离。在Atlas 800训练服务器&#xff08;型号9…

作者头像 李华
网站建设 2026/10/25 10:34:55

贪心算法在建筑抢修任务调度中的应用与实践

1. 项目背景与问题定义"建筑抢修"这个题目让我想起了十年前参与过的一个工业厂房紧急维修项目。当时我们团队需要在72小时内完成一栋危房的加固改造&#xff0c;那种与时间赛跑的紧张感至今记忆犹新。这道题目本质上考察的是在资源有限条件下的最优决策能力——就像现…

作者头像 李华
网站建设 2026/10/25 10:34:48

基于DQN的无人艇避障控制:MATLAB实现与避坑指南

简介&#xff1a;这份资源面向无人艇&#xff08;USV&#xff09;控制、深度强化学习入门与课程设计人群&#xff0c;聚焦基于DQN的避障控制问题。包内提供无人艇与巡逻艇的运动学建模脚本、DQN训练主流程、经验回放与目标Q值计算、奖励函数设计、ε-greedy策略及模糊集合等模块…

作者头像 李华
网站建设 2026/10/25 10:27:39

网络建设与运维国赛脚本文件与答案视频导出实战指南

简介&#xff1a;这份资源面向备战职业院校技能大赛《网络建设与运维》赛项的选手与指导教师&#xff0c;核心解决赛题答案自动导出与配置复盘问题。包内共44个文件&#xff0c;以21个sh与9个ps1脚本为主&#xff0c;覆盖Linux、Windows及网络设备配置导出&#xff0c;另含7个z…

作者头像 李华