☰
基于YOLOv8的工地安全网漏洞检测实战:数据标注、小目标训练与可视化部署
2026/9/26 13:15:27 网站建设 项目流程

简介:基于YOLOv8的工地安全网漏洞检测是一套面向计算机视觉、人工智能、电子信息等专业学生的毕业设计和课程设计完整方案,聚焦工地安全网丢失、破损、漏洞等目标的检测识别,覆盖数据准备、模型训练、推理检测与结果可视化整个流程。压缩包共8个文件、整体约15.91MB,主要包含三类内容:Python程序(训练脚本、视频检测脚本、可视化页面设计)、YOLOv8系列权重文件以及说明文档;代码文件用于执行训练与检测,权重文件可直接加载推理,文本文件帮助快速了解目录结构与运行步骤。内置完整数据集和部署说明,代码经测试运行成功,能够输出损失曲线、混淆矩阵、F1分数曲线、精确率-召回率曲线、验证集预测结果以及标签分布图等一系列评估图表,便于在毕业设计答辩中直观呈现模型性能。目前已有33人学习下载,适合需要在较短时间内完成目标检测类毕设、课设或深度学习教育项目的人群,也可作为二次开发与算法选型的参考起点。

1. 基于YOLOv8的工地安全网漏洞检测,先解决三个前置问题

工地安全网漏洞检测,动手做起来会发现问题不在模型套用,而在数据标注、小目标漏检和最终交付三块。安全网上的孔洞和破口在监控画面里往往只占几十个像素,光线不足时连人眼都看不准,绿网本身密集的网格纹理又会干扰检测器;而毕设或课设最后要交付的是带可视化界面、能简单部署运行的完整包,不只是训练脚本。基于YOLOv8做这个题目,需要把施工安全数据集准备、YOLOv8训练自己的数据集的入口、检测脚本和界面串成一条能落地的链路。下面就从这条链路出发,给出每一步的选型、参数和避坑方法。

2. 构建工地安全网数据集:采集、标注与YOLOv8格式转换

2.1 先盘点数据:优先用现成的施工安全数据集,再补自己拍的帧

工地安全网的公开专门数据集非常少,更多是以“施工安全数据集”的形式散落在各种课题和竞赛里,里面通常包含了安全帽、反光衣、挖掘机等类别,其中有些帧里正好带安全网,但没有漏洞标注。常见做法是先把这类图片筛选出来,再用标注工具补标漏洞区域,这样比完全从零采集照片省力得多。我一般会按这样的优先级收集:

  1. 学校或导师提供的现场照片,这是最贴合实际的数据,但往往量少且视角单一;
  2. 公开的工地安全数据集,筛选出包含绿色密目网、高空作业平台、脚手架的区域,进一步裁剪或抽帧;
  3. 自己用手机或监控录像拍视频,每隔3到5秒抽一帧,制造不同光线和遮挡条件。

目标检测训练对图片量的要求并不苛刻,单类别“漏洞”建议有300到500个标注实例。如果目标太小,200张图也能跑通YOLOv8训练流程,只是mAP波动会很大。还有一个容易忽略的点:工地安全网的“漏洞”类别定义要统一。有的标注者会把整片破损的网子标成一个框,有的则把每个洞分开标,这会让模型无所适从。建议只设一个类别hole,并约定“网面出现明显破口,且破口最短边大于20像素”才标注,小于这个尺寸的噪点交给模型去忽略。

2.2 标注工具选择与类别定义

目前给YOLOv8默认格式做标注,最省事的是LabelImg和X-AnyLabeling。LabelImg能直接保存为Pascal VOC XML,后续转YOLO格式很稳定;X-AnyLabeling支持更快的半自动标注,适合需要补充大量框的场景。不建议用通用图像标注软件输出COCO JSON再做复杂转换,除非你准备做实例分割。

标注时有三个实操经验。第一,安全网本身有规律的网格纹理,容易把网格交点误标成洞,标注时要把框贴住破口的实际边缘,宁可稍微内缩,也不要包住完整网格。第二,一张图里可能有多个紧挨着的洞,如果粘连严重,可以合并成一个破损区域,模型对小区域的置信度会更稳定。第三,类别名不要用中文,模型和界面显示另做映射,训练配置里统一用小写英文字母,例如break、hole、net_defect,避免编码问题。

