简介面向计算机视觉与深度学习学习者这套资源以dlib库为核心完整演示了从图片和视频中提取68个人脸关键点的过程关键点覆盖眼睛、眉毛、鼻子、嘴巴、脸颊等面部区域可直接用于后续分析。压缩包共6个文件包括两个可运行Python脚本——分别针对图片和视频处理一个预训练的shape_predictor_68_face_landmarks.dat模型两张样例图片以及一段avi测试视频整体大小约69.46MB文件组织清晰拿到后即可按需调用附带的样例素材让用户无需额外准备数据即可快速验证效果。目前已有346人学习下载适合需要快速上手人脸关键点检测、表情识别或人脸对齐任务的开发者参考。通过运行脚本不仅能直观看到面部68个特征点的标注效果还能学习dlib配合OpenCV完成人脸检测、关键点坐标提取与逐帧处理视频的完整技术路径借助该预训练模型和代码可进一步扩展到面部识别、虚拟化妆、3D建模等实际应用场景。1. dlib 提取 68 个人脸关键点到底解决什么问题人脸关键点提取是很多视觉项目的第一步美颜需要知道眼睛在哪疲劳监测需要实时盯着眼睛的开合人脸识别前需要把人脸对齐。dlib 提供的 68 点关键点检测是目前本地部署最常用的开源方案之一模型文件接近 100MBCPU 上也能跑实时。这篇笔记从图片和视频两个场景展开讲清楚 dlib 怎么装、模型怎么用、代码怎么写、参数怎么调以及实际踩过的坑。适合打算做本地人脸分析、又不希望引入深度学习框架的开发者读完能直接照着复现。2. 68 点模型原理与部署先搞懂 dlib 在做什么再动手2.1 68 个点都是什么五官分布的编号规则dlib 的 68 点模型输出的是一组固定顺序的坐标编号从 0 到 67。每个编号对应人脸固定的语义位置这一点非常关键因为后续所有计算都依赖这个顺序如果你想算眼睛开合度你必须知道 36 到 47 是眼睛如果你想画下颌线你必须知道 0 到 16 是轮廓而不是自己去猜。整个 68 点的分布可以分成六大块脸部轮廓、左右眉毛、鼻子、左右眼睛、嘴巴。其中轮廓占 17 个点眉毛各 5 个点鼻子 9 个点每个眼睛 6 个点嘴巴 20 个点加起来正好 68 个。鼻子的编号里30 号是鼻尖33 号是鼻根这两个点在侧脸时的稳定性其实相当不错做姿态估计时经常拿它们做参考。编号区间部位点数0 - 16脸部轮廓 / 下颌1717 - 21左眉522 - 26右眉527 - 35鼻梁与鼻尖936 - 41左眼轮廓642 - 47右眼轮廓648 - 67嘴巴含内外唇20注意一个容易绕晕的点这里的左和右是相对人脸本身的不是相对观察者。也就是说图片里人物面向镜头时画面左侧的那只眼睛其实是模型的右眼42 到 47。如果你在做人脸对齐或者镜像操作这个方向关系一定要先确认清楚否则后面的几何计算全反了。2.2 检测器与关键点模型选型为什么用 68 点而不是 5 点很多刚开始接触关键点的人会问同一个问题现在深度学习人脸关键点那么多为什么还要用 dlib 的 68 点答案是两个字够用且省心。dlib 的经典工作流由两部分组成前半段用get_frontal_face_detector()做人脸检测它基于 HOG 特征和线性分类器后半段用shape_predictor做关键点回归本质是一串梯度增强回归树。这个组合不依赖 GPU纯 CPU 单张图片也就几十毫秒对于 68 点这种规模来说性价比很高。对比 5 点模型5 点通常只够做对齐缺失了嘴巴、眉毛这些表情相关的信息对比稠密的 468 点方案虽然点位更密但往往要求更重的模型和更长的推理时间。68 点正好卡在中间点数足够支撑眨眼检测、嘴部开合、头部姿态估算这类常见需求模型体积和推理耗时又控制得住。这个中间态就是它一直被用到现在的原因。如果你需要做的是极端姿态下的人脸——比如低头 60 度或者被口罩遮住大半张脸dlib 这套方案的稳定性会明显下降。此时可以考虑换成深度学习方案但代价是引入更多依赖。我一般会建议先跑通 dlib 的 68 点流程把业务验证完再决定要不要上更重的模型避免一上来就被环境问题拖住。2.3 环境安装与模型文件准备编译 dlib 的最小可行步骤安装 dlib 是踩坑率最高的环节没有之一。pip install dlib在大部分机器上会走源码编译因为它依赖 C 编译工具链和 CMake任何一个环境变量不对都会报错。最稳妥的做法是优先尝试 pip 安装如果报错再换 conda。conda-forge 上提供预编译好的 dlib 包能免掉本机编译那一堆破事。# 方式一pip 直接安装新版本多数平台有预编译轮子 pip install dlib # 方式二如果 pip 卡在编译阶段改用 conda conda install -c conda-forge dlib装完之后建议先跑一条 import 语句确认环境没问题再做其他操作。这一步虽然简单但能把环境问题和代码问题立刻分隔开排查时少走弯路。import dlib print(dlib.__version__) # 能输出版本号说明安装成功代码就一件事验证 dlib 能不能正常导入。如果这里报错说明编译产物没对上 Python 版本或者缺少运行时依赖需要回头查编译环境而不是继续往下写代码。接下来是模型文件。68 点关键点检测需要一个预训练模型文件通常叫shape_predictor_68_face_landmarks.dat从 dlib 项目官方页面的模型下载区就能拿到。这个文件直接决定了关键点输出的质量下载完最好放在项目根目录或者单独的models目录下并在代码里把路径写明确避免因为相对路径问题导致程序跑一半报错找不到文件。3. 图片人脸关键点提取从最小脚本到逐点输出3.1 最小可运行的图片关键点提取脚本图片是我们验证流程最直接的载体。这里给出一段完整可跑的脚本输入一张正面人脸图输出带 68 个标记点的结果图。我习惯把灰度图转出来单独传给关键点预测器这是因为 dlib 的检测和预测都基于灰度图彩色信息对它没有增益反而多一次通道转换的开销。import dlib import cv2 # 加载检测器与关键点预测器 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 读取图片并转灰度 img cv2.imread(input.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 检测人脸1 表示上采样 1 次再检测 faces detector(gray, 1) print(f检测到 {len(faces)} 张人脸) # 对每张人脸提取 68 个关键点 for face in faces: shape predictor(gray, face) for i in range(68): x shape.part(i).x y shape.part(i).y cv2.circle(img, (x, y), 2, (0, 255, 0), -1) cv2.imwrite(output.jpg, img)这段代码的逻辑很直接先检测人脸框再把每个框交给关键点预测器。detector(gray, 1)第二个参数是最影响检测效果的数字1表示把输入图像在内部放大 1 倍再找人脸人脸较小时能明显提高召回率代价是耗时增加。predictor(gray, face)的第一个参数必须是灰度图第二个参数是 dlib 的 rectangle 对象也就是检测器返回的元素。shape.part(i)取出第 i 个关键点.x和.y就是该点在原图中的像素坐标。3.2 68 个点的坐标怎么用保存、编号与按部位可视化很多人只会在图上画点但实际业务里更常见的是把坐标交给下游计算距离、判断状态、传给 OpenGL 做网格。所以我建议一开始就把坐标结构化保存而不是只画个图。下面这段在 3.1 的基础上增加了分组绘制不同部位用不用颜色同时把坐标写进文本文件。import json # 定义五官对应的编号范围 parts { jaw: list(range(0, 17)), browL: list(range(17, 22)), browR: list(range(22, 27)), nose: list(range(27, 36)), eyeL: list(range(36, 42)), eyeR: list(range(42, 48)), mouth: list(range(48, 68)), } colors { jaw: (255, 0, 0), browL: (0, 255, 0), browR: (0, 255, 0), nose: (0, 128, 255), eyeL: (0, 0, 255), eyeR: (0, 0, 255), mouth: (255, 0, 255), } shape predictor(gray, faces[0]) landmarks [] for name, idx_list in parts.items(): for i in idx_list: x, y shape.part(i).x, shape.part(i).y landmarks.append({id: i, x: x, y: y, part: name}) cv2.circle(img, (x, y), 2, colors[name], -1) with open(landmarks.json, w, encodingutf-8) as f: json.dump(landmarks, f, indent2)这里唯一要理解的是parts字典的编号含义眼周 6 个点中包含内外眼角嘴角 20 个点包含上下唇边缘它们的编号是固定的你可以像查字典一样按编号取点。保存 JSON 的唯一目的是让下游模块解耦画图脚本和计算脚本可以分开维护改一处不影响另一处。3.3 参数调整上采样次数与多脸场景的取舍detector(gray, N)里的 N 是图片阶段最需要调的数字。N 越大检测小脸的能力越强但耗时会成倍增长。在单人自拍这种场景N 设为 0 或 1 就够了如果是合照且人脸只占画面很小比例可能需要上采样 2 次甚至 3 次。这个参数没有绝对标准建议根据你实际图片的分辨率和人脸尺寸拍脑袋定先试 1看漏检情况再往上加。上采样参数行为推荐场景0直接用原图检测人脸占比大、图清晰1图像放大 1 倍再检测常规通用速度与召回平衡2放大 2 倍再检测小脸、远距离、多人合影多脸场景还有一个容易踩的点predictor接收的必须是一个检测框如果你同时检测到 5 个人就要循环 5 次调用预测器。千万不要手写一个固定尺寸的框往里传框稍微偏移关键点就会跟着偏特别是侧脸的时候输出直接没法看。另外要注意faces的返回结果没有顺序保证如果业务需要固定人物顺序先按框的 x 坐标排序再做后续处理否则每次输出的索引指向的人可能不一样。4. 视频人脸关键点提取逐帧检测、提速与结果输出4.1 从视频帧到实时绘制的完整流程视频和图片的本质区别只有一点视频是连续帧每一帧都必须快速处理否则画面就会卡顿。直接从图片代码扩展成视频其实很简单无非是把读单张图换成循环读帧import dlib import cv2 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) # 打开摄像头如果读视频文件把 0 换成文件路径 cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 1) for face in faces: shape predictor(gray, face) for i in range(68): x shape.part(i).x y shape.part(i).y cv2.circle(frame, (x, y), 1, (0, 255, 0), -1) cv2.rectangle(frame, (face.left(), face.top()), (face.right(), face.bottom()), (255, 0, 0), 2) cv2.imshow(dlib 68 points, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码里有一个新手很容易问的点cv2.waitKey(1)的作用。它不是单纯地等待键盘输入而是会刷新 OpenCV 的窗口事件循环1表示最多阻塞 1 毫秒。你把它值调大比如 30画面帧率就会被人为限制到 30 FPS 左右在某些同步场景里可以用它来降帧率。q键退出是常规做法注意 0xFF是为了在 64 位系统上正确读取按键的低 8 位千万不能省。4.2 视频提速检测器加追踪器的组合模式逐帧全量检测是最直接的写法但也是最慢的写法。dlib 的 HOG 检测器每帧跑一次CPU 上往往只有十几 FPS做实时交互不够用。常见的提速方案是隔帧检测 追踪器每隔 N 帧才做一次完整人脸检测中间帧用correlation_tracker跟踪上一帧的人脸位置跟踪成本远低于检测成本。tracker dlib.correlation_tracker() tracking False frame_count 0 DETECT_EVERY 5 # 每 5 帧做一次全量检测 while True: ret, frame cap.read() if not ret: break gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) frame_count 1 # 周期性检测或尚未开始追踪时首次检测 if frame_count % DETECT_EVERY 0 or not tracking: faces detector(gray, 1) if len(faces) 0: tracker.start_track(frame, faces[0]) tracking True else: tracker.update(frame) pos tracker.get_position() # 把追踪结果转成 dlib rectangle 再交给关键点预测器 face dlib.rectangle(int(pos.left()), int(pos.top()), int(pos.right()), int(pos.bottom())) shape predictor(gray, face) # 在此处绘制关键点逻辑同 4.1DETECT_EVERY是这个方案的灵魂。设成 5 意味着每 5 帧检测一次检测帧之外全部走追踪整体速度能提升 3 到 5 倍。但要注意追踪器在目标快速移动或突然转身时会漂移一旦漂移后续所有关键点都会跟着错。所以在实时项目里我一般会额外加一个兜底如果追踪返回的框面积变化超过一定比例或者关键点预测结果离上一帧太远就强制触发一次新的完整检测恢复锁定。4.3 输出视频文件编码器与帧率设置调试时在窗口里看没问题但交付演示时通常要把结果存成视频文件。OpenCV 的VideoWriter需要严格匹配编码器、帧率和分辨率这三个参数任何一个和输入不匹配输出的视频都可能打不开或者音画不同步。fourcc cv2.VideoWriter_fourcc(*mp4v) fps 30.0 frame_width int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) frame_height int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) out cv2.VideoWriter(output.mp4, fourcc, fps, (frame_width, frame_height)) # 在 4.1 的循环体末尾写 out.write(frame) # 结束后释放 out.release()这里mp4v是一个兼容性很好的编码器绝大多数播放器都能打开。fps一般取输入视频的原帧率取高了文件会很大取低了视频看起来一卡一卡。要注意的是VideoWriter的宽高必须和写入的frame完全一致如果前面做过 resize记得把宽高也改成 resize 后的值否则写出来的视频是花屏或者干脆写不进内容。5. dlib 关键点提取避坑指南5 个常见问题与排查方法5.1 pip 安装 dlib 卡在编译阶段现象pip install dlib执行到一半报 C 编译错误或者卡在 Building wheel 十几分钟不动。原因本机缺少完整的 C 编译工具链或者编译环境版本不匹配源码编译失败。解决不要死磕 pip直接用 conda 安装预编译版本或者从 dlib 官方仓库下载对应系统的预编译 whl 包安装。装好之后务必回到 2.3 节用import dlib验证一遍再继续。5.2 模型文件报错 FileNotFoundError现象代码在dlib.shape_predictor(shape_predictor_68_face_landmarks.dat)这行报找不到文件。原因工作目录不对模型文件不在当前运行路径下。解决用绝对路径或者把模型文件统一放在项目根的models子目录并在代码里基于脚本所在目录拼接路径。还有个更隐蔽的情况网页下载的模型文件被安全软件拦截改名导致实际文件名多了一个后缀检查一下文件属性最直接。5.3 视频或图片中检测不到人脸现象程序正常跑但faces返回空列表一张脸都没框出来。原因光照太暗、人脸过小、角度过于倾斜。解决先提高上采样参数把detector(gray, 1)改成detector(gray, 2)如果还不行就对灰度图做直方图均衡化增强对比度。侧脸场景 HOG 检测器天生弱不要在这上面较劲优先保证正脸输入或者换深度学习检测器。5.4 视频帧率低得没法用现象摄像头预览明显卡顿CPU 占用接近 100%。原因每一帧都做全量人脸检测HOG 检测在 CPU 上是最大的耗时点。解决按 4.2 的方案改成隔帧检测 追踪器同时在进入检测前把帧缩小 50%检测完再把坐标映射回原图。实测这一套组合下来帧率提升通常在两倍以上且精度损失很小。5.5 关键点位置漂移或左右错位现象标记点在人脸上乱跳眼睛的点跑到眉毛上嘴巴的点歪到脸颊。原因输入框不准导致回归结果异常或者图像被水平翻转过导致左右眼编号反了。解决确保传给predictor的框来源可靠追踪状态下要检查追踪框是否漂移如果是自拍相机前置摄像头画面是镜像的需要根据业务统一左右眼编号的解释方式不能拿标准编号直接硬算。6. 从关键点到落地应用人脸对齐与眨眼检测的进阶技巧6.1 用 68 点做标准化的正脸对齐很多下游模型对输入人脸有对齐要求对齐后的脸识别率会明显提升。常见做法是用两只眼睛的坐标计算旋转角度再把整张脸转正并缩放到统一尺寸。这里的关键点在于选取稳定的锚点我一般用左眼 36 号和右眼 45 号这两个点在正脸时相对稳定。import math def align_face(img, shape): left_eye (shape.part(36).x, shape.part(36).y) right_eye (shape.part(45).x, shape.part(45).y) angle math.atan2(right_eye[1] - left_eye[1], right_eye[0] - left_eye[0]) * 180 / math.pi center ((left_eye[0] right_eye[0]) // 2, (left_eye[1] right_eye[1]) // 2) rot_mat cv2.getRotationMatrix2D(center, angle, 1.0) aligned cv2.warpAffine(img, rot_mat, (img.shape[1], img.shape[0])) return alignedatan2算出两眼连线的角度getRotationMatrix2D生成旋转矩阵warpAffine执行旋转。注意人眼坐标系是 y 轴向下所以角度方向要和实际倾斜方向严格对应写代码时最好拿一张歪着的自拍图亲自验证一下转向对不对这属于典型的差一个正负号就能让你怀疑人生的坑。6.2 只靠眼睛 6 个点做眨眼检测眨眼检测是疲劳驾驶场景最常见的需求公式很简单EAR 小于某个阈值就认为闭眼。它只用 6 个点稳定性比很多花哨方法都好。def eye_aspect_ratio(eye): # eye 是 6 个点的坐标列表顺序为 dlib 的编号顺序 p2_p6 math.dist(eye[1], eye[5]) p3_p5 math.dist(eye[2], eye[4]) p1_p4 math.dist(eye[0], eye[3]) return (p2_p6 p3_p5) / (2.0 * p1_p4)左右眼各算一次平均值。正常情况下 EAR 在 0.25 到 0.35 之间闭眼时掉到 0.1 以下。实际项目里不要只拿一帧判断连续 2 到 3 帧都低于阈值才触发否则一秒钟的单帧抖动就能把系统搅得乱报警报。这个阈值和摄像头距离有关建议在每个人身上做一次标定。回到关键点这件事本身我做这类项目多年最大的习惯就是先把坐标保存下来把可视化做好把分组编号搞清楚再谈算法。坐标是一切的中间层中间层稳了后面换检测器、换模型都不伤筋骨。如果你也打算在本地做人脸分析dlib 的 68 点流程值得认真跑一遍它能帮你把数据形态、坐标系、处理节奏全部理顺希望帮到你。本文还有配套的精品资源点击获取