ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Halcon例程精读:用GMM实现机器视觉分类的完整闭环

Halcon例程精读:用GMM实现机器视觉分类的完整闭环

做机器视觉这几年,Halcon自带例程就像我的工具箱底座。很多项目的第一版方案,都是先从一个例程改出来的。classify_citrus_fruits.hdev是其中之一,而且是为数不多我从头到尾逐行看过、也真在产线上改过的一个。它表面上是给柑橘做分类,核心其实是用GMM模型(高斯混合模型)做一次完整的落地闭环:从图像分割出候选果实、设计特征向量、用带标签样本训练分类器、再到按置信度输出分类结果。你可以不关心柑橘,但你大概绕不开"用GMM做视觉在线分类"这件事。

这个例程最值得拆的地方在于,它不是一个只调接口的玩具。它逼着你去回答几个实际问题:什么样的特征能区分两个品种或两级品质?每个类内部长得不均匀时,一个高斯够不够?训练集上很准的分类器,换到另一批光照下为什么就翻车?这些问题在Halcon里都有对应的算子、参数和调优路径。把这套逻辑吃透,你换到别的检测分类项目,比如零件分型、色差分级、外观缺陷筛选,思路是完全通用的。

我打算按照我实际读这个例程的顺序来讲。先看它要解决什么业务问题,再看GMM分类器为什么适合这类任务,然后一步步拆预处理和特征提取,接着进到训练参数里讲每个数字背后的含义,最后用分类测试和评估把整个流程串起来,再把我在产线上踩过的坑列出来。

1. 柑橘分类不是demo,是GMM分类流程的最小闭环

1.1 一条产线的真实需求被我压缩进了一个例程

水果分拣线到货之后,第一个问题永远是靠什么把"好果"和"次果"分开。人工分拣靠眼睛看颜色、大小、表面瑕疵,一人一小时能挑几百斤,但标准不统一,疲劳之后准确率往下掉,还要付持续的人工成本。机器视觉的替代思路本质上就是把人眼的判断翻译成数字:颜色不均匀就提取颜色均值方差,大小不一致就提取面积和直径,表面带疤就提取纹理和边缘特征。classify_citrus_fruits.hdev就是这个思路的标准示范。

在这个例程里,训练图像被分割成一个个独立的果实区域,每个区域计算一组特征,然后作为一条样本交给GMM分类器。测试阶段读入新图像,走一模一样的预处理链条,把每个区域的特征喂给训练好的模型,得到类别和置信度。整个过程让人看清了一件事:分类器本身只是很小的一部分,真正决定上限的是前面的图像处理和特征设计。

这个例程对应到工业现场,通常前面接一条传送带、一个触发相机,后面接吹气阀或机械臂。相机拍到一帧,逐果实分类,挡在置信度阈值之下的当作未知,合格的和不合格的走不同路径。Halcon里GMM推理一次大概在微秒到亚毫秒级,做在线分拣完全来得及。这也是为什么不管项目最终用什么模型,很多人还是会先拿这个例程跑通一把全流程。

1.2 例程的数据流向和关键算子链条

我习惯把这个例程拆成四个大环节:图像预处理、特征提取、模型训练、分类推理。每一环节对应一组Halcon算子,数据流向也清晰。

读取图像之后,先把原始RGB图按通道拆开,或者转成HSV等颜色空间。原因是柑橘的品类差异往往在颜色上最敏感,但RGB受光照影响很大,所以后续可以取色相饱和度的组合。分割阶段通常用阈值提取前景,再用形态学修复边缘、填充孔洞,接着用连通域分析把一个个果实变成独立区域。这个过程最终生成Region数组,每个Region就代表一个候选目标。

特征提取这里很关键。每个Region上会同时算形状特征和灰度特征。形状方面包括面积、圆度、最大直径、矩形度;灰度方面则可以取原始图R、G、B三个通道的均值和标准差,也可以取HSV下H、S、V的均值和方差。十来个数字拼成一个一维数组,就是GMM的一条特征向量。这个数组的维度、顺序、统计口径一旦确定,训练和测试必须完全一致,否则模型等于白训。

