YOLO Vision 2026:

Cách huấn luyện YOLO trên COCO JSON mà không cần chuyển đổi#

Các annotation ở định dạng COCO JSON có thể được sử dụng trực tiếp để huấn luyện Ultralytics YOLO mà không cần chuyển đổi trước sang các file .txt. Cách này hoạt động bằng việc tạo lớp con của YOLODataset để phân tích COCO JSON ngay trong lúc chạy và kết nối lớp này vào pipeline huấn luyện thông qua một trainer tùy chỉnh.

Tại sao nên huấn luyện trực tiếp trên COCO JSON#

Cách tiếp cận này giữ COCO JSON làm nguồn dữ liệu chuẩn duy nhất — không cần gọi convert_coco(), không cần tổ chức lại thư mục và không cần các file label trung gian. YOLO26 và tất cả model phát hiện Ultralytics YOLO khác đều được hỗ trợ. Các model segmentation và pose yêu cầu thêm các trường label (xem FAQ).

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

Xem hướng dẫn Chuyển đổi COCO sang YOLO để biết workflow convert_coco() tiêu chuẩn.

Tổng quan kiến trúc#

Cần hai class:

  1. COCODataset — đọc COCO JSON và chuyển đổi bounding box 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 cho YOLODataset mặc định

Phần triển khai là phiên bản đơn giản hóa của GroundingDataset tích hợp sẵn, vốn cũng đọc annotation JSON trực tiếp. Ba method được ghi đè ở đây — get_img_files(), cache_labels()get_labels() — trong đó GroundingDataset ghi đè nhiều thành phần hơn, bao gồm cả các kiểm tra cache-hash và số lượng instance riêng.

Xây dựng class Dataset cho COCO JSON#

Class COCODataset kế thừa từ YOLODataset và ghi đè logic tải label. Thay vì đọc các file .txt từ thư mục labels, class này mở file COCO JSON, lặp qua các annotation được nhóm theo image 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 annotation crowd (iscrowd: 1) và bounding box có diện tích bằng 0 sẽ được tự động bỏ qua.

