Meet YOLO26: next-gen vision AI.

Cách huấn luyện YOLO trên 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

Annotations ở định dạng COCO JSON có thể được sử dụng trực tiếp cho việc huấn luyện Ultralytics YOLO mà không cần chuyển đổi sang các tệp .txt trước. Điều này được thực hiện bằng cách tạo lớp con của YOLODataset để phân tích COCO JSON ngay khi chạy và đưa nó vào pipeline huấn luyện thông qua một bộ huấn luyện (trainer) tùy chỉnh.

Cách tiếp cận này giữ cho COCO JSON là nguồn sự thật duy nhất — không cần lệnh convert_coco(), không cần sắp xếp lại thư mục, không cần cá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 (segmentation) và tư thế (pose) yêu cầu các trường nhãn bổ sung (xem FAQ).

Bạn đang tìm cách chuyển đổi một lần?

Xem hướng dẫn chuyển đổi COCO sang YOLO để biết quy trình convert_coco() tiêu chuẩn.

Tổng quan về kiến trúc

Cần có 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 với GroundingDataset tích hợp sẵn, vốn cũng đọc các chú thích JSON trực tiếp. Ba phương thức được ghi đè là: get_img_files(), cache_labels()get_labels().

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

Lớp COCODataset kế thừa từ YOLODataset và ghi đè logic tải nhãn. Thay vì đọc các tệp .txt từ thư mục nhãn, nó mở tệp COCO JSON, lặp qua các chú thích được nhóm theo ảnh và chuyển đổi từng bounding box 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 0 sẽ tự động bị bỏ qua.

Phương thức get_img_files() 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 trong JSON 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ả 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 tệp .cache bên cạnh tệp JSON (ví dụ: instances_train.cache). Trong các lần huấn luyện tiếp theo, bộ nhớ cache đượ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 hash sẽ thất bại và cache sẽ tự động được xây dựng lại.

Kết nối Tập dữ liệu với Pipeline Huấn luyện

Thay đổi duy nhất cần thiết trong trainer là ghi đè build_dataset(). DetectionTrainer mặc định 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, trainer sẽ đọc từ COCO JSON thay vì đọc các tệp đó.

Đườ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 kiểm định, mode="val" phân giải thành val_json. Nếu val_json không được đặt, nó sẽ quay lại sử dụng 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

dataset.yaml sử dụng các trường path, trainval 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 sẽ đọ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 dự kiến:

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 trainer và cấu hình YAML đã sẵn sàng, quá trình 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 quy trình huấn luyện thông thường là đối số trainer=COCOTrainer, yêu cầu Ultralytics sử dụng bộ tải tập dữ liệu tùy chỉnh thay vì bộ 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ộ pipeline huấn luyện chạy như mong đợi, bao gồm kiểm định, lưu checkpoint và ghi nhật ký số liệu.

Triển khai đầy đủ

Để thuận tiện, triển khai đầy đủ đượ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, trainer 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 của bạn và chạy 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)

Để biết các khuyến nghị về siêu tham số, hãy xem hướng dẫn 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 phương pháp này và convert_coco() là gì?

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

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

Không thể với pipeline hiện tại của Ultralytics, vốn mặc định yêu cầu nhãn YOLO .txt. 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 trainer. Sau khi định nghĩa, việc huấn luyện chỉ yêu cầu một lệnh gọi model.train() tiêu chuẩn.

Phương pháp này có hỗ trợ phân đoạn và ước tính tư thế không?

This guide covers object detection. To add instance segmentation support, include the segmentation polygon data from COCO annotations in the segments field of each label dictionary. For pose estimation, include keypoints. The GroundingDataset source code provides a reference implementation for handling segments.

Các phép tăng cường dữ liệu 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 phép tăng cường dữ liệu tích hợp sẵn — mosaic, mixup, copy-paste và những cái khác — đều hoạt động mà không cần sửa đổi.

Các ID danh mục được ánh xạ tới 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à các ID không liên tục. Từ điển names trong dataset.yaml nên tuân theo thứ tự đã sắp xếp giống như mảng categories trong COCO.

Có tốn kém về hiệu năng so với các nhãn đã chuyển đổi trước không?

COCO JSON được phân tích 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 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 lưu trong bộ nhớ. Bộ nhớ cache được xây dựng lại tự động nếu tệp JSON thay đổi.

Bình luận