Multiply是PS中常用的一种混合方式,中文版称为“正片叠底”模式。
说起来非常简单,就是颜色分量相乘。但实际上简单的相乘并没有考虑到alpha channel。
如果两个图层都是RGBA四个通道,Multiply的算法还得加入alpha。
为了搞清楚这个问题,我琢磨了好几个小时。最后从颜色含义入手,配合一些测试结果,终于推出了PS的混合公式:
OutAlpha = A1 + A2 - A1*A2 OutColor = (1.0 - (1.0 - C1)*A1) * (1.0 - (1.0 - C2)*A2)
可以看出这个公式对于A1和A2是对称的,所以被混合的两个图层没有前后之分。
推理过程如下:Multiply混合可以看作是两张幻灯片叠在一起,因为变得更厚所以透过的颜色变少,从而达到减色效果。在这里颜色分量Color是光可以透过的程度,1.0即表示该颜色分量全部可以透过。1.0-Color表示被吸收掉的部分。在PS的混合算法中,Alpha用来调节吸收程度。因为Alpha经常用于表示透明度,1.0表示完全不透明,即吸收程度为100%。
结合以上两个定义:
(1.0 - Color) * Alpha // 表示被吸收的颜色 1.0 - (1.0 - Color) * Alpha // 表示最终透过的颜色
把两个最终透过的颜色相乘,即得到我们需要的最终减色结果。
在忽略Alpha只有RGB混合的情况下,Alpha都取1.0,则公式可以化简为:
OutAlpha = 1.0 OutColor = C1 * C2
结果为颜色分量直接相乘,也就是到处都可以找到的对Multiply的算法的描述。
以下是c#版的实现……
static byte Clamp(int result)
{
if (result < 0)
return (byte)0;
return result > 255 ? (byte)255 : (byte)result;
}
static byte MultiplyColor(int lhs, int rhs, int lhsAlpha, int rhsAlpha)
{
if (lhsAlpha == 0)
return Clamp(rhs);
else if (rhsAlpha == 0)
return Clamp(lhs);
int lhsMultiply = (255 - (255 - lhs) * lhsAlpha / 255);
int rhsMultiply = (255 - (255 - rhs) * rhsAlpha / 255);
int result = rhsMultiply * lhsMultiply / 255;
return Clamp(result);
}
// same as Photoshop multiply blend mode
static public void Multiply(Bitmap lhs, Bitmap rhs, Rectangle roi)
{
BitmapData lhsData = SetImageToProcess(lhs, roi);
BitmapData rhsData = SetImageToProcess(rhs, roi);
int width = lhsData.Width;
int height = lhsData.Height;
int offset = lhsData.Stride;
unsafe
{
byte* lhsPtr = (byte*)lhsData.Scan0;
byte* rhsPtr = (byte*)rhsData.Scan0;
for (int y = 0; y < height; ++y)
{
for (int x = 0; x < width * 4; x+=4)
{
int lhsAlpha = lhsPtr[x + 3];
int rhsAlpha = rhsPtr[x + 3];
// multiply color with alpha factor
lhsPtr[x + 0] = MultiplyColor(lhsPtr[x + 0], rhsPtr[x + 0], lhsAlpha, rhsAlpha);
lhsPtr[x + 1] = MultiplyColor(lhsPtr[x + 1], rhsPtr[x + 1], lhsAlpha, rhsAlpha);
lhsPtr[x + 2] = MultiplyColor(lhsPtr[x + 2], rhsPtr[x + 2], lhsAlpha, rhsAlpha);
// also blend the alpha channel
int retAlpha = (lhsAlpha + rhsAlpha - lhsAlpha * rhsAlpha / 255);
lhsPtr[x + 3] = Clamp(retAlpha);
}
lhsPtr += offset;
rhsPtr += offset;
}
}
lhs.UnlockBits(lhsData);
rhs.UnlockBits(rhsData);
}