4. 对象检测
任务目标
在Android应用中集成TensorFlow Lite的SSD MobileNet模型,对摄像头预览帧执行实时对象检测,并在预览画面中绘制检测到的对象边界框与类别标签。
环境依赖
- Android Studio 4.2+,minSdkVersion 21+
- TensorFlow Lite 2.10.0(
org.tensorflow:tensorflow-lite:2.10.0) - 模型文件:
detect.tflite(SSD MobileNet v1,输入尺寸300×300,输出为边界框/类别/分数/数量),放置于app/src/main/assets/目录 - 标签文件:
labelmap.txt(每行一个类别名,与模型输出索引对应),同样置于assets目录
核心实现
以下代码包含:模型加载与预处理、推理执行、结果解析,以及一个最小可运行的Activity,使用静态图片(从资源读取)模拟单帧检测流程,便于验证核心逻辑。摄像头实时预览接入只需将processFrame()的输入替换为ImageProxy转换后的Bitmap即可。
// ObjectDetector.kt - 核心检测器
import android.content.Context
import android.graphics.Bitmap
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
data class Detection(val label: String, val score: Float, val left: Float, val top: Float, val right: Float, val bottom: Float)
class ObjectDetector(context: Context, modelPath: String = "detect.tflite", labelPath: String = "labelmap.txt") {
private val interpreter: Interpreter
private val labels: List<String>
private val inputSize = 300
private val pixelSize = 3
private val numDetections = 10
init {
interpreter = Interpreter(loadModelFile(context, modelPath))
labels = context.assets.open(labelPath).bufferedReader().readLines()
}
private fun loadModelFile(context: Context, path: String): MappedByteBuffer {
val fd = context.assets.openFd(path)
val input = FileInputStream(fd.fileDescriptor).channel
return input.map(FileChannel.MapMode.READ_ONLY, fd.startOffset, fd.declaredLength)
}
fun processFrame(bitmap: Bitmap): List<Detection> {
val scaled = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true)
val input = convertBitmapToByteBuffer(scaled)
val output = Array(1) { Array(numDetections) { FloatArray(4) } } // boxes
val classes = Array(1) { FloatArray(numDetections) }
val scores = Array(1) { FloatArray(numDetections) }
val count = Array(1) { FloatArray(1) }
interpreter.runForMultipleInputsOutputs(
arrayOf(input),
mapOf(0 to output, 1 to classes, 2 to scores, 3 to count)
)
val results = mutableListOf<Detection>()
val detectionCount = count[0][0].toInt().coerceAtMost(numDetections)
for (i in 0 until detectionCount) {
val score = scores[0][i]
if (score > 0.5f) {
val box = output[0][i]
val labelIdx = classes[0][i].toInt()
val label = if (labelIdx in labels.indices) labels[labelIdx] else "unknown"
results.add(Detection(label, score, box[1], box[0], box[3], box[2]))
}
}
return results
}
private fun convertBitmapToByteBuffer(bitmap: Bitmap): ByteBuffer {
val byteBuffer = ByteBuffer.allocateDirect(4 * inputSize * inputSize * pixelSize)
byteBuffer.order(ByteOrder.nativeOrder())
val pixels = IntArray(inputSize * inputSize)
bitmap.getPixels(pixels, 0, inputSize, 0, 0, inputSize, inputSize)
for (pixel in pixels) {
val r = (pixel shr 16) and 0xFF
val g = (pixel shr 8) and 0xFF
val b = pixel and 0xFF
byteBuffer.putFloat(r / 255.0f)
byteBuffer.putFloat(g / 255.0f)
byteBuffer.putFloat(b / 255.0f)
}
return byteBuffer
}
}
// MainActivity.kt - 最小验证入口
import android.graphics.BitmapFactory
import android.os.Bundle
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val imageView = findViewById<ImageView>(R.id.imageView)
val textView = findViewById<TextView>(R.id.textView)
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.test_image)
val detector = ObjectDetector(this)
val detections = detector.processFrame(bitmap)
// 在Bitmap上绘制边界框(简化:仅输出文本信息)
val sb = StringBuilder()
detections.forEach { d ->
sb.append("${d.label} (${"%.2f".format(d.score)})\n")
sb.append(" box: [${"%.2f".format(d.left)}, ${"%.2f".format(d.top)}, ${"%.2f".format(d.right)}, ${"%.2f".format(d.bottom)}]\n")
}
textView.text = if (sb.isEmpty()) "No objects detected" else sb.toString()
imageView.setImageBitmap(bitmap)
}
}
运行与验证
- 将
detect.tflite和labelmap.txt放入app/src/main/assets/目录。 - 准备一张包含已知物体的测试图片(如包含人、车),命名为
test_image.jpg放入res/drawable/。 - 在
app/build.gradle中添加依赖:implementation 'org.tensorflow:tensorflow-lite:2.10.0'。 - 运行App,观察TextView输出。
通过条件:TextView中显示至少一个检测结果,且类别名称与图片内容一致(例如图片中有"person",输出包含"person"),分数大于0.5。边界框坐标值在0~1之间且符合对象位置比例。
常见问题
- 模型加载失败:确认assets路径正确,且模型文件未损坏。
- 输出全为0:检查输入归一化是否正确(SSD MobileNet需要[0,1]范围的float输入)。
- 坐标方向:模型输出为[y_min, x_min, y_max, x_max]归一化坐标,绘制时需乘以视图宽高。