Cách huấn luyện YOLO trên COCO JSON mà không cần chuyển đổi#
Annotations trong đị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 sang các tệp .txt trước. Cách này hoạt động bằng cách tạo lớp con YOLODataset để phân tích cú pháp COCO JSON ngay khi chạy và tích hợp nó vào pipeline huấn luyện thông qua một custom trainer.
Tại sao nên huấn luyện trực tiếp trên COCO JSON#
Phương pháp này giữ cho COCO JSON làm nguồn chân lý duy nhất (single source of truth) — không cần gọi convert_coco(), không cần sắp xếp lại thư mục, không cần 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).
Xem Hướng dẫn chuyển đổi COCO sang YOLO để biết quy trình chuẩn convert_coco().
Tổng quan về kiến trúc#
Cần có hai lớp:
COCODataset— đọc COCO JSON và chuyển đổi bounding box sang định dạng YOLO trên bộ nhớ trong quá trình huấn luyệnCOCOTrainer— ghi đèbuild_dataset()để sử dụngCOCODatasetthay vìYOLODatasetmặc định
Việc triển khai tuân theo cùng một mẫu như GroundingDataset tích hợp sẵn, cũng đọc các annotation dạng JSON trực tiếp. Ba phương thức được ghi đè: get_img_files(), cache_labels() và get_labels().
Xây dựng lớp Dataset cho 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 annotation đượ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 annotation đá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() trả về một danh sách trống vì đường dẫn ảnh được phân giải 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 chỉ mục lớp bắt đầu từ 0, do đó cả sơ đồ ID bắt đầu từ 1 (COCO chuẩn) và ID 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):
"""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."""
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",
}
)
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"]Nhãn đã phân tích được lưu vào tệp .cache nằm cạnh tệp 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, bỏ qua việc phân tích cú pháp JSON. Nếu tệp JSON thay đổi, quá trình kiểm tra băm (hash check) sẽ thất bại và cache được xây dựng lại tự động.
Kết nối Dataset với quy trình 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 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 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 Cấu hình dataset.yaml). Trong quá trình huấn luyện, mode="train" phân giải thành train_json; trong quá trình đánh giá, mode="val" phân giải thành val_json. Nếu val_json không được thiết lập, nó sẽ quay về 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):
"""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.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 tiêu chuẩn path, train và val để định vị thư mục ảnh. Hai trường bổ sung, train_json và val_json, chỉ định các tệp annotation COCO mà COCOTrainer đọc. Các trường nc và names 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/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
nc: 80
names:
0: person
1: bicycle
# ... remaining class namesCấ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.yamlChạy huấn luyện trên COCO JSON#
Khi đã có sẵn lớp dataset, lớp trainer và cấu hình YAML, quá trình huấn luyện diễn ra thông qua lệnh model.train() chuẩn. Điểm khác biệt duy nhất so với một lần huấn luyện bình thường là đối số trainer=COCOTrainer, thông báo cho Ultralytics sử dụng trình tải dataset 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)Pipeline huấn luyện đầy đủ chạy như mong đợi, bao gồm đánh giá, lưu checkpoint và ghi log metric.
Triển khai đầy đủ#
Để thuận tiện, toàn bộ mã nguồn đượ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 dataset tùy chỉnh, trainer tùy chỉnh và lệnh huấn luyện. Lưu tệp này bên cạnh 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):
"""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."""
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",
}
)
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.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)Bạn hiện có một dataset và trainer tối thiểu để huấn luyện Ultralytics YOLO trực tiếp trên COCO JSON, với các annotation vẫn là nguồn chân lý duy nhất và không có tệp .txt trung gian nào. Mở rộng phương thức cache_labels() bằng segments hoặc keypoints để bao phủ phân đoạn và tư thế, và xem hướng dẫn Mẹo huấn luyện mô hình để biết các khuyến nghị tinh chỉnh hyperparameter.
Câu hỏi thường gặp#
convert_coco()ghi các tệp nhãn.txtvào đĩa dưới dạng một lần chuyển đổi duy nhất. Phương pháp này phân tích cú pháp JSON ở đầu mỗi lần chạy huấn luyện và chuyển đổi các annotation trong bộ nhớ. Sử dụngconvert_coco()khi ưu tiên các nhãn định dạng YOLO vĩnh viễn; sử dụng phương pháp này để giữ COCO JSON làm nguồn chân lý duy nhất mà không tạo thêm các tệp bổ sung.Không thể thực hiện điều đó với pipeline Ultralytics hiện tại, vốn mặc định mong đợi các nhãn
.txtkiểu 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 dataset và một lớp trainer. Sau khi được định nghĩa, quá trình huấn luyện chỉ yêu cầu một lệnhmodel.train()chuẩn.Hướng dẫn này bao gồm object detection. Để thêm hỗ trợ instance segmentation, hãy bao gồm dữ liệu đa giác
segmentationtừ các chú thích COCO trong trườngsegmentscủa mỗi từ điển nhãn. Đối với pose estimation, hãy bao gồmkeypoints.GroundingDatasetsource code cung cấp một triển khai tham khảo để xử lý các phân đoạn.Có.
COCODatasetmở rộngYOLODataset, vì vậy tất cả các tăng cường dữ liệu tích hợp sẵn — mosaic, mixup, copy-paste và các phương pháp khác — đều chạy mà không cần sửa đổi.Các danh mục được sắp xếp theo
idvà được á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 chuẩn), ID bắt đầu từ 0 và ID không liên tục. Từ điểnnamestrongdataset.yamlnên tuân theo cùng thứ tự sắp xếp như mảng COCOcategories.COCO JSON được phân tích cú pháp một lần vào lần chạy huấn luyện đầu tiên. Các nhãn đã phân tích được lưu vào tệp
.cache, do đó 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 cú pháp 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 annotation được giữ trong bộ nhớ. Cache được xây dựng lại tự động nếu tệp JSON thay đổi.