简介:一份面向图像分割与标注初学者的Labelme安装与使用教程,目标是帮助读者快速掌握从环境搭建到实际标注的全流程。内容基于Anaconda发行版展开,涵盖下载安装的前置准备,并针对Python2与Python3两种版本分别给出虚拟环境创建、Labelme安装与激活的完整命令,同时补充Windows系统下打开cmd窗口的具体方法,降低新手配置门槛。资源共1个docx文档,压缩包整体仅14KB,体积小巧、便于下载后随时查阅或打印。目前已有15213人学习参考,适合计算机视觉、机器学习、数据科学及地理信息等需要完成图像标注任务的用户,也为需要开展目标检测、语义分割等项目的学习者提供清晰的起步指引。文档不仅列出安装步骤,还说明了启动Labelme后将JSON标注文件转换为可视化结果图片的实用操作,帮助读者完成标注数据输出,减少环境配置与工具使用中的常见弯路,让读者能够快速把标注结果应用到后续模型训练或数据分析中。
1. 为什么一个标注工具值得你花十分钟看完
你有没有遇到过这种场景:A同学花了两天标注完一千张图,打开训练脚本却提示"标签文件不存在",最后发现他标完的 JSON 全留在另一个目录里,训练脚本根本没读到。或者更常见的——标注到一半,多边形撤销了一次,整张图的标注全没了。这类问题在图像标注环节里反复出现,而大部分时候不是工具不行,是你还没把 labelme 的底层逻辑摸透。
labelme 是目前做实例分割和语义分割最常用的图像标注工具之一,它把每次标注的结果存成 JSON 文件,里面记录了多边形坐标、类别名、图像路径和图像尺寸。本文不讲虚的,直接按安装、界面操作、格式转换、常见坑、进阶技巧这条线走,保证你看完就能上手标图,而且标完的数据能直接喂给常见训练框架。
2. labelme 安装:conda、pip 和三种常踩的版本坑
2.1 为什么推荐用 conda 装而不是直接 pip
labelme 依赖 PyQt 和 Pillow,这两者在不同 Python 版本下的兼容性差异很大。直接pip install labelme往往能装成功,但启动时报错ModuleNotFoundError: No module named 'PyQt5'或者ImportError: cannot import name 'QtWebEngineWidgets',根源是系统 Python 环境太杂,PyQt 版本被其他包干扰。
所以我一般会先建一个干净的 conda 环境,再在环境里用 pip 安装。这样做的好处是:依赖隔离、卸载干净、换版本不污染主环境。
# 创建独立环境,指定 Python 版本,3.8 到 3.10 均可 conda create -n labelme-env python=3.9 -y # 激活环境 conda activate labelme-env # 安装 labelme 本体 pip install labelme这里的-n labelme-env是环境名,可以随便起;python=3.9指定解释器版本,实测 3.8~3.10 都能正常跑通,不建议用 3.7 以下,因为新版 PyQt 对旧解释器支持不好。pip install labelme会连带装好 PyQt5 和 Pillow,不需要手动补装。
装完验证一下:
# 查看安装位置,确认不是系统全局环境 which labelme # 启动图形界面 labelme如果执行labelme能弹出主窗口,说明安装完成。如果弹出报错,大概率是下面 2.2 里说的几种情况。
2.2 三个版本相关的坑
第一个坑是 conda 环境里 Python 版本太高,比如 3.11 或 3.12。PyQt5 的某些旧版本在这些解释器上无法直接安装,报错通常是Could not find a version that satisfies the requirement PyQt5。解决办法是把 Python 版本降到 3.9 再装,一步到位。
第二个坑是系统里同时存在多个 Python,导致labelme命令被指向一个旧环境。用which labelme看路径,如果输出在/usr/local/bin而不是 conda 环境目录,说明 PATH 顺序不对。执行conda activate labelme-env后检查echo $PATH,确保环境目录在前面。
第三个坑是 macOS 上装完双击启动没反应。这是因为 PyQt5 在 macOS 上和系统自带显示组件冲突,终端里运行会看到Abort trap: 6。常见做法是升级 PyQt5 到 5.15 以上:pip install PyQt5>=5.15,如果还不行,改用 Python 3.8 环境再装一遍。
2.3 离线环境怎么办
如果你的机器不能联网,需要提前在能联网的机器上把包下载好,再拷贝过去安装:
# 在有网的机器上执行 pip download labelme PyQt5 Pillow -d ./labelme_pkgs # 拷贝到离线机器后执行 pip install --no-index --find-links=./labelme_pkgs labelme参数说明:--no-index让 pip 不访问 PyPI;--find-links指向本地包目录。注意 PyQt5 的体积比较大,下载时务必带上它,否则离线安装会在最后一步报缺依赖。
3. 标注界面与操作流程:从打开图片到导出 JSON
3.1 界面布局和文件加载方式
打开 labelme 后,界面主要由菜单栏、画布区域、文件列表和标注区组成。第一次用的人最容易迷的地方是:到底怎么把一整批图片导进来,而不是一张一张点"打开"。
常见做法是先把要标注的图片统一放到一个文件夹里,然后在 labelme 里通过File > Open Dir选择这个文件夹。此时画布上方会出现一个图片滑动条,右下角有上一张/下一张按钮,标注完一张按D就能切到下一张。
注意区分Open File和Open Dir:前者只加载单张图,后者加载整个目录。用目录模式标注时,labelme 会自动把已标注的 JSON 文件和图片关联起来,重新打开目录时能继续标注,不用从头标。
3.2 多边形标注:画点、闭合、改点和取消
这是 labelme 的核心操作。左侧工具栏选中Create Polygons,然后在图上点击左键打点,打完全部顶点后点击右键(或按回车)闭合多边形,会弹出类别选择框,输入类别名并确认。
操作流程(Windows / Linux): 1. 点击左侧 Create Polygons 2. 左键逐个点击目标边缘打点 3. 打完后右键点击任意位置闭合 4. 在弹出的 Edit Label 里输入类别名 5. 按 D 保存当前 JSON 并切到下一张闭合后如果要修正,选中Edit Polygons工具,可以拖动已有顶点位置,也可以框选多个顶点一起移动。如果某个点打错了想撤销,按Ctrl+Z删除最近一个点。如果整个多边形都不想要,选中它按Delete删除。
这里有一个容易被忽略的操作:在闭合多边形之后、切到下一张之前,图片中央会出现一个小的编辑框,要在这个框里输入类别名并回车确认。如果这一步直接点空白处跳过,标注会丢失。
3.3 标注结果 JSON 的结构:你存下来的到底是什么
每标完一张图,labelme 会在图片同目录下生成同名 JSON 文件。打开一个最简单的 JSON,内容结构大致是:
{ "version": "5.2.0", "flags": {}, "shapes": [ { "label": "cat", "points": [[120, 50], [130, 80], [110, 90]], "group_id": null, "shape_type": "polygon", "flags": {} } ], "imagePath": "cat_001.jpg", "imageData": null, "imageHeight": 600, "imageWidth": 800 }字段说明:shapes是所有标注对象的列表,每个人标注对象包含label(类别名)、points(多边形顶点坐标,单位是像素)、shape_type(多边形、矩形、圆等,默认是 polygon);imagePath是它引用的原始图片名,注意是相对路径;imageData如果为 null,表示 JSON 没有内嵌图片数据,只保存了路径。
这个结构直接决定了后面转换脚本怎么写,也决定了训练框架怎么读。很多人训练时报"找不到标注",就是因为imagePath里写的是文件名,而实际图片在子目录里,路径拼接不上。
3.4 矩形、圆形和其他标注模式
labelme 不只支持多边形。新版左边工具栏里还有Create Rectangle和Create Circle。矩形适合目标本身接近矩形轮廓的场景(如车牌、路牌),圆适合标注细胞、按钮等圆形目标。
创建矩形时,按住左键拖动到目标对角即可,松开后同样会弹出类别输入框。圆形标注则是先点击圆心,再拖动到半径边缘后点击确认。不管用哪种形状,输出的 JSON 结构不变,只是shape_type字段不同。
从实际项目角度,建议尽量统一形状类型。混合使用多边形和矩形会极大增加后续数据清洗的成本,因为转换脚本通常只针对 polygon 写,矩形需要额外做一次顶点扩展。
4. 把 labelme 的 JSON 转成训练格式:YOLO 与 COCO 两种路径
4.1 为什么不能直接拿 JSON 丢给训练框架
labelme 的 JSON 是标注工具的存档格式,不是训练框架的标准输入。YOLO 系列要求每张图对应一个同名 TXT,里面每行是类别ID x_center y_center width height,坐标全部归一化到 0~1;Mask R-CNN 等框架通常用 COCO 格式的 JSON,里面是图片、标注、类别三类信息的字典。
如果你直接拿 labelme 的 JSON 去训练,框架会报看不懂,这不是训练代码的问题,是格式没对齐。常见做法是写一段转换脚本(或者用社区里的转换工具),把 JSON 转成目标格式。
4.2 转 YOLO 格式:转换脚本与坐标归一化
这里给一个自用的转换脚本,只处理多边形,跳过没有标注的图。把下面的脚本放在图片目录的上层,调整input_dir和output_dir即可:
import json import os def convert_labelme_to_yolo(json_dir, output_dir, class_list): # 建立类别名到 ID 的映射 class_map = {name: idx for idx, name in enumerate(class_list)} for json_file in os.listdir(json_dir): if not json_file.endswith(".json"): continue with open(os.path.join(json_dir, json_file), "r", encoding="utf-8") as f: data = json.load(f) # 读取图片尺寸,必须在 JSON 里存在 img_w = data["imageWidth"] img_h = data["imageHeight"] txt_name = os.path.splitext(json_file)[0] + ".txt" txt_path = os.path.join(output_dir, txt_name) lines = [] for shape in data["shapes"]: label = shape["label"] if label not in class_map: print(f"跳过 {json_file} 中的未知类别: {label}") continue points = shape["points"] # 计算多边形外接框 xs = [p[0] for p in points] ys = [p[1] for p in points] x_min, x_max = min(xs), max(xs) y_min, y_max = min(ys), max(ys) x_center = (x_min + x_max) / 2 / img_w y_center = (y_min + y_max) / 2 / img_h width = (x_max - x_min) / img_w height = (y_max - y_min) / img_h class_id = class_map[label] # YOLO 格式:类别ID 中心x 中心y 宽 高,全部归一化到 0~1 lines.append(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}") with open(txt_path, "w", encoding="utf-8") as f: f.write("\n".join(lines)) if __name__ == "__main__": convert_labelme_to_yolo( json_dir="./annotations", output_dir="./yolo_labels", class_list=["cat", "dog", "car"] )逻辑说明:脚本先读取 JSON 里的imageWidth和imageHeight,再遍历每个shape,取其多边形的所有顶点横纵坐标的最小值和最大值,组成外接框,再用框的中心点和宽高分别除以图片宽高得到归一化坐标。
参数说明:class_list的排列顺序决定了类别的 ID,顺序一旦改变,已经生成的 TXT 里的 ID 全部无效,需要重新生成。这是 YOLO 训练里最隐蔽的一个坑。
4.3 转 COCO 格式:从 JSON 到 annotations 字典
COCO 格式适合 Mask R-CNN、Detectron2 这类框架。核心是把所有图片的信息放到images列表,把标注信息放到annotations列表,类别信息放到categories列表:
import json def labelme_to_coco(image_dir, label_dirs, coco_output_path): coco_format = { "images": [], "annotations": [], "categories": [] } category_names = ["cat", "dog"] # 先注册类别 for idx, name in enumerate(category_names): coco_format["categories"].append({ "id": idx + 1, "name": name, "supercategory": "object" }) ann_id = 1 image_id = 1 for idx, json_file in enumerate(os.listdir(label_dirs)): with open(os.path.join(label_dirs, json_file), "r", encoding="utf-8") as f: data = json.load(f) # 注册图片信息 coco_format["images"].append({ "id": image_id, "file_name": os.path.join(image_dir, data["imagePath"]), "width": data["imageWidth"], "height": data["imageHeight"] }) # 注册每一个多边形的标注 for shape in data["shapes"]: points = shape["points"] # 展平为 x1, y1, x2, y2 ... 的格式 flat_points = [coord for p in points for coord in p] xs = [p[0] for p in points] ys = [p[1] for p in points] annotation = { "id": ann_id, "image_id": image_id, "category_id": category_names.index(shape["label"]) + 1, "segmentation": [flat_points], "bbox": [min(xs), min(ys), max(xs) - min(xs), max(ys) - min(ys)], "area": (max(xs) - min(xs)) * (max(ys) - min(ys)), "iscrowd": 0 } coco_format["annotations"].append(annotation) ann_id += 1 image_id += 1 with open(coco_output_path, "w", encoding="utf-8") as f: json.dump(coco_format, f, indent=2)逻辑说明:COCO 最关键的三个字段是segmentation(多边形坐标展平列表)、bbox(左上角坐标加宽高)、category_id。如果是矩形标注,segmentation只有四个点,没有实际分割轮廓,很多人转换后训练 Mask R-CNN 发现 mask 是方形的,就是这个原因。
参数说明:image_dir是训练代码读取图片时的基础路径,COCO 的file_name是相对这个路径的,一般写成原始imagePath即可,不要拼成绝对路径,否则数据集换机器就失效。
4.4 输出验证:转换完怎么看对没对
转换完成后的第一件事不是训练,而是随机抽几张图,把生成的 YOLO 标签叠加到图上可视化。常见做法是用 OpenCV 画框:
import cv2 img = cv2.imread("cat_001.jpg") # 读取对应的 YOLO txt,把归一化坐标乘回像素值 with open("cat_001.txt") as f: for line in f: cls_id, x_center, y_center, w, h = line.split() img_h, img_w = img.shape[:2] x1 = int(float(x_center) * img_w - float(w) * img_w / 2) y1 = int(float(y_center) * img_h - float(h) * img_h / 2) x2 = int(float(x_center) * img_w + float(w) * img_w / 2) y2 = int(float(y_center) * img_h + float(h) * img_h / 2) cv2.rectangle(img, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite("check.jpg", img)这段验证代码的作用是肉眼比对标注框和目标实际位置的偏差。如果框整体偏移,多半是归一化的时候除错了图片尺寸;如果框是斜的,说明转换脚本把多边形的点顺序弄乱了。
5. labelme 常见问题排查:五个翻车现场与修复办法
5.1 标注完重启软件,图片全不见了
现象:标注了一百多张,关掉 labelme 再打开,图片列表是空的。
原因:你之前用的是Open File逐张打开的图片,没有用Open Dir。labelme 的目录模式会在目录下维护一个图片列表,而单文件模式不会保存列表状态。
解决:以后统一用Open Dir打开图片所在文件夹。如果 JSON 文件已经生成但列表加载不出来,检查图片和 JSON 是否在同一目录,以及 JSON 里的imagePath是否和实际图片文件名一致。
5.2 右键闭合后多边形直接没了
现象:打完所有顶点,点击右键闭合,结果多边形在画布上消失了,或者弹出一个空白类别框但没法输入。
原因:常见有两种。一是点右键时鼠标悬停在画布边缘,触发了画布缩放而不是闭合;二是多边形闭合时顶点数少于 3 个,无法构成有效区域。
解决:画布边缘缩放时,等鼠标移动到画布中心再右键闭合。顶点少于 3 个时,先按Ctrl+Z删除最近的点,重新打够至少 3 个点再闭合。
5.3 类别名不小心打错了,几百个 JSON 都要改
现象:标注完检查时发现 "cat" 打成了 "cats",或者类别名混用了中英文,比如 "猫" 和 "cat" 同时存在。一个个点开改不现实。
解决:直接批量改 JSON 文件里的label字段。写一个简单脚本遍历目录下所有 JSON,把shapes里的label替换成标准名称:
import json import os def fix_label_in_json(json_dir, old_name, new_name): for json_file in os.listdir(json_dir): if not json_file.endswith(".json"): continue path = os.path.join(json_dir, json_file) with open(path, "r", encoding="utf-8") as f: data = json.load(f) for shape in data["shapes"]: if shape["label"] == old_name: shape["label"] = new_name with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False, indent=2) fix_label_in_json("./annotations", "cats", "cat")参数说明:old_name是错误的名字,new_name是统一后的正确名字。执行前建议先把json_dir指向的目录备份一份,因为批量替换脚本会直接覆盖原文件,没有后悔药。
5.4 图片太大,标注完保存 JSON 要等好几秒
现象:5000×3000 像素的大图,每保存一张要等 3~5 秒,整个标注效率被拖垮。
原因:labelme 的 JSON 里imageData字段默认保存了一张 base64 编码的图片缩略图,图片越大编码越慢,JSON 文件也越大。这个字段对后续训练通常没有用。
解决:保存前在File > Save Auto关闭自动保存,手动保存后如果 JSON 太大,可以用脚本把imageData置空。实测一个 2MB 的图片 JSON,清除 imageData 后体积能降到几十 KB。
5.5 标注时图片看起来模糊,细节看不清
现象:图片在 labelme 里显示模糊,放大后边界也看不清,很难标准。
原因:labelme 默认开启了图片缩放适配窗口,图片分辨率高于显示区域时会被压缩显示,这并不影响实际标注数据,但影响标注精度。
解决:用滚轮放大图片后再打点。放大的快捷键是滚轮向上,缩小是滚轮向下。注意放大后按住左键可以平移画布,但此时左键也是打点手势,容易误打点。我一般放大后先用Edit Polygons工具来拖动已有顶点,不用打点模式。
6. 标注前的数据体检:批量校验与类别统计分析
标注不是点完开始就算完,训练前至少要做三重校验。第一重是校验每个 JSON 是否有空标注(没有任何 shapes 的 JSON),第二重是校验imagePath指向的图片是否真的存在,第三重是统计各类别的样本数量,看类别是否严重不平衡。我习惯把这些逻辑合成一个脚本,每次换数据集都先跑一遍。
import json import os from collections import Counter def check_annotations(json_dir, image_root): total = 0 empty = 0 class_counter = Counter() missing_images = [] for json_file in os.listdir(json_dir): if not json_file.endswith(".json"): continue with open(os.path.join(json_dir, json_file), "r", encoding="utf-8") as f: data = json.load(f) total += 1 img_path = os.path.join(image_root, data["imagePath"]) if not os.path.exists(img_path): missing_images.append(data["imagePath"]) if len(data["shapes"]) == 0: empty += 1 continue for shape in data["shapes"]: class_counter[shape["label"]] += 1 print(f"JSON 总数: {total}") print(f"空标注 JSON 数: {empty}") print("类别分布:", dict(class_counter)) if missing_images: print("缺失图片示例:", missing_images[:5]) else: print("所有图片路径均有效") check_annotations("./annotations", "./images")这段脚本会把三类问题一次性报告出来:空标注文件数量、类别分布、缺失图片。空标注文件一定要处理,因为很多训练框架读到空列表会直接报错;类别分布如果某一类比另一类少一个数量级,建议在标注阶段就补齐,不要指望训练时做样本均衡权重。
我自己的习惯是每次标注完、转换格式前,必跑一遍这个校验脚本。这个习惯救过我一次:某次标注到一半图片被移动过目录,标签的 imagePath 全指向了不存在的路径,如果不是校验跑得早,后面转换出来的 YOLO 标签会全部作废。标注是基础工作,但基础工作里也全是细节。希望这些内容能帮到你,少走点弯路。
本文还有配套的精品资源,点击获取