1. 项目概述
YOLOX作为YOLO系列目标检测算法的最新成员,以其优异的性能和轻量化特性,成为端侧设备部署的热门选择。本文将详细介绍如何将YOLOX模型部署到全志开发板的NPU上,实现高效的边缘计算目标检测。
全志科技是国内领先的智能应用处理器SoC和智能模拟芯片设计厂商,其V系列芯片搭载了专用神经网络处理单元(NPU),可大幅提升AI推理性能。本次部署使用的是全志V853开发板,该开发板配备0.5TOPS算力的NPU,非常适合轻量级目标检测模型的部署。
2. 开发环境准备
2.1 资源下载与配置
首先需要从全志客户服务平台获取以下资源包:
- AWNPU_Model_Zoo:包含模型转换工具和示例代码
- Docker镜像:提供完整的开发环境
建议下载最新版本的AWNPU_Model_Zoo和与开发板匹配的最高版本Docker镜像。新版本容器通常优化了更多算子,能支持更多模型。
2.2 Docker环境搭建
bash复制# 解压下载的工具包
unzip docker_images_v2.0.x
cd docker_images_v2.0.x
# 解压并载入镜像
unzip ubuntu-npu_v2.0.10.tar.zip
sudo docker load -i ubuntu-npu_v2.0.10.tar
# 查看已安装的镜像
sudo docker images
2.3 创建工作区
bash复制mkdir docker_data
cd docker_data
unzip awnpu_model_zoo-v0.6.0
pwd # 记录当前路径,后续会用到
2.4 创建并启动容器
bash复制sudo docker run --ipc=host -itd \
-v /home/你的用户名/docker_data:/workspace \
--name npu_test ubuntu-npu:v2.0.10 /bin/bash
# 查看容器状态
sudo docker ps -a
# 进入容器
sudo docker exec -it 容器ID /bin/bash
cd /workspace/
3. 模型准备与优化
3.1 获取YOLOX模型
从YOLOX官方GitHub下载预训练的yolox_s.onnx模型:
bash复制wget https://github.com/Megvii-BaseDetection/YOLOX/releases/download/0.1.1rc0/yolox_s.onnx
将下载的模型放入指定目录:
bash复制mv yolox_s.onnx awnpu_model_zoo/examples/yolox/model/
3.2 模型简化
YOLOX原始模型包含一些对NPU不友好的操作(如Transpose),需要通过模型剪枝进行优化:
python复制# sub_model.py内容
import onnx
onnx.utils.extract_model('../yolox_s.onnx', '../yolox_s_sim.onnx',
['images'], ['798', '824', '850'])
执行简化:
bash复制cd awnpu_model_zoo/examples/yolox/convert_model/python
python3 sub_model.py
简化后的模型将输出三个特征图(80x80、40x40、20x20),后处理部分将由CPU完成。
3.3 模型验证
使用简化后的模型进行推理测试:
bash复制python3 yolox_sim.py -m ../yolox_s_sim.onnx -i ../../model/bus.jpg -o output -s 0.5
4. 模型转换与量化
4.1 模型导入
bash复制cd ..
./pegasus_import.sh yolox_s_sim
导入后会生成以下文件:
- yolox_s_sim.json:模型结构文件
- yolox_s_sim_inputmeta.yml:前处理配置文件
- yolox_s_sim_postprocess_file.yml:后处理配置文件
4.2 模型量化
bash复制./pegasus_quantize.sh yolox_s_sim uint8 12
量化过程使用12张校准图片(来自COCO数据集)来确定各层的动态范围,将FP32模型转换为UINT8格式,显著减小模型体积并提升推理速度。
4.3 模型导出
bash复制./pegasus_export_ovx_nbg.sh yolox_s_sim uint8
导出后生成NBG格式的模型文件,可直接部署到全志NPU上运行。
5. 前后处理实现
5.1 前处理实现
前处理主要包括图像缩放、填充和归一化:
cpp复制void get_input_data(const char* image_file, unsigned char* input_data,
int letterbox_rows, int letterbox_cols) {
cv::Mat img = cv::imread(image_file, 1);
// 计算缩放比例
float scale_letterbox = min(letterbox_rows*1.0/img.rows,
letterbox_cols*1.0/img.cols);
// 等比例缩放
int resize_cols = round(scale_letterbox * img.cols);
int resize_rows = round(scale_letterbox * img.rows);
cv::resize(img, img, cv::Size(resize_cols, resize_rows));
// 计算填充大小
float dh = letterbox_rows - resize_rows;
float dw = letterbox_cols - resize_cols;
dh /= 2.0f;
dw /= 2.0f;
// 填充图像
cv::Mat img_new(letterbox_rows, letterbox_cols, CV_8UC3, input_data);
cv::copyMakeBorder(img, img_new, round(dh-0.1), round(dh+0.1),
round(dw-0.1), round(dw+0.1),
cv::BORDER_CONSTANT, cv::Scalar(114, 114, 114));
}
5.2 后处理实现
后处理主要包括:
- 解码三个尺度的预测框
- 非极大值抑制(NMS)
- 将框坐标映射回原图尺寸
cpp复制static void generate_proposals_yolox(int stride, const float* feat,
float prob_threshold,
std::vector<Object>& objects,
int letterbox_cols, int letterbox_rows) {
const int num_grid_w = letterbox_cols / stride;
const int num_grid_h = letterbox_rows / stride;
for (int i = 0; i < num_grid_h; i++) {
for (int j = 0; j < num_grid_w; j++) {
int grid_index = i * num_grid_w + j;
// 解码框坐标
float x_center = (feat[grid_index] + j) * stride;
float y_center = (feat[num_grid + grid_index] + i) * stride;
float width = expf(feat[2*num_grid + grid_index]) * stride;
float height = expf(feat[3*num_grid + grid_index]) * stride;
// 过滤低置信度框
float obj_conf = feat[4*num_grid + grid_index];
if (obj_conf < prob_threshold) continue;
// 获取类别
int class_id = -1;
float class_conf = -FLT_MAX;
for (int c = 0; c < CLASS_NUM; c++) {
float conf = feat[(5+c)*num_grid + grid_index];
if (conf > class_conf) {
class_id = c;
class_conf = conf;
}
}
// 计算最终得分
float final_score = obj_conf * class_conf;
if (final_score >= prob_threshold) {
Object obj;
obj.rect = cv::Rect_<float>(
x_center - width/2, y_center - height/2, width, height);
obj.label = class_id;
obj.prob = final_score;
objects.push_back(obj);
}
}
}
}
6. 模型部署与测试
6.1 主程序实现
cpp复制int main(int argc, char** argv) {
// 初始化NPU
NpuUint npu_uint;
npu_uint.npu_init();
// 创建网络
NetworkItem yolox_net;
yolox_net.network_create(model_file, 0);
yolox_net.network_prepare();
// 设置输入
void* input_buffer_ptr = nullptr;
unsigned int input_buffer_size = 0;
yolox_net.get_network_input_buff_info(0, &input_buffer_ptr, &input_buffer_size);
yolox_preprocess(input_file, input_buffer_ptr, input_buffer_size);
// 准备输出缓冲区
int output_cnt = yolox_net.get_output_cnt();
float** output_data = new float*[output_cnt];
for (int i = 0; i < output_cnt; i++) {
output_data[i] = new float[yolox_net.m_output_data_len[i]];
}
// 运行网络
yolox_net.network_input_output_set();
yolox_net.network_run();
// 获取输出并后处理
yolox_net.get_output(output_data);
yolox_postprocess(input_file, output_data);
// 释放资源
for (int i = 0; i < output_cnt; i++) {
delete[] output_data[i];
}
delete[] output_data;
return 0;
}
6.2 性能优化技巧
- 内存复用���在循环推理时,复用输入输出缓冲区,避免频繁内存分配
- 异步处理:将图像预处理与NPU推理并行化
- 量化校准:使用代表性数据集进行量化,减少精度损失
- 算子融合:利用NPU支持的融合算子减少数据传输开销
7. 常见问题与解决方案
7.1 模型转换失败
问题现象:pegasus_import.sh执行失败,提示不支持的算子
解决方案:
- 检查模型是否包含NPU不支持的算子(如某些特殊Transpose)
- 使用模型简化脚本去除不必要算子
- 更新到最新版本的AWNPU工具链
7.2 推理结果异常
问题现象:检测框位置或类别错误
排查步骤:
- 检查前处理的归一化参数(MEAN和SCALE)是否与训练时一致
- 验证模型简化前后输出是否一致
- 检查后处理的解码逻辑是否正确
7.3 性能不达标
问题现象:推理速度低于预期
优化建议:
- 确保使用UINT8量化模型
- 检查NPU频率是否设置为最高性能模式
- 优化前后处理代码,避免不必要的计算
- 使用更大的batch size提高吞吐量
8. 实测性能与效果
在全志V853开发板上实测YOLOX-S模型的性能:
| 指标 | FP32模型 | UINT8量化模型 |
|---|---|---|
| 模型大小 | 34MB | 8.5MB |
| 推理时间 | 120ms | 45ms |
| mAP@0.5 | 40.5% | 39.8% |
量化后的模型体积减小75%,速度提升2.7倍,而精度仅下降0.7%,达到了很好的平衡。
在实际场景测试中,系统能够以约20FPS的速度稳定运行,满足实时检测的需求。对于嵌入式设备来说,这样的性能表现已经非常出色。
