新聞中心

EEPW首頁 > 嵌入式系統(tǒng) > 牛人業(yè)話 > 一個(gè)菜鳥的圖像處理入門

一個(gè)菜鳥的圖像處理入門

作者: 時(shí)間:2017-04-05 來源:網(wǎng)絡(luò) 收藏

  本來就是入門的 那就先說下gdi 跟  這些東西吧。

本文引用地址:http://m.butianyuan.cn/article/201704/346204.htm

  1 gdi跟

  vc里的CDC 也就是設(shè)備上下文 相當(dāng)于c#里的graphics ,也有l(wèi)ineTo等方法。

  其實(shí)我們?cè)赾#中使用graphics的時(shí)候就已經(jīng)在使用gdi+了我們卻渾然不覺

  那么gdi到底在哪里呢 試著在c盤搜索gdiplus或者gdi32名字的文件 你應(yīng)該會(huì)找到 就像這個(gè)  

 

  直接刪除應(yīng)該刪不掉 不過你可以給他改個(gè)名字 別改了自己都搞忘了O(∩_∩)O哈!。

  然后你隨便運(yùn)行個(gè)程序比如QQ 腫么樣

  Initialization failure:0x0000000E

  使用windows自帶的圖片查看器

  加載 c:windowssystem32shimgvw.dll時(shí)出錯(cuò) 系統(tǒng)找不到指定的文件。

  所以說windows下到處都是gdi

  不要以為bitmap是一種圖像格式 就像jpg gif 一樣 實(shí)際上他們是兩個(gè)完全不同的概念。

  在vc++里叫cbitmap 也就是對(duì)應(yīng)的gdi數(shù)據(jù)模型 等同于c#里的bitmap

  可以這樣說.的位圖文件是gdi的文件表現(xiàn)形式。 位圖文件不進(jìn)行圖像壓縮算法操作直接存儲(chǔ)像素矩陣信息所以文件體積非常大

  jpg文件體積非常小為什么 jpg實(shí)際上它是按照完全不同的算法跟理念來存儲(chǔ)圖像的 都知道人的視覺效應(yīng)

  主要體現(xiàn)在兩個(gè)方面 色彩的明暗度 色彩的飽和度 也就是色調(diào)(說俗點(diǎn)就是赤橙黃綠青藍(lán)紫 )并且肉眼根本達(dá)不到每個(gè)像素那么大的分辨能力

  jpg就是按照這種方式來存儲(chǔ)的

  2 位圖文件格式

  那么就先說下bmp文件格式吧,本人不是那種長篇大論型的 不說廢話。

  這個(gè)壓縮包里工程的bin目錄有一個(gè)叫bmpTestImg.bmp兩色的位圖文件。以16進(jìn)制編輯器打開對(duì)照下圖看 :

  這里是下載鏈接

 

  vc++里有定義好的bitmapheader 用來表示位圖頭信息 在C#里沒有。

  其實(shí)沒多大關(guān)系的這只是一種數(shù)據(jù)組織方式

  如果你愿意也可以定義這么一個(gè)結(jié)構(gòu)。

  gdi與設(shè)備無關(guān) 但是他并不代表跟設(shè)備沒有任何關(guān)聯(lián) 計(jì)算機(jī)之間傳遞的是圖片文件或者圖像數(shù)據(jù) 并不是gdi對(duì)象。

  微軟幫我們搞了這一層?xùn)|西,就是說只要是接入windows的設(shè)備我們都可以通過

  gdi在那個(gè)設(shè)備上顯示 輸出東西 而不用關(guān)系設(shè)備本身 ,可以說整個(gè)windows提供給我們的就是gdi 所有窗體

  等等都是gdi繪制的,比如說做啥xx編程的時(shí)候要直接操縱顯卡 實(shí)際上直接操縱的方式速度更快但是沒有必要

  所謂的24位真彩色 mspaint畫圖新建圖像默認(rèn)存儲(chǔ) 就是24位真彩色 這并不是什么高深技術(shù)因?yàn)?nbsp;一個(gè)像素的顏色用3個(gè)八位

  來表示就是24位真彩色

  真彩圖像是說他具有顯示 256x256x256種顏色的能力

  還有就是c#里默認(rèn)新建的bitmap對(duì)象就是24位真彩的 并且graphic提供的函數(shù)不能操作非真彩色的位圖

  3 水平不咋滴,還是來敲點(diǎn)代碼吧,(^o^)/~

  先賣個(gè)關(guān)子哈 上面你下載的示例圖片你看到東西了嗎 還是黑乎乎一片 嘿嘿。如果你看到了那你見鬼了 還是趕快拜拜春哥吧

  最近在研究那啥dicom 也學(xué)會(huì)拽文了 嘿嘿

  是否可以這么描述:bmp是一種約定俗成的有規(guī)律的數(shù)據(jù)組織方式 不論他在內(nèi)存中 在文件中 他跟特定編程語言無關(guān) 跟平臺(tái)無關(guān)

  bmp格式簡而言之一句話 前54字節(jié)存儲(chǔ)文件頭信息最主要就是圖像位數(shù)跟寬度高度,從54位開始有調(diào)色板則是調(diào)色板信息 無調(diào)色板則是像素?cái)?shù)據(jù)。

  由于本文不是專門探討bmp文件格式 詳細(xì)請(qǐng)參見bmp格式

  好下面我們就來讀取這種有規(guī)律的數(shù)據(jù): 寫第一個(gè)按鈕事件的代碼

  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個(gè)字節(jié) 因?yàn)閎mp格式"龜腚"在18字節(jié)那個(gè)地方開始用32位整型存儲(chǔ)圖像的寬度跟高度

  bmpData.Seek(18, SeekOrigin.Begin);

  int width = br.ReadInt32();

  int height = br.ReadInt32();

  MessageBox.Show(string.Format("寬{0},高{1}", width, height));

  //第11個(gè)字節(jié)處儲(chǔ)存數(shù)據(jù)字節(jié)的起始位置

  bmpData.Seek(10, SeekOrigin.Begin);

  int dataStart = br.ReadInt32();

  byte[] datas = new byte[width * height];

  int indx = 0;

  bmpData.Seek(dataStart, SeekOrigin.Begin);

  //注意咯 這是調(diào)色板開始的位置 更改調(diào)色板將會(huì)讓"看不見"的圖像顯示出來

  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⊙)哦 都看得懂吧 別忘了要在執(zhí)行文件同級(jí)目錄放上偶的圖片哦 嘿嘿。

  這個(gè)適合用來給girlfriend表白啊啥的O(∩_∩)O哈!

  有幾個(gè)需要說明的地方

  bmp文件的兩色 并非一定得是黑白 對(duì)吧 可以是紅色綠色, 也可以是兩種相同的色兒 對(duì)吧

  為什么寬度要在第19字節(jié)的位置開始存儲(chǔ) 沒有為什么 這是bmp格式的“龜腚”對(duì)吧 要問去問蓋茨大叔

  對(duì)于“流”的操作 seek到前面去了 再進(jìn)行write操作 是否就把對(duì)應(yīng)位置的數(shù)據(jù)“擠”到后面去了呢?

  NO 數(shù)據(jù)流是一種游標(biāo) “覆蓋”型的操作 長度會(huì)自動(dòng)標(biāo)識(shí)到游標(biāo)到過最遠(yuǎn)的地方 文件流內(nèi)存流都一樣

  所以說想要做數(shù)據(jù)插入啊 文件合并啊之類的東東的話得弄兩個(gè)數(shù)據(jù)流對(duì)象哈 互相倒騰數(shù)據(jù) 這樣才能達(dá)到目的。

  又扯遠(yuǎn)了哈 打住。

  不是說讀取數(shù)據(jù)嗎 就是讀取像素值數(shù)據(jù)啊,現(xiàn)在開始

  既然是讀取像素值,咱得一行一行的讀啊 就像掃描一樣的。實(shí)際上他就是以這種方式存儲(chǔ)的哈 只不過稍微有點(diǎn)不一樣

  那就是圖像數(shù)據(jù)每行以四倍字節(jié)為基數(shù)不足以0補(bǔ)齊 乃明白了木有 。

  比如說這一個(gè)掃描行有3個(gè)像素 那么就是9字節(jié) ,4字節(jié)的倍數(shù)那么他必須要有12字節(jié) 那么剩下的3字節(jié)全是0。

  比如說這一個(gè)掃描行有20個(gè)像素 那么就是60字節(jié) ,4字節(jié)的倍數(shù)那么他必須要有60字節(jié) 因?yàn)?0/4正好除凈。

  先來說下這個(gè)破公式 ((width * 24 + 31) / 32 * 4) 不知道是哪個(gè)頭腦發(fā)熱的人想出來的 ,注意這里的32是指32位 即4字節(jié)。

  實(shí)際上我只想說兩個(gè)字非常扯淡 一定是很深入的掌握了數(shù)據(jù)長度運(yùn)算的本質(zhì), 一句把我上面那n多句都代替了

  width是圖像寬度 24代表每個(gè)像素位數(shù)。 計(jì)算出實(shí)際字節(jié)數(shù) 先假設(shè)他會(huì)超出一位 補(bǔ)齊31位 然后通過整型數(shù)據(jù)相除的性質(zhì) 除以4字節(jié)得到4字節(jié)的倍數(shù)

  注意最終得到的是掃描行的字節(jié)數(shù)

  就這樣從圖像左下角第一個(gè)點(diǎn)開始 一行一行從左至右的往上掃描

  然后是bmp圖像素的存儲(chǔ)方式是BGR的順序哈 而不是通常的RGB 哦 別搞錯(cuò)了,

  以前很菜的時(shí)候用SetPixel()處理像素 被人罵慘了 現(xiàn)在俺依然來寫個(gè)setPix() 嘿嘿 第二個(gè)按鈕的代碼:

  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的順序存儲(chǔ)的哦

  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++) 可以看下效果 可以證明在文件中是按照?qǐng)D像從左至右往上 的方式存儲(chǔ)的

  通過以上可以看出任何環(huán)境下他都是按照同種規(guī)律存儲(chǔ)存儲(chǔ)的,就像dicom 只要遵循這種規(guī)律就能通過這種格式實(shí)現(xiàn)數(shù)據(jù)共享。

  都說lockBitmap的方式是最快的 ,確實(shí)是最快的哈 因?yàn)樗鞘褂弥羔樀姆绞?/p>

  下面是把一個(gè)圖像轉(zhuǎn)成灰度圖 你看 不但代碼少了很多 并且還不用費(fèi)盡心思去確定每一個(gè)掃描行的索引 你看 刷的一下 就出來了 嘿嘿

  注意有unsafe代碼 在項(xiàng)目->屬性 勾選“允許不安全代碼” :

  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();

  }

  灰度圖 哎呀 跟你說得又俗又土點(diǎn)就是對(duì)每個(gè)像素 rgb三個(gè)值加起來除以3 不想跟你講那些我自己都不怎么明白的東西

  但是還是不得不跟你說下所謂的yuv表示方式 y代表明度 說到這個(gè)又得要講下矩陣乘法 真麻煩ya。

    

 

  這個(gè)什么意思呢,先說說矩陣乘法吧

  比如你商店里有帽子鞋子 襪子 單價(jià)分別表示為:

  [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]

  沒錯(cuò) 你看到的這就是矩陣乘法 不想講什么線性代數(shù) 什么的那么高深的理論

  比如上面RGB轉(zhuǎn)轉(zhuǎn)YUV公式的3行3列 乘 3行1列 乘出來是 3行1列 ,

  規(guī)律就是第一個(gè)矩陣的列跟第二個(gè)矩陣的行一致,得到一個(gè)首行尾列數(shù)的二維矩陣。

  注意一定是得到一個(gè)首行尾列數(shù)的二維矩陣,如果不符合這個(gè)標(biāo)準(zhǔn) 那么運(yùn)算是無意義的。

  運(yùn)算規(guī)則:要從結(jié)果矩陣的往前推,先確定結(jié)果矩陣元素所在行數(shù)列數(shù) C(i,j)。然后把A矩陣對(duì)應(yīng)行 與B矩陣對(duì)應(yīng)列

  相同索引位置的數(shù)兩兩相乘 然后加起來 即為C(i,j)的值。 其實(shí)還是挺簡單的。

  這里有關(guān)于矩陣乘法 運(yùn)算規(guī)則的簡介:http://www2.edu-edu.com.cn/lesson_crs78/self/j_0022/soft/ch0605.html

  如果rgb值分別是{115,20,65} 那么轉(zhuǎn)換成yuv表示應(yīng)該是

  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

  貌似很難理解 因?yàn)檫@個(gè)跟前面那個(gè)賣東西的又不一樣了可以換個(gè)角度看 。

  把第二個(gè)矩陣往左“倒下來” 就是說讓他的行跟第一個(gè)矩陣的列 對(duì)齊 是不是感覺好多了O(∩_∩)O哈!

  整點(diǎn)復(fù)雜的 那再來隨便整個(gè)吧

  [2] [4] [-1] [6]

  [1] [0] [3 ] [5]

  結(jié)果是多少

  2x-1+4x3 2x6+4x5

  1x-1+0x3 1x6+0x5

  最終結(jié)果

  [10] [32]

  [-1] [6]

  也可以把它分解為單行單列的來看 就簡單多了哈

  有種很特殊的矩陣 有點(diǎn)像對(duì)角線

  任何跟他相乘的矩陣都等于那個(gè)矩陣本身 有點(diǎn)像 “任何數(shù)乘以1 都等于那個(gè)數(shù)本身”

  [1][0]

  [0][1]

  看吧矩陣乘法就是這么神奇的東東,通過矩陣乘法還可以進(jìn)行角度旋轉(zhuǎn) 縮放等等

  這個(gè)是很高深的研究課題了O(∩_∩)O哈! 這里就不討論了

  終于說完了 俺喝口水了先。也不知講清楚了沒 下面是各種矩陣乘法的示例代碼 關(guān)于為什么是5x5的矩陣這個(gè)可以看下msdn:

  知識(shí)學(xué)無止境

  第三個(gè)按鈕:

  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個(gè)能量值 我們看到屏幕上花花綠綠 顏色是因?yàn)槿齻€(gè)能量值產(chǎn)生差異化 說俗點(diǎn)就是三個(gè)值的比例不一樣

  如果三個(gè)值一樣的話那就跟電燈泡無異了 就是純亮度表示 即我們常說的灰度圖。

  來寫個(gè)手工滴 很山寨滴 效率很低滴 更改亮度的函數(shù)

  void light(ref int r, ref int g, ref int b)

  {

  //計(jì)算后的平均值

  //增加亮度

  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;

  其實(shí)呢也遠(yuǎn)可以不必這樣 直接rgb分別乘以1.2 或者1.1之類的就可以了 只不過顏色會(huì)失真

  好了終于寫完啦 好累ya

  完了 ( ⊙ o ⊙ ) 本來就很菜 這點(diǎn)破秘密全被你們曉得了 以后出去俺還雜混吶

  當(dāng)然作為一個(gè)商業(yè)化的軟件 代碼的容錯(cuò)也是很重要的 你看acdsee 你把文件數(shù)據(jù)部分刪除一些他照樣能夠顯示 當(dāng)然這些都是很簡單的哈。



關(guān)鍵詞: 圖像處理 bmp

評(píng)論


相關(guān)推薦

技術(shù)專區(qū)

關(guān)閉