6点关键点+OpenCV solvePnP的头部姿态估计实战
2026/9/16 4:40:04 网站建设 项目流程

简介:从2D人脸关键点检测到3D姿态估计是计算机视觉中的常见需求,涉及相机内参标定、PnP求解和欧拉角分解等基础原理。通过Dlib或MTCNN获取少量面部关键点,结合OpenCV的solvePnP反算旋转向量,即可稳定输出偏航角、俯仰角与翻滚角。该方法计算量小、鲁棒性好,适用于驾驶员疲劳检测、视线估计、体感交互等实时场景。文章分享了一套仅用6个关键点完成头部姿态估计的工程实践,涵盖相机矩阵校准、关键点选取及常见坑点,帮助开发者快速落地。 写这个项目的时候,我其实刚踩完一个很大的坑:用标准68点关键点做姿态估计,代码写了一大堆,结果实时跑起来帧率拉垮,而且很多人脸在侧脸、遮挡情况下68点直接检测失败。后来干脆换了个思路,只取6个关键点,结合OpenCV的solvePnP和Dlib、MTCNN两套检测方案,反而把姿态估计这事做稳了。这项目就是围绕这个思路落地的——用最少的计算量,把头部偏航角、俯仰角、翻滚角实时算出来,并且集成成了可以直接跑的完整流程。

这套方案的核心链路是:人脸检测(Dlib或MTCNN)→ 提取6个面部关键点 → 将2D像素点和3D人脸模型点做对应 → 调用OpenCV的solvePnP反算旋转向量 → 通过Rodrigues变换得到旋转矩阵 → 分解出欧拉角。整个流程不算复杂,但里面每一环都有讲究,尤其是关键点选择和相机矩阵校准这两个地方,搞不定的话算出来的角度会飘得没法用。

这篇文章我会把整个项目的设计思路、每个模块的实现细节、代码结构和调参过程中遇到的坑全部写清楚,适合正在做人脸姿态估计、头部位姿测量、驾驶员疲劳检测、视线估计这类方向的同学参考。无论你是刚接触OpenCV还是已经做过一些人脸项目,这套方案里都有可以直接抄走的经验。

1. 整体设计与核心思路拆解

1.1 项目场景和需求定位:为什么需要估计头部姿态

人脸姿态估计说白了就是回答一个问题:面前这张人脸,他的头正在朝哪个方向转。在2D图像里这个问题肉眼能判断个大概,但让机器输出精确的偏航角、俯仰角、翻滚角,就需要一套完整的几何计算链路。

这个需求在很多实际场景里都会用到。比如驾驶行为分析要判断驾驶员有没有低头看手机、是否疲劳闭眼;远程面试系统要分析面试者眼神是否游离;体感交互游戏要根据头部转向调整视角;还有一些辅助驾驶系统需要根据头部朝向预判驾驶员注意力范围。这些场景共同的痛点在于:不能只靠人脸框的位置来判断头部方向,因为头转了但人脸框可能还在原来的位置。

我这个项目最初的定位就是做一个通用的头部姿态分析模块,输入摄像头画面,输出三个欧拉角度值,同时支持离线处理视频文件和实时摄像头输入。为了适配不同性能的机器,人脸检测这块做了双引擎支持,Dlib和MTCNN可以自由切换。

1.2 技术选型:Dlib和MTCNN的取舍逻辑

先说Dlib。Dlib的68点人脸关键点检测非常成熟,HOG特征加级联回归器的方案在CPU上跑起来很快。但我最终并没有用它的68点,而是只取了其中6个点。原因后面细说,这里先提一个关键矛盾:Dlib的检测器对正脸效果好,大角度侧脸时会丢点或者检测失败。

MTCNN则是多任务级联卷积网络,它的人脸检测和关键点检测是一体的,只输出5个点(两眼、鼻尖、左右嘴角),比Dlib的68点少很多。但MTCNN的模型本身对侧脸、遮挡、暗光环境的鲁棒性要好于Dlib的传统方法,而且它有PNet、RNet、ONet三级级联结构,在检测人脸的同时还输出人脸框和关键点,接口很干净。

所以我在设计上做了一件事:定义统一的关键点接口层,Dlib返回的68点里抽取需要的6个点,MTCNN返回的5点里对应的坐标直接复用,额外补一个点做姿态解算。这样上层姿态解算逻辑完全不用感知底层用的是哪个检测器,切换引擎只需改一行配置。

