Day 2:图像内存、数据类型与stride

一、uint8 和 float32 到底是什么?

先不急着看 OpenCV。我们从最基础的 C++ 数据类型开始。

1. uint8:8 位无符号整数

例如:

复制代码
#include <cstdint>

std::uint8_t pixel = 180;

这里的 uint8_t 可以理解为:

使用 8 个二进制位存储一个非负整数。

8 位可以表示:

种不同的数值,因此范围是:

这也是为什么普通 8 位灰度图像的像素值通常为 0~255。

2. float32:32 位浮点数

例如:

复制代码
float pixel = 0.705882f;

在常见的平台上,C++ 的 float 使用 IEEE 754 32 位浮点格式,占 4 字节。

它可以表示小数,也可以表示 180.0 这样的大数值。

对比 uint8 float32
类型 无符号整数 浮点数
每个数值占用 1 字节 4 字节
常见数值 0、128、255 0.0、0.5、1.0
OpenCV 深度 CV_8U CV_32F
典型用途 普通图片数据 图像计算、神经网络输入

注意一个容易误解的问题:

float32 不是只能存储 0~1,而是可以存储大量不同范围的浮点数。

例如:

复制代码
float a = 180.0f;
float b = 0.705882f;

两者都是 float32。

因此数据类型和数值范围,是两个不同概念。

二、理解 CV_8UC3 和 CV_32FC3

昨天已经学了:

复制代码
CV_8UC3

它表示:

复制代码
8U → 8 位无符号整数
C3 → 三个通道

而:

复制代码
CV_32FC3

表示:

复制代码
32F → 32 位浮点数
C3  → 三个通道

两种格式的每像素内存差异:

所以对于同一张三通道图像:

换句话说,转换成 float32 后,同样数量的像素数据通常需要 4 倍内存。

这也是为什么你以后进行模型预处理时,要关注数据类型,不能觉得只是改了一个名字。

三、convertTo():数据类型转换不等于归一化

这是今天非常重要的一个知识点。

假设某个像素原来是:

复制代码
B = 30
G = 180
R = 50

原始类型:

复制代码
CV_8UC3

现在执行:

复制代码
cv::Mat imageFloat;

image.convertTo(
    imageFloat,
    CV_32F
);

注意,转换后:

复制代码
B = 30.0
G = 180.0
R = 50.0

它只是从整数变成浮点数。

数值本身并没有自动除以 255。

如果想同时归一化,需要:

复制代码
image.convertTo(
    imageFloat,
    CV_32F,
    1.0 / 255.0
);

此时:

复制代码
B = 30 / 255 ≈ 0.1176
G = 180 / 255 ≈ 0.7059
R = 50 / 255 ≈ 0.1961
这里为什么写 CV_32F 而不是 CV_32FC3?

你可能会问:

复制代码
image.convertTo(imageFloat, CV_32F);

原图是三通道,为什么不是 CV_32FC3?

因为 convertTo() 会保留原图的通道数,主要改变每个通道的数据深度。

所以:

复制代码
输入:CV_8UC3
          ↓
convertTo(CV_32F)
          ↓
输出:CV_32FC3

这里不用额外告诉它 C3。

四、图像在连续内存里到底怎么排列?

现在进入今天最关键的部分。

假设我们有一张 2×2 的 BGR 图像:

虽然看起来是二维图像,但计算机内存的地址是一维排列的。

对于连续 CV_8UC3 图像,数据按行存储:

复制代码
第 0 行:
10,20,30, 40,50,60

第 1 行:
70,80,90, 100,110,120

将两行连接起来:

复制代码
10,20,30,40,50,60,70,80,90,100,110,120

总共有:

Photo/Pasted image 20261009093010.png

假设从地址 0x1000 开始:

复制代码
地址        内容
0x1000      10   ← (0,0) 的 B
0x1001      20   ← (0,0) 的 G
0x1002      30   ← (0,0) 的 R

0x1003      40   ← (0,1) 的 B
0x1004      50   ← (0,1) 的 G
0x1005      60   ← (0,1) 的 R

0x1006      70   ← (1,0) 的 B
...

于是一个问题就出现了:

既然内存是一维的,为什么你可以直接通过 (row, col) 找到二维图像中的像素?

因为 OpenCV 记录了图像的形状、单像素大小和行跨度。

五、step 和 stride 到底是什么?

对于这张 2×2 的连续 CV_8UC3 图像:

复制代码
每行像素数:2
每像素字节数:3

因此每行数据占:

所以:

复制代码
image.step[0]

通常是:

复制代码
6

这就是行跨度(row stride)。

更准确地说,step[0] 表示相邻两行起始数据地址的字节距离。

内存布局图

但是,如果图像是 ROI(感兴趣区域),情况就不同了。

假设原图宽度是 6 像素,每像素占 3 字节:

复制代码
原图每行:
6 × 3 = 18 Bytes

从中裁出宽度为 3 像素的 ROI:

复制代码
ROI 每行有效数据:
3 × 3 = 9 Bytes

但 ROI 依然引用原图的内存,因此:

复制代码
ROI step[0] = 18 Bytes

而不是 9。

这就解释了:

复制代码
image.step[0]

为什么不能永远直接写成:

复制代码
image.cols * image.channels()

更一般地说,计算每行有效像素字节数应使用:

复制代码
image.cols * image.elemSize()

但它也不一定等于 step[0]。

六、亲手通过 at() 访问像素

现在我们已经知道图像的内存排列,再来理解:

复制代码
image.at<cv::Vec3b>(row, col)

就容易了。

这里:

复制代码
cv::Vec3b

表示由三个 uchar(8 位无符号整数)组成的向量,非常适合表示一个 CV_8UC3 像素。

例如:

复制代码
cv::Vec3b pixel =
    image.at<cv::Vec3b>(0, 0);

代表读取第 0 行、第 0 列的像素。

复制代码
pixel[0] // B
pixel[1] // G
pixel[2] // R

注意:

复制代码
image.at<cv::Vec3b>(0, 0)

的两个位置参数是:

复制代码
(row, col)

也就是:

复制代码
(y, x)

而不是 (x, y)。(笛卡尔坐标系)

今天的完整编程实验

建议新建建一个独立的 mat_memory_demo.cpp,不要直接破坏之前已经跑通的 ImageProcessor 类。

复制代码
#include <opencv2/opencv.hpp>
#include <iostream>

int main() {
    // 1. 创建2行2列、3通道的8位图像
    cv::Mat image(2, 2, CV_8UC3);

    // 2. 人工设置四个像素的 BGR 值
    image.at<cv::Vec3b>(0, 0) = cv::Vec3b(10, 20, 30);
    image.at<cv::Vec3b>(0, 1) = cv::Vec3b(40, 50, 60);
    image.at<cv::Vec3b>(1, 0) = cv::Vec3b(70, 80, 90);
    image.at<cv::Vec3b>(1, 1) = cv::Vec3b(100, 110, 120);

    // 3. 逐个打印像素
    for (int row = 0; row < image.rows; ++row) {
        for (int col = 0; col < image.cols; ++col) {
            cv::Vec3b pixel = image.at<cv::Vec3b>(row, col);

            std::cout << "(" << row << "," << col << ") "
                      << "B=" << static_cast<int>(pixel[0])
                      << " G=" << static_cast<int>(pixel[1])
                      << " R=" << static_cast<int>(pixel[2])
                      << '\n';
        }
    }

    // 4. 打印内存信息
    std::cout << "\n--- uint8 image ---\n";
    std::cout << "Data address: "
              << static_cast<const void*>(image.data) << '\n';
    std::cout << "Step: " << image.step[0] << '\n';
    std::cout << "Elem size: " << image.elemSize() << '\n';
    std::cout << "Total bytes: "
              << image.total() * image.elemSize() << '\n';

    // 5. 转换为 float32,不归一化
    cv::Mat floatImage;
    image.convertTo(floatImage, CV_32F);

    // 6. 转换为 float32,同时归一化
    cv::Mat normalizedImage;
    image.convertTo(normalizedImage, CV_32F, 1.0 / 255.0);

    std::cout << "\n--- float32 image ---\n";
    std::cout << "Elem size: " << floatImage.elemSize() << '\n';
    std::cout << "Step: " << floatImage.step[0] << '\n';

    cv::Vec3f fp = floatImage.at<cv::Vec3f>(0, 0);
    cv::Vec3f np = normalizedImage.at<cv::Vec3f>(0, 0);

    std::cout << "Float BGR: "
              << fp[0] << ", " << fp[1] << ", " << fp[2] << '\n';

    std::cout << "Normalized BGR: "
              << np[0] << ", " << np[1] << ", " << np[2] << '\n';

    // 7. BGR 转 RGB
    cv::Mat rgb;
    cv::cvtColor(image, rgb, cv::COLOR_BGR2RGB);

    cv::Vec3b rgbPixel = rgb.at<cv::Vec3b>(0, 0);

    std::cout << "\nRGB: "
              << static_cast<int>(rgbPixel[0]) << ", "
              << static_cast<int>(rgbPixel[1]) << ", "
              << static_cast<int>(rgbPixel[2]) << '\n';

    return 0;
}

