news 2026/10/15 19:11:43

SeetaFace6 Android集成实战:ABI适配与JNI调用避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SeetaFace6 Android集成实战:ABI适配与JNI调用避坑指南

简介:SeetaFace6 是中科视拓开放的人脸识别算法集合,对应到 Android 平台的这份 Demo 工程面向移动端开发者,内置 arm64-v8a 与 armeabi-v7a 双架构 so 动态库,可直接集成到 Android 项目,快速调用人脸检测、关键点定位、人脸识别、活体检测、口罩检测、质量评估、年龄性别估计等能力。压缩包共 565 个文件,以 hpp/h 头文件(接口与声明)、xml(模型配置)、so(预编译库)为主,并包含 java/cpp 调用代码、Gradle 构建脚本及 gradlew 封装,整体约 146.83MB,目录结构便于按模块查阅。目前已有 751 人下载学习。相比手工编译 SeetaFace6 的方式,这份工程省去了 NDK 交叉编译与依赖配置环节,拿到后可在 Android Studio 中直接打开、编译运行,便于快速跑通人脸识别全流程,理解 JNI 衔接、模型加载和算法调用方式,适合移动端算法集成、二次开发及入门参考使用。

1. 为什么拿到 seetaface6 AndroidDEMO 工程后,第一件事是检查 so 库架构

如果你接手过一个带 so 库的 Android 人脸识别项目,大概率经历过这种场景:同一个 APK,在测试机上跑得好好的,发给客户后直接崩在启动页。打开 logcat 一看,java.lang.UnsatisfiedLinkError,后面跟着一句dlopen failed: library "libseeta.so" not found。这类问题的根源,十有八九不在代码逻辑,而在 so 库的 ABI 架构没覆盖全。

seetaface6 AndroidDEMO 工程就是典型代表。它把 SeetaFace6 的离线人脸检测、关键点定位、特征提取和比对能力封装成了 Android 能直接调用的 JNI 接口,同时内置了arm64-v8a和armeabi-v7a两套 so 库。前者对应 64 位 ARM 设备,基本覆盖 2017 年之后的手机;后者对应 32 位 ARM 设备,主要服务老型号、低内存设备和一些特殊行业终端。缺少任何一套,都会在对应架构的设备上出现 so 加载失败。

这个工程解决的痛点是:你不需要自己从零编译 SeetaFace6 的 C++ 源码,也不用自己写 JNI 胶水层。拿到工程后,改一改包名、塞进自己的模型文件、调一下检测阈值,就能在 Android 上跑通人脸检测和 1:1 比对。适合三类人:一是要做离线人脸能力验证的算法工程师,二是要快速出 Demo 的 APP 开发,三是需要把 SeetaFace6 集成进现有 Android 应用的 SDK 工程师。

接下来,我从工程导入、JNI 调用、ABI 适配到踩坑记录,完整过一遍这个 Demo 的落地路径。

2. 工程结构和 ABI 选型:先搞懂 arm64-v8a 与 armeabi-v7a 的关系

2.1 工程目录里每个文件夹是干什么的

拿到工程后,先用 Android Studio 打开,等待 Gradle 同步完成。同步过程中,重点确认app/src/main下的目录结构。常见做法是:

app/src/main/ ├── java/ # Kotlin/Java 源码,包含 MainActivity 和 JNI 封装类 ├── jniLibs/ # 存放 so 库的目录 │ ├── arm64-v8a/ │ └── armeabi-v7a/ ├── assets/ # 模型文件目录,通常放 face_detector.csta 等 └── res/ # 布局和资源文件

jniLibs 是 Android Gradle 插件的约定目录,里面的 so 文件会自动打包进 APK。arm64-v8a是 64 位 ARM 架构,armeabi-v7a是 32 位 ARM 架构,两者是独立目录,不能混放。如果某个架构目录下没有对应的 so 文件,在该架构设备上运行时会直接报UnsatisfiedLinkError。

assets 目录放的是 SeetaFace6 的模型文件。需要确认这些模型文件的命名与 JNI 层SeetaFace6JNI初始化代码中的文件名是否一致。曾见过有人把模型放在assets/models/子目录里,但 JNI 层代码写的是直接从assets根目录读取,结果初始化时报找不到模型。

