简介这份资源聚焦三维视线估计这一计算机视觉细分方向面向具备一定深度学习基础、希望从人眼图像中推导视线方向的研究者与开发者。其核心思路是用深度卷积神经网络对虹膜与瞳孔像素进行分类完成特征提取并跟踪三维眼球的注视状态同时兼顾眼睛状态与头部姿态对视线方向的影响可应用于人机交互、注意力分析等场景。压缩包共37个文件约14.37MB以py脚本为主体辅以pyc缓存、xml与json配置、params权重、tflite轻量模型及npy数据文件另附mp4与avi演示视频、md说明和license目录涵盖人脸检测、对齐、头部姿态、虹膜定位与注视分割等模块。目前已有1287人学习下载。借助现成权重与模块化脚本读者可快速复现从人脸到三维视线的完整流程理解虹膜定位与注视估计的工程实现并对照演示视频验证效果。1. 三维视线估计从「看屏幕哪个位置」到「眼球在空间中的朝向」做眼动人机交互的同行大概率都遇到过这个场景用户坐在显示器前你手里只有一颗普通 RGB 摄像头产品经理却要你判断他到底在看屏幕上的哪个按钮。二维视线估计只能给你一个归一化的屏幕坐标头一动就飘而三维视线估计要输出的是眼球在相机坐标系下的三维方向向量也就是 gaze vector再和屏幕平面求交才能拿到稳定的注视点。这套方案解决的核心问题就是在无红外、无深度相机的条件下把「眼睛看向哪里」变成一个可计算的三维几何问题。它适合做疲劳监测、注意力分析、无鼠标交互的工程师也适合想从零搭一套视线估计 pipeline 的算法同学。下面我按自己落地的顺序把这条链路拆开讲。2. 三维视线估计的几何底座坐标系、眼球模型与误差来源2.1 为什么必须从二维升级到三维二维方法本质是回归一张人脸图到屏幕坐标的映射它把头部姿态、相机内参、人机距离全部隐式编码进了网络权重里。一旦用户往前挪 20 厘米或者相机换了个俯仰角映射关系就崩了。三维方法把问题拆成两步先估计头部姿态和眼球在头坐标系里的朝向再通过刚体变换转到相机坐标系最后和屏幕平面求交。这样相机位置、屏幕位置都成了显式参数换设备只需要重新标定外参模型本身不用重训。这是它比二维方案值钱的地方也是它复杂度上来的原因。2.2 眼球模型球体假设够不够用最常见的做法是把眼球近似成一个固定半径的球体半径取 12mm 左右眼球中心在头坐标系里是个固定偏移量。视线方向就是从眼球中心指向瞳孔中心再往外延伸。这个假设在正视前方时误差很小但眼球转动角度大了以后角膜曲率和瞳孔偏移会带来几度的系统误差。工程上一般接受这个误差因为屏幕求交时几度对应到屏幕上的偏移通常还在可容忍范围。如果做的是高精度眼动仪那就得上角膜反射点做个性化标定普通 RGB 方案做不到也不用强求。2.3 相机模型与屏幕平面求交相机用针孔模型内参矩阵 K 通过棋盘格标定拿到。屏幕在相机坐标系里是一个平面用平面上一点 P0 和法向量 n 表示。给定眼球中心 E 和视线方向 d求交就是解一条射线和一个平面的交点import numpy as np def ray_plane_intersection(E, d, P0, n): E: 眼球中心, shape (3,) d: 视线方向单位向量, shape (3,) P0: 屏幕上一点, shape (3,) n: 屏幕平面法向量, shape (3,) 返回射线与平面交点, 无交点返回 None denom np.dot(d, n) if abs(denom) 1e-6: return None # 视线与屏幕平行 t np.dot(P0 - E, n) / denom if t 0: return None # 交点在眼球后方, 说明方向估计反了 return E t * d这段代码里denom是视线方向和屏幕法向量的夹角余弦接近零说明用户在看屏幕边缘甚至侧后方此时求交数值不稳定直接判无效比硬算更安全。t 0是个很实用的自检如果解出来的交点在眼球后面基本可以断定视线方向估计出了符号错误这是新手最常翻的车之一。2.4 误差是怎么被放大的三维链路里误差会级联。头部姿态差 2 度眼球中心位置就偏几毫米瞳孔中心检测差 1 个像素在 60 厘米距离上方向就差约 0.1 度但经过屏幕求交1 米外可能放大到 1 厘米以上。所以整条链路里瞳孔定位精度和头部姿态精度是两个最敏感的环节后面调参要优先保这两个。3. 用 MediaPipe 自定义头姿跑通最小三维视线 pipeline3.1 关键点从哪来MediaPipe Face Mesh 的 468 点MediaPipe Face Mesh 输出 468 个三维关键点其中眼周点足够定位瞳孔区域和眼角。它给的是归一化图像坐标加一个粗略的 z不能直接当三维用但用来算头部姿态和瞳孔中心足够了。我一般取左右眼各 6 个眼角膜轮廓点做椭圆拟合椭圆中心近似瞳孔中心。注意 MediaPipe 没有直接给瞳孔点网上有些说法说 iris 点能用那是另一套模型别混。3.2 头部姿态解算solvePnP 的坑用 6 个稳定的人脸关键点鼻尖、下巴、左右眼角、左右嘴角和标准三维人脸模型做 solvePnP拿到旋转向量和平移向量。这里有个血泪经验三维人脸模型的尺度必须和真实人脸大致匹配否则解出来的平移量是错的眼球中心位置跟着错。我一般把模型尺度设成真实人脸宽度的量级再用一个已知距离做一次粗标定。import cv2 import numpy as np # 标准三维人脸模型点(单位: 毫米, 近似值) MODEL_POINTS np.array([ [0.0, 0.0, 0.0], # 鼻尖 [0.0, -63.6, -12.5], # 下巴 [-43.3, 32.7, -26.0], # 左眼角 [43.3, 32.7, -26.0], # 右眼角 [-28.9, -28.9, -24.1], # 左嘴角 [28.9, -28.9, -24.1], # 右嘴角 ], dtypenp.float64) def estimate_head_pose(landmarks_2d, camera_matrix, dist_coeffs): landmarks_2d: 对应 MODEL_POINTS 顺序的 6 个图像点, shape (6,2) 返回 rvec, tvec ok, rvec, tvec cv2.solvePnP( MODEL_POINTS, landmarks_2d, camera_matrix, dist_coeffs, flagscv2.SOLVEPNP_ITERATIVE ) if not ok: return None, None return rvec, tvecSOLVEPNP_ITERATIVE在点数少的时候比 EPnP 稳但需要给初值OpenCV 内部会用当前估计迭代。如果发现头部姿态抖动大先检查这 6 个点的图像坐标有没有跳变通常是关键点检测在侧脸时不准导致的不是 solvePnP 的问题。3.3 眼球中心与视线方向的组装拿到头姿的旋转矩阵 R 和平移 t 后眼球中心在头坐标系里取固定偏移比如左右眼各偏 ±32mmz 方向偏 40mm变换到相机坐标系def eye_center_camera(rvec, tvec, eye_offset_head): eye_offset_head: 眼球中心在头坐标系下的偏移, shape (3,) R, _ cv2.Rodrigues(rvec) return (R eye_offset_head.reshape(3, 1) tvec).flatten()视线方向由眼球中心指向瞳孔中心瞳孔中心也要从图像点反投影到三维或者用头坐标系下的瞳孔偏移近似。这一步是整个 pipeline 里最玄学的地方因为瞳孔中心的三维重建依赖深度而单目 RGB 没有可靠深度。我的做法是假设瞳孔在眼球球面上用二维瞳孔位置和眼球半径反推球面点再算方向。这个近似在正脸时够用侧脸会偏。3.4 屏幕求交与坐标映射把屏幕平面在相机坐标系里定义好调用 2.3 的求交函数拿到交点再通过屏幕的局部坐标系映射到像素坐标。屏幕外参标定我一般用屏幕上显示四个已知点让用户依次注视采集对应的视线方向反解屏幕平面。这一步比相机标定还重要屏幕平面法向量差 1 度注视点就能偏出好几厘米。4. 参数怎么调从瞳孔检测到屏幕标定的可复现配置4.1 瞳孔检测的阈值与平滑MediaPipe 眼周点做椭圆拟合时轮廓点的选取范围直接决定瞳孔中心稳定性。我一般取上下眼睑各 4 个点加左右眼角共 10 个点左右太多会把眼睑抖动带进来。拟合出来的中心做一阶低通滤波系数 0.3 到 0.5 之间太小延迟大太大抖动明显。这个系数没有理论最优得看帧率和用户头部运动速度我通常先用 0.4 跑一段录屏再定。4.2 头部姿态的稳定性处理solvePnP 出来的 rvec 在相邻帧之间可能跳变尤其是关键点检测置信度低的时候。做法是对旋转向量做球面插值平滑或者直接对旋转矩阵做低通。注意不要对欧拉角直接滤波万向锁会让角度在边界处跳。我一般把 rvec 转成旋转矩阵再滤波然后转回去。4.3 屏幕标定的采样策略屏幕标定至少采 9 个点3x3 网格覆盖屏幕四角和中心。每个点让用户注视 1 到 2 秒取这段时间内视线方向的均值。采样时提醒用户尽量保持头部不动但不要僵住自然状态下的标定结果更鲁棒。采完用最小二乘拟合屏幕平面残差大的点剔除再拟合一次。4.4 一套可用的初始参数表参数建议初值调整方向眼球半径12 mm偏大注视点外扩偏小内收瞳孔滤波系数0.4抖动大调小延迟大调大头姿滤波系数0.5同上屏幕标定点数9精度不够加到 16求交最小余弦0.1低于此判无效眼球中心偏移±32, 40 mm按人脸尺度微调这张表是我起手常用的具体数值要按相机焦距和人机距离微调。别指望一套参数通吃换相机就得重标。5. 避坑与排查三维视线估计里最容易翻车的 5 个地方5.1 注视点整体偏移一个固定量现象所有注视点都往同一个方向偏比如统一偏左。原因屏幕平面外参标定有系统偏差或者眼球中心偏移量设错了。解决先检查屏幕标定的残差如果残差本身很小但整体偏那就是眼球中心偏移的问题微调偏移量直到中心注视点对准。5.2 头部一动注视点就飞现象用户转头时注视点剧烈跳动。原因头姿和视线方向的时序不同步或者头姿滤波延迟太大。解决检查头姿和瞳孔检测是不是同一帧的数据滤波系数调小必要时用预测补偿。5.3 侧脸时视线方向完全错误现象用户侧脸超过 30 度后估计的视线方向指向奇怪的地方。原因单目 RGB 在侧脸时瞳孔被遮挡椭圆拟合退化且眼球球面假设误差变大。解决加一个头部姿态有效性判断超过阈值就输出无效别硬算。这是方案本身的边界不是调参能解决的。5.4 不同用户精度差异巨大现象有的人准有的人怎么标都不准。原因眼球半径和眼球中心偏移是个体差异戴眼镜的用户还有镜片折射。解决给每个用户做一次快速个性化标定至少拟合眼球半径这一个参数能明显改善。5.5 帧率一高就抖动现象低帧率还行高帧率反而抖。原因高帧率下相邻帧噪声相关性低滤波没跟上。解决滤波系数按帧率重新整定或者做多帧加权平均。别直接套用低帧率的参数。6. 进阶用个性化标定把误差压到 2 度以内前面讲的都是通用 pipeline真正让三维视线估计从「能用」到「好用」的是个性化标定。我一般让用户注视屏幕上 9 个点每个点采集 30 帧视线方向然后用这些数据拟合三个参数眼球半径、眼球中心在头坐标系下的偏移、屏幕平面的微调量。拟合用最小二乘目标函数是重投影到屏幕的像素误差。from scipy.optimize import least_squares def calibrate(params, gaze_data, screen_params): params: [eye_radius, offset_x, offset_y, offset_z] gaze_data: 每项为 (rvec, tvec, pupil_2d, target_2d) screen_params: 屏幕平面参数 返回重投影误差向量 eye_radius, ox, oy, oz params errors [] for rvec, tvec, pupil_2d, target_2d in gaze_data: # 用当前参数重算视线方向和注视点 pred compute_gaze_point(rvec, tvec, pupil_2d, eye_radius, (ox, oy, oz), screen_params) errors.extend(pred - target_2d) return np.array(errors) result least_squares(calibrate, x0[12.0, 32.0, 32.0, 40.0], args(gaze_data, screen_params), bounds([8, 20, 20, 20], [16, 45, 45, 60]))这段拟合里bounds很关键不加边界优化器可能跑出物理上不合理的值比如眼球半径 3mm。compute_gaze_point就是前面 2.3 和 3.3 的组合。拟合完把参数存下来下次这个用户直接用精度通常能从 5 度左右压到 2 度以内。验证方法我习惯用留一法9 个标定点里留一个不参与拟合用拟合好的参数预测那个点看误差。如果留一误差和训练误差差很多说明过拟合了标定点要加。这个习惯帮我省过好几次「标定看着好、实际用起来飘」的尴尬。最后说个我自己的教训别一上来就追求高精度先把整条链路跑通哪怕误差 10 度只要方向对再逐步加标定和滤波。我见过太多人卡在瞳孔检测那一步反复调结果整条 pipeline 从来没端到端跑起来过。先跑通再优化这个顺序别反。希望帮到你。本文还有配套的精品资源点击获取