25  形状特征

经过前面几章,我们手里已经有了不少”把图像变成数”的工具:blob 分析给出面积与重心(章节 23),轮廓分析给出周长与圆度,边缘检测给出位置与方向。但产线上的问题远不止这些。一条蜿蜒的 PCB 走线,它的”长度”和”宽度”该怎么定义?两块平均亮度完全相同的表面,一块是均匀的合格涂层、一块是布满麻点的次品,用什么数字区分它们?一块板卡要做粗定位,去哪里找几个”既稀疏又稳定”的参考点?本章把三件各自独立、但都回答”如何把形状压缩成可判定的数”的工具收进同一个工具箱:骨架(skeleton)给细长结构提取中轴,灰度与纹理特征给表面外观提取统计指纹,角点(corner)给几何轮廓提取地标。图 25.1 是骨架实验的测试场景——一条 Y 形分叉走线、一条 L 形走线和一个带方孔的团块,都是 5 个像素宽或包含细长/块状结构的典型样本。

图 25.1: 骨架实验的二值测试场景:左上为 Y 形分叉走线(5 px 宽),左下为 L 形走线(5 px 宽),右上为带方孔的团块。

25.1 骨架

想象在二值形状内部放一个尽可能大的圆,让它在形状里滚动且始终与边界相切——所有这些最大内切圆圆心的轨迹,就是形状的中轴(medial axis),把每个圆心再附上对应的内切圆半径,就得到中轴变换(medial axis transform)。直觉上,中轴是形状的”脊梁”:对一条等宽走线,中轴恰好是沿走线方向的中心线,而内切圆半径恰好是半宽。这正是细长结构最自然的描述方式——把二维的”一片像素”还原成一维的”一条线加宽度”。

中轴对边界噪声很敏感:边界上一个小凸起就会让中轴长出一条多余的分枝。工程实现(包括细化算法)通常配合 章节 8 的开闭运算先平滑边界,或对短分枝做剪枝(pruning)。

工程上计算骨架最常用的是细化(thinning)算法:迭代地从形状边界剥除像素,但保证每次删除都不改变连通性——既不把一个形状剥成两段,也不把孔剥没了——直到只剩一条单像素宽的中心线为止。它属于 章节 8 介绍的形态学算法家族,输出是中轴的离散近似。

图 25.1 运行 SciVision 的骨架算法,结果见 图 25.2。三个形状产出 3 条骨架轮廓;整个场景的前景共 17729 个像素,骨架只剩 738 个像素——24.0 倍的压缩,而细长结构的拓扑与走向信息一点没丢。注意右上角带孔团块的骨架:它围绕方孔形成一个闭环,这正是细化”保拓扑”的体现——形状有一个孔,骨架就有一个环,断不开也缩不掉。

图 25.2: 骨架结果:原形状压暗显示,单像素宽的骨架以白色叠加。Y 形走线的骨架是带一个分叉的三条枝,带孔团块的骨架绕孔形成闭环。

骨架是一条结构化的曲线,它身上的特征点本身就有工程含义:端点(endpoint)是走线的起止,交叉点(junction)是分叉处。对 Y 形走线的骨架做特征点提取,得到 3 个端点、1 个交叉点,与真值完全一致;端点位于 (70, 51)、(120, 190)、(170, 51),交叉点位于 (120, 119),与几何真值 (120, 120) 仅差 1 个像素。对走线检查来说,这两类点就是”线路图”:端点数对不上意味着断路,交叉点数对不上意味着桥连。

骨架最重要的应用范式是一个简单的除法:

\[ \bar{w} = \frac{A}{L}, \]

其中 \(A\) 是走线的前景面积,\(L\) 是骨架长度(像素数),\(\bar{w}\) 即走线的平均宽度。对场景中的 L 形走线(设计宽度恰为 5.00 px)实测:面积 1313 px,骨架长 257 px,平均宽度 \(1313/257 = 5.11\) px。误差从哪来?骨架长度按 4 连通像素计数,在 L 形的拐角处,离散计数会比几何弧长略短(拐角被”抄了近路”),分母偏小自然让宽度略偏大。2% 的系统偏差对趋势监控完全够用,但若要绝对宽度合格判定,应当用弧长(对角步长按 \(\sqrt{2}\) 计)或卡尺测量校准。诚实地报告这类离散化偏差,比假装”实测=真值”更有工程价值。

