人脸关键点 468 点:把一张脸拆成能贴东西的坐标
美颜、贴纸、换脸、眨眼检测 —— 这些功能都要先知道眼角在哪、嘴角在哪。一个「人脸框」给不了这些,它只给你四个数。468 个点才是可用的形状。有意思的是:这 468 个点不是靠检测一次算出来的,而是先找脸、把脸裁下来放大、再在一张 192 见方的裁片里「钉」上去。
01目标:468 个点贴住脸,转头它跟着转
- 说清「框」和「点」的区别:框是四个数,点是 468 组坐标;以及为什么必须先检测再裁再回归
- 跑通单图与实时两条命令,看到左栏绿框
face 0.96、右栏 468 个黄点与绿色轮廓线 - 调出两种效果:
min_detection_confidence调高 → 侧脸不再画点;num_faces改 1 → 第二个人被丢掉
难点:直觉是「一个模型直接输出 468 个点」,实际是两个模型、两种输入尺寸;点是在裁片坐标系里算出来的,必须靠那次的变换矩阵反算回原图,否则点会整体错位。
一张局部人像照。也可以换 --ros 的相机画面。
双栏图:左 = 绿框 + face 0.96,右 = 468 个黄点 + 绿色轮廓线,状态栏打 Faces: 1。
02问题:光有「人脸框」做不了美颜
手机相机里那个「贴纸」功能,猫耳朵为什么总能贴在头顶、胡子为什么总在嘴唇上方?因为程序知道那两处在哪。
如果只有一个人脸框,你能做的只有「把整张图按框裁出来」。想贴猫耳朵?你不知道头顶在哪。想瘦脸?你不知道脸颊边界在哪。框只有四个数,五官需要几百个。
| 对比 | 平时你看到的(黑盒) | 机器必须做的(白盒) |
|---|---|---|
| 贴纸为什么贴得准 | 猫耳朵跟着头转,像磁铁吸住 | ? —— 得先知道「头顶中点」这一组坐标,且每帧重算 |
| 为什么不用框裁 | 看不出程序在框和点之间有什么区别 | ? —— 框里混着大量背景,直接回归点位会飘 |
| 两个人在画面里 | 两个人都有贴纸 | ? —— 要对每张脸各跑一遍「裁 + 钉点」 |
三个问号拆成三个具体问题:
① 468 个点是怎么算出来的?
② 为什么要先把脸裁下来、还放大到 192 见方?
③ 画面里有两个人,谁的点会显示、谁不会?
03现象:左栏绿框 face 0.96,右栏 468 点浮在脸上
你会看到:一张双栏图。左栏是原图 + 绿框 + 角标 face 0.96;右栏是同一张图,上面浮着 468 个黄点,绿色细线把眼睛、眉毛、嘴唇和脸廓连了起来。底部状态栏写着 FPS: 0 Faces: 1。
images/face_demo.jpg
怎么跑出来:进项目根目录,把包目录加进 PYTHONPATH,用模块方式跑单图。
cd ~/ros2_ws/src/xflz_eai_car1/vision/09_face_landmarks export PYTHONPATH=$PWD/car1_face_landmark python -m face_landmark_core.demo_image --config config/face_config.yaml --save out.jpg
🔍 看什么:三处细节,后面都要用到。
- 点是「贴」在五官上的,不是均匀铺满 —— 眼睛、嘴唇周围明显更密。模型把分辨率花在了需要精度的地方。
- 轮廓线只有几组 —— 绿线只画了脸廓、眉、眼、唇,共几段;468 个点之间的连线有上千条,全画出来会糊成一团。
- 右栏背景被压暗了 —— 只留点和线,方便看清形状。这是绘制层做的,不是模型输出。
这就是本实验最终要呈现的现象。此时照做、看懂它就行;第 05 节会补上实时相机与多人两路。
04原理:先找脸、再裁下来放大、最后在 192 见方里钉 468 个点
不是一个模型直接吐 468 个点,而是两个模型接力:128×128 的小模型先找到脸,把这块脸裁下来放大到 192×192,再让第二个模型在这张裁片里钉 468 个点。
打个比方:像找人写字 —— 先在一张合影里圈出「就是这个人」,再把那一块放大复印成一张近照,最后拿尺子在近照上量眼角到嘴角的距离。 揭个盖:量出来的尺寸是近照的尺寸,不是合影的。所以最后必须按当初放大的倍数反算回去 —— 这一步漏了,点会整体偏移,且越靠画面边缘偏得越多。
四步回答第 02 节的三问:① 468 点怎么来 → 步骤 3;② 为什么要裁 → 步骤 1、2;③ 两个人画谁 → 步骤 1 的 num_faces 与步骤 3 的 presence。
1一张大图里,脸在哪?
吃什么:整帧图,先等比缩放并补边到 128×128。做什么:跑 BlazeFace 短距检测器。吐什么:若干候选框 + 每张脸的 6 个基准点(双眼中心、鼻、嘴中心、两耳),再用 NMS 去重、按分数排序,只取前 num_faces 张(默认 2)。
这 6 个基准点非常关键:下一步要靠它们决定「怎么裁」—— 两眼连线定水平方向,眼睛与嘴的距离定大小。所以模型不只给框,还给了脸朝向的线索。
动它会怎样:min_detection_confidence(默认 0.5)是这一步的门限,调高 → 侧脸、小脸直接不进候选;num_faces 决定最多处理几张脸。
2为什么非要裁下来、还放大到 192?
这是第 02 节第二问的答案,也是很多人做不出来时的根因:如果把整张 640×480 的图直接喂给点位模型,脸只占几十个像素,模型根本「看不清」五官在哪。
所以要把脸搬进一张 192×192 的裁片:用上一步的 6 个基准点算一个仿射矩阵(roi_affine),再 warp_roi 把这块区域摆正、拉伸到 192 见方。从此脸在裁片里占满画面 —— 同一个模型,效果立刻不一样。
动它会怎样:这一步没有可调参数,但它是最容易出错的地方。裁得太松(框放大不够)→ 脸只占裁片一半,点位精度掉;裁得太紧 → 下巴或额头被切掉,边缘的点会挤在边界上。
3468 个点是怎么算出来的?
裁片进第二个模型,一次前向吐出两组东西:
- 468×3 个归一化坐标 —— 每个点是
(x, y, z),x/y是裁片内的相对位置(0~1),z是相对深度(越小越靠近镜头)。所以它其实是三维的,只是显示的时候只用前两维。 - 一个 presence 分数 —— 「这张裁片里真的有一张能标点的脸吗」。它是 logit,要先过 sigmoid 再和
min_detection_confidence比;低于门限,整张脸的点全部丢弃。
presence 就是第 02 节第三问的答案:两个人的时候,谁的点会被画,取决于谁先通过检测、谁又通过 presence,以及 num_faces 有没有轮到它。
动它会怎样:这一步同样没有旋钮(模型给定),但门限 min_detection_confidence 同时作用于「检测」和「presence」两处 —— 这一条在配置里只有一个名字,很容易误以为只影响检测。
4点算在裁片里,怎么画回原图?
用第 ② 步那个仿射矩阵的逆变换(denormalize_landmarks),把归一化坐标先还原到 192 空间、再映射回原图像素。框则直接用检测结果裁到帧内。
最后是绘制层:黄点半径 2、绿色拓扑线只连脸廓 / 眉 / 眼 / 唇这几组(FACEMESH_CONTOURS,取自 mediapipe 官方的连接表)。468 个点之间的完整连接有上千条,全画会糊成一团 —— 「只画该画的线」是设计决定,不是模型能力。
动它会怎样:画点/画线的开关在 view.py,不影响识别结果;但把拓扑表换成全部连接,画面会变成一张密不透风的网,反而看不出脸。
先认识 5 个词
只有 128×128 输入的小检测器,专门找人脸,速度极快。什么时候失效:脸小于约 30 像素、或大角度侧脸时检不出。
从整帧里抠出来的那一小块(这里是脸)。什么时候失效:ROI 算错(框给歪)时,后面几百个点全歪。
只含平移、缩放、旋转的变换,用 2×3 矩阵表示;这里用来「把脸摆正并放大」。什么时候失效:大角度侧脸时仿射不够用(需要透视变换),点位会挤在一侧。
点位模型对自己的判断:「这裁片里真有脸吗」,取值 0~1。什么时候失效:严重遮挡(戴口罩、手挡半张脸)时会掉到门限以下,整张脸不画。
哪些点之间连线,用来画出五官轮廓。什么时候失效:换模型(比如 478 点的 iris 版)后索引会变,连接表必须同步换。
什么时候会翻车:五个真实边界选读 1 分钟
- 大角度侧脸 —— 超过约 45° 时,仿射裁片里只剩半张脸,点会挤到边界上,presence 也容易掉到 0.5 以下。
- 戴口罩 / 手挡脸 —— 嘴部点会「猜」出来,位置看着挺像,其实不可信。做眨眼、张嘴判定时要注意这一点。
- 脸太小 —— 远景里人脸只有十几像素,检测器直接漏掉;先把画面裁近或提高分辨率。
- 多人拥挤 ——
num_faces: 2只处理前两张;人再多要改配置,代价是帧率线性下降。 - 强逆光 / 过曝 —— 检测分数整体下移,表现是「有时能画有时不能」,不是模型坏了。
| 步 | 数据变成什么 | 为什么非它不可 / 少了会怎样 | 谁在干 | 动参数会怎样 |
|---|---|---|---|---|
| 1 | 整帧 → 候选框 + 6 个基准点(最多 2 张) | 不先找脸,点位模型不知道往哪看 | BlazeFace + nms | min_detection_confidence↑ 侧脸丢失;num_faces 限人数 |
| 2 | 一个框 → 192×192 的裁片 | 不裁,脸只占几十像素,点位精度崩 | roi_affine / warp_roi | 无参数;裁错=全歪 |
| 3 | 裁片 → 468×3 坐标 + presence | 核心回归;presence 决定这张脸要不要 | 点位模型 + sigmoid | 门限同 Step 1,共用一个配置项 |
| 4 | 归一化坐标 → 原图像素点 → 点/线/框 | 不反算,点整体偏移且边缘更偏 | denormalize_landmarks / draw | 只影响显示,不影响识别 |
这张表只看第 2、5 两列就够:第 2 列是数据形态,第 5 列是你能拧的旋钮。函数名在第四列。
05实操:单图 / 实时 / 两个人三条命令
先看「敲的命令」与「原理第几步」怎么对上号。
| 环节 | 函数(文件) | 吃哪个参数 | 对应原理第几步 |
|---|---|---|---|
| 取图 / 取流 | cv2.imread / Ros2Camera(demo_image.py / camera_demo.py) | source.mode --ros | 链外 |
| 找脸 | LandmarkEngine.process 前半(landmark.py:121-126) | num_faces min_detection_confidence | 步骤 1 |
| 裁片 | detection2roi / roi_affine / warp_roi | (无) | 步骤 2 |
| 钉点 | 点位模型 predict + sigmoid(landmark.py:133-146) | min_detection_confidence | 步骤 3 |
| 反算与绘制 | denormalize_landmarks + view.draw | (无) | 步骤 4 |
| 推流 | MjpegServer.set_frame | server.port stream_name | 链外 |
-
单图跑通,认出每一层约 3 分钟
单图模式最快,也不依赖相机。命令见第 03 节;跑完打开
out.jpg,对照下面三行就够判断对错。左栏:绿框 + face 0.96 右栏:468 个黄点 + 绿色五官轮廓 底部:FPS: 0 Faces: 1顺手做一次对照:把
min_detection_confidence从 0.5 改成 0.1,重跑 —— 分数更低的候选也会进,状态栏的Faces可能变成 2(同一张脸的重复框)。这说明 NMS 与门限是两件事。 -
实时相机:看它跟不跟得住约 5 分钟
实时模式走 MJPEG,端口 8081(避开 EXP-07 的 8080)。先起驱动再跑程序,两个终端
ROS_DOMAIN_ID一致(N 取 0~101,各设备不同)。终端一 · 起相机驱动
source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash export ROS_DOMAIN_ID=N ros2 launch ascamera hp60c.launch.py
日志持续滚动,无 ERROR终端二 · 跑关键点
source /opt/ros/humble/setup.bash && source ~/ros2_ws/install/setup.bash export ROS_DOMAIN_ID=N cd ~/ros2_ws/src/xflz_eai_car1/vision/09_face_landmarks export PYTHONPATH=$PWD/car1_face_landmark python -m face_landmark_core.camera_demo --config config/face_config.yaml --ros --port 8081
[camera_demo] MJPEG on http://<板IP>:8081/face.mjpg ← 浏览器打开这个地址,画面里头的点会跟着你动对着镜头转头、低头、把脸凑近 —— 看点在侧脸时是否还贴合。这是模型能力边界最直观的一次检验。
-
两个人一起入镜约 5 分钟
默认
num_faces: 2,所以两个人会各画一套点。把配置改成 1,第二个人立刻不再画点 —— 但状态栏Faces也会变 1。这一步的意义:多人不是「一个模型算所有人」,而是「对每个框各跑一遍裁片 + 钉点」。人数越多越慢,是线性增长。
Faces: 1。两处对上了,说明找脸、裁片、钉点、反算四段你已完整走通。你该看到的画面
眼角、嘴角、脸廓都对得上,点在有细节的地方更密。
仿射裁片里只剩半张脸,靠外侧的点被压到边界,presence 也可能掉下来。
点位照样输出,但不可信 —— 做张嘴判定会误触发。
先看脸够不够大、光线够不够;再检查 min_detection_confidence 是否太高。
踩坑速查
| 现象 | 原因 | 怎么办 |
|---|---|---|
报 ModuleNotFoundError: face_landmark_core | 没把包目录加进 PYTHONPATH | export PYTHONPATH=$PWD/car1_face_landmark,用 -m 方式跑 |
| 点整体偏到一边 | 裁片坐标没反算回原图(或框被裁到帧外) | 检查 denormalize_landmarks 用的变换;框要裁剪到帧内 |
状态栏 Faces: 0 | 脸太小、光太暗,或门限太高 | min_detection_confidence 降到 0.3;让脸占画面更大 |
| 同一张脸出现两套点 | NMS 没压住重复框,或门限过低 | 确认 min_suppression_threshold 生效;门限不要低于 0.3 |
| 第二个人不画点 | num_faces 只到 2,或第三名分数太低 | 改 num_faces;注意帧率会随人数线性下降 |
--ros 报 cannot open ros topic | 驱动没起 / ROS_DOMAIN_ID 不一致 / 话题被 remap | 先起 ascamera;两终端 export 同一个 N;别改话题名 |
进阶 1 · 用两个点算「眨眼」约 20 分钟
上下眼睑各取几个点,用「上下距离 ÷ 左右距离」得到一个比值。睁眼时比值大、闭眼时接近 0,再配一条门限就是眨眼检测。
观察点:低头看手机时比值会持续偏小,会误判成「一直闭眼」—— 这正是「失效边界」的典型例子,需要配合头部姿态一起判断。
进阶 2 · 把 468 点做成一张「对齐后的正脸」约 25 分钟
取左眼、右眼、鼻尖、嘴角几个点作参考,算一个相似变换把整张脸摆正 —— 这是人脸识别的标配前处理(face alignment)。摆正之后再比对,比直接比原图稳定得多。
本实验的 roi_affine 已经在做类似的事,只是目标是「裁片」而不是「识别」。
🧪 Vibe Coding · 自己把这段代码读明白约 20 分钟
你要产出三样:模块职责表 → 控制流流程图 → 时序图。与真源码逐条对照。
提示词 1 · 模块职责分析
请阅读这个人脸关键点项目的模块:face_landmark_core/landmark.py、blaze.py、config.py、view.py、demo_image.py、camera_demo.py。 它们共同完成「读一张图 → 找到脸 → 输出 468 个点 → 画出来」。 请输出一张表,每行一个模块,四列:模块名 / 一句话职责 / 被谁调用 / 从哪个配置项取值。 关键函数要标出行号;说不清调用关系的地方请明说,不要猜。
提示词 2 · 控制流流程图
请按「处理一帧」的视角,为 landmark.py 的 process 方法画流程图。 必须包含:检测器输出的候选框如何经 NMS 与 num_faces 截断;对每个框如何算 ROI、做仿射、裁片;presence 低于门限时如何跳过;点坐标如何反算回原图。 每个框写函数名,判断框写条件。
提示词 3 · 时序图
请为实时模式画一张时序图,参与者用真实名字:ros2_camera 节点、Ros2Camera 回调、LandmarkEngine.process、MjpegServer。 按时间顺序标出:话题帧到达、检测器推理、点位模型推理、绘制、推流。 并标注两次推理是串行(同一张脸必须等第一次结果),而不同脸之间可以并行。
自查三问:每个模块都能指到真实文件与行号?流程图里有 presence 的跳过分支?时序图里说清了「两张脸是串行还是并行」?
三处必然的差异:
| 差异点 | AI 版常见写法 | 工程版(本仓) | 为什么工程这么写 |
|---|---|---|---|
| 点位怎么算 | 整张图直接回归 468 点 | 先裁 192 裁片再回归(landmark.py:130-132) | 脸在原图里太小,直接回归精度崩;裁片让脸占满输入 |
| 多个候选框 | 全部跑一遍点位模型 | NMS 后只取前 num_faces 张(landmark.py:126) | 点位模型最贵,人多时帧率线性下降;限人数是必要的取舍 |
| 分数怎么判 | 直接把输出当概率和 0.5 比 | presence 是 logit,先做数值稳定的 sigmoid 再比(landmark.py:136-142) | logit 直接比会溢出/误判;负数分支用等价式避免 exp 爆炸 |
改一处、先预测再验证:把 num_faces 从 2 改成 1,先写下两人入镜时画面会变成什么样,再跑一次对答案。
06互动:拖检测门限,看哪几张脸还算数
📌 这一节是「掌握检查」:亲手改一个参数,看现象是否朝你预期的方向变,还要能说出为什么 —— 说得出,才算掌握。
第 04 节说「谁的点会画」由两件事决定:分数够不够(min_detection_confidence)和排不排得上号(num_faces)。这两条在命令里只能各试一次,这里摆三张真实难度不同的脸,随便拖。
num_faces 张。点数按真实 468 的密度示意,不是逐点渲染。怎么读出规律:先把 num_faces 拉到 3,只动门限 —— 看到「分数不够就一张都不画」;再把门限压到最低,只动 num_faces —— 看到「分数够、但排不上号也不画」。两条规则先后作用,别混成一条。
0.96 与 0.58 通过,0.42 被挡。默认配置的效果。
侧脸 0.58 掉到门限以下 —— 这就是「转头就丢点」的原因。
两张都过门限,但只取分数最高的那一张。
远处的糊脸也被画上点,但它那套点基本不可信 —— 门限低不等于更准。
07验证:三道小测
回页首对照「实验目标」:① 框与点的区别、两段式接力讲得清吗 → 试 Q1 与 Q2;② 单图与实时跑通了没有、状态栏 Faces 能否解释 → 回第 05 节自查;③ 两个门限调过没有 → 试 Q3,并回第 06 节把门限与人数各推到极端。三条全 ✓ 就完成。
presence 低于门限时,会发生什么?presence 是点位模型对自己裁片的自检,是 logit,要先过 sigmoid 再和 min_detection_confidence 比。它和检测框的分数共用同一个配置项,这一点很容易被忽略:调这个门限,其实同时动了两个地方。min_detection_confidence: 0.5、num_faces: 1,结果是?num_faces 张。所以「过了门限」不等于「一定会画」。想两个人都画,把 num_faces 改成 2(注意帧率会掉)。08章节总结:468 点不是「检测」出来的,是「裁出来再钉上去」
这页讲了什么,它们怎么串起来
| 概念 | 它解决了什么 | 和别的关系 |
|---|---|---|
| BlazeFace 检测 | 在整帧里快速找到脸,并给出朝向线索(6 个基准点) | 上游吃整帧,下游决定 ROI 怎么裁 |
| ROI + 仿射 | 把脸搬到一张 192 见方的裁片里,摆正、放大 | 是精度的来源;也是步骤 4 反算的依据(同一组变换用两遍) |
| 468 点回归 | 在裁片里给出五官的精确坐标 | 输出 468×3(含相对深度 z),比框细两个数量级 |
presence | 判断「这张裁片里到底有没有可标的脸」 | 与检测分数共用 min_detection_confidence;决定整张脸要不要 |
num_faces | 限制每帧最多处理几张脸 | 是性能取舍:越多人越慢,线性增长 |
| 拓扑连接表 | 决定画哪些线,让形状可读 | 只影响显示;换模型(如带虹膜的 478 点)必须同步换表 |
| 反算回原图 | 把裁片坐标变回画面坐标 | 漏了这一步,点整体偏移,且越靠边缘偏得越多 |
② 直觉是模型越大点位越准,实际是「脸在输入里占多大」更决定精度 —— 裁片让同一个小模型变得够用;
③ 直觉是点画出来就是真的,实际是戴口罩时嘴部的点是「猜」的,看着像、其实不能用。
②
min_detection_confidence 一个名字管两处(检测分数与 presence),调它等于同时动两道门;③ 端口是 8081(
face.mjpg),与 EXP-07 的 8080 不冲突,但同时跑才需要错开;④ 单图模式不打印统计行,判据是图上的状态栏
Faces: N —— 别看终端没输出就以为失败了。
前置依赖 · 缺一样就跑不起来
| 类型 | 具体项 | 缺了会怎样 / 怎么补 |
|---|---|---|
| 环境 | 板上 Python 在 ~/ros2_venv;实时模式还需 ROS 2 Humble 两级 source 与相机节点 | 未激活 venv → python: command not found;未 source ROS → ImportError: librcl_action.so |
| Python 库 | numpy、opencv-python、pyyaml、rknnlite、mjpeg_streamer | 缺 rknnlite 模型加载失败;缺 mjpeg_streamer 实时模式启动即退出 |
| 模型 / 资源 | models/face_detection_short_range.rknn(584KB)+ models/face_landmark.rknn(1.6MB)+ images/face_demo.jpg | 缺检测模型就没有框;缺点位模型则一个点都没有;换图要保证脸够大 |
| 配置 | config/face_config.yaml:num_faces: 2、min_detection_confidence: 0.5、source.mode: image、server.port: 8081 | 缺 key 抛 ConfigError;mode: image 却没写 rgb_path 直接报错 |
| 运行方式 | 必须在项目根目录、且 PYTHONPATH 含 car1_face_landmark,用 -m 模块方式运行 | 直接 python car1_face_landmark/.../demo_image.py 会因相对导入失败 |
这套代码用在哪儿
- 美颜 / 贴纸 / 换脸:眼睛放大、瘦脸、贴纸锚点全依赖这几个关键点;
z还能判断转头角度。 - 疲劳与注意力检测:用眼睑点算眨眼频率、用头部姿态判低头 —— 司机监控、在线课堂的专注度统计。
- 交互触发:张嘴 → 拍照、皱眉 → 提示,比手势更容易做,因为只需要一张脸。
- 不该用它的场合:只想判断「有没有人」时不必上关键点(EXP-04/06 的检测或分割更便宜);需要身份识别时还要额外的识别模型,点位只是形状。
下一个实验把镜头拉远:脸上的 468 点变成全身的 33 点 —— 点位从「五官」升级成「姿态」,判定动作就有了依据。