Changeset - 6f9ec51a8142
[Not reviewed]
default
0 6 0
Laman - 6 years ago 2018-12-12 22:28:59

moving responsibilities
GUI got its own ImageAnalyzer instance for doing previews
accordingly changed APIs to keep only useful messages
ImageAnalyzer params wrapped in a separate class
6 files changed with 81 insertions and 65 deletions:
0 comments (0 inline, 0 general)
src/analyzer/__init__.py
Show inline comments
 
import logging
 

	
 
from .grid import Grid
 
from util import BLACK,WHITE,EMPTY, exportBoard
 

	
 
log=logging.getLogger(__name__)
 

	
 

	
 
class Params:
 
	def __init__(self):
 
		self.tresB=30
 
		self.tresW=60
 
		self.corners=[]
 

	
 
	def copy(self):
 
		res=Params()
 
		res.tresB=self.tresB
 
		res.tresW=self.tresW
 
		res.corners=[p*1 for p in self.corners] # *1 forces copy
 
		return res
 

	
 

	
 
class ImageAnalyzer:
 
	def __init__(self,tresB=30,tresW=60):
 
		self.board=[[EMPTY]*19 for r in range(19)]
 
		self.grid=None
 

	
 
		self.tresB=tresB
 
		self.tresW=tresW
 
		self.params=Params()
 
		self.params.tresB=tresB
 
		self.params.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:
 
			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)))
 
@@ -34,33 +49,39 @@ class ImageAnalyzer:
 

	
 
		((x1,y1),(x2,y2))=relevantRect(imageCoords,stoneWidth,stoneHeight)
 

	
 
		for y in range(y1,y2+1):
 
			for x in range(x1,x2+1):
 
				try:
 
					red,green,blue=image.getpixel((x,y))
 
				except IndexError: continue
 

	
 
				I=(red+green+blue)/255/3
 
				m=min(red,green,blue)
 
				S=1-m/I if I!=0 else 0
 
				if 100*I<self.tresB: b+=1
 
				elif 100*I>self.tresW: w+=1
 
				if 100*I<self.params.tresB: b+=1
 
				elif 100*I>self.params.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 BLACK
 
		if w>=b and w>=e: return WHITE
 
		return EMPTY
 

	
 
	def setGridCorners(self,corners):
 
	def setParams(self,params):
 
		self.params=params.copy()
 
		if len(params.corners)==4:
 
			self.grid=Grid(self.params.corners)
 

	
 
	def setCorners(self,corners):
 
		self.params.corners=[p*1 for p in 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/analyzer/framecache.py
Show inline comments
 
from collections import deque
 

	
 

	
 
class FrameCache:
 
	def __init__(self,capacity=600): # 600 is enough for 10 min of 1 fps
 
		self._capacity=capacity
 
		self._cache=deque()
 
		self._keys=deque()
 
		self._values=dict()
 
		self._i=0
 

	
 
	def put(self,frame):
 
		if len(self._cache)>=self._capacity:
 
			self._cache.popleft()
 
		self._cache.append(frame)
 
	def put(self,item):
 
		self._i+=1
 
		if len(self._keys)>=self._capacity:
 
			k=self._keys.popleft()
 
			del self._values[k]
 
		self._values[self._i]=item
 
		self._keys.append(self._i)
 
		return self._i
 

	
 
	def get(self,k):
 
		return self._values.get(k)
 

	
 
	def getRelative(self,k):
 
		return (self._keys[k], self._values[self._keys[k]])
src/core.py
Show inline comments
 
import os
 
import multiprocessing
 
import threading
 
import logging
 
import PIL
 
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
 
import config as cfg
 
@@ -18,74 +17,52 @@ 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(self._handleEvent)
 
		self._guiMessages=MsgQueue()
 
		self._vidMessages=MsgQueue()
 

	
 
		self._imgs=sorted(os.listdir(cfg.misc.imgDir))
 
		self._imgIndex=cfg.misc.defaultImage
 
		imgPath=os.path.join(cfg.misc.imgDir,self._imgs[self._imgIndex])
 
		self._frame=PIL.Image.open(imgPath)
 
		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 setCorners(self,corners):
 
		self.detector.setGridCorners(corners)
 
		self.analyze()
 

	
 
	def setTresholds(self,tresB=None,tresW=None):
 
		if tresB is not None: self.detector.tresB=tresB
 
		if tresW is not None: self.detector.tresW=tresW
 
		self.preview()
 

	
 
	def relativeFrame(self,step):
 
		self._imgIndex=(self._imgIndex+step)%len(self._imgs)
 
		imgPath=os.path.join(cfg.misc.imgDir,self._imgs[self._imgIndex])
 
		self._frame=PIL.Image.open(imgPath)
 
		self._guiMessages.send("setCurrentFrame",(self._frame.copy(),gui.PREVIEW))
 
		self.preview()
 
	def showFrame(self,key):
 
		frame=self._cache.get(key)
 
		if frame is None:
 
			(key,frame)=self._cache.getRelative(10)
 
		self._guiMessages.send("setCurrentFrame", (frame.copy(), gui.PREVIEW, key))
 

	
 
	def putFrame(self,frame):
 
		img=PIL.Image.fromarray(frame)
 
		self._cache.put(img)
 
		self._guiMessages.send("setCurrentFrame",(img,gui.PREVIEW)) # !! RECORDING
 
		# self.analyze()
 
		self._frame=PIL.Image.fromarray(frame)
 
		k=self._cache.put(self._frame)
 
		self._guiMessages.send("setCurrentFrame", (self._frame, gui.RECORDING, k))
 
		self.analyze()
 

	
 
	def sendParams(self):
 
		params={
 
			"tresB": self.detector.tresB,
 
			"tresW": self.detector.tresW
 
		}
 
		self._guiMessages.send("setParams",tuple(),params)
 
		self._guiMessages.send("setParams",(self.detector.params.copy(),))
 

	
 
	def setParams(self,tresB=0,tresW=0):
 
		self.detector.tresB=tresB
 
		self.detector.tresW=tresW
 

	
 
	def preview(self):
 
		if self.detector.analyze(self._frame):
 
			self._guiMessages.send("setGameState", (self.detector.board,[]))
 
	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)
 
