微信跳一跳好像火了很久了茄唐,我才開始接觸,作為一個(gè)手殘黨, 玩了幾次不到20分驹暑,打算直接放棄了或衡。但作為一個(gè)技術(shù)宅,下一反應(yīng)肯定是“上腳本”宣增。最初想法是用arduino(不會(huì)可以學(xué)懊蛋颉)+樹梅派,樹梅派用OpenCV處理攝像頭爹脾,讀取手機(jī)屏幕帖旨,判斷跳躍距離,發(fā)命令給arduino控制機(jī)械手臂(觸控筆)灵妨,任務(wù)完成解阅。但問(wèn)題來(lái)了
- 不會(huì)arduino,也沒(méi)有arduino和觸控筆泌霍。
- 沒(méi)有樹梅派用的攝像頭货抄。
- 不會(huì)OpenCV
arduino控制觸控筆,估計(jì)很麻煩朱转,但同時(shí)肯定很有意思蟹地。就算入手arduino,學(xué)會(huì)控制機(jī)械手臂藤为,難度好像是整個(gè)程序中最難的怪与。然后退一步,用adb給屏幕發(fā)送模擬觸屏命令缅疟,暫時(shí)解決第一部分分别。樹梅派的攝像頭也要買,暫時(shí)用筆記本攝像頭代替(把手機(jī)舉在攝像頭前真的很二)存淫,再退一步耘斩,用adb截屏讀取圖像,處理圖像比視頻流要簡(jiǎn)單些纫雁。然后開始作煌往,哎倾哺,我肯定不是第一個(gè)想到這些的轧邪,去搜搜,果然就找到了這里3羞海。具體做法是:
- 用adb讀取游戲截屏
- 根據(jù)顏色差判斷棋子和下一跳棋盤的位置忌愚。計(jì)算兩點(diǎn)之間的距離
- 找到合適的參數(shù),得到屏幕按壓時(shí)間却邓,用adb把命令發(fā)過(guò)去硕糊。
這里最關(guān)鍵的地方是判斷棋子和下一跳棋盤的位置,因?yàn)樽畛蹙褪且蒙螼penCV,他人處理顏色的代碼看了兩眼覺(jué)得太麻煩就直接放棄了简十。所以這篇日志的最主要的地方就是怎么用OpenCV找到這兩個(gè)點(diǎn)檬某。
從最開始寫這個(gè)腳本到開始寫個(gè)記錄,不斷搜索到新的方法來(lái)找這兩個(gè)點(diǎn)螟蝙。最近的發(fā)現(xiàn)2, 這里用了Tensorflow恢恼,對(duì)此代碼我還理解不能。我打算放棄寫這篇記錄胰默,認(rèn)真看看TF场斑。 轉(zhuǎn)念一想,我的方法雖然沒(méi)有其他人的高大上牵署,也沒(méi)什么效率漏隐,但總算是一種解決方式,所以還是記下來(lái)吧奴迅。
OpenCV的全稱是Open Source Computer Vision Library青责,是一個(gè)跨平臺(tái)的計(jì)算機(jī)視覺(jué)庫(kù),可用于開發(fā)實(shí)時(shí)的圖像處理取具、計(jì)算機(jī)視覺(jué)以及模式識(shí)別程序1爽柒。聽介紹就知道用在這里很合適。根據(jù)OpenCV的官方教程4者填, 初步確定尋找所需的兩個(gè)點(diǎn)的方法:
- 將圖像的色彩空間由RGB轉(zhuǎn)換HSV
- 確定圖形邊緣及輪廓
- 假定其中一個(gè)輪廓的中心就是所需的點(diǎn)浩村。
為什么要把色彩空間轉(zhuǎn)化為HSV,而不是使用原始的RGB或者灰度占哟? 據(jù)說(shuō)HSV比RGB能更好的處理顏色心墅,處理一個(gè)值H比三個(gè)RGB要簡(jiǎn)單,飽和度S和明度V能幫助處理光照和陰影榨乎。下圖可以看出怎燥,只有轉(zhuǎn)成HSV色彩空間的在后面的處理中可以正確的檢測(cè)到方塊的頂面邊緣,同樣的閾值條件下蜜暑,RGB模式對(duì)頂面和右側(cè)面的邊緣檢測(cè)能力較弱铐姚,灰度模式下頂面和兩個(gè)側(cè)面都分不清楚。而降噪處理在這張圖沒(méi)什么明顯的變化肛捍,但對(duì)于有些圖像--比如有木頭紋理的桌子--還是有幫助的隐绵,所以需要保留,高斯模糊應(yīng)該就足夠了拙毫,Bilateral Filtering is highly effective in noise removal while keeping edges sharp. But the operation is slower compared to other filters依许, 所以沒(méi)必要。
根據(jù)上面的圖缀蹄,即便有了邊緣點(diǎn)峭跳,得到很多個(gè)輪廓的中心膘婶,仍然無(wú)法確定哪個(gè)中心點(diǎn)是棋子。根據(jù)經(jīng)驗(yàn)蛀醉,發(fā)現(xiàn)棋子總是某種黑紫色悬襟,而不論是下一跳的棋盤或者背景都不會(huì)跟這種顏色相近,所以根據(jù)顏色特征找到棋子更簡(jiǎn)單拯刁。所以接下來(lái)就是找到這個(gè)顏色值的上下限古胆,面向Google編程,我找到了別人的一個(gè)方法筛璧,自己修改后放gist逸绎。
def get_start_point(image):
chess = image.copy()
blurred = cv2.GaussianBlur(chess, (3, 3), 1)
mask = cv2.inRange(blurred, lower_purple, upper_purple)
mask_1 = cv2.inRange(blurred, lower_purple_1, upper_purple_1)
masked = cv2.bitwise_and(blurred, blurred, mask=mask+mask_1)
chess_edges = cv2.Canny(masked, 100, 200)
thresh = cv2.adaptiveThreshold(
chess_edges,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
7,
1)
_, cnts, _ = cv2.findContours(
thresh, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
cs = list()
for cnt in cnts:
(x, y), r = cv2.minEnclosingCircle(cnt)
r = int(r)
# print(r)
if 25 < r < 40:
cs.append((int(x), int(y)))
mx = min(cs, key=operator.itemgetter(1))
return mx
邊緣檢測(cè)函數(shù)就是上面的Canny()
,該方法的參數(shù)有三個(gè)夭谤,第一個(gè)是要處理的圖像棺牧,后兩個(gè)是閾值的上下限。Canny
之前的是標(biāo)準(zhǔn)的選取顏色方法朗儒。有了邊緣值再用adaptiveThreshold
函數(shù)進(jìn)行二值化處理颊乘,然后用findContours
尋找棋子的輪廓, 輪廓(Contour)在這里其實(shí)就是一系列邊緣點(diǎn)的列表,根據(jù)這些點(diǎn)就可以計(jì)算輪廓的很多特性醉锄,比如形狀乏悄,面積等,OpenCV有很多函數(shù)幫助處理這些特性恳不, 在這里可以用boundingRect
(包括了所有輪廓點(diǎn)的長(zhǎng)方形)檩小,minEnclosingCircle
(包括了所有輪廓點(diǎn)的圓)或 moments
(輪廓的幾何重心),。因?yàn)槠灞P頂了個(gè)球烟勋,并且minEnclosingCircle
同時(shí)返回圓心坐標(biāo)和半徑规求,在這里最合適不過(guò)了。由于findContours
是返回的所有輪廓卵惦,很是雜亂阻肿,如圖右下里的紅圈,所以要過(guò)濾下沮尿,根據(jù)半徑過(guò)濾是最直接的想法丛塌,因?yàn)榍虻陌霃娇梢詼y(cè)出來(lái),在1080p的屏幕上大概為30個(gè)像素畜疾。這樣就能得到球心坐標(biāo)了赴邻,如同左下的綠圈,注意:1. 即使過(guò)濾后也可能有多個(gè)坐標(biāo)庸疾,這里取最上面的乍楚,就是mx = min(cs, key=operator.itemgetter(1))
当编。2.這個(gè)坐標(biāo)不是起始點(diǎn)的坐標(biāo)届慈,起始點(diǎn)應(yīng)該為棋子底部圓心徒溪,y坐標(biāo)應(yīng)加上120px(圖的左上為坐標(biāo)原點(diǎn))。
有了棋子的位置坐標(biāo)金顿,接下來(lái)就是找到下一跳的棋盤的頂部中心坐標(biāo)了臊泌,方法大概是這樣的:如果前一跳正好落在了當(dāng)前棋盤的中心,下一跳棋盤的中心會(huì)有個(gè)白點(diǎn)(這樣每一跳的分?jǐn)?shù)也會(huì)遞增揍拆,朋友圈排行上數(shù)k的分?jǐn)?shù)都是外掛刷來(lái)的)渠概,只要找到這個(gè)白點(diǎn)就ok了。尋找這個(gè)白點(diǎn)跟找棋子一樣也是通過(guò)顏色特征white_dot = np.array([0, 0, 245])
嫂拴。但沒(méi)有這個(gè)白點(diǎn)之前還是要用檢測(cè)邊緣的辦法播揪,然后計(jì)算棋盤的頂面中心,如果沒(méi)有噪音筒狠,這個(gè)方法和直接找到白點(diǎn)坐標(biāo)應(yīng)該是一樣的猪狈,如果沒(méi)有噪音。處理圖像的步驟還是一樣辩恼,有個(gè)細(xì)節(jié)可以減少計(jì)算量:棋子和下一跳的棋盤總是在屏幕中心的兩側(cè)雇庙,利用這點(diǎn)可以把棋盤從圖上挖出來(lái)
def get_end_point(image, start_point):
global half_width
global cutted
cutted = image.copy()
if start_point[0] < half_width:
#棋子在屏幕左側(cè),取棋子坐標(biāo)右側(cè)的圖
cutted = cutted[0:start_point[1] +
chess_height, start_point[0]+chess_width:]
else:
cutted = cutted[0:start_point[1] +
chess_height, :start_point[0]-chess_width]
w_dot = _get_end_dot(cutted)
if w_dot is None:
print("get by edges")
w_dot = _get_end_by_edges(cutted)
x, y = w_dot
### 最后得到坐標(biāo)要還原回去
if start_point[0] < half_width:
x += start_point[0]+chess_width
return (x, y)
上面_get_end_dot()
跟get_start_point
類似灶伊,_get_end_by_edges
是在沒(méi)找到白點(diǎn)的情況下的笨方法疆前,_get_ul
方法來(lái)自這里。findContours
方法也和前面不太一樣聘萨,返回輪廓的同時(shí)還返回了輪廓的層次結(jié)構(gòu)竹椒,因?yàn)橹恍枰獌?nèi)側(cè)的輪廓,用ch[2] < 0
過(guò)濾出來(lái)米辐。關(guān)于這個(gè)的 解釋碾牌。 最后還要處理一部分噪音造成的太小的輪廓圓及整個(gè)棋盤+陰影造成的太大的圓。
def _get_ul(c, sigma=0.15):
l = int(max(0, (1.0 - sigma) * c))
u = int(min(255, (1.0 + sigma) * c))
return (l, u)
def _get_end_by_edges(image):
image = cv2.GaussianBlur(image, (1, 1), 0)
bg = image[:20, :]
mid = np.median(bg)
l, u = _get_ul(mid)
end_edges = cv2.Canny(image, l, u)
end_thresh = cv2.adaptiveThreshold(
end_edges,
255,
cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY,
5,
1)
_, cnts, hierarchy = cv2.findContours(
end_thresh, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
hierarchy = hierarchy[0]
ms = list()
for cpnt in zip(cnts, hierarchy):
cnt = cpnt[0]
ch = cpnt[1]
if ch[2] < 0:
(x, y), r = cv2.minEnclosingCircle(cnt)
r = int(r)
# print(r)
if 25 < r < 230:
ms.append((int(x), int(y)))
mx = min(ms, key=operator.itemgetter(1))
return mx
利用檢測(cè)邊緣的方法尋找中心點(diǎn)的效果如下:?jiǎn)紊倚螤钜?guī)則些的可以找到正中心或偏差不大儡循。奇形怪狀的連能不能跳到上面都不保證舶吗。
主要方法就是這些了,整個(gè)代碼可以去我的gist查看择膝。整個(gè)代碼還是會(huì)有些問(wèn)題誓琼,有些能改進(jìn)的地方,但是作為一個(gè)演示程序應(yīng)該夠了肴捉。我測(cè)試了幾次腹侣,都沒(méi)到1k,汗齿穗。但是不管跑多少分傲隶,都會(huì)遇到下圖(當(dāng)然我的歷史最高分也是腳本跑了,只是當(dāng)時(shí)還沒(méi)有現(xiàn)在的外掛檢測(cè))窃页,反正除了測(cè)試改進(jìn)代碼跺株,我是不會(huì)用這個(gè)跑分了复濒。
如果有興趣,我可能會(huì)增加點(diǎn)關(guān)于python的環(huán)境配置乒省。