import sys sys.path.append("../src") import os import random import itertools import logging as log import cv2 as cv import numpy as np import scipy.cluster import scipy.ndimage import scipy.signal from geometry import Line from ransac import DiagonalRansac from annotations import DataFile,computeBoundingBox from hough import show,prepareEdgeImg,HoughTransform from analyzer.epoint import EPoint from analyzer.corners import Corners random.seed(361) log.basicConfig(level=log.DEBUG,format="%(message)s") def kmeans(img): arr=np.reshape(img,(-1,3)).astype(np.float) wood=[193,165,116] (centers,distortion)=scipy.cluster.vq.kmeans(arr,3) log.debug("k-means centers: %s",centers) (black,empty,white)=sorted(centers,key=sum) if np.linalg.norm(black)>np.linalg.norm(black-wood): black=None if np.linalg.norm(white-[255,255,255])>np.linalg.norm(white-wood): white=None log.debug("black, white: %s, %s",black,white) return (black,white,centers) def quantize(img,centers): origShape=img.shape data=np.reshape(img,(-1,3)) (keys,dists)=scipy.cluster.vq.vq(data,centers) pixels=np.array([centers[k] for k in keys],dtype=np.uint8).reshape(origShape) return pixels def filterStones(contours,bwImg,stoneDims): contourImg=cv.cvtColor(bwImg,cv.COLOR_GRAY2BGR) res=[] for (i,c) in enumerate(contours): keep=True moments=cv.moments(c) center=(moments["m10"]/(moments["m00"] or 1), moments["m01"]/(moments["m00"] or 1)) area=cv.contourArea(c) (x,y,w,h)=cv.boundingRect(c) if w>stoneDims[0] or h>stoneDims[1]*1.5 or w<2 or h<2: cv.drawMarker(contourImg,tuple(map(int,center)),(0,0,255),cv.MARKER_TILTED_CROSS,12) keep=False coverage1=area/(w*h or 1) hull=cv.convexHull(c) coverage2=area/(cv.contourArea(hull) or 1) # if coverage2<0.8: # cv.drawMarker(contourImg,tuple(map(int,center)),(0,127,255),cv.MARKER_DIAMOND,12) # keep=False if keep: res.append((EPoint(*center),c)) cv.drawMarker(contourImg,tuple(map(int,center)),(255,0,0),cv.MARKER_CROSS) log.debug("accepted: %s",len(res)) log.debug("rejected: %s",len(contours)-len(res)) show(contourImg,"accepted and rejected stones") return res class BoardDetector: def __init__(self,annotationsPath): self._annotations=DataFile(annotationsPath) self._rectW=0 self._rectH=0 self._rect=None self._hough=None self._rectiMatrix=None self._inverseMatrix=None def __call__(self,img,filename): # approximately detect the board (h,w)=img.shape[:2] log.debug("image dimensions: %s x %s",w,h) show(img,filename) (x1,y1,x2,y2)=self._detectRough(img,filename) rect=img[y1:y2,x1:x2] self._rectW=x2-x1 self._rectH=y2-y1 self._rect=rect # quantize colors (black,white,colors)=self._sampleColors(rect) quantized=quantize(rect,colors) gray=cv.cvtColor(rect,cv.COLOR_BGR2GRAY) edges=cv.Canny(gray,70,130) show(edges,"edges") quantized=quantized & (255-cv.cvtColor(edges,cv.COLOR_GRAY2BGR)) show(quantized,"quantized, edges separated") # detect black and white stones stones=self._detectStones(quantized,black,white) # detect lines from edges and stones edgeImg=prepareEdgeImg(rect) self._hough=HoughTransform(edgeImg) stonesImg=np.zeros((self._rectH,self._rectW),np.uint8) for (point,c) in stones: cv.circle(stonesImg,(int(point.x),int(point.y)),2,255,-1) show(stonesImg,"detected stones") self._hough.update(stonesImg,10) lines=self._hough.extract() linesImg=np.copy(rect) for line in itertools.chain(*lines): self._drawLine(linesImg,line) show(linesImg,"detected lines") # # rectify the image matrix=self._computeTransformationMatrix(lines[0][0],lines[0][-1],lines[1][0],lines[1][-1]) transformed=cv.warpPerspective(rect,matrix,(self._rectW,self._rectH)) rectiLines=[[line.transform(matrix) for line in pack] for pack in lines] # determine precise board edges self._detectBestGrid(rectiLines,linesImg) def _detectRough(self,img,filename): corners=self._annotations[filename][0] (x1,y1,x2,y2)=computeBoundingBox(corners) log.debug("bounding box: (%s,%s) - (%s,%s)",x1,y1,x2,y2) return (x1,y1,x2,y2) def _sampleColors(self,rect): (h,w)=rect.shape[:2] minirect=rect[h//4:3*h//4, w//4:3*w//4] return kmeans(minirect) def _detectStones(self,quantized,black,white): (h,w)=quantized.shape[:2] mask=self._maskStones(quantized,black,white) stoneDims=(w/19,h/19) log.debug("stone dims: %s - %s",tuple(x/2 for x in stoneDims),stoneDims) (contours,hierarchy)=cv.findContours(mask,cv.RETR_LIST,cv.CHAIN_APPROX_SIMPLE) stoneLocs=filterStones(contours,mask,stoneDims) return stoneLocs def _maskStones(self,quantized,black,white): unit=np.array([1,1,1],dtype=np.uint8) if black is not None: maskB=cv.inRange(quantized,black-unit,black+unit) distTransform=cv.distanceTransform(maskB,cv.DIST_L2,5) maskB=cv.inRange(distTransform,6,20) show(maskB,"black areas") else: maskB=np.zeros(quantized.shape[:2],dtype=np.uint8) if white is not None: maskW=cv.inRange(quantized,white-unit,white+unit) distTransform=cv.distanceTransform(maskW,cv.DIST_L2,5) maskW=cv.inRange(distTransform,6,20) show(maskW,"white areas") else: maskW=np.zeros(quantized.shape[:2],dtype=np.uint8) stones=cv.bitwise_or(maskB,maskW) show(stones,"black and white areas") return stones def _computeTransformationMatrix(self,p,q,r,s): # p || q, r || s (a,b,c,d)=Corners([p.intersect(r),p.intersect(s),q.intersect(r),q.intersect(s)]) # canonize the abcd order pad=20 a_=EPoint(b.x+pad,min(a.y,d.y)+pad) b_=EPoint(b.x+pad,max(b.y,c.y)-pad) c_=EPoint(c.x-pad,max(b.y,c.y)-pad) d_=EPoint(c.x-pad,min(a.y,d.y)+pad) abcd=[list(point) for point in (a,b,c,d)] abcd_=[list(point) for point in (a_,b_,c_,d_)] log.debug("abcd: %s ->",(a,b,c,d)) log.debug("-> abcd_: %s",(a_,b_,c_,d_)) matrix=cv.getPerspectiveTransform(np.float32(abcd),np.float32(abcd_)) log.debug("transformation matrix: %s",matrix) rect=np.copy(self._rect) for point in (a,b,c,d): cv.drawMarker(rect,(int(point.x),int(point.y)),(0,255,255),cv.MARKER_TILTED_CROSS) show(rect) transformed=cv.warpPerspective(rect,matrix,(self._rectW,self._rectH)) show(transformed,"rectified image") self._rectiMatrix=matrix self._inverseMatrix=np.linalg.inv(matrix) return matrix def _detectBestGrid(self,lines,img): intersections=[] for p in lines[0]: for q in lines[1]: intersections.append(p.intersect(q)) sack=DiagonalRansac(intersections,19) diagonals=sack.extract(10,3000) log.debug("diagonals candidates: %s",diagonals) for line in diagonals: self._drawLine(img,line.transform(self._inverseMatrix),[0,255,255]) show(img,"diagonals candidates") best=(0,None) transformedImg=cv.warpPerspective(img,self._rectiMatrix,(self._rectW,self._rectH)) for e in diagonals: for f in diagonals: center=e.intersect(f) if not center: continue if center.x<0 or center.x>self._rectW or center.y<0 or center.y>self._rectH: continue for line in itertools.chain(*lines): for i in range(1,10): # 10th is useless, 11-19 are symmetrical to 1-9 grid=self._constructGrid(e,f,line,i) if not grid: continue score=self._scoreGrid(grid) if score>best[0]: best=(score,grid) log.debug("new best grid: %s",score) self._showGrid(transformedImg,grid) return best[1] def _constructGrid(self,e,f,line,i): """Contruct a grid. :param e: (Line) one diagonal :param f: (Line) other diagonal :param line: (Line) one of the grid lines :param i: (int) line's index among the grid's lines, 1<=i<=9""" center=e.intersect(f) p1=line.intersect(e) p2=line.intersect(f) a=center+9*(p1-center)/(10-i) b=center+9*(p2-center)/(10-i) c=2*center-a d=2*center-b # abort unfitting sizes if not all(0<=point.x10**5 for x in [*a,*b]): log.debug("ignored") return cv.line(img,(int(a.x),int(a.y)),(int(b.x),int(b.y)),color) def _showGrid(self,img,lines): img=np.copy(img) (rows,cols)=lines for row in rows: for col in cols: point=row.intersect(col) xy=(int(point.x),int(point.y)) cv.circle(img,xy,4,[0,0,255],-1) show(img,"grid candidate") if __name__=="__main__": detector=BoardDetector(sys.argv[2]) filepath=sys.argv[1] filename=os.path.basename(filepath) img=cv.imread(filepath) detector(img,filename)