核心课程 / 视觉入门课 / EXP-08
EXP-08 · 第 5 阶段 · 人体感知 · 验证性 · 4 学时

人脸关键点 468 点:把一张脸拆成能贴东西的坐标

美颜、贴纸、换脸、眨眼检测 —— 这些功能都要先知道眼角在哪、嘴角在哪。一个「人脸框」给不了这些,它只给你四个数。468 个点才是可用的形状。有意思的是:这 468 个点不是靠检测一次算出来的,而是先找脸、把脸裁下来放大、再在一张 192 见方的裁片里「钉」上去。

难度 ⭐⭐ · 4 学时 BlazeFace 检测ROI 裁剪 468 点回归双模型接力
01 颜色02 轮廓03 二维码04 检测05 分类06 分割07 跟踪08 人脸09 骨骼10 3D11 体感12 手势

01目标:468 个点贴住脸,转头它跟着转

🎯 实验目标 · 学完你将能
  • 说清「框」和「点」的区别:框是四个数,点是 468 组坐标;以及为什么必须先检测再裁再回归
  • 跑通单图与实时两条命令,看到左栏绿框 face 0.96、右栏 468 个黄点与绿色轮廓线
  • 调出两种效果:min_detection_confidence 调高 → 侧脸不再画点;num_faces 改 1 → 第二个人被丢掉
⚠️ 重点与难点 重点:两段式接力 —— 128×128 找脸、192×192 钉点,以及中间那次「裁片 + 仿射变换」。
难点:直觉是「一个模型直接输出 468 个点」,实际是两个模型、两种输入尺寸;点是在裁片坐标系里算出来的,必须靠那次的变换矩阵反算回原图,否则点会整体错位。
INPUT · 你给它
images/face_demo.jpg

一张局部人像照。也可以换 --ros 的相机画面。

→
OUTPUT · 你拿到
Faces: 1 + 468 个点

双栏图:左 = 绿框 + face 0.96,右 = 468 个黄点 + 绿色轮廓线,状态栏打 Faces: 1。

本实验的目标:一张脸进来,468 组坐标出去 —— 眼角、嘴角、脸廓全都有数,转头时它们跟着动。

02问题:光有「人脸框」做不了美颜

手机相机里那个「贴纸」功能,猫耳朵为什么总能贴在头顶、胡子为什么总在嘴唇上方?因为程序知道那两处在哪。

如果只有一个人脸框,你能做的只有「把整张图按框裁出来」。想贴猫耳朵?你不知道头顶在哪。想瘦脸?你不知道脸颊边界在哪。框只有四个数,五官需要几百个。

一个框:只知道「脸在这一块」 face 0.96 4 个数,贴纸贴不准 ? 468 个点:五官位置全都有 468 组坐标,猫耳朵有地方可贴 .
图 2-1|同一张脸的两种输出。左图那种「框」在 EXP-04/07 已经用得很熟,右图这种「点」才能支撑贴纸、瘦脸、眨眼判定。
对比平时你看到的(黑盒)机器必须做的(白盒)
贴纸为什么贴得准猫耳朵跟着头转,像磁铁吸住? —— 得先知道「头顶中点」这一组坐标,且每帧重算
为什么不用框裁看不出程序在框和点之间有什么区别? —— 框里混着大量背景,直接回归点位会飘
两个人在画面里两个人都有贴纸? —— 要对每张脸各跑一遍「裁 + 钉点」

三个问号拆成三个具体问题:

① 468 个点是怎么算出来的?

② 为什么要先把脸裁下来、还放大到 192 见方?

③ 画面里有两个人,谁的点会显示、谁不会?

框回答「脸在哪一块」,点回答「五官在哪一点」。这一页就是从「一块」到「几百点」的那一步。

03现象:左栏绿框 face 0.96,右栏 468 点浮在脸上

🎬 现象速览 · 30 秒先看效果

你会看到:一张双栏图。左栏是原图 + 绿框 + 角标 face 0.96;右栏是同一张图,上面浮着 468 个黄点,绿色细线把眼睛、眉毛、嘴唇和脸廓连了起来。底部状态栏写着 FPS: 0 Faces: 1。

