08 实战 套环检测#
一句话总结
遍历文件夹里的多张图像,逐张做 threshold → connection → select_shape,用 area_center + |Area| 得到零件个数,并用 dev_disp_text 把个数和每个零件的面积写到窗口上。
源文件:
note/04套环检测.hdev素材:note/套环检测/图像00228.BMP、note/套环检测/图像00235.BMP(800×600,8 位灰度)
一、素材分析(先看清图,再写代码)#

| 项目 | 实测值 |
|---|---|
| 尺寸 | 800 × 600,8 位灰度(get_image_size 得 Width=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 |
阈值数字的来历
213574、22.54、8.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 |
为什么最终个数比\"肉眼数的个数\"略少
- 开口件不被计数(左上角那个 "C" 形),因为环体不闭合,内孔与外背景连通;
- 被遮挡的内孔可能面积不足或被压成月牙形,
inner_radius不达标而被剔除; - 图像边缘被裁掉一半的零件同理。
这恰好说明:这套参数是为"完整、闭合、不重叠"的挡圈场景设计的。
五、可复用的套路(批量处理模板)#
这段代码的骨架可以直接搬到任何"一个文件夹一堆图,逐张统计"的任务:
* ===== 批量处理通用骨架 =====
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'] |
两个坑
list_files返回的是相对于 HDevelop 当前工作目录的路径。如果换一台机器打开工程,工作目录变了,read_image就会找不到文件 —— 工程迁移时务必把套环检测/目录一起带上,并确认 HDevelop 的"设置 → 通用 → 当前目录"。- 目录里若混入
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)
六、改进方向#
如果零件是\"暗色物体\"(更常规的做法)
直接对暗色零件做二值化,再判别"是不是环":
这种做法对开口件也有效(不需要环体闭合),鲁棒性更好。其他可改进点:
| 问题 | 改进手段 |
|---|---|
| 破损/开口环漏检 | 改用上面的"直接检测暗色零件 + holes_num"思路 |
| 光照不均 | 先用 illuminate 补偿,或改用 binary_threshold 自适应阈值 |
| 重叠粘连 | 加 opening_circle 或分水岭 watersheds 分割 |
| 定位精度要求高 | 用 smallest_circle 拟合外圆,或转 XLD 用 fit_circle_contour_xld 亚像素拟合 |
| 需要输出报表 | AllCount 累积到元组,循环结束后统一 write_ 到文件 |
七、自检#
Question
- 为什么
num := |Area|得到的就是零件个数?——Area是area_center的输出元组,长度 = 保留区域个数,而每个保留区域对应一个挡圈内孔。 - 如果把
threshold改成0 ~ 100会怎样?—— 选中的变成暗色环体本身,connection后得到的是一个个环形区域;此时inner_radius会接近 0(环体是细窄的),8.7的下限会把它们全部剔除,结果为空。 - 想让左上角那个开口件也被计数,最小改动是什么?—— 先对暗色零件做
closing_circle把开口补上,再按本例思路处理;或改用第六节的"直接检测暗色零件"方案。
上一步:区域测量与结果可视化 · 下一步:实战 曲别针计数与角度