1. 图像分类入门

任务目标

在Android应用中集成TensorFlow Lite,加载MobileNetV2图像分类模型,对摄像头预览帧执行实时推理,并在屏幕上叠加显示Top-1类别标签与置信度。

环境依赖

  • Android Studio Hedgehog或更新版本,minSdk 24+,compileSdk 34+
  • CameraX库(1.3.0+)用于预览帧获取
  • TensorFlow Lite运行时:org.tensorflow:tensorflow-lite:2.14.0
  • 模型文件:mobilenet_v2_1.0_224.tflite(输入224×224×3,输出1000类)
  • 标签文件:labels.txt(1000行,每行一个类别名)

核心实现

以下代码包含完整的TFLite分类器封装与CameraX分析器接入。模型文件放入app/src/main/assets/目录,标签文件同目录。

// TFLiteClassifier.kt - 模型加载与推理
import android.content.Context
import org.tensorflow.lite.Interpreter
import java.io.FileInputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.nio.MappedByteBuffer
import java.nio.channels.FileChannel

class TFLiteClassifier(context: Context) {
    private val interpreter: Interpreter
    private val labels: List<String>

    init {
        interpreter = Interpreter(loadModelFile(context))
        labels = context.assets.open("labels.txt").bufferedReader().readLines()
    }

    private fun loadModelFile(context: Context): MappedByteBuffer {
        val fd = context.assets.openFd("mobilenet_v2_1.0_224.tflite")
        val inputStream = FileInputStream(fd.fileDescriptor)
        val channel = inputStream.channel
        return channel.map(FileChannel.MapMode.READ_ONLY, fd.startOffset, fd.declaredLength)
    }

    fun classify(bitmap: android.graphics.Bitmap): Pair<String, Float> {
        val modelInput = preprocess(bitmap)
        val output = Array(1) { FloatArray(1000) }
        interpreter.run(modelInput, output)

        val maxIdx = output[0].indices.maxByOrNull { output[0][it] } ?: 0
        val confidence = output[0][maxIdx]
        return labels[maxIdx] to confidence
    }

    private fun preprocess(bitmap: android.graphics.Bitmap): ByteBuffer {
        val resized = android.graphics.Bitmap.createScaledBitmap(bitmap, 224, 224, true)
        val buffer = ByteBuffer.allocateDirect(4 * 224 * 224 * 3)
        buffer.order(ByteOrder.nativeOrder())

        val pixels = IntArray(224 * 224)
        resized.getPixels(pixels, 0, 224, 0, 0, 224, 224)

        for (pixel in pixels) {
            val r = (pixel shr 16 and 0xFF) / 255.0f
            val g = (pixel shr 8 and 0xFF) / 255.0f
            val b = (pixel and 0xFF) / 255.0f
            buffer.putFloat(r)
            buffer.putFloat(g)
            buffer.putFloat(b)
        }
        resized.recycle()
        return buffer
    }

    fun close() {
        interpreter.close()
    }
}
// CameraAnalyzer.kt - 实时帧处理
import androidx.camera.core.ImageAnalysis
import androidx.camera.core.ImageProxy
import android.graphics.Bitmap
import android.graphics.Matrix

class CameraAnalyzer(
    private val classifier: TFLiteClassifier,
    private val onResult: (String, Float) -> Unit
) : ImageAnalysis.Analyzer {

    override fun analyze(image: ImageProxy) {
        val bitmap = image.toBitmap()
        val rotated = rotateBitmap(bitmap, image.imageInfo.rotationDegrees)
        val (label, confidence) = classifier.classify(rotated)

        onResult(label, confidence)
        rotated.recycle()
        bitmap.recycle()
        image.close()
    }

    private fun ImageProxy.toBitmap(): Bitmap {
        val buffer = planes[0].buffer
        val bytes = ByteArray(buffer.remaining())
        buffer.get(bytes)
        return android.graphics.BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
    }

    private fun rotateBitmap(src: Bitmap, degrees: Int): Bitmap {
        if (degrees == 0) return src
        val matrix = Matrix().apply { postRotate(degrees.toFloat()) }
        return Bitmap.createBitmap(src, 0, 0, src.width, src.height, matrix, true)
    }
}
// MainActivity.kt - 相机绑定与UI更新
import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.camera.core.CameraSelector
import androidx.camera.core.ImageAnalysis
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat

class MainActivity : AppCompatActivity() {
    private lateinit var classifier: TFLiteClassifier
    private lateinit var resultText: TextView

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        resultText = findViewById(R.id.resultText)
        classifier = TFLiteClassifier(this)
        startCamera()
    }

    private fun startCamera() {
        val providerFuture = ProcessCameraProvider.getInstance(this)
        providerFuture.addListener({
            val provider = providerFuture.get()
            val preview = androidx.camera.core.Preview.Builder().build()
            val analysis = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
            analysis.setAnalyzer(ContextCompat.getMainExecutor(this), CameraAnalyzer(classifier) { label, conf ->
                runOnUiThread {
                    resultText.text = String.format("%s  %.1f%%", label, conf * 100)
                }
            })

            provider.unbindAll()
            provider.bindToLifecycle(this, CameraSelector.DEFAULT_BACK_CAMERA, preview, analysis)
        }, ContextCompat.getMainExecutor(this))
    }

    override fun onDestroy() {
        super.onDestroy()
        classifier.close()
    }
}

布局与配置

<!-- activity_main.xml 核心部分 -->
<androidx.camera.view.PreviewView
    android:id="@+id/previewView"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

<TextView
    android:id="@+id/resultText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="bottom|center_horizontal"
    android:textSize="20sp"
    android:textColor="#FFFFFF"
    android:background="#80000000"
    android:padding="8dp" />
// build.gradle (app级别) 关键依赖
dependencies {
    implementation 'org.tensorflow:tensorflow-lite:2.14.0'
    implementation 'androidx.camera:camera-core:1.3.0'
    implementation 'androidx.camera:camera-camera2:1.3.0'
    implementation 'androidx.camera:camera-lifecycle:1.3.0'
    implementation 'androidx.camera:camera-view:1.3.0'
}

运行与验证

  1. 将mobilenet_v2_1.0_224.tflite与labels.txt放入app/src/main/assets/目录
  2. 连接Android设备(API 24+),点击Run构建安装
  3. 授予相机权限,将手机摄像头对准常见物体(如杯子、键盘、植物)

通过条件:屏幕底部实时显示类别名称与置信度百分比;对准不同物体时标签随之变化,置信度通常高于50%(光线充足、物体清晰时)。若显示“Unknown”或置信度持续低于30%,检查模型输入是否匹配(224×224、RGB归一化到[0,1])。

常见问题

  • 崩溃提示模型加载失败:确认assets目录下文件名完全一致,且模型为有效的TFLite格式
  • 预览画面倒转:已通过rotateBitmap处理旋转角度,若仍异常检查设备方向传感器配置
  • 帧率过低:将ImageAnalysis分辨率设为640×480,或增加setTargetFrameRate(15)限制