跳转至

13 形态学调整 结构元半径标定#

一句话总结

形态学只有一个旋钮 —— 结构元半径 Radius。它不是拍脑袋试出来的:先用距离变换算出缺陷的"内切圆半径",那就是半径的理论下限;再在它附近从小到大扫一遍,取"缺陷清零、主体面积变化最小"的最小半径。 本篇把 5 张素材的调参过程全部量化,并给出三个只看原理推不出来的坑

源素材: note/形态学调整/1.bmp ~ 5.bmp(与 note/x1~x5.bmp 是同一批图,仅编号不同,映射见文末「附录」) 理论前置: 形态学运算(四个算子的定义)· 图像预处理与滤波(二值化阈值)

一、为什么半径可以"算"出来,而不是"试"出来#

圆盘结构元有一个极好的性质:腐蚀和膨胀都等价于"距离阈值"

\(A\) 是白区,\(A^c\) 是黑区,\(d(p, S)\) 表示点 \(p\) 到集合 \(S\) 的最近距离,则

\[\text{erode}_R(A) = \{\, p \in A \;\big|\; d(p, A^c) > R \,\} \qquad \text{dilate}_R(A) = \{\, p \;\big|\; d(p, A) \le R \,\}\]

于是开运算、闭运算能干掉多大的缺陷,完全由"缺陷到边界的距离"决定

缺陷 被清除的条件 结论
白斑(噪点) 斑内每一点到最近黑点的距离都 \(\le R\) opening_circle\(R \ge\) 白斑的内切圆半径
孔洞(黑斑) 孔内每一点到最近白点的距离都 \(\le R\) closing_circle\(R \ge\) 孔洞的内切圆半径
flowchart LR
    A["二值图 Region"] --> B["距离变换<br/>每个像素到边界的距离"]
    B -->     C["取缺陷区域内的<br/>最大距离 = 内切圆半径"]
    C --> D["贴着它取略大的值<br/>→ 理论最小半径"]
    D --> E["在它附近扫描 1~5<br/>验证清零 + 面积变化"]
    E --> F["定稿配方"]

这个判据的几何说法

开运算 = "能放进区域内部的、所有半径 \(R\) 的圆盘"的并集。 所以一句话:白斑活得下来 ⇔ 斑里塞得下一个半径 \(R\) 的圆盘。半径一旦比缺陷的内切圆大,缺陷必然被抹掉 —— 这就是"理论最小半径"的含义。

HDevelop 里怎么算这个距离

HALCON 原生算子:distance_transform (Region : DistanceImage : Metric, Foreground, Width, Height)Metric'euclidean'Foreground'true' 时可得到区域内部各点到边界的距离图。 在缺陷区域内取极大值,就是它的内切圆半径。 注意官方文档写的是 "approximately Euclidean",'euclidean' 是近似欧氏距离;要更精确可改用 'city-block' 交叉验证。

二、动手之前:先把"字符本身"和"缺陷"分开#

这一步做错,后面全错。阈值 128 二值化之后,画面里其实混着五类东西(其中只有两类是缺陷):

类别 典型例子 是缺陷吗
字符主体 笔画最大的那块白 不是
字符的附加白块 手写体上方那个分离的点 不是
白噪点 散落的孤立小白斑、毛刺 ✅ 用开运算
字符自带的环孔 "口/日/回"式笔画围出的内部空白 不是
多余孔洞 笔画里被噪声啃出的黑点 ✅ 用闭运算

所以统计时我用两条规则把它们分开:

  • 白块:按面积排序,前两大白块算"字符"(主体 + 顶部的点),其余白块才是白噪点;
  • 孔洞:只统计不触边的内部孔洞,其中最大的那个孔算"字符自带的环",其余才算多余孔洞。

两种典型翻车

不分"字符"和"噪声":干净图 1.bmp 会被统计出两万多像素的"白噪点"(其实是笔画和点),调参方向直接跑偏。 ② 把环孔当缺陷:手写体那个 2231~5432 px 的环孔是字的一部分,半径取大就会把"口"磨小(实测见 5.3)。

判缺陷要看\"数量\",不是\"有没有\"

