Source code for psychopy.visual.simpleimage

#!/usr/bin/env python
# -*- coding: utf-8 -*-

"""A simple stimulus for loading images from a file and presenting at exactly
the resolution and color in the file (subject to gamma correction if set).
"""

# 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).

import os

# 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+
import pyglet

from ..layout import Size

pyglet.options['debug_gl'] = False
GL = pyglet.gl

import psychopy  # so we can get the __path__
from psychopy import core, logging

# tools must only be imported *after* event or MovieStim breaks on win32
# (JWP has no idea why!)
from psychopy.tools.arraytools import val2array
from psychopy.tools.monitorunittools import convertToPix
from psychopy.tools.attributetools import setAttribute, attributeSetter
from psychopy.tools.filetools import pathToString
from psychopy.visual.basevisual import MinimalStim, WindowMixin
from . import globalVars

try:
    from PIL import Image
except ImportError:
    from . import Image

import numpy


[docs]class SimpleImageStim(MinimalStim, WindowMixin): """A simple stimulus for loading images from a file and presenting at exactly the resolution and color in the file (subject to gamma correction if set). This is a lazy-imported class, therefore import using full path `from psychopy.visual.simpleimage import SimpleImageStim` when inheriting from it. Unlike the ImageStim, this type of stimulus cannot be rescaled, rotated or masked (although flipping horizontally or vertically is possible). Drawing will also tend to be marginally slower, because the image isn't preloaded to the graphics card. The slight advantage, however is that the stimulus will always be in its original aspect ratio, with no interplotation or other transformation, and it is slightly faster to load into PsychoPy. """ def __init__(self, win, image="", units="", pos=(0.0, 0.0), flipHoriz=False, flipVert=False, name=None, autoLog=None): """ """ # all doc is in the attributeSetter # what local vars are defined (these are the init params) for use by # __repr__ self._initParams = dir() self._initParams.remove('self') self.autoLog = False self.__dict__['win'] = win super(SimpleImageStim, self).__init__(name=name) self.units = units # call attributeSetter # call attributeSetter. Use shaders if available by default, this is a # good thing self.pos = pos # call attributeSetter self.image = image # call attributeSetter # check image size against window size print(self.size) if (self.size[0] > self.win.size[0] or self.size[1] > self.win.size[1]): msg = ("Image size (%s, %s) was larger than window size " "(%s, %s). Will draw black screen.") logging.warning(msg % (self.size[0], self.size[1], self.win.size[0], self.win.size[1])) # check position with size, warn if stimuli not fully drawn if ((self.pos[0] + self.size[0]/2) > self.win.size[0]/2 or (self.pos[0] - self.size[0]/2) < -self.win.size[0]/2): logging.warning("The image does not completely fit inside " "the window in the X direction.") if ((self.pos[1] + self.size[1]/2) > self.win.size[1]/2 or (self.pos[1] - self.size[1]/2) < -self.win.size[1]/2): logging.warning("The image does not completely fit inside " "the window in the Y direction.") # flip if necessary # initially it is false, then so the flip according to arg above self.__dict__['flipHoriz'] = False self.flipHoriz = flipHoriz # call attributeSetter # initially it is false, then so the flip according to arg above self.__dict__['flipVert'] = False self.flipVert = flipVert # call attributeSetter self._calcPosRendered() # set autoLog (now that params have been initialised) wantLog = autoLog is None and self.win.autoLog self.__dict__['autoLog'] = autoLog or wantLog if self.autoLog: logging.exp("Created {} = {}".format(self.name, self)) @attributeSetter def flipHoriz(self, value): """True/False. If set to True then the image will be flipped horizontally (left-to-right). Note that this is relative to the original image, not relative to the current state. """ if value != self.flipHoriz: # We need to make the flip # Numpy and pyglet disagree about ori so ud<=>lr self.imArray = numpy.flipud(self.imArray) self.__dict__['flipHoriz'] = value self._needStrUpdate = True def setFlipHoriz(self, newVal=True, 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, 'flipHoriz', newVal, log) @attributeSetter def flipVert(self, value): """True/False. If set to True then the image will be flipped vertically (top-to-bottom). Note that this is relative to the original image, not relative to the current state. """ if value != self.flipVert: # We need to make the flip # Numpy and pyglet disagree about ori so ud<=>lr self.imArray = numpy.fliplr(self.imArray) self.__dict__['flipVert'] = value self._needStrUpdate = True def setFlipVert(self, newVal=True, 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, 'flipVert', newVal, log) def _updateImageStr(self): self._imStr = self.imArray.tobytes() self._needStrUpdate = False def draw(self, win=None): """ Draw the stimulus in its relevant window. You must call this method after every MyWin.flip() if you want the stimulus to appear on that frame and then update the screen again. """ if win is None: win = self.win self._selectWindow(win) # push the projection matrix and set to orthorgaphic GL.glMatrixMode(GL.GL_PROJECTION) GL.glPushMatrix() GL.glLoadIdentity() # this also sets the 0,0 to be top-left GL.glOrtho(0, self.win.size[0], 0, self.win.size[1], 0, 1) # but return to modelview for rendering GL.glMatrixMode(GL.GL_MODELVIEW) GL.glLoadIdentity() GL.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1) if self._needStrUpdate: self._updateImageStr() # unbind any textures GL.glActiveTexture(GL.GL_TEXTURE0) GL.glEnable(GL.GL_TEXTURE_2D) GL.glBindTexture(GL.GL_TEXTURE_2D, 0) GL.glActiveTexture(GL.GL_TEXTURE1) GL.glEnable(GL.GL_TEXTURE_2D) GL.glBindTexture(GL.GL_TEXTURE_2D, 0) # move to center of stimulus x, y = self._posRendered[:2] GL.glRasterPos2f(self.win.size[0]/2 - self._size.pix[0]/2 + x, self.win.size[1]/2 - self._size.pix[1]/2 + y) # GL.glDrawPixelsub(GL.GL_RGB, self.imArr) GL.glDrawPixels(int(self._size.pix[0]), int(self._size.pix[1]), self.internalFormat, self.dataType, self._imStr) # return to 3D mode (go and pop the projection matrix) GL.glMatrixMode(GL.GL_PROJECTION) GL.glPopMatrix() GL.glMatrixMode(GL.GL_MODELVIEW) def _set(self, attrib, val, op='', log=True): """Deprecated. Use methods specific to the parameter you want to set. e.g. :: stim.pos = [3,2.5] stim.ori = 45 stim.phase += 0.5 NB this method does not flag the need for updates any more - that is done by specific methods as described above. """ if op is None: op = '' # format the input value as float vectors if isinstance(val, (tuple, list)): val = numpy.array(val, float) setAttribute(self, attrib, val, log, op) @property def pos(self): """:ref:`x,y-pair <attrib-xy>` specifying the centre of the image relative to the window center. Stimuli can be positioned off-screen, beyond the window! :ref:`operations <attrib-operations>` are supported. """ return WindowMixin.pos.fget(self) @pos.setter def pos(self, value): WindowMixin.pos.fset(self, value) self._calcPosRendered() def setPos(self, newPos, operation='', 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, 'pos', newPos, log, operation) @attributeSetter def depth(self, value): """DEPRECATED. Depth is now controlled simply by drawing order. """ self.__dict__['depth'] = value def setDepth(self, newDepth, operation='', log=None): """DEPRECATED. Depth is now controlled simply by drawing order. """ setAttribute(self, 'depth', newDepth, log, operation) def _calcPosRendered(self): """Calculate the pos of the stimulus in pixels""" self._posRendered = convertToPix(pos=self.pos, vertices=numpy.array([0, 0]), units=self.units, win=self.win) @attributeSetter def image(self, filename): """Filename, including relative or absolute path. The image can be any format that the Python Imaging Library can import (almost any). Can also be an image already loaded by PIL. """ filename = pathToString(filename) self.__dict__['image'] = filename if isinstance(filename, str): # is a string - see if it points to a file if os.path.isfile(filename): self.filename = filename im = Image.open(self.filename) im = im.transpose(Image.FLIP_TOP_BOTTOM) else: logging.error("couldn't find image...%s" % filename) core.quit() else: # not a string - have we been passed an image? try: im = filename.copy().transpose(Image.FLIP_TOP_BOTTOM) except AttributeError: # apparently not an image logging.error("couldn't find image...%s" % filename) core.quit() self.filename = repr(filename) # '<Image.Image image ...>' self._size = Size(im.size, units='pix', win=self.win) # set correct formats for bytes/floats if im.mode == 'RGBA': self.imArray = numpy.array(im).astype(numpy.ubyte) self.internalFormat = GL.GL_RGBA else: self.imArray = numpy.array(im.convert("RGB")).astype(numpy.ubyte) self.internalFormat = GL.GL_RGB self.dataType = GL.GL_UNSIGNED_BYTE self._needStrUpdate = True def setImage(self, filename=None, log=None): """Usually you can use 'stim.attribute = value' syntax instead, but use this method if you need to suppress the log message. """ filename = pathToString(filename) setAttribute(self, 'image', filename, log)

Back to top