Changeset - 5f4489f36388
[Not reviewed]
default
0 3 1
Laman - 8 years ago 2017-01-07 14:01:04

restored showing the game state, split out message passing
4 files changed with 54 insertions and 63 deletions:
0 comments (0 inline, 0 general)
src/core.py
Show inline comments
 
import multiprocessing
 
import logging as log
 
import PIL
 
from util import MsgQueue
 
from gui import gui
 
from image_analyzer import ImageAnalyzer
 
from go import Go
 

	
 

	
 
class Core:
 
	def __init__(self):
 
		self.grid=None
 
		self.go=Go()
 
		self.detector=ImageAnalyzer()
 
		self.tresW=60.0
 
		self.tresB=30.0
 

	
 
		self._msgQueue=multiprocessing.Queue()
 
		self._guiQueue=multiprocessing.Queue()
 
		self._incomingEvent=multiprocessing.Event()
 
		self._guiEvent=multiprocessing.Event()
 
		self._ownMessages=MsgQueue(self._handleEvent)
 
		self._guiMessages=MsgQueue()
 

	
 
		self._frame=PIL.Image.open("../images/7.jpg")
 

	
 
		self._guiProc=multiprocessing.Process(name="gui", target=gui, args=(self._guiQueue,self._guiEvent,self._msgQueue,self._incomingEvent))
 
		self._guiProc=multiprocessing.Process(name="gui", target=gui, args=(self._guiMessages,self._ownMessages))
 
		self._guiProc.start()
 
		self._guiQueue.put(("setCurrentFrame",(self._frame,),dict()))
 
		self._guiEvent.set()
 
		self._guiMessages.send("setCurrentFrame",(self._frame,))
 

	
 
	def setCorners(self,corners):
 
		self.detector.setGridCorners(corners)
 
		self.detector.analyze(self._frame)
 
		for r in self.detector.board: log.info(r)
 
		self._guiMessages.send("setGameState",(self.detector.board,))
 

	
 
	def setTresholds(self,tresB=None,tresW=None):
 
		if tresB is not None: self.tresB=tresB
 
		if tresW is not None: self.tresW=tresW
 
		if self.detector.analyze(self._frame):
 
			self._guiMessages.send("setGameState",(self.detector.board,))
 

	
 
	def listen(self):
 
		while True:
 
			self._incomingEvent.wait()
 
			msg=self._msgQueue.get()
 
			if self._msgQueue.empty():
 
				self._incomingEvent.clear()
 
			log.info(msg)
 
			self._handleEvent(msg)
 
		self._ownMessages.listen()
 

	
 
	def _handleEvent(self,e):
 
		actions={"setCorners":self.setCorners, "setTresholds":self.setTresholds}
 
		(actionName,args,kwargs)=e
 

	
 
		return actions[actionName](*args,**kwargs)
 

	
 
core=Core()
 
core.listen()
 

	
 
"""
 
core
 
====
 
grid
 
go
 
imageAnalyzer
 

	
 

	
 
gui
 
===
 
corners
 

	
 
a) keeps references to important objects and uses them
 
b) gets and sets all relevant data through method calls with core
 

	
 
GUI
 
<- addCorner(corner)
 
-> redrawImgView(img,grid)
 
<- refreshTresholds(tresB,tresW)
 

	
 
BoardView
 
-> redrawState(go)
 

	
 

	
 
core-gui: just pass messages with relevant data (!! always pass object copies, don't share instances)
 
"""
 
\ No newline at end of file
src/gui/__init__.py
Show inline comments
 
import threading
 
import logging as log
 
import tkinter as tk
 
from PIL import ImageTk
 
import PIL
 
 
import config
 
from epoint import EPoint
 
from corners import Corners
 
import image_analyzer
 
from go import Go
 
from grid import Grid
 
 
 