干净图 1.bmp 里其实也有 1 个 10 px 的小孔(笔画转折处),但那是正常结构。 所以判定门槛设成 ≥ 3 个才算缺陷 —— 只有 1~2 个小孔时,不值得为它动形态学。

三、5 张素材的实测总表(阈值 128)#

原图诊断:

素材 尺寸 字符面积 自带环孔 白噪点 多余孔洞 诊断
1.bmp 321×443 22502 px 2231 px 0 个 1 个 / 10 px 干净,不动
2.bmp 321×443 22339 px 2166 px 5 个 / 16 px 12 个 / 505 px 黑孔洞为主 + 少量白斑
3.bmp 321×443 22148 px 2274 px 55 个 / 278 px 0 个 白噪点
4.bmp 445×602 42508 px 3974 px 359 个 / 5107 px 0 个 白噪点(密集)
5.bmp 498×646 49837 px 5432 px 0 个 74 个 / 1580 px 黑孔洞为主

理论最小半径 → 推荐配方:

素材 缺陷内切圆半径(白噪点 / 孔洞) 推荐配方 处理后 主体面积变化
1.bmp — / 2.00 不处理 —— +0.00%
2.bmp 1.41 / 4.47 closing_circle(4.5) 白噪点 0、孔洞 0 +3.49%
3.bmp 1.00 / — opening_circle(1) 白噪点 0 −0.97%
4.bmp 3.00 / — opening_circle(3) 白噪点 0 −1.52%
5.bmp — / 3.00 ※ closing_circle(4.5) 孔洞 0 +6.25%

※ 唯一一处\"理论值和实测对不上\"的地方,见第五节 5.1

5.bmp 的孔洞内切圆半径最大只有 3.00,按理 Radius = 3.5 就该填完; 但实测 33.5 各残留 1 个孔,要 4.5 才干净 —— 因为闭运算自己会造出新孔

四、逐张解读#

4.1 1.bmp —— 干净对照,正确做法是"不动它"#

0 个白噪点、1 个 10 px 小孔(不足 3 个 → 不算缺陷)。这张图的价值是当基准:后面所有面积变化都以它为参照系,也提醒我们"形态学不是必做步骤"。

4.2 2.bmp —— 黑孔洞为主,顺带吞掉白斑 → closing_circle(4.5)#

12 个孔洞 > 5 个白斑,按"先治数量多的"原则先做闭运算。半径扫描:

closing_circle(r) 白噪点 多余孔洞 主体面积变化 判定
1 5 / 16 px 7 / 438 px +0.38% 残留
2 3 / 10 px 6 / 349 px +0.90% 残留
3 0 / 0 px 2 / 148 px +2.59% 残留
4 0 / 0 px 1 / 68 px +3.42% 残留
4.5 0 0 +3.49% ★ 清零
5 0 0 +4.00% ★ 清零(但更伤)
  • 孔洞内切圆半径 4.47 → 半径必须 >4.47,取 4.5 最省;
  • 注意白噪点从 5 个变 0 个、白块数从 7 个变 2 个 —— 这不是"去噪",而是闭运算把贴近笔画的 5 个小斑粘连并吞进主体了(见 5.2);
  • 主体面积从 22339 涨到 23119 px,+3.49%

4.3 3.bmp —— 典型白毛刺/小白点 → opening_circle(1)#

55 个白噪点共 278 px,内切圆半径最大只有 1.00,所以半径取 1 就够了:

opening_circle(r) 1 1.5 2 3 5
白噪点 0 0 0 0 0
主体面积变化 −0.97% −0.95% −0.99% −1.36% −1.63%

r=1 起全部清零,后面每加大一点都只是在多削主体 —— 这正是"取理论下限"的意义。

与 [[06 形态学运算]] 的对照

06 篇里对同一张图(那边叫 x2.bmp)用的是 opening_circle(3.5),属"宁大勿小"的经验值; 实测 r=1 就已经清零,3.5 的额外代价是主体多掉 0.06%,而且开运算还会把内部孔洞略微撑大(环孔:r=1 时 +0.04%、r=6 时 +0.44%,因为腐蚀先把孔撑开、膨胀并不能完全补回)。 结论:经验值可以作起点,但标定一遍能省下无谓的形变。

