Skip to content

Instantly share code, notes, and snippets.

@cosacog
Last active March 5, 2018 03:47
Show Gist options
  • Save cosacog/3293f721bc7832b8f6b66599ed167d10 to your computer and use it in GitHub Desktop.
Save cosacog/3293f721bc7832b8f6b66599ed167d10 to your computer and use it in GitHub Desktop.
line bisection task using psychopy. refer to Fierro et al. (2000) PMID: 10841369、ダウンロードする時は右上のダウンロードボタンからお願いします。
#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
This experiment was created using PsychoPy2 Experiment Builder (v1.85.2),
on 2017_11_15_2037
If you publish work using this script please cite the PsychoPy publications:
Peirce, JW (2007) PsychoPy - Psychophysics software in Python.
Journal of Neuroscience Methods, 162(1-2), 8-13.
Peirce, JW (2009) Generating stimuli for neuroscience using PsychoPy.
Frontiers in Neuroinformatics, 2:10. doi: 10.3389/neuro.11.010.2008
"""
from __future__ import absolute_import, division
from psychopy import locale_setup, sound, gui, visual, core, data, event, logging
from psychopy.constants import (NOT_STARTED, STARTED, PLAYING, PAUSED,
STOPPED, FINISHED, PRESSED, RELEASED, FOREVER)
import numpy as np # whole numpy lib is available, prepend 'np.'
from numpy import (sin, cos, tan, log, log10, pi, average,
sqrt, std, deg2rad, rad2deg, linspace, asarray)
from numpy.random import random, randint, normal, shuffle
import os # handy system and path functions
import sys # to get file system encoding
from datetime import datetime
#-------------- settings -------------------------
# line info: see also "stim info"
pixelN_of_shortestLine = 200 # pixel num of shortest line (in this case 70 mm)
pixelN_of_lineWidth = 5 # pixel num of line width
# window info
pixelN_of_window_width = 1024 # pixel num of window resolution (horizontal)
pixelN_of_window_height = 768 # pixel num of window resolution (vertical)
show_full_screen = False # True or False
screenN = 2 # screen number, starts from 1 (main screen), 2(sub screen)....
# time info
maxSec_for_responseWait = 5.0 # seconds to wait response
ITI_sec = 2.0 # seconds from 1 trial end to next stim
interval_sec_from_stim_to_response = 1.0 # second of blank after showing stim
durationSec_to_show_selected_response = 0.5 # seconds to show selected response button
durationSec_to_show_arrow = 1.0 # seconds to show arrow
frameN_to_show_line = 3 # frame number to show line stim (16.7ms/frame@ 60 Hz monitor)
blankSec_from_arrow_to_lineStim = 0.05 # sec of blank from arrow disappear to show line stim
# stim info: list of stim patterns
scores_original = {'rt_is_longer':1, 'lt_is_longer':-1, 'same_length':0}
answer_which_of_lt_rt_line_is_longer = True # False-answer the position of vertical line
scores = {}
scores['rt'] = scores_original['rt_is_longer']
scores['lt'] = scores_original['lt_is_longer']
scores['mid'] = scores_original['same_length']
msgTask = u"線分のどちらが長いか\n答えてください"
if not answer_which_of_lt_rt_line_is_longer:
scores['rt'] = scores_original['rt_is_longer']*-1
scores['lt'] = scores_original['lt_is_longer']*-1
scores['mid'] = scores_original['same_length']*-1
msgTask = u"真ん中の縦線が左右どちらに寄っているか\n答えてください"
# stim patterns
list_dictStimConditions = [
{'name':'line1', 'rt':75, 'lt':75, 'trialN':2},
{'name':'line2_lt_elongated', 'rt':70, 'lt':75, 'trialN':1},
{'name':'line3_lt_elongated', 'rt':75, 'lt':80, 'trialN':1},
{'name':'line4_rt_elongated', 'rt':75, 'lt':70, 'trialN':1},
{'name':'line5_rt_elongated', 'rt':80, 'lt':75, 'trialN':1},
]
# stim info-continued
len_vertical_line = 10 # mm of length of vertical line
color_vertical_line = [1,-1,-1] # color of vertical line. default:red
# button info
# lt/rt button
posLtBtn = (100, -250) # lt button position (pixel). default (-300, -200)
posRtBtn = (400, -250) # rt button position (pixel). default (300, -200)
sizeBtnLtRt = (150, 100) # lt/rt button size (pixel). default (300, 200)
# middle button
posMidBtn = (250, -250) # mid button position (pixel). default (0, -200)
sizeMidBtn = (100, 100) # mid button size (pixel). default (200, 200)
#-------------- functions ---------------------
def getBtnBorder(posBtn, sizeBtn):
'''
get border of lt, rt, up, down of button
'''
btnBorder = {
'lt': posBtn[0]-0.5*sizeBtn[0],
'rt': posBtn[0]+0.5*sizeBtn[0],
'up': posBtn[1]+0.5*sizeBtn[1],
'down':posBtn[1]-0.5*sizeBtn[1]
}
return btnBorder
def isInsideBtn(x,y,border):
'''
judge mouse pointer is inside buttton area or not
'''
isInside = \
(border['lt'] < x) and \
(x < border['rt']) and \
(border['down'] < y) and \
(y < border['up'])
return isInside
def getStimSide(lt, rt, answer_which_of_lt_rt_line_is_longer):
'''
get stim side (lt, rt, mid)
params:
answer_which_of_lt_rt_line_is_longer:bool
return:
'lt','rt' or 'mid'
'''
import numpy as np
strOut = ""
if answer_which_of_lt_rt_line_is_longer:
if lt > rt:
strOut = "lt"
elif lt < rt:
strOut = "rt"
else:
strOut = "mid"
else:
if lt > rt:
strOut = "rt"
elif lt < rt:
strOut = "lt"
else:
strOut = "mid"
return strOut
def lineupStim(list_dictStimConditions, minPixelLtOrRt, randomise=True):
'''
line up each stim according to stim conditions
'''
import numpy as np
# get minimum length => minLen
minLen = _getMinLen(list_dictStimConditions)
baseMinLen = 1.0/float(minLen)
# lineup each stimuli: convert length -> pixels
listStim = list()
for idx, item in enumerate(list_dictStimConditions):
trialNum = item['trialN']
pixLt = np.round(item['lt']*baseMinLen * minPixelLtOrRt)
pixRt = np.round(item['rt']*baseMinLen * minPixelLtOrRt)
# dictStim = {'name':item['name'],'lt_pix':pixLt, 'rt_pix':pixRt, 'score':item['score']}
dictStim = {'name':item['name'],'lt_pix':pixLt, 'rt_pix':pixRt}
for i in np.arange(trialNum):
listStim.append(dictStim)
if randomise:
listStim = np.random.permutation(listStim)
return listStim
def calcPixelNforLineLength(lenLine, list_dictStimConditions, minPixelLtOrRt):
'''
calculate pixel num for vertical line
'''
import numpy as np
minLen = _getMinLen(list_dictStimConditions)
return np.round(lenLine/float(minLen)*minPixelLtOrRt)
def _getMinLen(list_dictStimConditions):
'''
get minimum length in conditions
'''
for idx, item in enumerate(list_dictStimConditions):
minLenInItem = min([item['rt'], item['lt']])
if idx==0:
minLen = minLenInItem
else:
minLen = min([minLen, minLenInItem])
return minLen
# ---------- initialise stimuli ---------------------
# Setup the Window
win = visual.Window(
size=(pixelN_of_window_width, pixelN_of_window_height),
fullscr=show_full_screen,
screen=screenN,
allowGUI=False, allowStencil=False, units='pix',
monitor='testMonitor', color=[0,0,0], colorSpace='rgb',
blendMode='avg', useFBO=True)
frameRate = win.getActualFrameRate
# Initialize components for Routine "trial"
# cross line
pixelN_of_vertical_line = calcPixelNforLineLength(len_vertical_line,
list_dictStimConditions, pixelN_of_shortestLine) # pixelN of vertical line
colorLine = [-1,-1,-1] # black
horz_line = visual.Line(
win=win, name='horz_line',units='pix',
start=(-200, 0), end=(200, 0),
ori=0, pos=(0, 0),
lineWidth=pixelN_of_lineWidth, lineColor=colorLine, lineColorSpace='rgb',
opacity=1, depth=0.0, interpolate=True)
vert_line = visual.Line(
win=win, name='vert_line',
start=(0, -pixelN_of_vertical_line),
end=(0, pixelN_of_vertical_line),
ori=0, pos=(0, 0), units='pix',
lineWidth=pixelN_of_lineWidth, lineColor=color_vertical_line, lineColorSpace='rgb',
opacity=1, depth=-1.0, interpolate=True)
# arrow: refer to goo.gl/SJAoog
widthArrow = 0.02
arrowVert = [(-0.4,widthArrow),(-0.4,-widthArrow),(-.2,-widthArrow),(-.2,-0.1),(0,0),(-.2,0.1),(-.2,widthArrow)]
arrowVert = [(n*1000,m*1000) for (n,m) in arrowVert]
arrow = visual.ShapeStim(win, vertices=arrowVert, fillColor='black', size=.5, lineColor='black')
arrow.ori = 90.0
# button:Lt
imageLtBtn = visual.ImageStim(
win=win, name='imageRest',units='pix',
image=u'btn_lt.png', mask=None,
ori=0, pos=posLtBtn, size=sizeBtnLtRt,
color=[1,1,1], colorSpace='rgb', opacity=1,
flipHoriz=False, flipVert=False,
texRes=128, interpolate=True, depth=0.0)
btnBorderLt = getBtnBorder(posLtBtn, sizeBtnLtRt)
# button:Rt
imageRtBtn = visual.ImageStim(
win=win, name='imageRest',units='pix',
image=u'btn_rt.png', mask=None,
ori=0, pos=posRtBtn, size=sizeBtnLtRt,
color=[1,1,1], colorSpace='rgb', opacity=1,
flipHoriz=False, flipVert=False,
texRes=128, interpolate=True, depth=0.0)
btnBorderRt = getBtnBorder(posRtBtn, sizeBtnLtRt)
# button: mid
imageMidBtn = visual.ImageStim(
win=win, name='imageMid',units='pix',
image=u'btn_mid.png', mask=None,
ori=0, pos=posMidBtn, size=sizeMidBtn,
color=[1,1,1], colorSpace='rgb', opacity=1,
flipHoriz=False, flipVert=False,
texRes=128, interpolate=True, depth=0.0)
btnBorderMid = getBtnBorder(posMidBtn, sizeMidBtn)
# text
textMsg = visual.TextStim(win=win, name='text',
text=u'temporary text',
font=u'Meiryo', units='pix',
pos=(0, 0), height=50, wrapWidth=None, ori=0,
color=u'black', colorSpace='rgb', opacity=1,
depth=0.0)
def showMessage(msg, t_dur):
'''
show message
'''
textMsg.text = msg
textMsg.setAutoDraw(True)
win.flip()
core.wait(t_dur)
textMsg.setAutoDraw(False)
win.flip()
# mouse
mouse = event.Mouse(win=win)
x, y = [None, None]
# ------ line up stimulus conditions -------
list_dictStim = lineupStim(list_dictStimConditions, pixelN_of_shortestLine, randomise=True)
# ------Prepare to start Routine "trial"-------
trialClock = core.Clock()
# create file to save results
fnameSave = "result{0}.csv".format(datetime.now().strftime("%y%m%d%H%M"))
with open(fnameSave, "w") as f:
f.write("no,stim side,lt length,rt length,response side,score\n")
# -------Start Routine "trial"-------
frameRate = win.getActualFrameRate()
msgInit = u"テストを開始します\n画面の中心を注視してください\n\nrefresh rate:\n{0:0.1f} Hz".format(frameRate)
showMessage(msgTask, 3.0)
showMessage(msgInit, 2.0)
# ----settings ----------
trialN = len(list_dictStim)
# --------------------
# for idxTrial in np.arange(trialN):
for idxTrial, dictStim in enumerate(list_dictStim):
print(dictStim['name'])
print(dictStim['lt_pix'])
print(dictStim['rt_pix'])
# ------- start Routine "arrow" --------------
continueRoutine = True
arrow.setAutoDraw(True)
win.flip()
t=0
trialClock.reset()
while continueRoutine:
t=trialClock.getTime()
if t > durationSec_to_show_arrow:
continueRoutine = False
arrow.setAutoDraw(False)
win.flip()
core.wait(blankSec_from_arrow_to_lineStim) # sec
# ------- start Routine "flash line"------------
frameN = -1
continueRoutine = True
horz_line.status = NOT_STARTED
horz_line.start = (-1*dictStim['lt_pix'], 0)
horz_line.end = (dictStim['rt_pix'], 0)
# stimSide = getLongerSide(horz_line.start, horz_line.end)
stimSide = getStimSide(dictStim['lt_pix'], dictStim['rt_pix'],
answer_which_of_lt_rt_line_is_longer)
while continueRoutine:
# get current time
frameN = frameN + 1
# horz_line, vert_line updates
if frameN >= 0 and horz_line.status == NOT_STARTED:
# keep track of start time/frame for later
horz_line.frameNStart = frameN # exact frame index
horz_line.setAutoDraw(True)
vert_line.setAutoDraw(True)
horz_line.status = STARTED
if horz_line.status == STARTED and frameN >= frameN_to_show_line:
horz_line.setAutoDraw(False)
vert_line.setAutoDraw(False)
continueRoutine = False
# refresh the screen
win.flip()
# ------ Start Routine "wait button response"
core.wait(interval_sec_from_stim_to_response)
# initial settings
trialClock.reset()
continueRoutine = True
t = 0
event.mouseButtons = [0, 0, 0] # mouse button status
wasLtMouseBtnDown = False
# show images
imageLtBtn.setAutoDraw(True)
imageRtBtn.setAutoDraw(True)
imageMidBtn.setAutoDraw(True)
responseSide = "" # "lt", "rt" or "mid"
while continueRoutine:
t = trialClock.getTime()
x, y = mouse.getPos()
# judge pointer position
isInsideLtBtn = isInsideBtn(x, y, btnBorderLt)
isInsideRtBtn = isInsideBtn(x, y, btnBorderRt)
isInsideMidBtn = isInsideBtn(x, y, btnBorderMid)
# judge mouse button status: pressed, released
isLtMouseBtnPressed,_,_ = mouse.getPressed()
isLtMouseBtnReleased = (wasLtMouseBtnDown) & (not isLtMouseBtnPressed)
wasLtMouseBtnDown = isLtMouseBtnPressed
# determine lt button image according to button status
if isInsideLtBtn and not isLtMouseBtnPressed:
imageLtBtn.image = 'btn_lt_mouseover.png'
elif isInsideLtBtn and isLtMouseBtnPressed:
# push
imageLtBtn.image= 'btn_lt_push.png'
responseSide = "lt"
# erase other buttons
imageRtBtn.setAutoDraw(False)
imageMidBtn.setAutoDraw(False)
win.flip()
core.wait(durationSec_to_show_selected_response)
continueRoutine = False
else:
imageLtBtn.image= 'btn_lt.png'
# determine rt button image according to button status
if isInsideRtBtn and not isLtMouseBtnPressed:
imageRtBtn.image = 'btn_rt_mouseover.png'
elif isInsideRtBtn and isLtMouseBtnPressed:
# push
imageRtBtn.image= 'btn_rt_push.png'
responseSide = "rt"
# erase other buttons
imageLtBtn.setAutoDraw(False)
imageMidBtn.setAutoDraw(False)
win.flip()
core.wait(durationSec_to_show_selected_response)
continueRoutine = False
else:
imageRtBtn.image= 'btn_rt.png'
# determine mid button image according to button status
if isInsideMidBtn and not isLtMouseBtnPressed:
imageMidBtn.image = 'btn_mid_mouseover.png'
elif isInsideMidBtn and isLtMouseBtnPressed:
# push
imageMidBtn.image= 'btn_mid_push.png'
responseSide = "mid"
# erase other buttons
imageLtBtn.setAutoDraw(False)
imageRtBtn.setAutoDraw(False)
win.flip()
core.wait(durationSec_to_show_selected_response)
continueRoutine = False
else:
imageMidBtn.image= 'btn_mid.png'
if t >= maxSec_for_responseWait:
continueRoutine = False
# check for quit (the Esc key)
if event.getKeys(keyList=["escape"]):
core.quit()
# refresh the screen
win.flip()
# delete images
imageLtBtn.setAutoDraw(False)
imageRtBtn.setAutoDraw(False)
imageMidBtn.setAutoDraw(False)
win.flip()
# calculate score
try:
responseScore = scores[responseSide]
stimScore = scores[stimSide]
# score = responseScore - dictStim['score']
score = responseScore - stimScore
except KeyError:
score = ''
# save stim info and response
with open(fnameSave, "a") as f:
strResults = ",".join(map(str,[
idxTrial+1, # trial no.
stimSide, # stim side: lt, rt or mid
np.abs(horz_line.start[0]), # left segment length
horz_line.end[0], # right segment length
responseSide,
score # response side: lt, rt or mid
]))+"\n"
f.write(strResults)
# Inter trial interval
if idxTrial == trialN-1:
# shoter interval for last trial
ITI_sec = 0.5
core.wait(ITI_sec)
# ----------- Start Routine "Finish" ----------------
showMessage(u"テストは終了です\nお疲れさまでした", 2.0)
#thisExp.abort() # or data files will save again on exit
win.close()
core.quit()
@cosacog
Copy link
Author

cosacog commented Nov 26, 2017

Psychopyで線分二等分課題をするスクリプト

Fierro et al. (2000) PMID: 10841369を参照しました

使い方

  • すべてのファイルを同じディレクトリにダウンロードして、line_bisection.pyをpsychopy上から走らせるとウィンドウが出てきます。
  • 結果は同じディレクトリにcsvを出力します。

タスクの内容

  • 基本は線分の左右のどちらが長いか答えてもらいます.
  • L47 answer_which_of_lt_rt_line_is_longer をTrueからFalseにすると、縦線が左右どちらに偏っているか答えるように変更します.
  • 後者の答え方は縦線が右=左の線分が長い、という解釈になるので、それに沿って点数を出すよう処理を調整してます

結果一覧用CSVの表示について

  • csvの表示は基本のタスクの場合は 線分が右が長い=> rt となります。一方
  • 後者のタスク(縦線が左右どちらに偏るか)では 縦線が右に偏る=> rt と表示します。
  • すなわち、いずれも応答は(刺激も)タスクに合わせて表示するようにしてます.
  • 繰り返しになりますが、L47 のanswer_which_of_lt_rt_line_is_longerのパラメータで変わります。

設定 25-81行目まで

線の長さ関係(L27-28)

  • pixelN_of_shortestLine = 200 # 最短の線分(デフォルトでは70 mm)のピクセル数 (in this case 70 mm)
  • pixelN_of_lineWidth = 5 # 線の幅のピクセル数

ウィンドウ関係 (L31-34)

  • pixelN_of_window_width = 1024 # ウィンドウの横幅ピクセル数
  • pixelN_of_window_height = 768 # 縦幅ピクセル数
  • show_full_screen = False # フルスクリーン表示 (枠が消える)かどうか:True/Falseで設定
  • screenN = 2 # スクリーンの番号(メインが1で、サブが2で、みたいな感じ)

時間関係 (L37-43)

  • maxSec_for_responseWait = 5.0 # 反応を待つ最大の時間(秒)
  • ITI_sec = 2.0 # 反応ボタンが消えて次の刺激が出るまでのISI (秒)
  • interval_sec_from_stim_to_response = 1.0 # 線分の刺激が消えてボタンが出るまでの時間(秒)
  • durationSec_to_show_selected_response = 0.5 # ボタンを押した時一瞬押したボタンだけ表示する時間(秒)
  • durationSec_to_show_arrow = 1.0 # 線分の前に矢印を提示する時間(秒)
  • frameN_to_show_line = 3 # 線分を提示する時間(フレーム数: 通常60 Hzのモニタだと1フレーム 1000/60 = 16.7msを基に計算してください)
  • blankSec_from_arrow_to_lineStim = 0.05 # 矢印が消えて線分が表示されるまでの一瞬のブランクの時間(秒)

刺激関係 (L46-47)

  • scores_original = {'rt_is_longer':1, 'lt_is_longer':-1, 'same_length':0} # 右が長いと答えたら1点、とかそんな感じ
  • answer_which_of_lt_rt_line_is_longer = True # 左を読んでください. 線分が左右に寄っているか答えさせる時はFalseにする

刺激パターン (L61-67): なんとなく察してください.

list_dictStimConditions = [
    {'name':'line1',              'rt':75, 'lt':75, 'trialN':2},
    {'name':'line2_lt_elongated', 'rt':70, 'lt':75, 'trialN':1},
    {'name':'line3_lt_elongated', 'rt':75, 'lt':80, 'trialN':1},
    {'name':'line4_rt_elongated', 'rt':75, 'lt':70, 'trialN':1},
    {'name':'line5_rt_elongated', 'rt':80, 'lt':75, 'trialN':1},
]

項目の説明: 普通はtrialNを変更するだけと思います

  • 'name' 名前
  • 'rt' 右の線分の長さ (mm)
  • 'lt' 左の線分の長さ (mm)
  • 'trialN' 試行回数

刺激の縦線の関係 (L70-71)

  • len_vertical_line = 10 # 縦線の長さ (mm)
  • color_vertical_line = [1,-1,-1] # 縦線の色 (RGBそれぞれ-1から1までで設定する。この例では赤になる。)

ボタン関係 (L75-80)

位置はモニタの中心が(0, 0)で、そこを基準にした座標になる

左右ボタン

  • posLtBtn = (100, -250) # 左ボタンの位置. 中心の座標 (pixel). 以前は (-300, -200) にしていた
  • posRtBtn = (400, -250) # 同じく右の位置 (pixel). 以前は (300, -200) にしていた
  • sizeBtnLtRt = (150, 100) # 左右ボタンのサイズ (pixel). 以前は (300, 200) にしていた

中ボタン

  • posMidBtn = (250, -250) # 中央ボタンの位置 (pixel). 以前は (0, -200)
  • sizeMidBtn = (100, 100) # 中央ボタンのサイズ (pixel). 以前は (200, 200)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment