Skip to main content

Cách Huấn Luyện YOLO bằng COCO JSON mà không Cần Chuyển Đổi

Tại sao nên Huấn Luyện Trực tiếp trên COCO JSON

sạch hơn. trong COCO JSON định dạng có thể được sử dụng trực tiếp cho Ultralytics YOLO huấn luyện mà không cần chuyển đổi sang .txt các tệp trước. Điều này được thực hiện bằng cách kế thừa YOLODataset để phân tích cú pháp COCO JSON ngay lập tức và tích hợp nó vào quy trình huấn luyện thông qua một trình huấn luyện tùy chỉnh.

Cách tiếp cận này giữ COCO JSON làm nguồn chân lý duy nhất — không cần convert_coco() lệnh gọi, không tổ chức lại thư mục, không có tệp nhãn trung gian. YOLO26 và tất cả các mô hình phát hiện Ultralytics YOLO khác đều được hỗ trợ. Các mô hình phân đoạn và pose yêu cầu các trường nhãn bổ sung (xem Câu hỏi thường gặp (FAQ)).

Bạn đang tìm kiếm một phương pháp chuyển đổi một lần?

Xem phần Hướng dẫn Chuyển đổi COCO sang YOLO cho tiêu chuẩn convert_coco() quy trình làm việc.

Tổng quan kiến trúc

Cần hai lớp:

  1. COCODataset — đọc COCO JSON và chuyển đổi bounding boxes sang định dạng YOLO trong bộ nhớ trong quá trình huấn luyện
  2. COCOTrainer — ghi đè build_dataset() để sử dụng COCODataset thay vì YOLODataset

mặc định. Việc triển khai tuân theo cùng một mô hình như GroundingDataset tích hợp sẵn, vốn cũng đọc trực tiếp các chú thích JSON. Ba phương thức được ghi đè:get_img_files(), cache_labels(), và get_labels().

Xây dựng Lớp Tập dữ liệu COCO JSON

Phương thức COCODataset lớp kế thừa từ YOLODataset và ghi đè logic tải nhãn. Thay vì đọc .txt tệp từ thư mục nhãn, nó mở tệp COCO JSON, lặp qua các chú thích được nhóm theo hình ảnh và chuyển đổi từng hộp bao từ định dạng pixel COCO [x_min, y_min, width, height] sang định dạng tâm chuẩn hóa YOLO [x_center, y_center, width, height] . Các chú thích đám đông (iscrowd: 1) và các hộp có diện tích bằng không sẽ tự động bị bỏ qua.

Phương thức get_img_files() phương thức trả về một danh sách trống vì đường dẫn hình ảnh được giải quyết từ trường file_name bên trong cache_labels(). ID danh mục được sắp xếp và ánh xạ lại thành các chỉ mục lớp bắt đầu từ 0, vì vậy cả hai lược đồ ID bắt đầu từ 1 (COCO tiêu chuẩn) và không liên tục đều hoạt động chính xác.

import json
from collections import defaultdict
from pathlib import Path

import numpy as np

from ultralytics.data.dataset import DATASET_CACHE_VERSION, YOLODataset
from ultralytics.data.utils import get_hash, load_dataset_cache_file, save_dataset_cache_file
from ultralytics.utils import TQDM

