【问题标题】:Efficient way to split up RGB values in C在C中拆分RGB值的有效方法
【发布时间】:2017-09-13 18:26:27
【问题描述】:

我正在用 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。有没有一种有效的方法在两者之间进行转换?

【问题讨论】:

  • 你为什么认为它是低效的?你计时了吗?尤其是因为它确实是对单独组件“做数学”的唯一方法。
  • 至于另一个问题,而且 BBRRGG 似乎是一种奇怪的格式,您需要一个 24 位 roll (而不是 shift)。尝试搜索算法来进行 n 位滚动。
  • 我想我不知道它效率低下,但我没有替代方法来比较它,所以我怎么知道是否有更好的方法?
  • 另一种可能的技术(C 标准不保证可移植)是使用 32 位整数的 union 和 4 uint8_t 的数组。您可以从数组中选择颜色,并在要使用整个值时使用 32 位整数。它实际上是否更有效还有待观察——你必须衡量。
  • 取决于你做什么样的数学,你也许可以把它打包。有更多的技巧可以防止进位在通道之间传递,而不是像这样的蛮力,你通常可以做一些很好的位数学。

标签: c colors bit-manipulation integer-arithmetic


【解决方案1】:

如果您使用struct,则无需执行任何位移操作。我不知道这对您的特定处理器是否有效,但只是做一些简单的事情,例如:

typedef struct xRGBPixel {
    unsigned char unused;
    unsigned char red;
    unsigned char green;
    unsigned char blue;
} xRGBPixel;

对于 BRG 像素,您可以使用类似的结构。 (你确定它是 BRG 而不是 BGR?这非常奇怪和非常规。)

如果效率不高,那么 Jonathan Leffler 在 cmets 中关于 32 位 int 和 4 个 unsigned char 值数组的联合的建议可能更合适。像这样的:

typedef union Pixel {
    uint32_t pixelAsInt;
    unsigned char pixelAsChar[4];
} Pixel;

【讨论】:

  • 我希望得到这样的答案。我会试一试。是的,它可能是 BGR,我必须仔细检查。
  • 检查编译器编译指示,了解如何声明结构和联合打包。
  • 请记住,这样的代码不可移植,因为它依赖于字节序。此示例使用大端序,而 ARM 通常默认为小端序。
【解决方案2】:

要将 0x00RRGGBB 转换为 0x00BBRRGG,您可以使用字节序转换器:

REV    r0,r0     ;0x00RRGGBB -> 0xBBGGRR00
LSRS   r0,r0,#8  ;0xBBGGRR00 -> 0x00BBGGRR

一种有效的方法是编写一个汇编函数,在空闲寄存器中加载最大数量的数据,对所有寄存器执行转换,然后将它们写回。
使用ARM procedure call standard 作为参考,了解如何编写从 C 调用的汇编函数。

另一种方法是简单地执行字节复制,但这需要 3-4* 次读取/写入,而上面每个像素只需要 2 次。

*3 如果不关心 xxRRGGBB,4 如果 00RRGGBB。

【讨论】:

    【解决方案3】:

    它不是便携式的,但因为你在 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,但并非所有嵌入式编译器都支持它们。

    【讨论】:

      【解决方案4】:

      您的“低效”方式可能归结为几行机器代码,并且转换速度很快 - 这意味着转换版本将执行非常快,并且在 99% 的所有应用程序中都不应该担心这样的微优化.

      通过指针/数组寻址单个字节不一定会提高性能。很可能正好相反——检查生成的程序集。如果您要使用 struct/union 解决方案,那应该是为了可读性,而不是为了微观管理性能。

      但是,在便携性方面,shift 版本更胜一筹。移位时,您不必担心字节顺序、填充、对齐、指针别名——所有这些都可能是结构/联合解决方案的问题。

      问题的根源实际上是 32 位整数表示。如果你能摆脱它,它会解决很多问题。这里的理想格式是uint8_t color[3];

      【讨论】:

        【解决方案5】:

        我希望能够对它们进行数学运算,而不必担心颜色之间的进位溢出,因此我需要将它们分成三个 uint8 值。

        不,通常您不需要(将它们分成三个 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;
        }
        

        这个函数实现了从颜色argb0 (phase &lt;= 0) 到argb1 (phase &gt;= 256) 的线性混合,通过将每个输入向量(具有四个 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 位值加载到完整寄存器中的指令,将所有高位设置为零(零扩展,在 Cortex-M0 架构上uxtb;C编译器会为你做这个)。标记指针和指向的值,以及中间值const,应该允许编译器优化访问,以便它发生在生成代码中的最佳时刻/位置,而不是必须保留它在寄存器中。 (这在具有很少(可用)寄存器的架构上尤其如此,例如 32 位和 64 位 Intel 和 AMD 架构(x86 和 x86-64)。Cortex-M0 有 12 个通用 32 位寄存器,但这取决于关于使用的 ABI,哪些是“免费”在函数中使用的。)

         
        请注意,如果您使用 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, r0lsrs r0, r0, #8bx lr,但编译器可以内联它并使用另一个寄存器代替(r0)。

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-02-22
          • 1970-01-01
          • 1970-01-01
          • 2015-05-29
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多