通过Alpha混合将两个16位RGB颜色合并

6
我有一个TFT显示器,可以绘制16位颜色,格式为RGB 565。我想在显示内容中添加一些透明度。

假设我有一个黑色背景(0x0000),我想绘制一个半透明的白色前景(0xFFFF)(不透明度由另一个字节控制),这样它就会呈现灰色。如何计算出相同RGB 565格式的16位灰色颜色,以便我可以将其发送到我的TFT,并正确显示它(可能会有一些损失,但我不在意)?

我需要一个这样的函数:

unsigned short calcColor_RGB565(unsigned short background_RGB565, unsigned short foreground_RGB565, unsigned char opacity)

calcColor_RGB565(0x0000, 0xFFFF, 128) 的结果为0x8410(或者0x1084,因为我向TFT发送两个单独的字节,所以如果需要只需颠倒顺序)。

感谢任何能帮助我的人,我尝试了一些方法,但是我无法得到正确的结果,甚至都不接近 :/。

欢迎提供类似C语言的伪代码,但我更喜欢有关如何执行此操作的解释。

编辑:忘记说了,我希望它尽可能快,因为这是为旧微处理器设计的,所以如果分别计算2个字节更快(因此我也不必稍后将它们分开),那么我非常感兴趣这样的优化。

编辑9月27日:5天过去了,仍未解决。我可以从rgb565转换为rgb8888,执行alpha混合,然后再转换回rgb565,但这太慢了,一定有更好的方法!


3
请张贴您当前的代码。 - Cory Nelson
我的当前代码最初是从这里复制的:https://developers.sifteo.com/docs/SifteoSDK/1.0.0/color_8h_source.html,但是那个lerp函数没有给出正确的结果,所以我尝试修改了一些东西,但是没有得到更好的结果...我是初学者。 - guix
4个回答

6

我的(未经测试的)解决方案:我将前景色和背景色分成(红色+蓝色)和(绿色)组件,并将它们乘以一个6位alpha值。享受吧!(仅在它起作用时:))

                            //   rrrrrggggggbbbbb
#define MASK_RB       63519 // 0b1111100000011111
#define MASK_G         2016 // 0b0000011111100000
#define MASK_MUL_RB 4065216 // 0b1111100000011111000000
#define MASK_MUL_G   129024 // 0b0000011111100000000000
#define MAX_ALPHA        64 // 6bits+1 with rounding

uint16 alphablend( uint16 fg, uint16 bg, uint8 alpha ){

  // alpha for foreground multiplication
  // convert from 8bit to (6bit+1) with rounding
  // will be in [0..64] inclusive
  alpha = ( alpha + 2 ) >> 2;
  // "beta" for background multiplication; (6bit+1);
  // will be in [0..64] inclusive
  uint8 beta = MAX_ALPHA - alpha;
  // so (0..64)*alpha + (0..64)*beta always in 0..64

  return (uint16)((
            (  ( alpha * (uint32)( fg & MASK_RB )
                + beta * (uint32)( bg & MASK_RB )
            ) & MASK_MUL_RB )
          |
            (  ( alpha * ( fg & MASK_G )
                + beta * ( bg & MASK_G )
            ) & MASK_MUL_G )
         ) >> 6 );
}

/*
  result masks of multiplications
  uppercase: usable bits of multiplications
  RRRRRrrrrrrBBBBBbbbbbb // 5-5 bits of red+blue
        1111100000011111 // from MASK_RB * 1
  1111100000011111000000 //   to MASK_RB * MAX_ALPHA // 22 bits!


  -----GGGGGGgggggg----- // 6 bits of green
        0000011111100000 // from MASK_G * 1
  0000011111100000000000 //   to MASK_G * MAX_ALPHA
*/

非常感谢并为迟到道歉。我已经测试了你的函数,虽然它与Dietrich Epp的函数相比给出了不同的结果(我想是因为舍入误差不同),但速度也快了两倍以上!我还会进行一些更多的测试,看哪个能提供最准确的颜色...但我想我仍然会使用你的。所以我将你的答案设置为最佳答案,但两个答案都很好;) - guix
谢谢。我的版本不太精确,因为它从每个颜色中削减了2位 :) 顺便说一下,我在这里“发明”了这个6位解决方案:http://www.openprocessing.org/sketch/36882 如果你需要模糊函数,可以用它。 - biziclop
1
变量 a 是指 alpha,对吧? - Joe

5
正确的公式应该类似于这样:
unsigned short blend(unsigned short fg, unsigned short bg, unsigned char alpha)
{
    // Split foreground into components
    unsigned fg_r = fg >> 11;
    unsigned fg_g = (fg >> 5) & ((1u << 6) - 1);
    unsigned fg_b = fg & ((1u << 5) - 1);

    // Split background into components
    unsigned bg_r = bg >> 11;
    unsigned bg_g = (bg >> 5) & ((1u << 6) - 1);
    unsigned bg_b = bg & ((1u << 5) - 1);

    // Alpha blend components
    unsigned out_r = (fg_r * alpha + bg_r * (255 - alpha)) / 255;
    unsigned out_g = (fg_g * alpha + bg_g * (255 - alpha)) / 255;
    unsigned out_b = (fg_b * alpha + bg_b * (255 - alpha)) / 255;

    // Pack result
    return (unsigned short) ((out_r << 11) | (out_g << 5) | out_b);
}

有一种快捷的方法可以将数值除以255。编译器应该能够提供一些强度削弱,但您可能可以通过使用以下公式来获得更好的效果:

