Changeset - f125391c937d
[Not reviewed]
default
0 4 0
Laman - 6 years ago 2018-12-30 18:47:24

corners: order canonization moved to the add method
4 files changed with 13 insertions and 7 deletions:
0 comments (0 inline, 0 general)
exp/color_sampler.py
Show inline comments
 
@@ -79,88 +79,88 @@ class Sampler:
 
		self._dirty=True
 
		self._markCorners()
 

	
 
	def save(self):
 
		if self._dirty:
 
			self.annotations.save()
 
			self.dirty=False
 

	
 
	def _createGUI(self):
 
		root=tk.Tk()
 
		frame=tk.Frame(root)
 
		frame.grid(column=0,row=0,sticky=(N,S,E,W))
 

	
 
		self.canvas=tk.Canvas(frame)
 
		self.canvas.grid(row=1,column=0)
 

	
 
		bar=self._createBar(frame)
 
		bar.grid(row=2,column=0,sticky=(E,W))
 

	
 
		self.showImage()
 

	
 
		self.canvas.bind('<1>',lambda e: self.printSample())
 
		self.canvas.bind('<3>',self.addCorner)
 
		self.canvas.bind("<Motion>",self.sample)
 
		root.bind("<Left>",lambda e: self.switchImage(-1))
 
		root.bind("<Right>",lambda e: self.switchImage(1))
 
		root.bind("<b>",lambda e: self.setLetter("b"))
 
		root.bind("<e>",lambda e: self.setLetter("e"))
 
		root.bind("<w>",lambda e: self.setLetter("w"))
 

	
 
		root.mainloop()
 

	
 
	def _createBar(self,frame):
 
		bar=tk.Frame(frame,height=20,borderwidth=1,relief="sunken")
 
		self.letterLabel=tk.Label(bar,width=8,text="Letter: _")
 
		self.letterLabel.pack(side=LEFT)
 
		self.posLabel=tk.Label(bar,width=16,text="(,)")
 
		self.posLabel.pack(side=LEFT)
 
		self.colorLabel=tk.Label(bar,width=20,text="HSV: (,,)")
 
		self.colorLabel.pack(side=LEFT)
 

	
 
		return bar
 

	
 
	def _markCorners(self):
 
		self.canvas.delete("mark")
 
		for c in self.corners:
 
			(x,y)=(c.x,c.y)
 
			self.canvas.create_oval(x-2,y-2,x+2,y+2,fill="#00ff00",tags="mark")
 
		if self.corners.canonizeOrder():
 
		if self.corners.is_canon():
 
			(a,b,c,d)=self.corners
 
			self.canvas.create_line(a.x,a.y,b.x,b.y,fill="#00ff00",tags="mark")
 
			self.canvas.create_line(b.x,b.y,c.x,c.y,fill="#00ff00",tags="mark")
 
			self.canvas.create_line(c.x,c.y,d.x,d.y,fill="#00ff00",tags="mark")
 
			self.canvas.create_line(d.x,d.y,a.x,a.y,fill="#00ff00",tags="mark")
 

	
 

	
 
class DataFile(MutableMapping):
 
	"""self._data: {filename: [EPoint,EPoint,EPoint,EPoint]}"""
 
	def __init__(self,filename):
 
		self.filename=filename
 
		try:
 
			with gzip.open(filename,mode="rt",encoding="utf8") as f:
 
				self._data=json.load(f,object_hook=self.deserialize)
 
		except OSError:
 
			self._data=dict()
 

	
 
	def save(self):
 
		with gzip.open(self.filename,mode="wt",encoding="utf8") as f:
 
			json.dump(self._data,f,default=self.serialize,indent="\t")
 

	
 
	def serialize(self,obj):
 
		if isinstance(obj,EPoint):
 
			return {"type": "EPoint", "val": [obj.x,obj.y]}
 
		raise TypeError(obj)
 

	
 
	def deserialize(self,obj):
 
		if obj.get("type")!="EPoint": return obj
 
		return EPoint(*obj["val"])
 

	
 
	def __getitem__(self, key): return self._data[key]
 
	def __setitem__(self, key, val): self._data[key]=val
 
	def __delitem__(self, key): del self._data[key]
 
	def __iter__(self): return iter(self._data)
 
	def __len__(self): return len(self._data)
 

	
 

	
 
if __name__=="__main__":
 
	s=Sampler()
src/analyzer/corners.py
Show inline comments
 
import logging
 

	
 
from .epoint import EPoint
 

	
 
log=logging.getLogger(__name__)
 

	
 

	
 
