YOLOv11安卓目标检测App完整开发指南
目录
1. 项目概述
1.1 项目目标
开发一款基于YOLOv11的安卓移动端目标检测应用,实现:
- 实时相机拍照/视频流检测
- 离线模型推理(无需联网)
- 自动应用和模型更新
- 检测结果上传至MES系统
- 高性能、低延迟的用户体验
1.2 技术栈
- 编程语言: Kotlin/Java
- 深度学习框架: TensorFlow Lite / ONNX Runtime / NCNN
- UI框架: Jetpack Compose / XML布局
- 网络库: Retrofit2 + OkHttp3
- 图像处理: CameraX + OpenCV
- 依赖注入: Hilt/Dagger2
- 数据库: Room Database
1.3 核心功能模块
┌─────────────────────────────────────┐
│ 安卓App主应用 │
├─────────────────────────────────────┤
│ 相机模块 │ 检测模块 │ 更新模块 │
│ 拍照/录像 │ YOLOv11 │ APK/模型 │
├─────────────────────────────────────┤
│ 结果处理 │ 数据存储 │ 网络通信 │
│ 可视化 │ 本地DB │ MES对接 │
└─────────────────────────────────────┘
2. 系统架构设计
2.1 整体架构
采用MVVM(Model-View-ViewModel)架构模式:
┌──────────────┐
│ View层 │ Activity/Fragment + Jetpack Compose
├──────────────┤
│ ViewModel层 │ 业务逻辑 + LiveData/StateFlow
├──────────────┤
│ Repository │ 数据仓库层
├──────────────┤
│ Data Source │ 本地DB + 网络API + 模型推理
└──────────────┘
2.2 模块划分
app/
├── data/ # 数据层
│ ├── local/ # 本地数据源
│ │ ├── dao/ # Room DAO
│ │ ├── database/ # 数据库
│ │ └── entity/ # 数据实体
│ ├── remote/ # 远程数据源
│ │ ├── api/ # API接口
│ │ └── dto/ # 数据传输对象
│ └── repository/ # 数据仓库
├── domain/ # 业务逻辑层
│ ├── model/ # 业务模型
│ └── usecase/ # 用例
├── presentation/ # 展示层
│ ├── camera/ # 相机界面
│ ├── detection/ # 检测界面
│ └── settings/ # 设置界面
├── ml/ # 机器学习模块
│ ├── detector/ # 检测器
│ ├── preprocessor/ # 预处理
│ └── postprocessor/ # 后处理
└── utils/ # 工具类
├── camera/ # 相机工具
├── network/ # 网络工具
└── update/ # 更新工具
3. 开发环境配置
3.1 Android Studio配置
gradle
// build.gradle (Project level)
buildscript {
ext.kotlin_version = '1.9.20'
ext.compose_version = '1.5.4'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.2.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.dagger:hilt-android-gradle-plugin:2.48'
}
}
3.2 App模块依赖
gradle
// build.gradle (App level)
plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-kapt'
id 'dagger.hilt.android.plugin'
}
android {
namespace 'com.example.yolodetector'
compileSdk 34
defaultConfig {
applicationId "com.example.yolodetector"
minSdk 24
targetSdk 34
versionCode 1
versionName "1.0.0"
// 支持armeabi-v7a和arm64-v8a
ndk {
abiFilters 'armeabi-v7a', 'arm64-v8a'
}
}
buildFeatures {
viewBinding true
compose true
mlModelBinding true
}
composeOptions {
kotlinCompilerExtensionVersion = compose_version
}
packagingOptions {
pickFirst 'lib/arm64-v8a/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
}
}
dependencies {
// Kotlin
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.12.0'
// UI
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation "androidx.compose.ui:ui:$compose_version"
implementation "androidx.compose.material3:material3:1.1.2"
// CameraX
def camerax_version = "1.3.1"
implementation "androidx.camera:camera-core:$camerax_version"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:$camerax_version"
// TensorFlow Lite
implementation 'org.tensorflow:tensorflow-lite:2.14.0'
implementation 'org.tensorflow:tensorflow-lite-gpu:2.14.0'
implementation 'org.tensorflow:tensorflow-lite-support:0.4.4'
// ONNX Runtime (可选)
implementation 'com.microsoft.onnxruntime:onnxruntime-android:1.16.3'
// OpenCV
implementation 'org.opencv:opencv:4.8.0'
// 网络请求
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'
implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'
// 依赖注入
implementation 'com.google.dagger:hilt-android:2.48'
kapt 'com.google.dagger:hilt-compiler:2.48'
// 数据库
def room_version = "2.6.1"
implementation "androidx.room:room-runtime:$room_version"
implementation "androidx.room:room-ktx:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// 协程
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
// ViewModel
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0'
}
3.3 权限配置
xml
<!-- AndroidManifest.xml -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 相机权限 -->
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- 存储权限 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 更新安装权限 -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
<application
android:name=".YoloApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true">
<!-- FileProvider配置 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>
4. YOLOv11模型准备与转换
4.1 模型导出
使用Python将YOLOv11模型转换为移动端格式:
python
from ultralytics import YOLO
# 加载训练好的模型
model = YOLO('yolov11n.pt') # 或使用自己训练的模型
# 方案1: 转换为TensorFlow Lite
model.export(format='tflite', imgsz=640, int8=False)
# 方案2: 转换为ONNX
model.export(format='onnx', imgsz=640, simplify=True, dynamic=False)
# 方案3: 转换为NCNN (适合移动端)
model.export(format='ncnn', imgsz=640)
4.2 模型量化(可选,减小体积)
python
import tensorflow as tf
# INT8量化
converter = tf.lite.TFLiteConverter.from_saved_model('yolov11_saved_model')
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_types = [tf.int8]
# 提供代表性数据集
def representative_dataset():
for _ in range(100):
yield [np.random.rand(1, 640, 640, 3).astype(np.float32)]
converter.representative_dataset = representative_dataset
tflite_quant_model = converter.convert()
with open('yolov11_int8.tflite', 'wb') as f:
f.write(tflite_quant_model)
4.3 模型文件放置
将转换后的模型文件放入Android项目:
app/src/main/assets/
├── yolov11.tflite # TFLite模型
├── labels.txt # 类别标签
└── config.json # 模型配置
labels.txt示例:
person
bicycle
car
motorcycle
...
config.json示例:
json
{
"model_name": "yolov11n",
"input_size": 640,
"num_classes": 80,
"conf_threshold": 0.25,
"iou_threshold": 0.45,
"max_detections": 300,
"normalization": {
"mean": [0, 0, 0],
"std": [255, 255, 255]
}
}
5. 安卓App核心功能实现
5.1 Application类初始化
kotlin
@HiltAndroidApp
class YoloApplication : Application() {
override fun onCreate() {
super.onCreate()
// 初始化OpenCV
if (!OpenCVLoader.initDebug()) {
Log.e("OpenCV", "Unable to load OpenCV!")
}
// 初始化全局配置
AppConfig.init(this)
}
}
5.2 依赖注入配置
kotlin
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideContext(application: Application): Context {
return application.applicationContext
}
@Provides
@Singleton
fun provideYoloDetector(context: Context): YoloDetector {
return YoloDetector(context)
}
@Provides
@Singleton
fun provideRetrofit(): Retrofit {
return Retrofit.Builder()
.baseUrl(BuildConfig.BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.client(
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = HttpLoggingInterceptor.Level.BODY
})
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.build()
)
.build()
}
@Provides
@Singleton
fun provideMesApi(retrofit: Retrofit): MesApi {
return retrofit.create(MesApi::class.java)
}
}
5.3 数据库设计
kotlin
// 检测结果实体
@Entity(tableName = "detection_results")
data class DetectionResult(
@PrimaryKey(autoGenerate = true)
val id: Long = 0,
val timestamp: Long,
val imagePath: String,
val detections: String, // JSON格式的检测框信息
val isSynced: Boolean = false,
val mesOrderId: String? = null
)
// DAO接口
@Dao
interface DetectionDao {
@Insert
suspend fun insert(result: DetectionResult): Long
@Query("SELECT * FROM detection_results WHERE isSynced = 0 ORDER BY timestamp DESC")
fun getUnsyncedResults(): Flow<List<DetectionResult>>
@Query("UPDATE detection_results SET isSynced = 1 WHERE id = :id")
suspend fun markAsSynced(id: Long)
@Query("SELECT * FROM detection_results ORDER BY timestamp DESC LIMIT :limit")
fun getRecentResults(limit: Int): Flow<List<DetectionResult>>
}
// 数据库
@Database(entities = [DetectionResult::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun detectionDao(): DetectionDao
}
6. 自动更新机制
6.1 更新检查服务
kotlin
data class UpdateInfo(
val versionCode: Int,
val versionName: String,
val apkUrl: String,
val modelUrl: String?,
val updateMessage: String,
val forceUpdate: Boolean,
val md5: String
)
interface UpdateApi {
@GET("api/version/check")
suspend fun checkUpdate(
@Query("currentVersion") currentVersion: Int,
@Query("packageName") packageName: String
): Response<UpdateInfo>
@Streaming
@GET
suspend fun downloadFile(@Url fileUrl: String): Response<ResponseBody>
}
6.2 更新管理器
kotlin
class UpdateManager @Inject constructor(
private val context: Context,
private val updateApi: UpdateApi
) {
private val downloadDir = File(context.getExternalFilesDir(null), "downloads")
init {
if (!downloadDir.exists()) {
downloadDir.mkdirs()
}
}
// 检查更新
suspend fun checkForUpdates(): UpdateInfo? {
return try {
val currentVersion = context.packageManager
.getPackageInfo(context.packageName, 0).versionCode
val response = updateApi.checkUpdate(
currentVersion,
context.packageName
)
if (response.isSuccessful && response.body() != null) {
val updateInfo = response.body()!!
if (updateInfo.versionCode > currentVersion) {
updateInfo
} else {
null
}
} else {
null
}
} catch (e: Exception) {
Log.e("UpdateManager", "检查更新失败", e)
null
}
}
// 下载APK
suspend fun downloadApk(
url: String,
onProgress: (Int) -> Unit
): File? = withContext(Dispatchers.IO) {
try {
val response = updateApi.downloadFile(url)
if (!response.isSuccessful) return@withContext null
val apkFile = File(downloadDir, "update_${System.currentTimeMillis()}.apk")
val body = response.body() ?: return@withContext null
val totalBytes = body.contentLength()
val inputStream = body.byteStream()
val outputStream = FileOutputStream(apkFile)
val buffer = ByteArray(8192)
var downloadedBytes = 0L
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
downloadedBytes += bytesRead
val progress = ((downloadedBytes * 100) / totalBytes).toInt()
withContext(Dispatchers.Main) {
onProgress(progress)
}
}
outputStream.close()
inputStream.close()
apkFile
} catch (e: Exception) {
Log.e("UpdateManager", "下载APK失败", e)
null
}
}
// 下载模型文件
suspend fun downloadModel(
url: String,
onProgress: (Int) -> Unit
): File? = withContext(Dispatchers.IO) {
try {
val response = updateApi.downloadFile(url)
if (!response.isSuccessful) return@withContext null
val modelFile = File(context.filesDir, "models/yolov11_new.tflite")
modelFile.parentFile?.mkdirs()
val body = response.body() ?: return@withContext null
val totalBytes = body.contentLength()
val inputStream = body.byteStream()
val outputStream = FileOutputStream(modelFile)
val buffer = ByteArray(8192)
var downloadedBytes = 0L
var bytesRead: Int
while (inputStream.read(buffer).also { bytesRead = it } != -1) {
outputStream.write(buffer, 0, bytesRead)
downloadedBytes += bytesRead
val progress = ((downloadedBytes * 100) / totalBytes).toInt()
withContext(Dispatchers.Main) {
onProgress(progress)
}
}
outputStream.close()
inputStream.close()
modelFile
} catch (e: Exception) {
Log.e("UpdateManager", "下载模型失败", e)
null
}
}
// 安装APK
fun installApk(apkFile: File) {
val intent = Intent(Intent.ACTION_VIEW)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val uri = FileProvider.getUriForFile(
context,
"${context.packageName}.fileprovider",
apkFile
)
intent.setDataAndType(uri, "application/vnd.android.package-archive")
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} else {
intent.setDataAndType(
Uri.fromFile(apkFile),
"application/vnd.android.package-archive"
)
}
context.startActivity(intent)
}
// 验证MD5
fun verifyMd5(file: File, expectedMd5: String): Boolean {
return try {
val md = MessageDigest.getInstance("MD5")
val fis = FileInputStream(file)
val buffer = ByteArray(8192)
var bytesRead: Int
while (fis.read(buffer).also { bytesRead = it } != -1) {
md.update(buffer, 0, bytesRead)
}
fis.close()
val digest = md.digest()
val calculatedMd5 = digest.joinToString("") { "%02x".format(it) }
calculatedMd5.equals(expectedMd5, ignoreCase = true)
} catch (e: Exception) {
false
}
}
}
6.3 更新ViewModel
kotlin
@HiltViewModel
class UpdateViewModel @Inject constructor(
private val updateManager: UpdateManager
) : ViewModel() {
private val _updateState = MutableStateFlow<UpdateState>(UpdateState.Idle)
val updateState: StateFlow<UpdateState> = _updateState.asStateFlow()
private val _downloadProgress = MutableStateFlow(0)
val downloadProgress: StateFlow<Int> = _downloadProgress.asStateFlow()
fun checkForUpdates() {
viewModelScope.launch {
_updateState.value = UpdateState.Checking
val updateInfo = updateManager.checkForUpdates()
_updateState.value = if (updateInfo != null) {
UpdateState.Available(updateInfo)
} else {
UpdateState.NoUpdate
}
}
}
fun downloadAndInstall(updateInfo: UpdateInfo) {
viewModelScope.launch {
_updateState.value = UpdateState.Downloading
val apkFile = updateManager.downloadApk(updateInfo.apkUrl) { progress ->
_downloadProgress.value = progress
}
if (apkFile != null) {
if (updateManager.verifyMd5(apkFile, updateInfo.md5)) {
_updateState.value = UpdateState.Downloaded(apkFile)
updateManager.installApk(apkFile)
} else {
_updateState.value = UpdateState.Error("文件校验失败")
}
} else {
_updateState.value = UpdateState.Error("下载失败")
}
}
}
}
sealed class UpdateState {
object Idle : UpdateState()
object Checking : UpdateState()
object NoUpdate : UpdateState()
data class Available(val updateInfo: UpdateInfo) : UpdateState()
object Downloading : UpdateState()
data class Downloaded(val file: File) : UpdateState()
data class Error(val message: String) : UpdateState()
}
7. 相机拍照功能
7.1 CameraX实现
kotlin
class CameraManager(private val context: Context) {
private var imageCapture: ImageCapture? = null
private var imageAnalyzer: ImageAnalysis? = null
private var camera: Camera? = null
private var cameraProvider: ProcessCameraProvider? = null
// 初始化相机
fun startCamera(
lifecycleOwner: LifecycleOwner,
previewView: PreviewView,
analyzer: ImageAnalysis.Analyzer
) {
val cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener({
cameraProvider = cameraProviderFuture.get()
// 预览
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(previewView.surfaceProvider)
}
// 拍照
imageCapture = ImageCapture.Builder()
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
.setTargetRotation(previewView.display.rotation)
.build()
// 图像分析
imageAnalyzer = ImageAnalysis.Builder()
.setTargetResolution(Size(640, 640))
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.setOutputImageFormat(ImageAnalysis.OUTPUT_IMAGE_FORMAT_RGBA_8888)
.build()
.also {
it.setAnalyzer(ContextCompat.getMainExecutor(context), analyzer)
}
// 选择后置摄像头
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
try {
// 解绑所有用例
cameraProvider?.unbindAll()
// 绑定用例
camera = cameraProvider?.bindToLifecycle(
lifecycleOwner,
cameraSelector,
preview,
imageCapture,
imageAnalyzer
)
} catch (e: Exception) {
Log.e("CameraManager", "用例绑定失败", e)
}
}, ContextCompat.getMainExecutor(context))
}
// 拍照
fun takePhoto(onImageCaptured: (File) -> Unit, onError: (Exception) -> Unit) {
val imageCapture = imageCapture ?: return
val photoFile = File(
context.getExternalFilesDir(Environment.DIRECTORY_PICTURES),
"IMG_${System.currentTimeMillis()}.jpg"
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
imageCapture.takePicture(
outputOptions,
ContextCompat.getMainExecutor(context),
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
onImageCaptured(photoFile)
}
override fun onError(exception: ImageCaptureException) {
onError(exception)
}
}
)
}
// 停止相机
fun stopCamera() {
cameraProvider?.unbindAll()
}
// 切换闪光灯
fun toggleFlash() {
camera?.cameraControl?.enableTorch(
camera?.cameraInfo?.torchState?.value == TorchState.OFF
)
}
}
7.2 相机Activity
kotlin
@AndroidEntryPoint
class CameraActivity : AppCompatActivity() {
private lateinit var binding: ActivityCameraBinding
private lateinit var cameraManager: CameraManager
@Inject
lateinit var yoloDetector: YoloDetector
private var isRealTimeDetection = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityCameraBinding.inflate(layoutInflater)
setContentView(binding.root)
cameraManager = CameraManager(this)
// 请求权限
if (allPermissionsGranted()) {
startCamera()
} else {
requestPermissions()
}
setupUI()
}
private fun startCamera() {
cameraManager.startCamera(
lifecycleOwner = this,
previewView = binding.previewView,
analyzer = createImageAnalyzer()
)
}
private fun createImageAnalyzer() = ImageAnalysis.Analyzer { imageProxy ->
if (isRealTimeDetection) {
// 实时检测
val bitmap = imageProxy.toBitmap()
val results = yoloDetector.detect(bitmap)
runOnUiThread {
binding.overlayView.setDetectionResults(results)
}
}
imageProxy.close()
}
private fun setupUI() {
// 拍照按钮
binding.btnCapture.setOnClickListener {
cameraManager.takePhoto(
onImageCaptured = { file ->
processImage(file)
},
onError = { exception ->
Toast.makeText(this, "拍照失败: ${exception.message}",
Toast.LENGTH_SHORT).show()
}
)
}
// 实时检测开关
binding.switchRealTime.setOnCheckedChangeListener { _, isChecked ->
isRealTimeDetection = isChecked
}
// 闪光灯按钮
binding.btnFlash.setOnClickListener {
cameraManager.toggleFlash()
}
}
private fun processImage(file: File) {
lifecycleScope.launch {
val bitmap = BitmapFactory.decodeFile(file.absolutePath)
val results = yoloDetector.detect(bitmap)
// 显示结果
val intent = Intent(this@CameraActivity, ResultActivity::class.java)
intent.putExtra("image_path", file.absolutePath)
intent.putExtra("results", Gson().toJson(results))
startActivity(intent)
}
}
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
ContextCompat.checkSelfPermission(baseContext, it) ==
PackageManager.PERMISSION_GRANTED
}
private fun requestPermissions() {
ActivityCompat.requestPermissions(
this,
REQUIRED_PERMISSIONS,
REQUEST_CODE_PERMISSIONS
)
}
companion object {
private const val REQUEST_CODE_PERMISSIONS = 10
private val REQUIRED_PERMISSIONS = arrayOf(
Manifest.permission.CAMERA,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
}
}
8. 目标检测实现
8.1 YOLOv11检测器
kotlin
class YoloDetector(private val context: Context) {
private var interpreter: Interpreter? = null
private var labels: List<String> = emptyList()
private var inputSize = 640
private var confThreshold = 0.25f
private var iouThreshold = 0.45f
init {
loadModel()
loadLabels()
loadConfig()
}
private fun loadModel() {
try {
val modelFile = loadModelFile("yolov11.tflite")
val options = Interpreter.Options()
// 使用GPU加速(如果可用)
val gpuDelegate = GpuDelegate()
options.addDelegate(gpuDelegate)
// 或使用NNAPI
// options.setUseNNAPI(true)
// 设置线程数
options.setNumThreads(4)
interpreter = Interpreter(modelFile, options)
Log.d("YoloDetector", "模型加载成功")
} catch (e: Exception) {
Log.e("YoloDetector", "模型加载失败", e)
}
}
private fun loadModelFile(filename: String): ByteBuffer {
val assetFileDescriptor = context.assets.openFd(filename)
val inputStream = FileInputStream(assetFileDescriptor.fileDescriptor)
val fileChannel = inputStream.channel
val startOffset = assetFileDescriptor.startOffset
val declaredLength = assetFileDescriptor.declaredLength
return fileChannel.map(FileChannel.MapMode.READ_ONLY, startOffset, declaredLength)
}
private fun loadLabels() {
try {
labels = context.assets.open("labels.txt").bufferedReader().readLines()
} catch (e: Exception) {
Log.e("YoloDetector", "标签加载失败", e)
}
}
private fun loadConfig() {
try {
val json = context.assets.open("config.json").bufferedReader().readText()
val config = Gson().fromJson(json, ModelConfig::class.java)
inputSize = config.input_size
confThreshold = config.conf_threshold
iouThreshold = config.iou_threshold
} catch (e: Exception) {
Log.e("YoloDetector", "配置加载失败", e)
}
}
// 主检测方法
fun detect(bitmap: Bitmap): List<Detection> {
val startTime = System.currentTimeMillis()
// 1. 预处理
val inputBitmap = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true)
val inputBuffer = preprocessImage(inputBitmap)
// 2. 推理
val outputBuffer = runInference(inputBuffer)
// 3. 后处理
val detections = postprocess(outputBuffer, bitmap.width, bitmap.height)
val inferenceTime = System.currentTimeMillis() - startTime
Log.d("YoloDetector", "检测耗时: ${inferenceTime}ms, 检测到: ${detections.size}个对象")
return detections
}
private fun preprocessImage(bitmap: Bitmap): ByteBuffer {
val inputBuffer = ByteBuffer.allocateDirect(4 * inputSize * inputSize * 3)
inputBuffer.order(ByteOrder.nativeOrder())
val intValues = IntArray(inputSize * inputSize)
bitmap.getPixels(intValues, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
var pixel = 0
for (i in 0 until inputSize) {
for (j in 0 until inputSize) {
val value = intValues[pixel++]
// 归一化到[0, 1]
inputBuffer.putFloat(((value shr 16) and 0xFF) / 255.0f) // R
inputBuffer.putFloat(((value shr 8) and 0xFF) / 255.0f) // G
inputBuffer.putFloat((value and 0xFF) / 255.0f) // B
}
}
return inputBuffer
}
private fun runInference(inputBuffer: ByteBuffer): Array<FloatArray> {
// YOLOv11输出形状: [1, 84, 8400] (80类 + 4个框坐标)
val outputShape = intArrayOf(1, 84, 8400)
val outputBuffer = Array(outputShape[0]) {
Array(outputShape[1]) {
FloatArray(outputShape[2])
}
}
interpreter?.run(inputBuffer, outputBuffer)
return outputBuffer[0]
}
private fun postprocess(
output: Array<FloatArray>,
originalWidth: Int,
originalHeight: Int
): List<Detection> {
val detections = mutableListOf<Detection>()
// output shape: [84, 8400]
// 前4行是边界框坐标 (x, y, w, h)
// 后80行是类别置信度
for (i in 0 until output[0].size) {
// 获取最大置信度类别
var maxConf = 0f
var maxClassIdx = 0
for (classIdx in 4 until output.size) {
val conf = output[classIdx][i]
if (conf > maxConf) {
maxConf = conf
maxClassIdx = classIdx - 4
}
}
// 过滤低置信度
if (maxConf < confThreshold) continue
// 解析边界框
val cx = output[0][i] * originalWidth / inputSize
val cy = output[1][i] * originalHeight / inputSize
val w = output[2][i] * originalWidth / inputSize
val h = output[3][i] * originalHeight / inputSize
val left = cx - w / 2
val top = cy - h / 2
val right = cx + w / 2
val bottom = cy + h / 2
detections.add(
Detection(
classIndex = maxClassIdx,
className = labels.getOrNull(maxClassIdx) ?: "Unknown",
confidence = maxConf,
bbox = RectF(left, top, right, bottom)
)
)
}
// NMS非极大值抑制
return nms(detections, iouThreshold)
}
private fun nms(detections: List<Detection>, iouThreshold: Float): List<Detection> {
val sortedDetections = detections.sortedByDescending { it.confidence }
val selected = mutableListOf<Detection>()
for (detection in sortedDetections) {
var shouldSelect = true
for (selectedDetection in selected) {
if (detection.classIndex == selectedDetection.classIndex) {
val iou = calculateIoU(detection.bbox, selectedDetection.bbox)
if (iou > iouThreshold) {
shouldSelect = false
break
}
}
}
if (shouldSelect) {
selected.add(detection)
}
}
return selected
}
private fun calculateIoU(box1: RectF, box2: RectF): Float {
val intersectionLeft = maxOf(box1.left, box2.left)
val intersectionTop = maxOf(box1.top, box2.top)
val intersectionRight = minOf(box1.right, box2.right)
val intersectionBottom = minOf(box1.bottom, box2.bottom)
val intersectionWidth = maxOf(0f, intersectionRight - intersectionLeft)
val intersectionHeight = maxOf(0f, intersectionBottom - intersectionTop)
val intersectionArea = intersectionWidth * intersectionHeight
val box1Area = (box1.right - box1.left) * (box1.bottom - box1.top)
val box2Area = (box2.right - box2.left) * (box2.bottom - box2.top)
val unionArea = box1Area + box2Area - intersectionArea
return if (unionArea > 0) intersectionArea / unionArea else 0f
}
fun close() {
interpreter?.close()
interpreter = null
}
}
// 检测结果数据类
data class Detection(
val classIndex: Int,
val className: String,
val confidence: Float,
val bbox: RectF
)
data class ModelConfig(
val input_size: Int,
val conf_threshold: Float,
val iou_threshold: Float
)
8.2 结果可视化View
kotlin
class DetectionOverlayView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : View(context, attrs, defStyleAttr) {
private val boxPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = 5f
color = Color.GREEN
}
private val textPaint = Paint().apply {
style = Paint.Style.FILL
color = Color.WHITE
textSize = 40f
typeface = Typeface.DEFAULT_BOLD
}
private val backgroundPaint = Paint().apply {
style = Paint.Style.FILL
color = Color.argb(180, 0, 255, 0)
}
private var detections: List<Detection> = emptyList()
fun setDetectionResults(results: List<Detection>) {
detections = results
invalidate()
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
for (detection in detections) {
// 绘制边界框
canvas.drawRect(detection.bbox, boxPaint)
// 绘制标签背景
val label = "${detection.className} ${(detection.confidence * 100).toInt()}%"
val textBounds = Rect()
textPaint.getTextBounds(label, 0, label.length, textBounds)
val labelLeft = detection.bbox.left
val labelTop = detection.bbox.top - textBounds.height() - 10
val labelRight = labelLeft + textBounds.width() + 20
val labelBottom = detection.bbox.top
canvas.drawRect(labelLeft, labelTop, labelRight, labelBottom, backgroundPaint)
// 绘制标签文本
canvas.drawText(label, labelLeft + 10, labelBottom - 5, textPaint)
}
}
}
9. MES系统对接
9.1 MES API接口定义
kotlin
interface MesApi {
// 上传检测结果
@Multipart
@POST("api/mes/detection/upload")
suspend fun uploadDetectionResult(
@Part("orderId") orderId: RequestBody,
@Part("stationId") stationId: RequestBody,
@Part("operatorId") operatorId: RequestBody,
@Part("timestamp") timestamp: RequestBody,
@Part("detectionData") detectionData: RequestBody,
@Part image: MultipartBody.Part
): Response<MesResponse>
// 获取工单信息
@GET("api/mes/order/{orderId}")
suspend fun getOrderInfo(
@Path("orderId") orderId: String
): Response<OrderInfo>
// 批量上传
@POST("api/mes/detection/batch")
suspend fun batchUpload(
@Body results: List<DetectionUploadData>
): Response<MesResponse>
}
// 数据模型
data class MesResponse(
val code: Int,
val message: String,
val data: Any?
)
data class OrderInfo(
val orderId: String,
val productCode: String,
val productName: String,
val quantity: Int,
val status: String
)
data class DetectionUploadData(
val orderId: String,
val stationId: String,
val operatorId: String,
val timestamp: Long,
val imagePath: String,
val detections: List<DetectionData>
)
data class DetectionData(
val className: String,
val confidence: Float,
val x: Float,
val y: Float,
val width: Float,
val height: Float
)
9.2 MES同步服务
kotlin
@HiltViewModel
class MesSyncViewModel @Inject constructor(
private val mesApi: MesApi,
private val detectionDao: DetectionDao
) : ViewModel() {
private val _syncState = MutableStateFlow<SyncState>(SyncState.Idle)
val syncState: StateFlow<SyncState> = _syncState.asStateFlow()
// 上传单个检测结果
fun uploadResult(
result: DetectionResult,
orderId: String,
stationId: String,
operatorId: String
) {
viewModelScope.launch {
_syncState.value = SyncState.Uploading
try {
val imageFile = File(result.imagePath)
val imagePart = MultipartBody.Part.createFormData(
"image",
imageFile.name,
imageFile.asRequestBody("image/jpeg".toMediaTypeOrNull())
)
val detectionData = Gson().toJson(
mapOf(
"detections" to Gson().fromJson(
result.detections,
Array<Detection>::class.java
).map {
DetectionData(
className = it.className,
confidence = it.confidence,
x = it.bbox.left,
y = it.bbox.top,
width = it.bbox.width(),
height = it.bbox.height()
)
}
)
)
val response = mesApi.uploadDetectionResult(
orderId = orderId.toRequestBody(),
stationId = stationId.toRequestBody(),
operatorId = operatorId.toRequestBody(),
timestamp = result.timestamp.toString().toRequestBody(),
detectionData = detectionData.toRequestBody(),
image = imagePart
)
if (response.isSuccessful && response.body()?.code == 200) {
// 标记为已同步
detectionDao.markAsSynced(result.id)
_syncState.value = SyncState.Success("上传成功")
} else {
_syncState.value = SyncState.Error("上传失败: ${response.body()?.message}")
}
} catch (e: Exception) {
_syncState.value = SyncState.Error("上传异常: ${e.message}")
Log.e("MesSyncViewModel", "上传失败", e)
}
}
}
// 批量同步未上传的结果
fun syncUnsyncedResults() {
viewModelScope.launch {
detectionDao.getUnsyncedResults().collect { unsyncedList ->
if (unsyncedList.isEmpty()) {
_syncState.value = SyncState.Success("没有待同步数据")
return@collect
}
_syncState.value = SyncState.Uploading
try {
val uploadDataList = unsyncedList.map { result ->
DetectionUploadData(
orderId = result.mesOrderId ?: "",
stationId = "STATION_001", // 从配置获取
operatorId = "OPERATOR_001", // 从配置获取
timestamp = result.timestamp,
imagePath = result.imagePath,
detections = Gson().fromJson(
result.detections,
Array<Detection>::class.java
).map {
DetectionData(
className = it.className,
confidence = it.confidence,
x = it.bbox.left,
y = it.bbox.top,
width = it.bbox.width(),
height = it.bbox.height()
)
}
)
}
val response = mesApi.batchUpload(uploadDataList)
if (response.isSuccessful && response.body()?.code == 200) {
// 标记所有为已同步
unsyncedList.forEach { result ->
detectionDao.markAsSynced(result.id)
}
_syncState.value = SyncState.Success("批量同步成功")
} else {
_syncState.value = SyncState.Error("批量同步失败")
}
} catch (e: Exception) {
_syncState.value = SyncState.Error("同步异常: ${e.message}")
}
}
}
}
private fun String.toRequestBody(): RequestBody {
return this.toRequestBody("text/plain".toMediaTypeOrNull())
}
}
sealed class SyncState {
object Idle : SyncState()
object Uploading : SyncState()
data class Success(val message: String) : SyncState()
data class Error(val message: String) : SyncState()
}
9.3 后台同步Service
kotlin
class SyncService : Service() {
@Inject
lateinit var mesApi: MesApi
@Inject
lateinit var detectionDao: DetectionDao
private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
override fun onBind(intent: Intent?): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// 创建前台通知
createNotificationChannel()
val notification = createNotification()
startForeground(NOTIFICATION_ID, notification)
// 开始同步
startSync()
return START_STICKY
}
private fun startSync() {
serviceScope.launch {
detectionDao.getUnsyncedResults().collect { unsyncedList ->
if (unsyncedList.isNotEmpty()) {
syncResults(unsyncedList)
}
delay(60000) // 每分钟检查一次
}
}
}
private suspend fun syncResults(results: List<DetectionResult>) {
for (result in results) {
try {
// 上传逻辑(简化版)
val uploadData = DetectionUploadData(
orderId = result.mesOrderId ?: "",
stationId = "STATION_001",
operatorId = "OPERATOR_001",
timestamp = result.timestamp,
imagePath = result.imagePath,
detections = emptyList() // 实际应解析
)
val response = mesApi.batchUpload(listOf(uploadData))
if (response.isSuccessful) {
detectionDao.markAsSynced(result.id)
}
} catch (e: Exception) {
Log.e("SyncService", "同步失败", e)
}
}
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
CHANNEL_ID,
"数据同步",
NotificationManager.IMPORTANCE_LOW
)
val notificationManager = getSystemService(NotificationManager::class.java)
notificationManager.createNotificationChannel(channel)
}
}
private fun createNotification(): Notification {
return NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("数据同步中")
.setContentText("正在后台同步检测结果到MES系统")
.setSmallIcon(R.drawable.ic_sync)
.setPriority(NotificationCompat.PRIORITY_LOW)
.build()
}
override fun onDestroy() {
super.onDestroy()
serviceScope.cancel()
}
companion object {
private const val CHANNEL_ID = "sync_channel"
private const val NOTIFICATION_ID = 1001
}
}
10. 性能优化
10.1 模型优化
kotlin
// 1. 使用GPU加速
class OptimizedYoloDetector(context: Context) : YoloDetector(context) {
override fun loadModel() {
val options = Interpreter.Options()
// GPU委托
val gpuDelegate = GpuDelegate(
GpuDelegate.Options().apply {
setPrecisionLossAllowed(true) // 允许精度损失以提升速度
setInferencePreference(GpuDelegate.Options.INFERENCE_PREFERENCE_FAST_SINGLE_ANSWER)
}
)
options.addDelegate(gpuDelegate)
// XNNPACK委托(CPU优化)
val xnnpackDelegate = XNNPackDelegate()
options.addDelegate(xnnpackDelegate)
interpreter = Interpreter(modelFile, options)
}
}
// 2. 图像预处理优化
class FastPreprocessor {
private val intValues = IntArray(640 * 640)
private val floatValues = FloatArray(640 * 640 * 3)
fun preprocess(bitmap: Bitmap): ByteBuffer {
// 使用对象池复用ByteBuffer
val buffer = ByteBuffer.allocateDirect(4 * 640 * 640 * 3)
buffer.order(ByteOrder.nativeOrder())
bitmap.getPixels(intValues, 0, bitmap.width, 0, 0, bitmap.width, bitmap.height)
// 并行处理
(0 until 640 * 640).toList().parallelStream().forEach { i ->
val pixelValue = intValues[i]
floatValues[i * 3] = ((pixelValue shr 16) and 0xFF) / 255.0f
floatValues[i * 3 + 1] = ((pixelValue shr 8) and 0xFF) / 255.0f
floatValues[i * 3 + 2] = (pixelValue and 0xFF) / 255.0f
}
buffer.asFloatBuffer().put(floatValues)
return buffer
}
}
10.2 内存优化
kotlin
class MemoryManager {
// Bitmap对象池
private val bitmapPool = object : LruCache<String, Bitmap>(20) {
override fun sizeOf(key: String, value: Bitmap): Int {
return value.byteCount / 1024
}
override fun entryRemoved(
evicted: Boolean,
key: String,
oldValue: Bitmap,
newValue: Bitmap?
) {
if (!oldValue.isRecycled) {
oldValue.recycle()
}
}
}
fun getBitmap(key: String, width: Int, height: Int): Bitmap {
return bitmapPool.get(key) ?: Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
.also { bitmapPool.put(key, it) }
}
// 定期清理
fun trimMemory() {
bitmapPool.trimToSize(bitmapPool.size() / 2)
System.gc()
}
}
10.3 网络优化
kotlin
class OptimizedNetworkModule {
@Provides
@Singleton
fun provideOkHttpClient(): OkHttpClient {
return OkHttpClient.Builder()
// 连接池
.connectionPool(ConnectionPool(5, 5, TimeUnit.MINUTES))
// 缓存
.cache(Cache(File(context.cacheDir, "http_cache"), 50L * 1024 * 1024))
// 重试
.retryOnConnectionFailure(true)
// 超时
.connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
// 拦截器
.addInterceptor(CacheInterceptor())
.addInterceptor(RetryInterceptor(3))
.build()
}
}
// 缓存拦截器
class CacheInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val request = chain.request()
val response = chain.proceed(request)
return response.newBuilder()
.header("Cache-Control", "public, max-age=60")
.build()
}
}
// 重试拦截器
class RetryInterceptor(private val maxRetry: Int) : Interceptor {
override fun intercept(chain: Interceptor.Chain): okhttp3.Response {
val request = chain.request()
var response = chain.proceed(request)
var tryCount = 0
while (!response.isSuccessful && tryCount < maxRetry) {
tryCount++
Thread.sleep(1000 * tryCount.toLong())
response.close()
response = chain.proceed(request)
}
return response
}
}
11. 部署与测试
11.1 打包配置
gradle
android {
signingConfigs {
release {
storeFile file('../keystore/release.jks')
storePassword 'your_store_password'
keyAlias 'your_key_alias'
keyPassword 'your_key_password'
}
}
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
// 多渠道打包
flavorDimensions "version"
productFlavors {
production {
dimension "version"
buildConfigField "String", "BASE_URL", '"https://api.production.com"'
}
staging {
dimension "version"
buildConfigField "String", "BASE_URL", '"https://api.staging.com"'
}
}
}
11.2 ProGuard规则
proguard
# proguard-rules.pro
# 保留TensorFlow Lite
-keep class org.tensorflow.** { *; }
-keep interface org.tensorflow.** { *; }
# 保留ONNX Runtime
-keep class ai.onnxruntime.** { *; }
# 保留数据类
-keep class com.example.yolodetector.data.** { *; }
-keep class com.example.yolodetector.domain.** { *; }
# 保留Retrofit
-keepattributes Signature
-keepattributes *Annotation*
-keep class retrofit2.** { *; }
# 保留Gson
-keep class com.google.gson.** { *; }
-keep class * implements com.google.gson.TypeAdapter
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
11.3 单元测试
kotlin
@RunWith(AndroidJUnit4::class)
class YoloDetectorTest {
private lateinit var detector: YoloDetector
private lateinit var context: Context
@Before
fun setup() {
context = ApplicationProvider.getApplicationContext()
detector = YoloDetector(context)
}
@Test
fun testModelLoading() {
assertNotNull(detector)
}
@Test
fun testDetection() {
// 创建测试图片
val testBitmap = Bitmap.createBitmap(640, 640, Bitmap.Config.ARGB_8888)
val canvas = Canvas(testBitmap)
canvas.drawColor(Color.WHITE)
// 执行检测
val results = detector.detect(testBitmap)
assertNotNull(results)
assertTrue(results is List)
}
@Test
fun testNMS() {
val detections = listOf(
Detection(0, "person", 0.9f, RectF(10f, 10f, 100f, 100f)),
Detection(0, "person", 0.8f, RectF(15f, 15f, 105f, 105f)),
Detection(1, "car", 0.7f, RectF(200f, 200f, 300f, 300f))
)
// 测试NMS逻辑
// ...
}
@After
fun tearDown() {
detector.close()
}
}
11.4 性能测试
kotlin
@RunWith(AndroidJUnit4::class)
@LargeTest
class PerformanceTest {
@Test
fun testInferenceTime() {
val detector = YoloDetector(ApplicationProvider.getApplicationContext())
val testBitmap = BitmapFactory.decodeResource(
ApplicationProvider.getApplicationContext<Context>().resources,
R.drawable.test_image
)
val times = mutableListOf<Long>()
// 预热
repeat(5) {
detector.detect(testBitmap)
}
// 测试
repeat(100) {
val start = System.currentTimeMillis()
detector.detect(testBitmap)
val end = System.currentTimeMillis()
times.add(end - start)
}
val avgTime = times.average()
val maxTime = times.maxOrNull() ?: 0L
val minTime = times.minOrNull() ?: 0L
Log.d("PerformanceTest", "平均推理时间: ${avgTime}ms")
Log.d("PerformanceTest", "最大推理时间: ${maxTime}ms")
Log.d("PerformanceTest", "最小推理时间: ${minTime}ms")
// 断言平均时间应该在合理范围内
assertTrue("推理时间过长", avgTime < 500)
}
}
12. 常见问题与解决方案
12.1 模型加载问题
问题: 模型加载失败或内存溢出
解决方案:
kotlin
// 1. 使用模型量化
// 2. 延迟加载
class LazyYoloDetector(private val context: Context) {
private val detector: YoloDetector by lazy {
YoloDetector(context)
}
fun detect(bitmap: Bitmap) = detector.detect(bitmap)
}
// 3. 检查模型文件完整性
fun verifyModelFile(context: Context): Boolean {
return try {
val fd = context.assets.openFd("yolov11.tflite")
fd.length > 0
} catch (e: Exception) {
false
}
}
12.2 相机权限问题
问题: Android 10+存储权限变化
解决方案:
kotlin
// 使用Scoped Storage
fun saveImageToGallery(context: Context, bitmap: Bitmap) {
val contentValues = ContentValues().apply {
put(MediaStore.Images.Media.DISPLAY_NAME, "IMG_${System.currentTimeMillis()}.jpg")
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val uri = context.contentResolver.insert(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
contentValues
)
uri?.let {
context.contentResolver.openOutputStream(it)?.use { outputStream ->
bitmap.compress(Bitmap.CompressFormat.JPEG, 95, outputStream)
}
}
}
12.3 网络连接问题
问题: MES系统连接不稳定
解决方案:
kotlin
// 实现离线队列
class OfflineQueueManager(
private val dao: DetectionDao,
private val workManager: WorkManager
) {
fun enqueueUpload(result: DetectionResult) {
val uploadWork = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.MIN_BACKOFF_MILLIS,
TimeUnit.MILLISECONDS
)
.setInputData(workDataOf("result_id" to result.id))
.build()
workManager.enqueue(uploadWork)
}
}
class UploadWorker(
context: Context,
params: WorkerParameters
) : CoroutineWorker(context, params) {
override suspend fun doWork(): Result {
val resultId = inputData.getLong("result_id", -1)
// 上传逻辑
return try {
// upload...
Result.success()
} catch (e: Exception) {
Result.retry()
}
}
}
12.4 性能优化建议
kotlin
// 1. 使用协程优化
class OptimizedDetectionViewModel @Inject constructor(
private val detector: YoloDetector
) : ViewModel() {
private val detectionScope = CoroutineScope(
Dispatchers.Default + SupervisorJob()
)
fun detectAsync(bitmap: Bitmap, callback: (List<Detection>) -> Unit) {
detectionScope.launch {
val results = detector.detect(bitmap)
withContext(Dispatchers.Main) {
callback(results)
}
}
}
}
// 2. 图像降采样
fun downscaleImage(bitmap: Bitmap, maxSize: Int): Bitmap {
val ratio = maxSize.toFloat() / maxOf(bitmap.width, bitmap.height)
if (ratio >= 1) return bitmap
val newWidth = (bitmap.width * ratio).toInt()
val newHeight = (bitmap.height * ratio).toInt()
return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
}
// 3. 使用RenderScript加速(已弃用,改用Vulkan或GPU Compute)
总结
本文档详细介绍了基于YOLOv11的安卓目标检测应用的完整开发流程,涵盖:
✅ 架构设计 : MVVM架构 + 模块化设计
✅ 模型部署 : TensorFlow Lite/ONNX Runtime集成
✅ 核心功能 : 相机、检测、更新、MES对接
✅ 性能优化 : GPU加速、内存管理、网络优化
✅ 生产就绪: 完整的错误处理和离线支持
下一步建议
- 安全加固: 添加证书绑定、API加密
- 数据分析: 集成统计分析功能
- 多模型支持: 支持动态切换不同检测模型
- 云端训练: 实现模型在线训练和更新
- 跨平台: 考虑Flutter/React Native实现