skip to main | skip to sidebar

Thursday, July 24, 2014

OpenCV Python: 2048 Game Solver

A sample application of Digit Recognition.
2048 solver algorithm based on term2048-AI

import cv2
import numpy as np
import win32api, win32gui, win32ui, win32con, win32com.client
from PIL import Image, ImageFont, ImageDraw, ImageOps
# create training model based on the given TTF font file
# http://projectproto.blogspot.com/2014/07/opencv-python-digit-recognition.html
def createDigitsModel(fontfile, digitheight):
 font = ImageFont.truetype(fontfile, digitheight)
 samples = np.empty((0,digitheight*(digitheight/2)))
 responses = []
 for n in range(10):
 pil_im = Image.new("RGB", (digitheight, digitheight*2))
 ImageDraw.Draw(pil_im).text((0, 0), str(n), font=font)
 pil_im = pil_im.crop(pil_im.getbbox())
 pil_im = ImageOps.invert(pil_im)
 #pil_im.save(str(n) + ".png")
 # convert to cv image
 cv_image = cv2.cvtColor(np.array( pil_im ), cv2.COLOR_RGBA2BGRA)
 gray = cv2.cvtColor(cv_image, cv2.COLOR_BGR2GRAY)
 blur = cv2.GaussianBlur(gray,(5,5),0)
 thresh = cv2.adaptiveThreshold(blur,255,1,1,11,2)
 roi = cv2.resize(thresh,(digitheight,digitheight/2))
 responses.append( n )
 sample = roi.reshape((1,digitheight*(digitheight/2)))
 samples = np.append(samples,sample,0)
 samples = np.array(samples,np.float32)
 responses = np.array(responses,np.float32)
 
 model = cv2.KNearest()
 model.train(samples,responses)
 return model
