255 lines
9.5 KiB
Python
255 lines
9.5 KiB
Python
'''
|
|
(*)~----------------------------------------------------------------------------------
|
|
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
|
|
#logging
|
|
import logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_map_from_cloud(cal_pt_cloud,screen_size=(2,2),threshold = 35,return_inlier_map=False,return_params=False):
|
|
"""
|
|
we do a simple two pass fitting to a pair of bi-variate polynomials
|
|
return the function to map vector
|
|
"""
|
|
# fit once using all avaiable data
|
|
model_n = 7
|
|
cx,cy,err_x,err_y = fit_poly_surface(cal_pt_cloud,model_n)
|
|
err_dist,err_mean,err_rms = fit_error_screen(err_x,err_y,screen_size)
|
|
if cal_pt_cloud[err_dist<=threshold].shape[0]: #did not disregard all points..
|
|
# fit again disregarding extreme outliers
|
|
cx,cy,new_err_x,new_err_y = fit_poly_surface(cal_pt_cloud[err_dist<=threshold],model_n)
|
|
map_fn = make_map_function(cx,cy,model_n)
|
|
new_err_dist,new_err_mean,new_err_rms = fit_error_screen(new_err_x,new_err_y,screen_size)
|
|
|
|
logger.info('first iteration. root-mean-square residuals: %s, in pixel' %err_rms)
|
|
logger.info('second iteration: ignoring outliers. root-mean-square residuals: %s in pixel',new_err_rms)
|
|
|
|
logger.info('used %i data points out of the full dataset %i: subset is %i percent' \
|
|
%(cal_pt_cloud[err_dist<=threshold].shape[0], cal_pt_cloud.shape[0], \
|
|
100*float(cal_pt_cloud[err_dist<=threshold].shape[0])/cal_pt_cloud.shape[0]))
|
|
|
|
if return_inlier_map and return_params:
|
|
return map_fn,err_dist<=threshold,(cx,cy,model_n)
|
|
if return_inlier_map and not return_params:
|
|
return map_fn,err_dist<=threshold
|
|
if return_params and not return_inlier_map:
|
|
return map_fn,(cx,cy,model_n)
|
|
return map_fn
|
|
else: # did disregard all points. The data cannot be represented by the model in a meaningful way:
|
|
map_fn = make_map_function(cx,cy,model_n)
|
|
logger.info('First iteration. root-mean-square residuals: %s in pixel, this is bad!'%err_rms)
|
|
logger.warning('The data cannot be represented by the model in a meaningfull way.')
|
|
|
|
if return_inlier_map and return_params:
|
|
return map_fn,err_dist<=threshold,(cx,cy,model_n)
|
|
if return_inlier_map and not return_params:
|
|
return map_fn,err_dist<=threshold
|
|
if return_params and not return_inlier_map:
|
|
return map_fn,(cx,cy,model_n)
|
|
return map_fn
|
|
|
|
|
|
|
|
def fit_poly_surface(cal_pt_cloud,n=7):
|
|
M = make_model(cal_pt_cloud,n)
|
|
U,w,Vt = np.linalg.svd(M[:,:n],full_matrices=0)
|
|
V = Vt.transpose()
|
|
Ut = U.transpose()
|
|
pseudINV = np.dot(V, np.dot(np.diag(1/w), Ut))
|
|
cx = np.dot(pseudINV, M[:,n])
|
|
cy = np.dot(pseudINV, M[:,n+1])
|
|
# compute model error in world screen units if screen_res specified
|
|
err_x=(np.dot(M[:,:n],cx)-M[:,n])
|
|
err_y=(np.dot(M[:,:n],cy)-M[:,n+1])
|
|
return cx,cy,err_x,err_y
|
|
|
|
def fit_error_screen(err_x,err_y,(screen_x,screen_y)):
|
|
err_x *= screen_x/2.
|
|
err_y *= screen_y/2.
|
|
err_dist=np.sqrt(err_x*err_x + err_y*err_y)
|
|
err_mean=np.sum(err_dist)/len(err_dist)
|
|
err_rms=np.sqrt(np.sum(err_dist*err_dist)/len(err_dist))
|
|
return err_dist,err_mean,err_rms
|
|
|
|
def make_model(cal_pt_cloud,n=7):
|
|
n_points = cal_pt_cloud.shape[0]
|
|
|
|
if n==3:
|
|
X=cal_pt_cloud[:,0]
|
|
Y=cal_pt_cloud[:,1]
|
|
Ones=np.ones(n_points)
|
|
ZX=cal_pt_cloud[:,2]
|
|
ZY=cal_pt_cloud[:,3]
|
|
M=np.array([X,Y,Ones,ZX,ZY]).transpose()
|
|
|
|
elif n==7:
|
|
X=cal_pt_cloud[:,0]
|
|
Y=cal_pt_cloud[:,1]
|
|
XX=X*X
|
|
YY=Y*Y
|
|
XY=X*Y
|
|
XXYY=XX*YY
|
|
Ones=np.ones(n_points)
|
|
ZX=cal_pt_cloud[:,2]
|
|
ZY=cal_pt_cloud[:,3]
|
|
M=np.array([X,Y,XX,YY,XY,XXYY,Ones,ZX,ZY]).transpose()
|
|
|
|
elif n==9:
|
|
X=cal_pt_cloud[:,0]
|
|
Y=cal_pt_cloud[:,1]
|
|
XX=X*X
|
|
YY=Y*Y
|
|
XY=X*Y
|
|
XXYY=XX*YY
|
|
XXY=XX*Y
|
|
YYX=YY*X
|
|
Ones=np.ones(n_points)
|
|
ZX=cal_pt_cloud[:,2]
|
|
ZY=cal_pt_cloud[:,3]
|
|
M=np.array([X,Y,XX,YY,XY,XXYY,XXY,YYX,Ones,ZX,ZY]).transpose()
|
|
else:
|
|
raise Exception("ERROR: Model n needs to be 3, 7 or 9")
|
|
return M
|
|
|
|
|
|
def make_map_function(cx,cy,n):
|
|
if n==3:
|
|
def fn((X,Y)):
|
|
x2 = cx[0]*X + cx[1]*Y +cx[2]
|
|
y2 = cy[0]*X + cy[1]*Y +cy[2]
|
|
return x2,y2
|
|
|
|
elif n==7:
|
|
def fn((X,Y)):
|
|
x2 = cx[0]*X + cx[1]*Y + cx[2]*X*X + cx[3]*Y*Y + cx[4]*X*Y + cx[5]*Y*Y*X*X +cx[6]
|
|
y2 = cy[0]*X + cy[1]*Y + cy[2]*X*X + cy[3]*Y*Y + cy[4]*X*Y + cy[5]*Y*Y*X*X +cy[6]
|
|
return x2,y2
|
|
|
|
elif n==9:
|
|
def fn((X,Y)):
|
|
# X Y XX YY XY XXYY XXY YYX Ones
|
|
x2 = cx[0]*X + cx[1]*Y + cx[2]*X*X + cx[3]*Y*Y + cx[4]*X*Y + cx[5]*Y*Y*X*X + cx[6]*Y*X*X + cx[7]*Y*Y*X + cx[8]
|
|
y2 = cy[0]*X + cy[1]*Y + cy[2]*X*X + cy[3]*Y*Y + cy[4]*X*Y + cy[5]*Y*Y*X*X + cy[6]*Y*X*X + cy[7]*Y*Y*X + cy[8]
|
|
return x2,y2
|
|
else:
|
|
raise Exception("ERROR: Model n needs to be 3, 7 or 9")
|
|
|
|
return fn
|
|
|
|
|
|
def preprocess_data(pupil_pts,ref_pts):
|
|
'''small utility function to deal with timestamped but uncorrelated data
|
|
input must be lists that contain dicts with at least "timestamp" and "norm_pos"
|
|
'''
|
|
cal_data = []
|
|
|
|
if len(ref_pts)<=2:
|
|
return cal_data
|
|
|
|
cur_ref_pt = ref_pts.pop(0)
|
|
next_ref_pt = ref_pts.pop(0)
|
|
while True:
|
|
matched = []
|
|
while pupil_pts:
|
|
#select all points past the half-way point between current and next ref data sample
|
|
if pupil_pts[0]['timestamp'] <=(cur_ref_pt['timestamp']+next_ref_pt['timestamp'])/2.:
|
|
matched.append(pupil_pts.pop(0))
|
|
else:
|
|
for p_pt in matched:
|
|
#only use close points
|
|
if abs(p_pt['timestamp']-cur_ref_pt['timestamp']) <= 1/15.: #assuming 30fps + slack
|
|
data_pt = p_pt["norm_pos"][0], p_pt["norm_pos"][1],cur_ref_pt['norm_pos'][0],cur_ref_pt['norm_pos'][1]
|
|
cal_data.append(data_pt)
|
|
break
|
|
if ref_pts:
|
|
cur_ref_pt = next_ref_pt
|
|
next_ref_pt = ref_pts.pop(0)
|
|
else:
|
|
break
|
|
return cal_data
|
|
|
|
|
|
|
|
# if __name__ == '__main__':
|
|
# import matplotlib.pyplot as plt
|
|
# from matplotlib import cm
|
|
# from mpl_toolkits.mplot3d import Axes3D
|
|
|
|
# cal_pt_cloud = np.load('cal_pt_cloud.npy')
|
|
# # plot input data
|
|
# # Z = cal_pt_cloud
|
|
# # ax.scatter(Z[:,0],Z[:,1],Z[:,2], c= "r")
|
|
# # ax.scatter(Z[:,0],Z[:,1],Z[:,3], c= "b")
|
|
|
|
# # fit once
|
|
# model_n = 7
|
|
# cx,cy,err_x,err_y = fit_poly_surface(cal_pt_cloud,model_n)
|
|
# map_fn = make_map_function(cx,cy,model_n)
|
|
# err_dist,err_mean,err_rms = fit_error_screen(err_x,err_y,(1280,720))
|
|
# print err_rms,"in pixel"
|
|
# threshold =15 # err_rms*2
|
|
|
|
# # fit again disregarding crass outlines
|
|
# cx,cy,new_err_x,new_err_y = fit_poly_surface(cal_pt_cloud[err_dist<=threshold],model_n)
|
|
# map_fn = make_map_function(cx,cy,model_n)
|
|
# new_err_dist,new_err_mean,new_err_rms = fit_error_screen(new_err_x,new_err_y,(1280,720))
|
|
# print new_err_rms,"in pixel"
|
|
|
|
# print "using %i datapoints out of the full dataset %i: subset is %i percent" \
|
|
# %(cal_pt_cloud[err_dist<=threshold].shape[0], cal_pt_cloud.shape[0], \
|
|
# 100*float(cal_pt_cloud[err_dist<=threshold].shape[0])/cal_pt_cloud.shape[0])
|
|
|
|
# # plot residuals
|
|
# fig_error = plt.figure()
|
|
# plt.scatter(err_x,err_y,c="y")
|
|
# plt.scatter(new_err_x,new_err_y)
|
|
# plt.title("fitting residuals full data set (y) and better subset (b)")
|
|
|
|
|
|
# # plot projection of eye and world vs observed data
|
|
# X,Y,ZX,ZY = cal_pt_cloud.transpose().copy()
|
|
# X,Y = map_fn((X,Y))
|
|
# X *= 1280/2.
|
|
# Y *= 720/2.
|
|
# ZX *= 1280/2.
|
|
# ZY *= 720/2.
|
|
# fig_projection = plt.figure()
|
|
# plt.scatter(X,Y)
|
|
# plt.scatter(ZX,ZY,c='y')
|
|
# plt.title("world space projection in pixes, mapped and observed (y)")
|
|
|
|
# # plot the fitting functions 3D plot
|
|
# fig = plt.figure()
|
|
# ax = fig.gca(projection='3d')
|
|
# outliers =cal_pt_cloud[err_dist>threshold]
|
|
# inliers = cal_pt_cloud[err_dist<=threshold]
|
|
# ax.scatter(outliers[:,0],outliers[:,1],outliers[:,2], c= "y")
|
|
# ax.scatter(outliers[:,0],outliers[:,1],outliers[:,3], c= "y")
|
|
# ax.scatter(inliers[:,0],inliers[:,1],inliers[:,2], c= "r")
|
|
# ax.scatter(inliers[:,0],inliers[:,1],inliers[:,3], c= "b")
|
|
# Z = cal_pt_cloud
|
|
# X = np.linspace(min(Z[:,0])-.2,max(Z[:,0])+.2,num=30,endpoint=True)
|
|
# Y = np.linspace(min(Z[:,1])-.2,max(Z[:,1]+.2),num=30,endpoint=True)
|
|
# X, Y = np.meshgrid(X,Y)
|
|
# ZX,ZY = map_fn((X,Y))
|
|
# ax.plot_surface(X, Y, ZX, rstride=1, cstride=1, linewidth=.1, antialiased=True,alpha=0.4,color='r')
|
|
# ax.plot_surface(X, Y, ZY, rstride=1, cstride=1, linewidth=.1, antialiased=True,alpha=0.4,color='b')
|
|
# plt.xlabel("Pupil x in Eye-Space")
|
|
# plt.ylabel("Pupil y Eye-Space")
|
|
# plt.title("Z: Gaze x (blue) Gaze y (red) World-Space, yellow=outliers")
|
|
|
|
# # X,Y,_,_ = cal_pt_cloud.transpose()
|
|
|
|
# # pts= map_fn((X,Y))
|
|
# # import cv2
|
|
# # pts = np.array(pts,dtype=np.float32).transpose()
|
|
# # print cv2.convexHull(pts)[:,0]
|
|
# plt.show()
|