MediaPipe Iris虹膜关键点检测与视线交互示例

发布时间:2026/10/24 6:48:40

MediaPipe Iris虹膜关键点检测与视线交互示例 简介iris_tracking_sample.zip 面向在 Windows 10 下使用 Mediapipe Iris 进行眼部关键点追踪的开发者聚焦眼角、眼睑、眼球边界与虹膜坐标的获取可用于 AR/VR、生物识别及健康监测等场景的原型验证。包内共 5 个文件以 cpp、cc、h 源码和 BUILD 构建配置为主压缩包约 7KB体量轻巧便于快速浏览流水线组织与接口调用。已有 411 人学习说明该示例在入门与实践参考上有一定关注度。读者可从中了解 Iris 流水线的初始化、图像输入、逐帧处理与结果可视化思路并参考 Landmark 输出的解析方式为后续调整检测精度与速度、结合 FaceMesh 扩展面部追踪功能提供起点。1. 摄像头前面坐一天我只想要那两个瞳孔坐标接手一个用摄像头做视线交互的 demo 时我第一反应是找现成的瞳孔检测结果一圈试下来阈值法怕反光、Haar 怕睫毛、传统 68 点人脸关键点根本没有瞳孔位置。后来把 MediaPipe Iris 拉进来跑了一版478 个面部关键点里专门留了 10 个给虹膜每只眼睛 5 个中心那个就是瞳孔中心坐标直接给归一化值这个坑才算填上。iris_tracking_sample.zip 就是这么一份最小可跑的示例图片、视频文件、摄像头三条输入通道输出关键点坐标加上可视化叠加。适合谁想在一两天内把眼动、眨眼、注视方向这类功能先跑出个能演示的版本再决定要不要上自训练模型的开发者拿它当第一块垫脚石正合适。2. 项目定位与技术选型iris_tracking_sample 补上了哪 10 个点把这份示例当成“调个库”就低估它了。它真正解决的问题是在没有专用眼动仪、没有红外摄像头、只有一颗普通 RGB 网络摄像头的条件下稳定拿到瞳孔在画面里的二维位置。这条路线上市面上常见的选择就那么几种各自能走到哪一步值得先说清楚因为这直接决定你后面改代码的成本。用 OpenCV 做阈值分割找瞳孔是最便宜的一条路灰度化、二值化、找轮廓、取质心。它在实验室的均匀光线下能跑但一到真实环境就崩——屏幕反光、睫毛遮挡、眼镜片上的倒影任何一个都能让质心跑到眼角去。用传统人脸关键点做问题换了一种面部轮廓是有了眼睑、眼角都有但眼睛内部是空的没有瞳孔你只能靠眼角的中点去猜注视方向精度天然差一截。MediaPipe Iris 走的是第三条路在面部网格的基础上用一个专门的虹膜细分模型把眼睛内部的 5 个点回归出来其中 1 个是虹膜中心另外 4 个落在虹膜边缘上。这条路的好处是不挑硬件普通摄像头就够。2.1 468 与 478 的差别全在 refine_landmarks 这一个参数上很多人第一次用会踩这个坑跑完之后len(face_landmarks.landmark)是 468找不到文档里说的 468 到 477 号点。原因不在模型在初始化参数——refine_landmarks默认是False它关掉的时候只输出基础的面部网格 468 点打开之后模型会在眼睛区域再跑一次细分把虹膜那 10 个点补到队尾。这个参数是整个项目的地基开与不开输出结构完全不同。补上的这 10 个点索引和含义是一一对应的索引归属含义468第一只眼虹膜中心瞳孔中心近似469 – 472第一只眼虹膜边缘 4 点绕中心分布473另一只眼虹膜中心瞳孔中心近似474 – 477另一只眼虹膜边缘 4 点绕中心分布需要提醒的是模型内部对“左眼”“右眼”的命名是按输入图像的坐标来的不是按被拍摄者的生理左右。如果你后续接了镜像显示自拍场景几乎一定会接这两组索引在画面上会左右对调。我一般的做法是不纠结命名代码里统一叫eye_a和eye_b靠中心点的 x 坐标大小来区分解算画面翻转时逻辑不用动。提示索引顺序469 到 472 究竟是顺时针还是逆时针在不同输入下可能有细微差别如果你的算法依赖“哪个点是上边缘”别写死索引用 y 坐标最小/最大去找更稳妥。2.2 目录结构与依赖一份能两天跑通的最小骨架示例包的目录组织得很克制没有为了“看起来像工程”而堆分层。拆开大致是这么几块iris_tracking_sample/ ├── data/ │ └── test_face.jpg # 一张带正脸的测试图先验模型能不能出 478 点 ├── src/ │ ├── detect_image.py # 单图入口最简的一根线 │ ├── detect_video.py # 视频文件入口逐帧读、逐帧写 │ ├── detect_webcam.py # 摄像头入口实时循环 │ ├── iris_utils.py # 虹膜点位、EAR、偏移量等纯计算函数 │ └── draw_utils.py # 所有 cv2 绘制逻辑集中在这里 ├── requirements.txt └── README.md这个结构的价值在于三个入口脚本只负责“取帧 显示”真正的业务计算全在iris_utils.py里绘制全在draw_utils.py里。你后续要把它塞进自己的工程只需要搬这两个文件入口脚本扔掉都不影响。我见过太多示例把检测、计算、绘制、UI 全揉在一个 300 行的main.py里改一行要通读全文那种代码只适合演示不适合二次开发。依赖清单也很短基本就是三件套# 建议单独建虚拟环境MediaPipe 的依赖树不算浅 python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install opencv-python mediapipe numpyopencv-python负责取帧、颜色空间转换和绘制mediapipe提供 FaceMesh 与虹膜细分numpy用于关键点数组化之后的角度、距离计算。这三个装齐示例就能跑。不用装 TensorFlow也不用单独下载模型权重文件——模型是随包分发的第一次process()调用时会从包内加载这点对新手很友好省掉了“模型路径配错”这类经典事故。2.3 三条输入通道图片、视频文件、摄像头三个入口脚本对应三种典型调试阶段不是简单重复。单图入口detect_image.py是最先该跑的。它的作用是排除变量不涉及帧率、不涉及摄像头驱动、不涉及写入编码器只验证“模型能不能在我这台机器上把 478 个点吐出来”。我调试任何视觉项目都从这一步开始因为后面所有问题卡顿、闪退、坐标漂移在单图阶段都不会出现一旦单图能出正确结果问题范围立刻缩小到输入源或循环逻辑上。视频文件入口detect_video.py的价值在于可复现。摄像头是实时的出了问题你没法倒回去看那一帧到底长什么样视频文件可以反复喂同一段素材改一版跑一遍逐帧对比输出。做参数调优比如检测阈值、平滑系数时这是唯一靠谱的回归手段。摄像头入口detect_webcam.py才是最终形态它比前两个多出来的东西是帧循环、按键退出、实时 FPS 显示。这三样看着简单但决定了你能不能直观感受到延迟。注视方向这类交互端到端延迟超过 150 毫秒人就能明显感觉到“拖”所以在摄像头入口里挂一个 FPS 计数是必要的不是装饰。3. 核心链路拆解一帧图像从进入到拿到虹膜坐标这条链路的长度其实很短读帧 → 转 RGB → 送进模型 → 从结果里取关键点 → 换算坐标。短归短中间每一步都有容易写错的地方尤其是颜色空间和坐标系这两处堪称新手劝退双雄。下面按调用顺序拆。3.1 FaceMesh 初始化refine_landmarks 是总开关模型对象不要在每个帧循环里创建那等于每帧重新加载一次计算图帧率会直接掉到个位数。正确的做法是在循环外创建一次循环内反复调用process()。初始化代码如下import mediapipe as mp mp_face_mesh mp.solutions.face_mesh def create_face_mesh(static_modeFalse, max_faces1): 创建一个 FaceMesh 实例虹膜点依赖 refine_landmarksTrue return mp_face_mesh.FaceMesh( static_image_modestatic_mode, # 图片用 True视频/摄像头用 False max_num_facesmax_faces, # 虹膜细分只在单脸上准建议就设 1 refine_landmarksTrue, # 关键不开只有 468 点没有虹膜 min_detection_confidence0.5, # 首帧人脸检测阈值 min_tracking_confidence0.5, # 后续帧关键点跟踪阈值 )参数逐个说清楚因为它们直接影响你后面遇到的绝大多数问题。static_image_mode决定模型走哪种策略。设为True时每一帧都当作独立图片重新做人脸检测好处是不会把上一帧的错误带到下一帧坏处是慢。设为False时首帧做检测后续帧走跟踪速度快很多但如果人脸快速移动或者短暂离开画面跟踪可能跟丢之后需要几帧才能恢复。所以单图脚本传True视频和摄像头脚本传False这不是风格问题是性能问题。max_num_faces建议保持 1。虹膜细分模型在多人场景下的表现并不均衡检测到多张脸时返回的列表顺序在不同帧之间可能变化你按索引 0 取脸就会看到关键点在两个人之间来回跳表现为画面剧烈闪烁。做单用户交互时设成 1 从源头规避这个麻烦。两个 confidence 阈值是权衡项。调低比如 0.3能让你在侧脸、暗光下更容易检出代价是误检率上升可能出现“画面里没人也返回一张脸”的玄学现象调高比如 0.7检测更严格但用户在画面边缘或者低头时会频繁丢帧。0.5 是个能覆盖大多数室内场景的起点实际项目里我一般会把min_tracking_confidence稍微调低一点到 0.4让跟踪更黏减少闪烁。3.2 单帧推理BGR 到 RGB 的转换与结果读取OpenCV 读进来的帧是 BGR 顺序MediaPipe 要的是 RGB。这一步写错的话不会报错只会得到一个“检测不到人脸”的结果非常容易误判成模型坏了。完整的一帧处理函数长这样import cv2 def detect_frame(frame_bgr, face_mesh): 输入 BGR 帧返回像素坐标下的 478 个关键点无人脸时返回 None h, w frame_bgr.shape[:2] # 1. BGR - RGB这一步漏了就是永远检测不到 rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) # 2. 标记为只读避免 MediaPipe 内部拷贝帧率能抬一点 rgb.flags.writeable False # 3. 推理 results face_mesh.process(rgb) if not results.multi_face_landmarks: return None # 4. 取第一张脸把归一化坐标换算成像素坐标 face results.multi_face_landmarks[0] points [] for lm in face.landmark: points.append((lm.x * w, lm.y * h, lm.z * w)) return points逐段解释。cv2.cvtColor那行是硬性要求没有替代方案。rgb.flags.writeable False是个小优化告诉底层这块内存不会被改写省掉一次防御性拷贝单帧看不出来跑实时的时候能省几个毫秒。results.multi_face_landmarks是个列表没检测到脸时是None不是空列表判断的时候别写if len(...) 0会抛异常。最关键的是最后那段坐标换算。模型输出的lm.x、lm.y都是 0 到 1 之间的归一化值相对于输入图像的宽高。你要画图、要算距离就必须乘回像素尺寸。lm.z稍微特殊一点它是以头部中心为原点的相对深度量纲和 x 相同也是按图像宽度归一化的所以换算成像素尺度要乘w而不是乘某个固定的数。这个 z 值不是真实物理距离只能用来做相对比较——比如判断鼻子和耳朵谁更靠前——不能直接当厘米用后面讲深度估计时会用另一种方式绕开它。注意归一化坐标乘以w和h得到的是原图像素坐标。如果你在送入模型之前对帧做了缩放比如为了提速缩到 640 宽那这里乘的必须是缩放后的尺寸否则画出来的点会整体偏移。我吃过这个亏画面上的人脸和关键点整整错开半个身位。3.3 归一化坐标到像素坐标先统一尺度再谈算法拿到 478 个像素点之后第一件事不是急着算眼睛而是先做个切片把注意力集中到有用的点上。面部网格的 468 个基础点里和眼睛关系最密切的是眼角和上下眼睑那几个一只眼的轮廓常用索引33外眼角、133内眼角、159上眼睑、145下眼睑另一只眼的对应索引362、263、386、374这四个点加虹膜中心就够算出一堆指标了。把它们单独提出来做成一个结构化对象比在一堆魔法索引里翻来翻去清爽得多# 常用眼部索引按图像坐标下的第一只眼/第二只眼分组 EYE_A {outer: 33, inner: 133, upper: 159, lower: 145, iris: 468} EYE_B {outer: 362, inner: 263, upper: 386, lower: 374, iris: 473} def pick_eye(points, idx_map): 从 478 点里抽出一只眼的 5 个关键点 return {name: points[i] for name, i in idx_map.items()}这个pick_eye看着简单作用是给后续所有计算提供一个统一入口。等你以后要换另一套索引比如想用 6 点版本算 EAR只改EYE_A、EYE_B两个字典就行下游一行不用动。这类小封装在示例项目里看着多余等你把它搬进真实工程、需求改了三版之后就知道值了。顺带说一句尺度问题。像素坐标是跟分辨率绑定的同一张脸在 640×480 和 1920×1080 下算出来的眼角距离能差三倍。所以凡是涉及“距离”的判断比如虹膜偏移是否超标要么全部转成相对于眼睛宽度的比值要么全部归一化到 0 到 1绝不能拿裸像素值去和阈值比。这是血泪经验换了个摄像头分辨率之后所有阈值全部失效的场景我遇到过不止一次。3.4 摄像头主循环与帧率控制主循环的骨架很固定但顺序有讲究import cv2 import time cap cv2.VideoCapture(0) # 0 是默认摄像头多设备时按顺序试 1、2 face_mesh create_face_mesh(static_modeFalse, max_faces1) prev time.time() while True: ok, frame cap.read() if not ok: break # 水平翻转让画面像镜子一样符合用户直觉 frame cv2.flip(frame, 1) points detect_frame(frame, face_mesh) if points: draw_all(frame, points) # 绘制逻辑见下一章 # 实时 FPS用于判断端到端延迟是否可接受 now time.time() fps 1.0 / max(now - prev, 1e-6) prev now cv2.putText(frame, fFPS {fps:.1f}, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow(iris, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()cap.read()返回的ok必须判断。摄像头被其他程序占用、USB 松动、驱动异常都会让ok变成False而frame是None这时候如果直接送进detect_frame会在cvtColor那里抛一个看不出根因的错误。cv2.flip(frame, 1)这个翻转要放在检测之前还是之后放在检测之前意味着模型看到的是翻转后的图输出的坐标直接就是显示坐标绘制时不用再做任何变换最省事。放在检测之后你就得自己把 x 坐标做w - x的镜像容易漏掉。所以统一建议先翻转再检测。cv2.waitKey(1)里的 1 是毫秒不能省它同时承担了刷新窗口和处理键盘事件两个职责。省掉的话窗口不会更新你会以为程序卡死了。 0xFF是为了跨平台兼容某些系统上返回值高位有脏数据不做掩码比较会失效。FPS 那一小段不是给你看的是给你判断“要不要做性能优化”的。普通笔记本摄像头跑 478 点模型30 到 60 FPS 属于正常区间如果掉到 15 以下先排查是不是每帧都在重建模型对象其次看是不是分辨率太高把帧缩到 640 宽再送进去通常能翻倍。4. 关键实现细节虹膜偏移、EAR 与深度近似坐标拿到手只是原材料真正能拿去做功能的是从这些点里算出来的几个指标。这一章讲三个最常用的都是纯几何计算不依赖额外模型。4.1 虹膜相对眼角的归一化偏移人看不同方向时眼球在眼眶里转动虹膜中心相对眼角的水平位置会变。这个变化量除以眼睛宽度就得到一个无量纲的比例可以跨分辨率、跨人脸大小直接比较def iris_offset(eye): 返回虹膜中心在眼睛框内的归一化位置 (水平, 垂直)范围约 0~1 ox, oy, _ eye[outer] ix, iy, _ eye[inner] cx, cy, _ eye[iris] eye_width max(abs(ix - ox), 1e-6) # 防零除 eye_height max(abs(eye[upper][1] - eye[lower][1]), 1e-6) # 用眼角中点作为参考原点消除整张脸平移的影响 mid_x (ox ix) / 2.0 mid_y (oy iy) / 2.0 return (cx - mid_x) / eye_width, (cy - mid_y) / eye_height这里用眼角中点而不是外眼角做原点。原因很实在外眼角在转头时位置变化比中点大用中点能抵消掉一部分头部平移带来的干扰。max(..., 1e-6)是防零除侧脸角度过大时眼角 x 可能几乎重合不防的话会直接抛ZeroDivisionError而且是偶发的特别难复现。输出的两个值大致在 -0.5 到 0.5 之间波动正向对应往某一侧看。注意这只是“眼球相对眼眶”的位置不是“视线在屏幕上的落点”两者之间还差一个头部姿态的修正。做简单的左右/上下四方向判断够用要做九宫格那种精度就得加标定第 6 章讲。4.2 眼睛纵横比 EAR睁眼闭眼与眨眼检测EAREye Aspect Ratio是判断睁闭眼最省事的指标本质是“眼睛高度除以宽度”def eye_aspect_ratio(eye): 经典 6 点 EAR值越大眼越睁约 0.2 以下算闭眼 p1 eye[outer] # 外眼角 p4 eye[inner] # 内眼角 p2 eye[upper] # 上眼睑 p3 eye[lower] # 下眼睑 # 示例里是 4 点简化版2 * 高度 / 宽度 height abs(p2[1] - p3[1]) width max(abs(p4[0] - p1[0]), 1e-6) return height / width标准版 EAR 用 6 个点算两组眼睑距离的平均值能抵消单点抖动。示例里的 4 点简化版已经够判断睁闭误差主要来自侧脸——脸转到 30 度以上时眼睛在图像里的投影被压扁宽度变小EAR 会虚高本来闭着的眼可能被判成睁着。所以这个阈值不能写死我一般会让它跟着头部偏航角动态调整或者干脆在检测到明显侧脸时跳过眨眼判定。判眨眼的常规做法是EAR 连续低于阈值若干帧再回到阈值以上才算一次有效眨眼。只判单帧会出现大量误触发因为关键点本身有抖动偶尔一帧掉下去很正常。用计数器做去抖是这类信号处理的标准套路。4.3 虹膜直径与深度近似用针孔模型倒推距离前面说过lm.z不能当物理距离用。想拿到真正的距离量级得换一种思路人的虹膜直径个体差异不大通常在 11.7 毫米左右那么在图像里测得虹膜占据多少像素就能反推距离。这是标准针孔相机模型的应用import math IRIS_DIAMETER_MM 11.7 # 成年人虹膜水平直径的常用近似值 def focal_length_px(frame_width, hfov_deg60.0): 由水平视场角估算焦距像素单位 return frame_width / (2.0 * math.tan(math.radians(hfov_deg) / 2.0)) def estimate_distance_mm(iris_width_px, focal_px, real_mmIRIS_DIAMETER_MM): d f * W / ww 为虹膜在图像中的像素直径 if iris_width_px 1e-6: return float(inf) return focal_px * real_mm / iris_width_px三个参数说清楚。iris_width_px是虹膜在画面里的像素宽度用 469 到 471 或者 470 到 472 这两组相对的点去算取两次的平均更准。focal_px是相机焦距换算成像素单位后的值普通网络摄像头不知道真实焦距用视场角反推是个够用的近似60 度是笔记本内置摄像头的常见水平视场。real_mm是物理先验成年人虹膜直径大概 11 到 12 毫米取值偏差 1 毫米距离估算就会偏 8% 左右。这套算法给出的是量级正确的估计不是精密测量。做“用户离屏幕是不是太近了”这类判断完全够用做医疗级测量那是不可能的。但它比lm.z强的地方在于z 是相对头部中心的无量纲值跨帧、跨人都不好比较这套算法给的是厘米量级至少能设一个有意义的上限阈值。4.4 绘制把点位画回画面上绘制代码看着琐碎但把它单独放进draw_utils.py有个实际好处——调试时你能一眼看出到底是检测错了还是画错了。这两类问题的表征很像点跑到奇怪的地方排查方向却完全相反。import cv2 def draw_all(frame, points): 把两只眼的虹膜中心和边缘画出来 # 每只眼(中心索引, 边缘索引范围) eyes ((468, range(469, 473)), (473, range(474, 478))) for center_idx, ring in eyes: cx, cy, _ points[center_idx] cv2.circle(frame, (int(cx), int(cy)), 2, (0, 255, 0), -1) for i in ring: x, y, _ points[i] cv2.circle(frame, (int(x), int(y)), 1, (0, 200, 255), -1) # 用四根线把边缘点连成菱形方便肉眼判断虹膜是否贴合 ring_pts [points[i] for i in ring] for a, b in zip(ring_pts, ring_pts[1:] ring_pts[:1]): cv2.line(frame, (int(a[0]), int(a[1])), (int(b[0]), int(b[1])), (255, 128, 0), 1)cv2.circle的坐标必须是整数浮点会抛类型错误所以每处都做了int()截断。截断而不是四舍五入在点密集的区域会造成轻微偏移但虹膜点只有 5 个看不出来。如果以后要画全部 478 个点建议改用np.round再转int视觉上更均匀。连成菱形的那个循环用ring_pts[1:] ring_pts[:1]做首尾相接是个常用技巧比写(i1) % 4省事。这套绘制的意义在于你盯着屏幕看虹膜菱形有没有跟着眼球一起动、有没有跑到眼白上去就能判断是模型问题还是你的索引写错了。这比打印坐标值效率高得多。5. 常见问题排查跑不通的时候先看这几处这套东西的报错信息普遍不给力表现为“不报错但结果不对”的情况特别多。下面几条都是实际调试时反复遇到的按现象、原因、解决三段写。5.1 现象multi_face_landmarks一直是None原因颜色空间没转帧还是 BGR 就送进去了或者摄像头被别的程序占用cap.read()失败但没判断ok送进去的是None还有一种情况是光照太差人脸在画面里整体偏暗检测阈值过不去。解决按顺序排查三件事。第一在cvtColor之后存一帧到本地看一眼确认是彩色且颜色正常人脸是肉色不是蓝色。第二打印cap.isOpened()和每帧的ok。第三把min_detection_confidence临时降到 0.3 试试如果降了就出结果说明是光照问题补光比调参更根本。5.2 现象能检测到脸但关键点只有 468 个原因refine_landmarks是False或者根本没传这个参数默认值就是False。解决检查FaceMesh(...)的构造参数把refine_landmarksTrue显式写上。另外注意别在循环里重复构造——有人为了“重置状态”每帧新建一次模型那不仅慢还容易在参数复制过程中漏掉这一项。判断是否生效最快的办法就是打印len(results.multi_face_landmarks[0].landmark)看是不是 478。5.3 现象画面里点和脸对不上整体偏移原因送入模型的图像尺寸和换算坐标时用的尺寸不一致。典型场景是为了提速把帧resize到 640 宽再送进模型但坐标换算时用的还是原图w、h。解决把尺寸变量跟着帧一起传别用全局的。写成detect_frame(frame)函数内部第一行取h, w frame.shape[:2]这样进什么尺寸就用什么尺寸换算。另外如果做了cv2.flip翻转必须在检测之前否则 x 坐标要额外做w - x处理。5.4 现象实时画面一卡一卡FPS 掉到个位数原因优先级从高到低通常是——模型对象建在循环内摄像头分辨率开到了 1920×1080 但实际只需要 640 宽每帧都在做全画面绘制static_image_mode设成了True导致每帧重新检测。解决先关掉所有绘制只保留推理看 FPS 能到多少以此判断是绘制拖的还是推理拖的。推理拖的话用cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)降分辨率通常立竿见影。绘制拖的话别画全部 478 个点只画关心的那些。还有一个容易忽略的点cv2.imshow在高分屏上会做缩放本身就有开销调试时可以临时不显示只统计 FPS。5.5 现象换到另一台机器上import mediapipe直接失败原因依赖版本冲突尤其是numpy和protobuf这两个。mediapipe对它们的版本范围有要求如果环境里已经装了别的库锁定了旧版numpy就会在导入时抛出看起来和mediapipe毫无关系的错误。解决新建一个干净的虚拟环境只装opencv-python、mediapipe、numpy三个先跑通再往里面加别的东西。这是最省时间的排查方式——在污染过的环境里逐个降级依赖能折腾一下午。装的时候如果下载很慢换一个能连通的软件源就行。6. 二次开发从示例脚本到能用的眼动模块示例脚本能跑通只是起点真要接进项目第一件事是把它从“一个脚本”变成“一个对象”。我习惯把检测逻辑封成一个持有模型实例的类外部只调两个方法process(frame)拿结果close()释放资源。这样调用方不需要知道模型是什么、初始化参数有几个将来换检测后端的成本被压到最低。class IrisTracker: def __init__(self, static_modeFalse): self.mesh create_face_mesh(static_modestatic_mode, max_faces1) self._smooth {} # 按 key 存历史值做 EMA 平滑 def process(self, frame_bgr): points detect_frame(frame_bgr, self.mesh) if points is None: return None eye_a pick_eye(points, EYE_A) eye_b pick_eye(points, EYE_B) return { offset_a: self._ema(a, iris_offset(eye_a)), offset_b: self._ema(b, iris_offset(eye_b)), ear_a: self._ema(ear_a, eye_aspect_ratio(eye_a)), ear_b: self._ema(ear_b, eye_aspect_ratio(eye_b)), } def close(self): self.mesh.close()平滑那层别省。原始输出的虹膜偏移在静止注视时会持续小幅抖动映射到界面光标上就是永远停不下来的微颤用起来非常难受。指数滑动平均一行就够new alpha * cur (1 - alpha) * oldalpha取 0.2 到 0.4 之间越小平滑越强但延迟越大。这个参数没有通用最优值得拿着秒表边看边调。要做真正的注视点落屏还得补一步标定。做法很朴素让用户依次注视屏幕上九个已知位置每次记录一组虹膜偏移最后用最小二乘拟合一个从偏移到屏幕坐标的仿射变换。标定数据存成配置下次启动直接加载用户不用每次重来。这一步不加四方向判断能凑合九宫格定位就是玄学。改完之后怎么验证没坏我固定走三步先用一张标准正脸图跑单图模式确认还是 478 点且虹膜画在瞳孔上再跑一段录好的视频对比改动前后的偏移量曲线看整体趋势有没有漂最后开摄像头连续跑五分钟盯 FPS 和人脸转侧面时的丢帧率。三步都过才敢往主工程里合。从那以后我每次改这类视觉模块都强制先跑单图再跑视频最后才开摄像头——因为摄像头是唯一不能复现的输入源一旦在它上面才发现问题你连那一帧长什么样都存不下来。希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/24 6:48:40

