资讯中心

MindSpore复现MTCNN人脸检测与关键点定位实践

📅 2026/10/5 15:58:12
MindSpore复现MTCNN人脸检测与关键点定位实践
简介基于华为MindSpore框架的MTCNN人脸识别与关键点检测课程大作业源码包源自北航与华为合作的《AI开源计算系统前沿技术》课程面向希望在端侧部署人脸检测算法的学生与开发者。工程采用轻量级backbone覆盖PNet、RNet、ONet三阶段级联网络的训练、推理与模型转换流程并包含MindSpore Lite端侧部署思路适合计算机、人工智能等相关专业的课程设计或毕业设计参考。压缩包共53个文件约7.14MB核心为19个Python脚本和12个编译后的pyc文件另有4个txt说明/标签文件、3个ckpt权重、3个mindir推理模型、5个xml项目配置及jpg测试样例等目录按train_PNet、train_RNet、train_ONet、models、utils等模块组织便于按阶段检索学习。目前已有248人学习下载压缩包内附README说明与数据文件从数据生成、训练、测试到导出模型均有对应代码可帮助读者快速理解MTCNN在华为生态中的完整实现链路。1. 课程大作业选 MindSpore 复现 MTCNN人脸识别和关键点检测一起拿分做课程大作业时,最怕的就是算法演示完了,老师问一句你这模型能训练吗?然后卡壳。基于华为 MindSpore 框架实现的人脸识别和关键点检测网络 MTCNN,恰好是那种代码结构清晰、训练链路完整、可视化效果好的选题一个前向推理能把人脸框和五个关键点(双眼、鼻尖、嘴角)同时输出,既能展示深度学习基础,又能蹭上国产框架的热点,答辩时天然有话题。这个项目通常自带 python 源码、文档说明、数据和预训练模型,但如果你只把它当跑通 demo就亏了——真正的价值在于把 MTCNN 的三级级联架构、MindSpore 的张量接口和训练数据管线串起来,遇到问题自己能修。本文按我实际做这门大作业的顺序来写先讲清楚网络怎么搭,再讲数据怎么喂,然后讲训练怎么调,最后讲推理和避坑。2. MTCNN 在 MindSpore 下的网络结构与推理流程2.1 P-Net/R-Net/O-Net三阶段级联为什么省算力MTCNN 全称 Multi-task Cascaded Convolutional Networks,核心思想不是用一个重型网络一次出结果,而是用三个小网络从粗到细过一遍。P-Net(Proposal Network)输入是 12×12 的图,用少量卷积快速扫一遍全图,产出大量候选框R-Net(Refine Network)输入 24×24,把 P-Net 的候选框精修一遍,去掉大部分误检O-Net(Output Network)输入 48×48,精度最高,不仅输出最终框,还输出五个关键点坐标。人脸识别门禁机、手机相机的人脸框,很多轻量方案都在用这个思路。在 MindSpore 里复现时,没必要从零手写卷积,直接用nn.Conv2d、nn.PReLU、nn.MaxPool2d这些高层 API。P-Net 的输出层比较特殊它除了分类分支(2 个输出,是人脸/非人脸)和框回归分支(4 个输出),还有一个关键点分支(10 个输出,5 个点各 x/y),只是实际训练时 P-Net 的关键点分支常常被忽略或置零。O-Net 才是真正输出关键点的网络。2.2 MindSpore 算子选型与模型定义Conv/PReLU/Softmax 的映射MindSpore 的nn接口和 PyTorch 很像,但有三个地方要单独留意。第一是填充策略,原版 MTCNN 卷积层用的是SAME填充,在 MindSpore 里要写成pad_modesame,不写默认是valid,会导致特征图尺寸对不上第二是 PReLU 的权重初始化,P-Net/R-Net 这类小网络对 PReLU 的weight初始值不敏感,但最好用 0.25 初始化,不然前期训练分类分支容易震荡第三是 Softmax 只在推理时用,训练时用nn.SoftmaxCrossEntropyWithLogits,MindSpore 的SoftmaxCrossEntropyWithLogits会把 softmax 和交叉熵合并计算,数值更稳。下面是一段 P-Net 的 MindSpore 定义,选自课程作业里常见的源码结构import mindspore.nn as nn import mindspore.ops as ops from mindspore.common.initializer import HeNormal, Constant class PNet(nn.Cell): def __init__(self): super(PNet, self).__init__() self.conv1 nn.Conv2d(3, 10, kernel_size3, pad_modesame, weight_initHeNormal()) self.prelu1 nn.PReLU(10, w_initConstant(0.25)) self.pool1 nn.MaxPool2d(kernel_size2, stride2, pad_modesame) self.conv2 nn.Conv2d(10, 16, kernel_size3, pad_modesame, weight_initHeNormal()) self.prelu2 nn.PReLU(16, w_initConstant(0.25)) self.conv3 nn.Conv2d(16, 32, kernel_size3, pad_modesame, weight_initHeNormal()) self.prelu3 nn.PReLU(32, w_initConstant(0.25)) # 分类分支1x1 卷积输出 2 个通道(人脸/非人脸) self.conv4_1 nn.Conv2d(32, 2, kernel_size1, pad_modesame, weight_initHeNormal()) # 框回归分支输出 4 个偏移量 self.conv4_2 nn.Conv2d(32, 4, kernel_size1, pad_modesame, weight_initHeNormal()) # 关键点分支输出 10 个值(5 点 x,y) self.conv4_3 nn.Conv2d(32, 10, kernel_size1, pad_modesame, weight_initHeNormal()) def construct(self, x): x self.pool1(self.prelu1(self.conv1(x))) x self.prelu2(self.conv2(x)) x self.prelu3(self.conv3(x)) cls self.conv4_1(x) box self.conv4_2(x) landmark self.conv4_3(x) return cls, box, landmark逻辑说明construct是 MindSpore 的nn.Cell前向入口,不能用forward。 头部三个 1×1 卷积分别对应分类、框回归、关键点回归,1×1 卷积本质是跨通道线性组合,不改变特征图空间尺寸,所以能和原版 MTCNN 保持一致。 参数说明pad_modesame保证 12×12 输入经过 3×3 卷积后还是 12×12,MaxPool2d 之后变成 6×6HeNormal初始化适合 ReLU/PReLU 族激活,能避免深层网络梯度消失。 如果你在 GPU 上跑,可以把pool1的pad_mode也设为same,这样 P-Net 的任意尺寸输入都能前向,方便后续构建图像金字塔。2.3 关键点检测的回归头5 点坐标如何在 O-Net 输出O-Net 的结构比 P-Net 深,多了两个全连接层。关键点分支在全连接层之后输出 10 个值,顺序是[left_eye_x, left_eye_y, right_eye_x, right_eye_y, nose_x, nose_y, left_mouth_x, left_mouth_y, right_mouth_x, right_mouth_y]。注意,训练标签一般也是这个顺序,但有些开源数据集的标注顺序是[left_eye, right_eye, nose, left_mouth, right_mouth],如果你从网上下载的代码里看到坐标对不上,优先怀疑顺序问题。O-Net 的回归头是全连接层,输入是 48×48×64 特征图展平后的向量,输出维度就是 10。MindSpore 里nn.Dense(64*3*3, 10)之后,不需要再接激活函数,因为回归目标是无界的偏移量。真正落地时,关键点的预测值要除以图像尺寸做归一化,否则不同分辨率下损失函数数值差异很大。3. 从数据集到 MindRecord人脸检测训练数据的预处理链路3.1 WIDER Face 与 CelebA 的标注格式bbox 和关键点怎么读MTCNN 训练通常用两个公开数据集WIDER Face 提供大量人脸框标注,用于分类和框回归训练CelebA 提供 5 个关键点标注,用于关键点回归。WIDER Face 的标注是 txt 格式,每张图先写一行路径,下一行是人脸数,再往下每行是x1 y1 x2 y2四个整数。CelebA 的标注是image_id x1 y1 w h landmark_x1 landmark_y1 ...,需要自己换算成 x2、y2。课程大作业数据量不大,常见做法是直接用 WIDER Face 的wider_face_train_bbx_gt.txt,把坐标从 (x, y, w, h) 转成 (x1, y1, x2, y2),再过滤掉宽高小于 20 像素的极小框。关键点数据可以只用 CelebA 的前几万张,或者干脆用 WIDER Face 里自带的少量关键点标注(部分版本有),免得数据太大训练时间失控。3.2 生成 MindRecord把图像和标签转成 MindSpore 原生格式MindSpore 训练时的数据管线推荐用 MindRecord 格式,类似 TFRecord。一次性把所有图像路径和标签写进一个.mindrecord文件,训练时用MindDataset读取,IO 效率比每次从硬盘读图加解析高很多。下面这段代码是把标注文件转成 MindRecord 的骨架from mindspore.mindrecord import FileWriter def create_mindrecord(annotation_lines, output_path): # 每条样本是一个 dict,字段和 MindDataset 的 schema 对应 data [] for line in annotation_lines: img_path, boxes, landmarks parse_line(line) # 图像路径作为字符串存储,读取时再解码 sample { image: img_path, boxes: boxes, # shape: [n, 4], float32 landmarks: landmarks, # shape: [n, 10], float32 num_boxes: len(boxes) } data.append(sample) # 定义 schema,字段类型和形状必须严格声明 schema { image: {type: string}, boxes: {type: float32, shape: [-1, 4]}, landmarks: {type: float32, shape: [-1, 10]}, num_boxes: {type: int32} } writer FileWriter(output_path, shard_num1) writer.add_schema(schema, mtcnn_train) writer.write_raw_data(data) writer.commit() # 说明Image path 只是字符串,不存二进制,防止 .mindrecord 文件过大逻辑说明FileWriter分片数shard_num设 1 即可,数据量小不用拆多个文件。boxes的shape用[-1, 4]表示第一维可变,因为每张图的人脸数不同,这是 MindRecord 支持的动态维度写法。 参数说明image字段为什么存路径而不是像素因为 MindRecord 里存图片二进制会让文件膨胀,并且图像增强在读取时做更灵活。 训练时用MindDataset读取后,再配合map操作做解码、裁剪、归一化如果你之后要加新数据集,只需要改parse_line函数,保持 sample 的四个字段不变。3.3 在线增强的坑随机裁剪要和 IOU 阈值联动MTCNN 训练的核心是生成正样本、部分样本、负样本。常见做法是对每张训练图,随机裁剪若干区域,计算裁剪框与真实框的 IOU,IOU 0.65 标为正样本,0.4 IOU 0.65 标为部分样本,IOU 0.3 标为负样本。部分样本只参与框回归,不参与分类负样本只参与分类,不参与框回归和关键点回归。这里最容易翻车的是随机裁剪的坐标范围。如果裁剪框完全随机,大概率裁到背景,负样本多到爆炸如果只围绕真实框裁剪,正样本又太单一。我一般这样控制以真实框中心为基准,在边长的 [0.8, 1.2] 倍范围内随机取裁剪框,再对裁剪框做镜像、亮度抖动。MindSpore 的RandomCrop是不带 IOU 逻辑的,所以必须自己写 Python 函数,放到mindspore.dataset的map里。import random import numpy as np def random_crop_with_iou(image, boxes, landmarks, size): h, w, _ image.shape # 尝试 20 次随机裁剪,直到满足 IOU 条件 for _ in range(20): crop_w random.randint(int(size*0.8), int(size*1.2)) crop_h random.randint(int(size*0.8), int(size*1.2)) left random.randint(0, w - crop_w) top random.randint(0, h - crop_h) crop np.array([left, top, left crop_w, top crop_h]) ious compute_iou(crop, boxes) # 和所有真实框算 IOU if ious.max() 0.65: return crop_image(image, crop), crop_boxes, crop_landmarks # 20 次都不命中就退回最大 IOU 的裁剪 return largest_iou_crop(image, boxes, landmarks)逻辑说明size是目标网络输入尺寸,训练 P-Net 时传 12,R-Net 传 24,O-Net 传 48。 20 次随机尝试是常见的折中,试太少正样本不足,试太多训练启动慢。 参数说明compute_iou建议用向量化写法,一次算完所有框的 IOU,别写两层 for 循环,否则数据管线会成为瓶颈。 这个函数要返回裁剪后的图、调整后的框和关键点坐标,并且关键点坐标也要跟着裁剪偏移同步减掉left, top。很多新手只裁剪图像、忘记把关键点坐标平移,导致关键点标签全错,模型怎么训都不收敛。4. 在 MindSpore 上训练 MTCNN损失函数、采样策略与收敛性检查4.1 三个任务的损失相加分类/回归/关键点如何配平MTCNN 的总损失是分类损失、框回归损失、关键点回归损失的加权和,常见公式是L L_cls 0.5 * L_box 0.5 * L_landmark但要注意,P-Net 和 R-Net 阶段关键点任务不是重点,权重可以设 0.1O-Net 阶段才把关键点权重提到 0.5。框回归和关键点回归都使用欧氏距离损失,注意只对有效样本计算框回归只计算部分样本和正样本,关键点回归只计算正样本。MindSpore 里可以用掩码矩阵把无效位置置零,然后对有效位置求均值。import mindspore.ops as ops from mindspore import Tensor from mindspore import dtype as mstype def mtcnn_loss(cls_pred, box_pred, lm_pred, cls_label, box_label, lm_label, valid_mask): # valid_mask: 每个样本是否参与对应任务,shape [B, 1/4/10] cls_loss ops.SoftmaxCrossEntropyWithLogits()(cls_pred, cls_label) cls_loss cls_loss * valid_mask[:, 0:1] cls_loss cls_loss.mean() box_loss ops.reduce_sum((box_pred - box_label) ** 2, 1) box_loss box_loss * valid_mask[:, 1:2] box_loss box_loss.mean() lm_loss ops.reduce_sum((lm_pred - lm_label) ** 2, 1) lm_loss lm_loss * valid_mask[:, 2:3] lm_loss lm_loss.mean() return cls_loss 0.5 * box_loss 0.5 * lm_loss逻辑说明valid_mask的构造要跟样本类型对应——负样本只有第 0 位是 1,部分样本第 0、1 位是 1,正样本三位都是 1。 为什么要用reduce_sum((...)**2, 1)而不是nn.MSELoss因为 MTCNN 的框回归和关键点回归都是多输出,MSELoss默认对所有元素取平均,会把四个坐标的误差混合,不方便按样本做掩码。 参数说明损失里对box_loss和lm_loss乘了 0.5,如果发现框不准,可以把框回归权重提到 1.0如果关键点抖动大,关键点权重提到 1.0。这个权重没有绝对标准,课程作业里我建议先用默认值跑 10 个 epoch,再看验证集表现调。4.2 难样本挖掘与在线采样让 P-Net 不学废P-Net 网络小、感受野有限,如果每张图只取 1 个正样本、1 个负样本,训练会很不稳定。常见做法是每张图固定采 64 个样本32 个负样本、16 个正样本、16 个部分样本。如果某张图没有足够正样本,就重复已有正样本,或者干脆跳过这张图。难样本挖掘是另一个关键点。训练初期负样本太好区分,损失很小,梯度被大量简单负样本淹没。MindSpore 里可以直接在损失函数里做把每个 batch 的负样本损失排序,只取前 70% 的负样本参与反传。def hard_negative_mining(cls_loss, cls_label, neg_mask, ratio0.7): # cls_loss: [B, 2], 取人脸类的 loss face_score ops.Softmax(cls_loss)[:, 1] # 只对负样本(标签为0)做排序 neg_loss face_score * neg_mask k max(1, int(neg_mask.sum() * ratio)) # 取 top-k 大的 loss 作为有效负样本 top_k ops.top_k(neg_loss, k) return top_k.values.mean()逻辑说明负样本的face_score越大说明模型越觉得它是人脸,也就是分错的难样本,所以取 top-k 的 loss 来训练。ratio0.7是论文里的常用值,训练后期可以降到 0.5,让梯度更聚焦。 参数说明ops.top_k在 MindSpore CPU 上也能跑,但k不能超过样本数,所以代码里加了max(1, ...)兜底。如果 batch size 很小,比如 16,这个方法会退化成取全部,不要因此怀疑代码写错。4.3 用 ModelCheckpoint 恢复训练中间断点怎么续MindSpore 的ModelCheckpoint回调可以每个 epoch 存一次 ckpt,但默认只存网络权重,不存优化器状态。课程作业经常遇到训练了 20 个小时,断电了,重新跑的惨剧。正确的做法是同时保存优化器状态和 epoch 数,MindSpore 的CheckpointConfig支持append_dict把额外信息写进 checkpoint。from mindspore.train.callback import CheckpointConfig, ModelCheckpoint from mindspore.train.serialization import save_checkpoint # 这里假设 trainer 是 mindspore.Model config_ck CheckpointConfig(save_checkpoint_steps1000, keep_checkpoint_max5, append_dict{epoch: current_epoch, best_loss: best_loss}) ckpoint_cb ModelCheckpoint(prefixmtcnn_pnet, directory./ckpt, configconfig_ck) model.train(epoch_size, train_dataset, callbacks[ckpoint_cb])逻辑说明append_dict里存的current_epoch要在训练循环外自己维护,回调只负责把它写进文件。 恢复训练时,用load_checkpoint加载网络权重,再用load_param_into_net灌回去,但优化器的momentum或Adam的state会丢失,导致恢复后前几步学习率异常。如果用的SGDmomentum,丢失 state 影响不大用Adam的话,建议恢复后把学习率临时调低 50%,跑几个 step 再调回正常值。 参数说明keep_checkpoint_max5是为了省磁盘,一个 O-Net 的 ckpt 可能上百 MB,别设太大。5. 推理部署与常见问题避坑从 checkpoint 到图片上的框和点5.1 推理脚本的预处理图像金字塔与边界框回归训练完 P-Net、R-Net、O-Net 三个模型,推理时不能直接把原图送进网络——MTCNN 靠图像金字塔处理多尺度人脸。常见做法是设min_face_size20,scale_factor0.709, 循环缩放图像直到最短边小于min_face_size。每一层缩放后的图像都过一遍 P-Net,得到候选框,再用框回归偏移量修正框位置import math import numpy as np def calculate_boxes(img, min_face_size20, scale_factor0.709): h, w, _ img.shape min_length min(h, w) scales [] current_scale 1.0 while min_length * current_scale min_face_size: scales.append(current_scale) current_scale * scale_factor # 每层 scale 对应一个缩放后的图像 all_boxes [] for scale in scales: new_h, new_w int(h * scale), int(w * scale) resized cv2.resize(img, (new_w, new_h)) # 送入 PNet 前需要转成 NCHW 格式 tensor transform_to_tensor(resized) # [1, 3, new_h, new_w] cls, box, _ pnet(tensor) # cls: [1, 2, new_h, new_w], 取人脸分数 scores cls[0, 1].asnumpy() offsets box[0].asnumpy() # [4, new_h, new_w] # 阈值 0.6,低于阈值的框直接丢掉 y, x np.where(scores 0.6) for i in range(len(x)): cx, cy x[i], y[i] # 每个特征点对应原图 2x2 的区域,这里简化按 1 个像素处理 x1 (cx * 2) / scale y1 (cy * 2) / scale x2 (cx * 2 12) / scale y2 (cy * 2 12) / scale # 框回归偏移量修正 reg_w offsets[2, cy, cx] * 12 / scale reg_h offsets[3, cy, cx] * 12 / scale x1 offsets[0, cy, cx] * 12 / scale y1 offsets[1, cy, cx] * 12 / scale x2 reg_w y2 reg_h all_boxes.append([x1, y1, x2, y2, scores[cy, cx]]) return all_boxes逻辑说明P-Net 的 stride 是 2,所以特征图上一个点对应原图 2×2 区域,cx * 2就是把特征图坐标映射回缩放后图像坐标,再除以scale映射回原图。 框回归修正用的是offsets,这四个值表示预测框相对当前框的偏移比例,训练时标签也是按这个方式归一化的,所以推理时直接乘当前框的宽高。 参数说明scale_factor0.709是论文原值,近似sqrt(0.5),意思是每次缩放面积减半。min_face_size越小,金字塔层数越多,检测越慢,课程演示设 20 就够。transform_to_tensor要做三件事BGR 转 RGB、减 127.5 除 128、HWC 转 CHW,这步忘了会导致所有分数异常。5.2 NMS 合并与关键点对齐坐标还原到原图P-Net 输出的候选框重叠度很高,需要 NMS 合并。NMS 的核心是对同一类的框按分数排序,保留分数最高的框,删除与其 IOU 超过阈值的其他框。MTCNN 常用两个阈值P-Net 和 R-Net 的 NMS 阈值设 0.7,O-Net 设 0.5。关键点坐标的还原更麻烦O-Net 输出的 10 个值就是归一化的偏移比例,直接乘上 O-Net 输入框的宽高,再加上框左上角坐标。这里有个很容易踩的坑O-Net 输入的是从原图裁剪并缩放到 48×48 的 patch,推理时要记录裁剪框在原图中的坐标,否则关键点全画到图像左上角。def landmark_to_original(landmark, box): # landmark: [10], 归一化偏移比例,box: [x1, y1, x2, y2] w box[2] - box[0] h box[3] - box[1] points [] for i in range(5): x landmark[2 * i] * w box[0] y landmark[2 * i 1] * h box[1] points.append((x, y)) return points逻辑说明训练和推理必须用同一套归一化方式。有的开源实现把 landmark 除以 48(输入尺寸),有的除以框宽高,如果混用,关键点就会偏。 参数说明landmark的顺序要和训练标签一致,一般是左眼、右眼、鼻子、左嘴角、右嘴角。如果发现左右眼画反了,先查是不是数据标注顺序和网络输出顺序不一致,而不是去调 NMS 阈值。5.3 五个高频踩坑记录坑一MindSpore 版本不兼容,nn.PReLU的w_init参数名变了。现象加载课程作业源码报TypeError: __init__() got an unexpected keyword argument w_init。 原因MindSpore 1.x 到 2.x 改了不少 API 参数名,网上源码大多是旧版写法。 解决查看当前版本的help(nn.PReLU),把w_init改成weight_init,或者直接不传,用默认0.25。坑二推理时图像维度顺序不对,检测框全部错位。现象框画出来是斜的或整体偏移。 原因OpenCV 读图是 HWC,BGRMindSpore 模型输入是 NCHW,RGB。推理前只做了 resize,忘了transpose和归一化。 解决在transform_to_tensor里依次img[:, :, ::-1]转 RGB,img / 128 - 1归一化,再np.transpose(img, (2, 0, 1))加 batch 维。用np.ascontiguousarray确保内存连续,否则 MindSpore 可能报Tensor copy的警告。坑三P-Net 训练 loss 不降,分类准确率卡在 60% 左右。现象P-Net 一直把所有人脸区域都当成背景。 原因随机裁剪生成正样本时,裁出来的图块占比太小,模型看到的正样本几乎都是 带背景的人脸,特征混乱。 解决把随机裁剪中心限制在真实框中心附近,即left和top的采样范围缩小到box_w * 0.3以内,而不是整图随机。另外把负样本和正样本的比例从 2:1 调到 3:1,减少背景噪声干扰。坑四训练时内存持续上涨,最后 OOM。现象跑几百 step 后内存占用越来越大,进程被 kill。 原因MindSpore 的GeneratorDataset在 Python 端做数据增强,如果map里的函数每次返回 numpy 数组但没有释放中间变量,或者num_parallel_workers开太多,内存就会累积。 解决把map里的函数用mindspore.dataset.GeneratorDataset的__getitem__配合cache使用,或者把num_parallel_workers从默认的 1 调到 2,不要超过 CPU 核心数的一半。另外确认global_step变量没有在循环里无限追加到列表。坑五O-Net 输出关键点很准,但人脸框偏大。现象框比实际人脸大一圈,关键点却都在五官上。 原因R-Net 到 O-Net 的框回归没有做二次精修。O-Net 输入的是 R-Net 输出的框,但 R-Net 的框已经经过一次回归,偏移量有累积误差。 解决在把 R-Net 候选框送入 O-Net 之前,先对框做一次clip to image并扩大 1.2 倍,让脸部上下文包含更多,关键点回归更稳同时 O-Net 的 NMS 阈值用 0.5,宁可多丢一点框,也要保证输出框的准确性。6. 验证与进阶在课程答辩里展示效果和说明边界6.1 用 FDDB 或自建测试集算检出率与关键点误差答辩时光放几张截图是不够的,至少要有客观数字。常见做法是准备 100 张不含训练集的图片(可以从 WIDER Face 的验证集里抽),人工标出人脸框,然后跑推理脚本统计检测到的人脸数 / 真实人脸数 检出率,同时计算预测框中心与真实框中心的偏差。关键点误差一般用归一化平均误差(NME),把每个关键点的欧氏距离除以两眼距离,小于 0.05 就算对齐成功。MindSpore 提供nn.Accuracy这类指标,但人脸检测更常用自写脚本,输出precision、recall和每张图的耗时,再和课程报告里的基线对比。6.2 在 CPU 上跑推理的实用技巧课程大作业不一定有 GPU,CPU 上跑推理最怕慢。两个实用技巧第一,推理时不整图过网络,先把图像金字塔的最低几层跳过,实际场景中 20 像素的小脸多半不是重点,min_face_size提到 40,速度能快一倍第二,把 O-Net 的输入裁剪用cv2.resize的INTER_AREA插值,人脸下采样效果比默认的双线性好,关键点抖动明显减少。MindSpore 在 CPU 上默认只跑 1 个线程,手动设置mindspore.set_context(modecontext.GRAPH_MODE, device_targetCPU)后,OMP_NUM_THREADS4环境变量可以加速卷积。另外一个我自己的习惯把三个网络的 ckpt 合并成一个 dict,推理时只加载一次,避免每次load_checkpoint都扫一遍文件。做完这些,一套端到端的 MTCNN 人脸识别加关键点检测就闭环了。最深的体会是,课程大作业的分数往往不取决于网络多复杂,而取决于你亲手踩过几个坑、能不能把数据管线和推理链路的细节讲清楚。希望这篇笔记帮到你。本文还有配套的精品资源点击获取

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

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

免费获取方案