dlib 人脸 68 关键点检测与轮廓绘制实战
本文以两段完整的 Python 代码为主线,逐步拆解 dlib
shape_predictor_68模型的调用方式,并重点对比两种轮廓绘制策略:顺序连线(drawLine) 与 凸包轮廓(drawConvexHull)。读者可直接复制代码运行。
一、技术背景
人脸关键点(Landmark)检测是人脸分析的核心基础,其下游任务包括:
| 应用 | 依赖的关键点 |
|---|---|
| 人脸对齐(Face Alignment) | 眼角、鼻尖、嘴角 5 点 |
| 表情识别 | 眉毛、嘴唇区域变化量 |
| 活体检测 | 眼睛/嘴唇开合幅度 |
| 3D 人脸重建 | 全 68 点三维坐标拟合 |
| AR 特效贴图 | 实时轮廓区域定位 |
dlib 提供的 shape_predictor_68_face_landmarks 模型基于级联回归树(Cascade Regression Trees),在检测到人脸框后,迭代回归出 68 个关键点的精确坐标,推理速度快、精度高。
二、68 点分布总览

68 个关键点按照解剖区域划分如下:
| 区域 | 索引范围 | 点数 |
|---|---|---|
| 下颌轮廓 | 0 -- 16 | 17 |
| 左眉 | 17 -- 21 | 5 |
| 右眉 | 22 -- 26 | 5 |
| 鼻梁 | 27 -- 30 | 4 |
| 鼻底 | 31 -- 35 | 5 |
| 左眼 | 36 -- 41 | 6 |
| 右眼 | 42 -- 47 | 6 |
| 外嘴唇 | 48 -- 59 | 12 |
| 内嘴唇 | 60 -- 67 | 8 |
点 0 在左侧下颌角,点 16 在右侧下颌角,点 27 在鼻根,索引沿顺时针方向递增。
三、完整检测流程

