图像格式转换与内存对齐详解
涵盖 RGB/BGR/NV12 格式互转、stride/高度对齐、嵌入式芯片内存布局等核心概念。
一、图像格式说明
1.1 RGB / BGR
-
RGB:每像素 3 字节,通道顺序 R→G→B
-
BGR:每像素 3 字节,通道顺序 B→G→R(OpenCV 默认格式)
-
内存布局:逐行存储,总大小 =
width × height × 3内存排列(4×2 图像):
R G B R G B R G B R G B ← 第 0 行
R G B R G B R G B R G B ← 第 1 行
1.2 NV12(YUV 4:2:0 交织格式)
-
Y 平面 :
strideW × strideH字节,每个像素对应一个亮度值 Y -
UV 交织平面 :
strideW × (strideH / 2)字节,U(Cb) 和 V(Cr) 交替存储 -
每 2×2 像素块共享一组 UV(4:2:0 子采样)
-
总大小 =
strideW × strideH × 3 / 2内存布局示意(4×4 图像):
Y Y Y Y ← Y 平面 (4×4)
Y Y Y Y
Y Y Y Y
Y Y Y Y
U V U V ← UV 交织平面 (4×2)
U V U V
1.3 RGB 与 NV12 的对比
| 属性 | RGB/BGR | NV12 |
|---|---|---|
| 每像素字节数 | 3 | 1.5(Y=1, UV 共享 0.5) |
| 色彩保真 | 无损 | UV 子采样有损 |
| 适用场景 | 图像处理、显示 | 视频编解码、摄像头输出 |
| 典型来源 | 软件生成 | 硬件 VPU/ISP 输出 |
二、色彩空间转换公式(BT.601 Full Range)
2.1 RGB → YUV
Y = 0.299 × R + 0.587 × G + 0.114 × B
U = -0.168736 × R - 0.331264 × G + 0.5 × B + 128
V = 0.5 × R - 0.418688 × G - 0.081312 × B + 128
2.2 YUV → RGB
R = Y + 1.402 × (V - 128)
G = Y - 0.344136 × (U - 128) - 0.714136 × (V - 128)
B = Y + 1.772 × (U - 128)
2.3 定点化实现(20-bit 精度)
为避免浮点运算,将系数乘以 2^20 后取整:
c
#define SHIFT 20
#define HALF (1 << (SHIFT - 1)) // 四舍五入
// RGB → YUV 系数
#define COEFF_YR 313524 // 0.299 × 2^20
#define COEFF_YG 615600 // 0.587 × 2^20
#define COEFF_YB 119592 // 0.114 × 2^20
#define COEFF_UR (-176858) // -0.168736 × 2^20
#define COEFF_UG (-347358) // -0.331264 × 2^20
#define COEFF_UB 524288 // 0.5 × 2^20
#define COEFF_VR 524288 // 0.5 × 2^20
#define COEFF_VG (-439150) // -0.418688 × 2^20
#define COEFF_VB (-85249) // -0.081312 × 2^20
// YUV → RGB 系数
#define COEFF_VR_INV 1469052 // 1.402 × 2^20
#define COEFF_UG_INV (-360560)// -0.344136 × 2^20
#define COEFF_VG_INV (-748438)// -0.714136 × 2^20
#define COEFF_UB_INV 1856376 // 1.772 × 2^20
// 使用示例
int y = (COEFF_YR * r + COEFF_YG * g + COEFF_YB * b + HALF) >> SHIFT;
三、Width vs Stride
3.1 核心概念
-
Width :图像的有效像素宽度,即实际可见的图像数据列数
-
Stride (也叫 pitch/step):图像在内存中一行所占的实际字节数,包含对齐填充
内存布局示意(width=640, stride=768):
|<--- width (640 pixels) --->|<- padding ->|
| 有效像素数据 (640×3 bytes) | 填充字节 | ← 第0行
| 有效像素数据 (640×3 bytes) | 填充字节 | ← 第1行
| ... | ... |
|<----------- stride (768×3 bytes) -------->|
3.2 为什么需要 Stride?
- 内存对齐:硬件加速器(VPU/MPP)通常要求行起始地址对齐到 16/32/64/128 字节边界
- DMA 传输效率:对齐的行宽可以让 DDR burst 传输效率最高
- 视频解码输出:解码器内部 buffer 可能按更大 stride 分配
- 避免越界:某些硬件做缩放/裁剪时,stride 不等于 width 可以让 buffer 复用更灵活
3.3 实际对齐例子
图像分辨率: 1920×1080, stride 对齐到 64
Y 平面 stride: 1920 → 对齐到 64 → 1920 (刚好对齐)
UV 平面 stride: 1920
图像分辨率: 1080×720, stride 对齐到 64
Y 平面 stride: 1080 → 对齐到 64 → 1088 (补了8字节)
图像分辨率: 500×400, stride 对齐到 16
Y 平面 stride: 500 → 对齐到 16 → 512
四、三种对齐详解
嵌入式芯片上存在三种对齐需求:
4.1 宽度对齐(Stride / strideW)
-
作用对象:每行字节数
-
典型要求:对齐到 16/32/64/128 字节
-
原因:DMA burst 传输效率、硬件行缓存对齐
width=500, align=64 → stride=512 (每行多12字节padding)
|<-- 500 有效像素 -->| 12字节pad |
|<-------- 512 stride -------->|
4.2 高度对齐(strideH / aligned_height)
-
作用对象:图像总行数
-
典型要求:对齐到 2(NV12 最小要求)/ 16(宏块对齐)
-
原因:视频编解码以宏块(16×16/32×32)为单位,高度不够要补齐
实际图像 500×401,高度对齐到 16 → 分配 500×416
|<-- 有效区域 401 行 -->|
|<-- 15 行 padding -->| ← 黑色填充行
为什么需要高度对齐?
- H.264/H.265 编码以 16×16 宏块为单位,图像高度不是 16 的倍数时需要补齐
- NV12 格式要求高度至少对齐到 2
- 视频编解码器通常要求高度对齐到 16
4.3 Buffer 地址对齐
- 作用对象:帧缓冲区起始物理地址
- 典型要求:4K/64K 页对齐
- 原因:DMA 引擎要求物理地址对齐
- 处理方式:由内存分配器保证(如专用连续物理内存分配器),不由转换代码处理
4.4 三种对齐的对比
| 对齐类型 | 作用对象 | 典型要求 | 常见场景 |
|---|---|---|---|
| 宽度 stride | 每行字节数 | 对齐到 16/32/64/128 | 所有硬件 VPU/ISP/Display |
| 高度对齐 | 总行数 | 对齐到 2/16/32 | 编码器输入、解码器输出 |
| buffer 地址 | 内存起始地址 | 4K/64K 页对齐 | DMA 连续内存分配 |
4.5 为什么不是"像素数量对齐"?
不是 width × height 整体对齐,而是行级别 stride × 行数独立对齐。原因:
- 行是内存访问的基本单位 --- 硬件逐行 DMA 传输,每行起始地址必须对齐
- stride 和 height 对齐是独立的 --- stride 由 DMA 宽度决定,height 由编解码宏块决定
- 不能用总像素数对齐替代 --- 比如 500×400=200000 像素对齐到某个值,并不能保证每行起始地址对齐
五、strideW 与 strideH
strideW 和 strideH 是 SDK 文档中对宽度 stride 和高度 stride 的常见叫法:
strideW = 水平方向 stride,即每行字节数(含 padding)
strideH = 垂直方向 stride,即对齐后的总行数
5.1 strideW 就是每行字节数
strideW 的本质是每行字节数,这是硬件对齐的直接约束。不存在"像素宽度对齐"这种独立概念------NV12 的 Y 平面看起来像像素对齐,只是因为每像素恰好 1 字节,数值相同而已。
NV12 Y 平面: strideW = align_up(width × 1, 64) = align_up(500, 64) = 512
RGB888: strideW = align_up(width × 3, 64) = align_up(1500, 64) = 1536
// 有些人看到 NV12 strideW=512 以为是对"像素宽度 500 对齐到 512",
// 这只是巧合------因为 Y 每像素 1 字节,对齐字节数和对齐像素数结果一样。
// RGB 每像素 3 字节,strideW=1536,就不是像素宽度了。
六、NV12 内存布局详解(含 stride 和高度对齐)
NV12 (width=500, height=401, strideW=512字节, strideH=416):
Y 平面: strideW × strideH 字节
|<--- width --->|<- pad ->|
| Y Y Y Y ... Y | 0 0 0 | ← 第 0 行
| Y Y Y Y ... Y | 0 0 0 | ← 第 1 行
| ... ...
| Y Y Y Y ... Y | 0 0 0 | ← 第 400 行 (最后一行有效数据)
| 0 0 0 0 ... 0 | 0 0 0 | ← 第 401 行 (高度 padding 开始)
| ... ...
| 0 0 0 0 ... 0 | 0 0 0 | ← 第 415 行 (对齐后最后一行)
UV 平面: strideW × (strideH / 2) 字节
| U V U V ... U V | 0 0 0 | ← 第 0 行
| ... ...
| U V U V ... U V | 0 0 0 | ← 第 200 行 (最后一行有效数据)
| 0 0 0 0 ... 0 0 | 0 0 0 | ← 第 201 行 (高度 padding)
| ... ...
| 0 0 0 0 ... 0 0 | 0 0 0 | ← 第 207 行
总大小 = strideW × strideH + strideW × (strideH / 2)
= 512 × 416 + 512 × 208
= 212992 + 106496
= 319488 字节
关键 :UV 平面偏移量 = strideW × strideH(不是 strideW × height!)
七、IMAGE_S 结构体详解
c
struct IMAGE_S
{
IMAGE_TYPE_E type; // 图像格式类型
uint32_t width; // 有效像素宽度
uint32_t height; // 有效像素高度
uint32_t channel; // 通道数
uint32_t element_size; // 每个元素的字节数
uint32_t stride_w; // 水平 stride(每行字节数,含 padding)
uint32_t stride_h; // 垂直 stride(对齐后的总行数)
uint32_t bytes; // 整个 buffer 的总字节数
uint64_t phy_addr; // 物理地址(DMA 硬件访问用)
void* vir_addr; // 虚拟地址(CPU 软件访问用)
};
7.1 各字段详解
| 字段 | 含义 | 示例值 |
|---|---|---|
type |
图像格式枚举,如 RGB/BGR/NV12/YUV420 等 | IMAGE_TYPE_NV12 |
width |
图像有效像素列数 | 500 |
height |
图像有效像素行数 | 401 |
channel |
每个像素的通道数 | RGB=3, NV12的Y=1, 灰度=1 |
element_size |
单个元素占的字节数(像素位深/8) | uint8=1, float=4 |
stride_w |
每行字节数(含 padding) | 1536 (RGB) / 512 (NV12 Y) |
stride_h |
对齐后的总行数 | 416 |
bytes |
buffer 总大小 | 见下方计算 |
phy_addr |
物理内存地址,供 DMA/VPU 等硬件访问 | 0x82000000 |
vir_addr |
虚拟地址,供 CPU 通过指针读写 | mmap 映射后的指针 |
7.2 channel × element_size = 每像素字节数
| 图像类型 | channel | element_size | 每像素字节 |
|---|---|---|---|
| RGB888 | 3 | 1 | 3 |
| BGR888 | 3 | 1 | 3 |
| RGBA | 4 | 1 | 4 |
| 灰度图 | 1 | 1 | 1 |
| float32 灰度 | 1 | 4 | 4 |
| NV12 的 Y 平面 | 1 | 1 | 1 |
7.3 bytes 的计算
c
// RGB / BGR (单平面)
// stride_w 已经是每行字节数,直接乘行数
bytes = stride_w * stride_h
// NV12 (双平面: Y + UV交织)
bytes = stride_w * stride_h + stride_w * (stride_h / 2)
// 灰度 (单通道单平面)
bytes = stride_w * stride_h
7.4 phy_addr vs vir_addr
┌──────────────────────────────────────────────┐
│ 物理内存 (DDR) │
│ ┌──────────────────────────────────────┐ │
│ │ 图像数据 buffer │ │
│ │ 物理地址: 0x8200_0000 (phy_addr) │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
▲ ▲
│ DMA 传输 │ CPU 读写
│ (硬件直接用物理地址) │ (通过 MMU 映射)
│ │
┌────┴─────┐ ┌─────┴──────┐
│ VPU/ISP │ │ CPU │
│ DMA引擎 │ │ 软件处理 │
└──────────┘ └────────────┘
vir_addr = mmap(phy_addr) 的结果
CPU 通过 vir_addr 读写 ↔ 硬件通过 phy_addr DMA
phy_addr:物理地址,硬件 DMA 引擎/ISP/VPU 直接使用,不需要经过 MMUvir_addr:虚拟地址,CPU 代码中用memcpy(vir_addr, ...)访问,由mmap或ioremap映射得到
嵌入式芯片上,视频 buffer 通常从专用连续物理内存池分配,以保证物理地址连续。
7.5 对齐的本质:strideW 是每行字节数
strideW 的本质是每行字节数,这是硬件 DMA 对齐的直接约束。
NV12 的 Y 平面每像素 1 字节,所以"对齐像素宽度"和"对齐字节数"数值恰好相同,容易让人误解。但 RGB 每像素 3 字节,两者完全不同:
c
// NV12 Y 平面: 每像素 1 字节
int stride_w = align_up(width * 1, 64); // align_up(500, 64) = 512
// RGB888: 每像素 3 字节
int stride_w = align_up(width * 3, 64); // align_up(1500, 64) = 1536
// 灰度 float32: 每像素 4 字节
int stride_w = align_up(width * 4, 64); // align_up(2000, 64) = 2048
统一定义:stride_w = align_up(width × channel × element_size, align_value)
7.6 RGB 实例
c
// RGB888 图像, width=500, height=401
// 硬件要求:每行字节数对齐到 64
struct IMAGE_S img = {
.type = IMAGE_TYPE_RGB,
.width = 500,
.height = 401,
.channel = 3, // R, G, B 三通道
.element_size = 1, // uint8_t
// stride_w = align_up(width × channel × element_size, 64)
// = align_up(500 × 3 × 1, 64) = align_up(1500, 64) = 1536
.stride_w = 1536, // 每行字节数(含 padding)
.stride_h = 416, // align_up(401, 16) = 416
.bytes = 1536 * 416, // = 638976
.phy_addr = 0x82000000,
.vir_addr = mmap_ptr,
};
// 访问第 j 行第 i 列的 RGB 值:
uint8_t *row = (uint8_t *)img.vir_addr + j * img.stride_w;
uint8_t r = row[i * 3 + 0];
uint8_t g = row[i * 3 + 1];
uint8_t b = row[i * 3 + 2];
RGB 内存布局 (width=500, stride_w=1536字节, stride_h=416):
|<--- width×3=1500 字节 --->|<- padding=36 字节 ->|
| R G B R G B ... | 0 0 0 0 0 0 ... | ← 第 0 行
| R G B R G B ... | 0 0 0 0 0 0 ... | ← 第 1 行
| ... ...
| R G B R G B ... | 0 0 0 0 0 0 ... | ← 第 400 行 (最后有效行)
| 0 0 0 0 0 0 ... | 0 0 0 0 0 0 ... | ← 第 401~415 行 (高度padding)
|<----------------- stride_w = 1536 字节 ---------------->|
总大小 = stride_w × stride_h = 1536 × 416 = 638976 字节
7.7 BGR 实例
c
// BGR888 图像, width=500, height=401
struct IMAGE_S img = {
.type = IMAGE_TYPE_BGR,
.width = 500,
.height = 401,
.channel = 3, // B, G, R 三通道
.element_size = 1, // uint8_t
.stride_w = 1536, // align_up(500×3×1, 64) = 1536
.stride_h = 416, // align_up(401, 16) = 416
.bytes = 1536 * 416, // = 638976
.phy_addr = 0x83000000,
.vir_addr = mmap_ptr2,
};
// 访问第 j 行第 i 列的 BGR 值:
uint8_t *row = (uint8_t *)img.vir_addr + j * img.stride_w;
uint8_t b = row[i * 3 + 0];
uint8_t g = row[i * 3 + 1];
uint8_t r = row[i * 3 + 2];
注意 :BGR 与 RGB 的 channel、element_size、stride_w、stride_h、bytes 完全相同,唯一的区别是 type 标识不同,以及内存中通道排列顺序不同(B 在前 vs R 在前)。
7.8 stride_w 计算总结
c
// stride_w = 每行字节数(含 padding),统一计算公式:
stride_w = align_up(width × channel × element_size, align_value)
// NV12 Y 平面: align_up(500 × 1 × 1, 64) = 512
// NV12 UV 平面: align_up(500 × 1 × 1, 64) = 512 (UV交织,每对2字节,行宽与Y相同)
// RGB888: align_up(500 × 3 × 1, 64) = 1536
// RGBA: align_up(500 × 4 × 1, 64) = 2048
// float32 灰度: align_up(500 × 1 × 4, 64) = 2048
// 访问第 j 行的起始地址:
uint8_t *row = (uint8_t *)vir_addr + j * stride_w; // 直接用 stride_w,无需再乘其他值
7.9 完整 NV12 实例
c
struct IMAGE_S img = {
.type = IMAGE_TYPE_NV12,
.width = 500,
.height = 401,
.channel = 1, // NV12 每个平面按单通道看待
.element_size = 1, // uint8_t
.stride_w = 512, // align_up(500 × 1 × 1, 64) = 512
.stride_h = 416, // align_up(401, 16) = 416
.bytes = 512*416 + 512*(416/2), // = 319488
.phy_addr = 0x82000000,
.vir_addr = mmap_ptr,
};
// 访问第 j 行第 i 列的 Y 值:
uint8_t *y_plane = (uint8_t *)img.vir_addr;
uint8_t y = y_plane[j * img.stride_w + i];
// 访问第 j 行第 i 列的 UV:
uint8_t *uv_plane = (uint8_t *)img.vir_addr + img.stride_w * img.stride_h;
uint8_t u = uv_plane[(j/2) * img.stride_w + (i & ~1) + 0];
uint8_t v = uv_plane[(j/2) * img.stride_w + (i & ~1) + 1];
八、典型对齐需求
| 场景 | stride 对齐 | 高度对齐 (编码) | 高度对齐 (NV12) |
|---|---|---|---|
| 视频编解码 | 64/128 字节 | 16 | 2 |
| 通用图像处理 | 16/32 字节 | 16 | 2 |
| GPU 纹理 | 32 字节 | 1 | 2 |
对齐辅助函数
c
// 将值向上对齐到 alignment 的整数倍
static inline int align_up(int value, int alignment)
{
return (value + alignment - 1) & ~(alignment - 1);
}
// 使用示例
int y_stride = align_up(width * 1, 64); // NV12 Y: 每像素1字节,对齐到 64
int a_height = align_up(height, 16); // 高度对齐到 16
int buf_size = y_stride * a_height + y_stride * (a_height / 2); // NV12 总大小
uint8_t *buf = aligned_alloc(4096, buf_size); // buffer 4K 地址对齐
九、常见问题
Q1: height=400 时对齐到 16 还是 400?
align_up(400, 16) = 400,因为 400 已经是 16 的倍数,无需补齐。只有当 height 不是对齐值的倍数时才会有 padding 行。
Q2: 写入和读取的 aligned_height 不一致会怎样?
UV 平面偏移量计算错误,导致颜色完全错乱。写入和读取必须使用同一个 aligned_height。
Q3: NV12 为什么要求高度为偶数?
因为 UV 平面高度是 Y 平面的一半(4:2:0 子采样),奇数高度会导致 UV 行数不是整数。
Q4: RGB 转 NV12 再转回来为什么有误差?
NV12 的 4:2:0 子采样是有损的:2×2 像素块共享一组 UV,丢失了色彩细节。另外定点化系数也有 ±1 级的量化误差。典型 round-trip 最大像素差约 2 级灰度。
Q5: phy_addr 和 vir_addr 能否只用一个?
不能。硬件(DMA/VPU)只认物理地址,CPU 只认虚拟地址。两者通过 mmap/ioremap 关联,指向同一块物理内存。