
1. 项目概述从像素坐标到几何洞察在机器视觉的日常开发中我们常常会遇到这样的场景从一张图像中我们通过边缘检测、模板匹配或者深度学习模型得到了一条或多条线段。这些线段在Halcon的世界里通常以一组像素坐标的形式存在——一个起点一个终点。对于很多新手来说拿到这两个点坐标后下一步就卡壳了。中点怎么算角度是多少度这条线是水平的还是倾斜的这些看似基础的几何计算恰恰是连接底层图像处理结果与上层应用逻辑如测量、定位、引导的关键桥梁。我自己在早期做视觉项目时就曾因为一个简单的角度计算偏差了0.5度导致机械臂抓取位置偏移了几个毫米整个流程都得停下来排查。后来才发现问题就出在我自己手写的几何计算函数忽略了Halcon坐标系和角度定义的一些细节。所以今天我们就来彻底搞懂在Halcon环境下如何准确、高效地进行线段的中点、端点和角度计算。这不仅仅是调用几个算子那么简单更重要的是理解其背后的坐标系、角度定义以及在实际项目中可能遇到的坑。本文将围绕“线段”这一核心几何元素深入探讨其关键属性的计算方法。无论你是用Halcon进行尺寸测量、零件定位还是做机器人视觉引导这些基础而坚实的几何处理能力都是你构建稳定、可靠视觉系统的基石。我们将从最基础的坐标获取开始一步步推导公式并用Halcon代码和实际案例来验证确保你不仅能“知其然”更能“知其所以然”最终能将这些知识灵活应用到你的项目中。2. 线段数据的获取与理解一切计算的起点在Halcon中线段本身并不是一个原生的、独立的数据类型。我们通常通过一系列图像处理算子来“得到”线段而得到的其实是一组或多组点的集合。理解这些数据的来源和格式是进行任何后续计算的前提。2.1 常见的线段生成方式线段数据通常来源于以下几种典型的图像处理流程边缘提取与线段拟合这是最经典的方法。首先使用edges_sub_pix算子提取亚像素精度的边缘轮廓得到一组无序的边缘点。然后使用segment_contours_xld将轮廓分割成直线段、圆弧段等基本几何元素。对于分割出的直线段部分我们可以使用fit_line_contour_xld算子进行直线拟合从而得到线段的参数。这是精度非常高的一种方式尤其适用于具有清晰、连续边缘的物体。基于区域的几何特征有时我们处理的是二值化后的区域Region。例如一个细长的缺陷区域我们可以将其近似看作一条线段。使用smallest_rectangle2算子可以获取该区域的最小外接矩形这个矩形的长边中心线和方向就为我们提供了一条有意义的线段信息。虽然这不是严格意义上的线段但在许多定位应用中非常实用。模板匹配与位姿输出在使用Halcon进行模板匹配如find_shape_model,find_ncc_model后我们会得到一个匹配项的位置和姿态位姿。这个位姿包含了旋转角度和一个参考点。我们可以根据这个位姿在图像中“画”出一条有方向的线段例如从匹配中心点沿某个方向延伸一定距离用以表示物体的方向或用于后续的引导。直接交互或赋值在开发调试阶段我们也可以直接通过鼠标在Halcon图形窗口上画线或者直接在代码中给起点和终点的行、列坐标Row1, Column1, Row2, Column2赋值来定义一条线段。2.2 线段的数据表示起点与终点无论通过上述哪种方式一条线段在Halcon中最核心、最通用的表示方法就是它的两个端点坐标。在Halcon的坐标系中行坐标 (Row)图像垂直方向的位置从上往下递增。相当于数学坐标系中的Y轴但方向相反。列坐标 (Column)图像水平方向的位置从左往右递增。相当于数学坐标系中的X轴。因此一条线段由四个标量值唯一确定Row1, Column1, Row2, Column2。这里并没有规定哪个点是起点哪个是终点它们是对称的。但在计算方向向量和角度时我们需要约定一个顺序通常将(Row1, Column1)视为起点(Row2, Column2)视为终点。注意Halcon的像素坐标系原点(0,0)在图像的左上角。这与许多数学坐标系和CAD坐标系原点在左下角不同在进行角度计算和与外部机械系统对接时必须时刻牢记这一点否则会导致方向完全错误。2.3 从XLD轮廓到线段参数当我们通过fit_line_contour_xld拟合直线后得到的线段参数是一种更丰富的表示形式。该算子会返回拟合直线的参数通常包括直线上一个点的坐标(Row, Column)。直线的方向向量(Phi)即直线与图像水平轴Column轴的夹角。线段的起点和终点在直线上的参数(t)。这种表示方法对于计算点到直线的距离、线段延长等操作更为方便。但无论是点坐标表示还是参数表示我们都可以相互转换并计算出我们需要的中点和角度。接下来我们就从最基础的端点坐标表示法开始进行各项计算。3. 核心计算一线段中点的求解线段中点的计算是所有几何计算中最简单直接的一个但它却是很多复杂操作的基础比如计算两条线段的中点连线、判断点是否在线段附近等。3.1 基础数学原理在欧几里得几何中给定线段两个端点 A(R1, C1) 和 B(R2, C2)其中点 M(Rm, Cm) 的坐标由两端点坐标的算术平均值决定。公式非常简单Rm (R1 R2) / 2 Cm (C1 C2) / 2这个公式的几何意义非常直观中点就是连接起点和终点的向量的中点。在Halcon的像素坐标系下这个公式完全适用因为它是线性运算。3.2 Halcon实现与代码示例在Halcon中我们可以直接使用元组Tuple运算来实现这个公式。Halcon的元组支持基本的算术运算,-,*,/并且是元素级element-wise的这使得向量计算非常简洁。假设我们已经从某个算子比如get_contour_xld后处理或者从匹配结果推导得到了线段的两个端点* 假设 line_rows 和 line_cols 是包含多条线段端点的数组 * 例如line_rows [r1_1, r2_1, r1_2, r2_2, ...] * line_cols [c1_1, c2_1, c1_2, c2_2, ...] * 这里我们以一条线段为例 Row1 : 100.5 Column1 : 200.3 Row2 : 300.7 Column2 : 450.9 * 计算中点坐标 MidRow : (Row1 Row2) / 2 MidColumn : (Column1 Column2) / 2 * 输出结果 dev_disp_text (‘中点坐标(‘ MidRow$’.2f’ ‘, ‘ MidColumn$’.2f’ ‘)’, ‘window’, ‘top’, ‘left’, ‘black’, [], [])对于处理多条线段的情况如果端点坐标存储在多个元组中我们需要利用元组操作函数。一种常见的存储方式是每条线段的信息单独存储或者所有起点在一个元组所有终点在另一个元组* 方式一每条线段独立存储适用于线段数量不多时 * lines 是一个包含多个结构的数组每个结构有 .Row1, .Col1, .Row2, .Col2 属性 * 需要循环计算 * 方式二起点集和终点集存储适用于批量计算 StartRows : [100.5, 150.0, 80.2] StartCols : [200.3, 350.5, 500.1] EndRows : [300.7, 180.3, 250.8] EndCols : [450.9, 400.2, 550.6] * 批量计算中点 MidRows : (StartRows EndRows) / 2 MidCols : (StartCols EndCols) / 2 * 现在 MidRows 和 MidCols 元组包含了所有线段的中点坐标3.3 实际应用中的注意事项亚像素精度在Halcon中许多算子如edges_sub_pix的输出是亚像素精度的即坐标值可能是小数如100.5。在进行中点计算时务必保持使用real类型Halcon中默认为实数进行计算而不要过早地转换为整数int否则会损失精度在后续的高精度测量中产生累积误差。空线段处理在实际代码中需要增加健壮性检查。如果一条线段的两个端点完全相同即线段长度为零那么其中点就是它自身。虽然数学上没问题但在业务逻辑上这可能意味着一个错误或无效的检测结果需要根据实际情况进行处理或报警。性能考量当需要处理成千上万条线段时使用元组的批量运算如上面的方式二远比在循环中逐条计算要高效得多。Halcon的元组运算是底层优化的能充分发挥性能。中点计算虽然简单但它是构建更复杂空间关系判断的基石。例如在计算两条平行线段的中心线时可以先分别计算两条线段的中点然后连接这两个中点得到中心线。4. 核心计算二线段端点处理与排序在很多应用场景下我们不仅需要知道线段的两个端点还需要对它们进行排序或赋予特定的意义。例如在测量一个零件的宽度时我们需要确保所有线段的“左端点”和“右端点”定义一致才能进行正确的统计分析。4.1 端点顺序的重要性原始的(Row1, Column1)和(Row2, Column2)并没有内在的顺序。但在计算方向向量、角度或者需要区分线段“首尾”时例如在生成路径时我们必须建立一个一致的顺序。一个常见的约定是将行坐标较小即更靠图像上方的点作为起点如果行坐标相同则将列坐标较小更靠左的点作为起点。这种排序方式类似于“从左到右从上到下”的阅读顺序在很多情况下能产生一致的结果。4.2 端点排序算法与实现我们可以通过一个简单的比较和交换逻辑来实现端点排序。以下Halcon代码演示了如何对一条线段的端点进行标准化排序* 输入可能无序的两个端点 Row1_in : 300.0 Col1_in : 500.0 Row2_in : 100.0 Col2_in : 200.0 * 排序逻辑 if (Row1_in Row2_in) * 交换两个点 Row1 : Row2_in Col1 : Col2_in Row2 : Row1_in Col2 : Col1_in else if (Row1_in Row2_in and Col1_in Col2_in) * 行相同比较列 Row1 : Row2_in Col1 : Col2_in Row2 : Row1_in Col2 : Col1_in else * 已经是有序的 Row1 : Row1_in Col1 : Col1_in Row2 : Row2_in Col2 : Col2_in endif endif * 输出排序后的端点 (Row1, Col1) 为起点(Row2, Col2) 为终点对于批量处理我们可以利用Halcon的元组比较和条件赋值函数tuple_gen_const,tuple_less,tuple_greater等来实现向量化操作但逻辑会稍显复杂。通常如果线段数量巨大且排序是必须步骤可以编写一个小的Halcon外部函数C来实现以获得最佳性能或者在HDevelop中使用循环。4.3 从拟合直线参数还原端点当我们使用fit_line_contour_xld并设置参数‘drop’为‘-1’不丢弃任何点时算子除了返回直线参数外还会返回线段的起点和终点参数Begin和End。这里的Begin和End是直线方程参数t的值。直线参数方程通常表示为P(t) P0 t * Dir其中P0是直线上一个参考点Dir是单位方向向量。Begin和End就是用于计算线段起止点的t值。要获取实际的像素坐标端点我们需要进行计算* 假设从 fit_line_contour_xld 得到以下结果 * Row, Column: 直线上的一个点 * Phi: 直线方向弧度与水平轴夹角 * Begin, End: 线段在直线上的参数范围 * 计算方向向量的单位分量 DirRow : -sin(Phi) // 注意Halcon中Phi是与水平轴夹角方向向量为(cos(Phi), sin(Phi))但行坐标轴向下故行分量为负 DirCol : cos(Phi) * 计算起点和终点坐标 RowStart : Row Begin * DirRow ColStart : Column Begin * DirCol RowEnd : Row End * DirRow ColEnd : Column End * DirCol这里有一个至关重要的细节方向向量(DirRow, DirCol)的计算。因为Halcon的图像行坐标轴是向下的所以当直线角度Phi从水平轴逆时针旋转为正给定时其方向向量的行分量是-sin(Phi)列分量是cos(Phi)。这是Halcon几何计算中最容易出错的地方之一。很多开发者直接使用(sin(Phi), cos(Phi))导致计算出的端点位置完全错误。4.4 端点应用案例计算线段长度和方向一旦我们有了有序的端点起点和终点线段长度和方向向量的计算就水到渠成了。长度是两点之间的欧氏距离* 计算线段长度 Length : sqrt((Row2 - Row1) * (Row2 - Row1) (Col2 - Col1) * (Col2 - Col1))方向向量从起点指向终点为DeltaRow : Row2 - Row1 DeltaCol : Col2 - Col1这个方向向量(DeltaRow, DeltaCol)将直接用于下一步的角度计算。理解并正确处理端点是确保所有后续几何计算正确的第一步。5. 核心计算三线段角度的计算与解读线段的角度是其最重要的方向属性在机器视觉中用于判断物体的偏转、进行坐标对齐、计算相对位置关系等。然而Halcon中的角度计算涉及坐标系转换和角度定义是新手最容易混淆的部分。5.1 Halcon中的角度定义Phi与角度制Halcon中直线或线段的角度通常用Phi表示单位是弧度radians。Phi的定义是直线与图像水平轴即Column轴向右为正方向的夹角。这个夹角是以水平轴为起始边逆时针旋转到与直线方向一致时所转过的角度。这里有一个关键点直线方向。对于一条无限长的直线它有两个相反的方向。Halcon约定Phi的范围是(-π, π]。对于一条线段我们通常使用从起点指向终点的方向来计算Phi。根据这个定义一条水平向右的线段Phi 0。一条垂直向上的线段在图像中是从上到下因为行坐标向下Phi -π/2或3π/2但Halcon会将其规范到-π/2。一条水平向左的线段Phi π或-π。一条垂直向下的线段Phi π/2。5.2 从方向向量计算角度Phi给定线段的有序端点起点(R1, C1), 终点(R2, C2)我们首先计算方向向量(dR, dC) (R2-R1, C2-C1)。然后使用反正切函数atan2来计算角度。atan2(y, x)函数返回的是从正X轴到点(x, y)的夹角。在我们的语境中X轴对应的是列方向dC向右为正。Y轴对应的是行方向dR。但是注意图像的行坐标轴是向下的而数学坐标系Y轴是向上的。因此当我们把图像坐标向量(dR, dC)代入atan2时相当于Y坐标取反。所以正确的公式是dRow : Row2 - Row1 dCol : Column2 - Column1 Phi : atan2(-dRow, dCol) // 关键对 dRow 取负让我们验证一下如果线段水平向右dRow0,dCol0。atan2(-0, 正数) 0。正确。如果线段垂直向下图像中从上到下dRow0,dCol0。atan2(-正数, 0)。在atan2的定义中当x0, y0时结果为-π/2。因为-dRow是负数所以Phi -π/2。正确根据定义从水平轴顺时针旋转90度即-π/2。这个atan2(-dRow, dCol)是Halcon角度计算的核心公式务必牢记。5.3 角度归一化与度制转换atan2函数返回的角度范围是(-π, π]。有时我们需要将其归一化到[0, 2π)或其他范围。Halcon提供了angle_ll算子来计算两条线的夹角但自己处理时可以这样归一化到[0, 2π)Phi : atan2(-dRow, dCol) if (Phi 0) Phi : Phi 2 * pi endif在工业现场人们更习惯使用角度制degree。转换公式很简单Phi_deg : deg(Phi) // 弧度转度 deg() 是Halcon函数 // 或者 Phi_deg : Phi * 180 / pi5.4 角度计算的常见陷阱与验证忽略行坐标轴方向这是最常见的错误直接使用atan2(dRow, dCol)导致角度与预期相差90度或完全相反。务必记住atan2的第一个参数是-dRow。线段方向不明确如果不对端点进行排序直接拿任意两个端点计算dRow, dCol那么计算出的Phi可能代表的是相反的方向相差 π 弧度。在需要一致方向的场景如所有零件边缘线的方向都应从内指向外必须先统一端点顺序。特殊情况的处理当线段是水平或垂直时dRow或dCol为零。atan2函数能正确处理这些情况但需要注意除零问题。我们自己实现时应避免使用atan(dRow/dCol)这种形式因为它无法处理dCol0的情况且无法判断象限。验证方法一个简单的验证方法是在Halcon窗口中画出线段并用disp_line或disp_arrow显示。同时用计算出的Phi画一条从线段中点出发、方向为(cos(Phi), -sin(Phi))的短线注意行分量符号。如果短线方向与原线段方向一致则计算正确。* 计算并显示方向进行验证 MidRow : (Row1Row2)/2 MidCol : (Col1Col2)/2 * 根据计算出的Phi计算一个单位方向向量用于显示 DirRow : -sin(Phi) // 从Phi推导出的方向向量行分量 DirCol : cos(Phi) // 列分量 * 画一个箭头 dev_disp_arrow (MidRow, MidCol, MidRow DirRow*50, MidCol DirCol*50, 2)通过这样的可视化验证可以快速确认角度计算是否正确。6. 综合实战一个完整的线段测量案例为了将中点、端点和角度的计算融会贯通我们来看一个实际的机器视觉测量案例测量一个矩形金属零件的边长和角度以检测其是否加工合格。6.1 案例场景与目标假设我们有一张金属冲压件的图像需要测量其四个边的长度以及相邻边的夹角应为90度。零件可能有一定的旋转。我们的流程是图像预处理提取零件轮廓。将轮廓分割并拟合为四条主要的直线段。计算每条线段的长度通过端点和中点。计算相邻线段之间的夹角。输出测量结果并与公差进行比较。6.2 Halcon实现步骤详解步骤1图像采集与预处理read_image (Image, ‘metal_part.png‘) rgb1_to_gray (Image, GrayImage) * 增强对比度便于阈值分割 emphasize (GrayImage, ImageEmphasize, 7, 7, 1.0) * 动态阈值分割适应光照不均 dyn_threshold (GrayImage, ImageEmphasize, RegionDynThresh, 5, ‘dark‘) connection (RegionDynThresh, ConnectedRegions) select_shape (ConnectedRegions, SelectedRegions, ‘area‘, ‘and‘, 50000, 1000000) fill_up (SelectedRegions, FilledRegion)步骤2提取亚像素轮廓并拟合直线* 获取区域边界 boundary (FilledRegion, RegionBorder, ‘inner‘) gen_contour_region_xld (RegionBorder, Contours, ‘border‘) * 将轮廓分割为直线段 segment_contours_xld (Contours, ContoursSplit, ‘lines_circles‘, 5, 4, 2) * 选择长度较长的轮廓段假设是四条边 select_shape_xld (ContoursSplit, LongSegments, ‘contlength‘, ‘and‘, 200, 99999) count_obj (LongSegments, NumberSegments) * 我们需要恰好4条长边 if (NumberSegments ! 4) dev_disp_text (‘检测到的线段数量不是4条‘ NumberSegments, ‘window‘, ‘top‘, ‘left‘, ‘red‘, [], []) return endif * 拟合直线 fit_line_contour_xld (LongSegments, ‘tukey‘, -1, 0, 5, 2, RowBegin, ColBegin, RowEnd, ColEnd, Nr, Nc, Dist) * RowBegin, ColBegin, RowEnd, ColEnd 就是拟合后线段的端点步骤3计算线段长度与中点* 初始化数组 Lengths : [] MidRows : [] MidCols : [] * 循环处理每条线段 for i : 0 to NumberSegments-1 by 1 R1 : RowBegin[i] C1 : ColBegin[i] R2 : RowEnd[i] C2 : ColEnd[i] * 计算长度 length : sqrt((R2-R1)*(R2-R1) (C2-C1)*(C2-C1)) Lengths : [Lengths, length] * 计算中点 MidRows : [MidRows, (R1R2)/2] MidCols : [MidCols, (C1C2)/2] endfor步骤4计算相邻边夹角计算夹角需要线段的方向。我们先计算每条线段的方向角Phi然后计算相邻Phi的差值。* 计算每条线段的方向角Phi Phis : [] for i : 0 to NumberSegments-1 by 1 dR : RowEnd[i] - RowBegin[i] dC : ColEnd[i] - ColBegin[i] phi : atan2(-dR, dC) // 核心公式 * 归一化到 [0, 2π) if (phi 0) phi : phi 2 * pi endif Phis : [Phis, phi] endfor * 假设线段顺序是环绕轮廓的计算相邻夹角 AnglesBetween : [] for i : 0 to NumberSegments-1 by 1 j : (i1) mod NumberSegments // 下一条边循环 phi_i : Phis[i] phi_j : Phis[j] * 计算夹角取绝对值并确保是锐角或直角 delta_phi : abs(phi_j - phi_i) * 因为两条线的夹角有两个可能值互补取小于等于π/2的那个 if (delta_phi pi) delta_phi : 2*pi - delta_phi endif * 转换为角度制 angle_deg : deg(delta_phi) AnglesBetween : [AnglesBetween, angle_deg] endfor步骤5结果可视化与输出* 在图像上显示长度和角度 dev_set_color (‘green‘) for i : 0 to NumberSegments-1 by 1 * 显示长度 dev_disp_text (‘L‘ (i1)$‘d‘ ‘:‘ Lengths[i]$‘.2f‘ ‘ px‘, ‘window‘, MidRows[i], MidCols[i]-30, ‘green‘, [], []) * 显示中点 dev_set_color (‘blue‘) disp_cross (WindowHandle, MidRows[i], MidCols[i], 6, 0) endfor dev_set_color (‘red‘) * 显示夹角在两条线段的角点附近 for i : 0 to NumberSegments-1 by 1 * 简单计算角点位置两条线段端点的平均 R_corner : (RowEnd[i] RowBegin[(i1) mod NumberSegments]) / 2 C_corner : (ColEnd[i] ColBegin[(i1) mod NumberSegments]) / 2 dev_disp_text (AnglesBetween[i]$‘.1f‘ ‘°‘, ‘window‘, R_corner, C_corner, ‘red‘, ‘box‘, ‘true‘) endfor * 在控制台输出结果 print (‘--- 矩形零件测量报告 ---‘) for i : 0 to NumberSegments-1 by 1 print (‘边 ‘ (i1) ‘: 长度 ‘ Lengths[i]$‘.2f‘ ‘ px‘) endfor for i : 0 to NumberSegments-1 by 1 print (‘角 ‘ (i1) ‘: ‘ AnglesBetween[i]$‘.1f‘ ‘ °‘) endfor6.3 案例中的经验与技巧线段排序问题上述案例假设segment_contours_xld和fit_line_contour_xld输出的线段顺序是沿着轮廓连续的。但实际中这并不总是保证。更稳健的做法是根据线段中点的位置使用sort_contours_xld或自定义排序逻辑如按中点极角排序对四条边进行排序确保相邻索引的线段在物理上也相邻。角度差计算直接相减得到的角度差可能大于180度π。在矩形检测中内角应该是接近90度的锐角。所以代码中通过if (delta_phi pi) delta_phi : 2*pi - delta_phi来得到那个锐角。这是计算两条直线夹角的标准处理方法。像素到实际尺寸的转换本例中长度单位是像素。在实际测量中必须通过标定将像素距离转换为毫米或英寸。这需要事先进行相机标定获取每个像素代表的实际物理尺寸如 mm/pixel。然后将所有像素长度乘以这个比例系数即可。误差分析与滤波拟合出的直线段端点可能因为噪声而有微小抖动。可以对计算出的长度和角度进行简单的滤波比如剔除明显偏离均值的异常值或对多次测量结果取平均以提高测量的稳定性和重复性。通过这个完整案例我们可以看到中点、端点、角度这些基础计算是如何被有机地组合起来解决一个实际工业检测问题的。每一个步骤都依赖于对Halcon几何概念的清晰理解和对细节的准确把握。