☰
C#接入海康机器人工业相机SDK:环境配置、取流与排障
2026/10/24 5:47:35 网站建设 项目流程

简介:面向使用 C# 开发工业视觉采集程序的工程师与学习者,这份海康机器人工业相机 SDK 示例工程围绕回调取图与软触发取图两条主线展开,可帮助读者理解相机初始化、参数配置、事件回调、触发控制、图像数据接收与释放的基本流程。压缩包共 71 个文件、约 786KB,以 C# 源码、DLL 依赖、resx 资源、JSON/XML 配置及说明文本为主;工程采用接口、基类与海康具体实现分层组织,包含相机抽象接口、基础相机类、海康 SDK 封装、配置模型和 WinForms 演示界面等模块,便于按层次阅读、对照调试与二次改造。已有 2638 人学习,适合需要快速搭建采集演示、梳理 SDK 调用链路、验证回调与软触发逻辑,或排查取图流程问题的开发者参考。

1. 从「MVS 能看到图,C# 里枚举不到设备」说起

第一次在 C# 里接海康机器人工业相机 SDK,十有八九不是被 API 难住,而是被一个DllNotFoundException或者nDeviceNum == 0挡在门外:MVS 客户端里画面好好的,代码里MV_CC_EnumDevices_NET却一个设备都返回不了,甚至直接抛“找不到 MvCameraControl.dll”。这套 SDK 的难点不在接口,而在它横跨三层——相机与网卡、MVS 客户端与驱动、C# 工程的平台目标和输出目录,任何一层没对齐,报出来的现象都长得差不多。

这篇笔记按我自己部署的顺序写:先把环境列清,再引 SDK 写第一帧,然后讲曝光、触发、网卡这几组参数怎么落到位,最后落到怎么验证这套环境是真跑通了而不是“看起来能跑”。适合刚拿到相机、要在 WinForms 或 WPF 里出第一张图的人。

2. 环境准备:MVS 客户端、SDK 目录与 C# 工程的对齐

装这套东西最忌讳的是“先写代码,跑不通再回头找驱动”。工业相机和普通 USB 摄像头不一样,它的链路是「相机 → 网卡/USB 控制器 → 系统驱动 → SDK 动态库 → 你的托管代码」,中间断一节,上层表现都是同一句“枚举不到”。所以我把环境准备拆成三个决定:机器上装什么、SDK 目录里拿什么、工程怎么配。

2.1 开发机和部署机该装什么

开发机上我一般直接装完整的 MVS 客户端,而不是只拷 SDK 文件夹。原因很实际:MVS 安装包顺手把 GigE 的过滤器驱动、USB3.0 相机驱动、以及一堆注册表项都装好了,客户端本身还是个现成的硬件自检工具——相机供电、IP、固件版本、能否出图,全在客户端里一眼看到。跳过客户端只拷 dll,等于把唯一的对照组扔了,后面出问题你连“是相机的问题还是代码的问题”都判断不了。

部署机(也就是最终跑你程序的那台工控机)有两种形态,取舍很清楚:

部署形态装什么优点代价
完整安装同版本 MVS 客户端排障工具齐全,驱动版本一致安装包大,开机自启项多,需要手动关
精简部署程序目录下自带 SDK 运行库干净、免安装、好做版本管理要自己补齐 VC 运行库和驱动

我倾向开发期一律完整安装,等程序定型了再做精简部署,并且精简部署的那台机器上仍然保留一份 MVS 客户端安装包,出问题随时能装上做对照。

版本上有一条硬规矩:MVS 客户端的版本不要低于你编译时用的 SDK 版本。SDK 目录是跟着客户端一起装的,客户端升级会把Development目录整个换掉,你在工程里引用的那个 dll 路径如果写的是绝对路径,升级当天就会编译不过。所以我在工程里从来不写绝对路径引用,一律把 dll 拷进工程目录下的Libs文件夹再引用,客户端升级后手动对比一次版本再决定要不要换。

2.2 SDK 目录里 C# 真正要用的只有这几个文件

MVS 装完之后,SDK 在安装目录的Development下面。这个目录里东西很多,C++、C#、Python 的示例混在一起,第一次进去容易懵。C# 侧真正要动的其实就两块:托管库和纯声明的源码文件。

