Changeset - 7f6fac7f6d8e
[Not reviewed]
default
0 6 0
Laman - 6 years ago 2018-12-04 17:17:22

enhanced logging
6 files changed with 46 insertions and 7 deletions:
0 comments (0 inline, 0 general)
src/analyzer/__init__.py
Show inline comments
 
import logging as log
 
import logging
 

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

	
 
log=logging.getLogger(__name__)
 

	
 

	
 
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
 

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

	
 
		log.info("board analyzed:\n%s", exportBoard(self.board))
 
		return True
 

	
 
	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):
 
				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
 
				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):
 
		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/config.py
Show inline comments
 
import os
 
import logging
 
import logging.config
 
import json
 

	
 

	
 
srcDir=os.path.dirname(__file__)
 

	
 
logging.basicConfig(format='%(asctime)s %(levelname)s: %(message)s', level=logging.DEBUG)
 
with open(os.path.join(srcDir, "..", "config.json")) as f:
 
	cfgFile=json.load(f)
 

	
 

	
 
logCfg = cfgFile.get("logging") or {
 
	"version": 1,
 
	"handlers": {
 
		"file": {
 
			"level": "DEBUG",
 
			"class": "logging.FileHandler",
 
			"filename": "/tmp/oneeye.log",
 
			"formatter": "default"
 
		},
 
		"console": {
 
			"level": "DEBUG",
 
			"class": "logging.StreamHandler",
 
			"formatter": "default"
 
		}
 
	},
 
	"root": {
 
		"handlers": ["console","file"],
 
		"level": "DEBUG"
 
	},
 
	"formatters": {
 
		"default": {
 
			"format": "%(asctime)s %(levelname)s: %(message)s",
 
			"datefmt": "%Y-%m-%d %H:%M:%S"
 
		}
 
	}
 
}
 

	
 
logging.config.dictConfig(logCfg)
 

	
 

	
 
class misc:
 
	file=cfgFile["misc"]
 
	version=(0,0,0)
 
	defaultImage=file.get("defaultImage", 0)
 

	
 
	_imgDir=file.get("imgDir","../images")
 
	imgDir=_imgDir if os.path.isabs(_imgDir) else os.path.join(srcDir,_imgDir)
 

	
 

	
 
class gui:
 
	file=cfgFile["gui"]
 
	showBigPoints=file.get("showBigPoints", False)
 
	showGrid=file.get("showGrid", True)
src/core.py
Show inline comments
 
import os
 
import multiprocessing
 
import threading
 
import logging as log
 
import logging
 
import PIL
 
from util import MsgQueue
 
from gui import gui
 
from analyzer import ImageAnalyzer
 
from go.core import Go, isLegalPosition
 
from statebag import StateBag
 
import config as cfg
 

	
 
log=logging.getLogger(__name__)
 

	
 

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

	
 
		self._ownMessages=MsgQueue(self._handleEvent)
 
		self._guiMessages=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._guiProc=multiprocessing.Process(name="gui", target=gui, args=(self._guiMessages,self._ownMessages))
 
		self._guiProc.start()
 
		self.relativeFrame(0)
 

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

	
 
	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(),))
 
		self.analyze()
 

	
 
	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):
 
		listenerThread=threading.Thread(target=lambda: self._ownMessages.listen())
 
		listenerThread.start()
 

	
 
	def joinGui(self):
 
		self._guiProc.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)
 
		}
 
		(actionName,args,kwargs)=e
 

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

	
 
core=Core()
 
core.listen()
 
core.joinGui()
 

	
 
