Cách huấn luyện YOLO trên COCO JSON mà không cần chuyển đổi#
Có thể sử dụng trực tiếp annotation ở định dạng COCO JSON để huấn luyện Ultralytics YOLO mà không cần chuyển đổi sang tệp .txt trước. Cách này hoạt động bằng cách tạo lớp con từ YOLODataset để phân tích COCO JSON ngay trong quá trình chạy và tích hợp lớp đó vào pipeline huấn luyện thông qua 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 sắp xếp lại thư mục, không cần tạo tệp nhãn trung gian. YOLO26 và tất cả model phát hiện YOLO khác của Ultralytics đều được hỗ trợ. Model phân đoạn và pose cần thêm các trường nhãn (xem Câu hỏi thường gặp).
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 kiến trúc#
Cần hai class:
COCODataset— đọc COCO JSON và chuyển bounding box sang định dạng YOLO trong bộ nhớ khi huấn luyệnCOCOTrainer— ghi đèbuild_dataset()để sử dụngCOCODatasetthay choYOLODatasetmặc định
Phần triển khai này là phiên bản đơn giản hóa của GroundingDataset tích hợp sẵn, vốn cũng đọc trực tiếp annotation JSON. Ở đây, ba method được ghi đè — get_img_files(), cache_labels() và get_labels() — trong khi GroundingDataset ghi đè thêm nhiều phần, bao gồm kiểm tra hash cache 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 nhãn. Thay vì đọc tệp .txt từ thư mục labels, class này mở tệp COCO JSON, duyệt các annotation được nhóm theo ảnh và chuyển 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 của YOLO [x_center, y_center, width, height]. Annotation crowd (iscrowd: 1) và bounding box có diện tích bằng 0 sẽ tự động bị bỏ qua.
Method get_img_files() trả về danh sách rỗng vì đường dẫn ảnh được phân giải từ trường JSON file_name bên trong cache_labels(). ID danh mục được sắp xếp và ánh xạ lại thành chỉ số class bắt đầu từ 0, nhờ đó cả sơ đồ ID bắt đầu từ 1 (COCO tiêu chuẩn) lẫn 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."""
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 nhãn đã phân tích được lưu vào tệp .cache bên cạnh JSON (ví dụ: instances_train.cache). Ở các lần huấn luyện tiếp theo, cache được tải trực tiếp, bỏ qua bước phân tích JSON.
get_hash() băm kích thước và đường dẫn tệp thay vì nội dung tệp, 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 ảnh cũng có thể làm thay đổi kích thước riêng của thư mục ảnh và kích hoạt quá trình dựng lại, nhưng đừng dựa vào điều đó — hash không kiểm tra từng tệp ảnh, nên việc thay ảnh này bằng ảnh 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 — dịch tọa độ một chút, đảo iscrowd, đổi chỗ hai tên class có độ dài bằng nhau — sẽ giữ cache cũ và khiến quá trình huấn luyện dùng annotation cũ mà không cảnh báo; thay ảnh tại chỗ cũng không được phát hiện vì cùng lý do. Hãy xóa tệp .cache sau khi chỉnh sửa annotation hoặc thay ảnh tại chỗ.
Kết nối Dataset với quy trình 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 tạo YOLODataset để quét các tệp nhãn .txt. Khi thay bằng COCODataset, trainer sẽ đọc dữ liệu từ COCO JSON.
Đườ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" đượ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ần có cả hai khóa — hai split đọc từ các thư mục ảnh khác nhau, nên JSON huấn luyện không thể thay thế cho val_json còn thiếu.
Dataset cũng đặt lại fraction thành 1.0. BaseDataset áp dụng đối số đó khi quét thư mục ảnh, nhưng COCODataset bỏ qua bước này, nên không thể đáp ứng yêu cầu chỉ dùng một phần dataset; việc đặt lại giá trị giúp dataset không tạo cảm giác rằng một giá trị được chấp nhận trong khi thực tế bị bỏ qua. GroundingDataset tích hợp sẵn cũng có cùng hạn chế 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 ảnh. Lưu ý rằng ở đây path trỏ đến thư mục gốc chứa ảnh, nên train và val chỉ là tên split — 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 tệp 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; số lượng class được suy ra từ đó nên 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.yamlHuấn luyện trên COCO JSON#
Sau khi đã có class dataset, class trainer và cấu hình YAML, bạn có thể huấn luyện bằng lệnh model.train() tiêu chuẩn. Điểm khác biệt duy nhất so với một lượt huấn luyện thông thường là đối số trainer=COCOTrainer, yêu cầu 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)Toàn bộ quy trình huấn luyện chạy như mong đợi, bao gồm validation trong quá trình huấn luyện, lưu checkpoint và ghi log metric.
Chỉ validation trong lúc huấn luyện mới đi qua COCOTrainer.build_dataset. Một lệnh model.val() riêng sẽ tạo YOLODataset tiêu chuẩn, vốn quét các nhãn .txt bên cạnh ảnh nhưng không tìm thấy nhãn nào. Lệnh này không báo lỗi: ảnh được tính là ảnh nền, nên validation chạy đến hết và báo mọi metric là 0, kèm cảnh báo No labels found in ... và no labels found in detect set, cannot compute metrics without labels. Để validation ngoài lượt huấn luyện, hãy tạo subclass validator với cùng phần ghi đè build_dataset và truyền subclass đó vào model.val(validator=...).
Phần triển khai đầy đủ#
Để tiện sử dụng, 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ể sao chép và chạy. Script bao gồm dataset tùy chỉnh, trainer tùy chỉnh và lệnh huấn luyện. Lưu script 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)Giờ đây, bạn đã có dataset và trainer tối giản để huấn luyện Ultralytics YOLO trực tiếp trên COCO JSON, giữ annotation làm nguồn dữ liệu chuẩn duy nhất mà không cần tạo tệp .txt trung gian. Mở rộng method cache_labels() bằng segments hoặc keypoints để hỗ trợ phân đoạn và pose; xem hướng dẫn Mẹo huấn luyện model để biết khuyến nghị tinh chỉnh hyperparameter.
Câu hỏi thường gặp#
convert_coco()ghi các tệp nhãn.txtra đĩa để chuyển đổi một lần. Cách tiếp cận này phân tích JSON ở đầu mỗi lượt huấn luyện và chuyển annotation trong bộ nhớ. Dùngconvert_coco()khi muốn lưu nhãn vĩnh viễn theo định dạng YOLO; 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 tệp.Không thể với quy trình Ultralytics hiện tại, vốn mặc định yêu cầu nhãn 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 định nghĩa hai class, chỉ cần gọimodel.train()tiêu chuẩn để huấn luyện.Hướng dẫn này đề cập đến phát hiện đối tượng. Để thêm hỗ trợ phân đoạn instance, hãy đưa dữ liệu polygon
segmentationtừ annotation COCO vào trườngsegmentscủa từng dictionary nhãn. Với ước tính pose, hãy đưakeypointsvào. Mã nguồnGroundingDatasetcung cấp phần triển khai tham khảo để xử lý segment.Có.
COCODatasetmở rộngYOLODataset, vì vậy tất cả phép tăng cường dữ liệu tích hợp sẵn — mosaic, mixup, CutMix và các 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à ánh xạ thành các chỉ số tuần tự bắt đầu từ 0. Cách này xử lý đượ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. Dictionarynamestrongdataset.yamlphải theo cùng thứ tự đã sắp xếp như mảng COCOcategories.COCO JSON được phân tích một lần trong lượt huấn luyện đầu tiên. Nhãn đã phân tích được lưu vào tệp
.cache, vì vậy các lượt chạy tiếp theo tải dữ liệu tức thì 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 dựa trên kích thước tệp JSON, vì vậy hãy xóa tệp.cachesau mọi chỉnh sửa không làm thay đổi độ dài tệp.