project/android/demo/READNE.md

未实现 环境配置

MNN Android Demo

1. 环境准备

开发工具

  • Android Studio
  • NDK

模型下载与转换:

首先编译(如果已编译可以跳过)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)

shicai/MobileNet-Caffe

model/MobileNet/v1/mobilenet_v1.caffe.mnn

MobileNet V2 (Caffe)

shicai/MobileNet-Caffe

model/MobileNet/v2/mobilenet_v2.caffe.mnn

SqueezeNet V1.0 (Caffe)

DeepScale/SqueezeNet

model/SqueezeNet/v1.0/squeezenet_v1.0.caffe.mnn

SqueezeNet V1.1 (Caffe)

DeepScale/SqueezeNet

model/SqueezeNet/v1.1/squeezenet_v1.1.caffe.mnn

MobileNet V2 (TFLite)

TensorFlow 官方模型库

model/MobileNet/v2/mobilenet_v2_1.0_224.tflite.mnn

MobileNet V2 Quantized (TFLite)

TensorFlow 官方模型库

model/MobileNet/v2/mobilenet_v2_1.0_224_quant.tflite.mnn

DeepLab v3 (Portrait, TFLite)

TensorFlow 官方模型库

model/Portrait/Portrait.tflite.mnn

中间文件

在转换过程中,脚本会在 resource/build 目录下临时存放下载的原始模型文件(如 .caffemodel.prototxt.tflite 等)。如果转换成功,这些文件不会被自动删除(但可以手动清理)。

注意事项

  • 脚本执行前需要确保 MNN 转换工具已经编译好,并位于 ../build/MNNConvert 路径(相对于脚本位置)。如果找不到该工具,脚本会退出并提示先编译转换器。
  • 如果某个 MNN 文件已经存在,脚本会跳过对应的下载和转换步骤(避免重复操作)。
  • 网络连接必须正常,因为需要从 GitHub 和 TensorFlow 官网下载模型文件。

执行环境要求

  • 操作系统:Linux / macOS(或 Windows 的 Bash 环境)
  • 工具:curltarbash
  • 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.dirndk.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

  1. 打开 File > Settings(或 File > Project Structure,取决于版本)。
  2. 导航到 Build, Execution, Deployment > Build Tools > Gradle
  3. Gradle JDK 下拉菜单中,选择一个 JDK 版本 118(如果已有)。如果列表中没有,可以点击下拉菜单旁边的 Download JDK 或手动添加已安装的 JDK 路径。
  4. 点击 ApplyOK
  5. 点击 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

  1. 在 Android Studio 的顶部菜单栏,点击 Tools -> SDK Manager(或者点击右上角那个长得像个向下箭头的方块图标)。
  2. 在弹出的窗口中,选择中间的 SDK Tools 选项卡。
  3. 勾选右下角的 Show Package Details(显示详细包信息)。
  4. 在列表中找到 NDK (Side by side) 并展开它。
  5. 找到并勾选 21.4.7075529 (或者其他 21.x 开头的版本)。
  6. 点击 ApplyOK,等待它下载并安装完成。

第二步:修改配置

下载完成后,回到你的 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.nativeConvertBitmapToTensornativeConvertBufferToTensor,这些 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_VulkanMNN_CLMNN_GL),最后加载 mnncore(可能包含 Java 层对应的 native 实现)。
  • 声明了所有与 C++ 交互的 native 方法,按功能分组:
    • NetnativeCreateNetFromFilenativeCreateNetFromBuffernativeReleaseNet
    • SessionnativeCreateSessionnativeReleaseSessionnativeRunSessionnativeRunSessionWithCallbacknativeReshapeSessionnativeGetSessionInputnativeGetSessionOutput
    • TensornativeReshapeTensornativeTensorGetDimensionsnativeSetInputIntDatanativeSetInputFloatDatanativeTensorGetData(float)、nativeTensorGetIntDatanativeTensorGetUINT8Data
    • ImageProcessnativeConvertBitmapToTensornativeConvertBufferToTensor
  • 注意:所有 native 方法均为 protected,仅供同包下的 Java 类调用,对外不可见。

整体工作流程(典型用法)

  1. 加载模型MNNNetInstance instance = MNNNetInstance.createFromFile("model.mnn");
  2. 配置会话:创建 MNNNetInstance.Config 对象,设置后端、线程数等。
  3. 创建会话MNNNetInstance.Session session = instance.createSession(config);
  4. 获取输入张量Tensor inputTensor = session.getInput("input_name");
  5. 准备输入数据
    • 若输入为图像,可使用 MNNImageProcess.convertBitmap()convertBuffer() 将 Bitmap 或字节数组直接填入张量。
    • 若输入为普通数据,调用 inputTensor.setInputFloatData(data)
  1. 执行推理session.run();
  2. 获取输出Tensor outputTensor = session.getOutput("output_name"); float[] result = outputTensor.getFloatData();
  3. 释放资源instance.release();

注意事项

  • 线程安全MNNNetInstanceSession 不是线程安全的,建议每个线程使用独立的实例或加锁保护。
  • 内存管理:必须显式调用 release() 释放 native 对象,避免内存泄漏。
  • 数据格式:张量数据在内存中按 NHWCNCHW 排列?需根据模型实际要求设置,通常图像输入为 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 视图(idrenderer_view)。
  • 生命周期管理:将 onResumeonPauseonDestroy 事件传递给 mRenderer,以便正确管理 OpenGL 上下文和相机资源。
  • 作用:这个 Activity 通常用于验证 OpenGL 后端(MNNForwardType.FORWARD_OPENGL)是否能正常工作,或者作为集成 MNN 与 OpenGL 处理的测试入口。