2.3 转成YOLOv8训练格式:从VOC XML到txt的完整脚本

LabelImg保存的是VOC XML,YOLOv8需要的是归一化的txt文件。格式为一行一个目标,内容是class_id x_center y_center width height,所有坐标值都在0到1之间。下面这段脚本可以把VOC XML转换到YOLO txt,脚本本身只需要xml.etree和os、shutil,不依赖额外库。

import os import xml.etree.ElementTree as ET from pathlib import Path CLASSES = {"hole": 0} # 只有漏洞一个类别,id从0开始 def convert_voc_xml_to_yolo(xml_path, out_dir): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) lines = [] for obj in root.findall("object"): name = obj.find("name").text.strip() if name not in CLASSES: continue # 过滤掉不需要的类别 class_id = CLASSES[name] 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) # 转成归一化yolo坐标,并且防止越界 x_center = ((xmin + xmax) / 2) / img_w y_center = ((ymin + ymax) / 2) / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") out_path = Path(out_dir) / (Path(xml_path).stem + ".txt") out_path.write_text("\n".join(lines), encoding="utf-8") print(f"converted {xml_path} -> {out_path}") # 使用示例:遍历所有xml for xml_file in Path("annotations/voc").glob("*.xml"): convert_voc_xml_to_yolo(str(xml_file), "labels")

这段脚本的核心是先从XML根节点读取图片宽高,然后遍历每个object,取出bndbox的四个坐标。YOLO格式要求中心点坐标和宽高都相对原图归一化,所以每一处都要除以img_w或img_h。如果标注工具导出的坐标越界,比如xmax比图片宽度还大,脚本里最好加一个clip操作,否则训练时会出现警告且损失不收敛。也可以用同样的方式把XML替换成JSON,只要提取出相同信息即可,核心思路不变。

2.4 数据划分与目标尺寸统计

数据准备好后,把图片和同名txt放到images和labels两个目录,并将样本按8:1:1切分成train、val、test。下面这段脚本同时输出目标尺寸分布,帮助你判断是否需要切图或提高输入分辨率。

import random, shutil, glob from collections import Counter images = glob.glob("safety_net/images/*.jpg") random.seed(42) random.shuffle(images) train, val, test = images[:int(len(images)*0.8)], images[int(len(images)*0.8):int(len(images)*0.9)], images[int(len(images)*0.9):] for split, paths in [("train", train), ("val", val), ("test", test)]: for img in paths: shutil.copy(img, f"dataset/{split}/images/") txt = img.replace("images", "labels").replace(".jpg", ".txt") shutil.copy(txt, f"dataset/{split}/labels/") # 打印目标宽高的像素分布,判断是否属于小目标 sizes = [] for txt in glob.glob("dataset/train/labels/*.txt"): with open(txt) as f: for line in f: parts = line.strip().split() w, h = float(parts[3]), float(parts[4]) sizes.append((w, h)) counter = Counter(round(w, 2) for w, _ in sizes) print(counter.most_common(10))

这段代码在划分时直接复制了文件,实际使用时要注意先创建好目录结构。target尺寸打印出来的是归一化宽高,比如0.02左右对应640像素为13像素左右,属于典型小目标。如果大量目标都集中在0.02以下,后面训练时建议把imgsz调到1280,或者把原图切成几块再训练,否则YOLOv8的默认特征层很难捕获到足够信息。

3. YOLOv8训练自己的数据集:模型配置、参数调整与训练监控

3.1 选择yolov8s还是yolov8n,先理解C2f结构对漏检的影响

YOLOv8训练自己的数据集,第一个选择是模型尺寸。n、s、m、l、x之间的差异主要在通道数和可训练层数,而YOLOv8网络结构图里最核心的组件是C2f模块。C2f把输入特征图拆成两条支路,一条直接经过卷积,另一条通过Bottleneck堆叠并横向拼接,最后再concat起来。这种设计在轻量化的同时保留了更多梯度流,比上一代C3更擅长捕捉细粒度纹理。安全网漏洞很难用单一几何特征描述,网格纹理和破损区域的纹理差异非常细致,所以C2f的输出丰富度直接影响小目标召回。

