支持向量机(SVM)是建立在统计学理论理论上的一种数据挖方法,能成功的处理回归问题和模式识别(分类问题、判别问题)等诸多问题,并可推广与预测和综合评价等领域学科。SVM理论的机理是寻找一个满足分类要求的最优解分类超平面,使得该超平面在保证精度的同时,能够支持超平面两侧的空白区域最大化。理论上,支持向量机能够实现对线性可分类数据的最优分类。
在机器视觉的领域中比较重要的有分类问题,我们就可以使用SVM进行图像的分类。当然,如果需要跟进一步的知道图像中的目标在图像的那个位置,具体是什么,这就涉及到目标检测的方法。
使用SVM支持向量机算法主要解决了识别物体是什么的问题,物体的具体位置通过相关的计算机视觉算法完成。具体的实现步骤分为3步:第一步,将输入图像进行预处理,转化为合适大小的灰度图;第二步,查找图像轮廓并根据轮廓大小进行筛选,找到符合预期大小的轮廓位置;第三步,根据轮廓的最小外界矩提取出图像ROI并拉伸为固定的60*60大小;第四步,加载模型数据,将拉伸后的ROI进行数据转化为可以处理的标准数据后进行预测并输出预测结果。
通过这样的方法其实是可以获得轮廓的外界矩中心坐标点的,这个坐标点反应的位置是一个大概的估计,对比起深度学习方法的目标检测还欠缺很多。如果你的场景只是在简单的应用,并进行一些分类识别,那SVM也是可以胜任的。
在SVM方法中有两个重要的参数对结果的影响较大,分别是(1)误差惩罚参数C和(2)核函数的形式及其参数。
在(1)中通过设置C实现对错样本比例和算法复杂度折衷,即在确定的特征空间中调节学习机器的置信范围和经验风险比例,使学习机器的推广能力最好。其选取由具体的问题而定,并取决于数据中噪声的数量。每个特征空间至少存在一个合适的C使得SVM的推广能力最好。
在(2)中不同的核函数对分类性能有影响,相同核函数不同参数也有影响。
SVM的数据采集、数据标准化、模型训练、预测结果等几个主要流程。使用SVM的主要步骤如下:根据目标物使用轮廓筛选等方法采集特征区域图像,将特征区域图像进行保存和分类,之后再进一步将分类好的图像数据进行格式转化和数据降维,处理好的事数据将导入到模型训练中,根据设定的核函数参数以及惩罚系数进行模型的训练,训练好模型后将其保存。在预测时通过加载训练好的模型文件,用轮廓筛选的方法查找出图像中的待定目标区域,截取该区域后输入到SVM分类器中,根据模型文件分类器会给出当前模型的分类值。进而就得到了目标的轮廓中心位置和分类信息。
在这里我们一步一步的实现
代码的环境为:windws; pycharm; anaconda; python3.6.9; opencv-python4.5.1.48; numpy1.19.5
第一步当然是采集需要分类的数字图像,通过采集到的推向调节图像预处理的各个参数,达到将数字从图像中提取的效果。
下面的代码可以将我图像中的数字给抠出来,在不同的摄像机和背景条件下需要修改几个参数,来让roi提取的准确。后期的数字能准确的识别到的基础就是前期参数的调节是否准确。需要调参的地方看注释
import cv2 as cv
img = cv.imread('101.jpg',0)
kernel = np.ones((3,3),np.uint8) #开运算算子
im = cv.morphologyEx(img, cv.MORPH_OPEN, kernel) #腐蚀、膨胀
#thresh = cv.adaptiveThreshold(bul,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY,11,2) #自适应阈值化(高斯均值),需要改自己的合适的阈值
thresh = cv.adaptiveThreshold(im,255,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY,13,4) #自适应阈值化(平均值),需要改自己的合适的阈值,两个自适应阈值方法那个提取的区域更纯净就用哪个
contours, hierarchy = cv.findContours(thresh, cv.RETR_LIST, cv.CHAIN_APPROX_NONE) #寻找轮廓
x0 = 30 #截取部分坐标记录值
count = 0
for cnt in contours:
mates = cv.contourArea(cnt)
if mates>13000 and mates<15500: #使用边缘面积过滤较小边缘框 ,需要自己调节参数,找出合适的轮廓面积
rect = cv.minAreaRect(cnt)
box = cv.boxPoints(rect)
box = np.int0(box)
cv.drawContours(im, cnt, -1, (175,0), 3) #绘制轮廓
#[x,y,w,h] = []
[x,y,w,h] = cv.boundingRect(cnt) #读取当前轮廓的坐标值
#cv.imshow('norm2', im)
if(abs((x-x0))>60): #过滤重复的选框
x0 = x
if h>120 and h < 160 and w>90 and w<120: #使用高过滤小框和大框,需要自己调节参数,找出高和宽合适的轮廓
count+=1
cv.rectangle(im,(x,y),(x+w,y+h),(0,0,255),2) #画框
roi = thresh[y:y+h,x:x+w] #框出带选定区域
roismall = cv.resize(roi,(60,60)) #拉伸剪裁区域
cv.imshow('norm2'+ str(count),roismall)
cv.imshow('norm',im)
cv.waitKey(0)
cv.destroyAllWindows()
在程序中定义了x0 = 30 count = 0,x0的作用是过滤掉位置比较接近的框,保证一个数字周围只有一个框。count 的作用是计数提取到的roi区域。
通过原始图像提取出来的数字全部拉升到固定的尺寸,提取的效果就像上面截图中小的那两个一样。
这里我们简单的把上面识别一张图形的代码该做摄像头的视频流,然后把取出来的60*60小数字图像保存在自己电脑的文件夹里。
import cv2 as cv
import numpy as np
num = 1 # 递增,用来保存文件名
x0 = 30 # 截取部分坐标记录值
num0 = 0
num1 = 0
num2 = 0
kernel = np.ones((3,3),np.uint8) # 开运算算子
def imgewrite(imge): #保存采集的数据
global num
cv.imwrite("F:/CV/Project/num/newlimg/" + str(num) + ".jpg", imge) #我电脑是windows,这里根据自己需要修改保存图像的路径
print("success to save" + str(num) + ".jpg")
num += 1
if __name__ == "__main__":
count = 0
cap = cv.VideoCapture(0) # 从摄像头读取数据
while cap.isOpened():
ret,frame = cap.read()
img = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
if not ret:
print("Can't receive frame (stream end?). Exiting ...")
break
#img = cv.imread('18.jpg',0)
bul = cv.GaussianBlur(img,(7,7),0) # 定义高斯核滤波
im = cv.morphologyEx(img, cv.MORPH_OPEN, kernel) # 腐蚀、膨胀
#thresh = cv.adaptiveThreshold(bul,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY,11,2) # 自适应阈值化(高斯均值)
thresh = cv.adaptiveThreshold(im,255,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY,13,4) # 自适应阈值化(平均值)
contours, hierarchy = cv.findContours(thresh, cv.RETR_LIST, cv.CHAIN_APPROX_NONE) #寻找轮廓
# cv.imshow('image' ,thresh)
for cnt in contours:
mates = cv.contourArea(cnt)
if mates>13000 and mates<15500: # 使用边缘面积过滤较小边缘框
[x,y,w,h] = cv.boundingRect(cnt) # 读取当前轮廓的坐标值
#cv.imshow('norm2', im)
if(abs((x-x0))>60): # 过滤重复的选框
x0 = x
if h>120 and h < 160 and w>90 and w<120: # 使用高过滤小框和大框
count+=1
roi = thresh[y:y + h, x:x + w] # 框出带选定区域
roismall = cv.resize(roi, (60, 60)) # 拉伸剪裁区域
imgewrite(roismall) # 保存剪裁的区域用于图像训练,预测数据时注释
cv.drawContours(im, cnt, -1, (175, 0), 3) # 绘制轮廓
cv.rectangle(im,(x,y),(x+w,y+h),(0,0,255),2) #画框
cv.imshow('test',im)
if cv.waitKey(1) == ord('q'):
break
cap.release() #释放摄像头
cv.destroyAllWindows() #释放窗口
运行脚本,然后移动摄像头,图像会自动采集并且保存在路径中。因为是数字图像,我们需要手动把保存下来的图像分类,放到不同的文件夹中。
效果呢大概就是上面图像中的那样。到这里我们就完成了图像的采集。
采集到了分类好的图像数据,我们需要对图像数据进行转化和处理,让他变为我们训练SVM模型时需要的数据。通过下面的脚本可以生成需要的CSV文件。这里的脚本注意修改自己的文件路径。
#从指定路径下遍历每个数字标签对应的文件夹中的所有图片
#读取出该文件夹下的所有图片并保存到csv文件中
#读取的图片大小必须拉伸为固定长宽的图像
# @2022-1-12
# @from SWUST-IPC
import csv
import os
import cv2
def convert_img_to_csv(img_dir):
#设置需要保存的csv路径
with open(r"F:\CV\Project\SVM\num\newlimg\limge.csv","w",newline="")as f:
#设置csv文件的列名
column_name = ["label"]
column_name.extend(["pixel%d"%i for i in range(60*60)])
#将列名写入到csv文件中
writer = csv.writer(f)
writer.writerow(column_name)
#该目录下有9个目录,目录名从0-9
for i in [1,2,3,4,5,6,7,8]:
#获取目录的路径
img_temp_dir = os.path.join(img_dir,str(i))
#获取该目录下所有的文件
img_list = os.listdir(img_temp_dir)
#遍历所有的文件名称
for img_name in img_list:
#判断文件是否为目录,如果为目录则不处理
if not os.path.isdir(img_name):
#获取图片的路径
img_path = os.path.join(img_temp_dir,img_name)
#因为图片是黑白的,所以以灰色读取图片
img = cv2.imread(img_path,cv2.IMREAD_GRAYSCALE)
#图片标签
row_data = [i]
#获取图片的像素
row_data.extend(img.flatten())
#将图片数据写入到csv文件中
writer.writerow(row_data)
if __name__ == "__main__":
#将该目录下的图片保存为csv文件
convert_img_to_csv(r"F:\CV\Project\SVM\num\newlimg")
在训练这里我没有添加数据测试,有需要的自己改一改就可以。
需要注意的是自己各个文件对应的路径,不要弄错。
import pandas as pd
from sklearn.decomposition import PCA
# PCA方法用于数据的预处理,数据降维,PCA的一般步骤是:先对原始数据零均值化,然后求协方差矩阵,
# 接着对协方差矩阵求特征向量和特征值,这些特征向量组成了新的特征空间。
from sklearn import svm
import joblib # 导入包用于模型的保存和加载预测
import time
if __name__ =="__main__":
# train_num = 5000
# test_num = 7000
data = pd.read_csv('limge.csv')
train_data = data.values[0: ,1:]
train_label = data.values[0: ,0 ]
#test_data = data.values[train_num:test_num,1:]
#test_label = data.values[train_num:test_num,0]
t = time.time()
# PCA降维
# PCA降维,数据预处理
# @n_componentsPCA:算法中所要保留的主成分个数n,也即保留下来的特征个数n
# @whiten:白化,使得每个特征具有相同的方差。
pca = PCA(n_components=0.8, whiten=True)
print('start pca...')
train_x = pca.fit_transform(train_data)
#test_x = pca.transform(test_data)
print(train_x.shape)
# svm训练
# @C:惩罚系数,用来控制损失函数的惩罚系数
# @kernel:算法使用的核函数类型
# "RBF"径向基核,也就是高斯核函数;Linear指的是线性核函数;Poly指的是多项式核;Sigmoid指的是双曲正切函数tanh核
print('start svc...')
svc = svm.SVC(kernel = 'rbf', C = 10)
svc.fit(train_x,train_label) # @fit方法:在数据集(X,y)上拟合SVM模型
#pre = svc.predict(test_x) #训练后返回预测标签值
#保存模型
joblib.dump(svc, 'model.m') # 保存SVM模型
joblib.dump(pca, 'pca.m') # 保存预处理后的数据
# 计算准确率
#score = svc.score(test_x, test_label)
#print(u'准确率:%f,花费时间:%.2fs' % (score, time.time() - t))
这之后会生成两个文件,分别为model.m和pca.m。只要生成了这两个文件就说明我们训练成功了。
这里我直接把最终的预测本展示,其中包含了保存训练数据的部分,通过对不同部分的注释与使用就可以进行预测或者采集。
# 检测部分函数与数据采集
# 使用opencv轮廓处理与筛选提取出目标区域,使用支持向量机对图像数字进行分类识别,并输出
# @2022-1-12
# @from SWUST-IPC-CVTeam
# @测试环境为:windws; pycharm; anaconda; python3.6.9; opencv-python4.5.1.48; numpy1.19.5
import numpy as np
from matplotlib import pyplot as plt
import sys
import cv2 as cv
import joblib
num = 1 #递增,用来保存文件名
x0 = 30 #截取部分坐标记录值
num0 = 0
num1 = 0
num2 = 0
kernel = np.ones((3,3),np.uint8) #开运算算子
def svmdetect(imges):
#res_img = cv.resize(imges , (28,28)) #修改图像尺寸
#test = res_img.res_img(1,784) #将图片转化为1行784(28*28)列自然数
test = imges.reshape(1,3600)
# 加载模型
svc = joblib.load("model.m")
pca = joblib.load("pca.m")
# svm
#print('start pca...')
test_x = pca.transform(test) #标准化数据
# print(test_x.shape)
pre = svc.predict(test_x) #预测
#print(pre[0]) #显示预测标签值
return pre[0]
def labershow(num): #使相同数据只输出一次
global num1,num0,num2
if (num0 + num) != num1:
num2 = 0
if num2 == 0:
print("laber is ",num)
num2 = 1
num1 = num0 + num
num0 = num
def imgewrite(imge): # 保存训练图片
global num
cv.imwrite("F:/CV/Project/num/newlimg/" + str(num) + ".jpg", imge)
print("success to save" + str(num) + ".jpg")
num += 1
if __name__ =="__main__":
cap = cv.VideoCapture(0)
while cap.isOpened():
successful, frame = cap.read()
img = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)
if not successful:
print("Can't receive frame (stream end?). Exiting ...")
break
#img = cv.imread('18.jpg',0)
bul = cv.GaussianBlur(img,(7,7),0) #定义高斯核滤波
im = cv.morphologyEx(img, cv.MORPH_OPEN, kernel) #腐蚀、膨胀
#thresh = cv.adaptiveThreshold(bul,255,cv.ADAPTIVE_THRESH_GAUSSIAN_C,cv.THRESH_BINARY,11,2) #自适应阈值化(高斯均值)
thresh = cv.adaptiveThreshold(im,255,cv.ADAPTIVE_THRESH_MEAN_C,cv.THRESH_BINARY,13,4) #自适应阈值化(平均值)
contours, hierarchy = cv.findContours(thresh, cv.RETR_LIST, cv.CHAIN_APPROX_NONE) #寻找轮廓
samples = np.empty((0,900))
#cv.imshow('image' ,thresh)
responses = []
keys = [i for i in range(48,58)] #48-58为ASCII码
count =0
for cnt in contours:
mates = cv.contourArea(cnt)
if mates>13000 and mates<15500: #使用边缘面积过滤较小边缘框
[x,y,w,h] = cv.boundingRect(cnt) #读取当前轮廓的坐标值
#cv.imshow('norm2', im)
if(abs((x-x0))>60): #过滤重复的选框
x0 = x
if h>120 and h < 160 and w>90 and w<120: #使用高过滤小框和大框
count+=1
roi = thresh[y:y + h, x:x + w] # 框出带选定区域
roismall = cv.resize(roi, (60, 60)) # 拉伸剪裁区域
#imgewrite(roismall) # 保存剪裁的区域用于图像训练,预测数据时注释
laber = svmdetect(roismall) # 预测当前区域数字,采集数据时注释
labershow(laber) # 输出,采集数据时注释
#font = cv.FONT_HERSHEY_SIMPLEX # 文本显示实例化
#cv.putText(im, laber, (10, 400), font, 4, (255, 255), 2, cv.LINE_AA) #将识别到的数字显示到源屏幕上
cv.drawContours(im, cnt, -1, (175, 0), 3) # 绘制轮廓
cv.rectangle(im,(x,y),(x+w,y+h),(0,0,255),2) # 画框
#cv.imshow('norm2'+ str(count) , roismall) # 显示截取出的部分内容
#rect = cv.minAreaRect(cnt) #计算最小外界矩
#box = cv.boxPoints(rect) #计算外界矩坐标
#box = np.int0(box)
#cv.drawContours(im,[box],0,(0,0,255),2) #画出矩形
#key = cv.waitKey(0)
#if key == 27: # (escape to quit)
# sys.exit()
#elif key in keys:
# responses.append(int(chr(key)))
# sample = roismall.reshape((1,900))
# samples = np.append(samples,sample,0)
#font = cv.FONT_HERSHEY_SIMPLEX #文本显示实例化
#cv.putText(img, laber , (10, 400), font, 4, (255, 255, 255), 2, cv.LINE_AA)
cv.imshow('norm',im)
if cv.waitKey(1) == ord('q'):
break
cap.release()
cv.destroyAllWindows()