在WPF中使用WriteableBitmap对接工业相机及常用操作
写作背景
写这篇文章主要是因为工业相机(海康、大恒等)提供的.NET开发文档和示例程序都是用WinForm项目来说明的,而在WPF项目中对图像的使用和处理与在WinForm项目中有很大不同。在WinForm中用System.Drawing.Bitmap来处理图像,而在WPF中是用System.Windows.Media.Imaging.WriteableBitmap来处理图像的。
本文的主要内容也是对WriteableBitmap类使用的介绍以及与使用Drawing.Bitmap的比较。
从相机中接收图像
首先当然要创建一个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, null);
虽然工业相机有多个品牌,但是获取位图像素数据的方式基本有两种:
1、代表位图像素地址的IntPtr作为相机SDK方法的参数,由SDK方法向该地址写入像素数据。
2、相机SDK方法返回代表位图像素地址的IntPtr。
针对第一种,将WBitmap.BackBuffer传给SDK方法,BackBuffer代表的就是WriteableBitmap对象像素数据的地址。
针对第二种,使用
Int32Rect rect = new Int32Rect(0, 0, wbBitmap.PixelWidth, wbBitmap.PixelHeight);
wbBitmap.WritePixels(rect, ppixel, wbBitmap.PixelWidth * wbBitmap.PixelHeight * 3, wbBitmap.PixelWidth * 3);
WritePixels方法是专门用来修改一个矩形区域中像素数据的方法,其中参数rect代表修改的区域,ppixel代表相机SDK方法返回的代表像素数据的地址。
与Bitmap比较
在WinForm中使用Bitmap则有两种方式接收图像。
针对第一种,使用Bitmap(int width, int height, PixelFormat format)创建Bitmap,然后调用LockBits方法获得BitmapData对象,BitmapData的scan0属性表示图像像素数据地址。
针对第二种,在创建Bitmap时使用Bitmap(int width, int height, int stride, PixelFormat format, IntPtr scan0)构造函数,将ppixel作为scan0的值传入。
图像的显示
WriteableBitmap使用两个缓冲区,一个后端缓冲区和一个前端缓冲区(后端缓冲区用来处理图像像素数据,前端缓冲区用来显示图像),所以一个WriteableBitmap对象存着图像的两份数据。
如果在【从相机中接收图像】中使用第一种方式创建WriteableBitmap,那么图像数据存在后端缓冲区中(BackBuffer),而界面上Image控件显示图像用的是前端缓冲区中的图像。
所以现在我们需要把后端缓冲区中的数据更新到前端缓冲区中去,然后传给Image的Source属性即可。
WBitmap.Lock();
WBitmap.AddDirtyRect(new Int32Rect(0, 0, PhotoWidth, PhotoHeight));
WBitmap.Unlock();
MyImage.Source = WBitmap;
Lock锁定后端缓冲区,AddDirtyRect将后端缓冲区数据更新到前端缓冲区,Unlock解锁后端缓冲区。AddDirtyRect的使用模式是固定的,都是先Lock然后Unlock。
如果在前面【从相机中接收图像】使用的是方式二WritePixels方法,则在图像显示时只需要MyImage.Source = WBitmap即可,因为WritePixels的内部已经调用了AddDirtyRect方法。
与Bitmap比较
WinForm中使用PictureBox控件显示图像。使用方法是:
Image showImage= Image.FromHbitmap(bitmap.GetHbitmap());
MyPictureBox.Image = showImage;
像素操作
WriteableBitmap中的像素操作有两种方式
1、使用像素地址
该方式涉及到代表像素地址的指针。在前面【从相机中接收图像】中方式一提到用一个指针地址去接受图像,
所以图像的所有像素数据都保存在这个起始地址的内存中,也就是后端缓冲区中。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写值。
2、使用WritePixels
WritePixels方法适合修改一个特定矩形内的像素。源像素数据通常来自另一个已生成的图像的数据。WritePixels方法接受IntPtr类型(数据地址)或byte[]类型(数据内容)的值。可参考前面【从相机中接收图像】的例子。
与Bitmap比较
使用Bitmap也有两种方式操作像素。1:Bitmap提供GetPixel和SetPixel方法操作单个像素。2:调用LockBits方法获得BitmapData对象,BitmapData对象的Scan0即像素数据地址。
图像的保存
与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等。
与Bitmap比较
调用Save方法即可。
注意事项
1:工业相机的开发也可以查看C/C++版本的开发文档,C#可以使用DllImport调用C/C++版SDK中的函数。
2:使用工业相机采图一般都是使用回调函数的形式,所以在回调函数的多线程环境中要注意跨线程访问资源的问题。
3:图像保存用的是后端缓冲区中的数据(再次证明前端缓冲区只是用来在界面上展示的)。