1.3 欧拉角的核心定义:偏航角、俯仰角、翻滚角

在入手写代码之前,先把三个物理量讲清楚,因为很多新手搞了半天代码,最后调出来的角度完全不对,根源就是坐标系和角度定义乱了。

偏航角(Yaw)是绕垂直轴(Y轴)旋转的角度,对应的是头部左右转。俯仰角(Pitch)是绕水平横轴(X轴)旋转的角度,对应的是头部上下点头或抬头。翻滚角(Roll)是绕纵深轴(Z轴)旋转的角度,对应的是头部左右歪头。

这三个角度合在一起,就完整定义了头部在三维空间中的朝向。用生活化的类比来理解:偏航角就是你摇头说"不"的时候头转的角度,俯仰角就是你点头说"是"的时候头动的角度,翻滚角就是你歪着头卖萌的时候脖子倾斜的角度。

我在代码里最后输出的角度值,用的是OpenCV默认的坐标系定义加上自定义的映射关系。这里敲个重点:OpenCV的solvePnP返回的旋转向量经过Rodrigues变换后得到旋转矩阵,再从旋转矩阵提取欧拉角时有多种分解方式,不同的分解顺序会得到完全不同的结果。很多人角度算出负数或者转来转去就是个坑,后面实操部分我会给出经过验证的分解代码。

2. 6点面部关键点检测:原理与实操要点

2.1 为什么选6点而不是68点

很多姿态估计教程一上来就让你标68个点,然后用全套关键点做PnP求解。这么做确实精度上限更高,但在实际落地中代价很高:第一,68点检测本身需要较大的计算量;第二,在侧脸、低头、遮挡场景下,68点里不少点会丢失或者偏移;第三,68点里有大量点是面部轮廓和下巴区域的点,这些点对姿态解算的贡献其实很有限,反而可能因为轮廓点的2D定位误差引入噪声。

我最终选定的6个点是:左眼外眼角、右眼外眼角、鼻尖、左嘴角、右嘴角、下巴尖。这6个点几乎都是面部语义明确的锚点,人眼很容易验证坐标是否准确,而且检测器对这几个点的输出稳定性要好得多。

从几何上看,这6个点散布在面部的中轴区域和双眼/嘴巴水平带,能较好地约束旋转自由度的解算。特别是双眼两个点,对偏航角和翻滚角的约束非常强,鼻尖和下巴尖则对俯仰角提供关键约束。

2.2 6点在世界坐标系中的三维坐标定义

姿态估计的本质是求解2D-3D对应关系。你有了图像上的2D像素坐标,还必须有人脸上这6个点的3D坐标参考值。这个3D坐标值需要一个标准人脸模型来定义,简单说就是假设一张正面正对相机的人脸在真实世界坐标系中,各个关键点相对鼻子或某个参考点的大致三维位置。

我在项目中采用的3D参考坐标是经过多次实验修正的经验值,以鼻尖为坐标系原点,单位是毫米级别的近似值:

关键点X坐标Y坐标Z坐标
左眼外眼角-32.036.0-20.0
右眼外眼角32.036.0-20.0
鼻尖0.00.00.0
左嘴角-22.0-28.0-30.0
右嘴角22.0-28.0-30.0
下巴尖0.0-55.0-40.0

这里先说明一点:Z轴负值表示这些点比鼻尖更远离相机。这个模型是从真实人脸比例抽象出来的,没有用真实测量数据精确标定每个人的脸型,但对绝大多数成年人脸来说,用这套参考值反算姿态角已经能满足工程精度。如果你做的是特定人群(比如儿童、特定人种),可以重新标定一套3D模型点,整体的精度会更好。

2.3 Dlib和MTCNN关键点提取的实现差异

Dlib部分我用了官方预训练的shape_predictor_68_face_landmarks.dat模型。加载这个模型后,先用detector检测人脸框,再对每个框做关键点预测,最后从68个点里抽取6个目标点。这里有个容易踩的坑:Dlib的68点索引是从0开始的,眼角的索引并不是直观的左右顺序,需要对照官方文档或者自己画出来验证。

