| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505 |
- #!/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()
|