QQ群聊的背景色为白色,而打开图片后的背景色为黑色,如果能巧妙修改图片各个像素的透明度,就可以达到在不同背景下显示出不同图片的功能.
效果
点开前
点开后
原理分析
我们已经知道是通过修改透明度来实现这个效果,现在只需要计算出透明度就行了.
假设有两张图片,一张是在白色背景下可以看到的,我们称之为“白图”,另一种是在黑色背景下才能看到的,我们称之为“黑图”.为了把两张图混合在一起,对任意像素点G(i,j),计算(i+j),根据即奇偶性来选择显示白图还是黑图,相当于把两张图片穿插在一起,整体上看起来不会有任何异常.
对于透明度具体怎么计算,下面提供了三种思路.
透明度叠加算法
设有两张图A,B,A在B的上面,B的不透明度为255(0表示全透明,255表示不透明),A的不透明度为alpha,则实际看到的像素值为
255A⋅alpha+B⋅(255−alpha)
灰度图算法
设白图在点(i,j)处像素值为(R,G,B),求出灰度值Gray,通常采用的灰度值算法为
Gray=(0.299⋅R+0.587⋅G+0.114⋅B)
现在已经知道了灰度值Gray,我们希望它能在白色背景下显示出原图的灰度图,此时前景色为黑色,背景色为白色,列出表达式:
2550⋅alpha+255⋅(255−alpha)=Gray
得到alpha=255−Gray,即Gray的反色.
设黑图在(i,j)处的灰度值为Gray,此时前景色为白色,背景色为黑色,列出表达式:
255255⋅alpha+0⋅(255−alpha)=Gray
得到alpha=Gray.
所以对于白图,把它的不透明度设置为(255−Gray),对于黑图,把它的不透明度设置为Gray,就能做到在白色背景下能看到白图,在黑色背景下能看到黑图的效果.
算法代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| private static Bitmap Merge_GRAY(Bitmap p1, Bitmap p2) { bool flag; Color color; int transparency; Bitmap bitmap = new Bitmap(p1.Width, p1.Height); for (int i = 0; i < p1.Width; i++) { for (int j = 0; j < p1.Height; j++) { flag = (i + j) % 2 == 0; if (flag) { color = p1.GetPixel(i, j); transparency = (color.R * R + color.G * G + color.B * B) / 1000; bitmap.SetPixel(i, j, Color.FromArgb(255 - transparency, Color.Black)); } else { color = p2.GetPixel(i, j); transparency = (color.R * R + color.G * G + color.B * B) / 1000; bitmap.SetPixel(i, j, Color.FromArgb(transparency, Color.White)); } } } return bitmap; }
|
黑白图算法
与灰度图类似,但是要将灰度值用黑白两种颜色来代替,所以需要先遍历整张图,计算平均灰度值,把高于平均值的一律替换成白色,即255,把低于平均值的一律替换成黑色,即0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| private static Bitmap Merge_BLACK_AND_WHITE(Bitmap p1, Bitmap p2) { bool flag; Color color; int RGB; Bitmap bitmap = new Bitmap(p1.Width, p1.Height); int[,] gray1 = new int[p1.Width,p1.Height]; int[,] gray2 = new int[p1.Width, p1.Height]; int sum1 = 0, sum2 = 0; for (int i = 0; i < p1.Width; i++) { for (int j = 0; j < p1.Height; j++) { color = p1.GetPixel(i,j); gray1[i, j] = (color.R * R + color.G * G + color.B * B) / 1000; sum1 += gray1[i, j]; color = p2.GetPixel(i, j); gray2[i, j] = (color.R * R + color.G * G + color.B * B) / 1000; sum2 += gray2[i, j]; } } sum1 /= p1.Width * p1.Height; sum2 /= p1.Width * p1.Height; for(int i = 0; i < p1.Width; i++) { for(int j = 0; j < p1.Height; j++) { flag = (i + j) % 2 == 0; if (flag) { RGB = gray1[i, j] > sum1 ? 0 : 255; bitmap.SetPixel(i, j, Color.FromArgb(RGB, Color.Black)); } else { RGB = gray2[i, j] > sum2 ? 0 : 255; bitmap.SetPixel(i, j, Color.FromArgb(255 - RGB, Color.White)); } } } return bitmap; }
|
彩色图算法
若一张白图想要在白色背景下显示,设不透明度为alpha=(R,G,B)T,列出表达式
255Gray⋅alpha+(255,255,255)[(255,255,255)T−alpha)]=Gray
显然alpha=(255,255,255)T
当这张图在黑色背景下时,需要它完全隐藏.
255Gray⋅alpha+(0,0,0)[(255,255,255)T−alpha)]=Gray
则只能alpha=Gray=(0,0,0)T
为什么会出现两个完全相反的答案,而之前不会?原来之前的灰度图中,使用灰色像素来显示白图,在白色背景下通过不透明度让灰色像素显示,而在黑色背景下,灰色像素有颜色优势,无论不透明度是多少都不影响它在黑色背景下隐藏.但是彩色像素就不一样了,如果不透明度太大,会导致它在黑色背景下无法隐藏,最终出现两个图显示在一起的效果.
现在的问题在于:如果要图片更清晰,则需要增大不透明度,如果要白图黑图互不干扰,则需要减少不透明度.显然减少黑白图的相互干扰比清晰显示更重要.白图在黑色背景下,灰度值越高(颜色越白),则不透明度应该越低.
所以我们得出结论,不透明度应随着灰度值的增大而减小,且具有相同区间[0,255],显然正比例函数就具有上述特性
设不透明度alpha,灰度值Gray=3(0.299⋅R+0.587⋅G+0.114⋅B);
alpha=k⋅Gray+m
代入(alpha,Gray)(0,255),(255,0),得到 k=−1,m=255.
当然不一定是一次函数,也可以是二次函数,但是实现起来较为复杂,所以不考虑.
现在我们得到alpha=255−G,这是白图的计算方法.
对于黑图,它想要在黑色背景下显示,因此灰度值越大(颜色越白),不透明度越高,即不透明度与灰度值也成正比,我们也用上面那式子来代入计算,
得到 alpha=G,这是黑图的计算方法.
代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| private static Bitmap Merge_COLORFUL(Bitmap p1,Bitmap p2) { bool flag; Color color; int transparency; Bitmap bitmap = new Bitmap(p1.Width, p1.Height); for (int i = 0; i < p1.Width; i++) { for (int j = 0; j < p1.Height; j++) { flag = (i + j) % 2 == 0; if (flag) { color = p1.GetPixel(i, j); transparency = (color.R * R + color.G * G + color.B * B) / 1000; bitmap.SetPixel(i, j, Color.FromArgb(255 - transparency, p1.GetPixel(i, j))); } else { color = p2.GetPixel(i, j); transparency = (color.R * R + color.G * G + color.B * B) / 1000; bitmap.SetPixel(i, j, Color.FromArgb(transparency, p2.GetPixel(i,j))); } } } return bitmap; }
|
图像放缩
想要让两种图片混合,显然它们必须具有相同的宽度和高度,使用C#
自带的放缩方法即可完成
1 2 3 4 5 6 7 8 9
| private static Bitmap Scale(Bitmap b,int width,int height) { Bitmap bitmap = new Bitmap(width, height); Graphics g = Graphics.FromImage(bitmap); g.InterpolationMode = interpolationMode; g.DrawImage(b, new Rectangle(0, 0, width, height), new Rectangle(0, 0, b.Width, b.Height), GraphicsUnit.Pixel); g.Dispose(); return bitmap; }
|
界面设计
主界面显示黑白两种颜色,分别用来模拟白色和黑色背景
下图左右两边显示出不同的图片,实际上是同一张,由于背景色的缘故看起来不同,这就是QQ上的最终效果.
点击“原图”后显示原来的图片,点击“导出”后把生成的图片保存到本地.
设置里提供了高度自定义功能,可以修改灰度算法的参数,图片比例,放缩算法和图片类型.
备注
上述的算法都是基于理论的,现实中QQ的背景不一定是纯白,所以会导致图片显示异常.据我所知,目前最新版QQ的默认背景不是纯白,因此直接使用我的源文件可能会出现显示异常,你可以对我的源文件进行修改,使之能够适应最新版QQ背景颜色.
如果图片太大,其缩略图会被自动压缩,也可能会导致显示异常,甚至“白图”会直接变成纯黑色,必须点开才能看到“黑图”.如果尝试生成彩色图,而两张图颜色对比度太大,可能会导致两种图片同时显示.而本程序里显示的最终效果是严格基于理想条件的,所以不保证最终显示在QQ里的效果和程序里的效果相同.
源文件