@@ -96,26 +73,24 @@ class Core:
 
	def listen(self):
 
		listenerThread=threading.Thread(target=lambda: self._ownMessages.listen())
 
		listenerThread.start()
 

	
 
	def joinChildren(self):
 
		self._guiProc.join()
 
		self._vidMessages.send("shutDown")
 
		self._vidProc.join()
 
		self._ownMessages.send("!kill",("core",))
 

	
 
	def _handleEvent(self,e):
 
		actions={
 
			"setCorners": self.setCorners,
 
			"setTresholds": self.setTresholds,
 
			"prevFrame": lambda: self.relativeFrame(-1),
 
			"nextFrame": lambda: self.relativeFrame(1),
 
			"putFrame": self.putFrame,
 
			"fetchParams": self.sendParams,
 
			"setParams": self.setParams
 
		}
 
		(actionName,args,kwargs)=e
 

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

	
 
if __name__=="__main__":
 
	core=Core()
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._frameKey=0
 

	
 
		self._ownMessages=None
 
		self._coreMessages=None
 

	
 
		self._state=GUI.SETUP
 

	
 
		self.mainWindow = MainWindow(self, master=self.root)
 
		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())
 
@@ -52,51 +56,57 @@ class GUI:
 
	def setUp(self):
 
		self.mainWindow.setUp()
 
		self.root.bind("<Left>",lambda e: self.sendMsg("prevFrame"))
 
		self.root.bind("<Right>",lambda e: self.sendMsg("nextFrame"))
 

	
 
	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()
 

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

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

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

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

	
 
	def _stateHandler(self,gameState,moves):
 
		labels={(row,col):(i+1) for (i,(c,row,col)) in enumerate(moves)}
 
		self.mainWindow.boardView.redrawState(gameState,labels)
 

	
 
	def _paramsHandler(self,**params):
 
	def _paramsHandler(self,params):
 
		if not self.settings:
 
			log.warning("received 'setParams' message while settings is '%s'",str(self.settings))
 
			return
 
		self.settings.setParams(**params)
 
		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 #
src/gui/imgview.py
Show inline comments
 
@@ -57,39 +57,42 @@ class ImgView(ResizableCanvas):
 
	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.sendMsg("setCorners",(corners,))
 
			self._parent.detector.setCorners(corners)
 

	
 
		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
src/gui/settings.py
Show inline comments
 
@@ -31,28 +31,23 @@ class Settings(tk.Toplevel,MsgMixin):
 
		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,_):
 
		self.parent.sendMsg("setTresholds",tuple(),self._compileParams())
 
		params=self.parent.detector.params
 
		params.tresB=self.scaleTresB.get()
 
		params.tresW=self.scaleTresW.get()
 

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

	
 
	def sendParams(self):
 
		self.parent.sendMsg("setParams",tuple(),self._compileParams())
 
		self.parent.sendParams()
 
		self.destroy()
 

	
 
	def _compileParams(self):
 
		params={
 
			"tresB": self.scaleTresB.get(),
 
			"tresW": self.scaleTresW.get()
 
		}
 
		return params
0 comments (0 inline, 0 general)