// Alpha blend components
unsigned out_r = fg_r * a + bg_r * (255 - alpha);
unsigned out_g = fg_g * a + bg_g * (255 - alpha);
unsigned out_b = fg_b * a + bg_b * (255 - alpha);
out_r = (out_r + 1 + (out_r >> 8)) >> 8;
out_g = (out_g + 1 + (out_g >> 8)) >> 8;
out_b = (out_b + 1 + (out_b >> 8)) >> 8;

请注意函数中的大量变量...这是可以接受的。如果您尝试通过重新编写方程以减少临时变量来“优化”代码,那么您只是在做编译器已经为您完成的工作。除非您使用的是真的很差的编译器。
如果速度还不够快,有几个选项可以继续进行。但是,选择正确的选项取决于分析结果、代码的使用方式和目标架构。

不,即使使用32位,0x7BEF也是完全正确的。在二进制中,它是01111 011111 01111,正如您所看到的,每个组件都恰好等于50%的满量程,向下取整。 - Dietrich Epp
如果您将alpha设置为132而不是127,则会得到10000 100000 10000,这比50%的灰色略微更亮。不确定要向哪个方向舍入,但通常不会产生任何视觉差异。 - Dietrich Epp
结果已经因为缺少伽马校正而是“错误”的;花费更多的指令来四舍五入也不会使其更加准确。 - Cory Nelson
很奇怪,因为我在Paint.NET中尝试制作黑色背景0x000000和alpha 255以及白色前景0xFFFFFF和alpha 128,然后使用插件导出到rgb565,它显示为0x8410(好吧,由于某些原因是0x1084...)。然后我尝试导出到普通位图文件并使用我在网上找到的另一个转换器,这个也显示为0x8410。不是说你的代码不正确,但我想了解为什么会有(微小的)差异,所以如果您知道,请解释一下;) - guix
@user2534317:你正在尝试将50%的水平写入缓冲区,但是你必须将其舍入到51%或49%。哪一个正确?都不正确。哪一个误差较小?误差相等。 - Dietrich Epp
显示剩余3条评论

4
我找到了一种替代方法,比biziclop的近似算法快约25%。这也是一种近似,因为它将alpha级别从0-255降低到0-31(32个alpha级别),但据我所知,它不会截断颜色位。在我的TFT显示器上,结果在视觉上与biziclop算法的结果相同,但我没有检查单个像素值以查看差异(如果有的话)。请注意,虽然fg和bg参数是32位无符号数,但实际上只需传入16位RGB565颜色。函数内部算法需要32位宽度。
/**
 * Fast RGB565 pixel blending
 * @param fg      The foreground color in uint16_t RGB565 format
 * @param bg      The background color in uint16_t RGB565 format
 * @param alpha   The alpha in range 0-255
 **/
color alphaBlendRGB565( uint32_t fg, uint32_t bg, uint8_t alpha ){
    alpha = ( alpha + 4 ) >> 3;
    bg = (bg | (bg << 16)) & 0b00000111111000001111100000011111;
    fg = (fg | (fg << 16)) & 0b00000111111000001111100000011111;
    uint32_t result = ((((fg - bg) * alpha) >> 5) + bg) & 0b00000111111000001111100000011111;
    return (uint16_t)((result >> 16) | result);
}

我在 Chris Chua 提交的 Adafruit Arduino framebuffer 库的拉取请求中找到了这个解决方案。这里是一个带注释的扩展版本,以解释其中的数学:

// Fast RGB565 pixel blending
// Found in a pull request for the Adafruit framebuffer library. Clever!
// https://github.com/tricorderproject/arducordermini/pull/1/files#diff-d22a481ade4dbb4e41acc4d7c77f683d
color alphaBlendRGB565( uint32_t fg, uint32_t bg, uint8_t alpha ){
    // Alpha converted from [0..255] to [0..31]
    alpha = ( alpha + 4 ) >> 3;

    // Converts  0000000000000000rrrrrggggggbbbbb
    //     into  00000gggggg00000rrrrr000000bbbbb
    // with mask 00000111111000001111100000011111
    // This is useful because it makes space for a parallel fixed-point multiply
    bg = (bg | (bg << 16)) & 0b00000111111000001111100000011111;
    fg = (fg | (fg << 16)) & 0b00000111111000001111100000011111;

    // This implements the linear interpolation formula: result = bg * (1.0 - alpha) + fg * alpha
    // This can be factorized into: result = bg + (fg - bg) * alpha
    // alpha is in Q1.5 format, so 0.0 is represented by 0, and 1.0 is represented by 32
    uint32_t result = (fg - bg) * alpha; // parallel fixed-point multiply of all components
    result >>= 5;
    result += bg;
    result &= 0b00000111111000001111100000011111; // mask out fractional parts
    return (color)((result >> 16) | result); // contract result
}

你有任何想法为什么 alpha = ( alpha + 4 ) >> 3; 中的 uint_8 加上 +4 可能会得到 255 吗? - MandoMando
+4是为了进行更准确的范围转换。是的,在255(移位之前)加上+4绝对可行,但我(使用gcc)它可以正常工作,因此中间结果可以存储为16或32位整数。 - Projectitis

2

这个是基于 http://www-personal.umich.edu/~bazald/l/api/_s_d_l___r_l_eaccel_8c_source.html 的。

在我的RGB565 16位设备上,它产生了最清晰的结果。

COLOUR ALPHA_BLIT16_565(uint32_t fg, uint32_t bg, int8u alpha) {
    // Alpha converted from [0..255] to [0..31]
    uint32_t ALPHA = alpha >> 3;     
    fg = (fg | fg << 16) & 0x07e0f81f;
    bg = (bg | bg << 16) & 0x07e0f81f;
    bg += (fg - bg) * ALPHA >> 5;
    bg &= 0x07e0f81f;
        return (COLOUR)(bg | bg >> 16);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接