模型参数量速度小目标表现建议
yolov8n最小最快漏检较多显存有限或只需要演示流程
yolov8s较小快适中毕设/课设默认推荐
yolov8m中等中等更好有6GB以上显存可尝试
yolov8l/x大慢最好不推荐,部署包会膨胀

如果你的显卡是GTX 1660 Ti这样的入门卡,跑yolov8s 640分辨率训练一版大约需要3到4小时,yolov8n会更快。后续推理阶段是CPU还是GPU也会影响选择,尽量在项目一开始就定好:如果给用户的是普通笔记本,就选yolov8n或s;如果服务端GPU部署,可以直接升到m。

3.2 准备data.yaml并启动训练

YOLOv8用ultralytics库训练,最小配置是一份data.yaml和一个预训练权重。data.yaml里要写清楚训练集图片路径、验证集图片路径、类别数量和类别名。注意路径尽量用绝对路径,防止跨目录运行时找不到数据。

train: D:/safety_net/dataset/train/images val: D:/safety_net/dataset/val/images nc: 1 names: 0: hole

然后启动训练:

pip install ultralytics yolo detect train data=safety_net.yaml model=yolov8s.pt epochs=100 batch=16 imgsz=640 device=0

这里device=0表示使用第一块GPU,纯CPU训练就把device设为cpu并把batch降到4。训练过程中会在runs/detect/train目录生成weights/best.pt和weights/last.pt,best.pt是根据验证集指标保存的最优权重,后续可视化界面和部署都用它。如果训练到一半需要恢复,用resume=True加上原来的命令行参数即可,不需要重新标注数据。

3.3 需要调整的关键参数:epoch、batch、imgsz、lr0与patience

只靠默认参数也能训练,但要达到“漏洞看得准”的效果,至少把下面这张表调一遍:

参数默认值建议值理由
epochs100150~200小目标数据集收敛更慢,太早停下会欠拟合
batch8显存允许就16或32batch太小BN不稳定,安全网纹理容易导致指标震荡
imgsz640640或1280目标占比小,提高分辨率比加网络深度更直接
lr00.010.005~0.01标注数量少,学习率过大容易过拟合
patience10020提前终止,避免最后几十轮浪费训练时间
degrees045施工现场拍摄角度变化大,适当旋转帮助泛化

上面参数都是通过命令行传入的,例如yolo detect train ... epochs=200 imgsz=1280 patience=20。这里特别说明imgsz:安全网漏洞如果平均只有20像素,把输入从640提升到1280,等于目标相对尺寸扩大了一倍,比换更大的模型收益更明显。缺点是显存占用接近翻倍,1660 Ti这类卡建议保持640并同时使用切图策略。degrees这类增强参数可以加到训练命令中,增加旋转模拟不同视角。如果训练过程中loss出现nan,优先检查batch是否过大、学习率是否超过0.01、数据标注是否有空txt文件混入。

3.4 用训练日志画损失函数曲线图,判断是否过拟合

YOLOv8每次训练结束后都会在results.csv里保存每个epoch的box_loss、cls_loss、dfl_loss以及mAP50等指标。想确认模型是不是在后期过拟合,可以用pandas和matplotlib读取csv并画出曲线图。

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") df.columns = [c.strip() for c in df.columns] # 去掉列名空格 fig, axes = plt.subplots(1, 2, figsize=(12, 4)) axes[0].plot(df["epoch"], df["train/box_loss"], label="train box_loss") axes[0].plot(df["epoch"], df["val/box_loss"], label="val box_loss") axes[1].plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50") axes[0].legend(); axes[1].legend() plt.savefig("loss_curve.png", dpi=150)

