psychopy.org | Reference | Downloads | Github

ImageStim texture handle swapping

Hello,

Is it possible for a single ImageStim object change its texture to another stored in video memory?

Some background, I’m porting an experiment over to PsychoPy from a PyOpenGL+GLFW implementation to take advantage of its feature set. The original experiment (in PyOpenGL) presents images in quick succession at the same location to make a movie. These images were loaded from the hard drive prior to the start of trials and the texture handles returned by glGenTextures were stored. The program would simply change the texture handle to the next one in the sequence to change the image. ImageStim seems to only present a single texture that is specified upon initializing it. It might not be feasible to create in excess 10,000 of ImageStim objects to do what I’ve previously done. Due to the requirements of the project, textures must be loaded into video memory. They cannot be made into a movie beforehand.

I looked at the ImageStim code and I’m prepared the modify for my needs if there is no quick solution.

Thanks

Interesting question. I hadn’t had that use-case in mind so this is going to be a little bit of a hack, but I think it will work OK. The key pieces of info are that stimuli like ImageStim and GratingStim use the _createTexture() method that does all the heavy lifting around creating textures of the appropriate format and uploading them to the graphics card with an ID that you provide (generally the stim._texID for instance). Then, during draw() the value of stim._texID is used

So you might have something like this (untested pseudo-code):

import glob
import ctypes
from psychopy import visual
gl = visual.GL

imFiles = glob.glob("stims/*.png")
stim = visual.ImageStim(win, .....) # so that the image can be formatted for the stim correctly

imTextures = {} #create the textures on gfx card and store the IDs
for thisImage in imFiles:
    thisID = gl.GLuint()
    gl.glGenTextures(1, ctypes.byref(thisID)) # just creates the id
    #create the actual texture
    stim._createTexture( 
                    thisImage,
                    stim=stim,
                    pixFormat=gl.GL_RGB,
                    dataType=gl.GL_UNSIGNED_BYTE, # or gl.GL_FLOAT?
                    forcePOW2=False)
    imTextures[thisImage] = thisID

# later, to assign the different textures to the stimulus:
id = imTextures[imFilename] # extract the id for this file name
stim._texID = id
stim. _needUpdate = True # so the stimulus is updated during draw
stim.draw()

Note that the graphics card is not deleting these textures until the end of the script but you can delete them explicitly with gl.glDeleteTextures(1, id)

I guess, ideally, we should create a Texture class to handle this better. If ImageStim/GratingStim is handed a texture then needn’t recreate it, but just use the appropriate GL id.

Thanks Jon,

I had to modify your code a bit to make it work. The following works perfectly on my system and should others.

import glob
import ctypes
from psychopy import visual, event
from pyglet import gl

def main(args):

    SCREEN_SIZE = [1920,1080]
    
    win = visual.Window(SCREEN_SIZE, fullscr=True, rgb=[-1,-1,-1], checkTiming=True, waitBlanking=True)
    
    imFiles = glob.glob("*.png")
    stim = visual.ImageStim(win, units='pix', size=SCREEN_SIZE) # images are the size of the screen

    imTextures = list()
    for thisImage in imFiles:
        thisID = gl.GLuint()
        
        gl.glGenTextures(1, ctypes.byref(thisID))
        stim._createTexture(thisImage, thisID, 
            gl.GL_RGB, stim, res=128, maskParams=None, 
            forcePOW2=False, dataType=gl.GL_UNSIGNED_BYTE)
            
        imTextures.append(thisID)
    
    n = 0
    while 1:
        if not event.getKeys(keyList=['q'], timeStamped=False):
            if n < len(imTextures):
                id = imTextures[n]
                stim._texID = id
                stim. _needUpdate = True
                stim.draw()
                win.flip()
                n += 1
            else:
                n = 0
        else:
            break
    
if __name__ == '__main__':
    import sys
    sys.exit(main(sys.argv))

I’ll verify the on screen timings when I have the chance but there is no perceptible juddering at 100 Hz.