
搞机器视觉的同行一定遇到过这种场景模板匹配或者Blob分析给出一个旋转矩形的结果参数就五个数——Row、Column、Phi、Length1、Length2也就是Halcon里的rectangle2仿射矩形。显示的时候画一个框倒是简单但真要拿这个矩形去干活比如视觉引导抓取要计算物料四个角的位置、测量功能要沿着四条边的中点摆放卡尺、图像拼接要拿顶点做特征对齐你就会发现Halcon居然没有一个现成算子能把矩形角点直接拿出来。这个问题几乎每隔一段时间就有人在群里问一次我干脆把方案完整整理出来从坐标公式推导到实际代码一次讲透新手可以直接抄作业老手也可以看看有没有更优雅的写法。先说清楚这个思路能解决什么问题只要你的手里有一组合法的rectangle2参数不管它来自模板匹配的匹配结果、smallest_rectangle2求出的区域最小外接矩形还是find_metrology_object的测量对象都可以用同一套方法在微秒级算出四个顶点和四条边中点的精确坐标精度完全取决于这五个参数本身的精度。本文适合正在做视觉引导、缺陷检测、尺寸测量、图像校正的工程师参考也适合刚接触Halcon、对行列坐标系还不熟的初学者。1. 先搞清楚rectangle2的五个参数到底是什么1.1 Row、Column、Phi、Length1、Length2的含义Halcon里生成一个仿射矩形最常见的方式是调用gen_rectangle2算子它的输入参数顺序是这样的gen_rectangle2(Rectangle, Row, Column, Phi, Length1, Length2)分别对应中心点的行坐标、中心点的列坐标、旋转角度、半宽、半高。注意这里的Length1和Length2都是“半”长度不是完整的长和宽。也就是说一个Length1100的矩形它的主轴方向总长度是200这一点很多刚开始用的人会搞错。需要特别注意的是Phi使用的是弧度制。90度要写成rad(90)或者直接写0.785398而不是写90。如果你从其他地方拿到一个角度值是度数一定要先用rad()算子转成弧度再传进来否则矩形旋转的角度会完全不对这是一个极其常见的低级错误。还有一点容易被忽略Length1和Length2不要求Length1一定大于Length2但从实际约定上看大多数场景里Length1对应主轴方向的半长度Length2对应副轴方向的半宽度。如果调用smallest_rectangle2获取区域的最小外接矩形算子返回的参数就是经过排序的Length1永远是长边方向的半长度Length2是短边方向的半宽度Phi也自动是长轴的角度使用起来非常省心。1.2 为什么Halcon没有现成算子给角点坐标有人可能会问Halcon这么成熟的机器视觉库为什么不直接提供类似get_rectangle2_points这样的算子我猜测原因是rectangle2本质上是一个参数化的数学对象当它在图像上显示时Halcon内部已经在显示层把轮廓栅格化绘制出来了但作为“数据”来说五个参数已经完整定义了矩形的全部几何信息剩下的属于简单的坐标变换问题没必要为了这个单独封装一个算子。另外一个原因是算出来的角点和边中点怎么使用跟具体业务强相关。有人需要左上角作为抓取原点有人需要右上角作为测量起点有人需要四条边按顺序排列。Halcon与其提供一堆固定顺序的算子不如让开发者自己根据公式灵活组合。所以在实际项目中最常见的做法就是自己封装一个小函数把一组rectangle2参数转换出顶点和中点坐标放到公共库里反复调用。实际业务里这种需求特别常见比如最常见的一个场景用find_shape_model做模板匹配输出的匹配框就是一个仿射矩形。要在监控画面里把这个框变成四个角上带十字标记、边上带刻度的样子就需要把坐标算出来。再比如做测量项目时用gen_measure_rectangle2去创建测量句柄这个算子本身就接受rectangle2参数但如果要沿着矩形四条边分别取测量区域就得预先算好每条边的中点坐标和方向角这时候边中点的几何计算就成了绕不开的环节。2. 数学原理与坐标公式推导2.1 Halcon坐标系和角度方向的约定——最容易搞混的地方在推公式之前必须先把Halcon的坐标约定讲清楚因为这是整个方案里最容易翻车的地方。Halcon使用行列坐标Row表示从上往下数的行数Column表示从左往右数的列数。这跟数学里常见的x-y笛卡尔坐标系最大的区别在于Row轴的正方向是朝下的y轴的正方向变成图像下方而不是上方。在这样一个坐标系里Halcon规定rectangle2的Phi从Column轴正方向开始测量逆时针方向为正单位是弧度。也就是说当Phi0时矩形的主轴方向是完全水平的沿Column正方向延伸当Phiπ/2时主轴方向变成垂直向下的沿Row正方向延伸。这个约定刚接触时特别容易跟数学里的极坐标搞混我见过不少同行辛辛苦苦把公式推出来了结果套进去发现角度方向不对最后排查半天才发现是坐标系理解错了。如果你觉得“逆时针”这个概念在这种Row向下的坐标系里容易绕晕有一个更直观的记忆方法直接用Halcon的显示结果来验证。在HDevelop里随便生成一个Phi0.3的rectangle2并显示出来看看它相对于水平方向是怎么倾斜的记住这个图像感觉以后写代码的时候心里就有底了。实际编程时也可以先画一个标准矩形做自检避免在角度符号上栽跟头。2.2 顶点坐标公式的完整推导假设矩形的中心点坐标是(Row, Column)旋转角度是Phi主轴方向的半长度是Length1副轴方向的半长度是Length2。令c cos(Phi)s sin(Phi)。矩形主轴方向的单位向量在(Column, Row)坐标系中可以表示为(c, s)。意思是沿着主轴方向走单位长度Column方向增加cRow方向增加s。当Phi0时c1s0主轴沿Column正方向符合上面说的约定当Phiπ/2时c0s1主轴沿Row正方向也符合约定。副轴方向与主轴方向垂直在主轴的逆时针方向转过90度所以副轴方向的单位向量是(-s, c)。同样验证一下Phi0时副轴方向为(0,1)也就是Row正方向这是主轴水平时从上方向下看的右旋方向符合几何直觉。有了这两个方向向量之后从中心点出发沿四个方向分别走主轴和副轴的组合距离就得到了四个顶点坐标。我习惯按顺时针顺序定义四个顶点用右上、右下、左下、左上来命名这在Halcon的Row向下坐标系里看起来是顺时针但用起来非常直观右上顶点P1 ColumnP1 Column c * Length1 - s * Length2 RowP1 Row s * Length1 c * Length2右下顶点P2 ColumnP2 Column c * Length1 s * Length2 RowP2 Row s * Length1 - c * Length2左下顶点P3 ColumnP3 Column - c * Length1 s * Length2 RowP3 Row - s * Length1 - c * Length2左上顶点P4 ColumnP4 Column - c * Length1 - s * Length2 RowP4 Row - s * Length1 c * Length2这四个公式不要死记硬背只要记住中心点坐标分别加上主轴方向和副轴方向的组合偏移就行。写代码的时候如果怕搞混可以直接把主轴方向向量和副轴方向向量先算出来存在两个临时变量里后续的代码可读性会好很多。2.3 中点坐标公式与简化思路四个顶点都算出来之后四条边的中点最简单的方法自然是取两个相邻顶点的平均值但这不是最优解。仔细看的话可以发现每条边的中点本质上就是中心点沿主轴方向走Length1或者沿副轴方向走Length2得到的点公式可以简化成下面这样右边的中点P1和P2之间 ColumnM1 Column c * Length1 RowM1 Row s * Length1下边的中点P2和P3之间 ColumnM2 Column s * Length2 RowM2 Row - c * Length2左边的中点P3和P4之间 ColumnM3 Column - c * Length1 RowM3 Row - s * Length1上边的中点P4和P1之间 ColumnM4 Column - s * Length2 RowM4 Row c * Length2这个简化里的几何含义非常清楚右边中点就是中心点沿主轴正方向走半个主轴长度下边中点就是中心点沿副轴负方向走半个副轴长度。相比先算四个顶点再取平均这种方法不仅少了几次加法更重要的是代码语义更清晰别人读你代码的时候一眼就能看出是在求哪条边的中点。如果确实需要按顺序得到四个边中点也可以先算出顶点再两两平均这样可以严格保证点的顺序和顶点顺序对应。但如果你只是想在矩形边缘特定位置放测量卡尺直接用简化公式更方便算完还能少几次浮点运算。实际项目中我推荐两个版本都封装好以备不同场景使用。3. 完整Halcon代码实现3.1 基础版用三角函数直接计算下面这段代码是基础版本逻辑最直白适合刚接触Halcon的同学理解和调试。假设你从一个模板匹配结果里拿到了一组矩形参数现在要计算出四个顶点和四个边中点并在图像上画出来验证。* 输入矩形参数这里假设是从模板匹配结果中获取的 Row : 300.5 Column : 400.2 Phi : 0.35 Length1 : 120.0 Length2 : 80.0 * 计算三角函数值 CosPhi : cos(Phi) SinPhi : sin(Phi) * 计算四个顶点坐标顺序为右上、右下、左下、左上 P1_Row : Row SinPhi * Length1 CosPhi * Length2 P1_Col : Column CosPhi * Length1 - SinPhi * Length2 P2_Row : Row SinPhi * Length1 - CosPhi * Length2 P2_Col : Column CosPhi * Length1 SinPhi * Length2 P3_Row : Row - SinPhi * Length1 - CosPhi * Length2 P3_Col : Column - CosPhi * Length1 SinPhi * Length2 P4_Row : Row - SinPhi * Length1 CosPhi * Length2 P4_Col : Column - CosPhi * Length1 - SinPhi * Length2 * 计算四条边的中点坐标顺序为右、下、左、上 M1_Row : Row SinPhi * Length1 M1_Col : Column CosPhi * Length1 M2_Row : Row - CosPhi * Length2 M2_Col : Column SinPhi * Length2 M3_Row : Row - SinPhi * Length1 M3_Col : Column - CosPhi * Length1 M4_Row : Row CosPhi * Length2 M4_Col : Column - SinPhi * Length2 * 可视化验证生成十字标记 gen_cross_contour_xld(CrossP1, P1_Row, P1_Col, 20, 0.785398) gen_cross_contour_xld(CrossP2, P2_Row, P2_Col, 20, 0.785398) gen_cross_contour_xld(CrossP3, P3_Row, P3_Col, 20, 0.785398) gen_cross_contour_xld(CrossP4, P4_Row, P4_Col, 20, 0.785398) gen_cross_contour_xld(CrossM1, M1_Row, M1_Col, 15, 0.785398) gen_cross_contour_xld(CrossM2, M2_Row, M2_Col, 15, 0.785398) gen_cross_contour_xld(CrossM3, M3_Row, M3_Col, 15, 0.785398) gen_cross_contour_xld(CrossM4, M4_Row, M4_Col, 15, 0.785398) * 同时画出原来的矩形轮廓便于对比 dev_display(Image) gen_rectangle2_contour_xld(RectContour, Row, Column, Phi, Length1, Length2) dev_display(RectContour) dev_display(CrossP1) dev_display(CrossP2) dev_display(CrossP3) dev_display(CrossP4) dev_display(CrossM1) dev_display(CrossM2) dev_display(CrossM3) dev_display(CrossM4)注意上面顶点的Row坐标公式里主轴方向用的是SinPhi乘以Length1Column方向用的是CosPhi乘以Length1这跟前文推导的公式是一致的。跑一遍如果发现十字标记的位置和矩形轮廓没有对齐优先检查Phi的单位是不是弧度其次检查SinPhi和CosPhi是不是写反了。3.2 优雅版用向量风格组织代码Halcon本身支持元组操作如果你在一个项目里要同时处理很多个矩形或者希望把坐标对组织成向量方便后续循环处理可以写成下面这种更紧凑的风格。这种写法尤其在同时处理多个模板匹配结果时很有优势一次性把所有矩形的顶点都算出来。* 假设有一组矩形参数分别存放在元组中 TupleRow : [300.5, 250.0, 410.2] TupleCol : [400.2, 500.8, 320.6] TuplePhi : [0.35, 0.15, -0.52] TupleL1 : [120.0, 95.0, 150.0] TupleL2 : [80.0, 60.0, 70.0] * 计算所有矩形的四个顶点列方向坐标 P1_Cols : TupleCol cos(TuplePhi) * TupleL1 - sin(TuplePhi) * TupleL2 P2_Cols : TupleCol cos(TuplePhi) * TupleL1 sin(TuplePhi) * TupleL2 P3_Cols : TupleCol - cos(TuplePhi) * TupleL1 sin(TuplePhi) * TupleL2 P4_Cols : TupleCol - cos(TuplePhi) * TupleL1 - sin(TuplePhi) * TupleL2 * 计算所有矩形的四个顶点行方向坐标 P1_Rows : TupleRow sin(TuplePhi) * TupleL1 cos(TuplePhi) * TupleL2 P2_Rows : TupleRow sin(TuplePhi) * TupleL1 - cos(TuplePhi) * TupleL2 P3_Rows : TupleRow - sin(TuplePhi) * TupleL1 - cos(TuplePhi) * TupleL2 P4_Rows : TupleRow - sin(TuplePhi) * TupleL1 cos(TuplePhi) * TupleL2 * 同理可以计算四个边中点 M1_Cols : TupleCol cos(TuplePhi) * TupleL1 M1_Rows : TupleRow sin(TuplePhi) * TupleL1 M2_Cols : TupleCol sin(TuplePhi) * TupleL2 M2_Rows : TupleRow - cos(TuplePhi) * TupleL2 M3_Cols : TupleCol - cos(TuplePhi) * TupleL1 M3_Rows : TupleRow - sin(TuplePhi) * TupleL1 M4_Cols : TupleCol - sin(TuplePhi) * TupleL2 M4_Rows : TupleRow cos(TuplePhi) * TupleL2这里用到了Halcon对元组的广播运算cos、sin、加法和乘法都会自动对元组中每个元素操作。实际处理大批量矩形时这种写法比写循环快得多也更接近Halcon推崇的向量化编程风格。如果后面还需要把坐标传给其他算子比如用gen_cross_contour_xld一次性画所有点直接把元组传进去就行。3.3 实战与模板匹配结果无缝衔接讲完纯数学计算来一个完整的实战串联。下面这段代码展示了从模板匹配到算出矩形顶点再到用顶点生成旋转ROI的全流程。这个流程在视觉引导和定位项目中很典型值得直接参考。* 创建模板模型这里使用形状匹配示例 read_image(Image, needle_matched) create_shape_model(Image, 5, rad(-30), rad(60), rad(1), none, use_polarity, 30, 10, ModelID) * 在运行图中执行模板匹配 find_shape_model(Image, ModelID, rad(-30), rad(60), 0.5, 1, 0.5, least_squares, 0, 0.8, RowMatch, ColumnMatch, AngleMatch, Score) * 取第一个匹配结果 Row : RowMatch[0] Column : ColumnMatch[0] Phi : AngleMatch[0] * 模板匹配结果里的Length1和Length2需要从模板本身获取 get_shape_model_contours(ShapeModelContour, ModelID, 1) smallest_rectangle2(ShapeModelContour, TmpRow, TmpCol, TmpPhi, Length1, Length2) * 用前面讲的公式计算顶点和边中点 CosPhi : cos(Phi) SinPhi : sin(Phi) P1_Row : Row SinPhi * Length1 CosPhi * Length2 P1_Col : Column CosPhi * Length1 - SinPhi * Length2 P2_Row : Row SinPhi * Length1 - CosPhi * Length2 P2_Col : Column CosPhi * Length1 SinPhi * Length2 P3_Row : Row - SinPhi * Length1 - CosPhi * Length2 P3_Col : Column - CosPhi * Length1 SinPhi * Length2 P4_Row : Row - SinPhi * Length1 CosPhi * Length2 P4_Col : Column - CosPhi * Length1 - SinPhi * Length2 M1_Row : Row SinPhi * Length1 M1_Col : Column CosPhi * Length1 M2_Row : Row - CosPhi * Length2 M2_Col : Column SinPhi * Length2 M3_Row : Row - SinPhi * Length1 M3_Col : Column - CosPhi * Length1 M4_Row : Row CosPhi * Length2 M4_Col : Column - SinPhi * Length2 * 在图像中绘制结果便于现场调试 gen_cross_contour_xld(Crosses, [P1_Row, P2_Row, P3_Row, P4_Row, M1_Row, M2_Row, M3_Row, M4_Row], [P1_Col, P2_Col, P3_Col, P4_Col, M1_Col, M2_Col, M3_Col, M4_Col], 25, rad(45)) dev_display(Image) dev_display(Crosses)这套流程把模板匹配的匹配结果和几何计算串起来了测试的时候可以直接看到十字标记是否贴合目标位置。如果实际项目中用的不是模板匹配而是smallest_rectangle2求区域外接矩形那更简单因为smallest_rectangle2直接返回Row、Column、Phi、Length1、Length2五个参数直接代入公式就行不需要额外转换。4. 常见问题与踩坑记录4.1 弧度与角度Phi最坑的地方第一次用这个公式的人十个里有八个会栽在角度单位上。Halcon的Phi明确规定是弧度但很多外部系统或者第三方SDK返回的角度是度数。比如从PLC通信接口拿到一个旋转角度对方文档写的是0到360度你如果不做转换直接塞进公式算出来的顶点位置会完全错乱。转换方式非常简单记住一个原则不进HDevelop的旋转相关算子之前只要涉及display或者几何运算一律先把度数转成弧度。度转弧度的公式是弧度等于度数乘以π除以180。在Halcon里可以直接用rad()算子完成比如rad(90)就得到90度对应的弧度值不需要自己写数学公式。反过来也一样如果在Halcon里算出来一个弧度角度要发给PLC或者上位机用deg()算子把弧度转成度数再发出去。这个坑特别隐蔽的地方在于矩形如果接近正方形角度错了90度或者方向反了图像上看起来还好像差不多但在高精度测量场景下就差之毫厘谬之千里了。4.2 顶点顺序、行列坐标与可视化验证顶点的命名和顺序在不同人写的代码里可能不一样这是个很大的坑。有人的P1是左上角有人的P1是右上角如果项目里多人协作或者代码复用顺序不统一非常容易产生Bug。我的建议是定义一个约定写清楚注释最好在公司内部公共代码库里统一下来。前面推的公式里在Phi0且Column向右、Row向下的坐标约定下P1是右上角P2是右下角P3是左下角P4是左上角。这个顺序画在屏幕上看起来是顺时针方向。如果你想让顶点顺序变成逆时针只需要把P3和P1对调或者把加号减号整体取反具体按你的业务需求来。另外一点要提醒的是Row和Column千万别搞反。Halcon的坐标系统是(Row, Column)第一个是行坐标第二个是列坐标跟常见图像处理库的(x, y)正好反过来。用Halcon的算子比如gen_cross_contour_xld时参数顺序是(Row, Column)如果你从OpenCV的习惯带过来了第一参数填x坐标第二参数填y坐标算出来的点会直接画到图像外边去。这一点在混合使用OpenCV和Halcon的时候特别需要警惕。4.3 浮点精度与调试技巧Halcon内部坐标都用双精度浮点数表示正常情况下直接算不会出问题。但如果你在C#或者C里通过外部语言调用Halcon算子要确认拿到的坐标类型是double而不是float否则精度损失在旋转角度大的时候会被放大最终算出来的顶点偏差可能到亚像素级别在高精度测量项目里是不可接受的。做调试的时候建议先不谈业务逻辑用一组简单参数验证公式正确性。比如设置Phi0、Length1100、Length250中心点设在(200, 300)手动算一下顶点应该落在(200, 350)、(300, 300)、(200, 250)、(100, 300)这些位置。如果代码算出来的结果和手算结果一致再引入旋转角度。这个方法虽然笨但能快速把你代码里的符号错误或者坐标顺序错误定位出来。还有一个实用的验证技巧用gen_rectangle2_contour_xld生成矩形轮廓再用select_points_obj_first? 不对其实更简单的做法是用前面算出的四个顶点坐标生成一个XLD多边形然后比较这个多边形和原矩形的轮廓是否重合。不过Halcon里没有直接比较两个XLD轮廓差异的内置算子最简单还是在HDevelop里同时显示原矩形轮廓和算出来的十字标记用眼睛扫一遍是否贴合现场调试足够高效。4.4 外部语言调用时的注意事项很多实际项目是用C#或者C调用Halcon的这种情况下坐标计算的代码可能不写在HDevelop里而是写在主程序里。这个时候有个重要细节在C#里用HalconDotNet调用Halcon算子拿到的是HTuple类型要转成double数组再参与计算或者直接用HOperatorSet的泛型方法减少装箱拆箱的开销。考虑到后期维护我建议把矩形参数转顶点坐标这个功能封装成一个独立函数传入Row、Column、Phi、Length1、Length2返回四个顶点坐标和四个边中点坐标。这样不管是HDevelop脚本、C#程序还是C程序都调用同一个逻辑避免多个语言各维护一套公式改一处忘一处的尴尬情况。封装的时候可以把本文的公式直接写成代码C#版本大概长这样public static void Rectangle2ToPoints( double row, double col, double phi, double len1, double len2, out double p1Row, out double p1Col, out double p2Row, out double p2Col, out double p3Row, out double p3Col, out double p4Row, out double p4Col, out double m1Row, out double m1Col, out double m2Row, out double m2Col, out double m3Row, out double m3Col, out double m4Row, out double m4Col) { double c Math.Cos(phi); double s Math.Sin(phi); p1Row row s * len1 c * len2; p1Col col c * len1 - s * len2; p2Row row s * len1 - c * len2; p2Col col c * len1 s * len2; p3Row row - s * len1 - c * len2; p3Col col - c * len1 s * len2; p4Row row - s * len1 c * len2; p4Col col - c * len1 - s * len2; m1Row row s * len1; m1Col col c * len1; m2Row row - c * len2; m2Col col s * len2; m3Row row - s * len1; m3Col col - c * len1; m4Row row c * len2; m4Col col - s * len2; }这套代码我在多个项目里直接复用唯一的改动只是函数名和参数名。如果你用C也差不多本质上就几行三角函数运算没有任何依赖。这样做的好处是当Halcon版本升级或者换了别的视觉库只要输入输出参数不变业务层代码就不用动。4.5 批量矩形并行计算的性能建议如果你在一个项目里要同时处理几十个或者上百个匹配框逐个用循环计算公式会显得有点浪费。Halcon的元组广播在这种场景下优势很明显就像3.2里写的那样一次性传入所有矩形的Row、Column、Phi、Length1、Length2元组所有运算都是一次完成的不需要显式写循环。在并行度更高的情况下还可以考虑用Halcon的并行编程框架或者直接把计算放到GPU上。不过对一个矩形转顶点的操作来说计算量微乎其微真正影响性能的反而是后续的轮廓绘制和图像显示。我的经验是只要矩形的数量在万级以下元组运算级别的开销完全可以忽略不需要刻意优化。真到了海量矩形的场景瓶颈也一定不在这个环节。在实际项目里还有一个更常见的需求需要判断某个点是否落在rectangle2内部。这种判断可以直接用本文的思路逆推把目标点坐标除以Length1和Length2方向的投影长度看投影比例是否在-1到1之间。具体的实现就是在以中心点为原点、主轴方向为x轴的局部坐标系里转换坐标然后做范围判断。这个思路跟本文的公式同源理解了坐标变换的本质很多几何问题都是同一个套路。我个人在实际操作中还有一个习惯凡是涉及矩形角点计算的项目我都会在图像上同时显示原始矩形轮廓和角点十字标记并且把它们叠在同一个窗口里看几秒钟确认没有符号错误才继续往下做。这个习惯帮我避免了至少五六次由于角度正负号理解错导致的返工。另一个小技巧是把这套计算封成公共函数之后在所有需要用的项目里统一调用不要每次去复制粘贴旧代码否则一旦要修正某个符号错误你就得把所有项目都改一遍那画面太惨烈这里就不展开了。