class Corners:
 
	def __init__(self,cornerList=[]):
 
		self._corners=cornerList[:]
 
		self._is_canon=False
 
		self._canonizeOrder()
 

	
 
	## Adds a new corner if there are less than four, replaces the closest otherwise.
 
	def add(self,x,y):
 
		a=EPoint(x,y)
 
		# for i,c in enumerate(self.corners): # move an improperly placed point
 
			# if a.dist(c)<20:
 
				# self.corners[i]=a
 
				# return
 

	
 
		if len(self._corners)<4: # add a new corner
 
			self._corners.append(a)
 

	
 
		if len(self._corners)<4:
 
			return
 

	
 
		index,minDist=0,float('inf') # replace the corner closest to the clicked point
 
		for i,c in enumerate(self._corners):
 
			if a.dist(c)<minDist:
 
				index,minDist=i,a.dist(c)
 

	
 
		self._corners[index]=a
 
		self._canonizeOrder()
 

	
 
	## Order the corners (0,1,2,3) so they make a quadrangle with vertices KLMN in counter-clockwise order, K being in the upper left.
 
	#
 
	#  For four points ABCD, there are 24 possible permutations corresponding to the desired KLMN.
 
	#  When we relax the condition of K being the upper left one, we get six groups of four equivalent permutations. KLMN ~ LMNK ~ MNKL ~ NKLM.
 
	#
 
	#  We determine which of the points' triplets are oriented clockwise and which counter-clockwise (minus/plus in the table below)
 
	#  and swap them so that all triangles turn counter-clockwise.
 
	#
 
	#  xxxx -> KLMN | ABC | ABD | ACD | BCD | index | swap
 
	#  ------------ | :-: | :-: | :-: | :-: | ----: | ----
 
	#  A BCD        |  +  |  +  |  +  |  +  |    15 | 0
 
	#  A BDC        |  +  |  +  |  -  |  -  |    12 | CD
 
	#  A CBD        |  -  |  +  |  +  |  -  |     6 | BC
 
	#  A CDB        |  -  |  -  |  +  |  +  |     3 | AB
 
	#  A DBC        |  +  |  -  |  -  |  +  |     9 | AD
 
	#  A DCB        |  -  |  -  |  -  |  -  |     0 | BD
 
	#
 
	#  For every non-degenerate quadrangle, there must be 1-3 edges going right-left (from a higher to a lower x coordinate).
 
	#  From these pick the one with the lowest slope (dy/dx) and declare its ending point the upper left corner. For the same slope pick the one further left.
 
	#
 
	#  @return True for a convex quadrangle, False for concave and degenerate cases.
 
	def canonizeOrder(self):
 
		if len(self._corners)!=4: return False # erroneus call
 
	def _canonizeOrder(self):
 
		if len(self._corners)!=4: self._is_canon=False
 

	
 
		a,b,c,d=self._corners
 
		abc=doubleTriangleArea(a,b,c)
 
		abd=doubleTriangleArea(a,b,d)
 
		acd=doubleTriangleArea(a,c,d)
 
		bcd=doubleTriangleArea(b,c,d)
 

	
 
		if any(x==0 for x in (abc,abd,acd,bcd)): return False # collinear degenerate
 
		if any(x==0 for x in (abc,abd,acd,bcd)): self._is_canon=False # collinear degenerate
 

	
 
		swaps=[(1,3),(0,1),(1,2),(0,3),(2,3),(0,0)]
 
		index=(8 if abc>0 else 0)|(4 if abd>0 else 0)|(2 if acd>0 else 0)|(1 if bcd>0 else 0)
 
		if index%3!=0: return False # concave degenerate
 
		swap=swaps[index//3]
 

	
 
		self._corners[swap[0]], self._corners[swap[1]] = self._corners[swap[1]], self._corners[swap[0]] # counter-clockwise order
 

	
 
		kIndex=None
 
		lowestSlope=float("inf")
 

	
 
		for i,corner in enumerate(self._corners): # find the NK edge: going right-left with the lowest slope, secondarily the one going down
 
			ii=(i+1)%4
 
			slope=abs(getSlope(corner, self._corners[ii]))
 
			if corner.x>self._corners[ii].x and (slope < lowestSlope or (slope == lowestSlope and corner.y < self._corners[ii].y)):
 
				kIndex=ii
 
				lowestSlope=slope
 

	
 
		self._corners= self._corners[kIndex:] + self._corners[:kIndex] # rotate the upper left corner to the first place
 

	
 
		log.debug(self._corners)
 
		return True # success
 
		self._is_canon=True # success
 

	
 
	def scale(self,scale):
 
		self._corners=[c * scale for c in self._corners]
 

	
 
	def __iter__(self):
 
		return iter(self._corners)
 

	
 
	def __len__(self):
 
		return len(self._corners)
 

	
 
	def is_canon(self):
 
		return self._is_canon
 

	
 

	
 
## Computes twice the area of the triangle formed by points a,b,c.
 
#
 
#  @return positive value for points oriented counter-clockwise, negative for clockwise, zero for degenerate cases.
 
def doubleTriangleArea(a,b,c):
 
	return (a.x-b.x)*(c.y-a.y)-(c.x-a.x)*(a.y-b.y)
 

	
 

	
 
def getSlope(a,b):
 
	if(b.x==a.x): return float("inf")
 
	return (b.y-a.y)/(b.x-a.x)
src/analyzer/grid.py
Show inline comments
 
import numpy
 
from .epoint import EPoint
 

	
 

	
 
## Projective transformation of a point with a matrix A.
 
#  
 
#  Takes a point as a horizontal vector, transposes it and multiplies with A from left.
 
#  
 
#  @return transformed point as a numpy.array
 
def transformPoint(point,A):
 
	return (A*numpy.matrix(point).transpose()).getA1()
 

	
 

	
 
class Grid:
 
	## Creates a Grid from the provided Corners object.
 
	#
 
	#  Finds the vanishing points of the board lines (corner points define perspectively transformed parallel lines). The vanishing points define the image horizon.
 
	#
 
	#  The horizon can be used to construct a matrix for affine rectification of the image (restoring parallel lines parallelism). We transform the corner points by this matrix,
 
	#  interpolate them to get proper intersections' coordinates and then transform these back to get their placement at the original image.
 
	#
 
	#  The result is stored in grid.intersections, a boardSize*boardSize list with [row][column] coordinates.
 
	#
 
	#  @param corners list of EPoints in ABCD order per corners.Corners.canonizeOrder().
 
	#  @param corners iterable of 4 EPoints in ABCD order per corners.Corners._canonizeOrder().
 
	# !! Needs a check for proper initialization.
 
	def __init__(self,corners):
 
		# ad
 
		# bc
 
		a,b,c,d=[c.toProjective() for c in corners]
 

	
 
		p1=numpy.cross(a,b)
 
		p2=numpy.cross(c,d)
 
		vanish1=numpy.cross(p1,p2)
 
		# !! 32 bit int can overflow. keeping it reasonably small. might want to use a cleaner solution
 
		vanish1=EPoint.fromProjective(vanish1).toProjective() # !! EPoint fails with point in infinity
 

	
 
		p3=numpy.cross(a,d)
 
		p4=numpy.cross(b,c)
 
		vanish2=numpy.cross(p3,p4)
 
		vanish2=EPoint.fromProjective(vanish2).toProjective()
 

	
 
		horizon=numpy.cross(vanish1,vanish2)
 

	
 
		horizon=EPoint.fromProjective(horizon).toProjective()
 

	
 
		rectiMatrix=numpy.matrix([horizon,[0,1,0],[0,0,1]])
 
		rectiMatrixInv=numpy.linalg.inv(rectiMatrix)
 

	
 

	
 
		affineCorners=[EPoint.fromProjective(transformPoint(x,rectiMatrix)) for x in (a,b,c,d)]
 
		x=[affineCorners[0]-affineCorners[3],affineCorners[1]-affineCorners[2],affineCorners[0]-affineCorners[1],affineCorners[3]-affineCorners[2]]
 

	
 
		self.intersections=[]
 
		boardSize=19
 
		for r in range(boardSize):
 
			self.intersections.append([None]*boardSize)
 
			rowStart=(affineCorners[0]*(boardSize-1-r)+affineCorners[1]*r) / (boardSize-1)
 
			rowEnd=(affineCorners[3]*(boardSize-1-r)+affineCorners[2]*r) / (boardSize-1)
 

	
 
			for c in range(boardSize):
 
				affineIntersection=(rowStart*(boardSize-1-c)+rowEnd*c) / (boardSize-1)
 
				self.intersections[r][c]=EPoint.fromProjective(transformPoint(affineIntersection.toProjective(),rectiMatrixInv))
 

	
 
	def stoneSizeAt(self,r,c):
 
		intersection=self.intersections[r][c]
 

	
 
		if c>0: width=intersection.x - self.intersections[r][c-1].x
 
		else: width=self.intersections[r][c+1].x - intersection.x
 
		if r>0: height=intersection.y - self.intersections[r-1][c].y
 
		else: height=self.intersections[r+1][c].y - intersection.y
 

	
 
		return (width,height)
src/gui/imgview.py
Show inline comments
 
@@ -18,90 +18,90 @@ class ImgView(ResizableCanvas):
 

	
 
		self._gui=gui
 
		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:
 
			w,h=self._img.size
 
			ratio=min(self._width/w, self._height/h)
 
			img=self._img.resize((int(w*ratio),int(h*ratio)))
 
			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 c in self._corners:
 
			self.markPoint(c.x,c.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():
 
		if self._corners.is_canon():
 
			# transform corners from show coordinates to real coordinates
 
			self._boardGrid=Grid(self._corners)
 
			corners=[self._transformPoint(c) for c in self._corners]
 
			self._gui.detector.setCorners(corners)
 
			self._gui.preview()
 

	
 
		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)==4:
 
			self._boardGrid=Grid(self._corners)
 
		self.redraw()
 

	
 
	def _transformPoint(self,point):
 
		w=int(self._width)
 
		h=int(self._height)
 
		wo,ho=self._img.size # o for original
 
		log.debug("image: %sx%s, view: %sx%s",wo,ho,w,h)
 
		widthRatio=wo/w
 
		heightRatio=ho/h
 
		imgSizeCoef=max(widthRatio,heightRatio)
 
		# shift compensates possible horizontal or vertical empty margins from unmatching aspect ratios
 
		imgShift=EPoint(wo-w*imgSizeCoef,ho-h*imgSizeCoef)/2
 
		return EPoint(self.canvasx(point.x),self.canvasy(point.y)) * imgSizeCoef + imgShift
0 comments (0 inline, 0 general)