1. 从零开始:为什么选择QCS8550与YOLO11-seg?

大家好,我是老张,一个在AI和边缘计算领域摸爬滚打了十来年的工程师。最近,我接到了一个挺有挑战性的任务:在一条智能产线的质检工位上,部署一套能实时识别并分割产品缺陷的AI视觉系统。产线环境嘈杂,空间有限,对设备的功耗、算力和实时性要求都极高。经过一番选型,我最终把目光锁定在了高通QCS8550这颗SoC和YOLO11-seg这个模型上。今天,我就把这次从模型转换到最终在产线旁跑起来的完整实战经验,毫无保留地分享给大家。

先说说为什么是这对“黄金搭档”。QCS8550是高通面向高端物联网和边缘AI推出的旗舰平台,它最吸引我的地方,就是那颗高达48 TOPS算力的NPU(神经网络处理单元)。简单来说,TOPS可以理解为芯片每秒能进行多少万亿次运算,这个数字直接决定了模型推理的速度。在产线旁,我们处理的是高清摄像头传回的实时视频流,每一帧的延迟都至关重要,48 TOPS的NPU算力给了我足够的底气。此外,它集成的强大CPU、GPU以及先进的Wi-Fi 7连接能力,也让整个系统集成变得非常顺畅。

而模型方面,我选择了Ultralytics最新推出的YOLO11-seg。YOLO系列大家都很熟了,速度快、精度高。YOLO11-seg在YOLO11的基础上,专门强化了实例分割能力。什么叫实例分割?它比单纯的目标检测(画个框)更进了一步,不仅要找出图片里每个目标物体在哪里,还要精确地勾勒出它的轮廓边界。这对于工业质检太关键了——我们不仅要发现电路板上的一个焊点缺陷,还得知道这个缺陷具体是什么形状、有多大面积,是圆形虚焊还是条状划痕。YOLO11-seg正好能输出每个目标的像素级掩码(mask),完美契合这个需求。

把强大的模型塞进小巧的边缘设备里,并让它高效地跑起来,这就是我们接下来要做的全部事情。这个过程,我习惯称之为“边缘炼丹”。下面,我就带你一步步“开炉炼丹”,从准备“药材”(模型)开始,直到在QCS8550上“药到病除”(稳定运行)。

2. 炼丹第一步:模型转换与量化(从PyTorch到QNN)

拿到一个在云端训练好的PyTorch(.pt)模型,第一步就是把它“翻译”成边缘设备NPU能听懂的语言。对于高通的NPU,这个语言就是QNN(Qualcomm Neural Network SDK) 支持的格式。这个过程主要分两步:先转成通用的ONNX格式,再通过高通的AIMO平台转换成QNN格式。

2.1 环境准备与PyTorch转ONNX

首先,我们需要一个工作环境。我强烈推荐使用AidLux。这是一个基于ARM架构的跨平台AI应用开发环境,它原生集成了对高通芯片和QNN SDK的支持,省去了我们交叉编译的麻烦,可以直接在电脑上模拟出类似QCS8550的环境进行开发调试。

在AidLux的终端里,我们先把必要的工具包安装好。这里有个小坑要注意,就是pip的版本和yolo命令的环境变量。

# 升级pip到指定版本,避免后续安装冲突
python3.10 -m pip install --upgrade pip

# 安装模型转换的核心库:ultralytics 和 onnx
pip install ultralytics onnx

安装完ultralytics后,你可能会发现直接输入 yolo 命令找不到。这是因为安装路径没在系统环境变量里。解决起来很简单:

# 临时添加(关闭终端后失效)
export PATH="$PATH:$HOME/.local/bin"

# 或者永久添加,写入bash配置文件
echo 'export PATH="$PATH:$HOME/.local/bin"' >> ~/.bashrc
source ~/.bashrc

完成后,运行 yolo --version,如果能看到版本号(比如8.3.152),说明环境就绪了。有时候会遇到用户组权限的警告,可以执行下面命令解决:

sudo chown -R aidlux:aidlux ~/.config/Ultralytics

环境搞定后,就可以开始转换模型了。我准备了一个Python脚本,核心代码就几行:

from ultralytics import YOLO

# 加载你训练好的.pt模型文件
model = YOLO('yolo11n-seg.pt')  # 这里替换成你的模型路径

