> ## Documentation Index
> Fetch the complete documentation index at: https://dragonwingdocs.qualcomm.com/llms.txt
> Use this file to discover all available pages before exploring further.

# 第 3 部分：使用 Qualcomm 工具逐步实现球拍检测

> 在 IQ-8275 EVK 上的完整手动流水线：数据集、训练、ONNX 导出、QAIRT 量化，以及在 NPU 上实时运行的常驻 C++ 守护进程。

<div style={{ marginBottom: "2rem" }}>
  <div
    style={{
fontSize: "0.72rem",
fontWeight: 700,
color: "#31017D",
letterSpacing: "1.5px",
textTransform: "uppercase",
marginBottom: "0.5rem"
}}
  >
    Qualcomm Linux · Edge AI · Deep Dive
  </div>

  <p style={{ fontSize: "0.95rem", color: "#555", lineHeight: 1.7, margin: "0 0 0.75rem" }}>
    完整的手动流水线：每一条命令、每一个脚本、每一个设计决策的解释，从第一张照片到以 2 ms 在 Qualcomm NPU 上实时运行的 C++ 守护进程。
  </p>

  <div style={{ fontSize: "0.85rem", color: "#888", display: "flex", gap: "0.5rem", flexWrap: "wrap", alignItems: "center" }}>
    <a href="https://www.linkedin.com/in/raulrosettomunoz/" target="_blank" rel="noopener noreferrer" style={{ color: "#888", textDecoration: "none" }}>Raul Muñoz</a>
    <span>·</span>
    <span>2026 年 8 月 10 日</span>
    <span>·</span>
    <a href="/zh/tutorials/paddle-npu-story" style={{ color: "#31017D", fontWeight: 600, textDecoration: "none" }}>← 完整故事</a>
  </div>
</div>

<hr style={{ border: "none", borderTop: "1px solid #eee", margin: "0 0 2rem" }} />

这是不走捷径的版本。每条命令都在这里。每个脚本都在[配套文件页面](/zh/tutorials/paddle-npu-story-files)上，请从那里将它们复制到所示路径中。步骤 1-6 在 Mac 和开发板上运行。步骤 7-9（NPU 部分）需要免费的 QAIRT（Qualcomm AI Runtime SDK）和一台 x86 Linux 机器。

四个阶段，让你始终清楚自己在哪台机器上操作：

| 阶段          | 发生什么               | 在哪里                    |
| ----------- | ------------------ | ---------------------- |
| 获取数据        | 采集照片、绘制边界框         | Edge Impulse（浏览器）      |
| 在 Mac 上训练   | 微调 YOLOv8n，导出 ONNX | Mac（PyTorch + MPS）     |
| 在 CPU 上实时运行 | 摄像头 → 检测 → 浏览器     | Mac，然后是开发板 CPU         |
| 在 NPU 上加速   | 转换模型，在 AI 芯片上运行    | x86 Linux（转换）+ 开发板（运行） |

***

## 你需要什么

**硬件：** 一台用于训练的计算机 — 本指南使用配备 Apple Silicon 的 Mac（PyTorch MPS（Metal Performance Shaders）后端），但配备 CUDA GPU（Graphics Processing Unit）的 Linux 或 Windows 同样适用；只需在训练命令中将 `--device mps` 替换为 `--device cuda`。IQ-8275 EVK（QCS8300，Hexagon V75 NPU），aarch64 Qualcomm Linux，Qualcomm Linux 镜像上已预装 Python 3.x 和 `onnxruntime`。一个用于实时演示的 USB 摄像头。一台用于 QAIRT SDK 的 x86-64 Linux 机器（NPU 编译器仅支持 x86）。

**软件：** Mac 上的 Python 3 和 `git`。x86 Linux 机器上的免费 **QAIRT SDK v2.47.0.260601**。要构建实时 C++ 守护进程：一个 aarch64 交叉编译器（`aarch64-linux-gnu-g++-13`）。

**脚本：** 下面引用的所有脚本都在[配套文件页面](/zh/tutorials/paddle-npu-story-files)上。将每个脚本复制到其头部所示的路径。

***

## 步骤 1：构建数据集

数据集是一切的基础。你需要球拍的照片，每张照片上都绘制了边界框，还需要没有球拍的背景帧。

