如何快速alpha混合RGBA无符号字节颜色?

时间:2009-07-09 09:04:37

标签: c++ performance

我正在使用c ++,我想使用以下代码进行alpha混合。

#define CLAMPTOBYTE(color) \
    if ((color) & (~255)) { \
        color = (BYTE)((-(color)) >> 31); \
    } else { \
        color = (BYTE)(color); \
    }
#define GET_BYTE(accessPixel, x, y, scanline, bpp) \
    ((BYTE*)((accessPixel) + (y) * (scanline) + (x) * (bpp))) 

    for (int y = top ; y < bottom; ++y)
    {
        BYTE* resultByte = GET_BYTE(resultBits, left, y, stride, bytepp);
        BYTE* srcByte = GET_BYTE(srcBits, left, y, stride, bytepp);
        BYTE* srcByteTop = GET_BYTE(srcBitsTop, left, y, stride, bytepp);
        BYTE* maskCurrent = GET_GREY(maskSrc, left, y, width);
        int alpha = 0;
        int red = 0;
        int green = 0;
        int blue = 0;
        for (int x = left; x < right; ++x)
        {
            alpha = *maskCurrent;
            red = (srcByteTop[R] * alpha + srcByte[R] * (255 - alpha)) / 255;
            green = (srcByteTop[G] * alpha + srcByte[G] * (255 - alpha)) / 255;
            blue = (srcByteTop[B] * alpha + srcByte[B] * (255 - alpha)) / 255;
            CLAMPTOBYTE(red);
            CLAMPTOBYTE(green);
            CLAMPTOBYTE(blue);
            resultByte[R] = red;
            resultByte[G] = green;
            resultByte[B] = blue;
            srcByte += bytepp;
            srcByteTop += bytepp;
            resultByte += bytepp;
            ++maskCurrent;
        }
    }

然而我发现它仍然很慢,当编写两个600 * 600图像时需要大约40-60毫秒。 有没有什么方法可以将速度提高到不到16毫秒?

任何人都可以帮我加速这段代码吗?非常感谢!

17 个答案:

答案 0 :(得分:26)

Use SSE - 从第131页开始。

基本工作流程

  1. 从src加载4个像素(16个1个字节的数字)RGBA RGBA RGBA RGBA(流式加载)

  2. 加载另外4个要与srcbytetop RGBx RGBx RGBx RGBx混合的

  3. 进行一些调整以使1中的A项填充每个插槽I.e

    xxxA xxxB xxxC xxxD - &gt; AAAA BBBB CCCC DDDD

    在我的解决方案中,我选择重新使用现有的“maskcurrent”数组,但将alpha集成到1的“A”字段中将需要较少的内存负载,因此速度更快。在这种情况下,调配可能是:并使用面具选择A,B,C,D。向右移8,或用原始,向右移16或再次移动。

  4. 将上述内容添加到每个插槽中全部为-255的向量

  5. 将1 * 4(具有255-alpha的源)和2 * 3(具有alpha的结果)相乘。

    你应该可以使用“乘法和丢弃底部8位”SSE2指令。

  6. 将这两个(4和5)加在一起

  7. 将其存放在其他地方(如果可能)或目的地(如果必须)

  8. 以下是您的起点:

        //Define your image with __declspec(align(16)) i.e char __declspec(align(16)) image[640*480]
        // so the first byte is aligned correctly for SIMD.
        // Stride must be a multiple of 16.
    
        for (int y = top ; y < bottom; ++y)
        {
            BYTE* resultByte = GET_BYTE(resultBits, left, y, stride, bytepp);
            BYTE* srcByte = GET_BYTE(srcBits, left, y, stride, bytepp);
            BYTE* srcByteTop = GET_BYTE(srcBitsTop, left, y, stride, bytepp);
            BYTE* maskCurrent = GET_GREY(maskSrc, left, y, width);
            for (int x = left; x < right; x += 4)
            {
                //If you can't align, use _mm_loadu_si128()
                // Step 1
                __mm128i src = _mm_load_si128(reinterpret_cast<__mm128i*>(srcByte)) 
                // Step 2
                __mm128i srcTop = _mm_load_si128(reinterpret_cast<__mm128i*>(srcByteTop)) 
    
                // Step 3
                // Fill the 4 positions for the first pixel with maskCurrent[0], etc
                // Could do better with shifts and so on, but this is clear
                __mm128i mask = _mm_set_epi8(maskCurrent[0],maskCurrent[0],maskCurrent[0],maskCurrent[0],
                                            maskCurrent[1],maskCurrent[1],maskCurrent[1],maskCurrent[1],
                                            maskCurrent[2],maskCurrent[2],maskCurrent[2],maskCurrent[2],
                                            maskCurrent[3],maskCurrent[3],maskCurrent[3],maskCurrent[3],
                                            ) 
    
                // step 4
                __mm128i maskInv = _mm_subs_epu8(_mm_set1_epu8(255), mask) 
    
                //Todo : Multiply, with saturate - find correct instructions for 4..6
                //note you can use Multiply and add _mm_madd_epi16
    
                alpha = *maskCurrent;
                red = (srcByteTop[R] * alpha + srcByte[R] * (255 - alpha)) / 255;
                green = (srcByteTop[G] * alpha + srcByte[G] * (255 - alpha)) / 255;
                blue = (srcByteTop[B] * alpha + srcByte[B] * (255 - alpha)) / 255;
                CLAMPTOBYTE(red);
                CLAMPTOBYTE(green);
                CLAMPTOBYTE(blue);
                resultByte[R] = red;
                resultByte[G] = green;
                resultByte[B] = blue;
                //----
    
                // Step 7 - store result.
                //Store aligned if output is aligned on 16 byte boundrary
                _mm_store_si128(reinterpret_cast<__mm128i*>(resultByte), result)
                //Slow version if you can't guarantee alignment
                //_mm_storeu_si128(reinterpret_cast<__mm128i*>(resultByte), result)
    
                //Move pointers forward 4 places
                srcByte += bytepp * 4;
                srcByteTop += bytepp * 4;
                resultByte += bytepp * 4;
                maskCurrent += 4;
            }
        }
    

    要了解哪些AMD处理器将运行此代码(目前正在使用SSE2指令),请参阅Wikipedia's List of AMD Turion microprocessors。您还可以查看维基百科上的其他处理器列表,但我的研究表明,大约4年前的AMD cpus至少都支持SSE2。

    您应该期望SSE2的良好运行速度比当前代码快8-16倍。这是因为我们消除了循环中的分支,一次处理4个像素(或12个通道),并通过使用流指令提高缓存性能。作为SSE的替代方案,您可以通过消除用于饱和的if检查来更快地运行现有代码。除此之外,我还需要在您的工作负载上运行一个分析器。

    当然,最好的解决方案是使用硬件支持(即在DirectX中编写问题代码)并在视频卡上完成。

答案 1 :(得分:20)

您始终可以同时计算红色和蓝色的alpha值。您也可以将此技巧与前面提到的SIMD实现一起使用。

unsigned int blendPreMulAlpha(unsigned int colora, unsigned int colorb, unsigned int alpha)
{
    unsigned int rb = (colora & 0xFF00FF) + ( (alpha * (colorb & 0xFF00FF)) >> 8 );
    unsigned int g = (colora & 0x00FF00) + ( (alpha * (colorb & 0x00FF00)) >> 8 );
    return (rb & 0xFF00FF) + (g & 0x00FF00);
}


unsigned int blendAlpha(unsigned int colora, unsigned int colorb, unsigned int alpha)
{
    unsigned int rb1 = ((0x100 - alpha) * (colora & 0xFF00FF)) >> 8;
    unsigned int rb2 = (alpha * (colorb & 0xFF00FF)) >> 8;
    unsigned int g1  = ((0x100 - alpha) * (colora & 0x00FF00)) >> 8;
    unsigned int g2  = (alpha * (colorb & 0x00FF00)) >> 8;
    return ((rb1 | rb2) & 0xFF00FF) + ((g1 | g2) & 0x00FF00);
}

0&lt; = alpha&lt; = 0x100

答案 2 :(得分:17)

对于想要除以255的人,我找到了一个完美的公式:

pt->r = (r+1 + (r >> 8)) >> 8; // fast way to divide by 255

答案 3 :(得分:6)

这里有一些指示。

考虑使用Porter and Duff所述的预乘前景图像。除了可能更快,您还可以避免很多潜在的色边效应。

合成方程式从

变化
r =  kA + (1-k)B

...到......

r =  A + (1-k)B

或者,您可以重新设计标准公式以删除一个乘法。

r =  kA + (1-k)B
==  kA + B - kB
== k(A-B) + B

我可能错了,但我认为你不应该需要夹紧......

答案 4 :(得分:4)

没有完全回答这个问题,但......

有一件事是快速做,另一件事是做得对。 阿尔法合成是一种危险的野兽,看起来很直观,直观,但常见的错误几十年来已经普及,没有人注意到它(差不多)!

最着名和最常见的错误是不使用预乘alpha 。我强烈推荐这个:Alpha Blending for Leaves

答案 5 :(得分:3)

我无法发表评论,因为我没有足够的声誉,但我想说Jasper的版本不会溢出以获得有效输入。 屏蔽乘法结果是必要的,否则红色+蓝色乘法会在绿色通道中留下位(如果你分别将红色和蓝色相乘,你仍然需要掩盖蓝色通道中的位)绿色乘法会在蓝色通道中留下位。 如果将组件分开,这些是右移的丢失位,这通常是alpha混合的情况。 所以他们不会溢出或下溢。它们只是无用的位,需要被掩盖才能达到预期的效果。

那就是说,贾斯帕的版本不正确。它应该是0xFF-alpha(255-alpha),而不是0x100-alpha(256-alpha)。这可能不会产生明显的错误。 什么会产生一个明显的错误是他使用|合并乘法结果时代替+。

我发现改编的Jasper代码要比我原来的alpha混合代码更快,这个代码已经不错了,我目前正在我的软件渲染器项目中使用它。我使用32位ARGB像素:

Pixel AlphaBlendPixels(Pixel p1, Pixel p2)
{
    static const int AMASK = 0xFF000000;
    static const int RBMASK = 0x00FF00FF;
    static const int GMASK = 0x0000FF00;
    static const int AGMASK = AMASK | GMASK;
    static const int ONEALPHA = 0x01000000;
    unsigned int a = (p2 & AMASK) >> 24;
    unsigned int na = 255 - a;
    unsigned int rb = ((na * (p1 & RBMASK)) + (a * (p2 & RBMASK))) >> 8;
    unsigned int ag = (na * ((p1 & AGMASK) >> 8)) + (a * (ONEALPHA | ((p2 & GMASK) >> 8)));
    return ((rb & RBMASK) | (ag & AGMASK));
}

答案 6 :(得分:3)

您可以在两个图像中使用每个像素4个字节(用于存储器对齐),然后使用SSE指令一起处理所有通道。搜索“visual studio sse intrinsics”。

答案 7 :(得分:3)

首先,让我们为每个颜色成分使用正确的公式

你从这开始:

  v = ( 1-t ) * v0 + t * v1

,其中   t =插值参数[0..1]   v0 =源颜色值   v1 =转移颜色值   v =输出值

重新调整条款,我们可以减少操作次数:

  v = v0 + t * (v1 - v0)

每个颜色通道需要执行一次此计算(RGB的3次)。

对于8位无符号颜色分量,您需要使用正确的定点数学运算:

  i = i0 + t * ( ( i1 - i0 ) + 127 ) / 255

,其中   t =插值参数[0..255]   i0 =源颜色值[0..255]   i1 =转移颜色值[0..255]   i =输出颜色

如果你忽略了+127那么你的颜色会偏向更暗的一端。通常,人们使用/ 256或&gt;&gt; 8速度。这不对!如果除以256,则永远无法达到纯白色(255,255,255),因为255/256略小于1。

我希望这会有所帮助。

答案 8 :(得分:2)

我认为硬件支持会对您有所帮助。如果可行,尝试将逻辑从软件转移到硬件

答案 9 :(得分:2)