我实际项目中用到的Dlib关键点索引是:

  • 左眼外眼角:第36个点(索引值需注意是36还是39,取决于人脸朝向)
  • 右眼外眼角:第45个点
  • 鼻尖:第30个点
  • 左嘴角:第48个点
  • 右嘴角:第54个点
  • 下巴尖:第8个点

这里特别容易搞错一点:Dlib返回的关键点顺序是按照标准人脸模板排列的,如果你的图片里有左右镜像的情况,左眼角和右眼角指的是图像中的人脸左眼还是观察者的左眼,需要自己在验证阶段多测几张图确认一下。我当时就是在这里绕了一圈,最后打印出68个点可视化才搞定。

MTCNN部分相对省心,它直接输出5个关键点:左眼中心、右眼中心、鼻尖、左嘴角、右嘴角。但问题来了,MTCNN没有输出下巴尖,而我对姿态解算又需要下巴尖。所以在MTCNN模式下,我的处理方式是:用左右眼中心的坐标补一个第四点的垂直投影关系来估算下巴尖位置。具体做法是取鼻尖到两嘴角连线的方向向量,向下延伸一定的距离比例,经验系数大概取0.6到0.8之间。

这个补充点的做法并不完美,但实测下来对俯仰角的计算结果影响不大,因为鼻尖和嘴角的贡献已经能约束俯仰角了,下巴点主要是提供冗余约束。

3. 相机矩阵校准与三维投影变换

3.1 相机内参矩阵为什么不能瞎填

很多入门教程会告诉你,直接填一个假想的相机矩阵,比如fx=fy=600, cx=320, cy=240,然后跑solvePnP就能出角度。这种做法的确能出结果,但结果只在特定条件下有意义:当你的视频帧分辨率刚好是640x480左右,而且镜头畸变很小的时候。

我一开始也是这么干的,后来换了一台像素更高的摄像头,发现同样的头部角度,计算出来的欧拉角完全偏离正常范围,才意识到相机矩阵是姿态估计里最关键的标定环节。

相机内参矩阵包含焦距fx、fy和光心坐标cx、cy,它们描述了三维空间中的点在二维成像平面上的映射关系。如果内参矩阵和实际相机不匹配,solvePnP解出来的旋转矩阵就会有系统性偏差,而且这种偏差不是简单的等比例缩放,是那种角度越大偏得越离谱的差。

3.2 相机标定的具体操作流程

我用的标定方案是OpenCV自带的棋盘格标定工具。找一块标准的棋盘格标定板(我用的是9x6内角点的规格),对着摄像头多角度拍摄20到30张照片,然后跑一遍OpenCV的calibrateCamera函数,就能得到内参矩阵和畸变系数。

标定的过程有几个关键点:

第一,拍摄时棋盘格要占据画面的不同位置,不能只放在正中间,要覆盖画面的四角和边缘,这样标定出的畸变参数才有效。

第二,棋盘格和相机的距离要有变化,从近到远多拍几组,保证标定结果在不同景深下都适用。

第三,如果画面边缘有明显的桶形畸变或者枕形畸变,在标定完成后要用undistort函数对输入图像做去畸变处理。很多视频聊天摄像头和广角摄像头畸变都挺明显的,不做这步的话,关键点的2D坐标本身就有偏差,姿态角就更不用说了。

标定完成后你会得到类似这样的结果:

Camera matrix: [[623.45, 0. , 325.17], [ 0. , 622.83, 248.26], [ 0. , 0. , 1. ]] Distortion coefficients: [0.092, -0.318, 0.0013, -0.0007, 0.384]

把这个矩阵存成配置文件,之后每次启动程序直接读取。需要注意的是,如果你换了摄像头或者改了视频分辨率,标定结果就不能直接沿用,必须重新标定。

3.3 从2D像素点到3D姿态的数学链路

这整个项目的核心数学链路,我用大白话拆解一遍:

第一步,你有了2D像素坐标(6个点的图像坐标),还有对应的3D模型坐标(6个点的标准人脸模型坐标)。

第二步,solvePnP函数通过这6组对应关系,求解一个旋转向量和一个平移向量。旋转向量描述了头部相对相机坐标系的旋转,平移向量描述了头部中心相对相机的位置。

第三步,旋转向量是三维的,但不好直观解读,需要把它转成旋转矩阵。这一步用的是Rodrigues变换,OpenCV里有现成的函数。

