1#!/usr/bin/env python2 2 3'''Provides class BaseVisualStim and mixins; subclass to get visual stimuli''' 4 5# Part of the PsychoPy library 6# Copyright (C) 2015 Jonathan Peirce 7# Distributed under the terms of the GNU General Public License (GPL). 8 9# Ensure setting pyglet.options['debug_gl'] to False is done prior to any 10# other calls to pyglet or pyglet submodules, otherwise it may not get picked 11# up by the pyglet GL engine and have no effect. 12# Shaders will work but require OpenGL2.0 drivers AND PyOpenGL3.0+ 13import pyglet 14pyglet.options['debug_gl'] = False 15GL = pyglet.gl 16try: 17 from PIL import Image 18except ImportError: 19 import Image 20 21import copy 22import sys 23import os 24 25import psychopy # so we can get the __path__ 26from psychopy import logging 27 28# tools must only be imported *after* event or MovieStim breaks on win32 29# (JWP has no idea why!) 30from psychopy.tools.arraytools import val2array 31from psychopy.tools.attributetools import attributeSetter, logAttrib, setAttribute 32from psychopy.tools.colorspacetools import dkl2rgb, lms2rgb 33from psychopy.tools.monitorunittools import cm2pix, deg2pix, pix2cm, pix2deg, convertToPix 34from psychopy.visual.helpers import pointInPolygon, polygonsOverlap, setColor 35from psychopy.tools.typetools import float_uint8 36from psychopy.tools.arraytools import makeRadialMatrix 37from . import glob_vars 38 39import numpy 40from numpy import pi 41 42from psychopy.constants import NOT_STARTED, STARTED, STOPPED 43 44reportNImageResizes = 5 #permitted number of resizes 45 46""" 47There are several base and mix-in visual classes for multiple inheritance: 48 - MinimalStim: non-visual house-keeping code common to all visual stim 49 RatingScale inherits only from MinimalStim. 50 - WindowMixin: attributes/methods about the stim relative to a visual.Window. 51 - LegacyVisualMixin: deprecated visual methods (eg, setRGB) 52 added to BaseVisualStim 53 - ColorMixin: for Stim that need color methods (most, not Movie) 54 color-related methods and attribs 55 - ContainerMixin: for stim that need polygon .contains() methods (most, not Text) 56 .contains(), .overlaps() 57 - TextureMixin: for texture methods namely _createTexture (Grating, not Text) 58 seems to work; caveat: There were issues in earlier (non-MI) versions 59 of using _createTexture so it was pulled out of classes. Now it's inside 60 classes again. Should be watched. 61 - BaseVisualStim: = Minimal + Window + Legacy. Furthermore adds common attributes 62 like orientation, opacity, contrast etc. 63 64Typically subclass BaseVisualStim to create new visual stim classes, and add 65mixin(s) as needed to add functionality. 66""" 67 68class MinimalStim(object): 69 """Non-visual methods and attributes for BaseVisualStim and RatingScale. 70 71 Includes: name, autoDraw, autoLog, status, __str__ 72 """ 73 def __init__(self, name=None, autoLog=None): 74 self.__dict__['name'] = name if name not in (None, '') else 'unnamed %s' %self.__class__.__name__ 75 self.status = NOT_STARTED 76 self.autoLog = autoLog 77 super(MinimalStim, self).__init__() 78 if self.autoLog: 79 logging.warning("%s is calling MinimalStim.__init__() with autolog=True. Set autoLog to True only at the end of __init__())" \ 80 %(self.__class__.__name__)) 81 82 def __str__(self, complete=False): 83 """ 84 """ 85 if hasattr(self, '_initParams'): 86 className = self.__class__.__name__ 87 paramStrings = [] 88 for param in self._initParams: 89 if hasattr(self, param): 90 val = getattr(self, param) 91 valStr = repr(getattr(self, param)) 92 if len(repr(valStr))>50 and not complete: 93 if val.__class__.__name__ == 'attributeSetter': 94 valStr = "%s(...)" %val.__getattribute__.__class__.__name__ 95 else: 96 valStr = "%s(...)" %val.__class__.__name__ 97 else: 98 valStr = 'UNKNOWN' 99 paramStrings.append("%s=%s" %(param, valStr)) 100 #this could be used if all params are known to exist: 101 # paramStrings = ["%s=%s" %(param, getattr(self, param)) for param in self._initParams] 102 params = ", ".join(paramStrings) 103 s = "%s(%s)" %(className, params) 104 else: 105 s = object.__repr__(self) 106 return s 107 108 # Might seem simple at first, but this ensures that "name" attribute 109 # appears in docs and that name setting and updating is logged. 110 @attributeSetter 111 def name(self, value): 112 """String or None. The name of the object to be using during logged messages about 113 this stim. If you have multiple stimuli in your experiment this really 114 helps to make sense of log files! 115 116 If name = None your stimulus will be called "unnamed <type>", e.g. 117 visual.TextStim(win) will be called "unnamed TextStim" in the logs. 118 """ 119 self.__dict__['name'] = value 120 121 @attributeSetter 122 def autoDraw(self, value): 123 """Determines whether the stimulus should be automatically drawn on every frame flip. 124 125 Value should be: `True` or `False`. You do NOT need to set this on every frame flip! 126 """ 127 self.__dict__['autoDraw'] = value 128 toDraw = self.win._toDraw 129 toDrawDepths = self.win._toDrawDepths 130 beingDrawn = (self in toDraw) 131 if value == beingDrawn: 132 return #nothing to do 133 elif value: 134 #work out where to insert the object in the autodraw list 135 depthArray = numpy.array(toDrawDepths) 136 iis = numpy.where(depthArray < self.depth)[0]#all indices where true 137 if len(iis):#we featured somewhere before the end of the list 138 toDraw.insert(iis[0], self) 139 toDrawDepths.insert(iis[0], self.depth) 140 else: 141 toDraw.append(self) 142 toDrawDepths.append(self.depth) 143 self.status = STARTED 144 elif value == False: 145 #remove from autodraw lists 146 toDrawDepths.pop(toDraw.index(self)) #remove from depths 147 toDraw.remove(self) #remove from draw list 148 self.status = STOPPED 149 150 def setAutoDraw(self, value, log=None): 151 """Sets autoDraw. Usually you can use 'stim.attribute = value' syntax instead, 152 but use this method if you need to suppress the log message""" 153 setAttribute(self, 'autoDraw', value, log) 154 155 @attributeSetter 156 def autoLog(self, value): 157 """Whether every change in this stimulus should be logged automatically 158 159 Value should be: `True` or `False`. Set to `False` if your stimulus is updating frequently (e.g. 160 updating its position every frame) and you want to avoid swamping the log file with 161 messages that aren't likely to be useful. 162 """ 163 self.__dict__['autoLog'] = value 164 165 def setAutoLog(self, value=True, log=None): 166 """Usually you can use 'stim.attribute = value' syntax instead, 167 but use this method if you need to suppress the log message""" 168 setAttribute(self, 'autoLog', value, log) 169 170class LegacyVisualMixin(object): 171 """Class to hold deprecated visual methods and attributes. 172 173 Intended only for use as a mixin class for BaseVisualStim, to maintain 174 backwards compatibility while reducing clutter in class BaseVisualStim. 175 """ 176 #def __init__(self): 177 # super(LegacyVisualMixin, self).__init__() 178 179 def _calcSizeRendered(self): 180 """DEPRECATED in 1.80.00. This functionality is now handled by _updateVertices() and verticesPix""" 181 #raise DeprecationWarning, "_calcSizeRendered() was deprecated in 1.80.00. This functionality is now handled by _updateVertices() and verticesPix" 182 if self.units in ['norm','pix', 'height']: self._sizeRendered=copy.copy(self.size) 183 elif self.units in ['deg', 'degs']: self._sizeRendered=deg2pix(self.size, self.win.monitor) 184 elif self.units=='cm': self._sizeRendered=cm2pix(self.size, self.win.monitor) 185 else: 186 logging.error("Stimulus units should be 'height', 'norm', 'deg', 'cm' or 'pix', not '%s'" %self.units) 187 188 def _calcPosRendered(self): 189 """DEPRECATED in 1.80.00. This functionality is now handled by _updateVertices() and verticesPix""" 190 #raise DeprecationWarning, "_calcSizeRendered() was deprecated in 1.80.00. This functionality is now handled by _updateVertices() and verticesPix" 191 if self.units in ['norm','pix', 'height']: self._posRendered= copy.copy(self.pos) 192 elif self.units in ['deg', 'degs']: self._posRendered=deg2pix(self.pos, self.win.monitor) 193 elif self.units=='cm': self._posRendered=cm2pix(self.pos, self.win.monitor) 194 195 def _getPolyAsRendered(self): 196 """DEPRECATED. Return a list of vertices as rendered; used by overlaps() 197 """ 198 oriRadians = numpy.radians(self.ori) 199 sinOri = numpy.sin(-oriRadians) 200 cosOri = numpy.cos(-oriRadians) 201 x = self._verticesRendered[:,0] * cosOri - self._verticesRendered[:,1] * sinOri 202 y = self._verticesRendered[:,0] * sinOri + self._verticesRendered[:,1] * cosOri 203 return numpy.column_stack((x,y)) + self._posRendered 204 205 def setDKL(self, newDKL, operation=''): 206 """DEPRECATED since v1.60.05: Please use the `color` attribute 207 """ 208 self._set('dkl', val=newDKL, op=operation) 209 self.setRGB(dkl2rgb(self.dkl, self.win.dkl_rgb)) 210 def setLMS(self, newLMS, operation=''): 211 """DEPRECATED since v1.60.05: Please use the `color` attribute 212 """ 213 self._set('lms', value=newLMS, op=operation) 214 self.setRGB(lms2rgb(self.lms, self.win.lms_rgb)) 215 def setRGB(self, newRGB, operation='', log=None): 216 """DEPRECATED since v1.60.05: Please use the `color` attribute 217 """ 218 from psychopy.visual.helpers import setTexIfNoShaders 219 self._set('rgb', newRGB, operation) 220 setTexIfNoShaders(self) 221 if self.__class__.__name__ == 'TextStim' and not self.useShaders: 222 self._needSetText=True 223 224 @attributeSetter 225 def depth(self, value): 226 """DEPRECATED. Depth is now controlled simply by drawing order. 227 """ 228 self.__dict__['depth'] = value 229 230class ColorMixin(object): 231 """Mixin class for visual stim that need color and or contrast. 232 """ 233 #def __init__(self): 234 # super(ColorStim, self).__init__() 235 236 @attributeSetter 237 def color(self, value): 238 """Color of the stimulus 239 240 Value should be one of: 241 + string: to specify a :ref:`colorNames`. Any of the standard 242 html/X11 `color names <http://www.w3schools.com/html/html_colornames.asp>` 243 can be used. 244 + :ref:`hexColors` 245 + numerically: (scalar or triplet) for DKL, RGB or other :ref:`colorspaces`. For 246 these, :ref:`operations <attrib-operations>` are supported. 247 248 When color is specified using numbers, it is interpreted with 249 respect to the stimulus' current colorSpace. If color is given as a 250 single value (scalar) then this will be applied to all 3 channels. 251 252 Examples:: 253 # ... for whatever stim you have, e.g. stim = visual.ShapeStim(win): 254 stim.color = 'white' 255 stim.color = 'RoyalBlue' # (the case is actually ignored) 256 stim.color = '#DDA0DD' # DDA0DD is hexadecimal for plum 257 stim.color = [1.0, -1.0, -1.0] # if stim.colorSpace='rgb': a red color in rgb space 258 stim.color = [0.0, 45.0, 1.0] # if stim.colorSpace='dkl': DKL space with elev=0, azimuth=45 259 stim.color = [0, 0, 255] # if stim.colorSpace='rgb255': a blue stimulus using rgb255 space 260 stim.color = 255 # interpreted as (255, 255, 255) which is white in rgb255. 261 262 263 :ref:`Operations <attrib-operations>` work as normal for all numeric 264 colorSpaces (e.g. 'rgb', 'hsv' and 'rgb255') but not for strings, like 265 named and hex. For example, assuming that colorSpace='rgb':: 266 267 stim.color += [1, 1, 1] # increment all guns by 1 value 268 stim.color *= -1 # multiply the color by -1 (which in this space inverts the contrast) 269 stim.color *= [0.5, 0, 1] # decrease red, remove green, keep blue 270 271 You can use `setColor` if you want to set color and colorSpace in one 272 line. These two are equivalent:: 273 274 stim.setColor((0, 128, 255), 'rgb255') 275 # ... is equivalent to 276 stim.colorSpace = 'rgb255' 277 stim.color = (0, 128, 255) 278 """ 279 self.setColor(value, log=False) # logging already done by attributeSettter 280 281 @attributeSetter 282 def colorSpace(self, value): 283 """The name of the color space currently being used (for numeric colors) 284 285 Value should be: a string or None 286 287 For strings and hex values this is not needed. 288 If None the default colorSpace for the stimulus is 289 used (defined during initialisation). 290 291 Please note that changing colorSpace does not change stimulus parameters. 292 Thus you usually want to specify colorSpace before setting the color. Example:: 293 294 # A light green text 295 stim = visual.TextStim(win, 'Color me!', color=(0, 1, 0), colorSpace='rgb') 296 297 # An almost-black text 298 stim.colorSpace = 'rgb255' 299 300 # Make it light green again 301 stim.color = (128, 255, 128) 302 """ 303 self.__dict__['colorSpace'] = value 304 305 @attributeSetter 306 def contrast(self, value): 307 """A value that is simply multiplied by the color 308 309 Value should be: a float between -1 (negative) and 1 (unchanged). 310 :ref:`Operations <attrib-operations>` supported. 311 312 Set the contrast of the stimulus, i.e. scales how far the stimulus 313 deviates from the middle grey. You can also use the stimulus 314 `opacity` to control contrast, but that cannot be negative. 315 316 Examples:: 317 318 stim.contrast = 1.0 # unchanged contrast 319 stim.contrast = 0.5 # decrease contrast 320 stim.contrast = 0.0 # uniform, no contrast 321 stim.contrast = -0.5 # slightly inverted 322 stim.contrast = -1.0 # totally inverted 323 324 Setting contrast outside range -1 to 1 is permitted, but may 325 produce strange results if color values exceeds the monitor limits.:: 326 327 stim.contrast = 1.2 # increases contrast 328 stim.contrast = -1.2 # inverts with increased contrast 329 """ 330 self.__dict__['contrast'] = value 331 332 # If we don't have shaders we need to rebuild the stimulus 333 if hasattr(self, 'useShaders'): 334 if not self.useShaders: 335 #we'll need to update the textures for the stimulus 336 #(sometime before drawing but not now) 337 if self.__class__.__name__ == 'TextStim': 338 self.text = self.text # call attributeSetter to rebuild text 339 elif hasattr(self,'_needTextureUpdate'): #GratingStim, RadialStim, ImageStim etc 340 self._needTextureUpdate = True 341 elif self.__class__.__name__ in ('ShapeStim','DotStim'): 342 pass # They work fine without shaders? 343 elif self.autoLog: 344 logging.warning('Tried to set contrast while useShaders = False but stimulus was not rebuild. Contrast might remain unchanged.') 345 elif self.autoLog: 346 logging.warning('Contrast was set on class where useShaders was undefined. Contrast might remain unchanged') 347 def setColor(self, color, colorSpace=None, operation='', log=None): 348 """Usually you can use 'stim.attribute = value' syntax instead, 349 but use this method if you need to suppress the log message 350 and/or set colorSpace simultaneously. 351 """ 352 # NB: the setColor helper function! Not this function itself :-) 353 setColor(self,color, colorSpace=colorSpace, operation=operation, 354 rgbAttrib='rgb', #or 'fillRGB' etc 355 colorAttrib='color') 356 if self.__class__.__name__ == 'TextStim' and not self.useShaders: 357 self._needSetText = True 358 logAttrib(self, log, 'color', value='%s (%s)' %(self.color, self.colorSpace)) 359 def setContrast(self, newContrast, operation='', log=None): 360 """Usually you can use 'stim.attribute = value' syntax instead, 361 but use this method if you need to suppress the log message 362 """ 363 setAttribute(self, 'contrast', newContrast, log, operation) 364 365 def _getDesiredRGB(self, rgb, colorSpace, contrast): 366 """ Convert color to RGB while adding contrast 367 Requires self.rgb, self.colorSpace and self.contrast""" 368 # Ensure that we work on 0-centered color (to make negative contrast values work) 369 if colorSpace not in ['rgb', 'dkl', 'lms', 'hsv']: 370 rgb = (rgb / 255.0) * 2 - 1 371 372 # Convert to RGB in range 0:1 and scaled for contrast 373 # NB glColor will clamp it to be 0-1 (whether or not we use FBO) 374 desiredRGB = (rgb * contrast + 1) / 2.0 375 if not self.win.useFBO: 376 # Check that boundaries are not exceeded. If we have an FBO that can handle this 377 if numpy.any(desiredRGB > 1.0) or numpy.any(desiredRGB < 0): 378 logging.warning('Desired color %s (in RGB 0->1 units) falls outside the monitor gamut. Drawing blue instead' %desiredRGB) #AOH 379 desiredRGB=[0.0, 0.0, 1.0] 380 381 return desiredRGB 382 383class ContainerMixin(object): 384 """Mixin class for visual stim that have verticesPix attrib and .contains() methods. 385 """ 386 def __init__(self): 387 super(ContainerMixin, self).__init__() 388 self._verticesBase = numpy.array([[0.5,-0.5],[-0.5,-0.5],[-0.5,0.5],[0.5,0.5]]) #sqr 389 self._borderBase = numpy.array([[0.5,-0.5],[-0.5,-0.5],[-0.5,0.5],[0.5,0.5]]) #sqr 390 self._rotationMatrix = [[1.,0.],[0.,1.]] #no rotation as a default 391 392 @property 393 def verticesPix(self): 394 """This determines the coordinates of the vertices for the 395 current stimulus in pixels, accounting for size, ori, pos and units 396 """ 397 #because this is a property getter we can check /on-access/ if it needs updating :-) 398 if self._needVertexUpdate: 399 self._updateVertices() 400 return self.__dict__['verticesPix'] 401 @property 402 def _borderPix(self): 403 """Allows for a dynamic border that differs from self.vertices, but gets 404 updated dynamically with identical transformations. 405 """ 406 if not hasattr(self, 'border'): 407 msg = "%s._borderPix requested without .border" % self.name 408 logging.error(msg) 409 raise AttributeError(msg) 410 if self._needVertexUpdate: 411 self._updateVertices() 412 return self.__dict__['_borderPix'] 413 def _updateVertices(self): 414 """Sets Stim.verticesPix and ._borderPix from pos, size, ori, flipVert, flipHoriz 415 """ 416 #check whether stimulus needs flipping in either direction 417 flip = numpy.array([1,1]) 418 if hasattr(self, 'flipHoriz'): 419 flip[0] = self.flipHoriz*(-2)+1 # True=(-1), False->(+1) 420 if hasattr(self, 'flipVert'): 421 flip[1] = self.flipVert*(-2)+1 # True=(-1), False->(+1) 422 423 if hasattr(self, 'vertices'): 424 verts = self.vertices 425 else: 426 verts = self._verticesBase 427 # set size and orientation, combine with position and convert to pix: 428 verts = numpy.dot(self.size*verts*flip, self._rotationMatrix) 429 verts = convertToPix(vertices=verts, pos=self.pos, win=self.win, units=self.units) 430 self.__dict__['verticesPix'] = verts 431 432 if hasattr(self, 'border'): 433 #border = self.border 434 border = numpy.dot(self.size*self.border*flip, self._rotationMatrix) 435 border = convertToPix(vertices=border, pos=self.pos, win=self.win, units=self.units) 436 self.__dict__['_borderPix'] = border 437 438 self._needVertexUpdate = False 439 self._needUpdate = True #but we presumably need to update the list 440 441 def contains(self, x, y=None, units=None): 442 """Returns True if a point x,y is inside the stimulus' border. 443 444 Can accept variety of input options: 445 + two separate args, x and y 446 + one arg (list, tuple or array) containing two vals (x,y) 447 + an object with a getPos() method that returns x,y, such 448 as a :class:`~psychopy.event.Mouse`. 449 450 Returns `True` if the point is within the area defined either by its 451 `border` attribute (if one defined), or its `vertices` attribute if there 452 is no .border. This method handles 453 complex shapes, including concavities and self-crossings. 454 455 Note that, if your stimulus uses a mask (such as a Gaussian) then 456 this is not accounted for by the `contains` method; the extent of the 457 stimulus is determined purely by the size, position (pos), and orientation (ori) settings 458 (and by the vertices for shape stimuli). 459 460 See Coder demos: shapeContains.py 461 """ 462 #get the object in pixels 463 if hasattr(x, 'border'): 464 xy = x._borderPix #access only once - this is a property 465 units = 'pix' #we can forget about the units 466 elif hasattr(x, 'verticesPix'): 467 xy = x.verticesPix #access only once - this is a property (slower to access) 468 units = 'pix' #we can forget about the units 469 elif hasattr(x, 'getPos'): 470 xy = x.getPos() 471 units = x.units 472 elif type(x) in [list, tuple, numpy.ndarray]: 473 xy = numpy.array(x) 474 else: 475 xy = numpy.array((x,y)) 476 #try to work out what units x,y has 477 if units is None: 478 if hasattr(xy, 'units'): 479 units = xy.units 480 else: 481 units = self.units 482 if units != 'pix': 483 xy = convertToPix(xy, pos=(0,0), units=units, win=self.win) 484 # ourself in pixels 485 if hasattr(self, 'border'): 486 poly = self._borderPix # e.g., outline vertices 487 else: 488 poly = self.verticesPix # e.g., tesselated vertices 489 490 return pointInPolygon(xy[0], xy[1], poly=poly) 491 492 def overlaps(self, polygon): 493 """Returns `True` if this stimulus intersects another one. 494 495 If `polygon` is 496 another stimulus instance, then the vertices and location of that stimulus 497 will be used as the polygon. Overlap detection is typically very good, but it 498 can fail with very pointy shapes in a crossed-swords configuration. 499 500 Note that, if your stimulus uses a mask (such as a Gaussian blob) then 501 this is not accounted for by the `overlaps` method; the extent of the 502 stimulus is determined purely by the size, pos, and orientation settings 503 (and by the vertices for shape stimuli). 504 505 See coder demo, shapeContains.py 506 """ 507 return polygonsOverlap(self, polygon) 508 509class TextureMixin(object): 510 """Mixin class for visual stim that have textures. 511 512 Could move visual.helpers.setTexIfNoShaders() into here 513 """ 514 #def __init__(self): 515 # super(TextureMixin, self).__init__() 516 517 def _createTexture(self, tex, id, pixFormat, stim, res=128, maskParams=None, 518 forcePOW2=True, dataType=None): 519 """ 520 :params: 521 id: 522 is the texture ID 523 pixFormat: 524 GL.GL_ALPHA, GL.GL_RGB 525 useShaders: 526 bool 527 interpolate: 528 bool (determines whether texture will use GL_LINEAR or GL_NEAREST 529 res: 530 the resolution of the texture (unless a bitmap image is used) 531 dataType: 532 None, GL.GL_UNSIGNED_BYTE, GL_FLOAT. Only affects image files (numpy arrays will be float) 533 534 For grating stimuli (anything that needs multiple cycles) forcePOW2 should 535 be set to be True. Otherwise the wrapping of the texture will not work. 536 """ 537 538 """ 539 Create an intensity texture, ranging -1:1.0 540 """ 541 notSqr=False #most of the options will be creating a sqr texture 542 wasImage=False #change this if image loading works 543 useShaders = stim.useShaders 544 interpolate = stim.interpolate 545 if dataType is None: 546 if useShaders and pixFormat==GL.GL_RGB: 547 dataType = GL.GL_FLOAT 548 else: 549 dataType = GL.GL_UNSIGNED_BYTE 550 551 # Fill out unspecified portions of maskParams with default values 552 if maskParams is None: 553 maskParams = {} 554 allMaskParams = {'fringeWidth': 0.2, 'sd': 3} # fringeWidth affects the proportion of the stimulus diameter that is devoted to the raised cosine. 555 allMaskParams.update(maskParams) 556 557 if type(tex) == numpy.ndarray: 558 #handle a numpy array 559 #for now this needs to be an NxN intensity array 560 intensity = tex.astype(numpy.float32) 561 if intensity.max()>1 or intensity.min()<-1: 562 logging.error('numpy arrays used as textures should be in the range -1(black):1(white)') 563 if len(tex.shape)==3: 564 wasLum=False 565 else: wasLum = True 566 ##is it 1D? 567 if tex.shape[0]==1: 568 stim._tex1D=True 569 res=tex.shape[1] 570 elif len(tex.shape)==1 or tex.shape[1]==1: 571 stim._tex1D=True 572 res=tex.shape[0] 573 else: 574 stim._tex1D=False 575 #check if it's a square power of two 576 maxDim = max(tex.shape) 577 powerOf2 = 2**numpy.ceil(numpy.log2(maxDim)) 578 if forcePOW2 and (tex.shape[0]!=powerOf2 or tex.shape[1]!=powerOf2): 579 logging.error("Requiring a square power of two (e.g. 16x16, 256x256) texture but didn't receive one") 580 res=tex.shape[0] 581 if useShaders: 582 dataType=GL.GL_FLOAT 583 elif tex in [None,"none", "None"]: 584 res = 1 #4x4 (2x2 is SUPPOSED to be fine but generates weird colors!) 585 intensity = numpy.ones([res,res],numpy.float32) 586 wasLum = True 587 elif tex == "sin": 588 onePeriodX, onePeriodY = numpy.mgrid[0:res, 0:2*pi:1j*res]# NB 1j*res is a special mgrid notation 589 intensity = numpy.sin(onePeriodY-pi/2) 590 wasLum = True 591 elif tex == "sqr":#square wave (symmetric duty cycle) 592 onePeriodX, onePeriodY = numpy.mgrid[0:res, 0:2*pi:1j*res]# NB 1j*res is a special mgrid notation 593 sinusoid = numpy.sin(onePeriodY-pi/2) 594 intensity = numpy.where(sinusoid>0, 1, -1) 595 wasLum = True 596 elif tex == "saw": 597 intensity = numpy.linspace(-1.0,1.0,res,endpoint=True)*numpy.ones([res,1]) 598 wasLum = True 599 elif tex == "tri": 600 intensity = numpy.linspace(-1.0,3.0,res,endpoint=True)#-1:3 means the middle is at +1 601 intensity[int(res/2.0+1):] = 2.0-intensity[int(res/2.0+1):]#remove from 3 to get back down to -1 602 intensity = intensity*numpy.ones([res,1])#make 2D 603 wasLum = True 604 elif tex == "sinXsin": 605 onePeriodX, onePeriodY = numpy.mgrid[0:2*pi:1j*res, 0:2*pi:1j*res]# NB 1j*res is a special mgrid notation 606 intensity = numpy.sin(onePeriodX-pi/2)*numpy.sin(onePeriodY-pi/2) 607 wasLum = True 608 elif tex == "sqrXsqr": 609 onePeriodX, onePeriodY = numpy.mgrid[0:2*pi:1j*res, 0:2*pi:1j*res]# NB 1j*res is a special mgrid notation 610 sinusoid = numpy.sin(onePeriodX-pi/2)*numpy.sin(onePeriodY-pi/2) 611 intensity = numpy.where(sinusoid>0, 1, -1) 612 wasLum = True 613 elif tex == "circle": 614 rad=makeRadialMatrix(res) 615 intensity = (rad<=1)*2-1 616 wasLum=True 617 elif tex == "gauss": 618 rad=makeRadialMatrix(res) 619 intensity = numpy.exp( -rad**2.0 / (2.0*(1.0 / allMaskParams['sd'])**2.0) )*2-1 #3sd.s by the edge of the stimulus 620 wasLum=True 621 elif tex == "cross": 622 X, Y = numpy.mgrid[-1:1:1j*res, -1:1:1j*res] 623 tf_neg_cross = ((X < -0.2) & (Y < -0.2)) | ((X < -0.2) & (Y > 0.2)) | ((X > 0.2) & (Y < -0.2)) | ((X > 0.2) & (Y > 0.2)) 624 #tf_neg_cross == True at places where the cross is transparent, i.e. the four corners 625 intensity = numpy.where(tf_neg_cross, -1, 1) 626 wasLum = True 627 elif tex == "radRamp":#a radial ramp 628 rad=makeRadialMatrix(res) 629 intensity = 1-2*rad 630 intensity = numpy.where(rad<-1, intensity, -1)#clip off the corners (circular) 631 wasLum=True 632 elif tex == "raisedCos": # A raised cosine 633 wasLum=True 634 hamming_len = 1000 # This affects the 'granularity' of the raised cos 635 636 rad = makeRadialMatrix(res) 637 intensity = numpy.zeros_like(rad) 638 intensity[numpy.where(rad < 1)] = 1 639 raised_cos_idx = numpy.where( 640 [numpy.logical_and(rad <= 1, rad >= 1 - allMaskParams['fringeWidth'])])[1:] 641 642 # Make a raised_cos (half a hamming window): 643 raised_cos = numpy.hamming(hamming_len)[:hamming_len/2] 644 raised_cos -= numpy.min(raised_cos) 645 raised_cos /= numpy.max(raised_cos) 646 647 # Measure the distance from the edge - this is your index into the hamming window: 648 d_from_edge = numpy.abs((1 - allMaskParams['fringeWidth']) - rad[raised_cos_idx]) 649 d_from_edge /= numpy.max(d_from_edge) 650 d_from_edge *= numpy.round(hamming_len/2) 651 652 # This is the indices into the hamming (larger for small distances from the edge!): 653 portion_idx = (-1 * d_from_edge).astype(int) 654 655 # Apply the raised cos to this portion: 656 intensity[raised_cos_idx] = raised_cos[portion_idx] 657 658 # Scale it into the interval -1:1: 659 intensity = intensity - 0.5 660 intensity = intensity / numpy.max(intensity) 661 662 #Sometimes there are some remaining artifacts from this process, get rid of them: 663 artifact_idx = numpy.where(numpy.logical_and(intensity == -1, 664 rad < 0.99)) 665 intensity[artifact_idx] = 1 666 artifact_idx = numpy.where(numpy.logical_and(intensity == 1, rad > 667 0.99)) 668 intensity[artifact_idx] = 0 669 670 else: 671 if type(tex) in [str, unicode, numpy.string_]: 672 # maybe tex is the name of a file: 673 if not os.path.isfile(tex): 674 logging.error("Couldn't find image file '%s'; check path?" %(tex)); logging.flush() 675 raise OSError, "Couldn't find image file '%s'; check path? (tried: %s)" \ 676 % (tex, os.path.abspath(tex))#ensure we quit 677 try: 678 im = Image.open(tex) 679 im = im.transpose(Image.FLIP_TOP_BOTTOM) 680 except IOError: 681 logging.error("Found file '%s' but failed to load as an image" %(tex)); logging.flush() 682 raise IOError, "Found file '%s' [= %s] but it failed to load as an image" \ 683 % (tex, os.path.abspath(tex))#ensure we quit 684 else: 685 # can't be a file; maybe its an image already in memory? 686 try: 687 im = tex.copy().transpose(Image.FLIP_TOP_BOTTOM) # ? need to flip if in mem? 688 except AttributeError: # nope, not an image in memory 689 logging.error("Couldn't make sense of requested image."); logging.flush() 690 raise AttributeError, "Couldn't make sense of requested image."#ensure we quit 691 # at this point we have a valid im 692 stim._origSize=im.size 693 wasImage=True 694 #is it 1D? 695 if im.size[0]==1 or im.size[1]==1: 696 logging.error("Only 2D textures are supported at the moment") 697 else: 698 maxDim = max(im.size) 699 powerOf2 = int(2**numpy.ceil(numpy.log2(maxDim))) 700 if im.size[0]!=powerOf2 or im.size[1]!=powerOf2: 701 if not forcePOW2: 702 notSqr=True 703 elif glob_vars.nImageResizes<reportNImageResizes: 704 logging.warning("Image '%s' was not a square power-of-two image. Linearly interpolating to be %ix%i" %(tex, powerOf2, powerOf2)) 705 glob_vars.nImageResizes+=1 706 im=im.resize([powerOf2,powerOf2],Image.BILINEAR) 707 elif glob_vars.nImageResizes==reportNImageResizes: 708 logging.warning("Multiple images have needed resizing - I'll stop bothering you!") 709 im=im.resize([powerOf2,powerOf2],Image.BILINEAR) 710 #is it Luminance or RGB? 711 if pixFormat==GL.GL_ALPHA and im.mode!='L':#we have RGB and need Lum 712 wasLum = True 713 im = im.convert("L")#force to intensity (in case it was rgb) 714 elif im.mode=='L': #we have lum and no need to change 715 wasLum = True 716 if useShaders: 717 dataType=GL.GL_FLOAT 718 elif pixFormat==GL.GL_RGB: #we want RGB and might need to convert from CMYK or Lm 719 #texture = im.tostring("raw", "RGB", 0, -1) 720 im = im.convert("RGBA") 721 wasLum=False 722 if dataType==GL.GL_FLOAT: 723 #convert from ubyte to float 724 intensity = numpy.array(im).astype(numpy.float32)*0.0078431372549019607-1.0 # much faster to avoid division 2/255 725 else: 726 intensity = numpy.array(im) 727 if pixFormat==GL.GL_RGB and wasLum and dataType==GL.GL_FLOAT: #grating stim on good machine 728 #keep as float32 -1:1 729 if sys.platform!='darwin' and stim.win.glVendor.startswith('nvidia'): 730 #nvidia under win/linux might not support 32bit float 731 internalFormat = GL.GL_RGB16F_ARB #could use GL_LUMINANCE32F_ARB here but check shader code? 732 else:#we've got a mac or an ATI card and can handle 32bit float textures 733 internalFormat = GL.GL_RGB32F_ARB #could use GL_LUMINANCE32F_ARB here but check shader code? 734 data = numpy.ones((intensity.shape[0],intensity.shape[1],3),numpy.float32)#initialise data array as a float 735 data[:,:,0] = intensity#R 736 data[:,:,1] = intensity#G 737 data[:,:,2] = intensity#B 738 elif pixFormat==GL.GL_RGB and wasLum and dataType!=GL.GL_FLOAT and stim.useShaders: 739 #was a lum image: stick with ubyte for speed 740 internalFormat = GL.GL_RGB 741 data = numpy.ones((intensity.shape[0],intensity.shape[1],3),numpy.ubyte)#initialise data array as a float 742 data[:,:,0] = intensity#R 743 data[:,:,1] = intensity#G 744 data[:,:,2] = intensity#B 745 elif pixFormat==GL.GL_RGB and wasLum and not stim.useShaders: #Grating on legacy hardware, or ImageStim with wasLum=True 746 #scale by rgb and convert to ubyte 747 internalFormat = GL.GL_RGB 748 if stim.colorSpace in ['rgb', 'dkl', 'lms','hsv']: 749 rgb=stim.rgb 750 else: 751 rgb=stim.rgb/127.5-1.0#colour is not a float - convert to float to do the scaling 752 # if wasImage it will also have ubyte values for the intensity 753 if wasImage: 754 intensity = intensity/127.5-1.0 755 #scale by rgb 756 data = numpy.ones((intensity.shape[0],intensity.shape[1],4),numpy.float32)#initialise data array as a float 757 data[:,:,0] = intensity*rgb[0] + stim.rgbPedestal[0]#R 758 data[:,:,1] = intensity*rgb[1] + stim.rgbPedestal[1]#G 759 data[:,:,2] = intensity*rgb[2] + stim.rgbPedestal[2]#B 760 data[:,:,:-1] = data[:,:,:-1]*stim.contrast 761 #convert to ubyte 762 data = float_uint8(data) 763 elif pixFormat==GL.GL_RGB and dataType==GL.GL_FLOAT: #probably a custom rgb array or rgb image 764 internalFormat = GL.GL_RGB32F_ARB 765 data = intensity 766 elif pixFormat==GL.GL_RGB:# not wasLum, not useShaders - an RGB bitmap with no shader optionsintensity.min() 767 internalFormat = GL.GL_RGB 768 data = intensity #float_uint8(intensity) 769 elif pixFormat==GL.GL_ALPHA: 770 internalFormat = GL.GL_ALPHA 771 dataType = GL.GL_UNSIGNED_BYTE 772 if wasImage: 773 data = intensity 774 else: 775 data = float_uint8(intensity) 776 #check for RGBA textures 777 if len(data.shape)>2 and data.shape[2] == 4: 778 if pixFormat==GL.GL_RGB: 779 pixFormat=GL.GL_RGBA 780 if internalFormat==GL.GL_RGB: 781 internalFormat=GL.GL_RGBA 782 elif internalFormat==GL.GL_RGB32F_ARB: 783 internalFormat=GL.GL_RGBA32F_ARB 784 texture = data.ctypes#serialise 785 786 #bind the texture in openGL 787 GL.glEnable(GL.GL_TEXTURE_2D) 788 GL.glBindTexture(GL.GL_TEXTURE_2D, id)#bind that name to the target 789 GL.glTexParameteri(GL.GL_TEXTURE_2D,GL.GL_TEXTURE_WRAP_S,GL.GL_REPEAT) #makes the texture map wrap (this is actually default anyway) 790 GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1) # data from PIL/numpy is packed, but default for GL is 4 bytes 791 #important if using bits++ because GL_LINEAR 792 #sometimes extrapolates to pixel vals outside range 793 if interpolate: 794 GL.glTexParameteri(GL.GL_TEXTURE_2D,GL.GL_TEXTURE_MAG_FILTER,GL.GL_LINEAR) 795 if useShaders:#GL_GENERATE_MIPMAP was only available from OpenGL 1.4 796 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR) 797 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_GENERATE_MIPMAP, GL.GL_TRUE) 798 GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, internalFormat, 799 data.shape[1],data.shape[0], 0, # [JRG] for non-square, want data.shape[1], data.shape[0] 800 pixFormat, dataType, texture) 801 else:#use glu 802 GL.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR_MIPMAP_NEAREST) 803 GL.gluBuild2DMipmaps(GL.GL_TEXTURE_2D, internalFormat, 804 data.shape[1],data.shape[0], pixFormat, dataType, texture) # [JRG] for non-square, want data.shape[1], data.shape[0] 805 else: 806 GL.glTexParameteri(GL.GL_TEXTURE_2D,GL.GL_TEXTURE_MAG_FILTER,GL.GL_NEAREST) 807 GL.glTexParameteri(GL.GL_TEXTURE_2D,GL.GL_TEXTURE_MIN_FILTER,GL.GL_NEAREST) 808 GL.glTexImage2D(GL.GL_TEXTURE_2D, 0, internalFormat, 809 data.shape[1],data.shape[0], 0, # [JRG] for non-square, want data.shape[1], data.shape[0] 810 pixFormat, dataType, texture) 811 GL.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_MODULATE)#?? do we need this - think not! 812 GL.glBindTexture(GL.GL_TEXTURE_2D, 0)#unbind our texture so that it doesn't affect other rendering 813 return wasLum 814 815 def clearTextures(self): 816 """ 817 Clear all textures associated with the stimulus. 818 As of v1.61.00 this is called automatically during garbage collection of 819 your stimulus, so doesn't need calling explicitly by the user. 820 """ 821 GL.glDeleteTextures(1, self._texID) 822 GL.glDeleteTextures(1, self._maskID) 823 824 @attributeSetter 825 def mask(self, value): 826 """The alpha mask (forming the shape of the image) 827 828 This can be one of various options: 829 + 'circle', 'gauss', 'raisedCos', 'cross', **None** (resets to default) 830 + the name of an image file (most formats supported) 831 + a numpy array (1xN or NxN) ranging -1:1 832 """ 833 self.__dict__['mask'] = value 834 if self.__class__.__name__ == 'ImageStim': 835 dataType = GL.GL_UNSIGNED_BYTE 836 else: 837 dataType = None 838 self._createTexture(value, id=self._maskID, pixFormat=GL.GL_ALPHA, dataType=dataType, 839 stim=self, res=self.texRes, maskParams=self.maskParams) 840 def setMask(self, value, log=None): 841 """Usually you can use 'stim.attribute = value' syntax instead, 842 but use this method if you need to suppress the log message. 843 """ 844 setAttribute(self, 'mask', value, log) 845 846 @attributeSetter 847 def texRes(self, value): 848 """Power-of-two int. Sets the resolution of the mask and texture. 849 texRes is overridden if an array or image is provided as mask. 850 851 :ref:`Operations <attrib-operations>` supported. 852 """ 853 self.__dict__['texRes'] = value 854 855 # ... now rebuild textures (call attributeSetters without logging). 856 if hasattr(self, 'tex'): 857 setAttribute(self, 'tex', self.tex, log=False) 858 if hasattr(self, 'mask'): 859 setAttribute(self, 'mask', self.mask, log=False) 860 861 @attributeSetter 862 def maskParams(self, value): 863 """Various types of input. Default to None. 864 This is used to pass additional parameters to the mask if those are needed. 865 866 - For 'gauss' mask, pass dict {'sd': 5} to control standard deviation. 867 - For the 'raisedCos' mask, pass a dict: {'fringeWidth':0.2}, 868 where 'fringeWidth' is a parameter (float, 0-1), determining 869 the proportion of the patch that will be blurred by the raised 870 cosine edge.""" 871 self.__dict__['maskParams'] = value 872 setAttribute(self, 'mask', self.mask, log=False) # call attributeSetter without log 873 874 @attributeSetter 875 def interpolate(self, value): 876 """Whether to interpolate (linearly) the texture in the stimulus 877 878 If set to False then nearest neighbour will be used when needed, 879 otherwise some form of interpolation will be used. 880 """ 881 self.__dict__['interpolate'] = value 882 883class WindowMixin(object): 884 """Window-related attributes and methods. 885 Used by BaseVisualStim, SimpleImageStim and ElementArrayStim.""" 886 887 @attributeSetter 888 def win(self, value): 889 """ The :class:`~psychopy.visual.Window` object in which the stimulus will be rendered 890 by default. (required) 891 892 Example, drawing same stimulus in two different windows and display 893 simultaneously. Assuming that you have two windows and a stimulus (win1, win2 and stim):: 894 895 stim.win = win1 # stimulus will be drawn in win1 896 stim.draw() # stimulus is now drawn to win1 897 stim.win = win2 # stimulus will be drawn in win2 898 stim.draw() # it is now drawn in win2 899 win1.flip(waitBlanking=False) # do not wait for next monitor update 900 win2.flip() # wait for vertical blanking. 901 902 Note that this just changes **default** window for stimulus. 903 You could also specify window-to-draw-to when drawing:: 904 905 stim.draw(win1) 906 stim.draw(win2) 907 """ 908 self.__dict__['win'] = value 909 910 @attributeSetter 911 def units(self, value): 912 """ 913 None, 'norm', 'cm', 'deg', 'degFlat', 'degFlatPos', or 'pix' 914 915 If None then the current units of the :class:`~psychopy.visual.Window` will be used. 916 See :ref:`units` for explanation of other options. 917 918 Note that when you change units, you don't change the stimulus parameters 919 and it is likely to change appearance. Example:: 920 921 # This stimulus is 20% wide and 50% tall with respect to window 922 stim = visual.PatchStim(win, units='norm', size=(0.2, 0.5) 923 924 # This stimulus is 0.2 degrees wide and 0.5 degrees tall. 925 stim.units = 'deg' 926 """ 927 if value != None and len(value): 928 self.__dict__['units'] = value 929 else: 930 self.__dict__['units'] = self.win.units 931 932 # Update size and position if they are defined (tested as numeric). If not, this is probably 933 # during some init and they will be defined later, given the new unit. 934 try: 935 self.size * self.pos # quick and dirty way to check that both are numeric. This avoids the heavier attributeSetter calls. 936 self.size = self.size 937 self.pos = self.pos 938 except: 939 pass 940 941 @attributeSetter 942 def useShaders(self, value): 943 """Should shaders be used to render the stimulus (typically leave as `True`) 944 945 If the system support the use of OpenGL shader language then leaving 946 this set to True is highly recommended. If shaders cannot be used then 947 various operations will be slower (notably, changes to stimulus color 948 or contrast) 949 """ 950 if value == True and self.win._haveShaders == False: 951 logging.error("Shaders were requested but aren't available. Shaders need OpenGL 2.0+ drivers") 952 if value != self.useShaders: # if there's a change... 953 self.__dict__['useShaders'] = value 954 if hasattr(self,'tex'): 955 self.tex = self.tex # calling attributeSetter 956 elif hasattr(self, 'mask'): 957 self.mask = self.mask # calling attributeSetter (does the same as mask) 958 if hasattr(self,'_imName'): 959 self.setIm(self._imName, log=False) 960 if self.__class__.__name__ == 'TextStim': 961 self._needSetText = True 962 self._needUpdate = True 963 def setUseShaders(self, value=True, log=None): 964 """Usually you can use 'stim.attribute = value' syntax instead, 965 but use this method if you need to suppress the log message""" 966 setAttribute(self, 'useShaders', value, log) # call attributeSetter 967 968 def draw(self): 969 raise NotImplementedError('Stimulus classes must override visual.BaseVisualStim.draw') 970 971 def _selectWindow(self, win): 972 #don't call switch if it's already the curr window 973 if win!=glob_vars.currWindow and win.winType=='pyglet': 974 win.winHandle.switch_to() 975 glob_vars.currWindow = win 976 977 def _updateList(self): 978 """ 979 The user shouldn't need this method since it gets called 980 after every call to .set() 981 Chooses between using and not using shaders each call. 982 """ 983 if self.useShaders: 984 self._updateListShaders() 985 else: 986 self._updateListNoShaders() 987 988class BaseVisualStim(MinimalStim, WindowMixin, LegacyVisualMixin): 989 """A template for a visual stimulus class. 990 991 Actual visual stim like GratingStim, TextStim etc... are based on this. 992 Not finished...? 993 994 Methods defined here will override Minimal & Legacy, but best to avoid 995 that for simplicity & clarity. 996 """ 997 def __init__(self, win, units=None, name='', autoLog=None): 998 self.autoLog = False # just to start off during init, set at end 999 self.win = win 1000 self.units = units 1001 self._rotationMatrix = [[1.,0.],[0.,1.]] #no rotation as a default 1002 # self.autoLog is set at end of MinimalStim.__init__ 1003 super(BaseVisualStim, self).__init__(name=name, autoLog=autoLog) 1004 if self.autoLog: 1005 logging.warning("%s is calling BaseVisualStim.__init__() with autolog=True. Set autoLog to True only at the end of __init__())" \ 1006 %(self.__class__.__name__)) 1007 1008 @attributeSetter 1009 def opacity(self, value): 1010 """Determines how visible the stimulus is relative to background 1011 1012 The value should be a single float ranging 1.0 (opaque) to 0.0 1013 (transparent). :ref:`Operations <attrib-operations>` are supported. 1014 Precisely how this is used depends on the :ref:`blendMode`. 1015 """ 1016 self.__dict__['opacity'] = value 1017 1018 if not 0 <= value <= 1 and self.autoLog: 1019 logging.warning('Setting opacity outside range 0.0 - 1.0 has no additional effect') 1020 1021 #opacity is coded by the texture, if not using shaders 1022 if hasattr(self, 'useShaders') and not self.useShaders: 1023 if hasattr(self,'mask'): 1024 self.mask = self.mask # call attributeSetter 1025 1026 @attributeSetter 1027 def ori(self, value): 1028 """The orientation of the stimulus (in degrees). 1029 1030 Should be a single value (:ref:`scalar <attrib-scalar>`). :ref:`Operations <attrib-operations>` are supported. 1031 1032 Orientation convention is like a clock: 0 is vertical, and positive 1033 values rotate clockwise. Beyond 360 and below zero values wrap 1034 appropriately. 1035 1036 """ 1037 self.__dict__['ori'] = value 1038 radians = value*0.017453292519943295 1039 self._rotationMatrix = numpy.array([[numpy.cos(radians), -numpy.sin(radians)], 1040 [numpy.sin(radians), numpy.cos(radians)]]) 1041 self._needVertexUpdate=True #need to update update vertices 1042 self._needUpdate = True 1043 1044 @attributeSetter 1045 def size(self, value): 1046 """The size (width, height) of the stimulus in the stimulus :ref:`units <units>` 1047 1048 Value should be :ref:`x,y-pair <attrib-xy>`, :ref:`scalar <attrib-scalar>` (applies to both dimensions) 1049 or None (resets to default). :ref:`Operations <attrib-operations>` are supported. 1050 1051 Sizes can be negative (causing a mirror-image reversal) and can extend beyond the window. 1052 1053 Example:: 1054 1055 stim.size = 0.8 # Set size to (xsize, ysize) = (0.8, 0.8), quadratic. 1056 print(stim.size) # Outputs array([0.8, 0.8]) 1057 stim.size += (0.5, -0.5) # make wider and flatter. Is now (1.3, 0.3) 1058 1059 Tip: if you can see the actual pixel range this corresponds to by 1060 looking at `stim._sizeRendered` 1061 """ 1062 value = val2array(value) # Check correct user input 1063 self._requestedSize = value #to track whether we're just using a default 1064 # None --> set to default 1065 if value is None: 1066 """Set the size to default (e.g. to the size of the loaded image etc)""" 1067 #calculate new size 1068 if self._origSize is None: #not an image from a file 1069 value = numpy.array([0.5, 0.5]) #this was PsychoPy's original default 1070 else: 1071 #we have an image - calculate the size in `units` that matches original pixel size 1072 if self.units == 'pix': 1073 value = numpy.array(self._origSize) 1074 elif self.units in ['deg', 'degFlatPos', 'degFlat']: 1075 #NB when no size has been set (assume to use orig size in pix) this should not 1076 #be corrected for flat anyway, so degFlat==degFlatPos 1077 value = pix2deg(numpy.array(self._origSize, float), self.win.monitor) 1078 elif self.units == 'norm': 1079 value = 2 * numpy.array(self._origSize, float) / self.win.size 1080 elif self.units == 'height': 1081 value = numpy.array(self._origSize, float) / self.win.size[1] 1082 elif self.units == 'cm': 1083 value = pix2cm(numpy.array(self._origSize, float), self.win.monitor) 1084 else: 1085 raise AttributeError, "Failed to create default size for ImageStim. Unsupported unit, %s" %(repr(self.units)) 1086 self.__dict__['size'] = value 1087 self._needVertexUpdate=True 1088 self._needUpdate = True 1089 if hasattr(self, '_calcCyclesPerStim'): 1090 self._calcCyclesPerStim() 1091 1092 @attributeSetter 1093 def pos(self, value): 1094 """The position of the center of the stimulus in the stimulus :ref:`units <units>` 1095 1096 `value` should be an :ref:`x,y-pair <attrib-xy>`. :ref:`Operations <attrib-operations>` 1097 are also supported. 1098 1099 Example:: 1100 1101 stim.pos = (0.5, 0) # Set slightly to the right of center 1102 stim.pos += (0.5, -1) # Increment pos rightwards and upwards. Is now (1.0, -1.0) 1103 stim.pos *= 0.2 # Move stim towards the center. Is now (0.2, -0.2) 1104 1105 Tip: If you need the position of stim in pixels, you can obtain it like this: 1106 1107 from psychopy.tools.monitorunittools import posToPix 1108 posPix = posToPix(stim) 1109 """ 1110 self.__dict__['pos'] = val2array(value, False, False) 1111 self._needVertexUpdate=True 1112 self._needUpdate = True 1113 1114 def setPos(self, newPos, operation='', log=None): 1115 """Usually you can use 'stim.attribute = value' syntax instead, 1116 but use this method if you need to suppress the log message 1117 """ 1118 setAttribute(self, 'pos', val2array(newPos, False), log, operation) 1119 def setDepth(self, newDepth, operation='', log=None): 1120 """Usually you can use 'stim.attribute = value' syntax instead, 1121 but use this method if you need to suppress the log message 1122 """ 1123 setAttribute(self, 'depth', newDepth, log, operation) 1124 def setSize(self, newSize, operation='', units=None, log=None): 1125 """Usually you can use 'stim.attribute = value' syntax instead, 1126 but use this method if you need to suppress the log message 1127 """ 1128 if units is None: 1129 units=self.units#need to change this to create several units from one 1130 setAttribute(self, 'size', val2array(newSize, False), log, operation) 1131 def setOri(self, newOri, operation='', log=None): 1132 """Usually you can use 'stim.attribute = value' syntax instead, 1133 but use this method if you need to suppress the log message 1134 """ 1135 setAttribute(self, 'ori', newOri, log, operation) 1136 def setOpacity(self, newOpacity, operation='', log=None): 1137 """Usually you can use 'stim.attribute = value' syntax instead, 1138 but use this method if you need to suppress the log message 1139 """ 1140 setAttribute(self, 'opacity', newOpacity, log, operation) 1141 def _set(self, attrib, val, op='', log=None): 1142 """DEPRECATED since 1.80.04 + 1. Use setAttribute() and val2array() instead.""" 1143 #format the input value as float vectors 1144 if type(val) in [tuple, list, numpy.ndarray]: 1145 val = val2array(val) 1146 1147 # Set attribute with operation and log 1148 setAttribute(self, attrib, val, log, op) 1149 1150 # For DotStim 1151 if attrib in ['nDots','coherence']: 1152 self.coherence=round(self.coherence*self.nDots)/self.nDots 1153