一个菜鸟的图像处理入门

本来就是入门的 那就先说下gdi 跟 bmp 这些东西吧。

1 gdi跟bmp


vc里的CDC 也就是设备上下文 相当于c#里的graphics ,也有lineTo等方法。
其实我们在c#中使用graphics的时候就已经在使用gdi+了我们却浑然不觉
那么gdi到底在哪里呢 试着在c盘搜索gdiplus或者gdi32名字的文件 你应该会找到 就像这个
直接删除应该删不掉 不过你可以给他改个名字 别改了自己都搞忘了O(∩_∩)O哈!。
然后你随便运行个程序比如QQ 肿么样
Initialization failure:0x0000000E
使用windows自带的图片查看器
加载 c:\windows\system32\shimgvw.dll时出错 系统找不到指定的文件。
所以说windows下到处都是gdi
不要以为bitmap是一种图像格式 就像jpg gif 一样 实际上他们是两个完全不同的概念。
在vc++里叫cbitmap 也就是对应的gdi数据模型 等同于c#里的bitmap
可以这样说.bmp的位图文件是gdi的文件表现形式。 位图文件不进行图像压缩算法操作直接存储像素矩阵信息所以文件体积非常大
jpg文件体积非常小为什么 jpg实际上它是按照完全不同的算法跟理念来存储图像的 都知道人的视觉效应
主要体现在两个方面 色彩的明暗度 色彩的饱和度 也就是色调(说俗点就是赤橙黄绿青蓝紫 )并且肉眼根本达不到每个像素那么大的分辨能力
jpg就是按照这种方式来存储的

2 位图文件格式


那么就先说下bmp文件格式吧,本人不是那种长篇大论型的 不说废话。
这个压缩包里工程的bin目录有一个叫bmpTestImg.bmp两色的位图文件。以16进制编辑器打开对照下图看 :
这里是下载链接
一个菜鸟的图像处理入门_第1张图片
vc++里有定义好的bitmapheader 用来表示位图头信息 在C#里没有。
其实没多大关系的这只是一种数据组织方式
如果你愿意也可以定义这么一个结构。
gdi与设备无关 但是他并不代表跟设备没有任何关联 计算机之间传递的是图片文件或者图像数据 并不是gdi对象。
微软帮我们搞了这一层东西,就是说只要是接入windows的设备我们都可以通过
gdi在那个设备上显示 输出东西 而不用关系设备本身 ,可以说整个windows提供给我们的就是gdi 所有窗体
等等都是gdi绘制的,比如说做啥xx编程的时候要直接操纵显卡 实际上直接操纵的方式速度更快但是没有必要

所谓的24位真彩色 mspaint画图新建图像默认存储 就是24位真彩色 这并不是什么高深技术因为 一个像素的颜色用3个八位
来表示就是24位真彩色
真彩图像是说他具有显示 256x256x256种颜色的能力
还有就是c#里默认新建的bitmap对象就是24位真彩的 并且graphic提供的函数不能操作非真彩色的位图

3 水平不咋滴,还是来敲点代码吧,\(^o^)/~

先卖个关子哈 上面你下载的示例图片你看到东西了吗 还是黑乎乎一片 嘿嘿。如果你看到了那你见鬼了 还是赶快拜拜春哥吧
最近在研究那啥dicom 也学会拽文了 嘿嘿
是否可以这么描述:bmp是一种约定俗成的有规律的数据组织方式 不论他在内存中 在文件中 他跟特定编程语言无关 跟平台无关
bmp格式简而言之一句话 前54字节存储文件头信息最主要就是图像位数跟宽度高度,从54位开始有调色板则是调色板信息 无调色板则是像素数据。
由于本文不是专门探讨bmp文件格式 详细请参见bmp格式
好下面我们就来读取这种有规律的数据: 写第一个按钮事件的代码

void bmpRead()//读取bmp文件格式
{
    Image bmp = (Bitmap)Image.FromFile("bmpTestImg.bmp");

    MemoryStream bmpData = new MemoryStream();
    bmp.Save(bmpData, ImageFormat.Bmp);
    BinaryReader br = new BinaryReader(bmpData);
    //为什么要偏移18个字节 因为bmp格式"龟腚"在18字节那个地方开始用32位整型存储图像的宽度跟高度
    bmpData.Seek(18, SeekOrigin.Begin);
    int width = br.ReadInt32();
    int height = br.ReadInt32();
    MessageBox.Show(string.Format("宽{0},高{1}", width, height));
    //第11个字节处储存数据字节的起始位置
    bmpData.Seek(10, SeekOrigin.Begin);
    int dataStart = br.ReadInt32();

    byte[] datas = new byte[width * height];
    int indx = 0;
    bmpData.Seek(dataStart, SeekOrigin.Begin);
    //注意咯 这是调色板开始的位置 更改调色板将会让"看不见"的图像显示出来
    bmpData.Seek(54, SeekOrigin.Begin);
    Random rd = new Random();
    bmpData.Write(new byte[] { (byte)rd.Next(0, 255), (byte)rd.Next(0, 255), 
        (byte)rd.Next(0, 255), 0 }, 0, 4);
    bmpData.Write(new byte[] { (byte)rd.Next(0, 255), (byte)rd.Next(0, 255), 
        (byte)rd.Next(0, 255), 0 }, 0, 3);

    Image newbmp = Bitmap.FromStream(bmpData);
    Graphics.FromHwnd(this.Handle).DrawImage(newbmp, new Point(0, 0));
    bmpData.Close();
    br.Close();
}

