YOLO Object Detection skill for Codex

Custom detectors that survive the edge device. Codex checks the AGPL licensing question and every label file before training, splits data by camera or video so validation is honest, and trains Ultralytics YOLO with sizes and image resolutions chosen for the deployment target. It reads per-class mAP and confusion matrices, tunes the confidence threshold on validation, and re-validates exported ONNX, TensorRT or CoreML models.

  • AI & LLM Engineering
  • $computer-vision-yolo
  • 162 lines
  • 3 files
  • ~1,700 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- computer-vision-yolo

Installs to ~/.agents/skills/computer-vision-yolo/ for every project on this machine.

104-line POSIX script. No sudo, no telemetry. Read install.sh

Use it in Codex

Start a prompt with $computer-vision-yolo, or pick it from /skills. You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.

What Codex reads before loading it

Trains, evaluates and deploys Ultralytics YOLO models (YOLO26, YOLO11) for object detection, instance segmentation, pose and classification - dataset YAML and label format checks, train/val/test splits without leakage, transfer learning, mAP50-95 and per-class analysis, confidence thresholds, and export to ONNX, TensorRT, CoreML or OpenVINO with accuracy and latency verification. Use when asked to train a custom detector, fix poor mAP, convert labels to YOLO format, run inference on images or video, or deploy a YOLO model to an edge device.

Use it when

  • You need a custom object detector, segmenter or pose model
  • Labels come from another tool and must become YOLO format
  • A trained model underperforms on certain classes or small objects
  • A model must run on Jetson, mobile, OpenVINO or CPU at a latency target

Use something else when

  • You are writing a custom PyTorch training loop for another architecture: use pytorch-training
  • You need text out of scanned documents rather than objects: use document-ai-ocr

How to check the result

The label checker exits clean, test-split mAP50-95 beats the previous model per class, and the exported model re-validated on the device matches PyTorch accuracy within a point at the target latency.

The mistake it prevents

Putting neighbouring frames of the same video in train and validation, which inflates mAP and hides real failures.

~/.agents/skills/computer-vision-yolo/SKILL.md

--- name: computer-vision-yolo description: Trains, evaluates and deploys Ultralytics YOLO models (YOLO26, YOLO11) for object detection, instance segmentation, pose and classification - dataset… license: MIT ---

Done well means: labels are verified before training, validation images come from different scenes than training images, the model beats the pretrained baseline on a held-out set with per-class numbers, and the exported model matches PyTorch accuracy on the target device at the required latency.

Check first

  1. Licensing: Ultralytics code and weights are AGPL-3.0. Closed-source or SaaS use needs an Ultralytics Enterprise license or a differently licensed model. Raise this with the user before building a product on it.
  2. Read AGENTS.md, the existing data.yaml, and pip show ultralytics for the version. YOLO26 (January 2026, NMS-free end to end) needs a recent ultralytics; YOLO11 works on older releases.
  3. Hardware: GPU (nvidia-smi), Apple mps, or CPU. Note the deployment target too (Jetson, phone, CPU server), since it decides model size and export format.

Installing ultralytics and downloading pretrained weights need network; in a Codex sandbox ask for approval or place .pt files locally first.

Dataset layout and labels