输入照片:一个人正视镜头的人像
① 输入 images/face_demo.jpg
运行结果双栏图:左栏原图带绿框与 face 0.96,右栏 468 个黄点勾出五官,底部状态栏 FPS: 0 Faces: 1
② 输出:左框右点,Faces: 1

怎么跑出来:进项目根目录,把包目录加进 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
W rknn-toolkit-lite2 version: 2.3.2 ← 单图模式不打印统计行;判据是 out.jpg 里的状态栏 ← 打开 out.jpg:左栏绿框 face 0.96,右栏 468 点,底部 Faces: 1

🔍 看什么:三处细节,后面都要用到。

  • 点是「贴」在五官上的,不是均匀铺满 —— 眼睛、嘴唇周围明显更密。模型把分辨率花在了需要精度的地方。
  • 轮廓线只有几组 —— 绿线只画了脸廓、眉、眼、唇,共几段;468 个点之间的连线有上千条,全画出来会糊成一团。
  • 右栏背景被压暗了 —— 只留点和线,方便看清形状。这是绘制层做的,不是模型输出。

这就是本实验最终要呈现的现象。此时照做、看懂它就行;第 05 节会补上实时相机与多人两路。

04原理:先找脸、再裁下来放大、最后在 192 见方里钉 468 个点

CORE IDEA · 一句话原理

不是一个模型直接吐 468 个点,而是两个模型接力:128×128 的小模型先找到脸,把这块脸裁下来放大到 192×192,再让第二个模型在这张裁片里钉 468 个点。

打个比方:像找人写字 —— 先在一张合影里圈出「就是这个人」,再把那一块放大复印成一张近照,最后拿尺子在近照上量眼角到嘴角的距离。 揭个盖:量出来的尺寸是近照的尺寸,不是合影的。所以最后必须按当初放大的倍数反算回去 —— 这一步漏了,点会整体偏移,且越靠画面边缘偏得越多。

四步回答第 02 节的三问:① 468 点怎么来 → 步骤 3;② 为什么要裁 → 步骤 1、2;③ 两个人画谁 → 步骤 1 的 num_faces 与步骤 3 的 presence。

① 128 见方找脸 BlazeFace 框 + 6 个基准点 取分数最高的前 2 张 ② 裁下来放大到 192 roi_affine + warp 按基准点摆正、放大 只留脸,背景被切掉 ③ 在裁片里钉 468 点 468 x 3 (x,y,z) 再带一个 presence 分数 低于 0.5 整张脸丢弃 ④ 反算回原图再画 denormalize_landmarks 按 ② 的变换反算 画点 / 画拓扑线 / 画框 ② 和 ④ 是同一组变换的正反两用:② 把脸搬到裁片,④ 把点搬回原图 468 个点的精度来自「脸在裁片里足够大」,而不是来自模型更大 两人场景 = 对每个框各跑一次 ②③④;num_faces 限制最多跑几次
图 4-1|四步接力。关键在第 ② 与第 ④ 步是同一组仿射变换的正反两次使用 —— 漏了反算,点就整片偏移。

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,不影响识别结果;但把拓扑表换成全部连接,画面会变成一张密不透风的网,反而看不出脸。

现在回看第 03 节第 ② 张图:右栏那些点为什么整整齐齐贴着五官、又没有背景噪点 —— 因为它们是在一张「只有脸」的 192 裁片里算出来的。

先认识 5 个词

BlazeFace

只有 128×128 输入的小检测器,专门找人脸,速度极快。什么时候失效:脸小于约 30 像素、或大角度侧脸时检不出。

ROI 感兴趣区域

从整帧里抠出来的那一小块(这里是脸)。什么时候失效:ROI 算错(框给歪)时,后面几百个点全歪。

仿射变换

只含平移、缩放、旋转的变换,用 2×3 矩阵表示;这里用来「把脸摆正并放大」。什么时候失效:大角度侧脸时仿射不够用(需要透视变换),点位会挤在一侧。

presence

点位模型对自己的判断:「这裁片里真有脸吗」,取值 0~1。什么时候失效:严重遮挡(戴口罩、手挡半张脸)时会掉到门限以下,整张脸不画。

拓扑 topology

哪些点之间连线,用来画出五官轮廓。什么时候失效:换模型(比如 478 点的 iris 版)后索引会变,连接表必须同步换。