class Board(object):
 UP, DOWN, LEFT, RIGHT = 1, 2, 3, 4
 FONT = "font/ClearSans-Bold.ttf"
 def __init__(self, clientwindowtitle):
 self.hwnd = self.getClientWindow(clientwindowtitle)
 if not self.hwnd:
 return
 self.hwndDC = win32gui.GetWindowDC(self.hwnd)
 self.mfcDC = win32ui.CreateDCFromHandle(self.hwndDC)
 self.saveDC = self.mfcDC.CreateCompatibleDC()
 self.cl, self.ct, right, bot = win32gui.GetClientRect(self.hwnd)
 self.cw, self.ch = right-self.cl, bot-self.ct
 self.cl += win32api.GetSystemMetrics(win32con.SM_CXSIZEFRAME)
 self.ct += win32api.GetSystemMetrics(win32con.SM_CYSIZEFRAME)
 self.ct += win32api.GetSystemMetrics(win32con.SM_CYCAPTION)
 self.ch += win32api.GetSystemMetrics(win32con.SM_CYSIZEFRAME)*2
 self.saveBitMap = win32ui.CreateBitmap()
 self.saveBitMap.CreateCompatibleBitmap(self.mfcDC, self.cw, self.ch)
 self.saveDC.SelectObject(self.saveBitMap)
 self.tiles, self.tileheight, self.contour = self.findTiles(self.getClientFrame())
 if not len(self.tiles):
 return
 self.digitheight = self.tileheight / 2
 self.digitsmodel = createDigitsModel(self.FONT, self.digitheight)
 self.update()
 def getClientWindow(self, windowtitle):
 toplist, winlist = [], []
 def enum_cb(hwnd, results):
 winlist.append((hwnd, win32gui.GetWindowText(hwnd)))
 win32gui.EnumWindows(enum_cb, toplist)
 window = [(hwnd, title) for hwnd, title in winlist if windowtitle.lower() in title.lower()]
 if not len(window):
 return 0
 return window[0][0]
 def getClientFrame(self):
 self.saveDC.BitBlt((0, 0), (self.cw, self.ch),
 self.mfcDC, (self.cl, self.ct), win32con.SRCCOPY)
 bmpinfo = self.saveBitMap.GetInfo()
 bmpstr = self.saveBitMap.GetBitmapBits(True)
 pil_img = Image.frombuffer( 'RGB',
 (bmpinfo['bmWidth'], bmpinfo['bmHeight']),
 bmpstr, 'raw', 'BGRX', 0, 1)
 array = np.array( pil_img )
 cvimage = cv2.cvtColor(array, cv2.COLOR_RGBA2BGRA)
 return cvimage
 def findTiles(self, cvframe):
 tiles, avgh = [], 0
 
 gray = cv2.cvtColor(cvframe,cv2.COLOR_BGRA2GRAY)
 thresh = cv2.adaptiveThreshold(gray,255,1,1,11,2)
 contours, hierarchy = cv2.findContours(thresh.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
 def findBoard(contours): # get largest square
 ww, sqcnt = 10, None
 for cnt in contours:
 x,y,w,h = cv2.boundingRect(cnt)
 if w>ww and abs(w-h)<w/10:
 ww = w
 sqcnt = cnt
 return sqcnt
 board = findBoard(contours)
 if board==None:
 print 'board not found!'
 return tiles, avgh, board
 
 bx,by,bw,bh = cv2.boundingRect(board)
 #cv2.rectangle(cvframe,(bx,by),(bx+bw,by+bh),(0,255,0),2)
 #cv2.imshow('board',cvframe)
 #cv2.waitKey(0)
 #cv2.destroyWindow( 'board' ) 
 maxh = bh/4
 minh = (maxh*4)/5
 count = 0
 for contour in contours:
 x,y,w,h = cv2.boundingRect(contour)
 if y>by and w>minh and w<maxh and h>minh and h<maxh:
 avgh += h
 count += 1
 if not count:
 print 'no tile found!'
 return tiles, avgh, board
 avgh = avgh / count
 margin = (bh-avgh*4)/5
 for row in range(4):
 for col in range(4):
 x0 = bx + avgh*col + margin*(col+1)
 x1 = x0 + avgh
 y0 = by + avgh*row + margin*(row+1)
 y1 = y0 + avgh
 tiles.append([x0, y0, x1, y1])
 #cv2.rectangle(cvframe,(x0,y0),(x1,y1),(0,255,0),2)
 #cv2.imshow('tiles',cvframe)
 #cv2.waitKey(0)
 #cv2.destroyWindow( 'tiles' ) 
 return tiles, avgh, board
 
 def getTileThreshold(self, tileimage):
 gray = cv2.cvtColor(tileimage,cv2.COLOR_BGR2GRAY)
 row, col = gray.shape
 tmp = gray.copy().reshape(1, row*col)
 counts = np.bincount(tmp[0])
 sort = np.sort(counts)
 modes, freqs = [], []
 for i in range(len(sort)):
 freq = sort[-1-i]
 if freq < 4:
 break
 mode = np.where(counts==freq)[0][0]
 modes.append(mode)
 freqs.append(freq)
 bg, fg = modes[0], modes[0]
 for i in range(len(modes)):
 fg = modes[i]
 #if abs(bg-fg)>=48:
 if abs(bg-fg)>32 and abs(fg-150)>4: # 150?!
 break
 #print bg, fg
 if bg>fg: # needs dark background ?
 tmp = 255 - tmp
 bg, fg = 255-bg, 255-fg
 
 tmp = tmp.reshape(row, col)
 ret, thresh = cv2.threshold(tmp,(bg+fg)/2,255,cv2.THRESH_BINARY) 
 return thresh
 
 def getTileNumbers(self, cvframe):
 numbers = []
 outframe = np.zeros(cvframe.shape,np.uint8)
 def guessNumber(digits):
 for i in range(1,16):
 nn = 2**i
 ss = str(nn)
 dd = [int(c) for c in ss]
 if set(digits) == set(dd):
 return nn
 return 0
 
 for tile in self.tiles:
 x0,y0,x1,y1 = tile
 tileimage = cvframe[y0:y1,x0:x1]
 cv2.rectangle(cvframe,(x0,y0),(x1,y1),(0,255,0),2)
 cv2.rectangle(outframe,(x0,y0),(x1,y1),(0,255,0),1)
 thresh = self.getTileThreshold(tileimage)
 contours,hierarchy = cv2.findContours(thresh.copy(),cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)
 dh = self.digitheight
 digits = []
 for cnt in contours:
 x,y,w,h = cv2.boundingRect(cnt)
 if h>w and h>(dh*1)/5 and h<(dh*6)/5:
 cv2.rectangle(cvframe,(x0+x,y0+y),(x0+x+w,y0+y+h),(0,0,255),1)
 roi = thresh[y:y+h,x:x+w]
 roi = cv2.resize(roi,(dh,dh/2))
 roi = roi.reshape((1,dh*(dh/2)))
 roi = np.float32(roi)
 retval, results, neigh_resp, dists = self.digitsmodel.find_nearest(roi, k=1)
 digit = int((results[0][0]))
 string = str(digit)
 digits.append(digit)
 cv2.putText(outframe,string,(x0+x,y0+y+h),0,float(h)/24,(0,255,0))
 numbers.append(guessNumber(digits))
 return numbers, outframe
 def getWindowHandle(self):
 return self.hwnd
 def getBoardContour(self):
 return self.contour
 
 def update(self):
 frame = self.getClientFrame()
 self.tilenumbers, outframe = self.getTileNumbers(frame)
 return self.tilenumbers, frame, outframe
 def copyTileNumbers(self):
 return self.tilenumbers[:]
 def getCell(self, tiles, x, y):
 return tiles[(y*4)+x]
 def setCell(self, tiles, x, y, v):
 tiles[(y*4)+x] = v
 return tiles
 def getCol(self, tiles, x):
 return [self.getCell(tiles, x, i) for i in range(4)]
 def setCol(self, tiles, x, col):
 for i in range(4):
 self.setCell(tiles, x, i, col[i])
 return tiles
 def getLine(self, tiles, y):
 return [self.getCell(tiles, i, y) for i in range(4)]
 def setLine(self, tiles, y, line):
 for i in range(4):
 self.setCell(tiles, i, y, line[i])
 return tiles
 def validMove(self, tilenumbers, direction):
 if direction == self.UP or direction == self.DOWN:
 for x in range(4):
 col = self.getCol(tilenumbers, x)
 for y in range(4):
 if(y < 4-1 and col[y] == col[y+1] and col[y]!=0):
 return True
 if(direction == self.DOWN and y > 0 and col[y] == 0 and col[y-1]!=0):
 return True
 if(direction == self.UP and y < 4-1 and col[y] == 0 and col[y+1]!=0):
 return True
 if direction == self.LEFT or direction == self.RIGHT:
 for y in range(4):
 line = self.getLine(tilenumbers, y)
 for x in range(4):
 if(x < 4-1 and line[x] == line[x+1] and line[x]!=0):
 return True
 if(direction == self.RIGHT and x > 0 and line[x] == 0 and line[x-1]!=0):
 return True
 if(direction == self.LEFT and x < 4-1 and line[x] == 0 and line[x+1]!=0):
 return True
 return False
 def moveTileNumbers(self, tilenumbers, direction):
 def collapseline(line, direction):
 if (direction==self.LEFT or direction==self.UP):
 inc = 1
 rg = xrange(0, 4-1, inc)
 else:
 inc = -1
 rg = xrange(4-1, 0, inc)
 pts = 0
 for i in rg:
 if line[i] == 0:
 continue
 if line[i] == line[i+inc]:
 v = line[i]*2
 line[i] = v
 line[i+inc] = 0
 pts += v
 return line, pts
 def moveline(line, directsion):
 nl = [c for c in line if c != 0]
 if directsion==self.UP or directsion==self.LEFT:
 return nl + [0] * (4 - len(nl))
 return [0] * (4 - len(nl)) + nl
 
 score = 0
 if direction==self.LEFT or direction==self.RIGHT:
 for i in range(4):
 origin = self.getLine(tilenumbers, i)
 line = moveline(origin, direction)
 collapsed, pts = collapseline(line, direction)
 new = moveline(collapsed, direction)
 tilenumbers = self.setLine(tilenumbers, i, new)
 score += pts
 elif direction==self.UP or direction==self.DOWN:
 for i in range(4):
 origin = self.getCol(tilenumbers, i)
 line = moveline(origin, direction)
 collapsed, pts = collapseline(line, direction)
 new = moveline(collapsed, direction)
 tilenumbers = self.setCol(tilenumbers, i, new)
 score += pts
 
 return score, tilenumbers 
# AI based on "term2048-AI"
# https://github.com/Nicola17/term2048-AI
class AI(object):
 def __init__(self, board):
 self.board = board
 
 def nextMove(self):
 tilenumbers = self.board.copyTileNumbers()
 m, s = self.nextMoveRecur(tilenumbers[:],3,3)
 return m
 def nextMoveRecur(self, tilenumbers, depth, maxDepth, base=0.9):
 bestMove, bestScore = 0, -1
 for m in range(1,5):
 if(self.board.validMove(tilenumbers, m)):
 score, newtiles = self.board.moveTileNumbers(tilenumbers[:], m)
 score, critical = self.evaluate(newtiles)
 newtiles = self.board.setCell(newtiles,critical[0],critical[1],2)
 if depth != 0:
 my_m,my_s = self.nextMoveRecur(newtiles[:],depth-1,maxDepth)
 score += my_s*pow(base,maxDepth-depth+1)
 if(score > bestScore):
 bestMove = m
 bestScore = score
 
 return bestMove, bestScore
 def evaluate(self, tilenumbers, commonRatio=0.25):
 maxVal = 0.
 criticalTile = (-1, -1)
 
 for i in range(8):
 linearWeightedVal = 0
 invert = False if i<4 else True
 weight = 1.
 ctile = (-1,-1)
 cond = i%4
 for y in range(4):
 for x in range(4):
 if cond==0:
 b_x = 4-1-x if invert else x
 b_y = y
 elif cond==1:
 b_x = x
 b_y = 4-1-y if invert else y
 elif cond==2:
 b_x = 4-1-x if invert else x
 b_y = 4-1-y
 elif cond==3:
 b_x = 4-1-x
 b_y = 4-1-y if invert else y
 
 currVal=self.board.getCell(tilenumbers,b_x,b_y)
 if(currVal == 0 and ctile == (-1,-1)):
 ctile = (b_x,b_y)
 linearWeightedVal += currVal*weight
 weight *= commonRatio
 invert = not invert
 
 if linearWeightedVal > maxVal:
 maxVal = linearWeightedVal
 criticalTile = ctile
 return maxVal, criticalTile
 def solveBoard(self, moveinterval=500):
 boardHWND = self.board.getWindowHandle()
 if not boardHWND:
 return False
 bx, by, bw, bh = cv2.boundingRect(self.board.getBoardContour())
 x0, x1, y0, y1 = bx, bx+bw, by, by+bh
 win32gui.SetForegroundWindow(boardHWND)
 shell = win32com.client.Dispatch('WScript.Shell')
 print 'Set the focus to the Game Window, and the press this arrow key:'
 keymove = ['UP', 'DOWN', 'LEFT', 'RIGHT']
 delay = moveinterval / 3 # milliseconds delay to cancel board animation effect
 prev_numbers = []
 while True:
 numbers, inframe, outframe = self.board.update()
 if numbers != prev_numbers:
 cv2.waitKey(delay)
 numbers, inframe, outframe = self.board.update()
 if numbers == prev_numbers: # recheck if has changed
 continue
 prev_numbers = numbers
 move = ai.nextMove()
 if move:
 key = keymove[move-1]
 shell.SendKeys('{%s}'%key)
 print key
 cv2.waitKey(delay)
 cv2.imshow('CV copy',inframe[y0:y1,x0:x1])
 cv2.imshow('CV out', outframe[y0:y1,x0:x1])
 cv2.waitKey(delay)
 cv2.destroyWindow( 'CV copy' )
 cv2.destroyWindow( 'CV out' )
 
# http://gabrielecirulli.github.io/2048/
# http://ov3y.github.io/2048-AI/
board = Board("2048 - Google Chrome")
#board = Board("2048 - Mozilla Firefox")
ai = AI(board)
ai.solveBoard(360)
print 'stopped.'
download: 2048opencv.py

# open this site with Chrome: http://gabrielecirulli.github.io/2048/
board = Board("2048 - Google Chrome")
ai = AI(board)
ai.solveBoard()

demo video:
[フレーム]

==================================
update:
Solving the Android version of 2048..
Android phone is running a VNC server (vnc server & viewer are both slow!).
[フレーム]
python script: 2048opencv_adb.py

tip: send [touchscreen]swipe event through adb:
adb shell input swipe x1 y1 x2 y2


sources:
gabrielecirulli.github
diaryofatinker.blogspot
stackoverflow.com


Posted by 'yus at 3:05 PM

9 comments:

  1. WizBidzPenny Auction Wizbidz, Auction,
    Live Online Auctions,
    antique Auctions, electronic Auctions, bid site, penny Auction, deals, camera Auction,
    laptop Auction, appliances Auction, best tv deals, tools, coin Auctions,jewelry Auctions , Internet Auctions sites ,home, mobile Auctions, sale

    Reply Delete
  2. When I run this I get:

    D:\>python 2048.py
    2048.py:109: FutureWarning: comparison to `None` will result in an elementwise o
    bject comparison in the future.
    if board==None:
    no tile found!
    Set the focus to the Game Window, and the press this arrow key:

    Then nothing. I have the game grid open in Chrome. Any suggestions?

    Reply Delete
    Replies
    1. Maybe you need to write:
      from ctypes import windll
      user32 = windll.user32
      user32.SetProcessDPIAware()
      at the beginning of your programm

      Delete



  3. Caralase - Women's Clothing Online Store in USA. Shop latest trends apparels from our wide range of maxi dresses , tops , skirts , , , , ,, , , , , and more at best prices.


    Reply Delete
  4. This comment has been removed by the author.

    Reply Delete
  5. I had this program does not work. When i transformed it, it worked. https://github.com/AlexanderBiryuckoff/2048bot

    Reply Delete

Subscribe to: Post Comments (Atom)
 

AltStyle によって変換されたページ (->オリジナル) /