Ubuntu22.04 安装和使用标注工具labelImg
文章目录
- 一、LabelImg 的安装及配置
- 1. 安装
- 2. 配置
- 二、使用
- 1. 基础操作介绍
- 2. 创建自定义标签
- 2.1 修改 predefined_classes.txt
- 2.2 直接软件界面新增
- 3. 图像标注
- 3.1 重命名排序
- 3.2 标注
- 3.2 voc2yolo 格式转换
LabelImg 是一个用Python编写,并使用Qt作为其图形界面的图形图像注释工具。
一、LabelImg 的安装及配置
1. 安装
Python版本不同,安装方式也不同
1.1 Python 2 + Qt4
sudo apt-get install pyqt4-dev-tools
sudo pip install lxml
git clone https://github.com/tzutalin/labelImg.git
cd labelImg
make all # 编译报错不用关注
python labelImg.py #打开 labelImg
python labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
1.2 Python 3 + Qt5
sudo apt-get install pyqt5-dev-tools
sudo pip3 install lxml
git clone https://github.com/tzutalin/labelImg.git
cd labelImg
make qt5py3
python3 labelImg.py #打开 labelImg
python3 labelImg.py [IMAGE_PATH] [PRE-DEFINED CLASS FILE]
2. 配置
上述是使用命令行启动,如需图形界面触发,可参考如下方法:
2.1 新建 run_labelImg.sh
#!/bin/bash# <labelImg 软件包路径>/labelImg.py
python3 /home/gene/Documents/software/labelImg/labelImg.py
2.2 新建 labelImg.desktop
内容如下:
[Desktop Entry]
Version=1.0
Name=labelImg
Comment=labelImg
Exec=/home/gene/Documents/software/labelImg/run_labelImg.sh
Icon=/home/gene/Documents/software/labelImg/logo.png
StartupNotify=true
Terminal=false
Type=Application
Categories=Applications;
Exec
为上一步骤的 run_labelImg.sh
Icon
为软件打开后的 logo,可以找中意的图片代替(建议与软件相关);
2.3 图形化启动
拷贝到应用管理器使得系统识别。
sudo cp labelImg.desktop /usr/share/applications/
执行完上述操作,即可通过图形界面启动,或者图中搜索框搜索启动。
二、使用
1. 基础操作介绍
打开labelImg软件后,首先可以进行界面配置,将 view -> Auto Save mode
和 view -> Display Labels
勾选,前者表示会自动保存(注意:最后一张需要手动保存), 后者表示标签可视化。
接着是界面左侧各操作按键的介绍:
- Open:打开单张图片路径;
- Open Dir :打开图片文件夹路径;
- Change Save Dir : 标签保存路径;
- Next Image: 下一张图片;
- Prev Image:上一张图片;
- Verify Image:可更改xml文件的内容;
- Save:保存;
- PascalVOC / Yolo / CreateML : 三种标签保存模式切换,推荐使用PascalVOC(方便可视化,yolo使用可以使用工具转换);
快捷键:
- w :创建矩形框;
- a :上一张图;
- d :下一张图;
- Ctrl+滚轮:放大与缩小;
2. 创建自定义标签
2.1 修改 predefined_classes.txt
一般来说 labelImg 会存在默认标签文件,保存在 labelImg/data/predefined_classes.txt
路径下,下面便是默认的标签:
dog
person
cat
tv
car
meatballs
marinara sauce
tomato soup
chicken noodle soup
french onion soup
chicken breast
ribs
pulled pork
hamburger
cavity
注意:每行一个标签,默认标签序号从上到下从0 开始。
现在修改文件内容,创建自己的预标签文件:
person
helmet
no_helmet
注意:修改 预标签文件后,记得重启 labelImg 软件才能识别。
2.2 直接软件界面新增
直接在软件里 创建矩形框,然后手动输入类别,如下图所示,添加新标签house
。
进行后续图片标注时可以看到,标签 house
依旧可以使用:
有了方便的方法二,为啥还要有方法一嘞?
个人理解原因是:方法二并没有修改predefined_classes.txt
文件,导致下次软件打开,方法二添加的预标签不存在,而标签需要顺序,方便后续管理。所以应该把常见的标签使用方法一进行永久保存,只有临时性的少数标签使用方法二标注。
3. 图像标注
3.1 重命名排序
首先,为方便管理需要保证数据集的文件名从1开始递增进行重命名,img_sort.sh 脚本 内容如下:
#!/bin/bash# 图片重命名排序脚本
# 用法: ./img_sort.sh /path/to/image/folder [padding_option] [start_number]
# padding_option: 0 - 使用动态前导零补齐 (默认)
# 1 - 不使用前导零补齐
# start_number: 起始编号 (默认为1)# 检查是否提供了文件夹路径参数
if [ $# -eq 0 ]; thenecho "错误: 请提供文件夹路径作为参数"echo "用法: $0 /path/to/image/folder [padding_option] [start_number]"echo "padding_option: 0 - 使用动态前导零补齐 (默认)"echo " 1 - 不使用前导零补齐"echo "start_number: 起始编号 (默认为1)"exit 1
fi# 获取输入的文件夹路径
INPUT_DIR="$1"# 获取填充选项 (默认为0 - 使用动态前导零补齐)
PADDING_OPTION=${2:-0}# 获取起始编号 (默认为1)
START_NUMBER=${3:-1}# 验证填充选项
if [ "$PADDING_OPTION" != "0" ] && [ "$PADDING_OPTION" != "1" ]; thenecho "错误: 填充选项必须是 0 或 1"echo "0 - 使用动态前导零补齐"echo "1 - 不使用前导零补齐"exit 1
fi# 验证起始编号是否为数字
if ! [[ "$START_NUMBER" =~ ^[0-9]+$ ]]; thenecho "错误: 起始编号必须是正整数"exit 1
fi# 检查文件夹是否存在
if [ ! -d "$INPUT_DIR" ]; thenecho "错误: 文件夹 '$INPUT_DIR' 不存在"exit 1
fi# 创建输出文件夹路径 (在原文件夹名后添加 _sort)
OUTPUT_DIR="${INPUT_DIR%/}_sort"# 如果输出文件夹已存在,询问用户是否覆盖
if [ -d "$OUTPUT_DIR" ]; thenecho "警告: 输出文件夹 '$OUTPUT_DIR' 已存在"read -p "是否覆盖? (y/N): " -n 1 -rechoif [[ ! $REPLY =~ ^[Yy]$ ]]; thenecho "操作已取消"exit 1fi# 删除现有输出文件夹rm -rf "$OUTPUT_DIR"
fi# 创建输出文件夹
mkdir -p "$OUTPUT_DIR"# 检查文件夹是否创建成功
if [ ! -d "$OUTPUT_DIR" ]; thenecho "错误: 无法创建输出文件夹 '$OUTPUT_DIR'"exit 1
fi# 支持的图片扩展名
IMAGE_EXTS=("jpg" "jpeg" "png" "gif" "bmp" "tiff" "webp")# 首先统计图片数量
# echo "正在统计图片数量..."
IMAGE_COUNT=0
while IFS= read -r -d '' FILE; doEXTENSION=$(echo "${FILE##*.}" | tr '[:upper:]' '[:lower:]')if [[ " ${IMAGE_EXTS[@]} " =~ " ${EXTENSION} " ]]; thenIMAGE_COUNT=$((IMAGE_COUNT + 1))fi
done < <(find "$INPUT_DIR" -maxdepth 1 -type f -print0 2>/dev/null)if [ $IMAGE_COUNT -eq 0 ]; thenecho "在指定文件夹中没有找到图片文件"exit 1
fiecho "找到 $IMAGE_COUNT 张图片"# 计算需要的位数(基于图片总数和起始编号,仅当使用前导零时)
if [ "$PADDING_OPTION" = "0" ]; then# 计算最大可能的数字(起始编号 + 图片数量 - 1)MAX_NUMBER=$((START_NUMBER + IMAGE_COUNT - 1))PADDING_LENGTH=${#MAX_NUMBER}echo "使用动态前导零补齐,位数: $PADDING_LENGTH (最大编号: $MAX_NUMBER)"
elseecho "不使用前导零补齐"
fi# 计数器从起始编号开始
COUNT=$START_NUMBER# 处理所有图片文件
echo "开始处理图片..."
PROCESSED_COUNT=0
while IFS= read -r -d '' FILE; doEXTENSION=$(echo "${FILE##*.}" | tr '[:upper:]' '[:lower:]')# 检查是否为图片文件if [[ " ${IMAGE_EXTS[@]} " =~ " ${EXTENSION} " ]]; then# 构建新文件名if [ "$PADDING_OPTION" = "0" ]; then# 使用动态前导零FORMATTED_NUMBER=$(printf "%0${PADDING_LENGTH}d" "$COUNT")NEW_FILENAME="${FORMATTED_NUMBER}.${EXTENSION}"else# 不使用前导零NEW_FILENAME="${COUNT}.${EXTENSION}"fiNEW_PATH="$OUTPUT_DIR/$NEW_FILENAME"# 复制文件到新位置cp "$FILE" "$NEW_PATH"echo "已复制: $(basename "$FILE") -> $NEW_FILENAME"# 增加计数器COUNT=$((COUNT + 1))PROCESSED_COUNT=$((PROCESSED_COUNT + 1))fi
done < <(find "$INPUT_DIR" -maxdepth 1 -type f -print0 2>/dev/null | sort -z)echo "处理完成!"
echo "所有图片已重命名并保存到: $OUTPUT_DIR"
echo "共处理了 $PROCESSED_COUNT 张图片,从 $START_NUMBER 开始编号"
img_sort.sh 脚本 使用命令
# 默认 重命名用0补齐
./img_sort.sh ./helmet_test/image/# 显示 重命名用0补齐
./img_sort.sh ./helmet_test/image/ 0# 显示 重命名不用0补齐
./img_sort.sh ./helmet_test/image/ 1# 显示 重命名用0补齐,且从数字29开始递增命名
./img_sort.sh ./helmet_test/image/ 0 29
3.2 标注
标注过程如下:
① 建议在 数据集同目录创建 annotations
文件夹,用于保存标签;
② 打开 labelImg软件,配置如下:
a) 添加数据集文件夹目录;b) 添加标签保存目录; c) 设置标签格式为 PascalVOC 模式。
③ 标注图像,标注1张,自动保存标签到 annotations
文件夹,如下所示:
内容格式如下:
<annotation><folder>image_sort</folder><filename>01.jpeg</filename><path>/home/gene/project/helmet_test/image_sort/01.jpeg</path><source><database>Unknown</database></source><size><width>265</width><height>180</height><depth>3</depth></size><segmented>0</segmented><object><name>person</name><pose>Unspecified</pose><truncated>1</truncated><difficult>0</difficult><bndbox><xmin>79</xmin><ymin>1</ymin><xmax>206</xmax><ymax>179</ymax></bndbox></object>
</annotation>
3.2 voc2yolo 格式转换
若需将 PascalVOC
格式转成 yolo
格式,则新建 voc2yolo.py 文件,执行如下指令进行转换:
# 单参数: 待转换voc标签文件夹路径
# 默认类别:参考 DEFAULT_CLASSES = ['Ball', 'Post', 'L', 'T', 'X']
python3 voc2yolo.py ./Labels# 双参数: 待转换voc标签文件夹路径 标签路径
python3 voc2yolo.py ./Labels /home/gene/Documents/software/labelImg/data/predefined_classes.txt
voc2yolo.py 文件内容如下:
#!/usr/bin/env python3
"""
Pascal VOC 转 YOLO 格式转换脚本
用法: python3 voc2yolo.py [输入文件夹] [类别文件(可选)] [输出文件夹(可选)]
示例: python3 voc2yolo.py ./Labels # 使用默认类别python3 voc2yolo.py ./Labels /path/to/predefined_classes.txt # 使用指定类别文件
"""import os
import sys
import xml.etree.ElementTree as ET
from pathlib import Path# 默认类别列表
DEFAULT_CLASSES = ['Ball', 'Post', 'L', 'T', 'X']def parse_args():"""解析命令行参数"""if len(sys.argv) < 2:print("错误: 参数不足")print("用法: python3 voc2yolo.py [输入文件夹] [类别文件(可选)] [输出文件夹(可选)]")print("示例: python3 voc2yolo.py ./Labels # 使用默认类别")print("示例: python3 voc2yolo.py ./Labels /path/to/predefined_classes.txt")sys.exit(1)input_dir = sys.argv[1]# 处理可选参数class_file = Noneoutput_dir = os.path.join(os.path.dirname(input_dir), "Labels_yolo")if len(sys.argv) > 2:# 检查第二个参数是类别文件还是输出目录if os.path.isfile(sys.argv[2]):class_file = sys.argv[2]if len(sys.argv) > 3:output_dir = sys.argv[3]else:# 第二个参数可能是输出目录output_dir = sys.argv[2]return input_dir, class_file, output_dirdef load_classes(class_file):"""从文件加载类别列表,如果没有提供文件则使用默认类别"""if class_file is None:print("使用默认类别:", DEFAULT_CLASSES)return DEFAULT_CLASSESclasses = []try:with open(class_file, 'r') as f:for line in f:class_name = line.strip()if class_name: # 跳过空行classes.append(class_name)except FileNotFoundError:print(f"警告: 类别文件 '{class_file}' 不存在,使用默认类别")return DEFAULT_CLASSESexcept Exception as e:print(f"读取类别文件时出错: {e},使用默认类别")return DEFAULT_CLASSESif not classes:print("警告: 类别文件为空,使用默认类别")return DEFAULT_CLASSESprint(f"从文件加载了 {len(classes)} 个类别: {', '.join(classes)}")return classesdef convert_voc_to_yolo(xml_file, classes, output_dir):"""转换单个VOC XML文件到YOLO格式"""try:tree = ET.parse(xml_file)root = tree.getroot()except Exception as e:print(f"解析XML文件 {xml_file} 时出错: {e}")return# 获取图像尺寸size = root.find('size')if size is None:print(f"警告: {xml_file} 中没有找到尺寸信息,跳过此文件")returnwidth = int(size.find('width').text)height = int(size.find('height').text)if width <= 0 or height <= 0:print(f"警告: {xml_file} 中的图像尺寸无效,跳过此文件")return# 准备输出文件xml_name = Path(xml_file).stemoutput_file = os.path.join(output_dir, f"{xml_name}.txt")yolo_annotations = []# 处理每个对象for obj in root.findall('object'):# 获取类别class_name = obj.find('name').textif class_name not in classes:print(f"警告: 在文件 {xml_file} 中发现未定义类别 '{class_name}',跳过此对象")continueclass_id = classes.index(class_name)# 获取边界框bbox = obj.find('bndbox')if bbox is None:print(f"警告: 在文件 {xml_file} 中发现没有边界框的对象,跳过")continuexmin = float(bbox.find('xmin').text)ymin = float(bbox.find('ymin').text)xmax = float(bbox.find('xmax').text)ymax = float(bbox.find('ymax').text)# 转换为YOLO格式 (中心点x, 中心点y, 宽度, 高度)x_center = (xmin + xmax) / 2.0 / widthy_center = (ymin + ymax) / 2.0 / heightw = (xmax - xmin) / widthh = (ymax - ymin) / height# 确保值在0-1范围内x_center = max(0, min(1, x_center))y_center = max(0, min(1, y_center))w = max(0, min(1, w))h = max(0, min(1, h))yolo_annotations.append(f"{class_id} {x_center:.6f} {y_center:.6f} {w:.6f} {h:.6f}")# 写入输出文件try:with open(output_file, 'w') as f:for annotation in yolo_annotations:f.write(annotation + '\n')except Exception as e:print(f"写入文件 {output_file} 时出错: {e}")def main():"""主函数"""input_dir, class_file, output_dir = parse_args()# 检查输入目录if not os.path.isdir(input_dir):print(f"错误: 输入目录 '{input_dir}' 不存在")sys.exit(1)# 加载类别classes = load_classes(class_file)# 创建输出目录os.makedirs(output_dir, exist_ok=True)print(f"输出目录: {output_dir}")# 处理所有XML文件xml_files = [f for f in os.listdir(input_dir) if f.endswith('.xml')]print(f"找到 {len(xml_files)} 个XML文件")for i, xml_file in enumerate(xml_files):xml_path = os.path.join(input_dir, xml_file)print(f"处理文件 {i+1}/{len(xml_files)}: {xml_file}")convert_voc_to_yolo(xml_path, classes, output_dir)print("转换完成!")if __name__ == "__main__":main()
待续 。。。