YOLO挖掘机目标检测实战:从模型训练到Flask网页部署

📅 发布时间:2026/8/27 2:28:54
YOLO挖掘机目标检测实战:从模型训练到Flask网页部署 简介计算机视觉在工业与工程机械场景中应用广泛其中目标检测技术承担着识别和定位重型设备的关键任务。YOLO作为兼顾检测精度与推理速度的主流算法能有效应对工地、矿区等复杂环境下的实时识别需求。深度学习模型的落地离不开工程化封装通过Python Web框架将训练好的权重文件封装为可交互的服务是打通算法与业务应用的重要环节。Flask轻量灵活适合快速搭建模型推理接口结合前端页面实现图片上传、检测结果可视化等完整链路。本文从目标检测的基本原理出发介绍YOLO模型的训练流程、数据准备要点及推理参数调优并详细说明如何利用Flask构建后端服务、封装模型调用接口、设计前端展示页面最终实现一个可运行的挖掘机识别演示系统。无论用于毕业设计还是工程视觉项目入门该方案都提供了清晰的实践路径和避坑参考。从训练到上线我如何用 YOLO 做挖掘机目标检测再用 Flask 搭出可交互的前端展示做工程机械相关的视觉项目有个很典型的痛点大型工地、矿区或者港口堆场需要监控挖掘机、装载机、自卸车这类重装设备的实时位置和作业状态却总不可能靠人眼盯着一堆监控屏。我之前接过一个项目需求很简单——用摄像头识别画面里的挖掘机标出位置和置信度给管理者一个能随时打开网页查看的展示界面。当时我首先想到的就是 YOLO 系列目标检测模型。这种场景里模型需要实时或准实时地跑YOLO 是公认在“精度”和“速度”之间平衡得最好的方案之一。而后端展示我选了 Flask不选 Django 也不选 FastAPI原因后面细说。最终做出来的效果就是用户打开本地网页上传一张工地照片或视频帧请求打到 Flask 后端后端调用训练好的 YOLO 模型做推理把检测结果和画了框的图片返回前端展示。整个链路算是一次典型的目标检测模型落地适合做毕设、入门项目也是工业视觉小应用的一个标准雏形。这篇文章就是把我从选型、训练、封装到 Flask 展示的那套流程完完整整拉出来讲一遍。涉及的代码、参数、踩坑记录我都会尽量还原保证你照着能做出来。1. 项目整体设计与方案选型1.1 为什么是 YOLO以及我为什么选 YOLOv8先回答一个不少人会问的问题目标检测模型那么多SSD、RetinaNet、Faster R-CNN为什么是 YOLO我的判断标准只有两条第一能不能在普通硬件上跑实时推理第二社区生态是否成熟遇到问题能不能很快找到解法。YOLO 在这两点上都占优。Faster R-CNN 精度确实高但速度慢单张图片在 CPU 上跑一次推理可能要好几秒不适合做交互式展示。SSD 速度还行但小目标检测一直弱一些挖掘机这种设备通常体积大、外形方正SSD 其实也能做但 YOLO 做起来更顺手。在 YOLO 具体版本上我选了 YOLOv8。Ultralytics 团队把 YOLOv5 和 v8 的生态维护得很完整pip 直接装、命令行直接训练、Python API 干净利落对快速搭项目非常友好。当时 YOLOv9、v10 也出来了但 v8 的文档和教程最成熟对新手最友好而且实测精度并不落下风。如果你的需求是识别挖掘机这种“中大尺寸目标”YOLOv8n 或 YOLOv8s 这种轻量级模型就够用完全没必要上最大的 x 版本后面我会给具体数据。1.2 Flask 在这个项目里到底扮演什么角色很多人习惯把 Flask 理解成一个“写 API 的工具”确实Flask 最核心的能力就是路由——把 URL 和 Python 函数绑定起来。但在我们这个项目里Flask 承担的其实是一个“模型服务网关”的角色接收前端上传的图片把图片转成模型能处理的格式调用 YOLO 模型推理把检测结果整理成 JSON 返回前端。为什么不用 Django因为这里没有任何数据库实体、用户系统、后台管理的需求Django 自带的 admin、ORM、中间件全是负担。为什么不用 FastAPIFastAPI 确实性能更好、自带 OpenAPI 文档但它的异步方式和 Pytorch 的模型推理混在一起处理不好反而容易出妖蛾子。Flask 的同步请求/响应模型在这种“请求过来、跑模型、返回结果”的场景里反而是最省心的。提示如果你的项目需要承受高并发请求比如同时上百路视频流访问那 Flask 同步网关会成瓶颈建议换成 FastAPI 异步推理队列。但如果是课堂演示、毕设展示、中小型工地监控Flask 完全够用。1.3 整体架构和目录结构我先说我建议的代码组织方式这个结构我调试过很多次清晰且容易扩展digger_detection/ ├── data/ │ ├── images/ # 原始图片 │ ├── labels/ # YOLO格式标注文件 │ ├── train.txt │ ├── val.txt │ └── digger.yaml # 数据集配置文件 ├── runs/ │ └── detect/ │ └── train/ # 训练产出权重、图表 ├── app.py # Flask主应用 ├── detector.py # YOLO模型封装类 ├── templates/ │ └── index.html # 前端展示页面 ├── static/ │ ├── css/ │ └── js/ └── requirements.txt整个数据流向是html页面 - HTTP请求 - Flask路由 - detector.py推理 - JSON/图片返回 - 前端渲染。下面每一层我都会拆开讲。2. 数据准备与模型训练全流程2.1 数据集从哪来怎么标注做挖掘机检测最忌讳一上来就想着自己拍几万张图。我的建议是分三步先用公开数据集打底。Roboflow、Kaggle 上都有挖掘机、工程车辆相关的数据集你可以搜索 “excavator detection dataset” 或 “construction machinery detection”。这类数据集大多是 YOLO 格式或者 COCO 格式下载下来清洗一下就能用。我当时用了一个包含挖掘机、装载机、推土机三个类别的数据集约 8000 张图片类别均衡度还行。然后再补充自己的数据。项目现场拍 200~500 张不同角度、不同距离、不同光照条件下的挖掘机图片。为什么一定要补充公开数据集里的挖掘机大多是完整的、正对着镜头的但真实监控里挖掘机往往被遮挡、逆光、或者只露出机械臂这部分数据公开集里很少。最后用 LabelImg 或 Roboflow 标注。我比较推荐 Roboflow 的在线标注工具因为标完可以直接导出 YOLOv8 格式省去转换格式的坑。标注时只需要注意一个原则挖掘机的“工作装置”和“上车体”如果连在一起就标一个框如果挖斗离得很远可以考虑单独标。不过我一般建议保守一点——只标整体这样模型的泛化能力更好不容易学习到破碎的局部特征。2.2 YOLO 格式和数据集配置YOLOv8 的标注格式是每个图片对应一个同名 .txt 文件每行代表一个目标class_id x_center y_center width height注意所有值都是相对于图片宽高的归一化值范围在 0 到 1 之间。比如一张 1920x1080 的图里一个挖掘机框的左上角在 (960, 540)右下角在 (1440, 810)那么标注就是0 0.625 0.625 0.25 0.25为什么用归一化坐标因为模型在训练时会把输入图片调整成正方形比如 640x640如果不归一化缩放后所有标注就全偏移了。这是初学者最容易踩的坑——标注完图片Visual Studio 里看着没问题一训练 loss 怎么都降不下来最后发现是标注格式错了。数据集配置文件 digger.yaml 长这样path: /path/to/digger_detection/data train: train.txt val: val.txt nc: 1 names: [excavator]注意 YOLOv8 的 path 是绝对路径如果你在多个机器间拷贝项目记得改这个文件不然训练时会报 “dataset not found” 或者莫名其妙加载了 0 张图片。2.3 训练配置和关键参数选择我是直接用 Ultralytics 的命令行工具训练的没有写额外的训练脚本命令是yolo detect train datadigger.yaml modelyolov8n.pt epochs100 batch16 imgsz640 device0几个关键参数的逻辑modelyolov8n.pt表示在 COCO 预训练权重的基础上做迁移学习。为什么不从零开始训练因为挖掘机这种目标其底层特征边缘、纹理、形状结构在 COCO 这种千万级数据集上已经学得很好了迁移过来只要微调高层特征就行训练速度快且精度高。imgsz640是 YOLOv8 默认输入尺寸。如果你的挖掘机在图中占比普遍很小比如监控视角拍远处的设备建议改成 960 或 1280小目标检测会有明显提升但训练和推理速度会下降。epochs100不是固定死的。我看训练曲线时一般等 loss 曲线不再下降再停。很多时候 50 个 epoch 就已经收敛了。训练过程会实时打印 mAP50、mAP50-95、precision、recall 这些指标。建议重点关注 mAP50它衡量的是 IoU 阈值 0.5 下的平均精度对挖掘机这种不需要像素级精确定位的场景mAP50 达到 90% 以上就非常好用了。训练完成后在runs/detect/train/weights/下面会生成best.pt和last.pt。自测的时候我强烈建议用best.pt别贪方便用 last。2.4 训练过程避坑记录训练这个环节我前后折腾了不少时间印象最深的有三个坑第一个是 batch size 过大导致显存溢出OOM。如果你的显卡只有 6GB 显存batch16 可能直接炸可以降到 8 或者 4或者开启梯度累积。Ultralytics 支持batch-1让它自动检测显存大小决定 batch我实测这个自动模式挺智能的可以放心用。第二个是类别不平衡。如果你训练集里挖掘机的图片有 7000 张但自卸车只有 100 张模型会倾向于把一切像“黄色大车”的东西都识别成挖掘机。解决方法是做数据增强或者简单地对少数类图片做过采样复制。第三个是验证集里出现训练集图片这会导致 metrics 虚高。用 Roboflow 导出数据时它会自动划分 train/val/test但如果你自己手动整理数据务必保证同一台挖掘机、同一个拍摄场景的相似图片不要同时出现在训练集和验证集里。视觉效果相近的图片会造成数据泄露模型看似 mAP 95%一到真实场景直接打回原形。3. 用 Flask 把模型包成服务3.1 模型封装类不要直接裸调模型很多人拿到 best.pt 之后直接在 Flask 路由里写model YOLO(best.pt)然后就开始 detect。这样跑通没问题但工程上很不优雅——模型应该被封装成一个独立的类把初始化、预加载、推理逻辑、结果格式化全部收敛到一起。这样未来你换模型、加预处理逻辑、写单元测试都只改一个文件。我写的detector.py大概是这个思路from ultralytics import YOLO class DiggerDetector: def __init__(self, model_path: str, conf_threshold: float 0.4): self.model YOLO(model_path) self.conf_threshold conf_threshold def predict(self, image_path: str): results self.model.predict( sourceimage_path, confself.conf_threshold, imgsz640, verboseFalse ) boxes [] for r in results: for box in r.boxes: x1, y1, x2, y2 box.xyxy.cpu().numpy().tolist()[0] conf float(box.conf.cpu().numpy()[0]) cls int(box.cls.cpu().numpy()[0]) boxes.append({ bbox: [int(x1), int(y1), int(x2), int(y2)], confidence: round(conf, 4), class: self.model.names[cls] }) return boxes这里有两个细节值得解释verboseFalse必须加。否则每次推理控制台都会刷一大段检测信息日志完全没法看。坐标先转成 int 再返回前端。坐标是 float 的话前端画框会带偏移而且 JSON 序列化时看着也不干净。3.2 Flask 路由设计Flask 应用本身可以非常薄核心就两个路由GET /返回前端页面POST /detect接收图片调用模型返回检测结果和标注后的图片我当时的app.py核心逻辑from flask import Flask, request, jsonify, render_template from detector import DiggerDetector import cv2 import os import base64 app Flask(__name__) detector DiggerDetector(runs/detect/train/weights/best.pt) app.route(/) def index(): return render_template(index.html) app.route(/detect, methods[POST]) def detect(): file request.files.get(image) if not file: return jsonify({error: no image uploaded}), 400 image_bytes file.read() temp_path temp_upload.jpg with open(temp_path, wb) as f: f.write(image_bytes) boxes detector.predict(temp_path) # 在原图上画框 img cv2.imread(temp_path) for box in boxes: x1, y1, x2, y2 box[bbox] cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 3) label f{box[class]} {box[confidence]:.2f} cv2.putText(img, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) _, buffer cv2.imencode(.jpg, img) encoded base64.b64encode(buffer).decode(utf-8) return jsonify({ boxes: boxes, image_base64: encoded }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse)几个关键点第一debugFalse。如果打开 debug 模式Flask 会开启代码热重载但这也意味着每次代码变更后它会在同一个端口重启两次而且 debug 模式下的交互式调试器有安全风险上线前必须关掉。第二host0.0.0.0。如果不写Flask 默认只监听 127.0.0.1那你只能在自己电脑上访问局域网里的同事、手机都访问不了。写 0.0.0.0 后同一局域网内可以通过http://你的IP:5000访问。第三为什么传 base64 而不是直接返回图片 URL因为 Flask 内置的静态文件服务不适合存放动态生成的结果图。如果每次检测都生成一张图存到 static 文件夹大量请求会堆积垃圾文件还得定期清理。base64 塞进 JSON 返回给前端前端直接解码渲染轻量且无状态。3.3 模型加载时机优化第一次启动 Flask 服务时加载 YOLO 模型可能需要 1~2 秒取决于硬件但之后的每次推理就很快了。所以要避免在路由函数内部加载模型——如果每个请求都从磁盘加载一次权重那速度会慢到让人怀疑人生。我把detector DiggerDetector(...)放在模块顶层这样 Flask 进程启动时就完成了模型加载请求进来直接推理延迟可以控制在几十到几百毫秒量级。注意如果你用 gunicorn 部署 Flask建议只开一个 worker。YOLO 模型加载到内存后每个 worker 会复制一份模型两个 worker 就意味着双倍显存/内存占用。对一个小型展示项目一个 worker 完全够用。4. 前端页面的设计与交互实现4.1 页面结构前端我没有用任何框架纯 HTML JavaScript 就够了因为功能非常简单引入 Vue 或者 React 反而把简单问题复杂化。index.html的核心结构!DOCTYPE html html head title挖掘机目标检测/title /head body h2挖掘机目标检测系统/h2 input typefile idinputImage acceptimage/* button iddetectBtn开始检测/button div idresultArea canvas idresultCanvas width640 height480/canvas p idresultInfo/p /div script src/static/js/main.js/script /body /html我选择用 canvas 来展示结果图而不是 img 标签。原因很简单canvas 可以直接绘制后的框叠加层方便后续如果想做“选中某个目标查看详细信息”的交互扩展性好很多。4.2 前端调用逻辑main.js的核心逻辑document.getElementById(detectBtn).addEventListener(click, async () { const fileInput document.getElementById(inputImage); if (!fileInput.files.length) { alert(请先选择一张图片); return; } const formData new FormData(); formData.append(image, fileInput.files[0]); const resp await fetch(/detect, { method: POST, body: formData }); const data await resp.json(); if (data.error) { alert(data.error); return; } // 解码base64图片 const img new Image(); img.onload function() { const canvas document.getElementById(resultCanvas); canvas.width img.width; canvas.height img.height; const ctx canvas.getContext(2d); ctx.drawImage(img, 0, 0); }; img.src data:image/jpeg;base64, data.image_base64; // 展示检测信息 const infoEl document.getElementById(resultInfo); infoEl.innerHTML 检测到 ${data.boxes.length} 个目标; data.boxes.forEach((box) { infoEl.innerHTML br类别: ${box.class}, 置信度: ${box.confidence}, 坐标: [${box.bbox}]; }); });一个常见的坑canvas 直接展示 base64 图片时如果图片本身特别大比如 4000x3000canvas 会非常大把页面撑爆。处理方案是给 canvas 设置 CSS 最大宽度比如max-width: 100%; height: auto;同时在 JavaScript 里也可以做缩放。另外如果检测时想同时支持“上传图片”和“摄像头实时画面”两种输入那摄像头视频流的实现就复杂了需要用到navigator.mediaDevices.getUserMediarequestAnimationFrame 定时抓帧。我在真实项目里做过但那种场景下 Flask 的同步接口就会成为瓶颈需要 WebSocket 或者轮询机制来推送视频帧。这篇文章不过度扩展如果你确实需要先看基础的图片上传跑通后再加视频流不然排查问题会特别痛苦。4.3 前后端联调时怎么排查联调时最常见的错误是跨域问题CORS。如果你用 Flask 跑在 5000 端口前端页面直接从file://协议打开fetch 请求大概率会报跨域错误。解决办法也很简单不要直接双击 html 文件打开而是通过 Flask 的http://localhost:5000/访问页面。这样前端和后端同源就不存在跨域问题了。如果实在需要前后端分离部署可以在 Flask 里安装flask-corspip install flask-cors然后在 app.py 里加上from flask_cors import CORS CORS(app)但个人项目里我建议能不加就不加同源部署最简单。5. 实测结果与性能调优5.1 推理耗时和检测效果我用一张 1920x1080 的真实工地照片做了测试模型是 YOLOv8n输入尺寸 640测试环境是 i7-12700 RTX 3060 显卡。结果如下模型版本推理耗时mAP50显存占用YOLOv8n45ms0.891约 1.2GBYOLOv8s70ms0.924约 2.5GBYOLOv8m130ms0.943约 4.8GB对挖掘机检测这个场景来说YOLOv8n 已经足够了。你在网页上点击上传图片、到页面显示结果整个过程大约 100ms 左右体验非常流畅。如果你用 CPU 推理耗时大概会到 1~3 秒也还能接受毕竟不是实时视频流。5.2 置信度阈值该设多少置信度阈值conf_threshold的设定直接影响误检率。如果设得太低比如 0.2模型会把很多背景物体——比如黄色的集装箱、水泥搅拌车——误判成挖掘机。如果设得太高比如 0.8又可能漏掉一些被遮挡的挖掘机。我的经验值是 0.4~0.5 之间。你可以先设 0.4 跑一批测试图如果发现误检多就往 0.6 调如果发现漏检多就往 0.3 调。这个没有绝对正确的值必须结合你自己的数据分布和场景来调。5.3 一个提高精度的技巧TTAUltralytics 的训练和推理都支持 Test Time Augmentation (TTA)简单说就是推理时把图片做多种变换翻转、缩放等把多次推理结果融合在一起。好处是精度会提高一些坏处是速度会慢好几倍。results model.predict( sourceimage_path, conf0.4, imgsz640, augmentTrue # 开启 TTA )我在实验中发现TTA 对挖掘机这种外形刚硬、方向性明显的物体提升不大可能只有 0.5~1 个百分点的 mAP 提升。所以正常情况下不建议开。如果你的场景是识别被严重遮挡的目标可以试试但要做好响应变慢的心里准备。6. 常见问题与排查技巧实录6.1 问题速查表我把这段时间遇到的高频问题整理成一个表格。你在复现项目时如果碰了壁先看这张表现象可能原因解决方法训练时 loss 变成 NaN学习率过大或数据有异常值将 lr 从默认的 0.01 降到 0.001检查数据集是否有全黑/全白图片检测框偏移框的位置和物体明显错位推理时输入尺寸与训练时不一致统一设置imgsz640Flask 页面能打开但点击检测没反应浏览器的 CORS 策略或 fetch 路径错误通过http://localhost:5000访问页面而不是 file://首次推理很慢模型加载时初始化 CUDA在 Flask 启动时就完成模型加载见 3.3 节上传大图时内存涨得厉害OpenCV 读取原始大图占用内存用 imread 后再做 resize限制最大边长不超过 1920检测结果 mAP 高但真实场景效果差数据泄露检查训练集和验证集是否有相似图片重新划分数据集两个挖掘机靠太近时只检出一个NMS 阈值过高降低iou参数比如model.predict(..., iou0.4)6.2 两个最值得展开的坑第一个坑是“训练时 loss 在下降但 mAP 一直为 0”。很多人以为是训练有问题但其实大部分情况是标注文件里 class_id 超出了nc数量。比如数据集配置里写了nc1但某一张标注文件里写了class_id2模型训练时根本不知道这个类别存在导致 loss 计算异常。排查方法写个小脚本扫描 labels 目录下所有 txt 文件检查 class_id 是否都小于 nc。import os label_dir data/labels nc 1 error_files [] for label_file in os.listdir(label_dir): with open(os.path.join(label_dir, label_file), r) as f: for line in f: cls int(line.split()[0]) if cls nc: error_files.append((label_file, cls)) print(error_files)第二个坑是 Flask 端口被占用。如果你的 5000 端口之前被其他进程占用启动服务会直接报Address already in use。当然可以换端口但我更推荐直接找到占用进程、把它停掉# Linux / macOS lsof -i :5000 kill -9 PIDWindows 上则是netstat -ano | findstr :5000 taskkill /PID PID /F6.3 模型效果不好时先别急着换大模型很多人训练完发现检测效果不行第一反应是换更大的模型。但实际上在挖掘机检测这个任务里模型容量的影响往往远小于数据质量的影响。如果你训练集里挖掘机只有单一角度、单一光线条件你换 YOLOv8x 一样会挂。所以我会建议按这个顺序排查问题先看训练集的多样性不同角度、不同环境、不同尺寸再看数据标注准确度框有没有紧贴物体最后才考虑模型容量。数据问题不解决换再大的模型也只是浪费显存。7. 后续扩展方向项目跑通之后如果你还愿意继续深入有两条很自然的延伸路径一是把静态图片检测扩展成视频流检测。方法是在前端用video标签 canvas.captureStream()每隔几帧抽一帧上传到 Flask 后端后端做推理后返回结果。但要注意这种方式实时性取决于网络延迟和推理速度在实际部署时建议用 WebSocket 或 RTMP 流媒体服务而不是 HTTP 轮询。二是把检测结果接入告警逻辑。比如挖掘机检测到后进一步判断它是否进入了某个电子围栏区域如果进入了就触发告警。这就需要在检测框的基础上做坐标映射和区域判断Flask 端加一个规则引擎就行。我当时做完这个项目最大的体会是目标检测模型本身只是第一步真正决定一个项目能不能落地的是模型与 Web 服务的衔接方式、前后端的交互设计、以及对各种异常情况的处理。把 Flask 玩熟其实和把 YOLO 训练好一样重要。希望这份记录能帮你少踩一些我踩过的坑。本文还有配套的精品资源点击获取