Changeset - 2b850618ba88
[Not reviewed]
default
1 7 0
Laman - 6 years ago 2018-12-13 18:02:07

refactoring and debugging GUI
8 files changed with 48 insertions and 39 deletions:
0 comments (0 inline, 0 general)
src/core.py
Show inline comments
 
import multiprocessing
 
import threading
 
import logging
 

	
 
import PIL
 

	
 
import config as cfg
 
from util import MsgQueue
 
from gui import gui
 
from analyzer import ImageAnalyzer
 
from analyzer.framecache import FrameCache
 
from go.core import Go, isLegalPosition
 
from statebag import StateBag
 
from video import capVideo
 

	
 
log=logging.getLogger(__name__)
 

	
 

	
 
class Core:
 
	def __init__(self):
 
		self.grid=None
 
		self.go=Go()
 
		self.detector=ImageAnalyzer()
 
		self._cache=FrameCache()
 
		self.states=StateBag()
 

	
 
		self._ownMessages=MsgQueue("Core",self._handleEvent)
 
		self._guiMessages=MsgQueue("GUI")
 
		self._vidMessages=MsgQueue("Video")
 
		self._listenerThread=None
 

	
 
		self._frame=None
 

	
 
		self._guiProc=multiprocessing.Process(name="gui", target=gui, args=(self._guiMessages,self._ownMessages))
 
		self._guiProc.start()
 
		self._vidProc=multiprocessing.Process(
 
			name="video",
 
			target=capVideo,
 
			args=(cfg.misc.video, self._vidMessages,self._ownMessages)
 
		)
 
		self._vidProc.start()
 

	
 
	def fetchFrame(self,key):
 
		frame=self._cache.get(key)
 
		if frame is None:
 
			(key,frame)=self._cache.getRelative(10)
 
		self._guiMessages.send("setFrame", (frame.copy(), gui.PREVIEW, key))
 

	
 
	def putFrame(self,frame):
 
		self._frame=PIL.Image.fromarray(frame)
 
		k=self._cache.put(self._frame)
 
		self._guiMessages.send("setFrame", (self._frame, gui.RECORDING, k))
 
		self._guiMessages.send("setFrame", (self._frame, gui.REAL, k))
 
		self.analyze()
 

	
 
	def sendParams(self):
 
		self._guiMessages.send("setParams",(self.detector.params.copy(),))
 

	
 
	def setParams(self,params):
 
		self.detector.setParams(params)
 

	
 
	def analyze(self):
 
		if self.detector.analyze(self._frame):
 
			if isLegalPosition(self.detector.board):
 
				state=self.states.pushState(self.detector.board)
 
				rec=[]
 
				if state:
 
					rec=state.exportRecord()
 
					log.debug("progressive game record: %s",rec)
 
				self._guiMessages.send("setGameState", (self.detector.board,rec))
 

	
 
				self.go.transitionMove(self.detector.board)
 
				log.debug("conservative game record: %s",self.go._record)
 
			else:
 
				log.info("illegal position detected")
 

	
 
	def listen(self):
 
		self._listenerThread=threading.Thread(target=lambda: self._ownMessages.listen())
 
		self._listenerThread.start()
 

	
 
	def joinChildren(self):
 
		self._guiProc.join()
 
		self._vidMessages.send("shutDown")
 
		self._vidProc.join()
 
		self._ownMessages.send("!kill",("core",))
 
		self._listenerThread.join()
 
		log.info("Core exiting.")
 

	
 
	def _handleEvent(self,e):
 
		actions={
 
			"fetchFrame": self.fetchFrame,
 
			"putFrame": self.putFrame,
 
			"fetchParams": self.sendParams,
 
			"setParams": self.setParams
 
		}
 
		(actionName,args,kwargs)=e
 

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

	
 
if __name__=="__main__":
 
	core=Core()
 
	core.listen()
 
	log.info("OneEye started.")
 
	core.joinChildren()
 
	log.info("OneEye done.")
src/gui/__init__.py
Show inline comments
 
import logging
 
import threading
 
import tkinter as tk
 

	
 
import config
 
from analyzer import ImageAnalyzer
 
from .mainwindow import MainWindow
 
from .boardview import BoardView
 
from .settings import Settings
 

	
 
log=logging.getLogger(__name__)
 

	
 

	
 