class MainWindow(tk.Frame):
 
	def __init__(self,parent,master=None):
 
		self.parent=parent
 
		self.corners=Corners()
 
 
		self.currentFrame=None
 
		self._boardGrid=None
 
		self.gameState=None
 
 
		self.img=None
 
		self._imgSizeCoef=1
 
		self._imgShift=EPoint(0,0)
 
 
		tk.Frame.__init__(self, master)
 
		self.grid(column=0,row=0)
 
		self._createWidgets()
 
 
	def setCurrentFrame(self,frame):
 
		self.currentFrame=frame
 
 
		w=int(self.imgView['width'])
 
		h=int(self.imgView['height'])
 
		self.img=ImageTk.PhotoImage(frame.resize((w,h),resample=PIL.Image.BILINEAR))
 
 
		wo,ho=self.currentFrame.size # o for original
 
		widthRatio=wo/w
 
		heightRatio=ho/h
 
		self._imgSizeCoef=max(widthRatio,heightRatio)
 
		# shift compensates possible horizontal or vertical empty margins from unmatching aspect ratios
 
		self._imgShift=EPoint(wo-w*self._imgSizeCoef,ho-h*self._imgSizeCoef)/2
 
 
	def setGameState(self,gameState):
 
		pass
 
 
	def setCallbacks(self,setCorners,setTresholds):
 
		self.cornersCallback=setCorners
 
		self.tresholdsCallback=setTresholds
 
 
	def _createWidgets(self):
 
		# a captured frame with overlay graphics
 
		self.imgView=tk.Canvas(self)
 
		self.imgView.configure(width=480,height=360)
 
 
		self.imgView.bind('<1>',lambda e: self.addCorner(e.x,e.y))
 
 
		self.imgView.grid(column=0,row=0)
 
 
		# board with detected stones
 
		self.boardView=BoardView(self)
 
		self.boardView.grid(column=1,row=0)
 
 
		# more controls below the board
 
		self.scaleTresB=tk.Scale(self, orient=tk.HORIZONTAL, length=200, from_=0.0, to=100.0, command=self.refreshTresholds)
 
		self.scaleTresW=tk.Scale(self, orient=tk.HORIZONTAL, length=200, from_=0.0, to=100.0, command=self.refreshTresholds)
 
		self.scaleTresB.set(30.0) # !! proper defaults
 
		self.scaleTresW.set(60.0)
 
		self.scaleTresB.grid(column=0,row=1,columnspan=2)
 
		self.scaleTresW.grid(column=0,row=2,columnspan=2)
 
 
		# render everything
 
		self.redrawImgView()
 
 
	## Stores a grid corner located at x,y coordinates.
 
	def addCorner(self,x,y):
 
		self.corners.add(x,y)
 
		if self.corners.canonizeOrder():
 
			# transform corners from show coordinates to real coordinates
 
			log.debug(self.corners.corners)
 
			self._boardGrid=Grid(self.corners.corners)
 
			corners=[(c*self._imgSizeCoef+self._imgShift) for c in self.corners.corners]
 
			self.parent.sendMsg("setCorners",(corners,))
 
		# 	self.boardGrid=Grid(self.corners)
 
		# 	self.boardView.setBoardGrid(self.boardGrid)
 
		#
 
 
		self.redrawImgView()
 
 
	## Redraws the current image and its overlay.
 
	def redrawImgView(self):
 
		if self.currentFrame and self.img:
 
			self.imgView.create_image(2,2,anchor="nw",image=self.img)
 
 
		for corner in self.corners.corners:
 
			self.markPoint(corner.x,corner.y)
 
 
		if self._boardGrid!=None and config.gui.showGrid:
 
			for r in range(19):
 
				a=self._boardGrid.intersections[r][0]
 
				b=self._boardGrid.intersections[r][-1]
 
				self.imgView.create_line(a.x,a.y,b.x,b.y,fill='#00ff00')
 
			for c in range(19):
 
				a=self._boardGrid.intersections[0][c]
 
				b=self._boardGrid.intersections[-1][c]
 
				self.imgView.create_line(a.x,a.y,b.x,b.y,fill='#00ff00')
 
 
		if self._boardGrid!=None and config.gui.showBigPoints:
 
			for r in range(19):
 
				for c in range(19):
 
					((r1,c1),(r2,c2))=image_analyzer.relevantRect(self._boardGrid.intersections[r][c], *(self._boardGrid.stoneSizeAt(r, c)))
 
					self.imgView.create_rectangle(r1,c1,r2,c2,outline="#00ffff")
 
 
		self.imgView.grid()
 
 
	## Marks a point at the image with a green cross. Used for corners.
 
	def markPoint(self,x,y):
 
		self.imgView.create_line(x-3,y-3,x+4,y+4,fill="#00ff00")
 
		self.imgView.create_line(x-3,y+3,x+4,y-4,fill="#00ff00")
 
 
		self.imgView.grid()
 
 
	def refreshTresholds(self,_):
 
		self.parent.sendMsg("setTresholds",tuple(),{"tresB":self.scaleTresB.get(), "tresW":self.scaleTresW.get()})
 
 
 
## Handles and presents the game state as detected by the program.
 
class BoardView(tk.Canvas):
 
	def __init__(self, master=None):
 
		# self.detector=ImageAnalyzer()
 
 
		tk.Canvas.__init__(self, master)
 
		self.configure(width=360,height=360,background="#ffcc00")
 
 
		self.drawGrid()
 
 
		self.grid()
 
 
	def redrawState(self,gameState):
 
		# !! will need to remove old stones or redraw the element completely
 
		for r,row in enumerate(gameState):
 
			for c,point in enumerate(row):
 
				self.drawStone(r,c,point)
 
 
		self.grid()
 
 
	## Redraws and reananalyzes the board view.
 
	# def redrawState(self,img,sizeCoef,shift):
 
	# 	self.create_rectangle(0,0,360,360,fill="#ffcc00")
 
	# 	self.drawGrid()
 
	#
 
	# 	self.detector.analyze(img,sizeCoef,shift)
 
	#
 
	# 	for r,row in enumerate(self.detector.board):
 
	# 		for c,point in enumerate(row):
 
	# 			self.drawStone(r,c,point)
 
	#
 
	# 	self.grid()
 
 
	def drawGrid(self):
 
		for i in range(19):
 
			self.create_line(18,18*(i+1),360-18,18*(i+1),fill="#000000") # rows
 
			self.create_line(18*(i+1),18,18*(i+1),360-18,fill="#000000") # cols
 
 
		self.drawStars()
 
 
	def drawStars(self):
 
		for r in range(4,19,6):
 
			for c in range(4,19,6):
 
				self.create_oval(r*18-2,c*18-2,r*18+2,c*18+2,fill='#000000')
 
 
	## Draws a stone at provided coordinates.
 
	#
 
	#  For an unknown color draws nothing and returns False.
 
	#
 
	#  @param r row coordinate, [0-18], counted from top
 
	#  @param c column coordinate, [0-18], counted from left
 
	#  @param color color indicator, Go.BLACK or Go.WHITE
 
	def drawStone(self,r,c,color):
 
		if color==Go.BLACK: hexCode='#000000'
 
		elif color==Go.WHITE: hexCode='#ffffff'
 
		else: return False
 
		r+=1
 
		c+=1
 
		self.create_oval(c*18-9,r*18-9,c*18+9,r*18+9,fill=hexCode)
 
 
 
class GUI:
 
	def __init__(self):
 
		self.root = tk.Tk()
 
		self.root.title("OneEye {0}.{1}.{2}".format(*config.misc.version))
 
 
		self.outcomingQueue=None
 
		self.outcomingEvent=None
 
		self._coreMessages=None
 
 
		self.mainWindow = MainWindow(self,master=self.root)
 
 
		self.root.bind("<<redrawImgView>>", lambda e: self.mainWindow.redrawImgView())
 
		self.root.bind("<<receiveState>>", lambda e: print("fired receiveState"))
 
 
	def __call__(self,incomingQueue,incomingEvent,outcomingQueue,outcomingEvent):
 
		self.outcomingQueue=outcomingQueue
 
		self.outcomingEvent=outcomingEvent
 
	def __call__(self,ownMessages,coreMessages):
 
		self._coreMessages=coreMessages
 
 
		self.listenerThread=threading.Thread(target=lambda: self._listen(incomingQueue,incomingEvent))
 
		self.listenerThread=threading.Thread(target=lambda: ownMessages.listen(self._handleEvent))
 
		self.listenerThread.start()
 
 
		self.mainWindow.mainloop()
 
 
	def sendMsg(self,actionName,args=tuple(),kwargs=dict()):
 
		self.outcomingQueue.put((actionName,args,kwargs))
 
		self.outcomingEvent.set()
 
 
	def _listen(self,incomingQueue,incomingEvent):
 
		while True:
 
			incomingEvent.wait()
 
			msg=incomingQueue.get()
 
			if incomingQueue.empty():
 
				incomingEvent.clear()
 
			log.info(msg)
 
			self._handleEvent(msg)
 
	def sendMsg(self,actionName,args=tuple(),kwargs=None):
 
		self._coreMessages.send(actionName,args,kwargs)
 
 
	def _handleEvent(self,e):
 
		actions={"setCurrentFrame":self._frameHandler}
 
		actions={"setCurrentFrame":self._frameHandler, "setGameState":self._stateHandler}
 
		(actionName,args,kwargs)=e
 
 
		return actions[actionName](*args,**kwargs)
 
 
	def _frameHandler(self,newFrame):
 
		self.mainWindow.setCurrentFrame(newFrame)
 
		self.root.event_generate("<<redrawImgView>>")
 
 
	def _stateHandler(self,e):
 
		pass
 
	def _stateHandler(self,gameState):
 
		self.mainWindow.boardView.redrawState(gameState)
 
 
