资讯详情

资讯详情

起重机小样本数据集双格式解析与YOLOv8训练实战

简介面向起重机目标检测任务提供一份包含689张真实场景图片的VOCYOLO双格式标注数据集。数据集中仅含Mobile_crane移动起重机一个类别共标注764个矩形框图片清晰且未做增强处理适合用于智慧工地安全监控、工业车辆识别等场景下的模型训练与算法验证。压缩包共2000个文件以jpg图片、xml标注和txt标签文件为主整体大小34.35MB三个文件夹分别存放图像、VOC格式标注与YOLO格式标注目录结构清晰便于直接进行数据集划分与格式转换。已有170人学习下载。标注信息完整且格式规范既满足YOLO系列模型的快速训练需求也可经转换用于Faster R-CNN、SSD等检测框架省去自行标注与格式整理的时间适合目标检测初学者练习及工程人员快速搭建起重机识别基线数据集。1. 起重机数据集是一个带双格式标注的小样本目标检测起点拿到“起重机数据集yolovoc格式-689张.zip”之后第一反应不是解压就跑训练而是先想清楚它到底解决了什么问题。689张图像意味着这是一个典型的小样本工业场景数据集——图像规模不大但标注信息是齐的VOC格式的XML里有目标框和类别名YOLO格式的txt里已经换成了归一化坐标和类别索引。对做施工安全监测、塔吊碰撞预警或者港口吊机识别的人来说这组数据可以直接当训练集用也可以用来练习标注格式互转和数据清洗。适合刚接触yolo训练自己的数据集的工程师也适合需要快速验证一套检测pipeline的老手。关键是先把两套格式的对应关系读明白训练才不会被格式问题卡住。2. 解析起重机数据集的双格式结构YOLO与VOC并存不是重复劳动2.1 解压后先确认目录与配对关系拿到zip先别急着改目录我一般会先解压并观察它的组织方式。一套常见的安排是images目录放jpglabels_voc目录放同名xmllabels_yolo目录放同名txt。两类标注描述的是同一批图像区别只在坐标编码方式上所以完全可以用一个脚本互相校验。unzip 起重机数据集yolovoc格式-689张.zip -d crane_dataset find crane_dataset -maxdepth 2 -type d | sort意料之中的输出结构是crane_dataset/ ├── images/ ├── labels_voc/ └── labels_yolo/接着要做三件事检查每张jpg是否同时存在xml和txt检查xml里的图像尺寸与实际图片是否一致检查txt文件是否非空。第一件最容易被忽视因为训练时YOLO对缺失标签的图片只会静默跳过并不会报错。for img in crane_dataset/images/*.jpg; do base$(basename $img .jpg) [ -f crane_dataset/labels_yolo/$base.txt ] || echo missing yolo label: $img [ -f crane_dataset/labels_voc/$base.xml ] || echo missing voc label: $img done这段循环的逻辑很简单用basename去掉jpg后缀再拼出预期路径-f判断文件是否存在。如果输出为空说明配对关系完整如果有输出不要直接补一个空文件而是找出缺失原因通常是下载解压过程中文件被中断。2.2 VOC格式里最该读的字段object、bndbox与sizeVOC格式的XML结构固定根节点是annotation下面有size节点记录图像宽高以及若干object节点。每个object里有name和bndbox后者包含xmin、ymin、xmax、ymax四个整数坐标。起重机数据集里常见的坑有三个bndbox出现宽高为0的退化框name字符串大小写不统一吊臂延伸到画面外导致框坐标超出图像边界。写一个健壮的解析函数顺便做边界修正import xml.etree.ElementTree as ET def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) objects [] for obj in root.findall(object): name obj.find(name).text.strip().lower() 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) if xmin xmax or ymin ymax: continue xmin, xmax max(0, xmin), min(img_w, xmax) ymin, ymax max(0, ymin), min(img_h, ymax) objects.append((name, (xmin, ymin, xmax, ymax))) return img_w, img_h, objects代码里有两个决定后续方向的动作全部转小写是为了避免Crane和crane被当成两个类别对越界框做裁剪而不是丢弃是因为吊臂出画是正常现象强行丢弃会让正样本数量进一步缩水。float接收坐标是因为部分标注工具会输出浮点值如果直接用int接收遇到浮点数时精度会在后续归一化中被放大。2.3 YOLO格式的txt五列数值的还原逻辑YOLO格式每行有五个数依次是class_id x_center y_center width height。前4个数值全部相对图像宽高做归一化范围通常在0到1之间。文件里没有类别名类别ID的含义完全取决于训练配置里的names列表顺序所以同一份txt在不同顺序的配置下会解读成完全不同的物体。def parse_yolo(txt_path, img_w, img_h): boxes [] with open(txt_path) as f: for line in f: parts line.strip().split() if len(parts) ! 5: continue cls int(parts[0]) cx, cy, bw, bh map(float, parts[1:]) xmin (cx - bw / 2) * img_w ymin (cy - bh / 2) * img_h xmax (cx bw / 2) * img_w ymax (cy bh / 2) * img_h boxes.append((cls, (xmin, ymin, xmax, ymax))) return boxes还原坐标时需要特别注意img_w和img_h必须来自与txt同名的原图而不是训练配置里的imgsz。一旦把640、416这样的推理尺寸当作归一化分母生成的像素框会整体偏移这是格式转换里最隐蔽的误差来源。表格可以作为快速对照特性VOC XMLYOLO txt坐标形式像素坐标左上右下归一化坐标中心点加宽高类别记录object/name 字符串类别ID整数依赖文件size节点提供图片宽高不需要但转换时要外部传入宽高常见错误bndbox越界或退化class_id与names顺序错位如果同一张图的xml和txt解析结果对不上优先以VOC为准。因为XML里保留了完整的类别名和像素坐标出问题后能直接定位YOLO格式经过一次编码反而不好反推原始标注意图。3. 把VOC重建成YOLO一套经得起推敲的转换脚本3.1 先固定类别映射表而不是写复杂解析逻辑VOC转YOLO最忌讳的做法是边读XML边动态分配类别ID。假设第一张图先出现crane第二张图先出现hook动态分配就会让同一类别在不同文件里对应不同ID训练时整个类别体系都是乱的。正确做法是先人工确认数据集的类别集合再写死一张映射表。起重机场景比较常见的类别是crane和hook有些版本还会带person和safety_helmet但任何类别的最终名单都以你解压出的xml里实际存在的name为准。import os import glob import xml.etree.ElementTree as ET CATEGORIES [crane, hook] mapping {name: idx for idx, name in enumerate(CATEGORIES)} def parse_voc(xml_path): tree ET.parse(xml_path) root tree.getroot() size root.find(size) img_w int(size.find(width).text) img_h int(size.find(height).text) objects [] for obj in root.findall(object): name obj.find(name).text.strip().lower() 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) if xmin xmax or ymin ymax or name not in mapping: continue objects.append((mapping[name], (xmin, ymin, xmax, ymax))) return img_w, img_h, objects def convert_voc_to_yolo(xml_path, output_dir): img_w, img_h, objects parse_voc(xml_path) base os.path.splitext(os.path.basename(xml_path))[0] lines [] for cls, (xmin, ymin, xmax, ymax) in objects: dw 1.0 / img_w dh 1.0 / img_h cx (xmin xmax) / 2.0 * dw cy (ymin ymax) / 2.0 * dh bw (xmax - xmin) * dw bh (ymax - ymin) * dh lines.append(f{cls} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}) with open(os.path.join(output_dir, base .txt), w) as f: f.write(\n.join(lines)) os.makedirs(labels_yolo, exist_okTrue) for xml_file in glob.glob(labels_voc/*.xml): convert_voc_to_yolo(xml_file, labels_yolo)这里的核心是把VOC的左上右下像素坐标变换成YOLO的中心点坐标cx取xmin和xmax的均值再除以图像宽度bw取差值再除以宽度。代码里故意没有做整除而是用乘法代替除法因为除法在浮点精度上更容易累积误差。输出保留6位小数对像素坐标来说已经足够如果只写4位某些极窄的吊钩框在归一化后可能会退化成宽度为0的数值。3.2 转换后的清点空文件、越界框和类别分布转换完成不等于转换正确。先用工具脚本统计每个类别的目标数量顺便找出内容为空的txtfrom collections import Counter counter Counter() empty_files [] for txt_path in glob.glob(labels_yolo/*.txt): with open(txt_path) as f: lines [line.strip() for line in f if line.strip()] if not lines: empty_files.append(txt_path) continue for line in lines: cls int(line.split()[0]) counter[cls] 1 print(per-class object count:, dict(counter)) print(empty label files:, len(empty_files))运行之后如果发现某个类别的目标数不到另一个类别的十分之一不要直接开训。YOLO虽然内置了类别损失加权机制但权重只在计算loss时生效对极端不平衡帮助有限。更实际的做法是回到原始图像确认这类目标是否真的只出现这么多次如果是后面要考虑重采样或者放弃该类别。3.3 重复图像与重复标注的排查小样本数据集最容易出现重复图像被放在不同文件名下。直接靠人眼看689张图不现实常见做法是用md5校验文件内容md5sum crane_dataset/images/*.jpg | sort | awk {print $1} | uniq -d如果输出了相同的哈希值说明有完全重复的图像。重复图像会导致训练集和验证集之间出现数据泄漏mAP虚高但实际部署时效果明显下降。处理办法是只保留其中一张并同步删除对应标签文件。4. 用YOLOv8在起重机数据集上跑一次完整训练4.1 按YOLO惯例组织train/val目录YOLOv8和YOLOv5的默认数据布局要求图像和标签分别位于images和labels目录下且训练子集放在train子目录、验证子集放在val子目录。常见的划分比例是85%训练、15%验证689张图大约对应586张训练和103张验证。python - EOF import glob import os import shutil from sklearn.model_selection import train_test_split images sorted(glob.glob(crane_dataset/images/*.jpg)) train_imgs, val_imgs train_test_split( images, test_size0.15, random_state42 ) for subset, paths in [(train, train_imgs), (val, val_imgs)]: os.makedirs(fcrane_dataset/images/{subset}, exist_okTrue) os.makedirs(fcrane_dataset/labels/{subset}, exist_okTrue) for path in paths: base os.path.splitext(os.path.basename(path))[0] shutil.move(path, fcrane_dataset/images/{subset}/{base}.jpg) shutil.move( fcrane_dataset/labels_yolo/{base}.txt, fcrane_dataset/labels/{subset}/{base}.txt, ) EOFtest_size0.15表示验证集占比对百级数据集是个合理经验值。random_state42用来固定随机种子保证后续每次实验的图像划分完全一致这样对比不同模型或超参数时才不会把数据差异也算进性能差异里。移动文件而不是复制文件是为了避免两个子集里出现同一张图。4.2 编写crane.yaml并理解每个字段进入训练前要写一个数据配置。以YOLOv8为例path: /absolute/path/to/crane_dataset train: images/train val: images/val names: 0: crane 1: hookpath字段建议写绝对路径因为训练进程的工作目录经常会变相对路径一旦找不到图像训练会直接中断或出现0样本警告。train和val是相对path的目录标签目录不需要写在配置里框架会自动把images替换成labels去读取。names的顺序就是类别ID的语义这里0对应crane、1对应hook和转换脚本里的CATEGORIES列表顺序完全一致。4.3 训练命令、三个关键超参数与收敛判定我通常从yolov8s.pt预训练权重开始而不是从零初始化。虽然689张图不算多但预训练权重里已经包含了对边缘、纹理等基础特征的表征在工业场景的吊车数据上微调可以在更少的epoch内收敛。yolo detect train \ datacrane.yaml \ modelyolov8s.pt \ epochs200 \ imgsz640 \ batch16 \ lr00.01 \ patience30 \ projectruns/crane \ nameexp_sepochs200对小样本偏多但有patience30兜底验证集连续30轮不提升就会提前停止不必担心训练时间过长。imgsz640与预训练权重输入尺寸一致原始图里吊钩即便很小先用640训练之后再考虑针对小目标做patch切分。batch16是8G显存环境下比较稳妥的选择如果训练时显存溢出优先把batch降到8不要直接降分辨率因为降低imgsz会把本来就小的吊钩进一步缩小损害召回率。训练过程的收敛可以看results.csv里的val/box_loss。前20轮这个值会快速下降之后进入缓慢波动期如果训练结束时曲线仍在明显下行说明epochs不够可以接着用--resume继续训练而不是盲目加大学习率。5. 小样本与小目标吊车数据集的增强和类别不平衡处理5.1 Mosaic增强什么时候开、什么时候关YOLOv8默认开启Mosaic增强把四张图拼接成一张相当于在每张视图里同时引入多尺度、多种光照和多个目标。这对泛化有利但有一个副作用四张图拼完之后大部分目标被等比缩小。对于吊车数据集来说吊钩本身就是小目标Mosaic反而会把它们缩得更小。常见做法是前几十轮开着Mosaic让模型学习多尺度特征后几十轮关掉用完整尺度的图像微调。用ultralytics的回调机制可以精确控制from ultralytics import YOLO model YOLO(yolov8s.pt) def schedule_mosaic(trainer): if trainer.epoch 160: trainer.args.mosaic 0.0 model.add_callback(on_train_epoch_start, schedule_mosaic) model.train(datacrane.yaml, epochs200, imgsz640, patience30)on_train_epoch_start会在每个epoch开始时触发此时trainer.epoch是当前轮数。把mosaic设为0之后后续epoch只使用平移、缩放、翻转等轻量增强。阈值160不是固定值建议配合patience设置如果预计模型在120轮左右收敛就设成100轮以后关闭。5.2 类别不平衡时先重采样再谈loss权重如果crane类有上千个框而hook类只有几十个框直接训练会让损失被多数类主导。修改loss权重是很多人的第一反应但YOLOv8的类别损失权重在超参文件里可以配置效果却不如数据层面重采样直观。import glob import shutil hook_images [] for txt_path in glob.glob(crane_dataset/labels/train/*.txt): with open(txt_path) as f: labels f.read() if any(line.startswith(1 ) for line in labels.splitlines()): hook_images.append(txt_path) for txt_path in hook_images: img_path txt_path.replace(/labels/, /images/).replace(.txt, .jpg) dup_idx len(glob.glob(txt_path.replace(.txt, _dup*.txt))) for k in range(dup_idx, dup_idx 2): shutil.copy(txt_path, txt_path.replace(.txt, f_dup{k}.txt)) shutil.copy(img_path, img_path.replace(.jpg, f_dup{k}.jpg))重采样的逻辑是把含少数类的样本复制两份后加入训练集让模型每个epoch看到更多hook样本。注意复制的必须是labels/train目录里的文件验证集不能动否则验证结果会失真。5.3 针对吊钩和吊臂的小目标优化切patch当吊车整体占据画面的上半部分吊钩只有几十个像素时全局检测效果通常不理想。与其直接追求更大的模型不如先做切片检测。把每张图切成带重叠的patch让模型在小patch上以更大的相对尺度看到吊钩。from PIL import Image def extract_patches(img_path, patch_size640, stride320): img Image.open(img_path) img_w, img_h img.size patches [] for y in range(0, max(img_h - patch_size 1, 1), stride): for x in range(0, max(img_w - patch_size 1, 1), stride): box (x, y, min(x patch_size, img_w), min(y patch_size, img_h)) patches.append((x, y, img.crop(box))) return patchespatch_size640和stride320组成50%重叠的滑动窗口目标被切到两半的概率降低。坐标还原时才要用到(x, y)偏移量预测得到的框加上偏移值才是原图坐标。切patch会让训练数据量倍增训练时间也相应增加这是小目标检测中比较常见的取舍。这种切patch方案配合前面Mosaic分阶段开关可以让吊钩这类小目标在不牺牲吊臂检测精度的前提下获得更高召回。6. 从权重到部署验证、导出ONNX与推理结果可视化6.1 在验证集上计算mAP并解读per-class结果训练结束后先跑一遍验证确认模型在没见过的图像上的表现yolo detect val \ datacrane.yaml \ modelruns/crane/exp_s/weights/best.pt \ imgsz640 \ conf0.25输出里重点看mAP50和mAP50-95。对吊车这种目标结构相对规整的工业场景mAP50超过0.7就算可用如果mAP50-95偏低说明预测框和真实框的重合精度不够问题大多出在标注框本身而不是模型容量。per-class的AP一栏能直接暴露出短板。如果hook类的AP比crane低一大截优先回到数据层面看hook样本的多样性而不是急着换更大的backbone。6.2 导出ONNX并在本地推理验证通过后把权重导出成ONNX方便脱离PyTorch环境部署yolo export modelruns/crane/exp_s/weights/best.pt formatonnx opset12 dynamicTrue导出后用onnxruntime做推理import onnxruntime as ort import numpy as np from PIL import Image session ort.InferenceSession(best.onnx, providers[CPUExecutionProvider]) img Image.open(crane_dataset/images/0001.jpg).resize((640, 640)) input_tensor np.array(img)[..., ::-1].transpose(2, 0, 1).astype(np.float32) input_tensor / 255.0 input_tensor input_tensor[None, ...] outputs session.run( None, {session.get_inputs()[0].name: input_tensor}, )transpose(2, 0, 1)把HWC布局转换成CHW/255.0是框架要求的归一化方式。session.get_inputs()[0].name用来动态获取输入节点名称不写死字符串防止不同导出选项下节点名变化导致兼容问题。6.3 部署中容易踩的三个坑第一个坑是坐标还原。ONNX输出对应的是640x640输入下的坐标需要按原始图像和输入尺寸的比例缩放回原图坐标系。如果直接拿输出框在原图上画位置会整体偏移。第二个坑是输出张量的解码方式。YOLOv8导出的ONNX在动态shape下输出维度通常是1x84xN其中前4位是cxcywh坐标后面是类别分数。做可视化前必须先把cxcywh转成x1y1x2y2再用阈值过滤低分框。第三个坑是推理帧率不足。如果边缘设备上模型跑不到实时先尝试把imgsz从640降到416看精度损失是否可接受或者用TensorRT精度模式重新转换而不是换成一个更小的模型从头训练。把这三个问题在项目初期就纳入验证流程能在部署阶段省下大量排查时间。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →