文章目录
- 一. OBB说明
- 二. yolo obb数据标签定义
- 三. yolo obb检测结果输出
- 四. anylabeling中的多边形标签转换yolo obb标签方法
- (一) 通过x-anylabeling进行转换
- (二) 通过脚本文件进行转换
一. OBB说明
OBB 全称Oriented Bounding Box(定向旋转边界框),在传统水平框(x,y,w,h)基础上增加旋转角度 θ,用 5 个参数精准贴合任意倾斜角度的目标,避免水平框框入大量背景、密集目标 IoU 失真漏检的问题。
方式 1:OpenCV 定义(最常用,DOTA、YOLOv8/11 OBB 默认)
- 角度范围:θ ∈ [-90°, 0°]
- 规则:以矩形最短底边为基准,从 X 轴水平方向顺时针旋转到该底边的角度;w、h 不强制区分长边短边,可互换。
方式 2:长边定义法(YOLO 系列主流)
- 规则:固定w 为矩形长边、h 为短边,θ 是长边与 X 轴正方向逆时针夹角,彻底避免角度周期性歧义。
二. yolo obb数据标签定义
由于一个 OBB 及其 180° 旋转是相同的,因此旋转定义为模 180°,且该框没有方向性。在内部,角度以弧度存储并归一化为 [-π/4, 3π/4) ([-45°, 135°)),边框宽度 w 被视为较长的一边,角度定义为从正 x 轴到 w 方向的顺时针角度。[0°, 90°) 形式是正则化的 DOTA 风格惯例,在训练或推理过程中并不适用。
- YOLO OBB 格式通过其四个角点的坐标来指定边框,这些坐标按照此结构在 0 到 1 之间进行归一化
class_index x1 y1 x2 y2 x3 y3 x4 y4 - 在内部,YOLO 以 xywhr 格式处理损失和输出,该格式表示 边界框 的中心点 (xy)、宽度、高度和旋转角度
三. yolo obb检测结果输出
旋转边框检测为每张图像返回一个 Results 对象。主要的预测字段是 result.obb,其中包含每个检测到物体的旋转边框、类别 ID 和置信度分数。
| 属性 | 类型 | 形状 | 描述 |
|---|---|---|---|
result.obb | OBB | (N) | 旋转边界框。 |
result.obb.data | torch.float32 | (N,7/8) | 包含置信度/类别的原始旋转框。 |
result.obb.xywhr | torch.float32 | (N,5) | xywhr旋转框。 |
result.obb.xyxyxyxy | torch.float32 | (N,4,2) | 四个角点。 |
result.obb.conf | torch.float32 | (N,) | 置信度得分。 |
四. anylabeling中的多边形标签转换yolo obb标签方法
(一) 通过x-anylabeling进行转换
- 将多边形标签转换为有向边界框
- 将有向边界框转为yolo obb标签
(二) 通过脚本文件进行转换
importosimportjsonimportrandomimportshutilfrompathlibimportPathfromPILimportImagedefget_image_size(img_path):"""获取图片宽高"""withImage.open(img_path)asimg:returnimg.width,img.heightdefanylabeling_json_to_obb_txt(json_path,img_w,img_h,save_txt_path):""" AnyLabeling JSON 转 YOLO OBB 标签txt OBB格式:class_id x1 y1 x2 y2 x3 y3 x4 y4 (全部归一化0~1) """withopen(json_path,"r",encoding="utf-8")asf:data=json.load(f)lines=[]forshapeindata.get("shapes",[]):label=shape.get("label")iflabelnotinCLASS_MAP:print(f"警告:未定义类别{label},跳过该目标")continuecls_id=CLASS_MAP[label]points=shape.get("points")iflen(points)!=4:print(f"警告:非4点旋转框,跳过{json_path}中的该标注")continue# 4个点归一化norm_points=[]forx,yinpoints:nx=x/img_w ny=y/img_h norm_points.append(f"{nx:.6f}{ny:.6f}")line=f"{cls_id}{' '.join(norm_points)}"lines.append(line)# 写入标签文件withopen(save_txt_path,"w",encoding="utf-8")asf:f.write("\n".join(lines))definit_folder():"""初始化输出文件夹结构"""img_dir=Path(OUTPUT_DIR)/"images"label_dir=Path(OUTPUT_DIR)/"labels"forsplitin["train","val","test"]:(img_dir/split).mkdir(parents=True,exist_ok=True)(label_dir/split).mkdir(parents=True,exist_ok=True)defgenerate_yaml():"""生成YOLO数据集yaml配置文件"""yaml_path=Path(OUTPUT_DIR)/"dataset.yaml"class_names=[kfork,vinsorted(CLASS_MAP.items(),key=lambdax:x[1])]yaml_content=f"""path:{os.path.abspath(OUTPUT_DIR)}train: images/train val: images/val test: images/test names:{chr(10).join([f"{i}:{name}"fori,nameinenumerate(class_names)])}"""withopen(yaml_path,"w",encoding="utf-8")asf:f.write(yaml_content)print(f"✅ 数据集配置文件已生成:{yaml_path}")defcollect_all_samples():"""递归遍历指定子文件夹,收集(图片,json)配对样本"""samples=[]root_path=Path(ROOT_DIR)# 如果指定了目标子文件夹,只遍历这些文件夹ifTARGET_SUB_FOLDERS:forsub_nameinTARGET_SUB_FOLDERS:sub_dir=root_path/sub_nameifnotsub_dir.exists():print(f"⚠️ 子文件夹{sub_name}不存在,跳过")continue# 在当前子文件夹下递归查找所有jsonforjson_fileinsub_dir.rglob("*.json"):img_path=NoneforsuffixinIMG_SUFFIX:temp_img=json_file.with_suffix(suffix)iftemp_img.exists():img_path=temp_imgbreakifimg_pathisNone:print(f"⚠️ 未找到{json_file.name}对应的图片,跳过")continuesamples.append((img_path,json_file))else:# 未指定子文件夹,遍历根目录下所有子文件夹forjson_fileinroot_path.rglob("*.json"):img_path=NoneforsuffixinIMG_SUFFIX:temp_img=json_file.with_suffix(suffix)iftemp_img.exists():img_path=temp_imgbreakifimg_pathisNone:print(f"⚠️ 未找到{json_file.name}对应的图片,跳过")continuesamples.append((img_path,json_file))print(f"📦 总共收集到有效样本数量:{len(samples)}")returnsamplesdefsplit_dataset(samples):"""按8:1:1随机划分数据集"""random.shuffle(samples)total=len(samples)train_num=int(total*TRAIN_RATIO)val_num=int(total*VAL_RATIO)train_samples=samples[:train_num]val_samples=samples[train_num:train_num+val_num]test_samples=samples[train_num+val_num:]print(f"📊 划分结果:")print(f"训练集:{len(train_samples)}张")print(f"验证集:{len(val_samples)}张")print(f"测试集:{len(test_samples)}张")return{"train":train_samples,"val":val_samples,"test":test_samples}defcopy_and_convert(split_name,sample_list):"""复制图片+转换json到obb标签并保存到对应数据集文件夹"""img_out_base=Path(OUTPUT_DIR)/"images"/split_name label_out_base=Path(OUTPUT_DIR)/"labels"/split_nameforimg_path,json_pathinsample_list:img_w,img_h=get_image_size(img_path)stem=img_path.stem# 复制图片dst_img=img_out_base/img_path.name shutil.copy2(img_path,dst_img)# 转换并保存obb标签dst_label=label_out_base/f"{stem}.txt"anylabeling_json_to_obb_txt(json_path,img_w,img_h,dst_label)defmain():random.seed(RANDOM_SEED)ifnotPath(ROOT_DIR).exists():print(f"❌ 错误:根文件夹不存在{ROOT_DIR}")returninit_folder()all_samples=collect_all_samples()ifnotall_samples:print("❌ 未找到任何图片+json配对样本,程序退出")returnsplit_data=split_dataset(all_samples)forsplit,samplesinsplit_data.items():copy_and_convert(split,samples)print(f"✅{split}集处理完成")generate_yaml()print("\n🎉 全部任务执行完成!")print(f"数据集输出目录:{os.path.abspath(OUTPUT_DIR)}")if__name__=="__main__":# ===================== 所有配置参数放在此处:if __name__下、main()调用前 =====================ROOT_DIR=r"D:\\test\\"OUTPUT_DIR=r"D:\\train\\"TRAIN_RATIO=0.8VAL_RATIO=0.1TEST_RATIO=0.1RANDOM_SEED=42IMG_SUFFIX=(".jpg",".jpeg",".png",".bmp")# 新增:需要处理的子文件夹列表,为空列表则处理全部子文件夹# 示例:TARGET_SUB_FOLDERS = ["folder1", "folder2", "folder3"]TARGET_SUB_FOLDERS=[]# 类别映射:标签名:类别ID(从0开始)CLASS_MAP={"rect3_1":0,}# =====================================================================================main()