简介:这是一份面向深度学习与计算机视觉初学者、数据标注实践者的 labelImg 图像标注工具资源包,基于 Python 与 Qt 构建,主要用于在图像上绘制边界框、标注多类别目标,并将结果保存为 PASCAL VOC 标准 XML,服务于目标检测等任务的数据准备。压缩包共 108 个文件,约 6.61MB,包含 21 个 py 源码、38 个 png 界面与示例图、15 个 pyc 编译文件、6 个 svg 图标、6 个 sh 脚本,以及 rst、md、txt 等说明文档,兼顾运行、界面资源与二次开发参考。已有 1145 人学习下载。资源保留完整工程结构与示例图片,支持跨平台使用、自定义类别、快捷键调整边界框,标注结果可对接 TensorFlow、PyTorch 等框架;其图形界面直观,支持 JPEG、PNG 等常见格式,并提供 W、A、S、D、Q 等快捷键辅助框选与微调,适合缺少编程背景的标注人员快速上手。读者可据此了解工具实现、快速搭建标注环境,并用于物体检测、医学影像分析等场景的数据预处理。
1. labelImg 装不上,多半是 Python 与 Qt 绑定的版本没用对
上周某实验室的 A 同学把 labelImg 装到第三遍才跑起来:第一次在 Python 3.11 环境下安装 PyQt5 时编译报错;第二次是因为源码目录里缺了libs/resources.py,导致启动直接报 ImportError;第三次pip install明明成功了,敲labelImg却提示命令不存在。labelImg 这个图像标注工具在深度学习数据准备环节出现频率极高,安装本身不难,难的是 Python 版本、PyQt5 绑定和启动入口这三件事同时匹配到正确组合。这篇记录按「选版本 → 装包 → 配参数 → 排错 → 验证」的顺序走一遍,主线环境是 Windows 11 + Python 3.9 虚拟环境,Linux 和 macOS 的差异单独标出来。新手照着敲能跑通,熟手可以直接跳到版本取舍和排查那两章。
2. 选版本与备环境:labelImg 1.8.6 该落在哪个 Python 上
labelImg 安装失败,多数情况不是 labelImg 自己的问题,而是它所依赖的 PyQt5 没装稳。这个工具从很早就开始流行,一路迭代到 1.8.x,代码里用到了不少 PyQt5 的早期 API,同时又依赖 lxml 解析 Pascal VOC 的 XML。所以动手之前的第一件事不是敲 pip,而是先把 Python、PyQt5、labelImg 三者的版本关系定下来。我一般会在装之前确认两件事:这台机器上有没有 Python 3.8 或 3.9 可用,以及全局环境里是不是已经有别的项目占了 PyQt5。这两条确认完,后面 90% 的意外都能提前避开。
2.1 Python 版本与 PyQt5 的对应关系
labelImg 1.8.6 的setup.py里写的是 Python 3.6+,但这只是最低门槛,不代表高版本就顺。真正的分水岭在 PyQt5 的预编译轮子(wheel)覆盖到哪个 Python 版本,以及pyrcc5这个资源编译工具还能不能正常工作。
| Python 版本 | PyQt5 安装表现 | 建议 |
|---|---|---|
| 3.6 / 3.7 | 轮子齐全,但已停止维护,lxml 新版可能装不上 | 不推荐,只在老机器上妥协 |
| 3.8 | 全部有预编译轮子,labelImg 1.8.6 实测最稳 | 首选 |
| 3.9 | 全部有预编译轮子,与 3.8 表现基本一致 | 首选 |
| 3.10 | PyQt5 5.15.9 可用,个别发行版要手动补依赖 | 可用,遇到问题先回退 3.9 |
| 3.11 / 3.12 | pip 解析 PyQt5 时容易拉到需要本地编译的源码包,Windows 上大概率卡住 | 别在这个版本上折腾 |
我一般固定用 Python 3.9 + PyQt5 5.15.9 这一组。PyQt5 不要装最新版,5.15.10 之后部分发行版把 Qt 的插件路径改了,labelImg 启动时会去platforms目录找qwindows.dll而找不到,报出来的错跟 Python 一点关系都没有,很容易被误导。锁 5.15.9 是最省事的选择。
注意:不要在一个已经装了 PyQt6 或 PySide6 的全局环境里装 labelImg。三者共用 Qt 的运行时目录,混装后
Qt platform plugin报错会间歇性出现,排查成本极高。
2.2 三种安装方式:pip 轮子、源码、免安装可执行文件
labelImg 的获取方式有三条路,选哪条取决于你的网络条件、是否需要改代码、以及目标机器能不能装 Python。
| 方式 | 适合场景 | 优点 | 代价 |
|---|---|---|---|
pip install labelImg | 本机开发、有 Python 环境 | 一条命令,自动带 PyQt5 和 lxml | 装的是发布版,改不了源码 |
| 源码编译 | 需要改界面、改保存逻辑、打补丁 | 可调试、可打包 | 要手动编译resources.qrc |
| 免安装可执行文件 | 标注外包、非开发同事的机器 | 双击即用,不碰 Python | 版本固定,出问题只能换包 |
标注团队里常见的做法是:算法同学用 pip 或源码装,标注同学统一发免安装包。这不是技术洁癖,而是标注同学的机器上往往还装着一堆办公软件,一旦让他们自己配 Python 环境变量,问题就会成倍增加。
2.3 虚拟环境与目录规划
不管走哪条路,都不要装进全局 site-packages。labelImg 对 PyQt5 的版本很敏感,一旦跟别的项目撞上,修起来比重装还慢。
# Windows(cmd 或 PowerShell 都适用) python -m venv .venv-labelimg .venv-labelimg\Scripts\activate python -V# Linux / macOS python3.9 -m venv .venv-labelimg source .venv-labelimg/bin/activate python -V第一段是 Windows 的激活脚本路径,Scripts目录下同时会生成labelImg.exe;第二段是类 Unix 系统,激活脚本在bin下。python -V这一步别省,很多人激活失败但没察觉,后面 pip 照样装到全局去了。如果习惯用 conda,conda create -n labelimg python=3.9效果一样,好处是 PyQt5 可以直接走 conda 的二进制渠道,省掉编译。
目录上我一般这么排:
| 路径 | 用途 |
|---|---|
D:\labelimg\.venv-labelimg | 虚拟环境,不提交、不迁移 |
D:\labelimg\src | 源码目录(走源码安装时) |
D:\dataset\images | 待标注图片 |
D:\dataset\annotations | 标注结果输出目录 |
D:\dataset\predefined_classes.txt | 预定义类别 |
虚拟环境和数据集目录分开放,是因为虚拟环境动辄几百兆,而数据集经常要拷来拷去。把两者放进同一个文件夹,同步盘一开就会把 site-packages 一起传上去,血泪经验。
3. 装 labelImg:pip 一条命令背后实际发生了什么
确认好 Python 版本和虚拟环境,接下来的安装反而快。但快不等于可以盲敲,知道 pip 那一条命令替你做了什么,出问题时才知道往哪看。
3.1 pip 安装与启动入口
# 先升级 pip,老版本 pip 在解析 PyQt5 依赖时容易拉到源码包 python -m pip install --upgrade pip # 锁定 labelImg 版本,避免团队里各人装到不同小版本 pip install labelImg==1.8.6 # 确认包装到了当前虚拟环境里 pip show labelImg这三步里,第二步锁版本是关键。不锁版本时,pip 会取最新发布版,不同人装出来的界面可能差一个按钮的位置,团队协作时沟通成本很高。第三步pip show会打出Location字段,那串路径如果指向全局 site-packages,说明虚拟环境根本没激活,回去重来。
装完启动有两个入口:
# 入口一:直接敲命令,前提是 Scripts / bin 目录在 PATH 里 labelImg # 入口二:入口不在 PATH 时用模块方式启动,最稳 python -m labelImgWindows 上pip install会在.venv-labelimg\Scripts\下生成labelImg.exe,虚拟环境激活后这个目录会被临时加进 PATH,所以能直接敲。退出虚拟环境后再敲就会提示「不是内部或外部命令」,这不是装坏了,是入口不在 PATH 里。python -m labelImg不依赖 PATH,任何时候都能用,我一般把它当作默认启动方式。
pip 装下来的 wheel 里已经包含了编译好的libs/resources.py,所以 pip 路线不需要碰pyrcc5。这一点很重要——后面源码路线报的错,pip 路线根本不会遇到。
3.2 源码安装:先补上 libs/resources.py 再谈启动
需要改界面、改导出逻辑或者打包分发时,走源码。源码包解压后进入目录,依次做四件事:装依赖、编译资源、确认入口、启动。
# 进入解压后的源码目录 cd labelImg-1.8.6 # 按 requirements 装依赖,PyQt5 单独锁版本 pip install pyqt5==5.15.9 lxml # 把 resources.qrc 编译成 Python 模块,这一步漏了必然 ImportError pyrcc5 -o libs/resources.py resources.qrc # 启动 python labelImg.pyresources.qrc是 Qt 的资源描述文件,里面登记了图标、样式表这些二进制资源。pyrcc5是 PyQt5 自带的资源编译器,作用是把.qrc翻译成一个可以直接import的resources.py。源码仓库里只放.qrc不放编译产物,所以任何一次全新拉取或解压源码,都必须重新编译一遍。
-o libs/resources.py里的-o是输出路径,必须落在libs/目录下,因为labelImg.py顶部写的是from libs.resources import *。有人习惯编译到当前目录,然后启动时报ModuleNotFoundError: No module named 'libs.resources',原因就在这里。libs目录下没有__init__.py也没关系,Python 3 的命名空间包机制能处理。
装完依赖后可以先跑一次自检,确认 Qt 能加载:
# 单独验证 PyQt5 是否可用,不涉及 labelImg 代码 python -c "from PyQt5.QtWidgets import QApplication; print('qt ok')"能打qt ok说明 PyQt5 这一层通了,后面再报错就只可能是 labelImg 自己的问题,排查范围一下缩小一半。
3.3 离线机器:把依赖打成轮子带过去
标注机器经常是内网,连不上任何包源。这时候在有网机器上把整串依赖下载成 wheel 文件,拷过去离线安装。
# 在有网的同架构机器上执行,--only-binary 保证只下预编译轮子 pip download labelImg==1.8.6 \ --python-version 3.9 \ --platform win_amd64 \ --only-binary=:all: \ -d ./wheels--python-version和--platform必须和目标机器完全一致,写错一个就会下到不适用的包,拷过去装的时候报is not a supported wheel on this platform。-d ./wheels是输出目录,跑完之后目录里应该有 labelImg、PyQt5、PyQt5-Qt5、PyQt5-sip、lxml 这几组 wheel。
离线机器上的安装:
# --no-index 禁止联网,--find-links 指向本地轮子目录 pip install --no-index --find-links=./wheels labelImg==1.8.6--no-index关掉包源索引,--find-links让 pip 只在本地目录里找。两个参数少一个,pip 都会尝试联网,内网环境下会卡到超时再回退,看着像卡死。这套流程我在几个隔离环境里跑过,比现场配源稳定得多。
4. 配标注环境:类别文件、保存格式与启动参数
工具装起来只是能开界面,真正决定标注产出的,是类别文件、保存格式和启动参数这三项配置。这三项配错,标注出来的数据在训练脚本里读不出来,返工成本比装环境高一个量级。
4.1 预定义类别:data/predefined_classes.txt 只负责少打字
labelImg 支持一个预定义类别列表,界面上双击创建标注框时,下拉框里直接给出这些类别,不用每次手打。文件位置分两种情况:pip 安装的在site-packages/labelImg/data/predefined_classes.txt,源码安装的在源码目录的data/predefined_classes.txt。
person vehicle helmet格式极简,一行一个类别,不能有逗号、不能有编号、不能有空行夹在中间。文件编码用 UTF-8,Windows 记事本另存时容易存成带 BOM 的 UTF-8,labelImg 读到第一行会带一个不可见字符,界面上表现为第一个类别名前面多一个方块,训练时这个类别永远匹配不上。改完文件必须重启 labelImg 才生效,因为它只在启动时读一次。
这里有个常见误解要说清:预定义类别文件只影响下拉框的候选项,不做任何校验。你完全可以在界面上手打一个不在列表里的类别名,labelImg 会照写不误。所以团队协作时,这份文件是约定,不是约束,最终还是要靠导出后的校验脚本兜底。
提示:把
predefined_classes.txt和数据集一起放进版本库,谁改了类别顺序都能追溯。类别顺序在 YOLO 格式里是硬约定,顺序一变,历史标注全部错位。
4.2 保存格式:Pascal VOC 与 YOLO 的切换时机
labelImg 支持 Pascal VOC 的 XML、YOLO 的 txt,以及 CreateML 的 json。格式不是全局设置,而是在保存的那一刻由界面上的格式开关决定。这意味着两件事:切换格式必须在开始标注之前,标到一半再切,前面那些图还是老格式;以及「更改保存目录」是独立的,改目录不会自动改格式。
YOLO 格式的一行长这样:
# 说明用:真实文件里不能带 # 开头的注释行 # 格式为 <class_id> <x_center> <y_center> <width> <height>,后四个值已归一化到 0~1 0 0.482353 0.561111 0.175294 0.244444 2 0.812500 0.336111 0.121569 0.188889class_id从 0 开始,对应classes.txt里的行序,第一行是 0,第二行是 1,以此类推。后四个值是相对整图宽高的比例,不是像素,所以换分辨率不会失效。这四个数由 labelImg 自动算,不用手填,但校验的时候要拿它们反算回像素,确认框没超出图边界。
YOLO 模式要求保存目录下存在一个classes.txt,labelImg 第一次在该目录保存时会尝试生成。如果目录是只读的,或者挂的是网络盘,生成会失败,表现是标注框能画但保存后 txt 里只有数字没有类别映射,后面训练时类别名全丢。我一般会在开始标注前手动放一个classes.txt进去,内容跟predefined_classes.txt完全一致,多一层保险。
| 格式 | 输出文件 | 适用训练框架 | 注意点 |
|---|---|---|---|
| Pascal VOC | 每图一个.xml | 多数检测框架的 VOC 数据集加载器 | 坐标是绝对像素,换分辨率要重算 |
| YOLO | 每图一个.txt+ 一个classes.txt | YOLO 系列 | 坐标归一化,类别 id 依赖行序 |
| CreateML | 单个.json | Apple 生态的训练工具 | 全部标注汇总在一个文件里,多人协作易冲突 |
4.3 启动参数与快捷键
启动时可以带参数,直接把图片目录和类别文件喂进去,省掉每次开界面再点目录。
# 第一个参数是图片目录,第二个是预定义类别文件 labelImg D:\dataset\images D:\dataset\predefined_classes.txt # 自动保存模式 + 关闭标签排序,批量标注时效率提升明显 labelImg D:\dataset\images D:\dataset\predefined_classes.txt --autosave --nosortlabels--autosave让每次切图时自动落盘,代价是没有「这张还没标完」的缓冲,画错一个框也会被立刻写进文件。--nosortlabels关掉标签列表的字母排序,改成按出现顺序排列,标注类别多的时候找起来更快。这两个参数在批量作业里我基本默认开着。
| 参数 | 作用 | 什么时候用 |
|---|---|---|
--autosave | 切图时自动保存 | 大批量、流程固定的标注 |
--nosortlabels | 标签列表不排序 | 类别超过 10 个 |
--labels | 从文件加载标签列表 | 类别集中管理 |
--keep-prev | 保留上一张图的标注框 | 视频抽帧、连续画面 |
--config | 指定配置文件路径 | 多套数据集切换 |
--keep-prev这个参数值得单独说一句。视频抽帧出来的连续图片,前后帧目标位置几乎一样,保留上一帧的框再微调,比每次从零画快好几倍。但它也是双刃剑,目标走出画面后如果忘了删,就会留下一堆错标,校验脚本一定要能查出框超出边界的记录。
常用快捷键记住六个就够日常用:
| 快捷键 | 动作 |
|---|---|
W | 创建矩形框 |
Ctrl + S | 保存当前标注 |
D | 下一张 |
A | 上一张 |
Ctrl + U | 加载目录下全部图片 |
Del | 删除选中的框 |
Ctrl + U和直接指定目录的区别在于,前者会把目录下所有图片一次性列进左侧文件列表,后者只加载当前这一张。批量标注一定要用Ctrl + U,否则每换一张都要重新点开文件对话框。
5. 常见问题排查:labelImg 起不来、存不下的五个现场
下面五条是我在实际环境里反复遇到的,按「现象 → 原因 → 解决」记,遇到同类问题可以直接对号。
5.1 启动报 No module named 'libs.resources'
现象:源码目录下执行python labelImg.py,立刻抛出ModuleNotFoundError: No module named 'libs.resources',界面根本不出现。
原因:resources.qrc没有编译,或者编译产物没落在libs/目录下。源码仓库不包含这个生成文件,每次全新拉取都要重新生成一次。
解决:在源码根目录执行pyrcc5 -o libs/resources.py resources.qrc,然后确认libs/resources.py确实存在且大小不为 0。如果提示pyrcc5: command not found,说明 PyQt5 没装进当前虚拟环境,先pip install pyqt5==5.15.9再重试。
5.2 Linux 上 Qt platform plugin "xcb" 加载失败
现象:命令行启动后报qt.qpa.plugin: Could not load the Qt platform plugin "xcb" in "" even though it was found,程序直接退出。
原因:精简版 Linux 发行版或容器镜像里缺 xcb 相关的系统库,PyQt5 的platforms/libqxcb.so找不到依赖。这跟 Python 包无关,是系统层的动态库缺失。
解决:先定位到底缺哪个库,再按名字补装。常见做法是装libxcb-xinerama0、libxcb-icccm4、libxcb-image0、libxcb-keysyms1、libxcb-render-util0这一组。实在定位不出来时,临时设export QT_DEBUG_PLUGINS=1再启动,日志会把缺失的.so全名打出来。
5.3 macOS 上双击图标闪退,命令行也没输出
现象:启动后窗口闪一下就消失,终端里没有任何堆栈。
原因:macOS 新版本对 Qt 的窗口管理接口做了调整,PyQt5 5.15.x 早期的某些构建与之不兼容,表现为进程直接被杀,连 Python 的异常处理都来不及触发。
解决:把 PyQt5 降到 5.14.0 试一次,pip install pyqt5==5.14.0;如果还不行,改用 conda 渠道安装 PyQt5,conda install pyqt=5,conda 的构建针对 macOS 做过适配。两条都不通时,直接用免安装可执行文件,别在源码层死磕。
5.4 图片列表是空的,或者点开显示一片白
现象:Ctrl + U选了目录,左侧文件列表里有文件名,但主编辑区一片空白,缩放也没反应。
原因:路径里带中文或空格,或者图片本身是 CMYK 色彩空间、16 位深这类非标准格式。labelImg 走 Qt 的图像解码路径,对非 RGB 8 位图的兼容性一般。
解决:先把图片目录改成纯英文路径试一次,这一步能排掉大部分情况。如果路径没问题,用脚本把图片统一转成 RGB 8 位再标,PIL三行就能批量处理。别指望在原图上硬标,标出来的坐标可能和训练时读到的尺寸对不上。
5.5 换机器后标注全乱,框还在但类别不对
现象:数据集从一台机器拷到另一台,打开同一批图,框的位置没错,但类别名全变了,或者干脆显示成数字。
原因:YOLO 格式只存类别 id,类别名靠classes.txt的行序还原。两台机器的classes.txt行序不一致,同一个 id 就指向了不同类别。这种错最阴,因为文件本身没坏,只有对回原图才能发现。
解决:把classes.txt和数据集绑定,一起进版本库、一起拷、一起校验。校验脚本里加一条,读classes.txt的行数和标注文件里出现的最大 id 做比对,id 越界直接报错。从那以后我给任何数据集加的第一道闸就是这个。
6. 装完怎么验证:拿十张图走一遍闭环
判断 labelImg 有没有真正装好,不看界面能不能弹出,而看标注产出能不能被下游读进去。我的做法是固定拿十张图跑一遍完整闭环,全过了才算这台机器可用,过不了就退回上一章排查。
十张图覆盖的典型场景是:五张正常单目标、两张多目标、一张小目标贴边、一张文件名带空格、一张分辨率特别大。这十张跑完,环境层面能踩的坑基本都暴露了。
具体步骤是:新建一个干净的输出目录,启动时用labelImg D:\dataset\images D:\dataset\predefined_classes.txt --autosave,逐张画框、切图,最后检查输出目录里的文件数量和命名是否与图片一一对应。数量对不上,多半是某张图换格式前就保存了,或者保存目录中途被改过。
然后拿脚本把产出反读一遍,这一步比肉眼检查可靠得多:
# verify_voc.py:反读 Pascal VOC 标注,确认坐标合法、类别非空 import glob import os import xml.etree.ElementTree as ET for xml_path in sorted(glob.glob("dataset/annotations/*.xml")): root = ET.parse(xml_path).getroot() size = root.find("size") w, h = int(size.find("width").text), int(size.find("height").text) objects = root.findall("object") assert objects, f"{xml_path} 没有任何标注对象" for obj in objects: name = obj.find("name").text assert name and name.strip(), f"{xml_path} 存在空类别名" box = obj.find("bndbox") x1, y1 = int(box.find("xmin").text), int(box.find("ymin").text) x2, y2 = int(box.find("xmax").text), int(box.find("ymax").text) # 坐标必须落在图内且左上小于右下,否则下游训练会直接崩 assert 0 <= x1 < x2 <= w and 0 <= y1 < y2 <= h, f"{xml_path} 坐标越界: {name}" print(os.path.basename(xml_path), "OK", f"{w}x{h}")ET.parse读 XML 树,size节点拿到原图尺寸,object节点逐个取类别名和四个坐标。三条断言分别卡三件事:有没有标注、类别名空不空、坐标是否越界。最后一行打印文件名和尺寸,滚一遍就知道哪张漏了。走 YOLO 格式时把这段换成读 txt、把归一化值乘回宽高再断言,逻辑一样。
| 检查项 | 通过标准 | 不通过时先看哪 |
|---|---|---|
| 输出文件数 | 等于图片数 | 保存目录、格式切换时机 |
| 类别名 | 全部在classes.txt里 | 预定义类别文件编码 |
| 坐标范围 | 在0~宽/高之间 | --keep-prev残留框 |
| 类别 id | 小于classes.txt行数 | 两台机器的行序是否一致 |
| 大图 | 尺寸与原图一致 | 缩放后是否误存 |
这套检查我一开始觉得麻烦,直到有一次用--keep-prev标了两千多张视频帧,训练时才发现有一批框整体偏了一个目标位置,只能全部返工。从那以后我每次装完 labelImg,都强制拿十张图走一遍「标注 → 反读 → 断言」的闭环,机器换了、环境重装了、版本升级了,一个都不省略。希望帮到你。
本文还有配套的精品资源,点击获取