ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

双相机视觉项目实战:C#海康工业相机连接与并发取流全解析

2026/9/3 20:40:04 拓冰建站 浏览量
双相机视觉项目实战:C#海康工业相机连接与并发取流全解析 简介一套基于C#的海康工业相机双机连接示例工程采用WinForms界面面向工业视觉开发者、自动化设备调试人员和C#硬件交互学习者目标是解决两台相机同步连接、独立参数配置与图像采集的常见问题。资源共48个文件压缩包约33.54MB工程内包含9个C#源文件.cs、7个动态库.dll、3个可执行文件.exe另有解决方案、配置文件、资源文件、调试符号及日志等类型覆盖源码、依赖库、编译产物和项目配置结构清晰便于直接打开工程调试运行。已有2852人学习下载。读者可据此快速理解海康SDK的调用流程掌握设备枚举、连接、参数设置、图像抓取、多线程处理以及界面绑定等完整实现同时还能借鉴多相机同步和数据缓冲的写法适合直接套用于自动化检测或视觉定位项目附带调试符号与日志也便于定位相机连接异常减少开发踩坑。1. 项目概述双相机连接这件事到底难在哪先说结论用C#连接两台海康工业相机本身不是一个“从零造轮子”的活真正的难点在于搞清楚SDK的调用套路、资源管理的边界以及两台相机并发取流时的调度策略。这个项目我在实际产线上做过也帮朋友排查过类似问题这里把完整思路和踩坑记录整理出来希望对做上位机开发的朋友有帮助。这个需求的典型场景很明确一条流水线上一台相机拍产品正面另一台拍背面或者一台负责定位一台负责读码。两台相机如果接到同一台工控机上就需要上位机软件能同时枚举、打开、取流、回调处理两路图像。用C#来实现是因为C#在WinForm/WPF下做界面、做通信、对接数据库的效率确实高开发周期短后期维护也方便。适合谁来参考正在用海康MVS SDK做上位机开发的人尤其是第一次接触多相机项目、对SDK调用流程还不够熟悉的C#工程师。我用的方案是海康机器人官方的MVSMachine Vision Software软件配套的SDKC#封装在MvCameraControl.Net这个命名空间下。整体流程说白了就四步枚举设备、创建句柄并打开、注册图像回调、开始取流。两台的逻辑跟一台几乎没有区别区别只在于“区分设备”和“并发处理”这两个环节。2. 核心设计思路为什么两条取流链路必须独立2.1 双相机的连接方式选择USB3.0还是千兆网海康工业相机主流接口有两种GigE千兆网口和USB3.0。做双相机方案时接口选型直接决定代码写法因为SDK枚举设备时返回的MV_CC_DEVICE_INFO结构体里的传输层类型nTLayerType不一样取流逻辑也不同。GigE相机走网口需要配置IP地址能用PoE供电传输距离远适合相机离工控机比较远的产线。USB3.0相机走USB口供电和数据共用一根线带宽上限高理论5Gbps但线缆长度限制在3米以内抗干扰能力弱一些。混合接法一个网口一个USB口也是允许的SDK枚举时两种类型都会返回。我的建议是如果现场没有特殊要求优先用GigE。原因很实际——千兆网卡在工控机上通常有两个口两个相机各插一个可以避免共享带宽导致的丢帧。USB3.0虽然带宽高但如果两个USB相机插在同一个USB控制器下争抢带宽的问题会很头疼。而网口的IP配置相对独立一个网卡配192.168.1.x另一个配192.168.2.x逻辑上也好区分。2.2 取流模式回调模式才是多相机项目的正确选择海康SDK取流有几种方式很多新手一上来就纠结。简单归类方式说明适用场景主动拉流循环调用MV_CC_GetImageBuffer获取图像单相机、逻辑简单的小工具回调模式SDK内部线程收到图像后自动触发回调函数多相机、实时性要求高的产线项目硬触发 回调外部信号触发相机曝光曝光完成后通过回调通知上位机运动控制同步、飞拍、高速检测双相机项目不要用主动拉流。两个相机各开一个线程去循环取图不是不行但代码复杂度高还要自己处理缓冲区和超时问题很容易出隐蔽Bug。我实测下来的最优解是每个相机注册一个独立的回调函数SDK底层用独立线程取流回调里只做“把图像Buffer拷贝出来丢进队列”这件事处理逻辑放到工作线程里。这样做的好处是取流线程永远不会被业务逻辑阻塞相机端不丢帧。2.3 资源模型句柄、回调、缓冲区的生命周期管理这是整个项目最容易出问题的地方。海康SDK的C#封装底层还是P/Invoke调用C接口所以资源管理必须非常小心。核心资源有这么几个设备句柄MyCamera.MV_CC_HANDLE每台相机一个句柄创建后用MV_CC_OpenDevice打开程序退出前必须MV_CC_CloseDeviceMV_CC_DestroyHandle顺序不能反。图像回调注册回调后SDK会为每个相机维护内部取流线程如果MV_CC_StopGrabbing不调用就释放句柄线程可能会悬空。图像数据缓冲区回调里拿到的MV_FRAME_OUT_INFO_EX指向的图像Buffer是SDK内部内存不能保存引用必须马上拷贝到自己申请的托管数组或byte[]里。我见过太多新手在回调里直接把pData指针存到全局变量或者把IntPtr转成Bitmap后不释放跑一晚上内存直接爆掉。这里的关键认知是SDK给你的是一个“临时借用的内存”不是“送你的内存”用完必须立即拷走。3. 双相机连接与并发取流的完整实现3.1 环境准备SDK获取与引用去海康机器人官网下载MVS安装包安装后开发包里自带C#示例。这里注意MVS安装完有两个东西一个是调试用的客户端软件用来手动调焦、测试图像另一个是SDK开发包两者都有用。开发时只需要在C#工程里引用两个东西MvCameraControl.Net.dllSDK的C#封装MvCameraControl.dll底层C接口dll运行时需要放在exe同目录如果你用NuGet搜MvCameraControl.Net也能直接装省去手动复制dll的麻烦。但我的建议是直接用MVS安装目录下的dll因为版本匹配不会出幺蛾子。装完MVS后把dll引用进来代码里using MvCamCtrl.NET;就能用了。3.2 双相机枚举与设备信息打印枚举这一步是所有操作的前提也是排查问题最常用的手段。核心代码其实很短using MvCamCtrl.NET; // 枚举设备 MyCamera.MV_CC_DEVICE_INFO_LIST deviceList new MyCamera.MV_CC_DEVICE_INFO_LIST(); int nRet MyCamera.MV_CC_EnumDevices(MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref deviceList); if (nRet ! MyCamera.MV_OK || deviceList.nDeviceNum 0) { Console.WriteLine(未枚举到设备请检查网线/USB线连接); return; } // 遍历设备信息 for (uint i 0; i deviceList.nDeviceNum; i) { IntPtr pDeviceInfo Marshal.UnsafeAddrOfPinnedArrayElement(deviceList.pDeviceInfo, (int)i); MyCamera.MV_CC_DEVICE_INFO deviceInfo (MyCamera.MV_CC_DEVICE_INFO)Marshal.PtrToStructure(pDeviceInfo, typeof(MyCamera.MV_CC_DEVICE_INFO)); if (deviceInfo.nTLayerType MyCamera.MV_GIGE_DEVICE) { MyCamera.MV_GIGE_DEVICE_INFO gigeInfo (MyCamera.MV_GIGE_DEVICE_INFO)Marshal.PtrToStructure( deviceInfo.SpecialInfo.stGigEInfo, typeof(MyCamera.MV_GIGE_DEVICE_INFO)); Console.WriteLine($相机{i}: GigE, IP{gigeInfo.nCurrentIp}, 厂商{gigeInfo.chManufacturer}); } else if (deviceInfo.nTLayerType MyCamera.MV_USB_DEVICE) { MyCamera.MV_USB_DEVICE_INFO usbInfo (MyCamera.MV_USB_DEVICE_INFO)Marshal.PtrToStructure( deviceInfo.SpecialInfo.stUsb3VInfo, typeof(MyCamera.MV_USB_DEVICE_INFO)); Console.WriteLine($相机{i}: USB3.0, 序列号{usbInfo.chSerialNumber}); } }这段代码里比较讲究的是Marshal.UnsafeAddrOfPinnedArrayElement的用法。deviceList.pDeviceInfo本身是IntPtr数组里面存的是每个设备信息结构体的内存地址要用PtrToStructure抠出来。枚举后的设备序号deviceList里的索引在后续打开设备时是要用到的所以一定要打印出来确认哪台是哪个索引。实际项目里为了稳妥我会让用户通过界面下拉框选择“相机1对应序号几、相机2对应序号几”而不是写死因为网口相机枚举顺序不保证固定。3.3 打开两台相机并分别设置参数枚举到两台相机后分别创建句柄、打开设备、设置参数。这里必须为每台相机准备一个独立的实例不能共用同一个MyCamera对象。我习惯用一个简单的类把核心操作包起来public class HikCamera { private MyCamera _camera new MyCamera(); private string _deviceKey; // GigE用IP标识USB用序列号标识 private nint _handle IntPtr.Zero; public bool Open(string deviceKey) { _deviceKey deviceKey; // 枚举找到匹配的设备 MyCamera.MV_CC_DEVICE_INFO_LIST deviceList new MyCamera.MV_CC_DEVICE_INFO_LIST(); MyCamera.MV_CC_EnumDevices(MyCamera.MV_GIGE_DEVICE | MyCamera.MV_USB_DEVICE, ref deviceList); for (uint i 0; i deviceList.nDeviceNum; i) { IntPtr pDeviceInfo ...; // 取设备信息 string key GetDeviceKey(pDeviceInfo); // 统一提取IP或序列号 if (key deviceKey) { _camera.MV_CC_CreateHandle(ref _handle, pDeviceInfo); int nRet _camera.MV_CC_OpenDevice(_handle); return nRet MyCamera.MV_OK; } } return false; } }参数设置方面产线上最常改的是这几个触发模式TriggerMode工业检测一般设成硬件触发实验室调试设成连续采集、曝光时间ExposureTime微秒为单位、像素格式PixelFormatBayerRG8/G16之类的、帧率上限AcquisitionFrameRate。两个相机参数可能是不同的比如一台曝光500微秒拍正面另一台曝光2000微秒拍暗光背面所以设置参数时一定要确保是在对应的句柄上操作。这个环节常见的低级错误就是把参数设置代码写在枚举循环里结果两个相机被设置成了一样的参数。3.4 注册图像回调并启动取流这是整个项目的核心环节。海康SDK注册回调的函数是MV_CC_RegisterImageCallBackC#里传入一个委托。注意委托不能被GC回收所以要声明成类的成员变量否则运行一段时间后SDK调用回调时可能会因为委托被回收而崩溃。回调注册的完整流程// 定义回调委托签名必须是这样 private MyCamera.cbOutputExdelegate _imageCallback; public void StartGrabbing() { // 回调里只做拷贝把图像数据塞进队列 _imageCallback new MyCamera.cbOutputExdelegate(OnImageCallback); _camera.MV_CC_RegisterImageCallBack(_handle, _imageCallback, _handle); // 设置触发模式为连续采集调试用 _camera.MV_CC_SetEnumValue(_handle, TriggerMode, 0); // 开始取流 int nRet _camera.MV_CC_StartGrabbing(_handle); if (nRet ! MyCamera.MV_OK) { Console.WriteLine($启动取流失败, 错误码0x{nRet:X8}); } } private void OnImageCallback(IntPtr pData, ref MyCamera.MV_FRAME_OUT_INFO_EX pFrameInfo, IntPtr pUser) { // 在回调里立即把图像数据拷贝到托管数组 byte[] buffer new byte[pFrameInfo.nFrameLen]; Marshal.Copy(pData, buffer, 0, (int)pFrameInfo.nFrameLen); // 丢进线程安全队列由工作线程处理 _imageQueue.Enqueue(new FrameData(buffer, pFrameInfo.nWidth, pFrameInfo.nHeight)); }这段代码有两点至关重要。第一回调函数里面绝对不能做耗时操作比如保存图片、跑算法、更新UI这些全部要丢到工作线程里。因为SDK的取流线程是共享的如果回调阻塞了后面的帧就会丢掉。第二pFrameInfo.nFrameLen是回调传出来的图像字节数在拷贝前必须先判断这个值的合法性别一不小心分配一个超大数组造成内存溢出。两台的启动逻辑就是分别调用Camera1.StartGrabbing()和Camera2.StartGrabbing()互不干扰。因为每个HikCamera实例内部都有自己的_camera、_handle、_imageQueue相当于两条完全独立的取流链路。3.5 停止取流与资源释放释放顺序错了程序退出时必崩。正确的释放顺序是public void Close() { // 1. 先停止取流 _camera.MV_CC_StopGrabbing(_handle); // 2. 再关闭设备 _camera.MV_CC_CloseDevice(_handle); // 3. 最后销毁句柄 _camera.MV_CC_DestroyHandle(_handle); _handle IntPtr.Zero; }注意MV_CC_DestroyHandle之后_handle一定要置零不然下次打开时如果忘记重新创建句柄SDK内部会返回错误。另外如果你的程序里还有其他线程在访问_handle释放前必须确保这些线程已经退出或者加锁保护否则会产生并发访问已释放内存的问题表现形式就是偶发的AccessViolationException。4. 常见问题与排查技巧实录4.1 枚举不到设备第一步不是查代码很多人在两台相机连不上时第一反应是去看代码逻辑其实这是最耗时间的做法。我实际调试中总结的排查顺序是先查物理链路再查SDK枚举最后才查代码。物理链路方面GigE相机要看网卡是否有绿色指示灯、IP是否跟相机在同一网段可以用海康MVS客户端软件直接连接验证。如果MVS客户端软件能连接相机但自己的C#程序枚举不到那问题基本出在dll版本或环境变量上比如MVS软件没安装、MvCameraControl.dll不在exe目录、或者NuGet包装的SDK版本跟相机固件不兼容。排查时推荐一个小技巧用MVS客户端软件把相机工作模式调好触发模式、曝光时间、分辨率等然后保存配置文件C#代码里连接时直接加载配置文件能省掉大量参数调试时间。4.2 两个相机连上了但图像卡顿、丢帧严重双相机连接后最常见的性能问题就是丢帧。原因无非以下几种。GigE相机带宽不足两个千兆相机共用一张网卡时总带宽被分成两份高分辨率高帧率下很容易丢帧。解决办法是给每个相机分配独立网卡或者降低帧率/分辨率。USB带宽争抢两个USB3.0相机插在同一个USB控制器上虽然USB3.0理论带宽很高但控制器内部还是有资源竞争。解决办法是把两个相机插到不同的USB控制器上通常在设备管理器里可以查到每个相机的“位置路径”。回调里处理太慢上面说了回调里千万别做耗时操作。如果你的回调里拷贝大数组比如500万像素的Bayer图像拷贝本身耗时很大也容易拖慢取流。优化方案是用对象池复用byte[]避免每次回调都重新分配托管数组。我第二次做双相机项目时吃过一个亏两个相机都是500万像素、30帧回调里我做了图像格式转换Bayer转RGB结果直接掉到12帧。后来把格式转换挪到工作线程帧率才恢复正常。这个经验很重要回调里拷贝数据都要尽量省最好直接传指针和尺寸给工作线程由工作线程拷贝。4.3 相机掉线了怎么办产线环境里相机掉线是躲不开的问题。网线松了、PoE供电不稳定、USB被误拔都可能触发掉线。问题在于SDK的取流线程在掉线时可能会无限等待或抛出异常导致整个程序卡死。我采取的兜底方案是在工作线程里定时通过GetDeviceInfo或主动拉流检测相机状态超时则执行自动重连。重连逻辑简单粗暴——先执行Close()再延迟几百毫秒然后重新枚举设备、重新打开、重新注册回调、重新开始取流。虽然不优雅但稳定可靠。如果你希望更平滑还可以用海康SDK的“断线重连”能力SDK内部会自动尝试恢复链路但实测下来恢复速度不一定满足产线节拍要求所以还是自己写重连更可控。需要注意的是重连时相机的参数设置曝光、触发模式、像素格式会恢复成默认值或掉电保存值所以重连后必须重新设置一遍参数否则拍出来的图可能一片黑或者参数不对。这个坑我踩过特别隐蔽——程序重连成功后看似正常但曝光时间变成了默认值图像曝光严重不足。4.4 程序跑久了内存只涨不降这个问题的根源基本都在回调函数里。很多人把SDK传来的IntPtr直接转成Bitmap后没有调用Dispose()或者每次都new byte[]但没有被GC及时回收。对于一个长期运行的产线上位机程序这些没释放的对象会不断堆积。排查方法也很直接用dotnet-counters或者Visual Studio的诊断工具抓内存快照看byte[]和Bitmap的量是否持续增长。解决思路就三条回调里尽量少分配对象用预先申请好的缓冲区池复用。Bitmap用完立即Dispose()或者用using块。如果是byte[]的GC压力太大可以考虑直接用Marshal.AllocHGlobal申请非托管内存手动管理释放虽然要写释放代码但性能好很多。4.5 两台相机的帧同步问题如果你的项目需求是两台的图像必须严格同步比如3D重建、双目视觉单纯靠软件时间戳是不够的因为两台相机的曝光时刻无法保证一致。这种情况必须用硬件触发相机A和B同时接收同一路外部触发信号或者相机A的输出触发信号接到相机B的输入。海康SDK里设置TriggerSource为Line0或Line1再配置TriggerActivation配合外部接线就能实现帧级同步。如果只是“大概同时拍”的需求两台相机用软件触发也行但要在每次触发前做一次握手——即等待两台相机的取流线程都进入等待状态后再同时触发。这种软件同步的精度在毫秒级能满足大多数不需要严格同步的场景。5. 实操中的一些心得和细节补充最后分享几个我在实际项目里总结的小经验这些内容文档里一般不写但对项目稳定运行帮助很大。第一个是线程安全的UI更新问题。回调线程和工作线程都不能直接操作WinForm控件否则会抛异常。我一般用BeginInvoke或者ConcurrentQueue把要显示的图像传给UI线程UI线程定时器去队列里取。注意这里不要每帧都刷新UI否则刷新太频繁界面会卡我一般控制在15-20帧显示就足够了。第二个是日志记录。产线上的上位机程序一定要有完整的日志系统尤其是相机的开关、掉线、重连、参数修改这些关键事件。排查现场问题时一个带时间戳的日志文件比什么都管用。我用的是NLog配置了文件滚动保留最近30天的日志每天一个文件。第三是部署时把MVS运行库一起打包。MVS软件安装后运行时会依赖一些底层的视觉库和USB驱动如果目标电脑没装MVS光拷dll可能不够。最简单的做法是部署时把MVS安装包一起装一遍或者在海康文档里找到“运行时依赖”的dll清单手动打包过去。这里建议直接用MVS自带的“运行时部署”工具或者进行完整安装省得出莫名其妙的dll缺失问题。第四个细节是关于多线程帧队列的长度控制。如果工作线程处理慢队列会越积越长内存越来越大而且图像延迟会越来越高看起来像卡顿但不丢帧。我通常给队列设置上限比如200帧满了就丢最旧的帧保证实时性。总结下来两台海康工业相机C#连接项目难点不在SDK调用而在资源管理、线程调度和异常兜底。只要把离线枚举、独立句柄、回调快速拷贝、停止释放顺序这几条主线理清楚项目就能跑得很稳。以后再接4台、6台相机方案也是一样往上叠加的无非是设备和队列多几套。如果后面有时间我打算再做一期关于海康相机与VisionPro联合开发的实践分享到时候再细说图像处理那部分的东西。本文还有配套的精品资源点击获取