ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于dlib与OpenCV的人脸识别系统:从关键点检测到128维特征实战解析

基于dlib与OpenCV的人脸识别系统:从关键点检测到128维特征实战解析 简介基于OpenCV与dlib的Python人脸识别系统毕业设计资源包含完整源码与说明文档主要面向计算机、通信、人工智能、自动化等专业的学生和从业者适合用于期末课程设计、课程大作业或毕业设计参考。代码经过调试测试运行稳定答辩评审得分达98分整体具有较高学习借鉴价值。压缩包内共118个文件以人脸样本图片、特征数据表格、Python源码、预训练模型以及演示视频为主类型涵盖jpg/png、csv、py、dat、mp4/avi等包体大小约121.31MB。其中dat模型文件用于人脸关键点检测与深度特征提取csv文件存储多人的面部特征向量可直观体验从人脸检测、特征提取到识别比对的完整流程。目前已有283人学习下载适合基础薄弱者循序渐进学习也可供有能力的开发者在此基础上调整算法或界面扩展考勤、门禁等应用。1. 这个开箱即跑的人脸识别系统核心不在OpenCV而在dlib拿到这份毕业设计源码时先把文件清单过了一遍shape_predictor_68_face_landmarks.dat 和 dlib_face_recognition_resnet_model_v1.dat 这两个权重文件占了大头说明真正干活的是 dlib 的检测与识别模型而 OpenCV 在这里只承担视频读取、图像格式转换和画框显示。项目自带 ross.csv、rachel.csv、phoebe.csv 等特征文件以及一段写好的测试视频 result1.avi这意味着它不是一个只有 main.py 的玩具项目而是一套完整的“注册人脸 → 生成特征文件 → 与视频流中的人脸做对比”的闭环。答辩能拿到 98 分并不意外因为整个流程覆盖了人脸检测、关键点定位、特征嵌入、距离度量、阈值判定这几个标准模块。对于计算机、人工智能、自动化方向的学生来说这套代码既可以直接当作课程大作业交差也能作为研究人脸识别 pipeline 的起点——尤其是 dlib 特征提取部分换一个数据集就能迁移到其他识别任务上。2. 先立理论dlib的人脸检测与68点关键点模型2.1 为什么选dlib而不是OpenCV的Haar级联很多初学者在做人脸识别时第一反应是用 OpenCV 自带的 haar_cascade 分类器。那个东西在正脸、光照均匀的场景下确实能框出人脸但一到侧脸、戴眼镜、光线变化大的环境误检和漏检就非常明显。而且 Haar 检测器只输出一个矩形框没有五官的关键点坐标后续如果要做人脸对齐、姿态校正还得另想办法。dlib 的 HOG SVM 检测器则是一个方向梯度直方图特征配合线性分类器的方案dlib 官方在公开数据集上做过评测它的误检率比 Haar 低不少对角度变化也更容忍。更重要的是dlib 的检测结果可以直接喂给 68 点关键点模型一步到位拿到眼、鼻、嘴的位置这些关键点正是后续对齐和特征提取的“地基”。这个系统里选择了 dlib.get_frontal_face_detector() 作为默认检测器而不是 dlib 的 CNN 检测器。原因是 HOG 版本在 CPU 上跑实时视频流能到 20~30 帧而 CNN 检测器即便在 GPU 上也只有几帧对毕业设计演示来说实时性往往比极致准确率更重要。如果你的使用场景是离线批量处理一批照片可以考虑把 detector 换成 dlib.cnn_face_detection_model_v1但要注意它的返回值是一个带 confidence 的 mmod_rectangle后续取 .rect 才能兼容 predictor 接口。2.2 检测器和关键点模型的加载姿势代码里第一步是实例化两个模型文件。注意shape_predictor_68_face_landmarks.dat 是 dlib 官方在 iBUG 300-W 数据集上训练好的 68 点人脸关键点模型大约 99MB而 dlib_face_recognition_resnet_model_v1.dat 是另一个 128 维特征提取模型约 32MB。下面的代码演示了如何正确加载并绘制关键点import dlib import cv2 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) img cv2.imread(test_face.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) # 第二个参数表示上采样次数 for face in faces: landmark predictor(gray, face) for i in range(68): x landmark.part(i).x y landmark.part(i).y cv2.circle(img, (x, y), 1, (0, 255, 0), -1)这段代码的关键点有两个第一detector(gray, 1) 中的第二个参数是图像金字塔的上采样次数设为 1 表示把输入图像放大一倍后再检测这样可以捕捉更小的人脸但检测耗时也会显著上升在实时视频流中我一般会设为 0用原始尺寸检测来换取速度。第二predictor 接受一个 dlib.rectangle 对象和灰度图返回的 landmark 对象通过 part(i) 访问第 i 个关键点坐标顺序是固定的0~16 是下颚轮廓17~26 是眉毛27~35 是鼻子36~47 是眼睛48~67 是嘴巴。提示如果检测不到人脸先检查输入是不是灰度图。dlib 的关键点模型虽然在彩色图上也能跑但官方示例都用灰度图且 OpenCV 的 BGR 通道顺序与 RGB 不同直接使用 cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) 是常见做法不要用 cv2.imread 后只取第一个通道那种写法会损失亮度信息。这张 68 点的关键点索引图可以从 dlib 官方博客找到建议打印出来贴在屏幕边上后面写对齐代码时会反复用到。3. 从人脸图到128维特征ResNet特征提取与相似度度量3.1 dlib_face_recognition_resnet_model_v1 到底做了什么68 个关键点只是对人脸形状的描述还不足以用来判定“这是张三还是李四”。真正承担身份识别的是 dlib_face_recognition_resnet_model_v1.dat 所封装的 ResNet 网络。这个模型经过改造后的输入是一张经过对齐的人脸图输出是一个 128 维的浮点向量。深度网络在训练时会把同一人的不同姿态、表情、光照映射到相近的向量区域而不同人之间的向量距离会明显拉大。因此“人脸识别”这个任务在这里被转换成了“向量最近邻搜索”。在 Python 中加载这个模型并计算特征向量的方式如下facerec dlib.face_recognition_model_v1(dlib_face_recognition_resnet_model_v1.dat) # 先用检测器和关键点模型拿到 shape face faces[0] shape predictor(gray, face) # 计算128维特征num_jitters1表示不做抖动增强 descriptor facerec.compute_face_descriptor(img, shape, num_jitters1)注意 compute_face_descriptor 有三个常见参数第一个参数是包含人脸的三通道图像第二个是已经求出的关键点对象第三个 num_jitters 是抖动次数。这个参数很有讲究它控制模型对图像做多少次随机缩放、平移、旋转后再取平均用于提高特征的鲁棒性。num_jitters1 时只算一次速度最快适合实时识别在离线批量生成特征文件时我一般会把它调到 10这样每张人脸的特征会更稳定代价是多花十倍的推理时间。项目中 feature_all.csv 和 ross.csv 这些文件实际上就是把这些 128 维向量按行写进 CSV 的结果。每一行代表一张人脸的特征行数就是样本数。0.102134 -0.085392 0.093291 ... (共128列)在 CSV 里一般用逗号分隔。加载时直接用 csv.reader 逐行读出来再转成 numpy 数组即可。3.2 欧氏距离与余弦相似度的取舍拿到两个 128 维向量之后怎么判断是不是同一个人dlib 官方推荐使用欧氏距离并且给出了一个经验阈值通常小于 0.6 可以认为是同一人介于 0.6 到 0.8 之间是可疑区间大于 0.8 则大概率是不同的人。在实际工程里阈值需要根据摄像头采集的质量和你接受误报的程度来调。如果安全要求高可以把阈值压到 0.5如果希望尽量减少漏报可以扩到 0.7。计算距离的代码非常简单import numpy as np def euclidean_distance(vec_a, vec_b): a np.array(vec_a, dtypenp.float32) b np.array(vec_b, dtypenp.float32) return np.linalg.norm(a - b)有时候也会看到有人用余弦相似度把距离阈值换成相似度大于 0.8 之类的条件。这两种做法在 dlib 特征向量上表现差异不大但余弦相似度对特征向量的长度不敏感所以如果发现某个人的特征范数明显偏大比如图像过亮或过暗导致向量模变大余弦相似度会更稳定。我个人的习惯是同一摄像头下的样本用欧氏距离跨摄像头或跨设备对比时优先用余弦相似度因为不同设备对同一人脸的响应可能引入整体缩放。下面是一张我在实际项目里用过的阈值参考表通常可以在答辩演示时拿出来讲解使用场景建议阈值备注单摄像头、固定光照0.5误识别极少但偶有漏检室内多角度0.55 ~ 0.6平衡漏检与误检室外光线变化大0.65 ~ 0.7需要接受一定的误识率跨摄像头/设备0.6 ~ 0.7建议先做特征归一化阈值调好之后整个识别主轴就清晰了检测人脸 → 取关键点 → 算 128 维特征 → 与库中所有特征计算距离 → 取最小距离对应的标签。这套流程不管后续怎么优化骨架是不会变的。4. 特征库与注册流程CSV 里存的是什么4.1 批量提取人脸特征并写入 ross.csv打开项目目录会看到 ross.csv、rachel.csv、phoebe.csv 这一组单人类的特征文件以及一个 feature_all.csv。前者是单独某个人的全部样本特征后者是把所有人特征合并在一起的训练/查询库。我推测作者的原始流程是收集每个人若干张照片逐张计算 128 维特征然后把属于同一个人的特征平均成一个向量写进单独的 CSV最后再把这个人的多个样本或者一个平均向量追加到 feature_all.csv 里。这里提供一段可复用的批量生成脚本import csv import dlib import cv2 import os detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) facerec dlib.face_recognition_model_v1(dlib_face_recognition_resnet_model_v1.dat) def encode_face(image_path): img cv2.imread(image_path) if img is None: return None gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) if len(faces) ! 1: return None shape predictor(gray, faces[0]) descriptor facerec.compute_face_descriptor(img, shape, num_jitters10) return list(descriptor) def build_single_csv(person_dir, output_csv): vectors [] for filename in os.listdir(person_dir): path os.path.join(person_dir, filename) vec encode_face(path) if vec is not None: vectors.append(vec) if not vectors: return mean_vec [sum(col) / len(vectors) for col in zip(*vectors)] with open(output_csv, w, newline) as f: writer csv.writer(f) writer.writerow(mean_vec)这段代码的逻辑说明encode_face 里先做检测要求每个人脸图像恰好包含一张脸如果有多张说明图片选择有问题需要人工裁剪。num_jitters 设成 10是为了在注册阶段拿到一个“稳定特征”。build_single_csv 把同一个人的多张照片的特征逐列求平均得到一行的 128 维数据。为什么要平均而不是直接保存每张照片的特征因为注册库只需要一个代表性特征平均能有效降低光照与表情带来的噪声避免后期比对时同一人的特征点过于分散。4.2 在线追加注册把当前摄像头画面中的特征写进 CSV如果你的应用场景是动态添加新用户而不是提前准备照片那就得走在线注册流程从摄像头中抓几帧检测到人脸后提取特征取平均后追加到 CSV。下面是简化的追加逻辑def append_face_to_csv(new_feature, csv_path): with open(csv_path, a, newline) as f: writer csv.writer(f) writer.writerow(new_feature)这里有一个容易被忽略的坑CSV 文件追加写入时如果 feature_all.csv 用于后续比对那么每次加载时都需要把所有行读进内存。当用户量达到几百人、每人 10 个特征时CSV 会有上千行线性扫描所有特征的距离会明显变慢。对于毕业设计来说问题不大但如果你想把它做成真正能用的员工打卡系统就需要把 CSV 换成 SQLite或者用 faiss 建立向量索引。读 CSV 的方式也要注意dlib 的 vector 对象直接遍历会得到 float 值但 numpy.array(descriptor) 后保存时默认的 str 格式可能带有括号建议在写文件前用 list(descriptor) 转换一次。项目里 feature_all.csv 和 ross.csv 都没有表头和标签列这就是一个纯特征矩阵。比对时你需要一个人名的列表来对应每一行否则只知道距离最小却不知道是谁。建议把名单维护在一个单独的 txt 文件里或者像这个项目一样按单人文件命名在识别时用文件名映射标签。这种“一个文件代表一个人”的做法简单直观也更不容易把不同人的特征混在一起。5. 实时识别摄像头视频流下的跳帧、放大与阈值调优5.1 用 OpenCV 读取视频并调用 dlib 检测先说 result1.avi 这段测试视频它是用普通摄像头在室内环境中录制的画面比较稳定没有剧烈的运动模糊。我们可以直接拿它验证整套流程import cv2 import dlib import numpy as np import csv detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) facerec dlib.face_recognition_model_v1(dlib_face_recognition_resnet_model_v1.dat) known_features [] known_names [] with open(feature_all.csv) as f: reader csv.reader(f) for row in reader: known_features.append([float(v) for v in row]) # 简单映射前五行对应五个出场角色 known_names [ross, rachel, phoebe, joey, chandler] cap cv2.VideoCapture(result1.avi) while True: ret, frame cap.read() if not ret: break small cv2.resize(frame, (0, 0), fx0.5, fy0.5) gray cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) for face in faces: shape predictor(gray, face) descriptor facerec.compute_face_descriptor(small, shape, num_jitters1) dists [np.linalg.norm(np.array(descriptor) - np.array(kf)) for kf in known_features] idx int(np.argmin(dists)) if dists[idx] 0.6: x1, y1, x2, y2 face.left(), face.top(), face.right(), face.bottom() cv2.rectangle(small, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(small, known_names[idx], (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) cv2.imshow(face recognition, small) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里我先用 cv2.resize 把画面缩小到一半再进行检测。这是实时人脸识别中一个非常重要的手段dlib 的 HOG 检测对输入尺寸比较挑剔图像越大检测耗时越高而识别精度并不会因为缩小一半而大幅下降。OpenCV 的 VideoCapture 调用摄像头时本质是读取相机驱动的帧缓冲你可以通过 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) 来强制设置采集分辨率这就回答了常见的“opencv调用相机原理是什么”的问题——它只是把 V4L2 或 DirectShow 的帧流转成了 numpy 数组。5.2 提速三板斧降采样、跳帧和关键点缓存如果视频分辨率是 1080pHOG 检测器每帧可能耗时 100ms 以上实时性会很差。常见做法除了上面提到的整体降采样还有跳帧处理第 N 帧做完整检测和特征提取第 N1 帧到第 N3 帧直接用 dlib 的 correlation_tracker 跟踪上一帧的结果只在跟踪丢失时重新检测。下面给一个核心替换片段tracker dlib.correlation_tracker() tracking False while True: ret, frame cap.read() if not ret: break small cv2.resize(frame, (0, 0), fx0.5, fy0.5) gray cv2.cvtColor(small, cv2.COLOR_BGR2GRAY) if not tracking: faces detector(gray, 0) if len(faces) 0: tracker.start_track(small, faces[0]) tracking True else: tracker.update(small) pos tracker.get_position() face dlib.rectangle(int(pos.left()), int(pos.top()), int(pos.right()), int(pos.bottom())) # 用 face 继续后续操作跟踪的好处是显著减少重复检测的次数坏处是人脸快速转头时跟踪框可能漂移所以还需要在跟踪一定帧数后重新检测一次。这个逻辑在答辩时可以作为性能优化亮点讲讲容易拿分。5.3 调阈值与处理误识别的技巧最后说一个很实用的排错技巧运行 result1.avi 后如果发现经常把两个人混淆不要只改阈值先把每个候选人的距离值打印出来。比如面对一个未知人脸输出“ross: 0.42, rachel: 0.61, phoebe: 0.57”如果最小距离和第二小距离只差 0.03说明特征库里的这两个人本身很接近这时即便把阈值降到 0.5 也无法解决。这种问题的根源是注册时用了太多相似光线下的照片正确的做法是对每个人采集多个角度、多光照类型的样本再求平均特征。另一个技巧是把 128 维向量做 PCA 降维到 2 维并画散点图肉眼观察每个人的特征是否聚成独立的簇。如果两个簇重叠说明特征维度区分度不够这时候再去加训练样本或更换特征提取模型才有意义。本文还有配套的精品资源点击获取
返回列表