ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

三维视线估计实战:从几何模型到深度学习端到端方案

三维视线估计实战:从几何模型到深度学习端到端方案 简介这份资源围绕三维视线估计展开面向计算机视觉与深度学习方向的学习者和开发者解决从眼睛或人脸图像中推导视线方向的问题。其核心思路是用深度卷积神经网络对虹膜与瞳孔像素进行分类提取特征并跟踪三维眼球的注视状态同时兼顾眼睛状态与头部姿态的影响可应用于人机交互、注意力分析等场景。压缩包共37个文件约14.37MB包含11个Python脚本、6个pyc、4个xml、3个params、3个json以及tflite、npy、mp4、avi等模型与演示文件覆盖人脸检测、对齐、头部姿态估计、虹膜定位与注视分割等模块并附参考链接与说明文档。目前已有1287人学习下载。读者可据此了解三维视线估计的完整实现流程参考模型权重与脚本组织方式结合演示视频直观理解注视跟踪效果适合作为课程设计或相关课题的实践起点。1. 从一张普通摄像头照片到三维视线向量这件事到底难在哪你面前放着一台普通 RGB 摄像头画面里一个人正看着屏幕。你想知道他的视线在三维空间里指向哪里——不是「大概朝左还是朝右」而是能算出俯仰角、偏航角甚至能反推出他到底在看屏幕上的哪个像素点。这就是对人眼进行三维视线估计要解决的问题。它和传统的二维视线分类有本质区别。二维方案只给你「左/右/上/下/中」几个离散标签而三维视线估计输出的是一个连续的三维方向向量通常用偏航角 yaw 和俯仰角 pitch 两个角度表示。这个向量可以直接和屏幕平面求交得到注视点坐标。听起来只是多了一个维度但落地难度是数量级的差异。适合谁看如果你在做驾驶员注意力监测、远程会议视线追踪、无障碍交互、或者零售场景的顾客关注度分析三维视线估计就是核心能力。新手可以跟着后面的步骤跑通一个最小系统熟手可以直接跳到参数调优和避坑部分。整条链路涉及人脸检测、眼部关键点定位、头部姿态估计、视线向量回归四个环节每个环节都有翻车的可能。2. 三维视线估计的技术路线选型从模型驱动到数据驱动2.1 两条主流路线的本质区别做三维视线估计绕不开一个根本选择你是走几何模型驱动的路线还是走数据驱动回归的路线。几何模型驱动的核心思路是把人眼近似成一个球体瞳孔中心相对于眼球中心的偏移量决定了视线方向。具体做法是先用关键点定位出眼角、瞳孔、虹膜的位置然后通过头部姿态估计得到头部坐标系再把瞳孔偏移量从图像坐标系变换到世界坐标系最终解算出视线向量。这条路线的好处是可解释性强你清楚每一步在算什么参数可以手动调。坏处是对关键点精度极其敏感瞳孔定位偏两个像素视线角度可能就差好几度。数据驱动回归的思路是直接训练一个神经网络输入眼部区域图像加头部姿态信息输出 yaw 和 pitch。这条路线在近几年的公开数据集上表现更好泛化能力也更强但需要大量标注数据而且模型是个黑匣子出了问题不好排查。我一般的做法是如果目标场景固定比如驾驶员监控摄像头位置和角度基本不变走几何模型驱动调参可控如果场景多变比如手机前置摄像头用户手持角度随机走数据驱动用预训练模型做迁移。2.2 最小可复现系统的搭建步骤下面用一个具体的例子把整条链路串起来。目标输入一张人脸照片输出左右眼的视线角度。第一步人脸检测与关键点定位import cv2 import numpy as np import mediapipe as mp # 初始化 MediaPipe 人脸网格模型 mp_face_mesh mp.solutions.face_mesh face_mesh mp_face_mesh.FaceMesh( static_image_modeTrue, max_num_faces1, refine_landmarksTrue, # 关键开启虹膜关键点检测 min_detection_confidence0.5 ) # 读取图像 image cv2.imread(face.jpg) rgb_image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) results face_mesh.process(rgb_image) # 提取关键点 if results.multi_face_landmarks: landmarks results.multi_face_landmarks[0] h, w image.shape[:2] # 左眼虹膜中心MediaPipe 索引 468 left_iris landmarks.landmark[468] # 右眼虹膜中心索引 473 right_iris landmarks.landmark[473] # 左眼内外眼角索引 33 和 133 left_inner landmarks.landmark[133] left_outer landmarks.landmark[33] # 右眼内外眼角索引 362 和 263 right_inner landmarks.landmark[362] right_outer landmarks.landmark[263]这里refine_landmarksTrue是必须开的否则拿不到虹膜关键点。MediaPipe 的虹膜索引 468 和 473 分别对应左右眼虹膜中心这是整个链路里最关键的输入。min_detection_confidence设 0.5 是保守值如果画面质量差可以降到 0.3但会引入误检。第二步头部姿态估计# 用 solvePnP 估计头部姿态 # 3D 人脸模型点通用人脸模型的六个基准点 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) # 对应的 2D 图像点从 landmarks 中提取 image_points np.array([ [landmarks.landmark[1].x * w, landmarks.landmark[1].y * h], # 鼻尖 [landmarks.landmark[152].x * w, landmarks.landmark[152].y * h], # 下巴 [landmarks.landmark[33].x * w, landmarks.landmark[33].y * h], # 左眼外角 [landmarks.landmark[263].x * w, landmarks.landmark[263].y * h], # 右眼外角 [landmarks.landmark[61].x * w, landmarks.landmark[61].y * h], # 左嘴角 [landmarks.landmark[291].x * w, landmarks.landmark[291].y * h] # 右嘴角 ], dtypenp.float64) # 相机内参假设无畸变焦距取图像宽度 focal_length w center (w / 2, h / 2) camera_matrix np.array([ [focal_length, 0, center[0]], [0, focal_length, center[1]], [0, 0, 1] ], dtypenp.float64) dist_coeffs np.zeros((4, 1)) # 假设无畸变 success, rotation_vector, translation_vector cv2.solvePnP( model_points, image_points, camera_matrix, dist_coeffs, flagscv2.SOLVEPNP_ITERATIVE )solvePnP输出的是旋转向量和平移向量。旋转向量可以通过 Rodrigues 变换转成旋转矩阵再分解出 yaw、pitch、roll 三个角度。这里用的是通用 3D 人脸模型如果你有特定场景的标定数据替换model_points会显著提升精度。SOLVEPNP_ITERATIVE适合点数少的情况点数多可以用SOLVEPNP_EPNP。第三步视线向量解算# 将旋转向量转为旋转矩阵 rotation_matrix, _ cv2.Rodrigues(rotation_vector) # 计算头部坐标系下的视线方向 # 左眼虹膜中心相对于眼角的偏移 left_eye_center np.array([ (left_inner.x left_outer.x) / 2 * w, (left_inner.y left_outer.y) / 2 * h ]) left_iris_2d np.array([left_iris.x * w, left_iris.y * h]) left_offset left_iris_2d - left_eye_center # 归一化偏移量以眼宽为单位 left_eye_width np.linalg.norm( np.array([left_inner.x - left_outer.x, left_inner.y - left_outer.y]) * [w, h] ) left_offset_normalized left_offset / left_eye_width # 在头部坐标系下构造视线向量 # 假设眼球半径对应的偏移系数 k需要标定 k 1.2 # 经验值不同人种和个体有差异 gaze_local np.array([ left_offset_normalized[0] * k, left_offset_normalized[1] * k, -1.0 # z 轴指向正前方 ]) gaze_local gaze_local / np.linalg.norm(gaze_local) # 变换到世界坐标系 gaze_world rotation_matrix gaze_local # 计算 yaw 和 pitch yaw np.degrees(np.arctan2(gaze_world[0], -gaze_world[2])) pitch np.degrees(np.arcsin(gaze_world[1])) print(f左眼视线: yaw{yaw:.2f}°, pitch{pitch:.2f}°)这段代码的核心逻辑是先在头部坐标系下算出视线方向再通过旋转矩阵变换到世界坐标系。k值是眼球半径和眼宽的比例系数一般取 1.0 到 1.5 之间需要根据具体场景标定。gaze_local的 z 轴分量设为 -1.0 是因为相机坐标系下 z 轴指向屏幕外视线正前方对应负 z。2.3 参数标定与精度验证上面代码里有两个关键参数需要标定k值和相机焦距。k值决定了瞳孔偏移到视线角度的映射比例焦距决定了头部姿态估计的精度。标定方法很简单让测试者依次注视屏幕上五个已知位置的点屏幕四角和中心记录每次的left_offset_normalized和实际视线角度用最小二乘法拟合出k。焦距可以用棋盘格标定板做标准相机标定或者直接用图像宽度作为近似值——后者在摄像头距离人脸超过 50cm 时误差可以接受。验证精度时用注视点误差Gaze Error作为指标计算预测视线与屏幕的交点和实际注视点的欧氏距离。桌面场景下好的系统能做到 3-5 度以内的平均角度误差对应屏幕上大约 5-8 厘米的偏差。3. 数据驱动方案用深度学习模型做端到端回归3.1 什么时候该上深度学习几何模型驱动在受控环境下够用但遇到以下情况就会力不从心用户戴眼镜镜片折射导致瞳孔位置偏移、光照剧烈变化虹膜对比度下降、头部大角度偏转关键点被遮挡。这些场景下数据驱动方案的优势就体现出来了。常见做法是用一个 backbone 网络比如 ResNet-18 或 MobileNetV3提取眼部区域特征拼接头部姿态角度后送入全连接层回归 yaw 和 pitch。训练数据可以用公开数据集也可以用自己采集的数据做微调。3.2 训练数据的准备与增强import torch from torch.utils.data import Dataset, DataLoader import torchvision.transforms as transforms class GazeDataset(Dataset): def __init__(self, image_paths, head_poses, gaze_labels, transformNone): self.image_paths image_paths self.head_poses head_poses # (N, 2) 数组每行是 yaw, pitch self.gaze_labels gaze_labels # (N, 2) 数组每行是 yaw, pitch self.transform transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): image cv2.imread(self.image_paths[idx]) image cv2.cvtColor(image, cv2.COLOR_BGR2RGB) if self.transform: image self.transform(image) head_pose torch.tensor(self.head_poses[idx], dtypetorch.float32) gaze torch.tensor(self.gaze_labels[idx], dtypetorch.float32) return image, head_pose, gaze # 数据增强只对图像做不对标签做 train_transform transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.ColorJitter(brightness0.3, contrast0.3), transforms.RandomHorizontalFlip(p0.0), # 注意视线估计不能水平翻转 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])这里有一个血泪教训RandomHorizontalFlip在视线估计任务里绝对不能开。水平翻转图像后左眼变右眼视线方向也镜像了但标签没有对应翻转模型直接学废。同理RandomRotation也要慎用小角度±5度可以大角度会破坏视线和头部姿态的对应关系。3.3 模型定义与训练循环import torch.nn as nn from torchvision.models import resnet18 class GazeNet(nn.Module): def __init__(self): super().__init__() self.backbone resnet18(pretrainedTrue) # 替换最后的全连接层 self.backbone.fc nn.Identity() # 图像特征 512 维 头部姿态 2 维 self.regressor nn.Sequential( nn.Linear(512 2, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, 128), nn.ReLU(), nn.Linear(128, 2) # 输出 yaw, pitch ) def forward(self, image, head_pose): features self.backbone(image) combined torch.cat([features, head_pose], dim1) return self.regressor(combined) # 训练配置 model GazeNet().cuda() optimizer torch.optim.Adam(model.parameters(), lr1e-4) criterion nn.MSELoss() for epoch in range(50): model.train() total_loss 0 for images, head_poses, gazes in train_loader: images images.cuda() head_poses head_poses.cuda() gazes gazes.cuda() optimizer.zero_grad() outputs model(images, head_poses) loss criterion(outputs, gazes) loss.backward() optimizer.step() total_loss loss.item() print(fEpoch {epoch1}, Loss: {total_loss/len(train_loader):.4f})损失函数用 MSE 是最直接的但如果你更关心角度误差而不是数值误差可以用1 - cos(θ)作为损失其中 θ 是预测向量和真实向量的夹角。学习率 1e-4 是微调预训练模型的常用值如果从零训练可以调到 1e-3。Dropout 设 0.3 是为了防止过拟合数据量大的话可以降到 0.1。3.4 推理阶段的性能优化训练完的模型要部署到实际场景推理速度是关键。ResNet-18 在 GPU 上单帧推理大约 5ms但在 CPU 上可能超过 50ms。如果目标是实时应用30fps要么用 MobileNetV3 替换 backbone要么做模型量化。# 动态量化示例 model_cpu GazeNet() model_cpu.load_state_dict(torch.load(gaze_model.pth, map_locationcpu)) model_cpu.eval() quantized_model torch.quantization.quantize_dynamic( model_cpu, {nn.Linear}, dtypetorch.qint8 ) # 量化后模型大小约为原来的 1/4CPU 推理速度提升 2-3 倍量化只对全连接层做卷积层保持浮点。这样精度损失通常在 0.5 度以内但速度提升明显。如果还嫌慢可以考虑把输入分辨率从 224 降到 112精度会掉 1-2 度但速度翻倍。4. 避坑与排查三维视线估计的五个典型翻车现场4.1 瞳孔定位漂移导致角度跳变现象静止状态下视线角度输出在 ±3 度范围内随机跳动画面稍微一晃就跳到 ±10 度。原因虹膜关键点检测不稳定。MediaPipe 在眼部区域对比度低比如深色虹膜配深色皮肤或者有眼镜反光时虹膜中心会在几个像素之间来回跳。几何模型驱动方案对这个尤其敏感因为瞳孔偏移量直接乘以系数 k 得到视线角度两个像素的抖动可能放大成好几度。解决加时间域滤波。最简单的是滑动平均窗口大小取 5 帧。更好的是用卡尔曼滤波把视线角度作为状态量角速度作为控制量。如果延迟允许可以用 One Euro Filter它在低速时平滑、高速时响应快适合视线这种既有静止注视又有快速扫视的信号。4.2 头部姿态估计的万向节死锁现象当用户头部大角度偏转yaw 超过 ±60 度时pitch 角度突然翻转或剧烈跳动。原因用欧拉角表示旋转时当 yaw 接近 ±90 度会出现万向节死锁pitch 和 roll 的自由度耦合。solvePnP输出的旋转向量转欧拉角时如果分解顺序不对就会在特定角度出现跳变。解决不要在欧拉角空间做插值或滤波用旋转矩阵或四元数。如果必须输出欧拉角确保分解顺序和你的应用场景匹配。对于视线估计推荐用ZYX顺序先绕 z 轴 yaw再绕 y 轴 pitch最后绕 x 轴 roll这样 yaw 的奇点在 ±90 度而人眼正常注视范围不会超过 ±60 度。4.3 眼镜用户的系统性偏差现象戴眼镜的用户视线估计结果整体偏向某个方向误差比不戴眼镜的用户大 2-3 倍。原因镜片折射改变了瞳孔在图像中的表观位置。近视镜片会让瞳孔看起来比实际位置更靠近镜片中心远视镜片则相反。这个偏移量和镜片度数、镜片到眼球的距离都有关。解决两个思路。一是数据驱动方案里加入戴眼镜的样本做微调让模型自己学习补偿。二是几何方案里加一个眼镜补偿项根据检测到的镜片区域估计折射偏移量。实际操作中如果目标用户群体里戴眼镜的比例高直接在训练数据里保证 30% 以上是戴眼镜样本比事后补偿更有效。4.4 光照变化导致虹膜对比度下降现象在侧光或逆光环境下虹膜和巩膜的边界模糊关键点定位精度急剧下降。原因MediaPipe 和大多数关键点检测模型都是在均匀光照数据上训练的遇到强侧光时虹膜区域过曝或过暗纹理特征丢失。解决预处理阶段加自适应直方图均衡化CLAHE限制对比度增强的幅度避免噪声放大。如果硬件允许加一个红外补光灯虹膜在红外波段下的对比度比可见光高得多而且不受环境光变化影响。这是驾驶员监控系统的标准做法。4.5 训练集和测试集的头部姿态分布不匹配现象模型在测试集上整体误差不大但在某些头部姿态下误差突然增大。原因训练数据里头部姿态分布不均匀。公开数据集比如 GazeCapture里正脸样本占大多数大角度偏转的样本很少。模型在训练时没见过多少大角度样本自然学不好。解决做数据平衡。统计训练集里 yaw 和 pitch 的分布对稀疏区域做过采样或数据增强。增强方法包括在 3D 空间旋转头部模型生成新样本或者用 GAN 生成不同角度的眼部图像。最简单有效的是在损失函数里给稀疏区域的样本更高权重让模型更关注这些难样本。5. 把视线向量落到屏幕坐标从角度到像素的最后一公里前面得到的 yaw 和 pitch 是视线方向的角度表示但实际应用里你往往需要知道用户在看屏幕上的哪个位置。这一步叫注视点映射Gaze Mapping是把三维视线向量和屏幕平面求交。假设屏幕在相机坐标系下的平面方程是z dd 是屏幕到相机的距离视线起点是眼球中心在相机坐标系下的位置(ex, ey, ez)视线方向是单位向量(gx, gy, gz)。参数方程是x ex t * gx y ey t * gy z ez t * gz令z d解出t (d - ez) / gz然后代入得到x和y就是注视点在屏幕平面上的坐标。再根据屏幕的物理尺寸和分辨率换算成像素坐标。def gaze_to_screen(gaze_vector, eye_center, screen_distance, screen_width_cm, screen_height_cm, screen_width_px, screen_height_px): gaze_vector: (gx, gy, gz) 单位向量相机坐标系 eye_center: (ex, ey, ez) 眼球中心在相机坐标系下的位置单位 cm screen_distance: 屏幕到相机的距离单位 cm gx, gy, gz gaze_vector ex, ey, ez eye_center # 避免 gz 接近 0 导致除零 if abs(gz) 1e-6: return None t (screen_distance - ez) / gz if t 0: return None # 视线指向相机后方 screen_x_cm ex t * gx screen_y_cm ey t * gy # 换算到像素坐标假设屏幕中心在相机光轴上 px (screen_x_cm / screen_width_cm 0.5) * screen_width_px py (0.5 - screen_y_cm / screen_height_cm) * screen_height_px return int(px), int(py)这里有几个容易忽略的细节。第一眼球中心的位置eye_center不是固定的它随头部运动变化需要从头部姿态估计的结果里推算。第二屏幕平面不一定垂直于相机光轴如果屏幕有倾斜平面方程要改成一般式ax by cz d 0。第三t 0的判断很重要它过滤掉视线指向相机后方的无效情况。实际部署时我习惯在屏幕四角贴红外标记点用相机同时检测标记点来实时计算屏幕平面方程。这样即使用户移动了屏幕或者相机被碰歪了系统也能自动校正。这个技巧在远程会议场景里特别有用因为用户经常会调整显示器角度。精度验证的方法让测试者注视屏幕上随机出现的 9 个点每个点注视 2 秒记录预测坐标和实际坐标的偏差。好的系统在 60cm 距离下能做到平均误差小于 5cm对应大约 4-5 度的角度误差。如果误差超过 8cm优先检查相机标定和k值是否准确这两个是影响最大的因素。最后说一个我踩过的坑不要用训练集上的误差来评估系统精度。训练集里的头部姿态和光照条件都是模型见过的误差会明显偏低。一定要留一个独立的测试集最好是在不同时间段、不同光照条件下采集的。我当初就是被训练集上 2 度的误差骗了实际部署到新环境后误差直接翻倍。希望帮到你。本文还有配套的精品资源点击获取
返回列表