十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

YOLOv11安卓端部署实战:从模型训练到移动端实时火焰烟雾检测

YOLOv11安卓端部署实战:从模型训练到移动端实时火焰烟雾检测 简介本资源是一套面向高校学生与开发者的目标检测移动端落地实践方案聚焦YOLO11模型在Android平台的端侧部署解决从模型训练、转换、集成到APP实时推理的全流程技术难点适用于毕设、课设、竞赛及实训项目。压缩包含2000个文件总大小338.97MB涵盖Android Studio工程Java/KotlinNDK调用、PyCharm训练工程PythonPyTorch、C/C底层推理代码.c/.cpp/.h/.hpp、配置文件.json/.yaml/.xml、数据标注.txt及说明文档.md/.properties结构清晰、模块解耦便于按需复用与二次开发。已有1029人学习下载配套详细设计文档与实操演示视频含50轮训练效果展示手把手指导环境搭建、JNI接口封装、TensorFlow Lite模型转换及APP界面交互实现所有步骤经实战验证无误支持快速替换为车牌、人脸、手势等其他目标检测任务。1. 项目概述从模型到指尖的跨越最近在社区里看到不少同学在讨论如何把训练好的YOLO模型塞进安卓手机里做成一个能实时跑起来的APP。这确实是个挺有意思的课题无论是做毕设、参加竞赛还是搞个小项目练手都很有价值。毕竟能让算法在移动端实时运行才算真正把AI“落地”了。我自己也折腾过好几次从早期的YOLOv5到现在的YOLOv11踩过不少坑也积累了一些还算靠谱的经验。这次我们就以“火焰烟雾检测”这个具体的场景为例手把手走一遍完整的流程。为什么选这个例子一来它在安防、森林防火等场景下有明确的应用价值不是个“玩具”demo二来它的实现过程具有通用性你完全可以把模型换成检测猫狗、车牌、或者某种特定工业零件的整个流程的骨架是一样的。我们的目标很明确把一个在电脑上训练好的YOLOv11模型经过必要的处理和优化最终部署到一个安卓APP中实现通过手机摄像头进行实时目标检测。整个过程会涉及到几个关键环节模型训练与导出、模型格式转换与优化、安卓开发环境搭建、以及最终的APP集成与调试。听起来步骤不少但别担心我会把每个环节的“为什么”和“怎么做”都讲清楚特别是那些官方文档里可能不会提但实际做起来又绕不开的细节。2. 核心思路与方案选型在开始动手之前我们得先想清楚要走哪条路。把YOLO部署到安卓上主流方案有好几种每种都有它的适用场景和优缺点。2.1 部署路径的权衡最常见的有这么几条路使用NCNN、MNN等移动端推理框架这是目前社区里最流行、资源也最丰富的方案。这些框架由国内大厂开源对安卓平台和ARM芯片做了深度优化性能通常很不错。你需要把PyTorch训练好的模型先转换成ONNX格式然后再用这些框架的工具转换成其私有格式如NCNN的.param和.bin文件。优点是成熟、稳定、社区活跃缺点是多了一层转换有时候会遇到算子不支持或者精度损失的问题。使用PyTorch Mobile或TensorFlow Lite (TFLite)这是“官方”或“半官方”的路径。PyTorch提供了直接转换到TorchScript然后用于移动端的方案而如果你用TensorFlow训练YOLO例如使用TF官方的实现则可以很方便地转换成TFLite格式。这条路的优点是生态整合好如果是简单的模型转换比较顺畅。但缺点是对YOLO这种结构比较复杂的模型PyTorch Mobile的优化可能不如专用框架而TFLite路径则需要你一开始就在TensorFlow生态下。使用OpenCV的DNN模块OpenCV从4.x版本开始强化了DNN模块对ONNX模型的支持。你可以直接将YOLO模型导出为ONNX然后在安卓上使用OpenCV库来加载和推理。这条路的优点是依赖相对单一主要就是OpenCV且ONNX作为中间格式通用性强。缺点是OpenCV DNN在安卓端的性能优化可能不如NCNN等专用框架对于需要高帧率的实时检测可能有点吃力。我的选择与理由经过多次实践我倾向于选择方案一NCNN。原因很简单它在安卓端的推理速度经过充分验证社区庞大遇到问题容易找到解决方案。特别是对于YOLO系列模型NCNN的支持一直很及时相关的转换工具和示例代码也最全。我们本次的实践就将以NCNN为核心来展开。这并不意味着其他方案不好而是这条路径对于大多数想要快速实现、追求性能的开发者来说踩坑的概率相对较小。2.2 技术栈确认基于以上选择我们最终的技术栈如下训练框架PyTorch (Ultralytics YOLOv11)。这是目前最活跃的YOLO维护版本API友好生态完善。中间格式ONNX。作为PyTorch到NCNN之间的桥梁它是一种开放的模型表示格式。移动端推理引擎NCNN。负责在安卓设备上高效执行模型计算。安卓开发Android Studio (Java/Kotlin) NCNN Android SDK。我们主要用Java进行演示原理与Kotlin相通。目标检测任务火焰与烟雾检测。我们将准备一个小的自定义数据集进行训练以演示完整流程。这个组合拳打下来基本上能覆盖从研发到部署的全链路并且每个环节都有比较可靠的工具支持。3. 模型训练与导出打造专属检测器部署的起点是一个训练好的、性能良好的模型。我们不可能直接用官方的预训练权重因为它不认识“火焰”和“烟雾”。所以第一步是进行自定义训练。3.1 数据准备与标注数据是模型的基石。对于火焰烟雾检测你可以从公开数据集如FireSmoke-Dataset中获取一部分也可以自己从网络上收集相关图片和视频并通过抽帧的方式获取图像。这里的关键是质量而非单纯的数量确保图像中火焰和烟雾的特征清晰、多样不同场景、大小、亮度。标注工具推荐使用labelImg或Roboflow。标注时将火焰和烟雾视为两个独立的类别例如fire和smoke。标注文件采用YOLO格式即每个图像对应一个.txt文件每行内容为class_id x_center y_center width height坐标和宽高都是相对于图像尺寸的归一化值。数据准备好后按照如下结构组织datasets/ └── fire_smoke/ ├── train/ │ ├── images/ (存放训练图片) │ └── labels/ (存放对应的txt标注文件) └── val/ ├── images/ └── labels/3.2 YOLOv11模型训练我们使用Ultralytics框架进行训练这是目前最便捷的方式。# 安装ultralytics pip install ultralytics # 准备数据集配置文件 fire_smoke.yaml # 内容如下 # path: /path/to/your/datasets/fire_smoke # 数据集根目录 # train: train/images # val: val/images # nc: 2 # 类别数这里是火焰和烟雾 # names: [fire, smoke] # 类别名称然后在Python中或通过命令行启动训练from ultralytics import YOLO # 加载一个预训练模型作为起点这里以YOLOv11n为例 model YOLO(yolo11n.pt) # 开始训练 results model.train( datafire_smoke.yaml, epochs100, imgsz640, batch16, device0, # 使用GPU如果是CPU则设为cpu workers4, projectfire_smoke_detection, nameexp1 )训练过程会输出损失曲线、精度指标等。你需要关注mAP50-95这个值它综合反映了模型在不同IoU阈值下的平均精度。对于火焰烟雾这种任务由于目标形态多变能达到0.5以上的mAP通常就算不错了。训练完成后最好的模型权重会保存在runs/detect/exp1/weights/best.pt。实操心得训练参数调优imgsz图像尺寸部署到手机考虑到性能通常不会使用很大的分辨率。训练时就用640是合理的兼顾速度和精度。batch根据你的GPU显存调整。如果出现CUDA out of memory错误就减小batch size或imgsz。数据增强YOLO默认会启用一些数据增强如 mosaic, mixup。对于小数据集这是防止过拟合的关键。除非你有特殊理由否则不要轻易关闭。早停Early StoppingUltralytics内置了早停机制。如果验证集指标长时间不提升训练会自动停止避免过拟合。3.3 模型导出为ONNX训练得到.pt文件后我们需要将其转换为ONNX格式这是通向NCNN的必经之路。from ultralytics import YOLO # 加载训练好的模型 model YOLO(runs/detect/exp1/weights/best.pt) # 导出模型指定格式为ONNX success model.export(formatonnx, imgsz640, simplifyTrue, opset12)关键参数解释formatonnx指定导出格式。imgsz640指定模型输入的固定尺寸。这对于移动端部署至关重要固定的输入尺寸可以简化预处理和优化。simplifyTrue启用ONNX简化器会优化计算图结构移除不必要的操作有时能提升推理速度并减少转换出错概率。强烈建议开启。opset12指定ONNX算子集版本。版本不宜过低可能缺少新算子也不宜过高NCNN等后端可能还未完全支持。opset 12是一个比较通用和稳定的选择。导出成功后你会得到一个best.onnx文件。务必使用Netron一个可视化工具打开这个.onnx文件检查一下。主要看两点1. 输入节点的名字和形状通常是images: [1, 3, 640, 640]2. 输出节点的名字和形状。记下这些名字后面在NCNN和安卓端写代码时会用到。4. 模型转换与优化为移动端瘦身拿到ONNX模型后还不能直接给NCNN用需要转换成NCNN专用的格式。同时我们还要考虑模型优化以便在手机有限的算力下跑得更快。4.1 搭建NCNN转换环境NCNN提供了完整的工具链。首先我们需要编译生成模型转换工具onnx2ncnn。# 1. 克隆ncnn仓库 git clone https://github.com/tencent/ncnn.git cd ncnn # 2. 创建构建目录并编译 (以Ubuntu为例其他系统请参考官方文档) mkdir -p build cd build cmake -DCMAKE_BUILD_TYPERelease -DNCNN_VULKANOFF -DNCNN_SYSTEM_GLSLANGOFF -DNCNN_BUILD_EXAMPLESON .. make -j$(nproc) # 编译完成后在 build/tools/onnx 目录下会生成 onnx2ncnn 可执行文件 # 为了方便可以将其复制到系统路径或项目目录 cp tools/onnx/onnx2ncnn /usr/local/bin/ # 或你的自定义路径4.2 执行模型转换转换命令非常简单onnx2ncnn best.onnx best.param best.bin这将会生成两个文件best.param: 模型结构文件文本格式描述了网络层和连接关系。best.bin: 模型权重文件二进制格式包含了所有可训练参数。4.3 模型优化与调试直接转换得到的模型可能不是最优的甚至可能包含一些NCNN不支持的算子。因此优化和调试是必不可少的步骤。优化1模型剪枝与量化可选但推荐对于追求极致性能的场景可以对模型进行INT8量化。量化能将模型权重和激活值从32位浮点数FP32转换为8位整数INT8从而大幅减少模型体积和内存占用并利用芯片的整数计算单元加速。NCNN提供了ncnnoptimize工具来进行量化。# 首先确保编译时开启了NCNN_INT8选项并编译了ncnnoptimize工具 # 在之前的cmake命令中加入 -DNCNN_INT8ON # 重新编译后在 build/tools/ 下会有 ncnnoptimize # 执行量化 (需要准备一个校准数据集这里用训练集图片举例) ncnnoptimize best.param best.bin best_int8.param best_int8.bin 256 [校准数据集路径列表文件]量化需要一小部分代表性数据校准集来统计激活值的分布。量化后的模型速度可能有显著提升但精度会有轻微损失需要评估是否可接受。对于火焰烟雾检测如果精度下降不明显强烈建议量化。优化2融合与重参数化NCNN的转换工具会自动尝试进行一些算子融合优化如Conv-BN-ReLU融合。你可以通过查看转换时的输出信息或使用ncnnoptimize工具不量化只做优化来进一步优化图结构。ncnnoptimize best.param best.bin best_opt.param best_opt.bin 0参数0表示不进行量化只做图优化。调试处理不支持的算子如果转换失败或转换后推理出错很可能是遇到了不支持的ONNX算子。此时需要用Netron仔细查看ONNX模型中出错的算子类型。查阅NCNN的 算子支持列表 。如果确实不支持可以考虑修改模型结构例如用一组支持的算子来等效实现或者寻找是否有其他转换路径例如先转到其他框架再转NCNN。对于YOLOv11其主流算子通常都已得到良好支持。注意事项模型输入输出名转换后务必打开best.param文件查看开头的几行。你会看到类似Input input_name 0 1 images这样的内容这里的images就是NCNN模型中的输入节点名。同样在文件末尾附近找到输出节点名。这些名字可能与ONNX中的名字不同在后续编写安卓代码时必须使用best.param里定义的输入输出名。5. 安卓开发环境与NCNN集成模型准备好了接下来就是打造承载它的安卓APP。我们从一个简单的摄像头预览应用开始逐步集成检测功能。5.1 创建安卓项目与基础配置打开Android Studio新建一个Empty Views Activity项目语言选择Java。在app/build.gradle文件中添加NCNN库的依赖。最方便的方式是使用Maven Central的预编译包。android { ... // 确保设置了这些配置以支持NCNN的C代码 defaultConfig { ... externalNativeBuild { cmake { cppFlags -stdc11 // 根据你的手机架构添加通常armeabi-v7a和arm64-v8a是必须的 abiFilters armeabi-v7a, arm64-v8a } } ndk { abiFilters armeabi-v7a, arm64-v8a } } ... } dependencies { ... // 添加NCNN依赖 implementation com.tencent.ncnn:ncnn-android:2024.11.10 // 请使用最新版本 }同步Gradle项目。5.2 准备模型文件与C推理代码放置模型文件在app/src/main/目录下新建一个名为assets的文件夹如果没有的话。将我们转换优化好的best.param和best.bin文件复制进去。Android Studio会自动将这个目录下的文件打包进APK。编写C推理代码在app/src/main/cpp目录下如果没有则创建创建yolov11.cpp和yolov11.h。在app/src/main/cpp下创建CMakeLists.txt文件用于编译C代码。yolov11.h头文件定义接口#ifndef YOLOV11_H #define YOLOV11_H #include android/bitmap.h #include jni.h #include opencv2/core/core.hpp #include opencv2/imgproc/imgproc.hpp #include net.h // NCNN的头文件 class YoloV11 { public: YoloV11(); ~YoloV11(); bool loadModel(AAssetManager* mgr); // 从assets加载模型 std::vectorDetection detect(cv::Mat rgb); // 执行检测 private: ncnn::Net net; int input_size 640; // 与训练导出时保持一致 float score_threshold 0.25f; float nms_threshold 0.45f; // 内部预处理、后处理函数 cv::Mat preprocess(cv::Mat rgb); std::vectorDetection decode_outputs(ncnn::Mat output); void nms(std::vectorDetection detections); }; // 定义一个检测结果结构体 struct Detection { cv::Rect bbox; float score; int class_id; }; #endif //YOLOV11_Hyolov11.cpp是实现文件包含模型加载、图像预处理、推理和后处理解码输出NMS的全部逻辑。由于代码较长这里给出核心的detect函数框架和关键点bool YoloV11::loadModel(AAssetManager* mgr) { net.opt.use_vulkan_compute false; // 根据是否使用Vulkan API选择 // 使用AAsset从apk的assets目录读取模型文件 int ret net.load_param(mgr, best.param); ret net.load_model(mgr, best.bin); return ret 0; } std::vectorDetection YoloV11::detect(cv::Mat rgb) { std::vectorDetection results; // 1. 预处理Resize到640x640归一化BGR2RGB减均值除标准差等 cv::Mat input_blob preprocess(rgb); // 2. 创建NCNN的Mat并填入数据 ncnn::Mat in ncnn::Mat::from_pixels(input_blob.data, ncnn::Mat::PIXEL_RGB, input_size, input_size); // 归一化处理假设你训练时用了 --norm 0.00392 (1/255) in.substract_mean_normalize(0, norm_vals); // 3. 推理 ncnn::Extractor ex net.create_extractor(); ex.set_num_threads(4); // 设置线程数根据CPU核心数调整 ex.input(images, in); // “images”是你在param文件中看到的输入节点名 ncnn::Mat out; ex.extract(output, out); // “output”是输出节点名请根据param文件确认 // 4. 后处理解码边界框、置信度、类别并执行NMS results decode_outputs(out); nms(results); // 5. 将检测框坐标映射回原始图像尺寸 for (auto det : results) { det.bbox.x det.bbox.x * rgb.cols / input_size; det.bbox.y det.bbox.y * rgb.rows / input_size; det.bbox.width det.bbox.width * rgb.cols / input_size; det.bbox.height det.bbox.height * rgb.rows / input_size; } return results; }CMakeLists.txt文件用于组织编译cmake_minimum_required(VERSION 3.10.2) project(yolov11ncnn) add_library( yolov11ncnn SHARED yolov11.cpp) find_library( log-lib log ) # 引入NCNN和OpenCV的预编译库 # 假设你将编译好的ncnn android库和opencv android sdk放在了 libs 目录下 set(ncnn_DIR ${CMAKE_SOURCE_DIR}/../../../libs/ncnn-android/${ANDROID_ABI}/lib/cmake/ncnn) set(OpenCV_DIR ${CMAKE_SOURCE_DIR}/../../../libs/opencv-mobile-4.8.1-android/${ANDROID_ABI}/lib/cmake/opencv4) find_package(ncnn REQUIRED) find_package(OpenCV REQUIRED) target_link_libraries( yolov11ncnn ncnn ${OpenCV_LIBS} ${log-lib} )5.3 编写JNI接口与Java层调用为了让Java代码能够调用C的推理函数我们需要使用JNIJava Native Interface来搭建桥梁。在app/src/main/cpp下创建native-lib.cpp#include jni.h #include android/asset_manager_jni.h #include yolov11.h extern C { JNIEXPORT jlong JNICALL Java_com_example_firesmokedetection_YoloV11NCNN_initModel(JNIEnv *env, jobject thiz, jobject assetManager) { AAssetManager* mgr AAssetManager_fromJava(env, assetManager); YoloV11* detector new YoloV11(); if (detector-loadModel(mgr)) { return (jlong) detector; } else { delete detector; return 0; } } JNIEXPORT jobjectArray JNICALL Java_com_example_firesmokedetection_YoloV11NCNN_detect(JNIEnv *env, jobject thiz, jlong netPtr, jint width, jint height, jbyteArray yuvData) { // 将Java层传来的YUV数据来自摄像头转换为OpenCV的Mat // 调用 detector-detect(mat) 进行检测 // 将C的Detection结果转换为Java对象数组并返回 // 代码较长主要是数据格式转换和JNI对象构造 } JNIEXPORT void JNICALL Java_com_example_firesmokedetection_YoloV11NCNN_deinitModel(JNIEnv *env, jobject thiz, jlong netPtr) { if (netPtr ! 0) { YoloV11* detector (YoloV11*) netPtr; delete detector; } } }在Java层创建对应的类YoloV11NCNN.javapackage com.example.firesmokedetection; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.RectF; public class YoloV11NCNN { static { System.loadLibrary(yolov11ncnn); } private long mNativePtr 0; public boolean initModel(AssetManager assetManager) { mNativePtr initModel(assetManager); return mNativePtr ! 0; } public DetectionResult[] detect(Bitmap bitmap) { if (mNativePtr 0) return new DetectionResult[0]; return detect(mNativePtr, bitmap); } public void deinitModel() { if (mNativePtr ! 0) { deinitModel(mNativePtr); mNativePtr 0; } } // 定义检测结果的数据结构 public static class DetectionResult { public RectF bbox; public float score; public int label; // 0 for fire, 1 for smoke } // Native方法声明 private native long initModel(AssetManager assetManager); private native DetectionResult[] detect(long netPtr, Bitmap bitmap); private native void deinitModel(long netPtr); }6. APP UI构建与摄像头流处理有了核心的检测引擎我们需要一个界面来展示摄像头画面并绘制检测结果。6.1 设计主界面与权限处理activity_main.xml可以设计为一个简单的布局中心是一个TextureView用于显示摄像头预览上面叠加一个自定义的OverlayView用于绘制检测框。?xml version1.0 encodingutf-8? RelativeLayout xmlns:androidhttp://schemas.android.com/apk/res/android android:layout_widthmatch_parent android:layout_heightmatch_parent TextureView android:idid/textureView android:layout_widthmatch_parent android:layout_heightmatch_parent / com.example.firesmokedetection.OverlayView android:idid/overlayView android:layout_widthmatch_parent android:layout_heightmatch_parent android:backgroundandroid:color/transparent / Button android:idid/btnSwitch android:layout_widthwrap_content android:layout_heightwrap_content android:layout_alignParentBottomtrue android:layout_centerHorizontaltrue android:textSwitch Camera / /RelativeLayout在MainActivity.java中需要处理动态权限申请摄像头、存储权限并初始化摄像头。6.2 实现摄像头数据流捕获与转换我们使用Camera2 API来获取摄像头数据流因为它提供了更精细的控制。在onResume中打开摄像头并设置一个ImageReader来接收预览帧。private void openCamera() { CameraManager manager (CameraManager) getSystemService(Context.CAMERA_SERVICE); try { String cameraId manager.getCameraIdList()[0]; // 默认后置摄像头 manager.openCamera(cameraId, new CameraDevice.StateCallback() { Override public void onOpened(NonNull CameraDevice camera) { mCameraDevice camera; createCameraPreviewSession(); } // ... 其他回调方法 }, null); } catch (CameraAccessException e) { e.printStackTrace(); } } private void createCameraPreviewSession() { // ... 创建CaptureRequest.Builder设置TextureView作为预览输出目标 // 同时创建ImageReader用于获取YUV_420_888格式的预览帧进行推理 mImageReader ImageReader.newInstance(mPreviewSize.getWidth(), mPreviewSize.getHeight(), ImageFormat.YUV_420_888, 2); // 设置缓冲区数量 mImageReader.setOnImageAvailableListener(mOnImageAvailableListener, mBackgroundHandler); // 将ImageReader的surface也加入到预览会话的targets中 ListSurface surfaces new ArrayList(); surfaces.add(mPreviewSurface); surfaces.add(mImageReader.getSurface()); // ... 创建CaptureSession }关键在mOnImageAvailableListener当有新的一帧图像可用时我们将其传递给后台线程进行推理。private final ImageReader.OnImageAvailableListener mOnImageAvailableListener new ImageReader.OnImageAvailableListener() { Override public void onImageAvailable(ImageReader reader) { Image image reader.acquireLatestImage(); if (image ! null) { // 将Image对象转换为YUV数据然后转换为RGB Bitmap // 注意这个转换过程比较耗时必须在后台线程进行 ByteBuffer yBuffer image.getPlanes()[0].getBuffer(); ByteBuffer uBuffer image.getPlanes()[1].getBuffer(); ByteBuffer vBuffer image.getPlanes()[2].getBuffer(); // ... 调用YUV转RGB的函数例如使用RenderScript或手动转换 Bitmap rgbBitmap convertYUV420ToRGB(yBuffer, uBuffer, vBuffer, image.getWidth(), image.getHeight()); image.close(); // 将Bitmap传递给推理线程 mInferenceHandler.post(() - { YoloV11NCNN.DetectionResult[] results mYoloDetector.detect(rgbBitmap); // 将结果传递给UI线程进行绘制 runOnUiThread(() - mOverlayView.setResults(results)); }); } } };6.3 实现绘制层与性能优化OverlayView是一个自定义View负责在onDraw方法中绘制检测框和标签。public class OverlayView extends View { private ListDetectionResult mResults new ArrayList(); public void setResults(ListDetectionResult results) { mResults results; postInvalidate(); // 请求重绘 } Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); Paint paint new Paint(); paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(4.0f); paint.setTextSize(36.0f); for (DetectionResult result : mResults) { // 根据类别设置不同颜色 if (result.label 0) { // fire paint.setColor(Color.RED); } else { // smoke paint.setColor(Color.BLUE); } // 绘制矩形框 canvas.drawRect(result.bbox, paint); // 绘制标签和置信度 String label result.label 0 ? Fire : Smoke; String text String.format(%s %.2f, label, result.score); canvas.drawText(text, result.bbox.left, result.bbox.top - 10, paint); } } }性能优化核心技巧异步推理绝对不能在OnImageAvailableListener的回调中直接进行耗时的模型推理这会导致预览严重卡顿。必须将图像数据抛到单独的HandlerThread或线程池中处理。帧率控制手机摄像头帧率可能高达30fps但模型推理可能只能达到10-15fps。盲目处理每一帧会导致队列堆积。一个简单的策略是使用一个“帧采样”机制比如每处理完一帧后丢弃接下来的1-2帧只处理最新的帧。图像缩放从ImageReader获取的图像是摄像头原始分辨率如1920x1080直接缩放到640x640进行推理开销很大。可以在YUV域直接进行下采样或者使用Bitmap.createScaledBitmap时选择更高效的滤波方式如Bitmap.Config.RGB_565和Matrix.ScaleToFit。内存复用避免在每一帧都创建新的Bitmap和byte[]。可以预先分配好固定大小的缓冲区在循环中复用。线程数调整在NCNN的Extractor中设置ex.set_num_threads()。对于现代手机8核设置为4是个不错的起点。可以通过实测找到最佳线程数。7. 调试、优化与常见问题排查将各个部分组装起来后真正的挑战才刚刚开始。你几乎一定会遇到各种问题从APP崩溃到检测框错乱。下面是一些常见问题及其排查思路。7.1 模型加载失败或推理崩溃症状APP启动时闪退或点击运行后立即崩溃logcat中看到signal 11 (SIGSEGV)等native层错误。排查检查模型文件确认best.param和best.bin已正确放入assets文件夹且文件名在代码中拼写无误。特别注意Android对资源文件名大小写敏感检查输入输出名这是最常见的问题。用文本编辑器打开best.param确认第一行的输入层名称如Input images和最后一层之前的输出层名称。确保在C代码ex.input()和ex.extract()中使用的字符串与之完全一致包括大小写。检查预处理确认预处理步骤缩放、归一化、颜色通道顺序与模型训练和导出时的设置完全一致。YOLO通常输入RGB格式像素值归一化到0-1即除以255。在NCNN中这通过in.substract_mean_normalize(0, norm_vals)实现其中norm_vals[0] norm_vals[1] norm_vals[2] 1/255.0f。检查OpenCV链接如果使用了OpenCV进行图像处理确保在CMakeLists.txt中正确链接了OpenCV库并且ABIarmeabi-v7a/arm64-v8a匹配。7.2 检测结果框位置或类别错误症状能画出框但框的位置完全不对或者所有框的类别都是同一个。排查坐标映射错误检查后处理中将640x640坐标系下的检测框映射回原始图像坐标系的代码。公式应为x_original x_det * width_original / 640。后处理解码错误这是最复杂的部分。YOLOv11的输出格式需要你精确解析。你需要清楚你的模型输出是(batch, num_anchors, 85)这样的形状其中854(bbox)1(obj_conf)80(cls_conf)。对于自定义的2类模型最后应该是4127维。务必使用Netron打开ONNX模型仔细查看输出节点的形状和维度顺序。NCNN的Mat数据布局是C/H/W通道/高/宽可能需要通过.reshape()或直接计算索引来正确访问数据。置信度阈值和NMS参数score_threshold设得太高所有框都被过滤掉了设得太低会留下大量噪声框。nms_threshold设得太小本应合并的框没有合并设得太大会把不同物体的框错误合并。需要根据你的数据集特点进行调整。7.3 APP运行卡顿帧率很低症状预览画面卡顿检测延迟很高。排查与优化测量耗时在推理代码前后加时间戳打印出预处理、推理、后处理各阶段的耗时找到瓶颈。降低输入分辨率如果训练时用的是640可以尝试在安卓端使用480甚至320进行推理速度会快很多但精度会下降。这是一个速度与精度的权衡。启用量化如4.3节所述使用INT8量化模型通常能带来30%-50%的速度提升且精度损失在可接受范围内。尝试Vulkan后端如果你的手机GPU支持Vulkan可以在NCNN中启用Vulkan计算net.opt.use_vulkan_compute true。对于某些模型和芯片GPU推理比CPU更快。优化图像传递检查从ImageReader获取YUV数据到转换成Bitmap或直接转换成ncnn::Mat的路径是否高效。避免不必要的拷贝和格式转换。7.4 更换自定义检测目标如果你想检测其他目标比如“安全帽”或“车牌”整个流程是完全通用的只需修改以下几点数据准备新的数据集用labelImg标注对应的类别如helmet,no_helmet。训练修改数据集配置文件data.yaml中的nc类别数和names类别名称列表。重新训练模型。导出与转换重复第3、4步得到新的best.param和best.bin。安卓端替换assets目录下的模型文件。在C或Java代码中更新类别数量num_classes。在OverlayView的绘制代码中根据新的class_id修改标签文字和框的颜色映射。如果类别含义变化可能还需要调整后处理中类别置信度的处理逻辑但通常不变。整个过程的核心——模型转换、NCNN集成、安卓摄像头流处理、异步推理框架——都是完全复用的。这正体现了本项目作为“模板”的价值。从模型训练到安卓APP上看到实时检测框这条路走下来确实不短涉及到深度学习、模型工程、移动开发等多个领域的知识。但每一步拆解开来都是有标准做法和工具支持的。最大的挑战往往不是某个具体的技术点而是整个链条的贯通和调试。我个人的体会是耐心和细致的调试日志是你最好的朋友。每次卡住时回到最基本的环节验证模型输出对吗数据转换对吗名字匹配吗参数对吗把这几个问题搞清楚大部分难题都能迎刃而解。最后别忘了在真机上多测试不同型号和系统版本的手机兼容性往往是项目“落地”前的最后一关。本文还有配套的精品资源点击获取
返回列表