"""
 
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/go/core.py
Show inline comments
 
import logging as log
 
import logging
 

	
 
from util import EMPTY,BLACK,WHITE, colorNames,hashBoard,diffHash
 
from .helperboard import HelperBoard
 
from .gamerecord import GameRecord
 

	
 
log=logging.getLogger(__name__)
 

	
 
PASS=(99,99)
 

	
 

	
 
class Go:
 
	## Initializes self.board to a list[r][c]=EMPTY.
 
	def __init__(self,boardSize=19):
 
		self.boardSize=boardSize
 
		self.board=[[EMPTY]*boardSize for x in range(boardSize)]
 
		self.toMove=BLACK
 

	
 
		# utility field to allow undoing moves. but only useful right after doMove. this class doesn't need it, so it is not kept correct at other times
 
		self.captures=[None]*4
 
		self.captureCount=0
 

	
 
		self._helper=HelperBoard(self.board)
 
		self._freshHash=hashBoard(self.board) # always reflecting current state of the board
 
		self._hashes=[self._freshHash]
 
		self._record=GameRecord()
 

	
 
	## Executes a move.
 
	#
 
	#  Doesn't check for kos. Suicide not allowed.
 
	#
 
	#  @param color BLACK or WHITE
 
	#  @return True on success, False on failure (illegal move)
 
	def doMove(self,color,r,c):
 
		if color!=self.toMove: log.warning("move by %s out of order",colorNames[color])
 
		if (r,c)==PASS:
 
			self.toMove*=-1 # pass
 
			self._hashes.append(self._freshHash)
 
			return True
 
		if self.board[r][c]!=EMPTY: return False
 

	
 
		self.board[r][c]=color
 
		self._freshHash^=diffHash(r,c,EMPTY,color)
 

	
 
		# capture neighbors
 
		self.captureCount=0
 
		for dr,dc in ((-1,0),(1,0),(0,-1),(0,1)):
 
			self._helper.clear()
 
			if not self._helper.floodFill(-color,r+dr,c+dc,EMPTY):
 
				self.captures[self.captureCount]=(r+dr,c+dc)
 
				self.captureCount+=1
 
				self._remove()
 

	
 
		# check for suicide and prevent it
 
		self._helper.clear()
 
		if not self._helper.floodFill(color,r,c,EMPTY):
 
			self.board[r][c]=EMPTY
 
			self._freshHash=self._hashes[-1]
 
			return False
 
		self._record.move(color,r,c)
 
		self.toMove=-1*color
 
		self._hashes.append(self._freshHash)
 
		return True
 

	
 
	def undoMove(self,r,c,captures):
 
		if (r,c)!=PASS:
 
			assert self.board[r][c]==-1*self.toMove, "{0}!={1}".format(self.board[r][c],-1*self.toMove)
 

	
 
			if len(captures)>0:
 
				self._helper.clear()
 
				for (ri,ci) in captures:
 
					self._helper.floodFill(EMPTY,ri,ci)
 
				self._fill(self.toMove)
 

	
 
			self.board[r][c]=EMPTY
 
		self.toMove*=-1
 
		self._hashes.pop()
 
		self._freshHash=self._hashes[-1]
 

	
 
	def transitionMove(self,board):
 
		res=transitionMove(self.board,board)
 
		if not res: return res
 
		(r,c,color)=res
 
		return self.doMove(color,r,c)
 

	
 
	def load(self,board):
 
		for (r,row) in enumerate(board):
 
			for (c,x) in enumerate(row):
 
				self.board[r][c]=x
 
		self._freshHash=hashBoard(self.board)
 
		self._hashes=[self._freshHash]
 

	
 
	def hash(self):
 
		return self._hashes[-1]
 

	
 
	## Removes stones at coordinates marked with True in self.helper.
 
	def _remove(self):
 
		self._fill(EMPTY)
 

	
 
	def _fill(self,filling):
 
		for (r,c) in self._helper.getContinuousArea():
 
			self._freshHash^=diffHash(r,c,self.board[r][c],filling)
 
			self.board[r][c]=filling
 

	
 

	
 
def isLegalPosition(board):
 
	boardSize=len(board)
 
	temp=[[None]*boardSize for x in range(boardSize)]
 

	
 
	for r in range(boardSize):
 
		for c in range(boardSize):
 
			if board[r][c]==EMPTY: continue
 
			if temp[r][c]: continue
 
			if not dfs([(r,c)],board,temp): return False
 
	return True
 

	
 

	
 
def transitionMove(state1,state2):
 
	moves=[]
 
	for (r,(row1,row2)) in enumerate(zip(state1,state2)):
 
		for (c,(item1,item2)) in enumerate(zip(row1,row2)):
 
			if item1==EMPTY and item2!=EMPTY:
 
				moves.append((r,c,item2))
 

	
 
	if len(moves)==0:
 
		log.info("no new stone")
 
		return None
 
	elif len(moves)==1:
 
		log.info("new stone: %s",moves[0])
 
		return moves[0]
 
	else:
 
		log.warning("too many new stones: %s",moves)
 
		return False
 

	
 

	
 
def dfs(stack,board,mask):
 
	boardSize=len(board)
 
	(r,c)=stack[0]
 
	color=board[r][c]
 

	
 
	while len(stack)>0:
 
		(r,c)=stack.pop()
 
		if board[r][c]==EMPTY: return True
 
		elif board[r][c]!=color: continue
 
		elif mask[r][c]: return True
 
		mask[r][c]=True
 
		for (x,y) in ((0,-1),(-1,0),(0,1),(1,0)):
 
			if 0<=r+y<boardSize and 0<=c+x<boardSize:
 
				stack.append((r+y,c+x))
 
	return False
src/gui/imgview.py
Show inline comments
 
import logging as log
 
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):
 
		super().__init__(master)
 

	
 
		self._parent=parent
 
		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)))
 
			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.sendMsg("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 _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
 
		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
 
		return EPoint(self.canvasx(point.x),self.canvasy(point.y)) * self._imgSizeCoef + self._imgShift
src/util.py
Show inline comments
 
import random
 
import multiprocessing
 
import logging as log
 
import logging
 

	
 
log=logging.getLogger(__name__)
 

	
 
EMPTY=0
 
BLACK=1
 
WHITE=-1
 

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

	
 

	
 
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)
 
			if msg[0]=="!kill": break
 
			self._handleEvent(msg)
 

	
 
	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={EMPTY:".", BLACK:"X", WHITE:"O"}
 
	return "\n".join("".join(substitutions.get(x,"?") for x in row) for row in board)
0 comments (0 inline, 0 general)