4. 实时图像分类

任务目标

在Android端通过CameraX获取摄像头预览帧,将其转换为TFLite模型所需的输入张量,执行实时图像分类,并在预览画面中叠加显示Top-1类别及置信度。本案例使用Google提供的MobilenetV1量化模型(mobilenet_v1_1.0_224_quant.tflite),输入尺寸为224×224,输出为1000类ImageNet标签。

环境依赖

  • Android Studio Arctic Fox或更高版本,minSdkVersion 21+
  • 依赖库:org.tensorflow:tensorflow-lite:2.14.0、androidx.camera:camera-core:1.3.0、androidx.camera:camera-camera2:1.3.0、androidx.camera:camera-lifecycle:1.3.0、androidx.camera:camera-view:1.3.0
  • 模型文件放入 assets/ 目录,标签文件 labels.txt(每行一个类别名,共1000行)同样放入 assets/

核心实现

以下代码包含完整的分类器封装与相机帧处理逻辑。分类器负责加载模型、执行推理并返回Top-1结果;相机部分通过ImageAnalysis.Analyzer将每一帧的YUV_420_888格式图像旋转后缩放至224×224,再转换为ByteBuffer供模型使用。

// ImageClassifier.java - TFLite模型封装
public class ImageClassifier {
    private final Interpreter interpreter;
    private final List<String> labels;
    private final int inputSize = 224;
    private final int[] imagePixels = new int[inputSize * inputSize];

    public ImageClassifier(Context context) throws IOException {
        interpreter = new Interpreter(loadModelFile(context));
        labels = loadLabels(context);
    }

    private MappedByteBuffer loadModelFile(Context context) throws IOException {
        AssetFileDescriptor afd = context.getAssets().openFd("mobilenet_v1_1.0_224_quant.tflite");
        FileInputStream fis = new FileInputStream(afd.getFileDescriptor());
        FileChannel channel = fis.getChannel();
        return channel.map(FileChannel.MapMode.READ_ONLY, afd.getStartOffset(), afd.getDeclaredLength());
    }

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

    // 将Bitmap缩放至224x224并转换为量化输入ByteBuffer
    private ByteBuffer convertBitmapToByteBuffer(Bitmap bitmap) {
        Bitmap resized = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true);
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect(4 * inputSize * inputSize * 3);
        byteBuffer.order(ByteOrder.nativeOrder());

        resized.getPixels(imagePixels, 0, inputSize, 0, 0, inputSize, inputSize);
        for (int pixel : imagePixels) {
            int r = (pixel >> 16) & 0xFF;
            int g = (pixel >> 8) & 0xFF;
            int b = pixel & 0xFF;
            byteBuffer.put((byte) r);
            byteBuffer.put((byte) g);
            byteBuffer.put((byte) b);
        }
        return byteBuffer;
    }

    // 执行推理,返回[类别名, 置信度]数组
    public String[] recognize(Bitmap bitmap) {
        ByteBuffer input = convertBitmapToByteBuffer(bitmap);
        byte[][] output = new byte[1][labels.size()];
        interpreter.run(input, output);

        int maxIndex = 0;
        byte maxScore = Byte.MIN_VALUE;
        for (int i = 0; i < output[0].length; i++) {
            if (output[0][i] > maxScore) {
                maxScore = output[0][i];
                maxIndex = i;
            }
        }
        float confidence = (maxScore & 0xFF) / 255.0f;
        return new String[]{labels.get(maxIndex), String.format("%.2f", confidence)};
    }

    public void close() {
        interpreter.close();
    }
}

// CameraAnalyzer.java - 相机帧实时分析
public class CameraAnalyzer implements ImageAnalysis.Analyzer {
    private final ImageClassifier classifier;
    private final OnResultListener listener;

    public interface OnResultListener {
        void onResult(String label, String confidence);
    }

    public CameraAnalyzer(ImageClassifier classifier, OnResultListener listener) {
        this.classifier = classifier;
        this.listener = listener;
    }

    @Override
    public void analyze(@NonNull ImageProxy imageProxy) {
        Image image = imageProxy.getImage();
        if (image == null) {
            imageProxy.close();
            return;
        }

        Bitmap bitmap = ImageUtils.yuvToRgbBitmap(image, imageProxy.getImageInfo().getRotationDegrees());
        String[] result = classifier.recognize(bitmap);
        listener.onResult(result[0], result[1]);

        bitmap.recycle();
        imageProxy.close();
    }
}

上述代码中ImageUtils.yuvToRgbBitmap()负责将YUV_420_888格式的Image转换为RGB Bitmap,并根据旋转角度校正方向。该工具方法约40行,核心逻辑为从三个Plane中提取Y/U/V分量并合并为RGB像素数组,此处不展开完整代码,按标准YUV转RGB公式实现即可。

相机绑定与UI显示

// 在Activity或Fragment中初始化
ImageClassifier classifier = new ImageClassifier(context);
ImageAnalysis analysis = new ImageAnalysis.Builder()
        .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
        .build();
analysis.setAnalyzer(Executors.newSingleThreadExecutor(), new CameraAnalyzer(classifier, (label, conf) ->
        runOnUiThread(() -> resultTextView.setText(label + " " + conf)));

CameraX.bindToLifecycle(this, cameraProvider, preview, analysis);

运行与验证

  1. 连接Android设备(API 21+),点击Run安装应用
  2. 授权相机权限,将手机摄像头对准任意物体(如杯子、手机、书籍)
  3. 观察屏幕下方TextView是否显示类别名称及置信度,例如对准苹果时显示Granny Smith 0.85
  4. 缓慢移动摄像头切换不同物体,确认标签随之变化且无明显卡顿(帧率≥10fps即满足实时性要求)

通过条件:画面中物体能被正确识别为对应类别(Top-1准确),置信度显示在0.5~1.0之间,且从摄像头画面变化到结果更新延迟不超过500ms。若识别结果持续错误,检查模型文件是否匹配输入尺寸224及标签文件顺序是否与模型输出对应。