psychopy 2024.2.1__py3-none-any.whl → 2024.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of psychopy might be problematic. Click here for more details.

Files changed (276) hide show
  1. psychopy/.DS_Store +0 -0
  2. psychopy/GIT_SHA +1 -1
  3. psychopy/VERSION +1 -1
  4. psychopy/__init__.py +10 -1
  5. psychopy/__init__.py.orig +65 -0
  6. psychopy/app/{locale/ar_001/.DS_Store → .DS_Store} +0 -0
  7. psychopy/app/Resources/.DS_Store +0 -0
  8. psychopy/app/_psychopyApp.py +11 -3
  9. psychopy/app/appData.spec +1 -1
  10. psychopy/app/builder/builder.py +1 -1
  11. psychopy/app/builder/builder.py.orig +3932 -0
  12. psychopy/app/builder/dialogs/__init__.py.orig +1679 -0
  13. psychopy/app/builder/dialogs/paramCtrls.py +1 -1
  14. psychopy/app/builder/dialogs/paramCtrls.py.orig +713 -0
  15. psychopy/app/colorpicker/__init__.py.orig +411 -0
  16. psychopy/app/cortex.log +0 -0
  17. psychopy/app/jobs.py +8 -1
  18. psychopy/app/locale/ar_001/LC_MESSAGE/messages.po +2452 -1731
  19. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.mo +0 -0
  20. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN.po +6127 -0
  21. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.mo +0 -0
  22. psychopy/app/locale/zh_CN/LC_MESSAGE/zh_CN_allFlagged.po +7366 -0
  23. psychopy/app/plugin_manager/dialog.py +9 -7
  24. psychopy/app/ribbon.py +2 -1
  25. psychopy/app/runner/runner.py +7 -5
  26. psychopy/clock.py +8 -4
  27. psychopy/core.py.orig +169 -0
  28. psychopy/demos/builder/Design Templates/randomisedBlocks/html/index.html +23 -0
  29. psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks-legacy-browsers.js +423 -0
  30. psychopy/demos/builder/Design Templates/randomisedBlocks/html/randomisedBlocks.js +427 -0
  31. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/chooseBlock.xlsx +0 -0
  32. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/facesBlock.xlsx +0 -0
  33. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/housesBlock.xlsx +0 -0
  34. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face01.jpg +0 -0
  35. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face02.jpg +0 -0
  36. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/face03.jpg +0 -0
  37. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house01.jpg +0 -0
  38. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house02.jpg +0 -0
  39. psychopy/demos/builder/Design Templates/randomisedBlocks/html/resources/stims/house03.jpg +0 -0
  40. psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks.py +330 -0
  41. psychopy/demos/builder/Design Templates/randomisedBlocks/randomisedBlocks_lastrun.py +330 -0
  42. psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xml +298 -0
  43. psychopy/demos/builder/Feature Demos/eyetracking/eyetracking.xsd +120 -0
  44. psychopy/demos/builder/Tools/.DS_Store +0 -0
  45. psychopy/demos/builder/Tools/gammaCalibration/.DS_Store +0 -0
  46. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.csv +38 -0
  47. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.log +3418 -0
  48. psychopy/demos/builder/Tools/gammaCalibration/data/_gamma_correction_visual_2022-05-18_14h18.29.439.psydat +0 -0
  49. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.csv +2 -0
  50. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.log +15 -0
  51. psychopy/demos/builder/Tools/gammaCalibration/data/x1_gamma_correction_visual_2022-05-17_13h59.42.928.psydat +0 -0
  52. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.psyexp +323 -0
  53. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual.py +562 -0
  54. psychopy/demos/builder/Tools/gammaCalibration/gamma_correction_visual_lastrun.py +562 -0
  55. psychopy/demos/builder/Tools/gammaCalibration/questStairs.xlsx +0 -0
  56. psychopy/demos/builder/Tools/gammaCalibration/readme.md +0 -0
  57. psychopy/demos/builder/Tools/gammaCalibration/resources/low_contrast.png +0 -0
  58. psychopy/demos/builder/Tools/gammaCalibration/resources/make_2nd_order_tex.py +59 -0
  59. psychopy/demos/builder/Tools/gammaCalibration/resources/second_order_tex.png +0 -0
  60. psychopy/demos/coder/.DS_Store +0 -0
  61. psychopy/demos/coder/experiment control/info_gamma.pickle +0 -0
  62. psychopy/demos/coder/iohub/.iohpid +1 -0
  63. psychopy/demos/coder/iohub/eyetracking/.iohpid +1 -0
  64. psychopy/demos/coder/iohub/wintab/.DS_Store +0 -0
  65. psychopy/demos/coder/stimuli/.DS_Store +0 -0
  66. psychopy/demos/coder/stimuli/radialGratingContracting.py +29 -0
  67. psychopy/experiment/_experiment.py.orig +1032 -0
  68. psychopy/experiment/components/.DS_Store +0 -0
  69. psychopy/experiment/components/_base.py +13 -4
  70. psychopy/experiment/components/_base.py.orig +823 -0
  71. psychopy/experiment/components/form/.DS_Store +0 -0
  72. psychopy/experiment/components/microphone/__init__.py +10 -1
  73. psychopy/experiment/components/microphone/__init__.py.orig +490 -0
  74. psychopy/experiment/components/polygon/__init__.py +21 -22
  75. psychopy/experiment/components/settings/__init__.py +13 -14
  76. psychopy/experiment/components/settings/__init__.py.orig +1337 -0
  77. psychopy/experiment/components/textbox/__init__.py.orig +310 -0
  78. psychopy/experiment/components/webcam/.DS_Store +0 -0
  79. psychopy/experiment/components/webcam/light/.DS_Store +0 -0
  80. psychopy/experiment/flow.py +10 -8
  81. psychopy/experiment/loops.py.orig +829 -0
  82. psychopy/experiment/params.py +8 -3
  83. psychopy/experiment/params.py.orig +408 -0
  84. psychopy/experiment/routine.py.orig +503 -0
  85. psychopy/experiment/routines/_base.py +15 -6
  86. psychopy/experiment/routines/counterbalance/__init__.py +1 -0
  87. psychopy/gui/qtgui.py +14 -7
  88. psychopy/gui/util.py +10 -14
  89. psychopy/gui/wxgui.py +10 -4
  90. psychopy/hardware/.DS_Store +0 -0
  91. psychopy/hardware/brainproducts.py.orig +680 -0
  92. psychopy/hardware/iolab.py.orig +238 -0
  93. psychopy/hardware/manager.py +1 -1
  94. psychopy/hardware/photodiode.py +59 -27
  95. psychopy/hardware/serialport.py +51 -0
  96. psychopy/hardware/speaker.py +4 -4
  97. psychopy/iohub/datastore/__init__.py.orig +443 -0
  98. psychopy/iohub/datastore/util.py.orig +692 -0
  99. psychopy/iohub/devices/mouse/darwin.py.orig +427 -0
  100. psychopy/iohub/devices/mouse/linux2.py.orig +198 -0
  101. psychopy/preferences/.DS_Store +0 -0
  102. psychopy/projects/pavlovia.py +10 -3
  103. psychopy/projects/pavlovia.py.orig +1295 -0
  104. psychopy/sound/backend_ptb.py +22 -5
  105. psychopy/sound/transcribe.py +24 -4
  106. psychopy/tests/.DS_Store +0 -0
  107. psychopy/tests/data/.DS_Store +0 -0
  108. psychopy/tests/data/TestCircle_fill_local.png +0 -0
  109. psychopy/tests/data/__test.png +0 -0
  110. psychopy/tests/data/aperture1_normHexbackground_local.png +0 -0
  111. psychopy/tests/data/aperture1_norm_local.png +0 -0
  112. psychopy/tests/data/aperture2_normHexbackground_local.png +0 -0
  113. psychopy/tests/data/beatandrcos_height_local.png +0 -0
  114. psychopy/tests/data/beatandrcos_normAddBlend_local.png +0 -0
  115. psychopy/tests/data/beatandrcos_normHexbackground_local.png +0 -0
  116. psychopy/tests/data/beatandrcos_norm_local.png +0 -0
  117. psychopy/tests/data/beatandrcos_stencil_local.png +0 -0
  118. psychopy/tests/data/blend_add_height_local.png +0 -0
  119. psychopy/tests/data/blend_add_normAddBlend_local.png +0 -0
  120. psychopy/tests/data/blend_add_normHexbackground_local.png +0 -0
  121. psychopy/tests/data/blend_add_normNoShade_local.png +0 -0
  122. psychopy/tests/data/blend_add_norm_local.png +0 -0
  123. psychopy/tests/data/blend_add_stencil_local.png +0 -0
  124. psychopy/tests/data/bufferimg_gabor_height_local.png +0 -0
  125. psychopy/tests/data/bufferimg_gabor_normAddBlend_local.png +0 -0
  126. psychopy/tests/data/bufferimg_gabor_normHexbackground_local.png +0 -0
  127. psychopy/tests/data/bufferimg_gabor_normNoShade_local.png +0 -0
  128. psychopy/tests/data/bufferimg_gabor_norm_local.png +0 -0
  129. psychopy/tests/data/bufferimg_gabor_stencil_local.png +0 -0
  130. psychopy/tests/data/circleHex_height_local.png +0 -0
  131. psychopy/tests/data/circleHex_normAddBlend_local.png +0 -0
  132. psychopy/tests/data/circleHex_normHexbackground_local.png +0 -0
  133. psychopy/tests/data/circleHex_normNoShade_local.png +0 -0
  134. psychopy/tests/data/circleHex_norm_local.png +0 -0
  135. psychopy/tests/data/circleHex_stencil_local.png +0 -0
  136. psychopy/tests/data/color_comparison_local.png +0 -0
  137. psychopy/tests/data/corrFullRandom_local.csv +16 -0
  138. psychopy/tests/data/corrFullRandom_local.tsv +6 -0
  139. psychopy/tests/data/correctScript/.DS_Store +0 -0
  140. psychopy/tests/data/dots_height_local.png +0 -0
  141. psychopy/tests/data/dots_normAddBlend_local.png +0 -0
  142. psychopy/tests/data/dots_normHexbackground_local.png +0 -0
  143. psychopy/tests/data/dots_normNoShade_local.png +0 -0
  144. psychopy/tests/data/dots_norm_local.png +0 -0
  145. psychopy/tests/data/dots_stencil_local.png +0 -0
  146. psychopy/tests/data/elarray1_height_local.png +0 -0
  147. psychopy/tests/data/elarray1_normAddBlend_local.png +0 -0
  148. psychopy/tests/data/elarray1_normHexbackground_local.png +0 -0
  149. psychopy/tests/data/elarray1_norm_local.png +0 -0
  150. psychopy/tests/data/elarray1_stencil_local.png +0 -0
  151. psychopy/tests/data/envelopeandrcos_height_local.png +0 -0
  152. psychopy/tests/data/envelopeandrcos_normAddBlend_local.png +0 -0
  153. psychopy/tests/data/envelopeandrcos_normHexbackground_local.png +0 -0
  154. psychopy/tests/data/envelopeandrcos_norm_local.png +0 -0
  155. psychopy/tests/data/envelopeandrcos_stencil_local.png +0 -0
  156. psychopy/tests/data/envelopepowerandrcos_height_local.png +0 -0
  157. psychopy/tests/data/envelopepowerandrcos_normAddBlend_local.png +0 -0
  158. psychopy/tests/data/envelopepowerandrcos_normHexbackground_local.png +0 -0
  159. psychopy/tests/data/envelopepowerandrcos_norm_local.png +0 -0
  160. psychopy/tests/data/envelopepowerandrcos_stencil_local.png +0 -0
  161. psychopy/tests/data/gabor1_height_local.png +0 -0
  162. psychopy/tests/data/gabor1_normAddBlend_local.png +0 -0
  163. psychopy/tests/data/gabor1_normHexbackground_local.png +0 -0
  164. psychopy/tests/data/gabor1_normNoShade_local.png +0 -0
  165. psychopy/tests/data/gabor1_norm_local.png +0 -0
  166. psychopy/tests/data/gabor1_stencil_local.png +0 -0
  167. psychopy/tests/data/greyscale_normHexbackground_local.png +0 -0
  168. psychopy/tests/data/imageAndGauss_height_local.png +0 -0
  169. psychopy/tests/data/imageAndGauss_normAddBlend_local.png +0 -0
  170. psychopy/tests/data/imageAndGauss_normHexbackground_local.png +0 -0
  171. psychopy/tests/data/imageAndGauss_normNoShade_local.png +0 -0
  172. psychopy/tests/data/imageAndGauss_norm_local.png +0 -0
  173. psychopy/tests/data/imageAndGauss_stencil_local.png +0 -0
  174. psychopy/tests/data/movFrame1_stencil_local.png +0 -0
  175. psychopy/tests/data/noiseAndRcos_height_local.png +0 -0
  176. psychopy/tests/data/noiseAndRcos_normAddBlend_local.png +0 -0
  177. psychopy/tests/data/noiseAndRcos_normHexbackground_local.png +0 -0
  178. psychopy/tests/data/noiseAndRcos_normNoShade_local.png +0 -0
  179. psychopy/tests/data/noiseAndRcos_norm_local.png +0 -0
  180. psychopy/tests/data/noiseAndRcos_stencil_local.png +0 -0
  181. psychopy/tests/data/noiseFiltersAndRcos_height_local.png +0 -0
  182. psychopy/tests/data/noiseFiltersAndRcos_normAddBlend_local.png +0 -0
  183. psychopy/tests/data/noiseFiltersAndRcos_normHexbackground_local.png +0 -0
  184. psychopy/tests/data/noiseFiltersAndRcos_normNoShade_local.png +0 -0
  185. psychopy/tests/data/noiseFiltersAndRcos_norm_local.png +0 -0
  186. psychopy/tests/data/noiseFiltersAndRcos_stencil_local.png +0 -0
  187. psychopy/tests/data/numpyImage_height_local.png +0 -0
  188. psychopy/tests/data/numpyImage_normAddBlend_local.png +0 -0
  189. psychopy/tests/data/numpyImage_normHexbackground_local.png +0 -0
  190. psychopy/tests/data/numpyImage_normNoShade_local.png +0 -0
  191. psychopy/tests/data/numpyImage_norm_local.png +0 -0
  192. psychopy/tests/data/numpyImage_stencil_local.png +0 -0
  193. psychopy/tests/data/shape2_1_normAddBlend_local.png +0 -0
  194. psychopy/tests/data/shape2_1_normHexbackground_local.png +0 -0
  195. psychopy/tests/data/shape2_1_normNoShade_local.png +0 -0
  196. psychopy/tests/data/shape2_1_norm_local.png +0 -0
  197. psychopy/tests/data/shape2_1_stencil_local.png +0 -0
  198. psychopy/tests/data/testLoopsBlocks.psyexp_local.py +328 -0
  199. psychopy/tests/data/text1_height_local.png +0 -0
  200. psychopy/tests/data/text1_normAddBlend_local.png +0 -0
  201. psychopy/tests/data/text1_normHexbackground_local.png +0 -0
  202. psychopy/tests/data/text1_norm_local.png +0 -0
  203. psychopy/tests/data/text1_stencil_local.png +0 -0
  204. psychopy/tests/data/text2_height.png +0 -0
  205. psychopy/tests/data/text2_normAddBlend.png +0 -0
  206. psychopy/tests/data/text2_normHexbackground.png +0 -0
  207. psychopy/tests/data/text2_stencil.png +0 -0
  208. psychopy/tests/data/wedge1_height_local.png +0 -0
  209. psychopy/tests/data/wedge1_normAddBlend_local.png +0 -0
  210. psychopy/tests/data/wedge1_normHexbackground_local.png +0 -0
  211. psychopy/tests/data/wedge1_normNoShade_local.png +0 -0
  212. psychopy/tests/data/wedge1_norm_local.png +0 -0
  213. psychopy/tests/data/wedge1_stencil_local.png +0 -0
  214. psychopy/tests/test_app/.DS_Store +0 -0
  215. psychopy/tests/test_app/test_builder/.DS_Store +0 -0
  216. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.csv +9 -0
  217. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.log +177 -0
  218. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.psydat +0 -0
  219. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1206.xlsx +0 -0
  220. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.csv +9 -0
  221. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.log +168 -0
  222. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.psydat +0 -0
  223. psychopy/tests/test_app/test_builder/data/_2021_ 5_03_1324.xlsx +0 -0
  224. psychopy/tests/test_data/.DS_Store +0 -0
  225. psychopy/tests/test_hardware/test_CRS_BitsSharp.py.orig +68 -0
  226. psychopy/tests/test_tools/test_arraytools.py +112 -0
  227. psychopy/tests/test_visual/test_image.py.orig +219 -0
  228. psychopy/tools/arraytools.py +47 -0
  229. psychopy/tools/versionchooser.py +1 -1
  230. psychopy/visual/backends/pygletbackend.py +26 -8
  231. psychopy/visual/basevisual.py.orig +1723 -0
  232. psychopy/visual/form.py.orig +1181 -0
  233. psychopy/visual/text.py.orig +752 -0
  234. psychopy/visual/textbox2/textbox2.py.orig +1315 -0
  235. psychopy/visual/window.py +13 -5
  236. psychopy/visual/windowwarp.py.orig +463 -0
  237. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/METADATA +9 -9
  238. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/RECORD +244 -78
  239. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/WHEEL +1 -1
  240. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/entry_points.txt +2 -0
  241. psychopy/app/locale/ar_001/LC_MESSAGE/messages.mo +0 -0
  242. psychopy/app/locale/cs_CZ/LC_MESSAGE/messages.mo +0 -0
  243. psychopy/app/locale/da_DK/LC_MESSAGE/messages.mo +0 -0
  244. psychopy/app/locale/de_DE/LC_MESSAGE/messages.mo +0 -0
  245. psychopy/app/locale/el_GR/LC_MESSAGE/messages.mo +0 -0
  246. psychopy/app/locale/en_NZ/LC_MESSAGE/messages.mo +0 -0
  247. psychopy/app/locale/en_US/LC_MESSAGE/messages.mo +0 -0
  248. psychopy/app/locale/es_CO/LC_MESSAGE/messages.mo +0 -0
  249. psychopy/app/locale/es_ES/LC_MESSAGE/messages.mo +0 -0
  250. psychopy/app/locale/es_US/LC_MESSAGE/messages.mo +0 -0
  251. psychopy/app/locale/et_EE/LC_MESSAGE/messages.mo +0 -0
  252. psychopy/app/locale/fa_IR/LC_MESSAGE/messages.mo +0 -0
  253. psychopy/app/locale/fi_FI/LC_MESSAGE/messages.mo +0 -0
  254. psychopy/app/locale/fr_FR/LC_MESSAGE/messages.mo +0 -0
  255. psychopy/app/locale/he_IL/LC_MESSAGE/messages.mo +0 -0
  256. psychopy/app/locale/hi_IN/LC_MESSAGE/messages.mo +0 -0
  257. psychopy/app/locale/hu_HU/LC_MESSAGE/messages.mo +0 -0
  258. psychopy/app/locale/it_IT/LC_MESSAGE/messages.mo +0 -0
  259. psychopy/app/locale/ja_JP/LC_MESSAGE/messages.mo +0 -0
  260. psychopy/app/locale/ko_KR/LC_MESSAGE/messages.mo +0 -0
  261. psychopy/app/locale/ms_MY/LC_MESSAGE/messages.mo +0 -0
  262. psychopy/app/locale/nl_NL/LC_MESSAGE/messages.mo +0 -0
  263. psychopy/app/locale/nn_NO/LC_MESSAGE/messages.mo +0 -0
  264. psychopy/app/locale/pl_PL/LC_MESSAGE/messages.mo +0 -0
  265. psychopy/app/locale/pt_PT/LC_MESSAGE/messages.mo +0 -0
  266. psychopy/app/locale/ro_RO/LC_MESSAGE/messages.mo +0 -0
  267. psychopy/app/locale/ru_RU/LC_MESSAGE/messages.mo +0 -0
  268. psychopy/app/locale/sv_SE/LC_MESSAGE/messages.mo +0 -0
  269. psychopy/app/locale/tr_TR/LC_MESSAGE/messages.mo +0 -0
  270. psychopy/app/locale/zh_CN/LC_MESSAGE/messages.mo +0 -0
  271. psychopy/app/locale/zh_TW/LC_MESSAGE/messages.mo +0 -0
  272. psychopy-2024.2.1.dist-info/licenses/AUTHORS.md +0 -138
  273. /psychopy/{app/locale/ar_001/LC_MESSAGE → demos/builder}/.DS_Store +0 -0
  274. /psychopy/{app/locale/es_ES/LC_MESSAGE → demos/builder/Experiments}/.DS_Store +0 -0
  275. /psychopy/{visual → demos/builder/Tools/gammaCalibration/data}/.DS_Store +0 -0
  276. {psychopy-2024.2.1.dist-info → psychopy-2024.2.4.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,1723 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ """Provides class BaseVisualStim and mixins; subclass to get visual stimuli
5
+ """
6
+
7
+ # Part of the PsychoPy library
8
+ # Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2021 Open Science Tools Ltd.
9
+ # Distributed under the terms of the GNU General Public License (GPL).
10
+
11
+ from pathlib import Path
12
+ from statistics import mean
13
+ from psychopy.colors import Color, colorSpaces
14
+ from psychopy.layout import Vector, Position, Size, Vertices, unitTypes
15
+
16
+ # Ensure setting pyglet.options['debug_gl'] to False is done prior to any
17
+ # other calls to pyglet or pyglet submodules, otherwise it may not get picked
18
+ # up by the pyglet GL engine and have no effect.
19
+ # Shaders will work but require OpenGL2.0 drivers AND PyOpenGL3.0+
20
+
21
+ import pyglet
22
+ pyglet.options['debug_gl'] = False
23
+ GL = pyglet.gl
24
+
25
+ try:
26
+ from PIL import Image
27
+ except ImportError:
28
+ from . import Image
29
+
30
+ import copy
31
+ import sys
32
+ import os
33
+ from psychopy import logging
34
+
35
+ # tools must only be imported *after* event or MovieStim breaks on win32
36
+ # (JWP has no idea why!)
37
+ from psychopy.tools.arraytools import val2array
38
+ from psychopy.tools.attributetools import (attributeSetter, logAttrib,
39
+ setAttribute)
40
+ from psychopy.tools.monitorunittools import (cm2pix, deg2pix, pix2cm,
41
+ pix2deg, convertToPix)
42
+ from psychopy.visual.helpers import (pointInPolygon, polygonsOverlap,
43
+ setColor, findImageFile)
44
+ from psychopy.tools.typetools import float_uint8
45
+ from psychopy.tools.arraytools import makeRadialMatrix, createLumPattern
46
+ from psychopy.tools.colorspacetools import dkl2rgb, lms2rgb # pylint: disable=W0611
47
+
48
+ from . import globalVars
49
+
50
+ import numpy
51
+ from numpy import pi
52
+
53
+ from psychopy.constants import NOT_STARTED, STARTED, STOPPED
54
+
55
+ reportNImageResizes = 5 # permitted number of resizes
56
+
57
+ """
58
+ There are several base and mix-in visual classes for multiple inheritance:
59
+ - MinimalStim: non-visual house-keeping code common to all visual stim
60
+ RatingScale inherits only from MinimalStim.
61
+ - WindowMixin: attributes/methods about the stim relative to
62
+ a visual.Window.
63
+ - LegacyVisualMixin: deprecated visual methods (eg, setRGB) added
64
+ to BaseVisualStim
65
+ - ColorMixin: for Stim that need color methods (most, not Movie)
66
+ color-related methods and attribs
67
+ - ContainerMixin: for stim that need polygon .contains() methods.
68
+ Most need this, but not Text. .contains(), .overlaps()
69
+ - TextureMixin: for texture methods namely _createTexture
70
+ (Grating, not Text)
71
+ seems to work; caveat: There were issues in earlier (non-MI) versions
72
+ of using _createTexture so it was pulled out of classes.
73
+ Now it's inside classes again. Should be watched.
74
+ - BaseVisualStim: = Minimal + Window + Legacy. Furthermore adds c
75
+ ommon attributes like orientation, opacity, contrast etc.
76
+
77
+ Typically subclass BaseVisualStim to create new visual stim classes, and add
78
+ mixin(s) as needed to add functionality.
79
+ """
80
+
81
+
82
+ class MinimalStim:
83
+ """Non-visual methods and attributes for BaseVisualStim and RatingScale.
84
+
85
+ Includes: name, autoDraw, autoLog, status, __str__
86
+ """
87
+
88
+ def __init__(self, name=None, autoLog=None):
89
+ if name not in (None, ''):
90
+ self.__dict__['name'] = name
91
+ else:
92
+ self.__dict__['name'] = 'unnamed %s' % self.__class__.__name__
93
+ self.status = NOT_STARTED
94
+ self.autoLog = autoLog
95
+ super(MinimalStim, self).__init__()
96
+ if self.autoLog:
97
+ msg = ("%s is calling MinimalStim.__init__() with autolog=True. "
98
+ "Set autoLog to True only at the end of __init__())")
99
+ logging.warning(msg % self.__class__.__name__)
100
+
101
+ def __str__(self, complete=False):
102
+ """
103
+ """
104
+ if hasattr(self, '_initParams'):
105
+ className = self.__class__.__name__
106
+ paramStrings = []
107
+ for param in self._initParams:
108
+ if hasattr(self, param):
109
+ val = getattr(self, param)
110
+ valStr = repr(getattr(self, param))
111
+ if len(repr(valStr)) > 50 and not complete:
112
+ if val.__class__.__name__ == 'attributeSetter':
113
+ _name = val.__getattribute__.__class__.__name__
114
+ else:
115
+ _name = val.__class__.__name__
116
+ valStr = "%s(...)" % _name
117
+ else:
118
+ valStr = 'UNKNOWN'
119
+ paramStrings.append("%s=%s" % (param, valStr))
120
+ # this could be used if all params are known to exist:
121
+ # paramStrings = ["%s=%s" %(param, getattr(self, param))
122
+ # for param in self._initParams]
123
+ params = ", ".join(paramStrings)
124
+ s = "%s(%s)" % (className, params)
125
+ else:
126
+ s = object.__repr__(self)
127
+ return s
128
+
129
+ # Might seem simple at first, but this ensures that "name" attribute
130
+ # appears in docs and that name setting and updating is logged.
131
+ @attributeSetter
132
+ def name(self, value):
133
+ """The name (`str`) of the object to be using during logged messages
134
+ about this stim. If you have multiple stimuli in your experiment this
135
+ really helps to make sense of log files!
136
+
137
+ If name = None your stimulus will be called "unnamed <type>", e.g.
138
+ visual.TextStim(win) will be called "unnamed TextStim" in the logs.
139
+ """
140
+ self.__dict__['name'] = value
141
+
142
+ @attributeSetter
143
+ def autoDraw(self, value):
144
+ """Determines whether the stimulus should be automatically drawn
145
+ on every frame flip.
146
+
147
+ Value should be: `True` or `False`. You do NOT need to set this
148
+ on every frame flip!
149
+ """
150
+ self.__dict__['autoDraw'] = value
151
+ toDraw = self.win._toDraw
152
+ toDrawDepths = self.win._toDrawDepths
153
+ beingDrawn = (self in toDraw)
154
+ if value == beingDrawn:
155
+ return # nothing to do
156
+ elif value:
157
+ # work out where to insert the object in the autodraw list
158
+ depthArray = numpy.array(toDrawDepths)
159
+ # all indices where true:
160
+ iis = numpy.where(depthArray < self.depth)[0]
161
+ if len(iis): # we featured somewhere before the end of the list
162
+ toDraw.insert(iis[0], self)
163
+ toDrawDepths.insert(iis[0], self.depth)
164
+ else:
165
+ toDraw.append(self)
166
+ toDrawDepths.append(self.depth)
167
+ # Add to editable list (if needed)
168
+ self.win.addEditable(self)
169
+ # Mark as started
170
+ self.status = STARTED
171
+ elif value == False:
172
+ # remove from autodraw lists
173
+ toDrawDepths.pop(toDraw.index(self)) # remove from depths
174
+ toDraw.remove(self) # remove from draw list
175
+ # Remove from editable list (if needed)
176
+ self.win.removeEditable(self)
177
+ # Mark as stopped
178
+ self.status = STOPPED
179
+
180
+ def setAutoDraw(self, value, log=None):
181
+ """Sets autoDraw. Usually you can use 'stim.attribute = value'
182
+ syntax instead, but use this method to suppress the log message.
183
+ """
184
+ setAttribute(self, 'autoDraw', value, log)
185
+
186
+ @attributeSetter
187
+ def autoLog(self, value):
188
+ """Whether every change in this stimulus should be auto logged.
189
+
190
+ Value should be: `True` or `False`. Set to `False` if your stimulus is
191
+ updating frequently (e.g. updating its position every frame) and you
192
+ want to avoid swamping the log file with messages that aren't likely to
193
+ be useful.
194
+ """
195
+ self.__dict__['autoLog'] = value
196
+
197
+ def setAutoLog(self, value=True, log=None):
198
+ """Usually you can use 'stim.attribute = value' syntax instead,
199
+ but use this method if you need to suppress the log message.
200
+ """
201
+ setAttribute(self, 'autoLog', value, log)
202
+
203
+
204
+ class LegacyVisualMixin:
205
+ """Class to hold deprecated visual methods and attributes.
206
+
207
+ Intended only for use as a mixin class for BaseVisualStim, to maintain
208
+ backwards compatibility while reducing clutter in class BaseVisualStim.
209
+ """
210
+ # def __init__(self):
211
+ # super(LegacyVisualMixin, self).__init__()
212
+
213
+ def _calcSizeRendered(self):
214
+ """DEPRECATED in 1.80.00. This functionality is now handled
215
+ by _updateVertices() and verticesPix
216
+ """
217
+ # raise DeprecationWarning, "_calcSizeRendered() was deprecated in
218
+ # 1.80.00. This functionality is now handled by _updateVertices()
219
+ # and verticesPix"
220
+ if self.units in ['norm', 'pix', 'height']:
221
+ self._sizeRendered = copy.copy(self.size)
222
+ elif self.units in ['deg', 'degs']:
223
+ self._sizeRendered = deg2pix(self.size, self.win.monitor)
224
+ elif self.units == 'cm':
225
+ self._sizeRendered = cm2pix(self.size, self.win.monitor)
226
+ else:
227
+ logging.error("Stimulus units should be 'height', 'norm', "
228
+ "'deg', 'cm' or 'pix', not '%s'" % self.units)
229
+
230
+ def _calcPosRendered(self):
231
+ """DEPRECATED in 1.80.00. This functionality is now handled
232
+ by _updateVertices() and verticesPix.
233
+ """
234
+ # raise DeprecationWarning, "_calcSizeRendered() was deprecated
235
+ # in 1.80.00. This functionality is now handled by
236
+ # _updateVertices() and verticesPix"
237
+ if self.units in ['norm', 'pix', 'height']:
238
+ self._posRendered = copy.copy(self.pos)
239
+ elif self.units in ['deg', 'degs']:
240
+ self._posRendered = deg2pix(self.pos, self.win.monitor)
241
+ elif self.units == 'cm':
242
+ self._posRendered = cm2pix(self.pos, self.win.monitor)
243
+
244
+ def _getPolyAsRendered(self):
245
+ """DEPRECATED. Return a list of vertices as rendered.
246
+ """
247
+ oriRadians = numpy.radians(self.ori)
248
+ sinOri = numpy.sin(-oriRadians)
249
+ cosOri = numpy.cos(-oriRadians)
250
+ x = (self._verticesRendered[:, 0] * cosOri -
251
+ self._verticesRendered[:, 1] * sinOri)
252
+ y = (self._verticesRendered[:, 0] * sinOri +
253
+ self._verticesRendered[:, 1] * cosOri)
254
+ return numpy.column_stack((x, y)) + self._posRendered
255
+
256
+ @attributeSetter
257
+ def depth(self, value):
258
+ """DEPRECATED, depth is now controlled simply by drawing order.
259
+ """
260
+ self.__dict__['depth'] = value
261
+
262
+
263
+ <<<<<<< HEAD
264
+ class LegacyColorMixin:
265
+ =======
266
+ class LegacyForeColorMixin:
267
+ """
268
+ Mixin class to give an object all of the legacy functions for setting foreground color
269
+ """
270
+ >>>>>>> release
271
+ def setDKL(self, color, operation=''):
272
+ """DEPRECATED since v1.60.05: Please use the `color` attribute
273
+ """
274
+ self.setForeColor(color, 'dkl', operation)
275
+
276
+ def setLMS(self, color, operation=''):
277
+ """DEPRECATED since v1.60.05: Please use the `color` attribute
278
+ """
279
+ self.setForeColor(color, 'lms', operation)
280
+
281
+ @property
282
+ def foreRGB(self):
283
+ """
284
+ DEPRECATED: Legacy property for setting the foreground color of a stimulus in RGB, instead use `obj._foreColor.rgb`
285
+ """
286
+ return self._foreColor.rgb
287
+
288
+ @foreRGB.setter
289
+ def foreRGB(self, value):
290
+ self.foreColor = Color(value, 'rgb')
291
+
292
+ @property
293
+ def RGB(self):
294
+ """
295
+ DEPRECATED: Legacy property for setting the foreground color of a stimulus in RGB, instead use `obj._foreColor.rgb`
296
+ """
297
+ return self.foreRGB
298
+
299
+ @RGB.setter
300
+ def RGB(self, value):
301
+ self.foreRGB = value
302
+
303
+ def setRGB(self, color, operation='', log=None):
304
+ """
305
+ DEPRECATED: Legacy setter for foreground RGB, instead set `obj._foreColor.rgb`
306
+ """
307
+ self.setForeColor(color, 'rgb', operation, log)
308
+
309
+ def setForeRGB(self, color, operation='', log=None):
310
+ """
311
+ DEPRECATED: Legacy setter for foreground RGB, instead set `obj._foreColor.rgb`
312
+ """
313
+ self.setForeColor(color, 'rgb', operation, log)
314
+
315
+ @property
316
+ def foreColorSpace(self):
317
+ """Deprecated, please use colorSpace to set color space for the entire
318
+ object.
319
+ """
320
+ return self.colorSpace
321
+
322
+ @foreColorSpace.setter
323
+ def foreColorSpace(self, value):
324
+ logging.warning(
325
+ "Setting color space by attribute rather than by object is deprecated. Value of foreColorSpace has been assigned to colorSpace.")
326
+ self.colorSpace = value
327
+
328
+
329
+ class LegacyFillColorMixin:
330
+ """
331
+ Mixin class to give an object all of the legacy functions for setting fill color
332
+ """
333
+ @property
334
+ def fillRGB(self):
335
+ """
336
+ DEPRECATED: Legacy property for setting the fill color of a stimulus in RGB, instead use `obj._fillColor.rgb`
337
+ """
338
+ return self._fillColor.rgb
339
+
340
+ @fillRGB.setter
341
+ def fillRGB(self, value):
342
+ self.fillColor = Color(value, 'rgb')
343
+
344
+ @property
345
+ def backRGB(self):
346
+ """
347
+ DEPRECATED: Legacy property for setting the fill color of a stimulus in RGB, instead use `obj._fillColor.rgb`
348
+ """
349
+ return self.fillRGB
350
+
351
+ @backRGB.setter
352
+ def backRGB(self, value):
353
+ self.fillRGB = value
354
+
355
+ def setFillRGB(self, color, operation='', log=None):
356
+ """
357
+ DEPRECATED: Legacy setter for fill RGB, instead set `obj._fillColor.rgb`
358
+ """
359
+ self.setFillColor(color, 'rgb', operation, log)
360
+
361
+ def setBackRGB(self, color, operation='', log=None):
362
+ """
363
+ DEPRECATED: Legacy setter for fill RGB, instead set `obj._fillColor.rgb`
364
+ """
365
+ self.setFillColor(color, 'rgb', operation, log)
366
+
367
+ @property
368
+ def fillColorSpace(self):
369
+ """Deprecated, please use colorSpace to set color space for the entire
370
+ object.
371
+ """
372
+ return self.colorSpace
373
+
374
+ @fillColorSpace.setter
375
+ def fillColorSpace(self, value):
376
+ logging.warning("Setting color space by attribute rather than by object is deprecated. Value of fillColorSpace has been assigned to colorSpace.")
377
+ self.colorSpace = value
378
+
379
+ @property
380
+ def backColorSpace(self):
381
+ """Deprecated, please use colorSpace to set color space for the entire
382
+ object.
383
+ """
384
+ return self.colorSpace
385
+
386
+ @backColorSpace.setter
387
+ def backColorSpace(self, value):
388
+ logging.warning(
389
+ "Setting color space by attribute rather than by object is deprecated. Value of backColorSpace has been assigned to colorSpace.")
390
+ self.colorSpace = value
391
+
392
+
393
+ class LegacyBorderColorMixin:
394
+ """
395
+ Mixin class to give an object all of the legacy functions for setting border color
396
+ """
397
+ @property
398
+ def borderRGB(self):
399
+ """
400
+ DEPRECATED: Legacy property for setting the border color of a stimulus in RGB, instead use `obj._borderColor.rgb`
401
+ """
402
+ return self._borderColor.rgb
403
+
404
+ @borderRGB.setter
405
+ def borderRGB(self, value):
406
+ self.borderColor = Color(value, 'rgb')
407
+
408
+ @property
409
+ def lineRGB(self):
410
+ """
411
+ DEPRECATED: Legacy property for setting the border color of a stimulus in RGB, instead use `obj._borderColor.rgb`
412
+ """
413
+ return self.borderRGB
414
+
415
+ @lineRGB.setter
416
+ def lineRGB(self, value):
417
+ self.borderRGB = value
418
+
419
+ def setBorderRGB(self, color, operation='', log=None):
420
+ """
421
+ DEPRECATED: Legacy setter for border RGB, instead set `obj._borderColor.rgb`
422
+ """
423
+ self.setBorderColor(color, 'rgb', operation, log)
424
+
425
+ def setLineRGB(self, color, operation='', log=None):
426
+ """
427
+ DEPRECATED: Legacy setter for border RGB, instead set `obj._borderColor.rgb`
428
+ """
429
+ self.setBorderColor(color, 'rgb', operation, log)
430
+
431
+ @property
432
+ def borderColorSpace(self):
433
+ """Deprecated, please use colorSpace to set color space for the entire
434
+ object
435
+ """
436
+ return self.colorSpace
437
+
438
+ @borderColorSpace.setter
439
+ def borderColorSpace(self, value):
440
+ logging.warning(
441
+ "Setting color space by attribute rather than by object is deprecated. Value of borderColorSpace has been assigned to colorSpace.")
442
+ self.colorSpace = value
443
+
444
+ @property
445
+ def lineColorSpace(self):
446
+ """Deprecated, please use colorSpace to set color space for the entire
447
+ object
448
+ """
449
+ return self.colorSpace
450
+
451
+ @lineColorSpace.setter
452
+ def lineColorSpace(self, value):
453
+ logging.warning(
454
+ "Setting color space by attribute rather than by object is deprecated. Value of lineColorSpace has been assigned to colorSpace.")
455
+ self.colorSpace = value
456
+
457
+
458
+ class LegacyColorMixin(LegacyForeColorMixin, LegacyFillColorMixin, LegacyBorderColorMixin):
459
+ """
460
+ Mixin class to give an object all of the legacy functions for setting all colors (fore, fill and border
461
+ """
462
+
463
+
464
+ class BaseColorMixin:
465
+ """
466
+ Mixin class giving base color methods (e.g. colorSpace) which are needed for any color stuff.
467
+ """
468
+ @property
469
+ def colorSpace(self):
470
+ """The name of the color space currently being used
471
+
472
+ Value should be: a string or None
473
+
474
+ For strings and hex values this is not needed.
475
+ If None the default colorSpace for the stimulus is
476
+ used (defined during initialisation).
477
+
478
+ Please note that changing colorSpace does not change stimulus
479
+ parameters. Thus you usually want to specify colorSpace before
480
+ setting the color. Example::
481
+
482
+ # A light green text
483
+ stim = visual.TextStim(win, 'Color me!',
484
+ color=(0, 1, 0), colorSpace='rgb')
485
+
486
+ # An almost-black text
487
+ stim.colorSpace = 'rgb255'
488
+
489
+ # Make it light green again
490
+ stim.color = (128, 255, 128)
491
+ """
492
+ if hasattr(self, '_colorSpace'):
493
+ return self._colorSpace
494
+ else:
495
+ return 'rgba'
496
+
497
+ @colorSpace.setter
498
+ def colorSpace(self, value):
499
+ if value in colorSpaces:
500
+ self._colorSpace = value
501
+ else:
502
+ logging.error(f"'{value}' is not a valid color space")
503
+
504
+ @property
505
+ def contrast(self):
506
+ """A value that is simply multiplied by the color.
507
+
508
+ Value should be: a float between -1 (negative) and 1 (unchanged).
509
+ :ref:`Operations <attrib-operations>` supported.
510
+
511
+ Set the contrast of the stimulus, i.e. scales how far the stimulus
512
+ deviates from the middle grey. You can also use the stimulus
513
+ `opacity` to control contrast, but that cannot be negative.
514
+
515
+ Examples::
516
+
517
+ stim.contrast = 1.0 # unchanged contrast
518
+ stim.contrast = 0.5 # decrease contrast
519
+ stim.contrast = 0.0 # uniform, no contrast
520
+ stim.contrast = -0.5 # slightly inverted
521
+ stim.contrast = -1.0 # totally inverted
522
+
523
+ Setting contrast outside range -1 to 1 is permitted, but may
524
+ produce strange results if color values exceeds the monitor limits.::
525
+
526
+ stim.contrast = 1.2 # increases contrast
527
+ stim.contrast = -1.2 # inverts with increased contrast
528
+
529
+ """
530
+ if hasattr(self, '_foreColor'):
531
+ return self._foreColor.contrast
532
+
533
+ @contrast.setter
534
+ def contrast(self, value):
535
+ if hasattr(self, '_foreColor'):
536
+ self._foreColor.contrast = value
537
+ if hasattr(self, '_fillColor'):
538
+ self._fillColor.contrast = value
539
+ if hasattr(self, '_borderColor'):
540
+ self._borderColor.contrast = value
541
+
542
+ def setContrast(self, newContrast, operation='', log=None):
543
+ """Usually you can use 'stim.attribute = value' syntax instead,
544
+ but use this method if you need to suppress the log message
545
+ """
546
+ if newContrast is not None:
547
+ self.contrast = newContrast
548
+ if operation in ['', '=']:
549
+ self.contrast = newContrast
550
+ elif operation in ['+']:
551
+ self.contrast += newContrast
552
+ elif operation in ['-']:
553
+ self.contrast -= newContrast
554
+ else:
555
+ logging.error(f"Operation '{operation}' not recognised.")
556
+
557
+ def _getDesiredRGB(self, rgb, colorSpace, contrast):
558
+ """ Convert color to RGB while adding contrast.
559
+ Requires self.rgb, self.colorSpace and self.contrast
560
+ """
561
+ col = Color(rgb, colorSpace)
562
+ col.contrast *= contrast or 0
563
+ return col.render('rgb')
564
+
565
+ def updateColors(self):
566
+ """Placeholder method to update colours when set externally, for example updating the `pallette` attribute of
567
+ a textbox"""
568
+ return
569
+
570
+
571
+ class ForeColorMixin(BaseColorMixin, LegacyForeColorMixin):
572
+ """
573
+ Mixin class for visual stim that need fore color.
574
+ """
575
+ @property
576
+ def foreColor(self):
577
+ """Foreground color of the stimulus
578
+
579
+ Value should be one of:
580
+ + string: to specify a :ref:`colorNames`. Any of the standard
581
+ html/X11 `color names
582
+ <http://www.w3schools.com/html/html_colornames.asp>`
583
+ can be used.
584
+ + :ref:`hexColors`
585
+ + numerically: (scalar or triplet) for DKL, RGB or
586
+ other :ref:`colorspaces`. For
587
+ these, :ref:`operations <attrib-operations>` are supported.
588
+
589
+ When color is specified using numbers, it is interpreted with
590
+ respect to the stimulus' current colorSpace. If color is given as a
591
+ single value (scalar) then this will be applied to all 3 channels.
592
+
593
+ Examples
594
+ --------
595
+ For whatever stim you have::
596
+
597
+ stim.color = 'white'
598
+ stim.color = 'RoyalBlue' # (the case is actually ignored)
599
+ stim.color = '#DDA0DD' # DDA0DD is hexadecimal for plum
600
+ stim.color = [1.0, -1.0, -1.0] # if stim.colorSpace='rgb':
601
+ # a red color in rgb space
602
+ stim.color = [0.0, 45.0, 1.0] # if stim.colorSpace='dkl':
603
+ # DKL space with elev=0, azimuth=45
604
+ stim.color = [0, 0, 255] # if stim.colorSpace='rgb255':
605
+ # a blue stimulus using rgb255 space
606
+ stim.color = 255 # interpreted as (255, 255, 255)
607
+ # which is white in rgb255.
608
+
609
+
610
+ :ref:`Operations <attrib-operations>` work as normal for all numeric
611
+ colorSpaces (e.g. 'rgb', 'hsv' and 'rgb255') but not for strings, like
612
+ named and hex. For example, assuming that colorSpace='rgb'::
613
+
614
+ stim.color += [1, 1, 1] # increment all guns by 1 value
615
+ stim.color *= -1 # multiply the color by -1 (which in this
616
+ # space inverts the contrast)
617
+ stim.color *= [0.5, 0, 1] # decrease red, remove green, keep blue
618
+
619
+ You can use `setColor` if you want to set color and colorSpace in one
620
+ line. These two are equivalent::
621
+
622
+ stim.setColor((0, 128, 255), 'rgb255')
623
+ # ... is equivalent to
624
+ stim.colorSpace = 'rgb255'
625
+ stim.color = (0, 128, 255)
626
+ """
627
+ if hasattr(self, '_foreColor'):
628
+ return self._foreColor.render(self.colorSpace)
629
+
630
+ @foreColor.setter
631
+ def foreColor(self, value):
632
+ if isinstance(value, Color):
633
+ # If supplied with a Color object, set as that
634
+ self._foreColor = value
635
+ else:
636
+ # Otherwise, make a new Color object
637
+ self._foreColor = Color(value, self.colorSpace, contrast=self.contrast)
638
+ if not self._foreColor:
639
+ self._foreColor = Color()
640
+ logging.error(f"'{value}' is not a valid {self.colorSpace} color")
641
+
642
+ @property
643
+ def color(self):
644
+ """Alternative way of setting `foreColor`."""
645
+ return self.foreColor
646
+
647
+ @color.setter
648
+ def color(self, value):
649
+ self.foreColor = value
650
+
651
+ def setForeColor(self, color, colorSpace=None, operation='', log=None):
652
+ """Hard setter for foreColor, allows suppression of the log message,
653
+ simultaneous colorSpace setting and calls update methods.
654
+ """
655
+ setColor(obj=self, colorAttrib="foreColor", color=color, colorSpace=colorSpace or self.colorSpace, operation=operation)
656
+ # Trigger color update for components like Textbox which have different behaviours for a hard setter
657
+ self.updateColors()
658
+
659
+ def setColor(self, color, colorSpace=None, operation='', log=None):
660
+ self.setForeColor(color, colorSpace=colorSpace, operation=operation, log=log)
661
+
662
+
663
+ class FillColorMixin(BaseColorMixin, LegacyFillColorMixin):
664
+ """
665
+ Mixin class for visual stim that need fill color.
666
+ """
667
+
668
+ @property
669
+ def fillColor(self):
670
+ """Set the fill color for the shape."""
671
+ if hasattr(self, '_fillColor'):
672
+ return getattr(self._fillColor, self.colorSpace) # return self._fillColor.render(self.colorSpace)
673
+
674
+ @fillColor.setter
675
+ def fillColor(self, value):
676
+ if isinstance(value, Color):
677
+ # If supplied with a color object, set as that
678
+ self._fillColor = value
679
+ else:
680
+ # Otherwise, make a new Color object
681
+ self._fillColor = Color(value, self.colorSpace, contrast=self.contrast)
682
+ if not self._fillColor:
683
+ # If given an invalid color, set as transparent and log error
684
+ self._fillColor = Color()
685
+ logging.error(f"'{value}' is not a valid {self.colorSpace} color")
686
+
687
+ @property
688
+ def backColor(self):
689
+ """Alternative way of setting fillColor"""
690
+ return self.fillColor
691
+
692
+ @backColor.setter
693
+ def backColor(self, value):
694
+ self.fillColor = value
695
+
696
+ def setFillColor(self, color, colorSpace=None, operation='', log=None):
697
+ """Hard setter for fillColor, allows suppression of the log message,
698
+ simultaneous colorSpace setting and calls update methods.
699
+ """
700
+ setColor(obj=self, colorAttrib="fillColor", color=color, colorSpace=colorSpace or self.colorSpace, operation=operation)
701
+ # Trigger color update for components like Textbox which have different behaviours for a hard setter
702
+ self.updateColors()
703
+
704
+ def setBackColor(self, color, colorSpace=None, operation='', log=None):
705
+ self.setFillColor(color, colorSpace=None, operation='', log=None)
706
+
707
+
708
+ class BorderColorMixin(BaseColorMixin, LegacyBorderColorMixin):
709
+ @property
710
+ def borderColor(self):
711
+ if hasattr(self, '_borderColor'):
712
+ return self._borderColor.render(self.colorSpace)
713
+
714
+ @borderColor.setter
715
+ def borderColor(self, value):
716
+ if isinstance(value, Color):
717
+ # If supplied with a color object, set as that
718
+ self._borderColor = value
719
+ else:
720
+ # If supplied with a valid color, use it to make a color object
721
+ self._borderColor = Color(value, self.colorSpace, contrast=self.contrast)
722
+ if not self._borderColor:
723
+ # If given an invalid color, set as transparent and log error
724
+ self._borderColor = Color()
725
+ logging.error(f"'{value}' is not a valid {self.colorSpace} color")
726
+
727
+ @property
728
+ def lineColor(self):
729
+ """Alternative way of setting `borderColor`."""
730
+ return self.borderColor
731
+
732
+ @lineColor.setter
733
+ def lineColor(self, value):
734
+ self.borderColor = value
735
+
736
+ def setBorderColor(self, color, colorSpace=None, operation='', log=None):
737
+ """Hard setter for `fillColor`, allows suppression of the log message,
738
+ simultaneous colorSpace setting and calls update methods.
739
+ """
740
+ setColor(obj=self, colorAttrib="borderColor", color=color, colorSpace=colorSpace or self.colorSpace, operation=operation)
741
+ # Trigger color update for components like Textbox which have different behaviours for a hard setter
742
+ self.updateColors()
743
+
744
+ def setLineColor(self, color, colorSpace=None, operation='', log=None):
745
+ self.setBorderColor(color, colorSpace=None, operation='', log=None)
746
+
747
+
748
+ class ColorMixin(ForeColorMixin, FillColorMixin, BorderColorMixin):
749
+ """
750
+ Mixin class for visual stim that need fill, fore and border color.
751
+ """
752
+
753
+
754
+ class ContainerMixin:
755
+ """Mixin class for visual stim that have verticesPix attrib
756
+ and .contains() methods.
757
+ """
758
+
759
+ def __init__(self):
760
+ super(ContainerMixin, self).__init__()
761
+ self._verticesBase = numpy.array(
762
+ [[0.5, -0.5], [-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5]]) # sqr
763
+ self._borderBase = numpy.array(
764
+ [[0.5, -0.5], [-0.5, -0.5], [-0.5, 0.5], [0.5, 0.5]]) # sqr
765
+ self._rotationMatrix = [[1., 0.], [0., 1.]] # no rotation by default
766
+
767
+ @property
768
+ def verticesPix(self):
769
+ """This determines the coordinates of the vertices for the
770
+ current stimulus in pixels, accounting for size, ori, pos and units
771
+ """
772
+ # because this is a property getter we can check /on-access/ if it
773
+ # needs updating :-)
774
+ if self._needVertexUpdate:
775
+ self._updateVertices()
776
+ return self.__dict__['verticesPix']
777
+
778
+ @property
779
+ def _borderPix(self):
780
+ """Allows for a dynamic border that differs from self.vertices, gets
781
+ updated dynamically with identical transformations.
782
+ """
783
+ if not hasattr(self, 'border'):
784
+ msg = "%s._borderPix requested without .border" % self.name
785
+ logging.error(msg)
786
+ raise AttributeError(msg)
787
+ if self._needVertexUpdate:
788
+ self._updateVertices()
789
+ return self.__dict__['_borderPix']
790
+
791
+ def _updateVertices(self):
792
+ """Sets Stim.verticesPix and ._borderPix from pos, size, ori,
793
+ flipVert, flipHoriz
794
+ """
795
+
796
+ verts = numpy.dot(self.vertices, self._rotationMatrix)
797
+ # If needed, sub in missing values for flip and anchor
798
+ flip = None
799
+ if hasattr(self, "flip"):
800
+ flip = self.flip
801
+ anchor = None
802
+ if hasattr(self, "anchor"):
803
+ anchor = self.anchor
804
+ # Convert to a vertices object if not already
805
+ verts = Vertices(verts, obj=self, flip=flip, anchor=anchor).pix
806
+ self.__dict__['verticesPix'] = self.__dict__['_borderPix'] = verts
807
+
808
+ if hasattr(self, '_tesselVertices'): # Shapes need to render from this
809
+ tesselVerts = self._tesselVertices
810
+ tesselVerts = numpy.dot(tesselVerts, self._rotationMatrix)
811
+ # Convert to a vertices object if not already
812
+ tesselVerts = Vertices(tesselVerts, obj=self, flip=self.flip, anchor=self.anchor).pix
813
+ self.__dict__['verticesPix'] = tesselVerts
814
+
815
+ self._needVertexUpdate = False
816
+ self._needUpdate = True # but we presumably need to update the list
817
+
818
+ def contains(self, x, y=None, units=None):
819
+ """Returns True if a point x,y is inside the stimulus' border.
820
+
821
+ Can accept variety of input options:
822
+ + two separate args, x and y
823
+ + one arg (list, tuple or array) containing two vals (x,y)
824
+ + an object with a getPos() method that returns x,y, such
825
+ as a :class:`~psychopy.event.Mouse`.
826
+
827
+ Returns `True` if the point is within the area defined either by its
828
+ `border` attribute (if one defined), or its `vertices` attribute if
829
+ there is no .border. This method handles
830
+ complex shapes, including concavities and self-crossings.
831
+
832
+ Note that, if your stimulus uses a mask (such as a Gaussian) then
833
+ this is not accounted for by the `contains` method; the extent of the
834
+ stimulus is determined purely by the size, position (pos), and
835
+ orientation (ori) settings (and by the vertices for shape stimuli).
836
+
837
+ See Coder demos: shapeContains.py
838
+ See Coder demos: shapeContains.py
839
+ """
840
+ # get the object in pixels
841
+ if hasattr(x, 'border'):
842
+ xy = x._borderPix # access only once - this is a property
843
+ units = 'pix' # we can forget about the units
844
+ elif hasattr(x, 'verticesPix'):
845
+ # access only once - this is a property (slower to access)
846
+ xy = x.verticesPix
847
+ units = 'pix' # we can forget about the units
848
+ elif hasattr(x, 'getPos'):
849
+ xy = x.getPos()
850
+ units = x.units
851
+ elif type(x) in [list, tuple, numpy.ndarray]:
852
+ xy = numpy.array(x)
853
+ else:
854
+ xy = numpy.array((x, y))
855
+ # try to work out what units x,y has
856
+ if units is None:
857
+ if hasattr(xy, 'units'):
858
+ units = xy.units
859
+ else:
860
+ units = self.units
861
+ if units != 'pix':
862
+ xy = convertToPix(xy, pos=(0, 0), units=units, win=self.win)
863
+ # ourself in pixels
864
+ if hasattr(self, 'border'):
865
+ poly = self._borderPix # e.g., outline vertices
866
+ elif hasattr(self, 'boundingBox'):
867
+ if abs(self.ori) > 0.1:
868
+ raise RuntimeError("TextStim.contains() doesn't currently "
869
+ "support rotated text.")
870
+ w, h = self.boundingBox # e.g., outline vertices
871
+ x, y = self.posPix
872
+ poly = numpy.array([[x+w/2, y-h/2], [x-w/2, y-h/2],
873
+ [x-w/2, y+h/2], [x+w/2, y+h/2]])
874
+ else:
875
+ poly = self.verticesPix # e.g., tessellated vertices
876
+
877
+ return pointInPolygon(xy[0], xy[1], poly=poly)
878
+
879
+ def overlaps(self, polygon):
880
+ """Returns `True` if this stimulus intersects another one.
881
+
882
+ If `polygon` is another stimulus instance, then the vertices
883
+ and location of that stimulus will be used as the polygon.
884
+ Overlap detection is typically very good, but it
885
+ can fail with very pointy shapes in a crossed-swords configuration.
886
+
887
+ Note that, if your stimulus uses a mask (such as a Gaussian blob)
888
+ then this is not accounted for by the `overlaps` method; the extent
889
+ of the stimulus is determined purely by the size, pos, and
890
+ orientation settings (and by the vertices for shape stimuli).
891
+
892
+ See coder demo, shapeContains.py
893
+ """
894
+ return polygonsOverlap(self, polygon)
895
+
896
+
897
+ class TextureMixin:
898
+ """Mixin class for visual stim that have textures.
899
+
900
+ Could move visual.helpers.setTexIfNoShaders() into here.
901
+
902
+ Parameters
903
+ ----------
904
+ tex : Any
905
+ Texture data. Value can be anything that resembles image data.
906
+ id : int or :class:`~pyglet.gl.GLint`
907
+ Texture ID.
908
+ pixFormat : :class:`~pyglet.gl.GLenum` or int
909
+ Pixel format to use, values can be `GL_ALPHA` or `GL_RGB`.
910
+ stim : Any
911
+ Stimulus object using the texture.
912
+ res : int
913
+ The resolution of the texture (unless a bitmap image is used).
914
+ maskParams : dict or None
915
+ Additional parameters to configure the mask used with this texture.
916
+ forcePOW2 : bool
917
+ Force the texture to be stored in a square memory area. For grating
918
+ stimuli (anything that needs multiple cycles) `forcePOW2` should be
919
+ set to be `True`. Otherwise the wrapping of the texture will not
920
+ work.
921
+ dataType : class:`~pyglet.gl.GLenum`, int or None
922
+ None, `GL_UNSIGNED_BYTE`, `GL_FLOAT`. Only affects image files
923
+ (numpy arrays will be float).
924
+ wrapping : bool
925
+ Enable wrapping of the texture. A texture will be set to repeat (or
926
+ tile).
927
+
928
+ """
929
+ def _createTexture(self, tex, id, pixFormat, stim, res=128, maskParams=None,
930
+ forcePOW2=True, dataType=None, wrapping=True):
931
+
932
+ # transform all variants of `None` to that, simplifies conditions below
933
+ if tex in ["none", "None", "color"]:
934
+ tex = None
935
+
936
+ # Create an intensity texture, ranging -1:1.0
937
+ notSqr = False # most of the options will be creating a sqr texture
938
+ wasImage = False # change this if image loading works
939
+ interpolate = stim.interpolate
940
+ if dataType is None:
941
+ if pixFormat == GL.GL_RGB:
942
+ dataType = GL.GL_FLOAT
943
+ else:
944
+ dataType = GL.GL_UNSIGNED_BYTE
945
+
946
+ # Fill out unspecified portions of maskParams with default values
947
+ if maskParams is None:
948
+ maskParams = {}
949
+ # fringeWidth affects the proportion of the stimulus diameter that is
950
+ # devoted to the raised cosine.
951
+ allMaskParams = {'fringeWidth': 0.2, 'sd': 3}
952
+ allMaskParams.update(maskParams)
953
+
954
+ if type(tex) == numpy.ndarray:
955
+ # handle a numpy array
956
+ # for now this needs to be an NxN intensity array
957
+ intensity = tex.astype(numpy.float32)
958
+ if intensity.max() > 1 or intensity.min() < -1:
959
+ logging.error('numpy arrays used as textures should be in '
960
+ 'the range -1(black):1(white)')
961
+ if len(tex.shape) == 3:
962
+ wasLum = False
963
+ else:
964
+ wasLum = True
965
+ # is it 1D?
966
+ if tex.shape[0] == 1:
967
+ stim._tex1D = True
968
+ res = tex.shape[1]
969
+ elif len(tex.shape) == 1 or tex.shape[1] == 1:
970
+ stim._tex1D = True
971
+ res = tex.shape[0]
972
+ else:
973
+ stim._tex1D = False
974
+ # check if it's a square power of two
975
+ maxDim = max(tex.shape)
976
+ powerOf2 = 2 ** numpy.ceil(numpy.log2(maxDim))
977
+ if (forcePOW2 and
978
+ (tex.shape[0] != powerOf2 or
979
+ tex.shape[1] != powerOf2)):
980
+ logging.error("Requiring a square power of two (e.g. "
981
+ "16 x 16, 256 x 256) texture but didn't "
982
+ "receive one")
983
+ res = tex.shape[0]
984
+
985
+ dataType = GL.GL_FLOAT
986
+ elif tex in ("sin", "sqr", "saw", "tri", "sinXsin", "sqrXsqr", "circle",
987
+ "gauss", "cross", "radRamp", "raisedCos", None):
988
+ if tex is None:
989
+ res = 1
990
+ wrapping = True # override any wrapping setting for None
991
+
992
+ # compute array of intensity value for desired pattern
993
+ intensity = createLumPattern(tex, res, None, allMaskParams)
994
+ wasLum = True
995
+ else:
996
+ if isinstance(tex, (str, Path)):
997
+ # maybe tex is the name of a file:
998
+ filename = findImageFile(tex)
999
+ if not filename:
1000
+ msg = "Couldn't find image %s; check path? (tried: %s)"
1001
+ logging.error(msg % (tex, os.path.abspath(tex)))
1002
+ logging.flush()
1003
+ raise IOError(msg % (tex, os.path.abspath(tex)))
1004
+ try:
1005
+ im = Image.open(filename)
1006
+ im = im.transpose(Image.FLIP_TOP_BOTTOM)
1007
+ except IOError:
1008
+ msg = "Found file '%s', failed to load as an image"
1009
+ logging.error(msg % (filename))
1010
+ logging.flush()
1011
+ msg = "Found file '%s' [= %s], failed to load as an image"
1012
+ raise IOError(msg % (tex, os.path.abspath(tex)))
1013
+ else:
1014
+ # can't be a file; maybe its an image already in memory?
1015
+ try:
1016
+ im = tex.copy().transpose(Image.FLIP_TOP_BOTTOM)
1017
+ except AttributeError: # nope, not an image in memory
1018
+ msg = "Couldn't make sense of requested image."
1019
+ logging.error(msg)
1020
+ logging.flush()
1021
+ raise AttributeError(msg)
1022
+ # at this point we have a valid im
1023
+ stim._origSize = im.size
1024
+ wasImage = True
1025
+ # is it 1D?
1026
+ if im.size[0] == 1 or im.size[1] == 1:
1027
+ logging.error("Only 2D textures are supported at the moment")
1028
+ else:
1029
+ maxDim = max(im.size)
1030
+ powerOf2 = int(2**numpy.ceil(numpy.log2(maxDim)))
1031
+ if im.size[0] != powerOf2 or im.size[1] != powerOf2:
1032
+ if not forcePOW2:
1033
+ notSqr = True
1034
+ elif globalVars.nImageResizes < reportNImageResizes:
1035
+ msg = ("Image '%s' was not a square power-of-two ' "
1036
+ "'image. Linearly interpolating to be %ix%i")
1037
+ logging.warning(msg % (tex, powerOf2, powerOf2))
1038
+ globalVars.nImageResizes += 1
1039
+ im = im.resize([powerOf2, powerOf2], Image.BILINEAR)
1040
+ elif globalVars.nImageResizes == reportNImageResizes:
1041
+ logging.warning("Multiple images have needed resizing"
1042
+ " - I'll stop bothering you!")
1043
+ im = im.resize([powerOf2, powerOf2], Image.BILINEAR)
1044
+
1045
+ # is it Luminance or RGB?
1046
+ if pixFormat == GL.GL_ALPHA and im.mode != 'L':
1047
+ # we have RGB and need Lum
1048
+ wasLum = True
1049
+ im = im.convert("L") # force to intensity (need if was rgb)
1050
+ elif im.mode == 'L': # we have lum and no need to change
1051
+ wasLum = True
1052
+ dataType = GL.GL_FLOAT
1053
+ elif pixFormat == GL.GL_RGB:
1054
+ # we want RGB and might need to convert from CMYK or Lm
1055
+ # texture = im.tostring("raw", "RGB", 0, -1)
1056
+ im = im.convert("RGBA")
1057
+ wasLum = False
1058
+ else:
1059
+ raise ValueError('cannot determine if image is luminance or RGB')
1060
+
1061
+ if dataType == GL.GL_FLOAT:
1062
+ # convert from ubyte to float
1063
+ # much faster to avoid division 2/255
1064
+ intensity = numpy.array(im).astype(
1065
+ numpy.float32) * 0.0078431372549019607 - 1.0
1066
+ else:
1067
+ intensity = numpy.array(im)
1068
+
1069
+ if pixFormat == GL.GL_RGB and wasLum and dataType == GL.GL_FLOAT:
1070
+ # grating stim on good machine
1071
+ # keep as float32 -1:1
1072
+ if (sys.platform != 'darwin' and
1073
+ stim.win.glVendor.startswith('nvidia')):
1074
+ # nvidia under win/linux might not support 32bit float
1075
+ # could use GL_LUMINANCE32F_ARB here but check shader code?
1076
+ internalFormat = GL.GL_RGB16F_ARB
1077
+ else:
1078
+ # we've got a mac or an ATI card and can handle
1079
+ # 32bit float textures
1080
+ # could use GL_LUMINANCE32F_ARB here but check shader code?
1081
+ internalFormat = GL.GL_RGB32F_ARB
1082
+ # initialise data array as a float
1083
+ data = numpy.ones((intensity.shape[0], intensity.shape[1], 3),
1084
+ numpy.float32)
1085
+ data[:, :, 0] = intensity # R
1086
+ data[:, :, 1] = intensity # G
1087
+ data[:, :, 2] = intensity # B
1088
+ elif (pixFormat == GL.GL_RGB and
1089
+ wasLum and
1090
+ dataType != GL.GL_FLOAT):
1091
+ # was a lum image: stick with ubyte for speed
1092
+ internalFormat = GL.GL_RGB
1093
+ # initialise data array as a float
1094
+ data = numpy.ones((intensity.shape[0], intensity.shape[1], 3),
1095
+ numpy.ubyte)
1096
+ data[:, :, 0] = intensity # R
1097
+ data[:, :, 1] = intensity # G
1098
+ data[:, :, 2] = intensity # B
1099
+ elif pixFormat == GL.GL_RGB and dataType == GL.GL_FLOAT:
1100
+ # probably a custom rgb array or rgb image
1101
+ internalFormat = GL.GL_RGB32F_ARB
1102
+ data = intensity
1103
+ elif pixFormat == GL.GL_RGB:
1104
+ # not wasLum, not useShaders - an RGB bitmap with no shader
1105
+ # optionsintensity.min()
1106
+ internalFormat = GL.GL_RGB
1107
+ data = intensity # float_uint8(intensity)
1108
+ elif pixFormat == GL.GL_ALPHA:
1109
+ internalFormat = GL.GL_ALPHA
1110
+ dataType = GL.GL_UNSIGNED_BYTE
1111
+ if wasImage:
1112
+ data = intensity
1113
+ else:
1114
+ data = float_uint8(intensity)
1115
+ else:
1116
+ raise ValueError("invalid or unsupported `pixFormat`")
1117
+
1118
+ # check for RGBA textures
1119
+ if len(data.shape) > 2 and data.shape[2] == 4:
1120
+ if pixFormat == GL.GL_RGB:
1121
+ pixFormat = GL.GL_RGBA
1122
+ if internalFormat == GL.GL_RGB:
1123
+ internalFormat = GL.GL_RGBA
1124
+ elif internalFormat == GL.GL_RGB32F_ARB:
1125
+ internalFormat = GL.GL_RGBA32F_ARB
1126
+ texture = data.ctypes # serialise
1127
+
1128
+ # bind the texture in openGL
1129
+ GL.glEnable(GL.GL_TEXTURE_2D)
1130
+ GL.glBindTexture(GL.GL_TEXTURE_2D, id) # bind that name to the target
1131
+ # makes the texture map wrap (this is actually default anyway)
1132
+ if wrapping:
1133
+ GL.glTexParameteri(
1134
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_REPEAT)
1135
+ GL.glTexParameteri(
1136
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_REPEAT)
1137
+ else:
1138
+ GL.glTexParameteri(
1139
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP)
1140
+ GL.glTexParameteri(
1141
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP)
1142
+ # data from PIL/numpy is packed, but default for GL is 4 bytes
1143
+ GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1)
1144
+ # important if using bits++ because GL_LINEAR
1145
+ # sometimes extrapolates to pixel vals outside range
1146
+ if interpolate:
1147
+ GL.glTexParameteri(
1148
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR)
1149
+ # GL_GENERATE_MIPMAP was only available from OpenGL 1.4
1150
+ GL.glTexParameteri(
1151
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR)
1152
+ GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_GENERATE_MIPMAP,
1153
+ GL.GL_TRUE)
1154
+ GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, internalFormat,
1155
+ data.shape[1], data.shape[0], 0,
1156
+ pixFormat, dataType, texture)
1157
+ else:
1158
+ GL.glTexParameteri(
1159
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_NEAREST)
1160
+ GL.glTexParameteri(
1161
+ GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_NEAREST)
1162
+ GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, internalFormat,
1163
+ data.shape[1], data.shape[0], 0,
1164
+ pixFormat, dataType, texture)
1165
+
1166
+ GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE,
1167
+ GL.GL_MODULATE) # ?? do we need this - think not!
1168
+ # unbind our texture so that it doesn't affect other rendering
1169
+ GL.glBindTexture(GL.GL_TEXTURE_2D, 0)
1170
+
1171
+ return wasLum
1172
+
1173
+ def clearTextures(self):
1174
+ """Clear all textures associated with the stimulus.
1175
+
1176
+ As of v1.61.00 this is called automatically during garbage collection
1177
+ of your stimulus, so doesn't need calling explicitly by the user.
1178
+ """
1179
+ GL.glDeleteTextures(1, self._texID)
1180
+ if hasattr(self, '_maskID'):
1181
+ GL.glDeleteTextures(1, self._maskID)
1182
+
1183
+ @attributeSetter
1184
+ def mask(self, value):
1185
+ """The alpha mask (forming the shape of the image).
1186
+
1187
+ This can be one of various options:
1188
+ * 'circle', 'gauss', 'raisedCos', 'cross'
1189
+ * **None** (resets to default)
1190
+ * the name of an image file (most formats supported)
1191
+ * a numpy array (1xN or NxN) ranging -1:1
1192
+
1193
+ """
1194
+ self.__dict__['mask'] = value
1195
+ if self.__class__.__name__ == 'ImageStim':
1196
+ dataType = GL.GL_UNSIGNED_BYTE
1197
+ else:
1198
+ dataType = None
1199
+ self._createTexture(
1200
+ value, id=self._maskID, pixFormat=GL.GL_ALPHA, dataType=dataType,
1201
+ stim=self, res=self.texRes, maskParams=self.maskParams,
1202
+ wrapping=False)
1203
+
1204
+ def setMask(self, value, log=None):
1205
+ """Usually you can use 'stim.attribute = value' syntax instead,
1206
+ but use this method if you need to suppress the log message.
1207
+ """
1208
+ setAttribute(self, 'mask', value, log)
1209
+
1210
+ @attributeSetter
1211
+ def texRes(self, value):
1212
+ """Power-of-two int. Sets the resolution of the mask and texture.
1213
+ texRes is overridden if an array or image is provided as mask.
1214
+
1215
+ :ref:`Operations <attrib-operations>` supported.
1216
+ """
1217
+ self.__dict__['texRes'] = value
1218
+
1219
+ # ... now rebuild textures (call attributeSetters without logging).
1220
+ if hasattr(self, 'tex'):
1221
+ setAttribute(self, 'tex', self.tex, log=False)
1222
+ if hasattr(self, 'mask'):
1223
+ setAttribute(self, 'mask', self.mask, log=False)
1224
+
1225
+ @attributeSetter
1226
+ def maskParams(self, value):
1227
+ """Various types of input. Default to `None`.
1228
+
1229
+ This is used to pass additional parameters to the mask if those are
1230
+ needed.
1231
+
1232
+ - For 'gauss' mask, pass dict {'sd': 5} to control
1233
+ standard deviation.
1234
+ - For the 'raisedCos' mask, pass a dict: {'fringeWidth':0.2},
1235
+ where 'fringeWidth' is a parameter (float, 0-1), determining
1236
+ the proportion of the patch that will be blurred by the raised
1237
+ cosine edge."""
1238
+ self.__dict__['maskParams'] = value
1239
+ # call attributeSetter without log
1240
+ setAttribute(self, 'mask', self.mask, log=False)
1241
+
1242
+ @attributeSetter
1243
+ def interpolate(self, value):
1244
+ """Whether to interpolate (linearly) the texture in the stimulus.
1245
+
1246
+ If set to False then nearest neighbour will be used when needed,
1247
+ otherwise some form of interpolation will be used.
1248
+ """
1249
+ self.__dict__['interpolate'] = value
1250
+
1251
+
1252
+ class WindowMixin:
1253
+ """Window-related attributes and methods.
1254
+
1255
+ Used by BaseVisualStim, SimpleImageStim and ElementArrayStim.
1256
+
1257
+ """
1258
+ @property
1259
+ def win(self):
1260
+ """The :class:`~psychopy.visual.Window` object in which the
1261
+ stimulus will be rendered by default. (required)
1262
+
1263
+ Example, drawing same stimulus in two different windows and display
1264
+ simultaneously. Assuming that you have two windows and a stimulus
1265
+ (win1, win2 and stim)::
1266
+
1267
+ stim.win = win1 # stimulus will be drawn in win1
1268
+ stim.draw() # stimulus is now drawn to win1
1269
+ stim.win = win2 # stimulus will be drawn in win2
1270
+ stim.draw() # it is now drawn in win2
1271
+ win1.flip(waitBlanking=False) # do not wait for next
1272
+ # monitor update
1273
+ win2.flip() # wait for vertical blanking.
1274
+
1275
+ Note that this just changes **default** window for stimulus.
1276
+
1277
+ You could also specify window-to-draw-to when drawing::
1278
+
1279
+ stim.draw(win1)
1280
+ stim.draw(win2)
1281
+
1282
+ """
1283
+ return self.__dict__['win']
1284
+
1285
+ @win.setter
1286
+ def win(self, value):
1287
+ self.__dict__['win'] = value
1288
+ # Update window ref in size and pos objects
1289
+ if hasattr(self, "_size") and isinstance(self._size, Vector):
1290
+ self._size.win = value
1291
+ if hasattr(self, "_pos") and isinstance(self._pos, Vector):
1292
+ self._pos.win = value
1293
+
1294
+ @property
1295
+ def pos(self):
1296
+ if hasattr(self, "_pos"):
1297
+ return getattr(self._pos, self.units)
1298
+
1299
+ @pos.setter
1300
+ def pos(self, value):
1301
+ self._pos = Position(value, units=self.units, win=self.win)
1302
+
1303
+ @property
1304
+ def size(self):
1305
+ if hasattr(self, "_size"):
1306
+ return getattr(self._size, self.units)
1307
+
1308
+ @size.setter
1309
+ def size(self, value):
1310
+ if value is None:
1311
+ value = (None, None)
1312
+ self._size = Size(value, units=self.units, win=self.win)
1313
+
1314
+ @property
1315
+ def width(self):
1316
+ if len(self.size.shape) == 1:
1317
+ # Return first value if a 1d array
1318
+ return self.size[0]
1319
+ elif len(self.size.shape) == 2:
1320
+ # Return first column if a 2d array
1321
+ return self.size[:, 0]
1322
+
1323
+ @width.setter
1324
+ def width(self, value):
1325
+ # Convert to a numpy array
1326
+ value = numpy.array(value)
1327
+ # Set size
1328
+ if len(self.size.shape) == 1:
1329
+ # Set first value if a 1d array
1330
+ self.size[0] = value
1331
+ elif len(self.size.shape) == 2:
1332
+ # Set first column if a 2d array
1333
+ self.size[:, 0] = value
1334
+
1335
+ @property
1336
+ def height(self):
1337
+ if len(self.size.shape) == 1:
1338
+ # Return first value if a 1d array
1339
+ return self.size[1]
1340
+ elif len(self.size.shape) == 2:
1341
+ # Return first column if a 2d array
1342
+ return self.size[:, 1]
1343
+
1344
+ @height.setter
1345
+ def height(self, value):
1346
+ # Convert to a numpy array
1347
+ value = numpy.array(value)
1348
+ # Set size
1349
+ if len(self.size.shape) == 1:
1350
+ # Set first value if a 1d array
1351
+ self.size[1] = value
1352
+ elif len(self.size.shape) == 2:
1353
+ # Set first column if a 2d array
1354
+ self.size[:, 1] = value
1355
+
1356
+ @property
1357
+ def vertices(self):
1358
+ # Get or make Vertices object
1359
+ if hasattr(self, "_vertices"):
1360
+ verts = self._vertices
1361
+ else:
1362
+ # If not defined, assume vertices are just a square
1363
+ verts = self._vertices = Vertices(numpy.array([
1364
+ [0.5, -0.5],
1365
+ [-0.5, -0.5],
1366
+ [-0.5, 0.5],
1367
+ [0.5, 0.5],
1368
+ ]), obj=self, flip=self.flip, anchor=self.anchor)
1369
+ return verts.base
1370
+
1371
+ @vertices.setter
1372
+ def vertices(self, value):
1373
+ # If None, use defaut
1374
+ if value is None:
1375
+ value = [
1376
+ [0.5, -0.5],
1377
+ [-0.5, -0.5],
1378
+ [-0.5, 0.5],
1379
+ [0.5, 0.5],
1380
+ ]
1381
+ # Create Vertices object
1382
+ self._vertices = Vertices(value, obj=self, flip=self.flip, anchor=self.anchor)
1383
+
1384
+ @property
1385
+ def flip(self):
1386
+ """
1387
+ 1x2 array for flipping vertices along each axis; set as True to flip or False to not flip. If set as a single value, will duplicate across both axes. Accessing the protected attribute (`._flip`) will give an array of 1s and -1s with which to multiply vertices.
1388
+ """
1389
+ # Get base value
1390
+ if hasattr(self, "_flip"):
1391
+ flip = self._flip
1392
+ else:
1393
+ flip = numpy.array([[False, False]])
1394
+ # Convert from boolean
1395
+ return flip == -1
1396
+
1397
+ @flip.setter
1398
+ def flip(self, value):
1399
+ if value is None:
1400
+ value = False
1401
+ # Convert to 1x2 numpy array
1402
+ value = numpy.array(value)
1403
+ value.resize((1, 2))
1404
+ # Ensure values were bool
1405
+ assert value.dtype == bool, "Flip values must be either a boolean (True/False) or an array of booleans"
1406
+ # Set as multipliers rather than bool
1407
+ self._flip = numpy.array([[
1408
+ -1 if value[0, 0] else 1,
1409
+ -1 if value[0, 1] else 1,
1410
+ ]])
1411
+ self._flipHoriz, self._flipVert = self._flip[0]
1412
+ # Apply to vertices
1413
+ if not hasattr(self, "_vertices"):
1414
+ self.vertices = None
1415
+ self._vertices.flip = self.flip
1416
+ # Mark as needing vertex update
1417
+ self._needVertexUpdate = True
1418
+
1419
+ @property
1420
+ def flipHoriz(self):
1421
+ return self.flip[0][0]
1422
+
1423
+ @flipHoriz.setter
1424
+ def flipHoriz(self, value):
1425
+ self.flip = [value, self.flip[0, 1]]
1426
+
1427
+ @property
1428
+ def flipVert(self):
1429
+ return self.flip[0][1]
1430
+
1431
+ @flipVert.setter
1432
+ def flipVert(self, value):
1433
+ self.flip = [self.flip[0, 0], value]
1434
+
1435
+ @property
1436
+ def anchor(self):
1437
+ if hasattr(self, "_vertices"):
1438
+ return self._vertices.anchor
1439
+ elif hasattr(self, "_anchor"):
1440
+ # Return a backup value if there's no vertices yet
1441
+ return self._anchor
1442
+
1443
+ @anchor.setter
1444
+ def anchor(self, value):
1445
+ if hasattr(self, "_vertices"):
1446
+ self._vertices.anchor = value
1447
+ else:
1448
+ # Set a backup value if there's no vertices yet
1449
+ self._anchor = value
1450
+
1451
+ def setAnchor(self, value, log=None):
1452
+ setAttribute(self, 'anchor', value, log)
1453
+
1454
+ @property
1455
+ def units(self):
1456
+ if hasattr(self, "_units"):
1457
+ return self._units
1458
+ else:
1459
+ return self.win.units
1460
+
1461
+ @units.setter
1462
+ def units(self, value):
1463
+ """
1464
+ Units to use when drawing.
1465
+
1466
+ Possible options are: None, 'norm', 'cm', 'deg', 'degFlat',
1467
+ 'degFlatPos', or 'pix'.
1468
+
1469
+ If None then the current units of the
1470
+ :class:`~psychopy.visual.Window` will be used.
1471
+ See :ref:`units` for explanation of other options.
1472
+
1473
+ Note that when you change units, you don't change the stimulus
1474
+ parameters and it is likely to change appearance.
1475
+
1476
+ Example::
1477
+
1478
+ # This stimulus is 20% wide and 50% tall with respect to window
1479
+ stim = visual.PatchStim(win, units='norm', size=(0.2, 0.5)
1480
+
1481
+ # This stimulus is 0.2 degrees wide and 0.5 degrees tall.
1482
+ stim.units = 'deg'
1483
+
1484
+ """
1485
+ if value in unitTypes:
1486
+ self._units = value or self.win.units
1487
+ self._needVertexUpdate = True
1488
+ else:
1489
+ raise ValueError(f"Invalid unit type '{value}', must be one of: {unitTypes}")
1490
+
1491
+ def draw(self):
1492
+ raise NotImplementedError('Stimulus classes must override '
1493
+ 'visual.BaseVisualStim.draw')
1494
+
1495
+ def _selectWindow(self, win):
1496
+ """Switch drawing to the specified window. Calls the window's
1497
+ _setCurrent() method which handles the switch.
1498
+ """
1499
+ win._setCurrent()
1500
+
1501
+ def _updateList(self):
1502
+ """The user shouldn't need this method since it gets called
1503
+ after every call to .set()
1504
+ Chooses between using and not using shaders each call.
1505
+ """
1506
+ self._updateListShaders()
1507
+
1508
+
1509
+ class BaseVisualStim(MinimalStim, WindowMixin, LegacyVisualMixin):
1510
+ """A template for a visual stimulus class.
1511
+
1512
+ Actual visual stim like GratingStim, TextStim etc... are based on this.
1513
+ Not finished...?
1514
+
1515
+ Methods defined here will override Minimal & Legacy, but best to avoid
1516
+ that for simplicity & clarity.
1517
+
1518
+ """
1519
+ def __init__(self, win, units=None, name='', autoLog=None):
1520
+ self.autoLog = False # just to start off during init, set at end
1521
+ self.win = win
1522
+ self.units = units
1523
+ self._rotationMatrix = [[1., 0.], [0., 1.]] # no rotation by default
1524
+ # self.autoLog is set at end of MinimalStim.__init__
1525
+ super(BaseVisualStim, self).__init__(name=name, autoLog=autoLog)
1526
+ if self.autoLog:
1527
+ msg = ("%s is calling BaseVisualStim.__init__() with autolog=True"
1528
+ ". Set autoLog to True only at the end of __init__())")
1529
+ logging.warning(msg % (self.__class__.__name__))
1530
+
1531
+ @property
1532
+ def opacity(self):
1533
+ """Determines how visible the stimulus is relative to background.
1534
+
1535
+ The value should be a single float ranging 1.0 (opaque) to 0.0
1536
+ (transparent). :ref:`Operations <attrib-operations>` are supported.
1537
+ Precisely how this is used depends on the :ref:`blendMode`.
1538
+ """
1539
+ alphas = []
1540
+ if hasattr(self, '_foreColor'):
1541
+ alphas.append(self._foreColor.alpha)
1542
+ if hasattr(self, '_fillColor'):
1543
+ alphas.append(self._fillColor.alpha)
1544
+ if hasattr(self, '_borderColor'):
1545
+ alphas.append(self._borderColor.alpha)
1546
+ if alphas:
1547
+ return mean(alphas)
1548
+ else:
1549
+ return 1
1550
+
1551
+ @opacity.setter
1552
+ def opacity(self, value):
1553
+ # Setting opacity as a single value makes all colours the same opacity
1554
+ if value is None:
1555
+ # If opacity is set to be None, this indicates that each color should handle its own opacity
1556
+ return
1557
+ if hasattr(self, '_foreColor'):
1558
+ if self._foreColor != None:
1559
+ self._foreColor.alpha = value
1560
+ if hasattr(self, '_fillColor'):
1561
+ if self._fillColor != None:
1562
+ self._fillColor.alpha = value
1563
+ if hasattr(self, '_borderColor'):
1564
+ if self._borderColor != None:
1565
+ self._borderColor.alpha = value
1566
+
1567
+ def updateOpacity(self):
1568
+ """Placeholder method to update colours when set externally, for example
1569
+ updating the `pallette` attribute of a textbox."""
1570
+ return
1571
+
1572
+ @attributeSetter
1573
+ def ori(self, value):
1574
+ """The orientation of the stimulus (in degrees).
1575
+
1576
+ Should be a single value (:ref:`scalar <attrib-scalar>`).
1577
+ :ref:`Operations <attrib-operations>` are supported.
1578
+
1579
+ Orientation convention is like a clock: 0 is vertical, and positive
1580
+ values rotate clockwise. Beyond 360 and below zero values wrap
1581
+ appropriately.
1582
+
1583
+ """
1584
+ self.__dict__['ori'] = float(value)
1585
+ radians = value * 0.017453292519943295
1586
+ sin, cos = numpy.sin, numpy.cos
1587
+ self._rotationMatrix = numpy.array([[cos(radians), -sin(radians)],
1588
+ [sin(radians), cos(radians)]])
1589
+ self._needVertexUpdate = True # need to update update vertices
1590
+ self._needUpdate = True
1591
+
1592
+ @property
1593
+ def size(self):
1594
+ """The size (width, height) of the stimulus in the stimulus
1595
+ :ref:`units <units>`
1596
+
1597
+ Value should be :ref:`x,y-pair <attrib-xy>`,
1598
+ :ref:`scalar <attrib-scalar>` (applies to both dimensions)
1599
+ or None (resets to default). :ref:`Operations <attrib-operations>`
1600
+ are supported.
1601
+
1602
+ Sizes can be negative (causing a mirror-image reversal) and can
1603
+ extend beyond the window.
1604
+
1605
+ Example::
1606
+
1607
+ stim.size = 0.8 # Set size to (xsize, ysize) = (0.8, 0.8)
1608
+ print(stim.size) # Outputs array([0.8, 0.8])
1609
+ stim.size += (0.5, -0.5) # make wider and flatter: (1.3, 0.3)
1610
+
1611
+ Tip: if you can see the actual pixel range this corresponds to by
1612
+ looking at `stim._sizeRendered`
1613
+ """
1614
+ return WindowMixin.size.fget(self)
1615
+
1616
+ @size.setter
1617
+ def size(self, value):
1618
+ # Supply default for None
1619
+ if value is None:
1620
+ value = Size((1, 1), units="height", win=self.win)
1621
+ # Duplicate single values
1622
+ if isinstance(value, (float, int)):
1623
+ value = (value, value)
1624
+ # Do setting
1625
+ WindowMixin.size.fset(self, value)
1626
+ # Mark any updates needed
1627
+ self._needVertexUpdate = True
1628
+ self._needUpdate = True
1629
+ if hasattr(self, '_calcCyclesPerStim'):
1630
+ self._calcCyclesPerStim()
1631
+
1632
+ @property
1633
+ def pos(self):
1634
+ """
1635
+ The position of the center of the stimulus in the stimulus
1636
+ :ref:`units <units>`
1637
+
1638
+ `value` should be an :ref:`x,y-pair <attrib-xy>`.
1639
+ :ref:`Operations <attrib-operations>` are also supported.
1640
+
1641
+ Example::
1642
+
1643
+ stim.pos = (0.5, 0) # Set slightly to the right of center
1644
+ stim.pos += (0.5, -1) # Increment pos rightwards and upwards.
1645
+ Is now (1.0, -1.0)
1646
+ stim.pos *= 0.2 # Move stim towards the center.
1647
+ Is now (0.2, -0.2)
1648
+
1649
+ Tip: If you need the position of stim in pixels, you can obtain
1650
+ it like this::
1651
+
1652
+ from psychopy.tools.monitorunittools import posToPix
1653
+ posPix = posToPix(stim)
1654
+
1655
+ """
1656
+ return WindowMixin.pos.fget(self)
1657
+
1658
+ @pos.setter
1659
+ def pos(self, value):
1660
+ # Supply defualt for None
1661
+ if value is None:
1662
+ value = Position((0, 0), units="height", win=self.win)
1663
+ # Do setting
1664
+ WindowMixin.pos.fset(self, value)
1665
+ # Mark any updates needed
1666
+ self._needVertexUpdate = True
1667
+ self._needUpdate = True
1668
+
1669
+ def setPos(self, newPos, operation='', log=None):
1670
+ """Usually you can use 'stim.attribute = value' syntax instead,
1671
+ but use this method if you need to suppress the log message.
1672
+ """
1673
+ setAttribute(self, 'pos', val2array(newPos, False), log, operation)
1674
+
1675
+ def setDepth(self, newDepth, operation='', log=None):
1676
+ """Usually you can use 'stim.attribute = value' syntax instead,
1677
+ but use this method if you need to suppress the log message
1678
+ """
1679
+ setAttribute(self, 'depth', newDepth, log, operation)
1680
+
1681
+ def setSize(self, newSize, operation='', units=None, log=None):
1682
+ """Usually you can use 'stim.attribute = value' syntax instead,
1683
+ but use this method if you need to suppress the log message
1684
+ """
1685
+ if units is None:
1686
+ # need to change this to create several units from one
1687
+ units = self.units
1688
+ setAttribute(self, 'size', val2array(newSize, False), log, operation)
1689
+
1690
+ def setOri(self, newOri, operation='', log=None):
1691
+ """Usually you can use 'stim.attribute = value' syntax instead,
1692
+ but use this method if you need to suppress the log message
1693
+ """
1694
+ setAttribute(self, 'ori', newOri, log, operation)
1695
+
1696
+ def setOpacity(self, newOpacity, operation='', log=None):
1697
+ """Hard setter for opacity, allows the suppression of log messages and calls the update method
1698
+ """
1699
+ if operation in ['', '=']:
1700
+ self.opacity = newOpacity
1701
+ elif operation in ['+']:
1702
+ self.opacity += newOpacity
1703
+ elif operation in ['-']:
1704
+ self.opacity -= newOpacity
1705
+ else:
1706
+ logging.error(f"Operation '{operation}' not recognised.")
1707
+ # Trigger color update for components like Textbox which have different behaviours for a hard setter
1708
+ self.updateOpacity()
1709
+
1710
+ def _set(self, attrib, val, op='', log=None):
1711
+ """DEPRECATED since 1.80.04 + 1.
1712
+ Use setAttribute() and val2array() instead.
1713
+ """
1714
+ # format the input value as float vectors
1715
+ if type(val) in [tuple, list, numpy.ndarray]:
1716
+ val = val2array(val)
1717
+
1718
+ # Set attribute with operation and log
1719
+ setAttribute(self, attrib, val, log, op)
1720
+
1721
+ # For DotStim
1722
+ if attrib in ('nDots', 'coherence'):
1723
+ self.coherence = round(self.coherence * self.nDots) / self.nDots