YOLO挖掘机目标检测实战:从模型训练到Flask网页部署
2026/8/27 2:28:39 网站建设 项目流程

简介:计算机视觉在工业与工程机械场景中应用广泛,其中目标检测技术承担着识别和定位重型设备的关键任务。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、中间件全是负担。为什么不用 FastAPI?FastAPI 确实性能更好、自带 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 data=digger.yaml model=yolov8n.pt epochs=100 batch=16 imgsz=640 device=0

几个关键参数的逻辑:

  • model=yolov8n.pt表示在 COCO 预训练权重的基础上做迁移学习。为什么不从零开始训练?因为挖掘机这种目标,其底层特征(边缘、纹理、形状结构)在 COCO 这种千万级数据集上已经学得很好了,迁移过来只要微调高层特征就行,训练速度快且精度高。
  • imgsz=640是 YOLOv8 默认输入尺寸。如果你的挖掘机在图中占比普遍很小(比如监控视角拍远处的设备),建议改成 960 或 1280,小目标检测会有明显提升,但训练和推理速度会下降。
  • epochs=100不是固定死的。我看训练曲线时,一般等 loss 曲线不再下降再停。很多时候 50 个 epoch 就已经收敛了。

训练过程会实时打印 mAP50、mAP50-95、precision、recall 这些指标。建议重点关注 mAP50,它衡量的是 IoU 阈值 0.5 下的平均精度,对挖掘机这种不需要像素级精确定位的场景,mAP50 达到 90% 以上就非常好用了。

训练完成后,在runs/detect/train/weights/下面会生成best.ptlast.pt。自测的时候我强烈建议用best.pt,别贪方便用 last。

2.4 训练过程避坑记录

训练这个环节我前后折腾了不少时间,印象最深的有三个坑:

第一个是 batch size 过大导致显存溢出(OOM)。如果你的显卡只有 6GB 显存,batch=16 可能直接炸,可以降到 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( source=image_path, conf=self.conf_threshold, imgsz=640, verbose=False ) 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

这里有两个细节值得解释:

  • verbose=False必须加。否则每次推理,控制台都会刷一大段检测信息,日志完全没法看。
  • 坐标先转成 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(host="0.0.0.0", port=5000, debug=False)

几个关键点:

第一,debug=False。如果打开 debug 模式,Flask 会开启代码热重载,但这也意味着每次代码变更后它会在同一个端口重启两次,而且 debug 模式下的交互式调试器有安全风险,上线前必须关掉。