主要问题是糟糕的循环结构,编译器未能消除CSE可能会使情况更糟。将实际公共位移到循环外部。 int red并不常见,因为它应该在内循环中。

此外,红色,绿色和蓝色是独立的。如果依次计算它们,则在计算绿色结果时不需要在寄存器中保留临时红色结果。这对于像x86这样的寄存器有限的CPU尤为重要。

bytepp只允许有限数量的值。使其成为模板参数,然后从交换机调用正确的实例化。这将生成您的功能的多个副本,但每个副本都可以更好地进行优化。

如上所述,不需要夹紧。在alphablending中,你创建了两个图像a [x] [y]和b [x] [y]的线性组合。由于0 <= alpha <= 255,您知道每个输出都受max(255 * a [x] [y],255 * b [x] [y])的约束。由于您的输出范围与两个输入范围(0-255)相同,因此可以。

精度损失很小,您可以计算(a[x][y]*alpha * b[x][y]*(256-alpha))>>8。变速通常比分裂更快。

答案 10 :(得分:2)

将其移至GPU。

答案 11 :(得分:2)

我在不安全的C#中做过类似的代码。您是否有任何理由不直接遍历每个像素?为什么要使用所有BYTE *和GET_BYTE()调用?这可能是速度问题的一部分。

GET_GRAY是什么样的?

更重要的是,您确定您的平台没有公开alpha混合功能吗?您定位的平台是什么? Wiki告诉我,以下内容支持开箱即用:

  • Mac OS X
  • Windows 2000,XP,Server 2003,Windows CE,Vista和Windows 7
  • X Window系统的XRender扩展(包括现代Linux系统)
  • RISC OS调整
  • QNX Neutrino
  • 计划9
  • 地狱
  • AmigaOS 4.1
  • BeOS,Zeta和Haiku
  • 音节
  • MorphOS

答案 12 :(得分:1)

我假设您希望以完全可移植的方式执行此操作,无需GPU的帮助,使用专有的intel SIMD库(在AMD处理器上可能效率不高)。

将以下内容替换为RGB

的计算
R = TopR + (SourceR * alpha) >> 8;
G = TopG + (SourceG * alpha) >> 8;
B = TopB + (SourceB * alpha) >> 8; 

这是一种更有效的计算方法。

也可以在获取像素宏上使用shift left指令,而不是乘以BPP。

答案 13 :(得分:1)

根据目标架构,您可以尝试向量化或并行化该功能。

除此之外,尝试线性化整个方法(即没有循环中的循环)并同时使用四个字节,这将失去使用单个字节的开销,并使编译器更容易优化代码。

答案 14 :(得分:1)

当第一种颜色(colora,目标)也具有alpha通道(混合两种透明的ARGB颜色)时,此方法有效 Alpha位于第二种颜色的Alpha中(Colorb,源)

这将添加两个Alpha(0 =透明,255 =完全不透明) 这是贾斯珀·贝克斯答案的修改版本。

我用它来将透明像素艺术混合到透明屏幕上。

Uint32 alphaBlend(unsigned int colora, unsigned int colorb) {
    unsigned int a2  = (colorb & 0xFF000000) >> 24;
    unsigned int alpha = a2;
    if (alpha == 0) return colora;
    if (alpha == 255) return colorb;
    unsigned int a1  = (colora & 0xFF000000) >> 24;
    unsigned int nalpha = 0x100 - alpha;
    unsigned int rb1 = (nalpha * (colora & 0xFF00FF)) >> 8;
    unsigned int rb2 = (alpha * (colorb & 0xFF00FF)) >> 8;
    unsigned int g1  = (nalpha * (colora & 0x00FF00)) >> 8;
    unsigned int g2  = (alpha * (colorb & 0x00FF00)) >> 8;
    unsigned int anew = a1 + a2;
    if (anew > 255) {anew = 255;}
    return ((rb1 + rb2) & 0xFF00FF) + ((g1 + g2) & 0x00FF00) + (anew << 24);
}

答案 15 :(得分:0)

这是我对软件alpha混合的改编,适用于2个无符号整数。

