☰
LabelImg可运行版完整排障指南:环境自检、YOLO标注与抽检校验
2026/10/21 4:42:28 网站建设 项目流程

简介:LabelImg 是图像标注领域广泛使用的开源工具,本压缩包提供一个可运行的版本,面向需要制作目标检测训练数据集的算法工程师、数据标注人员及深度学习初学者,支持 Windows、macOS 与 Linux 等常见平台,尤其适合遭遇新版兼容性问题或依赖固定版本环境的用户。该版本虽非最新,但核心功能稳定,足以满足日常标注需求。资源以 zip 格式打包,共 75 个文件,压缩后约 977KB,主要包含 Python 源码(12 个 py)、界面图标(png/svg)、预设类别与配置文件(txt/yml)、构建脚本(sh/makefile)及说明文档(md),整体结构精简,便于直接解压运行。libs 目录下的 canvas.py、labelDialog.py、pascal_voc_io.py 等核心文件完整保留,支持绘制矩形框与多边形、管理标签类别,并将标注结果保存为 PASCAL VOC XML 或 YOLO txt 格式;predefined_classes.txt 可自定义标签,README 与 demo 图片帮助快速上手标注流程。目前已有 4727 人学习下载,对想理解标注工具实现细节或快速搭建数据标注环节的读者来说,是一份可直接运行、易于修改扩展的实用资源。

1. 为LabelImg图片标注折腾环境返工两次之后,我转向了可运行版

上周帮A同学解决一个LabelImg图片标注闪退问题:同一份压缩包在他电脑上双击就退出,换到隔壁工位的机器却完全正常。翻出启动日志对比了一下,唯一区别是系统环境变量里PyQt5的指向不同。这个经历让我把注意力从“标注动作本身”转到了“启动方式”上。后面我再帮人搭标注环境,默认方案就是这种可运行版本:把Python解释器、PyQt5依赖和libs核心模块全打包在项目目录中,启动脚本自己定位环境,绕开系统里各种版本混装造成的玄学问题。这篇就把这份可运行版从目录结构、启动检查、标注操作、避坑排障到结果抽检完整拆一遍。新手可以照着步骤上手,老手可以重点看第四章和第五章的排障与校验脚本。

2. 解压后先别急:目录结构、启动脚本与依赖自检

拿到任何一个可运行版LabelImg,我建议先认结构再双击,能比直接点开少走很多弯路。原因是LabelImg本质是一组Python脚本,它对工作目录非常敏感:启动时如果当前目录不对,解释器找不到libs,窗口就会一闪而过或者报“No module named”。下面三个小节按“认清目录→正确启动→自检环境”推进,走完这些再开始标注。

2.1 快速识别压缩包里的六个关键角色

不同发布者打出来的包目录名会有差异,但角色是固定的。拿到包后先对照下面这张表逐项确认,能避免把源码版误当成可运行版。

路径/文件角色拿到后做什么
labelImg.py主程序入口后续所有启动操作都由它触发
libs/核心依赖包,包含画布、格式转换等模块不要改,也不要单独拷出去用
data/predefined_classes.txt预置类别清单按当前项目改成自己的类别
requirements.txt依赖清单只需要核对,可运行版一般不用手动安装
runtime/(或dep/、venv/)内置Python解释器与PyQt5启动脚本优先调用这里的解释器
start.bat 或 start.sh环境初始化 + 启动日常入口,优先用它,不要绕过

我的判断逻辑是:如果压缩包里同时出现了labelImg.py、libs/和runtime/这类自带的依赖目录,才叫真正的可运行版。如果只有labelImg.py和libs,那本质还是源码版,环境问题一个都躲不掉。另外,包内如果有多个不同的启动脚本,优先看带“start”字样的那个;它是打包者帮你固定环境变量和路径的地方。

2.2 启动顺序:先确定“谁去调用LabelImg”

问题往往不是“能不能跑”,而是“用哪个Python在跑”。Windows下常见的做法是双击start.bat,但它内部做了三件重要的事。如果发布包里没提供启动脚本,我一般会手动建一个,内容如下:

@echo off cd /d %~dp0 set PYTHONPATH=%~dp0;%PYTHONPATH% set QT_QPA_PLATFORM_PLUGIN_PATH=%~dp0\runtime\plugins\platforms "%~dp0runtime\python.exe" "%~dp0labelImg.py"

