14、图像分类实战(一):模型准备(MobileNet)、图像预处理(Bitmap转张量)、加载模型并推理

各位同学,欢迎来到实战环节。前面我们聊了不少NNAPI的理论和基础API,今天终于要上手做点真正能跑的东西了。图像分类,可以说是移动端AI最经典的应用场景之一。你想想看,手机拍张照片,立刻告诉你这是猫还是狗,是杯子还是手机——这背后就是图像分类模型在干活。

今天这一章,我们聚焦在三个核心环节:模型准备、图像预处理、模型加载与推理。我会用MobileNet这个轻量级网络作为例子,带大家走通整个流程。

本章核心流程: 拿到一个训练好的MobileNet模型 → 把手机拍的照片转成模型能吃的张量 → 加载模型并跑一次推理 → 拿到分类结果。

图像分类实战流程(MobileNet + NNAPI) ① 模型准备 加载 .tflite 文件 ② 图像预处理 Bitmap → 张量 ③ 加载 & 推理 NNAPI 执行 从 assets 读取 缩放 224×224 归一化 [0,1] Interpreter.run() 解析输出 输入:Bitmap → 输出:Top-1 类别标签 + 置信度

14.1 模型准备:拿到MobileNet并放进项目

首先,你得有一个模型文件。MobileNet是Google为移动端设计的轻量级分类网络,参数量小、速度快,特别适合在手机上跑。我个人习惯用MobileNet V1,因为它的结构简单,NNAPI兼容性也最好。

去哪里拿?TensorFlow官方提供了预训练好的模型,你直接下载mobilenet_v1_1.0_224.tflite就行。这个模型输入是224×224的RGB图像,输出是1000个类别的置信度分数。

我的小建议: 把 .tflite 文件放在 app/src/main/assets/ 目录下。这样打包APK时模型会自动打进安装包,运行时直接从assets读取,省去下载的麻烦。

嗯,这里要注意一点:模型文件大小。MobileNet V1的tflite大约4.3MB,还算友好。但如果你用更大的模型,比如ResNet-50,那就要考虑是否放在assets里了——APK体积会暴涨。我在项目中遇到过,一个客户非要用Inception V3,结果APK直接多了80MB,最后只能改成运行时从服务器下载。

14.2 图像预处理:从Bitmap到张量

模型准备好了,接下来是关键一步:把手机拍的照片转成模型能理解的张量。说白了,就是把Bitmap对象变成float数组,形状是[1, 224, 224, 3]

为什么是224×224?因为MobileNet训练时用的就是224×224的图片。你给一张1920×1080的照片进去,模型会直接报错——它不认识这么大的输入。

预处理步骤其实就三件事:

  1. 缩放:把Bitmap缩放到224×224
  2. 转格式:把像素值从ARGB_8888转成RGB顺序的float数组
  3. 归一化:把[0, 255]的像素值映射到[-1, 1]或[0, 1]区间

来看代码,我直接贴一个我常用的工具方法:

// Bitmap转张量,适配MobileNet输入
public static float[] bitmapToFloatArray(Bitmap bitmap) {
    // 1. 缩放到224x224
    Bitmap resized = Bitmap.createScaledBitmap(bitmap, 224, 224, true);
    
    // 2. 分配输出数组:1 * 224 * 224 * 3
    float[] input = new float[1 * 224 * 224 * 3];
    int[] pixels = new int[224 * 224];
    resized.getPixels(pixels, 0, 224, 0, 0, 224, 224);
    
    // 3. 遍历每个像素,提取RGB并归一化到[-1, 1]
    for (int i = 0; i < pixels.length; i++) {
        int pixel = pixels[i];
        // 提取RGB分量(注意:Android中ARGB顺序)
        float r = ((pixel >> 16) & 0xFF) / 127.5f - 1.0f;
        float g = ((pixel >> 8) & 0xFF) / 127.5f - 1.0f;
        float b = (pixel & 0xFF) / 127.5f - 1.0f;
        
        // 按CHW顺序填充(实际是NHWC,但batch=1所以简化)
        input[i * 3] = r;
        input[i * 3 + 1] = g;
        input[i * 3 + 2] = b;
    }
    
    // 别忘了回收中间Bitmap
    resized.recycle();
    return input;
}