class COCODataset(YOLODataset):
    """Dataset that reads COCO JSON annotations directly without conversion to .txt files."""

    def __init__(self, *args, json_file="", **kwargs):
        self.json_file = json_file
        super().__init__(*args, data={"channels": 3}, **kwargs)

    def get_img_files(self, img_path):
        """Image paths are resolved from the JSON file, not from scanning a directory."""
        return []

    def cache_labels(self, path=Path("./labels.cache")):
        """Parse COCO JSON and convert annotations to YOLO format. Results are saved to a .cache file."""
        x = {"labels": []}
        with open(self.json_file) as f:
            coco = json.load(f)

        images = {img["id"]: img for img in coco["images"]}

        # Sort categories by ID and map to 0-indexed classes
        categories = {cat["id"]: i for i, cat in enumerate(sorted(coco["categories"], key=lambda c: c["id"]))}

        img_to_anns = defaultdict(list)
        for ann in coco["annotations"]:
            img_to_anns[ann["image_id"]].append(ann)

        for img_info in TQDM(coco["images"], desc="reading annotations"):
            h, w = img_info["height"], img_info["width"]
            im_file = Path(self.img_path) / img_info["file_name"]
            if not im_file.exists():
                continue

            self.im_files.append(str(im_file))
            bboxes = []
            for ann in img_to_anns.get(img_info["id"], []):
                if ann.get("iscrowd", False):
                    continue
                # COCO: [x, y, w, h] top-left in pixels -> YOLO: [cx, cy, w, h] center normalized
                box = np.array(ann["bbox"], dtype=np.float32)
                box[:2] += box[2:] / 2  # top-left to center
                box[[0, 2]] /= w  # normalize x
                box[[1, 3]] /= h  # normalize y
                if box[2] <= 0 or box[3] <= 0:
                    continue
                cls = categories[ann["category_id"]]
                bboxes.append([cls, *box.tolist()])

            lb = np.array(bboxes, dtype=np.float32) if bboxes else np.zeros((0, 5), dtype=np.float32)
            x["labels"].append(
                {
                    "im_file": str(im_file),
                    "shape": (h, w),
                    "cls": lb[:, 0:1],
                    "bboxes": lb[:, 1:],
                    "segments": [],
                    "normalized": True,
                    "bbox_format": "xywh",
                }
            )
        x["hash"] = get_hash([self.json_file, str(self.img_path)])
        save_dataset_cache_file(self.prefix, path, x, DATASET_CACHE_VERSION)
        return x

    def get_labels(self):
        """Load labels from .cache file if available, otherwise parse JSON and create the cache."""
        cache_path = Path(self.json_file).with_suffix(".cache")
        try:
            cache = load_dataset_cache_file(cache_path)
            assert cache["version"] == DATASET_CACHE_VERSION
            assert cache["hash"] == get_hash([self.json_file, str(self.img_path)])
            self.im_files = [lb["im_file"] for lb in cache["labels"]]
        except (FileNotFoundError, AssertionError, AttributeError, KeyError, ModuleNotFoundError):
            cache = self.cache_labels(cache_path)
        cache.pop("hash", None)
        cache.pop("version", None)
        return cache["labels"]

Các nhãn đã phân tích được lưu vào một .cache tệp bên cạnh JSON (ví dụ: instances_train.cache). Trong các lần chạy huấn luyện tiếp theo, bộ đệm được tải trực tiếp, bỏ qua việc phân tích JSON. Nếu tệp JSON thay đổi, kiểm tra băm sẽ thất bại và bộ đệm sẽ được xây dựng lại tự động.

Kết nối Tập dữ liệu với Quy trình Huấn luyện

Thay đổi duy nhất cần thiết trong trình huấn luyện là ghi đè build_dataset(). Mặc định DetectionTrainer xây dựng một YOLODataset để quét các tệp nhãn .txt. Bằng cách thay thế nó bằng COCODataset, trình huấn luyện sẽ đọc từ COCO JSON thay thế.

Đường dẫn tệp JSON được lấy từ trường train_json / val_json tùy chỉnh trong cấu hình dữ liệu (xem Bước 3). Trong quá trình huấn luyện, mode="train" phân giải thành train_json; trong quá trình xác thực, mode="val" phân giải thành val_json. Nếu val_json không được đặt, nó sẽ quay lại train_json.

from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import colorstr

class COCOTrainer(DetectionTrainer):
    """Trainer that uses COCODataset for direct COCO JSON training."""

    def build_dataset(self, img_path, mode="train", batch=None):
        json_file = self.data["train_json"] if mode == "train" else self.data.get("val_json", self.data["train_json"])
        return COCODataset(
            img_path=img_path,
            json_file=json_file,
            imgsz=self.args.imgsz,
            batch_size=batch,
            augment=mode == "train",
            hyp=self.args,
            rect=self.args.rect or mode == "val",
            cache=self.args.cache or None,
            single_cls=self.args.single_cls or False,
            stride=int(self.model.stride.max()) if hasattr(self, "model") and self.model else 32,
            pad=0.0 if mode == "train" else 0.5,
            prefix=colorstr(f"{mode}: "),
            task=self.args.task,
            classes=self.args.classes,
            fraction=self.args.fraction if mode == "train" else 1.0,
        )

Cấu hình dataset.yaml cho COCO JSON

Phương thức dataset.yaml sử dụng các trường path, train, và val tiêu chuẩn để định vị các thư mục hình ảnh. Hai trường bổ sung, train_jsonval_json, chỉ định các tệp chú thích COCO mà COCOTrainer đọc. Các trường ncnames xác định số lượng lớp và tên của chúng, khớp với thứ tự đã sắp xếp của categories trong JSON.

