COCO数据集是计算机视觉领域最常用的基准数据集之一,包含超过33万张图像和80个常见物体类别。每张图像都带有精确的边界框标注和实例分割标注,这些标注信息以JSON格式存储。而YOLOv8-seg作为Ultralytics公司推出的最新实例分割模型,其训练数据需要特定的TXT格式标注文件。
在实际项目中,我们往往只需要训练特定类别的模型。比如在智能宠物监控场景中,可能只需要检测人和宠物(猫、狗)。这时候就需要从COCO的全类别标注中提取特定类别的数据。YOLOv8-seg的标注文件格式要求每个对象用一行表示,包含类别索引和归一化的多边形坐标点,格式如下:
code复制<class-index> <x1> <y1> <x2> <y2> ... <xn> <yn>
理解这个转换过程的核心在于把握三个关键点:COCO的JSON结构、目标类别的筛选逻辑,以及多边形坐标的归一化处理。COCO的标注文件采用层次化结构,主要包含images、annotations和categories三个关键部分。其中annotations字段中的segmentation字段存储的就是我们需要提取的多边形坐标。
在开始转换之前,我们需要搭建合适的Python环境。推荐使用Python 3.8+和以下关键库:
python复制pip install numpy opencv-python Pillow tqdm pycocotools
数据目录结构对YOLOv8训练至关重要。我们需要预先创建符合YOLOv8要求的目录树:
code复制mydata/
├── images/
│ ├── train/
│ └── val/
└── labels/
├── train/
└── val/
建议使用Pathlib模块来管理路径,这样可以避免跨平台路径分隔符的问题。以下代码可以快速创建这个目录结构:
python复制from pathlib import Path
def create_yolo_dirs(base_path="mydata"):
base = Path(base_path)
for split in ["train", "val"]:
(base/"images"/split).mkdir(parents=True, exist_ok=True)
(base/"labels"/split).mkdir(parents=True, exist_ok=True)
return base
COCO的标注文件是一个大型JSON,我们需要从中提取特定类别的分割标注。首先加载JSON文件:
python复制import json
with open("instances_val2017.json") as f:
coco_data = json.load(f)
建立类别名称到ID的映射关系:
python复制category_map = {cat["name"]: cat["id"] for cat in coco_data["categories"]}
target_classes = ["person", "cat", "dog"]
target_ids = [category_map[name] for name in target_classes]
为了提高处理效率,我们可以预先构建两个字典:
python复制images = {img["id"]: img for img in coco_data["images"]}
img_to_anns = defaultdict(list)
for ann in coco_data["annotations"]:
if ann["category_id"] in target_ids:
img_to_anns[ann["image_id"]].append(ann)
处理crowd标注时需要特别注意,YOLOv8不支持crowd实例的分割训练,我们需要过滤掉iscrowd=1的标注。
COCO中的分割标注有两种形式:
对于简单多边形,我们可以直接使用;对于复杂形状,需要合并多个多边形。以下是合并多边形的关键函数:
python复制def merge_multi_segment(segments):
s = []
segments = [np.array(i).reshape(-1, 2) for i in segments]
idx_list = [[] for _ in range(len(segments))]
for i in range(1, len(segments)):
idx1, idx2 = min_index(segments[i-1], segments[i])
idx_list[i-1].append(idx1)
idx_list[i].append(idx2)
for k in range(2):
if k == 0:
for i, idx in enumerate(idx_list):
if len(idx) == 2 and idx[0] > idx[1]:
segments[i] = segments[i][::-1, :]
segments[i] = np.roll(segments[i], -idx[0], axis=0)
segments[i] = np.concatenate([segments[i], segments[i][:1]])
if i in [0, len(idx_list)-1]:
s.append(segments[i])
else:
for i in range(len(idx_list)-1, -1, -1):
if i not in [0, len(idx_list)-1]:
s.append(segments[i][nidx:])
return s
坐标归一化是转换过程中的关键步骤。我们需要将绝对坐标转换为相对于图像宽高的比例:
python复制def normalize_coordinates(segmentation, img_width, img_height):
points = np.array(segmentation).reshape(-1, 2)
points[:, 0] /= img_width # x坐标归一化
points[:, 1] /= img_height # y坐标归一化
return points.reshape(-1).tolist()
有了前面的基础,现在可以编写完整的转换函数了。这个函数需要处理以下任务:
python复制def convert_to_yolo_format(img_to_anns, images, target_classes, output_dir):
class_to_idx = {name: idx for idx, name in enumerate(target_classes)}
for img_id, anns in img_to_anns.items():
img_info = images[img_id]
txt_path = Path(output_dir) / f"{img_info['file_name'].split('.')[0]}.txt"
with open(txt_path, "w") as f:
for ann in anns:
class_name = next(
cat["name"] for cat in coco_data["categories"]
if cat["id"] == ann["category_id"]
)
class_idx = class_to_idx[class_name]
if len(ann["segmentation"]) > 1:
merged = merge_multi_segment(ann["segmentation"])
seg = normalize_coordinates(merged, img_info["width"], img_info["height"])
else:
seg = normalize_coordinates(ann["segmentation"][0], img_info["width"], img_info["height"])
line = [class_idx] + seg
f.write(" ".join(map(str, line)) + "\n")
在实际项目中,你可能还需要处理一些特殊情况:
生成TXT文件后,还需要将对应的图像文件复制到正确的目录。这里提供一个实用的图像复制函数:
python复制def copy_images_with_annotations(coco_img_dir, txt_dir, output_img_dir):
txt_dir = Path(txt_dir)
output_img_dir = Path(output_img_dir)
for txt_file in txt_dir.glob("*.txt"):
img_name = txt_file.with_suffix(".jpg").name
src = Path(coco_img_dir) / img_name
if src.exists():
shutil.copy(src, output_img_dir / img_name)
为了确保数据质量,建议进行以下检查:
转换完成后,强烈建议可视化检查结果。以下代码可以帮助你快速验证标注是否正确:
python复制def visualize_annotation(img_path, txt_path):
img = cv2.imread(str(img_path))
h, w = img.shape[:2]
with open(txt_path) as f:
for line in f:
parts = list(map(float, line.strip().split()))
class_idx = int(parts[0])
points = np.array(parts[1:]).reshape(-1, 2)
points[:, 0] *= w
points[:, 1] *= h
points = points.astype(np.int32)
cv2.polylines(img, [points], isClosed=True,
color=(0,255,0), thickness=2)
cv2.putText(img, target_classes[class_idx],
tuple(points[0]), cv2.FONT_HERSHEY_SIMPLEX,
0.5, (255,0,0), 1)
cv2.imshow("Annotation", img)
cv2.waitKey(0)
在实际项目中,我遇到过几个常见问题:
当处理完整的COCO数据集时,可能会遇到性能问题。以下是几个优化建议:
python复制from multiprocessing import Pool
def process_image(args):
img_id, anns = args
# 处理单张图像的代码
with Pool(processes=8) as pool:
pool.map(process_image, img_to_anns.items())
增量写入:避免在内存中保存所有结果
进度显示:使用tqdm显示处理进度
内存优化:逐文件处理而非加载整个JSON
python复制import ijson
def stream_json_parse(json_file):
with open(json_file, "rb") as f:
for record in ijson.items(f, "annotations.item"):
if record["category_id"] in target_ids:
yield record
在某些场景下,你可能需要更灵活的类别映射。例如:
以下是一个高级映射示例:
python复制class_mapping = {
"person": 0,
"cat": 1,
"dog": 1, # 将狗和猫映射到同一类别
"horse": -1, # 忽略马
"sheep": -1 # 忽略羊
}
def should_include_annotation(ann):
cat_name = next(
cat["name"] for cat in coco_data["categories"]
if cat["id"] == ann["category_id"]
)
return class_mapping.get(cat_name, -1) >= 0
COCO官方已经提供了训练集和验证集的划分。我们需要保持这种划分:
如果你需要自定义划分比例,可以使用以下函数:
python复制def split_dataset(image_ids, train_ratio=0.9):
np.random.shuffle(image_ids)
split_idx = int(len(image_ids) * train_ratio)
return image_ids[:split_idx], image_ids[split_idx:]
记得在划分时保持图像和标注的对应关系,一个常见的错误是只划分了图像而忘记划分对应的标注文件。
在多个实际项目中应用这个流程后,我总结了一些宝贵经验:
标注质量检查:COCO数据集中存在少量标注错误,建议转换前先过滤掉明显错误的标注。特别是对小物体的分割标注,质量往往不太理想。
类别不平衡处理:像"人"这样的类别样本数远多于其他类别。在训练时可以适当调整采样策略或损失权重。
内存管理:处理完整COCO数据集时,原始JSON文件可能超过1GB。使用ijson这样的流式解析器可以大幅降低内存占用。
版本兼容性:不同年份的COCO数据集JSON结构略有差异,建议明确指定使用的版本(如2017或2014)。
增量处理:对于特别大的数据集,可以考虑分批次处理并将中间结果保存到临时文件。
日志记录:详细记录处理过程中的统计信息,如每个类别的实例数、跳过的标注数量等。这有助于后续分析模型表现。