Skip to main content

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

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

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

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

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

راجع دليل تحويل COCO إلى YOLO لمعيار 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() تُرجع الطريقة قائمة فارغة لأن مسارات الصور يتم حلها من حقل 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):
        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() قياسياً فقط.

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

يغطي هذا الدليل اكتشاف الكائنات. لإضافة دعم تجزئة النماذج (instance segmentation)، قم بتضمين بيانات المضلع segmentation من تعليقات COCO في حقل segments لكل قاموس تسميات. لـ تقدير الوضع (pose estimation)، قم بتضمين keypoints.GroundingDataset الكود المصدري يوفر تنفيذاً مرجعياً للتعامل مع الأجزاء.

هل تعمل التوسعات (augmentations) مع مجموعة البيانات المخصصة هذه؟

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

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

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

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

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

التعليقات