简介:基于Python与TensorFlow的深度学习人脸识别检测系统源码包,面向计算机相关专业学生的期末大作业与课程设计场景,提供一套可直接落地的人脸检测识别实现方案。资源共34个文件、约3.64MB,涵盖8个Python源码文件,涉及MTCNN人脸检测、特征提取、人脸识别、界面窗口等核心模块;7个特征数据文件用于模型比对,另有图片、Markdown和txt使用说明等辅材,目录结构清晰,便于按功能定位。目前已有1551人学习下载,项目完成度经过较多学习者检验。代码包含从人脸检测、特征提取到识别比对的完整流程,配有使用说明和README,并准备了可直接运行的界面程序,下载后无需修改即可运行;同时附带结果展示图片,方便结题汇报与答辩展示,能有效节省课程设计与期末作业的开发时间。
1. 人脸识别期末大作业:这套 tensorflow 源码包怎么用才不白下
期末大作业要求做一个深度学习人脸识别检测系统,很多人第一反应就是去下载一份现成的源码包。这个基于 python + tensorflow 的压缩包,里面是完整的检测与识别代码,外加一份使用说明,看起来解压就能跑。可实际上我在帮别人复现这类项目时,十个里有五个卡在数据集路径,三个卡在 tensorflow 版本不兼容,最后只剩两个能勉强跑到训练。这篇笔记就把它拆开讲:整体流程、训练脚本的核心参数、数据集怎么摆,以及最容易翻车的那几个地方。适合正在补交课设、想做复现但不想从零搭网络的人,也能顺便帮你把“检测”和“识别”这两个概念理清楚。
2. 先看懂系统架构:从图像输入到人脸框和特征向量
2.1 这套系统的整体处理流程
把人脸识别拆开,其实是两段式任务。第一段是人脸检测,负责在图片里找到“哪里有人脸”,输出一个矩形框;第二段是人脸识别,把框出来的人脸区域转成一组特征,再和预先注册的身份特征做比对,输出“这是谁”。很多期末代码把两段写在同一个脚本里,看起来是一个模型,实际是两条独立的流水线。
我拆过的期末项目里,检测部分大多直接用 OpenCV 的 Haar Cascade 或者 MTCNN 预训练模型,识别部分则自己拿 CNN 训练一个分类器。这套 tensorflow 包大概率也走的是“检测 + 分类”路线:先用检测器框出人脸,再把人脸区域截下来缩放到固定尺寸,送进卷积神经网络,最后 softmax 输出各个身份的置信度。整体数据流可以写成下面这样:
原始图片 → 人脸检测器 → 人脸框坐标 → 截取人脸区域 → resize 到固定尺寸 → 归一化 → CNN 特征提取 → softmax 分类 → 输出身份标签与置信度
这里有一个关键点:检测模型不参与训练,它只是起到“区域提案”的作用。真正需要训练的是后面的 CNN 分类器。期末作业的标签通常是“张三”“李四”这类人物文件夹名,网络最后一层的节点数就等于训练集里的身份数量,不是固定的。很多人把这个节点数写死,换了自己的数据集以后训练直接报 shape 不匹配,这是第一个容易踩的坑。
确定要用什么输入尺寸也很重要。常见的期末代码里,人脸区域会被缩放到 64x64 或 112x112。64x64 训练快、CPU 也能跑,但识别精度会低一些;112x112 更接近一些主流人脸模型输入,训练慢一点,不过对小尺寸和模糊人脸更友好。我一般会先看代码里预处理函数指定的 target_size,如果写的是 64,那模型第一层的输入形状就是 (None, 64, 64, 3),其中 3 是 RGB 通道,None 是 batch 大小,batch 要留到训练时才会确定。如果使用说明里画了网络结构图,你就按这个顺序找第一层 Conv2D 的 input_shape,很快就能核对清楚。
2.2 数据预处理:为什么不能直接喂原始图片
原始图片没有人脸框,也没有统一尺寸,亮度和对比度差别很大。直接把这些图丢给网络,训练 loss 会抖得非常厉害,大概率不收敛。常见做法是:读取图片后先转成 RGB,缩放到目标尺寸,把像素值从 0~255 归一化到 0~1,再按训练集的均值方差做标准化。有些代码还会做左右翻转、随机亮度变化,用来扩充数据集,期末场景下够用了。
下面这段是期末代码里最常见的预处理函数,我把它单独拆出来看:
import cv2 import numpy as np def load_preprocess(img_path, target_size=(64, 64)): # cv2 读取时默认是 BGR,如果不转 RGB,模型学到的颜色是乱的 img = cv2.imread(img_path) if img is None: raise ValueError("图片路径错误: %s" % img_path) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) # 直接拉伸到固定尺寸。按比例裁剪再 resize 会更保真, # 但期末项目数据量小,拉伸影响不明显 img = cv2.resize(img, target_size, interpolation=cv2.INTER_LINEAR) # 归一化到 [0, 1],再减均值除方差,跟常见预训练模型保持一致 img = img.astype(np.float32) / 255.0 mean = np.array([0.485, 0.456, 0.406], dtype=np.float32) std = np.array([0.229, 0.224, 0.225], dtype=np.float32) img = (img - mean) / std # 部分老代码要求 CHW 顺序,即 channel 在前, # tensorflow 的 keras 默认是 HWC,很多踩坑就是这个顺序没对上 img = np.transpose(img, (2, 0, 1)) return img # 调用示例 image = load_preprocess("dataset/person1/001.jpg") print(image.shape) # 输出 (3, 64, 64)这段代码里有两个参数容易忽略。一个是target_size,如果你后续模型用的是 112x112,这里还写 64x64,最后model.predict一定会报维度不匹配。另一个是最后的np.transpose,TensorFlow 2.x 里 Keras 的卷积层默认输入是(height, width, channels),也就是 HWC;但有一些从 1.x 时代迁过来的代码内部是纯 TensorFlow 的tf.nn.conv2d,它要求的是(batch, height, width, channels),其实还是 NHWC,并没有问题。真正需要注意的反而是数据标注工具生成的.npy文件里存了 CHW,直接拿来训练就会报 batch 维度对不上。遇到这种情况,我一般会在load_preprocess里去掉 transpose,保持 HWC,再跑一次训练,问题就没了。
数据增强方面,期末作业不需要做得太猛。随机亮度扰动和水平翻转可以加,但不要用随机裁剪,因为人脸区域已经被检测器框好了,再裁剪容易切掉半边脸。增强参数通常控制在:亮度因子 0.8~1.2,翻转概率 0.5。如果你的训练集每人只有 10 张图,增强能帮你把 epoch 拉长一些,但真正稳定的还是采集更多人脸样本。
3. 搭建与训练:把模型跑起来的完整步骤
3.1 环境配置与版本兼容
拿到源码包后,第一步不是读代码,而是先把环境装上。这类期末大作业最多的翻车点就是 tensorflow 版本和代码不匹配。许多老代码是照着 tensorflow 1.x 写的,里面有tf.Session()、tf.placeholder(),这些在 2.x 的默认模式下根本不存在。如果你现在pip install tensorflow,装到的是 2.x,直接跑会抛一堆AttributeError。
我建议先固定一个兼容性较好的版本再装依赖:
pip install tensorflow==2.10.0 opencv-python numpy pillow选 2.10.0 是因为它是最后一个原生支持 Windows GPU 的版本,之后的版本要么只支持 Linux 要么需要额外的插件,期末项目在 Windows 上跑的概率很大。装完后用一个命令验证:
python -c "import tensorflow as tf; print(tf.__version__)"如果能正常打印版本号,再跑源码包里的入口脚本。如果代码是 1.x 写法,打开入口脚本,在 import tensorflow 之后立刻加两行:
import tensorflow.compat.v1 as tf tf.disable_v2_behavior()这两行的作用是把 2.x 的默认行为切换成 1.x 的图模式,让tf.Session()、tf.placeholder()重新可用。注意,加了这两行之后,tf.keras部分可能需要重新适配,因为 disable_v2_behavior 会影响 Keras 的内部行为。所以更稳妥的做法是看代码主体用的是tf.keras还是原生tf.nn。如果是tf.keras,那它大概率是 2.x 写法,不需要这两行;如果是tf.Session(),才需要加。
还有一个小点:如果电脑只有 CPU,安装完 GPU 版 tensorflow 也没关系。可以显式设置让程序不找 GPU:
import tensorflow as tf gpus = tf.config.experimental.list_physical_devices('GPU') if gpus: try: for gpu in gpus: tf.config.experimental.set_memory_growth(gpu, True) except RuntimeError as e: print(e)这段代码放在训练脚本开头,可以避免 CUDA 初始化报错。期末项目数据量不大,CPU 训练也能接受,只是时间稍长。
3.2 模型核心代码解读与参数调整
识别部分最常见的模型结构是一个三层卷积叠加全连接层。下面这段是期末代码里经常出现的 Keras 写法,我用注释标出了每一步的含义:
import tensorflow as tf from tensorflow.keras import layers, models def build_face_model(num_classes, input_shape=(64, 64, 3)): model = models.Sequential() # 第一层卷积:提取边缘与纹理,输出 32 个特征图 model.add(layers.Conv2D(32, (3, 3), activation='relu', padding='same', input_shape=input_shape)) model.add(layers.BatchNormalization()) model.add(layers.MaxPooling2D((2, 2))) # 第二层卷积:特征加深到 64 个通道 model.add(layers.Conv2D(64, (3, 3), activation='relu', padding='same')) model.add(layers.BatchNormalization()) model.add(layers.MaxPooling2D((2, 2))) # 第三层卷积:提取高层语义 model.add(layers.Conv2D(128, (3, 3), activation='relu', padding='same')) model.add(layers.BatchNormalization()) model.add(layers.MaxPooling2D((2, 2))) # 全局池化,把特征图压成向量 model.add(layers.GlobalAveragePooling2D()) # 全连接层:进一步压缩特征 model.add(layers.Dense(128, activation='relu')) model.add(layers.Dropout(0.5)) # 最后输出 num_classes 个类别的概率 model.add(layers.Dense(num_classes, activation='softmax')) return model model = build_face_model(num_classes=5) model.summary()这里的核心参数是num_classes,它必须等于你的身份文件夹个数。如果你数据里有 5 个人,这个值就是 5;有 20 个人,就改成 20。另外input_shape的宽高必须和预处理里的target_size一致。可以看到 64x64 输入经过三次 pool 后,空间尺寸降到 8x8,再用全局平均池化变成一维,这是比较标准的小型分类网络。Dropout 设为 0.5 是为了防止过拟合,如果你的训练集每个人只有 5~10 张,这个值可以适当降到 0.3,否则欠拟合。
训练循环的代码也要看仔细。期末代码常用 Keras 的compile和fit:
model.compile( optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics=['accuracy'] ) history = model.fit( train_generator, validation_data=val_generator, epochs=30, batch_size=16, callbacks=[ tf.keras.callbacks.ModelCheckpoint('face_model.h5', save_best_only=True), tf.keras.callbacks.ReduceLROnPlateau(patience=3, factor=0.5) ] )这里train_generator可以是tf.keras.utils.image_dataset_from_directory生成的对象,也可以是自己定义的手写 generator。用image_dataset_from_directory最省事,它会自动从文件夹名生成标签。batch_size在 CPU 上建议 8~16,GPU 可以 32 甚至 64,太大了容易内存溢出。loss用SparseCategoricalCrossentropy是因为标签是整数,不需要转成 one-hot;如果你的代码里用了CategoricalCrossentropy,那标签必须先做 one-hot 编码,两者不能混用。ReduceLROnPlateau的作用是当验证 loss 三轮不下降时,把学习率减半,这个回调对期末项目很管用,能帮你省掉反复手动调参的时间。
4. 数据集准备与标签制作:最容易翻车的环节
4.1 目录结构与数据采集
模型代码本身不难,真正让期末大作业崩塌的是数据集。很多人下载了源码包,里面带着作者自己的照片,直接跑没问题,但换成自己的数据就各种报错。问题多半出在目录结构不符合代码预期。最常见的结构是下面这样:
dataset/ ├── person1/ │ ├── 001.jpg │ ├── 002.jpg │ └── ... ├── person2/ │ ├── 001.jpg │ └── ... └── person3/ └── ...每个身份一个文件夹,文件夹名就是标签。然后用image_dataset_from_directory读取时,它可以自动解析这种结构。如果你的代码是自己写os.listdir遍历,那也得按照这个思路来,否则标签和图像对不上。
采集人脸图时,可以用 OpenCV 的摄像头截帧,也可以用现成人脸检测器从照片里裁。我建议先把人脸区域从原始图片里裁出来,存成 64x64 或 112x112 的小图,这样做有两个好处:一是训练时不需要在每次 epoch 都做一次人脸检测,速度会快很多;二是裁剪时已经过滤了大量背景干扰,模型更容易学到人脸特征。
每类样本数量上,期末答辩要求一般不会太高。如果每人只有 10 张,我建议保证每人尽量在 15~20 张,并且背景要有变化,不要十张都坐在同一个位置同一个光照下。否则训练准确率能到 90%,但摄像头一开就露馅——换个人脸角度立刻认不出来。如果实在凑不够,可以用水平翻转、小幅旋转、亮度调整来扩增,每类扩到 40~50 张,效果会稳定不少。
4.2 标签编码与训练集/验证集划分
文件夹名是字符串,模型输出是整数概率分布,所以需要把字符串标签映射成整数。最常见的方式是LabelEncoder,代码很简单:
import os import numpy as np from sklearn.preprocessing import LabelEncoder dataset_root = "dataset" people = sorted([d for d in os.listdir(dataset_root) if os.path.isdir(os.path.join(dataset_root, d))]) print("发现身份类别:", people) encoder = LabelEncoder() y_all = encoder.fit_transform(people) print("标签映射:", dict(zip(people, y_all)))fit_transform会把第一个文件夹映射成 0,第二个映射成 1,以此类推。这个顺序必须固定,否则之后加载模型预测时,输出的 0 对应谁就错乱了。所以有的人会把映射表存成一个 json 文件,我一般也会这么做:
import json label_dict = {int(i): name for i, name in enumerate(people)} with open("label_map.json", "w", encoding="utf-8") as f: json.dump(label_dict, f, ensure_ascii=False, indent=2)这样在写应用脚本的时候,读回 json 就能把预测的整数索引转换成真实姓名。
训练集和验证集的划分也有讲究,不能直接把所有图片混在一起随机切。因为同一个人的照片很可能有强相关,如果把一个人的大部分照片放训练集、少部分放验证集,模型很可能只记住了身份特征而没泛化。常见做法是按文件夹划分:每个身份取出前 80% 的图片做训练,后 20% 做验证。或者用train_test_split时设置stratify=y,保证每个类别在两个集里的比例一致。
下面是一个完整的划分示例,用Path处理路径,兼容 Windows 和 Linux:
import random from pathlib import Path import shutil random.seed(42) val_ratio = 0.2 for person_folder in Path(dataset_root).iterdir(): if not person_folder.is_dir(): continue imgs = list(person_folder.glob("*.jpg")) random.shuffle(imgs) val_count = int(len(imgs) * val_ratio) val_dir = Path("dataset_val") / person_folder.name train_dir = Path("dataset_train") / person_folder.name val_dir.mkdir(parents=True, exist_ok=True) train_dir.mkdir(parents=True, exist_ok=True) for img in imgs[:val_count]: shutil.copy(img, val_dir / img.name) for img in imgs[val_count:]: shutil.copy(img, train_dir / img.name)这里的val_ratio是验证集比例,我习惯取 0.2。如果总共只有 20 张图,那验证集只有 4 张,很容易出现验证准确率波动,这时候可以把比例降到 0.15,或者干脆用 k 折交叉验证,但期末作业没必要做那么重。我见过不少同学把验证集设置成 0.5,结果训练数据不足,模型直接欠拟合,这是典型的矫枉过正。
5. 避坑指南:期末大作业最常见的五个翻车点
5.1 图片路径含中文或空格,训练直接崩
现象:运行训练脚本,报错信息里有UnicodeDecodeError或FileNotFoundError,路径明明存在却读不到。
原因:Windows 下 tensorflow 数据读取 API 对中文路径支持不好,尤其是image_dataset_from_directory,它底层用的是文件系统编码,路径里带中文或者空格时,很容易在某个环节编码出错。我遇到过项目根目录在D:\课程设计\人脸识别\,一跑就崩。
解决:把所有数据集和工程文件放到全英文路径下,例如D:\face_project\dataset。如果代码里已经有硬编码的中文路径,全局替换成英文。另外读图代码里用Path而不是字符串拼接,能减少转义问题。
5.2 训练 loss 不降,准确率一直徘徊在 20%
现象:训练十几个 epoch,loss 基本不怎么变,准确率停在很低的水平,像猜一样。
原因:最常见的是标签和图像错位,或者预处理完全没有归一化。有人直接从灰度图转成三通道,但颜色通道顺序没调整,网络学了一堆无效特征。还有人把标签从 1 开始编号,却忘记在损失函数里减一,导致类别数对不上。
解决:先跑一个最小验证,取 batch_size=1,打印输入图片的 label 和经过预处理的数组,人工确认图像内容与标签是匹配的。然后把归一化代码加上,确认数据不是原图裸奔。最后检查num_classes是否等于len(encoder.classes_),多一个少一个都不行。
5.3 摄像头实时检测卡成 PPT
现象:运行实时检测脚本,画面一卡一顿,FPS 只有个位数,完全没法演示。
原因:检测脚本在每一帧都做一遍人脸检测和识别。如果检测器是 MTCNN,在 CPU 上一帧可能就要 300ms,再加上识别网络的推理,So 整体就很慢。
解决:跳帧检测。每 3 帧检测一次人脸,中间 2 帧直接沿用上一次的检测结果;或者把输入到检测器的帧缩小到 320 分辨率,检测完再把框坐标映射回原图。识别部分也可以做成每 5 帧只跑一次,用历史结果兜底。代码里常见的做法是:
frame_count = 0 detect_interval = 3 while cap.isOpened(): ret, frame = cap.read() if not ret: break frame_count += 1 if frame_count % detect_interval == 0: # 缩小帧做检测,提高速度 small_frame = cv2.resize(frame, (320, 240)) boxes = detect_faces(small_frame) # 坐标按比例映射回原图 boxes = [(int(x * frame.shape[1] / 320), int(y * frame.shape[0] / 240), int(w * frame.shape[1] / 320), int(h * frame.shape[0] / 240)) for x, y, w, h in boxes] for (x, y, w, h) in boxes: face = frame[y:y+h, x:x+w] face = preprocess(face) label_id = int(np.argmax(model.predict(face[None, ...]))) name = label_map[str(label_id)] cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cv2.putText(frame, name, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2)注意这里model.predict(face[None, ...])的批处理维度,如果不加None,模型会报维度错误。detect_interval可以按实际速度调到 5,但不要太大,否则人脸移动后框会滞后明显。
5.4 保存模型之后加载报 key 错误
现象:训练完保存了face_model.h5,重新写一个预测脚本加载,结果model.load_weights报KeyError或 shape 不匹配。
原因:load_weights只能加载权重,权重里的变量名必须和当前模型结构完全一致。如果创建模型时num_classes传错了,或者输入形状不一致,加载就会失败。另一个常见原因是保存的是model.save_weights()而不是model.save(),前者不包含结构。
解决:用model.save("face_model.h5")保存完整模型,加载时直接tf.keras.models.load_model("face_model.h5"),这样不用重新构建模型也不会出现结构不匹配。如果你确实只保存了权重,那加载前一定要用完全相同的函数参数重新build_face_model,而且compile与否不影响权重加载。
5.5 内存不足:数据集大时一次性全读爆内存
现象:训练刚开始,进程直接Killed或报MemoryError,尤其在使用大量扩增图片时。
原因:代码把整个数据集的所有图片一次性读到内存里,转成 numpy 数组再喂给模型。期末项目虽然通常只有几百张图,但如果每张图都是 1080p 转成数组,也会吃掉大量内存。
解决:使用tf.keras.utils.image_dataset_from_directory,它返回的是生成器,按 batch 逐步加载。或者使用ImageDataGenerator.flow_from_directory,也能避免一次性全读。我建议把图片先 resize 到 64x64 再存成 numpy 数组文件,比如train_data.npz,加载速度快很多。要注意生成器内部默认 shuffle,如果想固定每次 shuffle 的结果,需要设seed参数。
6. 进阶:用 OpenCV 摄像头实时检测与结果可视化
训练完模型,下一步就是把照片上的识别搬到摄像头画面里。这里有一个很实用的技巧:不要在每一帧都做完整流程,而是把“人脸检测”和“身份识别”两个步骤拆开,用不同的频率执行。检测通常更耗时间,识别稍微快一些,所以检测帧间隔大一点,识别间隔小一点,整体体验会流畅很多。
下面这个写法是我在期末演示前常用的模板:
import cv2 import numpy as np import tensorflow as tf model = tf.keras.models.load_model("face_model.h5") label_map = json.load(open("label_map.json", encoding="utf-8")) cap = cv2.VideoCapture(0) frame_count = 0 last_boxes = [] detect_interval = 3 while True: ret, frame = cap.read() if not ret: break frame_count += 1 # 检测部分间隔执行,人力省事情 if frame_count % detect_interval == 0: small = cv2.resize(frame, (320, 240)) # 这里调用你的检测函数,返回归一化后的框坐标 last_boxes = detect_faces_small(small) for (sx, sy, sw, sh) in last_boxes: # 映射回原图坐标 x = int(sx * frame.shape[1] / 320) y = int(sy * frame.shape[0] / 240) w = int(sw * frame.shape[1] / 320) h = int(sh * frame.shape[0] / 240) face = frame[y:y+h, x:x+w] face = preprocess(face, target_size=(64, 64)) logits = model.predict(face[None, ...], verbose=0) pred_id = int(np.argmax(logits[0])) name = label_map.get(str(pred_id), "unknown") cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 3) cv2.putText(frame, name, (x, y-10), cv2.FONT_HERSHEY_SIMPLEX, 1.0, (0, 255, 0), 2) cv2.imshow("face recognition", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这个片段里最关键的是detect_interval和缩放比例320x240。实际演示时,如果机器性能一般,可以把间隔拉到 5,再把缩放宽高降到 224,检测速度会快很多。cv2.imshow显示的画面是原始帧,如果你发现画面左右颠倒,需要在读取后加一行frame = cv2.flip(frame, 1)。我上次就是在答辩现场被人看出来摄像头画面和镜子一样,特别尴尬,从那以后我每次接摄像头都会先imshow一帧检查翻转,确认没问题再往下写逻辑。
人脸识别系统的坑大多集中在数据集和版本兼容上,代码本身反而不是重点。你在复现这套期末大作业时,先按第三条的数据集格式把文件夹建好,再按第二部分的兼容代码跑通训练,最后用第六部分的模板接摄像头,基本上就能从“能跑”进化到“能演示”。希望帮到你。
本文还有配套的精品资源,点击获取