这段脚本主要关注两个信号:验证集的box_loss如果先下降后回升,说明模型开始死记训练数据,应该减小lr0或增加增强;mAP50曲线在20轮内没有上升趋势,则先检查data.yaml路径是否指向val目录、类别名是否匹配,再考虑增大imgsz。训练画损失函数曲线图也是答辩时的必要素材,建议把train和val两条线放在一起,评委能直接看到过拟合与否。

4. 将YOLOv8检测结果做成可视化界面:PyQt与实时推理

4.1 最小推理脚本:用训练好的best.pt检测漏洞并画框

在写界面之前,先把模型推理跑通。YOLOv8的Ultralytics API封装得很干净,一个模型对象就能完成前处理、推理、后处理。下面是最小可运行脚本:

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="data/test/images/001.jpg", conf=0.25, iou=0.45, imgsz=640, save=True, project="runs/detect", name="predict" ) print(results[0].boxes.xyxy)

这里的conf是置信度阈值,只保留得分大于0.25的框;iou是非极大值抑制的IoU阈值,漏洞框挨得近时调高到0.5以上,否则可能合并成一个大框;save=True会自动把画好框的图保存到runs/detect/predict。第一次运行会自动下载模型结构定义,但best.pt已包含权重,所以离线环境也能推理。如果处理的是视频,把source改成视频文件路径即可,ultralytics内部会自动抽帧并写回视频。批量测试时可以用source="data/test/images"指向整个目录,结果会全部输出到指定项目路径。

4.2 可视化界面选型:PyQt和Web界面的取舍

毕设或课设里的“可视化界面”通常要求能上传图片、显示检测结果和统计信息。我一般会优先选择PyQt5或PySide6,原因是打包成exe时依赖更清晰,后续接摄像头也比较自然。如果你只做一个演示面板,也可以用Streamlit或Gradio,代码量更少,但最终交付时要让用户装Python环境,不算“简单部署即可运行”。下面表格是两种路线的对比:

方案开发速度打包体积摄像头适合场景
PyQt5 + OpenCV中等50MB以上方便需要交付exe的毕设
Streamlit很快不需要打包受限演示模型效果
Flask + Vue慢需要Node环境不方便Web端多人访问

如果只是课程设计,Streamlit足够;如果是毕设,建议用PyQt,因为答辩时现场环境无法预测,浏览器依赖少。下面以PyQt5为例给出核心代码。

4.3 用PyQt搭建一个带文件选择和置信度滑块的界面

import sys from PyQt5.QtWidgets import QApplication, QMainWindow, QLabel, QPushButton, QSlider, QFileDialog, QVBoxLayout, QWidget from PyQt5.QtGui import QImage, QPixmap from ultralytics import YOLO class SafetyNetApp(QMainWindow): def __init__(self): super().__init__() self.model = YOLO("best.pt") self.image_label = QLabel() self.open_btn = QPushButton("选择图片") self.conf_slider = QSlider() self.conf_slider.setRange(1, 99) self.conf_slider.setValue(25) self.open_btn.clicked.connect(self.detect) layout = QVBoxLayout() layout.addWidget(self.image_label) layout.addWidget(self.open_btn) layout.addWidget(self.conf_slider) container = QWidget() container.setLayout(layout) self.setCentralWidget(container) def detect(self): path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "Images (*.jpg *.png)") conf = self.conf_slider.value() / 100.0 results = self.model.predict(path, conf=conf) img = results[0].plot() h, w, c = img.shape qimg = QImage(img.data, w, h, 3 * w, QImage.Format_RGB888).rgbSwapped() self.image_label.setPixmap(QPixmap.fromImage(qimg)) if __name__ == "__main__": app = QApplication(sys.argv) win = SafetyNetApp() win.show() sys.exit(app.exec_())

上面这个界面把模型创建放到了__init__里,避免了点击一次加载一次权重。QSlider的值除以100后作为置信度阈值,设置范围1到99,对应0.01到0.99。detect方法里调用了results[0].plot(),Ultralytics会自动把原始检测结果绘制成带框的BGR图,转成QImage时通过rgbSwapped变成RGB,否则颜色会偏蓝。更完整的界面还能在状态栏显示检测数量,实际项目里用results[0].boxes.shape[0]即可。

