简介:iris_tracking_sample.zip 是一份基于 Mediapipe Iris 的虹膜追踪示例代码包,面向需要在 Windows 10 环境下从摄像头或视频流中实时提取眼角、眼睑、眼球轮廓及虹膜关键点坐标的计算机视觉开发者。资源采用 C++ 接口实现,共 5 个文件:两个 cpp 与一个 cc 源文件负责核心逻辑,一个 h 头文件声明数据结构,一个 build 文件用于构建配置,压缩包整体仅 7KB,十分轻量。示例代码清晰展示了 Iris 流水线初始化、逐帧处理以及 Landmark 三维坐标输出机制,这些坐标可直接用于增强现实眼镜叠加、眼动分析、健康监测等场景;通过查看 build 文件还能理解模块依赖关系,便于将虹膜追踪能力快速移植到自有工程。目前已有 411 人学习下载,对于希望以最小成本快速上手 Mediapipe Iris 的开发者而言,这是一份聚焦且具有很强参考价值的入门样例。
1. iris_tracking_sample.zip:一段能跑的虹膜追踪代码,到底在解决什么
拿到iris_tracking_sample.zip这个名字时,我第一反应不是「又一个开源包」,而是「这大概率是一个能直接出画面的样例工程」。虹膜追踪这个方向,真正难的不是检测那一帧里瞳孔在哪里,而是把连续帧里的瞳孔中心稳定地输出成一条坐标流,再映射到屏幕上的注视点。很多从业者第一次跑虹膜追踪项目都会遇到同一个问题:单张图检测很准,一连起来跑就疯狂抖动,而这正是样例包最值得拆解的部分。
这个 sample 包适合三类人:做注意力检测或眼疲劳判断的应用开发者,想在边缘设备上跑视线估计的嵌入式工程师,以及刚入行想理解眼动追踪原理的前端或算法新人。它解决的是一个很具体的问题——用普通 RGB 摄像头或近红外摄像头,实时定位虹膜中心、估算视线方向,为上层应用提供稳定输入。这篇笔记会按「样例包里有什么 → 两种主流实现路线 → 参数怎么调 → 高频踩坑」的顺序,把它拆成一篇可以照着复现的实战记录。
2. 解包与运行环境:先搞清样例包的结构,再决定怎么复现
2.1 样例包的目录结构:一份可运行的视觉追踪工程该有什么
拿到iris_tracking_sample.zip后,先别急着unzip就开跑。我的习惯是先把压缩包里的文件列出来看一遍结构,判断它是 torch 工程、OpenCV 工程还是带前端的完整 Demo。常见的一份虹膜追踪样例包,目录结构大致长这样:
iris_tracking_sample/ ├── data/ │ ├── sample_01.mp4 # 测试视频或图像序列 │ └── calib_points.json # 屏幕标定坐标,用于注视点映射 ├── src/ │ ├── detect.py # 虹膜/瞳孔检测主逻辑 │ ├── track.py # 连续帧追踪与滤波 │ ├── gaze_mapping.py # 虹膜中心到屏幕坐标的映射 │ └── visualize.py # 实时画框、画十字、输出注视点 ├── requirements.txt ├── config.yaml # 摄像头编号、ROI、阈值、滤波参数 ├── run_webcam.py # 本地摄像头入口 └── README.mddata/目录里的测试视频是判断样例包质量的关键。如果里面只有几张静态图,那这个包大概率只做了检测,没做时序追踪;如果有连续视频,才能完整验证追踪稳定性。config.yaml则负责集中管理参数,虹膜追踪里最容易翻车的阈值、感兴趣区域(ROI)大小、滤波强度都在这里面调,而不是散落在代码里。
检查完目录后,我建议先读README.md里的「运行方式」段落,而不急着读代码。虹膜追踪项目对环境的要求通常集中在三块:Python 版本、摄像头驱动(V4L2 或 DirectShow)、以及 OpenCV 与 MediaPipe 等依赖的版本兼容性。这三点里任何一点不一致,样例包跑起来都会在奇怪的地方报错。
2.2 跑通最小例子的依赖清单与一条命令验证
虹膜追踪样例包最常见的依赖是 OpenCV 加一个关键点检测库。如果你的包里没有自带模型文件,而依赖mediapipe的虹膜模型,那么环境配置的典型命令如下:
# 建议用 Python 3.8~3.11,太新的 3.12 某些依赖轮子不全 python -m venv venv_iris source venv_iris/bin/activate # Windows 下用 venv_iris\Scripts\activate pip install opencv-python numpy mediapipe pyyaml依赖装完后,先跑一个最小的「打开摄像头 + 显示画面」脚本,确认摄像头索引和画面翻转状态正确,再跑完整的虹膜追踪入口。这一步的作用是隔离问题:如果摄像头打不开,问题在驱动或索引;如果画面正常但检测框不动,问题在模型加载或输入尺寸。
# quick_camera_test.py —— 验证摄像头可用性的最小脚本 import cv2 cap = cv2.VideoCapture(0) # 0 是默认摄像头索引,笔记本通常是 0 if not cap.isOpened(): raise SystemExit("摄像头索引 0 打不开,尝试改为 1 或 2") while True: ok, frame = cap.read() if not ok: break cv2.imshow("camera test", frame) # 弹出实时画面窗口 if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()这段脚本里最关键的是VideoCapture(0)的索引参数。外接摄像头和内置摄像头可能占用不同索引,有些设备上索引 0 是无画面但isOpened()仍返回True,所以跑通这一步后再验证是否真的读到了画面,别只看返回值。另外,OpenCV 读出的画面通常来自摄像头的原生物理方向,很多笔记本摄像头是倒置的,如果画面上下颠倒,需要在追踪主干脚本里预处理翻转,否则后续的注视点映射会全部偏掉。
跑通摄像头后,就进入了样例包的主流程验证。我一般会先跑run_webcam.py,看三件事:画面帧率是否超过 15 FPS、虹膜中心是否平滑移动、以及 CPU 占用是否在可接受范围。如果帧率很低,先检查是不是用了过大的输入分辨率,再检查是否有 OpenCV 的 DNN 模块或 MediaPipe 在跑 CPU 推理。
3. 传统 CV 路线的核心逻辑:灰度、阈值、椭圆拟合拿瞳孔中心
3.1 从 ROI 到阈值化:三行代码定出瞳孔候选区域
虹膜追踪的传统方案不依赖深度学习,核心思路是:瞳孔是眼球上颜色最深的区域,把图像转为灰度后,用一个合适的阈值就能分离出来。但在全图上做阈值化效率低,而且容易把眉毛、头发、眼镜框也卷进来。所以第一步是裁剪 ROI(感兴趣区域),把搜索范围限定在眼睛周围。
# roi_extract.py —— 从人脸bbox中截取左右眼区域 import cv2 def extract_eye_roi(face_bbox, frame, side="left", eye_scale=0.5): x, y, w, h = face_bbox eye_w = int(w * eye_scale) # 眼睛区域宽度约为人脸宽度的 0.5 eye_h = int(h * 0.2) # 高度约为人脸高度的 0.2 if side == "left": ex, ey = x + int(w * 0.15), y + int(h * 0.3) else: ex, ey = x + int(w * 0.55), y + int(h * 0.3) roi = frame[ey:ey + eye_h, ex:ex + eye_w] return roi, (ex, ey)这段代码中的eye_scale和坐标偏移比例是经验值,来自人脸关键点统计,左右眼大致位于人脸下半部分的两个角。实际场景中,如果摄像头距离人脸很近,这个比例就得调大;距离远则调小。ROI 裁剪不精确会导致后续阈值化把下眼皮的阴影也当成瞳孔一部分,这个问题在光照不均匀时特别明显,我建议在 ROI 内再做一次局部直方图均衡化,用cv2.equalizeHist,让瞳孔区域和背景的对比度拉开。
裁剪出 ROI 后,就可以做阈值化找瞳孔了。这里有一个常见误区:直接用固定阈值,比如cv2.threshold(roi, 50, 255, cv2.THRESH_BINARY),这在室内灯光下也许有效,但一旦背景或皮肤反光变化,瞳孔可能被切断或漏检。更稳妥的做法是用自适应阈值或先做一个最小像素灰度值统计。
# threshold_pupil.py —— 基于局部灰度分布获取瞳孔二值图 import cv2 import numpy as np def get_pupil_binary(roi_gray): min_val, max_val, _, _ = cv2.minMaxLoc(roi_gray) lower = min_val + int((max_val - min_val) * 0.10) # 取最低灰度往上10%区间 _, binary = cv2.threshold(roi_gray, lower, 255, cv2.THRESH_BINARY_INV) # 开运算去除睫毛造成的孤立小白点 binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) return binaryTHRESH_BINARY_INV会把灰度值低于阈值的区域置为白色,这样瞳孔区域变成了白色团块,方便下一步找轮廓。开运算主要处理睫毛和眼睑边缘的干扰,这些细线条在二值图上表现为零散小白点,用 3×3 的结构元素做一次腐蚀再膨胀就能去掉。这个阈值策略会有光照适应能力,因为lower是根据当前帧的灰度动态范围算出来的,而不是写死一个数字。
3.2 轮廓筛选排除睫毛和反光:面积、宽高比两个硬条件
拿到二值图后,瞳孔并不一定是最大的那个白色团块。眼镜框反光、眼白上的光线反射、甚至是深色眉毛的下缘,都可能在阈值化后形成团块。这时靠传统的连通域筛选就能去掉大部分干扰,不需要上模型。
# contour_filter.py —— 筛选瞳孔候选轮廓 import cv2 def pick_pupil_contour(binary, min_area=20, max_area_ratio=0.5): contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None h, w = binary.shape max_area = w * h * max_area_ratio # 瞳孔面积不能超过 ROI 面积一半 valid = [] for cnt in contours: area = cv2.contourArea(cnt) x, y, cw, ch = cv2.boundingRect(cnt) aspect = cw / max(ch, 1e-6) # 宽高比 if min_area <= area <= max_area and 0.3 <= aspect <= 3.0: valid.append(cnt) if not valid: return None # 取面积最大的候选作为瞳孔 return max(valid, key=cv2.contourArea)宽高比的上下限 0.3~3.0 是常用经验值。瞳孔在正视时接近圆形,宽高比接近 1.0;在看向左右时会被眼皮遮挡一部分,变成扁椭圆,但宽高比一般不会低于 0.3。如果设定过窄,用户侧视时检测会频繁丢失;设得过宽,则容易把眼睑阴影也圈进来。
面积阈值里的max_area_ratio=0.5是个值得注意的坑。在低光照下,瞳孔会生理性放大,占据 ROI 的比例可能接近 0.6,这时候硬性限制 0.5 会把真正的瞳孔过滤掉。我建议这个参数要和你的光照场景绑定:暗光环境下提到 0.7,正常室内光照保持 0.5。如果样例包的配置文件里有这个参数,优先调它而不是改代码。
筛选出轮廓后,用cv2.fitEllipse(cnt)就能得到瞳孔的拟合椭圆,椭圆的中心就是瞳孔中心。这一步会返回(cx, cy)坐标,这个坐标是在 ROI 内的局部坐标,需要加上 ROI 左上角偏移,转换回全图坐标,才能交给后续的注视点映射模块。
3.3 把中心输出成连续坐标流:帧率与延迟的取舍
瞳孔中心检测是逐帧进行的,如果直接把每一帧的(cx, cy)输出给上层应用,会出现两个问题:一是坐标在高频抖动,因为像素级检测本身有 ±1~2 像素的噪声;二是当检测偶尔丢失时,输出会突然跳到 0,0,导致上层逻辑误判为剧烈转头。
# output_stream.py —— 维护一个带丢失标记的坐标输出 import collections class IrisStreamOutput: def __init__(self, max_history=5): self.history = collections.deque(maxlen=max_history) self.last_valid = None def push(self, cx, cy, valid): if valid: self.history.append((cx, cy)) self.last_valid = (cx, cy) # 关键:检测丢失时输出最近的有效值,而不是(0,0) if self.last_valid is None: return None if not valid: return self.last_valid avg_x = sum(p[0] for p in self.history) / len(self.history) avg_y = sum(p[1] for p in self.history) / len(self.history) return (avg_x, avg_y)这里用了一个简单的历史均值窗口,max_history=5大约对应 5 帧的平滑,在 30 FPS 下是约 160ms 的延迟。如果应用是驾驶注意力检测,这种延迟可以接受;但如果是眼控交互,用户移动视线后要立刻看到光标移动,160ms 就会感觉肉。眼控场景建议把窗口缩到 2~3 帧,或者在丢失帧时不输出均值,而是直接输出last_valid,保证光标的即时响应。
另一条取舍是分辨率与帧率。虹膜检测不需要全高清输入,把摄像头采集分辨率降到 640×480,ROI 裁剪和阈值化速度都会大幅提升,帧率从 15 FPS 提到 30 FPS 并不奇怪。有些样例包支持在config.yaml里设置cap_width: 640和cap_height: 480,如果你的包没有这个配置,就在VideoCapture初始化后手动设置一下,这是提帧率最直接的手段。
4. 预训练关键点路线:用回归替代手工特征,虹膜追踪的现代解法
4.1 为什么样例包里的 tracking 部分往往改用关键点回归
传统 CV 路线的天花板很明确:强烈反光、重度遮挡、佩戴眼镜时瞳孔提取稳定性差;而且它做的是「瞳孔位置检测」,不是真正的「虹膜追踪」——不区分虹膜边缘和瞳孔边缘,也不包含眼部形状的先验信息。现代样例包里的虹膜追踪部分,更多是采用预训练的关键点模型,输出虹膜中心、虹膜边界和眼球轮廓的二维坐标,然后再用这些坐标计算注视方向。
选择关键点回归而不是自己训练检测模型的理由很实际:私有人眼关键点数据集的采集标注成本极高——需要多个人在多个视角下录制,逐帧标注虹膜边缘点,工作量远超普通目标检测标注。预训练模型能直接提供虹膜中心与边界,省去数据准备环节,开箱即用。而且关键点输出天然包含眼睑位置,可以区分「正常睁开眼睛」和「半闭眼/眨眼」状态。
这也解释了一个现象:很多虹膜追踪样例包用 MediaPipe 作为前置关键点模块,再用轻量逻辑做注视映射,而不是自己写一套阈值算法。这并不代表传统方案一文不值——它仍有意义,主要在低功耗无依赖场景,以及作为关键点模型的校验和兜底。
4.2 关键点推理:输入预处理与输出坐标映射
用预训练关键点模型时,最容易犯的错是直接把整帧高清图像塞进去推理,然后又期待它输出绝对精确的虹膜坐标。实际上,这类模型的输入一般是 256×256 或 128×128 的人脸对齐图,输出的是归一化到 [0,1] 区间的相对坐标,必须经过换算才能得到像素坐标。
# mediapipe_iris_infer.py —— 关键点推理与坐标还原示例 import cv2 import numpy as np # 该模型返回468个人脸关键点外加10个虹膜关键点 # 这里仅演示整体流程,具体API以你的样例包源码为准 def infer_iris(frame, model): rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results = model.process(rgb) if not results or not results.multi_face_landmarks: return None h, w = frame.shape[:2] landmarks = results.multi_face_landmarks[0].landmark # 虹膜关键点在整个人脸关键点序列中处于特定位置区间 iris_point_idx = list(range(468, 478)) iris_points = [] for i in iris_point_idx: pt = landmarks[i] x_px, y_px = pt.x * w, pt.y * h iris_points.append((x_px, y_px)) iris_center = np.mean(iris_points, axis=0) return iris_center, iris_points坐标换算里有一个隐藏问题:landmark.x是相对人脸包围盒的归一化坐标,但它不一定相对整帧图像宽度。某些预训练模型返回的坐标相对的是原始输入图的宽高,某些则相对检测到的人脸框,读样例包源码时一定要确认这一点。如果相对人脸框,换算就应该是x_px = pt.x * bbox_width + bbox_x,而不是乘以整帧宽高。
关键点模型相比传统方案的一个优势是自带眼部形状信息。通过眼睑关键点的相对位置,可以直接算出眼睛的睁开程度,也就是 EAR(Eye Aspect Ratio),这个指标在检测眨眼和疲劳状态时非常好用,而不需要单独再跑一个分类模型。样例包的输出结构里一般会包含眼部轮廓坐标,顺手算一下 EAR 就能扩展出疲劳检测功能。
4.3 从虹膜关键点到注视方向:一个最简映射
拿到虹膜中心后,怎么把它变成「人正在看屏幕哪个位置」?这是注视点估计的核心。一个工程上可用的简化假设是:当人脸正对摄像头且头部保持基本不动时,虹膜中心相对眼睑边界的位置偏移和屏幕注视点之间存在近似线性关系。所以可以用最小二乘拟合一个从(iris_center_offset_x, iris_center_offset_y)到(screen_x, screen_y)的线性映射。
# gaze_linear_map.py —— 最小二乘线性注视映射 import numpy as np class LinearGazeMapper: def __init__(self): self.M = None # 2x3 仿射变换矩阵 def fit(self, iris_offsets, screen_points): # iris_offsets: [(ox, oy), ...] 屏幕标定时采集的虹膜偏移 # screen_points: [(sx, sy), ...] 对应的屏幕坐标 src = np.hstack([np.array(iris_offsets), np.ones((len(iris_offsets), 1))]) dst = np.array(screen_points, dtype=np.float32) self.M, _ = cv2.estimateAffinePartial2D(src, dst) return self.M def map(self, iris_offset): if self.M is None: return None src = np.array([[iris_offset[0], iris_offset[1], 1.0]], dtype=np.float32) return cv2.transform(src.reshape(1, 1, 3), self.M)[0][0]estimateAffinePartial2D需要至少三个非共线标定点,实际使用时建议取 9 个点(屏幕九宫格中心),每看一个点保持头部稳定,采集对应的虹膜偏移。这个线性模型在屏幕中心区域表现不错,但屏幕边缘误差会变大,因为眼球转动到极限时虹膜边缘会被眼皮遮挡,此时偏移量和屏幕坐标的关系不再是线性的。
如果你的样例包里没有现成的标定脚本,可以自己写一个简单的:在屏幕上轮询显示圆点,用户盯着圆点按空格,程序记录对应的虹膜偏移并送入fit函数。这个流程看起来很原始,但它就是很多视线估计产品的基础——所谓「标定」,本质是把眼球运动的个体差异用一个变换矩阵补偿掉。头部固定得越稳,标定效果越好;头部自由移动时,就需要引入头部位姿补偿,复杂度会上升一个量级。
5. 虹膜追踪避坑:光线、遮挡与抖动导致的翻车现场
5.1 过曝和昏暗场景下虹膜中心整体漂移
现象:室内靠窗位置,阳光照到半张脸,瞳孔中心突然往反方向偏了 5~10 个像素;关灯后用屏幕光照明,检测框开始不稳定闪烁。
原因:传统阈值方案对光照高度敏感。过曝时虹膜纹理被压缩成一片亮色,阈值分割会把瞳孔区域割裂成多个小团块,轮廓筛选后只能选到部分瞳孔;昏暗场景下瞳孔放大、灰度对比度降低,ROI 内的动态范围变小,阈值下限min_val + 10%还会把虹膜外圈一起划进瞳孔区域。
解决:优先限制环境变量——调整摄像头曝光时间,在驱动层设置固定曝光,避免自动曝光每帧都在变。代码层面,可以在config.yaml里添加强制曝光参数:cap_exposure: -5,其中 -5 是 V4L2 下的曝光等级,数值越小曝光越短。如果光线过暗,请用红外照明配合红外摄像头,这是目前眼动追踪设备最主流的光学方案,因为红外光照不刺激瞳孔收缩且不受可见光变化影响。普通 RGB 摄像头在无额外照明的情况下,很难把虹膜追踪做到稳定。
5.2 眨眼瞬间关键点坐标突变导致误判
现象:用户眨眼的 100~200ms 内,虹膜关键点输出坐标跳到了眼睛角落,上层应用误判为一次剧烈扫视,触发了错误的交互或告警。
原因:眨眼时眼睑完全覆盖虹膜,关键点模型无法从遮挡区域回归出真实虹膜位置,输出的会是视觉上最「像」的残差结果,坐标往往偏向眼睑边缘。传统方案更直接——二值图里没有足够面积的瞳孔团块,轮廓筛选返回空,但某些实现此时会沿用上一次结果,导致输出产生假检测。
解决:加入状态机,区分「睁眼追踪」和「闭眼丢弃」两种状态。用上一步提到的 EAR 指标判断眼睛是否闭合:EAR < 0.2视为闭眼,闭眼期间直接跳过坐标更新,保留闭眼前的最后一个有效值。关键点模型场景下,闭眼检测也可以直接用眼睑关键点距离,不必依赖瞳孔检测是否成功。注意闭合阈值 0.2 是个参考值,上下眼皮距离在不同人脸上有差异,最好标定阶段记录下来。
5.3 头部微动带来的高频抖动,滤波参数永远在打架
现象:用户明明盯着同一个点,虹膜中心坐标却在 3~5 个像素范围内来回跳动;手调平滑窗口调大后抖动消失,但视线移动时光标反应迟钝,像拖着一条橡皮筋。
原因:虹膜追踪输出的抖动来源有两个:一是像素级检测噪声,二是头部根本无法做到绝对静止——呼吸、心跳都会让头部产生亚毫米位移,这个位移在 50cm 拍摄距离下映射到图像上就是几个像素的偏移。时序滤波只能压噪声,无法区分「头部微动」和「眼球扫视」,所以窗口越大,真实眼动也被抹平得越多。
解决:不要在坐标上做单一滤波,而是拆两条路径。路径一,用窗口 3 帧的均值滤掉像素噪声;路径二,维护头部区域的基准位置,每次追到人脸框中心后计算人脸的位移与旋转,再对虹膜偏移做头部运动补偿。头部补偿后,剩下的抖动幅度会小很多,此时再用小窗口移动平均就不会牺牲太多响应速度。另外我把经验放在这里:jitter问题靠滤波是治标不治本,优先检查是否开了自动曝光,其次检查摄像头自动对焦,这两个因素贡献了大约 70% 的抖动现象。
5.4 摄像头位置和取景角度让标定结果完全不可用
现象:标定阶段拟合误差很小,但实际使用时光标一直偏在屏幕左上区域,怎么动鼠标都对不上。
原因:标定要求头部保持固定,但实际使用时用户会自然往后靠、微微侧头,甚至调整坐姿后头部位置发生了变化。线性映射模型只包含虹膜偏移与屏幕坐标的关系,没有把头部位置作为输入,所以头部移动后整个映射关系立即失效。另外,摄像头安装位置如果明显偏左或偏上,会导致虹膜在图像中的运动轨迹与屏幕平面不平行,进一步放大映射误差。
解决:严格固定摄像头位置,优先安放在屏幕正上方中央,镜头光轴尽量水平朝向用户面部。标定前提示用户把头部调到习惯坐姿,标定后再检查映射残差:让用户依次盯住屏幕四个角,如果光标偏差方向一致,说明头部姿态发生了整体偏移,而不是映射模型错了。这类问题往往是摄像头安装角度造成的系统误差,重新摆放摄像头比调整算法参数更有效。
6. 让追踪结果真正可用:去抖滤波与注视点映射的落地细节
6.1 从单帧中心到平滑光标:一套不过度牺牲延迟的滤波组合
上一章说了分开处理噪声和头部微动,这一节给出一套实操上可以直接套用的滤波组合。我会在坐标输出前串联两个步骤:先用一阶低通滤掉高频像素噪声,再用死区(deadzone)检测来消除静止时的低频漂移。
# smoothing_chain.py —— 低通 + 死区滤波链 class SmoothChain: def __init__(self, alpha=0.35, deadzone=0.8): self.alpha = alpha # 低通系数,越大越跟随,越小越平滑 self.deadzone = deadzone # 死区像素半径,静止时抑制漂移 self.out_x, self.out_y = None, None def update(self, raw_x, raw_y): if self.out_x is None: self.out_x, self.out_y = raw_x, raw_y else: self.out_x += self.alpha * (raw_x - self.out_x) self.out_y += self.alpha * (raw_y - self.out_y) dx, dy = raw_x - self.out_x, raw_y - self.out_y if abs(dx) < self.deadzone and abs(dy) < self.deadzone: return self.out_x, self.out_y # 小位移不更新输出 return self.out_x, self.out_y这段代码里值得关注的参数是alpha=0.35。在 30 FPS 下,这个系数让坐标大约在 100ms 内跟上真实眼动,交互上不会觉得拖沓;如果场景是远距离注意力分析,可以降到 0.2 获得更平滑的轨迹。死区0.8像素的作用是过滤掉静止时由于传感器噪声产生的亚像素抖动——当滤波后的位置和原始位置相差小于 0.8 像素时,不更新输出,这样视线静止时光标是完全钉住的,这也是很多眼动仪产品「稳如泰山」的秘密。
设置死区有一个副作用:当用户做微小扫视时,这些 1 像素以内的小幅移动会被吃掉,表现为边缘处细微的失控感。应对方法是把死区只应用在静止检测,一旦检测到连续两帧位移超过死区,立即切换为全跟随模式。这个逻辑用一段简单的状态变量就能实现,不必引入卡尔曼滤波器——对大多数虹膜追踪用途,一阶低通加死区在实现复杂度和效果之间已经足够平衡。
6.2 标定结果的可移植性与重复标定策略
线性注视映射最不友好的地方在于,它绑定一个具体的头部位置和摄像头角度。换个用户就拉高映射误差的例子我见过太多次了,一个团队做完 Demo 换个人演示,光标突然对不准,现场调试了几分钟才反应过来是标定数据失效。在生产级项目里,我会把标定结果序列化保存,并且在应用启动时让用户选择「新用户标定」或「沿用上次标定」。
标定结果的保存格式很简单,就是把 M 矩阵的三个浮点参数写到 JSON 文件里。载入时再附上一组校验点:用户依次看屏幕三个固定位置,如果预测位置与实际位置偏差超过屏幕宽度的 10%,提示重新标定。这个 10% 的阈值是根据眼动追踪产品的一般精度水平定的,如果你做的是高精度的眼控打字场景,阈值要收紧到 5%。校验点的采集不需要用户额外操作,在应用使用前的几秒内静默完成即可。
最后分享一个我自己的教训:调试虹膜追踪时不要只盯着一张静态图看检测精度,真正要做的是开着实时画面盯住同一个点,观察光标 10 秒内的漂移轨迹。精度高代表的是「检测对」,但用户感知到的永远是「光标稳不稳、跟不跟手」。我习惯把「稳定性」排在「精度」前面去调参数——先保证光标不跳,再提高映射准确度。希望这篇笔记能帮你把iris_tracking_sample.zip里的代码跑成一套真正能用的注视追踪链路,少走几趟我走过的弯路。
本文还有配套的精品资源,点击获取