OpenCV + dlib 人脸关键点检测学习笔记(68点)

本文是我学习人脸关键点检测过程中整理的笔记,包含原理、代码详解、常见问题及改进方案。适合 OpenCV 初学者参考。


一、什么是人脸关键点检测?

人脸关键点检测(Facial Landmark Detection)是在检测到的人脸区域内,定位出面部关键部位(眼睛、鼻子、嘴巴、眉毛、下巴轮廓等)的坐标。最常用的是 68 点模型,其索引分布如下:

索引范围 部位 点数
0 -- 16 下巴轮廓 17
17 -- 21 左眉毛 5
22 -- 26 右眉毛 5
27 -- 35 鼻梁 + 鼻尖 9
36 -- 41 右眼 6
42 -- 47 左眼 6
48 -- 59 嘴巴外轮廓 12
60 -- 67 嘴巴内轮廓 8

其效果图如下:


二、dlib 库的两个核心组件

1. 人脸检测器 dlib.get_frontal_face_detector()

  • 基于 HOG(方向梯度直方图)+ 线性分类器

  • 返回人脸矩形框列表(dlib.rectangles

  • 用法:detector = dlib.get_frontal_face_detector()

  • 检测:faces = detector(img, 1) 其中 1 表示上采样次数(提高小脸检测率)

2. 关键点预测器 dlib.shape_predictor()

  • 需要加载预训练模型文件 shape_predictor_68_face_landmarks.dat

  • 输入:图像 + 人脸矩形框

  • 输出:dlib.full_object_detection 对象,包含 68 个关键点

  • 用法:predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")

  • 预测:shape = predictor(img, face)


三、完整代码示例及详解(一):绘制所有68个关键点并编号

3.1 先看完整代码

复制代码
import numpy as np
import cv2
import dlib
import os

# 1. 读取图像
img = cv2.imread("cxk.jpg")
if img is None:
    print("图像加载失败,请检查路径")
    exit()
img = cv2.resize(img, None, fx=0.3, fy=0.3)   # 缩小到原来的30%

# 2. 初始化人脸检测器
detector = dlib.get_frontal_face_detector()

# 3. 检查模型文件是否存在
model_path = "shape_predictor_68_face_landmarks.dat"
if not os.path.exists(model_path):
    print("请下载模型文件 shape_predictor_68_face_landmarks.dat")
    exit()
predictor = dlib.shape_predictor(model_path)

# 4. 检测人脸(上采样1次,提高小脸检测率)
faces = detector(img, 1)
if len(faces) == 0:
    print("未检测到人脸")
    exit()

# 5. 对每个人脸预测关键点并绘制
for face in faces:
    shape = predictor(img, face)                     # 预测68个点
    landmarks = np.array([[p.x, p.y] for p in shape.parts()])  # 转换为numpy数组

    for idx, point in enumerate(landmarks):
        pos = (int(point[0]), int(point[1]))
        cv2.circle(img, pos, radius=2, color=(0, 255, 0), thickness=-1)   # 画绿点
        cv2.putText(img, str(idx), pos, cv2.FONT_HERSHEY_SIMPLEX,
                    0.4, (255, 255, 255), 1, cv2.LINE_AA)                # 写编号

# 6. 显示结果
cv2.imshow("68 Landmarks", img)
cv2.waitKey(0)
cv2.destroyAllWindows()

运行结果展示:

3.2 代码逐块讲解

(1)faces 的本质是什么?
复制代码
faces = detector(img, 1)
  • faces 是一个 dlib.rectangles 对象,行为类似于 Python 列表 ,内部存储 0 个或多个 dlib.rectangle 对象。

  • 每个 rectangleleft(), top(), right(), bottom() 属性,代表人脸矩形框的边界。

  • 我们可以像操作列表一样获取长度和遍历:

    复制代码
    print(len(faces))          # 人脸数量
    for face in faces:
        print(face.left(), face.top(), face.right(), face.bottom())
  • 如果 len(faces) == 0,说明图像中没有检测到人脸,后续代码需要做相应处理(上面代码已做)。

(2)提取关键点坐标并转换为 NumPy 数组
复制代码
landmarks = np.array([[p.x, p.y] for p in shape.parts()])
  • shape.parts() 返回一个可迭代对象,包含所有 dlib.point 对象。

  • 列表推导式 [ [p.x, p.y] for p in shape.parts() ] 提取每个点的 xy,生成一个 Python 列表。

  • np.array() 将其转换为形状 (68, 2) 的 NumPy 数组。这样做的好处是:方便后续使用数组切片、数学运算等

(3)绘制关键点:cv2.circle()
复制代码
cv2.circle(img, pos, radius=2, color=(0, 255, 0), thickness=-1)
  • 参数含义:

    • img:要绘制的图像

    • pos:圆心坐标 (x, y)

    • radius=2:半径 2 像素

    • color=(0,255,0):绿色(BGR 顺序)

    • thickness=-1:表示填充圆(实心点)

(4)绘制编号:cv2.putText()
复制代码
cv2.putText(img, str(idx), pos, cv2.FONT_HERSHEY_SIMPLEX,
            0.4, (255, 255, 255), 1, cv2.LINE_AA)
  • org:文字左下角坐标(注意不是左上角)。

  • fontFace :字体类型。cv2.FONT_HERSHEY_SIMPLEX 是 OpenCV 内置的一种简单无衬线字体(类似 Arial)。

  • fontScale=0.4:字体缩放因子,控制文字大小。

  • color=(255,255,255):白色。

  • thickness=1:文字线条粗细。

  • lineType=cv2.LINE_AA:抗锯齿,让文字边缘平滑。

小提示:OpenCV 的 putText 不支持中文,如需绘制中文需要使用 PIL 等库。


四、完整代码示例及详解(二):分区绘制人脸轮廓(连线 + 凸包)

4.1 先看完整代码

复制代码
import numpy as np
import dlib
import cv2
import os

def drawLine(start, end, shape, image):
    """将指定区间的关键点用线段连接(不闭合)"""
    pts = shape[start:end]               # 提取点集(左闭右开)
    for l in range(1, len(pts)):
        ptA = tuple(pts[l-1].astype(int))
        ptB = tuple(pts[l].astype(int))
        cv2.line(image, ptA, ptB, (0, 255, 0), 2)

def drawConvexHull(start, end, shape, image):
    """将指定区间的关键点生成凸包并绘制轮廓(闭合)"""
    Facial = shape[start:end+1]          # 注意:包含 end 索引
    hull = cv2.convexHull(Facial)        # 计算凸包
    cv2.drawContours(image, [hull], -1, (0, 255, 0), 2)

# 主程序
image = cv2.imread("cxk.jpg")
if image is None:
    print("图像加载失败")
    exit()
image = cv2.resize(image, None, fx=0.3, fy=0.3)

detector = dlib.get_frontal_face_detector()
if not os.path.exists("shape_predictor_68_face_landmarks.dat"):
    print("请下载模型文件")
    exit()
predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat")

faces = detector(image, 1)
if len(faces) == 0:
    print("未检测到人脸")
    exit()

for face in faces:
    shape = predictor(image, face)
    shape = np.array([[p.x, p.y] for p in shape.parts()])   # 转换为numpy数组

    # 绘制凸包(眼睛、嘴巴)
    drawConvexHull(36, 41, shape, image)   # 右眼
    drawConvexHull(42, 47, shape, image)   # 左眼
    drawConvexHull(48, 59, shape, image)   # 嘴外部
    drawConvexHull(60, 67, shape, image)   # 嘴内部

    # 绘制连线(脸颊、眉毛、鼻子)
    drawLine(0, 17, shape, image)    # 下巴轮廓
    drawLine(17, 22, shape, image)   # 左眉毛
    drawLine(22, 27, shape, image)   # 右眉毛
    drawLine(27, 36, shape, image)   # 鼻子

cv2.imshow("Face Contours", image)
cv2.waitKey(0)
cv2.destroyAllWindows()

效果展示:

4.2 代码逐块讲解

(1)drawLine 函数:顺序连接相邻点(不闭合)
复制代码
def drawLine(start, end, shape, image):
    pts = shape[start:end]          # 提取从 start 到 end-1 的点
    for l in range(1, len(pts)):
        ptA = tuple(pts[l-1].astype(int))
        ptB = tuple(pts[l].astype(int))
        cv2.line(image, ptA, ptB, (0, 255, 0), 2)
  • 切片规则shape[start:end] 遵循 Python 左闭右开规则,即包含索引 start,不包含 end

  • 循环从第 2 个点开始,将当前点和前一个点连接,形成折线。

  • 不闭合:最后一个点不会与第一个点连接(适合下巴轮廓、眉毛等开放曲线)。

  • 适用场景 :脸颊轮廓(0~16)、眉毛(17~26)、鼻子(27~35)等凹形或开放区域。

(2)drawConvexHull 函数:计算凸包并绘制(闭合)
复制代码
def drawConvexHull(start, end, shape, image):
    Facial = shape[start:end+1]          # 包含 end 索引
    hull = cv2.convexHull(Facial)
    cv2.drawContours(image, [hull], -1, (0, 255, 0), 2)
  • 切片规则shape[start:end+1] 使得包含 end 索引的点(因为右边界要 +1)。例如右眼索引 36~41,调用 drawConvexHull(36,41, ...) 会取出索引 36,37,38,39,40,41。

  • 凸包概念

    想象一组钉子在平面上,用一根橡皮筋从外部绷紧,橡皮筋形成的多边形就是凸包

    • 凸包是包含所有点的最小凸多边形

    • 它没有凹陷,顶点一定是原始点集中的点。

  • cv2.convexHull(Facial):输入点集(N×2 数组),返回凸包顶点(形状 (M,1,2))。

  • cv2.drawContours(image, [hull], -1, color, thickness):绘制凸包轮廓,-1 表示绘制所有轮廓。

  • 适用场景 :眼睛(36~47)、嘴巴外轮廓(48~59)等凸形区域,自动闭合且保证凸性。

(3)为什么有些部位用凸包,有些用连线?
部位 使用方式 原因
眼睛、嘴巴外轮廓 凸包 这些区域的关键点排列近似凸多边形,凸包能快速得到平滑闭合轮廓
嘴巴内轮廓 凸包(或连线) 内轮廓实际上是凹形(上下嘴唇之间有空隙),凸包会"封死"缝隙,有时效果不佳,可改为连线
脸颊、眉毛、鼻子 连线(不闭合) 这些部位是开放曲线或凹形,使用凸包会丢失细节(如下巴尖会被填平)
(4)凸包的直观理解与验证

可以运行以下代码观察凸包的效果:

复制代码
import cv2
import numpy as np

img = np.zeros((300,300,3), dtype=np.uint8)
points = np.array([[150,30], [180,110], [270,110], [200,170],
                   [220,250], [150,200], [80,250], [100,170],
                   [30,110], [120,110]], dtype=np.int32)
hull = cv2.convexHull(points)
cv2.drawContours(img, [hull], -1, (0,255,0), 2)
for p in points:
    cv2.circle(img, tuple(p), 3, (0,0,255), -1)
cv2.imshow('Convex Hull', img)
cv2.waitKey(0)

你会看到凸包将五角星的"凹陷"填平,形成一个大的凸多边形。


五、常见问题与解决办法

Q1:ImportError: No module named 'dlib'

  • 安装:pip install dlib

    Windows 用户建议使用 conda install -c conda-forge dlib 或下载预编译 whl。

Q2:RuntimeError: Unable to open shape_predictor_68_face_landmarks.dat

Q3:检测不到人脸

  • 调整 detector(img, 1) 中的上采样次数(1 或 2)

  • 检查图像是否太暗或人脸太小

  • 尝试先对图像做直方图均衡化 cv2.equalizeHist()

Q4:画出的点或线位置不对

  • 确认缩放操作在检测之前进行

  • 绘制时坐标直接使用 shape 中的值,无需反向缩放


六、总结与扩展

通过本次学习,我掌握了:

✅ 使用 dlib 进行人脸检测和 68 点关键点定位

✅ 提取关键点坐标并转换为 NumPy 数组

✅ 用 OpenCV 绘制点、文字、线段和凸包

✅ 理解凸包的概念及其适用场景

✅ 编写健壮的代码(增加错误处理、模型检查)

🔭 下一步可以尝试:

  • 实时摄像头关键点检测(cv2.VideoCapture

  • 人脸对齐(通过仿射变换将眼睛旋转到水平)

  • 使用 MediaPipe 实现更快的 468 点模型

  • 基于关键点的疲劳检测(眨眼、打哈欠)


笔记到此结束。希望对你也有帮助!如果有任何问题,欢迎评论区交流。

本文为原创学习笔记,转载时请注明出处。

相关推荐
SCBAiotAigc3 小时前
2026.4.13:vim编程简单配置
人工智能·ubuntu·vim·具身智能
飞哥数智坊3 小时前
全新 SOLO 帮我做 PPT,半小时出稿,效果直接惊艳
人工智能·solo
飞哥数智坊3 小时前
Gemini-3.1-Pro vs Gemini-3-Flash:效果与花费的真实对比
人工智能·ai编程·gemini
IT大师兄吖3 小时前
SAM3 提示词 图片分割 ComfyUI 懒人整合包
人工智能
幻风_huanfeng3 小时前
人工智能之数学基础:内点法和外点法的区别和缺点
人工智能·算法·机器学习·内点法·外点法
luoganttcc3 小时前
一个 warp 同时 运行 32 个thread 就是 同时 运行 32 core
人工智能
AIData搭子3 小时前
溯源难题破解:搭建原始文件与向量数据之间的映射关系
人工智能