本文介绍了如何将Eyelink眼动仪与Psychopy连接并输出Eyelink官方.EDF数据文件。在网上找了很久没找到这方面的操作流程,有用iohub(Psychopy内置眼动组件)实现的,但是感觉问题比较多,所以自己研究了一下。
主要参考了Eyelink官方例程(picture.py),对其中的一些代码段进行了修改,虽然可能还存在问题,但在实际实验中确认了可行性。该操作过程不需要从头编写代码,仅用Builder中的Code组件即可实现。
写在这里作为笔记,也供有需要的同学参考。
目录
1 准备工作
2 代码解读
2.1 实验前预设代码
2.1.1 额外引用
2.1.2 函数定义
结束/中止实验函数
中止记录函数
清屏函数
2.1.3 模式预设
2.1.4 本地数据文件夹创建
2.1.5 连接眼动仪
2.1.6 Host数据文件创建
2.1.8 显示设置
2.1.9 校准前设置
2.2 功能模块
2.2.1 开始校准
2.2.2 试次数记录与主机显示
2.2.3 试次前drift-check
2.2.4 开始记录
2.2.5 标记图片刺激呈现时刻
2.2.6 将背景图片录入数据文件
2.2.7 兴趣区
2.2.8 停止记录
2.2.9 试次结束后写入数据文件
3 可行的操作流程
4 可能存在的问题
5 总结
1 准备工作
首先要在实验程序运行机下载Eyelink SDK ,可以去官网下载,需要注册并等待(最长)24小时审核。网上也可以搜到网盘版本。安装好后,在SR Research文件夹下会有例程文件,打开python子文件夹下的examples文件夹,里面有Psychopy例程。
随便打开一个例程文件夹,将里面的 EyeLinkCoreGraphicsPsychoPy.py 文件复制到实验程序目录下。
2 代码解读
首先对实验中需要用到的代码进行功能解读,这些代码段将被置于Psychopy实验程序中的不同位置,具体操作流程见下一部分。
2.1 实验前预设代码
预设部分将在实验开始前对眼动仪进行基本配置,这一部分可以作为整体插入实验。
2.1.1 额外引用
import pylink
import platform
import time
from EyeLinkCoreGraphicsPsychoPy import EyeLinkCoreGraphicsPsychoPy
2.1.2 函数定义
结束/中止实验函数
用于关闭并下载数据文件到被试机。
def terminate_task():
""" Terminate the task gracefully and retrieve the EDF data file
file_to_retrieve: The EDF on the Host that we would like to download
win: the current window used by the experimental script
"""
el_tracker = pylink.getEYELINK()
if el_tracker.isConnected():
# Terminate the current trial first if the task terminated prematurely
error = el_tracker.isRecording()
if error == pylink.TRIAL_OK:
abort_trial()
# Put tracker in Offline mode
el_tracker.setOfflineMode()
# Clear the Host PC screen and wait for 500 ms
el_tracker.sendCommand('clear_screen 0')
pylink.msecDelay(500)
# Close the edf data file on the Host
el_tracker.closeDataFile()
# Download the EDF data file from the Host PC to a local data folder
# parameters: source_file_on_the_host, destination_file_on_local_drive
local_edf = os.path.join(session_folder, session_identifier + '.EDF')
try:
el_tracker.receiveDataFile(edf_file, local_edf)
except RuntimeError as error:
print('ERROR:', error)
# Close the link to the tracker.
el_tracker.close()
中止记录函数
用于异常中止数据记录。
def abort_trial():
"""Ends recording """
el_tracker = pylink.getEYELINK()
# Stop recording
if el_tracker.isRecording():
# add 100 ms to catch final trial events
pylink.pumpDelay(100)
el_tracker.stopRecording()
# clear the screen
clear_screen(win)
# Send a message to clear the Data Viewer screen
bgcolor_RGB = (116, 116, 116)
el_tracker.sendMessage('!V CLEAR %d %d %d' % bgcolor_RGB)
# send a message to mark trial end
el_tracker.sendMessage('TRIAL_RESULT %d' % pylink.TRIAL_ERROR)
return pylink.TRIAL_ERROR
清屏函数
用于清空屏幕。
def clear_screen(win):
""" clear up the PsychoPy window"""
win.fillColor = win.color
win.flip()
2.1.3 模式预设
设置了是否为Retina屏幕以及是否为模拟模式。
# Set this variable to True if you use the built-in retina screen as your
# primary display device on macOS. If have an external monitor, set this
# variable True if you choose to "Optimize for Built-in Retina Display"
# in the Displays preference settings.
use_retina = False #选择是否为Mac Retina屏幕
# Set this variable to True to run the script in "Dummy Mode"
dummy_mode = False #选择是否为模拟模式(无眼动仪情况下)
2.1.4 本地数据文件夹创建
此处将被试号作为数据文件名,在实验程序文件夹中创建了results文件夹,并针对每个数据创建了数据文件名+时间的子文件夹。
#以被试号(str(expInfo['Participant']))作为数据文件名
edf_fname=(str(expInfo['Participant']))
results_folder = 'results'
if not os.path.exists(results_folder):
os.makedirs(results_folder)
time_str = time.strftime("_%Y_%m_%d_%H_%M", time.localtime())
session_identifier = edf_fname + time_str
# create a folder for the current session in the "results" folder
session_folder = os.path.join(results_folder, session_identifier)
if not os.path.exists(session_folder):
os.makedirs(session_folder)
2.1.5 连接眼动仪
连接眼动仪主机,眼动仪主机IP一般为100.1.1.1。
if dummy_mode:
el_tracker = pylink.EyeLink(None)
else:
try:
el_tracker = pylink.EyeLink("100.1.1.1")
except RuntimeError as error:
print('ERROR:', error)
core.quit()
2.1.6 Host数据文件创建
在主机创建并打开数据文件(会覆盖主机上先前的同名文件)。
edf_file = edf_fname + ".EDF" #数据文件名称
try:
el_tracker.openDataFile(edf_file)
except RuntimeError as err:
print('ERROR:', err)
# close the link if we have one open
if el_tracker.isConnected():
el_tracker.close()
core.quit()
#实验程序名记录,建议路径及文件名中不要用中文
preamble_text = 'RECORDED BY %s' % os.path.basename(__file__)
el_tracker.sendCommand("add_file_preamble_text '%s'" % preamble_text)
2.1.7 配置眼动仪
# Put the tracker in offline mode before we change tracking parameters
el_tracker.setOfflineMode()
# Get the software version: 1-EyeLink I, 2-EyeLink II, 3/4-EyeLink 1000,
# 5-EyeLink 1000 Plus, 6-Portable DUO
eyelink_ver = 0 # set version to 0, in case running in Dummy mode
if not dummy_mode:
vstr = el_tracker.getTrackerVersionString()
eyelink_ver = int(vstr.split()[-1].split('.')[0])
# print out some version info in the shell
print('Running experiment on %s, version %d' % (vstr, eyelink_ver))
# File and Link data control
# what eye events to save in the EDF file, include everything by default
file_event_flags = 'LEFT,RIGHT,FIXATION,SACCADE,BLINK,MESSAGE,BUTTON,INPUT'
# what eye events to make available over the link, include everything by default
link_event_flags = 'LEFT,RIGHT,FIXATION,SACCADE,BLINK,BUTTON,FIXUPDATE,INPUT'
# what sample data to save in the EDF data file and to make available
# over the link, include the 'HTARGET' flag to save head target sticker
# data for supported eye trackers
if eyelink_ver > 3:
file_sample_flags = 'LEFT,RIGHT,GAZE,HREF,RAW,AREA,HTARGET,GAZERES,BUTTON,STATUS,INPUT'
link_sample_flags = 'LEFT,RIGHT,GAZE,GAZERES,AREA,HTARGET,STATUS,INPUT'
else:
file_sample_flags = 'LEFT,RIGHT,GAZE,HREF,RAW,AREA,GAZERES,BUTTON,STATUS,INPUT'
link_sample_flags = 'LEFT,RIGHT,GAZE,GAZERES,AREA,STATUS,INPUT'
el_tracker.sendCommand("file_event_filter = %s" % file_event_flags)
el_tracker.sendCommand("file_sample_data = %s" % file_sample_flags)
el_tracker.sendCommand("link_event_filter = %s" % link_event_flags)
el_tracker.sendCommand("link_sample_data = %s" % link_sample_flags)
# Optional tracking parameters
# Sample rate, 250, 500, 1000, or 2000, check your tracker specification
# if eyelink_ver > 2:
# el_tracker.sendCommand("sample_rate 1000")
# Choose a calibration type, H3, HV3, HV5, HV13 (HV = horizontal/vertical),
el_tracker.sendCommand("calibration_type = HV9")
# Set a gamepad button to accept calibration/drift check target
# You need a supported gamepad/button box that is connected to the Host PC
#el_tracker.sendCommand("button_function 5 'accept_target_fixation'")
2.1.8 显示设置
根据实际情况在psychopy显示器中心提前设置显示器参数(width, distance)
# get the native screen resolution used by PsychoPy
scn_width, scn_height = win.size #置于Win创建后
# resolution fix for Mac retina displays
if 'Darwin' in platform.system():
if use_retina:
scn_width = int(scn_width/2.0)
scn_height = int(scn_height/2.0)
# Pass the display pixel coordinates (left, top, right, bottom) to the tracker
# see the EyeLink Installation Guide, "Customizing Screen Settings"
el_coords = "screen_pixel_coords = 0 0 %d %d" % (scn_width - 1, scn_height - 1)
el_tracker.sendCommand(el_coords)
# Write a DISPLAY_COORDS message to the EDF file
# Data Viewer needs this piece of info for proper visualization, see Data
# Viewer User Manual, "Protocol for EyeLink Data to Viewer Integration"
dv_coords = "DISPLAY_COORDS 0 0 %d %d" % (scn_width - 1, scn_height - 1)
el_tracker.sendMessage(dv_coords)
2.1.9 校准前设置
设置校准界面的背景色、前景色、校准目标类型、大小以及校准时的声音(可复制例程中的音频文件),并在psychopy中打开该图形界面。
# Configure a graphics environment (genv) for tracker calibration
genv = EyeLinkCoreGraphicsPsychoPy(el_tracker, win)
print(genv) # print out the version number of the CoreGraphics library
# Set background and foreground colors for the calibration target
# in PsychoPy, (-1, -1, -1)=black, (1, 1, 1)=white, (0, 0, 0)=mid-gray
foreground_color = (-1, -1, -1) #校准时的文字颜色
background_color = win.color #校准时的背景色,此处与实验一致
genv.setCalibrationColors(foreground_color, background_color)
# Set up the calibration target
#
# The target could be a "circle" (default), a "picture", a "movie" clip,
# or a rotating "spiral". To configure the type of calibration target, set
# genv.setTargetType to "circle", "picture", "movie", or "spiral", e.g.,
# genv.setTargetType('picture')
#
# Use gen.setPictureTarget() to set a "picture" target
# genv.setPictureTarget(os.path.join('images', 'fixTarget.bmp'))
#
# Use genv.setMovieTarget() to set a "movie" target
# genv.setMovieTarget(os.path.join('videos', 'calibVid.mov'))
# Use a circle as the calibration target
genv.setTargetType('circle') #设置校准目标类型,此处为圆圈
# Configure the size of the calibration target (in pixels)
genv.setTargetSize(24) #设置目标大小
genv.setCalibrationSounds('', '', '') #设置校准过程中声音(可省略)
# resolution fix for macOS retina display issues
if use_retina:
genv.fixMacRetinaDisplay()
# Request Pylink to use the PsychoPy window we opened above for calibration
pylink.openGraphicsEx(genv)
2.2 功能模块
2.2.1 开始校准
启动设置界面进行校准(空屏后按回车开始)。
if not dummy_mode:
try:
clear_screen(win)
el_tracker.doTrackerSetup()
except RuntimeError as err:
print('ERROR:', err)
el_tracker.exitCalibration()
2.2.2 试次数记录与主机显示
将试次计数和刺激图片路径显示在主机界面下方,此处需要提前定义变量:trial_index(试次计数)及 picdir(刺激图片路径)。
el_tracker = pylink.getEYELINK()
# put the tracker in the offline mode first
el_tracker.setOfflineMode()
# clear the host screen before we draw the backdrop
# el_tracker.sendCommand('clear_screen 0')
el_tracker.sendMessage('TRIALID %d' % trial_index) #
el_tracker.sendMessage('PIC %s' % picdir)
# Component updates done
# record_status_message : show some info on the Host PC
# here we show how many trial has been tested
status_msg = 'TRIAL number %d-PIC:%s' % (trial_index,picdir)
el_tracker.sendCommand("record_status_message '%s'" % status_msg)
# Skip drift-check if running the script in Dummy Mode
2.2.3 试次前drift-check
(慎用)在每个试次前进行drift-check,实际实验中按ESC可以从该环节进入设置界面进行重新校准,但校准完成后很大概率实验程序会报错闪退,可能是和实验程序本身的ESC有冲突。
# Skip drift-check if running the script in Dummy Mode
while not dummy_mode:
# terminate the task if no longer connected to the tracker or
# user pressed Ctrl-C to terminate the task
if (not el_tracker.isConnected()) or el_tracker.breakPressed():
terminate_task()
# drift-check and re-do camera setup if ESCAPE is pressed
try:
error = el_tracker.doDriftCorrect(int(scn_width/2.0),
int(scn_height/2.0), 1, 1)
# break following a success drift-check
if error is not pylink.ESC_KEY:
break
except:
pass
clear_screen(win)
2.2.4 开始记录
运行该代码,眼动仪开始记录。
if not dummy_mode:
try:
el_tracker.startRecording(1, 1, 1, 1)
except RuntimeError as error:
print("ERROR:", error)
abort_trial()
2.2.5 标记图片刺激呈现时刻
最好在刺激呈现后立即运行,中间不要隔太多行,但在Psychopy中有点难实现。
el_tracker.sendMessage('image_onset')
img_onset_time = core.getTime() # record the image onset time
2.2.6 将背景图片录入数据文件
将背景图片的相对路径(bg_imag)写入数据文件,在数据查看器中会作为背景显示,文件路径里千万不要有空格,最好也不要有中文。
因为是相对数据文件的位置,所以要加 '../../'。 S[0] 和 S[1] 为图片材料像素大小,可根据具体情况进行修改。
clear_screen(win)
bgcolor_RGB = (116, 116, 116)
el_tracker.sendMessage('!V CLEAR %d %d %d' % bgcolor_RGB)
bg_image = '../../'+picdir
imgload_msg = '!V IMGLOAD CENTER %s %d %d %d %d' % (bg_image,
int(scn_width/2.0),
int(scn_height/2.0),
int(S[0]),
int(S[1]))
el_tracker.sendMessage(imgload_msg)
2.2.7 兴趣区
创建一个矩形兴趣区(可省略)。
ia_pars = (1, left, top, right, bottom, 'screen_center')
el_tracker.sendMessage('!V IAREA RECTANGLE %d %d %d %d %d %s' % ia_pars)
2.2.8 停止记录
停止数据记录。
el_tracker.stopRecording()
2.2.9 试次结束后写入数据文件
在试次结束后将试次信息写入数据文件,此处写入了两个条件、图片材料路径、按键反应类型及反应时。除了最后一句代码,其它部分可按实际情况进行修改。
# record trial variables to the EDF data file, for details, see Data
# Viewer User Manual, "Protocol for EyeLink Data to Viewer Integration"
el_tracker.sendMessage('!V TRIAL_VAR condition1 %s' % con1)
el_tracker.sendMessage('!V TRIAL_VAR condition2 %s' % con2)
el_tracker.sendMessage('!V TRIAL_VAR image %s' % picdir)
el_tracker.sendMessage('!V TRIAL_VAR RT %f' % key_resp_10.rt)
el_tracker.sendMessage('!V TRIAL_VAR Key %s' % key_resp_10.keys)
# send a 'TRIAL_RESULT' message to mark the end of trial, see Data
# Viewer User Manual, "Protocol for EyeLink Data to Viewer Integration"
el_tracker.sendMessage('TRIAL_RESULT %d' % pylink.TRIAL_OK)
3 可行的操作流程
基础程序编写:将包含以上代码的Code组件置于Psychopy的不同部分即可实现连通,所以第一步是编好一个不需要眼动仪的完整程序。
插入实验前预设代码:在整个实验程序最前端新建一个空白Routine,添加Code组件,将所有实验前预设代码按顺序写入(Begin Routine中,如无特殊说明,下同)。
开始校准:一般每次休息后都应该进行校准,因此可以在每次休息后的部分新建空白Routine,添加Code组件,写入校准代码段。
试次中流程:其余代码应该在每个试次循环中均运行一次。
首先可以在每个试次循环的最开始创建空白Routine,插入试次数(提前定义并在每个试次中更新)记录与主机显示代码,发送要显示在屏幕上的试次信息,可以在其后添加drift-check代码(注:drift-check过程中最好不要按ESC进入设置界面,很可能会闪退,程序较短这一步可以不加,或者用注视点检测代替);
随后,在刺激呈现后立即开始数据记录、标记图片刺激呈现时刻并将背景图片录入数据文件。在Psychopy中,图片刺激好像是在进入Routine的第二帧才刷新,所以很难保持完全同步,不过要求不高的话应该也可以忽略不计了。这三部分代码可以放在呈现刺激Routine里的Code组件中。此处也可以设置兴趣区,不过一般会在数据处理阶段设置。
然后在刺激消失时(或者是需要结束记录的地方)添加结束记录代码,如果刺激呈现单独占用一个Routine,可以将结束记录代码写入这一部分的End Routine中。
最后,在试次结束后(记录被试反应后)将试次条件、反应时等信息录入数据文件,可将这部分代码放在试次最后一个Routine的Code组件(End Routine)中。
结束实验并下载数据:在实验的最后加入Code组件,运行 terminate_task() 函数。
4 可能存在的问题
计时问题:没办法保证时间的精确记录,不过要求不高的话这个问题可以忽略;
按ESC退出后数据文件无法保存,记录无法中止:在Psychopy中可以设置按ESC退出实验程序,但是无法保存眼动数据也无法结束记录,如果在试次记录开始后按ESC,主机上依然会继续记录。一个可行的解决办法是将程序导出为python代码,然后在代码中搜索Esc,在每一个ESC按键事件后添加terminate_task()及abort_trial()两个函数;
代码的简洁性:可用,但是可能有一些代码是多余的;
Drift-check阶段进入设置界面会闪退:可能存在按键冲突。
5 总结
还是有一点点复杂的,如果没有特殊需求的话还是用Eyelink自带的实验编程软件吧,编写好一定要完整试运行一下。
用pylink包还可以将图片刺激传输到主机作为背景,但是好像会比较耗时,如果是视频刺激的话应该也可以解决。
其他功能可以参考官方例程及Pylink api userguide。