简介:基于Python OpenCV与dlib机器学习库构建的人脸录入与识别系统,是一份面向计算机视觉初学者、开发者和高校学生的实践型项目。资源提供完整Python工程代码,并通过图形界面支持录入人脸及中英文姓名信息,清晰串联了人脸采集、特征提取、特征库存储与实时识别等环节,适合学习人脸识别原理和进行二次开发。压缩包共25个文件,整体约96.15MB。其中9个py脚本分别实现摄像头取流、人脸检测、特征建模、CSV特征库保存、UI交互等核心功能;9张png图片用于界面展示与示例说明;2个dat文件为dlib人脸检测和特征提取模型;另附中文字体文件、演示文稿说明、依赖清单和开源许可等辅助资源,便于快速搭建运行环境。目前已有232人学习浏览。通过该资源可获得可直接运行的工程源码、预训练模型和中文显示方案,既可本地复现完整的人脸注册与识别流程,也能作为课程设计、毕业设计或人脸识别应用开发的基础模板,按需改造与扩展。
1. 基于OpenCV和dlib的人脸识别方案:一个摄像头就能录能认
用 Python 和 OpenCV 搭一套人脸录入与识别的小系统,特征部分直接交给 dlib 机器学习库,是我接触过门槛最低的落地方案。不需要 GPU、不需要自己训练模型,只要一台带摄像头的笔记本,就能把「录入人脸、留存样本、提取特征、实时识别」整条链路跑通。这个项目从摄像头里抓脸、批量存样本,到生成 128 维人脸描述子、再用欧氏距离去比对,每一步都对应一个能独立跑的脚本。适合正在做人脸识别课设、想在自己电脑上复现一遍完整流程,或者要做门禁原型验证的人。整套跑下来没有黑匣子,每个环节你都能看到数据长什么样。
2. 环境与项目结构:requirements、模型文件与目录分配
先讲选型。这套项目用 dlib 做人脸检测和特征提取,OpenCV 负责摄像头取帧和图像预处理,Tkinter 负责录入界面。选型理由很直接:dlib 的 HOG 人脸检测和 ResNet 特征描述子模型都是预训练好的,开发者不用碰训练,拿到模型直接推理;OpenCV 只干视频帧的活,两者职责分得很清楚。相比纯 OpenCV 的 Haar Cascade,dlib 在侧脸、遮挡和光照变化下的检出率要稳一个档次,这正是人脸识别里最先遇到的现实问题——正脸谁都能检出,偏一点头就丢目标才是家常便饭。
2.1 两个核心依赖的安装顺序与版本选择
requirements.txt 里大概就是 opencv-python、dlib、numpy、Pillow 这几项。安装顺序有讲究,我一般按下面走:
pip install numpy pip install opencv-python pip install dlib pip install pillow先装 numpy 是因为 opencv-python 和 dlib 在安装时都要检测 numpy 头文件,顺序反了容易在 import 阶段报版本不匹配。opencv-python 提供 cv2 模块,dlib 提供 detector、shape_predictor、face_recognition_model 三件套,Pillow 用在中文人名渲染上,少一个后面都会卡壳。
dlib 是整条链路里最容易翻车的一环。Windows 上 pip install dlib 默认走源码编译,需要预先装好 Visual Studio Build Tools 和 CMake,否则直接报错。Linux 和 macOS 相对省事,但同样要有 cmake 和 g++。装完建议立刻做一次导入验证,别等跑项目才发现环境是坏的:
python -c "import cv2, dlib, numpy; print(cv2.__version__, dlib.__version__, numpy.__version__)"能打印三个版本号,说明核心环境已通。Python 版本有条件就选 3.8 或 3.9,dlib 对新版 Python 的支持通常滞后,3.10 之后在 Windows 上很难找到现成 wheel,硬编译容易踩一晚上坑。
提示:在 Windows 上不想自己编译 dlib 的话,常见做法是去 PyPI 镜像站找对应 Python 小版本的 dlib 预编译 wheel,pip install 本地 whl 文件,能省掉整个 MSVC 编译过程。
2.2 项目文件在干什么:一张表看清分工
这套资源里脚本分得比较清楚,我按执行顺序整理成表格,新下载资源后先对照一遍,心里就有谱了:
| 脚本 | 职责 | 什么时候跑 |
|---|---|---|
| get_faces_from_camera.py | 打开摄像头、检测人脸、把脸截图存到 data 目录 | 每个新用户首次录入 |
| get_faces_from_camera_tkinter.py | 带 Tkinter 界面的录入程序,录入中英文姓名后自动建目录 | 录人脸时优先用这个 |
| features_extraction_to_csv.py | 遍历 data 下所有人脸样本,提取特征写入 CSV | 样本采集完成后 |
| face_reco_from_camera.py | 实时识别,画面里显示姓名或 Unknown | 日常使用 |
| face_reco_from_camera_single_face.py | 只识别画面里最突出的那张脸 | 门禁、单人工位场景 |
| face_descriptor_from_camera.py | 直接输出当前人脸的特征描述子 | 调试特征时用 |
| test_tkinter.py | 验证 Tkinter 界面可用性 | 环境刚搭完时 |
| how_to_use_camera.py | 摄像头基础拉流测试 | 换摄像头或怀疑设备被占用时 |
两个录入脚本的区别在入口:纯命令行版跑起来就是黑框摄像头窗口;tkinter 版先弹一个输入框,填姓名后自动创建 data/姓名 目录,再开摄像头采集。我习惯用 tkinter 版,因为姓名是中英文混合时它能直接走系统字体,绕开 OpenCV 的 putText 中文乱码问题。simsun.ttc 字体文件也在包里,这是识别脚本用来把中文名渲染到画面上的关键资源,别删。README.rst 和 LICENSE 是说明与协议,intro 目录里一般是演示素材,可以先不看。
2.3 模型文件与 data 目录的准备
文件清单里没有直接列出 .dat 模型文件,这是跑之前必须补齐的一块。dlib 的人脸检测和特征提取分别依赖两个预训练模型:shape_predictor_68_face_landmarks.dat 负责定位 68 个关键点,dlib_face_recognition_resnet_model_v1.dat 负责提取 128 维特征。这两个模型是 dlib 官方发布的,不属于项目自带代码,通常放到 model 或 models 目录,脚本里用相对路径引用。如果跑 features_extraction_to_csv.py 时报找不到模型文件,就是这段路径没对上。
data 目录由录入程序自动创建,结构是 data/姓名/xxx.jpg。比如录入「Zhang San」,就会生成 data/Zhang San/ 文件夹,里面按帧存截图。命名规则可以是连续数字或时间戳,也可以自己改成「姓名_序号.jpg」,只要后续提取脚本能遍历到就行。正式开工前,我建议先跑一遍 how_to_use_camera.py,确认摄像头能被 OpenCV 正常拉流,再往下走录入流程。
3. 人脸录入:get_faces_from_camera 采集逻辑与tkinter录入
录入是整个流程的源头,样本质量直接决定识别下限。这一步不是随便按几下快门就完事,录得太随意,后面特征再准也救不回来。
3.1 视频流里的帧抓取与正脸检测
get_faces_from_camera.py 的主循环逻辑,核心代码可以拆解成下面这样:
import cv2 import dlib import os detector = dlib.get_frontal_face_detector() # HOG 人脸检测器 cap = cv2.VideoCapture(0) # 0 表示默认摄像头 current_name = "Zhang San" # 命令行录入姓名,tkinter 版从界面获取 save_path = os.path.join("data", current_name) os.makedirs(save_path, exist_ok=True) count = 0 while count < 100: # 采集 100 张后自动退出 ret, frame = cap.read() if not ret: break gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = detector(gray, 1) # 第二参数 1 表示上采样一次,小脸检出率更高 for face in faces: x1, y1, x2, y2 = face.left(), face.top(), face.right(), face.bottom() cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) if cv2.waitKey(1) & 0xFF == ord('s'): crop = frame[y1:y2, x1:x2] cv2.imwrite(os.path.join(save_path, f"{count}.jpg"), crop) count += 1 print(f"captured {count}/100") cv2.imshow("capture", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()这段代码有两个参数值得细说。detector(gray, 1) 的第二个参数是 upsample 次数:0 表示在原始分辨率上检测,速度快但对远距离小脸容易漏检;1 表示把图像放大一倍再检测,小脸检出率明显提升,代价是每帧多花几十毫秒。录入阶段我会用 1,宁可慢一点也要保证每张脸都框住;识别阶段如果电脑性能一般,回到 0 换帧率。
waitKey 配合 s 键是按帧手动保存,好处是能控制采集节奏——我一般是让用户坐在固定位置,缓慢左右转头、轻微上下抬脸,每到一个新角度就按一次 s。100 张的目标量差不多两分钟能采完。如果完全不控制节奏,连续按 100 下得到的全是同一个角度的脸,后面特征平均出来会非常挑角度。
3.2 自动裁剪与批量落盘:样本数量与命名
裁剪时直接 frame[y1:y2, x1:x2] 取的是原始宽度,只是彩色帧上对应的一块区域。我每次都会改两处:一是给脸部区域加一圈边距,比如上下左右各扩 10 到 20 像素,不然裁剪紧贴头皮轮廓,特征提取时会损失部分脸部边缘的上下文信息;二是统一把裁剪结果缩小到固定尺寸,常见做法是 128x128 或 224x224。dlib 特征模型本身不要求固定输入,但统一尺寸能让后续特征向量的分布更集中,减少因分辨率抖动带来的数值波动。
按 s 手动存只是其中一种方式,实际用下来我可以改成另一种——人脸中心接近画面中线就自动落盘,效率更高:
h, w = frame.shape[:2] cx, cy = (x1 + x2) // 2, (y1 + y2) // 2 if abs(cx - w // 2) < w * 0.1 and count < 100: crop = frame[max(0, y1 - 10):y2 + 10, max(0, x1 - 10):x2 + 10] cv2.imwrite(os.path.join(save_path, f"{count:04d}.jpg"), crop) count += 1 print(f"auto captured {count}/100")判定逻辑是脸部中心的 x 坐标落在画面中线左右 10% 的带状区域内,才触发保存。这样能让用户自然地晃动头部,系统只采集正对镜头的帧,脏样本比例会低很多。样本数量方面,我的经验是每人大约 30~100 张之间:太少,特征平均之后方差大,换个光线就认不出;太多,录入时间拖长,还会攒下一堆重复角度的废帧。另外,采集过程中如果拍到眨眼、模糊帧,直接删掉——你可以在保存前用 Laplacian 方差做一次清晰度判断,低于阈值就跳过,这一步能显著减少后面 CSV 里的脏数据。
3.3 tkinter界面:中英文姓名录入为什么不用cv2.putText
这是我觉得项目里最该记下来的一个设计。OpenCV 自带的窗口加 cv2.putText 只支持 ASCII,直接传中文字符串,渲染出来就是??或者一排乱码。项目里的 tkinter 版本正是绕开这条路:录入的时候用 Tkinter 的 Entry 接收姓名,识别的时候再用 simsun.ttc 加 Pillow 把中文绘制到帧上。三个库各管一段——界面输入归 Tkinter,画面文字归 Pillow,OpenCV 只负责视频帧,互不打架。test_tkinter.py 就是用来验证这个界面环境是否正常的,跑它如果能弹出窗口、正常输入中文,后续录入界面基本不会出问题。
4. 特征提取:features_extraction_to_csv 与dlib 128维描述子
样本采集完成,下一步是把图片变成特征向量。这个环节是整套系统的核心,也是理解 dlib 人脸识别原理的好入口——它做的不是图像比对,而是把一张脸压缩成 128 个浮点数,用向量的距离表示人的相似度。
4.1 读取data目录并逐张提取特征
features_extraction_to_csv.py 本质上是一个批处理脚本:遍历 data 下每个子目录,对每张 jpg 做一次检测、对齐、提取,然后把结果写进 CSV。核心逻辑如下:
import os import cv2 import dlib import numpy as np detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("model/shape_predictor_68_face_landmarks.dat") face_model = dlib.face_recognition_model_v1("model/dlib_face_recognition_resnet_model_v1.dat") data_root = "data" csv_lines = [] for person_name in os.listdir(data_root): person_dir = os.path.join(data_root, person_name) if not os.path.isdir(person_dir): continue for img_name in os.listdir(person_dir): img_path = os.path.join(person_dir, img_name) img = cv2.imread(img_path) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces = detector(gray, 0) if len(faces) != 1: continue # 多脸或没脸直接跳过,避免脏数据 shape = predictor(gray, faces[0]) descriptor = face_model.compute_face_descriptor(img, shape) csv_lines.append([person_name] + list(descriptor))提取过程分三步:detector 在灰度图里框出人脸位置,predictor 在框上定位 68 个关键点,face_model 根据关键点对齐后的人脸输出 128 维浮点向量。有个小坑要提醒:compute_face_descriptor 的输入应该是彩色原图 img,如果你图省事传 gray 灰度图,特征会丢肤色纹理信息,数值上普遍偏弱。这里的灰度图只用于检测和关键点定位,最终特征计算一定要用 BGR 彩色帧。
关键点这一环节本质上是在做「人脸对齐」:68 个关键点确定眼睛、鼻子、嘴角位置后,模型内部会把脸旋转校正到标准姿态。所以录入时轻微歪头不影响最终特征,但大幅侧脸超过 60 度时,关键点定位容易漂移,特征向量会在不同角度之间跳来跳去。这也是为什么录入阶段一定要让人面向镜头缓慢转动,而不是随便抓拍。
4.2 CSV的列结构和识别时的读取方式
生成的 features_all.csv 每一行是一条样本,第一列是姓名,后面 128 列是浮点特征。用 np.savetxt 写文件时如果 fmt 写成 %s,浮点数会先转成字符串,读回来需要做类型转换:
import csv import numpy as np with open("features_all.csv", "r") as f: reader = csv.reader(f) rows = list(reader) names = [r[0] for r in rows] features = np.array([r[1:] for r in rows], dtype=float)读回来之后,强烈建议把特征转成 numpy 浮点数组,后面比对才快。128 维的欧氏距离计算如果在纯 Python 循环里逐行做,一帧比对几十号人就会卡成幻灯片;用 numpy 广播一次算完,识别帧率能差出 5 倍。如果特征库比较固定,还可以把均值向量直接存成 .npy 文件加载,省去每次解析 CSV 的启动时间:
np.save("features_all.npy", avg_features)npy 格式在加载速度上有明显优势,而且不会像 CSV 那样出现字符串精度损失。CSV 的好处是能直接打开看,适合核对样本数量,npy 适合正式跑识别时用,两者可以并存。
4.3 平均特征与单条特征:怎么选更稳
同一个人的 30 张样本会产生 30 个 128 维特征向量,识别库不可能全量拿去做逐条比对,既慢又容易被极端角度带偏。这个项目的常用做法是对同一姓名下的所有特征做均值,得到一个「平均脸描述子」,后续每帧提取的特征跟均值比,稳定性最好:
person_features = {} for name, feat in zip(names, features): person_features.setdefault(name, []).append(feat) avg_features = {} for name, feat_list in person_features.items(): avg_features[name] = np.mean(np.vstack(feat_list), axis=0)做均值之前,我建议先对每个向量做一次 L2 归一化,再做平均:
features = features / np.linalg.norm(features, axis=1, keepdims=True)这样做的原因是:如果某张样本过亮,特征数值整体偏大,直接平均会让这张图的特征在均值里占主导。归一化之后每个向量的模长都是 1,平均结果才真正代表「几何中心」,匹配阶段欧氏距离的表现也更稳定。这一步不做,后面调阈值时会发现同一人不同光照下的距离忽大忽小,非常折磨人。
5. 避坑与排查:安装失败、中文乱码与识别翻车的五个现场
这条链路跑下来,问题集中在几个固定位置。我把踩过的和身边人问得最多的五类整理一遍,每一条都按「现象→原因→解决」的模式写,方便照着排查。
5.1 dlib安装失败:CMake与wheel两条路线
现象:pip install dlib 跑到一大半直接报错,常见的有 error: command 'cl.exe' failed 或者找不到 CMake。原因:dlib 在 Windows 上默认走源码编译,本机必须装了 MSVC 编译器和 CMake,缺一个都编不过。解决方案分两条:一是装 Visual Studio Build Tools,勾选「使用 C++ 的桌面开发」工作负载,装完重新执行 pip install dlib;二是找对应 Python 小版本的 dlib 预编译 wheel,pip install 本地 whl 文件,几分钟装完,不需要编译环境。装完验证方式不变:import dlib 后打印version,能跑通再继续。千万别在环境没验证的情况下直接去跑录入脚本,摄像头一开才发现 dlib 没进来,浪费时间。
5.2 中文姓名显示成方块
现象:识别窗口里「张三」的位置出现一排方框或问号。原因:cv2.putText 不支持中文字符,直接传 unicode 字符串,OpenCV 内部按 ASCII 渲染,中文字形根本画不出来。解决:用 simsun.ttc 加 Pillow 把中文绘制到帧上。具体操作是把 OpenCV 的 BGR 帧转成 PIL Image,用 ImageDraw 以指定字体把名字画到人脸框上方,再转回 numpy 数组。注意字体大小建议 28 以上,太小在低分辨率帧上会糊成一团,看起来像乱码,实际是字号问题。这个项目自带 simsun.ttc,跑识别前先确认它和脚本在同一目录,路径写错了中文名照样全是方块。
5.3 摄像头打不开或画面黑屏
现象:VideoCapture(0) 返回 True,但 read 一直拿到空帧,或者直接返回 False。原因:多半是摄像头被其他程序占用,也可能是索引不对。Windows 下微信、浏览器视频通话经常会后台占着摄像头,OpenCV 抢不到设备。解决:先单独测摄像头,不要直接跑大脚本:
import cv2 cap = cv2.VideoCapture(0) ret, frame = cap.read() print(ret, frame.shape if frame is not None else None) cap.release()ret 为 True 且能打印出 shape,说明摄像头没问题,问题在占用,关掉占用程序再跑;ret 为 False,就把索引从 0 试到 1、2。台式机外接摄像头常见索引 1,笔记本内置摄像头一般才是 0。另外,同一时刻不要让两个 Python 进程同时打开摄像头,OpenCV 在一个进程内只能有一个实例独占设备,重复打开会直接黑屏。
5.4 识别结果不稳定:光照和角度在捣乱
现象:同一张脸有时识别成本人,有时判成 Unknown,偶尔还认成别人。原因:特征提取对光照非常敏感。逆光和强侧光会让 128 维特征向量整体偏移,偏移量超过阈值就翻车。解决分两步:录入时尽量在均匀正面光下采集样本,识别环境和录入环境的光照差异越小越好;如果某方向集中翻车,比如下午靠窗坐就识别失败,那就针对这个光线角度重新录一批样本,让均值特征覆盖到该场景。另外把 detector 的上采样参数从 1 降回 0,虽然小脸检出率低一点,但帧率提升后画面更流畅,连续多帧比对的稳定性会弥补采样率损失。
5.5 CSV里混进脏数据,识别直接罢工
现象:识别脚本加载 features_all.csv 时报 ValueError,说行的列数对不上;或者识别永远返回 Unknown,怎么调阈值都没用。原因:data 目录里混入了非人脸图片,某张图检测到 0 张脸被跳过但文件名已被引用,或者录制过程中中断导致半张 jpg 写入,特征提取后出现 nan。解决:提取特征时检测不到人脸不要只是 continue,先把文件路径打进日志,提取完后加一行全局检查:
if np.isnan(features).any(): print("nan detected, locate and remove corrupted samples")有 nan 就定位到具体图片删除重录,别硬扛。还有另一个常见情况:有些人脸照片里检测出了多张脸,比如背景带合影,脚本会跳过它,但该样本已经被记入 CSV 行的序号,后续对齐会错位。提取时最好严格要求 len(faces) == 1 才写入,多脸直接 skip 并打印文件名,留个复盘依据。
6. 实时识别与阈值:欧氏距离判定和单脸模式怎么选
识别主循环跟采集很像,区别在于多了「和特征库比对」这一步。每帧检测到人脸后,提取描述子,然后和库里的平均特征逐个人算欧氏距离,取最小距离的那个名字作为候选:
def recognize(descriptor, avg_features, threshold=0.6): distances = {} for name, avg in avg_features.items(): distances[name] = np.linalg.norm(descriptor - avg) best_name = min(distances, key=distances.get) best_dist = distances[best_name] return best_name if best_dist < threshold else "Unknown"欧氏距离越小,说明当前人脸和库里的平均特征越接近。0.6 是一个经验阈值——在笔记本内置摄像头、日常光线下,同一人正常角度的距离通常在 0.35 到 0.55 之间,陌生人基本在 0.7 以上。但不要迷信 0.6,每个人的摄像头、光照、人脸库构成都不一样。我的调法是这样:先收集 10 次自己人被识别时的距离最大值,再收集 5 次陌生人的距离最小值,取两者中间值当阈值。太严容易把自家人拒之门外,太松陌生人也会放进。
单脸模式值得多说一句。face_reco_from_camera_single_face.py 适合「画面里一次只该有一个人」的场景,比如闸机、工位签到。它的内部逻辑是取面积最大或正脸置信度最高的一张脸,其余脸全部丢弃,避免多人经过时识别框在姓名之间跳来跳去。如果做的是教室点名那种多人同框场景,就要用主版本 face_reco_from_camera.py,逐个人脸框分别去做比对。
前年我在一台旧笔记本上跑这个流程,图省事把阈值调到了 0.4,结果自己坐在摄像头前任凭怎么摆脸都显示 Unknown,那一刻我才理解什么叫「阈值不是拍脑袋定的」。从那以后,我每次换摄像头、换光照环境、换录入人,都强制走一遍「录入→提取→阈值标定→测距」的闭环,不做完这一步不上线。这套流程跑顺后你会发现,识别系统的复杂度有一大半其实在数据采集和阈值标定上,模型本身反而是最省心的环节。希望帮到你。
本文还有配套的精品资源,点击获取