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