简介:图像标注工具LabelImg是一款开源、高效的标注软件,专为计算机视觉中的目标检测与实例分割任务而设计,解决了人工标注图像边界框和多边形耗时耗力的问题。这份压缩包提供了该工具的完整源代码、项目配置和图形资源,面向需要批量处理图像数据的研究人员、算法工程师和学生,可用于快速生成符合PASCAL VOC等标准的XML标注文件。资源共118个文件,压缩包大小约6.95MB,核心为27个Python源文件,同时包含38个PNG图标、SVG矢量图、Shell启动脚本、Markdown文档,以及安装脚本、配置文件、Makefile等工程化构建工具,支持Windows、Linux和macOS多平台。目前已有704人学习下载,适合希望了解标注工具内部实现、参与开源项目或进行二次开发的读者。深入研读主程序与资源管理模块,可以掌握界面交互逻辑、文件组织方式和打包构建全流程,为搭建自定义标注流水线或扩展标注功能提供直接参考。
1. labelImg是什么:做目标检测绕不开的免费标注工具
我研究生阶段第一次跑目标检测实验,导师丢过来两百多张配电柜的现场照片,让我“先标出来”。当时我用画图工具一张张框,框到第11张就彻底崩溃了——不是觉得累,是意识到这么标的坐标根本没法用。后来用了labelImg这个工具,才发觉之前一周的笨功夫,别人半天就能干完。
labelImg是一个用Python写的图形化图像标注工具,界面基于PyQt5,功能纯粹:打开图片目录,鼠标画矩形框,填入类别名,保存成PascalVOC格式的XML或YOLO格式的txt。它解决的就是目标检测落地前最耗时最枯燥的数据准备环节,适合刚入门的小白,也适合要快速产出小批量验证样本的算法工程师。这个工具不需要训练、不需要显卡,解压即用,上手成本在标注工具里几乎是最低的。
2. 从压缩包到可运行:Windows与Ubuntu两条部署路径
拿到手的「labelImg-master图像标注工具.zip」解压后,本质上是整个项目的源码仓库。目录结构很典型:根目录下的labelImg.py是主程序,libs/目录放着界面控件和画框逻辑,data/目录里有预定义类别文件predefined_classes.txt,资源文件是resources.qrc。这是一个标准Python应用,不需要编译安装,部署的本质就一件事:把PyQt5和lxml两个第三方依赖装好,然后让Python能拉起主程序。
很多人在这一步就翻车,是因为把“解压”当成了“安装”,双击py文件没反应就以为包坏了。实际上labelImg的运行逻辑是解释执行,不是安装式启动。搞清楚这一点,后面所有报错都能顺着“环境变量→依赖缺失→解释器版本”这条线去查。
2.1 Windows下的安装:依赖顺序与两个常见启动报错
Windows上我最推荐的做法是先建虚拟环境再装依赖。不要图省事直接pip install到全局Python,尤其开发机上同时装了Anaconda和官方Python的情况,两个解释器抢PATH,装完依赖后根本不知道被哪边接管。我在新机器上的固定操作序列是:
cd labelImg-master python -m venv venv venv\Scripts\activate pip install PyQt5==5.15.6 lxml==4.9.1 python labelImg.py第一步进入解压后的源码目录;第二步创建的venv是一个独立的Python环境,所有依赖只装在这个目录里,不污染系统解释器;第三步是Windows下的激活语法,激活后终端提示符前面会出现(venv);第四步我用等号锁了版本号——PyQt5锁定5.15.6是因为5.15.7之后在部分Windows高分辨率缩放设置下出现过按钮错位,lxml锁定4.9.1是为了避开新版在Python 3.10上偶发的解析崩溃;最后一步直接以脚本方式启动GUI。
如果报「ModuleNotFoundError: No module named 'PyQt5'」,几乎都是开了新终端后忘了重新激活虚拟环境。注意PowerShell和CMD对激活命令的支持程度不一样,CMD里一定要先执行activate再跑python。如果报错是lxml缺失,补pip install即可。还有一个特别常见的现象:在文件管理器里双击labelImg.py,结果弹出了记事本窗口——这是Windows把.py文件关联到了编辑器,不是程序坏了,正确姿势是在终端里手动执行。
如果机器上有Anaconda,我建议换一条路:conda create -n labelimg python=3.8,然后pip install PyQt5 lxml。Python 3.8是我试过兼容性最稳的版本,3.10以上偶发和旧版PyQt5不兼容的情况。这样整条链路最干净,以后卸载也不会留垃圾。
2.2 Ubuntu 18.04上的部署:apt装依赖与源码启动的排查顺序
在Ubuntu 18.04上部署labelImg是另一套逻辑。系统自带的Python 3.6比较旧,apt源里也没有pip版PyQt5那么激进,所以最稳的组合是让apt装Qt界面库,pip只补lxml这一个解析库。我在Ubuntu 18.04上的命令序列:
sudo apt install python3-pyqt5 -y python3 -m pip install lxml==4.9.1 --user cd labelImg-master python3 labelImg.py第一行从apt源直接安装系统级PyQt5,这是Ubuntu下最省事的方式,绕开了pip在Linux上编译Qt库的一堆坑;第二行的--user参数解决权限问题;第三行进目录;第四行启动。注意这里不要再执行pip install PyQt5,否则会和系统自带库重复,运行时可能出现动态库装载冲突。
如果在桌面环境下双击没反应,我的排查顺序有固定套路:先echo $DISPLAY看有没有输出——通过SSH远程连服务器时这个变量通常是空的,GUI程序起不来,解决方法是回到物理终端执行,或者手动export DISPLAY=:0;再确认python3指向谁,如果which python3显示的是Anaconda路径,而Anaconda环境里又没装PyQt5,就会报无模块错误,此时要么进conda环境补装,要么把启动命令换成/usr/bin/python3 labelImg.py。
Linux下还有一个源码编译相关的坑:如果你从GitHub拉的代码比较新,直接跑可能会在资源加载时报AttributeError,提示libs.resources里缺属性。这是因为新版代码改了图标资源,但没有用Qt的编译工具重新生成resources.py。解决方式是在根目录执行:
pyrcc5 -o libs/resources.py resources.qrc这个命令把qrc资源文件编译成Python模块,labelImg在导入时才能找到对应的图标和样式。如果提示pyrcc5命令不存在,说明缺少PyQt5的开发工具包,apt install pyqt5-dev-tools装一下即可。
3. 亲手标注一版数据:操作拆解与格式落地
部署跑通只是起点,真正决定标注效率的是工作流。很多人把labelImg当画框软件用,打开、框选、保存,然后抱怨慢。实际上它内置了一套以键盘为主的操作体系,配合合理的目录规划,一小时标两百张图完全可行。这一章我把一个完整标注项目的操作细节、快捷键和产物格式全部拆开讲。
3.1 新建标注工程:目录结构、预定义类别与一张图的完整标注动作
拿到一批待标注图片后,先别急着打开labelImg。按我习惯,先建立这样的目录结构:
project/ images/ # 原始图片,jpg或png annotations/ # 标注产物,XML文件 classes.txt # 类别清单,一行一个图片文件名里不要带中文和空格,也不要放在带中文的路径下。这不是洁癖,而是labelImg的XML保存逻辑在部分Windows环境下对非ASCII路径处理不稳定,中文路径轻则保存报错,重则闪退。
启动labelImg后,默认界面顶部是菜单栏,左侧是工具栏,中间是图片显示区。第一次用需要先做两件事:在菜单Edit里选「Open Dir」打开images目录,再点「Change Save Dir」把保存目录指定为annotations。很多人只开了图片目录、没改保存目录,最后发现XML不知道存哪了。
画框的完整动作是:按键盘W键进入画框模式,在目标左上角按住鼠标左键,拖到右下角松开,弹窗里输入类别名,回车确认。一个目标框完,按D键切到下一张,继续画。一个标注样本的产出就是这么朴素——不需要在工具栏上点来点去。
类别名第一次输入后会被记录到predefined_classes.txt,但要注意这个文件只有在「保存目录」指定的位置才能自动创建和更新。如果你在项目中后期发现类别名总是别名,检查一下是不是多个项目共用了同一个predefined_classes.txt。
3.2 快捷键与批量操作:效率差距在这里拉开
观察一下标注效率高的人,你会发现他们的鼠标几乎只用来框目标,其余操作全在键盘上。labelImg默认快捷键里最核心的几个,我直接列成表:
| 快捷键 | 作用 | 使用场景 |
|---|---|---|
| W | 进入画框模式 | 每张图开始前按一次 |
| D / A | 下一张 / 上一张 | 批量切换图片 |
| Ctrl+S | 保存当前XML | 每张图画完按一次 |
| Ctrl+E | 编辑当前框类别名 | 框错类、想改类时 |
| Del | 删除当前选中框 | 框歪了、框多了 |
| Ctrl+Z | 撤销画框过程中的最后一步 | 画到一半手抖 |
| 空格 | 标记当前图为已标注 | 跳过某张图时做状态记录 |
| Ctrl+滚轮 | 缩放图片 | 小目标看不清时 |
这里有两个操作习惯建议。第一,保存要勤快,我的节奏是一张图画完立即Ctrl+S,再按D去下一张,养成肌肉记忆后不会漏存;第二,当一张图里目标很多时,每框完一个类别就顺手回车,不要让弹窗停留太久,弹窗挂着的时候快捷键是失灵的。
还有一个隐藏功能:菜单View里有「Auto Labeling」和「Single Class Mode」。Single Class Mode开启后,多个目标框都沿用当前类别名,不再弹输入框,适合一张图里同一个类别出现很多次的场景,能省掉一半时间。Auto Labeling适合配合辅助模型做预标注,属于进阶玩法,后面章节单独讲。
3.3 保存成什么:PascalVOC XML的结构与坐标含义
labelImg默认输出PascalVOC格式的XML文件,一张图对应一个同名XML。这个格式本身就是目标检测领域最通用的标注存储格式,所以它也是一张“图纸”,后续转YOLO、转COCO都要基于它。一个典型的XML长这样:
<annotation> <folder>images</folder> <filename>img_001.jpg</filename> <path>D:/project/images/img_001.jpg</path> <source><database>Unknown</database></source> <size> <width>1920</width> <height>1080</height> <depth>3</depth> </size> <object> <name>helmet</name> <bndbox> <xmin>100</xmin> <ymin>120</ymin> <xmax>400</xmax> <ymax>380</ymax> </bndbox> </object> </annotation>关键是size和object两段:size里的width、height、depth记录图片原始尺寸,object里的bndbox记录目标的左上角和右下角坐标。注意这个坐标是相对原始图片的像素坐标,不是归一化的。后续做模型训练时,不管是转YOLO还是转COCO,都要靠这两个信息做换算。
如果一张图里有多个目标,XML里就会有多个object块。还有一个容易被忽略的点:标注框超出图像边界时,labelImg不会自动修正,xmin可能大于width,这类脏数据进去训练模型会直接报错或导致loss异常,所以标注环节就尽量框在边界内。
4. labelImg避坑指南:闪退、乱码、漏标这些坑我都踩过
用labelImg的时间久了,踩过的坑能写一张纸。这一章挑我遇到最多、也最影响进度的五个问题,按「现象→原因→解决」的方式记录下来。很多问题在网上搜半天找不到答案,其实原因简单得让人无语。
4.1 画框到一半闪退,一整天白干
现象:图片加载正常,但只要鼠标开始拖框,或者拖到一半按了右键,程序直接消失,连报错弹窗都没有。Windows上重启后再打开,发现刚才没保存的标注全丢了。
原因:两个最普遍的源头。一是图片路径或文件名里有中文、特殊符号,Qt在保存XML或者绘制图像时触发编码异常,直接退出;二是单张图片分辨率过高,比如动辄6000x4000的航拍图,labelImg的绘制组件在拖框过程中内存暴涨,被系统杀掉了。
解决:图片统一改名成纯英文+数字,路径不要放中文目录;高分辨率图先缩放到2000像素边长以内再标注,标注完用原始分辨率训练时再按坐标比例换算回去;操作上要养成每张图Ctrl+S的习惯,这样就算闪退也只丢一张。
4.2 XML里中文类别名乱码
现象:类别名输入中文后,保存的XML文件用记事本打开是乱码,或者保存时直接报UnicodeEncodeError,用网上的XML转VOC脚本解析时直接报错。
原因:labelImg的master分支在Python 3下默认用UTF-8处理字符串,但如果用的是旧版源码、或者lxml版本太老,编码处理会退化。还有一个隐藏因素:Windows下输入法输入中文时,Qt的弹窗控件偶发把字符状态搞乱。
解决:最省心的是类别名直接用英文,比如helmet、person、defect,训练时再在脚本里做category_map映射成中文标签显示。如果一定要用中文,就把lxml升级到4.9.1以上,并且在labelImg.py开头强制加上# -- coding: utf-8 --声明,然后重新启动。
4.3 Qt资源编译报错,启动即崩溃
现象:从GitHub拉最新的labelImg代码,在Ubuntu上执行python3 labelImg.py,启动瞬间报AttributeError,日志末尾有libs.resources相关字样;或者界面按钮上全是空白的占位符,图标不显示。
原因:新版源码修改了resources.qrc里的资源引用,但仓库里没有附带编译好的resources.py。labelImg在导入时执行qInitResources(),找不到对应属性,整个模块初始化失败。
解决:按第二章的方法重新pyrcc5编译资源文件,编译完确认libs目录下生成了resources.py,再启动就正常了。MacOS用户注意,pyrcc5在Qt5的环境里一般自带,如果命令找不到,检查是否完整安装了PyQt5。
4.4 复杂背景下漏标小目标
现象:图片里目标小而密集,或背景纹理和目标颜色接近时,标注完总觉得框全了,跑出来的模型却漏检严重,复盘才发现大量小目标根本没框。
原因:这不是labelImg的故障,是人的视觉盲区。屏幕上目标小到一定程度,人眼扫过就是会漏。高强度连续标注时,注意力还会随疲劳下降。这个坑比软件bug更隐蔽,因为没人会怀疑自己漏标。
解决:我一般标完一轮后会强制休息几分钟再看第二遍,只看自己第一遍可能漏的区域;也可以让另一个人交叉复核同一批图。更工程化的做法是用一个小脚本,把每个类的实例数量统计出来,如果某些类数量异常少,就重点复查对应图片。脚本写法在第六章给出来。
4.5 删错框没有后悔药
现象:鼠标点选框后按Del,框直接消失,想撤销却怎么按Ctrl+Z都没反应,原来标好的目标只能重新画。
原因:labelImg的撤销机制只覆盖画框过程中的节点操作,对已经确认的框执行删除后,是不进撤销栈的。习惯性按Ctrl+Z是文本编辑器的肌肉记忆,在labelImg里并不总是有效。
解决:点选框之前先确认是不是当前选中了正确的框,框被选中时边框会明显高亮;删除前宁可先按Ctrl+E看一眼类别,再按Del。我自己的习惯是每隔五张图关闭并重新打开一下当前XML,确认最近的删除操作没有误伤到上一张的标注。
5. 把VOC格式转成YOLO格式:坐标换算与按类别拆分
labelImg的XML格式虽然通用,但当前主流检测模型YOLOv5、YOLOv8的标注输入是纯文本的txt格式,每行一条记录:类别编号、目标中心x、目标中心y、目标宽度w、目标高度h,全部相对图片尺寸归一化。所以拿到一批XML后,第一件事就是把它们转成YOLO标签。这一步用脚本批量做,比手工在labelImg里切格式靠谱得多。
5.1 两种坐标体系的换算关系
VOC的XML里存的是绝对坐标——目标左上角(xmin, ymin)和右下角(xmax, ymax)。YOLO格式需要的是相对坐标——目标中心点(x_center, y_center)和宽高(w, h),且取值范围在0到1之间。换算公式是:
x_center = (xmin + xmax) / 2 / image_width y_center = (ymin + ymax) / 2 / image_height w = (xmax - xmin) / image_width h = (ymax - ymin) / image_height
注意两个边界问题:一是XML里如果目标框越界,算出来的中心点可能落在图像外面或宽高超过1,这种值喂给模型会直接报错;二是类别名在YOLO里必须是整数编号,YOLO不认字符串类名,需要一个类别到编号的映射文件,这就是每个项目的data.yaml里classes列表的来源。
5.2 转换脚本:一张图生成一个txt
我每次拿到新项目的标注后,都会用一个固定脚本做转换。脚本遍历XML目录,逐个读坐标、做归一化、写同名txt文件。核心逻辑如下:
# voc_to_yolo.py # 用法: python voc_to_yolo.py <xml_dir> <out_dir> <class_file> import os import sys import glob import xml.etree.ElementTree as ET def convert_one(xml_path, out_dir, class_map): tree = ET.parse(xml_path) root = tree.getroot() img_w = int(root.find("size/width").text) img_h = int(root.find("size/height").text) txt_name = os.path.splitext(os.path.basename(xml_path))[0] + ".txt" lines = [] for obj in root.findall("object"): name = obj.find("name").text if name not in class_map: print(f"跳过未知类别: {name}") continue box = obj.find("bndbox") xmin = int(box.find("xmin").text) ymin = int(box.find("ymin").text) xmax = int(box.find("xmax").text) ymax = int(box.find("ymax").text) x_center = (xmin + xmax) / 2.0 / img_w y_center = (ymin + ymax) / 2.0 / img_h w = (xmax - xmin) / img_w h = (ymax - ymin) / img_h # 裁剪到[0,1]区间,防止越界 x_center = min(max(x_center, 0.0), 1.0) y_center = min(max(y_center, 0.0), 1.0) w = min(max(w, 0.0), 1.0) h = min(max(h, 0.0), 1.0) lines.append(f"{class_map[name]} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}") with open(os.path.join(out_dir, txt_name), "w") as f: f.write("\n".join(lines)) if __name__ == "__main__": xml_dir, out_dir, class_file = sys.argv[1], sys.argv[2], sys.argv[3] os.makedirs(out_dir, exist_ok=True) class_map = {} with open(class_file) as f: for idx, line in enumerate(f.read().strip().splitlines()): class_map[line] = idx for xml_path in glob.glob(os.path.join(xml_dir, "*.xml")): convert_one(xml_path, out_dir, class_map) print(f"done: {os.path.basename(xml_path)}")脚本里class_map的构建是关键:classes.txt里每行一个类名,行号就是YOLO里的类别编号。class_file这个参数不要传错,类和编号对应关系必须和训练配置里的data.yaml一致。坐标格式保留6位小数,过多的小数位不会提升训练精度,反而会让文件变大。f-string里的:06f是格式化控制,想改成4位精度也完全可以。
5.3 按类别统计与train/val划分的批处理
转完之后先别急着训练,跑一个统计脚本检查标签质量。这个脚本读所有txt,统计每个类别出现了多少次,以及哪些txt是空的——空txt代表这张图一个目标都没标注,如果它还在训练集里,模型会被喂一张没有学习目标的图,影响收敛。统计脚本很短:
# check_labels.py # 用法: python check_labels.py <label_dir> <class_file> import os import sys import glob from collections import Counter label_dir, class_file = sys.argv[1], sys.argv[2] classes = [line.strip() for line in open(class_file) if line.strip()] counter = Counter() empty_files = [] for txt in glob.glob(os.path.join(label_dir, "*.txt")): with open(txt) as f: lines = [line.strip() for line in f if line.strip()] if not lines: empty_files.append(os.path.basename(txt)) for line in lines: counter[classes[int(line.split()[0])]] += 1 print("类别统计:", dict(counter)) print("空标注文件数:", len(empty_files), empty_files[:10])如果某个类别的数量比预期少了一个量级,回去翻一下是不是漏标了;如果空文件里混进了原本有目标的图,检查一下XML转txt时是不是坐标读取出了问题。
train/val划分我习惯直接用命令行工具做,不需要写Python。图片和标签在同一个目录的情况下,按8:2随机切分:
ls images/*.jpg | shuf -n 100 > val_images.txt这里100是验证集大小,按你的总量调整。补集自动就是训练集。更稳妥的做法是用Python的random.sample,因为shuf在Windows Git Bash里不一定有。划分完要确认验证集里每个类别都有样本,避免出现验证集缺少某个类导致指标虚高或虚低。
6. 给标注结果做量化自检:一个脚本筛出漏标与错标
最后一章分享一个我一直在用的收尾技巧。标注和数据转换全部完成后,我不会急着训模型,而是先跑一遍自检脚本,把漏标和错标“打”出来。这个脚本的思路很简单:统计所有txt标签文件的行数分布,一组图片的目标数量如果明显少于同类其他图片,大概率是漏标了;同时检查类别名是否和class_file完全一致,防止手滑把helmet拼成helment。
脚本的核心逻辑是遍历标签目录,按文件名排序对比,输出目标数最少和最多的几张图,再按类别做一次柱状图式的数量统计。我用的版本长这样:
# quality_check.py # 用法: python quality_check.py <label_dir> <class_file> import os import sys from collections import Counter label_dir = sys.argv[1] class_file = sys.argv[2] classes = [line.strip() for line in open(class_file) if line.strip()] obj_count = {} invalid = [] for fname in os.listdir(label_dir): if not fname.endswith(".txt"): continue path = os.path.join(label_dir, fname) with open(path) as f: lines = [line.strip() for line in f if line.strip()] obj_count[fname] = len(lines) for line in lines: cls_id = line.split()[0] if int(cls_id) >= len(classes): invalid.append((fname, cls_id)) sorted_files = sorted(obj_count.items(), key=lambda x: x[1]) print("目标数最少的前10张:", sorted_files[:10]) print("出现未知类别编号的文件:", invalid)对查出来的可疑文件,回到labelImg里打开对应的图片和XML人工复核。这个动作看起来多了一步,实际上能省下后面排查mAP异常的大把时间。我现在每个项目标注结束后都会跑一遍,已经成了固定习惯。早期我跳过这一步直接训练,结果模型在小目标上惨不忍睹,最后回头查才明白是标注环节的锅——数据质量问题靠训练是救不回来的。希望帮到你,少走这一段弯路。
本文还有配套的精品资源,点击获取