第四步,旋转矩阵是一个3x3的矩阵,里面每列元素其实是相机坐标系的基向量在人脸坐标系中的投影。要从这个矩阵里恢复出欧拉角,就需要按照定义的旋转顺序反推。

我在这套系统里采用的分解方式是Yaw-Pitch-Roll的顺序,也就是Z-Y-X(或者按照旋转矩阵列向量来反算)。代码实现一定要对,否则你看到的角度就不是你认知的那个角。

从旋转矩阵R恢复欧拉角的代码是这样的:

def rotation_matrix_to_euler_angles(R): sy = math.sqrt(R[0, 0] * R[0, 0] + R[1, 0] * R[1, 0]) singular = sy < 1e-6 if not singular: x = math.atan2(R[2, 1], R[2, 2]) y = math.atan2(-R[2, 0], sy) z = math.atan2(R[1, 0], R[0, 0]) else: x = math.atan2(-R[1, 2], R[1, 1]) y = math.atan2(-R[2, 0], sy) z = 0 return math.degrees(x), math.degrees(y), math.degrees(z)

这里返回的x对应翻滚角Roll,y对应俯仰角Pitch,z对应偏航角Yaw,但具体哪个对应哪个,要看你的3D模型坐标系定义和solvePnP的输入顺序。建议在项目里加一个调试模式,实时打印三个角度并让人实际转头验证正负方向。

4. 实时人脸姿态分析的完整实现

4.1 环境配置与依赖安装

这个项目的依赖以OpenCV和Dlib为主,Python版本用的3.8,操作系统的兼容性都很好。如果你从零开始搭环境,建议直接用Anaconda建一个虚拟环境,避免系统里已有Python环境被搞乱。

安装命令我直接给出:

pip install opencv-python pip install opencv-contrib-python pip install dlib pip install numpy

MTCNN部分我用的MXNet后端版本,也可以用TensorFlow或者PyTorch的MTCNN实现,接口类似。如果你只需要Dlib版本,MTCNN的依赖可以不装。

这里有个值得注意的问题:某些镜像源装OpenCV的时候会遇到"the function/feature is not implemented"这类报错,这多半是opencv-python和opencv-contrib-python版本不匹配导致的。建议两个包版本保持一致。如果装的是精简版缺少某些模块,直接卸载后换官方源重装就能解。

Dlib在Windows上用pip安装会稍微麻烦一点,因为没有打包好的wheel包,需要先用CMake编译。编译前确保装了Visual Studio的C++开发组件。Linux和macOS一般直接pip安装就行。

4.2 核心代码结构和关键模块实现

我的项目分成了三个模块,职责分离很清晰:

  • face_detector.py:人脸检测器封装,支持Dlib和MTCNN
  • pose_estimator.py:姿态解算,负责PnP求解和欧拉角分解
  • main.py:主循环,负责视频流读取、界面绘制和逻辑串联

先看人脸检测器的核心接口:

class FaceLandmarkDetector: def __init__(self, backend='dlib'): self.backend = backend if backend == 'dlib': self.detector = dlib.get_frontal_face_detector() self.predictor = dlib.shape_predictor("shape_predictor_68_face_landmarks.dat") elif backend == 'mtcnn': from mtcnn import MTCNN self.detector = MTCNN() def get_landmarks(self, frame): # 返回六个关键点的2D像素坐标列表 if self.backend == 'dlib': faces = self.detector(frame, 0) if len(faces) == 0: return None shape = self.predictor(frame, faces[0]) points = [shape.part(36), shape.part(45), shape.part(30), shape.part(48), shape.part(54), shape.part(8)] return [(p.x, p.y) for p in points] else: faces = self.detector.detect_faces(frame) if len(faces) == 0: return None keypoints = faces[0]['keypoints'] # MTCNN 输出的是眼中心,这里用两眼中点向下延伸估算下巴 nose = keypoints['nose'] mouth_left = keypoints['mouth_left'] mouth_right = keypoints['mouth_right'] # 估算下巴尖 chin_y = nose[1] + (mouth_left[1] - nose[1]) * 1.8 points = [keypoints['left_eye'], keypoints['right_eye'], nose, mouth_left, mouth_right, (nose[0], int(chin_y))] return points

