C#/Halcon:简单介绍在AOI设备软件中的应用
前言
C#目前在国内的工业场景中还是有一定的应用的,使用的框架有Winform、WPF,很多会结合Halcon、OpenCV做一些偏视觉类的应用。目前据我所经历的在半导体封装设备、AOI设备中使用都挺多的。
今天跟大家简单分享一下C#/Halcon在AOI设备软件中的应用。
AOI(Automated Optical Inspection,自动光学检测)是一种利用计算机视觉技术和光学设备对产品进行自动化检测的技术。
看起来AOI算是一种技术,那AOI设备就是就是使用这种技术的一套设备,看起来就像下面这样:
在这个设备上有很多硬件,比如相机、光源等等,AOI设备软件就是用来控制这些硬件,执行检测操作等等部署在这个设备上的软件。
现在我们大概了解了什么是AOI/AOI设备/AOI设备软件了。
现在就需要知道我们拿AOI这个技术用来干嘛,AOI一般可以用来检测产品的缺陷。这个技术又是如何检测的呢?接下来我们通过一个简单的例子了解这个过程。
简单的检测过程
比如我们有一个标准的东西,比如说在设计时就是这样设计的,还有一个实际的东西,也就是最终生产出来的东西。
比如现在这个标准的东西长这样:
然后实际的东西长这样:
我们可以很直观的看到矩形的一角与设计图纸不一样,不一样的地方大概率就是出问题的地方。那么使用AOI进行检测的第一个步骤就是先把设计图纸与实物图对齐,因为实际上它们大概率坐标系不同。
现在在读取了这两张图片之后,先让它们的坐标系变得不一样。
* 1. 读取图像 read_image (Test, 'F:/Users/user/Desktop/Halcon脚本/333.png') read_image (Test2, 'F:/Users/user/Desktop/Halcon脚本/444.png') * 2. 将设计图纸缩小2倍,模拟图纸与实物尺寸不一致 zoom_image_factor (Test, TestScaled, 0.5, 0.5, 'constant')然后现在我们就要想怎么样让这两张图对齐?
这时候就需要用到仿射变换矩阵了,它是个什么东西呢?
先来过一下它的定义:
仿射变换矩阵是一种线性代数工具,用于描述二维/三维空间中的仿射变换。仿射变换是保持直线平行性和直线比例的几何变换——即变换后,直线仍为直线,平行线仍平行,但长度和角度可能改变。
可以理解为就是平面对平面的变换,也就是在这张图上的某个点使用这个矩阵之后就会变换到另一张图上的对应点。
那么现在的问题来到了怎么计算得到这个仿射变换矩阵呢?
对AOI熟悉的朋友可能会经常听到定位点这个词,我们只要找到这两张图片上3个对应点就可以计算了。而具体如何计算要根据实际情况,在这个例子中通过对图像进行网格拆分然后寻找封闭轮廓选择圆度比较高的来当定位点。
* 3. 对设计图纸查找封闭轮廓 edges_sub_pix (TestScaled, Edges1, 'canny', 1.5, 10, 20) select_shape_xld (Edges1, ClosedEdges1, 'contlength', 'and', 50, 99999) * 4. 对实物图查找封闭轮廓 edges_sub_pix (Test2, Edges2, 'canny', 1.5, 10, 20) select_shape_xld (Edges2, ClosedEdges2, 'contlength', 'and', 50, 99999)edges_sub_pix从图像中提取亚像素级精度的边缘轮廓。
select_shape_xld根据轮廓的形状特征对 XLD 轮廓进行筛选,只保留满足条件的轮廓。
现在设计图纸变成了这样:
实物图变成了这样:
接下来选点这一步简单了解一下就行,实际上要根据实际情况进行判断,很难找到全部适用的情况。
* ======================================== * 5 & 6. 从两幅图中选定位点(2×3网格,设计图优先,实物图就近匹配) * ======================================== get_image_size (TestScaled, Width1, Height1) get_image_size (Test2, Width2, Height2) count_obj (ClosedEdges1, NumEdges1) count_obj (ClosedEdges2, NumEdges2) GridRows := 2 GridCols := 3 CellHeight1 := Height1 / GridRows CellWidth1 := Width1 / GridCols CellHeight2 := Height2 / GridRows CellWidth2 := Width2 / GridCols MinCircularity := 0.6 RowDesign := [] ColDesign := [] RowReal := [] ColReal := [] for gr := 0 to GridRows - 1 by 1 for gc := 0 to GridCols - 1 by 1 * ============================================ * 第一步:在设计图该网格中找圆度最高的特征点 * ============================================ RowMin1 := gr * CellHeight1 ColMin1 := gc * CellWidth1 RowMax1 := (gr + 1) * CellHeight1 ColMax1 := (gc + 1) * CellWidth1 BestCircularity1 := 0 BestRow1 := -1 BestCol1 := -1 for i := 1 to NumEdges1 by 1 select_obj (ClosedEdges1, EdgeSelected, i) area_center_xld (EdgeSelected, Area, Row, Column, PointOrder) if (Row >= RowMin1 and Row < RowMax1 and Column >= ColMin1 and Column < ColMax1) gen_region_contour_xld (EdgeSelected, Region, 'filled') circularity (Region, Circularity) if (Circularity > BestCircularity1 and Circularity >= MinCircularity) BestCircularity1 := Circularity BestRow1 := Row BestCol1 := Column endif endif endfor * 设计图该网格没找到,跳过 if (BestRow1 < 0) continue endif * ============================================ * 第二步:推算实物图中的预期位置 * 设计图点在网格内的相对比例 → 映射到实物图网格 * ============================================ RelRow := (BestRow1 - RowMin1) / CellHeight1 RelCol := (BestCol1 - ColMin1) / CellWidth1 RowMin2 := gr * CellHeight2 ColMin2 := gc * CellWidth2 RowMax2 := (gr + 1) * CellHeight2 ColMax2 := (gc + 1) * CellWidth2 ExpectedRow2 := RowMin2 + RelRow * CellHeight2 ExpectedCol2 := ColMin2 + RelCol * CellWidth2 * ============================================ * 第三步:在实物图该网格中找圆度>0.6且距离预期位置最近的特征点 * ============================================ BestDist2 := 999999 BestRow2 := -1 BestCol2 := -1 for i := 1 to NumEdges2 by 1 select_obj (ClosedEdges2, EdgeSelected, i) area_center_xld (EdgeSelected, Area, Row, Column, PointOrder) if (Row >= RowMin2 and Row < RowMax2 and Column >= ColMin2 and Column < ColMax2) gen_region_contour_xld (EdgeSelected, Region, 'filled') circularity (Region, Circularity) if (Circularity >= MinCircularity) Dist := sqrt((Row - ExpectedRow2) * (Row - ExpectedRow2) + (Column - ExpectedCol2) * (Column - ExpectedCol2)) if (Dist < BestDist2) BestDist2 := Dist BestRow2 := Row BestCol2 := Column endif endif endif endfor * ============================================ * 第四步:两幅图都找到才添加点对 * ============================================ if (BestRow1 >= 0 and BestRow2 >= 0) RowDesign := [RowDesign, BestRow1] ColDesign := [ColDesign, BestCol1] RowReal := [RowReal, BestRow2] ColReal := [ColReal, BestCol2] endif endfor endfor反正通过这一步我们获取到了设计图与实物图上对应的一组点,现在可以通过这些点来计算仿射变换矩阵了。
* 7. 检查定位点数量是否足够(仿射变换至少需要3对点) NumPoints := |RowDesign| if (NumPoints < 3) disp_message (3600, '定位点不足(需要至少3对,当前' + NumPoints + '对),无法计算变换矩阵', 'window', 12, 12, 'red', 'true') stop () endif * 8. 计算仿射变换矩阵(从设计图纸坐标 → 实物图坐标) vector_to_hom_mat2d (RowDesign, ColDesign, RowReal, ColReal, HomMat2D)然后就可以使用仿射变换矩阵将设计图纸与实物图对齐,或者使用逆矩阵实物图与设计图对齐,反正现在可以实现两者的对齐了。
* 9. 将设计图纸通过仿射变换对齐到实物图 affine_trans_image (TestScaled, TestAligned, HomMat2D, 'constant', 'true')可以看下我们的定位点选的情况怎么样。
* 10. 在设计图上绘制定位点十字标记 gen_cross_contour_xld (Cross1, RowDesign, ColDesign, 15, 0.785398) dev_display (TestScaled) dev_display (Cross1) * 11. 在实物图上绘制定位点十字标记 gen_cross_contour_xld (Cross2, RowReal, ColReal, 15, 0.785398) dev_display (Test2) dev_display (Cross2)设计图定位点坐标:
实物图定位点坐标:
只要我们定位点选的好,仿射变换矩阵一般效果就不错。
现在得到了仿射变换矩阵,现在就可以开始检测了,今天介绍一下最简单的检测,实际的检测肯定没有这么简单。
* ======================================== * 13. 缺陷检测:对齐后的设计图与实物图做差 + 轮廓掩膜排除 * ======================================== * 13.1 裁剪对齐后的设计图,使其与实物图尺寸一致 affine_trans_point_2d (HomMat2D, 0, 0, OffsetRow, OffsetCol) get_image_size (Test2, W2, H2) crop_part (TestAligned, TestAlignedCrop, OffsetRow, OffsetCol, W2, H2) * 13.2 将两幅图转为灰度 rgb1_to_gray (TestAlignedCrop, GrayDesign) rgb1_to_gray (Test2, GrayReal) * 13.3 计算绝对差值 abs_diff_image (GrayReal, GrayDesign, DiffImage, 1) * ======================================== * 13.4 提取设计图中的正常轮廓区域(作为掩膜) * ======================================== * 对设计图做边缘检测 + 膨胀,生成轮廓掩膜 edges_sub_pix (TestAlignedCrop, DesignEdges, 'canny', 1.5, 10, 20) gen_region_contour_xld (DesignEdges, DesignEdgeRegion, 'margin') * 膨胀轮廓区域(覆盖轮廓线宽 + 配准误差范围) dilation_circle (DesignEdgeRegion, DesignEdgeDilated, 5.0) * 合并所有轮廓为一个掩膜 union1 (DesignEdgeDilated, ContourMask) * ======================================== * 13.5 差分结果中排除正常轮廓区域 * ======================================== * 阈值分割提取差异 threshold (DiffImage, DiffRegion, 30, 255) * 从差异区域中减去轮廓掩膜 difference (DiffRegion, ContourMask, DefectCandidates) * ======================================== * 13.6 形态学处理 + 面积过滤 * ======================================== * 填充小孔洞 fill_up (DefectCandidates, DefectFilled) * 连通域分割 connection (DefectFilled, DefectConnected) * 去除小噪声 + 面积过滤 select_shape (DefectConnected, DefectRegions, 'area', 'and', 200, 5000) * ======================================== * 13.7 显示结果(用圆标记缺陷位置) * ======================================== * 显示实物图 dev_display (Test2) * 显示被排除的轮廓区域(绿色,半透明) dev_set_draw ('margin') dev_set_line_width (1) dev_set_color ('green') dev_display (DesignEdgeDilated) * 用圆标记缺陷位置 dev_set_draw ('margin') dev_set_line_width (2) dev_set_color ('red') * 计算每个缺陷的等效圆半径,用于绘制 smallest_circle (DefectRegions, DefectCenterRows, DefectCenterCols, DefectRadii) * 绘制圆标记 gen_circle_contour_xld (DefectCircles, DefectCenterRows, DefectCenterCols, DefectRadii*1.5, 0, 6.28318, 'positive', 1) dev_display (DefectCircles) * 在圆心处画十字 gen_cross_contour_xld (DefectCrosses, DefectCenterRows, DefectCenterCols, 15, 0.785398) dev_set_color ('yellow') dev_display (DefectCrosses)我们来看这一个过程,首先是灰度化之后做差。
然后我们发现直接做差并不全是缺陷区域,也有一些是正常的轮廓,然后就把轮廓当掩膜。
减去掩膜区域之后就会得到真正的缺陷位置。
然后我们再标注出这个缺陷位置。
这只是简化的例子,实际上可能还要再做一些操作,找到缺陷之后还需要判断是什么缺陷,也要结合深度学习去训练模型去做一些过滤之类的。
当我们有Halcon脚本之后再转化为C#问题就不大了,现在有AI了一般交给AI就可以了,篇幅有限,转化为WPF应用后面再进行分享。
以上是AOI检测流程的一个简单分享与自己的学习记录,希望对你有所帮助。
