简介:这份资源是面向安卓开发方向毕业设计学习者与计算机交互技术爱好者的开源项目源码,主题为EyeTracker眼动仪,尝试用安卓手机充当眼动传感器,通过目视控制实现鼠标移动、点击、滚动及屏幕键盘键入,为行动不便群体提供低成本的操作电脑方案。项目包含安卓客户端与服务端两部分:客户端完成眼动识别移植、校准与基础交互控制,服务端借助socket实现Windows PC与安卓设备的网络通信,并依据识别精度调整交互方式,适合作为网络编程与移动端视觉应用的实践参考。压缩包为zip格式,约21.87MB,文件总数暂未提供,具体类型明细亦无数据,可结合源码目录自行梳理模块结构。目前已有651人学习下载,读者可从中获取眼动交互的完整实现思路、客户端与服务端通信框架及校准控制逻辑,便于在此基础上完成毕设改造或功能扩展。
1. 眼动追踪App源码拆解:一个安卓毕业设计能跑出什么
安卓上做眼动追踪,第一反应往往是“手机前置摄像头那点帧率能行吗”。我最初也这么想,直到把一个叫 EyeTracker 的安卓毕业设计源码包完整跑了一遍——它用前置摄像头做瞳孔定位,配合屏幕注视点映射,在一台中端机上跑出了能用的实时追踪效果。这份资源适合三类人:正在找安卓+计算机视觉方向毕业设计题目的学生、想快速搭一个眼动交互原型的开发者、以及需要一套可复现的摄像头图像处理管线做二次开发的人。它不是什么工业级眼动仪替代品,但作为一套结构完整、依赖清晰、能编译能跑的安卓工程,它把“摄像头取帧→人脸/眼部区域定位→瞳孔中心提取→屏幕坐标映射”这条链路走通了,省掉了从零搭 OpenCV4Android 环境的那几天折腾。
2. 工程结构与技术栈:先看清这套源码的骨架
拿到一个安卓源码包,最忌讳上来就点 Run。先花十分钟把目录和依赖理清楚,后面编译报错能少一半。EyeTracker 这类眼动追踪 App 的工程结构,和普通安卓项目最大的差别在于它多了一层 native 视觉处理,所以目录里会同时出现 Java/Kotlin 业务代码和 C++ 图像处理代码。
2.1 目录分层与模块职责
典型的工程会分成这么几块:app/src/main/java下是 Activity、相机管理、追踪服务这些业务逻辑;app/src/main/cpp下是 OpenCV 相关的 JNI 桥接和瞳孔检测算法;app/src/main/res里放布局和相机预览的配置;根目录的CMakeLists.txt和build.gradle决定整个工程能不能编过。我一般会先打开build.gradle看三样东西:compileSdkVersion、minSdkVersion、以及dependencies里 OpenCV 是以什么形式引入的——是本地.aar、是 Maven 坐标、还是直接把jniLibs塞进工程。这三种方式对应的排错路径完全不同。
# 先摸清工程依赖形态,别急着编译 cd EyeTracker grep -rn "opencv" app/build.gradle find . -name "*.aar" -o -name "*.so" | head -20 ls app/src/main/cpp/这段命令的作用是快速定位 OpenCV 的接入方式。如果grep出来是implementation 'org.opencv:opencv:4.x.x'这种坐标,说明走的是 Maven 依赖,编译时自动拉取;如果find出一堆.so文件,说明是手动集成的 native 库,这时候就要重点检查jniLibs下的 ABI 目录(armeabi-v7a、arm64-v8a)是否和你的测试机匹配。ls cpp/则是确认有没有 JNI 源码需要 CMake 参与编译——有的话,CMakeLists.txt里的find_library路径写错是最常见的翻车点。
2.2 相机取帧与图像处理管线
眼动追踪的实时性瓶颈几乎全在取帧和预处理这一段。这套源码的管线大致是:Camera2 API 或 CameraX 拿到预览帧 → 转成灰度图 → 人脸检测缩小搜索范围 → 眼部区域裁剪 → 阈值分割/边缘检测找瞳孔轮廓 → 拟合圆心。每一步都在和帧率抢时间,所以代码里通常会有降采样和 ROI(感兴趣区域)裁剪的优化。
// 相机帧回调里做降采样,别拿全分辨率硬算 @Override public void onImageAvailable(ImageReader reader) { Image image = reader.acquireLatestImage(); // 常见做法:先缩到 640x480 再进算法,帧率能翻倍 Mat src = imageToMat(image); Mat gray = new Mat(); Imgproc.cvtColor(src, gray, Imgproc.COLOR_RGBA2GRAY); Imgproc.resize(gray, gray, new Size(640, 480)); nativeProcessFrame(gray.getNativeObjAddr()); // 交给 native 层 image.close(); }这里的关键参数是降采样目标尺寸。640×480 是眼动场景下比较稳的折中——再小瞳孔轮廓就糊了,再大中端机扛不住 30fps。nativeProcessFrame通过getNativeObjAddr()把 Mat 的指针传给 C++ 层,避免在 Java 层做像素级操作,这是安卓视觉类 App 的标准做法。如果你在自己的机器上跑出来卡顿,第一件事就是把resize的目标尺寸往下调一档试试,而不是去优化算法本身。
2.3 瞳孔定位的核心思路
瞳孔检测这块,源码里大概率用的是“灰度阈值 + 轮廓筛选”的经典组合,而不是深度学习模型——毕业设计级别的工程很少上模型,因为模型体积和推理耗时在手机上不好控。思路是:眼部 ROI 内做二值化,瞳孔在灰度图上通常是最暗的区域,阈值分割后找轮廓,按面积和圆度筛掉睫毛、反光点这些干扰,最后用最小外接圆或矩拟合出瞳孔中心。
// 瞳孔轮廓筛选的典型逻辑(参数需按实际光照调) std::vector<std::vector<cv::Point>> contours; cv::findContours(binaryEye, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_SIMPLE); for (auto& c : contours) { double area = cv::contourArea(c); if (area < 50 || area > 2000) continue; // 面积过滤:太小是噪点,太大是眼睑 double circularity = 4 * CV_PI * area / (cv::arcLength(c, true) * cv::arcLength(c, true)); if (circularity < 0.6) continue; // 圆度过滤:瞳孔接近圆形 cv::minEnclosingCircle(c, center, radius); }面积上下限和圆度阈值是这套算法最需要手调的三个参数。area的 50~2000 是像素面积的经验区间,和你的 ROI 尺寸强相关,换了分辨率就得重算;circularity用 0.6 作为下限,是因为真实瞳孔被眼睑遮挡时圆度会掉,卡太严会频繁丢帧。我一般会先把中间结果画到预览上,肉眼确认轮廓筛得对不对,再去调数值——盲调参数是血泪教训。
3. 从编译到跑通:一份可复现的落地步骤
源码能编译和能跑出追踪效果是两回事。这一章按“环境准备→编译→权限与相机→标定映射”的顺序走,每一步都给出可对照的检查点,跑不通时知道该看哪。
3.1 环境与依赖对齐
安卓视觉工程的编译失败,八成出在 SDK/NDK 版本和 OpenCV 版本对不上。先把本地环境和工程要求对齐:Android Studio 版本、Gradle 插件版本、NDK 版本、CMake 版本,这四样任何一个错位都可能报出看不懂的 native 链接错误。
| 组件 | 常见要求 | 检查方式 |
|---|---|---|
| compileSdk | 与工程 targetSdk 一致 | 看build.gradle |
| NDK | 工程指定版本,别用最新 | local.properties或 SDK Manager |
| CMake | 3.10 以上 | CMakeLists.txt首行 |
| OpenCV | 4.x,ABI 与测试机匹配 | jniLibs目录或依赖坐标 |
对齐之后先做一次./gradlew assembleDebug,别在 IDE 里点运行——命令行报错信息更完整,能直接看到是 Java 编译错还是 native 链接错。如果是UnsatisfiedLinkError,基本就是.so的 ABI 和手机架构不匹配,去jniLibs里补上对应目录即可。
3.2 相机权限与预览配置
眼动追踪必须拿到相机权限,而且要用前置摄像头。安卓 6.0 以后权限是运行时申请的,源码里如果只写了AndroidManifest.xml的静态声明,在真机上会直接黑屏。检查MainActivity里有没有requestPermissions的调用,以及onRequestPermissionsResult里有没有真正去开相机。
// 运行时权限的最小闭环,缺一个都会黑屏 if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA}, REQ_CAMERA); } else { startCamera(); // 权限已给,直接开预览 }REQ_CAMERA是自定义的请求码,回调里要用同一个值去比对。startCamera()里还要注意选前置摄像头——CameraCharacteristics.LENS_FACING_FRONT,选错了就变成拍环境,瞳孔根本不在画面里。预览分辨率也别贪高,1080p 预览在中端机上掉帧明显,720p 足够算法用。
3.3 注视点标定与屏幕映射
瞳孔中心是摄像头坐标系里的像素坐标,要变成“用户在看屏幕哪个位置”,中间必须做一次映射。这套源码通常提供一个简单的标定流程:屏幕上依次出现几个已知坐标的点,用户盯着看,程序记录对应的瞳孔坐标,拟合出一个仿射或多项式映射关系。
# 标定映射的拟合思路(源码里多为 Java 实现,这里用 Python 说明逻辑) import numpy as np # 标定点:屏幕坐标 (sx, sy) 与对应瞳孔坐标 (px, py) screen_pts = np.array([[100,100],[900,100],[900,600],[100,600]], dtype=np.float32) pupil_pts = np.array([[320,240],[400,238],[398,300],[322,302]], dtype=np.float32) # 仿射变换:2x3 矩阵,够用且不易过拟合 M, _ = cv2.estimateAffine2D(pupil_pts, screen_pts) # 实时映射:新瞳孔点乘 M 即得屏幕坐标标定点数量建议 4~9 个,太少映射不准,太多用户盯得累还容易引入噪声。estimateAffine2D给出的是 2×3 仿射矩阵,对头部基本不动的场景够用;如果用户头会晃,就得考虑加二次项或者做头部姿态补偿,那是进阶话题。标定完一定要让用户重新看几个随机点做验证,误差超过屏幕宽度的 10% 就重标——这一步很多源码包省略了,但实际用起来差别很大。
4. 避坑与排查:真机上最容易翻车的五件事
4.1 预览正常但追踪框不动
现象是相机画面出来了,但瞳孔检测的叠加层一直停在固定位置或者干脆不刷新。原因通常是帧回调里image.close()没调用,ImageReader 的缓冲区被占满后不再回调新帧。解决方法是确保每个acquireLatestImage()都有配对的close(),并且放在 try-finally 里,异常路径也不能漏。
4.2 瞳孔检测在强光下完全失效
现象是室内能用,一到窗边或灯光直射就检测不到瞳孔。原因是瞳孔阈值分割依赖“瞳孔是最暗区域”这个假设,强光下角膜反光形成高亮斑点,破坏了灰度分布。解决办法是在阈值分割前先做一次反光抑制——对高亮区域做形态学闭运算或者直接掩膜掉,再找轮廓;同时把阈值从固定值改成基于 ROI 均值的自适应阈值。
4.3 编译报 native 库找不到符号
现象是UnsatisfiedLinkError: dlopen failed或undefined reference。原因九成是 ABI 不匹配:工程只带了armeabi-v7a的.so,测试机是arm64-v8a。解决方法是补齐jniLibs下对应 ABI 目录,或者在build.gradle的ndk { abiFilters }里显式声明只打你有的架构,避免装到不支持的机器上。
4.4 标定后注视点整体偏移
现象是标定流程走完了,但用户看屏幕中心,光标却偏到一边。原因是标定时用户头部位置和实际使用时的位置不一致,仿射矩阵是在旧姿态下拟合的。解决办法是标定前提示用户保持自然坐姿,标定和使用之间不要大幅移动;工程层面可以加一个“重新标定”的快捷入口,别让用户只能杀进程重来。
4.5 长时间运行后帧率断崖下跌
现象是刚启动很流畅,跑几分钟后越来越卡。原因是每帧都new Mat()却没有释放,native 内存持续增长触发 GC 或 OOM。解决办法是把 Mat 对象提到循环外复用,用Mat.release()或让 JNI 层管理生命周期;Java 侧的 Bitmap 同理,别在帧回调里反复创建。
5. 二次开发与验证:把这份源码用出毕业设计之外的价值
跑通只是起点。这套 EyeTracker 源码真正的价值在于它是一条完整的“摄像头→视觉算法→屏幕交互”链路,你可以在这条链路上做很多毕业设计之外的延伸。我一般会先做一件事:把中间结果可视化。在预览画面上叠加瞳孔轮廓、ROI 框、标定点,肉眼确认每一步的输出对不对,这比看日志高效得多。验证追踪精度也有个土办法——让测试者盯着屏幕上均匀分布的九个点,记录映射后的落点,算平均误差和最大误差,误差在屏幕对角线 5% 以内就算可用,超过 10% 就得回头查标定或算法参数。
二次开发方向上,比较现实的有三条。一是换检测算法:把阈值+轮廓换成基于灰度投影的瞳孔定位,或者引入轻量的人脸关键点模型先精确定位眼部,再在眼区内找瞳孔,鲁棒性会明显提升。二是加交互层:把注视点做成“注视 dwell 点击”——盯住某个按钮超过 800ms 触发点击,这是眼动交互最经典的范式,代码量不大但演示效果很好。三是做数据记录:把每帧的瞳孔坐标、时间戳、映射后的屏幕坐标落盘成 CSV,后面可以做注视轨迹热力图,答辩时比一段实时视频有说服力。
# 验证阶段建议开一个轻量日志,别用 Log.d 刷屏 adb logcat -s EyeTracker:V | grep -E "pupil|calib|fps"这条命令只过滤追踪相关的日志标签,pupil看瞳孔坐标是否稳定,calib看标定矩阵是否合理,fps看实时性有没有掉。我习惯在每次改完算法参数后先跑这条命令看三十秒,确认没有异常跳变再上真机交互测试。
最后说个习惯:这类视觉类安卓工程,我每次改完 native 层代码都会先./gradlew clean再重新编译,因为 CMake 的增量编译偶尔会漏掉.so的更新,导致改了代码却跑的是旧库,排查半天以为是算法问题。从那以后我每次动 C++ 都强制走一遍 clean build,省下的时间远比那几十秒多。希望这份拆解能帮你把这份源码顺利跑起来,少走几个我踩过的坑。
本文还有配套的精品资源,点击获取