رؤية YOLO لعام 2026:

كيفية تدريب YOLO على COCO JSON دون تحويل#

يمكن استخدام Annotations بتنسيق COCO JSON مباشرة للتدريب في Ultralytics YOLO دون الحاجة إلى التحويل إلى ملفات .txt أولاً. يعمل هذا من خلال إنشاء فئة فرعية من YOLODataset لتحليل ملفات COCO JSON أثناء التنقل وربطها بخط أنابيب التدريب من خلال مدرب مخصص.

لماذا يتم التدريب مباشرة على COCO JSON#

يحافظ هذا النهج على ملف COCO JSON كمصدر واحد موثوق للحقيقة — بدون استدعاء convert_coco()، وبدون إعادة تنظيم المجلدات، وبدون ملفات تصنيف وسيطة. يتم دعم YOLO26 وجميع نماذج الكشف الأخرى من Ultralytics YOLO. تتطلب نماذج التقسيم وتقدير الوضعيات حقول تصنيف إضافية (راجع FAQ).

هل تبحث عن تحويل لمرة واحدة بدلاً من ذلك؟

راجع COCO to YOLO Conversion guide لمعرفة سير العمل القياسي لـ convert_coco().

نظرة عامة على المعمارية#

هناك حاجة إلى فئتين:

  1. COCODataset — تقرأ ملفات COCO JSON وتحول bounding boxes إلى تنسيق YOLO في الذاكرة أثناء التدريب
  2. COCOTrainer — تتجاوز build_dataset() لاستخدام COCODataset بدلاً من YOLODataset الافتراضي

يتبع التنفيذ نفس النمط الخاص بالفئة المدمجة GroundingDataset، والتي تقرأ أيضاً تعليقات JSON مباشرة. يتم تجاوز ثلاث طرق: get_img_files() وcache_labels() وget_labels().

بناء فئة مجموعة بيانات 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() قائمة فارغة نظراً لأنه يتم حل مسارات الصور من حقل JSON file_name داخل 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."""
        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"]

يتم حفظ التسميات التي تم تحليلها في ملف .cache بجوار ملف JSON (على سبيل المثال instances_train.cache). في عمليات التدريب اللاحقة، يتم تحميل ذاكرة التخزين المؤقت مباشرة، مما يتخطى عملية تحليل JSON. إذا تغير ملف JSON، يفشل فحص التجزئة ويتم إعادة إنشاء ذاكرة التخزين المؤقت تلقائياً.

ربط مجموعة البيانات بخط أنابيب التدريب#

التغيير الوحيد المطلوب في المدرب هو تجاوز build_dataset(). يقوم DetectionTrainer الافتراضي بنشاط إنشاء YOLODataset الذي يبحث عن ملفات تسميات .txt. من خلال استبداله بـ COCODataset، يقرأ المدرب من ملف COCO JSON بدلاً من ذلك.

يتم سحب مسار ملف JSON من حقل مخصص train_json / val_json في تكوين البيانات (راجع Configuring dataset.yaml). أثناء التدريب، يتم حل mode="train" إلى train_json؛ وأثناء التحقق، يتم حل mode="val" إلى val_json. إذا لم يتم تعيين val_json، فإنه يعود افتراضياً إلى 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,
        )

تهيئة dataset.yaml لـ COCO JSON#

تستخدم dataset.yaml حقول path وtrain وval القياسية لتحديد مواقع مجلدات الصور. يحدد حقلان إضافيان، train_json وval_json، ملفات تعليقات COCO التوضيحية التي تقرأها COCOTrainer. ويقوم حقلا nc وnames بتعريف عدد الفئات وأسمائها، مطابقين للترتيب المفروز لـ categories في ملف 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 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)

يعمل خط أنابيب training بالكامل كما هو متوقع، بما في ذلك validation وحفظ نقاط التحقق وتسجيل المقاييس.

التنفيذ الكامل#

للتسهيل، يتم توفير التنفيذ الكامل أدناه كبرنامج نصي واحد قابل للنسخ واللصق. يتضمن مجموعة البيانات المخصصة، والمدرب المخصص، واستدعاء التدريب. احفظ هذا بجوار 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."""
        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)

لديك الآن مجموعة بيانات ومدرب مصغران يقومان بتدريب Ultralytics YOLO مباشرة على COCO JSON، مع بقاء التعليقات التوضيحية المصدر الوحيد للحقيقة ودون وجود ملفات .txt وسيطة. قم بتوسيع طريقة cache_labels() باستخدام segments أو keypoints لتغطية التقسيم وتقدير الوضعيات، وراجع دليل Model Training Tips للحصول على توصيات ضبط hyperparameter.

الأسئلة الشائعة#

  • تقوم convert_coco() بكتابة ملفات تسميات .txt على القرص كعملية تحويل لمرة واحدة. يقوم هذا النهج بتحليل ملف JSON في بداية كل تشغيل للتدريب ويقوم بتحويل التعليقات التوضيحية في الذاكرة. استخدم convert_coco() عندما تكون التسميات الدائمة بتنسيق YOLO مفضلة؛ واستخدم هذا النهج للحفاظ على ملف COCO JSON كمصدر واحد للحقيقة دون إنتاج ملفات إضافية.

  • ليس مع خط أنابيب Ultralytics الحالي، والذي يتوقع تسميات .txt الخاصة بـ YOLO افتراضياً. يوفر هذا الدليل الحد الأدنى من الكود المخصص المطلوبة — فئة مجموعة بيانات واحدة وفئة مدرب واحدة. بمجرد تحديدها، لا يتطلب التدريب سوى استدعاء قياسي لـ model.train().

  • يغطي هذا الدليل object detection. لإضافة دعم instance segmentation، قم تضمين بيانات مضلع segmentation من تعليقات COCO التوضيحية في حقل segments لكل قاموس تسميات. بالنسبة إلى pose estimation، قم تضمين keypoints. يوفر source code الخاص بـ GroundingDataset تنفيذاً مرجعياً للتعامل مع الأجزاء.

  • نعم. توسع COCODataset النطاق لتشمل YOLODataset، لذلك تعمل جميع data augmentations المدمجة — مثل mosaic وmixup وcopy-paste وغيرها — بدون تعديل.

  • يتم فرز الفئات بواسطة id ومطابقتها مع مؤشرات متسلسلة تبدأ من 0. يتعامل هذا مع المعرفات التي تبدأ بـ 1 (COCO القياسي)، والمعرفات التي تبدأ بـ 0، والمعرفات غير المتصلة. يجب أن يتبع قاموس names في dataset.yaml نفس الترتيب المفروز لصفوف categories في COCO.

  • يتم تحليل ملف COCO JSON مرة واحدة في تشغيل التدريب الأول. يتم حفظ التسميات التي تم تحليلها في ملف .cache، بحيث يتم تحميل العمليات اللاحقة فوراً دون إعادة التحليل. سرعة التدريب مطابقة لتدريب YOLO القياسي نظراً لأنه يتم الاحتفاظ بالتعليقات التوضيحية في الذاكرة. يتم إعادة بناء ذاكرة التخزين المؤقت تلقائياً إذا تغير ملف JSON.

التعليقات