姿态解算的模块就更核心了。这里有个很多人忽略的细节:solvePnP默认的求解方式用的是迭代法(ITERATIVE),需要提供一个好的初始解。当人脸姿态变化剧烈的时候,迭代法可能陷入局部最优。我用的是CV_ITERATIVE模式,但参数里加了useExtrinsicGuess=False,让OpenCV自行估计初始值。实测下来,对头部转动范围在正负60度以内的情况,这个方法的精度完全够用。

姿态解算的核心代码是:

def estimate_head_pose(self, landmarks_2d): # 3D model points from reference face model model_points = np.array([ [-32.0, 36.0, -20.0], # 左眼外眼角 [32.0, 36.0, -20.0], # 右眼外眼角 [0.0, 0.0, 0.0], # 鼻尖 [-22.0, -28.0, -30.0], # 左嘴角 [22.0, -28.0, -30.0], # 右嘴角 [0.0, -55.0, -40.0] # 下巴尖 ], dtype=np.float32) image_points = np.array(landmarks_2d, dtype=np.float32) # 相机内参 cam_matrix = np.array(self.camera_matrix, dtype=np.float32) dist_coeffs = np.array(self.distortion_coeffs, dtype=np.float32) success, rotation_vector, translation_vector = cv2.solvePnP( model_points, image_points, cam_matrix, dist_coeffs, flags=cv2.SOLVEPNP_ITERATIVE) if not success: return None # 旋转向量转旋转矩阵 rotation_matrix, _ = cv2.Rodrigues(rotation_vector) # 转欧拉角 roll, pitch, yaw = self.rotation_matrix_to_euler_angles(rotation_matrix) return yaw, pitch, roll

4.3 实时视频流的性能优化和角度平滑

实时运行和离线处理视频有个关键区别:你需要在每帧的间隔内完成检测、特征提取和姿态解算。如果处理速度不稳定,画面就会卡顿,角度的实时性也会受影响。

我在性能调优方面做了三件事:

第一,处理分辨率控制。不要把整张1080P原图直接送给人脸检测器,那样Dlib的速度会掉到个位数帧率。我先把图像按比例缩放到宽度480像素左右,检测到人脸后,把关键点坐标按缩放比例映射回原图。这样既能保证检测速度,又不影响关键点坐标的精度。

第二,检测频率和姿态解算频率解耦。人脸检测器并不是每一帧都需要跑的。我在代码里设置了一个面部跟踪模式:一旦检测到人脸,就只用上一帧的人脸位置周围一定范围内搜索这一帧的关键点,而不是全图重新检测。如果连续几帧都没有找到人脸,才重新跑全图检测。这种方式在单人脸场景下性能提升非常明显。

第三,角度值的平滑处理。由于2D关键点定位本身有噪声,直接输出的欧拉角在每一帧之间会有明显的抖动。我加了一个基于指数移动平均的平滑器:

def smooth_angle(self, new_value, previous_value, alpha=0.6): return alpha * new_value + (1 - alpha) * previous_value

alpha的取值需要根据帧率来调。30帧率下alpha取0.3到0.5效果比较好,太低的话角度响应迟钝,太高的话噪声抑制不足。这个参数建议在实际场景中微调。

5. 常见问题与排查技巧

5.1 遇到的关键问题和解决方案整理

我在这套系统的开发和调试过程中,把遇到的典型问题和排查思路整理成了表格,方便大家按图索骥:

问题现象可能原因解决方案
角度输出一直是同一个值,不随头部转动变化3D模型点和2D关键点对应关系错乱打印关键点坐标并可视化,检查6点顺序是否和3D模型点顺序一致
偏航角正负方向和实际转头方向相反旋转矩阵分解出来的Yaw取反在验证阶段明确约定正方向,必要时对输出取负号
俯仰角在抬头和低头时都正常,但幅度不对3D模型点的Y坐标比例不正确调整3D模型点中鼻尖和下巴的相对距离,重新测量或根据人脸比例修正
画面抖动剧烈,角度噪声很大缺少平滑处理,或者关键点检测本身有噪声加指数移动平均,同时增加关键点检测的置信度判断
侧脸超过60度后人脸丢失Dlib正面人脸检测器限制切换到MTCNN后端,或者增加多角度检测策略
标定后画面仍然有畸变忘记调用undistort或者畸变系数加载错误确认代码中确实把畸变系数传入了去畸变函数
MTCNN模式下下巴点估算不准确下巴点估算系数不合适实测调整系数,或改用ONet的68点扩展输出版本

