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); }