AI Frontier Post
Tutorials

Detect anything in your photos with YOLO26, locally and offline: the complete hands-on guide

Your photos already contain the answers — what is in them, and where. This guide turns a laptop into a vision system: Ultralytics' newest detector, YOLO26, running entirely on your machine, with every output quoted from real CPU runs and a folder-to-spreadsheet workflow at the end.

TL;DR: YOLO26, Ultralytics' newest detector, runs on an ordinary CPU with no account and no API key. Verified here: ultralytics 8.4.192 + PyTorch 2.9.1+cpu, nano weights 5.3 MB, demo bus photo detected as 4 persons + 1 bus in 192.3 ms of inference. This guide installs it, reads detections as data in Python, tunes confidence and the NMS-free head on the same image, and batches a folder of photos into a CSV.
  • Who: YOLO26 (Ultralytics, January 2026), the nano model yolo26n.pt — 2,408,932 parameters, 5.5 GFLOPs, 40.9 COCO mAP per the docs.
  • What: detect.py (single image to labels, confidences, boxes, JSON) and batch_detect.py (a folder to one CSV row per detection), plus confidence and NMS-free tuning.
  • When: verified October 29, 2026: ultralytics 8.4.192, torch 2.9.1+cpu, Python 3.12.3, CPU-only, no GPU.
  • Requirement: Linux or macOS (WSL on Windows), Python 3.11+, about 1.5 GB of disk. No account, no API key, no GPU. Note the AGPL-3.0 licence before building a closed commercial product on it.
A city street scene with glowing bounding boxes around a bus, pedestrians, a bicycle and a dog
Illustration generated by AI Frontier Post.

Why this matters now

Most of the AI you read about here lives behind an API: you send data out, you get text back, you pay per call. Object detection is the opposite kind of problem. The question is usually simple — what is in this photo, and where? — and the photo is often private, numerous, or both: product shots, security-camera stills, wildlife-trap images, warehouse shelves. Sending thousands of those to a hosted vision API is slow, expensive, and, for many of them, a non-starter on privacy grounds.

That is why the newest YOLO release is worth your afternoon. YOLO26, released by Ultralytics in January 2026, is the current end of the longest-running real-time detection lineage in computer vision. Its headline change is an optional end-to-end detection head that needs no non-maximum suppression (NMS) — the post-processing step that has, for a decade, deduplicated overlapping boxes. The docs report 40.9–57.5 mAP on COCO across its five detection scales, and up to 43% faster CPU ONNX inference for the nano model versus YOLO11n. You do not need to take those numbers on faith: this guide installs the real package, runs the real model on a real photo on an ordinary CPU, and quotes every output.

By the end you will have a working detector you can point at a folder of images and get back a CSV of everything it found — no account, no API key, no GPU, and no image leaving your machine.

What you'll need

  • Python 3.11 or newer. Verified here on Python 3.12.3 on Linux. macOS works the same way; on Windows, use WSL.
  • About 1.5 GB of disk. The ultralytics package pulls in PyTorch, and the nano model weights are 5.3 MB. The verification environment's full install measured 1.4 GB.
  • No GPU, no account, no API key. Everything below runs on CPU and downloads only open weights.
  • One licence note, read before you ship anything. Ultralytics code and models are under AGPL-3.0, with a commercial Enterprise licence available. Personal projects, internal tools, and open-source work are fine; if you build a closed commercial product on it, read the licence or buy the Enterprise licence first. This guide is for learning and internal use.

Step 1 — Install YOLO26 in a virtual environment

Never install a package this large into your system Python. Make a folder, a virtual environment, and install into that:

mkdir yolo26-demo && cd yolo26-demo
python3 -m venv .venv
source .venv/bin/activate
pip install ultralytics

