多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

Basler相机配合Halcon在C#中的采集源码拆解与避坑指南

Basler相机配合Halcon在C#中的采集源码拆解与避坑指南 简介这是一份结合Balser工业相机与Halcon机器视觉库、采用C#语言开发的图像采集示例工程面向自动化质检、工业4.0与机器视觉检测领域的技术人员。工程完整演示了从相机初始化、参数配置分辨率、曝光时间、增益、图像采集、预处理、特征提取到结果输出的全流程借助Halcon的.NET接口调用HObject、HTuple等核心对象并封装了形状匹配、模板匹配、条码识别等常用算子便于快速理解硬件设备与算法库如何协同工作。压缩包共包含38个文件以C#源码窗体代码、主程序、Visual Studio解决方案文件、可执行程序、Halcon依赖库及资源配置为主整体大小约为21.37MB目录中同时保留了调试生成的pdb文件与图标资源结构清晰既可直接运行查看效果也方便在此基础上修改拓展。目前已有382人学习下载对于希望掌握工业相机采集与机器视觉处理流程的开发者来说是一份具有实践参考价值的入门范例。1. Basler 相机在 C# 里走 Halcon 采集源码包拆的就是这条链路手头任务是用 Basler 工业相机做视觉采集最省力的方式不是直接写 Pylon SDK而是让 Halcon 的 Framegrabber 层去接管相机。这个源码包给我最直观的价值就是把「相机连接 → 参数设置 → 抓帧 → 图像显示」这条完整链路用 C# 工程呈现出来了。它不是一个教学片段而是可运行的 Demo.sln打开就能编译里面能直接看到 HFramegrabber 的初始化写法、GrabImage 的实际调用位置以及 HObject 转 Bitmap 的常规处理。适合做自动化、质检、视觉引导的工程师参考也适合刚接触 Halcon 的 C# 上位机开发人员照着改自己的相机接入代码。2. 链路选型与运行环境为什么走 GigEVision2 而不直接调 Pylon2.1 Halcon 不直接认相机认的是相机接口协议刚接触 Halcon 的人容易把相机驱动想成一个黑匣子以为装了 Basler 的 Pylon SDKHalcon 就能自动找到相机。实际不是这样。Halcon 的 Framegrabber 层通过协议识别设备它关心的是你的相机走哪种物理接口和协议族GigE Vision、USB3 Vision、Camera Link还是专门的品牌接口。Basler 相机型号覆盖面很广千兆网口相机走 GigE Vision 协议USB3 相机走 USB3 Vision 协议Camera Link 相机则是另一种接口。对应到 Halcon 里最常用的相机类型参数就是GigEVision2和USB3Vision而不是Basler这个品牌名。老版本 Halcon 里确实有过针对特定品牌的接口类型官方后来明确推荐用通用协议接口原因是通用接口能拿到更完整的标准参数并且不会因为固件更新而失效。选 GigEVision2 还有一个现实好处你不需要在业务代码里引入 Pylon 的依赖。Halcon 的 GigE Vision 组件自带设备发现功能直接走网口协议访问相机输出直接就是 Halcon 的 HObject 图像数据。如果用 Pylon 先采出来再转给 Halcon中间要经手字节数组和格式转换多拷贝一遍内存不说类型处理还容易出问题。这就是为什么资源包里看不到 Pylon 相关代码整个采集动作都发生在 Halcon 框架内的根本原因。对于 Basler 相机以下几点需要提前确认清楚旧固件的千兆网相机参数多叫ExposureTimeAbs新固件已经统一成ExposureTime代码里写死任何一个都会在另一台相机上失效。如果相机接入的是多网口工控机Halcon 枚举出来的设备顺序和 Windows 网络适配器顺序强相关代码里应该通过设备描述符选中目标相机而不是假设设备索引固定。GigEVision2 接口要求网卡支持巨型帧不经设置直接跑大分辨率相机丢包率会高到图像直接花屏。2.2 环境配置驱动、网段与 halcondotnet.dll 引用这份源码要在本机跑起来环境上需要满足三层准备Halcon 运行时、Basler 相机能被系统访问、C# 工程引用到 Halcon 的 .NET 接口。三个环节缺一程序编译过了也会在打开相机那一步崩掉。Halcon 运行时方面只要本机安装了带有效 license 的 Halcon 即可版本不用刻意追求最新。需要留意的是工程引用的halcondotnet.dll和你安装的 Halcon 版本要匹配否则运行到构造函数时容易报找不到程序集。Basler 千兆网相机访问方面最常见的做法是先用网线直连相机和电脑然后把相机 IP 配到和电脑网卡同一网段。相机出厂默认 IP 通常是192.168.0.2这类地址电脑网卡要改成同一个网段才能发现设备。可以用下面的命令快速检查# 查看当前网卡 IPv4 地址确认与相机同段 ipconfig /all # 如果相机 IP 未知先试常见的 Basler 出厂地址192.168.0.2 ping 192.168.0.2检查完网络之后还要在 Halcon 里确认能枚举到设备。打开 HDevelop执行list_framegrabber_attributes (GigEVision2, 0, 0, 0, () , device, Information, Value)或者直接打开 Halcon 自带的 Image Acquisition 助手选择 GigEVision2 接口点一下设备列表能看到 Basler 相机的描述符说明设备通路没问题。C# 工程引用方面在 Visual Studio 里打开 Demo.sln 之后进入项目引用管理器确认是否有 halcondotnet.dll。路径一般在 Halcon 安装目录下C:\Program Files\MVTec\HALCON-版本号\bin\dotnet\halcondotnet.dll如果没有手动添加该引用并且把复制本地属性设为 True这样生成 exe 时会自动带上这个 DLL。整个源码包本身不含 Halcon 安装文件它依赖的是你机器上已有的 Halcon 运行环境。2.3 OpenFramegrabber 的 17 个参数怎么填HFramegrabber 的初始化是整份源码里第一个让人犯晕的地方。C# 里调用 OpenFramegrabber 时参数一口气传十几个里面还有多个位置用0、-1、default占位从视觉上很容易混淆。把参数拆开看其实每一组都有明确含义。参数位置常用取值作用相机类型GigEVision2/USB3Vision指定协议接口决定后续参数名空间cameralink / device / port0,0,0接口卡索引普通单网口场景全填 0lineIn / lineOut0,0硬件触发线不用硬件触发时填 0默认高度 / 默认宽度0,00 表示完全由相机提供分辨率占位参数 dummydefault固定值按官方文档填就行图像类型 imageType-1留空由相机输出格式决定色彩空间 colorSpacedefault灰度相机填 default 即可位深 bitsPerChannel-18bit 相机填 -1由相机自己上报genericfalse一般填 false需要特殊参数可改为文件名或参数对externalTriggerdefault软触发或自由运行模式留 default相机设备名第二台相机才需要精确指定末位参数0, -1兼容旧版相机类型保持默认这份表格对应的典型调用如下HFramegrabber grabber new HFramegrabber(); grabber.OpenFramegrabber( GigEVision2, // 相机类型Basler 千兆网相机统一走这个 0, 0, 0, 0, 0, 0, // 网卡索引、设备索引、端口、行输入输出全部默认 default, -1, // 高宽不限制 default, -1, // 图像类型由相机上报位深自动 false, // 通用参数关闭 default, , // 外部触发不启用设备名留空自动枚举第一台 0, -1);填参时有两个容易翻车的点。第一表格里第 4 组的占位参数在不同 Halcon 版本里顺序略有调整如果你手上的版本更老建议先在 HDevelop 里用 Image Acquisition 助手生成一段代码再对照着参数顺序填 C#。第二如果工程里有多个相机设备名字段不要留空用grabber.GetFramegrabberParam(device)先列出设备描述符再把目标相机的完整描述符填进去这样重启电脑或者换网口不会因为设备顺序变化抓错相机。3. 从抓帧到上屏HFramegrabber 核心代码与参数调整3.1 初始化相机对象并设置曝光与增益连接部分真正的核心动作是两步用 OpenFramegrabber 建立会话再用 SetFramegrabberParam 把曝光、增益这类相机参数写进设备。参数写入失败不会抛异常这是 Halcon 最容易让人忽视的问题——你设了曝光 5000但固件参数名不对程序照跑画面照出就是曝光值完全没生效。下面这段代码展示了 Demo 工程里最常见的一段初始化逻辑// 创建 Framegrabber 对象 HFramegrabber grabber new HFramegrabber(); try { // 连接 Basler 千兆网相机 grabber.OpenFramegrabber( GigEVision2, 0, 0, 0, 0, 0, 0, default, -1, default, -1, false, default, , 0, -1); // 相机参数写入Basler 旧固件识别 ExposureTimeAbs新固件识别 ExposureTime grabber.SetFramegrabberParam(ExposureTimeAbs, 5000.0); // 曝光 5ms grabber.SetFramegrabberParam(GainAbs, 0.0); // 增益 0dB // 自由运行模式不启用外部触发 grabber.SetFramegrabberParam(TriggerMode, Off); } catch (HalconException ex) { MessageBox.Show(相机初始化失败 ex.GetErrorMessage(0)); }写这段代码时参数名一定要按相机实际支持的命名来。Halcon 对 GigE Vision 相机的参数写入是透传的参数名完全取决于相机固件。Basler 新固件里曝光时间就叫ExposureTime老固件则是ExposureTimeAbs一旦写错SetFramegrabberParam 不报错但画面亮度纹丝不动。我自己的习惯是先在 HDevelop 的 Image Acquisition 助手点开参数树确认当前相机真正的参数名再回 C# 里同步修改。3.2 单帧 GrabImage 与异步 GrabImageAsync 的区别连接建立之后抓帧只有两个入口GrabImage和GrabImageAsync。字面看一个是同步一个是异步但实际差别在等待策略上。GrabImage是阻塞式抓帧相机没有新图时线程一直挂着GrabImageAsync则允许你传入一个超时时间时间到了没抓到帧就返回由上层代码决定是重试还是报错。Demo 工程如果只是采集单张图像做静态检测用GrabImage就够了// 单帧采集阻塞等待相机返回一帧图像 HObject hImage new HObject(); grabber.GrabImage(out hImage); // hImage 就是 Halcon 格式的灰度图像可直接送往算法模块如果是连续动态显示比如需要像相机预览一样实时刷界面那GrabImageAsync更合适。常见做法是单独开一个采集线程循环调用每次拿到新图就触发界面刷新// 异步连续采集timeout 参数 -1 表示无限等待实际项目中常设为 500ms HObject hImage; while (isGrabbing) { grabber.GrabImageAsync(out hImage, -1); // 这里拿到的 hImage 需要在界面线程上转换为 Bitmap 后显示 // 也可以在此处直接执行模板匹配、OCR 等 Halcon 算法 }循环里拿到的hImage对象是复用的还是新建的取决于 Halcon 内部的内存管理策略。稳妥做法是每次循环结束把上一帧hImage.Dispose()掉免得大分辨率相机长时间运行撑爆非托管内存。3.3 HObject 转成 Bitmap 显示到界面上Halcon 的图像数据是私有格式C# 的 PictureBox 不认识必须转成System.Drawing.Bitmap才能显示。这也是新手最容易卡住的地方因为 Halcon 的HObject底层数据存放在非托管内存里取出指针之后要自己负责数据拷贝和释放。常规写法是先用GetImagePointer1拿到灰度图像的内存地址、宽、高再把数据逐字节复制到 Bitmap 的缓冲区里// HObject 转 8 位灰度 Bitmap public Bitmap HObjectToBitmap(HObject hObject) { HTuple pointer, type, width, height; HOperatorSet.GetImagePointer1(hObject, out pointer, out type, out width, out height); int w width.I; int h height.I; byte[] buffer new byte[w * h]; // 从非托管内存拷贝图像数据到托管数组 System.Runtime.InteropServices.Marshal.Copy(pointer.IP, buffer, 0, w * h); // 构造 8 位灰度 Bitmap 并写入像素 Bitmap bmp new Bitmap(w, h, System.Drawing.Imaging.PixelFormat.Format8bppIndexed); System.Drawing.Imaging.ColorPalette palette bmp.Palette; for (int i 0; i 256; i) { palette.Entries[i] Color.FromArgb(i, i, i); } bmp.Palette palette; System.Drawing.Imaging.BitmapData bmpData bmp.LockBits( new Rectangle(0, 0, w, h), System.Drawing.Imaging.ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format8bppIndexed); System.Runtime.InteropServices.Marshal.Copy(buffer, 0, bmpData.Scan0, buffer.Length); bmp.UnlockBits(bmpData); return bmp; }这套代码有两个细节值得记住。第一GetImagePointer1只适用于单通道灰度图彩色图要用GetImagePointer3拿到的pointer是三个数组分别对应 R、G、B 通道拷贝时不能像灰度图这样直接一整块搬。第二Marshal.Copy是数据从非托管到托管的桥梁但HObject对象本身在调用完成后要及时Dispose()否则长期连续采集会出现内存稳步增长的状况。4. 源码文件拆解Demo.sln 从入口到界面各文件职责4.1 Program.cs 与 Form1.cs 里藏着的主线这份源码包里文件列出来很容易看花眼但真正决定程序行为的是三个文件Program.cs、Form1.cs和Form1.Designer.cs。多数采集 Demo 的长相都差不多Program.cs负责启动窗口程序Form1.cs承载采集和显示逻辑Designer.cs是 Visual Studio 自动生成的界面布局代码。Program.cs没什么神秘的地方就是标准 WinForms 入口// Program.cs static class Program { [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); // 主窗口所有采集动作从 Form1 开始 } }真正的关键逻辑在Form1.cs里。你会在里面找到一个HFramegrabber类型的成员变量、一个窗体加载或按钮点击事件里执行的相机初始化方法以及一个 PictureBox 控件来承接转换后的 Bitmap。从源码包结构能推断出这个 Demo 采用了「按钮触发 事件回调」的简单模型点一下采集按钮程序调用 GrabImage 拿到 HObject再转 Bitmap 扔给 PictureBox。Form1.Designer.cs里要关注的点只有一个PictureBox 的SizeMode属性。低速预览场景用Zoom保持比例高速连续采集场景建议保留Normal避免每次图像尺寸变化都触发重算布局否则界面卡顿会直接影响操作手感。4.2 csproj 引用和 bin 目录决定了程序怎么跑起来Demo.csproj这个文件平时不常打开但一旦换电脑编译它决定了工程能不能顺利还原。双击打开会发现里面对halcondotnet.dll的引用路径指向了具体的 Halcon 安装目录。路径一旦不存在Visual Studio 的引用管理器里会显示黄色警告图标编译也会在调用 Halcon 类型时报错。常规处理方式是把 DLL 引用改为复制本地让程序输出目录自带一份副本!-- Demo.csproj 中的 Halcon 引用节点重点是 Private 标签 -- Reference Includehalcondotnet HintPathC:\Program Files\MVTec\HALCON-20.11\bin\dotnet\halcondotnet.dll/HintPath PrivateTrue/Private /Referencebin目录是程序集的最终输出位置运行 Demo.exe 后可以看看它旁边是否躺着halcondotnet.dll和 Halcon 运行时的其他依赖文件。如果缺失运行到HFramegrabber初始化时大概率会崩。obj目录则是编译中间产物平时不用碰也不用提交到代码仓库UpgradeLog.XML是工程升级时 Visual Studio 自动生成的升级日志看系统版本升级情况时可以翻平时忽略即可。4.3 从这份 Demo 改造成自己项目要动哪几个文件拿着这份源码去接自己现场的新相机不需要逐文件理解改三个地方就能跑通。第一确认相机接口类型是 GigEVision2 还是 USB3Vision改掉 OpenFramegrabber 的第一个参数第二改成自己相机实际支持的曝光参数名和值第三把抓帧后接的显示逻辑替换成实际业务逻辑。以换另一台不同分辨率的 Basler 相机为例要动的代码位置如下// 假设现场相机是 USB3 接口把相机类型改为 USB3Vision grabber.OpenFramegrabber( USB3Vision, // 相机类型调整 0, 0, 0, 0, 0, 0, default, -1, default, -1, false, default, , 0, -1); // 分辨率无需手动设置OpenFramegrabber 会用默认高宽 0 去读相机当前格式 // 但曝光参数名要一起核对USB3Vision 接口下命名风格和 GigE 略有差异实际操作中相机分辨率的变化会引起 HObject 转 Bitmap 时数组大小的重新计算但只要代码里宽高都读的是实际返回的hWidth和hHeight就不会有越界问题。最忌讳的是把宽高写死因为换相机或者调整 ROI 之后图像尺寸一变越界错误只在采图的那一刻暴露排查起来反倒比编译错误麻烦得多。5. 避坑排查Basler 相机 Halcon 采集的常见故障与处理5.1 相机打开失败枚举不到设备现象OpenFramegrabber不报错但后续GrabImage时抛出HALCON error #5300或者提示设备不可用更常见的是 HDevelop 里设备列表为空。原因九成是网络不通或者网卡巨型帧关闭。Basler 千兆网相机裸机直连电脑时如果电脑网卡是自动获取 IP而相机是固定 IP两边不在同一网段Halcon 的设备发现机制就扫不到。另一部分原因是 Halcon 安装时没有勾选对应相机接口的运行时支持。解决先把电脑网卡 IP 改成手动与相机同段。然后在 HDevelop 里执行list_framegrabber_attributes (GigEVision2, 0, 0, 0, (), device, Information, Value)能看到设备描述符说明链路没问题。如果还是不显示到网卡驱动属性里手动开启巨型帧Jumbo Frame值设为 9000 字节。从那以后我再接任何 GigE 相机第一次跑通的前提永远是这招先确认设备枚举过了再谈写法。5.2 采出来的图全黑曝光和增益没有真正写进相机现象相机连接正常抓帧也不报错但 pictureBox 显示的图全黑或者亮度完全不受曝光参数控制。原因参数名不匹配。Halcon 的 SetFramegrabberParam 对相机固件里不存在的参数名不报错只是静默忽略。Basler 老固件用ExposureTimeAbs新固件用ExposureTime两者混写就出现这种不报错但无效的翻车现象。解决先在 HDevelop 里用 Image Acquisition 助手打开相机查看参数列表里实际的名字和单位。拿到确切参数名后再回 C# 里改。顺便确认曝光单位Basler 多为微秒但个别型号是可配置的单位不对同样导致画面暗到没法用。这个坑我踩过一次之后每次新接相机都会花两分钟先看一遍参数树再写代码。5.3 画面撕裂和掉帧包大小、缓冲区与网卡设置现象连续采集时画面出现横条撕裂或者帧率远低于相机标称值画面每隔几秒卡一下。原因GigE Vision 相机把一帧图像拆成多个网络包传输网卡不支持巨型帧时一帧数据需要拆成更多小包带宽占用暴涨丢包就会表现为撕裂和掉帧。另一个原因是 Halcon 帧缓冲不足抓帧速度跟不上采集速度时新图直接覆盖旧图造成花屏。解决网卡开启巨型帧设置接收缓冲区为最大值在 Halcon 侧调大帧缓冲参数MaxPartBuffer和MaxPartBufferMemory缓解瞬时突发流量。这两个参数在 SetFramegrabberParam 里可以动态调整建议在 OpenFramegrabber 之后立刻设置。现场调试时先看相机每秒输出多少帧再把业务处理的耗时压到帧间隔以下连续采集才能稳。5.4 关闭窗体时程序卡死取流线程没有及时退出现象程序运行时一切正常但点击关闭按钮后窗体无响应任务管理器里能看到进程还在CPU 占用还不低。原因采集线程陷入GrabImageAsync的阻塞等待窗体关闭事件触发之后没有先通知线程退出后台线程还在等相机的下一帧数据。线程退出逻辑写得不干净窗体 Dispose 时采集对象被释放但线程还在使用它直接引发访问已释放对象的异常。解决窗体关闭前先设置退出标志位并调用一次超时很短的GrabImageAsync让线程从阻塞中醒来再Join等待线程结束。我给这个 Demo 工程的改造建议是// 关闭窗体的处理顺序先停线程再释放相机 private void Form1_FormClosing(object sender, FormClosingEventArgs e) { isGrabbing false; // 让循环条件失效 GC.Collect(); // 不能保证线程退出实际工程不建议这样处理 // 正确做法是给 GrabImageAsync 传一个短超时例如 200ms // 这样线程能在超时后检查 isGrabbing 标志并主动退出 }实际操作中给GrabImageAsync的等待时间设一个固定值比如 200ms循环里一进来就检查退出标志这样窗体关闭时最多延迟 200ms 就能干净退出。这个习惯我后来用到了所有相机项目中再没出现过关窗卡死或句柄泄漏。5.5 与 OpenCV 混用时的图像数据格式冲突现象用 Halcon 采完图调用 Emgu CV 的 Mat 做处理时崩溃或者图像颜色通道顺序不对红色显示成蓝色。原因Halcon 的 HObject 和 OpenCV 的 Mat 内存布局不兼容。Halcon 的彩色图通道顺序是 RGBOpenCV 是 BGR直接拿 Halcon 的内存指针转 Mat 后不交换通道颜色必然出问题。更深一层是步长问题Halcon 图像每行数据有对齐补齐OpenCV 默认连续存放行跨越版本和分辨率不同而不同。解决转换时用 GetImagePointer3 取三个通道的独立指针分别拷贝进 Mat 的三个通道之后再合并灰度图相对安全直接拷贝到单通道 Mat 就行。千万不要图省事把 HObject 内部指针直接赋给 Mat 的数据区Halcon 释放图像时这个指针就变成悬空地址崩溃只是时间问题。我现在的固定套路是Halcon 采图 → 转成Bitmap→ 再用Bitmap构造 Mat虽然多一次拷贝但每个环节的数据生命周期都清晰可控。6. 把采集链路做稳参数顺序、异步取流与验证习惯6.1 参数设置顺序和取流线程的推荐结构同样是那套初始化代码参数写入顺序不同表现出来的稳定性完全不同。推荐顺序是先 OpenFramegrabber 建会话再调图像格式相关的参数最后设置触发模式和曝光增益。原因在于 GigE Vision 相机的很多参数之间存在依赖关系比如先启用了外部触发再改触发源触发源参数会被相机忽略。自由运行模式下则是先设TriggerModeOff再设曝光顺序反了偶尔会出现首次抓帧黑图的假死现象。采集线程我习惯单独封装成一个类把相机对象、运行标志、错误回调都收敛进去窗体只管调 Start 和 Stop。线程内部保持一个循环每轮做四件事检查运行标志、调用 GrabImageAsync、转换格式、触发外部回调。这样的结构哪怕相机意外断开线程也能在超时之后响应上层命令而不是永远卡死在抓帧里。6.2 验证链路健康的两个快速手段第一个手段是连续采集 100 帧统计帧间隔抖动。稳定链路的最大帧间隔和最小帧间隔之差应该在一帧以内波动差出三帧以上就说明有阻塞在干扰取流优先检查网卡中断调节和杀毒软件的实时扫描。第二个手段是断开网线再插回去看程序能否自动重连。Halcon 的 GigEVision2 接口对设备热插拔支持得不错但前提是你在异常回调里做了清理和重新枚举跑通这个测试现场维护时能少接很多电话。另一个值得坚持的习惯是每次改完相机参数重新编译先在 HDevelop 里用同一参数跑一遍再回 C# 里跑。HDevelop 会直接报参数不支持的详细错误C# 里常常只是静默吞掉。分开验证能节省大量查参数名的时间遇到 C# 里不生效的参数HDevelop 往往一句话就告诉你真相。从那以后我每次新接一台相机都强制自己先走一遍设备枚举、HDevelop 参数验证、C# 连续采集跑帧率这三板斧确认没问题再往下写业务逻辑。希望这份源码的拆解和避坑梳理能帮到你。本文还有配套的精品资源点击获取
返回列表