ARGB8888 → RGB565 像素格式转换详解

在嵌入式 Linux 显示开发中,ARGB8888 到 RGB565 的像素格式转换是一项高频操作。无论是帧缓冲区(framebuffer)输出、DRM/KMS 显示管线,还是 GUI 渲染引擎,都可能涉及这一转换。本文从原理到实现,系统梳理这一转换的方方面面。


一、两种格式的基本结构

ARGB8888(32位/像素)

每个像素占用 4 字节,各通道均为 8 位,取值范围 0255:

表格

字节偏移 通道 位数 取值范围
Byte 3 Alpha (A) 8 bit 0255
Byte 2 Red (R) 8 bit 0255
Byte 1 Green (G) 8 bit 0255
Byte 0 Blue (B) 8 bit 0255

注意:字节序(大端/小端)会影响实际内存排列,以下代码示例均基于小端序(x86/ARM 默认)。

RGB565(16位/像素)

每个像素占用 2 字节,无 Alpha 通道,位域分配如下:

Bit:  15 14 13 12 11 | 10  9  8  7  6  5 |  4  3  2  1  0
      ─────────────── ────────────────── ──────────────────
        R4 R3 R2 R1 R0   G5 G4 G3 G2 G1 G0   B4 B3 B2 B1 B0

表格

通道 位数 取值范围 位掩码
Red 5 bit 031 0xF800
Green 6 bit 063 0x07E0
Blue 5 bit 031 0x001F

绿色通道多分配 1 位,是因为人眼对绿色光谱最为敏感。


二、转换原理

核心思路

ARGB8888 → RGB565 的本质是位宽压缩 + 丢弃 Alpha 通道

  1. 丢弃 Alpha 通道:ARGB8888 的 A 通道在 RGB565 中不存在,直接忽略。
  2. 位宽截断:将 8 位通道压缩到 5 位或 6 位,保留高位,丢弃低位。
  3. 位域拼接:将截断后的 R、G、B 按 RGB565 的位域布局组合成 16 位值。

位操作详解

ARGB8888:  [A7..A0] [R7..R0] [G7..G0] [B7..B0]
                                    ↓ 转换
RGB565:    [R7..R3] [G7..G2] [B7..B3]
            \__5bit__/ \__6bit__/ \__5bit__/

具体操作:

  • R:取 8 位的高 5 位,右移 3 位 → R8 >> 3
  • G:取 8 位的高 6 位,右移 2 位 → G8 >> 2
  • B:取 8 位的高 5 位,右移 3 位 → B8 >> 3

三、代码实现

基础版:直接位移

#include <stdint.h>

/**
 * ARGB8888 转 RGB565(基础版)
 * @param argb  32位 ARGB8888 像素值(小端序:0xAARRGGBB)
 * @return      16位 RGB565 像素值
 */
static inline uint16_t argb8888_to_rgb565(uint32_t argb)
{
    uint8_t r = (argb >> 16) & 0xFF;  // 提取 R 通道
    uint8_t g = (argb >>  8) & 0xFF;  // 提取 G 通道
    uint8_t b = (argb >>  0) & 0xFF;  // 提取 B 通道

    return (uint16_t)((r >> 3) << 11) |   // R: 高5位,放到 bit[15:11]
                      ((g >> 2) <<  5) |   // G: 高6位,放到 bit[10:5]
                       (b >> 3);           // B: 高5位,放到 bit[4:0]
}

优化版:减少中间变量

/**
 * ARGB8888 转 RGB565(优化版,减少移位次数)
 * 利用整体右移来减少操作
 */
static inline uint16_t argb8888_to_rgb565_fast(uint32_t argb)
{
    // 直接对 32 位值做位移和掩码
    return (uint16_t)(((argb >> 8) & 0xF800) |  // R: (argb>>8) 后取高5位
                      ((argb >> 5) & 0x07E0) |  // G: (argb>>5) 后取高6位
                      ((argb >> 3) & 0x001F));  // B: (argb>>3) 后取高5位
}

批量转换:整帧缓冲区

/**
 * 批量将 ARGB8888 帧缓冲区转换为 RGB565
 * @param src   源缓冲区(ARGB8888,4字节/像素)
 * @param dst   目标缓冲区(RGB565,2字节/像素)
 * @param count 像素总数(width × height)
 */
void convert_argb8888_to_rgb565(const uint32_t *src,
                                 uint16_t *dst,
                                 uint32_t count)
{
    for (uint32_t i = 0; i < count; i++) {
        uint32_t pixel = src[i];
        dst[i] = (uint16_t)(((pixel >> 8) & 0xF800) |
                            ((pixel >> 5) & 0x07E0) |
                            ((pixel >> 3) & 0x001F));
    }
}