什么时候会翻车:五个真实边界选读 1 分钟
  • 大角度侧脸 —— 超过约 45° 时,仿射裁片里只剩半张脸,点会挤到边界上,presence 也容易掉到 0.5 以下。
  • 戴口罩 / 手挡脸 —— 嘴部点会「猜」出来,位置看着挺像,其实不可信。做眨眼、张嘴判定时要注意这一点。
  • 脸太小 —— 远景里人脸只有十几像素,检测器直接漏掉;先把画面裁近或提高分辨率。
  • 多人拥挤 —— num_faces: 2 只处理前两张;人再多要改配置,代价是帧率线性下降。
  • 强逆光 / 过曝 —— 检测分数整体下移,表现是「有时能画有时不能」,不是模型坏了。
步数据变成什么为什么非它不可 / 少了会怎样谁在干动参数会怎样
1整帧 → 候选框 + 6 个基准点(最多 2 张)不先找脸,点位模型不知道往哪看BlazeFace + nmsmin_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_frameserver.port stream_name链外
  1. 单图跑通,认出每一层约 3 分钟

    单图模式最快,也不依赖相机。命令见第 03 节;跑完打开 out.jpg,对照下面三行就够判断对错。

    左栏:绿框 + face 0.96 右栏:468 个黄点 + 绿色五官轮廓 底部:FPS: 0 Faces: 1

    顺手做一次对照:把 min_detection_confidence 从 0.5 改成 0.1,重跑 —— 分数更低的候选也会进,状态栏的 Faces 可能变成 2(同一张脸的重复框)。这说明 NMS 与门限是两件事。

  2. 实时相机:看它跟不跟得住约 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 ← 浏览器打开这个地址,画面里头的点会跟着你动

    对着镜头转头、低头、把脸凑近 —— 看点在侧脸时是否还贴合。这是模型能力边界最直观的一次检验。

  3. 两个人一起入镜约 5 分钟

    默认 num_faces: 2,所以两个人会各画一套点。把配置改成 1,第二个人立刻不再画点 —— 但状态栏 Faces 也会变 1。

    这一步的意义:多人不是「一个模型算所有人」,而是「对每个框各跑一遍裁片 + 钉点」。人数越多越慢,是线性增长。

🔁 现象呼应这三步跑完出现的画面,就是第 03 节那张双栏图:左绿框、右黄点、绿轮廓、底部 Faces: 1。两处对上了,说明找脸、裁片、钉点、反算四段你已完整走通。

你该看到的画面

正常
点贴着五官

眼角、嘴角、脸廓都对得上,点在有细节的地方更密。

侧脸 45°
点挤到一侧

仿射裁片里只剩半张脸,靠外侧的点被压到边界,presence 也可能掉下来。

戴口罩
嘴部点是「猜」的

点位照样输出,但不可信 —— 做张嘴判定会误触发。

异常 · Faces: 0
一个点都没有

先看脸够不够大、光线够不够;再检查 min_detection_confidence 是否太高。

踩坑速查

现象原因怎么办
报 ModuleNotFoundError: face_landmark_core没把包目录加进 PYTHONPATHexport 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)。这两条在命令里只能各试一次,这里摆三张真实难度不同的脸,随便拖。

⚡ 参数实验台 · 三张候选脸,谁被画出来
0.50
2
默认门限 0.50、num_faces = 2:正脸 0.96 与侧脸 0.58 画点,远处小脸 0.42 被门限挡下。
互动 6-1|三张脸的分数是固定场景值(正脸 0.96 / 侧脸 0.58 / 远处小脸 0.42),判定规则照源码:先按分数过滤,再按分数排序取前 num_faces 张。点数按真实 468 的密度示意,不是逐点渲染。

怎么读出规律:先把 num_faces 拉到 3,只动门限 —— 看到「分数不够就一张都不画」;再把门限压到最低,只动 num_faces —— 看到「分数够、但排不上号也不画」。两条规则先后作用,别混成一条。

门限 0.50 · 2 人
两套点

0.96 与 0.58 通过,0.42 被挡。默认配置的效果。

门限 0.70
只剩正脸