On this machine that installed ultralytics 8.4.192 with PyTorch 2.9.1+cpu. This verification deliberately used PyTorch's CPU-only build, because the default bundle — a 539 MB wheel plus its CUDA libraries — kept stalling on this machine's proxy; on a normal connection, plain pip install ultralytics fetches the standard build, which also runs on CPU. If your own download stalls, re-run the command with pip install --resume-retries 10 ultralytics, or take the smaller CPU-only build from PyTorch's CPU wheel index. Confirm the install:

yolo version
python3 -c "import ultralytics; print(ultralytics.__version__)"

Expected output, with your versions in place of these:

8.4.192
8.4.192

If yolo version fails with "command not found", your virtual environment is not activated — the source .venv/bin/activate line is the step people skip.

Step 2 — Run your first detection from the command line

Ultralytics ships a demo image, and the CLI downloads the nano weights on first use. One command detects everything in it:

yolo predict model=yolo26n.pt source='https://ultralytics.com/images/bus.jpg'

The first run downloads yolo26n.pt (about 5 MB) and prints a one-line summary per image, then saves the annotated image under runs/detect/predict/. On this CPU-only machine the run reported:

YOLO26n summary (fused): 120 layers, 2,408,932 parameters, 0 gradients, 5.5 GFLOPs
image 1/1 bus.jpg: 640x480 4 persons, 1 bus, 192.3ms
Speed: 6.2ms preprocess, 192.3ms inference, 73.7ms postprocess per image at shape (1, 3, 640, 480)
Results saved to runs/detect/predict

Read that line right to left: the image was run at the 640x480 inference shape, the model found 4 persons and 1 bus, and the annotated copy — the same photo with a box around each detection — is in runs/detect/predict/. Open it. That annotated image is the whole product: a label, a box, and a confidence for every object the model is sure enough about.

Two defaults are doing quiet work here, and you should know them before you trust any output. The confidence threshold defaults to 0.25: anything the model is less than 25% sure about is dropped silently. And the model knows exactly 80 classes — the COCO set (person, bicycle, car, bus, dog, and 75 others). It cannot label anything outside that set. A forklift, a defect on a circuit board, a species of bird: all of those are "not one of the 80" until you train or switch models, and the model will either miss them or force them into the nearest familiar class. That is the single most common source of "the detector is wrong" complaints, and it is a vocabulary problem, not a vision problem.

A four-step pipeline: a photo, a neural network, bounding boxes, and a results table
Illustration generated by AI Frontier Post.

Step 3 — Do it in Python and get the numbers, not just the picture

The CLI is for a first look. Real work happens through the Python API, where every detection is data you can filter, count, and save. Save this as detect.py:

import json, sys, time
from pathlib import Path
from ultralytics import YOLO

src = sys.argv[1]
out = Path(sys.argv[2]) if len(sys.argv) > 2 else Path("annotated.jpg")
model = YOLO("yolo26n.pt")
t0 = time.time()
results = model(src, conf=0.25, verbose=False)
dt = time.time() - t0
r = results[0]
rows = []
for box in r.boxes:
    rows.append({
        "label": r.names[int(box.cls)],
        "confidence": round(float(box.conf), 3),
        "xyxy": [round(float(v), 1) for v in box.xyxy[0]],
    })
rows.sort(key=lambda d: -d["confidence"])
print(f"image: {src}  size: {r.orig_shape[1]}x{r.orig_shape[0]}"
      f"  detections: {len(rows)}  wall: {dt:.2f}s")
for d in rows:
    print(f"  {d['label']:<12} {d['confidence']:.3f}  {d['xyxy']}")
r.save(str(out))
Path("detections.json").write_text(json.dumps(rows, indent=2))

Run it on the same demo image:

python3 detect.py bus.jpg annotated.jpg

On the verification machine this printed:

image: bus.jpg  size: 810x1080  detections: 5  wall: 2.01s
  bus          0.881  [0.0, 230.5, 803.2, 750.7]
  person       0.872  [48.6, 397.7, 240.3, 902.4]
  person       0.861  [222.9, 404.8, 345.5, 861.0]
  person       0.846  [669.6, 394.5, 809.9, 879.4]
  person       0.656  [0.2, 553.0, 59.7, 871.3]

