简介:这是一套适合Python初学者的入门级人脸识别项目代码包,覆盖人脸检测、特征提取、匹配识别三大核心环节。压缩包大小为3.33MB,共13个文件,包含5个py脚本、6张jpg测试图像和2张png图片,脚本与示例图片配套,可直接运行观察效果。代码中既能看到基于OpenCV Haar级联分类器的人脸检测实现,也涉及PCA/LDA特征提取和余弦相似度匹配等经典方法,并配有可用于实验的样例图像,便于理解从像素到特征向量的转换过程。Python脚本与示例图片一一对应,方便对照输出结果调试参数,对于希望结合动手实践掌握计算机视觉基础的中级学习者,这套代码能帮助快速建立人脸识别技术框架,同时提供了扩展至深度学习模型的参考路径。资源已有160人学习下载,文件组织简洁,适合自学与课程作业参考。
1. 为什么人脸识别代码要从Python入手:调试体验与学习曲线
在人脸识别项目里,选型第一件事不是选算法,而是选语言。很多工业项目用C++或Java落地,但如果你要快速验证“人脸检测-特征提取-身份比对”这条链路,Python是最容易跑通的选择。原因很直接:OpenCV、dlib、face_recognition、InsightFace这些库都有Python绑定,社区示例多到随手可抄。我自己经历过从Java切换到Python做原型的过程,原本需要手动管理内存的模型加载,在Python里用一个列表就能把多张人脸的特征向量接住。下面我会从零开始写一套可离线运行的人脸识别代码,只组合成熟算法,把“人传人”式的人脸流转流程跑通,既适合在公司内部做门禁机演示,也给毕设或边缘设备预研留了可扩展的接口。
2. 人脸识别算法选型:从Haar、DNN到人脸向量模型
2.1 先分清人脸检测和人脸识别,别混用
很多人说“人脸识别代码”时,其实只写了人脸检测——把脸框出来,然后程序就不知道是谁了。真正的识别要分两步:检测(人脸在哪)和识别(这张脸是谁)。检测的输出是人脸框坐标,识别的输出是身份ID或置信度。如果你用的是OpenCV自带的Haar级联分类器,那只能做检测,不能做识别。想识别,就得在检测基础上提取人脸特征向量,再和一个底库做比对。这里就引出“人脸——特征向量——身份”的传递链条,也就是标题里说的“人传人人脸”。
2.2 检测模型选型:Haar、HOG、DNN与RetinaFace
人脸检测的公共参数是速度与召回率。选型时先问三个问题:跑在CPU还是GPU?侧脸多不多?目标人的最小像素尺寸是多少?
| 检测方案 | 模型体积 | CPU实时性 | 抗遮挡/侧脸 | 典型场景 |
|---|---|---|---|---|
| Haar Cascades | 约1MB | 较好 | 差,只吃正脸 | 文档扫描、手机解锁辅助 |
| HOG + Linear SVM | 约80MB | 一般 | 中,能接受轻微偏转 | 静态图片、低功耗DSP |
| OpenCV DNN (SSD/RFB) | 约5-10MB | 好 | 中上,对光照鲁棒 | 摄像头实时、门禁机 |
| MTCNN | 约6MB+(PNet/RNet/ONet) | 差 | 较好,能出5点对齐 | 高精度标注、GPU |
| RetinaFace | 约16MB+ | 差 | 好,带密集关键点 | 质量要求高的识别链路 |
Haar在2010年前是主力,现在只适合做快速预筛。OpenCV DNN的SSD模型是MobileNet-SSD,在树莓派上可以跑到15到20帧每秒,适合瘦死的边缘设备。MTCNN和RetinaFace会先缩放图像金字塔,所以CPU上会掉帧,但返回的关键点信息对后续对齐非常关键。我的常见做法是:如果只用OpenCV系列,就用OpenCV DNN做检测;如果后续识别要追求门禁机级别精度,检测换成RetinaFace或MTCNN,再把检测框传给下一级特征模型。
2.3 人脸特征模型:dlib、face_recognition与InsightFace
检测完人脸后,需要把框内图像压缩成一个向量。常见的人脸识别算法有三个层次:
- face_recognition:封装了dlib的128维ResNet特征,接口只有两行,适合入门和内部工具。缺点是模型老,对极端光照和儿童脸容易翻车。
- dlib:提供68点关键点检测和128维特征模型,训练数据来自LFW,精度中等。好处是可离线、跨平台,坏处是安装麻烦。
- InsightFace/ArcFace:现在工业界主流,用ArcFace损失训练的512维特征,在LFW、MegaFace上指标领先,且对侧脸、口罩遮挡更稳。如果做人脸识别门禁机这类产品,推荐直接用InsightFace的输出做比对。
选型时不要迷信越新越好。如果你的底库只有几十人,face_recognition完全够用。如果要做大量数据的人群识别,ArcFace类模型更合适。后面所有代码都以face_recognition为主,因为它的API固定,方便你看懂流程,换成InsightFace时只需要替换特征提取部分。
2.4 特征比对:欧氏距离与余弦相似度
人脸特征向量的维度常见有128维(dlib/face_recognition)、256维、512维(ArcFace)。识别本质是比较两个向量。比对方式一般为:
- 欧氏距离:越小越像,取值范围理论上0到无穷。face_recognition的compare_faces的tolerance参数使用的就是归一化后的欧氏距离。
- 余弦相似度:越大越像,取值范围-1到1。需要提前对向量做L2归一化,再算点积。
一个容易被忽略的点:不同模型输出的向量空间不同,阈值不能通用。dlib的0.6与InsightFace的0.6意义完全不一样。每次更换模型,都要重新采集一批底库样本标定阈值,否则识别率全是幻觉。
3. 用Python跑通最小人脸识别代码:安装、依赖与第一个识别结果
3.1 环境准备:Python版本与依赖清单
热词里大量出现python安装、python入门,这里直接给出可复制的命令。我通常用Python 3.10或3.11,避免3.12下dlib某些旧版本编译失败。装依赖前先建虚拟环境:
python -m venv venv source venv/bin/activate # Windows: venv\Scripts\activate pip install --upgrade pip pip install opencv-python numpy cmake dlib face-recognition参数说明:cmake是dlib编译需要,Windows用户必须装Visual Studio Build Tools才有C++编译环境。如果你不想自己编译dlib,可以尝试pip install dlib-bin,或者在Anaconda里用conda install -c conda-forge dlib。face_recognition会自动安装dlib,但版本可能不是最新的,遇到接口报错时先看版本。
如果这一步失败,最常见的坑是:没有安装cmake、没有Python开发头文件、或者pip下载源码超时。建议先装cmake,再装dlib,最后装face_recognition,分开执行。不要盲目信任网上那些一键安装的命令,尤其是csdn上转载的老博客,很多依赖已经过时。
3.2 最小人脸识别代码:编码已知人脸并比对
先写一个最小可运行脚本。这个脚本解决“这张照片是不是张三”的问题。
import face_recognition # 加载已知人脸并计算特征向量 known_image = face_recognition.load_image_file("alice.jpg") alice_encoding = face_recognition.face_encodings(known_image)[0] # 加载未知图片 unknown_image = face_recognition.load_image_file("test.jpg") unknown_encodings = face_recognition.face_encodings(unknown_image) # 如果没人脸,直接返回不匹配 if not unknown_encodings: print("未检测到人脸") else: results = face_recognition.compare_faces([alice_encoding], unknown_encodings[0], tolerance=0.6) distance = face_recognition.face_distance([alice_encoding], unknown_encodings[0])[0] print(f"是否匹配: {results[0]}, 距离: {distance:.4f}")逻辑说明:load_image_file直接读图返回RGB数组,face_encodings内部依次完成检测、对齐、特征提取,返回一个list。因为一张图可能有多张脸,所以代码里取第一个人的边界框。compare_faces会把tolerance转换成距离阈值,小于阈值视为同一人。face_distance给你原始距离,方便后续自己定阈值。注意一个问题:face_encodings(known_image)[0]如果原图中没有人脸会抛IndexError,生产代码里要提前判断长度。
3.3 实时摄像头人脸识别代码:从图片到视频帧
接下来的代码是很多教程都会写的摄像头识别,但我会把缩放、多目标匹配、显示三部分的坑直接标出来。
import face_recognition import cv2 import numpy as np known_face_encodings = [] known_face_names = [] def register_face(name, image_path): img = face_recognition.load_image_file(image_path) encodings = face_recognition.face_encodings(img) if len(encodings) == 0: raise ValueError(f"{image_path} 中没检测到人脸") known_face_encodings.append(encodings[0]) known_face_names.append(name) register_face("Li", "li.jpg") register_face("Chen", "chen.jpg") video_capture = cv2.VideoCapture(0) if not video_capture.isOpened(): raise RuntimeError("摄像头无法打开,可能被占用或驱动有问题") while True: ret, frame = video_capture.read() if not ret: break # 缩小帧到1/2,提高处理速度 small_frame = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) rgb_small_frame = cv2.cvtColor(small_frame, cv2.COLOR_BGR2RGB) # 当前帧的人脸位置与特征 face_locations = face_recognition.face_locations(rgb_small_frame) face_encodings = face_recognition.face_encodings(rgb_small_frame, face_locations) # 逐个匹配 for face_encoding, face_location in zip(face_encodings, face_locations): matches = face_recognition.compare_faces(known_face_encodings, face_encoding, tolerance=0.5) name = "Unknown" if True in matches: distances = face_recognition.face_distance(known_face_encodings, face_encoding) best_match_index = np.argmin(distances) if matches[best_match_index]: name = known_face_names[best_match_index] top, right, bottom, left = face_location # 因为缩小过,画框要乘2 top *= 2; right *= 2; bottom *= 2; left *= 2 cv2.rectangle(frame, (left, top), (right, bottom), (0, 255, 0), 2) cv2.putText(frame, name, (left, top - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow("Face Recognition", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break video_capture.release() cv2.destroyAllWindows()这段代码里,face_locations返回top、right、bottom、left,注意顺序不要记错。face_recognition.face_locations默认使用HOG模型,如果想让检测更准可以加参数model="cnn",但CPU上帧率会明显下降。cv2.resize缩小操作非常关键——face_recognition对越小的图检测越快,但对太小的脸会漏检。一般把短边限制在480到640之间比较合适。
3.4 常见坑:IndexError、编译失败与接口变化
- IndexError:
face_encodings返回空列表,说明检测漏了。解决办法是换检测模型,或检查图片有没有真的加载成功。 - dlib安装失败:最常见是无法找到dlib的C++库。在Ubuntu上先
sudo apt install build-essential cmake,Windows要安装Visual Studio Build Tools。 - 接口版本:face_recognition的老教程使用的内部结构在不同版本中有微调。遇到
AttributeError时,先print(face_recognition.__version__)确认版本。
提示:不要在for循环里反复调用
load_image_file读取同一个人脸,底库图片应该一次性加载并缓存成向量。
4. 人脸识别代码的工程化:人脸对齐、特征比对与阈值调优
4.1 为什么要先做人脸对齐
人脸检测框是矩形的,但人脸的姿态、拍摄角度会让五官在框内错位。直接把这个框传给特征提取模型,同一个人会因为角度差几度而得到差别很大的特征向量。所以正规的人脸识别代码里,检测和人脸对齐是紧耦合的。对齐的物理意义是把两只眼睛和鼻子的位置扭到一个标准坐标系,让特征模型拿到的输入尽量是正面照。
人脸识别门禁机上最常见的现象是:同一人正面识别通过,低头或侧脸就变成Unknown。答案不是调大阈值,而是先检查有没有对齐。如果你用RetinaFace检测,它会同时返回5个关键点,直接用这些关键点做相似变换即可。如果你用face_recognition,它内部已经默认做了对齐,但它的对齐依赖68点模型,对极端遮挡依然会失效。
4.2 用dlib手工做关键点对齐的Python代码
一个可靠的对齐实现,只需要三组点:左眼外角、右眼外角、鼻子。dlib的68点索引中,36是左眼外角,45是右眼外角,30是鼻尖。使用OpenCV的estimateAffinePartial2D可以直接求得相似变换矩阵。下面代码把对齐后的脸统一缩放到200x200。
import cv2 import dlib import numpy as np detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") def align_face(img, left_eye, right_eye, nose): # 源点:左眼、右眼、鼻尖 src_points = np.array([left_eye, right_eye, nose], dtype=np.float32) # 目标点:标准坐标,左眼(0.35,0.25)、右眼(0.65,0.25)、鼻尖(0.5,0.45) dst_points = np.array([(0.35, 0.25), (0.65, 0.25), (0.5, 0.45)], dtype=np.float32) transform, _ = cv2.estimateAffinePartial2D(src_points, dst_points) aligned = cv2.warpAffine(img, transform, (200, 200)) return aligned # 调用示例 img = cv2.imread("input.jpg") gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) rects = detector(gray, 1) if len(rects) > 0: landmarks = predictor(gray, rects[0]) left_eye = (landmarks.part(36).x, landmarks.part(36).y) right_eye = (landmarks.part(45).x, landmarks.part(45).y) nose = (landmarks.part(30).x, landmarks.part(30).y) aligned_face = align_face(img, left_eye, right_eye, nose) cv2.imwrite("aligned.jpg", aligned_face)理解这段代码的关键在于estimateAffinePartial2D:它只计算旋转、缩放和平移,不计算裁剪和投影,正好符合人脸对齐的需求。目标点是相对坐标,最终输出被缩放到200x200。如果你要处理非正脸,还可以增加两个嘴角点做仿射变换,但对大多数门禁场景,三点对齐已经够用。
4.3 特征比对阈值怎么调:tolerance 与验证集
很多项目上线后出现问题,都在于直接把网上代码里的tolerance=0.6拿过来用。不同场景、不同摄像头分辨率、甚至不同年龄段人群,合适的阈值都不同。我的标定方法:准备三类图片,正样本(同一个人多角度多天拍摄)、负样本(不同人)、空背景图,然后跑一个阈值扫描。
import face_recognition import numpy as np def threshold_tuning(known_paths, positive_paths, negative_paths): known = [] for path in known_paths: img = face_recognition.load_image_file(path) enc = face_recognition.face_encodings(img)[0] known.append(enc) pos_dists = [] for path in positive_paths: img = face_recognition.load_image_file(path) enc = face_recognition.face_encodings(img)[0] pos_dists.append(face_recognition.face_distance(known, enc).min()) neg_dists = [] for path in negative_paths: img = face_recognition.load_image_file(path) enc = face_recognition.face_encodings(img)[0] neg_dists.append(face_recognition.face_distance(known, enc).min()) for threshold in np.arange(0.3, 0.8, 0.05): tp = sum(1 for d in pos_dists if d <= threshold) fp = sum(1 for d in neg_dists if d <= threshold) fn = len(pos_dists) - tp tn = len(neg_dists) - fp acc = (tp + tn) / (len(pos_dists) + len(neg_dists)) print(f"tolerance={threshold:.2f}, ACC={acc:.3f}, FP={fp}, FN={fn}") # 示例 threshold_tuning(["alice.jpg"], ["alice_1.jpg", "alice_2.jpg"], ["bob.jpg"])注意:这段代码直接用全部负样本,没做交叉验证。正式标定时要按人分组,防止同一人出现在正负样本里。表格式的经验数据如下,但不要当标准用。
| tolerance | 安全方向 | 误识别率 | 适用场景 |
|---|---|---|---|
| 0.4 | 更严格 | 很低 | 门禁、支付 |
| 0.5 | 平衡 | 低 | 考勤、单机验证 |
| 0.6 | 更宽松 | 中 | 快速通行、手机 |
4.4 底库从list到faiss:大量数据的比对
当底库超过几百个人,用list做for循环比对会很慢。这时常见做法是把所有人脸向量全部堆叠成矩阵,用faiss做近似最近邻检索。
import faiss import numpy as np # 假设 known_face_encodings 是一个list encodings = np.array(known_face_encodings, dtype=np.float32) # L2归一化,让点积等价于余弦相似度 faiss.normalize_L2(encodings) dim = encodings.shape[1] index = faiss.IndexFlatIP(dim) index.add(encodings) # 查询时也要归一化 query = np.array([face_encoding], dtype=np.float32) faiss.normalize_L2(query) scores, idx = index.search(query, k=1) if scores[0][0] > 0.6: # 余弦阈值,需自行标定 print("命中:", known_face_names[idx[0][0]]) else: print("Unknown")faiss的IndexFlatIP是暴力内积搜索,数据量大时换IndexIVFFlat。但注意:faiss索引一旦构建,新增人脸需要重建或使用IndexIDMap。而且归一化这一步必须在训练和查询时都用同一套预处理,否则阈值会漂移。
5. 人脸识别代码在门禁机与边缘设备上的部署验证技巧
5.1 先把“摄像头回放”跑通,再做真机联调
很多项目卡在“摄像头前一站,代码就崩”,因为摄像头依赖环境,难以重复。我的技巧是做成视频文件回放模式,代码里用一个参数切换视频源。
source = "test_video.mp4" # 0 表示摄像头 cap = cv2.VideoCapture(source) while cap.isOpened(): ret, frame = cap.read() if not ret: cap.set(cv2.CAP_PROP_POS_FRAMES, 0) # 循环播放 continue # 复用上一章的识别逻辑,帧数据统一走函数接口这样做的好处是:能拿着同一段视频在不同机器上对比帧率和准确率,还能检查是不是某个特定帧导致检测器崩掉。实际部署到人脸识别门禁机上,大多数模块(比如tx510人脸识别模块)接受的是jpeg流或视频流,你仍然可以用这段伪摄像头代码模拟输入,避免调试时把人锁在门外。
5.2 定量验证:帧率、耗时与失败率怎么记录
记录识别结果时不要只print,用CSV记录每帧的时间戳、检测人数、匹配身份、距离值。用脚本统计平均识别耗时、最大耗时和失败率。下面是一段记录逻辑。
import csv from time import time with open("metrics.csv", "w", newline="") as f: writer = csv.writer(f) writer.writerow(["ts", "face_count", "name", "distance", "duration_ms"]) while True: start = time() # 处理一帧,得到 faces_info,这是你识别函数的结构化返回 duration_ms = (time() - start) * 1000 for info in faces_info: writer.writerow([info["ts"], info["count"], info["name"], info["distance"], duration_ms])最后一条建议:在真实代码里关闭cv2.imshow窗口也能省下5%到10%的帧率损失。部署到边缘设备时,尽量使用无界面模式运行,把识别结果通过MQTT或HTTP发送出去。
本文还有配套的精品资源,点击获取