4XS.DEV / prototypes GitHub
An AXIS bullet camera beside an ARTPEC-8 chip, with a street scene showing detected people and vehicles.
Promotional illustration — detection overlays shown are not real output. The measured results are below.

YOLOv8
on AXIS

Stock COCO YOLOv8n running on the camera's own DLPU. 80 classes, on real hardware, no server, no cloud.

Prototype ARTPEC-8 AXIS OS 12 / 13 ACAP Native SDK AGPL-3.0

01 / What it is

Nothing leaves the camera.

An ACAP that loads a quantized YOLOv8n into larod on an AXIS Q1656, decodes the detection head on the camera's own CPU, draws bounding boxes on the live stream, and publishes detections as a native Axis camera event that any VMS can subscribe to.

It is a weekend project — me teaching an Axis camera a new trick, and pushing the envelope to find out where it breaks. Claude Code wrote most of the lines. The hardware, the domain judgement and the debugging are mine.

The point is the pipeline, not the class list: an arbitrary detector, quantized, running on the camera itself, with a settings UI and VMS events. Swapping in a model trained on something you actually care about is the interesting direction.

1
Sensor frame
16:9 live stream
2
YOLOv8n on the DLPU
larod · quantized uint8 · DFL head included
3
Decode + NMS on the CPU
worker thread · 5040 anchors × 80 classes
4
Boxes on the live stream
coloured by class · one frame of lag
5
Native Axis event
any VMS can subscribe

02 / What actually got measured

Numbers from the repo, not estimates.

Inference figures are from the 640×640 model on an AXIS Q1656.

54ms
Inference per frame on axis-a8-dlpu-tflite
14ms
CPU decode + NMS, 8400 anchors × 80 classes
~12.5fps
End-to-end, ~80 ms, before pipelining
yes
Whole graph on the DLPU, DFL head included
Pipelined loop · frame N decodes while N+1 is on the DLPU
DLPU
N · 54 ms N+1 N+2
CPU
N N+1

Decoding runs on a worker thread, so the loop is inference-bound rather than inference-plus-decode. The cost is one frame of lag between video and overlay, which is not visible at these rates.

03 / The aspect-ratio finding

Thirty points of recall from nothing but aspect ratio.

larod's convert preprocessor scales the frame to the model input without letterboxing, so a square model sees a 16:9 scene squashed to about 56 % of its width — nothing like the photographs COCO was trained on.

Re-exported at 384×640 and scored against the float32 PyTorch model, on 28 images held out of both models' calibration sets, decoded exactly as the ACAP decodes:

Frames cropped to 16:9 — what the camera actually delivers
modelinputrecallfalse positives
square640×64057.5 %19
rectangular384×64087.6 %11
The same images uncropped — ordinary ~4:3 photos
modelinputrecallfalse positives
square640×64065.3 %16
rectangular384×64051.6 %9

The lesson is not that rectangular is better — it is that the model's aspect ratio has to match the sensor's, and that getting it wrong costs more than most of the tuning anyone bothers with. The rectangular model also uses 40 % fewer input pixels.

04 / The quantization trap

A model that silently detects nothing.

Worth knowing if you ever quantize a YOLOv8 yourself. Ultralytics' single output0 concatenates box coordinates, which range 0..~650, with class scores, which range 0..1. Per-tensor int8 quantization then picks one scale for both, around 2.54. Every class score below ~1.27 rounds to zero, and the exported model detects nothing at all — with no error anywhere.

Maximum class score on the same test image
exportmax score
float32 reference0.665
single-output int80.000
split-output int80.625

The fix is to cut the graph before the final Concat so boxes and scores become two tensors with their own quantization scales. The ACAP tells them apart by byte size rather than index, because larod does not guarantee output order.

05 / Which cameras

It does not travel as far as it looks.

The DLPU model format is not the same across Axis SoCs.

SoCDLPU model formatthis repo
ARTPEC-8TFLite int8verified — AXIS Q1656, AXIS OS 12.11
ARTPEC-9TFLite int8runs on ARTPEC-9 — first trial
ARTPEC-7TFLite int8untested, probably too slow
CV25Ambarella CVflow .binno — different toolchain
CV75proprietary, from ONNXno

If the device named in runOptions is absent, the app enumerates what larod does offer and picks a DLPU device whose name also contains tflite, logs the substitution and carries on. The tflite part matters: an ARTPEC-9 product offers both a9-dlpu-native and a9-dlpu-tflite, and taking the first name containing dlpu picked the native one, which rejected the model outright.

One evening on an AXIS Q6358. The same .tflite loads and detects with no rebuild. It is slower — 5–10 fps against about 12.5 — and compiling the model for the DLPU takes around 90 seconds. The first load after every start failed once and succeeded on the retry, four times in a row, so the app now retries in place. And VDO numbers its channels differently there: the overview is channel 1, with view areas from 2, so the view area you pick on the settings page may not be the one you get. That last one is why this says runs rather than verified.

CV25 and CV75 are a port, not a flag: they take a proprietary format converted through Ambarella's toolchain.

06 / What this is not

Read this part.

It takes the DLPU exclusively. The manifest declares deepLearningProcessor.required, so AXIS Object Analytics must be stopped before this app will start. You are trading AOA for this, not adding it.

06b / On one view

Detect on a view area, not just the whole sensor.

A view area is a crop of the sensor, and the detector can run on one: a gate or a doorway fills the model input instead of occupying a tenth of it. The same number selects the frames and the surface the boxes are drawn on, so the picture and the detections cannot describe different crops.

AXIS Object Analytics always runs on the full view of the first sensor. This is one of the few places where a prototype can do something the built-in analytics does not.

Verified on a Q1656 with two view areas configured. A view the camera does not have falls back to the full view with a warning rather than exiting.

Colour says what a box is.

The camera's box drawer renders rectangles and no text, so colour carries the class: people green, vehicles blue, animals purple, everything else amber — the same four in the settings page's class list and confidence bars, so a box on the video and a row in the list are tied together. One flat colour is a setting for anyone who prefers it.

Labels on the boxes would need a Cairo render per frame. Not built: the colours carry most of the information for a fraction of the cost.

07 / Get it

Build it yourself.

An unsigned .eap is attached to the latest release for convenience. It is unsigned, it claims the DLPU exclusively, and it has been run on exactly one camera — read the limitations above first. Building it yourself needs Docker and the ACAP Native SDK image, and shows you exactly what you are installing.

git clone https://github.com/kotyzap/YOLOv8-on-AXIS-ACAP
cd YOLOv8-on-AXIS-ACAP
sh acap/build.sh

curl --digest -u root:PASS -F "packfil=@acap/YOLOv8_Detector_0_10_14_aarch64.eap" \
  "http://CAMERA/axis-cgi/applications/upload.cgi"

Re-export the model — different input size, different calibration set, your own weights — with sh tools/export_yolov8.sh. The build refuses to run if the committed quantization header and the model file have drifted apart.

Read the code. It includes improvements.md, a full code review of this repo and what came of it.

08 / Licence

AGPL-3.0, and not by choice.

This repo ships a model derived from Ultralytics YOLOv8, which is AGPL-3.0, and Ultralytics reads that as covering the whole derivative work. To build something closed on top of this you need either an Ultralytics Enterprise licence or a different model — several good detectors are Apache-2.0. I am not a lawyer and this is not legal advice.

The ACAP skeleton derives from Axis's object-detection-yolov5 example (Apache-2.0), which is one-way compatible into AGPL-3.0.