
上周做视觉定位项目时遇到一个很实际的需求用smallest_rectangle2找到了零件的倾斜最小外接矩形拿到Row, Column, Phi, Length1, Length2五个参数之后客户却要求输出矩形的四个顶点和四条边的中点说要给机械手做抓取点规划和边缘测量。我愣了一下因为Halcon里的rectangle2只是一个参数化描述并没有现成的取顶点算子——你得自己把这五个参数换算成实际坐标点。折腾了一番之后我把几种可行方案都整理了一遍这篇就当复盘笔记给同样被rectangle2的仿射矩形取点问题卡住的朋友做个参考。1. 为什么拿到的矩形参数不能直接输出顶点坐标rectangle2在Halcon里不是一个带点的对象它只是一组数学参数中心坐标、角度和两个半轴长度。你可以用gen_rectangle2生成一个区域对象但这个区域是由像素填充的边缘并不光滑直接去取区域轮廓上的角点会得到一个锯齿状的轮廓根本拿不到精确的顶点坐标。很多新手的误区是把rectangle2当成XLD轮廓来用试图通过get_contour_xld去取点但rectangle2参数本身根本没有轮廓点。正确做法是根据几何关系把参数换算成顶点和中点的坐标。这个换算本质上就是一次旋转平移的二维仿射变换以矩形中心为局部坐标系原点沿角度方向定义两个正交轴然后把局部坐标映射到图像全局坐标。理解这点之后思路就清晰了先算出矩形在局部坐标系下的顶点和中点坐标再做旋转和平移变换。下面两种方法都能实现一种偏数学公式一种偏矩阵变换实际项目中都够用。2. rectangle2的五个参数到底在说什么动手写代码之前一定要先把Row, Column, Phi, Length1, Length2这几个参数的几何意义过一遍否则公式一写就容易把方向搞反。2.1 中心、角度和两个半轴参数含义例子Row矩形中心的行坐标单位像素300.0Column矩形中心的列坐标单位像素400.0Phi主轴与图像水平方向列轴的夹角弧度制0.3Length1沿主轴方向的半长度也就是矩形长边的一半100.0Length2沿次轴方向的半宽度也就是矩形短边的一半50.0需要特别注意的是Phi是弧度不是角度。很多从没接触过Halcon的人会直接传一个度数进去结果矩形歪得离谱。用rad(30)或者rad(PhiDeg)做转换或者直接用tuple_rad处理。2.2 主轴和次轴的方向关系主轴方向就是角度为Phi时局部坐标的x方向次轴方向与主轴垂直。具体到图像坐标里主轴方向的单位向量(cos(Phi), sin(Phi))次轴方向的单位向量(-sin(Phi), cos(Phi))这里有一个特别容易踩坑的点Halcon的图像坐标系中行坐标向下增长但角度正方向依然是逆时针。所以正角度会让矩形沿逆时针方向倾斜这一点跟数学里常见的坐标系一致不用额外取反。有了这个基础我们可以用两种方式把局部坐标转换为全局坐标。3. 方法一纯几何公式一次算出所有点这种方法适合不想依赖矩阵、想直观理解每一步在做什么的人。核心思路就是先取局部坐标(u, v)u沿主轴方向v沿次轴方向然后用下面两个公式变换到全局坐标全局行坐标 Row u * sin(Phi) v * cos(Phi) 全局列坐标 Column u * cos(Phi) - v * sin(Phi)这个公式是从单位向量中心平移推导出来的。如果你把局部坐标(u, v)看成是在旋转前的坐标系里的偏移量那么旋转后它的行、列偏移量分别是u * sin(Phi) v * cos(Phi)和u * cos(Phi) - v * sin(Phi)最后加上中心点坐标即可。3.1 四个顶点的局部坐标把矩形的四个角点在局部坐标系里写出来按顺序走一圈角点1u -Length1, v -Length2角点2u Length1, v -Length2角点3u Length1, v Length2角点4u -Length1, v Length2在HDevelop里直接用元组运算三行代码就能算完* rectangle2参数 Row : 300 Column : 400 Phi : 0.3 Length1 : 100 Length2 : 50 * 顶点的局部坐标 PX : [-Length1, Length1, Length1, -Length1] PY : [-Length2, -Length2, Length2, Length2] * 变换到全局坐标 CornerRow : Row PX * sin(Phi) PY * cos(Phi) CornerCol : Column PX * cos(Phi) - PY * sin(Phi)这段代码跑完后CornerRow和CornerCol就是按顺序排列的四个顶点坐标。元组里四个点依次对应左下、右下、右上、左上具体顺序取决于旋转角度但几何上都是那个顺时针或逆时针序列。3.2 四条边中点的局部坐标边的中点更好算因为中点一定落在某个轴上。两条长边的中点分别在长边1中点u 0, v -Length2长边2中点u 0, v Length2两条短边的中点分别在短边1中点u -Length1, v 0短边2中点u Length1, v 0对应HDevelop代码* 长边中点平行于主轴的边 LongEdge1Row : Row Length2 * cos(Phi) LongEdge1Col : Column - Length2 * sin(Phi) LongEdge2Row : Row - Length2 * cos(Phi) LongEdge2Col : Column Length2 * sin(Phi) * 短边中点平行于次轴的边 ShortEdge1Row : Row Length1 * sin(Phi) ShortEdge1Col : Column Length1 * cos(Phi) ShortEdge2Row : Row - Length1 * sin(Phi) ShortEdge2Col : Column - Length1 * cos(Phi)我把这个计算封装成了一个HDevelop过程复制进项目直接改参数就能用。3.3 为什么我推荐先写局部坐标再变换直接记住最终公式的难点在于角度方向容易搞混。但如果你先把点在局部坐标系里写出来再用统一的旋转映射就不容易错。我在前期调试时就是靠这个习惯避开了很多坑。这里也验证一下数值方便你自测。假设Row 300, Column 400, Phi 0.3, Length1 100, Length2 50算出来的点应该是点行坐标列坐标角点1222.681319.242角点2281.785510.310角点3377.319480.758角点4318.215289.690长边中点1252.233414.776长边中点2347.767385.224短边中点1270.448304.466短边中点2329.552495.534如果你的结果和表里一致说明公式用对了。4. 方法二用hom_mat2d仿射变换流程更清晰公式法直白但遇到更复杂的变换组合比如先旋转再平移再缩放时容易犯晕。Halcon里提供了hom_mat2d系列算子用矩阵的方式来描述仿射变换这套方法更工程化也更适合封装复用。4.1 构建旋转加平移的变换矩阵思路是先把矩形中心当作局部坐标系原点然后构造一个先旋转、后平移的变换矩阵。* 创建单位矩阵 hom_mat2d_identity (HomMat2DIdentity) * 注意平移矩阵的参数顺序是 Tx列方向、Ty行方向 hom_mat2d_translate (HomMat2DIdentity, Column, Row, HomMat2DTranslate) * 围绕原点0,0旋转 hom_mat2d_rotate (HomMat2DTranslate, Phi, 0, 0, HomMat2DRotate)这里的顺序是先在原点旋转再平移。如果把旋转和平移顺序写反了结果会完全不同。4.2 把局部坐标点喂给变换矩阵构造好矩阵后把顶点的局部坐标当成以中心为原点的偏移量再调用affine_trans_point_2d即可* 顶点的局部坐标列偏移放前面行偏移放后面 LocalCol : [-Length1, Length1, Length1, -Length1] LocalRow : [-Length2, -Length2, Length2, Length2] * 仿射变换求出全局坐标 affine_trans_point_2d (HomMat2DRotate, LocalCol, LocalRow, GlobalCol, GlobalRow)这里有个细节必须强调affine_trans_point_2d的参数顺序是(Px, Py)对应到图像坐标里就是(列, 行)不是(行, 列)。我一开始在C#里调用Halcon接口时把行和列搞反了一次所有点都镜像错位排查了半天才发现是参数顺序的问题。边中点用同样的方式* 长边中点局部坐标 LocalLongCol : [0, 0] LocalLongRow : [-Length2, Length2] affine_trans_point_2d (HomMat2DRotate, LocalLongCol, LocalLongRow, LongGlCol, LongGlRow) * 短边中点局部坐标 LocalShortCol : [-Length1, Length1] LocalShortRow : [0, 0] affine_trans_point_2d (HomMat2DRotate, LocalShortCol, LocalShortRow, ShortGlCol, ShortGlRow)实测下来方法二的结果和方法一完全一致但代码看起来更有结构感尤其在需要连续做多个变换的视觉标定流程里矩阵法更稳。4.3 两种方法的选型建议我的建议是日常在HDevelop里调试用方法一简单直接如果写正式项目、或者涉及到多坐标系标定用方法二因为矩阵可以在后续扩展到缩放、剪切等变换可维护性更好。5. 把坐标点可视化验证结果对不对计算完之后不验证是危险的。Halcon里最直观的验证方式就是把点画在图像上看。5.1 用XLD十字叉画出顶点和边中点* 生成顶点十字叉 gen_cross_contour_xld (Corners, CornerRow, CornerCol, 20, 0.785398) * 生成边中点十字叉 EdgeRow : [LongEdge1Row, LongEdge2Row, ShortEdge1Row, ShortEdge2Row] EdgeCol : [LongEdge1Col, LongEdge2Col, ShortEdge1Col, ShortEdge2Col] gen_cross_contour_xld (EdgeMids, EdgeRow, EdgeCol, 15, 0.785398) * 生成矩形外轮廓 gen_contour_polygon_xld (RectContour, CornerRow, CornerCol)gen_contour_polygon_xld会把四个顶点依次连成闭合多边形显示出来是一个倾斜的矩形框。把十字叉叠加上去如果顶点和边中点都落在轮廓上说明计算正确。5.2 和原始区域做叠加验证如果rectangle2是从某个区域来的你还可以把矩形轮廓和原区域叠加显示dev_display (Image) dev_set_color (green) dev_display (Region) dev_set_color (red) dev_display (RectContour) dev_set_color (blue) dev_display (Corners)绿色是原区域红色是生成的矩形框蓝色是顶点。如果矩形框能贴合区域的边缘、顶点落在区域边缘的交界处就说明参数和计算都正确。6. 实战中的避坑指南和进阶操作踩过几次坑之后我总结了一套检查和防御性的写法分享几个最典型的坑。6.1 角度方向引起的镜像错误最常见的问题就是把Phi当成了与行轴的夹角。在Halcon中角度约定是相对列轴水平方向的逆时针角。如果你的项目依赖的坐标系是行轴朝下、角度顺时针那么必须做对应转换。一个稳妥的办法是先在一个已知中心、已知角度的图像上测试旋转180度再旋转回去确保角度方向是预期的那一侧。6.2Length1和Length2不一定代表长边和短边smallest_rectangle2返回的Length1和Length2有明确语义Length1是主轴方向上的半轴Length2是次轴方向上的半轴通常Length1 Length2。但如果你从其他来源拿到rectangle2参数比如自己拟合的、从JSON读取的一定要先确认哪一个是长轴、哪一个是短轴否则算出来的顶点顺序会乱。6.3 HDevelop元组和C#/C里数组的差异HDevelop里直接写PX * sin(Phi)就是整个元组都乘很方便。但换成C#HalconDotNet或者C接口时要注意不能直接对整个HTuple做标量乘法后当数组用需要循环或逐个索引。Halcon的元组运算规则在HDevelop里很优雅一旦改到外部语言就成了最容易出问题的地方。6.4 从顶点坐标反推断矩形参数有时候手上有四个顶点坐标需要反推出rectangle2的参数。这可以借助gen_contour_polygon_xld生成轮廓后调用fit_rectangle2_contour_xld或者直接对四个点求中心、计算主方向、再求半轴长度。一个简单的处理方法是用tuple_mean求中心用tuple_atan2求角度。6.5 用结果做边缘测量时怎么取边中点连线获取边中点不仅仅是为了画十字叉。我一般会用中点和相邻顶点来构造测量线用gen_measure_rectangle2做边缘对测量。比如在电池片外观检测中我用四条边的中点连线矩形的中心线再沿垂直方向开测量窗口就能测量边缘到中心的偏差精度稳定在亚像素级。7. 收尾把你的取点逻辑封装成独立过程最后建议无论你用哪种方法都把这套计算封装成一个独立的HDevelop过程或者函数输入Row, Column, Phi, Length1, Length2输出顶点和边中点。这样在多个项目里复用时不需要重写也方便做单元验证。我目前在项目里使用的做法是把这个函数做成一个.hdvp库同时提供C#版本遇到需要给上位机交付时直接调同一个算法逻辑省去了很多沟通成本。如果你刚接触Halcon建议先手动跑一遍方法一的元组代码把数值对照表核对一遍建立手感等真正理解了旋转映射再去用方法二的矩阵法做封装。这两种方式无论哪个都比从轮廓上抠点靠谱得多。