做了好久自动化打大神符,等比赛结束之后写一篇长博客把过程和代码放出来
时间线
比赛结束了
还有因为刚开始用CSDN写博客,所以格式排版很bug所以希望大家 将就看
南部赛区比赛终于结束了,很遗憾本大人的队伍还是搞不过老司机们(毕竟一年级新生),而且因为某些”前戏“没有准备好,所以最后开发的大神符系统也没能使用,所以在此分享一下目前为止(实际也就认真做了几天成果,队长知道肯定不会很开心的)。
基本的设想架构:我方采用树莓派+摄像头实现图像处理部分,然后通过运算得出结果(密码串中的第n个数字在九宫格的m位置,m的取值范围为1 -9 )通过树莓派的UART传送到步兵车的接收系统,然后步兵车receive到树莓派传输回来的指令之后根据经验法移动到m位置处(假设初始步兵的发射装置:准星指向8位置,注意此处说的是九宫格的8位置),经验法就是我们在比赛之前,先对我们的步兵车进行一波训练:利用arudio通过设置不同的向上方和左方移动的时间,来使你的步兵车的准星能够准确的移动到目标位置,然后给从8移动到1-9的位置的这一套固定流程设置一个代号,这个代号可以随便设置,只要不重复即可,比如说从8位置移动到1位置的代号是‘q’,那我们当前密码串要射击的位置正好是九宫格中的1位置(左上角),那我们UART发送一个‘q’字符过去,步兵车接收到这个信号之后,按照预测的流程进行移动保证能够打中目标区域,该过程结束之后,我们在让发射装置回到原本的8位置(为什么要回到初始位置之后我会进行一下交代,这是出于节约时间的目的,毕竟我们的打击时间只有1.5s)。
大致的思路已经介绍完了,相信大家通过我足够冗长的描述已经差不多能够get到我的点了,下面我们进入算法部分,
首先是树莓派部分:此处主要是调用UART发送和接收指令的部分:
代码:
以上部分只有一个发送信号的功能,调用的函数主题就是我们的图像处理代码:首先说明一下,2017年DJI的robomaster比赛大神符系统中密码串的显示为标准的LED数字显示,这个东西非常无比好训练直接opencv中调用一个knn的库,在此之前搞一个训练集resize一下大小完全可以解决,对于九宫格中的数字,其使用的是minist字体集(本人亲测,在其视频流中截取图片时,我使用svm模型训练minist字体库,训练样本为5w,在minist自带的测试样本中正确率为98%,实际的预测过程应用到大神符的识别当中平均可以达到九宫格中九个数字只预测错一个),话不多说,作为一个粗糙的工程师先上一下代码,我相信聪明的大家很容易可以看懂我的代码:同时提示所用的代码是python搭载opencv库实现的,要问原因的话只是因为在树莓派上运行python相比c语言来说容易并且舒服很多。
1)SVM训练部分
# decoding:utf-8
import os
import cv2
import numpy as np
import codecs
from cv2.ml import VAR_ORDERED
import cPickle
import gzip
def vectorized_result(j):
e = np.zeros((10, 1))
e[j] = 1.0
return e
def load_data():
mnist = gzip.open(os.path.join('data', 'mnist.pkl.gz'), 'rb')
training_data, classification_data, test_data = cPickle.load(mnist)
mnist.close()
return training_data, classification_data, test_data
def wrap_data():
tr_d, va_d, te_d = load_data()
# print type(tr_d), type(va_d), type(te_d)
training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]]
training_results = [vectorized_result(y) for y in tr_d[1]]
training_data = zip(training_inputs, training_results)
validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]]
validation_data = zip(validation_inputs, va_d[1])
test_input = [np.reshape(x, (784, 1)) for x in te_d[0]]
test_data = zip(test_input, te_d[1])
return training_data, validation_data, test_data
def train_svm(train_file='train_data.txt', test_file= 'train_result.txt'):
svm = cv2.ml.SVM_create()
svm.setType(cv2.ml.SVM_C_SVC)
svm.setGamma(0.01)
svm.setC(10.0)
svm.setDegree(2)
svm.setKernel(cv2.ml.SVM_POLY)
t_d = np.loadtxt(train_file, np.float32)
m_d = np.loadtxt(test_file, np.int32)
train_data = cv2.ml.TrainData_create(t_d, cv2.ml.ROW_SAMPLE, m_d)
svm.train(train_data)
return svm
def svm_test(svm, test_data):
le = len(test_data)
sum_tem = 0
for i in range(le):
sample = np.array([test_data[i][0].ravel()], dtype=np.float32).reshape(28, 28)
a, b =svm.predict(np.array([test_data[i][0].ravel()], dtype=np.float32))
if b[0][0] == test_data[i][1] or test_data[i][1] == 0:
sum_tem += 1
print '正确率 ', float(sum_tem * 1.0/ le)
def svm_predict(svm, sample):
resized = sample.copy()
rows, cols = resized.shape
pts1 = np.float32[[0, 0], [0, rows-1], [cols-1, 0]]
pts2 = np.float32[[0, 0], [0, 27], [27, 0]]
print 'pts1', 'pts2', pts1, pts2
M = cv2.getAffineTransform(pts1, pts2)
# 第三个参数:变换后的图像大小
res = cv2.warpAffine(resized, M, (28, 28))
if (rows != 28 or cols != 28) and rows * cols > 0:
resized = cv2.resize(resized, (28, 28), interpolation=cv2.INTER_CUBIC)
return svm.predict(np.array([resized.ravel()], dtype=np.float32))
if __name__ == '__main__':
tr, val, test = wrap_data()
save_path = os.path.join('data', 'best_svm_50000')
if os.path.exists(save_path):
print 'find it'
svm = cv2.ml.SVM_load(save_path)
else:
svm = train_svm()
svm.save(save_path)
svm_test(svm, test)
通过以上的代码,我们对SVM模型进行了训练,并且将模型保存到了data目录下的名为best_svm_50000的文件当中,这样的作用是在赛前训练,训练结束之后赛场上直接load这个文件中的svm模型直接进行使用。其中svm模型各参数的设置也是我‘精心调了好久才达到的比较好的效果’,训练集可以达到98%的准确率,实际测试中也可以实现90%的预测准确率。
对于train_svm函数当中的两个文件,是我从mnist字体库中搞下来的训练集:train_data.txt当中是28*28列5w行数据,对应的train_result.txt当中是5w行样本的结果,生成这两个文件的方法在下面的目录当中附上,因为当前时间有点完了,为了保护程序员生命防止以后老婆孩子留给别人还是早点休息,先写到这里(虽然目前还没有女票)
生成训练数据的代码:
# coding:utf-8 import os import cv2 import codecs from cv2.ml import VAR_ORDERED import numpy as np import cPickle import gzip # decoding:utf-8 def load_data(): mnist = gzip.open(os.path.join('data', 'mnist.pkl.gz'), 'rb') training_data, classification_data, test_data = cPickle.load(mnist) mnist.close() return training_data, classification_data, test_data def wrap_data(): tr_d, va_d, te_d = load_data() # print type(tr_d), type(va_d), type(te_d) training_inputs = [np.reshape(x, (784, 1)) for x in tr_d[0]] training_results = [vectorized_result(y) for y in tr_d[1]] training_data = zip(training_inputs, training_results) validation_inputs = [np.reshape(x, (784, 1)) for x in va_d[0]] validation_data = zip(validation_inputs, va_d[1]) test_input = [np.reshape(x, (784, 1)) for x in te_d[0]] test_data = zip(test_input, te_d[1]) return training_data, validation_data, test_data def vectorized_result(j): e = np.zeros((1, 1)) e[0] = j return e def train(samples=10000, epochs=1): all_data = np.empty((0, 28 * 28)) all_data_result = np.empty((0, 1)) tr, val, test = wrap_data() samples = len(tr) print 'len', samples for x in xrange(epochs): counter = 0 for img in tr: if counter > samples: break if counter % 1000 == 0: print "Epoch %d : Trained %d/%d" % (x, counter, samples) counter += 1 data, digit = img if digit[0][0] == 0: continue all_data = np.append(all_data, np.array([data.ravel()], dtype=np.float32), 0) all_data_result = np.append(all_data_result, np.array([digit.ravel()], dtype=np.float32), 0) print 'Epoch %d complete' % x np.savetxt('train_data.txt', all_data) np.savetxt('train_result.txt', all_data_result) return True def main(): train(50000) if __name__ == '__main__': main()
上述结果训练完成后,我们相当于已经将一个效果十分好的svm模型down到一个文本中去了,然后在我们主函数中要对数字图像进行预测的时候,我们再调用opencv中自带的load svm模型的函数,将这个模型load出来,当然这个部分肯定是没有打大神符的时候已经开始加载的,因为这个模型load的速度很慢,我们只要从步兵车方面receive一个开始信号过来,然后不断地处理拍到的图片,将图片中的密码串和九宫格分别解析出来然后调用svm模型预测一下即可。那我们现在进入到最重要的数字扣取步骤。
首先在此处展现一下我的处理成果:
这是我从视频流中截取出来的一帧图片,对其进行了二值化+高斯模糊+腐蚀之后的效果图,右侧为对应着的预测结果,从图中可以看出,利用训练的svm模型处理下来只有一个1(3位置)被错误识别为了7,其他的全部预测正确。为了防止你们说这是个案,再从视频流中截取出一张照片以及其预测结果:第二张图片中的7(5位置)被错误识别为了9,其他的全部预测正确,首先在这里声明一下我是试过其他很多张图片的,识别的效果是肯定可以保证的,此处的展示并不是个案,同时minist字体库中很多字确实很让人抓狂,比如说第二幅图片中的7,有些人写的7确实跟9的差异度不是很大,所以总的来说我们的正确率还是有保证的。
展示完了结果之后,我们来看一下代码部分:
首先我们要对视频流中的图片进行处理,处理的方法:当前帧图片-> 灰度化-> 高斯模糊-> 二值化->腐蚀:之所以这样处理的是因为大神符的屏幕与周围的干扰景物是有很大差别的,所以我们根据调节合理的阈值,可以将其明显的分割出来,虽然说这样处理下之后的图片当中也存在一些其他的噪声。
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) gray = cv2.GaussianBlur(gray, (7, 7), 0) ret, thbw = cv2.threshold(gray, 200, 255, cv2.THRESH_BINARY_INV) thbw = cv2.erode(thbw, np.ones((2, 2), np.int8), iterations=2)
那么问题来了,在有噪声的情况下,我们如何将九宫格中的数字提取出来呢,这个很简单,我们可以利用opencv 自带的函数cv2.findContours将图片中所有的轮廓都找出来,然后利用函数cv2.boundingRect(contour)解析出这个图片的左上角以及长宽,与我们预设的在打大神符位置处摄像头所能看到的数字的长宽是否在一个合理的阈值范围内即可,事实证明我们经过场面那个步骤处理之后找出轮廓设定阈值找到的是足够正确的,此处赋上整个函数的代码,这个代码的输入为上述处理完之后的thbw,输出为九宫格的九个数字所对应的位置信息(位置信息包括左上角位置以及长宽高):
def findcontours(img): rect = [] ret, thresh = cv2.threshold(img.copy(), 0, 255, cv2.THRESH_BINARY) image, contours, hier = cv2.findContours(thresh, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) # print 'length', len(contours) save_c = [] for k, c in enumerate(contours): x, y, w, h = cv2.boundingRect(c) print x, y, w, h if (w < low_w or h < low_h) or (w > high_w or h > high_h) \ or h >= w: pass else: # print x, y, w, h save_c.append(c) rect.append((x, y, w, h)) print 'length', len(save_c) res = sorted(rect, cmp=cmp) res.reverse() if len(save_c) < 1: return False, [] else: return True, res
上述return的res就是九宫格的位置,我们利用return回来的这9个位置,从thbw上将九个位置对应的部分截取出来,你可以看一下我放出来的两幅预测图片,那个就是处理之后thbw,你们可以看到在它每个数字周围是一个黑色的长方形边框,这个边框就是根据res得到的,此时相当于我们模型已经训练好了,我们的svm也已经load出来了,现在测试集也过来了,我们只需要将截取出来的图片resize成28*28然后调用函数ravel将其展成一行然后放入到svm模型当中,return出的结果就是对与该部分的预测值。详细代码如下:
flag, pos = findcontours(thbw) for i in pos: sample = np.array(thbw[i[1]:i[1] + i[3], i[0]:i[0] + i[2]], dtype=np.float32) try: rectangle(thbw, (i[0], i[1]), (i[0] + i[2], i[1] + i[3]), (0, 255, 0)) cv2.imshow('number', thbw) sample_tem = cv2.resize(sample, (28, 28), interpolation=1) cv2.imshow('预测的图片', sample_tem) a, b = svm.predict(np.array([sample_tem.ravel()], dtype=np.float32)) print b[0][0] except: print '存在异常' while waitKey() != 27: pass end = time.time() print '总共用时',end- start success, frame = cameraCapture.read()
当然这部分代码是接上一部分代码的,看到这里应改存在疑问说:你这些处理以及预测都是对下面的九宫格进行的,你对密码串的处理在哪里呢,其实这个很简单,处理九宫格和处理密码串如出一辙,我们也是对同样一张图片进行 二值化 + 腐蚀(注意此处不和那个九宫格中数字的提取采用相同的二值化是因为这两个没有出现在同一个二值化图片里面或者说如果我们用了一个阈值去提取两部分,反而会让这两部分都变得不准确,我相信大家也能理解,然后我们仍然是根据预设的轮廓大小挑选一下密码串,这个东西我们随便训练一个knn模型就可以轻松解决,这个原因很简单就是一个很 清晰地LED显示数字的方法嘛),所以到了此处,整个步骤基本已经完结了,我们还需要做的就是在视频流中每隔一个很小的时间段采一张照片出来与之前的图片做个比较看看两者是否存在什么样的变化,这个具体的比较方法是:先比较密码串是否相同,因为如果打对了的情况下,密码串不会变化而下面的九宫格的内容会发生变化,而如果之前没有打对,那这一次的密码串就会不同,所以我们可以根据这个密码串正确与否来 保持一个count计数器,看这个count此时应该指向第几个位置,这个很重要,比如说你现在需要打1,而1在九宫格的3位置,此时树莓派发送一个3过来,步兵车receive这个3,然后利用自己训练的经验法从当前位置移动到3位置进行一个击打,这样一整套的流程就完成了。
此处在附上一个密码串处理之后的图片:从这幅图片可以看出这个密码串处理完之后是相当清晰的,我们只需要找合适他大小的轮廓即可。so 到此虽然说没有写的很详尽,也只是把我做的很多工作粗浅的写了出来,我希望一些有一定基础的(当然这个基础不用很深)的同学可以根据我的成果,稍稍做一些优化或者补充,就可以完成一个十分完美的打大符的流程出来,同时有需要源码或者知识方面援助的其他队伍或者对此感兴趣的人都可以联系我进行一下交流或者我把git给出来share一下,希望留下的队伍继续努力,让一些更高深的机器视觉(大神符,装甲识别,飞机自动停降等等)更多的出现在赛场上,而不仅仅是一些机械方面的比赛(当然机械方面的部分同样十分高深和重要因为我不会,咩哈哈哈~),希望其它队伍表现越来越好,也希望中大逸仙狮战队在以后的RM比赛当中也能有更加出色的表现,加油