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函数还可以避免上一篇文章中提到的跨线程访问控件的问题。