写作背景
写这篇文章主要是因为工业相机(海康、大恒等)提供的.NET开发文档和示例程序都是用WinForm项目来说明举例的,而在WPF项目中对图像的使用和处理与在WinForm项目中有很大不同。在WinForm中用System.Drawing.Bitmap来处理图像,而在WPF中是用System.Windows.Media.Imaging.WriteableBitmap来处理图像的。本文的主要内容也是对WriteableBitmap类使用的介绍。
从相机中接收图像
首先当然要创建一个WriteableBitmap,这里以PixelFormats.Bgr24像素格式举例说明
PropertyInfo dpiXProperty = typeof(SystemParameters).GetProperty("DpiX", BindingFlags.NonPublic | BindingFlags.Static);
PropertyInfo dpiYProperty = typeof(SystemParameters).GetProperty("Dpi", BindingFlags.NonPublic | BindingFlags.Static);
int dpiX = (int)dpiXProperty.GetValue(null);
int dpiY = (int)dpiYProperty.GetValue(null);
WriteableBitmap WBitmap = new WriteableBitmap(PhotoWidth, PhotoHeight, dpiX, dpiY, PixelFormats.Bgr24, BitmapPalettes.Halftone256);
接收相机中的照片数据得使用相机SDK提供的方法,一般都是向方法提供一个IntPtr变量,然后相机SDK会将图像数据复制一份到这个内存地址中。
WriteableBitmap对象表示像素数据的地址是WBitmap.BackBuffer。
而在WinForm中的Bitmap则有两种方式接收图像。
一种是创建指定大小和像素格式的Bitmap后使用LockBits获得BitmapData,BitmapData的scan0表示像素数据地址然后和前面的方式一样。
另一种是在创建Bitmap时使用Bitmap(int width, int height, int stride, PixelFormat format, IntPtr scan0)构造函数,使用代表像素数据的IntPtr传给scan0参数即可。
图像的显示
WriteableBitmap使用两个缓冲区,一个后端缓冲区和一个前端缓冲区,所以一个WriteableBitmap对象存着图像的两份数据。前面我们接收图像是把图像存入后端缓冲区中,而界面上Image控件
显示图像用的是前端缓冲区中的图像。所以现在我们需要把后端缓冲区中的数据更新到前端缓冲区中去,然后传给Image的Source属性即可。
WBitmap.Lock();
WBitmap.AddDirtyRect(new Int32Rect(0, 0, PhotoWidth, PhotoHeight));
WBitmap.Unlock();
MyImage.Source = WBitmap;
Lock锁定后端缓冲区,AddDirtyRect将后端缓冲区数据跟新到前端缓冲区,Unlock解锁后端缓冲区。AddDirtyRect的使用模式是固定的,都是先Lock然后Unlock。
像素操作
System.Drawing.Bitmap对象有GetPixel和SetPixel方法,读取、修改某点的像素值很方便。在WriteableBitmap中则需要用指针区操作。在前面【接收图像】中提到用一个指针地址去接受图像,
所以图像的所有像素数据都保存在这个起始地址的内存中,也就是后端缓冲区中。WBitmap.BackBuffer指向的就是坐标(0,0)点的像素数据,以读取(100,200)坐标点的像素数据为例。
先介绍要用到的两个属性:WBitmap.BackBufferStride表示一行图像数据的字节数,WBitmap.Format.BitsPerPixel表示一个像素的位数。
首先计算(100,200)处的偏移量应该是WBitmap.BackBufferStride*200 + WBitmap.Format.BitsPerPixel / 8*100,那么BackBuffer加上偏移量就是(100,200)处的地址 ,所以完整的读取像素值的代码如下:
int offset = WBitmap.BackBufferStride * 200 + PixelFormats.Bgr24.BitsPerPixel / 8 * 100;
unsafe {
byte* pb = (byte*)WBitmap.BackBuffer.ToPointer();
byte cB = pb[offset];
byte cG = pb[offset + 1];
byte cR = pb[offset + 2];
}
或者使用System.Runtime.InteropServices.Marshal.ReadByte,不需要unsafe模式
byte cB = Marshal.ReadByte(WBitmap.BackBuffer, offset);
byte cG = Marshal.ReadByte(WBitmap.BackBuffer, offset+1);
byte cR = Marshal.ReadByte(WBitmap.BackBuffer, offset+2);
像素修改也是同样的方法,把读取变成赋值即可,或者用Marshal.WriteByte写值。
图像的保存
与Bitmap使用Save不同,WriteableBitmap需要使用Encoder编码后才能保存成文件。
using FileStream stream = new FileStream(@"C:\newu8.bmp", FileMode.Create);
BmpBitmapEncoder encoder = new BmpBitmapEncoder();
encoder.Frames.Add(BitmapFrame.Create(WBitmap));
encoder.Save(stream);
这里使用BmpBitmapEncoder编码器来保存bmp图像,要保存成其他格式则使用对应的编码器即可,如JpegBitmapEncoder等。
注意事项
1:像素格式问题,相机SDK提供转化成你需要的格式的方法,在接收图像时要确保两边像素格式一致。相机SDK中提供的像素格式、Bitmap的System.Drawing.Imaging.PixelFormat和WriteableBitmap的System.Windows.Media.PixelFormats对同一像素格式的命名是不同的。比如本文中的PixelFormats.Bgr24对应的是Bitmap中的PixelFormat.Format24bppRgb。可以通过解析同一张图像来确定两者之间的对应关系。
2:使用工业相机采图的方式一般都是使用回调函数的形式,所以在回调函数的多线程环境中执行显示图像的代码要注意控件的跨线程访问问题。
3:图像保存用的是后端缓冲区中的数据(再次证明前端缓冲区只是用来在界面上展示的),意味着只需要在界面上展示图像的时才调用AddDirtyRect。
4:修改部分像素点值后需要在界面上展示的,调用AddDirtyRect方法时Int32Rect参数应该是包含你修改位置的最小面积矩形区域,出于性能考虑不建议使用整个图像区域。