简介:Ybat - YOLO BBox 注释工具是一款面向计算机视觉中目标检测数据集准备的轻量级标注工具,支持YOLO以及基础的Pascal VOC、COCO格式,适用于需要快速生成边界框标注的开发者与算法工程师。工具完全在浏览器本地运行,无需上传图像,支持缩放和平移辅助精确框选,并提供内存自动保存与裁剪导出能力。资源包共15个文件、约215KB,核心包括ybat.html、ybat.js、ybat.css等前端脚本,以及README、CHANGELOG等参数配置和项目文档,结构紧凑可直接打开使用,已有78人学习/下载。通过该资源可获得完整的YOLO标注工具源码、ES6兼容配置说明及VOC/COCO扩展支持,便于根据自身需求调整parameters参数或嵌入标注流程,省去从零搭建工具的时间。同时也能在主流浏览器环境中稳定运行,适合小规模样本标注和算法验证前的数据准备环节。
1. Ybat:给YOLO数据打标时的“轻量派”选择,这套bbox注释工具到底解决什么问题
我最早接触Ybat是在处理一批D435i深度相机拍的工业零部件照片,准备做成YOLOv8检测训练集。LabelImg能装能用,但每次开图都要重新找保存目录,标完一张还得手动点下一张,两千张图标到两百张就开始烦躁。Ybat不一样,它只依赖OpenCV,一个py文件就能启动,鼠标左键拖一个矩形框就是一个bbox,按一下键翻下一张,标签直接落成YOLO训练的txt。对只需要矩形框的检测任务来说,它证明了一件事——打标不非得套重型GUI。适合刚跑通YOLO、急需自建数据集的开发者,也适合快速出一批初版标签验证模型可行性的阶段。这套zip解压开,主程序就是一个ybat.py,旁边是示例图片和class文件。
2. Ybat的坐标体系与bbox数据流:OpenCV回调、归一化参数与txt落盘
2.1 为什么轻量工具够用:Ybat与LabelImg、labelme的选型对比
选标注工具之前先想清楚你要什么格式的输出。YOLO检测任务的训练样本只关心两件事:目标属于哪个类、目标框在哪。矩形框足够表达这两件事,不需要多边形mask那些顶点坐标。所以labelme这类为实例分割设计的工具,在你只需要detection时反而是累赘——JSON导出后还得专门写转换脚本,中间多一步就多一个出错口。
Ybat与同类工具的核心差异体现在依赖和保存格式上:
| 工具 | 依赖 | 保存格式 | 标注速度 | 适合场景 |
|---|---|---|---|---|
| Ybat | opencv-python | YOLO txt | 快 | 纯矩形检测、快速建初版数据集 |
| LabelImg | PyQt5 | Pascal VOC / YOLO | 中 | 老牌通用工具,兼容性强 |
| labelme | PyQt5 | JSON | 中 | 实例分割多边形标注 |
| X-AnyLabeling | 完整GUI | 多格式 | 中 | 集成自动标注的进阶场景 |
Ybat直接输出txt,少掉一次VOC转YOLO或JSON转YOLO的转换。转换脚本不是不能写,但转换过程里容易丢精度,尤其是坐标从像素转归一化时四舍五入位数的差异。工具链越短,中间变量越少,训练集的质量越可控。这也是我在做快速验证时选择Ybat而不是重型GUI的原因——我的目标是尽快得到一批“格式干净、能用起来”的标签,而不是一个功能齐全的标注平台。
2.2 核心机制:鼠标回调、拖拽与坐标归一化
Ybat背后的逻辑不复杂,就是OpenCV窗口上的setMouseCallback。关键在处理左键按下、拖拽、抬起三个事件的衔接:按下时把坐标记为框的起点,抬起时和当前位置组成矩形。很多初次读源码的人会漏掉反向拖拽,也就是从右下往左上画框,如果不做大小比较,宽高算出来是负数,写进txt之后YOLO读取时直接判废。
import cv2 drawing = False start_x = start_y = -1 boxes = [] def on_mouse(event, x, y, flags, param): global drawing, start_x, start_y if event == cv2.EVENT_LBUTTONDOWN: drawing = True start_x, start_y = x, y elif event == cv2.EVENT_MOUSEMOVE: if drawing: img_copy = img.copy() cv2.rectangle(img_copy, (start_x, start_y), (x, y), (0, 255, 0), 2) cv2.imshow("ybat", img_copy) elif event == cv2.EVENT_LBUTTONUP: drawing = False x_min = min(start_x, x) y_min = min(start_y, y) x_max = max(start_x, x) y_max = max(start_y, y) boxes.append((x_min, y_min, x_max, y_max))这段代码把鼠标按下到抬起的过程收敛成一个bbox列表,img是每次切图时重新载入的全局变量。min、max那两行就是防反向拖拽的关键,别省。真省了,碰到习惯从右下往左上拖的标注员,宽度和高度变成负数,后面落盘就是脏数据。实时绘制用的img_copy是原图的拷贝,为的是不让预览框污染原图,否则下一框会叠出残影。
坐标归一化写在抬起后的另一个转换函数里,这是YOLO格式的核心转换:
h, w = img.shape[:2] x_center = (x_min + x_max) / 2 / w y_center = (y_min + y_max) / 2 / h box_w = (x_max - x_min) / w box_h = (y_max - y_min) / h label_line = f"{cls_id} {x_center:.6f} {y_center:.6f} {box_w:.6f} {box_h:.6f}"归一化把像素坐标压到0到1之间,YOLO训练时再按输入尺寸等比放回。这里有个细节:格式是class x_center y_center width height,不是x1 y1 x2 y2。宽高是除以整张图宽高的比值,而不是像素差值。写精度保留6位小数属于惯例,Ultralytics的dataset代码读txt时按float解析,精度低会导致框在resize到640后产生一两个像素的偏移。另外,x_center、box_w这些值算出来可能略大于1,贴边目标中心点接近0.99没问题,但如果某个坐标因为手抖超过1.05,训练时会被当作越界框丢弃。
2.3 数据落盘:txt标签与coco80类的映射关系
Ybat保存的目录结构一般长这样:
dataset/ ├── images/ │ ├── 0001.jpg │ └── 0002.jpg └── labels/ ├── 0001.txt └── 0002.txt图片和txt同名是YOLO数据集的铁律,路径上要求images和labels两个目录配对存在。0001.txt的内容是:
2 0.486328 0.531250 0.226562 0.318750 0 0.123047 0.654297 0.087891 0.141406第一列是类别id,第二三列是中心点归一化坐标,第四五列是宽高。类别id从0开始计,coco80的意思就是COCO数据集的80个类,对应names文件里第0行person、第1行bicycle这样的排列顺序。你在Ybat里选的类编号,最终落到txt第一列的数字,必须和训练时data.yaml里names的index严格一致。很多人问“coco80怎么读在yolo里”,其实问的就是names文件怎么被加载成类表。Ultralytics加载data.yaml时按names列表顺序分配index,和txt第一列的整数一一对应。换句话说,names文件的物理路径不重要,重要的是列表顺序。
3. 把Ybat跑起来:从装OpenCV到第一批标签落盘,参数与操作全流程
3.1 环境准备:Python虚拟环境与OpenCV安装
Ybat的依赖面收得比较窄,核心就是OpenCV。不建议直接在系统Python里pip装,先建虚拟环境省得以后装torch或onnx时版本打架。
python -m venv venv source venv/bin/activate pip install opencv-pythonWindows下激活命令是venv\Scripts\activate。国内网络下载opencv-python可能慢,可以先换源再装:pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python。装完验证import cv2能正常通过。如果是在没有图形界面的服务器上跑Ybat,imshow这一步会直接报错,要么本地装好X服务,要么把图片和标签目录挂到本机再标。
明确一点,Ybat只需要opencv-python,别顺手装opencv-contrib-python。contrib版把sift、surf这些算法一起打包进来,对标注工具一个都用不上,装多了反而可能有动态库冲突,import时报错排查起来很费时间。我一般会确认安装的是4.x版本,4.x的高DPI支持和waitKeyEx行为更稳定,能避开后面要讲的鼠标坐标漂移问题。
3.2 启动与界面操作:命令行参数与标注流程
Ybat这类工具的启动命令通常长这样,参数名在不同版本里可能略有差异:
python ybat.py \ --images ./dataset/images \ --labels ./dataset/labels \ --classes ./coco80.names \ --window ybat--images必填,告诉程序去哪读图;--labels指定标签输出目录,不传默认在图片目录旁边建一个labels文件夹;--classes传一个按行排列的类别名称文件,Ybat按行号作为类的index;--window就是窗口标题名,主要方便多开标注任务时区分。
整个标注流程可以压成这张操作表:
| 操作 | 说明 |
|---|---|
| 鼠标左键拖拽 | 画一个矩形bbox,松开完成 |
| 滚轮或A/D | 切换上一张下一张图片 |
| 右键点击图片 | 撤销最近一个框 |
| 空格 | 保存标签并跳到下一张 |
| Esc | 退出程序,退出前把当前图片的框写盘 |
这里有个取舍,有的版本边标边写,有的版本离开图片时才写。我更推荐后者,因为边标边写容易产生半截操作:右键刚撤销一个框,还没补画就切了图,那个缺失就永久留在txt里。> 提示:优先选“离开当前图片时才写盘”的模式,可以避免大量半截标签。
进入标注循环时,第一时间确认窗口显示的图片序号。起步时用三张图片试标一遍,保存后打开txt检查行数和框数,确认流程没问题再开始大批量标。这一步花不了两分钟,但能避免标了两百张才发现labels目录路径配错了的惨剧。
3.3 常用参数调整:颜色、按键与保存策略
觉得绿色框在暗色图片上看不清,那就改画框颜色和线宽。Ybat源码里这部分集中在一个绘制函数中:
cv2.rectangle(img_copy, (x_min, y_min), (x_max, y_max), (0, 0, 255), 3)三个通道默认(0, 255, 0),我习惯在深色工业图像上改成(0, 0, 255)红色,对比度更高;线宽从2改成3在4K分辨率下更醒目。注意修改的是img_copy上的绘制,每次绘制基于原图的副本,不要直接在原图画,否则框会叠加累积。
保存策略上,我会在源码里找保存函数,把write_label()里的文件名拼接逻辑确认一遍。常见做法是取图片的stem加.txt后缀,再拼到labels目录前面,比如images/img_001.jpg对应labels/img_001.txt。如果你在代码里发现它把绝对路径都拼进去,比如labels/C:/dataset/images/img_001.txt,那一定要改成只保留文件名,不然后续train.py按相对路径找标签时会全部落空。
4. 标签对齐YOLO训练管线:class映射、coco80陷阱与数据集校验
4.1 txt格式与损失函数的关系:为什么归一化坐标不能错
YOLO损失函数的任务就是让预测bbox和标签bbox尽可能重合。具体实现上,训练进程读取每个txt时按行解析出类别和坐标,把它当作损失计算的基准。如果标签里的中心点x_center写成0.48,而真实目标在0.75,那损失函数反传的梯度就会把预测框往0.48方向拽,这个框从头到尾就学错了。边界框的w和h写错同理,把框宽写成两倍,IOU计算直接崩掉,loss曲线可能一开始就nan。
很多人把txt这一行数字当成“配置文件”,以为随便改改无所谓。实际上这五个数就是训练样本的真实值,承担着梯度回传的正确方向。yolo损失函数对归一化坐标的精度敏感,中心点偏0.01,在640分辨率图片上相当于6个像素,足够让一个小目标完全错位。这就是为什么标注工具的精度直接决定模型上限——标注错位1像素,模型学到的就是偏移1像素的目标位置,后面的NMS筛选也会跟着受影响。
4.2 class文件对齐与coco80陷阱:names顺序错了模型白练
class对齐是训练集最容易翻车的一环。Ybat打开--classes文件时按行顺序分配数字:第一行是0,第二行是1。训练时Ultralytics读data.yaml里的names列表,也是按列表顺序分配数字。于是出现一个坑:同一个类别在两个文件里顺序不一致,标签里的数字含义就被偷换了。
最常见的案例就是COCO数据集。coco80的叫法来源于COCO的80个检测类别,从person开始,往下是bicycle、car、motorcycle,数据集自带的names文件与官方顺序一致。但如果你自己整理数据时重排过names,而历史标注数据没跟着重排,那所有txt第一列的数字都错位了。这个事故最阴险的地方在于:训练集看起来一切正常,loss也正常下降,但val集的mAP就是上不去,因为模型学的是错位的映射关系。
可以写个小脚本强制校验:
import yaml with open("data.yaml", "r") as f: cfg = yaml.safe_load(f) with open("coco80.names", "r") as f: names = [line.strip() for line in f if line.strip()] assert len(names) == cfg["nc"], f"类数量不一致: names={len(names)}, yaml={cfg['nc']}" for i in range(len(names)): if names[i] != cfg["names"][i]: print(f"顺序不一致: index={i}, names={names[i]}, yaml={cfg['names'][i]}")这段逻辑在很多数据集校验工具里都能见到。它把“给人看的类名”和“给训练用的配置文件”掰开揉碎对比一遍,当场抓出顺序错位。更彻底的做法是把names文件直接放在data.yaml里,Ybat启动时用同一个data.yaml作为--classes来源,从源头消除两份文件漂移的可能。
4.3 数据集自检脚本:坐标范围、类id与空标签一次性排查
标注完之后,跑一个全面自检脚本值得形成习惯。下面的脚本不依赖任何训练框架,纯Python就能跑:
import os, glob label_dir = "./dataset/labels" class_num = 80 for txt in sorted(glob.glob(os.path.join(label_dir, "*.txt"))): lines = [line.strip() for line in open(txt) if line.strip()] if not lines: print(f"[空标签] {os.path.basename(txt)}") continue for line in lines: parts = line.split() if len(parts) != 5: print(f"[格式异常] {os.path.basename(txt)}: {line}") continue cls_id = int(parts[0]) x_c, y_c, w, h = map(float, parts[1:]) if cls_id < 0 or cls_id >= class_num: print(f"[类id越界] {os.path.basename(txt)}: cls={cls_id}") if not (0 <= x_c <= 1 and 0 <= y_c <= 1 and 0 < w <= 1 and 0 < h <= 1): print(f"[坐标越界] {os.path.basename(txt)}: {line}") if w * h < 0.0001: print(f"[框过小] {os.path.basename(txt)}: {line}")自检脚本检查四类问题:空标签、行格式、类id范围、坐标范围。这里有一个容易被忽视的点——空标签。YOLO允许不完全标注,训练时空标签图片作为负样本参与背景loss计算,如果一张图里明明有目标却因为标注事故变成全背景,它不但不给模型提供正样本,还会轻微拉低置信度。框过小同样要警惕,小于万分之一面积的目标在YOLOv8里基本学不动,不是不能标,而是你得先确认backbone能否感知那么小的像素区域。
5. Ybat踩坑笔记:坐标漂移、回退无响应与格式半兼容
5.1 鼠标坐标漂移:4K屏与缩放比例的“玄学”
现象:在4K屏幕、Windows缩放125%的环境下用Ybat画框,框的位置总是比鼠标落点偏左上,越往屏幕右下偏得越严重。
原因:OpenCV的GUI是高DPI感知缺失的重灾区。系统把逻辑坐标换算成物理坐标,而OpenCV拿到的回调坐标还是逻辑坐标,两者不一致就出现偏移。这跟Ybat本身代码无关,纯是OpenCV窗口和Windows显示缩放之间的老问题。
解决:在ybat.py文件开头,import cv2之前加一段:
import ctypes try: ctypes.windll.shcore.SetProcessDpiAwareness(1) except Exception: pass import cv2加完如果还偏移,临时把显示缩放调回100%,标完再调回来,是最省事的绕法。血泪经验是别在缩放200%的笔记本上标框,不然你以为是工具的bug,其实是系统换了坐标系。
5.2 空格键回退无响应:输入法在吞键
现象:标完一张图按空格,窗口没反应,再按回车也不翻页,有时要多按两下才有响应。
原因:中文输入法处于光标模式时,空格键被当成选字确认键,本地程序根本收不到这个按键事件。这是所有OpenCV waitKey类程序的通病,不是Ybat独有。
解决:标注期间切成英文输入法,或者至少切到英文标点状态。保守一点的改法是用waitKeyEx同时监听多个键位,把原来监听空格的逻辑扩展成同时监听P和空格两个按键,虽然治标,但能在输入法忘切时兜个底。
5.3 中文路径导致黑屏跳过:OpenCV imread不背的锅
现象:图片放在中文目录下,Ybat打开后窗口黑屏或直接跳过这张图,txt里写了个文件但内容是空的。
原因:OpenCV的imread对Windows中文路径支持不完整,读到中文路径返回None,后续画布绘制全部失效。代码里对None做判空时,程序会表现得很“神经”,有时候黑屏,有时候直接不处理。
解决:预处理阶段把图片目录统一改成英文或拼音命名,批量改名:
python batch_rename.py --images ./dataset/images --prefix img_重命名成img_0001.jpg这种格式,既绕开中文路径,也让后续标签和图片配对更简单。从那以后我拿到任何图片数据,第一件事就是看目录和文件名有没有中文,有就先规范化。
5.4 图片重名覆盖:标签被无声写穿
现象:标到第20张时,发现第5张的标签文件行数不对,往下排查发现同一文件名被多次覆盖。
原因:标注工具按图片名写txt,如果数据目录里存在来自不同子目录的同名文件,比如A/01.png和B/01.png,前者标注完写入labels/01.txt,切到后者时又把01.txt覆盖了。这种错误在训练集中段才暴露,代价极大。
解决:数据装载阶段,在代码里对文件名做全局去重,发现冲突直接报错退出:
names = [os.path.basename(p) for p in glob.glob("**/*.jpg", recursive=True)] dup = [n for n in names if names.count(n) > 1] if dup: raise RuntimeError(f"重名文件: {set(dup)}")把重名当成硬错误处理,而不是落下警告继续跑,省得后面花几小时排查。
5.5 标签格式半兼容:多余空行与精度丢失
现象:Ybat生成的txt在Ultralytics训练时报corrupt label警告,打开文件发现行尾有多余空格,或者坐标只有两位小数。
原因:部分工具落盘时没做字符串strip,行尾带着\r或空格;另外用round而不是f"{:.6f}"格式化坐标,精度被截断。Ultralytics解析标签时对多余空字符有兼容,但对精度丢失非常敏感,框中心点裁切后位置偏差会被放大。
解决:统一用格式化输出,每条记录写成f"{cls} {x:.6f} {y:.6f} {w:.6f} {h:.6f}\n",最后一行也要带换行。自检环节额外统计文件行尾有没有\r或多余空白,一次写干净比事后清洗省事得多。
6. 进阶玩法:伪标签预打底,把Ybat从标注器变成精修器
6.1 半自动工作流:推理脚本生成候选框,人工只做删改
如果要标上千张图,全手动拖框确实太慢。有个很实用的做法是让模型先跑一遍,Ybat只负责精修。流程是:用现成的YOLOv8模型对未标注图片批量推理,生成一套伪标签txt,然后打开Ybat把这些txt读进来,删掉误检、补上漏检,人工工作量能压缩一半以上。
伪标签生成脚本很简单:
from ultralytics import YOLO model = YOLO("yolov8n.pt") model.predict( source="./dataset/images", conf=0.25, save_txt=True, project="./pseudo", name="labels", exist_ok=True, )conf是最关键的参数,调成0.25偏宽松,多出一些假正例没关系,因为后面要人工删;如果调成0.6,漏检会很多,精修补框反而费劲。save_txt=True让模型把推理结果直接落成YOLO格式txt,和Ybat的标签格式完全兼容。跑完后把pseudo/labels里的txt复制到dataset/labels,再用Ybat打开,你会看到图片上已经有模型画好的框,直接用右键删错框、左键补漏框就行。
实际操作里我还会让Ybat支持一个“跳过已有标签图片”的选项,这样精修时只需要处理模型跑过的那些图,而不是从头到尾再把所有图过一遍。检查伪标签质量时看两个指标:一是空标签的图片数量是否异常多,二是单张图片的框数量是否超出合理范围。如果模型在一张图里吐了三四十个框,多半是conf阈值太低加上背景噪声,值得先把模型调优再进精修环节。
伪标签打底这套流程的价值在于:模型负责记住大部分目标的位置,人工只负责修正边界和类别错误。从那以后我每次给新数据集打标,都强制先跑一轮伪标签,再拿Ybat把错框删掉、把漏框补上,确认无误后才允许进校验脚本。希望帮到你。
本文还有配套的精品资源,点击获取