# 设置导出参数
export_params = {
    'format': 'onnx',        # 导出为ONNX格式
    'opset': 12,             # ONNX算子集版本,12比较稳定
    'simplify': True,        # 启用模型简化,移除冗余操作
    'dynamic': False,        # 使用固定输入尺寸,便于后续优化
    'imgsz': 640,            # 模型的固定输入尺寸,与训练时一致
    'half': False            # 先保持FP32精度,量化步骤后续进行
}

# 执行转换,会在同级目录生成 .onnx 文件
model.export(**export_params)

运行这个脚本,眨眼功夫,一个 yolo11n-seg.onnx 文件就生成了。这个ONNX文件就像一个“中间商”,它用一种标准格式描述了模型的整个计算图结构,任何支持ONNX的推理引擎(比如QNN)都能读取它。对于YOLO11s/l/m/x等其他尺寸的模型,操作一模一样,只需替换模型文件名即可。

2.2 使用AIMO平台进行量化与格式转换

拿到了ONNX,接下来就要用高通的AIMO(AI Model Optimizer)平台进行“精加工”。AIMO是一个在线模型优化工具,它的核心作用有两个:一是量化,二是格式转换

量化是什么?你可以理解为给模型“瘦身”。原始的FP32(单精度浮点数)模型精度高但体积大、计算慢。通过量化,我们可以将权重和激活值从FP32转换为INT8(8位整数)。这样,模型体积能缩小近4倍,在NPU上运行的速度也能提升数倍,而精度损失通常控制在可接受的微小范围内。这对于边缘设备的内存带宽和计算效率是巨大的提升。

打开AIMO平台,操作流程非常直观:

  1. 上传模型:选择“模型优化”,上传我们刚生成的 .onnx 文件。
  2. 选择硬件:在目标芯片型号里,找到并选择 QCS8550,软件栈选择 QNN 2.31。这一步至关重要,它确保了生成的模型能充分利用QCS8550 NPU的特定指令集和硬件加速单元。
  3. 配置输入输出:这里需要用到 Netron 这个工具。用Netron打开你的ONNX文件,可视化模型的输入输出节点。对于YOLO11-seg,输入节点很好找,就是一个形状为 [1, 3, 640, 640] 的张量。输出节点通常有多个,你需要仔细查看。以我转换的模型为例,输出节点名称为:
    • /model.23/Concat_output_0
    • /model.23/Mul_2_output_0
    • /model.23/Sigmoid_output_0 在AIMO的对应位置填写这些名称。
  4. 启用量化与转换:在高级设置中,务必勾选INT8量化选项。AIMO会自动使用校准数据集(如果没有上传,它会使用随机数据生成)来确定每一层数据的最佳量化参数。然后点击“转换”,平台就会在云端开始工作。

转换完成后,下载得到一个压缩包,解压后找到后缀为 .bin.aidem 的文件(有时也可能是 .dlc.qnn 等格式,具体看AIMO输出),这就是我们最终能在QCS8550 NPU上运行的模型文件了。这个过程就像把一份详细的建筑设计图(ONNX),交给专业的施工队(AIMO),加工成了可以直接在特定工地(QCS8550 NPU)上使用的预制构件(QNN模型)。

3. 在AidLux中部署与NPU加速推理实战

模型“丹药”炼成了,接下来就是把它“服用”到设备上,并编写“服用说明书”——也就是推理代码。我们将在AidLux环境中,使用 AidLite SDK 来调用QNN库,完成NPU加速推理。

3.1 部署环境与AidLite SDK配置

首先,在AidLux终端里检查是否安装了正确版本的AidLite SDK。我们需要确保SDK的QNN版本与之前在AIMO平台转换时选择的版本(2.31)一致。

# 查看已安装的AidLite包
sudo aid-pkg installed

# 如果列表里没有 aidlite-qnn231,则需要安装
sudo aid-pkg update
sudo aid-pkg install aidlite-qnn231  # 安装指定QNN版本的SDK

安装成功后,我们就可以在Python代码中导入 aidlite 库了。这个库封装了底层QNN的复杂接口,让我们能用相对简单的API调用NPU。

3.2 编写NPU推理代码全解析

推理代码是整个部署的核心,看起来有点长,但别怕,我把它拆解成几个关键部分,你一定能看懂。核心是一个 qnn_predict 类。

第一步:初始化与模型加载 在类的 __init__ 方法中,我们配置AidLite的运行环境,并加载模型。

