这种同时处理颜色组件的颜色混合技巧是如何工作的?

8

我看到了一段非常高效的Java代码,可以完美地将两种RGB888颜色混合成50%:

public static int blendRGB(int a, int b) {
    return (a + b - ((a ^ b) & 0x00010101)) >> 1;
}

这明显相当于分别提取和平均各个通道。就像这样:
public static int blendRGB_(int a, int b) {
    int aR = a >> 16;
    int bR = b >> 16;
    int aG = (a >> 8) & 0xFF;
    int bG = (b >> 8) & 0xFF;
    int aB = a & 0xFF;
    int bB = b & 0xFF;
    int cR = (aR + bR) >> 1;
    int cG = (aG + bG) >> 1;
    int cB = (aB + bB) >> 1;
    return (cR << 16) | (cG << 8) | cB;
}

然而,第一种方法要高效得多。我的问题是:这个神奇的方法是如何工作的?我还能用它做什么?是否还有类似的技巧?


我必须处理许多从同事那里继承来的旧代码...其中一些人更喜欢类似于第一个代码块的代码,而另一些人则更喜欢类似于第二个代码块的代码 - 我必须承认,当我发现那些更长的代码片段时,我感到非常感激,因为它们要容易阅读得多!所以无论何时您编写代码(即使是正确的),请使用那些“更长”的代码,因为这将使其他人更容易理解。 - Martin Frank
1
快速颜色平均化,高彩色(16位)像素的快速平均化 - phuclv
1个回答

9
如果从右侧没有进位,(a ^ b) & 0x00010101就是通道最不重要的位在a + b中的值。将其从总和中减去可确保位移入下一个通道的最高有效位的位刚好是该通道的进位,未被此通道影响。当然,这也意味着此通道不再受到下一个通道的进位的影响。
另一种理解方法是,有效地更改输入,使它们的总和对于所有通道都是偶数。进位很好地进入了最不重要的位(因为是偶数),而没有干扰任何内容。当然,它实际上所做的事情有点相反,首先只是对它们求和,然后才确保所有通道的总和都是偶数。但顺序并不重要。
更具体地说,有4种情况(在应用来自下一个通道的进位之前):
  1. 通道的最不重要位为0,并且没有来自下一个通道的进位。
  2. 通道的最不重要位为0,并且存在来自下一个通道的进位。
  3. 通道的最不重要位为1,并且没有来自下一个通道的进位。
  4. 通道的最不重要位为1,并且存在来自下一个通道的进位。
前两种情况很简单。移位将带有进位的比特放回到它所属的通道中,无论它是0还是1都无关紧要。
第三种情况更有趣。如果最不重要位为1,则意味着移位会将该位移入下一个通道的最高有效位。那很糟糕。必须以某种方式取消该位-但您不能只是屏蔽它,因为也许您处于第4种情况。
第四种情况最有趣。如果最不重要位为1,并且存在进位,则会滚动到0并传播进位。这不能通过屏蔽来撤消,但可以通过反转过程来完成,即从最不重要位减去1(将其放回1并撤消由传播进位造成的任何损坏)。
正如您所看到的,在第3和第4种情况下,治愈方法是从最不重要位中减去1,并且这些情况下最不重要位确实希望是1(尽管可能不再是,由于来自下一个通道的进位),而在第1和第2种情况下,您无需执行任何操作(换句话说,减去0)。这恰好对应于减去“如果没有来自右侧的进位,a + b中最不重要的位将是什么”。
此外,蓝色通道只能落入情况1或3(没有下一个可用的通道),移位操作将丢弃该位而不是将其放入下一个通道中(因为不存在)。因此,你可以这样写(请注意掩码已失去最低有效位)。
public static int blendRGB(int a, int b) {
    return (a + b - ((a ^ b) & 0x00010100)) >> 1;
}

虽然并没有什么区别。

要使其适用于ARGB8888,您可以切换到传统的"SWAR平均":

// channel-by-channel average, no alpha blending
public static int blendARGB(int a, int b) {
    return (a & b) + (((a ^ b) & 0xFEFEFEFE) >>> 1);
}

这是一种递归定义加法的变体:x + y = (x ^ y) + ((x & y) << 1),它可以计算出没有进位的和,然后单独添加进位。其中一个操作数为零时,基本情况就出现了。
两个半部分都被有效地向右移动了1位,这样最高位的进位就不会丢失。掩码确保位不会移动到右侧的通道,并同时确保进位不会传播到其通道外。

我理解了你的第一行,但没看懂后面的内容。 - Boann
谢谢,我现在明白了。让我困惑的是当你说“下一个通道”时,我以为你指的是左边的那个,但实际上你指的是右边的那个。现在我看到原始代码实际上有一个微妙的错误:最佳掩码不是0x000101010x00010100,而是0x01010100。通过这个改变,如果低位alpha位是非零的,红色通道就不会再被破坏了。它仍然无法正确地平均前8位,但至少它不介意它们被设置了。 - Boann
@Boann 嗯,公平地说,名称和描述表明它期望RGB888,而不是ARGB8888。混合两个ARGB8888有点棘手,因为A通道的进位消失了,但是这是可行的。 - harold
@harold:我猜在64位JVM上,使用long作为ARGB中间结果的最快方式。然而,无论如何平均两个ARGB值都没有意义;你需要加权和或其他方法。 - maaartinus
@Boann 我并没有发明它(虽然我很想这样做),在chessprogramming:SWAR上有一些相关内容(可能有用的链接),TAOCP 4A中有更多位运算技巧和技术,可以同时调整几个字节,Hacker's Delight中也可能有相关内容,但我现在找不到了。 - harold
显示剩余4条评论

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