骨架的典型用途可以列成一张清单:

  • 线宽检测:面积/骨架长得平均宽度,沿骨架逐点取内切圆半径得宽度序列;
  • 断路与桥连检测:数端点与交叉点,与设计网表比对;
  • 路径规划与长度测量:蜿蜒走线、裂纹、纤维的”真实长度”就是骨架长度。

25.2 灰度与纹理特征

第二组工具回答的问题是:一块区域的表面外观怎么变成数?最先想到的是一阶统计——把区域内所有像素的灰度直方图算出来,取均值、标准差、熵(entropy)。它们便宜、直观,但有一个根本盲区:一阶统计只看”有哪些灰度值”,完全不看”这些灰度值怎么排列”

图 25.3 的四个 100×100 纹理区就是为暴露这个盲区设计的:均匀灰度 140、高对比棋盘(40/240 相间)、水平渐变(40→240)、高斯噪声(\(\mu=140\), \(\sigma=20\))——四个区的均值全部约等于 140(实测 140.00 / 139.84 / 139.51 / 139.76)。靠均值,四个区是同一个区。标准差能拉开一些差距(0 / 100.00 / 58.18 / 20.04),但请看熵:渐变区 6.64、噪声区 6.36,几乎一样——一阶统计眼里,“平滑过渡的渐变”与”乱成一团的噪声”难分彼此,因为两者的直方图都摊得很开。

图 25.3: 四个均值约 140 的纹理区(自左至右):均匀 140、高对比棋盘、水平渐变、高斯噪声 \(\sigma=20\)。一阶统计无法完全区分它们,区分全靠二阶统计。

要看”排列”,就要看像素对灰度共生矩阵(gray-level co-occurrence matrix, GLCM)统计的是:在给定方向与距离上(最常用:水平相邻),灰度为 \(i\) 的像素旁边出现灰度为 \(j\) 的像素的频率 \(P(i,j)\)——它是相邻像素对灰度的联合分布。把灰度量化成少数等级(本实验为 16 级)后,这个矩阵不大,但纹理的”排列规律”全写在里面:均匀纹理的能量集中在对角线一个格子里,渐变纹理集中在对角线附近的窄带上,噪声纹理摊满整个矩阵。从 \(P(i,j)\) 提炼出四个常用标量:

  • 能量(energy) \(\sum_{i,j} P(i,j)^2\):分布越集中(纹理越单一、越规则)值越大,均匀区为 1;
  • 对比度(contrast) \(\sum_{i,j} (i-j)^2 P(i,j)\):相邻像素灰度差越大值越大,度量”局部反差”;
  • 相关性(correlation)\(i\)\(j\) 的统计相关系数,相邻像素”知道彼此”的程度;
  • 同质性(homogeneity) \(\sum_{i,j} \frac{P(i,j)}{1+|i-j|}\):质量越靠近对角线值越大,度量”局部平滑”。

GLCM 是有方向的:本实验统计水平相邻对。对各向同性的纹理(噪声、均匀),方向无关紧要;对各向异性的纹理(划痕、拉丝、本例的水平渐变),换个方向数字会完全不同——垂直方向统计渐变区会得到对比度 0。工程上要么按缺陷的已知走向选方向,要么取四个方向的均值。

四个区的实测全表如下(均值/标准差为自算,原因见 小节 25.4;GLCM 取水平方向、16 灰度级):

区域 均值 标准差 熵(一阶) GLCM 能量 对比度 相关性 同质性
均匀 140 140.00 0.00 0.00 1.000 0.000 0.000 1.000
棋盘 139.84 100.00 1.00 0.393 20.485 0.758 0.879
渐变 139.51 58.18 6.64 0.061 0.131 0.996 0.934
噪声 \(\sigma=20\) 139.76 20.04 6.36 0.048 3.350 −0.015 0.467

逐行读一遍”为什么是这个签名”。均匀区是退化情形:只有一个灰度,能量 1、同质 1、对比 0,相关性因方差为零而无定义(SDK 返回 0)。棋盘区的签名是高对比(20.48,全表最高两个数量级):8 px 的格子让约 1/8 的水平相邻对横跨黑白边界,每跨一次贡献巨大的 \((i-j)^2\);其余 7/8 的像素对同格同灰度,所以相关性仍有 0.758——“反差剧烈但排列有规律”。渐变区的签名是高相关(0.996)配低对比(0.131):水平相邻像素只差约 2 个灰度级,彼此几乎完全可预测——“变化大但变化得有秩序”。噪声区的签名是低同质(0.467,全表最低)加零相关(−0.015):相邻像素相互独立,谁也预测不了谁。四个均值相同的区,在 GLCM 的四个量上各占一个角,清晰可分——这就是二阶统计的价值:它把”排列”变成了数。

