''' (*)~---------------------------------------------------------------------------------- Pupil - eye tracking platform Copyright (C) 2012-2015 Pupil Labs Distributed under the terms of the CC BY-NC-SA License. License details are in the file license.txt, distributed as part of this software. ----------------------------------------------------------------------------------~(*) ''' import numpy as np try: import numexpr as ne except: ne = None import cv2 import logging logger = logging.getLogger(__name__) class Roi(object): """this is a simple 2D Region of Interest class it is applied on numpy arrays for convenient slicing like this: roi_array_slice = full_array[r.view] # do something with roi_array_slice this creates a view, no data copying done """ def __init__(self, array_shape): self.array_shape = array_shape self.lX = 0 self.lY = 0 self.uX = array_shape[1] self.uY = array_shape[0] self.nX = 0 self.nY = 0 @property def view(self): return slice(self.lY,self.uY,),slice(self.lX,self.uX) @view.setter def view(self, value): raise Exception('The view field is read-only. Use the set methods instead') def add_vector(self,(x,y)): """ adds the roi offset to a len2 vector """ return (self.lX+x,self.lY+y) def sub_vector(self,(x,y)): """ subs the roi offset to a len2 vector """ return (x-self.lX,y-self.lY) def set(self,vals): if vals is not None and len(vals) is 5: if vals[-1] == self.array_shape: self.lX,self.lY,self.uX,self.uY,_ = vals else: logger.info('Image size has changed: Region of Interest has been reset') elif vals is not None and len(vals) is 4: self.lX,self.lY,self.uX,self.uY= vals def get(self): return self.lX,self.lY,self.uX,self.uY,self.array_shape def bin_thresholding(image, image_lower=0, image_upper=256): binary_img = cv2.inRange(image, np.asarray(image_lower), np.asarray(image_upper)) return binary_img def make_eye_kernel(inner_size,outer_size): offset = (outer_size - inner_size)/2 inner_count = inner_size**2 outer_count = outer_size**2-inner_count val_inner = -1.0 / inner_count val_outer = -val_inner*inner_count/outer_count inner = np.ones((inner_size,inner_size),np.float32)*val_inner kernel = np.ones((outer_size,outer_size),np.float32)*val_outer kernel[offset:offset+inner_size,offset:offset+inner_size]= inner return kernel def dif_gaus(image, lower, upper): lower, upper = int(lower-1), int(upper-1) lower = cv2.GaussianBlur(image,ksize=(lower,lower),sigmaX=0) upper = cv2.GaussianBlur(image,ksize=(upper,upper),sigmaX=0) # upper +=50 # lower +=50 dif = lower-upper # dif *= .1 # dif = cv2.medianBlur(dif,3) # dif = 255-dif dif = cv2.inRange(dif, np.asarray(200),np.asarray(256)) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5,5)) dif = cv2.dilate(dif, kernel, iterations=2) dif = cv2.erode(dif, kernel, iterations=1) # dif = cv2.max(image,dif) # dif = cv2.dilate(dif, kernel, iterations=1) return dif def equalize(image, image_lower=0.0, image_upper=255.0): image_lower = int(image_lower*2)/2 image_lower +=1 image_lower = max(3,image_lower) mean = cv2.medianBlur(image,255) image = image - (mean-100) # kernel = cv2.getStructuringElement(cv2.MORPH_CROSS, (3,3)) # cv2.dilate(image, kernel, image, iterations=1) return image def erase_specular(image,lower_threshold=0.0, upper_threshold=150.0): """erase_specular: removes specular reflections within given threshold using a binary mask (hi_mask) """ thresh = cv2.inRange(image, np.asarray(float(lower_threshold)), np.asarray(256.0)) kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7,7)) hi_mask = cv2.dilate(thresh, kernel, iterations=2) specular = cv2.inpaint(image, hi_mask, 2, flags=cv2.INPAINT_TELEA) # return cv2.max(hi_mask,image) return specular def find_hough_circles(img): circles = cv2.HoughCircles(pupil_img,cv2.cv.CV_HOUGH_GRADIENT,1,20, param1=50,param2=30,minRadius=0,maxRadius=80) if circles is not None: circles = np.uint16(np.around(circles)) for i in circles[0,:]: # draw the outer circle cv2.circle(img,(i[0],i[1]),i[2],(0,255,0),2) # draw the center of the circle cv2.circle(img,(i[0],i[1]),2,(0,0,255),3) def chessboard(image, pattern_size=(9,5)): status, corners = cv2.findChessboardCorners(image, pattern_size, flags=4) if status: mean = corners.sum(0)/corners.shape[0] # mean is [[x,y]] return mean[0], corners else: return None def curvature(c): try: from vector import Vector except: return c = c[:,0] curvature = [] for i in xrange(len(c)-2): #find the angle at i+1 frm = Vector(c[i]) at = Vector(c[i+1]) to = Vector(c[i+2]) a = frm -at b = to -at angle = a.angle(b) curvature.append(angle) return curvature def GetAnglesPolyline(polyline,closed=False): """ see: http://stackoverflow.com/questions/3486172/angle-between-3-points ported to numpy returns n-2 signed angles """ points = polyline[:,0] if closed: a = np.roll(points,1,axis=0) b = points c = np.roll(points,-1,axis=0) else: a = points[0:-2] # all "a" points b = points[1:-1] # b c = points[2:] # c points # ab = b.x - a.x, b.y - a.y ab = b-a # cb = b.x - c.x, b.y - c.y cb = b-c # float dot = (ab.x * cb.x + ab.y * cb.y); # dot product # print 'ab:',ab # print 'cb:',cb # float dot = (ab.x * cb.x + ab.y * cb.y) dot product # dot = np.dot(ab,cb.T) # this is a full matrix mulitplication we only need the diagonal \ # dot = dot.diagonal() # because all we look for are the dotproducts of corresponding vectors (ab[n] and cb[n]) dot = np.sum(ab * cb, axis=1) # or just do the dot product of the correspoing vectors in the first place! # float cross = (ab.x * cb.y - ab.y * cb.x) cross product cros = np.cross(ab,cb) # float alpha = atan2(cross, dot); alpha = np.arctan2(cros,dot) return alpha*(180./np.pi) #degrees # return alpha #radians # if ne: # def GetAnglesPolyline(polyline): # """ # see: http://stackoverflow.com/questions/3486172/angle-between-3-points # ported to numpy # returns n-2 signed angles # same as above but implemented using numexpr # SLOWER than just numpy! # """ # points = polyline[:,0] # a = points[0:-2] # all "a" points # b = points[1:-1] # b # c = points[2:] # c points # ax,ay = a[:,0],a[:,1] # bx,by = b[:,0],b[:,1] # cx,cy = c[:,0],c[:,1] # # abx = '(bx - ax)' # # aby = '(by - ay)' # # cbx = '(bx - cx)' # # cby = '(by - cy)' # # # float dot = (ab.x * cb.x + ab.y * cb.y) dot product # # dot = '%s * %s + %s * %s' %(abx,cbx,aby,cby) # # # float cross = (ab.x * cb.y - ab.y * cb.x) cross product # # cross = '(%s * %s - %s * %s)' %(abx,cby,aby,cbx) # # # float alpha = atan2(cross, dot); # # alpha = "arctan2(%s,%s)" %(cross,dot) # # term = '%s*%s'%(alpha,180./np.pi) # term = 'arctan2(((bx - ax) * (by - cy) - (by - ay) * (bx - cx)),(bx - ax) * (bx - cx) + (by - ay) * (by - cy))*57.2957795131' # return ne.evaluate(term) def split_at_angle(contour, curvature, angle): """ contour is array([[[108, 290]],[[111, 290]]], dtype=int32) shape=(number of points,1,dimension(2) ) curvature is a n-2 list """ segments = [] kink_index = [i for i in range(len(curvature)) if curvature[i] < angle] for s,e in zip([0]+kink_index,kink_index+[None]): # list of slice indecies 0,i0,i1,i2,None if e is not None: segments.append(contour[s:e+1]) #need to include the last index else: segments.append(contour[s:e]) return segments def find_kink(curvature, angle): """ contour is array([[[108, 290]],[[111, 290]]], dtype=int32) shape=(number of points,1,dimension(2) ) curvature is a n-2 list """ kinks = [] kink_index = [i for i in range(len(curvature)) if abs(curvature[i]) < angle] return kink_index def find_change_in_general_direction(curvature): """ return indecies of where the singn of curvature has flipped """ curv_pos = curvature > 0 split = [] currently_pos = curv_pos[0] for c, is_pos in zip(range(curvature.shape[0]),curv_pos): if is_pos !=currently_pos: currently_pos = is_pos split.append(c) return split def find_kink_and_dir_change(curvature,angle): split = [] if curvature.shape[0] == 0: return split curv_pos = curvature > 0 currently_pos = curv_pos[0] for idx,c, is_pos in zip(range(curvature.shape[0]),curvature,curv_pos): if (is_pos !=currently_pos) or abs(c) < angle: currently_pos = is_pos split.append(idx) return split def find_slope_disc(curvature,angle = 15): # this only makes sense when your polyline is longish if len(curvature)<4: return [] i = 2 split_idx = [] for anchor1,anchor2,candidate in zip(curvature,curvature[1:],curvature[2:]): base_slope = anchor2-anchor1 new_slope = anchor2 - candidate dif = abs(base_slope-new_slope) if dif>=angle: split_idx.add(i) print i,dif i +=1 return split_list def find_slope_disc_test(curvature,angle = 15): # this only makes sense when your polyline is longish if len(curvature)<4: return [] # mean = np.mean(curvature) # print '------------------- start' i = 2 split_idx = set() for anchor1,anchor2,candidate in zip(curvature,curvature[1:],curvature[2:]): base_slope = anchor2-anchor1 new_slope = anchor2 - candidate dif = abs(base_slope-new_slope) if dif>=angle: split_idx.add(i) # print i,dif i +=1 i-= 3 for anchor1,anchor2,candidate in zip(curvature[::-1],curvature[:-1:][::-1],curvature[:-2:][::-1]): avg = (anchor1+anchor2)/2. dif = abs(avg-candidate) if dif>=angle: split_idx.add(i) # print i,dif i -=1 split_list = list(split_idx) split_list.sort() # print split_list # print '-------end' return split_list def points_at_corner_index(contour,index): """ contour is array([[[108, 290]],[[111, 290]]], dtype=int32) shape=(number of points,1,dimension(2) ) #index n-2 because the curvature is n-2 (1st and last are not exsistent), this shifts the index (0 splits at first knot!) """ return [contour[i+1] for i in index] def split_at_corner_index(contour,index): """ contour is array([[[108, 290]],[[111, 290]]], dtype=int32) shape=(number of points,1,dimension(2) ) #index n-2 because the curvature is n-2 (1st and last are not exsistent), this shifts the index (0 splits at first knot!) """ segments = [] index = [i+1 for i in index] for s,e in zip([0]+index,index+[10000000]): # list of slice indecies 0,i0,i1,i2, segments.append(contour[s:e+1])# +1 is for not loosing line segments return segments def convexity_defect(contour, curvature): """ contour is array([[[108, 290]],[[111, 290]]], dtype=int32) shape=(number of points,1,dimension(2) ) curvature is a n-2 list """ kinks = [] mean = np.mean(curvature) if mean>0: kink_index = [i for i in range(len(curvature)) if curvature[i] < 0] else: kink_index = [i for i in range(len(curvature)) if curvature[i] > 0] for s in kink_index: # list of slice indecies 0,i0,i1,i2,None kinks.append(contour[s+1]) # because the curvature is n-2 (1st and last are not exsistent) return kinks,kink_index def is_round(ellipse,ratio,tolerance=.8): center, (axis1,axis2), angle = ellipse if axis1 and axis2 and abs( ratio - min(axis2,axis1)/max(axis2,axis1)) < tolerance: return True else: return False def size_deviation(ellipse,target_size): center, axis, angle = ellipse return abs(target_size-max(axis)) def circle_grid(image, pattern_size=(4,11)): """Circle grid: finds an assymetric circle pattern - circle_id: sorted from bottom left to top right (column first) - If no circle_id is given, then the mean of circle positions is returned approx. center - If no pattern is detected, function returns None """ status, centers = cv2.findCirclesGridDefault(image, pattern_size, flags=cv2.CALIB_CB_ASYMMETRIC_GRID) if status: return centers else: return None def calibrate_camera(img_pts, obj_pts, img_size): # generate pattern size camera_matrix = np.zeros((3,3)) dist_coef = np.zeros(4) rms, camera_matrix, dist_coefs, rvecs, tvecs = cv2.calibrateCamera(obj_pts, img_pts, img_size, camera_matrix, dist_coef) return camera_matrix, dist_coefs def gen_pattern_grid(size=(4,11)): pattern_grid = [] for i in xrange(size[1]): for j in xrange(size[0]): pattern_grid.append([(2*j)+i%2,i,0]) return np.asarray(pattern_grid, dtype='f4') def normalize(pos, (width, height),flip_y=False): """ normalize return as float """ x = pos[0] y = pos[1] x /=float(width) y /=float(height) if flip_y: return x,1-y return x,y def denormalize(pos, (width, height), flip_y=False): """ denormalize """ x = pos[0] y = pos[1] x *= width if flip_y: y = 1-y y *= height return x,y def dist_pts_ellipse(((ex,ey),(dx,dy),angle),points): """ return unsigned euclidian distances of points to ellipse """ pts = np.float64(points) rx,ry = dx/2., dy/2. angle = (angle/180.)*np.pi # ex,ey =ex+0.000000001,ey-0.000000001 #hack to make 0 divisions possible this is UGLY!!! pts = pts - np.array((ex,ey)) # move pts to ellipse appears at origin , with this we copy data -deliberatly! M_rot = np.mat([[np.cos(angle),-np.sin(angle)],[np.sin(angle),np.cos(angle)]]) pts = np.array(pts*M_rot) #rotate so that ellipse axis align with coordinate system # print "rotated",pts pts /= np.array((rx,ry)) #normalize such that ellipse radii=1 # print "normalize",norm_pts norm_mag = np.sqrt((pts*pts).sum(axis=1)) norm_dist = abs(norm_mag-1) #distance of pt to ellipse in scaled space # print 'norm_mag',norm_mag # print 'norm_dist',norm_dist ratio = (norm_dist)/norm_mag #scale factor to make the pts represent their dist to ellipse # print 'ratio',ratio scaled_error = np.transpose(pts.T*ratio) # per vector scalar multiplication: makeing sure that boradcasting is done right # print "scaled error points", scaled_error real_error = scaled_error*np.array((rx,ry)) # print "real point",real_error error_mag = np.sqrt((real_error*real_error).sum(axis=1)) # print 'real_error',error_mag # print 'result:',error_mag return error_mag if ne: def dist_pts_ellipse(((ex,ey),(dx,dy),angle),points): """ return unsigned euclidian distances of points to ellipse same as above but uses numexpr for 2x speedup """ pts = np.float64(points) pts.shape=(-1,2) rx,ry = dx/2., dy/2. angle = (angle/180.)*np.pi # ex,ey = ex+0.000000001 , ey-0.000000001 #hack to make 0 divisions possible this is UGLY!!! x = pts[:,0] y = pts[:,1] # px = '((x-ex) * cos(angle) + (y-ey) * sin(angle))/rx' # py = '(-(x-ex) * sin(angle) + (y-ey) * cos(angle))/ry' # norm_mag = 'sqrt(('+px+')**2+('+py+')**2)' # norm_dist = 'abs('+norm_mag+'-1)' # ratio = norm_dist + "/" + norm_mag # x_err = ''+px+'*'+ratio+'*rx' # y_err = ''+py+'*'+ratio+'*ry' # term = 'sqrt(('+x_err+')**2 + ('+y_err+')**2 )' term = 'sqrt((((x-ex) * cos(angle) + (y-ey) * sin(angle))/rx*abs(sqrt((((x-ex) * cos(angle) + (y-ey) * sin(angle))/rx)**2+((-(x-ex) * sin(angle) + (y-ey) * cos(angle))/ry)**2)-1)/sqrt((((x-ex) * cos(angle) + (y-ey) * sin(angle))/rx)**2+((-(x-ex) * sin(angle) + (y-ey) * cos(angle))/ry)**2)*rx)**2 + ((-(x-ex) * sin(angle) + (y-ey) * cos(angle))/ry*abs(sqrt((((x-ex) * cos(angle) + (y-ey) * sin(angle))/rx)**2+((-(x-ex) * sin(angle) + (y-ey) * cos(angle))/ry)**2)-1)/sqrt((((x-ex) * cos(angle) + (y-ey) * sin(angle))/rx)**2+((-(x-ex) * sin(angle) + (y-ey) * cos(angle))/ry)**2)*ry)**2 )' error_mag = ne.evaluate(term) return error_mag def metric(l): """ example metric for search """ # print 'evaluating', idecies global evals evals +=1 return sum(l) < 3 def pruning_quick_combine(l,fn,seed_idx=None,max_evals=1e20,max_depth=5): """ l is a list of object to quick_combine. the evaluation fn should accept idecies to your list and the list it should return a binary result on wether this set is good this search finds all combinations but assumes: that a bad subset can not be bettered by adding more nodes that a good set may not always be improved by a 'passing' superset (purging subsets will revoke this) if all items and their combinations pass the evaluation fn you get n**2 -1 solutions which leads to (2**n - 1) calls of your evaluation fn it needs more evaluations than finding strongly connected components in a graph because: (1,5) and (1,6) and (5,6) may work but (1,5,6) may not pass evaluation, (n,m) being list idx's """ if seed_idx: non_seed_idx = [i for i in range(len(l)) if i not in seed_idx] else: #start from every item seed_idx = range(len(l)) non_seed_idx = [] mapping = seed_idx+non_seed_idx unknown = [[node] for node in range(len(seed_idx))] # print mapping results = [] prune = [] while unknown and max_evals: path = unknown.pop(0) max_evals -= 1 # print '@idx',[mapping[i] for i in path] # print '@content',path if not len(path) > max_depth: # is this combination even viable, or did a subset fail already? if not any(m.issubset(set(path)) for m in prune): #we have not tested this and a subset of this was sucessfull before if fn([l[mapping[i]] for i in path]): # yes this was good, keep as solution results.append([mapping[i] for i in path]) # lets explore more by creating paths to each remaining node decedents = [path+[i] for i in range(path[-1]+1,len(mapping)) ] unknown.extend(decedents) else: # print "pruning",path prune.append(set(path)) return results # def is_subset(needle,haystack): # """ Check if needle is ordered subset of haystack in O(n) # taken from: # http://stackoverflow.com/questions/1318935/python-list-filtering-remove-subsets-from-list-of-lists # """ # if len(haystack) < len(needle): return False # index = 0 # for element in needle: # try: # index = haystack.index(element, index) + 1 # except ValueError: # return False # else: # return True # def filter_subsets(lists): # """ Given list of lists, return new list of lists without subsets # taken from: # http://stackoverflow.com/questions/1318935/python-list-filtering-remove-subsets-from-list-of-lists # """ # for needle in lists: # if not any(is_subset(needle, haystack) for haystack in lists # if needle is not haystack): # yield needle def filter_subsets(l): return [m for i, m in enumerate(l) if not any(set(m).issubset(set(n)) for n in (l[:i] + l[i+1:]))] if __name__ == '__main__': # tst = [] # for x in range(10): # tst.append(gen_pattern_grid()) # tst = np.asarray(tst) # print tst.shape #test polyline # *-* * # | \ | # * *-* # | # *-* pl = np.array([[[0, 0]],[[0, 1]],[[1, 1]],[[2, 1]],[[2, 2]],[[1, 3]],[[1, 4]],[[2,4]]], dtype=np.int32) curvature = GetAnglesPolyline(pl,closed=0) print curvature curvature = GetAnglesPolyline(pl,closed=1) # print curvature # print find_curv_disc(curvature) # idx = find_kink_and_dir_change(curvature,60) # print idx # print split_at_corner_index(pl,idx) # ellipse = ((0,0),(np.sqrt(2),np.sqrt(2)),0) # pts = np.array([(0,1),(.5,.5),(0,-1)]) # # print pts.dtype # print dist_pts_ellipse(ellipse,pts) # print pts # # print test() # l = [1,2,1,0,1,0] # print len(l) # # evals = 0 # # r = quick_combine(l,metric) # # # print r # # print filter_subsets(r) # # print evals # evals = 0 # r = pruning_quick_combine(l,metric,[2]) # print r # print filter_subsets(r) # print evals