class GUI:
 
	SETUP=PREVIEW=0
 
	RECORDING=REAL=1
 

	
 
	def __init__(self):
 
		self.root = tk.Tk()
 
		self.root.title("OneEye {0}.{1}.{2}".format(*config.misc.version))
 
		self.root.option_add('*tearOff',False) # for menu
 

	
 
		self.detector=ImageAnalyzer()
 
		self._frame=None
 
		self._frameKey=0
 

	
 
		self._ownMessages=None
 
		self._coreMessages=None
 

	
 
		self._state=GUI.SETUP
 
		self._state=GUI.RECORDING
 

	
 
		self.mainWindow = MainWindow(self, master=self.root)
 
		self.mainWindow = MainWindow(self)
 
		self.settings=None
 
		self.root.columnconfigure(0,weight=1)
 
		self.root.rowconfigure(0,weight=1)
 

	
 
		self.root.bind("<<redrawImgView>>", lambda e: self.mainWindow.redrawImgView())
 
		self.root.bind("<<setUp>>", lambda e: self.setUp())
 
		self.root.bind("<<setRecording>>", lambda e: self.setRecording())
 
		self.root.bind("<F12>",lambda e: Settings(self))
 
		self.mainWindow.bind("<Destroy>",lambda e: self._shutDown())
 

	
 
		self.setUp()
 
		self.setRecording()
 

	
 
	def __call__(self,ownMessages,coreMessages):
 
		self._ownMessages=ownMessages
 
		self._coreMessages=coreMessages
 

	
 
		self._listenerThread=threading.Thread(target=lambda: ownMessages.listen(self._handleEvent))
 
		self._listenerThread.start()
 

	
 
		self.mainWindow.mainloop()
 

	
 
	def sendMsg(self,actionName,args=tuple(),kwargs=None):
 
		self._coreMessages.send(actionName,args,kwargs)
 

	
 
	def setUp(self):
 
		self.mainWindow.setUp()
 
		self.root.bind("<Left>",lambda e: self.sendMsg("prevFrame"))
 
		self.root.bind("<Right>",lambda e: self.sendMsg("nextFrame"))
 
		self._state=GUI.SETUP
 

	
 
	def setRecording(self):
 
		self.mainWindow.setRecording()
 
		self.root.bind("<Left>",lambda e: None)
 
		self.root.bind("<Right>",lambda e: None)
 
		if self.settings:
 
			self.settings.destroy()
 
			self.settings=None
 
		self.sendParams()
 
		if self._coreMessages: self.sendParams()
 
		self._state=GUI.RECORDING
 

	
 
	def sendParams(self):
 
		self.sendMsg("setParams",(self.detector.params.copy(),))
 

	
 
	def preview(self):
 
		if self.detector.analyze(self._frame):
 
			self.mainWindow.boardView.redrawState(self.detector.board)
 

	
 
	def _shutDown(self):
 
		log.info("GUI proc exiting.")
 
		self._ownMessages.send("!kill",("gui",))
 
		self._listenerThread.join()
 

	
 
	def _handleEvent(self,e):
 
		actions={
 
			"setFrame": self._frameHandler,
 
			"setGameState": self._stateHandler,
 
			"setParams": self._paramsHandler
 
		}
 
		(actionName,args,kwargs)=e
 

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

	
 
	def _frameHandler(self,frame,type,key):
 
		if self._state!=type and self.mainWindow.imgView.isSet():
 
			log.info("ignored setFrame event, wrong type")
 
			return
 
		self._frame=frame
 
		self._frameKey=key
 
		self.mainWindow.setFrame(frame)
 
		self.root.event_generate("<<redrawImgView>>")
 

	
 
	def _stateHandler(self,gameState,moves):
 
		if self._state==GUI.SETUP:
 
			log.info("ignored incoming gameState")
 
			return
 
		labels={(row,col):(i+1) for (i,(c,row,col)) in enumerate(moves)}
 
		self.mainWindow.boardView.redrawState(gameState,labels)
 

	
 
	def _paramsHandler(self,params):
 
		if not self.settings:
 
			log.warning("received 'setParams' message while settings is '%s'",str(self.settings))
 
			return
 
		self.detector.setParams(params)
 
		self.settings.setParams(params)
 

	
 

	
 
gui=GUI()
 

	
 