这段批处理的核心不在最后一行,而在前面三行。cd /d %~dp0把工作目录切换到批处理文件所在位置,防止从资源管理器双击时当前目录跑到了桌面或下载目录;set PYTHONPATH=%~dp0强制让Python把当前目录加入模块搜索路径,这样libs才能被import到;QT_QPA_PLATFORM_PLUGIN_PATH指向PyQt5的platforms插件目录,是Windows下解决“Failed to load platform plugin”的关键。如果拿到手的是源码版而不是可运行版,恰好又缺这几个文件,直接装一遍PyQt5再调环境变量反而没这么麻烦。

Linux环境下的思路一样,只是语法不同:

cd /home/tools/labelImg_run export PYTHONPATH=$PWD python3 labelImg.py
# 可运行版如果自带解释器,优先用它 /home/tools/labelImg_run/runtime/bin/python3 /home/tools/labelImg_run/labelImg.py

我在实践中遇到过一种很隐蔽的情况:明明是用start.bat启动的,程序仍然找不到PyQt5。最后排查发现发布包里的runtime目录是个空壳,发布者在打包时把大体积依赖排除了,只留了目录名。所以启动脚本不是万能的,还得靠一下面这一步做实际验证。

2.3 用自检脚本验证基础依赖是否真的就绪

启动后看不到窗口不一定是GUI问题,也可能Python解释器路径不对。与其盯着屏幕等报错,不如先把下面这个脚本放到LabelImg目录里跑一遍:

import os import sys import importlib.util required = ["PyQt5", "libs"] print("当前解释器:", sys.executable) print("工作目录:", os.getcwd()) missing = [] for mod in required: if importlib.util.find_spec(mod) is None: missing.append(mod) if missing: print("缺少模块:", ", ".join(missing)) print("请确认使用的 python.exe 来自可运行版自带的 runtime/ 目录") sys.exit(1) else: print("基础依赖正常,可以进入标注界面")

这段脚本的判定逻辑很直接:importlib.util.find_spec()在模块存在时返回一个spec对象,查不到才返回None。重点看脚本打印的“当前解释器”路径。如果你命令行里执行的是系统Python,报告却显示缺少PyQt5,那多半是当前环境变量里命中了另一个解释器。可运行版的正确做法是调用runtime目录下的python.exe,而不是系统里Anaconda或系统自带的Python。我把这一步叫“五分钟环境自检”,因为它是排障成本最低的入口,很多你以为的LabelImg故障,其实到这里就已经能定位了。

3. 标注出效率的三个关键设置:类别清单、快捷键和格式切换

很多刚接触LabelImg的人上来就按W画框,画到一半才发现标签列表还是默认的dog、person,保存格式也还在Pascal VOC上,最后导出的数据要花大量时间重导。下面三个小节把标注前的设置、画框节奏和底层坐标体系一次讲清楚。

3.1 predefined_classes.txt 的写法和顺序影响

打开data/predefined_classes.txt,这个文件是标注界面右侧类别的来源,也是YOLO格式下class_id的唯一定义顺序。它的规则是:一行一个类别,行号从0开始计数。比如第一行是person,那person在YOLO训练标签里就是0;第二行是car,car就是1。这个顺序一旦变了,所有已经标好的txt文件对应关系全部作废。

predefined_classes.txt 内容YOLO txt中输出的class_id
person 在第一行,car 在第二行person=0, car=1
car 被调到第一行,person在第二行car=0, person=1,历史标签全部错位

这个坑最隐蔽的地方在于:改动predefined_classes.txt之后,LabelImg界面看不出任何异常,保存txt也不会报警,直到模型训练出来mAP异常低你才会怀疑数据是不是早乱了。所以我每次开始新项目,第一个动作不是建文件夹,而是先把类别顺序在predefined_classes.txt里定死,定完就尽量不动。

关于类别命名,我的建议是坚持英文,并且别带空格。中文标签在Windows下容易因为编码问题乱码,空格会造成命令行工具解析时的边界问题。如果团队需要中文语义,我一般会在项目根目录另放一个class_mapping.json做中英文映射,predefined_classes.txt里只保存英文。

3.2 界面操作节奏:画框、翻页与自动保存

进入标注界面后,先用“文件”菜单把自动保存打开。自动保存模式下,每画完一个框并敲回车,标签会立即写入磁盘;这在程序崩溃时能救回大量工作。以下是日常最高频的快捷键:

快捷键作用
W在图片上创建一个新框
A / D切换到上一张 / 下一张图片
Ctrl+S保存当前标注
Ctrl+Shift+S另存为
Ctrl+E编辑当前选中框的标签
Delete删除当前选中框
Ctrl+C / Ctrl+V复制 / 粘贴当前标注框
Ctrl+滚轮缩放图片视图