Method get_img_files() trả về một danh sách rỗng vì đường dẫn image được xác định từ trường file_name trong JSON cache_labels(). Các category ID được sắp xếp và ánh xạ lại thành chỉ số class bắt đầu từ 0, vì vậy cả scheme ID bắt đầu từ 1 (COCO tiêu chuẩn) lẫn scheme ID không liên tiếp đề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):
        """Initialize the dataset with a COCO JSON annotation file."""
        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."""
        self.fraction = 1.0  # fraction is applied while scanning a directory, which this dataset skips
        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)

        # 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",
                }
            )
        if not x["labels"]:
            raise RuntimeError(f"No images listed in {self.json_file} were found in {self.img_path}")
        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 label đã phân tích được lưu vào file .cache bên cạnh JSON (ví dụ: instances_train.cache). Trong các lần huấn luyện tiếp theo, cache được tải trực tiếp và bỏ qua bước phân tích JSON.

Khóa cache là kích thước file JSON, không phải nội dung của file

get_hash() băm kích thước và đường dẫn file thay vì nội dung file, vì vậy khi chạy lại, JSON chỉ được phân tích lại nếu số byte của JSON thay đổi. Việc thêm hoặc xóa image cũng có thể làm thay đổi kích thước riêng của thư mục image và kích hoạt quá trình xây dựng lại, nhưng không nên dựa vào điều này — hash không kiểm tra từng file image, nên việc thay một image bằng image khác có thể không làm thay đổi kích thước. Một chỉnh sửa giữ nguyên số byte — điều chỉnh một tọa độ, đảo giá trị iscrowd, thay hai tên class có cùng độ dài — sẽ giữ lại cache cũ và huấn luyện trên annotation cũ mà không có cảnh báo; việc thay image tại chỗ cũng không được phát hiện vì cùng lý do. Hãy xóa file .cache sau khi chỉnh sửa annotation hoặc image tại chỗ.

Kết nối Dataset với Pipeline Huấn luyện#

Thay đổi duy nhất cần thực hiện trong trainer là ghi đè build_dataset(). DetectionTrainer mặc định xây dựng một YOLODataset quét các file label .txt. Khi thay thế bằng COCODataset, trainer sẽ đọc từ COCO JSON.

Đường dẫn file JSON được lấy từ trường tùy chỉnh train_json / val_json trong cấu hình dữ liệu (xem Cấu hình dataset.yaml). Trong quá trình huấn luyện, mode="train" được phân giải thành train_json; trong quá trình validation, mode="val" được phân giải thành val_json. Cả hai key đều bắt buộc — hai split đọc các thư mục image khác nhau, nên JSON huấn luyện không thể thay thế cho val_json bị thiếu.

Dataset cũng đặt lại fraction thành 1.0. BaseDataset áp dụng argument đó khi quét một thư mục image, còn bước này bị COCODataset bỏ qua, nên nó không thể xử lý yêu cầu dataset một phần; việc đặt lại giúp dataset không tạo cảm giác rằng nó chấp nhận một giá trị nhưng lại bỏ qua giá trị đó. GroundingDataset tích hợp sẵn cũng đưa ra thỏa hiệp tương tự vì cùng lý do.

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):
        """Build a COCODataset for the given split using the JSON file from the data config."""
        json_file = self.data["train_json"] if mode == "train" else self.data["val_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 tiêu chuẩn path, trainval để xác định thư mục image. Lưu ý rằng path trỏ đến thư mục gốc của image ở đây, vì vậy trainval là tên split thuần túy — khác với hướng dẫn chuyển đổi, trong đó path là thư mục gốc của dataset và các split có tiền tố images/. Hai trường bổ sung, train_jsonval_json, chỉ định các file annotation COCO mà COCOTrainer đọc. Trường names liệt kê tên class theo thứ tự đã sắp xếp của categories trong JSON, và số lượng class được suy ra từ trường này, vì vậy không cần đặt nc.

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

# COCO JSON annotation files (use absolute paths; these custom keys are not resolved against `path`)
train_json: /path/to/my_dataset/annotations/instances_train.json
val_json: /path/to/my_dataset/annotations/instances_val.json

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#

Sau khi đã chuẩn bị class dataset, class trainer và cấu hình YAML, quá trình huấn luyện hoạt động thông qua lệnh gọi model.train() tiêu chuẩn. Điểm khác biệt duy nhất so với một lần chạy huấn luyện thông thường là argument trainer=COCOTrainer, cho Ultralytics biết cần sử dụng data loader tùy chỉnh thay vì loader 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 cả validation trong quá trình huấn luyện, lưu checkpoint và ghi log metric.

`model.val()` độc lập cần override riêng

Chỉ validation trong thời gian huấn luyện mới đi qua COCOTrainer.build_dataset. Một lệnh gọi model.val() riêng biệt sẽ xây dựng YOLODataset tiêu chuẩn, vốn quét các label .txt bên cạnh image nhưng không tìm thấy file nào. Lệnh này không phát sinh lỗi: các image được đếm là background, vì vậy validation chạy đến hết và báo cáo mọi metric là 0, đồng thời cảnh báo No labels found in ...no labels found in detect set, cannot compute metrics without labels. Để validation bên ngoài một lần chạy huấn luyện, hãy tạo class con của validator với cùng override build_dataset và truyền nó vào model.val(validator=...).

Triển khai đầy đủ#

Để thuận tiện, toàn bộ phần triển khai được cung cấp bên dưới dưới dạng một script duy nhất có thể copy-paste. Script bao gồm dataset tùy chỉnh, trainer tùy chỉnh và lệnh gọi huấn luyện. Lưu script này cùng với dataset.yaml rồi 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):
        """Initialize the dataset with a COCO JSON annotation file."""
        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."""
        self.fraction = 1.0  # fraction is applied while scanning a directory, which this dataset skips
        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)

        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",
                }
            )
        if not x["labels"]:
            raise RuntimeError(f"No images listed in {self.json_file} were found in {self.img_path}")
        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"]

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

    def build_dataset(self, img_path, mode="train", batch=None):
        """Build a COCODataset for the given split using the JSON file from the data config."""
        json_file = self.data["train_json"] if mode == "train" else self.data["val_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)

Bây giờ bạn đã có một dataset và trainer tối giản để huấn luyện Ultralytics YOLO trực tiếp trên COCO JSON, trong đó annotation vẫn là nguồn dữ liệu chuẩn duy nhất và không có các file .txt trung gian. Mở rộng method cache_labels() với segments hoặc keypoints để hỗ trợ segmentation và pose, đồng thời xem hướng dẫn Mẹo huấn luyện Model để biết các khuyến nghị tinh chỉnh hyperparameter.

FAQ#

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

  • Không với pipeline Ultralytics hiện tại, vốn mặc định yêu cầu label YOLO .txt. Hướng dẫn này cung cấp lượng code tùy chỉnh tối thiểu cần thiết — một class dataset và một class trainer. Sau khi được định nghĩa, quá trình huấn luyện chỉ cần một lệnh gọi model.train() tiêu chuẩn.

  • Hướng dẫn này bao quát object detection. Để thêm hỗ trợ instance segmentation, hãy đưa dữ liệu polygon segmentation từ annotation COCO vào trường segments của mỗi dictionary label. Đối với pose estimation, hãy thêm keypoints. Source code của GroundingDataset cung cấp phần triển khai tham khảo để xử lý segment.

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

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

  • COCO JSON được phân tích một lần trong lần chạy huấn luyện đầu tiên. Các label đã phân tích được lưu vào file .cache, vì vậy những lần chạy tiếp theo tải ngay mà không cần phân tích lại. Tốc độ huấn luyện giống hệt huấn luyện YOLO tiêu chuẩn vì annotation được giữ trong bộ nhớ. Cache được lập khóa theo kích thước file JSON, vì vậy hãy xóa file .cache sau bất kỳ chỉnh sửa nào giữ nguyên độ dài file.

Bình luận