path: /path/to/images # root directory with train/ and val/ subfolders
train: train
val: val

# COCO JSON annotation files
train_json: /path/to/annotations/instances_train.json
val_json: /path/to/annotations/instances_val.json

nc: 80
names:
    0: person
    1: bicycle
    # ... remaining class names

Cấu trúc thư mục mong đợi:

my_dataset/
  images/
    train/
      img_001.jpg
      ...
    val/
      img_100.jpg
      ...
  annotations/
    instances_train.json
    instances_val.json
  dataset.yaml

Chạy Huấn luyện trên COCO JSON

Với lớp tập dữ liệu, lớp huấn luyện và cấu hình YAML đã sẵn sàng, việc huấn luyện hoạt động thông qua lệnh gọi model.train() tiêu chuẩn. Sự khác biệt duy nhất so với một lần chạy huấn luyện bình thường là đối số trainer=COCOTrainer, cho biết Ultralytics sử dụng trình tải tập dữ liệu tùy chỉnh thay vì trình tải mặc định.

from ultralytics import YOLO

model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOTrainer)

Toàn bộ đào tạo quy trình chạy như mong đợi, bao gồm xác thực, lưu checkpoint và ghi nhật ký số liệu.

Triển khai Đầy đủ

Để thuận tiện, toàn bộ quá trình triển khai được cung cấp bên dưới dưới dạng một tập lệnh sao chép-dán duy nhất. Nó bao gồm tập dữ liệu tùy chỉnh, trình huấn luyện tùy chỉnh và lệnh gọi huấn luyện. Lưu tệp này cùng với dataset.yaml và chạy nó trực tiếp.

import json
from collections import defaultdict
from pathlib import Path

import numpy as np

from ultralytics import YOLO
from ultralytics.data.dataset import DATASET_CACHE_VERSION, YOLODataset
from ultralytics.data.utils import get_hash, load_dataset_cache_file, save_dataset_cache_file
from ultralytics.models.yolo.detect import DetectionTrainer
from ultralytics.utils import TQDM, colorstr

class COCODataset(YOLODataset):
    """Dataset that reads COCO JSON annotations directly without conversion to .txt files."""

    def __init__(self, *args, json_file="", **kwargs):
        self.json_file = json_file
        super().__init__(*args, data={"channels": 3}, **kwargs)

    def get_img_files(self, img_path):
        return []

    def cache_labels(self, path=Path("./labels.cache")):
        x = {"labels": []}
        with open(self.json_file) as f:
            coco = json.load(f)

        images = {img["id"]: img for img in coco["images"]}
        categories = {cat["id"]: i for i, cat in enumerate(sorted(coco["categories"], key=lambda c: c["id"]))}

        img_to_anns = defaultdict(list)
        for ann in coco["annotations"]:
            img_to_anns[ann["image_id"]].append(ann)

        for img_info in TQDM(coco["images"], desc="reading annotations"):
            h, w = img_info["height"], img_info["width"]
            im_file = Path(self.img_path) / img_info["file_name"]
            if not im_file.exists():
                continue

            self.im_files.append(str(im_file))
            bboxes = []
            for ann in img_to_anns.get(img_info["id"], []):
                if ann.get("iscrowd", False):
                    continue
                box = np.array(ann["bbox"], dtype=np.float32)
                box[:2] += box[2:] / 2
                box[[0, 2]] /= w
                box[[1, 3]] /= h
                if box[2] <= 0 or box[3] <= 0:
                    continue
                cls = categories[ann["category_id"]]
                bboxes.append([cls, *box.tolist()])

            lb = np.array(bboxes, dtype=np.float32) if bboxes else np.zeros((0, 5), dtype=np.float32)
            x["labels"].append(
                {
                    "im_file": str(im_file),
                    "shape": (h, w),
                    "cls": lb[:, 0:1],
                    "bboxes": lb[:, 1:],
                    "segments": [],
                    "normalized": True,
                    "bbox_format": "xywh",
                }
            )
        x["hash"] = get_hash([self.json_file, str(self.img_path)])
        save_dataset_cache_file(self.prefix, path, x, DATASET_CACHE_VERSION)
        return x

    def get_labels(self):
        cache_path = Path(self.json_file).with_suffix(".cache")
        try:
            cache = load_dataset_cache_file(cache_path)
            assert cache["version"] == DATASET_CACHE_VERSION
            assert cache["hash"] == get_hash([self.json_file, str(self.img_path)])
            self.im_files = [lb["im_file"] for lb in cache["labels"]]
        except (FileNotFoundError, AssertionError, AttributeError, KeyError, ModuleNotFoundError):
            cache = self.cache_labels(cache_path)
        cache.pop("hash", None)
        cache.pop("version", None)
        return cache["labels"]

