def __init__(self, state: State, timer): super().__init__() self.state = state self.timer = timer self.setLayout(QVBoxLayout()) self.wid_save_options = ParameterGui(state.save_settings) self.save_location_button = QPushButton() self.manual_duration_chk = QCheckBox("Triggered experiment") self.wid_manual_duration = ParameterGui(self.state.trigger_settings) self.layout().addWidget(self.wid_save_options) self.layout().addWidget(self.save_location_button) self.layout().addWidget(self.wid_manual_duration) self.layout().addWidget(self.manual_duration_chk) self.set_locationbutton() self.save_location_button.clicked.connect(self.set_save_location) self.manual_duration_chk.stateChanged.connect(self.update_triggered_option) self.state.trigger_settings.sig_param_changed.connect( self.state.send_manual_duration ) self.manual_duration_chk.setChecked(True)
class SaveWidget(QWidget): def __init__(self, state: State, timer): super().__init__() self.state = state self.timer = timer self.setLayout(QVBoxLayout()) self.wid_save_options = ParameterGui(state.save_settings) self.save_location_button = QPushButton() self.manual_duration_chk = QCheckBox("Triggered experiment") self.wid_manual_duration = ParameterGui(self.state.trigger_settings) self.layout().addWidget(self.wid_save_options) self.layout().addWidget(self.save_location_button) self.layout().addWidget(self.wid_manual_duration) self.layout().addWidget(self.manual_duration_chk) self.set_locationbutton() self.save_location_button.clicked.connect(self.set_save_location) self.manual_duration_chk.stateChanged.connect(self.update_triggered_option) self.state.trigger_settings.sig_param_changed.connect( self.state.send_manual_duration ) self.manual_duration_chk.setChecked(True) def set_save_location(self): save_dir = QFileDialog.getExistingDirectory() self.state.save_settings.save_dir = save_dir self.set_locationbutton() def set_locationbutton(self): pathtext = self.state.save_settings.save_dir # check if there is a stack in this location if (Path(pathtext) / "original" / "stack_metadata.json").is_file(): self.save_location_button.setText("Overwrite " + pathtext) self.save_location_button.setStyleSheet( "background-color:#b5880d; border-color:#fcc203" ) self.state.save_settings.overwrite_save_folder = 1 else: self.save_location_button.setText("Save in " + pathtext) self.save_location_button.setStyleSheet("") self.state.save_settings.overwrite_save_folder = 0 def update_triggered_option(self, is_checked): if is_checked: self.wid_manual_duration.setEnabled(False) self.state.trigger_settings.is_triggered = True self.state.set_trigger_mode(True) else: self.wid_manual_duration.setEnabled(True) self.state.trigger_settings.is_triggered = False self.state.set_trigger_mode(False)
def show_metadata_gui(self): """ """ self.metadata_win = QWidget() self.metadata_win.setLayout(QHBoxLayout()) self.metadata_win.layout().addWidget( ParameterGui(self.experiment.metadata)) self.metadata_win.layout().addWidget( ParameterGui(self.experiment.metadata_animal)) self.metadata_win.show()
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 __init__(self, state, wid_display, timer): super().__init__() self.wid_display = wid_display self.roi = wid_display.roi self.roi_state = RoiState.FULL self.state = state self.state.camera_settings.sig_param_changed.connect( self.update_on_bin_change) self.timer = timer self.camera_msg_queue = Queue() self.sensor_resolution = self.wid_display.max_sensor_resolution self.full_size = True self.current_binning = conf["camera"]["default_binning"] self.wid_camera_settings = ParameterGui(self.state.camera_settings) self.setLayout(QVBoxLayout()) self.btn_roi = QPushButton(ROI_TEXTS[self.roi_state]) self.btn_cancel_roi = QPushButton("Cancel") self.btn_cancel_roi.hide() self.layout().addWidget(self.wid_camera_settings) self.layout().addWidget(self.btn_roi) self.layout().addWidget(self.btn_cancel_roi) self.btn_roi.clicked.connect(self.iterate_roi_state) self.btn_cancel_roi.clicked.connect(self.cancel_roi_selection)
def __init__(self, state: ExperimentState): self.state = state super().__init__() self.scanning_layout = QVBoxLayout() self.scanning_settings_gui = ParameterGui(self.state.scanning_settings) self.scanning_calc = CalculatedParameterDisplay() self.pause_button = QPushButton() self.pause_button.clicked.connect(self.toggle_pause) self.scanning_layout.addWidget(self.scanning_settings_gui) self.scanning_layout.addWidget(self.scanning_calc) self.scanning_layout.addWidget(self.pause_button) self.setLayout(self.scanning_layout) self.state.sig_scanning_changed.connect(self.update_calc_display) self.update_button()
def __init__(self, state, timer): super().__init__() self.state = state self.timer = timer self.setLayout(QVBoxLayout()) self.wid_volume = ParameterGui(state.volume_setting) self.chk_pause = QCheckBox("Pause after experiment") self.wid_wave = WaveformWidget(timer=self.timer, state=self.state) self.wid_collapsible_wave = CollapsibleWidget( child=self.wid_wave, name="Piezo impulse-response waveform") self.wid_collapsible_wave.toggle_collapse() self.layout().addWidget(self.wid_volume) self.layout().addWidget(self.chk_pause) self.layout().addWidget(self.wid_collapsible_wave) self.chk_pause.clicked.connect(self.change_pause_status) self.chk_pause.click()
def open_tracking_params_tree(self): """ """ self.track_params_wnd = QWidget() self.track_params_wnd.setLayout(QVBoxLayout()) if hasattr(self.experiment, "pipeline"): for paramsname, paramspar in self.experiment.pipeline.all_params.items( ): if (paramsname == "diagnostics" or paramsname == "reset" or len(paramspar.params.items()) == 0): continue self.track_params_wnd.layout().addWidget( QLabel(paramsname.replace("/", "→"))) self.track_params_wnd.layout().addWidget( ParameterGui(paramspar)) self.track_params_wnd.layout().addWidget( ControlButton(self.experiment.pipeline.all_params["reset"], "reset")) self.track_params_wnd.setWindowTitle("Tracking parameters") self.track_params_wnd.show()
def __init__(self, state, timer): super().__init__() self.state = state self.main_layout = QHBoxLayout() self.lbl_text = QLabel("Light source") self.btn_off = QPushButton("ON") self.btn_off.clicked.connect(self.toggle) self.main_layout.addWidget(self.lbl_text) self.main_layout.addWidget(self.btn_off) self.wid_settings = ParameterGui(self.state.light_source_settings) self.main_layout.addWidget(self.wid_settings) self.main_layout.setContentsMargins(0, 0, 0, 0) self.setLayout(self.main_layout) self.laser_on = False self.previous_current = self.state.light_source_settings.intensity timer.timeout.connect(self.update_current)
def __init__(self, state: ExperimentState): super().__init__() self.state = state self.experiment_settings_gui = ParameterGui(state.experiment_settings) self.save_location_button = QPushButton() self.set_locationbutton() self.save_location_button.clicked.connect(self.set_save_location) self.startstop_button = QPushButton() self.set_saving() self.chk_pause = QCheckBox("Pause after experiment") self.stack_progress = QProgressBar() self.plane_progress = QProgressBar() self.plane_progress.setFormat("Frame %v of %m") self.stack_progress.setFormat("Plane %v of %m") self.startstop_button.clicked.connect(self.toggle_start) self.setLayout(QVBoxLayout()) self.layout().addWidget(self.experiment_settings_gui) self.layout().addWidget(self.save_location_button) self.layout().addWidget(self.startstop_button) self.layout().addWidget(self.chk_pause) self.layout().addWidget(self.plane_progress) self.layout().addWidget(self.stack_progress)
def __init__(self, state): super().__init__() self.state = state self.setLayout(QVBoxLayout()) self.wid_planar = ParameterGui(state.planar_setting) self.layout().addWidget(self.wid_planar)
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 ViewingWidget(QWidget): """Central widget that displays the images/volumes streamed from the camera, and progress bar. Internally, the display is powered via a Napari viewer. The get_stuff methods replace properties, which seem to be an issue for children of QWidget when used in the __init__. Parameters ---------- state : State object Main state. timer : QTimer Timer from the main GUI. """ _DELAY_REFRESH_COUNT = 10 def __init__(self, state: State, timer: QTimer, style: str): super().__init__() self.state = state timer.timeout.connect(self.refresh) # Note: here we are assuming that the camera has a square sensor, and resolution is # described by only one number (most scientific camera are) if conf["scopeless"]: self.max_sensor_resolution = [256, 256] else: self.max_sensor_resolution = conf["camera"][ "max_sensor_resolution"] self.noise_subtraction_set = False self.count_from_change = None self.refresh_display = True self.is_first_frame = True self.auto_contrast = True s = self.get_fullframe_size() self.image_shape = (1, s, s) self.viewer = napari.Viewer(show=False) # setting napari style to sashimi's self.viewer.window.qt_viewer.setStyleSheet(style) # Add image layer that will be used to show frames/volumes: self.frame_layer = self.viewer.add_image( np.zeros([ 1, ] + self.max_sensor_resolution), blending="translucent", name="frame_layer", ) # Add square ROI of size max image size: self.roi = self.viewer.add_shapes( [np.array([[0, 0], [s[0], 0], [s[0], s[1]], [0, s[1]]])], blending="translucent", face_color="transparent", face_contrast_limits=(0, 0), opacity=1, visible=False, name="roi_layer", ) self.main_layout = QVBoxLayout() self.bottom_layout = QHBoxLayout() self.viewer.window.qt_viewer.viewerButtons.consoleButton.hide() self.viewer.window.qt_viewer.viewerButtons.rollDimsButton.hide() self.viewer.window.qt_viewer.viewerButtons.gridViewButton.hide() self.viewer.window.qt_viewer.viewerButtons.transposeDimsButton.hide() self.viewer.window.qt_viewer.viewerButtons.resetViewButton.setText( "Reset view") self.ndisplay_button = self.viewer.window.qt_viewer.viewerButtons.ndisplayButton self.ndisplay_button.setText("3D mode") self.ndisplay_button.clicked.connect(self.toggle_ndims) self.viewer.dims.events.connect(self.update_current_plane) self.bottom_layout.addWidget( self.viewer.window.qt_viewer.viewerButtons) self.auto_contrast_chk = QCheckBox("Autoadjust contrast") self.contrast_range = ContrastSettings() self.wid_contrast_range = ParameterGui(self.contrast_range) self.bottom_layout.addWidget(self.auto_contrast_chk) self.bottom_layout.addWidget(self.wid_contrast_range) self.bottom_layout.addStretch() self.main_layout.addWidget(self.viewer.window.qt_viewer) self.main_layout.addLayout(self.bottom_layout) self.setLayout(self.main_layout) # Connect changes of camera and laser to contrast reset: self.auto_contrast_chk.stateChanged.connect(self.update_auto_contrast) self.contrast_range.sig_param_changed.connect(self.set_manual_contrast) self.auto_contrast_chk.setChecked(True) self.state.camera_settings.sig_param_changed.connect( self.launch_delayed_contrast_reset) self.state.light_source_settings.sig_param_changed.connect( self.launch_delayed_contrast_reset) self.viewer.window.qt_viewer.viewerButtons.resetViewButton.pressed.connect( self.reset_contrast) def get_fullframe_size(self): """Maximum size of the image at current binning. As stated above, we assume square sensors.""" binning = int(self.state.camera_settings.binning) return [r // binning for r in self.max_sensor_resolution] @property def voxel_size(self): return get_voxel_size( self.state.volume_setting, self.state.camera_settings, ) def refresh(self) -> None: """Main refresh loop called by timeout of the main timer.""" self.refresh_image() self.check_noise_subtraction_state() # This pattern with the counting is required to update the image range with some delay, # as the first received afterwards might still be the wrong one when changing exposure or noise subtraction if self.count_from_change is not None: self.count_from_change += 1 if self.count_from_change == self._DELAY_REFRESH_COUNT: self.reset_contrast() self.count_from_change = None def check_noise_subtraction_state(self): """If we toggled noise subtraction, reset contrast.""" noise_subtraction_set = self.state.noise_subtraction_active.is_set() if noise_subtraction_set != self.noise_subtraction_set: self.noise_subtraction_set = noise_subtraction_set self.launch_delayed_contrast_reset() def launch_delayed_contrast_reset(self): """By setting this from None to 0, we schedule a delayed contrast reset. The actual counting happens in the main refresh, connected to the timer. """ self.count_from_change = 0 def refresh_image(self): current_image = self.state.get_volume() if current_image is None: return # If not volumetric or out of range, reset indexes: if current_image.shape[0] == 1: self.viewer.dims.reset() self.frame_layer.data = current_image # self.frame_layer.scale = [self.voxel_size[0] / self.voxel_size[1], 1.0, 1.0] # Check if anything changed in the image shape, which would mean that changes of the contrast # are required (in case a parameter update was missed). if self.is_first_frame or self.image_shape != current_image.shape: self.reset_contrast() self.viewer.reset_view() self.image_shape = current_image.shape self.is_first_frame = False # Keep current plane in synch when the number of planes changes self.viewer.dims.set_current_step(0, self.state.current_plane) def toggle_ndims(self): """We set the scale only if we are in 3D mode, otherwise there can be funny problems with the image slider in the 2D view. Hopefully all of this will be improved in newer versions of Napari """ if self.ndisplay_button.isChecked(): self.frame_layer.scale = [ self.voxel_size[0] / self.voxel_size[1], 1.0, 1.0 ] else: self.frame_layer.scale = [1.0, 1.0, 1.0] def update_current_plane(self, _): self.state.current_plane = self.viewer.dims.current_step[0] def reset_contrast(self): if self.auto_contrast: self.frame_layer.reset_contrast_limits() else: self.set_manual_contrast() def update_auto_contrast(self, is_checked): if is_checked: self.wid_contrast_range.setEnabled(False) self.auto_contrast = True self.reset_contrast() else: self.wid_contrast_range.setEnabled(True) self.auto_contrast = False self.set_manual_contrast() def set_manual_contrast(self): self.frame_layer.contrast_limits = self.contrast_range.contrast_range
def __init__(self, state: State, timer: QTimer, style: str): super().__init__() self.state = state timer.timeout.connect(self.refresh) # Note: here we are assuming that the camera has a square sensor, and resolution is # described by only one number (most scientific camera are) if conf["scopeless"]: self.max_sensor_resolution = [256, 256] else: self.max_sensor_resolution = conf["camera"][ "max_sensor_resolution"] self.noise_subtraction_set = False self.count_from_change = None self.refresh_display = True self.is_first_frame = True self.auto_contrast = True s = self.get_fullframe_size() self.image_shape = (1, s, s) self.viewer = napari.Viewer(show=False) # setting napari style to sashimi's self.viewer.window.qt_viewer.setStyleSheet(style) # Add image layer that will be used to show frames/volumes: self.frame_layer = self.viewer.add_image( np.zeros([ 1, ] + self.max_sensor_resolution), blending="translucent", name="frame_layer", ) # Add square ROI of size max image size: self.roi = self.viewer.add_shapes( [np.array([[0, 0], [s[0], 0], [s[0], s[1]], [0, s[1]]])], blending="translucent", face_color="transparent", face_contrast_limits=(0, 0), opacity=1, visible=False, name="roi_layer", ) self.main_layout = QVBoxLayout() self.bottom_layout = QHBoxLayout() self.viewer.window.qt_viewer.viewerButtons.consoleButton.hide() self.viewer.window.qt_viewer.viewerButtons.rollDimsButton.hide() self.viewer.window.qt_viewer.viewerButtons.gridViewButton.hide() self.viewer.window.qt_viewer.viewerButtons.transposeDimsButton.hide() self.viewer.window.qt_viewer.viewerButtons.resetViewButton.setText( "Reset view") self.ndisplay_button = self.viewer.window.qt_viewer.viewerButtons.ndisplayButton self.ndisplay_button.setText("3D mode") self.ndisplay_button.clicked.connect(self.toggle_ndims) self.viewer.dims.events.connect(self.update_current_plane) self.bottom_layout.addWidget( self.viewer.window.qt_viewer.viewerButtons) self.auto_contrast_chk = QCheckBox("Autoadjust contrast") self.contrast_range = ContrastSettings() self.wid_contrast_range = ParameterGui(self.contrast_range) self.bottom_layout.addWidget(self.auto_contrast_chk) self.bottom_layout.addWidget(self.wid_contrast_range) self.bottom_layout.addStretch() self.main_layout.addWidget(self.viewer.window.qt_viewer) self.main_layout.addLayout(self.bottom_layout) self.setLayout(self.main_layout) # Connect changes of camera and laser to contrast reset: self.auto_contrast_chk.stateChanged.connect(self.update_auto_contrast) self.contrast_range.sig_param_changed.connect(self.set_manual_contrast) self.auto_contrast_chk.setChecked(True) self.state.camera_settings.sig_param_changed.connect( self.launch_delayed_contrast_reset) self.state.light_source_settings.sig_param_changed.connect( self.launch_delayed_contrast_reset) self.viewer.window.qt_viewer.viewerButtons.resetViewButton.pressed.connect( self.reset_contrast)
def __init__(self, state): super().__init__() self.state = state self.setLayout(QVBoxLayout()) self.wid_singleplane = ParameterGui(state.single_plane_settings) self.layout().addWidget(self.wid_singleplane)
def show_params_gui(self): """ """ self.param_widget = ParameterGui(self.control_params) self.param_widget.show()
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 edit_params(self): self.wnd_params = ParameterGui(self.detection_params) self.wnd_params.show()
app = QApplication([]) class TestParametrized1(ParametrizedWidget): def __init__(self, **kwargs): super().__init__(name="a/gino", **kwargs) self.random = 5 self.a = Param(1) self.b = Param(2.0) self.c = Param(5) self.a_list = Param("a", ["a", "b", "c"]) self.sig_param_changed.connect(self.update_param) # self.show() def update_param(self): """Calculate mm/px from calibrator length""" if self.c is not None: self.block_signal = True self.c = self.a / self.b self.block_signal = False class TestParametrized2(ParametrizedQt): def __init__(self, **kwargs): super().__init__(name="b/c/pino", **kwargs) self.an_int = Param(4) self.a_float = Param(1.0, (-1.0, 10.0)) p = ParameterGui(TestParametrized1()) p.show() app.exec_()
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()
class BoutPlot(QWidget): """ Plots the last few bouts in normalized coordinates, with fish facing to the right. """ def __init__(self, acc: QueueDataAccumulator, i_fish=0, n_bouts=10, n_save_max=300): super().__init__() self.title = "Bout shape" self.acc = acc self.bouts = deque() self.i_fish = i_fish self.processed_index = 0 self.detection_params = Parametrized(params=dict( threshold=Param(0.2, (0.01, 5.0)), n_without_crossing=Param(5, (0, 10)), pad_before=Param(5, (0, 20)), pad_after=Param(5, (0, 20)), min_bout_len=Param(1, (1, 30)), )) self.n_bouts = n_bouts self.old_coords = None self.i_curve = 0 self.n_save_max = n_save_max self.setLayout(QVBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self.btn_editparam = QPushButton("Detection parameters") self.btn_editparam.clicked.connect(self.edit_params) self.layout().addWidget(self.btn_editparam) self.wnd_params = None self.vmax = 0 self.lbl_vmax = QLabel() self.layout().addWidget(self.lbl_vmax) self.display_widget = pg.GraphicsLayoutWidget() self.layout().addWidget(self.display_widget) self.vb_display = pg.ViewBox() self.vb_display.setAspectLocked(True, 1) self.vb_display.setRange(xRange=[-1, 5], disableAutoRange=True) self.vb_display.invertY(True) self.display_widget.addItem(self.vb_display) self.bout_curves = [ pg.PlotCurveItem(connect="finite") for _ in range(self.n_bouts) ] self.colors = np.zeros(self.n_bouts) self.decay_constant = 0.99 self.bout_coords = None self.bout_state = BoutState(0, 0.0, 0, 0, 0) for c in self.bout_curves: self.vb_display.addItem(c) def edit_params(self): self.wnd_params = ParameterGui(self.detection_params) self.wnd_params.show() def update(self): if not self.isVisible(): return current_index = len(self.acc.stored_data) if current_index == 0 or current_index < self.processed_index + 2: return # Pull the new data from the accumulator new_coords = np.array( self.acc.stored_data[max(self.processed_index, current_index - self.n_save_max):current_index]) self.processed_index = current_index ix, iy, ith = (self.acc.header_dict["f{:d}_{}".format( self.i_fish, var)] for var in ["x", "y", "theta"]) new_coords = new_coords[:, [ix, iy, ith]] # if in the previous refresh we ended up inside a bout, there are still # coordinates left to process if self.old_coords is not None: pre_start = len(self.old_coords) new_coords = np.concatenate([self.old_coords, new_coords], 0) else: pre_start = 0 vel = np.sum(np.diff(new_coords[:, :2], axis=0)**2, axis=1) self.vmax = np.nanmax(vel) self.lbl_vmax.setText("max velocity sq {:.1f}".format(self.vmax)) if self.bout_coords is None: self.bout_coords = [new_coords[0, :]] new_bout = None if self.detection_params.threshold > 0: self.bout_coords, bout_finished, self.bout_state = find_bouts_online( vel, new_coords, self.bout_state, bout_coords=self.bout_coords, shift=pre_start, **self.detection_params.params.values, ) if bout_finished: new_bout = self.bout_coords self.bout_coords = [new_coords[0, :]] self.old_coords = new_coords[-self.n_save_max:, :] self.colors *= self.decay_constant if new_bout and len(new_bout) > 2: nb = normalise_bout(np.array(new_bout[1:])) self.bout_curves[self.i_curve].setData(x=nb[:, 0], y=nb[:, 1]) self.colors[self.i_curve] = 255 self.i_curve = (self.i_curve + 1) % self.n_bouts for i_c, (curve, color) in enumerate(zip(self.bout_curves, self.colors)): col = int(color) if col < 10: curve.setData(x=[], y=[]) else: curve.setPen((col, col, col))