接下来进入模型环节。特征是数组,类别是整数标号,这两者一起交给训练接口。训练完成拿到一个句柄,测试时把新特征传进去,返回一个类别号和一个置信度值。整条链路上,预处理和特征提取的耗时通常远大于GMM推理本身,所以你要是发现实时性不够,先优化图像处理,而不是去换分类器。

2. 高斯混合模型到底在分类器的世界里站什么位置

2.1 混合高斯:把"一类"拆成"多个簇"来描述

GMM全称Gaussian Mixture Model,中文叫高斯混合模型。它假设每个类别的特征在空间里不是单一的团状分布,而是由多个高斯分布加权叠加而成。为什么要叠加?因为真实物体的特征经常是"多峰的"。比如同一品种的柑橘,有套袋生长的、有露天生长的,它们的大小分布和颜色分布可能差异明显,但都算同一个合格品类别。用一个高斯去拟合这种双峰数据,均值会被拉在两个峰中间,方差被撑得很大,分类边界自然就不准。

Halcon里的GMM实现,把这"多个高斯"拆成了多个聚类中心。每个聚类中心都带着自己的均值和协方差矩阵,训练时通过最大似然估计不断调整这些参数。你可以把每个聚类中心理解成"一个亚型":一类柑橘可能有"大果亚型"和"小果亚型",模型自动把它们分别描述,最终给整个类别一个混合密度。

分类的时候,待测特征向量会被代入每个类别的概率密度函数,算出来在这个类别下的密度值,比较哪一类最大就归哪一类。这个密度值本身不是概率,所以还要除以所有类别的密度之和,得到一个可以当置信度用的比例。Halcon的classify_class_gmm返回的Confidence就是这个归一化之后的结果。

2.2 协方差类型:球形、对角、全矩阵的本质差别

GMM里每个高斯分量都要描述特征之间的分散程度,这就是协方差矩阵。Halcon给了三种选择,分别是'spherical'、'diag'和'full',理解清楚这个选项,能省很多调参的功夫。

我整理了一张表,方便对比:

协方差类型自由度表达能力训练数据需求典型场景
spherical1个方差参数最弱,只能描述"各方向同宽"的团最低,小样本也能训特征维度高但样本少,求稳
diag每维1个方差参数中等,能描述各维度宽度不同每类几十条样本可起步大多数中等维度的分类任务
full完整协方差矩阵最强,能描述维度间的相关关系需要较多样本特征维度低、样本充足、精度优先

我用一个生活化的例子解释:特征只有两个维度,一个是重量,一个是颜色饱和度。spherical相当于假设所有柑橘的"重量-饱和度的散布"是一个圆形,宽度一致;diag允许这个散布是椭圆,但椭圆轴必须平行于坐标轴,也就是重量和饱和度互不影响;full允许椭圆带旋转角度,能够表达"越重的往往颜色越深"这种相关性。

选full不一定好。特征维度一高,协方差矩阵的参数会爆炸式增长,比如10维特征,full协方差矩阵要对每一类每个分量算55个独立参数。样本不够时模型会把噪声也学了去,训练集精度高,测试集直接崩。我自己的经验是,在小样本阶段优先spherical或diag,等样本量上来再试full,用交叉验证对比。

2.3 为什么偏轻量的分类场景里GMM常常压过KNN和SVM

很多人会问,现在深度学习这么流行,SVM也有很多现成库,为什么还要用GMM?这里有几个非常现实的原因。

第一,GMM输出置信度,而KNN和很多SVM实现输出的是距离或边缘值,转化成置信度很别扭。分拣产线最需要的就是置信度,因为你可以设一个阈值,把低置信度的目标单独挑出来人工复检,而不是盲目跟随机器的判断。

