為加快處理速度,在圖像處理算法中,往往需要把彩色圖像轉換為灰度圖像,在灰度圖像上得到驗證的算法,很容易移
植到彩色圖像上。
24位彩色圖像每個像素用3個字節表示,每個字節對應着R、G、B分量的亮度(紅、綠、藍)。當R、G、B分量值不同時,表現為彩色圖像;當R、G、B分量值相同時,表現為灰度圖像,該值就是我們所求的一般來說,轉換公式有3種。第一種轉換公式為:
Gray(i,j)=[R(i,j)+G(i,j)+B(i,j)]÷3 (2.1)
其中,Gray(i,j)為轉換后的灰度圖像在(i,j)點處的灰度值。該方法雖然簡單,但人眼對顏色的感應是不同的,因此有了第二種轉換公式:
Gray(i,j)=0299R(i,j)+0.587×G(i,j)+0.114×B(i,j) (2.2)
觀察上式,發現綠色所占的比重最大,所以轉換時可以直接使用G值作為轉換后的灰度
Gray(i,j)=G(i,j) (2.3)
在這里,我們應用最常用的公式(2.2),並且變換后的灰度圖像仍然用24位圖像表示。
1.提取像素法
這種方法簡單易懂,但相當耗時,完全不可取.
該方法使用的是GD+中的 Bitmap Getpixel和 BitmapSetpixel.方法。為了將位圖的顏色設置為灰度或其他顏色,就需要使用 Gepiⅸxel來讀取當前像素的顏色,再計算灰度值,最后使用 Setpixel來應用新的顏色。雙擊“提取像素法” Button控件,為該控件添加 Click事件,
代碼如下:
/// <summary> /// 提取像素法 /// </summary> private void pixel_Click(object sender, EventArgs e) {if (curBitmpap != null) { Color curColor; int ret; //二維圖像數組循環 for(int i = 0; i < curBitmpap.Width; i++) { for(int j = 0; j < curBitmpap.Height; j++) { //獲取該像素點的RGB顏色值 curColor = curBitmpap.GetPixel(i, j); //利用公式計算灰度值 ret = (int)(curColor.R * 0.299 + curColor.G * 0.587 + curColor.B * 0.114); //設置該像素點的灰度值,R=G=B=ret curBitmpap.SetPixel(i, j, Color.FromArgb(ret, ret, ret)); } }//對窗體進行重新繪制,這將強制執行Paint事件處理程序 Invalidate(); } }
2.內存法
該方法就是把圖像數據直接復制到內存中,這樣就使程序的運行速度大大提高。雙擊“內存法”按鈕控件,為該控件添加Cick事件,代碼如下:
/// <summary> /// 內存法(適用於任意大小的24位彩色圖像) /// </summary> private void memory_Click(object sender, EventArgs e) {if (curBitmpap != null) { //位圖矩形 Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height); //以可讀寫的方式鎖定全部位圖像素 System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat); //得到首地址 IntPtr ptr = bmpData.Scan0; //定義被鎖定的數組大小,由位圖數據與未用空間組成的 int bytes = bmpData.Stride * bmpData.Height; //定義位圖數組 byte[] rgbValues = new byte[bytes]; //復制被鎖定的位圖像素值到該數組內 System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes); //灰度化 double colorTemp = 0; for (int i = 0; i < bmpData.Height; i++) { //只處理每行中是圖像像素的數據,舍棄未用空間 for (int j = 0; j < bmpData.Width * 3; j += 3) { //利用公式計算灰度值 colorTemp = rgbValues[i * bmpData.Stride + j + 2] * 0.299 + rgbValues[i * bmpData.Stride + j + 1] * 0.587 + rgbValues[i * bmpData.Stride + j] * 0.114; //R=G=B rgbValues[i * bmpData.Stride + j] = rgbValues[i * bmpData.Stride + j + 1] = rgbValues[i * bmpData.Stride + j + 2] = (byte)colorTemp; } } //把數組復制回位圖 System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes); //解鎖位圖像素 curBitmpap.UnlockBits(bmpData);//對窗體進行重新繪制,這將強制執行Paint事件處理程序 Invalidate(); } }
3.指針法
該方法與內存法相似,開始都是通過 Lockbits方法來獲取位圖的首地址。但該方法更簡潔,直接應用指針對位圖進行操作。
為了保持類型安全,在默認情況下,C#是不支持指針運算的,因為使用指針會帶來相關的風險。所以C#只允許在特別標記的代碼塊中使用指針。通過使用 unsafe關鍵字,可以定義可使用指針的不安全上下文。
雙擊“指針法”按鈕控件,為該控件添加 Click事件,代碼如下:
/// <summary> /// 指針法 /// </summary> private void pointer_Click(object sender, EventArgs e) {if (curBitmpap != null) { //位圖矩形 Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height); //以可讀寫的方式鎖定全部位圖像素 System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat); byte temp = 0; //啟用不安全模式 unsafe { //得到首地址 byte* ptr = (byte*)(bmpData.Scan0); //二維圖像循環 for (int i = 0; i < bmpData.Height; i++) { for (int j = 0; j < bmpData.Width; j++) { //利用公式計算灰度值 temp = (byte)(0.299 * ptr[2] + 0.587 * ptr[1] + 0.114 * ptr[0]); //R=G=B ptr[0] = ptr[1] = ptr[2] = temp; //指向下一個像素 ptr += 3; } //指向下一行數組的首個字節 ptr += bmpData.Stride - bmpData.Width * 3; } } //解鎖位圖像素 curBitmpap.UnlockBits(bmpData);//對窗體進行重新繪制,這將強制執行Paint事件處理程序 Invalidate(); } }
由於啟動了不安全模式,為了能夠順利地編譯該段代碼,必須設置相關選項。在主菜單中選擇“項目|gray屬性”,在打開的屬性頁中選擇“生成”屬性頁,最后選中“允許不安全代碼”復選框。
三種方法的比較
從3段代碼的長度和難易程度來看,提取像素法又短又簡單。它直接應用GD+中的Bitmap. Getpixel方法和 Bitmap. Setpixel方法,大大減少了代碼的長度,降低了使用者的難度,並且可讀性好。但衡量程序好壞的標准不是僅僅看它的長度和難易度,而是要看它的效率,尤其是像圖像處理這種往往需要處理二維數據的大信息量的應用領域,就更需要考慮效率了為了比較這3種方法的效率,我們對其進行計時。首先在主窗體內添加一個 Label控件和 Textbox控件。
添加一個類:HiPerfTimer
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices; using System.ComponentModel; using System.Threading; namespace gray { internal class HiPerfTimer { //引用win32API中的QueryPerformanceCounter()方法 //該方法用來查詢任意時刻高精度計數器的實際值 [DllImport("Kernel32.dll")] //using System.Runtime.InteropServices; private static extern bool QueryPerformanceCounter(out long lpPerformanceCount); //引用win32API中的QueryPerformanceCounter()方法 //該方法用來查詢任意時刻高精度計數器的實際值 [DllImport("Kernel32.dll")] private static extern bool QueryPerformanceFrequency(out long lpFrequency); private long startTime, stopTime; private long freq; public HiPerfTimer() { startTime = 0; stopTime = 0; if(QueryPerformanceFrequency(out freq) == false) { //不支持高性能計時器 throw new Win32Exception(); //using System.ComponentModel; } } //開始計時 public void Start() { //讓等待線程工作 Thread.Sleep(0); //using System.Threading; QueryPerformanceCounter(out startTime); } //結束計時 public void Stop() { QueryPerformanceCounter(out stopTime); } //返回計時結果(ms) public double Duration { get { return (double)(stopTime - startTime) * 1000 / (double)freq; } } } }
在Form1類內定義HiPerfTimer類並在構造函數內為其實例化
private HiPerfTimer myTimer; public Form1() { InitializeComponent(); myTimer = new gray.HiPerfTimer(); }
分別在“提取像素法”、“內存法”和“指針法” Button控件的 Click事件程序代碼內的
if判斷語句之間的最開始一行添加以下代碼:
//啟動計時器 myTimer.Start();
在上述3個單擊事件內的 Invalidate0語句之前添加以下代碼:
//關閉計時器 myTimer.Stop(); //在TextBox內顯示計時時間 timeBox.Text = myTimer.Duration.ToString("####.##") + "毫秒";
最后,編譯並運行該段程序。可以明顯看出,內存法和指針法比提取像素法要快得多。提取像素法應用GDI+中的方法,易於理解,方法簡單,很適合於C#的初學者使用,但它的運行速度最慢,效率最低。內存法把圖像復制到內存中,直接對內存中的數據進行處理,速度明顯提高,程序難度也不大。指針法直接應用指針來對圖像進行處理,所以速度最快。但在C#中是不建議使用指針的,因為使用指針,代碼不僅難以編寫和調試,而且無法通過CLR的內存類型安全檢查,不能發揮C#的特長。只有對C#和指針有了充分的理解,才能用好該方法。究竟要使用哪種方法,還要看具體情況而定。但3種方法都能有效地對圖像進行處理。
全文代碼:
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows.Forms; namespace gray { public partial class Form1 : Form { private HiPerfTimer myTimer; public Form1() { InitializeComponent(); myTimer = new gray.HiPerfTimer(); } //文件名 private string curFileName; //圖像對象 private System.Drawing.Bitmap curBitmpap; /// <summary> /// 打開圖像文件 /// </summary> private void open_Click(object sender, EventArgs e) { //創建OpenFileDialog OpenFileDialog opnDlg = new OpenFileDialog(); //為圖像選擇一個篩選器 opnDlg.Filter = "所有圖像文件|*.bmp;*.pcx;*.png;*.jpg;*.gif;" + "*.tif;*.ico;*.dxf;*.cgm;*.cdr;*.wmf;*.eps;*.emf|" + "位圖(*.bmp;*.jpg;*.png;...)|*.bmp;*.pcx;*.png;*.jpg;*.gif;*.tif;*.ico|" + "矢量圖(*.wmf;*.eps;*.emf;...)|*.dxf;*.cgm;*.cdr;*.wmf;*.eps;*.emf"; //設置對話框標題 opnDlg.Title = "打開圖像文件"; //啟用“幫助”按鈕 opnDlg.ShowHelp = true; //如果結果為“打開”,選定文件 if(opnDlg.ShowDialog()==DialogResult.OK) { //讀取當前選中的文件名 curFileName = opnDlg.FileName; //使用Image.FromFile創建圖像對象 try { curBitmpap = (Bitmap)Image.FromFile(curFileName); } catch(Exception exp) { MessageBox.Show(exp.Message); } } //對窗體進行重新繪制,這將強制執行paint事件處理程序 Invalidate(); } private void Form1_Paint(object sender, PaintEventArgs e) { //獲取Graphics對象 Graphics g = e.Graphics; if (curBitmpap != null) { //使用DrawImage方法繪制圖像 //160,20:顯示在主窗體內,圖像左上角的坐標 //curBitmpap.Width, curBitmpap.Height圖像的寬度和高度 g.DrawImage(curBitmpap, 160, 20, curBitmpap.Width, curBitmpap.Height); } } /// <summary> /// 保存圖像文件 /// </summary> private void save_Click(object sender, EventArgs e) { //如果沒有創建圖像,則退出 if (curBitmpap == null) return; //調用SaveFileDialog SaveFileDialog saveDlg = new SaveFileDialog(); //設置對話框標題 saveDlg.Title = "保存為"; //改寫已存在文件時提示用戶 saveDlg.OverwritePrompt = true; //為圖像選擇一個篩選器 saveDlg.Filter = "BMP文件(*.bmp)|*.bmp|" + "Gif文件(*.gif)|*.gif|" + "JPEG文件(*.jpg)|*.jpg|" + "PNG文件(*.png)|*.png"; //啟用“幫助”按鈕 saveDlg.ShowHelp = true; //如果選擇了格式,則保存圖像 if (saveDlg.ShowDialog() == DialogResult.OK) { //獲取用戶選擇的文件名 string filename = saveDlg.FileName; string strFilExtn = filename.Remove(0, filename.Length - 3); //保存文件 switch (strFilExtn) { //以指定格式保存 case "bmp": curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Bmp); break; case "jpg": curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Jpeg); break; case "gif": curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Gif); break; case "tif": curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Tiff); break; case "png": curBitmpap.Save(filename, System.Drawing.Imaging.ImageFormat.Png); break; default: break; } } } //關閉窗體 private void close_Click(object sender, EventArgs e) { this.Close(); } /// <summary> /// 提取像素法 /// </summary> private void pixel_Click(object sender, EventArgs e) { //啟動計時器 myTimer.Start(); if (curBitmpap != null) { Color curColor; int ret; //二維圖像數組循環 for(int i = 0; i < curBitmpap.Width; i++) { for(int j = 0; j < curBitmpap.Height; j++) { //獲取該像素點的RGB顏色值 curColor = curBitmpap.GetPixel(i, j); //利用公式計算灰度值 ret = (int)(curColor.R * 0.299 + curColor.G * 0.587 + curColor.B * 0.114); //設置該像素點的灰度值,R=G=B=ret curBitmpap.SetPixel(i, j, Color.FromArgb(ret, ret, ret)); } } //關閉計時器 myTimer.Stop(); //在TextBox內顯示計時時間 timeBox.Text = myTimer.Duration.ToString("####.##") + "毫秒"; //對窗體進行重新繪制,這將強制執行Paint事件處理程序 Invalidate(); } } /// <summary> /// 內存法(適用於任意大小的24位彩色圖像) /// </summary> private void memory_Click(object sender, EventArgs e) { //啟動計時器 myTimer.Start(); if (curBitmpap != null) { //位圖矩形 Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height); //以可讀寫的方式鎖定全部位圖像素 System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat); //得到首地址 IntPtr ptr = bmpData.Scan0; //定義被鎖定的數組大小,由位圖數據與未用空間組成的 int bytes = bmpData.Stride * bmpData.Height; //定義位圖數組 byte[] rgbValues = new byte[bytes]; //復制被鎖定的位圖像素值到該數組內 System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes); //灰度化 double colorTemp = 0; for (int i = 0; i < bmpData.Height; i++) { //只處理每行中是圖像像素的數據,舍棄未用空間 for (int j = 0; j < bmpData.Width * 3; j += 3) { //利用公式計算灰度值 colorTemp = rgbValues[i * bmpData.Stride + j + 2] * 0.299 + rgbValues[i * bmpData.Stride + j + 1] * 0.587 + rgbValues[i * bmpData.Stride + j] * 0.114; //R=G=B rgbValues[i * bmpData.Stride + j] = rgbValues[i * bmpData.Stride + j + 1] = rgbValues[i * bmpData.Stride + j + 2] = (byte)colorTemp; } } //把數組復制回位圖 System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes); //解鎖位圖像素 curBitmpap.UnlockBits(bmpData); //關閉計時器 myTimer.Stop(); //在TextBox內顯示計時時間 timeBox.Text = myTimer.Duration.ToString("####.##") + "毫秒"; //對窗體進行重新繪制,這將強制執行Paint事件處理程序 Invalidate(); } } /// <summary> /// 指針法 /// </summary> private void pointer_Click(object sender, EventArgs e) { //啟動計時器 myTimer.Start(); if (curBitmpap != null) { //位圖矩形 Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height); //以可讀寫的方式鎖定全部位圖像素 System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat); byte temp = 0; //啟用不安全模式 unsafe { //得到首地址 byte* ptr = (byte*)(bmpData.Scan0); //二維圖像循環 for (int i = 0; i < bmpData.Height; i++) { for (int j = 0; j < bmpData.Width; j++) { //利用公式計算灰度值 temp = (byte)(0.299 * ptr[2] + 0.587 * ptr[1] + 0.114 * ptr[0]); //R=G=B ptr[0] = ptr[1] = ptr[2] = temp; //指向下一個像素 ptr += 3; } //指向下一行數組的首個字節 ptr += bmpData.Stride - bmpData.Width * 3; } } //解鎖位圖像素 curBitmpap.UnlockBits(bmpData); //關閉計時器 myTimer.Stop(); //在TextBox內顯示計時時間 timeBox.Text = myTimer.Duration.ToString("####.##") + "毫秒"; //對窗體進行重新繪制,這將強制執行Paint事件處理程序 Invalidate(); } } /// <summary> /// 內存法(僅適用於512*512的圖像) /// </summary> //private void memory_Click(object sender, EventArgs e) //{ // if (curBitmpap != null) // { // //位圖矩形 // Rectangle rect = new Rectangle(0, 0, curBitmpap.Width, curBitmpap.Height); // //以可讀寫的方式鎖定全部位圖像素 // System.Drawing.Imaging.BitmapData bmpData = curBitmpap.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadWrite, curBitmpap.PixelFormat); // //得到首地址 // IntPtr ptr = bmpData.Scan0; // //24位bmp位圖字節數 // int bytes = curBitmpap.Width * curBitmpap.Height * 3; // //定義位圖數組 // byte[] rgbValues = new byte[bytes]; // //復制被鎖定的位圖像素值到該數組內 // System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes); // //灰度化 // double colorTemp = 0; // for(int i = 0; i < rgbValues.Length; i += 3) // { // //利用公式計算灰度值 // colorTemp = rgbValues[i + 2] * 0.299 + rgbValues[i + 1] * 0.587 + rgbValues[i] * 0.114; // //R=G=B // rgbValues[i]=rgbValues[i+1]=rgbValues[i+2]=(byte)colorTemp; // } // //把數組復制回位圖 // System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, bytes); // //解鎖位圖像素 // curBitmpap.UnlockBits(bmpData); // //對窗體進行重新繪制,這將強制執行Paint事件處理程序 // Invalidate(); // } //} } }
/// <summary> /// 圖像灰度化 /// </summary> /// <param name="bmp"></param> /// <returns></returns> public static Bitmap ToGray(Bitmap bmp) { for (int i = 0; i < bmp.Width; i++) { for (int j = 0; j < bmp.Height; j++) { //獲取該點的像素的RGB的顏色 Color color = bmp.GetPixel(i, j); //利用公式計算灰度值 int gray = (int)(color.R * 0.3 + color.G * 0.59 + color.B * 0.11); Color newColor = Color.FromArgb(gray, gray, gray); bmp.SetPixel(i, j, newColor); } } return bmp; }
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Runtime.InteropServices; using System.ComponentModel; using System.Threading; namespace gray { internal class HiPerfTimer { //引用win32API中的QueryPerformanceCounter()方法 //該方法用來查詢任意時刻高精度計數器的實際值 [DllImport("Kernel32.dll")] //using System.Runtime.InteropServices; private static extern bool QueryPerformanceCounter(out long lpPerformanceCount); //引用win32API中的QueryPerformanceCounter()方法 //該方法用來查詢任意時刻高精度計數器的實際值 [DllImport("Kernel32.dll")] private static extern bool QueryPerformanceFrequency(out long lpFrequency); private long startTime, stopTime; private long freq; public HiPerfTimer() { startTime = 0; stopTime = 0; if(QueryPerformanceFrequency(out freq) == false) { //不支持高性能計時器 throw new Win32Exception(); //using System.ComponentModel; } } //開始計時 public void Start() { //讓等待線程工作 Thread.Sleep(0); //using System.Threading; QueryPerformanceCounter(out startTime); } //結束計時 public void Stop() { QueryPerformanceCounter(out stopTime); } //返回計時結果(ms) public double Duration { get { return (double)(stopTime - startTime) * 1000 / (double)freq; } } } }
灰度圖像二值化:
在進行了灰度化處理之后,圖像中的每個象素只有一個值,那就是象素的灰度值。它的大小決定了象素的亮暗程度。為了更加便利的開展下面的圖像處理操作,還需要對已經得到的灰度圖像做一個二值化處理。圖像的二值化就是把圖像中的象素根據一定的標准分化成兩種顏色。在系統中是根據象素的灰度值處理成黑白兩種顏色。和灰度化相似的,圖像的二值化也有很多成熟的算法。它可以采用自適應閥值法,也可以采用給定閥值法。
#region Otsu閾值法二值化模塊 /// <summary> /// Otsu閾值 /// </summary> /// <param name="b">位圖流</param> /// <returns></returns> public Bitmap OtsuThreshold(Bitmap b) { // 圖像灰度化 // b = Gray(b); int width = b.Width; int height = b.Height; byte threshold = 0; int[] hist = new int[256]; int AllPixelNumber = 0, PixelNumberSmall = 0, PixelNumberBig = 0; double MaxValue, AllSum = 0, SumSmall = 0, SumBig, ProbabilitySmall, ProbabilityBig, Probability; BitmapData data = b.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); unsafe { byte* p = (byte*)data.Scan0; int offset = data.Stride - width * 4; for (int j = 0; j < height; j++) { for (int i = 0; i < width; i++) { hist[p[0]]++; p += 4; } p += offset; } b.UnlockBits(data); } //計算灰度為I的像素出現的概率 for (int i = 0; i < 256; i++) { AllSum += i * hist[i]; // 質量矩 AllPixelNumber += hist[i]; // 質量 } MaxValue = -1.0; for (int i = 0; i < 256; i++) { PixelNumberSmall += hist[i]; PixelNumberBig = AllPixelNumber - PixelNumberSmall; if (PixelNumberBig == 0) { break; } SumSmall += i * hist[i]; SumBig = AllSum - SumSmall; ProbabilitySmall = SumSmall / PixelNumberSmall; ProbabilityBig = SumBig / PixelNumberBig; Probability = PixelNumberSmall * ProbabilitySmall * ProbabilitySmall + PixelNumberBig * ProbabilityBig * ProbabilityBig; if (Probability > MaxValue) { MaxValue = Probability; threshold = (byte)i; } } return this.Threshoding(b, threshold); } // end of OtsuThreshold 2 #endregion #region 固定閾值法二值化模塊 public Bitmap Threshoding(Bitmap b, byte threshold) { int width = b.Width; int height = b.Height; BitmapData data = b.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); unsafe { byte* p = (byte*)data.Scan0; int offset = data.Stride - width * 4; byte R, G, B, gray; for (int y = 0; y < height; y++) { for (int x = 0; x < width; x++) { R = p[2]; G = p[1]; B = p[0]; gray = (byte)((R * 19595 + G * 38469 + B * 7472) >> 16); if (gray >= threshold) { p[0] = p[1] = p[2] = 255; } else { p[0] = p[1] = p[2] = 0; } p += 4; } p += offset; } b.UnlockBits(data); return b; } } #endregion