class COCOTrainer(DetectionTrainer):
    """Trainer that uses COCODataset for direct COCO JSON training."""

    def build_dataset(self, img_path, mode="train", batch=None):
        json_file = self.data["train_json"] if mode == "train" else self.data.get("val_json", self.data["train_json"])
        return COCODataset(
            img_path=img_path,
            json_file=json_file,
            imgsz=self.args.imgsz,
            batch_size=batch,
            augment=mode == "train",
            hyp=self.args,
            rect=self.args.rect or mode == "val",
            cache=self.args.cache or None,
            single_cls=self.args.single_cls or False,
            stride=int(self.model.stride.max()) if hasattr(self, "model") and self.model else 32,
            pad=0.0 if mode == "train" else 0.5,
            prefix=colorstr(f"{mode}: "),
            task=self.args.task,
            classes=self.args.classes,
            fraction=self.args.fraction if mode == "train" else 1.0,
        )

model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOTrainer)

Đối với siêu tham số khuyến nghị, xem Mẹo Huấn luyện Mô hình.

Câu hỏi thường gặp (FAQ)

Sự khác biệt giữa cách này và convert_coco() là gì?

convert_coco() ghi các tệp nhãn .txt ra đĩa dưới dạng chuyển đổi một lần. Cách tiếp cận này phân tích JSON vào đầu mỗi lần chạy huấn luyện và chuyển đổi các chú thích trong bộ nhớ. Sử dụng convert_coco() khi các nhãn định dạng YOLO vĩnh viễn được ưu tiên; sử dụng cách tiếp cận này để giữ COCO JSON làm nguồn chân lý duy nhất mà không tạo thêm tệp.

YOLO có thể huấn luyện trên COCO JSON mà không cần mã tùy chỉnh không?

Không phải với quy trình Ultralytics hiện tại, quy trình này mặc định mong đợi các nhãn .txt YOLO. Hướng dẫn này cung cấp mã tùy chỉnh tối thiểu cần thiết — một lớp tập dữ liệu và một lớp huấn luyện. Sau khi xác định, việc huấn luyện chỉ cần một lệnh gọi model.train() tiêu chuẩn.

Nó có hỗ trợ phân đoạn và ước tính tư thế (pose estimation) không?

Hướng dẫn này bao gồm object detection. Để thêm hỗ trợ instance segmentation, bao gồm dữ liệu đa giác segmentation từ các chú thích COCO trong trường segments của mỗi từ điển nhãn. Đối với pose estimation, bao gồm keypoints. Chữ GroundingDataset mã nguồn cung cấp một triển khai tham chiếu để xử lý các phân đoạn.

Các phép tăng cường (augmentations) có hoạt động với tập dữ liệu tùy chỉnh này không?

Có. COCODataset mở rộng YOLODataset, vì vậy tất cả các tích hợp sẵn data augmentationsmosaic, mixup, copy-paste và các tính năng khác — chạy mà không cần sửa đổi.

ID danh mục được ánh xạ tới các chỉ mục lớp như thế nào?

Các danh mục được sắp xếp theo id và ánh xạ tới các chỉ mục tuần tự bắt đầu từ 0. Điều này xử lý các ID bắt đầu từ 1 (COCO tiêu chuẩn), ID bắt đầu từ 0 và ID không liên tục. Từ điển names trong dataset.yaml phải tuân theo cùng thứ tự sắp xếp như mảng categories COCO.

Có chi phí hiệu năng nào so với các nhãn đã được chuyển đổi trước không?

COCO JSON được phân tích cú pháp một lần trong lần huấn luyện đầu tiên. Các nhãn đã phân tích được lưu vào một tệp .cache, vì vậy các lần chạy tiếp theo tải ngay lập tức mà không cần phân tích lại. Tốc độ huấn luyện giống hệt với huấn luyện YOLO tiêu chuẩn vì các chú thích được giữ trong bộ nhớ. Bộ đệm sẽ được xây dựng lại tự động nếu tệp JSON thay đổi.

Bình luận