第二,GMM模型体积小、推理快。深度学习模型动辄几十上百MB,GMM往往只有几十KB。在Halcon里加载一个GMM分类器做一次推理,耗时可忽略不计,这对高速产线是实打实的好处。

第三,GMM支持增量更新。产线上不断有新的真实图像进来,你可以筛选出有价值的样本,继续往分类器里加,然后重新训练。不需要把整个数据集重来一遍,这种节奏很贴合工厂持续优化的习惯。

GMM的短板在于它的分布假设:特征如果严重不符合高斯分布,比如数据呈现明显的长尾或者多模态极强,它的上限会受限制。不过实践当中,只要特征设计得当,GMM在视觉分类任务里通常能打出接近深度学习的效果,同时换来的是低得多的工程成本和更快的迭代速度。

3. 预处理与特征提取:从一张彩图到一组可训练的数字

3.1 读图之后,颜色通道的选择就已经在影响结果

我见过太多人拿到图片直接三通道均值一顿算,然后抱怨分类器不稳定。其实从读图开始,每一步都在决定特征质量。RGB虽然是相机最直接的输出,但三个通道相关性极高,而且受光照强度和阴影影响大。同一颗柑橘,光照亮一点,R和G同时变大,直接拿RGB均值做特征,模型会学成"亮的都是好果"。

一种常用做法是转到HSV颜色空间。H是色相,S是饱和度,V是明度。柑橘的成熟度差异在H上体现得最稳定,S可以反映表皮颜色浓淡,V受光照支配,可以留作辅助。Halcon里转换颜色空间的算子很成熟,转完之后取通道也是一行命令的事。我通常不会把所有通道全用上,而是先做相关性分析,挑选区分度高的那几维。

除了颜色,灰度图上的局部对比度也很重要。对果实表面做灰度标准差统计,可以捕捉到表面纹理的粗糙程度。有一段时间我用三个颜色通道的方差组合,误以为特征是越多越好,后来发现有些特征之间高度相关,实际贡献等于冗余,还会拖累训练。

3.2 阈值分割和形态学修复:先找到"像水果"的区域

分割的目标是把果实区域从背景里抠出来。经典做法是把图像转成灰度或选一个对比度最强的通道,然后做阈值化。因为柑橘与背景的颜色差异通常较大,固定阈值在第一版里能跑通。但工业现场光照经常变,固定阈值大概率翻车,所以我后来更倾向动态阈值或基于颜色距离的分割。

分割后得到的是一个个二值掩膜,通常不是干净的。果柄、阴影、反光会造成孔洞和毛刺,这时候要用形态学算子处理。闭运算把细小的暗缝填上,开运算把边缘毛刺削掉,填充孔洞把果蒂附近的暗斑补平。这些操作的核大小很讲究,给柑橘用圆形结构元素,半径取果实直径的百分之几量级比较稳妥。核太大,相邻果实会被合并成一块;核太小,孔洞填不干净。

形态学之后是连通域分析。每个连通域对应一个候选目标,紧接着用面积、外接矩形长宽比等约束把明显不是果实的区域过滤掉。比如画面边缘被切了一半的果实、远处露出的背景杂物,都可以用select_shape直接排除,让后续特征提取只处理真正需要分类的目标。

3.3 特征设计:均值、方差和形状参数拼出特征向量

这个例程最精华的部分,就是教你如何把"一个区域"变成"一组有区分度的数字"。特征一般分三类。第一类是颜色特征,在每个颜色通道上取区域内的均值和方差,用来描述"这个果实整体是什么颜色、颜色均匀不均匀"。第二类是形状特征,包括面积、圆度、最大直径、最小外接矩形长宽比,用来描述"这个果实长什么样子"。第三类是纹理特征,可以用灰度共生矩阵或者频域能量来描述表面细纹、斑点,通常用于识别表面缺陷。

特征向量不是随便拼的。每条样本的数值顺序必须固定,维度要提前定好。我建议先把想到的特征全部算出来,存成一张特征表,然后做一次分类对比,剔除区分度差和高度相关的特征。比如面积和最大直径通常相关度很高,留一个就够了。这个例程里特征的维度本身不高,但思路非常典型:宁可少而稳定,不要多而杂乱。

