# Fade an image to black instead of inverting colors

**URL:** https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036
**Category:** Coding
**Tags:** screen, color
**Created:** [August 9, 2018, 2:13pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036 "2018-08-09T14:13:12Z")
**Posts on this page:** 12
**Page:** 1

<div class="post-metadata">

### Author: ![gbrookshire](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/gbrookshire/32/2857_2.png) [@gbrookshire](https://discourse.psychopy.org/u/gbrookshire)
#### Post date: [August 9, 2018, 2:13pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/1 "2018-08-09T14:13:12Z")

</div>

Hi everyone,

I’m trying to dynamically change the color of an image on the screen. To accomplish this, I’m using `visual.ImageStim` and changing the colors with `stimulus.color = [r, g, b]`. This works great for any color brighter than neutral grey: light colors fade out to grey. However, it does not work for any colors darker than neutral grey. When I try to make the whole image black, for example (`stimulus.color = [-1, -1, -1]`), the colors in the image are inverted instead of displaying as black.

The math of the color maps makes sense here: Multiply by a negative number to invert the color. However, the same outcome occurs regardless of whether I use the `rgb` or the `rgb255` colormap.

**Do you have any suggestions for how to dynamically recolor an image to a dark color?**

I’m including a minimal working example here.

Thanks!  
geoff

```python
from psychopy import visual, core
import numpy as np

CS = 'rgb' # ColorSpace
WHITE = [1, 1, 1]
LIGHT_GREY = [0.5, 0.5, 0.5]
GREY = [0, 0, 0]
BLACK = [-1, -1, -1]

## ---- Comment this section in to try a different colorspace
# CS = 'rgb255' # ColorSpace
# WHITE = [255, 255, 255]
# LIGHT_GREY = [200, 200, 200]
# GREY = [128, 128, 128]
# BLACK = [0, 0, 0]

win = visual.Window([800, 800], monitor='testMonitor',
                    color=LIGHT_GREY, colorSpace=CS,
                    units='pix')

img = np.array([[-1, 0], [0, 1]]) # Image bitmap

stimulus = visual.ImageStim(win=win, image=img,
                     colorSpace=CS,
                     size=(100, 100),
                     units='pix')

# Show the normal stimulus
stimulus.color = WHITE
stimulus.draw()
win.flip()
core.wait(1.0)

# I want to show the stimulus faded to black.
# Instead, this inverts the stimulus, showing
# black areas as white and vice versa, leaving
# neutral grey areas unchanged.
stimulus.color = BLACK
stimulus.draw()
win.flip()
core.wait(1.0)

# This strategy does work for fading to neutral grey, however.
stimulus.color = GREY
stimulus.draw()
win.flip()
core.wait(1.0)

```

---

<div class="post-metadata">

### Author: ![gbrookshire](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/gbrookshire/32/2857_2.png) [@gbrookshire](https://discourse.psychopy.org/u/gbrookshire)
#### Post date: [August 9, 2018, 2:28pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/2 "2018-08-09T14:28:20Z")

</div>

Just realized I forgot to include my system info!

I’m running Psychopy 1.90.2 on Windows 10.

thanks,  
geoff

---

<div class="post-metadata">

### Author: ![dvbridges](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/dvbridges/32/1918_2.png) [@dvbridges](https://discourse.psychopy.org/u/dvbridges)
#### Post date: [August 9, 2018, 2:31pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/3 "2018-08-09T14:31:41Z")

</div>

Hi, a quick hack would be to fade your image by performing operations on your numpy array, using np.clip to ensure all values are within range (i…e, -1, 1), and setting your image on every frame. There is probably a less computationally expensive way than setting image on every refresh.

Adding the following to your code…

```auto
for i in range(200):
    img = img-.01
    np.clip(img, -1, 1, img)
    stimulus.setImage(img)
    stimulus.draw()
    win.flip()

```

---

<div class="post-metadata">

