首页 > 技术文章 > Halcon视觉入门芯片识别

LearningC 2022-02-08 13:47 原文

Halcon视觉入门芯片识别

需求

有如下图的一个摆盘,摆盘的方格中摆放芯片,一个格子中只放一个,我们需要知道每个方格中是否有芯片去指导我们将芯片放到空的方格中。

image

分析

通过图片分析得出

  1. 我们感兴趣的区域在中间,每个方格大小类似

  2. 芯片大小相同

  3. 方格是白色的

  4. 方格有100个,方格外的不识别

解决问题的思路

  1. 建立ROI关注每一个方格

  2. 需要提取方格的特征

  3. 单独分析每一个方格,如果方格中存在芯片(通过特征分析芯片是否存在),则标注出来,如果不存在,标注为红色,代表是空的,可以摆放芯片。

在编写代码之前,大体思路出来了,需要的是尝试各种参数得到我们想要的结果。

代码如下:


dev_close_window ()
dev_open_window (0, 0, 1200, 850, 'black', WindowHandle) 
dev_clear_window() 
* read_image (Image, 'D:/temp/chips/20220110112001.jpg')  
* read_image (Image, 'D:/temp/chips/20220110112121.jpg')  
* read_image (Image, 'D:/temp/chips/20220110112137.jpg')
 read_image (Image, 'D:/temp/chips/Image_20211227154621354.bmp') 

*使用canny算子提取亚像素边界
edges_sub_pix (Image, Edges, 'canny', 10, 20, 40) 
*根据边长和面积提取xld
select_shape_xld (Edges, SelectedXLD, ['rect2_len1','area'], 'and', [70,18000], [100,33000]) 
*根据xld绘出区域
gen_region_contour_xld (SelectedXLD, Region, 'margin') 
*根据区域面积提取芯片盒子
select_shape (Region, ChipsImage, ['width','height'], \
'and', [155,155], [170,170]) 
*合并区域
union1 (ChipsImage, RegionUnion)
*填充区域
fill_up (RegionUnion, ROI_0)
*根据联合区域裁剪感兴趣区域
reduce_domain (Image, ROI_0, ImageReduced)
*联通分割区域
connection (ImageReduced, ConnectedRegions) 
*将每个芯片方格转换成标准矩形
shape_trans (ConnectedRegions, RegionTransRect, 'rectangle1')
*排序
sort_region (RegionTransRect, SortedRegions, 'character', 'true', 'row') 
*总孔位数量
count_obj (SortedRegions, Number) 
*结果数组,0 代表没有 1代表有芯片
result:= [] 
*空位的数量
EmptyCount :=0 
dev_display (Image) 
for Index := 1 to Number by 1 
  * 找到索引对应的区域
   select_obj (SortedRegions, ALL_ROI_OF_SINGLES, Index)
   *裁剪区域
   reduce_domain (Image, ALL_ROI_OF_SINGLES, ChipBoxRegion)   
   *动态阈值分割
   binary_threshold (ChipBoxRegion, ChipRegion, 'max_separability', 'dark', UsedThreshold)
   *反向选择
   difference (ChipBoxRegion, ChipRegion, ChipWithLine)  
   *联通区域
   connection (ChipWithLine, ConnectedRegions1) 
   *根据面积特征筛选区域
   select_shape (ConnectedRegions1, SelectedRegions, 'area', 'and', 1000.59, 3001.78)
   *腐蚀
   erosion_circle (SelectedRegions, ChipErosion, 2)    
   *再次通过面积筛选  
   select_shape (ChipErosion, ChipInside, 'area', 'and', 1000, 3000)
   *腐蚀
   erosion_circle (ChipInside, ChipErosion1, 2) 
   *计算赛选个数
   count_obj (ChipErosion1, chipExists) 
  
   if (chipExists == 0) 
       *判断不存在芯片的逻辑
        EmptyCount := EmptyCount +1
        *显示用
        dev_update_on()
        dev_set_color ('red')
        dev_set_draw ('fill')
        dev_display (ALL_ROI_OF_SINGLES)   
        dev_update_off()
   endif  
   if(chipExists == 1) 
       *判断有芯片的逻辑 
       
       *显示用
        dev_update_on()
        dev_set_color ('green')
        dev_set_draw ('fill') 
        shape_trans (ChipErosion1, RegionTrans, 'rectangle2')
        dev_display (RegionTrans)  
        dev_update_off()
   endif
   result[Index-1] := chipExists  
