labelme关键点标注转yolo txt

标签都是数字的 比如 1 2 3 4 5

点击查看代码
import os
import json
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
import math

# ==============================
# 工具:中文字体加载(防止中文乱码)
# 优先加载黑体,没有则加载Arial,都没有用默认字体
# ==============================
try:
    font = ImageFont.truetype("simhei.ttf", 40)
except:
    try:
        font = ImageFont.truetype("Arial.ttf", 20)
    except:
        font = ImageFont.load_default()

# ==============================
# 功能:判断一个点 (px,py) 是否在矩形框内
# 输入:点坐标 + 矩形对角坐标(已归一化或像素坐标都可以)
# ==============================
def point_in_rect(px, py, x1, y1, x2, y2):
    """判断点是否在矩形内(x1,y1,x2,y2 是规范化的 min/max)"""
    return x1 <= px <= x2 and y1 <= py <= y2

# ==============================
# 功能:计算两点之间欧氏距离
# 用于:当同一个关键点有多个标注时,选择离框中心最近的那个
# ==============================
def distance(p1, p2):
    return math.hypot(p1[0] - p2[0], p1[1] - p2[1])

# ==============================
# 核心函数:单个json文件转换 + 可视化
# 作用:把LabelMe标注的矩形+关键点 → YOLO格式关键点txt + 标注效果图
# ==============================
def convert_and_visualize(
    labelme_json_path,   # LabelMe导出的json文件路径
    image_path,          # 对应原图路径
    output_txt_path,     # 输出YOLO格式txt路径
    output_vis_path,     # 输出可视化图片路径
    img_width,           # 图像宽度(用于归一化)
    img_height           # 图像高度(用于归一化)
):
    # 1. 读取LabelMe的json标注文件
    with open(labelme_json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)

    # 2. 从json中分离:矩形框 + 关键点
    shapes = data.get("shapes", [])
    rectangles = [s for s in shapes if s["shape_type"] == "rectangle"]  # 所有矩形框
    points = [s for s in shapes if s["shape_type"] == "point"]          # 所有关键点

    # 3. 过滤有效关键点:label必须是数字 1~7(代表6个关键点编号)
    valid_points = []
    for p in points:
        label = p["label"]
        try:
            idx = int(label)
            if 1 <= idx <= 7:            # 只保留编号1-7的关键点
                x, y = p["points"][0]    # 取出点坐标
                valid_points.append((idx, x, y))
        except (ValueError, IndexError, TypeError):
            continue  # 忽略无效标签、格式错误的点

    # 4. 打开原图,准备绘制可视化结果
    img = Image.open(image_path).convert("RGB")
    draw = ImageDraw.Draw(img)
    yolo_lines = []                                  # 存储最终YOLO格式行
    colors = ["red", "blue", "green", "orange", "purple", "brown", "cyan", "magenta"]

    # 5. 遍历每一个标注的矩形(每个矩形对应一个目标)
    for i, rect in enumerate(rectangles):
        # 【可选】如果只想转换特定类别框,可在这里加过滤
        # if rect["label"] != "mangkong":
        #     continue

        # 6. 获取矩形框的两个对角点
        pts = rect["points"]
        if len(pts) != 2:
            print(f"⚠️ 跳过无效 rectangle(非两点): {labelme_json_path.name}")
            continue

        # 7. 统一矩形坐标:x1=最小x, x2=最大x,y同理
        x_coords = [p[0] for p in pts]
        y_coords = [p[1] for p in pts]
        x1, x2 = min(x_coords), max(x_coords)
        y1, y2 = min(y_coords), max(y_coords)
        rect_center = ((x1 + x2) / 2.0, (y1 + y2) / 2.0)  # 矩形中心点

        # 8. 把【落在当前矩形内的关键点】归为这个目标
        #    如果同一个编号出现多次 → 选离框中心最近的那个
        candidate_points = {}  # key:关键点编号  value:坐标(x,y)
        for idx, px, py in valid_points:
            if point_in_rect(px, py, x1, y1, x2, y2):
                if idx not in candidate_points:
                    candidate_points[idx] = (px, py)
                else:
                    # 已存在同编号点 → 保留距离框中心更近的
                    old_pt = candidate_points[idx]
                    if distance((px, py), rect_center) < distance(old_pt, rect_center):
                        candidate_points[idx] = (px, py)

        # 9. 构建YOLO关键点格式:固定6个关键点(编号2~7)
        #    格式:x y v,v=2可见,v=0不可见/无标注
        keypoints = []
        vis_list = []  # 用于可视化:保存有效点(编号,x,y)
        for k in range(2, 8):       # 关键点固定取 2,3,4,5,6,7
            if k in candidate_points:
                px, py = candidate_points[k]
                px_norm = px / img_width    # 归一化到 0~1
                py_norm = py / img_height
                keypoints.extend([f"{px_norm:.6f}", f"{py_norm:.6f}", "2"])
                vis_list.append((k, px, py))
            else:
                keypoints.extend(["0", "0", "0"])  # 无标注则填 0 0 0

        # 10. 计算YOLO标准框:中心x、中心y、宽、高(全部归一化)
        cx = (x1 + x2) / 2.0 / img_width
        cy = (y1 + y2) / 2.0 / img_height
        w = (x2 - x1) / img_width
        h = (y2 - y1) / img_height

        # 11. 拼接成一行YOLO格式:class_id cx cy w h kp1_x kp1_y v1 ... kp6 v6
        line = f"0 {cx:.6f} {cy:.6f} {w:.6f} {h:.6f} " + " ".join(keypoints)
        yolo_lines.append(line)

        # 12. 绘制可视化框 + 关键点 + 编号
        color = colors[i % len(colors)]
        draw.rectangle([x1, y1, x2, y2], outline=color, width=3)
        for idx, px, py in vis_list:
            r = 5
            draw.ellipse([px - r, py - r, px + r, py + r], fill=color, outline="white", width=1)
            draw.text((px + 6, py - 14), str(idx), fill=color, font=font)

    # 13. 保存YOLO标注txt
    with open(output_txt_path, 'w') as f:
        f.write("\n".join(yolo_lines))

    # 14. 保存可视化效果图
    img.save(output_vis_path)
    print(f"✅ 转换完成: {labelme_json_path.name}")

