左眼水星

导航

在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:图像保存用的是后端缓冲区中的数据(再次证明前端缓冲区只是用来在界面上展示的)。

posted on 2024-06-28 13:38  左眼水星  阅读(829)  评论(0编辑  收藏  举报