Three things in that output are worth understanding, because they are the entire API surface you will use from here on:

  • r.boxes is the list of detections. Each box carries cls (a class index), conf (a 0–1 confidence), and xyxy (the box corners in pixels: left, top, right, bottom).
  • r.names maps class indices to names — r.names[0] is person, r.names[5] is bus. Never hard-code your own class list; the model ships its own.
  • r.save() writes the annotated image, and detections.json holds the same detections as data. The picture is for your eyes; the JSON is for everything else you build.

The wall-clock time includes model loading on the first call in the process, and it varies a lot between runs (later runs here were much faster once the libraries were warm in memory). The stable number is the CLI's per-image inference time: 192.3 ms on this CPU. The docs' 38.9 ms figure for YOLO26n is for the exported ONNX model, not the PyTorch path used here — exporting to ONNX is how you close that gap. Measure on your own machine before promising anyone a frame rate.

Two panels comparing many overlapping boxes at a low threshold with a few clean boxes at a high threshold
Illustration generated by AI Frontier Post.

Step 4 — Tune the two knobs that actually matter: confidence and NMS

Almost every disappointing result from a detector comes down to one of two settings. Try both on the same image before you change anything else.

Confidence (conf). Raise it and weak detections vanish; lower it and more appear, including mistakes. Compare the same image at 0.25 and at 0.70:

python3 detect.py bus.jpg annotated-conf25.jpg   # conf=0.25 in the script
# then edit conf=0.25 to conf=0.70 in detect.py and re-run:
python3 detect.py bus.jpg annotated-conf70.jpg

On the demo image, raising the threshold from 0.25 to 0.70 changed the count from 5 detections to 4. The one that disappeared was the weakest detection in the set: a partially visible person at the left edge of the frame, scored 0.656. That is exactly the kind of detection a threshold is for — a real person, but one the model itself is unsure about. An instructive detail: raising the threshold to 0.50 first changed nothing at all, because every detection here scores above 0.50. A threshold only bites when it passes an actual score in your data, which is why you should always check the scores before picking a number. The rule of thumb: start at 0.25 while exploring, raise to 0.5 or higher when a false positive costs you something (an alert, a rejected item, a wrong count), and never go below 0.25 in production without a reason you can state out loud.

NMS, YOLO26's headline feature. YOLO26 ships two heads: the default one-to-many head, which needs NMS to merge overlapping boxes, and an optional one-to-one head that produces end-to-end predictions with nms=False. Switch it on in one argument:

results = model(src, conf=0.25, nms=False, verbose=False)

On the demo image the NMS-free head returned the same 5 detections as the default head, but with slightly higher confidences across the board: bus 0.924 instead of 0.881, the three clear persons 0.913/0.905/0.870 instead of 0.872/0.861/0.846, and the weak edge person 0.534 instead of 0.656. The practical difference is not the count on one photo — it is deployment: with no NMS pass to tune or port, the model exports more cleanly to ONNX and edge runtimes, and its latency stops varying with how crowded the scene is. The docs are honest about the trade: the end-to-end head scores slightly lower on COCO (40.1 vs 40.9 mAP for the nano model). Use the default head when accuracy is all that matters; use nms=False when you are shipping to a device and want one less moving part.

Step 5 — Point it at a folder and get a spreadsheet back

One photo is a demo. A folder is a tool. Save this as batch_detect.py — it runs the model over every image in a folder and writes one CSV row per detection, plus a count per class:

import csv, sys
from collections import Counter
from pathlib import Path
from ultralytics import YOLO

folder, csv_path = Path(sys.argv[1]), Path(sys.argv[2])
model = YOLO("yolo26n.pt")
images = sorted(p for p in folder.iterdir()
                if p.suffix.lower() in {".jpg", ".jpeg", ".png", ".webp"})
