Training on Your Own Data
Labels, formats, and YOLO.
The biggest difference between a demo and a product is your own data. No public dataset contains Orchard Vision's apples, on its own conveyor, in its own lighting. So you'll need your own labelled images, and the good news is that you don't need very many to start.

- No dataset has your apples
- Your conveyor, your lighting
- You don't need many to start
The label format
YOLO's labels are simple text files, with one line per object. The class, then the centre, width, and height, all as fractions of the image size. That means they don't depend on resolution. We turn one label back into pixels. Run it. That's the whole format. Tools such as Label Studio and Roboflow let you draw these boxes, and export this format.
label = "0 0.512 0.430 0.210 0.300"
image_w, image_h = 640, 480
cls, cx, cy, w, h = label.split()
cx, cy, w, h = float(cx), float(cy), float(w), float(h)
x1 = (cx - w / 2) * image_w
y1 = (cy - h / 2) * image_h
x2 = (cx + w / 2) * image_w
y2 = (cy + h / 2) * image_h
print("Class:", cls)
print("Pixels:", round(x1), round(y1), round(x2), round(y2))Class: 0 Pixels: 260 134 395 278
Training a custom detector
Here's a complete training run. To keep it self contained, we draw fifty fake images, with round red apples, and write the labels. A small yaml file tells YOLO where everything is. Then one line trains it. Run it. We start from pretrained weights, for six quick epochs, on a small dataset. And the score climbs high. It's a simple task, but the very same code trains on your real apple photos, and that's what makes this so useful.
import cv2
import numpy as np
from pathlib import Path
from ultralytics import YOLO
rng = np.random.default_rng(0)
root = Path("apples")
for split, n in [("train", 40), ("val", 10)]:
(root / "images" / split).mkdir(parents=True)
(root / "labels" / split).mkdir(parents=True)
for i in range(n):
img = np.full((320, 320, 3), 40, np.uint8) + rng.integers(0, 30, (320, 320, 3), dtype=np.uint8)
rows = []
for _ in range(rng.integers(1, 4)):
r = int(rng.integers(20, 45)); x = int(rng.integers(r, 320 - r)); y = int(rng.integers(r, 320 - r))
cv2.circle(img, (x, y), r, (40, 40, 220), -1)
rows.append(f"0 {x / 320} {y / 320} {2 * r / 320} {2 * r / 320}")
cv2.imwrite(str(root / "images" / split / f"{i}.jpg"), img)
(root / "labels" / split / f"{i}.txt").write_text("\n".join(rows))
(root / "data.yaml").write_text(f"path: {root.resolve()}\ntrain: images/train\nval: images/val\nnames:\n 0: apple\n")
model = YOLO("yolo11n.pt")
out = model.train(data=str(root / "data.yaml"), epochs=6, imgsz=320, batch=8, device="cpu",
workers=0, verbose=False, plots=False, project="runs", name="a")
print("mAP at 0.5:", round(float(out.box.map50), 3))
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
[K 3/6 0G 0.6657 0.6343 0.9528 18 320: 0% ──────────── 0/5 0.4s
[K 3/6 0G 0.5727 0.6996 0.8943 13 320: 20% ━━────────── 1/5 1.4s/it 0.8s<5.5s
all 10 21 0.975 1 0.995 0.931
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
[K 4/6 0G 0.5838 0.5662 1.046 17 320: 0% ──────────── 0/5 0.4s
[K 4/6 0G 0.6874 0.5662 0.9537 16 320: 20% ━━────────── 1/5 1.4s/it 0.9s<5.7s
all 10 21 1 0.797 0.945 0.808
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
[K 5/6 0G 0.6968 0.7312 0.9707 17 320: 0% ──────────── 0/5 0.4s
[K 5/6 0G 0.8654 0.7376 0.9385 17 320: 20% ━━────────── 1/5 1.4s/it 0.9s<5.8s
all 10 21 0.995 1 0.995 0.88
Epoch GPU_mem box_loss cls_loss dfl_loss Instances Size
[K 6/6 0G 0.7784 0.5927 0.9454 15 320: 0% ──────────── 0/5 0.5s
[K 6/6 0G 0.6586 0.5812 0.891 16 320: 20% ━━────────── 1/5 1.4s/it 0.9s<5.8s
all 10 21 0.995 1 0.995 0.88
6 epochs completed in 0.004 hours.
Optimizer stripped from /privateruns/detect/runs/a/weights/last.pt, 5.5MB
Optimizer stripped from /privateruns/detect/runs/a/weights/best.pt, 5.5MB
Validating /privateruns/detect/runs/a/weights/best.pt...
Ultralytics 8.4.197 🚀 Python-3.13.5 torch-2.15.0 CPU (Apple M3 Pro)
YOLO11n summary (fused): 101 layers, 2,597,051 parameters, 0 gradients, 6.4 GFLOPs
all 10 21 0.974 1 0.995 0.931
Speed: 0.1ms preprocess, 12.6ms inference, 0.0ms loss, 0.2ms postprocess per image
mAP at 0.5: 0.995Tips for real projects
Collect images from the real camera, in the real place, across different lights and days. Start with a hundred or two hundred images per class. Label consistently, and write down the rules. Keep a test set from a different day. Use image sizes large enough for your smallest object. And after a first run, look at the failures, and collect more of those. That's how models really improve.
Summary
Your own data beats everything else. AppleEye can now find apples. But for grading, a box isn't enough. We need the exact shape of each bruise. Next, labelling each pixel.
Key takeaways- Labels are normalised text
- One line trains the model
- Failures tell you what to collect