整个流程分为六步,核心依赖两个 dlib 对象:
get_frontal_face_detector()--- 基于 HOG + 线性分类器的人脸检测器,返回人脸框列表shape_predictor(.dat)--- 级联回归树关键点预测器,输入人脸框,输出 68 个(x, y)坐标
四、代码一:关键点检测与标注(3关键点检测.py)
4.1 初始化检测器与预测器
python
import numpy as np
import cv2
import dlib
# HOG人脸检测器(内置,无需加载文件)
detector = dlib.get_frontal_face_detector()
# 68点关键点预测器(需要下载 .dat 文件)
# 下载地址:https://github.com/davisking/dlib-models
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
.dat文件说明:模型文件约 99MB,使用级联回归树存储。首次使用需从 GitHub dlib-models 仓库下载,放于脚本同级目录即可。
4.2 检测人脸并预测关键点
python
img = cv2.imread("handou2.jpg")
# 参数 0:上采样次数,0=不上采样(速度最快),1=上采样一次(检测更多小人脸)
faces = detector(img, 0)
for face in faces:
# predictor 返回 dlib.full_object_detection 对象
shape = predictor(img, face)
# 将 68 个 dlib.point 对象转为 numpy 数组,shape=(68, 2)
landmarks = np.array([[p.x, p.y] for p in shape.parts()])
关键参数解析:
| 参数 | 含义 | 推荐值 |
|---|---|---|
detector(img, n) 中的 n |
上采样次数,增大可检测远处小人脸,但速度成倍下降 | 0 或 1 |
shape.parts() |
返回 68 个 dlib.point 对象的列表 |
--- |
4.3 绘制关键点与编号
python
for idx, point in enumerate(landmarks):
pos = [point[0], point[1]]
# 绘制实心圆(绿色,半径 2)
cv2.circle(img, pos, 2, (0, 255, 0), 1)
# 在圆旁标注索引编号(红色,抗锯齿字体)
cv2.putText(img, str(idx), pos,
cv2.FONT_HERSHEY_SIMPLEX, 0.4,
(0, 0, 255), 1, cv2.LINE_AA)
cv2.imshow("img", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.LINE_AA(抗锯齿线条) 相比默认的 cv2.LINE_8,文字边缘更平滑,适合小字号渲染。
五、代码二:结构化轮廓绘制(4轮廓绘制.py)
5.1 两种绘制方法设计思路

代码定义了两个辅助函数,分别应对不同区域的轮廓特点:
5.2 drawLine:顺序连线(适合开放曲线)
python
def drawLine(start, end):
pts = shape[start:end] # 切片取对应区域的点集
for l in range(1, len(pts)):
ptA = tuple(pts[l - 1]) # 前一个点
ptB = tuple(pts[l]) # 当前点
cv2.line(image, ptA, ptB, (0, 255, 0), 2)
适用区域(开放轮廓,有明确起止点):
python
drawLine(0, 17) # 下颌轮廓(左→右弧线)
drawLine(17, 22) # 左眉(由内向外)
drawLine(22, 27) # 右眉
drawLine(27, 36) # 鼻梁 + 鼻底(上→下)
注意 :
shape[start:end]是 Python 切片,不含end位置的点 。因此drawLine(0, 17)实际绘制 0~16 共 17 个点之间的 16 段线。
5.3 drawConvexHull:凸包轮廓(适合封闭区域)
python
def drawConvexHull(start, end):
Facial = shape[start:end + 1] # 含 end(+1 补偿切片)
mouthHull = cv2.convexHull(Facial) # 计算凸包
cv2.drawContours(image, [mouthHull], -1, (0, 255, 0), 2)
适用区域(封闭轮廓,需包裹整体):
python
drawConvexHull(36, 41) # 左眼(6点)
drawConvexHull(42, 47) # 右眼(6点)
drawConvexHull(48, 59) # 外嘴唇(12点)
drawConvexHull(60, 67) # 内嘴唇(8点)
cv2.convexHull 原理:给定一组点,计算能包裹所有点的最小凸多边形(Graham Scan 算法,时间复杂度 O(n log n))。眼眶、嘴唇等区域轮廓近似凸形,因此效果优良。
drawLinevsdrawConvexHull关键区别:
drawLine(start, end)中切片shape[start:end],不含enddrawConvexHull(start, end)中切片shape[start:end+1],含end(+1 补偿)
5.4 索引速查表

六、两段代码输出效果对比

| 对比项 | 3关键点检测.py | 4轮廓绘制.py |
|---|---|---|
| 输出内容 | 68 个绿色圆点 + 红色编号 | 连线 + 凸包轮廓(无编号) |
| 视觉效果 | 调试友好,点位清晰 | 结构干净,贴近人脸轮廓 |
| 适合场景 | 标注验证、模型调试 | 特效贴图、视觉展示 |
| 信息密度 | 高(每点均有索引) | 低(只保留结构线) |
七、完整代码整合
7.1 代码一:关键点检测
python
import numpy as np
import cv2
import dlib
img = cv2.imread("handou2.jpg")
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
faces = detector(img, 0)
for face in faces:
shape = predictor(img, face)
landmarks = np.array([[p.x, p.y] for p in shape.parts()])
for idx, point in enumerate(landmarks):
pos = [point[0], point[1]]
cv2.circle(img, pos, 2, (0, 255, 0), 1)
cv2.putText(img, str(idx), pos,
cv2.FONT_HERSHEY_SIMPLEX, 0.4,
(0, 0, 255), 1, cv2.LINE_AA)
cv2.imshow("img", img)
cv2.waitKey(0)
cv2.destroyAllWindows()
7.2 代码二:结构化轮廓绘制
python
import numpy as np
import dlib
import cv2
image = cv2.imread("handou2.jpg")
detector = dlib.get_frontal_face_detector()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")
def drawLine(start, end):
pts = shape[start:end]
for l in range(1, len(pts)):
cv2.line(image, tuple(pts[l-1]), tuple(pts[l]), (0, 255, 0), 2)
def drawConvexHull(start, end):
Facial = shape[start:end+1]
hull = cv2.convexHull(Facial)
cv2.drawContours(image, [hull], -1, (0, 255, 0), 2)
faces = detector(image, 1)
for face in faces:
shape = predictor(image, face)
shape = np.array([[p.x, p.y] for p in shape.parts()])
# 凸包封闭区域
drawConvexHull(36, 41) # 左眼
drawConvexHull(42, 47) # 右眼
drawConvexHull(48, 59) # 外嘴唇
drawConvexHull(60, 67) # 内嘴唇
# 顺序连线开放区域
drawLine(0, 17) # 下颌
drawLine(17, 22) # 左眉
drawLine(22, 27) # 右眉
drawLine(27, 36) # 鼻子
cv2.imshow("image", image)
cv2.waitKey(0)
cv2.destroyAllWindows()
八、常见问题与注意事项
Q1:运行报错 RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat
模型文件缺失,从以下地址下载(bz2 压缩包,解压后约 99MB):
https://github.com/davisking/dlib-models/raw/master/shape_predictor_68_face_landmarks.dat.bz2
将 .dat 文件放到 Python 脚本同级目录。
Q2:detector(img, 0) 和 detector(img, 1) 有什么区别?
| 参数 | 效果 | 速度影响 |
|---|---|---|
n=0 |
原始分辨率检测,速度最快 | 基准 |
n=1 |
图像放大 2x 后检测,可找到更小/更远的人脸 | 约慢 4x |
n=2 |
放大 4x,极小人脸也可检测 | 约慢 16x |
代码一用 n=0(静态图,追求速度);代码二用 n=1(精度优先,确保关键点准确)。
Q3:drawConvexHull 中为什么要写 end+1?
Python 切片 a[start:end] 不含 end,而 drawLine 利用了这一点(终止点不需要绘制)。但 drawConvexHull 需要完整的闭合区域点集(如左眼的 6 个点全部参与凸包计算),所以要写 end+1。
九、扩展方向
| 方向 | 说明 |
|---|---|
| 眨眼检测(EAR) | 计算眼睛纵横比,EAR < 阈值时判断为闭眼 |
| 打哈欠检测(MAR) | 计算嘴部开合比,MAR > 阈值判断张嘴 |
| 驾驶疲劳监测 | 结合眨眼频率 + 打哈欠触发告警 |
| 人脸对齐 | 用眼角坐标做仿射变换,将人脸旋转至水平 |
| MediaPipe 替代 | 谷歌 MediaPipe 提供 468 点检测,精度更高,支持实时 |
小结
本文完整介绍了 dlib shape_predictor_68 模型的调用链路,核心要点如下:
- 两步检测 :先用
get_frontal_face_detector()找人脸框,再用shape_predictor精细化到 68 个点 - 坐标转换 :
np.array([[p.x, p.y] for p in shape.parts()])是固定范式,转为 numpy 数组便于后续处理 - 开放曲线用
drawLine(下颌、眉毛、鼻子),封闭区域用drawConvexHull(眼睛、嘴唇) - 切片陷阱 :
drawLine用[start:end],drawConvexHull用[start:end+1],务必区分
掌握 68 点检测后,即可在此基础上快速实现疲劳检测、表情分析、AR 特效等更复杂的人脸应用。
ector()找人脸框,再用shape_predictor 精细化到 68 个点 2. **坐标转换**:np.array([[p.x, p.y] for p in shape.parts()])是固定范式,转为 numpy 数组便于后续处理 3. **开放曲线用drawLine**(下颌、眉毛、鼻子),**封闭区域用 drawConvexHull**(眼睛、嘴唇) 4. **切片陷阱**:drawLine用[start:end],drawConvexHull用[start:end+1]`,务必区分
掌握 68 点检测后,即可在此基础上快速实现疲劳检测、表情分析、AR 特效等更复杂的人脸应用。