MNN Android Demo readme 和 代码解析
project/android/demo/READNE.md
未实现 环境配置
MNN Android Demo
1. 环境准备
开发工具
Android StudioNDK
模型下载与转换:
首先编译(如果已编译可以跳过)MNNConvert,操作如下:
cd MNN
mkdir build && cd build
cmake -DMNN_BUILD_CONVERTER=ON ..
make -j8
然后下载模型,可以直接执行 sh ../tools/script/get_model.sh ,
get_model.sh
#!/bin/bash
# 进入脚本所在目录的上级目录中的 resource 子目录
# pushd 命令会切换到指定目录并记录当前目录,之后可以用 popd 返回
pushd "$(dirname $0)"/../.. > /dev/null
pushd resource > /dev/null
# 定义 MNN 转换器可执行文件的路径
CONVERTER=../build/MNNConvert
# 检查转换器是否存在,如果不存在则给出提示并退出脚本
if [ ! -e ${CONVERTER} ]; then
echo "can't find ${CONVERTER}, building converter firstly "
exit
fi
# 函数定义部分开始
# download 函数:用于下载文件
# 参数 $1: 文件的 URL
# 参数 $2: 本地保存路径(含文件名)
# 如果本地文件已存在,则直接返回成功;否则使用 curl 下载,并检查 HTTP 状态码
download() {
if [ -e $2 ]; then return 0; fi
name=`basename $2` # 提取文件名用于显示
echo "downloading $name ..."
# 使用 curl 下载,-s 静默模式,-w 输出 HTTP 状态码,-o 保存到指定文件
status=`curl $1 -s -w %{http_code} -o $2`
if (( status == 200 )); then
return 0 # 下载成功返回 0
else
echo "download $name failed" 1>&2
return -1 # 下载失败返回 -1
fi
}
# get_caffe1 函数:下载 Caffe 模型(.caffemodel 和 .prototxt)并转换为 MNN 格式
# 参数 $1: .caffemodel 文件的 URL
# 参数 $2: .caffemodel 本地保存路径
# 参数 $3: .prototxt 文件的 URL
# 参数 $4: .prototxt 本地保存路径
# 参数 $5: 模型描述信息(仅用于日志)
# 参数 $6: 输出的 MNN 模型文件路径
get_caffe1() { # model_URL, model_path, prototxt_URL, prototxt_path, model, MNN_path
if [ ! -e $6 ]; then
echo "download and convert $2 $4"
# 先下载 .caffemodel 和 .prototxt,然后调用 MNNConvert 进行转换
download $1 $2 && download $3 $4 && ./$CONVERTER -f CAFFE --modelFile $2 --prototxt $4 --MNNModel $6 --bizCode 0000 --keepInputFormat=0
fi
}
# get_tensorflow_lite 函数:下载 TensorFlow Lite 模型的压缩包(.tgz)并解压,然后转换为 MNN 格式
# 参数 $1: .tgz 文件的 URL
# 参数 $2: 压缩包内的 .tflite 文件名(也是本地解压后的文件名)
# 参数 $3: 模型描述信息(仅用于日志)
# 参数 $4: 输出的 MNN 模型文件路径
get_tensorflow_lite() {
if [ ! -e $4 ]; then
mkdir -p build # 创建 build 目录用于临时存放
pushd build > /dev/null
download $1 $2.tgz && tar -xzf $2.tgz $2 # 下载并解压,只提取指定的 .tflite 文件
succ=$? # 记录上一条命令的退出状态
popd > /dev/null
[ $succ -eq 0 ] && ./$CONVERTER -f TFLITE --modelFile build/$2 --MNNModel $4 --bizCode 0000 --keepInputFormat=0
fi
}
# get_portrait_lite 函数:直接下载 TensorFlow Lite 模型文件(非压缩包)并转换为 MNN 格式
# 参数 $1: .tflite 文件的 URL
# 参数 $2: 本地保存的 .tflite 文件名
# 参数 $3: 模型描述信息(仅用于日志)
# 参数 $4: 输出的 MNN 模型文件路径
get_portrait_lite() {
if [ ! -e $4 ]; then
mkdir -p build
pushd build > /dev/null
download $1 $2 # 直接下载 .tflite 文件
succ=$?
popd > /dev/null
[ $succ -eq 0 ] && ./$CONVERTER -f TFLITE --modelFile build/$2 --MNNModel $4 --bizCode 0000 --keepInputFormat=0
fi
}
# 模型下载与转换开始
## 使用 MobileNet V1 (Caffe 版本),模型来源:https://github.com/shicai/MobileNet-Caffe/
get_caffe1 \
"https://raw.githubusercontent.com/shicai/MobileNet-Caffe/master/mobilenet.caffemodel" \
"build/mobilenet_v1.caffe.caffemodel" \
"https://raw.githubusercontent.com/shicai/MobileNet-Caffe/master/mobilenet_deploy.prototxt" \
"build/mobilenet_v1.caffe.prototxt" \
"MobileNet V1" \
"model/MobileNet/v1/mobilenet_v1.caffe.mnn"
## 使用 MobileNet V2 (Caffe 版本),模型来源:https://github.com/shicai/MobileNet-Caffe/
get_caffe1 \
"https://raw.githubusercontent.com/shicai/MobileNet-Caffe/master/mobilenet_v2.caffemodel" \
"build/mobilenet_v2.caffe.caffemodel" \
"https://raw.githubusercontent.com/shicai/MobileNet-Caffe/master/mobilenet_v2_deploy.prototxt" \
"build/mobilenet_v2.caffe.prototxt" \
"MobileNet V2" \
"model/MobileNet/v2/mobilenet_v2.caffe.mnn"
## 使用 SqueezeNet V1.0 (Caffe 版本),模型来源:https://github.com/DeepScale/SqueezeNet/
get_caffe1 \
"https://raw.githubusercontent.com/DeepScale/SqueezeNet/master/SqueezeNet_v1.0/squeezenet_v1.0.caffemodel" \
"build/squeezenet_v1.0.caffe.caffemodel" \
"https://raw.githubusercontent.com/DeepScale/SqueezeNet/master/SqueezeNet_v1.0/deploy.prototxt" \
"build/squeezenet_v1.0.caffe.prototxt" \
"SqueezeNet V1.0" \
"model/SqueezeNet/v1.0/squeezenet_v1.0.caffe.mnn"
## 使用 SqueezeNet V1.1 (Caffe 版本),模型来源:https://github.com/DeepScale/SqueezeNet/
get_caffe1 \
"https://raw.githubusercontent.com/DeepScale/SqueezeNet/master/SqueezeNet_v1.1/squeezenet_v1.1.caffemodel" \
"build/squeezenet_v1.1.caffe.caffemodel" \
"https://raw.githubusercontent.com/DeepScale/SqueezeNet/b6b5ae2ce884a3866c21efd31e103defde8631ae/SqueezeNet_v1.1/deploy.prototxt" \
"build/squeezenet_v1.1.caffe.prototxt" \
"SqueezeNet V1.1" \
"model/SqueezeNet/v1.1/squeezenet_v1.1.caffe.mnn"
## 使用 MobileNet V2 (TensorFlow Lite 版本),模型来源:http://download.tensorflow.org/models/tflite_11_05_08/mobilenet_v2_1.0_224.tgz
get_tensorflow_lite \
"http://download.tensorflow.org/models/tflite_11_05_08/mobilenet_v2_1.0_224.tgz" \
"mobilenet_v2_1.0_224.tflite" \
"MobileNet V2 TFLite" \
"model/MobileNet/v2/mobilenet_v2_1.0_224.tflite.mnn"
## 使用量化版的 MobileNet V2 (TensorFlow Lite 版本),模型来源:http://download.tensorflow.org/models/tflite_11_05_08/mobilenet_v2_1.0_224_quant.tgz
get_tensorflow_lite \
"http://download.tensorflow.org/models/tflite_11_05_08/mobilenet_v2_1.0_224_quant.tgz" \
"mobilenet_v2_1.0_224_quant.tflite" \
"MobileNet V2 TFLite Quantized" \
"model/MobileNet/v2/mobilenet_v2_1.0_224_quant.tflite.mnn"
## 使用 DeepLab v3 肖像分割模型 (TensorFlow Lite 版本),模型来源:https://storage.googleapis.com/download.tensorflow.org/models/tflite/gpu/deeplabv3_257_mv_gpu.tflite
get_portrait_lite \
"https://storage.googleapis.com/download.tensorflow.org/models/tflite/gpu/deeplabv3_257_mv_gpu.tflite" \
"deeplabv3_257_mv_gpu.tflite" \
"deeplabv3" \
"model/Portrait/Portrait.tflite.mnn"
# 返回原始目录
popd > /dev/null
popd > /dev/null
它会自动下载多个深度学习模型(Caffe 和 TensorFlow Lite 格式),并使用 MNN 提供的转换工具(MNNConvert)将它们转换为 MNN 框架支持的模型格式(.mnn 文件),最终将这些 .mnn 文件保存到指定的目录中。
具体生成的文件列表
以下模型会被下载并转换,生成对应的 MNN 模型文件(路径相对于脚本所在的 resource 目录):
|
原始模型 |
来源 |
转换后的 MNN 文件路径 |
|
MobileNet V1 (Caffe) |
|
|
|
MobileNet V2 (Caffe) |
|
|
|
SqueezeNet V1.0 (Caffe) |
|
|
|
SqueezeNet V1.1 (Caffe) |
|
|
|
MobileNet V2 (TFLite) |
TensorFlow 官方模型库 |
|
|
MobileNet V2 Quantized (TFLite) |
TensorFlow 官方模型库 |
|
|
DeepLab v3 (Portrait, TFLite) |
TensorFlow 官方模型库 |
|
中间文件
在转换过程中,脚本会在 resource/build 目录下临时存放下载的原始模型文件(如 .caffemodel、.prototxt、.tflite 等)。如果转换成功,这些文件不会被自动删除(但可以手动清理)。
注意事项
- 脚本执行前需要确保 MNN 转换工具已经编译好,并位于
../build/MNNConvert路径(相对于脚本位置)。如果找不到该工具,脚本会退出并提示先编译转换器。 - 如果某个 MNN 文件已经存在,脚本会跳过对应的下载和转换步骤(避免重复操作)。
- 网络连接必须正常,因为需要从 GitHub 和 TensorFlow 官网下载模型文件。
执行环境要求
- 操作系统:Linux / macOS(或 Windows 的 Bash 环境)
- 工具:
curl、tar、bash - MNN 转换器:已编译好的
MNNConvert可执行文件
运行脚本后,你将在 resource/model/ 下获得一系列可以直接在 MNN 框架中使用的模型文件,便于后续的推理部署或测试。
自行下载
也可以按如下步骤自行下载与转换:
MobileNet_v2
wget https://github.com/shicai/MobileNet-Caffe/blob/master/mobilenet_v2.caffemodel
wget https://github.com/shicai/MobileNet-Caffe/blob/master/mobilenet_v2_deploy.prototxt
./MNNConvert -f CAFFE --modelFile mobilenet_v2.caffemodel --prototxt mobilenet_v2_deploy.prototxt --MNNModel mobilenet_v2.caffe.mnn
mv mobilenet_v2.caffe.mnn ../resource/model/MobileNet/v2/
手动下载单个文件
如果你只想获取 mobilenet_v2.caffemodel 这个文件:
直接下载地址(来自 GitHub):
- https://github.com/shicai/MobileNet-Caffe/blob/master/mobilenet_v2.caffemodel?raw=true
将链接中的 /blob/ 改为 /raw/ 可以得到直接下载链接:
- https://github.com/shicai/MobileNet-Caffe/raw/master/mobilenet_v2.caffemodel
- 在浏览器中打开上述链接即可下载。下载后放到脚本期望的目录:
D:\xm_xiangmu\minimind\MNN\resource\build\mobilenet_v2.caffe.caffemodel
注意脚本里保存的文件名是mobilenet_v2.caffe.caffemodel,不是原文件名,可能需要重命名。
同样地,如果需要对应的 prototxt 文件,可以从这里下载:https://github.com/shicai/MobileNet-Caffe/blob/master/mobilenet_v2_deploy.prototxt
MNNConvert -f CAFFE --modelFile mobilenet_v2.caffemodel --prototxt mobilenet_v2_deploy.prototxt --MNNModel mobilenet_v2.caffe.mnn
D:\xm_xiangmu\minimind\MNN\resource\model\MobileNet\v2>conda activate pytorch21
(pytorch21) D:\xm_xiangmu\minimind\MNN\resource\model\MobileNet\v2>MNNConvert -f CAFFE --modelFile mobilenet_v2.caffemodel --prototxt mobilenet_v2_deploy.prototxt --MNNModel mobilenet_v2.caffe.mnn
The device supports: i8sdot:0, fp16:0, i8mm: 0, sve2: 0, sme2: 0
The device supports: i8sdot:0, fp16:0, i8mm: 0, sve2: 0, sme2: 0
Don't has bizCode, use MNNTest for default
Start to Convert Other Model Format To MNN Model..., target version: 3.4
Start to Optimize the MNN Net...
inputTensors : [ data, ]
outputTensors: [ prob, ]
Converted Success!
(pytorch21) D:\xm_xiangmu\minimind\MNN\resource\model\MobileNet\v2>

