def __init__(self, protocol_runner: ProtocolRunner, main_window=None): """ """ super().__init__("Protocol running") self.setIconSize(QSize(32, 32)) self.main_window = main_window self.protocol_runner = protocol_runner self.update_duration_each = 120 self._update_duration_i = 0 self.toggleStatus = ToggleIconButton( icon_off="play", icon_on="stop", action_on="play", on=False ) self.toggleStatus.clicked.connect(self.toggle_protocol_running) self.addWidget(self.toggleStatus) # Progress bar for monitoring the protocol: self.progress_bar = QProgressBar() self.addSeparator() self.addWidget(self.progress_bar) # Window with the protocol parameters: self.act_edit = IconButton( action_name="Edit protocol parameters", icon_name="edit_protocol" ) self.act_edit.clicked.connect(self.show_stim_params_gui) self.addWidget(self.act_edit) # Connect events and signals from the ProtocolRunner to update the GUI: self.update_progress() self.protocol_runner.sig_timestep.connect(self.update_progress) self.protocol_runner.sig_protocol_finished.connect(self.toggle_icon) self.protocol_runner.sig_protocol_updated.connect(self.update_progress) self.protocol_runner.sig_protocol_interrupted.connect(self.toggle_icon)
def __init__(self, experiment, **kwargs): """ """ super().__init__(**kwargs) self.experiment = experiment self.setWindowTitle("Stytra | " + pretty_name(type(experiment.protocol).name)) self.docks = dict() # self.label_debug = DebugLabel(debug_on=experiment.debug_mode) # if not self.experiment.offline: # self.widget_projection = ProjectorAndCalibrationWidget(experiment) self.toolbar_control = ProtocolControlToolbar( experiment.protocol_runner, self) self.toolbar_control.setObjectName("toolbar") # Connect signals from the protocol_control: self.toolbar_control.sig_start_protocol.connect( experiment.start_protocol) self.toolbar_control.sig_stop_protocol.connect(experiment.end_protocol) self.btn_metadata = IconButton(icon_name="edit_fish", action_name="Edit metadata") self.btn_metadata.clicked.connect(self.show_metadata_gui) self.toolbar_control.addWidget(self.btn_metadata) self.act_folder = self.toolbar_control.addAction("Save in {}".format( self.experiment.base_dir)) self.act_folder.triggered.connect(self.change_folder_gui) if self.experiment.database is not None: self.chk_db = ToggleIconButton( action_on="Use DB", icon_on="dbON", icon_off="dbOFF", on=self.experiment.use_db, ) self.chk_db.toggled.connect(self.toggle_db) self.toolbar_control.addWidget(self.chk_db) if experiment.trigger is not None: self.chk_scope = QCheckBox("Wait for trigger signal") self.logger = QPlainTextEditLogger() self.experiment.logger.addHandler(self.logger) self.status_display = StatusMessageDisplay() self.statusBar().addWidget(self.status_display) self.plot_framerate = MultiFrameratesWidget() self.plot_framerate.add_framerate( self.experiment.protocol_runner.framerate_acc) self.metadata_win = None
class ProtocolControlToolbar(QToolBar): """GUI for controlling a ProtocolRunner. This class implements the toolbar for controlling the protocol: - toggle button for starting/stopping the Protocol; - progress bar to display progression of the Protocol. - a button and window for controlling Protocol parameters; Parameters ---------- protocol_runner: :class:`ProtocolRunner <stytra.stimulation.ProtocolRunner>` object ProtocolRunner that is controlled by the GUI. Signals: """ sig_start_protocol = pyqtSignal() """ Emitted via the toggle button click, meant to start the protocol.""" sig_stop_protocol = pyqtSignal() """ Emitted via the toggle button click, meant to abort the protocol.""" def __init__(self, protocol_runner: ProtocolRunner, main_window=None): """ """ super().__init__("Protocol running") self.setIconSize(QSize(32, 32)) self.main_window = main_window self.protocol_runner = protocol_runner self.update_duration_each = 120 self._update_duration_i = 0 self.toggleStatus = ToggleIconButton( icon_off="play", icon_on="stop", action_on="play", on=False ) self.toggleStatus.clicked.connect(self.toggle_protocol_running) self.addWidget(self.toggleStatus) # Progress bar for monitoring the protocol: self.progress_bar = QProgressBar() self.addSeparator() self.addWidget(self.progress_bar) # Window with the protocol parameters: self.act_edit = IconButton( action_name="Edit protocol parameters", icon_name="edit_protocol" ) self.act_edit.clicked.connect(self.show_stim_params_gui) self.addWidget(self.act_edit) # Connect events and signals from the ProtocolRunner to update the GUI: self.update_progress() self.protocol_runner.sig_timestep.connect(self.update_progress) self.protocol_runner.sig_protocol_finished.connect(self.toggle_icon) self.protocol_runner.sig_protocol_updated.connect(self.update_progress) self.protocol_runner.sig_protocol_interrupted.connect(self.toggle_icon) def show_stim_params_gui(self): """Create and show window to update protocol parameters.""" self.prot_param_win = ParameterGui(self.protocol_runner.protocol) self.prot_param_win.show() def toggle_protocol_running(self): """Emit the start and stop signals. These can be used in the Experiment class or directly connected with the respective ProtocolRunner start() and stop() methods. Parameters ---------- Returns ------- """ # Start/stop the protocol: if not self.protocol_runner.running: self.progress_bar.setValue(0) self.sig_start_protocol.emit() else: self.sig_stop_protocol.emit() def toggle_icon(self): self.toggleStatus.flip_icon(self.protocol_runner.running) self.update_progress() def update_progress(self): """Update progress bar""" # if self._update_duration_i == 0: # pass # self.protocol_runner.duration = self.protocol_runner.get_duration() self._update_duration_i = ( self._update_duration_i + 1 ) % self.update_duration_each self.progress_bar.setMaximum(int(self.protocol_runner.duration)) self.progress_bar.setValue(int(self.protocol_runner.t)) rem = ceil(self.protocol_runner.duration - self.protocol_runner.t) rem_min = int(floor(rem / 60)) time_info = "{}/{}s ({}:{} remaining)".format( int(self.protocol_runner.t), int(self.protocol_runner.duration), rem_min, int(rem - rem_min * 60), ) # If experiment started, add expected end time: if self.protocol_runner.running: exp_end_time = self.protocol_runner.experiment.t0 + datetime.timedelta( seconds=self.protocol_runner.duration ) time_info += " - Ending at {}:{}:{}".format( exp_end_time.hour, exp_end_time.minute, exp_end_time.second ) self.progress_bar.setFormat(time_info)
def __init__(self, *args, experiment=None, **kwargs): """ :param experiment: experiment to which this belongs (:class:Experiment <stytra.Experiment> object) """ super().__init__(*args, **kwargs) self.experiment = experiment if experiment is not None: self.camera = experiment.camera experiment.gui_timer.timeout.connect(self.retrieve_image) else: self.gui_timer = QTimer() self.gui_timer.setSingleShot(False) self.control_params = self.experiment.camera_state # Create the layout for the camera view: self.camera_display_widget = pg.GraphicsLayoutWidget() # Display area for showing the camera image: self.display_area = pg.ViewBox(lockAspect=1, invertY=False) self.display_area.setRange( QRectF(0, 0, 640, 480), update=True, disableAutoRange=True ) self.scale = 640 self.display_area.invertY(True) # Image to which the frame will be set, initially black: self.image_item = pg.ImageItem() self.image_item.setImage(np.zeros((640, 480), dtype=np.uint8)) self.display_area.addItem(self.image_item) self.camera_display_widget.addItem(self.display_area) # Queue of frames coming from the camera if hasattr(experiment, "frame_dispatcher"): self.frame_queue = self.experiment.frame_dispatcher.gui_queue else: self.frame_queue = self.camera.frame_queue # Queue of control parameters for the camera: self.control_queue = self.camera.control_queue self.camera_rotation = self.camera.rotation self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.camera_display_widget) self.layout_control = QHBoxLayout() self.layout_control.setContentsMargins(10, 0, 10, 10) self.btn_pause = ControlToggleIcon( self.experiment.camera_state, "paused", icon_on=get_icon("play"), icon_off=get_icon("pause"), action_off="Pause", action_on="Play", ) self.layout_control.addWidget(self.btn_pause) if hasattr(self.experiment.camera_state, "replay"): self.experiment.camera_state.replay = False self.btn_rewind = ControlToggleIcon( self.experiment.camera_state, "replay", icon_on=get_icon("rewind"), action_off="Resume", action_on="Replay", ) self.layout_control.addWidget(self.btn_rewind) if self.control_queue is not None: self.btn_camera_param = IconButton( icon_name="edit_camera", action_name="Configure camera" ) self.btn_camera_param.clicked.connect(self.show_params_gui) self.layout_control.addWidget(self.btn_camera_param) self.btn_capture = IconButton( icon_name="camera_flash", action_name="Capture frame" ) self.btn_capture.clicked.connect(self.save_image) self.layout_control.addWidget(self.btn_capture) self.btn_autorange = ToggleIconButton( icon_off="autoscale", icon_on="autoscaleOFF", action_on="Autoscale" ) self.layout_control.addWidget(self.btn_autorange) self.layout.addLayout(self.layout_control) self.current_image = None self.setLayout(self.layout) self.current_frame_time = None self.param_widget = None
class CameraViewWidget(QWidget): """A widget to show images from a frame source and display the camera controls. Parameters ---------- Returns ------- """ def __init__(self, *args, experiment=None, **kwargs): """ :param experiment: experiment to which this belongs (:class:Experiment <stytra.Experiment> object) """ super().__init__(*args, **kwargs) self.experiment = experiment if experiment is not None: self.camera = experiment.camera experiment.gui_timer.timeout.connect(self.retrieve_image) else: self.gui_timer = QTimer() self.gui_timer.setSingleShot(False) self.control_params = self.experiment.camera_state # Create the layout for the camera view: self.camera_display_widget = pg.GraphicsLayoutWidget() # Display area for showing the camera image: self.display_area = pg.ViewBox(lockAspect=1, invertY=False) self.display_area.setRange( QRectF(0, 0, 640, 480), update=True, disableAutoRange=True ) self.scale = 640 self.display_area.invertY(True) # Image to which the frame will be set, initially black: self.image_item = pg.ImageItem() self.image_item.setImage(np.zeros((640, 480), dtype=np.uint8)) self.display_area.addItem(self.image_item) self.camera_display_widget.addItem(self.display_area) # Queue of frames coming from the camera if hasattr(experiment, "frame_dispatcher"): self.frame_queue = self.experiment.frame_dispatcher.gui_queue else: self.frame_queue = self.camera.frame_queue # Queue of control parameters for the camera: self.control_queue = self.camera.control_queue self.camera_rotation = self.camera.rotation self.layout = QVBoxLayout() self.layout.setContentsMargins(0, 0, 0, 0) self.layout.addWidget(self.camera_display_widget) self.layout_control = QHBoxLayout() self.layout_control.setContentsMargins(10, 0, 10, 10) self.btn_pause = ControlToggleIcon( self.experiment.camera_state, "paused", icon_on=get_icon("play"), icon_off=get_icon("pause"), action_off="Pause", action_on="Play", ) self.layout_control.addWidget(self.btn_pause) if hasattr(self.experiment.camera_state, "replay"): self.experiment.camera_state.replay = False self.btn_rewind = ControlToggleIcon( self.experiment.camera_state, "replay", icon_on=get_icon("rewind"), action_off="Resume", action_on="Replay", ) self.layout_control.addWidget(self.btn_rewind) if self.control_queue is not None: self.btn_camera_param = IconButton( icon_name="edit_camera", action_name="Configure camera" ) self.btn_camera_param.clicked.connect(self.show_params_gui) self.layout_control.addWidget(self.btn_camera_param) self.btn_capture = IconButton( icon_name="camera_flash", action_name="Capture frame" ) self.btn_capture.clicked.connect(self.save_image) self.layout_control.addWidget(self.btn_capture) self.btn_autorange = ToggleIconButton( icon_off="autoscale", icon_on="autoscaleOFF", action_on="Autoscale" ) self.layout_control.addWidget(self.btn_autorange) self.layout.addLayout(self.layout_control) self.current_image = None self.setLayout(self.layout) self.current_frame_time = None self.param_widget = None def retrieve_image(self): """Update displayed frame while emptying frame source queue. This is done through a while loop that takes all available frames at every update. # TODO fix this somehow? **Important!** if the input queue is too fast this will produce an infinite loop and block the interface! Parameters ---------- Returns ------- """ first = True while True: try: # In this way, the frame displayed is actually the most # recent one added to the queue, as a queue is FILO: if first: qr = self.frame_queue.get(timeout=0.0001) self.current_image = qr[-1] self.current_frame_time = qr[0] # first = False else: # Else, get to free the queue: _, _ = self.frame_queue.get(timeout=0.001) except Empty: break # Once obtained current image, display it: if self.isVisible(): if self.current_image is not None: if self.current_image.shape[0] != self.scale: self.scale = self.current_image.shape[0] self.scale_changed() self.display_area.setRange( QRectF( 0, 0, self.current_image.shape[1], self.current_image.shape[0], ), update=True, disableAutoRange=True, ) self.image_item.setImage( self.current_image, autoLevels=self.btn_autorange.isChecked() ) def scale_changed(self): self.display_area.setRange( QRectF(0, 0, self.current_image.shape[1], self.current_image.shape[0]), update=True, disableAutoRange=True, ) def save_image(self, name=None): """Save a frame to the current directory.""" if name is None or not name: timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") name = self.experiment.filename_base() + timestamp + "_img.png" imsave(name, self.image_item.image) def show_params_gui(self): """ """ self.param_widget = ParameterGui(self.control_params) self.param_widget.show()
class ExperimentWindow(QMainWindow): """Window for controlling a simple experiment including only a monitor the relative controls and the buttons for data_log and protocol control. All widgets objects are created and connected in the `__init__` and then added ti the GUI in the `construct_ui` method Parameters ---------- experiment : `Experiment <stytra.experiments.Experiment>` object experiment for which the window is built. Returns ------- """ def __init__(self, experiment, **kwargs): """ """ super().__init__(**kwargs) self.experiment = experiment self.setWindowTitle("Stytra | " + pretty_name(type(experiment.protocol).name)) self.docks = dict() self.toolbar_control = ProtocolControlToolbar( experiment.protocol_runner, self) self.toolbar_control.setObjectName("toolbar") # Connect signals from the protocol_control: self.toolbar_control.sig_start_protocol.connect( experiment.start_protocol) self.toolbar_control.sig_stop_protocol.connect(experiment.end_protocol) self.btn_metadata = IconButton(icon_name="edit_fish", action_name="Edit metadata") self.btn_metadata.clicked.connect(self.show_metadata_gui) self.toolbar_control.addWidget(self.btn_metadata) self.act_folder = self.toolbar_control.addAction("Save in {}".format( self.experiment.base_dir)) self.act_folder.triggered.connect(self.change_folder_gui) if self.experiment.database is not None: self.chk_db = ToggleIconButton( action_on="Use DB", icon_on="dbON", icon_off="dbOFF", on=self.experiment.use_db, ) self.chk_db.toggled.connect(self.toggle_db) self.toolbar_control.addWidget(self.chk_db) if experiment.trigger is not None: self.chk_scope = QCheckBox("Wait for trigger signal") self.logger = QPlainTextEditLogger() self.experiment.logger.addHandler(self.logger) self.status_display = StatusMessageDisplay() self.statusBar().addWidget(self.status_display) self.plot_framerate = MultiFrameratesWidget() self.plot_framerate.add_framerate( self.experiment.protocol_runner.framerate_acc) self.metadata_win = None def change_folder_gui(self): """Open dialog window to specify a new saving directory.""" folder = QFileDialog.getExistingDirectory( caption="Results folder", directory=self.experiment.base_dir) print(folder) if folder is not None: self.experiment.base_dir = folder self.act_folder.setText("Save in {}".format( self.experiment.base_dir)) def show_metadata_gui(self): """Open Param GUI to control general experiment and animal metadata.""" # Create widget, horizontal layout self.metadata_win = QWidget() self.metadata_win.setLayout(QHBoxLayout()) # Add metadata widgets to the main one self.metadata_win.layout().addWidget( ParameterGui(self.experiment.metadata)) self.metadata_win.layout().addWidget( ParameterGui(self.experiment.metadata_animal)) self.metadata_win.show() def add_dock(self, item: QDockWidget): """Adding a new DockWidget updating the docks dictionary.""" self.docks[item.objectName()] = item def construct_ui(self): """UI construction function.""" self.addToolBar(Qt.TopToolBarArea, self.toolbar_control) log_dock = QDockWidget("Log", self) log_dock.setObjectName("dock_log") log_dock.setWidget(self.logger.widget) self.add_dock(log_dock) self.addDockWidget(Qt.RightDockWidgetArea, log_dock) dockFramerate = QDockWidget("Frame rates", self) dockFramerate.setWidget(self.plot_framerate) dockFramerate.setObjectName("dock_framerates") self.addDockWidget(Qt.RightDockWidgetArea, dockFramerate) self.add_dock(dockFramerate) if self.experiment.trigger is not None: self.toolbar_control.addWidget(self.chk_scope) self.experiment.gui_timer.timeout.connect(self.plot_framerate.update) self.toolbar_control.setObjectName("toolbar_control") self.setCentralWidget(None) def write_log(self, msg): """Write something in the log window.""" self.log_widget.textCursor().appendPlainText(msg) def toggle_db(self, tg): """Toggle database button.""" if self.chk_db.isChecked(): self.experiment.use_db = True else: self.experiment.use_db = False def closeEvent(self, *args, **kwargs): """ Parameters ---------- *args : **kwargs : Returns ------- """ self.experiment.wrap_up()