|
@@ -0,0 +1,505 @@
|
|
|
|
|
+#!/usr/bin/env python3
|
|
|
|
|
+# -*- coding: utf-8 -*-
|
|
|
|
|
+"""
|
|
|
|
|
+JSON标签到YOLO格式转换脚本
|
|
|
|
|
+支持多种常见的JSON标注格式转换为YOLO格式
|
|
|
|
|
+
|
|
|
|
|
+功能特性:
|
|
|
|
|
+- 支持LabelMe、COCO、YOLO等多种JSON格式
|
|
|
|
|
+- 矩形标注转换为YOLO边界框格式 (class_id x_center y_center width height)
|
|
|
|
|
+- 多边形标注保留所有点位信息 (class_id x1 y1 x2 y2 ... xn yn)
|
|
|
|
|
+- 自动归一化坐标到[0,1]范围
|
|
|
|
|
+- 支持自定义类别映射文件
|
|
|
|
|
+"""
|
|
|
|
|
+
|
|
|
|
|
+import os
|
|
|
|
|
+import json
|
|
|
|
|
+import glob
|
|
|
|
|
+from pathlib import Path
|
|
|
|
|
+import argparse
|
|
|
|
|
+
|
|
|
|
|
+def convert_bbox_to_yolo(bbox, img_width, img_height, format_type="xywh"):
|
|
|
|
|
+ """
|
|
|
|
|
+ 将边界框坐标转换为YOLO格式(归一化的中心点坐标和宽高)
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ bbox: 边界框坐标
|
|
|
|
|
+ img_width: 图片宽度
|
|
|
|
|
+ img_height: 图片高度
|
|
|
|
|
+ format_type: 输入格式类型 ("xywh", "xyxy", "coco")
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ tuple: (center_x, center_y, width, height) 归一化坐标
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ if format_type == "xyxy":
|
|
|
|
|
+ # 格式: [x_min, y_min, x_max, y_max]
|
|
|
|
|
+ x_min, y_min, x_max, y_max = bbox
|
|
|
|
|
+ width = x_max - x_min
|
|
|
|
|
+ height = y_max - y_min
|
|
|
|
|
+ center_x = x_min + width / 2
|
|
|
|
|
+ center_y = y_min + height / 2
|
|
|
|
|
+
|
|
|
|
|
+ elif format_type == "xywh":
|
|
|
|
|
+ # 格式: [x, y, width, height] (左上角坐标)
|
|
|
|
|
+ x, y, width, height = bbox
|
|
|
|
|
+ center_x = x + width / 2
|
|
|
|
|
+ center_y = y + height / 2
|
|
|
|
|
+
|
|
|
|
|
+ elif format_type == "coco":
|
|
|
|
|
+ # COCO格式: [x, y, width, height] (左上角坐标)
|
|
|
|
|
+ x, y, width, height = bbox
|
|
|
|
|
+ center_x = x + width / 2
|
|
|
|
|
+ center_y = y + height / 2
|
|
|
|
|
+
|
|
|
|
|
+ else:
|
|
|
|
|
+ raise ValueError(f"不支持的格式类型: {format_type}")
|
|
|
|
|
+
|
|
|
|
|
+ # 归一化坐标
|
|
|
|
|
+ center_x_norm = center_x / img_width
|
|
|
|
|
+ center_y_norm = center_y / img_height
|
|
|
|
|
+ width_norm = width / img_width
|
|
|
|
|
+ height_norm = height / img_height
|
|
|
|
|
+
|
|
|
|
|
+ return center_x_norm, center_y_norm, width_norm, height_norm
|
|
|
|
|
+
|
|
|
|
|
+def convert_polygon_to_yolo(points, img_width, img_height):
|
|
|
|
|
+ """
|
|
|
|
|
+ 将多边形点位转换为YOLO格式(归一化坐标)
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ points: 多边形点位列表 [[x1, y1], [x2, y2], ...]
|
|
|
|
|
+ img_width: 图片宽度
|
|
|
|
|
+ img_height: 图片高度
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ list: 归一化的点位坐标 [x1_norm, y1_norm, x2_norm, y2_norm, ...]
|
|
|
|
|
+ """
|
|
|
|
|
+ normalized_points = []
|
|
|
|
|
+
|
|
|
|
|
+ for point in points:
|
|
|
|
|
+ x, y = point
|
|
|
|
|
+ # 归一化坐标
|
|
|
|
|
+ x_norm = x / img_width
|
|
|
|
|
+ y_norm = y / img_height
|
|
|
|
|
+ normalized_points.extend([x_norm, y_norm])
|
|
|
|
|
+
|
|
|
|
|
+ return normalized_points
|
|
|
|
|
+
|
|
|
|
|
+def parse_labelme_json(json_data):
|
|
|
|
|
+ """
|
|
|
|
|
+ 解析LabelMe格式的JSON文件
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ json_data: JSON数据
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ list: 包含(class_name, bbox)的列表
|
|
|
|
|
+ """
|
|
|
|
|
+ annotations = []
|
|
|
|
|
+ img_width = json_data.get('imageWidth', 0)
|
|
|
|
|
+ img_height = json_data.get('imageHeight', 0)
|
|
|
|
|
+
|
|
|
|
|
+ if img_width == 0 or img_height == 0:
|
|
|
|
|
+ raise ValueError("JSON文件中缺少图片尺寸信息")
|
|
|
|
|
+
|
|
|
|
|
+ for shape in json_data.get('shapes', []):
|
|
|
|
|
+ label = shape.get('label', '')
|
|
|
|
|
+ shape_type = shape.get('shape_type', 'rectangle')
|
|
|
|
|
+ points = shape.get('points', [])
|
|
|
|
|
+
|
|
|
|
|
+ if shape_type == 'rectangle' and len(points) == 2:
|
|
|
|
|
+ # 矩形格式: [[x1, y1], [x2, y2]]
|
|
|
|
|
+ x1, y1 = points[0]
|
|
|
|
|
+ x2, y2 = points[1]
|
|
|
|
|
+
|
|
|
|
|
+ # 确保坐标顺序正确
|
|
|
|
|
+ x_min = min(x1, x2)
|
|
|
|
|
+ y_min = min(y1, y2)
|
|
|
|
|
+ x_max = max(x1, x2)
|
|
|
|
|
+ y_max = max(y1, y2)
|
|
|
|
|
+
|
|
|
|
|
+ bbox = [x_min, y_min, x_max, y_max]
|
|
|
|
|
+ annotations.append((label, bbox, "xyxy", img_width, img_height))
|
|
|
|
|
+
|
|
|
|
|
+ elif shape_type == 'polygon' and len(points) >= 3:
|
|
|
|
|
+ # 多边形格式: 保留所有点位信息
|
|
|
|
|
+ annotations.append((label, points, "polygon", img_width, img_height))
|
|
|
|
|
+
|
|
|
|
|
+ return annotations
|
|
|
|
|
+
|
|
|
|
|
+def parse_coco_json(json_data):
|
|
|
|
|
+ """
|
|
|
|
|
+ 解析COCO格式的JSON文件
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ json_data: JSON数据
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ dict: 按图片ID分组的标注信息
|
|
|
|
|
+ """
|
|
|
|
|
+ # 构建类别映射
|
|
|
|
|
+ categories = {cat['id']: cat['name'] for cat in json_data.get('categories', [])}
|
|
|
|
|
+
|
|
|
|
|
+ # 构建图片信息映射
|
|
|
|
|
+ images = {img['id']: img for img in json_data.get('images', [])}
|
|
|
|
|
+
|
|
|
|
|
+ # 按图片分组标注
|
|
|
|
|
+ annotations_by_image = {}
|
|
|
|
|
+
|
|
|
|
|
+ for ann in json_data.get('annotations', []):
|
|
|
|
|
+ image_id = ann['image_id']
|
|
|
|
|
+ category_id = ann['category_id']
|
|
|
|
|
+ bbox = ann['bbox'] # COCO格式: [x, y, width, height]
|
|
|
|
|
+
|
|
|
|
|
+ if image_id not in annotations_by_image:
|
|
|
|
|
+ annotations_by_image[image_id] = []
|
|
|
|
|
+
|
|
|
|
|
+ if image_id in images:
|
|
|
|
|
+ img_info = images[image_id]
|
|
|
|
|
+ img_width = img_info['width']
|
|
|
|
|
+ img_height = img_info['height']
|
|
|
|
|
+ class_name = categories.get(category_id, f'class_{category_id}')
|
|
|
|
|
+
|
|
|
|
|
+ annotations_by_image[image_id].append((
|
|
|
|
|
+ class_name, bbox, "coco", img_width, img_height, img_info['file_name']
|
|
|
|
|
+ ))
|
|
|
|
|
+
|
|
|
|
|
+ return annotations_by_image
|
|
|
|
|
+
|
|
|
|
|
+def parse_yolo_json(json_data):
|
|
|
|
|
+ """
|
|
|
|
|
+ 解析自定义YOLO JSON格式
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ json_data: JSON数据
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ list: 包含(class_name, bbox)的列表
|
|
|
|
|
+ """
|
|
|
|
|
+ annotations = []
|
|
|
|
|
+ img_width = json_data.get('image_width', json_data.get('width', 0))
|
|
|
|
|
+ img_height = json_data.get('image_height', json_data.get('height', 0))
|
|
|
|
|
+
|
|
|
|
|
+ if img_width == 0 or img_height == 0:
|
|
|
|
|
+ raise ValueError("JSON文件中缺少图片尺寸信息")
|
|
|
|
|
+
|
|
|
|
|
+ for obj in json_data.get('objects', json_data.get('annotations', [])):
|
|
|
|
|
+ class_name = obj.get('class', obj.get('category', obj.get('label', '')))
|
|
|
|
|
+
|
|
|
|
|
+ # 支持多种边界框格式
|
|
|
|
|
+ if 'bbox' in obj:
|
|
|
|
|
+ bbox = obj['bbox']
|
|
|
|
|
+ bbox_format = obj.get('bbox_format', 'xywh')
|
|
|
|
|
+ elif 'bounding_box' in obj:
|
|
|
|
|
+ bbox = obj['bounding_box']
|
|
|
|
|
+ bbox_format = obj.get('bbox_format', 'xywh')
|
|
|
|
|
+ elif all(k in obj for k in ['x', 'y', 'width', 'height']):
|
|
|
|
|
+ bbox = [obj['x'], obj['y'], obj['width'], obj['height']]
|
|
|
|
|
+ bbox_format = 'xywh'
|
|
|
|
|
+ elif all(k in obj for k in ['x_min', 'y_min', 'x_max', 'y_max']):
|
|
|
|
|
+ bbox = [obj['x_min'], obj['y_min'], obj['x_max'], obj['y_max']]
|
|
|
|
|
+ bbox_format = 'xyxy'
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f"警告: 无法解析对象的边界框格式: {obj}")
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ annotations.append((class_name, bbox, bbox_format, img_width, img_height))
|
|
|
|
|
+
|
|
|
|
|
+ return annotations
|
|
|
|
|
+
|
|
|
|
|
+def convert_json_to_yolo(json_file_path, output_dir, class_mapping=None, json_format="auto"):
|
|
|
|
|
+ """
|
|
|
|
|
+ 将JSON标注文件转换为YOLO格式
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ json_file_path: JSON文件路径
|
|
|
|
|
+ output_dir: 输出目录
|
|
|
|
|
+ class_mapping: 类别名称到ID的映射字典
|
|
|
|
|
+ json_format: JSON格式类型 ("auto", "labelme", "coco", "yolo")
|
|
|
|
|
+ """
|
|
|
|
|
+
|
|
|
|
|
+ with open(json_file_path, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ json_data = json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+ # 自动检测JSON格式
|
|
|
|
|
+ if json_format == "auto":
|
|
|
|
|
+ if 'shapes' in json_data and 'imageWidth' in json_data:
|
|
|
|
|
+ json_format = "labelme"
|
|
|
|
|
+ elif 'categories' in json_data and 'annotations' in json_data and 'images' in json_data:
|
|
|
|
|
+ json_format = "coco"
|
|
|
|
|
+ else:
|
|
|
|
|
+ json_format = "yolo"
|
|
|
|
|
+
|
|
|
|
|
+ print(f"检测到JSON格式: {json_format}")
|
|
|
|
|
+
|
|
|
|
|
+ # 解析JSON数据
|
|
|
|
|
+ if json_format == "labelme":
|
|
|
|
|
+ annotations = parse_labelme_json(json_data)
|
|
|
|
|
+ # 为LabelMe格式生成单个txt文件
|
|
|
|
|
+ base_name = Path(json_file_path).stem
|
|
|
|
|
+ output_file = os.path.join(output_dir, f"{base_name}.txt")
|
|
|
|
|
+
|
|
|
|
|
+ with open(output_file, 'w', encoding='utf-8') as f:
|
|
|
|
|
+ for class_name, data, data_format, img_width, img_height in annotations:
|
|
|
|
|
+ # 获取类别ID
|
|
|
|
|
+ if class_mapping and class_name in class_mapping:
|
|
|
|
|
+ class_id = class_mapping[class_name]
|
|
|
|
|
+ else:
|
|
|
|
|
+ class_id = 0 # 默认类别ID
|
|
|
|
|
+
|
|
|
|
|
+ if data_format == "polygon":
|
|
|
|
|
+ # 处理多边形点位
|
|
|
|
|
+ normalized_points = convert_polygon_to_yolo(data, img_width, img_height)
|
|
|
|
|
+ # 写入YOLO格式的多边形标注
|
|
|
|
|
+ points_str = ' '.join([f"{coord:.6f}" for coord in normalized_points])
|
|
|
|
|
+ f.write(f"{class_id} {points_str}\n")
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 处理边界框
|
|
|
|
|
+ center_x, center_y, width, height = convert_bbox_to_yolo(
|
|
|
|
|
+ data, img_width, img_height, data_format
|
|
|
|
|
+ )
|
|
|
|
|
+ # 写入YOLO格式的边界框标注
|
|
|
|
|
+ f.write(f"{class_id} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}\n")
|
|
|
|
|
+
|
|
|
|
|
+ print(f"已生成: {output_file}")
|
|
|
|
|
+
|
|
|
|
|
+ elif json_format == "coco":
|
|
|
|
|
+ annotations_by_image = parse_coco_json(json_data)
|
|
|
|
|
+
|
|
|
|
|
+ for image_id, annotations in annotations_by_image.items():
|
|
|
|
|
+ if not annotations:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ # 使用第一个标注的文件名信息
|
|
|
|
|
+ file_name = annotations[0][5] # file_name
|
|
|
|
|
+ base_name = Path(file_name).stem
|
|
|
|
|
+ output_file = os.path.join(output_dir, f"{base_name}.txt")
|
|
|
|
|
+
|
|
|
|
|
+ with open(output_file, 'w', encoding='utf-8') as f:
|
|
|
|
|
+ for class_name, bbox, bbox_format, img_width, img_height, _ in annotations:
|
|
|
|
|
+ # 获取类别ID
|
|
|
|
|
+ if class_mapping and class_name in class_mapping:
|
|
|
|
|
+ class_id = class_mapping[class_name]
|
|
|
|
|
+ else:
|
|
|
|
|
+ class_id = 0 # 默认类别ID
|
|
|
|
|
+
|
|
|
|
|
+ # 转换为YOLO格式
|
|
|
|
|
+ center_x, center_y, width, height = convert_bbox_to_yolo(
|
|
|
|
|
+ bbox, img_width, img_height, bbox_format
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 写入YOLO格式
|
|
|
|
|
+ f.write(f"{class_id} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}\n")
|
|
|
|
|
+
|
|
|
|
|
+ print(f"已生成: {output_file}")
|
|
|
|
|
+
|
|
|
|
|
+ elif json_format == "yolo":
|
|
|
|
|
+ annotations = parse_yolo_json(json_data)
|
|
|
|
|
+ base_name = Path(json_file_path).stem
|
|
|
|
|
+ output_file = os.path.join(output_dir, f"{base_name}.txt")
|
|
|
|
|
+
|
|
|
|
|
+ with open(output_file, 'w', encoding='utf-8') as f:
|
|
|
|
|
+ for class_name, bbox, bbox_format, img_width, img_height in annotations:
|
|
|
|
|
+ # 获取类别ID
|
|
|
|
|
+ if class_mapping and class_name in class_mapping:
|
|
|
|
|
+ class_id = class_mapping[class_name]
|
|
|
|
|
+ else:
|
|
|
|
|
+ class_id = 0 # 默认类别ID
|
|
|
|
|
+
|
|
|
|
|
+ # 转换为YOLO格式
|
|
|
|
|
+ center_x, center_y, width, height = convert_bbox_to_yolo(
|
|
|
|
|
+ bbox, img_width, img_height, bbox_format
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ # 写入YOLO格式
|
|
|
|
|
+ f.write(f"{class_id} {center_x:.6f} {center_y:.6f} {width:.6f} {height:.6f}\n")
|
|
|
|
|
+
|
|
|
|
|
+ print(f"已生成: {output_file}")
|
|
|
|
|
+
|
|
|
|
|
+def load_class_mapping(mapping_file):
|
|
|
|
|
+ """
|
|
|
|
|
+ 从文件加载类别映射
|
|
|
|
|
+
|
|
|
|
|
+ Args:
|
|
|
|
|
+ mapping_file: 映射文件路径 (支持txt和json格式)
|
|
|
|
|
+
|
|
|
|
|
+ Returns:
|
|
|
|
|
+ dict: 类别名称到ID的映射
|
|
|
|
|
+ """
|
|
|
|
|
+ if not os.path.exists(mapping_file):
|
|
|
|
|
+ return None
|
|
|
|
|
+
|
|
|
|
|
+ mapping = {}
|
|
|
|
|
+
|
|
|
|
|
+ if mapping_file.endswith('.json'):
|
|
|
|
|
+ with open(mapping_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ mapping = json.load(f)
|
|
|
|
|
+ else:
|
|
|
|
|
+ # txt格式兼容:
|
|
|
|
|
+ # 1) "类名"(行号作为ID)
|
|
|
|
|
+ # 2) "ID 类名" 或 "ID,类名"(显式ID与类名)
|
|
|
|
|
+ # 3) "类名 ID"(显式ID在末尾)
|
|
|
|
|
+ # 会自动忽略行首/行尾的空白与注释(# 开始的内容)
|
|
|
|
|
+ with open(mapping_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ for i, raw in enumerate(f):
|
|
|
|
|
+ line = raw.strip()
|
|
|
|
|
+ if not line:
|
|
|
|
|
+ continue
|
|
|
|
|
+ # 去除行内注释
|
|
|
|
|
+ if '#' in line:
|
|
|
|
|
+ line = line.split('#', 1)[0].strip()
|
|
|
|
|
+ if not line:
|
|
|
|
|
+ continue
|
|
|
|
|
+
|
|
|
|
|
+ cls_name = None
|
|
|
|
|
+ cls_id = None
|
|
|
|
|
+
|
|
|
|
|
+ # 尝试按逗号分隔(例如:"0,fire")
|
|
|
|
|
+ if ',' in line:
|
|
|
|
|
+ parts = [p.strip() for p in line.split(',') if p.strip()]
|
|
|
|
|
+ if len(parts) == 2 and parts[0].isdigit():
|
|
|
|
|
+ cls_id = int(parts[0])
|
|
|
|
|
+ cls_name = parts[1]
|
|
|
|
|
+
|
|
|
|
|
+ # 若未解析到,尝试按空白分隔(例如:"0 fire" 或 "fire 0" 或 "fire")
|
|
|
|
|
+ if cls_name is None:
|
|
|
|
|
+ tokens = [t for t in line.split() if t]
|
|
|
|
|
+ if len(tokens) == 1:
|
|
|
|
|
+ # 仅类名:按行号作为ID
|
|
|
|
|
+ cls_name = tokens[0]
|
|
|
|
|
+ cls_id = i
|
|
|
|
|
+ elif len(tokens) >= 2:
|
|
|
|
|
+ # 两段或以上:尝试识别前后是否为ID
|
|
|
|
|
+ if tokens[0].isdigit():
|
|
|
|
|
+ # "ID 类名(可能包含空格)"
|
|
|
|
|
+ cls_id = int(tokens[0])
|
|
|
|
|
+ cls_name = ' '.join(tokens[1:])
|
|
|
|
|
+ elif tokens[-1].isdigit():
|
|
|
|
|
+ # "类名(可能包含空格) ID"
|
|
|
|
|
+ cls_id = int(tokens[-1])
|
|
|
|
|
+ cls_name = ' '.join(tokens[:-1])
|
|
|
|
|
+ else:
|
|
|
|
|
+ # 都不是数字,则将整行视为类名,按行号作为ID
|
|
|
|
|
+ cls_name = ' '.join(tokens)
|
|
|
|
|
+ cls_id = i
|
|
|
|
|
+
|
|
|
|
|
+ if cls_name:
|
|
|
|
|
+ mapping[cls_name] = cls_id
|
|
|
|
|
+
|
|
|
|
|
+ return mapping
|
|
|
|
|
+
|
|
|
|
|
+def main():
|
|
|
|
|
+ parser = argparse.ArgumentParser(description='JSON标签到YOLO格式转换工具')
|
|
|
|
|
+ parser.add_argument('input_path', help='输入JSON文件或包含JSON文件的目录')
|
|
|
|
|
+ parser.add_argument('-o', '--output', default='./20251124/yolo_labels', help='输出目录 (默认: ./yolo_labels)')
|
|
|
|
|
+ parser.add_argument('-c', '--classes', help='类别映射文件 (txt或json格式)')
|
|
|
|
|
+ parser.add_argument('-f', '--format', choices=['auto', 'labelme', 'coco', 'yolo'],
|
|
|
|
|
+ default='auto', help='JSON格式类型 (默认: auto)')
|
|
|
|
|
+ parser.add_argument('--test', action='store_true', help='测试模式,仅显示解析结果不生成文件')
|
|
|
|
|
+
|
|
|
|
|
+ args = parser.parse_args()
|
|
|
|
|
+
|
|
|
|
|
+ # 创建输出目录
|
|
|
|
|
+ output_dir = args.output
|
|
|
|
|
+ if not args.test:
|
|
|
|
|
+ os.makedirs(output_dir, exist_ok=True)
|
|
|
|
|
+
|
|
|
|
|
+ # 加载类别映射
|
|
|
|
|
+ class_mapping = None
|
|
|
|
|
+ if args.classes:
|
|
|
|
|
+ class_mapping = load_class_mapping(args.classes)
|
|
|
|
|
+ if class_mapping:
|
|
|
|
|
+ print(f"已加载类别映射: {class_mapping}")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f"警告: 无法加载类别映射文件: {args.classes}")
|
|
|
|
|
+
|
|
|
|
|
+ # 处理输入路径
|
|
|
|
|
+ input_path = args.input_path
|
|
|
|
|
+
|
|
|
|
|
+ if os.path.isfile(input_path):
|
|
|
|
|
+ # 单个文件
|
|
|
|
|
+ json_files = [input_path]
|
|
|
|
|
+ elif os.path.isdir(input_path):
|
|
|
|
|
+ # 目录中的所有JSON文件
|
|
|
|
|
+ json_files = glob.glob(os.path.join(input_path, "*.json"))
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f"错误: 输入路径不存在: {input_path}")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ if not json_files:
|
|
|
|
|
+ print(f"错误: 在 {input_path} 中没有找到JSON文件")
|
|
|
|
|
+ return
|
|
|
|
|
+
|
|
|
|
|
+ print(f"找到 {len(json_files)} 个JSON文件")
|
|
|
|
|
+
|
|
|
|
|
+ # 转换文件
|
|
|
|
|
+ success_count = 0
|
|
|
|
|
+ error_count = 0
|
|
|
|
|
+
|
|
|
|
|
+ for json_file in json_files:
|
|
|
|
|
+ try:
|
|
|
|
|
+ print(f"\n处理文件: {json_file}")
|
|
|
|
|
+
|
|
|
|
|
+ if args.test:
|
|
|
|
|
+ # 测试模式:仅解析和显示信息
|
|
|
|
|
+ with open(json_file, 'r', encoding='utf-8') as f:
|
|
|
|
|
+ json_data = json.load(f)
|
|
|
|
|
+
|
|
|
|
|
+ print(f" JSON键: {list(json_data.keys())}")
|
|
|
|
|
+
|
|
|
|
|
+ if 'shapes' in json_data:
|
|
|
|
|
+ print(f" LabelMe格式,包含 {len(json_data['shapes'])} 个标注")
|
|
|
|
|
+ elif 'annotations' in json_data:
|
|
|
|
|
+ print(f" COCO格式,包含 {len(json_data['annotations'])} 个标注")
|
|
|
|
|
+ else:
|
|
|
|
|
+ print(f" 自定义格式")
|
|
|
|
|
+ else:
|
|
|
|
|
+ convert_json_to_yolo(json_file, output_dir, class_mapping, args.format)
|
|
|
|
|
+
|
|
|
|
|
+ success_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ except Exception as e:
|
|
|
|
|
+ print(f" 错误: {e}")
|
|
|
|
|
+ error_count += 1
|
|
|
|
|
+
|
|
|
|
|
+ print(f"\n转换完成:")
|
|
|
|
|
+ print(f" 成功: {success_count} 个文件")
|
|
|
|
|
+ print(f" 失败: {error_count} 个文件")
|
|
|
|
|
+
|
|
|
|
|
+ if not args.test and success_count > 0:
|
|
|
|
|
+ print(f" 输出目录: {output_dir}")
|
|
|
|
|
+
|
|
|
|
|
+if __name__ == "__main__":
|
|
|
|
|
+ # 如果没有命令行参数,使用交互模式
|
|
|
|
|
+ import sys
|
|
|
|
|
+ if len(sys.argv) == 1:
|
|
|
|
|
+ print("JSON标签到YOLO格式转换工具")
|
|
|
|
|
+ print("=" * 50)
|
|
|
|
|
+
|
|
|
|
|
+ # 交互式输入
|
|
|
|
|
+ input_path = input("请输入JSON文件或目录路径: ").strip()
|
|
|
|
|
+ if not input_path:
|
|
|
|
|
+ print("错误: 必须提供输入路径")
|
|
|
|
|
+ sys.exit(1)
|
|
|
|
|
+
|
|
|
|
|
+ output_dir = input("请输入输出目录 (默认: ./yolo_labels): ").strip()
|
|
|
|
|
+ if not output_dir:
|
|
|
|
|
+ output_dir = "./yolo_labels"
|
|
|
|
|
+
|
|
|
|
|
+ classes_file = input("请输入类别映射文件路径 (可选): ").strip()
|
|
|
|
|
+
|
|
|
|
|
+ json_format = input("请输入JSON格式 (auto/labelme/coco/yolo, 默认: auto): ").strip()
|
|
|
|
|
+ if not json_format:
|
|
|
|
|
+ json_format = "auto"
|
|
|
|
|
+
|
|
|
|
|
+ test_mode = input("是否启用测试模式?(y/N): ").strip().lower() == 'y'
|
|
|
|
|
+
|
|
|
|
|
+ # 模拟命令行参数
|
|
|
|
|
+ sys.argv = ['json_to_yolo.py', input_path, '-o', output_dir, '-f', json_format]
|
|
|
|
|
+ if classes_file:
|
|
|
|
|
+ sys.argv.extend(['-c', classes_file])
|
|
|
|
|
+ if test_mode:
|
|
|
|
|
+ sys.argv.append('--test')
|
|
|
|
|
+
|
|
|
|
|
+ main()
|