Labelme标注的图片与标注框批量旋转

一个用于将Labelme标注后的图片以及标签同时旋转一定角度的工具。

默认逆时针旋转90度(通过更改 parser 参数来改变逆时针旋转的角度)。

注意:只要你输出路径和原文件路径不同,就不会破坏原有数据。如果你不放心,可以先备份,再使用。

两种使用方式(选一即可):

  1. 将 main() 函数中 parser 的所有 default 参数配置完成后,直接运行脚本文件。

  2. 在命令行调用,引号内需要你填入相应的参数:

    python 复制代码
    # 指定输入输出文件夹,逆时针旋转180度
    python rotate_labelme.py \
      --img_dir  "你的图片路径" \
      --json_dir "你的json文件路径" \
      --out_img_dir "输出图片路径" \
      --out_json_dir "输出json路径" \
      --angle 90

使用建议:

最好是旋转90度的整数倍,否则标签可能不会完全贴合。比如一个旋转45°的例子:

代码:

python 复制代码
import os
import sys
import json
import math
import argparse
from PIL import Image, ImageOps



def rotate_one(img_path, json_path, out_img_path, out_json_path, angle):
    """
    旋转图片 + labelme 的 polygon 标注点
    angle: 逆时针旋转角度(正数 = 逆时针)
    """
    # 1. 读取并修正 exif 方向
    img = Image.open(img_path)
    img = ImageOps.exif_transpose(img)
    orig_width, orig_height = img.size

    # 2. 旋转图像(带 expand,画布会自动扩大)
    rotated_img = img.rotate(
        angle,
        expand=True,
        resample=Image.Resampling.BICUBIC   
    )
    new_width, new_height = rotated_img.size

    # 3. 计算正确的旋转矩阵(图像坐标系:y向下)
    # 逆时针旋转 → 在 y 向下坐标系需要调整 sin 符号
    angle_rad = math.radians(angle)
    cos_a = math.cos(angle_rad)
    sin_a = math.sin(angle_rad)

    # 4. 读取 labelme json
    with open(json_path, "r", encoding="utf-8") as f:
        data = json.load(f)

    # 5. 变换每一个点的坐标
    for shape in data.get("shapes", []):
        new_points = []
        for x, y in shape["points"]:
            # -------------------------------
            #   步骤1:移到原图中心
            # -------------------------------
            dx = x - orig_width / 2.0
            dy = y - orig_height / 2.0

            # -------------------------------
            #   步骤2:图像坐标系下的逆时针旋转
            #   (注意 sin 前的符号与数学标准相反)
            # -------------------------------
            new_dx =  dx * cos_a + dy * sin_a
            new_dy = -dx * sin_a + dy * cos_a

            # -------------------------------
            #   步骤3:移到新画布中心
            # -------------------------------
            new_x = new_dx + new_width / 2.0
            new_y = new_dy + new_height / 2.0

            new_points.append([new_x, new_y])

        shape["points"] = new_points

    # 6. 更新 json 元信息
    data["imageWidth"] = new_width
    data["imageHeight"] = new_height
    data["imagePath"] = os.path.basename(out_img_path)
    if "imageData" in data:
        data["imageData"] = None   # 建议清空,避免 base64 过大

    # 7. 保存
    rotated_img.save(out_img_path, quality=95)  # jpg 可加 quality
    with open(out_json_path, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)


def batch_rotate(img_dir, json_dir, out_img_dir, out_json_dir, angle):
    os.makedirs(out_img_dir, exist_ok=True)
    os.makedirs(out_json_dir, exist_ok=True)

    # 先收集所有需要处理的文件(这样才能知道总数)
    tasks = []

    for name in os.listdir(img_dir):
        if not name.lower().endswith((".jpg", ".jpeg", ".png", ".bmp")):
            continue

        base = os.path.splitext(name)[0]
        img_path = os.path.join(img_dir, name)
        json_path = os.path.join(json_dir, base + ".json")

        if not os.path.isfile(json_path):
            print(f"[SKIP] 缺少 json: {name}")
            continue

        out_img = os.path.join(out_img_dir, name)
        out_json = os.path.join(out_json_dir, base + ".json")

        tasks.append((img_path, json_path, out_img, out_json, name))


    total = len(tasks)
    if total == 0:
        print("没有找到任何需要处理的文件")
        return

    print(f"总共找到 {total} 个文件需要处理\n")

    processed_count = 0
    error_count = 0
    error_list = []

    for i, (img_path, json_path, out_img, out_json, name) in enumerate( tasks, 1):
        try:
            processed_count += 1
            rotate_one(img_path, json_path, out_img, out_json, angle)
        except Exception as e:
            error_count += 1
            error_list.append(e)

        # 无论成功失败都更新进度条
        progress_bar(i, total, bar_length=40)


    print(f"处理完成:成功 {processed_count} 个,失败 {error_count} 个")


def progress_bar(current, total, bar_length=40, prefix="完成进度"):
    percent = current / total
    filled = int(bar_length * percent)
    bar = '█' * filled + '░' * (bar_length - filled)

    sys.stdout.write(f'\r{prefix}: |{bar}| {percent * 100:.1f}%  {current}/{total}')
    sys.stdout.flush()

def main():
    parser = argparse.ArgumentParser(description='旋转 labelme 标注图片与对应 json 文件')
    parser.add_argument("--img_dir",  default='./path_to_your_img', help="原始图片目录")
    parser.add_argument("--json_dir", default='./path_to_your_json', help="原始 labelme json 目录")
    parser.add_argument("--out_img_dir",  default='./path_to_output_img', help="输出旋转后图片目录")
    parser.add_argument("--out_json_dir",  default='./path_to_output_label', help="输出旋转后 json 目录")
    parser.add_argument("--angle",type=float, default=90, help="逆时针旋转角度(单位:度,可为任意浮点数)")

    args = parser.parse_args()

    batch_rotate(
        args.img_dir,
        args.json_dir,
        args.out_img_dir,
        args.out_json_dir,
        args.angle
    )


if __name__ == "__main__":
    main()
相关推荐
饭来_8 天前
解决在 Docker 中启动 labelme 的相关报错
docker·标注工具·labelme
封奚泽优2 个月前
使用Labelme进行图像标注
开发语言·python·labelme
小关会打代码3 个月前
数据集打标签labelme安装及使用教程
yolo·labelme
养个小橘猫1 年前
labelme标签批量转换数据集json_to_dataset
labelme
一颗小树x1 年前
YOLO格式转Labelme | 标签信息 | 辅助标注 | txt转json
labelme·yolo格式·标签信息·辅助标注·txt转json
Diros1g2 年前
老版本labelme如何不保存imagedata
labelme·标注·语义3分割
FriendshipT2 年前
Python将原始数据集和标注文件进行数据增强(随机仿射变换),并生成随机仿射变换的数据集和标注文件
开发语言·人工智能·python·目标检测·json·labelme
FriendshipT2 年前
Python将已标注的两张图片进行上下拼接并修改、合并其对应的Labelme标注文件
开发语言·人工智能·python·目标检测·json·labelme
大胡子大叔2 年前
深度学习标注工具(包括自动标注)总结——持续更新
自动标注·标注工具·labelme·视频标注·点云标注