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).
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:
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ệnCOCOTrainer— ghi đèbuild_dataset()để sử dụngCOCODatasetthay choYOLODatasetmặ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() và 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.
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, train và val để 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 train và val 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_json và val_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 namesCấ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.yamlChạ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.
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 ... và 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.txtxuố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ụngconvert_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ọimodel.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
segmentationtừ annotation COCO vào trườngsegmentscủa mỗi dictionary label. Đối với pose estimation, hãy thêmkeypoints. Source code củaGroundingDatasetcung cấp phần triển khai tham khảo để xử lý segment.Có.
COCODatasetmở rộngYOLODataset, 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
idvà á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. Dictionarynamestrongdataset.yamlphải tuân theo cùng thứ tự đã sắp xếp với mảng COCOcategories.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.cachesau bất kỳ chỉnh sửa nào giữ nguyên độ dài file.