"""
 
# setup #
 
* we can click around the ImgView
 
* we can walk through the frames back and forth
 
* BoardView is showing what the reading of ImgView _would_ be
 
* core is reading and analyzing frames, pushing results to StateBag, but not showing them
 

	
 
# recording #
 
* ImgView is showing the current picture, is not clickable
 
* BoardView is showing last detected position
 
* on switch to recording (if parameters have changed):
 
	* feed analyzer new parameters and start using them
 
	* in the background reanalyze cached frames with the new parameters and merge them into StateBag
 
"""
src/gui/imgview.py
Show inline comments
 
import logging
 

	
 
from PIL import ImageTk
 

	
 
import config
 
from .resizablecanvas import ResizableCanvas
 
from analyzer.corners import Corners
 
from analyzer.epoint import EPoint
 
from analyzer.grid import Grid
 
import analyzer
 

	
 
log=logging.getLogger(__name__)
 

	
 

	
 
class ImgView(ResizableCanvas):
 
	def __init__(self,master=None,parent=None):
 
	def __init__(self,gui,master=None):
 
		super().__init__(master)
 

	
 
		self._parent=parent
 
		self._gui=gui
 
		self._corners=Corners()
 
		self._boardGrid=None
 

	
 
		self._img=None
 
		self._tkImg=None
 

	
 
		self.configure(width=480,height=360)
 
		self.bind('<1>',lambda e: self.addCorner(e.x,e.y))
 

	
 
	## Redraws the current image and its overlay.
 
	def redraw(self):
 
		self.delete("all")
 

	
 
		if self._img:
 
			img=self._img.resize((int(self._width),int(self._height)))
 
			w,h=self._img.size
 
			ratio=min(self._width/w, self._height/h)
 
			img=self._img.resize((int(w*ratio),int(h*ratio)))
 
			self._tkImg=ImageTk.PhotoImage(img) # just to save the image from garbage collector
 
			self.create_image(self._width//2, self._height//2, anchor="center", image=self._tkImg)
 

	
 
		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.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.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))=analyzer.relevantRect(self._boardGrid.intersections[r][c], *(self._boardGrid.stoneSizeAt(r, c)))
 
					self.create_rectangle(r1,c1,r2,c2,outline="#00ffff")
 

	
 
	def setImg(self,img):
 
		self._img=img
 

	
 
	## Stores a grid corner located at x,y coordinates.
 
	def addCorner(self,x,y):
 
		self._corners.add(x,y)
 
		log.debug("click on %d,%d",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=[self._transformPoint(c) for c in self._corners.corners]
 
			self._parent.detector.setCorners(corners)
 
			self._gui.detector.setCorners(corners)
 
			self._gui.preview()
 

	
 
		self.redraw()
 

	
 
	## Marks a point at the image with a green cross. Used for corners.
 
	def markPoint(self,x,y):
 
		self.create_line(x-3,y-3,x+4,y+4,fill="#00ff00")
 
		self.create_line(x-3,y+3,x+4,y-4,fill="#00ff00")
 

	
 
	def setUp(self):
 
		self.bind('<1>',lambda e: self.addCorner(e.x,e.y))
 

	
 
	def setRecording(self):
 
		self.bind('<1>',lambda e: None)
 

	
 
	def isSet(self):
 
		return self._img is not None
 

	
 
	def _onResize(self,event):
 
		w=self._width
 
		super()._onResize(event)
 
		self._corners.scale(self._width/w)
 
		if len(self._corners.corners)==4:
 
			self._boardGrid=Grid(self._corners.corners)
 
		self.redraw()
 

	
 
	def _transformPoint(self,point):
 
		w=int(self._width)
 
		h=int(self._height)
 
		wo,ho=self._img.size # o for original
 
		log.debug("image: %sx%s, view: %sx%s",wo,ho,w,h)
 
		widthRatio=wo/w
 
		heightRatio=ho/h
 
		self._imgSizeCoef=max(widthRatio,heightRatio)
 
		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
 
		return EPoint(self.canvasx(point.x),self.canvasy(point.y)) * self._imgSizeCoef + self._imgShift
 
		imgShift=EPoint(wo-w*imgSizeCoef,ho-h*imgSizeCoef)/2
 
		return EPoint(self.canvasx(point.x),self.canvasy(point.y)) * imgSizeCoef + imgShift
src/gui/mainwindow.py
Show inline comments
 
import tkinter as tk
 
from tkinter import N,S,E,W
 

	
 
from .util import MsgMixin
 
from .menu import MainMenu
 
from .boardview import BoardView
 
from .imgview import ImgView
 
from .statusbar import StatusBar
 

	
 

	
 
class MainWindow(tk.Frame,MsgMixin):
 
	def __init__(self,parent,master=None):
 
class MainWindow(tk.Frame):
 
	def __init__(self,parent):
 
		self.parent=parent
 

	
 
		tk.Frame.__init__(self, master)
 
		tk.Frame.__init__(self, parent.root)
 
		self.grid(column=0,row=0,sticky=(N,S,E,W))
 
		self._createWidgets()
 

	
 
	def setFrame(self,frame):
 
		self.imgView.setImg(frame)
 

	
 
	def _createWidgets(self):
 
		# menu
 
		self.parent.root.option_add('*tearOff',False)
 
		self._menu=MainMenu(self.parent,self.parent.root)
 
		self._menu=MainMenu(self.parent)
 

	
 
		# a captured frame with overlay graphics
 
		self._imgWrapper=tk.Frame(self,width=480,height=360)
 
		self.imgView=ImgView(self._imgWrapper,self)
 
		self.imgView=ImgView(self.parent,self._imgWrapper)
 

	
 
		self._imgWrapper.grid(column=0,row=0,sticky=(N,S,E,W))
 

	
 
		# board with detected stones
 
		self._boardWrapper=tk.Frame(self,width=360,height=360)
 
		self.boardView=BoardView(self._boardWrapper)
 
		self._boardWrapper.grid(column=1,row=0,sticky=(N,S,E,W))
 

	
 
		self.columnconfigure(0,weight=1)
 
		self.columnconfigure(1,weight=1)
 
		self.rowconfigure(0,weight=1)
 

	
 
		self._statusBar=StatusBar(self)
 
		self._statusBar.grid(column=0,row=1,columnspan=2,sticky=(S,E,W))
 

	
 
		self.rowconfigure(1,weight=0)
 

	
 
		# render everything
 
		self.imgView.redraw()
 

	
 
	## Redraws the current image and its overlay.
 
	def redrawImgView(self):
 
		self.imgView.redraw()
 

	
 
	def setUp(self):
 
		self._statusBar.setUp()
 
		self.imgView.setUp()
 

	
 
	def setRecording(self):
 
		self._statusBar.setRecording()
 
		self.imgView.setRecording()
src/gui/menu.py
Show inline comments
 
import tkinter as tk
 

	
 
from .util import MsgMixin
 
from .settings import Settings
 

	
 

	
 
class MainMenu(MsgMixin):
 
	def __init__(self,parent,root):
 
		self.root=root
 
		self.parent=parent
 
class MainMenu:
 
	def __init__(self,gui):
 
		self.gui=gui
 
		self.root=gui.root
 
		self._createWidgets()
 

	
 
	def _createWidgets(self):
 
		bar=self._createTopBar()
 

	
 
		file = tk.Menu(bar)
 
		help_ = tk.Menu(bar)
 
		bar.add_cascade(menu=file, label='File')
 
		bar.add_cascade(menu=help_, label='Help')
 

	
 
		file.add_command(label="Settings",command=lambda: Settings(self.parent))
 
		file.add_command(label="Settings",command=lambda: Settings(self.gui))
 

	
 
		help_.add_command(label="About")
 

	
 
	def _createTopBar(self):
 
		menubar = tk.Menu(self.root)
 
		self.root['menu'] = menubar
 
		return menubar
src/gui/settings.py
Show inline comments
 
import tkinter as tk
 
from tkinter import N,S,E,W,LEFT
 

	
 
from .util import MsgMixin
 

	
 

	
 
class Settings(tk.Toplevel,MsgMixin):
 
	def __init__(self,parent):
 
		self.parent=parent
 
class Settings(tk.Toplevel):
 
	def __init__(self,gui):
 
		self._gui=gui
 

	
 
		tk.Toplevel.__init__(self, parent.root)
 
		tk.Toplevel.__init__(self, gui.root)
 

	
 
		self.title("Settings | OneEye")
 

	
 
		self.columnconfigure(0,weight=1)
 
		self.content=tk.Frame(self)
 
		self.content.grid(column=0,row=0,sticky=(N,S,E,W))
 
		self.content.columnconfigure(0,weight=1)
 
		self._create()
 
		self.parent.settings=self
 
		self.parent.root.event_generate("<<setUp>>")
 
		self.parent.sendMsg("fetchParams")
 
		self._gui.settings=self
 
		self._gui.root.event_generate("<<setUp>>")
 
		self._gui.sendMsg("fetchParams")
 

	
 
	def _create(self):
 
		self.scaleTresB=tk.Scale(self.content, orient=tk.HORIZONTAL, length=200, from_=0.0, to=100.0, command=self.refreshTresholds)
 
		self.scaleTresW=tk.Scale(self.content, orient=tk.HORIZONTAL, length=200, from_=0.0, to=100.0, command=self.refreshTresholds)
 

	
 
		blackLabel=tk.Label(self.content,text="Black stone treshold (intensity)")
 
		whiteLabel=tk.Label(self.content,text="White stone treshold (intensity)")
 
		blackLabel.grid(column=0,row=0)
 
		self.scaleTresB.grid(column=0,row=1,sticky=(E,W))
 
		whiteLabel.grid(column=0,row=2)
 
		self.scaleTresW.grid(column=0,row=3,sticky=(E,W))
 

	
 
		self.buttonFrame=tk.Frame(self.content)
 
		self.buttonFrame.grid(column=0,row=4,sticky=(W,))
 
		self.confirmButton=tk.Button(self.buttonFrame,text="OK",command=self.sendParams)
 
		self.cancelButton=tk.Button(self.buttonFrame,text="Cancel",command=lambda: self.destroy())
 
		self.confirmButton.pack(side=LEFT)
 
		self.cancelButton.pack(side=LEFT)
 

	
 
	def refreshTresholds(self,_):
 
		params=self.parent.detector.params
 
		params=self._gui.detector.params
 
		params.tresB=self.scaleTresB.get()
 
		params.tresW=self.scaleTresW.get()
 
		self._gui.preview()
 

	
 
	def setParams(self,params):
 
		self.scaleTresB.set(params.tresB)
 
		self.scaleTresW.set(params.tresW)
 

	
 
	def sendParams(self):
 
		self.parent.sendParams()
 
		self._gui.sendParams()
 
		self.destroy()
src/gui/util.py
Show inline comments
 
deleted file
src/util.py
Show inline comments
 
import random
 
import multiprocessing
 
import logging
 

	
 
log=logging.getLogger(__name__)
 

	
 
EMPTY=0
 
BLACK=1
 
WHITE=-1
 

	
 
colorNames={BLACK:"B",WHITE:"W"}
 

	
 

	
 
class MsgQueue:
 
	def __init__(self,name,handler=None):
 
		self.name=name
 
		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 if msg[0]!="putFrame" else "('putFrame', ..., {})")
 
			log.info("%s <- %s", self.name, msg if msg[0]!="putFrame" else "('putFrame', ..., {})")
 
			if msg[0]=="!kill": break
 
			self._handleEvent(msg)
 
		log.info("%s MsgQueue exiting.",self.name)
 

	
 
	def setHandler(self,handler):
 
		self._handleEvent=handler
 

	
 

	
 
rand=random.Random()
 
rand.seed(361)
 
zobristNums=tuple(
 
	tuple(
 
		tuple(rand.getrandbits(32) for i in range(3)) for c in range(19)
 
	) for r in range(19)
 
)
 

	
 
def hashBoard(board):
 
	res=0
 
	for (r,row) in enumerate(board):
 
		for (c,item) in enumerate(row):
 
			res^=zobristNums[r][c][item+1]
 
	return res
 

	
 

	
 
def diffHash(r,c,oldItem,newItem):
 
	h=zobristNums[r][c]
 
	return h[oldItem+1] ^ h[newItem+1]
 

	
 

	
 
def exportBoard(board):
 
	substitutions={BLACK:"X", WHITE:"O"}
 
	template=[["."]*19 for r in range(19)]
 
	for r in range(3,19,6):
 
		for c in range(3,19,6):
 
			template[r][c]=","
 

	
 
	for (row,templateRow) in zip(board,template):
 
		for (c,x) in enumerate(row):
 
			if x!=EMPTY: templateRow[c]=substitutions[x]
 

	
 
	return "\n".join("".join(row) for row in template)
0 comments (0 inline, 0 general)