Skip to main content
Qualcomm Linux · AI Hub · QNN · NPU

A complete prototype path for live monocular depth on Dragonwing IQ-8275: Ultralytics YOLO depth → AI Hub W8A16 quantization → QNN DLC → generated QNN context binary → persistent native C++ QNN runner → Python/OpenCV live camera UI.

Heath Blandford·Jul 29, 2026·← All posts

This tutorial shows how to run a live USB-camera monocular depth demo on the Hexagon NPU of a Dragonwing IQ-8275 EVK using a model compiled by Qualcomm AI Hub and executed through a native C++ QNN app. The final demo is intentionally split into two parts: That split keeps the NPU path realistic and persistent while still making the live demo easy to modify.
Target used for this prototype: Dragonwing IQ-8275 EVK running Ubuntu 24.04 on aarch64, QCS8275/QCS8300-class platform, Hexagon V75, USB camera, and an attached display. The same pattern applies to other Dragonwing targets, but the AI Hub target, QAIRT version, and generated context binary must match your hardware/runtime.

What you will build

By the end, the live path looks like this:
In the prototype run, steady-state native QNN inference was about 16 ms/inference, or roughly 62 FPS for the model execution itself. The live display FPS is lower because it also includes camera capture, Python preprocessing, file I/O between Python and the native process, colorization, and OpenCV display.

Reference benchmark from the prototype

All measurements below used imgsz=320.
Benchmark numbers depend on the board image, QAIRT version, model version, camera resolution, display resolution, thermal state, and power mode. Treat these as a reference, not a product specification.

Prerequisites

Hardware

  • Dragonwing IQ-8275 EVK with HTP/NPU support. If your board is not set up yet, follow Set up the IQ-8275 EVK on Ubuntu.
  • USB camera
  • Display attached to the board for the live OpenCV window
  • Network connectivity for package/model downloads and AI Hub job submission

Software on the board

Install the runtime, development headers, and Python packages:
Confirm the HTP backend is available:
A healthy setup reports a Hexagon architecture and that the backend DSP test passed, for example:
Set up the Python environment:
Keep your AI Hub token private. If a token is ever pasted into a chat, issue tracker, terminal recording, or shared log, revoke or rotate it from your AI Hub account.
Configure AI Hub once:

Step 1: Export YOLO depth to ONNX

Download/load the Ultralytics YOLO depth model and export a fixed 320×320 ONNX model.
The expected output is:

Clean duplicate ONNX metadata if necessary

Some exporters can place a graph output in both graph.output and graph.value_info. AI Hub can reject that with a duplicate-name error. This small sanitizer removes any duplicated value_info entries.

Step 2: Capture calibration images

The model input is NHWC float32 RGB, normalized to [0, 1], with Ultralytics-style square letterboxing to 320×320. Create make_aihub_calib.py:
Capture calibration data:
For better visual quality, use more frames from representative scenes:

Step 3: Quantize with AI Hub

This example uses W8A16 quantization: 8-bit weights and 16-bit activations. Create submit_aihub_quant.py:
Run it:
Quantized ONNX graphs often contain integer tensors internally. In this prototype, the public output remained FLOAT [1,1,320,320]. The internal tensor before dequantization was uint16, followed by DequantizeLinear to the public float output. Check your own graph before assuming output type or layout.

Step 4: Compile the quantized model to QNN DLC

In AI Hub, compile the quantized model for a target that matches your board’s SoC/NPU generation and select the Qualcomm AI Runtime / QNN DLC target runtime. Download the compiled DLC to the board:
Sanity-check it with qnn-net-run before writing a native app:
If this fails, fix the model/runtime issue before building the native app.

Step 5: Generate a QNN context binary

The downloaded DLC may contain topology, parameters, and weights, not a prebuilt HTP context cache. A native app can compose that, but the standard fast path is to generate a QNN context binary once and load it directly.
The generated file may have a doubled suffix, depending on the tool version:
Treat a QNN context binary as target-specific. It is tied to the hardware target and QAIRT/QNN version. Regenerate it when you change the board image, QAIRT version, target device, or model.

