在 C 中拆分 RGB 值的有效方法
Efficient way to split up RGB values in C
我正在用 C 为 32 位 cortex M0 微控制器编写一些软件,并且我正在对 32 位 RGB 值进行大量操作。它们以 32 位整数格式处理,如 0x00BBRRGG
。我希望能够用它们进行数学运算而不用担心进位位在颜色之间溢出,所以我需要将它们分成三个 uint8 值。有没有一种有效的方法来做到这一点?我假设效率低下的方式如下:
blue = (RGB >> 16) & 0xFF;
green = (RGB >> 8) & 0xFF;
red = RGB & 0xFF;
//do math
new_RGB = (blue << 16) | (green << 8) | red;
此外,我有几个接口,其中一个使用 0x00RRGGBB
格式,另一个使用 0x00BBRRGG
格式。有没有一种有效的方法可以在两者之间进行转换?
如果您使用 struct
,则无需进行任何移位操作。我不知道这对你的特定处理器是否有效,但只是做一些简单的事情,比如:
typedef struct xRGBPixel {
unsigned char unused;
unsigned char red;
unsigned char green;
unsigned char blue;
} xRGBPixel;
您可以对 BRG 像素使用类似的结构。 (你确定是 BRG 而不是 BGR 吗?这真的很奇怪而且非常规。)
如果那样效率不高,那么 Jonathan Leffler 在评论中关于 32 位 int
和 4 个 unsigned char
值的并集的建议可能更合适。像这样:
typedef union Pixel {
uint32_t pixelAsInt;
unsigned char pixelAsChar[4];
} Pixel;
要将 0x00RRGGBB 转换为 0x00BBRRGG,您可以使用字节序转换器:
REV r0,r0 ;0x00RRGGBB -> 0xBBGGRR00
LSRS r0,r0,#8 ;0xBBGGRR00 -> 0x00BBGGRR
一种有效的方法是编写一个汇编函数,在空闲寄存器中加载最大量的数据,对所有寄存器执行转换,然后将它们写回。
使用 ARM procedure call standard 作为如何编写从 C 调用的汇编函数的参考。
另一种方法是简单地执行字节复制,但这需要 3-4* read/writes,而上面每个像素只需要 2 个。
*3 如果不关心 xxRRGGBB,如果 00RRGGBB 则为 4。
它不可移植,但由于您使用的是 M0 并且可能处于小端模式。使用位字段或 uint32_t 的联合和 uint8_t.
的数组
typedef struct {
uint32_t red: 8;
uint32_t green: 8;
uint32_t blue: 8;
uint32_t spare: 8;
} rgb_s;
static rgb_s var; // statics init to zero
var.red = 0x56
var.green = 0x34
var.blue = 0x12
uint32_t myInt = *(uint32_t*)&var; // myInt is now 0x00123456;
如果重要,请使用静态或确保将备用字段清零。
或工会
enum {Red, Green, Blue, Colors};
typedef union {
uint32_t rgb;
uint8_t color[Colors];
} rgb_u;
rgb_u var;
var.rgb = 0x0;
var.color[red] = 0x56;
var.color[green] = 0x34;
var.color[blue] = 0x12;
assert(var.rgb == 0x123456); //the uint32 overlays the array
同样,两者都不是真正可移植的,但在嵌入式中都很常见。您需要知道处理器的字节序。 (M0 可大可小,但默认为小)
C 现在也有匿名联合,但并非所有嵌入式编译器都支持它们。
您的 "inefficient" 方法可能只是归结为几行机器代码,并且转换速度很快 - 这意味着转换版本将执行得非常快,并且像这样的微优化在 99 中不应该是一个问题占所有应用程序的百分比。
通过 pointers/arrays 寻址单个字节不一定会提高性能。它很可能恰恰相反——检查生成的程序集。如果您要使用 struct/union 解决方案,那应该是为了可读性,而不是为了微观管理性能。
不过,就便携性而言,shift版本更胜一筹。移位时,您不必担心字节顺序、填充、对齐、指针别名 - 所有这些都可能是 struct/union 解决方案的问题。
问题的根源实际上是 32 位整数表示。如果你能摆脱它,它会解决很多问题。这里的理想格式是 uint8_t color[3];
.
I want to be able to do math with them without worrying about carry bits spilling between the colors, so I need to split them up into three uint8 values.
不,通常你不需要(将它们分成三个 uint8 值)。考虑这个函数:
uint32_t blend(const uint32_t argb0, const uint32_t argb1, const int phase)
{
if (phase <= 0)
return argb0;
else
if (phase < 256) {
const uint32_t rb0 = argb0 & 0x00FF00FF;
const uint32_t rb1 = argb1 & 0x00FF00FF;
const uint32_t ag0 = (argb0 >> 8) & 0x00FF00FF;
const uint32_t ag1 = (argb1 >> 8) & 0x00FF00FF;
const uint32_t rb = rb1 * phase + (256 - phase) * rb0;
const uint32_t ag = ag1 * phase + (256 - phase) * ag0;
return ((rb & 0xFF00FF00u) >> 8)
| (ag & 0xFF00FF00u);
} else
return argb1;
}
此函数通过拆分每个输入向量(具有四个 8 位组件)转换为两个向量,其中包含两个 16 位组件。
如果您不需要 alpha 通道,那么处理成对的颜色值(例如,对于每对像素)可能更有效 -- 所以 (0xRRGGBB
, 0xrrggbb
) 被拆分为 (0x00RR00BB
, 0x00rr00bb
, 0x00GG00gg
) -- 在上面的 blend
函数中意味着少了一个乘法(但多了一个 AND 和一个 OR 运算).
Cortex-M0 设备上的 32 位乘法运算因实现而异。有些具有单周期乘法运算,有些则需要 32 个周期。因此,根据所使用的确切 Cortex-M0 内核,用 AND 和 OR 替换一次乘法可能会大大加快速度,也可能会稍微慢下来。
当您确实需要单独的组件时,将拆分留给编译器通常会生成更好的代码:传递指向颜色值的指针而不是指定颜色,
uint32_t some_op(const uint32_t *const argb)
{
const uint32_t a = ((const uint8_t *)argb)[0];
const uint32_t r = ((const uint8_t *)argb)[1];
const uint32_t g = ((const uint8_t *)argb)[2];
const uint32_t b = ((const uint8_t *)argb)[3];
/* Do something ... */
}
这是因为许多架构都有将 8 位值加载到完整寄存器的指令,将所有高位设置为零(零扩展,uxtb
Cortex-M0 架构;C 编译器会为您做这件事)。标记指针和指向的值以及中间值 const
应该允许编译器优化访问,以便它在生成的代码中最好 moment/position 发生,而不是必须将其保存在寄存器中。 (在(可用)寄存器很少的架构上尤其如此,例如 32 位和 64 位 Intel 和 AMD 架构(x86 和 x86-64)。Cortex-M0 有 12 个通用 32 位寄存器,但这取决于在使用的 ABI 上,哪些 "free" 可以在函数中使用。)
请注意,如果您使用 GCC 编译代码,则可以使用
uint32_t oabc_to_ocba(uint32_t c)
{
asm volatile ( "rev %0, %0\n\t"
: "=r" (c)
: "r" (c)
);
return c >> 8;
}
将 0x0ABC
转换为 0x0CBA
,反之亦然。通常,它编译为 rev r0, r0
、lsrs r0, r0, #8
、bx lr
,但编译器可以内联它并使用另一个寄存器代替(r0
)。
我正在用 C 为 32 位 cortex M0 微控制器编写一些软件,并且我正在对 32 位 RGB 值进行大量操作。它们以 32 位整数格式处理,如 0x00BBRRGG
。我希望能够用它们进行数学运算而不用担心进位位在颜色之间溢出,所以我需要将它们分成三个 uint8 值。有没有一种有效的方法来做到这一点?我假设效率低下的方式如下:
blue = (RGB >> 16) & 0xFF;
green = (RGB >> 8) & 0xFF;
red = RGB & 0xFF;
//do math
new_RGB = (blue << 16) | (green << 8) | red;
此外,我有几个接口,其中一个使用 0x00RRGGBB
格式,另一个使用 0x00BBRRGG
格式。有没有一种有效的方法可以在两者之间进行转换?
如果您使用 struct
,则无需进行任何移位操作。我不知道这对你的特定处理器是否有效,但只是做一些简单的事情,比如:
typedef struct xRGBPixel {
unsigned char unused;
unsigned char red;
unsigned char green;
unsigned char blue;
} xRGBPixel;
您可以对 BRG 像素使用类似的结构。 (你确定是 BRG 而不是 BGR 吗?这真的很奇怪而且非常规。)
如果那样效率不高,那么 Jonathan Leffler 在评论中关于 32 位 int
和 4 个 unsigned char
值的并集的建议可能更合适。像这样:
typedef union Pixel {
uint32_t pixelAsInt;
unsigned char pixelAsChar[4];
} Pixel;
要将 0x00RRGGBB 转换为 0x00BBRRGG,您可以使用字节序转换器:
REV r0,r0 ;0x00RRGGBB -> 0xBBGGRR00
LSRS r0,r0,#8 ;0xBBGGRR00 -> 0x00BBGGRR
一种有效的方法是编写一个汇编函数,在空闲寄存器中加载最大量的数据,对所有寄存器执行转换,然后将它们写回。
使用 ARM procedure call standard 作为如何编写从 C 调用的汇编函数的参考。
另一种方法是简单地执行字节复制,但这需要 3-4* read/writes,而上面每个像素只需要 2 个。
*3 如果不关心 xxRRGGBB,如果 00RRGGBB 则为 4。
它不可移植,但由于您使用的是 M0 并且可能处于小端模式。使用位字段或 uint32_t 的联合和 uint8_t.
的数组typedef struct {
uint32_t red: 8;
uint32_t green: 8;
uint32_t blue: 8;
uint32_t spare: 8;
} rgb_s;
static rgb_s var; // statics init to zero
var.red = 0x56
var.green = 0x34
var.blue = 0x12
uint32_t myInt = *(uint32_t*)&var; // myInt is now 0x00123456;
如果重要,请使用静态或确保将备用字段清零。
或工会
enum {Red, Green, Blue, Colors};
typedef union {
uint32_t rgb;
uint8_t color[Colors];
} rgb_u;
rgb_u var;
var.rgb = 0x0;
var.color[red] = 0x56;
var.color[green] = 0x34;
var.color[blue] = 0x12;
assert(var.rgb == 0x123456); //the uint32 overlays the array
同样,两者都不是真正可移植的,但在嵌入式中都很常见。您需要知道处理器的字节序。 (M0 可大可小,但默认为小) C 现在也有匿名联合,但并非所有嵌入式编译器都支持它们。
您的 "inefficient" 方法可能只是归结为几行机器代码,并且转换速度很快 - 这意味着转换版本将执行得非常快,并且像这样的微优化在 99 中不应该是一个问题占所有应用程序的百分比。
通过 pointers/arrays 寻址单个字节不一定会提高性能。它很可能恰恰相反——检查生成的程序集。如果您要使用 struct/union 解决方案,那应该是为了可读性,而不是为了微观管理性能。
不过,就便携性而言,shift版本更胜一筹。移位时,您不必担心字节顺序、填充、对齐、指针别名 - 所有这些都可能是 struct/union 解决方案的问题。
问题的根源实际上是 32 位整数表示。如果你能摆脱它,它会解决很多问题。这里的理想格式是 uint8_t color[3];
.
I want to be able to do math with them without worrying about carry bits spilling between the colors, so I need to split them up into three uint8 values.
不,通常你不需要(将它们分成三个 uint8 值)。考虑这个函数:
uint32_t blend(const uint32_t argb0, const uint32_t argb1, const int phase)
{
if (phase <= 0)
return argb0;
else
if (phase < 256) {
const uint32_t rb0 = argb0 & 0x00FF00FF;
const uint32_t rb1 = argb1 & 0x00FF00FF;
const uint32_t ag0 = (argb0 >> 8) & 0x00FF00FF;
const uint32_t ag1 = (argb1 >> 8) & 0x00FF00FF;
const uint32_t rb = rb1 * phase + (256 - phase) * rb0;
const uint32_t ag = ag1 * phase + (256 - phase) * ag0;
return ((rb & 0xFF00FF00u) >> 8)
| (ag & 0xFF00FF00u);
} else
return argb1;
}
此函数通过拆分每个输入向量(具有四个 8 位组件)转换为两个向量,其中包含两个 16 位组件。
如果您不需要 alpha 通道,那么处理成对的颜色值(例如,对于每对像素)可能更有效 -- 所以 (0xRRGGBB
, 0xrrggbb
) 被拆分为 (0x00RR00BB
, 0x00rr00bb
, 0x00GG00gg
) -- 在上面的 blend
函数中意味着少了一个乘法(但多了一个 AND 和一个 OR 运算).
Cortex-M0 设备上的 32 位乘法运算因实现而异。有些具有单周期乘法运算,有些则需要 32 个周期。因此,根据所使用的确切 Cortex-M0 内核,用 AND 和 OR 替换一次乘法可能会大大加快速度,也可能会稍微慢下来。
当您确实需要单独的组件时,将拆分留给编译器通常会生成更好的代码:传递指向颜色值的指针而不是指定颜色,
uint32_t some_op(const uint32_t *const argb)
{
const uint32_t a = ((const uint8_t *)argb)[0];
const uint32_t r = ((const uint8_t *)argb)[1];
const uint32_t g = ((const uint8_t *)argb)[2];
const uint32_t b = ((const uint8_t *)argb)[3];
/* Do something ... */
}
这是因为许多架构都有将 8 位值加载到完整寄存器的指令,将所有高位设置为零(零扩展,uxtb
Cortex-M0 架构;C 编译器会为您做这件事)。标记指针和指向的值以及中间值 const
应该允许编译器优化访问,以便它在生成的代码中最好 moment/position 发生,而不是必须将其保存在寄存器中。 (在(可用)寄存器很少的架构上尤其如此,例如 32 位和 64 位 Intel 和 AMD 架构(x86 和 x86-64)。Cortex-M0 有 12 个通用 32 位寄存器,但这取决于在使用的 ABI 上,哪些 "free" 可以在函数中使用。)
请注意,如果您使用 GCC 编译代码,则可以使用
uint32_t oabc_to_ocba(uint32_t c)
{
asm volatile ( "rev %0, %0\n\t"
: "=r" (c)
: "r" (c)
);
return c >> 8;
}
将 0x0ABC
转换为 0x0CBA
,反之亦然。通常,它编译为 rev r0, r0
、lsrs r0, r0, #8
、bx lr
,但编译器可以内联它并使用另一个寄存器代替(r0
)。