SqueezeNet_v1.1
wget https://github.com/forresti/SqueezeNet/blob/master/SqueezeNet_v1.1/squeezenet_v1.1.caffemodel
wget https://github.com/forresti/SqueezeNet/blob/master/SqueezeNet_v1.1/deploy.prototxt
./MNNConvert -f CAFFE --modelFile squeezenet_v1.1.caffemodel --prototxt deploy.prototxt --MNNModel squeezenet_v1.1.caffe.mnn
mv squeezenet_v1.1.caffe.mnn ../resource/model/SqueezeNet/v1.1/
DeepLab_v3
wget https://storage.googleapis.com/download.tensorflow.org/models/tflite/gpu/deeplabv3_257_mv_gpu.tflite
./MNNConvert -f TFLITE --modelFile deeplabv3_257_mv_gpu.tflite --MNNModel Portrait.tflite.mnn
mv Portrait.tflite.mnn ../resource/model/Portrait/
2. 编译运行
使用Android Studio打开demo目录,在local.properties中指定sdk.dir与ndk.dir,即可编译执行。
这个版本太老了 有点难运行
D:\xm_xiangmu\minimind\MNN\project\android\demo
D:\xm_xiangmu\minimind\MNN\resource\model\MobileNet\v2

得使用和 ndk 配套的 cmake
sdk.dir=D\:\\Android\\sdk
ndk.dir=D\:\\Android\\sdk\\ndk\\21.4.7075529
# 将此路径替换为你实际的 CMake 版本路径
cmake.dir=D\:\\Android\\sdk\\cmake\\3.10.2.4988404

