多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

RoLabelImg旋转框标注全流程:从安装到mmrotate训练对接

RoLabelImg旋转框标注全流程:从安装到mmrotate训练对接 简介2022-RoLabelImg 是一款面向计算机视觉与机器学习研发者的图像标注工具尤其适合从事目标检测、自动驾驶等方向的研究人员与学生使用。该版本针对 Windows 环境做了专门优化修复了以往安装与运行中可能出现的兼容性故障解压后即可直接使用省去繁琐配置。资源包共 121 个文件约 32.62MB以 png 界面截图、py 源码脚本、pyc 编译文件、sample 示例数据及 svg 图标为主另含 xml 配置、sh 脚本与 rst 说明文档结构完整便于二次开发与查阅。目前已有 779 人学习下载。工具提供直观图形界面支持矩形、多边形、圆形及点线等多种标注方式可批量加载图像提升效率并支持导出 PASCAL VOC XML、YOLO YAML、COCO JSON 等主流格式方便对接 TensorFlow、PyTorch 等框架。其版本控制与自定义快捷键功能能有效避免标注进度丢失并加快操作速度是图像标注任务中值得信赖的实用选择。1. 旋转框标注这件事为什么 2022 年还有人回头翻 RoLabelImg如果你做过遥感影像、工业质检或者文档版面分析大概率遇到过这样的场景目标本身是斜的用水平框标注时框里塞满了背景模型学到的特征一半是噪声。2022 年我在做一个遥感舰船检测项目时就卡在这里水平框的 mAP 死活上不去换成旋转框后同样的 backbone 直接涨了十几个点。问题随之而来——旋转框怎么标labelImg 只能画水平框coco-annotator 部署又太重翻来翻去RoLabelImg 这个名字反复出现在搜索结果里。RoLabelImg 是 labelImg 的一个分支核心改动是支持旋转矩形框rotated bounding box标注输出格式兼容 PASCAL VOC 的 XML额外记录了旋转角度。它解决的就是「斜目标标注」这一件事适合做遥感、文本检测、工业缺陷检测的从业者。这篇不是科普我会把安装、标注、格式转换、训练对接和踩过的坑一次讲清楚让你看完就能在自己机器上跑通。2. RoLabelImg 的旋转框到底怎么表示角度、方向和坐标系2.1 旋转框的两种主流表示法在动手之前必须搞清楚一件事旋转框在数据里到底长什么样。目前主流有两种表示法RoLabelImg 用的是第一种。第一种是「中心点 宽高 角度」记作 (cx, cy, w, h, θ)。cx、cy 是框中心坐标w 是框的宽h 是框的高θ 是旋转角度。这种表示法在 OpenCV 的 minAreaRect 和 mmrotate 里都是默认格式好处是角度和尺寸解耦回归时比较稳定。第二种是「四角点坐标」直接存 (x1,y1,x2,y2,x3,y3,x4,y4)。这种表示法没有角度歧义但网络回归时四个点互相耦合训练容易抖。RoLabelImg 的 XML 里其实两种信息都有——它存了中心点和角度同时也能反算出四个角点。RoLabelImg 的 XML 结构大致是这样annotation folderimages/folder filenameship_001.jpg/filename size width1024/width height1024/height depth3/depth /size object nameship/name poseUnspecified/pose truncated0/truncated difficult0/difficult robndbox cx512.0/cx cy480.0/cy w120.0/w h40.0/h angle1.5708/angle /robndbox /object /annotation注意这里的关键点robndbox节点替代了普通 labelImg 的bndbox角度angle用的是弧度制不是角度制。这一点后面避坑章节会重点讲。2.2 角度定义最容易翻车的地方角度这件事不同工具的定义能差出 90 度甚至 180 度。RoLabelImg 的角度定义是以框的中心为原点w 边宽边与水平方向的夹角逆时针为正范围是 [0, π)。也就是说角度只在 0 到 180 度之间不会出现负角度或者超过 180 度的情况。为什么范围是 [0, π) 而不是 [0, 2π)因为一个矩形旋转 180 度和旋转 0 度在几何上是同一个框没必要区分。这个约定和 OpenCV 的 minAreaRect 一致但和某些论文里用 [0, 2π) 的定义不同。如果你拿 RoLabelImg 标的数据去喂一个假设角度范围是 [0, 2π) 的模型训练会直接崩。我一般会在标注前先确认三件事角度单位是弧度还是角度、角度范围是 [0,π) 还是 [0,2π)、角度零度对应的是水平还是垂直。这三个问题问清楚能省掉后面几天的调试时间。2.3 从 XML 反算四角点的公式很多时候训练框架要的是四角点需要从 RoLabelImg 的 XML 转换。转换公式如下import math def robndbox_to_corners(cx, cy, w, h, angle): 将 RoLabelImg 的中心点宽高角度转换为四角点 cx, cy: 中心点坐标 w, h: 框的宽和高 angle: 弧度制逆时针为正 返回: [(x1,y1), (x2,y2), (x3,y3), (x4,y4)] # 计算宽高的一半 dx w / 2.0 dy h / 2.0 cos_a math.cos(angle) sin_a math.sin(angle) # 四个角点相对于中心的偏移按逆时针顺序 # 顺序: 右下 - 右上 - 左上 - 左下取决于坐标系y轴方向 corners [] for sx, sy in [(dx, dy), (dx, -dy), (-dx, -dy), (-dx, dy)]: # 旋转矩阵作用于偏移量 rx sx * cos_a - sy * sin_a ry sx * sin_a sy * cos_a corners.append((cx rx, cy ry)) return corners这段代码的逻辑是先把四个角点相对于中心的偏移量算出来然后用旋转矩阵旋转最后加回中心坐标。参数说明——angle必须是弧度如果你从 XML 里读出来是弧度就直接用如果是角度记得先math.radians()转一下。w和h的顺序不能反RoLabelImg 里 w 是框的长边还是短边取决于你标注时怎么拉的这个后面也会讲。提示转换完的四角点建议用 OpenCV 的cv2.polylines画出来肉眼核对一遍尤其是角度接近 0 或 π/2 的框最容易看出方向对不对。3. 从零跑通 RoLabelImg安装、标注、导出全流程3.1 环境准备与安装RoLabelImg 是 Python 写的依赖 PyQt5 和 lxml。我一般用 conda 建一个干净环境避免和系统里的 Qt 冲突。# 创建独立环境Python 版本建议 3.8 到 3.10 conda create -n rolabelimg python3.9 -y conda activate rolabelimg # 安装依赖 pip install PyQt5 lxml # 从源码运行假设你已经把仓库 clone 到本地 cd RoLabelImg python rolabelimg.py如果你不想用 conda用 venv 也行但要注意 PyQt5 在某些 Linux 发行版上需要额外装系统库比如libxcb-xinerama0。Windows 用户一般直接 pip 装就能跑macOS 上如果报 Qt 插件错误通常是 PyQt5 版本和系统不匹配换个版本试试。启动后界面和 labelImg 几乎一样区别在工具栏上多了几个旋转相关的按钮。左侧是文件列表中间是画布右侧是标注框列表。3.2 标注旋转框的完整操作标注流程和 labelImg 类似但有几个关键差异第一步打开图片目录。点「Open Dir」选择你的图片文件夹再点「Change Save Dir」选 XML 的保存目录。建议图片和 XML 分开放后面转换脚本好处理。第二步切换标注模式。工具栏上有个按钮可以在「水平框」和「旋转框」之间切换图标是一个带角度的矩形。一定要确认切到旋转框模式否则画出来的还是普通 bndbox。第三步画框。在旋转框模式下鼠标左键点一下确定框的中心然后拖动确定大小松开后再移动鼠标调整角度最后点一下确认。这个交互和水平框的「对角拖拽」不一样第一次用会有点不习惯。第四步微调。画完之后框的四个角上有控制点可以拖动调整。角度也可以通过拖动框边缘来改。如果角度差一点点可以用键盘的方向键微调具体快捷键在菜单里能查到。第五步保存。按 CtrlS 保存当前 XML或者点「Save」按钮。建议每标十几张就批量保存一次避免软件崩了丢数据。3.3 批量导出与格式检查标完一批之后先别急着拿去训练做一次格式检查。我一般写个小脚本扫一遍所有 XML确认没有空框、角度越界、坐标超出图片范围这些问题。import os import xml.etree.ElementTree as ET import math def check_rolabelimg_xml(xml_dir, img_width1024, img_height1024): 检查 RoLabelImg 导出的 XML 是否有常见问题 issues [] for fname in os.listdir(xml_dir): if not fname.endswith(.xml): continue path os.path.join(xml_dir, fname) tree ET.parse(path) root tree.getroot() for obj in root.findall(object): robndbox obj.find(robndbox) if robndbox is None: issues.append(f{fname}: 缺少 robndbox 节点) continue cx float(robndbox.find(cx).text) cy float(robndbox.find(cy).text) w float(robndbox.find(w).text) h float(robndbox.find(h).text) angle float(robndbox.find(angle).text) # 检查角度范围 if angle 0 or angle math.pi: issues.append(f{fname}: 角度 {angle} 超出 [0, pi) 范围) # 检查框是否超出图片边界粗略判断 if cx - w/2 0 or cx w/2 img_width: issues.append(f{fname}: 框水平方向可能超出图片) if cy - h/2 0 or cy h/2 img_height: issues.append(f{fname}: 框垂直方向可能超出图片) # 检查宽高是否合理 if w 0 or h 0: issues.append(f{fname}: 宽或高非正数 w{w}, h{h}) return issues # 使用示例 problems check_rolabelimg_xml(./annotations) for p in problems: print(p) print(f共发现 {len(problems)} 个问题)这个脚本检查四类问题缺 robndbox 节点、角度越界、框超出图片、宽高非正。参数img_width和img_height要按你实际图片尺寸改如果你的图片尺寸不统一可以先读图片再传进来。跑完如果输出为空说明格式没问题可以进入下一步。3.4 转成训练框架要的格式RoLabelImg 的 XML 不能直接喂给 mmrotate 或 YOLO 系列需要转换。以 mmrotate 为例它要的是 DOTA 格式的 txt每行是x1 y1 x2 y2 x3 y3 x4 y4 category difficult。import os import math import xml.etree.ElementTree as ET def xml_to_dota(xml_dir, out_txt, img_width1024, img_height1024): 将 RoLabelImg XML 转为 DOTA 格式 txt 每行: x1 y1 x2 y2 x3 y3 x4 y4 category difficult with open(out_txt, w, encodingutf-8) as f: for fname in sorted(os.listdir(xml_dir)): if not fname.endswith(.xml): continue tree ET.parse(os.path.join(xml_dir, fname)) root tree.getroot() for obj in root.findall(object): name obj.find(name).text robndbox obj.find(robndbox) cx float(robndbox.find(cx).text) cy float(robndbox.find(cy).text) w float(robndbox.find(w).text) h float(robndbox.find(h).text) angle float(robndbox.find(angle).text) # 转四角点 dx, dy w/2.0, h/2.0 cos_a, sin_a math.cos(angle), math.sin(angle) corners [] for sx, sy in [(dx,dy),(dx,-dy),(-dx,-dy),(-dx,dy)]: rx sx*cos_a - sy*sin_a ry sx*sin_a sy*cos_a corners.append((cxrx, cyry)) # 裁剪到图片范围内 coords [] for x, y in corners: x max(0, min(img_width, x)) y max(0, min(img_height, y)) coords.extend([str(int(round(x))), str(int(round(y)))]) line .join(coords) f {name} 0\n f.write(line) # 使用 xml_to_dota(./annotations, ./train_dota.txt)转换时有两个细节要注意一是坐标要裁剪到图片范围内否则 mmrotate 读数据时会报越界二是角点顺序要一致DOTA 格式默认是顺时针或逆时针不同版本要求可能不同转换完最好可视化几张确认。4. 标注质量与效率那些影响模型精度的隐藏细节4.1 角度标注的一致性比精度更重要做旋转框检测最怕的不是标得不够准而是标得不一致。同一类目标有的标成 0 度有的标成 90 度模型根本学不明白。我见过一个项目两个人标同一批数据一个人习惯把长边当 w另一个人习惯把短边当 w结果角度差了 90 度训练 loss 一直震荡。解决办法是定一个标注规范对于有明确方向的目标比如舰船有船头船尾统一以某个方向为 0 度参考对于没有方向的目标比如圆形储罐统一把长边作为 w角度取 [0, π/2) 范围。规范定好之后先标 50 张让所有人对齐确认一致了再批量标。4.2 边界目标的处理策略图片边缘的目标怎么标直接影响模型在边缘的召回。常见做法有三种一是直接不标只标完整目标二是标出来但打上 truncated 标记三是标出来不管。我一般用第二种因为遥感图像里边缘目标很多全丢掉会损失不少正样本。RoLabelImg 的 XML 里有truncated字段标的时候在右侧面板勾上就行。训练时可以在 loss 里对 truncated 样本降权或者单独统计这类样本的召回率。4.3 用快捷键把标注速度提上去纯鼠标标注一张图要一两分钟用快捷键能压到二三十秒。RoLabelImg 支持自定义快捷键我一般会改这几个W 创建旋转框、A 上一张、D 下一张、CtrlS 保存、Del 删除选中框。具体在菜单「Edit」里能找到快捷键设置。另外一个小技巧是先把图片按目标密度排序先标密集的再标稀疏的因为密集图片标完后面会越标越顺手。这个习惯让我在一个 5000 张的项目里省了大概两天时间。5. 避坑与排查RoLabelImg 用起来最容易翻车的 5 个地方5.1 角度单位搞混训练 loss 直接爆炸现象转换完数据拿去训练loss 一开始就是几千甚至几万完全不收敛。原因RoLabelImg 的 XML 里 angle 是弧度制但有些转换脚本或者训练配置默认按角度制处理导致角度值差了 57 倍。解决在转换脚本里统一用弧度如果训练框架要角度在最后一步再转。检查方法是打印几个框的 angle 值正常应该在 0 到 3.14 之间如果出现 90 这种数就是单位错了。5.2 宽高顺序反了框的形状完全不对现象可视化转换后的框发现框的长宽比和标注时完全相反本来细长的船变成了扁的。原因RoLabelImg 里 w 和 h 的定义取决于你画框时先拉哪个方向不同人标出来的 w/h 顺序可能不一致。转换脚本如果假设 w 一定是长边就会出错。解决在转换时不要假设 w 和 h 的大小关系直接按 XML 里的值算四角点。如果训练框架要求 w 是长边在转换时判断一下如果 w h 就交换 w 和 h 并把角度加 π/2。5.3 坐标越界导致数据加载报错现象训练时 dataloader 报错提示坐标超出图片范围或者可视化时框跑到图片外面。原因标注时框的边缘可能超出图片边界RoLabelImg 不会自动裁剪导出的坐标就是负的或者大于图片宽高。解决在转换脚本里加裁剪逻辑把所有坐标 clamp 到 [0, width] 和 [0, height]。上面 3.4 节的脚本里已经加了这一步直接用就行。5.4 中文路径导致 XML 读取失败现象脚本读 XML 时报编码错误或者读出来的中文类别名是乱码。原因Windows 下路径含中文时某些 Python 版本的 XML 解析器默认编码不是 UTF-8。解决读文件时显式指定编码ET.parse(path)改成先open(path, r, encodingutf-8)再ET.parse。另外图片路径也尽量用英文避免不必要的麻烦。5.5 标完忘记保存软件崩溃全白干现象标了一下午软件突然闪退重新打开发现 XML 还是旧的。原因RoLabelImg 默认不会自动保存只有手动按 CtrlS 才写盘。解决养成每标 10 到 20 张就按一次 CtrlS 的习惯。另外可以定期把 annotations 目录备份一份我一般用rsync或者简单的cp -r定时跑。6. 把 RoLabelImg 数据接进 mmrotate一个可复现的最小训练配置标注只是第一步真正验证数据质量的是能不能训起来。这里给一个 mmrotate 的最小配置用前面转好的 DOTA 格式数据跑通训练。首先是数据目录结构按 mmrotate 的要求组织data/split_ss_dota/ ├── trainval/ │ ├── images/ # 所有训练图片 │ └── annfiles/ # 对应的 DOTA txt └── test/ ├── images/ └── annfiles/然后是配置文件的关键部分# configs/rotated_retinanet/rotated_retinanet_r50_fpn_1x_dota.py 的核心改动 dataset_type DOTADataset data_root data/split_ss_dota/ img_norm_cfg dict( mean[123.675, 116.28, 103.53], std[58.395, 57.12, 57.375], to_rgbTrue) train_pipeline [ dict(typeLoadImageFromFile), dict(typeLoadAnnotations, with_bboxTrue), dict(typeRResize, img_scale(1024, 1024)), dict(typeRRandomFlip, flip_ratio0.5), dict(typeNormalize, **img_norm_cfg), dict(typePad, size_divisor32), dict(typeDefaultFormatBundle), dict(typeCollect, keys[img, gt_bboxes, gt_labels]), ] data dict( samples_per_gpu2, workers_per_gpu2, traindict( typedataset_type, ann_filedata_root trainval/annfiles/, img_prefixdata_root trainval/images/, pipelinetrain_pipeline), valdict( typedataset_type, ann_filedata_root test/annfiles/, img_prefixdata_root test/images/, pipelinetest_pipeline), testdict( typedataset_type, ann_filedata_root test/annfiles/, img_prefixdata_root test/images/, pipelinetest_pipeline))几个关键参数说明img_scale设成 1024 是因为遥感图片通常比较大太小会丢小目标samples_per_gpu2是显存不够时的保守值显存够可以加到 4RRandomFlip是旋转框专用的翻转增强比普通 flip 多了角度处理。启动训练# 单卡训练 python tools/train.py configs/rotated_retinanet/rotated_retinanet_r50_fpn_1x_dota.py # 多卡训练 bash tools/dist_train.sh configs/rotated_retinanet/rotated_retinanet_r50_fpn_1x_dota.py 4训练起来之后重点看两个指标loss 是否稳定下降以及验证集的可视化结果框的方向对不对。如果 loss 降但可视化框方向乱大概率是角度定义和模型假设不一致回头检查 2.2 节说的那三个问题。验证数据质量还有一个土办法拿训练好的模型在训练集上推理如果训练集上框都画不准那一定是标注或转换有问题不用怀疑模型。这个习惯帮我省了很多次瞎调参的时间。最后说个我自己的教训早期做旋转框项目时我总觉得标注是脏活累活随便标标就行结果模型效果一直上不去回头查数据发现 30% 的框角度都是错的。后来我定了个规矩——标注规范先写清楚标完先抽检 10%确认没问题再批量。这个习惯比任何调参技巧都管用。希望帮到你。本文还有配套的精品资源点击获取
返回列表