上面的代码很简单滴 (⊙o⊙)哦 都看得懂吧 别忘了要在执行文件同级目录放上偶的图片哦 嘿嘿。
这个适合用来给girlfriend表白啊啥的O(∩_∩)O哈!
有几个需要说明的地方
bmp文件的两色 并非一定得是黑白 对吧  可以是红色绿色, 也可以是两种相同的色儿 对吧
为什么宽度要在第19字节的位置开始存储 没有为什么 这是bmp格式的“龟腚”对吧 要问去问盖茨大叔

对于“流”的操作 seek到前面去了 再进行write操作 是否就把对应位置的数据“挤”到后面去了呢?
NO 数据流是一种游标 “覆盖”型的操作 长度会自动标识到游标到过最远的地方 文件流内存流都一样
所以说想要做数据插入啊 文件合并啊之类的东东的话得弄两个数据流对象哈 互相倒腾数据 这样才能达到目的。
又扯远了哈 打住。

不是说读取数据吗 就是读取像素值数据啊,现在开始 
既然是读取像素值,咱得一行一行的读啊 就像扫描一样的。实际上他就是以这种方式存储的哈 只不过稍微有点不一样
那就是图像数据每行以四倍字节为基数不足以0补齐 乃明白了木有 。
比如说这一个扫描行有3个像素 那么就是9字节 ,4字节的倍数那么他必须要有12字节 那么剩下的3字节全是0。
比如说这一个扫描行有20个像素 那么就是60字节 ,4字节的倍数那么他必须要有60字节 因为60/4正好除净。

先来说下这个破公式 ((width * 24 + 31) / 32 * 4) 不知道是哪个头脑发热的人想出来的 ,注意这里的32是指32位 即4字节。
实际上我只想说两个字非常扯淡 一定是很深入的掌握了数据长度运算的本质, 一句把我上面那n多句都代替了
width是图像宽度 24代表每个像素位数。 计算出实际字节数 先假设他会超出一位 补齐31位 然后通过整型数据相除的性质 除以4字节得到4字节的倍数
注意最终得到的是扫描行的字节数
就这样从图像左下角第一个点开始 一行一行从左至右的往上扫描
然后是bmp图像素的存储方式是BGR的顺序哈 而不是通常的RGB 哦 别搞错了,
以前很菜的时候用SetPixel()处理像素 被人骂惨了 现在俺依然来写个setPix() 嘿嘿 第二个按钮的代码:

void setPix()//
{
    FileStream bmpData = File.Open("mm.bmp", FileMode.Open); BinaryReader br = new BinaryReader(bmpData);
    bmpData.Seek(10, SeekOrigin.Begin);int bmpDataStart = br.ReadInt32();
    bmpData.Seek(18, SeekOrigin.Begin);int width = br.ReadInt32();int height = br.ReadInt32();

    Bitmap newBmp = (Bitmap)new Bitmap(width, height, PixelFormat.Format24bppRgb);
    MemoryStream newBmpData = new MemoryStream();
    newBmp.Save(newBmpData, ImageFormat.Bmp);BinaryReader br2 = new BinaryReader(newBmpData);
    newBmpData.Seek(10, SeekOrigin.Begin); int newBmpDataStart = br2.ReadInt32();
    newBmpData.Seek(newBmpDataStart, SeekOrigin.Begin);

    for (int i = 0; i < height; i++)
    {
        bmpData.Seek(((width * 24 + 31) / 32 * 4) * i + bmpDataStart, SeekOrigin.Begin);
        newBmpData.Seek(((width * 24 + 31) / 32 * 4) * i + newBmpDataStart, SeekOrigin.Begin);
        for (int j = 0; j < width; j++)
        {
            //注意bmp的像素值是按照bgr的顺序存储的哦
            byte[] data = new byte[3];
            bmpData.Read(data, 0, 3);
            newBmpData.Write(new byte[] { data[2], data[1], data[0] }, 0, 3);                   
        }
        //下面的填充值要不要都可以 
        int fill = ((width * 24 + 31) / 32 * 4) - width * 3;
        if (fill > 0)
        {
            byte[] fills = new byte[] { 0, 0, 0 };
            newBmpData.Write(fills, 0, fills.Length);
        }
    }
    newBmpData.Flush();
    newBmp = (Bitmap)Bitmap.FromStream(newBmpData);
    Graphics.FromHwnd(this.Handle).DrawImage(newBmp, new Point(0, 0));

    bmpData.Close(); newBmpData.Close();
    br.Close(); br2.Close();
}

