使用 GDI+ 混合图像、栅格操作和基本颜色调整






4.78/5 (33投票s)
2003年11月28日
6分钟阅读

146989

2914
一篇关于使用栅格操作混合图像以及模拟 Photoshop 等软件中的混合模式的文章。
引言
我正在做一个项目,需要渲染一些图形。在开发过程中,我意识到能够控制一个图像如何在另一个图像上渲染会非常有帮助。
通常,像素只是简单地绘制在彼此之上,您唯一能控制的过程就是 alpha 通道。您还可以使用颜色矩阵在将源图像渲染到背景之上之前缩放、平移或旋转其颜色,但您无法执行诸如“获取背景颜色,反转它,然后乘以前景颜色”或“结果颜色 = 源 XOR 目标”之类的操作。我想能够告诉我的绘图例程,两个图像的像素将如何混合在一起。
我记得 GDI 有栅格操作 (ROP),您可以在 BitBlt
和 StretchBlt
函数中指定它们。所以起初,我决心尝试用 GDI+ 来模仿这些栅格操作。我可以导入 GDI32.DLL 并直接调用上述函数,但我很快发现有限的 ROP 集对我不够好,而且 BitBlt
中可用的 ROP 太丑陋,对于任何类型的数字图像处理来说都不是很实用。我希望能够扩展现有的和/或提供我自己的混合函数。
最后,我认为能够实现 Photoshop 中的图像混合模式会非常酷:叠加、乘法、屏幕、颜色减淡等等。
因此,本文与图像的 alpha 混合无关。有很多文章讨论 alpha 混合。事实上,在开发初期,我的代码完全忽略了 alpha 通道。本文的大部分内容是关于如何逐像素控制分离混合的图像。
混合有什么大不了的?
嗯,没什么大不了的。一切都很简单。我们从背景图像中取一个像素,从源图像中取一个像素,然后使用某些规则或公式将它们组合在一起。
这里基本上只有 2 种情况。
第一种情况是相同的公式适用于图像的每个通道(红色、绿色和蓝色)。在这种情况下,我们可以定义一个原型函数:
private delegate byte PerChannelProcessDelegate(ref byte nSrc, ref byte nDst);
该函数接收源字节和目标字节,并返回结果字节。以下是此类函数的一个示例:
// Choose darkest color
private byte BlendDarken(ref byte Src, ref byte Dst)
{
return ((Src < Dst) ? Src : Dst);
}
第二种类型的函数是接收源像素和目标像素的所有 RGB 数据,并计算结果 R、G 和 B 值。我为此类函数定义了一个委托,如下所示:
private delegate void RGBProcessDelegate(byte sR, byte sG, byte sB,
ref byte dR, ref byte dG, ref byte dB);
结果值在 dR
、dG
和 dB
参数中返回。下面是此类函数的一个示例:
// use source Hue
private void BlendHue(byte sR, byte sG, byte sB,
ref byte dR, ref byte dG, ref byte dB)
{
ushort sH, sL, sS, dH, dL, dS;
RGBToHLS(sR, sG, sB, out sH, out sL, out sS);
RGBToHLS(dR, dG, dB, out dH, out dL, out dS);
HLSToRGB(sH, dL, dS, out dR, out dG, out dB);
}
RGB 值首先转换为 HLS(色调、亮度、饱和度)颜色空间,然后使用背景(目标)像素的亮度和饱和度以及源像素的色调重新组合并转换回 RGB 空间。
使用这两种类型的函数,我们几乎可以在 RGB 颜色空间中描述任何两个像素的混合。
将混合函数应用于图像
为了将上述任何函数应用于图像,我定义了 2 个单独的处理函数,一个用于每种类型的混合函数。
第一个函数将任何指定的每个通道处理函数应用于混合源图像和目标图像的每个通道。
private Bitmap PerChannelProcess(ref Image destImg,
int destX, int destY, int destWidth, int destHeight,
ref Image srcImg, int srcX, int srcY,
PerChannelProcessDelegate ChannelProcessFunction)
{
Bitmap dst = new Bitmap(destImg);
Bitmap src = new Bitmap(srcImg);
BitmapData dstBD =
dst.LockBits( new Rectangle(destX, destY, destWidth, destHeight),
ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
BitmapData srcBD =
src.LockBits( new Rectangle(srcX, srcY, destWidth, destHeight),
ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
int dstStride = dstBD.Stride;
int srcStride = srcBD.Stride;
System.IntPtr dstScan0 = dstBD.Scan0;
System.IntPtr srcScan0 = srcBD.Scan0;
unsafe
{
byte *pDst = (byte *)(void *)dstScan0;
byte *pSrc = (byte *)(void *)srcScan0;
for(int y = 0; y < destHeight; y++)
{
for(int x = 0; x < destWidth * 3; x++)
{
pDst[x + y * dstStride] =
ChannelProcessFunction(ref pSrc[x + y * srcStride],
ref pDst[x + y * dstStride]);
}
}
}
src.UnlockBits(srcBD);
dst.UnlockBits(dstBD);
return dst;
}
使用 LockBits
方法锁定源图像和目标图像的指定区域,然后逐字节处理。PerChannelProcessDelegate
函数作为最后一个参数传递,并应用于源图像和目标图像每个像素的每个通道。
第二个函数做的事情几乎一样,但它接受第二种类型的混合函数(RGBProcessDelegate
)作为参数,并且它一次处理一个像素的数据。
private Bitmap RGBProcess(ref Image destImg,
int destX, int destY, int destWidth, int destHeight,
ref Image srcImg, int srcX, int srcY,
RGBProcessDelegate RGBProcessFunction)
{
Bitmap dst = new Bitmap(destImg);
Bitmap src = new Bitmap(srcImg);
BitmapData dstBD =
dst.LockBits( new Rectangle(destX, destY, destWidth, destHeight),
ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
BitmapData srcBD =
src.LockBits( new Rectangle(srcX, srcY, destWidth, destHeight),
ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb);
int dstStride = dstBD.Stride;
int srcStride = srcBD.Stride;
System.IntPtr dstScan0 = dstBD.Scan0;
System.IntPtr srcScan0 = srcBD.Scan0;
unsafe
{
byte *pDst = (byte *)(void *)dstScan0;
byte *pSrc = (byte *)(void *)srcScan0;
for(int y = 0; y < destHeight; y++)
{
for(int x = 0; x < destWidth; x++)
{
RGBProcessFunction(
pSrc[x * 3 + 2 + y * srcStride],
pSrc[x * 3 + 1 + y * srcStride],
pSrc[x * 3 + y * srcStride],
ref pDst[x * 3 + 2 + y * dstStride],
ref pDst[x * 3 + 1 + y * dstStride],
ref pDst[x * 3 + y * dstStride]
);
}
}
}
src.UnlockBits(srcBD);
dst.UnlockBits(dstBD);
return dst;
}
如您所见,以上所有方法都定义为 private
,并且仅在此处展示,以向您展示我的代码如何在内部处理混合。
公开地,我的类公开了 BlendImages
方法(以及一些重载版本),其签名如下:
/*
destImage - image that will be used as background
destX, destY - define position on destination
image where to start applying blend operation
destWidth, destHeight - width and height of the area to apply blending
srcImage - image to use as foreground (source of blending)
srcX, srcY - starting position of the source image
*/
public void BlendImages(Image destImage,
int destX, int destY, int destWidth, int destHeight,
Image srcImage, int srcX, int srcY, BlendOperation BlendOp)
这里的一切都很简单。唯一的新项是 BlendOperation
参数。BlendOperation
定义为枚举,用于指定要应用于图像的混合函数。目前定义了以下混合操作值:
SourceCopy // no blending, source is simply copied over destination
// Following are are equivalents of some of GDI's ROP functions
ROP_MergePaint
ROP_NOTSourceErase
ROP_SourceAND
ROP_SourceErase
ROP_SourceInvert
ROP_SourcePaint
// Following set is an attempt to simulate Photoshop's blending modes
// these are per-channel functions
Blend_Darken
Blend_Multiply
Blend_ColorBurn
Blend_Lighten
Blend_Screen
Blend_ColorDodge
Blend_Overlay
Blend_SoftLight
Blend_HardLight
Blend_PinLight // does not look as the one in Photoshop
Blend_Difference
Blend_Exclusion
// these are per pixel functions
Blend_Hue
Blend_Saturation // does not look as the one in Photoshop
Blend_Color // does not look as the one in Photoshop
Blend_Luminosity // does not look as the one in Photoshop
我必须说,虽然我尽了最大的努力使所有混合模式尽可能接近 Photoshop 混合模式产生的效果,但有几项我并未成功。Adobe 的创作者不公开其混合模式背后的确切数学公式,像我们这样的普通人只能猜测公式。
关于这个主题,互联网上的信息不多,我特别要感谢 Jens Gruschel 编译并发布了 混合模式公式列表。
基本颜色调整函数
除了我的示例类提供的图像混合功能外,我还包含了一些有用的颜色调整函数。所有这些函数都利用了 GDI+ 提供的 ColorMatrix
功能。它快速、强大且易于使用。每个图像颜色调整函数都调用以下方法:
public void ApplyColorMatrix(ref Image img, ColorMatrix colMatrix)
{
Graphics gr = Graphics.FromImage(img);
ImageAttributes attrs = new ImageAttributes();
attrs.SetColorMatrix(colMatrix);
gr.DrawImage(img, new Rectangle(0, 0, img.Width, img.Height),
0, 0, img.Width, img.Height, GraphicsUnit.Pixel, attrs);
gr.Dispose();
}
以下是我的类中使用的颜色矩阵:
// Invert
ColorMatrix cMatrix = new ColorMatrix(new float[][] {
new float[] {-1.0f, 0.0f, 0.0f, 0.0f, 0.0f },
new float[] { 0.0f,-1.0f, 0.0f, 0.0f, 0.0f },
new float[] { 0.0f, 0.0f,-1.0f, 0.0f, 0.0f },
new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
new float[] { 1.0f, 1.0f, 1.0f, 0.0f, 1.0f }
} );
// Adjust brightness
ColorMatrix cMatrix = new ColorMatrix(new float[][] {
new float[] { 1.0f, 0.0f, 0.0f, 0.0f, 0.0f },
new float[] { 0.0f, 1.0f, 0.0f, 0.0f, 0.0f },
new float[] { 0.0f, 0.0f, 1.0f, 0.0f, 0.0f },
new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
new float[] { adjValueR, adjValueG, adjValueB, 0.0f, 1.0f }
} );
亮度矩阵简单地通过指定值平移每个通道中的颜色。-1.0f 将导致完全黑暗(黑色),1.0f 将导致纯白色。
// Adjust saturation
ColorMatrix cMatrix = new ColorMatrix(new float[][] {
new float[] { (1.0f-sat)*rweight+sat,
(1.0f-sat)*rweight, (1.0f-sat)*rweight, 0.0f, 0.0f },
new float[] { (1.0f-sat)*gweight,
(1.0f-sat)*gweight+sat, (1.0f-sat)*gweight, 0.0f, 0.0f },
new float[] { (1.0f-sat)*bweight,
(1.0f-sat)*bweight, (1.0f-sat)*bweight+sat, 0.0f, 0.0f },
new float[] { 0.0f, 0.0f, 0.0f, 1.0f, 0.0f },
new float[] { 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }
} );
饱和度矩阵利用分配给每个 RGB 通道的权重。权重对应于我们眼睛对颜色通道的敏感度。标准的 NTSC 权重定义为:红色 0.299f,绿色 0.587f,蓝色 0.144f。sat
是饱和度值。饱和度有几个有趣的值。0.0f 的值会将图像转换为灰度(去饱和图像),1.0f 的值会将矩阵转换为单位矩阵(无颜色变化),-1.0f 的值将导致图像的互补色。
整合所有内容
在演示应用程序中,我包含了一些代码,展示了如何使用我的 ImageBlender
;-) 类来改善图像的阴影区域。我女儿 Leah 的原始图像,她的脸几乎完全处于阴影中。应用一系列简单的图像处理/混合步骤可以恢复图像阴影区域的一些细节。
以下是生成上述结果的代码:
// create ImageBlender object
KVImage.ImageBlender ib = new KVImage.ImageBlender();
// Load original image
Image imgLeah = Image.FromFile(@"..\..\leah.jpg");
// display the original
pic1.Image = new Bitmap(imgLeah,
pic1.ClientSize.Width, pic1.ClientSize.Height);
// create a copy of image
Image imgTemp = new Bitmap(imgLeah);
// convert it to grayscale
ib.Desaturate(imgTemp);
// invert the image
ib.Invert(imgTemp);
// display intermediate step in pic2
pic2.Image = new Bitmap(imgTemp,
pic2.ClientSize.Width, pic2.ClientSize.Height);
// blend images using SoftLight function
ib.BlendImages(imgLeah, imgTemp,
KVImage.ImageBlender.BlendOperation.Blend_SoftLight);
// Add some brightness to the image
ib.AdjustBrightness(imgLeah, 0.075f);
// display the result
pic3.Image = new Bitmap(imgLeah,
pic3.ClientSize.Width, pic3.ClientSize.Height);
imgTemp.Dispose();
imgLeah.Dispose();
当然,这只是一个非常基础的例子,但我认为它很好地展示了图像混合模式的潜在力量。
改进空间
我的代码还有很多可以改进的地方:更多的注释、速度优化、更高的灵活性、alpha 通道处理、混合不透明度、选择和通道蒙版的添加。我将在未来版本的类中尝试添加其中一些功能。
感谢您花时间阅读本文,希望您觉得它有用。