使用GDI+點陣圖資料掃描線處理影象的小技巧
在GDI+影象處理中,我們經常利用BitmapData結構對影象資料掃描線進行操作,在我的大部分BOLG文章中,都使用了這個方法。GDI+點陣圖通過其LockBits方法和UnlockBits方法,分別用來鎖定(獲取)和解鎖(釋放)BitmapData資料,我們一般都在這2個方法之間操作影象資料掃描線,如:
- Bitmap *bmp = new Bitmap(L"d://001-1.jpg");
- BitmapData data;
- Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight());
-
// 鎖定為32位畫素格式
- bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite, PixelFormat32bppARGB, &data);
- // 這裡對影象資料掃描線操作,由於bmp已經鎖定,很多方法不能呼叫
- bmp->UnlockBits(&data); // 解鎖
- Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle);
- g->DrawImage(bmp, 0, 0);
- delete g;
- delete bmp;
由於在LockBits方法和UnlockBits方法之間,點陣圖物件是鎖定的,很多方法無法呼叫,有時也感到有些不方便,甚至繁瑣。比如對影象資料掃描線進行多次處理,在處理過程中想分步驟顯示或者儲存時,就不得不反覆呼叫這2個方法;還有就是點陣圖格式低於24位格式的影象無法鎖定為24位或32位資料進行操作(我們大多利用24位或者32位畫素掃描線進行影象處理)等等。
可以使用一些小技巧來規避因點陣圖物件鎖定而帶來的不方便,也可對低於24位格式的影象進行24位或32點陣圖像資料掃描線操作。請看下面的例子:
- Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle);
- Bitmap *bmp = new Bitmap(L"d://001-1.jpg");
- BitmapData data;
- Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight());
- // 預先設定掃描線長度和影象資料
-
data.Stride = r.Width * 4;
- data.Scan0 = (void*)newchar[r.Height * data.Stride];
- // 建立一個32位畫素格式的自定義資料點陣圖物件
- Bitmap *bmp2 = new Bitmap(r.Width, r.Height, data.Stride,
- PixelFormat32bppARGB, (BYTE*)data.Scan0);
- // 使用ImageLockModeRead標記使bmp影象資料拷貝到data.Scan0。
- // 使用ImageLockModeUserInputBuf標記鎖定影象bmp,使bmp和bmp2共享影象資料。
- // 使用ImageLockModeWrite標記使bmp和bmp2同步,即影象資料處理同時
- // 作用於bmp和bmp2,去掉ImageLockModeWrite後,bmp影象不改變,
- // 低於24位格式的影象必須去掉ImageLockModeWrite
- bmp->LockBits(&r, ImageLockModeRead | ImageLockModeWrite | ImageLockModeUserInputBuf,
- PixelFormat32bppARGB, &data);
- // 這裡可分步操作影象掃描線,同時也可顯示bmp2,或者呼叫bmp2的任何方法
- ImageGray(&data); // 影象灰度化,具體過程略
- g->DrawImage(bmp2, 0, 0); // 畫影象(或者其它方法呼叫)
- ImageTwoValues(&dada, 127); // 影象二值化,具體過程略
- g->DrawImage(bmp2, 200, 0); // 畫影象(或者其它方法呼叫)
- bmp->UnlockBits(&data);
- delete g;
- delete bmp;
- delete bmp2;
- delete[] data.Scan0; // 必須釋放
上面例子程式碼中作了較詳細的說明,就不再解釋。
上面的例子為了解釋點陣圖物件共享和資料處理同步,程式碼顯得有些凌亂,其實只要記住一點:例子中,自定義點陣圖物件bmp2通過bmp->LockBits方法取得資料後,如果無特殊需要,bmp就可解鎖甚至delete,這時也不再需要對bmp2鎖定,就可通過對data的處理,達到對bmp2包含的影象資料進行改變的目的。
將上面程式碼重新規劃一下,使之清晰一些:
- BOOL GetBitmapData(WCHAR *fileName, PixelFormat pixelFormat, BitmapData *data)
- {
- Bitmap *bmp = new Bitmap(fileName);
- if (bmp->GetLastStatus() != Ok)
- return FALSE;
- Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight());
- UINT pixelSize = GetPixelFormatSize(pixelFormat);
- data->Stride = ((pixelSize * r.Width + 31) & 0xffffffe0) >> 3;
- data->Scan0 = (void*)newchar[r.Height * data->Stride];
- bmp->LockBits(&r, ImageLockModeRead | ImageLockModeUserInputBuf,
- pixelFormat, data);
- bmp->UnlockBits(data);
- delete bmp;
- return TRUE;
- }
- void __fastcall TForm2::Button1Click(TObject *Sender)
- {
- // 取得影象的24位畫素格式資料
- BitmapData data;
- if (!GetBitmapData(L"d://001-1.jpg", PixelFormat24bppRGB, &data))
- return;
- // 建立一個24位畫素格式的自定義資料點陣圖物件
- Bitmap *bmp = new Bitmap(data.Width, data.Height, data.Stride,
- data.PixelFormat, (BYTE*)data.Scan0);
- Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle);
- // 這裡可操作影象資料掃描線,不必再鎖定bmp,同時也可顯示bmp,或者呼叫bmp的任何方法
- ImageGray(&data); // 影象灰度化,具體過程略
- g->DrawImage(bmp, 0, 0); // 畫影象(或者其它方法呼叫)
- ImageTwoValues(&dada, 127); // 影象二值化,具體過程略
- g->DrawImage(bmp, 200, 0); // 畫影象(或者其它方法呼叫)
- delete g;
- delete bmp;
- delete[] data.Scan0; // 必須釋放
- }
通過上面程式碼,就可以看出呼叫GetBitmapData後,所有的影象資料資訊就已經包含在BitmapData結構中了,所以我們可以對這個資料結構進行任何的操作,而不再依賴任何GDI+物件,由此避免了本文前面所說的不方便。之所以又建立一個自定義資料點陣圖物件,只是要藉助它進行影象顯示、儲存等操作而已。
利用類似於前面的例子程式碼還可以進行拼圖操作:
- Gdiplus::Graphics *g = new Gdiplus::Graphics(Canvas->Handle);
- Bitmap *bmp = new Bitmap(L"d://001-1.jpg");
- BitmapData data;
- Gdiplus::Rect r(0, 0, bmp->GetWidth(), bmp->GetHeight());
- // 預先設定掃描線長度和影象資料,掃描線為2個影象的寬度
- data.Stride = r.Width * 2 * 4;
- data.Scan0 = (void*)newchar[r.Height * data.Stride];
- // 拷貝影象到左邊,注意這裡只是讀取資料,所以ImageLockModeWrite沒使用
- bmp->LockBits(&r, ImageLockModeRead | ImageLockModeUserInputBuf,
- PixelFormat32bppARGB, &data);
- bmp->UnlockBits(&data);
- // 拼合影象到右邊,為簡化,使用同一張圖。
- // 如果影象大小不同,只要r的尺寸一致(不能大於原圖)
- (char*)data.Scan0 += (r.Width * 4); // 掃描線地址向右移動
- bmp->LockBits(&r, ImageLockModeRead | ImageLockModeUserInputBuf,
- PixelFormat32bppARGB, &data);
- bmp->UnlockBits(&data);
- (char*)data.Scan0 -= (r.Width * 4); // 還原掃描線地址
- data.Width = r.Width * 2; // 重新設定拼圖寬度
- // 拼合圖資料資訊已經包含在data中了,可以繼續對data進行任何的操作
- // 建立一個自定義資料點陣圖物件
- Bitmap *bmp2 = new Bitmap(data.Width, data.Height, data.Stride,
- data.PixelFormat, (BYTE*)data.Scan0);
- // 顯示拼合後的影象。
- g->DrawImage(bmp2, 0, 0);
- delete g;
- delete bmp;
- delete bmp2;
- delete[] data.Scan0; // 必須釋放