Source code for psychopy.visual.bufferimage

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

"""Take a "screen-shot" (full or partial), save to a ImageStim()-like
RBGA object.`"""

# Part of the PsychoPy library
# Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2021 Open Science Tools Ltd.
# Distributed under the terms of the GNU General Public License (GPL).

from __future__ import absolute_import, division, print_function

from builtins import str

# 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
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.attributetools import attributeSetter, setAttribute
from psychopy.tools.typetools import float_uint8
from psychopy.visual.image import ImageStim

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

import numpy


[docs]class BufferImageStim(ImageStim): """Take a "screen-shot", save as an ImageStim (RBGA object). The screen-shot is a single collage image composed of static elements that you can treat as being a single stimulus. The screen-shot can be of the visible screen (front buffer) or hidden (back buffer). BufferImageStim aims to provide fast rendering, while still allowing dynamic orientation, position, and opacity. It's fast to draw but slower to init (same as an ImageStim). You specify the part of the screen to capture (in norm units), and optionally the stimuli themselves (as a list of items to be drawn). You get a screenshot of those pixels. If your OpenGL does not support arbitrary sizes, the image will be larger, using square powers of two if needed, with the excess image being invisible (using alpha). The aim is to preserve the buffer contents as rendered. Checks for OpenGL 2.1+, or uses square-power-of-2 images. **Example**:: # define lots of stimuli, make a list: mySimpleImageStim = ... myTextStim = ... stimList = [mySimpleImageStim, myTextStim] # draw stim list items & capture (slow; see EXP log for times): screenshot = visual.BufferImageStim(myWin, stim=stimList) # render to screen (very fast, except for the first draw): while <conditions>: screenshot.draw() # fast; can vary .ori, .pos, .opacity other_stuff.draw() # dynamic myWin.flip() See coder Demos > stimuli > bufferImageStim.py for a demo, with timing stats. :Author: - 2010 Jeremy Gray, with on-going fixes """ def __init__(self, win, buffer='back', rect=(-1, 1, 1, -1), sqPower2=False, stim=(), interpolate=True, flipHoriz=False, flipVert=False, mask='None', pos=(0, 0), name=None, autoLog=None): """ :Parameters: buffer : the screen buffer to capture from, default is 'back' (hidden). 'front' is the buffer in view after win.flip() rect : a list of edges [left, top, right, bottom] defining a screen rectangle which is the area to capture from the screen, given in norm units. default is fullscreen: [-1, 1, 1, -1] stim : a list of item(s) to be drawn to the back buffer (in order). The back buffer is first cleared (without the win being flip()ed), then stim items are drawn, and finally the buffer (or part of it) is captured. Each item needs to have its own .draw() method, and have the same window as win. interpolate : whether to use interpolation (default = True, generally good, especially if you change the orientation) sqPower2 : - False (default) = use rect for size if OpenGL = 2.1+ - True = use square, power-of-two image sizes flipHoriz : horizontally flip (mirror) the captured image, default = False flipVert : vertically flip (mirror) the captured image; default = False """ # depends on: window._getRegionOfFrame # what local vars are defined (these are the init params) for use by # __repr__ self._initParams = dir() self._initParams.remove('self') self.autoLog = False # set this False first and change later _clock = core.Clock() if stim: # draw all stim to the back buffer win.clearBuffer() buffer = 'back' if hasattr(stim, '__iter__'): for stimulus in stim: try: if stimulus.win == win: stimulus.draw() else: msg = ('BufferImageStim.__init__: user ' 'requested "%s" drawn in another window') logging.warning(msg % repr(stimulus)) except AttributeError: msg = 'BufferImageStim.__init__: "%s" failed to draw' logging.warning(msg % repr(stimulus)) else: raise ValueError('Stim is not iterable in BufferImageStim. ' 'It should be a list of stimuli.') # take a screenshot of the buffer using win._getRegionOfFrame(): glversion = pyglet.gl.gl_info.get_version() if glversion >= '2.1' and not sqPower2: region = win._getRegionOfFrame(buffer=buffer, rect=rect) else: if not sqPower2: msg = ('BufferImageStim.__init__: defaulting to square ' 'power-of-2 sized image (%s)') logging.debug(msg % glversion) region = win._getRegionOfFrame(buffer=buffer, rect=rect, squarePower2=True) if stim: win.clearBuffer() # turn the RGBA region into an ImageStim() object: if win.units in ['norm']: pos *= win.size / 2. size = region.size / win.size / 2. super(BufferImageStim, self).__init__( win, image=region, units='pix', mask=mask, pos=pos, size=size, interpolate=interpolate, name=name, autoLog=False) self.size = region.size # to improve drawing speed, move these out of draw: self.thisScale = numpy.array([4, 4]) self.flipHoriz = flipHoriz self.flipVert = flipVert # 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 %s = %s" % (self.name, str(self))) msg = 'BufferImageStim %s: took %.1fms to initialize' logging.exp(msg % (name, 1000 * _clock.getTime())) @attributeSetter def flipHoriz(self, flipHoriz): """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. """ self.__dict__['flipHoriz'] = flipHoriz @attributeSetter def flipVert(self, flipVert): """If set to True then the image will be flipped vertically (left-to-right). Note that this is relative to the original image, not relative to the current state. """ self.__dict__['flipVert'] = flipVert
[docs] 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) # call attributeSetter
[docs] 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) # call attributeSetter
[docs] def draw(self, win=None): """Draws the BufferImage on the screen, similar to :class:`~psychopy.visual.ImageStim` `.draw()`. Allows dynamic position, size, rotation, mirroring, and opacity. Limitations / bugs: not sure what happens with shaders and self._updateList() """ if win is None: win = self.win self._selectWindow(win) GL.glPushMatrix() # preserve state # GL.glLoadIdentity() # dynamic flip GL.glScalef(self.thisScale[0] * (1, -1)[self.flipHoriz], self.thisScale[1] * (1, -1)[self.flipVert], 1.0) # enable dynamic position, orientation, opacity; depth not working? GL.glColor4f(*self._foreColor.render('rgba1')) GL.glCallList(self._listID) # make it happen GL.glPopMatrix() # return the view to previous state

Back to top