啥是幻影坦克? 幻影坦克就是, 一张黑白图片, 在黑色背景下和白色背景下能够显示出不同的图像.
首先, 我可以明确的告诉你, 它的原理就是控制像素的颜色和Alpha通道(不透明度), 来使显示的图像在不同背景下显示不同的颜色.
最基本的, 就是, 一张半透明的黑色薄膜, 如果在黑色的纸上, 你什么也看不出来, 但如果在白色纸上, 你可以看见, 它是灰色.
本文中, Alpha 统一拟定为 0 ~ 1 的浮点数, 像素亮度统一为 0 ~ 1 即像素’白的程度’, 例如纯白为1, 纯黑则0.
注意:
本项目已经在 github.com/SlimeNull/Null.PhantomTank 开源. 你可以直接 clone 下来以查看源码.
幻影坦克合成库, NullLib.PhantomTank 已经发布到 nuget.org, 你可以直接在 VS 的 nuget 包管理器中直接安装本库
基本原理:
像素在某背景下最终显示出来的公式如下:
设: 这个图像的背景亮度为 , 这个像素的亮度为 , 不透明度为 , 则最终显示的颜色 就是:
理解起来也简单, 还拿刚刚的例子来讲, 例如一个纯黑的半透明薄膜, 那他肯定:
- 只有一半的黑色能显示出来, 即
- 而背景色, 也有一半的颜色能够透过来. 即:
- 总的颜色加起来, 也就是
幻影坦克:
而当一个像素为白色背景时, 能够显示出一个特定的颜色 , 当黑色背景时, 显示出 , 也可得出一个公式,
- 设: 颜色 的亮度为 , 颜色 的亮度为 , 这个像素的亮度为 , 不透明度为 , 则满足:
- 稍微处理一下, 可得到下面的公式:
- 由于 , , , 都是小于等于1, 大于等于0的值, 所以:
- 最终我们需要的算式是这些:
- 而 ARGB 通道的值是 0 ~ 255, 所以需要进行转换一下:
- 而刚刚我们需要满足的条件, 其中只有一个是我们真正需要进行处理的, 即:
解决方案:
关于 的条件, 很简单, 与 的值是在 到 之内的, 那我们只需要将其压制到 到 之间, 将 压制到 到 之间, 即可解决. 然后就可以直接用我们得出的公式来运算了.
Color CalcPixel(Color x, Color y){ int xc = (x.R + x.G + x.B) / 3, yc = (y.R + y.G + z.B) / 3; // 获取亮度
xc = (xc / 255f) * 127 + 128; yc = (yc / 255f) * 127; // 压制颜色
int za = yc - xc + 255, zc = za == 0 ? 0 : yc * 255 / za; // 运算结果颜色
return Color.FromArgb(ya, yc, yc, yc);}我们还可以加点功能, 就是 x 与 y 的颜色占用比例, 例如, 刚刚的就是 1:1, 如果是 10 : 245, 则 x 占用 245 ~ 255, y 占用 0 ~ 245.
// src1ColorRatio 为 x 的占用比例, 值域是0~1Color CalcPixel(Color src1, Color src2, float src1ColorRatio = 0.5f){ float src2ColorRatio = 1 - src1ColorRatio; // 运算出 y 的占用比例
int xc = (int)((src1.R + src1.G + src1.B) * src1ColorRatio / 3 + src2ColorRatio * 255 + 1), yc = (int)((src2.R + src2.G + src2.B) * src2ColorRatio / 3);
int za = yc - xc + 255, zc = za == 0 ? 0 : (yc * 255 / za);
return Color.FromArgb(za, zc, zc, zc);}完整代码:
需要的库:
- System.Drawing 程序集或 System.Drawing.Common 包.
LockBitmap 源码:
using System;using System.Drawing;using System.Drawing.Imaging;using System.Runtime.InteropServices;
namespace Null.PhantomTank.Library{ public class LockBitmap : IDisposable { Bitmap source = null; IntPtr Iptr = IntPtr.Zero; BitmapData bitmapData = null;
public byte[] Pixels { get; set; } public int Depth { get; private set; } public int Width { get; private set; } public int Height { get; private set; } public bool IsLocked { get; private set; }
private Func<int, Color> colorGetter; private Action<int, Color> colorSetter;
public LockBitmap(Bitmap source) { this.source = source; LockBits(); }
/// <summary> /// Lock bitmap data /// </summary> public void LockBits() { try { // Get width and height of bitmap Width = source.Width; Height = source.Height;
// get total locked pixels count //int PixelCount = Width * Height;
// Create rectangle to lock System.Drawing.Rectangle rect = new System.Drawing.Rectangle(0, 0, Width, Height);
// get source bitmap pixel format size Depth = System.Drawing.Bitmap.GetPixelFormatSize(source.PixelFormat);
// Check if bpp (Bits Per Pixel) is 8, 24, or 32 if (Depth == 8) { colorGetter = (offset) => Color.FromArgb(Pixels[offset], Pixels[offset], Pixels[offset]); colorSetter = (offset, color) => { Pixels[offset] = color.B; }; } else if (Depth == 24) { colorGetter = (offset) => Color.FromArgb(Pixels[offset + 2], Pixels[offset + 1], Pixels[offset]); colorSetter = (offset, color) => { Pixels[offset] = color.B; Pixels[offset + 1] = color.G; Pixels[offset + 2] = color.R; }; } else if (Depth == 32) { colorGetter = (offset) => Color.FromArgb(Pixels[offset + 3], Pixels[offset + 2], Pixels[offset + 1], Pixels[offset]); colorSetter = (offset, color) => { Pixels[offset] = color.B; Pixels[offset + 1] = color.G; Pixels[offset + 2] = color.R; Pixels[offset + 3] = color.A; }; } else { throw new ArgumentException("Only 8, 24 and 32 bpp images are supported."); }
// Lock bitmap and return bitmap data bitmapData = source.LockBits(rect, ImageLockMode.ReadWrite, source.PixelFormat);
// create byte array to copy pixel values int step = Depth / 8; Pixels = new byte[bitmapData.Stride * Height]; Iptr = bitmapData.Scan0;
IsLocked = true;
// Copy data from pointer to array Marshal.Copy(Iptr, Pixels, 0, Pixels.Length); } catch (Exception ex) { throw ex; } }
/// <summary> /// Unlock bitmap data /// </summary> public void UnlockBits() { try { // Copy data from byte array to pointer Marshal.Copy(Pixels, 0, Iptr, Pixels.Length);
// Unlock bitmap data source.UnlockBits(bitmapData);
IsLocked = false; } catch (Exception ex) { throw ex; } }
/// <summary> /// Get the color of the specified pixel /// </summary> /// <param name="x"></param> /// <param name="y"></param> /// <returns></returns> public Color GetPixel(int x, int y) { Color clr = Color.Empty;
// Get color components count int cCount = Depth / 8;
// Get start index of the specified pixel //int i = ((y * Width) + x) * cCount; int i = y * bitmapData.Stride + x * cCount;
if (i > Pixels.Length - cCount) throw new IndexOutOfRangeException();
// Get color by array index clr = colorGetter.Invoke(i);
return clr; }
/// <summary> /// Set the color of the specified pixel /// </summary> /// <param name="x"></param> /// <param name="y"></param> /// <param name="color"></param> public void SetPixel(int x, int y, Color color) { // Get color components count int cCount = Depth / 8;
// Get start index of the specified pixel //int i = ((y * Width) + x) * cCount; int i = y * bitmapData.Stride + x * cCount;
// Set color by array index and color object colorSetter.Invoke(i, color); }
public bool IsValidCoordinate(int x, int y) { return x >= 0 && x < this.Width && y > 0 && y < this.Height; }
#region IDisposable Support private bool disposedValue = false; // 要检测冗余调用
protected virtual void Dispose(bool disposing) { if (!disposedValue) { if (disposing) { // TODO: 释放托管状态(托管对象)。 }
// TODO: 释放未托管的资源(未托管的对象)并在以下内容中替代终结器。 // TODO: 将大型字段设置为 null。 UnlockBits(); disposedValue = true; } }
// TODO: 仅当以上 Dispose(bool disposing) 拥有用于释放未托管资源的代码时才替代终结器。 // ~LockBitmap() { // // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。 // Dispose(false); // }
// 添加此代码以正确实现可处置模式。 void IDisposable.Dispose() { // 请勿更改此代码。将清理代码放入以上 Dispose(bool disposing) 中。 Dispose(true); // TODO: 如果在以上内容中替代了终结器,则取消注释以下行。 // GC.SuppressFinalize(this); } #endregion }}幻影坦克制作源码:
using System;using System.Collections.Generic;using System.Drawing;using System.Linq;using System.Text;using Null.PhantomTank.Library;
namespace Null.PhantomTank{ public static class PhantomTank { /// 像素运算方式 变量 : 源色: Xc Yc, 输出: Za Zc /// 其中: c表示Color, 即颜色亮度, a代表Alpha通道, 即不透明度 /// 备注: 值全部为0~1的比值 /// /// Expr: | Xc = Za * Zc + 1 - Za /// | Yc = Za * Zc /// /// Calc: | Xc = Yc + 1 - Za /// | Yc = Xc + Za - 1 /// | Za = Yc - Xc + 1 /// /// Need: | 1 >= Yc + 1 - Za >= 0, 0 >= Yc - Za >= -1; Then: | Za >= Yc /// | 1 >= Xc + Za - 1 >= 0, 2 >= Xc + Za >= 1; Then: | Xc + Za >= 1 /// | 1 >= Yc - Xc + 1 >= 0, 0 >= Yc - Xc >= -1; Then: | Xc >= Yc /// /// Root: | Zc = Yc / Za /// | Za = Yc - Xc + 1 /// /// Basic Root: | Zc = Yc / (Za / 255) = Yc * 255 / Za /// | Za = Yc - Xc + 255
private static Color CalcPixel(Color src1, Color src2, float src1ColorRatio = 0.5f) { float src2ColorRatio = 1 - src1ColorRatio;
int xc = (int)((src1.R + src1.G + src1.B) * src1ColorRatio / 3 + src2ColorRatio * 255), yc = (int)((src2.R + src2.G + src2.B) * src2ColorRatio / 3);
int za = yc - xc + 255, zc = za == 0 ? 0 : (yc * 255 / za);
return Color.FromArgb(za, zc, zc, zc); } private static float ConvertRatio(float ratio) { return ratio / (ratio + 1); } public const float DefaultRatio = 1; public static Bitmap ResizeBitmap(Bitmap src, Color bgColor, ResizeMode resize, int newWidth, int newHeight) { Size srcSize = src.Size;
int srcWidth = srcSize.Width, srcHeight = srcSize.Height;
Bitmap result = new Bitmap(newWidth, newHeight, src.PixelFormat); Graphics rstG = Graphics.FromImage(result);
rstG.Clear(bgColor);
Size destSize; Rectangle srcRect, destRect;
switch(resize) { case ResizeMode.NoResize: destRect = new Rectangle((newWidth - srcWidth) / 2, (newHeight - srcHeight) / 2, srcWidth, srcHeight); rstG.DrawImageUnscaled(src, destRect); break; case ResizeMode.Stretch: srcRect = new Rectangle(0, 0, srcWidth, srcHeight); destRect = new Rectangle(0, 0, newWidth, newHeight); rstG.DrawImage(src, destRect, srcRect, GraphicsUnit.Pixel); break; case ResizeMode.Uniform: srcRect = new Rectangle(0, 0, srcWidth, srcHeight); destSize = new Size(newWidth, srcHeight * newWidth / srcWidth); if (destSize.Height > newHeight) destSize = new Size(srcWidth * newHeight / srcHeight, newHeight); destRect = new Rectangle(new Point((newWidth - destSize.Width) / 2, (newHeight - destSize.Height) / 2), destSize); rstG.DrawImage(src, destRect, srcRect, GraphicsUnit.Pixel); break; case ResizeMode.UniformToFill: srcRect = new Rectangle(0, 0, srcWidth, srcHeight); destSize = new Size(newWidth, srcHeight * newWidth / srcWidth); if (destSize.Height < newHeight) destSize = new Size(srcWidth * newHeight / srcHeight, newHeight); destRect = new Rectangle(new Point((newWidth - destSize.Width) / 2, (newHeight - destSize.Height) / 2), destSize); rstG.DrawImage(src, destRect, srcRect, GraphicsUnit.Pixel); break; }
return result; }
/// <summary> /// 最基本的合成方法, 请保证图片尺寸是一致的 /// </summary> /// <param name="src1">在白底下可以看到的图片</param> /// <param name="src2">在黑底下可以看到的图片</param> /// <returns>合并后的黑白图像</returns> public static Bitmap BasicCombineBitmap(Bitmap src1, Bitmap src2, float colorRatio) { if (src1 == null || src2 == null) throw new ArgumentNullException(); if (src1.Size != src2.Size) throw new ArgumentOutOfRangeException();
Size srcSize = src1.Size; int width = srcSize.Width, height = srcSize.Height;
Bitmap result = new Bitmap(width, height, src1.PixelFormat); result.SetResolution(src1.HorizontalResolution, src1.VerticalResolution);
LockBitmap lbmp1 = new LockBitmap(src1); LockBitmap lbmp2 = new LockBitmap(src2); LockBitmap lrst = new LockBitmap(result);
float whiteRatio = ConvertRatio(colorRatio);
for (int i = 0; i < height; i++) { for (int j = 0; j < width; j++) { Color srcPixel1 = lbmp1.GetPixel(j, i), srcPixel2 = lbmp2.GetPixel(j, i), outPixel = CalcPixel(srcPixel1, srcPixel2, whiteRatio);
lrst.SetPixel(j, i, outPixel); } }
lbmp1.UnlockBits(); lbmp2.UnlockBits(); lrst.UnlockBits();
return result; }
/// <summary> /// 转换图片 /// </summary> /// <param name="src">源图</param> /// <param name="tankType">坦克类型</param> /// <returns>转换结果</returns> public static Bitmap ConvertBitmap(Bitmap src, TankType tankType) { Size srcSize = src.Size;
int srcWidth = srcSize.Width, srcHeight = srcSize.Height;
Bitmap result = new Bitmap(srcWidth, srcHeight, src.PixelFormat); result.SetResolution(src.HorizontalResolution, src.VerticalResolution);
LockBitmap lsrc = new LockBitmap(src); LockBitmap lrst = new LockBitmap(result);
Func<int, Color> pixelCalcFunc; switch (tankType) { case TankType.AppearOnBlack: pixelCalcFunc = (srcPixel) => Color.FromArgb(srcPixel, 255, 255, 255); break; case TankType.AppearOnWhite: pixelCalcFunc = (srcPixel) => Color.FromArgb(255 - srcPixel, 0, 0, 0); break; default: throw new InvalidOperationException("Not supported."); }
for (int i = 0; i < srcHeight; i++) { for (int j = 0; j < srcWidth; j++) { Color pixel = lsrc.GetPixel(j, i); int light = (pixel.R + pixel.G + pixel.B) / 3; lrst.SetPixel(j, i, pixelCalcFunc.Invoke(light)); } }
lsrc.UnlockBits(); lrst.UnlockBits();
return result; } public static Bitmap ConvertImage(Image src, TankType tankType) { Bitmap newSrc = new Bitmap(src); Bitmap result = ConvertBitmap(newSrc, tankType);
newSrc.Dispose(); return result; }
public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, Color bgColor1, Color bgColor2, ResizeMode resize, float colorRatio) { Size src1Size = src1.Size, src2Size = src2.Size;
int src1Width = src1Size.Width, src1Height = src1Size.Height, src2Width = src2Size.Width, src2Height = src2Size.Height;
int maxWidth = src1Width > src2Width ? src1Width : src2Width, maxHeight = src1Height > src2Height ? src1Height : src2Height;
Bitmap newSrc1 = new Bitmap(maxWidth, maxHeight, src1.PixelFormat), newSrc2 = new Bitmap(maxWidth, maxHeight, src2.PixelFormat);
Graphics srcG1 = Graphics.FromImage(newSrc1), srcG2 = Graphics.FromImage(newSrc2);
srcG1.Clear(bgColor1); srcG2.Clear(bgColor2);
// 这里进行的是对图片的重新调整尺寸操作 switch(resize) { case ResizeMode.NoResize: srcG1.DrawImageUnscaled(src1, (maxWidth - src1Width) / 2, (maxHeight - src1Height) / 2); srcG2.DrawImageUnscaled(src2, (maxWidth - src2Width) / 2, (maxHeight - src2Height) / 2); break; case ResizeMode.Stretch: srcG1.DrawImage(src1, new Rectangle(0, 0, maxWidth, maxHeight), new Rectangle(0, 0, src1Width, src1Height), GraphicsUnit.Pixel); srcG2.DrawImage(src2, new Rectangle(0, 0, maxWidth, maxHeight), new Rectangle(0, 0, src2Width, src2Height), GraphicsUnit.Pixel); break; case ResizeMode.Uniform: Size scaleSize1 = new Size(maxWidth, (int)(src1Height * ((float)maxWidth / src1Width))), scaleSize2 = new Size(maxWidth, (int)(src2Height * ((float)maxWidth / src2Width))); if (scaleSize1.Height > maxHeight) scaleSize1 = new Size((int)(src1Width * ((float)maxHeight / src1Height)), maxHeight); if (scaleSize2.Height > maxHeight) scaleSize2 = new Size((int)(src2Width * ((float)maxHeight / src2Height)), maxHeight); srcG1.DrawImage(src1, new Rectangle(new Point((maxWidth - scaleSize1.Width) / 2, (maxHeight - scaleSize1.Height) / 2), scaleSize1), new Rectangle(0, 0, src1Width, src1Height), GraphicsUnit.Pixel); srcG2.DrawImage(src2, new Rectangle(new Point((maxWidth - scaleSize2.Width) / 2, (maxHeight - scaleSize2.Height) / 2), scaleSize2), new Rectangle(0, 0, src2Width, src2Height), GraphicsUnit.Pixel); break; case ResizeMode.UniformToFill: Size scaleFillSize1 = new Size(maxWidth, (int)(src1Height * ((float)maxWidth / src1Width))), scaleFillSize2 = new Size(maxWidth, (int)(src2Height * ((float)maxWidth / src2Width))); if (scaleFillSize1.Height < maxHeight) scaleFillSize1 = new Size((int)(src1Width * ((float)maxHeight / src1Height)), maxHeight); if (scaleFillSize2.Height < maxHeight) scaleFillSize2 = new Size((int)(src2Width * ((float)maxHeight / src2Height)), maxHeight); srcG1.DrawImage(src1, new Rectangle(new Point((maxWidth - scaleFillSize1.Width) / 2, (maxHeight - scaleFillSize1.Height) / 2), scaleFillSize1), new Rectangle(0, 0, src1Width, src1Height), GraphicsUnit.Pixel); srcG2.DrawImage(src2, new Rectangle(new Point((maxWidth - scaleFillSize2.Width) / 2, (maxHeight - scaleFillSize2.Height) / 2), scaleFillSize2), new Rectangle(0, 0, src2Width, src2Height), GraphicsUnit.Pixel); break; }
srcG1.Dispose(); srcG2.Dispose();
Bitmap result = BasicCombineBitmap(newSrc1, newSrc2, colorRatio); // 调用最终合成方法 newSrc1.Dispose(); newSrc2.Dispose();
return result; } public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, Color bgColor1, Color bgColor2, ResizeMode resize) { return CombineBitmap(src1, src2, Color.White, Color.Black, resize, DefaultRatio); } public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, Color bgColor1, Color bgColor2, float colorRatio) { return CombineBitmap(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, DefaultRatio); } public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, ResizeMode resize, float colorRatio) { return CombineBitmap(src1, src2, Color.White, Color.Black, resize, colorRatio); } public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, ResizeMode resize) { return CombineBitmap(src1, src2, Color.White, Color.Black, resize, 0); } public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2, float colorRatio) { return CombineBitmap(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, colorRatio); } public static Bitmap CombineBitmap(Bitmap src1, Bitmap src2) { return CombineBitmap(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, DefaultRatio); }
public static Bitmap CombineImage(Image src1, Image src2, Color bgColor1, Color bgColor2, ResizeMode resize, float colorRatio) { Bitmap newSrc1 = new Bitmap(src1), newSrc2 = new Bitmap(src2);
Bitmap result = CombineBitmap(newSrc1, newSrc2, bgColor1, bgColor2, resize, colorRatio); newSrc1.Dispose(); newSrc2.Dispose();
return result; } public static Bitmap CombineImage(Image src1, Image src2, Color bgColor1, Color bgColor2, ResizeMode resize) { return CombineImage(src1, src2, Color.White, Color.Black, resize, DefaultRatio); } public static Bitmap CombineImage(Image src1, Image src2, Color bgColor1, Color bgColor2, float colorRatio) { return CombineImage(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, colorRatio); } public static Bitmap CombineImage(Image src1, Image src2, ResizeMode resize, float colorRatio) { return CombineImage(src1, src2, Color.White, Color.Black, resize, colorRatio); } public static Bitmap CombineImage(Image src1, Image src2, ResizeMode resize) { return CombineImage(src1, src2, Color.White, Color.Black, resize, DefaultRatio); } public static Bitmap CombineImage(Image src1, Image src2, float colorRatio) { return CombineImage(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, colorRatio); } public static Bitmap CombineImage(Image src1, Image src2) { return CombineImage(src1, src2, Color.White, Color.Black, ResizeMode.NoResize, DefaultRatio); } } public enum ResizeMode { NoResize, Stretch, Uniform, UniformToFill, } public enum TankType { AppearOnBlack, AppearOnWhite, }}