Depth estimation (knowing how far away every pixel in a scene is) is one of those capabilities that sounds like it belongs in a data centre. Run a neural network over every frame, get a dense depth map back, repeat 30 times a second. The compute budget for that, conventional wisdom says, is measured in GPU-hours.
This article argues otherwise. We ran two depth estimation models on two Qualcomm Dragonwing edge AI boards, both doing real-time inference locally, with no cloud and no GPU. The results tell two different stories that are worth telling together.
The first story is about an autonomous mobile robot. A LeKiwi omni-drive base carries a Rubik Pi 3 (QCS6490, 12 TOPS) and an Intel RealSense D415. The robot drives around, streams LiDAR and RGB-D to a browser via Rerun, and simultaneously runs MiDaS V2 depth estimation on its own NPU, all on a single board drawing a few watts from a battery pack. That is the embodied edge story: depth estimation not as a standalone benchmark, but as one workload among several on a moving platform with a real power budget.
The second story is about what happens when you step up the compute. The IQ-9075 EVK (QCS9075, 50 TOPS) sits on the desk with its own D415. It runs Depth Anything V2, a newer and more capable model, at over 11 frames per second on its NPU alone. That is the industrial edge story: a board sized for vision systems, robotics arms, or smart cameras that need higher fidelity than a mobile platform can afford.
Same task. Same sensor. Different chips, different models, different contexts. Here is how we built both demos, what we learned deploying on Qualcomm's Hexagon NPU, and why the comparison matters.
HardwareRubik Pi 3 (QCS6490)The Rubik Pi 3 is a credit-card-sized SBC built around the Qualcomm QCS6490 SoC. It carries a Hexagon DSP with 12 TOPS of AI performance, an Adreno 643 GPU, and a Kryo CPU cluster. For a mobile robot it hits a practical sweet spot: enough compute for computer vision workloads, a standard Linux environment, and low enough power draw to run off a USB-C battery pack for extended sessions.
The IQ-9075 is a developer kit for the QCS9075 SoC, Qualcomm's higher-tier edge AI platform. At 50 TOPS it has more than four times the NPU headroom of the QCS6490, and its newer Hexagon architecture adds FP16 support, which matters for deploying models that were not quantised at training time.
Both setups use the same sensor: an Intel RealSense D415 RGB-D camera. It streams 640x480 colour and depth at up to 30 fps over USB 3. For this project it serves two purposes: providing the RGB input to the depth estimation model, and providing ground-truth depth via its stereo IR module for comparison.
The embodied demo uses a LeKiwi omni-drive base: three STS3215 servo-driven mecanum wheels arranged at 120-degree intervals, controlled via a Feetech serial bus. The Rubik Pi 3 sits on top alongside an RPLiDAR A2 for 2D scan data. The whole assembly is backdrivable, small enough to navigate indoor corridors, and light enough that a USB-C power bank runs both the compute and the servos.
The Rubik Pi 3 carrying MiDaS here is the same board that drives this robot's autonomous navigation stack: LiDAR mapping, kiwi-drive teleop, and multi-sensor streaming. For the full platform build, see the companion article: [].
Both boards run Qualcomm's version of Ubuntu 24.04, built by Canonical with the AI Hub toolchain for model deployment. The key components are:
- Qualcomm AI Hub: cloud-based model compilation, quantisation, and performance profiling for Hexagon targets. Used to fetch optimised TFLite models for each board.
- TFLite + QNN delegate: inference runtime on device. The QNN HTP delegate (
/usr/lib/libQnnTFLiteDelegate.so) offloads computation to the Hexagon NPU. Without it, inference falls back to XNNPACK on CPU and is 10-15x slower. - qai-hub-models: Qualcomm's Python package providing pre-trained, pre-quantised models with a consistent API. Used for both MiDaS V2 (Rubik Pi 3) and Depth Anything V2 (IQ-9075).
- Intel RealSense SDK (pyrealsense2): D415 capture, depth scale retrieval, and aligned RGB-D frames.
- Rerun 0.37: real-time telemetry visualisation. Streams RGB, depth maps, LiDAR scans, and model outputs to a browser viewer over gRPC.
- FastAPI + WebSocket: web-based joystick teleop for the LeKiwi, running as a separate process from the sensor/visualisation stack.
The choice of model per board was not arbitrary. Depth Anything V2's default export uses FP16 operations. The QCS6490's Hexagon V68 NPU rejects these at compile time with a validation error (SocModel doesn't support FP16). MiDaS V2 in w8a8 INT8 quantisation passes validation on both chips, making it the portable choice. The IQ-9075's newer Hexagon has FP16 support, so Depth Anything runs there without modification.
Both boards run the same pipeline: a TFLite model on the Hexagon NPU through the QNN delegate, fed by the D415 over USB. The runtime package differs between them, and the one thing worth getting right on both is confirming the NPU is actually in use and not silently falling back to CPU.
Rubik Pi 3 (QCS6490)Install the runtime and camera dependencies:
pip install ai-edge-litert pyrealsense2 opencv-python numpyThe QNN delegate ships with the board's Qualcomm software at /usr/lib/libQnnTFLiteDelegate.so. Load it when you create the interpreter and point it at the HTP (Hexagon) backend:
from ai_edge_litert.interpreter import Interpreter, load_delegate
delegate = load_delegate(
"/usr/lib/libQnnTFLiteDelegate.so",
options={"backend_type": "htp"},
)
interpreter = Interpreter(
model_path="midas-tflite-w8a8/midas.tflite",
experimental_delegates=[delegate],
)
interpreter.allocate_tensors()Use the w8a8 (8-bit) MiDaS model here. The QCS6490 NPU runs the quantized graph fully on-chip; an FP16 model falls back to CPU and the frame rate collapses.
IQ-9075 (QCS9075)Same pattern, different runtime package:
pip install tflite-runtime pyrealsense2 opencv-python numpyThe delegate path and HTP option are identical. The IQ-9075 has the headroom to run the larger Depth Anything model at 518x518 input while the Rubik Pi stays on the lighter MiDaS at 256x256.
from tflite_runtime.interpreter import Interpreter, load_delegate
delegate = load_delegate(
"/usr/lib/libQnnTFLiteDelegate.so",
options={"backend_type": "htp"},
)
interpreter = Interpreter(
model_path="depth_anything-tflite-float/depth_anything.tflite",
experimental_delegates=[delegate],
)
interpreter.allocate_tensors()The only differences from the Rubik Pi are the import (tflite_runtime instead of ai_edge_litert) and the model. Because the QCS9075 supports FP16, you can load the float Depth Anything graph directly, no w8a8 quantisation step required.
This is the step people skip. If the delegate fails to load, TFLite quietly runs on CPU and you only notice from the slow frame rate. Guard against it by making the CPU path explicit and comparing:
python3 depth_anything_live.py # NPU via QNN delegate
python3 depth_anything_live.py --cpu # force CPU, no delegateIf the two run at nearly the same speed, the delegate is not engaging. On the NPU you should see the per-frame inference time drop to the tens of milliseconds, matching the numbers in the benchmark table below.
Capture, inference, and the timing loopThe model is only half the pipeline. The D415 feeds it over USB. Configure aligned color and depth streams and pull the depth scale once at startup:
import pyrealsense2 as rs
import numpy as np
pipeline = rs.pipeline()
config = rs.config()
config.enable_stream(rs.stream.color, 640, 480, rs.format.rgb8, 15)
config.enable_stream(rs.stream.depth, 640, 480, rs.format.z16, 15)
profile = pipeline.start(config)
depth_scale = profile.get_device().first_depth_sensor().get_depth_scale()Then the per-frame loop: grab a frame, preprocess, run the NPU, and time just the invoke() call. That timing is exactly where the benchmark numbers come from:
import time
frames = pipeline.wait_for_frames()
color_img = np.asanyarray(frames.get_color_frame().get_data())
hw_depth = np.asanyarray(frames.get_depth_frame().get_data())
img = preprocess(color_img) # resize, normalize, add batch dim
interpreter.set_tensor(input_details[0]["index"], img)
t0 = time.perf_counter()
interpreter.invoke()
t1 = time.perf_counter()
infer_ms = (t1 - t0) * 1000
output = interpreter.get_tensor(output_details[0]["index"])
model_depth = postprocess(output, 480, 640)A warmup of a few invoke() calls before the loop matters: the first inference loads the Hexagon firmware and is much slower than steady state, so timing it would skew the average.
To produce the side-by-side panels in the clips above, log the RGB frame, the colorized model depth, and the D415 hardware depth to Rerun each frame:
import rerun as rr
rr.set_time("frame", sequence=frame_count)
rr.log("cameras/rgb", rr.Image(color_img))
rr.log("cameras/model_depth", rr.Image(model_depth_color[:, :, ::-1]))
rr.log("cameras/hw_depth", rr.DepthImage(hw_depth, meter=1.0 / depth_scale))
rr.log("perf/inference_ms", rr.Scalars(infer_ms))A note on onnxruntime-qnn: the ONNX route looks like the obvious path on Qualcomm hardware, but session.RegisterCustomOpsLibrary raised an error during setup and cost real time. The TFLite plus QNN delegate route above is what worked reliably on both boards.
The claim worth testing here is not just that the QCS6490 can run a depth model. It is that it can run one as part of a broader real-time workload without the model dominating the board.
On the LeKiwi robot, the Rubik Pi 3 is simultaneously:
- Driving three STS3215 servos via a Feetech serial bus using kiwi-drive inverse kinematics
- Reading the RPLiDAR A2 at 8000 points/sec
- Capturing and JPEG-encoding D415 RGB frames at 30 fps
- Streaming LiDAR scans and RGB-D to a browser via Rerun gRPC
- Running MiDaS V2 inference on the Hexagon NPU
The MiDaS thread is deliberately isolated. It reads the latest D415 colour frame from a thread-safe queue (max depth 1, so it always gets the freshest frame or skips), runs the TFLite+QNN interpreter, and logs the colorised depth output to Rerun. If the model stalls, the motor loop and LiDAR stream are unaffected.
How MiDaS runs on QCS6490The model is fetched from Qualcomm AI Hub as a pre-quantised TFLite file (w8a8, 256x256 input). Loading the QNN HTP delegate and creating the interpreter takes about 3-4 seconds on first run. After that, each inference call takes approximately 117ms, giving around 8.5 fps throughput.
# Load TFLite + QNN delegate
delegate = tflite.load_delegate(
'/usr/lib/libQnnTFLiteDelegate.so',
options={'backend_path': 'libQnnHtp.so'}
)
interpreter = tflite.Interpreter(
model_path=MIDAS_MODEL_PATH,
experimental_delegates=[delegate]
)
interpreter.allocate_tensors()Input preprocessing: resize to 256x256, normalise to [0, 1], expand dims to (1, 256, 256, 3). Output is a (1, 256, 256, 1) float32 depth map, which is normalised and colourised with OpenCV's COLORMAP_INFERNO before logging to Rerun.
The robot can be driven via a browser joystick while the Rerun viewer shows the LiDAR scan, the D415 colour frame, and the MiDaS depth map updating in real time. The depth map lags the colour frame by one inference cycle (117ms), but for navigation awareness that is acceptable. The board does not throttle, overheat, or drop motor commands while the model is running.
IQ-9075: Higher Fidelity at the Industrial EdgeThe IQ-9075 EVK is a different proposition. It sits on a desk, powered from mains, with a D415 connected over USB 3. The question here is not whether the board survives being a robot brain; it is how much inference quality and throughput the extra 38 TOPS buys you.
The answer: Depth Anything V2 at 11 fps, from a raw camera feed, with no GPU.
Getting Depth Anything runningThe path to NPU inference on the IQ-9075 was not the obvious one. The first instinct was to use onnxruntime-qnn, Qualcomm's QNN execution provider for ONNX Runtime. It installs cleanly and exposes the right symbols, but the RegisterCustomOps call fails at runtime with an undefined symbol error. After confirming this was a packaging issue rather than a model issue, we switched to TFLite + QNN delegate, the same runtime used on the Rubik Pi 3.
Depth Anything V2 was fetched from Qualcomm AI Hub as a TFLite FP16 model compiled for the QCS9075 target:
qai-hub-models fetch depth-anything-v2 -r tflite -p fp16 --device qcs9075Inference code follows the same delegate pattern as MiDaS, with the input resized to 518x518 and normalised to ImageNet mean/std before being passed to the interpreter.
ResultsOn the QCS9075 NPU with FP16 precision:
- Inference latency: 90.5ms per frame
- Throughput: ~11 fps
- CPU baseline (PyTorch): 1211ms / 0.8 fps
- ONNX CPU baseline: 645ms / 1.5 fps
The NPU gives a 13x speedup over PyTorch CPU and a 7x speedup over ONNX CPU, enough to cross the threshold from unusably slow to genuinely usable for real-time applications.
Compared to MiDaS V2, Depth Anything V2 produces sharper object boundaries and better handles thin structures and transparent surfaces. At 518x518 input versus MiDaS's 256x256, the output depth map carries more spatial detail. For applications like object detection, bin picking, or obstacle avoidance on a fixed camera, the natural home of the IQ-9075, that detail matters.
Benchmark ResultsAll numbers measured on device with a live D415 feed. CPU baselines use the same model weights without the QNN delegate.
The IQ-9075 is faster in absolute terms and runs a larger, more capable model. But the Rubik Pi 3 result is arguably more significant in context: 8.5 fps while simultaneously driving motors, reading LiDAR, and streaming sensor data to a browser.
Lessons LearnedDeploying depth models on Qualcomm's Hexagon NPU is not hard, but there are several things the documentation does not make obvious.
FP16 is not universal across Hexagon generations: The QCS6490 (Hexagon V68) rejects FP16 ops at NPU compile time. If you fetch a model from AI Hub without checking the supported precision for your target SoC, it will fail validation silently or with a cryptic error. Always run qai-hub-models numerics <model> to check precision support before committing to a model for a given board. INT8 / w8a8 quantisation is the safe choice if you need to support both chips from the same codebase.
onnxruntime-qnn does not work the way you expect: The package installs, the QNN EP name is accessible, but RegisterCustomOps raises an undefined symbol error at runtime. Use TFLite + the QNN HTP delegate instead. It is more mature, better tested on device, and the performance is the same.
The QNN delegate adds 3-4 seconds of startup time: The first interpreter allocation loads the Hexagon firmware. Build this into your app's startup sequence, and do not try to lazy-load the model on first inference request in a latency-sensitive loop.
Thread isolation keeps the system stable: Running depth inference in a separate thread with a max-1 queue means the motor loop and LiDAR stream never block waiting for the model. If you put inference in the main camera loop, a model stall stalls everything. Separate threads with queues are the right pattern for any multi-modal edge system.
Conclusion and What's NextTwo Dragonwing boards, one depth camera each, no cloud, no GPU. The Rubik Pi 3 runs MiDaS at 8.5 fps as part of a full robot stack. The IQ-9075 runs Depth Anything at 11 fps with higher output quality. Both are genuinely real-time on NPU hardware that fits in your hand or on a robot chassis.
The practical takeaway for builders: if you are targeting the QCS6490, stick to INT8/w8a8 models and use TFLite + QNN delegate. If you have the QCS9075, FP16 models work and you have enough headroom for larger architectures. The AI Hub workflow (fetch, delegate, allocate, infer) is the same on both.
For the navigation and teleop side of this platform, how the LeKiwi robot drives, maps, and streams its sensors, see the companion build - Link
What's nextSLAM and Nav2: The LiDAR is already streaming into Rerun. The natural next step is feeding it into a ROS 2 Nav2 stack for autonomous waypoint navigation, using the depth map to supplement the 2D LiDAR with obstacle height awareness.
Live camera feed in the teleop UI: The web joystick currently drives the robot blind. Adding an MJPEG stream of the D415 colourframe to the browser UI would make remote operation practical without needing the Rerun viewer open in a second tab.
VLM/VLA on Qualcomm NPU: A language-conditioned policy, telling the robot where to go in plain English and having it navigate there, is the longer-horizon goal. Qualcomm AI Hub has quantised vision-language models available. Combining one with the existing motor control and depth perception stack is the direction this project is heading.Here is the project repository - Link









Comments