资讯动态

C# 工业相机 SDK 从连上到稳定出图:调用链与丢帧排查

发布时间:2026/10/24 8:28:56 来源:尧图企业网站定制
简介面向C#工业视觉开发者的海康机器人工业相机SDK调用示例聚焦回调取图与软触发取图两类常用采集方式适合需要把相机接入WinForms上位机、完成图像采集与业务联调的初中级开发者参考。压缩包共71个文件约786KB以cs源码、dll引用库、resx资源、json与cache构建文件为主包含相机接口抽象、海康相机封装、配置类及示例窗体等模块便于按项目结构理解SDK接入方式。已有2638人学习说明其在工业相机二次开发场景中具有一定参考价值。读者可从中获得回调与软触发两种取图流程的可运行代码骨架、相机参数配置与界面集成思路并据此排查采集回调、触发模式切换和依赖引用等问题减少从零搭建采集模块的时间。1. C# 工业相机 SDK 的真实位置从“能连上”到“稳定出图”差在哪某产线视觉工位用 C# 写上位机相机能枚举、能预览看起来一切正常连续跑两个小时开始偶发丢帧操作员点一次“停止”再“开始”又恢复过一阵 UI 卡死换一台工控机后曝光参数全变。问题不在算法而在 C# 工业相机 SDK 的调用链句柄什么时候开、回调在哪个线程、缓冲区给多大、参数按什么顺序下发任何一环没对齐现场就会用“玄学”两个字概括。这份资源围绕 C# 工业相机 SDK 的工程落地展开适合做检测、测量、读码、定位的上位机开发者重点解决从“能连上”到“稳定出图”之间的工程细节。2. 拆开 SDK 的调用链C# 侧要抓住的五个对象2.1 设备枚举与句柄生命周期工业相机 SDK 在 C# 里的第一层不是“取图”而是设备句柄。枚举、打开、关闭、释放这四个动作必须成对出现常见翻车是打开失败后直接抛异常后面的Close和Dispose没走到下一次再打开就报“设备被占用”。我一般把打开和释放包在try/finally里枚举只用来拿序列号真正打开时用序列号而不是索引因为索引会随 USB 插拔顺序变化。using IndustrialCamera.Sdk; var manager new DeviceManager(); var devices manager.EnumerateDevices(); // 只枚举不打开 if (devices.Count 0) { throw new InvalidOperationException(未发现相机先检查供电和驱动); } var target devices.First(d d.SerialNumber SN-XXXX); CameraDevice camera null; try { camera manager.Open(target.SerialNumber); // 用序列号打开避免索引漂移 camera.SetPixelFormat(PixelFormat.Mono8); camera.StartGrabbing(); } finally { camera?.StopGrabbing(); camera?.Dispose(); // 释放句柄避免下一次打开被占用 manager.Dispose(); }这里的关键参数是SerialNumber和PixelFormat。序列号是设备的稳定标识索引只适合单相机演示。PixelFormat在打开后、取流前设置部分 SDK 允许运行中切换但切换瞬间会清空缓冲区现场尽量一次定好。Dispose不是可有可无的收尾它负责释放底层采集线程和缓冲区少调一次进程退出时可能留下僵尸句柄。2.2 取流模式回调、轮询与缓冲区取流一般有三种方式回调、轮询、异步等待。回调吞吐高适合多相机和高速线轮询写起来简单但延迟和 CPU 占用不好控异步等待介于两者之间。C# 里最容易踩的坑是把回调当成 UI 线程用回调线程里直接改控件属性程序不一定立刻崩但会在某个时间点抛跨线程异常。常见做法是回调里只做拷贝和入队UI 线程从队列取图。var queue new ConcurrentQueueFrameData(); camera.FrameReceived (sender, frame) { // 回调线程只做轻量拷贝不碰 UI不做文件 IO var copy frame.Clone(); queue.Enqueue(copy); if (queue.Count 8) // 消费跟不上时丢弃旧帧防止内存膨胀 { queue.TryDequeue(out var old); old?.Dispose(); } }; camera.StartGrabbing();queue.Count 8是保护阈值不是越大越好。缓冲区数量要跟消费速度匹配算法每帧 30 ms相机 30 fps队列留 3 到 5 帧就够留 50 帧只会让延迟滚雪球。回调里做保存图片、写日志、刷新界面都会拖慢回调线程缓冲区一满SDK 要么丢帧要么直接报超时。血泪经验是回调只负责“接住”不负责“处理”。2.3 像素格式与图像转换的边界工业相机常见输出有 Mono8、Mono12、BayerRG8、RGB24。C# 的Bitmap不是万能容器Mono8 可以直接按 8 位灰度构造Mono12 要决定是压到 8 位还是保留 16 位Bayer 格式需要先做去马赛克。转换时最容易错的是stride相机给的每行字节数不一定等于宽度尤其宽度不是 4 的倍数时按width * 1去拷会花屏。public static Bitmap Mono8ToBitmap(FrameData frame) { int width frame.Width; int height frame.Height; int stride frame.Stride; // 以 SDK 回读为准不要硬编码 width var bmp new Bitmap(width, height, PixelFormat.Format8bppIndexed); var rect new Rectangle(0, 0, width, height); var data bmp.LockBits(rect, ImageLockMode.WriteOnly, bmp.PixelFormat); for (int y 0; y height; y) { IntPtr src IntPtr.Add(frame.DataPtr, y * stride); IntPtr dst IntPtr.Add(data.Scan0, y * data.Stride); Buffer.MemoryCopy((void*)src, (void*)dst, data.Stride, width); } bmp.UnlockBits(data); return bmp; }这段代码只处理 Mono8frame.Stride必须从 SDK 回读不能假设等于宽度。LockBits之后一定要UnlockBits否则 Bitmap 会一直锁住内存后面Dispose也释放不干净。如果相机输出 Bayer不要在这层硬转 RGB先确认 SDK 是否支持内置转换自己写去马赛克容易把颜色通道顺序搞反调试时看起来像“相机偏色”其实是转换代码把 R 和 B 换了。2.4 参数配置顺序先固定再调曝光参数下发顺序不对表现是“设置成功了但图像没变化”。常见顺序是先停流再设触发模式、ROI、像素格式最后设曝光和增益设完回读确认再开流。曝光和增益不是独立旋钮曝光拉长能提亮但运动物体会拖影增益补亮会引入噪声。现场调参我一般先锁增益再调曝光满足运动模糊最后才用增益补剩下的亮度。参数常见设置注意点曝光时间5008000 微秒单位因 SDK 而异以回读值为准增益012 dB先低后高超过 12 dB 噪声明显触发模式软触发或硬触发自由运行只适合调试像素格式Mono8 优先带宽不够时先降位深超时10003000 ms太小会误报太大掩盖问题camera.StopGrabbing(); camera.SetTriggerMode(TriggerMode.Software); camera.SetRoi(0, 0, 1280, 1024); camera.SetPixelFormat(PixelFormat.Mono8); camera.SetExposureTime(3000); // 微秒具体单位看 SDK 文档 camera.SetGain(3.0); var actualExposure camera.GetExposureTime(); // 回读确认没被相机裁剪 camera.StartGrabbing();回读这一步不要省。有些相机在触发模式下限制最大曝光设 8000 微秒实际只生效 4000不回读就会误判为“曝光不准”。ROI 也要在停流时改运行中改 ROI 可能触发缓冲区重分配轻则丢几帧重则采集线程直接退出。3. 从零跑通第一张图C# 工程配置与最小采集闭环3.1 工程目标平台与 DLL 放置C# 工程最常见的启动失败不是代码写错而是位数不匹配。SDK 运行库是 64 位工程用 x86 编译或者反过来表现是“找不到 DLL”或“BadImageFormatException”。目标平台不要长期停在 AnyCPU现场工控机统一 x64 就固定 x64。SDK 的托管 DLL 和原生运行库要放在输出目录原生 DLL 一般不能只靠引用托管 DLL 解决。# 输出目录结构示意原生运行库要和 exe 同目录或在其子目录 bin/x64/Debug/ ├── App.exe ├── IndustrialCamera.Sdk.Managed.dll ├── IndustrialCamera.Sdk.Native.dll └── runtime/ └── win-x64/ └── native/ └── IndustrialCamera.Sdk.Native.dll托管 DLL 用“添加引用”或 NuGet 方式引入原生 DLL 复制到输出目录。不要手动改PATH指向开发机目录换一台机器就失效。发布时把runtime目录一起带走缺一个原生依赖程序会在DeviceManager构造时直接抛异常日志里只写“初始化失败”排查成本很高。3.2 枚举、打开、配置、取流的最小代码最小闭环不需要界面控制台程序就能验证。流程是初始化 SDK、枚举、按序列号打开、设像素格式、注册回调、开流、等若干帧、停流、释放。每一步都检查返回码不要用异常兜底所有错误SDK 的错误码里包含“设备离线”“缓冲区满”“参数越界”比异常信息具体。using IndustrialCamera.Sdk; using var manager new DeviceManager(); manager.Initialize(); var devices manager.EnumerateDevices(); var info devices.FirstOrDefault(); if (info null) throw new Exception(no camera); using var camera manager.Open(info.SerialNumber); camera.SetPixelFormat(PixelFormat.Mono8); camera.SetExposureTime(3000); camera.SetGain(2.0); int received 0; camera.FrameReceived (s, frame) { Interlocked.Increment(ref received); if (received 1) { var bmp Mono8ToBitmap(frame); bmp.Save(first_frame.png); bmp.Dispose(); } }; camera.StartGrabbing(); Thread.Sleep(2000); camera.StopGrabbing(); Console.WriteLine($received{received});Initialize只调一次重复调可能返回“已初始化”。Open之后设置参数StartGrabbing之前注册回调顺序反了会漏掉前几帧。Thread.Sleep(2000)只是演示实际工程用ManualResetEventSlim等第一帧到达再继续。收到的帧数明显低于预期先看曝光时间和帧率上限再看回调里有没有做耗时操作。3.3 保存与释放别让句柄悬空保存图像本身不难难的是保存时不阻塞采集。直接在回调里Bitmap.Save会写磁盘磁盘一慢回调线程被拖住缓冲区很快耗尽。我一般把帧拷贝进队列单独一个保存线程消费文件名带时间戳和帧号方便对丢帧。保存格式优先 PNG 或 BMPJPEG 有压缩耗时调试阶段不划算。var saveDir Path.Combine(AppContext.BaseDirectory, capture); Directory.CreateDirectory(saveDir); long frameNo 0; while (queue.TryDequeue(out var frame)) { using (frame) using (var bmp Mono8ToBitmap(frame)) { var name ${DateTime.Now:yyyyMMdd_HHmmss_fff}_{frameNo}.png; bmp.Save(Path.Combine(saveDir, name), ImageFormat.Png); } }using (frame)保证每帧的 native 内存释放using (var bmp)保证 GDI 句柄释放。保存线程退出时要先把队列排空再释放相机否则最后几帧还在队列里相机句柄已经关了访问帧内存会崩。文件名里的帧号不要用回调里的局部变量用Interlocked.Increment的全局计数否则多线程下会重名。3.4 用日志把失败点钉死采集程序出问题最怕日志只写“采集失败”。SDK 返回码、当前帧号、最后一次成功时间、相机序列号、曝光值这些上下文才能定位。日志不要每个回调都写按秒汇总或状态变化时写否则日志 IO 反过来拖慢采集。我一般用滚动文件单文件 20 MB保留 5 个够现场回溯。void LogState(string tag, long frameNo, int errorCode) { var line ${DateTime.Now:O} tag{tag} sn{info.SerialNumber} $frame{frameNo} err{errorCode} exp{camera.GetExposureTime()}; File.AppendAllText(camera.log, line Environment.NewLine); }errorCode不要转成“未知错误”就完事保留原始码查 SDK 文档时能对上。exp回读值能判断参数有没有被相机裁剪。日志里不要写图像二进制只写元数据否则文件膨胀很快。4. 参数怎么设才不飘曝光、增益、触发与超时的组合4.1 曝光与增益的联动边界曝光和增益都影响亮度但代价不同。曝光拉长进光量增加运动物体拖影增益放大信号暗部提亮但噪声同步放大。现场调参的顺序我固定为先定帧率上限再定曝光上限再调增益最后看直方图。帧率要求 30 fps曝光就不能超过 1/30 秒减去读出时间硬设 50000 微秒帧率一定掉。场景曝光策略增益策略风险高速运动尽量短配合频闪低增益补光优先拖影、欠曝静态测量可放宽06 dB过曝丢细节低照度受帧率限制不超过 12 dB噪声大、误判多相机同步统一曝光统一增益亮度不一致camera.SetExposureTime(2500); camera.SetGain(4.0); var exp camera.GetExposureTime(); var gain camera.GetGain(); if (Math.Abs(exp - 2500) 100) { LogState(exposure_clipped, 0, 0); }回读差超过 100 微秒就记一条日志说明相机内部按帧率或触发限制裁剪了曝光。增益单位有的是 dB有的是倍数不要跨品牌套公式以 SDK 回读为准。曝光和增益都设完再开流运行中反复改参数会让自动曝光算法和手动值打架图像亮度会“呼吸”。4.2 软触发与硬触发的选型软触发由软件发命令布线简单但触发时机受操作系统调度影响抖动大硬触发由光电传感器或 PLC 给电平实时性好适合运动中的工件。自由运行适合调试和静态场景不适合节拍固定的产线。选型不是越硬越好硬触发要配触发沿、去抖、延时接错线一样丢帧。触发方式延迟抖动适用场景软触发毫秒级较大静态工位、调试硬触发微秒级小运动工件、多相机同步自由运行固定周期中预览、录像camera.SetTriggerMode(TriggerMode.Hardware); camera.SetTriggerSource(TriggerSource.Line1); camera.SetTriggerActivation(TriggerActivation.RisingEdge); camera.SetTriggerDelay(0); // 微秒按传感器到工位的距离折算 camera.StartGrabbing();TriggerSource要跟实际接线一致Line1 接成 Line2 就是“一帧都没有”。TriggerDelay不是玄学按传送带速度和传感器到相机视野中心的距离算先算再试。硬触发模式下不要在回调里再发软触发两种触发源同时开帧号会乱。4.3 超时、重传与丢帧计数超时设置是采集线程的“后悔药”。设得太小网络抖动或磁盘忙时误报超时设得太大相机掉线后线程卡死界面像死机。我一般设 10003000 ms并且在超时后做一次“停流、清缓冲区、重开流”不要在原线程里无限重试。丢帧统计看帧号缺口比看“感觉卡了”可靠。long lastFrameNo -1; long gap 0; camera.FrameReceived (s, frame) { if (lastFrameNo 0 frame.FrameNo ! lastFrameNo 1) { Interlocked.Add(ref gap, frame.FrameNo - lastFrameNo - 1); } lastFrameNo frame.FrameNo; queue.Enqueue(frame.Clone()); };FrameNo是否连续取决于 SDK有的相机在触发模式下帧号按触发计数有的按传输计数先确认语义再算缺口。gap持续增长说明链路有问题不是算法慢。重开流之前要StopGrabbing并释放旧缓冲区直接StartGrabbing可能返回“已在采集”。4.4 多相机场景的带宽与命名多相机同时开流问题往往不是 SDK 不支持而是带宽和 USB 控制器分配。四台相机接同一个 USB 控制器每台都跑全分辨率超时是必然的。常见做法是分控制器、降分辨率、降帧率、改像素格式。相机命名不要用Camera0、Camera1用序列号后四位绑定工位换线后不会张冠李戴。var cameras new[] { manager.Open(SN-A001), manager.Open(SN-A002) }; foreach (var cam in cameras) { cam.SetPixelFormat(PixelFormat.Mono8); cam.SetRoi(0, 0, 1280, 1024); cam.StartGrabbing(); }打开多相机时逐台初始化一台失败不影响其他台但要把失败序列号记进日志。ROI 越小带宽越低先降 ROI 再降帧率图像质量损失比降位深小。多相机时间戳不要用DateTime.Now代替主机时间受系统对时影响用 SDK 时间戳做同步再用主机时间做展示。5. 避坑与排查五个让采集线程停不下来的现场问题5.1 现象启动报“找不到 DLL”换开发机又正常现象是程序在开发机跑得好拷到工控机启动就报DllNotFoundException或BadImageFormatException。原因通常有两个原生 DLL 没跟着输出目录走或者 C# 工程目标平台和 SDK 运行库位数不一致。解决时先看异常里的 DLL 名再去输出目录确认文件存在用dumpbin /headers看原生 DLL 是 x64 还是 x86工程平台改成一致。发布时把原生运行库目录整体复制不要只复制托管 DLL。改完平台后清一次bin和obj否则旧产物还会被加载。5.2 现象回调里更新 UI 直接崩或者偶发卡死现象是预览正常点“开始”后界面偶尔卡住日志里出现跨线程访问控件。原因是FrameReceived回调运行在 SDK 的采集线程不是 UI 线程直接给PictureBox.Image赋值违反 WinForms/WPF 线程规则。解决是把回调里的帧放进ConcurrentQueue用Control.BeginInvoke或 WPF 的Dispatcher.BeginInvoke在 UI 线程取最新一帧并且只取一帧不要每帧都排队刷新。刷新频率高于界面刷新率没有意义反而堆积消息。5.3 现象采几十帧后内存持续涨最后采集线程退出现象是任务管理器里内存曲线一直向上采几百帧后程序变慢或直接停流。原因多是帧对象没释放或者回调里克隆了帧但消费队列丢弃时没Dispose。解决是给FrameData加引用计数或确保Clone和Dispose成对队列满时丢弃旧帧要调用旧帧的Dispose保存线程退出前排空队列。用GC.GetTotalMemory只能看托管内存native 内存要用任务管理器或 SDK 自带统计看。每次改完采集逻辑跑十分钟压测再上线。5.4 现象触发模式下一帧都没有自由运行却正常现象是切到硬触发后回调不进来切回自由运行立刻出图。原因通常是触发源选错、触发沿不对、传感器没给到电平或者相机在等待触发时超时设置太短直接报错。解决顺序是先确认TriggerSource和接线一致再用软件强制触发一次看有没有帧然后用示波器或万用表看传感器输出。触发延迟设得过大也会表现为“没有帧”因为帧在等待延迟。触发模式下不要同时开自动曝光第一帧还没来自动曝光没有输入。5.5 现象多相机同时开流后频繁超时单台正常现象是单台相机跑满帧率没问题两台一起开就频繁超时日志里错误码指向缓冲区或传输超时。原因是共享带宽或 USB 控制器过载也可能是两台相机用了同一个 IP 段或同一根 USB 总线。解决是先降分辨率或帧率验证带宽再把相机分到不同控制器/网卡确认每台相机的序列号和 IP 绑定正确。多相机不要用索引打开索引在枚举顺序变化后会串台。超时后不要只重试当前相机先停掉所有流清一次缓冲区再逐台恢复。6. 把采集做成可验证的三个指标与一个回放习惯6.1 用时间戳和帧计数算真实帧率帧率不要看 SDK 界面上的标称值用回调时间戳算。每收到一帧记一次Stopwatch时间统计最近 100 帧的间隔得到实际回调帧率同时用帧号差算相机输出帧率。两者差得多说明回调或队列在丢帧。下面的代码只做统计不参与业务逻辑放在独立线程里跑。var sw Stopwatch.StartNew(); var stamps new Queuedouble(); camera.FrameReceived (s, frame) { var t sw.Elapsed.TotalSeconds; stamps.Enqueue(t); if (stamps.Count 100) stamps.Dequeue(); }; // 计算100 帧跨度 / 帧数 double fps stamps.Count 1 ? (stamps.Last() - stamps.First()) / (stamps.Count - 1) : 0;stamps只留最近 100 个避免长时间运行内存膨胀。算出来的 fps 要和相机设置的帧率、曝光时间对得上曝光 3000 微秒理论最高不到 333 fps设 500 fps 没有意义。6.2 用丢帧率判断链路瓶颈丢帧率比“感觉卡”可靠。用相机帧号缺口除以理论帧数得到链路丢帧率用入队帧数除以回调帧数得到消费丢帧率。前者高说明传输或触发有问题后者高说明算法或保存线程太慢。现场我一般把链路丢帧率控制在 0.1% 以内消费丢帧率单独看不混在一起。指标计算方式异常时先看回调帧率最近 100 帧时间差曝光、触发、带宽链路丢帧率帧号缺口 / 理论帧数网线、USB 控制器、超时消费丢帧率队列丢弃数 / 入队数算法耗时、磁盘 IO6.3 保存原始帧做回放验证只存 PNG 截图不够算法复现需要原始像素。调试阶段我会把前 200 帧按raw格式存下来连同宽度、高度、stride、像素格式写进一个json描述文件后面不用接相机也能回放。回放时按同样的转换路径走一遍能区分“相机问题”和“算法问题”。从那以后我每次接新相机都先跑最小闭环、算一遍丢帧率、存一段原始帧再谈检测逻辑。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