Posts about psyhophysics

IRM clouds

A feature of MotionClouds is the ability to precisely tune the precision of information following the principal axes. One which is particularly relevant for the primary visual cortical area of primates (area V1) is to tune the otirentation mean and bandwidth.

Studying the role of contrast in V1 using MotionClouds

Read more…

Mirror clouds

In [1]:
%matplotlib inline
import numpy as np
np.set_printoptions(precision=3, suppress=True)
import pylab
import matplotlib.pyplot as plt

An essential component of natural images is that they may contain order at large scales such as symmetries. Let's try to generate some textures with an axial (arbitrarily vertical) symmetry and take advantage of the different properties of random phase textures. For that, we will first create a "vanilla" texture:

In [2]:
import os
name = 'mirror'
DOWNSCALE = 1
V_X = .5
#!rm -fr ../files/mirror*
In [3]:
import MotionClouds as mc
N_X, N_Y, N_frame = mc.N_X/DOWNSCALE, mc.N_Y/DOWNSCALE, mc.N_frame/DOWNSCALE

fx, fy, ft = mc.get_grids(N_X, N_Y, N_frame)
z = mc.envelope_gabor(fx, fy, ft, V_X=V_X)

name_ = name + '_nomirror'
movie = mc.rectif(mc.random_cloud(z))
mc.anim_save(movie, os.path.join(mc.figpath, name_))
mc.in_show_video(name_)

The first thing to try is to use the periodicity of MotionClouds: Since they are generated in the Fourier space, they have a period in space (width of the display) and in time (there is no gap perceived when you play these in loops as we do here). So we may take the above texture, mirror it vertically and concatenate it to its right:

In [4]:
name_ = name + '_horizontal'
mirrored_movie = np.vstack((movie, movie[::-1, :, :])) 
mc.anim_save(mirrored_movie, os.path.join(mc.figpath, name_))
mc.in_show_video(name_)

Mathematically, taking the sum of these two parts $$ I_M(x, y, t) = \frac 1 2 \cdot (I(x, y, t) + I(x, -y, t)) $$

generates a symmetric pattern: $$ I_M(x, -y, t) = \frac 1 2 \cdot (I(x, -y, t) + I(x, y, t)) = I_M(x, y, t) $$

The vertical axis of symmetry is around $y=0$ and (from the periodicity $I(x, y+N_y, t)=I(x, y, t)$) around $y=N_y/2$: $$ I_M(x, -y-N_y/2, t) = \frac 1 2 \cdot (I(x, -y-N_y/2, t) + I(x, -(-y-N_y/2), t)) = \frac 1 2 \cdot (I(x, -N_y/2-y, t) + I(x, N_y/2+y, t)) = I_M(x, y-N_y/2, t) $$

Another advantage of these clouds is that they are random phase textures: the different features are not coherent. While adding these 2 textures (the original and the mirrored one) would produce an interference pattern if we would have used gratings, it is different here:

In [5]:
name_ = name + '_fuse'
mirrored_movie = np.vstack((movie, movie[::-1, :, :])) 
mirrored_movie = .5*(movie + movie[::-1, :, :])
mc.anim_save(mirrored_movie, os.path.join(mc.figpath, name_))
mc.in_show_video(name_)

What happens now if we add more symmetries?

In general, it is possible to make this axis of symmetry at every coordinate $y=Y$. Let's call this operation $\mathcal{M}^Y$, such that : $$ \mathcal{M}^Y(I)(x, y-Y, t) = \frac 1 2 \cdot (I(x, y-Y, t) + I(x, -y-Y, t)) $$ It follows: $$ \mathcal{M}^Y(I)(x, y, t) = \frac 1 2 \cdot (I(x, y, t) + I(x, -y-2Y, t)) $$

In particular, in the example above, $I_M = \mathcal{M}^0(I)$.

Interestingly, the Fourier spectrum of such a transform is dual and in particular: $$ \mathcal{F}(\mathcal{M}^0(I))(f_x, f_y, f_t) = \mathcal{F}(\frac 1 2 \cdot (I(x, y, t) + I(x, -y, t))) $$ thus $$ \mathcal{F}(\mathcal{M}^0(I))(f_x, f_y, f_t) =\frac 1 2 \cdot (\mathcal{F}(I)(f_x, f_y, f_t) - \mathcal{F}(I)(f_x, -f_y, f_t))) $$ It means that this "mirrored motion clouds" are still random phase textures but where the phase is bound to have a mirror symmetry with respect to the central plane (here, $f_x, f_t$).

For instance, let's add it at $N_y/4$ by creating $\mathcal{M}^{N_y/4}(I)$:

In [6]:
def mirror(movie, Y=0):
    # translate
    movie = np.roll(movie, -Y, axis=0)
    # add the mirror image
    mirrored_movie = .5*(movie + movie[::-1, :, :])
    # translate in the other direction
    return np.roll(mirrored_movie, Y, axis=0)

name_ = name + '_fuse_quarter'
mirrored_movie = mirror(movie, Y=int(N_Y/4))
mc.anim_save(mirrored_movie, os.path.join(mc.figpath, name_))
mc.in_show_video(name_)

Note that in the particular case of Motion Clouds (and everything created in the Fourier domain in general), the stimulus is periodic in time and space. Due to this, we have $I(x, y + N_Y, t)=I(x, y, t)$ and thus $$ \mathcal{M}^Y(I)(x, y-Y+N_Y/2, t) = \frac 1 2 \cdot (I(x, y-Y+N_Y/2, t) + I(x, -y-Y+N_Y/2- N_Y, t))= \mathcal{M}^Y(I)(x, -y-Y+N_Y/2, t) $$