操作节奏一般是:按W,左键拖出一个矩形,抬起鼠标后在弹窗里输入类别名回车,然后按D进入下一张。如果只想微调已有框,不要按W,直接点框边缘拖拽即可。要注意的是,如果你从别的标注工具转过来,会习惯先选中框再改标签;LabelImg里要先点中框,再按Ctrl+E,弹窗里的内容才是当前框的标签。快捷键记混了就会发生改了半天但txt没变的翻车。

3.3 两种保存格式底层的坐标差异

LabelImg工具栏上有一个格式下拉框,默认是PascalVOC。切换到YOLO后,保存的文件从.xml变成.txt,坐标体系完全不同。

Pascal VOC保存的XML里是像素坐标,例如xmin、ymin、xmax、ymax,表示框的左上角和右下角;YOLO保存的txt里是归一化坐标,格式为class_id x_center y_center width height,四个数值都除以了图片宽高,范围在0到1之间。

下面这个函数是两者转换最常用的逻辑:

def voc_to_yolo(size, box): # size: 图片宽高 (width, height) # box: VOC格式的 (xmin, ymin, xmax, ymax) dw = 1.0 / size[0] dh = 1.0 / size[1] cx = (box[0] + box[2]) / 2.0 cy = (box[1] + box[3]) / 2.0 w = box[2] - box[0] h = box[3] - box[1] return cx * dw, cy * dh, w * dw, h * dh

函数里dw和dh分别是宽和高的缩放比,把像素坐标除以图片尺寸,得到的就是归一化结果。实际调用的项目里,size必须和图片真实宽高一致;图片尺寸读错了,转换出来的YOLO标签全员漂移。

文件命名上也容易踩坑:VOC模式下保存为与图片同名的.xml,YOLO模式下保存为同名.txt。一个项目里最好只用一种格式,避免同一目录下xml和txt混在一起。我见过有人随手切换格式,最后训练脚本读到同名xml和txt内容对不上,去排查时已经晚了。

4. 避坑排查:5个真实卡点与排障记录

下面的五条排障记录,是我自己帮人处理LabelImg问题时遇到频率最高的,按“现象→原因→解决”整理。前三条集中在启动阶段,后两条集中在标注与导出阶段。

4.1 启动阶段的三个常见问题

踩坑1:双击start.bat后闪退,窗口一闪而过

  • 现象:屏幕上一道黑色命令行窗口瞬间消失,主程序没起来,也没有报错信息。
  • 原因:启动脚本没能切换到自身目录,或者依赖目录被移动了位置,Python解释器在此过程中异常退出,错误信息还没来得及显示。
  • 解决:不要直接双击,用cmd进入start.bat所在目录手动执行一次,让报错停留在命令行窗口里。然后确认路径中不要出现中文;把整个压缩包移动到纯英文路径下再启动。

踩坑2:报“Failed to load platform plugin windows”

  • 现象:报错信息中包含qt.qpa.plugin,提示找不到Windows平台插件,GUI起不来。
  • 原因:PyQt5运行时找不到plugins/platforms目录。常见于只拷贝了labelImg.py和libs,没把runtime目录一起带上。
  • 解决:在启动脚本里显式设置QT_QPA_PLATFORM_PLUGIN_PATH,指向runtime/plugins/platforms;设置完重启应用。若压缩包里根本没有plugins目录,说明它本身不是完整可运行版,光调环境变量救不回来。

踩坑3:报“No module named PyQt5”

  • 现象:Windows电脑里明明装过Python和PyQt5,但启动LabelImg仍然报缺少PyQt5。
  • 原因:当前命令行的解释器和那个安装了PyQt5的解释器不是同一个,最常见的是系统环境变量命中了不带PyQt的绿色版Python,或者Anaconda环境没有激活。
  • 解决:先运行第二章里check_env.py脚本,看sys.executable指向的是谁。如果指向runtime目录之外的解释器,就用start.bat强制调用内置Python;不要赌你手动设置的环境变量能生效。

4.2 标注与导出阶段的典型错误

踩坑4:画完框确认保存,切下一张再切回来,最后一个框丢了

  • 现象:自动保存已经打开,按D切到下一张,再按A回到上一张,发现最后一个标注框没有写盘。
  • 原因:自动保存并不等价于手动保存。某些版本的LabelImg在自动保存时会跳过尚未完全写入的画布状态,尤其在画框后立即切换图片的场景下,丢失概率会放大。
  • 解决:把“自动保存”当成辅助手段,别当成唯一保险。画完一个框或者一页图后,按Ctrl+S强制保存一次再切图。我自己的习惯是自动保存开着照用,同时每完成一张图做一次Ctrl+S,双保险。

