语义分割部署
1. 概述
1.1 功能简介
语义分割要解决的问题是给图像里的每个像素打上类别标签。目标检测只会告诉你「框里有一辆车」,语义分割能精确到每个像素属于什么,因此可以描出物体的准确边界和轮廓。
本文介绍如何在 RK3588(板载 6 TOPS 算力的 NPU)上部署两个分割模型:YOLOP(车道线分割)和 PP-HumanSeg(人体分割),覆盖模型转换、板端推理和性能测试的完整流程。TOPS 是每秒万亿次运算的算力单位,NPU(Neural Processing Unit)是专门跑神经网络的加速器。
这套方案的特点:
- NPU 支持 INT8 量化,分割模型推理速度快
- 三核 NPU 并行处理,满足实时分割需求
- 支持多任务分割,例如 YOLOP 一次推理同时输出检测和分割结果
- 像素级输出,适合对边界精度要求高的应用
1.2 应用场景
| 场景 | 模型 | 说明 |
|---|---|---|
| 自动驾驶车道线检测 | YOLOP | 实时分割车道线区域 |
| 视频会议背景替换 | PP-HumanSeg | 实时人像分割与背景替换 |
| 医学图像分析 | DeepLabV3+ | 器官/组织像素级分割 |
| 农业植被分析 | HRNet | 作物/杂草像素级分类 |
| 工业缺陷检测 | SegFormer | 产品表面缺陷分割 |
| 人脸分割 | FaceParsing | 人脸区域精细分割 |
| 遥感图像分析 | U-Net | 建筑物/道路分割 |
1.3 工作原理
图像输入 → RKNN NPU 推理 → 像素级分类 → 分割掩码输出
↓
每个像素一个类别标签分割网络可以拆成两半来理解:编码器(Encoder)逐步压缩图像、提取语义信息;解码器(Decoder)再把分辨率一层层恢复回来,最后对每个像素做分类,输出一张分割掩码(mask,即与原图同尺寸、每个像素标了类别的图像)。
编码器(Encoder) 解码器(Decoder)
┌─────────────────┐ ┌─────────────────┐
│ 骨干网络提取特征 │ │ 上采样恢复分辨率 │
│ 逐步降低分辨率 │ → │ 逐像素分类 │
│ 提取语义信息 │ │ 输出分割掩码 │
└─────────────────┘ └─────────────────┘RK3588 NPU 跑分割任务的优势:
- 高吞吐量:6 TOPS 算力支持高分辨率分割
- 多核并行:三个 NPU 核心可并行处理不同任务
- INT8 量化:减少内存占用,提升推理速度
- 低延迟:满足实时分割需求(>30 FPS)
2. 硬件准备
器件清单:
| 器件 | 数量 | 说明 |
|---|---|---|
| C-3588LQ 核心板 | 1 | RK3588 主芯片,含 NPU |
| SHIMETAPI-M5-R2A 底板 | 1 | 提供接口扩展 |
| USB 摄像头 | 1 | 可选,用于实时分割 |
| 电源适配器 | 1 | 12V/3A |
| HDMI 线 | 1 | 用于显示输出 |
| SD 卡 | 1 | 16GB+,用于系统启动 |
底板接口分布如下,接线时对照丝印即可:
SHIMETAPI-M5-R2A 底板接口:
┌─────────────────────────────────────┐
│ HDMI 输出 │ USB 3.0 │ 电源接口 │
│ Ethernet │ USB 2.0 │ SD 卡槽 │
│ CSI 摄像头 │ GPIO │ 调试串口 │
└─────────────────────────────────────┘摄像头按用途选择即可:
| 类型 | 分辨率 | 用途 |
|---|---|---|
| USB 摄像头 | 640x480 | 快速原型验证 |
| USB 摄像头 | 1920x1080 | 高清实时分割 |
| MIPI 摄像头 | 1920x1080 | 低延迟工业应用 |
3. 软件准备
3.0 运行环境(本机)
动手前先看一眼本机的环境,后面遇到版本差异时可以对照:
| 项 | 实际值 |
|---|---|
| 系统 | Debian 12 / Ubuntu 22.04(内核 6.1.141)/ aarch64 |
| NPU 驱动 | rknpu 0.9.8 内建(DRM 方式 /dev/dri/renderD129,无 /dev/rknpu) |
| C 运行时 | /usr/lib/librknnrt.so 2.3.2(出厂预装) |
| 推理链路 | rknn-toolkit-lite2 2.3.2 + NPU 已跑通(分类模型 224×224 fp16 4.62ms/INT8 1.91ms 三核,见 ch09) |
| 转换链路 | 板上装 toolkit2 2.3.2 可转换量化(见 ch03) |
先安装 Python 依赖。rknn-toolkit2 用于模型转换,rknn-toolkit-lite2 是板端推理用的轻量版运行时:
# 安装 RKNN 工具包
pip3 install rknn-toolkit2==2.3.2 -i https://pypi.org/simple
sudo pip3 install --break-system-packages rknn-toolkit-lite2==2.3.2 # 正确包名
# 安装依赖库
pip3 install numpy
pip3 install opencv-python
pip3 install pillow
pip3 install onnx然后下载两个模型的原始文件:
# 下载 YOLOP 模型
wget https://github.com/hustvl/YOLOP/raw/main/weights/yolop.onnx
# 下载 PP-HumanSeg 模型
wget https://paddleseg.bj.bcebos.com/humanseg/models/humanseg_server.zip
unzip humanseg_server.zip最后验证安装。两条命令分别打印 RKNN OK 和 RKNNLite OK,就说明环境就绪:
# 验证 RKNN 工具包
python3 -c "from rknn.api import RKNN; print('RKNN OK')"
# 验证 RKNN-Lite
python3 -c "from rknnlite.api import RKNNLite; print('RKNNLite OK')"4. 操作步骤
4.1 YOLOP 车道线分割模型转换
第一步把 YOLOP 的 ONNX 模型转成 RKNN 格式并做 INT8 量化(把浮点模型压缩成 8 位整数,换更快的推理速度)。转换的套路是固定的:config 配置预处理参数,load_onnx 加载模型,build 做量化,export_rknn 导出,任何一步返回非 0 都会报错退出:
from rknn.api import RKNN
# 初始化 RKNN
rknn = RKNN()
# 配置模型参数
rknn.config(
mean_values=[[123.675, 116.28, 103.53]],
std_values=[[58.395, 57.12, 57.375]],
target_platform='rk3588',
quantized_dtype='asymmetric_quantized-8'
)
# 加载 ONNX 模型
ret = rknn.load_onnx(model='yolop.onnx')
if ret != 0:
print('加载模型失败')
exit(ret)
# 构建 RKNN 模型(量化)
print('开始构建 RKNN 模型...')
ret = rknn.build(do_quantization=True, dataset='road_samples.txt')
if ret != 0:
print('构建模型失败')
exit(ret)
# 导出 RKNN 模型
ret = rknn.export_rknn('yolop.rknn')
if ret != 0:
print('导出模型失败')
exit(ret)
print('YOLOP 模型转换成功')
rknn.release()运行到最后打印「YOLOP 模型转换成功」并在当前目录生成 yolop.rknn,即转换成功。
量化还需要一份校准数据集,也就是一组真实场景图片,量化工具靠它统计每层输出的数值范围:
# 收集道路场景图片(20-100张)
# 保存路径列表到 road_samples.txt
ls road_images/*.jpg > road_samples.txt4.2 PP-HumanSeg 人体分割模型转换
流程与 4.1 完全一样,只是换了模型和校准集:
from rknn.api import RKNN
rknn = RKNN()
rknn.config(
mean_values=[[123.675, 116.28, 103.53]],
std_values=[[58.395, 57.12, 57.375]],
target_platform='rk3588',
quantized_dtype='asymmetric_quantized-8'
)
ret = rknn.load_onnx(model='pp_humanseg.onnx')
if ret != 0:
print('加载模型失败')
exit(ret)
print('开始构建 RKNN 模型...')
ret = rknn.build(do_quantization=True, dataset='human_samples.txt')
if ret != 0:
print('构建模型失败')
exit(ret)
ret = rknn.export_rknn('pp_humanseg.rknn')
if ret != 0:
print('导出模型失败')
exit(ret)
print('PP-HumanSeg 模型转换成功')
rknn.release()打印「PP-HumanSeg 模型转换成功」并生成 pp_humanseg.rknn 即成功。
4.3 板端 Python 推理(YOLOP)
把 yolop.rknn 拷到板上运行下面的脚本。它把摄像头画面缩放到 640×320 送进 NPU,把输出的车道线掩码放大回原尺寸,用绿色半透明叠加显示,按 q 退出:
from rknnlite.api import RKNNLite
import cv2
import numpy as np
# 初始化 RKNN-Lite
rknn = RKNNLite()
rknn.load_rknn('yolop.rknn')
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)
# 打开摄像头
cap = cv2.VideoCapture(0)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
while True:
ret, frame = cap.read()
if not ret:
break
# 预处理
img = cv2.resize(frame, (640, 320))
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 推理
outputs = rknn.inference(inputs=[img_rgb])
# YOLOP 输出 3 个分支
det_out = outputs[0] # 检测输出
da_out = outputs[1] # 车道线分割
ll_out = outputs[2] # 车道线检测
# 车道线分割掩码
da_mask = np.argmax(da_out[0], axis=0) # (320, 640)
da_mask = cv2.resize(
da_mask.astype(np.uint8),
(frame.shape[1], frame.shape[0])
)
# 可视化
overlay = frame.copy()
overlay[da_mask == 1] = [0, 255, 0] # 绿色显示车道线
frame = cv2.addWeighted(overlay, 0.5, frame, 0.5, 0)
cv2.imshow('YOLOP', frame)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
cap.release()
rknn.release()窗口正常弹出、车道线区域被绿色覆盖、画面流畅,就算部署成功。
4.4 板端 Python 推理(PP-HumanSeg)
对单张 person.jpg 做人体分割,并把背景替换成橙色,这是视频会议背景替换的最小示例:
from rknnlite.api import RKNNLite
import cv2
import numpy as np
rknn = RKNNLite()
rknn.load_rknn('pp_humanseg.rknn')
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)
# 读取图像
img = cv2.imread('person.jpg')
img = cv2.resize(img, (192, 192))
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 推理
outputs = rknn.inference(inputs=[img_rgb])
# 获取人体掩码
mask = outputs[0][0] # (192, 192)
mask = (mask > 0.5).astype(np.uint8) # 二值化
mask = cv2.resize(mask, (img.shape[1], img.shape[0]))
# 背景替换
background = np.zeros_like(img)
background[:] = [255, 128, 0] # 橙色背景
result = np.where(mask[:, :, None], img, background)
cv2.imshow('PP-HumanSeg', result)
cv2.waitKey(0)
cv2.destroyAllWindows()
rknn.release()弹出的窗口里人物保持原样、背景变成橙色,说明掩码提取和替换逻辑都正确。
4.5 分割性能测试
想知道模型在板上的真实速度,用下面的脚本:先预热 10 次排除首次推理的冷启动开销,再连续计时 100 次取平均:
import time
from rknnlite.api import RKNNLite
import numpy as np
# 初始化
rknn = RKNNLite()
rknn.load_rknn('pp_humanseg.rknn')
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)
# 创建测试数据
dummy = np.random.randint(0, 255, (1, 192, 192, 3), dtype=np.uint8)
# 预热
print('预热 NPU...')
for _ in range(10):
rknn.inference(inputs=[dummy])
# 测试
print('开始性能测试...')
times = []
for _ in range(100):
start = time.time()
rknn.inference(inputs=[dummy])
times.append(time.time() - start)
avg_ms = np.mean(times) * 1000
fps = 1000 / avg_ms
print(f'PP-HumanSeg: {avg_ms:.2f}ms, {fps:.1f} FPS')
rknn.release()跑完会打印一行 PP-HumanSeg: ...ms, ...FPS 的结果,这就是该模型三核模式的实际速度。
也可以把多个模型放在一起测,方便横向对比:
import time
from rknnlite.api import RKNNLite
import numpy as np
models = [
('yolop.rknn', (1, 320, 640, 3)),
('pp_humanseg.rknn', (1, 192, 192, 3)),
]
for model_name, input_shape in models:
rknn = RKNNLite()
rknn.load_rknn(model_name)
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)
dummy = np.random.randint(0, 255, input_shape, dtype=np.uint8)
# 预热
for _ in range(10):
rknn.inference(inputs=[dummy])
# 测试
times = []
for _ in range(100):
start = time.time()
rknn.inference(inputs=[dummy])
times.append(time.time() - start)
avg_ms = np.mean(times) * 1000
fps = 1000 / avg_ms
print(f'{model_name}: {avg_ms:.2f}ms, {fps:.1f} FPS')
rknn.release()5. 代码详解
5.1 分割后处理原理
NPU 输出的形状是 (H, W, C):H、W 是图像高宽,C 是类别数,相当于给每个像素一组「各类别的得分」。后处理只需沿通道取得分最高的那个类别(argmax,即取最大值所在位置的运算):
NPU 输出 (H, W, C) → argmax → (H, W) 分割掩码
↓
每个像素值 = 类别 ID具体分三步:
- 获取输出:NPU 输出形状为 (H, W, C),C 为类别数
- Argmax 操作:沿通道维度取最大值索引,得到类别 ID
- 掩码可视化:不同类别用不同颜色显示
# 后处理示例
def postprocess(output, num_classes=2):
"""
分割后处理
Args:
output: NPU 输出 (H, W, C)
num_classes: 类别数
Returns:
mask: 分割掩码 (H, W)
"""
# Argmax 获取类别 ID
mask = np.argmax(output, axis=-1)
# 验证掩码
assert mask.shape[0] == output.shape[0]
assert mask.shape[1] == output.shape[1]
assert np.max(mask) < num_classes
return mask5.2 YOLOP 多任务输出
YOLOP 是多任务模型,一次推理同时输出 3 个分支:
YOLOP 输出 3 个分支:
1. 检测分支:目标检测框
2. 车道线分割分支:像素级车道线掩码
3. 车道线点分支:车道线关键点解析时按输出顺序取用对应分支即可:
def yolop_postprocess(outputs):
"""
YOLOP 多任务输出解析
Args:
outputs: NPU 输出列表 [det, da, ll]
Returns:
boxes: 检测框
da_mask: 车道线分割掩码
ll_points: 车道线点
"""
det_out = outputs[0] # 检测输出
da_out = outputs[1] # 车道线分割
ll_out = outputs[2] # 车道线点
# 车道线分割掩码
da_mask = np.argmax(da_out[0], axis=0)
return da_mask5.3 NPU 分割性能
常见分割模型在 NPU 上的性能参考如下。mIoU(平均交并比)是衡量分割精度的常用指标,越高越好:
| 模型 | 输入尺寸 | FPS (三核) | 用途 | 精度 |
|---|---|---|---|---|
| YOLOP | 640×320 | ~80 | 车道线分割 | mIoU 72% |
| PP-HumanSeg | 192×192 | ~200 | 人体分割 | mIoU 86% |
| DeepLabV3+ | 512×512 | ~30 | 通用分割 | mIoU 79% |
| U-Net | 576×576 | ~25 | 医学分割 | mIoU 91% |
| SegFormer | 512×512 | ~35 | 场景分割 | mIoU 82% |
想再提速,可以按这几个方向调整:
- 使用三核 NPU:
RKNNLite.NPU_CORE_0_1_2 - INT8 量化:减少 75% 内存占用
- 降低分辨率:在精度允许时降低输入尺寸
- 批量推理:多帧并行处理
5.4 分割掩码可视化
把掩码按类别上色、再与原图做半透明叠加,是分割结果最常用的展示方式:
def visualize_mask(frame, mask, colors, alpha=0.5):
"""
可视化分割掩码
Args:
frame: 原始图像
mask: 分割掩码
colors: 类别颜色列表
alpha: 透明度
Returns:
result: 可视化结果
"""
overlay = frame.copy()
for cls_id, color in enumerate(colors):
overlay[mask == cls_id] = color
result = cv2.addWeighted(overlay, alpha, frame, 1 - alpha, 0)
return result
# 使用示例
colors = [
[0, 0, 0], # 背景:黑色
[0, 255, 0], # 车道线:绿色
[255, 0, 0], # 道路:红色
]
result = visualize_mask(frame, da_mask, colors)6. 常见问题
6.1 分割掩码模糊
现象:分割边界不清晰。
可以按下面三种思路依次尝试:
# 方案 1:增大输入分辨率
img = cv2.resize(frame, (1280, 640)) # 提高分辨率
# 方案 2:使用更高精度的模型
# 从 YOLOP 切换到 YOLOP-v2 或 PP-HumanSeg-v2
# 方案 3:检查量化是否过度
rknn.config(
quantized_dtype='asymmetric_quantized-8',
# 尝试使用混合量化或跳过量化
)6.2 背景替换有边缘锯齿
现象:人像边缘有锯齿状。
本质是掩码边缘太「硬」,把它处理得柔和一些即可:
# 方案 1:使用高斯模糊平滑掩码边缘
mask_blur = cv2.GaussianBlur(mask, (5, 5), 0)
result = np.where(mask_blur[:, :, None] > 0.5, img, background)
# 方案 2:使用形态学操作
kernel = np.ones((3, 3), np.uint8)
mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
# 方案 3:使用 Alpha 融合
alpha = mask.astype(np.float32) / 255.0
alpha = cv2.GaussianBlur(alpha, (5, 5), 0)
result = (alpha[:, :, None] * img +
(1 - alpha[:, :, None]) * background).astype(np.uint8)6.3 推理速度慢
现象:分割 FPS 低。
先确认 NPU 核心和频率,再考虑降低分辨率或换轻量模型:
# 1. 确认使用三核 NPU
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0_1_2)
# 2. 降低输入分辨率
# 640x320 → 320x160
# 3. 使用轻量级分割模型
# DeepLabV3+ → BiSeNet 或 PP-HumanSeg
# 4. 启用 NPU 优化
export RKNN_NPU_FREQ=10006.4 内存不足
现象:推理时报内存错误。
优先降低输入尺寸,并注意及时释放内存:
# 1. 减少输入分辨率
img = cv2.resize(frame, (320, 160))
# 2. 使用单核 NPU
rknn.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
# 3. 释放不需要的内存
del outputs
import gc
gc.collect()6.5 量化精度下降
现象:量化后分割精度明显下降。
问题多半出在校准数据或量化方式上:
# 1. 使用更好的量化数据集
# 确保数据集覆盖多种场景
# 2. 使用混合精度量化
rknn.config(
quantized_dtype='asymmetric_quantized-8',
# 对关键层使用 FP16
)
# 3. 跳过量化,使用 FP16
rknn.build(do_quantization=False)6.6 多模型冲突
现象:同时运行多个分割模型时报错。
多个模型抢同一个 NPU 核心时容易出问题,把它们错开或串行执行:
# 1. 分配不同 NPU 核心
rknn1.init_runtime(core_mask=RKNNLite.NPU_CORE_0)
rknn2.init_runtime(core_mask=RKNNLite.NPU_CORE_1)
# 2. 串行执行
output1 = rknn1.inference(inputs=[img1])
output2 = rknn2.inference(inputs=[img2])
# 3. 释放一个模型后再加载另一个
rknn1.release()
rknn2 = RKNNLite()
rknn2.load_rknn('model2.rknn')相关文档
- ch01-npu-architecture-and-performance.md
- 05_目标检测部署(YOLOv5-v7-v8).md
- ch09-npu-performance-and-debugging.md
