當使用C#進行影像處理的時候,如果遇上了需要大量運算的部分,C#可能不太夠力(C#的指標運算受到了限制而且在運算過程中做了很多邊界檢查),這時可以考慮呼叫C++的函式處理這些影像。
在開始之前可以先參考這篇文章:
https://corettainformation.blogspot.com/2019/06/cc.html
我們以C# Window Forms App作為範例。
我們拉一個Panel(裝PictureBox用的容器)進表單中,然後讓它填滿整個表單。
然後拉一個PictureBox(圖片),讓它填滿表單,樣式設定為Zoom(這樣就可以等比例縮放影像)
接著拉一個MenuStrip(選單)進表單中,設定兩組選項("載入影像"和"處理")
最後拉一個openFileDialog(開啟檔案用的對話方塊)進表單中。
這樣子需要的元件已經都佈置好了。
然後在表單的程式碼中新增一個Bitmap(點陣圖)
回到表單設計頁面,將"載入影像"點兩下,再次進入程式碼編輯器中。
鍵入以下程式碼
private void 載入影像ToolStripMenuItem_Click(object sender, EventArgs e)
{
openFileDialog1.Filter = "圖片檔 (*.png;*.jpg;*.bmp;*.gif;*.tif)|*.png;*.jpg;*.bmp;*.gif;*.tif";
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
bitmap = new Bitmap(openFileDialog1.FileName);//將選擇的影像載入至bitmap中
pictureBox1.Image = bitmap;//將bitmap顯示在pictureBox中
}
}
解說:
openFileDialog1.Filter可限制檔案的類型,我們選png、jpg、bmp、gif、tif這五種。
使用openFileDialog1.ShowDialog()可開啟瀏覽檔案的對話方塊,當DialogResult.OK(開啟成功)時會將檔案路徑(openFileDialog1.FileName)傳給Bitmap產生新的點陣物件,我們將這個點陣影像指派給bitmap,最後設定PictureBox的影像為這個點陣圖。
接著測試程式是否能正常執行,執行程式後,按下載入影像/選擇要瀏覽的圖片/按下開啟舊檔。
如果能正常顯示影像,代表到目前為只是成功的,可以繼續做下去。
回到表單設計頁面,點兩下"處理",進入程式碼編輯器中。
接著鍵入以下程式碼。
註:必須在標頭引入 using System.Drawing.Imaging;
Bitmap MyNewBmp = bitmap;
Rectangle MyRec = new Rectangle(0, 0, MyNewBmp.Width, MyNewBmp.Height);
BitmapData MyBmpData = MyNewBmp.LockBits(MyRec, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
unsafe
{
//empty
}
MyNewBmp.UnlockBits(MyBmpData);
pictureBox1.Image = MyNewBmp;
解說:
Rectangle可以是一種矩形範圍,此範例為(0,0)的座標到(總寬度,總高度)的矩形範圍(意即選取了整張影像)
BitmapData 是像素資料。
LockBits可將點陣影像鎖定在系統記憶體內,可提高處理效能(但是最後務必使用UnlockBits來解鎖)。
在MyNewBmp.LockBits的參數中第一個放入MyRec(剛剛的矩形範圍),第二個是數字3(ImageLockMode是列舉,其中的ReadWrite是列舉值3,代表這個BitmapData可以讀取也可以寫入),第三個參數是PixelFormat的列舉值,Format32bppArgb代表32位元的影像格式(包含Alpha、R、G、B的色彩通道)。
unsafe是C#要使用非安全性代碼時必須使用的關鍵字,此部分先空著,等一下會在這個括號裡加東西。
UnlockBits是用來解鎖LockBits鎖住的記憶體,這是必要的東做,不做的話有可能會出現難以預期的錯誤。
最後將MyMyNewBmp顯示在pictureBox上
註:如果無法使用unsafe的話,請對專案按右鍵/屬性,點選建置/勾選允許不安全的程式碼。
接著我們開始製作C++函式來給C#調用。
對方案按下右鍵/加入/新增專案
嘗試尋找一個C++的CLR類別庫專案。
註:如果找不到的話請見下一張圖。
如果沒有找到的話就必須先安裝CLI的套件。
新增完成後,進入C++的程式碼編輯器中。
接著在類別中新增以下函式:
public:
void inline colorTo255(unsigned char* ptr, int width, int height, int channel)
{
unsigned char** fp = new unsigned char* [height];
int Stride = width * channel, x = 0, y = 0;
for (int j = 0; j < height; j++)
fp[j] = ptr + (Stride * j);
for (y = 0; y < height; y++)
{
for (x = 0; x < Stride; x += channel)
{
fp[y][x] = 255 - fp[y][x];
fp[y][x + 1] = 255 - fp[y][x + 1];
fp[y][x + 2] = 255 - fp[y][x + 2];
}
}
delete[] fp;
}
解說:
我們會在稍後引入點陣圖第0個像素的指標(ptr),其他參數包括width(影像寬度)、height(影像高度)、channel(通道數,有ARGB四種,所以此範例中的值應為四)
Stride是指掃描寬度(影像的每一列有多少位元組),x、y為等一下會用到的座標。
在 for (int j = 0; j < height; j++)fp[j] = ptr + (Stride * j); 中,我們將fp設定為影像中每一列開頭的指標位置的指標。
在外層迴圈,為逐列掃描,一個影像有幾列高度(height)就有多高。
在內層迴圈,為逐行掃描,每次增加4個位元組。
fp[y][x] = 255 - fp[y][x]; 將影像中的(x,y)座標的B值反轉。
fp[y][x + 1] = 255 - fp[y][x + 1];將影像中的(x,y)座標的G值反轉。
fp[y][x + 2] = 255 - fp[y][x + 2];將影像中的(x,y)座標的R值反轉。
註:在這裡,色彩空間的排列方式為BGRA(意即fp[y][x + 3]會是Alpha值)
結束後,使用 delete[] fp; 刪除不再需要用到的fp
然後我們對這個C++專案按右鍵/建置。
接著對C#專案按右鍵/加入/參考。
選擇剛剛建立的C++專案,按下確定。
加入完參考後就可以讓C#呼叫剛剛建立的類別了,我們回到C#的程式碼編輯器中,在表頭加入剛剛的名稱空間,並新增C++物件。
using ClassLibrary2;
註:意思就是引入剛剛在C++程式碼中出現的「namespace ClassLibrary2」
接著回到處理影像的那部分(unsafe那裡),輸入以下程式碼:
private void 處理ToolStripMenuItem_Click(object sender, EventArgs e)
{
Bitmap MyNewBmp = bitmap;
Rectangle MyRec = new Rectangle(0, 0, MyNewBmp.Width, MyNewBmp.Height);
BitmapData MyBmpData = MyNewBmp.LockBits(MyRec, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb);
unsafe
{
C.colorTo255((byte*)MyBmpData.Scan0, MyNewBmp.Width, MyNewBmp.Height, 4);
}
MyNewBmp.UnlockBits(MyBmpData);
pictureBox1.Image = MyNewBmp;
}
解說:
colorTo255是剛剛建立的C++函式,MyBmpData.Scan0是MyBmpData的第0個像素(我們將它轉換成byte*),MyNewBmp.Width是影像寬度,MyNewBmp.Height是影像高度 ,4是指通道數(包含ARGB四個通道)。
最後可以來測試程式是否正確了,我們按照剛剛的步驟執行程式,試著載入圖片。
然後按下選單中的處理。
如果看到影像的色彩被反轉了,恭喜你,成功了。
=====分隔線=====
如果覺得這篇文有幫助到你們的話,請留言或幫忙按個廣告吧。
您的支持是我寫文的最大動力。
如果依然失敗了,也請留言讓我知道,看看哪個環節出了問題,我有看到就會回。
希望這篇文有幫助到各位ξ( ✿>◡❛)