PageRenderTime 26ms CodeModel.GetById 21ms RepoModel.GetById 1ms app.codeStats 0ms

/psychopy/visual/filters.py

https://gitlab.com/braintech/psychopy-brain
Python | 401 lines | 375 code | 6 blank | 20 comment | 2 complexity | 67e687be9f98d264fc09d807170e0b12 MD5 | raw file
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. """Various useful functions for creating filters and textures
  4. (e.g. for PatchStim)
  5. """
  6. # Part of the PsychoPy library
  7. # Copyright (C) 2002-2018 Jonathan Peirce (C) 2019 Open Science Tools Ltd.
  8. # Distributed under the terms of the GNU General Public License (GPL).
  9. from __future__ import absolute_import, division, print_function
  10. from past.utils import old_div
  11. import numpy
  12. from numpy.fft import fft2, ifft2, fftshift, ifftshift
  13. from psychopy import logging
  14. try:
  15. from PIL import Image
  16. except ImportError:
  17. import Image
  18. def makeGrating(res,
  19. ori=0.0, # in degrees
  20. cycles=1.0,
  21. phase=0.0, # in degrees
  22. gratType="sin",
  23. contr=1.0):
  24. """Make an array containing a luminance grating of the specified params
  25. :Parameters:
  26. res: integer
  27. the size of the resulting matrix on both dimensions (e.g 256)
  28. ori: float or int (default=0.0)
  29. the orientation of the grating in degrees
  30. cycles:float or int (default=1.0)
  31. the number of grating cycles within the array
  32. phase: float or int (default=0.0)
  33. the phase of the grating in degrees (NB this differs to most
  34. PsychoPy phase arguments which use units of fraction of a cycle)
  35. gratType: 'sin', 'sqr', 'ramp' or 'sinXsin' (default="sin")
  36. the type of grating to be 'drawn'
  37. contr: float (default=1.0)
  38. contrast of the grating
  39. :Returns:
  40. a square numpy array of size resXres
  41. """
  42. # to prevent the sinusoid ever being exactly at zero (for sqr wave):
  43. tiny = 0.0000000000001
  44. ori *= (old_div(-numpy.pi, 180))
  45. phase *= (old_div(numpy.pi, 180))
  46. cyclesTwoPi = cycles * 2.0 * numpy.pi
  47. xrange, yrange = numpy.mgrid[0.0: cyclesTwoPi: old_div(cyclesTwoPi, res),
  48. 0.0: cyclesTwoPi: old_div(cyclesTwoPi, res)]
  49. sin, cos = numpy.sin, numpy.cos
  50. if gratType is "none":
  51. res = 2
  52. intensity = numpy.ones((res, res), float)
  53. elif gratType is "sin":
  54. intensity = contr * sin(xrange * sin(ori) + yrange * cos(ori) + phase)
  55. elif gratType is "ramp":
  56. intensity = contr * (xrange * cos(ori) +
  57. yrange * sin(ori)) / (2 * numpy.pi)
  58. elif gratType is "sqr": # square wave (symmetric duty cycle)
  59. intensity = numpy.where(sin(xrange * sin(ori) + yrange * cos(ori) +
  60. phase + tiny) >= 0, 1, -1)
  61. elif gratType is "sinXsin":
  62. intensity = sin(xrange) * sin(yrange)
  63. else:
  64. # might be a filename of an image
  65. try:
  66. im = Image.open(gratType)
  67. except Exception:
  68. logging.error("couldn't find tex...", gratType)
  69. return
  70. # todo: opened it, now what?
  71. return intensity
  72. def maskMatrix(matrix, shape='circle', radius=1.0, center=(0.0, 0.0)):
  73. """Make and apply a mask to an input matrix (e.g. a grating)
  74. :Parameters:
  75. matrix: a square numpy array
  76. array to which the mask should be applied
  77. shape: 'circle','gauss','ramp' (linear gradient from center)
  78. shape of the mask
  79. radius: float
  80. scale factor to be applied to the mask (circle with radius of
  81. [1,1] will extend just to the edge of the matrix). Radius can
  82. be asymmetric, e.g. [1.0,2.0] will be wider than it is tall.
  83. center: 2x1 tuple or list (default=[0.0,0.0])
  84. the centre of the mask in the matrix ([1,1] is top-right
  85. corner, [-1,-1] is bottom-left)
  86. """
  87. # NB makeMask now returns a value -1:1
  88. alphaMask = makeMask(matrix.shape[0], shape, radius,
  89. center=(0.0, 0.0), range=[0, 1])
  90. return matrix * alphaMask
  91. def makeMask(matrixSize, shape='circle', radius=1.0, center=(0.0, 0.0),
  92. range=(-1, 1), fringeWidth=0.2):
  93. """Returns a matrix to be used as an alpha mask (circle,gauss,ramp).
  94. :Parameters:
  95. matrixSize: integer
  96. the size of the resulting matrix on both dimensions (e.g 256)
  97. shape: 'circle','gauss','ramp' (linear gradient from center),
  98. 'raisedCosine' (the edges are blurred by a raised cosine)
  99. shape of the mask
  100. radius: float
  101. scale factor to be applied to the mask (circle with radius of
  102. [1,1] will extend just to the edge of the matrix). Radius can
  103. asymmetric, e.g. [1.0,2.0] will be wider than it is tall.
  104. center: 2x1 tuple or list (default=[0.0,0.0])
  105. the centre of the mask in the matrix ([1,1] is top-right
  106. corner, [-1,-1] is bottom-left)
  107. fringeWidth: float (0-1)
  108. The proportion of the raisedCosine that is being blurred.
  109. range: 2x1 tuple or list (default=[-1,1])
  110. The minimum and maximum value in the mask matrix
  111. """
  112. rad = makeRadialMatrix(matrixSize, center, radius)
  113. if shape == 'ramp':
  114. outArray = 1 - rad
  115. elif shape == 'circle':
  116. # outArray=numpy.ones(matrixSize,'f')
  117. outArray = numpy.where(numpy.greater(rad, 1.0), 0.0, 1.0)
  118. elif shape == 'gauss':
  119. outArray = makeGauss(rad, mean=0.0, sd=0.33333)
  120. elif shape == 'raisedCosine':
  121. hammingLen = 1000 # This affects the 'granularity' of the raised cos
  122. fringeProportion = fringeWidth # This one affects the proportion of
  123. # the stimulus diameter that is devoted to the raised cosine.
  124. rad = makeRadialMatrix(matrixSize, center, radius)
  125. outArray = numpy.zeros_like(rad)
  126. outArray[numpy.where(rad < 1)] = 1
  127. raisedCosIdx = numpy.where(
  128. [numpy.logical_and(rad <= 1, rad >= 1 - fringeProportion)])[1:]
  129. # Make a raised_cos (half a hamming window):
  130. raisedCos = numpy.hamming(hammingLen)[:hammingLen//2]
  131. raisedCos -= numpy.min(raisedCos)
  132. raisedCos /= numpy.max(raisedCos)
  133. # Measure the distance from the edge - this is your index into the
  134. # hamming window:
  135. dFromEdge = numpy.abs((1 - fringeProportion) - rad[raisedCosIdx])
  136. dFromEdge /= numpy.max(dFromEdge)
  137. dFromEdge *= numpy.round(hammingLen/2)
  138. # This is the indices into the hamming (larger for small distances
  139. # from the edge!):
  140. portion_idx = (-1 * dFromEdge).astype(int)
  141. # Apply the raised cos to this portion:
  142. outArray[raisedCosIdx] = raisedCos[portion_idx]
  143. # Sometimes there are some remaining artifacts from this process, get
  144. # rid of them:
  145. artifact_idx = numpy.where(
  146. numpy.logical_and(outArray == 0, rad < 0.99))
  147. outArray[artifact_idx] = 1
  148. artifact_idx = numpy.where(
  149. numpy.logical_and(outArray == 1, rad > 0.99))
  150. outArray[artifact_idx] = 0
  151. else:
  152. raise ValueError('Unknown value for shape argument %s' % shape)
  153. mag = range[1] - range[0]
  154. offset = range[0]
  155. return outArray * mag + offset
  156. def makeRadialMatrix(matrixSize, center=(0.0, 0.0), radius=1.0):
  157. """Generate a square matrix where each element val is
  158. its distance from the centre of the matrix
  159. :Parameters:
  160. matrixSize: integer
  161. the size of the resulting matrix on both dimensions (e.g 256)
  162. radius: float
  163. scale factor to be applied to the mask (circle with radius of
  164. [1,1] will extend just to the edge of the matrix). Radius can
  165. be asymmetric, e.g. [1.0,2.0] will be wider than it is tall.
  166. center: 2x1 tuple or list (default=[0.0,0.0])
  167. the centre of the mask in the matrix ([1,1] is top-right
  168. corner, [-1,-1] is bottom-left)
  169. """
  170. if type(radius) in [int, float]:
  171. radius = [radius, radius]
  172. # NB need to add one step length because
  173. yy, xx = numpy.mgrid[0:matrixSize, 0:matrixSize]
  174. xx = ((1.0 - 2.0 / matrixSize * xx) + center[0]) / radius[0]
  175. yy = ((1.0 - 2.0 / matrixSize * yy) + center[1]) / radius[1]
  176. rad = numpy.sqrt(numpy.power(xx, 2) + numpy.power(yy, 2))
  177. return rad
  178. def makeGauss(x, mean=0.0, sd=1.0, gain=1.0, base=0.0):
  179. """
  180. Return the gaussian distribution for a given set of x-vals
  181. :Parameters:
  182. mean: float
  183. the centre of the distribution
  184. sd: float
  185. the width of the distribution
  186. gain: float
  187. the height of the distribution
  188. base: float
  189. an offset added to the result
  190. """
  191. simpleGauss = numpy.exp((-numpy.power(mean - x, 2) / (2 * sd**2)))
  192. return base + gain * (simpleGauss)
  193. def make2DGauss(x,y, mean=0.0, sd=1.0, gain=1.0, base=0.0):
  194. """
  195. Return the gaussian distribution for a given set of x-vals
  196. :Parameters:
  197. x,y should be x and y indexes as might be created by numpy.mgrid
  198. mean: float
  199. the centre of the distribution - may be a tuple
  200. sd: float
  201. the width of the distribution - may be a tuple
  202. gain: float
  203. the height of the distribution
  204. base: float
  205. an offset added to the result
  206. """
  207. if numpy.size(sd)==1:
  208. sd = [sd, sd]
  209. if numpy.size(mean)==1:
  210. mean = [mean, mean]
  211. simpleGauss = numpy.exp((-numpy.power(x - mean[0], 2) / (2 * sd[0]**2))-(numpy.power(y - mean[1], 2) / (2 * sd[1]**2)))
  212. return base + gain * (simpleGauss)
  213. def getRMScontrast(matrix):
  214. """Returns the RMS contrast (the sample standard deviation) of a array
  215. """
  216. RMScontrast = numpy.std(matrix)
  217. return RMScontrast
  218. def conv2d(smaller, larger):
  219. """Convolve a pair of 2d numpy matrices.
  220. Uses fourier transform method, so faster if larger matrix
  221. has dimensions of size 2**n
  222. Actually right now the matrices must be the same size (will sort out
  223. padding issues another day!)
  224. """
  225. smallerFFT = fft2(smaller)
  226. largerFFT = fft2(larger)
  227. invFFT = ifft2(smallerFFT * largerFFT)
  228. return invFFT.real
  229. def imfft(X):
  230. """Perform 2D FFT on an image and center low frequencies
  231. """
  232. return fftshift(fft2(X))
  233. def imifft(X):
  234. """Inverse 2D FFT with decentering
  235. """
  236. return numpy.abs(ifft2(ifftshift(X)))
  237. def butter2d_lp(size, cutoff, n=3):
  238. """Create lowpass 2D Butterworth filter.
  239. :Parameters:
  240. size : tuple
  241. size of the filter
  242. cutoff : float
  243. relative cutoff frequency of the filter (0 - 1.0)
  244. n : int, optional
  245. order of the filter, the higher n is the sharper
  246. the transition is.
  247. :Returns:
  248. numpy.ndarray
  249. filter kernel in 2D centered
  250. """
  251. if not 0 < cutoff <= 1.0:
  252. raise ValueError('Cutoff frequency must be between 0 and 1.0')
  253. if not isinstance(n, int):
  254. raise ValueError('n must be an integer >= 1')
  255. rows, cols = size
  256. x = numpy.linspace(-0.5, 0.5, cols)
  257. y = numpy.linspace(-0.5, 0.5, rows)
  258. # An array with every pixel = radius relative to center
  259. radius = numpy.sqrt((x**2)[numpy.newaxis] + (y**2)[:, numpy.newaxis])
  260. f = 1 / (1.0 + (radius/cutoff)**(2 * n)) # The filter
  261. return f
  262. def butter2d_bp(size, cutin, cutoff, n):
  263. """Bandpass Butterworth filter in two dimensions.
  264. :Parameters:
  265. size : tuple
  266. size of the filter
  267. cutin : float
  268. relative cutin frequency of the filter (0 - 1.0)
  269. cutoff : float
  270. relative cutoff frequency of the filter (0 - 1.0)
  271. n : int, optional
  272. order of the filter, the higher n is the sharper
  273. the transition is.
  274. :Returns:
  275. numpy.ndarray
  276. filter kernel in 2D centered
  277. """
  278. return butter2d_lp(size, cutoff, n) - butter2d_lp(size, cutin, n)
  279. def butter2d_hp(size, cutoff, n=3):
  280. """Highpass Butterworth filter in two dimensions.
  281. :Parameters:
  282. size : tuple
  283. size of the filter
  284. cutoff : float
  285. relative cutoff frequency of the filter (0 - 1.0)
  286. n : int, optional
  287. order of the filter, the higher n is the sharper
  288. the transition is.
  289. :Returns:
  290. numpy.ndarray:
  291. filter kernel in 2D centered
  292. """
  293. return 1.0 - butter2d_lp(size, cutoff, n)
  294. def butter2d_lp_elliptic(size, cutoff_x, cutoff_y, n=3,
  295. alpha=0, offset_x=0, offset_y=0):
  296. """Butterworth lowpass filter of any elliptical shape.
  297. :Parameters:
  298. size : tuple
  299. size of the filter
  300. cutoff_x, cutoff_y : float, float
  301. relative cutoff frequency of the filter (0 - 1.0) for x and y axes
  302. alpha : float, optional
  303. rotation angle (in radians)
  304. offset_x, offset_y : float
  305. offsets for the ellipsoid
  306. n : int, optional
  307. order of the filter, the higher n is the sharper
  308. the transition is.
  309. :Returns:
  310. numpy.ndarray:
  311. filter kernel in 2D centered
  312. """
  313. if not (0 < cutoff_x <= 1.0):
  314. raise ValueError('cutoff_x frequency must be between 0 and 1')
  315. if not (0 < cutoff_y <= 1.0):
  316. raise ValueError('cutoff_y frequency must be between 0 and 1')
  317. rows, cols = size
  318. # this time we start up with 2D arrays for easy broadcasting
  319. x = (numpy.linspace(-0.5, 0.5, int(cols)) - offset_x)[numpy.newaxis]
  320. y = (numpy.linspace(-0.5, 0.5, int(rows)) - offset_y)[:, numpy.newaxis]
  321. x2 = (x * numpy.cos(alpha) - y * numpy.sin(-alpha))
  322. y2 = (x * numpy.sin(-alpha) + y * numpy.cos(alpha))
  323. f = 1 / (1+((x2/(cutoff_x))**2+(y2/(cutoff_y))**2)**n)
  324. return f