C#使用像素數據直接顯示和保存圖像
概要:本篇將使用Win32函數完成圖像在控件上的顯示,使用直接向文件寫入字節數據的形式完成圖像保存。
本文也介紹了設備無關的位圖(DIB)的相關知識,是對上一篇文章《在WPF中使用WriteableBitmap對接工業相機及常用操作》中圖像顯示和保存功能的擴展。
圖像顯示
圖像的顯示只需要信息頭和像素位兩部分,不需要文件頭。
這里介紹使用SetStretchBltMode和StretchDIBits函數顯示圖像的方法,可以從官方文檔中了解它們的使用細節。它們在C#中的定義如下:
[DllImport("gdi32.dll", CharSet = CharSet.Auto)]
public static extern int SetStretchBltMode(IntPtr hdc,int mode);
[DllImport("gdi32.dll", CharSet = CharSet.Auto)]
public static extern int StretchDIBits(IntPtr hdc,int xDest,int yDest,int DestWidth,int DestHeight,
int xSrc,int ySrc,int SrcWidth,int SrcHeight,byte[] lpBits,IntPtr lpbmi,UInt32 iUsage,UInt32 rop);
使用這兩個函數的第一個參數hdc代表由Image控件生成的Graphics對象,它的生成方法如下:
完整實現如下:
//將文件頭信息寫入IntPtr中
int IHSize = Marshal.SizeOf(INFOHEADER);
byte[] IBuffer = new byte[IHSize];
pBitmapInfo = Marshal.AllocHGlobal(40);
Marshal.StructureToPtr(INFOHEADER, pBitmapInfo, false);
//獲取Image控件對應的Graphics
IntPtr hwnd = ((HwndSource)PresentationSource.FromVisual(MyImage)).Handle;
Graphics MyImageG = Graphics.FromHwnd(hwnd);
MyHdc = MyImageG.GetHdc();
//將像素數據復制到byte數組中
Marshal.Copy(pPixelOut, PixelData, 0, PixelData.Length);
SetStretchBltMode(MyHdc, 3);
StretchDIBits(MyHdc, 0, 0, (int)MyImage.Width, (int)MyImage.Height, 0, 0, PhotoWidth, PhotoHeight,PixelData, pBitmapInfo, 0, 0x00CC0020);
其中pPixelOut是從工業相機SDK方法得到的像素數據地址。
BitBlt是一個非常重要的Win32函數,DDB位圖的絕大多數操作最后都離不開這個函數。
DIB文件格式
.bmp圖像文件其實是與設備無關的位圖 (DIB) ,它的文件的結構如下所示:

如果是8位像素格式的圖則還有色彩表部分,16位及以上像素格式的圖不需要色彩。
所以其實只需要三部分內容就可以生成DIB文件,其中“像素位”指向的就是像素數據。
文件頭和信息頭
文件頭BITMAPFILEHEADER和信息頭BITMAPINFOHEADER(BITMAPINFOHADER結構后續已經發展了幾個新版本但是并不常用)定義在wingdi.h中。
文件頭包含文件類型、文件大小等信息,信息頭包含圖像大小等信息。具體細節可以查看官方文檔。它們用C#定義如下:
[StructLayout(LayoutKind.Sequential,Pack= 2)]
struct BITMAPFILEHEADER {
internal ushort bftype;
internal uint bfsize;
internal ushort bfreserved1;
internal ushort bfreserved2;
internal uint bfOffBits;
}
[StructLayout(LayoutKind.Sequential,Pack = 2)]
struct BITMAPINFOHEADER {
internal uint biSize;
internal int biWidth;
internal int biHeight;
internal ushort biPlanes;
internal ushort biBitCount;
internal uint biCompression;
internal uint biSizeImage;
internal int biXPelsPerMeter;
internal int biYPelsPerMeter;
internal uint biClrUsed;
internal int biClrImportant;
}
還是以PixelFormats.Bgr24像素格式為例,賦值情況如下:
BITMAPFILEHEADER FILEHEADER=new BITMAPFILEHEADER() {
bftype=0x4d42,
bfOffBits=54,
bfreserved1=0,
bfreserved2=0,
bfsize=(uint)(WBitmap.PixelWidth*WBitmap.PixelHeight*3+54)
};
BITMAPINFOHEADER INFOHEADER=new BITMAPINFOHEADER() {
biSize=40,
biWidth=WBitmap.PixelWidth,
biHeight=WBitmap.PixelHeight,
biPlanes=1,
biBitCount=24,
biClrImportant=0,
biSizeImage=0,
biXPelsPerMeter=0,
biYPelsPerMeter=0,
biClrUsed=0,
biCompression=0
};
bftype值固定是0x4d42,代表這個文件是bmp文件。bfOffBits值固定是54,代表像素數據起始地址的偏移量也就是BITMAPFILEHEADER加BITMAPINFOHEADER的大小,bfreserved1和bfreserved2是保留字段固定為0,bfsize是整個bmp文件的大小。
biSize固定是40表示BITMAPINFOHEADER結構的大小,biWidth和biHeight是寬和高,biPlanes固定值是1,biBitCount是像素格式的位數,其余均為固定值。
圖像保存
要創建bmp圖像,只需將DIB文件的三部分依次寫入文件即可。使用方法如下:
int FHSize = Marshal.SizeOf(FILEHEADER);
byte[] FBuffer = new byte[FHSize];
int IHSize = Marshal.SizeOf(INFOHEADER);
byte[] IBuffer = new byte[IHSize];
IntPtr prt=Marshal.AllocHGlobal(FHSize);
Marshal.StructureToPtr(FILEHEADER,prt,false);
Marshal.Copy(prt,FBuffer,0,FBuffer.Length);
Marshal.Release(prt);
prt=Marshal.AllocHGlobal(IHSize);
Marshal.StructureToPtr(INFOHEADER,prt,false);
Marshal.Copy(prt,IBuffer,0,IBuffer.Length);
Marshal.Release(prt);
using(FileStream fileStream = File.OpenWrite("C:\\duie.bmp")) {
fileStream.Write(FBuffer,0,FBuffer.Length);
fileStream.Write(IBuffer,0,IBuffer.Length);
for(int i = WBitmap.PixelHeight-1;i>-1;i--) {
int ki = i*WBitmap.PixelWidth*3;
int js = (i+1)*WBitmap.PixelWidth*3;
for(int k = ki;k<js;k++)
fileStream.WriteByte(Marshal.ReadByte(WBitmap.BackBuffer,k));
}
}
注意:在DIB中,圖像的底行是文件的第一行,圖像的頂行是文件的最后一行。所以這里用for循環從底下行開始往上讀字節。
總結
使用本文中的方法的好處是不用創建WriteableBitmap或Bitmap對象,可以在WPF和WinForm項目中通用。并且使用了更接近底層的方法相比上一篇文章中的顯示和保存功能有不少的性能提升。
使用StretchDIBits函數還可以避免上一篇文章中提到的跨線程訪問控件的問題。
浙公網安備 33010602011771號