现在的位置: 首页 > 综合 > 正文

降低颜色深度及调色板处理

2012年07月21日 ⁄ 综合 ⁄ 共 4734字 ⁄ 字号 评论关闭

上一次讲到哪了,说了下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”的位图。 有些部分没完善只起到抛砖引玉的作用 见谅 自己去搞:

示例文件及代码

抱歉!评论已关闭.