Change color of each pixel as a function of frame

Hello,

I wanted to present stimuli so that on each frame each pixel will change color as sin function. One way to do it is to generate 3d array with 1st dimension being number of stims, and shift color of each pixel of the original image as a function of frame. It works fine but limited to the size of the array. I cannot use fullscreen stim (1920x1080) because of memory error, so I was wondering whether anybody knows how to deal with this.

Alternatively, is there a function in psychopy to shift the color of each pixel as frames change (smth like setPhase for color)?

I posted code to make it easier to understand what I am trying to do.

Any help will be appreciated,

test_img= (np.random.choice( [0,1], [1920, 1080])*2 -1) # generate noise matrix, rescale to -1,1

framerate=60 # Hz
duration = 1 # in sec
frame_num = duration*framerate
temporal_frequency = 1
stim= np.zeros((frame_num,test_img.shape[0], test_img.shape[1] ), dtype=‘float32’)

starting_phase=np.arcsin(test_img)
phase_shift=2*np.pi/framerate

for i in range(stim.shape[0]):

starting_phase=starting_phase+phase_shift
new_frame=np.sin(temporal_frequency*(starting_phase))
sin_drift_noise[i] = new_frame

for j in range(stim.shape[0]):
img.image = stim[j]
#print j, sin_drift_noise[j][0][0]
img.draw()
win.flip()

Hello,

Please post more details regarding the memory error, any messages/traceback it dumped will do. Determining what raised it can help localize the problem.

Interesting question. I don’t know of a straightforward way to do this, but below is a way that might work by altering the texture shader. This currently misappropriates the color attribute to adjust the phase of each pixel - it could be done properly by adding another variable to the shader.

It’s quite a neat-looking stimulus.

import numpy as np

import psychopy.visual
import psychopy.event
import psychopy.visual.shaders

win = psychopy.visual.Window(
    size=(1920, 1080),
    fullscr=False,
    units="pix"
)

default_shader = win._progSignedTexMask

noise = np.random.uniform(0, 1, (2048, 2048))

noise_tex = psychopy.visual.GratingStim(
    win=win,
    tex=noise,
    mask=None,
    size=(2048,2048)
)

shader = """
(sin((textureFrag.rgb * 2.0 * radians(180.0)) + (((gl_Color.rgb * 2.0) - 1.0) * 2.0 * radians(180.0))) + 1.0) / 2.0;
"""

frag_shader = psychopy.visual.shaders.fragSignedColorTexMask.replace(
    "gl_FragColor.rgb = (textureFrag.rgb* (gl_Color.rgb*2.0-1.0)+1.0)/2.0;",
    "gl_FragColor.rgb = " + shader
)

shader = psychopy.visual.shaders.compileProgram(
    vertexSource=psychopy.visual.shaders.vertSimple,
    fragmentSource=frag_shader
)

phase = 0.0

frames_per_cycle = 60

phase_inc = 1.0 / frames_per_cycle

noise_tex.color = phase

keep_going = True

while keep_going:

    win._progSignedTexMask = shader

    noise_tex.draw()

    # reset the shader
    win._progSignedTexMask = default_shader

    win.flip()

    keys = psychopy.event.getKeys()

    keep_going = ("q" not in keys)

    phase = np.mod(phase + phase_inc, 1)

    noise_tex.color = phase

win.close()

2 Likes

Hello,

Thanks for the reply… I was getting memory error when I tried to initialize ndarray:

stim= np.zeros((frame_num,test_img.shape[0], test_img.shape[1] ), dtype=‘float32’)
it would be of shape (60, 1920, 1920)

Thanks,

Hello,

Thanks for taking time and making the stim. It works great! I didn’t know about the shader and how to use them. I will play around and change small things but it should work.

Thanks again,

Hello Alex,

A 32-bit float array with those dimensions would occupy ~500 megabytes of RAM. Modern PC’s should be able to handle this. Check how much free memory you have to see if you can accommodate such large arrays. Ensure you are using the 64-bit version of Python/numpy, 32-bit Python may not be able to allocate a contiguous block of memory within the 32-bit address range (not sure this is even an issue anymore).

Hello,

I actually have one more question regarding this stim. I wanted to bandpass filter (spatial filtering) the stim but not sure what exactly should I filter (noise, shader, ,frag_shader ). Normally, I would just multiply filter and stim in frequency domain but I am not sure what shader is. It seems to be a uniform texture but I cannot use it directly as array because it seems to be a function.

So, the question is how can I spatially filter this stimulus?

Thanks,

I’m not sure how sensible this is for your purposes, or from a signal processing perspective, but you can filter the starting phases (contained in noise). Filtering the shader output is tricky and I’m not sure the best way to go about that. In the long run, you are probably best to work out the memory issue so that you can pre-generate the stimuli.

You could filter the starting phases by doing something like the following (inserted below the noise = np.random.uniform(0, 1, (2048, 2048)) in the example above):

import psychopy.filters

filt_out = np.full((2048, 2048, 2), np.nan)

bp_filt = psychopy.filters.butter2d_bp(
    size=(2048, 2048),
    cutin=0.1,
    cutoff=0.3,
    n=10
)

for (i_trig, trig_func) in enumerate((np.sin, np.cos)):

    trig_noise = trig_func(noise * 2.0 * np.pi)

    trig_freq = np.fft.fft2(trig_noise)

    trig_filt = np.fft.fftshift(trig_freq) * bp_filt

    filt_out[..., i_trig] = np.real(np.fft.ifft2(np.fft.ifftshift(trig_filt)))

phases = np.angle(filt_out[..., 1] + filt_out[..., 0] * 1j)

noise = np.mod(phases, 2 * np.pi) / (2.0 * np.pi)

Thanks for a quick reply, I will try it.