# HG changeset patch
# User Laman
# Date 2019-03-11 14:39:44
# Node ID 5617054647db10ac6701f97a5131accf739947e3
# Parent  d07ae4bfa14582d219fbd9ffd6d071e2bdf114ce

grid construction and evaluation

diff --git a/exp/board_detect.py b/exp/board_detect.py
--- a/exp/board_detect.py
+++ b/exp/board_detect.py
@@ -81,6 +81,10 @@ class BoardDetector:
 		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]
@@ -106,14 +110,14 @@ class BoardDetector:
 
 		# detect lines from edges and stones
 		edgeImg=prepareEdgeImg(rect)
-		hough=HoughTransform(edgeImg)
+		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")
-		hough.update(stonesImg,10)
-		lines=hough.extract()
+		self._hough.update(stonesImg,10)
+		lines=self._hough.extract()
 
 		linesImg=np.copy(rect)
 		for line in itertools.chain(*lines):
@@ -123,18 +127,10 @@ class BoardDetector:
 		# # 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
-		intersections=[]
-		for p in lines[0]:
-			for q in lines[1]:
-				intersections.append(p.intersect(q))
-		sack=DiagonalRansac(intersections,19)
-		diagonals=sack.extract(10,2000)
-		log.debug("diagonals candidates: %s",diagonals)
-		for line in diagonals:
-			self._drawLine(linesImg,line,[0,255,255])
-		show(linesImg,"diagonals candidates")
+		self._detectBestGrid(rectiLines,linesImg)
 
 	def _detectRough(self,img,filename):
 		corners=self._annotations[filename][0]
@@ -181,10 +177,11 @@ class BoardDetector:
 
 	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
-		a_=EPoint(b.x,min(a.y,d.y))
-		b_=EPoint(b.x,max(b.y,c.y))
-		c_=EPoint(c.x,max(b.y,c.y))
-		d_=EPoint(c.x,min(a.y,d.y))
+		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))
@@ -199,8 +196,72 @@ class BoardDetector:
 		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.x<self._rectW and 0<=point.y<self._rectH for point in (a,b,c,d)):
+			return None
+		if any(g.dist(h)<19*10 for (g,h) in [(a,b),(a,c),(a,d),(b,c),(b,d),(c,d)]):
+			return None
+		(a,b,c,d)=Corners([a,b,c,d])
+		rows=[]
+		cols=[]
+		for j in range(19):
+			rows.append(Line.fromPoints((a*(18-j)+b*j)/18,(d*(18-j)+c*j)/18))
+			cols.append(Line.fromPoints((a*(18-j)+d*j)/18,(b*(18-j)+c*j)/18))
+		return (rows,cols)
+
+	def _scoreGrid(self,lines):
+		return sum(self._hough.scoreLine(p.transform(self._inverseMatrix)) for p in itertools.chain(*lines))
+
 	def _drawLine(self,img,line,color=None):
 		if not color: color=[0,255,0]
 		(h,w)=img.shape[:2]
@@ -220,6 +281,16 @@ class BoardDetector:
 			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])
diff --git a/exp/hough.py b/exp/hough.py
--- a/exp/hough.py
+++ b/exp/hough.py
@@ -66,7 +66,7 @@ class HoughTransform:
 				(alphaDeg,d)=keys[k]
 				line=Line(alphaDeg*math.pi/180,d-self._diagLen//2)
 				res[-1].append(self._transformOutput(line))
-			res[-1].sort(key=lambda line: line.d if line.alpha<math.pi else -line.d)
+			res[-1].sort(key=lambda line: line.d)
 			i+=1
 
 		self.show(img)
@@ -82,6 +82,13 @@ class HoughTransform:
 					self._acc[(alphaDeg,d)]+=weight
 		log.debug("Hough updated in %s s",round(datetime.now().timestamp()-start,3))
 
+	def scoreLine(self,line):
+		transformed=self._transformInput(line)
+		alphaDeg=round(transformed.alpha*180/math.pi)%180
+		d=round(transformed.d+self._diagLen//2)
+		if not 0<=d<self._diagLen: return 0
+		return self._acc[(alphaDeg,d)]
+
 	def show(self,img=None):
 		if img is None: img=self._createImg()
 		show(img,"Hough transform accumulator")
@@ -114,6 +121,15 @@ class HoughTransform:
 			res.append((k,i))
 		return res
 
+	def _transformInput(self,line):
+		reflectedLine=Line(math.pi*2-line.alpha,line.d)
+		(x,y)=self._center
+		basis=EPoint(x,-y)
+		shiftedLine=reflectedLine.shiftBasis(basis)
+		if shiftedLine.alpha>=math.pi:
+			shiftedLine=Line(shiftedLine.alpha-math.pi,-shiftedLine.d)
+		return shiftedLine
+
 	def _transformOutput(self,line):
 		(x,y)=self._center
 		basis=EPoint(-x,y)
diff --git a/src/analyzer/corners.py b/src/analyzer/corners.py
--- a/src/analyzer/corners.py
+++ b/src/analyzer/corners.py
@@ -88,7 +88,6 @@ class Corners:
 
 		self._corners= self._corners[kIndex:] + self._corners[:kIndex] # rotate the upper left corner to the first place
 
-		log.debug(self._corners)
 		self._is_canon=True # success
 		return True