distributionUrl=https://mirrors.aliyun.com/macports/distfiles/gradle/gradle-9.3.1-bin.zip
升级 Gradle 可能引入其他配置调整(如仓库替换、API 变更)
问题出在 build.gradle 文件中使用了 jcenter() 仓库,而当前 Gradle 版本已不再支持该方法(jcenter 已逐渐被废弃)。


google() // 替代 jcenter
mavenCentral() // 替代 jcenter
distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-5.1.1-all.zip
配置兼容的 JDK 版本
在Android Studio 中修改 Gradle JDK
- 打开 File > Settings(或 File > Project Structure,取决于版本)。
- 导航到 Build, Execution, Deployment > Build Tools > Gradle。
- 在 Gradle JDK 下拉菜单中,选择一个 JDK 版本 11 或 8(如果已有)。如果列表中没有,可以点击下拉菜单旁边的 Download JDK 或手动添加已安装的 JDK 路径。
- 点击 Apply 和 OK。
- 点击 Sync Now 重新同步项目。
如果使用命令行构建
- 设置环境变量
JAVA_HOME指向 JDK 11 的安装路径(例如C:\Program Files\Java\jdk-11.0.XX)。 - 确保
PATH中包含%JAVA_HOME%\bin。
jdk11 下载地址
C:\Users\XMICUser\.jdks

