简介:面向起重机crane目标检测任务,提供约2900张已标注图像数据及配套YOLO格式标签,类别为单一类别crane,适合作为计算机视觉初学者或进阶者的训练数据集。数据已完成训练集、验证集划分,可直接用于YOLOv5等主流检测框架的训练与评估,也可结合作者发布的改进实战教程进行网络结构优化实验。整个资源压缩包约146.53MB,共2000个文件,其中1999个为txt格式标签文件,另含1个Python脚本,用于标签可视化或数据整理,便于用户快速检查标注质量与样本分布。目前已有498人浏览学习,被用作目标检测入门练习和模型改进的基准数据。拿到压缩包后,可获得的不仅是一份标注规范的数据集,还包含按YOLO格式存储的边界框坐标、类别编号以及作者预处理脚本,能够大幅节省自行采集与标注时间,帮助快速启动起重机检测相关研究与工程实践。
1. 起重机 crane 图像目标检测:一份可直接开训的 YOLO 标注数据
YOLO 目标检测项目最耗时间的从来不是调模型,而是准备一份能直接开训的标注数据。约 2900 张起重机 crane 图像、单类别、已划分训练集和验证集、统一 YOLO 标注格式——这是我处理过最省心的一类数据集。但省心不等于不用复查:文件名带着 Roboflow 导出的哈希后缀,标签是归一化坐标的 txt,类别索引要对齐 classes 文件。标注格式一错,轻则 loss 不降,重则训练出来的模型把吊臂当背景。这篇从目录结构拆起,讲标注格式怎么读、show.py 怎么复核、四类高频格式坑怎么排查,最后落地到 YOLOv5 的 data.yaml 和训练参数,适合刚拿到数据准备跑第一次训练的新手,也适合想自查标注正确性的熟手。
2. 数据集结构拆解:文件命名、类别 ID 与训练验证划分
拿到数据集第一件事不是解压就跑,而是先摸清目录和文件之间的对应关系。这批起重机数据最明显的特征是“一张 JPEG 配一个同名 txt”,txt 是 YOLO 格式的标签,但文件名里带了一段哈希,不看清规则很容易在写 data.yaml 时翻车。
2.1 文件对应关系:一张 JPEG 配一个同名 txt
项目正文列出的文件带典型的 Roboflow 导出痕迹,比如:
n03126707_8705_JPEG.rf.31c56d01198ca7b2ac5c1d1b895409ad.jpg n03126707_8705_JPEG.rf.31c56d01198ca7b2ac5c1d1b895409ad.txt文件名拆成三部分看:n03126707是源图像集的类别编号,8705是图序,JPEG.rf.是 Roboflow 导出时附加的来源标记,最后那段 32 位十六进制字符串是 Roboflow 给每个样本生成的唯一 ID。关键结论:jpg 和 txt 必须放在同一个目录下,且主文件名完全一致,因为 YOLO 训练脚本就是靠os.path.splitext换扩展名来找标签的。
如果发现某张图片没有同名 txt,训练时会弹出WARNING: No labels found in ...。常见原因是导出时漏掉了空标签样本,或者是该样本因为没有目标被标注工具直接过滤。处理方法是写一个脚本比对两边文件列表,把缺标签的图片单独挪走,别让训练集里混入无标注图。
提示:不要手动去改文件名里的哈希段。YOLO 只关心主文件名一致,哈希冲突概率极低,改了反而容易破坏 jpg 与 txt 的配对关系。
2.2 YOLO 标注格式:归一化的 class x_center y_center width height
每张图的 txt 文件内容长这样:
0 0.5123 0.4781 0.3342 0.4156五个字段依次是:
| 字段 | 含义 | 取值范围 |
|---|---|---|
| 第 1 个 | 类别索引(class id) | 0 ~ nc-1,nc 是类别总数 |
| 第 2 个 | 目标框中心点的 x 坐标(归一化) | 0.0 ~ 1.0 |
| 第 3 个 | 目标框中心点的 y 坐标(归一化) | 0.0 ~ 1.0 |
| 第 4 个 | 目标框宽度(归一化) | 0.0 ~ 1.0 |
| 第 5 个 | 目标框高度(归一化) | 0.0 ~ 1.0 |
归一化的计算逻辑是:
x_center_norm = (x_min + x_max) / 2 / image_width y_center_norm = (y_min + y_max) / 2 / image_height width_norm = (x_max - x_min) / image_width height_norm = (y_max - y_min) / image_height这里的除以图像宽高很关键。很多人会写成除以 640,因为训练时 imgsz=640,这是错的。标注时图像是多少像素就除以多少,训练时 YOLO 内部会再做一次 letterbox 缩放,标签会跟着坐标一起变换,不需要在 txt 里提前归一化到 640。
如果看到某一行出现0 0 0 0 0,说明该标注框的宽或高被算成了零。这种行要直接删掉,否则训练时 YOLO 计算 IoU 会出现除零或 NaN,loss 曲线呈现诡异的锯齿形。
2.3 单类数据集的 classes 文件与 ID 管理
摘要里特别注明“类别个数【1】:起重机 crane【具体参考 classes 文件】”。单类别数据集最容易犯的错是把类别名直接当 ID 用。YOLO 的 txt 里写的是数字索引,不是字符串。
classes 文件内容一般是一行一个类名:
crane这份数据只有一行,所以 txt 里所有标签的第一位都应该是 0。如果哪一行出现1,就要警惕了:要么是标注工具自动补了背景类,要么是导出的训练集里混进了其他类。排查方法很简单:
awk '{print $1}' *.txt | sort -u正常输出应该只有0。如果出现其他数字,用脚本把所有非零首列的行打出来,看具体是哪些文件。这类问题在手工合并多个数据源时特别常见,crane 类和 tower 类混在一起,ID 对不上,训练时模型怎么学都学不好。
2.4 训练/验证划分怎么核实
摘要说已经做了训练集、验证集划分。YOLOv5 的目录分层一般是:
datasets/crane/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── classes.txt验证划分是否有效,不能只看目录里文件的数量,要检查三件事:
第一,训练和验证的图片不能有交集,用文件名集合求交集即可; 第二,每个 train/val 目录下图片数和标签数要相等; 第三,验证集里不能只有简单样本——随机划分下,复杂背景、小目标、遮挡场景要均匀分布。
如果发现验证集里全是近景大图,训练出的模型 mAP 可能虚高,换到真实工地场景立刻掉点。校验代码不长:
import os train_img = set(os.listdir('images/train')) train_lbl = set(os.listdir('labels/train')) val_img = set(os.listdir('images/val')) val_lbl = set(os.listdir('labels/val')) print('train img-lbl 差集:', train_img - train_lbl) print('val img-lbl 差集:', val_img - val_lbl) print('train/val 图片交集:', train_img & val_img)注意:Roboflow 导出默认还会生成
_train.txt、_val.txt这类路径清单文件,YOLOv5 的 data.yaml 可以直接引用它们,也可以不用、只靠目录结构。路径清单文件里的绝对路径换机器后会失效,所以我一般建议用目录结构 + data.yaml 的相对路径写法。
3. 标注可视化复检:show.py 脚本的用法与边界
2900 张图,如果只凭肉眼抽查,大概率漏掉坐标越界、标签错位、框尺寸异常这类问题。写一个 show.py 把标注框叠加到原图上,是最直接的质检手段。这个脚本在数据集里是自带的,但很多人拿到后不知道它能干什么、参数怎么改,这一章把它拆开讲清楚。
3.1 为什么要自写校验脚本而不是直接开训练
YOLO 训练过程中即使标签有问题,往往只在日志里打一行 warning,训练继续跑,最后模型在验证集上的 mAP 惨不忍睹,你甚至说不清是模型的问题还是数据的问题。show.py 的核心作用就是在训练前把数据问题暴露出来。
脚本的逻辑不复杂:读图片 → 读同名 txt → 把归一化坐标还原成像素坐标 → 用 OpenCV 画框和类别文字 → 保存或逐张显示。最大的价值是可以批量跑,把有问题的标注以图片形式输出,比看日志直观得多。尤其对 crane 这类结构不规则的目标,吊臂细长、支撑架镂空,标注框稍微偏一点肉眼很容易看出来。
3.2 show.py 的代码实现与逐行说明
数据包里自带的 show.py 逻辑骨架大致如下(具体行数以包内脚本为准):
import cv2 import os image_dir = 'images/train' label_dir = 'labels/train' output_dir = 'check_visual' os.makedirs(output_dir, exist_ok=True) for img_name in os.listdir(image_dir): if not img_name.endswith(('.jpg', '.jpeg', '.png')): continue img_path = os.path.join(image_dir, img_name) label_path = os.path.join(label_dir, os.path.splitext(img_name)[0] + '.txt') if not os.path.exists(label_path): print(f'[skip] 缺标签: {img_name}') continue img = cv2.imread(img_path) if img is None: print(f'[error] 图片无法读取: {img_path}') continue h, w = img.shape[:2] with open(label_path, 'r') as f: lines = f.read().strip().splitlines() for line in lines: parts = line.split() if len(parts) != 5: print(f'[error] 标签字段数异常: {label_path}: {line}') continue cls_id = int(parts[0]) x_center = float(parts[1]) * w y_center = float(parts[2]) * h box_w = float(parts[3]) * w box_h = float(parts[4]) * h x1 = int(x_center - box_w / 2) y1 = int(y_center - box_h / 2) x2 = int(x_center + box_w / 2) y2 = int(y_center + box_h / 2) if x1 < 0 or y1 < 0 or x2 > w or y2 > h: print(f'[warn] 越界框: {img_name} 类别{cls_id}') cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img, f'crane:{cls_id}', (x1, max(y1 - 5, 0)), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite(os.path.join(output_dir, img_name), img) print('可视化校验图已输出到:', output_dir)四个关键点:
一是归一化坐标还原时必须用img.shape拿到的原始宽高 h、w,不能用训练尺寸。因为标注是在原图上做的,只有用原图宽高还原出的像素坐标才能正确画出框。
二是越界判断写的是x1 < 0或x2 > w。归一化坐标理论上应该在 0~1 之间,但标注工具自动生成时偶尔会产出 1.05 这样的轻微越界值,训练时 letterbox 会把它裁掉,导致框和目标的边缘对不齐。提前打出来可以决定是修标注还是删样本。
三是 OpenCV 的putText在 y1 接近 0 时文字会画到画面外,所以写了max(y1 - 5, 0)做下限保护。
四是打印信息要分级别:缺标签用[skip],坐标问题用[warn],字段异常用[error]。批量跑完后直接按级别筛选日志,把问题按严重程度排队处理。
3.3 参数改动:多类显示、框线粗细与过滤坏标注
如果你的使用场景不是单类 crane,而是把这份数据集和其他数据合并,show.py 要做两处改动。
第一处是颜色映射。单类时所有框都用绿色没问题。多类时最好按 class id 生成固定颜色,避免同屏多个类别分不清:
colors = [ (0, 255, 0), # class 0 绿色 (0, 0, 255), # class 1 红色 (255, 0, 0), # class 2 蓝色 (0, 255, 255) # class 3 黄色 ] color = colors[cls_id % len(colors)]第二处是过滤坏标注。越界的框如果只是轻微超出,可以直接画出来供人工判断;但 w 或 h 算出来为 0 的框必须单独过滤,画出来的话会是一条线,影响判断。常见做法是加一个面积阈值:
if box_w < 1 or box_h < 1: print(f'[error] 尺寸为零: {img_name}: {line}') continue这样跑出来的校验图才不会被无效框干扰。另外注意:cv2.rectangle 画框时如果 x2 超过了图片宽度,OpenCV 不会报错,但画出来的框会被截断,所以越界判断必须在画框之前做。
提示:show.py 默认逐张保存到 check_visual 目录。如果只想抽查而不是全量输出,可以加一个
sample_ratio参数,比如0.2表示每 5 张抽查 1 张,省时间省磁盘。我自己跑 2900 张全量也就几分钟,但碰到超大数据集时抽样能快很多。
4. 标注数据避坑指南:四类高频格式陷阱与排查方法
跑 YOLO 训练翻车最多的阶段不是训练本身,而是数据接入。2900 张图听起来不多,但一旦格式问题成片出现,改起来足够让人怀疑人生。这一章把我在标注数据上踩过的、以及帮别人排查过的四类高频问题列出来,每条都按“现象 → 原因 → 解决”写。
4.1 现象:训练日志提示 No labels found,训练集被跳过
训练刚开始,终端刷出一排WARNING: No labels found in .../labels/train,然后 loss 全程不下降。
原因通常是两个目录配对失败。第一种是目录结构写成了 labels 下的子目录层级和 images 不一致,比如 images 下有train/和val/两个子目录,labels 下却只有train/,val 标签全找不到。第二种是 yaml 里train、val路径直接指向了 images 子目录,而 YOLOv5 是拿着 images 路径去换 labels 路径,目录配错就全盘找不到。
解决方法是先确认目录树,再用 find 命令比对两边数量:
find images/train -name "*.jpg" | wc -l find labels/train -name "*.txt" | wc -l两个数字必须一致。不一致就拿第 2 章的 Python 脚本定位缺哪个文件,缺标签的图片挪出训练目录,不要让训练代跑。
4.2 现象:训练能跑,但 loss 曲线在 0.1 附近高位震荡,mAP 不超过 0.3
loss 能降下去说明流程通,但降不到低位,多半是标签里有大量低质量框。
具体到这份 crane 数据,最常见的是两种:一是目标框把整个吊臂包含进去但落下了吊钩部分,导致学习目标不完整;二是归一化坐标里混入了逗号分隔的格式,比如0, 0.5, 0.4, 0.3, 0.2,YOLO 按空格分割后把0,当成类别索引,后面的值整体错位,框全画歪。
解决方法是写一个解析函数,先把每行的逗号去掉再按空格切分,兼容两种写法。同时统计每个框的宽高比,crane 的框宽高比通常在 0.3~3 之间,如果出现 0.01 或 100 这种极端值,基本可以判定是格式错位:
import os for txt_file in os.listdir('labels/train'): with open(os.path.join('labels/train', txt_file)) as f: for i, line in enumerate(f, 1): parts = line.strip().replace(',', ' ').split() if len(parts) != 5: print(f'{txt_file}:{i} 字段数={len(parts)}') continue try: vals = [float(p) for p in parts] except ValueError: print(f'{txt_file}:{i} 无法转float: {line}') continue w, h = vals[3], vals[4] if w <= 0 or h <= 0: print(f'{txt_file}:{i} 宽或高<=0: {line}')4.3 现象:训练正常,但验证输出把起重机标成了别的类别
这种情况在单类数据集里不多见,但一旦出现,原因大概率在 classes 文件顺序上。YOLOv5 的类别映射不是按文件名排的,而是严格按 classes 文件中类名出现的顺序生成索引。如果这份数据集你后来追加了其他类,在 classes 文件末尾加了一行新类,而某些标签文件里新类的 ID 已经写成了 0,那 crane 就会被识别成新类。
解决办法是重新统一标签:把所有 txt 的类别 ID 按新 classes 顺序重映射。写一个简单的字典映射即可:
old_to_new = {0: 1, 1: 2, 2: 0} # 示例:原ID0->新ID1,原ID1->新ID2,原ID2->新ID0 import os for txt_file in os.listdir('labels/train'): path = os.path.join('labels/train', txt_file) with open(path) as f: lines = f.readlines() with open(path, 'w') as f: for line in lines: parts = line.split() if not parts: continue cls = int(parts[0]) if cls in old_to_new: parts[0] = str(old_to_new[cls]) f.write(' '.join(parts) + '\n') else: print(f'[skip] 未知类别ID {cls}: {txt_file}')这个脚本在写回前没有加备份。实操建议先cp -r labels labels_bak,毕竟改错的成本比一条命令高得多。改完后再用第 2 章的awk命令复查首列分布。
4.4 现象:图片与标签数量对不上,差的正好是坏图
解压完 2900 张图,数了一遍 jpg 是 2900 个,txt 却只有 2896 个。差的 4 个文件对应的图片往往是打不开的坏图、零字节空文件,或者命名时带了空格。
cv2.imread 在遇到坏图时会返回 None,这个在 show.py 里已经兜住了。但如果数据直接进训练,YOLO 在数据加载阶段读到 None 就会报AssertionError: img is None。解决方法是先全量清洗一遍:
import cv2 import os import shutil bad_dir = 'bad_images' os.makedirs(bad_dir, exist_ok=True) for root, _, files in os.walk('images'): for name in files: if not name.endswith(('.jpg', '.jpeg', '.png')): continue p = os.path.join(root, name) img = cv2.imread(p) if img is None: shutil.move(p, os.path.join(bad_dir, name)) print(f'坏图已移走: {p}')零字节文件也可以顺手查:
find images -type f -size 0 -delete清理之后再跑一次数量比对,确保两边的数字一致。
4.5 一份可以复用的数据校验清单
把上面的坑收拢成固定流程,每次拿新数据集都跑一遍,比临时排查高效得多:
第一步,数量比对。find统计 images 和 labels 两边的数量,训练、验证分开数。 第二步,格式解析。检查每行字段数是否为 5、能否转 float、宽高是否大于 0。 第三步,类别 ID 范围。awk '{print $1}'统计出现的所有首列值,确保都在 [0, nc-1]。 第四步,可视化抽查。跑 show.py 输出 200 张左右,重点看小目标、遮挡和远距离的框。
提示:这一步检查不是只做一次。每次向数据集追加新图或重新标注后,都要重新跑一遍。我见过太多人加了图之后直接开训,结果新图用了另一种标注工具导出的格式,整个训练从头乱到尾。
5. 接进 YOLOv5 训练:data.yaml、超参与改进方向
数据质检通过之后,正式训练前的最后一道工序是把它接进 YOLOv5 的工程目录。这章讲 data.yaml 的写法、训练参数的设定规则,以及围绕这份 crane 数据可以做的改进参考。
5.1 data.yaml 配置:路径、nc 与 names
在 YOLOv5 的 datasets 目录下建好crane/结构后,data.yaml 长这样:
# 数据集路径,相对于 YOLOv5 工程根目录 path: ../datasets/crane train: images/train val: images/val # 类别数:单类 nc: 1 # 类别名,顺序与 classes 文件保持一致 names: 0: crane几个容易踩的细节:
train和val填写的是相对path的相对路径,不能写成images/train/带末尾斜杠,YOLOv5 拼接路径时多个斜杠不影响,但某些图像加载库会报路径错误。
nc必须和 names 列表长度相等。单类时 nc=1,names 只有一个元素。有些格式会写成names: ['crane']这种列表写法,YOLOv5 也能识别,但注意索引一定是 0 开始。如果 names 写成了['crane', 'tower']而 nc 还是 1,训练会直接报维度错误。
5.2 训练参数:imgsz、batch、epochs 与早停
以这份 crane 数据跑 YOLOv5s 为例,常用命令是:
python train.py --data data.yaml --weights yolov5s.pt --img 640 \ --batch 16 --epochs 100 --cache --patience 20参数含义不用多讲,但有几个针对这份数据的调整经验:
imgsz建议先 640 起。crane 目标在画面里通常占比较大,640 够用。如果后面部署到远距离抓拍场景,模型要识别的是画面里很小的塔吊,可以试 1280,代价是训练显存翻倍、推理变慢。
batch以显存为准。单卡 8G 跑 yolov5s、imgsz=640,batch 16 比较稳。如果显存不够,优先降 batch 而不是降 imgsz,否则目标太小边界会糊。
patience设 20 是防止验证集 mAP 到平台期后浪费算力。crane 是单类,模型通常 60~80 epoch 左右就收敛,100 epoch 上限够用了。
5.3 改进参考:efficient head、损失函数与 TensorRT 部署
原版 YOLOv5 的检测头对中等尺寸目标表现可以,但如果 crane 数据集里有大量远处小目标,可以考虑三个改进方向。
第一个是换 efficient head。它用更轻量的卷积组合替换原版 Detect 头的部分结构,参数量下降,小目标召回率略有提升。做法是把模型的 head 模块替换成 EfficientHead 类,保持输出维度不变,直接复用原训练脚本。注意替换后要重新跑 warmup,不能用原模型的 head 权重做 fine-tune 起点。
第二个是损失函数。YOLOv5 默认 CIoU loss 对遮挡目标有一定容忍度,但起重机吊臂细长、容易被钢缆遮挡,可以考虑把 box loss 换为 SIoU,它在角度对齐上有额外惩罚项。改一行配置即可:把hyp.scratch.yaml中的box=0.05保留,去utils/loss.py把 CIoU 相关分支换成 SIoU 的实现。改完单独跑一个 20 epoch 的对比实验,用验证集 mAP 决定是否保留。
第三个是部署侧的 TensorRT。训练收敛后导出 ONNX 再用trtexec --onnx crane.onnx --fp16转 TensorRT 引擎,1080p 25 帧的视频流下,单张 640 推理耗时可以压到十毫秒以内。但注意:TensorRT 的输入尺寸和 letterbox 逻辑必须和训练时完全一致,转换前先固定模型输入 shape,否则输出坐标整体偏移。
6. 部署前的最后一公里:验证模型输出与标注一致性
模型训练完,mAP 再好看,落地部署也绕不开“模型输出和原始标注是否对得上”这个问题。YOLO 预测输出的是归一化坐标,验证标签也是归一化坐标,两者坐标系定义完全一致,所以可以直接用一个小脚本把预测框和真实框叠加在同一张图上人工核对。
import cv2 import torch model = torch.hub.load('yolov5', 'custom', path='runs/train/exp/weights/best.pt', force_reload=True) model.conf = 0.25 model.iou = 0.45 img = cv2.imread('test.jpg') results = model(img) pred = results.xyxy[0].cpu().numpy() # [x1, y1, x2, y2, conf, cls] for x1, y1, x2, y2, conf, cls in pred: cv2.rectangle(img, (int(x1), int(y1)), (int(x2), int(y2)), (0, 0, 255), 2) cv2.putText(img, f'crane {conf:.2f}', (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imwrite('compare.jpg', img)核对标准不是“框是否都在物体上”,而是三点:一是远处小目标有没有漏检;二是目标部分遮挡时框是否过大或过小;三是同一场景多台起重机同时出现时,框之间是重叠还是各自独立。单类检测如果画面上出现了连续大块红色误检区域,基本可以判定训练集里缺了足够多的负样本场景——这一点在扩充数据时要格外留意。
我自己的习惯是每次训练收敛后,拿验证集里最难的 50 张图跑一遍这个可视化对比,再把预测结果和原标注重叠打印出来。从那以后,凡是新数据进项目,我都强制先走一遍第 4 章的校验清单,再做训练。数据没洗干净,后面所有改进都是无效功。希望这份 crane 数据集的操作笔记能帮你少走几步弯路。
本文还有配套的精品资源,点击获取