白云苍狗看复鹿 冥鸿相助见神龙
金庸先生在《鹿鼎记》里讲到,韦小宝因机缘巧合拜了诸多高明师傅,但只对铁剑门绝技“神行百变”大感兴趣,学得津津有味。虽说“神行”差得远,“百变”两字却和他天性很是相近。小宝虽然是市井出身,可仗着圆滑的处事之道和主角光环,做了朝廷的公爵、天地会青木堂主、神龙教白龙使,典故成语说得鲁鱼亥豕,做事倒是很照顾朋友。出生入死盗得八部四十二章经,把里面的碎羊皮拼凑成图,按图到了鹿鼎山,却忽然害怕起来,只盼找个甚麽借口,离得越远越好。一方面是胆怯,一方面是怕挖了龙脉坏了朋友小玄子康熙的性命。
百余年后,我得到DragonBoard™410c的板子,想来想去,决定做个给拼板自动打分的游戏。拼版就是这个:
这个东西叫四巧板,是一种类似七巧板的传统智力玩具。包括大小不同的直角梯形各一块,等腰直角三角形一块,凹五边形一块。这几个多边形的内角除了有直角外,还有45º、135º和270º的角,每个角度都是45度的倍数。四块板子间有着非常默契的关系,可以把整个四巧板看成由四个相同的等腰直角三角形和相对应的直角梯形组成。由于四块板子之间相互呼应的科学合理的结构,所以可以排列组合出成百种图形,训练和提高玩者的图形思维和智力。
应用的组件结构和操作流程是这样的:
回到DragonBoard,拿到板子之前,自然是做了不少作业:
快递送来板子那天,我小心翼翼的打开箱子:
拆开一看。纳尼?少东西!
少的是电源到板上电源插孔的转接头。经过和群里童鞋的讨论,得知了准确的型号,于是果断下单。然而身在帝都的我赶上了全世界的一件大事,于是快递就悠闲地在祖国大好河山旅游了一下下,时间比我年假都长。终于,它到了。
于是就上电,完全没有用烧好的micro sd,直接就起来了:
桌面是lxde,有评测说是最快的精简桌面。我更关心安装的组件有哪些,因为我要用的工作语言是python,原意是大蟒,用来配DragonBoard很搭哦。
当然,既然要做到能给拼板自动打分,就要用到视频视觉的库。说起来,既然用高通的芯,FastCV是最好的选择。但是鉴于时间有限,并且我现在只会使一种锤子,所以很不好意思的说,用的库是大家耳熟能详的——
先检查,gpio支持,不用管内核了,usb摄像头也认:
空间也够:
接下来,开始各种装:
有了opencv,最大的库算是更新完了,以后再有缺的,随时apt-get之。
此时可以用这个小程序验证opencv和摄像头的工作是否正常:
import cv2
cap = cv2.VideoCapture(0)
ret,frame=cap.read()
cv2.imshow('img',frame)
cv2.waitKey(0)
cap.release()
cv2.destroyAllWindows()
补充一句,python环境版本是2.7.9。
注意,此处没有华丽丽的分割线,但是下面开始就是干货了……
玩过树莓派的同学一定听说过RPi.GPIO,可惜那不是纯py的,要移植。考虑到我要用到的也就是个按钮,于是决定用python直接操作文件的方式来实现:
# -*- coding: utf-8 -*- class SimpleGPIO(): def __init__(self,gpio=None): self._base_path= "/sys/class/gpio/" self._gpio = gpio #self._direction = None #self._edge = None #self._last_value = None def setGPIO(self,gpio): self._gpio = gpio def getGPIO(self): return self._gpio def _writeFile(self,file_name,value): fo = open(file_name, "w") fo.write(value) fo.close() def _readFile(self,file_name): fo = open(file_name, "r") value = fo.read(7) fo.close() return value def export(self): self._writeFile( self._base_path+"export" , str(self._gpio) ) def unexport(self): self._writeFile( self._base_path+"unexport" , str(self._gpio) ) #in或out。写入low或high def _setDirection(self,direction): self._writeFile( self._base_path+"gpio"+str(self._gpio)+"/direction" , direction) def getDirection(self): return self._readFile( self._base_path+"gpio"+str(self._gpio)+"/direction") #”none”, “rising”, “falling”,”both” def _setEdge(self,edge): self._writeFile( self._base_path+"gpio"+str(self._gpio)+"/edge" , edge) def getEdge(self): return self._readFile( self._base_path+"gpio"+str(self._gpio)+"/edge" ) #setup gpio def setGPIOoutHIGH(self): self._setDirection("high") def setGPIOoutLOW(self): self._setDirection("out")#self._setDirection("low") def setGPIOin(self): self._setDirection("in") self._setEdge( "none") def setGPIOinRaising(self): self._setDirection("in") self._setEdge("rising") def setGPIOinFalling(self): self._setDirection("in") self._setEdge("falling") def setGPIOinBoth(self): self._setDirection("in") self._setEdge("both") # 1 , 0 def _setValue(self,value): self._writeFile( self._base_path+"gpio"+str(self._gpio)+"/value" , value ) def setValueHigh(self): self._setValue(self,'1') def setValueLow(self): self._setValue(self,'0') def getValue(self): return self._readFile( self._base_path+"gpio"+str(self._gpio)+"/value" )
|
下一个问题是界面用什么做。Opencv提供的窗口虽然刷新速度比较快,但是我这个水平,界面元素全靠画,太费劲。决定采用python广谱抗菌随身携带家居旅行必备的——TKinter。
Tkinter最大的优势是历史悠久,装了python就能支持;劣势也有不少,不过这个demo级的应用对这些缺点还真不太敏感。
先一条,有点想一下界面元素:我想用一个满屏的窗口显示,在屏幕顶端是一行提示,很窄的,有点windows或mac os窗口标题的那种意思。下面的屏幕分成左右两部分,左边是网上下载的题目,右边是定时刷新的摄像头快照。如果按了评分键,会在快照上层显示出一个评分。那么这样一个窗口需要如何实现呢?
class GameApp(object): def __init__(self, master, **kwargs): self.root = master self.frame = tk.Frame(self.root,bg='red') self.frame.pack(fill=tk.BOTH) self.statsbar = tk.Label(self.frame, text=TXT_TIPS,bg='black',fg = 'orange',font=('system', 16, "bold"),compound = tk.CENTER) self.statsbar.pack(fill=tk.X) win_screen_w = self.root.winfo_screenwidth() win_screen_h = self.root.winfo_screenheight() self.lbl_que=tk.Label(self.frame, text='',bg='black', bitmap="info" ,width = win_screen_w/2,compound = tk.CENTER, image=img_que) self.lbl_que.pack(side=tk.LEFT,fill=tk.Y) self.lbl_cap=tk.Label(self.frame, text='',bg='white', bitmap="question",width = win_screen_w/2,compound = tk.CENTER, image=img_cap, height=win_screen_h,font=('system', 40, "bold"),fg='red' ) self.lbl_cap.pack(fill=tk.BOTH,side=tk.RIGHT) self.root.bind("<Escape>", self.exit_application) self.root.bind("<KeyPress>", self.on_key_press) self.refreshCapture() def do_fullscreen(self): #self.root.attributes("-fullscreen", True) return def exit_application(self, event=None): self.root.destroy() return def on_key_press(self, event=None): print (event.char,event.keysym) return
|
有了界面,我们可以把重点放到主要逻辑上了。
首先是抓图,使用cv2.VideoCapture(0)可以获得摄像头,再调用摄像头的read()方法就可获得一帧,需要注意的是opencv的图像颜色顺序是BGR,如果交给tk显示,需要调整成RGB。
global img_cap,img_cv_cap cap = cv2.VideoCapture(0) ret, img = cap.read() cap.release() img_cv_cap = img.copy() #Rearrang the color channel im = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGBA)) img_cap = ImageTk.PhotoImage(image=im) self.lbl_cap.configure(image=img_cap) |
下一个关键点是定时抓图,这个是靠Tk的.after()方法实现。after()方法有两个参数,第一个是sleep的毫秒数,第二个是响应过程,比如本demo实现定时拍照刷新就是用这句实现的:
self.root.after(100,self.refreshCapture) |
最后一个关键问题是如何比较拼出的图形与标准图形是否相似,这里用到了cv2.matchShapes方法,返回的值越小说明相似程度越大。本来在做形状匹配之前,需要对照片进行转换色彩空间、按颜色准备模板、抠图这些操作。但是由于我用的是淡黄色拼版,而且是放在黑色背景上做自动识别,因此省了不少力气,只需要转换成灰度图即可。
def calc_diff(self,img1,img2,th=127): img1b=cv2.cvtColor(img1,cv2.COLOR_BGR2GRAY) img2b=cv2.cvtColor(img2,cv2.COLOR_BGR2GRAY) ret, thresh1 = cv2.threshold(img1b, th, 255,0) ret, thresh2 = cv2.threshold(img2b, th, 255,0) ret,contours1,hierarchy = cv2.findContours(thresh1,2,1) ret,contours2,hierarchy = cv2.findContours(thresh2,2,1) if ( len(contours1)<=0 or len(contours2)<=0 ): return 100 cnt1 = contours1[0] cnt2 = contours2[0] ret = cv2.matchShapes(cnt1,cnt2,1,0.0) return ret |
如果环境昏暗,背景和拼版的反差会变小,因此阈值th不能取得太大,大约80就可以了。80是在天刚黑的室内环境实验出来的,如果80都分辨不出来,那也太暗了,可以忽略这种情况。