特征值本身还涉及量纲问题。面积可能是几千,平均灰度可能是两百,方差可能是几十,这几类数值直接丢进GMM,距离计算会被量纲大的维度主导。Halcon的GMM接口在设计时就考虑了这一点,它可以在创建分类器时指定预处理方式为归一化或主成分降维,这个机制我会在训练章节详细讲。

4. 训练阶段:create_class_gmm里每个数字都在替你决策

4.1 六个关键参数的语义与推荐起点

创建GMM分类器是训练环节的起点。这个接口有多个参数,每个参数都相当于一次建模决策。我拆开来一个个说。

第一个是特征维度,必须和你在特征提取阶段拼出来的向量长度完全一致。维度填错,训练直接报错或者静默训练出垃圾模型。第二个是类别数,这个好理解,坑在于类别标号必须从0开始连续排列,不能出现"类别1和类别2中间跳个7"。

第三个是每个类别的混合分量数,这是最容易忽略的参数。分量数相当于允许每个类别内部有几个子簇。对柑橘分类,3个起步我认为是合理的选择。分量数太少,模型表达不了类内差异;分量数太多,小样本下容易过拟合。我调参时一般先固定3,然后逐个试5、7甚至10,看测试精度曲线,找到一个"涨不动了"的拐点就收手。

第四个是协方差类型,上一章节已经对比过三种选择的差别。第五个是预处理方式,可选'none'、'normalize'和'principal_components'。个人强烈建议对视觉特征用'normalize',因为特征量纲差异太大。第六个是随机种子,固定住它能让实验结果可复现,调试阶段务必固定同一个值。

我用一小段伪代码展示创建和训练的基本结构,具体书写请以你本机版本的帮助文档为准:

* 创建分类器:7维特征,2个类别,每类3个高斯分量,对角协方差,归一化预处理 create_class_gmm (7, 2, 3, 'diag', 'normalize', 7, 100, 1e-4, 42, ClassGMMHandle) * 添加样本:FeatureVector是数组,ClassID是类别标号 add_sample_class_gmm (ClassGMMHandle, FeatureVector, 0) add_sample_class_gmm (ClassGMMHandle, FeatureVector2, 1) * 训练 train_class_gmm (ClassGMMHandle, 100, 1e-4, 'default')

4.2 样本添加与训练迭代的正确姿势

训练样本的组织方式,直接影响模型质量。我的经验是:一边读图像、分割区域、提取特征,一边把特征向量逐个加入分类器。不是每张图都加全部区域,而是要经过筛选。那些分割残缺、边缘被截断、面积明显异常的区域,特征值本身就是坏的,加进去只会污染模型。

样本平衡同样重要。两类样本如果数量悬殊,比如合格品有500条、次品只有50条,GMM会把合格品的分布描述得更细,分类边界会偏向次品一侧。最简单的处理是多拍次品图像,或者对次品区域做小范围的平移旋转、亮度扰动,生成合理的新样本,补充少数类。这样比盲目收集一堆合格品更有效。

训练迭代的过程对使用者来说是一个黑盒,但有几个信号值得关注。一个是训练是否收敛,Halcon会依据收敛阈值判断模型参数更新是否足够小。如果训练集精度很好但测试精度差,常见原因大概率是分量数太多或者特征维度太高,而不是迭代次数不够。另一个是训练耗时,GMM训练一般很快,如果跑出分钟级,先查一下样本数和特征维度的组合是否合理。

训练完之后,一定要把分类器存成文件。Halcon有专门的保存加载接口,生成的模型文件很小,方便挪到不同机器上部署。每次重新训练的样本增补,都应该形成版本概念,保留历史模型,方便回归对比。

4.3 特征归一化:被很多人跳过却决定成败的一步

