Files
FoodClassifier/SegFormer/scripts/1_convert_coco_to_segformer.py
T

367 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
COCO格式数据转换为SegFormer训练格式
功能说明:
1. 读取CVAT导出的COCO格式标注文件
2. 将polygon标注转换为像素级maskPNG图像)
3. 生成训练集和验证集的划分
4. 输出符合SegFormer训练要求的目录结构
输出目录结构:
data/segformer_format/
├── images/
│ ├── train/
│ │ ├── img1.jpg
│ │ └── img2.jpg
│ └── val/
│ └── img3.jpg
└── annotations/
├── train/
│ ├── img1.png # 像素值:0=背景, 1=食物区域
│ └── img2.png
└── val/
└── img3.png
"""
import os
import json
import numpy as np
from PIL import Image, ImageDraw
from pathlib import Path
from typing import List, Dict, Tuple
import shutil
class COCOToSegFormerConverter:
"""COCO格式到SegFormer格式的转换器"""
def __init__(self, coco_json_path: str, coco_images_dir: str, output_dir: str):
"""
初始化转换器
Args:
coco_json_path: COCO标注JSON文件路径(例如:instances_default.json
coco_images_dir: COCO图像所在目录
output_dir: 输出目录(将创建segformer_format文件夹)
"""
self.coco_json_path = coco_json_path
self.coco_images_dir = coco_images_dir
self.output_dir = Path(output_dir)
# 加载COCO标注
print(f"加载COCO标注文件: {coco_json_path}")
with open(coco_json_path, 'r', encoding='utf-8') as f:
self.coco_data = json.load(f)
print(f"✓ 图像数量: {len(self.coco_data['images'])}")
print(f"✓ 标注数量: {len(self.coco_data['annotations'])}")
print(f"✓ 类别数量: {len(self.coco_data['categories'])}")
# 创建输出目录结构
self._create_output_dirs()
def _create_output_dirs(self):
"""创建输出目录结构"""
dirs = [
self.output_dir / "images" / "train",
self.output_dir / "images" / "val",
self.output_dir / "annotations" / "train",
self.output_dir / "annotations" / "val",
]
for d in dirs:
d.mkdir(parents=True, exist_ok=True)
print(f"✓ 输出目录创建完成: {self.output_dir}")
def _polygon_to_mask(self, segmentation: List, image_size: Tuple[int, int]) -> np.ndarray:
"""
将COCO的polygon格式转换为像素级mask
Args:
segmentation: COCO的segmentation字段(polygon列表)
image_size: 图像尺寸 (width, height)
Returns:
mask: 二值mask数组 (H, W),1表示目标区域,0表示背景
"""
width, height = image_size
mask = Image.new('L', (width, height), 0) # 黑色背景
# COCO的segmentation可能包含多个polygon(例如:一个物体被遮挡分成多个部分)
for polygon in segmentation:
# polygon格式: [x1, y1, x2, y2, x3, y3, ...]
# 转换为坐标点列表: [(x1,y1), (x2,y2), ...]
points = [(polygon[i], polygon[i+1]) for i in range(0, len(polygon), 2)]
# 在mask上绘制填充的多边形
ImageDraw.Draw(mask).polygon(points, outline=1, fill=1)
return np.array(mask)
def _rle_to_mask(self, rle: Dict, image_size: Tuple[int, int]) -> np.ndarray:
"""
将COCO的RLE (Run-Length Encoding) 格式转换为像素级mask
RLE格式说明:
- counts: [v1, v2, v3, ...] 交替表示背景和前景的像素数量
- 从背景(0)开始,然后是前景(1),再是背景(0),依此类推
- 像素顺序:从左到右,从上到下(行优先)
Args:
rle: COCO的RLE字典,格式为 {"counts": [n1, n2, ...], "size": [height, width]}
image_size: 图像尺寸 (width, height)
Returns:
mask: 二值mask数组 (H, W),1表示目标区域,0表示背景
"""
width, height = image_size
# 获取RLE的counts
if 'counts' in rle:
counts = rle['counts']
else:
print(f" ⚠️ 警告: RLE格式不包含counts字段")
return np.zeros((height, width), dtype=np.uint8)
# 创建一维数组
mask = np.zeros(height * width, dtype=np.uint8)
current_pos = 0
current_value = 0 # 从背景开始
for count in counts:
# 如果当前是前景(1),填充mask
if current_value == 1:
mask[current_pos:current_pos + count] = 1
# 移动位置
current_pos += count
# 切换值(0 -> 1 -> 0 -> 1 ...
current_value = 1 - current_value
# 重塑为2D数组 (H, W)
mask = mask.reshape((height, width))
return mask
def _merge_annotations(self, image_id: int, image_size: Tuple[int, int]) -> np.ndarray:
"""
合并一张图像的所有标注为单一mask
由于用户标注时所有食材区域都是同一类别,我们需要将同一图像的多个标注合并
支持两种格式:
1. Polygon格式: segmentation 是列表的列表
2. RLE格式: segmentation 是包含 'counts' 字段的字典
Args:
image_id: COCO图像ID
image_size: 图像尺寸 (width, height)
Returns:
merged_mask: 合并后的mask (H, W)
像素值: 0=背景(未标注区域), 1=食物区域
"""
width, height = image_size
merged_mask = np.zeros((height, width), dtype=np.uint8)
# 找到该图像的所有标注
annotations = [ann for ann in self.coco_data['annotations']
if ann['image_id'] == image_id]
# 将所有标注合并到同一个mask
for ann in annotations:
if 'segmentation' not in ann:
continue
segmentation = ann['segmentation']
# 判断是RLE格式还是Polygon格式
if isinstance(segmentation, dict) and 'counts' in segmentation:
# RLE格式
obj_mask = self._rle_to_mask(segmentation, image_size)
elif isinstance(segmentation, list) and len(segmentation) > 0:
# Polygon格式
obj_mask = self._polygon_to_mask(segmentation, image_size)
else:
print(f" ⚠️ 警告: 未知的segmentation格式: {type(segmentation)}")
continue
# 合并到总mask(取并集)
merged_mask = np.maximum(merged_mask, obj_mask)
return merged_mask
def convert(self, train_ratio: float = 0.8, random_seed: int = 42):
"""
执行转换流程
Args:
train_ratio: 训练集占比(0.8表示80%训练,20%验证)
random_seed: 随机种子,确保每次划分一致
"""
print("\n" + "="*60)
print("开始转换数据集")
print("="*60)
# 创建图像ID到文件名的映射
id_to_image = {img['id']: img for img in self.coco_data['images']}
# 随机划分训练集和验证集
np.random.seed(random_seed)
image_ids = list(id_to_image.keys())
np.random.shuffle(image_ids)
split_idx = int(len(image_ids) * train_ratio)
train_ids = image_ids[:split_idx]
val_ids = image_ids[split_idx:]
print(f"\n数据集划分:")
print(f" 训练集: {len(train_ids)} 张图像")
print(f" 验证集: {len(val_ids)} 张图像")
# 处理训练集
print(f"\n处理训练集...")
self._process_split(train_ids, id_to_image, split='train')
# 处理验证集
print(f"\n处理验证集...")
self._process_split(val_ids, id_to_image, split='val')
# 保存数据集统计信息
self._save_dataset_info(train_ids, val_ids)
print("\n" + "="*60)
print("✓ 数据集转换完成!")
print("="*60)
print(f"\n输出目录: {self.output_dir}")
print("\n下一步: 运行 2_visualize_data.py 查看转换结果")
def _process_split(self, image_ids: List[int], id_to_image: Dict, split: str):
"""
处理训练集或验证集
Args:
image_ids: 图像ID列表
id_to_image: ID到图像信息的映射
split: 'train' 或 'val'
"""
for idx, img_id in enumerate(image_ids, 1):
img_info = id_to_image[img_id]
file_name = img_info['file_name']
width = img_info['width']
height = img_info['height']
print(f" [{idx}/{len(image_ids)}] {file_name}")
# 1. 复制图像到目标目录
src_image_path = Path(self.coco_images_dir) / file_name
dst_image_path = self.output_dir / "images" / split / file_name
if not src_image_path.exists():
print(f" ⚠️ 警告: 图像文件不存在 {src_image_path}")
continue
shutil.copy2(src_image_path, dst_image_path)
# 2. 生成mask并保存为PNG
mask = self._merge_annotations(img_id, (width, height))
# 保存mask(像素值即为类别ID:0=背景, 1=食物)
mask_filename = Path(file_name).stem + '.png' # 改为.png扩展名
mask_path = self.output_dir / "annotations" / split / mask_filename
# 使用PIL保存,确保像素值不被压缩
Image.fromarray(mask, mode='L').save(mask_path)
# 统计信息
food_pixels = np.sum(mask == 1)
total_pixels = mask.size
food_ratio = food_pixels / total_pixels * 100
print(f" ✓ 食物区域占比: {food_ratio:.1f}%")
def _save_dataset_info(self, train_ids: List[int], val_ids: List[int]):
"""保存数据集统计信息"""
info = {
"dataset_name": "Food Segmentation Dataset",
"num_classes": 2, # 背景 + 食物
"class_names": ["background", "food"],
"train_size": len(train_ids),
"val_size": len(val_ids),
"total_size": len(train_ids) + len(val_ids),
"image_format": "jpg/png",
"annotation_format": "png (pixel value = class id)",
}
info_path = self.output_dir / "dataset_info.json"
with open(info_path, 'w', encoding='utf-8') as f:
json.dump(info, f, indent=2, ensure_ascii=False)
print(f"\n✓ 数据集信息已保存: {info_path}")
def main():
"""
主函数:配置路径并执行转换
使用前请修改以下路径:
1. COCO_JSON_PATH: CVAT导出的JSON文件路径
2. COCO_IMAGES_DIR: CVAT导出的图像目录
3. OUTPUT_DIR: 输出目录(将创建segformer_format文件夹)
"""
# ==================== 配置区 ====================
# 请根据您的实际路径修改以下三个变量
# CVAT导出的COCO标注文件(通常名为instances_default.json
COCO_JSON_PATH = "d:/MyProjects/PythonProjects/FoodClassifier/SegFormer/data/raw_coco/annotations/instances_default.json"
# CVAT导出的图像目录
COCO_IMAGES_DIR = "d:/MyProjects/PythonProjects/FoodClassifier/SegFormer/data/raw_coco/images"
# 输出目录(将在此目录下创建segformer_format文件夹)
OUTPUT_DIR = "d:/MyProjects/PythonProjects/FoodClassifier/SegFormer/data/segformer_format"
# 训练集/验证集划分比例(0.8表示80%训练,20%验证)
TRAIN_RATIO = 0.8
# 随机种子(保证每次运行划分结果一致)
RANDOM_SEED = 42
# ===============================================
print("COCO数据集转换工具")
print("目标格式: SegFormer训练格式\n")
# 检查输入文件是否存在
if not os.path.exists(COCO_JSON_PATH):
print(f"❌ 错误: COCO标注文件不存在")
print(f" 路径: {COCO_JSON_PATH}")
print(f"\n请检查:")
print(f" 1. 是否已从CVAT导出COCO格式数据")
print(f" 2. 标注文件路径是否正确")
return
if not os.path.exists(COCO_IMAGES_DIR):
print(f"❌ 错误: 图像目录不存在")
print(f" 路径: {COCO_IMAGES_DIR}")
return
# 创建转换器并执行转换
converter = COCOToSegFormerConverter(
coco_json_path=COCO_JSON_PATH,
coco_images_dir=COCO_IMAGES_DIR,
output_dir=OUTPUT_DIR
)
converter.convert(train_ratio=TRAIN_RATIO, random_seed=RANDOM_SEED)
print("\n" + "="*60)
print("转换完成! 接下来的步骤:")
print("="*60)
print("1. 运行 2_visualize_data.py 检查转换结果")
print("2. 运行 3_train_minimal.py 开始训练")
if __name__ == "__main__":
main()