endfor  
disp_message (WindowHandle, ' Holes Number:'+Number + ' , Exists Count:' + (Number - EmptyCount) , 'window', 12, 12, 'black', 'true')  

下面开始分析代码


dev_close_window ()
dev_open_window (0, 0, 1200, 850, 'black', WindowHandle) 
dev_clear_window() 
* read_image (Image, 'D:/temp/chips/20220110112001.jpg')  
* read_image (Image, 'D:/temp/chips/20220110112121.jpg')  
* read_image (Image, 'D:/temp/chips/20220110112137.jpg')
 read_image (Image, 'D:/temp/chips/Image_20211227154621354.bmp') 

这段代码,看算子名称就知道意思。关闭窗口;打开窗口,然后清空窗口;最后读取一张图片。读取的图片就是上图展示的图片。

*使用canny算子提取亚像素边界
edges_sub_pix (Image, Edges, 'canny', 10, 20, 40) 
*根据边长和面积提取xld
select_shape_xld (Edges, SelectedXLD, ['rect2_len1','area'], 'and', [70,18000], [100,33000]) 

这里是有两个概念 亚像素xld

亚像素 因为图片的最小单位是像素,亚像素是比像素还小的显示方式,但只是在显示上做的区分,并不是可以精确到亚像素。

xld 可以理解成轮廓

edges_sub_pix (Image, Edges, 'canny', 10, 20, 40)

Image 是输入;Edges 是输出, canny,10,20,40 是控制参数

为什么是这4个控制参数,可以参考F1

这里 只有 10 是 经过调整的参数,其他参数是默认值,数值越小精细度越高,细节也就越多,需要处理的数据也就越多。当它是1 的时候如图:
image

当它是10的时候如图:
image

当参数是10的时候,我们需要的轮廓已经出来了,然后通过边长(rect2_len1)和面积(area)过滤出我们需要的区域,如图:

image

可以看到,干扰不多了,接下来我们要将xld 绘制成区域,用到算子 gen_region_contour_xld

*根据xld绘出区域
gen_region_contour_xld (SelectedXLD, Region, 'margin') 

这里可以看到,上一步的结果,是这一步的输入,这是Halcon编程的基本套路。第二个参数是输出参数,也就是我们得到的区域,第三个参数是绘制方式,这里用到了margin 当然也可以用fill,得到的结果是这样的:

image

然后再根据面积过滤掉不要的对象,说的是面积,实际上用到的是 宽(width),高(height)

*根据区域面积提取芯片盒子
select_shape (Region, ChipsImage, ['width','height'], \
'and', [155,155], [170,170]) 

这里的参数 155-170 是通过特征窗口得到的,如图:

image

然后就是这样的结果了,如图:

image

*合并区域
union1 (ChipsImage, RegionUnion)
*填充区域
fill_up (RegionUnion, ROI_0)
*根据联合区域裁剪感兴趣区域
reduce_domain (Image, ROI_0, ImageReduced)
*联通分割区域
connection (ImageReduced, ConnectedRegions) 
*将每个芯片方格转换成标准矩形
shape_trans (ConnectedRegions, RegionTransRect, 'rectangle1')
*排序
sort_region (RegionTransRect, SortedRegions, 'character', 'true', 'row') 
*总孔位数量
count_obj (SortedRegions, Number) 

这段代码就很好理解了,合并区域(union1),为什么要合并呢,因为这些区域是分散的,我们需要建立一个感兴趣区域,然后对它进行裁剪,所以需要将他们合并在一起。合并后我做了一个填充也就是 (fill_up),填充后是这样的:

image

裁剪( reduce_domain (Image, ROI_0, ImageReduced) )后是这样的:

image

接下来就是连通区域(connection),将他们分割,然后 转成标准矩形(shape_trans),排序(sort_region),统计个数(count_obj),这样我们就得到了每个方格。

这里说一下排序,排序是为了让它从左到右从上到下方便我们遍历。

