资讯动态

用普通摄像头实现眼动追踪:从原理到代码的完整方案

发布时间:2026/10/19 9:00:07 来源:尧图企业网站定制
1. 为什么我会盯上“摄像头替代眼动仪”这个方向第一次听到“用普通摄像头做眼动追踪”这个想法是在一个做无障碍交互的小圈子里。当时有人提了一句专业的眼动仪动辄几万块标定流程还特别繁琐能不能用笔记本自带的那颗摄像头凑合一下这个念头一冒出来就压不住了。我自己前后折腾过三套方案从最粗糙的灰度阈值法到后来基于特征点回归的模型踩的坑足够写一本小册子。这篇文章就把我这一路摸出来的经验完整摊开讲从原理到代码到调参尽量让后来的人少走弯路。先把话说清楚摄像头替代眼动仪指的是用普通的RGB摄像头比如笔记本前置摄像头、手机前置摄像头、或者一个几十块的USB摄像头通过计算机视觉和机器学习的方法估算出人眼在屏幕上的注视点坐标。它做不到专业红外眼动仪那种0.5度以内的精度但在很多场景下——比如注意力粗估、无障碍鼠标替代、游戏交互、用户行为分析——已经够用了。适合谁来读有一定Python基础、懂一点OpenCV、想快速搭一个能跑的原型的人。如果你完全没写过代码也能看懂原理部分但实操章节需要你动手补一些基础。核心关键词我先摆出来人脸关键点检测、瞳孔中心定位、视线向量回归、头部姿态补偿、标定映射、平滑滤波。这几个词贯穿全文后面每一节都会反复出现。2. 整体方案设计与技术选型思路2.1 为什么不用红外普通摄像头到底能不能行专业眼动仪的核心原理是瞳孔角膜反射法PCCR用红外光源照射眼睛角膜上会形成一个稳定的反光点glint瞳孔中心相对于这个反光点的位置就能推算出视线方向。红外的好处是瞳孔在红外下对比度极高而且glint不受环境光影响所以精度能做到很高。普通摄像头没有红外光源也没有glint参考点只能靠瞳孔中心相对于眼眶的位置来估算视线。这就带来两个天然缺陷一是瞳孔定位精度受光照影响大二是没有绝对参考点必须依赖用户标定。但它的优势也很明显零硬件成本、无需额外设备、用户接受度高。我的判断是在头部相对固定的场景下比如坐在电脑前普通摄像头方案完全可用一旦头部大幅移动精度会断崖式下跌这时候就需要头部姿态补偿来救场。2.2 三条技术路线的取舍我实际试过的路线有三条这里做个横向对比方便你按自己的需求选。路线核心方法精度实现难度适用场景阈值法灰度阈值轮廓找瞳孔低极低快速验证、光照可控特征回归法人脸关键点回归模型中中通用场景、推荐深度学习端到端CNN直接回归注视点中高高有标注数据、追求精度阈值法我最早试用cv2.threshold把暗色瞳孔分割出来再找最大轮廓的质心。问题是眉毛、眼镜框、阴影全都会干扰稍微换个光照就崩。特征回归法是我现在主推的用现成的人脸关键点模型比如68点或478点拿到眼睛区域再在眼睛ROI里精定位瞳孔最后用一个回归模型把瞳孔位置头部姿态映射到屏幕坐标。端到端深度学习精度最好但需要大量带标注的注视数据个人开发者很难搞到所以我这篇重点讲第二条路线。2.3 整体数据流设计整个系统的数据流我设计成这样摄像头采集帧 → 人脸检测 → 关键点提取 → 眼睛ROI裁剪 → 瞳孔中心定位 → 头部姿态估计 → 特征拼接 → 回归映射 → 屏幕坐标 → 平滑滤波 → 输出。每一环都有坑后面逐环拆。这里先强调一个设计原则能用人脸关键点直接拿到的信息就不要自己从头算。比如眼睛的粗略位置关键点模型已经给了你没必要再去全图找瞳孔直接在眼睛ROI里做速度和鲁棒性都会好很多。3. 核心细节解析与实操要点3.1 人脸关键点检测选对模型省一半力气人脸关键点检测是整个流水线的地基。我试过dlib的68点、MediaPipe的468点、还有几个开源的人脸对齐库。实测下来MediaPipe Face Mesh在普通摄像头上的表现最稳468个点里包含了非常精细的眼部轮廓甚至能拿到上下眼睑的多个点这对后续瞳孔定位帮助极大。dlib的68点眼部只有6个点精度差不少而且dlib在侧脸时容易丢点。用MediaPipe的代码大概长这样import mediapipe as mp import cv2 mp_face_mesh mp.solutions.face_mesh face_mesh mp_face_mesh.FaceMesh( max_num_faces1, refine_landmarksTrue, # 关键开启虹膜细化 min_detection_confidence0.5, min_tracking_confidence0.5 ) cap cv2.VideoCapture(0) while cap.isOpened(): ret, frame cap.read() if not ret: break rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results face_mesh.process(rgb) if results.multi_face_landmarks: landmarks results.multi_face_landmarks[0] # 后续处理 cv2.imshow(frame, frame) if cv2.waitKey(1) 0xFF 27: break注意refine_landmarksTrue这个参数一定要开它会在眼睛区域额外输出虹膜的关键点左右眼各4个虹膜点1个瞳孔中心点这是普通模式没有的。很多人不知道这个参数白白浪费了MediaPipe最强的能力。3.2 瞳孔中心定位虹膜点直接用还是自己算开了refine_landmarks之后MediaPipe会给出虹膜的中心点索引左眼468右眼473。我一开始直接拿这个点当瞳孔中心用结果发现它在眼睛半闭或者快速眨眼时会飘。后来改成用虹膜4个边界点的均值作为瞳孔中心稳定性明显提升。具体做法是取左眼虹膜点469、470、471、472求它们的平均坐标。但这里有个细节MediaPipe输出的坐标是归一化的0到1之间需要乘以图像宽高转成像素坐标。而且它给的是相对于整帧的坐标如果你裁剪了ROI还要做坐标变换。我踩过的坑是忘了做这个变换导致瞳孔位置整体偏移标定怎么都标不准排查了大半天才发现是坐标系没对齐。3.3 头部姿态估计被大多数人忽略的关键环节头部姿态是普通摄像头方案里最容易被低估的一环。你想想头稍微转一下瞳孔在眼眶里的位置就变了但你的视线其实没动。如果不补偿头部姿态标定好的映射关系立刻就失效。我的做法是用solvePnP拿人脸关键点里的几个稳定点鼻尖、下巴、左右眼角、左右嘴角作为3D参考点反解出头部的旋转向量和平移向量。import numpy as np # 3D参考点通用人脸模型单位mm 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图像点从关键点里取 image_points np.array([...], dtypenp.float64) camera_matrix np.array([ [frame_width, 0, frame_width/2], [0, frame_width, frame_height/2], [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 )拿到旋转向量后用cv2.Rodrigues转成旋转矩阵再分解出俯仰角pitch、偏航角yaw、滚转角roll。这三个角度作为特征喂给后面的回归模型。实测下来加上头部姿态特征后侧头30度以内的精度能提升40%以上。3.4 特征工程到底喂什么给回归模型特征选得好模型事半功倍。我最终用的特征向量包含这几类瞳孔中心在眼睛ROI内的归一化坐标左右眼各2维、眼睛ROI的宽高2维、头部姿态三角度3维、瞳孔中心相对于眼角的位置4维。加起来大概15维左右。特征不是越多越好我试过把468个关键点全塞进去结果过拟合严重标定数据稍微少一点就崩。实操心得特征做归一化非常重要。瞳孔坐标除以眼睛宽度头部角度除以90度把所有特征压到相近的量级。我一开始没做归一化回归模型收敛极慢调了半天学习率才发现是量纲问题。4. 完整实操流程与核心环节实现4.1 环境搭建与依赖安装先把环境弄干净。我推荐用conda建一个独立环境避免和系统里的包打架。conda create -n gaze python3.9 conda activate gaze pip install opencv-python mediapipe numpy scikit-learn matplotlib版本上有个坑MediaPipe对Python版本比较挑3.9到3.11最稳3.12有时候会装不上。OpenCV用4.x就行别用太老的3.x有些API对不上。scikit-learn用来做回归如果你要上深度学习再装PyTorch但本文的方案用sklearn就够了。4.2 标定流程设计与实现标定是整个系统精度的命门。我的标定流程是屏幕上依次显示9个或16个点用户盯着每个点看采集这段时间内的特征向量和对应的屏幕坐标组成训练集。这里有几个关键细节第一每个点采集的时间要够。我一开始每个点只采0.5秒结果数据噪声大得没法用。后来改成每个点采集2秒取这段时间内特征的中位数不是均值中位数抗 outlier效果立竿见影。第二点与点之间要有过渡提示。用户从一个点移到下一个点需要时间如果直接切换采集到的数据里混着移动过程中的样本。我的做法是每个点先显示1秒的引导动画等用户视线稳定后再开始采集。第三标定点要覆盖屏幕边缘。很多人标定只标中间区域结果看屏幕角落时误差巨大。9点标定要均匀铺满整个屏幕边距留5%左右。标定数据采集的代码框架calibration_points [(x, y) for x in [0.1, 0.5, 0.9] for y in [0.1, 0.5, 0.9]] calibration_data [] for point in calibration_points: show_point_on_screen(point) time.sleep(1.0) # 引导期 samples [] start time.time() while time.time() - start 2.0: features get_current_features() if features is not None: samples.append(features) if samples: median_feature np.median(samples, axis0) calibration_data.append((median_feature, point))4.3 回归模型训练与映射标定数据拿到后训练一个回归模型把特征映射到屏幕坐标。我用的是岭回归Ridge Regression简单、快、不容易过拟合。也试过SVR和随机森林SVR精度略好但训练慢随机森林容易过拟合小样本。岭回归在这个任务上性价比最高。from sklearn.linear_model import Ridge from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline X np.array([d[0] for d in calibration_data]) y np.array([d[1] for d in calibration_data]) model make_pipeline(StandardScaler(), Ridge(alpha1.0)) model.fit(X, y) # 预测 predicted_screen_pos model.predict(features.reshape(1, -1))alpha这个正则化系数需要调。太小会过拟合太大会欠拟合。我的经验是标定点少于12个时用alpha1.0到3.0标定点多的时候可以降到0.3左右。可以用交叉验证快速选一下。4.4 平滑滤波让光标不抖原始预测出来的屏幕坐标抖动很厉害直接用体验极差。我试过三种滤波移动平均、指数平滑、卡尔曼滤波。移动平均延迟大指数平滑简单够用卡尔曼滤波效果最好但调参麻烦。最终我选了指数平滑公式是smoothed alpha * current (1 - alpha) * previousalpha取0.3到0.5之间比较合适。太小延迟明显太大抖动抑制不够。另外我还加了一个死区机制如果新预测点和上一个点的距离小于某个阈值比如屏幕宽度的1%就不更新这样静止时特别稳。注意平滑滤波会引入延迟如果你做的是实时交互比如用视线控制游戏延迟超过100ms就能明显感觉到。这时候要降低alpha或者改用预测性滤波用速度信息补偿延迟。5. 常见问题与排查技巧实录5.1 精度突然变差怎么排查精度问题是最常见的。我整理了一个排查顺序按这个走基本能定位到原因。现象可能原因排查方法解决整体偏移坐标系没对齐检查ROI裁剪后是否做了坐标变换统一到全帧坐标系边缘误差大标定点覆盖不足看标定点分布增加边缘标定点侧头就崩缺头部姿态补偿检查pitch/yaw是否入特征加入姿态特征时好时坏光照变化观察不同光照下瞳孔定位加自适应直方图均衡眨眼后飘瞳孔点跳变记录眨眼时特征加眨眼检测滤波5.2 光照问题的实战处理普通摄像头方案对光照极其敏感。我遇到过背光时瞳孔完全定位不到的情况。解决办法有几个一是CLAHE限制对比度自适应直方图均衡在眼睛ROI上做能显著提升暗光下的瞳孔对比度二是动态阈值不要用固定阈值分割瞳孔改用Otsu或者自适应阈值三是多帧验证如果连续几帧瞳孔定位结果跳变超过阈值就判定为不可信用上一帧的结果顶替。clahe cv2.createCLAHE(clipLimit2.0, tileGridSize(8, 8)) eye_roi_gray cv2.cvtColor(eye_roi, cv2.COLOR_BGR2GRAY) enhanced clahe.apply(eye_roi_gray)5.3 眼镜用户的特殊处理戴眼镜的用户是另一个大坑。镜片反光会在眼睛区域形成亮斑干扰瞳孔定位。我的处理是先用关键点定位眼镜框的大致区域然后在瞳孔定位时把高亮区域像素值超过220的连通域mask掉。另外镜片会折射光线导致瞳孔位置有系统性偏移这个只能靠标定来吸收所以戴眼镜的用户必须重新标定不能共用不戴眼镜时的模型。5.4 性能优化怎么跑到实时MediaPipe在CPU上跑468点大概20到30毫秒一帧加上其他处理整体能到25到30FPS基本够用。如果不够可以降摄像头分辨率到640x480或者隔帧做关键点检测中间帧用光流跟踪。我试过用cv2.calcOpticalFlowPyrLK在眼睛ROI上做跟踪能把整体帧率提到50FPS以上但跟踪久了会漂需要定期用关键点检测重置。实操心得别一上来就追求高帧率。先把精度调好精度不行帧率再高也没意义。我见过有人花大力气优化到60FPS结果注视点误差5厘米完全没法用。6. 精度边界与适用场景的实话实说6.1 这套方案到底能做到什么程度我把话说透普通摄像头方案在理想条件下光照稳定、头部移动小、认真标定屏幕中心区域的注视精度大概在2到4度视角换算到60厘米观看距离的24寸屏幕上大约是2到4厘米的误差。屏幕边缘会更差可能到5到6厘米。这个精度和专业眼动仪差了一个数量级但用于注意力区域粗分类比如判断用户在看屏幕左中右哪个区域完全够用。6.2 哪些场景能用哪些别硬上能用的场景阅读行为分析判断在看哪一段、无障碍交互用视线移动光标做粗定位、游戏辅助视线方向控制视角、广告注意力测试区域级热力图。别硬上的场景精确的注视点打字、需要亚度级精度的科研实验、快速眼跳分析。这些场景还是老老实实上专业设备省下来的时间比省下来的钱值钱。6.3 进一步提升精度的几个方向如果你确实需要更高精度有几个方向可以试。一是多摄像头用两个摄像头做立体视觉能更准地定位瞳孔三维位置二是主动光源自己加几个近红外LED成本不高但能大幅提升瞳孔对比度三是个性化模型用少量用户数据微调一个预训练模型比从零标定效果好。这几个方向我都试过多摄像头效果最明显但复杂度也最高加红外LED性价比最高。7. 我在实际折腾中攒下的几条经验最后分享几条踩坑换来的经验都是文档里不会写的。第一标定时让用户坐姿和实际使用时一致我遇到过标定时坐得笔直、使用时瘫在椅子上的情况精度直接崩掉。第二摄像头位置要固定笔记本摄像头如果屏幕角度变了映射关系就变了必须重新标定。第三别迷信深度学习我试过用CNN端到端回归在没有大量标注数据的情况下反而不如岭回归稳。第四记录失败样本每次精度崩掉的时候把特征和图像存下来攒多了你会发现规律这比盲目调参有用得多。这套方案我前后迭代了大概两个月从最初阈值法的一塌糊涂到现在能稳定做区域级注视估计中间推翻重来了好几次。如果你也在做类似的东西建议先从最小可行版本跑通别一上来就堆复杂模型。先把标定和头部姿态这两块做扎实精度自然就上来了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