用于此目的的工具是 [Edge Impulse Studio](https://studio.edgeimpulse.com)。其 Data acquisition 标签页允许你从连接的设备录制图像、在浏览器中绘制边界框，并以其自有格式导出。本项目中约 670 张图像就是这样标注的，无需外部工具。如果你已先完成了 [Edge Impulse 快速入门](/zh/tutorials/edge-impulse-quickstart)，那么你已经拥有此数据集 — 直接跳到下面的导出步骤。

以 **Object Detection** 格式导出：在 Studio 中前往 **Dashboard → Export → Object Detection format**。选择能够为每个数据分割提供一个图像文件夹外加一个 `bounding_boxes.labels` JSON 文件的格式。追求多样性：不同的距离、光照、角度和房间。包含约 15–20% 的背景帧。

最终你会得到：

```
pingpong-export/
  training/
    <image>.jpg ...
    bounding_boxes.labels
  testing/
    <image>.jpg ...
    bounding_boxes.labels
```

边界框以**绝对像素**表示，`x,y` 位于**左上角**。这是 `training/labels.py` 期望的约定。

***

## 步骤 2：Mac 环境

创建一个工作文件夹并将脚本复制进去：

```
my-project/
  pingpong-export/        ← your dataset from Step 1
  training/
    labels.py             ← from companion files
    preprocess.py         ← from companion files
    dataset.py            ← from companion files
    model.py              ← from companion files
    train.py              ← from companion files
    export_onnx.py        ← from companion files
    requirements.txt      ← from companion files
  yolo/
    prep_yolo.py          ← from companion files
    train_yolo.py         ← from companion files
    export_yolo.py        ← from companion files
    gen_calib.py          ← from companion files
    requirements.txt      ← from companion files
  web/
    infer.py              ← from companion files
    infer_yolo.py         ← from companion files
    infer_npu.py          ← from companion files
    server.py             ← from companion files
    bench_cpu_vs_npu.py   ← from companion files
```

检查数据集。你应该看到 539 张训练图像和 124 张测试图像：

```bash theme={null}
python3 training/labels.py
# [training] 539 images  |  with paddle: 295  |  background: 244
# [testing] 124 images   |  with paddle: 76   |  background: 48
```

我们有意使用两个独立的 venv：Phase A（精简的 PyTorch）和 Phase B（Ultralytics）。将它们分开意味着在安装较重的 YOLO 技术栈之后，Phase A 仍然保持可复现。

```bash theme={null}
# Phase A venv
python3 -m venv training/.venv
training/.venv/bin/python -m pip install -r training/requirements.txt
```

***

## 步骤 3：Phase A，从零开始的 CNN（有启发性的基线）

这一步从零构建一个小型网络，仅使用你的球拍照片进行训练。它的表现会不佳。这正是重点所在。它是解释 Phase B 为何有效的对照组。

### 3.1 预处理

```bash theme={null}
training/.venv/bin/python training/preprocess.py
```

对每张 JPEG 只解码一次，缩放到 320×320，并将结果缓存为 `.npy` 数组。缓存在训练时以内存映射方式读取。在一次训练运行中将 670 张图像解码 40 次需要几分钟；而从内存映射数组读取几乎没有开销。

### 3.2 训练

```bash theme={null}
training/.venv/bin/python training/train.py --epochs 20
```

可用时使用 MPS（Metal GPU 后端），否则使用 CPU。关注 `val_IoU` 列。这是诚实的衡量指标（Intersection over Union：预测框与真实框的重叠程度，0 = 完全未命中，1 = 完美）。它最高达到约 **0.56**。最佳检查点保存到 `training/checkpoints/best.pt`。

### 3.3 导出到 ONNX

```bash theme={null}
training/.venv/bin/python training/export_onnx.py \
    --ckpt training/checkpoints/best.pt --out training/checkpoints/best.onnx
```

### 3.4 观察它的失败

```bash theme={null}
training/.venv/bin/python web/server.py --model cnn
# open http://localhost:8080, pick your camera, hit start
```

凑近镜头，调暗房间。CNN 找不到球拍。把你的脸从画面中裁掉，它突然就能工作了。调亮图像，分数就会上升。这个模型记住了训练相册：光线良好、人站得较远的照片。这就是 Phase B 的动机。

***

## 步骤 4：Phase B，微调 YOLOv8n（真正有效的那个）

### 4.1 环境

```bash theme={null}
python3 -m venv yolo/.venv
yolo/.venv/bin/python -m pip install -r yolo/requirements.txt
```

### 4.2 将标签转换为 YOLO 格式

```bash theme={null}
yolo/.venv/bin/python yolo/prep_yolo.py
yolo/.venv/bin/python yolo/prep_yolo.py --check   # draws boxes back to verify conversion
```

### 4.3 微调

```bash theme={null}
yolo/.venv/bin/python yolo/train_yolo.py --epochs 80 --device mps
```

首次运行时会下载 COCO 预训练的 YOLOv8n 权重。微调是给一个已经知道"手中物体"长什么样的模型再增加"球拍"这一个词。结果：`yolo/runs/paddle/weights/best.pt`，**mAP\@0.5 ≈ 0.979**。

### 4.4 导出到 ONNX

```bash theme={null}
yolo/.venv/bin/python yolo/export_yolo.py
```

导出时使用 `nms=False`（NMS，即 Non-Maximum Suppression（非极大值抑制），位于 numpy 中而非计算图中）和 `dynamic=False`（固定 1x3x320x320，NPU 要求固定形状）。正是这两个标志让步骤 7-9 能够顺利进行。

***

## 步骤 5：浏览器中的实时演示（Mac）

```bash theme={null}
yolo/.venv/bin/python web/server.py --model yolo
# open http://localhost:8080, pick camera, hit start
```

服务器打开摄像头，将每一帧送入检测器，绘制边界框，并将 MJPEG 流传输到浏览器。稍后同样的 `--model npu` 标志将接入 NPU 引擎，无需修改服务器代码。

***

## 步骤 6：在开发板的 CPU 上运行

```bash theme={null}
BOARD_IP=192.168.15.86   # your board's IP

ssh root@$BOARD_IP 'mkdir -p /opt/pingpong/yolo /opt/pingpong/web'
scp yolo/best.onnx  root@$BOARD_IP:/opt/pingpong/yolo/
scp web/*.py        root@$BOARD_IP:/opt/pingpong/web/

ssh root@$BOARD_IP
cd /opt/pingpong
python3 web/server.py --model yolo --cameras 26 --backend v4l2 --port 8080
# open http://<board-ip>:8080 from any machine on the network
```

开发板上已安装 `onnxruntime`。这样你能在 **CPU 上获得大约 24 fps**，这是使用 NPU 之前的基线。

<Warning>
  如果 USB 摄像头从 `lsusb` 中消失且 `/dev/video26` 不见了，热插拔无法恢复它。请重启开发板。
</Warning>

***

## 步骤 7：为 NPU 转换模型

此步骤在 **x86-64 Linux 机器**上运行。NPU 编译器仅支持 x86。

从[配套文件页面](/zh/tutorials/paddle-npu-story-files)将这些脚本复制到 x86 机器上的 `npu/` 文件夹：

```
npu/
  env.sh              ← from companion files (edit SDK path here)
  convert_dlc.sh      ← from companion files
  requant_a16w8.sh    ← from companion files
```

### 7.0：安装 QAIRT SDK（一次性）

选择一个有约 4 GB 可用空间的工作目录。将其导出为 `QW`。当设置了此变量时，`env.sh` 文件会自动从中派生所有其他路径：

```bash theme={null}
export QW=/path/to/your/workdir   # edit once; everything below derives from it

mkdir -p "$QW" && cd "$QW"

# Download the Community edition (no login required):
wget https://softwarecenter.qualcomm.com/api/download/software/sdks/Qualcomm_AI_Runtime_Community/All/2.47.0.260601/v2.47.0.260601.zip
unzip v2.47.0.260601.zip   # creates ./qairt/2.47.0.260601/
```

创建 Python venv（使用 `virtualenv`，因为在许多没有 sudo 权限的 Ubuntu 环境中内置的 `python3 -m venv` 是坏的）：

```bash theme={null}
pip install --user --break-system-packages virtualenv
python3 -m virtualenv .venv
source .venv/bin/activate
```

安装确切可用的依赖版本（这些版本锁定源自真实的故障，而非猜测）：

```bash theme={null}
python3 qairt/2.47.0.260601/bin/check-python-dependency
pip install "numpy==1.26.4" "onnx==1.16.1" "onnxruntime==1.18.1"
# numpy 2.x breaks SDK native code; onnx 1.22 drops an attribute the converter reads
```

准备 SDK 原生工具所需的 LLVM 运行时库（干净的 Ubuntu 不自带它们）：

```bash theme={null}
cd /tmp
apt-get download libc++1-18 libc++abi1-18 libunwind-18
for d in libc++1-18_*.deb libc++abi1-18_*.deb libunwind-18_*.deb; do
  dpkg-deb -x "$d" "$QW/llvm-libs"
done
cd -
```

如果 `$QW` 已在你的 shell 中导出，你无需编辑任何内容。`env.sh` 文件会自动从中派生所有路径：

```bash theme={null}
source npu/env.sh
qairt-converter --version   # confirm the tools are on PATH
```

完整的 `env.sh` 在[配套文件页面](/zh/tutorials/paddle-npu-story-files#env-sh)上。

### 7.1：将模型和校准数据复制到 x86 机器

先在 Mac 上生成校准数据：

```bash theme={null}
# on the Mac
yolo/.venv/bin/python yolo/gen_calib.py
# -> yolo/calib/calib_0000.raw ... calib_0199.raw  (float32 NCHW, 320x320)
# -> yolo/calib/input_list.txt
```

然后复制到 x86 机器：

```bash theme={null}
# on the Mac — replace user@x86-box and the remote path
rsync -av yolo/best.onnx yolo/calib user@x86-box:/path/to/workdir/
```

### 7.2：ONNX 到浮点 DLC

```bash theme={null}
# on the x86 box
cd "$QW"
bash npu/convert_dlc.sh   # reads $WORK/best.onnx -> writes $WORK/best_fp.dlc
```

### 7.3：量化为 A16W8

这是微妙的部分。将所有内容量化为 INT8（8 位整数）会使置信度分数坍缩为零。边界框坐标是大数字（比如 400 像素），置信度分数很小（0.87），同一个 8 位刻度无法同时表示两者。解决方案是 **A16W8**：将权重保持为 8 位（紧凑），但让激活使用 16 位精度以保护分数。完整解释见[故事的第 6 部分](/zh/tutorials/paddle-npu-story#npu-quantization-trap)。

```bash theme={null}
bash npu/requant_a16w8.sh
# reads best_fp.dlc + calib/ -> writes best_a16w8.dlc + ctx16/best_a16w8_htpv75.bin
```

生成的 `best_a16w8_htpv75.bin` 是上下文二进制文件，为 HTP（Hexagon Tensor Processor）V75 提前编译。将其复制到开发板：

```bash theme={null}
ssh root@$BOARD_IP 'mkdir -p /home/weston/npu'
scp "$QW/ctx16/best_a16w8_htpv75.bin" root@$BOARD_IP:/home/weston/npu/
```

***

## 步骤 8：在 NPU 上运行

### 8.1：单次测试

开发板的 `/usr/lib` 中已经有 QNN 运行时。你不需要从 SDK 复制任何 `.so` 文件。只需复制上下文二进制文件（上面已完成）以及来自[配套文件页面](/zh/tutorials/paddle-npu-story-files#run-npu16-sh)的 `run_npu16.sh`。

`run_npu16.sh` 期望测试输入位于 `/home/weston/npu/emeet2_input.raw` — 一个原始 float32 NCHW 张量（形状 `1×3×320×320`）。在 Mac 上使用与模型训练时相同的 letterbox 预处理，从任意 JPEG 生成它：

```bash theme={null}
# on the Mac, inside the project venv
python3 - <<'EOF'
import cv2, numpy as np, sys
img = cv2.imread("path/to/any_frame.jpg")          # any photo will do for a smoke-test
from web.infer_yolo import _letterbox, IMG_SIZE
canvas, _, _, _ = _letterbox(img, IMG_SIZE)
rgb = cv2.cvtColor(canvas, cv2.COLOR_BGR2RGB)
chw = (rgb.astype("float32") / 255.0).transpose(2, 0, 1)[None]
chw.tofile("emeet2_input.raw")
print("wrote emeet2_input.raw")
EOF

scp emeet2_input.raw root@$BOARD_IP:/home/weston/npu/
```

然后运行单次测试：

```bash theme={null}
ssh root@$BOARD_IP "bash /home/weston/npu/run_npu16.sh"
```

此处每次调用看到的约 150-200 ms 包含进程启动开销。使用常驻守护进程后，这一开销会消失。

### 8.2：通过常驻 C++ 守护进程进行实时流传输

对于实时使用，你需要让模型常驻：加载一次，持续处理帧。守护进程将 QNN 上下文保存在内存中，从 FIFO 管道读取帧，运行推理，并通过 HTTP 传输 MJPEG 流。

构建它需要在 x86 机器上有一个 aarch64 交叉编译器。将 `npu/env.sh` 中的 `R` 设置为交叉编译器根目录（包含 `usr/bin/aarch64-linux-gnu-g++-13` 的目录）。

守护进程是 QNN SDK SampleApp 之上的一个小型覆盖层。先从 SDK 创建三个覆盖文件，然后应用来自[配套文件页面 — 守护进程 C++ 源代码部分](/zh/tutorials/paddle-npu-story-files#daemon-cpp-source)的差异补丁：

```bash theme={null}
# on the x86 box, inside $QW
source npu/env.sh
mkdir -p npu/daemon

cp "$SDK/examples/QNN/SampleApp/SampleApp/src/main.cpp"        npu/daemon/main.cpp
cp "$SDK/examples/QNN/SampleApp/SampleApp/src/QnnSampleApp.cpp" npu/daemon/QnnSampleApp.cpp
cp "$SDK/examples/QNN/SampleApp/SampleApp/src/QnnSampleApp.hpp" npu/daemon/QnnSampleApp.hpp
```

将配套页面的差异补丁应用到这三个文件，然后构建并复制守护进程：

```bash theme={null}
# on the x86 box, inside $QW
bash npu/build_base.sh    # builds QnnSampleApp base libs
bash npu/build_daemon.sh  # links qnn-daemon-aarch64

scp "$QW/daemon/qnn-daemon-aarch64" root@$BOARD_IP:/home/weston/npu/
```

在开发板上：

```bash theme={null}
ssh root@$BOARD_IP
python3 /opt/pingpong/web/server.py --model npu --cameras 26 --backend v4l2
# open http://<board-ip>:8080
```

`server.py --model npu` 会自动将守护进程作为子进程启动，并通过两个 FIFO 与其通信：`/tmp/npu_cmd.fifo`（命令）和 `/tmp/npu_resp.fifo`（响应）。无需单独的守护进程启动步骤。

<img src="https://mintcdn.com/qualcomm-prod/ZRoYdq-twSwPVBFY/tutorials/img/paddle-npu/yolo_live.png?fit=max&auto=format&n=ZRoYdq-twSwPVBFY&q=85&s=e37a800e88519a10570af48028331682" alt="YOLOv8n 通过常驻 NPU 守护进程在 IQ-8275 EVK 上实时运行" width="1089" height="765" data-path="tutorials/img/paddle-npu/yolo_live.png" />

***

## 步骤 9：诚实地对比 CPU 与 NPU 基准

```bash theme={null}
# from your Mac/workstation — copy a test frame to the board first
scp path/to/any_frame.jpg root@$BOARD_IP:/tmp/emeet2.jpg
```

```bash theme={null}
# on the board
python3 /opt/pingpong/web/bench_cpu_vs_npu.py
```

预期结果：

|        | CPU (onnxruntime) | NPU（守护进程，内存路径） |
| ------ | ----------------- | -------------- |
| 原始模型推理 | \~145 ms          | \~1.74 ms      |
| 端到端每帧  | \~42 ms           | \~25 ms        |
| 等效 FPS | \~24              | \~40           |

原始 NPU 计算快 84 倍。端到端的优势是 1.7 倍。在实现内存路径之前，NPU 版本的端到端速度*慢于* CPU。307,200 次逐个数字进行的 float 到 int16 转换耗费了 30 ms。修复这一点（用向量化调用转换整帧）才带来了真实世界的性能提升。

只测量芯片计算的基准测试不是真正的基准测试。

***

## 源文件

上面引用的所有脚本都在[配套文件页面](/zh/tutorials/paddle-npu-story-files)上，并带有复制按钮。

<Note>
  守护进程 C++ 源文件（`npu/daemon/main.cpp`、`QnnSampleApp.cpp`、`QnnSampleApp.hpp`）没有嵌入到配套页面中，因为它们部分派生自 Qualcomm QNN SDK SampleApp。确切的修改以差异补丁形式记录在[配套文件页面 — 守护进程 C++ 源代码部分](/zh/tutorials/paddle-npu-story-files#daemon-cpp-source)中。使用 `build_daemon.sh` 将它们应用到干净的 SDK SampleApp 检出中。
</Note>

关于 Edge Impulse 路线（无需 QAIRT SDK，一个下午即可达到 2 ms），请参阅[使用 Edge Impulse 进行球拍检测](/zh/tutorials/edge-impulse-quickstart)。