我看到不少初学者自己实现了GMM训练代码,绕开了Halcon的预处理参数,结果怎么调都调不好。原因多半就是没做归一化。设想7维特征里,面积数值在5000上下,R通道方差在50上下,两者直接参与距离计算时,面积的变化量会轻松盖过颜色的变化量。模型看起来在训练,实际上等于只学了面积一维。

Halcon的'normalize'预处理实际上就是按每个维度的均值和标准差做标准化,让所有特征都在相近的尺度上参与计算。如果改用'principal_components',还会顺带做PCA降维,把原来7维压成前几个主成分,可以抑制冗余维度的噪声。在特征数量不多的时候,我优先用'normalize',保留原始特征的物理含义,方便排查问题。特征超过十几维并且相关性较高时,再考虑PCA。

这里还有个隐蔽的坑:归一化参数是在训练阶段从样本统计出来的。测试阶段的新样本,也应该使用训练阶段统计的同一套均值方差去变换,而不是自己在测试端重新算一套。Halcon内部会把归一化参数保存在模型句柄里,推理时自动应用,这也是我推荐直接用内置机制、不要手工预处理的原因。

5. 分类与评估:置信度、混淆矩阵和可视化的三重校验

5.1 classify_class_gmm返回的不只是一个类别号

模型训练完,测试第一件事就是跑推理。Halcon里分类结果的ClassID是一个整数,但真正需要关心的是同时返回的Confidence。Confidence表示"这个样本被分到当前类别的把握有多大",通常取值接近1,类别之间很难分时会出现接近0.5的低置信度。

实际产线应用时,我不会直接采用ClassID,而是设置一个置信度阈值,比如0.9。低于阈值的样本一律标成"不确定",送回人工复检或者单独通道暂存。这么做立竿见影的两个好处是:人工复检量可控,系统自我纠错的空间也更多,不会因为一个边界样本把合格品打到废品里。

在批量评估模型时,我会遍历所有测试样本,记录每组真实类别、预测类别和置信度。根据记录把低置信度样本翻出来逐一看原图,找出模型"犹豫"的区域长什么样。很多时候你会发现犹豫点集中在光照偏暗、反光强烈、或者成熟度介于两者之间的果实上,这时候你便清楚该去补哪种样本了。

5.2 混淆矩阵怎么算,怎么读

混淆矩阵是评估分类器最直观的工具。以两类为例,矩阵有四个格子:真实合格预测合格、真实合格预测次品、真实次品预测合格、真实次品预测次品。对角线上的数字越大越好,非对角线是具体的错误模式。

只看总体精度很容易被数据掩盖。举个例子,合格品占95%,哪怕分类器把所有果实都判成合格,总体精度也有95%,看起来挺好看,但实际上次品全部被放过,产线根本不能用。所以必须单独看每个类别的召回率,尤其关注"次品被误判为合格"这种错分代价最高的组合。Halcon里自己写混淆矩阵不复杂,一行代码统计出各类数量,然后手工算比率就行。

读矩阵的要点是分辨错误的偏向性。如果次品经常被分到合格,说明特征里缺少能表达缺陷的信息,优先去补纹理或颜色均匀度特征。如果两类来回错,说明特征重叠严重,优先考虑增加特征维度或调整GMM的分量数。曾经有一次我调试良久,后来发现是样本标号给错了,所以复盘时先把标号校验放在第一位。

5.3 可视化不是装饰品,是定位错分的快捷方式

分类结果直接画在图像上,人眼一看就能发现问题。在Halcon里,每个区域分类后可以按类别着色,把区域叠加回原图显示。这样一眼就能看出哪些果实被标成红色、哪些标成绿色,以及它们各自分布在图像的什么位置。

可视化还有一个好处,是能暴露预处理阶段的隐藏错误。比如说图像右下角一堆被误分的区域,放大后发现其实是传送带边缘的污渍被误分割成了果实,这就不是分类器的问题,而是前面过滤条件没做够。所以排查错分问题时,我通常按顺序查:先看分割对不对,再看特征提取的区域是不是目标本身,最后才怀疑分类器参数。

