// 声明包名,必须与您的项目一致 package com.facesdk.wrapper; // 导入所有必需的 Android 工具 import android.content.Context; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.util.Log; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; /** * 封装了所有 JNI 调用的 SDK 包装器。 * 这是 Android App 唯一应该交互的类。 */ public class FaceSDKWrapper { private static final String TAG = "FaceSDKWrapper"; static { try { // 【【【关键修正】】】 // 我们必须在加载我们自己的库之前, // 首先加载我们的库所依赖的第三方库。 System.loadLibrary("c++_shared"); // 1. 加载 ONNX Runtime (来自 build.gradle.kts) System.loadLibrary("onnxruntime"); // 2. 加载 OpenCV (来自 build.gradle.kts, 库名是 opencv_java4) System.loadLibrary("opencv_java4"); // 3. 最后加载我们自己的库 System.loadLibrary("face_sdk_jni"); } catch (UnsatisfiedLinkError e) { // 如果这里出错,App 无法运行 Log.e(TAG, "!!! 致命错误: 无法加载一个或多个 Native 库 !!!", e); // 抛出运行时异常,使 App 立即停止,而不是稍后崩溃 throw new RuntimeException("Failed to load native libraries", e); } } // 2. 声明 C++ JNI 函数 (必须与 face_sdk_jni.cpp 匹配) // 这些是 Java 和 C++ 之间的“秘密通道” private native long nativeInit(String modelDir); private native float[] nativeExtractFeature(Bitmap bitmap); private native float nativeCompare(float[] feat1, float[] feat2); private native void nativeRelease(); // ----------------------------------------------------------------- // 公开的 Java API 接口 // ----------------------------------------------------------------- // C++ FaceSDK 实例的指针 (句柄) private long nativeHandle = 0; /** * [API 1] 初始化 SDK * 负责复制模型并初始化 C++ 引擎 * * @param context Android 应用上下文 * @return true 成功, false 失败 */ public boolean init(Context context) { if (nativeHandle != 0) { Log.w(TAG, "SDK already initialized."); return true; } // 步骤 1: 复制模型 String modelPath = copyModelsFromAssets(context); if (modelPath == null) { Log.e(TAG, "Failed to copy models from assets."); return false; } // 步骤 2: 调用 C++ JNI 进行初始化 try { this.nativeHandle = nativeInit(modelPath); if (this.nativeHandle == 0) { Log.e(TAG, "JNI nativeInit() returned 0 (Init failed)."); return false; } } catch (Exception e) { Log.e(TAG, "Exception during nativeInit()", e); return false; } Log.i(TAG, "SDK Initialized successfully. Handle: " + this.nativeHandle); return true; } /** * [API 2] 提取特征 * * @param bitmap 包含人脸的 Bitmap * @return 512维特征向量, 或在失败时返回 null */ public float[] extractFeature(Bitmap bitmap) { if (nativeHandle == 0) { Log.e(TAG, "SDK not initialized. Call init() first."); return null; } if (bitmap == null) { Log.e(TAG, "Input bitmap is null."); return null; } try { return nativeExtractFeature(bitmap); } catch (Exception e) { Log.e(TAG, "Exception during nativeExtractFeature()", e); return null; } } /** * [API 3] 比较特征 * * @param feat1 特征1 * @param feat2 特征2 * @return 余弦相似度 */ public float compare(float[] feat1, float[] feat2) { if (nativeHandle == 0) { Log.e(TAG, "SDK not initialized."); return -2.0f; // -2.0 作为错误码 } if (feat1 == null || feat2 == null || feat1.length != 512 || feat2.length != 512) { Log.e(TAG, "Invalid feature vectors for comparison."); return -2.0f; } try { return nativeCompare(feat1, feat2); } catch (Exception e) { Log.e(TAG, "Exception during nativeCompare()", e); return -2.0f; } } /** * [API 4] 释放 SDK * 在 App 退出时调用 */ public void release() { if (nativeHandle != 0) { nativeRelease(); Log.i(TAG, "SDK Released. Handle: " + nativeHandle); nativeHandle = 0; } } // ----------------------------------------------------------------- // 任务 2.6: 模型复制逻辑 // ----------------------------------------------------------------- /** * 将 assets 中的所有 .onnx 模型复制到应用的内部存储。 * C++ 只能从内部存储读取,不能直接读取 assets。 * * @param context 上下文 * @return 模型的存储目录路径, 或在失败时返回 null */ private String copyModelsFromAssets(Context context) { // 这是 C++ 需要的 7 个模型 final String[] modelFiles = { "faceboxesv2-640x640.onnx", "face_landmarker_pts5_net1.onnx", "face_landmarker_pts5_net2.onnx", "face_recognizer.onnx", "model_gray_mobilenetv2_rotcls.onnx", "fsanet-var.onnx", "fsanet-conv.onnx" }; // 目标目录: /data/data/com.facesdk.wrapper/files/models File modelDir = new File(context.getFilesDir(), "models"); if (!modelDir.exists()) { if (!modelDir.mkdirs()) { Log.e(TAG, "Failed to create directory: " + modelDir.getAbsolutePath()); return null; } } AssetManager assetManager = context.getAssets(); // 循环复制每一个模型 for (String filename : modelFiles) { File outFile = new File(modelDir, filename); // 如果文件已存在,跳过复制 (提高启动速度) if (outFile.exists()) { Log.i(TAG, "Model exists, skipping: " + filename); continue; } Log.i(TAG, "Copying model: " + filename); try (InputStream is = assetManager.open(filename); OutputStream os = new FileOutputStream(outFile)) { byte[] buffer = new byte[1024 * 4]; int bytesRead; while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); } } catch (IOException e) { Log.e(TAG, "Failed to copy model: " + filename, e); // 如果任何一个文件复制失败,则清理并返回失败 // (清理是可选的,但更健壮) // cleanUpModels(modelDir); return null; } } Log.i(TAG, "All models copied successfully to: " + modelDir.getAbsolutePath()); // 返回包含模型的目录路径 return modelDir.getAbsolutePath(); } }