
简介这是一份面向HALCON与C#联合编程学习者的入门示例工程解决如何在WinForms中读取并显示CAD图纸DXF格式的问题适合正在做机器视觉上位机集成的开发者参考。资源包共34个文件包含9个C#源文件、3个可执行程序、2个资源文件以及1个DXF测试图另有配置文件、PDB调试符号和工程解决方案等整体约452KB能在Visual Studio中直接打开调试。作者特意将CAD文件采用相对路径引用并将工程目录放在桌面运行便于快速验证效果。已吸引2132人浏览学习具有一定参考热度。通过这份工程读者可以掌握HALCON区域与CAD轮廓的数据转换思路、C#中引用halcondotnet.dll的常见配置方法以及从窗体界面触发读取与显示的基本流程工程内保留的PDB和调试文件也有助于初学者对照分析是快速上手HALCON/C#混合编程的实用样例。 很多做视觉项目的朋友第一次听到“HALCON读取CAD”这个需求时第一反应都是HALCON不是处理图像的吗怎么还跟CAD扯上关系了实际上在精密装配、缺陷检测、定位引导这类项目里CAD图纸就是工件的“理想模板”相机拍到的图像是“现实状态”把两者叠加在一起看偏差是视觉工程师最常干的活之一。这篇博客就把HALCON和C#联合编程里读取CAD文件并显示到界面这件事讲透。我最早接触这个需求是在做一款手机中框尺寸检测设备的时候。客户要求上位机界面上能同时显示相机拍到的图像和产品CAD图纸的轮廓操作员一眼就能看出哪个位置偏了、偏了多少。后来陆陆续续在好几个项目里都碰到类似场景索性把HALCON里和CAD/DXF相关的那套算子研究了个遍。本文就围绕“读取一个CAD并显示”这条主线把从环境准备到实际踩坑的完整链路写出来供刚开始接触HALCON和C#联合编程的朋友参考。1. 为什么要在视觉界面里显示CAD这个需求到底解决什么问题1.1 从一次客户需求说起那台中框检测设备客户给的初始需求只有一句话“界面上要能看到产品轮廓和图纸的对比图”。等真正落地的时候才发现这里面的“产品轮廓”来自相机拍到的图像“图纸”来自结构工程师提供的DWG文件。两套坐标系、两种数据格式要准确叠加到一个显示控件里难度比想象中大不少。1.2 显示CAD轮廓的三个典型应用场景第一个场景是尺寸测量项目。测量结果出来以后光给一堆数值操作员其实很难判断哪个位置超差。如果在测量结果界面把CAD轮廓画出来再把实际检测到的边缘点叠加上去所有偏差一目了然。第二个场景是装配引导。机器人抓取工件放到治具上之前视觉系统要先把工件在图像里的实际位置算出来和CAD定义的理想位置做对比把偏移量发给机器人。界面上显示CAD轮廓能让调试人员非常直观地验证偏移量的计算结果对不对。第三个场景是缺陷检测。很多表面缺陷的判定标准是“和标准形状的差异有多大”这时候CAD轮廓就是那个“标准形状”图像上的实际边缘和它的差异区域就是疑似缺陷。1.3 为什么选HALCON而不是用CAD控件这个问题我一开始也纠结过。C#里加载DXF的第三方库不少比如netDXF、Aspose.CAD还有各种收费控件。但最后我还是选HALCON原因有三个。一是HALCON的算子直接返回轮廓对象XLD后续跟图像处理流程无缝衔接。在netDXF里读出的是CAD实体列表要显示还得自己把线段、圆弧一条条画到界面上在HALCON里读出来就是现成的contour直接用disp_obj就能显示还能直接参与距离计算、形状匹配这些后续操作。二是HALCON自带窗口控件HWindowControl缩放、平移、灰度显示、彩色叠加全都内置了做一个简易CAD浏览器也就几十行代码。如果自己用GDI画DXF光是鼠标缩放平移的交互逻辑就够写好几天的。三是数据格式统一。图像是HObjectCAD轮廓也是HObject同一个对象类型同一个显示接口上层逻辑简洁很多。提示如果你的项目只是单纯查看CAD文件不需要和图像做任何关联那用专门的CAD浏览器控件确实更合适。但只要涉及“CAD坐标和图像坐标对应”这件事HALCON几乎是唯一不需要来回转换数据结构的方案我个人的建议是优先考虑HALCON。2. 开工之前HALCON版本、C#工程和运行环境的一次性配置2.1 HALCON版本选择读取DXF的算子是read_contour_xld_dxf这个算子从HALCON很早期的版本就有了接口一直没大变。我自己最早是在HALCON 13上用它后来项目换到HALCON 17.12代码基本没改直接跑。HALCON 20.11以后新增了一些基于深度学习的CAD相关功能但基础读取逻辑没变。只要你的HALCON版本不是太老≥13基本都行读DXF这步不会有版本差异问题。2.2 C#工程配置要点用Visual Studio创建WinForms项目我习惯用.NET Framework 4.6.1或4.7.2HALCON的.NET接口对这个版本支持最稳。如果用.NET Core或.NET 6需要确认你那个版本的HALCON是否提供了对应的运行时。添加引用的时候找到HALCON安装目录下的halcondotnet.dll一般在C:\Program Files\MVTec\HALCON-17.12\bin\dotnet35或者dotnet40文件夹里。手动Browse添加不要用NuGet。这里有个特别容易坑到新手的点平台目标一定要设置成X64。HALCON从13版开始就主推64位HDevelop里默认的运行时也是64位。如果你的项目平台目标是Any CPU或者X86运行时大概率会报System.BadImageFormatException或者HALCON error #4326之类莫名其妙的问题。项目右键属性 - 生成 - 平台目标选x64在最开始就改好能省掉后面一大串排查时间。License方面HALCON的license文件扩展名.dat默认放在C:\Program Files\MVTec\HALCON-17.12\license目录如果联网授权安装工具会自动配置。C#调用时license异常通常表现为HALCON error #20011这个也和授权方式无关确认license文件是否有效就行。2.3 运行库全局设置HALCON安装完后环境变量里会自动加上HALCONROOT和PATH。C#程序运行的机器上如果没装HALCON完整版就得把bin\x64-win64目录加到PATH或者把用到的DLLhalcon.dll、hdevengine.dll等直接拷到exe同目录。我一般推荐直接拷DLL部署省心。3. 读取DXF文件read_contour_xld_dxf算子的完整用法与原理拆解3.1 DXF是什么和DWG有什么区别这里先回答一个特别常见的困惑为什么只能读DXF不能读DWGDWG是AutoCAD的私有格式Autodesk从未公开完整规范所以几乎所有第三方库默认都只支持DXF。DXF是AutoCAD的公开交换格式本质是纯文本文件也有二进制变体里面一行行的组码定义了直线、圆弧、多段线等实体的几何信息。HALCON通过解析DXF文本里的实体记录再把圆弧、样条这些曲线转换成XLD轮廓。所以拿到的DXF文件里面的实体本身就是线段和弧线HALCON将它转成XLD轮廓数组输出。3.2 算子签名与返回值read_contour_xld_dxf在HDevelop里的签名是这样的read_contour_xld_dxf( : Contours : FileName, GenParamName, GenParamValue : DxfStatus)参数不多Contours输出读到的所有XLD轮廓FileName输入DXF文件的完整路径GenParamName / GenParamValue通用参数控制是否填充集合、坐标缩放等一般传空数组就行DxfStatus输出成功读到的实体数量正常情况下一个DXF文件里的所有几何实体直线、圆弧、多段线会被读取成多个独立轮廓对象放在Contours这个数组里。后面显示的时候就要注意不能只显示Contours[0]得把整个数组显示出来。3.3 C#里的实际调用代码C#里调用HALCON的算子统一走HOperatorSet这个静态类。以HALCON 17.12为例代码如下using HalconDotNet; HObject contours null; HTuple dxfStatus null; try { HOperatorSet.ReadContourXldDxf(out contours, D:\drawing\part.dxf, new HTuple(), new HTuple(), out dxfStatus); int contourCount contours.CountObj(); Console.WriteLine($成功读取 {contourCount} 条轮廓); } catch (HOperatorException ex) { Console.WriteLine(读取失败 ex.Message); }注意两个细节一是CountObj()方法HALCON的XLD对象默认是“对象数组”一个HObject里可能装着几条甚至几万条轮廓要遍历必须用SelectObj取出单个对象。二是算子的输出参数要用out关键字。3.4 提取轮廓数量与实际对象读完之后如果要在界面上逐个处理比如统计总长度、筛选特定长度的轮廓那就得遍历。遍历的方式永远是for (int i 1; i contourCount; i) { HObject singleContour contours.SelectObj(i); // 对单个轮廓做后续处理 singleContour.Dispose(); }HALCON的对象索引从1开始不是0。这一点和C#的习惯完全不同我见过不少同事因为循环从0开始结果SelectObj(0)直接报错。个人经验如果读到的CAD文件里含有大量标注线、中心线这些辅助图元建议在CAD里先新建一个图层只把真正需要显示的产品轮廓放到这个层然后把这个图层单独另存成DXF。这样HALCON读到的轮廓干净既省内存又省后面的筛选工作。4. 把轮廓显示到窗体HWindowControl的坐标系、缩放和居中方案4.1 HWindowControl控件的初始化在WinForms里显示HALCON图像/轮廓标准做法是拖一个HWindowControl控件到窗体上。这个控件本质是HALCON窗口的.NET封装内部持有一个HWindow句柄。初始化代码如下HWindowControl hWindowControl1; // 窗体加载时 hWindowControl1.HalconWindow.SetColor(green); hWindowControl1.HalconWindow.SetLineWidth(2); hWindowControl1.HalconWindow.SetDraw(margin);SetColor控制显示颜色SetLineWidth控制线条粗细SetDraw(margin)表示只显示轮廓边缘不填充内部。4.2 直接显示会遇到的问题坐标系镜像很多人第一次读到轮廓直接disp会发现一个奇怪现象显示的图形是上下颠倒的。原因很简单HALCON的图像坐标系原点在左上角Y轴向下而CAD的笛卡尔坐标系Y轴向上。HALCON里disp_obj是以图像坐标系为基准显示的直接把CAD读出来的轮廓丢进去Y轴就反了。解决办法是先用affine_trans_contour_xld做一个Y轴翻转。翻转的本质是构造一个缩放矩阵Y方向缩放因子为-1再配合平移把图形拉回可见区域。HTuple homMat2D; HOperatorSet.HomMat2dIdentity(out homMat2D); HOperatorSet.HomMat2dScale(homMat2D, 1.0, -1.0, 0, 0, out homMat2D); HOperatorSet.HomMat2dTranslate(homMat2D, 0, -1024, out homMat2D); // 1024为翻转后Y方向最大坐标需根据实际情况调整 HOperatorSet.AffineTransContourXld(contours, out contoursFliped, homMat2D);这个翻转操作看起来简单但确实容易栽跟头。我个人建议在所有CAD显示逻辑里都加上这一步不要偷懒。4.3 自动缩放和居中不依赖原始坐标范围CAD图纸坐标范围可能很大比如一张零件图从(-100, -100)到(100, 100)也可能很小比如微型芯片只有几个mm。如果直接按原始坐标显示要么图太大出界要么小到看不见。所以在显示前要计算轮廓的总包围盒然后根据窗口大小缩放。核心代码HTuple row1, col1, row2, col2; HOperatorSet.SmallestRectangle1Xld(contoursFliped, out row1, out col1, out row2, out col2); double width col2.D - col1.D; double height row2.D - row1.D; double scaleX hWindowControl1.Size.Width / width; double scaleY hWindowControl1.Size.Height / height; double scale Math.Min(scaleX, scaleY) * 0.9; // 留10%边距 HTuple scaleHomMat; HOperatorSet.HomMat2dIdentity(out scaleHomMat); HOperatorSet.HomMat2dScale(scaleHomMat, scale, scale, 0, 0, out scaleHomMat); HOperatorSet.AffineTransContourXld(contoursFliped, out contoursScaled, scaleHomMat); // 再平移到窗口中心 HTuple sRow1, sCol1, sRow2, sCol2; HOperatorSet.SmallestRectangle1Xld(contoursScaled, out sRow1, out sCol1, out sRow2, out sCol2); double centerX (sCol1.D sCol2.D) / 2.0; double centerY (sRow1.D sRow2.D) / 2.0; double offsetX hWindowControl1.Size.Width / 2.0 - centerX; double offsetY hWindowControl1.Size.Height / 2.0 - centerY; HOperatorSet.HomMat2dTranslate(scaleHomMat, offsetY, offsetX, out scaleHomMat); HOperatorSet.AffineTransContourXld(contoursFliped, out contoursDisp, scaleHomMat); HOperatorSet.DispObj(contoursDisp, hWindowControl1.HalconWindow);这段逻辑概括起来就是先缩放到窗口能装得下再平移到窗口中心。两个步骤用同一个homMat2D矩阵表示这样计算简单也好理解。4.4 直接使用窗口设置的简化方案如果不想做这么多矩阵变换也可以用HALCON的set_part自动适配。set_part设置窗口显示的图像区域把轮廓的包围盒设为显示区HALCON会自动缩放。HOperatorSet.SetPart(hWindowControl1.HalconWindow, (int)row1.D, (int)col1.D, (int)row2.D, (int)col2.D); HOperatorSet.DispObj(contoursFliped, hWindowControl1.HalconWindow);这个方法缺点是缩放是自动的不好控制边距比例而且如果轮廓坐标范围不是整数set_part会有偏差。我的做法是调试阶段用set_part正式功能用仿射变换矩阵因为后者可控性更强。5. 从“能看见”到“能用上”CAD轮廓和图像坐标的对应关系5.1 显示只是第一步叠加才是关键界面上显示CAD图形最终目的一定是让它和相机图像对齐用图像处理算出的实际边缘位置和CAD理想位置做对比。这意味着CAD轮廓的坐标必须和图像坐标在同一个参考系里。但两者天然不同图像坐标是像素原点在左上角CAD坐标是图纸的尺寸单位通常是毫米原点在图纸原始位置。不做一个坐标变换叠加出来的结果必然是错位的。5.2 两种常见坐标映射方式第一个方式忽略物理意义直接用像素坐标。如果相机分辨率固定可以测量图像里某一已知长度对应多少像素得出像素/毫米的换算系数然后对CAD轮廓统一缩放。这种方式适用于图像没有畸变、工件位置固定的场景。第二个方式通过标定Calibration确定世界坐标到像素坐标的变换。HALCON里有完整的标定工具箱calibrate_cameras标定后得到相机内参和外参然后构造HomMat2D。但对于“读取CAD并显示”这个需求本身多数时候我们都是先做交互式验证在窗口里显示CAD轮廓鼠标拖动轮廓到图像上大致对齐的位置验证转换逻辑是否正确。HALCON里实现拖动非常简单——鼠标按下记录起点鼠标移动时用一小段平移矩阵更新轮廓位置再重新显示。5.3 平移旋转变换参考代码假设我们已经知道了CAD轮廓应该平移多少offsetY, offsetX、旋转多少度angle用下面的代码生成变换矩阵并应用到轮廓上HTuple homMat2D; HOperatorSet.HomMat2dIdentity(out homMat2D); HOperatorSet.HomMat2dRotate(homMat2D, angleRad, 0, 0, out homMat2D); HOperatorSet.HomMat2dTranslate(homMat2D, offsetY, offsetX, out homMat2D); HObject contoursTransformed; HOperatorSet.AffineTransContourXld(contoursFliped, out contoursTransformed, homMat2D); HOperatorSet.DispObj(contoursTransformed, hWindowControl1.HalconWindow);特别注意HALCON里HomMat2d顺序是先旋转再平移如果你先Translate再Rotate结果会完全不一样。后面操作矩阵的时候要时刻记着“矩阵左乘”的规则算子API文档里明确写了旋转和平移的顺序写代码时务必确认。5.4 叠加显示的最简单实现如果只是想在图像上叠加CAD轮廓用disp_obj连续显示两个对象即可HALCON窗口自带的Paint模式会自动完成覆盖HOperatorSet.DispObj(image, hWindowControl1.HalconWindow); // 先显示图像 HOperatorSet.SetColor(hWindowControl1.HalconWindow, red); // 再换色 HOperatorSet.DispObj(contoursTransformed, hWindowControl1.HalconWindow); // 叠加轮廓这样图像是灰度的轮廓是红色的偏差位置一眼就能看出来。6. 实测中绕不开的坑DXF版本兼容、内存释放和界面卡顿6.1 DXF文件的“炸开”问题块和嵌套实体国产CAD软件另存的DXF经常包含块(Block)定义。块在DXF文件里是一个独立区域HALCON的read_contour_xld_dxf对块的支持一直有点弱——它主要解析模型空间(Model Space)里的实体块定义里的实体不一定能全部读出来。表现为DXF文件在CAD里看着好好的读进HALCON就少了一部分图形尤其那些由块组成的标准件、螺栓孔等。解决措施是在CAD里选中所有图形执行“炸开”Explode操作把块还原成基础实体然后再另存为DXF。我后来总结出一个标准流程收到DWG文件 - CAD里打开 - 全选炸开可能要炸两三次 - 删除标注、中心线等不需要的图层 - 另存为DXF R12或AutoCAD 2000格式这样HALCON读起来最稳。6.2 版本格式选择R12很多时候比高版本更可靠DXF文件格式从AutoCAD R12一直演化到现在的2024版。HALCON的DXF解析器推进得不算快高版本DXF里一些新实体类型比如动态块、约束、填充图案的特殊表达解析时容易出问题。我实测下来最稳妥的是保存为DXF R12格式。虽然它丢失了很多现代特性但基础几何直线、圆弧、多段线绰绰有余而且文本结构最简单HALCON解析速度更快也不容易出现“读出来是空的”这种诡异问题。6.3 内存释放HObject必须显式DisposeHALCON的.NET接口里HObject是继承IDisposable的。C#的GC不会主动释放HALCON内部的原生内存必须手动Dispose。否则程序跑一段时间内存占用哗哗往上涨最后直接OOM。规范写法HObject contours null; try { HOperatorSet.ReadContourXldDxf(out contours, filePath, new HTuple(), new HTuple(), out _); // 处理轮廓... HOperatorSet.DispObj(contours, hWindowControl1.HalconWindow); } catch (Exception ex) { // 处理异常 } finally { contours?.Dispose(); }如果多步处理中产生了中间轮廓比如翻转后的、缩放后的每个中间变量都要Dispose不要只释放最后那个。一个项目里几十处调用每一处都漏一点内存泄漏就非常可观了。建议写一个工具方法统一对HObject数组做Dispose。6.4 大图纸显示卡顿精简轮廓数量一份完整的产品DXF可能包含数千条轮廓一次性显示到窗口会很卡尤其在低配工控机上。HALCON自带的调试窗口和用户界面窗口处理大量对象都吃性能操作员拖动缩放时更是灾难。两个思路可以缓解按长度过滤标注线、中心线通常是很短的线段用select_shape_xld按轮廓长度或者包围盒面积过滤掉一部分。合并轮廓把相邻、同轴的轮廓用union_adjacent_contours_xld合并减少对象个数。HObject selectedContours; HOperatorSet.SelectShapeXld(contours, out selectedContours, contlength, and, 50, 99999);过滤的判断标准没有统一公式完全看图纸内容。我是先用一个临时窗口把所有轮廓按长度分布显示一遍肉眼确定阈值再写进正式代码。6.5 窗体关闭时的AccessViolation用HWindowControl遇到的另一个典型问题窗体关闭时如果HALCON后台还有绘图操作可能会报AccessViolationException程序直接崩掉。解法是窗体关闭事件里先把窗口停掉再释放资源protected override void OnFormClosing(FormClosingEventArgs e) { hWindowControl1.HalconWindow.ClearWindow(); hWindowControl1.Dispose(); base.OnFormClosing(e); }ClearWindow把画面清空避免关闭瞬间还在绘制。7. 一个完整的读取并显示CAD的最小示例最后给一个可以直接抄的完整示例。场景窗体上放一个HWindowControl和一个按钮点击按钮选择DXF文件读取后翻转、缩放、居中显示。private void btnLoadCad_Click(object sender, EventArgs e) { OpenFileDialog dlg new OpenFileDialog(); dlg.Filter DXF文件|*.dxf|DWG文件|*.dwg; if (dlg.ShowDialog() ! DialogResult.OK) return; HObject contours null; HObject contoursFliped null; HObject contoursScaled null; try { HOperatorSet.ReadContourXldDxf(out contours, dlg.FileName, new HTuple(), new HTuple(), out _); // 1. 翻转Y轴 HTuple homMat; HOperatorSet.HomMat2dIdentity(out homMat); HOperatorSet.HomMat2dScale(homMat, 1.0, -1.0, 0, 0, out homMat); HOperatorSet.HomMat2dTranslate(homMat, 0, -1024, out homMat); HOperatorSet.AffineTransContourXld(contours, out contoursFliped, homMat); // 2. 计算包围盒并缩放居中 HTuple r1, c1, r2, c2; HOperatorSet.SmallestRectangle1Xld(contoursFliped, out r1, out c1, out r2, out c2); double w c2.D - c1.D; double h r2.D - r1.D; double scale Math.Min(hWindowControl1.Width / w, hWindowControl1.Height / h) * 0.9; HTuple homScale; HOperatorSet.HomMat2dIdentity(out homScale); HOperatorSet.HomMat2dScale(homScale, scale, scale, 0, 0, out homScale); HOperatorSet.AffineTransContourXld(contoursFliped, out contoursScaled, homScale); // 3. 平移到窗口中心 HOperatorSet.SmallestRectangle1Xld(contoursScaled, out r1, out c1, out r2, out c2); double cx (c1.D c2.D) / 2.0; double cy (r1.D r2.D) / 2.0; double tx hWindowControl1.Width / 2.0 - cx; double ty hWindowControl1.Height / 2.0 - cy; HOperatorSet.HomMat2dTranslate(homScale, ty, tx, out homScale); HObject contoursDisp; HOperatorSet.AffineTransContourXld(contoursFliped, out contoursDisp, homScale); // 4. 显示 hWindowControl1.HalconWindow.ClearWindow(); HOperatorSet.SetColor(hWindowControl1.HalconWindow, green); HOperatorSet.SetLineWidth(hWindowControl1.HalconWindow, 2); HOperatorSet.DispObj(contoursDisp, hWindowControl1.HalconWindow); contoursDisp.Dispose(); } catch (HOperatorException ex) { MessageBox.Show(读取或显示失败: ex.Message); } finally { contours?.Dispose(); contoursFliped?.Dispose(); contoursScaled?.Dispose(); } }我在实际项目里写的基本就是这套逻辑换过不同的图纸、不同的分辨率只要DXF是高版本另存为R12的基本一次通过。只要这套逻辑跑通后面无论是增加鼠标交互点选、拖动、缩放还是叠加图像都只是在现有框架上增加功能。坐标变换这里我再多说一句整个流程里最值得花时间验证的就是HALCON的Y轴镜像问题。你在HDevelop里手动跑一遍上面的算子流程确认翻转之后图形方向和原始CAD方向一致再往C#里搬能省掉很多无效调试。本文还有配套的精品资源点击获取