Simplest way to add static random noise to a stimulus (letter)

Hello,
I am planning to run a letter discrimination experiment. In each trial, a white letter is presented on a black background. The letter is degraded by randomly reversing the contrast polarity of some
proportion of the pixels in both the letter and the background. That is, some proportion of the letter pixels are changed to black, while the same proportion of the background pixels are changed to white.
When this proportion equals 50%, the display becomes a homogeneous, random array of black and white pixels, and the letter is no longer visible. I would appreciate some guidance on the easiest way to build these stimuli within psychopy (an example is provided below).
Best,
Mat

image

see demo " maskReveal.py " in your psychopy install for a starter

One way might be to use a BufferImageStim to capture the image of the letter, manipulate the resulting pixels, and then use those to create and draw a GratingStim.

Rough demo below - it might get more complicated if img_size is not a power of 2.

import numpy as np

import psychopy.visual
import psychopy.event


img_size = 128
win_size = img_size * 2
img_frac = img_size / win_size

# probability of a pixel reversing in polarity
p_flip = 0.25
# whether to flip each pixel, as a vector; start as unflipped
pix_mod = np.ones(img_size ** 2)
# number of pixels to flip
n_to_flip = np.round((img_size ** 2) * p_flip).astype(int)
# set the required proportion to flip
pix_mod[:n_to_flip] = -1
# randomise their positions
np.random.shuffle(pix_mod)
# convert into an image
pix_mod = np.reshape(a=pix_mod, newshape=(img_size,) * 2)
pix_mod = pix_mod[..., np.newaxis]

with psychopy.visual.Window(
    size=(win_size,) * 2, color=(-1,) * 3, fullscr=False, units="norm",
) as win:

    # the desired letter, without noise
    text = psychopy.visual.TextStim(
        win=win, text="F", height=0.5, antialias=False, units="norm"
    )

    # region of the window to capture
    capture_rect = [-img_frac, img_frac, img_frac, -img_frac]

    # get the image of the letter
    buff = psychopy.visual.BufferImageStim(
        win=win, stim=[text], interpolate=False, rect=capture_rect
    )

    # convert from [0, 255] to [-1, +1]
    buff_img = np.array(buff.image).astype("float") / 255.0 * 2.0 - 1.0

    # do the polarity flipping
    img = np.flipud(buff_img) * pix_mod

    # generate a stimulus to show the noisy image
    img_tex = psychopy.visual.GratingStim(
        win=win, tex=img, mask=None, size=(img_size,) * 2, units="pix"
    )

    img_tex.draw()

    win.flip()

    psychopy.event.waitKeys()

image

1 Like

@djmannion Great idea, thank you so much! I can’t modify the code to make it fullscreen though. I tried this :

import numpy as np
from psychopy import visual, event, core, gui


img_size = 128
#win_size = img_size * 2#256
#img_frac = img_size / win_size


# probability of a pixel reversing in polarity
p_flip = 0.1
# whether to flip each pixel, as a vector; start as unflipped
pix_mod = np.ones(540*960)
# number of pixels to flip
n_to_flip = np.round((540*960) * p_flip).astype(int)
# set the required proportion to flip
pix_mod[:n_to_flip] = -1
# randomise their positions
np.random.shuffle(pix_mod)
# convert into an image
pix_mod = np.reshape(a=pix_mod, newshape=(540,960))#(128,128) array
pix_mod = pix_mod[..., np.newaxis]#(432,768,1) array   (height, width,  number of color channels (grayscale here))


win = visual.Window(monitor='mathieu', color=[0,0,0], colorSpace='rgb255', units='norm', fullscr=True)
expName="lexical decision"
expInfo={'participant':''}

dlg=gui.DlgFromDict(dictionary=expInfo,title=expName, order=['participant'])
if dlg.OK==False: core.quit() #user pressed cancel

text = visual.TextStim(
        win=win, text="F", height=0.15, antialias=False, units="norm", fullscr=True)

#text.draw()
#win.flip()
#event.waitKeys()
#core.quit()

#
### region of the window to capture
capture_rect = [-0.5, 0.5, 0.5, -0.5]    #[left, top, right, bottom] 
##
### get the image of the letter
buff = visual.BufferImageStim(win=win, stim=[text], interpolate=False, rect=capture_rect)#screen shot of the stimulus in the back buffer (hidden), norm units (mandatory)
#
#
## convert from [0, 255] to [-1, +1]
buff_img = np.array(buff.image).astype("float") / 255.0 * 2.0 - 1.0
#
## do the polarity flipping
img = np.flipud(buff_img) * pix_mod
#
## generate a stimulus to show the noisy image
img_tex = visual.GratingStim(
        win=win, tex=img, mask=None, size=(540*960), units="pix")