4.4 4.bmp —— 密集椒盐白噪 → opening_circle(3)#

359 个噪点、5107 px,是 5 张里最脏的。半径扫描:

opening_circle(r) 1 1.5 2 2.5 3 4 5
白噪点 277 个 205 个 109 个 45 个 0 0 0
主体面积变化 −0.47% −0.25% −1.15% −0.61% −1.52% −1.82% −1.77%

内切圆半径 3.00 → 取 3,白噪点一次清零,主体只掉 1.52%。 (r=1~2.5 会残留几十到几百个噪点,是典型的"半径取小了"。)

4.5 5.bmp —— 大量黑孔洞 → closing_circle(4.5)#

74 个孔洞、1580 px,但每个都很小(半径 ≤ 3.00)。扫描结果却有意外:

closing_circle(r) 1 2 3 3.5 4 4.5 5
多余孔洞 99 个 33 个 1 个 / 36 px 1 个 / 37 px 0 0 0
主体面积变化 +0.84% +4.20% +6.05% +5.87% +6.94% +6.25% +6.67%

r=3r=3.5 残留的那 1 个孔洞,在原图里根本不存在 —— 详见 5.1。取 4.5(清零且面积变化最小的一档)。

五、三个只有实测才能发现的坑#

5.1 闭运算会"制造"新孔洞#

5.bmp 的孔洞内切圆半径最大 3.00,理论上 r=3.5 应当填满所有孔。实测:

  • r=3 残留 1 个孔,位置 行 282–289 / 列 188–194,36 px;
  • r=3.5 残留同一个位置的孔,37 px;
  • 而原图在 行 275–300 / 列 180–205 范围内只有外背景,一个孔都没有。

也就是说这个孔是闭运算自己造出来的:膨胀把笔画之间的窄缝糊住,腐蚀又没能把它掏开,于是在两条笔画之间围出了一个新的空腔

由此得到两条硬结论

孔洞数不是半径的单调函数 —— 半径变大时,旧孔被填掉、新孔被造出来,数字可能先降后升; ② 扫描表里出现"仍有残留"时,必须回到原图核对残留位置,否则会把"新造的孔"误判成"没填干净的旧孔",从而把半径继续往上加、越加越糟。

5.2 闭运算会把贴近笔画的小白斑"咽"进去,而不是去掉#

2.bmp 原图有 7 个白块(主体 + 点 + 5 个白斑),closing_circle(4.5) 之后只剩 2 个白块、白噪点计数归零。看起来"白噪点被清掉了",但事实是:膨胀让笔画变胖,把旁边的小白斑黏了进来,腐蚀收回去时这些被黏住的斑块已经和主体连成一体、留在了主体里。

所以:闭运算不是去白噪点的手段。它的"白噪点归零"是副作用,代价是主体面积 +3.49%。要真去白噪点,用 opening_circle;两类缺陷同时存在时,先闭后开(或先开后闭,取决于哪一类更多 —— 这里按"多者优先"走的贪心顺序)。

5.3 半径取大,会持续蚕食"字本身的环孔"#

闭运算在填小孔的同时,也会把字符自带的大孔"磨圆"(凹角被填掉),环孔面积因此持续变小:

closing_circle(r) 4.5 6 8 10
5.bmp 主体面积变化 +6.25% +7.45% +8.31% +9.11%
5.bmp 环孔面积变化 −5.49% −7.44% −8.76% −10.44%

r=6(06 篇里对 x4.bmp 用的经验值)时,环孔已经掉了 7.4%;取 4.5 只掉 5.49%。

所以\"宁大勿小\"是错的

半径过大有两种代价:主体被削/增字的内部结构(环孔)被吃掉。 正确策略是贴着理论下限取值R 刚过缺陷内切圆半径即可。 需要"只填孔、不改变外形"时,改用 fill_upselect_shape'holes_num' 直接筛掉带孔的目标,别用闭运算。

六、可复用的调参流程#