可视化对特征分析也很有帮助。把每类样本的特征投影到两个主方向上,画出散点,能直观看到类别分布的重叠程度。有时候你以为自己的特征选得很好,画出来才发现两类完全混在一起,那后面的调参基本是白费功夫。先可视化,再决定要不要重新设计特征,这条工作顺序能节省大量时间。

6. 我在实际产线上复现这个例程踩过的坑

6.1 光照一跳,固定阈值就直接失效

之前在某条果品分拣线上调这个例程,上午调试好一切正常,下午太阳角度变了,透明棚顶投下一片阴影,分割区域一夜之间变得支离破碎,GMM输入的样本全是残缺区域,分类结果自然全乱。那次之后我彻底改了思路:凡是白光环境下做颜色分类,必须把光照当作一个变量来设计处理方案。

解决思路有几个方向。硬件上给相机加遮光罩、用恒定光源,是最省事的办法,能避免绝大多数光照波动。算法上把固定阈值改成动态阈值,或者把基于绝对颜色值的特征改成颜色比例特征,比如R分量占整像素亮度的比例,能在一定程度上抵消光照变化。我最终采用了频谱归一化加动态分割的组合方案,换产线设备或者换光照环境时,不再每次都被迫重调整个流程。

这里要说明一点:不要指望GMM帮你自动消除光照影响,它是分类器,不是图像归一化工具。光照鲁棒性问题必须在预处理阶段解决,这也是我在前面反复强调预处理是整个分类流程重心的原因。

6.2 样本太少时,GMM会比想象中更容易"自我欺骗"

有一次我只收集了每类30条样本就开始训练,训练集精度接近98%,当时以为模型很好了。结果到了现场,测试集精度只有70%左右。回头看原因:样本少的时候,GMM把每个训练样本所在的位置都描述成了高概率区,实际来了一个稍微偏离训练分布的样本,置信度就急剧下降。

GMM对于训练样本覆盖范围非常敏感。少样本情况下,分量数越多,过拟合越严重。后来我坚持一个经验规则:每类样本至少要有分量数的几倍以上才有意义。比如每类3个分量,每类样本最好不少于150条。样本收集阶段宁可多拍几种光照、角度、成熟度的图像,不要只盯着好拍的样本拍。

还有一种做法值得提,就是数据增强。对已有的果实区域做旋转、缩放、平移,再适当加一点高斯噪声或者亮度扰动,能有效扩大训练覆盖范围。Halcon里做这些变换都很方便,改出一个增强脚本放在训练前跑一遍就可以了。

6.3 换场景时,保留分类器结构只重训样本是否可行

项目推进过程中经常遇到这种情况:柑橘换了一季,或者客户改了一种包装,果实颜色分布整体变了。很多人会重新开始造轮子,从头练一个分类器,其实GMM的结构往往可以保留,只需要重新组织样本训练。

我的一般做法是:先从历史模型里读取结构参数,看看原来的特征维度、类别数、分量数是多少,然后结合新场景判断是否需要调整。如果只是整体颜色偏移,特征维度和分量数可以不动,直接用新旧样本混合重训。如果新场景引入了新的果实形态,比如圆形果变成椭圆形果,那就要增加分量数或者增加特征维度重新建模。

关键还是样本管理。每次现场采集的新样本都单独存目录,标注好拍摄条件、日期、品类来源,训练前把这些信息和旧样本一起做个分布对比。散点图揭示新旧样本严重分离的时候,说明场景切换跨度大,这时候直接合并重训不一定是最好的办法,更稳妥的是先确认特征空间里的整体偏移方向,再决定是重训一个模型还是保留旧模型加一个专门校正层。

聊到这里,这个例程能挖的东西基本讲完了。如果让我给后来者一个最直接的建议:别急着调GMM参数,先拿着你的真实图像把预处理这关做扎实,分割稳了、特征口径定了,再进创建分类器调参,你会发现自己省下的是数周的返工。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进