#
img_tex.draw()
win.flip()
#
#event.waitKeys()

event.waitKeys()
core.quit()

Which does not even return something fullscreen! There might something with the BufferImageStim function that I don’t understand. Any idea?
There is another complication when switching to fullscreen mode. On windows 10 (15’ laptop), 1920*1080 resolution, psychopy uses an incorrect resolution (WARNING User requested fullscreen with size [800 600], but screen is actually [1536, 864]). I think Psychopy uses the default 125% scaling from windows. However, adding a gui at the beginning of the code e.g.

expName="lexical decision"
expInfo={'participant':''}  
dlg=gui.DlgFromDict(dictionary=expInfo,title=expName, order=['participant'])
if dlg.OK==False: core.quit()

solves the issue (Psychopy now identify the 1920*1080 resolution) so I am a little lost here. Maybe @jon has some insight on this? (note: I am using Psychopy v3.2.4)

Hmmm, I don’t have any insight into why it isn’t capturing fullscreen.

The most elegant way of generating that particular kind of noise would be:

  • render the stimulus with no noise
  • change the blend mode to use multiplication (instead of the current options of adding or averaging)
  • render some pixels that are either -1 or +1 at your chosen ratio (where -1 pixels fell they would essentially flip the color)

Actually, another solution might be to use the dedicated Noise stimulus, which I think does allow for more complex combinations of textures. I don’t have an immediate suggestion for how to do it but @schofiaj might be able to help since he wrote that stimulus class and thinks a lot about noise stimuli

The demo assumes a square window size, which won’t be the case when running fullscreen. You would need to adjust the logic to handle non-square sizes.

Something like the below - though there may be issues with the window resolution scaling you mention (I don’t have any experience with that).

import numpy as np

import psychopy.visual
import psychopy.event


# horizontal and vertical sizes of the noise region
# these are relative to the vertical size of the window
noise_frac_xy = np.array([0.5, 0.5])

with psychopy.visual.Window(
    color=(-1,) * 3, fullscr=True, units="norm",
) as win:

    # size of the window, in pixels
    (win_x, win_y) = win.size

    # size of the captured region, in pxiels
    img_size_xy = np.round(noise_frac_xy * win_y).astype(int)

    # captured region as fractions of the window size
    img_frac_xy = img_size_xy / win.size
    (img_frac_x, img_frac_y) = img_frac_xy

    # captured size in (row, column) format
    img_size_ij = img_size_xy[::-1]

    # probability of a pixel reversing in polarity
    p_flip = 0.25
    # total number of pixels in the noise region
    n_pixels = np.prod(img_size_ij)
    # whether to flip each pixel, as a vector; start as unflipped
    pix_mod = np.ones(n_pixels)
    # number of pixels to flip
    n_to_flip = np.round(n_pixels * p_flip).astype(int)
    # set the required proportion to flip
    pix_mod[:n_to_flip] = -1
    # randomise their positions
    np.random.shuffle(pix_mod)
    # convert into an image
    pix_mod = np.reshape(a=pix_mod, newshape=img_size_ij)
    pix_mod = pix_mod[..., np.newaxis]

    # the desired letter, without noise
    text = psychopy.visual.TextStim(
        win=win, text="F", height=0.5, antialias=False, units="norm"
    )

    # region of the window to capture
    capture_rect = [
        -img_frac_x,  # left
        +img_frac_y,  # top
        +img_frac_x,  # right
        -img_frac_y,  # bottom
    ]

    # get the image of the letter
    buff = psychopy.visual.BufferImageStim(
        win=win, stim=[text], interpolate=False, rect=capture_rect
    )

    # convert from [0, 255] to [0, 1], binarise, then convert to [-1, +1]
    buff_img = (
        np.round(np.array(buff.image).astype("float") / 255.0) * 2.0 - 1.0
    )

    # do the polarity flipping
    img = np.flipud(buff_img) * pix_mod

    # generate a stimulus to show the noisy image
    img_tex = psychopy.visual.ImageStim(
        win=win, image=img, mask=None, size=img_size_xy, units="pix"
    )

    img_tex.draw()

    win.flip()

    psychopy.event.waitKeys()
1 Like

Hi, in reply to @jon it might be possible to use a binary noise sample as the envelope of an envelope grating with the underlying images as the carrier. This would effect the multiplication he describes. However, the binary noise type always has 50:50 pixels at +1 and -1 respectively and has no option to vary that ratio.

Thanks @djmannion, works like a charm