因为这里 floatImage 是 CV_32FC3,所以访问像素时使用:

复制代码
floatImage.at<cv::Vec3f>(0, 0)

而不是继续使用:

复制代码
floatImage.at<cv::Vec3b>(0, 0)

一定要让像素访问的数据类型与 Mat 实际存储类型匹配。

你应该观察到这些关键结果:

对比项目 CV_8UC3 CV_32FC3
channels() 3 3
elemSize() 3 12
step[0] 6 24
像素总字节数 12 48
第一个像素的 B 值(未归一化) 10 10.0

另外,cv::COLOR_BGR2RGB 会交换蓝、红通道:

复制代码
BGR:(10, 20, 30)
           ↓
RGB:(30, 20, 10)

注意 cvtColor() 改变了通道的含义和数值排列,但结果通常仍然是 CV_8UC3。

七、为什么模型推理还需要 HWC → CHW?

这是今天和未来 TensorRT 部署关系最密切的部分。

假设你现在有一张 2×2 的三通道图像。

OpenCV 默认常见的三通道图像采用 HWC、通道交错(interleaved) 排列:

复制代码
H = Height
W = Width
C = Channels

比如:

复制代码
像素1:R1 G1 B1
像素2:R2 G2 B2
像素3:R3 G3 B3
像素4:R4 G4 B4

那么 HWC 的连续内存顺序是:

复制代码
R1 G1 B1 | R2 G2 B2 | R3 G3 B3 | R4 G4 B4

但许多 CNN 模型的输入采用 CHW(通常再加一个批次维度 N,形成 NCHW):

复制代码
先放完整的 R 通道
再放完整的 G 通道
最后放完整的 B 通道

因此内存排列变成:

复制代码
R1 R2 R3 R4 | G1 G2 G3 G4 | B1 B2 B3 B4
对比 HWC 和 CHW

请特别注意:HWC → CHW 不是简单把 BGR 转成 RGB。

这两个操作解决的问题不一样。

操作 改变什么
BGR → RGB 颜色通道的顺序
uint8 → float32 数据类型
/255.0 数值范围
HWC → CHW 张量的维度和数据组织方式

这四件事在模型预处理中经常一起发生,但必须分别理解。

例如一个常见的模型输入处理流程:

复制代码
OpenCV 读图
H × W × 3,BGR,uint8
           ↓
resize
           ↓
BGR → RGB
           ↓
uint8 → float32
           ↓
归一化(按模型要求)
           ↓
HWC → CHW
           ↓
增加 Batch 维度
           ↓
N × C × H × W
           ↓
模型推理

但并不是每个模型都要求 NCHW、RGB 或 0~1 归一化。以后实际部署时,需要以模型的输入契约为准,不能一律套用。

相关推荐
ADAI_Bowen1 小时前
个人设计师用户心得:ADAI如何成为我的“第二大脑”
人工智能
一次调频AGCAVC1 小时前
一次调频系统控制柜的组成及一次调频装置主机功能详述
人工智能·嵌入式硬件
重生之小比特1 小时前
【算法拾光】模拟|提莫攻击 + N 字形变换
算法·模拟
虎鲸不是鱼1 小时前
【虎鲸养殖手册 B007】命令执行的安全细节:白名单不是防弹衣
人工智能·安全·agent·智能体·自进化agent
道尔柯南1 小时前
C++ 内存管理详解:从 malloc/free 到 new/delete
c++·面试
ZhengEnCi1 小时前
为什么别人的 Skill 不适合你:从「拿来就能用」到「改成自己的流程」的适配方法
人工智能·agent
一切皆是因缘际会1 小时前
当模型足够大
人工智能·深度学习·机器学习
東隅已逝,桑榆非晚1 小时前
AVL数实现
c++·笔记·学习·算法
摇滚侠1 小时前
《大模型是怎样跑起来的(轻科技)》硬件篇 阅读笔记
人工智能·笔记·科技