踩坑5:YOLO txt里出现大于1或小于0的坐标

  • 现象:不放心打开txt查看,发现x_center或width出现0.98、1.05,甚至负数值。
  • 原因:标注框被拖到了图片边界外,LabelImg不会自动把框裁剪回图片范围内,归一化后的坐标因此溢出。
  • 解决:把越界坐标统一标记出来,回到对应图片重画或拖回边界。不要寄希望于训练代码自动忽略非法框,很多检测框架读到这种标签会直接报shape错误或产生负loss,处理成本远比重画一遍高。

这五条是频率最高的,但不是全部。启动问题大多集中在环境指向和依赖路径,标注问题集中在保存机制和坐标范围。想全部揪出来,最好的办法是再走一遍第五章的抽检脚本,让数据自己说话。

5. 抽检校验:在训练之前把错位标签逼出来

标注完成并不代表可以立即拿去训练。我的流程是脚本先扫描,把所有异常行找出来,再人工抽10%的图目检。下面这两个脚本基本固定放在项目tools目录里,每次交付前强制跑一遍。

5.1 扫描YOLO标签:对应关系、行数与坐标范围

第一个脚本检查三件事:图片和txt是否一一对应、txt每行是否为5个字段、坐标是否都落在0到1区间。

import os import sys def check_folder(images_dir, labels_dir, class_count=None): image_exts = {"png", "jpg", "jpeg", "bmp"} images = { f for f in os.listdir(images_dir) if f.rsplit(".", 1)[-1].lower() in image_exts } problems = [] # 检查每个 txt 是否有对应图片 for file in os.listdir(labels_dir): if not file.endswith(".txt"): continue stem = file[:-4] if not any( os.path.exists(os.path.join(images_dir, stem + "." + ext)) for ext in image_exts ): problems.append((file, "找不到对应图片")) # 检查每行字段数量、数值类型与坐标范围 for file in os.listdir(labels_dir): if not file.endswith(".txt"): continue path = os.path.join(labels_dir, file) with open(path, encoding="utf-8") as f: for i, line in enumerate(f, 1): parts = line.split() if len(parts) != 5: problems.append((file, f"第{i}行字段数应为5,实际为{len(parts)}")) continue try: cls, x, y, w, h = map(float, parts) except ValueError: problems.append((file, f"第{i}行不是合法浮点数")) continue if class_count is not None and not (0 <= cls < class_count): problems.append((file, f"第{i}行类别索引越界")) if not (0 <= x <= 1 and 0 <= y <= 1 and 0 <= w <= 1 and 0 <= h <= 1): problems.append((file, f"第{i}行坐标超出[0,1]")) # 检查每个图片是否有 txt for img in images: stem = img.rsplit(".", 1)[0] txt = os.path.join(labels_dir, stem + ".txt") if not os.path.exists(txt): problems.append((img, "缺少txt标注")) print(f"图片数 {len(images)},txt数 {len(os.listdir(labels_dir))}") for item in problems[:50]: print("异常:", item[0], "|", item[1]) print("异常总数:", len(problems)) return problems if __name__ == "__main__": check_folder(sys.argv[1], sys.argv[2], class_count=10)

脚本里map(float, parts)把五个字段一次性转成浮点数,任何一个字段非数字都会抛出ValueError,从而被捕获并记录。class_count=10是我示例用的类别总数,你换成自己项目实际类别数即可。坐标越界判断用连续条件写成0 <= x <= 1,简洁且直观,不必拆成多条if。

用法上我会先跑这一层,再针对“异常总数”判断是否有返工。异常数量为0只能说明格式可用,不代表框画得准,所以还需要预览脚本。

5.2 把YOLO框画回原图,用眼睛做最终判断

格式检查过了,下一步是把框画到原图上,肉眼确认位置是否贴合目标。下面的脚本读取YOLO txt并用PIL绘制矩形,输出到preview目录。

import os from PIL import Image, ImageDraw images_dir = "images" labels_dir = "labels" save_dir = "preview" os.makedirs(save_dir, exist_ok=True) colors = [(255, 0, 0), (0, 255, 0), (0, 128, 255), (255, 255, 0)] for name in os.listdir(labels_dir): if not name.endswith(".txt"): continue stem = name[:-4] img_path = os.path.join(images_dir, stem + ".jpg") if not os.path.exists(img_path): print("跳过缺失图片:", img_path) continue img = Image.open(img_path).convert("RGB") draw = ImageDraw.Draw(img) width, height = img.size with open(os.path.join(labels_dir, name), encoding="utf-8") as f: for line in f: cls, cx, cy, w, h = map(float, line.split()) # YOLO坐标是中心点加宽高,换算回左上右下像素坐标 left = (cx - w / 2) * width top = (cy - h / 2) * height right = (cx + w / 2) * width bottom = (cy + h / 2) * height color = colors[int(cls) % len(colors)] draw.rectangle([left, top, right, bottom], outline=color, width=2) draw.text((left, max(0, top - 18)), f"cls={int(cls)}", fill=color) out_path = os.path.join(save_dir, stem + ".png") img.save(out_path) print("预览图已输出到", save_dir)