### Author: ![gbrookshire](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/gbrookshire/32/2857_2.png) [@gbrookshire](https://discourse.psychopy.org/u/gbrookshire)
#### Post date: [August 9, 2018, 4:15pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/4 "2018-08-09T16:15:09Z")

</div>

Hi there,

Thanks for the suggestion – it sounds like that could work in this case! However, I’m hoping for a generalizable solution that will work for any kind of stimulus – Cirlce or TextStim in addition to ImageStim.

The ideal solution would be the ability to do exacly what the current code does, but in a way that’s mathematically consistent with a 0–255 color map instead of a -1–1 colormap. Is it possible to change the colormap at a lower level? It’s perplexing that the color computations act as if they’re using a -1–1 colormap even when you specify 0–255.

Thanks again!

---

<div class="post-metadata">

### Author: ![djmannion](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/djmannion/32/37535_2.png) [@djmannion](https://discourse.psychopy.org/u/djmannion)
#### Post date: [August 9, 2018, 9:30pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/5 "2018-08-09T21:30:01Z")

</div>

You could draw a black object behind and then dynamically update the foreground object’s opacity. For example:

```python
from psychopy import visual, core
import numpy as np

CS = 'rgb' # ColorSpace
WHITE = [1, 1, 1]
LIGHT_GREY = [0.5, 0.5, 0.5]
GREY = [0, 0, 0]
BLACK = [-1, -1, -1]

## ---- Comment this section in to try a different colorspace
# CS = 'rgb255' # ColorSpace
# WHITE = [255, 255, 255]
# LIGHT_GREY = [200, 200, 200]
# GREY = [128, 128, 128]
# BLACK = [0, 0, 0]

win = visual.Window([800, 800], monitor='testMonitor',
                    color=LIGHT_GREY, colorSpace=CS,
                    units='pix')

img = np.array([[-1, 0], [0, 1]]) # Image bitmap

stimulus = visual.ImageStim(win=win, image=img,
                     colorSpace=CS,
                     size=(100, 100),
                     units='pix')

bg = visual.ImageStim(win=win, image=np.ones(img.shape) * -1.0,
                     colorSpace=CS,
                     size=(100, 100),
                     units='pix')

# Show the normal stimulus
stimulus.color = WHITE
stimulus.draw()
win.flip()
core.wait(1.0)

for fade_factor in np.linspace(0.0, 1.0, 60):
    bg.draw()
    stimulus.opacity = 1.0 - fade_factor
    stimulus.draw()
    win.flip()

core.wait(1.0)

```

---

<div class="post-metadata">

### Author: ![gbrookshire](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/gbrookshire/32/2857_2.png) [@gbrookshire](https://discourse.psychopy.org/u/gbrookshire)
#### Post date: [August 24, 2018, 1:39pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/6 "2018-08-24T13:39:28Z")

</div>

Thanks for the suggestion!

Unfortunately, after playing around with this, I see that it doesn’t work. I need to fade the color independently in each color channel.

For example, on one frame I might need white areas of the image to have the color [254, 10, 10], black areas to have the color [0, 0, 0], and grey areas to have the color halfway between the two [127, 5, 5]. This can’t be accomplished by adjusting the foreground object’s opacity.

If we could adjust the image colors assuming a colormap that varied from 0 to 255 instead of from -1 to 1, this would be trivial. Does anyone know if it’s possible to do that? (or know of another way to accomplish the same thing?)

thanks,  
geoff

---

<div class="post-metadata">

### Author: ![gbrookshire](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/gbrookshire/32/2857_2.png) [@gbrookshire](https://discourse.psychopy.org/u/gbrookshire)
#### Post date: [August 24, 2018, 3:37pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/7 "2018-08-24T15:37:38Z")

</div>

I just finished a version where I perform operations directly on a numpy array, and reset the image on every frame. Unfortunately, this is too slow. I have 8 stimuli, and it’s not capable of running `stimulus.setImage(img)` on each of those stimuli within the time it takes to refresh a frame.

---

<div class="post-metadata">