5.2 精度验证方法和踩坑经验

精度验证是个容易被忽略但非常重要的环节。我在项目里写了一个验证脚本:用手机或相机固定机位拍摄一个人坐在椅子上做缓慢转头动作的视频,导入系统后把三个角度实时绘制成曲线,同时把每一帧的画面保存下来。这样就能直观地看到,在头部转到某个角度时曲线值和实际画面是否对应。

这个验证方法帮我发现了一个隐蔽的问题:当头部转动到很大角度时,2D关键点中鼻尖和嘴角的位置会发生自遮挡,本来应该在一侧的点跑到脸的边缘或者被遮挡了。这时solvePnP的解会突然跳变,角度曲线出现毛刺。解决办法是在关键点检测环节增加一个简单的可见性判断:如果某个点的2D坐标偏离人脸框的范围太远,就舍弃这个点,用剩下的5点来解算。虽然5点求解的约束少了一个,但比用错误坐标强得多。

另外还有一次我遇到了"Unknown/unsupported a"类似的OpenCV报错,排查了半天发现是opencv-python和opencv-contrib-python混装导致solvePnP的某些标志位处理异常。卸载后用同一个来源重新安装,问题就消失了。所以建议所有依赖尽量通过同一个源统一安装,不要混合使用不同渠道的包。

5.3 图像预处理对姿态估计的影响

这部分是我在这个项目中体会很深的一点。最开始我直接把原始视频帧送进检测器,遇到光线不好的环境,人脸检测经常失败。后来我在人脸检测前增加了一个可选预处理开关,用OpenCV的直方图均衡化来提升低光照条件下的对比度。

OpenCV里有两种直方图均衡化方式,全局的equalizeHist和自适应的CLAHE。刚开始我用的是全局均衡化,结果发现它容易导致亮部过曝、暗部过暗,反而损害了关键点定位的稳定性。后来换成了CLAHE(createCLAHE函数),它把图像分成多个小块分别做均衡化,对光照不均的鲁棒性好得多。

clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)) gray = clahe.apply(gray)

这个预处理并不是所有场景都需要开。在光线正常的室内环境下,预处理反而会引入额外的计算开销和可能的人工纹理。我在代码里做了配置开关,默认关闭,只有在用户反馈检测率低或者关键点漂移明显的时候才建议打开。从实际效果来看,在逆光场景下,CLAHE能把人脸检测的成功率提升大概百分之二三十,非常值得一试。

6. 实战调试过程中的一点经验总结

整个项目从零开始到最终跑通,前后大概花了一周多时间。回头复盘,有两个改动对最终效果影响最大。

第一个是把相机内参矩阵从拍脑袋填数值改成了真实标定。这一改,直接让偏航角在左右各30度范围内的输出从"趋势正确但数值偏差大"变成了"和实际角度基本吻合"。那个拍脑袋填出来的内参矩阵,在边缘区域的角度误差能超过10度,而真实标定后误差能控制在3度以内。

第二个是把人脸检测从单纯依赖Dlib改成了双引擎架构。虽然MTCNN在普通正脸场景下速度不如Dlib,但它对侧脸和暗光环境的鲁棒性确实是传统方法比不了的。当Dlib经常丢脸的时候,切到MTCNN往往能稳定检测,而且MTCNN的5点输出本身就足够解算姿态,只是额外估算下巴点那一步需要根据不同的人脸比例微调。

如果你准备在这个项目基础上继续扩展,我个人建议优先考虑两个方向:一是把6点关键点替换成更高精度的语义关键点网络,比如用ONNX导出的移动端模型,精度和速度都能再上一个台阶;二是把当前的帧级姿态输出加上时序滤波,比如卡尔曼滤波或者滑动窗口回归,这样输出的角度曲线会更平滑,更适合需要精确角度的下游应用。

最后再分享一个小技巧:调试姿态估计时,不要只看数值,一定要写一个可视化工具把6个关键点和三个欧拉角实时绘制在画面上,同时把参考坐标系画出来。这样你能立刻发现是哪个环节出了问题——是人脸检测错了、关键点抖了,还是角度分解的正负号方向反了。这种"肉眼可见"的调试方式,比对着终端里刷屏的角度数字去猜要高效得多。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询