参数说明都在换算那一行:YOLO存储的是中心点cx, cy和宽高w, h,且都归一化过,所以乘以图片实际宽度width和高度height,再加减宽高的一半,得到左上角和右下角的像素坐标。colors数组只有四组颜色,类别超过4个时用int(cls) % len(colors)循环取色,不会因颜色数量不够而报错。

预览脚本的适用场景是抽样目检。我会从200张里抽20张看一遍,重点看两类问题:一是框是否明显偏移目标,二是边界坐标是否出现半截框被截断的情况。如果看到大量框都整体偏右上或左下,优先怀疑的是图片读取顺序不是标签错误,因为YOLO坐标是相对当前图片的,不存在统一偏移,只有转换脚本用错图片尺寸才会整批漂移。

5.3 在什么阶段执行抽检

我会强制自己在三个时间点跑一遍上面两个脚本:标注全部完成时、格式转换后、训练数据划分之前。标注完成时跑,是为了拦截漏标和坐标越界;格式转换后跑,是为了防转换脚本把宽高顺序写反;划分前跑,是为了保证每个训练子集里都没有孤儿xml或txt。这些阶段之间隔得越久,返工成本越高。

6. 让标注结果直接对接训练配置:两个可落地习惯

6.1 用predefined_classes.txt当类别唯一真源

YOLO训练配置里通常有一个类别列表,很多人会手敲进配置文件,结果和predefined_classes.txt顺序不一致,训练出来的类别索引对不上。我的做法是从标注端直接生成训练端要用的classes.txt,保持两边永远同步。

classes = [] with open("data/predefined_classes.txt", encoding="utf-8") as f: for line in f: line = line.strip() if line: classes.append(line) with open("classes.txt", "w", encoding="utf-8") as f: f.write("\n".join(classes)) print(f"共 {len(classes)} 个类别: {classes}")

这个脚本几十秒就能跑完,关键在它把“标签顺序”这个最容易出错的手工步骤变成了自动化动作。新项目开工第一天,先改predefined_classes.txt,再跑这个脚本生成classes.txt,后续训练配置直接引用classes.txt,不再手工维护。

6.2 小目标过滤与数据划分一起做完

标注数据里难免会有一些小于5像素的误标框,这些框对检测模型的贡献接近于噪声。我在划分train/val时会顺手把小于阈值的框过滤掉,代码也不复杂。

import os import random from PIL import Image images_dir = "images" labels_dir = "labels" min_pixel = 10 train_ratio = 0.9 random.seed(2024) names = [f[:-4] for f in os.listdir(labels_dir) if f.endswith(".txt")] random.shuffle(names) split = int(len(names) * train_ratio) for part, group in [("train", names[:split]), ("val", names[split:])]: os.makedirs(part + "_labels", exist_ok=True) with open(part + ".txt", "w", encoding="utf-8") as list_f: for name in group: img = Image.open(f"{images_dir}/{name}.jpg") keep_lines = [] with open(f"{labels_dir}/{name}.txt", encoding="utf-8") as f: for line in f: cls, cx, cy, w, h = map(float, line.split()) if w * img.size[0] < min_pixel or h * img.size[1] < min_pixel: continue keep_lines.append(f"{int(cls)} {cx:.6f} {cy:.6f} {w:.6f} {h:.6f}") with open(f"{part}_labels/{name}.txt", "w", encoding="utf-8") as out: out.write("\n".join(keep_lines) + "\n") list_f.write(f"{images_dir}/{name}.jpg\n")

这里的逻辑是先按比例划分train和val,再在写每个txt时过滤小目标。阈值min_pixel=10是常用默认值,具体项目里可以根据目标大小调整;不需要过滤时把它调成0即可。划分结果同时生成了图片列表文件train.txt、val.txt,以及过滤后的标签目录,训练脚本直接指向这几个路径。

从那以后,我每次交付标注数据都强制自己走一遍完整链路:启动自检脚本确认环境,进入标注界面确认类别清单,交付前跑扫描脚本和预览脚本,最后用划分脚本输出数据集。这套流程本身不复杂,但每一步都能把上一步的隐患卡在产生实际损失之前。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询