图像格式转换与内存对齐详解

图像格式转换与内存对齐详解

涵盖 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?

  1. 内存对齐:硬件加速器(VPU/MPP)通常要求行起始地址对齐到 16/32/64/128 字节边界
  2. DMA 传输效率:对齐的行宽可以让 DDR burst 传输效率最高
  3. 视频解码输出:解码器内部 buffer 可能按更大 stride 分配
  4. 避免越界:某些硬件做缩放/裁剪时,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 × 行数独立对齐。原因:

  1. 行是内存访问的基本单位 --- 硬件逐行 DMA 传输,每行起始地址必须对齐
  2. stride 和 height 对齐是独立的 --- stride 由 DMA 宽度决定,height 由编解码宏块决定
  3. 不能用总像素数对齐替代 --- 比如 500×400=200000 像素对齐到某个值,并不能保证每行起始地址对齐

五、strideW 与 strideH

strideWstrideH 是 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 直接使用,不需要经过 MMU
  • vir_addr :虚拟地址,CPU 代码中用 memcpy(vir_addr, ...) 访问,由 mmapioremap 映射得到

嵌入式芯片上,视频 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 的 channelelement_sizestride_wstride_hbytes 完全相同,唯一的区别是 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 关联,指向同一块物理内存。

相关推荐
二宝哥2 小时前
Failed connect to mirrorlist.centos.org:80; Connection refused
linux·运维·centos
humors2212 小时前
一些安全类网站(不定期更新)
linux·网络·windows·安全·黑客·白帽
Kk.08022 小时前
Linux(九)fork复制进程与写时拷贝技术
linux·运维·服务器
一个人旅程~2 小时前
双系统时windows如何读取linux ext4格式硬盘分区?
linux·windows·经验分享·电脑
齐齐大魔王2 小时前
linux-进程详解
linux·运维·服务器
應呈2 小时前
Bootloader与OTA学习记录
linux·运维·服务器
勤自省2 小时前
在Ubuntu20.04上安装ROS
linux·ros
楼田莉子3 小时前
同步/异步日志系统:工具类以及日志的简单模块
linux·服务器·数据结构·c++
corpse20103 小时前
VirtualBox 安装ubuntu-25 ,配置SSH工具登录
linux·ubuntu·ssh