*结果数组,0 代表没有 1代表有芯片
result:= [] 
*空位的数量
EmptyCount :=0 

这里定义了一个数组存放结果,定义了一个遍历存放空位。接下来就是开始遍历分析每一个方格对象。

这里说一下,这个语法跟pascal语法很像,之前有做过Delphi项目,所以这个语法我也没有专门学过,做到哪里查到哪里,有不足的地方还请指正。

for Index := 1 to Number by 1 
  * 找到索引对应的区域
   select_obj (SortedRegions, ALL_ROI_OF_SINGLES, Index)
   *裁剪区域
   reduce_domain (Image, ALL_ROI_OF_SINGLES, ChipBoxRegion)   
   *动态阈值分割
   binary_threshold (ChipBoxRegion, ChipRegion, 'max_separability', 'dark', UsedThreshold)
   *反向选择
   difference (ChipBoxRegion, ChipRegion, ChipWithLine)  
   *联通区域
   connection (ChipWithLine, ConnectedRegions1) 
   *根据面积特征筛选区域
   select_shape (ConnectedRegions1, SelectedRegions, 'area', 'and', 1000.59, 3001.78)
   *腐蚀
   erosion_circle (SelectedRegions, ChipErosion, 2)    
   *再次通过面积筛选  
   select_shape (ChipErosion, ChipInside, 'area', 'and', 1000, 3000)
   *腐蚀
   erosion_circle (ChipInside, ChipErosion1, 2) 
   *计算赛选个数
   count_obj (ChipErosion1, chipExists) 
  
   if (chipExists == 0) 
       *判断不存在芯片的逻辑
        EmptyCount := EmptyCount +1 
   endif  
   if(chipExists == 1) 
       *判断有芯片的逻辑  
   endif
   result[Index-1] := chipExists  
endfor  

这里说几个算子 select_obj 可以选择某个区域,select_obj (SortedRegions, ALL_ROI_OF_SINGLES, Index)
第一个参数是所有区域的集合,第二个参数是输出的区域,也就是我们通过索引得到的区域,第三个参数是索引

这里是索引是从1开始的,要格外注意

接下来就继续裁剪,根据每个小方格,继续裁剪,如图:

image

因为动态阈值分割的结果是这样的,如图:

image

它选择了我们不要的内容,所以需要求反。用到了 difference (ChipBoxRegion, ChipRegion, ChipWithLine) 。这里第一个参数是整体,第二个参数是阈值分割的结果,第三个参数是求差之后的结果,也就是我们需要的结果,如图:

image

此时,按照惯例,处理边缘的干扰,如果得到的区域是有内容的,说明是有芯片的,如果没有内容,说明没有放芯片。

   *联通区域
   connection (ChipWithLine, ConnectedRegions1) 
   *根据面积特征筛选区域
   select_shape (ConnectedRegions1, SelectedRegions, 'area', 'and', 1000.59, 3001.78)
   *腐蚀
   erosion_circle (SelectedRegions, ChipErosion, 2)    
   *再次通过面积筛选  
   select_shape (ChipErosion, ChipInside, 'area', 'and', 1000, 3000)
   *腐蚀
   erosion_circle (ChipInside, ChipErosion1, 2) 
   *计算赛选个数
   count_obj (ChipErosion1, chipExists) 

连通区域,通过面积过滤,然后 腐蚀,腐蚀的目的是去掉周边的干扰。这个数值 2 也是经过多次尝试得到的。

然后再次通过面积筛选,去掉干扰。继续腐蚀,也是为了去掉干扰。这里的逻辑会根据实际情况发生变化。

最后计数( count_obj (ChipErosion1, chipExists) )。如果chipExists ==1 说明有芯片,否则就是没有芯片。

然后根据 chipExists的值进行判断处理即可。我这里的处理方式是标注,如图:

image

红色代表是空的,如果不是空的,我们将芯片的区域绘制成绿色。

总结

在编程的过程中,感受到视觉识别是一门多方面配合的工作,如果光线情况比较好,精度高很多时候可以让我们节省很多处理步骤。所以,视觉识别不单单是编程的问题。如果能在前期将光打好,后期可以节省很多事情,提高处理效率。

最后如果本文有不对的地方,欢迎指正。

推荐阅读