YOLO CPU 前处理优化:5 种 HWC→NCHW 转换方法全网最详对比(速度测试+工程级代码)
在 YOLO 模型的 CPU 部署中,是前处理阶段最关键的一步。OpenCV 读取图像默认是格式,而模型输入要求格式,转换效率直接决定前处理速度。本文,提供,附带完整可运行代码 + 真实速度对比,帮你在工程中直接选用最优方案!
·
在 YOLO 模型的 CPU 部署中,图像格式转换(HWC → NCHW) 是前处理阶段最关键的一步。OpenCV 读取图像默认是 HWC(高×宽×通道) 格式,而模型输入要求 NCHW(批次×通道×高×宽) 格式,转换效率直接决定前处理速度。
本文纯 CPU 实现、无 GPU 依赖、无复杂语法,提供 5 种完全独立的 HWC→NCHW 转换方法,附带完整可运行代码 + 真实速度对比,帮你在工程中直接选用最优方案!
一、背景知识
1. 格式区别
- HWC(OpenCV 默认):像素交织存储 →
BGR BGR BGR ... - NCHW(模型输入):通道连续存储 →
BBBB... GGGG... RRRR...
2. YOLOv5 标准 CPU 前处理流程
- 读取图像(BGR)
- 缩放尺寸到 640×640
- BGR → RGB
- 归一化
/255.0 - HWC → NCHW(本文核心)
二、测试环境
- 系统:Linux x86_64
- OpenCV:4.2.0(兼容老版本)
- 输入尺寸:640×640(YOLOv5 标准输入)
- 测试方式:每种方法运行 100 次取平均耗时
- 编译:C++11
三、5 种 HWC→NCHW 实现方法(完整可运行)
所有方法独立函数、直接调用、无耦合。
方法1:三层循环遍历(教学版、最容易理解)
使用 at<Vec3f> 逐像素访问,代码直观,但速度最慢。
void hwc_to_nchw_loop3(const Mat& float_rgb, float* nchw) {
int H = float_rgb.rows;
int W = float_rgb.cols;
for (int c = 0; c < 3; c++) {
for (int h = 0; h < H; h++) {
for (int w = 0; w < W; w++) {
nchw[c * H * W + h * W + w] = float_rgb.at<Vec3f>(h, w)[c];
}
}
}
}
方法2:单循环指针扁平化(手写循环最快)
将图像视为一维数组,单循环同时写 3 个通道,效率极高。
void hwc_to_nchw_flat_ptr(const Mat& float_rgb, float* nchw) {
int H = float_rgb.rows;
int W = float_rgb.cols;
int area = H * W;
const float* src = (const float*)float_rgb.data;
for (int i = 0; i < area; i++) {
nchw[0 * area + i] = src[i * 3 + 0];
nchw[1 * area + i] = src[i * 3 + 1];
nchw[2 * area + i] = src[i * 3 + 2];
}
}
方法3:按行指针遍历(OpenCV 官方推荐风格)
逐行获取数据指针,缓存友好,工业代码常用风格。
void hwc_to_nchw_row_ptr(const Mat& float_rgb, float* nchw) {
int H = float_rgb.rows;
int W = float_rgb.cols;
int area = H * W;
float* c0 = nchw + 0 * area;
float* c1 = nchw + 1 * area;
float* c2 = nchw + 2 * area;
for (int y = 0; y < H; y++) {
const float* row = float_rgb.ptr<float>(y);
for (int x = 0; x < W; x++) {
c0[y * W + x] = row[x * 3 + 0];
c1[y * W + x] = row[x * 3 + 1];
c2[y * W + x] = row[x * 3 + 2];
}
}
}
方法4:split 直接写入目标内存(速度冠军、工程首选)
利用 OpenCV 内置 split,直接将通道拆分到 NCHW 内存,底层优化、极快。
void hwc_to_nchw_split(const Mat& float_rgb, float* nchw) {
int H = float_rgb.rows;
int W = float_rgb.cols;
int area = H * W;
Mat ch0(H, W, CV_32F, nchw + 0 * area);
Mat ch1(H, W, CV_32F, nchw + 1 * area);
Mat ch2(H, W, CV_32F, nchw + 2 * area);
vector<Mat> mats;
mats.push_back(ch0);
mats.push_back(ch1);
mats.push_back(ch2);
split(float_rgb, mats);
}
方法5:split + memcpy 拆分拷贝(稳定通用)
先 split 拆分,再用 memcpy 拷贝,兼容性极强、不易出错。
void hwc_to_nchw_split_copy(const Mat& float_rgb, float* nchw) {
int H = float_rgb.rows;
int W = float_rgb.cols;
int area = H * W;
vector<Mat> channels;
split(float_rgb, channels);
memcpy(nchw + 0 * area, channels[0].data, area * sizeof(float));
memcpy(nchw + 1 * area, channels[1].data, area * sizeof(float));
memcpy(nchw + 2 * area, channels[2].data, area * sizeof(float));
}
四、完整主程序(带速度测试)
#include <iostream>
#include <vector>
#include <opencv2/opencv.hpp>
#include <chrono>
using namespace std;
using namespace cv;
// 把上面 5 个函数粘贴在这里 ……
void preprocess(const Mat& bgr, Mat& float_rgb, int target_w, int target_h) {
Mat resized;
resize(bgr, resized, Size(target_w, target_h));
Mat rgb;
cvtColor(resized, rgb, COLOR_BGR2RGB);
rgb.convertTo(float_rgb, CV_32F, 1.0f / 255.0f);
}
int main() {
Mat bgr = imread("test.jpg");
const int WIDTH = 640, HEIGHT = 640;
vector<float> buffer(3 * HEIGHT * WIDTH);
Mat float_rgb;
cout << "\n======== 5种HWC->NCHW方法速度对比 ========\n" << endl;
// 方法1
preprocess(bgr, float_rgb, WIDTH, HEIGHT);
auto t1 = chrono::high_resolution_clock::now();
for(int i=0;i<100;i++)hwc_to_nchw_loop3(float_rgb,buffer.data());
auto t2=chrono::high_resolution_clock::now();
cout<<"方法1 loop3: "<<chrono::duration<float,milli>(t2-t1).count()/100<<" ms\n";
// 方法2
preprocess(bgr, float_rgb, WIDTH, HEIGHT);
auto t3=chrono::high_resolution_clock::now();
for(int i=0;i<100;i++)hwc_to_nchw_flat_ptr(float_rgb,buffer.data());
auto t4=chrono::high_resolution_clock::now();
cout<<"方法2 flat_ptr: "<<chrono::duration<float,milli>(t4-t3).count()/100<<" ms\n";
// 方法3
preprocess(bgr, float_rgb, WIDTH, HEIGHT);
auto t5=chrono::high_resolution_clock::now();
for(int i=0;i<100;i++)hwc_to_nchw_row_ptr(float_rgb,buffer.data());
auto t6=chrono::high_resolution_clock::now();
cout<<"方法3 row_ptr: "<<chrono::duration<float,milli>(t6-t5).count()/100<<" ms\n";
// 方法4
preprocess(bgr, float_rgb, WIDTH, HEIGHT);
auto t7=chrono::high_resolution_clock::now();
for(int i=0;i<100;i++)hwc_to_nchw_split(float_rgb,buffer.data());
auto t8=chrono::high_resolution_clock::now();
cout<<"方法4 split: "<<chrono::duration<float,milli>(t8-t7).count()/100<<" ms\n";
// 方法5
preprocess(bgr, float_rgb, WIDTH, HEIGHT);
auto t11=chrono::high_resolution_clock::now();
for(int i=0;i<100;i++)hwc_to_nchw_split_copy(float_rgb,buffer.data());
auto t12=chrono::high_resolution_clock::now();
cout<<"方法5 split_copy: "<<chrono::duration<float,milli>(t12-t11).count()/100<<" ms\n";
return 0;
}
五、速度对比结果(真实测试)
======== 5种HWC->NCHW方法速度对比 ========
方法1 loop3: 0.57 ms
方法2 flat_ptr: 0.48 ms
方法3 row_ptr: 0.47 ms
方法4 split: 0.17 ms ✅ 最快
方法5 split_copy: 0.21 ms
六、结论与工程建议
速度排名
split(方法4) > split_copy(方法5) > row_ptr(方法3) > flat_ptr(方法2) > loop3(方法1)
最佳方案推荐
- 追求极致速度 → 方法4 split(首选)
- 追求兼容性/稳定性 → 方法5 split_copy
- 教学/理解原理 → 方法1 / 方法2
- 工业部署正式使用 → 方法4 split(速度比手写循环快 2~3 倍)
七、适用场景
- YOLOv5 / YOLOv8 / YOLOv9 前处理
- TensorRT / ONNX 模型 CPU 前处理
- C++ 部署、嵌入式部署、纯 CPU 环境
更多推荐
所有评论(0)