gui=GUI()
src/image_analyzer.py
Show inline comments
 
import logging as log
 
from grid import Grid
 
from go import Go
 
 
 
class ImageAnalyzer:
 
 
	def __init__(self,tresB=30,tresW=60):
 
		self.board=[[Go.EMPTY]*19 for r in range(19)]
 
		self.grid=None
 
 
		self.tresB=tresB
 
		self.tresW=tresW
 
 
	# let's not concern ourselves with sizecoef and shift here anymore. we want corners to come already properly recomputed
 
	def analyze(self,image):
 
		if self.grid==None: return False
 
		if self.grid==None:
 
			log.info("ImageAnalyzer.analyze() aborted: no grid available.")
 
			return False
 
 
		for r in range(19):
 
			for c in range(19):
 
				intersection=self.grid.intersections[r][c]
 
 
				self.board[r][c]=self.analyzePoint(image,r,c,intersection,*(self.grid.stoneSizeAt(r,c)))
 
 
		boardStr="\n".join(str(row) for row in self.board)
 
		log.info("board analyzed:\n%s",boardStr)
 
 
	def analyzePoint(self,image,row,col,imageCoords,stoneWidth,stoneHeight):
 
		b=w=e=0
 
 
		((x1,y1),(x2,y2))=relevantRect(imageCoords,stoneWidth,stoneHeight)
 
 
		for y in range(y1,y2+1):
 
			for x in range(x1,x2+1):
 
				red,green,blue=image.getpixel((x,y))
 
 
				I=(red+green+blue)/255/3
 
				m=min(red,green,blue)
 
				S=1-m/I
 
				if 100*I<self.tresB: b+=1
 
				elif 100*I>self.tresW: w+=1
 
				else: e+=1
 
 
		log.debug("(%d,%d) ... (b=%d,w=%d,e=%d)", row, col, b, w, e)
 
 
		if b>=w and b>=e: return Go.BLACK
 
		if w>=b and w>=e: return Go.WHITE
 
		return Go.EMPTY
 
 
	def setGridCorners(self,corners):
 
		self.grid=Grid(corners)
 
 
 
def relevantRect(imageCoords,stoneWidth,stoneHeight):
 
	x=int(imageCoords.x)
 
	y=int(imageCoords.y)
 
	xmax=max(int(stoneWidth*2//7), 2) # !! optimal parameters subject to further research
 
	ymax=max(int(stoneHeight*2//7), 2)
 
 
	return ((x-xmax,y-ymax), (x+xmax,y+ymax))
src/util.py
Show inline comments
 
new file 100644
 
import multiprocessing
 
import logging as log
 

	
 

	
 
class MsgQueue:
 
	def __init__(self,handler=None):
 
		self._queue=multiprocessing.Queue()
 
		self._event=multiprocessing.Event()
 
		self._handleEvent=handler
 

	
 
	def send(self,actionName,args=tuple(),kwargs=None):
 
		if kwargs is None: kwargs=dict()
 
		self._queue.put((actionName,args,kwargs))
 
		self._event.set()
 

	
 
	def listen(self,handleEvent=None):
 
		if handleEvent is not None: self._handleEvent=handleEvent
 

	
 
		while True:
 
			self._event.wait()
 
			msg=self._queue.get()
 
			if self._queue.empty():
 
				self._event.clear()
 
			log.info(msg)
 
			self._handleEvent(msg)
 

	
 
	def setHandler(self,handler):
 
		self._handleEvent=handler
0 comments (0 inline, 0 general)