简介:面向高校毕业设计、课程设计与项目开发的基建裂缝目标检测系统,基于Python与YOLOv8实现,将数据准备、模型训练、评估与结果可视化整合为完整闭环,适合土木工程或计算机视觉方向的本科生、研究生及开发者直接参考。包体共850个文件,约666MB,其中329张jpg图像配合299个txt及158个xml标注构成训练数据,23个pt权重与4个yaml配置用于模型加载与推理,7个py脚本和开发文档md文件可帮助理解代码结构与快速改造,csv结果记录便于核对各项训练指标。系统源码已经严格测试,可直接运行,并在原框架基础上延伸使用,对想要快速搭建裂缝检测基准方案、完成课程报告或毕设模块的读者很有价值。已有206人在线学习,说明其真实可用程度较高,是一份能落地的实践型资源。
1. 基建裂缝目标检测为什么从yolov8起步:先解决“能不能用”
基建裂缝目标检测在公路、桥隧和房屋巡检里被反复提起,不是因为模型本身多难,而是人工目检的真实成本摆在那:一堵三百米的挡墙,两个人拿裂缝测宽仪走一上午,回来还要对着手写记录重新录入。用目标检测先在现场照片里把可疑裂缝区域自动框出来,巡检人员只需要复核框内位置,这是目前最成熟的自动化路线,而 yolov8 是这个方向上性价比很高的切入点。它训练生态完整、部署链路短,本科毕设或课程设计拿一张普通显卡,哪怕只有 CPU,也能从零跑通一个带标注、带训练、带评估、带展示的完整项目。
这篇文章适合两类人:一是正在做毕业设计或课程设计,需要一个“从数据集到结果展示”都说得通的项目;二是刚接触目标检测的从业者,想用基建场景练手,同时搞清楚细长目标检测和常规目标检测的差别。下面按数据制备、训练调参、结果评估、部署交付四个环节拆开讲,最后给一份我常用的训练日志可视化技巧。
2. 数据制备:现场照片、labelme标注与YOLO格式转换,这步决定模型上限
2.1 现场照片怎么拍才够用:光源、距离和背景多样性
裂缝检测的目标是细长线状物,和检测行人、车辆完全不同。行人是紧凑目标,一个框大致包住就好;裂缝可能横跨半张图,也可能只有十几个像素宽。数据采集阶段就要围绕这个特点设计。
我一般会按三个维度控制采集:光源条件,至少覆盖顺光、逆光、阴天三种;拍摄距离,近景 0.5 米到 1 米拍细节,中景 2 米到 3 米拍断裂趋势;背景种类,混凝土墙面、沥青路面、砖石砌体、水泥抹面都要有,因为模型在训练时会把背景纹理一起学进去。如果只拍一种表面,换到现场就容易翻车。
数量上,一个能支撑毕设演示的项目,500 到 1500 张原图比较合理。每张图上的裂缝目标尽量控制在 1 到 5 个,目标太密会让标签互相覆盖,太疏则训练时正样本不足。清洗规则要狠:虚焦的、过曝的、被树叶或阴影大面积遮挡的,直接删除,不要在清洗阶段心疼数据量。模糊图混进训练集,后果是模型学到“边缘模糊就是裂缝”。
2.2 labelme标注规则:一个框还是两个框,框边留多少
标注工具用 labelme 最省事,因为它的 json 格式里直接用像素坐标记录 shape,转成 YOLO 格式时逻辑透明。
标矩形框时有两条规则我建议坚持。第一条,框要贴着裂缝外接矩形,四个边离裂缝边缘不要超过 10 个像素。很多人习惯性留大片空白,结果 YOLO 的 anchor 匹配时中心点落在背景上,正样本质量被稀释,小裂缝直接学不到。第二条,一条裂缝断成几段时,如果段间距小于裂缝宽度的 3 倍,合进一个框;如果间距明显大于裂缝宽度,拆成多个框。这样模型学到的是“裂缝趋势是连续的”,而不是把每一小段当成独立物体。
类别命名上,如果只想完成毕设展示,单类 crack 就够;如果想做得更有区分度,可以拆成 crack_h 和 crack_v 两个类别,横向和纵向分开统计。建议初期只标 crack 一类,先把链路跑通,再考虑细分类。
2.3 把labelme的json转成YOLO格式:坐标换算与异常过滤
labelme 的 json 里存的是原始像素坐标,而 YOLO 训练需要的是归一化的中心点坐标和宽高。转换脚本的核心是:读取 json 的 shapes,取每个标注的外接矩形,再除以图片宽高得到 0 到 1 之间的数值。
import json import os import glob from pathlib import Path def convert_labelme_json(json_path, out_dir, class_map): # class_map 形如 {"crack": 0},类别名映射到类别编号 with open(json_path, "r", encoding="utf-8") as f: data = json.load(f) img_w = data["imageWidth"] img_h = data["imageHeight"] img_name = Path(json_path).stem # json文件名与图片同名 out_line = [] for shape in data["shapes"]: label = shape["label"] if label not in class_map: continue points = shape["points"] # labelme 的矩形用两个对角点表示,这里兼容多边形取外接矩形 xs = [p[0] for p in points] ys = [p[1] for p in points] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) # 转成 YOLO 的 cx, cy, w, h 并归一化 cx = (x_min + x_max) / 2.0 / img_w cy = (y_min + y_max) / 2.0 / img_h w = (x_max - x_min) / img_w h = (y_max - y_min) / img_h # 过滤异常:宽高为0的框会让训练时loss变成nan if w <= 0 or h <= 0: print(f"skip invalid box: {json_path}, label={label}") continue out_line.append(f"{class_map[label]} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") if out_line: out_path = Path(out_dir) / f"{img_name}.txt" with open(out_path, "w", encoding="utf-8") as f: f.write("\n".join(out_line))遍历标注目录时这样调用:
labelme_dir = "./labelme_jsons" out_dir = "./labels/train" class_map = {"crack": 0} os.makedirs(out_dir, exist_ok=True) for json_path in glob.glob(str(Path(labelme_dir) / "*.json")): convert_labelme_json(json_path, out_dir, class_map)这段脚本里最容易踩的坑是imageWidth和imageHeight字段与图片实际尺寸不一致。labelme 有时会在编辑后没有刷新这两个字段,尤其是你用旧版本标注、新版本看图时。转换完最好随机抽几张图,把标注框画回去肉眼复核。只看数据不看框的转换,等于埋雷。
2.4 数据划分与data.yaml:同一个裂缝不能横跨训练集和验证集
目录结构按 YOLO 惯例组织:
datasets/crack/ ├── images/ │ ├── train/ │ ├── val/ │ └── test/ ├── labels/ │ ├── train/ │ ├── val/ │ └── test/ └── data.yaml划分脚本用随机种子保证可复现,8:1:1 是毕设常见比例:
import random import shutil import pathlib src_img = pathlib.Path("images") # 未划分的原始图片 labels_src = pathlib.Path("labels_all") # 未划分的标签txt目录 train_dst = pathlib.Path("images/train") val_dst = pathlib.Path("images/val") test_dst = pathlib.Path("images/test") imgs = list(src_img.glob("*.jpg")) random.Random(42).shuffle(imgs) n = len(imgs) n_train = int(n * 0.8) n_val = int(n * 0.9) for i, img in enumerate(imgs): if i < n_train: dst = train_dst elif i < n_val: dst = val_dst else: dst = test_dst dst.mkdir(parents=True, exist_ok=True) shutil.copy(img, dst / img.name) label_txt = labels_src / f"{img.stem}.txt" if label_txt.exists(): label_dst = dst.parent.parent / "labels" / dst.name label_dst.mkdir(parents=True, exist_ok=True) shutil.copy(label_txt, label_dst / label_txt.name)固定随机种子 42 是故意的,保证你第二天重跑还是同样的划分,不会因为 shuffle 变了导致对比实验失真。
一个容易忽略的分组原则:同一个墙面的连续拍摄照片要整体进同一侧。如果 1 到 10 张是同一面墙的连拍,其中几张进 train、几张进 val,验证集的“简单”会超出实际,训练出的 mAP 虚高,换到真实场景立刻打回原形。
最后写data.yaml:
path: datasets/crack train: images/train val: images/val test: images/test nc: 1 names: 0: crackpath建议写相对路径,避免换机器后还要改绝对路径。如果只做毕设,test目录可以先不填,val就够评估用。
3. 训练环境与参数:CPU也能跑通,但imgsz和增强不能照抄默认值
3.1 Ubuntu20.04 CPU环境搭建:能跑,但要有耐心
很多同学的训练机器没有独显,看到 yolov8 第一反应是“先装 CUDA”。实际不需要,CPU 版本完全可以跑通,只是慢。训练 yolov8s、单张 1024 尺寸、CPU 跑一个 epoch 可能十几分钟,但数据量在 500 张上下时,几十个 epoch 也够出一个能演示的模型。
环境搭建放在 Ubuntu 20.04 上最省心:
conda create -n crack python=3.9 -y conda activate crack pip install ultralyticsPyTorch 的 CPU 版本按官网生成的 CPU 指令安装即可,不要在 GPU 版本上卡太久。装完后跑一句python -c "from ultralytics import YOLO; print(YOLO.__name__)"验证 import 正常。
预测单张图的耗时在我常用的设备上是 0.5 到 1.5 秒,训练一个 epoch 在 500 张图、1024 分辨率下大约十几分钟到半小时。这个速度说明什么?说明 CPU 能用于验证链路和数据,但大规模调参还是建议找云 GPU 或实验室机器。
3.2 模型选择:yolov8n还是yolov8s,看参数对比
| 对比维度 | yolov8n | yolov8s | 说人话 |
|---|---|---|---|
| 参数量 | 约 3.2M | 约 11.2M | s 的容量更大,能学到更多裂缝纹理特征 |
| CPU 推理速度 | 较快 | 中等 | n 适合边缘盒子,s 适合实验室演示 |
| mAP50 上限 | 基线 | 通常高 3 到 5 个点 | 数据量越足,s 优势越明显 |
| 训练显存占用 | 低 | 中等 | 8G 显存可跑,CPU 也能跑 |
我的习惯是先用 yolov8n 跑一次完整训练,确认数据流和代码链路没有问题,再换 yolov8s 出正式结果。两个模型共用同一份数据,只是改model参数,不折腾。如果你只需要快速验证标注质量,n 就是最好的调试工具。
3.3 训练命令:先照着跑,再解读参数
命令行版本:
yolo train model=yolov8s.pt data=data.yaml epochs=100 imgsz=1024 \ batch=8 patience=20 device=cpu mosaic=0.0等价的 Python 脚本:
from ultralytics import YOLO model = YOLO("yolov8s.pt") model.train( data="data.yaml", epochs=100, imgsz=1024, batch=8, patience=20, device="cpu", mosaic=0.0, )参数说明按优先级排:
imgsz=1024是裂缝检测里最值得改的参数。默认 640 对行人、车辆没问题,但裂缝宽度可能只有 10 到 20 像素,缩到 640 后特征被压缩,细裂缝直接消失。1024 是通用性较好的折中,显存吃紧时用 800 也行,但不建议低于 640。
mosaic=0.0是第二个值得改的。mosaic 会把四张图拼接成一张,对紧凑目标有效,但裂缝是长条状,切片拼接会把一条裂缝切成几十段,模型学到的是“断裂的短线才是裂缝”,推理时反而对完整裂缝漏检。关闭 mosaic 是常见做法,代价是训练收敛稍慢,但最终 mAP 更稳。
patience=20表示验证集 mAP50 连续 20 个 epoch 没提升就提前停。裂缝数据集偏小,训练到 50 到 70 个 epoch 往往就到平台期,没必要硬跑满 100。
batch=8在 CPU 上偏大,内存 16G 以下建议改 4。GPU 上如果显存 8G,1024 尺寸配 batch=8 是安全的,再大容易 OOM。
3.4 训练过程监控和续跑
训练时每隔一段时间看终端输出的 P、R、mAP50 和 loss。loss 下降慢不要慌,裂缝目标比行人检测收敛慢是常态。真正要警惕的是 val 指标涨到某一个点后开始掉头,这是过拟合信号,配合patience会自动停下,停的位置就是最好的 checkpoint。
如果训练中途断了,不要从头再来:
model = YOLO("runs/detect/trainXX/weights/last.pt") model.train(resume=True)last.pt是断电后悔药,best.pt是最终交付物。注意 resume 时不要再传data.yaml和数据增强参数,直接续跑上次的配置。
4. 看懂结果并做出“结果展示”:从best.pt到一份能答辩的验证报告
4.1 runs目录里的文件都是什么:别只盯着best.pt
训练结束后进入runs/detect/trainXX/,里面有几十个文件。毕设答辩时,评委大概率会指着results.png问“这是怎么画的”,所以每个文件都要知道用途:
| 文件 | 作用 | 答辩价值 |
|---|---|---|
| weights/best.pt | 验证集指标最好的权重 | 最终交付物 |
| weights/last.pt | 最后一个 epoch 的权重 | 续跑用 |
| results.png | 训练过程 loss 和指标曲线 | 说明调参过程 |
| confusion_matrix.png | 混淆矩阵 | 说明误检率 |
| PR_curve.png | Precision-Recall 曲线 | 说明置信度阈值选择 |
| val_batch*.jpg | 每轮验证集预测样例 | 展示检测效果 |
best.pt和last.pt的区别要讲清楚:val 指标可能在第 63 个 epoch 到达峰值,后面 37 个 epoch 在过拟合边缘挣扎,best.pt保存的是第 63 轮的状态,last.pt是最后一轮。交付永远用 best。
4.2 验证集指标解读:mAP50能到多少算合格
用 best.pt 做验证,命令要和训练时的 imgsz 保持一致:
yolo val model=runs/detect/trainXX/weights/best.pt data=data.yaml imgsz=1024 split=valPython 版:
from ultralytics import YOLO model = YOLO("runs/detect/trainXX/weights/best.pt") metrics = model.val(data="data.yaml", imgsz=1024, split="val") print("mAP50:", metrics.box.map50) print("mAP50-95:", metrics.box.map) print("precision:", metrics.box.mp) print("recall:", metrics.box.mr)解读这几个数字时要有个心理预期:裂缝的 mAP50-95 通常比 mAP50 低 0.2 到 0.4,这是线状目标的天然缺陷。IoU 计算是框和真值框的面积交并比,裂缝框只要错位几个像素,IoU 就从 0.8 掉到 0.4,所以 mAP50-95 会很难看。答辩时主动解释这一点,反而显得你理解任务本质。
如果 mAP50 能到 0.6 以上,recall 在 0.7 附近,这个模型就有演示价值了。注意别只报 best 指标,要报“最后一轮验证”的指标,才是真实水平。
4.3 把预测结果拼成一页网格图:直接放进毕设文档
验证集指标是数字,答辩还需要可视化。把每张验证图的预测结果拼接成 4x4 大图,能直观展示裂缝框的位置质量。
from PIL import Image import glob from ultralytics import YOLO model = YOLO("runs/detect/trainXX/weights/best.pt") results = model.predict( source="datasets/crack/images/val", conf=0.3, iou=0.45, imgsz=1024, line_width=2, save=False, ) # 收集预测结果图(BGR ndarray),转成RGB后拼接 thumbs = [] for r in results: img_bgr = r.plot() # 返回带框的BGR图 img_rgb = img_bgr[:, :, ::-1] thumbs.append(Image.fromarray(img_rgb)) # 4行4列拼接 grid_w, grid_h = 4, 4 cell_w, cell_h = 480, 360 grid = Image.new("RGB", (grid_w * cell_w, grid_h * cell_h), (255, 255, 255)) for i, img in enumerate(thumbs[: grid_w * grid_h]): img = img.resize((cell_w, cell_h)) x = (i % grid_w) * cell_w y = (i // grid_w) * cell_h grid.paste(img, (x, y)) grid.save("result_grid.jpg")r.plot()是 YOLO 自带的绘图方法,返回带目标框的 BGR 数组,比手动cv2.rectangle省事得多。conf=0.3对裂缝场景是合理起点:巡检优先别漏检,阈值太高会漏掉细裂缝。如果误检太严重再调高到 0.4 或 0.5。
拼接完看一眼:如果网格里大多数框能贴合裂缝轮廓、框边没有大面积背景,这套结果展示可以直接进论文第 4 章。
5. 部署与常见问题避坑:把模型封装成检测服务,再谈边界
5.1 把yolov8封装成检测服务:加载一次,复用多次
毕设答辩现场,很可能要当场演示“传入一张新照片,输出带框图”。这时不能每次调用都重新加载权重,要封装成一个类,初始化时加载一次,之后每一张图只走推理。
import cv2 from ultralytics import YOLO class CrackDetector: def __init__(self, weights="best.pt"): self.model = YOLO(weights) def detect(self, img): results = self.model.predict( source=img, conf=0.35, iou=0.5, imgsz=1024, verbose=False, ) r = results[0] boxes = r.boxes.xyxy.cpu().numpy() # 左上右下坐标 confs = r.boxes.conf.cpu().numpy() # 每个框的置信度 plot_img = r.plot() # 带框的BGR图 return boxes, confs, plot_img detector = CrackDetector("runs/detect/trainXX/weights/best.pt") boxes, confs, out_img = detector.detect("demo.jpg") print(f"detected {len(boxes)} cracks") cv2.imwrite("demo_result.jpg", out_img)这个类就是你的“交付物”,配合一个demo.jpg,写进开发文档的接口说明里,评委能直接理解你的工程结构。conf=0.35是巡检场景的默认值,优先级是别漏检;如果是做量化评估,可以提高到 0.5 减少误报。
5.2 导出onnx给边缘设备:先导出,再检查预处理
毕设做到部署演示时,常见需求是把模型导成 onnx,跑在带 NPU 的边缘盒子上,比如 RK3588 这类设备。导出命令:
yolo export model=runs/detect/trainXX/weights/best.pt format=onnx dynamic=True imgsz=1024 opset=12导出后使用 onnxruntime 推理:
import onnxruntime as ort import numpy as np session = ort.InferenceSession("best.onnx") input_name = session.get_inputs()[0].name # 输入需要和训练时一样的letterbox预处理,输出是 (1, 4+nc, 8400) 的原始预测 outputs = session.run(None, {input_name: input_blob})三个重要注意点:第一,onnx 输出去掉了 NMS,框的坐标和置信度要自己在后处理里算,不能直接用 ultralytics 的predict想当然;第二,letterbox 预处理必须和训练时一致,否则框的位置整体偏移;第三,1000 尺寸的输入在 onnx 里张量更大,如果设备内存有限就导出imgsz=800。
边缘设备部署这件事,适合在毕业设计的“系统展望”章节写一句“已完成 onnx 导出验证”,真要跑通 RKNN 转换,需要额外处理检测头,工作量会明显超出课程设计范围。
5.3 高频问题避坑清单:现象、原因、解决
坑 1:细裂缝一个都检不出来,只检到粗裂缝
现象:验证集里宽度 2 毫米以下的裂缝全部漏检,粗裂缝能框住。
原因:imgsz太小,细裂缝在 feature map 上只剩下 1 到 2 个像素;同时标签框留了太多背景,导致 anchor 正样本质量差。
解决:训练和预测统一用imgsz=1024;标注时框边紧贴裂缝外接矩形。如果还不行,对高分辨率原图做切块推理,把 4K 原图切成 1024 的 tiles 分别检测再合并坐标。
坑 2:训练loss居高不下,甚至出现nan
现象:train box loss 前 30 个 epoch 不降,偶尔跳出 nan。
原因:标签 txt 文件里出现img_w或img_h为 0 的归一化坐标,或者转换脚本读到的 json 里有两个 label 名,其中一个不在 class_map 里被忽略后,同一张图的正样本数量骤减。
解决:写完转换脚本立刻做回画检查,把生成的 txt 画到原图上。如果批量操作不方便,就随机抽 20 张检查,出现一张坐标错位说明转换逻辑有 bug,不要直接训练。
坑 3:训练集mAP很高,验证集mAP很低
现象:train 的 mAP50 有 0.9,val 只有 0.4 出头。
原因:数据量太小或者增强过强。裂缝是结构性很强的纹理,数据增强里的hsv_h、hsv_s调得过大时,颜色改变会让模型学到错误的关联;数据量不足时,模型把训练集的背景“背”下来了。
解决:先降低增强强度,hsv_h=0.01、hsv_s=0.5起步;同时增加无裂缝的背景负样本图,比例控制在训练集的 20% 到 30%,让模型学会“没有裂缝就是没有”。
坑 4:墙面污渍、水渍被当成裂缝误报
现象:precision 很低,验证集里墙面污渍、伸缩缝、工具阴影都被框了出来。
原因:负样本太单一,模型没有见过足够多的“像裂缝但不是裂缝”的区域。
解决:把典型的误报图片收集起来,标注成空背景,放进训练集。注意空背景集也要标注,但不产生任何框,YOLO 会把这些图作为背景学习。同时把推理阈值提到 0.45 以上,牺牲一点 recall 换 precision。
坑 5:换一个光源条件,效果下降一大截
现象:训练用的照片是阴天拍的,换到正午强光下漏检明显。
原因:训练集没有覆盖光照变化,颜色和阴影把模型带偏。
解决:补拍逆光和强光场景,或者在训练时用hsv_v做强光照扰动。这个坑在桥梁和隧道项目里特别常见,因为现场灯源角度和白天自然光完全不同。
6. 训练日志可视化:一张曲线图说清调参全过程
6.1 从results.csv里把训练曲线捞出来
训练结束后的runs/detect/trainXX/results.csv是官方记录的全量训练日志,包含每个 epoch 的 loss、precision、recall、mAP。答辩时把它画成曲线图,比贴一张截图专业得多。
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/trainXX/results.csv") # 不同版本列名略有差异,先打印列名确认 print(df.columns.tolist()) fig, axes = plt.subplots(1, 3, figsize=(15, 4)) axes[0].plot(df["epoch"], df["train/box_loss"], label="train box") axes[0].plot(df["epoch"], df["val/box_loss"], label="val box") axes[0].set_title("box loss") axes[0].legend() axes[1].plot(df["epoch"], df["metrics/precision(B)"], label="precision") axes[1].plot(df["epoch"], df["metrics/recall(B)"], label="recall") axes[1].set_title("precision & recall") axes[1].legend() axes[2].plot(df["epoch"], df["metrics/mAP50(B)"], label="mAP50") axes[2].plot(df["epoch"], df["metrics/mAP50-95(B)"], label="mAP50-95") axes[2].set_title("mAP") axes[2].legend() plt.tight_layout() plt.savefig("training_curves.png", dpi=200)如果 matplotlib 画出的图出现中文方块,是系统缺少中文字体,标题全部用英文即可,中文解释留给答辩 PPT。
6.2 再把验证集图片合成动图
把每轮验证集预测图合成 GIF,可以一眼看出“模型从乱框到收敛”的过程:
import glob import imageio.v2 as imageio files = sorted(glob.glob("runs/detect/trainXX/val_batch*.jpg")) if files: imageio.mimsave("training_progress.gif", [imageio.imread(f) for f in files], fps=5)这招在课程设计答辩时特别好用:当评委问“你怎么证明模型在学习”,放一张动图比列十个数字更有说服力。
最后说一个我的习惯:模型训完先画曲线,再看混淆矩阵,最后跑一次 val 看每类指标。这三步都能通过,才开始部署和封装接口。身边见过太多同学第一步就跳过,直接把 best.pt 丢进部署脚本,结果调了两天预处理才发现模型根本没有收敛。先验数据、再验模型、最后验部署,这个顺序能救回大量无效时间,希望帮到你。
本文还有配套的精品资源,点击获取