ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

dlib 68点人脸特征点检测实战:从图片到实时视频

dlib 68点人脸特征点检测实战:从图片到实时视频 简介面向人脸检测与计算机视觉进阶开发者这份资源包聚焦使用 dlib、OpenCV 和 Python 进行面部五官关键点检测覆盖眼睛、鼻子、嘴唇、下巴等部位的精准定位适合在面部表情分析、妆容特效、姿态估计等场景中拓展应用。包内共 4 个文件包括一个 Python 检测脚本、一个预训练的 68 点人脸关键点模型文件、一份 PDF 原理说明文档以及一张示例图片整体压缩包约 70.27MB。脚本演示了从读取图像、人脸检测到关键点标定的完整流程模型基于 68 点标准标注可直接预测面部特征点位置PDF 文档对 dlib 的人脸检测器与关键点回归原理进行了讲解示例图则便于读者对比校验程序输出。这套资料省去了自行训练模型的成本拿到后即可运行体验非常适合希望从基础人脸检测跨入关键点定位的开发者参考练习。目前已有 1633 人学习下载。1. 人脸检测进阶在进阶什么从「框住一张脸」到「知道五官在哪」「人脸检测」这个短语在不同人手里含义完全不同。用 OpenCV 的 Haar Cascade 能框出一张脸但它永远只给你一个矩形而标题里加上了「进阶」二字意味着要做到的是眼睛、鼻子、嘴唇、下巴各自的定位——这件事靠 dlib 的 68 点人脸特征点模型配合 OpenCV 的绘图与视频接口用 Python 串起来是目前从业者最常用的离线方案。不需要 GPU一台 CPU 机器就能跑模型是本地推理不依赖任何云端接口代码量控制在几百行内。本篇照着 dlib 前置环境、点序约定、单张图片验证、实时视频改造的主线讲最后给出安装编译期和运行期的踩坑记录。适合刚做完人脸框检测、想往面部特征分析走一步的开发者。2. 为什么用 dlib 点预测器68 点模型与 OpenCV 的明确分工2.1 68 点坐标长什么样iBUG 300-W 的点位约定Dlib 的shape_predictor_68_face_landmarks.dat是行业里被验证过无数次的模型。它做的是「在一个人脸框内回归出 68 个关键点」这套点位编号遵循 iBUG 300-W 数据集的标注规范。拿到手后点序是固定的0 到 67 每个索引代表一个解剖位置这一点是后续所有代码的地基。很多 OpenCV 图像处理项目里新人拿到模型第一反应是「先跑个 demo」结果画出来的点乱成一团多半是点序映射表写错了或者把 68 点模型当成了 5 点模型。先记住一张表区域点号范围点数典型用途下颌轮廓0-1617下巴尖在 8 号点脸型判断左眉17-215眉部高度、惊讶表情右眉22-265与左眉对称校验鼻梁27-304鼻尖在 30 号点鼻底31-355鼻孔位置、鼻翼宽度左眼36-416眨眼检测核心右眼42-476与左眼配合算纵横比外唇48-5912嘴唇轮廓、说话状态内唇60-678嘴部开合度计算这套约定里最常用的是 36-41 和 42-47 这 12 个点它们围住左右眼48-67 这 20 个点围住嘴唇外唇和内唇分开为的是后面算嘴部开合比。dlib 的接口设计也简单直接detector负责给矩形框predictor负责在框内出点一个shape对象里通过part(i).x和part(i).y取坐标。2.2 环境装配这样装 dlib 和 OpenCV 最不容易翻车环境问题占了这类项目排障的一半以上。先给最小安装命令再给一条验证命令。常见做法是用 pip 直接装预编译包省去手动编译# 建议 Python 3.8 ~ 3.11 pip install opencv-python pip install dlib pip install imutilspython -c import cv2, dlib; print(cv2.__version__, dlib.__version__)上面代码块的逻辑是按依赖层级安装opencv-python提供图像读写和绘图接口dlib提供检测器和特征点模型imutils是可选辅助库里面封装了 68 点点序的预定义字典省得自己背索引。验证命令同时导入两个库并打印版本如果No module named cv2或者No module named dlib说明装到了别的 Python 环境里。关于dlib安装失败的备选方案按可靠性排序优先尝试pip install dlib如果卡在 CMake 编译阶段说明你的 Python 版本太新或缺少编译工具链改用conda install -c conda-forge dlibconda 会拉预编译好的二进制再不行就换 Python 3.9 的干净虚拟环境重试。注意一个反直觉的点在 Anaconda Prompt 里pip install opencv后用 VS Code 跑脚本仍然报找不到模块这通常是 VS Code 里选了解释器但没选 Anaconda 的那个环境跟 OpenCV 本身无关。模型文件shape_predictor_68_face_landmarks.dat需要单独下载拿到后和脚本放同一目录或者用绝对路径。我一般会在代码开头定义一个常量方便换模型from pathlib import Path PREDICTOR_PATH Path(shape_predictor_68_face_landmarks.dat) if not PREDICTOR_PATH.exists(): raise FileNotFoundError(模型文件缺失请确认路径{}.format(PREDICTOR_PATH)) import dlib import cv2 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(str(PREDICTOR_PATH))这里用pathlib.Path是为了规避 Windows 路径反斜杠转义问题这是常见误用点。get_frontal_face_detector()创建的是 HOG SVM 检测器CPU 上单帧 640×480 大约 10-30 毫秒实际速度取决于机器和上采样参数。shape_predictor读取 68 点模型文件文件约一百多兆首次加载会有 1 到 3 秒延迟属于正常现象。3. 跑通第一版检测单张图片里画出眼睛、鼻子、嘴唇和下巴3.1 最小完整脚本先拿一张正面照验证模型是否工作拿到 zip 资源包我的习惯是先不急着读里面所有文件用一张网上随便找的正面照把最小链路跑通读图 → 检测人脸框 → 预测 68 点 → 画点。这一步能同时验证模型文件是否损坏、点序是否正常、OpenCV 的 imread 路径是否有中文问题。import dlib import cv2 detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) img cv2.imread(face.jpg) if img is None: raise FileNotFoundError(图片读取失败检查路径是否存在中文或空格) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # upsample 参数设为 1表示对图像做一次上采样再检测能检出更小的人脸 faces detector(gray, 1) print(检测到人脸数量, len(faces)) for face in faces: # face 是 dlib.rectangle左、上、右、下四个边界 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, 0, 255), -1) cv2.imwrite(output.jpg, img) print(结果已保存到 output.jpg)逻辑说明cv2.cvtColor把 BGR 转灰度dlib 的 HOG 检测器在灰度图上跑更快而predictor也接受灰度图作为输入。detector(gray, 1)的第二个参数是上采样次数设为 1 时内部会把图像放大一倍再检测能召回更小的脸代价是耗时增加约 3 倍如果你检测的都是摄像头近景大脸改成 0 更省时间。这里有个容易忽略的点img is None的判断必须写在 imread 之后。OpenCV 的 imread 在路径错误时不会抛异常而是返回None后续cvtColor直接报错报错信息却指向 cvtColor排错时容易走弯路。另外输出的output.jpg如果和原图在同一目录最好用imwrite的完整路径参数避免相对路径在 IDE 工作目录不同时写到意外位置。3.2 按区域上色把 68 个点还原成五官轮廓画完 68 个红点只能证明管线通了离「检测眼睛、鼻子、嘴唇和下巴」还差一步按点序把点连成区域。JAWLINE list(range(0, 17)) LEFT_EYEBROW list(range(17, 22)) RIGHT_EYEBROW list(range(22, 27)) NOSE_BRIDGE list(range(27, 31)) NOSE_BOTTOM list(range(31, 36)) LEFT_EYE list(range(36, 42)) RIGHT_EYE list(range(42, 48)) OUTER_LIP list(range(48, 60)) INNER_LIP list(range(60, 68)) REGION_COLORS { jawline: (255, 0, 0), left_eyebrow: (0, 255, 0), right_eyebrow: (0, 255, 0), nose_bridge: (0, 0, 255), nose_bottom: (0, 0, 255), left_eye: (255, 255, 0), right_eye: (255, 255, 0), outer_lip: (255, 0, 255), inner_lip: (255, 0, 255), }以上代码块的逻辑是把上一节的 68 点范围映射到语义区域。JAWLINE是 0 到 16 的 17 个点注意它其实是下颌到下巴再到右下颌的完整轮廓不是只有下巴尖NOSE_BRIDGE和NOSE_BOTTOM拆开是因为鼻梁是一条线而鼻底是一圈平面。颜色用 BGR 三元组OpenCV 画图函数统一走这个顺序写成 RGB 会红蓝互换。拿到映射后配合cv2.polylines把每个区域连线画出来import cv2 def draw_regions(img, shape): for name, indices in [ (jawline, JAWLINE), (left_eyebrow, LEFT_EYEBROW), (right_eyebrow, RIGHT_EYEBROW), (nose_bridge, NOSE_BRIDGE), (nose_bottom, NOSE_BOTTOM), (left_eye, LEFT_EYE), (right_eye, RIGHT_EYE), (outer_lip, OUTER_LIP), (inner_lip, INNER_LIP), ]: pts [(shape.part(i).x, shape.part(i).y) for i in indices] pts [tuple(p) for p in pts] color REGION_COLORS[name] # isClosedTrue 让区域首尾相连比如嘴唇闭合轮廓 cv2.polylines(img, [pts], isClosedTrue, colorcolor, thickness2) # 调用示例 for face in faces: shape predictor(gray, face) draw_regions(img, shape)参数说明cv2.polylines的第二个参数必须是「点的列表的列表」这里用[pts]包了一层isClosed设为True会把首尾点连起来对眼、唇这类闭合轮廓是必须的对眉毛和鼻梁则会把不该连的连上——所以我这个版本统一闭合实际观察时眉毛会有条斜线这不是 bug是闭合开关没做区分的表现。如果你在意按区域分别指定isClosed即可眼眶、嘴唇用True眉毛、鼻梁用False。如果不想手工维护这套索引imutils.face_utils.FACIAL_LANDMARKS_68_IDXS里已经内置了同样的映射返回字典格式key是区域名value是(start, end)元组。两种方式等价选一种就行。4. 从图片到实时视频EAR 与嘴部开合计算4.1 把坐标变成数值眼睛纵横比 EAR 与嘴部开合比 MAR单张图片能画点下一件值得做的事是把坐标量化成可判断的指标典型场景是实时摄制视频的人脸检测与标注里的疲劳检测、眨眼计数和说话状态判断。直接用「眼睛上眼皮到下眼皮的像素距离」是错的——人脸离摄像头越近这个值越大同一个睁眼状态会得到完全不同的数值。业界通用做法是用纵横比它无量纲对人脸缩放有天然不变性。EAR 的公式是垂直方向两组距离的平均值除以水平方向距离。左眼取点 36-41右眼取点 42-47其中 36 和 39 是左右眼角37、38 是上眼皮40、41 是下眼皮。import math def eye_aspect_ratio(eye_points): # eye_points: 包含 6 个坐标的列表 vertical_a math.dist(eye_points[1], eye_points[5]) vertical_b math.dist(eye_points[2], eye_points[4]) horizontal math.dist(eye_points[0], eye_points[3]) return (vertical_a vertical_b) / (2.0 * horizontal) def mouth_aspect_ratio(inner_lip_points): # 内唇取 60-67 共 8 个点用 61-67 和 63-65 两组垂直距离 vertical_a math.dist(inner_lip_points[1], inner_lip_points[7]) vertical_b math.dist(inner_lip_points[3], inner_lip_points[5]) horizontal math.dist(inner_lip_points[0], inner_lip_points[4]) return (vertical_a vertical_b) / (2.0 * horizontal)逻辑说明math.dist是 Python 3.8 起的标准库函数直接算二维欧氏距离比手写sqrt((x1-x2)**2 ...)更简洁。EAR 的分子取上眼皮两个点和下眼皮两个点交叉配对的距离均值分母取眼角间距正常情况下睁眼约 0.25 到 0.35闭眼会掉到 0.2 以下MAR 的阈值经验值在 0.4 到 0.5 之间高于这个值判为张嘴。这两个阈值会因人脸形状略微浮动建议在正式使用前采集自己 50 帧数据标定一次。4.2 VideoCapture 双循环与跳帧让 CPU 跑得动实时视频摄像头实时检测和单张图片最大的区别是帧率。dlib 的 68 点预测器单帧约 20 到 50 毫秒加上检测器、画图、EAR 计算在普通笔记本 CPU 上勉强能到 15 到 20 FPS。如果每帧都做一次完整检测会明显感觉到画面卡顿而且摄像头采集本身就有不稳定因素最常见的是read()返回空帧导致的拉流中断。import cv2 import dlib detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) cap cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError(摄像头无法打开检查设备占用情况) frame_skip 2 # 每 3 帧做一次完整检测中间帧复用上一次的结果 frame_count 0 last_shape None while True: ret, frame cap.read() if not ret: # 摄像头拉流中断时常见的表现是黑屏或卡死这里做兜底 print(读取帧失败尝试重新初始化摄像头) cap.release() cap cv2.VideoCapture(0) continue frame cv2.resize(frame, (640, 480)) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) if frame_count % (frame_skip 1) 0: faces detector(gray, 0) if len(faces) 0: last_shape predictor(gray, faces[0]) if last_shape is not None: pts [(last_shape.part(i).x, last_shape.part(i).y) for i in range(68)] # 左眼 36-41右眼 42-47内唇 60-67 left_eye pts[36:42] right_eye pts[42:48] inner_lip pts[60:68] ear (eye_aspect_ratio(left_eye) eye_aspect_ratio(right_eye)) / 2.0 mar mouth_aspect_ratio(inner_lip) cv2.putText(frame, EAR: {:.2f} MAR: {:.2f}.format(ear, mar), (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) frame_count 1 cv2.imshow(face landmarks, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()逻辑说明外层循环只做三件事——读帧、复制灰度图、显示结果。真正耗时的detector和predictor只在frame_count % 3 0时执行且只取faces[0]也就是画面里的第一张脸。中间两帧继续用last_shape的旧坐标画线和算 EAR视觉上几乎无感。frame_skip参数是性能和流畅度的调节旋钮设为 2 表示每 3 帧算一次适合 30 FPS 摄像头跑出约 10 FPS 的检测效果机器性能差就改成 3 或 4但超过 4 时人脸快速移动会有明显拖影。ret的判断是另一个关键点。摄像头被其他程序占用或 USB 断开时cap.read()会返回(False, None)不处理就直奔cvtColor必然崩。我的处理方式是把 release 和重新 open 放在一起相当于给摄像头一次「后悔药」。cv2.waitKey(1)的 1 毫秒延迟不能省否则窗口会无法响应键盘事件这是 OpenCV 高版本 GUI 的一个反直觉限制。5. 避坑手册安装、导入、卡顿、点乱跳的常见问题5.1 编译安装期的坑dlib 卡在 CMake 那一层现象执行pip install dlib后终端进入长久的编译输出最后报错CMake Error或Failed building wheel for dlib。原因pip 找不到与你 Python 版本匹配的预编译 wheel于是回退到源码编译此时需要系统里有完整的 C 工具链。Windows 上常见是缺 Visual Studio Build Tools 的 C 桌面开发组件Linux 上常见是缺libx11-dev和libgtk-3-dev。解决优先换 Python 版本。Python 3.9 和 3.10 的 dlib wheel 覆盖率最高实测踩坑最少如果项目锁定了 Python 3.12 或 3.13直接conda install -c conda-forge dlib走 conda 的预编译通道比折腾源码编译快得多。Linux 下也可以先sudo apt install libx11-dev libgtk-3-dev再重试 pip 安装。5.2 运行时导入的坑No module named cv2 与环境错位现象在 Anaconda Prompt 里pip list能看到 opencv但 VS Code 或 PyCharm 里运行脚本报ModuleNotFoundError: No module named cv2。原因IDE 里选的 Python 解释器和装包时用的解释器不是同一个。Anaconda 默认环境、conda 虚拟环境、系统 Python这三个各有一份 site-packages互相不共享。解决先python -c import sys; print(sys.executable)确认当前解释器路径然后在 IDE 的解释器设置里手动指定这个路径。如果用的是 PyCharm右下角解释器选择器直接搜路径VS Code 则用Python: Select Interpreter命令不要靠自动识别。还有一个隐蔽版本pip install opencv装的是旧版opencv包而代码import cv2需要opencv-python两个包名不同site-packages 里一个提供cv2一个不提供装错名字也会报同样的错。5.3 性能与稳定性的坑跳帧参数与画面撕裂现象实时检测画面明显卡顿帧率个位数CPU 占用率打满或者画面出现横向撕裂、人物有「残影」。原因每帧都跑detector(gray, 1)是最常见的性能杀手。上采样系数为 1 时HOG 检测在 640×480 下的耗时能到 80 到 120 毫秒直接把帧率拖到 8 FPS 以下。撕裂感则是跳帧策略下显示线程和检测线程工作节奏不一致。解决检测循环里把上采样系数改为 0只在人脸较小或距离远时调到 1结合第 4 章的frame_skip策略检测频率降到 1/3视觉帧率却几乎不变。如果还是不够把cap.read()读进来的帧先resize到 480 宽再送检测这一步能砍掉一半以上的检测耗时。最后用cv2.imshow显示时保证每帧都有输出即使检测结果复用上一帧画面仍然流畅。5.4 准确度的坑侧脸、遮挡和点乱跳现象人脸转侧脸超过 30 度时68 个点里靠近边缘的点开始乱跳用手挡半边脸时鼻子和嘴唇的点位明显偏移戴眼镜时眼睛点偶尔跑到镜框上。原因dlib 的 68 点模型在 iBUG 300-W 训练集上以正脸和轻微偏转为主要数据。HOG 检测器本身对侧脸召回率就差预测器拿到侧脸框后容易回归出「折叠」形态的点。眼镜框有边缘高光灰度图上和眼睛边界混淆导致眼角点被吸到框线上。解决应用层要做角度限制——检测出人脸框和点之后用 左眼到右眼的连线 与水平线的夹角判断头部偏转超过 45 度就认为该帧结果不可信直接复用上一帧有效数据戴眼镜场景下对 EAR 结果做滑动平均滤波窗口取 5 帧滤掉单帧抖动。记住一个边界认知68 点模型处理正脸和轻微转头是可靠的大角度侧脸应该换用 dlib 的 CNN 检测模型或引入头部姿态估计而不是试图在 68 点上硬调参数。6. 验证这套管线值不值得用的三个实用技巧6.1 与 solvePnP 联动把五官坐标变成头部姿态估计当 68 点检测稳定跑通后值得投入的方向是把这些 2D 坐标和 3D 人脸模型点做对应用 OpenCV 的cv2.solvePnP算出头部在三维空间里的旋转角度。操作上定义一组 3D 参考点鼻尖、下巴、左眼角、右眼角、嘴角配合 68 点里对应索引的 2D 坐标加上相机内参矩阵一次求解就能得到旋转向量和平移向量再转成欧拉角就是 roll、pitch、yaw 三个方向的人头姿态。这个能力在驾驶疲劳检测、视线估计、虚拟形象驱动里都直接可用。验证这套 68 点管线是否可靠我一般按三个步骤做回归测试而不是只看 demo 图验证项操作通过标准模型文件完整性用同一张图连续跑两次对比点坐标输出两次结果完全一致点位语义正确手动画出 30 号点确认落在鼻尖与肉眼判断一致指标随动性对着摄像头转头、眨眼观察 EAR 和姿态角EAR 随闭眼下探角度随转头连续变化第一个验证防的是模型文件加载了但内部数据损坏这种情况极少数但一旦发生点会整体偏移一个规律向量肉眼很难发现。第二个验证其实是给下游算法看的——你输出的shape.part(30)到底是不是鼻尖决定了解 solvePnP 时 3D-2D 点对是否配套。第三个验证是最终的实机测试EAR 曲线在眨眼时应该有一个明显的谷值如果这个谷值没出现多半是点序索引拿错了。我的习惯是每换一台新机器或新模型文件都先做一遍这套三连验证再进业务代码。68 点检测这条路最怕的不是模型不准而是数据标注约定没对上导致所有下游计算在一个错误的坐标系上白跑。先花十分钟确认基础正确性再往 solvePnP、疲劳识别这些方向扩展投入产出比最高。希望帮到你。本文还有配套的精品资源点击获取
返回列表