# Debugging Guide Source: https://imsdkdocs.qualcomm.com/advanced/debugging Developer guide for debugging, profiling, and tracing QIM SDK GStreamer pipelines. This guide covers practical debugging techniques for QIM SDK AI pipelines — from enabling verbose logs to measuring per-stage latency, tracing buffers, and diagnosing camera or performance issues. *** ## 1. Enable Debug Logs QIM SDK uses the standard GStreamer debug system. Pass `--gst-debug=` to `gst-launch-1.0`, or set the `GST_DEBUG` environment variable for Python/C++ apps. ### Debug Levels | Level | Name | Description | | - | - | - | | 0 | none | No debug information is output. | | 1 | ERROR | Logs all fatal errors. These are errors that do not allow the core or elements to perform the requested action. The application can still recover if programmed to handle the conditions that triggered the error. | | 2 | WARNING | Logs all warnings. Typically these are non-fatal, but user-visible problems are expected to happen. | | 3 | FIXME | Logs all "fixme" messages. Those typically that a codepath that is known to be incomplete has been triggered. It may work in most cases, but may cause problems in specific instances. | | 4 | INFO | Logs all informational messages. These are typically used for events in the system that only happen once, or are important and rare enough to be logged at this level. | | 5 | DEBUG | Logs all debug messages. These are general debug messages for events that happen only a limited number of times during an object's lifetime; these include setup, teardown, change of parameters, etc. | | 6 | LOG | Logs all log messages. These are messages for events that happen repeatedly during an object's lifetime; these include streaming and steady-state conditions. This is used for log messages that happen on every buffer in an element for example. | | 7 | TRACE | Logs all trace messages. Those are message that happen very very often. This is for example is each time the reference count of a GstMiniObject, such as a GstBuffer or GstEvent, is modified. | | 9 | MEMDUMP | Logs all memory dump messages. This is the heaviest logging and may include dumping the content of blocks of memory. | ### Global Level ```bash theme={null} gst-launch-1.0 --gst-debug=3 \ filesrc location=... ! ... ``` ### Per-Element Level Override the global level for a specific plugin — useful for isolating one stage without drowning in output from everything else: ```bash theme={null} # Global = 2 (warnings), but inference plugin = 6 (verbose) gst-launch-1.0 --gst-debug=2,qtimltflite:6 \ filesrc location=... ! ... ``` ### Common Starting Points | Flag | When to use | | - | - | | `--gst-debug=2` | Production runs — quiet, errors/warnings only | | `--gst-debug=2,qtimltflite:5` | Prints all ERROR and WARNING messages globally, plus ERROR, WARNING, INFO, and DEBUG messages specifically from the qtimltflite plugin | | `--gst-debug=2,qtimlvtransform:4,qtivcomposer:7` | Prints all ERROR and WARNING messages globally, ERROR, WARNING, and INFO messages from qtimlvtransform, and ERROR, WARNING, INFO, DEBUG, LOG, and TRACE messages from qtivcomposer | ### Python / C++ Apps ```bash theme={null} GST_DEBUG=3 python3 my_pipeline.py ``` ```bash theme={null} GST_DEBUG=5 ./my_pipeline_app ``` ### Redirect to File ```bash theme={null} GST_DEBUG=5 GST_DEBUG_FILE=/data/gst.log GST_DEBUG_COLOR_MODE=off gst-launch-1.0 ... ``` *** ## 2. Measure Pipeline KPIs (Latency per Stage) Use the built-in GStreamer **latency tracer** to get per-element mean/min/max processing time for every stage — no custom instrumentation required. ### Enable Tracing ```bash theme={null} GST_TRACERS="latency(flags=pipeline+element)" \ GST_DEBUG="GST_TRACER:7" \ gst-launch-1.0 -e \ qtimlvconverter name=stage_01_preproc \ qtimltflite name=stage_01_inference \ model=$HOME/Downloads/qimsdk_samples/models/$MODEL_NAME \ delegate=external external-delegate-path=libQnnTFLiteDelegate.so \ external-delegate-options="QNNExternalDelegate,backend_type=htp,log_level=(string)1;" \ qtimlpostprocess name=stage_01_postproc module=yolov8 \ labels=$HOME/Downloads/qimsdk_samples/labels/$LABELS_NAME \ settings="{\"confidence\": 51.0}" \ filesrc location=$HOME/Downloads/qimsdk_samples/media/$SRC_VIDEO_NAME ! qtdemux ! h264parse ! \ v4l2h264dec capture-io-mode=4 output-io-mode=4 ! video/x-raw,format=NV12 ! queue ! \ tee name=t ! qtimetamux name=obj_mux ! qtivoverlay ! \ fpsdisplaysink name=display sync=false text-overlay=false \ video-sink="waylandsink" \ t. ! queue ! stage_01_preproc. stage_01_preproc. ! queue ! \ stage_01_inference. stage_01_inference. ! queue ! \ stage_01_postproc. stage_01_postproc. ! text/x-raw ! queue ! obj_mux. \ 2> /tmp/trace.log ``` ### Parse Results ```bash theme={null} gst-stats-1.0 /tmp/trace.log ``` ### Reading the Output ``` Element Latency Statistics: stage_01_preproc.src: mean=2.19ms min=1.24ms max=12.2ms ← preprocessing stage_01_inference.src: mean=5.71ms min=4.95ms max=6.78ms ← inference (NPU) stage_01_postproc.src: mean=0.24ms min=0.12ms max=0.44ms ← postprocessing obj_mux.src: mean=0.76ms min=0.14ms max=2.03ms ← metadata sync ``` At 30 fps (33 ms budget per frame), the typical AI pipeline uses \~37% of the frame budget, leaving headroom for additional stages. ### Measure FPS Live (On-Screen) Add `fpsdisplaysink` in place of `waylandsink` to see FPS on the display in real time: ```bash theme={null} ... ! qtivoverlay ! fpsdisplaysink sync=false video-sink="waylandsink sync=false" ``` *** ## 3. Measure Inference Performance ### GST\_TRACER (inferences/sec) From the [tracer output](/advanced/debugging#enable-tracing), calculate inferences/sec: ``` inferences/sec = 1 / mean_inference_latency ``` Example: `mean = 5.71 ms` → `1 / 0.00571 ≈ 175 inferences/sec` ### pidstat (CPU/Memory alongside inference) ```bash theme={null} # Run in a second terminal while the pipeline is running pidstat -u -r -p $(pgrep gst-launch-1.0) 1 ``` This reports per-second CPU (`%usr`, `%sys`) and memory (`RSS`, `VSZ`) for the pipeline process. *** ## 4. Debug Performance Issues ### Frame Drops Frame drops manifest as: * Stuttering on the display * `queue` elements reporting overflows in debug logs (`--gst-debug=4`) * Tracer showing high `max` latency on inference (`stage_01_inference`) **Check for queue overflows:** ```bash theme={null} gst-launch-1.0 --gst-debug=4,queue:5 ... ``` Look for lines like: ``` WARN queue gstqueue.c:... overrun ... ``` **Fix:** Add `max-size-time=0 max-size-bytes=0 max-size-buffers=3` to queues around inference: ```bash theme={null} ... ! queue max-size-buffers=3 ! stage_01_preproc. ... ``` ### Slow Throughput If end-to-end latency is higher than expected: 1. Run the [latency tracer](/advanced/debugging#2-measure-pipeline-kpis-latency-per-stage) to find which stage is the bottleneck. 2. Check if the inference delegate is correct — GPU delegate is much slower than HTP (NPU): ```bash theme={null} # Slow — GPU qtimltflite delegate=gpu ... # Fast — NPU via HTP qtimltflite delegate=external \ external-delegate-path=libQnnTFLiteDelegate.so \ external-delegate-options="QNNExternalDelegate,backend_type=htp;" ``` 3. Check model quantization — W8A8 (quantized) is significantly faster on the NPU than float32. ### Memory Copies Unnecessary buffer copies add latency. To detect them, enable memory tracing: ```bash theme={null} GST_DEBUG=4,GST_BUFFER:5 gst-launch-1.0 ... 2>&1 | grep -i "copy\|memcpy\|alloc" ``` Use `capture-io-mode=4 output-io-mode=4` on decoder/encoder elements to enable zero-copy DMA buffer passing: ```bash theme={null} v4l2h264dec capture-io-mode=4 output-io-mode=4 ``` *** ## 5. Trace Buffers Buffer tracing lets you follow a specific buffer through the pipeline to diagnose where it is dropped, delayed, or corrupted. ### Enable Buffer Tracing ```bash theme={null} GST_DEBUG=GST_SCHEDULING:5 gst-launch-1.0 ... 2>&1 | head -200 ``` Each line shows a buffer's `PTS` (presentation timestamp), `DTS`, `size`, and which pad it passed through. ### Check for PTS Discontinuities If frames are out of order or the pipeline stalls, look for timestamp jumps: ```bash theme={null} GST_DEBUG=GST_SCHEDULING:5 gst-launch-1.0 ... 2>&1 | grep -i "pts\|discont\|gap" ``` ### Use the Dot Graph (Pipeline Visualization) Dump the pipeline as a `.dot` file to visually inspect element connections: ```bash theme={null} GST_DEBUG_DUMP_DOT_DIR=/tmp gst-launch-1.0 -e ... & # After pipeline is running: dot -Tpng /tmp/*.dot -o pipeline.png ``` *** ## 6. Debug Camera Issues ### Common Errors **QMMF Recorder StartCamera Failed:** ``` ERROR qticamsrc qmmf_source_context.cc: QMMF Recorder StartCamera Failed! WARN qticamsrc qmmf_source.c: error: Failed to Open Camera! ``` **Fix:** Kill and restart the camera server: ```bash theme={null} sudo killall cam-server ``` Then re-run the pipeline. The camera server restarts automatically on the next `gst-launch-1.0` invocation. ### Enable Camera Server Debug Logs ```bash theme={null} GST_DEBUG=2,qticamsrc:6 gst-launch-1.0 \ qticamsrc name=camsrc ! video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1 ! \ waylandsink fullscreen=true sync=false ``` ### Camera Not Detected ```bash theme={null} # List available V4L2 devices v4l2-ctl --list-devices # Check camera capabilities v4l2-ctl -d /dev/video0 --all ``` ### ISP Camera (Config #2 / qticamsrc) To switch from libcamera (Config #1) to `qticamsrc` (Config #2), write `"camx"` to the EFI variable: ```bash theme={null} echo -n "camx" > /var/data efivar -n 882f8c2b-9646-435f-8de5-f208ff80c1bd-VendorDtbOverlays -w -f /var/data sync && reboot ``` ### Verify Camera Stream is Running ```bash theme={null} # Quick sanity check — should show frames on display gst-launch-1.0 \ qticamsrc name=camsrc ! video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1 ! \ waylandsink fullscreen=true sync=false ``` *** ## 7. Quick Diagnostic Checklist Run this sequence when a pipeline doesn't work: ```bash theme={null} # Step 1 — Check GStreamer can see the plugins gst-inspect-1.0 qtimltflite gst-inspect-1.0 qtimlvconverter gst-inspect-1.0 qticamsrc # Step 2 — Run with level 3 logs gst-launch-1.0 --gst-debug=3 [your pipeline] 2>&1 | grep -E "ERROR|WARN|FIXME" # Step 3 — Check camera server sudo killall cam-server # reset if camera errors appear # Step 4 — Add latency tracing GST_TRACERS="latency(flags=pipeline+element)" GST_DEBUG="GST_TRACER:7" \ gst-launch-1.0 [your pipeline] 2> /tmp/trace.log gst-stats-1.0 /tmp/trace.log ``` *** ## 8. Report an Issue When filing a bug or asking for help, include: 1. **Platform** — Device model (e.g. RB3 Gen 2), OS (Ubuntu / Qualcomm Linux), QIM SDK version 2. **Full pipeline command** — the exact `gst-launch-1.0` or Python script 3. **Debug log** — captured with `--gst-debug=5 2> debug.log` 4. **Tracer output** — from `gst-stats-1.0` if a performance issue 5. **Error message** — the exact error line from the log 6. **Expected vs actual behaviour** File the issue through the [Qualcomm Support](https://www.qualcomm.com/support) portal. ```bash theme={null} # Collect everything in one command GST_DEBUG=5 GST_TRACERS="latency(flags=pipeline+element)" GST_DEBUG_FILE=/tmp/debug.log \ gst-launch-1.0 [your pipeline] gst-stats-1.0 /tmp/debug.log >> /tmp/debug.log # Attach /tmp/debug.log to your issue ``` # QIM SDK Docker Source: https://imsdkdocs.qualcomm.com/advanced/docker Build, configure, and run the QIM SDK Docker container on Qualcomm platforms. ## Overview Containers are lightweight, isolated environments that package everything needed to run an application — code, dependencies, and system libraries — so it behaves consistently across platforms. Advantages of containerization include: * **Consistency:** Containers provide a consistent environment across multiple platforms and operating systems. Developers can create images that transform into containers when deployed, ensuring uniformity regardless of the underlying infrastructure. * **Automation:** Containerization supports automated tasks. Developers can schedule actions without manual intervention, reducing repetitive work. * **Stability:** Containers are based on Linux, with the Linux kernel present in every container. This stability extends to systems and devices, including when containers run on macOS or Windows. * **Space efficiency:** Unlike traditional virtual machines, which consume significant server space and memory, containers include only application code and dependencies. They can run entirely in the cloud and minimize resource requirements. Container architecture overview ## QIM SDK in Docker QIM SDK in a Docker container means the full Qualcomm multimedia and AI software stack is delivered as containerized environments instead of being manually installed directly on a host or target system. In practice, a build container compiles and assembles all required components, including cross-compiled artifacts for Qualcomm ARM64 targets. The build then produces a deploy container that contains only the runtime pieces needed on the device. **Key benefits of this approach:** * **Reproducible builds:** Use the same toolchain, package versions, and scripts across developers and CI. Reduces "works on my machine" failures. * **Build and runtime separation:** Use a heavy build image to compile artifacts and a lean deploy image that contains only runtime essentials. * **Efficient cross-compilation:** Build on the host and target ARM64 Qualcomm devices reliably, faster than compiling directly on the target. * **Secure runtime posture:** Run as a non-root user by default, avoid broad privileged-container dependencies, and use CDI-based explicit hardware passthrough for GPU, camera, and media devices. * **Operational consistency:** Use versioned images that are testable, promotable, and rollback-friendly, with a predictable deployment flow using environment and CDI configuration on the target. ### Features * **Debian (Trixie) base:** Built on Debian Trixie for a streamlined and lightweight foundation. * **No platform dependencies:** Entirely self-sufficient — no eSDK or host platform content required to build or deploy. * **Self-contained dependencies:** All necessary dependencies are installed directly inside the container, minimizing host-container compatibility issues and simplifying deployment on edge devices. * **GStreamer 1.26:** Includes GStreamer 1.26 with the latest `gst-qti-oss-patches` for optimized performance on Qualcomm platforms. * **Broad target support:** Validated across QLI 2.x, Ubuntu Server, and Ubuntu Desktop. Supports IQ615, QCS6490, IQ8275, IQ9075, IQ-X5121 and IQ-X7181 platforms. ### Versions | Version | Status | | - | - | | 2.1.0 | Latest | This version of image targets only the following platforms. Support for more platforms & OS will be added in upcoming releases. * QCS6490 * IQ-9075 * IQ-8275 * IQ-615 * IQ-X7181 * IQ-X5121 * QCS6490 * IQ-9075 * IQ-8275 * IQ-X7181 * IQ-X5121 ## Architecture The following diagram explains the build and deployment flow of the QIM SDK Docker image. QIM SDK Docker build and deployment flow * The host machine runs a development container that compiles all QIM SDK sources and dependencies into Debian artifacts. * The generated artifacts are transferred to a Debian-based deploy container with CDI-managed hardware access that runs on the Qualcomm device. ## Quick Start Guide Before getting started, ensure the device is set up by following the [Installation](/installation) guide. Camera is not supported in the container. ### Download configuration files Before setting up the device, download the CDI and environment files for the target chipset and OS. The CDI JSON file provides platform-specific device mountings; the environment file sets the required variables inside the container. Select your chipset to download the corresponding files: | Target OS | CDI file | Environment file | | - | - | - | | QLI 2.x | qcs6490\_qli\_2x\_qimsdk.json | qcs6490\_qli\_2x\_qimsdk.env | | Ubuntu 24.04 Server | qcs6490\_ubuntu\_server\_qimsdk.json | qcs6490\_ubuntu\_server\_qimsdk.env | | Ubuntu 24.04 Desktop | qcs6490\_ubuntu\_desktop\_qimsdk.json | qcs6490\_ubuntu\_desktop\_qimsdk.env | | Target OS | CDI file | Environment file | | - | - | - | | QLI 2.x | qcs8275\_qli\_2x\_qimsdk.json | qcs8275\_qli\_2x\_qimsdk.env | | Ubuntu 24.04 Server | qcs8275\_ubuntu\_server\_qimsdk.json | qcs8275\_ubuntu\_server\_qimsdk.env | | Ubuntu 24.04 Desktop | qcs8275\_ubuntu\_desktop\_qimsdk.json | qcs8275\_ubuntu\_desktop\_qimsdk.env | | Target OS | CDI file | Environment file | | - | - | - | | QLI 2.x | qcs9075\_qli\_2x\_qimsdk.json | qcs9075\_qli\_2x\_qimsdk.env | | Ubuntu 24.04 Server | qcs9075\_ubuntu\_server\_qimsdk.json | qcs9075\_ubuntu\_server\_qimsdk.env | | Ubuntu 24.04 Desktop | qcs9075\_ubuntu\_desktop\_qimsdk.json | qcs9075\_ubuntu\_desktop\_qimsdk.env | | Target OS | CDI file | Environment file | | - | - | - | | QLI 2.x | qcs615\_qli\_2x\_qimsdk.json | qcs615\_qli\_2x\_qimsdk.env | | Target OS | CDI file | Environment file | | - | - | - | | QLI 2.x | iqx7181\_qli\_2x\_qimsdk.json | iqx7181\_qli\_2x\_qimsdk.env | | Ubuntu 26.04 Server | iqx7181\_ubuntu\_server\_qimsdk.json | iqx7181\_ubuntu\_server\_qimsdk.env | | Ubuntu 26.04 Desktop | iqx7181\_ubuntu\_desktop\_qimsdk.json | iqx7181\_ubuntu\_desktop\_qimsdk.env | | Target OS | CDI file | Environment file | | - | - | - | | QLI 2.x | iqx5121\_qli\_2x\_qimsdk.json | iqx5121\_qli\_2x\_qimsdk.env | | Ubuntu 26.04 Server | iqx5121\_ubuntu\_server\_qimsdk.json | iqx5121\_ubuntu\_server\_qimsdk.env | | Ubuntu 26.04 Desktop | iqx5121\_ubuntu\_desktop\_qimsdk.json | iqx5121\_ubuntu\_desktop\_qimsdk.env | ### Set up and run on the device ```bash theme={null} ssh @ ``` ```bash theme={null} mkdir -p /etc/cdi mkdir -p /etc/docker/env mkdir -p $HOME/Downloads/qimsdk_samples mkdir -p $HOME/Downloads/qimsdk_samples/media mkdir -p $HOME/Downloads/qimsdk_samples/models mkdir -p $HOME/Downloads/qimsdk_samples/labels ``` ```bash theme={null} scp .json @:/etc/cdi/qimsdk.json scp .env @:/etc/docker/env/qimsdk.env ``` On platforms such as iq615, iqx7181, and iqx8121, the Qualcomm downstream video drivers (`video32` and `video33`) are unavailable. Therefore, the upstream video driver is used. Since the upstream driver may be assigned to any device node between `/dev/video0` and `/dev/video28`, an additional configuration step is required to determine and configure the appropriate video nodes. Run the following command to list the video devices: ```bash theme={null} v4l2-ctl --list-devices ``` Example: ```bash theme={null} Qualcomm Venus video decoder (plat:aa00000.video-codec:dec): /dev/video2 Qualcomm Venus video encoder (plat:aa00000.video-codec:enc): /dev/video3 USB Camera (usb-0000:00:14.0-3): /dev/video4 /dev/video5 ``` Identify the relevant video device nodes and add them to the cdi.json file that has been deployed to the target system before creating the container. ``` "deviceNodes": [ ... { "path": "/dev/video2", "uid": 0, "gid": 44 }, { "path": "/dev/video3", "uid": 0, "gid": 44 }, { "path": "/dev/video4", "uid": 0, "gid": 44 }, { "path": "/dev/video5", "uid": 0, "gid": 44 }, ... ] ``` The QIM SDK native image is intended for native pipeline execution and includes all required runtime dependencies for QIM SDK applications. Python dependencies are intentionally excluded to keep the image lightweight and user can use the QIM SDK Python image for python support. ```bash theme={null} docker pull artifacts.codelinaro.org/iot-solutions-microservices/qimsdk:latest ``` Tag the image locally: ```bash theme={null} docker tag artifacts.codelinaro.org/iot-solutions-microservices/qimsdk:latest qimsdk:latest ``` The QIM SDK Python image includes all functionality available in the base image, along with Python support. ```bash theme={null} docker pull artifacts.codelinaro.org/iot-solutions-microservices/qimsdk-python:latest ``` Tag the image locally: ```bash theme={null} docker tag artifacts.codelinaro.org/iot-solutions-microservices/qimsdk-python:latest qimsdk-python:latest ``` An additional step is required for SE-Linux enabled builds to provide access to cdi, env & media artifacts. ```bash theme={null} # After pushing the CDI file to the device run the following command restorecon -Rv /etc/cdi # To get access to wayland and pipewire sockets. setsebool -P dockerd_connect_user_services on # To get access to the user Downloads directory setsebool -P dockerd_mounton_xdg_downloads on ``` ```bash theme={null} # Run the following commands whenever new artifacts are uploaded to the paths below. chcon -t xdg_downloads_t /var/roothome/Downloads chcon -R -t xdg_downloads_t /var/roothome/Downloads/qimsdk_samples ``` ```bash theme={null} # The following docker run command mounts $HOME/Downloads/qimsdk_samples # into the container for media assets and application artifacts. # Using this path is recommended to avoid potential SELinux permission issues. docker run -it -d \ --net host \ --env-file /etc/docker/env/qimsdk.env \ -v $HOME/Downloads/qimsdk_samples:/home/qimsdk/Downloads/qimsdk_samples \ --device qualcomm.com/device=qimsdk \ -h \ --name \ ``` After the container is running, execute a new terminal process in it: ```bash theme={null} docker exec -ti bash ``` ```bash theme={null} ssh @ ``` ```bash theme={null} sudo apt update sudo apt install docker.io sudo usermod -aG docker $USER # Log out and log back in for the group membership changes to take effect. ``` ```bash theme={null} sudo mkdir -p /etc/cdi sudo mkdir -p /etc/docker/env mkdir -p $HOME/Downloads/qimsdk_samples mkdir -p $HOME/Downloads/qimsdk_samples/media mkdir -p $HOME/Downloads/qimsdk_samples/models mkdir -p $HOME/Downloads/qimsdk_samples/labels ``` ```bash theme={null} scp .json @:/etc/cdi/qimsdk.json scp .env @:/etc/docker/env/qimsdk.env ``` On platforms such as iq615, iqx7181, and iqx8121, the Qualcomm downstream video drivers (`video32` and `video33`) are unavailable. Therefore, the upstream video driver is used. Since the upstream driver may be assigned to any device node between `/dev/video0` and `/dev/video28`, an additional configuration step is required to determine and configure the appropriate video nodes. Run the following command to list the video devices: ```bash theme={null} sudo apt install v4l-utils v4l2-ctl --list-devices ``` Example: ```bash theme={null} Qualcomm Venus video decoder (plat:aa00000.video-codec:dec): /dev/video2 Qualcomm Venus video encoder (plat:aa00000.video-codec:enc): /dev/video3 USB Camera (usb-0000:00:14.0-3): /dev/video4 /dev/video5 ``` Identify the relevant video device nodes and add them to the cdi.json file that has been deployed to the target system before creating the container. ``` "deviceNodes": [ ... { "path": "/dev/video2", "uid": 0, "gid": 44 }, { "path": "/dev/video3", "uid": 0, "gid": 44 }, { "path": "/dev/video4", "uid": 0, "gid": 44 }, { "path": "/dev/video5", "uid": 0, "gid": 44 }, ... ] ``` The QIM SDK native image is intended for native pipeline execution and includes all required runtime dependencies for QIM SDK applications. Python dependencies are intentionally excluded to keep the image lightweight and user can use the QIM SDK Python image for python support. ```bash theme={null} docker pull artifacts.codelinaro.org/iot-solutions-microservices/qimsdk:latest ``` Tag the image locally: ```bash theme={null} docker tag artifacts.codelinaro.org/iot-solutions-microservices/qimsdk:latest qimsdk:latest ``` The QIM SDK Python image includes all functionality available in the base image, along with Python support. ```bash theme={null} docker pull artifacts.codelinaro.org/iot-solutions-microservices/qimsdk-python:latest ``` Tag the image locally: ```bash theme={null} docker tag artifacts.codelinaro.org/iot-solutions-microservices/qimsdk-python:latest qimsdk-python:latest ``` ```bash theme={null} # The following docker run command mounts $HOME/Downloads/qimsdk_samples # into the container for media assets and application artifacts. # Using this path is recommended to avoid potential SELinux permission issues. # Automatically detect and pass the active Wayland display to the container, # as the WAYLAND_DISPLAY name may vary across systems and display configurations sudo docker run -it -d \ --net host \ -e WAYLAND_DISPLAY=$(basename \ $(ls ${XDG_RUNTIME_DIR}/wayland-* 2>/dev/null | head -n1) \ 2>/dev/null) \ --env-file /etc/docker/env/qimsdk.env \ -v $HOME/Downloads/qimsdk_samples:/home/qimsdk/Downloads/qimsdk_samples \ --device qualcomm.com/device=qimsdk \ -h \ --name \ ``` After the container is running, execute a new terminal process in it: ```bash theme={null} sudo docker exec -ti bash ``` Once inside the container, refer to the **Examples** section to run * [AI pipelines](/sample-pipelines/aipipelines) * [multimedia pipelines](/sample-pipelines/multimediapipelines) * [GStreamer C/C++ AI applications](/sample-application/ai-sample-applications) * [GStreamer C/C++ multimedia applications](/sample-application/multimedia-sample-applications) * [C++ App Builder](/app-builder/cpp-quickstart) * [Python App Builder](/app-builder/python-quickstart) ### Running GStreamer Sample applications inside container By default, sample application configuration files are installed under `/etc/configs`. However, this location cannot be mounted into the container due to SELinux restrictions. On the platform, copy the configuration files to `$HOME/Downloads/qimsdk_samples/configs`. Because `$HOME/Downloads/qimsdk_samples` is mounted into the container by the previous docker run command, the `configs` directory will be accessible from within the container. ```bash theme={null} mkdir -p $HOME/Downloads/qimsdk_samples/configs cp /etc/configs/* $HOME/Downloads/qimsdk_samples/configs chmod -R 0777 $HOME/Downloads/qimsdk_samples/configs ``` Inside the container, use `--config-file` argument to pass config file to sample app: ```bash theme={null} gst-ai-classification \ --config-file=$HOME/Downloads/qimsdk_samples/configs/config_classification.json ``` ## Build an QIM SDK container All steps in this section are performed on a Ubuntu 22.04 or 24.04 machine which can be x86 or ARM64. ### Prerequisites Install prerequisite packages on the host (one time only). ```bash theme={null} sudo apt install -y jq tofrodos sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq sudo chmod +x /usr/bin/yq ``` ```bash theme={null} sudo apt install -y jq tofrodos qemu-user-static qemu-system-arm sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq sudo chmod +x /usr/bin/yq wget http://archive.ubuntu.com/ubuntu/pool/universe/q/qemu/qemu-user-static_6.2+dfsg-2ubuntu6_amd64.deb sudo dpkg -i qemu-user-static_6.2+dfsg-2ubuntu6_amd64.deb rm qemu-user-static_6.2+dfsg-2ubuntu6_amd64.deb ``` Do **not** install `yq` via snap. If it was installed via snap, remove it and reinstall using the commands above: ```bash theme={null} sudo snap remove yq sudo systemctl stop snapd ``` Increase the maximum user watches and instances on the host to prevent this error during development: ```text theme={null} System limit for number of file watchers reached ``` Add these lines to `/etc/sysctl.conf`: ```text theme={null} fs.inotify.max_user_instances=8192 fs.inotify.max_user_watches=542288 ``` Save the file and reboot the host system. To build Docker image files, the host system requires at least 64 GB of RAM and a swap image of at least half the available RAM plus a small reserve. For example, with 64 GB of RAM, the recommended swap size is at least 32 GB. With an 8 GB reserve, total RAM + swap + reserved memory is 104 GB. Check whether swap is enabled: ```bash theme={null} sudo swapon --show ``` If swap is missing or too small, create a new swap file: ```bash theme={null} MEM_AVAIL=$(grep MemAvailable /proc/meminfo | awk '{printf "%.0f\n", $2/1024/1024}') let MEM_SWAP="$MEM_AVAIL / 2 + 8" sudo swapoff /swap.img sudo fallocate -l "${MEM_SWAP}G" /swap.img sudo chmod 600 /swap.img sudo mkswap /swap.img sudo swapon /swap.img ``` Edit `/etc/default/grub` and set: ```text theme={null} GRUB_CMDLINE_LINUX_DEFAULT="text cgroup_enable=memory swapaccount=1" ``` Update GRUB: ```bash theme={null} sudo update-grub ``` Edit `/etc/fstab` and add this line at the end of the file: ```text theme={null} /swap.img none swap sw 0 0 ``` Save and close all opened system files, then reboot the host system. After rebooting, verify the swap image matches the expected size: ```bash theme={null} sudo swapon --show ``` Docker must be configured on the host system once before building images. Remove old Docker Desktop versions if present: ```bash theme={null} sudo apt remove docker-desktop rm -r ~/.docker/desktop sudo rm /usr/local/bin/com.docker.cli sudo apt purge docker-desktop ``` Install repository dependencies: ```bash theme={null} sudo apt-get update sudo apt-get install \ ca-certificates \ curl \ gnupg \ lsb-release ``` Create the Docker keyring directory: ```bash theme={null} sudo mkdir -p /etc/apt/keyrings ``` Add the Docker GPG key: ```bash theme={null} curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg ``` Add the Docker repository: ```bash theme={null} echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null ``` ```bash theme={null} sudo apt-get update sudo apt-get install docker-ce docker-ce-cli ``` Add the user to the Docker group to run Docker commands without `sudo`: ```bash theme={null} sudo groupadd docker sudo usermod -aG docker $USER newgrp docker ``` Until the host is rebooted, run `newgrp docker` in every newly opened terminal. Tabs, unexpected spaces, and other invisible whitespace characters can break JSON configuration files and may cause a `docker.service failed to start` error. Add the registry mirror configuration to `/etc/docker/daemon.json`: ```json theme={null} { "registry-mirrors": [""] } ``` Restart Docker: ```bash theme={null} sudo fromdos -f /etc/docker/daemon.json sudo systemctl daemon-reload sudo systemctl restart docker ``` If network downloads require a proxy, add the proxy configuration to the Docker daemon. Tabs, unexpected spaces, and other invisible whitespace characters can break JSON configuration files and may cause a `docker.service failed to start` error. Add the proxy configuration to `/etc/docker/daemon.json`: ```json theme={null} { "proxies": { "http-proxy": "", "https-proxy": "", "no-proxy": "" } } ``` Restart Docker: ```bash theme={null} sudo fromdos -f /etc/docker/daemon.json sudo systemctl daemon-reload sudo systemctl restart docker ``` Set proxy environment variables before invoking Docker build commands: ```bash theme={null} export http_proxy= export https_proxy= export no_proxy= ``` This ensures that both the Docker daemon and build processes use the same proxy settings as the host system. Until the host is rebooted, run `newgrp docker` in every newly opened terminal. Run the Docker hello-world container: ```bash theme={null} docker run hello-world ``` If the build image script fails with a `No space left on device` error, move the Docker directory to `/local/mnt`. All containers must be restarted after moving the Docker directory because the Docker service is stopped during this procedure. Stop Docker: ```bash theme={null} service docker stop ``` Verify that no Docker process is running: ```bash theme={null} ps faux ``` Check the Docker directory structure: ```bash theme={null} sudo ls /var/lib/docker/ ``` Back up the current Docker directory: ```bash theme={null} tar -zcC /var/lib docker > /mnt/pd0/var_lib_docker-backup-$(date +%s).tar.gz ``` Move the Docker directory to the new partition: ```bash theme={null} mv /var/lib/docker /local/mnt/docker ``` Create a symlink to the Docker directory in the new partition: ```bash theme={null} ln -s /local/mnt/docker /var/lib/docker ``` Verify that the Docker directory structure is unchanged: ```bash theme={null} sudo ls /var/lib/docker/ ``` Start Docker: ```bash theme={null} service docker start ``` List container names: ```bash theme={null} docker ps -a ``` Start a listed container: ```bash theme={null} docker start ``` Check that there are no active cgroup CPU and memory utilization restrictions on the host. The Docker service cgroup memory configuration entries should have `max` set as the value. ```bash theme={null} sudo systemctl list-unit-files | grep -Ei docker sudo systemctl status docker.service cat /sys/fs/cgroup/system.slice/docker.service/ cat /sys/fs/cgroup/system.slice/docker.service/memory.max cat /sys/fs/cgroup/system.slice/docker.service/memory.swap.max ``` If the host system has 64 GB RAM or less, tune system VM page swap and dentry and inode cache reclaim behavior. **`vm.swappiness`** Higher values cause more aggressive application memory page swapping; lower values favor keeping pages in memory longer. The default is usually `60` on recent Ubuntu and Debian versions, which may not fit the Docker build environment on systems with low available RAM. A value of `10` is recommended. **`vm.vfs_cache_pressure`** Intermittent high VFS pressure can result from creating many small temporary files during package installation, causing the OS to prematurely swap memory pages associated with cached dentries and inodes. The default is usually `100`. Setting it to `400` relaxes cache management by freeing cached pages earlier, reducing the chance of running out of memory during high multi-threaded load. **`vm.min_free_kbytes`** In heavy multi-stage Docker builds with many buildx threads, the host OS can exhaust available RAM quickly. The mandatory VM free memory watermark is controlled by `vm.min_free_kbytes`. Too low risks deadlocks under load; too high causes premature OOM kills. A value of `262144` KB better balances free memory under excessive load. Edit `/etc/sysctl.conf` and add: ```text theme={null} vm.swappiness=10 vm.vfs_cache_pressure=400 vm.min_free_kbytes=262144 ``` ### Build steps Run the following commands on the host machine: ```bash theme={null} git clone https://git.codelinaro.org/clo/le/sdk-tools.git -b imsdk-tools.lnx.1.0 cd sdk-tools/qimsdk-debian/ git checkout fcc71f5f12791f77d8174cb5772eab6c785578d7 ``` Build `qimsdk_deploy_arm64`, which contains the minimal set of runtime binaries needed to execute GStreamer use cases: ```bash theme={null} docker build \ --build-arg QIMSDK_ARG_QNP_VERSION= \ --build-arg QIMSDK_ARG_CAMERA_SERVICE_TAG= \ --build-arg QIMSDK_ARG_GST_PLUGINS_TAG=\ --build-arg QIMSDK_ARG_SOLUTIONS_MICROSERVICES_TAG= \ --build-arg QIMSDK_ARG_GST_QTI_OSS_PATCHES_TAG= \ --build-arg QIMSDK_ARG_MAX_JOBS= \ --target qimsdk_deploy_arm64 \ -t qimsdk \ . ``` For example: Docker build command for `2.1.0` release ```bash theme={null} docker build \ --build-arg QIMSDK_ARG_QNP_VERSION=2.50.0.260828 \ --build-arg QIMSDK_ARG_CAMERA_SERVICE_TAG=1.0.6 \ --build-arg QIMSDK_ARG_GST_PLUGINS_TAG=2.0.2 \ --build-arg QIMSDK_ARG_SOLUTIONS_MICROSERVICES_TAG=805fd443c8ffe75eed16ca54a68602535c80e232 \ --build-arg QIMSDK_ARG_GST_QTI_OSS_PATCHES_TAG=a4db6968a1f467875407a2008d7c13974a826fe4 \ --build-arg QIMSDK_ARG_MAX_JOBS=8 \ --target qimsdk_deploy_arm64 \ -t qimsdk \ . ``` Build `qimsdk_deploy_py_arm64`, which contains the minimal set of runtime binaries needed to execute GStreamer use cases along with python support: ```bash theme={null} docker build \ --build-arg QIMSDK_ARG_QNP_VERSION= \ --build-arg QIMSDK_ARG_CAMERA_SERVICE_TAG= \ --build-arg QIMSDK_ARG_GST_PLUGINS_TAG=\ --build-arg QIMSDK_ARG_SOLUTIONS_MICROSERVICES_TAG= \ --build-arg QIMSDK_ARG_GST_QTI_OSS_PATCHES_TAG= \ --build-arg QIMSDK_ARG_MAX_JOBS= \ --target qimsdk_deploy_py_arm64 \ -t qimsdk-python \ . ``` For example: Docker build command for `2.1.0` release ```bash theme={null} docker build \ --build-arg QIMSDK_ARG_QNP_VERSION=2.50.0.260828 \ --build-arg QIMSDK_ARG_CAMERA_SERVICE_TAG=1.0.6 \ --build-arg QIMSDK_ARG_GST_PLUGINS_TAG=2.0.2 \ --build-arg QIMSDK_ARG_SOLUTIONS_MICROSERVICES_TAG=805fd443c8ffe75eed16ca54a68602535c80e232 \ --build-arg QIMSDK_ARG_GST_QTI_OSS_PATCHES_TAG=a4db6968a1f467875407a2008d7c13974a826fe4 \ --build-arg QIMSDK_ARG_MAX_JOBS=8 \ --target qimsdk_deploy_py_arm64 \ -t qimsdk-python \ . ``` #### Build Arguments | Argument | Description | Default | | - | - | - | | *QIMSDK\_ARG\_QNP\_VERSION* | Specifies the QAIRT SDK version to use. If not provided, the QNN and SNPE plugins are not built. | Not set | | *QIMSDK\_ARG\_CAMERA\_SERVICE\_TAG* | Commit ID or tag from the [Qualcomm Camera Service repository](https://github.com/qualcomm/camera-service) | `main` | | *QIMSDK\_ARG\_GST\_PLUGINS\_TAG* | Commit ID or tag from the [qimsdk](https://github.com/qualcomm/qimsdk) repository. | `main` | | *QIMSDK\_ARG\_SOLUTIONS\_MICROSERVICES\_TAG* | Commit ID or tag from the [solutions-microservices](https://git.codelinaro.org/clo/le/solutions-microservices/) repository. | `iot-solutions.lnx.1.0` | | *QIMSDK\_ARG\_GST\_QTI\_OSS\_PATCHES\_TAG* | Commit ID or tag from the [gst-qti-oss-patches](https://github.com/qualcomm/gst-qti-oss-patches) repository. | `1.26.2` | | *QIMSDK\_ARG\_MAX\_JOBS* | Limits the maximum number of parallel build jobs used during image creation. Valid values range from `1` to the number of available CPU cores (`nproc`). | All available CPU cores | See [qimsdk-debian/README.md](https://git.codelinaro.org/clo/le/sdk-tools/-/blob/fcc71f5f12791f77d8174cb5772eab6c785578d7/qimsdk-debian/README.md) for more information about the Docker image composition and workflow. Save the built image as a tar file: ```bash theme={null} docker save -o .tar ``` Copy the tar file to the device: ```bash theme={null} scp .tar @:~/Downloads/ ``` Load the image on the device: ```bash theme={null} docker load -i ~/Downloads/.tar ``` The home directory (\~) varies by platform: * QLI 2.x: /root * Ubuntu Noble: /home/ubuntu * Ubuntu Resolute: /home/qcom With the image copied to the device, follow the [Set up and run on the device](#set-up-and-run-on-the-device) steps to configure CDI and start the container. ## Install additional packages inside the container Use this procedure to install packages into an already running `qimsdk-debian` deploy container. The container must have been started with the `--net host` option. The following steps use vi as an example. ```bash theme={null} docker exec -ti qimsdk sh ``` In the root shell, install the required packages: ```bash theme={null} apt-get update DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC \ apt-get install -y --no-install-recommends vi apt -y upgrade apt-get autoremove -y apt-get clean ``` After installing the vi package in the previous step, it can be launched from any new shell session started as the `qimsdk` user: ```bash theme={null} docker exec -ti qimsdk bash ``` ## Debugging The following techniques apply to the QIM SDK container running on the Qualcomm device. ### Check container logs ```bash theme={null} docker logs -f ``` ```bash theme={null} docker logs --tail 100 -f ``` ### Inspect container state ```bash theme={null} docker ps -a ``` Inspect detailed container metadata including environment variables, mounts, and network settings: ```bash theme={null} docker inspect ``` ### Restart a container ```bash theme={null} docker stop ``` ```bash theme={null} docker start ``` # Technology Overview Source: https://imsdkdocs.qualcomm.com/advanced/technologyoverview Qualcomm Technology components used in by QIM SDK # GStreamer plugin architecture The QIM SDK encapsulates hardware complexity within a modular plugin architecture, freeing developers from the burden of managing low-level platform libraries or hardware-specific details that vary across Qualcomm chipsets and generations. Each [QIM SDK plugin](../plugin-reference/introduction) in the SDK maps directly to a dedicated hardware accelerator — including video encode/decode, camera ISP, GPU, display, and AI/ML accelerators — giving developers a clean, unified API surface to build sophisticated multimedia and AI pipelines with minimal integration overhead Figure:Qualcomm QIM SDK GStreamer plugin architecture ## Camera Architecture The QIM SDK abstracts the underlying camera driver and hardware through a client-server architecture, shielding developers from the complexities of low-level camera management and enabling rapid construction of single-stream, multi-stream, multi-client, and multi-camera applications. At its core, the [`qticamsrc`](../plugin-reference/qticamsrc) GStreamer element operates as a client to the le-camera service — a system-level service responsible for managing the camera HAL, stream lifecycle, buffer allocation, and capture control. This separation of responsibilities allows [`qticamsrc`](../plugin-reference/qticamsrc) to surface the full camera capability set to the GStreamer pipeline through a developer-friendly interface of source pads, camera properties, and control signals. At runtime, [`qticamsrc`](../plugin-reference/qticamsrc) performs the following on behalf of the pipeline: * Opens and configures the target camera device * Creates and manages video and image output streams * Receives captured frames from the le-camera service * Wraps camera buffers as GstBuffer objects and pushes them downstream through the corresponding source pads * Returns buffers to the camera service upon downstream consumption This architecture enables zero-copy camera output by retaining buffer ownership and stream resources within the service layer — maximizing pipeline throughput while minimizing memory overhead. Camera pipeline ### Related Information [Camera use cases](../sample-pipelines/multimediapipelines#1-camera) ## AI Inference Architecture The QIM SDK provides end-to-end support for AI/ML video and audio analytics pipelines, covering the full processing chain from raw media ingestion through model inference to real-time result visualization. AI pipelines in QIM SDK are built around a tensor-based, three-stage processing model: preprocessing, inference, and postprocessing. Each stage is handled by a dedicated plugin and executes in parallel across consecutive frames — source capture at frame N-2, preprocessing at N-1, inference at N, and postprocessing at N+1 — delivering maximum pipeline throughput and optimal hardware utilization. The pipeline source can be live camera frames via [`qticamsrc`](../plugin-reference/qticamsrc) (YUV) or offline media via `filesrc` (with format conversion); the sink can be an on-screen display via [`waylandsink`](../plugin-reference/waylandsink) or local storage via `filesink`. The [`qtimlvconverter`](../plugin-reference/qtimlvconverter) preprocessing plugin converts raw video frames into normalized tensors by performing color space conversion, resizing, and mean subtraction. It queries tensor dimensions and format requirements directly from the downstream inference plugin at runtime, making it a fully generic, model-agnostic preprocessing stage compatible with all inference backends. All inference plugins operate in tensor-in / tensor-out mode and share the same [`qtimlvconverter`](../plugin-reference/qtimlvconverter) for preprocessing. QIM SDK supports the following inference engines, each with hardware delegate support for acceleration across Qualcomm's neural processing subsystems: | Plugin | Engine | | - | - | | [`qtimltflite`](../plugin-reference/qtimltflite) | LiteRT / TensorFlow Lite | | [`qtimlsnpe`](../plugin-reference/qtimltflite) | Snapdragon Neural Processing Engine (SNPE) | | [`qtimlqnn`](../plugin-reference/qtimlqnn) | Qualcomm Neural Network (QNN) | | [`qtimlonnx`](../plugin-reference/qtimlonnx) | ONNX Runtime | Inference output tensors are passed directly to task-specific postprocessing plugins that decode multi-dimensional tensor data — bounding boxes, class labels, segmentation masks, and keypoints — into structured ML metadata. The postprocessing layer follows a modular sub-module architecture, allowing developers to author custom postprocessing modules to support proprietary or non-standard model architectures. Finally, the [`qtivoverlay`](../plugin-reference/qtivoverlay) plugin consumes the structured ML metadata and renders inference results directly onto the video buffer in real time. The annotated stream can then be rendered to a display, encoded and stored locally, or streamed over a network. Figure:AI Inference Architecture ### Related information [Run AI/ML use cases](../sample-pipelines/aipipelines) ## Video Architecture The QIM SDK provides hardware-accelerated video encode and decode through V4L2-based plugins that interface directly with the Qualcomm multimedia engine. Video encoding and decoding are performed entirely on the dedicated VPU (Video Processing Unit), offloading this work from the CPU and allowing concurrent camera capture, AI inference, and video processing without resource contention. The architecture supports AVC (H.264) and HEVC (H.265) formats for both encode and decode paths. Encoded output can be written to file via a multiplexer or streamed over the network. Decoded output is delivered as DMA-buf-backed NV12 frames, compatible with all downstream QIM SDK plugins including AI preprocessing and display. ### Encode Video frames captured from the camera via [`qticamsrc`](../plugin-reference/qticamsrc) are passed directly to the hardware encoder. The encoded bitstream is parsed and multiplexed into an MP4 or MPEGTS container before being written to file or transmitted over the network. Video encode pipeline | Component | Description | | - | - | | [`qticamsrc`](../plugin-reference/qticamsrc) | Captures video streams from the ISP camera. See [Camera Architecture](/advanced/technologyoverview#camera-architecture). | | [`v4l2h264enc`](../plugin-reference/v4l2h264enc) | Encodes the video stream to AVC (H.264) format using the V4L2 driver. | | [`v4l2h265enc`](../plugin-reference/v4l2h265enc) | Encodes the video stream to HEVC (H.265) format using the V4L2 driver. | | `h264parse` / `h265parse` | Parses the encoded bitstream and inserts headers required for downstream muxing or streaming. | | `mp4mux` / `mpegtsmux` | Multiplexes the encoded stream into an MP4 or MPEGTS container. | | `filesink` | Writes the muxed container to the file system. | ### Decode A video file is demultiplexed, the elementary stream is parsed, and the hardware decoder produces raw NV12 frames for display or downstream AI processing. Decode parameters are exposed as GStreamer element properties for direct application control. Video decode pipeline | Component | Description | | - | - | | `filesrc` | Reads the video container from the file system. | | `qtdemux` | Demultiplexes the container into separate elementary streams. | | `h264parse` / `h265parse` | Parses the encoded elementary stream before decoding. | | [`v4l2h264dec`](../plugin-reference/v4l2h264dec) | Decodes the AVC (H.264) video stream to raw NV12 frames using the V4L2 driver. | | [`v4l2h265dec`](../plugin-reference/v4l2h265dec) | Decodes the HEVC (H.265) video stream to raw NV12 frames using the V4L2 driver. | | [`waylandsink`](../plugin-reference/waylandsink) | Receives decoded NV12 frames as GBM buffers and submits them to the Weston compositor for display. | ### Related Information [Video encode and decode use cases](../sample-pipelines/multimediapipelines#2-camera-and-video-encode) [Video playback use cases](../sample-pipelines/multimediapipelines#5-video-playback-use-cases) ## Audio Architecture Audio capture and playback in QIM SDK are handled through the `pulsesrc` and `pulsesink` GStreamer plugins, which interface with the system-level PulseAudio server. The PulseAudio server in turn communicates with the ALSA driver to interact with the underlying audio hardware. Audio encode and decode use open-source GStreamer plugins (such as `flacenc`, `flacparse`, `flacdec`, `lamemp3enc`, `mpegaudioparse`, `mpg123audiodec`) combined with the PulseAudio integration, enabling a complete audio pipeline without requiring custom hardware-specific code. ### Capture The `pulsesrc` plugin captures raw PCM audio from the microphone and passes it to the PulseAudio server, which handles hardware interaction via the ALSA driver. The PCM stream can then be encoded or written directly to file. Audio capture pipeline | Component | Description | | - | - | | `pulsesrc` | Captures PCM audio samples from the microphone via the PulseAudio server. | | PulseAudio server | Interfaces with the ALSA driver to acquire audio data from the hardware. | | Encode / `filesink` | Optionally encodes and writes the captured audio to a file. | ### Playback The `pulsesink` plugin receives PCM audio data and routes it through the PulseAudio server for hardware playback. It supports both live audio sources and pre-encoded audio files after decoding. Audio playback pipeline | Component | Description | | - | - | | `filesrc` | Reads audio data from a file. | | Decode | Decodes encoded audio (e.g., MP3, FLAC, WAV) to raw PCM using open-source decoder plugins. | | `pulsesink` | Sends PCM audio data to the PulseAudio server for playback on the audio hardware. | | PulseAudio server | Interfaces with the ALSA driver to route audio to the hardware output. | ### Encode Captured PCM audio is encoded using an open-source encoder, parsed, and multiplexed into a container before being written to file. Audio encoding pipeline | Component | Description | | - | - | | `pulsesrc` | Captures PCM audio from the microphone. | | PulseAudio server | Interacts with the ALSA driver to supply raw audio data. | | Encode | Encodes PCM to a compressed format (MP3, FLAC, AAC) using open-source plugins. | | Parse | Parses the encoded bitstream before muxing. | | `mp4mux` / `mpegtsmux` | Multiplexes the encoded audio into an MP4 or MPEGTS container. | | `filesink` | Writes the container to the file system. | ### Decode An encoded audio file is read, demultiplexed, decoded back to PCM, and played back through `pulsesink`. Audio decode pipeline | Component | Description | | - | - | | `filesrc` | Reads the audio container from the file system. | | `qtdemux` | Demultiplexes the container to extract the audio elementary stream. | | Decode | Decodes the compressed audio to raw PCM using open-source decoder plugins. | | `pulsesink` | Sends the decoded PCM to the PulseAudio server for playback. | | PulseAudio server | Interfaces with the ALSA driver to route audio to the hardware output. | ### Related Information [Audio use cases](../sample-pipelines/multimediapipelines#6-audio-use-cases) ## Graphics and Display Architecture The QIM SDK uses the Wayland display protocol with the Weston compositor to manage display composition and output. Weston runs as an independent process and handles all display composition using OpenGL ES, communicating with the display hardware through the DRM/KMS subsystem. QIM SDK plugins deliver video buffers as GBM-backed DMA-buf handles. Weston receives these buffers via the Wayland protocol and composites them directly onto the display without intermediate CPU copies, maintaining the zero-copy path from camera capture or video decode through to the screen. Weston/Wayland architecture | Component | Description | | - | - | | Wayland/GLES client | GStreamer plugins such as [`waylandsink`](../plugin-reference/waylandsink) implement the Wayland client protocol to submit video buffers to Weston. | | Weston server | Implements the Wayland compositor. Uses KMS to configure the display and OpenGL ES with DRM for hardware-accelerated compositing. | | SDM back-end | Display hardware abstraction layer (HAL) that provides the DRM/KMS platform implementation for Weston. | | libGBM | Buffer allocation and sharing library. Provides a DMA-backed allocator enabling zero-copy buffer sharing between the GPU, display, and other hardware blocks. | | EGL sub-driver | Interface between GBM/EGL and the Wayland protocol, allowing GStreamer elements to share hardware-allocated buffers with the Weston compositor. | ### Related Information [Display and composition use cases](../sample-pipelines/multimediapipelines) # Test Automation Source: https://imsdkdocs.qualcomm.com/advanced/test-automation Automated testing strategies and tools for QIM SDK pipelines. This section is coming soon. Content will cover automated pipeline testing, regression frameworks, and CI/CD integration for QIM SDK. # Ubuntu platform Source: https://imsdkdocs.qualcomm.com/advanced/ubuntu-build This section explains how to compile QIM SDK plugins for Ubuntu platform. This section explains how to compile QIM SDK plugins directly on the **target device** running Ubuntu. ## Prerequisites Before you begin, ensure the following: * You have access to a Qualcomm development kit running Ubuntu Linux. * You have administrative privileges (sudo access) for installing packages. * Internet connectivity for downloading packages and source code. ## Steps to Compile and Install To download the source code and compile the sample applications, run the following commands on the Qualcomm development kit. ```bash theme={null} theme={null} sudo su ``` ```bash theme={null} theme={null} apt-add-repository -s ppa:ubuntu-qcom-iot/qcom-ppa ``` ```bash theme={null} theme={null} apt build-dep gst-plugins-qti-oss ``` ```bash theme={null} theme={null} cd /home/ubuntu ``` ```bash theme={null} theme={null} apt source gst-plugins-qti-oss ``` For example: ```bash theme={null} theme={null} cd gst-plugins-qti-oss-1.8.1+ubuntu3 ``` ```bash theme={null} theme={null} cmake -B build -S . \ -DCMAKE_INSTALL_PREFIX=/usr \ -DCMAKE_INSTALL_INCLUDEDIR=include \ -DENABLE_GST_PLUGIN_BASE=1 \ -DENABLE_GST_IMSDK_PLUGINS=1 \ -DENABLE_GST_PLUGIN_QMMFSRC=1 \ -DENABLE_GST_PYTHON_EXAMPLES=1 \ -DENABLE_GST_SAMPLE_APPS=1 \ -DENABLE_GST_TEST_FRAMEWORK=1 \ -DENABLE_GST_PLUGIN_TOOLS=1 \ -DENABLE_GST_PLUGIN_MLACLASSIFICATION=1 \ -DENABLE_GST_PLUGIN_MLVCLASSIFICATION=1 \ -DENABLE_GST_PLUGIN_MLVDETECTION=1 \ -DENABLE_GST_PLUGIN_MLVPOSE=1 \ -DENABLE_GST_PLUGIN_MLVSEGMENTATION=1 \ -DENABLE_GST_PLUGIN_MLVSUPERRESOLUTION=1 \ && cmake --build build \ && cmake --install build ``` After modifying any part of the source code, re-run step 7 to rebuild and reinstall your changes. ## Steps to build custom application Go through the [prerequisites](#prerequisites) and [Steps to Compile and Install](#steps-to-compile-and-install) section before starting this. ```bash theme={null} theme={null} cd gst-plugins-qti-oss-1.8.1+ubuntu3/gst-sample-apps && mkdir gst- ``` Follow [Step 7: Run CMake to build and install](#steps-to-compile-and-install) On-host compilation for Ubuntu is not yet available in this documentation. Please refer to the on-device compilation instructions. # Yocto platform Source: https://imsdkdocs.qualcomm.com/advanced/yocto-build This section explains how to compile QIM SDK plugins for Yocto platform. This section explains how to compile QIM SDK plugins directly on a x86 and Arm host machine running Ubuntu. ## Prerequisites Before you begin, ensure the following: * Internet connectivity for downloading packages and source code. * Sufficient space on the host to download the SDK. Upto \~5GB. ## Download and install the sdk. ```bash theme={null} theme={null} wget -O sdk.zip https://artifacts.codelinaro.org/artifactory/qli-ci/flashable-binaries/meta-qcom/qcom-distro/qcom-armv8a/x64-qli-2.0-qimsdk-2.0.0-standardsdk.zip ``` ```bash theme={null} theme={null} wget -O sdk.zip https://artifacts.codelinaro.org/artifactory/qli-ci/flashable-binaries/meta-qcom/qcom-distro/qcom-armv8a/arm-qli-2.0-qimsdk-2.0.0-standardsdk.zip ``` ```bash theme={null} theme={null} unzip sdk.zip && cd images/qcom-armv8a/sdk/ ``` ```bash theme={null} theme={null} bash qcom-distro-x86_64-qcom-multimedia-proprietary-image-armv8a-qcom-armv8a-toolchain-2.0.sh -d -y ``` ```bash theme={null} theme={null} bash qcom-distro-aarch64-qcom-multimedia-proprietary-image-armv8a-qcom-armv8a-toolchain-2.0.sh -d -y ``` ```bash theme={null} theme={null} source /environment-setup-armv8a-qcom-linux ``` Each time you wish to use the SDK in a new shell session, you need to source the environment setup script in Step 3 ## Steps to Compile To download the source code and compile the sample applications, run the following commands. ```bash theme={null} theme={null} git clone https://github.com/qualcomm/qimsdk.git cd qimsdk ``` ```bash theme={null} theme={null} cmake -B build-base -S . \ -DCMAKE_INSTALL_PREFIX=/usr \ -DENABLE_GST_PLUGIN_BASE=1 \ && cmake --build build-base ``` ```bash theme={null} theme={null} DESTDIR="${SDKTARGETSYSROOT}" cmake --install build-base --prefix /usr ``` ```bash theme={null} theme={null} cmake -B build -S . \ -DCMAKE_INSTALL_PREFIX=/usr \ -DENABLE_GST_IMSDK_PLUGINS=1 \ -DENABLE_GST_PLUGIN_MLTFLITE=1 \ -DENABLE_GST_PYTHON_EXAMPLES=1 \ -DENABLE_GST_SAMPLE_APPS=1 \ -DENABLE_GST_SAMPLE_APPS_CAMERA=1 \ -DENABLE_GST_PLUGIN_TOOLS=1 \ -DENABLE_GST_CAMERA_PLUGINS=1 \ -DENABLE_APP_BUILDER_CPP=1 \ -DENABLE_APP_BUILDER_PYTHON=1 \ && cmake --build build ``` After modifying any part of the source code, re-run step 4 to rebuild your changes. The corresponding libs from build directory can then be copied on to device. ## Steps to build custom application Go through the [prerequisites](#prerequisites) and [Steps to Compile](#steps-to-compile) section before starting this. ```bash theme={null} theme={null} cd qimsdk/gstreamer/gst-sample-apps && mkdir gst- ``` Follow [Steps to Compile](#steps-to-compile) ```bash theme={null} theme={null} #The application will be found in the below location qimsdk/build/gstreamer/gst-sample-apps/gst-/ ``` ```bash theme={null} theme={null} cd qimsdk/cpp/examples/reference-apps/ && mkdir ``` Follow [Steps to Compile](#steps-to-compile) ```bash theme={null} theme={null} #The application will be found in the below location qimsdk/build/cpp/examples/reference-apps// ``` On-device compilation for Yocto is not yet available in this documentation. Please refer to the on-host compilation instructions. # Coding Agent Source: https://imsdkdocs.qualcomm.com/app-builder-coding-agent Generate QIM SDK applications with AI coding agent skills QIM SDK provides AI coding skills that generate complete applications from a natural-language description of your pipeline. Pick the coding agent that matches your development path, then follow its guide to generate your first application with an AI coding agent (Claude Code, Cursor, Codex, etc)
Generate GStreamer C apps and gst-launch scripts using QIM SDK plugins.
[Go to Coding Agent →](/app-builder/gstreamer-coding-agent)
Generate high-performance C++ applications on the QIM SDK C++ API.
[Go to Coding Agent →](/app-builder/cpp-coding-agent)
Generate Python applications on the QIM SDK Python API for prototyping.
[Go to Coding Agent →](/app-builder/python-coding-agent)
The skills are available at [qimsdk-agentic-skills](https://github.com/qualcomm/qimsdk-agentic-skills/) # Quickstart Source: https://imsdkdocs.qualcomm.com/app-builder-quickstart Choose a development path and jump into its quickstart guide QIM SDK supports three ways to build AI and multimedia applications. Pick the path that best fits your workflow, then follow its quickstart guide to build your first pipeline.
Build pipelines with native GStreamer elements and QIM SDK plugins.
[Go to Quickstart →](/quickstart)
Build high-performance applications with the fluent C++ App Builder API.
[Go to Quickstart →](/app-builder/cpp-quickstart)
Build applications quickly with the high-level Python App Builder API.
[Go to Quickstart →](/app-builder/python-quickstart)
# Sample Applications Source: https://imsdkdocs.qualcomm.com/app-builder-sample-applications Explore ready-to-run sample applications for each development path QIM SDK provides ready-to-run sample applications for each development path. Pick the path that matches your workflow to browse its applications, or go to GitHub to view the source code.
Sample applications built with the GStreamer App Builder.
[View applications →](/sample-application/ai-sample-applications) [View source on GitHub →](https://github.com/qualcomm/qimsdk/tree/main/gstreamer/gst-sample-apps)
Reference applications built with the C++ App Builder API.
[View applications →](/app-builder/cpp-reference-applications) [View source on GitHub →](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps)
Reference applications built with the Python App Builder API.
[View applications →](/app-builder/python-reference-applications) [View source on GitHub →](https://github.com/qualcomm/qimsdk/tree/main/python/examples/reference-apps)
# SDK Architecture and API Reference Source: https://imsdkdocs.qualcomm.com/app-builder/cpp-architecture-api-reference Design and API details of the QIM SDK Pipeline SDK (C++ and Python) ## Pipeline SDK Design **Overview** This SDK delivers a streamlined, modern interfaces, engineered to facilitate the rapid development, execution, and management of sophisticated multimedia pipelines. By implementing a high-level abstraction layer, the SDK encapsulates complex underlying GStreamer based mechanisms — including element negotiation, signal management, event loops, and state transitions — allowing engineers to prioritize core application logic over boilerplate framework intricacies. **Key Capabilities** The wrapper empowers developers to architect advanced multimedia and AI-enhanced workflows with precision and efficiency. It supports the seamless integration of diverse components, such as video capture sources, signal processing nodes, machine learning inference engines, graphical overlays, and rendering outputs. Uniquely designed to decouple implementation from the underlying framework, the SDK eliminates the need for deep GStreamer expertise or compile-time dependencies on GStreamer headers, ensuring a clean, robust, and accessible development experience. Introduction ## APIs **Public APIs:** * `Pipeline` — build & control pipelines fluently. * `Element` — generic element wrapper with variadic property setting, and state control. \[it can be instantiated at create time as well as at runtime to update the pipeline.] Some element has special functions which can be controlled by dedicated elements types. Those elements derived from `Element` can be use as `Element` if special functions are not used. * `AppSrc` — app-facing source for zero-copy buffer pull. \[Providing the buffer from application to pipeline using standard lambdas, push APIs, EOS etc] * `AppSink` — app-facing sink for zero-copy buffer push \[Providing the buffer from pipeline to application with zero copy] * `CamSrc` - Build-in camera element which expose API for capture images \[provides functionality to take snapshots at runtime] * `MLVConverter` - ML video converter (`qtimlvconverter`). Expose API for custom pre-processing implementation at application utilizing standard lambdas. * `MLPostprocess` - ML postprocess element (`qtimlpostprocess`). Expose API for custom post-processing implementation at application utilizing standard lambdas. The callback is typed per ML task (classification, object detection, pose estimation, depth estimation, segmentation, raw tensors). * `MLVideoONNXBin` - ONNX inference bin (`qtimlvideoonnxbin`). Expose API for custom pre-processing and post-processing implementation at application utilizing standard lambdas. * `MLVideoQNNBin` - QNN inference bin (`qtimlvideoqnnbin`). Expose API for custom pre-processing and post-processing implementation at application utilizing standard lambdas. * `MLVideoSNPEBin` - SNPE inference bin (`qtimlvideosnpebin`). Expose API for custom pre-processing and post-processing implementation at application utilizing standard lambdas. * `MLVideoTFLiteBin` - TFLite inference bin (`qtimlvideotflitebin`). Expose API for custom pre-processing and post-processing implementation at application utilizing standard lambdas. * `Buffer` — opaque buffer that bridges Native Buffer with timestamp helpers and zero-copy paths. * `StreamFilter` and derived concrete stream helpers: `VideoFilter` (all RAW formats - YUV, RGB, bayer), `H264Filter`, `TensorFilter`, `TextFilter`, `AudioFilter`, `ImageFilter`. these filters are used to set characteristics of the streams. * Logging controls — `SetImsdkLogLevel()` and `SetImsdkGstLogMode()` select the SDK's log verbosity and whether GStreamer's output is simplified or passed through raw. See [Logging and Diagnostics](/app-builder/cpp-logging). **Reference flows** ### Pipeline **Purpose** A convenient, exception-safe wrapper around a `GstPipeline` that: * Builds pipelines using element factory names or pre-constructed wrapper elements. * Manages pipeline lifecycle and execution state. * Performs delayed linking for dynamic/request-pad elements automatically via `pad-added`. **Key Responsibilities** * Create and own the underlying `GstPipeline` instance. * Add elements, assign properties, and insert caps filters. * Link elements (immediately when possible, deferred otherwise). * Manage the full pipeline lifecycle (`prepare`, `start`, `wait`, `stop`). * Install bus watches to handle `ERROR`, `EOS`, and state transitions. * Integrate with runtime shutdown notifications. **A minimal pipeline** Add the elements in the order data flows through them, then run: ```cpp theme={null} Pipeline pipeline("my-pipeline"); pipeline .add("filesrc", "src", "location", "/home/user/media/video.mp4") .add("qtdemux", "demux") .add("h264parse", "parse") .add("v4l2h264dec", "decoder", "output-io-mode", 4, "capture-io-mode", 4) .add_stream_filter("vf", VideoFilter().format("NV12")) .add("waylandsink", "display", "fullscreen", true) .execute(); ``` `execute()` starts the pipeline, blocks until EOS/error/termination, and tears it down. **Adjacent elements are linked in add order.** In the example above `src` → `demux` → `parse` → `decoder` → `vf` → `display` are linked simply because that is the order of the `add()` calls, so a linear graph needs no explicit linking. Use [`link()`](#branching-with-explicit-links) when the graph is not linear — a `tee` splitting into a display branch and an inference branch, or a `qtimetamux` rejoining them. `qtdemux` is an example of the delayed linking described above: its src pads appear only once the container has been parsed, so the `demux` → `parse` link is completed automatically when the pad shows up. No `pad-added` handling is needed in application code. **Building the pipeline** | Method | Description | | - | - | | `Pipeline(const std::string &name)` | Creates an empty pipeline. `name` is the `GstPipeline` name, used in log messages. | | `Pipeline(const std::string &name, const std::string &config)` | Builds a complete pipeline from a YAML description. `config` is the YAML **content**, not a path. See [Describing a pipeline in YAML](#describing-a-pipeline-in-yaml). | | `Pipeline &add(String factory, String name, ...)` | Creates an element from the factory name and adds it under a unique instance name. Trailing arguments are property key/value pairs, applied immediately. | | `Pipeline &add(const Element &element)` | Adds an externally constructed element. Required for the specialized wrappers (`AppSrc`, `AppSink`, `CamSrc`, `MLVConverter`, `MLPostprocess`, the ML bins) so callbacks can be attached before the element joins the pipeline. | | `Pipeline &add_stream_filter(String name, const StreamFilter &filter)` | Inserts a caps filter between the previous and next element. When neighbouring elements have several formats in common, this fixates the one you want instead of leaving the choice to caps negotiation. See [StreamFilter](#streamfilter). | | `Pipeline &link(names...)` | Links the named elements in the given order. Needed only for non-linear graphs. | The optional key/value pairs passed to `add()` are the element's properties, and they are validated as soon as the element is added. Setting a property the element does not have throws `std::invalid_argument` naming the property, the element instance, and the element type — so a typo surfaces immediately at that `add()` call instead of being silently ignored: ```cpp theme={null} // "fulscreen" is not a waylandsink property -> throws std::invalid_argument pipeline.add("waylandsink", "display", "fulscreen", true); ``` **Branching with explicit links** Add order cannot express a branch, so name the chains explicitly. Element instance names are the identifiers. ```cpp theme={null} pipeline .add("filesrc", "src", "location", path) .add("qtdemux", "demux") .add("h264parse", "parse") .add("v4l2h264dec", "decoder", "output-io-mode", 4, "capture-io-mode", 4) .add_stream_filter("vf", VideoFilter().format("NV12")) .add("tee", "split") .add("queue", "q1") .add("qtimlvconverter", "preprocessing") .add("qtimltflite", "inferencing", "model", model_path) .add("qtimlpostprocess", "postprocessing", "module", "yolov8") .add_stream_filter("mlf", TextFilter()) .add("qtimetamux", "mlmuxer") .add("qtivoverlay", "overlay") .add("waylandsink", "display", "fullscreen", true) // trunk, up to the tee .link("src", "demux", "parse", "decoder", "vf", "split") // branch 1: raw video to the metadata muxer .link("split", "mlmuxer") // branch 2: inference path, rejoining at the muxer .link("split", "q1", "preprocessing", "inferencing", "postprocessing", "mlf", "mlmuxer", "overlay", "display"); ``` Elements whose src pads are `SOMETIMES` or `REQUEST` — `qtdemux`, `tsdemux`, `tee`, `qtivsplit` — cannot be linked at construction time, because the pads do not exist yet. The pipeline detects this from the pad templates, defers the link, and completes it from a `pad-added` handler it installs itself. Request pads on the sink side (for example `qtimetamux`, `qtivcomposer`) are requested and released automatically. No `pad-added` callback is needed in application code. A link between two elements that both have only static pads and no common caps fails immediately with `std::runtime_error` naming both elements. A deferred link that never resolves surfaces at run time instead; on error the pipeline logs which pads stayed unlinked. **Running the pipeline** A pipeline is always in one of three states, and the lifecycle methods are the transitions between them: Introduction | State | Meaning | | - | - | | `NULL` | No resources allocated. Initial state, and the state after `stop()`. Devices are closed, buffer pools released. | | `PAUSED` | Elements are allocated and caps are negotiated between linked pads, but the clock is not running and no data flows. Sinks have pre-rolled. This is the state in which elements can be added, removed, or relinked. | | `PLAYING` | The clock is running and buffers flow through the graph. | `execute()` performs the whole `NULL → PLAYING → NULL` cycle, so most applications never call the individual transitions. `prepare()`, `activate()`, and `deactivate()` — the transitions through `PAUSED` shown above — are covered in [Advanced state control](#advanced-state-control). | Method | Description | | - | - | | `void execute()` | `start()`, `wait()`, `stop()` in sequence. The normal entry point. | | `Pipeline &start()` | Sets the pipeline to `PLAYING`. Non-blocking: the state change is asynchronous and this returns without waiting for it. Throws `std::runtime_error` if the transition fails. | | `Pipeline &wait()` | Blocks until the pipeline is done: `EOS` on the bus, `ERROR` on the bus, `stop()` from another thread, or `SIGINT`/`SIGTERM`. | | `Pipeline &stop()` | Sets the pipeline to `NULL` and releases resources. Blocks until teardown completes. | | `Pipeline &eos(bool enabled)` | When enabled, `stop()` first sends an end-of-stream event through the pipeline and waits for it to come back from the sinks, so that data already inside the pipeline is processed before teardown. Off by default. See [Draining the pipeline on shutdown](#draining-the-pipeline-on-shutdown). | `start()` is non-blocking, so it must be paired with `wait()` (or your own blocking logic). A `main()` that calls `start()` and returns destroys the pipeline before buffers flow. To run the pipeline while the application does other work, split `execute()` apart: ```cpp theme={null} pipeline.start(); // returns immediately // ... application work, runtime property changes, snapshots ... pipeline.wait(); // block until EOS / error / Ctrl+C pipeline.stop(); ``` **Draining the pipeline on shutdown** By default `stop()` tears the pipeline down as fast as it can. Every element goes to `NULL` immediately and whatever buffers are still in flight — queued between elements, held inside an encoder, not yet written by a sink — are discarded. This is usually what you want: the pipeline stops without delay. Some pipelines must not lose those buffers, and for them `eos(true)` changes the shutdown sequence. Instead of stopping straight away, `stop()` sends an end-of-stream event into the pipeline. The event travels downstream element by element; each element finishes processing the data it has already accepted, passes the event on, and the sinks report it back on the bus. Only once that end-of-stream notification comes back does `stop()` tear the pipeline down. Nothing that had already entered the pipeline is dropped. The trade-off is shutdown latency: `stop()` now blocks until the last buffer has made it through the whole graph, which takes as long as the remaining data needs. Use `eos(true)` when the tail of the stream is part of the result: * **Muxed recordings.** Containers such as MP4 and MKV are written by a muxer (`mp4mux`, `matroskamux`) that writes its index and header metadata — the `moov` atom, in MP4's case — only on end-of-stream. Cut it off early and the file is left incomplete, often unplayable rather than merely short. * **Encoders with buffered frames.** Encoders keep several frames in flight for B-frames, lookahead, and similar optimizations. End-of-stream is what makes the encoder flush those frames downstream; without it the tail of the recording is simply missing. * **Any sink where losing the final buffered data is unacceptable** — file sinks, muxed outputs, and applications that expect to receive every frame through `AppSink`. Not every sink needs draining, though. A live network stream can usually just be cut off, and a receiver will not care that the last few buffers never arrived. The question is not whether a sink writes somewhere, but whether anything downstream depends on the data still sitting in the pipeline. Leave `eos()` off (the default) when the newest data is all that matters and stopping promptly is preferable — live preview to a display, a camera feed on screen, or writing self-contained files where each buffer is already complete on its own (JPEG frames through `multifilesink`, for example, which needs no finalization). ```cpp theme={null} Pipeline pipeline("cam-encoder-pipeline"); pipeline .add("qtiqmmfsrc", "source") .add_stream_filter("vf", VideoFilter().format("NV12") .resolution(1920, 1080) .framerate(30)) .add("v4l2h264enc", "encoder", "output-io-mode", "dmabuf-import", "capture-io-mode", "dmabuf") .add("h264parse", "parser") .add("mp4mux", "muxer") .add("filesink", "sink", "location", HOME_PATH + "/media/output.mp4") .eos(true) // drain, so mp4mux can finalize the container .execute(); ``` If the pipeline has already reached end-of-stream on its own — a file source that ran out of data, for instance — `stop()` detects it and tears down immediately instead of sending a second event and waiting again. Enabling `eos(true)` therefore costs nothing for pipelines that end naturally; it only matters when you stop a pipeline that is still running. **Advanced state control** For reconfiguring a loaded pipeline — adding, removing, or relinking elements without returning to `NULL`. | Method | Description | | - | - | | `Pipeline &prepare()` | `NULL` → `PAUSED`. Allocates resources and negotiates caps without running the clock. Blocking. | | `Pipeline &activate()` | `PAUSED` → `PLAYING`. | | `Pipeline &deactivate()` | `PLAYING` → `PAUSED`. Resources stay allocated. | **Accessing elements at run time** | Method | Description | | - | - | | `Element get(String name)` | Returns a wrapper for the named element, for runtime property changes and per-pad configuration. Throws `std::runtime_error` if the name is not in the pipeline. | | `T get(String name)` | Returns a typed wrapper, exposing that type's element-specific API — for example `get("source")` for the image-capture calls. Use it when the element was added by factory name but its specialized interface is needed. | **Runtime property and per-pad configuration** — `get()` reaches any element by name while the pipeline is loaded or playing: ```cpp theme={null} // Change a property at run time. pipeline.get("display").set("fullscreen", false); // Per-pad properties on a request pad of qtivcomposer. pipeline.get("composer").input(1).set("alpha", 0.5); ``` **Typed access to an element's own API** — `get()` returns the specialized wrapper, so element-specific calls become available on an element that was added by factory name: ```cpp theme={null} // qtiqmmfsrc was added as "source"; CamSrc exposes its capture API. auto cam = pipeline.get("source"); cam.image_capture(); ``` See the sample applications for complete image-capture pipelines. `get()` returns an element wrapper **by value**; each call yields a new wrapper referencing the same `GstElement`. Bind it with `auto`, not `auto&`. **Describing a pipeline in YAML** The graph — elements, properties, stream filters, and links — can be described in YAML instead of in code. This separates *what the pipeline is* from *what the application does with it*: the same binary can run a different topology, a different model, or a different source by pointing it at another configuration file. That makes it useful where the pipeline is not fixed at build time: * **Deploying one binary across variants** — the same application against a built-in camera, a USB camera, or an RTSP stream, switching source and format per device. * **Tuning without rebuilding** — resolution, framerate, model path, or delegate changed on the target, which matters when a rebuild means a full cross-compile and reflash. * **Keeping topology under review** — the pipeline shape lives in a file that can be diffed and versioned on its own. Loading from YAML does not give up application logic: the elements are ordinary pipeline members afterwards, so `get()` and `get()` reach them as usual. ```yaml theme={null} pipeline: elements: - type: filesrc name: src location: ~/media/video.mp4 - type: qtdemux name: demux - type: h264parse name: parse - type: v4l2h264dec name: decoder output-io-mode: 4 capture-io-mode: 4 - type: filter name: videofilter video: format: NV12 - type: waylandsink name: display fullscreen: true ``` Everything lives under a single root key, `pipeline:`, which takes three independent child keys: ```yaml theme={null} pipeline: eos: true # optional — EOS-on-shutdown, same as eos(true) in code elements: # required — the elements, in add order - type: ... links: # optional — explicit links, for non-linear graphs - [ ... ] ``` | Key | Required | Description | | - | - | - | | `eos` | No | Boolean. Equivalent to calling [`eos(bool)`](#draining-the-pipeline-on-shutdown). Defaults to false when omitted. | | `elements` | Yes | Sequence of elements. Add order is link order, exactly as with `add()`. | | `links` | No | Sequence of chains, each a list of element names. Equivalent to one `link()` call per entry. Omit it for linear pipelines. | Within `elements:`, each entry needs `type` (the factory name) and `name` (the instance name); every remaining key is applied as a property of that element. An entry with `type: filter` inserts a stream filter instead, described by a `video:`, `image:`, `h264:`, `tensor:`, `text:`, `audio:`, or raw `caps:` block. Any of those blocks may carry an `add:` sequence of `key=value` strings, appended to the filter's caps through [`add()`](#videofilter) — the same escape-hatch the typed filters expose in code. It is accepted both inside the filter block and alongside it: ```yaml theme={null} - type: filter name: videofilter video: format: NV12 add: - colorimetry=bt709 ``` `links:` mirrors [`link()`](#branching-with-explicit-links) — one sequence per chain, so the branching pipeline shown earlier becomes: ```yaml theme={null} links: - [split, mlmuxer] - [source, videostream, split, q1, preprocessing, inferencing, postprocessing, mlf, mlmuxer, overlay, display] ``` The constructor takes the YAML content, so read the file first: ```cpp theme={null} std::ifstream input(config_path); std::ostringstream buffer; buffer << input.rdbuf(); Pipeline pipeline("demo-pipeline", buffer.str()); pipeline.execute(); ``` The names in the configuration are the handles the application uses afterwards. Given a config that declares a `qtimlpostprocess` element named `postprocessing`, the application can claim its post-processing without the topology being hard-coded: ```cpp theme={null} Pipeline pipeline("demo-pipeline", buffer.str()); auto postprocessing = pipeline.get("postprocessing"); postprocessing.set_handler([](const MLFrame& frame, const MLParam& params, MLDetections& detections) { return decode_detection(frame, params, detections); }); pipeline.execute(); ``` ### Element **Purpose** Generic wrapper around a `GstElement` with fluent, variadic property setting. Can be instantiated at create time or used at runtime to update a live pipeline. Serves as the base class for all specialized element types. **Key Responsibilities** * Create GStreamer elements from a factory name. * Provide fluent, variadic property setting for all common types and `StreamFilter` (caps). * Implement pad linking/unlinking with error checking. * Support state publication to pipeline (`sync`, `deactivate`, `stop`). * Expose per-pad configuration through `Port`. * Serve as the base class for specialized wrappers. ```cpp theme={null} Element display("waylandsink", "display"); display.set("fullscreen", true); display.set("sync", false); ``` Any element can be constructed directly and handed to `Pipeline::add()`, or reached later by name with `Pipeline::get()`. Elements cannot be copied — only moved — so pass them by reference or move them where ownership has to change. | `qti::Element` | Description | | - | - | | `Element(const std::string &factory, const std::string &name = {})` | Creates the element from a GStreamer factory name. The instance name is optional but recommended, since it is how the element is addressed in `link()`, `get()`, and log messages. | | `Element &set(String prop, Value value, ...)` | Sets one or more properties. Accepts any number of key/value pairs in a single call and returns `*this`, so calls can be chained. | | `Element &link(Element &downstream, String src_pad = "src", String sink_pad = "sink")` | Links this element's src pad to `downstream`'s sink pad. Throws `std::runtime_error` naming both pads if the link fails. | | `Element &unlink(Element &downstream, String src_pad = "src", String sink_pad = "sink")` | Unlinks the named pad pair between this element and `downstream`. | | `Element &unlink(String src_pad = "src")` | Unlinks whatever is connected to the given src pad, without naming the peer. | | `Element &sync()` | Synchronizes the element's state with its parent pipeline's state. Call this after adding an element to a pipeline that is already running. | | `Element &deactivate()` | Moves the element to `PAUSED`. | | `Element &stop()` | Moves the element to `NULL`. | | `Port input(unsigned int id)` | Returns the sink pad at the given index, for per-pad configuration. | | `Port input(String name_or_type, unsigned int id)` | Returns the sink pad matching a pad-template name and index. | | `Port output(unsigned int id)` | Returns the src pad at the given index. | | `Port output(String name_or_type, unsigned int id)` | Returns the src pad matching a pad-template name and index. | **Setting properties** `set()` is variadic, so a whole configuration can go in one call, and property names are validated immediately — an unknown property throws `std::invalid_argument` naming the property and the element type rather than being silently ignored: ```cpp theme={null} Element decoder("v4l2h264dec", "decoder"); decoder.set("output-io-mode", 4, "capture-io-mode", 4); ``` Besides the usual scalar types and strings, `set()` accepts a `StreamFilter` wherever an element takes a caps property, and plain enums (converted to their underlying integer): ```cpp theme={null} AppSrc appsrc("appsrc"); appsrc.set("caps", VideoFilter().format("NV12") .resolution(1920, 1080) .framerate(30)); ``` **Per-pad configuration with Port** Some elements are configured per pad rather than per element — a compositor's inputs each have their own position, size, and alpha. `input()` and `output()` return a `Port` for one pad, which carries the same fluent, validated `set()`: ```cpp theme={null} // Second input of qtivcomposer: blend at 50% alpha. pipeline.get("composer").input(1).set("alpha", 0.5); ``` | `qti::Port` | Description | | - | - | | `Port &set(String prop, Value value, ...)` | Sets one or more pad properties. Validated like `Element::set()`, and additionally accepts `std::vector` and `{a, b}` initializer lists for array-valued pad properties such as positions and dimensions. | The `id` selects which pad, and `name_or_type` narrows it to a pad template when an element has several families of pads. A template containing `%u` is formatted with the index (`"image_%u"` with `id` 0 gives `image_0`); otherwise the name and index are joined with an underscore (`"sink"` with `id` 1 gives `sink_1`). Without a name, the pad is matched by numeric suffix first and by position among same-direction pads second. An unresolvable pad throws `std::runtime_error`. A `Port` refers to a pad of a live element and cannot be copied, only moved. Get it, set the properties, and let it go — as in the one-liner above — rather than storing it beyond the lifetime of the element. **Runtime changes** Because `Pipeline::get()` returns an `Element` for any element in the pipeline, the same `set()` works while the pipeline is playing: ```cpp theme={null} pipeline.get("display").set("fullscreen", false); ``` `sync()`, `deactivate()`, and `stop()` exist for reconfiguring a live pipeline. An element added to an already-running pipeline starts in `NULL` while the pipeline is `PLAYING`, so it will not process data until its state is brought up to match — that is what `sync()` does. `deactivate()` and `stop()` take a single element down without touching the rest of the graph. Runtime relinking is an advanced use of the API. Pad linking, unlinking, and per-element state changes on a live pipeline must respect GStreamer's rules about when pads may be changed; the wrappers report failures as exceptions but do not remove the need to get the sequence right. ### AppSrc **Purpose** `AppSrc` wraps the `appsrc` element, the entry point for feeding application-produced buffers into a pipeline. It manages the `need-data`/`enough-data` signal plumbing so the application only has to supply buffers, either from a callback or by pushing them directly. **Key Responsibilities** * Validate that the wrapped element is indeed an `appsrc`. * Register and dispatch the underlying `need-data` and `enough-data` signals. * Let the application supply buffers on demand through `set_buffer_producer()`, or push them directly with `push_buffer()`. * Signal end-of-stream on this element's output. * Manage lifetime of the connected signal handlers. ```cpp theme={null} AppSrc appsrc("src"); appsrc.set("is-live", true) .set("block", true) .set("format", AppSrc::Format::TIME) .set("do-timestamp", true) .set("caps", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)); ``` | `qti::AppSrc` | Description | | - | - | | `AppSrc(const std::string &name = {})` | Creates an `appsrc` element with an optional instance name. | | `AppSrc &set(String prop, Value value, ...)` | Sets one or more properties, same as `Element::set()`. | | `AppSrc &set_buffer_producer(std::function producer)` | Registers a callback invoked whenever appsrc needs more data. Fill the buffer and return `true` to push it, `false` to skip this round. | | `AppSrc &set_enough_handler(std::function enough)` | Registers a callback invoked when appsrc reports it is holding enough queued data. | | `bool push_buffer(qti::Buffer &buffer)` / `bool push_buffer(qti::Buffer &&buffer)` | Pushes a buffer into the stream directly, outside the `need-data` callback. Returns `true` once it is accepted downstream. | | `void end_of_stream()` | Sends an end-of-stream event on this element's output. | `AppSrc::Format` mirrors appsrc's own `format` property (`DEFAULT`, `BYTES`, `TIME`, `BUFFERS`, `PERCENT`); `TIME`, used above, is the usual choice for timestamped media. `push_buffer()` transfers a buffer's underlying `GstBuffer` without copying when the `Buffer` already owns one — which is exactly the case for a buffer just pulled from an `AppSink`. A `Buffer` filled with raw data instead (as inside a `set_buffer_producer()` callback) is copied into a new `GstBuffer` on push. ### AppSink **Purpose** `AppSink` wraps the `appsink` element, the exit point that delivers pipeline output back to application code. It pulls samples through appsink's action signals and hands them to the application as `Buffer`. **Key Responsibilities** * Validate that the wrapped element is indeed an `appsink`. * Pull samples via appsink's action signals and convert them into `Buffer`. * Deliver each buffer to the application through `set_buffer_consumer()`. * Deliver the first (preroll) sample separately through `set_preroll_handler()`. * Notify the application of end-of-stream through `set_eos_handler()`. * Manage lifetime of the connected signal handlers. ```cpp theme={null} AppSink appsink("sink"); appsink.set("emit-signals", true) .set("max-buffers", 5) .set("drop", true) .set_buffer_consumer([](Buffer buf) { // consume buf.data(), buf.size(), buf.pts() }); ``` | `qti::AppSink` | Description | | - | - | | `AppSink(const std::string &name = {})` | Creates an `appsink` element with an optional instance name. | | `AppSink &set(String prop, Value value, ...)` | Sets one or more properties, same as `Element::set()`. | | `AppSink &set_buffer_consumer(std::function consumer)` | Registers a callback invoked for every buffer pulled from the pipeline. | | `AppSink &set_preroll_handler(std::function preroll)` | Registers a callback invoked for the first (preroll) sample only; return `false` to report an error instead of accepting it. | | `AppSink &set_eos_handler(std::function eos)` | Registers a callback invoked when appsink receives end-of-stream. | **Bridging two pipelines** Because `AppSink` and `AppSrc` are ordinary elements, they can sit in two independent `Pipeline` objects and be wired together in application code — useful when one graph produces frames (say, from a camera) and another consumes them (say, encoding or display) on its own clock: ```cpp theme={null} AppSrc appsrc("src"); appsrc.set("is-live", true) .set("block", true) .set("format", AppSrc::Format::TIME) .set("do-timestamp", true) .set("caps", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)); AppSink appsink("sink"); appsink.set("emit-signals", true) .set("max-buffers", 5) .set("drop", true) .set_buffer_consumer([&](Buffer b) { appsrc.push_buffer(std::move(b)); }); Pipeline pipeline1("p1"); pipeline1.add("videotestsrc", "testsrc", "is-live", true, "pattern", "ball") .add_stream_filter("videostream", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) .add(appsink); Pipeline pipeline2("p2"); pipeline2.add(appsrc) .add("waylandsink", "display", "fullscreen", true); ``` Each buffer handed to `set_buffer_consumer()` on `pipeline1` is pushed straight into `pipeline2` through `appsrc.push_buffer()`, without a copy. ### CamSrc **Purpose** `CamSrc` wraps the `qtiqmmfsrc` camera source element and exposes its still/burst image-capture action signals as ordinary method calls, so an application can trigger a snapshot without touching GStreamer signals directly. **Key Responsibilities** * Validate that the wrapped element is indeed a `qtiqmmfsrc`. * Trigger still or burst image capture through `image_capture()`. * Cancel any pending capture request through `cancel_capture()`. | `qti::CamSrc` | Description | | - | - | | `CamSrc(const std::string &name = {})` | Creates a `qtiqmmfsrc` element with an optional instance name. | | `bool image_capture(unsigned int count = 1)` | Captures `count` still images. | | `bool image_capture(CaptureMode mode, unsigned int count = 1)` | Captures `count` images using `CaptureMode::kStill` or `CaptureMode::kBurst`. | | `bool image_capture(CaptureMode mode, unsigned int count, const std::vector &metadata_ptrs)` | Same as above, attaching a per-request metadata payload to each capture. | | `bool cancel_capture()` | Cancels any capture requests still pending. | A running pipeline built from a plain factory name still has a `qtiqmmfsrc` element in it; use `Pipeline::get()` to reach it as a `CamSrc` and call its capture API: ```cpp theme={null} auto cam = pipeline.get("source"); cam.image_capture(); ``` See `examples/test_camera_and_capture` for a complete pipeline that branches a `qtiqmmfsrc` into a live display and a `multifilesink` for captured images. ### MLVConverter **Purpose** `MLVConverter` wraps the `qtimlvconverter` element, which converts raw video frames into the input tensor format expected by a model. Beyond the properties available on the generic `Element`, it lets the application replace the built-in conversion with its own implementation through a lambda. **Key Responsibilities** * Validate that the wrapped element is a `qtimlvconverter`. * Convert incoming video frames into model input tensors. * Optionally delegate the conversion to an application-supplied pre-process callback. * Provide fluent, variadic property setting that returns `MLVConverter&` for chaining. **Pre-process callback** The callback receives the source frames as `MLVideoBlits` (each entry carries the image planes plus the source quadrilateral, destination rectangle, alpha and rotation) and must fill the tensors of the `MLFrame` output. Returning `false` marks the conversion as failed. ```cpp theme={null} using TensorsPreprocessCallback = std::function; ``` Set `engine` to `none` to disable the internal pre-processing path when you supply your own callback — otherwise the built-in conversion runs instead. **Usage** ```cpp theme={null} MLVConverter preprocessing("preprocessing"); // engine=none disables internal preprocessing path in qtimlvconverter. preprocessing.set("engine", "none"); preprocessing.set_handler( [](const MLVideoBlits& blits, MLFrame& output) { if (blits.entries.empty() || output.tensors.empty()) return false; const MLVideoBlit& blit = blits.entries.front(); return convert_nv12_to_nhwc_i8(blit.image, &blit, output.tensors.front()); }); pipeline.add(preprocessing); ``` | `qti::MLVConverter` | Description | | - | - | | `MLVConverter(const std::string &name = {})` | Creates a `qtimlvconverter` element with an optional instance name. | | `MLVConverter &set(String prop, Value value, ...)` | Sets one or more properties, same as `Element::set()`. | | `MLVConverter &set_handler(TensorsPreprocessCallback handler)` | Registers the external pre-process callback. Requires `engine=none` to take effect. | ### MLPostprocess **Purpose** `MLPostprocess` wraps the `qtimlpostprocess` element, which decodes model output tensors into ML metadata attached to the frame. It lets the application replace the built-in decoding module with its own implementation through a lambda. **Key Responsibilities** * Validate that the wrapped element is a `qtimlpostprocess`. * Decode output tensors into ML metadata (classifications, detections, poses, depth maps, segmentations), or pass the raw tensors through for a further stage. * Optionally delegate the decoding to an application-supplied post-process callback. * Select the callback type automatically from the lambda's third parameter. * Provide fluent, variadic property setting that returns `MLPostprocess&` for chaining. **Post-process callbacks** `set_handler()` is overloaded once per ML task. The overload is selected by the type of the third parameter of the callback, so the same method name registers any of the supported result types. Each callback receives the model output tensors as `MLFrame`, the element parameters as `MLParam`, and an output container to fill. Returning `false` marks the frame as failed. | Callback type | Signature | Output | | - | - | - | | `ClassificationPostprocessCallback` | `bool(const MLFrame&, const MLParam&, MLClassifications&)` | Class name, confidence, color | | `ObjectDetectionPostprocessCallback` | `bool(const MLFrame&, const MLParam&, MLDetections&)` | Bounding boxes, confidence, landmarks | | `PoseEstimationPostprocessCallback` | `bool(const MLFrame&, const MLParam&, MLPoses&)` | Keypoints and keypoint links | | `DepthEstimationPostprocessCallback` | `bool(const MLFrame&, const MLParam&, MLDepthMaps&)` | Per-cell depth values and colors | | `SegmentationPostprocessCallback` | `bool(const MLFrame&, const MLParam&, MLSegmentations&)` | Per-cell labels and colors | | `TensorsPostprocessCallback` | `bool(const MLFrame&, const MLParam&, MLFrame&)` | Raw output tensors, for daisy-chaining into a next stage | Use `MLParam::get()` to read element parameters inside the callback. For example `params.get("input-tensor-region", region)` returns the `Region` the frame was scaled into, which is needed to map model coordinates back to frame coordinates. **Usage** ```cpp theme={null} static const std::vector labels = load_labels(HOME_PATH + "/labels/resnet101.json"); MLPostprocess postprocessing("postprocessing"); postprocessing .set("results", 1) .set_handler([](const MLFrame& frame, const MLParam& params, MLClassifications& classifications) { return decode_top1_classification(frame, params, labels, classifications, /*confidence_threshold=*/51.0f, ""); }); pipeline.add(postprocessing); ``` | `qti::MLPostprocess` | Description | | - | - | | `MLPostprocess(const std::string &name = {})` | Creates a `qtimlpostprocess` element with an optional instance name. | | `MLPostprocess &set(String prop, Value value, ...)` | Sets one or more properties, same as `Element::set()`. | | `MLPostprocess &set_handler(ClassificationPostprocessCallback handler)` | Registers a callback that decodes tensors into classifications. | | `MLPostprocess &set_handler(ObjectDetectionPostprocessCallback handler)` | Registers a callback that decodes tensors into detections. | | `MLPostprocess &set_handler(PoseEstimationPostprocessCallback handler)` | Registers a callback that decodes tensors into poses. | | `MLPostprocess &set_handler(DepthEstimationPostprocessCallback handler)` | Registers a callback that decodes tensors into depth maps. | | `MLPostprocess &set_handler(SegmentationPostprocessCallback handler)` | Registers a callback that decodes tensors into segmentations. | | `MLPostprocess &set_handler(TensorsPostprocessCallback handler)` | Registers a callback that produces raw output tensors, for chaining into another ML stage. | ### ML inference bins **Purpose** The ML video bins wrap a full pre-process → inference → post-process chain into a single element, so a whole AI branch can be added to the pipeline as one node instead of being assembled from separate elements and queues. Four bins are available, one per inference runtime: | QIM SDK C++ class | GStreamer element | Runtime | | - | - | - | | `MLVideoONNXBin` | `qtimlvideoonnxbin` | ONNX Runtime | | `MLVideoQNNBin` | `qtimlvideoqnnbin` | Qualcomm® AI Engine Direct (QNN) | | `MLVideoSNPEBin` | `qtimlvideosnpebin` | Snapdragon® Neural Processing Engine (SNPE) | | `MLVideoTFLiteBin` | `qtimlvideotflitebin` | LiteRT (TensorFlow Lite) | All four expose an identical API and differ only in the wrapped element and the runtime-specific properties. Each inherits from both the pre-process and the post-process base, so it accepts a custom pre-process callback, a custom post-process callback, or both. **Key Responsibilities** * Validate that the wrapped element matches the expected bin factory. * Run pre-processing, inference, and post-processing inside a single element. * Optionally delegate pre-processing to an application-supplied callback via `set_preprocess_handler()`. * Optionally delegate post-processing to an application-supplied callback via `set_postprocess_handler()`. * Provide fluent, variadic property setting that returns the concrete bin type for chaining. **Properties** Because the bin embeds several stages, its properties are prefixed by the stage they configure — `preprocess-*`, `inference-*`, and `postprocess-*`. For example `inference-model` on the bin corresponds to `model` on a standalone inference element, and `postprocess-labels` corresponds to `labels` on a standalone `qtimlpostprocess`. Set `preprocess-engine` to `none` to disable the internal pre-processing path when you supply your own pre-process callback. **Usage** **Custom post-processing** — the bin handles pre-processing and inference internally, the application decodes the output tensors. ```cpp theme={null} static const std::vector labels = load_labels(HOME_PATH + "/labels/yolov8.json"); MLVideoTFLiteBin mlbin("mlbin"); mlbin .set("inference-delegate", "external", "inference-external-delegate-path", "libQnnTFLiteDelegate.so", "inference-external-delegate-options", "QNNExternalDelegate,backend_type=htp;", "inference-model", HOME_PATH + "/models/yolov8_det_quantized.tflite") .set_postprocess_handler( [&](const MLFrame& frame, const MLParam& params, MLDetections& detections) { return decode_detection(frame, detections, params, labels, /*confidence_threshold=*/0.70f); }); Pipeline pipeline("mlbin-external-detection"); pipeline .add("filesrc", "src", "location", HOME_PATH + "/media/video.mp4") .add("qtdemux", "demux") .add("h264parse", "parse") .add("v4l2h264dec", "decoder", "output-io-mode", 4, "capture-io-mode", 4) .add_stream_filter("vf", VideoFilter().format("NV12")) .add(mlbin) .add("qtivoverlay", "overlay") .add("waylandsink", "display", "fullscreen", true); pipeline.execute(); ``` **Custom pre-processing** — the application converts frames into input tensors, the bin runs inference and its built-in post-process module. ```cpp theme={null} MLVideoTFLiteBin mlbin("mlbin"); // preprocess-engine=none disables internal preprocessing path in mlbin. mlbin.set("preprocess-engine", "none"); mlbin.set("inference-delegate", "external"); mlbin.set("inference-external-delegate-path", "libQnnTFLiteDelegate.so"); mlbin.set("inference-external-delegate-options", "QNNExternalDelegate,backend_type=htp;"); mlbin.set("inference-model", HOME_PATH + "/models/yolov8_det_quantized.tflite"); mlbin.set("postprocess-module", "yolov8"); mlbin.set("postprocess-labels", HOME_PATH + "/labels/yolov8.json"); mlbin.set_preprocess_handler( [](const MLVideoBlits& blits, MLFrame& output) { if (blits.entries.empty() || output.tensors.empty()) return false; const MLVideoBlit& blit = blits.entries.front(); return convert_nv12_to_nhwc_i8(blit.image, &blit, output.tensors.front()); }); ``` Swapping runtimes is a matter of changing the class and the runtime-specific properties; the callback registration is unchanged. ```cpp theme={null} MLVideoQNNBin qnn_bin("mlbin"); // qtimlvideoqnnbin MLVideoSNPEBin snpe_bin("mlbin"); // qtimlvideosnpebin MLVideoONNXBin onnx_bin("mlbin"); // qtimlvideoonnxbin ``` **APIs** The table below applies to each of `MLVideoONNXBin`, `MLVideoQNNBin`, `MLVideoSNPEBin`, and `MLVideoTFLiteBin`; substitute the concrete class name for ``. | `qti::` | Description | | - | - | | `(const std::string &name = {})` | Creates the corresponding `qtimlvideo*bin` element with an optional instance name. | | ` &set(String prop, Value value, ...)` | Sets one or more properties, same as `Element::set()`. Properties are stage-prefixed: `preprocess-*`, `inference-*`, `postprocess-*`. | | ` &set_preprocess_handler(TensorsPreprocessCallback handler)` | Registers the external pre-process callback. Requires `preprocess-engine=none` to take effect. | | ` &set_postprocess_handler(Callback handler)` | Registers the external post-process callback. Accepts the same task-specific callback types as `MLPostprocess::set_handler()`. | The bins cannot be copied, only moved — the same as the other element wrappers. ### Buffer **Purpose** Unified buffer abstraction over native GStreamer memory. Provides read/write access, timestamp metadata (pts, dts, duration), resize support, and zero-copy transfer between AppSink and AppSrc. **Key Responsibilities** * Own and manage mapping/unmapping of underlying `GstBuffer` or `GstSample`. * Provide read/write access to buffer memory. * Support resizing and writable allocation for AppSrc. * Carry timestamps (PTS/DTS/duration) and apply them back to `GstBuffer`. * Enable zero-copy extraction using `take_gst_buffer()`. ```cpp theme={null} appsink.set_buffer_consumer([](Buffer buf) { auto* data = buf.data(); auto pts = buf.pts(); }); ``` A `Buffer` arrives either from an `AppSink` — wrapping the sample the pipeline just produced — or is allocated by the application to be filled and pushed through an `AppSrc`. Which of the two it is determines whether the payload is writable: samples coming out of the pipeline are read-only. **APIs** | `qti::Buffer` | Description | | - | - | | `Buffer()` | Creates an empty wrapper, referencing no data. `valid()` returns `false` until it is filled. | | `Buffer(size_t size)` | Allocates a writable buffer with a payload of `size` bytes, for pushing into an `AppSrc`. | | `static Buffer from_readable_sample(void *gst_sample_opaque)` | Wraps an existing `GstSample` and exposes its payload and metadata as a `Buffer`. | | `uint8_t *data()` / `const uint8_t *data() const` | Returns a pointer to the payload. The non-const overload requires a writable buffer. | | `size_t size() const` | Returns the payload size in bytes. | | `void resize(size_t n)` | Resizes the payload storage. | | `void set_pts(uint64_t ns)` / `void set_dts(uint64_t ns)` / `void set_duration(uint64_t ns)` | Sets the presentation timestamp, decode timestamp, or duration, in nanoseconds. | | `uint64_t pts() const` / `uint64_t dts() const` / `uint64_t duration() const` | Reads the corresponding timestamp field, in nanoseconds. | | `bool is_writable() const` | Returns `true` when the underlying memory can be modified in place. | | `bool is_readonly() const` | Returns `true` when the buffer came from an `AppSink` and its memory cannot be modified. | | `bool valid() const` | Returns `true` when the wrapper currently references valid data. | | `void *take_gst_buffer()` | Transfers ownership of the underlying `GstBuffer` to the caller and clears this wrapper. This is the zero-copy hand-off path used by `AppSrc::push_buffer()`. | | `void refill_for_appsrc(size_t n)` | Prepares the buffer's storage for reuse inside an `AppSrc` producer callback, resizing it to `n` bytes. | Like the element wrappers, a `Buffer` cannot be copied, only moved — ownership of the underlying memory is unique. Pass it by reference, or `std::move()` it where ownership has to change, as in the `AppSink` → `AppSrc` bridge above. ### StreamFilter **Purpose** StreamFilter describes the characteristics of a media stream between two pipeline stages so they can negotiate concrete parameters rather than relying on defaults. Without explicit parameters, adjacent components may fall back to suboptimal formats, trigger implicit conversions, or produce mismatched configurations. Specialized subtypes provide fluent, type-safe setters for each domain. Raw caps strings are also accepted for cases not covered by typed setters or when copying directly from gst-launch output. **When to Use StreamFilter** * When two adjacent stages can interoperate in multiple ways, and you want to select explicit parameters instead of relying on defaults. * When you need to optimize latency or performance by avoiding implicit conversions (e.g., choose a hardware-friendly pixel format). * When you want predictable behavior across platforms (e.g., lock color range/primaries/transfer/matrix). * When you integrate tensors for AI/ML and must precisely specify dimensions and element types. * When you need a human-readable summary of the stream parameters for logs and debugging. A `StreamFilter` is not an `Element` and is never added to a pipeline with `add()`. It is always attached between stages with `Pipeline::add_stream_filter(unique_name, filter)`, which internally creates a `capsfilter` element and applies the filter's caps to it. The base `StreamFilter` class itself only exposes a raw-caps-string constructor and `to_string()`; the fluent per-domain setters (`.format()`, `.resolution()`, `.framerate()`, ...) live on the typed subclasses such as `VideoFilter`. **Usage** **Typed subclass** ```cpp theme={null} Pipeline pipeline("video-pipeline"); pipeline.add("videotestsrc", "src", "pattern", "ball") .add_stream_filter("filter", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) .add("waylandsink", "sink") .execute(); ``` **Raw caps string** ```cpp theme={null} Pipeline pipeline("video-pipeline-streamfilter-string"); pipeline.add("videotestsrc", "src", "pattern", "ball") .add_stream_filter("filter", qti::StreamFilter( "video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1")) .add("waylandsink", "sink") .execute(); ``` **APIs** | `qti::StreamFilter` | Description | | - | - | | `StreamFilter()` | Creates an empty filter expression. | | `StreamFilter(const std::string &caps_or_mediatype)` | Creates a filter from a raw caps or media-type string, e.g. `"video/x-raw,format=NV12"`. | | `std::string to_string() const` | Serializes the current filter expression to a caps string, for logging/debugging. | ### VideoFilter **Purpose** Configures a raw video stream (YUV, RGB, Bayer) between a camera source and any downstream processing or display element. Supports format, resolution, framerate, colorimetry, interlace mode, and pixel aspect ratio. **Key Responsibilities** * Produce caps with standard raw video attributes: format, resolution, framerate, colorimetry, interlace mode, pixel aspect ratio, etc. **APIs** | Member | Description | | - | - | | `format(const std::string &fmt)` / `format(Format fmt)` | Sets the stream's pixel format, for example `NV12`, `YUY2`, `RGB`, `RGBA`. Determines how pixel data is laid out in memory; essential for hardware compatibility and color-conversion paths. Accepts a string or a `VideoFilter::Format` enumerator. | | `resolution(int width, int height)` | Specifies the frame dimensions in pixels. | | `framerate(int num, int den = 1)` / `framerate(float num)` | Sets the frame rate as a fraction, for example `30/1` or `30000/1001` frames-per-second. Pass a single value when you do not need a fractional frame rate, like `30.0` fps. | | `colorimetry(const std::string &value)` | Defines the colorimetry standard, for example `bt601`, `bt709`, `bt2020`. | | `range(const std::string &value)` | Selects the quantization range, `full` or `limited`. | | `interlace(const std::string &mode)` | Sets the interlacing mode, for example `progressive` or `interleaved`. Progressive is standard for modern pipelines; interlaced is still used in some camera/legacy systems. | | `pixel_aspect_ratio(int num, int den = 1)` | Specifies the pixel aspect ratio, also called PAR. Normally `1/1`; used for anamorphic or non-square-pixel formats. | | `add(const std::string &expr)` | Appends an arbitrary custom caps field using a `key=value` expression. Escape-hatch for advanced/experimental fields not covered by the typed API. | `VideoFilter::Format` enumerates the raw video formats (`video/x-raw`), `JPEG` (`image/jpeg`), and the Bayer patterns (`video/x-bayer`: `BGGR`, `RGGB`, `GBRG`, `GRBG`, `MONO`). **Usage** We can achieve the same results in three different ways: **Typed (enum) API** - if you want compile-time safety, IDE autocompletion, and guardrails against typos. Ideal for SDK consumers and most production code. ```cpp theme={null} auto v1 = VideoFilter() .format(VideoFilter::Format::NV12) .resolution(1920, 1080) .framerate(30) .colorimetry("bt709") .range("limited") .interlace("progressive"); ``` **String API** - if you need quick iteration or to pass values not (yet) covered by enums — e.g., enabling a freshly added GStreamer value on a target platform without immediately updating headers. ```cpp theme={null} auto v2 = VideoFilter() .format("NV12") .resolution(1920, 1080) .framerate(30) .colorimetry("bt709") .range("limited") .interlace("progressive"); ``` **Raw caps string** - very useful for setting arbitrary stream (caps) filter or you want to lift an existing caps line from logs, gst-launch, or a pad template and drop it as-is — great for debugging, prototyping, or reproducing pipelines exactly. ```cpp theme={null} auto v3 = StreamFilter("video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1,colorimetry=bt709,range=limited"); ``` ### H264Filter **Purpose** Configures an encoded H.264 stream between an encoder and a downstream parser or muxer. Supports resolution, framerate, stream-format, alignment, profile, and level. **Key Responsibilities** * Specify resolution, framerate, stream-format, alignment, profile, level. * Insert `codec_data` buffers. * Attach arbitrary name/value string properties. **APIs** | Member | Description | | - | - | | `resolution(int width, int height)` | Specifies the frame dimensions in pixels. | | `framerate(int num, int den = 1)` / `framerate(float num)` | Sets the frame rate as a fraction, or as a single value when a fractional rate is not needed. | | `profile(const std::string &p)` | Sets the H.264 profile caps field, for example `baseline`, `main`, `high`. | | `level(const std::string &lvl)` | Sets the H.264 level caps field. | | `stream_format(const std::string &f)` | Sets the H.264 stream-format caps field, for example `byte-stream`, `avc`. | | `alignment(const std::string &a)` | Sets the H.264 alignment caps field, for example `au`, `nal`. | | `codec_data(const std::string &bytes)` | Sets the H.264 `codec_data` caps field from a byte string. | | `set(const std::string &key, const std::string &val)` | Shorthand for `add(key + "=" + val)`. | | `add(const std::string &expr)` | Appends an arbitrary custom caps field using a `key=value` expression. | **Usage** ```cpp theme={null} auto h264f = H264Filter().resolution(1920, 1080) .framerate(30) .profile("baseline") .stream_format("byte-stream") .alignment("au"); Pipeline pipeline("h264-pipeline"); pipeline.add_stream_filter("h264f", h264f); ``` ### TensorFilter **Purpose** Configures an ML tensor stream with explicit data type and shape (e.g., UINT8, NHWC). Not needed in a full inference pipeline — when preprocess, inference, and postprocess elements are all present, tensor parameters are negotiated automatically. Use TensorFilter when: * The preprocessing stage does not have model dimension information * You need exact tensor data output from the inference plugin **Key Responsibilities** * Declare element type (e.g., `UINT8`, `FLOAT32`) for tensors. * Specify shape (dimensions). Supports: * Single tensor: a 1D list (e.g., `1, 520, 520, 3`). * Multiple tensors: array-of-arrays (e.g., `{ {1,3,H,W}, {1,32} }`) when needed. **APIs** | Member | Description | | - | - | | `type(const std::string &t)` / `type(Type t)` | Specifies the tensor data type, for example `UINT8` or `FLOAT32`. Accepts a string or a `TensorFilter::Type` enumerator (`UINT8`, `UINT16`, `UINT32`, `INT8`, `INT16`, `INT32`, `FLOAT16`, `FLOAT32`). | | `dimensions(UInts... dims)` | Specifies a single tensor's dimensions as a variadic list of integers, for example `dimensions(1, 520, 520, 3)`. | | `dimensions(const std::vector &one_tensor_dims)` | Specifies a single tensor's dimensions as a vector. | | `dimensions(const std::vector> &many_tensors_dims)` | Specifies several tensors at once, one inner vector per tensor. | | `add(const std::string &expr)` | Appends an arbitrary custom caps field using a `key=value` expression. Escape-hatch for advanced/experimental fields not covered by the typed API. | **Usage** We can achieve the same results in three different ways: **Typed (enum) API** - if you want compile-time safety, IDE autocompletion, and guardrails against typos. Ideal for SDK consumers and most production code. ```cpp theme={null} auto t1 = TensorFilter() .type(TensorFilter::Type::UINT8) .dimensions(1, 520, 520, 3); // NHWC, for example ``` **String API** - if you need quick iteration or to pass values not (yet) covered by enums — e.g., enabling a freshly added GStreamer value on a target platform without immediately updating headers. ```cpp theme={null} auto t2 = TensorFilter() .type("FLOAT32") .dimensions(1, 3, 640, 640); // NCHW, for example ``` **Raw caps string** - very useful for setting arbitrary stream (caps) filter or you want to lift an existing caps line from logs, gst-launch, or a pad template and drop it as-is — great for debugging, prototyping, or reproducing pipelines exactly. ```cpp theme={null} auto t3 = StreamFilter("neural-network/tensors,type=UINT8,dimensions=<<1,520,520,3>>"); ``` ### TextFilter **Purpose** Configures a raw text stream (text/x-raw) for pipelines that carry textual data such as metadata, subtitles, or logs. Minimal by design — attach only the fields you need using .add("key=value"). **Key Responsibilities** * Provide a lightweight caps descriptor for text (`text/x-raw`) without enforcing a schema. * Let callers attach arbitrary properties (e.g., encoding/charset, language, packetization hints) using `.add("key=value")`. **APIs** | Member | Description | | - | - | | `add(const std::string &expr)` | Appends an arbitrary custom caps field using a `key=value` expression. Escape-hatch for advanced/experimental fields not covered by the typed API. | **Usage** ```cpp theme={null} auto txt = TextFilter().add("charset=utf8"); ``` ### ImageFilter **Overview** Used exclusively with CamSrc (qtiqmmfsrc) to enable and link its dedicated image capture pad. Without ImageFilter, the image pad is not requested, not activated, and produces no buffers. It is a structural enabler, not a processing stage — it is not intended for general image processing or frame extraction from video sources. `ImageFilter` is used to: * Enable the image output pad exposed by `CamSrc` (`qtiqmmfsrc`) * Attach that pad to the pipeline so the source can activate image capture * Provide a concrete sink target required by GStreamer for pad activation It is not intended for: * Generic image processing * Frame extraction from arbitrary video sources * Replacement of `VideoFilter` or `AppSink` **Relationship to CamSrc (qtiqmmfsrc)** * `CamSrc` (`qtiqmmfsrc`) exposes a dedicated image pad that remains inactive unless linked. * `ImageFilter` explicitly exists to satisfy this requirement. * Without `ImageFilter`, the image pad: * Will not be requested * Will not be activated * Will not produce buffers This makes `ImageFilter` a structural enabler, not a processing stage. **APIs** | Member | Description | | - | - | | `format(const std::string &fmt)` | Sets the image format for the capture pad, for example `JPEG`. | | `resolution(int width, int height)` | Specifies the captured image dimensions in pixels. | | `framerate(int num, int den = 1)` / `framerate(float num)` | Sets the capture pad's frame rate. | | `add(const std::string &expr)` | Appends an arbitrary custom caps field using a `key=value` expression. | **Usage** ```cpp theme={null} auto imagefilter1 = ImageFilter() .format("JPEG") .resolution(3840, 2160); ``` ### AudioFilter **Purpose** Configures a raw PCM audio stream with sample format, channel count, sample rate, and layout. Use this to ensure audio converters and sinks negotiate exactly the format your hardware supports rather than inserting an implicit resampler. **Key Responsibilities** * Set audio sample format (e.g., `S16LE`, `F32LE`) to match HW/SW capabilities. * Define channels and rate for correct resampling/mixing. * Choose layout (`interleaved` vs `non-interleaved`) to align with downstream expectations. **APIs** | Member | Description | | - | - | | `format(const std::string &fmt)` / `format(Format f)` | Sets the audio sample format, for example `S16LE`, `F32LE`. Accepts a string or an `AudioFilter::Format` enumerator. | | `channels(int n)` | Specifies the number of audio channels. | | `rate(int hz)` | Specifies the audio sample rate in Hz. | | `layout(const std::string &l)` / `layout(Layout l)` | Specifies the audio layout. Accepts a string or `AudioFilter::Layout::INTERLEAVED` / `NON_INTERLEAVED`. | | `add(const std::string &expr)` | Appends an arbitrary custom caps field using a `key=value` expression. Escape-hatch for advanced/experimental fields not covered by the typed API. | `AudioFilter::Format` covers the signed/unsigned integer formats (`S8` through `U32BE`, including the packed 18/20/24-bit variants) and the float formats `F32LE`, `F32BE`, `F64LE`, `F64BE`. **Usage** We can achieve the same results in three different ways: **Typed (enum) API** - if you want compile-time safety, IDE autocompletion, and guardrails against typos. Ideal for SDK consumers and most production code. ```cpp theme={null} auto a1 = AudioFilter() .format(AudioFilter::Format::S16LE) .rate(48000) .channels(2) .layout(AudioFilter::Layout::INTERLEAVED); ``` **String API** - if you need quick iteration or to pass values not (yet) covered by enums — e.g., enabling a freshly added GStreamer value on a target platform without immediately updating headers. ```cpp theme={null} auto a2 = AudioFilter() .format("F32LE") .rate(44100) .channels(1) .layout("non-interleaved"); ``` **Raw caps string** - very useful for setting arbitrary stream (caps) filter or you want to lift an existing caps line from logs, gst-launch, or a pad template and drop it as-is — great for debugging, prototyping, or reproducing pipelines exactly. ```cpp theme={null} auto a3 = StreamFilter("audio/x-raw,format=S16LE,rate=48000,channels=2,layout=interleaved"); ``` ### Logging The SDK can either simplify GStreamer's log stream into a compact, uniform format or step aside and let GStreamer log raw, with `GST_DEBUG` fully in control. Both modes, the verbosity levels, and the ordering constraint on configuring them are covered in [Logging and Diagnostics](/app-builder/cpp-logging). ## QIM SDK Nodes All nodes are executed by GStreamer elements under the hood. Most elements are handled through the `Element` base class. However, elements that provide additional functionality — such as application buffer interfaces in appsink and appsrc, or image-capture signals in camera sources like `qtiqmmfsrc` — require extended interfaces. If you do not need these extra capabilities (for example, image capture), you can still use these elements via the generic `Element` base class without any limitations for standard data processing. Below is a table listing all supported elements and the corresponding QIM SDK wrapper classes used to work with them. **Sources** | GStreamer | QIM SDK C++ Class | | - | - | | `qtiqmmfsrc` | `qti::CamSrc` (also usable as `qti::Element`) | | `v4l2src` | `qti::Element` | | `filesrc` | `qti::Element` | | `multifilesrc` | `qti::Element` | | `appsrc` | `qti::AppSrc` | | `appsink` | `qti::AppSink` | | `qtisocketsrc` | `qti::Element` | **AI** | GStreamer | QIM SDK C++ Class | | - | - | | `qtimlaconverter` | `qti::Element` | | `qtimlvconverter` | `qti::MLVConverter` (also usable as `qti::Element`) | | `qtimlqnn` | `qti::Element` | | `qtimlsnpe` | `qti::Element` | | `qtimltflite` | `qti::Element` | | `qtimlpostprocess` | `qti::MLPostprocess` (also usable as `qti::Element`) | | `qtimlvideoonnxbin` | `qti::MLVideoONNXBin` (also usable as `qti::Element`) | | `qtimlvideoqnnbin` | `qti::MLVideoQNNBin` (also usable as `qti::Element`) | | `qtimlvideosnpebin` | `qti::MLVideoSNPEBin` (also usable as `qti::Element`) | | `qtimlvideotflitebin` | `qti::MLVideoTFLiteBin` (also usable as `qti::Element`) | | `qtimlaic` | `qti::Element` | | `qtimetamux` | `qti::Element` | | `qtimlmetaextractor` | `qti::Element` | | `qtimlmetaparser` | `qti::Element` | | `qtimldemux` | `qti::Element` | | `qtimetatransform` | `qti::Element` | | `mlonnx` | `qti::Element` | **Multimedia** | GStreamer | QIM SDK C++ Class | | - | - | | `qtiobjtracker` | `qti::Element` | | `qtismartvencbin` | `qti::Element` | | `qtivoverlay` | `qti::Element` | | `qtivsplit` | `qti::Element` | | `qtivcomposer` | `qti::Element` | | `qtibatch` | `qti::Element` | | `qticamreproc` | `qti::Element` | | `qtic2adec` | `qti::Element` | | `qtic2aenc` | `qti::Element` | | `qtic2vdec` | `qti::Element` | | `qtic2venc` | `qti::Element` | | `qtijpegenc` | `qti::Element` | | `qtivtransform` | `qti::Element` | | `qticvimgpyramid` | `qti::Element` | | `qticvoptclflow` | `qti::Element` | | `qtidfs` | `qti::Element` | | `v4l2h264dec` | `qti::Element` | | `v4l2h264enc` | `qti::Element` | | `v4l2h265dec` | `qti::Element` | | `v4l2h265enc` | `qti::Element` | | `tee` | `qti::Element` | | `h264parse` | `qti::Element` | | `rtpptdemux` | `qti::Element` | | `rtph264depay` | `qti::Element` | | `queue` | `qti::Element` | | `qtdemux` | `qti::Element` | | `tsdemux` | `qti::Element` | ## Common Application Design ### Create pipeline with config file The SDK supports fully declarative pipeline construction using YAML. A YAML pipeline describes elements and links between them, which are translated at runtime into a GStreamer pipeline. **TopLevel Structure** A YAML pipeline file has a single top-level key: ```yaml theme={null} pipeline: elements: [...] links: [...] ``` **Elements Section** `elements` is an ordered list of pipeline elements. Each element entry defines: * The GStreamer element type * A unique name used for linking * Optional properties * Optional filter configuration **Common Fields** ```yaml theme={null} - type: name: : ``` * `type`: GStreamer element factory name, or `filter` * `name`: Unique identifier within the pipeline * properties: Any remaining key/value pairs are passed directly to the element as GStreamer properties **Filter Elements** Filters are declared using `type: filter` and are further specialized by their configuration block. **Stream Filter** ```yaml theme={null} - type: capsfilter name: raw_caps caps: "video/x-raw,format=NV12,width=1280,height=720,framerate=30/1" ``` **Video Filter** ```yaml theme={null} - type: filter name: vf video: format: NV12 width: 1920 height: 1080 framerate: 30 ``` **Links Section** `links` defines explicit connection paths between elements. Each entry is an ordered list of element names. Elements are linked sequentially from left to right. ```yaml theme={null} links: - [src, demux, parse, decoder, vf, split] ``` **Example yaml files** **YAML config file** ```yaml theme={null} pipeline: elements: - type: qtiqmmfsrc name: source camera: "0" - type: filter name: filter video: format: NV12 width: 1920 height: 1080 framerate: 30.0 - type: tee name: split - type: qtimlvconverter name: preprocessing - type: qtimltflite name: inferencing delegate: gpu model: /etc/models/model.tflite - type: qtimlpostprocess name: postprocessing - type: qtimetamux name: mlmuxer - type: qtivoverlay name: overlay - type: waylandsink name: display sync: true async: true fullscreen: true links: - [source, filter, split, preprocessing, inferencing, postprocessing, mlmuxer, overlay, display] - [split, mlmuxer] ``` ### Multi-Model Daisy Chain Below is shown a logical diagram of an AI daisy chain pipeline with sequential and parallel inferencing. The same could be handled by a straight sequential pipeline of inference bins. But we must set manually the source for every inference bin. Introduction Several `.add(...)` calls in this example had missing/mismatched quotes in the source PDF (e.g. a stray closing paren instead of a closing quote, or a missing leading quote). These have been corrected below where unambiguous. One inline comment truncated at the page margin was completed using the identical sentence found in the prose above. ```cpp theme={null} Element ml_car("qtimlvideotflitebin", "car") .add("performance-profile", "high-performance") .add("inference-delegate", "dsp") .add("inference-model", "/opt/car_detect.dlc") .add("inference-layers", "< /model.22/Mul_2, /model.22/Sigmoid >") .add("postprocess-threshold", 60.0) .add("postprocess-results", 4) .add("postprocess-module", "yolov8") .add("postprocess-labels", "/opt/car_detect_model.labels"); // The same could be handle by straight sequential pipeline of inference bins. But we must set manually source to every inference bin. Element ml_plate("qtimlvideotflitebin", "plated") .add("source", 1) .add("image-disposition", "centre") .add("performance-profile", "high-performance") .add("inference-delegate", "dsp") .add("inference-model", "/opt/plate_detect.dlc") .add("inference-layers", "< /model.22/Mul_2, /model.22/Sigmoid >") .add("postprocess-threshold", 40.0) .add("postprocess-results", 1) .add("postprocess-module", "yolov8") .add("postprocess-labels", "/opt/plate_detect_model.labels"); Element ml_anpr("qtimlvideotflitebin", "anpr") .add("source", 2) .add("image-disposition", "centre") .add("inference-delegate", "dsp") .add("inference-model", "/opt/anpr.dlc") .add("inference-layers", "< /decode_head2/classifier/Conv, /decode_head/classifier/Conv >") .add("postprocess-threshold", 15.0) .add("postprocess-results", 1) .add("postprocess-module", "anpr") .add("postprocess-labels", "/opt/anpr_model.labels"); Element ml_siglip("qtimlvideotflitebin", "siglip") .add("source", 1) .add("image-disposition", "centre"); .add("inference-delegate", "dsp"); .add("inference-model", "/opt/car_make_model.dlc"); .add("inference-layers", "< /classifier/Gemm >"); .add("postprocess-threshold", 15.0); .add("postprocess-results", 1); .add("postprocess-module", "mobilenet"); .add("postprocess-labels", "/opt/car_make_model.labels"); Pipeline pipeline("SmartVehicleIdentification"); pipeline.add("camsrc", "source", "camera", "0") .add_stream_filter("filter", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) .add(ml_car) .add(ml_plate) .add(ml_anpr) .add(ml_siglip) .add("qtivoverlay", "overlay") .add("waylandsink", "display", "sync", "true", "async", "true", "fullscreen") .execute(); ``` ### Custom Postprocessing with YOLOv8 (Detection) QIM SDK implements AI model post-processing through dedicated modules that take tensors as input and produce predictions. All remaining complexity — such as batching, daisy-chaining, image mask support — is encapsulated within the post-processing plugin. This makes the post-processing module API an ideal integration point for application-specific post-processing logic. As a result, applications do not need to handle batching, chaining, image masks, or ML metadata themselves. We will implement a specialized post-processing module to manage bindings with the application. Introduction Custom postprocessing callback is attached to a [`qtimlpostprocess`](/plugin-reference/qtimlpostprocess) element via [`MLPostprocess::set_handler(...)`](/plugin-reference/qtimlpostprocess). Typical pipeline segment: 1. Preprocess frame/tensors ([`qtimlvconverter`](/plugin-reference/qtimlvconverter)) 2. Run ML inference ([`qtimltflite`](/plugin-reference/qtimltflite)) 3. Convert raw tensors in custom callback ([`qtimlpostprocess`](/plugin-reference/qtimlpostprocess)) **Supported callback types** * `ClassificationPostprocessCallback` * `ObjectDetectionPostprocessCallback` * `PoseEstimationPostprocessCallback` * `DepthEstimationPostprocessCallback` * `SegmentationPostprocessCallback` * `TensorsPostprocessCallback` **Minimal usage** ```cpp theme={null} MLPostprocess postprocessing("postprocessing"); postprocessing.set_handler(my_callback); pipeline.add("qtimltflite", "inferencing", ...) .add(postprocessing) .add_stream_filter("mlf", TextFilter()) ...; ``` **Input and output explained** `MLFrameView` (read-only model tensors) Used by classification/detection/pose/depth/segmentation callbacks. Each `TensorView` contains: * `type`: tensor element type (`MLTensorType`) * `dimensions`: tensor shape * `data`: raw tensor memory (`const uint8_t*`) * `size`: tensor bytes Common access pattern: ```cpp theme={null} const auto& t = frame.tensors[0]; const float* data = static_cast(t.data); ``` `MLParam` (runtime metadata and context) `MLParam` carries key/value fields provided by pipeline and preprocessing/postprocess context. Commonly used keys: * `input-tensor-width` * `input-tensor-height` * `input-tensor-region` (mapped to `Region {x, y, width, height}`) Example: ```cpp theme={null} float source_width = 0.0f; float source_height = 0.0f; Region region; params.get("input-tensor-width", source_width); params.get("input-tensor-height", source_height); params.get("input-tensor-region", region); ``` **Callback signatures** * Callbacks (`ClassificationPostprocessCallback`, `ObjectDetectionPostprocessCallback`, `PoseEstimationPostprocessCallback`, `DepthEstimationPostprocessCallback`, `SegmentationPostprocessCallback`) fill the provided output metadata container (`MLClassifications`, `MLDetections`, `MLPoses`, `MLDepthMaps`, `MLSegmentations`). * Tensor callback (`TensorsPostprocessCallback`) fills `MLFrame& output` for downstream tensor stages. * Input `MLFrameView` is read-only and must not be modified. **Classification callback** ```cpp theme={null} using ClassificationPostprocessCallback = std::function; ``` Output: fill `MLClassifications` with `MLClassification` items. Field meanings: * `name`: class label text * `confidence`: prediction confidence * `color`: optional UI/overlay color * `extra`: optional metadata map **Object detection callback** ```cpp theme={null} using ObjectDetectionPostprocessCallback = std::function; ``` Output: fill `MLDetections` with `MLDetection` items. Field meanings: * `left`, `top`, `right`, `bottom`: normalized box coordinates * `name`, `confidence`, `color`: class info * `landmarks`: optional keypoints * `extra`: optional metadata map Implemented example includes confidence thresholding, region coordinate transform, and NMS. **Segmentation callback** ```cpp theme={null} using SegmentationPostprocessCallback = std::function; ``` Input parameters * `frame` (`const MLFrameView&`): read-only model output tensors. * `params` (`const MLParam&`): runtime metadata (for example: `input-tensor-width`, `input-tensor-height`, `input-tensor-region`). * `segmentations` (`MLSegmentations&`): output container to fill. Output contract (`MLSegmentation`) * `labels`: per-pixel semantic labels. * `colors`: per-pixel colors; index-aligned with `labels`. * `n_rows`, `n_columns`: segmentation map dimensions. * `extra`: optional metadata fields. **Pose estimation callback** ```cpp theme={null} using PoseEstimationPostprocessCallback = std::function; ``` Output: fill `MLPoses` with `MLPose` items. Field meanings: * `keypoints`: list of `MLKeypoint` (`name`, `x`, `y`, `confidence`, `color`) * `links`: list of `MLKeypointLink` describing skeleton connections * `name`, `confidence`, `extra`: pose-level metadata **Depth estimation callback** ```cpp theme={null} using DepthEstimationPostprocessCallback = std::function; ``` Output: fill `MLDepthMaps` with one or more `MLDepthMap` items. Field meanings: * `values`: per-pixel depth values (`double`) * `colors`: per-pixel pseudo-color values (`uint32_t`) * `n_rows`, `n_columns`: depth map shape * `extra`: optional metadata map **Tensor-to-tensor callback** ```cpp theme={null} using TensorsPostprocessCallback = std::function; ``` output is owned and writable; callback typically: 1. validates input tensors from `frame` 2. copies/selects required tensors into output 3. updates output tensor dimensions/order if needed **Semantic callbacks** (`classification`, `detection`, `pose`, `depth`, `segmentation`) ```cpp theme={null} bool callback(const MLFrameView& frame, const MLParam& params, OutputType& output) ``` * `frame`: input tensors from inference (read-only) * `params`: runtime metadata (input sizes, region, etc.) * `output`: semantic objects to be filled by callback * return `true` on valid output * return `false` on validation/decode failure **Tensor callback** ```cpp theme={null} bool callback(const MLFrameView& frame, const MLParam& params, MLFrame& output) ``` * `frame`: input tensors from inference (read-only) * `params`: runtime metadata/context * `output`: writable output tensors for downstream tensor stage * return `true` on valid output * return `false` on validation/decode failure **Copy-ready callback skeletons** Classification ```cpp theme={null} ClassificationPostprocessCallback cb = [](const MLFrameView& frame, const MLParam& params, MLClassifications& out) { // decode tensors -> classes return true; }; ``` Detection ```cpp theme={null} ObjectDetectionPostprocessCallback cb = [](const MLFrameView& frame, const MLParam& params, MLDetections& out) { // decode tensors -> detections return true; }; ``` Pose estimation ```cpp theme={null} PoseEstimationPostprocessCallback cb = [](const MLFrameView& frame, const MLParam& params, MLPoses& out) { // decode tensors -> poses return true; }; ``` Depth estimation ```cpp theme={null} DepthEstimationPostprocessCallback cb = [](const MLFrameView& frame, const MLParam& params, MLDepthMaps& out) { // decode tensors -> depthmaps return true; }; ``` Segmentation ```cpp theme={null} SegmentationPostprocessCallback cb = [](const MLFrameView& frame, const MLParam& params, MLSegmentations& out) { // decode tensors -> segmentations return true; }; ``` Tensors ```cpp theme={null} TensorsPostprocessCallback cb = [](const MLFrameView& frame, const MLParam& params, MLFrame& output) { // map/copy selected tensors from frame -> output return true; }; ``` Check application source code on GitHub: [`qimsdk_ref_external_postprocess_detection_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_detection_yolov8) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_detection_yolov8` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_postprocess_detection_yolov8 ``` The application runs inference on the video file and uses the custom, application-defined postprocessing callback to decode detections, which are then overlaid and displayed fullscreen. To stop the application, press **CTRL + C**. # Coding Agent Source: https://imsdkdocs.qualcomm.com/app-builder/cpp-coding-agent Use the QIM SDK C++ App Builder skill to generate C++ pipeline apps with an AI coding agent. ## What is the C++ App Builder Skill? The **QIM SDK C++ App Builder** is an AI coding skill that generates C++ applications using the `QIM SDK C++ API`. You describe the pipeline behavior and configuration in natural language, and the agent produces a ready-to-build `main.cc` and `CMakeLists.txt` built on the `QIM SDK C++ API` — along with a `README.md` that documents the information about the generated application. **Flow** QIM SDK coding agent workflow **What it generates** For each request, the agent produces an artifact folder named `qimsdk-cpp-/` containing: * **`main.cc`** — a complete, buildable C++ application using the QIM SDK C++ API, including pipeline construction, element wiring, stream filters, and ML inference configuration * **`CMakeLists.txt`** — the build script that compiles `main.cc` and links against `qimsdk-app-builder` * **`README.md`** — a detailed document covering: * Purpose and pipeline behavior summary * Configuration placeholders (input, model, labels, output paths) * A step-by-step pipeline flow (text summary + Mermaid diagram) * Steps to compile and run the app on device * **`A YAML config file`** — only when you request declarative YAML pipeline mode **Supported use cases:** | Category | Details | | - | - | | **AI pipelines** | Object detection, image classification, semantic segmentation, face detection, pose estimation, super resolution, depth estimation, audio classification, object tracking, gesture recognition | | **Inputs** | ISP camera, USB camera, file source (MP4/H.264), RTSP stream | | **Outputs** | HDMI display, encoded file (MP4), RTSP stream, Redis/MQTT metadata | | **ML backends** | TFLite (HTP/NPU), QNN, SNPE | | **Pipeline patterns** | Single-stream, multi-stream, daisy-chained models, ML-bin fused inference, custom pre/post-processing, AppSrc/AppSink bridges, YAML config mode | ## Prerequisites ### For generating code Qualcomm dev kits are not needed for code generation. **Host Machine**: Any PC with internet access **Coding Agent**: Any AI coding agent installed (Claude Code, Cursor, Codex, etc.) which supports skills — the skills provided are agent-agnostic. This skill is designed to work well even with low-reasoning / smaller models. Eg: A Sonnet 4.5 model is sufficient. ### For deploying and running apps The target devices where QIM SDK is supported (see [QIM SDK Installation Guide](/installation)) ## How to Use ### Step 1: Get the skill Choose one installation method: Clone the skill repository and copy the **QIM SDK C++ App Builder** and **qimsdk-deploy** skills into your coding agent's skills directory: ```bash theme={null} git clone https://github.com/qualcomm/qimsdk-agentic-skills # The following commands are for Claude Code. # Set this to your coding agent's skills directory. cp -r qimsdk-agentic-skills/skills/qimsdk-cpp-app-builder ~/.claude/skills cp -r qimsdk-agentic-skills/skills/qimsdk-deploy ~/.claude/skills ``` The skills directory is agent-specific. Check your coding agent's documentation for its skills location. Restart or reload your coding agent after installation. Verify that `qimsdk-cpp-app-builder` and `qimsdk-deploy` are available with `/skills`. **Claude Code** — run inside a Claude Code session: ```text theme={null} /plugin marketplace add https://github.com/qualcomm/qimsdk-agentic-skills.git /plugin install qimsdk-agentic-skills@qimsdk-marketplace /reload-plugins ``` **Codex** — run in your terminal: ```bash theme={null} codex plugin marketplace add https://github.com/qualcomm/qimsdk-agentic-skills.git codex plugin add qimsdk-agentic-skills@qimsdk-marketplace ``` *** ### Step 2: Generate the application Describe your pipeline to the agent. It will automatically activate the skill and generate a complete `main.cc`, `CMakeLists.txt`, and `README.md`. #### Sample Prompt **Prompt format — describe pipeline behavior and configuration:** ``` Create a QIM SDK C++ app for single-stream YOLOX object detection from an MP4 file, encoding the result to a file. ## Pipeline Behavior - Decode an MP4 file using Qualcomm hardware decoder - Run YOLOX object detection on full frames using TFLite external delegate on HTP/NPU - Merge metadata with the video stream before overlay - Overlay bounding boxes and labels - Encode the overlaid video and save to an output MP4 file ## Configuration - Input: ~/Downloads/qimsdk_samples/media/ai_demo_sample.mp4 - Model: ~/Downloads/qimsdk_samples/models/yolox_w8a8.tflite - Labels: ~/Downloads/qimsdk_samples/labels/yolov8.json - Backend: TFLite external delegate, HTP/NPU - Confidence: 51.0 - Output: ~/Downloads/qimsdk_samples/media/obj_detect_out.mp4 ``` More sample prompts can be found in the [Available Sample Prompts](#available-sample-prompts) section below. #### Generated `README.md` The README documents the generated app in full: * **Purpose** — a plain-English summary of what the pipeline does * **Files** — lists `main.cc`, `CMakeLists.txt`, and `README.md` with descriptions * **Assumptions** — codec format, quantization requirements, camera defaults, output directory pre-conditions * **Configuration** — all user-supplied paths (input, model, labels, output) with instructions on where to change them * **Placeholders to Fill** — any values you still need to supply, or a note that none remain when the request was fully specified * **Pipeline Flow** — a `Text Summary` walkthrough of every element plus a `Mermaid Diagram` of the full pipeline * **Steps to compile** — how to build the app against the QIM SDK with CMake * **Steps to Run** — exact commands to run on device, including any setup (e.g. `mkdir -p` for output directories) # QIM SDK C++ App — Single-Stream YOLOX Object Detection (MP4 → MP4) ## Purpose Decode an MP4 file with the Qualcomm hardware decoder, run **YOLOX** object detection on full frames using the **TFLite external delegate on the HTP/NPU**, merge the detection metadata with the video stream, overlay bounding boxes and class labels, then hardware-encode the annotated video to an output MP4 file. Headless (no display branch). ## Files * `main.cc` — the qimsdk C++ pipeline app * `CMakeLists.txt` — build target (`qimsdk-cpp-yolox-obj-detect-encode`) * `README.md` — this file ## Assumptions * Input is H.264-in-MP4, decoded with `v4l2h264dec` (`capture-io-mode=4`, `output-io-mode=4` — file source decoded through the hardware decoder). * YOLOX (`yolox_w8a8.tflite`) uses the `yolov8` postprocess module with the `yolov8.json` labels, per the model catalog. * Confidence threshold is applied as inline postprocess settings `{"confidence": 51.0}`. * No display: the annotated stream is normalized back to NV12 (`render_vf`), encoded (`v4l2h264enc`, `capture/output-io-mode=4/4`, the correct pairing for a transform/decoder-produced NV12 buffer), parsed, muxed (`mp4mux`), and written to the output MP4 (`filesink`). * `pipeline.eos(true)` is set so `mp4mux` finalizes the container on EOS. * `HOME` is resolved once in C++ (`std::getenv("HOME")` with an unset/empty check) and prepended to every file path, because C++ string literals do not expand `$HOME`. ## Configuration (fixed constants in `main.cc`) | Value | Path | | - | - | | Input | `$HOME/Downloads/qimsdk_samples/media/ai_demo_sample.mp4` | | Model | `$HOME/Downloads/qimsdk_samples/models/yolox_w8a8.tflite` | | Labels | `$HOME/Downloads/qimsdk_samples/labels/yolov8.json` | | Output | `$HOME/Downloads/qimsdk_samples/media/obj_detect_out.mp4` | ## Placeholders to Fill None. All paths, model, labels, confidence, and delegate options are concrete. `${QIMSDK_BINDIR}` is expected to be defined by the parent SDK build. ## Pipeline Flow ### Text Summary `filesrc` reads the MP4 and `qtdemux` extracts the H.264 elementary stream, which `h264parse` prepares and `v4l2h264dec` hardware-decodes. A `queue` (`q_dec`) decouples the decoder thread, then a `VideoFilter` (`vf`) constrains the stream to NV12. A `tee` (`split`) branches: * **Passthrough / video branch** → `qtimetamux` (`metamux`). * **AI branch** → `queue` (`q_ai`) → `qtimlvconverter` (`pre`, preprocess to model tensor) → `qtimltflite` (`infer`, YOLOX, external delegate, HTP/NPU) → `qtimlpostprocess` (`post`, `module=yolov8`, labels, `{"confidence": 51.0}`) → `TextFilter` (`mlf`) → `qtimetamux` (`metamux`). `qtimetamux` merges the detection metadata back onto the video frames, `qtivoverlay` draws the bounding boxes and class labels, a `VideoFilter` (`render_vf`) normalizes the overlaid frames to NV12 for the encoder, and the stream is hardware-encoded by `v4l2h264enc`, parsed by `h264parse` (`h264parser`), muxed by `mp4mux`, and written by `filesink` to the output MP4. ### Mermaid Diagram ```mermaid theme={null} flowchart TD SRC[filesrc] --> DEMUX[qtdemux] DEMUX --> PARSE[h264parse] PARSE --> DEC[v4l2h264dec] DEC --> QDEC[queue q_dec] QDEC --> NV12[NV12 VideoFilter vf] NV12 --> SPLIT[tee split] SPLIT -->|passthrough| MUX[qtimetamux metamux] SPLIT -->|AI| QAI[queue q_ai] QAI --> PRE[qtimlvconverter pre] PRE --> INFER[qtimltflite YOLOX HTP] INFER --> POST[qtimlpostprocess module=yolov8] POST --> TEXT[TextFilter mlf] TEXT --> MUX MUX --> OVL[qtivoverlay] OVL --> RENDER[NV12 VideoFilter render_vf] RENDER --> ENC[v4l2h264enc] ENC --> PARSEOUT[h264parse h264parser] PARSEOUT --> MP4[mp4mux] MP4 --> SINK[filesink] ``` ## Steps to Compile Yocto: [https://imsdkdocs.qualcomm.com/advanced/yocto-build#steps-to-build-custom-application](https://imsdkdocs.qualcomm.com/advanced/yocto-build#steps-to-build-custom-application) ## Steps to Run First ensure all referenced files — the input MP4, the `yolox_w8a8.tflite` model, and the `yolov8.json` labels — are already present on the device at the paths configured in `main.cc`. ```bash theme={null} # Copy the app to device scp qimsdk-cpp-yolox-obj-detect-encode @:~/ # SSH into device and run ssh @ chmod +x ~/qimsdk-cpp-yolox-obj-detect-encode ./qimsdk-cpp-yolox-obj-detect-encode ``` The input MP4 must exist at `$HOME/Downloads/qimsdk_samples/media/ai_demo_sample.mp4` on the device, and the output directory (`$HOME/Downloads/qimsdk_samples/media/`) must be writable so the app can create `obj_detect_out.mp4`. The app runs to end-of-file, finalizes the MP4, and exits. #### Generated `main.cc` * The generated application constructs a tee-split pipeline: decoded frames are split into a passthrough branch and an AI branch. * The AI branch runs `qtimlvconverter` (preprocess) → `qtimltflite` (YOLOX inference via HTP/NPU) → `qtimlpostprocess` → `TextFilter`, then merges back into `qtimetamux` with the passthrough video. * `qtivoverlay` draws bounding boxes, and the result is hardware-encoded and written to an MP4 file via `filesink`. * Pipeline construction and `.execute()` live inside `create_and_execute_pipeline()`; `main()` sets up logging, wraps the call in a try/catch, and returns a non-zero exit code on error. * All paths are `$HOME`-relative and resolved once via a checked `std::getenv("HOME")` helper — C++ string literals do not expand shell variables. ```cpp theme={null} #include #include #include #include using namespace qti; // C++ string literals never expand $HOME; resolve it once at startup with an // explicit unset/empty check so HOME-relative paths fail loudly if HOME is unset. const std::string HOME_PATH = [] { const char* home = std::getenv("HOME"); if (home == nullptr || *home == '\0') { throw std::runtime_error("HOME is not set; cannot resolve filesystem paths"); } return std::string(home); }(); // Example pipeline: // // filesrc -> qtdemux -> h264parse -> v4l2h264dec -> [vf NV12] -> tee // tee (passthrough) -> qtimetamux -> qtivoverlay -> [render vf NV12] -> encoder -> parser -> muxer -> filesink // tee (AI branch) -> qtimlvconverter -> qtimltflite (YOLOX, HTP external delegate) // -> qtimlpostprocess (module=yolov8, labels, confidence=51.0) // -> [TextFilter] -> qtimetamux // // The pipeline reads an MP4/H.264 file, decodes it with the hardware decoder, // runs YOLOX object detection on full frames via the HTP/NPU (TFLite external // delegate), merges the detection metadata back with the video, overlays // bounding boxes and class labels on each frame, encodes the overlaid video // with the hardware H.264 encoder, and writes the result to an output MP4 file. void create_and_execute_pipeline() { // Reads the input media file as raw bytes. Element src("filesrc", "src"); src.set("location", HOME_PATH + "/Downloads/qimsdk_samples/media/ai_demo_sample.mp4"); // Extracts elementary streams from the MP4 container. // Dynamic pad-added linking is handled internally by the SDK. Element demux("qtdemux", "demux"); // Prepares the H.264 bitstream for the decoder. Element parse("h264parse", "parse"); // Decodes the compressed H.264 stream into raw video frames. // // The I/O mode is configured to enforce DMA buffer usage, // avoiding unnecessary buffer copies. Element decoder("v4l2h264dec", "decoder"); decoder.set("output-io-mode", 4); decoder.set("capture-io-mode", 4); // Queue immediately after the hardware decoder before any filter/tee. Element q_dec("queue", "q_dec"); // Normalizes decoded output to NV12 before branching/AI preprocessing. auto vf = VideoFilter().format("NV12"); // Splits the normalized video into a passthrough (main video) branch and // an AI inference branch. Element split("tee", "split"); // Queue to isolate the AI branch from the tee. Element q_ai("queue", "q_ai"); // Converts raw video frames into normalized tensors for inference. Element preprocess("qtimlvconverter", "pre"); // Runs YOLOX object detection via the TFLite HTP/NPU external delegate. Element infer("qtimltflite", "infer"); infer.set("delegate", "external"); infer.set("external-delegate-path", "libQnnTFLiteDelegate.so"); infer.set("external-delegate-options", "QNNExternalDelegate,backend_type=htp,log_level=(string)1;"); infer.set("model", HOME_PATH + "/Downloads/qimsdk_samples/models/yolox_w8a8.tflite"); // Decodes YOLOX output tensors into bounding boxes + class labels. // module=yolov8 is the documented compatibility mapping for YOLOX detection. // The confidence threshold (51.0) is applied as inline JSON settings. Element post("qtimlpostprocess", "post"); post.set("module", "yolov8"); post.set("labels", HOME_PATH + "/Downloads/qimsdk_samples/labels/yolov8.json"); post.set("settings", "{\"confidence\": 51.0}"); // Serialized detection metadata bus feeding qtimetamux. auto mlf = TextFilter(); // Synchronizes AI detection metadata with the original video buffer. Element metamux("qtimetamux", "metamux"); // Draws bounding boxes and class labels on the video frame. Element overlay("qtivoverlay", "overlay"); // Encoder requires NV12 raw video input. auto render_vf = VideoFilter().format("NV12"); // Encodes the overlaid raw video frames into H.264. // // File/decode source path: driver manages both encoder input and output // buffers via dmabuf, so both io-modes are 4. Element encoder("v4l2h264enc", "encoder"); encoder.set("output-io-mode", 4); encoder.set("capture-io-mode", 4); // Parses the encoded H.264 bitstream for muxing. Element h264parser("h264parse", "h264parser"); // Muxes the encoded stream into an MP4 container. Element muxer("mp4mux", "muxer"); // Writes the muxed MP4 stream to the output file. Element sink("filesink", "sink"); sink.set("location", HOME_PATH + "/Downloads/qimsdk_samples/media/obj_detect_out.mp4"); // Creates the pipeline, adds and links elements, and executes it. // // Explicit linking is applied for the tee/metamux branches. // eos(true) lets mp4mux finalize the container correctly on EOS. Pipeline pipeline("qimsdk-cpp-yolox-obj-detect-encode"); pipeline.add(src) .add(demux) .add(parse) .add(decoder) .add(q_dec) .add_stream_filter("vf", vf) .add(split) .add(metamux) .add(q_ai) .add(preprocess) .add(infer) .add(post) .add_stream_filter("mlf", mlf) .add(overlay) .add_stream_filter("render_vf", render_vf) .add(encoder) .add(h264parser) .add(muxer) .add(sink) .eos(true) .link("src", "demux", "parse", "decoder", "q_dec", "vf", "split") .link("split", "metamux") .link("split", "q_ai", "pre", "infer", "post", "mlf", "metamux") .link("metamux", "overlay", "render_vf", "encoder", "h264parser", "muxer", "sink") .execute(); } int main() { // Route GStreamer logs through the IMSDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); try { create_and_execute_pipeline(); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` #### Generated `CMakeLists.txt` * Defines a single executable target (`qimsdk-cpp-yolox-obj-detect-encode`) built from `main.cc`. * Links against `qimsdk-app-builder` — no direct GStreamer package dependency. * Installs the binary to `${QIMSDK_BINDIR}` (defined by the parent SDK build) with standard executable permissions. ```cmake theme={null} cmake_minimum_required(VERSION 3.8.2) set(TEST_TARGET qimsdk-cpp-yolox-obj-detect-encode) add_executable(${TEST_TARGET} main.cc ) target_link_libraries(${TEST_TARGET} PRIVATE qimsdk-app-builder ) install( TARGETS ${TEST_TARGET} RUNTIME DESTINATION ${QIMSDK_BINDIR} PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ ) ``` *** ### Step 3: Running the applications | File | Download | Save as | | - | - | - | | YOLOX W8A8 model | [Qualcomm AI Hub — YOLOX](https://aihub.qualcomm.com/iot/models/yolox) | `yolox_w8a8.tflite` | | Detection labels | yolov8.json | `yolov8.json` | | Sample video | Input video | `ai_demo_sample.mp4` | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` The input video, model, and labels must be present on the device before running — regardless of how you build and run the app. Copy them to the paths `main.cc` expects: ```bash theme={null} # Replace and with your device credentials. ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media ~/Downloads/qimsdk_samples/models ~/Downloads/qimsdk_samples/labels" # Copy the assets to device (paths must match main.cc) scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ scp yolox_w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` The C++ app must be compiled before it can run. With the assets in place, you have two options to build and run the agent-generated app: **Option A — Use the QIM SDK Deploy skill (Recommended):** The `qimsdk-deploy` skill builds the app on your host against the Yocto SDK, then pushes the compiled binary to the device and runs it over SSH. It handles only the app build and binary transfer — the assets from the previous step must already be on the device. **Option B — Follow the generated README:** Follow the steps in the [`README.md`](#generated-readme-md) to compile the app, then push the binary to the device and run it: 1. **Compile the app** by following the steps at [Yocto Build](/advanced/yocto-build). 2. **Push the compiled binary to the device and run it:** ```bash theme={null} # Copy the built binary to device scp qimsdk-cpp-yolox-obj-detect-encode @:~/ # SSH into device and run ssh @ chmod +x ~/qimsdk-cpp-yolox-obj-detect-encode ./qimsdk-cpp-yolox-obj-detect-encode ``` On completion (EOS), the annotated video is written to `~/Downloads/qimsdk_samples/media/obj_detect_out.mp4`. *** ## Available Sample Prompts | # | Prompt | | - | - | | 1 | [Camera YOLOv8 object detection using fused TFLite ML-bin](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_01_mlbin_yolov8_camera_overlay.md) | | 2 | [Two-stage PPE daisy-chain — person detection → PPE detection → overlay display](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_02_mlbin_ppe_daisy_chain_display.md) | | 3 | [Object detection with custom C++ postprocess placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_03_custom_postprocess_detection_placeholder.md) | | 4 | [Pose estimation with custom C++ postprocess placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_04_custom_postprocess_pose_placeholder.md) | | 5 | [Inference app with tensor output via custom callback placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_05_tensor_dump_callback_placeholder.md) | | 6 | [YOLOv8 object detection pipeline with generated YAML config](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_06_yaml_yolov8_generated_config.md) | | 7 | [Object detection with custom C++ preprocess placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_07_custom_preprocess_detection_placeholder.md) | | 8 | [Object detection with TFLite ML-bin custom preprocess placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_08_mlbin_custom_preprocess_detection_placeholder.md) | | 9 | [Load existing YOLOv8 detection pipeline from external YAML config](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_09_yaml_yolov8_external_config.md) | | 10 | [Single-stream YOLOX object detection from MP4, encoded to file](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_10_single_stream_object_detection.md) | | 11 | [AI wall — four parallel AI inference streams composed into a 2×2 grid display](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_11_ai_wall.md) | | 12 | [Four-stage gesture recognition daisy-chain pipeline](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_12_gesture_recognition.md) | | 13 | [Audio classification — classify audio events from video and overlay results](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/AI_13_audio_classification.md) | | 14 | [Two pipelines bridged through application-managed buffers (AppSrc/AppSink)](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-cpp-app-builder/MM_01_appsrc_appsink_bridge.md) | # Demo Applications Source: https://imsdkdocs.qualcomm.com/app-builder/cpp-demo-applications Ready-to-run C++ App Builder demo applications The table below lists the C++ App Builder demo applications built on top of the [C++ App Builder](/app-builder/cpp-app-builder) SDK. | # | Application | Source Code | Description | | - | - | - | - | | 1 | [Product Counting Application](/blogs/product-counting-application) | [demo\_product\_counting/main.cc](https://github.com/qualcomm/qimsdk/blob/main/cpp/examples/demo-apps/demo_product_counting/main.cc) | A YOLOv8-based object detection pipeline that counts products crossing a center region of interest in real time. | | 2 | [Smart Board Application](/blogs/smart-board-application) | [demo\_smartboard/main.cc](https://github.com/qualcomm/qimsdk/blob/main/cpp/examples/demo-apps/demo_smartboard/main.cc) | A touchless virtual whiteboard controlled entirely by hand gestures, supporting freehand drawing, shape recognition, erasing, and dragging content via a two-stage palm detection and hand landmark pipeline. | | 3 | [Touchless Wrist Steering Car Game](/blogs/touchless-wrist-steering-car-game) | [demo\_car\_game/main.cc](https://github.com/qualcomm/qimsdk/blob/main/cpp/examples/demo-apps/demo_car_game/main.cc) | A two-stage palm detection and hand landmark pipeline turns wrist movement into a virtual steering wheel for a real-time, touchless split-screen driving game. | # Quick start guide Source: https://imsdkdocs.qualcomm.com/app-builder/cpp-quickstart A Quick demonstration of C++ SDK for building AI application on Qualcomm platforms ## What is the QIM SDK C++ App Builder? ### Overview The **QIM SDK C++ App Builder** is a modern C++ SDK for building, running, and managing multimedia and AI pipelines on Qualcomm platforms. It sits as a high-level abstraction layer on top of GStreamer, taking care of framework-level concerns such as element negotiation, pad linking, signal management, event loops, and state transitions, so developers can stay focused on application logic instead of framework boilerplate. Introduction ### Key Capabilities * **No GStreamer expertise required** — the SDK manages all underlying GStreamer mechanics internally, with no compile-time dependency on GStreamer headers. * **Less code, no GStreamer boilerplate** — a fluent, high-level API bundles away the pipeline construction, element linking, and state management that GStreamer's native API would otherwise require, so a functional camera-to-display pipeline can be written in just a few lines of C++. * **Custom pre/post-processing** — plug in your own pre- and post-processing logic around the built-in ML bins without writing a custom GStreamer element. * **AI inference support** — built-in ML bins for TFLite, QNN, SNPE, and ONNX models. * **Broad multimedia support** — video capture, signal processing, ML inference, overlay rendering, and display output. ### API Reference
API Description API Code Reference
**Pipeline** Builds, links, and manages the full lifecycle of a GStreamer pipeline. Supports both programmatic (C++ fluent API) and declarative (YAML) pipeline definition. Key methods: `add()`, `link()`, `add_stream_filter()`, `execute()`, `prepare()`, `start()`, `stop()`, `get()`. ```cpp theme={null} Pipeline p("cam-pipeline"); p.add("qtiqmmfsrc", "src").add("waylandsink", "sink").execute(); ```
**Element** Generic wrapper around a GStreamer element. Supports fluent property setting, either at creation time or at runtime, to update a live pipeline. ```cpp theme={null} Element sink("waylandsink", "display"); sink.set("fullscreen", true); sink.set("sync", false); ```
**Buffer** Unified buffer abstraction over native GStreamer memory. Provides read/write access, timestamp metadata, and zero-copy transfer paths. Key methods: `data()`, `size()`, `resize()`, `pts()`, `dts()`, `duration()`. ```cpp theme={null} sink.setBufferConsumer([](Buffer buf) { auto* data = buf.data(); auto pts = buf.pts(); }); ```
**AppSrc** App-facing source. Feeds buffers from application code into the pipeline using lambda callbacks or a push API. ```cpp theme={null} AppSrc src(pipeline.get("src")); src.setBufferProducer([](Buffer& buf) { // fill buf.data() return true; }); ```
**AppSink** App-facing sink. Delivers pipeline output buffers to application code with zero-copy. ```cpp theme={null} AppSink sink(pipeline.get("sink")); sink.setBufferConsumer([](Buffer buf) { // consume buf.data() }); ```
**CamSrc** Built-in ISP camera source. Exposes an API for runtime image capture and snapshots. ```cpp theme={null} CamSrc cam(pipeline.get("source")); cam.captureImage("snapshot.jpg"); ```
**MLPreProcess** Attaches a custom C++ lambda for pre-processing input frames before ML inference. ```cpp theme={null} MLPreProcess pre(pipeline.get("preproc")); pre.set_handler([](Buffer& buf) { // transform input frame }); ```
**MLPostProcess** Attaches a custom C++ lambda for post-processing raw inference tensor output. ```cpp theme={null} MLPostProcess pp(pipeline.get("postproc")); pp.set_handler([](Buffer& buf) { // parse inference output }); ```
**MLVideoTFLiteBin** / **MLVideoQNNBin** / **MLVideoSNPEBin** / **MLVideoONNXBin** All-in-one preprocess + inference + postprocess bins. Supports TFLite, QNN, SNPE, and ONNX models. ```cpp theme={null} pipeline.add("MLVideoTFLiteBin", "ml", "model", "/models/model.tflite", "delegate", "htp"); ```
**VideoFilter** / **AudioFilter** / **ImageFilter** / **H264Filter** / **TensorFilter** / **TextFilter** Fluent caps builders for `add_stream_filter()`. Describes stream characteristics for caps negotiation between pipeline stages. ```cpp theme={null} pipeline.add_stream_filter("vf", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)); ```
*** ## Build your application Ensure you have QIM SDK installed. See [QIM SDK Installation Guide](../installation) Before running any of the applications below, create the sample directory tree on the target device: ```bash theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{media,models,labels}" ``` This is required even for apps that don't download any media, model, or label files (such as the camera capture and recording apps), since they still write their output under this path. ### USB Camera Source USB (UVC) cameras are handled by the `v4l2src` node. Each USB camera comes with its own format and frame constraints. Because of this, we always place `qtivtransform` right after the USB camera to ensure that the rest of the pipeline receives a hardware-friendly NV12 (Semi-planar YUV420) video format. `qtivtransform` automatically operates in passthrough mode if the USB camera already supports NV12, so you don't need to worry about any performance overhead. Introduction Check application source code on GitHub: [`qimsdk_ref_usb_camera`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_usb_camera) Pre-built application on device: `/usr/bin/qimsdk_ref_usb_camera` ```cpp theme={null} #include #include using namespace qti; // Example pipeline: // // source → transform → [videofilter] → display // // The pipeline reads frames from a USB (V4L2) camera, rotates them, restricts // the stream to NV12/1080p/30fps, and displays the result through Wayland. void create_and_execute_pipeline(const std::string &device) { // Captures frames from the USB camera source. Element source("v4l2src", "source"); source.set("device", device); // Applies geometric transforms to video frames. Element transform("qtivtransform", "transform"); // Stream filters used in branch links. // They define specific stream characteristics from the supported options. auto videofilter = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30); // Render video stream on display. // // sync=false disables strict rendering synchronization to the pipeline clock. // fullscreen=true renders the output fullscreen on the target display. Element display("waylandsink", "display"); display.set("sync", false); display.set("fullscreen", true); // Creates the pipeline, adds elements, links them explicitly, and executes it. Pipeline pipeline("usb-cam-pipeline"); pipeline.add(source) .add(transform) .add_stream_filter("videofilter", videofilter) .add(display) .link("source", "transform", "videofilter", "display") .execute(); } int main(int argc, char **argv) { // Route GStreamer logs through the QIM SDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); // V4L2 device node to capture from, defaults to /dev/video2. Pass -d/--device to override. std::string device = "/dev/video2"; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if ((arg == "-d" || arg == "--device") && i + 1 < argc) { device = argv[++i]; } } try { create_and_execute_pipeline(device); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} /usr/bin/qimsdk_ref_usb_camera --input-config /dev/video2 ``` The live camera feed is rotated and rendered fullscreen on the display in NV12 format at 1080p/30fps. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_usb_camera`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_usb_camera) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) Check application source code on GitHub: [`qimsdk_ref_usb_camera_impl`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_usb_camera_impl) Pre-built application on device: `/usr/bin/qimsdk_ref_usb_camera_impl` ```cpp theme={null} #include #include using namespace qti; // Example pipeline: // // source → transform → [videofilter] → display // // The pipeline reads frames from a USB (V4L2) camera, rotates them, restricts // the stream to NV12/1080p/30fps, and displays the result through Wayland. void create_and_execute_pipeline(const std::string &device) { // Creates the pipeline, adds and links elements, and executes it. // // Elements are created on the fly as they are added, and linking is // implicit, following the order in which elements are added. // // sync=false disables strict rendering synchronization to the pipeline clock. // fullscreen=true renders the output fullscreen on the target display. Pipeline pipeline("usb-cam-pipeline"); pipeline.add("v4l2src", "source", "device", device) .add("qtivtransform", "transform") .add_stream_filter("videofilter", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) .add("waylandsink", "display", "sync", false, "fullscreen", true) .execute(); } int main(int argc, char **argv) { // Route GStreamer logs through the QIM SDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); // V4L2 device node to capture from, defaults to /dev/video2. Pass -d/--device to override. std::string device = "/dev/video2"; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if ((arg == "-d" || arg == "--device") && i + 1 < argc) { device = argv[++i]; } } try { create_and_execute_pipeline(device); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} /usr/bin/qimsdk_ref_usb_camera_impl --input-config /dev/video2 ``` The live camera feed is rotated and rendered fullscreen on the display in NV12 format at 1080p/30fps. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_usb_camera_impl`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_usb_camera_impl) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) See the full YAML configuration here: [qimsdk\_ref\_usb\_camera.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_usb_camera.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) Pre-built application on device: `/usr/bin/qimsdk_ref_yml` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_usb_camera.yaml` ```yaml theme={null} pipeline: elements: - type: v4l2src name: source device: /dev/video2 - type: qtivtransform name: transform - type: filter name: videostream video: format: NV12 width: 1920 height: 1080 framerate: 30 - type: waylandsink name: display sync: false fullscreen: true ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} qimsdk_ref_yml /etc/qimsdk/qimsdk_ref_usb_camera.yaml ``` The live camera feed is rotated and rendered fullscreen on the display in NV12 format at 1080p/30fps. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Object Detection This pipeline captures from a built-in camera, runs an object detection model through an ML inference bin, renders bounding box overlays on the output frames, and displays the result. The ML bin handles preprocessing, inference, and postprocessing internally. The stream filter pins the input video format expected by the model. The overlay element draws detection results on each frame before display. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_yolov8) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_yolov8` | File | Download | Save as | | - | - | - | | YOLOv8 detection model | [Qualcomm AI Hub — YOLOv8](https://aihub.qualcomm.com/iot/models/yolov8_det) | `yolov8_det_quantized.tflite` | | Detection labels | yolov8.json | `yolov8.json` | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. The model and labels must be present on the device before running. Copy them to the paths the application expects: ```bash theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/models ~/Downloads/qimsdk_samples/labels" # Copy the assets to device scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` This application uses the built-in ISP camera. On platforms without an onboard ISP camera (e.g. IQ9), attach an external IMX camera (e.g. from RB3 Gen2) before running the application — see [ISP Camera (Config #2 / qticamsrc)](/advanced/debugging#isp-camera-config-2--qticamsrc) for the procedure to switch from `libcamera` to `qticamsrc`. ```bash theme={null} /usr/bin/qimsdk_ref_camera_yolov8 ``` The live camera feed is rendered fullscreen on the display with YOLOv8 bounding boxes and class labels overlaid on each detected object in real time. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_yolov8) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Post-Processing The QIM SDK implements AI model post-processing through dedicated modules that take tensors as input and produce predictions. All underlying complexity — batching, daisy-chaining, image mask support, and ML metadata handling — is encapsulated within the post-processing plugin. This makes `MLPostProcess` the ideal integration point for application-specific logic, since the application does not need to handle any of that complexity directly. A specialized post-processing module manages the bindings between the pipeline and the application. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_mlbin_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_mlbin_yolov8) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_mlbin_yolov8` | File | Download | Save as | | - | - | - | | YOLOv8 detection model | [Qualcomm AI Hub — YOLOv8](https://aihub.qualcomm.com/iot/models/yolov8_det) | `yolov8_det_quantized.tflite` | | Detection labels | yolov8.json | `yolov8.json` | | Sample video | Input video | `ai_demo_sample.mp4` | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. The input video, model, and labels must be present on the device before running. Copy them to the paths the application expects: ```bash theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media ~/Downloads/qimsdk_samples/models ~/Downloads/qimsdk_samples/labels" # Copy the assets to device scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_postprocess_mlbin_yolov8 ``` The video plays back with YOLOv8 bounding boxes and class labels decoded by the custom postprocessing callback, overlaid and rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_mlbin_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_mlbin_yolov8) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) *** ## Benefits of QIM SDK C++ App Builder * Significantly reduced source code * Improved readability * Internal GStreamer state transition handling * Hidden End-of-Stream handling * Automatic error and state event handling * Automatic resource cleanup * Simplified AppSink/AppSrc interaction using lambda functions * SDK-managed buffer mapping and queries * Simplified buffer access anywhere in the pipeline using wrapped GST probes * YAML-based pipeline description # Reference Applications Source: https://imsdkdocs.qualcomm.com/app-builder/cpp-reference-applications Reference C++ App Builder pipeline examples ## Multimedia Pipeline Before running any of the applications below, create the sample directory tree on the target device: ```bash theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{media,models,labels}" ``` This is required even for apps that don't download any media, model, or label files (such as the camera capture and recording apps), since they still write their output under this path. ### USB Camera Preview USB (UVC) cameras are handled by the `v4l2src` node. Each USB camera comes with its own format and frame constraints. Because of this, we always place `qtivcomposer` right after the USB camera to ensure that the rest of the pipeline receives a hardware-friendly NV12 (Semi-planar YUV420) video format. However, `qtivcomposer` will automatically operate in passthrough mode if the USB camera already supports NV12, so you don't need to worry about any performance overhead. Some USB webcams (e.g. Logitech Brio 4K) may render a green/garbled frame at 30fps depending on platform/build. If this happens, lower the pipeline's framerate (e.g. to 15fps) as a workaround. Introduction Check application source code on GitHub: [`qimsdk_ref_usb_camera`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_usb_camera) Pre-built application on device: `/usr/bin/qimsdk_ref_usb_camera` ```cpp theme={null} #include #include using namespace qti; // Example pipeline: // // source → transform → [videofilter] → display // // The pipeline reads frames from a USB (V4L2) camera, rotates them, restricts // the stream to NV12/1080p/30fps, and displays the result through Wayland. void create_and_execute_pipeline(const std::string &device) { // Captures frames from the USB camera source. Element source("v4l2src", "source"); source.set("device", device); // Applies geometric transforms to video frames. Element transform("qtivtransform", "transform"); // Stream filters used in branch links. // They define specific stream characteristics from the supported options. auto videofilter = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30); // Render video stream on display. // // sync=false disables strict rendering synchronization to the pipeline clock. // fullscreen=true renders the output fullscreen on the target display. Element display("waylandsink", "display"); display.set("sync", false); display.set("fullscreen", true); // Creates the pipeline, adds elements, links them explicitly, and executes it. Pipeline pipeline("usb-cam-pipeline"); pipeline.add(source) .add(transform) .add_stream_filter("videofilter", videofilter) .add(display) .link("source", "transform", "videofilter", "display") .execute(); } int main(int argc, char **argv) { // Route GStreamer logs through the QIM SDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); // V4L2 device node to capture from, defaults to /dev/video2. Pass -d/--device to override. std::string device = "/dev/video2"; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if ((arg == "-d" || arg == "--device") && i + 1 < argc) { device = argv[++i]; } } try { create_and_execute_pipeline(device); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} /usr/bin/qimsdk_ref_usb_camera --input-config /dev/video2 ``` The application connects to the USB camera, restricts the stream to NV12/1080p/30fps, and renders it fullscreen on the display. The live USB camera feed is rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_usb_camera`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_usb_camera) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) Check application source code on GitHub: [`qimsdk_ref_usb_camera_impl`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_usb_camera_impl) Pre-built application on device: `/usr/bin/qimsdk_ref_usb_camera_impl` ```cpp theme={null} #include #include using namespace qti; // Example pipeline: // // source → transform → [videofilter] → display // // The pipeline reads frames from a USB (V4L2) camera, rotates them, restricts // the stream to NV12/1080p/30fps, and displays the result through Wayland. void create_and_execute_pipeline(const std::string &device) { // Creates the pipeline, adds and links elements, and executes it. // // Elements are created on the fly as they are added, and linking is // implicit, following the order in which elements are added. // // sync=false disables strict rendering synchronization to the pipeline clock. // fullscreen=true renders the output fullscreen on the target display. Pipeline pipeline("usb-cam-pipeline"); pipeline.add("v4l2src", "source", "device", device) .add("qtivtransform", "transform") .add_stream_filter("videofilter", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) .add("waylandsink", "display", "sync", false, "fullscreen", true) .execute(); } int main(int argc, char **argv) { // Route GStreamer logs through the QIM SDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); // V4L2 device node to capture from, defaults to /dev/video2. Pass -d/--device to override. std::string device = "/dev/video2"; for (int i = 1; i < argc; ++i) { std::string arg = argv[i]; if ((arg == "-d" || arg == "--device") && i + 1 < argc) { device = argv[++i]; } } try { create_and_execute_pipeline(device); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} /usr/bin/qimsdk_ref_usb_camera_impl --input-config /dev/video2 ``` The application connects to the USB camera, restricts the stream to NV12/1080p/30fps, and renders it fullscreen on the display. The live USB camera feed is rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_usb_camera_impl`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_usb_camera_impl) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) See the full YAML configuration here: [qimsdk\_ref\_usb\_camera.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_usb_camera.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) Pre-built application on device: `/usr/bin/qimsdk_ref_yml` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_usb_camera.yaml` ```yaml theme={null} pipeline: elements: - type: v4l2src name: source device: /dev/video2 - type: qtivtransform name: transform - type: filter name: videostream video: format: NV12 width: 1920 height: 1080 framerate: 30 - type: waylandsink name: display sync: false fullscreen: true ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} qimsdk_ref_yml /etc/qimsdk/qimsdk_ref_usb_camera.yaml ``` The application connects to the USB camera, restricts the stream to NV12/1080p/30fps, and renders it fullscreen on the display. The live USB camera feed is rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Built-in Camera Preview This pipeline includes a built-in camera node, a stream filter, and a display node. If the device has more than one built-in camera, you can specify which one to use by providing the camera's unique ID. The stream filter defines the camera's output format, resolution, and framerate. You can also configure the display settings, such as enabling fullscreen rendering. IQ9 has no onboard ISP camera. You can attach an external IMX camera (e.g. from RB3 Gen2) instead — see [ISP Camera (Config #2 / qticamsrc)](/advanced/debugging#isp-camera-config-2--qticamsrc) for the procedure to switch from `libcamera` to `qticamsrc`. Introduction Check application source code on GitHub: [`qimsdk_ref_camera`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera) Pre-built application on device: `/usr/bin/qimsdk_ref_camera` ```cpp theme={null} #include #include using namespace qti; // Example pipeline: // // source → [videostream] → display // // The pipeline reads camera frames, runs ML inference and postprocessing, // and displays the result through Wayland. void create_and_execute_pipeline() { // Captures frames from the camera source. Element source("qtiqmmfsrc", "source"); // Render video stream on display. // // async=false enforce state transition to ensure the buffers are returned on time. // sync=false disables strict rendering synchronization to the pipeline clock. // fullscreen=true renders the output fullscreen on the target display. Element display("waylandsink", "display"); display.set("sync", false); display.set("fullscreen", true); // Stream filters used in branch links. // They define specific stream characteristics from the supported options. auto videostream = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30); // Creates the pipeline, adds and links elements, and executes it. // // Linking is implicit and follows the order in which elements are added. Pipeline pipeline("cam-pipeline"); pipeline.add(source) .add_stream_filter("videostream", videostream) .add(display) .execute(); } int main() { // Route GStreamer logs through the QIMSDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); try { create_and_execute_pipeline(); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` Check application source code on GitHub: [`qimsdk_ref_camera_impl`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_impl) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_impl` ```cpp theme={null} #include #include using namespace qti; void create_and_execute_pipeline() { Pipeline pipeline("cam-pipeline"); pipeline.add("qtiqmmfsrc", "source") .add_stream_filter("videostream", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) .add("waylandsink", "display", "sync", false, "fullscreen", true) .execute(); } int main() { qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); try { create_and_execute_pipeline(); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` ```bash theme={null} /usr/bin/qimsdk_ref_camera ``` The application captures live camera frames and renders them fullscreen on the display. A live preview of the camera feed is rendered fullscreen on the display. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera), [`qimsdk_ref_camera_impl`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_impl) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Built-in Camera Preview with Still Capture The application captures a still image a few seconds after starting and then stops automatically — this is expected behavior, not a hang. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_and_capture`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_and_capture) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_and_capture` ```cpp theme={null} #include #include #include #include using namespace qti; static const std::string home_path = std::getenv("HOME") ? std::getenv("HOME") : ""; // Example pipeline: // // source → [vf] → display → [if] → imagesink // // The pipeline reads camera frames, runs ML inference and postprocessing, // and displays the result through Wayland. void create_and_execute_pipeline() { // Captures frames from the camera source. Element source("qtiqmmfsrc", "source"); // Render video stream on display. // // async=false enforce state transition to ensure the buffers are returned on time. // sync=false disables strict rendering synchronization to the pipeline clock. // fullscreen=true renders the output fullscreen on the target display. Element display("waylandsink", "display"); display.set("async", false); display.set("sync", false); display.set("fullscreen", true); // Writes output buffers to files. Element imagesink("multifilesink", "imagesink"); imagesink.set("enable-last-sample", false); imagesink.set("location", home_path + "/Downloads/qimsdk_samples/media/image_%d.jpeg"); // Stream filters used in branch links. // They define specific stream characteristics from the supported options. auto vf = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30); auto image_filter = ImageFilter().format("jpeg").resolution(3840, 2160); // Creates the pipeline, adds and links elements, and executes it. // // Explicit linking is applied Pipeline pipeline("cam-pipeline"); pipeline.add(source) .add_stream_filter("vf", vf) .add(display) .add_stream_filter("if", image_filter) .add(imagesink) .link("source", "vf", "display") .link("source", "if", "imagesink") .start(); std::this_thread::sleep_for(std::chrono::seconds(2)); auto cam = pipeline.get("source"); cam.image_capture(); std::this_thread::sleep_for(std::chrono::seconds(2)); pipeline.stop(); } int main() { if (home_path.empty()) { std::cerr << "Error: HOME environment variable is not set." << std::endl; return 1; } // Route GStreamer logs through the QIMSDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); try { create_and_execute_pipeline(); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` ```bash theme={null} /usr/bin/qimsdk_ref_camera_and_capture ``` The application displays a live preview and captures a still JPEG image a few seconds after starting, saving it under `~/media/image_%d.jpeg`. A live preview is rendered, followed by a still JPEG image saved to `~/Downloads/qimsdk_samples/media/image_%d.jpeg`. The application then stops automatically. * **Source code:** [`qimsdk_ref_camera_and_capture`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_and_capture) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Camera Recording to MP4 This pipeline captures frames from the built-in camera, encodes them into an H.264 stream using the hardware encoder, parses the bitstream, muxes it into an MP4 container, and writes the resulting file to disk. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_encoder`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_encoder) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_encoder` ```cpp theme={null} #include #include #include using namespace qti; static const std::string home_path = std::getenv("HOME") ? std::getenv("HOME") : ""; // Example pipeline: // // source -> [vf] -> encoder -> parser -> muxer -> sink // // The pipeline reads camera frames, encodes them with H.264, muxes into MP4, // and writes the output to disk. void create_and_execute_pipeline() { // Captures frames from the camera source. Element source("qtiqmmfsrc", "source"); // Encodes raw video frames into H.264 stream. Element encoder("v4l2h264enc", "encoder"); encoder.set("output-io-mode", "dmabuf-import"); encoder.set("capture-io-mode", "dmabuf"); // Parses H.264 bitstream for downstream muxing. Element parser("h264parse", "parser"); // Muxes encoded stream into MP4 container. Element muxer("mp4mux", "muxer"); // Writes output stream to a file. Element sink("filesink", "sink"); sink.set("location", home_path + "/Downloads/qimsdk_samples/media/encoder_output.mp4"); // Stream filters used in branch links. // They define specific stream characteristics from the supported options. auto vf = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30); // Creates the pipeline, adds and links elements, and executes it. // // Explicit linking is applied. Pipeline pipeline("cam-encoder-pipeline"); pipeline.add(source) .add_stream_filter("vf", vf) .add(encoder) .add(parser) .add(muxer) .add(sink) .eos(true) .execute(); } int main() { if (home_path.empty()) { std::cerr << "Error: HOME environment variable is not set." << std::endl; return 1; } // Route GStreamer logs through the QIMSDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); try { create_and_execute_pipeline(); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` ```bash theme={null} /usr/bin/qimsdk_ref_camera_encoder ``` The application records camera frames encoded as H.264 into an MP4 file saved to `~/Downloads/qimsdk_samples/media/encoder_output.mp4`. The recorded MP4 file is written to `~/Downloads/qimsdk_samples/media/encoder_output.mp4` and can be played back to verify the camera recording. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera_encoder`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_encoder) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### MP4 File Playback This pipeline ingests an MP4 file, demultiplexes it to extract the video track, decodes the compressed stream (e.g., H.264/H.265) into raw frames, converts (or directly negotiates) the output to NV12 — a hardware-friendly YUV 4:2:0 semi-planar format — and then renders the frames to the display using a sink that supports zero-copy or hardware buffers. Introduction Check application source code on GitHub: [`qimsdk_ref_qtdemux_decode_display`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_qtdemux_decode_display) Pre-built application on device: `/usr/bin/qimsdk_ref_qtdemux_decode_display` ```cpp theme={null} #include #include #include using namespace qti; static const std::string home_path = std::getenv("HOME") ? std::getenv("HOME") : ""; // Example pipeline: // // src → demux → parse → decoder → [videofilter] → display // // The pipeline reads an MP4/H.264 file, decodes it through the hardware decoder, // overlays detected objects, and displays the result through Wayland. void create_and_execute_pipeline() { // Reads the input media file as raw bytes. Element src("filesrc", "src"); src.set("location", home_path + "/Downloads/qimsdk_samples/media/ai_demo_sample.mp4"); // Extracts elementary streams from the MP4 container. Element demux("qtdemux", "demux"); // Prepares the H.264 bitstream for the decoder. Element parse("h264parse", "parse"); // Decodes the compressed H.264 stream into raw video frames. // // The I/O mode is configured to enforce DMA buffer usage, // avoiding unnecessary buffer copies. Element decoder("v4l2h264dec", "decoder"); decoder.set("output-io-mode", 4); decoder.set("capture-io-mode", 4); // Render video stream on display. // // async=false enforce state transition to ensure the buffers are returned on time. // sync=true keeps rendering synchronized to the pipeline clock. // fullscreen=true renders the output fullscreen on the target display. Element display("waylandsink", "display"); display.set("fullscreen", true); // Stream filters used in branch links. // They define specific stream characteristics from the supported options. auto videofilter = VideoFilter().format("NV12"); // Creates the pipeline, adds and links elements, and executes it. // // Linking is implicit and follows the order in which elements are added. Pipeline pipeline("video-pipeline"); pipeline.add(src) .add(demux) .add(parse) .add(decoder) .add_stream_filter("videofilter", videofilter) .add(display); pipeline.start().wait().stop(); } int main() { if (home_path.empty()) { std::cerr << "Error: HOME environment variable is not set." << std::endl; return 1; } // Route GStreamer logs through the QIMSDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); try { create_and_execute_pipeline(); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` | File | Download | Save as | | - | - | - | | Sample video | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | `ai_demo_sample.mp4` | You can also use your own MP4 (H.264) file, or update the pipeline's `location` property to point to it. ```bash theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media" scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_qtdemux_decode_display ``` The application decodes the video file and renders it fullscreen on the display. The MP4 video plays back fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_qtdemux_decode_display`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_qtdemux_decode_display) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Transport Stream (.ts) File Playback This pipeline is similar to the Offline Video Source pipeline, but ingests an MPEG transport stream (`.ts`) file instead of an MP4 container. It demultiplexes the transport stream using `tsdemux` to extract the video track, decodes the compressed H.264 stream into raw frames, converts (or directly negotiates) the output to NV12, and renders the frames to the display. Introduction Check application source code on GitHub: [`qimsdk_ref_tsdemux_decode_display`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_tsdemux_decode_display) Pre-built application on device: `/usr/bin/qimsdk_ref_tsdemux_decode_display` ```cpp theme={null} #include #include #include using namespace qti; static const std::string home_path = std::getenv("HOME") ? std::getenv("HOME") : ""; // Example pipeline: // // src → demux → [vf1] → parse → decoder → [vf2] → display // // The pipeline reads an MPEG transport stream (.ts) file, demultiplexes it, // decodes the H.264 stream through the hardware decoder, and displays the // result through Wayland. void create_and_execute_pipeline() { // Reads a sequence of input files as stream data. Element src("multifilesrc", "src"); src.set("location", home_path + "/Downloads/qimsdk_samples/media/ai_demo_sample.ts"); src.set("stop-index", 0); // Extracts elementary streams from the transport stream. Element demux("tsdemux", "demux"); // Prepares the H.264 bitstream for the decoder. Element parse("h264parse", "parse"); // Decodes the compressed H.264 stream into raw video frames. // // The I/O mode is configured to enforce DMA buffer usage, // avoiding unnecessary buffer copies. Element decoder("v4l2h264dec", "decoder"); decoder.set("output-io-mode", 4); decoder.set("capture-io-mode", 4); // Render video stream on display. // // async=false enforce state transition to ensure the buffers are returned on time. // sync=true keeps rendering synchronized to the pipeline clock. // fullscreen=true renders the output fullscreen on the target display. Element display("waylandsink", "display"); display.set("fullscreen", true); auto vf1 = H264Filter().framerate(30); // Stream filters used in branch links. // They define specific stream characteristics from the supported options. auto vf2 = VideoFilter().format("NV12"); // Creates the pipeline, adds and links elements, and executes it. // // Linking is implicit and follows the order in which elements are added. Pipeline pipeline("video-pipeline"); pipeline.add(src) .add(demux) .add_stream_filter("vf1", vf1) .add(parse) .add(decoder) .add_stream_filter("vf2", vf2) .add(display); pipeline.start().wait().stop(); } int main() { if (home_path.empty()) { std::cerr << "Error: HOME environment variable is not set." << std::endl; return 1; } // Route GStreamer logs through the QIMSDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); try { create_and_execute_pipeline(); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` | File | Download | Save as | | - | - | - | | Sample transport stream | [ai\_demo\_sample.ts](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/raw/refs/heads/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.ts) | `ai_demo_sample.ts` | ```bash theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media" scp ai_demo_sample.ts @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_tsdemux_decode_display ``` The application decodes the transport stream file and renders it fullscreen on the display. The transport stream video plays back fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_tsdemux_decode_display`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_tsdemux_decode_display) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Synthetic Test Pattern (VideoFilter API) This pipeline generates synthetic test video frames using `videotestsrc` (a bouncing ball pattern), instead of reading from a camera or file. The frames are restricted to NV12/1080p/30fps, rotated 180 degrees by [`qtivtransform`](/plugin-reference/qtivtransform), downscaled to 1280x720 by a second stream filter, and rendered to the display. This is a useful pipeline for testing display and transform stages without any camera or media file dependency. Introduction Check application source code on GitHub: [`qimsdk_ref_videotestsrc_videofilter_display`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_videotestsrc_videofilter_display) Pre-built application on device: `/usr/bin/qimsdk_ref_videotestsrc_videofilter_display` ```cpp theme={null} #include #include using namespace qti; // Example pipeline: // // src → [vf1] → transform → [vf2] → sink // // The pipeline generates synthetic test video frames, rotates them, restricts // the stream to NV12/1080p/30fps, downscales to 720p, and displays the result // through Wayland. void create_and_execute_pipeline() { // Generates synthetic test video frames. Element src("videotestsrc", "src"); src.set("pattern", "ball"); // Applies geometric transforms to video frames. Element transform("qtivtransform", "transform"); transform.set("rotate", "180"); // Render video stream on display. Element sink("waylandsink", "sink"); sink.set("fullscreen", true); // Stream filters used in branch links. // They define specific stream characteristics from the supported options. auto vf1 = qti::VideoFilter().format("NV12").resolution(1920, 1080).framerate(30); auto vf2 = qti::VideoFilter().resolution(1280, 720); // Creates the pipeline, adds and links elements, and executes it. // // Linking is implicit and follows the order in which elements are added. Pipeline pipeline("video-pipeline"); pipeline.add(src) .add_stream_filter("vf1", vf1) .add(transform) .add_stream_filter("vf2", vf2) .add(sink) .execute(); } int main() { // Route GStreamer logs through the QIMSDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); try { create_and_execute_pipeline(); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` ```bash theme={null} /usr/bin/qimsdk_ref_videotestsrc_videofilter_display ``` The application generates a synthetic bouncing-ball test pattern, rotates and rescales it, and renders it fullscreen on the display. A rotated, rescaled bouncing-ball test pattern is rendered fullscreen on the display. * **Source code:** [`qimsdk_ref_videotestsrc_videofilter_display`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_videotestsrc_videofilter_display) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Synthetic Test Pattern (Raw Caps StreamFilter) This pipeline is functionally identical to the previous example, but it defines its stream constraints using raw GStreamer caps strings via `StreamFilter` (e.g. `"video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1"`), instead of the structured `VideoFilter` builder API. This is useful when you need to express filter capabilities that aren't covered by the structured builder, or when porting caps strings directly from existing GStreamer pipelines. Introduction Check application source code on GitHub: [`qimsdk_ref_videotestsrc_streamfilter_display`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_videotestsrc_streamfilter_display) Pre-built application on device: `/usr/bin/qimsdk_ref_videotestsrc_streamfilter_display` ```cpp theme={null} #include #include using namespace qti; // Example pipeline: // // src → [vf1] → transform → [vf2] → sink // // The pipeline generates synthetic test video frames, rotates them, restricts // the stream using raw caps strings, and displays the result through Wayland. void create_and_execute_pipeline() { // Generates synthetic test video frames. Element src("videotestsrc", "src"); src.set("pattern", "ball"); // Applies geometric transforms to video frames. Element transform("qtivtransform", "transform"); transform.set("rotate", "180"); // Render video stream on display. Element sink("waylandsink", "sink"); sink.set("fullscreen", true); // Stream filters expressed as raw GStreamer caps strings, used in branch links. qti::StreamFilter vf1( "video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1"); qti::StreamFilter vf2("video/x-raw,width=1280,height=720"); // Creates the pipeline, adds and links elements, and executes it. // // Linking is implicit and follows the order in which elements are added. Pipeline pipeline("video-pipeline-streamfilter-string"); pipeline.add(src) .add_stream_filter("vf1", vf1) .add(transform) .add_stream_filter("vf2", vf2) .add(sink) .execute(); } int main() { // Route GStreamer logs through the QIMSDK logger and enable debug output. qti::SetImsdkGstLogMode(qti::ImsdkGstLogMode::ImsdkLog); qti::SetImsdkLogLevel(qti::ImsdkLogLevel::Debug); try { create_and_execute_pipeline(); } catch (const std::exception &ex) { std::cerr << "Exception: " << ex.what() << std::endl; return 1; } return 0; } ``` ```bash theme={null} /usr/bin/qimsdk_ref_videotestsrc_streamfilter_display ``` The application generates a synthetic bouncing-ball test pattern, rotates and rescales it, and renders it fullscreen on the display. A rotated, rescaled bouncing-ball test pattern is rendered fullscreen on the display. * **Source code:** [`qimsdk_ref_videotestsrc_streamfilter_display`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_videotestsrc_streamfilter_display) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### RTSP Camera Stream Playback This pipeline connects to a network RTSP camera stream instead of a local (USB/built-in) camera or file. `rtspsrc` establishes the RTSP session and negotiates the H.264 RTP stream, a caps filter restricts it to H.264 video, `rtph264depay` extracts the H.264 payload from the RTP packets, `h264parse` prepares the bitstream for the hardware decoder, and the decoded frames are converted to NV12 and rendered to the display. Introduction See the full YAML configuration here: [qimsdk\_ref\_rtsp\_camera.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_rtsp_camera.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) Pre-built application on device: `/usr/bin/qimsdk_ref_yml` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_rtsp_camera.yaml` ```yaml theme={null} pipeline: elements: - type: rtspsrc name: source location: rtsp://username:password@192.168.1.188:554/Streaming/Channels/101 latency: 0 - type: capsfilter name: rtp_caps caps: application/x-rtp,media=video,encoding-name=H264 - type: rtph264depay name: depay - type: h264parse name: parse - type: v4l2h264dec name: decoder capture-io-mode: 4 output-io-mode: 4 - type: filter name: videostream video: format: NV12 - type: waylandsink name: display sync: false fullscreen: true ``` On the device, edit `/etc/qimsdk/qimsdk_ref_rtsp_camera.yaml` and update the `location` property with your RTSP camera's username, password, IP address, and stream path. ```bash theme={null} qimsdk_ref_yml /etc/qimsdk/qimsdk_ref_rtsp_camera.yaml ``` The application connects to the RTSP camera, decodes the H.264 stream, and renders it fullscreen on the display. The live RTSP camera stream plays back fullscreen on the display. To stop the application, press **CTRL + C**. * **Source code:** [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ## AI Pipeline ### Video-to-Tensor Dump (Offline Preprocessing) This pipeline ingests an MP4 file, demultiplexes it to extract the video track, decodes the compressed stream (e.g., H.264/H.265) into raw frames, converts (or directly negotiates) the output to NV12 — a hardware-friendly YUV 4:2:0 semi-planar format — and then goes through [`qtimlvconverter`](/plugin-reference/qtimlvconverter) (ML preprocessing). The `TensorFilter` stream filter describes the tensor format and shape as UINT8 with dimensions \[1, 520, 520, 3] (batch, height, width, channels). Finally, `multifilesink` writes the preprocessed tensors out as sequential `.rgb` files to `~/Downloads/qimsdk_samples/media/tensor_520_520_%d.rgb`, rolling after every 10 files. Introduction Check application source code on GitHub: [`qimsdk_ref_tensor_dump`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_tensor_dump) Pre-built application on device: `/usr/bin/qimsdk_ref_tensor_dump` | File | Download | Save as | | - | - | - | | Sample video | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | `ai_demo_sample.mp4` | You can also use your own MP4 (H.264) file, or update the pipeline's `location` property to point to it. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media" scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_tensor_dump ``` The application writes preprocessed tensors to `~/Downloads/qimsdk_samples/media/tensor_520_520_%d.rgb`. Sequential `.rgb` tensor files accumulate under `~/Downloads/qimsdk_samples/media/`, rolling after every 10 files. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_tensor_dump`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_tensor_dump) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Live Camera YOLOv8 Detection (Manual Chain) This pipeline captures live video from the on-device camera, then drives a full detection-and-visualization loop in real time: the raw NV12 stream is normalized and preprocessed into one or more tensors, fed to a TFLite inference stage (running on the external QNN delegate for efficient hardware execution), and the resulting tensors are postprocessed into semantic detections (classes, boxes, confidences). Those results are converted into QIM SDK-friendly metadata and attached back to the original video timeline, so the visual stream and its ML annotations stay synchronized. A parallel branch from the early tee ensures the original frames are available to a metadata muxer, which merges the detections with the video. Finally, the overlay stage renders the boxes/labels directly on the live frames, and the composed output is presented fullscreen to the display. In short, the graph turns a live camera feed into actionable on-screen insights by (1) creating a tensor view of the stream, (2) running detection, (3) translating inference outputs into QIM SDK metadata, (4) fusing metadata with the original frames, and (5) drawing the results on top of the video for low-latency, on-screen visualization. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_yolov8) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_yolov8` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_camera_yolov8 ``` The application overlays detected bounding boxes and labels on the live camera feed and renders it fullscreen on the display. The live camera feed is rendered fullscreen with detected objects outlined by bounding boxes and labeled with class names. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_yolov8) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) See the full YAML configuration here: [qimsdk\_ref\_camera\_yolov8.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_camera_yolov8.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) Pre-built application on device: `/usr/bin/qimsdk_ref_yml` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_camera_yolov8.yaml` ```yaml theme={null} pipeline: elements: - type: qtiqmmfsrc name: source camera: 0 - type: filter name: videostream video: format: NV12 width: 1920 height: 1080 framerate: 30 - type: tee name: split - type: queue name: q1 - type: qtimlvconverter name: preprocessing - type: queue name: q3 - type: qtimltflite name: inferencing delegate: external external-delegate-path: libQnnTFLiteDelegate.so external-delegate-options: "QNNExternalDelegate,backend_type=htp;" model: ~/Downloads/qimsdk_samples/models/yolov8_det_quantized.tflite - type: queue name: q4 - type: qtimlpostprocess name: postprocessing results: 5 module: yolov8 labels: ~/Downloads/qimsdk_samples/labels/yolov8.json settings: '{"confidence": 70.0}' - type: filter name: mlf text: {} - type: qtimetamux name: mlmuxer - type: queue name: q5 - type: qtivoverlay name: overlay - type: waylandsink name: display sync: false fullscreen: true links: - [split, mlmuxer] - [source, videostream, split, q1, preprocessing, q3, inferencing, q4, postprocessing, mlf, mlmuxer, q5, overlay, display] ``` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} qimsdk_ref_yml /etc/qimsdk/qimsdk_ref_camera_yolov8.yaml ``` The application overlays detected bounding boxes and labels on the live camera feed and renders it fullscreen on the display. The live camera feed is rendered fullscreen with detected objects outlined by bounding boxes and labeled with class names. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Live Camera YOLOv8 Detection (ML Bin) This solution is identical to the previous one. The only difference is that here we use `qtimlvideotflitebin` instead of manually linking the preprocessing, inference, postprocessing, and ML muxer components, which simplifies the pipeline. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_yolov8_mlbin`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_yolov8_mlbin) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_yolov8_mlbin` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_camera_yolov8_mlbin ``` The application overlays detected bounding boxes and labels on the live camera feed and renders it fullscreen on the display. The live camera feed is rendered fullscreen with detected objects outlined by bounding boxes and labeled with class names. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera_yolov8_mlbin`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_camera_yolov8_mlbin) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) See the full YAML configuration here: [qimsdk\_ref\_mlbin\_yolov8.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_mlbin_yolov8.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) Pre-built application on device: `/usr/bin/qimsdk_ref_yml` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_mlbin_yolov8.yaml` ```yaml theme={null} pipeline: elements: - type: filesrc name: src location: ~/Downloads/qimsdk_samples/media/ai_demo_sample.mp4 - type: qtdemux name: demux - type: h264parse name: parse - type: v4l2h264dec name: decoder output-io-mode: 4 capture-io-mode: 4 - type: filter name: videofilter video: format: NV12 - type: qtimlvideotflitebin name: mlbin inference-delegate: "external" inference-external-delegate-path: "libQnnTFLiteDelegate.so" inference-external-delegate-options: "QNNExternalDelegate,backend_type=htp;" inference-model: "~/Downloads/qimsdk_samples/models/yolov8_det_quantized.tflite" postprocess-module: "yolov8" postprocess-labels: "~/Downloads/qimsdk_samples/labels/yolov8.json" - type: qtivoverlay name: overlay - type: waylandsink name: display fullscreen: true ``` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} qimsdk_ref_yml /etc/qimsdk/qimsdk_ref_mlbin_yolov8.yaml ``` The application overlays detected bounding boxes and labels on the live camera feed and renders it fullscreen on the display. The live camera feed is rendered fullscreen with detected objects outlined by bounding boxes and labeled with class names. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Offline Video YOLOv8 Detection (Manual Chain, Metamux Overlay) This pipeline is the offline-video counterpart to Live Camera YOLOv8 Detection (Manual Chain): instead of a live camera feed, an MP4 file is demuxed and decoded, then split by a `tee` into a display branch and an ML branch. The ML branch runs preprocessing, inference, and postprocessing manually, and the resulting detections are attached back onto the original frame via [`qtimetamux`](/plugin-reference/qtimetamux) before being overlaid and rendered. This Metamux-based approach is more flexible than the video composer approach (it allows chaining additional ML models or streaming metadata separately), whereas the composer overlay approach used in the next example offers better performance. Introduction Check application source code on GitHub: [`qimsdk_ref_yolov8_metamux_overlay`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yolov8_metamux_overlay) Pre-built application on device: `/usr/bin/qimsdk_ref_yolov8_metamux_overlay` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_yolov8_metamux_overlay ``` The application decodes the video file, runs YOLOv8 detection, attaches the detections to the original frame via [`qtimetamux`](/plugin-reference/qtimetamux), and overlays the results, rendering them fullscreen. The video plays back with YOLOv8 bounding boxes and class labels overlaid on each detected object, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_yolov8_metamux_overlay`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yolov8_metamux_overlay) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Offline Video YOLOv8 Detection (ML Bin, Composer Overlay) The AI output can be overlaid directly on top of the frame instead of attaching AI metadata to the main frame. This approach provides better performance, whereas the Metamux solution is more flexible and can be used for chaining AI models or streaming AI metadata separately. Introduction Check application source code on GitHub: [`qimsdk_ref_yolov8_composer_overlay`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yolov8_composer_overlay) Pre-built application on device: `/usr/bin/qimsdk_ref_yolov8_composer_overlay` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_yolov8_composer_overlay ``` The application composes the AI detection overlay directly on top of the decoded video frames and displays it fullscreen. The video plays back with YOLOv8 bounding boxes and class labels composited directly onto each frame, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_yolov8_composer_overlay`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yolov8_composer_overlay) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Gesture Recognition This pipeline demonstrates a daisy-chained, multi-model gesture recognition graph built entirely from standard SDK elements ([`qtimlvconverter`](/plugin-reference/qtimlvconverter), [`qtimltflite`](/plugin-reference/qtimltflite), [`qtimlpostprocess`](/plugin-reference/qtimlpostprocess), [`qtimetamux`](/plugin-reference/qtimetamux), `qtimetatransform`) rather than a custom postprocessing callback. Palm detection locates the hand region in the live camera frame; that region is transformed and fed into hand landmark detection; the resulting landmarks are passed through a gesture embedder followed by a canned gesture classifier to decode the recognized gesture. Two parallel [`qtimetamux`](/plugin-reference/qtimetamux) merge points combine metadata from each stage back onto the original frame before the final overlay is rendered to the display. Introduction Check application source code on GitHub: [`qimsdk_ref_gesture_recognition`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_gesture_recognition) Pre-built application on device: `/usr/bin/qimsdk_ref_gesture_recognition` See the full YAML configuration here: [qimsdk\_ref\_gesture\_recognition.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_gesture_recognition.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) Pre-built application on device: `/usr/bin/qimsdk_ref_yml` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_gesture_recognition.yaml` #### Download Required Files Download the gesture recognizer task bundle from Google MediaPipe to obtain the palm detection, hand landmark, gesture embedder, and canned gesture classifier models: ```bash theme={null} # Download the gesture recognizer task bundle wget https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/latest/gesture_recognizer.task # Extract the top-level task unzip gesture_recognizer.task # Extract hand landmarker models unzip hand_landmarker.task # → hand_detector.tflite, hand_landmarks_detector.tflite # Extract gesture recognizer models unzip hand_gesture_recognizer.task # → gesture_embedder.tflite, canned_gesture_classifier.tflite ``` | File | Save as | | - | - | | hand\_detector.tflite (see steps above) | palm\_detection\_full.tflite | | hand\_landmarks\_detector.tflite (see steps above) | hand\_landmark\_full.tflite | | gesture\_embedder.tflite (see steps above) | gesture\_embedder.tflite | | canned\_gesture\_classifier.tflite (see steps above) | canned\_gesture\_classifier.tflite | | palmd\_labels.json | palmd\_labels.json | | palmd\_settings.json | palmd\_settings.json | | hlandmarks.json | hlandmarks.json | | hlandmark\_settings.json | hlandmark\_settings.json | | gesture\_rec.json | gesture\_rec.json | If a downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` This pipeline's source is the on-device camera, so no video file needs to be copied. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp palm_detection_full.tflite @:~/Downloads/qimsdk_samples/models/ scp hand_landmark_full.tflite @:~/Downloads/qimsdk_samples/models/ scp gesture_embedder.tflite @:~/Downloads/qimsdk_samples/models/ scp canned_gesture_classifier.tflite @:~/Downloads/qimsdk_samples/models/ scp palmd_labels.json @:~/Downloads/qimsdk_samples/labels/ scp palmd_settings.json @:~/Downloads/qimsdk_samples/labels/ scp hlandmarks.json @:~/Downloads/qimsdk_samples/labels/ scp hlandmark_settings.json @:~/Downloads/qimsdk_samples/labels/ scp gesture_rec.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_gesture_recognition ``` The application captures live video from the on-device camera, runs the daisy-chained palm detection → hand landmark → gesture embedder → canned gesture classifier models, and overlays the recognized gesture on the display fullscreen. The live camera feed is rendered fullscreen with hand landmarks and the recognized gesture label overlaid. * **Source code:** [`qimsdk_ref_gesture_recognition`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_gesture_recognition), [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### PPE & Person/Foot Detection (ML Bin) This pipeline demonstrates a two-model ML bin pipeline for personal protective equipment (PPE) and person/foot detection: `foot_track_net` detects persons and their feet in the frame, while `gear_guard_net` detects PPE gear. Both models run through `qtimlvideotflitebin`, which internally handles preprocessing, inference, and postprocessing, simplifying the pipeline compared to manually chaining each stage. Introduction Check application source code on GitHub: [`qimsdk_ref_mlbin_ppe`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_mlbin_ppe) Pre-built application on device: `/usr/bin/qimsdk_ref_mlbin_ppe` See the full YAML configuration here: [qimsdk\_ref\_mlbin\_ppe.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_mlbin_ppe.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) Pre-built application on device: `/usr/bin/qimsdk_ref_yml` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_mlbin_ppe.yaml` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/models/gear_guard_net?searchTerm=gear) | gear\_guard\_net-ppe-detection-w8a8.tflite | | [Model](https://aihub.qualcomm.com/models/foot_track_net?searchTerm=foot) | foot\_track\_net-person-foot-detection-w8a8.tflite | | gear\_guard\_net.json | gear\_guard\_net.json | | foot\_track\_net.json | foot\_track\_net.json | | foot\_track\_net\_settings.json | foot\_track\_net\_settings.json | | [ppe\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/refs/heads/main/qualcomm-linux/artifacts/videos/demo_samples/ppe_sample.mp4) | ppe\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp gear_guard_net-ppe-detection-w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp foot_track_net-person-foot-detection-w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp gear_guard_net.json @:~/Downloads/qimsdk_samples/labels/ scp foot_track_net.json @:~/Downloads/qimsdk_samples/labels/ scp foot_track_net_settings.json @:~/Downloads/qimsdk_samples/labels/ scp ppe_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_mlbin_ppe ``` The application runs inference on the video file, detecting persons/feet with `foot_track_net` and PPE gear with `gear_guard_net`, overlaying the resulting detections on the video and rendering it fullscreen. The video plays back fullscreen with detected persons, feet, and PPE gear outlined by bounding boxes and labeled with class names. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_mlbin_ppe`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_mlbin_ppe), [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### YOLOv8 Detection (ML Bin) This pipeline reads an MP4/H.264 file, decodes it through the hardware decoder, and runs YOLOv8 object detection using `qtimlvideotflitebin`, which internally handles preprocessing, inference, and postprocessing. The detected objects are then overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_mlbin_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_mlbin_yolov8) Pre-built application on device: `/usr/bin/qimsdk_ref_mlbin_yolov8` See the full YAML configuration here: [qimsdk\_ref\_mlbin\_yolov8.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_mlbin_yolov8.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) Pre-built application on device: `/usr/bin/qimsdk_ref_yml` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_mlbin_yolov8.yaml` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_mlbin_yolov8 ``` The application decodes the video file, runs YOLOv8 detection through `qtimlvideotflitebin`, and overlays the detected objects on the video, rendering it fullscreen. The video plays back with YOLOv8 bounding boxes and class labels overlaid on each detected object, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_mlbin_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_mlbin_yolov8), [qimsdk\_ref\_yml](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_yml) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ## AppSink and AppSrc Introduction AppSink and AppSrc are special elements because they expose buffers directly to and from the application. Because of this, we create and configure these elements separately, using their dedicated wrapper classes. Once configured, we insert them into the pipeline as fully constructed element objects. Check application source code on GitHub: [`qimsdk_ref_appsrc_and_appsink`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_appsrc_and_appsink) Pre-built application on device: `/usr/bin/qimsdk_ref_appsrc_and_appsink` ```bash theme={null} /usr/bin/qimsdk_ref_appsrc_and_appsink ``` The application generates synthetic test frames, pushes them through an `AppSink`/`AppSrc` bridge, and renders the resulting stream fullscreen on the display. The synthetic test frames generated in application code are rendered fullscreen on the display after passing through the `AppSink`/`AppSrc` bridge. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_appsrc_and_appsink`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_appsrc_and_appsink) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ## Custom Pre and Post Processing Introduction ### Custom Preprocessing for YOLOv8 (Detection) QIM SDK plugin support different backend: GLES, OpenCV, etc. This level of interface could be exposed to application. This API takes all input and output at once in case of batching or daisy for example. It can be simplified by calling application implementation for every input. Advantage of this approach is that all complexity of batching, depth, daisy chain support remains hidden in the plugin and application will do only pre-processing on a single frame/tensor at once. Introduction Check application source code on GitHub: [`qimsdk_ref_external_preprocess_detection_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_preprocess_detection_yolov8) Pre-built application on device: `/usr/bin/qimsdk_ref_external_preprocess_detection_yolov8` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_preprocess_detection_yolov8 ``` The video plays back with YOLOv8 bounding boxes and class labels overlaid on each detected object, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_preprocess_detection_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_preprocess_detection_yolov8) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Preprocessing for YOLOv8 (ML Bin variant) This example demonstrates external preprocessing support inside `qtimlvideotflitebin`. Setting `preprocess-engine` to `none` disables the bin's internal preprocessing path and hands frame-to-tensor conversion off to an application-defined callback registered via `MLVideoTFLiteBin::set_preprocess_handler(...)`. The callback receives the decoded NV12 blit(s) and writes directly into the bin's tensor buffer, converting each pixel to the quantized int8 NHWC layout expected by the YOLOv8 model, with letterbox padding for any area the blit doesn't cover. Inference and postprocessing then run inside the same bin, and the detected objects are overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_external_preprocess_detection_mlbin_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_preprocess_detection_mlbin_yolov8) Pre-built application on device: `/usr/bin/qimsdk_ref_external_preprocess_detection_mlbin_yolov8` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_preprocess_detection_mlbin_yolov8 ``` The application decodes the video file, runs the custom, application-defined preprocessing callback inside `mlbin` to convert NV12 frames into the quantized int8 tensor expected by YOLOv8, then runs inference and postprocessing, overlaying the detected objects and rendering the result fullscreen. The video plays back with YOLOv8 bounding boxes and class labels overlaid on each detected object, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_preprocess_detection_mlbin_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_preprocess_detection_mlbin_yolov8) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Preprocessing with Lightweight Face Detection This example demonstrates external preprocessing with [`qtimlvconverter`](/plugin-reference/qtimlvconverter) (rather than `qtimlvideotflitebin`), so the pipeline links preprocessing, inference, and postprocessing manually through explicit queues, and merges the detections back onto the original frame via [`qtimetamux`](/plugin-reference/qtimetamux) before overlaying. Setting `engine` to `none` on the [`MLVConverter`](/plugin-reference/qtimlvconverter) element disables its internal preprocessing path and hands frame-to-tensor conversion off to an application-defined callback registered via [`MLVConverter::set_handler(...)`](/plugin-reference/qtimlvconverter). Unlike the YOLOv8 example, the `face_det_lite_w8a8.tflite` model consumes raw uint8 grayscale, so the callback resizes only the NV12 luma (Y) plane into a single-channel `[1, H, W, 1]` tensor, skipping color conversion and normalization entirely and leaving the chroma plane unread. A `tee` right after decoding splits the stream into a display branch and an ML branch, and a queue on each branch (including the display one) keeps the two branches from stalling each other on a shared thread. Introduction Check application source code on GitHub: [`qimsdk_ref_external_preprocess_lightweight_face_detect`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_preprocess_lightweight_face_detect) Pre-built application on device: `/usr/bin/qimsdk_ref_external_preprocess_lightweight_face_detect` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/face_det_lite) | face\_det\_lite\_w8a8.tflite | | qfd-labels.json | qfd-labels.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | face\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp face_det_lite_w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp qfd-labels.json @:~/Downloads/qimsdk_samples/labels/ scp face_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_preprocess_lightweight_face_detect ``` The application decodes the video file, runs the custom, application-defined preprocessing callback to convert the NV12 luma plane into the grayscale tensor expected by the lightweight face detector, then runs inference and postprocessing, merging the detected faces onto the original frame via [`qtimetamux`](/plugin-reference/qtimetamux), overlaying them, and rendering the result fullscreen. The video plays back fullscreen with detected faces outlined by bounding boxes. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_preprocess_lightweight_face_detect`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_preprocess_lightweight_face_detect) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Preprocessing with Palm Detection This example is the live-camera counterpart to the lightweight face detection pipeline: [`qtiqmmfsrc`](/plugin-reference/qticamsrc) captures frames directly instead of decoding a file, but the branching structure is otherwise identical — a `tee` right after the stream filter splits the pipeline into a display branch and an ML branch, each guarded by its own queue, and [`qtimetamux`](/plugin-reference/qtimetamux) merges the ML branch's detections back onto the original frame before overlay. Setting `engine` to `none` on the [`MLVConverter`](/plugin-reference/qtimlvconverter) element disables its internal preprocessing path and hands frame-to-tensor conversion off to an application-defined callback registered via [`MLVConverter::set_handler(...)`](/plugin-reference/qtimlvconverter). Unlike the grayscale face-detection example, `palm_detection_full.tflite` expects a `[1, H, W, 3]` float32 RGB tensor normalized to `[0, 1]`, so the callback converts the full NV12 image (both luma and chroma planes) to RGB and scales each channel into that range, with letterbox padding for any area the blit doesn't cover. Introduction Check application source code on GitHub: [`qimsdk_ref_external_preprocess_palm_detect`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_preprocess_palm_detect) Pre-built application on device: `/usr/bin/qimsdk_ref_external_preprocess_palm_detect` #### Download Required Files Download the gesture recognizer task bundle from Google MediaPipe to obtain the palm detection model: ```bash theme={null} # Download the gesture recognizer task bundle wget https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/latest/gesture_recognizer.task # Extract the top-level task unzip gesture_recognizer.task # Extract hand landmarker models unzip hand_landmarker.task # → hand_detector.tflite, hand_landmarks_detector.tflite ``` | File | Save as | | - | - | | hand\_detector.tflite (see steps above) | palm\_detection\_full.tflite | | palmd\_labels.json | palmd\_labels.json | | palmd\_settings.json | palmd\_settings.json | If a downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` This pipeline's source is the on-device camera, so no video file needs to be copied. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp palm_detection_full.tflite @:~/Downloads/qimsdk_samples/models/ scp palmd_labels.json @:~/Downloads/qimsdk_samples/labels/ scp palmd_settings.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_preprocess_palm_detect ``` The application captures live video from the on-device camera, runs the custom, application-defined preprocessing callback to convert NV12 frames into the normalized float32 RGB tensor expected by the palm detector, then runs inference and postprocessing, merging the detected palms onto the original frame via [`qtimetamux`](/plugin-reference/qtimetamux), overlaying them, and rendering the result fullscreen. The live camera feed is rendered fullscreen with detected palms outlined by bounding boxes. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_preprocess_palm_detect`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_preprocess_palm_detect) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Postprocessing with YOLOv5 (Detection) This example demonstrates the object detection postprocessing callback (`ObjectDetectionPostprocessCallback`) using a YOLOv5 model. Preprocessing and inference are handled by the SDK, and the raw output tensors are decoded into bounding boxes, class names, and confidence scores by an application-defined callback, which are then overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_detection_yolov5`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_detection_yolov5) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_detection_yolov5` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/models/yolov5?searchTerm=yolov5) | yolov5m-320x320-int8.tflite | | yolov5m.json | yolov5m.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov5m-320x320-int8.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov5m.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_postprocess_detection_yolov5 ``` The application runs inference on the video file and uses the custom, application-defined postprocessing callback to decode detections, which are then overlaid and displayed fullscreen. The video plays back with YOLOv5 bounding boxes and class labels decoded by the custom postprocessing callback, overlaid and rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_detection_yolov5`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_detection_yolov5) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Post processing with ML Bin Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_mlbin_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_mlbin_yolov8) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_mlbin_yolov8` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_postprocess_mlbin_yolov8 ``` The application runs preprocessing, inference, and the custom, application-defined postprocessing callback all inside `mlbin`, overlaying the resulting detections on the video and rendering it fullscreen. The video plays back with YOLOv8 bounding boxes and class labels decoded by the custom postprocessing callback, overlaid and rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_mlbin_yolov8`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_mlbin_yolov8) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Postprocessing with ResNet101 (Classification) This example demonstrates the classification postprocessing callback (`ClassificationPostprocessCallback`). Preprocessing and inference are handled by the SDK, and the raw output tensors from a ResNet101 model are decoded into labeled classification results (name and confidence) by an application-defined callback. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_classification_resnet101`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_classification_resnet101) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_classification_resnet101` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/resnet101) | Resnet101\_Quantized.tflite | | resnet101.json | resnet101.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | classification\_sample.mp4 | No ideal sample video ships with the SDK for this use case. Supply your own content for best results. If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp Resnet101_Quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp resnet101.json @:~/Downloads/qimsdk_samples/labels/ scp classification_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_postprocess_classification_resnet101 ``` The application runs inference on the ResNet101 model and uses the custom, application-defined postprocessing callback to decode classification results, which are then displayed. The video plays back with the predicted classification label and confidence score displayed on screen for each frame. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_classification_resnet101`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_classification_resnet101) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Postprocessing with MiDaS v2 (Depth Estimation) This example demonstrates the depth estimation postprocessing callback (`DepthEstimationPostprocessCallback`). Preprocessing and inference are handled by the SDK, and the raw output tensors from a MiDaS v2 model are decoded by an application-defined callback into a per-pixel depth map (values and pseudo-color) for downstream visualization. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_depth_estimation_midasv2`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_depth_estimation_midasv2) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_depth_estimation_midasv2` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/models/midas) | midas-tflite-w8a8.tflite | | midas-v2-labels.json | midas-v2-labels.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp midas-tflite-w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp midas-v2-labels.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_postprocess_depth_estimation_midasv2 ``` The application runs inference on the MiDaS v2 model and uses the custom, application-defined postprocessing callback to decode the depth map, which is then rendered. The video plays back with a pseudo-colored depth map rendered fullscreen, where color represents estimated per-pixel depth. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_depth_estimation_midasv2`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_depth_estimation_midasv2) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Postprocessing with Gesture Recognition This example demonstrates gesture recognition using a daisy-chained, multi-model pipeline: palm detection locates the hand region, hand landmark detection extracts keypoints from that region, and a gesture embedder followed by a canned gesture classifier decode the landmarks into a recognized gesture. Preprocessing and inference are handled by the SDK, and the raw output tensors are decoded by an application-defined postprocessing callback. The pipeline uses the on-device camera as its source. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_gesture_recognition`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_gesture_recognition) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_gesture_recognition` #### Download Required Files Download the gesture recognizer task bundle from Google MediaPipe to obtain the palm detection, hand landmark, gesture embedder, and canned gesture classifier models: ```bash theme={null} # Download the gesture recognizer task bundle wget https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/latest/gesture_recognizer.task # Extract the top-level task unzip gesture_recognizer.task # Extract hand landmarker models unzip hand_landmarker.task # → hand_detector.tflite, hand_landmarks_detector.tflite # Extract gesture recognizer models unzip hand_gesture_recognizer.task # → gesture_embedder.tflite, canned_gesture_classifier.tflite ``` | File | Save as | | - | - | | hand\_detector.tflite (see steps above) | palm\_detection\_full.tflite | | hand\_landmarks\_detector.tflite (see steps above) | hand\_landmark\_full.tflite | | gesture\_embedder.tflite (see steps above) | gesture\_embedder.tflite | | canned\_gesture\_classifier.tflite (see steps above) | canned\_gesture\_classifier.tflite | | palmd\_labels.json | palmd\_labels.json | | hlandmarks.json | hlandmarks.json | | gesture\_rec.json | gesture\_rec.json | If a downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp palm_detection_full.tflite @:~/Downloads/qimsdk_samples/models/ scp hand_landmark_full.tflite @:~/Downloads/qimsdk_samples/models/ scp gesture_embedder.tflite @:~/Downloads/qimsdk_samples/models/ scp canned_gesture_classifier.tflite @:~/Downloads/qimsdk_samples/models/ scp palmd_labels.json @:~/Downloads/qimsdk_samples/labels/ scp hlandmarks.json @:~/Downloads/qimsdk_samples/labels/ scp gesture_rec.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_postprocess_gesture_recognition ``` The application captures live video from the on-device camera, runs the daisy-chained palm detection → hand landmark → gesture embedder → canned gesture classifier models, and uses the custom, application-defined postprocessing callback to decode the recognized gesture, which is then overlaid and displayed fullscreen. The live camera feed is rendered fullscreen with the recognized gesture label overlaid. * **Source code:** [`qimsdk_ref_external_postprocess_gesture_recognition`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_gesture_recognition) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Postprocessing with HRNet (Pose Estimation) This example demonstrates the pose estimation postprocessing callback (`PoseEstimationPostprocessCallback`) using a two-stage pipeline: a YOLOv5 model detects persons in the frame, and an HRNet model estimates keypoints for each detected person. Preprocessing and inference are handled by the SDK, and the raw output tensors are decoded into keypoints and skeleton links by an application-defined callback, which are then overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_pose_estimation_hrnet`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_pose_estimation_hrnet) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_pose_estimation_hrnet` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/models/yolov5?searchTerm=yolov5) | yolov5m-320x320-int8.tflite | | [Model](https://aihub.qualcomm.com/models/hrnet_pose?searchTerm=hr) | hrnet\_pose\_w8a8.tflite | | yolov5m.json | yolov5m.json | | hrnet.json | hrnet.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | pose\_sample.mp4 | No ideal sample video ships with the SDK for this use case. Supply your own content for best results. If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov5m-320x320-int8.tflite @:~/Downloads/qimsdk_samples/models/ scp hrnet_pose_w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov5m.json @:~/Downloads/qimsdk_samples/labels/ scp hrnet.json @:~/Downloads/qimsdk_samples/labels/ scp pose_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_postprocess_pose_estimation_hrnet ``` The application runs inference on the video file, first detecting persons with YOLOv5 and then estimating pose keypoints with HRNet, using the custom, application-defined postprocessing callback to decode the results, which are then overlaid and displayed fullscreen. The video plays back fullscreen with detected persons' pose keypoints and skeleton links overlaid. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_pose_estimation_hrnet`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_pose_estimation_hrnet) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) ### Custom Postprocessing with DeepLabv3 (Segmentation) This example demonstrates the segmentation postprocessing callback (`SegmentationPostprocessCallback`) using a DeepLabv3 model. Preprocessing and inference are handled by the SDK, and the raw output tensors are decoded into per-pixel semantic labels and colors by an application-defined callback, which are then overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_segmentation_deeplab`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_segmentation_deeplab) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_segmentation_deeplab` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/deeplabv3_plus_mobilenet) | dv3\_argmax\_int32.tflite | | dv3-argmax-labels.json | dv3-argmax-labels.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp dv3_argmax_int32.tflite @:~/Downloads/qimsdk_samples/models/ scp dv3-argmax-labels.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} /usr/bin/qimsdk_ref_external_postprocess_segmentation_deeplab ``` The application runs inference on the video file, segmenting the frame with DeepLabv3, and uses the custom, application-defined postprocessing callback to decode the per-pixel segmentation results, which are then overlaid and displayed fullscreen. The video plays back fullscreen with a color-coded segmentation mask overlaid, where each color represents a detected semantic class. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_segmentation_deeplab`](https://github.com/qualcomm/qimsdk/tree/main/cpp/examples/reference-apps/qimsdk_ref_external_postprocess_segmentation_deeplab) * **Build instructions:** [Steps to build custom application](/advanced/yocto-build#steps-to-build-custom-application) # Coding Agent Source: https://imsdkdocs.qualcomm.com/app-builder/gstreamer-coding-agent Use the QIM SDK GStreamer App Builder skill to generate native C pipeline apps and gst-launch scripts with an AI coding agent. ## What is the GStreamer App Builder Skill? The **QIM SDK GStreamer App Builder** is an AI coding skill that generates GStreamer pipelines/applications using the `QIM SDK GStreamer API`. You describe the pipeline behavior and configuration in natural language, and the agent produces either a **native GStreamer C application** (`main.c` + `CMakeLists.txt`) or a **gst-launch shell script** — along with a `README.md` that documents the generated app. The skill can generate both **native C applications** and **gst-launch shell scripts**. The example on this page shows a native C application; use the prompt to request a `gst-launch` script instead when you want a quick, build-free pipeline. **Flow** QIM SDK coding agent workflow **What it generates** For a native C application request, the agent produces an artifact folder containing: * **`main.c`** — a complete, buildable native GStreamer C application, including element creation, property configuration, dynamic pad handling, linking, and bus/event-loop management * **`CMakeLists.txt`** — the build script that compiles `main.c` against GStreamer and links `gstappsutils` * **`README.md`** — a detailed document covering: * Purpose and pipeline behavior summary * Configuration placeholders (input, model, labels, output paths) * A step-by-step pipeline flow (text summary + Mermaid diagram) * Steps to build and run the app on device For a `gst-launch` request, the agent instead produces a shell script containing the full `gst-launch-1.0` pipeline plus a `README.md`. **Supported use cases:** | Category | Details | | - | - | | **AI pipelines** | Object detection, image classification, semantic segmentation, face detection, pose estimation, super resolution, depth estimation, audio classification, object tracking, gesture recognition | | **Inputs** | ISP camera, USB camera, file source (MP4/H.264), RTSP stream | | **Outputs** | HDMI display, encoded file (MP4), RTSP stream, Redis/MQTT metadata | | **ML backends** | TFLite (HTP/NPU), QNN, SNPE | | **Pipeline patterns** | Single-stream, multi-stream, daisy-chained models, ML-bin fused inference, batching, zero-copy cross-process pipelines, gst-launch scripts | ## Prerequisites ### For generating code Qualcomm dev kits are not needed for code generation. **Host Machine**: Any PC with internet access **Coding Agent**: Any AI coding agent installed (Claude Code, Cursor, Codex, etc.) which supports skills — the skills provided are agent-agnostic. This skill is designed to work well even with low-reasoning / smaller models. Eg: A Sonnet 4.5 model is sufficient. ### For deploying and running apps The target devices where QIM SDK is supported (see [QIM SDK Installation Guide](/installation)) ## How to Use ### Step 1: Get the skill Choose one installation method: Clone the skill repository and copy the **QIM SDK GStreamer App Builder** and **qimsdk-deploy** skills into your coding agent's skills directory: ```bash theme={null} git clone https://github.com/qualcomm/qimsdk-agentic-skills # The following commands are for Claude Code. # Set this to your coding agent's skills directory. cp -r qimsdk-agentic-skills/skills/qimsdk-gstreamer-app-builder ~/.claude/skills cp -r qimsdk-agentic-skills/skills/qimsdk-deploy ~/.claude/skills ``` The skills directory is agent-specific. Check your coding agent's documentation for its skills location. Restart or reload your coding agent after installation. Verify that `qimsdk-gstreamer-app-builder` and `qimsdk-deploy` are available with `/skills`. **Claude Code** — run inside a Claude Code session: ```text theme={null} /plugin marketplace add https://github.com/qualcomm/qimsdk-agentic-skills.git /plugin install qimsdk-agentic-skills@qimsdk-marketplace /reload-plugins ``` **Codex** — run in your terminal: ```bash theme={null} codex plugin marketplace add https://github.com/qualcomm/qimsdk-agentic-skills.git codex plugin add qimsdk-agentic-skills@qimsdk-marketplace ``` *** ### Step 2: Generate the application Describe your pipeline to the agent. It will automatically activate the skill and generate a complete `main.c`, `CMakeLists.txt`, and `README.md` (or a `gst-launch` script plus `README.md`). #### Sample Prompt **Prompt format — describe pipeline behavior and configuration:** ``` Create a QIM SDK Native C app for single-stream YOLOX object detection from an MP4 file, encoding the result to a file. ## Pipeline Behavior - Decode an MP4 file using Qualcomm hardware decoder - Run YOLOX object detection on full frames using TFLite external delegate on HTP/NPU - Merge metadata with the video stream before overlay - Overlay bounding boxes and labels - Encode the overlaid video and save to an output MP4 file ## Configuration - Input: ~/Downloads/qimsdk_samples/media/ai_demo_sample.mp4 - Model: ~/Downloads/qimsdk_samples/models/yolox_w8a8.tflite - Labels: ~/Downloads/qimsdk_samples/labels/yolov8.json - Backend: TFLite external delegate, HTP/NPU - Confidence: 51.0 - Output: ~/Downloads/qimsdk_samples/media/obj_detect_out.mp4 ``` To generate a `gst-launch` shell script instead of a C app, ask for it explicitly — e.g. "Create a `gst-launch` script for single-stream YOLOX object detection ...". More sample prompts can be found in the [Available Sample Prompts](#available-sample-prompts) section below. #### Generated `README.md` The README documents the generated app in full: * **Purpose** — a plain-English summary of what the pipeline does * **Files** — lists `main.c`, `CMakeLists.txt`, and `README.md` with descriptions * **Assumptions** — codec format, quantization requirements, camera defaults, output directory pre-conditions * **Configuration** — all user-supplied paths (input, model, labels, output) with instructions on where to change them * **Placeholders to Fill** — any values you still need to supply, or a note that none remain when the request was fully specified * **Pipeline Flow** — a `Text Summary` walkthrough of every element plus a `Mermaid Diagram` of the full pipeline * **Steps to Compile** — how to build the app for Yocto * **Steps to Run** — exact commands to run on device # QIM SDK GStreamer C App — Single-Stream YOLOX Object Detection (MP4 → MP4) ## Purpose Decode an MP4 file with the Qualcomm hardware decoder, run **YOLOX** object detection on full frames using the **TFLite external delegate on the HTP/NPU**, merge the detection metadata with the video stream, overlay bounding boxes and class labels, then hardware-encode the annotated video to an output MP4 file. Headless (no display). ## Files * `main.c` — GStreamer C sample app (uses `gst_parse_launch`) * `CMakeLists.txt` — build target `gst-qimsdk-yolox-obj-detect-encode` * `README.md` — this file ## Assumptions * Input is H.264-in-MP4, decoded with `v4l2h264dec` (`capture-io-mode=4`, `output-io-mode=4`). * YOLOX (`yolox_w8a8.tflite`) uses the `yolov8` postprocess module with `yolov8.json` labels, per the model catalog. * Confidence threshold: `{"confidence": 51.0}`. * No `bbox-stabilization` — this is a file source, not a live camera feed. * Encoder `v4l2h264enc` io-modes `4/4` — correct for decoder-produced (DMA) NV12 buffers from a file source. * All file paths are resolved at runtime via `g_getenv("HOME")` — C string literals do not expand `$HOME`. ## Configuration (resolved at runtime from `$HOME`) | Value | Path | | - | - | | Input | `$HOME/Downloads/qimsdk_samples/media/ai_demo_sample.mp4` | | Model | `$HOME/Downloads/qimsdk_samples/models/yolox_w8a8.tflite` | | Labels | `$HOME/Downloads/qimsdk_samples/labels/yolov8.json` | | Output | `$HOME/Downloads/qimsdk_samples/media/obj_detect_out.mp4` | ## Placeholders to Fill None — all paths and values are concrete. If you need different paths, edit the `g_strdup_printf` calls in `main.c`. ## Pipeline Flow ### Text Summary `filesrc` reads the MP4; `qtdemux` demuxes the H.264 stream, `h264parse` prepares it, and `v4l2h264dec` hardware-decodes to NV12. A `tee` (`t`) splits: * **AI branch**: `qtimlvconverter` → `qtimltflite` (YOLOX, HTP/NPU external delegate) → `qtimlpostprocess` (`module=yolov8`, confidence 51.0) → `text/x-raw` caps-filter → `qtimetamux.` * **Passthrough / video branch**: → `qtimetamux` (`metamux`). `qtimetamux` merges metadata onto the video frames, `qtivoverlay` draws bounding boxes and labels, and `v4l2h264enc` encodes back to H.264. `h264parse` + `mp4mux` + `filesink` write the output MP4. ### Mermaid Diagram ```mermaid theme={null} flowchart TD SRC[filesrc] --> DEMUX[qtdemux] DEMUX --> PARSE[h264parse] PARSE --> DEC[v4l2h264dec] DEC --> NV12[video/x-raw NV12] NV12 --> TEE[tee t] TEE -->|AI| PRE[qtimlvconverter] PRE --> INFER[qtimltflite YOLOX HTP] INFER --> POST[qtimlpostprocess yolov8] POST --> TEXT[text/x-raw] TEXT --> MUX[qtimetamux metamux] TEE -->|passthrough| MUX MUX --> OVL[qtivoverlay] OVL --> ENC[v4l2h264enc] ENC --> ENCQ[queue] ENCQ --> PARSEOUT[h264parse] PARSEOUT --> MP4[mp4mux] MP4 --> SINK[filesink] ``` ## Steps to Compile Yocto: [https://imsdkdocs.qualcomm.com/advanced/yocto-build#steps-to-build-custom-application](https://imsdkdocs.qualcomm.com/advanced/yocto-build#steps-to-build-custom-application) ## Steps to Run All referenced files — the input MP4, the `yolox_w8a8.tflite` model, and the `yolov8.json` labels — must already be present on the device at the paths listed in Configuration above. ```bash theme={null} # Copy the binary to device scp gst-qimsdk-yolox-obj-detect-encode @:~/ # SSH into device and run ssh @ chmod +x ~/gst-qimsdk-yolox-obj-detect-encode ./gst-qimsdk-yolox-obj-detect-encode ``` The app runs to end-of-file, finalizes the MP4 container, and exits. The output file is written to `$HOME/Downloads/qimsdk_samples/media/obj_detect_out.mp4` on the device. #### Generated `main.c` * The generated application uses `gst_parse_launch` to build the full pipeline from a single pipeline string — no manual element creation or pad linking. * All file paths are resolved at runtime via `g_getenv("HOME")` and formatted with `g_strdup_printf` — C string literals do not expand `$HOME`. * A GLib main loop and bus watch handle EOS and errors; a `SIGINT` handler (`g_unix_signal_add`) allows clean shutdown. * `qtivoverlay` draws bounding boxes, and the result is hardware-encoded and written to an MP4 file via `filesink`. ```c theme={null} #include #include #include /* * Single-stream YOLOX object detection from an MP4 file, encoding the * annotated video to an output MP4 file. * * Pipeline: * filesrc -> qtdemux -> h264parse -> v4l2h264dec -> video/x-raw,format=NV12 * -> tee name=t * t. -> qtimlvconverter -> qtimltflite (YOLOX, HTP/NPU external delegate) * -> qtimlpostprocess (module=yolov8, confidence=51.0) -> text/x-raw * -> qtimetamux. * t. -> qtimetamux name=metamux -> qtivoverlay * -> v4l2h264enc -> h264parse -> mp4mux -> filesink */ int main(int argc, char *argv[]) { GstAppContext appctx = { 0 }; guint interrupt_watch_id = 0; GstBus *bus = NULL; GError *error = NULL; /* Resolve HOME at runtime; C string literals do not expand $HOME. */ const gchar *home = g_getenv ("HOME"); if (!home || *home == '\0') { g_printerr ("ERROR: HOME is not set; cannot resolve file paths.\n"); return -1; } gchar *input_file = g_strdup_printf ( "%s/Downloads/qimsdk_samples/media/ai_demo_sample.mp4", home); gchar *model_path = g_strdup_printf ( "%s/Downloads/qimsdk_samples/models/yolox_w8a8.tflite", home); gchar *labels_path = g_strdup_printf ( "%s/Downloads/qimsdk_samples/labels/yolov8.json", home); gchar *output_file = g_strdup_printf ( "%s/Downloads/qimsdk_samples/media/obj_detect_out.mp4", home); gst_init (&argc, &argv); /* * Build the full pipeline string from source, AI overlay, and encode sink. * * Notes: * - video/x-raw,format=NV12 caps-filter after hardware decode normalizes * the decoded buffers before branching. * - No bbox-stabilization: this is a file source (not a live camera feed). * - text/x-raw caps-filter routes the postprocess metadata to qtimetamux. * - Encoder io-modes 4/4: file-decoded NV12 (DMA, driver-managed buffers). */ gchar *pipeline_str = g_strdup_printf ( /* Source: hardware-decode the input MP4 */ "filesrc location=\"%s\" ! qtdemux ! h264parse ! " "v4l2h264dec capture-io-mode=4 output-io-mode=4 ! " "video/x-raw,format=NV12 ! " /* Split into passthrough (video) branch and AI inference branch */ "tee name=t " /* AI branch: preprocess -> inference -> postprocess -> metadata */ "t. ! qtimlvconverter name=preprocess ! queue ! " "qtimltflite name=inference delegate=external " "external-delegate-path=libQnnTFLiteDelegate.so " "external-delegate-options=\"QNNExternalDelegate,backend_type=htp,log_level=(string)1;\" " "model=\"%s\" ! queue ! " "qtimlpostprocess name=postprocess module=yolov8 " "labels=\"%s\" " "settings=\"{\\\"confidence\\\": 51.0}\" ! text/x-raw ! metamux. " /* Passthrough branch: merge metadata, overlay, encode, write */ "t. ! qtimetamux name=metamux ! " "qtivoverlay ! " "v4l2h264enc capture-io-mode=4 output-io-mode=4 ! queue ! " "h264parse ! mp4mux ! " "filesink location=\"%s\"", input_file, model_path, labels_path, output_file ); g_print ("Pipeline:\n%s\n\n", pipeline_str); appctx.pipeline = gst_parse_launch (pipeline_str, &error); g_free (pipeline_str); if (!appctx.pipeline) { g_printerr ("ERROR: Pipeline creation failed: %s\n", error ? error->message : "unknown error"); if (error) g_error_free (error); goto cleanup; } appctx.mloop = g_main_loop_new (NULL, FALSE); bus = gst_pipeline_get_bus (GST_PIPELINE (appctx.pipeline)); if (!bus) { g_printerr ("ERROR: Failed to get pipeline bus.\n"); goto cleanup; } gst_bus_add_signal_watch (bus); g_signal_connect (bus, "message::eos", G_CALLBACK (eos_cb), appctx.mloop); g_signal_connect (bus, "message::error", G_CALLBACK (error_cb), appctx.mloop); gst_object_unref (bus); interrupt_watch_id = g_unix_signal_add (SIGINT, handle_interrupt_signal, &appctx); g_print ("Starting pipeline...\n"); gst_element_set_state (appctx.pipeline, GST_STATE_PAUSED); g_main_loop_run (appctx.mloop); g_print ("Pipeline finished.\n"); g_source_remove (interrupt_watch_id); cleanup: if (appctx.pipeline) { gst_element_set_state (appctx.pipeline, GST_STATE_NULL); gst_object_unref (appctx.pipeline); } if (appctx.mloop) g_main_loop_unref (appctx.mloop); g_free (input_file); g_free (model_path); g_free (labels_path); g_free (output_file); gst_deinit (); return 0; } ``` #### Generated `CMakeLists.txt` * Uses `pkg-config` to locate the required `gstreamer-1.0` development package. * Builds a single executable (`gst-qimsdk-yolox-obj-detect-encode`) from `main.c` and links GStreamer libraries plus `gstappsutils`. * Installs the binary to `${GST_PLUGINS_QTI_OSS_INSTALL_BINDIR}` with standard executable permissions. ```cmake theme={null} cmake_minimum_required(VERSION 3.16) project(GST-QIMSDK-YOLOX-OBJ-DETECT-ENCODE LANGUAGES C CXX) set(CMAKE_INCLUDE_CURRENT_DIR ON) find_package(PkgConfig) pkg_check_modules(GST REQUIRED gstreamer-1.0>=${GST_VERSION_REQUIRED}) set(GST_EXAMPLE_BIN gst-qimsdk-yolox-obj-detect-encode) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wno-unused-parameter") add_executable(${GST_EXAMPLE_BIN} main.c ) target_include_directories(${GST_EXAMPLE_BIN} PRIVATE ${GST_INCLUDE_DIRS} ) target_link_libraries(${GST_EXAMPLE_BIN} PRIVATE ${GST_LIBRARIES} gstappsutils ) install( TARGETS ${GST_EXAMPLE_BIN} RUNTIME DESTINATION ${GST_PLUGINS_QTI_OSS_INSTALL_BINDIR} PERMISSIONS OWNER_EXECUTE OWNER_WRITE OWNER_READ GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ ) ``` *** ### Step 3: Running the applications | File | Download | Save as | | - | - | - | | YOLOX W8A8 model | [Qualcomm AI Hub — YOLOX](https://aihub.qualcomm.com/iot/models/yolox) | `yolox_w8a8.tflite` | | Detection labels | yolov8.json | `yolov8.json` | | Sample video | Input video | `ai_demo_sample.mp4` | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` The input video, model, and labels must be present on the device before running — regardless of how you build and run the app. Copy them to the paths `main.c` expects: ```bash theme={null} # Replace and with your device credentials. ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media ~/Downloads/qimsdk_samples/models ~/Downloads/qimsdk_samples/labels" # Copy the assets to device (paths must match main.c) scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ scp yolox_w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` Unlike a `gst-launch` script, a native C app must be compiled before it can run. With the assets in place, you have two options to build and run the agent-generated app: **Option A — Use the QIM SDK Deploy skill:** The `qimsdk-deploy` skill builds the app on your host against the SDK, then pushes the compiled binary to the device and runs it over SSH. It handles only the app build and binary transfer — the assets from the previous step must already be on the device. **Option B — Follow the generated README:** Follow the steps in the [generated `README.md`](#generated-readme-md) to compile the app, then push the binary to the device and run it: 1. **Compile the app** by following the build steps for your platform: * **Yocto:** [Yocto Build](/advanced/yocto-build) 2. **Push the compiled binary to the device and run it:** ```bash theme={null} # Copy the built binary to device scp gst-qimsdk-yolox-obj-detect-encode @:~/ # SSH into device and run ssh @ chmod +x ~/gst-qimsdk-yolox-obj-detect-encode ./gst-qimsdk-yolox-obj-detect-encode ``` On completion (EOS), the annotated video is written to `~/Downloads/qimsdk_samples/media/obj_detect_out.mp4`. *** ## Available Sample Prompts | # | Prompt | | - | - | | 1 | [Single-stream YOLOX object detection from MP4, encoded to file](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/AI_01_single_stream_object_detection.md) | | 2 | [AI wall — four parallel AI inference streams composed into a 2×2 grid display](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/AI_02_ai_wall.md) | | 3 | [Two-stage detection + classification daisy-chain pipeline](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/AI_03_detection_classification_daisy_chain.md) | | 4 | [Event-triggered recording — record to file only when a person is detected](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/AI_04_event_triggered_recording.md) | | 5 | [Four-stage gesture recognition daisy-chain pipeline](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/AI_05_gesture_recognition.md) | | 6 | [Audio classification — classify audio events from video and overlay results](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/AI_06_audio_classification.md) | | 7 | [Smart codec detection — YOLOv8 detection driving adaptive bitrate encoding](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/AI_07_smartcodec_detection.md) | | 8 | [Event encoder conditional recording from MP4 file, result on Wayland display](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/AI_08_event_encoder_conditional_recording.md) | | 9 | [2-stream face detection from MP4 file, result on Wayland display](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/AI_09_multistream_face_detection.md) | | 10 | [AV playback — H.264 video + MP3 audio from MP4, video on display, audio on speaker](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/c-app/MM_01_av_playback_h264_mp3.md) | | # | Prompt | | - | - | | 1 | [Single-stream YOLOX object detection from MP4, encoded to file](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/AI_01_single_stream_object_detection.md) | | 2 | [AI wall — four parallel AI inference streams composed into a 2×2 grid display](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/AI_02_ai_wall.md) | | 3 | [Two-stage detection + classification daisy-chain pipeline](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/AI_03_detection_classification_daisy_chain.md) | | 4 | [Four-stage gesture recognition daisy-chain pipeline](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/AI_04_gesture_recognition.md) | | 5 | [Audio classification — classify audio events from video and overlay results](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/AI_05_audio_classification.md) | | 6 | [Single-stream YOLOv8 object detection using SNPE DSP delegate, MP4 to file](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/AI_06_snpe_dsp_object_detection.md) | | 7 | [RTSP input YOLOv8 detection — video + metadata streamed over RTSP](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/AI_07_rtsp_input_yolov8_meta_over_rtsp.md) | | 8 | [2-stream face detection pipeline](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/AI_08_multistream_face_detection.md) | | 9 | [AV record — ISP camera 1080p video + microphone audio muxed to MP4](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/MM_01_av_record_h264_mp3.md) | | 10 | [AV playback — H.264 video + MP3 audio from MP4, video on display, audio on speaker](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-gstreamer-app-builder/gst-launch/MM_02_av_playback_h264_mp3.md) | # SDK Architecture and API Reference Source: https://imsdkdocs.qualcomm.com/app-builder/python-architecture-api-reference Design and API details of the QIM SDK Pipeline SDK (C++ and Python) ## Pipeline SDK Design **Overview** This SDK delivers a streamlined, modern interfaces, engineered to facilitate the rapid development, execution, and management of sophisticated multimedia pipelines. By implementing a high-level abstraction layer, the SDK encapsulates complex underlying GStreamer based mechanisms — including element negotiation, signal management, event loops, and state transitions — allowing engineers to prioritize core application logic over boilerplate framework intricacies. **Key Capabilities** The wrapper empowers developers to architect advanced multimedia and AI-enhanced workflows with precision and efficiency. It supports the seamless integration of diverse components, such as video capture sources, signal processing nodes, machine learning inference engines, graphical overlays, and rendering outputs. Uniquely designed to decouple implementation from the underlying framework, the SDK eliminates the need for deep GStreamer expertise or compile-time dependencies on GStreamer headers, ensuring a clean, robust, and accessible development experience. Introduction ## APIs **Public APIs:** * `Pipeline` — build & control pipelines fluently. * `Element` — generic element wrapper with variadic property setting, and state control. It can be instantiated at create time as well as at runtime to update the pipeline. Some elements have special functions which can be controlled by dedicated element types. Those derived types can be used as a plain `Element` if the special functions are not needed. * `AppSrc` — app-facing source for zero-copy buffer pull. Feeds application-produced buffers into the pipeline through a callback or a direct push API, and can signal EOS. * `AppSink` — app-facing sink for zero-copy buffer push. Delivers pipeline output back to application code with zero copy. * `CamSrc` — built-in camera element that exposes an API for capturing images at runtime. * `MLVConverter` — ML video converter (`qtimlvconverter`). Exposes an API for custom pre-processing implementation in application code through a callback. * `MLPostprocess` — ML postprocess element (`qtimlpostprocess`). Exposes an API for custom post-processing implementation in application code through a callback. The callback's output type annotation selects the ML task (classification, object detection, pose estimation, depth estimation, segmentation, raw tensors). * `MLVideoONNXBin` — ONNX inference bin (`qtimlvideoonnxbin`). Exposes an API for custom pre-processing and post-processing implementation through callbacks. * `MLVideoQNNBin` — QNN inference bin (`qtimlvideoqnnbin`). Exposes an API for custom pre-processing and post-processing implementation through callbacks. * `MLVideoSNPEBin` — SNPE inference bin (`qtimlvideosnpebin`). Exposes an API for custom pre-processing and post-processing implementation through callbacks. * `MLVideoTFLiteBin` — TFLite inference bin (`qtimlvideotflitebin`). Exposes an API for custom pre-processing and post-processing implementation through callbacks. * `Buffer` — opaque buffer that bridges native GStreamer memory with timestamp helpers and zero-copy paths. * `StreamFilter` and derived concrete stream helpers: `VideoFilter` (all RAW formats - YUV, RGB, bayer), `H264Filter`, `TensorFilter`, `TextFilter`, `AudioFilter`, `ImageFilter`. These filters are used to set characteristics of the streams. * Logging controls — `SetImsdkLogLevel()` and `SetImsdkGstLogMode()` select the SDK's log verbosity and whether GStreamer's output is simplified or passed through raw. See [Logging and Diagnostics](/app-builder/python-logging). **Reference flows** ### Pipeline **Purpose** A convenient wrapper around a `Gst.Pipeline` that: * Builds pipelines using element factory names or pre-constructed wrapper elements. * Manages pipeline lifecycle and execution state. * Performs delayed linking for dynamic/request-pad elements automatically via `pad-added`. **Key Responsibilities** * Create and own the underlying `Gst.Pipeline` instance. * Add elements, assign properties, and insert caps filters. * Link elements (immediately when possible, deferred otherwise). * Manage the full pipeline lifecycle (`prepare`, `start`, `wait`, `stop`). * Install bus watches to handle `ERROR`, `EOS`, and state transitions. * Integrate with runtime shutdown notifications. **A minimal pipeline** Add the elements in the order data flows through them, then run: ```python theme={null} from qimsdk import Pipeline, VideoFilter pipeline = ( Pipeline("my-pipeline") .add("filesrc", "src", "location", "/home/user/media/video.mp4") .add("qtdemux", "demux") .add("h264parse", "parse") .add("v4l2h264dec", "decoder", "output-io-mode", 4, "capture-io-mode", 4) .add_stream_filter("vf", VideoFilter().format("NV12")) .add("waylandsink", "display", "fullscreen", True) ) pipeline.execute() ``` `execute()` starts the pipeline, blocks until EOS/error/termination, and tears it down. **Adjacent elements are linked in add order.** In the example above `src` → `demux` → `parse` → `decoder` → `vf` → `display` are linked simply because that is the order of the `add()` calls, so a linear graph needs no explicit linking. Use [`link()`](#branching-with-explicit-links) when the graph is not linear — a `tee` splitting into a display branch and an inference branch, or a `qtimetamux` rejoining them. `qtdemux` is an example of the delayed linking described above: its src pads appear only once the container has been parsed, so the `demux` → `parse` link is completed automatically when the pad shows up. No `pad-added` handling is needed in application code. **Building the pipeline** | Method | Description | | - | - | | `Pipeline(name: str)` | Creates an empty pipeline. `name` is the `Gst.Pipeline` name, used in log messages. | | `classmethod Pipeline.from_yaml(name: str, config: str)` | Builds a complete pipeline from YAML configuration content. `config` is the YAML text itself, not a path — read the file first. See [Describing a pipeline in YAML](#describing-a-pipeline-in-yaml). | | `Pipeline.add(factory_or_element, name=None, *args, **props) -> Pipeline` | Creates an element from a factory name (or adds an externally constructed `Element`) under a unique instance name, and applies any trailing key/value properties immediately. Required for the specialized wrappers (`AppSrc`, `AppSink`, `CamSrc`, `MLVConverter`, `MLPostprocess`, the ML bins) so callbacks can be attached before the element joins the pipeline. | | `Pipeline.add_stream_filter(name, caps=None) -> Pipeline` | Inserts a caps filter between the previous and next element. Accepts either `(name, caps)` or a single `StreamFilter` argument, in which case the element name is derived from the filter's class. See [StreamFilter](#streamfilter). | | `Pipeline.link(*names) -> Pipeline` | Links the named elements in the given order. Needed only for non-linear graphs. | `add()`'s optional key/value pairs are the element's properties, validated as soon as the element is added — an unknown property raises `ValueError` naming the property and the element type, instead of being silently ignored: ```python theme={null} # "fulscreen" is not a waylandsink property -> raises ValueError pipeline.add("waylandsink", "display", "fulscreen", True) ``` **Branching with explicit links** Add order cannot express a branch, so name the chains explicitly. Element instance names are the identifiers. ```python theme={null} pipeline = ( Pipeline("branching-pipeline") .add("filesrc", "src", "location", path) .add("qtdemux", "demux") .add("h264parse", "parse") .add("v4l2h264dec", "decoder", "output-io-mode", 4, "capture-io-mode", 4) .add_stream_filter("vf", VideoFilter().format("NV12")) .add("tee", "split") .add("queue", "q1") .add("qtimlvconverter", "preprocessing") .add("qtimltflite", "inferencing", "model", model_path) .add("qtimlpostprocess", "postprocessing", "module", "yolov8") .add_stream_filter("mlf", TextFilter()) .add("qtimetamux", "mlmuxer") .add("qtivoverlay", "overlay") .add("waylandsink", "display", "fullscreen", True) # trunk, up to the tee .link("src", "demux", "parse", "decoder", "vf", "split") # branch 1: raw video to the metadata muxer .link("split", "mlmuxer") # branch 2: inference path, rejoining at the muxer .link("split", "q1", "preprocessing", "inferencing", "postprocessing", "mlf", "mlmuxer", "overlay", "display") ) pipeline.execute() ``` Elements whose src pads are `SOMETIMES` or `REQUEST` — `qtdemux`, `tsdemux`, `tee`, `qtivsplit` — cannot be linked at construction time, because the pads do not exist yet. The pipeline detects this from the pad templates, defers the link, and completes it from a `pad-added` handler it installs itself. Request pads on the sink side (for example `qtimetamux`, `qtivcomposer`) are requested and released automatically. No `pad-added` callback is needed in application code. A link between two elements that both have only static pads and no common caps fails immediately with `RuntimeError` naming both elements. A deferred link that never resolves surfaces at run time instead; on error the pipeline logs which pads stayed unlinked. **Running the pipeline** A pipeline is always in one of three states, and the lifecycle methods are the transitions between them: Introduction | State | Meaning | | - | - | | `NULL` | No resources allocated. Initial state, and the state after `stop()`. Devices are closed, buffer pools released. | | `PAUSED` | Elements are allocated and caps are negotiated between linked pads, but the clock is not running and no data flows. Sinks have pre-rolled. This is the state in which elements can be added, removed, or relinked. | | `PLAYING` | The clock is running and buffers flow through the graph. | `execute()` performs the whole `NULL → PLAYING → NULL` cycle, so most applications never call the individual transitions. `prepare()`, `activate()`, and `deactivate()` — the transitions through `PAUSED` — are covered in [Advanced state control](#advanced-state-control). | Method | Description | | - | - | | `Pipeline.execute() -> None` | `start()`, `wait()`, `stop()` in sequence. The normal entry point. | | `Pipeline.start() -> Pipeline` | Moves the pipeline to `PLAYING`. Non-blocking: the state change is asynchronous and this returns without waiting for it. Raises `RuntimeError` if the transition fails. | | `Pipeline.wait() -> Pipeline` | Blocks until the pipeline is done: `EOS` on the bus, `ERROR` on the bus, `stop()` from another thread, or `SIGINT`/`SIGTERM`. | | `Pipeline.stop() -> Pipeline` | Moves the pipeline to `NULL` and releases resources. Blocks until teardown completes. | | `Pipeline.eos(enabled: bool) -> Pipeline` | When enabled, `stop()` first sends an end-of-stream event through the pipeline and waits for it to come back from the sinks, so that data already inside the pipeline is processed before teardown. Off by default. See [Draining the pipeline on shutdown](#draining-the-pipeline-on-shutdown). | `start()` is non-blocking, so it must be paired with `wait()` (or your own blocking logic). A script that calls `start()` and exits destroys the pipeline before buffers flow. To run the pipeline while the application does other work, split `execute()` apart: ```python theme={null} pipeline.start() # returns immediately # ... application work, runtime property changes, snapshots ... pipeline.wait() # block until EOS / error / Ctrl+C pipeline.stop() ``` **Draining the pipeline on shutdown** By default `stop()` tears the pipeline down as fast as it can. Every element goes to `NULL` immediately and whatever buffers are still in flight — queued between elements, held inside an encoder, not yet written by a sink — are discarded. This is usually what you want: the pipeline stops without delay. Some pipelines must not lose those buffers, and for them `eos(True)` changes the shutdown sequence. Instead of stopping straight away, `stop()` sends an end-of-stream event into the pipeline. The event travels downstream element by element; each element finishes processing the data it has already accepted, passes the event on, and the sinks report it back on the bus. Only once that end-of-stream notification comes back does `stop()` tear the pipeline down. Nothing that had already entered the pipeline is dropped. The trade-off is shutdown latency: `stop()` now blocks until the last buffer has made it through the whole graph, which takes as long as the remaining data needs. Use `eos(True)` when the tail of the stream is part of the result: * **Muxed recordings.** Containers such as MP4 and MKV are written by a muxer (`mp4mux`, `matroskamux`) that writes its index and header metadata — the `moov` atom, in MP4's case — only on end-of-stream. Cut it off early and the file is left incomplete, often unplayable rather than merely short. * **Encoders with buffered frames.** Encoders keep several frames in flight for B-frames, lookahead, and similar optimizations. End-of-stream is what makes the encoder flush those frames downstream; without it the tail of the recording is simply missing. * **Any sink where losing the final buffered data is unacceptable** — file sinks, muxed outputs, and applications that expect to receive every frame through `AppSink`. Not every sink needs draining, though. A live network stream can usually just be cut off, and a receiver will not care that the last few buffers never arrived. The question is not whether a sink writes somewhere, but whether anything downstream depends on the data still sitting in the pipeline. Leave `eos()` off (the default) when the newest data is all that matters and stopping promptly is preferable — live preview to a display, a camera feed on screen, or writing self-contained files where each buffer is already complete on its own (JPEG frames through `multifilesink`, for example, which needs no finalization). ```python theme={null} pipeline = ( Pipeline("cam-encoder-pipeline") .add("qtiqmmfsrc", "source") .add_stream_filter("vf", VideoFilter().format("NV12") .resolution(1920, 1080) .framerate(30)) .add("v4l2h264enc", "encoder", "output-io-mode", "dmabuf-import", "capture-io-mode", "dmabuf") .add("h264parse", "parser") .add("mp4mux", "muxer") .add("filesink", "sink", "location", f"{HOME_PATH}/media/output.mp4") .eos(True) # drain, so mp4mux can finalize the container ) pipeline.execute() ``` If the pipeline has already reached end-of-stream on its own — a file source that ran out of data, for instance — `stop()` detects it and tears down immediately instead of sending a second event and waiting again. Enabling `eos(True)` therefore costs nothing for pipelines that end naturally; it only matters when you stop a pipeline that is still running. **Advanced state control** For reconfiguring a loaded pipeline — adding, removing, or relinking elements without returning to `NULL`. | Method | Description | | - | - | | `Pipeline.prepare() -> Pipeline` | `NULL` → `PAUSED`. Allocates resources and negotiates caps without running the clock. Blocking. | | `Pipeline.activate() -> Pipeline` | `PAUSED` → `PLAYING`. | | `Pipeline.deactivate() -> Pipeline` | `PLAYING` → `PAUSED`. Resources stay allocated. | **Accessing elements at run time** | Method | Description | | - | - | | `Pipeline.get(name: str, as_type=None) -> Any` | Returns the named element. With `as_type` omitted, returns the generic `Element` wrapper. With `as_type` set to a specialized class (for example `CamSrc`), returns that class's `cast()` of the element instead. Raises `KeyError` if the name is not in the pipeline. | Unlike the C++ SDK's `get()`, Python has no compile-time generics, so the type is passed as an ordinary argument. To reach a specialized element's own API (for example `CamSrc.image_capture()`), pass the class as `as_type`: ```python theme={null} from qimsdk import CamSrc # qtiqmmfsrc was added as "source"; CamSrc exposes its capture API. cam = pipeline.get("source", CamSrc) cam.image_capture() ``` **Runtime property and per-pad configuration** — `get()` reaches any element by name while the pipeline is loaded or playing: ```python theme={null} # Change a property at run time. pipeline.get("display").set("fullscreen", False) # Per-pad properties on a request pad of qtivcomposer. pipeline.get("composer").input(1).set("alpha", 0.5) ``` See the sample applications for complete image-capture pipelines. `get()` returns a new `Element` wrapper on every call, each referencing the same underlying `Gst.Element`. There is no cached identity to keep in sync — call it again whenever you need the element. **Describing a pipeline in YAML** The graph — elements, properties, stream filters, and links — can be described in YAML instead of in code. This separates *what the pipeline is* from *what the application does with it*: the same script can run a different topology, a different model, or a different source by pointing it at another configuration file. That makes it useful where the pipeline is not fixed up front: * **Deploying one script across variants** — the same application against a built-in camera, a USB camera, or an RTSP stream, switching source and format per device. * **Tuning without code changes** — resolution, framerate, model path, or delegate changed on the target, without touching the script. * **Keeping topology under review** — the pipeline shape lives in a file that can be diffed and versioned on its own. Loading from YAML does not give up application logic: the elements are ordinary pipeline members afterwards, so `get()` reaches them as usual. ```yaml theme={null} pipeline: elements: - type: filesrc name: src location: ~/media/video.mp4 - type: qtdemux name: demux - type: h264parse name: parse - type: v4l2h264dec name: decoder output-io-mode: 4 capture-io-mode: 4 - type: filter name: videofilter video: format: NV12 - type: waylandsink name: display fullscreen: true ``` Everything lives under a single root key, `pipeline:`, which takes three independent child keys: ```yaml theme={null} pipeline: eos: true # optional — EOS-on-shutdown, same as .eos(True) in code elements: # required — the elements, in add order - type: ... links: # optional — explicit links, for non-linear graphs - [ ... ] ``` | Key | Required | Description | | - | - | - | | `eos` | No | Boolean. Equivalent to calling [`eos()`](#running-the-pipeline). Defaults to false when omitted. | | `elements` | Yes | Sequence of elements. Add order is link order, exactly as with `add()`. | | `links` | No | Sequence of chains, each a list of element names. Equivalent to one `link()` call per entry. Omit it for linear pipelines. | Within `elements:`, each entry needs `type` (the factory name) and `name` (the instance name); every remaining key is applied as a property of that element. An entry with `type: filter` inserts a stream filter instead, described by a `video:`, `image:`, `h264:`, `tensor:`, `text:`, `audio:`, or raw `caps:` block. `links:` mirrors [`link()`](#branching-with-explicit-links) — one sequence per chain, so the branching pipeline shown earlier becomes: ```yaml theme={null} links: - [split, mlmuxer] - [source, videostream, split, q1, preprocessing, inferencing, postprocessing, mlf, mlmuxer, overlay, display] ``` `from_yaml()` takes the configuration content itself, so read the file first: ```python theme={null} from qimsdk import Pipeline with open(config_path, "r", encoding="utf-8") as f: config = f.read() pipeline = Pipeline.from_yaml("demo-pipeline", config) pipeline.execute() ``` The names in the configuration are the handles the application uses afterwards. Given a config that declares a `qtimlpostprocess` element named `postprocessing`, the application can attach its post-processing callback without the topology being hard-coded: ```python theme={null} from qimsdk import Pipeline, MLPostprocess, ObjectDetections with open(config_path, "r", encoding="utf-8") as f: config = f.read() pipeline = Pipeline.from_yaml("demo-pipeline", config) def detection_callback(mlframe, mlparams, detections: ObjectDetections): ... return True postprocessing = pipeline.get("postprocessing", MLPostprocess) postprocessing.set_handler(detection_callback) pipeline.execute() ``` ### Element **Purpose** Generic wrapper around a `Gst.Element` with fluent, variadic property setting. Can be instantiated at create time or used at runtime to update a live pipeline. Serves as the base class for all specialized element types. **Key Responsibilities** * Create GStreamer elements from a factory name. * Provide fluent, variadic property setting for all common types and `StreamFilter` (caps). * Implement pad linking/unlinking with error checking. * Support state publication to pipeline (`sync`, `deactivate`, `stop`). * Expose per-pad configuration through `Port`. * Serve as the base class for specialized wrappers. ```python theme={null} from qimsdk import Element display = Element("waylandsink", "display") display.set("fullscreen", True) display.set("sync", False) ``` | Method | Description | | - | - | | `Element(factory: str, name: str \| None = None)` | Creates the element from a GStreamer factory name. | | `Element.set(*args, **props) -> Element` | Sets one or more properties. Accepts key/value pairs positionally, keyword arguments, or both, and returns `self` for chaining. | | `Element.link(downstream, src_pad="src", sink_pad="sink") -> Element` | Links this element's src pad to `downstream`'s sink pad. | | `Element.unlink(downstream=None, src_pad="src", sink_pad="sink") -> Element` | Unlinks this element's src pad from `downstream`'s sink pad. | | `Element.sync() -> Element` | Synchronizes the element's state with its parent pipeline's state. For runtime link/unlink. | | `Element.deactivate() -> Element` | Sets the element's state to `PAUSED`. | | `Element.stop() -> Element` | Moves the element to `NULL` state. For runtime link/unlink. | | `Element.input(name_or_id=0, id=None) -> Port` | Returns a reference to an input port, for per-pad configuration. | | `Element.output(name_or_id=0, id=None) -> Port` | Returns a reference to an output port, for per-pad configuration. | **Per-pad configuration with Port** Some elements are configured per pad rather than per element — a compositor's inputs each have their own position, size, and alpha. `input()` and `output()` return a `Port` for one pad, which carries the same fluent, validated `set()`: ```python theme={null} # Second input of qtivcomposer: blend at 50% alpha. pipeline.get("composer").input(1).set("alpha", 0.5) ``` | Method | Description | | - | - | | `Port.set(*args, **props) -> Port` | Sets one or more pad properties, same as `Element.set()`. | ### AppSrc **Purpose** `AppSrc` wraps the `appsrc` element, the entry point for feeding application-produced buffers into a pipeline. It manages the `need-data`/`enough-data` signal plumbing so the application only has to supply buffers, either from a callback or by pushing them directly. **Key Responsibilities** * Validate that the wrapped element is indeed an `appsrc`. * Register and dispatch the underlying `need-data` and `enough-data` signals. * Let the application supply buffers on demand through `set_buffer_producer()`, or push them directly with `push_buffer()`. * Signal end-of-stream on this element's output. * Manage lifetime of the connected signal handlers. ```python theme={null} from qimsdk import AppSrc, VideoFilter appsrc = AppSrc("src") appsrc.set("is-live", True, "block", True) appsrc.set("format", AppSrc.Format.TIME, "do-timestamp", True) appsrc.set("caps", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) ``` | Method | Description | | - | - | | `AppSrc(name: str \| None = None)` | Instantiates an application source element with an optional instance name. | | `classmethod AppSrc.cast(element) -> AppSrc` | Wraps an existing `Element` (for example one returned by `Pipeline.get()`) as an `AppSrc`. | | `AppSrc.set(*args, **props) -> AppSrc` | Sets one or more properties, same as `Element.set()`. | | `AppSrc.set_caps(caps) -> AppSrc` | Sets the `caps` property from a `StreamFilter`, a raw caps string, or a `Gst.Caps` object. | | `AppSrc.set_buffer_producer(producer) -> AppSrc` | Attaches a callback executed when more data is needed. Data control is driven by the pipeline. | | `AppSrc.set_enough_handler(enough) -> AppSrc` | Attaches a callback executed when the data source should be stopped. Data control is driven by the pipeline. | | `AppSrc.push_buffer(buffer: Buffer) -> bool` | Pushes a buffer into the AppSrc/pipeline stream. Data control is driven by the application. | | `AppSrc.end_of_stream() -> None` | Sends an end-of-stream event to the AppSrc/pipeline stream. | `AppSrc.Format` mirrors appsrc's own `format` property — an `IntEnum` with `DEFAULT`, `BYTES`, `TIME`, `BUFFERS`, `PERCENT`. ### AppSink **Purpose** `AppSink` wraps the `appsink` element, the exit point that delivers pipeline output back to application code. It pulls samples through appsink's action signals and hands them to the application as `Buffer`. **Key Responsibilities** * Validate that the wrapped element is indeed an `appsink`. * Pull samples via appsink's action signals and convert them into `Buffer`. * Deliver each buffer to the application through `set_buffer_consumer()`. * Deliver the first (preroll) sample separately through `set_preroll_handler()`. * Notify the application of end-of-stream through `set_eos_handler()`. * Manage lifetime of the connected signal handlers. ```python theme={null} from qimsdk import AppSink appsink = AppSink("sink") appsink.set("emit-signals", True, "max-buffers", 5, "drop", True) appsink.set_buffer_consumer(lambda buffer: consume(buffer)) ``` | Method | Description | | - | - | | `AppSink(name: str \| None = None)` | Instantiates an application sink element with an optional instance name. | | `classmethod AppSink.cast(element) -> AppSink` | Wraps an existing `Element` (for example one returned by `Pipeline.get()`) as an `AppSink`. | | `AppSink.set(*args, **props) -> AppSink` | Sets one or more properties, same as `Element.set()`. | | `AppSink.set_buffer_consumer(consumer) -> AppSink` | Attaches a listener for buffers pulled from the pipeline. | | `AppSink.set_preroll_handler(preroll) -> AppSink` | Attaches a listener for the first (preroll) sample only. | | `AppSink.set_eos_handler(eos) -> AppSink` | Attaches a listener for end-of-stream. | ### CamSrc **Purpose** `CamSrc` wraps the `qtiqmmfsrc` camera source element and exposes its still/burst image-capture action signals as ordinary method calls, so an application can trigger a snapshot without touching GStreamer signals directly. **Key Responsibilities** * Validate that the wrapped element is indeed a `qtiqmmfsrc`. * Trigger still or burst image capture through `image_capture()`. * Cancel any pending capture request through `cancel_capture()`. | Method | Description | | - | - | | `CamSrc(name: str \| None = None)` | Instantiates a camera source element with an optional instance name. | | `classmethod CamSrc.cast(element) -> CamSrc` | Wraps an existing `Element` (for example one returned by `Pipeline.get()`) as a `CamSrc`. | | `CamSrc.image_capture(*args, **kwargs) -> bool` | Captures still or burst images. Accepts `count`, `mode`, and `metadata` positionally or as keyword arguments. | | `CamSrc.cancel_capture() -> bool` | Cancels any capture requests still pending. | Reach a `qtiqmmfsrc` already in the pipeline through `Pipeline.get()`'s `as_type` argument to use its capture API: ```python theme={null} from qimsdk import CamSrc cam = pipeline.get("source", CamSrc) cam.image_capture() ``` ### MLVConverter **Purpose** `MLVConverter` wraps the `qtimlvconverter` element, which converts raw video frames into the input tensor format expected by a model. Beyond the properties available on the generic `Element`, it lets the application replace the built-in conversion with its own implementation through a callback. **Key Responsibilities** * Validate that the wrapped element is a `qtimlvconverter`. * Convert incoming video frames into model input tensors. * Optionally delegate the conversion to an application-supplied pre-process callback. ```python theme={null} from qimsdk import MLVConverter preprocessing = MLVConverter("preprocessing") # engine=none disables internal preprocessing path in qtimlvconverter. preprocessing.set("engine", "none") preprocessing.set_handler(preprocess_callback) ``` | Method | Description | | - | - | | `MLVConverter(name: str \| None = None)` | Instantiates a `qtimlvconverter` element with an optional instance name. | | `classmethod MLVConverter.cast(element) -> MLVConverter` | Wraps an existing `Element` (for example one returned by `Pipeline.get()`) as an `MLVConverter`. | | `MLVConverter.set(*args, **props) -> MLVConverter` | Sets one or more properties, same as `Element.set()`. | | `MLVConverter.set_handler(handler) -> MLVConverter` | Registers the external pre-process callback. Requires `engine=none` to take effect. | ### MLPostprocess **Purpose** `MLPostprocess` wraps the `qtimlpostprocess` element, which decodes model output tensors into ML metadata attached to the frame. It lets the application replace the built-in decoding module with its own implementation through a callback. **Key Responsibilities** * Validate that the wrapped element is a `qtimlpostprocess`. * Decode output tensors into ML metadata (classifications, detections, poses, depth maps, segmentations, or raw tensors). * Optionally delegate the decoding to an application-supplied post-process callback. * Select the callback's ML task automatically from its output-parameter type annotation. **Post-process callback** `set_handler()` takes a single callback and, unlike a per-task setter, infers which ML task it handles from the type annotation on the callback's output parameter. The callback takes either 3 parameters (`mlframe, mlparams, results`) or 4 (`mlpostprocess, mlframe, mlparams, results`); the output parameter must be annotated with one of the marker types below. | Output annotation | ML task | | - | - | | `ImageClassifications` | Image classification | | `AudioClassifications` | Audio classification | | `ObjectDetections` | Object detection | | `Poses` | Pose estimation | | `DepthMaps` | Depth estimation | | `Segmentations` | Image segmentation | | `Tensors` | Raw output tensors | ```python theme={null} from qimsdk import MLPostprocess, ImageClassifications def classification_callback(mlframe, mlparams, classifications: ImageClassifications): ... return True postprocessing = MLPostprocess("postprocessing") postprocessing.set("results", 1) postprocessing.set_handler(classification_callback) ``` | Method | Description | | - | - | | `MLPostprocess(name: str \| None = None)` | Instantiates a `qtimlpostprocess` element with an optional instance name. | | `classmethod MLPostprocess.cast(element) -> MLPostprocess` | Wraps an existing `Element` (for example one returned by `Pipeline.get()`) as an `MLPostprocess`. | | `MLPostprocess.set(*args, **props) -> MLPostprocess` | Sets one or more properties, same as `Element.set()`. | | `MLPostprocess.set_handler(handler, kind: str \| None = None) -> MLPostprocess` | Registers a callback that decodes tensors into the ML task selected by `kind`, or inferred from the callback's output-parameter type annotation if `kind` is omitted. | ### ML inference bins **Purpose** The ML video bins wrap a full pre-process → inference → post-process chain into a single element, so a whole AI branch can be added to the pipeline as one node instead of being assembled from separate elements. Four bins are available, one per inference runtime: | Python class | GStreamer element | Runtime | | - | - | - | | `MLVideoONNXBin` | `qtimlvideoonnxbin` | ONNX Runtime | | `MLVideoQNNBin` | `qtimlvideoqnnbin` | Qualcomm® AI Engine Direct (QNN) | | `MLVideoSNPEBin` | `qtimlvideosnpebin` | Snapdragon® Neural Processing Engine (SNPE) | | `MLVideoTFLiteBin` | `qtimlvideotflitebin` | LiteRT (TensorFlow Lite) | All four expose an identical API and differ only in the wrapped element and the runtime-specific properties. Each accepts a custom pre-process callback, a custom post-process callback, or both. **Key Responsibilities** * Validate that the wrapped element matches the expected bin factory. * Run pre-processing, inference, and post-processing inside a single element. * Optionally delegate pre-processing to an application-supplied callback via `set_preprocess_handler()`. * Optionally delegate post-processing to an application-supplied callback via `set_postprocess_handler()`. **Properties** Because the bin embeds several stages, its properties are prefixed by the stage they configure — `preprocess-*`, `inference-*`, and `postprocess-*`. For example `inference-model` on the bin corresponds to `model` on a standalone inference element, and `postprocess-labels` corresponds to `labels` on a standalone `qtimlpostprocess`. Set `preprocess-engine` to `none` to disable the internal pre-processing path when you supply your own pre-process callback. The bin's callbacks are `set_preprocess_handler()` and `set_postprocess_handler()` — distinct names from the standalone `MLVConverter.set_handler()` and `MLPostprocess.set_handler()`, since a bin exposes both stages on one object. **Usage** **Custom post-processing** — the bin handles pre-processing and inference internally, the application decodes the output tensors: ```python theme={null} from qimsdk import MLVideoTFLiteBin, VideoFilter, ObjectDetections def detection_callback(mlframe, mlparams, detections: ObjectDetections): ... return True mlbin = MLVideoTFLiteBin("mlbin") mlbin.set("inference-delegate", "external", "inference-external-delegate-path", "libQnnTFLiteDelegate.so", "inference-external-delegate-options", "QNNExternalDelegate,backend_type=htp;", "inference-model", f"{HOME_PATH}/models/yolov8_det_quantized.tflite") mlbin.set_postprocess_handler(detection_callback) pipeline = ( Pipeline("mlbin-external-detection") .add("filesrc", "src", "location", f"{HOME_PATH}/media/video.mp4") .add("qtdemux", "demux") .add("h264parse", "parse") .add("v4l2h264dec", "decoder", "output-io-mode", 4, "capture-io-mode", 4) .add_stream_filter("vf", VideoFilter().format("NV12")) .add(mlbin) .add("qtivoverlay", "overlay") .add("waylandsink", "display", "fullscreen", True) ) pipeline.execute() ``` **Custom pre-processing** — the application converts frames into input tensors, the bin runs inference and its built-in post-process module: ```python theme={null} mlbin = MLVideoTFLiteBin("mlbin") # preprocess-engine=none disables internal preprocessing path in mlbin. mlbin.set("preprocess-engine", "none") mlbin.set("inference-delegate", "external") mlbin.set("inference-external-delegate-path", "libQnnTFLiteDelegate.so") mlbin.set("inference-external-delegate-options", "QNNExternalDelegate,backend_type=htp;") mlbin.set("inference-model", f"{HOME_PATH}/models/yolov8_det_quantized.tflite") mlbin.set("postprocess-module", "yolov8") mlbin.set("postprocess-labels", f"{HOME_PATH}/labels/yolov8.json") def preprocess_callback(blits, output) -> bool: ... return True mlbin.set_preprocess_handler(preprocess_callback) ``` Swapping runtimes is a matter of changing the class and the runtime-specific properties; the callback registration is unchanged. ```python theme={null} from qimsdk import MLVideoQNNBin, MLVideoSNPEBin, MLVideoONNXBin qnn_bin = MLVideoQNNBin("mlbin") # qtimlvideoqnnbin snpe_bin = MLVideoSNPEBin("mlbin") # qtimlvideosnpebin onnx_bin = MLVideoONNXBin("mlbin") # qtimlvideoonnxbin ``` **APIs** The table below applies to each of `MLVideoONNXBin`, `MLVideoQNNBin`, `MLVideoSNPEBin`, and `MLVideoTFLiteBin`; substitute the concrete class name for ``. | Method | Description | | - | - | | `(name: str \| None = None)` | Creates the corresponding `qtimlvideo*bin` element with an optional instance name. | | `.set(*args, **props) -> ` | Sets one or more properties, same as `Element.set()`. Properties are stage-prefixed: `preprocess-*`, `inference-*`, `postprocess-*`. | | `.set_preprocess_handler(handler) -> ` | Registers the external pre-process callback. Requires `preprocess-engine=none` to take effect. | | `.set_postprocess_handler(handler, kind: str \| None = None) -> ` | Registers the external post-process callback. Accepts the same callback shapes as `MLPostprocess.set_handler()`. | ### Buffer **Purpose** Unified buffer abstraction over native GStreamer memory. Provides read/write access, timestamp metadata (pts, dts, duration), resize support, and zero-copy transfer between AppSink and AppSrc. **Key Responsibilities** * Own and manage mapping/unmapping of underlying `Gst.Buffer` or `Gst.Sample`. * Provide read/write access to buffer memory. * Support resizing and writable allocation for AppSrc. * Carry timestamps (PTS/DTS/duration) and apply them back to `Gst.Buffer`. * Enable zero-copy extraction using `take_gst_buffer()`. ```python theme={null} def on_buffer(buf): data = buf.data() pts = buf.pts appsink.set_buffer_consumer(on_buffer) ``` | Method | Description | | - | - | | `Buffer(gst_buffer=None, size=None)` | Wraps an existing `Gst.Buffer`, or allocates a new one of `size` bytes. | | `classmethod Buffer.from_readable_sample(sample) -> Buffer` | Wraps the buffer inside a `Gst.Sample`, as delivered by `AppSink`'s preroll handler. | | `Buffer.data() -> memoryview \| None` | Maps the buffer for reading (or writing, if writable) and returns a `memoryview` onto it. | | `Buffer.size() -> int` | Returns the buffer's size in bytes. | | `Buffer.resize(size: int) -> None` | Reallocates the buffer to `size` bytes, preserving existing data. | | `Buffer.is_writable() -> bool` | Returns whether the buffer can be mapped for writing. | | `Buffer.is_readonly() -> bool` | Returns `True` if the buffer is valid but not writable. | | `Buffer.valid() -> bool` | Returns `True` if the buffer wraps a `Gst.Buffer`. | | `Buffer.take_gst_buffer() -> Any \| None` | Transfers ownership of the underlying `Gst.Buffer` and clears this wrapper, enabling zero-copy hand-off (used internally by `AppSrc.push_buffer()`). | | `Buffer.set_pts(pts_ns)` / `Buffer.set_dts(dts_ns)` / `Buffer.set_duration(duration_ns)` | Sets the buffer's timestamp fields, in nanoseconds. | | `Buffer.pts` / `Buffer.dts` / `Buffer.duration` | Read-only properties for the buffer's timestamp fields; `None` if the buffer is empty. | ### StreamFilter **Purpose** StreamFilter describes the characteristics of a media stream between two pipeline stages so they can negotiate concrete parameters rather than relying on defaults. Without explicit parameters, adjacent components may fall back to suboptimal formats, trigger implicit conversions, or produce mismatched configurations. Specialized subtypes provide fluent, type-safe setters for each domain. Raw caps strings are also accepted for cases not covered by typed setters or when copying directly from gst-launch output. **When to Use StreamFilter** * When two adjacent stages can interoperate in multiple ways, and you want to select explicit parameters instead of relying on defaults. * When you need to optimize latency or performance by avoiding implicit conversions (e.g., choose a hardware-friendly pixel format). * When you want predictable behavior across platforms (e.g., lock color range/primaries/transfer/matrix). * When you integrate tensors for AI/ML and must precisely specify dimensions and element types. * When you need a human-readable summary of the stream parameters for logs and debugging. A `StreamFilter` is not an `Element` and is never added to a pipeline with `add()`. It is always attached between stages with `Pipeline.add_stream_filter(name, filter)`, which internally creates a `capsfilter` element and applies the filter's caps to it. The base `StreamFilter` class itself only exposes a raw-caps-string constructor and `to_string()`; the fluent per-domain setters (`.format()`, `.resolution()`, `.framerate()`, ...) live on the typed subclasses such as `VideoFilter`. **Usage** **Typed subclass** ```python theme={null} from qimsdk import Pipeline, VideoFilter pipeline = ( Pipeline("video-pipeline") .add("videotestsrc", "src", "pattern", "ball") .add_stream_filter("filter", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) .add("waylandsink", "sink") ) pipeline.execute() ``` **Raw caps string** ```python theme={null} from qimsdk import Pipeline, StreamFilter pipeline = ( Pipeline("video-pipeline-streamfilter-string") .add("videotestsrc", "src", "pattern", "ball") .add_stream_filter("filter", StreamFilter( "video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1")) .add("waylandsink", "sink") ) pipeline.execute() ``` **APIs** | Method | Description | | - | - | | `StreamFilter(caps: str)` | Creates a filter from a raw caps or media-type string, e.g. `"video/x-raw,format=NV12"`. | | `StreamFilter.to_string() -> str` | Serializes the current filter expression to a caps string, for logging/debugging. | ### VideoFilter **Purpose** Configures a raw video stream (YUV, RGB, Bayer) between a camera source and any downstream processing or display element. Supports format, resolution, framerate, colorimetry, interlace mode, and pixel aspect ratio. **Key Responsibilities** * Produce caps with standard raw video attributes: format, resolution, framerate, colorimetry, interlace mode, pixel aspect ratio, etc. **APIs** | Method | Description | | - | - | | `format(fmt: str \| VideoFormat)` | Sets the stream's pixel format, for example: `NV12`, `YUY2`, `RGB`, `RGBA`, etc. Determines how pixel data is laid out in memory; essential for hardware compatibility and color-conversion paths. | | `resolution(w: int, h: int)` | Specifies the frame dimensions in pixels. | | `framerate(num: int \| float, den: int = 1)` | Sets the frame rate as a fraction, for example: `30/1`, `30000/1001` frames-per-second. You can pass only `num` if you do not need a fractional frame rate, like `30.0` fps. | | `colorimetry(value: str)` | Defines the colorimetry standard, for example: `bt601`, `bt709`, `bt2020`. | | `range(value: str)` | Selects the quantization range, for example: `full` or `limited`. | | `interlace(mode: str)` | Sets the interlacing mode, for example: `progressive`, `interleaved`, etc. Progressive is standard for modern pipelines; interlaced is rare but still used in some camera/legacy systems. | | `pixel_aspect_ratio(num: int, den: int = 1)` | Specifies the pixel aspect ratio, also called PAR. Normally `1/1`; used for anamorphic or non-square-pixel formats. | | `add(expr: str)` | Appends an arbitrary custom caps field using a `key=value` expression. Escape-hatch for advanced/experimental fields not covered by the typed API. | **Usage** We can achieve the same results in two different ways: **Typed (enum) API** - if you want IDE autocompletion and guardrails against typos. Ideal for most application code. ```python theme={null} from qimsdk import VideoFilter, VideoFormat v1 = ( VideoFilter() .format(VideoFormat.NV12) .resolution(1920, 1080) .framerate(30) .colorimetry("bt709") .range("limited") .interlace("progressive") ) ``` **String API** - if you need quick iteration or to pass values not (yet) covered by the enum — e.g., a freshly added GStreamer value on a target platform. ```python theme={null} from qimsdk import VideoFilter v2 = ( VideoFilter() .format("NV12") .resolution(1920, 1080) .framerate(30) .colorimetry("bt709") .range("limited") .interlace("progressive") ) ``` **Raw caps string** - very useful for setting an arbitrary stream (caps) filter, or if you want to lift an existing caps line from logs, gst-launch, or a pad template and drop it as-is — great for debugging, prototyping, or reproducing pipelines exactly. ```python theme={null} from qimsdk import StreamFilter v3 = StreamFilter("video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1,colorimetry=bt709,range=limited") ``` ### H264Filter **Purpose** Configures an encoded H.264 stream between an encoder and a downstream parser or muxer. Supports resolution, framerate, stream-format, alignment, profile, and level. **Key Responsibilities** * Specify resolution, framerate, stream-format, alignment, profile, level. * Insert `codec_data` buffers. * Attach arbitrary name/value string properties. **APIs** | Method | Description | | - | - | | `resolution(w: int, h: int)` | Specifies the frame dimensions in pixels. | | `framerate(num: int \| float, den: int = 1)` | Sets the frame rate as a fraction. | | `profile(profile: str)` | Sets the H.264 profile caps field, for example `baseline`, `main`, `high`. | | `level(level: str)` | Sets the H.264 level caps field. | | `stream_format(fmt: str)` | Sets the H.264 stream-format caps field, for example `byte-stream`, `avc`. | | `alignment(value: str)` | Sets the H.264 alignment caps field, for example `au`, `nal`. | | `codec_data(value: str)` | Sets the H.264 `codec_data` caps field. | | `set(key: str, value: str)` | Shorthand for `add(f"{key}={value}")`. | | `add(expr: str)` | Appends an arbitrary custom caps field using a `key=value` expression. | **Usage** ```python theme={null} from qimsdk import Pipeline, H264Filter h264f = ( H264Filter() .resolution(1920, 1080) .framerate(30) .profile("baseline") .stream_format("byte-stream") .alignment("au") ) pipeline = Pipeline("h264-pipeline").add_stream_filter("h264f", h264f) ``` ### TensorFilter **Purpose** Configures an ML tensor stream with explicit data type and shape (e.g., UINT8, NHWC). Not needed in a full inference pipeline — when preprocess, inference, and postprocess elements are all present, tensor parameters are negotiated automatically. Use TensorFilter when: * The preprocessing stage does not have model dimension information * You need exact tensor data output from the inference plugin **Key Responsibilities** * Declare element type (e.g., `UINT8`, `FLOAT32`) for tensors. * Specify shape (dimensions). Supports: * Single tensor: a flat list of ints (e.g., `1, 520, 520, 3`). * Multiple tensors: a list of lists (e.g., `[[1,3,H,W], [1,32]]`) when needed. **APIs** | Method | Description | | - | - | | `type(t: TensorFilter.Type \| str)` | Specify tensor data type, for example: `UINT8`, `FLOAT32`. | | `dimensions(*dims)` | Specify tensor dimensions. Pass ints directly for a single tensor, or one or more `list`/`tuple` arguments for multiple tensors. | | `add(expr: str)` | Appends an arbitrary custom caps field using a `key=value` expression. Escape-hatch for advanced/experimental fields not covered by the typed API. | **Usage** We can achieve the same results in two different ways: **Typed (enum) API** - if you want IDE autocompletion and guardrails against typos. Ideal for most application code. ```python theme={null} from qimsdk import TensorFilter t1 = ( TensorFilter() .type(TensorFilter.Type.UINT8) .dimensions(1, 520, 520, 3) # NHWC, for example ) ``` **String API** - if you need quick iteration or to pass values not (yet) covered by the enum. ```python theme={null} from qimsdk import TensorFilter t2 = ( TensorFilter() .type("FLOAT32") .dimensions(1, 3, 640, 640) # NCHW, for example ) ``` **Raw caps string** - very useful for setting an arbitrary stream (caps) filter, or if you want to lift an existing caps line from logs, gst-launch, or a pad template and drop it as-is. ```python theme={null} from qimsdk import StreamFilter t3 = StreamFilter("neural-network/tensors,type=UINT8,dimensions=<<1,520,520,3>>") ``` ### TextFilter **Purpose** Configures a raw text stream (text/x-raw) for pipelines that carry textual data such as metadata, subtitles, or logs. Minimal by design — attach only the fields you need using `.add("key=value")`. **Key Responsibilities** * Provide a lightweight caps descriptor for text (`text/x-raw`) without enforcing a schema. * Let callers attach arbitrary properties (e.g., encoding/charset, language, packetization hints) using `.add("key=value")`. **APIs** | Method | Description | | - | - | | `add(expr: str)` | Appends an arbitrary custom caps field using a `"key=value"` expression. Escape-hatch for advanced/experimental fields not covered by the typed API. | **Usage** ```python theme={null} from qimsdk import TextFilter txt = TextFilter().add("charset=utf8") ``` ### ImageFilter **Overview** Used exclusively with CamSrc (`qtiqmmfsrc`) to enable and link its dedicated image capture pad. Without ImageFilter, the image pad is not requested, not activated, and produces no buffers. It is a structural enabler, not a processing stage — it is not intended for general image processing or frame extraction from video sources. `ImageFilter` is used to: * Enable the image output pad exposed by `CamSrc` (`qtiqmmfsrc`) * Attach that pad to the pipeline so the source can activate image capture * Provide a concrete sink target required by GStreamer for pad activation It is not intended for: * Generic image processing * Frame extraction from arbitrary video sources * Replacement of `VideoFilter` or `AppSink` **Relationship to CamSrc (qtiqmmfsrc)** * `CamSrc` (`qtiqmmfsrc`) exposes a dedicated image pad that remains inactive unless linked. * `ImageFilter` explicitly exists to satisfy this requirement. * Without `ImageFilter`, the image pad: * Will not be requested * Will not be activated * Will not produce buffers This makes `ImageFilter` a structural enabler, not a processing stage. **Usage** ```python theme={null} from qimsdk import ImageFilter imagefilter1 = ( ImageFilter() .format("JPEG") .resolution(3840, 2160) ) ``` ### AudioFilter **Purpose** Configures a raw PCM audio stream with sample format, channel count, sample rate, and layout. Use this to ensure audio converters and sinks negotiate exactly the format your hardware supports rather than inserting an implicit resampler. **Key Responsibilities** * Set audio sample format (e.g., `S16LE`, `F32LE`) to match HW/SW capabilities. * Define channels and rate for correct resampling/mixing. * Choose layout (`interleaved` vs `non-interleaved`) to align with downstream expectations. **APIs** | Method | Description | | - | - | | `format(fmt: str \| AudioFormat)` | Specifies the audio sample format, e.g. `S16LE`, `F32LE`. | | `channels(n: int)` | Specifies the number of audio channels. | | `rate(hz: int)` | Specifies the audio sample rate in Hz. | | `layout(layout: str \| AudioLayout)` | Specifies the audio layout, `interleaved` or `non-interleaved`. | | `add(expr: str)` | Appends an arbitrary custom caps field using a `"key=value"` expression. Escape-hatch for advanced/experimental fields not covered by the typed API. | **Usage** We can achieve the same results in two different ways: **Typed (enum) API** - if you want IDE autocompletion and guardrails against typos. Ideal for most application code. ```python theme={null} from qimsdk import AudioFilter, AudioFormat, AudioLayout a1 = ( AudioFilter() .format(AudioFormat.S16LE) .rate(48000) .channels(2) .layout(AudioLayout.INTERLEAVED) ) ``` **String API** - if you need quick iteration or to pass values not (yet) covered by the enums. ```python theme={null} from qimsdk import AudioFilter a2 = ( AudioFilter() .format("F32LE") .rate(44100) .channels(1) .layout("non-interleaved") ) ``` **Raw caps string** - very useful for setting an arbitrary stream (caps) filter, or if you want to lift an existing caps line from logs, gst-launch, or a pad template and drop it as-is. ```python theme={null} from qimsdk import StreamFilter a3 = StreamFilter("audio/x-raw,format=S16LE,rate=48000,channels=2,layout=interleaved") ``` ### Logging The SDK can either simplify GStreamer's log stream into a compact, uniform format or step aside and let GStreamer log raw, with `GST_DEBUG` fully in control. Both modes, the verbosity levels, the integration with Python's `logging` module, and the ordering constraint on configuring them are covered in [Logging and Diagnostics](/app-builder/python-logging). ## QIM SDK Nodes All nodes are executed by GStreamer elements under the hood. Most elements are handled through the `Element` base class. However, elements that provide additional functionality — such as application buffer interfaces in appsink and appsrc, or image-capture signals in camera sources like `qtiqmmfsrc` — have a specialized Python wrapper class. If you do not need these extra capabilities (for example, image capture), you can still use these elements via the generic `Element` base class without any limitations for standard data processing. Below is a table listing all supported elements and the corresponding QIM SDK Python wrapper classes used to work with them. **Sources** | GStreamer | QIM SDK Python Class | | - | - | | `qtiqmmfsrc` | `CamSrc` (also usable as `Element`) | | `v4l2src` | `Element` | | `filesrc` | `Element` | | `multifilesrc` | `Element` | | `appsrc` | `AppSrc` | | `appsink` | `AppSink` | | `qtisocketsrc` | `Element` | **AI** | GStreamer | QIM SDK Python Class | | - | - | | `qtimlaconverter` | `Element` | | `qtimlvconverter` | `MLVConverter` (also usable as `Element`) | | `qtimlqnn` | `Element` | | `qtimlsnpe` | `Element` | | `qtimltflite` | `Element` | | `qtimlpostprocess` | `MLPostprocess` (also usable as `Element`) | | `qtimlvideoonnxbin` | `MLVideoONNXBin` (also usable as `Element`) | | `qtimlvideoqnnbin` | `MLVideoQNNBin` (also usable as `Element`) | | `qtimlvideosnpebin` | `MLVideoSNPEBin` (also usable as `Element`) | | `qtimlvideotflitebin` | `MLVideoTFLiteBin` (also usable as `Element`) | | `qtimlaic` | `Element` | | `qtimetamux` | `Element` | | `qtimlmetaextractor` | `Element` | | `qtimlmetaparser` | `Element` | | `qtimldemux` | `Element` | | `qtimetatransform` | `Element` | | `mlonnx` | `Element` | **Multimedia** | GStreamer | QIM SDK Python Class | | - | - | | `qtiobjtracker` | `Element` | | `qtismartvencbin` | `Element` | | `qtivoverlay` | `Element` | | `qtivsplit` | `Element` | | `qtivcomposer` | `Element` | | `qtibatch` | `Element` | | `qticamreproc` | `Element` | | `qtic2adec` | `Element` | | `qtic2aenc` | `Element` | | `qtic2vdec` | `Element` | | `qtic2venc` | `Element` | | `qtijpegenc` | `Element` | | `qtivtransform` | `Element` | | `qticvimgpyramid` | `Element` | | `qticvoptclflow` | `Element` | | `qtidfs` | `Element` | | `v4l2h264dec` | `Element` | | `v4l2h264enc` | `Element` | | `v4l2h265dec` | `Element` | | `v4l2h265enc` | `Element` | | `tee` | `Element` | | `h264parse` | `Element` | | `rtpptdemux` | `Element` | | `rtph264depay` | `Element` | | `queue` | `Element` | | `qtdemux` | `Element` | | `tsdemux` | `Element` | ## Common Application Design ### Create pipeline with config file The SDK supports fully declarative pipeline construction using YAML. A YAML pipeline describes elements and links between them, which are translated at runtime into a GStreamer pipeline. **TopLevel Structure** A YAML pipeline file has a single top-level key: ```yaml theme={null} pipeline: elements: [...] links: [...] ``` **Elements Section** `elements` is an ordered list of pipeline elements. Each element entry defines: * The GStreamer element type * A unique name used for linking * Optional properties * Optional filter configuration **Common Fields** ```yaml theme={null} - type: name: : ``` * `type`: GStreamer element factory name, or `filter` * `name`: Unique identifier within the pipeline * properties: Any remaining key/value pairs are passed directly to the element as GStreamer properties **Filter Elements** Filters are declared using `type: filter` and are further specialized by their configuration block. **Stream Filter** ```yaml theme={null} - type: capsfilter name: raw_caps caps: "video/x-raw,format=NV12,width=1280,height=720,framerate=30/1" ``` **Video Filter** ```yaml theme={null} - type: filter name: vf video: format: NV12 width: 1920 height: 1080 framerate: 30 ``` **Links Section** `links` defines explicit connection paths between elements. Each entry is an ordered list of element names. Elements are linked sequentially from left to right. ```yaml theme={null} links: - [src, demux, parse, decoder, vf, split] ``` **Example yaml files** **YAML config file** ```yaml theme={null} pipeline: elements: - type: qtiqmmfsrc name: source camera: "0" - type: filter name: filter video: format: NV12 width: 1920 height: 1080 framerate: 30.0 - type: tee name: split - type: qtimlvconverter name: preprocessing - type: qtimltflite name: inferencing delegate: gpu model: /etc/models/model.tflite - type: qtimlpostprocess name: postprocessing - type: qtimetamux name: mlmuxer - type: qtivoverlay name: overlay - type: waylandsink name: display sync: true async: true fullscreen: true links: - [source, filter, split, preprocessing, inferencing, postprocessing, mlmuxer, overlay, display] - [split, mlmuxer] ``` ### Multi-Model Daisy Chain Below is shown a logical diagram of an AI daisy chain pipeline with sequential and parallel inferencing. The same could be handled by a straight sequential pipeline of inference bins. But we must set manually the source for every inference bin. Introduction Several `.add(...)` calls in this example had missing/mismatched quotes in the source PDF (e.g. a stray closing paren instead of a closing quote, or a missing leading quote). These have been corrected below where unambiguous. One inline comment truncated at the page margin was completed using the identical sentence found in the prose above. ```cpp theme={null} Element ml_car("qtimlvideotflitebin", "car") .add("performance-profile", "high-performance") .add("inference-delegate", "dsp") .add("inference-model", "/opt/car_detect.dlc") .add("inference-layers", "< /model.22/Mul_2, /model.22/Sigmoid >") .add("postprocess-threshold", 60.0) .add("postprocess-results", 4) .add("postprocess-module", "yolov8") .add("postprocess-labels", "/opt/car_detect_model.labels"); // The same could be handle by straight sequential pipeline of inference bins. But we must set manually source to every inference bin. Element ml_plate("qtimlvideotflitebin", "plated") .add("source", 1) .add("image-disposition", "centre") .add("performance-profile", "high-performance") .add("inference-delegate", "dsp") .add("inference-model", "/opt/plate_detect.dlc") .add("inference-layers", "< /model.22/Mul_2, /model.22/Sigmoid >") .add("postprocess-threshold", 40.0) .add("postprocess-results", 1) .add("postprocess-module", "yolov8") .add("postprocess-labels", "/opt/plate_detect_model.labels"); Element ml_anpr("qtimlvideotflitebin", "anpr") .add("source", 2) .add("image-disposition", "centre") .add("inference-delegate", "dsp") .add("inference-model", "/opt/anpr.dlc") .add("inference-layers", "< /decode_head2/classifier/Conv, /decode_head/classifier/Conv >") .add("postprocess-threshold", 15.0) .add("postprocess-results", 1) .add("postprocess-module", "anpr") .add("postprocess-labels", "/opt/anpr_model.labels"); Element ml_siglip("qtimlvideotflitebin", "siglip") .add("source", 1) .add("image-disposition", "centre"); .add("inference-delegate", "dsp"); .add("inference-model", "/opt/car_make_model.dlc"); .add("inference-layers", "< /classifier/Gemm >"); .add("postprocess-threshold", 15.0); .add("postprocess-results", 1); .add("postprocess-module", "mobilenet"); .add("postprocess-labels", "/opt/car_make_model.labels"); Pipeline pipeline("SmartVehicleIdentification"); pipeline.add("camsrc", "source", "camera", "0") .add_stream_filter("filter", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) .add(ml_car) .add(ml_plate) .add(ml_anpr) .add(ml_siglip) .add("qtivoverlay", "overlay") .add("waylandsink", "display", "sync", "true", "async", "true", "fullscreen") .execute(); ``` ### Custom Postprocessing for YOLOv8 (Detection) QIM SDK implements AI model post-processing through dedicated modules that take tensors as input and produce predictions. All remaining complexity — such as batching, daisy-chaining, image mask support — is encapsulated within the post-processing plugin. This makes the post-processing module API an ideal integration point for application-specific post-processing logic. As a result, applications do not need to handle batching, chaining, image masks, or ML metadata themselves. We will implement a specialized post-processing module to manage bindings with the application. Introduction Custom postprocessing callback is attached to a [`qtimlpostprocess`](/plugin-reference/qtimlpostprocess) element via [`MLPostprocess.set_handler(...)`](/plugin-reference/qtimlpostprocess). Typical pipeline segment: 1. Preprocess frame/tensors ([`qtimlvconverter`](/plugin-reference/qtimlvconverter)) 2. Run ML inference ([`qtimltflite`](/plugin-reference/qtimltflite)) 3. Convert raw tensors in custom callback ([`qtimlpostprocess`](/plugin-reference/qtimlpostprocess)) **Supported output types** * `ImageClassifications` * `ObjectDetections` * `Poses` * `DepthMaps` * `Segmentations` * `Tensors` **Minimal usage** ```python theme={null} from qimsdk import MLPostprocess, ObjectDetections def my_callback(mlframe, mlparams, out: ObjectDetections): ... return True postprocessing = MLPostprocess("postprocessing") postprocessing.set_handler(my_callback) pipeline = ( Pipeline("my-pipeline") .add("qtimltflite", "inferencing", ...) .add(postprocessing) .add_stream_filter("mlf", TextFilter()) ... ) ``` **Input and output explained** `mlframe` (read-only model tensors) Passed to every callback. Its `tensors` list holds one entry per model output tensor, each exposing: * `type`: tensor element type * `dimensions`: tensor shape * `data`: raw tensor memory (as a `memoryview`) * `size`: tensor bytes Common access pattern: ```python theme={null} t = mlframe.tensors[0] data = t.data ``` `mlparams` (runtime metadata and context) `mlparams` carries key/value fields provided by the pipeline and preprocessing/postprocess context. Commonly used keys: * `input-tensor-width` * `input-tensor-height` * `input-tensor-region` (an `(x, y, width, height)` region) Example: ```python theme={null} source_width = mlparams.get("input-tensor-width") source_height = mlparams.get("input-tensor-height") region = mlparams.get("input-tensor-region") ``` **Callback signatures** * Callbacks annotated with `ImageClassifications`, `AudioClassifications`, `ObjectDetections`, `Poses`, `DepthMaps`, or `Segmentations` fill the corresponding output metadata container. * A callback annotated with `Tensors` fills an output `Tensors` object for downstream tensor stages. * `mlframe` is read-only and must not be modified. **Classification callback** ```python theme={null} def classification_callback(mlframe, mlparams, out: ImageClassifications): ... return True ``` Output: append classification items to `out`. Field meanings: * `name`: class label text * `confidence`: prediction confidence * `color`: optional UI/overlay color * `extra`: optional metadata map **Object detection callback** ```python theme={null} def detection_callback(mlframe, mlparams, out: ObjectDetections): ... return True ``` Output: append detection items to `out`. Field meanings: * `left`, `top`, `right`, `bottom`: normalized box coordinates * `name`, `confidence`, `color`: class info * `landmarks`: optional keypoints * `extra`: optional metadata map Implemented example includes confidence thresholding, region coordinate transform, and NMS. **Segmentation callback** ```python theme={null} def segmentation_callback(mlframe, mlparams, segmentations: Segmentations): ... return True ``` Input parameters * `mlframe`: read-only model output tensors. * `mlparams`: runtime metadata (for example: `input-tensor-width`, `input-tensor-height`, `input-tensor-region`). * `segmentations`: output container to fill. Output contract * `labels`: per-pixel semantic labels. * `colors`: per-pixel colors; index-aligned with `labels`. * `n_rows`, `n_columns`: segmentation map dimensions. * `extra`: optional metadata fields. **Pose estimation callback** ```python theme={null} def pose_callback(mlframe, mlparams, out: Poses): ... return True ``` Output: append pose items to `out`. Field meanings: * `keypoints`: list of keypoints (`name`, `x`, `y`, `confidence`, `color`) * `links`: list of keypoint links describing skeleton connections * `name`, `confidence`, `extra`: pose-level metadata **Depth estimation callback** ```python theme={null} def depth_callback(mlframe, mlparams, out: DepthMaps): ... return True ``` Output: append depth map items to `out`. Field meanings: * `values`: per-pixel depth values * `colors`: per-pixel pseudo-color values * `n_rows`, `n_columns`: depth map shape * `extra`: optional metadata map **Tensor-to-tensor callback** ```python theme={null} def tensor_callback(mlframe, mlparams, out: Tensors): ... return True ``` `out` is owned and writable; callback typically: 1. validates input tensors from `mlframe` 2. copies/selects required tensors into `out` 3. updates output tensor dimensions/order if needed **Semantic callbacks** (classification, detection, pose, depth, segmentation) ```python theme={null} def callback(mlframe, mlparams, output: OutputType) -> bool: ... ``` * `mlframe`: input tensors from inference (read-only) * `mlparams`: runtime metadata (input sizes, region, etc.) * `output`: semantic objects to be filled by callback * return `True` on valid output * return `False` on validation/decode failure **Tensor callback** ```python theme={null} def callback(mlframe, mlparams, output: Tensors) -> bool: ... ``` * `mlframe`: input tensors from inference (read-only) * `mlparams`: runtime metadata/context * `output`: writable output tensors for downstream tensor stage * return `True` on valid output * return `False` on validation/decode failure **Copy-ready callback skeletons** Classification ```python theme={null} def classification_callback(mlframe, mlparams, out: ImageClassifications): # decode tensors -> classes return True ``` Detection ```python theme={null} def detection_callback(mlframe, mlparams, out: ObjectDetections): # decode tensors -> detections return True ``` Pose estimation ```python theme={null} def pose_callback(mlframe, mlparams, out: Poses): # decode tensors -> poses return True ``` Depth estimation ```python theme={null} def depth_callback(mlframe, mlparams, out: DepthMaps): # decode tensors -> depthmaps return True ``` Segmentation ```python theme={null} def segmentation_callback(mlframe, mlparams, out: Segmentations): # decode tensors -> segmentations return True ``` Tensors ```python theme={null} def tensor_callback(mlframe, mlparams, out: Tensors): # map/copy selected tensors from mlframe -> out return True ``` Check application source code on GitHub: [`qimsdk_ref_external_postprocess_detection_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_detection_yolov8.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_detection_yolov8.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. Also copy an MP4 (H.264) file for the pipeline's video input, or update the pipeline's `location` property to point to your own file. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp video.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_postprocess_detection_yolov8.py ``` The application runs inference on the video file and uses the custom, application-defined postprocessing callback to decode detections, which are then overlaid and displayed fullscreen. To stop the application, press **CTRL + C**. # Coding Agent Source: https://imsdkdocs.qualcomm.com/app-builder/python-coding-agent Use the QIM SDK Python App Builder skill to generate Python pipeline apps with an AI coding agent. ## What is the Python App Builder Skill? The **QIM SDK Python App Builder** is an AI coding skill that generates Python applications using `QIM SDK Python API`. You describe the pipeline behavior and configuration in natural language, and the agent produces a ready-to-run `main.py` built on the `QIM SDK Python API` — along with a `README.md` that documents information about the generated application. **Flow** QIM SDK coding agent workflow **What it generates** For each request, the agent produces an artifact folder named `qimsdk-python-/` containing: * **`main.py`** — a complete, runnable Python application using qimsdk python package, including pipeline construction, element wiring, stream filters, and ML inference configuration * **`README.md`** — a detailed document covering: * Purpose and pipeline behavior summary * Configuration placeholders (input, model, labels, output paths) * A step-by-step pipeline flow (text summary + Mermaid diagram) * Steps to run the app on device * **`A YAML config file`** — only when you request declarative YAML pipeline mode **Supported use cases:** | Category | Details | | - | - | | **AI pipelines** | Object detection, image classification, semantic segmentation, face detection, pose estimation, super resolution, depth estimation, audio classification, object tracking, gesture recognition | | **Inputs** | ISP camera, USB camera, file source (MP4/H.264), RTSP stream | | **Outputs** | HDMI display, encoded file (MP4), RTSP stream, Redis/MQTT metadata | | **ML backends** | TFLite (HTP/NPU), QNN, SNPE | | **Pipeline patterns** | Single-stream, multi-stream, daisy-chained models, ML-bin fused inference, custom pre/post-processing, AppSrc/AppSink bridges, YAML config mode | ## Prerequisites ### For generating code Qualcomm dev kits are not needed for code generation. **Host Machine**: Any PC with internet access **Coding Agent**: Any AI coding agent installed (Claude Code, Cursor, Codex, etc.) which supports skills — the skills provided are agent-agnostic. This skill is designed to work well even with low-reasoning / smaller models. Eg: A Sonnet 4.5 model is sufficient. ### For deploying and running apps The target devices where QIM SDK is supported (see [QIM SDK Installation Guide](/installation)) ## How to Use ### Step 1: Get the skill Choose one installation method: Clone the skill repository and copy the **QIM SDK Python App Builder** and **qimsdk-deploy** skills into your coding agent's skills directory: ```bash theme={null} git clone https://github.com/qualcomm/qimsdk-agentic-skills # The following commands are for Claude Code. # Set this to your coding agent's skills directory. cp -r qimsdk-agentic-skills/skills/qimsdk-python-app-builder ~/.claude/skills cp -r qimsdk-agentic-skills/skills/qimsdk-deploy ~/.claude/skills ``` The skills directory is agent-specific. Check your coding agent's documentation for its skills location. Restart or reload your coding agent after installation. Verify that `qimsdk-python-app-builder` and `qimsdk-deploy` are available with `/skills`. **Claude Code** — run inside a Claude Code session: ```text theme={null} /plugin marketplace add https://github.com/qualcomm/qimsdk-agentic-skills.git /plugin install qimsdk-agentic-skills@qimsdk-marketplace /reload-plugins ``` **Codex** — run in your terminal: ```bash theme={null} codex plugin marketplace add https://github.com/qualcomm/qimsdk-agentic-skills.git codex plugin add qimsdk-agentic-skills@qimsdk-marketplace ``` *** ### Step 2: Generate the application Describe your pipeline to the agent. It will automatically activate the skill and generate a complete `main.py` and `README.md`. #### Sample Prompt **Prompt format — describe pipeline behavior and configuration:** ``` Create a QIM SDK Python app for single-stream YOLOX object detection from an MP4 file, encoding the result to a file. ## Pipeline Behavior - Decode an MP4 file using Qualcomm hardware decoder - Run YOLOX object detection on full frames using TFLite external delegate on HTP/NPU - Merge metadata with the video stream before overlay - Overlay bounding boxes and labels - Encode the overlaid video and save to an output MP4 file ## Configuration - Input: ~/Downloads/qimsdk_samples/media/ai_demo_sample.mp4 - Model: ~/Downloads/qimsdk_samples/models/yolox_w8a8.tflite - Labels: ~/Downloads/qimsdk_samples/labels/yolov8.json - Backend: TFLite external delegate, HTP/NPU - Confidence: 51.0 - Output: ~/Downloads/qimsdk_samples/media/obj_detect_out.mp4 ``` More sample prompts can be found in the [Available Sample Prompts](#available-sample-prompts) section below. #### Generated `README.md` The README documents the generated app in full: * **Purpose** — a plain-English summary of what the pipeline does * **Files** — lists `main.py` and `README.md` with descriptions * **Assumptions** — codec format, quantization requirements, camera defaults, output directory pre-conditions * **Configuration** — all user-supplied paths (`INPUT_FILE`, `MODEL_PATH`, `LABELS_PATH`, `OUTPUT_FILE`) with instructions on where to change them * **Placeholders to Fill** — any values you still need to supply, or a note that none remain when the request was fully specified * **Pipeline Flow** — a `Text Summary` walkthrough of every element plus a `Mermaid Diagram` of the full pipeline * **Steps to Run** — exact commands to run on device # QIM SDK Python App — Single-Stream YOLOX Object Detection (MP4 → MP4) ## Purpose Decode an MP4 file with the Qualcomm hardware decoder, run **YOLOX** object detection on full frames using the **TFLite external delegate on the HTP/NPU**, merge the detection metadata with the video stream, overlay bounding boxes and class labels, then hardware-encode the annotated video to an output MP4 file. Headless (no display branch). ## Files * `main.py` — the qimsdk pipeline app * `README.md` — this file ## Assumptions * Input is H.264-in-MP4, decoded with `v4l2h264dec` (`capture-io-mode=4`, `output-io-mode=4` — file source decoded through the hardware decoder). * YOLOX (`yolox_w8a8.tflite`) uses the `yolov8` postprocess module with the `yolov8.json` labels, per the model catalog. * Confidence threshold is applied as inline postprocess settings `{"confidence": 51.0}`. * No display: the annotated stream is encoded and written straight to the output MP4 (`v4l2h264enc` → `h264parse` → `mp4mux` → `filesink`). * `pipeline.eos(True)` is set so `mp4mux` finalizes the container on EOS. ## Configuration (fixed constants in `main.py`) | Value | Path | | - | - | | Input | `$HOME/Downloads/qimsdk_samples/media/ai_demo_sample.mp4` | | Model | `$HOME/Downloads/qimsdk_samples/models/yolox_w8a8.tflite` | | Labels | `$HOME/Downloads/qimsdk_samples/labels/yolov8.json` | | Output | `$HOME/Downloads/qimsdk_samples/media/obj_detect_out.mp4` | `$HOME` is expanded in Python (`os.environ['HOME']`) before being passed to any element property, so the element receives a fully-resolved absolute path. ## Placeholders to Fill None. All paths, model, labels, confidence, and delegate options are concrete. ## Pipeline Flow ### Text Summary `filesrc` reads the MP4 and `qtdemux` extracts the H.264 elementary stream, which `h264parse` prepares and `v4l2h264dec` hardware-decodes. A `queue` (`q_dec`) decouples the decoder thread, then a `VideoFilter` constrains the stream to NV12. A `tee` (`split`) branches: * **Passthrough / video branch** → `qtimetamux` (`obj_mux`). * **AI branch** → `queue` → `qtimlvconverter` (preprocess to model tensor) → `queue` → `qtimltflite` (YOLOX, external delegate, HTP/NPU) → `queue` → `qtimlpostprocess` (`module=yolov8`, labels, `{"confidence": 51.0}`) → `TextFilter` → `queue` → `qtimetamux` (`obj_mux`). `qtimetamux` merges the detection metadata back onto the video frames, `qtivoverlay` draws the bounding boxes and class labels, and the annotated NV12 stream is hardware-encoded by `v4l2h264enc`, prepared by `h264parse`, muxed by `mp4mux`, and written by `filesink` to the output MP4. ### Mermaid Diagram ```mermaid theme={null} flowchart TD SRC[filesrc] --> DEMUX[qtdemux] DEMUX --> PARSE[h264parse] PARSE --> DEC[v4l2h264dec] DEC --> QDEC[queue q_dec] QDEC --> NV12[NV12 VideoFilter] NV12 --> SPLIT[tee split] SPLIT -->|passthrough| MUX[qtimetamux obj_mux] SPLIT -->|AI| Q1[queue q1] Q1 --> PRE[qtimlvconverter] PRE --> Q2[queue q2] Q2 --> INFER[qtimltflite YOLOX HTP] INFER --> Q4[queue q4] Q4 --> POST[qtimlpostprocess module=yolov8] POST --> TEXT[TextFilter] TEXT --> Q5[queue q5] Q5 --> MUX MUX --> OVL[qtivoverlay] OVL --> ENC[v4l2h264enc] ENC --> PARSEOUT[h264parse parse_out] PARSEOUT --> MP4[mp4mux] MP4 --> SINK[filesink] ``` ## Steps to Run First ensure all referenced files — the input MP4, the `yolox_w8a8.tflite` model, and the `yolov8.json` labels — are already present on the device at the paths configured in `main.py`. ```bash theme={null} # Copy the app to device scp main.py @:~/ # SSH into device and run ssh @ python3 ~/main.py ``` The input MP4 must exist at `$HOME/Downloads/qimsdk_samples/media/ai_demo_sample.mp4` on the device, and the output directory (`$HOME/Downloads/qimsdk_samples/media/`) must be writable so the app can create `obj_detect_out.mp4`. The app runs to end-of-file, finalizes the MP4, and exits. #### Generated `main.py` * The generated application constructs a tee-split pipeline: decoded frames are split into a passthrough branch and an AI branch. * The AI branch runs `qtimlvconverter` (preprocess) → `qtimltflite` (YOLOX inference via HTP/NPU) → `qtimlpostprocess` → `TextFilter`, then merges back into `qtimetamux` with the passthrough video. * `qtivoverlay` draws bounding boxes, and the result is hardware-encoded and written to an MP4 file via `filesink`. * Pipeline construction and `pipeline.execute()` live inside `create_and_execute_pipeline()`; `main()` handles logging setup and calls it. * All paths use `$HOME` expansion via `os.environ['HOME']` — element properties do not perform shell expansion. ```python theme={null} #!/usr/bin/env python3 """Single-stream YOLOX object detection from an MP4 file, encoded to an output MP4 file.""" import os from qimsdk import Element, Pipeline, TextFilter, VideoFilter # Example pipeline: # # src -> demux -> parse -> decoder -> q_dec -> [vf:NV12] -> tee name=split # split. -> obj_mux -> qtivoverlay -> encoder -> parse_out -> mux -> filesink # split. -> q1 -> preprocessing -> q2 -> inferencing -> q4 # -> postprocessing -> [mlf:text] -> q5 -> obj_mux. # # The pipeline reads an MP4/H.264 file, decodes it through the hardware decoder, # runs YOLOX (yolov8 postprocess module) object detection on full frames using # the HTP/NPU external delegate, merges the detection metadata back with the # video, overlays the detected bounding boxes and class labels on each frame, # and hardware-encodes the annotated stream to an output MP4 file. INPUT_FILE = f"{os.environ['HOME']}/Downloads/qimsdk_samples/media/ai_demo_sample.mp4" MODEL_PATH = f"{os.environ['HOME']}/Downloads/qimsdk_samples/models/yolox_w8a8.tflite" LABELS_PATH = f"{os.environ['HOME']}/Downloads/qimsdk_samples/labels/yolov8.json" OUTPUT_FILE = f"{os.environ['HOME']}/Downloads/qimsdk_samples/media/obj_detect_out.mp4" def create_and_execute_pipeline() -> None: # Reads the input MP4 file as raw bytes. source = ( Element("filesrc", "source") .set("location", INPUT_FILE) ) # Extracts the elementary H.264 stream from the MP4 container. demux = Element("qtdemux", "demux") # Prepares the H.264 bitstream for the decoder. parser = Element("h264parse", "parser") # Decodes the compressed H.264 stream into raw video frames. # # DMA I/O modes are used to avoid unnecessary buffer copies. This is a # file source decoded through the hardware decoder, so both io-modes are 4. decoder = ( Element("v4l2h264dec", "decoder") .set("capture-io-mode", 4) .set("output-io-mode", 4) ) # Queue immediately after hardware decode to decouple the decoder thread. q_dec = Element("queue", "q_dec") # Restricts the decoded stream to NV12 before branching for AI/overlay. videofilter = VideoFilter().format("NV12") # Splits decoded frames into a video/overlay branch and an AI branch. split = Element("tee", "split") # Queues frames from the tee into the AI branch. q1 = Element("queue", "q1") # Converts raw NV12 video frames into the model input tensor format. preprocessing = Element("qtimlvconverter", "preprocessing") # Queues converted tensors before inference. q2 = Element("queue", "q2") # Runs YOLOX object detection on full frames using the TFLite external # delegate targeting the HTP/NPU. inferencing = ( Element("qtimltflite", "inferencing") .set("model", MODEL_PATH) .set("delegate", "external") .set("external-delegate-path", "libQnnTFLiteDelegate.so") .set("external-delegate-options", "QNNExternalDelegate,backend_type=htp,log_level=(string)1;") ) # Queues tensor outputs before postprocessing. q4 = Element("queue", "q4") # Decodes YOLOX model output tensors into detection metadata. # YOLOX detection uses the yolov8 postprocess module per the model catalog. # The confidence threshold (51.0) is applied via inline JSON settings. postprocessing = ( Element("qtimlpostprocess", "postprocessing") .set("module", "yolov8") .set("labels", LABELS_PATH) .set("settings", '{"confidence": 51.0}') ) # Stream filter marking the AI branch output as text metadata. mlf = TextFilter() # Queues metadata before it is merged back with the video branch. q5 = Element("queue", "q5") # Merges detection metadata produced by the AI branch with the original # video frames from the passthrough branch. obj_mux = Element("qtimetamux", "obj_mux") # Renders detected bounding boxes and class labels on top of the frame. overlay = Element("qtivoverlay", "overlay") # Hardware-encodes the annotated NV12 stream back to H.264. # # File source path decoded via hardware decoder: capture/output io-mode 4. encoder = ( Element("v4l2h264enc", "encoder") .set("capture-io-mode", 4) .set("output-io-mode", 4) ) # Prepares the encoded H.264 stream for muxing. parse_out = Element("h264parse", "parse_out") # Multiplexes the H.264 stream into an MP4 container. mux = Element("mp4mux", "mux") # Writes the final annotated video to the output MP4 file. sink = ( Element("filesink", "sink") .set("location", OUTPUT_FILE) ) # Creates the pipeline, adds and links elements, and executes it. pipeline = ( Pipeline("obj-detect-file-pipeline") .add(source) .add(demux) .add(parser) .add(decoder) .add(q_dec) .add_stream_filter("videofilter", videofilter) .add(split) .add(q1) .add(preprocessing) .add(q2) .add(inferencing) .add(q4) .add(postprocessing) .add_stream_filter("mlf", mlf) .add(q5) .add(obj_mux) .add(overlay) .add(encoder) .add(parse_out) .add(mux) .add(sink) .link("source", "demux", "parser", "decoder", "q_dec", "videofilter", "split") .link("split", "obj_mux") .link("split", "q1", "preprocessing", "q2", "inferencing", "q4", "postprocessing", "mlf", "q5", "obj_mux") .link("obj_mux", "overlay", "encoder", "parse_out", "mux", "sink") ) # Ensures the MP4 muxer finalizes the container correctly on EOS. pipeline.eos(True) pipeline.execute() def main() -> None: from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) create_and_execute_pipeline() if __name__ == "__main__": main() ``` *** ### Step 3: Running the applications | File | Download | Save as | | - | - | - | | YOLOX W8A8 model | [Qualcomm AI Hub — YOLOX](https://aihub.qualcomm.com/iot/models/yolox) | `yolox_w8a8.tflite` | | Detection labels | yolov8.json | `yolov8.json` | | Sample video | Input video | `ai_demo_sample.mp4` | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` The input video, model, and labels must be present on the device before running — regardless of how you run the app. Copy them to the paths `main.py` expects: ```bash theme={null} # Replace and with your device credentials. ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media ~/Downloads/qimsdk_samples/models ~/Downloads/qimsdk_samples/labels" # Copy the assets to device (paths must match main.py) scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ scp yolox_w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` With the assets in place, you have two options to deploy and run the agent-generated `main.py`: **Option A — Use the QIM SDK Deploy skill (Recommended):** The `qimsdk-deploy` skill pushes `main.py` and runs it directly on the target device over SSH. It copies only `main.py` — the assets from the previous step must already be on the device. **Option B — Follow the generated README:** Follow the **steps to run** in the [generated `README.md`](#generated-readme-md) to manually copy `main.py` to device and run the app: ```bash theme={null} # Copy the app to device scp main.py @:~/ # SSH into device and run ssh @ python3 ~/main.py ``` *** ## Available Sample Prompts | # | Prompt | | - | - | | 1 | [Camera YOLOv8 object detection using fused TFLite ML-bin](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_01_mlbin_yolov8_camera_overlay.md) | | 2 | [Two-stage PPE daisy-chain — person detection → PPE detection → overlay display](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_02_mlbin_ppe_daisy_chain_display.md) | | 3 | [Object detection with custom postprocess placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_03_custom_postprocess_detection_placeholder.md) | | 4 | [Pose estimation with custom postprocess placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_04_custom_postprocess_pose_placeholder.md) | | 5 | [Inference app with tensor output via custom callback placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_05_tensor_dump_callback_placeholder.md) | | 6 | [YOLOv8 object detection pipeline with generated YAML config](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_06_yaml_yolov8_generated_config.md) | | 7 | [Object detection with custom preprocess placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_07_custom_preprocess_detection_placeholder.md) | | 8 | [Object detection with ML-bin custom preprocess placeholder](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_08_mlbin_custom_preprocess_detection_placeholder.md) | | 9 | [Load existing YOLOv8 detection pipeline from external YAML config](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_09_yaml_yolov8_external_config.md) | | 10 | [Single-stream YOLOX object detection from MP4, encoded to file](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_10_single_stream_object_detection.md) | | 11 | [AI wall — four parallel AI inference streams composed into a 2×2 grid display](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_11_ai_wall.md) | | 12 | [Four-stage gesture recognition daisy-chain pipeline](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_12_gesture_recognition.md) | | 13 | [Audio classification — classify audio events from video and overlay results](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/AI_13_audio_classification.md) | | 14 | [Two pipelines bridged through application-managed buffers (AppSrc/AppSink)](https://github.com/qualcomm/qimsdk-agentic-skills/blob/main/sample-prompts/qimsdk-python-app-builder/MM_01_appsrc_appsink_bridge.md) | # Demo Applications Source: https://imsdkdocs.qualcomm.com/app-builder/python-demo-applications Ready-to-run Python App Builder demo applications The table below lists the Python App Builder demo applications built on top of the [Python App Builder](/app-builder/python-app-builder) SDK. | # | Application | Source Code | Description | | - | - | - | - | | 1 | [Product Counting Application](/blogs/product-counting-application) | [qimsdk\_demo\_product\_counting.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/demo-apps/qimsdk_demo_product_counting.py) | A YOLOv8-based object detection pipeline that counts products crossing a center region of interest in real time. | | 2 | [Smart Board Application](/blogs/smart-board-application) | [qimsdk\_demo\_smartboard.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/demo-apps/qimsdk_demo_smartboard.py) | A touchless virtual whiteboard controlled entirely by hand gestures, supporting freehand drawing, shape recognition, erasing, and dragging content via a two-stage palm detection and hand landmark pipeline. | | 3 | [Touchless Wrist Steering Car Game](/blogs/touchless-wrist-steering-car-game) | [qimsdk\_demo\_car\_game.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/demo-apps/qimsdk_demo_car_game.py) | A two-stage palm detection and hand landmark pipeline turns wrist movement into a virtual steering wheel for a real-time, touchless split-screen driving game. | # Quick start guide Source: https://imsdkdocs.qualcomm.com/app-builder/python-quickstart A Quick demonstration of Python SDK for building AI application on Qualcomm platforms ## What is the QIM SDK Python App Builder? ### Overview The **QIM SDK Python App Builder** is a modern Python SDK for building, running, and managing multimedia and AI pipelines on Qualcomm platforms. It sits as a high-level abstraction layer on top of GStreamer, taking care of framework-level concerns such as element negotiation, pad linking, signal management, event loops, and state transitions, so developers can stay focused on application logic instead of framework boilerplate. Introduction ### Key Capabilities * **No GStreamer expertise required** — the SDK manages all underlying GStreamer mechanics internally, with no compile-time dependency on GStreamer headers. * **Less code, no GStreamer boilerplate** — a fluent, high-level API bundles away the pipeline construction, element linking, and state management that GStreamer's native API would otherwise require, so a functional camera-to-display pipeline can be written in just a few lines of Python. * **Custom pre/post-processing** — plug in your own pre- and post-processing logic around the built-in ML bins without writing a custom GStreamer element. * **AI inference support** — built-in ML bins for TFLite, QNN, SNPE, and ONNX models. * **Broad multimedia support** — video capture, signal processing, ML inference, overlay rendering, and display output. ### API Reference
API Description API Code Reference
**Pipeline** Builds, links, and manages the full lifecycle of a GStreamer pipeline. Supports both programmatic (Python fluent API) and declarative (YAML) pipeline definition. Key methods: `add()`, `link()`, `add_stream_filter()`, `execute()`, `prepare()`, `start()`, `stop()`, `get()`. ```python theme={null} p = Pipeline("cam-pipeline") p.add("qtiqmmfsrc", "src").add("waylandsink", "sink").execute() ```
**Element** Generic wrapper around a GStreamer element. Supports fluent property setting at create time or at runtime to update a live pipeline. ```python theme={null} sink = Element("waylandsink", "display") sink.set("fullscreen", True) sink.set("sync", False) ```
**Buffer** Unified buffer abstraction over native GStreamer memory. Provides read/write access, timestamp metadata, and zero-copy transfer paths. Key methods: `data()`, `size()`, `resize()`, `pts()`, `dts()`, `duration()`. ```python theme={null} def consume_buffer(buf): data = buf.data() pts = buf.pts() sink.set_buffer_consumer(consume_buffer) ```
**AppSrc** App-facing source. Feeds buffers from application code into the pipeline using lambda callbacks or a push API. ```python theme={null} def produce_buffer(buf): # fill buf.data() return True src = AppSrc(pipeline.get("src")) src.set_buffer_producer(produce_buffer) ```
**AppSink** App-facing sink. Delivers pipeline output buffers to application code with zero-copy. ```python theme={null} def consume_buffer(buf): # consume buf.data() pass sink = AppSink(pipeline.get("sink")) sink.set_buffer_consumer(consume_buffer) ```
**CamSrc** Built-in ISP camera source. Exposes an API for runtime image capture and snapshots. ```python theme={null} cam = CamSrc(pipeline.get("source")) cam.capture_image("snapshot.jpg") ```
**MLPreProcess** Attaches a custom Python callback for pre-processing input frames before ML inference. ```python theme={null} def preprocess(buf): # transform input frame pass pre = MLPreProcess(pipeline.get("preproc")) pre.set_handler(preprocess) ```
**MLPostProcess** Attaches a custom Python callback for post-processing raw inference tensor output. ```python theme={null} def postprocess(buf): # parse inference output pass pp = MLPostProcess(pipeline.get("postproc")) pp.set_handler(postprocess) ```
**MLVideoTFLiteBin** / **MLVideoQNNBin** / **MLVideoSNPEBin** / **MLVideoONNXBin** All-in-one preprocess + inference + postprocess bins. Supports TFLite, QNN, SNPE, and ONNX models. ```python theme={null} pipeline.add("MLVideoTFLiteBin", "ml", "model", "/models/model.tflite", "delegate", "htp") ```
**VideoFilter** / **AudioFilter** / **ImageFilter** / **H264Filter** / **TensorFilter** / **TextFilter** Fluent caps builders for `add_stream_filter()`. Describes stream characteristics for caps negotiation between pipeline stages. ```python theme={null} pipeline.add_stream_filter("vf", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30)) ```
*** ## Build your application Ensure you have QIM SDK installed. See [QIM SDK Installation Guide](../installation) Before running any of the applications below, create the sample directory tree on the target device: ```bash theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{media,models,labels}" ``` This is required even for apps that don't download any media, model, or label files (such as the camera capture and recording apps), since they still write their output under this path. ### USB Camera Source USB (UVC) cameras are handled by the `v4l2src` node. Each USB camera comes with its own format and frame constraints. Because of this, we always place `qtivtransform` right after the USB camera to ensure that the rest of the pipeline receives a hardware-friendly NV12 (Semi-planar YUV420) video format. `qtivtransform` automatically operates in passthrough mode if the USB camera already supports NV12, so you don't need to worry about any performance overhead. Introduction Check application source code on GitHub: [`qimsdk_ref_usb_camera`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_usb_camera.py) Pre-built application on device: `/usr/bin/qimsdk_ref_usb_camera.py` ```python theme={null} """USB camera preview using explicit linking.""" import argparse from qimsdk import Element, Pipeline, VideoFilter # Example pipeline: # # source -> transform -> [videofilter] -> display # # The pipeline reads frames from a USB (V4L2) camera, rotates them, restricts # the stream to NV12/1080p/30fps, and displays the result through Wayland. def create_and_execute_pipeline(device: str = "/dev/video2") -> None: # Captures frames from the USB camera source. source = ( Element("v4l2src", "source") .set("device", device) ) # Applies geometric transforms to video frames. transform = Element("qtivtransform", "transform") # Stream filters used in branch links. # They define specific stream characteristics from the supported options. videofilter = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30) # Render video stream on display. # # sync=false disables strict rendering synchronization to the pipeline clock. # fullscreen=true renders the output fullscreen on the target display. display = ( Element("waylandsink", "display") .set("sync", False) .set("fullscreen", True) ) # Creates the pipeline, adds elements, links them explicitly, and executes it. pipeline = ( Pipeline("usb-cam-pipeline") .add(source) .add(transform) .add_stream_filter("videofilter", videofilter) .add(display) .link("source", "transform", "videofilter", "display") ) pipeline.execute() def main() -> None: from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-d", "--device", default="/dev/video2", help="V4L2 device node to capture from (default: %(default)s)", ) args = parser.parse_args() create_and_execute_pipeline(args.device) if __name__ == "__main__": main() ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} python3 /usr/bin/qimsdk_ref_usb_camera.py --input-config /dev/video2 ``` The live camera feed is rotated and rendered fullscreen on the display in NV12 format at 1080p/30fps. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_usb_camera`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_usb_camera.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. Check application source code on GitHub: [`qimsdk_ref_usb_camera_impl`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_usb_camera_impl.py) Pre-built application on device: `/usr/bin/qimsdk_ref_usb_camera_impl.py` ```python theme={null} """USB camera preview using auto-linking.""" import argparse from qimsdk import Pipeline, VideoFilter # Example pipeline: # # source -> transform -> [videofilter] -> display # # The pipeline reads frames from a USB (V4L2) camera, rotates them, restricts # the stream to NV12/1080p/30fps, and displays the result through Wayland. def create_and_execute_pipeline(device: str = "/dev/video2") -> None: # Creates the pipeline, adds and links elements, and executes it. # # Elements are created on the fly as they are added, and linking is # implicit, following the order in which elements are added. # # sync=false disables strict rendering synchronization to the pipeline clock. # fullscreen=true renders the output fullscreen on the target display. pipeline = ( Pipeline("usb-cam-pipeline") # Captures frames from the USB camera source. .add("v4l2src", "source", "device", device) # Applies geometric transforms to video frames. .add("qtivtransform", "transform") # Stream filter used in the branch link. # It defines specific stream characteristics from the supported options. .add_stream_filter( "videofilter", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30), ) # Render video stream on display. .add("waylandsink", "display", "sync", False, "fullscreen", True) ) pipeline.execute() def main() -> None: from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-d", "--device", default="/dev/video2", help="V4L2 device node to capture from (default: %(default)s)", ) args = parser.parse_args() create_and_execute_pipeline(args.device) if __name__ == "__main__": main() ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} python3 /usr/bin/qimsdk_ref_usb_camera_impl.py --input-config /dev/video2 ``` The live camera feed is rotated and rendered fullscreen on the display in NV12 format at 1080p/30fps. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_usb_camera_impl`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_usb_camera_impl.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. See the full YAML configuration here: [qimsdk\_ref\_usb\_camera.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_usb_camera.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Pre-built application on device: `/usr/bin/qimsdk_ref_yml.py` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_usb_camera.yaml` ```yaml theme={null} pipeline: elements: - type: v4l2src name: source device: /dev/video2 - type: qtivtransform name: transform - type: filter name: videostream video: format: NV12 width: 1920 height: 1080 framerate: 30 - type: waylandsink name: display sync: false fullscreen: true ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} qimsdk_ref_yml.py /etc/qimsdk/qimsdk_ref_usb_camera.yaml ``` The live camera feed is rotated and rendered fullscreen on the display in NV12 format at 1080p/30fps. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Object Detection This pipeline captures from a built-in camera, runs an object detection model through an ML inference bin, renders bounding box overlays on the output frames, and displays the result. The ML bin handles preprocessing, inference, and postprocessing internally. The stream filter pins the input video format expected by the model. The overlay element draws detection results on each frame before display. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_yolov8`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_yolov8.py) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_yolov8.py` | File | Download | Save as | | - | - | - | | YOLOv8 detection model | [Qualcomm AI Hub — YOLOv8](https://aihub.qualcomm.com/iot/models/yolov8_det) | `yolov8_det_quantized.tflite` | | Detection labels | yolov8.json | `yolov8.json` | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. The model and labels must be present on the device before running. Copy them to the paths the application expects: ```bash theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/models ~/Downloads/qimsdk_samples/labels" # Copy the assets to device scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` This application uses the built-in ISP camera. On platforms without an onboard ISP camera (e.g. IQ9), attach an external IMX camera (e.g. from RB3 Gen2) before running the application — see [ISP Camera (Config #2 / qticamsrc)](/advanced/debugging#isp-camera-config-2--qticamsrc) for the procedure to switch from `libcamera` to `qticamsrc`. ```bash theme={null} python3 /usr/bin/qimsdk_ref_camera_yolov8.py ``` The live camera feed is rendered fullscreen on the display with YOLOv8 bounding boxes and class labels overlaid on each detected object in real time. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera_yolov8`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_yolov8.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Post-Processing The QIM SDK implements AI model post-processing through dedicated modules that take tensors as input and produce predictions. All underlying complexity — batching, daisy-chaining, image mask support, and ML metadata handling — is encapsulated within the post-processing plugin. This makes `MLPostProcess` the ideal integration point for application-specific logic, since the application does not need to handle any of that complexity directly. A specialized post-processing module manages the bindings between the pipeline and the application. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_mlbin_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_mlbin_yolov8.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_mlbin_yolov8.py` | File | Download | Save as | | - | - | - | | YOLOv8 detection model | [Qualcomm AI Hub — YOLOv8](https://aihub.qualcomm.com/iot/models/yolov8_det) | `yolov8_det_quantized.tflite` | | Detection labels | yolov8.json | `yolov8.json` | | Sample video | Input video | `ai_demo_sample.mp4` | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. The input video, model, and labels must be present on the device before running. Copy them to the paths the application expects: ```bash theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media ~/Downloads/qimsdk_samples/models ~/Downloads/qimsdk_samples/labels" # Copy the assets to device scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_postprocess_mlbin_yolov8.py ``` The video plays back with YOLOv8 bounding boxes and class labels decoded by the custom postprocessing callback, overlaid and rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_mlbin_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_mlbin_yolov8.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. *** ## Benefits of QIM SDK Python App Builder * Significantly reduced source code * Improved readability * Internal GStreamer state transition handling * Hidden End-of-Stream handling * Automatic error and state event handling * Automatic resource cleanup * Simplified AppSink/AppSrc interaction using lambda functions * SDK-managed buffer mapping and queries * Simplified buffer access anywhere in the pipeline using wrapped GST probes * YAML-based pipeline description # Reference Applications Source: https://imsdkdocs.qualcomm.com/app-builder/python-reference-applications Reference Python App Builder pipeline examples ## Multimedia Pipeline Before running any of the applications below, create the sample directory tree on the target device: ```bash theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{media,models,labels}" ``` This is required even for apps that don't download any media, model, or label files (such as the camera capture and recording apps), since they still write their output under this path. ### USB Camera Preview USB (UVC) cameras are handled by the `v4l2src` node. Each USB camera comes with its own format and frame constraints. Because of this, we always place `qtivcomposer` right after the USB camera to ensure that the rest of the pipeline receives a hardware-friendly NV12 (Semi-planar YUV420) video format. However, `qtivcomposer` will automatically operate in passthrough mode if the USB camera already supports NV12, so you don't need to worry about any performance overhead. Some USB webcams (e.g. Logitech Brio 4K) may render a green/garbled frame at 30fps depending on platform/build. If this happens, lower the pipeline's framerate (e.g. to 15fps) as a workaround. Introduction Check application source code on GitHub: [`qimsdk_ref_usb_camera.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_usb_camera.py) Pre-built application on device: `/usr/bin/qimsdk_ref_usb_camera.py` ```python theme={null} """USB camera preview using explicit linking.""" import argparse from qimsdk import Element, Pipeline, VideoFilter # Example pipeline: # # source -> transform -> [videofilter] -> display # # The pipeline reads frames from a USB (V4L2) camera, rotates them, restricts # the stream to NV12/1080p/30fps, and displays the result through Wayland. def create_and_execute_pipeline(device: str) -> None: # Captures frames from the USB camera source. source = ( Element("v4l2src", "source") .set("device", device) ) # Applies geometric transforms to video frames. transform = Element("qtivtransform", "transform") # Stream filters used in branch links. # They define specific stream characteristics from the supported options. videofilter = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30) # Render video stream on display. # # sync=false disables strict rendering synchronization to the pipeline clock. # fullscreen=true renders the output fullscreen on the target display. display = ( Element("waylandsink", "display") .set("sync", False) .set("fullscreen", True) ) # Creates the pipeline, adds elements, links them explicitly, and executes it. pipeline = ( Pipeline("usb-cam-pipeline") .add(source) .add(transform) .add_stream_filter("videofilter", videofilter) .add(display) .link("source", "transform", "videofilter", "display") ) pipeline.execute() def main() -> None: from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-d", "--device", default="/dev/video2", help="V4L2 device node to capture from (default: %(default)s)", ) args = parser.parse_args() create_and_execute_pipeline(args.device) if __name__ == "__main__": main() ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} /usr/bin/qimsdk_ref_usb_camera.py --input-config /dev/video2 ``` The live USB camera feed is rendered fullscreen on the display, rotated and scaled to 1080p/30fps NV12 as configured by the pipeline's stream filter. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_usb_camera.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_usb_camera.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. Check application source code on GitHub: [`qimsdk_ref_usb_camera_impl.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_usb_camera_impl.py) Pre-built application on device: `/usr/bin/qimsdk_ref_usb_camera_impl.py` ```python theme={null} """USB camera preview using auto-linking.""" import argparse from qimsdk import Pipeline, VideoFilter # Example pipeline: # # source -> transform -> [videofilter] -> display # # The pipeline reads frames from a USB (V4L2) camera, rotates them, restricts # the stream to NV12/1080p/30fps, and displays the result through Wayland. def create_and_execute_pipeline(device: str) -> None: # Creates the pipeline, adds and links elements, and executes it. # # Elements are created on the fly as they are added, and linking is # implicit, following the order in which elements are added. # # sync=false disables strict rendering synchronization to the pipeline clock. # fullscreen=true renders the output fullscreen on the target display. pipeline = ( Pipeline("usb-cam-pipeline") # Captures frames from the USB camera source. .add("v4l2src", "source", "device", device) # Applies geometric transforms to video frames. .add("qtivtransform", "transform") # Stream filter used in the branch link. # It defines specific stream characteristics from the supported options. .add_stream_filter( "videofilter", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30), ) # Render video stream on display. .add("waylandsink", "display", "sync", False, "fullscreen", True) ) pipeline.execute() def main() -> None: from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( "-d", "--device", default="/dev/video2", help="V4L2 device node to capture from (default: %(default)s)", ) args = parser.parse_args() create_and_execute_pipeline(args.device) if __name__ == "__main__": main() ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} /usr/bin/qimsdk_ref_usb_camera_impl.py --input-config /dev/video2 ``` The live USB camera feed is rendered fullscreen on the display, rotated and scaled to 1080p/30fps NV12 as configured by the pipeline's stream filter. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_usb_camera_impl.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_usb_camera_impl.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. Check application source code on GitHub: [`qimsdk_ref_usb_camera.yaml`](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_usb_camera.yaml) Pre-built application on device: `qimsdk_ref_yml.py` ```yaml theme={null} pipeline: elements: - type: v4l2src name: source device: /dev/video2 - type: qtivtransform name: transform - type: filter name: videostream video: format: NV12 width: 1920 height: 1080 framerate: 30 - type: waylandsink name: display sync: false fullscreen: true ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the pipeline's `device` property if your camera is exposed on a different node). ```bash theme={null} qimsdk_ref_yml.py /etc/qimsdk/qimsdk_ref_usb_camera.yaml ``` The live USB camera feed is rendered fullscreen on the display, rotated and scaled to 1080p/30fps NV12 as configured by the YAML pipeline description. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_usb_camera.yaml`](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_usb_camera.yaml) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Built-in Camera Preview This pipeline includes a built-in camera node, a stream filter, and a display node. If the device has more than one built-in camera, you can specify which one to use by providing the camera's unique ID. The stream filter defines the camera's output format, resolution, and framerate. You can also configure the display settings, such as enabling fullscreen rendering. IQ9 has no onboard ISP camera. You can attach an external IMX camera (e.g. from RB3 Gen2) instead — see [ISP Camera (Config #2 / qticamsrc)](/advanced/debugging#isp-camera-config-2--qticamsrc) for the procedure to switch from `libcamera` to `qticamsrc`. Introduction Check application source code on GitHub: [`qimsdk_ref_camera.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera.py) Pre-built application on device: `/usr/bin/qimsdk_ref_camera.py` ```python theme={null} #!/usr/bin/env python3 """Basic camera preview using explicit linking.""" from qimsdk import Element, Pipeline, VideoFilter # Example pipeline: # # source -> [videofilter] -> display # # The pipeline reads camera frames and displays the result through Wayland. def create_and_execute_pipeline() -> None: # Captures frames from the camera source. source = Element("qtiqmmfsrc", "source") # Stream filters used in branch links. # They define specific stream characteristics from the supported options. videofilter = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30) # Render video stream on display. # # async=false enforce state transition to ensure the buffers are returned on time. # sync=false disables strict rendering synchronization to the pipeline clock. # fullscreen=true renders the output fullscreen on the target display. display = ( Element("waylandsink", "display") .set("sync", False) .set("fullscreen", True) ) # Creates the pipeline, adds elements, links them explicitly, and executes it. pipeline = ( Pipeline("cam-pipeline") .add(source) .add_stream_filter("videofilter", videofilter) .add(display) .link("source", "videofilter", "display") ) pipeline.execute() def main() -> None: from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) create_and_execute_pipeline() if __name__ == "__main__": main() ``` Check application source code on GitHub: [`qimsdk_ref_camera_impl.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_impl.py) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_impl.py` ```python theme={null} #!/usr/bin/env python3 """Basic camera preview using auto-linking.""" from qimsdk import Pipeline, VideoFilter # Example pipeline: # # source -> [videofilter] -> display # # The pipeline reads camera frames and displays the result through Wayland. def create_and_execute_pipeline() -> None: # Creates the pipeline, adds and links elements, and executes it. # # Elements are created on the fly as they are added, and linking is # implicit, following the order in which elements are added. # # async=false enforce state transition to ensure the buffers are returned on time. # sync=false disables strict rendering synchronization to the pipeline clock. # fullscreen=true renders the output fullscreen on the target display. pipeline = ( Pipeline("cam-pipeline") # Captures frames from the camera source. .add("qtiqmmfsrc", "source") # Stream filter used in the branch link. # It defines specific stream characteristics from the supported options. .add_stream_filter( "videofilter", VideoFilter().format("NV12").resolution(1920, 1080).framerate(30), ) # Render video stream on display. .add("waylandsink", "display", "sync", False, "fullscreen", True) ) pipeline.execute() def main() -> None: from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) create_and_execute_pipeline() if __name__ == "__main__": main() ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_camera.py ``` The live camera feed is rendered fullscreen on the display at 1080p/30fps NV12 as configured by the pipeline's stream filter. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera.py), [`qimsdk_ref_camera_impl.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_impl.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Built-in Camera Preview with Still Capture The application captures a still image a few seconds after starting and then stops automatically — this is expected behavior, not a hang. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_and_capture.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_and_capture.py) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_and_capture.py` ```python theme={null} #!/usr/bin/env python3 """Camera preview + still image capture using CamSrc and explicit links.""" import time import os from qimsdk import CamSrc, Element, ImageFilter, Pipeline, VideoFilter # Example pipeline: # # source -> [vf] -> display # source -> [if] -> imagesink # # The pipeline reads camera frames, displays them through Wayland, and on # request captures a still image to a JPEG file. def create_and_execute_pipeline() -> None: # Captures frames from the camera source. source = CamSrc("source") # Restricts the preview stream to NV12/1080p/30fps before display. vf = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30) # Render video stream on display. display = ( Element("waylandsink", "display") .set("sync", False) .set("async", False) .set("fullscreen", True) ) # Restricts the still capture stream to a 4K JPEG format. imagefilter = ImageFilter().format("JPEG").resolution(3840, 2160) # Writes output buffers to files. imagesink = ( Element("multifilesink", "imagesink") .set("enable-last-sample", False) .set("location", f"{os.environ['HOME']}/Downloads/qimsdk_samples/media/image_%d.jpeg") ) # Creates the pipeline, adds and links elements, and executes it. # # Explicit linking is applied. pipeline = ( Pipeline("cam-capture-pipeline") .add(source) .add_stream_filter("vf", vf) .add(display) .add_stream_filter("if", imagefilter) .add(imagesink) .link("source", "vf", "display") .link("source", "if", "imagesink") ) pipeline.start() time.sleep(2) # image capture trigger cam = pipeline.get("source", CamSrc) cam.image_capture() time.sleep(2) pipeline.stop().wait() def main() -> None: if "HOME" not in os.environ: raise EnvironmentError("Error: HOME environment variable is not set.") from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) create_and_execute_pipeline() if __name__ == "__main__": main() ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_camera_and_capture.py ``` The application displays a live preview and captures a still JPEG image a few seconds after starting, saving it under `~/media/image_%d.jpeg`. The live camera preview is rendered briefly on the display, and a still JPEG image is saved to `~/Downloads/qimsdk_samples/media/image_%d.jpeg`. The application then exits automatically. * **Source code:** [`qimsdk_ref_camera_and_capture.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_and_capture.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Camera Recording to MP4 This pipeline captures frames from the built-in camera, encodes them into an H.264 stream using the hardware encoder, parses the bitstream, muxes it into an MP4 container, and writes the resulting file to disk. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_encoder.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_encoder.py) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_encoder.py` ```python theme={null} #!/usr/bin/env python3 """Camera preview + still image capture using CamSrc and explicit links.""" import time import os from qimsdk import CamSrc, Element, Pipeline, VideoFilter # Example pipeline: # # source -> [vf] -> encoder -> parser -> muxer -> sink # # The pipeline reads camera frames, encodes them with H.264, muxes into MP4, # and writes the output to disk. def create_and_execute_pipeline() -> None: # Captures frames from the camera source. source = CamSrc("source") # Stream filters used in branch links. # They define specific stream characteristics from the supported options. vf = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30) # Encodes raw video frames into H.264 stream. encoder = ( Element("v4l2h264enc", "encoder") .set("output-io-mode", "dmabuf-import") .set("capture-io-mode", "dmabuf") ) # Parses H.264 bitstream for downstream muxing. parser = Element("h264parse", "parser") # Muxes encoded stream into MP4 container. muxer = Element("mp4mux", "muxer") # Writes output stream to a file. sink = ( Element("filesink", "sink") .set("location", f"{os.environ['HOME']}/Downloads/qimsdk_samples/media/encoder_output.mp4") ) # Creates the pipeline, adds and links elements, and executes it. # # Explicit linking is applied. pipeline = ( Pipeline("cam-encoder-pipeline") .add(source) .add_stream_filter("vf", vf) .add(encoder) .add(parser) .add(muxer) .add(sink) ) pipeline.eos(True).execute() def main() -> None: if "HOME" not in os.environ: raise EnvironmentError("Error: HOME environment variable is not set.") from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) create_and_execute_pipeline() if __name__ == "__main__": main() ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_camera_encoder.py ``` The application records camera frames encoded as H.264 into an MP4 file saved to `~/Downloads/qimsdk_samples/media/encoder_output.mp4`. The application records silently with no display output. An MP4 file is written to `~/Downloads/qimsdk_samples/media/encoder_output.mp4`, which you can copy off the device and play back to verify the recording. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera_encoder.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_encoder.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### MP4 File Playback This pipeline ingests an MP4 file, demultiplexes it to extract the video track, decodes the compressed stream (e.g., H.264/H.265) into raw frames, converts (or directly negotiates) the output to NV12 — a hardware-friendly YUV 4:2:0 semi-planar format — and then renders the frames to the display using a sink that supports zero-copy or hardware buffers. Introduction Check application source code on GitHub: [`qimsdk_ref_qtdemux_decode_display.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_qtdemux_decode_display.py) Pre-built application on device: `/usr/bin/qimsdk_ref_qtdemux_decode_display.py` ```python theme={null} #!/usr/bin/env python3 """Transport stream demux sample that exercises H264Filter.""" from qimsdk import Element, Pipeline, VideoFilter import os # Example pipeline: # # src -> demux -> parse -> decoder -> [videofilter] -> display # # The pipeline reads an MP4/H.264 file, decodes it through the hardware decoder, # and displays the result through Wayland. def create_and_execute_pipeline() -> None: # Reads the input media file as raw bytes. src = ( Element("filesrc", "src") .set("location", f"{os.environ['HOME']}/Downloads/qimsdk_samples/media/ai_demo_sample.mp4") ) # Extracts elementary streams from the MP4 container. demux = Element("qtdemux", "demux") # Prepares the H.264 bitstream for the decoder. parse = Element("h264parse", "parse") # Decodes the compressed H.264 stream into raw video frames. # # The I/O mode is configured to enforce DMA buffer usage, # avoiding unnecessary buffer copies. decoder = ( Element("v4l2h264dec", "decoder") .set("output-io-mode", 4) .set("capture-io-mode", 4) ) # Restricts the decoded stream to NV12 before it reaches the display. videofilter = VideoFilter().format("NV12") # Render video stream on display. display = ( Element("waylandsink", "display") .set("fullscreen", True) ) # Creates the pipeline, adds and links elements, and executes it. # # Linking is implicit and follows the order in which elements are added. pipeline = ( Pipeline("video-pipeline") .add(src) .add(demux) .add(parse) .add(decoder) .add_stream_filter("videofilter", videofilter) .add(display) ) pipeline.execute() def main() -> None: if "HOME" not in os.environ: raise EnvironmentError("Error: HOME environment variable is not set.") from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) create_and_execute_pipeline() if __name__ == "__main__": main() ``` | File | Download | Save as | | - | - | - | | Sample video | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | `ai_demo_sample.mp4` | You can also use your own MP4 (H.264) file, or update the pipeline's `location` property to point to it. ```bash theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media" scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_qtdemux_decode_display.py ``` The application decodes the video file and renders it fullscreen on the display. The MP4 file's video track is decoded and rendered fullscreen on the display until the end of the file is reached. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_qtdemux_decode_display.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_qtdemux_decode_display.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Transport Stream (.ts) File Playback This pipeline is similar to the Offline Video Source pipeline, but ingests an MPEG transport stream (`.ts`) file instead of an MP4 container. It demultiplexes the transport stream using `tsdemux` to extract the video track, decodes the compressed H.264 stream into raw frames, converts (or directly negotiates) the output to NV12, and renders the frames to the display. Introduction Check application source code on GitHub: [`qimsdk_ref_tsdemux_decode_display.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_tsdemux_decode_display.py) Pre-built application on device: `/usr/bin/qimsdk_ref_tsdemux_decode_display.py` ```python theme={null} #!/usr/bin/env python3 """Transport stream demux sample that exercises H264Filter.""" from qimsdk import Element, H264Filter, Pipeline, VideoFilter import os # Example pipeline: # # src -> demux -> [vf1] -> parse -> decoder -> [vf2] -> display # # The pipeline reads a sequence of transport-stream files, extracts the H.264 # elementary stream, decodes it through the hardware decoder, and displays # the result through Wayland. def create_and_execute_pipeline() -> None: # Reads a sequence of input files as stream data. src = ( Element("multifilesrc", "src") .set("location", f"{os.environ['HOME']}/Downloads/qimsdk_samples/media/ai_demo_sample.ts") .set("stop-index", 0) ) # Extracts elementary streams from the transport stream. demux = Element("tsdemux", "demux") # Restricts the extracted stream to a fixed framerate before parsing. vf1 = H264Filter().framerate(30) # Prepares the H.264 bitstream for the decoder. parse = Element("h264parse", "parse") # Decodes the compressed H.264 stream into raw video frames. # # The I/O mode is configured to enforce DMA buffer usage, # avoiding unnecessary buffer copies. decoder = ( Element("v4l2h264dec", "decoder") .set("output-io-mode", 4) .set("capture-io-mode", 4) ) # Restricts the decoded stream to NV12 before it reaches the display. vf2 = VideoFilter().format("NV12") # Render video stream on display. display = ( Element("waylandsink", "display") .set("fullscreen", True) ) # Creates the pipeline, adds and links elements, and executes it. # # Linking is implicit and follows the order in which elements are added. pipeline = ( Pipeline("video-pipeline") .add(src) .add(demux) .add_stream_filter("vf1", vf1) .add(parse) .add(decoder) .add_stream_filter("vf2", vf2) .add(display) ) pipeline.execute() def main() -> None: if "HOME" not in os.environ: raise EnvironmentError("Error: HOME environment variable is not set.") from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) create_and_execute_pipeline() if __name__ == "__main__": main() ``` | File | Download | Save as | | - | - | - | | Sample transport stream | [ai\_demo\_sample.ts](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/raw/refs/heads/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.ts) | `ai_demo_sample.ts` | ```bash theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media" scp ai_demo_sample.ts @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_tsdemux_decode_display.py ``` The application decodes the transport stream file and renders it fullscreen on the display. The transport stream file's video track is decoded and rendered fullscreen on the display until the end of the file is reached. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_tsdemux_decode_display.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_tsdemux_decode_display.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Synthetic Test Pattern (VideoFilter API) This pipeline generates synthetic test video frames using `videotestsrc` (a bouncing ball pattern), instead of reading from a camera or file. The frames are restricted to NV12/1080p/30fps, rotated 180 degrees by [`qtivtransform`](/plugin-reference/qtivtransform), downscaled to 1280x720 by a second stream filter, and rendered to the display. This is a useful pipeline for testing display and transform stages without any camera or media file dependency. Introduction Check application source code on GitHub: [`qimsdk_ref_videotestsrc_videofilter_display.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_videotestsrc_videofilter_display.py) Pre-built application on device: `/usr/bin/qimsdk_ref_videotestsrc_videofilter_display.py` ```python theme={null} #!/usr/bin/env python3 """Test video test source with stream filter.""" from qimsdk import Element, Pipeline, VideoFilter # Example pipeline: # # src -> [vf1] -> transform -> [vf2] -> display # # The pipeline generates synthetic test video frames, restricts them to NV12 # at 1920x1080/30fps, rotates the frames, restricts the output to 1280x720, # and displays the result through Wayland. def create_and_execute_pipeline() -> None: # Generates synthetic test video frames. src = ( Element("videotestsrc", "src") .set("pattern", "ball") ) # Restricts the generated stream to NV12 at 1920x1080, 30fps. vf1 = VideoFilter().format("NV12").resolution(1920, 1080).framerate(30) # Applies geometric transforms to video frames. transform = ( Element("qtivtransform", "transform") .set("rotate", "180") ) # Restricts the transformed stream to 1280x720. vf2 = VideoFilter().resolution(1280, 720) # Render video stream on display. display = ( Element("waylandsink", "display") .set("fullscreen", True) ) # Creates the pipeline, adds and links elements, and executes it. # # Linking is implicit and follows the order in which elements are added. pipeline = ( Pipeline("video-pipeline") .add(src) .add_stream_filter("vf1", vf1) .add(transform) .add_stream_filter("vf2", vf2) .add(display) ) pipeline.execute() def main() -> None: from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) create_and_execute_pipeline() if __name__ == "__main__": main() ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_videotestsrc_videofilter_display.py ``` The application generates a synthetic bouncing-ball test pattern, rotates and rescales it, and renders it fullscreen on the display. A synthetic bouncing-ball test pattern is rendered fullscreen on the display, rotated 180 degrees and rescaled to 1280x720 — with no camera or media file required. * **Source code:** [`qimsdk_ref_videotestsrc_videofilter_display.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_videotestsrc_videofilter_display.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Synthetic Test Pattern (Raw Caps StreamFilter) This pipeline is functionally identical to the previous example, but it defines its stream constraints using raw GStreamer caps strings via `StreamFilter` (e.g. `"video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1"`), instead of the structured `VideoFilter` builder API. This is useful when you need to express filter capabilities that aren't covered by the structured builder, or when porting caps strings directly from existing GStreamer pipelines. Introduction Check application source code on GitHub: [`qimsdk_ref_videotestsrc_streamfilter_display.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_videotestsrc_streamfilter_display.py) Pre-built application on device: `/usr/bin/qimsdk_ref_videotestsrc_streamfilter_display.py` ```python theme={null} #!/usr/bin/env python3 """Test video test source with stream filter.""" from qimsdk import Element, Pipeline, StreamFilter # Example pipeline: # # src -> [vf1] -> transform -> [vf2] -> display # # The pipeline generates synthetic test video frames, applies a caps-string-based # stream filter, rotates the frames, applies a second caps-string-based stream # filter, and displays the result through Wayland. def create_and_execute_pipeline() -> None: # Generates synthetic test video frames. src = ( Element("videotestsrc", "src") .set("pattern", "ball") ) # Restricts the generated stream to NV12 at 1920x1080, 30fps. vf1 = StreamFilter("video/x-raw,format=NV12,width=1920,height=1080,framerate=30/1") # Applies geometric transforms to video frames. transform = ( Element("qtivtransform", "transform") .set("rotate", "180") ) # Restricts the transformed stream to 1280x720. vf2 = StreamFilter("video/x-raw,width=1280,height=720") # Render video stream on display. display = ( Element("waylandsink", "display") .set("fullscreen", True) ) # Creates the pipeline, adds and links elements, and executes it. # # Linking is implicit and follows the order in which elements are added. pipeline = ( Pipeline("video-pipeline-streamfilter-string") .add(src) .add_stream_filter("vf1", vf1) .add(transform) .add_stream_filter("vf2", vf2) .add(display) ) pipeline.execute() def main() -> None: from qimsdk import ImsdkGstLogMode, ImsdkLogLevel, SetImsdkGstLogMode, SetImsdkLogLevel SetImsdkGstLogMode(ImsdkGstLogMode.ImsdkLog) SetImsdkLogLevel(ImsdkLogLevel.Debug) create_and_execute_pipeline() if __name__ == "__main__": main() ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_videotestsrc_streamfilter_display.py ``` The application generates a synthetic bouncing-ball test pattern, rotates and rescales it, and renders it fullscreen on the display. A synthetic bouncing-ball test pattern is rendered fullscreen on the display, rotated 180 degrees and rescaled to 1280x720, identical in appearance to the `VideoFilter` API example above. * **Source code:** [`qimsdk_ref_videotestsrc_streamfilter_display.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_videotestsrc_streamfilter_display.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### RTSP Camera Stream Playback This pipeline connects to a network RTSP camera stream instead of a local (USB/built-in) camera or file. `rtspsrc` establishes the RTSP session and negotiates the H.264 RTP stream, a caps filter restricts it to H.264 video, `rtph264depay` extracts the H.264 payload from the RTP packets, `h264parse` prepares the bitstream for the hardware decoder, and the decoded frames are converted to NV12 and rendered to the display. Introduction See the full YAML configuration here: [qimsdk\_ref\_rtsp\_camera.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_rtsp_camera.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Pre-built application on device: `qimsdk_ref_yml.py` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_rtsp_camera.yaml` ```yaml theme={null} pipeline: elements: - type: rtspsrc name: source location: rtsp://username:password@192.168.1.188:554/Streaming/Channels/101 latency: 0 - type: capsfilter name: rtp_caps caps: application/x-rtp,media=video,encoding-name=H264 - type: rtph264depay name: depay - type: h264parse name: parse - type: v4l2h264dec name: decoder capture-io-mode: 4 output-io-mode: 4 - type: filter name: videostream video: format: NV12 - type: waylandsink name: display sync: false fullscreen: true ``` On the device, edit `/etc/qimsdk/qimsdk_ref_rtsp_camera.yaml` and update the `location` property with your RTSP camera's username, password, IP address, and stream path. ```bash theme={null} qimsdk_ref_yml.py /etc/qimsdk/qimsdk_ref_rtsp_camera.yaml ``` The application connects to the RTSP camera, decodes the H.264 stream, and renders it fullscreen on the display. The RTSP camera's live H.264 stream is decoded and rendered fullscreen on the display, for as long as the network stream remains available. To stop the application, press **CTRL + C**. * **Source code:** [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ## AI Pipeline ### Video-to-Tensor Dump (Offline Preprocessing) This pipeline ingests an MP4 file, demultiplexes it to extract the video track, decodes the compressed stream (e.g., H.264/H.265) into raw frames, converts (or directly negotiates) the output to NV12 — a hardware-friendly YUV 4:2:0 semi-planar format — and then goes through [`qtimlvconverter`](/plugin-reference/qtimlvconverter) (ML preprocessing). The `TensorFilter` stream filter describes the tensor format and shape as UINT8 with dimensions \[1, 520, 520, 3] (batch, height, width, channels). Finally, `multifilesink` writes the preprocessed tensors out as sequential `.rgb` files to `~/Downloads/qimsdk_samples/media/tensor_520_520_%d.rgb`, rolling after every 10 files. Introduction Check application source code on GitHub: [`qimsdk_ref_tensor_dump.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_tensor_dump.py) Pre-built application on device: `/usr/bin/qimsdk_ref_tensor_dump.py` | File | Download | Save as | | - | - | - | | Sample video | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | `ai_demo_sample.mp4` | You can also use your own MP4 (H.264) file, or update the pipeline's `location` property to point to it. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/media" scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_tensor_dump.py ``` The application writes preprocessed tensors to `~/Downloads/qimsdk_samples/media/tensor_520_520_%d.rgb`. There is no display output. Sequential `.rgb` tensor files are written to `~/Downloads/qimsdk_samples/media/`, rolling after every 10 files, which you can inspect offline for use in other ML pipelines. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_tensor_dump.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_tensor_dump.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Live Camera YOLOv8 Detection (Manual Chain) This pipeline captures live video from the on-device camera, then drives a full detection-and-visualization loop in real time: the raw NV12 stream is normalized and preprocessed into one or more tensors, fed to a TFLite inference stage (running on the external QNN delegate for efficient hardware execution), and the resulting tensors are postprocessed into semantic detections (classes, boxes, confidences). Those results are converted into QIM SDK-friendly metadata and attached back to the original video timeline, so the visual stream and its ML annotations stay synchronized. A parallel branch from the early tee ensures the original frames are available to a metadata muxer, which merges the detections with the video. Finally, the overlay stage renders the boxes/labels directly on the live frames, and the composed output is presented fullscreen to the display. In short, the graph turns a live camera feed into actionable on-screen insights by (1) creating a tensor view of the stream, (2) running detection, (3) translating inference outputs into QIM SDK metadata, (4) fusing metadata with the original frames, and (5) drawing the results on top of the video for low-latency, on-screen visualization. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_yolov8.py) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_yolov8.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_camera_yolov8.py ``` The application overlays detected bounding boxes and labels on the live camera feed and renders it fullscreen on the display. The live camera feed is rendered fullscreen on the display with YOLOv8 detection bounding boxes and class labels overlaid on each frame in real time. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_yolov8.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. See the full YAML configuration here: [qimsdk\_ref\_camera\_yolov8.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_camera_yolov8.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Pre-built application on device: `qimsdk_ref_yml.py` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_camera_yolov8.yaml` ```yaml theme={null} pipeline: elements: - type: qtiqmmfsrc name: source camera: 0 - type: filter name: videostream video: format: NV12 width: 1920 height: 1080 framerate: 30 - type: tee name: split - type: queue name: q1 - type: qtimlvconverter name: preprocessing - type: queue name: q3 - type: qtimltflite name: inferencing delegate: external external-delegate-path: libQnnTFLiteDelegate.so external-delegate-options: "QNNExternalDelegate,backend_type=htp;" model: ~/Downloads/qimsdk_samples/models/yolov8_det_quantized.tflite - type: queue name: q4 - type: qtimlpostprocess name: postprocessing results: 5 module: yolov8 labels: ~/Downloads/qimsdk_samples/labels/yolov8.json settings: '{"confidence": 70.0}' - type: filter name: mlf text: {} - type: qtimetamux name: mlmuxer - type: queue name: q5 - type: qtivoverlay name: overlay - type: waylandsink name: display sync: false fullscreen: true links: - [split, mlmuxer] - [source, videostream, split, q1, preprocessing, q3, inferencing, q4, postprocessing, mlf, mlmuxer, q5, overlay, display] ``` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} qimsdk_ref_yml.py /etc/qimsdk/qimsdk_ref_camera_yolov8.yaml ``` The live camera feed is rendered fullscreen on the display with YOLOv8 detection bounding boxes and class labels overlaid on each frame in real time, identical to the explicit coding style example above. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Live Camera YOLOv8 Detection (ML Bin) This solution is identical to the previous one. The only difference is that here we use `qtimlvideotflitebin` instead of manually linking the preprocessing, inference, postprocessing, and ML muxer components, which simplifies the pipeline. Introduction Check application source code on GitHub: [`qimsdk_ref_camera_yolov8_mlbin.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_yolov8_mlbin.py) Pre-built application on device: `/usr/bin/qimsdk_ref_camera_yolov8_mlbin.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_camera_yolov8_mlbin.py ``` The application overlays detected bounding boxes and labels on the live camera feed and renders it fullscreen on the display. The live camera feed is rendered fullscreen on the display with YOLOv8 detection bounding boxes and class labels overlaid on each frame in real time. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_camera_yolov8_mlbin.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_camera_yolov8_mlbin.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. See the full YAML configuration here: [qimsdk\_ref\_mlbin\_yolov8.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_mlbin_yolov8.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Pre-built application on device: `qimsdk_ref_yml.py` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_mlbin_yolov8.yaml` ```yaml theme={null} pipeline: elements: - type: filesrc name: src location: ~/Downloads/qimsdk_samples/media/ai_demo_sample.mp4 - type: qtdemux name: demux - type: h264parse name: parse - type: v4l2h264dec name: decoder output-io-mode: 4 capture-io-mode: 4 - type: filter name: videofilter video: format: NV12 - type: qtimlvideotflitebin name: mlbin inference-delegate: "external" inference-external-delegate-path: "libQnnTFLiteDelegate.so" inference-external-delegate-options: "QNNExternalDelegate,backend_type=htp;" inference-model: "~/Downloads/qimsdk_samples/models/yolov8_det_quantized.tflite" postprocess-module: "yolov8" postprocess-labels: "~/Downloads/qimsdk_samples/labels/yolov8.json" - type: qtivoverlay name: overlay - type: waylandsink name: display fullscreen: true ``` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} qimsdk_ref_yml.py /etc/qimsdk/qimsdk_ref_mlbin_yolov8.yaml ``` The application overlays detected bounding boxes and labels on the live camera feed and renders it fullscreen on the display. The live camera feed is rendered fullscreen on the display with YOLOv8 detection bounding boxes and class labels overlaid on each frame in real time, produced by the all-in-one ML bin instead of a manually chained pipeline. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Offline Video YOLOv8 Detection (Manual Chain, Metamux Overlay) This pipeline is the offline-video counterpart to Live Camera YOLOv8 Detection (Manual Chain): instead of a live camera feed, an MP4 file is demuxed and decoded, then split by a `tee` into a display branch and an ML branch. The ML branch runs preprocessing, inference, and postprocessing manually, and the resulting detections are attached back onto the original frame via [`qtimetamux`](/plugin-reference/qtimetamux) before being overlaid and rendered. This Metamux-based approach is more flexible than the video composer approach (it allows chaining additional ML models or streaming metadata separately), whereas the composer overlay approach used in the next example offers better performance. Introduction Check application source code on GitHub: [`qimsdk_ref_yolov8_metamux_overlay.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yolov8_metamux_overlay.py) Pre-built application on device: `/usr/bin/qimsdk_ref_yolov8_metamux_overlay.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_yolov8_metamux_overlay.py ``` The application decodes the video file, runs YOLOv8 detection, attaches the detections to the original frame via [`qtimetamux`](/plugin-reference/qtimetamux), and overlays the results, rendering them fullscreen. The video plays back with YOLOv8 bounding boxes and class labels overlaid on each detected object, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_yolov8_metamux_overlay.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yolov8_metamux_overlay.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Offline Video YOLOv8 Detection (ML Bin, Composer Overlay) The AI output can be overlaid directly on top of the frame instead of attaching AI metadata to the main frame. This approach provides better performance, whereas the Metamux solution is more flexible and can be used for chaining AI models or streaming AI metadata separately. Introduction Check application source code on GitHub: [`qimsdk_ref_yolov8_composer_overlay.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yolov8_composer_overlay.py) Pre-built application on device: `/usr/bin/qimsdk_ref_yolov8_composer_overlay.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_yolov8_composer_overlay.py ``` The application composes the AI detection overlay directly on top of the decoded video frames and displays it fullscreen. The video plays back with YOLOv8 bounding boxes and class labels composited directly onto each frame, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_yolov8_composer_overlay.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yolov8_composer_overlay.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Gesture Recognition This pipeline demonstrates a daisy-chained, multi-model gesture recognition graph built entirely from standard SDK elements ([`qtimlvconverter`](/plugin-reference/qtimlvconverter), [`qtimltflite`](/plugin-reference/qtimltflite), [`qtimlpostprocess`](/plugin-reference/qtimlpostprocess), [`qtimetamux`](/plugin-reference/qtimetamux), `qtimetatransform`) rather than a custom postprocessing callback. Palm detection locates the hand region in the live camera frame; that region is transformed and fed into hand landmark detection; the resulting landmarks are passed through a gesture embedder followed by a canned gesture classifier to decode the recognized gesture. Two parallel [`qtimetamux`](/plugin-reference/qtimetamux) merge points combine metadata from each stage back onto the original frame before the final overlay is rendered to the display. Introduction Check application source code on GitHub: [`qimsdk_ref_gesture_recognition.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_gesture_recognition.py) Pre-built application on device: `/usr/bin/qimsdk_ref_gesture_recognition.py` See the full YAML configuration here: [qimsdk\_ref\_gesture\_recognition.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_gesture_recognition.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Pre-built application on device: `qimsdk_ref_yml.py` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_gesture_recognition.yaml` #### Download Required Files Download the gesture recognizer task bundle from Google MediaPipe to obtain the palm detection, hand landmark, gesture embedder, and canned gesture classifier models: ```bash theme={null} # Download the gesture recognizer task bundle wget https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/latest/gesture_recognizer.task # Extract the top-level task unzip gesture_recognizer.task # Extract hand landmarker models unzip hand_landmarker.task # → hand_detector.tflite, hand_landmarks_detector.tflite # Extract gesture recognizer models unzip hand_gesture_recognizer.task # → gesture_embedder.tflite, canned_gesture_classifier.tflite ``` | File | Save as | | - | - | | hand\_detector.tflite (see steps above) | palm\_detection\_full.tflite | | hand\_landmarks\_detector.tflite (see steps above) | hand\_landmark\_full.tflite | | gesture\_embedder.tflite (see steps above) | gesture\_embedder.tflite | | canned\_gesture\_classifier.tflite (see steps above) | canned\_gesture\_classifier.tflite | | palmd\_labels.json | palmd\_labels.json | | palmd\_settings.json | palmd\_settings.json | | hlandmarks.json | hlandmarks.json | | hlandmark\_settings.json | hlandmark\_settings.json | | gesture\_rec.json | gesture\_rec.json | If a downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` This pipeline's source is the on-device camera, so no video file needs to be copied. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp palm_detection_full.tflite @:~/Downloads/qimsdk_samples/models/ scp hand_landmark_full.tflite @:~/Downloads/qimsdk_samples/models/ scp gesture_embedder.tflite @:~/Downloads/qimsdk_samples/models/ scp canned_gesture_classifier.tflite @:~/Downloads/qimsdk_samples/models/ scp palmd_labels.json @:~/Downloads/qimsdk_samples/labels/ scp palmd_settings.json @:~/Downloads/qimsdk_samples/labels/ scp hlandmarks.json @:~/Downloads/qimsdk_samples/labels/ scp hlandmark_settings.json @:~/Downloads/qimsdk_samples/labels/ scp gesture_rec.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_gesture_recognition.py ``` The application captures live video from the on-device camera, runs the daisy-chained palm detection → hand landmark → gesture embedder → canned gesture classifier models, and overlays the recognized gesture on the display fullscreen. The live camera feed is rendered fullscreen on the display with the recognized gesture label overlaid on each frame in real time. * **Source code:** [`qimsdk_ref_gesture_recognition.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_gesture_recognition.py), [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### PPE & Person/Foot Detection (ML Bin) This pipeline demonstrates a two-model ML bin pipeline for personal protective equipment (PPE) and person/foot detection: `foot_track_net` detects persons and their feet in the frame, while `gear_guard_net` detects PPE gear. Both models run through `qtimlvideotflitebin`, which internally handles preprocessing, inference, and postprocessing, simplifying the pipeline compared to manually chaining each stage. Introduction Check application source code on GitHub: [`qimsdk_ref_mlbin_ppe.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_mlbin_ppe.py) Pre-built application on device: `/usr/bin/qimsdk_ref_mlbin_ppe.py` See the full YAML configuration here: [qimsdk\_ref\_mlbin\_ppe.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_mlbin_ppe.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Pre-built application on device: `qimsdk_ref_yml.py` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_mlbin_ppe.yaml` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/models/gear_guard_net?searchTerm=gear) | gear\_guard\_net-ppe-detection-w8a8.tflite | | [Model](https://aihub.qualcomm.com/models/foot_track_net?searchTerm=foot) | foot\_track\_net-person-foot-detection-w8a8.tflite | | gear\_guard\_net.json | gear\_guard\_net.json | | foot\_track\_net.json | foot\_track\_net.json | | foot\_track\_net\_settings.json | foot\_track\_net\_settings.json | | [ppe\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/refs/heads/main/qualcomm-linux/artifacts/videos/demo_samples/ppe_sample.mp4) | ppe\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp gear_guard_net-ppe-detection-w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp foot_track_net-person-foot-detection-w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp gear_guard_net.json @:~/Downloads/qimsdk_samples/labels/ scp foot_track_net.json @:~/Downloads/qimsdk_samples/labels/ scp foot_track_net_settings.json @:~/Downloads/qimsdk_samples/labels/ scp ppe_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_mlbin_ppe.py ``` The application runs inference on the video file, detecting persons/feet with `foot_track_net` and PPE gear with `gear_guard_net`, overlaying the resulting detections on the video and rendering it fullscreen. The video plays back with person/foot and PPE-gear detection bounding boxes overlaid, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_mlbin_ppe.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_mlbin_ppe.py), [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### YOLOv8 Detection (ML Bin) This pipeline reads an MP4/H.264 file, decodes it through the hardware decoder, and runs YOLOv8 object detection using `qtimlvideotflitebin`, which internally handles preprocessing, inference, and postprocessing. The detected objects are then overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_mlbin_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_mlbin_yolov8.py) Pre-built application on device: `/usr/bin/qimsdk_ref_mlbin_yolov8.py` See the full YAML configuration here: [qimsdk\_ref\_mlbin\_yolov8.yaml](https://github.com/qualcomm/qimsdk/blob/main/tools/appbuilders-configs/qimsdk_ref_mlbin_yolov8.yaml) Check application source code on GitHub: [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Pre-built application on device: `qimsdk_ref_yml.py` YAML configuration saved on device: `/etc/qimsdk/qimsdk_ref_mlbin_yolov8.yaml` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_mlbin_yolov8.py ``` The application decodes the video file, runs YOLOv8 detection through `qtimlvideotflitebin`, and overlays the detected objects on the video, rendering it fullscreen. The video plays back with YOLOv8 bounding boxes and class labels overlaid on each detected object, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_mlbin_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_mlbin_yolov8.py), [qimsdk\_ref\_yml.py](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_yml.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ## AppSink and AppSrc Introduction AppSink and AppSrc are special elements because they expose buffers directly to and from the application. Because of this, we create and configure these elements separately, using their dedicated wrapper classes. Once configured, we insert them into the pipeline as fully constructed element objects. Check application source code on GitHub: [`qimsdk_ref_appsrc_and_appsink.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_appsrc_and_appsink.py) Pre-built application on device: `/usr/bin/qimsdk_ref_appsrc_and_appsink.py` ```bash theme={null} python3 /usr/bin/qimsdk_ref_appsrc_and_appsink.py ``` The application generates synthetic test frames, pushes them through an `AppSink`/`AppSrc` bridge, and renders the resulting stream fullscreen on the display. The synthetic test frames pushed through the `AppSink`/`AppSrc` bridge are rendered fullscreen on the display, confirming the buffers passed through application code unmodified. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_appsrc_and_appsink.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_appsrc_and_appsink.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ## Custom Pre and Post Processing Introduction ### Custom Preprocessing for YOLOv8 (Detection) QIM SDK plugin support different backend: GLES, OpenCV, etc. This level of interface could be exposed to application. This API takes all input and output at once in case of batching or daisy for example. It can be simplified by calling application implementation for every input. Advantage of this approach is that all complexity of batching, depth, daisy chain support remains hidden in the plugin and application will do only pre-processing on a single frame/tensor at once. Introduction Check application source code on GitHub: [`qimsdk_ref_external_preprocess_detection_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_preprocess_detection_yolov8.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_preprocess_detection_yolov8.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_preprocess_detection_yolov8.py ``` The video plays back with YOLOv8 bounding boxes and class labels overlaid on each detected object, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_preprocess_detection_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_preprocess_detection_yolov8.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Preprocessing for YOLOv8 (ML Bin variant) This example demonstrates external preprocessing support inside `MLVideoTFLiteBin`. Registering a callback via `MLVideoTFLiteBin.set_preprocess_handler(...)` disables the bin's internal preprocessing path (the SDK sets `preprocess-engine=none` on the bin's internal `mlpreprocess` element automatically) and hands frame-to-tensor conversion off to an application-defined Python callback. The callback receives the decoded NV12 blit(s) as NumPy-backed buffers and writes directly into the bin's output tensor (a writable, in-place NumPy view), converting each pixel to the quantized int8 NHWC layout expected by the YOLOv8 model, with letterbox padding for any area the blit doesn't cover. Inference and postprocessing then run inside the same bin, and the detected objects are overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_external_preprocess_detection_mlbin_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_preprocess_detection_mlbin_yolov8.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_preprocess_detection_mlbin_yolov8.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_preprocess_detection_mlbin_yolov8.py ``` The application decodes the video file, runs the custom, application-defined Python preprocessing callback inside `mlbin` to convert NV12 frames into the quantized int8 tensor expected by YOLOv8, then runs inference and postprocessing, overlaying the detected objects and rendering the result fullscreen. The video plays back with YOLOv8 bounding boxes and class labels overlaid on each detected object, rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_preprocess_detection_mlbin_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_preprocess_detection_mlbin_yolov8.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Preprocessing with Lightweight Face Detection This example demonstrates external preprocessing with [`MLVConverter`](/plugin-reference/qtimlvconverter) (rather than `MLVideoTFLiteBin`), so the pipeline links preprocessing, inference, and postprocessing manually through explicit queues, and merges the detections back onto the original frame via [`qtimetamux`](/plugin-reference/qtimetamux) before overlaying. Setting `engine` to `"none"` on the [`MLVConverter`](/plugin-reference/qtimlvconverter) element disables its internal preprocessing path and hands frame-to-tensor conversion off to an application-defined callback registered via [`MLVConverter.set_handler(...)`](/plugin-reference/qtimlvconverter). Unlike the YOLOv8 example, the `face_det_lite_w8a8.tflite` model consumes raw uint8 grayscale, so the callback resizes only the NV12 luma (Y) plane into a single-channel `[1, H, W, 1]` NumPy tensor, skipping color conversion and normalization entirely and leaving the chroma plane unread. A `tee` right after decoding splits the stream into a display branch and an ML branch, and a queue on each branch (including the display one) keeps the two branches from stalling each other on a shared thread. Introduction Check application source code on GitHub: [`qimsdk_ref_external_preprocess_lightweight_face_detect.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_preprocess_lightweight_face_detect.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_preprocess_lightweight_face_detect.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/face_det_lite) | face\_det\_lite\_w8a8.tflite | | qfd-labels.json | qfd-labels.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | face\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp face_det_lite_w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp qfd-labels.json @:~/Downloads/qimsdk_samples/labels/ scp face_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_preprocess_lightweight_face_detect.py ``` The application decodes the video file, runs the custom, application-defined Python preprocessing callback to convert the NV12 luma plane into the grayscale tensor expected by the lightweight face detector, then runs inference and postprocessing, merging the detected faces onto the original frame via [`qtimetamux`](/plugin-reference/qtimetamux), overlaying them, and rendering the result fullscreen. The video plays back with face-detection bounding boxes decoded by the custom grayscale preprocessing callback, overlaid and rendered fullscreen on the display. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_preprocess_lightweight_face_detect.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_preprocess_lightweight_face_detect.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Preprocessing with Palm Detection This example is the live-camera counterpart to the lightweight face detection pipeline: [`CamSrc`](/plugin-reference/qticamsrc) captures frames directly instead of decoding a file, but the branching structure is otherwise identical — a `tee` right after the stream filter splits the pipeline into a display branch and an ML branch, each guarded by its own queue, and [`qtimetamux`](/plugin-reference/qtimetamux) merges the ML branch's detections back onto the original frame before overlay. Setting `engine` to `"none"` on the [`MLVConverter`](/plugin-reference/qtimlvconverter) element disables its internal preprocessing path and hands frame-to-tensor conversion off to an application-defined callback registered via [`MLVConverter.set_handler(...)`](/plugin-reference/qtimlvconverter). Unlike the grayscale face-detection example, `palm_detection_full.tflite` expects a `[1, H, W, 3]` float32 RGB tensor normalized to `[0, 1]`, so the callback converts the full NV12 image (both luma and chroma planes) to RGB using NumPy and scales each channel into that range, with letterbox padding for any area the blit doesn't cover. Introduction Check application source code on GitHub: [`qimsdk_ref_external_preprocess_palm_detect.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_preprocess_palm_detect.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_preprocess_palm_detect.py` #### Download Required Files Download the gesture recognizer task bundle from Google MediaPipe to obtain the palm detection model: ```bash theme={null} # Download the gesture recognizer task bundle wget https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/latest/gesture_recognizer.task # Extract the top-level task unzip gesture_recognizer.task # Extract hand landmarker models unzip hand_landmarker.task # → hand_detector.tflite, hand_landmarks_detector.tflite ``` | File | Save as | | - | - | | hand\_detector.tflite (see steps above) | palm\_detection\_full.tflite | | palmd\_labels.json | palmd\_labels.json | | palmd\_settings.json | palmd\_settings.json | If a downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` This pipeline's source is the on-device camera, so no video file needs to be copied. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp palm_detection_full.tflite @:~/Downloads/qimsdk_samples/models/ scp palmd_labels.json @:~/Downloads/qimsdk_samples/labels/ scp palmd_settings.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_preprocess_palm_detect.py ``` The application captures live video from the on-device camera, runs the custom, application-defined Python preprocessing callback to convert NV12 frames into the normalized float32 RGB tensor expected by the palm detector, then runs inference and postprocessing, merging the detected palms onto the original frame via [`qtimetamux`](/plugin-reference/qtimetamux), overlaying them, and rendering the result fullscreen. The live camera feed is rendered fullscreen on the display with palm-detection bounding boxes decoded by the custom float32 RGB preprocessing callback overlaid on each frame in real time. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_preprocess_palm_detect.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_preprocess_palm_detect.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Postprocessing with YOLOv5 (Detection) This example demonstrates the object detection postprocessing callback (`ObjectDetectionPostprocessCallback`) using a YOLOv5 model. Preprocessing and inference are handled by the SDK, and the raw output tensors are decoded into bounding boxes, class names, and confidence scores by an application-defined callback, which are then overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_detection_yolov5.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_detection_yolov5.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_detection_yolov5.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/models/yolov5?searchTerm=yolov5) | yolov5m-320x320-int8.tflite | | yolov5m.json | yolov5m.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov5m-320x320-int8.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov5m.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_postprocess_detection_yolov5.py ``` The application runs inference on the video file and uses the custom, application-defined postprocessing callback to decode detections, which are then overlaid and displayed fullscreen. The video plays back with YOLOv5 bounding boxes and class labels decoded by the custom postprocessing callback, overlaid and rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_detection_yolov5.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_detection_yolov5.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Post processing with ML Bin Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_mlbin_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_mlbin_yolov8.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_mlbin_yolov8.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_postprocess_mlbin_yolov8.py ``` The application runs preprocessing, inference, and the custom, application-defined postprocessing callback all inside `mlbin`, overlaying the resulting detections on the video and rendering it fullscreen. The video plays back with YOLOv8 bounding boxes and class labels decoded by the custom postprocessing callback, overlaid and rendered fullscreen on the display. Expected Output To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_mlbin_yolov8.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_mlbin_yolov8.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Postprocessing with ResNet101 (Classification) This example demonstrates the classification postprocessing callback (`ClassificationPostprocessCallback`). Preprocessing and inference are handled by the SDK, and the raw output tensors from a ResNet101 model are decoded into labeled classification results (name and confidence) by an application-defined callback. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_classification_resnet101.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_classification_resnet101.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_classification_resnet101.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/resnet101) | Resnet101\_Quantized.tflite | | resnet101.json | resnet101.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | classification\_sample.mp4 | No ideal sample video ships with the SDK for this use case. Supply your own content for best results. If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp Resnet101_Quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp resnet101.json @:~/Downloads/qimsdk_samples/labels/ scp classification_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_postprocess_classification_resnet101.py ``` The application runs inference on the ResNet101 model and uses the custom, application-defined postprocessing callback to decode classification results, which are then displayed. The video plays back with the top classification result and its confidence score decoded by the custom postprocessing callback, displayed as text overlaid on the frame. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_classification_resnet101.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_classification_resnet101.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Postprocessing with MiDaS v2 (Depth Estimation) This example demonstrates the depth estimation postprocessing callback (`DepthEstimationPostprocessCallback`). Preprocessing and inference are handled by the SDK, and the raw output tensors from a MiDaS v2 model are decoded by an application-defined callback into a per-pixel depth map (values and pseudo-color) for downstream visualization. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_depth_estimation_midasv2.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_depth_estimation_midasv2.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_depth_estimation_midasv2.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/models/midas) | midas-tflite-w8a8.tflite | | midas-v2-labels.json | midas-v2-labels.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp midas-tflite-w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp midas-v2-labels.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_postprocess_depth_estimation_midasv2.py ``` The application runs inference on the MiDaS v2 model and uses the custom, application-defined postprocessing callback to decode the depth map, which is then rendered. The video plays back with a pseudo-colored depth map decoded by the custom postprocessing callback, rendered fullscreen on the display in place of the original frame. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_depth_estimation_midasv2.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_depth_estimation_midasv2.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Postprocessing with Gesture Recognition This example demonstrates gesture recognition using a daisy-chained, multi-model pipeline: palm detection locates the hand region, hand landmark detection extracts keypoints from that region, and a gesture embedder followed by a canned gesture classifier decode the landmarks into a recognized gesture. Preprocessing and inference are handled by the SDK, and the raw output tensors are decoded by an application-defined postprocessing callback. The pipeline uses the on-device camera as its source. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_gesture_recognition.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_gesture_recognition.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_gesture_recognition.py` #### Download Required Files Download the gesture recognizer task bundle from Google MediaPipe to obtain the palm detection, hand landmark, gesture embedder, and canned gesture classifier models: ```bash theme={null} # Download the gesture recognizer task bundle wget https://storage.googleapis.com/mediapipe-models/gesture_recognizer/gesture_recognizer/float16/latest/gesture_recognizer.task # Extract the top-level task unzip gesture_recognizer.task # Extract hand landmarker models unzip hand_landmarker.task # → hand_detector.tflite, hand_landmarks_detector.tflite # Extract gesture recognizer models unzip hand_gesture_recognizer.task # → gesture_embedder.tflite, canned_gesture_classifier.tflite ``` | File | Save as | | - | - | | hand\_detector.tflite (see steps above) | palm\_detection\_full.tflite | | hand\_landmarks\_detector.tflite (see steps above) | hand\_landmark\_full.tflite | | gesture\_embedder.tflite (see steps above) | gesture\_embedder.tflite | | canned\_gesture\_classifier.tflite (see steps above) | canned\_gesture\_classifier.tflite | | palmd\_labels.json | palmd\_labels.json | | hlandmarks.json | hlandmarks.json | | gesture\_rec.json | gesture\_rec.json | If a downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp palm_detection_full.tflite @:~/Downloads/qimsdk_samples/models/ scp hand_landmark_full.tflite @:~/Downloads/qimsdk_samples/models/ scp gesture_embedder.tflite @:~/Downloads/qimsdk_samples/models/ scp canned_gesture_classifier.tflite @:~/Downloads/qimsdk_samples/models/ scp palmd_labels.json @:~/Downloads/qimsdk_samples/labels/ scp hlandmarks.json @:~/Downloads/qimsdk_samples/labels/ scp gesture_rec.json @:~/Downloads/qimsdk_samples/labels/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_postprocess_gesture_recognition.py ``` The application captures live video from the on-device camera, runs the daisy-chained palm detection → hand landmark → gesture embedder → canned gesture classifier models, and uses the custom, application-defined postprocessing callback to decode the recognized gesture, which is then overlaid and displayed fullscreen. The live camera feed is rendered fullscreen on the display with the recognized gesture label, decoded by the custom postprocessing callback, overlaid on each frame in real time. * **Source code:** [`qimsdk_ref_external_postprocess_gesture_recognition.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_gesture_recognition.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Postprocessing with HRNet (Pose Estimation) This example demonstrates the pose estimation postprocessing callback (`PoseEstimationPostprocessCallback`) using a two-stage pipeline: a YOLOv5 model detects persons in the frame, and an HRNet model estimates keypoints for each detected person. Preprocessing and inference are handled by the SDK, and the raw output tensors are decoded into keypoints and skeleton links by an application-defined callback, which are then overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_pose_estimation_hrnet.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_pose_estimation_hrnet.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_pose_estimation_hrnet.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/models/yolov5?searchTerm=yolov5) | yolov5m-320x320-int8.tflite | | [Model](https://aihub.qualcomm.com/models/hrnet_pose?searchTerm=hr) | hrnet\_pose\_w8a8.tflite | | yolov5m.json | yolov5m.json | | hrnet.json | hrnet.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | pose\_sample.mp4 | No ideal sample video ships with the SDK for this use case. Supply your own content for best results. If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp yolov5m-320x320-int8.tflite @:~/Downloads/qimsdk_samples/models/ scp hrnet_pose_w8a8.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov5m.json @:~/Downloads/qimsdk_samples/labels/ scp hrnet.json @:~/Downloads/qimsdk_samples/labels/ scp pose_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_postprocess_pose_estimation_hrnet.py ``` The application runs inference on the video file, first detecting persons with YOLOv5 and then estimating pose keypoints with HRNet, using the custom, application-defined postprocessing callback to decode the results, which are then overlaid and displayed fullscreen. The video plays back with skeleton keypoints and links decoded by the custom postprocessing callback, overlaid on each detected person and rendered fullscreen on the display. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_pose_estimation_hrnet.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_pose_estimation_hrnet.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. ### Custom Postprocessing with DeepLabv3 (Segmentation) This example demonstrates the segmentation postprocessing callback (`SegmentationPostprocessCallback`) using a DeepLabv3 model. Preprocessing and inference are handled by the SDK, and the raw output tensors are decoded into per-pixel semantic labels and colors by an application-defined callback, which are then overlaid on the video and displayed. Introduction Check application source code on GitHub: [`qimsdk_ref_external_postprocess_segmentation_deeplab.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_segmentation_deeplab.py) Pre-built application on device: `/usr/bin/qimsdk_ref_external_postprocess_segmentation_deeplab.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/deeplabv3_plus_mobilenet) | dv3\_argmax\_int32.tflite | | dv3-argmax-labels.json | dv3-argmax-labels.json | | [ai\_demo\_sample.mp4](https://github.com/qualcomm/sample-apps-for-qualcomm-linux/blob/main/qualcomm-linux/artifacts/videos/demo_samples/ai_demo_sample.mp4) | ai\_demo\_sample.mp4 | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` Some AI Hub models require running the AI Hub export/optimize step for your target runtime before the downloaded file is usable — download alone isn't always sufficient. ```bash SCP (SSH) theme={null} # Replace and with your device credentials. # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels,media}" scp dv3_argmax_int32.tflite @:~/Downloads/qimsdk_samples/models/ scp dv3-argmax-labels.json @:~/Downloads/qimsdk_samples/labels/ scp ai_demo_sample.mp4 @:~/Downloads/qimsdk_samples/media/ ``` ```bash theme={null} python3 /usr/bin/qimsdk_ref_external_postprocess_segmentation_deeplab.py ``` The application runs inference on the video file, segmenting the frame with DeepLabv3, and uses the custom, application-defined postprocessing callback to decode the per-pixel segmentation results, which are then overlaid and displayed fullscreen. The video plays back with a semantic segmentation color mask decoded by the custom postprocessing callback, overlaid on each frame and rendered fullscreen on the display. To stop the application, press **CTRL + C**. * **Source code:** [`qimsdk_ref_external_postprocess_segmentation_deeplab.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/reference-apps/qimsdk_ref_external_postprocess_segmentation_deeplab.py) Python applications run directly from source, so no build or compilation step is required. Edit the script directly and rerun it to apply your changes. # Benefits Source: https://imsdkdocs.qualcomm.com/benefits ### Why developers should use the Qualcomm Intelligent Multimedia SDK Combines multimedia pipelines and ML inference into a single, coherent developer experience. QIM SDK natively exploits QCOM accelerators including CPU, GPU, NPU, ISP, VPU and DSP through optimized plugins Built on Gstreamer, providing a mature, composable pipeline model with 40+ Qualcomm-optimized plugins. SDK supports running models from common frameworks such as TFLite/LiteRT, ONNX, and QNN. Browse, download, and deploy hundreds of pre-trained, quantized models directly from [Qualcomm AI Hub](https://aihub.qualcomm.com/). Models arrive ready to run — no manual conversion, calibration, or profiling required. *** ## How QIM SDK Delivers These Benefits **What developers get** * One pipeline for capture, preprocess, inference, overlay, encode, and streaming. * One metadata flow for pixels and AI results. * Less custom glue code between multimedia and ML stages. **How QIM SDK delivers it** * Builds on GStreamer, so media and AI elements live in the same pipeline graph. * Carries detections, labels, keypoints, and tensor info as frame metadata. * Supports sequential flows like detection → classification and parallel flows using `tee` + [`qtimetamux`](plugin-reference/qtimetamux). * Supports cross-pipeline designs with [`qtisocketsink`](plugin-reference/qtisocketsink) and [`qtisocketsrc`](plugin-reference/qtisocketsrc) when applications need modular stages. **Developer takeaway** You describe the full application as a pipeline instead of stitching together separate camera, inference, and rendering subsystems. **What developers get** * Lower latency. * Lower CPU load. * Better throughput for multi-stream and real-time AI. **How QIM SDK delivers it** * **ISP:** camera ingest and imaging path use Qualcomm imaging hardware. * **GPU:** plugins such as [`qtimlvconverter`](plugin-reference/qtimlvconverter), [`qtivtransform`](plugin-reference/qtivtransform), [`qtivoverlay`](plugin-reference/qtivoverlay), and [`qtivcomposer`](plugin-reference/qtivcomposer) offload resize, color convert, crop, rotate, overlay, and composition work. * **NPU:** inference workloads run through supported runtimes on dedicated AI hardware. * **VPU:** [`v4l2h264dec`](plugin-reference/v4l2h264dec), `v4l2h265dec`, `v4l2h264enc`, and `v4l2h265enc` handle video decode/encode efficiently. * **Zero-copy + buffer pools:** frames move through the pipeline as DMA-buf handles with fewer copies and lower allocation overhead. **Developer takeaway** Most performance-critical stages are expressed by choosing the right plugins, not by writing accelerator-specific application code. **What developers get** * Hundreds of pre-trained, profiled, quantized models ready to deploy. * No manual conversion, calibration, or accuracy validation. * Models arrive as `.tflite`, `.dlc`, or `.bin` files compatible with QIM SDK runtimes. **How QIM SDK delivers it** * Models from AI Hub are exported targeting specific Qualcomm SoC backends (HTP, GPU, CPU). * [`qtimltflite`](plugin-reference/qtimltflite), `qtimlsnpe`, and [`qtimlqnn`](plugin-reference/qtimlqnn) load these models directly — no format adaptation layer. * Confidence thresholds, post-processing modules, and label files are supplied alongside the model, matching the QIM SDK [`qtimlpostprocess`](plugin-reference/qtimlpostprocess) interface. **Developer takeaway** Browse AI Hub, download a model, drop it into the pipeline — no custom integration work required. **What developers get** * Frames never copied through CPU memory between accelerator stages. * Lower peak memory bandwidth utilization. * Reduced end-to-end pipeline latency, especially at 1080p and above. **How QIM SDK delivers it** * Plugins negotiate DMA-buf allocation through GStreamer's allocation query mechanism. * ISP (`qticamsrc`) → GPU ([`qtimlvconverter`](plugin-reference/qtimlvconverter), [`qtivtransform`](plugin-reference/qtivtransform)) → NPU ([`qtimltflite`](plugin-reference/qtimltflite)) → display ([`waylandsink`](plugin-reference/waylandsink)) all share the same buffer handle. * The `capture-io-mode=dmabuf` and `output-io-mode=dmabuf-import` properties on decode/encode plugins enable hardware-to-hardware buffer passing. **Developer takeaway** Zero-copy is opt-in via plugin properties, not a framework limitation — the pipeline negotiates it automatically when all stages support it. **What developers get** * Built-in modules for detection, classification, segmentation, pose, audio, and super-resolution. * Custom module support without SDK recompilation. * Runtime module swap — change post-processing logic by updating a `.so` file on the device. **How QIM SDK delivers it** * [`qtimlpostprocess`](plugin-reference/qtimlpostprocess) loads `libml-postprocess-.so` at runtime via `dlopen`. * Each module implements `Caps()`, `Configure()`, and `Process()` from the `IModule` interface. * Modules are deployed to `/usr/lib/gstreamer-1.0/ml/modules/` — no pipeline changes required after deployment. **Developer takeaway** Swap or extend post-processing without touching the pipeline definition or recompiling the application. **What developers get** * Hardware-demosaiced, ISP-processed frames delivered directly to downstream AI and multimedia stages. * Multi-camera support (primary + secondary sensor). * Zero-copy from ISP to GPU/NPU via GBM/DMA-buf. **How QIM SDK delivers it** * `qticamsrc` exposes a GStreamer source interface over the Qualcomm camera service. * Outputs `video/x-raw,format=NV12` — directly consumable by [`qtimlvconverter`](plugin-reference/qtimlvconverter), [`qtivtransform`](plugin-reference/qtivtransform), and encode plugins. * Camera properties (resolution, framerate, camera index) are set via standard GStreamer element properties. **Developer takeaway** A single pipeline element replaces the entire camera driver integration layer — no V4L2 or camera HAL code to manage. **What developers get** * Familiar pipeline construction. * Easier integration with upstream plugins. * Better portability of application logic. **How QIM SDK delivers it** * Follows standard GStreamer plugin architecture and negotiation rules. * Uses caps negotiation for format compatibility. * Uses allocation queries for memory negotiation and zero-copy paths. * Reuses standard image formats such as `NV12`, `RGBA`, and `I420`. * Reuses standard metadata where possible and adds AI-specific metadata only when needed. **Developer takeaway** If your team already knows GStreamer, QIM SDK fits into the same workflow with Qualcomm-optimized building blocks. **What developers get** * Freedom to choose runtime per use case. * Support for common edge AI workloads. * A path from prototype to optimized deployment. **How QIM SDK delivers it** * Supports `TFLite`, `SNPE`, and `QNN` runtime paths. * Supports use cases such as detection, classification, segmentation, pose, super resolution, and audio AI. * Supports chained and parallel model execution in the same media flow. * Preserves model outputs as metadata so downstream stages can reuse them. **Developer takeaway** You can keep one pipeline architecture while changing the model, runtime, or accelerator strategy based on product needs. ## Developer Notes Zero-copy pipelines keep the same frame buffer moving across decode, preprocess, inference, overlay, and encode stages. This reduces memory traffic, CPU overhead, and end-to-end latency — especially important for high-resolution and multi-stream workloads. A typical AI pipeline must resize, color-convert, and normalize frames before inference. QIM SDK offloads these steps through GPU-accelerated plugins instead of doing them in generic CPU-only stages, which helps preserve frame rate and lowers CPU utilization. Each example shows the same operation built with upstream GStreamer CPU elements and then with QIM SDK hardware-accelerated plugins. Timing numbers are measured on IQ9 with a USB camera at 1080p input. # QIM SDK Tutorials Source: https://imsdkdocs.qualcomm.com/blogs/blog-index
Qualcomm QIM SDK — AI pipeline tutorials, guides, and deep dives.

QIM SDK · Qualcomm
Computer Vision
Monitoring PPE Compliance with AI‑Powered Computer Vision

Build a real-time PPE detection pipeline using Qualcomm QIM SDK with daisy-chained ML models for person detection and protective equipment recognition — running entirely on-device with hardware-accelerated inference via Qualcomm HTP.

QIM SDK Team · May 12, 2026 Read more →
Gesture Recognition
Building a Real-Time Hand Gesture Recognition AI Pipeline Using the QIM SDK

A four-stage sequential AI pipeline — palm detection, hand landmark estimation, gesture embedding, and gesture classification — built with Qualcomm QIM SDK for real-time edge deployment.

QIM SDK Team · Jun 10, 2026 Read more →
Computer Vision
Multi Stream IP Camera Application

A real-time object detection pipeline built with QIM SDK using a YOLOv8 TensorFlow Lite model, supporting USB cameras, ISP cameras, RTSP streams, and video files — with RTSP/WebRTC output.

QIM SDK Team · Apr 10, 2026 Read more →
Multi-Stream AI
Security Video Wall — 31-Stream Parallel Object Detection

A scalable AI pipeline processing up to 31 input video streams in parallel with YOLOv8 object detection, compositing all streams into a single output and streaming over RTSP/WebRTC.

QIM SDK Team · Apr 5, 2026 Read more →
Safety & Security
Restricted Zone Alarm — Real-Time Zone Intrusion Detection

Real-time pipeline that identifies when a person steps into a predefined restricted zone using QIM SDK foot detection — with visual alerts, bounding box status indicators, and RTSP output.

QIM SDK Team · Mar 28, 2026 Read more →
Gesture Recognition
Building a Wrist-Controlled Car Game Using QIM SDK

A split-screen touchless driving game controlled entirely by wrist movement — built on a two-stage palm detection and hand landmark pipeline running on Qualcomm QIM SDK with QNN HTP acceleration.

QIM SDK Team · Aug 3, 2026 Read more →
Gesture Recognition
Touchless Smart Board — Transforming Digital Learning with QIM SDK

A touchless virtual whiteboard controlled entirely by hand gestures — built on a two-stage palm detection and hand landmark pipeline running on Qualcomm QIM SDK with QNN HTP acceleration.

QIM SDK Team · Aug 3, 2026 Read more →
Computer Vision
Edge AI for Smart Manufacturing — Real-Time Bottle Counting Using Qualcomm QIM SDK

A YOLOv8-based object detection pipeline that counts products crossing a center region of interest in real time — built with Qualcomm QIM SDK and QNN HTP acceleration.

QIM SDK Team · Aug 3, 2026 Read more →
# Monitoring PPE Compliance with AI-Powered Computer Vision Source: https://imsdkdocs.qualcomm.com/blogs/montioring-ppe-compliance-with-ai-powered Build a real-time PPE detection pipeline using Qualcomm QIM SDK with daisy-chained ML models for person detection and protective equipment recognition.
QIM SDK · Qualcomm
Computer Vision

Build a real-time PPE detection pipeline using Qualcomm QIM SDK with daisy-chained ML models for person detection and protective equipment recognition — running entirely on-device with hardware-accelerated inference via Qualcomm HTP.

QIM SDK Team · May 12, 2026 · ← All posts

## Introduction Ensuring worker safety in industrial and construction environments demands continuous, real-time monitoring at scale — a challenge that traditional manual approaches cannot meet efficiently. The QIM SDK addresses this directly by delivering a hardware-accelerated, end-to-end AI pipeline that automates PPE compliance monitoring at the edge, with minimal operational overhead. Leveraging Qualcomm's dedicated hardware accelerators through the SDK's GStreamer plugin architecture, compute-intensive tasks — including video decoding, frame preparation (resizing, color format conversion, and pixel normalization), multi-stage AI inference, and encoding — are offloaded entirely from the CPU to purpose-built hardware blocks. This enables low-latency, power-efficient AI execution directly on Qualcomm edge devices, making continuous, real-world safety monitoring both practical and scalable. At the core of this use case is a **multi-stage daisy-chain AI pipeline**, where models operate sequentially and build upon each other's outputs. A person detection model first identifies individuals within the full frame; a second model then performs per-person PPE compliance analysis — detecting helmets, vests, gloves, and masks — using dynamically cropped regions derived from the initial detections. This approach enables fine-grained, per-person analysis with high accuracy, while keeping compute focused on regions of interest rather than the full frame. The metadata produced by this pipeline is **hierarchically structured**: base detections (persons) from the first model serve as parent entries, with PPE detection results from the second model attached as child metadata linked to each individual. This context-aware structure ensures that every detected safety item is explicitly associated with a specific person — enabling precise visualization, tracking, and downstream analytics. The QIM SDK further accelerates visualization through **hardware-accelerated overlay rendering and blitting**, where bounding boxes, labels, and compliance indicators are composited directly onto video frames using optimized hardware operations — delivering smooth, real-time visualization without additional CPU load or pipeline latency. Beyond visualization, the SDK provides native support for **AI metadata streaming**, synchronizing structured inference results with the video stream and transmitting them alongside the media pipeline. This transforms raw video into actionable, structured data — enabling external monitoring and alerting systems to consume real-time PPE compliance insights without re-running inference. Integration with **Qualcomm AI Hub** further accelerates development by providing access to optimized, production-ready models for both person detection and PPE analysis, significantly reducing the effort required to move from prototype to production deployment. The pipeline supports multiple input sources — USB, RTSP, ISP camera, and file-based video — and delivers results through real-time on-screen visualization, RTSP streaming, or WebRTC, with inference metadata transmitted in parallel. The result is a scalable, efficient edge AI system that transforms raw video into actionable safety intelligence — empowering organizations to proactively enforce compliance and mitigate risk in real time. Check application source code on GitHub: [`gst-ppe-detection`](https://github.com/qualcomm/qimsdk/tree/main/gstreamer/gst-sample-apps/gst-ppe-detection) ## Use Case Overview The pipeline accepts continuous video input from multiple source types — RTSP streams, ISP camera feeds, USB cameras, and file-based video. Each frame is submitted to a person detection model that identifies individuals and their locations within the scene. For each detected person, a dedicated PPE detection model analyzes the dynamically cropped region to identify the presence or absence of safety equipment — including helmets, vests, gloves, and masks. Detection results are attached to the video stream as hierarchically structured metadata, explicitly linking each PPE detection to its corresponding individual. Bounding boxes and labels are rendered directly onto video frames in real time using hardware-accelerated overlay, providing intuitive interpretation of detection results. [`qtimetamux`](../plugin-reference/qtimetamux) synchronizes all inference results with the original video frames, maintaining per-frame consistency throughout the pipeline. The annotated stream is delivered via RTSP or WebRTC. Structured PPE compliance data is transmitted in parallel as a JSON metadata stream — enabling seamless integration with external monitoring, alerting, and analytics systems. ## Pipeline diagram ## Elements used in pipeline | Element | Description | | - | - | | `source` | Accepts input from an RTSP camera, ISP camera, USB camera, or a local file. | | `tee` | Splits the incoming stream into multiple parallel branches for simultaneous downstream processing. | | [`qtimlvconverter`](../plugin-reference/qtimlvconverter) | Prepares video frames for inference by performing resizing, YUV-to-RGB color space conversion, and pixel normalization to match the model's input requirements. | | [`qtimltflite`](../plugin-reference/qtimltflite) | Executes the TFLite inference model for person/feet detection on each incoming frame. | | [`qtimlpostprocess`](../plugin-reference/qtimlpostprocess) | Decodes raw output tensors into structured bounding boxes and labels. Post-processing logic is implemented as a dynamically loaded module, enabling model-specific strategies to be swapped without pipeline changes. | | [`qtimetamux`](../plugin-reference/qtimetamux) | Synchronizes inference results with the original video stream and attaches them as per-frame structured metadata. | | [`qtivoverlay`](../plugin-reference/qtivoverlay) | Renders bounding boxes, labels, and the restricted zone polygon directly onto video frames for real-time visual feedback. | | [`qtimetaparser`](../plugin-reference/qtimetaparser) | Serializes per-frame ML metadata into JSON format for integration with external monitoring and analytics systems. | | [`v4l2h264enc`](../plugin-reference/v4l2h264enc) / `h264parse` | Encodes the processed video stream into H.264 format for downstream transmission or storage. | | `sink` | Streams the encoded video and associated metadata over RTSP or WebRTC via the `rtspbin` or `webrtcbin` plugins respectively, enabling remote clients to consume results in real time. | | [`waylandsink`](../plugin-reference/waylandsink) | Renders the annotated video stream to a local Wayland display. | ## How it works The PPE detection pipeline implements a **two-stage daisy-chain architecture**, where two sequential AI models operate in tandem — the output of the first model directly driving the execution of the second. * **Stage 1 — Person Detection:** The first AI model processes the full video frame and produces bounding boxes identifying the location of each individual in the scene. * **Crop Generation:** Since the second PPE detection model requires image crops — not bounding boxes — as input, a second instance of [`qtimlvconverter`](../plugin-reference/qtimlvconverter) operates in crop generation mode, receiving the bounding boxes produced by the first model and dynamically generating a cropped image region from the original frame for each detected person. * **Stage 2 — PPE Detection:** The PPE detection model is invoked once per detected person, analyzing each cropped region independently to identify the presence or absence of safety equipment — including helmets, vests, gloves, and masks. * **Metadata Re-attachment:** A second [`qtimetamux`](../plugin-reference/qtimetamux) instance re-attaches the PPE detection results to the original video stream, ensuring all detections are synchronized with the corresponding frame and person. * **Hierarchical Metadata:** To preserve logical relationships across both model stages, the pipeline employs a **hierarchical metadata model** based on unique IDs and parent IDs — explicitly linking each PPE detection to its corresponding individual, enabling accurate per-person visualization, tracking, and downstream analytics. ## Run application on device ### Setup Requirements #### Hardware | Component | Description | | - | - | | **Edge Device** | RB3 Gen 2, IQ8, or IQ9 — Primary processing unit for AI inference and video composition. | | **Camera Source** | IP/RTSP camera, ISP (on-device) camera, or USB camera. A local file source may be substituted if no physical camera is available. | | **HDMI Display Monitor** | Connected to the edge device for rendering and visualizing pipeline output. | | **PoE Switch** | Powers IP/RTSP cameras and provides network connectivity over a single Ethernet cable per camera. (Required for IP/RTSP camera setups only.) | | **Local Network** | Ensures the edge device, RTSP camera, and host machine are reachable on the same network. (Required when using RTSP camera input or streaming results via RTSP or WebRTC.) | #### Software **Flash your Qualcomm Edge device** by following the device setup and flashing instructions [here](../installation) **Once your device is ready**, follow the instructions below to set up the PPE AI Pipeline: ##### AI Model and config files | File | Download | Save as | | - | - | - | | Person Foot Detection model | [Qualcomm AI Hub — FootTrackNet](https://aihub.qualcomm.com/models/foot_track_net) | `foot_track_net_quantized.tflite` | | PPE Detection model | [Qualcomm AI Hub — GearGuardNet](https://aihub.qualcomm.com/models/gear_guard_net) | `gear_guard_net.tflite` | | Foot track labels | foot\_track\_net.json | `foot_track_net.json` | | Foot track net settings | foot\_track\_net\_settings.json | `foot_track_net_settings.json` | | Gear guard labels | gear\_guard\_net.json | `gear_guard_net.json` | | PPE sample video | Input video | `ppe_sample.mp4` | **Copy files to device** ```bash SCP (SSH) theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). ssh @ "mkdir -p ~/{models,labels,media,media/output}" scp foot_track_net_quantized.tflite @:~/models/ scp gear_guard_net.tflite @:~/models/ scp foot_track_net.json @:~/labels/ scp foot_track_net_settings.json @:~/labels/ scp gear_guard_net.json @:~/labels/ scp ppe_sample.mp4 @:~/media/ ``` **Connect to device** ```bash theme={null} ssh @ ``` **Run the PPE Application** A display must be connected to the device. If no display is available, use the `--no-display` flag to run in headless mode. Use the following base path for model and label files based on your OS: ```bash theme={null} gst-ppe-detection \ --input-type=file \ --input-config=$HOME/media/ppe_sample.mp4 ``` ```bash theme={null} gst-ppe-detection \ --input-type=file \ --input-config=$HOME/media/ppe_sample.mp4 \ --output-type=webrtc \ --output-config=wss://webrtc.nirbheek.in:8443 \ --webrtc-id=1010 ``` ```bash theme={null} gst-ppe-detection \ --input-type=file \ --input-config=$HOME/media/ppe_sample.mp4 \ --output-type=rtsp \ --output-config=8900 ``` > **Note:** This example uses an offline video file as input. To use an IP/RTSP camera or USB camera instead, update the `--input-type` argument accordingly — refer to the **Command-Line Options** section below for details. It produces two key output results: an AI-annotated video stream and a JSON metadata stream. To visualize these results, refer to the **Host-Side Visualization** section below. To configure alternative input sources or output destinations, refer to the Command-Line Options section. ## Visualize the Results - Host-Side Visualization (Windows + WSL) This section describes how to run the visualization client on a Windows host machine using **WSL (Windows Subsystem for Linux)**. The client renders the live video stream alongside a real-time AI metadata panel. 📥 The visualization client script can be downloaded here: rtsp\_webrtc\_client.zip It displays: * **Left panel** — Live video stream. (Output Video stream with AI overlays) * **Right panel** — Real-time AI metadata (JSON): object detections, bounding boxes, and confidence scores. **Step 1 — Install WSL and Ubuntu** If WSL is not already installed, run the following from a Windows terminal: ```bash theme={null} wsl --install Ubuntu-24.04 ``` Once installed, open the Ubuntu terminal and update the system: ```bash theme={null} sudo apt update && sudo apt upgrade -y ``` **Step 2 — Install System Dependencies** The visualization script requires GStreamer and Python GObject Introspection (GI) bindings. Install all required packages with: ```bash theme={null} sudo apt install -y \ python3 python3-pip python3-gi python3-gi-cairo \ gir1.2-gstreamer-1.0 \ gir1.2-gst-plugins-base-1.0 \ gir1.2-gst-plugins-bad-1.0 \ gstreamer1.0-tools \ gstreamer1.0-plugins-base \ gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad \ gstreamer1.0-plugins-ugly \ gstreamer1.0-libav \ python3-websocket \ libnice10 \ libnice-dev \ gstreamer1.0-nice ``` **Step 3 — Run the Visualization Client Script** Navigate to the directory containing the script and run: ```bash theme={null} python3 rtsp_webrtc_client.py rtsp://:8900/live ``` ```bash theme={null} python3 rtsp_webrtc_client.py --source webrtc --signalling-server wss://webrtc.nirbheek.in:8443 --peer-id 1010 ``` **Step 4 — Expected Output** Once the client connects, the UI will display: | Panel Content | Description | | - | - | | Left | Real-time decoded video stream | | Right | Live AI metadata panel — object detections, bounding boxes, and confidence scores | After following the steps, the video and metadata streams should be up and running. The pipeline generates structured JSON metadata in the following format: ```json theme={null} { "object_detection": [ { "label": "person", "confidence": 76.62, "color": 16711935, "rectangle": { "x": 0.58, "y": 0.05, "width": 0.28, "height": 0.90 }, "landmarks": { "nose": { "x": 0.71, "y": 0.21 } }, "object_detection": [ { "label": "helmet", "confidence": 97.79, "color": 65535, "rectangle": { "x": 0.64, "y": 0.05, "width": 0.13, "height": 0.29 } } ] } ], "parameters": { "timestamp": "11341424121" } } ``` ## Command-Line Options Selects the video input source for the pipeline. | Value | Description | | - | - | | `usb` | USB camera. Requires `--input-config=/dev/video0`. | | `isp` | Built-in ISP (on-device) camera. Optionally specify a camera ID via `--input-config=0`. | | `rtsp` | External IP/RTSP camera or stream. Requires `--input-config=rtsp://...`. | | `file` | Local H.264-encoded video file. Requires `--input-config=/path/to/ppe_sample.mp4`. | Specifies the input source configuration corresponding to the selected `--input-type`. | Input Type | Value | | - | - | | USB | `/dev/videoX` | | ISP | `` | | RTSP | `rtsp://` | | File | `/path/to/ppe_sample.mp4` | Defines how the processed output video stream is delivered. | Value | Description | | - | - | | `none` | No video output (headless mode). Display output is controlled separately via `--no-display`. | | `file` | Saves the encoded output video stream to a file. Requires `--output-config`. | | `rtsp` | Streams the output video over RTSP. Requires `--output-config=`. Access at `rtsp://:/live`. | | `webrtc` | Streams the output video over WebRTC. Requires `--output-config=ws://...`. | Specifies the output destination configuration corresponding to the selected `--output-type`. | Output Type | Value | | - | - | | File | `/path/to/output.mp4` | | RTSP | `` | | WebRTC | `ws://:` | Root directory where the application looks for AI model, label, and configuration files. Assets are resolved automatically: | Asset Type | Resolved Path | | - | - | | Model files (`*.tflite`) | `/models/` | | Label/settings files (`*.json`) | `/labels/` | ```bash theme={null} --model-base-path=~ # QLI: /root, Ubuntu: /home/ubuntu ``` Disables local on-screen rendering of the output video stream. Recommended for: * Headless deployments * Remote streaming setups (RTSP/WebRTC) * Performance optimization where display overhead is undesirable Sets the raw input video resolution and frame rate. Applicable only to ISP and USB inputs. ```bash theme={null} --width=1920 --height=1080 --framerate=30 ``` Specifies the local WebRTC signaling client ID used for peer connection setup with the signaling server. ```bash theme={null} --webrtc-id=1010 ``` ## Implementation Deep-Dive The application cleanly separates user configuration from runtime state — organizing command-line parameters, GStreamer objects, dynamic pad tracking, WebRTC signaling, and shutdown handling into predictable, well-defined locations. ```c theme={null} typedef struct GstAppConfig { gchar *input_type; gchar *input_location; gchar *input_format; gchar *output_type; gchar *output_location; gboolean no_display; gint width, height, framerate, rtsp_latency_ms, webrtc_id; } GstAppConfig; typedef struct GstAppContext { GstAppConfig config; GstElement *pipeline; GMainLoop *mloop; GstElement *webrtc; gboolean is_shutting_down; } GstAppContext; ``` The pipeline is composed of three branches: common input, common output, and application-specific processing. Construction order is deliberate — input first, output second, processing last. ```c theme={null} static gboolean gst_app_create_pipe (GstAppContext *appctx) { GstElement *input_tail = NULL, *output_head = NULL, *meta_head = NULL; appctx->pipeline = gst_pipeline_new ("gst-ppe-detection"); if (!gst_app_create_input_pipe (appctx, &input_tail)) return FALSE; if (!gst_app_create_output_pipe (appctx, &output_head, &meta_head)) return FALSE; if (!gst_app_create_user_pipe (appctx, input_tail, output_head, meta_head)) return FALSE; return TRUE; } ``` Dedicated `qtimlvconverter`, `qtimltflite`, `qtimlpostprocess`, and `qtimetamux` elements are allocated for each inference stage. ```c theme={null} qtimlvconverter_stage1 = gst_app_make_element ("qtimlvconverter", "qtimlvconverter_stage1"); qtimlvconverter_stage2 = gst_app_make_element ("qtimlvconverter", "qtimlvconverter_stage2"); qtimltflite_stage1 = gst_app_make_element ("qtimltflite", "qtimltflite_stage1"); qtimltflite_stage2 = gst_app_make_element ("qtimltflite", "qtimltflite_stage2"); qtimlpostprocess_stage1 = gst_app_make_element ("qtimlpostprocess", "qtimlpostprocess_stage1"); qtimlpostprocess_stage2 = gst_app_make_element ("qtimlpostprocess", "qtimlpostprocess_stage2"); qtimetamux_stage1 = gst_app_make_element ("qtimetamux", "qtimetamux_stage1"); qtimetamux_stage2 = gst_app_make_element ("qtimetamux", "qtimetamux_stage2"); qtivoverlay = gst_app_make_element ("qtivoverlay", "qtivoverlay"); qtimlmetaparser = gst_app_make_element ("qtimlmetaparser", "qtimlmetaparser"); ``` Stage 2 uses cumulative ROI batching. Both models execute via the QNN external delegate. Each post-processing stage is configured with dedicated labels and settings. ```c theme={null} gst_element_set_enum_property (qtimlvconverter_stage2, "mode", "roi-batch-cumulative"); g_object_set (G_OBJECT (qtimlpostprocess_stage1), "results", 10, "labels", STAGE1_LABELS_PATH, "bbox-stabilization", TRUE, "settings", STAGE1_SETTINGS_PATH, NULL); gst_element_set_enum_property (qtimlpostprocess_stage1, "module", "qpd"); g_object_set (G_OBJECT (qtimlpostprocess_stage2), "results", 10, "labels", STAGE2_LABELS_PATH, "bbox-stabilization", TRUE, NULL); gst_element_set_enum_property (qtimlpostprocess_stage2, "module", "yolov8"); delegate_options = gst_structure_from_string ("QNNExternalDelegate,backend_type=htp;", NULL); g_object_set (G_OBJECT (qtimltflite_stage1), "external-delegate-path", "libQnnTFLiteDelegate.so", "external-delegate-options", delegate_options, "model", STAGE1_MODEL_PATH, NULL); gst_element_set_enum_property (qtimltflite_stage1, "delegate", "external"); g_object_set (G_OBJECT (qtimltflite_stage2), "external-delegate-path", "libQnnTFLiteDelegate.so", "external-delegate-options", delegate_options, "model", STAGE2_MODEL_PATH, NULL); gst_element_set_enum_property (qtimltflite_stage2, "delegate", "external"); gst_structure_free (delegate_options); ``` Stage 1 attaches person detection metadata to the stream; Stage 2 consumes the resulting ROIs, runs PPE inference, overlays results, and forwards the annotated stream to the output branch. ```c theme={null} gst_element_link_many (input_tail, tee[0], queue[0], qtimetamux_stage1, NULL); gst_element_link_many (tee[0], queue[1], qtimlvconverter_stage1, queue[2], qtimltflite_stage1, queue[3], qtimlpostprocess_stage1, postprocess_caps_stage1, queue[4], qtimetamux_stage1, NULL); gst_element_link_many (qtimetamux_stage1, queue[5], tee[1], NULL); gst_element_link_many (tee[1], queue[6], qtimetamux_stage2, NULL); gst_element_link_many (tee[1], queue[7], qtimlvconverter_stage2, queue[8], qtimltflite_stage2, queue[9], qtimlpostprocess_stage2, postprocess_caps_stage2, queue[10], qtimetamux_stage2, queue[11], qtivoverlay, queue[12], tee[2], NULL); if (output_head != NULL) gst_element_link_many (tee[2], queue[13], output_head, NULL); if (meta_head != NULL) gst_element_link_many (tee[2], queue[14], qtimlmetaparser, meta_head, NULL); ``` Metadata is handled through a dedicated branch and optionally exported. * **RTSP** — metadata linked via sink pad on [`qtirtspbin`](../plugin-reference/qtirtspbin) * **WebRTC** — metadata sent via a dedicated data channel ```c theme={null} if (meta_head != NULL) gst_element_link_many (tee[1], queue[7], qtimlmetaparser, meta_head, NULL); ``` **WebRTC metadata callback:** ```c theme={null} static GstFlowReturn gst_app_webrtc_meta_new_sample_cb (GstElement *appsink, gpointer userdata) { GstSample *sample = NULL; GstBuffer *buffer = NULL; g_signal_emit_by_name (appsink, "pull-sample", &sample); buffer = gst_sample_get_buffer (sample); gst_buffer_map (buffer, &mapinfo, GST_MAP_READ); metadata = g_strndup ((const gchar *) mapinfo.data, mapinfo.size); // send via WebRTC data channel } ``` **Key WebRTC signaling callbacks:** | Callback | Responsibility | | - | - | | `on_offer_created` | Constructs and sends the SDP offer to the remote peer | | `on_ice_candidate` | Transmits ICE candidates to the signaling server | | `on_ws_message` | Handles incoming signaling messages from the WebSocket | ## Build the Application * **Source code:** [gst-ppe-detection](https://github.com/qualcomm/qimsdk/tree/main/gstreamer/gst-sample-apps/gst-ppe-detection) * **Build instructions:** [Steps to build custom application](../advanced/yocto-build#steps-to-build-custom-application) ## Conclusion The QIM SDK's modular, plugin-based architecture enables developers to rapidly build scalable multi-stage AI video analytics pipelines without sacrificing flexibility. By attaching each model's output directly to the corresponding video frame as structured metadata, the SDK preserves inference context across pipeline stages and enables accurate downstream processing. Results can be delivered through on-screen overlays, network streams, or as independently transmitted metadata — giving developers full control over how and where AI-driven insights are consumed. # Multi Stream IP Camera application Source: https://imsdkdocs.qualcomm.com/blogs/multi-stream-ip-camera A real-time object detection pipeline built with QIM SDK using a YOLOv8 TensorFlow Lite model, supporting multiple input sources and RTSP/WebRTC output.
Computer Vision

A real-time object detection pipeline built with QIM SDK using a YOLOv8 TensorFlow Lite model, supporting USB cameras, ISP cameras, RTSP streams, and video files — with RTSP/WebRTC output.

QIM SDK Team · Apr 10, 2026 · ← All posts

## Introduction Managing video feeds from multiple IP cameras simultaneously is a core requirement in modern surveillance, retail analytics, smart city, and industrial monitoring deployments. Processing each stream independently on a CPU is costly and difficult to scale — but with the QIM SDK, multiple concurrent RTSP streams can be decoded, processed through AI inference, and rendered or streamed out in real time, all using Qualcomm's dedicated hardware accelerators. The QIM SDK's GStreamer-based plugin architecture offloads compute-intensive tasks — including multi-stream hardware H.264/H.265 decoding, frame preparation, AI inference via the Neural Processing Unit (NPU), and re-encoding — entirely from the CPU to purpose-built hardware blocks. This enables low-latency, power-efficient processing of multiple simultaneous IP camera feeds directly on Qualcomm edge devices. At the core of this use case is a **parallel multi-stream pipeline**, where each incoming RTSP stream is independently decoded and processed through its own AI inference chain. Results from all streams are then composited into a unified output — either for display, recording, or network streaming. The pipeline supports flexible input configurations, from a single RTSP source up to multiple concurrent streams, making it suitable for scalable, real-world deployments. Check application source code on GitHub: [`gst-ip-camera`](https://github.com/qualcomm/qimsdk/tree/main/gstreamer/gst-sample-apps/gst-ip-camera) ## Use Case Overview Accepts multiple concurrent RTSP streams from IP cameras, each decoded independently using hardware-accelerated H.264/H.265 decoding. Each decoded stream is passed through a dedicated AI inference chain — including preprocessing (`qtimlvconverter`), model execution (`qtimltflite`), and post-processing (`qtimlpostprocess`) — for per-stream object detection. Inference results are attached to each stream as structured per-frame metadata via `qtimetamux`, maintaining stream–detection correspondence. Bounding boxes and labels are rendered onto each stream's frames via `qtivoverlay` for real-time visual feedback. Annotated frames from all streams are composited into a unified output layout using `qtivcomposer`. The composited stream is delivered to a local Wayland display, encoded and saved to file, or streamed over RTSP/WebRTC. ## Pipeline diagram Pipeline Diagram ## Elements used in pipeline | Element | Description | | - | - | | `rtspsrc` | Receives an RTSP stream from an IP camera over the network. | | `rtph264depay / h264parse` | Depayloads and parses the incoming RTP/H.264 bitstream. | | [`v4l2h264dec`](../plugin-reference/v4l2h264dec) | Hardware-accelerated H.264 video decoder. | | `tee` | Splits each decoded stream into parallel branches for simultaneous AI inference and output. | | [`qtimlvconverter`](../plugin-reference/qtimlvconverter) | Prepares video frames for inference — handles resizing, YUV-to-RGB color space conversion, and pixel normalization. | | [`qtimltflite`](../plugin-reference/qtimltflite) | Executes the TFLite inference model on each frame using the Qualcomm NPU. | | [`qtimlpostprocess`](../plugin-reference/qtimlpostprocess) | Decodes raw output tensors into structured bounding boxes and class labels via a dynamically loaded module. | | [`qtimetamux`](../plugin-reference/qtimetamux) | Synchronizes inference results with the original video stream and attaches them as per-frame structured metadata. | | [`qtivoverlay`](../plugin-reference/qtivoverlay) | Renders bounding boxes and labels directly onto video frames for real-time visualization. | | [`qtivcomposer`](../plugin-reference/qtivcomposer) | Composes multiple annotated video streams into a single unified output frame. | | [`waylandsink`](../plugin-reference/waylandsink) | Renders the composited output to a local Wayland display. | | [`v4l2h264enc`](../plugin-reference/v4l2h264enc) / `h264parse` / `mp4mux` / `filesink` | Encodes and saves the output to a local file. | | [`qtirtspbin`](../plugin-reference/qtirtspbin) | Streams the output over RTSP for remote viewing. | ## How it works Each `rtspsrc` element receives an RTSP stream from an IP camera. The RTP/H.264 payload is depayloaded and parsed, then passed to a hardware-accelerated decoder (`v4l2h264dec`) to produce raw NV12 frames. A `tee` splits each decoded stream into two branches. The first branch feeds `qtimlvconverter` → `qtimltflite` → `qtimlpostprocess` for AI inference. The second branch feeds `qtimetamux` as the reference video. `qtimetamux` attaches inference results from the AI branch to the corresponding reference video frames, maintaining per-stream temporal alignment. `qtivoverlay` draws bounding boxes and labels onto each annotated stream. `qtivcomposer` tiles the annotated streams side-by-side (or in a configurable grid layout) into a single output frame. The composited frame is delivered to a Wayland display, saved to file, or streamed over RTSP. ## Run application on device ### Setup Requirements #### Hardware HW Setup | Component | Description | | - | - | | **Edge Device** | RB3 Gen 2, IQ8, or IQ9 — Primary processing unit for AI inference and video composition. | | **IP/RTSP Cameras** | One or more IP cameras accessible over the local network via RTSP. | | **HDMI Display Monitor** | Connected to the edge device for rendering the composited output. | | **PoE Switch** | Powers IP cameras and provides network connectivity over Ethernet. (Required for IP/RTSP camera setups.) | | **Local Network** | Ensures the edge device, IP cameras, and host machine are reachable on the same network. | #### Software **Flash your Qualcomm Edge device** by following the device setup and flashing instructions [here](../installation). **Once your device is ready**, follow the instructions below to set up the Multi-Stream IP Camera pipeline. ##### AI Model and config files | File | Download | Save as | | - | - | - | | YOLOv8 W8A8 model | [Qualcomm AI Hub — YOLOv8 Detection](https://aihub.qualcomm.com/iot/models/yolov8_det) | `yolov8_det_w8a8.tflite` | | Detection labels | yolov8.json | `yolov8.json` | | Sample video | Input video | `ai_demo_sample.mp4` | **Copy files to device** ```bash SCP (SSH) theme={null} # ~ resolves automatically to the correct home directory on the device (/root on QLI, /home/ubuntu on Ubuntu). # Run from your host machine — replace and ssh @ "mkdir -p ~/{models,labels,media,media/output}" scp yolov8_det_w8a8.tflite @:~/models/ scp yolov8.json @:~/labels/ scp ai_demo_sample.mp4 @:~/media/ ``` **Connect to device** ```bash theme={null} ssh @ ``` **Run the Multi-Stream IP Camera Application** A display must be connected to the device. If no display is available, use the `--no-display` flag to run in headless mode. Use the following base path for model and label files based on your OS: ```bash theme={null} gst-ip-camera \ --input-type=file \ --input-config=$HOME/media/ai_demo_sample.mp4 ``` ```bash theme={null} gst-ip-camera \ --input-type=file \ --input-config=$HOME/media/ai_demo_sample.mp4 \ --output-type=webrtc \ --output-config=wss://webrtc.nirbheek.in:8443 \ --webrtc-id=1010 ``` ```bash theme={null} gst-ip-camera \ --input-type=rtsp \ --input-config=rtsp://:/stream \ --output-type=rtsp \ --output-config=8900 ``` ```bash theme={null} gst-ip-camera \ --input-type=rtsp \ --input-config=rtsp://:/stream,rtsp://:/stream ``` > **Note:** This example uses an offline video file as input. To use an IP/RTSP camera or USB camera instead, update the `--input-type` argument accordingly — refer to the **Command-Line Options** section below for details. ## Visualize the Results - Host-Side Visualization (Windows + WSL) This section describes how to run the visualization client on a Windows host machine using **WSL (Windows Subsystem for Linux)**. The client renders the live composited video stream alongside a real-time AI metadata panel. 📥 The visualization client script can be downloaded here: rtsp\_webrtc\_client.zip It displays: * **Left panel** — Live composited video stream with AI overlays from all camera inputs. * **Right panel** — Real-time AI metadata (JSON): object detections, bounding boxes, and confidence scores per stream. **Step 1 — Install WSL and Ubuntu** If WSL is not already installed, run the following from a Windows terminal: ```bash theme={null} wsl --install Ubuntu-24.04 ``` Once installed, open the Ubuntu terminal and update the system: ```bash theme={null} sudo apt update && sudo apt upgrade -y ``` **Step 2 — Install System Dependencies** ```bash theme={null} sudo apt install -y \ python3 python3-pip python3-gi python3-gi-cairo \ gir1.2-gstreamer-1.0 \ gir1.2-gst-plugins-base-1.0 \ gir1.2-gst-plugins-bad-1.0 \ gstreamer1.0-tools \ gstreamer1.0-plugins-base \ gstreamer1.0-plugins-good \ gstreamer1.0-plugins-bad \ gstreamer1.0-plugins-ugly \ gstreamer1.0-libav \ python3-websocket \ libnice10 \ libnice-dev \ gstreamer1.0-nice ``` **Step 3 — Run the Visualization Client Script** ```bash theme={null} python3 rtsp_webrtc_client.py rtsp://:8900/live ``` ```bash theme={null} python3 rtsp_webrtc_client.py --source webrtc --signalling-server wss://webrtc.nirbheek.in:8443 --peer-id 1010 ``` **Step 4 — Expected Output** | Panel Content | Description | | - | - | | Left | Real-time composited video — all streams tiled into a single view with bounding boxes and labels | | Right | Live AI metadata panel — per-stream object detections, bounding boxes, and confidence scores | Expected Output ## Command-Line Options Selects the video input source for the pipeline. | Value | Description | | - | - | | `rtsp` | IP/RTSP camera stream. Requires `--input-config=rtsp://...`. | | `file` | Local H.264-encoded video file. Requires `--input-config=/path/to/ai_demo_sample.mp4`. | | `usb` | USB camera. Requires `--input-config=/dev/video0`. | | `isp` | Built-in ISP (on-device) camera. Optionally specify a camera ID via `--input-config=0`. | Specifies the input source configuration corresponding to the selected `--input-type`. | Input Type | Value | | - | - | | RTSP | `rtsp://` (comma-separated for multiple streams) | | File | `/path/to/ai_demo_sample.mp4` | | USB | `/dev/videoX` | | ISP | `` | Defines how the processed output video stream is delivered. | Value | Description | | - | - | | `display` | Renders composited output on a local Wayland display. | | `file` | Saves the encoded composited output to a file. Requires `--output-config`. | | `rtsp` | Streams composited output over RTSP. Requires `--output-config=`. | | `webrtc` | Streams composited output over WebRTC. Requires `--output-config=ws://...`. | | `none` | No video output (headless mode). | Specifies the output destination configuration corresponding to the selected `--output-type`. | Output Type | Value | | - | - | | File | `/path/to/output.mp4` | | RTSP | `` | | WebRTC | `ws://:` | Disables local on-screen rendering. Recommended for headless deployments and remote streaming setups. Specifies the root directory for AI model, label, and configuration files. | Asset Type | Resolved Path | | - | - | | Model files (`*.tflite`) | `/models/` | | Label/settings files (`*.json`) | `/labels/` | Sets the raw input video resolution and frame rate. Applicable only to ISP and USB inputs. ```bash theme={null} --width=1920 --height=1080 --framerate=30 ``` ## Build the Application * **Source code:** [gst-ip-camera](https://github.com/qualcomm/qimsdk/tree/main/gstreamer/gst-sample-apps/gst-ip-camera) * **Build instructions:** [Steps to build custom application](../advanced/yocto-build#steps-to-build-custom-application) ## Conclusion The QIM SDK enables developers to build scalable multi-stream IP camera applications without sacrificing performance or flexibility. By leveraging Qualcomm's dedicated hardware accelerators for decoding, inference, composition, and encoding — and by attaching structured AI metadata directly to each video frame — the SDK delivers a complete, production-ready foundation for multi-camera AI analytics at the edge. Whether the target use case is surveillance, retail analytics, or industrial monitoring, the pipeline can be configured to scale from a single stream to many concurrent feeds with minimal code changes. # Edge AI for Smart Manufacturing — Real-Time Bottle Counting Using Qualcomm QIM SDK Source: https://imsdkdocs.qualcomm.com/blogs/product-counting-application A YOLOv8-based object detection pipeline that counts products crossing a center region of interest in real time, built with Qualcomm QIM SDK and QNN HTP acceleration.
Computer Vision

A YOLOv8-based object detection pipeline that counts products crossing a center region of interest in real time, built with Qualcomm QIM SDK and QNN HTP acceleration.

QIM SDK Team · Aug 3, 2026 · ← All posts

## Introduction Counting products as they move along a conveyor belt or production line is a common but essential task in manufacturing, packaging, and warehouse environments. Manual counting is difficult to scale, while basic frame-by-frame object detection can easily over-count the same item as it appears across multiple consecutive frames. The Product Counting Application addresses this challenge with a Qualcomm QIM SDK pipeline that combines a YOLOv8 object detector with a center region-of-interest (ROI) counting strategy. Instead of counting every detection in every frame, the application defines a narrow vertical band in the center of the camera view. The count increases only when a tracked object's centroid passes through this band, similar to how a physical counting gate would operate on a production line. Inference runs fully on-device through the QNN HTP delegate, keeping the YOLOv8 TFLite model off the CPU and allowing the counting logic to keep pace with the live camera stream. Between detection and counting, a lightweight object tracker assigns each item a stable identity across frames, ensuring that each object is counted only once—even if it briefly leaves and re-enters the ROI. ## Use Case Overview A USB camera feed is captured and normalized to a fixed resolution, format, and frame rate before entering the inference branch. Frames are preprocessed and passed through a quantized YOLOv8 model on the QNN HTP delegate, producing per-frame bounding box detections. Detection metadata is synchronized with the video stream and delivered to the application through an `appsink` callback as JSON. Each detection's centroid is checked against a vertical counting band spanning the full frame height and the middle third of the frame width. A tracker matches detections to previously seen objects across frames and increments the count only once per confirmed object, supporting re-entry if an object leaves and comes back. The ROI band and running count are drawn onto the display branch alongside the YOLOv8 detection boxes, and the composited frame is shown on the connected display. ## Pipeline diagram Product Counting Pipeline ## Elements used in pipeline | Element | Description | | - | - | | `v4l2src` | Captures raw frames from the USB camera (`/dev/video2` by default). | | [`qtivtransform`](../plugin-reference/qtivtransform) | Formats the raw camera frames for the pipeline; a second instance later reformats frames for the display/overlay branch. | | Caps filter (`NV12`, `1920x1080`, `30/1`) | Fixes camera output to a known resolution, format, and frame rate for the rest of the pipeline. | | `tee` (`split`) | Splits the raw video into an inference branch and a pass-through branch carried into the metadata muxer. | | `queue` | Decouples and buffers data between branches; used multiple times throughout the pipeline. | | [`qtimlvconverter`](../plugin-reference/qtimlvconverter) | Resizes, converts color space, and normalizes frames ahead of inference. | | [`qtimltflite`](../plugin-reference/qtimltflite) | Runs the quantized YOLOv8 detection model through the QNN HTP external delegate (`libQnnTFLiteDelegate.so`). | | [`qtimlpostprocess`](../plugin-reference/qtimlpostprocess) | Decodes raw model output into structured detections using the `yolov8` module. | | `tee` (`post_split`) | Splits the detection-carrying stream again, feeding both the display path and the counting path. | | [`qtimetamux`](../plugin-reference/qtimetamux) | Synchronizes detection metadata with the corresponding video frame. | | [`qtivoverlay`](../plugin-reference/qtivoverlay) | Draws YOLOv8 bounding boxes and labels onto the video stream. | | [`qtimlmetaparse`](../plugin-reference/qtimetaparser) / [`qtimlmetaparser`](../plugin-reference/qtimetaparser) | Parses metadata into JSON. The application selects whichever element name is available at runtime. | | `appsink` (`count_sink`) | Delivers per-frame detection JSON to the counting logic (`emit-signals=true`, `sync=false`, `max-buffers=1`, `drop=true`). | | Caps filter (`BGRA`) | Reformats the display branch into a format Cairo can draw onto directly. | | `cairooverlay` (`roi_overlay`) | Renders the green ROI rectangle and the running "ROI Objects Counted" text onto the display branch. | | [`waylandsink`](../plugin-reference/waylandsink) | Displays the final composited output (`sync=true`, `async=true`). | ## How it works `v4l2src` captures the raw camera feed, which is normalized to a fixed NV12 resolution and frame rate before a `tee` splits it into an inference branch and a reference branch. The inference branch runs `qtimlvconverter` to prepare frames, then `qtimltflite` executes the quantized YOLOv8 model (`/etc/models/yolov8_det_w8a8.tflite`) via the QNN HTP delegate. `qtimlpostprocess`, using the `yolov8` module, decodes the raw output into bounding boxes, and `qtimetamux` merges the detections back onto the reference video frame. A second `tee` sends the metadata-carrying stream down both a display branch and a metadata branch. The metadata branch parses detections into JSON and hands them to the `count_sink` `appsink` callback for every frame. Because the detection-list and bounding-box key names can vary depending on the post-processing module version, the callback normalizes incoming JSON, checking a set of known key aliases for the detection list and for the bounding box, before extracting label, confidence, and rectangle values. The counting logic defines a vertical band covering the full frame height and the middle third of the frame width, then tests whether each detection's centroid falls inside that band. A tracker matches each frame's detections against previously tracked objects using centroid distance, bounding-box overlap, label agreement, and simple motion prediction. It smooths bounding boxes, tolerates brief detection dropouts, and increments the count only once an object is confirmed inside the ROI, while still allowing the same physical slot to be counted again if an object exits and re-enters. The display branch is reformatted to BGRA, and `cairooverlay` draws the ROI rectangle and the current count on top of the `qtivoverlay`-annotated frame before `waylandsink` renders the result. ## Setup Requirements ### Hardware HW Setup | Component | Description | | - | - | | **Edge Device** | RB3 Gen 2, IQ8, or IQ9. Runs the YOLOv8 detection inference and counting logic. | | **Camera Source** | USB camera by default; an IP/RTSP camera, ISP camera, or local video file can be substituted. | | **HDMI Display Monitor** | Connected to the edge device to show the live detection and counting overlay. | | **PoE Switch / Local Network** | Required only when using an IP/RTSP camera or streaming results over the network. | ### Software Flash your Qualcomm Edge device by following the device setup and flashing instructions [here](../installation), then install the Python and GStreamer prerequisites (`python3`, `gstreamer1.0`, `python3-gi`) needed to run the pipeline. The YOLOv8 detection model and its labels are expected at `/etc/models/yolov8_det_w8a8.tflite` and `/etc/labels/yolov8.json` respectively. Check application source code on GitHub: [`demo_product_counting/main.cc`](https://github.com/qualcomm/qimsdk/blob/main/cpp/examples/demo-apps/demo_product_counting/main.cc) Pre-built application on device: `/usr/bin/qimsdk_demo_product_counting` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` ```bash SCP (SSH) theme={null} # Replace and with your device credentials. ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the `--input-config` value below if your camera is exposed on a different node). ```bash theme={null} /usr/bin/qimsdk_demo_product_counting --input-config /dev/video2 ``` The application overlays YOLOv8 detection bounding boxes on the live USB camera feed and displays a running "ROI Objects Counted" total whenever a detected object's centroid crosses into the defined region of interest. To stop the application, press **CTRL + C**. Check application source code on GitHub: [`qimsdk_demo_product_counting.py`](https://github.com/qualcomm/qimsdk/blob/main/python/examples/demo-apps/qimsdk_demo_product_counting.py) Pre-built application on device: `/usr/bin/qimsdk_demo_product_counting.py` #### Download Required Files | File | Save as | | - | - | | [Model](https://aihub.qualcomm.com/iot/models/yolov8_det) | yolov8\_det\_quantized.tflite | | yolov8.json | yolov8.json | If the downloaded model file is a `.zip` archive, extract it on your host machine before copying: `unzip filename.zip` ```bash SCP (SSH) theme={null} # Replace and with your device credentials. ssh @ "mkdir -p ~/Downloads/qimsdk_samples/{models,labels}" scp yolov8_det_quantized.tflite @:~/Downloads/qimsdk_samples/models/ scp yolov8.json @:~/Downloads/qimsdk_samples/labels/ ``` Connect a USB (UVC) camera to the target device and verify it is exposed as `/dev/video2` (update the `--input-config` value below if your camera is exposed on a different node). ```bash theme={null} python3 /usr/bin/qimsdk_demo_product_counting.py --input-config /dev/video2 ``` The application overlays YOLOv8 detection bounding boxes on the live USB camera feed and displays a running "ROI Objects Counted" total whenever a detected object's centroid crosses into the defined region of interest. To stop the application, press **CTRL + C**. ## Application Function Breakdown The Product Counting application is organized into a handful of function groups, each responsible for one stage of the detection-to-count flow: * **Metadata parsing**: normalizes incoming JSON keys and extracts label, confidence, and bounding-box values regardless of which detection-list or bbox key names the post-processing module emits. * **ROI and counting**: computes the center ROI band from the frame dimensions and tests whether a detection's centroid falls inside it. * **Object tracking**: a cumulative tracker that assigns stable IDs, matches detections across frames using centroid distance, IoU, label, and motion prediction, smooths bounding boxes, tolerates brief dropouts, and prevents duplicate counts while still supporting re-entry counting. * **Rendering**: draws the ROI rectangle and the current count text onto the display branch each frame. * **Callback and pipeline utility**: the `appsink` callback that receives each frame's metadata, a helper that selects the correct metadata-parser element name at runtime, and setup logic that wires the runtime hooks together before the pipeline starts. ## Expected Output