在图像处理中,透明度是一个重要的概念,它允许在图像的某些部分创建透明效果。通常,图像编辑工具如Microsoft Paint允许编辑位图并为每个像素选择颜色,颜色由红色、绿色和蓝色三个值定义。例如,纯红色像素的RGB值为255,0,0,黑色像素的RGB值为0,0,0。对于使用alpha混合的应用程序,每个像素还会增加一个alpha值,该值定义了透明度:alpha值为255表示完全不透明,127表示半透明,0表示完全透明。尽管BMP或PNG文件可能包含alpha层,但大多数情况下并不包含。
为了编辑位图的alpha层,编写了一个小型C#工具。这个工具的工作原理是将原始图像复制到一个新的位图对象中,然后逐像素地将掩码图像复制到位图的alpha层。掩码图像的暗部表示透明,亮部表示不透明。如果掩码图像的绘制方式相反,可以使用“反转掩码”复选框来反转掩码图像。如果没有单独的掩码文件,可以使用“使用加载的图像作为掩码”复选框,程序将自动创建一个掩码,方法是从加载的图像创建一个灰度位图。可以随时通过保存掩码(按下“保存掩码”按钮),使用喜欢的位图编辑器编辑它,取消选中“使用加载的图像作为掩码”复选框,然后重新加载编辑后的掩码。
应用程序窗口显示三个位图:左上角是原始位图,右上角是用于定义alpha层的掩码(重要的是:此位图必须与原始位图大小相同),底部是将掩码应用于原始位图后的结果。如果通常不使用部分透明度,可以取消选中“允许部分不透明度”复选框。这将强制掩码成为两种颜色(黑色和白色)的图像,而不是灰度图像。定义黑色和白色之间的阈值是可调的。
编写到位图的alpha层:程序首先将原始图像复制到位图对象maskedImage中,然后逐像素地将掩码(对象位图maskImage)复制到位图的alpha层。要访问像素,需要一个管理的字节向量,包含位图像素的数据。
BitmapData bmpData1 = maskedImage.LockBits(
new Rectangle(0, 0, maskedImage.Width, maskedImage.Height),
System.Drawing.Imaging.ImageLockMode.ReadWrite,
maskedImage.PixelFormat);
byte[] maskedImageRGBAData = new byte[bmpData1.Stride * bmpData1.Height];
System.Runtime.InteropServices.Marshal.Copy(bmpData1.Scan0, maskedImageRGBAData, 0, maskedImageRGBAData.Length);
这创建了字节向量bmpData1,包含位图maskedImage的像素数据。
BitmapData bmpData2 = maskImage.LockBits(
new Rectangle(0, 0, maskImage.Width, maskImage.Height),
System.Drawing.Imaging.ImageLockMode.ReadOnly,
maskImage.PixelFormat);
byte[] maskImageRGBAData = new byte[bmpData2.Stride * bmpData2.Height];
System.Runtime.InteropServices.Marshal.Copy(bmpData2.Scan0, maskImageRGBAData, 0, maskImageRGBAData.Length);
这创建了字节向量bmpData2,包含位图maskImage的像素数据。重要的是要注意.NET Bitmap对象在PixelFormat.Format32bppArgb像素格式时使用的内部像素存储顺序是:字节0:像素1的蓝色值,字节1:像素1的绿色值,字节2:像素1的红色值,字节3:像素1的alpha值,依此类推。
一旦有了两个向量,只需简单地将掩码向量的蓝色分量值复制到位图向量的alpha分量:
for (int i = 0; i + 2 < maskedImageRGBAData.Length; i += 4)
{
maskedImageRGBAData[i + 3] = maskImageRGBAData[i];
}
然后,将像素信息复制回遮罩图像,并不要忘记解锁位图对象的非托管内部部分。
System.Runtime.InteropServices.Marshal.Copy(maskedImageRGBAData, 0, bmpData1.Scan0, maskedImageRGBAData.Length);
this.maskedImage.UnlockBits(bmpData1);
this.maskImage.UnlockBits(bmpData2);
由于加载的位图可以有任何像素格式,而程序期望PixelFormat.Format32bppArgb格式,因此必须转换位图格式。这是通过创建一个相同大小的新位图并将原始位图绘制到其中来完成的。
Bitmap returnedImage = new Bitmap(tmpImage.Width, tmpImage.Height, PixelFormat.Format32bppArgb);
Rectangle rect = new Rectangle(0, 0, tmpImage.Width, tmpImage.Height);
Graphics g = Graphics.FromImage(returnedImage);
g.DrawImage(tmpImage, rect, 0, 0, tmpImage.Width, tmpImage.Height, GraphicsUnit.Pixel);
g.Dispose();
注意:这个函数有一个积极的效果:出于优化原因,当从文件创建位图实例时,.NET框架只要位图被使用就会保持文件打开。这意味着当尝试写回位图文件时,会得到一个异常。由于上述函数创建了一个完全独立的新位图,与从文件加载的原始位图无关,.NET框架会关闭文件,使其可以被覆盖。
掩码是一个由加载的图像制作的黑白或灰度图像。这是通过直接操作像素信息来实现的。这在“编写到位图alpha层”部分中解释,只是在这里将红色、绿色、蓝色值设置为通过以下代码获得的greyValue:
for (int i = 0; i + 2 < maskImageRGBData.Length; i += 4)
{
byte greyLevel = (byte)(0.3 * maskImageRGBData[i + 2] + 0.59 * maskImageRGBData[i + 1] + 0.11 * maskImageRGBData[i]);
if (opaque)
{
greyLevel = (greyLevel < OpacityThreshold) ? byte.MinValue : byte.MaxValue;
}
if (invertedMask)
{
greyLevel = (byte)(255 - (int)greyLevel);
}
maskImageRGBData[i] = greyLevel;
maskImageRGBData[i + 1] = greyLevel;
maskImageRGBData[i + 2] = greyLevel;
}
注意:这个循环执行了很多次。它必须尽可能快地执行。这就是为什么使用变量opaque和invertedMask而不是在每次迭代中调用访问器this.checkBoxAllowPartialOpacity.Checked和this.checkBoxInvertMask.Checked,以节省宝贵的微秒。
当打开的位图文件已经定义了alpha层时,需要将其重置为完全不透明。可以通过改变alpha层的每个字节为255来实现这一点,但.NET框架提供了一个更好的解决方案:类ImageAttributes允许对每个像素执行以下操作:
float[][] colorMatrixElements = {
new float[] {1, 0, 0, 0, 0},
new float[] {0, 1, 0, 0, 0},
new float[] {0, 0, 1, 0, 0},
new float[] {0, 0, 0, 0, 0},
new float[] {0, 0, 0, 1, 1}
};
ColorMatrix colorMatrix = new ColorMatrix(colorMatrixElements);
ImageAttributes imageAttributes = new ImageAttributes();
imageAttributes.SetColorMatrix(colorMatrix, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
g.DrawImage(tmpImage, rect, 0, 0, tmpImage.Width, tmpImage.Height, GraphicsUnit.Pixel, imageAttributes);