Meet YOLO26: next-gen vision AI.

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

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

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

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

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

راجع دليل التحويل من COCO إلى YOLO للتعرف على سير العمل القياسي convert_coco().

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

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

  1. COCODataset — تقرأ COCO JSON وتحول صناديق التحديد إلى تنسيق 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() قائمة فارغة لأن مسارات الصور يتم حلها من حقل 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):
        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)

        images = {img["id"]: img for img in coco["images"]}

        # 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 مخصص في تكوين البيانات (انظر الخطوة 3). أثناء التدريب، يتم حل 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):
        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/images # root directory with train/ and val/ subfolders
train: train
val: val

# COCO JSON annotation files
train_json: /path/to/annotations/instances_train.json
val_json: /path/to/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)

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

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

للملاءمة، يتم توفير التنفيذ الكامل أدناه كبرنامج نصي واحد يمكن نسخه ولصقه. يتضمن مجموعة البيانات المخصصة، والمدرب المخصص، واستدعاء التدريب. احفظ هذا بجوار 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):
        self.json_file = json_file
        super().__init__(*args, data={"channels": 3}, **kwargs)

    def get_img_files(self, img_path):
        return []

    def cache_labels(self, path=Path("./labels.cache")):
        x = {"labels": []}
        with open(self.json_file) as f:
            coco = json.load(f)

        images = {img["id"]: img for img in coco["images"]}
        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):
        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):
        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)

للحصول على توصيات المعاملات الفائقة، راجع دليل نصائح تدريب النموذج.

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

ما الفرق بين هذا و convert_coco()؟

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

هل يمكن لـ YOLO التدريب على COCO JSON بدون كود مخصص؟

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

هل يدعم هذا التجزئة وتقدير الوضع؟

This guide covers object detection. To add instance segmentation support, include the segmentation polygon data from COCO annotations in the segments field of each label dictionary. For pose estimation, include keypoints. The GroundingDataset source code provides a reference implementation for handling segments.

هل تعمل عمليات التعزيز مع مجموعة البيانات المخصصة هذه؟

نعم. توسع COCODataset من YOLODataset، لذا فإن جميع تعزيزات البيانات المدمجة — mosaic و mixup و copy-paste وغيرها — تعمل دون تعديل.

كيف يتم تعيين معرفات الفئات إلى فهارس الفئات؟

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

هل هناك عبء أداء مقارنة بالتسميات المحولة مسبقاً؟

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

التعليقات