2. CameraView.java

作用

一个自定义的 SurfaceView,封装了 Android Camera API 的预览功能,并提供预览帧回调接口。它处理相机的打开、配置、旋转角度以及帧数据的回调。

关键点

  • 相机配置
    • 打开后置摄像头(Camera.open())。
    • 根据屏幕方向调整相机预览角度(setCameraDisplayOrientation)。
    • 选择合适的预览尺寸(getPropPreviewSize),确保最小尺寸不低于 MINIMUM_PREVIEW_SIZE(320)。
    • 设置预览格式为 NV21ImageFormat.NV21),这是 MNN 常用的输入格式。
  • 预览帧回调
    • 实现了 Camera.PreviewCallback 接口,在 onPreviewFrame 中每隔 PREVIEW_CALLBACK_FREQUENCE(5 帧)回调一次外部注册的 PreviewCallback,以减少处理频率。
    • 回调中会传递 NV21 数据、图像宽高以及相机的原始角度(mOrientationAngle)。
  • 生命周期:提供 onResumeonPause 方法供外部 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.convertBitmapBitmap 转换为模型输入张量。
    • 配置 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 和旋转缩放矩阵。
  • 推理调度
    • 使用 HandlerThreadHandler 在后台线程执行推理,避免阻塞 UI 线程。
    • 使用 AtomicBooleanmDrop)控制帧丢弃:如果上一帧推理未完成,则丢弃当前帧,防止任务堆积。
  • 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)作为掩码图像。
  • 掩码绘制
    • 使用一个位于上层的透明 SurfaceViewdrawView)来绘制分割结果。
    • drawBitmap 方法中,锁定画布,将掩码 Bitmap 缩放至屏幕大小并绘制,同时显示推理耗时。
    • 利用 PorterDuff.Mode.CLEAR 清除上一帧的绘制。
  • 前后摄像头切换
    • 通过 Switch 控制 mCameraView.switchCamera()(需在 PortraitCameraView 中实现)。
  • 资源释放onDestroy 中未显式释放 MNN 实例(但 Activity 销毁时实例会被回收,建议显式调用 release)。

总结

这些示例展示了 MNN 在 Android 上的典型应用场景:

用途

输入源

模型

输出处理

ImageActivity

静态图像分类

assets 图片

MobileNet

显示 top-3 类别及置信度

VideoActivity

实时视频分类

相机预览

MobileNet / SqueezeNet

实时更新 top-3 类别及置信度

PortraitActivity

实时人像分割

相机预览

Portrait(257x257)

绘制分割掩码叠加在预览上

OpenGLTestActivity

OpenGL 渲染测试

相机预览

(可能无推理)

仅渲染

技术要点总结

  • 图像预处理:使用 MNNImageProcessconvertBufferconvertBitmap,支持 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.vshaderR.raw.fshader)读取 GLSL 源码。
    • 分别编译顶点和片段着色器,然后链接为 OpenGL 程序对象。
    • 若链接失败,抛出异常并清理资源。
  • getHandle(String name)
    • 首先尝试作为顶点属性(glGetAttribLocation),失败则尝试作为统一变量(glGetUniformLocation)。
    • 结果缓存到 mShaderHandleMap,避免重复查询。
  • useProgram()deleteProgram() 管理程序激活和资源释放。
  • 该类封装了常见的着色器操作,使上层代码无需重复处理编译和链接细节。

3. CameraRenderer.java

作用:自定义 GLSurfaceView,集成了相机预览、纹理渲染和 MNN 推理(人像分割),用于演示 OpenGL 后端。
核心功能

3.1 相机与纹理管理

  • 继承 GLSurfaceView 并实现 RendererSurfaceTexture.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 后端是否能正常创建会话,或作为开发过程中的实验代码。

3.3 其他

  • 使用 RENDERMODE_WHEN_DIRTY 按需渲染,节省电量。
  • onSurfaceCreated 中加载着色器程序(vshaderfshader 应实现从外部纹理到屏幕的简单渲染)。
  • onDestroy 中释放相机和 SurfaceTexture 资源。

总结

  • OESTextureShader 是 OpenGL 基础辅助类,分别管理外部纹理和着色器。
  • CameraRenderer 是一个功能较完整的相机预览渲染器,但集成的 MNN 推理部分存在明显缺陷(数据未填充、线程忙等),不适合直接用于实际推理任务。
  • 该组件可能是 MNN Demo 中用于验证 OpenGL 后端的测试用例,或是一个未完成的功能演示。开发者在参考时需注意补充数据流和同步机制。

utils

这些文件位于 com.taobao.android.utils 包,是 MNN Android Demo 中使用的工具类,主要提供文件操作、权限请求、相机封装和旋转枚举等基础功能。下面分别解析每个文件的作用和关键实现。


1. RotateType.java

作用:定义一个枚举,表示图像旋转的角度。
关键点

  • 包含四个常量:Rotate0Rotate90Rotate180Rotate270,每个关联一个整数值(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() 释放资源。
  • ImageActivityVideoActivity 中用于加载 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)
    • 根据授权结果分别执行授权成功或失败的回调。
  • PortraitActivityVideoActivity 中用于请求相机和存储权限。

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 展示。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