SpringBoot微信扫码登录实战:OAuth2授权码与Redis票据

简介:面向具备 Java 与 Spring 基础的开发者,这份工程以 Spring Boot 结合微信开放平台,演示 Web 端扫码登录的完整实现,可用于解决第三方站点接入微信授权时流程不清、回调与令牌处理易出错的问题。代码围绕 OAuth 2.0 展开&…

2026/10/24 6:48:40

Mediapipe Iris 虹膜跟踪:478点配置、EAR与距离估计

简介:面向在 Windows 10 环境下使用 Mediapipe Iris 进行眼角、眼睛轮廓与眼球关键点追踪的开发者,这份示例工程提供了可编译的虹膜检测起点,帮助解决从接口调用到结果解析的入门问题。压缩包共 5 个文件,以 2 个 cpp 源文件、1 个…

2026/10/24 6:48:40

起源引擎1 的 Hammer 的3D笔刷

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/24 7:43:43

Qt5翻金币游戏源码解析:QGraphicsView与信号槽实现翻牌交互

简介:QT翻金币游戏源码是一套基于Qt5框架的跨平台小游戏完整工程,面向希望学习GUI布局、动画与窗口管理的Qt初中级开发者。工程围绕开始界面、菜单界面与关卡选择三部分展开,包含20个关卡的状态展示、自定义按钮悬停点击特效、金币翻转动画及…

