资讯详情

大恒CG300采集卡C# Demo实战:工业视觉图像采集与二次开发

📅 2026/10/20 7:40:04 | 华诺云谱 👁 阅读
大恒CG300采集卡C# Demo实战:工业视觉图像采集与二次开发
简介这份资源是大恒CG300采集卡的C#示例工程面向从事工业控制、图像处理与科学研究的C#开发者以及需要对接该采集卡进行二次开发的技术人员。Demo围绕采集卡初始化、多通道数据采集与实时帧显示展开并针对晶振频率不匹配导致采集异常的问题给出修正思路将晶振调整为28MHz后即可恢复正常工作对排查硬件与驱动配合问题具有参考价值。压缩包共28个文件约78KB以cs源码、exe可执行程序、resources资源、pdb调试符号、resx与settings配置、sln与csproj工程文件为主另含少量txt说明与缓存文件结构完整可直接编译运行。目前已有492人学习下载。读者可从中获取四路采集与场采集帧显示的完整实现框架理解多线程采集与数据可视化要点并借鉴晶振配置错误的定位与修正方法为构建稳定高效的采集系统提供实践参考。1. 从一次产线调试说起这块采集卡到底能干什么去年帮一个做视觉检测的朋友调设备相机图像死活进不了工控机换了三根线、重装两次驱动都没用最后发现是采集卡和相机之间的时钟配置没对上。那次之后我才认真去翻采集卡的 SDK 和 Demo也就是今天要拆的这份大恒 CG300 采集卡 C# Demo。它解决的就是一个很具体的问题把 CG300 这类图像采集卡接进来的相机画面稳定地抓进 C# 程序里做显示、存图、后续算法处理。适合谁做工业视觉、医疗影像、科研成像的 C# 开发者尤其是那种相机是 Camera Link 或类似接口、需要一块独立采集卡做中转的场景。这份 Demo 不是玩具它把初始化、取流、回调、存图这条链路都跑通了拿过来改改就能接进自己的检测软件里。2. 项目定位与技术选型为什么是 C# 加原生 SDK 封装2.1 这个 Demo 在整套采集链路里的位置工业相机的图像要进电脑通常有两条路一条是 GigE、USB3 这类相机直接出网口或 USB 口程序里用厂商的 SDK 直接取流另一条是相机出 Camera Link、CoaXPress 这类高速并行或同轴信号电脑主板本身没有对应接口必须插一块采集卡做信号转换和缓存再由采集卡把数据通过 PCIe 送到内存。CG300 属于后者它是一块插在工控机里的图像采集卡相机接在卡上程序通过卡的驱动和 SDK 拿数据。这份 C# Demo 的位置就在最上层它调用采集卡厂商提供的原生动态库一般是 C/C 写的 DLL把「打开卡、配置相机参数、启动采集、回调拿图、停止采集、关闭卡」这一整套流程用 C# 包了一遍。所以它不是纯 C# 项目而是一个 P/Invoke 或 C/CLI 桥接的混合项目。理解这一点很关键后面调试时遇到的坑一大半都出在托管代码和非托管代码的边界上。2.2 技术栈拆解C# 负责业务原生库负责时序从 Demo 的结构看技术栈大致分三层。最底层是采集卡驱动和厂商 SDK通常是几个 DLL比如采集控制库、相机配置库、图像处理辅助库接口是 C 风格的导出函数。中间层是 C# 的封装用DllImport声明外部函数或者用 C/CLI 写一个托管包装类把裸指针、句柄这些不安全的东西挡在业务代码外面。最上层是 WinForms 或 WPF 的界面负责显示图像、按钮控制、参数输入。为什么选 C# 而不是直接用 C因为上层业务逻辑、界面、和后续的算法调用很多视觉库有 .NET 版本用 C# 开发效率高得多而真正对时序敏感的采集和内存拷贝交给原生库各司其职。常见做法是采集回调里只做最轻量的动作把图像数据指针和帧信息塞进一个线程安全的队列界面线程再从队列里取出来显示或存盘避免在回调里做耗时操作把采集线程堵死。2.3 目录结构拿到源码先看哪几个文件一份典型的采集卡 C# Demo目录结构不会太复杂但有几个文件是必须优先看的。下面是我拿到这类项目时的惯用看法目录/文件作用阅读优先级SDK/或Lib/厂商原生 DLL 和头文件高先确认版本和位数Camera.cs/Device.cs设备打开、关闭、参数配置的封装类高核心状态机在这Grabber.cs/Acquisition.cs采集启动、停止、回调注册高取流主逻辑MainForm.cs界面事件按钮怎么调上面的类中看调用顺序Utils/图像格式转换、存图辅助中按需看app.config运行库绑定、DLL 搜索路径低但别忽略先看Device和Grabber这两个类基本就能把「卡怎么开、流怎么取」摸清楚。界面文件反而是最后看的因为它只是把已有方法按顺序调一遍。3. 核心模块拆解从打开设备到拿到第一帧图像3.1 设备初始化句柄、通道和相机参数采集卡编程和普通外设最大的不同是它有一堆「句柄」和「通道号」的概念。一块卡可能有多个通道每个通道接一台相机你得先打开卡拿到卡句柄再按通道号去操作对应相机。下面是一段典型的初始化封装接口名按常见命名习惯写实际以厂商 SDK 为准// 采集卡设备封装负责打开卡、配置通道、释放资源 public class CaptureDevice : IDisposable { private IntPtr _boardHandle IntPtr.Zero; // 采集卡句柄 private IntPtr _channelHandle IntPtr.Zero; // 通道句柄 private bool _isOpen false; // 打开采集卡boardIndex 一般从 0 开始 public bool Open(int boardIndex, int channelIndex) { // 第一步创建卡对象拿到板卡句柄 int ret NativeApi.CreateBoard(boardIndex, out _boardHandle); if (ret ! 0) { Console.WriteLine($CreateBoard 失败错误码 {ret}); return false; } // 第二步按通道号打开对应相机通道 ret NativeApi.OpenChannel(_boardHandle, channelIndex, out _channelHandle); if (ret ! 0) { Console.WriteLine($OpenChannel 失败错误码 {ret}); NativeApi.DestroyBoard(_boardHandle); // 失败要回滚别留野句柄 return false; } _isOpen true; return true; } public void Dispose() { if (_channelHandle ! IntPtr.Zero) NativeApi.CloseChannel(_channelHandle); if (_boardHandle ! IntPtr.Zero) NativeApi.DestroyBoard(_boardHandle); _isOpen false; } }这段代码的逻辑很直白先建卡再开通道任何一步失败都要把前面申请的资源释放掉。参数上boardIndex对应机器里插的第几块卡单卡场景填 0channelIndex对应卡上的第几个通道取决于你相机接在哪个口。失败时先看返回的错误码厂商 SDK 一般会配一份错误码表CreateBoard失败多半是驱动没装好或卡没插稳OpenChannel失败则可能是相机没上电或线缆没接对。3.2 采集回调图像数据是怎么从卡里冒出来的采集卡拿图有两种模式主动查询和回调推送。Demo 里基本都用回调因为效率高、延迟低。回调函数的签名通常是固定的由原生库在每一帧到达时调用参数里带着图像数据指针、宽度、高度、帧号等信息。C# 这边要做的是把回调委托传给原生库并在回调里尽快把数据搬走。// 采集回调每来一帧就被原生库调用一次 private void OnFrameArrived(IntPtr pData, int width, int height, int frameId) { // 关键回调里绝不做耗时操作只做数据搬运 int stride width * 3; // 假设是 24 位彩色每像素 3 字节 int dataSize stride * height; byte[] buffer new byte[dataSize]; // 从非托管内存拷贝到托管数组 Marshal.Copy(pData, buffer, 0, dataSize); // 塞进线程安全队列交给界面线程处理 _frameQueue.Enqueue(new FrameData { Buffer buffer, Width width, Height height, FrameId frameId }); }这里有几个点必须说清楚。第一pData指向的是采集卡驱动内部的内存它的生命周期只在这个回调函数执行期间有效回调一返回这块内存可能马上被下一帧覆盖所以必须立刻Marshal.Copy出来不能只存指针。第二stride不一定等于width * 3有些卡为了内存对齐会做行填充稳妥做法是从 SDK 查询实际的 stride 值否则图像会斜。第三回调线程不是界面线程直接在里面更新控件会抛跨线程异常所以用队列中转是标准做法。3.3 启动与停止采集顺序错了就翻车采集的启动和停止有严格的顺序顺序错了轻则取不到图重则卡死。常见做法是先注册回调再启动采集停止时先停采集再注销回调最后才关通道和卡。public bool StartGrab() { // 先注册回调确保启动后第一帧就能被接住 _callbackDelegate new FrameCallback(OnFrameArrived); int ret NativeApi.RegisterCallback(_channelHandle, _callbackDelegate); if (ret ! 0) return false; // 再启动采集 ret NativeApi.StartAcquisition(_channelHandle); if (ret ! 0) { NativeApi.UnregisterCallback(_channelHandle); // 启动失败要注销 return false; } return true; } public void StopGrab() { NativeApi.StopAcquisition(_channelHandle); // 先停流 NativeApi.UnregisterCallback(_channelHandle); // 再注销回调 }顺序背后的道理是如果先启动采集再注册回调中间那几帧没人接可能触发驱动的缓冲区溢出如果停止时先注销回调再停采集回调委托已经被回收原生库还在往一个空指针上打直接崩。这个顺序我踩过不止一次后来养成习惯启动和停止都写成对称的一对方法谁也别单独调。4. 跑起来环境准备、编译和第一次出图4.1 环境准备驱动、SDK 和位数匹配在编译之前有三件事必须先确认。第一采集卡驱动装好了设备管理器里能看到卡没有黄色感叹号。第二厂商 SDK 装好了通常会在系统目录或安装目录下留几个 DLLDemo 里引用的就是这些。第三位数要匹配如果 SDK 是 64 位的C# 项目也必须编译成 x64不能是 Any CPU 或 x86否则运行时会报「找不到 DLL」或「试图加载格式不正确的程序」。常见做法是把 SDK 的 DLL 直接拷到输出目录或者在app.config里配好搜索路径。我一般会在项目里建一个Lib文件夹放这些 DLL属性里设置「如果较新则复制」省得每次手动拷。4.2 编译与运行从空窗口到第一帧编译本身没什么特别的用 Visual Studio 打开解决方案选对平台x64直接生成。运行后一般会看到一个窗口上面有几个按钮打开设备、开始采集、停止采集、保存图像。第一次跑按这个顺序点点「打开设备」看日志区有没有报错成功的话设备状态会变成已连接。点「开始采集」如果相机和线缆都正常画面应该开始刷新。点「保存图像」检查存出来的图是不是完整、颜色对不对。如果打开设备就失败先查驱动和位数如果开始采集后黑屏查相机是否上电、触发模式是否设对如果图像是斜的或颜色错乱查 stride 和像素格式。4.3 参数怎么改曝光、增益和触发模式Demo 里一般会暴露几个常用参数给界面调最常见的是曝光时间、增益和触发模式。曝光和增益影响图像亮度触发模式决定是相机自己连续出图连续采集还是等外部信号才出一帧触发采集。改这些参数通常是在打开设备之后、启动采集之前调用 SDK 的设参函数。// 设置曝光时间单位一般是微秒具体看 SDK 定义 NativeApi.SetExposureTime(_channelHandle, 5000.0); // 设置增益单位可能是 dB 或原始值 NativeApi.SetGain(_channelHandle, 1.0); // 设置触发模式0 连续1 软触发2 硬触发 NativeApi.SetTriggerMode(_channelHandle, 0);参数改完如果没生效先确认是不是在启动采集之后才设的——很多卡要求参数在停流状态下配置运行中改会被忽略或报错。另外曝光和增益别一起猛加容易过曝先调曝光到合适再用增益微调。5. 避坑与排查那些让我加班到半夜的问题5.1 回调里做界面更新导致卡死现象程序跑一会儿就无响应采集也停了。原因在采集回调里直接调用了Invoke或直接改了控件属性回调线程被界面线程的锁拖住驱动缓冲区很快填满采集卡报溢出。解决回调里只做数据拷贝和入队界面更新全部放到界面线程的定时器或事件里做。5.2 图像斜切或花屏现象显示出来的图像整体倾斜或者每隔几行就错位。原因stride用了width * 像素字节数但采集卡实际有行对齐填充真实 stride 更大。解决从 SDK 查询实际 stride或者用 SDK 提供的图像转换函数直接输出标准格式别自己算。5.3 停止采集后再次启动失败现象第一次采集正常停止后再点开始就没反应或报错。原因停止时只调了StopAcquisition没注销回调或者句柄状态没复位第二次启动时 SDK 认为通道还在占用。解决停止流程写完整停流、注销回调、必要时复位通道再启动时重新注册回调。5.4 找不到 DLL 或入口点现象运行时报DllNotFoundException或EntryPointNotFoundException。原因DLL 不在输出目录、位数不匹配、或者 SDK 版本和 Demo 用的接口名对不上。解决确认 DLL 已复制到输出目录项目平台设为 x64用dumpbin或依赖查看工具核对导出函数名接口名大小写和调用约定Cdecl还是StdCall都要对。5.5 多通道同时采集时丢帧现象接了两台相机单独跑都正常一起跑就丢帧。原因回调里处理太慢或者共用了一个队列导致锁竞争或者 PCIe 带宽不够。解决每个通道独立队列回调里只拷贝不处理降低单帧处理耗时必要时降低分辨率或帧率确认主板 PCIe 通道数够用。6. 二次开发把 Demo 改成你自己的采集模块6.1 封装成可复用的采集服务Demo 的代码是给演示用的直接抄进项目里会有一堆界面耦合。我的习惯是先把设备、采集、存图三块拆成独立的服务类界面只负责调服务。设备服务管打开关闭和参数采集服务管启动停止和回调分发存图服务管格式转换和落盘。这样换界面、加算法都不影响采集核心。// 采集服务对外只暴露事件内部管好线程和队列 public class AcquisitionService { public event ActionFrameData FrameReady; // 界面订阅这个事件 private readonly CaptureDevice _device; private Thread _dispatchThread; private BlockingCollectionFrameData _queue; public void Start() { _queue new BlockingCollectionFrameData(64); // 有界队列防内存爆 _dispatchThread new Thread(DispatchLoop) { IsBackground true }; _dispatchThread.Start(); _device.StartGrab(); } private void DispatchLoop() { foreach (var frame in _queue.GetConsumingEnumerable()) { FrameReady?.Invoke(frame); // 在独立线程触发界面自己 Invoke } } }这样改的好处是采集回调只往_queue里塞分发线程负责触发事件界面订阅事件后自己决定怎么显示。队列设了上限万一界面处理不过来塞满就阻塞或丢弃不会把内存吃光。6.2 加一个存图队列别在回调里写文件存图是另一个容易翻车的地方。有人图省事直接在回调里File.WriteAllBytes结果采集帧率一高就丢帧因为磁盘 IO 比内存拷贝慢几个数量级。正确做法是再加一级存图队列采集回调只入队专门的存图线程从队列取数据写盘。写盘时用FileStream加缓冲或者用内存映射文件格式上优先存无损的 BMP 或 PNG需要压缩再转 JPEG。6.3 验证改造是否成功三个可量化的指标改完之后怎么确认没改坏我一般看三个数第一连续跑一小时不丢帧帧号是连续的第二回调到显示的延迟稳定不随时间增长第三内存占用平稳不持续上涨。前两个靠日志打帧号和耗时第三个靠任务管理器或性能计数器看。如果帧号有跳变说明中间有丢帧回去查队列是不是满了、回调是不是变慢了。6.4 一个我常犯的错误早些年我总想着在回调里顺手把图像转成Bitmap再入队觉得这样界面省事。结果帧率一高就崩因为Bitmap构造本身不便宜还在回调线程里分配了大量托管内存GC 一触发采集线程跟着卡。后来我改成回调只拷原始字节转换放到分发线程或界面线程问题就没了。从那以后我每次写采集回调都强制自己只做「拷贝 入队」两件事多一行都不加。希望这份拆解能帮你少走点弯路把 CG300 这块卡顺利用起来。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