NOTE: The Jupyter Notebook below is included in the Chimera SDK and can be run interactively by running the following CLI command:
$ quadric sdk notebook
From the Jupyter Notebook window in your browser, select the notebook named /quadric/sdk-cli/examples/models/blazepose/blazepose.ipynb.
BlazePose Pose Estimation on Chimera GPNPU
BlazePose is a two-stage, real-time body-pose pipeline from MediaPipe: a lightweight pose-proposal detector locates the person and frames an ROI, then a landmark network regresses 33 body keypoints inside that ROI. This notebook walks through the full two-stage pipeline using the pre-quantized INT8 ONNX models shipped with the SDK.
Why two stages?
A single-stage pose network has to cover the full frame at full resolution, which is wasteful when most pixels are background. BlazePose spends a small network once on coarse person detection, then crops and aligns the ROI so the (larger) landmark network only processes the person, at a pose-centric resolution.
Model: BlazePose (MediaPipe, INT8, detector 128×128 + landmark 256×256)
Note — this notebook demonstrates the BlazePose detector on the Chimera GPNPU.
- On the Chimera GPNPU: the pose detector (compiled with CGC, run through the simulator)
- On the host CPU: the pose landmark network (run through ONNX Runtime as the second-stage reference)
For end-to-end on-chip pipelines that compose CCL custom ops with ChiPy to keep every stage on the GPNPU, see
yolox_e2e_chipy.ipynborretina_net_e2e_chipy.ipynb.
1. Setup
Imports and per-run configuration. All heavy logic (paths, constants, preprocessing, ProposeROI glue, visualization) lives in blazepose_helpers.py.
import matplotlib.pyplot as plt
import onnx
from tvm.contrib.epu.chimera_job.chimera_job import ChimeraJob
from tvm.contrib.epu.chimera_job.hw_config import HWConfig
from blazepose_helpers import (
ANCHORS_PATH,
DETECTOR_INPUT_SIZE,
DETECTOR_MODEL_PATH,
LANDMARK_INPUT_SIZE,
LANDMARK_MODEL_PATH,
draw_pose_result,
load_input,
preprocess_pose_input,
propose_roi_from_detector,
)
product = "QC-U"
%matplotlib inline
2. Model Inputs
Two pre-quantized INT8 ONNX models ship with the notebook — a small BlazePose detector and the larger BlazePose landmark network — plus a .npy file of the 896 SSD-style anchors the detector was trained against.
from pathlib import Path
## Sanity-load the models so any I/O error surfaces early.
onnx.checker.check_model(onnx.load(DETECTOR_MODEL_PATH))
onnx.checker.check_model(onnx.load(LANDMARK_MODEL_PATH))
print(
f"Detector: {Path(DETECTOR_MODEL_PATH).name} | "
f"Landmark: {Path(LANDMARK_MODEL_PATH).name} | "
f"Anchors: {Path(ANCHORS_PATH).name}"
)
Detector: blazepose_opt_int8_q.onnx | Landmark: blazepose-landmark-sim_opt_sym_int8_q.onnx | Anchors: anchors_pose.npy
3. Preprocessing
Both networks expect a resize-and-pad preprocess that preserves aspect ratio: the image is scaled to fit inside the target box and black-padded to reach the exact network input size. The scale and pad offsets are retained and fed into ProposeROI later so we can project detections back into original-image coordinates.
image = load_input()
original_image, detector_input, scale, pad = preprocess_pose_input(image, DETECTOR_INPUT_SIZE)
## A second pass at landmark resolution is used only to refresh the scale/pad we
## carry into ProposeROI — the landmark network itself consumes the ROI crop.
_, _, scale, pad = preprocess_pose_input(image, LANDMARK_INPUT_SIZE)
print(f"Original image: {original_image.shape}")
print(f"Detector input: {detector_input.shape}, dtype={detector_input.dtype}")
print(f"Scale: {scale:.4f} Pad: {pad}")
Original image: (3, 5370, 6975)
Detector input: (1, 3, 128, 128), dtype=float32
Scale: 27.2589 Pad: (790, 0)
4. Compile the Detector with CGC
The detector is the only stage we compile here. It's wrapped in a ChimeraJob and built with the Chimera Graph Compiler; the same job object will be used to run inference on the Chimera GPNPU via the simulator.
hw = HWConfig(product=product, ocm_size="8MB")
detector_job = ChimeraJob(model_p=DETECTOR_MODEL_PATH, hw_config=hw)
detector_job.analyze_network()
detector_job.compile(quiet=True)
print(detector_job)
2026-06-19 03:53 - INFO - epu - chimera_job - START==================================onnx_ingest
2026-06-19 03:53 - INFO - epu - codegen - START===============================optimize_relay
2026-06-19 03:53 - INFO - epu - codegen - START====================quantize_to_cpu_runnable_fx
2026-06-19 03:53 - INFO - epu - fx -
Source name Op Output 0 Range Output 0 Frac Bits
-------------------- ---------------------- ----------------------------------------- --------------------
303_DequantizeLinear contrib.epu.dequantize (-113.10921478271484, 112.22554904222488) 24
284_DequantizeLinear contrib.epu.dequantize (-478.6881103515625, 474.9483594894409) 22
Analysis of /quadric/sdk-cli/examples/models/blazepose/onnx/blazepose_opt_int8_q.onnx
╒═════════════════════╤═══════════════════════════════════════════════════════════════════════════╕
│ Module Name │ blazepose_opt_int8_q_QC_U_1d7_8MB_4kB_128GBps_128GBps_16_OFF_x1_x1 │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ ONNX File │ /quadric/sdk-cli/examples/models/blazepose/onnx/blazepose_opt_int8_q.onnx │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ Product Target │ QC-U │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ Number of Cores │ 1 │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ ISS Clock Frequency │ 1.700 │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ L2M Size │ 8MB │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ LRM Size │ 4kB │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ External Read BW │ 128GBps │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ External Write BW │ 128GBps │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ MACS per PE │ 16 │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ Max L2M │ 2.032MB │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ Max LRM │ 0.500kB │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ Max Temp Ext Bytes │ 0.000MB │
├─────────────────────┼───────────────────────────────────────────────────────────────────────────┤
│ Network GMACs │ 0.493 │
╘═════════════════════╧═══════════════════════════════════════════════════════════════════════════╛
╒════╤════════╤═════════╤══════════════════╤══════════════════════════╤═══════╕
│ │ Type │ Name │ shape │ type │ mse │
╞════╪════════╪═════════╪══════════════════╪══════════════════════════╪═══════╡
│ 0 │ Input │ input.1 │ [1, 3, 128, 128] │ tensor[FixedPoint32<27>] │ n/a │
├────┼────────┼─────────┼──────────────────┼──────────────────────────┼───────┤
│ 1 │ Output │ 303 │ [1, 896, 12] │ tensor[FixedPoint32<24>] │ n/a │
├────┼────────┼─────────┼──────────────────┼──────────────────────────┼───────┤
│ 2 │ Output │ 284 │ [1, 896, 1] │ tensor[FixedPoint32<22>] │ n/a │
╘════╧════════╧═════════╧══════════════════╧══════════════════════════╧═══════╛
5. Stage 1 — Pose Detector (GPNPU)
The detector predicts per-anchor box regressions and classification scores. We run it on the Chimera GPNPU through the simulator; the harness also cross-checks the GPNPU outputs against an ONNX Runtime reference and reports per-output MSE.
detector_out = detector_job.run_inference_harness(inputs={"input.1": detector_input})
box_tensor, score_tensor = detector_out["303"].copy(), detector_out["284"].copy()
print(f"Boxes: {box_tensor.shape} Scores: {score_tensor.shape}")
2026-06-19 03:57 - INFO - epu - iss_testing - No tranges found for input, use default float range: <tvm.contrib.epu.interval.Interval object at 0x7016bef67e50>
FILM 15/15: 100%|███████████████████████████████████████████████████| 15/15 [00:11<00:00, 1.34it/s]
2026-06-19 03:58 - INFO - epu - iss_testing - No tranges found for input, use default float range: <tvm.contrib.epu.interval.Interval object at 0x7016bef67e50>
2026-06-19 03:58 - INFO - epu - iss_testing - Started Executing Onnxruntime...
2026-06-19 03:58 - INFO - epu - iss_testing - Done 0:00:00.175579
Boxes: (1, 896, 12) Scores: (1, 896, 1)
6. Stage 2 — Pose Landmark
Before invoking the landmark network, we crop the detector's ROI from the original image and align it to the landmark network's canonical 256×256 input orientation. The landmark network then consumes that crop and regresses 33 3D body keypoints plus a presence flag.
roi_out = propose_roi_from_detector(
original_image=original_image,
box_tensor=box_tensor,
score_tensor=score_tensor,
scale=scale,
pad=pad,
)
landmark_input = roi_out["landmark_input"]
denormalized_detections = roi_out["denormalized_detections"]
affine = roi_out["affine"]
box = roi_out["box"]
print(f"Landmark input: {landmark_input.shape}, dtype={landmark_input.dtype}")
Landmark input: (1, 3, 256, 256), dtype=float32
landmark_job = ChimeraJob(model_p=LANDMARK_MODEL_PATH, hw_config=hw)
landmark_out = landmark_job.run_onnx_inf_session(inputs={"onnx::Pad_0": landmark_input})
flags, normalized_landmarks = landmark_out["984"], landmark_out["997"]
print(f"Flags: {flags.shape}")
print(f"Keypoints: {normalized_landmarks.shape}")
2026-06-19 03:58 - INFO - epu - chimera_job - START==================================onnx_ingest
2026-06-19 03:58 - INFO - epu - iss_testing - No tranges found for input, use default float range: <tvm.contrib.epu.interval.Interval object at 0x7015758f1a50>
2026-06-19 03:58 - INFO - epu - iss_testing - Started Executing Onnxruntime...
2026-06-19 03:58 - INFO - epu - iss_testing - Done 0:00:00.190086
Flags: (1,)
Keypoints: (1, 31, 4)
Overlay the detected 33-keypoint skeleton on the original frame.
pose_image = draw_pose_result(
original_image=original_image,
flags=flags,
denormalized_detections=denormalized_detections,
normalized_landmarks=normalized_landmarks,
affine=affine,
box=box,
)
plt.figure(figsize=(9, 9))
plt.imshow(pose_image)
plt.axis("off")
plt.show()

Summary
| Model | BlazePose (MediaPipe, INT8 ONNX) |
| Pipeline | Detector (128×128, GPNPU) → ROI crop → Landmark (256×256) → pose overlay |
| On GPNPU | Detector (compiled with CGC, run through the simulator) |
Key takeaways
- Two-stage BlazePose trades a small person-detection network for a pose-centric, aligned ROI — giving the larger landmark network higher effective resolution per body than a single-stage pipeline would.
ChimeraJobis the consistent entry point for both stages:run_inference_harnesscompiles and runs on the GPNPU;run_onnx_inf_sessionruns the same model through ONNX Runtime when used as a reference.- For an end-to-end on-chip pipeline that composes CCL custom ops with ChiPy and keeps every stage on the GPNPU, see
yolox_e2e_chipy.ipynbandretina_net_e2e_chipy.ipynb.
Citation
@article{bazarevsky2020blazepose,
title = {BlazePose: On-device Real-time Body Pose tracking},
author = {Bazarevsky, Valentin and Grishchenko, Ivan and Raveendran, Karthik and
Zhu, Tyler and Zhang, Fan and Grundmann, Matthias},
journal = {arXiv preprint arXiv:2006.10204},
year = {2020}
}