我曾经踩过的坑: 归一化范围搞错。MobileNet V1用的是[-1, 1]归一化,但有些模型用的是[0, 1]。如果你用错了,推理结果会完全不对——模型输出的置信度全是0.001这种小数字。所以拿到模型后,第一件事就是查它的输入归一化方式。一般模型文档里会写,或者你直接看tflite的元数据。

你可能会问:为什么不用Bitmap.createScaledBitmap直接缩放?嗯,这个方法确实方便,但要注意它默认使用Bitmap.Config.ARGB_8888,如果你的原图是RGB_565,需要先转换一下。另外,缩放算法我建议用Bitmap.createScaledBitmap的默认双线性插值,效果和速度都比较均衡。

14.3 加载模型并推理:NNAPI登场

图像预处理完了,接下来就是加载模型、跑推理。Android上加载tflite模型最常用的就是Interpreter类,它是TensorFlow Lite的Java API入口。

加载模型很简单:

// 从assets加载模型文件
MappedByteBuffer tfliteModel = FileUtil.loadModelFile(context, "mobilenet_v1_1.0_224.tflite");

// 创建Interpreter,开启NNAPI加速
Interpreter.Options options = new Interpreter.Options();
options.setUseNNAPI(true);  // 关键!启用NNAPI委托

Interpreter interpreter = new Interpreter(tfliteModel, options);

这里有个细节:setUseNNAPI(true) 就是告诉Interpreter优先使用NNAPI进行硬件加速。如果设备支持,它会自动把计算任务交给NPU或GPU;如果不支持,就回退到CPU执行。你想想看,这多省心——一行代码搞定硬件适配。

推理执行:

// 准备输入输出
float[][] output = new float[1][1000];  // 1000个类别

// 执行推理
interpreter.run(input, output);

// 解析结果:找到置信度最高的类别
int maxIndex = 0;
float maxConfidence = 0;
for (int i = 0; i < 1000; i++) {
    if (output[0][i] > maxConfidence) {
        maxConfidence = output[0][i];
        maxIndex = i;
    }
}

// 根据索引查标签(需要提前加载labels.txt)
String label = labelList.get(maxIndex);
Log.d("NNAPI_Demo", "识别结果: " + label + " 置信度: " + maxConfidence);

// 用完记得释放
interpreter.close();

性能数据参考(实测): 在骁龙888设备上,MobileNet V1单次推理耗时约8-12ms(开启NNAPI),关闭NNAPI时约15-20ms。说白了,NNAPI能带来大约40%-50%的加速。如果你的设备有独立NPU,差距会更明显。

14.4 完整流程串起来

把上面三部分拼在一起,就是一个完整的图像分类流程。我习惯把它们封装成一个ImageClassifier类,对外只暴露一个classify(Bitmap bitmap)方法。这样上层调用者完全不用关心模型加载、预处理这些细节。

这里有个设计上的小建议:Interpreter实例要复用。不要每次分类都new一个Interpreter,那太慢了。我一般在Application初始化时创建一次,然后整个生命周期复用。记得在不需要时调用interpreter.close()释放资源。

我的经验: 如果你需要频繁切换模型(比如同时支持MobileNet和EfficientNet),可以用一个Map来管理多个Interpreter实例。但要注意内存占用——每个Interpreter都会加载完整的模型图,多个模型同时加载可能会OOM。我一般只保留当前活跃的模型,切换时关闭旧的再加载新的。

最后,关于标签文件。MobileNet输出的1000个类别对应的是ImageNet的标签。你需要准备一个labels.txt文件,每行一个标签名,顺序和模型输出索引一一对应。加载方式也很简单:

List<String> labels = new ArrayList<>();
BufferedReader reader = new BufferedReader(
    new InputStreamReader(context.getAssets().open("labels.txt")));
String line;
while ((line = reader.readLine()) != null) {
    labels.add(line);
}
reader.close();

好了,这一章的内容就到这里。模型准备、图像预处理、加载推理,这三个环节是图像分类的基石。下一章我们会继续深入,聊聊如何优化推理性能、处理多线程并发,以及一些实际项目中的坑。各位同学可以先动手把今天的内容跑通,有问题随时交流。


公众号:蓝海资料掘金营,微信deep3321