如果你把for (int i = 0; i < height; i++)改成 for (int i = 0; i < height/2; i++) 可以看下效果 可以证明在文件中是按照图像从左至右往上 的方式存储的
通过以上可以看出任何环境下他都是按照同种规律存储存储的,就像dicom 只要遵循这种规律就能通过这种格式实现数据共享。

都说lockBitmap的方式是最快的 ,确实是最快的哈 因为他是使用指针的方式
下面是把一个图像转成灰度图 你看 不但代码少了很多 并且还不用费尽心思去确定每一个扫描行的索引 你看 刷的一下 就出来了 嘿嘿
注意有unsafe代码 在项目->属性 勾选“允许不安全代码” :

void lockPix()
{
    Bitmap bmp = (Bitmap)Image.FromFile("mm.bmp");
    BitmapData datas = bmp.LockBits(new Rectangle(0, 0, bmp.Width, bmp.Height), 
        System.Drawing.Imaging.ImageLockMode.ReadWrite, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
    unsafe
    {
        byte* p = (byte*)datas.Scan0;
        int indx = 0;
        for (int i = 0; i < bmp.Height/2; i++)
        {
            for (int j = 0; j < bmp.Width; j++)
            {
                byte b, g, r; b = p[indx + 1]; g = p[indx + 2]; r = p[indx + 3];
                //byte lightLv = (byte)(r * 0.3 + g * 0.59 + b * 0.11);
                byte gray = (byte)((r + g + b) / 3);
                p[indx++] = gray; p[indx++] = gray; p[indx++] = gray;
            }
        }
    }

    bmp.UnlockBits(datas);
    Graphics.FromHwnd(this.Handle).DrawImage(bmp, new Point(0, 0));
    bmp.Dispose();
}

灰度图 哎呀 跟你说得又俗又土点就是对每个像素 rgb三个值加起来除以3  不想跟你讲那些我自己都不怎么明白的东西
但是还是不得不跟你说下所谓的yuv表示方式  y代表明度 说到这个又得要讲下矩阵乘法 真麻烦ya。

这个什么意思呢,先说说矩阵乘法吧
比如你商店里有帽子鞋子 袜子 单价分别表示为:
[25] [80] [15]
然后今天帽子卖了3件 鞋子卖了1件 袜子卖了两件,可表示为:
[3]
[1]
[2]
然后今天的收入呢=25x3+80x1+15x2 总共185 用矩阵表示为[185]
我第二天帽子卖了1件 鞋子卖了两件 袜子卖了3件,那么这两天的销售可表示为:
[3] [1]
[1] [2]
[2] [3]
那么这两天总共的收入呢=(25x3+80x1+15x2)+(25x1+80x2+15x3) 总共185+230=415 用矩阵表示为[185][230]
没错 你看到的这就是矩阵乘法 不想讲什么线性代数 什么的那么高深的理论

比如上面RGB转转YUV公式的3行3列 乘 3行1列 乘出来是 3行1列 ,
规律就是第一个矩阵的列跟第二个矩阵的行一致,得到一个首行尾列数的二维矩阵。
注意一定是得到一个首行尾列数的二维矩阵,如果不符合这个标准 那么运算是无意义的。
运算规则:要从结果矩阵的往前推,先确定结果矩阵元素所在行数列数 C(i,j)。然后把A矩阵对应行 与B矩阵对应列
相同索引位置的数两两相乘 然后加起来 即为C(i,j)的值。 其实还是挺简单的。
这里有关于矩阵乘法 运算规则的简介:http://www2.edu-edu.com.cn/lesson_crs78/self/j_0022/soft/ch0605.html

如果rgb值分别是{115,20,65} 那么转换成yuv表示应该是
y=115x0.299+20x0.587+65x0.114
u=115x-0.148+20x-0.289+65x0.437
v=115x0.615+20x-0.515+65x-0.1
貌似很难理解 因为这个跟前面那个卖东西的又不一样了可以换个角度看 。
把第二个矩阵往左“倒下来” 就是说让他的行跟第一个矩阵的列 对齐 是不是感觉好多了O(∩_∩)O哈!
整点复杂的 那再来随便整个吧
[2] [4]      [-1] [6]
[1] [0]      [3 ] [5]
结果是多少
2x-1+4x3      2x6+4x5
1x-1+0x3      1x6+0x5
最终结果
[10]      [32]
[-1]      [6]
也可以把它分解为单行单列的来看 就简单多了哈


有种很特殊的矩阵 有点像对角线
任何跟他相乘的矩阵都等于那个矩阵本身 有点像 “任何数乘以1 都等于那个数本身”
[1][0]
[0][1]

看吧矩阵乘法就是这么神奇的东东,通过矩阵乘法还可以进行角度旋转 缩放等等 
这个是很高深的研究课题了O(∩_∩)O哈! 这里就不讨论了

终于说完了 俺喝口水了先。也不知讲清楚了没 下面是各种矩阵乘法的示例代码 关于为什么是5x5的矩阵这个可以看下msdn
知识学无止境
第三个按钮:

void matrixColor()
{
    Bitmap bmp = (Bitmap)Image.FromFile("mm.bmp");
    ImageAttributes ia = new ImageAttributes();
    //灰阶
    //float[][] colorMatrix ={
    //                              new float[]{0.299f,0.299f, 0.299f, 0,  0},
    //                              new float[]{0.587f,0.587f, 0.587f, 0,  0},
    //                              new float[]{0.114f,0.114f, 0.114f, 0,  0},
    //                              new float[]{0,     0,      0,      1,  0},
    //                              new float[]{0,     0,      0,      0,  1}
    //                       };    

    //灰阶
    //float[][] colorMatrix ={
    //                              new float[]{0.3f,  0.3f,   0.3f,   0,  0},
    //                              new float[]{0.3f,  0.3f,   0.3f,   0,  0},
    //                              new float[]{0.3f,  0.3f,   0.3f,   0,  0},
    //                              new float[]{0,     0,      0,      1,  0},
    //                              new float[]{0,     0,      0,      0,  1}
    //                       };    

    //反色
    //float[][] colorMatrix ={
    //                              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,      1,  0},
    //                              new float[]{1,     1,      1,      0,  1}
    //                       };    

    //亮度
    float[][] colorMatrix ={
                                  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,      1,  0},
                                  new float[]{l,     l,      l,      0,  1}};
    l -= 0.1f;

    ColorMatrix cm = new ColorMatrix(colorMatrix);
    ia.SetColorMatrix(cm, ColorMatrixFlag.Default, ColorAdjustType.Bitmap);
    Graphics.FromHwnd(this.Handle).DrawImage(bmp, new Rectangle(0, 0, bmp.Width, bmp.Height),
        0, 0, bmp.Width, bmp.Height, GraphicsUnit.Pixel, ia);

}
float l = 0.5f;

 rgb为3个能量值 我们看到屏幕上花花绿绿 颜色是因为三个能量值产生差异化 说俗点就是三个值的比例不一样
