一個(gè)菜鳥的圖像處理入門
本來就是入門的 那就先說下gdi 跟 bmp 這些東西吧。
本文引用地址:http://m.butianyuan.cn/article/201704/346204.htm1 gdi跟bmp
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
可以這樣說.bmp的位圖文件是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)然這些都是很簡單的哈。
評(píng)論