#!/usr/bin/env python# -*- coding: utf-8 -*-"""Display an image on `psycopy.visual.Window`"""# Part of the PsychoPy library# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2024 Open Science Tools Ltd.# Distributed under the terms of the GNU General Public License (GPL).# Ensure setting pyglet.options['debug_gl'] to False is done prior to any# other calls to pyglet or pyglet submodules, otherwise it may not get picked# up by the pyglet GL engine and have no effect.# Shaders will work but require OpenGL2.0 drivers AND PyOpenGL3.0+importpygletfrompsychopy.layoutimportSizepyglet.options['debug_gl']=FalseimportctypesGL=pyglet.glimportnumpyfromfractionsimportFractionimportpsychopy# so we can get the __path__frompsychopyimportlogging,colors,layoutfrompsychopy.tools.attributetoolsimportattributeSetter,setAttributefrompsychopy.visual.basevisualimport(BaseVisualStim,DraggingMixin,ContainerMixin,ColorMixin,TextureMixin)
[docs]classImageStim(BaseVisualStim,DraggingMixin,ContainerMixin,ColorMixin,TextureMixin):"""Display an image on a :class:`psychopy.visual.Window` """def__init__(self,win,image=None,mask=None,units="",pos=(0.0,0.0),size=None,anchor="center",ori=0.0,color=(1.0,1.0,1.0),colorSpace='rgb',contrast=1.0,opacity=None,depth=0,interpolate=False,draggable=False,flipHoriz=False,flipVert=False,texRes=128,name=None,autoLog=None,maskParams=None):""" """# Empty docstring. All doc is in attributes# what local vars are defined (these are the init params) for use by# __repr__self._initParams=dir()self._initParams.remove('self')super(ImageStim,self).__init__(win,units=units,name=name,autoLog=False)# set at end of initself.draggable=draggable# use shaders if available by default, this is a good thingself.__dict__['useShaders']=win._haveShaders# initialise textures for stimulusself._texID=GL.GLuint()GL.glGenTextures(1,ctypes.byref(self._texID))self._maskID=GL.GLuint()GL.glGenTextures(1,ctypes.byref(self._maskID))self._pixbuffID=GL.GLuint()GL.glGenBuffers(1,ctypes.byref(self._pixbuffID))self.__dict__['maskParams']=maskParamsself.__dict__['mask']=mask# Not pretty (redefined later) but it works!self.__dict__['texRes']=texRes# Other stuffself._imName=imageself.isLumImage=Noneself.interpolate=interpolateself.vertices=Noneself.anchor=anchorself.flipHoriz=flipHorizself.flipVert=flipVertself._requestedSize=sizeself._origSize=None# updated if an image texture gets loadedself.size=sizeself.pos=numpy.array(pos,float)self.ori=float(ori)self.depth=depth# color and contrast etcself.rgbPedestal=[0,0,0]# does an rgb pedestal make sense for an image?self.colorSpace=colorSpace# omit decoratorself.color=colorself.contrast=float(contrast)self.opacity=opacity# Set the image and mask-self.setImage(image,log=False)self.texRes=texRes# rebuilds the maskself.size=size# generate a displaylist IDself._listID=GL.glGenLists(1)self._updateList()# ie refresh display list# set autoLog now that params have been initialisedwantLog=autoLogisNoneandself.win.autoLogself.__dict__['autoLog']=autoLogorwantLogifself.autoLog:logging.exp("Created %s = %s"%(self.name,str(self)))
[docs]def_updateListShaders(self):""" The user shouldn't need this method since it gets called after every call to .set() Basically it updates the OpenGL representation of your stimulus if some parameter of the stimulus changes. Call it if you change a property manually rather than using the .set() command """self._needUpdate=FalseGL.glNewList(self._listID,GL.GL_COMPILE)# setup the shaderprogramifself.isLumImage:# for a luminance image do recoloring_prog=self.win._progSignedTexMaskGL.glUseProgram(_prog)# set the texture to be texture unit 0GL.glUniform1i(GL.glGetUniformLocation(_prog,b"texture"),0)# mask is texture unit 1GL.glUniform1i(GL.glGetUniformLocation(_prog,b"mask"),1)else:# for an rgb image there is no recoloring_prog=self.win._progImageStimGL.glUseProgram(_prog)# set the texture to be texture unit 0GL.glUniform1i(GL.glGetUniformLocation(_prog,b"texture"),0)# mask is texture unit 1GL.glUniform1i(GL.glGetUniformLocation(_prog,b"mask"),1)# maskGL.glActiveTexture(GL.GL_TEXTURE1)GL.glBindTexture(GL.GL_TEXTURE_2D,self._maskID)GL.glEnable(GL.GL_TEXTURE_2D)# implicitly disables 1D# main textureGL.glActiveTexture(GL.GL_TEXTURE0)GL.glBindTexture(GL.GL_TEXTURE_2D,self._texID)GL.glEnable(GL.GL_TEXTURE_2D)# access just once because it's slower than basic propertyvertsPix=self.verticesPixGL.glBegin(GL.GL_QUADS)# draw a 4 sided polygon# right bottomGL.glMultiTexCoord2f(GL.GL_TEXTURE0,1,0)GL.glMultiTexCoord2f(GL.GL_TEXTURE1,1,0)GL.glVertex2f(vertsPix[0,0],vertsPix[0,1])# left bottomGL.glMultiTexCoord2f(GL.GL_TEXTURE0,0,0)GL.glMultiTexCoord2f(GL.GL_TEXTURE1,0,0)GL.glVertex2f(vertsPix[1,0],vertsPix[1,1])# left topGL.glMultiTexCoord2f(GL.GL_TEXTURE0,0,1)GL.glMultiTexCoord2f(GL.GL_TEXTURE1,0,1)GL.glVertex2f(vertsPix[2,0],vertsPix[2,1])# right topGL.glMultiTexCoord2f(GL.GL_TEXTURE0,1,1)GL.glMultiTexCoord2f(GL.GL_TEXTURE1,1,1)GL.glVertex2f(vertsPix[3,0],vertsPix[3,1])GL.glEnd()# unbind the texturesGL.glActiveTexture(GL.GL_TEXTURE1)GL.glBindTexture(GL.GL_TEXTURE_2D,0)GL.glDisable(GL.GL_TEXTURE_2D)# implicitly disables 1D# main textureGL.glActiveTexture(GL.GL_TEXTURE0)GL.glBindTexture(GL.GL_TEXTURE_2D,0)GL.glDisable(GL.GL_TEXTURE_2D)GL.glUseProgram(0)GL.glEndList()
def__del__(self):"""Remove textures from graphics card to prevent crash """try:ifhasattr(self,'_listID'):GL.glDeleteLists(self._listID,1)self.clearTextures()except(ImportError,ModuleNotFoundError,TypeError):pass# has probably been garbage-collected already
[docs]defdraw(self,win=None):"""Draw. """# check the type of image we're dealing withif(type(self.image)!=numpy.ndarrayandself.imagein(None,"None","none")):return# make the context for the window currentifwinisNone:win=self.winself._selectWindow(win)# If our image is a movie stim object, pull pixel data from the most# recent frame and write it to the memoryifhasattr(self.image,'getVideoFrame'):videoFrame=self.image.getVideoFrame()ifvideoFrameisnotNone:self._movieFrameToTexture(videoFrame)GL.glPushMatrix()# push before the list, pop afterwin.setScale('pix')GL.glColor4f(*self._foreColor.render('rgba1'))ifself._needTextureUpdate:self.setImage(value=self._imName,log=False)ifself._needUpdate:self._updateList()GL.glCallList(self._listID)# return the view to previous stateGL.glPopMatrix()
[docs]def_movieFrameToTexture(self,movieSrc):"""Convert a movie frame to a texture and use it. This method is used internally to copy pixel data from a camera object into a texture. This enables the `ImageStim` to be used as a 'viewfinder' of sorts for the camera to view a live video stream on a window. Parameters ---------- movieSrc : `~psychopy.hardware.camera.Camera` Movie source object. """# get the most recent video frame and extract color datacolorData=movieSrc.colorData# get the size of the movie frame and compute the buffer sizevidWidth,vidHeight=movieSrc.sizenBufferBytes=vidWidth*vidHeight*3# bind pixel unpack bufferGL.glBindBuffer(GL.GL_PIXEL_UNPACK_BUFFER,self._pixbuffID)# Free last storage buffer before mapping and writing new frame# data. This allows the GPU to process the extant buffer in VRAM# uploaded last cycle without being stalled by the CPU accessing it.GL.glBufferData(GL.GL_PIXEL_UNPACK_BUFFER,nBufferBytes*ctypes.sizeof(GL.GLubyte),None,GL.GL_STREAM_DRAW)# Map the buffer to client memory, `GL_WRITE_ONLY` to tell the# driver to optimize for a one-way write operation if it can.bufferPtr=GL.glMapBuffer(GL.GL_PIXEL_UNPACK_BUFFER,GL.GL_WRITE_ONLY)bufferArray=numpy.ctypeslib.as_array(ctypes.cast(bufferPtr,ctypes.POINTER(GL.GLubyte)),shape=(nBufferBytes,))# copy databufferArray[:]=colorData[:]# Very important that we unmap the buffer data after copying, but# keep the buffer bound for setting the texture.GL.glUnmapBuffer(GL.GL_PIXEL_UNPACK_BUFFER)# bind the texture in OpenGLGL.glEnable(GL.GL_TEXTURE_2D)GL.glActiveTexture(GL.GL_TEXTURE0)GL.glBindTexture(GL.GL_TEXTURE_2D,self._texID)# copy the PBO to the textureGL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT,1)GL.glTexSubImage2D(GL.GL_TEXTURE_2D,0,0,0,vidWidth,vidHeight,GL.GL_RGB,GL.GL_UNSIGNED_BYTE,0)# point to the presently bound buffer# update texture filtering only if neededifself.interpolate:texFilter=GL.GL_LINEARelse:texFilter=GL.GL_NEARESTGL.glTexParameteri(GL.GL_TEXTURE_2D,GL.GL_TEXTURE_MAG_FILTER,texFilter)GL.glTexParameteri(GL.GL_TEXTURE_2D,GL.GL_TEXTURE_MIN_FILTER,texFilter)# important to unbind the PBOGL.glBindBuffer(GL.GL_PIXEL_UNPACK_BUFFER,0)GL.glBindTexture(GL.GL_TEXTURE_2D,0)GL.glDisable(GL.GL_TEXTURE_2D)
@attributeSetterdefimage(self,value):"""The image file to be presented (most formats supported). This can be a path-like object to an image file, or a numpy array of shape [H, W, C] where C are channels. The third dim will usually have length 1 (defining an intensity-only image), 3 (defining an RGB image) or 4 (defining an RGBA image). If passing a numpy array to the image attribute, the size attribute of ImageStim must be set explicitly. """self.__dict__['image']=self._imName=value# If given a color array, get it in rgb1ifisinstance(value,colors.Color):value=value.render('rgb1')wasLumImage=self.isLumImageiftype(value)!=numpy.ndarrayandvalue=="color":datatype=GL.GL_FLOATelse:datatype=GL.GL_UNSIGNED_BYTEiftype(value)!=numpy.ndarrayandvaluein(None,"None","none"):self.isLumImage=Trueelse:self.isLumImage=self._createTexture(value,id=self._texID,stim=self,pixFormat=GL.GL_RGB,dataType=datatype,maskParams=self.maskParams,forcePOW2=False,wrapping=False)# update sizeself.size=self._requestedSizeifhasattr(value,'getVideoFrame'):# make sure we invert verticesself.flipVert=True# if we switched to/from lum image then need to update shader ruleifwasLumImage!=self.isLumImage:self._needUpdate=Trueself._needTextureUpdate=False
[docs]defsetImage(self,value,log=None):"""Usually you can use 'stim.attribute = value' syntax instead, but use this method if you need to suppress the log message. """setAttribute(self,'image',value,log)
@propertydefaspectRatio(self):""" Aspect ratio of original image, before taking into account the `.size` attribute of this object. returns : Aspect ratio as a (w, h) tuple, simplified using the smallest common denominator (e.g. 1080x720 pixels becomes (3, 2)) """# Return None if we don't have a texture yetif(nothasattr(self,"_origSize"))orself._origSizeisNone:return# Work out aspect ratio (w/h)frac=Fraction(*self._origSize)returnfrac.numerator,frac.denominator@propertydefsize(self):returnBaseVisualStim.size.fget(self)@size.setterdefsize(self,value):# store requested sizeself._requestedSize=valueisNone=numpy.asarray(value)==Noneif(self.aspectRatioisnotNone)and(isNone.any())and(notisNone.all()):# If only one value is None, replace it with a value which maintains aspect ratiopix=layout.Size(value,units=self.units,win=self.win).pix# Replace None value with scaled pix valuei=isNone.argmax()ni=isNone.argmin()pix[i]=pix[ni]*self.aspectRatio[i]/self.aspectRatio[ni]# Recreate layout object from pixvalue=layout.Size(pix,units="pix",win=self.win)elif(self.aspectRatioisnotNone)and(isNone.all()):# If both values are None, use pixel sizevalue=layout.Size(self._origSize,units="pix",win=self.win)# Do base settingBaseVisualStim.size.fset(self,value)@attributeSetterdefmask(self,value):"""The alpha mask that can be used to control the outer shape of the stimulus + **None**, 'circle', 'gauss', 'raisedCos' + or the name of an image file (most formats supported) + or a numpy array (1xN or NxN) ranging -1:1 """self.__dict__['mask']=valueself._createTexture(value,id=self._maskID,pixFormat=GL.GL_ALPHA,dataType=GL.GL_UNSIGNED_BYTE,stim=self,res=self.texRes,maskParams=self.maskParams,forcePOW2=False,wrapping=True)
[docs]defsetMask(self,value,log=None):"""Usually you can use 'stim.attribute = value' syntax instead, but use this method if you need to suppress the log message. """setAttribute(self,'mask',value,log)