降级使用 NDK 21
对于这类较老的 C++ Android 工程,最稳妥、最一劳永逸的解决办法就是下载并使用 NDK 21(这是 C++ 库编译最经典、兼容性最好的“万金油”版本)。
请按照以下步骤操作:
第一步:下载 NDK 21
- 在 Android Studio 的顶部菜单栏,点击 Tools -> SDK Manager(或者点击右上角那个长得像个向下箭头的方块图标)。
- 在弹出的窗口中,选择中间的 SDK Tools 选项卡。
- 勾选右下角的 Show Package Details(显示详细包信息)。
- 在列表中找到 NDK (Side by side) 并展开它。
- 找到并勾选 21.4.7075529 (或者其他 21.x 开头的版本)。
- 点击 Apply 或 OK,等待它下载并安装完成。
第二步:修改配置
下载完成后,回到你的 local.properties 文件,把刚才写的 28 版本的路径改成 21 版本的路径:
Properties
ndk.dir=D\:\\Android\\sdk\\ndk\\21.4.7075529
(注意:请确保末尾的版本号和你刚才在 SDK Manager 里下载的完全一致)
第三步:重新同步
点击 Android Studio 右上角的 "Sync Now"(或者那头小象图标)。
需要将 AGP 版本从 3.2.1 升级到 7.0.0 或更高版本
还是有问题 烦死了 算了
代码解析
tree app/src
├─androidTest
│ └─java
│ └─com
│ └─taobao
│ └─android
│ └─mnndemo
├─main
│ ├─java
│ │ └─com
│ │ └─taobao
│ │ └─android
│ │ ├─mnn
│ │ ├─mnndemo
│ │ ├─opengl
│ │ └─utils
│ └─res
│ ├─drawable
│ ├─drawable-v24
│ ├─layout
│ ├─mipmap-anydpi-v26
│ ├─mipmap-hdpi
│ ├─mipmap-mdpi
│ ├─mipmap-xhdpi
│ ├─mipmap-xxhdpi
│ ├─mipmap-xxxhdpi
│ ├─raw
│ └─values
└─test
└─java
└─com
└─taobao
└─android
└─mnndemo

