简介:一套面向计算机视觉入门与进阶人群的人脸姿态估计资源,基于OpenCV、Dlib与MTCNN实现6点面部关键点检测,通过欧拉角计算及三维投影变换完成偏航角、俯仰角、翻滚角的实时测量,同时提供相机矩阵校准方法。资源共10个文件、约341KB,包含Python核心脚本(pose_estimation.py)、说明文档(txt/md/PDF)、示例人脸图片(jpeg/jpg/png)及许可证文件,覆盖从原理说明到实际运行的完整链路。已有65人浏览学习,适合需要快速理解头部旋转角度测量原理、或想在实时视频流中部署人脸姿态分析功能的开发者。包内说明文件与PDF有助于理清Dlib、MTCNN两种关键点方案的差异;示例图片可直观验证检测效果,脚本中从坐标提取、姿态解算到角度输出的流程均有注释,便于自行拓展到其他应用场景。
1. 人脸姿态估计到底在算什么:从6个关键点到三个欧拉角
注意力检测、驾驶员疲劳预警、直播互动特效、智慧零售的顾客动线分析,这些场景背后都绕不开同一个基础问题——画面里这颗头到底转了多少度。人脸姿态估计做的就是这件事:从一帧普通 RGB 图像里把脸找出来,捡出几个关键点,再通过三维投影变换把像素坐标换算成三个欧拉角,也就是常说的偏航角(左右转头)、俯仰角(抬头低头)和翻滚角(歪头)。标题里这套方案把 OpenCV 的几何解算、Dlib 和 MTCNN 两类关键点检测器放到一起讲,核心是用 6 个面部关键点做姿态解算,而不是常见的 68 点全量拟合。我把它做成过实时版本,也踩过不少角度翻转、内参漂移的坑。这篇我按自己的落地路径来写,适合正在做视觉交互、安防分析,或者想给现有检测流程加一个“头部朝向”维度的工程师。
2. 选型与数学原理:Dlib、MTCNN 和 OpenCV solvePnP 各干哪一段
一条完整的人脸姿态估计链路是固定的:人脸检测 → 关键点定位 → 2D-3D 点对应 → 求解相机外参 → 转换成欧拉角。这里面 OpenCV 不是可选项,而是数学底座;Dlib 和 MTCNN 是竞争关系,二选一即可。先把这个分工看清楚,后面写代码才不会糊。
Dlib 走的是 HOG + 线性分类器做人脸检测,再用 ERT 回归出 68 个关键点,整套在 CPU 上运行,单帧耗时大约 15 到 30 毫秒,胜在部署简单、不需要 GPU。MTCNN 是三级联网络,P-Net 快速提候选框,R-Net 精筛,O-Net 输出人脸框和 5 个关键点,在 GPU 上单帧可以做到几毫秒,CPU 上反而比 Dlib 慢,大约 30 到 60 毫秒。OpenCV 自带的 YuNet 人脸检测器也能输出 5 点,但它不在标题范围内,我不展开。
2.1 6点关键点:为什么要砍掉68点
很多人一上手就习惯把 Dlib 的 68 个点全喂给 solvePnP,觉得点越多精度越高。实际翻车概率恰恰相反。68 点里的眉毛、脸颊轮廓、下巴边缘这些点,在人脸侧转超过 30° 时大面积自遮挡,回归器给出的位置是“硬猜”的,噪声极大。把这些不可靠的点送进 solvePnP,姿态角反而不如少量稳定点算出来的结果。
6 点方案只保留四种解剖结构:鼻尖、下巴、左右眼外角、左右嘴角。这些点在正脸到 ±45° 侧脸范围内基本可见,遮挡概率低,而且空间分布能同时约束三个轴——眼外角连线约束翻滚角,鼻尖到下巴的连线约束俯仰角,双眼连线和鼻尖形成的水平三角约束偏航角。
我见过的最小配置是 4 点(左眼、右眼、鼻尖、下巴),但少了嘴角之后,pitch 角对下巴点的噪声会非常敏感。6 点是对稳定性和解算刚性之间的折中,也是我目前在新项目里的默认选型。下表是三种检测器的选型对比:
| 检测器 | 关键点数 | CPU i5 单帧耗时(近似) | 侧脸鲁棒性 | 适合场景 |
|---|---|---|---|---|
| Dlib | 68 点 | 15~30 ms | 一般,>30° 开始飘 | CPU 部署、快速原型 |
| MTCNN | 5 点 | CPU 30~60 ms,GPU <10 ms | 较好,能扛到 45° | GPU 实时管线 |
| OpenCV YuNet | 5 点 | 20~40 ms(DNN 后端) | 中等 | 不想引入额外依赖 |
需要注意一个前提:6 点方案的姿态输出范围是有限度的。超出 ±35° 之后,yaw 角的线性度会明显下降,到 ±45° 附近基本饱和。这是所有稀疏关键点方案的物理边界,不是调参能解决的。
2.2 solvePnP 的数学:三维投影变换和欧拉角的计算顺序
OpenCV 的 solvePnP 解决的是一个经典几何问题:已知物体在世界坐标系下的 N 个 3D 点、它们在图像上的 2D 投影点、相机内参矩阵,求解相机相对物体的旋转向量 rvec 和平移向量 tvec。
这里的“投影”用的是针孔相机模型。一个 3D 点 X 先经过旋转平移变换到相机坐标系,再除以 z 做透视投影,最后乘内参矩阵得到像素坐标。solvePnP 就是在最小化这个重投影误差。默认用迭代法(Levenberg-Marquardt),如果 2D 点噪声比较大,可以换 solvePnPRansac,它通过随机采样剔除离群点,但耗时是迭代法的好几倍。
拿到 rvec 之后,想直接读出角度还得再做两步。先把旋转向量用 Rodrigues 公式转成 3x3 旋转矩阵 R,再从 R 里按固定顺序提取欧拉角。我这里采用的顺序是 Z-Y-X,也就是先绕 X 轴翻滚、再绕 Y 轴俯仰、最后绕 Z 轴偏航,对应公式为:
yaw = atan2(R[1][0], R[0][0]) pitch = atan2(-R[2][0], sqrt(R[2][1]^2 + R[2][2]^2)) roll = atan2(R[2][1], R[2][2])注意,这段公式只在“旋转矩阵右乘列向量”的约定下成立。很多博客不声明坐标系就直接给公式,导致读者在左转和右转的方向上完全相反。还有更隐蔽的坑:图像坐标系 y 轴向下,而 3D 世界坐标里我通常也把 y 定义为向下,这会让 pitch 的符号和人的直觉相反。这个问题我放在第 5 章的避坑清单里细说。
2.3 相机矩阵不对劲,姿态角就是“玄学”
solvePnP 里还有一个绕不开的输入:相机内参矩阵。它长这样:
fx 0 cx 0 fy cy 0 0 1fx 和 fy 是焦距的像素度量,cx 和 cy 是光心坐标。对一个没有标定过的普通摄像头,常见的近似取法是 fx = fy = 图像宽度,cx = 宽度/2,cy = 高度/2。这个近似对方向性应用(判断人在看左还是看右、低头还是抬头)够用,但要是想度量绝对值,比如“这个人向右转了 30°”,就必须用棋盘格标定一次。
为什么这么说?因为 fx 直接决定了角度换算的比例。如果 fx 估小了,同一个 2D 位移解出来的角度就会偏大;fx 估大了,角度则偏小。更麻烦的是,畸变系数如果完全不设,画面边缘的关键点投影误差会带进来,造成角度随人脸在画面中的位置漂移——人脸不动,光移动位置,角度就变了。
3. 用 OpenCV + Dlib 跑通最小方案:关键点检测、相机内参和欧拉角计算
这一章直接给可复现的代码。我的习惯是先跑通一条主流程,再考虑性能优化。环境上用 Ubuntu + Python 虚拟环境,OpenCV 用 pip 包就够了,没必要在算法开发阶段就源码编译。
3.1 环境准备:Ubuntu 上安装 OpenCV 与 Dlib
新建虚拟环境是个好习惯,能避开系统 Python 和 conda 环境互相污染的问题。很多人在 Ubuntu 上配置完 OpenCV 后,一跑代码就报 ModuleNotFoundError: No module named 'cv2',十有八九是装到了系统 Python 里,而解释器用的是虚拟环境。
python3 -m venv pose_env source pose_env/bin/activate pip install opencv-python dlib numpy这里有个经验:dlib 在 Python 3.12 上经常没有预编译包,pip 会现场拉源码编译,时间很长还容易踩 CMake 的坑。建议直接创建虚拟环境时指定 Python 3.8 或 3.9,能省掉大部分编译痛苦。OpenCV 的 pip 包已经包含常用模块,图像处理项目里用到的人脸检测、solvePnP、棋盘格标定全在里面,不需要从源码安装。
3.2 6 点 3D 参考模型:世界坐标怎么定
关键一步:定义 6 个面部点在“世界坐标”下的位置。这不是从哪个数据集扒的,而是我们自己定义的一组近似人脸比例的三维坐标,单位用厘米,原点放在鼻尖,x 轴向右、y 轴向下、z 轴朝人脸前方。
| 关键点 | 含义 | Dlib 68点索引 | x (cm) | y (cm) | z (cm) |
|---|---|---|---|---|---|
| 鼻尖 | 坐标原点 | 30 | 0.0 | 0.0 | 0.0 |
| 下巴 | 竖直约束点 | 8 | 0.0 | -6.0 | 1.5 |
| 左眼外角 | 水平约束点 | 36 | -3.5 | -3.0 | -1.5 |
| 右眼外角 | 水平约束点 | 45 | 3.5 | -3.0 | -1.5 |
| 左嘴角 | 水平辅助点 | 48 | -2.5 | -5.0 | -1.0 |
| 右嘴角 | 水平辅助点 | 54 | 2.5 | -5.0 | -1.0 |
这套坐标不需要精确到毫米,只要比例接近真实人脸即可。z 值的相对关系对 yaw 角幅值影响最大:如果 z 都取 0,侧面旋转时解算角度会偏大;如果 z 取得太大,角度又会偏小。我的经验是把眼角的 z 设为 -1.5、鼻尖为 0,让眼角比鼻尖更靠近相机,这样侧脸转角在 ±35° 内基本线性。
3.3 主流程代码:Dlib 检测 + solvePnP + 欧拉角提取
下面这段是完整的最小实现,可以直接单步跟读。我拆成了取关键点和算角度两个函数,方便后续替换检测器。
import cv2 import numpy as np import dlib # 初始化 Dlib 检测器和 68 点模型 detector = dlib.get_frontal_face_detector() predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") # 6 点 3D 参考模型,单位 cm,顺序与 landmark_idx 一一对应 model_3d = np.array([ (0.0, 0.0, 0.0), # 30 鼻尖 (0.0, -6.0, 1.5), # 8 下巴 (-3.5, -3.0, -1.5), # 36 左眼外角 (3.5, -3.0, -1.5), # 45 右眼外角 (-2.5, -5.0, -1.0), # 48 左嘴角 (2.5, -5.0, -1.0), # 54 右嘴角 ], dtype=np.float32) landmark_idx = [30, 8, 36, 45, 48, 54] # 近似相机内参,标定后可替换 W = 640 H = 480 camera_matrix = np.array([ [W, 0, W / 2], [0, W, H / 2], [0, 0, 1], ], dtype=np.float32) dist_coeffs = np.zeros((4, 1)) def get_landmarks_6(frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) rects = detector(gray, 1) if len(rects) == 0: return None shape = predictor(gray, rects[0]) return np.array([ (shape.part(i).x, shape.part(i).y) for i in landmark_idx ], dtype=np.float32) def pose_from_landmarks(landmarks_2d): _, rvec, tvec = cv2.solvePnP( model_3d, landmarks_2d, camera_matrix, dist_coeffs ) R, _ = cv2.Rodrigues(rvec) yaw = np.arctan2(R[1, 0], R[0, 0]) pitch = np.arctan2(-R[2, 0], np.sqrt(R[2, 1] ** 2 + R[2, 2] ** 2)) roll = np.arctan2(R[2, 1], R[2, 2]) return np.degrees([yaw, pitch, roll]) # 调用示例 frame = cv2.imread("face.jpg") lmk = get_landmarks_6(frame) if lmk is not None: yaw, pitch, roll = pose_from_landmarks(lmk) print(f"yaw={yaw:.1f}, pitch={pitch:.1f}, roll={roll:.1f}")逻辑说明:get_landmarks_6 里先转灰度再检测人脸框,然后用 predictor 取 68 点,最后只按 landmark_idx 摘出 6 个点。pose_from_landmarks 里 solvePnP 返回的 rvec 是旋转向量,Rodrigues 把它转成旋转矩阵 R,再从 R 按 Z-Y-X 顺序提取三个欧拉角,最后转成角度制输出。
参数说明:detector(gray, 1) 的第二个参数是金字塔上采样次数,越大越能检出小脸,但速度更慢,实时场景一般用 0 或 1。camera_matrix 第一行 fx=W、cx=W/2,第二行 fy=W、cy=H/2,这是最简单的近似内参;帧分辨率变了要同步更新。dist_coeffs 全零意味着假设镜头无畸变,对笔记本摄像头和普通 USB 摄像头够用,但对广角镜头必须替换成标定结果。
3.4 相机矩阵校准:棋盘格标定一次,受益终身
如果项目要求的姿态角误差在 2° 以内,近似内参不够,得用棋盘格标定。OpenCV 的 calibrateCamera 是标准做法。
import cv2 import numpy as np # 棋盘格内角点 9x6,格子边长按实际打印尺寸填,单位 mm objp = np.zeros((9 * 6, 3), np.float32) objp[:, :2] = np.mgrid[0:9, 0:6].T.reshape(-1, 2) * 25 obj_points = [] # 每张图的 3D 角点坐标 img_points = [] # 每张图的 2D 像素坐标 for fname in sorted(glob.glob("calib_*.jpg")): img = cv2.imread(fname) gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) ret, corners = cv2.findChessboardCorners(gray, (9, 6), None) if ret: obj_points.append(objp) img_points.append(corners) ret, mtx, dist, rvecs, tvecs = cv2.calibrateCamera( obj_points, img_points, gray.shape[::-1], None, None )参数说明:objp 的第三列全为 0,因为棋盘格是平面的。格子边长 25mm 是实际打印尺寸,标定板的物理尺寸不影响内参结果,但影响外参的平移量。标定图片至少拍 12 到 15 张,要覆盖各个角度——左上、右下、倾斜、近处、远处都来几张,不要全拍正脸对着镜头。如果棋盘格找不全,先检查角点数是不是 9x6,比如 8x6 的板子要用 (8, 6)。
标定完把 mtx 和 dist 替换掉第 3.3 节里的 camera_matrix 和 dist_coeffs 即可,之后的姿态角就从“相对量”变成了“绝对量”。
4. 换成 MTCNN 做实时检测:5 点扩 6 点、线程模型与参数调整
Dlib 在 CPU 上表现稳定,但到了 GPU 实时管线里就有点跟不上。MTCNN 的优势是检测和关键点定位都在同一个网络里完成,GPU 上延迟极低,而且对侧脸的鲁棒性比 Dlib 好。代价是它的输出只有 5 个关键点:左眼、右眼、鼻尖、左嘴角、右嘴角,没有下巴点。要凑齐第 3 章的 6 点模型,得补一个下巴点出来。
4.1 MTCNN 的 5 点输出怎么补成 6 点
补下巴点的方法有很多,我用下来最稳的是几何外推:利用眼距作为尺度参考,从鼻尖沿脸部中轴向下延伸约 1.3 倍眼距。这个比例来自平均人脸测量值,不是一个精确的生理学常数,但对姿态解算来说已经足够。
import numpy as np def extend_to_6_landmarks(landmarks_5): # landmarks_5 顺序按 MTCNN 约定: # [左眼, 右眼, 鼻尖, 左嘴角, 右嘴角],坐标是 [x, y] left_eye = np.array(landmarks_5[0], dtype=np.float32) right_eye = np.array(landmarks_5[1], dtype=np.float32) nose = np.array(landmarks_5[2], dtype=np.float32) eye_dist = np.linalg.norm(right_eye - left_eye) # 图像 y 轴向下,所以下巴在鼻尖的"下方" chin = nose + np.array([0.0, eye_dist * 1.3], dtype=np.float32) return np.array([ nose, chin, left_eye, right_eye, np.array(landmarks_5[3], dtype=np.float32), # 左嘴角 np.array(landmarks_5[4], dtype=np.float32), # 右嘴角 ], dtype=np.float32)逻辑说明:这个函数把 5 点数组重排成与第 3.3 节 model_3d 对应的顺序——鼻尖、下巴、左眼外角、右眼外角、左嘴角、右嘴角。下巴的 x 坐标直接复用鼻尖的 x,没有做水平偏移,因为正脸时下巴基本在鼻尖正下方;侧脸时这个假设会引入少量误差,但相比没有下巴点导致的 pitch 角漂移,这点误差可以接受。
参数说明:eye_dist * 1.3 里的 1.3 是调节下巴位置的关键。调小了下巴点上移,pitch 角整体偏负;调大了下巴点下移,pitch 角整体偏正。我一般让它落在 1.2 到 1.4 之间,具体值可以用第 6 章的往返验证法来定。
4.2 实时管线:检测和姿态解算要分层
MTCNN 在 CPU 上单帧 30 到 60 毫秒,如果每一帧都同步跑检测加姿态解算,帧率会很难看。常见的做法是把管线拆成三层:抓帧线程、检测线程、姿态解算线程,检测线程用最新帧覆盖策略,不排队、不阻塞。
下面给一个简化的线程骨架,去掉了平台相关的摄像头和界面代码:
import threading import queue import time frame_queue = queue.Queue(maxsize=1) pose_queue = queue.Queue(maxsize=1) def capture_loop(cap): while True: ret, frame = cap.read() if not ret: break # 老帧直接丢,保证队列里永远是最新的 if frame_queue.full(): try: frame_queue.get_nowait() except queue.Empty: pass frame_queue.put(frame) def detect_loop(): while True: frame = frame_queue.get() boxes, probs, landmarks = mtcnn.detect(frame, landmarks=True) if landmarks is not None: lmk_6 = extend_to_6_landmarks(landmarks[0]) pose_queue.put(lmk_6) # 只传关键点,不传整帧 def pose_loop(): while True: lmk_6 = pose_queue.get() yaw, pitch, roll = pose_from_landmarks(lmk_6) # 这里做滤波和可视化逻辑说明:调度是刻意做成多线程的。抓帧线程负责把摄像头数据塞进队列,检测线程每帧只处理最新的画面,姿态解算线程拿到关键点后做欧拉角求解。检测间隔不是每帧都做,可以每 2 帧检测一次,关键点缺失时用上一次的结果兜底。
参数说明:队列 maxsize=1 是关键,它保证不积压旧数据,实时性优先于完整性。detect_loop 和 pose_loop 之间的 pose_queue 我传的是关键点而不是整帧,因为姿态解算不需要图像数据,能省掉大量内存拷贝。MTCNN 的 detect 参数里,如果画面里可能出现多张人脸,需要把 keep_all 或对应的检测阈值调低,否则默认只返回最可靠的一张。
4.3 检测失败降级:宁可 5Hz 稳健,不要 30Hz 乱跳
MTCNN 在快速回头、眼镜反光、极端光照下也会间歇性丢脸。我在真实项目里的处理策略是:连续丢帧不超过 5 帧时,用上一次的姿态角加上一个零速度假设继续输出;超过 5 帧才判定跟丢,重新做完整检测。这个策略比每一帧都做完整检测要稳得多。
另外一个容易忽略的细节:人脸在画面边缘时,MTCNN 的置信度会下降,关键点位置也会轻微偏移。可以加一个中心距惩罚项,把离画面中心太远的人脸姿态标记为低置信度,而不是直接当失败。
5. 常见问题与排查:从角度翻转到环境报错的五个血泪经验
这章直接进入踩坑清单。每一条都是我在实际项目里遇到过的,按现象、原因、解决的顺序讲清楚。
5.1 角度方向“反直觉”:低头为正还是为负完全取决于约定
现象:同一套代码、同一个模型,在 A 机器上左转 yaw 为正,换一台机器跑同样的视频,左转居然变负了。原因不是机器差异,而是旋转矩阵到欧拉角的公式、3D 坐标系的 y 轴方向、图像 y 轴方向这三者之间的约定没有统一。只要坐标系或者公式顺序变一个,符号就翻转。解决方法是拍一段自己左右转头、上下点头的视频,跑一遍记录符号,然后在输出函数里固定符号约定并写进注释。比如我约定“人脸向左转(从摄像头视角看)yaw 为正”,那就在代码里加一句yaw = -yaw来对齐直觉。
5.2 相机内参不准:角度“玄学”漂移
现象:人脸完全不动,yaw 曲线却慢慢偏离零度;向左转 45°,解出来只有 20°。原因:fx/fy 估得不准,畸变系数全零,solvePnP 在约束条件下“强行”找了一个最小重投影误差的解,但那个解和真实姿态差得很远。解决:按第 3.4 节做棋盘格标定,把 mtx 和 dist 替换掉。如果实在没条件打印标定板,可以先用 fx = 图像宽度凑合,然后在离摄像头固定距离(比如 1 米)验证人脸宽度对应的像素占比,反推 fx 再微调。
5.3 Dlib 在侧脸和遮挡下定点失败
现象:转头超过 40° 时,Dlib 把眼睛外角点的位置定到了眼眶里面,嘴角点飘到脸颊上,姿态角瞬间跳变几十度。原因:68 点模型的训练数据以接近正脸为主,侧脸自遮挡之后,模型找不到真实的解剖点,只能根据上下文“硬猜”。解决:切换 MTCNN 或限制输出范围。我的做法是当 yaw 绝对值超过 35° 时,直接把姿态标记为不可信,不参与后续业务逻辑,避免误判。
5.4 单帧抖动:姿态曲线毛刺多
现象:角度序列在稳定头姿下也有 ±3° 的抖动,看起来像噪声,滤波也压不住。原因:solvePnP 默认迭代法对 2D 关键点的 1~2 像素噪声非常敏感,尤其是 pitch 角对下巴点的上下抖动响应很大。解决:先对 6 个关键点做时间域平滑,再进 solvePnP,而不是对角度结果做平滑。角度平滑会引入滞后,关键点平滑更能保留真实姿态变化。
5.5 环境坑:ModuleNotFoundError 和 dlib 编译失败
现象:pip install opencv-python 后 import cv2 成功,但项目一跑就报 ModuleNotFoundError: No module named 'cv2';装 dlib 时 pip 卡在源码编译上,最后报引擎错误。原因:pip 装到了全局环境,而解释器用的是虚拟环境;或者 Python 版本太新,没有 dlib 预编译包。解决:统一用 venv 管理依赖,Python 版本锁在 3.8 到 3.11 之间。dlib 装不上时,先看日志是不是缺 CMake,如果是,apt 装 cmake 后再重试,通常就没问题了。
6. 进阶技巧:平滑滤波、视线映射与往返验证法
最后一章给三个具体技巧,都是让这个东西从“能跑”变成“敢用”的关键。
第一个技巧是角度平滑。一阶低通滤波是最划算的做法:
alpha = 0.4 angle_smooth = alpha * angle_now + (1 - alpha) * angle_smoothalpha 越大越跟手,但噪声也越大;alpha 越小曲线越平,但滞后越大。我一般从 0.4 起步,对着实时画面转头部试,调到“自然不飘”为止。注意角度跨 180° 的环绕问题,人脸姿态一般遇不到,但如果以后做全景头盔,记得处理 359° 和 1° 的差值,简单办法是先在角度差上加 180 取模再减回去。
第二个技巧是把欧拉角映射到视线方向。假设摄像头正对用户,yaw 乘以一个距离系数 k 就能得到屏幕上的水平注视点偏移,pitch 对应垂直偏移。这个 k 和用户到屏幕的距离有关,通常建议现场标定,让用户看屏幕四个角记录角度值,插值出映射表。注意这只是“头部朝向”的粗估,不是真正的眼动追踪——眼珠转动没有参与计算,想看瞳孔位置得额外加关键点。
第三个技巧是往返验证法,我每次调完参数必做:录一段 1 分钟的视频,头从左到右扫再回到中间,重复三轮。然后看角度曲线的三个特征——单调性(左右扫时不能有突然跳跃)、对称性(去程和回程的角度曲线应当近似重合)、静止方差(刻意定住头 5 秒,角度波动应在 ±2° 以内)。三个特征都满足,这套参数才算可信;任何一个不满足,优先回去查内参和下巴点比例,而不是盲目调滤波。
最后说个我自己的教训。最早做这套方案时,我觉得 68 点全量参与解算肯定更准,结果侧脸一超过 25° 就飘,花了两天查问题。后来换回 6 点模型,调好 fx,静止方差从 4° 降到了 1.2°。从那以后我记住了稀疏关键点不是偷懒,是工程上的取舍。希望这篇能帮你在实时姿态分析这条路上少走几个弯,把翻车的概率降到最低。
本文还有配套的精品资源,点击获取