25.3 角点

第三组工具找的是几何上的地标。角点之所以特殊,可以用一个小窗口的”自相似性”来理解:把窗口沿任意方向挪一小步,看窗口内容变化多大。在平坦区域,往哪儿挪都没变化;在边缘上,沿边缘挪没变化、垂直于边缘挪变化大——只有一个方向”敏感”;而在角点处,往任何方向挪内容都剧烈变化——两个独立方向上都有强梯度。Harris 角点检测器 (Harris 和 Stephens 1988) 把这个直觉写成窗口内梯度结构矩阵的特征值判据:两个特征值都大,才是角。

实验场景是一块 L 形六边形板(6 个直角,其中 1 个是凹角)加一个干扰件——圆角半径 25 px 的圆角矩形,它没有任何尖角。用 Harris 方法、筛选比例 0.30 提取角点,结果见 图 25.4 (a):恰好检出 6 个角点,与真值一一对应,每个角点距几何真值都是 1.41 px——一个一致的 \((\pm 1, \pm 1)\) 偏置。偏置如此整齐并非巧合:Harris 响应在梯度结构最丰富处达到峰值,而对一个由 5×5 邻域(blockSize=5)评估的直角,这个峰值稳定地落在角的内侧一个对角像素上。系统性偏置可以标定补偿,随机抖动才是精度杀手——这 1.41 px 属于前者。

(a) filterRatio = 0.30:恰好 6 个角点,十字精确落在 L 形板的 6 个直角上(含凹角),圆角矩形上无检出
(b) filterRatio = 0.02:14 个”角点”,其中 8 个落在圆角矩形的四个圆角上
图 25.4: Harris 角点提取的阈值敏感性。强筛选只留真正的尖角;弱筛选让平缓的圆角也产生足够的 Harris 响应而被检出。

把筛选比例从 0.30 降到 0.02,检出数从 6 暴涨到 14(图 25.4 (b)),其中 8 个落在圆角矩形的四个圆角上——每个圆角被检出 2 个”角点”。这揭示了一个常被忽略的事实:角点”强度”是连续量,“是不是角”是工程定义。圆角处曲率不为零,两个方向上确实都有梯度,Harris 响应确实大于零——只是比直角弱。阈值定在哪里,“角”的边界就在哪里。这与 章节 13 的阈值整定是同一个哲学:算法输出的是响应图,“判定”永远是人替算法做的。整定方法也相同——用含已知干扰件的样本扫描阈值,找出”全检出真角、零检出圆角”的安全区间。

角点找到之后做什么?三类典型用途:用 2~3 个稳定角点做工件的粗定位(比模板匹配快得多);标定板的方格交叉点本质就是角点,是 相机标定 的输入;以及作为特征匹配的兴趣点——章节 18 的特征匹配内部第一步正是检测这类”往哪儿挪都变化”的点。

25.4 SciVision 实现

骨架由 SCIMV::SciSvSkeleton 提供,两个接口配合使用:

SCIMV::SciSvSkeleton sk;
SciContourArray arr;
sk.Skeleton(src, roi, &arr);          // 细化 -> 骨架轮廓数组
SciPointArray ends, juncs;
sk.ExtractJunctions(arr[i], &ends, &juncs);  // 单条轮廓的端点/交叉点

Skeleton 把 ROI 内每个连通形状细化为骨架,输出为轮廓数组——注意它在分叉处不拆分:Y 形走线的整个骨架是一条轮廓,而不是三条枝。这是刻意的工程设计:拆不拆、在哪儿拆,由 ExtractJunctions 决定——它接收单条骨架轮廓,输出端点数组与交叉点数组,调用方拿着交叉点再按需切分。“细化归细化、解析归解析”的两段式让一个 API 同时服务于只要长度的用户和要完整拓扑的用户。

灰度特征由 SCIMV::SciSvGreyFeature 提供:

SCIMV::SciSvGreyFeature gf;
double ent, aniso;
gf.GetEntropyGreyFeature(src, roi, &ent, &aniso);   // 一阶熵 + 各向异性
double ge, en, corr, homog, contr;
gf.GetGLCMFeature(src, roi,
    SCI_SV_GREYFEATURE_GLCM_DIRECTION_HORIZONTAL,    // 方向:水平相邻对
    16,                                              // 灰度量化级数
    &ge, &en, &corr, &homog, &contr);                // GLCM 熵/能量/相关/同质/对比

GetGLCMFeaturegreyClass=16 是量化级数:级数越多矩阵越大、对噪声越敏感,16 是常用折中。方向参数的意义见 小节 25.2 的边注。提醒一句旧坑:SciSvHistogram::CaculateHiststdValue 是直方图 bin 计数的标准差而不是灰度标准差,所以本章表格中的均值与标准差按定义自算。

角点由 SCIMV::SciSvCornerExtraction 提供:

SCIMV::SciSvCornerExtraction ce;
SciPointArray pts;
ce.ExtractCorners(src, roi,
    0,        // method: 0 = Harris
    5,        // blockSize: 梯度结构矩阵的评估邻域
    50,       // maxnumCorners: 数量上限
    0.30,     // filterRatio: 响应筛选比例(本章敏感性实验的主角)
    10,       // minDistance: 角点间最小距离,抑制同一角的重复检出
    &pts);

minDistance=10 解释了为什么弱阈值下每个圆角”只”检出 2 个点——没有它,整段圆弧都会冒出检出。生成本章全部图像与数据的完整工程位于 code/shape_features/

工业案例:柔性电路断路检测

某 FPC(柔性电路板)产线在蚀刻后检查走线健康度。最初方案用 blob 连通性分析:走线断成两个 blob 即报断路。它只能抓”完全断开”,而蚀刻过度造成的细颈(走线局部变细但未断)全部漏检——细颈件在弯折测试中批量失效。改进方案引入骨架:对每条走线提取骨架,沿骨架做滑动窗口的”局部面积 / 局部骨架长”,得到一条沿线宽度序列。完好走线的宽度序列平直,细颈表现为序列上一个清晰的谷值——设一条宽度下限,半断的走线在断之前就被拦下。教训:骨架把”二维形状健康度”降成了”一维信号”,而一维信号上,经典的阈值、趋势、SPC 工具全部立即可用——降维不是丢信息,是把信息摆到老工具够得着的地方

25.5 小结

  • 骨架是细长结构的中轴:细化算法迭代剥边界、保拓扑(孔变闭环),把 17729 px 的前景压成 738 px 的中心线(24 倍)而不丢走向与拓扑;端点/交叉点直接对应断路/桥连判据。
  • 宽度 = 面积 / 骨架长是骨架最实用的范式(实测 5.11 vs 真值 5.00);像素计数在拐角低估弧长带来约 2% 系统偏差,趋势监控可忽略,绝对判定需校准。
  • 一阶统计看不见”排列”:四个均值同为 140 的纹理区靠均值与熵无法完全区分;GLCM 把相邻像素对的联合分布变成能量/对比/相关/同质四个数,四区各得一个唯一签名(棋盘=高对比、渐变=高相关低对比、噪声=低同质零相关)。
  • 角点强度是连续量,“是不是角”是工程定义:filterRatio=0.30 恰好检出 6 角、一致偏置 1.41 px(可标定);放宽到 0.02 则圆角干扰件贡献 8 个误检——阈值整定必须用含干扰件的样本扫描。
  • 三件工具的共同点是压缩:把一片像素压成一条线、一个分布、几个点,后续的判定逻辑才有抓手。

中轴(medial axis)的原始定义出自 Blum 提出的形状描述变换 (Blum 1967),它正是本章骨架的理论母体;GLCM 纹理特征的经典出处是 Haralick 等人关于纹理图像分类的奠基论文 (Haralick, Shanmugam, 和 Dinstein 1973);与 Harris 角点 (Harris 和 Stephens 1988) 同族、并被广泛用于工件粗定位的 Shi–Tomasi 角点判据见 (Shi 和 Tomasi 1994);位姿无关的形状描述子方面,Hu 的矩不变量 (Hu 1962) 是经久不衰的经典。骨架与中轴变换的严格定义、GLCM 纹理特征的系统工程讨论,可进一步阅读 Steger 等人的著作 (Steger, Ulrich, 和 Wiedemann 2018)