Source code for psychopy.visual.filters

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""Various useful functions for creating filters and textures
(e.g. for PatchStim)
"""

# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).

import numpy
from numpy.fft import fft2, ifft2, fftshift, ifftshift


[docs]def makeGrating(res, ori=0.0, # in degrees cycles=1.0, phase=0.0, # in degrees gratType="sin", contr=1.0): """Make an array containing a luminance grating of the specified params :Parameters: res: integer the size of the resulting matrix on both dimensions (e.g 256) ori: float or int (default=0.0) the orientation of the grating in degrees cycles:float or int (default=1.0) the number of grating cycles within the array phase: float or int (default=0.0) the phase of the grating in degrees (NB this differs to most PsychoPy phase arguments which use units of fraction of a cycle) gratType: 'sin', 'sqr', 'ramp' or 'sinXsin' (default="sin") the type of grating to be 'drawn' contr: float (default=1.0) contrast of the grating :Returns: a square numpy array of size resXres """ # to prevent the sinusoid ever being exactly at zero (for sqr wave): tiny = 0.0000000000001 ori *= -numpy.pi / 180. phase *= numpy.pi / 180. cyclesTwoPi = cycles * 2.0 * numpy.pi xrange, yrange = numpy.mgrid[ 0.0:cyclesTwoPi:(cyclesTwoPi / res), 0.0:cyclesTwoPi:(cyclesTwoPi / res)] sin, cos = numpy.sin, numpy.cos if gratType == "none": res = 2 intensity = numpy.ones((res, res), float) elif gratType == "sin": intensity = contr * sin(xrange * sin(ori) + yrange * cos(ori) + phase) elif gratType == "ramp": intensity = contr * (xrange * cos(ori) + yrange * sin(ori)) / (2 * numpy.pi) elif gratType == "sqr": # square wave (symmetric duty cycle) intensity = numpy.where(sin(xrange * sin(ori) + yrange * cos(ori) + phase + tiny) >= 0, 1, -1) elif gratType == "sinXsin": intensity = sin(xrange) * sin(yrange) else: # might be a filename of an image # try: # im = Image.open(gratType) # except Exception: # logging.error("couldn't find tex...", gratType) # return # # todo: opened it, now what? raise ValueError("Invalid value for parameter `gratType`.") return intensity
[docs]def maskMatrix(matrix, shape='circle', radius=1.0, center=(0.0, 0.0)): """Make and apply a mask to an input matrix (e.g. a grating) :Parameters: matrix: a square numpy array array to which the mask should be applied shape: 'circle','gauss','ramp' (linear gradient from center) shape of the mask radius: float scale factor to be applied to the mask (circle with radius of [1,1] will extend just to the edge of the matrix). Radius can be asymmetric, e.g. [1.0,2.0] will be wider than it is tall. center: 2x1 tuple or list (default=[0.0,0.0]) the centre of the mask in the matrix ([1,1] is top-right corner, [-1,-1] is bottom-left) """ # NB makeMask now returns a value -1:1 alphaMask = makeMask(matrix.shape[0], shape, radius, center=(0.0, 0.0), range=[0, 1]) return matrix * alphaMask
[docs]def makeMask(matrixSize, shape='circle', radius=1.0, center=(0.0, 0.0), range=(-1, 1), fringeWidth=0.2): """Returns a matrix to be used as an alpha mask (circle,gauss,ramp). :Parameters: matrixSize: integer the size of the resulting matrix on both dimensions (e.g 256) shape: 'circle','gauss','ramp' (linear gradient from center), 'raisedCosine' (the edges are blurred by a raised cosine) shape of the mask radius: float scale factor to be applied to the mask (circle with radius of [1,1] will extend just to the edge of the matrix). Radius can asymmetric, e.g. [1.0,2.0] will be wider than it is tall. center: 2x1 tuple or list (default=[0.0,0.0]) the centre of the mask in the matrix ([1,1] is top-right corner, [-1,-1] is bottom-left) fringeWidth: float (0-1) The proportion of the raisedCosine that is being blurred. range: 2x1 tuple or list (default=[-1,1]) The minimum and maximum value in the mask matrix """ rad = makeRadialMatrix(matrixSize, center, radius) if shape == 'ramp': outArray = 1 - rad elif shape == 'circle': # outArray=numpy.ones(matrixSize,'f') outArray = numpy.where(numpy.greater(rad, 1.0), 0.0, 1.0) elif shape == 'gauss': outArray = makeGauss(rad, mean=0.0, sd=0.33333) elif shape == 'raisedCosine': hammingLen = 1000 # This affects the 'granularity' of the raised cos fringeProportion = fringeWidth # This one affects the proportion of # the stimulus diameter that is devoted to the raised cosine. rad = makeRadialMatrix(matrixSize, center, radius) outArray = numpy.zeros_like(rad) outArray[numpy.where(rad < 1)] = 1 raisedCosIdx = numpy.where( [numpy.logical_and(rad <= 1, rad >= 1 - fringeProportion)])[1:] # Make a raised_cos (half a hamming window): raisedCos = numpy.hamming(hammingLen)[:hammingLen//2] raisedCos -= numpy.min(raisedCos) raisedCos /= numpy.max(raisedCos) # Measure the distance from the edge - this is your index into the # hamming window: dFromEdge = numpy.abs((1 - fringeProportion) - rad[raisedCosIdx]) dFromEdge /= numpy.max(dFromEdge) dFromEdge *= numpy.round(hammingLen/2) # This is the indices into the hamming (larger for small distances # from the edge!): portion_idx = (-1 * dFromEdge).astype(int) # Apply the raised cos to this portion: outArray[raisedCosIdx] = raisedCos[portion_idx] # Sometimes there are some remaining artifacts from this process, get # rid of them: artifact_idx = numpy.where( numpy.logical_and(outArray == 0, rad < 0.99)) outArray[artifact_idx] = 1 artifact_idx = numpy.where( numpy.logical_and(outArray == 1, rad > 0.99)) outArray[artifact_idx] = 0 else: raise ValueError('Unknown value for shape argument %s' % shape) mag = range[1] - range[0] offset = range[0] return outArray * mag + offset
[docs]def makeRadialMatrix(matrixSize, center=(0.0, 0.0), radius=1.0): """Generate a square matrix where each element values is its distance from the centre of the matrix. Parameters ---------- matrixSize : int Matrix size. Corresponds to the number of elements along each dimension. Must be >0. radius: float scale factor to be applied to the mask (circle with radius of [1,1] will extend just to the edge of the matrix). Radius can be asymmetric, e.g. [1.0,2.0] will be wider than it is tall. center: 2x1 tuple or list (default=[0.0,0.0]) the centre of the mask in the matrix ([1,1] is top-right corner, [-1,-1] is bottom-left) Returns ------- ndarray Square matrix populated with distance values and `size == (matrixSize, matrixSize)`. """ if type(radius) in [int, float]: radius = [radius, radius] try: matrixSize = int(matrixSize) except ValueError: raise TypeError('parameter `matrixSize` must be a numeric type') if matrixSize <= 1: raise ValueError( 'parameter `matrixSize` must be positive and greater than 1, got: {}'.format( matrixSize)) # NB need to add one step length because yy, xx = numpy.mgrid[0:matrixSize, 0:matrixSize] xx = ((1.0 - 2.0 / matrixSize * xx) + center[0]) / radius[0] yy = ((1.0 - 2.0 / matrixSize * yy) + center[1]) / radius[1] rad = numpy.sqrt(numpy.power(xx, 2) + numpy.power(yy, 2)) return rad
[docs]def makeGauss(x, mean=0.0, sd=1.0, gain=1.0, base=0.0): """ Return the gaussian distribution for a given set of x-vals :Parameters: mean: float the centre of the distribution sd: float the width of the distribution gain: float the height of the distribution base: float an offset added to the result """ simpleGauss = numpy.exp((-numpy.power(mean - x, 2) / (2 * sd**2))) return base + gain * simpleGauss
[docs]def make2DGauss(x,y, mean=0.0, sd=1.0, gain=1.0, base=0.0): """ Return the gaussian distribution for a given set of x-vals Parameters ----------- x,y : should be x and y indexes as might be created by numpy.mgrid mean: float the centre of the distribution - may be a tuple sd: float the width of the distribution - may be a tuple gain: float the height of the distribution base: float an offset added to the result """ if numpy.size(sd)==1: sd = [sd, sd] if numpy.size(mean)==1: mean = [mean, mean] simpleGauss = numpy.exp((-numpy.power(x - mean[0], 2) / (2 * sd[0]**2))-(numpy.power(y - mean[1], 2) / (2 * sd[1]**2))) return base + gain * simpleGauss
[docs]def getRMScontrast(matrix): """Returns the RMS contrast (the sample standard deviation) of a array """ RMScontrast = numpy.std(matrix) return RMScontrast
[docs]def conv2d(smaller, larger): """Convolve a pair of 2d numpy matrices. Uses fourier transform method, so faster if larger matrix has dimensions of size 2**n Actually right now the matrices must be the same size (will sort out padding issues another day!) """ smallerFFT = fft2(smaller) largerFFT = fft2(larger) invFFT = ifft2(smallerFFT * largerFFT) return invFFT.real
[docs]def imfft(X): """Perform 2D FFT on an image and center low frequencies """ return fftshift(fft2(X))
[docs]def imifft(X): """Inverse 2D FFT with decentering """ return numpy.abs(ifft2(ifftshift(X)))
[docs]def butter2d_lp(size, cutoff, n=3): """Create lowpass 2D Butterworth filter. :Parameters: size : tuple size of the filter cutoff : float relative cutoff frequency of the filter (0 - 1.0) n : int, optional order of the filter, the higher n is the sharper the transition is. :Returns: numpy.ndarray filter kernel in 2D centered """ if not 0 < cutoff <= 1.0: raise ValueError('Cutoff frequency must be between 0 and 1.0') if not isinstance(n, int): raise ValueError('n must be an integer >= 1') rows, cols = size x = numpy.linspace(-0.5, 0.5, cols) y = numpy.linspace(-0.5, 0.5, rows) # An array with every pixel = radius relative to center radius = numpy.sqrt((x**2)[numpy.newaxis] + (y**2)[:, numpy.newaxis]) f = 1 / (1.0 + (radius/cutoff)**(2 * n)) # The filter return f
[docs]def butter2d_bp(size, cutin, cutoff, n): """Bandpass Butterworth filter in two dimensions. :Parameters: size : tuple size of the filter cutin : float relative cutin frequency of the filter (0 - 1.0) cutoff : float relative cutoff frequency of the filter (0 - 1.0) n : int, optional order of the filter, the higher n is the sharper the transition is. :Returns: numpy.ndarray filter kernel in 2D centered """ return butter2d_lp(size, cutoff, n) - butter2d_lp(size, cutin, n)
[docs]def butter2d_hp(size, cutoff, n=3): """Highpass Butterworth filter in two dimensions. :Parameters: size : tuple size of the filter cutoff : float relative cutoff frequency of the filter (0 - 1.0) n : int, optional order of the filter, the higher n is the sharper the transition is. :Returns: numpy.ndarray: filter kernel in 2D centered """ return 1.0 - butter2d_lp(size, cutoff, n)
[docs]def butter2d_lp_elliptic(size, cutoff_x, cutoff_y, n=3, alpha=0, offset_x=0, offset_y=0): """Butterworth lowpass filter of any elliptical shape. :Parameters: size : tuple size of the filter cutoff_x, cutoff_y : float, float relative cutoff frequency of the filter (0 - 1.0) for x and y axes alpha : float, optional rotation angle (in radians) offset_x, offset_y : float offsets for the ellipsoid n : int, optional order of the filter, the higher n is the sharper the transition is. :Returns: numpy.ndarray: filter kernel in 2D centered """ if not (0 < cutoff_x <= 1.0): raise ValueError('cutoff_x frequency must be between 0 and 1') if not (0 < cutoff_y <= 1.0): raise ValueError('cutoff_y frequency must be between 0 and 1') rows, cols = size # this time we start up with 2D arrays for easy broadcasting x = (numpy.linspace(-0.5, 0.5, int(cols)) - offset_x)[numpy.newaxis] y = (numpy.linspace(-0.5, 0.5, int(rows)) - offset_y)[:, numpy.newaxis] x2 = (x * numpy.cos(alpha) - y * numpy.sin(-alpha)) y2 = (x * numpy.sin(-alpha) + y * numpy.cos(alpha)) f = 1 / (1+((x2/(cutoff_x))**2+(y2/(cutoff_y))**2)**n) return f

Back to top