Ultralytics YOLO27: انضم إلى قائمة الانتظار.
ابدأ

كيفية تدريب 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.

مفتاح التخزين المؤقت هو بيانات الملف الوصفية، وليس محتواه

يحسب get_hash() تجزئة كل مسار باستخدام حجم الملف ووقت تعديله بدلًا من محتواه، لذا تؤدي تعديلات JSON إلى إعادة تحليلها في التشغيل التالي. يُحتسب دليل الصور كمجلد فقط: تؤدي إضافة صورة أو إزالتها مباشرةً داخله إلى إعادة إنشاء الفهرس، لكن استبدال صورة في مكانها، أو إضافة صورة أو إزالتها في مجلد فرعي (مثل file_name كـ sub/img.jpg)، لا يؤدي إلى ذلك. احذف الملف .cache بعد إجراء هذه التغييرات، أو إذا أعادت أداة كتابة JSON واستعادت وقت تعديله الأصلي.

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

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

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

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

التعليقات

اطرح سؤالًا، وشارك نتائجك، أو ساعد شخصًا آخر.