第二,host="0.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 type="file" id="inputImage" accept="image/*"> <button id="detectBtn">开始检测</button> <div id="resultArea"> <canvas id="resultCanvas" width="640" height="480"></canvas> <p id="resultInfo"></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 图片时,如果图片本身特别大(比如 4000x3000),canvas 会非常大,把页面撑爆。处理方案是给 canvas 设置 CSS 最大宽度,比如max-width: 100%; height: auto;,同时在 JavaScript 里也可以做缩放。

另外,如果检测时想同时支持“上传图片”和“摄像头实时画面”两种输入,那摄像头视频流的实现就复杂了,需要用到navigator.mediaDevices.getUserMedia+requestAnimationFrame+ 定时抓帧。我在真实项目里做过,但那种场景下 Flask 的同步接口就会成为瓶颈,需要 WebSocket 或者轮询机制来推送视频帧。这篇文章不过度扩展,如果你确实需要,先看基础的图片上传,跑通后再加视频流,不然排查问题会特别痛苦。

4.3 前后端联调时怎么排查

联调时最常见的错误是跨域问题(CORS)。如果你用 Flask 跑在 5000 端口,前端页面直接从file://协议打开,fetch 请求大概率会报跨域错误。解决办法也很简单:不要直接双击 html 文件打开,而是通过 Flask 的http://localhost:5000/访问页面。这样前端和后端同源,就不存在跨域问题了。

如果实在需要前后端分离部署,可以在 Flask 里安装flask-cors

pip 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.2GB
YOLOv8s70ms0.924约 2.5GB
YOLOv8m130ms0.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 一个提高精度的技巧:TTA

Ultralytics 的训练和推理都支持 Test Time Augmentation (TTA),简单说就是推理时把图片做多种变换(翻转、缩放等),把多次推理结果融合在一起。好处是精度会提高一些,坏处是速度会慢好几倍。

results = model.predict( source=image_path, conf=0.4, imgsz=640, augment=True # 开启 TTA )

我在实验中发现,TTA 对挖掘机这种外形刚硬、方向性明显的物体提升不大,可能只有 0.5~1 个百分点的 mAP 提升。所以正常情况下不建议开。如果你的场景是识别被严重遮挡的目标,可以试试,但要做好响应变慢的心里准备。

6. 常见问题与排查技巧实录

6.1 问题速查表

我把这段时间遇到的高频问题整理成一个表格。你在复现项目时如果碰了壁,先看这张表:

现象可能原因解决方法
训练时 loss 变成 NaN学习率过大或数据有异常值将 lr 从默认的 0.01 降到 0.001,检查数据集是否有全黑/全白图片
检测框偏移,框的位置和物体明显错位推理时输入尺寸与训练时不一致统一设置imgsz=640
Flask 页面能打开,但点击检测没反应浏览器的 CORS 策略或 fetch 路径错误通过http://localhost:5000访问页面,而不是 file://
首次推理很慢模型加载时初始化 CUDA在 Flask 启动时就完成模型加载,见 3.3 节
上传大图时内存涨得厉害OpenCV 读取原始大图占用内存用 imread 后再做 resize,限制最大边长不超过 1920
检测结果 mAP 高但真实场景效果差数据泄露检查训练集和验证集是否有相似图片,重新划分数据集
两个挖掘机靠太近时只检出一个NMS 阈值过高降低iou参数,比如model.predict(..., iou=0.4)

6.2 两个最值得展开的坑

第一个坑是“训练时 loss 在下降,但 mAP 一直为 0”。很多人以为是训练有问题,但其实大部分情况是标注文件里 class_id 超出了nc数量。比如数据集配置里写了nc=1,但某一张标注文件里写了class_id=2,模型训练时根本不知道这个类别存在,导致 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 <PID>

Windows 上则是:

netstat -ano | findstr :5000 taskkill /PID <PID> /F

6.3 模型效果不好时,先别急着换大模型

很多人训练完发现检测效果不行,第一反应是换更大的模型。但实际上,在挖掘机检测这个任务里,模型容量的影响往往远小于数据质量的影响。如果你训练集里挖掘机只有单一角度、单一光线条件,你换 YOLOv8x 一样会挂。

所以我会建议按这个顺序排查问题:先看训练集的多样性(不同角度、不同环境、不同尺寸),再看数据标注准确度(框有没有紧贴物体),最后才考虑模型容量。数据问题不解决,换再大的模型也只是浪费显存。

7. 后续扩展方向

项目跑通之后,如果你还愿意继续深入,有两条很自然的延伸路径:

一是把静态图片检测扩展成视频流检测。方法是在前端用<video>标签 +canvas.captureStream(),每隔几帧抽一帧上传到 Flask 后端,后端做推理后返回结果。但要注意,这种方式实时性取决于网络延迟和推理速度,在实际部署时建议用 WebSocket 或 RTMP + 流媒体服务,而不是 HTTP 轮询。

二是把检测结果接入告警逻辑。比如挖掘机检测到后,进一步判断它是否进入了某个电子围栏区域,如果进入了就触发告警。这就需要在检测框的基础上做坐标映射和区域判断,Flask 端加一个规则引擎就行。

我当时做完这个项目最大的体会是:目标检测模型本身只是第一步,真正决定一个项目能不能落地的是模型与 Web 服务的衔接方式、前后端的交互设计、以及对各种异常情况的处理。把 Flask 玩熟,其实和把 YOLO 训练好一样重要。希望这份记录能帮你少踩一些我踩过的坑。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询