diff --git a/.hgignore b/.hgignore old mode 100644 new mode 100755 --- a/.hgignore +++ b/.hgignore @@ -1,2 +1,3 @@ ^src/__pycache__/ -^\.idea/ \ No newline at end of file +^\.idea/ +^images/ diff --git a/license.txt b/license.txt old mode 100644 new mode 100755 diff --git a/project.md b/project.md new file mode 100755 --- /dev/null +++ b/project.md @@ -0,0 +1,35 @@ +OneEye +====== + +Program to extract moves of a go game from video and save them or broadcast online. + +Modules: + - Video: grabbing still frames from a video file / stream. Using FFmpeg. + - Graphic: extracting game position from an image. Using Pillow. + - Watcher: interpreting sequence of game positions as a sequence of moves. + - Broadcaster: interfacing with a go server. + + +Graphic +------- +Interpolating the board grid from specified corner points. Using vanishing points and horizon in projective geometry. + +Determining the point status based on majority voting of pixels in its neighbourhood (deciding by HSI intensity). + +Autodetection of the board? + + +Watcher +------- +Base case: we have two correctly recognized positions differing by a single move (single added stone). Easy. + +Issues: + - illegal positions -> ignorable + - positions unreachable from the previous state + - reachable from any past state. (Incorrect states inbetween). How to pick the correct leaf of such a tree? + - reachable by more than one move. Issues with branching factor. + - board shifts -> repaired manually (or automatically), further positions have to be reevaluated + - stone shifts + - stone stops being recognized -> fixable manually and even ignorable + - stone is recognized at an empty intersection. It can be occupied later for real. What do? + diff --git a/readme.md b/readme.md old mode 100644 new mode 100755 diff --git a/src/corners.py b/src/corners.py old mode 100644 new mode 100755 diff --git a/src/epoint.py b/src/epoint.py old mode 100644 new mode 100755 diff --git a/src/go.py b/src/go.py old mode 100644 new mode 100755 diff --git a/src/grid.py b/src/grid.py old mode 100644 new mode 100755 --- a/src/grid.py +++ b/src/grid.py @@ -60,6 +60,15 @@ class Grid: affineIntersection=(rowStart*(boardSize-1-c)+rowEnd*c) / (boardSize-1) self.intersections[r][c]=EPoint.fromProjective(transformPoint(affineIntersection.toProjective(),rectiMatrixInv)) + def stoneSizeAt(self,r,c,sizeCoef): + intersection=self.intersections[r][c] + + if c>0: width=sizeCoef*(intersection.x-self.intersections[r][c-1].x) + else: width=sizeCoef*(self.intersections[r][c+1].x-intersection.x) + if r>0: height=sizeCoef*(intersection.y-self.intersections[r-1][c].y) + else: height=sizeCoef*(self.intersections[r+1][c].y-intersection.y) + + return (width,height) # from corners import Corners diff --git a/src/gui.py b/src/gui.py old mode 100644 new mode 100755 --- a/src/gui.py +++ b/src/gui.py @@ -1,13 +1,14 @@ import tkinter as tk from PIL import ImageTk import PIL -import math -from epoint import * from corners import * from grid import * from image_analyzer import * from go import * +showBigPoints=True +showGrid=False + class Application(tk.Frame): def __init__(self, master=None): @@ -57,10 +58,18 @@ class Application(tk.Frame): ## Redraws the current image and its overlay. def redrawImgView(self): self.imgView.create_image(2,2,anchor="nw",image=self.img) + + origWidth,origHeight=self.imgOrig.size + widthRatio=origWidth/self.img.width() + heightRatio=origHeight/self.img.height() + sizeCoef=max(widthRatio,heightRatio) + tresB=self.scaleTresB.get() + tresW=self.scaleTresW.get() + for corner in self.corners.corners: self.markPoint(corner.x,corner.y) - if self.boardGrid!=None: + if self.boardGrid!=None and showGrid: for r in range(19): a=self.boardGrid.intersections[r][0] b=self.boardGrid.intersections[r][-1] @@ -69,16 +78,15 @@ class Application(tk.Frame): 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 showBigPoints: + for r in range(19): + for c in range(19): + ((r1,c1),(r2,c2))=self.boardView.detector.relevantRect(self.boardGrid.intersections[r][c],*(self.boardGrid.stoneSizeAt(r,c,sizeCoef))) + self.imgView.create_rectangle(r1,c1,r2,c2,outline="#00ffff") self.imgView.grid() - origWidth,origHeight=self.imgOrig.size - widthRatio=origWidth/self.img.width() - heightRatio=origHeight/self.img.height() - sizeCoef=max(widthRatio,heightRatio) - tresB=self.scaleTresB.get() - tresW=self.scaleTresW.get() - shift=EPoint(origWidth-self.img.width()*sizeCoef,origHeight-self.img.height()*sizeCoef)/2 self.boardView.redrawState(self.imgOrig,sizeCoef,shift,tresB,tresW) @@ -91,7 +99,7 @@ class Application(tk.Frame): self.imgView.grid() -## Handles and presents the game state as detected by the program. +## Handles and presents the game state as detected by the program. class BoardView(tk.Canvas): def __init__(self, master=None): self.detector=ImageAnalyzer() @@ -99,7 +107,7 @@ class BoardView(tk.Canvas): tk.Canvas.__init__(self, master) self.configure(width=360,height=360,background="#ffcc00") - + self.drawGrid() self.grid() @@ -109,6 +117,9 @@ class BoardView(tk.Canvas): # @param tresB upper intensity treshold for a pixel to be considered black, [0-100] # @param tresW lower intensity treshold for a pixel to be considered white, [0-100] def redrawState(self,img,sizeCoef,shift,tresB,tresW): + self.create_rectangle(0,0,360,360,fill="#ffcc00") + self.drawGrid() + self.detector.analyze(img,tresB,tresW,sizeCoef,shift) for r,row in enumerate(self.detector.board): diff --git a/src/image_analyzer.py b/src/image_analyzer.py old mode 100644 new mode 100755 --- a/src/image_analyzer.py +++ b/src/image_analyzer.py @@ -14,22 +14,16 @@ class ImageAnalyzer: for c in range(19): intersection=self.grid.intersections[r][c] - if c>0: stoneWidth=sizeCoef*(intersection.x-self.grid.intersections[r][c-1].x) - else: stoneWidth=sizeCoef*(self.grid.intersections[r][c+1].x-intersection.x) - if r>0: stoneHeight=sizeCoef*(intersection.y-self.grid.intersections[r-1][c].y) - else: stoneHeight=sizeCoef*(self.grid.intersections[r+1][c].y-intersection.y) - - self.board[r][c]=self.analyzePoint(image,intersection*sizeCoef+shift,stoneWidth,stoneHeight,tresB,tresW) + self.board[r][c]=self.analyzePoint(image,intersection*sizeCoef+shift,*(self.grid.stoneSizeAt(r,c,sizeCoef)),tresB,tresW) def analyzePoint(self,image,imageCoords,stoneWidth,stoneHeight,tresB,tresW): b=w=e=0 - - cmax=max(int(stoneWidth*2//7),2) # !! optimal parameters subject to further research - rmax=max(int(stoneHeight*2//7),2) - - for r in range(-rmax,rmax+1): - for c in range(-cmax,cmax+1): - red,green,blue=image.getpixel((imageCoords.x+c,imageCoords.y+r)) + + ((x1,y1),(x2,y2))=self.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) @@ -41,6 +35,14 @@ class ImageAnalyzer: if b>=w and b>=e: return Go.BLACK if w>=b and w>=e: return Go.WHITE return Go.EMPTY + + def relevantRect(self,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)) def setGrid(self,grid): self.grid=grid