分类器的作用是将目标对象指定为已给的多个类别中的一个。例如一张图中包含了几种形状不同的物体,我们可以通过分别对每一种形状对应样本的区域的某些特征值,并告诉它每一类形状的这些样本属于哪种类别来进行训练,完成训练后,再输入待判断物体的形状区域的相应特征值,这时分类器便可以根据之前的训练成果,来对输入区域进行判断,并给出判断的结果。
这里,我对区域进行了加粗,是因为需要大家知道,分类器所需要的输入是区域的特征值,而不是单纯的原始图像,无论是进行训练,还是进行识别,它的输入始终都是区域的特征值。因此,进行分类器操作之前,对原始图像进行一些预处理以及图像分割来得到我们需要输入的目标区域是必须的。具体如何分割,则需要具体场景具体分析。
Halcon提供了多种分类器,比较重要的分类器有
(1)基于神经网络,特别是多层感知器的MLP分类器
(2)基于支持向量机的SVM分类器
(3)基于高斯混合模型的GMM分类器
(4)基于k近邻的k-NN分类器
(1) MLP分类器
MLP分类器分类速度快,但训练速度慢,对内存的要求低,支持多维特征空间,特别适合需要快速分类并且支持离线训练的场景,但不支持缺陷检测。
(2)SVM分类器
分类检测的速度快,当向量维度低时速度最快,但比MLP分类器慢,尽量训练速度比MLP分类器快得多。其对内存的占用取决于样本的数量,如果有大量的样本,如字符库这样的样本需要训练,分类器将变得非常庞大。
(3) GMM分类器
训练速度和检测速度都很快,特别是类别较少时速度特别快,支持异常检测,但不适用于高维度特征检测。
(4)k-NN分类器
训练速度非常快,分类速度比MLP分类器慢,适合缺陷检测和多维度特征分类,但对内存的需求较高。
多数情况下,上面四种分类器都是可以胜任需求的。
(1)准备样本图片
(2)对样本图片进行处理(如二值化、图像分割等),提取出目标区域
(3)分析目标特征,选取可以用来将不同类别区分开来的几种特征值作为特征向量以训练分类器
(2)创建分类器
(3)训练分类器
(4)检测待测目标对象,分类器根据训练得到的类别的边界条件判断检测对象属于哪个分类
(5)清除分类器,释放内存
总的来说,针对不同的分类任务,需要选择一组合适的特征和合适的分类器,以及合适的训练样本。
create_class_mlp( : : NumInput, NumHidden, NumOutput, OutputFunction, Preprocessing, NumComponents, RandSeed : MLPHandle)
该算子除了MLPHandle是输出参数以外,其余都是输入参数,介绍如下:
NumInput:用于训练和分类的特征空间的维度数。默认是20,也可以更大甚至500。
NumHidden:表示神经网络中隐藏层的单元数量。改值会明显地影响分类结果,需要谨慎设置。其取值范围与NumInput及NumOutput相似。其值越小,超平面就越简单,有时能够得到更好的效果;如果取值过大,反而会有过拟合的可能,比如可能将噪声点用于训练分类器的边界,这时如果待测对象不包括这些非关键的点,则可能分类失败。
NumOutput:表示输出的分类数量,比如对两种物体进行分类,即为2,对三种物体分类即为3。
OutputFunction:表示神经网络的输出单元使用的函数,可选的有softmax、logistic、linear。绝大多数情况下,输出函数都可以使用softmax。logistic用于处理多个逻辑独立的属性的分类问题,非常少见。linear用于最小二乘法,而不是用于分类,可以忽略。
Preprocessing:该参数表示在训练与分类前对特征向量进行预处理。预处理可以加快训练或者分类的速度,有时也有助于提升分类的准确度。其可选方法有四种。选择none时不进行任何预处理,大多数情况下,选择normalization,表示将特征向量归一化为0至1的数。这种处理既不会改变特征向量的长度,又能有效提升速度,因此是预处理的首选。如果特征向量是高度相关的,可以选择principal_components
NumComponents:在Preprocessing选择canonical_variates或者principal_components时使用。因为这两种预处理方法可能会减少特征空间的维度,NumComponents就表示减少后的特征向量的维度。
RandSeed:用于初始化MLP中的权重值,该值表示随机数种子数,存储在RandSeed中。
MLPHandle:唯一的输出参数。这是分类器的句柄,用于后续对分类器进行各种操作,如调用、修改、删除等。
add_sample_class_mlp( : : MLPHandle, Features, Target : )
MLPHandle:表示分类器的句柄
Feaures:表示输入样本的特征向量。该向量类似于一个数组,其中的每一个值表示一种特征,其维度数应当与create_class_mlp中的NumInput的值相同。
Target:设置类别的标识号。它可以设置的值的个数与create_class_mlp中的NumOutput的值相同。比如有三种类型的物体需要进行图像分类,那么Target对应每种形状时就可以分别设置为0、1、2。当然也可以设置为其他数字。Target表示类别的标识ID,每一个Traget即代表是一种类别。
train_class_mlp( : : MLPHandle, MaxIterations, WeightTolerance, ErrorTolerance : Error, ErrorLog)
MaxIterations:表示优化算法的最大迭代次数,默认为200。多数情况下,算法会在迭代了MaxIterations次后结束。100~200的次数一般都是足够的。
WeightTolerance:表示优化算法的两次迭代之间的权重差异阈值。如果权重小于该值,优化算法将终止。一般默认为1,且无需改变。
ErrorTolerance:表示优化算法的两次迭代之间的误差均值的阈值。均值小于该值,算法也会终止。一般默认为0.01,且无需改变。
Error:表示MLP在最佳权重下的训练数据的平均误差。
ErrorLog:该参数将MLP的训练数据的平均误差作为迭代次数返回。它用于判断是否应该使用不同的RandSeed对同样的样本进行二次训练。合适的情况下,ErrorLog应当是在开始时急剧下降,后越来越平稳。如果是从头到尾其值都走势陡峭,则通常应重新设置参数重新训练。
evaluate_class_mlp( : : MLPHandle, Features : Result)
Features:表示输入的待评估的特征向量。这里的特征向量的维度与结构应当与add_sample_class_mlp中的训练样本的特征向量相同。
Result:表示由MLP评估后输出的结果。
classify_class_mlp( : : MLPHandle, Features, Num : Class, Confidence)
完成创建与训练后的分类器便可以对未知对象进行分类了。
Features:输入的待评估的特征向量。
Num:想要寻找的最佳分类的数量。如设置为1则只返回概率最大的那个类别,设置为2则将概率前两名的两个类别都返回。
class:输出所寻找的类别ID,若Num为1,则返回一个ID,Num为2则返回两个,每个值都为add_sample_class_mlp中Target设置的值之一,即代表每个类别的ID。
clear_class_mlp (MLPHandle)
根据分类器的句柄,将完成分类任务后不需要在使用的分类器清除释放。同时释放MLP分类器所占用的内存资源。
这里使用的是HALCON的一个相关例程,是一个非常标准的图像分类模板,笔者在注释中对本例程进行了详细的讲解,方便初学者学习。
另外,笔者对例程进行了稍微的改动。主要是原始例程编写了几个本地函数(即自定义函数,它使用已存在算子对多个步骤进行了封装,对于简化代码与提高代码模块化有很大作用),虽然合理但是不方便初学者查阅理解,因此在下述代码中笔者用原始算子替换了原来的本地函数,本地函数被注释。这样方便每一个步骤的精确讲解,也方便大家对程序进行改动移植而不出错。
本地函数包括segment,add_samples,get_features,classify,笔者已注释,读者可自行删除。
dev_update_off ()
dev_close_window ()
dev_open_window (0, 0, 640, 480, 'black', WindowHandle)
set_display_font (WindowHandle, 16, 'mono', 'true', 'false')
dev_set_colored (6)
dev_set_draw ('margin')
dev_set_line_width (3)
*
*一、创建mlp分类器。
*用于训练和分类的特征向量维度数为6,神经网络中隐藏层为5,共需要对3种类别进行分类,其他参数参考文章2.1 MLP分类器的关键算子
create_class_mlp (6, 5, 3, 'softmax', 'normalization', 3, 42, MLPHandle)
*
*二、添加样本到分类器
*循环读取每一个图片文件,对每一个图片进行二值化图像分割,获取每个区域的特征向量与各自所对应的类别作为样本添加入分类器
FileNames := ['nuts_01','nuts_02','nuts_03','washers_01','washers_02','washers_03','retainers_01','retainers_02','retainers_03']
*这里按照图片顺序将每张图片的类别ID(即add_sample_class_mlp中的Target)存放在数组Classes当中,方便在循环中自动根据图片顺序选择相应的类别ID
Classes := [0,0,0,1,1,1,2,2,2]
*对于每一张图片,将进行以下操作
for J := 0 to |FileNames| - 1 by 1
read_image (Image, 'rings/' + FileNames[J])
dev_display (Image)
dev_set_colored (6)
*segment (Image, Objects)
*二值化分割图像,提取图像中较暗的部分("dark"),即零件部分,'max_separability'表示在直方图中对最大的可分性进行分割,
*UsedThreshold是算法自动计算出用于图像分割的阈值,作为输出参数返回
binary_threshold (Image, Objects, 'max_separability', 'dark', UsedThreshold)
*合并像素相连部分成为一个个区域
connection (Objects, ConnectedRegions)
*填充每个区域(零件)中的孔或和缝等,形成新的实心区域。
fill_up (ConnectedRegions, Objects)
dev_display (Objects)
*设置字体颜色,显示当前循环位置,方便观察
dev_set_color ('black')
disp_message (WindowHandle, 'Add Sample ' + J + ', Class Index ' + Classes[J], 'window', 10, 10, 'black', 'true')
* add_samples (Objects, MLPHandle, Classes[J])
*计算区域的个数,也就是一张图像中零件的个数。
count_obj (Objects, Number)
*对于一张图片中的每一个处理后的实心区域(零件),将进行以下操作
for k := 1 to Number by 1
*选择第k个区域(零件),计算该区域的相关特征参数,作为特征向量准备输入给分类器
select_obj (Objects, Region, k)
* get_features (Region, Features)
select_obj (Region, SingleRegion, 1)
*与圆的相似度Circularity,roundness则由区域的轮廓与区域中心之间的平均距离(Distance)及与平均距离的偏差(Sigma)组合公式而来。
circularity (SingleRegion, Circularity)
roundness (SingleRegion, Distance, Sigma, Roundness, Sides)
*这个算子用于计算区域几何特性,输出4个代表区域几何特性的参数。
moments_region_central_invar (SingleRegion, PSI1, PSI2, PSI3, PSI4)
*前面通过三个算子得到了Circularity,roundness, PSI1, PSI2, PSI3, PSI4共6个参数,使用这六个特征参数组合成特征向量Features
Features := [Circularity,Roundness,PSI1,PSI2,PSI3,PSI4]
*将得到的特征向量作为样本添加进分类器,并指明对应的类别ID,即Classes[J]
add_sample_class_mlp (MLPHandle, Features, Classes[J])
endfor
*一张图片中多个区域处理的循环结束
disp_continue_message (WindowHandle, 'black', 'true')
stop ()
endfor
* 多张图片的处理的循环结束
* 三.利用添加好的样本特征向量组,训练分类器
dev_clear_window ()
dev_set_color ('black')
disp_message (WindowHandle, 'Training...', 'window', 10, 10, 'black', 'true')
*开始训练分类器
train_class_mlp (MLPHandle, 200, 1, 0.01, Error, ErrorLog)
*训练完成后,清除分类器中的样本数据,释放占用内存资源
clear_samples_class_mlp (MLPHandle)
*出现文字提示训练完成
disp_message (WindowHandle, 'Training... completed', 'window', 10, 10, 'black', 'true')
disp_continue_message (WindowHandle, 'black', 'true')
stop ()
*
* 四.实现分类器分类功能
*dev_set_draw ('fill')
*对于每一张图片,进行目标区域的获取与每一个区域特征向量的获取,输入给分类器进行类别的判断
for J := 1 to 4 by 1
read_image (Image, 'rings/mixed_' + J$'02d')
dev_display (Image)
dev_set_color ('black')
dev_set_draw ('margin')
disp_message (WindowHandle, 'Classifiy Image' + J, 'window', 10, 10, 'black', 'true')
*segment (Image, Objects)
*对每张图片进行区域分割,得到每个零件的区域并进行填充,具体步骤含义参考创建mlp分类器时的相同代码
binary_threshold (Image, Objects, 'max_separability', 'dark', UsedThreshold)
connection (Objects, ConnectedRegions)
fill_up (ConnectedRegions, Objects)
*classify (Objects, MLPHandle, Classes)
*完成对每个区域(也就是每一个零件)的提取后,分别计算每个区域的特征向量并输入给分类器进行分类,分类结果存储在Classes数组当中
count_obj (Objects, Number)
Classes := []
for k := 1 to Number by 1
*下面计算特征向量步骤与创建mlp分类器部分相应代码相同,自行向前查阅。
select_obj (Objects, Region, k)
*get_features (Region, Features)
select_obj (Region, SingleRegion, 1)
circularity (SingleRegion, Circularity)
roundness (SingleRegion, Distance, Sigma, Roundness, Sides)
moments_region_central_invar (SingleRegion, PSI1, PSI2, PSI3, PSI4)
Features := [Circularity,Roundness,PSI1,PSI2,PSI3,PSI4]
*得到特征向量Features输入分类器,进行分类,返回一个最可能的结果Class,并给出置信度Confidence
classify_class_mlp (MLPHandle, Features, 1, Class, Confidence)
*该语法含义是将每次循环中class的值添加入Classes当中,相当于很多语言中的数组的.push()方法,以此将分类结果class存储在数组Classes
*当中,如Class为0,则对应第一类,Class为2,则对应第三类。
Classes := [Classes,Class]
endfor
*结束对一张图片中所有区域(零件)的分类
stop()
*下面是针对每张图片分类完成后的结果,进行可视化显示
disp_obj_class (Objects, Classes)
count_obj (Objects, Number)
*采用三种不同颜色放在数组中,根据类别ID(0,1,2),来选择数组中的颜色
Colors := ['yellow','magenta','green']
for l := 1 to Number by 1
select_obj (Objects, Region, l)
dev_set_color (Colors[Classes[l - 1]])
dev_display (Region)
endfor
if (J < 4)
* 判断四张图片未处理完时,每一张处理完成后,暂停进行提示,再手动进入下一张的处理
disp_continue_message (WindowHandle, 'black', 'true')
stop ()
endif
endfor
*四张图片全部处理完成
stop ()
*
*5.清除分类器,释放分类器所占用的内存
clear_class_mlp (MLPHandle)
其他分类器的操作步骤也可以参考MLP分类器的操作步骤,希望读者结合HALCON的相关自带例程,自行发现相通之处。
(1)字符的训练
①读取样本图像,并对样本中的已知字符进行区域分割,分割的单位是单个字符的包围区域这时可以使用draw_rectangle1等算子选择出单个字符的区域
②将分割出的区域和对应的字符名称存储在训练文件中。可以使用append_ocr_class_mlp算子,将字符区域存在指定的以“.trf”结尾的训练文件中。
③检查训练文件中的对应关系,即图像与字符的名称一一对应。
④训练分类器。创建分类器后开始训练。
⑤保存分类器
⑥清除分类器
(2)字符检测
①读取分类器文件。
②对待检测的字符进行区域分割,提取出独立的字符区域。
③使用分类器进行分类。
④清除分类器
Halcon根目录下的ocr文件夹中内置了许多针对数字、字母和喷码等类字符的分类器。针对多种标准的、非中文的字符,可以直接调用这些训练好的分类器文件而无需自己进行训练。手写体或者中文字符则需要自己进行离线训练。
这里仅放置Halcon实例程序源代码,具体步骤参考之前的mlp分类器分类程序讲解。分类步骤及思想都是想通的。
(1)字符的训练
* The font is used in "bottle.hdev"
*
*
* Step 0: Preparations
FontName := 'bottle'
*
* Step 1: Segmentation
dev_update_window ('off')
read_image (Bottle, 'bottle2')
get_image_size (Bottle, Width, Height)
dev_close_window ()
dev_open_window (0, 0, 2 * Width, 2 * Height, 'black', WindowID)
set_display_font (WindowID, 27, 'mono', 'true', 'false')
threshold (Bottle, RawSegmentation, 0, 95)
fill_up_shape (RawSegmentation, RemovedNoise, 'area', 1, 5)
opening_circle (RemovedNoise, ThickStructures, 2.5)
fill_up (ThickStructures, Solid)
opening_rectangle1 (Solid, Cut, 1, 7)
connection (Cut, ConnectedPatterns)
intersection (ConnectedPatterns, ThickStructures, NumberCandidates)
select_shape (NumberCandidates, Numbers, 'area', 'and', 300, 9999)
sort_region (Numbers, FinalNumbers, 'first_point', 'true', 'column')
dev_display (Bottle)
dev_set_color ('green')
dev_set_line_width (2)
dev_set_shape ('rectangle1')
dev_set_draw ('margin')
dev_display (FinalNumbers)
*
* Step2: Training file generation
TrainingNames := ['0','1','0','8','9','4']
TrainingFileName := FontName + '.trf'
sort_region (FinalNumbers, SortedRegions, 'first_point', 'true', 'column')
shape_trans (SortedRegions, RegionTrans, 'rectangle1')
area_center (RegionTrans, Area, Row, Column)
MeanRow := mean(Row)
dev_set_check ('~give_error')
delete_file (TrainingFileName)
dev_set_check ('give_error')
for I := 0 to |TrainingNames| - 1 by 1
select_obj (SortedRegions, CharaterRegions, I + 1)
append_ocr_trainf (CharaterRegions, Bottle, TrainingNames[I], TrainingFileName)
disp_message (WindowID, TrainingNames[I], 'image', MeanRow - 40, Column[I] - 6, 'yellow', 'false')
endfor
*
* Step3: Training
CharNames := uniq(sort(TrainingNames))
create_ocr_class_mlp (8, 10, 'constant', 'default', CharNames, 10, 'none', 10, 42, OCRHandle)
trainf_ocr_class_mlp (OCRHandle, TrainingFileName, 200, 1, 0.01, Error, ErrorLog)
write_ocr_class_mlp (OCRHandle, FontName)
clear_ocr_class_mlp (OCRHandle)
(2)字符的在线检测
*
* Step 0: Preparations
* Specify the name of the font to use for reading the date on the bottle.
* It is easiest to use the pre-trained font Universal_0-9_NoRej. If you
* have run the program bottlet.hdev in this directory, you can activate
* the second line to use the font trained with this program.
FontName := 'Universal_0-9_NoRej'
* FontName := 'bottle'
*
* Step 1: Segmentation
dev_update_window ('off')
read_image (Bottle, 'bottle2')
get_image_size (Bottle, Width, Height)
dev_close_window ()
dev_open_window (0, 0, 2 * Width, 2 * Height, 'black', WindowID)
set_display_font (WindowID, 16, 'mono', 'true', 'false')
dev_display (Bottle)
disp_continue_message (WindowID, 'black', 'true')
stop ()
*
* Create Automatic Text Reader and set some parameters
create_text_model_reader ('auto', FontName, TextModel)
* The printed date has a significantly higher stroke width
set_text_model_param (TextModel, 'min_stroke_width', 6)
* The "best before" date has a particular and known structure
set_text_model_param (TextModel, 'text_line_structure', '2 2 2')
*
* Read the "best before" date
find_text (Bottle, TextModel, TextResultID)
*
* Display the segmentation results
get_text_object (Characters, TextResultID, 'all_lines')
dev_display (Bottle)
dev_display (Characters)
stop ()
* Display the reading results
get_text_result (TextResultID, 'class', Classes)
area_center (Characters, Area, Row, Column)
disp_message (WindowID, Classes, 'image', 80, Column - 3, 'green', 'false')
*
* Free memory
clear_text_result (TextResultID)
clear_text_model (TextModel)
内容粗浅,敬请各位读者朋友评论私信,批评指正!