简介:LabelImg 是一款可视化图像标注工具,面向从事目标检测任务的数据标注人员与算法学习者,用于为 Faster R-CNN、YOLO、SSD 等网络制作训练数据集,标注结果以遵循 PASCAL VOC 格式的 XML 文件保存。本资源为其源码包,适合无法访问 GitHub 的用户直接获取,运行前需配置 Python 与 lxml 环境。压缩包共 127 个文件,约 6.39MB,以 44 个 py 源码文件为核心,辅以 38 个 png 界面图标、6 个 sh 脚本、若干 txt、rst、md 说明文档及 cfg、yml 等配置文件,另含打包所需的 egg、qrc、icns 等工程文件,结构完整,便于本地安装、阅读源码与二次修改。目前已有 357 人学习下载,适合需要离线部署标注环境或研究工具实现细节的读者参考使用。
1. 拿到 labelImg-master.rar 先别急着解压:图像标注工具的定位与适用边界
如果你手上正躺着一个labelImg-master.rar,大概率是有人甩给你一个标注任务,或者你自己想给几十上百张图打框、准备训个检测模型。这个包就是 labelImg 的源码压缩包——一个用 Python + Qt 写的轻量级图像标注工具,核心产出是 Pascal VOC 格式的 XML 和 YOLO 格式的 txt。它不依赖 GPU,不联网,双击就能跑,特别适合中小规模数据集的人工标注、课程设计里的数据准备环节,以及需要离线作业的场景。但它也不是万能钥匙:没有自动预标注、没有多人协作、没有分割掩码,图像标注的活儿它能干,复杂标注需求得另找工具。下面按「解压能跑 → 快捷键提效 → 格式转换 → 踩坑排查」的顺序拆一遍,新手能照着复现,熟手能看清参数边界。
2. 从源码包到可运行环境:依赖、启动与目录结构
labelImg-master.rar解压后是一个典型的 Python 源码目录,不是打包好的 exe。这意味着你得自己把运行环境搭起来。很多人卡在第一步不是不会装 Python,而是没搞清楚这个包到底依赖什么、从哪个入口启动、配置文件在哪。这一章把环境这条链路走通。
2.1 解压后先看目录:哪些文件真正影响运行
解压出来大致是这么个结构(不同提交版本略有差异,但核心文件稳定):
labelImg-master/ ├── labelImg.py # 主入口,GUI 逻辑都在这 ├── libs/ # 核心模块目录 │ ├── canvas.py # 画布:画框、拖拽、缩放 │ ├── labelFile.py # 标注文件的读写(XML / txt) │ ├── shape.py # 单个标注框的数据结构 │ ├── yolo_io.py # YOLO 格式转换 │ ├── pascal_voc_io.py # Pascal VOC 格式读写 │ └── constants.py # 格式常量、默认路径 ├── data/ │ └── predefined_classes.txt # 预定义类别列表 ├── resources.py # Qt 资源(图标等),部分版本是编译好的 py └── requirements/ # 依赖清单目录真正影响能不能跑起来的是三样东西:labelImg.py是入口,libs/是功能实现,requirements/里列了依赖。data/predefined_classes.txt是很多人忽略但很关键的文件——你提前把类别名一行一个写进去,标注时就能用下拉框选,不用每次手敲,类别多了能省大量时间,也避免「person / Person / persone」这种拼写不一致的血泪教训。
2.2 依赖安装:PyQt5 与 lxml 是绕不开的两座山
labelImg 的 GUI 基于 PyQt5,标注文件解析用 lxml。常见做法是建一个独立虚拟环境再装,避免和系统里其他 Qt 版本打架:
# 建虚拟环境(Python 3.8~3.10 兼容性最稳) python -m venv labelimg_env # Windows 激活 labelimg_env\Scripts\activate # Linux / macOS 激活 source labelimg_env/bin/activate # 安装核心依赖 pip install pyqt5 lxml装完验证一下 Qt 是否真的可用:
python -c "from PyQt5 import QtWidgets; print('Qt OK')"能打印Qt OK说明 GUI 依赖没问题。如果报DLL load failed或could not find or load the Qt platform plugin,基本是 PyQt5 版本和系统不匹配,常见解法是降到pyqt5==5.15.9这类稳定版本重装。参数上没什么可调的,关键是版本对齐——Python 3.11 以上配老版本 PyQt5 容易出兼容问题,我一般直接用 3.9 或 3.10。
2.3 启动命令与首次配置:三个必须改的默认项
依赖装好后,在源码根目录执行:
python labelImg.py窗口能弹出来就算成功。但第一次用,有三处默认配置建议立刻改掉,否则后面标注会反复返工:
| 配置项 | 默认值 | 建议值 | 原因 |
|---|---|---|---|
| 保存格式 | PascalVOC | 按训练框架选 YOLO 或 VOC | 格式选错,后期要整批转换 |
| 自动保存 | 关闭 | 开启 | 防止切图时忘存丢标注 |
| 预定义类别 | 空 | 指向data/predefined_classes.txt | 统一类别名,避免拼写漂移 |
在界面左侧工具栏点「Change Save Dir」指定标注文件输出目录,点「Open Dir」选图片目录。保存目录和图片目录分开是常规做法,方便后续脚本批量处理。如果图片和标注混在一起,转换脚本遍历时容易把 XML 也当图片读进去,这是新手常翻的车。
3. 标注操作与快捷键:把重复劳动压到最低
工具能跑只是起点,真正决定效率的是快捷键肌肉记忆和格式选择的判断。这一章把标注流程拆成可复制的操作序列,并说清 VOC 和 YOLO 两种格式在什么场景下选哪个。
3.1 画框、改框、删框:一套完整操作序列
打开图片目录后,标准操作流是这样的:
- 按
W进入画框模式,鼠标拖拽出矩形,松手弹出类别输入框; - 输入类别名(或从预定义列表选),回车确认;
- 框画歪了,按
Ctrl+Z撤销,或点中框拖边角调整; - 框选错了,选中后按
Delete删除; - 当前图标注完,按
D下一张,A上一张; - 全部标完,按
Ctrl+S保存(开了自动保存则跳过)。
这套序列里最容易出错的是第 2 步的类别输入。如果没配预定义类别,手敲时大小写和拼写稍有偏差,生成的 XML 里就会出现多个「同类不同名」的标签,训练时模型会把它们当不同类别,精度直接受影响。所以前面强调的predefined_classes.txt不是可选项,是必做项。
3.2 VOC 与 YOLO 格式怎么选:看训练框架而不是看习惯
labelImg 支持两种主流输出,选哪个取决于你后面用什么框架训练:
<!-- Pascal VOC 格式:每张图一个 XML,坐标是绝对像素值 --> <annotation> <filename>img_001.jpg</filename> <size> <width>1280</width> <height>720</height> </size> <object> <name>defect</name> <bndbox> <xmin>312</xmin> <ymin>204</ymin> <xmax>498</xmax> <ymax>390</ymax> </bndbox> </object> </annotation># YOLO 格式:每张图一个 txt,每行 类别id 中心x 中心y 宽 高(全部归一化到 0~1) 0 0.3164 0.4125 0.1453 0.2583VOC 的坐标是绝对像素,可读性强,适合调试和人工核对;YOLO 是归一化中心点格式,直接喂给 YOLO 系列训练脚本。判断标准很简单:训练框架吃 VOC 就选 VOC,吃 YOLO 就选 YOLO。如果中途换框架,labelImg 自带格式转换入口(菜单里有 PascalVOC 与 YOLO 互转),但转换前务必备份原始标注——转换是覆盖式的,转错了没有后悔药。
3.3 批量预定义类别与标注一致性控制
类别一多,靠手敲必然出乱子。正确姿势是标注前先把类别清单写进data/predefined_classes.txt:
defect scratch stain missing_part每行一个,不要空行、不要逗号。启动后在菜单里勾选「Use default label」,画框时就能从下拉列表选类别。这一步的价值在团队协作时尤其明显——多人标注同一批数据,类别名统一了,合并标注文件时才不会出现同一类被拆成好几个标签的情况。我见过一个项目因为「crack」和「Crack」两种写法,训练时被当成两类,召回率莫名其妙掉了一截,排查半天才发现是标注阶段的拼写漂移。
4. 标注文件的结构与格式转换:从 XML 到训练可用的 txt
标完一批图,拿到手的是一堆 XML 或 txt,但训练脚本往往要求特定目录结构和特定格式。这一章讲清标注文件内部结构,并给出可复用的转换脚本,把「标完」推进到「能训」。
4.1 读懂 XML 字段:哪些字段训练时真正被用到
一份 VOC XML 里字段不少,但训练检测模型时真正被读取的只有几项:filename(对应图片名)、size下的width/height(用于坐标校验和归一化)、每个object的name和bndbox四个坐标。folder、path、segmented这些字段多数框架会忽略,但如果你自己写解析脚本,别假设它们一定存在——不同版本 labelImg 生成的字段完整度有差异,解析时用.get()兜底比直接索引稳。
一个容易忽略的点:bndbox坐标是左上角和右下角的绝对像素值,且 labelImg 允许框超出图片边界(拖拽时没限制)。训练前最好做一次坐标裁剪,把xmin/xmax夹到[0, width]区间,否则某些框架会因越界坐标报错或产生异常梯度。
4.2 写一个 VOC 转 YOLO 的转换脚本
下面这个脚本把 VOC XML 批量转成 YOLO txt,顺带做坐标裁剪和类别映射:
import os import xml.etree.ElementTree as ET # 类别映射:顺序决定 YOLO 里的 class_id,必须和训练配置一致 classes = ["defect", "scratch", "stain", "missing_part"] class_map = {name: idx for idx, name in enumerate(classes)} def convert(xml_dir, out_dir, img_w_default=1280, img_h_default=720): os.makedirs(out_dir, exist_ok=True) for fname in os.listdir(xml_dir): if not fname.endswith(".xml"): continue tree = ET.parse(os.path.join(xml_dir, fname)) root = tree.getroot() # 读图片尺寸,缺失时用默认值兜底 size = root.find("size") w = int(size.findtext("width", img_w_default)) if size is not None else img_w_default h = int(size.findtext("height", img_h_default)) if size is not None else img_h_default lines = [] for obj in root.findall("object"): name = obj.findtext("name") if name not in class_map: continue # 未登记类别直接跳过,避免 id 错位 box = obj.find("bndbox") xmin = float(box.findtext("xmin")) ymin = float(box.findtext("ymin")) xmax = float(box.findtext("xmax")) ymax = float(box.findtext("ymax")) # 坐标裁剪到图片范围内 xmin, xmax = max(0, xmin), min(w, xmax) ymin, ymax = max(0, ymin), min(h, ymax) if xmax <= xmin or ymax <= ymin: continue # 裁剪后无效的框丢弃 # 转归一化中心点格式 cx = (xmin + xmax) / 2 / w cy = (ymin + ymax) / 2 / h bw = (xmax - xmin) / w bh = (ymax - ymin) / h lines.append(f"{class_map[name]} {cx:.6f} {cy:.6f} {bw:.6f} {bh:.6f}") out_name = os.path.splitext(fname)[0] + ".txt" with open(os.path.join(out_dir, out_name), "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": convert("./annotations_xml", "./labels_yolo")逻辑说明:脚本遍历 XML 目录,逐文件解析object节点,把绝对坐标先裁剪再归一化。class_map的顺序就是 YOLO 的类别 id,必须和训练时的classes.txt或 data 配置严格一致,顺序错了模型学到的类别会整体错位。img_w_default和img_h_default是兜底参数,只在 XML 缺size字段时生效,正常情况用不到,但缺字段的脏数据在真实项目里并不罕见。转换后建议抽几张图用可视化脚本把框画回去核对,肉眼过一遍比盲信脚本靠谱。
4.3 目录组织:训练脚本认的目录长什么样
转换完只是有了 txt,训练脚本还要求图片和标签按约定摆放。YOLO 系列常见结构是:
dataset/ ├── images/ │ ├── train/ │ └── val/ └── labels/ ├── train/ └── val/图片和标签文件名必须一一对应(img_001.jpg对应img_001.txt),放在不同目录但同名。划分训练集和验证集时,常见做法是按 8:2 随机切分,切分脚本要保证图片和标签同步移动,只移图片不移标签是高频翻车点——训练时找不到标签,框架要么报错要么静默跳过,损失曲线看着正常但模型啥也没学到。
5. 避坑与排查:标注链路上最容易翻车的五个点
工具本身不复杂,但标注是个长链路活儿,从环境到格式到数据划分,每一环都有坑。这一章按「现象 → 原因 → 解决」记录五个高频问题,都是实操里反复遇到的。
5.1 启动报 Qt platform plugin 错误
现象:执行python labelImg.py弹窗失败,终端报could not find or load the Qt platform plugin "windows"。 原因:PyQt5 安装不完整,或系统里存在多个 Qt 版本互相干扰,插件路径没被正确加载。 解决:先pip uninstall pyqt5 pyqt5-tools清干净,再固定版本重装pip install pyqt5==5.15.9;仍不行就检查环境变量里有没有残留的QT_PLUGIN_PATH指向旧路径,清掉后重开终端。
5.2 标注保存了但文件是空的
现象:标完一批图,打开 XML 发现<object>节点一个都没有,或 txt 是空文件。 原因:多半是保存目录没设对,标注写到了默认目录;或者开了自动保存但图片目录切换后没重新指定保存路径。 解决:标注前先点「Change Save Dir」确认输出目录,标完第一张就打开对应文件核对内容,别等标完一百张才发现全是空的。这个检查花十秒,能省几小时返工。
5.3 类别 id 错位导致训练精度异常
现象:模型训练 loss 正常下降,但验证时某些类别完全识别不出,或类别张冠李戴。 原因:VOC 转 YOLO 时类别映射顺序和训练配置里的类别顺序不一致,class_id 整体偏移。 解决:转换脚本里的classes列表和训练 data 配置里的names必须逐字逐序一致。转换后抽一张图,把 txt 里的 id 反查回类别名,和原 XML 的name对一遍,确认无误再进训练。
5.4 图片和标签文件名不匹配
现象:训练脚本报「找不到标签文件」,或训练集数量比预期少。 原因:图片是img_001.JPG(大写扩展名),标签是img_001.txt,某些脚本按严格字符串匹配文件名主干,大小写或扩展名差异导致匹配失败;或划分数据集时只移动了图片。 解决:统一扩展名大小写,划分脚本里图片和标签成对操作,移动前先assert两边文件数相等。批量重命名用脚本做,别手动改。
5.5 框超出图片边界引发训练报错
现象:训练时偶发坐标越界异常,或某些样本 loss 突然飙高。 原因:labelImg 允许框拖出图片边界,XML 里记录了负坐标或超过宽高的坐标,部分框架不做裁剪直接使用。 解决:转换阶段统一做坐标裁剪(见 4.2 脚本里的max/min处理),裁剪后宽高小于阈值的框直接丢弃。这一步放在转换脚本里做一次,比训练时报错再回头查高效得多。
6. 进阶:用脚本批量校验标注质量,把返工挡在训练之前
标完、转完、目录也摆好了,很多人直接开训,结果训到一半发现标注有问题,回头查成本极高。我的习惯是在进训练之前强制跑一遍标注质量校验,把能自动发现的问题挡在训练之前。这一步不复杂,但收益很大。
校验主要看三件事:一是每个标签文件的坐标是否都在[0,1]区间内、宽高是否为正;二是图片和标签是否严格一一对应,有没有孤儿文件;三是每个类别的框数量分布,某个类别框数异常少或异常多,往往是漏标或误标的信号。
import os def validate(img_dir, label_dir, num_classes=4): imgs = {os.path.splitext(f)[0] for f in os.listdir(img_dir) if f.lower().endswith((".jpg", ".png", ".jpeg"))} labels = {os.path.splitext(f)[0] for f in os.listdir(label_dir) if f.endswith(".txt")} # 孤儿文件检查 only_img = imgs - labels only_label = labels - imgs if only_img: print(f"[警告] 无标签的图片 {len(only_img)} 张,示例: {list(only_img)[:3]}") if only_label: print(f"[警告] 无图片的标签 {len(only_label)} 个,示例: {list(only_label)[:3]}") class_count = [0] * num_classes bad_lines = 0 for name in labels & imgs: with open(os.path.join(label_dir, name + ".txt")) as f: for line in f: parts = line.strip().split() if len(parts) != 5: bad_lines += 1 continue cid = int(parts[0]) vals = list(map(float, parts[1:])) if cid >= num_classes or any(v < 0 or v > 1 for v in vals): bad_lines += 1 continue class_count[cid] += 1 print(f"[信息] 非法标注行: {bad_lines}") for i, c in enumerate(class_count): print(f"[信息] 类别 {i} 框数: {c}") if __name__ == "__main__": validate("./dataset/images/train", "./dataset/labels/train")这个脚本的num_classes参数要和类别数对齐,img_dir和label_dir分别指向图片和标签目录。输出里重点看两类信号:孤儿文件数量不为零,说明目录组织有问题;某个类别框数为零或远低于其他类别,要么是真没标到,要么是类别 id 映射错了。校验通过再开训,比训到一半发现数据问题从容得多。
从那以后我每次拿到标注数据,不管是谁标的、标了多少张,都强制先跑一遍这个校验脚本再进训练流程——有次就是靠它发现验证集里混进了一批没转格式的 XML,要是直接开训,验证指标会莫名其妙地难看,排查起来又是一轮黑匣子式的猜测。希望这套流程能帮到你,少走几段弯路。
本文还有配套的精品资源,点击获取