Changeset - 739df5e211d8
[Not reviewed]
default
0 3 0
Laman - 6 years ago 2019-03-02 14:54:49

fixed Hough transform input/ouput coordinates, fixed lines detection
3 files changed with 64 insertions and 15 deletions:
0 comments (0 inline, 0 general)
exp/geometry.py
Show inline comments
 
import math
 

	
 
from analyzer.epoint import EPoint
 

	
 

	
 
class Line:
 
	def __init__(self,alpha,d):
 
		self._alpha=alpha
 
		self._d=d
 
		self._sin=math.sin(alpha)
 
		self._cos=math.cos(alpha)
 

	
 
	@staticmethod
 
	def fromNormal(a,b,c):
 
		"""ax + by + c = 0"""
 
		sign=-c/abs(c) if c!=0 else 1
 
		norm=sign*math.sqrt(a**2+b**2)
 
		(a_,b_,c_)=(a/norm,b/norm,c/norm)
 
		alpha=math.acos(a_) if b_>=0 else 2*math.pi-math.acos(a_)
 
		return Line(alpha,-c_)
 

	
 
	@staticmethod
 
	def fromPoints(a,b):
 
		return Line.fromNormal(a.y-b.y, b.x-a.x, (b.y-a.y)*a.x+(a.x-b.x)*a.y)
 

	
 
	def toNormal(self):
 
		# https://en.wikipedia.org/wiki/Line_(mathematics)#In_normal_form
 
		"""ax + by + c = 0"""
 
		return (self._cos, self._sin, -self._d)
 

	
 
	def intersect(self,line):
 
		if self._alpha==line._alpha: return None
 
		(a,b,c)=self.toNormal()
 
		(d,e,f)=line.toNormal()
 
		x=(b*f-c*e)/(a*e-b*d)
 
		y=(c*d-a*f)/(a*e-b*d)
 
		return EPoint(x,y)
 

	
 
	def distanceTo(self,point):
 
		# https://en.wikipedia.org/wiki/Point-line_distance#Line_defined_by_an_equation
 
		(a,b,c)=self.toNormal()
 
		return abs(a*point.x+b*point.y+c) # a**2 + b**2 == 1 for Hesse normal form
 

	
 
	def __str__(self): return "({0},{1})".format(self._alpha,self._d)
 
	def __repr__(self): return "Line({0},{1})".format(repr(self._alpha),repr(self._d))
 
	def shiftBasis(self,newBasis):
 
		(a,b,c)=self.toNormal()
 
		if a!=0:
 
			point=EPoint(-c/a,0)
 
		else:
 
			point=EPoint(0,-c/b)
 
		(x_,y_)=point-newBasis
 
		c_=-a*x_-b*y_
 
		return Line.fromNormal(a,b,c_)
 

	
 
	@property
 
	def alpha(self): return self._alpha
 

	
 
	@property
 
	def d(self): return self._d
 

	
 
	def __str__(self): return "Line({0},{1})".format(round(self._alpha,3),round(self._d))
 
	def __repr__(self): return "Line({0},{1})".format(self._alpha,self._d)
 

	
 

	
 
def angleDiff(alpha,beta):
 
	diff=abs(alpha-beta)
 
	if diff>math.pi: diff=2*math.pi-diff
 
	return diff
exp/hough.py
Show inline comments
 
import sys
 
sys.path.append("../src")
 

	
 
import math
 
from datetime import datetime
 
import logging as log
 

	
 
import numpy as np
 
import scipy.optimize
 
import scipy.signal
 
import cv2 as cv
 

	
 
from geometry import EPoint,Line
 

	
 
DEBUG=True
 

	
 

	
 
class LineBag:
 
	def __init__(self):
 
		self._lines=[]
 

	
 
	def put(self,score,alpha,beta,peaks):
 
		self._lines.append((score,alpha,beta,peaks))
 

	
 
	def pull(self,count):
 
		self._lines.sort(reverse=True)
 
		res=[]
 
		for (score,alpha,beta,peaks) in self._lines:
 
			if any(abs(alpha-gamma)<10 and abs(beta-delta)<10 for (_,gamma,delta,_) in res): continue
 
			if any((beta-delta)!=0 and (alpha-gamma)/(beta-delta)<0 for (_,gamma,delta,_) in res): continue
 
			res.append((score,alpha,beta,peaks))
 
			if len(res)>=count: break
 
		return res
 

	
 

	
 