位置(相对 Development)内容C# 工程是否需要
DotNet 下的托管库MvCameraControl.Net.dll 及同名 xml必需,作为引用
DotNet 下的 MvImport 目录结构体、枚举、常量的 C# 声明源码必需,作为源码加入工程
头文件目录C/C++ 头文件不需要
库目录下的 win64/win32原生动态库运行时需要,随程序输出
示例目录各语言的 Demo建议留着当对照

这里有个新手常犯的误解:以为只引MvCameraControl.Net.dll就够了。实际上托管库里是MvCamera这个类的方法实现,而方法签名里用到的设备信息结构体、像素格式枚举、错误码常量,全都定义在MvImport的那几个.cs文件里。不把源码加进工程,编译时就会一片“找不到类型或命名空间”。这几个文件是纯声明,没有业务逻辑,加进工程不会污染代码结构,我通常单独建一个SdkDeclare文件夹放它们,和自己的代码隔开。

提示:MvImport里的声明文件是跟着 SDK 版本走的,客户端升级后不要图省事继续用旧的声明文件,结构体字段一变,内存布局对不上,表现出来就是取流时读到一堆乱码或者直接访问越界崩溃。

2.3 平台目标:AnyCPU 是这套 SDK 最大的陷阱

VS 新建 C# 工程默认是 AnyCPU,而且在 64 位系统上默认勾了“首选 32 位”。这两个默认值凑在一起,就是BadImageFormatException的温床——你的进程实际跑成了 32 位,却去加载 64 位的原生库,加载器直接拒绝。

我自己的固定做法是:新建工程第一件事,打开配置管理器,把平台目标改成 x64,顺手把“首选 32 位”的勾去掉。.NET Framework 的工程改项目属性里的“目标平台”即可;.NET 6/8 的工程则是在项目文件里锁运行时标识:

<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <!-- 平台目标锁死 x64,避免 AnyCPU 在 64 位系统上跑成 32 位进程 --> <PlatformTarget>x64</PlatformTarget> <!-- 关闭 32 位首选,这是 AnyCPU 最坑的一个默认值 --> <Prefer32Bit>false</Prefer32Bit> <!-- 发布时带上 x64 运行时标识,原生库才找得到 --> <RuntimeIdentifier>win-x64</RuntimeIdentifier> </PropertyGroup> </Project>

PlatformTarget决定编译产物是 64 位还是 32 位;Prefer32Bit只对 AnyCPU 生效,设成 false 才能让 AnyCPU 在 64 位系统上真正跑成 64 位;RuntimeIdentifier影响的是 .NET 新版本的输出目录结构和原生库解析路径,不写的话在某些发布模式下原生 dll 不会被复制到输出目录。

还有一层容易漏的:原生 dll 的复制。托管库引用进来之后,“复制到本地”默认是 true,但原生库不一定跟着走。我的做法是在项目文件里加一段复制任务,把 SDK 的原生库按平台拷到输出目录:

<Target Name="CopyNativeSdk" AfterTargets="Build"> <ItemGroup> <!-- 路径指向你本机 MVS 安装目录下的原生库,按实际版本替换 --> <SdkNative Include="$(SdkRoot)\Libraries\win64\*.dll" /> </ItemGroup> <Copy SourceFiles="@(SdkNative)" DestinationFolder="$(OutDir)" SkipUnchangedFiles="true" /> </Target>

AfterTargets="Build"保证在编译完成后执行;SkipUnchangedFiles让增量编译不至于每次都全量拷贝;$(SdkRoot)这种变量我一般写在项目文件顶部或者用环境变量传,绝不在多台机器上硬编码同一个绝对路径。团队协作时这一条尤其重要——你机器上 SDK 装在默认位置,别人的可能装在 D 盘,硬编码路径的工程在别人那儿第一步就红。

3. C# 工程接入 SDK:从引用到出第一帧

环境对齐之后,剩下的就是让代码真的拿到一帧图。这一段我按“接线 → 枚举 → 取流 → 显示 → 释放”的顺序写,每一段都给出能直接抄的骨架,同时说明哪些地方必须按你手上的 SDK 版本改。

3.1 引 dll 还是拖源码:两种接法的差别

前面说过托管库和声明源码都要,但“怎么引”有两种常见接法,选错了后期很麻烦。

第一种是引用编译好的托管 dll,把MvImport的源码文件也加进工程。这是我用得最多的一种:编译快,SDK 升级只要替换 dll,声明文件同步替换即可,工程结构也干净。

第二种是把 SDK 示例里的封装类整个拷进工程,直接在源码层面改。好处是调试时能单步进封装逻辑,坏处是升级 SDK 时你的改动会被覆盖,而且一旦改出问题,排查范围从“调 SDK”变成了“调 SDK 加调自己改的封装”。