Step 6: Build a persistent native QNN runner

The native runner does three things:
  1. Dynamically loads QNN providers from /usr/lib.
  2. Loads the generated context binary with QnnContext_createFromBinary().
  3. Reuses the graph and tensors for repeated QnnGraph_execute() calls.
It also has a simple line-based server mode so Python can stream frames without reloading the model:
Create a folder:
Create qnn_app/Makefile:
Copy the complete qnn_dlc_runner.cpp source from the companion files page. These are the key implementation requirements:
  • Include QNN headers from /usr/include/QNN.
  • Load libQnnHtp.so with dlopen().
  • Load QnnInterface_getProviders and choose a provider that exposes QNN_API_VERSION_MAJOR.
  • Call the QNN backend/device/context APIs in the same sequence used by QNN sample apps.
  • Load the generated context binary, not the original DLC, using QnnContext_createFromBinary().
  • Use the graph/tensor metadata from the context or the known model contract:
    • graph: graph_ymndtmzg
    • input: images, shape [1,320,320,3], float32, 1,228,800 bytes
    • output: output_0, shape [1,1,320,320], float32, 409,600 bytes
  • In server mode, copy each new input buffer into the registered input tensor, call QnnGraph_execute(), and write the output tensor to disk.
Build:
Run a one-shot benchmark:
Expected output resembles:
Compare against qnn-net-run output to verify correctness:
A correct native runner should match qnn-net-run exactly or within normal floating-point tolerance. In the prototype, max_abs_diff was 0.0.

Step 7: Add the live Python camera app

The live Python process owns the camera and display. The native C++ process owns the QNN context and graph. The Python app should:
  1. Start qnn_dlc_runner --server.
  2. Wait for READY.
  3. Open the USB camera with OpenCV.
  4. For each frame:
    • letterbox to 320×320, BGR → RGB, float32 [0,1], NHWC batch
    • write input.raw
    • send RUN input.raw output.raw to the native process
    • read output.raw as float32 [1,1,320,320]
    • crop away the letterbox padding and resize depth back to camera resolution
    • colorize depth and show RGB | DEPTH | OVERLAY
Copy the complete live Python app from the companion files page. A minimal loop looks like this:
Run the live app from the board’s desktop session:
If camera index 0 is not correct:
Suggested controls:

Output contract and dtype checks

For this prototype: Even though W8A16 quantization uses integer tensors internally, the compiled QNN runtime output was float32. Check this for every model. A common pattern is:
where output0_q may be 16-bit but the public model/runtime output is float32.

Troubleshooting

qnn-platform-validator fails

The NPU backend is not ready. Check that the correct board image, firmware, FastRPC devices, and QNN packages are installed before debugging the model.

qnn-context-binary-generator succeeds but the app cannot load the context

Regenerate the context on the same board/runtime that will run the app. Context binaries are not portable across arbitrary QAIRT versions and targets.

The native app exits before printing READY

In server mode, make sure the initial --input path exists and has the correct byte size. The prototype runner validates the input path during startup, even though later frames are supplied through RUN commands. Create a dummy input before launching the server:

Camera opens but display fails

Run from a terminal attached to the board’s graphical session, not a headless SSH shell. If using SSH for logs, keep the OpenCV display on the board’s monitor.

Depth colors look unstable

Use more representative calibration frames and recapture in the actual lighting/camera setup. For live demos, 100–300 frames is a better starting point than 32 frames.

Why not run qnn-net-run per frame?

qnn-net-run is excellent for validation, but it is a command-line test tool. If you spawn it per frame, most of your time goes into process startup, context setup, and teardown. For a live app, keep the QNN context alive:
That is the main reason the persistent native app measured around 16 ms while the qnn-net-run path measured around 48 ms amortized in the prototype.

Summary

This demo runs a Qualcomm AI Hub W8A16-quantized YOLO depth model on the Dragonwing IQ-8275 Hexagon NPU through a persistent native C++ QNN app, while Python handles camera capture, preprocessing, visualization, and the live UI. This gives you Python’s flexibility for the application layer with the performance characteristics of native QNN model execution.