2025-08-13 12:53:33 +08:00
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import glob
|
|
|
|
|
|
|
2025-08-14 18:24:45 +08:00
|
|
|
|
def labelme_to_yolo_keypoints_batch(json_dir, output_dir, target_box_label="J1", class_id=0, img_shape=None, keypoints_per_instance=4):
|
2025-08-13 12:53:33 +08:00
|
|
|
|
"""
|
2025-08-14 18:24:45 +08:00
|
|
|
|
批量转换 LabelMe JSON → YOLO Pose 格式 (.txt)
|
|
|
|
|
|
- 每 keypoints_per_instance 个关键点对应一个 target_box_label 实例
|
|
|
|
|
|
- 关键点必须与框一一对应
|
|
|
|
|
|
- 转换失败或数据不匹配时,删除 JSON 和对应图片
|
2025-08-13 12:53:33 +08:00
|
|
|
|
"""
|
|
|
|
|
|
if img_shape is None:
|
|
|
|
|
|
raise ValueError("必须提供 img_shape 参数,例如 (1440, 2506)")
|
|
|
|
|
|
|
|
|
|
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
|
|
json_files = glob.glob(os.path.join(json_dir, "*.json"))
|
|
|
|
|
|
json_files = [f for f in json_files if os.path.isfile(f) and not f.endswith("_mask.json")]
|
|
|
|
|
|
|
|
|
|
|
|
if not json_files:
|
|
|
|
|
|
print(f"❌ 在 {json_dir} 中未找到任何 JSON 文件")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
img_h, img_w = img_shape
|
|
|
|
|
|
converted_count = 0
|
2025-08-14 18:24:45 +08:00
|
|
|
|
deleted_count = 0
|
2025-08-13 12:53:33 +08:00
|
|
|
|
|
2025-08-14 18:24:45 +08:00
|
|
|
|
print(f"🔍 开始转换:目标框='{target_box_label}', 每实例 {keypoints_per_instance} 个关键点")
|
2025-08-13 12:53:33 +08:00
|
|
|
|
|
|
|
|
|
|
for json_file in json_files:
|
2025-08-14 18:24:45 +08:00
|
|
|
|
success = False
|
|
|
|
|
|
base_name = os.path.splitext(os.path.basename(json_file))[0]
|
|
|
|
|
|
output_path = os.path.join(output_dir, f"{base_name}.txt")
|
|
|
|
|
|
image_file_to_delete = None
|
|
|
|
|
|
|
2025-08-13 12:53:33 +08:00
|
|
|
|
try:
|
|
|
|
|
|
with open(json_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
|
data = json.load(f)
|
|
|
|
|
|
|
2025-08-14 18:24:45 +08:00
|
|
|
|
# 获取图片路径
|
|
|
|
|
|
image_path = data.get("imagePath")
|
|
|
|
|
|
if image_path:
|
|
|
|
|
|
image_file_to_delete = os.path.join(json_dir, os.path.basename(image_path))
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f"⚠️ {base_name}: JSON 中无 imagePath,无法定位图片")
|
|
|
|
|
|
image_file_to_delete = None
|
|
|
|
|
|
|
|
|
|
|
|
# 提取关键点
|
|
|
|
|
|
keypoint_labels = {str(i) for i in range(1, keypoints_per_instance + 1)} # 支持 1,2,3,4...
|
|
|
|
|
|
keypoints = []
|
|
|
|
|
|
for shape in data.get("shapes", []):
|
|
|
|
|
|
label = shape["label"]
|
|
|
|
|
|
if shape["shape_type"] == "point" and label in keypoint_labels:
|
|
|
|
|
|
x, y = shape["points"][0]
|
|
|
|
|
|
nx = x / img_w
|
|
|
|
|
|
ny = y / img_h
|
|
|
|
|
|
# 归一化并裁剪到 [0,1]
|
|
|
|
|
|
nx = max(0.0, min(1.0, nx))
|
|
|
|
|
|
ny = max(0.0, min(1.0, ny))
|
|
|
|
|
|
keypoints.append((label, nx, ny))
|
|
|
|
|
|
|
|
|
|
|
|
# 提取 J1 矩形框(每个框是一个实例)
|
|
|
|
|
|
j1_boxes = [
|
|
|
|
|
|
s for s in data.get("shapes", [])
|
|
|
|
|
|
if s["label"] == target_box_label and s["shape_type"] == "rectangle"
|
|
|
|
|
|
]
|
|
|
|
|
|
num_instances = len(j1_boxes)
|
|
|
|
|
|
total_keypoints = len(keypoints)
|
|
|
|
|
|
expected_total = num_instances * keypoints_per_instance
|
|
|
|
|
|
|
|
|
|
|
|
# 检查数量匹配
|
|
|
|
|
|
if total_keypoints != expected_total:
|
|
|
|
|
|
print(f"❌ {base_name}: 关键点数量不匹配!期望 {expected_total},实际 {total_keypoints}")
|
|
|
|
|
|
raise ValueError("关键点数量不匹配")
|
|
|
|
|
|
if num_instances == 0:
|
|
|
|
|
|
print(f"❌ {base_name}: 未找到任何 '{target_box_label}' 框")
|
|
|
|
|
|
raise ValueError("无目标框")
|
|
|
|
|
|
|
|
|
|
|
|
# 提取并归一化每个框的坐标
|
|
|
|
|
|
bboxes = []
|
|
|
|
|
|
for box in j1_boxes:
|
|
|
|
|
|
x1, y1 = box["points"][0]
|
|
|
|
|
|
x2, y2 = box["points"][1]
|
|
|
|
|
|
# 计算中心点和宽高
|
|
|
|
|
|
x_center = (x1 + x2) / 2 / img_w
|
|
|
|
|
|
y_center = (y1 + y2) / 2 / img_h
|
|
|
|
|
|
w = abs(x2 - x1) / img_w
|
|
|
|
|
|
h = abs(y2 - y1) / img_h
|
|
|
|
|
|
# 裁剪到 [0,1]
|
|
|
|
|
|
x_center = max(0.0, min(1.0, x_center))
|
|
|
|
|
|
y_center = max(0.0, min(1.0, y_center))
|
|
|
|
|
|
w = max(0.0, min(1.0, w))
|
|
|
|
|
|
h = max(0.0, min(1.0, h))
|
|
|
|
|
|
bboxes.append((x_center, y_center, w, h))
|
|
|
|
|
|
|
|
|
|
|
|
# 开始写入 YOLO 文件
|
|
|
|
|
|
with open(output_path, 'w', encoding='utf-8') as f_out:
|
|
|
|
|
|
for i in range(num_instances):
|
|
|
|
|
|
# 获取该实例对应的 4 个关键点
|
|
|
|
|
|
start_idx = i * keypoints_per_instance
|
|
|
|
|
|
end_idx = start_idx + keypoints_per_instance
|
|
|
|
|
|
group = keypoints[start_idx:end_idx]
|
|
|
|
|
|
|
|
|
|
|
|
if len(group) != keypoints_per_instance:
|
|
|
|
|
|
print(f"❌ {base_name}: 实例 {i+1} 缺少关键点")
|
|
|
|
|
|
raise ValueError("实例关键点不足")
|
|
|
|
|
|
|
|
|
|
|
|
# 按标签排序关键点 (1,2,3,4)
|
|
|
|
|
|
sorted_group = sorted(group, key=lambda x: x[0])
|
|
|
|
|
|
|
|
|
|
|
|
# 构造 YOLO 行:class + bbox + keypoints
|
|
|
|
|
|
yolo_line = [
|
|
|
|
|
|
str(class_id),
|
|
|
|
|
|
f"{bboxes[i][0]:.6f}", # x_center
|
|
|
|
|
|
f"{bboxes[i][1]:.6f}", # y_center
|
|
|
|
|
|
f"{bboxes[i][2]:.6f}", # w
|
|
|
|
|
|
f"{bboxes[i][3]:.6f}" # h
|
|
|
|
|
|
]
|
|
|
|
|
|
for _, kx, ky in sorted_group:
|
|
|
|
|
|
yolo_line.extend([f"{kx:.6f}", f"{ky:.6f}", "2"]) # v=2 表示可见
|
|
|
|
|
|
|
|
|
|
|
|
f_out.write(" ".join(yolo_line) + "\n")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"✅ 已转换: {os.path.basename(json_file)} -> {num_instances} 个实例")
|
|
|
|
|
|
success = True
|
|
|
|
|
|
converted_count += 1
|
2025-08-13 12:53:33 +08:00
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
2025-08-14 18:24:45 +08:00
|
|
|
|
print(f"❌ 转换失败 {base_name}: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
# 删除无效文件
|
|
|
|
|
|
try:
|
|
|
|
|
|
if os.path.exists(output_path):
|
|
|
|
|
|
os.remove(output_path)
|
|
|
|
|
|
if image_file_to_delete and os.path.exists(image_file_to_delete):
|
|
|
|
|
|
os.remove(image_file_to_delete)
|
|
|
|
|
|
print(f"🗑️ 已删除图片: {os.path.basename(image_file_to_delete)}")
|
|
|
|
|
|
if os.path.exists(json_file):
|
|
|
|
|
|
os.remove(json_file)
|
|
|
|
|
|
print(f"🗑️ 已删除 JSON: {os.path.basename(json_file)}")
|
|
|
|
|
|
deleted_count += 1
|
|
|
|
|
|
except Exception as del_e:
|
|
|
|
|
|
print(f"💥 删除文件时出错: {del_e}")
|
|
|
|
|
|
|
|
|
|
|
|
print("\n" + "="*60)
|
|
|
|
|
|
print(f"🎉 批量转换完成!")
|
|
|
|
|
|
print(f"✅ 成功保留: {converted_count} 个文件")
|
|
|
|
|
|
print(f"❌ 异常删除: {deleted_count} 个文件(JSON + 图片)")
|
2025-08-13 12:53:33 +08:00
|
|
|
|
print(f"📁 输出目录: {output_dir}")
|
2025-08-14 18:24:45 +08:00
|
|
|
|
print(f"📦 每实例关键点数: {keypoints_per_instance}")
|
|
|
|
|
|
print(f"🏷️ 目标框标签: {target_box_label}")
|
|
|
|
|
|
print("="*60)
|
2025-08-13 12:53:33 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ================== 用户配置区 ==================
|
2025-08-14 18:24:45 +08:00
|
|
|
|
JSON_DIR = "/media/hx/04e879fa-d697-4b02-ac7e-a4148876ebb0/dataset/3/folder_end"
|
|
|
|
|
|
OUTPUT_DIR = "/media/hx/04e879fa-d697-4b02-ac7e-a4148876ebb0/dataset/3/labels_keypoints"
|
|
|
|
|
|
TARGET_BOX_LABEL = "J1"
|
|
|
|
|
|
CLASS_ID = 0
|
|
|
|
|
|
IMG_SHAPE = (1440, 2506) # (height, width)
|
|
|
|
|
|
KEYPOINTS_PER_INSTANCE = 4
|
2025-08-13 12:53:33 +08:00
|
|
|
|
|
|
|
|
|
|
# ================== 执行转换 ==================
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
labelme_to_yolo_keypoints_batch(
|
|
|
|
|
|
json_dir=JSON_DIR,
|
|
|
|
|
|
output_dir=OUTPUT_DIR,
|
2025-08-14 18:24:45 +08:00
|
|
|
|
target_box_label=TARGET_BOX_LABEL,
|
2025-08-13 12:53:33 +08:00
|
|
|
|
class_id=CLASS_ID,
|
|
|
|
|
|
img_shape=IMG_SHAPE,
|
2025-08-14 18:24:45 +08:00
|
|
|
|
keypoints_per_instance=KEYPOINTS_PER_INSTANCE
|
2025-08-13 12:53:33 +08:00
|
|
|
|
)
|