totals = Counter()
with open(csv_path, "w", newline="") as f:
    w = csv.writer(f)
    w.writerow(["image", "label", "confidence", "x1", "y1", "x2", "y2"])
    for img in images:
        r = model(str(img), conf=0.25, verbose=False)[0]
        for box in r.boxes:
            label = r.names[int(box.cls)]
            totals[label] += 1
            w.writerow([img.name, label, round(float(box.conf), 3)] +
                       [round(float(v), 1) for v in box.xyxy[0]])
print(f"images: {len(images)}  detections: {sum(totals.values())}")
for label, n in totals.most_common():
    print(f"  {label:<12} {n}")
print("csv:", csv_path)

Make a folder, drop in a few photos of your own, and run it:

mkdir photos
# copy your own .jpg/.png files into photos/
python3 batch_detect.py photos detections.csv

On the verification machine, a 2-image test folder (the bus photo plus a second demo photo of two people) produced:

images: 2  detections: 8
  person       6
  bus          1
  tie          1

That CSV is the deliverable most projects actually need: it loads straight into a spreadsheet, a database, or a pandas script, and it is the input to every downstream step — counting people per frame, flagging images with no safety helmet, or finding the one photo in a thousand that contains a dog. If you also want the cropped objects themselves, the coordinates are already in the CSV: a few lines of Pillow will cut each box out of its source image.

Which size should you use?

YOLO26 ships five detection scales, all trained on the same 80 COCO classes. The docs' published figures:

ModelCOCO mAP 50–95ParametersFLOPsCPU ONNX speed
yolo26n (nano)40.92.4M5.5B38.9 ms
yolo26s (small)48.69.5M20.9B87.2 ms
yolo26m (medium)53.120.4M68.4B220.0 ms
yolo26l (large)55.024.8M86.8B286.2 ms
yolo26x (extra-large)57.555.7M194.4B525.8 ms

The choice is a ladder, and you climb it only when the smaller rung demonstrably fails:

  • Start with nano. It is the model this guide verified, it runs on anything, and its 40.9 mAP is already good enough for counting, triage, and prototyping.
  • Move to small or medium when small objects disappear. Distant people, small products on a shelf, and birds in a wide shot are where the extra parameters pay first.
  • Large and extra-large are for accuracy-first, offline work. At 286–526 ms per image on CPU they are not real-time on a laptop, but for overnight batch jobs over an archive they are the right call — swap one filename, YOLO("yolo26m.pt"), and nothing else in this guide changes.

The same one-line swap reaches the rest of the family: yolo26n-seg.pt for instance segmentation, yolo26n-pose.pt for keypoints, yolo26n-cls.pt for classification, and yolo26n-obb.pt for oriented boxes. Those variants are documented in the same model family; this guide verified detection only, so treat them as the next experiment, not as verified here.

The takeaway

Local object detection in 2026 is a solved installation problem and an interesting judgement problem. The installation is three commands and a 5 MB model. The judgement is everything after: which of the 80 classes you actually need, what confidence threshold your use case can afford, whether the NMS-free head's simpler deployment is worth its small accuracy cost, and when your problem has outgrown the COCO vocabulary and needs its own trained model. Get those four decisions right and a laptop becomes a perfectly good vision system — private, free at the margin, and fast enough to chew through a folder of photos while you make coffee.

Sources: the Ultralytics YOLO26 documentation (model family, five scales, COCO mAP table, NMS-free head, licence, read October 29, 2026), the ultralytics 8.4.192 package metadata (dependencies, Python requirement), and the verification runs quoted in Steps 1–5 (install versions, CLI summary and speed lines, detect.py outputs at conf 0.25/0.70 and with nms=False, and the 2-image batch counts, all from the verification machine). The ONNX speed figure and the segmentation/pose/classification variants are the docs' figures and documented family members, not runs performed here; the article says so where it uses them.