跳转至

08 实战 套环检测#

一句话总结

遍历文件夹里的多张图像,逐张做 threshold → connection → select_shape,用 area_center + |Area| 得到零件个数,并用 dev_disp_text 把个数和每个零件的面积写到窗口上。

源文件: note/04套环检测.hdev 素材: note/套环检测/图像00228.BMPnote/套环检测/图像00235.BMP(800×600,8 位灰度)

一、素材分析(先看清图,再写代码)#

项目 实测值
尺寸 800 × 600,8 位灰度(get_image_sizeWidth=800, Height=600
背景 ,灰度 ≈ 255,占约 92.5% 的像素
零件 ,灰度 ≈ 30,占约 7.5%
零件形态 带小凸耳的闭合圆环(挡圈 / 卡簧类),环体被暗色笔画围成一个闭合圈
数量 一图约 40 个左右(含个别破损/开口件)

二、核心思路:为什么阈值取 192~255?#

这是本例最容易被误读的地方

threshold (Image, Regions, 192, 255) 选中的是亮背景不是零件本身

但这不是写错,而是一种巧妙的取反思路: 每个挡圈的暗色环体是一条闭合曲线,它把环内的一小块亮背景框死了。于是对"亮背景"做 connection 之后: - 环外的背景 → 1 个巨大的连通域(铺满整幅图) - 每个闭合环的内部空白各自成为 1 个独立的小连通域

因此统计"环内空白"的个数 = 统计挡圈的个数select_shape 里的 inner_radius(最大内切圆半径)条件,正是在保证"这块亮区确实是被环体围出来的内孔"。

上图为按源码参数复算的结果:红色 = 保留的区域(每个挡圈的内孔),蓝色 = 被剔除的区域(外圈大背景 + 环体上的细小夹缝)。左上角那个开口的 "C" 形挡圈因为环体不闭合,内部与外部背景连通,所以不会被计数 —— 这是本方法的原理性局限,见文末"改进方向"。

三、完整源码解读#

* 读取文件夹
list_files ('套环检测', 'files', Files)
read_image (Image, Files[0])

* 得到图片的尺寸
get_image_size (Image, Width, Height)

* 关闭原来的窗口, 打开与图像同尺寸的新窗口
dev_close_window ()
dev_open_window (0, 0, Width, Height, 'black', WindowHandle)
dev_display (Image)

* 循环加载文件夹里面的所有图片
for Index := 0 to |Files| -1 by 1
    read_image (Image, Files[Index])

    * 二值化处理
    threshold (Image, Regions, 192, 255)

    * 连通域处理, 分割
    connection (Regions, ConnectedRegions)

    * 过滤筛选
    select_shape (ConnectedRegions, SelectedRegions, \
                  ['area','max_diameter','inner_radius'], 'and', \
                  [0, 22.54, 8.7], [213574, 91.72, 200])

    dev_set_color ('blue')
    dev_clear_window ()
    dev_display (Image)
    dev_display (SelectedRegions)

    * 获取区域集合中所有数据的坐标和面积
    area_center (SelectedRegions, Area, Row, Column)

    * 个数
    num := |Area|

    * 显示个数 (显示的内容, 显示在窗口上, x坐标, y坐标)
    dev_disp_text ('个数'+num, 'window', 100, 100, 'blue', ['border_radius'], [1])

    * 每一个的中间都显示面积信息
    for Index1 := 0 to num-1 by 1
        dev_disp_text (Area[Index1], 'image', Row[Index1], Column[Index1], 'black', [], [])
    endfor

    stop ()
endfor

3.1 逐行关键点#

代码 关键点
list_files ('套环检测', 'files', Files) 第 1 个参数是目录名,第 2 个是选项,'files' 只要文件(另有 'directories''recursive''max_depth N')。输出 Files路径字符串元组
read_image (Image, Files[0]) 先读第一张,只为了 get_image_size 拿到尺寸来开窗口
for Index := 0 to \|Files\| -1 by 1 \|Files\| 是文件个数;注意是"长度减 1",不是 \|Files - 1\|
threshold (..., 192, 255) 取亮背景(见上文原理分析)
connection (...) 打散成 57 个连通域(1 大 + 56 小)
select_shape (...) 三重过滤,见下表
area_center (...) 输出 Area, Row, Column,都是长度 = 区域个数的元组
num := \|Area\| 元组长度 = 区域个数 = 零件个数
dev_disp_text (..., 'window', 100, 100, ...) 统计信息用 window 坐标固定在左上角
dev_disp_text (..., 'image', Row[i], Column[i], ...) 单体信息用 image 坐标贴在每个零件上
stop () 每张图处理完停一下,方便逐张核对

3.2 select_shape 参数逐条拆解#

select_shape (ConnectedRegions, SelectedRegions, \
              ['area',      'max_diameter', 'inner_radius'], 'and', \
              [0,           22.54,          8.7], \
              [213574,      91.72,          200])
特征 下限 上限 作用 实测数据支撑
'area' 0 213574 下限 0 = 不限制;上限用来踢掉外圈大背景 外圈背景实测约 412,000 ~ 416,000 像素,远超上限;内孔只有 ~1000 像素
'max_diameter' 22.54 91.72 踢掉细小碎块(环体上的夹缝),也防止过大的粘连块 真正的内孔直径实测 34.4 ~ 35.5;被剔除的碎块直径只有 ~10 ~ 17
'inner_radius' 8.7 200 本方法的关键:只有"内部能放下一个半径 ≥ 8.7 的圆"的区域才被认可为内孔 内孔的内切圆半径实测 10 ~ 13;碎块只有 ≤ 6

阈值数字的来历

21357422.548.7 这些带小数的数字不是推导出来的,而是实测出来的。流程是: 1. 先不筛选,area_center + 把特征打到窗口上,观察数值分布; 2. 找到"目标"与"干扰"之间明显的数值分界线; 3. 在分界线附近取值,留一点余量。

例如 area 上限 213574:背景 41 万 vs 内孔 1000,分界极其明显,取 21 万完全安全。

四、预期输出结果(实测复算)#

步骤 图像00228 图像00235
threshold 192~255 后的连通域个数 57 57
剔除外圈大背景(area ≤ 213574)后 56 56
再过 max_diameter 27 28
再过 inner_radius最终计数 27 个 25 个
保留区域的面积范围 934 ~ 995 816 ~ 994

为什么最终个数比\"肉眼数的个数\"略少

  1. 开口件不被计数(左上角那个 "C" 形),因为环体不闭合,内孔与外背景连通;
  2. 被遮挡的内孔可能面积不足或被压成月牙形,inner_radius 不达标而被剔除;
  3. 图像边缘被裁掉一半的零件同理。

这恰好说明:这套参数是为"完整、闭合、不重叠"的挡圈场景设计的

五、可复用的套路(批量处理模板)#

这段代码的骨架可以直接搬到任何"一个文件夹一堆图,逐张统计"的任务:

* ===== 批量处理通用骨架 =====
list_files (DirName, 'files', Files)          * 1 列出待处理文件
read_image (Image, Files[0])                  * 2 用第一张确定窗口尺寸
get_image_size (Image, Width, Height)
dev_close_window ()
dev_open_window (0, 0, Width, Height, 'black', WindowHandle)

AllCount := []                                * 5 准备累积结果的元组
for Index := 0 to |Files| - 1 by 1
    read_image (Image, Files[Index])          * 3 读图
    * ---- 处理链 ----
    threshold (Image, Region, MinGray, MaxGray)
    connection (Region, ConnectedRegions)
    select_shape (ConnectedRegions, SelectedRegions, 'area', 'and', 500, 99999)
    * ---- 统计与显示 ----
    area_center (SelectedRegions, Area, Row, Column)
    Count := |Area|
    AllCount := [AllCount, Count]             * 6 追加到总表
    dev_clear_window ()
    dev_display (Image)
    dev_display (SelectedRegions)
    dev_disp_text ('个数'+Count, 'window', 20, 20, 'blue', ['border_radius'], [1])
    stop ()                                   * 4 逐张停一下方便核对
endfor

list_files 的常用选项:

选项 含义
'files' 只返回文件
'directories' 只返回目录
'recursive' 递归子目录
'max_depth 2' 限定递归深度
'follow_links' 跟随符号链接
[选项1, 选项2] 组合使用,如 ['files','recursive']

两个坑

  1. list_files 返回的是相对于 HDevelop 当前工作目录的路径。如果换一台机器打开工程,工作目录变了,read_image 就会找不到文件 —— 工程迁移时务必把 套环检测/ 目录一起带上,并确认 HDevelop 的"设置 → 通用 → 当前目录"。
  2. 目录里若混入 Thumbs.db.txt 等非图像文件,read_image直接报错中断循环。稳妥写法是先筛掉:
list_files (DirName, ['files','recursive'], Files)
* 只保留常见图像后缀HALCON  tuple_regexp_select 比字符串匹配更稳妥
tuple_regexp_select (Files, ['\\.(bmp|BMP|png|jpg|jpeg|tif|tiff)$'], ImageFiles)

六、改进方向#

如果零件是\"暗色物体\"(更常规的做法)

直接对暗色零件做二值化,再判别"是不是环":

threshold (Image, Dark, 0, 100)              * 直接取暗色零件
connection (Dark, Parts)
select_shape (Parts, Rings, ['area','holes_num','circularity'], 'and', \
              [3000, 1, 0.6], [99999, 10, 1.0])   * 有孔 + 够圆 = 
这种做法对开口件也有效(不需要环体闭合),鲁棒性更好。

其他可改进点:

问题 改进手段
破损/开口环漏检 改用上面的"直接检测暗色零件 + holes_num"思路
光照不均 先用 illuminate 补偿,或改用 binary_threshold 自适应阈值
重叠粘连 opening_circle 或分水岭 watersheds 分割
定位精度要求高 smallest_circle 拟合外圆,或转 XLD 用 fit_circle_contour_xld 亚像素拟合
需要输出报表 AllCount 累积到元组,循环结束后统一 write_ 到文件

七、自检#

Question

  1. 为什么 num := |Area| 得到的就是零件个数?—— Areaarea_center 的输出元组,长度 = 保留区域个数,而每个保留区域对应一个挡圈内孔。
  2. 如果把 threshold 改成 0 ~ 100 会怎样?—— 选中的变成暗色环体本身,connection 后得到的是一个个环形区域;此时 inner_radius 会接近 0(环体是细窄的),8.7 的下限会把它们全部剔除,结果为空。
  3. 想让左上角那个开口件也被计数,最小改动是什么?—— 先对暗色零件做 closing_circle 把开口补上,再按本例思路处理;或改用第六节的"直接检测暗色零件"方案。

上一步:区域测量与结果可视化 · 下一步:实战 曲别针计数与角度