That is, $Y$ is an axis of symmetry for $\mathcal{M}^Y(I)$, but $Y+N_Y/2$ is too. For $Y=0$, there is an axis of symmetry at $y=0$ and one at $y=N_y/2$.

Now we may pipe both operations: $$ I_MM = \mathcal{M}^{N_y/4}(\mathcal{M}^0(I)) $$

We check that: $$ I_MM(-y) = \mathcal{M}^{N_y/4}(\mathcal{M}^0(I(-y))) = \mathcal{M}^{N_y/4}(\mathcal{M}^0(I(y))) = I_MM(y) $$ and $$ I_MM(-y-N_y/4) = \mathcal{M}^{N_y/4}(.5 * (I(y-N_y/4)+I(-y+N_y/4)))) = .25 * (I(y-N_y/4)+I(-y-N_y/4)+I(y+N_y/4)+I(-y+N_y/4)) = I_MM(y-N_y/4) $$ The stimulus is thus symmetric at $y \in \{ 0, N_y/4, N_y/2, 3*N_y/4 \} $.

In [7]:
name_ = name + '_fuse_double'

mirrored_movie = mirror(mirror(movie, Y=0), Y=int(N_Y/4))
mc.anim_save(mirrored_movie, os.path.join(mc.figpath, name_))
mc.in_show_video(name_)

You may have noticed that contrast has decreased: that's quite normal as we make sums and make the random phase texture more coherent. It can be easily proven that if we repeat this procedure again and again to be symmetric at every signle coordinate $y$, we end up with a constant stimulus (in the $y$ coordinate, not $x$ or $t$).

Speed distributions

In [1]:
%matplotlib inline
import numpy as np
np.set_printoptions(precision=3, suppress=True)
import pylab
import matplotlib.pyplot as plt
#!rm -fr ../files/speed*
In [2]:
import MotionClouds as mc
name = 'noisy-speed'
fx, fy, ft = mc.get_grids(mc.N_X, mc.N_Y, mc.N_frame)
print(mc.envelope_speed.__doc__)
    Returns the speed envelope:
    selects the plane corresponding to the speed ``(V_X, V_Y)`` with some bandwidth ``B_V``.

    * (V_X, V_Y) = (0,1) is downward and  (V_X, V_Y) = (1, 0) is rightward in the movie.
    * A speed of V_X=1 corresponds to an average displacement of 1/N_X per frame.
    To achieve one spatial period in one temporal period, you should scale by
    V_scale = N_X/float(N_frame)
    If N_X=N_Y=N_frame and V=1, then it is one spatial period in one temporal
    period. It can be seen along the diagonal in the fx-ft face of the MC cube.

    A special case is used when ``B_V=0``, where the ``fx-ft`` plane is used as
    the speed plane: in that case it is desirable to set ``(V_X, V_Y)`` to ``(0, 0)``
    to avoid aliasing problems.

    Run the 'test_speed' notebook to explore the speed parameters, see
    http://motionclouds.invibe.net/posts/testing-speed.html

    
In [3]:
# explore parameters
for B_V in [0.0, 0.01, 0.1, 1.0, 10.0]:
    name_ = name + '-B_V-' + str(B_V).replace('.', '_')
    z = mc.envelope_gabor(fx, fy, ft, V_X=0, B_V=B_V)
    mc.figures(z, name_)
    mc.in_show_video(name_)

Smooth transition between MCs

In [1]:
"""
A smooth transition while changing parameters

(c) Laurent Perrinet - INT/CNRS

"""

import MotionClouds as mc
import numpy as np
import os

name = 'smooth'

#initialize
fx, fy, ft = mc.get_grids(mc.N_X, mc.N_Y, mc.N_frame)

name_ = mc.figpath + name

seed = 123456
B_sf_ = [0.025, 0.05, 0.1, 0.2, 0.4, 0.2, 0.1, 0.05]
im = np.empty(shape=(mc.N_X, mc.N_Y, 0))
name_ = name + '-B_sf'
for i_sf, B_sf in enumerate(B_sf_):
    im_new = mc.random_cloud(mc.envelope_gabor(fx, fy, ft, B_sf=B_sf), seed=seed)
    im = np.concatenate((im, im_new), axis=-1)

mc.anim_save(mc.rectif(im), os.path.join(mc.figpath, name_))
mc.in_show_video(name_)
In [2]:
name_ += '_smooth'
smooth = (ft - ft.min())/(ft.max() - ft.min()) # smoothly progress from 0. to 1.
N = len(B_sf_)
im =  np.empty(shape=(mc.N_X, mc.N_Y, 0))
for i_sf, B_sf in enumerate(B_sf_):
    im_old = mc.random_cloud(mc.envelope_gabor(fx, fy, ft, B_sf=B_sf), seed=seed)
    im_new = mc.random_cloud(mc.envelope_gabor(fx, fy, ft, B_sf=B_sf_[(i_sf+1) % N]), seed=seed)
    im = np.concatenate((im, (1.-smooth)*im_old+smooth*im_new), axis=-1)

mc.anim_save(mc.rectif(im), os.path.join(mc.figpath, name_))
mc.in_show_video(name_)