高精度版:带四舍五入

直接右移会截断低位,导致颜色偏暗。Apple 的 vImage 库采用四舍五入策略来减少误差:

/**
 * ARGB8888 转 RGB565(带四舍五入,精度更高)
 * 参考 Apple vImage 的转换公式
 */
static inline uint16_t argb8888_to_rgb565_round(uint32_t argb)
{
    uint8_t r = (argb >> 16) & 0xFF;
    uint8_t g = (argb >>  8) & 0xFF;
    uint8_t b = (argb >>  0) & 0xFF;

    // 四舍五入公式(来自 Apple vImage)
    uint16_t r5 = (uint16_t)((r * (31 * 2) + 255) / (255 * 2));  // 0~31
    uint16_t g6 = (uint16_t)((g * 63 + 127) / 255);              // 0~63
    uint16_t b5 = (uint16_t)((b * 31 + 127) / 255);              // 0~31

    return (r5 << 11) | (g6 << 5) | b5;
}

四、精度损失与色带问题

为什么会有色带(Color Banding)?

ARGB8888 可表示约 1677 万色(2²⁴),而 RGB565 仅能表示 65536 色(2¹⁶)。转换时低位信息被永久丢弃,在渐变区域会出现明显的色阶断层。

常用颜色对照

表格

颜色 ARGB8888 RGB565
纯黑 0xFF000000 0x0000
纯白 0xFFFFFFFF 0xFFFF
纯红 0xFFFF0000 0xF800
纯绿 0xFF00FF00 0x07E0
纯蓝 0xFF0000FF 0x001F
黄色 0xFFFFFF00 0xFFE0
青色 0xFF00FFFF 0x07FF
洋红 0xFFFF00FF 0xF81F

缓解色带的方案:抖动(Dithering)

在转换前对像素值加入微小随机噪声,打散色阶边界,视觉上可显著改善渐变效果。Apple vImage 提供了带抖动参数的转换接口。

// 简单的有序抖动示例(Bayer 2x2 矩阵)
static const int bayer[2][2] = {{0, 2}, {3, 1}};

static inline uint16_t argb8888_to_rgb565_dither(uint32_t argb, int x, int y)
{
    int threshold = bayer[y % 2][x % 2] - 2; // -2 ~ +1

    int r = ((argb >> 16) & 0xFF) + threshold;
    int g = ((argb >>  8) & 0xFF) + threshold;
    int b = ((argb >>  0) & 0xFF) + threshold;

    // 钳位到 0~255
    r = r < 0 ? 0 : (r > 255 ? 255 : r);
    g = g < 0 ? 0 : (g > 255 ? 255 : g);
    b = b < 0 ? 0 : (b > 255 ? 255 : b);

    return (uint16_t)((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3);
}

五、在 Linux DRM/VKMS 中的应用

在 Linux 内核的 DRM 子系统中,VKMS(Virtual KMS)驱动实现了像素格式转换的框架。其核心思路是:

  1. 读取阶段:通过 pixel_read 回调将源格式(如 ARGB8888)转换为内部统一格式 pixel_argb_u16
  2. 合成阶段:在 pixel_argb_u16 格式下完成 alpha 混合。
  3. 写回阶段:通过 pixel_write 回调将合成结果转换为目标格式(如 RGB565)。

其中 RGB565 的写回函数实现如下(来自内核源码):

static void argb_u16_to_RGB565(u8 *dst_pixels, struct pixel_argb_u16 *in_pixel)
{
    u16 *pixels = (u16 *)dst_pixels;
    s32 r = in_pixel->r >> 8;  // u16 转 u8
    s32 g = in_pixel->g >> 8;
    s32 b = in_pixel->b >> 8;

    *pixels = cpu_to_le16(r << 11 | g << 5 | b);
}

六、总结

对比项 ARGB8888 RGB565
位深 32 bit/px 16 bit/px
显存占用 4 × W × H 2 × W × H
颜色数 ~1677万 65536
Alpha 通道 ✅ 支持 ❌ 不支持
适用场景 高质量渲染、合成 资源受限的显示输出

ARGB8888 → RGB565 的转换本质上是一次有损压缩:丢弃 Alpha 通道,并将 R/B 从 8 位截断到 5 位、G 从 8 位截断到 6 位。在显存紧张的场景下(如 8MB RAM 堆),切换到 RGB565 可以将帧缓冲区占用减半,是嵌入式显示系统中非常实用的优化手段。

Logo

社区规范:仅讨论OpenHarmony相关问题。

更多推荐