### Author: ![djmannion](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/djmannion/32/37535_2.png) [@djmannion](https://discourse.psychopy.org/u/djmannion)
#### Post date: [August 24, 2018, 8:29pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/8 "2018-08-24T20:29:35Z")

</div>

I can’t quite follow what your stimulus requirements are. Can you either expand on what a ‘working’ stimulus would be like, or post an example of slow-but-working code?

---

<div class="post-metadata">

### Author: ![gbrookshire](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/gbrookshire/32/2857_2.png) [@gbrookshire](https://discourse.psychopy.org/u/gbrookshire)
#### Post date: [August 27, 2018, 1:13pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/9 "2018-08-27T13:13:24Z")

</div>

Sure thing – apologies for the confusion. You can think of it like putting a colored piece of glass in front of the computer screen. I need to filter my stimuli through color filters like you’d put on a camera lens. Here’s a working example of what I need.

```auto
from __future__ import division
from psychopy import core, visual
import numpy as np

CS = 'rgb' # ColorSpace
WHITE = [1, 1, 1]
LIGHT_GREY = [0.5, 0.5, 0.5]
GREY = [0, 0, 0]
BLACK = [-1, -1, -1]
RED = [1, -1, -1]
BLUE = [-1, -1, 1]
GREEN = [-1, 1, -1]

def color_filter(img, rgb_filter):
    """ Filter a greyscale image through an RGB color filter

    img: a 2D numpy array
    filter: a 3-number RGB sequence (-1)-(1)
    """
    img_rgb = np.empty([img.shape[0], img.shape[1], 3])
    rgb_filter = rescale_zero_min(rgb_filter)
    img = rescale_zero_min(img)
    for n_channel in range(3): # For each color channel
        img_rgb[:,:,n_channel] = img * rgb_filter[n_channel]
    return img_rgb

def rescale_zero_min(x):
    """ Rescale a color from (-1)-(1) to 0-1.
    """
    return (np.array(x) + 1) / 2

img = np.array([[-1, 0], [0, 1]]) # Image bitmap

win = visual.Window([400, 400], monitor='testMonitor',
                    color=LIGHT_GREY, colorSpace=CS,
                    units='pix')

stimulus = visual.ImageStim(win=win, image=img,
                     colorSpace=CS,
                     size=(100, 100),
                     units='pix')

# Show the example stimuli
for color in (WHITE, LIGHT_GREY, GREY, BLACK, RED, BLUE, GREEN):
    filtered_image = color_filter(img, color)
    stimulus.setImage(filtered_image) ## <---- This is too slow
    stimulus.draw()
    win.flip()
    core.wait(1.0)

```

I’ve been looking through the psychopy source code, and I think I’ve found a couple places that prevent me from getting this behavior by setting the `color` attribute of `visual.ImageStim` objects. The class `psychopy.visual.basevisual.ColorMixin` has a `_getDesiredRGB` method with the comment: “Ensure that we work on 0-centered color (to make negative contrast values work)”. This is exactly what I want to avoid, but overwriting that method doesn’t seem to solve the problem.

I also looked at the `image` method (an `attributeSetter`) of the `visual.ImageStim` class, and found that this method specifies what looks like an OpenGL colormap. When creating the texture (line 285 of psychopy.visual.image.py), `self._createTexture` takes the argument `pixFormat=GL.GL_RGB`. Maybe I could solve the problem by changing that argument, but I haven’t been able to make sense of the OpenGL documentation.

Thank you for your help!

---

<div class="post-metadata">

### Author: ![djmannion](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/djmannion/32/37535_2.png) [@djmannion](https://discourse.psychopy.org/u/djmannion)
#### Post date: [August 27, 2018, 10:02pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/10 "2018-08-27T22:02:58Z")

</div>