2.2 为什么很多工程只保留这两套 ABI

Android 支持的 ABI 种类其实不少,包括x86、x86_64、mips等。但对于人脸识别这种以 ARM 手机为主要载体的场景,arm64-v8a和armeabi-v7a已经覆盖 95% 以上的真实设备。x86 模拟器上跑 Demo 不是不行,但性能参考价值不大,而且很多开源工程压根没编 x86 的 so。

建议检查app/build.gradle里的abiFilters配置,确认是否做了一致性限制:

android { defaultConfig { ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' } } }

注意,abiFilters配合jniLibs目录使用时会做交集过滤。如果jniLibs里没有x86的 so,而abiFilters写了x86,构建会报错。保持两者一致是底线。

2.3 先说清楚:Demo 里没有的模块,不要指望直接调用

SeetaFace6 包含人脸检测、关键点定位、人脸识别、属性分析(年龄/性别/表情)、活体检测、质量评估等多个模块。但 Demo 工程不一定把所有模块都编译进了 so。打开SeetaFace6JNI.java看native方法声明,就能知道这个工程实际暴露了哪些能力。常见的只有四个:face_detect、face_landmark、face_recognize、face_compare。如果你的业务需要活体检测和防伪能力,得确认 so 库中确实包含对应的anti_spoofing函数,否则会出现UnsatisfiedLinkError或null返回。

这一步能帮你提前判断这个 Demo 是一个完整 SDK 还是一个最小验证工程,避免后面做到一半才发现能力缺失。

3. 跑通最小 Demo:从 Gradle 导入到人脸检测

3.1 修改包名和应用名(三项)

导入工程后,直接编译运行大概率能跑,但这是别人的包名。需要改成自己的,否则后面接自己的后台服务或推送时会有冲突。具体改三处:build.gradle里的applicationId、MainActivity所在包目录、AndroidManifest.xml 里的package属性(如果 Manifest 里写了包名的话)。

android { defaultConfig { applicationId "com.yourcompany.seetaface" // ... } }

改包名时有一个细节常被忽略:native方法所在的类如果用了package标识,JNI 函数名的自动映射规则会受影响。这个 Demo 用的 JNI 写法通常是Java_类名_方法名的静态注册方式,改包名时如果改了类名或包名,需要同步修改 C++ 层的函数名或改为RegisterNatives动态注册。不过,若你只是改applicationId而不改 Java 类名和包路径,则不影响 JNI 层。

3.2 把模型放到 assets 目录

SeetaFace6 的模型文件有固定的文件名约定。常见做法是:

assets/ ├── face_detector.csta # 检测模型 ├── face_landmarker_pts5.csta # 5点关键点模型 ├── face_landmarker_pts68.csta # 68点关键点模型(可选) ├── face_recognizer.csta # 识别模型,输出特征向量 └── face_aligner.csta # 人脸对齐模型(可选)

如果工程自带模型,先确认是否与 JNI 层的初始化函数参数一致。例如SeetaFace6JNI.init()可能接收一个模型目录路径,而assets下的文件需要先释放到应用私有目录才能被 C++ 层以文件路径方式读取。

以下是一段常见做法的模型释放和初始化代码,注释已标明关键点:

public class SeetaFaceManager { static { System.loadLibrary("seetaface6_jni"); } // 从 assets 拷贝模型到 /data/data/<包名>/models/ public static void copyModels(Context context) { File modelDir = new File(context.getFilesDir(), "models"); if (!modelDir.exists()) { modelDir.mkdirs(); } // 逐个拷贝,这里以 face_detector.csta 为例 copyAsset(context, "face_detector.csta", modelDir); // 其他模型同理,省略 } public static native long init(String modelDir); public static native long detect(long handle, byte[] imageData, int width, int height); }

这里有个关键点:copyModels的调用时机要在init之前,并且init传入的路径必须是绝对路径。如果传入相对路径或assets路径,C++ 层用标准文件读取函数打不开,会返回空句柄或直接崩溃。

3.3 人脸检测最小调用链

Demo 工程中,MainActivity的整体调用逻辑通常如下:

// 1. 初始化 SeetaFaceManager.copyModels(this); long handle = SeetaFaceManager.init(getFilesDir().getAbsolutePath() + "/models"); // 2. 加载图片(来自相册或相机) Bitmap bitmap = loadBitmapFromPath(imagePath); byte[] argb = bitmapToARGB(bitmap); // 转成 byte[],注意通道顺序 // 3. 调用检测 long faceInfo = SeetaFaceManager.detect(handle, argb, bitmap.getWidth(), bitmap.getHeight()); // 4. 判断结果 if (faceInfo != 0) { // 拿到人脸框和关键点,画框或做下一步特征提取 } else { Log.i("Seeta", "未检测到人脸"); }

bitmapToARGB这一步容易翻车。SeetaFace6 的SeetaImageData数据结构要求的是连续内存的像素数据,一般用ARGB或BGR顺序,具体看 so 库的编译选项。如果图片带透明度或色彩空间不是预想的格式,检测结果会异常甚至内存越界。

建议先确认图片输入格式。常见做法是:

Bitmap rgbBitmap = bitmap.copy(Bitmap.Config.ARGB_8888, false); int[] pixels = new int[width * height]; rgbBitmap.getPixels(pixels, 0, width, 0, 0, width, height); byte[] data = new byte[width * height * 4]; // ARGB_8888 的 int 排列是 A,R,G,B,按字节展开 for (int i = 0; i < pixels.length; i++) { data[i * 4] = (byte) ((pixels[i] >> 16) & 0xFF); // R data[i * 4 + 1] = (byte) ((pixels[i] >> 8) & 0xFF); // G data[i * 4 + 2] = (byte) (pixels[i] & 0xFF); // B data[i * 4 + 3] = (byte) ((pixels[i] >> 24) & 0xFF); // A }

按照上述方式转换后,传给 JNI 的byte[]就是连续内存块。C++ 端拿到jbyteArray后通过GetByteArrayElements取出指针,填充到SeetaImageData里即可。整个过程不涉及拷贝到Bitmap再读取的二次转换,性能上更优。

3.4 检测器参数怎么调

face_detector的初始化参数通常通过SeetaFace6JNI的setProperty或init的结构体传入。常见的可调参数有三个:

参数名作用建议值
min_face_size最小人脸像素(宽或高)20 或 40。越小越容易检测到远距离小脸,但误检率和耗时都会上升
threshold检测得分阈值0.9 左右。低于 0.7 误检显著增多
max_image_width/height输入图片的最大尺寸限制2000x2000 以内,超出会被缩放

需要注意,min_face_size的设置影响多尺度检测的层数。设为 20 时,C++ 层会构建更多的图像金字塔,耗时比 40 明显增加,但小脸召回率更高。Demo 工程默认设置往往是为了演示效果,实际项目中建议用真实业务图片做测试后确定。

例如,可以通过 Java 层调用setMinFaceSize传入值:

SeetaFace6JNI.setProperty(handle, "min_face_size", 40); SeetaFace6JNI.setProperty(handle, "threshold", 0.93f);

改完之后跑一遍检测,确认性能符合预期。如果发现检测速度过慢,优先检查是否在每次调用时重新初始化了检测器,而不是调整模型。初始化检测器本身耗时几十毫秒,绝对不能在每帧调用。

4. 关键点定位和特征提取:JNI 层如何把 C++ 对象映射为 Java 对象

4.1 从人脸框到关键点

SeetaFace6 的检测结果是一个SeetaFaceInfo结构体,包含score、x、y、width、height。JNI 层拿到检测框后,下一步是提取关键点。这里有个容易混淆的地方:检测器输出的是人脸框,关键点定位需要使用face_landmarker模块。

以下是一段 JNI 层处理关键点的简化逻辑:

// 假设 detect 返回的人脸框信息已经在 faceInfo 里 float[] landmarks = SeetaFace6JNI.face_landmark( handle, argb, width, height, faceInfo.x, faceInfo.y, faceInfo.width, faceInfo.height ); // landmarks 长度是 5 或 68,每两个一组,分别是 x 和 y 坐标

JNI C++ 端的大致流程:

SeetaFaceInfo faceInfo; // 填充人脸框 SeetaImageData imageData; imageData.data = (uint8_t*)env->GetByteArrayElements(...); imageData.width = width; imageData.height = height; SeetaPointF* points = new SeetaPointF[5]; landmarker->Detect(imageData, faceInfo.pos, &points[0]); jfloatArray result = env->NewFloatArray(5 * 2); env->SetFloatArrayRegion(result, 0, 10, (jfloat*)points);

注意关键点检测的输入是检测框,不是整张图。若直接把整张图传给 landmarker,部分实现会内部再做一次检测,耗时增加且结果可能不一致。

4.2 特征提取和比对:阈值别乱用

识别模块输出的是一个固定维度的特征向量,常见的是 512 维或 1024 维。SeetaFace6 的face_recognizer返回的是 float 数组,比对的相似度通过余弦相似度或欧氏距离计算。 人脸 1:1 比对的相似度阈值需要根据模型和数据分布来确定,不能直接套用某个固定值。

常见的阈值范围如下:

场景余弦相似度阈值建议说明
手机解锁0.62 ~ 0.65要求高安全,误识率低
闸机通行0.50 ~ 0.55兼顾通过率
安防监控0.45 左右低阈值,优先召回

如果 Demo 工程中自带比对函数,确认代码里用的阈值是写死的还是从配置读取的。写死阈值是常见埋雷点。你可以手动调整阈值,但更重要的是先确认特征提取时传入的图像是否经过人脸对齐。SeetaFace6 提供face_aligner模块,将检测到的人脸旋转到正脸后裁剪固定尺寸,再送入识别器,特征质量会明显提升。

4.3 JNI 内存泄漏:谁分配谁释放

JNI 层最容易翻车的不是逻辑,而是内存管理。C++ 端通过new创建的对象,如果 Java 层没有对应的delete方法,每次调用都会泄漏。Demo 工程一般提供了destroy(handle)方法,但很多人调完检测后忘记释放。

常见做法是在MainActivity.onDestroy里调用释放逻辑:

@Override protected void onDestroy() { super.onDestroy(); if (handle != 0) { SeetaFace6JNI.destroy(handle); handle = 0; } }

JNI 端的释放函数:

JNIEXPORT void JNICALL Java_com_example_SeetaFace6JNI_destroy(JNIEnv* env, jobject obj, jlong handle) { SeetaFace6JNI* engine = (SeetaFace6JNI*)handle; if (engine != nullptr) { delete engine->detector; delete engine->landmarker; delete engine->recognizer; delete engine; } }

一句话总结:init返回的句柄是 C++ 对象指针,destroy必须对应释放,否则跑几分钟后内存涨到应用被杀。

5. arm64-v8a 与 armeabi-v7a 的适配避坑

5.1 现象:同一套代码,32 位设备崩,64 位设备正常

某次在模拟项目 X 上集成 seetaface6 时,客户的设备只支持armeabi-v7a,安装 APK 后点按钮直接闪退。排查过程如下:

  • 现象:UnsatisfiedLinkError: dlopen failed: library "libseetaface6_jni.so" not found
  • 原因:APK 里只打了arm64-v8a的 so,32 位设备找不到库
  • 解决:在build.gradle的abiFilters里补上armeabi-v7a,并确认jniLibs目录下有对应的 so 文件

这个问题在下载 Demo 工程后很常见:有些人为了节省包体积,手动删除了armeabi-v7a目录,但没意识到目标设备是 32 位系统。Android 的 so 加载机制是:系统根据设备的主 ABI 选择对应的 so 目录,不存在向下兼容的情况。

5.2 现象:onCreate 里加载 so 正常,但运行时报错

  • 现象:System.loadLibrary("seetaface6_jni")执行成功,但调用detect时崩溃,报错信息指向Undefined reference to ...
  • 原因:so 库本身编译时依赖了其他 so(例如 OpenCV、cblas 等),而打包时只打了核心 so,没有打依赖 so
  • 解决:检查jniLibs/<abi>/下是否有多个 so 文件。SeetaFace6 的 so 有时依赖libc++_shared.so或libopencv_java.so,这些都需要一并放入

另外,如果 so 库是用较新版本的 NDK 编译的,而应用引用了较低版本的android:extractNativeLibs="false"配置,会导致 so 无法从 APK 中解压到应用私有目录。解决方法是删除AndroidManifest.xml中的extractNativeLibs="false"或设置为true,或者改用android:useLegacyPackaging="true"。

5.3 现象:模拟器上检测结果正常,真机检测不到人脸

  • 现象:在 x86 模拟器上运行 Demo,能够正常检测人脸;换成 ARM 真机后,检测结果为 0
  • 原因:模型的输入尺寸与真机相机输出的尺寸不一致。真机相机预览分辨率通常是 1920x1080,而 Demo 里写死的分辨率可能是 640x480,导致图像被错误裁剪或缩放
  • 解决:从CameraX或Camera2取帧时,先通过ImageProxy获取cropRect和rotationDegrees,再做坐标映射。具体实现参考下方代码。
ImageProxy image = imageProxy; Rect cropRect = image.getCropRect(); // 旋转角度需要根据相机朝向计算 Matrix matrix = new Matrix(); matrix.setRotate(degrees, cropRect.width() / 2f, cropRect.height() / 2f); Bitmap rotatedBitmap = Bitmap.createBitmap( srcBitmap, 0, 0, srcBitmap.getWidth(), srcBitmap.getHeight(), matrix, false );

这段逻辑说起来简单,但实际排查时通常会消磨很多时间。建议在 Java 层打印bitmap.getWidth()和bitmap.getHeight(),再和 JNI 层收到的宽高对比,如果不一致,问题就出在相机帧时的旋转和裁剪上。

5.4 现象:模型文件变大,APK 包体积翻倍

当前模型文件不小:face_detector.csta通常在几十 MB 量级。这个体量对 APK 包的影响较大,尤其是要上架应用市场时需要注意。

常见做法有两种:

  • 从 APK 中移除模型文件,首次启动从服务器下载并校验 MD5
  • 保留在 assets 中,配合aria2分片下载或内置压缩包,启动后解压

如果选择第一种,确保下载完成前不初始化引擎,并做好失败重试机制。曾见过有人在Application.onCreate里同步初始化引擎,导致首次启动卡在模型下载上,直接 ANR。

5.5 现象:应用被杀后重启,人脸检测初始化失败

  • 现象:应用在后台被系统回收,再次打开时init返回 0 或直接崩溃
  • 原因:未处理 JNI 层的重复初始化或重入问题。C++ 端init函数内部可能在创建引擎前没有判断句柄是否已存在,导致重复加载模型或重复创建单例
  • 解决:在 Java 层用synchronized保证init只执行一次,并且保存全局句柄。进程被杀重建时,JNI 静态变量已重置,但System.loadLibrary不会重新执行。检查static { System.loadLibrary(...); }块是否在Application或MainActivity中,如果只在 Activity 里加载,进程重建后其他类访问 JNI 方法时可能还没加载 so 库。

另一种做法是使用ContentProvider在进程启动时提前加载 so:

public class SeetaInitProvider extends ContentProvider { @Override public boolean onCreate() { System.loadLibrary("seetaface6_jni"); return true; } }

然后在 Manifest 中注册该ContentProvider,确保 so 库最先被加载。

6. 进阶:自己编译 so 替换 Demo 的 so,需要注意什么

如果 Demo 工程自带的 so 不满足需求,比如需要支持x86_64模拟器或开启 GPU 加速,可以自行从源码编译。SeetaFace6 开源仓库是完整可编译的,但第三方开发者需要注意以下几点:

先确认编译环境:

  • Android NDK r21 及以上(推荐 r23)
  • CMake 3.10 以上
  • OpenCV 4.x(用于图像处理,但不是必选)

编译时建议只开启需要的模块:

cmake -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \ -DANDROID_ABI=arm64-v8a \ -DANDROID_PLATFORM=android-21 \ -DBUILD_EXAMPLES=OFF \ -DBUILD_TEST=OFF \ ..

编译完成后生成的libseetaface6_jni.so替换到jniLibs/arm64-v8a/下,但有一个坑很容易踩:OpenCV 可能被静态编译进 so,也可能是动态引用。如果是动态的,libopencv_java.so也要一起拷贝进jniLibs。

替换 so 后,用以下命令检查 so 文件的依赖是否完整:

llvm-readelf -d libseetaface6_jni.so | grep NEEDED

输出的依赖列表里如果出现libopencv_java.so或libc++_shared.so,确认它们在jniLibs/<abi>/目录下存在。

编译完成后用自己的 so 替换 Demo 的 so,最大的风险是模型文件版本不匹配。SeetaFace6 的模型文件有固定的二进制格式,不同版本的face_recognizer.csta与 so 库存在兼容性约束。如果替换 so 后初始化正常但识别结果异常,优先检查模型和 so 版本是否一致。版本号信息在 C++ 头文件或SeetaFace6JNI.java注释中通常能找到,确保两边对齐。

另外需要注意测试覆盖范围。不要只在 64 位设备上验证,至少准备一台armeabi-v7a的旧设备,用真实光线下的人脸图做回归测试,防止 ABI 差异导致的精度变化。

最后说一个实际经验:虽然这个方向能快速跑通 Demo,但真要在生产环境用,务必先做三项验证:一是确认模型文件的分发方式是否符合你的业务更新节奏;二是以真实业务的图片集做误检率和召回率评估,不能只看 Demo 自带图片的效果;三是压测内存占用和连续运行的稳定性。人脸识别这种离线方案,坑大多不在 AI 效果,而在工程集成,希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 19:10:33

CSS绝对定位下margin:0 auto失效原因与水平居中方案

1. 从一次布局翻车说起&#xff1a;绝对定位下 auto 为什么失灵刚入行那会儿&#xff0c;我遇到过一个特别典型的场景&#xff1a;一个弹窗组件&#xff0c;外层用position: absolute定位到页面中央&#xff0c;内层内容想用margin: 0 auto做水平居中。结果不管我怎么调&#x…

作者头像 李华
网站建设 2026/10/15 19:10:29

空压机物联网远程监控方案,实现预测性维保,减少非计划停机损失

空压机物联网远程监控方案&#xff0c;通过工业网关采集空压机PLC/控制器的运行数据&#xff0c;经4G/5G/以太网上传至物联网云平台&#xff0c;实现空压站集中远程监控、故障预警、能耗统计、远程运维&#xff0c;把传统人工巡检的空压机变成无人值守智能空压站。一、系统架构…

作者头像 李华
网站建设 2026/10/15 19:08:57

QX1504原边反馈恒压控制器

概述QX1504 是一款应用于单级隔离型反激式AC/DC 的控制器&#xff1b;系统工作于原边反馈控制模式&#xff0c;省去了光耦和TL431。QX1504 内置可靠的三极管驱动电路&#xff0c;可驱动三极管&#xff0c;降低系统成本。QX1504 通过变压器辅助绕组获得输出电压的大小。芯片内置…

作者头像 李华
网站建设 2026/10/15 19:08:52

UDS学习二:安全访问0x27(Security Access)

安全访问0x27&#xff08;Security Access&#xff09;1. 为什么需要安全访问2. 子功能成对设计3. 报文格式4. 常见NRC5. 解锁后的状态变化1. 为什么需要安全访问 会话控制解决了"能不能进这个模式"的问题&#xff0c;但还不够。进了 Extended 会话后&#xff0c;写…

作者头像 李华
网站建设 2026/10/15 19:08:40

2026上海网易企业邮箱有哪些,功能差异与适用场景介绍

导读:在数字化办公持续深化的背景下,企业邮箱早已超越单纯的收发工具,成为商务沟通、资料归档与内部管控的关键基础设施。网易企业邮箱自2009年上线至今,历经十余年运维迭代,针对不同规模与行业的企业推出了多套版本方案。本文将从产品背景、版本功能、核心差异、行业适配四个维…

作者头像 李华
网站建设 2026/10/15 19:07:59

X-Reset:跨具身重置与物体中心化智能体恢复机制

1. 项目概述&#xff1a;这不是“重置按钮”&#xff0c;而是一套让AI智能体真正学会“从头再来”的底层机制你有没有试过教一个机器人捡起杯子——它第一次成功了&#xff0c;第二次却把杯子推下桌子&#xff0c;第三次干脆绕着桌子转圈&#xff1f;传统强化学习里&#xff0c…

作者头像 李华