datasets/parts/
  images/{train,val,test}/*.jpg
  labels/{train,val,test}/*.txt     # same stem as the image
# data.yaml
path: datasets/parts
train: images/train
val: images/val
test: images/test
names:
  0: scratch
  1: dent
  2: missing_screw

Label lines (normalized 0-1, relative to image width/height):

  • Detection: class cx cy w h
  • Segmentation: class x1 y1 x2 y2 ... xn yn (polygon, at least 3 points)
  • Pose: class cx cy w h followed by keypoints x y v per keypoint (kpt_shape in YAML)
  • Images with no objects: keep them with an empty or missing label file (they teach “background”).

Validate before every training run (stdlib only):

python3 scripts/check_yolo_labels.py datasets/parts/data.yaml

It checks every split for missing or orphan label files, out-of-range coordinates, unknown class ids, malformed polygons, zero-area boxes and duplicate boxes, prints per-split class counts, and exits non-zero on errors.

Split without leakage

  • Split by source (video, camera, site, day), not by frame. Consecutive video frames in both train and val inflate mAP and hide failures.
  • Keep a test split that is never used for early stopping or threshold tuning.
  • Every class needs real presence in val; under about 50 instances the per-class AP is noise.

Train

yolo detect train data=datasets/parts/data.yaml model=yolo26s.pt \
  epochs=150 imgsz=640 batch=-1 patience=30 seed=0 project=runs name=parts-s
from ultralytics import YOLO

model = YOLO("yolo26s.pt")                     # or yolo11s.pt on older ultralytics
results = model.train(data="datasets/parts/data.yaml", epochs=150, imgsz=640,
                      batch=-1, patience=30, seed=0, project="runs", name="parts-s")
DecisionDefaultChange when
Model sizesEdge CPU: n; accuracy matters and GPU available: m or l
imgsz640Small objects (under about 16 px): 960-1280, and accept slower inference
batch-1 (auto, about 60% GPU memory)Multi-GPU: fixed batch with device=0,1
Pretrained weightsCOCO checkpointAlways start from pretrained unless the domain is truly alien (and even then, try it)
AugmentationDefaultsDisable fliplr=0.0 when left/right carries meaning (text, arrows)
Epochs100-300 with patienceVal mAP still rising at the end: extend

Task variants: yolo segment train ... model=yolo26s-seg.pt, yolo pose train ... model=yolo26s-pose.pt, yolo classify train data=<folder> model=yolo26s-cls.pt.

Evaluate

yolo detect val model=runs/parts-s/weights/best.pt data=datasets/parts/data.yaml split=test
  • Report mAP50-95 (primary), mAP50, precision and recall overall and per class.
  • Read confusion_matrix.png and the val prediction mosaics; look at the worst images.
  • Compare against the untuned pretrained baseline only on shared classes, and against the previous production model on the same test split.
SymptomLikely cause / fix
High train, low val mAPLeakage-free split now exposes overfitting: more varied data, stronger aug, smaller model
One class far worseToo few or inconsistent labels; review 50 of its labels by hand
Many false positives on backgroundAdd background images (10% of set) from deployment scenes
Small objects missedLarger imgsz, tiling at inference (SAHI-style), check labels exist for small instances
mAP fine, product feels badWrong operating threshold: tune conf per use case on the val set

Inference

model = YOLO("runs/parts-s/weights/best.pt")
for r in model.predict(source="line2.mp4", conf=0.35, iou=0.5, stream=True, device=0):
    boxes = r.boxes.xyxy.cpu().numpy(); cls = r.boxes.cls.cpu().numpy(); score = r.boxes.conf.cpu().numpy()

Use stream=True for video and folders to avoid holding all results in memory. Tracking: model.track(source=..., tracker="bytetrack.yaml").

Export and deploy

TargetFormatCommand
CPU server, cross-platformONNXyolo export model=best.pt format=onnx imgsz=640
NVIDIA GPU / JetsonTensorRTformat=engine half=True (build on the target GPU)
Intel CPUOpenVINOformat=openvino (int8=True data=data.yaml to calibrate)
iOS / macOSCoreMLformat=coreml nms=True
Android / edge TPUTFLiteformat=tflite int8=True data=data.yaml

After export, validate the exported file on the test split: yolo val model=best.onnx data=data.yaml. INT8 exports can lose several mAP points; accept only with the measured number. Benchmark latency on the actual device with the real input size and batch 1, including pre- and post-processing.

Gotchas

  • Class ids in labels must match the order in names; merging datasets with different orders silently mislabels everything.
  • Ultralytics caches labels (*.cache); after editing labels delete the cache files.
  • EXIF rotation: phone images may display rotated relative to labels; check a few overlays.
  • imgsz at inference should match training; mismatches shift accuracy noticeably.
  • Faces, license plates and people in images are personal data; confirm consent and retention rules before collecting or uploading datasets, and before any hosted training.

Report template

**Model:** yolo26s, ultralytics x.y, imgsz 640, 150 epochs (best at 118) | **License:** AGPL noted to user
**Data:** 4,820 train / 960 val / 1,010 test images, split by production line; checker clean
**Test:** mAP50-95 0.612, mAP50 0.874 | scratch 0.66, dent 0.58, missing_screw 0.60
**Operating point:** conf 0.35 -> precision 0.91, recall 0.86 (val-tuned)
**Export:** TensorRT FP16 on Jetson Orin NX: mAP50-95 0.609, 14 ms/frame end to end

What the zip contains

Codex reads bundled files only when a step points to them. Every one is shown here, so you can review it before installing.

agents/openai.yamlgenerated from this page

Gives the Codex app a display name, a one-line description and a starter prompt for this skill.

interface:
  display_name: "YOLO Object Detection"
  short_description: "Custom detectors that survive the edge device"
  default_prompt: "$computer-vision-yolo Train a YOLO model to detect scratches and dents from our labelled factory photos"
scripts/check_yolo_labels.py150 lines, 6.0 KB
#!/usr/bin/env python3
"""Check a YOLO-format dataset (detection, segmentation or pose labels) before training.

Usage:
    python3 check_yolo_labels.py path/to/data.yaml

Reads train/val/test image dirs from data.yaml (simple key: value YAML, names as a
mapping or inline list), finds the matching labels/ dirs, and reports errors. Exits 1 on errors.
"""
import ast
import os
import sys
from collections import Counter

IMG_EXT = {".jpg", ".jpeg", ".png", ".bmp", ".webp", ".tif", ".tiff"}


def parse_yaml(path):
    data, names, in_names = {}, {}, False
    with open(path, encoding="utf-8") as fh:
        for raw in fh:
            line = raw.split("#", 1)[0].rstrip()
            if not line.strip():
                continue
            indented = line[0] in " \t"
            key, _, val = line.strip().partition(":")
            val = val.strip().strip("'\"")
            if in_names and indented:
                names[int(key)] = val
                continue
            in_names = False
            if key == "names":
                if val.startswith("["):
                    items = [x.strip().strip("'\"") for x in val.strip("[]").split(",")]
                    names = dict(enumerate(x for x in items if x))
                else:
                    in_names = True
            elif not indented:
                data[key] = val
    data["names"] = names
    return data


def label_dir_for(img_dir):
    parts = img_dir.split(os.sep)
    if "images" in parts:
        idx = len(parts) - 1 - parts[::-1].index("images")
        parts[idx] = "labels"
        return os.sep.join(parts)
    return os.path.join(os.path.dirname(img_dir), "labels", os.path.basename(img_dir))


def check_split(split, img_dir, n_classes, kpt, errors, warnings):
    lbl_dir = label_dir_for(img_dir)
    if not os.path.isdir(img_dir):
        errors.append(f"{split}: image dir not found: {img_dir}")
        return Counter(), 0, 0
    images = {os.path.splitext(f)[0] for f in os.listdir(img_dir) if os.path.splitext(f)[1].lower() in IMG_EXT}
    labels = set()
    if os.path.isdir(lbl_dir):
        labels = {os.path.splitext(f)[0] for f in os.listdir(lbl_dir) if f.endswith(".txt")}
    for orphan in sorted(labels - images)[:20]:
        errors.append(f"{split}: label without image: {orphan}.txt")
    counts, background = Counter(), 0
    for stem in sorted(images):
        path = os.path.join(lbl_dir, stem + ".txt")
        if not os.path.exists(path):
            background += 1
            continue
        seen = set()
        with open(path, encoding="utf-8") as fh:
            rows = [r.split() for r in fh if r.strip()]
        if not rows:
            background += 1
        for i, parts in enumerate(rows, start=1):
            where = f"{split}/{stem}.txt:{i}"
            try:
                cls = int(parts[0])
                vals = [float(v) for v in parts[1:]]
            except (ValueError, IndexError):
                errors.append(f"{where}: non-numeric values")
                continue
            if not 0 <= cls < n_classes:
                errors.append(f"{where}: class {cls} not in names (0..{n_classes - 1})")
            if len(vals) < 4:
                errors.append(f"{where}: expected at least 4 coordinates, got {len(vals)}")
                continue
            coords = vals
            if kpt and kpt[1] == 3:
                vis = vals[6::3]
                coords = [v for j, v in enumerate(vals) if j < 4 or (j - 4) % 3 != 2]
                if any(v not in (0.0, 1.0, 2.0) for v in vis):
                    errors.append(f"{where}: keypoint visibility must be 0, 1 or 2")
            if any(v < 0 or v > 1.0001 for v in coords):
                errors.append(f"{where}: coordinates outside 0-1 (not normalized?)")
            if kpt:
                if len(vals) != 4 + kpt[0] * kpt[1]:
                    errors.append(f"{where}: expected {4 + kpt[0] * kpt[1]} values for kpt_shape {kpt}, got {len(vals)}")
            elif len(vals) == 4:
                if vals[2] <= 0 or vals[3] <= 0:
                    errors.append(f"{where}: zero or negative box size")
            elif len(vals) % 2 or len(vals) < 6:
                errors.append(f"{where}: polygon needs an even count of at least 6 values, got {len(vals)}")
            key = (cls, tuple(round(v, 4) for v in vals))
            if key in seen:
                warnings.append(f"{where}: duplicate annotation")
            seen.add(key)
            counts[cls] += 1
    return counts, len(images), background


def main():
    if len(sys.argv) != 2:
        print(__doc__)
        return 2
    yaml_path = sys.argv[1]
    cfg = parse_yaml(yaml_path)
    names = cfg["names"]
    if not names:
        print("error: no class names found in data.yaml")
        return 1
    kpt = ast.literal_eval(cfg["kpt_shape"]) if cfg.get("kpt_shape") else None
    root = cfg.get("path", "")
    if not os.path.isabs(root):
        root = os.path.join(os.path.dirname(os.path.abspath(yaml_path)), root) if root else os.path.dirname(os.path.abspath(yaml_path))
        if not os.path.isdir(root) and cfg.get("path"):
            root = os.path.abspath(cfg["path"])
    errors, warnings = [], []
    for split in ("train", "val", "test"):
        if not cfg.get(split):
            if split != "test":
                errors.append(f"data.yaml has no '{split}' entry")
            continue
        img_dir = os.path.normpath(os.path.join(root, cfg[split]))
        counts, n_img, bg = check_split(split, img_dir, len(names), kpt, errors, warnings)
        dist = ", ".join(f"{names.get(c, c)}={counts.get(c, 0)}" for c in sorted(names))
        print(f"{split}: {n_img} images, {bg} background, instances: {dist}")
        for c in sorted(names):
            if split != "train" and n_img and counts.get(c, 0) < 50:
                warnings.append(f"{split}: class '{names[c]}' has only {counts.get(c, 0)} instances")
    for w in warnings[:40]:
        print(f"warn: {w}")
    for e in errors[:100]:
        print(f"error: {e}")
    print(f"\n{len(errors)} error(s), {len(warnings)} warning(s)")
    return 1 if errors else 0


if __name__ == "__main__":
    sys.exit(main())

About this skill

Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.

esc
Just launched

Codex Skills Pro is live

79 premium skills for Codex, each with scripts, references and an evidence report that proves the result.

Bought one by one$882.21
All 79 skills$49
94% off

That's $0.62 a skill, paid once.

Get all 79 for $49Or pick single skills from $4.99 →

Secure checkout by Creem