简介这份资源提供基于MTCNN实现人脸检测的完整代码面向具备一定深度学习基础、希望深入理解级联卷积网络检测流程的开发者与研究者。MTCNN通过P-Net、R-Net、O-Net三级网络依次完成候选框生成、边界框细化与关键点定位资源围绕这一经典框架给出可运行的工程实现适合用于人脸检测算法学习、复现与二次开发。压缩包共80个文件约23.13MB以48个Python脚本为核心涵盖模型定义、训练与测试流程并配有prototxt与caffemodel权重文件、npy数据、ipynb演示笔记及少量图像与视频素材便于对照代码理解网络结构与推理细节。目前已有989人学习下载。读者可从中获得从数据准备、级联训练到非极大值抑制测试的完整实现思路结合配置参数与脚本组织方式快速搭建实验环境并开展人脸检测相关研究。1. 从一张合影里把人脸框出来MTCNN 到底能干什么前阵子帮一个做活动签到系统的朋友看代码他手里有一批活动现场的合影想自动把每张照片里的人脸框出来再交给后面的识别模型做比对。他试过直接用 OpenCV 的 Haar 级联光线一复杂就疯狂误检侧脸基本全军覆没。后来换成 MTCNN同一批图里侧脸、遮挡、小尺寸人脸都能稳定框住误检率肉眼可见地降下来。这就是 MTCNN 在工程里的真实位置——它不负责认出“这是谁”只负责把“人脸在哪”这件事做扎实是整个人脸识别流水线的第一道关卡。这份资源是一套基于 MTCNN 实现人脸检测的完整代码属于深度学习里比较经典的检测任务落地。它适合两类人一类是刚接触人脸检测、想跑通一个能出结果的小项目的新手另一类是要把检测模块嵌进自己业务里的开发者比如签到、相册聚类、直播贴纸、门禁预处理。整套代码围绕 MTCNN 的三级网络结构展开输入一张图输出若干个人脸框和五个关键点坐标拿来就能跑改起来也不复杂。2. 三级网络拆解P-Net、R-Net、O-Net 是怎么接力干活的MTCNN 全称 Multi-task Cascaded Convolutional Networks核心思路是“由粗到细、层层过滤”。它不像 YOLO 那样一次前向就出结果而是用三个小网络串成流水线前一级负责广撒网后一级负责精挑细选。理解这条流水线是看懂整套代码的前提。2.1 图像金字塔先把图缩成不同尺寸MTCNN 第一步不是直接送网络而是构建图像金字塔。因为照片里人脸大小不一固定输入尺寸的网络没法同时兼顾远近。常见做法是把原图按比例不断缩小缩到最短边小于某个阈值为止每一层都送进 P-Net。def build_image_pyramid(img, min_face_size20, scale_factor0.709): 构建图像金字塔返回每层缩放后的图像和缩放比例 pyramid [] m 12.0 / min_face_size # 12 是 P-Net 的最小检测尺寸 min_length min(img.shape[0], img.shape[1]) cur_scale 1.0 while min_length * cur_scale 12: cur_scale m * (scale_factor ** len(pyramid)) if cur_scale 1.0: continue w int(img.shape[1] * cur_scale) h int(img.shape[0] * cur_scale) if w 12 or h 12: break resized cv2.resize(img, (w, h)) pyramid.append((resized, cur_scale)) return pyramid这段代码里min_face_size决定最小能检测多小的人脸默认 20 像素scale_factor是每层缩小的比例0.709 是原论文给的经典值缩得太快会漏掉中间尺寸的人脸缩得太慢计算量翻倍。金字塔层数直接决定后面 P-Net 要跑多少次是速度和召回率之间的第一个权衡点。2.2 P-Net全卷积网络快速筛候选框P-Net 是个全卷积网络输入任意尺寸输出一堆候选框和置信度。它不关心框得准不准只关心“这里像不像人脸”把明显不是的区域先扔掉。因为要处理金字塔的每一层P-Net 必须足够轻。class PNet(nn.Module): def __init__(self): super(PNet, self).__init__() self.conv1 nn.Conv2d(3, 10, 3, 1) self.prelu1 nn.PReLU(10) self.pool1 nn.MaxPool2d(2, 2, ceil_modeTrue) self.conv2 nn.Conv2d(10, 16, 3, 1) self.prelu2 nn.PReLU(16) self.conv3 nn.Conv2d(16, 32, 3, 1) self.prelu3 nn.PReLU(32) # 两个分支分类是否人脸和回归框偏移 self.conv4_1 nn.Conv2d(32, 2, 1, 1) self.conv4_2 nn.Conv2d(32, 4, 1, 1) def forward(self, x): x self.prelu1(self.conv1(x)) x self.pool1(x) x self.prelu2(self.conv2(x)) x self.prelu3(self.conv3(x)) cls self.conv4_1(x) # 分类输出 reg self.conv4_2(x) # 回归输出 return cls, regP-Net 只有三层卷积参数量很小所以能在金字塔的每一层上快速滑窗。conv4_1输出 2 通道表示每个位置的“人脸/非人脸”得分conv4_2输出 4 通道表示框的偏移量。注意这里用的是 PReLU 而不是 ReLU负半轴有可学习斜率对小网络的收敛更友好。P-Net 输出后要做 NMS 去重再按得分阈值过滤剩下的框才交给 R-Net。2.3 R-Net 与 O-Net精修和关键点回归R-Net 比 P-Net 深一点输入是固定尺寸的候选框裁剪图作用是进一步过滤掉 P-Net 的误检同时做更精细的框回归。O-Net 最深除了分类和框回归还多一个关键点回归分支输出五个点左右眼、鼻尖、左右嘴角。class ONet(nn.Module): def __init__(self): super(ONet, self).__init__() self.conv1 nn.Conv2d(3, 32, 3, 1) self.prelu1 nn.PReLU(32) self.pool1 nn.MaxPool2d(3, 2, ceil_modeTrue) self.conv2 nn.Conv2d(32, 64, 3, 1) self.prelu2 nn.PReLU(64) self.pool2 nn.MaxPool2d(3, 2, ceil_modeTrue) self.conv3 nn.Conv2d(64, 64, 3, 1) self.prelu3 nn.PReLU(64) self.pool3 nn.MaxPool2d(2, 2, ceil_modeTrue) self.conv4 nn.Conv2d(64, 128, 2, 1) self.prelu4 nn.PReLU(128) self.fc5 nn.Linear(128 * 3 * 3, 256) self.prelu5 nn.PReLU(256) self.fc6_1 nn.Linear(256, 2) # 分类 self.fc6_2 nn.Linear(256, 4) # 框回归 self.fc6_3 nn.Linear(256, 10) # 5 个关键点每个 2 坐标 def forward(self, x): x self.prelu1(self.conv1(x)) x self.pool1(x) x self.prelu2(self.conv2(x)) x self.pool2(x) x self.prelu3(self.conv3(x)) x self.pool3(x) x self.prelu4(self.conv4(x)) x x.view(x.size(0), -1) x self.prelu5(self.fc5(x)) cls self.fc6_1(x) reg self.fc6_2(x) landmark self.fc6_3(x) return cls, reg, landmarkO-Net 的三个输出分支对应三个任务分类决定这个框最终留不留回归把框的位置修得更准关键点回归给出五官坐标。关键点在很多业务里比框本身还重要比如人脸对齐、活体检测、贴纸定位都依赖这五个点。三级网络串起来P-Net 负责召回R-Net 负责过滤O-Net 负责精修和关键点这就是 MTCNN 的完整接力逻辑。3. 从零跑通环境、权重和一次完整推理代码能跑起来的关键一是环境依赖装对二是权重文件放对位置。MTCNN 的权重是训练好的三个网络参数没有权重网络结构再对也出不了结果。这一章把跑通流程拆成可照抄的步骤。3.1 环境依赖与目录约定常见做法是用 Python 3.8 以上PyTorch 或 TensorFlow 都行这份代码以 PyTorch 为主。依赖不多主要是数值计算和图像处理。pip install torch torchvision numpy opencv-python pillow目录上建议保持清晰权重文件和代码分开方便后面替换或重新训练。mtcnn_project/ ├── models/ │ ├── pnet.pt │ ├── rnet.pt │ └── onet.pt ├── mtcnn/ │ ├── network.py # 三个网络定义 │ ├── detector.py # 检测主流程 │ └── utils.py # NMS、框变换等工具 ├── weights/ │ ├── pnet.npy │ ├── rnet.npy │ └── onet.npy └── demo.py # 推理入口network.py放网络结构detector.py放三级流水线的调度逻辑utils.py放 NMS 和框坐标变换。权重文件命名要和加载代码里的路径一致这是新手最容易翻车的地方——路径写错程序不报错但结果为空排查半天。3.2 加载权重并初始化检测器检测器初始化时把三个网络和对应权重绑在一起同时设定阈值参数。阈值分三级每一级都可以单独调。class MTCNN: def __init__(self, model_path, min_face_size20, thresholds[0.6, 0.7, 0.7], nms_thresholds[0.7, 0.7, 0.7]): self.min_face_size min_face_size self.thresholds thresholds # 三级网络的分类阈值 self.nms_thresholds nms_thresholds # 三级网络的 NMS 阈值 self.pnet PNet() self.rnet RNet() self.onet ONet() self.pnet.load_state_dict(torch.load(model_path /pnet.pt)) self.rnet.load_state_dict(torch.load(model_path /rnet.pt)) self.onet.load_state_dict(torch.load(model_path /onet.pt)) self.pnet.eval() self.rnet.eval() self.onet.eval()thresholds三个值分别对应 P-Net、R-Net、O-Net 的分类阈值。第一个值调低召回更多候选框但后面计算量变大调高则漏检风险上升。nms_thresholds控制重叠框合并的激进程度值越小合并越狠密集人脸场景要适当调大否则相邻两张脸会被误合并成一个。3.3 一次完整推理从读图到画框推理入口把三级网络串起来输入一张图输出框和关键点再画到图上保存。def detect(self, img): # 第一级P-Net 在金字塔上出候选框 boxes self.pnet_stage(img) # 第二级R-Net 过滤并精修 boxes self.rnet_stage(img, boxes) # 第三级O-Net 出最终框和关键点 boxes, landmarks self.onet_stage(img, boxes) return boxes, landmarks if __name__ __main__: detector MTCNN(model_path./models) img cv2.imread(test.jpg) boxes, landmarks detector.detect(img) for box in boxes: x1, y1, x2, y2 box[:4].astype(int) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(result.jpg, img)detect方法里三级依次调用前一级的输出是后一级的输入。boxes每个元素包含框坐标和得分landmarks是五个关键点的坐标。跑通后先拿一张单人清晰图验证再换多人、侧脸、暗光的图观察哪一级出问题这样排查有方向。常见做法是先把thresholds第一项降到 0.5 看召回是否变好再逐步回调找到业务能接受的平衡点。4. 避坑与排查那些让结果为空或框歪的细节MTCNN 代码本身不长但工程里踩的坑大多集中在坐标变换、阈值和输入格式上。下面几条是实际调试中反复遇到的按“现象 → 原因 → 解决”整理。4.1 检测结果为空但图里明明有人脸现象是程序正常跑完boxes却是空列表。原因通常有三个权重路径不对导致网络输出全是噪声输入图没有做 BGR 到 RGB 的转换颜色通道反了或者min_face_size设得比图里最小人脸还大金字塔直接把它过滤掉。解决方法是先打印 P-Net 原始输出的得分分布确认网络确实在工作再检查cv2.imread后有没有cv2.cvtColor(img, cv2.COLOR_BGR2RGB)最后把min_face_size调到 12 试一次。4.2 框的位置整体偏移或大小不对现象是框能出来但明显偏了或者框比脸大一圈。原因是坐标变换时缩放比例没还原回去。P-Net 在金字塔的某一层输出框这个框的坐标是相对该层缩放图的必须除以对应的scale才能映射回原图。常见错误是只还原了位置没还原宽高或者scale用错层。解决方法是把每层金字塔的scale和框一起传下去在utils.py里统一做bbox_inverse变换别在多个地方各写一遍。4.3 密集人脸被合并成一个框现象是合影里挨得近的几张脸最后只输出一个框。原因是 NMS 阈值太小重叠度稍微高一点就被判定为同一个。解决方法是把nms_thresholds调大比如从 0.7 调到 0.8 甚至 0.9让合并更保守。但调太大又会留下重复框所以要在自己的数据集上试几组值别照搬默认。4.4 GPU 和 CPU 结果不一致现象是同一张图CPU 跑和 GPU 跑框的位置差几个像素。原因是浮点运算精度和某些算子的实现差异属于正常现象。如果业务对一致性要求高常见做法是推理统一走 CPU或者固定torch.set_num_threads(1)减少并行带来的不确定性。别在这个问题上花太多时间几个像素的差异在大多数检测场景里不影响后续流程。4.5 关键点画出来是反的现象是五个关键点位置左右颠倒。原因是训练时关键点定义的顺序和推理时画点的顺序不一致或者图像做了水平翻转增强但没同步翻转标签。解决方法是把关键点顺序在代码里写死成常量比如[左眼, 右眼, 鼻尖, 左嘴角, 右嘴角]画点和后续对齐都引用同一个常量避免各处各写一套。5. 二次开发换阈值、换输入、接自己的业务跑通只是起点真正要用起来得知道改哪里、怎么验证。MTCNN 的可改造点集中在阈值、输入尺寸和输出后处理三块下面按改造目标给思路。5.1 按业务调阈值召回优先还是精度优先不同业务对检测的要求完全不同。签到系统怕漏人召回优先thresholds第一项可以降到 0.5nms_thresholds调大防止合并相册聚类怕误检精度优先第一项提到 0.7 以上宁可少框也别框错。改完别凭感觉拿一批标注好的图跑一遍统计漏检和误检数量用数据决定。参数召回优先精度优先说明thresholds[0]0.50.7P-Net 分类阈值thresholds[1]0.60.7R-Net 分类阈值nms_thresholds[0]0.80.6P-Net 的 NMS 阈值min_face_size1240最小检测人脸尺寸5.2 替换输入源从单图到视频流把detect从单图改成视频流核心是循环读帧、复用同一个检测器实例别每帧重新加载权重。视频里相邻帧结果变化不大可以加一个简单的帧间缓存隔几帧检测一次中间帧用上一帧结果速度能提不少。cap cv2.VideoCapture(0) detector MTCNN(model_path./models) frame_id 0 last_boxes [] while True: ret, frame cap.read() if not ret: break # 每 3 帧检测一次其余帧复用上次结果 if frame_id % 3 0: last_boxes, _ detector.detect(frame) for box in last_boxes: x1, y1, x2, y2 box[:4].astype(int) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imshow(mtcnn, frame) frame_id 1 if cv2.waitKey(1) 0xFF ord(q): breakframe_id % 3控制检测频率数字越大越快但框的跟随越滞后按业务对实时性的要求调。last_boxes缓存上一帧结果避免中间帧没框导致画面闪烁。5.3 接自己的后处理对齐、裁剪、入库检测出框和关键点后常见下一步是用关键点做人脸对齐把脸摆正再送识别模型。对齐的核心是根据两眼坐标算旋转角做仿射变换。这一步做完识别准确率通常能涨一截因为识别模型大多是在对齐后的人脸上训练的。def align_face(img, landmarks): 根据左右眼坐标做仿射变换返回对齐后的人脸 left_eye landmarks[0] right_eye landmarks[1] dy right_eye[1] - left_eye[1] dx right_eye[0] - left_eye[0] angle np.degrees(np.arctan2(dy, dx)) center tuple(np.mean(landmarks, axis0).astype(int)) M cv2.getRotationMatrix2D(center, angle, 1.0) aligned cv2.warpAffine(img, M, (img.shape[1], img.shape[0])) return alignedlandmarks[0]和landmarks[1]是左右眼坐标angle是两眼连线相对水平线的夹角getRotationMatrix2D生成旋转矩阵warpAffine执行变换。对齐后的人脸再按框裁剪就能直接送识别模型。这套后处理是 MTCNN 在业务里最常见的用法检测只是第一步对齐和裁剪才是衔接下游的关键。改造完一定要做回归验证拿改造前的测试集跑一遍对比漏检、误检和关键点误差确认改动没有引入新问题。我自己的习惯是每次动阈值或输入尺寸都强制走一遍固定测试集把结果存成表格对比别凭肉眼感觉。从那以后我每次改检测参数都先跑回归省了很多“改完好像好了又好像没好”的纠结。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?