flowchart TD
    A["1 读图 + 二值化<br/>threshold(Image, Region, 128, 255)"] --> B["2 区分字符与缺陷<br/>前两大白块=字符 / 最大孔=自带环"]
    B --> C{"3 哪类缺陷多?"}
    C -->|白噪点多| D["opening_circle"]
    C -->|孔洞多| E["closing_circle"]
    D --> F["4 距离变换求缺陷内切圆半径<br/>→ 得到理论最小 R"]
    E --> F
    F --> G["5 在 R 附近扫描 1 / 1.5 / 2 / 3 / 4 / 4.5 / 5"]
    G --> H["6 取'缺陷清零 + 面积变化最小'的最小半径"]
    H --> I["7 回原图核对残留位置<br/>(可能是新造的孔)"]
    I --> J["8 若另一类缺陷仍在,再做一轮"]

HDevelop 里的落地写法(以 2.bmp 为例):

* 1 读图 + 二值化
read_image (Image, '2.bmp')
threshold (Image, Region, 128, 255)

* 2 打散方便区分"字符""缺陷"
connection (Region, ConnectedRegions)

* 3 取黑区候选孔洞),并排除触边的外背景
complement (Region, RegionComplement)
connection (RegionComplement, Holes)
*    外背景一定从图像边缘开始  要求 row1  1  column1  1 即可把它踢掉
select_shape (Holes, InnerHoles, ['row1','column1'], 'and', [1,1], [99999,99999])

* 4 距离变换  在孔洞区域内取极大值 = 内切圆半径半径下限
get_image_size (Image, Width, Height)
distance_transform (Region, DistanceImage, 'euclidean', 'true', Width, Height)

* 5~6 按标定结果落地4.5 "刚好填满全部孔洞"的最小半径
closing_circle (Region, RegionClosing, 4.5)

HDevelop 的注释必须独占一行

官方原文:a comment is marked by entering an asterisk (*) as first non-whitespace character。 所以 xxx (…) * 说明 这种行尾注释是不合法的* 只能出现在行首。

半径不必是整数

HALCON 的 Radius实数4.47 的内切圆半径取 4.5 就够,没必要跳到 5—— 每多 0.5 都在多削一点主体。上面的表里 4.5 → +3.49%5 → +4.00%,差别是实打实的。

七、自检#

Question

  1. 一个白噪点是半径 2.5 的圆斑,开运算半径至少取多少?——2.5 以上(严格大于)。取 2 会残留、取 3 稳妥;理论值就是"圆斑内切圆半径"。
  2. 为什么孔洞数会随半径"先降后升"?——见 5.1:闭运算糊住窄缝会新围出空腔,旧孔没了新孔来了。
  3. 1.bmp 明明有 1 个孔洞,为什么推荐"不处理"?——因为 1 个 10 px 的孔在阈值下属正常结构,判缺陷看数量(≥3),只有 1~2 个时动形态学是得不偿失。
  4. 想保留零件的真实尺寸,同时又想弄干净噪点,怎么办?——不要把形态学放进测量链路:改用 select_shape'area' / 'holes_num' 直接筛掉噪点与带孔目标;或只在"修形区域"上取形状、在原区域上取尺寸。
  5. closing_circle(4.5) 之后白噪点计数变成 0,说明闭运算能去白噪点吗?——不能。计数归零是小白斑被"黏并"进主体(见 5.2),不是被消除。

附录:编号映射与素材说明#

note/形态学调整/ 这 5 张图与库里原有的 note/x1~x5.bmp 逐字节相同(已用 MD5 校验),只是编号顺序不同:

本篇用 等价于 内容 备注
形态学调整/1.bmp x5.bmp 干净对照 形态学运算 里的 x5
形态学调整/2.bmp x1.bmp 少量白斑 + 12 个黑孔洞 ——
形态学调整/3.bmp x2.bmp 白毛刺 / 孤立小白点 06 篇里的 x2
形态学调整/4.bmp x3.bmp 密集白噪点(359 个) ——
形态学调整/5.bmp x4.bmp 大量黑孔洞(74 个) 06 篇里的 x4

这批素材没有配套的 .hdev

文件夹名 形态学调整 本身就点明了用途 —— 它是用来调参数的。 因此本篇的定位不是"解读某个练习工程",而是补上 [[06 形态学运算]] 缺的那一环: 把"半径该取多大"从经验值升级成可标定的流程


上一步:形态学运算 · 返回:Halcon 学习地图 · 相关:图像预处理与滤波 · 算子速查表