def __init__(self, args):
    # 创建配置实例,指定使用本地QNN加速
    config = aidlite.Config.create_instance()
    config.implement_type = aidlite.ImplementType.TYPE_LOCAL
    config.framework_type = aidlite.FrameworkType.TYPE_QNN
    config.accelerate_type = aidlite.AccelerateType.TYPE_DSP  # 使用DSP进行加速
    config.is_quantify_model = 1  # 声明我们加载的是量化模型(INT8)

    # 加载模型文件
    model = aidlite.Model.create_instance(args.target_model)
    # 设置模型的输入输出张量形状和数据类型
    self.interpreter = aidlite.InterpreterBuilder.build_interpretper_from_model_and_config(model, config)
    self.interpreter.load_model()

这里有几个关键点:accelerate_type 选择了DSP,这是因为在高通平台上,DSP和NPU协同工作能获得最佳能效比。is_quantify_model 必须设置为1,告诉SDK这是量化模型,它会用对应的INT8计算管线。

第二步:图像预处理 NPU对输入数据格式有严格要求。我们的模型输入是640x640的RGB图像,数据需要归一化到0-1,并且布局是NHWC(批次、高度、宽度、通道)。

def pretreat_img(self, frame):
    # 等比例缩放并填充,保持图像不变形
    img, scale = eqprocess(frame, self.height, self.width)
    # 归一化
    img = img / 255
    # 转换数据类型
    img = img.astype(np.float32)
    return img, scale

这个 eqprocess 函数是我自己写的,它把任意尺寸的输入图像,先按长边缩放到640,短边用黑色填充,同时计算出缩放比例 scale,这个比例在后面要把检测框坐标映射回原图时非常重要。

第三步:执行推理与后处理 这是最核心的 qnn_run 方法。它负责将预处理好的图像数据送入NPU,取回原始输出,并进行解码。

def qnn_run(self, orig_imgs, args):
    # 1. 预处理图像
    input_img_f, scale = self.pretreat_img(orig_imgs)
    input_img = np.expand_dims(input_img_f, 0)  # 增加批次维度,变成[1,640,640,3]

    # 2. 设置输入张量并执行推理
    self.interpreter.set_input_tensor(0, input_img.data)
    self.interpreter.invoke()  # 触发NPU计算

    # 3. 获取输出。YOLO11-seg有4个输出层
    boxes_output = self.interpreter.get_output_tensor(3)  # 边界框 (x, y, w, h)
    scores_output = self.interpreter.get_output_tensor(2)  # 类别置信度
    mask_coeff_output = self.interpreter.get_output_tensor(1) # 掩码系数
    protos_output = self.interpreter.get_output_tensor(0)    # 原型掩码

    # 4. 后处理:解码边界框、执行NMS、处理掩码
    # ... (具体解码和NMS代码,见下文)

从NPU取回的输出是“原始”的,需要我们自己解码。对于边界框,我们需要将输出重塑成 [num_anchors, 4+num_classes+32] 的形状,其中4是框坐标,80是COCO数据集的类别数,32是掩码系数的维度。然后,根据置信度阈值进行过滤,再使用非极大值抑制(NMS)去除重叠的冗余框。

掩码的处理稍微复杂一些,它涉及一个矩阵乘法操作: masks = mask_coeff @ protos.reshape(32, -1) 这行代码的意思是,将每个检测框对应的32维掩码系数向量,与原型掩码图(protos)相乘,合成出该目标最终的像素级分割掩码。之后,再根据边界框的位置对掩码进行裁剪,并利用OpenCV的 findContours 函数将掩码转换成轮廓线段,方便可视化绘制。

第四步:结果可视化 最后,我们用OpenCV把检测框和分割掩码画到原图上。为了让不同类别的实例更易区分,我定义了一个调色板,给不同类别的框和掩码赋予不同的颜色。

def draw_detect_res(img, det_pred, segments):
    color_palette = [(255,0,0), (0,255,0), (0,0,255), ...] # BGR颜色列表
    for i in range(len(det_pred)):
        x1, y1, x2, y2 = map(int, det_pred[i][:4])
        cls_id = int(det_pred[i][5])
        color = color_palette[cls_id % len(color_palette)] # 按类别取颜色

        # 画框和标签
        cv2.rectangle(img, (x1, y1), (x2, y2), color, 2)
        # 画分割掩码(半透明填充)
        cv2.fillPoly(overlay, [segments[i]], color)
        img = cv2.addWeighted(overlay, 0.3, img, 0.7, 0) # 融合
    return img

这样,一个完整的从图像输入到带分割结果输出的NPU推理流程就完成了。你可以通过命令行参数指定模型路径、图片路径、置信度阈值等,灵活地进行测试。

