【实战指南】从零到一:将YOLOv5模型部署至Android App的完整流程与性能调优

发布时间:2026/6/30 15:44:20
【实战指南】从零到一:将YOLOv5模型部署至Android App的完整流程与性能调优
1. 环境准备与模型训练想要在Android上跑YOLOv5目标检测第一步得把模型训练好。我建议直接用PyTorch官方提供的YOLOv5代码库这个项目维护得相当不错社区支持也很活跃。先装个Anaconda创建虚拟环境避免把本地环境搞乱conda create -n yolov5 python3.8 conda activate yolov5 pip install torch1.9.0cu111 torchvision0.10.0cu111 -f https://download.pytorch.org/whl/torch_stable.html数据集处理是个容易踩坑的地方。VOC数据集下载解压后记得按照YOLOv5要求的格式重组文件夹结构。关键是要创建正确的.yaml文件比如我的VOC.yaml长这样train: ../VOC/images/train val: ../VOC/images/val nc: 20 # 类别数 names: [aeroplane, bicycle, bird, boat, bottle, bus, car, cat, chair, cow, diningtable, dog, horse, motorbike, person, pottedplant, sheep, sofa, train, tvmonitor]训练参数调优我踩过不少坑。batch size不是越大越好我的RTX 3070显卡在8-12之间表现最佳。workers数量要根据CPU核心数来定设太高反而会导致数据加载阻塞。建议先用小规模数据跑几个epoch验证管道是否通畅python train.py --img 640 --batch 8 --epochs 50 --data VOC.yaml --weights yolov5s.pt训练完成后别急着收工一定要用验证集测试模型表现。我习惯用--task study参数生成各种指标可视化报告特别是PR曲线和混淆矩阵能直观看出哪些类别识别效果差python val.py --weights runs/train/exp/weights/best.pt --data VOC.yaml --task study2. 模型转换与优化PyTorch模型不能直接在Android跑需要先转成移动端友好的格式。我推荐PyTorch→ONNX→NCNN这条转换路径实测兼容性最好。转换前有个关键步骤修改YOLOv5的Focus层实现。原始实现用了数组切片操作在移动端容易出问题# 修改前训练用 def forward(self, x): return self.conv(torch.cat([x[..., ::2, ::2], x[..., 1::2, ::2], x[..., ::2, 1::2], x[..., 1::2, 1::2]], 1)) # 修改后部署用 def forward(self, x): return self.conv(torch.cat([x, x, x, x], 1)) # 简单复制四份ONNX转换时这几个参数特别重要--dynamic让输入尺寸可变--simplify自动优化计算图--opset 11确保算子兼容性。转换完记得用onnx-simplifier进一步优化python export.py --weights best.pt --include onnx --dynamic --simplify --opset 11 python -m onnxsim yolov5s.onnx yolov5s-sim.onnx转NCNN时我试过在线转换工具但最终选择本地编译onnx2ncnn工具。关键是要下载对应版本的预编译工具包转换后一定要做optimize./onnx2ncnn yolov5s-sim.onnx yolov5s.param yolov5s.bin ./ncnnoptimize yolov5s.param yolov5s.bin yolov5s-opt.param yolov5s-opt.bin 65536最后记得手动修改.param文件把最后的三个输出层维度全改为-1。这个坑我踩过不改的话Android端会输出无数个无效检测框画面直接糊掉。3. Android工程配置Android Studio工程配置有几个关键点。首先在build.gradle里正确引入NCNN库我用的2022年稳定版dependencies { implementation com.tencent.ncnn:ncnn-android-vulkan:20220216 }NDK配置要注意ABI过滤现在主流设备都是arm64-v8a没必要支持armeabi-v7a徒增包体积android { defaultConfig { ndk { abiFilters arm64-v8a } } }模型文件要放在assets目录但Android对单个文件大小有限制。我用过的最稳方案是把.bin文件拆分成多个小文件运行时再合并// 在assets目录创建 yolov5s-opt.bin.0, yolov5s-opt.bin.1... InputStream[] streams new InputStream[4]; for (int i 0; i 4; i) { streams[i] getAssets().open(yolov5s-opt.bin. i); } FileOutputStream fos new FileOutputStream(mergedFile); byte[] buffer new byte[8192]; for (InputStream is : streams) { int length; while ((length is.read(buffer)) 0) { fos.write(buffer, 0, length); } is.close(); }别忘了在AndroidManifest.xml开启硬件加速和Vulkan支持uses-feature android:nameandroid.hardware.vulkan.version android:requiredfalse/ uses-feature android:nameandroid.hardware.opengles.version android:requiredtrue/4. 模型加载与推理优化模型加载阶段最容易出现内存问题。我的经验是先创建静态Net对象在Application初始化时预加载public class YOLOApp extends Application { public static Net yoloNet; Override public void onCreate() { super.onCreate(); yoloNet new Net(); yoloNet.opt.use_vulkan_compute true; yoloNet.loadParam(getAssets().open(yolov5s-opt.param)); yoloNet.loadModel(getAssets().open(yolov5s-opt.bin)); } }图像预处理要特别注意颜色通道和归一化。YOLOv5的输入需要RGB格式且数值归一化到0-1Mat rgb new Mat(); Imgproc.cvtColor(inputMat, rgb, Imgproc.COLOR_BGR2RGB); rgb.convertTo(rgb, CvType.CV_32FC3, 1.0 / 255.0); Mat blob Dnn.blobFromImage(rgb); Extractor extractor yoloNet.createExtractor(); extractor.input(input, blob);后处理阶段最耗CPU我优化后的方案是先用Java实现基础版本再用C重写关键部分。特别是NMS非极大值抑制算法用C能提速3倍以上extern C JNIEXPORT jobjectArray JNICALL Java_com_example_yolo_YOLOv5_nativeProcess(JNIEnv *env, jobject thiz, jfloatArray output, jint num_classes) { jfloat *outputPtr env-GetFloatArrayElements(output, nullptr); // C实现的高效NMS算法 ... env-ReleaseFloatArrayElements(output, outputPtr, 0); return result; }实测发现动态调整推理分辨率能显著提升帧率。我的策略是当连续3帧检测耗时超过50ms时自动将输入尺寸从640x640降到416x416。5. 性能调优实战内存优化是第一要务。通过Android Profiler发现每次推理都会产生临时内存碎片解决方案是复用Mat对象private Mat reusableMat; private Mat reusableBlob; void detect(Mat input) { if (reusableMat null) { reusableMat new Mat(input.size(), CvType.CV_32FC3); } Imgproc.cvtColor(input, reusableMat, Imgproc.COLOR_BGR2RGB); if (reusableBlob null) { reusableBlob new Mat(); } Dnn.blobFromImage(reusableMat, reusableBlob, 1.0 / 255.0); ... }线程管理也很关键。我建议用单线程Executor处理推理任务配合Handler实现结果回调private ExecutorService inferenceExecutor Executors.newSingleThreadExecutor(); void asyncDetect(Mat input, DetectionCallback callback) { inferenceExecutor.execute(() - { Result result blockingDetect(input); new Handler(Looper.getMainLooper()).post(() - { callback.onDetected(result); }); }); }功耗优化方面我总结出三个有效手段检测到设备温度超过45℃时自动降低推理频率屏幕关闭时暂停检测任务使用WakeLock保持CPU唤醒但每次最多持有500msPowerManager powerManager (PowerManager) getSystemService(POWER_SERVICE); WakeLock wakeLock powerManager.newWakeLock( PowerManager.PARTIAL_WAKE_LOCK, YOLOv5::WakeLock); wakeLock.acquire(500);最后别忘了做机型适配。有些低端GPU不支持FP16运算需要在运行时动态判断boolean supportFP16 false; if (yoloNet.opt.use_vulkan_compute) { int[] vulkanInfo new int[3]; yoloNet.getVulkanGpuInfo(vulkanInfo); supportFP16 (vulkanInfo[2] 0x1) ! 0; } extractor.setEnableFp16Storate(supportFP16);6. 实用技巧与问题排查模型量化能大幅减小体积。我用的Post-training量化方案虽然精度损失约2%但模型体积缩小4倍./ncnn2int8 yolov5s-opt.param yolov5s-opt.bin yolov5s-int8.param yolov5s-int8.bin imagelist.txt常见的图像方向问题可以通过EXIF信息解决。Android相机拍的照片可能带旋转标记int orientation ExifInterface.ORIENTATION_NORMAL; try { ExifInterface exif new ExifInterface(imagePath); orientation exif.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); } catch (IOException e) { e.printStackTrace(); } Matrix matrix new Matrix(); switch (orientation) { case ExifInterface.ORIENTATION_ROTATE_90: matrix.postRotate(90); break; case ExifInterface.ORIENTATION_ROTATE_180: matrix.postRotate(180); break; case ExifInterface.ORIENTATION_ROTATE_270: matrix.postRotate(270); break; } Bitmap rotatedBitmap Bitmap.createBitmap( originalBitmap, 0, 0, originalBitmap.getWidth(), originalBitmap.getHeight(), matrix, true);遇到模型加载失败时先检查文件MD5值是否匹配。我遇到过assets文件压缩导致模型损坏的情况解决方案是在build.gradle中禁用压缩android { aaptOptions { noCompress bin, param } }如果出现检测框漂移大概率是输入尺寸不匹配。确保Android端的预处理和训练时的预处理完全一致包括相同的归一化方式0-1或0-255相同的颜色通道顺序RGB或BGR相同的resize算法通常用INTER_LINEAR