简介:面向计算机视觉与深度学习学习者,这套资源以dlib库为核心,完整演示了从图片和视频中提取68个人脸关键点的过程,关键点覆盖眼睛、眉毛、鼻子、嘴巴、脸颊等面部区域,可直接用于后续分析。压缩包共6个文件,包括两个可运行Python脚本——分别针对图片和视频处理,一个预训练的shape_predictor_68_face_landmarks.dat模型,两张样例图片以及一段avi测试视频,整体大小约69.46MB,文件组织清晰,拿到后即可按需调用;附带的样例素材让用户无需额外准备数据即可快速验证效果。目前已有346人学习下载,适合需要快速上手人脸关键点检测、表情识别或人脸对齐任务的开发者参考。通过运行脚本,不仅能直观看到面部68个特征点的标注效果,还能学习dlib配合OpenCV完成人脸检测、关键点坐标提取与逐帧处理视频的完整技术路径;借助该预训练模型和代码,可进一步扩展到面部识别、虚拟化妆、3D建模等实际应用场景。
1. dlib 提取 68 个人脸关键点,到底解决什么问题
人脸关键点提取是很多视觉项目的第一步:美颜需要知道眼睛在哪,疲劳监测需要实时盯着眼睛的开合,人脸识别前需要把人脸对齐。dlib 提供的 68 点关键点检测,是目前本地部署最常用的开源方案之一,模型文件接近 100MB,CPU 上也能跑实时。这篇笔记从图片和视频两个场景展开,讲清楚 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 | 脸部轮廓 / 下颌 | 17 |
| 17 - 21 | 左眉 | 5 |
| 22 - 26 | 右眉 | 5 |
| 27 - 35 | 鼻梁与鼻尖 | 9 |
| 36 - 41 | 左眼轮廓 | 6 |
| 42 - 47 | 右眼轮廓 | 6 |
| 48 - 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", encoding="utf-8") as f: json.dump(landmarks, f, indent=2)这里唯一要理解的是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 点流程值得认真跑一遍,它能帮你把数据形态、坐标系、处理节奏全部理顺,希望帮到你。
本文还有配套的精品资源,点击获取