Search code examples
iostimeframespsychopy

PsychoPy Coder: define image duration based on frames


I have some experience in Matlab, but am very new to PsychoPy.

For now I would like to continuously switch between two images until there is a keyboard response. Each image should stay on the screen exactly for 100ms, and I want to be able to verify that this is the case (e.g. in the log file).

I sort of got it right by using core.wait(.084) after win.flip() - on a 60Hz screen that gives approximately 100ms. I am verifying it by writing the frame of each flip to the log file with win.logOnFlip()

But I believe I could be way more precise I only knew how to define the duration of the image in terms of frames.

The function core.wait() only takes time in seconds, and not in frames, correct?

I would be very grateful if you could give me some tips on how to achieve (and verify) presentation of each image for 6 frames.

Thanks a ton in advance

Best

Sebastian

Here my code:

import os                           # for file/folder operations
from psychopy import visual, event, core, gui, data, logging

# Ensure that relative paths start from the same directory as this script
_thisDir = os.path.dirname(os.path.abspath(__file__))
os.chdir(_thisDir)

# screen size in pixels
scrsize = (600,400)                

# gather info participant
exp_name = 'MyFirstPsychoPy'
exp_info = {
        'participant': '',  
        }
dlg = gui.DlgFromDict(dictionary=exp_info, title=exp_name)

# if user pressed cancel quit
if dlg.OK == False:
    core.quit()  

# Get date and time
exp_info['date'] = data.getDateStr()
exp_info['exp_name'] = exp_name

#save a log file for detail verbose info
filename = _thisDir + os.sep + 'data/%s_%s_%s' %(exp_info['participant'], exp_name, exp_info['date'])
# print filename   #to see if path correct
logFile = logging.LogFile(filename+'.log', level=logging.DEBUG)
logging.console.setLevel(logging.WARNING)  #  outputs to the screen, not a file


# Create a window small window
win = visual.Window(size=scrsize, color='white', units='pix', fullscr=False)

# or go full screen
#win = visual.Window([1280,1024], fullscr=True, allowGUI=False, waitBlanking=True)

# this is supposed to record all frames
win.setRecordFrameIntervals(True)   

# show instructions until spacebar
start_message = visual.TextStim(win,
                            text="hello. you will see mondrians. press space to respond.",
                            color='red', height=20)
event.clearEvents()
keys = event.getKeys(keyList=['space', 'escape'])  #allow only space and escape keys
while len(keys) == 0:
    start_message.draw()
    win.flip()

    keys = event.getKeys(keyList=['space', 'escape'])
    if len(keys)>0:
        break

print keys  #show on output screen
keys = event.clearEvents()  # empty keys
keys = event.getKeys(keyList=['space', 'escape'])


# define 2 pictures
bitmap1 = visual.ImageStim(win, 'Mondrians/Mask_1.bmp', size=scrsize)
bitmap2 = visual.ImageStim(win, 'Mondrians/Mask_2.bmp', size=scrsize)
bitmap = bitmap1


# Initialize clock to register response time
rt_clock = core.Clock()
rt_clock.reset()  # set rt clock to 0


# show alternating pics until response
frameN = 0
while len(keys) == 0:       

    if bitmap == bitmap1:
        bitmap = bitmap2
    else:
        bitmap = bitmap1

    bitmap.draw() 

    win.logOnFlip(msg='frame=%i' %frameN, level=logging.DEBUG)  #record the time of win.flip() in the log file
    win.flip()  # show image
    frameN = frameN + 1 

    core.wait(.084)  # wait 100 ms


    keys = event.getKeys(keyList=['space', 'escape'])    #record resp

    # if response stop
    if len(keys)>0:
        rt = rt_clock.getTime()
        break      

print keys, rt  #show resp and rt on screen

win.saveFrameIntervals(filename+'.log', clear=True)

win.close()
core.quit()

Solution

  • Yes, there is a better way! The standard solution exploits the fact that win.flip() halts code execution until the next monitor update. So looping over win.flip() will give you exactly one frame per loop. So to switch between two imagesStims (bitmap1 and bitmap2) until there is a response:

    clock = core.Clock()  # to assess timing
    keepLooping = True
    while keepLooping:  # continue until break
        for thisBitmap in [bitmap1, bitmap2]:  # alternate between images
            if keepLooping:  # do not show bitmap2 if a key was pressed on bitmap1
                for frameN in range(6):  # 100 ms
                    thisBitmap.draw()
                    print clock.getTime()
                    win.callOnFlip(clock.reset)  # ... or use win.logOnFlip
                    win.flip()  # inner loop is timed to this, as long as the rest of the code in here doesn't take longer than a frame.
    
                    keys = event.getKeys(keyList=['space', 'escape'])
                    if keys:  # notice simplification. [] evaluates to False.
                        rt = rt_clock.getTime()
                        keepLooping = False
                        break
    

    ... and then the rest. I've used core.Clock() to assess the time here, but your win.logOnFlip() is just as good. Depends on what kind of output you want.

    Note that event.getKeys() records the time this line was executed, not the time that the key was pressed. Therefore it adds a small delay. In this "frame-locked loop" the key response is therefore discretized to frame intervals. If you want to get real asynchronous polling of the keyboard state (i.e. if up to +16ms error in RT recording matters), use the iohub module. Many keyboards have an inherent 10-30 ms latency anyway, so you can't get rid of all latency.