4. 性能调优与不同精度模型对比

模型跑起来了,但作为工程师,我们肯定不满足于“能跑”,还要追求“跑得快”、“跑得稳”。这部分,我们来深入聊聊性能调优,并对比一下FP16和INT8量化模型的实际表现。

4.1 关键性能指标解读

在边缘部署中,我们主要关注三个核心指标:

  1. 延迟(Latency):处理一帧图像需要多少时间,单位通常是毫秒(ms)。这直接决定了系统能否满足实时性要求(例如,30FPS对应约33ms每帧)。
  2. 吞吐量(Throughput):单位时间内能处理多少帧图像,即帧率(FPS)。FPS = 1000 / 延迟(ms)。
  3. 精度(Accuracy):主要是平均精度(mAP),量化通常会带来轻微的精度下降,我们需要在速度和精度之间找到平衡。

为了准确测量,我在推理循环中加入了计时代码,并循环运行多次(比如100次)取平均值,以消除偶然误差。

invoke_times = []
for i in range(100):
    t0 = time.time()
    self.interpreter.invoke() # NPU推理
    t1 = time.time()
    invoke_times.append((t1 - t0) * 1000) # 转换为毫秒

mean_time = np.mean(invoke_times)
fps = 1000 / mean_time
print(f"平均推理时间:{mean_time:.2f}ms, 帧率:{fps:.2f}FPS")

4.2 FP16 vs INT8:一场速度与精度的权衡

我分别测试了YOLO11n-seg模型在FP16(半精度浮点)和INT8(8位整型)量化后的性能。下面这个表格直观地展示了差异:

模型精度平均推理时间 (ms)帧率 (FPS)模型大小mAP50 (COCO) 预估下降
FP32 (原始)119.678.36~12 MB基准 (100%)
FP1616.7159.84~6 MB< 1%
INT85.54180.51~3 MB1% - 3%

解读与选择建议:

  • INT8模型无疑是速度冠军。推理时间仅需5.54毫秒,帧率高达180 FPS,模型体积也最小。这意味着在QCS8550上,你可以轻松处理多路高清视频流。精度损失通常在1到3个百分点,对于很多对实时性要求极端苛刻、且对绝对精度有一定容忍度的场景(如某些类型的移动机器人避障、高速流水线上的粗筛)来说是首选。
  • FP16模型均衡之选。速度相比INT8慢约3倍,但依然远超FP32,达到近60 FPS。它的优势在于精度损失极小,通常可以忽略不计。如果你的应用对精度非常敏感,比如医疗影像分析、精密零件质检,又需要比FP32快得多的速度,那么FP16是最佳选择。
  • FP32模型在边缘端通常不作为部署首选,除非你的算法对数值精度有极其严苛的要求,且对速度不敏感。

实测中的调优技巧:

  1. 输入尺寸:YOLO11-seg默认输入是640x640。如果你的场景中目标都比较大,可以尝试降低到480x480甚至320x320,速度会有显著提升,但小目标检测能力会下降。
  2. 置信度与IOU阈值:代码中的 conf_thresiou_thres 是调节检测灵敏度和冗余度的阀门。在产线环境中,如果背景干净、目标明确,可以适当调高置信度阈值(如0.5),减少误检,也能稍微加快后处理速度。NMS的IOU阈值也可以根据目标重叠程度调整。
  3. 利用多核:虽然NPU是主力,但QCS8550的Kryo CPU也很强大。你可以将图像预处理、后处理(NMS、画图)等任务放在CPU的不同核心上并行执行,与NPU推理流水线化,进一步压榨硬件性能。
  4. 内存复用:在连续处理视频流时,尽量避免频繁申请和释放内存。可以预先分配好输入输出张量所需的内存空间,在循环中重复使用。

通过这样的量化对比和微调,我最终为那条产线选择了 YOLO11n-seg INT8 模型。它在保持足够检出率的前提下,实现了单帧小于6毫秒的推理速度,完全满足了产线高速传送带的需求。整个系统部署在工控机形态的QCS8550开发板上,通过USB摄像头取流,稳定运行至今。这次实战让我深刻体会到,在边缘AI落地的最后一公里,选择合适的硬件平台、完成高效的模型转换与量化、并进行细致的性能调优,每一个环节都至关重要,也充满了工程师的乐趣。希望我的这些踩坑经验和实操代码,能帮你更快地将想法变成现实。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