在 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 前处理流程

  1. 读取图像(BGR)
  2. 缩放尺寸到 640×640
  3. BGR → RGB
  4. 归一化 /255.0
  5. 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)

最佳方案推荐

  1. 追求极致速度方法4 split(首选)
  2. 追求兼容性/稳定性方法5 split_copy
  3. 教学/理解原理 → 方法1 / 方法2
  4. 工业部署正式使用方法4 split(速度比手写循环快 2~3 倍)

七、适用场景

  • YOLOv5 / YOLOv8 / YOLOv9 前处理
  • TensorRT / ONNX 模型 CPU 前处理
  • C++ 部署、嵌入式部署、纯 CPU 环境

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