4.4 摄像头实时检测:QThread与坐标缩放的常见坑

实时检测不能把模型predict放在UI线程里,否则拖动窗口会卡死。常见做法是单独开一个QThread,在run方法里循环读取摄像头帧,再把检测后的帧通过信号发给主线程更新画面。核心代码如下:

from PyQt5.QtCore import QThread, pyqtSignal import cv2 class DetectThread(QThread): frame_ready = pyqtSignal(object) def run(self): cap = cv2.VideoCapture(0) while not self.isInterruptionRequested(): ok, frame = cap.read() if not ok: break results = self.model.predict(frame, imgsz=640, verbose=False) frame = results[0].plot() self.frame_ready.emit(frame)

这里要注意,QThread内部如果每帧都调用model.predict,单帧耗时可能达到50ms以上,界面看起来只有十几帧。更好的做法是控制推理频率,比如每隔2帧再检测一次,中间帧直接显示上一帧结果。另一个容易被忽视的点是坐标缩放:如果模型输入是640x640,而原图是1920x1080,predict返回的xyxy坐标是相对于原图的,YOLOv8会在内部还原,所以不要在界面上手动除以比例。如果为了显示窗口把图像缩放过,画框后再整体缩放会造成框偏移,应该先在原图坐标画框,最后再resize整个画面。

5. 把项目完整部署到另一台电脑:环境固化、PyInstaller打包与路径修复

5.1 用requirements.txt和conda export固化环境

“简单部署即可运行”指的不是把源码发给别人再现场配环境,而是把依赖固定成一包。我一般会在项目根目录生成requirements.txt,内容至少包含ultralytics、PyQt5、opencv-python。如果用的是conda,可以用conda env export > environment.yml,然后再配合pip freeze生成精确到版本号的清单,防止依赖版本互相冲突。离线部署时还要提前下载对应平台和Python版本的wheel包,到目标机器上用pip install --no-index --find-links安装。

5.2 用PyInstaller打包GUI程序

pyinstaller -w -F main.py --name SafetyNetDetector --collect-all ultralytics --collect-all PyQt5 --add-data "best.pt;."

-w表示无控制台窗口,-F打包成单个exe,--collect-all会把ultralytics内置的配置文件和动态库都收进包,PyQt5也要同类收集。--add-data把best.pt放在解压临时目录,最终运行时需要通过sys._MEIPASS定位。

import sys, os base_path = getattr(sys, "_MEIPASS", os.path.dirname(os.path.abspath(__file__))) model_path = os.path.join(base_path, "best.pt")

5.3 常见部署失败:模型路径、Qt插件和算力设备

打包后最常见的报错是找不到模型文件,原因是PyInstaller不会自动包含非代码资源。将模型放在base_path下后,再也不要假设当前工作目录是exe所在目录。Qt插件缺失会导致运行时提示“platform plugin windows”无法加载,添加--collect-all PyQt5后基本能解决。如果目标机器没有NVIDIA显卡,程序会退到CPU推理,速度会慢3到5倍,需要在界面上显示当前推理设备,并在代码里用torch.cuda.is_available()做判断。

5.4 部署自检:首次启动时自动验证

最后在程序启动时增加一个自检函数,检查模型文件、测试图片、设备类型和推理耗时,能省掉大量远程排查时间:

import time, torch import numpy as np def selfcheck(model_path): if not os.path.exists(model_path): raise FileNotFoundError("best.pt missing") model = YOLO(model_path) img = np.zeros((640, 640, 3), dtype=np.uint8) t0 = time.time() model.predict(img, verbose=False) print(f"inference device: {torch.cuda.get_device_name(0) if torch.cuda.is_available() else 'cpu'}") print(f"warmup cost: {time.time() - t0:.3f}s")

如果你需要把检测放到rk3588这类边缘板子上,就不要带PyQt界面,先把best.pt导出为ONNX,再用rknn-toolkit2转成RKNN格式,输入尺寸按480对齐。换模型文件比改代码更容易迁移,但前提是训练时imgsz和导出时保持一致。

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

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

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

立即咨询