My suggestion is to draw the stimulus, change the blend mode, draw the filter, and then reset the blend mode. If you change the blend mode to `GL_DST_COLOR, GL_ZERO`, then the filter will be multiplied with the stimulus to produce the output (see [this great blend mode helper site](https://www.andersriggelsen.dk/glblendfunc.php)).

Here is an example:

```python
from __future__ import division
from psychopy import core, visual, event
import numpy as np

import pyglet.gl

CS = 'rgb' # ColorSpace
WHITE = [1, 1, 1]
LIGHT_GREY = [0.5, 0.5, 0.5]
GREY = [0, 0, 0]
BLACK = [-1, -1, -1]
RED = [1, -1, -1]
BLUE = [-1, -1, 1]
GREEN = [-1, 1, -1]

def color_filter(img, rgb_filter):
    """ Filter a greyscale image through an RGB color filter

    img: a 2D numpy array
    filter: a 3-number RGB sequence (-1)-(1)
    """
    img_rgb = np.empty([img.shape[0], img.shape[1], 3])
    rgb_filter = rescale_zero_min(rgb_filter)
    img = rescale_zero_min(img)
    for n_channel in range(3): # For each color channel
        img_rgb[:,:,n_channel] = img * rgb_filter[n_channel]
    return img_rgb

def rescale_zero_min(x):
    """ Rescale a color from (-1)-(1) to 0-1.
    """
    return (np.array(x) + 1) / 2

img = np.array([[-1, 0], [0, 1]]) # Image bitmap

win = visual.Window([400, 400], monitor='testMonitor',
                    color=LIGHT_GREY, colorSpace=CS,
                    useFBO=True,
                    units='pix')

stimulus = visual.ImageStim(win=win, image=img,
                     colorSpace=CS,
                     pos=(0, -100),
                     size=(100, 100),
                     units='pix')

new_stimulus = visual.ImageStim(win=win, image=img,
                     colorSpace=CS,
                     pos=(0, 100),
                     size=(100, 100),
                     units='pix')

img_filter = visual.ImageStim(win=win, image=np.ones((100, 100)),
                     colorSpace=CS,
                     pos=(0, 100),
                     size=(100, 100),
                     units='pix')

# Show the example stimuli
for color in (WHITE, LIGHT_GREY, GREY, BLACK, RED, BLUE, GREEN):
    filtered_image = color_filter(img, color)
    stimulus.setImage(filtered_image) ## <---- This is too slow

    stimulus.draw()

    new_stimulus.draw()

    pyglet.gl.glBlendFunc(pyglet.gl.GL_DST_COLOR, pyglet.gl.GL_ZERO)

    img_filter.color = color
    img_filter.draw()

    # reset the blend mode
    pyglet.gl.glBlendFunc(pyglet.gl.GL_SRC_ALPHA, pyglet.gl.GL_ONE_MINUS_SRC_ALPHA)

    win.flip()

    frame = np.array(win.getMovieFrame())

    assert np.all(frame[:200, :] == frame[200:, :])

    event.waitKeys()

```

---

<div class="post-metadata">

### Author: ![gbrookshire](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/gbrookshire/32/2857_2.png) [@gbrookshire](https://discourse.psychopy.org/u/gbrookshire)
#### Post date: [August 28, 2018, 10:59am UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/11 "2018-08-28T10:59:45Z")

</div>

Fantastic! This solves the problem, with one caveat. I’m trying to use circular alpha-masks on my stimuli, using the `mask='circle'` argument when initializing `visual.ImageStim`. But changing the GL blend function appears to disable the alpha mask. Is it possible to choose a GL blend function that preserves those masks?

Thank you!

---

<div class="post-metadata">

### Author: ![gbrookshire](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.psychopy.org/gbrookshire/32/2857_2.png) [@gbrookshire](https://discourse.psychopy.org/u/gbrookshire)
#### Post date: [August 28, 2018, 4:32pm UTC](https://discourse.psychopy.org/t/fade-an-image-to-black-instead-of-inverting-colors/5036/12 "2018-08-28T16:32:31Z")

</div>

I fixed the broken mask by adding a stimulus object on top of these that is just a mask with an aperture in the middle (after resetting the GL blend mode). Everything is working great now.

Thank you for all your help!
