尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

目标检测App开发全流程:从数据标注到YOLO移动端部署

发布时间:2026/9/24 18:29:15

资讯中心
01
ARTICLE

目标检测App开发全流程:从数据标注到YOLO移动端部署

目标检测App开发全流程:从数据标注到YOLO移动端部署
简介基于深度学习的目标检测App毕业设计/课程作业完整源码包面向计算机类毕业生与课程设计学生覆盖数据准备、模型训练、移动端部署与系统交互设计全流程适合作为目标检测课题的工程参考与二次开发基础。资源共1347个文件包含Java界面逻辑、XML布局、Gradle构建配置、JSON数据配置、打包生成的APK及底层SO库整体压缩包约117.59MB目录结构以Graduation Design为主线便于按模块查找学习。已有113人学习查看源码中同时涉及Python训练脚本、C推理模块与Android前端代码可帮助读者理解YOLO等模型如何完成端侧移植也能从中梳理出完整的模型优化、工程化封装与调试思路对毕业设计答辩准备和课程项目实践均有直接参考价值。1. 从一份毕设源码聊起目标检测 App 到底拆成了几块拿到这份“基于深度学习的目标检测 app”源码包时我原本以为就是一个 Android 工程套个 YOLO 模型真正解开才发现它把数据标注、模型训练、模型压缩、移动端 C 调用都串成了一整条链路。对正在做毕设或课程设计的同学来说这份资源的价值在于能让你看清楚一个真实的目标检测系统是怎么从图像数据一路走到手机界面的而不是只看一段训练代码或者一个 App 壳。资源里既有 Python 写的训练和数据处理脚本也有 C 的端侧推理相关代码适合想快速复现一个完整项目的人也适合第一次把模型往移动端部署的初学者。我会按拆包顺序往下讲先讲数据准备再讲训练、导出和部署避坑。2. 数据准备标注格式、数据集划分与预处理这次全理顺2.1 先确认标注格式VOC XML 到 YOLO txt 的转换思路大多数公开检测数据集用 Pascal VOC 的 XML 标注而 YOLO 系模型训练时更习惯读 YOLO txt 格式。两种格式的核心区别在坐标定义上VOC 存的是 bndbox 四个角点也就是 xmin、ymin、xmax、ymax而 YOLO 存的是归一化后的中心点坐标和宽高也就是 x_center、y_center、w、h。这个差异如果没理顺训练时会读到错误边界框模型可能直接学成一个废模型。在毕设项目里拿到手的数据集往往不是现成的 YOLO 布局可能是从标注平台导出的 zip也可能是老师给的一份 VOC 文件夹。第一步一定是把所有 XML 统一转成 txt同时生成统一的 images 目录和 labels 目录。我一般会先按类别建一个 classes 列表然后逐个 XML 解析。顺序必须固定因为 YOLO 用数字索引代表类别一旦训练配置里的 names 顺序变了前面做的一切都白费。下面这段是我常用的转换脚本针对单张图片import os import xml.etree.ElementTree as ET classes [person, car, dog] # 与训练时 dataset 的 names 保持一致 def convert_voc_xml(xml_path, txt_path): tree ET.parse(xml_path) root tree.getroot() w int(root.find(size/width).text) h int(root.find(size/height).text) lines [] for obj in root.iter(object): name obj.find(name).text if name not in classes: continue box obj.find(bndbox) xmin float(box.find(xmin).text) ymin float(box.find(ymin).text) xmax float(box.find(xmax).text) ymax float(box.find(ymax).text) x_center (xmin xmax) / 2.0 / w y_center (ymin ymax) / 2.0 / h box_w (xmax - xmin) / w box_h (ymax - ymin) / h lines.append(f{classes.index(name)} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}) with open(txt_path, w) as f: f.write(\n.join(lines))这个脚本的核心逻辑很直白先从 XML 根节点拿到 size 里的图像宽高再遍历所有 object取类名和 bndbox 坐标。计算中心点坐标和宽高后全部除以宽高做归一化这样不同分辨率的图片数值范围才会一致。最后按 classes.index(name) 输出类别索引而不是字符串YOLO 训练时只认数字。参数说明classes 列表顺序直接决定输出数字如果训练端 dataset 里 names 顺序不一致轻则 mAP 报错为 0重则训练不收敛。所以转换前先打开 dataset 配置文件确认顺序不要凭记忆写。box_w 和 box_h 是框的真实宽高除以图像宽高不是归一化中心点后再求差别算反了。2.2 数据集划分与完整性检查转换完后通常还要按 train/val 比例把图像和对应的 txt 分成两份。YOLO 训练时要求 images 和 labels 一一对应不能出现某张图没有标签文件也不能出现标签文件对应不上图。我一般会先跑一个检查脚本再生成 train.txt、val.txt。import os import random def split_dataset(img_dir, val_ratio0.2, seed42): random.seed(seed) imgs [f for f in os.listdir(img_dir) if f.lower().endswith((.jpg, .jpeg, .png))] random.shuffle(imgs) val_cnt int(len(imgs) * val_ratio) val_imgs imgs[:val_cnt] train_imgs imgs[val_cnt:] with open(train.txt, w) as f: for name in train_imgs: f.write(os.path.join(img_dir, name) \n) with open(val.txt, w) as f: for name in val_imgs: f.write(os.path.join(img_dir, name) \n) print(ftrain: {len(train_imgs)}, val: {len(val_imgs)})这个脚本本身不复杂但有两个点值得注意。第一random.seed 固定住随机种子保证每次运行的划分结果一致否则你第二天跑训练训练集和验证集会变对比实验就没意义。第二val_ratio 一般取 0.15 到 0.2 之间小数据集可以稍微提高验证集比例防止验证结果波动太大。划分完以后别着急训练先做一次“标签完整性检查”。遍历 train.txt 里的每一张图确认它对应的 labels 目录下有一个同名 txt并且 txt 非空。空标签文件在训练时一般会报 warning有些框架会直接跳过这张图导致你训练了 100 轮实际有效样本比预期少。检查方法很简单用 Python 读一下 txt 文件的行数就够了。def check_labels(img_list_path, label_dir): with open(img_list_path) as f: lines f.read().strip().split(\n) bad [] for line in lines: base os.path.splitext(os.path.basename(line))[0] label_path os.path.join(label_dir, base .txt) if not os.path.exists(label_path) or os.path.getsize(label_path) 0: bad.append(line) print(fbad files: {len(bad)})2.3 预处理里容易忽略的两个细节坐标转换和划分只是数据准备工作的一部分真正影响训练效果的是预处理细节。最容易被忽略的是图像尺寸。YOLO 训练时会做 letterbox保持原图比例并在四周填充灰色避免直接拉伸变形。这个操作一般由训练框架内置不需要在脚本里额外实现。但如果你的数据是手机拍摄的竖屏图片建议在训练参数里明确设置一个能整除 32 的输入尺寸比如 640否则模型对竖屏场景的泛化会差很多。另一个容易被忽略的是数据增强的重复叠加。训练框架默认开了 mosaic、随机翻转、HSV 扰动这些增强手段你就不要在预处理脚本里再做一次随机裁剪或者颜色抖动否则双重增强会让数据分布被过度破坏。我见过不少人一边在数据管线里做了随机裁剪一边又开着 mosaic最后训练集和验证集标准都不一致模型表现一塌糊涂。总之一句话数据准备阶段的目标是让后续训练脚本拿到的每一张图都有干净、对齐、非空的标签。如果这一步做到位后面会省很多事。3. 模型训练与选择为什么毕设项目往往选 YOLO 而不是 Faster R-CNN3.1 不同检测框架在移动端的取舍目标检测的模型选择直接决定了后面部署要踩多少坑。Faster R-CNN 在 VOC 和 COCO 上精度确实不低但它是两阶段模型RPN 到 RoI Head 的链路在端侧推理时非常吃亏。SSD 是单阶段速度尚可但在小尺寸目标上的表现不如 YOLO 后期版本。YOLO 把目标分类和边框回归一次性做掉结构简单推理快而且 YOLOv5/v8 生态里预训练权重、导出脚本、NCNN 转换工具全都有这几点叠加起来让它成为本科毕设和课程设计里的主流选择。在移动 App 场景里用户对实时性的要求往往高于那两三个点的 mAP 提升。一个能稳定跑 30 FPS 的 YOLO 模型比一个只能跑 8 FPS 的 Faster R-CNN 演示效果好得多。所以这份资源里的系统大概率是以 YOLO 系模型作为检测主干配合 Python 训练脚本和 C 部署层来完成的。3.2 训练准备dataset.yaml 与路径约定不管用 YOLOv5 还是 YOLOv8训练前都要准备一个数据描述文件。以 YOLOv5 为例dataset.yaml 内容如下train: ./train.txt val: ./val.txt nc: 3 names: [person, car, dog]这个文件用的是相对路径还是绝对路径取决于你执行训练命令的位置。我习惯把 train.txt 和 val.txt 放在项目路径下数据集 yaml 里直接写相对路径这样换电脑后不容易因为绝对路径失效报错。nc 是类别数量names 顺序必须和前面转换脚本的 classes 保持一致这是已经第二次强调但依然很多人在这里翻车。3.3 训练命令怎么跑参数怎么调在 YOLOv5 目录下执行python train.py --img 640 --batch 16 --epochs 100 --data dataset.yaml --weights yolov5s.pt --cache拆开看每个参数--img 是训练输入尺寸640 是最常见的平衡点再大能保留更多细节但显存占用翻倍--batch 受显卡显存限制16G 显存跑 YOLOv5s 可以到 32但为了稳定我一般从 16 起步--epochs 100 对毕设数据集通常够用如果验证集 mAP 还在涨可以加到 150--weights yolov5s.pt 表示加载 COCO 预训练权重这样模型不用从零学底层特征收敛快得多--cache 把图片缓存进内存减少磁盘 IO提升训练速度。如果你用的是 ultralytics 的 YOLOv8等价写法是from ultralytics import YOLO model YOLO(yolov8s.pt) model.train(datadataset.yaml, epochs100, imgsz640, batch16)Python API 的好处是可以在 Jupyter 里直接看训练曲线调试起来更直观。两种方式底层流程一样无非是接口封装不同。训练过程中你只需要盯住 box_loss、cls_loss 和验证集 mAP。如果 mAP 在某个 epoch 后不再上升甚至下降通常不是过拟合就是学习率没调好。此时可以适当降低初始学习率或者打开早停。3.4 训练结果验证别只盯着 mAP训练结束后weights 目录下会生成 best.pt 和 last.pt。best.pt 是验证集上表现最好的权重一般用它做后续部署。看验证结果时除了 mAP还要拿 best.pt 在验证集上跑一批预测图肉眼确认检测框是否贴合目标边缘。我经常遇到 mAP 到 0.8 但实际边框偏移半身的情况多半是标签坐标本身不准确这时候正确做法是回头修数据而不是继续加训练轮次。还有一个常被忽略的点训练时启用了 autoanchor也就是自动计算 anchor 大小。这个机制会在训练开始前扫描你的标注框分布生成更适合当前数据集的预设锚框。如果你的数据里目标尺寸分布和默认 COCO 差别很大比如全是细长物体建议保留 autoanchor不要手动关掉。很多看起来不可复现的结果差异其实都来自这里。4. 模型压缩与导出量化、转 ONNX、再落到移动端能跑的格式4.1 为什么移动端不能直接跑 PyTorch 模型模型训练完很多人会误以为拿到 best.pt 就能往 App 里塞。实际上 PyTorch 的 .pt 文件至少有三个问题体积偏大、依赖 Python 运行环境、算子不支持移动端硬件加速。即使 YOLOv5sFP32 权重也有 14MB 左右还包含大量冗余的计算节点。手机上想跑要么用 NCNN、MNN 这类端侧推理框架要么转成 TFLite 或 Core ML。所以路线一般是先转 ONNX再由 ONNX 转到目标平台的格式中间加一步量化来减小体积、提高速度。4.2 导出 ONNX 的完整操作与验证在 YOLOv5 目录里执行python export.py --weights best.pt --include onnx --opset 11 --dynamic--include onnx 表示导出 ONNX 格式--opset 11 是比较安全的算子版本太新的 opset 在转 ncnn 时不一定被支持--dynamic 让输入尺寸动态可变方便端侧根据自己的分辨率做 letterbox如果不加模型输入尺寸会被固定成训练时的 640x640。如果你用的是 ultralytics导出命令是yolo export modelbest.pt formatonnx opset11 dynamicTrue导出完成后先用 onnxruntime 在 PC 上验证一遍 ONNX 模型能正常推理import onnxruntime as ort import numpy as np inp np.random.rand(1, 3, 640, 640).astype(np.float32) sess ort.InferenceSession(best.onnx) outs sess.run(None, {sess.get_inputs()[0].name: inp}) print(outs[0].shape)这里用随机数作为输入形状是 (1,3,640,640)输出 shape 应该是一个类似 (1, 25200, 85) 的三维向量其中 25200 是不同尺度 anchor 预测框总数85 是 4 个坐标 1 个置信度 80 个类别概率。如果你的训练类别只有 3 类输出可能是 8 维。如果发现输出结构和预期不一致先检查导出参数不要继续往下走。4.3 ONNX 转 ncnn模型格式与算子兼容性打开手机 App 之前先把 ONNX 转成 ncnn 格式onnx2ncnn best.onnx best.param best.bin转换成功后得到两个文件best.param 描述网络结构best.bin 存权重。param 是文本格式可以用编辑器打开检查输入层尺寸和网络拓扑是否正常。如果转换过程出现不支持的算子先不要急着换模型跑一下 onnx-simplifier 把计算图简化再重新转python -m onnxsim best.onnx best_sim.onnx然后再执行 onnx2ncnn。这个问题在带有上采样、自适应池化这类算子的网络里经常遇到。为了避免后面部署时看代码全靠猜在 App 里加载 ncnn 模型的最小 C 片段长这样#include net.h ncnn::Net detector; detector.opt.num_threads 4; if (detector.load_param(best.param) ! 0 || detector.load_model(best.bin) ! 0) { // 处理加载失败 }load_param 和 load_model 返回非 0 表示失败失败原因大多是 param 和 bin 版本不匹配或者路径不对。opt.num_threads 可以提前配置避免每帧推理都花时间在创建线程池上。4.4 INT8 量化与校准图片选择原始 FP32 模型在手机上能跑但速度往往不够漂亮。比较成熟的做法是把权重转成 FP16 或 INT8。FP16 几乎不掉精度但在部分 CPU 上没有加速INT8 需要校准数据集要统计每层激活值的分布工程成本略高。如果你不是准备打比赛只是做毕设演示建议先做 FP16 量化如果帧率还不能接受再考虑 INT8。在 ncnn 里INT8 量化一般通过 ncnn2int8 配合校准图片完成ncnn2int8 best.param best.bin best_int8.param best_int8.bin calib_list.txtcalib_list.txt 里每一行写一张典型图像的路径最好从验证集里挑 100 到 200 张覆盖各类别、各种光照和不同目标尺寸。量化后要用测试集重新跑一遍 mAP对比 INT8 和 FP32 的精度差距。你会发现掉点通常集中在小目标和边缘目标上遇到这种情况可以往校准图片里多塞一些小目标样本让激活值分布更贴近真实数据。4.5 部署格式小结部署到 Android 或 iOS最终拿到的是 param、bin、模型配置和一份后处理逻辑。ncnn 的输出通常是一维数组或按 anchor 排列的向量你要做的后处理包括按置信度阈值过滤低分框、用 NMS 抑制重叠框、把归一化坐标映射回原图坐标系。后处理代码量不大但每个细节都能压榨性能。建议把预处理、推理、后处理分成三个独立函数方便后面做性能分析和单元测试。5. 移动端部署避坑ABI 不匹配、内存抖动和线程数的几个真问题5.1 现象模型加载成功但输出全部是错框挪到 App 里之后最常遇到的情况就是模型加载成功、推理也执行了但画出的框要么错位要么全部是 0。原因绝大多数时候是预处理没对齐。训练时图像会先做 letterbox再转成 RGB、除以 255而你在 Android 里拿到 Bitmap 是 BGRA 排列如果不做转换就直接丢给模型模型看到的是完全不同的数值分布。解决方法是严格复现训练时的预处理步骤先做 resize 或 letterbox再做颜色空间转换最后归一化。不要跳过任何一环这是目标检测端到端部署里翻车率最高的位置。一个常见的预处理片段ncnn::Mat in ncnn::Mat::from_pixels_resize(rgb.data, ncnn::Mat::PIXEL_RGB, width, height, target_w, target_h);from_pixels_resize 会同时做颜色转换和缩放缩放后你还要继续做归一化。如果训练时用的是 ImageNet 统计值那还要补上减均值和除方差float mean_vals[3] {0.485f, 0.456f, 0.406f}; float norm_vals[3] {0.229f, 0.224f, 0.225f}; in.substract_mean_normalize(mean_vals, norm_vals);不同训练代码的归一化方式可能不一样有的直接除以 255 后不处理均值有的使用这组 ImageNet 均值。写死任何一套都有可能出错最好从训练源码里把预处理那几行抄过来。5.2 现象App 内存持续上涨跑几分钟就卡死如果你的检测主循环放在 Java 层每次相机预览回调都 new 一个 Bitmap 或 ByteBuffer内存肯定上涨。原生层也一样每次推理都重新分配 ncnn::Mat而不是复用缓冲区内存碎片很快就堆起来。正确做法是在初始化阶段一次性申请好 buffer之后每帧只做数据拷贝循环使用。Java 侧可以用 BitmapFactory.Options.inBitmap 复用 BitmapNatvie 侧把推理输出放在一个固定大小的容器里。比如输出向量通常是一个固定 shape可以提前 reservestd::vectorfloat output_buffer; output_buffer.reserve(25200 * (5 class_count));每次推理前只 clear 不重新分配。这样既减少 GC也避免 native 内存反复 malloc。如果你看到内存曲线像锯齿一样上涨基本就是这一类问题。5.3 现象在手机上跑得慢十几帧每秒都达不到慢的原因要分开看。第一可能没有开启多线程。ncnn 默认线程数可能只有 1手动设置 detector.opt.num_threads std::min(4, get_cpu_core_count()) 能明显改善这也是最简单的调优手段。第二FP32 模型没有量化到 FP16 或 INT8在 CPU 上速度差别很大。第三如果输入分辨率太高手机算力扛不住。我一般会先把输入尺寸设成 416 或 320调通一版确认稳定后再往上加不要在刚开始跑的时候就和 640 分辨率死磕。另外不要把 ncnn 推理放在 Android 主线程。相机预览回调本身就要求低延迟你可以用一个 HandlerThread 来做推理或者用更轻量级的线程池否则一旦遇到掉帧整条链路都会连锁变慢。5.4 现象编译 ABI 时提示找不到 so 库在 Android Studio 里集成 C 推理库时出现 native library not found 报错很多时候是项目 jniLibs 目录里只放了 arm64-v8a而真机是 armeabi-v7a 架构或者 CMakeLists.txt 中的 ABI 过滤不匹配。解决方法是把不同架构的 so 文件放到对应目录或者在 build.gradle 里限制 abiFilters 为 arm64-v8a先保证一台测试机能跑通。常见配置写法android { defaultConfig { ndk { abiFilters arm64-v8a } } }如果你手头只有 64 位测试机这样写能减少很多兼容性问题。如果还要兼容老的 32 位设备就需要为 armeabi-v7a 也编译一套 so 文件。注意不要把 .a 静态库和 .so 动态库混在一起否则链接时报一堆 undefined reference排查起来很耗时。5.5 现象检测框整体偏移目标位置明显不对有时候模型输出的框存在但整体往左或往右偏一截多是因为 letterbox 产生的 padding 没被还原。在预处理阶段你把原图填充成正方形后原图的坐标相对于模型输入已经平移了一段距离后处理时如果直接把模型输出坐标当作原图坐标必然整体偏移。解决方法是记录 letterbox 时的缩放比例 scale 和填充的左右、上下边距 pad_w、pad_h然后在后处理里做逆变换x (x - pad_w) / scale; y (y - pad_h) / scale;很多项目在 PC 上直接用 cv2.dnn 跑通后搬到端侧就把这层逆转代码漏掉了因为 PC 端可能没做 letterbox 而是直接 reszie。记住凡是在前面加了填充和缩放后处理就必须补回来。6. 跑通之后的验证方法处理延时拆解、视频队列与一个固定测试图6.1 把整条链路拆成三段计时在模型跑通后不要只顾着看整体 FPS。把处理链路拆成预处理、推理、后处理三段分别计时才能定位瓶颈。代码示意auto t0 std::chrono::steady_clock::now(); preprocess(camera_frame, in_mat); auto t1 std::chrono::steady_clock::now(); net.run(in_mat, out_mat); auto t2 std::chrono::steady_clock::now(); decode_detections(out_mat, boxes); auto t3 std::chrono::steady_clock::now(); LOGI(preproc: %ld ms, infer: %ld ms, postproc: %ld ms, duration_caststd::chrono::milliseconds(t1 - t0).count(), duration_caststd::chrono::milliseconds(t2 - t1).count(), duration_caststd::chrono::milliseconds(t3 - t2).count());从计时结果能看出问题源头如果预处理占了 60% 时间那多半是颜色转换和内存拷贝写得低效如果推理本身占了 70% 以上就要考虑量化或降低输入尺寸。6.2 视频流里用队列避免阻塞相机回调线程是高频的如果把模型推理直接放在回调里画面会卡顿更容易丢帧。我习惯用生产者-消费者模式相机回调只负责把帧放进环形队列专门的推理线程从队列取帧并执行检测。队列长度控制在 3 到 5 帧超过就丢旧帧保证实时性。这个细节对演示流畅度影响很大值得花时间写。真机上如果发现画面延迟越来越大多半是消费速度跟不上生产速度这时候优先检查推理耗时而不是盲目增加线程数。6.3 一个固定测试图改一次跑一次我在部署阶段养成的习惯是准备一张包含多种目标的测试图放在固定路径每次修改模型或代码后先跑这张图的单帧推理确认输出框和上次一致再进入视频流或相机测试。这样可以隔离问题来源如果单帧都不对问题大概率在预处理或模型如果单帧正常但视频流卡才是线程或队列的问题。从那以后我每次改完模型都会强制走一遍这个流程先跑单帧再跑延时最后进相机实拍希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。