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);
运行与验证
- 连接Android设备(API 21+),点击Run安装应用
- 授权相机权限,将手机摄像头对准任意物体(如杯子、手机、书籍)
- 观察屏幕下方TextView是否显示类别名称及置信度,例如对准苹果时显示
Granny Smith 0.85 - 缓慢移动摄像头切换不同物体,确认标签随之变化且无明显卡顿(帧率≥10fps即满足实时性要求)
通过条件:画面中物体能被正确识别为对应类别(Top-1准确),置信度显示在0.5~1.0之间,且从摄像头画面变化到结果更新延迟不超过500ms。若识别结果持续错误,检查模型文件是否匹配输入尺寸224及标签文件顺序是否与模型输出对应。