我一般前期用第一种,等业务逻辑稳定了再考虑要不要封一层自己的相机管理类。至于声明源码里的命名空间,不同大版本之间会有调整,代码里using的那一行如果编译不过,去MvImport目录里搜一下namespace关键字,按实际名字改就行,不要凭记忆写。

3.2 枚举设备与打开设备的骨架

枚举这一段是整个接入过程里最能暴露环境问题的地方,因为它同时依赖驱动、网络和 dll 位数。我习惯把返回值拆开判断,而不是一句“没找到设备”糊过去:

// 设备列表结构由 MvImport 里的声明文件定义,字段名以你手上的版本为准 var devList = new MV_CC_DEVICE_INFO_LIST(); // 同时枚举网口相机和 USB 相机;返回非 0 表示枚举接口本身失败 int nRet = MvCamera.MV_CC_EnumDevices_NET( MV_GIGE_DEVICE | MV_USB_DEVICE, ref devList); if (nRet != 0) { // 接口失败:优先怀疑 dll 位数不对、原生库没拷到输出目录、驱动没装 throw new InvalidOperationException($"枚举接口失败,错误码 0x{nRet:X8}"); } if (devList.nDeviceNum == 0) { // 接口成功但没设备:优先怀疑网段、供电、网卡选择、防火墙 throw new InvalidOperationException("枚举成功但设备数为 0"); }

MV_GIGE_DEVICE和MV_USB_DEVICE是设备类型掩码,按位或起来表示两类都枚举;nDeviceNum是实际找到的数量。把这两种失败分开,排障方向立刻就不一样了:前者是软件环境问题,后者是物理链路问题。

打开设备的顺序是「创建设备句柄 → 打开 → 按需配置 → 开始取流」,每一步失败都要把已经创建的句柄销毁掉,否则连续重试几次就会泄漏句柄,最后连 MVS 客户端都连不上相机,只能重启。这套顺序我写成固定模板:

IntPtr handle = IntPtr.Zero; try { // 1. 用设备信息创建设备句柄 nRet = MvCamera.MV_CC_CreateDevice_NET(ref devInfo, ref handle); if (nRet != 0) throw new Exception($"创建设备失败 0x{nRet:X8}"); // 2. 打开设备,内部会完成与相机的握手 nRet = MvCamera.MV_CC_OpenDevice_NET(handle, MV_ACCESS_Exclusive, 0); if (nRet != 0) throw new Exception($"打开设备失败 0x{nRet:X8}"); // 3. 网口相机建议在打开后立刻下发最优包长,减少丢包 int nPacketSize = MvCamera.MV_CC_GetOptimalPacketSize_NET(handle); if (nPacketSize > 0) MvCamera.MV_CC_SetIntValue_NET(handle, "GevSCPSPacketSize", (uint)nPacketSize); } catch { // 任何一步失败都要回滚,否则句柄泄漏 if (handle != IntPtr.Zero) { MvCamera.MV_CC_CloseDevice_NET(handle); MvCamera.MV_CC_DestroyDevice_NET(handle); } throw; }

MV_ACCESS_Exclusive表示独占访问,同一台相机被 MVS 客户端占着的时候,你的程序打开会失败——这是最常见的一种“代码没问题但就是打不开”,关掉客户端就好。GevSCPSPacketSize是网口相机的包长,用MV_CC_GetOptimalPacketSize_NET拿系统推荐值比自己拍脑袋填 9000 靠谱得多,尤其在不是专用网卡的机器上。

3.3 取流的两种方式:回调与主动取流

取流有两种主流写法,选哪种取决于你的业务节奏。

回调方式是注册一个图像回调,SDK 内部的工作线程采到图就调你。优点是省事、不容易漏帧;缺点是回调线程不是 UI 线程,在里面做耗时操作会拖慢整个采集链路,直接更新控件还会抛跨线程异常。我的习惯是在回调里只做一件事:把图像数据拷进一个预分配的缓冲区,然后丢进BlockingCollection或者环形队列,真正的处理和显示放到消费线程。

主动取流是自己起一个线程循环调取帧接口,带超时。优点是节奏完全可控,适合做“取一帧、算一次、再取一帧”的检测流程;缺点是要自己管线程生命周期,超时设短了会频繁返回超时错误,设长了退出时又卡住。

取流方式适合场景主要风险
回调取流连续采集、预览、录像回调里做重活导致丢帧
主动取流触发式检测、单帧处理超时参数与退出逻辑写不好
混合预览走回调、检测走主动两种方式抢同一台相机,必须二选一

这里有个血泪经验:同一台相机不要同时用两种方式取流。有些型号不会报错,而是安静地给你一半的帧,你查半天以为是网络丢包。

3.4 像素格式转换与 Bitmap 显示

SDK 采上来的原始数据不一定是 BGR,黑白相机多半是 Mono8,彩色相机常见的是 Bayer 格式。直接把 Bayer 数据塞进Bitmap会得到一张彩色噪点图,很多人第一次看到那画面以为相机坏了。

正确做法是先查当前像素格式,需要的话用转换接口转成BGR8Packed再构造位图:

// 查询当前像素格式,不同型号默认值不同,不要假设 var pixelType = new MVCC_ENUMVALUE(); MvCamera.MV_CC_GetEnumValue_NET(handle, "PixelFormat", ref pixelType); // 目标格式统一成 BGR8Packed,方便直接构造 Bitmap var dstInfo = new MVCC_PIXEL_TYPE_INFO { enPixelType = PixelType_Gvsp_BGR8_Packed }; // 转换接口会按目标格式计算所需缓冲区大小 MvCamera.MV_CC_ConvertPixelType_NET(handle, ref dstInfo);

MV_CC_GetEnumValue_NET拿到的nCurValue就是当前像素格式的枚举值,MV_CC_ConvertPixelType_NET的第二个参数既是输入也是输出,转换完成后从里面取缓冲区指针和实际大小。构造Bitmap时 stride 要用「宽度 × 每像素字节数」并且按 4 字节对齐,Mono8 是 1 字节、BGR8 是 3 字节,对齐算错的表现是图像斜着撕开,像老电视的场不同步。

3.5 关闭顺序:反过来写就会崩

释放顺序必须是「停流 → 反注册回调 → 关设备 → 销毁句柄」。我见过最典型的翻车是只调了关设备没停流,程序退出时 SDK 的工作线程还在往一块已经释放的内存里写数据,表现是偶发的退出崩溃,日志里什么都没有,纯黑匣子。所以退出逻辑我固定写成一个方法,并且用标志位保证只执行一次。

4. 参数配置:曝光、增益、触发与网络四组开关

代码能出图只是起点,真正决定这套环境能不能用在产线上的,是参数配置。SDK 的参数体系是“节点名 + 类型”的模型,浮点、整型、枚举、命令各有对应的读写接口,节点名写错不会编译报错,只会在运行时返回一个错误码。

4.1 用节点名读写参数

几个我每天都在用的节点:

节点名类型单位/取值说明
ExposureTime浮点微秒曝光时间,配合自动曝光使用
ExposureAuto枚举0 关闭 / 1 一次 / 2 连续自动曝光模式
Gain浮点dB增益,越大噪声越明显
AcquisitionFrameRate浮点帧每秒需先关闭帧率使能才生效
TriggerMode枚举0 关 / 1 开触发总开关
TriggerSource枚举软触发 / Line0 等触发源
GevSCPSacketSize整型字节网口包长,别超过网卡巨帧

写参数的固定套路是「先读再写再校验」:读一次确认节点存在且可写,写一次,再读回来确认值真的变了。有些节点在取流过程中是只读的,写入会返回错误码,不看返回值就会出现“我明明设了曝光 5ms,图还是过曝”。

// 关掉自动曝光,否则手动设的曝光值会被自动逻辑覆盖 MvCamera.MV_CC_SetEnumValue_NET(handle, "ExposureAuto", 0); // 设置曝光时间,单位微秒,5000 即 5ms int nRet = MvCamera.MV_CC_SetFloatValue_NET(handle, "ExposureTime", 5000f); if (nRet != 0) { // 常见原因:相机正在取流且该节点运行中不可写,先停流再设 throw new Exception($"曝光设置失败 0x{nRet:X8}"); } // 回读校验,确认参数真的落地了 var fValue = new MVCC_FLOATVALUE(); MvCamera.MV_CC_GetFloatValue_NET(handle, "ExposureTime", ref fValue);

MV_CC_SetEnumValue_NET的第三个参数是枚举项的整型值,具体取值在MvImport的声明里能查到;MVCC_FLOATVALUE回读时不仅拿到当前值,还能拿到最小值和最大值,做参数界面时直接把这两个值绑到滑块范围上,比硬编码安全。

4.2 触发模式:配置顺序错了就是不出图

从连续采集切到触发采集,顺序不能乱:先停流,再改TriggerMode,再改TriggerSource,最后重新开始取流。部分型号在取流过程中允许改触发源,但改TriggerMode会直接返回错误;更麻烦的是有些型号不报错,而是把触发模式改了却仍然按自由运行出图,你在那儿等软触发等不到,图却一直在刷。

软触发的写法是往命令节点写一次:

// 前置条件:已停流,且 TriggerMode 已置为 1、TriggerSource 为软触发 MvCamera.MV_CC_SetEnumValue_NET(handle, "TriggerMode", 1); MvCamera.MV_CC_SetEnumValue_NET(handle, "TriggerSource", 7); // 软触发 // 重新开始取流后,每次需要一帧就往命令节点发一次 MvCamera.MV_CC_SetCommandValue_NET(handle, "TriggerSoftware");

命令节点的写接口和普通参数不一样,用的是MV_CC_SetCommandValue_NET,写进去的值被忽略,只起到“执行一次”的作用。硬触发则要把TriggerSource改成对应的 Line 口,并且确认 IO 电平匹配——这一块翻车基本都是接线和电平问题,不是代码问题。

4.3 网卡与相机 IP:九成的“枚举不到”在这

网口相机的链路比 USB 复杂得多,我按出现频率排:相机和网卡不在同一网段、机器上有多张网卡导致选错出口、网卡没开巨帧、防火墙拦了发现协议。

同一网段这条听起来废话,实际现场经常踩——工控机上有两块网卡,一块连厂内网拿了 DHCP 地址,一块连相机,你只改了相机 IP,没给相机那块网卡配静态地址,系统就永远从另一块网卡出去找。我的做法是给相机单独一块网卡,配一个和相机同网段的静态地址,并且在网卡高级设置里把巨帧开到 9014,流控关掉。

防火墙这块有个很典型的症状:MVS 客户端能看到相机,你自己的程序枚举不到。客户端在安装时已经把自己的规则加进白名单了,你的 exe 是新的,第一次运行时被拦了。私有网络放行一次即可,别图省事整个关掉。

4.4 心跳与断线重连

产线环境里网线被碰掉、交换机重启都是常事,代码不处理就是整个程序卡死在那等一帧永远不来的图。我的做法是打开设备后设置心跳超时,并注册异常回调:

// 心跳超时设成 3000ms,网线拔掉后大约这个时间能收到断线回调 MvCamera.MV_CC_SetIntValue_NET(handle, "GevHeartbeatTimeout", 3000); // 注册异常回调,回调里只做标记,重连逻辑放到独立线程 MvCamera.MV_CC_RegisterExceptionCallBack_NET(handle, OnException, IntPtr.Zero);

回调里千万不要直接重建连接,SDK 的回调线程上做销毁动作很容易死锁。我一般只置一个volatile bool标志,由一个后台线程轮询这个标志,走完整的「停流 → 关设备 → 销毁 → 重新枚举 → 打开」流程,重连间隔从 500ms 起步,连续失败就退避到几秒。

5. 排查:装完跑不起来的五个现场

下面这五条是我在不同机器上重复遇到过的,按「现象 → 原因 → 解决」记,遇到同类问题可以先对号入座。

5.1 DllNotFoundException 与 BadImageFormatException

现象:编译通过,一运行就抛DllNotFoundException: 无法加载 MvCameraControl.dll,或者更迷惑的BadImageFormatException: 试图加载格式不正确的程序。

原因:前者是原生库压根没被复制到输出目录,或者进程的 dll 搜索路径里找不到;后者几乎都是位数不匹配——进程是 32 位,原生库是 64 位(或反过来)。

解决:先确认输出目录里有没有原生 dll,没有就补复制任务;再确认平台目标。查位数最直接的办法是任务管理器看进程有没有“32 位”标注,或者代码里打印IntPtr.Size,4 是 32 位、8 是 64 位。这一条我踩过不止一次,AnyCPU 加“首选 32 位”的组合是惯犯。

5.2 枚举成功但设备数为 0

现象:MV_CC_EnumDevices_NET返回 0,nDeviceNum也是 0,MVS 客户端里相机却好好的。

原因:网口相机多半是网段或网卡选择问题;USB 相机多半是供电不足或者被别的进程占用。

解决:先用 MVS 客户端确认能出图,排除硬件;再检查相机所在网卡的 IP 是否同网段;然后临时禁用其他网卡排除多网卡干扰;最后看防火墙。USB 相机换一个后置的 USB3.0 口,别用前面板或者 Hub。

5.3 打开成功但取流超时

现象:设备打开返回 0,MV_CC_StartGrabbing_NET也成功,但取帧一直超时,一帧都拿不到。

原因:曝光时间设得太长(比如几十万微秒)导致出图周期远超你的取帧超时;或者触发模式开着但从来没发过触发;或者包长设得比网卡巨帧还大。

解决:先把曝光调回自动或者一个较小的值;再查TriggerMode是不是被上一次调试留成了开启状态——这个特别常见,昨天调触发忘了关,今天怎么都不出图;最后用MV_CC_GetOptimalPacketSize_NET重置包长。

5.4 回调里更新界面抛跨线程异常

现象:第一帧能显示,第二帧开始抛Cross-thread operation not valid,或者界面卡死。

原因:图像回调运行在 SDK 的工作线程上,直接给控件赋值就是跨线程访问;在回调里做图像处理则会阻塞采集线程导致丢帧。

解决:回调里只做数据拷贝和入队,显示和处理放到消费线程,跨线程更新用控件的Invoke或者Dispatcher。判断有没有丢帧可以看回调进去和出来的时间差,超过帧间隔就说明回调里干太重的活了。

5.5 开发机能跑,换台机器就崩

现象:自己机器上一切正常,拷到工控机上启动就报找不到 dll,或者打开设备返回一个看不懂的错误码。

原因:目标机器没装 VC 运行库,或者装的 MVS 版本比开发机低,原生库的导出符号对不上。

解决:部署前在目标机器上装一遍对应版本的 VC 运行库;MVS 客户端版本尽量和开发机一致;实在要精简部署,就把原生库和运行库一起打包,别指望目标机器“应该有”。

6. 验证:怎么确认这套环境是真的跑通了

装完能出一帧图,和“这套环境稳定可用”是两回事。我验证一套新环境走三步,任何一步不过就不往下走。

第一步,用 MVS 客户端连续预览五分钟,看帧率是否稳定、有没有花屏。这一步排除硬件和驱动。第二步,跑自己的程序,只做枚举和打开,打印设备型号、序列号、当前像素格式、支持的最大分辨率,确认读到的信息和客户端里一致。第三步,连续取 300 帧,统计实际帧率、超时次数和像素格式转换耗时。

验证项通过标准不通过先看哪
客户端预览五分钟无花屏、帧率波动小于 5%网线、供电、网卡巨帧
程序枚举设备数与客户端一致平台目标、原生库、防火墙
参数回读曝光/增益设置后回读值一致是否在取流中写只读节点
连续取流300 帧零超时或超时少于 1%包长、曝光时间、回调耗时
断线恢复拔网线后 5 秒内识别并自动重连心跳超时、异常回调、重连退避

第三方的统计代码很简单,但一定要有,凭肉眼看不出一秒到底出了几帧:

var sw = Stopwatch.StartNew(); int ok = 0, timeout = 0; // 连续取 300 帧,超时按 1000ms 计 for (int i = 0; i < 300; i++) { var frame = new MV_FRAME_OUT_INFO_EX(); int nRet = MvCamera.MV_CC_GetOneFrameTimeout_NET( handle, buffer, (uint)buffer.Length, ref frame, 1000); if (nRet == 0) ok++; else timeout++; // 超时不一定是错,但要统计比例 } sw.Stop(); Console.WriteLine($"成功 {ok} 帧,超时 {timeout} 次," + $"实际帧率 {ok / sw.Elapsed.TotalSeconds:F1} fps");

MV_CC_GetOneFrameTimeout_NET的超时参数单位是毫秒,设成 1000 意味着只要一秒内有一帧就不会报错,做稳定性统计够用;MV_FRAME_OUT_INFO_EX里还能拿到帧号和时间戳,帧号不连续就说明中间真丢帧了,比看帧率更准。

注意:验证一定要在最终部署的那台机器、那根网线、那块网卡上做。我在开发机上跑得再漂亮,换到工控机上照样可能因为网卡节能设置翻车。

从那以后,我每次部署新环境都强制走一遍这三步,尤其是拔网线重连那项——它不出现在任何“入门教程”里,但产线上真出事的时候,能不能自己缓过来就靠这一下。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询