虽然上过机器学习的课程,但是那么课既没有课程设计也没有需要敲代码的作业,寻思着毕业设计选一个来挑战一下。“划线框识别”这个题目有两个,一个是深度学习实现,另一个是支持向量机实现,和舍友一人选了一个。考研复试结束了没什么事情,开始动手写这个,指不定自己哪天就忘了。
还是写博客舒服,写论文太痛苦。
python + libsvm + opencv
SVM选择的是C_SVC和RBF内核
刚刚看到题目的时候我以为“划线框”和高中时候用的答题卡差不多,后来给了数据集,感觉没人会叫这东西划线框吧...看了一下确实没有见过,可能一些调查问卷会用这种?调查问卷也只接触过打 √ 打勾的,倒是不需要用2B铅笔来涂。
目标就是用这些数据训练一个SVM模型来实现对这种划线框的识别。
划线框的图片示例(一部分):
选择的特征是HOG特征,后期的时候准确率上不去,想到既然样本里笔迹都是红色的,那么是不是也可以用一下颜色,加了颜色直方图特征,结果相比于原来只用HOG,效果并没有什么变化。
学习的时候参考的是:
python opencv教程里面使用SVM进行手写图片识别以及Histogram of Oriented Gradients,其实搜一下HOG能发现很多博客都翻译过这篇文章。比较着这两篇里面的讲述和代码,差不多能够理解HOG特征提取的流程。在按照这两篇文章学习时的一些问题:
1、我通过Sobel算子在水平方向和竖直方向卷积之后得到的图像如下。
1:原图像,2:Gx,3:Gy,4:梯度图像而Histogram of Oriented Gradients中使用算子卷积之后的图像是像下面这样的。(分别对应上图的2-4)
出现这样的情况是因为我计算完梯度之后再显示图片时,每个像素点处值的范围是0~255,只需将其映射到0~1即可。
img = np.float32(img)/255.0
越亮的地方值越大,说明梯度变化也越大。
2、因为最后的block是为了归一化,来减小光照的影响,我看了一下手里的样本,光照的影响几乎没有,于是就将最后block那一步给省略掉了。
计算HOG特征的代码放在末尾,拆开说说其中的几个部分。
1、提取特征之前的预处理。一开始我只是单纯地使用一个阈值进行划分,将图像转换为二值图像,但是在后期训练的时候我发现这样的处理有问题。像是下面这个图像,仅使用单个阈值进行二值化,选项“11”本身在二值化后是被保留的。
但我们其实并不关心黑色选项,我们只关心红色的笔迹。而且因为有的选项是个位数,有的选项是两位数,有的甚至带着小数点,就会导致选项对提取出的特征有很大的影响,所以需要排除选项对特征提取的干扰。
后期正确率一直上不去。我一开始以为是特征维度不够,试着将HOG划分的更细,也试着增加了颜色直方图特征。对准确率的提升效果都不大。后来使用了双阈值划分,即设置两个阈值min和max,如果像素点的值在min和max之间,那么值被保留,否则被置为0。
1.原图像 2.双阈值划分后 3.腐蚀后 4.按位取反后腐蚀是因为只靠双阈值划分还是会有选项的一些边缘残留,因为我的样本图像本身尺寸就很小,只能使用最小的2x2大小的腐蚀算子,用3x3的就差不多已经渣都不剩了。
#输入图像路径,将图像通过两个阈值分割
#阈值分割后的图像按2*2的邻域大小的模板进行腐蚀,只留下划线框
def HandleImgByThreshAndErode(img_gray):
#读取灰度图像
#img_gray = cv.imread(path, cv.IMREAD_GRAYSCALE)
#两个阈值处理
# THRESH_TOZERO将小于阈值的灰度值设为0,大于阈值的值保持不变
retvl, img_gray = cv.threshold(img_gray, svm_parameter.bottom_thresh, 255, cv.THRESH_TOZERO)
# THRESH_TOZERO_INV 将大于阈值的灰度值设为0,小于阈值的值保持不变
retvl, img_gray = cv.threshold(img_gray, svm_parameter.top_thresh, 255, cv.THRESH_TOZERO_INV)
#生成2*2的核 再大会腐蚀过头
kernel = np.ones((2, 2), np.uint8)
#腐蚀灰度图像 迭代一次就够了
img_final = cv.erode(img_gray, kernel, iterations=1)
#按位取反
img_final = cv.bitwise_not(img_final)
#得到的结果还是一个灰度图,可以拿去计算HOG特征
return img_final
有了gx和gy之后就可以计算梯度向量,举一个简单情况方便理解。
gx = cv.Sobel(img, cv.CV_32F, 1, 0)
gy = cv.Sobel(img, cv.CV_32F, 0, 1)
#计算梯度和梯度变化方向
mag, ang = cv.cartToPolar(gx, gy)
在直方图相关的概念中,bin经常出现,其实bin指的就是直方图中出现的“柱”。比如下图中就有7个bin,“bin=1”可以理解为“柱1”,其存储的值为a。
output = np.bincount(a),a是1维数组,其元素为非负整数。计数数组a中每个元素出现的次数,然后以元素为下标,值为出现次数输出结果。
a = [1,0,0,0,5,3,0,0,1],其中0出现了5次,1出现了2次,3和5都出现了1次,2和4出现了0次。m出现了n次,output [m]=n。
output = [5,2,0,1,0,1]
output = np.bincount(a,weight),当有第二个参数[数组]时,计算的就不再是每个元素出现的次数,而是以b中的元素作为a中对应下标元素的权重,计算a中同一元素的权重之和。
a = [1,0,0,0,5,3,0,0,1]
w = [5,1,3,4,2,1,2,2,3]
a中元素1出现在下标0的位置,那么output[1]+=w[0],1还出现在下标8,output[1]+=w[8]。
output = [12,8,0,1,0,2]
也就是说当只有一个参数时,权重默认都是1。
关于bincount这个函数,这个老哥讲得比较清楚,当然最好还是自己动手试一试。
在python-opencv教程的代码中比较难理解的是这一行。
hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
zip函数将bin_cells和mag_cells中的元素一对一对地打包成元组,例如a=[元素a1,元素a2,元素a3],b=[元素b1,元素b2,元素b3],那么zip(a,b)=[(元素a1,元素b1),(元素a2,元素b2),(元素a3,元素b3)]。然后使用for循环提取出其中的每一对,用b,m存储,以便在bincount函数中使用。
bin_cells = bins[:center_row,:center_col],
bins[center_row:,:center_col],
bins[:center_row,center_col:],
bins[center_row:,center_col:]
mag_cells = mag[:center_row,:center_col],
mag[center_row:,:center_col],
mag[:center_row,center_col:],
mag[center_row:,center_col:]
可以在上面看到,bin_cells和mag_cells都只有4个元素且都是矩阵。那么for循环中的每一对b,m也是一对矩阵。
使用ravel函数是将二维的b和m扁平化,方便进行遍历,原本n*m的数据变成(n*m)的一维数据。bins中记录的是梯度变化的方向,mag中记录的是梯度值。以方向作为bin,以梯度值为权重,计算bincount。
最终hists是一个包含4个元素的数组,这4个元素都是1维数组,每个都至少有16项。因为是都是一维数组,使用hstack相当于将他们拼接成一个数组。
hist = np.hstack(hists)
之所以要使用hstack而不是vstack,因为这4个cell每个都作为独立的一部分,使用vstack就相当于将整个图像作为一个cell,前面做的划分等工作也就没意义了。就好比你将语数英综合4门成绩列出来能看出来一个学生擅长什么不擅长什么,而只列出一个总分就只能看到他的总体水平。划分为4个cell是因为样本图像本身大小并不大。划分太小像素点都有些不够用...具体使用多少cell要根据自己的实际情况来决定。
完整代码:
def GetHOGFeature(img_src,show_img=False,show_gradient_img=False,show_gradient_img_isGray=False):
#彩色图像,之后可能会用来显示与原图像的对比
#img_3c = img_src.copy()
#灰度图像
img_1c = cv.cvtColor(img_3c,cv.COLOR_BGR2GRAY)
#这里是自己写的一个“双阈值化+腐蚀”的函数,主要是对图像进行预处理
img_1c = HandleImgByThreshAndErode(img_1c)
img = img_1c.copy()
#GetBinary是获得一个二值化函数
img = GetBinaryImg(img)
#bin_n为梯度直方图至少有多少列
bin_n = 16
#分别在水平和竖直方向使用Sobel进行卷积
gx = cv.Sobel(img, cv.CV_32F, 1, 0)
gy = cv.Sobel(img, cv.CV_32F, 0, 1)
#计算梯度和梯度变化方向
mag, ang = cv.cartToPolar(gx, gy)
#ang为Mat类型
#量化
#cartToPolar默认输出的ang是弧度0到2π
#有符号梯度 将角度的值从[0,2π]映射到[0,16]
#bins = np.int32(bin_n*ang/(2*np.pi))
#无符号梯度,将角度的值从[0,2π]映射到[0,16],但是将a和a+pi看做是一样的
bins = np.int32(bin_n*(ang%np.pi)/(np.pi))
# 每个bins中对应的坐标x都从0到np.size(bins,1) y从0到np.size(bins,0)
# bins(mag)、ang图像划分为4个子矩形
# bin_cells和mag_cell的类型应为Mat数组
center_row = int(img.shape[0]/2)
center_col = int(img.shape[1]/2)
bin_cells = bins[:center_row,:center_col], bins[center_row:,:center_col], bins[:center_row,center_col:], bins[center_row:,center_col:]
mag_cells = mag[:center_row,:center_col], mag[center_row:,:center_col], mag[:center_row,center_col:], mag[center_row:,center_col:]
hists = [np.bincount(b.ravel(), m.ravel(), bin_n) for b, m in zip(bin_cells, mag_cells)]
hist = np.hstack(hists)
#print("HOG特征元素个数")
#print(len(hist))
#保留2位小数方便计算
hist = np.round(hist,2)
return hist
试着根据计算出来的梯度向量在原图像上绘制了一下。能让效果直观一点。