#!/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)