YOLO Vision 2026:

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

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

لماذا ندرّب مباشرةً على COCO JSON#

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

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

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

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

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

  1. COCODataset — يقرأ COCO JSON ويحوّل المربعات المحيطة إلى تنسيق YOLO في الذاكرة أثناء التدريب
  2. 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.

مفتاح ذاكرة التخزين المؤقت هو حجم ملف 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)

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

تحتاج `model.val()` المستقلة إلى تجاوز خاص بها

يمر التحقق أثناء التدريب فقط عبر 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، ولذلك تعمل جميع عمليات زيادة البيانات المضمّنة — الفسيفساء وmixup والنسخ واللصق وغيرها — دون تعديل.

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

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

التعليقات