我的代码略有不同,因为上面的代码基本上总是假设目标alpha是255.

使用一个不错的优化编译器,大多数计算应该在寄存器中,因为大多数变量的范围非常短。我还选择逐步改变结果&lt;&lt; 8逐渐地避免&lt;&lt; 24,&lt;&lt;将ARGB重新组合在一起时我很久以前就知道了...但我记得在286个周期中换班(1 + 1 *每个位移位)所以假设对于更大的班次仍有某种惩罚。

另外......而不是&#34; / 255&#34;我选择&#34;&gt;&gt; 8&#34;可以根据需要进行更改。

/*
    alpha blend source and destination, either may have an alpha!!!!

    Src  AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB
    Dest AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB

    res  AAAAAAAA RRRRRRRR GGGGGGGG BBBBBBBB

    NOTE - α = αsrc + αdest(1.0-αsrc)  where α = 0.0 - 1.0

    ALSO - DWORD is unsigned int so (F8000000 >> 24) = F8 not FFFFFFF8 as it would with int (signed)
    */

    inline DWORD raw_blend(const DWORD src, const DWORD dest)
    {       
        // setup and calculate α

        DWORD src_a = src >> 24;       
        DWORD src_a_neg = 255 - src_a;
        DWORD dest_a = dest >> 24;

        DWORD res = src_a + ((dest_a * src_a_neg) >> 8);

        // setup and calculate R

        DWORD src_r = (src >> 16) & 255;
        DWORD dest_r = (dest >> 16) & 255;

        res = (res << 8) | (((src_r * src_a) + (dest_r * src_a_neg)) >> 8);

        // setup and calculate G

        DWORD src_g = (src >> 8) & 255;
        DWORD dest_g = (dest >> 8) & 255;

        res = (res << 8) | (((src_g * src_a) + (dest_g * src_a_neg)) >> 8);

        // setup and calculate B

        DWORD src_b = src & 255;
        DWORD dest_b = dest & 255;

        return (res << 8) | (((src_b * src_a) + (dest_b * src_a_neg)) >> 8);
    }

答案 16 :(得分:0)

; In\   EAX = background color (ZRBG) 32bit (Z mean zero, always is zero)
; In\   EDX = foreground color (RBGA) 32bit
; Out\  EAX = new color
; free registers (R10, RDI, RSI, RSP, RBP)
abg2:
    mov r15b, dl                ; av
    movzx ecx, dl
    not ecx                     ; faster than 255 - dl
    mov r14b, cl                ; rem

    shr edx, 8
    and edx, 0x00FFFFFF
    mov r12d, edx
    mov r13d, eax               ; RBGA ---> ZRGB

    ; s: eax
    ; d: edx

    ;=============================red = ((s >> 16) * rem + (d >> 16) * av) >> 8;
    mov edx, r12d
    shr edx, 0x10
    movzx eax, r14b
    imul edx, eax
    mov ecx, r13d
    shr ecx, 0x10
    movzx eax, r15b
    imul eax, ecx
    lea eax, [eax + edx]                    ; faster than add eax, edx
    shr eax, 0x8
    mov r9b, al
    shl r9d, 8

    ;=============================green = (((s >> 8) & 0x0000ff) * rem + ((d >> 8) & 0x0000ff) * av) >> 8;
    mov eax, r12d
    shr eax, 0x8
    movzx edx, al
    movzx eax, r14b
    imul edx, eax
    mov eax, r13d
    shr eax, 0x8
    movzx ecx, al
    movzx eax, r15b
    imul eax, ecx
    lea eax, [eax, + edx]                   ; faster than add eax, edx
    shr eax, 0x8
    mov r9b, al
    shl r9d, 8

    ;=============================blue = ((s & 0x0000ff) * rem + (d & 0x0000ff) * av) >> 8;
    movzx edx, r12b
    movzx eax, r14b
    imul edx, eax
    movzx ecx, r13b
    movzx eax, r15b
    imul eax, ecx
    lea eax, [eax + edx]                ; faster than add eax, edx
    shr eax, 0x8
    mov r9b, al


    mov eax, r9d
    ret