Last active
June 1, 2017 17:44
-
-
Save mkassner/cb02e399c46b81b6a1e45f86b94678c5 to your computer and use it in GitHub Desktop.
Chin rest calibration plugin
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
''' | |
(*)~--------------------------------------------------------------------------- | |
Pupil - eye tracking platform | |
Copyright (C) 2012-2017 Pupil Labs | |
Distributed under the terms of the GNU | |
Lesser General Public License (LGPL v3.0). | |
See COPYING and COPYING.LESSER for license details. | |
---------------------------------------------------------------------------~(*) | |
''' | |
import os | |
import cv2 | |
import numpy as np | |
from methods import normalize,denormalize | |
from gl_utils import adjust_gl_view,clear_gl_screen,basic_gl_setup | |
import OpenGL.GL as gl | |
from glfw import * | |
from circle_detector import find_concetric_circles | |
from file_methods import load_object,save_object | |
from platform import system | |
import audio | |
from pyglui import ui | |
from pyglui.cygl.utils import draw_points, draw_points_norm, draw_polyline, draw_polyline_norm, RGBA,draw_concentric_circles | |
from pyglui.pyfontstash import fontstash | |
from pyglui.ui import get_opensans_font_path | |
from calibration_routines.calibration_plugin_base import Calibration_Plugin | |
from calibration_routines.finish_calibration import finish_calibration | |
#logging | |
import logging | |
logger = logging.getLogger(__name__) | |
# window calbacks | |
def on_resize(window,w,h): | |
active_window = glfwGetCurrentContext() | |
glfwMakeContextCurrent(window) | |
adjust_gl_view(w,h) | |
glfwMakeContextCurrent(active_window) | |
# easing functions for animation of the marker fade in/out | |
def easeInOutQuad(t, b, c, d): | |
"""Robert Penner easing function examples at: http://gizma.com/easing/ | |
t = current time in frames or whatever unit | |
b = beginning/start value | |
c = change in value | |
d = duration | |
""" | |
t /= d/2 | |
if t < 1: | |
return c/2*t*t + b | |
t-=1 | |
return -c/2 * (t*(t-2) - 1) + b | |
def interp_fn(t,b,c,d,start_sample=15.,stop_sample=55.): | |
# ease in, sample, ease out | |
if t < start_sample: | |
return easeInOutQuad(t,b,c,start_sample) | |
elif t > stop_sample: | |
return 1-easeInOutQuad(t-stop_sample,b,c,d-stop_sample) | |
else: | |
return 1.0 | |
class Chin_Rest_Calibration(Calibration_Plugin): | |
"""Calibrate using a marker on your screen using a chinrest without world camera. | |
We use a ring detector that moves across the screen to 9 sites | |
Points are collected at sites - not between | |
""" | |
def __init__(self, g_pool,fullscreen=True,marker_scale=1.0,sample_duration=40): | |
super().__init__(g_pool) | |
self.detected = False | |
self.screen_marker_state = 0. | |
self.sample_duration = sample_duration # number of frames to sample per site | |
self.fixation_boost = sample_duration/2. | |
self.lead_in = 25 #frames of marker shown before starting to sample | |
self.lead_out = 5 #frames of markers shown after sampling is donw | |
self.active_site = None | |
self.sites = [] | |
self.display_pos = None | |
self.on_position = False | |
self.markers = [] | |
self.pos = None | |
self.marker_scale = marker_scale | |
self._window = None | |
self.menu = None | |
self.button = None | |
self.fullscreen = fullscreen | |
self.clicks_to_close = 5 | |
self.glfont = fontstash.Context() | |
self.glfont.add_font('opensans',get_opensans_font_path()) | |
self.glfont.set_size(32) | |
self.glfont.set_color_float((0.2,0.5,0.9,1.0)) | |
self.glfont.set_align_string(v_align='center') | |
# UI Platform tweaks | |
if system() == 'Linux': | |
self.window_position_default = (0, 0) | |
elif system() == 'Windows': | |
self.window_position_default = (8, 31) | |
else: | |
self.window_position_default = (0, 0) | |
def init_gui(self): | |
self.monitor_idx = 0 | |
self.monitor_names = [glfwGetMonitorName(m) for m in glfwGetMonitors()] | |
#primary_monitor = glfwGetPrimaryMonitor() | |
self.info = ui.Info_Text("Calibrate gaze parameters using a screen based animation.") | |
self.g_pool.calibration_menu.append(self.info) | |
self.menu = ui.Growing_Menu('Controls') | |
self.g_pool.calibration_menu.append(self.menu) | |
self.menu.append(ui.Selector('monitor_idx',self,selection = range(len(self.monitor_names)),labels=self.monitor_names,label='Monitor')) | |
self.menu.append(ui.Switch('fullscreen',self,label='Use fullscreen')) | |
self.menu.append(ui.Slider('marker_scale',self,step=0.1,min=0.5,max=2.0,label='Marker size')) | |
self.menu.append(ui.Slider('sample_duration',self,step=1,min=10,max=100,label='Sample duration')) | |
self.button = ui.Thumb('active',self,label='C',setter=self.toggle,hotkey='c') | |
self.button.on_color[:] = (.3,.2,1.,.9) | |
self.g_pool.quickbar.insert(0,self.button) | |
def deinit_gui(self): | |
if self.menu: | |
self.g_pool.calibration_menu.remove(self.menu) | |
self.g_pool.calibration_menu.remove(self.info) | |
self.menu = None | |
if self.button: | |
self.g_pool.quickbar.remove(self.button) | |
self.button = None | |
def start(self): | |
audio.say("Starting Calibration") | |
logger.info("Starting Calibration") | |
if self.g_pool.detection_mapping_mode == '3d': | |
self.sites = [ (.5, .5), | |
(0.,1.),(1.,1.), | |
(1., 0.),(0.,0.)] | |
else: | |
self.sites = [ (.25, .5), (0,.5), | |
(0.,1.),(.5,1.),(1.,1.), | |
(1.,.5), | |
(1., 0.),(.5, 0.),(0.,0.), | |
(.75,.5)] | |
self.active_site = self.sites.pop(0) | |
self.active = True | |
self.ref_list = [] | |
self.pupil_list = [] | |
self.clicks_to_close = 5 | |
self.open_window("Calibration") | |
def open_window(self, title='new_window'): | |
if not self._window: | |
if self.fullscreen: | |
monitor = glfwGetMonitors()[self.monitor_idx] | |
width, height, redBits, blueBits, greenBits, refreshRate = glfwGetVideoMode(monitor) | |
else: | |
monitor = None | |
width,height= 640,360 | |
self._window = glfwCreateWindow(width, height, title, monitor=monitor, share=glfwGetCurrentContext()) | |
if not self.fullscreen: | |
glfwSetWindowPos(self._window, self.window_position_default[0], self.window_position_default[1]) | |
glfwSetInputMode(self._window, GLFW_CURSOR, GLFW_CURSOR_HIDDEN) | |
# Register callbacks | |
glfwSetFramebufferSizeCallback(self._window, on_resize) | |
glfwSetKeyCallback(self._window, self.on_key) | |
glfwSetMouseButtonCallback(self._window, self.on_button) | |
on_resize(self._window, *glfwGetFramebufferSize(self._window)) | |
# gl_state settings | |
active_window = glfwGetCurrentContext() | |
glfwMakeContextCurrent(self._window) | |
basic_gl_setup() | |
# refresh speed settings | |
glfwSwapInterval(0) | |
glfwMakeContextCurrent(active_window) | |
def on_key(self,window, key, scancode, action, mods): | |
if action == GLFW_PRESS: | |
if key == GLFW_KEY_ESCAPE: | |
self.clicks_to_close = 0 | |
def on_button(self,window,button, action, mods): | |
if action ==GLFW_PRESS: | |
self.clicks_to_close -=1 | |
def stop(self): | |
# TODO: redundancy between all gaze mappers -> might be moved to parent class | |
audio.say("Stopping Calibration") | |
logger.info("Stopping Calibration") | |
self.smooth_pos = 0,0 | |
self.counter = 0 | |
self.close_window() | |
self.active = False | |
self.button.status_text = '' | |
print(self.ref_list) | |
finish_calibration(self.g_pool,self.pupil_list,self.ref_list) | |
def close_window(self): | |
if self._window: | |
# enable mouse display | |
active_window = glfwGetCurrentContext() | |
glfwSetInputMode(self._window, GLFW_CURSOR, GLFW_CURSOR_NORMAL) | |
glfwDestroyWindow(self._window) | |
self._window = None | |
glfwMakeContextCurrent(active_window) | |
def recent_events(self, events): | |
frame = events.get('frame') | |
if self.active and frame: | |
recent_pupil_positions = events['pupil_positions'] | |
gray_img = frame.gray | |
if self.clicks_to_close <=0: | |
self.stop() | |
return | |
if self.display_pos is None: | |
self.detected = False | |
else: | |
self.detected = True | |
self.pos = self.display_pos.tolist() # position of marker in screen in norm coords | |
# only save a valid ref position if within sample window of calibraiton routine | |
on_position = self.lead_in < self.screen_marker_state < (self.lead_in+self.sample_duration) | |
if on_position and self.detected: | |
ref = {} | |
ref["norm_pos"] = self.pos | |
ref["screen_pos"] = self.pos | |
ref["timestamp"] = frame.timestamp | |
self.ref_list.append(ref) | |
# always save pupil positions | |
for p_pt in recent_pupil_positions: | |
if p_pt['confidence'] > self.pupil_confidence_threshold: | |
self.pupil_list.append(p_pt) | |
if on_position and self.detected and events.get('fixations', []): | |
self.screen_marker_state = min( | |
self.sample_duration+self.lead_in, | |
self.screen_marker_state+self.fixation_boost) | |
# Animate the screen marker | |
if self.screen_marker_state < self.sample_duration+self.lead_in+self.lead_out: | |
if self.detected or not on_position: | |
self.screen_marker_state += 1 | |
else: | |
self.screen_marker_state = 0 | |
if not self.sites: | |
self.stop() | |
return | |
self.active_site = self.sites.pop(0) | |
logger.debug("Moving screen marker to site at {} {}".format(*self.active_site)) | |
# use np.arrays for per element wise math | |
self.display_pos = np.array(self.active_site) | |
self.on_position = on_position | |
self.button.status_text = '{} / {}'.format(self.active_site, 9) | |
def gl_display(self): | |
""" | |
use gl calls to render | |
at least: | |
the published position of the reference | |
better: | |
show the detected postion even if not published | |
""" | |
# debug mode within world will show green ellipses around detected ellipses | |
if self.active and self.detected: | |
for marker in self.markers: | |
e = marker[-1] # outermost ellipse | |
pts = cv2.ellipse2Poly((int(e[0][0]), int(e[0][1])), | |
(int(e[1][0]/2), int(e[1][1]/2)), | |
int(e[-1]), 0, 360, 15) | |
draw_polyline(pts, 1, RGBA(0.,1.,0.,1.)) | |
else: | |
pass | |
if self._window: | |
self.gl_display_in_window() | |
def gl_display_in_window(self): | |
active_window = glfwGetCurrentContext() | |
if glfwWindowShouldClose(self._window): | |
self.close_window() | |
return | |
glfwMakeContextCurrent(self._window) | |
clear_gl_screen() | |
hdpi_factor = glfwGetFramebufferSize(self._window)[0]/glfwGetWindowSize(self._window)[0] | |
r = 110*self.marker_scale * hdpi_factor | |
gl.glMatrixMode(gl.GL_PROJECTION) | |
gl.glLoadIdentity() | |
p_window_size = glfwGetFramebufferSize(self._window) | |
gl.glOrtho(0, p_window_size[0], p_window_size[1], 0, -1, 1) | |
# Switch back to Model View Matrix | |
gl.glMatrixMode(gl.GL_MODELVIEW) | |
gl.glLoadIdentity() | |
def map_value(value,in_range=(0,1),out_range=(0,1)): | |
ratio = (out_range[1]-out_range[0])/(in_range[1]-in_range[0]) | |
return (value-in_range[0])*ratio+out_range[0] | |
pad = .7*r | |
screen_pos = map_value(self.display_pos[0],out_range=(pad,p_window_size[0]-pad)),map_value(self.display_pos[1],out_range=(p_window_size[1]-pad,pad)) | |
alpha = interp_fn(self.screen_marker_state,0.,1.,float(self.sample_duration+self.lead_in+self.lead_out),float(self.lead_in),float(self.sample_duration+self.lead_in)) | |
draw_concentric_circles(screen_pos,r,4,alpha) | |
#some feedback on the detection state | |
if self.detected and self.on_position: | |
draw_points([screen_pos],size=10*self.marker_scale,color=RGBA(0.,.8,0.,alpha),sharpness=0.5) | |
else: | |
draw_points([screen_pos],size=10*self.marker_scale,color=RGBA(0.8,0.,0.,alpha),sharpness=0.5) | |
if self.clicks_to_close <5: | |
self.glfont.set_size(int(p_window_size[0]/30.)) | |
self.glfont.draw_text(p_window_size[0]/2.,p_window_size[1]/4.,'Touch {} more times to cancel calibration.'.format(self.clicks_to_close)) | |
glfwSwapBuffers(self._window) | |
glfwMakeContextCurrent(active_window) | |
def get_init_dict(self): | |
d = {} | |
d['fullscreen'] = self.fullscreen | |
d['marker_scale'] = self.marker_scale | |
return d | |
def cleanup(self): | |
"""gets called when the plugin get terminated. | |
either voluntarily or forced. | |
""" | |
if self.active: | |
self.stop() | |
if self._window: | |
self.close_window() | |
self.deinit_gui() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment