上一次讲到哪了,说了下bmp位图格式以及图像处理入门。门也入了 搞点别的吧,好 我们继续接着折腾。bmp格式的数据就放在内存里 你爱折腾不折腾他就在那
总之一句话 搞清楚他的结构 用你清晰的逻辑去处理它。
我们这次要做的事情是降低颜色深度及调色板处理,反正我是找了园子里也没看见类似的东西 都是C++或者其他什么的。总之我们要做的这两个事情都要用到调色板。
要想取得一个图像的调色板的所有颜色Image.Palette.Entries 就可以了 得到的是一个Color数组。有些固定颜色深度的图像 都有默认的调色板比如4位(16色)8位(256色)等。 你用过win31 或者win95没装显卡驱动时一定见过那些猪肝色的图像 。我们初始化一个4位的bmp图像(16色)来看看他默认的调色板都有哪些颜色,添加一个叫“颜色样本”的按钮 这是他的代码:
//显示windows默认颜色样本
void makeColor()
{
Bitmap bmp = new Bitmap(1, 1, PixelFormat.Format4bppIndexed);
Color[] colors = bmp.Palette.Entries;
Graphics gph = Graphics.FromHwnd(this.Handle);
int j = 0;
for (int i = 0; i < 4; i++)
{
for (int k = 0; k < 4; k++)
{
gph.FillRectangle(new SolidBrush(colors[j]), new Rectangle(new Point(k * 100,
i * 100), new Size(100, 100)));
gph.DrawString(colors[j].ToArgb().ToString("X").Substring(2),
new Font(new FontFamily("黑体"), 15),
Brushes.Lavender, new Point(k * 100, i * 100));
j++;
}
}
bmp.Dispose();
}
颜色表有了(就是上面代码里抓出来的颜色表),然后是怎样让每个像素的颜色去根据调色板匹配最近似的颜色呢 http://dev.gameres.com/Program/Visual/Other/256color.htm 这是一个C++的实现 不过我没看懂 - -! 用了另外一种傻瓜的方式去实现,添加一个名为“调色板算法”的按钮 这是他的代码:
//256色调色板的匹配处理 http://dev.gameres.com/Program/Visual/Other/256color.htm
//没看懂不过我实现了他的另外一个效率很低的算法
//调色板匹配/降低颜色深度的 算法 效率有点低 原理是对的
//出来的结果跟画图板另存为16色 是一模一样的 要256色同理
void tranTo16()
{
Bitmap img = (Bitmap)Bitmap.FromFile("mm.bmp");
//准备调色板 这个是从画图板新建16色位图文件里抓出来的
Color[] colors = new Color[16];
colors[0] = Color.FromArgb(0x00, 0x00, 0x00);
colors[1] = Color.FromArgb(0x80, 0x00, 0x00);
colors[2] = Color.FromArgb(0x00, 0x80, 0x00);
colors[3] = Color.FromArgb(0x80, 0x80, 0x00);
colors[4] = Color.FromArgb(0x00, 0x00, 0x80);
colors[5] = Color.FromArgb(0x80, 0x00, 0x80);
colors[6] = Color.FromArgb(0x00, 0x80, 0x80);
colors[7] = Color.FromArgb(0x80, 0x80, 0x80);
colors[8] = Color.FromArgb(0xc0, 0xc0, 0xc0);
colors[9] = Color.FromArgb(0xff, 0x00, 0x00);
colors[10] = Color.FromArgb(0x00, 0xff, 0x00);
colors[11] = Color.FromArgb(0xff, 0xff, 0x00);
colors[12] = Color.FromArgb(0x00, 0x00, 0xff);
colors[13] = Color.FromArgb(0xff, 0x00, 0xff);
colors[14] = Color.FromArgb(0x00, 0xff, 0xff);
colors[15] = Color.FromArgb(0xff, 0xff, 0xff);
Graphics gph = Graphics.FromHwnd(this.Handle);
int tmp;
for (int i = 0; i < img.Height; i++)
{
for (int j = 0; j < img.Width; j++)
{
tmp = 255 * 3;
Color tmpCol = Color.FromArgb(255, 255, 255);
for (int k = 0; k < colors.Length; k++)
{
Color src = colors[k];
Color des = img.GetPixel(j, i);
//原理就是检查每个像素的rgb跟调色板中的比较 选中差值最小的那个,那么就一定是"最相近"的颜色了
int val = Math.Abs(des.R - src.R) + Math.Abs(des.G - src.G) + Math.Abs(des.B - src.B);
if (val < tmp)
{
tmp = val;
tmpCol = src;
}
}
img.SetPixel(j, i, tmpCol);
}
}
gph.DrawImage(img, new Point(0, 0));
gph.Dispose();
img.Dispose();
}
怎么样试下吧 用画图板转存为16色位图 看下是不是跟他是一模一样的,说明咱的方法是对的 要的就是这个效果。 看下这个图像的画面真的惨不忍睹啊
有时候win95下有些很神奇的程序他们用4位的模式竟然也可以显示出不算太差的图像(抖动算法 这个俺还不会 ╮(╯﹏╰)╭)
有没有既稍微保留画面质量又降低数据量的方法呢。有啊 改调色板不就得了嘛 让调色板的颜色尽量跟画面整体匹配,上面说了取得调色板跟调色板里的颜色都很简单Image.Palette 但是试了就知道通过给调色板的颜色赋值 或者直接更改调色板根本都不起作用的,如果有哪位哥们儿知道 告诉我下。参考上一篇第一个例子 我们知道 这个得用非常手段 根据格式直接对bmp内存数据进行操作,本文只讨论方法这里就不写调色板操作的代码了,通过一段偷懒的代码见证他的可行性。把对colors数组赋值的16行改成这样:
Random rdm = new Random();
for (int i = 0; i < colors.Length; i++)
colors[i] = img.GetPixel(rdm.Next(0,
img.Width - 1), rdm.Next(0, img.Height - 1));
这个图像像是16色的图像吗 不会吧再怎么看着也比16色好很多啊,怎么样调色板的神奇之处 瞬间图像质量就有很大提升吧。你可以写更好的算法对图像的色调进行分析生成更智能的调色板 以前的老游戏由于发色数有限 调色板应用非常广泛。
该结尾了我们来实际写个24位真彩色bmp图像转8位256色的例子,添加一个叫“降低颜色深度”的按钮 这是对应的代码:
void tranTo256()
{
Bitmap srcImg = new Bitmap("mm.bmp");
Bitmap desImg = new Bitmap(srcImg.Width, srcImg.Height, PixelFormat.Format8bppIndexed);
MemoryStream desImgData = new MemoryStream();
desImg.Save(desImgData, ImageFormat.Bmp);
Color[] pal = desImg.Palette.Entries;
BitmapData data = srcImg.LockBits(new Rectangle(0, 0, srcImg.Width, srcImg.Height),
ImageLockMode.ReadWrite, srcImg.PixelFormat);
unsafe
{
byte* ptr = (byte*)data.Scan0;
int tmp;
//扫描的时候是由上到下,存储的时候是由下到上
for (int i =0 ; i <srcImg.Height ; i++)//(int i = srcImg.Height - 1; i > 0; i--)
{
int offSet = data.Stride * i;
for (int j = 0; j < srcImg.Width * 3; j += 3)
{
tmp = 255 * 3;
byte palIndx = 0;
for (int k = 0; k < pal.Length; k++)
{
Color src = pal[k];
Color des = Color.FromArgb(ptr[offSet + j + 2], ptr[offSet + j + 1], ptr[offSet + j]);
int val = Math.Abs(des.R - src.R) + Math.Abs(des.G - src.G) + Math.Abs(des.B - src.B);
if (val < tmp)
{
tmp = val;
palIndx = (byte)k;
}
}
desImgData.WriteByte(palIndx);
}
}
}
desImg = (Bitmap)Bitmap.FromStream(desImgData);
srcImg.UnlockBits(data);
Graphics.FromHwnd(this.Handle).DrawImage(desImg, new Point(0, 0));
desImg.Save("gs.bmp");
desImgData.Close();
srcImg.Dispose();
desImg.Dispose();
}
貌似什么都对的就是出不来呢 一个纯黑背景的图片,因为写数据之前游标没有到达指定位置 在unsafe前面加上这句就ok了:
desImgData.Seek(54 + 256 * 4, SeekOrigin.Begin);//数据开始位置,参考位图文件格式说明
看下效果吧:
哇 怎么回事倒了 怎么会倒了呢 看见咱代码里注释的提示了没 //扫描的时候是由上到下,存储的时候是由下到上
原理不多说 ,把外层for循环圆括号里部分用注释的那段替换就行了
这就没问题了 真的么 真的就没问题了吗
用画图板打开bin目录那个叫"mm.bmp"图像 ,然后 单击“图像”菜单->属性 现在宽度是不是400 ,请改成399 按ctr+s保存
然后再运行程序:
为什么400可以399就不行呢。又涉及4倍字节数这个扯淡的问题。
我们像素宽是400对吧 8位256色 一个像素一个字节 对吧 正好400字节 能被4整除
如果399像素 就需要补齐一个字节 在最外层循环加上这句就ok了:
desImgData.WriteByte(0x00);
不行 咱得让他更智能点 把刚刚那段代码替换成这段:
int fill = ((srcImg.Width * 8 + 31) / 32 * 4) - srcImg.Width;
if (fill > 0)
{
byte[] fills = new byte[fill];
desImgData.Write(fills, 0, fills.Length);
}
好了折腾够了 大功告成,对应目录已经生成了一个8位名字“gs.bmp”的位图。 有些部分没完善只起到抛砖引玉的作用 见谅 自己去搞: