一個菜鳥的圖像處理入門
本來就是入門的 那就先說下gdi 跟 bmp 這些東西吧。
本文引用地址:http://www.ex-cimer.com/article/201704/346204.htm1 gdi跟bmp
vc里的CDC 也就是設(shè)備上下文 相當(dāng)于c#里的graphics ,也有l(wèi)ineTo等方法。
其實我們在c#中使用graphics的時候就已經(jīng)在使用gdi+了我們卻渾然不覺
那么gdi到底在哪里呢 試著在c盤搜索gdiplus或者gdi32名字的文件 你應(yīng)該會找到 就像這個
直接刪除應(yīng)該刪不掉 不過你可以給他改個名字 別改了自己都搞忘了O(∩_∩)O哈!。
然后你隨便運(yùn)行個程序比如QQ 腫么樣
Initialization failure:0x0000000E
使用windows自帶的圖片查看器
加載 c:windowssystem32shimgvw.dll時出錯 系統(tǒng)找不到指定的文件。
所以說windows下到處都是gdi
不要以為bitmap是一種圖像格式 就像jpg gif 一樣 實際上他們是兩個完全不同的概念。
在vc++里叫cbitmap 也就是對應(yīng)的gdi數(shù)據(jù)模型 等同于c#里的bitmap
可以這樣說.bmp的位圖文件是gdi的文件表現(xiàn)形式。 位圖文件不進(jìn)行圖像壓縮算法操作直接存儲像素矩陣信息所以文件體積非常大
jpg文件體積非常小為什么 jpg實際上它是按照完全不同的算法跟理念來存儲圖像的 都知道人的視覺效應(yīng)
主要體現(xiàn)在兩個方面 色彩的明暗度 色彩的飽和度 也就是色調(diào)(說俗點就是赤橙黃綠青藍(lán)紫 )并且肉眼根本達(dá)不到每個像素那么大的分辨能力
jpg就是按照這種方式來存儲的
2 位圖文件格式
那么就先說下bmp文件格式吧,本人不是那種長篇大論型的 不說廢話。
這個壓縮包里工程的bin目錄有一個叫bmpTestImg.bmp兩色的位圖文件。以16進(jìn)制編輯器打開對照下圖看 :
這里是下載鏈接
vc++里有定義好的bitmapheader 用來表示位圖頭信息 在C#里沒有。
其實沒多大關(guān)系的這只是一種數(shù)據(jù)組織方式
如果你愿意也可以定義這么一個結(jié)構(gòu)。
gdi與設(shè)備無關(guān) 但是他并不代表跟設(shè)備沒有任何關(guān)聯(lián) 計算機(jī)之間傳遞的是圖片文件或者圖像數(shù)據(jù) 并不是gdi對象。
微軟幫我們搞了這一層?xùn)|西,就是說只要是接入windows的設(shè)備我們都可以通過
gdi在那個設(shè)備上顯示 輸出東西 而不用關(guān)系設(shè)備本身 ,可以說整個windows提供給我們的就是gdi 所有窗體
等等都是gdi繪制的,比如說做啥xx編程的時候要直接操縱顯卡 實際上直接操縱的方式速度更快但是沒有必要
所謂的24位真彩色 mspaint畫圖新建圖像默認(rèn)存儲 就是24位真彩色 這并不是什么高深技術(shù)因為 一個像素的顏色用3個八位
來表示就是24位真彩色
真彩圖像是說他具有顯示 256x256x256種顏色的能力
還有就是c#里默認(rèn)新建的bitmap對象就是24位真彩的 并且graphic提供的函數(shù)不能操作非真彩色的位圖
3 水平不咋滴,還是來敲點代碼吧,(^o^)/~
先賣個關(guān)子哈 上面你下載的示例圖片你看到東西了嗎 還是黑乎乎一片 嘿嘿。如果你看到了那你見鬼了 還是趕快拜拜春哥吧
最近在研究那啥dicom 也學(xué)會拽文了 嘿嘿
是否可以這么描述:bmp是一種約定俗成的有規(guī)律的數(shù)據(jù)組織方式 不論他在內(nèi)存中 在文件中 他跟特定編程語言無關(guān) 跟平臺無關(guān)
bmp格式簡而言之一句話 前54字節(jié)存儲文件頭信息最主要就是圖像位數(shù)跟寬度高度,從54位開始有調(diào)色板則是調(diào)色板信息 無調(diào)色板則是像素數(shù)據(jù)。
由于本文不是專門探討bmp文件格式 詳細(xì)請參見bmp格式
好下面我們就來讀取這種有規(guī)律的數(shù)據(jù): 寫第一個按鈕事件的代碼
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個字節(jié) 因為bmp格式"龜腚"在18字節(jié)那個地方開始用32位整型存儲圖像的寬度跟高度
bmpData.Seek(18, SeekOrigin.Begin);
int width = br.ReadInt32();
int height = br.ReadInt32();
MessageBox.Show(string.Format("寬{0},高{1}", width, height));
//第11個字節(jié)處儲存數(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)色板將會讓"看不見"的圖像顯示出來
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í)行文件同級目錄放上偶的圖片哦 嘿嘿。
這個適合用來給girlfriend表白啊啥的O(∩_∩)O哈!
有幾個需要說明的地方
bmp文件的兩色 并非一定得是黑白 對吧 可以是紅色綠色, 也可以是兩種相同的色兒 對吧
為什么寬度要在第19字節(jié)的位置開始存儲 沒有為什么 這是bmp格式的“龜腚”對吧 要問去問蓋茨大叔
對于“流”的操作 seek到前面去了 再進(jìn)行write操作 是否就把對應(yīng)位置的數(shù)據(jù)“擠”到后面去了呢?
NO 數(shù)據(jù)流是一種游標(biāo) “覆蓋”型的操作 長度會自動標(biāo)識到游標(biāo)到過最遠(yuǎn)的地方 文件流內(nèi)存流都一樣
所以說想要做數(shù)據(jù)插入啊 文件合并啊之類的東東的話得弄兩個數(shù)據(jù)流對象哈 互相倒騰數(shù)據(jù) 這樣才能達(dá)到目的。
又扯遠(yuǎn)了哈 打住。
不是說讀取數(shù)據(jù)嗎 就是讀取像素值數(shù)據(jù)啊,現(xiàn)在開始
既然是讀取像素值,咱得一行一行的讀啊 就像掃描一樣的。實際上他就是以這種方式存儲的哈 只不過稍微有點不一樣
那就是圖像數(shù)據(jù)每行以四倍字節(jié)為基數(shù)不足以0補(bǔ)齊 乃明白了木有 。
比如說這一個掃描行有3個像素 那么就是9字節(jié) ,4字節(jié)的倍數(shù)那么他必須要有12字節(jié) 那么剩下的3字節(jié)全是0。
比如說這一個掃描行有20個像素 那么就是60字節(jié) ,4字節(jié)的倍數(shù)那么他必須要有60字節(jié) 因為60/4正好除凈。
先來說下這個破公式 ((width * 24 + 31) / 32 * 4) 不知道是哪個頭腦發(fā)熱的人想出來的 ,注意這里的32是指32位 即4字節(jié)。
實際上我只想說兩個字非常扯淡 一定是很深入的掌握了數(shù)據(jù)長度運(yùn)算的本質(zhì), 一句把我上面那n多句都代替了
width是圖像寬度 24代表每個像素位數(shù)。 計算出實際字節(jié)數(shù) 先假設(shè)他會超出一位 補(bǔ)齊31位 然后通過整型數(shù)據(jù)相除的性質(zhì) 除以4字節(jié)得到4字節(jié)的倍數(shù)
注意最終得到的是掃描行的字節(jié)數(shù)
就這樣從圖像左下角第一個點開始 一行一行從左至右的往上掃描
然后是bmp圖像素的存儲方式是BGR的順序哈 而不是通常的RGB 哦 別搞錯了,
以前很菜的時候用SetPixel()處理像素 被人罵慘了 現(xiàn)在俺依然來寫個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++) 可以看下效果 可以證明在文件中是按照圖像從左至右往上 的方式存儲的
通過以上可以看出任何環(huán)境下他都是按照同種規(guī)律存儲存儲的,就像dicom 只要遵循這種規(guī)律就能通過這種格式實現(xiàn)數(shù)據(jù)共享。
都說lockBitmap的方式是最快的 ,確實是最快的哈 因為他是使用指針的方式
下面是把一個圖像轉(zhuǎn)成灰度圖 你看 不但代碼少了很多 并且還不用費(fèi)盡心思去確定每一個掃描行的索引 你看 刷的一下 就出來了 嘿嘿
注意有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]
沒錯 你看到的這就是矩陣乘法 不想講什么線性代數(shù) 什么的那么高深的理論
比如上面RGB轉(zhuǎn)轉(zhuǎn)YUV公式的3行3列 乘 3行1列 乘出來是 3行1列 ,
規(guī)律就是第一個矩陣的列跟第二個矩陣的行一致,得到一個首行尾列數(shù)的二維矩陣。
注意一定是得到一個首行尾列數(shù)的二維矩陣,如果不符合這個標(biāo)準(zhǔn) 那么運(yùn)算是無意義的。
運(yùn)算規(guī)則:要從結(jié)果矩陣的往前推,先確定結(jié)果矩陣元素所在行數(shù)列數(shù) C(i,j)。然后把A矩陣對應(yīng)行 與B矩陣對應(yīng)列
相同索引位置的數(shù)兩兩相乘 然后加起來 即為C(i,j)的值。 其實還是挺簡單的。
這里有關(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
貌似很難理解 因為這個跟前面那個賣東西的又不一樣了可以換個角度看 。
把第二個矩陣往左“倒下來” 就是說讓他的行跟第一個矩陣的列 對齊 是不是感覺好多了O(∩_∩)O哈!
整點復(fù)雜的 那再來隨便整個吧
[2] [4] [-1] [6]
[1] [0] [3 ] [5]
結(jié)果是多少
2x-1+4x3 2x6+4x5
1x-1+0x3 1x6+0x5
最終結(jié)果
[10] [32]
[-1] [6]
也可以把它分解為單行單列的來看 就簡單多了哈
有種很特殊的矩陣 有點像對角線
任何跟他相乘的矩陣都等于那個矩陣本身 有點像 “任何數(shù)乘以1 都等于那個數(shù)本身”
[1][0]
[0][1]
看吧矩陣乘法就是這么神奇的東東,通過矩陣乘法還可以進(jìn)行角度旋轉(zhuǎn) 縮放等等
這個是很高深的研究課題了O(∩_∩)O哈! 這里就不討論了
終于說完了 俺喝口水了先。也不知講清楚了沒 下面是各種矩陣乘法的示例代碼 關(guān)于為什么是5x5的矩陣這個可以看下msdn:
知識學(xué)無止境
第三個按鈕:
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個能量值 我們看到屏幕上花花綠綠 顏色是因為三個能量值產(chǎn)生差異化 說俗點就是三個值的比例不一樣
如果三個值一樣的話那就跟電燈泡無異了 就是純亮度表示 即我們常說的灰度圖。
來寫個手工滴 很山寨滴 效率很低滴 更改亮度的函數(shù)
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;
其實呢也遠(yuǎn)可以不必這樣 直接rgb分別乘以1.2 或者1.1之類的就可以了 只不過顏色會失真
好了終于寫完啦 好累ya
完了 ( ⊙ o ⊙ ) 本來就很菜 這點破秘密全被你們曉得了 以后出去俺還雜混吶
當(dāng)然作為一個商業(yè)化的軟件 代碼的容錯也是很重要的 你看acdsee 你把文件數(shù)據(jù)部分刪除一些他照樣能夠顯示 當(dāng)然這些都是很簡單的哈。
評論