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.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- computer-vision-yolo Installs to ~/.agents/skills/computer-vision-yolo/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install computer-vision-yolo The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project computer-vision-yolo Installs to .agents/skills/computer-vision-yolo/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/computer-vision-yolo.zipunzip -o computer-vision-yolo.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) computer-vision-yolo PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
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.
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
- 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.
- Read
AGENTS.md, the existingdata.yaml, andpip show ultralyticsfor the version. YOLO26 (January 2026, NMS-free end to end) needs a recentultralytics; YOLO11 works on older releases. - Hardware: GPU (
nvidia-smi), Applemps, 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 hfollowed by keypointsx y vper keypoint (kpt_shapein 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")
| Decision | Default | Change when |
|---|---|---|
| Model size | s | Edge CPU: n; accuracy matters and GPU available: m or l |
imgsz | 640 | Small 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 weights | COCO checkpoint | Always start from pretrained unless the domain is truly alien (and even then, try it) |
| Augmentation | Defaults | Disable fliplr=0.0 when left/right carries meaning (text, arrows) |
| Epochs | 100-300 with patience | Val 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.pngand 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.
| Symptom | Likely cause / fix |
|---|---|
| High train, low val mAP | Leakage-free split now exposes overfitting: more varied data, stronger aug, smaller model |
| One class far worse | Too few or inconsistent labels; review 50 of its labels by hand |
| Many false positives on background | Add background images (10% of set) from deployment scenes |
| Small objects missed | Larger imgsz, tiling at inference (SAHI-style), check labels exist for small instances |
| mAP fine, product feels bad | Wrong 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
| Target | Format | Command |
|---|---|---|
| CPU server, cross-platform | ONNX | yolo export model=best.pt format=onnx imgsz=640 |
| NVIDIA GPU / Jetson | TensorRT | format=engine half=True (build on the target GPU) |
| Intel CPU | OpenVINO | format=openvino (int8=True data=data.yaml to calibrate) |
| iOS / macOS | CoreML | format=coreml nms=True |
| Android / edge TPU | TFLite | format=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.
imgszat 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.yaml
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.py
#!/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.