资讯动态

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

发布时间:2026/10/25 10:47:07 来源:尧图企业网站定制
简介基于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_size12, factor0.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_size12, factor0.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] # 特征图上每个点对应原图 (stride2) 的偏移 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], axis1).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), dtypenp.uint8) crops.append(cv2.resize(patch, (size, size), interpolationcv2.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, alpha0.5, beta0.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_size256, thresh0.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, size24) # 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, thresh0.7, modeunion): 标准 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 会留下很多互相叠着的框调用方还得自己再处理一次。modemin这一支是很多人忽略的地方用标准 union 时大框对小框的 IOU 会偏小导致同一个人的大小两个框都活下来。三级网络之后通常要做一次regress加权平均def box_regress(boxes, scores, thresh0.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(axis0) return result这一步不改变存活框的数量只微调坐标。实测对视频流特别有用框不会随帧抖动得太厉害。3.4 推理主流程串联把上面所有模块按顺序拼起来就是detect.py里的主函数。逻辑是线性的金字塔逐层 P-Net → 汇总候选框 → NMS → R-Net 精修 → NMS → O-Net 精修 → 最终 NMS 和关键点输出。def detect_face(img, pnet, rnet, onet, min_size20, thresholds(0.6, 0.7, 0.7), nms_thresh0.7): # 1) 生成金字塔缩放系数 scales build_pyramid(img, min_sizemin_size, factor0.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, modeunion) 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_size256, threshthresholds[1]) keep nms(boxes, scores, nms_thresh, modemin) boxes, scores boxes[keep], scores[keep] # 5) O-Net 精修同时输出关键点 boxes, scores, landmarks onet_forward(img, boxes, scores, batch_size128, threshthresholds[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框回归分类标 00负样本背景 0.3只参与分类-2关键点样本对某张脸 IOU ≥ 0.65 且有 5 点标注框回归 关键点回归打标签这步的坑在于比例。正负样本要保持 1:3 左右如果直接随机采样背景样本会占九成以上分类器学会全部判负就能拿到 90% 准确率。常见做法是先采足正样本和部分脸再按比例采负样本。def sample_by_iou(box, gt_boxes, gt_landmarks, pos0.65, part0.4, neg0.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_ratio0.7): 只保留损失最大的前 keep_ratio 样本返回布尔 mask n int(len(loss_per_sample) * keep_ratio) if n 0: return np.zeros(len(loss_per_sample), dtypebool) 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如果联合训前一级一动后一级的数据分布就变了收敛非常困难。具体顺序训 P-Net输入 12×12数据用随机裁剪 缩放构造训完之后在验证集上跑一遍把漏检的图找出来。用训好的 P-Net 在训练集上生成候选框与 GT 做 IOU 分配构造 24×24 的 R-Net 数据训练并验证。同理用 P-Net R-Net 生成候选构造 48×48 的 O-Net 数据这一级额外要有关键点标注。三级都训完之后做端到端评估重点看两个指标在固定误检率下的人脸召回率以及关键点归一化误差。验证集必须与训练集在场景上分开比如训练用室内、验证用室外。如果同源划分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 个框坐标的对照流程不再凭肉眼扫一眼觉得没问题就打包发出去——人脸检测这种多阶段串联的东西前面错一点后面放大一百倍。希望这些记录能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