C#调用工业相机入门:USB/网口相机图像采集的基础实现
工业相机是机器视觉系统的核心组件,广泛应用于缺陷检测、尺寸测量、条码识别、产品定位等工业场景。对于C#工业上位机开发者来说,掌握工业相机的调用方法是必备技能。
很多新手在刚接触工业相机时,会被各种厂商SDK、不同接口类型搞得晕头转向。本文将从最基础的原理讲起,详细讲解USB和GigE网口这两种最主流工业相机的C#调用方法,所有代码均经过实际验证,可直接用于项目开发。
工业相机基础与选型
在开始写代码之前,我们先了解一下工业相机的基本分类和核心参数,这对后续的开发和选型非常重要。
主流接口类型对比
目前工业现场最常用的是USB3.0和GigE网口两种接口的相机,它们的优缺点和适用场景如下:
| 接口类型 | 传输带宽 | 最大传输距离 | 供电方式 | 易用性 | 适用场景 |
|---|---|---|---|---|---|
| USB3.0 | 5Gbps | 5米 | USB总线供电 | 高 | 近距离、小体积设备、移动检测 |
| GigE | 1Gbps | 100米(网线) | 单独供电/PoE | 中 | 远距离、多相机系统、工业现场 |
核心参数解读
- 分辨率:决定图像的清晰度,常见的有130万、200万、500万像素
- 帧率:每秒采集的图像数量,决定系统的检测速度
- 快门类型:全局快门适合拍摄高速运动物体,卷帘快门成本更低
- 像素深度:通常为8位(灰度)或24位(彩色)
整体开发流程
无论使用哪种接口的相机,其调用流程基本一致,都遵循"连接-配置-采集-显示-释放"的标准流程。
USB相机通用实现(DirectShow)
对于USB相机,最通用的方式是使用DirectShow接口。几乎所有的USB工业相机和普通摄像头都支持DirectShow,不需要安装任何厂商SDK,非常适合入门和快速开发。
环境搭建
我们使用AForge.NET库,它对DirectShow进行了很好的封装,使用起来非常简单。
Install-Package AForge.Video Install-Package AForge.Video.DirectShow核心代码实现
publicclassUsbCamera:IDisposable{privateVideoCaptureDevice_videoDevice;privateBitmap_currentFrame;privatereadonlyobject_frameLock=newobject();// 枚举所有可用的USB相机publicstaticList<string>EnumerateCameras(){varcameras=newList<string>();varvideoDevices=newFilterInfoCollection(FilterCategory.VideoInputDevice);foreach(FilterInfodeviceinvideoDevices){cameras.Add(device.Name);}returncameras;}// 连接相机publicvoidConnect(intcameraIndex){varvideoDevices=newFilterInfoCollection(FilterCategory.VideoInputDevice);if(cameraIndex<0||cameraIndex>=videoDevices.Count)thrownewArgumentException("无效的相机索引");_videoDevice=newVideoCaptureDevice(videoDevices[cameraIndex].MonikerString);// 设置分辨率和帧率(根据相机支持的参数选择)_videoDevice.VideoResolution=_videoDevice.VideoCapabilities[0];// 注册新帧事件_videoDevice.NewFrame+=VideoDevice_NewFrame;}// 启动采集publicvoidStart(){if(_videoDevice==null||_videoDevice.IsRunning)return;_videoDevice.Start();}// 停止采集publicvoidStop(){if(_videoDevice==null||!_videoDevice.IsRunning)return;_videoDevice.SignalToStop();_videoDevice.WaitForStop();}// 获取当前帧publicBitmapGetCurrentFrame(){lock(_frameLock){return_currentFrame?.Clone()asBitmap;}}privatevoidVideoDevice_NewFrame(objectsender,NewFrameEventArgseventArgs){lock(_frameLock){_currentFrame?.Dispose();_currentFrame=(Bitmap)eventArgs.Frame.Clone();}}publicvoidDispose(){Stop();_videoDevice?.NewFrame-=VideoDevice_NewFrame;_currentFrame?.Dispose();}}使用示例(WinForm)
privateUsbCamera_camera;privatevoidForm1_Load(objectsender,EventArgse){// 枚举相机并添加到下拉框varcameras=UsbCamera.EnumerateCameras();comboBoxCameras.Items.AddRange(cameras.ToArray());if(cameras.Count>0)comboBoxCameras.SelectedIndex=0;}privatevoidbtnStart_Click(objectsender,EventArgse){_camera=newUsbCamera();_camera.Connect(comboBoxCameras.SelectedIndex);_camera.Start();// 定时器刷新显示timerDisplay.Start();}privatevoidtimerDisplay_Tick(objectsender,EventArgse){varframe=_camera.GetCurrentFrame();if(frame!=null){pictureBox.Image?.Dispose();pictureBox.Image=frame;}}常见问题
- 图像卡顿:不要在NewFrame事件中直接操作UI控件,应该使用定时器异步刷新
- 内存泄漏:一定要及时释放Bitmap对象,否则会导致内存快速增长
- 分辨率设置:必须选择相机支持的分辨率,否则会抛出异常
GigE网口相机实现(海康MVS SDK)
GigE网口相机是工业现场的主流选择,几乎所有的工业相机厂商都提供了自己的SDK。这里以国内最常用的海康威视MVS SDK为例,讲解网口相机的调用方法。
环境搭建
- 下载并安装海康威视MVS客户端软件
- 在项目中引用MVS SDK提供的
MvCameraControl.Net.dll - 将SDK目录下的所有dll文件复制到项目输出目录
核心代码实现
publicclassHikGigeCamera:IDisposable{privateIntPtr_handle=IntPtr.Zero;privateBitmap_currentFrame;privatereadonlyobject_frameLock=newobject();privatebool_isGrabbing=false;// 枚举所有可用的GigE相机publicstaticList<string>EnumerateCameras(){varcameras=newList<string>();vardeviceList=newMV_CC_DEVICE_INFO_LIST();varstatus=MvCameraControl.MV_CC_EnumDevices_NET(MvCameraControl.MV_GIGE_DEVICE,refdeviceList);if(status!=MvCameraControl.MV_OK)thrownewException($"枚举相机失败,错误码:{status}");for(inti=0;i<deviceList.nDeviceNum;i++){vardeviceInfo=(MV_CC_DEVICE_INFO)Marshal.PtrToStructure(deviceList.pDeviceInfo[i],typeof(MV_CC_DEVICE_INFO));varip=$"{deviceInfo.SpecialInfo.stGigEInfo.nCurrentIp&0xFF}."+$"{(deviceInfo.SpecialInfo.stGigEInfo.nCurrentIp>>8)&0xFF}."+$"{(deviceInfo.SpecialInfo.stGigEInfo.nCurrentIp>>16)&0xFF}."+$"{(deviceInfo.SpecialInfo.stGigEInfo.nCurrentIp>>24)&0xFF}";cameras.Add($"{deviceInfo.SpecialInfo.stGigEInfo.chModelName}({ip})");}returncameras;}// 连接相机publicvoidConnect(intcameraIndex){vardeviceList=newMV_CC_DEVICE_INFO_LIST();MvCameraControl.MV_CC_EnumDevices_NET(MvCameraControl.MV_GIGE_DEVICE,refdeviceList);if(cameraIndex<0||cameraIndex>=deviceList.nDeviceNum)thrownewArgumentException("无效的相机索引");// 创建句柄varstatus=MvCameraControl.MV_CC_CreateDevice_NET(ref_handle,deviceList.pDeviceInfo[cameraIndex]);if(status!=MvCameraControl.MV_OK)thrownewException($"创建设备失败,错误码:{status}");// 打开设备status=MvCameraControl.MV_CC_OpenDevice_NET(_handle);if(status!=MvCameraControl.MV_OK)thrownewException($"打开设备失败,错误码:{status}");// 注册图像回调status=MvCameraControl.MV_CC_RegisterImageCallBack_NET(_handle,ImageCallBack,IntPtr.Zero);if(status!=MvCameraControl.MV_OK)thrownewException($"注册回调失败,错误码:{status}");}// 设置触发模式publicvoidSetTriggerMode(boolenable){varmode=enable?1:0;MvCameraControl.MV_CC_SetEnumValue_NET(_handle,"TriggerMode",(uint)mode);if(enable){// 设置触发源为软触发MvCameraControl.MV_CC_SetEnumValue_NET(_handle,"TriggerSource",7);}}// 软触发一次publicvoidTriggerOnce(){if(!_isGrabbing)return;MvCameraControl.MV_CC_TriggerSoftwareExecute_NET(_handle);}// 启动采集publicvoidStartGrabbing(){if(_handle==IntPtr.Zero||_isGrabbing)return;varstatus=MvCameraControl.MV_CC_StartGrabbing_NET(_handle);if(status==MvCameraControl.MV_OK)_isGrabbing=true;}// 停止采集publicvoidStopGrabbing(){if(_handle==IntPtr.Zero||!_isGrabbing)return;MvCameraControl.MV_CC_StopGrabbing_NET(_handle);_isGrabbing=false;}// 获取当前帧publicBitmapGetCurrentFrame(){lock(_frameLock){return_currentFrame?.Clone()asBitmap;}}privatevoidImageCallBack(IntPtrpData,refMV_FRAME_OUT_INFO_EXpFrameInfo,IntPtrpUser){lock(_frameLock){_currentFrame?.Dispose();// 将原始数据转换为Bitmapvardata=newbyte[pFrameInfo.nFrameLen];Marshal.Copy(pData,data,0,(int)pFrameInfo.nFrameLen);unsafe{fixed(byte*pDataPtr=data){_currentFrame=newBitmap((int)pFrameInfo.nWidth,(int)pFrameInfo.nHeight,(int)pFrameInfo.nWidth*3,System.Drawing.Imaging.PixelFormat.Format24bppRgb,(IntPtr)pDataPtr);}}}}publicvoidDispose(){StopGrabbing();if(_handle!=IntPtr.Zero){MvCameraControl.MV_CC_CloseDevice_NET(_handle);MvCameraControl.MV_CC_DestroyDevice_NET(_handle);_handle=IntPtr.Zero;}_currentFrame?.Dispose();}}关键注意事项
- SDK版本匹配:必须使用与MVS客户端相同版本的SDK,否则会出现各种奇怪的问题
- 网络配置:GigE相机需要与工控机在同一网段,建议使用静态IP
- 巨帧设置:为了提高传输效率,建议在网卡属性中开启巨帧(Jumbo Frame)
- 多线程安全:图像回调是在SDK的线程中执行的,访问共享资源时必须加锁
图像显示与保存
WinForm显示
如前面的示例所示,使用PictureBox控件显示图像即可。需要注意的是,不要在UI线程中进行耗时的图像处理操作。
WPF显示
在WPF中显示Bitmap需要进行格式转换:
publicstaticBitmapSourceConvertToBitmapSource(Bitmapbitmap){varbitmapData=bitmap.LockBits(newRectangle(0,0,bitmap.Width,bitmap.Height),ImageLockMode.ReadOnly,bitmap.PixelFormat);varbitmapSource=BitmapSource.Create(bitmapData.Width,bitmapData.Height,bitmap.HorizontalResolution,bitmap.VerticalResolution,PixelFormats.Bgr24,null,bitmapData.Scan0,bitmapData.Stride*bitmapData.Height,bitmapData.Stride);bitmap.UnlockBits(bitmapData);returnbitmapSource;}图像保存
publicvoidSaveImage(stringpath,Bitmapimage){image.Save(path,ImageFormat.Bmp);}工业级稳定性优化
从demo到生产环境,还需要进行以下几个方面的优化:
- 多线程采集:将相机采集和图像处理放在单独的线程中,避免阻塞UI线程
- 异常处理:为所有相机操作添加try-catch块,记录详细的异常信息
- 自动重连:当相机连接断开时,系统能够自动尝试重连
- 内存管理:及时释放所有IDisposable对象,避免内存泄漏
- 心跳检测:定期检测相机的连接状态,及时发现设备故障
常见问题排查
相机连接不上
- 检查相机电源和网线是否连接正常
- 检查工控机和相机是否在同一网段
- 关闭防火墙或添加例外
- 重启相机和工控机
图像不显示
- 检查是否调用了StartGrabbing方法
- 检查图像回调是否正常触发
- 检查图像格式是否正确
采集卡顿或丢帧
- 开启网卡巨帧
- 降低相机分辨率或帧率
- 优化图像处理代码
- 使用性能更好的工控机
内存泄漏
- 确保所有Bitmap对象都被正确释放
- 不要在循环中创建大量临时对象
- 使用内存分析工具定位泄漏点
总结
本文详细讲解了USB和GigE网口两种主流工业相机的C#调用方法,从基础原理到核心代码,再到常见问题排查,覆盖了入门开发的所有环节。
需要注意的是,不同厂商的SDK虽然API不同,但调用流程基本一致。掌握了海康SDK的使用方法,再去学习其他厂商(如Basler、大华、大恒)的SDK就会非常容易。
在实际项目中,我们还需要结合具体的应用场景,进行相机参数优化、图像处理算法开发和系统稳定性测试,才能打造出真正可用的工业视觉系统。
