简介:本资源为SeetaFace6人脸识别算法的Android DEMO工程,面向需要在移动端集成人脸检测、关键点定位、人脸识别等能力的Android开发者,尤其适合具备一定NDK与JNI基础的中高级工程师。工程同时提供arm64-v8a与armeabi-v7a两套so库,可直接用于真机调试与二次开发。压缩包共565个文件,约146.83MB,包含276个hpp与87个h头文件、75个xml配置、60个so动态库,以及少量java、gradle、mk等构建与源码文件,覆盖算法头文件、模型资源与Android工程结构。目前已有751人学习下载。借助该工程,读者可省去手动编译SeetaFace6的繁琐流程,快速验证人脸检测、活体检测、质量评估、年龄性别估计及口罩检测等模型在Android端的实际效果,并参考作者博文完成环境配置与排错,适合作为移动端人脸识别项目的起步模板。
1. 从一次编译翻车说起:seetaface6 Android DEMO 到底该怎么装
去年帮一个做门禁考勤的团队调 seetaface6 的 Android 端,他们从别处拷来一份 DEMO 工程,armeabi-v7a的 so 能跑,切到arm64-v8a真机直接闪退,logcat 里只有一行UnsatisfiedLinkError。折腾一下午才发现,工程里只塞了 32 位的 so,64 位目录是空的,而现在的手机默认只加载 64 位库。这就是 seetaface6 Android DEMO 最典型的翻车姿势——不是代码写错,是 so 库没配对。
这份资源解决的就是这件事:一个已经配好arm64-v8a和armeabi-v7a两套 so 库的 Android DEMO 工程,拿到手不用自己去编译 C++ 源码、不用折腾 NDK 版本,直接能跑人脸检测、关键点定位这些基础能力。适合两类人:一是刚接触 android seetaface 集成、想先看效果再决定要不要深入的新手;二是被 so 架构问题卡住、只想找个能跑通的参考工程的老手。下面按我实际装一遍的顺序,把环境、导入、配置、验证和排错讲清楚。
2. 装前把环境列清:Android Studio、NDK 与 so 架构的版本取舍
2.1 版本对照表:别让工具链先把你绊倒
seetaface6 的 Android DEMO 本质是一个带 JNI 层的普通 Android 工程,so 库已经预编译好,所以对 NDK 的依赖没有想象中那么重,但版本选错照样出问题。我一般按下面这张表来配,踩坑概率最低。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| Android Studio | 2022.3 及以上 | 老版本对abiFilters提示不友好 |
| Gradle Plugin | 7.4 左右 | 太新会改jniLibs默认路径行为 |
| compileSdk | 33 或 34 | 低于 30 部分 API 缺失 |
| minSdk | 21 | seetaface6 so 最低支持到 21 |
| NDK | 21.4.7075529 | 仅当你要自己重编 so 时才需要 |
| JDK | 11 | JDK 17 偶发 Gradle 同步失败 |
这里有个反直觉的点:很多人以为 so 库工程必须装 NDK,其实这份 DEMO 的 so 是现成的,NDK 只在你想改 C++ 层、重新生成 so 时才用得上。新手阶段完全可以不装 NDK,少一个变量少一份玄学。
2.2 目录结构先看懂:jniLibs 放哪、叫什么名
导入工程前,先确认 so 库的摆放位置。Android 工程加载 so 有两条路:一是放在app/src/main/jniLibs/<ABI>/下,二是放在app/libs/<ABI>/再在 Gradle 里指定sourceSets。这份 DEMO 用的是第一种,标准做法。
# 正确的目录结构,ABI 目录名必须一字不差 app/src/main/jniLibs/ ├── arm64-v8a/ │ └── libseetaface_detector.so # 64 位库 └── armeabi-v7a/ └── libseetaface_detector.so # 32 位库目录名arm64-v8a和armeabi-v7a是 Android 规定的 ABI 标识,写错一个字母,Gradle 打包时不会报错,但运行时会找不到库。so 文件名也要和 Java 层System.loadLibrary("seetaface_detector")里的字符串对应,lib前缀和.so后缀由系统自动补,你写中间那段就行。
2.3 确认手机 ABI:别拿 32 位库往 64 位机器上怼
装之前花十秒确认目标设备的 ABI,能省掉后面半小时的排查。用 adb 一条命令就能看:
# 查看已连接设备的 CPU 架构 adb shell getprop ro.product.cpu.abi # 输出示例:arm64-v8a如果输出是arm64-v8a,说明设备优先加载 64 位库,工程里必须有对应的arm64-v8a目录,否则就会走到UnsatisfiedLinkError。如果输出armeabi-v7a,那 32 位库就够用。现在主流机型基本都是arm64-v8a,所以这份同时带两套 so 的工程才省事——不用自己判断,两个目录都放上,系统按设备自动挑。
3. 导入与配置:Gradle 里 abiFilters 和 jniLibs 怎么写
3.1 导入工程后先做三件事
工程导入 Android Studio 后别急着点运行,先按顺序确认三件事,顺序错了会白等一次 Gradle 同步。
第一步,检查local.properties里的 SDK 路径是否指向你本机的 SDK,路径带空格或中文会同步失败。第二步,打开app/build.gradle,确认ndk { abiFilters }里同时包含两个 ABI。第三步,确认jniLibs目录没有被.gitignore之类的东西排除掉,有时候从压缩包解压会丢文件。
// app/build.gradle 关键片段 android { defaultConfig { ndk { // 同时保留两套 ABI,系统按设备自动选择 abiFilters 'arm64-v8a', 'armeabi-v7a' } } sourceSets { main { // 显式指定 jniLibs 路径,避免默认路径被改后找不到 jniLibs.srcDirs = ['src/main/jniLibs'] } } }abiFilters的作用是告诉 Gradle 只打包这两个架构的 so,不写的话某些版本会把所有 ABI 都塞进去,包体积翻倍。jniLibs.srcDirs是兜底,万一你的工程结构不是标准布局,这行能把它拉回来。这两处是 seetaface6 DEMO 配置里最容易漏的,漏了不一定立刻报错,但运行到加载库那一步就崩。
3.2 加载 so 的 Java 层代码长什么样
配置对了,还得看 Java 层怎么加载。这份 DEMO 一般会在 Application 或某个工具类里做静态加载,代码不长但位置有讲究。
public class SeetaFaceEngine { static { // 名字对应 libseetaface_detector.so 去掉 lib 和 .so System.loadLibrary("seetaface_detector"); } // native 方法声明,具体实现在 so 里 public native long initDetector(String modelPath); public native int detect(long handle, byte[] imageData, int width, int height); }static块保证类第一次被引用时就加载 so,加载失败会抛UnsatisfiedLinkError,这个异常信息里通常带couldn't find "libxxx.so",看到它就直接去查 ABI 目录和文件名,不用怀疑别的地方。native方法的方法名要和 so 里导出的符号一致,这份 DEMO 已经对齐好了,你改方法名就得同步改 C++ 层,新手别动。
3.3 模型文件放哪:assets 还是外部存储
seetaface6 的检测能力依赖模型文件,DEMO 里通常把模型放在assets目录,首次运行时拷贝到应用私有目录再传给 native 层。这一步不涉及 so,但模型路径传错,native 层会返回空句柄,表现是检测一直失败但不崩。
// 从 assets 拷贝模型到 filesDir,再传给 native private String copyModel(String assetName) throws IOException { File outFile = new File(getFilesDir(), assetName); if (!outFile.exists()) { try (InputStream is = getAssets().open(assetName); OutputStream os = new FileOutputStream(outFile)) { byte[] buf = new byte[4096]; int len; while ((len = is.read(buf)) > 0) { os.write(buf, 0, len); } } } // native 层需要的是绝对路径 return outFile.getAbsolutePath(); }常见做法是只在文件不存在时拷贝,避免每次启动都覆盖。返回的必须是绝对路径,native 层不认assets://这种 URI。模型文件一般几十 MB,别放错到会被清理的缓存目录。
4. 避坑与排查:so 加载失败的四种典型现象
4.1 现象:UnsatisfiedLinkError,提示找不到库
原因基本是 ABI 不匹配或目录名写错。先adb shell getprop ro.product.cpu.abi看设备架构,再核对jniLibs下目录名是否完全一致。还有一种隐蔽情况:工程里同时存在jniLibs和libs两个目录,Gradle 只认配置的那个,另一个里的 so 等于没放。解决方法是统一到jniLibs,并在build.gradle里显式声明路径。
4.2 现象:能加载库,但调用 native 方法直接崩
多半是 so 和 Java 层方法签名对不上,或者模型文件路径传了相对路径。用adb logcat | grep -i seeta过滤日志,native 层崩溃前一般会打印模型加载失败的提示。确认模型已拷贝到filesDir且路径是绝对路径,再检查 native 方法名有没有被混淆——release 包开了混淆但没保留 native 方法名,也会崩。
4.3 现象:32 位能跑,64 位闪退
这是最典型的架构坑。原因是arm64-v8a目录缺失或里面的 so 是空文件。检查方法很简单,解压 APK 看lib/arm64-v8a/下有没有对应的 so:
# 解压 APK 查看实际打包进去的 so unzip -l app-release.apk | grep "lib/" # 正常应同时看到 lib/arm64-v8a/ 和 lib/armeabi-v7a/如果只有armeabi-v7a,说明abiFilters没写全或 64 位目录没被识别。补上目录、确认abiFilters包含arm64-v8a,重新打包即可。
4.4 现象:Gradle 同步报 jniLibs 相关错误
常见于 Gradle Plugin 版本和sourceSets写法不匹配。新版插件对jniLibs.srcDirs的赋值方式有变化,如果报Could not set unknown property,改成jniLibs.srcDirs = ['src/main/jniLibs']这种直接赋值,别用+=。另外路径里的反斜杠在 Windows 上要写成正斜杠或双反斜杠,否则会被当转义符。
4.5 现象:装到真机提示 INSTALL_FAILED_NO_MATCHING_ABIS
这个报错和 so 无关,是 APK 里没有任何与设备匹配的 ABI。检查abiFilters是否把设备架构排除了,或者打包时用了splits只出了单一架构的包。解决方法是确认abiFilters包含设备 ABI,或者关掉 ABI splits 打全量包。
5. 装完怎么验证跑通:三步确认 so 真的生效了
5.1 第一步:确认 APK 里两套 so 都在
打包完成后别急着装,先解压 APK 核对。这一步能提前拦住 90% 的架构问题。
# 列出 APK 内所有 so,确认两个 ABI 目录都有内容 unzip -l app-debug.apk | grep "\.so$" # 期望输出包含: # lib/arm64-v8a/libseetaface_detector.so # lib/armeabi-v7a/libseetaface_detector.so如果只看到一个,回到第 3 章的abiFilters检查。两个都在,再往下走。
5.2 第二步:真机安装后看加载日志
装到真机,打开应用,用 logcat 盯加载过程。加载成功不会有明显提示,加载失败会抛异常,所以更稳的做法是在 Java 层加一行日志。
static { try { System.loadLibrary("seetaface_detector"); Log.i("SeetaFace", "so loaded, abi=" + Build.SUPPORTED_ABIS[0]); } catch (UnsatisfiedLinkError e) { Log.e("SeetaFace", "so load failed", e); } }Build.SUPPORTED_ABIS[0]会打印设备首选 ABI,和 APK 里的目录对一下就知道走的是哪套库。看到so loaded且 ABI 是arm64-v8a,说明 64 位库生效了。
5.3 第三步:跑一次检测,看返回结果
最后用一张带人脸的图跑一次检测,确认 native 层真的在工作。DEMO 里一般有现成的按钮,点一下看有没有画出人脸框。如果框出来了,整套环境就算通了。如果没框但没崩,检查模型路径和图片格式——native 层通常要求传 RGB 或 BGR 的字节数组,格式不对会返回 0 个结果。
我自己的习惯是,每次换机器或换 Android Studio 版本后,都强制走一遍这三步:解压看 so、logcat 看加载、跑图看结果。有一次偷懒跳过第一步,结果 64 位目录里是个 0 字节的占位文件,装到真机才崩,又回头查了半天。从那以后这三步一步都不省。希望这份记录帮你少走一段弯路。
本文还有配套的精品资源,点击获取