如果三个值一样的话那就跟电灯泡无异了 就是纯亮度表示 即我们常说的灰度图。
来写个手工滴 很山寨滴 效率很低滴 更改亮度的函数

void light(ref int r, ref int g, ref int b)
{
    //计算后的平均值
    //增加亮度
    float gray= (r + g + b) + level * 90 > 255 * 3 ? 255 * 3 : (r + g + b) + level * 90;
    //降低亮度
    //float gray = (r + g + b) - level * 90 < 0 ? 0 : (r + g + b) - level * 90; ;
    float percentR = (float)r / (r + g + b), percentG = (float)g / (r + g + b), percentB = (float)b / (r + g + b);

    r = (int)(gray * percentR > 255 ? 255 : gray * percentR);
    g = (int)(gray * percentG > 255 ? 255 : gray * percentG);
    b = (int)(gray * percentB > 255 ? 255 : gray * percentB);

    float ren = gray - (r + g + b);
    if (ren >= 3)
    {
        r = (r + (int)ren) > 255 ? 255 : (r + (int)ren);
        g = (g + (int)ren) > 255 ? 255 : (g + (int)ren);
        b = (b + (int)ren) > 255 ? 255 : (b + (int)ren);
    }

}
int level = 0;

 其实呢也远可以不必这样 直接rgb分别乘以1.2 或者1.1之类的就可以了 只不过颜色会失真
示例文件及代码

 好了终于写完啦 好累ya

完了 ( ⊙ o ⊙ ) 本来就很菜 这点破秘密全被你们晓得了 以后出去俺还杂混呐

当然作为一个商业化的软件 代码的容错也是很重要的 你看acdsee 你把文件数据部分删除一些他照样能够显示 当然这些都是很简单的哈。

 

你可能感兴趣的:(图像处理)