كيفية تدريب YOLO على COCO JSON دون تحويل#
يمكن استخدام التعليقات التوضيحية بتنسيق COCO JSON مباشرةً لتدريب Ultralytics YOLO دون تحويلها أولًا إلى ملفات .txt. يتم ذلك بإنشاء فئة فرعية من YOLODataset لتحليل COCO JSON أثناء التشغيل، وربطها بمسار التدريب عبر مدرّب مخصص.
لماذا تتدرب مباشرةً على COCO JSON#
يحافظ هذا النهج على COCO JSON بوصفه المصدر الوحيد للحقيقة، من دون استدعاء convert_coco() أو إعادة تنظيم المجلدات أو إنشاء ملفات تسميات وسيطة. وهو يدعم YOLO26 وجميع نماذج Ultralytics YOLO الأخرى لاكتشاف الأجسام. وتتطلب نماذج التجزئة وتقدير الوضع حقول تسميات إضافية (راجع الأسئلة الشائعة).
راجع دليل التحويل من COCO إلى YOLO للاطلاع على سير العمل القياسي convert_coco().
نظرة عامة على المعمارية#
نحتاج إلى فئتين:
COCODataset— يقرأ COCO JSON ويحوّل الصناديق المحيطة إلى تنسيق YOLO في الذاكرة أثناء التدريبCOCOTrainer— يتجاوزbuild_dataset()لاستخدامCOCODatasetبدلاً منYOLODatasetالافتراضي
التنفيذ نسخة مبسطة من GroundingDataset المضمّن، والذي يقرأ التعليقات التوضيحية من JSON مباشرةً أيضًا. تُتجاوز هنا ثلاث طرق — get_img_files() وcache_labels() وget_labels() — بينما يتجاوز GroundingDataset مزيدًا من الطرق، بما في ذلك عمليات التحقق الخاصة به من تجزئة ذاكرة التخزين المؤقت وعدد العناصر.
إنشاء فئة مجموعة بيانات COCO JSON#
ترث الفئة COCODataset من YOLODataset وتتجاوز منطق تحميل التسميات. وبدلاً من قراءة ملفات .txt من مجلد التسميات، تفتح ملف COCO JSON، وتكرّر التعليقات التوضيحية المجمّعة حسب الصورة، وتحول كل صندوق محيط من تنسيق بكسلات COCO [x_min, y_min, width, height] إلى تنسيق المركز المُطبّع في YOLO [x_center, y_center, width, height]. ويتم تخطي التعليقات التوضيحية الخاصة بالحشود (iscrowd: 1) والصناديق ذات المساحة الصفرية تلقائيًا.
تعيد الطريقة get_img_files() قائمة فارغة، لأن مسارات الصور تُحل من الحقل file_name في JSON داخل cache_labels(). وتُرتّب معرّفات الفئات وتُعاد مطابقتها مع مؤشرات الفئات التي تبدأ من الصفر، لذا تعمل مخططات المعرّفات التي تبدأ من 1 (معيار COCO) والمخططات ذات المعرّفات غير المتتابعة على نحو صحيح.
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"]تُحفَظ التسميات المحللة في ملف .cache بجوار JSON (مثل instances_train.cache). وفي عمليات التدريب اللاحقة، تُحمّل ذاكرة التخزين المؤقت مباشرةً، متجاوزةً تحليل JSON.
تُجزّئ get_hash() أحجام الملفات ومساراتها بدلاً من محتوياتها، لذا لا يعيد التشغيل تحليل JSON إلا عند تغيّر عدد بايتات ملف JSON. وقد تؤدي إضافة الصور أو إزالتها أيضًا إلى تغيير حجم مجلد الصور نفسه وتحفيز إعادة الإنشاء، لكن لا تعتمد على ذلك — إذ لا تفحص التجزئة ملفات الصور الفردية مطلقًا، لذا قد لا يتغير الحجم عند استبدال صورة بأخرى. ويؤدي تعديل يحافظ على عدد البايتات — مثل تحريك إحداثية، أو قلب iscrowd، أو تبديل اسمَي فئتين متساويين في الطول — إلى إبقاء ذاكرة التخزين المؤقت القديمة دون تغيير، ويجري التدريب على التعليقات التوضيحية القديمة بلا تحذير؛ كما أن استبدال صورة في مكانها لا يُكتشف للسبب نفسه. احذف ملف .cache بعد تعديل التعليقات التوضيحية أو الصور في أماكنها.
ربط مجموعة البيانات بخط أنابيب التدريب#
التغيير الوحيد المطلوب في المدرّب هو تجاوز build_dataset(). ينشئ DetectionTrainer الافتراضي كائن YOLODataset الذي يبحث عن ملفات التسميات .txt. وباستبداله بـ COCODataset، يقرأ المدرّب من COCO JSON بدلاً من ذلك.
يُستخرج مسار ملف JSON من حقل مخصص train_json / val_json في إعدادات البيانات (راجع إعداد dataset.yaml). أثناء التدريب، يُحل mode="train" إلى train_json؛ وأثناء التحقق، يُحل mode="val" إلى val_json. والمفتاحان مطلوبان — إذ يقرأ القسمان المنفصلان مجلدي صور مختلفين، لذلك لا يمكن استخدام JSON التدريب بديلاً عن val_json المفقود.
تعيد مجموعة البيانات أيضًا ضبط fraction إلى 1.0. يطبّق BaseDataset هذه الوسيطة أثناء فحص مجلد صور، وهي خطوة يتخطاها COCODataset؛ لذلك لا يمكنه تنفيذ طلب مجموعة بيانات جزئية. ويمنع إعادة ضبطها مجموعة البيانات من الظهور وكأنها تقبل قيمة تتجاهلها. ويتخذ GroundingDataset المضمّن الحل الوسط نفسه للسبب ذاته.
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,
)إعداد dataset.yaml لبيانات COCO JSON#
يستخدم dataset.yaml الحقول القياسية path وtrain وval لتحديد مواقع مجلدات الصور. لاحظ أن path يشير هنا إلى جذر الصور، لذا فإن train وval اسمان مجردان للقسمين — بخلاف دليل التحويل، حيث يكون path هو جذر مجموعة البيانات وتبدأ أسماء الأقسام بالبادئة images/. ويحدد الحقلان الإضافيان train_json وval_json ملفات التعليقات التوضيحية COCO التي يقرأها COCOTrainer. ويسرد الحقل names أسماء الفئات وفق الترتيب المرتّب لـ categories في JSON، ويُشتق عدد الفئات منه؛ لذلك لا حاجة إلى تعيين 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بنية المجلدات المتوقعة:
my_dataset/
images/
train/
img_001.jpg
...
val/
img_100.jpg
...
annotations/
instances_train.json
instances_val.json
dataset.yamlتشغيل التدريب على COCO JSON#
بعد إعداد فئة مجموعة البيانات وفئة المدرّب وتهيئة YAML، يجري التدريب باستخدام الاستدعاء القياسي model.train(). والاختلاف الوحيد عن تشغيل تدريب عادي هو وسيطة trainer=COCOTrainer، التي تخبر Ultralytics باستخدام محمّل مجموعة البيانات المخصص بدلاً من المحمّل الافتراضي.
from ultralytics import YOLO
model = YOLO("yolo26n.pt")
model.train(data="dataset.yaml", epochs=100, imgsz=640, trainer=COCOTrainer)يعمل خط أنابيب التدريب الكامل كما هو متوقع، بما في ذلك التحقق أثناء التدريب، وحفظ نقاط التحقق، وتسجيل المقاييس.
يمر التحقق أثناء التدريب فقط عبر COCOTrainer.build_dataset. أما استدعاء model.val() المنفصل فينشئ YOLODataset القياسي، الذي يبحث عن تسميات .txt بجوار الصور ولا يجد أيًا منها. ولا يُصدر خطأ: بل تُحتسب الصور على أنها خلفيات، فيكتمل التحقق وتُسجّل كل المقاييس بقيمة 0، مع التحذيرين No labels found in ... وno labels found in detect set, cannot compute metrics without labels. ولإجراء التحقق خارج تشغيل التدريب، أنشئ فئة فرعية من المدقّق باستخدام التجاوز نفسه build_dataset ومرّرها إلى model.val(validator=...).
التنفيذ الكامل#
للتيسير، يرد التنفيذ الكامل أدناه في نص برمجي واحد يمكن نسخه ولصقه. ويتضمن مجموعة البيانات المخصصة والمدرّب المخصص واستدعاء التدريب. احفظه بجوار dataset.yaml وشغّله مباشرةً.
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)أصبحت لديك الآن مجموعة بيانات ومدرّب بالحد الأدنى اللازمين لتدريب Ultralytics YOLO مباشرةً على COCO JSON، مع إبقاء التعليقات التوضيحية مصدر الحقيقة الوحيد ومن دون ملفات .txt وسيطة. وسّع الطريقة cache_labels() باستخدام segments أو keypoints لدعم التجزئة وتقدير الوضع، وراجع دليل نصائح تدريب النماذج للاطلاع على توصيات ضبط المعلمات الفائقة.
الأسئلة الشائعة#
تكتب
convert_coco()ملفات التسميات.txtعلى القرص كتحويل لمرة واحدة. أما هذا النهج فيحلل JSON عند بدء كل عملية تدريب، ويحوّل التعليقات التوضيحية في الذاكرة. استخدمconvert_coco()عندما تكون التسميات الدائمة بتنسيق YOLO هي الخيار المفضل؛ واستخدم هذا النهج للإبقاء على COCO JSON مصدر الحقيقة الوحيد من دون إنشاء ملفات إضافية.ليس ضمن خط أنابيب Ultralytics الحالي، الذي يتوقع افتراضيًا تسميات YOLO
.txt. يقدم هذا الدليل الحد الأدنى من الشيفرة المخصصة اللازمة — فئة واحدة لمجموعة البيانات وفئة واحدة للمدرّب. وبعد تعريفهما، لا يتطلب التدريب سوى استدعاءmodel.train()قياسي.يغطي هذا الدليل اكتشاف الأجسام. ولإضافة دعم تجزئة المثيلات، أدرج بيانات المضلعات
segmentationمن تعليقات COCO التوضيحية في الحقلsegmentsلكل قاموس تسميات. أما تقدير الوضع، فأدرج لهkeypoints. وتوفر الشيفرة المصدرية لـGroundingDatasetتنفيذًا مرجعيًا لمعالجة المقاطع.نعم. يوسّع
COCODatasetالفئةYOLODataset، لذا تعمل جميع عمليات زيادة البيانات المضمّنة — الفسيفساء والمزج وCutMix وغيرها — من دون تعديل.تُرتّب الفئات حسب
idوتُطابَق مع مؤشرات متتابعة تبدأ من 0. وهذا يعالج المعرّفات التي تبدأ من 1 (معيار COCO) والمعرّفات التي تبدأ من 0 والمعرّفات غير المتتابعة. وينبغي أن يتبع قاموسnamesفيdataset.yamlالترتيب المرتّب نفسه لمصفوفة COCOcategories.يُحلل COCO JSON مرة واحدة عند أول تشغيل للتدريب. وتُحفظ التسميات المحللة في ملف
.cache، لذا تُحمّل عمليات التشغيل اللاحقة فورًا من دون إعادة التحليل. وسرعة التدريب مطابقة للتدريب القياسي باستخدام YOLO، لأن التعليقات التوضيحية محفوظة في الذاكرة. وتعتمد ذاكرة التخزين المؤقت على حجم ملف JSON، لذا احذف ملف.cacheبعد أي تعديل يُبقي طول الملف كما هو.