2026/10/24 7:43:43

AE86椿线下山:第三人称追尾相机与街机漂移调校

“头文字D Arcade Stage 8 无限”里的椿线下山,AE86 第三人称追尾视角,这段画面在不少赛车内容里都出现过。把它放到技术视角看,它其实是一套很典型的组合:第三人称跟随相机 街机化漂移物理 线性油门刹车输入 固定机位录制。这…

2026/10/24 7:43:43

Python 音频特征提取与歌曲初筛打分实践

很多人拿到一首歌,第一反应都是"不知道好不好听"——做歌单要听、做短视频配乐要听、做播客片头也要听,几百首候选全部听完,一天就没了。更麻烦的是,"好听"这件事看起来毫无抓手:有人觉得低频厚实…

2026/10/24 7:43:43

实时控制实现边界:周期、抖动与 3 类验收判据

在工业自动化项目里,「这个平台能不能做实时控制」是一个高频问题,也是一个高频误答的问题。常见的回答只有两种:能,或者不能。前者往往来自一句「我们支持实时」,后者往往来自一次没有拆开分析的现场超时。两种回答都…

2026/10/24 7:43:43

省级SHP三级区划与道路网落图:坐标、编码与县级路网密度统计

简介:这套面向GIS从业者、城市规划与交通研究人员及地理信息专业学生的空间数据包,聚焦四川省2022年7月的行政区划与交通网络底图需求。包内共33个文件,以shp、dbf、shx、prj为主,辅以xml元数据及sbn/sbx空间索引文件,…