class HoughTransform:
 
	"""Find line sequences with Hough transform.
 

	
 
	Uses usual image coordinates on input and output, with [0,0] in the upper left corner and [height-1,width-1] in the lower right.
 
	However, internally it uses the usual cartesian coordinates, centered at the image center. [-w/2,-h/2] in the upper left and [w/2,h/2] in the lower right."""
 
	def __init__(self,img):
 
		self._angleBandwidth=30 # degrees
 

	
 
		(h,w)=img.shape[:2]
 
		self._diagLen=int(np.sqrt(h**2+w**2))+1
 
		self._center=(w//2,h//2)
 
		self._acc=np.zeros((180,self._diagLen),dtype=np.int32)
 

	
 
		self.update(img)
 

	
 
	def extract(self):
 
		img=self._createImg()
 
		self.show(img)
 
		lines=self._detectLines()
 
		res=[]
 
		i=0
 
		for (score,alpha,beta,peaks) in lines:
 
			log.debug("score: %s",score)
 
			log.debug("alpha, beta: %s, %s",alpha,beta)
 
			self._drawLine(img,alpha,beta,peaks,i)
 

	
 
			keys=self._readLineKeys(alpha,beta)
 
			for k in peaks:
 
				(alphaDeg,d)=keys[k]
 
				alphaRad=alphaDeg*math.pi/180
 
				baseLine=Line(alphaRad,0)
 
				dd=baseLine.distanceTo(EPoint(*self._center)) # to shift d from the center to 0,0
 
				res.append(Line(alphaRad, dd+d-self._diagLen//2))
 
				line=Line(alphaDeg*math.pi/180,d-self._diagLen//2)
 
				res.append(self._transformOutput(line))
 
			i+=1
 

	
 
		self.show(img)
 
		return res
 

	
 
	def update(self,img,weight=1):
 
		start=datetime.now().timestamp()
 
		for (r,row) in enumerate(img):
 
			for (c,pix) in enumerate(row):
 
				if pix==0: continue
 
				for alphaDeg in range(0,180):
 
					d=self._computeDist(c,r,alphaDeg)+self._diagLen//2
 
					self._acc[(alphaDeg,d)]+=weight
 
		log.debug("Hough updated in %s s",round(datetime.now().timestamp()-start,3))
 

	
 
	def show(self,img=None):
 
		if img is None: img=self._createImg()
 
		show(img,"Hough transform accumulator")
 

	
 
	def _computeDist(self,x,y,alphaDeg):
 
		alphaRad=alphaDeg*math.pi/180
 
		(x0,y0)=self._center
 
		(dx,dy)=(x-x0,y-y0)
 
		(dx,dy)=(x-x0,y0-y)
 
		d=dx*math.cos(alphaRad)+dy*math.sin(alphaRad)
 
		return round(d)
 

	
 
	def _detectLines(self):
 
		bag=LineBag()
 
		for alpha in range(0,180,2):
 
			for beta in range(max(alpha-60,0),alpha+60,2):
 
		for alpha in range(0,180+60,2):
 
			for beta in range(max(alpha-60,0),min(alpha+60,180+60),2):
 
				accLine=[self._acc[key] for key in self._readLineKeys(alpha,beta)]
 
				(peaks,props)=scipy.signal.find_peaks(accLine,prominence=0)
 
				(prominences,peaks)=zip(*sorted(zip(props["prominences"],peaks),reverse=True)[:19])
 
				bag.put(sum(prominences),alpha,beta,peaks)
 
		return bag.pull(2)
 

	
 
	def _readLineKeys(self,alpha,beta):
 
		n=self._diagLen-1
 
		res=[]
 
		for i in range(n+1):
 
			k=round((alpha*(n-i)+beta*i)/n)
 
			if k<0 or k>=180:
 
			if k>=180:
 
				k=k%180
 
				i=n+1-i
 
				i=n-i
 
			res.append((k,i))
 
		return res
 

	
 
	def show(self,img=None):
 
		if img is None: img=self._createImg()
 

	
 
		show(img,"Hough transform accumulator")
 
	def _transformOutput(self,line):
 
		(x,y)=self._center
 
		basis=EPoint(-x,y)
 
		shiftedLine=line.shiftBasis(basis)
 
		reflectedLine=Line(math.pi*2-shiftedLine.alpha,shiftedLine.d)
 
		log.debug("%s -> %s",line,reflectedLine)
 
		return reflectedLine
 

	
 
	def _createImg(self):
 
		maxVal=self._acc.max()
 
		arr=np.expand_dims(np.uint8(255*self._acc//maxVal),axis=2)
 
		img=np.concatenate((arr,arr,arr),axis=2)
 

	
 
		(h,w)=img.shape[:2]
 

	
 
		for x in range(0,w,4): # y axis
 
			img[h//2,x]=[255,255,255]
 
		for y in range(0,h,4):
 
			img[y,w//2]=[255,255,255]
 

	
 
		return img
 

	
 
	def _drawLine(self,img,alpha,beta,peaks,colorKey):
 
		colors=[[0,255,255],[255,0,255],[255,255,0]]
 
		color=colors[colorKey]
 
		(h,w)=img.shape[:2]
 
		keys=self._readLineKeys(alpha,beta)
 
		for (y,x) in keys:
 
			if x%3!=0: continue
 
			if y<0 or y>=h: continue
 
			img[y,x]=color
 
		for k in peaks:
 
			(y,x)=keys[k]
 
			cv.drawMarker(img,(x,y),color,cv.MARKER_TILTED_CROSS,8)
 

	
 

	
 
def show(img,filename="x"):
 
	cv.imshow(filename,img)
 
	cv.waitKey(0)
 
	cv.destroyAllWindows()
 

	
 

	
 
def filterVert(edges):
 
	kernel = np.array([[1,0,1],[1,0,1],[1,0,1]],np.uint8)
 
	edges = cv.erode(edges,kernel)
 
	kernel=np.array([[0,1,0],[0,1,0],[0,1,0]],np.uint8)
 
	edges=cv.dilate(edges,kernel)
 
	return edges
 

	
 
def filterHor(edges):
 
	kernel = np.array([[1,1,1],[0,0,0],[1,1,1]],np.uint8)
 
	edges = cv.erode(edges,kernel)
 
	kernel=np.array([[0,0,0],[1,1,1],[0,0,0]],np.uint8)
 
	edges=cv.dilate(edges,kernel)
 
	return edges
 

	
 
def filterDiag(edges):
 
	kernel = np.array([[0,0,1],[1,0,0],[0,1,0]],np.uint8)
 
	edges1 = cv.erode(edges,kernel)
 
	kernel=np.array([[1,0,0],[0,1,0],[0,0,1]],np.uint8)
 
	edges1=cv.dilate(edges1,kernel)
 

	
 
	kernel = np.array([[0,1,0],[1,0,0],[0,0,1]],np.uint8)
 
	edges2 = cv.erode(edges,kernel)
 
	kernel=np.array([[0,0,1],[0,1,0],[1,0,0]],np.uint8)
 
	edges2=cv.dilate(edges2,kernel)
 

	
 
	return edges1+edges2
 

	
 
def prepareEdgeImg(img):
 
	gray=cv.cvtColor(img,cv.COLOR_BGR2GRAY)
 
	show(gray,"greyscale image")
 
	edges=cv.Canny(gray,70,130)
 
	show(edges,"Canny edge detector")
 
	edges=filterHor(edges)+filterVert(edges)+filterDiag(edges)
 
	show(edges,"kernel filtered edges")
 
	return edges
exp/tests/test_geometry.py
Show inline comments
 
import math
 
import random
 
from unittest import TestCase
 

	
 
from geometry import Line,EPoint
 

	
 
random.seed(361)
 

	
 

	
 
class TestLine(TestCase):
 
	def testFromNormal(self):
 
		p=Line.fromNormal(1,0,-1) # x-1=0
 
		self.assertEqual(p._alpha,0)
 
		self.assertEqual(p._d,1)
 

	
 
		q=Line.fromNormal(1,1,-2) # x+y-2=0
 
		self.assertAlmostEqual(q._alpha,math.pi/4)
 
		self.assertAlmostEqual(q._d,math.sqrt(2))
 

	
 
		r=Line.fromNormal(0,1,1) # y+1=0
 
		self.assertAlmostEqual(r._alpha,math.pi*3/2)
 
		self.assertEqual(r._d,1)
 

	
 
	def testFromPoints(self):
 
		ab=Line.fromPoints(EPoint(1,3),EPoint(1,-1))
 
		self.assertEqual(ab._alpha,0)
 
		self.assertEqual(ab._d,1)
 

	
 
		cd=Line.fromPoints(EPoint(0,2),EPoint(-1,3))
 
		self.assertAlmostEqual(cd._alpha,math.pi/4)
 
		self.assertAlmostEqual(cd._d,math.sqrt(2))
 

	
 
		ef=Line.fromPoints(EPoint(-2,-1),EPoint(-4,-1))
 
		self.assertAlmostEqual(ef._alpha,math.pi*3/2)
 
		self.assertEqual(ef._d,1)
 

	
 
	def testIntersect(self):
 
		for i in range(10):
 
			a=EPoint(random.randint(-100,100),random.randint(-100,100))
 
			b=EPoint(random.randint(-100,100),random.randint(-100,100))
 
			c=EPoint(random.randint(-100,100),random.randint(-100,100))
 
			ab=Line.fromPoints(a,b)
 
			ac=Line.fromPoints(a,c)
 
			a_=ab.intersect(ac)
 
			self.assertAlmostEqual(a.x,a_.x)
 
			self.assertAlmostEqual(a.y,a_.y)
 

	
 
	def testDistanceTo(self):
 
		p=Line(0,1)
 
		q=Line(math.pi/4,math.sqrt(2))
 
		r=Line(math.pi*3/2,1)
 

	
 
		a=EPoint(0,0)
 
		b=EPoint(1,0)
 
		c=EPoint(-1,-1)
 

	
 
		self.assertAlmostEqual(p.distanceTo(a),1)
 
		self.assertAlmostEqual(p.distanceTo(b),0)
 
		self.assertAlmostEqual(p.distanceTo(c),2)
 

	
 
		self.assertAlmostEqual(q.distanceTo(a),math.sqrt(2))
 
		self.assertAlmostEqual(q.distanceTo(b),math.sqrt(2)/2)
 
		self.assertAlmostEqual(q.distanceTo(c),2*math.sqrt(2))
 

	
 
		self.assertAlmostEqual(r.distanceTo(a),1)
 
		self.assertAlmostEqual(r.distanceTo(b),1)
 
		self.assertAlmostEqual(r.distanceTo(c),0)
 

	
 
	def testShiftBasis(self):
 
		newBasis=EPoint(-200,150)
 
		r=Line(0,-100)
 
		r_=r.shiftBasis(newBasis)
 
		self.assertAlmostEqual(r_._alpha,0)
 
		self.assertAlmostEqual(r_._d,100)
 

	
 
		s=Line(0,100)
 
		s_=s.shiftBasis(newBasis)
 
		self.assertAlmostEqual(s_._alpha,0)
 
		self.assertAlmostEqual(s_._d,300)
 

	
 
		newBasis=EPoint(-100,100)
 
		diag=100*math.sqrt(2)
 
		p=Line(math.pi*3/4,diag/2)
 
		p_=p.shiftBasis(newBasis)
 
		self.assertAlmostEqual(p_._alpha,math.pi*7/4)
 
		self.assertAlmostEqual(p_._d,diag/2)
 

	
 
		q=Line(math.pi*3/4,-diag/2)
 
		q_=q.shiftBasis(newBasis)
 
		self.assertAlmostEqual(q_._alpha,math.pi*7/4)
 
		self.assertAlmostEqual(q_._d,3/2*diag)
0 comments (0 inline, 0 general)