尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

多目标人脸识别实战:基于MTCNN与FaceNet的完整实现

多目标人脸识别实战:基于MTCNN与FaceNet的完整实现 简介人脸识别是计算机视觉和深度学习中的经典任务其核心流程通常包括人脸检测、人脸对齐、特征提取与身份比对。MTCNN作为多任务级联卷积网络能够高效完成画面中多张人脸的检测与关键点定位FaceNet则将人脸图像映射为512维特征向量通过欧氏距离度量实现身份判断。基于预训练模型开发者无需从零训练即可快速构建可靠的人脸识别系统广泛应用于安防监控、智能考勤、课堂签到等场景。本文从工程实践角度系统讲解基于Python的多目标人脸识别项目的完整实现涵盖环境配置、特征库构建、核心代码解析、阈值调节方法以及视频流性能优化并针对常见问题给出排查思路帮助读者从原理到落地全面掌握该技术。 每年到这个时候就会有一大批同学抱着Python毕业设计基于深度学习的多目标人脸识别代码.zip这种标题的文件来找我。说实话这个题目已经快被做烂了但每年照样有人选为什么因为它的技术栈清晰、演示效果好、可讲的东西多而且——网上开源资源充足哪怕自己写不出来至少能跑通一个demo。但我要泼一盆冷水单靠把开源代码跑起来答辩那关你都过不了。老师问一句你的多目标是怎么实现的batch size为什么设成32阈值为什么是0.6你就得愣住。所以这篇文章我打算从一个真正做过这个题目的角度出发把整个项目的底层逻辑、实现思路、踩坑记录全部摊开来讲不管你是打算自己做、还是在别人代码基础上改都能用得上。1. 项目整体设计思路拆解1.1 题目拆解什么是多目标人脸识别先说清楚一个最基本的问题人脸识别Face Recognition和人脸检测Face Detection不是一回事。检测是找到画面里有没有人脸、人脸的框在哪里识别是这张脸是谁。而多目标这个定语指的是画面里同时出现多张人脸时系统要把每一张人脸都单独检测出来并且分别判断它们各自是谁。所以整个项目的技术链条是视频帧/图片输入 → 人脸检测找出所有人脸框 → 人脸对齐关键点校正 → 特征提取转成向量 → 特征比对和库里的已知人脸算相似度 → 输出识别结果很多初学者拿到代码后看到里面又是YOLO又是FaceNet的就晕了。其实你只需要抓住一条主线先框住所有人脸再逐张判断身份。前者是检测模型后者是识别模型。这两个模型是可以分开独立训练的也可以直接用开源预训练权重。1.2 技术路线选型为什么是MTCNN FaceNet现在主流的方案其实有好几套我列个表格直接对比方案检测部分识别部分优势劣势方案AMTCNNFaceNet轻量、facenet-pytorch一行调用、CPU也能跑检测精度一般极小人脸漏检方案BRetinaFaceArcFace精度高、关键点检测准依赖重、配置麻烦方案CYOLOv5检测分类网络速度快、适合视频流自定义人脸分类效果一般需要自己训练分类头方案DOpenCV HaarLBPH代码最简单精度差换角度/光照就崩我这些年看到毕设里最稳妥的就是方案A也就是MTCNN FaceNet原因其实很现实facenet-pytorch这个库封装得极其友好两行代码就能加载预训练好的MTCNN检测器和InceptionResnetV1识别网络不需要自己写模型结构。MTCNN输出的人脸框还会附带关键点眼睛、鼻子、嘴角可以用来做对齐而对齐这一步对识别率的提升是决定性的。FaceNet把每张人脸映射成一个512维的向量比对时只需要算欧氏距离非常直观答辩讲起来也容易。如果你的机器配置好想冲高一点的上限可以用RetinaFace做检测。但毕设场景下MTCNN已经足够了而且它有个天生的好处检测阈值可以调得很低多目标场景下不容易漏人。1.3 项目功能模块划分一个完整的毕设项目代码结构应该按照功能去拆而不是把所有逻辑堆在一个文件里。我建议按下面这种方式组织face_recognition_project/ ├── data/ # 人脸数据库每个人一个文件夹 │ ├── zhangsan/ │ ├── lisi/ │ └── wangwu/ ├── models/ # 预训练权重和模型缓存 ├── utils/ │ ├── detector.py # MTCNN人脸检测封装 │ ├── aligner.py # 人脸对齐处理 │ ├── embedder.py # FaceNet特征提取封装 │ └── database.py # 人脸特征库管理 ├── train/ │ └── build_database.py # 构建人脸特征库的脚本 ├── test/ │ └── recognize_image.py # 单张图片识别测试 │ └── recognize_video.py # 视频/摄像头多目标识别 └── requirements.txt这种分层的好处是每个模块独立可测换一个检测器只需要改detector.py换数据集只需要重新跑build_database.py不用动其他代码。答辩的时候老师问你这个模块怎么改你也能清晰地回答。2. 环境配置与数据集准备2.1 从零搭好Python深度学习环境做这个项目Python版本我建议直接用3.8或者3.9别追新。深度学习这块很多老库对3.10以上版本支持得并不好有些编译好的轮子都找不到装个包能折腾一下午。核心依赖其实就四个torch1.10.0 torchvision0.11.0 facenet-pytorch2.5.0 opencv-python4.5.0安装指令pip install torch torchvision pip install facenet-pytorch pip install opencv-python这里有三个非常关键的坑我当年都踩过第一个坑facenet-pytorch和torch的版本要匹配。如果你用的是新版torch2.x一定要装facenet-pytorch2.5.0老版本会用torch.nn.functional.normalize之类的接口报错。第二个坑安装完一定要先跑一个小测试确认MTCNN能正常加载它会自动下载预训练权重到~/.cache/torch/checkpoints/。如果网络不好下载失败报错是RuntimeError: Couldnt load custom C ops这时候需要手动下载权重文件放到对应目录。第三个坑opencv-python装完导入的时候如果报ImportError: libGL.so.1: cannot open shared object file说明服务器上缺图形库。Ubuntu/Debian系统执行sudo apt-get update sudo apt-get install -y libgl1-mesa-glx搞定后再跑import cv2问题就消失了。2.2 构建自己的人脸数据库很多同学问识别谁的脸答案是识别你自己建库的人脸。这个项目本质上是一个1:N比对系统你往库里加了哪些人它就能认出哪些人。标准流程是这样的收集每个人至少10-20张不同角度、不同光照条件下的清晰正脸照。按姓名建文件夹把所有照片放进去。运行预处理脚本自动检测、对齐、提特征保存成一个特征库文件。这里强调一下照片质量决定了识别效果的底线模型再强输入模糊的照片也白搭。我用MTCNN处理图片时会把keep_allTrue打开因为每个人的照片里可能有多张人脸防止误检漏检。另外照镜子拍的自拍、戴了口罩的、侧脸超过45度的都会严重影响特征质量宁可少要也不要硬塞进库。2.3 数据集扩充让模型更扛打如果你想让项目在答辩演示时更出彩可以在自建数据的基础上做一个简单的数据增强随机旋转±15度内、水平翻转、亮度变化、加高斯噪声。这么做能让特征提取网络在应对角度和光照变化时更稳定。但要注意一个原则增强不能过于夸张把人脸翻转成非人脸效果反而变差。这是我在数据预处理时用的增强片段import cv2 import numpy as np def augment_face(image): # 水平翻转 if np.random.random() 0.5: image cv2.flip(image, 1) # 亮度调节 if np.random.random() 0.5: image cv2.convertScaleAbs(image, alphanp.random.uniform(0.8, 1.2), beta0) # 小角度旋转 if np.random.random() 0.5: angle np.random.uniform(-10, 10) h, w image.shape[:2] M cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0) image cv2.warpAffine(image, M, (w, h)) return image注意增强只用于扩大训练样本入库的人脸特征还是要用原始无增强照片提取这样才能保证特征库的纯净。3. 核心原理与关键代码实现3.1 深入理解MTCNN检测原理MTCNN全称是 Multi-task Cascaded Convolutional Networks翻译过来就是多任务级联卷积网络。它分三个阶段工作P-NetProposal Network在图像金字塔上快速滑动生成大量候选框。这一步追求的是宁可错杀一千不可放过一个所以召回率很高但误检也很多。R-NetRefine Network对P-Net的候选框做精修过滤掉大部分非人脸区域。O-NetOutput Network输出最终的人脸框和5个关键点坐标左眼、右眼、鼻子、左嘴角、右嘴角。整个流程可以这样理解先粗筛再精修最后定位关键点。这种级联结构让它在CPU上也能跑得动同时精度也不差。代码层面你只需要一行from facenet_pytorch import MTCNN mtcnn MTCNN(keep_allTrue, min_face_size20, thresholds[0.6, 0.7, 0.7])这里的thresholds是三个网络的置信度阈值默认是[0.6, 0.7, 0.7]。越低越容易检测出人脸但误检也多越高越严格但容易漏检。多目标场景下我建议把第一个阈值调到0.5左右因为某些被遮挡的、较远的、侧脸的人脸置信度就是偏低阈值卡太死直接漏人。3.2 FaceNet的孪生网络与三元组损失FaceNet的思路跟普通分类网络完全不同。普通分类网络最后是一个全连接层输出类别概率FaceNet不同它是把模型当做一个特征提取器最后输出的是512维的特征向量然后通过损失函数让同一个人的向量距离近、不同人的向量距离远。核心损失函数是三元组损失Triplet Loss它的逻辑是取三张照片——锚点Anchor、正样本Positive同一个人、负样本Negative不同的人让锚点和正样本的距离小于锚点和负样本的距离并且留出一个Margin间隔。Loss max(d(A, P) - d(A, N) margin, 0)如果你的识别模型需要微调可以按这个思路做一个简单的三元组数据加载和训练。但大多数情况下直接用预训练的FaceNet权重就够了不需要自己训练。3.3 检测到识别的完整代码这是整个项目最核心的一段从图片读取到画框识别全程串联起来import torch import cv2 import numpy as np from facenet_pytorch import MTCNN, InceptionResnetV1 from scipy.spatial.distance import cosine device torch.device(cuda:0 if torch.cuda.is_available() else cpu) # 初始化检测器与识别模型 mtcnn MTCNN(keep_allTrue, min_face_size20, thresholds[0.5, 0.7, 0.7], devicedevice) facenet InceptionResnetV1(pretrainedvggface2).eval().to(device) # 加载特征库 db np.load(face_database.npy, allow_pickleTrue).item() # db {zhangsan: np.array([...512维...]), lisi: np.array([...])} def recognize_frame(frame): # 1. 检测所有可能的人脸框 boxes, probs, landmarks mtcnn.detect(frame, landmarksTrue) if boxes is None: return frame # 2. 逐个人脸框处理 for box, landmark in zip(boxes, landmarks): x1, y1, x2, y2 [int(v) for v in box] # 裁剪人脸 face frame[y1:y2, x1:x2] if face.size 0: continue # 提取512维特征 face_tensor mtcnn(frame, save_pathNone) if face_tensor is None: continue face_tensor face_tensor.unsqueeze(0).to(device) embedding facenet(face_tensor).detach().cpu().numpy()[0] # 3. 与特征库比对 best_name unknown best_dist float(inf) threshold 0.8 # 欧氏距离阈值 for name, db_emb in db.items(): dist np.linalg.norm(embedding - db_emb) if dist best_dist: best_dist dist best_name name # 4. 画框和信息展示 label f{best_name} ({best_dist:.2f}) if best_dist threshold else unknown color (0, 255, 0) if best_dist threshold else (0, 0, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2) return frame注意这里比对方式用的是欧氏距离不是余弦相似度。FaceNet原始论文使用的是欧氏距离距离越小说明越像。0.8这个阈值是经验值实际应该用你自己库里的数据去调。这个我后面会专门讲。3.4 多目标识别在视频流中的优化如果只是图片识别多目标还不算有挑战性。真正的难点在于视频/摄像头因为画面一直在动。一个直接的思路是对视频的每一帧都做完整检测识别但在CPU机器上一帧可能就得跑200-300毫秒画面会卡成PPT。我的做法是引入一个轻量级的帧差法 预测框跳过策略检测模块不需要每帧都跑。如果当前帧的画面变化不大可以直接复用上一帧的人脸框位置。对上一帧的框小幅扩大范围比如扩大20%只在这个区域内做特征提取不用重新全局检测。每隔15帧做一次全量检测用于纠正漂移。这个方案在教室/办公室等固定场景下能把FPS提升2-3倍同时不容易丢目标。4. 特征库构建与比对策略4.1 如何构建高质量的人脸特征库特征库是整个系统的基准它的质量直接决定了识别的准确率。构建逻辑很简单遍历每个人的照片逐张提取特征然后求平均作为这个人最终的特征向量。def build_database(person_images): features [] for img in person_images: face_tensor mtcnn(img) if face_tensor is None: continue face_tensor face_tensor.unsqueeze(0).to(device) emb facenet(face_tensor).detach().cpu().numpy()[0] features.append(emb) # 取平均归一化 avg np.mean(features, axis0) avg avg / np.linalg.norm(avg) return avg这里有三个容易犯错的地方人脸对齐不是可选项而是必选项。MTCNN输出的关键点如果不用识别效果会下降不少。facenet-pytorch在mtcnn(frame)时已经自动做了对齐所以上面代码里不需要额外处理。但如果用OpenCV人脸检测器就必须自己做仿射变换对齐不然特征质量差很多。入特征库前一定要做归一化。因为比对用的是距离或余弦尺度不一致会导致距离不可比。每个人入库照片至少5张以上少于5张特征均值会很不稳定换个角度就认不出来了。4.2 阈值调节的硬核方法很多同学喜欢问阈值到底设多少这个问题没有固定答案但是有标准的调法。我推荐用同类/异类距离分布直方图来确定阈值取库里的每个人分别与自己的不同照片算距离得到类内距离集合。取不同的人之间算距离得到类间距离集合。把两组距离画成直方图阈值设在两个分布的交汇谷底。一般来说FaceNet预训练模型的欧氏距离分布大概是类内距离集中在0.5-0.9类间距离集中在1.0-1.6。所以0.8最常见的初始阈值。但如果你是特定领域的数据比如老年人、小孩、戴口罩的人分布会变必须实测后重调。我实际测试过一组结果大致是这样的场景阈值0.7阈值0.8阈值0.9正常光线单人100%准确100%准确100%准确多人同框92%准确96%准确88%准确有误识暗光/侧脸82%准确91%准确83%准确所以阈值高会漏认阈值低会认错实际项目里通常要按使用场景权衡。如果你希望安全性优先比如门禁阈值就设严一点认不出总比认错强。4.3 1:N比对的性能优化如果你的特征库里人特别多比如上百人每次识别都全量遍历所有特征做距离计算虽然512维向量算距离也就几毫秒但会随人数线性增长。应对方案是使用faiss这样的向量检索库import faiss # 构建索引 dim 512 index faiss.IndexFlatL2(dim) index.add(np.array([db[name] for name in db_names])) # 检索k1返回最近的1个 distances, indices index.search(np.array([query_emb]), k1)用FAISS以后上千人的特征库检索时间能压到微秒级视频识别完全无压力。不过毕设阶段数据量通常不大用字典遍历也够这个优化可以作为答辩亮点提一下。5. 训练与微调实战5.1 什么时候需要微调大多数情况下搬预训练权重直接提特征就能达到不错的准确率。但如果你发现某些特定人群比如亚洲人脸、戴眼镜的人、特定肤色的面孔识别效果差那说明预训练模型在常见人脸上表现好但在你的数据分布上不够好。这时候就需要微调。一个常见的微调方案是用ArcFace替换原来的分类层但这对毕设来说工程量大。更简单可行的是用三元组损失微调整个网络只调整最后几层。下面这段代码展示了核心训练循环from torch.utils.data import DataLoader, Dataset class TripletDataset(Dataset): def __init__(self, triplets): self.triplets triplets # [(anchor, positive, negative), ...] def __len__(self): return len(self.triplets) def __getitem__(self, idx): a_path, p_path, n_path self.triplets[idx] a mtcnn(cv2.imread(a_path)).squeeze(0) p mtcnn(cv2.imread(p_path)).squeeze(0) n mtcnn(cv2.imread(n_path)).squeeze(0) return a, p, n def triplet_loss(anchor, positive, negative, margin0.5): d_pos torch.nn.functional.pairwise_distance(anchor, positive, 2) d_neg torch.nn.functional.pairwise_distance(anchor, negative, 2) return torch.relu(d_pos - d_neg margin).mean() optimizer torch.optim.Adam(facenet.parameters(), lr1e-4) for batch in dataloader: a, p, n [x.to(device) for x in batch] optimizer.zero_grad() emb_a facenet(a) emb_p facenet(p) emb_n facenet(n) loss triplet_loss(emb_a, emb_p, emb_n) loss.backward() optimizer.step()5.2 训练参数与损失函数选择微调时一定要注意学习率不能太大建议1e-4到1e-5。因为预训练权重已经很接近一个好的特征空间了lr太大会把学到的特征全部打乱。batch size至少大于8否则三元组采样方差太大loss会上下震荡。关于损失函数这里多说几句。FaceNet原版用的是Triplet Loss但triplet采样难度很高。ArcFace这类基于margin的softmax损失效果更稳定。如果你想在毕设里整点高级感可以在微调时用ArcFace替换最后全连接层class ArcFaceHead(torch.nn.Module): def __init__(self, embedding_dim, num_classes, s64.0, m0.5): super().__init__() self.weight torch.nn.Parameter(torch.FloatTensor(num_classes, embedding_dim)) self.s s self.m m torch.nn.init.xavier_uniform_(self.weight) def forward(self, embedding, label): # 归一化 embedding torch.nn.functional.normalize(embedding) weight torch.nn.functional.normalize(self.weight) cos_theta torch.nn.functional.linear(embedding, weight) theta torch.acos(torch.clamp(cos_theta, -1.0, 1.0)) target_logits torch.cos(theta self.m) one_hot torch.zeros_like(cos_theta) one_hot.scatter_(1, label.view(-1, 1), 1.0) output cos_theta * (1 - one_hot) target_logits * one_hot return output * self.s用ArcFace的好处是训练更稳定而且类间边界更清晰识别效果通常优于Triplet Loss。这部分可以作为论文里的创新点来写。5.3 训练时显存不足的解法毕设党最常遇到的就是CUDA out of memory。我给出几个实测有效的方案减小batch size从32降到16或8这是最直接的办法。降低图片分辨率检测时先resize到640x640识别时再resize到160x160显存占用直线下降。开启混合精度训练使用torch.cuda.amp自动混合精度显存占用减少约40%而且速度更快。scaler torch.cuda.amp.GradScaler() with torch.cuda.amp.autocast(): emb_a facenet(a) loss triplet_loss(emb_a, emb_p, emb_n) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()这里注意混合精度不是所有算子都支持如果报精度相关的错误可以把受影响的层强制为float32或者直接放弃AMP换小batch。6. 常见问题与排查技巧实录6.1 代码跑不通先排查这三个地方我看了很多同学发来的报错截图90%的问题都出在下面三个地方问题一模型权重下载失败MTCNN和FaceNet首次运行时会自动下载预训练权重。如果网速差或者被限制训练会直接卡住。解决方案是手动下载权重文件放入~/.cache/torch/checkpoints/。具体的文件名是vggface2_feat_dim512.pth和mtcnn_weights.pt网上搜这两个名字能找到。问题二张量维度对不上拿mtcnn的输出直接喂给facenet时经常会报维度错误。原因在于mtcnn返回的是四维张量[batch, 3, 160, 160]而facenet需要也是四维。如果用了squeeze(0)就会变成三维直接报错。记住一条规则facenet的输入必须保持4维最后一个维度是160x160。问题三摄像头打不开很多人用cv2.VideoCapture(0)打不开摄像头在Windows上直接返回False。我的建议是先用系统自带的相机应用测试摄像头是否被占用然后换cap cv2.VideoCapture(0, cv2.CAP_DSHOW)强制使用DirectShow后端。6.2 检测效果不好三个方向去调整如果你发现多目标人脸检测经常漏掉某些人脸排查顺序如下先把min_face_size调小到15-20这能解决远处小脸漏检的问题。把三个thresholds阈值整体往下调0.05-0.1允许更多低置信度候选框通过。如果还是不行可以考虑换检测模型用RetinaFace替换MTCNN。虽然代码要改一点但检测精度确实高一截。如果发现自己训练的数据在测试集上效果很好一到现场演示就翻车那大概率是训练数据和真实场景分布不一致。比如训练集全是室内光下的照片演示时在室外逆光环境下跑效果自然差。解决办法就一条多收集现场场景的照片入库。6.3 识别准确率不高从数据看问题准确率低的时候不要急着换模型先做一件事把识别错误的样本打印出来看看是认错了人还是认不出人。认错人通常是因为库里有长相相似的人比如双胞胎或者两个戴同款眼镜的解决方法是提高阈值或者给这两个人增加训练样本。认不出人通常是因为光照、角度变化太大解决方法是增加数据增强、增加不同场景的照片。另外很多人忽略的一点是入库的时候要把背景裁干净。如果照片里除了人脸还有大量背景信息特征提取时会混合进背景噪声导致特征不稳定。MTCNN虽然能裁出人脸框但人脸框偶尔会包含过多的下巴、脖子区域。可以在检测后按关键点位置把人脸归一化到统一尺寸比如160x160去掉多余背景。6.4 多目标场景下的性能优化清单实际跑视频流时我总结了一份性能优化清单按优先级排列优先级优化手段效果高检测帧间隔策略每N帧全量检测速度提升2-3倍高缩小处理分辨率640x480速度提升30%中使用跟踪器保持目标ID多目标身份不跳变中FAISS快速检索特征库大时速度提升明显低模型量化半精度推理显存减少30%速度略提升用上前面提到的帧间复用定期全量检测策略后我在笔记本CPUi5-1135G7上能从4FPS提升到9FPS左右画面虽然谈不上丝滑但至少能用。如果换上GTX 1660以上的显卡全流程GPU加速后能稳定在25FPS以上现场演示完全够用。7. 项目演示与论文撰写建议7.1 现场演示最容易翻车的细节毕设答辩现场演示要注意几个小细节提前把特征库构建好不要在现场临时跑入库脚本摄像头提前测好确保驱动正常代码里所有路径要用相对路径别用绝对路径否则换一台机器就崩。我见过最惨的一个例子是现场演示时因为笔记本USB供电不足导致摄像头画面黑屏最后只能临时改用视频文件演示。所以准备一段录好的视频作为备选方案永远是对的。另外演示的时候如果识别错了千万别慌。大多数老师不会因为一个误识别挂掉你但你要能解释清楚为什么会错——这个案例里库中样本太少或者光照变化导致特征偏移说得有理有据反而加分。7.2 论文里怎么写这个项目论文结构可以参考下面的逻辑线绪论人脸识别的应用价值、国内外研究现状。相关技术MTCNN人脸检测原理、FaceNet网络结构、损失函数。系统设计整体架构、各模块功能、数据流程图。实验数据集介绍、评价指标准确率、召回率、F1、FPS、实验结果对比。总结与展望。最关键的一步是在实验部分列出对比数据。比如使用MTCNNFaceNet方案在自建数据集上准确率93.2%单帧处理时间82ms在引入数据增强和阈值优化后准确率提升到96.8%这种有前后对比的数据是老师最想看到的。7.3 在开源代码基础上改底线是什么我理解很多同学确实时间紧打算直接拿网上的源码改一改。如果你真要这么做务必做到以下三点否则答辩一定会被问穿逐行读懂核心代码尤其是检测和识别这两个主流程的代码至少能回答上来每一行在做什么。至少改一个核心模块比如换检测模型、改损失函数、加跟踪算法这样论文才不会有明显抄袭痕迹。最常见也最推荐的是在原有单目标人脸识别基础上增加了多人脸跟踪与ID保持功能这个改动代码量不大但非常有展示价值。用自己的数据重新构建特征库这条最简单但也最容易被忽略。特征库还是原作者的人老师一看就能质疑你。最后提醒一句毕业设计的本质是让你把学过的知识串联起来不是让你真的做出一个商用人脸识别系统。所以别压力太大把原理弄清楚、代码能跑通、演示不翻车你就已经超过80%的人了。我见过太多人在调参和训模型上死磕结果连论文都没时间写那才是本末倒置。合理的节奏是先让整个流程跑通再优化准确率最后留足够时间写论文和做PPT。按照这个顺序来这个题目真的不难。本文还有配套的精品资源点击获取
返回列表