上周刚把天津那个汽配厂的视觉检测项目上线连续跑了7天7夜没断过终于能睡个安稳觉了。这个项目前后折腾了我两个月大部分时间都在跟工业相机的各种异常较劲。客户用的是海康威视MV-CA013-20GC千兆网口20帧每秒。一开始我从海康官网下了个Demo改了改就扔现场了结果第一天晚上就炸了。客户凌晨三点给我打电话说相机断连了生产线停了让我赶紧过去。我开车两个小时到现场重启了一下相机就好了但是根本不知道为什么断。接下来的一个星期我每天都要往现场跑一趟有时候一天两趟。我换了网线换了交换机换了电脑甚至把相机寄回厂家检测都说没问题。最后我才发现问题出在我自己写的代码里。断连90%的问题都不是网络的锅我一开始以为断连肯定是网络不稳定毕竟是工业现场电磁干扰大。我把网线换成了屏蔽线交换机换成了工业级的甚至给相机加了个电源滤波器结果该断还是断。直到有一次我在调试的时候发现任务管理器里的句柄数一直在涨每采集一帧就涨两个涨到一万多的时候相机就断了。我当时就懵了我明明释放了资源啊。后来我翻了海康SDK的文档翻到了一个没人注意的角落MV_CC_GetImageBuffer()获取的图像数据必须调用MV_CC_FreeImageBuffer()释放否则会导致句柄泄漏。网上所有的Demo里都他妈没写这一行我当时差点把键盘砸了。那些写Demo的人自己都没跑过72小时以上吧跑几个小时没问题句柄泄漏到一定程度系统就会把相机的连接给干掉。还有一个更坑的地方如果相机异常断开比如网线被拔了你直接调用MV_CC_CloseDevice()是没用的SDK会卡死。必须先调用MV_CC_StopGrabbing()然后等至少500ms再关闭设备。不然句柄就彻底泄漏了下次连接的时候会提示“设备已被占用”只能重启相机。我给你们写个正确的释放流程别再踩这个坑了publicvoidReleaseCamera(){try{// 先停止采集这一步必须有不然SDK会卡死if(_isGrabbing){_isGrabbingfalse;// 这里必须等500ms让采集线程退出Thread.Sleep(500);intretMvSdk.MV_CC_StopGrabbing(_handle);if(ret!MvSdk.MV_OK)Console.WriteLine($停止采集失败错误码:{ret});}// 关闭设备if(_handle!IntPtr.Zero){intretMvSdk.MV_CC_CloseDevice(_handle);if(ret!MvSdk.MV_OK)Console.WriteLine($关闭设备失败错误码:{ret});_handleIntPtr.Zero;}// 销毁SDK实例MvSdk.MV_CC_DestroyHandle(_handle);}catch(Exceptionex){Console.WriteLine($释放相机异常:{ex.Message});// 异常情况下强制置空下次重连_handleIntPtr.Zero;}}句柄泄漏的问题解决之后断连的情况少了很多但还是偶尔会断。这次真的是网络问题了工业现场有时候会有瞬时的网络波动持续几百毫秒SDK自己不会重连。网上很多人说用SDK自带的心跳检测我试过了根本没用。海康的心跳检测是30秒一次而且检测到断开之后不会自动重连。我自己写了个心跳检测机制每秒发一个命令给相机如果连续3次没响应就自动重连。privatevoidHeartbeatThread(){while(_isRunning){try{if(_handleIntPtr.Zero){// 未连接尝试重连ConnectCamera();Thread.Sleep(3000);continue;}// 读取相机温度作为心跳uinttemp0;intretMvSdk.MV_CC_GetIntValue(_handle,DeviceTemperature,reftemp);if(ret!MvSdk.MV_OK){_heartbeatFailCount;Console.WriteLine($心跳失败次数:{_heartbeatFailCount});if(_heartbeatFailCount3){Console.WriteLine(相机连接断开开始重连...);ReleaseCamera();_heartbeatFailCount0;}}else{_heartbeatFailCount0;}}catch(Exceptionex){Console.WriteLine($心跳线程异常:{ex.Message});_heartbeatFailCount;}Thread.Sleep(1000);}}这个心跳线程我跑了一个月从来没出过问题。不管是网线被拔了再插上还是交换机重启相机都会自动重连根本不用人管。对了还有一个坑不要在UI线程里调用任何SDK的方法。我之前犯过这个错误在按钮点击事件里调用MV_CC_OpenDevice()结果有时候SDK会卡几秒钟整个UI就卡死了。所有和相机相关的操作都必须放在单独的线程里。卡顿别把所有活都扔给采集回调断连的问题搞定之后我以为终于能松口气了结果又遇到了更恶心的卡顿。客户要求20帧每秒但是实际跑起来只有10帧左右而且UI特别卡拖动窗口都一顿一顿的。我一开始以为是相机帧率没设对进相机参数里看了明明是20帧。后来我用VS的性能分析工具看了一下发现CPU占用率只有20%但是GC的频率特别高每秒钟都要回收好几次。我当时就知道肯定是内存泄漏了。我翻了一遍代码发现了一个傻逼错误我在采集回调里每次都new一个Bitmap然后传给UI显示但是从来没有Dispose过。// 这是错误的写法会导致严重的内存泄漏和卡顿privatevoidOnImageGrabbed(IntPtrpData,refMvSdk.MV_FRAME_OUT_INFO_EXframeInfo){// 每次都new一个Bitmap从不释放BitmapbmpnewBitmap(frameInfo.nWidth,frameInfo.nHeight,(int)frameInfo.nWidth*3,System.Drawing.Imaging.PixelFormat.Format24bppRgb,pData);// 直接在回调里更新UIpictureBox1.Invoke(newAction((){pictureBox1.Imagebmp;}));}这段代码跑起来内存会以每秒几十MB的速度上涨涨到一定程度GC就会开始回收一回收整个程序就卡一下。而且采集回调是SDK的线程你在回调里做任何耗时操作都会导致SDK的内部缓存堆积最后帧率下降。正确的做法是采集回调只负责把数据复制出来然后扔到队列里图像处理和UI显示都放在别的线程里做。我用BlockingCollection实现了一个生产者消费者模式采集线程是生产者处理线程是消费者。这样采集线程永远不会被阻塞帧率就稳定了。privateBlockingCollectionbyte[]_imageQueuenewBlockingCollectionbyte[](newConcurrentQueuebyte[](),10);privatevoidOnImageGrabbed(IntPtrpData,refMvSdk.MV_FRAME_OUT_INFO_EXframeInfo){try{// 只复制数据不做任何处理intdataSize(int)(frameInfo.nWidth*frameInfo.nHeight*3);byte[]imageDatanewbyte[dataSize];Marshal.Copy(pData,imageData,0,dataSize);// 如果队列满了丢弃最老的一帧if(_imageQueue.Count10){_imageQueue.Take();Console.WriteLine(队列满丢弃一帧);}_imageQueue.Add(imageData);}catch(Exceptionex){Console.WriteLine($采集回调异常:{ex.Message});}}privatevoidProcessThread(){while(_isRunning){try{byte[]imageData_imageQueue.Take();// 在这里做图像处理// ...// 转换为Bitmap并显示using(BitmapbmpnewBitmap(1280,960,1280*3,System.Drawing.Imaging.PixelFormat.Format24bppRgb,Marshal.UnsafeAddrOfPinnedArrayElement(imageData,0))){pictureBox1.Invoke(newAction((){pictureBox1.Image?.Dispose();pictureBox1.ImagenewBitmap(bmp);}));}}catch(Exceptionex){Console.WriteLine($处理线程异常:{ex.Message});}}}改完之后帧率直接稳定在20帧CPU占用率降到了10%UI也不卡了。还有一个能大幅提升性能的技巧用不安全代码直接操作图像数据不要用Bitmap的GetPixel和SetPixel方法。那两个方法慢得要死处理一张1280x960的图片要几十毫秒。我给你们写个灰度化的例子用不安全代码比用GetPixel快100倍以上publicunsafevoidGrayScale(Bitmapbmp){varrectnewRectangle(0,0,bmp.Width,bmp.Height);vardatabmp.LockBits(rect,System.Drawing.Imaging.ImageLockMode.ReadWrite,bmp.PixelFormat);byte*ptr(byte*)data.Scan0;intstridedata.Stride;for(inty0;ybmp.Height;y){for(intx0;xbmp.Width;x){intindexy*stridex*3;byterptr[index2];bytegptr[index1];bytebptr[index];// 灰度化公式bytegray(byte)(r*0.299g*0.587b*0.114);ptr[index]gray;ptr[index1]gray;ptr[index2]gray;}}bmp.UnlockBits(data);}记住所有的图像处理都要放在处理线程里做绝对不要放在采集回调里。采集回调的唯一职责就是把数据复制出来多做一点事情都可能导致卡顿。丢帧最隐蔽也最致命的问题卡顿和断连都是显性的你一眼就能看出来。但是丢帧不一样很多时候丢帧是隐性的客户不说你根本发现不了。我这个项目里客户要检测零件的表面缺陷要求每个零件都要拍一张照片。有一次客户说有时候会有零件漏检我去现场看了半天也没发现问题。直到我加了个帧计数才发现原来相机偶尔会丢帧。丢帧的原因有很多我遇到过的就有这么几种第一个没开巨帧。千兆网相机默认的MTU是1500一张1280x960的RGB图片大概是3.5MB需要分成2000多个包传输。如果开了巨帧MTU设成9000只需要400多个包丢包率会大幅下降。很多人不知道这个设置结果高帧率下丢帧丢得妈都不认识。我之前用Basler的相机180帧每秒没开巨帧的时候丢帧率高达30%开了之后就变成0了。第二个相机缓存太小。海康的SDK默认缓存是10帧如果你的图像处理一帧需要超过50ms20帧每秒的话缓存很快就会满然后SDK就会开始丢帧。你可以通过设置SDK的缓存大小来解决这个问题// 设置缓存大小为30帧intretMvSdk.MV_CC_SetIntValue(_handle,MaxBufferSize,30);if(ret!MvSdk.MV_OK)Console.WriteLine($设置缓存大小失败错误码:{ret});但是缓存也不是越大越好太大的话会导致延迟增加。一般设成帧率的1.5倍就够了。第三个应用层处理不过来。这个是最常见的原因也是最难解决的。如果你的图像处理速度跟不上采集速度队列就会越来越长最后还是会丢帧。解决方法除了优化图像处理代码之外还可以用多线程处理。比如开4个处理线程同时处理队列里的图片。还有一个很重要的点一定要加帧计数校验。相机每一帧都会有一个递增的帧号你可以通过比较当前帧号和上一帧号来判断有没有丢帧。privateuint_lastFrameId0;privatevoidOnImageGrabbed(IntPtrpData,refMvSdk.MV_FRAME_OUT_INFO_EXframeInfo){try{// 帧计数校验if(_lastFrameId!0frameInfo.nFrameNum!_lastFrameId1){Console.WriteLine($丢帧了上一帧:{_lastFrameId}当前帧:{frameInfo.nFrameNum}丢失:{frameInfo.nFrameNum-_lastFrameId-1}帧);}_lastFrameIdframeInfo.nFrameNum;// 复制数据到队列// ...}catch(Exceptionex){Console.WriteLine($采集回调异常:{ex.Message});}}加了帧计数之后有没有丢帧一目了然。我之前就是因为没加这个被客户骂了好几次。对了还有一个坑不要用Wi-Fi连接工业相机。我见过有人图省事用Wi-Fi连接相机结果丢帧丢得一塌糊涂。工业相机必须用有线连接而且最好是千兆网。就先写这么多吧还有一些小坑比如白平衡不对、图像偏色、触发模式不对什么的下次有空再讲。反正工业相机开发就是个踩坑的过程踩多了就熟练了。