2026/10/24 7:38:43

制造业桌面云怎么选?4 款主流方案横向测评对比

制造业数字化改造不断提速,车间工位、研发工位、分支机构三处终端的运维压力,正成为生产型企业的共同负担。传统台式机放进车间,粉尘与震动会缩短硬件寿命,工艺图纸、生产数据散落在本地,管控存在漏洞,产线…

2026/10/23 22:56:35

金融AI落地指南:从调研报告到技术选型与工程实践

简介:这份PDF报告聚焦2025年金融服务业中国AI现状与趋势,面向金融科技从业者、AI战略规划人员及关注行业数字化转型的研究者,帮助读者把握生成式AI在银行、支付、交易等场景中的落地路径与投资方向。资源包内含1个PDF文件,大小约1…

2026/10/24 0:02:40

MATLAB缩进快捷键与批量格式化实战指南

1. 被自动缩进折磨的那些年如果你写过MATLAB代码,大概率经历过这种场景:从某个文档、网页或者聊天记录里复制了一段代码,粘贴进编辑器之后,每一行前面都带着莫名其妙的空格,有的行首是四个空格,有的是一个T…

2026/10/24 0:02:40

用Poco实现Qt风格信号与槽的跨平台C++方案

简介:本资源是一份面向C中级开发者的技术实践工程,聚焦于在非Qt框架环境下模拟实现Qt核心通信机制——信号与槽,借助Poco网络库的事件系统完成解耦式对象通信。适用于需在跨平台C项目中引入松耦合回调机制、但受限于无法集成Qt依赖的开发场景…

2026/10/24 0:02:40

ESXi裸机虚拟化安装指南:从硬件兼容到虚拟机部署

1. 为什么还要折腾ESXi:从一台闲置机器说起手里攒了几台淘汰下来的迷你主机,配置不算差,i5加16G内存,扔了可惜,装个桌面系统又觉得浪费。这种场景下,ESXi就成了一个绕不开的选择。它是一套裸机虚拟化平台&a…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