Recipe — Python image classification

For LYNX SDK 1.0. Complete, end-to-end. Install: install/python.md. API: api/python.md. Conventions: api/conventions.md.

Goal: run a whole-image classification head — "what is this image?" (one top-1 label + score for the frame), not "where are the objects?".

Read this first — capability honesty. Classification is a real head in the SDK (lynx.Task.CLASSIFICATION, read back through frame.classifications), but neither catalog model has one. lynx-basic declares detection, segmentation, pose, and depth; lynx-ocr-fleet declares detection only (see models/catalog.md). So on today's keyless models frame.classifications is empty — there is no "every model has a classifier" fallback. The code below always checks model.capabilities first and tells you the truth, then shows the exact same code working against a classifier model you'd supply by slug.

Check the capability first

model.capabilities is a lynx.Task bitmask of the heads the model actually declares. Test for the classification head with in before you trust frame.classifications.

1import lynx
2
3with lynx.open("lynx-basic") as model:
4 has_cls = lynx.Task.CLASSIFICATION in model.capabilities
5 print("capabilities:", model.capabilities) # e.g. Task.BOX|SEGMENTATION|POSE|DEPTH
6 print("has a classification head:", has_cls) # False for lynx-basic

For lynx-basic this prints False — it is a detector, so classification is genuinely unavailable. Don't pretend otherwise; branch on has_cls.

The whole thing

lynx.open and model.predict are synchronous and raise on failure; the first open downloads + verifies + caches the model, so do it once and reuse the Model. predict takes an image path directly (or a (H, W, 3) uint8 RGB numpy array). Narrow the run to just the classification head with tasks=lynx.Task.CLASSIFICATION. The class name is resolved through model.classes — there's no Classification.class_name.

1import lynx
2from lynx.errors import LynxError, ModelNotFound
3
4def classify(slug, image_path):
5 """Open a model (keyless), run the classification head, print top-1 label + score.
6
7 Honest about capability: if the model has no classification head (lynx-basic
8 does not), say so instead of faking a label.
9 """
10 with lynx.open(slug) as model:
11 if lynx.Task.CLASSIFICATION not in model.capabilities:
12 print(f"{slug!r} has no classification head "
13 f"(capabilities: {model.capabilities}). It is not a classifier.")
14 return
15
16 # tasks= narrows the run to just the classification head.
17 frame = model.predict(image_path, tasks=lynx.Task.CLASSIFICATION)
18
19 # frame.classifications is the whole-image top-1 collection: len 0 or 1.
20 if len(frame.classifications) == 0:
21 print(f"No classification produced for {image_path!r}.")
22 return
23
24 top = frame.classifications[0] # a lynx.Classification
25 name = model.classes(top.class_id).name # e.g. "GOLDEN_RETRIEVER"
26 print(f"{name} {top.confidence:.2%}")
27
28
29if __name__ == "__main__":
30 import sys
31 slug = sys.argv[1] if len(sys.argv) > 1 else "lynx-basic"
32 image_path = sys.argv[2] if len(sys.argv) > 2 else "photo.jpg"
33 try:
34 classify(slug, image_path)
35 except ModelNotFound as e:
36 print("model not found (bad slug/version):", e.message)
37 except LynxError as e:
38 print(f"lynx error [{e.code}]: {e.message}")

Run it against lynx-basic and it honestly reports "not a classifier"; run it against a classifier model's slug and the same code prints the label + score.

Reading the result

frame.classifications is a small collection over the C surface's whole-image top-1:

1cls = frame.classifications
2
3len(cls) # 0 when the model emitted no classification, else 1
4top = cls[0] # lynx.Classification — raises IndexError if len == 0
5top.class_id # int class id, resolve the name via model.classes(top.class_id).name
6top.confidence # float in [0, 1]
7
8# vectorized columns (parallel to the collection), for uniform handling:
9cls.class_id # (N,) intp — N is 0 or 1
10cls.confidence # (N,) float32

There is no top_k / top5 and no probs array — the surface exposes top-1 only (the collection shape is forward-compat for a future top-k, but today len is 0 or 1). Resolve the human-readable label with model.classes(top.class_id).name, the same classes enum detection uses.

Passing pixels instead of a path

If the image is already decoded (Pillow, a camera, lynx.camera_open), hand predict the array directly — C-contiguous (H, W, 3) uint8 RGB:

1import numpy as np
2from PIL import Image # pip install pillow
3
4rgb = np.asarray(Image.open("photo.jpg").convert("RGB"))
5frame = model.predict(rgb, tasks=lynx.Task.CLASSIFICATION)

Notes

  • No universal classifier. Only models that declare lynx.Task.CLASSIFICATION in model.capabilities return classifications; the catalog's lynx-basic and lynx-ocr-fleet do not. If you need whole-image categories, train/supply a classification model and load it by slug — see models/no-model.md. The old classify() / results.probs.top1() API is fiction; use model.predict(..., tasks=lynx.Task.CLASSIFICATION)frame.classifications.
  • Detection ≠ classification. To find where objects are, use the box head (every model has it) — see recipes/python-detection.md. Classification answers what is this whole image.
  • top.confidence is a float in [0, 1]; gate on it (if top.confidence < 0.6: mark_uncertain()).
  • One model, many heads: a classifier-plus-detector model can run both — open it and pass tasks=lynx.Task.BOX | lynx.Task.CLASSIFICATION (or omit tasks to run every head it declares), then read both frame.detections and frame.classifications.
  • Reuse one loaded Model across calls; don't reopen per image. Run the first open off the UI thread (it downloads).