安卓毕业设计:基于MediaPipe与TFLite的眼动追踪App全链路实现

发布时间:2026/10/13 19:43:34
安卓毕业设计:基于MediaPipe与TFLite的眼动追踪App全链路实现
简介这是一份面向安卓开发方向毕业设计学习者的完整项目源码主题为EyeTracker眼动仪尝试用手机摄像头作为眼动传感器实现目视控制鼠标移动、点击、滚动以及屏幕键盘输入等交互功能为行动不便群体提供低成本的眼球交互方案。资源以zip压缩包形式提供包内共0个文件文件类型明细暂无数据整体大小约21.87MB属于轻量级工程包便于直接导入Android Studio查看与二次开发。项目关键技术覆盖安卓客户端眼动识别移植、校准流程、鼠标控制逻辑以及基于socket的Windows PC与Android设备通信涉及网络编程与模型精度调优参考了Krafka等人的眼动识别研究。目前已有650人学习关注适合作为毕业设计选题参考、课程项目原型或眼动交互入门实践读者可从中获取客户端与服务端通信结构、眼动校准与交互映射思路并据此扩展自己的功能模块。1. 眼动追踪App到底难在哪从摄像头到注视点的完整链路很多人第一次听到「安卓毕业设计app源码-EyeTracker:眼动仪」这个题目脑子里浮现的画面是手机前置摄像头对着脸屏幕上一个小红点跟着眼睛跑。真动手才发现从一帧摄像头画面到一个稳定的屏幕坐标中间隔着人脸检测、关键点定位、头部姿态解算、视线向量回归、屏幕映射五道坎任何一道没处理好光标就会满屏乱跳。这个方向适合两类人一类是毕设需要完整可运行安卓工程、又不想做烂大街的管理系统的同学另一类是想把视觉算法真正落到移动端、理解端侧推理全链路的开发者。它解决的核心问题是在不依赖专用红外眼动仪硬件的条件下用普通手机摄像头估计用户注视屏幕的位置。难点不在算法本身而在移动端的算力、延迟和光照鲁棒性三者互相拉扯。下面按「先跑通、再调准、最后避坑」的顺序拆开讲。2. 技术选型与最小可跑通链路MediaPipe TFLite 怎么搭2.1 为什么是「人脸关键点 回归」而不是端到端眼动估计有两条主流路线。第一条是端到端直接把眼部图像块喂给网络输出屏幕坐标。第二条是分阶段先检测人脸和眼部关键点再根据眼睛、头部姿态、屏幕几何关系回归出注视点。毕设场景我一般推荐第二条原因有三个。第一端到端模型对数据量要求极高公开的眼动数据集规模远不如人脸关键点数据集自己采集几百条根本训不动。第二分阶段链路的每一环都能单独调试和可视化出问题时你知道是检测歪了还是回归飘了而不是面对一个黑匣子。第三安卓端现成的人脸关键点方案成熟MediaPipe Face Mesh 能给出 468 个三维关键点其中就包含虹膜和眼睑的精细位置直接拿来用能省掉大量标注工作。具体到关键点我们真正关心的是这几组左右眼各四个眼角点、上下眼睑中点、以及虹膜中心。MediaPipe 的 refine_landmarks 模式会额外输出每只眼睛 4 个虹膜点取平均就是瞳孔中心。有了瞳孔中心和眼角点就能算出眼睛在眼眶里的相对偏移量这个偏移量是视线方向最直接的代理特征。2.2 安卓工程的最小依赖与目录结构一个能跑的最小工程依赖其实不多。下面是我常用的 Gradle 配置片段注意 MediaPipe 的 Tasks 库和 TFLite 的 Interpreter 是两套东西别混。// app/build.gradle android { // 摄像头用 CameraX别再用已废弃的 Camera1 compileSdk 34 defaultConfig { minSdk 24 // MediaPipe Tasks 要求 24 以上 targetSdk 34 } } dependencies { // CameraX负责预览和取帧 implementation androidx.camera:camera-core:1.3.0 implementation androidx.camera:camera-camera2:1.3.0 implementation androidx.camera:camera-lifecycle:1.3.0 // MediaPipe Tasks Vision人脸关键点检测 implementation com.google.mediapipe:tasks-vision:0.10.9 // TFLite跑我们自己的视线回归小模型 implementation org.tensorflow:tensorflow-lite:2.14.0 implementation org.tensorflow:tensorflow-lite-gpu:2.14.0 }目录上建议把「取帧」「检测」「回归」「映射」拆成四个包别全塞进 MainActivity。取帧用 CameraX 的 ImageAnalysis 用例设置 STRATEGY_KEEP_ONLY_LATEST保证不会因为处理慢而堆积帧。检测层封装一个 FaceLandmarkerHelper输入 Bitmap 或 ImageProxy输出归一化关键点。回归层加载 TFLite 模型输入是拼好的特征向量输出是屏幕归一化坐标。映射层负责把归一化坐标乘上屏幕宽高再做一层平滑滤波。2.3 从关键点到注视点的特征工程这一步是整条链路里最容易被低估的。直接把 468 个点全丢给网络维度太高、噪声太大。我一般手工构造一个 12 维特征向量左右眼瞳孔相对眼角中心的水平偏移、垂直偏移各 2 维两眼间距 1 维头部偏航角和俯仰角的粗略估计各 1 维再加上两眼开合度 2 维。头部姿态可以用鼻尖、下巴、左右耳根这几个点做简单的几何估算不需要上 solvePnP毕设够用。# 特征构造示意训练侧推理侧逻辑一致 def build_feature(landmarks, screen_w, screen_h): # landmarks: shape (468, 3)已归一化到 [0,1] left_iris landmarks[468:472].mean(axis0) # 左虹膜中心 right_iris landmarks[473:477].mean(axis0) # 右虹膜中心 left_corner_l, left_corner_r landmarks[33], landmarks[133] right_corner_l, right_corner_r landmarks[362], landmarks[263] # 瞳孔在眼眶内的相对位置这是视线方向的核心特征 l_dx (left_iris[0] - left_corner_l[0]) / (left_corner_r[0] - left_corner_l[0] 1e-6) l_dy (left_iris[1] - landmarks[159][1]) / (landmarks[145][1] - landmarks[159][1] 1e-6) r_dx (right_iris[0] - right_corner_l[0]) / (right_corner_r[0] - right_corner_l[0] 1e-6) r_dy (right_iris[1] - landmarks[386][1]) / (landmarks[374][1] - landmarks[386][1] 1e-6) # 头部姿态粗估鼻尖相对两眼中心的偏移 eye_center (left_iris right_iris) / 2 nose landmarks[1] yaw (nose[0] - eye_center[0]) * 2.0 pitch (nose[1] - eye_center[1]) * 2.0 return [l_dx, l_dy, r_dx, r_dy, yaw, pitch]这段代码的关键在于所有偏移量都做了归一化除以眼眶宽度或眼睑高度这样不同人眼型大小不同也不会影响特征尺度。yaw 和 pitch 乘 2 是个经验系数目的是让头部姿态在特征里占的权重大一些因为头部转动对注视点的影响比眼球转动更大。实际训练时这 6 维特征加上两眼间距和开合度凑成 10 到 12 维用两三层全连接就能拟合得不错。2.4 屏幕映射与坐标变换模型输出的是归一化坐标映射到屏幕要经过三步。第一步把模型输出从 [0,1] 还原到屏幕像素。第二步处理前置摄像头的镜像问题——前置预览默认是镜像的但用户看屏幕时左右方向和摄像头看到的是反的所以水平坐标要翻转。第三步做时间平滑。原始输出帧间抖动很大直接显示会像帕金森。我一般用一阶低通滤波alpha 取 0.3 到 0.5 之间太小会延迟明显太大又压不住抖动。// 平滑与映射alpha 越小越平滑但延迟越大 class GazeSmoother(private val alpha: Float 0.4f) { private var sx 0.5f private var sy 0.5f fun update(nx: Float, ny: Float): PairFloat, Float { // 一阶低通新值 alpha * 当前 (1-alpha) * 历史 sx alpha * nx (1 - alpha) * sx sy alpha * ny (1 - alpha) * sy return Pair(sx, sy) } }alpha 这个参数没有标准答案得根据你的帧率调。30fps 下 0.4 比较均衡15fps 下要降到 0.25 左右否则延迟会让人明显感觉光标跟不上眼睛。3. 模型训练与端侧部署数据、量化与推理加速3.1 训练数据的采集与标注方式毕设没有专用眼动仪标注怎么办。常见做法是做一个全屏标定流程屏幕上依次出现 9 个或 16 个目标点用户依次注视每个点程序同时记录当前帧的特征向量和目标点坐标。每个点采集 2 到 3 秒按 30fps 算就是 60 到 90 个样本16 个点下来接近 1500 条。一个人采一轮大概两分钟找五六个同学帮忙就能凑到七八千条。这里有个血泪经验采集时头部一定要有轻微自然晃动不能像木头一样定住否则训出来的模型对头部姿态完全不鲁棒换个坐姿就废了。标注本身不需要人工目标点坐标就是标签这是标定式采集最大的好处。但要注意剔除眨眼帧判断依据是眼睛开合度低于阈值这些帧的特征是错的留着会污染训练集。3.2 一个够用的回归网络结构不需要上 ResNet输入才十几维用全连接就够。下面这个结构我实测在几千条数据上就能收敛验证集平均误差能压到屏幕宽度的 4% 到 6%。import tensorflow as tf def build_gaze_model(input_dim12): model tf.keras.Sequential([ tf.keras.layers.Input(shape(input_dim,)), tf.keras.layers.Dense(64, activationrelu), tf.keras.layers.Dropout(0.2), # 防过拟合数据少时必加 tf.keras.layers.Dense(32, activationrelu), tf.keras.layers.Dense(16, activationrelu), tf.keras.layers.Dense(2, activationsigmoid) # 输出归一化屏幕坐标 ]) model.compile( optimizertf.keras.optimizers.Adam(1e-3), lossmse, metrics[mae] ) return model损失用 MSE 就行因为输出是连续坐标。如果发现某个区域误差特别大比如屏幕四角可以改用加权 MSE给边缘样本更高权重。Dropout 在数据量少于一万条时几乎必加否则训练 loss 降得很快但验证 loss 反弹典型的过拟合。3.3 量化与 TFLite 转换训练完的 Keras 模型要转成 TFLite 才能在安卓上跑。直接转 float32 模型大概几十 KB其实已经很小了但为了推理速度可以上动态范围量化或全整型量化。动态范围量化最简单一行代码精度损失几乎可以忽略。converter tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations [tf.lite.Optimize.DEFAULT] # 动态范围量化 tflite_model converter.convert() with open(gaze.tflite, wb) as f: f.write(tflite_model)如果要用 GPU 代理加速注意 GPU 代理对全整型量化支持不好动态范围量化反而更稳。毕设场景下这个模型推理耗时在 1ms 以内瓶颈根本不在回归网络而在 MediaPipe 的关键点检测那部分大概占 15 到 25ms。所以优化重点应该放在降低检测频率上比如每两帧检测一次中间帧用光流或直接复用上一帧关键点。3.4 安卓端加载与推理端侧加载 TFLite 模型用 Interpreter 就行。注意输入输出的数据类型和形状要和训练时严格一致否则会抛异常或者输出垃圾值。// 加载模型并推理 val interpreter Interpreter(loadModelFile(gaze.tflite)) val input Array(1) { FloatArray(12) } // 12 维特征 val output Array(1) { FloatArray(2) } // 归一化坐标 fun predict(feature: FloatArray): PairFloat, Float { input[0] feature interpreter.run(input, output) return Pair(output[0][0], output[0][1]) }这里有个容易翻车的点特征构造的顺序必须和训练时完全一致差一个维度或者顺序错了模型不会报错但输出会完全乱掉。建议把特征名和顺序写成一个常量数组训练和推理共用同一份定义。4. 避坑与排查那些让光标乱跳的元凶4.1 光标持续偏向一侧现象是无论怎么标定光标总是稳定地偏左或偏右。原因通常是前置摄像头的镜像处理搞反了。前置预览默认镜像但如果你在映射时又翻转了一次就等于没翻。解决办法是拿一个已知位置的点测试让用户注视屏幕正中央看输出坐标是否接近 0.5如果稳定偏到 0.3 或 0.7就是镜像问题。排查时把镜像开关做成可配置项现场切换验证。4.2 眨眼瞬间光标飞到屏幕角落现象是每次眨眼光标会突然跳到一个固定或随机的远处。原因是眨眼帧的虹膜关键点会失效MediaPipe 在闭眼时给出的虹膜位置是估计值可能完全错误。解决办法是在特征构造前加一道开合度判断低于阈值直接跳过该帧复用上一帧的平滑结果。开合度可以用上下眼睑关键点的距离除以眼眶宽度来算阈值取 0.15 左右比较稳。4.3 头部稍微一动就完全失准现象是用户坐正时还行一歪头或前后移动误差急剧变大。原因是训练数据里头部姿态变化太少模型没学到头部姿态和注视点的耦合关系。解决办法有两个一是采集数据时强制要求头部自然晃动覆盖前后左右各方向二是在特征里显式加入头部姿态估计并且给它足够的权重。如果已经训完了不想重采可以做一个在线微调让用户在使用前做一次 9 点标定用这 9 个点对模型输出做一个线性校正能明显改善。4.4 帧率上不去光标延迟明显现象是预览很流畅但光标移动总是慢半拍。原因通常是 ImageAnalysis 的回调在跑检测而检测耗时超过了帧间隔导致处理队列堆积。解决办法是设置 STRATEGY_KEEP_ONLY_LATEST并且把检测频率降下来比如每三帧检测一次中间帧用上一帧的关键点加简单位移补偿。另外确认没有在主线程做推理TFLite 的 run 要放在后台线程。4.5 不同手机效果差异巨大现象是在 A 手机上能用换到 B 手机误差翻倍。原因是不同手机前置摄像头的视场角、畸变、安装位置都不同模型是在特定设备上训的换设备就分布偏移了。解决办法是每台设备单独做一次标定或者采集数据时就用多台设备混合训练。毕设如果只要求在一台设备上演示这个问题可以暂时绕过但要在论文里说明局限性。5. 精度验证与进阶技巧把误差压到可接受范围验证眼动估计精度标准做法是计算预测点和真实注视点的欧氏距离再除以屏幕对角线长度得到归一化误差。行业里专用眼动仪能做到 1% 到 2%手机摄像头方案做到 4% 到 6% 就算不错毕设能压到 5% 以内基本够看。验证时不要只用训练时的标定点要另取一组没参与训练的点比如屏幕上的随机位置让用户注视并记录这样得到的才是真实泛化误差。# 精度评估归一化欧氏误差 import numpy as np def evaluate(preds, targets, screen_diag): # preds/targets: (N, 2)单位像素 dist np.linalg.norm(preds - targets, axis1) return (dist / screen_diag).mean() # 平均归一化误差想再往上提精度有几个具体技巧。第一做分区校正把屏幕分成九宫格每个格子单独拟合一个线性偏移量因为误差往往不是均匀分布的边缘区域偏差更大。第二引入头部姿态补偿当检测到头部有明显转动时对输出坐标做一个基于姿态的偏移修正这个修正量可以用少量标定数据拟合。第三用多帧投票连续 5 帧的输出取中位数而不是均值中位数对偶发的跳变帧更鲁棒。还有一个容易被忽略的点是屏幕亮度和环境光。强光下瞳孔收缩虹膜特征会变化模型误差会上升。如果演示环境可控尽量在室内均匀光照下进行。如果要做鲁棒版本可以在特征里加入图像整体亮度作为一个维度让模型自己学会补偿。我自己踩过最深的一个坑是早期版本没做眨眼过滤答辩演示时一紧张眨了几下眼光标直接飞到屏幕外场面一度非常尴尬。后来加了开合度判断和平滑滤波才稳定下来。所以如果你只记一件事就记住眼动追踪的稳定性比精度更重要一个偶尔偏一点但从不乱跳的光标体验远好于一个大部分时候很准但时不时抽风的光标。希望帮到你。本文还有配套的精品资源点击获取