资讯中心

MTCNN+MindSpore实战:三级级联网络实现人脸检测与关键点定位

📅 2026/9/26 15:53:58
MTCNN+MindSpore实战:三级级联网络实现人脸检测与关键点定位
简介一套基于华为MindSpore框架的MTCNN人脸检测与关键点定位网络实现源自北航与华为合作的《AI开源计算系统前沿技术》课程大作业包含Python源码、说明文档、数据集和训练模型。资源面向计算机视觉、人工智能方向的学生或开发者适合学习轻量级人脸检测算法以及MindSpore模型训练、转换与端侧部署的完整流程。压缩包共53个文件以19个Python脚本为核心覆盖PNet/RNet/ONet三个子网络的训练与数据生成代码另有MindIR、CKPT模型文件XML标注、TXT训练列表、JPG示例图片及README说明整体大小7.14MB目录结构清晰便于对照学习。目前已有248人学习。项目完整展示了从WIDER Face数据预处理、三阶段级联网络训练、模型导出到MindSpore Lite推理的全链路实现并附有模型转换脚本和图片/摄像头推理脚本可作为课程设计、毕业设计或初期项目立项的参考也方便在现有代码上二次开发。1. 课程大作业选MTCNNMindSpore一个能讲清楚原理的人脸识别方向如果你正在为课程大作业发愁又不想拿OpenCV的Haar级联糊弄事MTCNNMulti-task Cascaded Convolutional Networks加华为MindSpore框架这个组合值得认真考虑。它用PNet、RNet、ONet三级网络级联从粗到精地完成人脸框检测与五个关键点双眼、鼻尖、左右嘴角定位整套流程和数据准备方式都适合在课程报告里展开。先泼一盆冷水标题里的“人脸识别”严格说只做到检测和对齐真正的人脸比对还得再接一个特征提取网络MTCNN是前面负责“把脸找出来并对正”的部分别在答辩时把概念讲混了。适合有一定Python基础、想完整跑通一个视觉项目的你。2. MTCNN工作原理与MindSpore落地选型为什么要用三级网络做关键点定位2.1 三级级联的设计动机算力分配与由粗到精的搜索策略MTCNN的思路非常朴素先用一个极小的全卷积网络PNet把可能是脸的候选框全部捞出来再用中等规模的RNet把这些框逐一批判最后用ONet输出精确的人脸框和五个关键点。PNet输入12x12RNet输入24x24ONet输入48x48输入尺寸逐级翻倍意味着感受野变大、特征更精细代价是更深的网络和更大的计算量。为什么不用一个大网络一步到位因为图片中人脸尺寸不确定大脸和小脸需要不同层次的语义信息。如果直接用48x48的输入在全图上滑窗计算量完全不可接受而PNet通过卷积共享和stride2下采样可以快速扫描整张图先把候选框压缩到几百个后续网络只需要评估这批框这就是“粗到精”搜索策略的核心价值。MTCNN虽然是2016年提出的方法至今仍然值得作为课程大作业的素材因为三个子网络结构清晰、职责分明每一级都有自己的输入输出规范和损失函数。做完之后你能完整讲清楚“人脸检测的级联思想”而不是只调一个黑匣子。骨架网络全是卷积加全连接没有任何花哨模块用MindSpore手写一遍顺便能把卷积特征图尺寸变化、感受野、stride这些基础概念吃透。2.2 关键点检测的本质回归任务与坐标归一化五个关键点指的是左眼、右眼、鼻尖、左嘴角、右嘴角各有两个坐标值共10个数值。ONet的最后一层输出一个10维向量训练时用欧氏距离损失和真实的归一化坐标做比较。这里有个新手常搞混的点关键点检测是回归任务不是分类任务不能用交叉熵也不能像分类那样只看准确率。归一化是把关键点坐标除以它所在裁剪patch的宽高让数值范围落到0到1之间。如果不做这一步48x48的patch里坐标范围是0到48模型回归小数点的能力会明显变差甚至发散成几千。推理阶段再把输出结果乘回原图框的宽高还原成真实像素坐标。归一化这件事看着简单却是关键点检测能不能收敛的分水岭。2.3 人脸识别任务拆解检测、对齐、比对各管一段课程大作业标题“人脸识别和关键点检测”实际包含两个不同层次的子任务。完整刷脸流程是先检测出人脸位置再根据关键点把人脸对齐到标准姿态然后输入特征提取网络得到向量最后和库里的向量做比对。MTCNN负责前两步特征比对需要另接一个分类网络或者度量学习网络。作业报告里把“MTCNN只做检测和对齐不做特征比对”这句话写在引言里能让老师一眼看出你对任务边界有理解。很多同学答辩被追问“你的识别率是多少”时答不上来就是因为没把任务边界交代清楚——MTCNN本身没有识别准确率的概念只有检测率和关键点误差。2.4 MindSpore框架选型理由算子覆盖、API风格和模型导出选MindSpore做这件事有三个实际理由。第一MTCNN用到的都是基础算子卷积、池化、全连接、PReLU、SoftmaxMindSpore全都有对应实现不存在“框架缺算子”的问题。第二MindSpore的nn模块风格和PyTorch很像如果你之前看过PyTorch代码迁移成本很低很多类名几乎一样。第三MindSpore支持导出MindIR格式可以部署到华为的推理环境课程展示阶段这是个加分项。对纯课程作业来说用CPU跑也能完成整个训练流程只是慢一些把batch size调小、epoch适当减少即可。MindSpore还有个好处是静态图模式加速推理训练完转成静态图后三级网络级联推理的速度在CPU上也能接受。3. 用MindSpore在本地跑通数据准备链路安装、数据集与样本生成3.1 MindSpore安装CPU环境下的最小可跑通方案课程作业的第一关是把环境搭起来。我习惯先用conda创建一个独立的Python虚拟环境避免MindSpore的依赖和系统里其他包互相打架。Python版本选3.9兼容性和稳定性都比较好。然后用pip装CPU版MindSpore国内环境记得配清华源否则下载速度会让人怀疑人生。conda create -n mtcnn python3.9 -y conda activate mtcnn pip install mindspore -i https://pypi.tuna.tsinghua.edu.cn/simple装完之后验证一下import mindspore as ms print(ms.__version__)能正常输出版本号就说明安装成功。接下来还要装opencv-python和numpy这是读取图片和处理标注的常用工具组合CV方向的数据预处理基本离不开这两个库。pip install opencv-python numpy -i https://pypi.tuna.tsinghua.edu.cn/simple用VSCode打开项目时记得通过命令面板把Python解释器切换到mtcnn这个虚拟环境不然运行时用的还是系统Python报“找不到mindspore模块”的错。这个细节看起来不起眼却是新手最容易卡住的环节。CPU版本的MindSpore能覆盖MTCNN所有算子的前向和反向计算训练PNet时一个batch 32张12x12输入基本没有压力瓶颈主要在大图预处理和样本生成阶段。3.2 数据准备WIDER Face和CelebA怎么配合使用MTCNN训练的数据来源主要分两部分人脸框检测用WIDER Face关键点用CelebA。WIDER Face的标注方式是每张图片对应一个txt文件按行记录人脸框左上角和右下角坐标CelebA的标注在list_landmarks_align_celeba.txt里包含五个关键点的像素坐标。两个数据集格式不同课程作业里有个省事的做法只用CelebA完成全部训练因为它同时具备人脸框和关键点。代价是CelebA的人脸大多居中、角度小模型对侧脸的泛化能力会比官方模型差。如果时间充裕建议按“WIDER Face提供框样本、CelebA提供关键点样本”的方式组合数据这也是MTCNN官方训练的标准做法。实操时先读标注文件把每张图的人脸框和关键点存成字典结构后续生成样本会反复用到。这里给出读取CelebA标注的基础代码import numpy as np def load_celeba_landmarks(anno_file): 解析CelebA关键点标注文件 返回: {图片名: {box: [x1,y1,x2,y2], landmarks: [[x,y]*5]}} data {} with open(anno_file, r) as f: line f.readline() while line: parts line.strip().split() if len(parts) 11: line f.readline() continue img_name parts[0] # CelebA的框是(x, y, width, height)格式 x, y, w, h [int(float(v)) for v in parts[1:5]] box [x, y, x w, y h] # 后10个值是5个关键点的x, y坐标 kp_values [float(v) for v in parts[5:15]] landmarks [(kp_values[i], kp_values[i 1]) for i in range(0, 10, 2)] data[img_name] {box: box, landmarks: landmarks} line f.readline() return data注意这里把宽高格式转换成了左上右下两点格式后续计算IoU就统一了。如果用的是WIDER Face格式本身就是x1y1x2y2不需要额外转换但WIDER Face没有关键点标注需要写一个单独的函数去解析它的txt格式。3.3 生成PNet训练样本IoU判定与样本分类训练样本的生成逻辑是MTCNN里第一个真正的难点。思路是对每张图片做随机裁剪裁剪出来的patch和图片里所有人脸框算IoU按IoU值分成三类IoU大于0.65的是正样本小于0.3的是负样本0.4到0.65之间的是部分样本。正样本和部分样本参与框回归损失负样本只参与分类损失。import cv2 def compute_iou(crop_box, gt_boxes): 计算裁剪框与所有人脸框的IoU x1 np.maximum(crop_box[0], gt_boxes[:, 0]) y1 np.maximum(crop_box[1], gt_boxes[:, 1]) x2 np.minimum(crop_box[2], gt_boxes[:, 2]) y2 np.minimum(crop_box[3], gt_boxes[:, 3]) inter_w np.maximum(x2 - x1, 0) inter_h np.maximum(y2 - y1, 0) inter_area inter_w * inter_h crop_area (crop_box[2] - crop_box[0]) * (crop_box[3] - crop_box[1]) gt_area (gt_boxes[:, 2] - gt_boxes[:, 0]) * (gt_boxes[:, 3] - gt_boxes[:, 1]) union_area crop_area gt_area - inter_area 1e-6 return inter_area / union_area有了IoU计算函数接下来写样本生成循环。每张训练图随机裁剪若干次每次裁剪得到的patch按IoU归类并resize到目标尺寸保存同时记录类别标签。PNet输入是12x12RNet是24x24ONet是48x48生成样本时只需要改resize的目标尺寸。负样本和正样本的比例要严格控制。经验值是负样本最多不超过正样本的3倍否则网络会严重偏向“什么都不是人脸”这个类别训练出来的模型什么都不检测。部分样本数量介于正负之间它只参与回归不参与分类不要让它把正样本的占比稀释掉。3.4 关键点标注同步归一化翻转时的左右眼交换从CelebA裁剪patch时关键点要同步裁剪并把坐标转换到patch局部坐标系里。假如patch左上角在原图是(patch_x1, patch_y1)patch宽高是pw和ph关键点kp在patch内的坐标就是(kp_x - patch_x1) / pw和(kp_y - patch_y1) / ph除以宽高完成归一化。def crop_landmark_patch(img, box, landmarks, target_size): 从原图裁剪人脸patch关键点坐标同步转换并归一化 box: [x1, y1, x2, y2]landmarks: 5个坐标点 x1, y1, x2, y2 [int(v) for v in box] # 在框周围随机扩展一点模拟真实检测误差 expand np.random.randint(-5, 10) x1 max(0, x1 - expand) y1 max(0, y1 - expand) x2 min(img.shape[1], x2 expand) y2 min(img.shape[0], y2 expand) patch img[y1:y2, x1:x2] patch cv2.resize(patch, (target_size, target_size)) pw x2 - x1 ph y2 - y1 kp_norm [] for (kx, ky) in landmarks: kx_norm (kx - x1) / pw ky_norm (ky - y1) / ph kp_norm.append([kx_norm, ky_norm]) return patch, np.array(kp_norm, dtypenp.float32).reshape(-1)这里踩过一个坑数据增强做随机水平翻转时关键点的顺序要跟着变否则左眼和右眼的坐标对调了网络却不知道。翻转后左眼坐标变成了右眼位置标签对不上关键点loss会一直降不下去。解决方法是翻转时间时交换关键点索引左眼和右眼互换左嘴角和右嘴角互换鼻尖不变。4. 用MindSpore实现PNet/RNet/ONet网络定义、损失与训练4.1 PNet全卷积结构与输出三路分支PNet输入12x12x3结构是四层卷积前面三层是普通卷积加PReLU最后一层用1x1卷积把通道分别映射到分类、框回归和关键点回归三个分支。因为PNet是全卷积结构推理时可以直接输入整张大图输出是一张热力图每一格对应原图的一个候选窗口。import mindspore.nn as nn class PNet(nn.Cell): def __init__(self): super(PNet, self).__init__() self.conv1 nn.Conv2d(3, 10, kernel_size3, stride1, pad_modesame) self.prelu1 nn.PReLU(channel10) self.pool1 nn.MaxPool2d(kernel_size2, stride2) self.conv2 nn.Conv2d(10, 16, kernel_size3, stride1, pad_modesame) self.prelu2 nn.PReLU(channel16) self.conv3 nn.Conv2d(16, 32, kernel_size3, stride1, pad_modesame) self.prelu3 nn.PReLU(channel32) # 三路输出1x1卷积只改变通道数不改变空间尺寸 self.conv4_cls nn.Conv2d(32, 2, kernel_size1, stride1) self.conv4_box nn.Conv2d(32, 4, kernel_size1, stride1) self.conv4_kp nn.Conv2d(32, 10, kernel_size1, stride1) def construct(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_cls(x) box self.conv4_box(x) kp self.conv4_kp(x) return cls, box, kp两个参数细节值得说。一是PReLU必须传channel参数MindSpore 2.0之后这个参数是必填的不写会直接报错二是卷积用了pad_modesame配合stride1时输出尺寸和输入一致。MindSpore里如果想手动补零需要把pad_mode改成pad再传padding参数新手第一次写经常在这两个参数上翻车。4.2 RNet与ONet结构差异和Dense层尺寸计算RNet输入24x24前几层卷积结构和PNet类似但后面接了全连接层。ONet输入48x48网络更深最后输出的人脸框偏移、关键点坐标和分类分数都是通过全连接层直接得到的。class RNet(nn.Cell): def __init__(self): super(RNet, self).__init__() self.conv1 nn.Conv2d(3, 28, kernel_size3, stride1, pad_modesame) self.prelu1 nn.PReLU(channel28) self.pool1 nn.MaxPool2d(kernel_size3, stride2, pad_modesame) self.conv2 nn.Conv2d(28, 48, kernel_size3, stride1, pad_modesame) self.prelu2 nn.PReLU(channel48) self.pool2 nn.MaxPool2d(kernel_size3, stride2, pad_modesame) self.conv3 nn.Conv2d(48, 64, kernel_size2, stride1) self.prelu3 nn.PReLU(channel64) self.flatten nn.Flatten() # 注意这里Dense输入维度取决于conv3输出的feature map尺寸 # 24 - pool1 - 12 - pool2 - 6 - conv3(2x2) - 5 self.dense1 nn.Dense(64 * 5 * 5, 128) self.prelu4 nn.PReLU(channel128) self.dense_cls nn.Dense(128, 2) self.dense_box nn.Dense(128, 4) self.dense_kp nn.Dense(128, 10) def construct(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.flatten(x) x self.prelu4(self.dense1(x)) cls self.dense_cls(x) box self.dense_box(x) kp self.dense_kp(x) return cls, box, kpDense层的输入维度是这里最容易写错的地方。计算方式是24x24输入经过两次stride2的池化变成6x6再经过一个2x2无padding的卷积变成5x5所以展平后的维度是64x5x5。如果改了池化的padding策略或卷积核尺寸这个数要重新算。ONet的结构类似但网络更深输入48x48最终展平后维度更大输出层多了一个256维的中间全连接。通道数28、48、64是原论文里的配置改小能减小模型体积但精度会降。做课程作业没必要改直接抄这个配置报告里写清楚参数选择的依据就行。4.3 多任务损失与难例挖掘训练不崩的核心MTCNN的损失是三路加权和分类损失用交叉熵只计算正样本和负样本框回归损失用欧氏距离只计算正样本和部分样本关键点损失用欧氏距离只计算有标注关键点的样本。总loss是三者的加权和权重一般取1:0.5:0.5。难例挖掘是训练能收敛的关键。具体做法是对batch里的负样本按loss从大到小排序只取前70%参与反向传播剩下的简单负样本直接忽略。目的是让网络专注学那些“看起来像脸但实际不是脸”的困难样本否则大量简单负样本会淹没梯度。import mindspore as ms import mindspore.ops as ops import mindspore.common.dtype as mstype class MtcnnLoss(nn.Cell): def __init__(self, neg_keep_ratio0.7): super(MtcnnLoss, self).__init__() self.neg_keep_ratio neg_keep_ratio self.cls_fn nn.SoftmaxCrossEntropyWithLogits(sparseTrue, reductionnone) self.eps 1e-6 def construct(self, pred_cls, pred_box, pred_kp, gt_cls, gt_box, gt_kp, box_mask, kp_mask): # 分类loss: 所有样本先算再通过mask保留有效样本 cls_loss_all self.cls_fn(pred_cls, gt_cls) # 难例挖掘负样本按loss排序取前70% neg_index ops.nonzero(gt_cls 0).squeeze(1) hard_mask ops.ones(cls_loss_all.shape, ms.float32) if neg_index.shape[0] 0: neg_loss ops.gather(cls_loss_all, neg_index, 0) keep_num max(1, int(neg_index.shape[0] * self.neg_keep_ratio)) _, top_idx ops.top_k(neg_loss, keep_num) keep_neg ops.gather(neg_index, top_idx, 0) hard_mask ops.ones(cls_loss_all.shape, ms.float32) # 被保留的负样本位置置1其余负样本位置置0 zeros_mask ops.zeros(cls_loss_all.shape, ms.float32) hard_mask ops.tensor_scatter_update( zeros_mask, keep_neg.reshape(-1, 1), ops.ones(keep_neg.shape, ms.float32)) cls_loss (cls_loss_all * hard_mask).sum() / (hard_mask.sum() self.eps) # 框回归loss只算正样本和部分样本 box_diff pred_box - gt_box box_loss (box_diff * box_diff).sum(axis1) * box_mask box_loss box_loss.sum() / (box_mask.sum() self.eps) # 关键点loss只算有标注的样本 kp_diff pred_kp - gt_kp kp_loss (kp_diff * kp_diff).sum(axis1) * kp_mask kp_loss kp_loss.sum() / (kp_mask.sum() self.eps) return cls_loss 0.5 * box_loss 0.5 * kp_loss解释一下几个mask的用途。box_mask只在正样本和部分样本位置为1kp_mask只在有关键点标注的样本位置为1这两个mask在数据加载时就和标签一起放进batch里了。难例挖掘那一小段代码如果觉得绕可以单独抽出来画个流程图再理解这是整个MTCNN训练里最考验代码功底的部分。4.4 训练循环与超参数PNet到ONet的先后顺序三网络的训练顺序是PNet先训然后训RNet最后训ONet。每个网络训练时用到的正负样本生成方式类似只是patch尺寸不同。训练PNet时用12x12的patchRNet用24x24ONet用48x48。不同网络单独训练不要试图一次性训三个网络。from mindspore import nn, ms, ops net PNet() loss_fn MtcnnLoss() optimizer nn.Adam(net.trainable_params(), learning_rate0.001) def forward_fn(img, gt_cls, gt_box, gt_kp, box_mask, kp_mask): pred_cls, pred_box, pred_kp net(img) return loss_fn(pred_cls, pred_box, pred_kp, gt_cls, gt_box, gt_kp, box_mask, kp_mask) grad_fn ms.value_and_grad(forward_fn, None, optimizer.parameters) for epoch in range(30): total_loss 0 for batch in dataset.create_dict_iterator(): loss, grads grad_fn(batch[img], batch[cls], batch[box], batch[kp], batch[box_mask], batch[kp_mask]) optimizer(grads) total_loss float(loss.asnumpy()) print(fepoch {epoch}, loss {total_loss / len(dataset):.4f})这里用了MindSpore 2.x的函数式训练写法没有zero_grad()这一步梯度由optimizer(grads)内部处理。如果你从PyTorch迁过来记住这个差异不然会报参数找不到的错。超参数参考优化器用Adam初始学习率0.001前15个epoch保持0.001后15个epoch衰减到0.0001。batch size在CPU上取128比较稳妥GPU可以提到512。PNet训练30个epoch在CPU上大约需要几个小时RNet和ONet数据量小一些耗时反而更短。每个epoch结束后把模型保存一份后面推理会用到。MindSpore保存和加载checkpoint的方式是ms.save_checkpoint(net, pnet.ckpt) # 加载时 ms.load_checkpoint(pnet.ckpt, net)训练过程中建议盯住loss曲线。分类loss通常在0.2到0.5之间波动如果一直等于0.693附近不动说明网络什么都没学到正负样本比例大概率出了问题。5. MTCNNMindSpore训练避坑5个高频翻车现场与排查方法5.1 PNet训练几个小时loss纹丝不动现象分类loss稳定在0.69左右约等于ln2训练多个epoch没有任何下降趋势验证时所有输入都被判成非人脸。原因正负样本比例严重失衡负样本占比过高网络学到的最优策略就是“永远输出非人脸”因为这样分类loss最小。另一种可能是样本生成时IoU阈值写错导致正样本文件里混入了大量负样本。解决方法先检查生成的样本目录正样本和负样本的数量比应控制在1:2到1:3之间。再检查IoU计算函数可以把随机裁剪的结果可视化出来把裁剪框和人脸框画在同一张图上确认。如果比例没问题把难例挖掘临时关掉跑几个epoch确认loss能降再逐步加回来。5.2 关键点坐标输出几千甚至上万现象训练loss是收敛的loss数值也不大但推理时输出的关键点坐标是2000、5000这种明显不合理的数值。原因训练时关键点标签没有做归一化直接把原图像素坐标丢给了网络。模型在回归0到50范围内的数值时因为训练样本里坐标分布极不均匀网络学偏了。另一个常见原因是加载checkpoint后没有调用net.set_train(False)模型仍处于训练模式PReLU和Dropout行为不对导致输出异常。解决方法确认样本生成时关键点坐标统一除以patch宽高范围控制在0到1之间。推理时先model.set_train(False)把模型固定到eval模式再对输入图片做和训练时一致的预处理最后把输出坐标乘回原图框尺寸就能正常画点了。5.3 MindSpore 2.x下PReLU报参数错误现象实例化PNet时直接报ValueError提示PReLU参数数量不匹配。网上搜出来的老代码是nn.PReLU()不带参数照抄就报错。原因MindSpore 2.0之后PReLU的接口变了channel参数从可选变成必填不再允许不传参数直接实例化。这也是从旧博客抄代码最容易踩的坑。解决方法统一写成nn.PReLU(channel通道数)每个PReLU都要带channel参数。如果确实不想传也可以用nn.LeakyReLU(0.25)替代训练效果差别不大PReLU的channel参数误配问题也绕开了。5.4 训练正常但推理检测率断崖式下降现象训练集上loss正常但拿一张测试图推理时一个框都检不出来或者检测框的位置整体偏移。原因推理时的预处理和训练时不一致。常见的有三种情况图像金字塔的缩放因子和训练时不同、输入图片没有转换通道顺序OpenCV读出来是BGR模型训练时用的是RGB、测试图片存在全图缩放而训练样本是随机裁剪。解决方法训练和推理共用一套预处理函数把resize、通道转换、归一化全部统一。写推理代码时直接复用数据加载里的预处理逻辑不要嫌麻烦重新写一遍。对检测框偏移的情况重点检查PNet输出坐标映射回原图的公式stride和感受野的换算错了会出现框整体往左上偏的症状。5.5 图像金字塔参数不会调小脸检不出、大脸误检多现象测试图片上方有个小脸检测不到但下半部分的大脸正常或者大脸周围出现大量重叠的误检框。原因minsize参数设得太大小于这个尺寸的人脸直接被跳过了scale_factor设得不够小金字塔层数太多推理耗时成倍增长。这两个参数是衡量检测效果最直接的旋钮。解决方法原论文默认minsize20实际做作业时建议收紧到40因为PNet在小人脸上的召回率本来就有限与其检出一堆错误候选框增加后级网络负担不如把阈值提高。scale_factor固定用0.709这是原论文给的值对应每次缩放把短边缩小约30%兼顾了召回率和速度。调这两个参数时用一张包含大小脸对比的测试图最快能看出效果差异。6. 从模型到成果三级推理串联与课程展示验证6.1 推理流程图像金字塔、NMS与逐级精修训练完三个网络最后一步是把它们串成完整的检测管线。推理时先对原图做图像金字塔缩放把不同尺度的人脸都缩放到PNet能识别的范围然后逐级精修。这个流程是MTCNN能处理任意尺寸人脸的关键也是报告里最值得展开写的部分。def pnet_detect(img, pnet, minsize40, scale_factor0.709, threshold0.6): PNet粗筛返回原图坐标系下的候选框 h, w img.shape[:2] min_side min(h, w) scale 12.0 / minsize all_boxes [] while min_side * scale 12: new_w, new_h int(w * scale), int(h * scale) resized cv2.resize(img, (new_w, new_h)) tensor Tensor(resized.transpose(2, 0, 1)[None], ms.float32) / 127.5 - 1.0 cls, box, _ pnet(tensor) cls cls[0, 1].asnumpy() # 取人脸类别的概率 box box[0].asnumpy() idx np.where(cls threshold) for (x, y) in zip(idx[1], idx[0]): # 注意是(x, y)不是(y, x) score cls[y, x] # 热力图坐标映射回原图 x1 (x * 2) / scale y1 (y * 2) / scale x2 (x * 2 12) / scale y2 (y * 2 12) / scale # bbox偏移修正格式为[dx1, dy1, dx2, dy2] bbox box[:, y, x] * 12.0 / scale x1 bbox[0] y1 bbox[1] x2 bbox[2] y2 bbox[3] all_boxes.append([x1, y1, x2, y2, score]) scale * scale_factor return np.array(all_boxes)这段代码里最容易被忽略的是热力图坐标和原图坐标的换算。PNet的stride是2输出热力图上的一个点(x, y)对应原图位置(x2, y2)候选框的边长是12个像素映射回原图要除以当前金字塔的scale。如果不除以scale小脸检测框会全部偏大这是级联检测里最常见的坐标映射错误。得到候选框后接NMS去重再做RNet和ONet的逐级精修最终输出带关键点的检测结果。6.2 可视化验证让老师一眼看懂你在做什么课程作业的验收重点是“能讲清楚原理”加“能跑通流程”。建议做三组可视化对比单人正脸、多人合影、大角度侧脸每组图同时画出检测框和关键点贴到报告里。再做一个消融对比开启难例挖掘和不开启难例挖掘的检测效果差异这个对比能让老师看到你对训练机制有深入理解。报告结构按四章走就行。第一章写问题背景明确“人脸识别任务拆解为检测、对齐、识别”交代清楚MTCNN负责的边界第二章写三级网络结构和级联思想第三章写MindSpore实现细节附上损失函数和难例挖掘的核心代码第四章放实验可视化结果加上不同minsize和threshold参数下的检测效果对比表。附录里放环境版本说明、数据集来源和文件清单。每次做完视觉方向的作业我都习惯先把“这个模型不做什么”写进引言答辩被追问边界问题时靠这句话能省掉不少解释。希望你这次也能把MTCNN吃透做出一个能经得起追问的课程大作业希望帮到你。本文还有配套的精品资源点击获取

看完文章,想为自己的企业也做一次专业网站诊断?

尧图顾问免费为您评估现有网站,并给出建站/改版建议与报价方案。

免费获取方案