# ==============================
# 批量转换函数
# 遍历文件夹所有json → 自动匹配图片 → 批量生成txt + 可视化图
# ==============================
def batch_convert_with_vis(labelme_dir, images_dir, output_labels_dir, output_vis_dir):
    # 路径统一转为Path对象(方便跨平台)
    labelme_dir = Path(labelme_dir)
    images_dir = Path(images_dir)
    output_labels_dir = Path(output_labels_dir)
    output_vis_dir = Path(output_vis_dir)

    # 创建输出文件夹(不存在则创建)
    output_labels_dir.mkdir(parents=True, exist_ok=True)
    output_vis_dir.mkdir(parents=True, exist_ok=True)

    # 遍历所有 .json 文件
    for json_path in labelme_dir.glob("*.json"):
        # 自动匹配同名图片(支持多种格式)
        img_path = None
        for ext in [".jpg", ".jpeg", ".png", ".bmp", ".JPG", ".PNG"]:
            p = images_dir / (json_path.stem + ext)
            if p.exists():
                img_path = p
                break
        if not img_path:
            print(f"⚠️ 图像未找到: {json_path.stem}")
            continue

        # 获取图片宽高(用于归一化)
        try:
            with Image.open(img_path) as im:
                img_w, img_h = im.size
        except Exception as e:
            print(f"❌ 无法打开图像 {img_path}: {e}")
            continue

        # 输出文件路径
        txt_path = output_labels_dir / (json_path.stem + ".txt")
        vis_path = output_vis_dir / (json_path.stem + "_vis.jpg")

        # 调用核心转换函数
        convert_and_visualize(
            labelme_json_path=json_path,
            image_path=img_path,
            output_txt_path=txt_path,
            output_vis_path=vis_path,
            img_width=img_w,
            img_height=img_h
        )

# ==============================
# 主函数:程序入口
# 只需要在这里改路径即可!
# ==============================
def main():
    # 源文件夹(放json和原图的目录)
    src_dir = r"D:\pic\Guangzhoumeiwei\img_raw_3mangkong\img_raw_3mangkong"

    # 批量转换
    batch_convert_with_vis(
        labelme_dir = src_dir,            # json所在目录
        images_dir = src_dir,             # 图片所在目录
        output_labels_dir = src_dir + "_yolo",   # 输出YOLO txt
        output_vis_dir = src_dir + "_view"       # 输出可视化图
    )

if __name__ == "__main__":
    main()
posted @ 2026-03-25 16:37  阳光天气  阅读(6)  评论(0)    收藏  举报