mnn
这些 Java 文件位于 com.taobao.android.mnn 包下,是 MNN(Mobile Neural Network)推理引擎在 Android 平台的 JNI 封装接口。它们提供了从模型加载、会话管理、张量操作到图像预处理的一整套 API,方便开发者在 Java 层调用 MNN 的 C++ 核心功能。下面逐一解析每个文件的作用和关键点。
1. MNNPortraitNative.java
package com.taobao.android.mnn;
public class MNNPortraitNative {
public static native int[] nativeConvertMaskToPixelsMultiChannels(float[] mask, int length);
}
- 作用:声明了一个本地方法,可能用于特定的人像分割任务。它将模型输出的浮点型掩码(mask)转换为像素级别的整型数组(例如分割类别索引)。
length参数可能表示掩码的元素个数。 - 说明:该类只包含一个 native 声明,其具体实现在 C++ 层(对应
libmnncore.so或其他库)。推测是 MNN 提供的一个工具函数,用于后处理分割结果。
2. MNNForwardType.java
package com.taobao.android.mnn;
public enum MNNForwardType {
FORWARD_CPU(0),
FORWARD_OPENCL(3),
FORWARD_AUTO(4),
FORWARD_OPENGL(6),
FORWARD_VULKAN(7);
public int type;
MNNForwardType(int t) { type = t; }
}
- 作用:枚举了 MNN 支持的计算后端类型:
-
FORWARD_CPU:CPU 推理FORWARD_OPENCL:OpenCL(GPU)FORWARD_AUTO:自动选择FORWARD_OPENGL:OpenGL(GPU)FORWARD_VULKAN:Vulkan(GPU)
- 说明:每个枚举值关联一个整数常量,用于 JNI 层识别后端类型。该枚举通常在创建会话时作为配置传入。
3. MNNImageProcess.java
package com.taobao.android.mnn;
import android.graphics.Bitmap;
import android.graphics.Matrix;
public class MNNImageProcess {
// 内部枚举:Format, Filter, Wrap
public static class Config { ... }
public static boolean convertBuffer(byte[] buffer, int width, int height,
MNNNetInstance.Session.Tensor tensor,
Config config, Matrix matrix) { ... }
public static boolean convertBitmap(Bitmap sourceBitmap,
MNNNetInstance.Session.Tensor tensor,
Config config, Matrix matrix) { ... }
}
- 作用:提供图像预处理功能,将原始图像数据转换为模型输入张量所需的格式。
- 关键点:
-
Format:定义源图像格式和目标图像格式,如 RGBA、RGB、BGR、GRAY 等。Filter:插值方式(最近邻、双线性、双三次)。Wrap:边界填充方式(边缘 clamp、填零、重复)。Config:配置均值(mean)、归一化系数(normal)、源/目标格式、滤波和填充方式。convertBuffer:将字节数组(如相机预览数据)转换为张量,需指定宽高和变换矩阵(Matrix)。convertBitmap:将 Bitmap 转换为张量,自动处理 Bitmap 的内部像素格式。
- 底层实现:调用
MNNNetNative.nativeConvertBitmapToTensor和nativeConvertBufferToTensor,这些 native 方法会利用 MNN 的ImageProcess模块完成实际转换。
4. MNNNetInstance.java
核心类,代表一个已加载的 MNN 网络模型实例。
静态工厂方法
createFromFile(String fileName):从本地文件加载模型,返回MNNNetInstance对象。createFromBuffer(byte[] buffer):从内存字节数组加载模型。
内部类 Config
用于配置会话参数:
forwardType:后端类型(使用MNNForwardType中的值)。numThread:线程数。saveTensors:需要保存中间结果的张量名称数组(可选)。outputTensors:指定输出张量名称数组(可选,若不指定则使用模型默认输出)。
内部类 Session
代表一个推理会话,通过 createSession(Config) 创建。一个网络实例可创建多个会话(不同配置)。
Session 的方法:
reshape():在所有输入张量 reshape 后调用,重新计算内部维度。run():执行推理。runWithCallback(String[] names):推理后返回指定名称的张量数组。getInput(String name)/getOutput(String name):获取指定名称的输入/输出张量。release():释放会话资源(通常不需手动调用,释放网络实例时会自动释放会话)。
内部类 Tensor(张量):
- 通过
getInput/getOutput获得。 - 方法:
-
reshape(int[] dims):改变张量形状(仅对输入张量有效)。setInputIntData(int[] data)/setInputFloatData(float[] data):设置输入数据。getDimensions():获取张量维度。getFloatData()/getIntData()/getUINT8Data():获取输出数据,按需缓存。
- 注意:张量数据在底层是共享的,多次获取不会重复拷贝(除非尺寸变化)。
网络实例生命周期
- 通过
createFromFile/Buffer创建,内部持有 native 指针mNetInstance。 - 使用完毕后必须调用
release()释放 native 资源,否则会造成内存泄漏。
5. MNNNetNative.java
JNI 方法声明和库加载类。
- 静态代码块中加载 MNN 核心库和可选 GPU 后端库(
MNN_Vulkan、MNN_CL、MNN_GL),最后加载mnncore(可能包含 Java 层对应的 native 实现)。 - 声明了所有与 C++ 交互的 native 方法,按功能分组:
-
- Net:
nativeCreateNetFromFile、nativeCreateNetFromBuffer、nativeReleaseNet - Session:
nativeCreateSession、nativeReleaseSession、nativeRunSession、nativeRunSessionWithCallback、nativeReshapeSession、nativeGetSessionInput、nativeGetSessionOutput - Tensor:
nativeReshapeTensor、nativeTensorGetDimensions、nativeSetInputIntData、nativeSetInputFloatData、nativeTensorGetData(float)、nativeTensorGetIntData、nativeTensorGetUINT8Data - ImageProcess:
nativeConvertBitmapToTensor、nativeConvertBufferToTensor
- Net:
- 注意:所有 native 方法均为
protected,仅供同包下的 Java 类调用,对外不可见。
整体工作流程(典型用法)
- 加载模型:
MNNNetInstance instance = MNNNetInstance.createFromFile("model.mnn"); - 配置会话:创建
MNNNetInstance.Config对象,设置后端、线程数等。 - 创建会话:
MNNNetInstance.Session session = instance.createSession(config); - 获取输入张量:
Tensor inputTensor = session.getInput("input_name"); - 准备输入数据:
-
- 若输入为图像,可使用
MNNImageProcess.convertBitmap()或convertBuffer()将 Bitmap 或字节数组直接填入张量。 - 若输入为普通数据,调用
inputTensor.setInputFloatData(data)。
- 若输入为图像,可使用
- 执行推理:
session.run(); - 获取输出:
Tensor outputTensor = session.getOutput("output_name"); float[] result = outputTensor.getFloatData(); - 释放资源:
instance.release();
注意事项
- 线程安全:
MNNNetInstance和Session不是线程安全的,建议每个线程使用独立的实例或加锁保护。 - 内存管理:必须显式调用
release()释放 native 对象,避免内存泄漏。 - 数据格式:张量数据在内存中按
NHWC或NCHW排列?需根据模型实际要求设置,通常图像输入为NHWC(高度、宽度、通道)。 - GPU 支持:使用 GPU 后端需确保设备支持且相应库已加载。
这些文件构成了 MNN Android Java API 的核心,开发者可基于它们快速集成 MNN 推理能力到 Android 应用中。
mnndemo
这些 Java 文件位于 com.taobao.android.mnndemo 包下,是 MNN Android Demo 的示例代码,展示了如何在实际应用中集成 MNN 进行推理。它们涵盖了静态图像分类、实时视频流分类、人像分割以及OpenGL 渲染测试等场景。下面逐一解析每个文件的作用和关键实现。
1. OpenGLTestActivity.java
作用
一个简单的 Activity,用于测试 OpenGL 渲染。它加载一个自定义的 CameraRenderer(位于 com.taobao.android.opengl 包)来显示相机预览并进行可能的 OpenGL 图像处理。
关键点
- 全屏隐藏导航栏:通过设置
SYSTEM_UI_FLAG_HIDE_NAVIGATION | SYSTEM_UI_FLAG_FULLSCREEN实现沉浸式。 - 布局:
R.layout.opengl_test中应包含一个自定义的CameraRenderer视图(id为renderer_view)。 - 生命周期管理:将
onResume、onPause、onDestroy事件传递给mRenderer,以便正确管理 OpenGL 上下文和相机资源。 - 作用:这个 Activity 通常用于验证 OpenGL 后端(
MNNForwardType.FORWARD_OPENGL)是否能正常工作,或者作为集成 MNN 与 OpenGL 处理的测试入口。
2. CameraView.java
作用
一个自定义的 SurfaceView,封装了 Android Camera API 的预览功能,并提供预览帧回调接口。它处理相机的打开、配置、旋转角度以及帧数据的回调。
关键点
- 相机配置:
-
- 打开后置摄像头(
Camera.open())。 - 根据屏幕方向调整相机预览角度(
setCameraDisplayOrientation)。 - 选择合适的预览尺寸(
getPropPreviewSize),确保最小尺寸不低于MINIMUM_PREVIEW_SIZE(320)。 - 设置预览格式为
NV21(ImageFormat.NV21),这是 MNN 常用的输入格式。
- 打开后置摄像头(
- 预览帧回调:
-
- 实现了
Camera.PreviewCallback接口,在onPreviewFrame中每隔PREVIEW_CALLBACK_FREQUENCE(5 帧)回调一次外部注册的PreviewCallback,以减少处理频率。 - 回调中会传递 NV21 数据、图像宽高以及相机的原始角度(
mOrientationAngle)。
- 实现了
- 生命周期:提供
onResume和onPause方法供外部 Activity 调用,以打开/释放相机资源。 - 接口:定义了
PreviewCallback接口,包含onGetPreviewOptimalSize(通知外部最优预览尺寸)和onPreviewFrame(传递帧数据)两个方法。
3. ImageActivity.java
作用
静态图像分类示例。从 assets 中读取一张测试图片(cat.jpg),使用 MobileNet 模型进行推理,并显示前几个分类结果及置信度。
关键点
- 模型准备:
-
- 模型文件:
MobileNet/v2/mobilenet_v2.caffe.mnn(从 assets 复制到缓存目录)。 - 类别标签:
MobileNet/synset_words.txt(每行一个类别名称)。
- 模型文件:
- 异步任务:
-
NetPrepareTask:在后台加载模型和标签,完成后 UI 上的“Start MobileNet Inference”按钮可点击。ImageProcessTask:在后台执行图像预处理、推理和后处理,返回结果更新 UI。
- 图像预处理:
-
- 使用
MNNImageProcess.convertBitmap将Bitmap转换为模型输入张量。 - 配置
Config:mean = [103.94, 116.78, 123.68](ImageNet 均值),normal = [0.017, 0.017, 0.017](即 1/255 * 2? 实际为 1/128? 这里稍奇怪,但来自示例),目标格式为 BGR。 - 应用矩阵变换:将图片缩放到模型输入尺寸(224x224),并取逆矩阵(因为 MNN 的
convertBitmap内部需要从目标图到源图的映射)。
- 使用
- 推理:
-
mSession.run()执行推理。- 获取输出张量:
mSession.getOutput(null)(null 表示获取模型默认输出)。 - 输出为
float[],长度为类别数(MobileNet 为 1000)。
- 后处理:
-
- 筛选置信度 >0.01 的结果,按置信度降序排序。
- 显示前几个结果及置信度。
- 资源释放:在
onDestroy中释放MNNNetInstance。
4. VideoActivity.java
作用
实时视频流分类示例。从相机获取预览帧,对每一帧进行推理(分类),并在 UI 上实时显示结果。支持切换模型(MobileNet / SqueezeNet)、切换后端(CPU/OpenCL/OpenGL/Vulkan)和调整线程数。
关键点
- 模型准备:
-
- MobileNet(224x224)和 SqueezeNet(227x227)两个模型及其对应的类别标签文件。
- 通过
mSelectedModelIndex选择当前使用的模型。
- 相机处理:
-
- 使用自定义的
CameraView获取 NV21 预览帧,并设置预览回调。 - 在回调中根据屏幕旋转角度(
mRotateDegree)和相机原始角度计算实际需要旋转的角度。 - 通过
MNNImageProcess.convertBuffer将 NV21 数据转换为模型输入张量,应用相应的 mean/normal 和旋转缩放矩阵。
- 使用自定义的
- 推理调度:
-
- 使用
HandlerThread和Handler在后台线程执行推理,避免阻塞 UI 线程。 - 使用
AtomicBoolean(mDrop)控制帧丢弃:如果上一帧推理未完成,则丢弃当前帧,防止任务堆积。
- 使用
- UI 更新:
-
- 推理完成后,通过
runOnUiThread更新三个TextView显示置信度最高的三个类别及其置信度。 - 显示推理耗时。
- 推理完成后,通过
- 动态配置:
-
- 通过 Spinner 选择后端类型、线程数、模型等。
- 当选择变化时,重新创建会话(
prepareNet),并锁定 UI 渲染(mLockUIRender)直到新会话准备就绪。
- 屏幕旋转监听:
-
- 使用
OrientationEventListener监听设备旋转角度,更新mRotateDegree,用于正确旋转输入图像。
- 使用
- 资源释放:在
onDestroy中释放网络实例。
5. PortraitActivity.java
作用
人像分割示例。从相机获取预览帧,使用人像分割模型(Portrait 模型)进行推理,将分割结果(掩码)转换为像素级掩码,并叠加显示在原图上。
关键点
- 模型准备:
-
- 模型文件:
Portrait/Portrait.tflite.mnn,输入输出尺寸均为 257x257。 - 模型从 assets 复制到缓存目录。
- 模型文件:
- 相机处理:
-
- 使用自定义的
PortraitCameraView(可能继承自CameraView或单独实现)获取预览帧。 - 同样处理旋转角度,通过
MNNImageProcess.convertBuffer将 NV21 转换为输入张量。 - 预处理配置:mean = [127.5, 127.5, 127.5],normal = [2.0/255, ...](即归一化到 [-1,1]),源格式 YUV_NV21,目标格式 RGB。
- 使用自定义的
- 推理:
-
mSession.run()执行分割。- 输出张量为
float[],每个元素对应一个像素的类别置信度(多通道)。
- 后处理:
-
- 调用本地方法
nativeConvertMaskToPixelsMultiChannels(来自MNNPortraitNative)将浮点掩码转换为像素级整型数组(每个像素一个颜色值?)。 - 生成
Bitmap(ARGB_8888)作为掩码图像。
- 调用本地方法
- 掩码绘制:
-
- 使用一个位于上层的透明
SurfaceView(drawView)来绘制分割结果。 - 在
drawBitmap方法中,锁定画布,将掩码 Bitmap 缩放至屏幕大小并绘制,同时显示推理耗时。 - 利用
PorterDuff.Mode.CLEAR清除上一帧的绘制。
- 使用一个位于上层的透明
- 前后摄像头切换:
-
- 通过
Switch控制mCameraView.switchCamera()(需在PortraitCameraView中实现)。
- 通过
- 资源释放:
onDestroy中未显式释放 MNN 实例(但 Activity 销毁时实例会被回收,建议显式调用release)。
总结
这些示例展示了 MNN 在 Android 上的典型应用场景:
|
类 |
用途 |
输入源 |
模型 |
输出处理 |
|
|
静态图像分类 |
assets 图片 |
MobileNet |
显示 top-3 类别及置信度 |
|
|
实时视频分类 |
相机预览 |
MobileNet / SqueezeNet |
实时更新 top-3 类别及置信度 |
|
|
实时人像分割 |
相机预览 |
Portrait(257x257) |
绘制分割掩码叠加在预览上 |
|
|
OpenGL 渲染测试 |
相机预览 |
(可能无推理) |
仅渲染 |
技术要点总结
- 图像预处理:使用
MNNImageProcess的convertBuffer或convertBitmap,支持 YUV_NV21、RGB、BGR 等格式,可配置 mean/normal、旋转缩放矩阵。 - 张量操作:通过
MNNNetInstance.Session.Tensor设置输入、获取输出,支持reshape、获取维度等。 - 会话管理:一个
MNNNetInstance可创建多个Session(不同配置),每个Session可多次run。 - 多线程处理:在视频流中通过
HandlerThread将推理放到后台,并使用丢弃机制防止任务堆积。 - 屏幕旋转适配:结合
OrientationEventListener和相机原始角度,正确旋转输入图像。 - 后端切换:通过
MNNForwardType选择 CPU/GPU 后端,动态调整线程数。
这些示例为开发者提供了清晰的集成参考,可以快速在自己的应用中引入 MNN 进行 AI 推理。
opengl
这三个文件位于 com.taobao.android.opengl 包,共同实现了一个基于 OpenGL ES 的相机预览渲染器,并尝试集成 MNN 推理(人像分割)来演示 OpenGL 后端的使用。下面分别解析每个类的作用和关键实现细节。
1. OESTexture.java
作用:封装一个用于相机预览的 OES(External OpenGL ES)纹理。
关键点:
- 使用
GLES11Ext.GL_TEXTURE_EXTERNAL_OES纹理目标,这是 Android 中配合SurfaceTexture从相机或其他视频源接收图像数据的专用纹理类型。 init()方法生成纹理 ID,绑定并设置纹理参数:
-
- 纹理环绕方式:
GL_CLAMP_TO_EDGE(边缘像素拉伸) - 纹理过滤:
GL_LINEAR(双线性插值)
- 纹理环绕方式:
- 提供
getTextureId()获取纹理句柄,供外部绑定使用。
2. Shader.java
作用:简化着色器程序的编译、链接和属性/统一变量访问。
关键点:
setProgram(int vertexShader, int fragmentShader, Context context):
-
- 从 raw 资源(如
R.raw.vshader和R.raw.fshader)读取 GLSL 源码。 - 分别编译顶点和片段着色器,然后链接为 OpenGL 程序对象。
- 若链接失败,抛出异常并清理资源。
- 从 raw 资源(如
getHandle(String name):
-
- 首先尝试作为顶点属性(
glGetAttribLocation),失败则尝试作为统一变量(glGetUniformLocation)。 - 结果缓存到
mShaderHandleMap,避免重复查询。
- 首先尝试作为顶点属性(
useProgram()和deleteProgram()管理程序激活和资源释放。- 该类封装了常见的着色器操作,使上层代码无需重复处理编译和链接细节。
3. CameraRenderer.java
作用:自定义 GLSurfaceView,集成了相机预览、纹理渲染和 MNN 推理(人像分割),用于演示 OpenGL 后端。
核心功能:
3.1 相机与纹理管理
- 继承
GLSurfaceView并实现Renderer和SurfaceTexture.OnFrameAvailableListener。 - 在
onSurfaceChanged中:
-
- 创建
OESTexture并初始化。 - 创建
SurfaceTexture并绑定到该纹理,设置帧可用监听器(onFrameAvailable)。 - 打开前置摄像头(通过
findFrontCamera()),设置预览尺寸为 1920x1080,并将预览输出到该SurfaceTexture。 - 注册预览回调
mPreviewCB并添加缓冲区,以获取 NV21 数据(但实际未使用)。
- 创建
onFrameAvailable触发后设置updateTexture = true并请求渲染。onDrawFrame中:
-
- 调用
mSurfaceTexture.updateTexImage()更新纹理内容。 - 使用
mOffscreenShader渲染全屏四边形,将相机纹理绘制到屏幕。 - 传递变换矩阵
uTransformM(来自SurfaceTexture.getTransformMatrix)、方向旋转矩阵uOrientationM和比例因子ratios,用于正确处理相机图像的方向和缩放。
- 调用
3.2 MNN 推理集成(未完整实现)
prepareModels():从 assets 复制人像分割模型Portrait/Portrait.tflite.mnn到缓存目录。prepareNet():加载模型,创建会话,获取输入输出张量(输入尺寸 257x257,输出亦然)。- 在
onSurfaceChanged中创建了一个后台线程MnnThread,其run()方法不断循环调用mSession.run()。 - 预览回调
mPreviewCB中实现了一个粗糙的同步机制:
-
- 检查
mOutputDateReady标志,若为 true 则将其置为 false,然后忙等待直到该标志变回 true,最后才调用addCallbackBuffer放回缓冲区。
- 检查
- 问题与缺陷:
-
- 数据未传递:从未将相机预览数据(NV21)设置到输入张量,因此
mSession.run()使用的是未初始化的数据,推理结果无意义。 - 线程阻塞:预览回调中的忙等待会阻塞相机线程,可能导致 ANR 或帧率下降。
- 无限循环:
MnnThread没有退出机制,即使 Activity 销毁,线程仍可能持续运行(虽然GLSurfaceView销毁时会释放 EGL 上下文,但线程可能继续访问已释放的资源)。 - 整体来看,该类的推理部分是一个未完成的占位实现,可能仅用于测试 OpenGL 后端是否能正常创建会话,或作为开发过程中的实验代码。
- 数据未传递:从未将相机预览数据(NV21)设置到输入张量,因此
3.3 其他
- 使用
RENDERMODE_WHEN_DIRTY按需渲染,节省电量。 - 在
onSurfaceCreated中加载着色器程序(vshader和fshader应实现从外部纹理到屏幕的简单渲染)。 onDestroy中释放相机和 SurfaceTexture 资源。
总结
OESTexture和Shader是 OpenGL 基础辅助类,分别管理外部纹理和着色器。CameraRenderer是一个功能较完整的相机预览渲染器,但集成的 MNN 推理部分存在明显缺陷(数据未填充、线程忙等),不适合直接用于实际推理任务。- 该组件可能是 MNN Demo 中用于验证 OpenGL 后端的测试用例,或是一个未完成的功能演示。开发者在参考时需注意补充数据流和同步机制。
utils
这些文件位于 com.taobao.android.utils 包,是 MNN Android Demo 中使用的工具类,主要提供文件操作、权限请求、相机封装和旋转枚举等基础功能。下面分别解析每个文件的作用和关键实现。
1. RotateType.java
作用:定义一个枚举,表示图像旋转的角度。
关键点:
- 包含四个常量:
Rotate0、Rotate90、Rotate180、Rotate270,每个关联一个整数值(0、90、180、270)。 - 用于在相机预览回调中传递图像的旋转角度,以便后续正确处理图像方向。
- 在
PortraitCameraView中,根据摄像头传感器方向计算出预览图像的旋转类型,并通过回调传递给上层。
2. TxtFileReader.java
作用:从 assets 目录的文本文件中逐行读取内容,常用于读取模型标签文件(如 ImageNet 类别名称)。
关键点:
- 内部类
ImageUrlProvider封装了BufferedReader,提供同步的getLine()方法逐行读取,并标记是否读完。 getUniqueUrls(Context context, String fileName, int count)方法返回指定数量的行(最多 count 行)。注释中原本有去重逻辑(rets.contains(url)),但被注释掉了,因此返回所有读取的行,可能存在重复。- 使用完毕后调用
close()释放资源。 - 在
ImageActivity和VideoActivity中用于加载synset_words.txt等标签文件。
3. Common.java
作用:提供通用工具方法,目前只有一个静态方法用于复制 assets 文件到文件系统。
关键点:
copyAssetResource2File(Context context, String assetsFile, String outFile):
-
- 从 assets 打开输入流,创建输出文件流。
- 以 1024 字节缓冲区循环读写,完成复制。
- 最后刷新并关闭流,设置输出文件可读。
- 在多个 Activity 中用于将模型文件从 assets 复制到缓存目录(如
getCacheDir() + "/model.mnn"),以便 MNN 加载。
4. PermissionUtils.java
作用:简化 Android 6.0+ 运行时权限请求的处理。
关键点:
askPermission(Activity context, String[] permissions, int req, Runnable runnable):
-
- 检查第一个权限是否已授予(
checkSelfPermission)。 - 若已授予,直接执行
runnable;否则请求指定权限。
- 检查第一个权限是否已授予(
onRequestPermissionsResult(boolean isReq, int[] grantResults, Runnable okRun, Runnable deniRun):
-
- 根据授权结果分别执行授权成功或失败的回调。
- 在
PortraitActivity和VideoActivity中用于请求相机和存储权限。
5. PortraitCameraView.java
作用:自定义的相机预览视图,专门为人像分割 demo 设计。它封装了 Camera API 的打开、配置、预览回调等功能,并处理屏幕旋转和前后摄像头切换。
关键点:
5.1 内部接口 PreviewCallback
- 定义回调方法:
onPreviewFrame(byte[] data, int imageWidth, int imageHeight, int angle, int degree, boolean needFlipX)。 - 传递原始 NV21 数据、预览尺寸、摄像头传感器角度(
angle)、屏幕旋转角度(degree)以及是否需要水平翻转(用于前置摄像头镜像)。
5.2 相机配置
- 内部类
Config存储宽高比、最小预览/图片宽度。 - 通过
CameraSizeComparator对支持的尺寸按高度排序,选择符合宽高比且不小于最小宽度的最合适尺寸。 - 默认配置:宽高比 1.334(约 4:3),最小预览/图片宽度 720。
5.3 核心方法
openCamera(SurfaceHolder holder, int cameraId):
-
- 释放旧相机,打开指定 ID 的摄像头。
- 设置预览方向和屏幕旋转角度(
setCameraDisplayOrientation)。 - 获取并设置预览尺寸和图片尺寸,固定预览格式为
NV21。 - 设置预览显示、注册预览回调、启动预览。
setCameraDisplayOrientation:
-
- 计算预览图像的正确显示方向(
setDisplayOrientation),同时计算出摄像头传感器的原始角度(mPreviewRotateType)和屏幕旋转角度(mDegree),供回调使用。
- 计算预览图像的正确显示方向(
switchCamera():切换前置/后置摄像头。onPreviewFrame:当有预览帧时,将数据通过mPreviewCallback传出,附带计算好的旋转参数。
5.4 生命周期管理
- 实现
SurfaceHolder.Callback,在surfaceCreated时打开相机。 - 提供
onResume/onPause方法供外部 Activity 调用,以打开/释放相机资源。
5.5 使用场景
- 在
PortraitActivity中,通过setPreviewCallback设置回调,获取每一帧的 NV21 数据和旋转信息,然后进行 MNN 推理(人像分割)并绘制结果。
总结
这些工具类为 MNN Demo 提供了基础支持:
- RotateType:统一旋转角度表示。
- TxtFileReader:读取文本标签。
- Common:复制 assets 文件。
- PermissionUtils:简化权限请求。
- PortraitCameraView:定制相机预览,专为人像分割场景设计。
它们共同构成了 Demo 的底层基础设施,使上层 Activity 能专注于模型推理和 UI 展示。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)