Source code for stytra.stimulation.stimulus_display

from datetime import datetime

import numpy as np
import qimage2ndarray
from PyQt5.QtCore import QPoint, QRect
from PyQt5.QtGui import QPainter, QBrush, QColor
from PyQt5.QtWidgets import QOpenGLWidget, QWidget

from lightparam.param_qt import ParametrizedWidget, Param


[docs]class StimulusDisplayWindow(ParametrizedWidget): """Display window for a visual simulation protocol, with a display area that can be controlled and changed from a ProtocolControlWindow. The display area (either a QWidget or a QOpenGLWidget, see below) is where the paint() method of the current Stimulus will draw the current image. The paint() method is called in the paintEvent() of the QWidget. Stimuli sequence and its timing is handled via a linked ProtocolRunner object. Information about real dimensions of the display comes from a calibrator object. If required, a movie of the displayed stimulus can be acquired and saved. Parameters ---------- Returns ------- """ def __init__( self, protocol_runner, calibrator, record_stim_framerate=None, gl=False, **kwargs ): """ :param protocol_runner: ProtocolRunner object that handles the stim sequence. :param calibrator: Calibrator object :param record_stim_framerate: either None or the framerate at which the stimulus is to be recorded """ super().__init__( name="stimulus/display_params", tree=protocol_runner.experiment.dc, **kwargs ) self.setWindowTitle("Stytra stimulus display") # QOpenGLWidget is faster in painting complicated stimuli (but slower # with easy ones!) but does not allow stimulus recording. Therefore, # parent class for the StimDisplay window is created at runtime: if record_stim_framerate is not None or not gl: QWidgetClass = QWidget else: QWidgetClass = QOpenGLWidget StimDisplay = type("StimDisplay", (StimDisplayWidget, QWidgetClass), {}) self.widget_display = StimDisplay( self, calibrator=calibrator, protocol_runner=protocol_runner, record_stim_framerate=record_stim_framerate, ) self.widget_display.setMaximumSize(2000, 2000) self.pos = Param((0, 0)) self.size = Param((400, 400)) self.setStyleSheet("background-color:black;") self.sig_param_changed.connect(self.set_dims) self.set_dims()
[docs] def set_dims(self): """ Set monitor dimensions when changed from the control GUI. """ self.widget_display.setGeometry(*(tuple(self.pos) + tuple(self.size)))
[docs]class StimDisplayWidget: """Widget for the actual display area contained inside the StimulusDisplayWindow. Parameters ---------- Returns ------- """ def __init__(self, *args, protocol_runner, calibrator, record_stim_framerate): """ Check ProtocolControlWindow __init__ documentation for description of arguments. """ super().__init__(*args) self.calibrator = calibrator self.protocol_runner = protocol_runner self.record_framerate = record_stim_framerate self.img = None self.calibrating = False self.dims = None # storing of displayed frames self.k = 0 if record_stim_framerate is None: self.stored_frames = None else: self.stored_frames = [] # Connect protocol_runner timer to stimulus updating function: self.protocol_runner.sig_timestep.connect(self.display_stimulus) self.k = 0 self.starting_time = None self.last_time = self.starting_time self.movie = [] self.movie_timestamps = []
[docs] def paintEvent(self, QPaintEvent): """Generate the stimulus that will be displayed. A QPainter object is defined, which is then passed to the current stimulus paint function for drawing the stimulus. Parameters ---------- QPaintEvent : Returns ------- """ p = QPainter(self) p.setBrush(QBrush(QColor(0, 0, 0))) w = self.width() h = self.height() if self.protocol_runner is not None: if self.protocol_runner.running: try: self.protocol_runner.current_stimulus.paint(p, w, h) except AttributeError: pass else: p.drawRect(QRect(-1, -1, w + 2, h + 2)) p.setRenderHint(QPainter.SmoothPixmapTransform, 1) if self.img is not None: p.drawImage(QPoint(0, 0), self.img) if self.calibrator is not None: if self.calibrator.enabled: self.calibrator.paint_calibration_pattern(p, h, w) p.end()
[docs] def display_stimulus(self): """Function called by the protocol_runner timestep timer that update the displayed image and, if required, grab a picture of the current widget state for recording the stimulus movie. """ self.update() current_time = datetime.now() # Grab frame if recording is enabled. if self.starting_time is None: self.starting_time = current_time if self.record_framerate: now = datetime.now() # Only one every self.record_stim_every frames will be captured. if ( self.last_time is None or (now - self.last_time).total_seconds() >= 1 / self.record_framerate ): # # QImage from QPixmap taken with QWidget.grab(): img = self.grab().toImage() arr = qimage2ndarray.rgb_view(img) # Convert to np array self.movie.append(arr.copy()) self.movie_timestamps.append( (current_time - self.starting_time).total_seconds() ) self.last_time = current_time
[docs] def get_movie(self): """Finalize stimulus movie. :return: a channel x time x N x M array with stimulus movie Parameters ---------- Returns ------- """ if self.record_framerate is not None: movie_arr = self.movie movie_timestamps = np.array(self.movie_timestamps) return movie_arr, movie_timestamps else: return None, None
[docs] def reset(self): """ Resets the movie recorder Returns ------- """ self.movie = [] self.movie_timestamps = [] self.starting_time = None