简介:面向道路裂缝检测与路面病害识别场景,提供一套目标检测专用数据集,可直接用于YOLO系列模型训练与算法验证。数据按YOLO标准目录结构保存,标注格式为yolo格式,包含crack单一类别及对应classes文件,并已做好数据划分:训练集约520张图片及标签、验证集约130张图片及标签,标签文件与图片一一对应,解压后即可投入训练。资源包共1323个文件,其中661个txt标签、660张jpg原图为主体,配合1个Python可视化脚本和1张类别示意图,整体仅7.3MB,轻量易用,便于快速下载与部署。配套可视化脚本无需修改即可运行,随机传入一张图片便能绘制边界框并保存结果,方便自查标注质量与数据均衡性。目前已有88人学习下载,适合目标检测入门者、算法工程师及道路检测项目开发者用于模型训练、性能对比与教学演示。
1. 道路裂缝检测数据集:拿来就能训,但先别急着跑训练
城市道路巡检、高速路面检测、桥梁健康监测这类场景里,道路裂缝检测数据集是最常见的目标检测落地样本之一。这个数据集已经把图片、标签、类别class文件和数据可视化脚本打包好了,并且预先做了数据划分,意思是你解压之后,理论上配好YOLOv8就能开始训练。适合想快速跑通yolov8目标检测数据集处理流程的入门者,也适合做裂缝检测方案预研、想先拿现成数据验证模型选型的人。
但我见过太多人拿到这类“开箱即用”的数据集直接跑train.py,最后被标签错位、类别顺序对不上、划分失衡折腾到怀疑人生。下面按拿到数据集后的落地顺序来:先拆格式,再做可视化体检,然后决定要不要重新划分,最后把常见翻车点列出来。这套流程往前多花半小时,后面调参至少省两天。
2. 拆开数据集看格式:数据、标签、class文件是怎么对齐的
拿到压缩包第一步不是解压完就跑训练,而是先把目录结构看明白。规范的数据集,10秒内就能看出组织方式。裂缝检测数据集最常见的做法是images和labels同级,里面再按train/val分好,class文件单独放。这个结构对YOLO系列训练框架很友好,因为它们默认的标签查找方式,就是把图片路径里的“images”替换成“labels”,再把图片后缀换成.txt。
也有人会把train.txt、val.txt这类文件列表一起放进来,那说明训练脚本是走文件列表加载的。两种模式没有优劣,但你要先确认自己用的是哪一种,别拿着文件列表数据集去套目录扫描的配置。下面按最常见的目标检测数据集处理方式展开。
2.1 目录结构与命名规则:训练脚本认的是路径,不是文件名好不好看
一个典型的裂缝数据集目录长这样:
dataset_root/ ├── images/ │ ├── train/ │ │ ├── 000001.jpg │ │ └── ... │ └── val/ │ ├── 000101.jpg │ └── ... ├── labels/ │ ├── train/ │ │ ├── 000001.txt │ │ └── ... │ └── val/ │ ├── 000101.txt │ └── ... ├── classes.txt ├── visualize.py └── train.txt / val.txt(可选)这个结构里有两套约定比较隐蔽。第一,images和labels是同级但彼此独立的目录,训练框架靠路径替换去找标签,所以/images和/labels这两个目录名最好别改,改了就得同步改配置文件。第二,图片和标签的匹配靠的是文件主名,也就是去掉扩展名后的部分,比如000001.jpg对应000001.txt。我发现有人会把训练图片重命名为看起来更直观的名字,比如IMG_20231214_082531.jpg,结果标签全对不上,光清理就对了一整天。
另外要注意:像CUB这类学术鸟类数据集,组织方式是文件夹名当类别名;而道路裂缝这类目标检测工程数据集,类别语义全部放在class文件里,文件夹只承担train/val划分功能。很多刚转来做检测的人会在这点上绕晕。还有jpg、png、jpeg混存的情况,训练脚本一般能认,但可视化脚本里要显式过滤后缀,后面3.3节会给出写法。
拿到数据集先做一次对齐检查,脚本不复杂,但能一次性暴露文件名不一致的问题:
# 先看总体数量是否一致 find images/train -type f \( -name "*.jpg" -o -name "*.png" \) | wc -l find labels/train -type f -name "*.txt" | wc -l # 列出有图片但没有对应标签的文件,一张都不该有 for f in $(find images/train -type f \( -name "*.jpg" -o -name "*.png" \)); do base=$(basename "$f" .jpg) base=$(basename "$base" .png) if [ ! -f "labels/train/${base}.txt" ]; then echo "MISSING $f" fi done这段脚本的逻辑是:取出图片文件名,剥掉.jpg或.png后缀,再去labels下找同名txt,找不到就打印。注意basename命令连剥两次,是应付.jpg和.png混存的情况。如果发现输出了一大堆MISSING,先检查是不是目录拼写错了,比如images和labels的train子目录不配对;如果确认路径没问题,那就是文件名真的对不上,需要重新做匹配。
2.2 标签txt里那五个数字:class_id与归一化坐标的读法
YOLO格式的标签文件是每行一个目标的txt,一行五个数字,顺序固定。拿这个裂缝数据集里的某一行举例:
0 0.5231 0.4312 0.0324 0.1185第一个0是类别编号,后面四个分别是归一化中心点x、中心点y、归一化宽度、归一化高度。归一化指的是相对图片宽高的比例,取值范围0到1,不是像素坐标。比如一张4096×3072的检测原图,0.5231乘以4096等于2142像素,0.1185乘以3072等于364像素,还原成像素坐标要这样乘回来。
读取标签最直接的办法是纯Python解析,不依赖任何第三方库:
from pathlib import Path def read_yolo_label(path): boxes = [] for line in Path(path).read_text().strip().splitlines(): parts = line.split() if len(parts) < 5: continue cls = int(parts[0]) cx, cy, w, h = map(float, parts[1:5]) boxes.append((cls, cx, cy, w, h)) return boxes boxes = read_yolo_label("labels/train/000001.txt") print(len(boxes), boxes[:2])这段代码有几个细节值得注意。第一,空标签文件会让read_text返回空字符串,strip之后没有splitlines,返回空列表,这正是负样本的正确表现,不要当成错误。第二,用split()按空白字符分割,能自动处理行尾有多余空格的情况。第三,如果某一行解析时报错,大概率是混入了COCO格式的json数据,那个不是这种纯文本结构。裂缝数据里常见的长宽比很大的框,五个数字里的w和h会相差很多倍,这是正常现象,不代表标签异常。
2.3 class文件与类别顺序:从classes.txt到“软标签”的关联
class文件通常叫classes.txt,一行一个类别名,行号和标签txt里的class_id一一对应。如果这个裂缝数据集只标了一类,那classes.txt内容很简单:
crack如果按裂缝走向细分成了横向、纵向、网状,那class文件会是三行,分别对应标签里的0、1、2:
crack_horizontal crack_vertical crack_network这个对应关系是整个数据集的“编号表”。class_id只是序号,不携带语义,所以这个文件就是“硬标签编号表”。你训练时YOLO配置里的names参数,必须和这个文件逐行对齐。如果把标注工具里的类别重排一遍,或者往中间插入一个新类,已有标签的编号整体错位,这是后面5.1节要展开的大坑。
知识蒸馏里说的“软标签”指模型输出的概率分布,和这里完全是两码事,这个数据集里我们只认class文件和标签id的硬绑定。很多目标检测常用标注工具,比如LabelImg、roLabelImg,在标注前就要先加载class文件,保存txt时回填的就是这个顺序。所以拿到数据集后第一件事就是打开classes.txt,确认它的顺序没有被改动过。
类别命名上我建议遵循一个工程习惯:英文小写加下划线,不要用中文,不要带空格。因为后续转COCO、训练、部署时,类别名会被写进很多配置文件和日志里,带空格容易在解析时断掉。如果class文件已经是这种命名,就保持原样。你要是想合并自己的数据,比如把燃气管道图像数据集也一起训练,合并的第一步也是把两个class文件对齐,不是把图片拷到一个目录就完事。
3. 可视化脚本先跑通:画框确认标签真的和图片对齐
数据集自带的“数据可视化脚本”通常就是画框脚本。我的建议是不要直接拿来就用,先自己动手写一个最小的画框程序。可视化不只是为了看效果,而是给数据做体检:框和裂缝是否贴合、有没有漏标、类别颜色是否合理。用100张画框图去检查,比训练完之后看100张预测图值钱得多。
如果你用的还是YOLOv8这类目标检测数据集处理流程,可视化这一步跳过,后面训练时出现的很多怪问题都没法定位是数据问题还是模型问题。下面给出一个可以改一改就用于检查的版本。
3.1 最小可视化脚本:OpenCV读图、画框、另存
基于OpenCV的画框脚本是所有检查工作的基础设施:
import cv2 from pathlib import Path data_root = Path(".") img_path = data_root / "images/train/000001.jpg" lab_path = data_root / "labels/train/000001.txt" img = cv2.imread(str(img_path)) if img is None: raise FileNotFoundError(f"图片读不出来: {img_path}") h, w = img.shape[:2] for line in lab_path.read_text().strip().splitlines(): parts = line.split() cls, cx, cy, bw, bh = ( int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]), ) x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 0, 255), 2) cv2.putText( img, str(cls), (x1, max(y1 - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2, ) cv2.imwrite("check_000001.jpg", img)画框颜色用的是BGR顺序的(0,0,255)纯红,裂缝在灰色路面上红色最显眼。坐标还原时要注意:bw和bh是归一化宽高,必须乘图片宽高;中心点坐标减半才能换算成左上角。线宽固定2对4096宽的大图会偏细,可以改成max(2, w // 1500),让大图小图都能看。putText的y坐标如果太靠近顶部,文字会画出画布,所以用max(y1 - 5, 0)兜底。
3.2 按类别染色与漏标检查:一眼看出标签噪声
当数据不止一个类别时,全部画成红色看不出类别归属,按class_id染色更直观:
COLORS = [(0, 0, 255), (0, 255, 0), (255, 0, 0), (0, 255, 255)] for line in lab_path.read_text().strip().splitlines(): parts = line.split() cls = int(parts[0]) color = COLORS[cls % len(COLORS)] # 坐标换算同 3.1,省略 cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText( img, f"cls{cls}", (x1, max(y1 - 8, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2, )染色逻辑是让不同类别用不同颜色,肉眼扫一遍就能发现“这个框颜色不对”。裂缝这类细长目标,正常框应该紧贴裂缝边缘。如果发现大量框和裂缝错位、框内没有裂缝、或者同一条裂缝被框了两次,基本可以判定标签有问题。还有一种更隐蔽的坏标签:框是对的,但缩得很小,只框住裂缝的一小段,这通常是标注时用了不同的坐标系约定。
漏标比错标更难发现。错标至少有个框在那里,漏标则是图片上明明有一条明显裂缝,一个框都没有。检查漏标的方法是先看原图心里数一遍裂缝数量,再切到带框图对比。几百张图里抽查50张,重点关注大于80像素的裂缝是否都有框。这个阈值可以自己按项目精度要求定。
3.3 批量生成到输出目录:几百张图的快速巡检
单张检查太慢,把整个目录跑一遍,输出到check目录:
import cv2 import numpy as np from pathlib import Path images_dir = Path("images/train") labels_dir = Path("labels/train") out_dir = Path("check") out_dir.mkdir(exist_ok=True) COLORS = [(0, 0, 255), (0, 255, 0), (255, 0, 0)] for img_p in sorted(images_dir.iterdir()): if img_p.suffix.lower() not in (".jpg", ".png", ".jpeg"): continue lab_p = labels_dir / (img_p.stem + ".txt") img = cv2.imread(str(img_p)) if img is None: print("读图失败:", img_p) continue h, w = img.shape[:2] if lab_p.exists(): for line in lab_p.read_text().strip().splitlines(): parts = line.split() cls, cx, cy, bw, bh = ( int(parts[0]), float(parts[1]), float(parts[2]), float(parts[3]), float(parts[4]), ) color = COLORS[cls % len(COLORS)] x1 = int((cx - bw / 2) * w) y1 = int((cy - bh / 2) * h) x2 = int((cx + bw / 2) * w) y2 = int((cy + bh / 2) * h) cv2.rectangle(img, (x1, y1), (x2, y2), color, 2) cv2.putText( img, str(cls), (x1, max(y1 - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.6, color, 2, ) cv2.imwrite(str(out_dir / img_p.name), img) print("done:", img_p.name)关键点在几个边界处理:suffix.lower()过滤掉非图片文件,避免把隐藏文件也读进来;lab_p.exists()保证缺标签的图片不报错;读图失败时打印日志并跳过,而不是让整个脚本中断。Windows下如果图片路径带中文,cv2.imread会返回None却不会抛异常,就会打印一堆“读图失败”,解决办法在第5章展开,这里先提一句:用np.fromfile配合cv2.imdecode可以解决。
批量跑完别急着收工,打开check目录按文件名顺序翻,重点挑两类看:一类是很多框挤在一起的小图,一类是完全没有框的图。如果时间有限,每类随机抽20张就够了,不需要每张都过。
4. 重新做数据划分:随机和分层差在哪,验证集缺类怎么查
“已做数据划分”不等于这个划分适合你的场景。原始划分可能是纯随机打乱,也可能验证集只占了很小的比例,而你想在不同光照、不同路面材质上验证泛化能力,那就需要重新划分。特别是处理数据集用于yolov8训练时,划分这一步出问题的概率比想象中大得多。
先确认原划分比例,用wc命令数一下train和val下的图片数量,大概知道原划分是8:2还是9:1。如果只有train和val两堆,没有独立的test,那val承担的是验证和调参双重任务,划分时就要更长点心。
4.1 复现原划分:固定随机种子与文件列表打乱
最简单可靠的划分方式是:固定随机种子,对完整文件列表做shuffle,再按比例切分。这样任何人在任何机器上运行,得到的结果都是一样的,实验对比才不会打架。
import random import shutil from pathlib import Path random.seed(42) # 固定种子,保证两次运行结果一致 src_img = Path("images/all") src_lab = Path("labels/all") dst = Path("split_dataset") imgs = sorted(list(src_img.glob("*.jpg")) + list(src_img.glob("*.png"))) random.shuffle(imgs) val_ratio = 0.2 n_val = int(len(imgs) * val_ratio) val_imgs, train_imgs = imgs[:n_val], imgs[n_val:] for split, split_imgs in [("val", val_imgs), ("train", train_imgs)]: img_out = dst / split / "images" lab_out = dst / split / "labels" img_out.mkdir(parents=True, exist_ok=True) lab_out.mkdir(parents=True, exist_ok=True) for img_p in split_imgs: lab_p = src_lab / (img_p.stem + ".txt") if not lab_p.exists(): print(f"标签缺失,跳过: {img_p.name}") continue shutil.copy2(img_p, img_out / img_p.name) shutil.copy2(lab_p, lab_out / (img_p.stem + ".txt"))几个参数值得说清楚。random.seed(42)不是玄学,是工程必需:不固定种子,每次跑出的train/val都不同,模型对比实验就没有基线。val_ratio=0.2是常用起步值,裂缝数据如果是几千张图,20%能拿到几百张验证图,足够评估;如果总图数不到500,val建议提到25%,不然验证集AP曲线抖动会很严重。标签缺失时直接跳过,而不是把图片单独扔到train里,这点很重要:无标签图片进YOLO训练会被当成全背景图,模型会学到错误信息。
复制图片会占磁盘空间,但换来的是目录干净、工程独立。如果图片量特别大,改成写文件列表更合适,训练脚本走txt列表加载:
with (dst / "train.txt").open("w") as f: for img_p in train_imgs: f.write(str(img_p.resolve()) + "\n")YOLO系列和很多基于mmdetection的工程都支持这种txt列表模式,把图片路径逐行写进去就行。
4.2 按类别分层划分:避免验证集里缺类
纯随机划分最大的坑是:数量少的类别在验证集里可能一个样本都抽不到。裂缝数据集常见2到5个类别,有的类只有一两百张,随机划分后该类在验证集里为0的概率不低。结果就是训练时模型见过这个类,验证时却从不考核它,整体mAP看着不错,部署到市政道路上立刻翻车。
简单的办法是按主类别分层抽样:
from collections import defaultdict def main_class(img_p, src_lab): lab_p = src_lab / (img_p.stem + ".txt") if not lab_p.exists(): return None cls_counts = defaultdict(int) for line in lab_p.read_text().strip().splitlines(): cls_counts[int(line.split()[0])] += 1 if not cls_counts: return -1 # 空标签 return max(cls_counts, key=cls_counts.get) grouped = defaultdict(list) for img_p in imgs: c = main_class(img_p, src_lab) if c is not None: grouped[c].append(img_p) train_imgs, val_imgs = [], [] for c, group in grouped.items(): random.shuffle(group) cut = int(len(group) * (1 - val_ratio)) train_imgs.extend(group[:cut]) val_imgs.extend(group[cut:])这段逻辑是先把所有图片按主类别分组,再在每个组内单独做随机划分。这样每个类别在验证集里的比例都一致,不会出现某类在验证集里为0。它的缺点是只按主类归属,一张图同时含多个类别时归类有误差,但对以裂缝为主体的道路数据集,这个误差可以接受。如果类别数很少,还可以在验证集里做补样,让最少的一类也有30个实例以上,但要注意别让同一张图既在train又出现在val。
4.3 划分完必须做的两个校验:数量一致与类别均衡
划分结束后要做校验,这一步不该省。数量校验用ls数目录:
echo "train imgs: $(ls split_dataset/train/images | wc -l)" echo "train labels: $(ls split_dataset/train/labels | wc -l)" echo "val imgs: $(ls split_dataset/val/images | wc -l)" echo "val labels: $(ls split_dataset/val/labels | wc -l)"两个数字应该一致。如果不一致,回到2.1的互查脚本,找出是哪个文件没配对。类别均衡校验用一行awk统计验证集里每个class_id的框数量:
for f in split_dataset/val/labels/*.txt; do awk '{print $1}' "$f" done | sort | uniq -c输出会像这样:
134 0 42 1 0 2如果某个类别显示为0,这一类的验证AP就是无效的。处理办法是回到4.2,把这一类的最小验证数量卡到至少30,或者手动从训练集里挪几张进val。裂缝数据里容易出现“修补痕迹”这种少样本类别,恰恰是实际业务最关心的类,宁可验证集整体小一圈,也不能让这行是0。
提示:这里的“空标签文件”和“缺失标签文件”是两回事。空标签是文件存在但0字节,是合法负样本;缺失标签是根本没有txt文件,是脏数据。4.1脚本只跳过了缺失情况,不会误删空标签。
5. 避坑记录:裂缝数据使用中翻过车的5个真实案例
下面这五个坑不完全是我一个人的记录,基本是团队里做这个方向绕不开的坎。每条按现象、原因、解决三步写,对着检查比自己瞎猜快。
5.1 class_id和class文件顺序错位:损失正常,预测全错
现象:训练时损失曲线正常下降,验证mAP也有0.8以上,但一跑推理,所有的横向裂缝都预测成了纵向裂缝。整体错位一个位置,不是个别图片的问题。
原因:数据集的classes.txt被重排过,但标签txt里的class_id没有同步更新。比如原始顺序是crack_horizontal(0)、crack_vertical(1),后来有人想把crack_network插到第一位,改了class文件,标签id却还停留在旧顺序。模型本身没有学错,它学的是“0号目标长什么样”,只是0号含义被换掉了。
解决:先找出新旧class文件的对应关系,生成旧id到新id的映射,然后把labels下所有txt逐行替换:
from pathlib import Path mapping = {0: 1, 1: 2, 2: 0} # 旧class_id -> 新class_id for lab_p in Path("labels/train").glob("*.txt"): lines = [] for line in lab_p.read_text().strip().splitlines(): parts = line.split() parts[0] = str(mapping[int(parts[0])]) lines.append(" ".join(parts)) lab_p.write_text("\n".join(lines))这个脚本的关键是把mapping先写清楚再动手。替换完必须重新跑一次可视化,确认横向裂缝框上显示的是“cls1”而不是“cls0”。血泪经验是:这个坑在训练阶段几乎不露馅,损失正常、mAP正常,只有部署时才炸,所以拿到数据集的第一步就核class顺序。
5.2 图片与标签文件名不匹配:漏检一张都发现不了
现象:训练日志里train的图片数量比标签数量多出十几张,可视化时某些图片完全没有任何框。
原因:有人重命名了部分图片,或者标注工具导出时给文件名加了批次前缀,导致图片主名和标签主名对不上。YOLO训练框架对无标签图片的默认处理方式是不计算该图的标签损失,等于把这张图当纯背景训练,模型会从中学到“这里没目标”。
解决:跑双向互查,一次列出有图无标签,一次列出有标签无图:
# 有图无标签 for f in images/train/*.jpg images/train/*.png; do base=$(basename "$f") base=${base%.*} [ -f "labels/train/${base}.txt" ] || echo "no label: $base" done # 有标签无图 for f in labels/train/*.txt; do base=$(basename "$f" .txt) [ -f "images/train/${base}.jpg" ] || [ -f "images/train/${base}.png" ] || echo "no image: $base" done两段命令都要跑。对“有图无标签”的处理要果断:要么找到原标签放回来,要么直接从训练集里删掉这张图,绝不能留在那里当背景。对“有标签无图”的情况,多半是图片被误删或移动,找到放回来即可。整批整理完再用2.1的脚本复查一遍。
5.3 中文路径导致可视化脚本黑屏:cv2.imread返回None
现象:脚本运行不报错,但输出的检查图是黑色的,或者文件根本没生成,日志里也没有明显异常。
原因:OpenCV的cv2.imread在Windows下遇到非ASCII码路径时会返回None,而cv2.imwrite不会因此报错,于是黑图被写出来。数据集放在D:\道路裂缝\images这类目录下,就会触发。
解决:读图时改用np.fromfile读二进制数据,再用cv2.imdecode解码,绕开imread的路径处理逻辑:
import cv2 import numpy as np def load_image(path): data = np.fromfile(str(path), dtype=np.uint8) img = cv2.imdecode(data, cv2.IMREAD_COLOR) return img写文件也建议用imencode加tofile,避免保存路径带中文时再次翻车:
ret, buf = cv2.imencode(".jpg", img) buf.tofile(str(out_path))这个坑在Linux下几乎不出现,所以很多开源可视化脚本没做兼容。如果你是在Windows上做数据集处理,直接把load_image封装成工具函数,全项目统一调用。
5.4 验证集类别缺失:mAP看着高,部署就翻车
现象:验证mAP@0.5看着有0.9,但现场测试时模型对少见类别完全不认识,比如“修补痕迹”这类框一个都预测不出来。
原因:划分数据时用了纯随机,某个类别样本太少,验证集里一个都没分到。训练时模型见过这个类,但验证流程里没有它的AP计算项,mAP被其他类别拉高了,问题被掩盖。
解决:回到4.2做分层划分,并且对每个类别单独看AP。验证集里单个类别的实例数低于30时,AP的方差会很大,一次验证跑出0.5一次跑出0.9都不奇怪。处理办法是把该类少量样本从训练集挪进验证集,也可以对少样本类别做复制增强,但复制时只能加进训练集,不要复制进验证集,否则验证结果虚高。
注意:市政路况里“修补痕迹”出现频率低,不代表甲方不关心。少样本类别的漏检往往是项目验收时被diss最多的问题。
5.5 空标签文件别乱删:负样本是训练的一部分
现象:labels目录里有一些0字节的txt文件,清理“脏数据”时把空标签删了,训练完模型在干净路面上疯狂误检,把阴影、伸缩缝、路面积水都标成了裂缝。
原因:空标签对应的是没有裂缝的路面图片,是刻意采集的负样本。YOLO的损失计算里背景项权重不低,负样本让模型学会“图片里可以没有目标”。把所有空标签删掉,等于告诉模型每张图都必须有目标,于是它开始乱标。
解决:保留空标签,并且在训练配置里把mosaic增强比例调低一点。mosaic会把多张图拼在一起,负样本图会和正样本图拼成一张,负样本的“纯背景”语义被稀释。如果用的是YOLOv8这类工程,mosaic默认值比较高,跑裂缝这种细长目标数据集时建议从0.5起步试。如果你确实要删空标签,必须连同对应的图片一起删除,绝不能只删txt不删图。
6. 进阶:把裂缝数据集转COCO格式,再用mmrotate做旋转目标检测
普通目标检测流程跑通之后,裂缝这类细长目标还有一个常见升级方向:旋转目标检测。水平框在斜向裂缝上会框进大量背景,NMS时相邻框互相抑制,定位精度上不去。把数据转成COCO JSON,接mmrotate做旋转框训练,是业界比较常见的做法。想玩开放词汇目标检测那套,也建议先把这个有监督旋转框基线跑出来。
6.1 转COCO JSON:把YOLO txt换成通用交换格式
转换脚本的核心是构造images、annotations、categories三块结构:
import json from pathlib import Path def convert(images_dir: Path, labels_dir: Path, output: Path, class_names: list): out_imgs, out_anns, ann_id = [], [], 0 for i, img_p in enumerate(sorted(images_dir.iterdir())): img = load_image(img_p) # 用5.3的兼容读法 h, w = img.shape[:2] out_imgs.append({"id": i, "file_name": img_p.name, "width": w, "height": h}) lab_p = labels_dir / (img_p.stem + ".txt") if not lab_p.exists(): continue for line in lab_p.read_text().strip().splitlines(): cls, cx, cy, bw, bh = map(float, line.split()) x = (cx - bw / 2) * w y = (cy - bh / 2) * h bw_px, bh_px = bw * w, bh * h out_anns.append({ "id": ann_id, "image_id": i, "bbox": [x, y, bw_px, bh_px], "area": bw_px * bh_px, "category_id": int(cls), "iscrowd": 0, }) ann_id += 1 output.write_text(json.dumps({ "images": out_imgs, "annotations": out_anns, "categories": [{"id": i, "name": name} for i, name in enumerate(class_names)], }, indent=2))COCO和YOLO的坐标表示不同:COCO的bbox是左上角x、左上角y、像素宽、像素高,YOLO是归一化中心点加宽高,转换时要做坐标原点和尺度的双重变换。area字段值是像素面积,iscrowd在裂缝数据里统一填0,表示没有群体目标。
6.2 旋转框是裂缝检测的加分项:从水平框到minAreaRect
mmrotate不能直接消费水平COCO框,但它支持把水平框转成旋转框作为baseline。对每个bbox的四个顶点,用cv2.minAreaRect求最小外接矩形,就能得到带角度信息的旋转框:
import cv2 import numpy as np box = [x, y, bw_px, bh_px] # 6.1的COCO bbox pts = np.array([ [x, y], [x + bw_px, y], [x + bw_px, y + bh_px], [x, y + bh_px], ], dtype=np.float32) rotated = cv2.minAreaRect(pts) # 返回 (cx, cy), (w, h), angle这个转换不依赖新标注,直接从水平框生成旋转框,粗粒度但足够当baseline。我之前用YOLO水平框做了一版裂缝检测,到弯道和斜接缝场景里NMS抑制特别严重,换旋转框后mAP提升了近3个百分点,代价是训练参数变多、收敛变慢。
我的习惯是:拿到数据集先做可视化体检,再固定种子重划数据,最后才碰train.py。这套流程看起来笨,但能让你后面调参时少遇一次“看起来正常却不知从何下手”的玄学问题。希望帮到你。
本文还有配套的精品资源,点击获取