侧脸 0.58 掉到门限以下 —— 这就是「转头就丢点」的原因。

num_faces = 1
侧脸没名额

两张都过门限,但只取分数最高的那一张。

门限 0.40
三张全画

远处的糊脸也被画上点,但它那套点基本不可信 —— 门限低不等于更准。

「画不画」是两条规则的叠加:先过分数门限,再抢人数名额。调门限控制质量,调人数控制代价。

07验证:三道小测

✅ 目标对账 · 验证掌握得有多好

回页首对照「实验目标」:① 框与点的区别、两段式接力讲得清吗 → 试 Q1 与 Q2;② 单图与实时跑通了没有、状态栏 Faces 能否解释 → 回第 05 节自查;③ 两个门限调过没有 → 试 Q3,并回第 06 节把门限与人数各推到极端。三条全 ✓ 就完成。

Q1为什么不能用一个模型直接在整张 640×480 的图上回归 468 个点?
因为 468 这个数字必须是偶数
因为脸在原图里往往只占几十像素,模型看不清五官;先裁脸放大到 192 见方,同一模型精度立刻上来
因为 Opencv 不支持那么大的输出
对。这正是「先找脸、再裁片」的全部理由 —— 精度来自脸在输入里的占比,不是模型规模。裁剪还会顺带把背景切掉,减少干扰。代价是多了一次仿射变换,点必须反算回原图。
Q2点位模型输出的 presence 低于门限时,会发生什么?
点会变少,剩下照样画
这张脸的 468 个点全部丢弃,框也不画 —— 相当于「这张脸不算数」
程序崩溃退出
对。presence 是点位模型对自己裁片的自检,是 logit,要先过 sigmoid 再和 min_detection_confidence 比。它和检测框的分数共用同一个配置项,这一点很容易被忽略:调这个门限,其实同时动了两个地方。
Q3两个人都在画面里、分数分别是 0.88 和 0.62,min_detection_confidence: 0.5、num_faces: 1,结果是?
两个人各画一套点
只有 0.88 那张画点 —— 两张都过了门限,但名额只有一个,取分数高的
两张都不画
对。两条规则是先后关系:先按门限把不够分的筛掉,再按分数排序取前 num_faces 张。所以「过了门限」不等于「一定会画」。想两个人都画,把 num_faces 改成 2(注意帧率会掉)。

08章节总结:468 点不是「检测」出来的,是「裁出来再钉上去」

这页讲了什么,它们怎么串起来

概念它解决了什么和别的关系
BlazeFace 检测在整帧里快速找到脸,并给出朝向线索(6 个基准点)上游吃整帧,下游决定 ROI 怎么裁
ROI + 仿射把脸搬到一张 192 见方的裁片里,摆正、放大是精度的来源;也是步骤 4 反算的依据(同一组变换用两遍)
468 点回归在裁片里给出五官的精确坐标输出 468×3(含相对深度 z),比框细两个数量级
presence判断「这张裁片里到底有没有可标的脸」与检测分数共用 min_detection_confidence;决定整张脸要不要
num_faces限制每帧最多处理几张脸是性能取舍:越多人越慢,线性增长
拓扑连接表决定画哪些线,让形状可读只影响显示;换模型(如带虹膜的 478 点)必须同步换表
反算回原图把裁片坐标变回画面坐标漏了这一步,点整体偏移,且越靠边缘偏得越多
⚠ 三个反直觉的地方 ① 直觉是一个模型直接输出 468 点,实际是两个模型接力,中间还夹一次裁剪与仿射变换;
② 直觉是模型越大点位越准,实际是「脸在输入里占多大」更决定精度 —— 裁片让同一个小模型变得够用;
③ 直觉是点画出来就是真的,实际是戴口罩时嘴部的点是「猜」的,看着像、其实不能用。
💡 四条要注意 ① 两个输入尺寸别记混:检测 128、点位 192;配置里没有暴露它们,改模型才需要动;
② 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 点 = 先 128 找脸 → 裁成 192 裁片 → 在裁片里回归 → 再反算回原图。精度来自「脸有多大」,不是「模型有多大」。

下一个实验把镜头拉远:脸上的 468 点变成全身的 33 点 —— 点位从「五官」升级成「姿态」,判定动作就有了依据。