Beispiel #1
0
    def initUI(self):

        OVER_CAPACITY = 750

        sld = QSlider(Qt.Orientations.Horizontal, self)
        sld.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        sld.setRange(1, OVER_CAPACITY)
        sld.setValue(75)
        sld.setGeometry(30, 40, 150, 30)

        self.c = Communicate()
        self.wid = BurningWidget()
        self.c.updateBW[int].connect(self.wid.setValue)

        sld.valueChanged[int].connect(self.changeValue)
        hbox = QHBoxLayout()
        hbox.addWidget(self.wid)
        vbox = QVBoxLayout()
        vbox.addStretch(1)
        vbox.addLayout(hbox)
        self.setLayout(vbox)

        self.setGeometry(300, 300, 390, 210)
        self.setWindowTitle('Burning widget')
        self.show()
Beispiel #2
0
    def initUI(self):

        lcd = QLCDNumber(self)
        sld = QSlider(Qt.Orientation.Horizontal, self)

        vbox = QVBoxLayout()
        vbox.addWidget(lcd)
        vbox.addWidget(sld)

        self.setLayout(vbox)
        sld.valueChanged.connect(lcd.display)

        self.setGeometry(300, 300, 350, 250)
        self.setWindowTitle('Signal and slot')
        self.show()
    def __init__(
        self,
        onValueChanged: Callable[[int], None],
        parent: Optional[QWidget] = None,
        *args: Tuple[Any, Any],
        **kwargs: Tuple[Any, Any],
    ) -> None:
        """
        Slow/fast slider for maze solver agents speed
        """
        super(MazeSolverSpeedControlView, self).__init__(parent=parent,
                                                         *args,
                                                         **kwargs)
        self.setContentsMargins(0, 0, 0, 0)

        layout = QVBoxLayout(self)
        layout.setContentsMargins(0, 0, 0, 0)

        slider = QSlider(Qt.Orientations.Horizontal)
        # minimum of 1 op/s (0 would result in a divide by zero op and thus crash)
        slider.setMinimum(1)
        # maximum of 50 op/s
        slider.setMaximum(70)
        # initial value in the middle
        slider.setValue(35)

        # connect the onValueChange event to the method passed to this widget
        slider.valueChanged.connect(onValueChanged)  # type: ignore

        # slow/fast horizontal labels view
        slowFastLabelsLayout = QHBoxLayout()
        slowFastLabelsLayout.setContentsMargins(0, 0, 0, 0)

        slowLabel = QLabel("Slow")
        fastLabel = QLabel("Fast")

        slowFastLabelsLayout.addWidget(slowLabel)
        slowFastLabelsLayout.addStretch()
        slowFastLabelsLayout.addWidget(fastLabel)

        layout.addWidget(slider)
        layout.addLayout(slowFastLabelsLayout)

        self.setLayout(layout)
Beispiel #4
0
    def initUI(self):

        sld = QSlider(Qt.Orientation.Horizontal, self)
        sld.setFocusPolicy(Qt.FocusPolicy.NoFocus)
        sld.setGeometry(30, 40, 200, 30)
        sld.valueChanged[int].connect(self.changeValue)

        self.label = QLabel(self)
        self.label.setPixmap(QPixmap('mute.png'))
        self.label.setGeometry(250, 40, 80, 30)

        self.setGeometry(300, 300, 350, 250)
        self.setWindowTitle('QSlider')
        self.show()
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        # region Create CartPole instance and load initial settings

        # Create CartPole instance
        self.initial_state = create_cartpole_state()
        self.CartPoleInstance = CartPole(initial_state=self.initial_state)

        # Set timescales
        self.CartPoleInstance.dt_simulation = dt_simulation
        self.CartPoleInstance.dt_controller = controller_update_interval
        self.CartPoleInstance.dt_save = save_interval

        # set other settings
        self.CartPoleInstance.set_controller(controller_init)
        self.CartPoleInstance.stop_at_90 = stop_at_90_init
        self.set_random_experiment_generator_init_params()

        # endregion

        # region Decide whether to save the data in "CartPole memory" or not
        self.save_history = save_history_init
        self.show_experiment_summary = show_experiment_summary_init
        if self.save_history or self.show_experiment_summary:
            self.CartPoleInstance.save_data_in_cart = True
        else:
            self.CartPoleInstance.save_data_in_cart = False

        # endregion

        # region Other variables initial values as provided in gui_default_parameters.py

        # Start user controlled experiment/ start random experiment/ load and replay - on start button
        self.simulator_mode = simulator_mode_init
        self.slider_on_click = slider_on_click_init  # Update slider on click/update slider while hoovering over it
        self.speedup = speedup_init  # Default simulation speed-up

        # endregion

        # region Initialize loop-timer
        # This timer allows to relate the simulation time to user time
        # And (if your computer is fast enough) run simulation
        # slower or faster than real-time by predefined factor (speedup)
        self.looper = loop_timer(
            dt_target=(self.CartPoleInstance.dt_simulation / self.speedup))
        # endregion

        # region Variables controlling the state of various processes (DO NOT MODIFY)

        self.terminate_experiment_or_replay_thread = False  # True: gives signal causing thread to terminate
        self.pause_experiment_or_replay_thread = False  # True: gives signal causing the thread to pause

        self.run_set_labels_thread = True  # True if gauges (labels) keep being repeatedly updated
        # Stop threads by setting False

        # Flag indicating if the "START! / STOP!" button should act as start or as stop when pressed.
        # Can take values "START!" or "STOP!"
        self.start_or_stop_action = "START!"
        # Flag indicating whether the pause button should pause or unpause.
        self.pause_or_unpause_action = "PAUSE"

        # Flag indicating that saving of experiment recording to csv file has finished
        self.experiment_or_replay_thread_terminated = False

        self.user_time_counter = 0  # Measures the user time

        # Slider instant value (which is draw in GUI) differs from value saved in CartPole instance
        # if the option updating slider "on-click" is enabled.
        self.slider_instant_value = self.CartPoleInstance.slider_value

        self.noise = 'OFF'
        self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise

        # endregion

        # region Create GUI Layout

        # region - Create container for top level layout
        layout = QVBoxLayout()
        # endregion

        # region - Change geometry of the main window
        self.setGeometry(300, 300, 2500, 1000)
        # endregion

        # region - Matplotlib figures (CartPole drawing and Slider)
        # Draw Figure
        self.fig = Figure(
            figsize=(25, 10)
        )  # Regulates the size of Figure in inches, before scaling to window size.
        self.canvas = FigureCanvas(self.fig)
        self.fig.AxCart = self.canvas.figure.add_subplot(211)
        self.fig.AxSlider = self.canvas.figure.add_subplot(212)
        self.fig.AxSlider.set_ylim(0, 1)

        self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart,
                                                     self.fig.AxSlider)

        # Attach figure to the layout
        lf = QVBoxLayout()
        lf.addWidget(self.canvas)

        # endregion

        # region - Radio buttons selecting current controller
        self.rbs_controllers = []
        for controller_name in self.CartPoleInstance.controller_names:
            self.rbs_controllers.append(QRadioButton(controller_name))

        # Ensures that radio buttons are exclusive
        self.controllers_buttons_group = QButtonGroup()
        for button in self.rbs_controllers:
            self.controllers_buttons_group.addButton(button)

        lr_c = QVBoxLayout()
        lr_c.addStretch(1)
        for rb in self.rbs_controllers:
            rb.clicked.connect(self.RadioButtons_controller_selection)
            lr_c.addWidget(rb)
        lr_c.addStretch(1)

        self.rbs_controllers[self.CartPoleInstance.controller_idx].setChecked(
            True)

        # endregion

        # region - Create central part of the layout for figures and radio buttons and add it to the whole layout
        lc = QHBoxLayout()
        lc.addLayout(lf)
        lc.addLayout(lr_c)
        layout.addLayout(lc)

        # endregion

        # region - Gauges displaying current values of various states and parameters (time, velocity, angle,...)

        # First row
        ld = QHBoxLayout()
        # User time
        self.labTime = QLabel("User's time (s): ")
        self.timer = QTimer()
        self.timer.setInterval(100)  # Tick every 1/10 of the second
        self.timer.timeout.connect(self.set_user_time_label)
        self.timer.start()
        ld.addWidget(self.labTime)
        # Speed, angle, motor power (Q)
        self.labSpeed = QLabel('Speed (m/s):')
        self.labAngle = QLabel('Angle (deg):')
        self.labMotor = QLabel('')
        self.labTargetPosition = QLabel('')
        ld.addWidget(self.labSpeed)
        ld.addWidget(self.labAngle)
        ld.addWidget(self.labMotor)
        ld.addWidget(self.labTargetPosition)
        layout.addLayout(ld)

        # Second row of labels
        # Simulation time, Measured (real) speed-up, slider-value
        ld2 = QHBoxLayout()
        self.labTimeSim = QLabel('Simulation Time (s):')
        ld2.addWidget(self.labTimeSim)
        self.labSpeedUp = QLabel('Speed-up (measured):')
        ld2.addWidget(self.labSpeedUp)
        self.labSliderInstant = QLabel('')
        ld2.addWidget(self.labSliderInstant)
        layout.addLayout(ld2)

        # endregion

        # region - Buttons "START!" / "STOP!", "PAUSE", "QUIT"
        self.bss = QPushButton("START!")
        self.bss.pressed.connect(self.start_stop_button)
        self.bp = QPushButton("PAUSE")
        self.bp.pressed.connect(self.pause_unpause_button)
        bq = QPushButton("QUIT")
        bq.pressed.connect(self.quit_application)
        lspb = QHBoxLayout()  # Sub-Layout for Start/Stop and Pause Buttons
        lspb.addWidget(self.bss)
        lspb.addWidget(self.bp)

        # endregion

        # region - Sliders setting initial state and buttons for kicking the pole

        # Sliders setting initial position and angle
        lb = QVBoxLayout()  # Layout for buttons
        lb.addLayout(lspb)
        lb.addWidget(bq)
        ip = QHBoxLayout()  # Layout for initial position sliders
        self.initial_position_slider = QSlider(
            orientation=Qt.Orientation.Horizontal)
        self.initial_position_slider.setRange(
            -int(float(1000 * TrackHalfLength)),
            int(float(1000 * TrackHalfLength)))
        self.initial_position_slider.setValue(0)
        self.initial_position_slider.setSingleStep(1)
        self.initial_position_slider.valueChanged.connect(
            self.update_initial_position)
        self.initial_angle_slider = QSlider(
            orientation=Qt.Orientation.Horizontal)
        self.initial_angle_slider.setRange(-int(float(100 * np.pi)),
                                           int(float(100 * np.pi)))
        self.initial_angle_slider.setValue(0)
        self.initial_angle_slider.setSingleStep(1)
        self.initial_angle_slider.valueChanged.connect(
            self.update_initial_angle)
        ip.addWidget(QLabel("Initial position:"))
        ip.addWidget(self.initial_position_slider)
        ip.addWidget(QLabel("Initial angle:"))
        ip.addWidget(self.initial_angle_slider)
        ip.addStretch(0.01)

        # Slider setting latency
        self.LATENCY_SLIDER_RANGE_INT = 1000
        self.latency_slider = QSlider(orientation=Qt.Orientation.Horizontal)
        self.latency_slider.setRange(0, self.LATENCY_SLIDER_RANGE_INT)
        self.latency_slider.setValue(
            int(self.CartPoleInstance.LatencyAdderInstance.latency *
                self.LATENCY_SLIDER_RANGE_INT /
                self.CartPoleInstance.LatencyAdderInstance.max_latency))
        self.latency_slider.setSingleStep(1)
        self.latency_slider.valueChanged.connect(self.update_latency)
        ip.addWidget(QLabel("Latency:"))
        ip.addWidget(self.latency_slider)
        self.labLatency = QLabel('Latency (ms): {:.1f}'.format(
            self.CartPoleInstance.LatencyAdderInstance.latency * 1000))
        ip.addWidget(self.labLatency)

        # Buttons activating noise
        self.rbs_noise = []
        for mode_name in ['ON', 'OFF']:
            self.rbs_noise.append(QRadioButton(mode_name))

        # Ensures that radio buttons are exclusive
        self.noise_buttons_group = QButtonGroup()
        for button in self.rbs_noise:
            self.noise_buttons_group.addButton(button)

        lr_n = QHBoxLayout()
        lr_n.addWidget(QLabel('Noise:'))
        for rb in self.rbs_noise:
            rb.clicked.connect(self.RadioButtons_noise_on_off)
            lr_n.addWidget(rb)

        self.rbs_noise[1].setChecked(True)

        ip.addStretch(0.01)
        ip.addLayout(lr_n)
        ip.addStretch(0.01)

        # Buttons giving kick to the pole
        kick_label = QLabel("Kick pole:")
        kick_left_button = QPushButton()
        kick_left_button.setText("Left")
        kick_left_button.adjustSize()
        kick_left_button.clicked.connect(self.kick_pole)
        kick_right_button = QPushButton()
        kick_right_button.setText("Right")
        kick_right_button.adjustSize()
        kick_right_button.clicked.connect(self.kick_pole)
        ip.addWidget(kick_label)
        ip.addWidget(kick_left_button)
        ip.addWidget(kick_right_button)

        lb.addLayout(ip)
        layout.addLayout(lb)

        # endregion

        # region - Text boxes and Combobox to provide settings concerning generation of random experiment
        l_generate_trace = QHBoxLayout()
        l_generate_trace.addWidget(QLabel('Random experiment settings:'))
        l_generate_trace.addWidget(QLabel('Length (s):'))
        self.textbox_length = QLineEdit()
        l_generate_trace.addWidget(self.textbox_length)
        l_generate_trace.addWidget(QLabel('Turning Points (m):'))
        self.textbox_turning_points = QLineEdit()
        l_generate_trace.addWidget(self.textbox_turning_points)
        l_generate_trace.addWidget(QLabel('Interpolation:'))
        self.cb_interpolation = QComboBox()
        self.cb_interpolation.addItems(
            ['0-derivative-smooth', 'linear', 'previous'])
        self.cb_interpolation.currentIndexChanged.connect(
            self.cb_interpolation_selectionchange)
        self.cb_interpolation.setCurrentText(
            self.CartPoleInstance.interpolation_type)
        l_generate_trace.addWidget(self.cb_interpolation)

        layout.addLayout(l_generate_trace)

        # endregion

        # region - Textbox to provide csv file name for saving or loading data
        l_text = QHBoxLayout()
        textbox_title = QLabel('CSV file name:')
        self.textbox = QLineEdit()
        l_text.addWidget(textbox_title)
        l_text.addWidget(self.textbox)
        layout.addLayout(l_text)

        # endregion

        # region - Make strip of layout for checkboxes
        l_cb = QHBoxLayout()
        # endregion

        # region - Textbox to provide the target speed-up value
        l_text_speedup = QHBoxLayout()
        tx_speedup_title = QLabel('Speed-up (target):')
        self.tx_speedup = QLineEdit()
        l_text_speedup.addWidget(tx_speedup_title)
        l_text_speedup.addWidget(self.tx_speedup)
        self.tx_speedup.setText(str(self.speedup))
        l_cb.addLayout(l_text_speedup)

        self.wrong_speedup_msg = QMessageBox()
        self.wrong_speedup_msg.setWindowTitle("Speed-up value problem")
        self.wrong_speedup_msg.setIcon(QMessageBox.Icon.Critical)
        # endregion

        # region - Checkboxes

        # region -- Checkbox: Save/don't save experiment recording
        self.cb_save_history = QCheckBox('Save results', self)
        if self.save_history:
            self.cb_save_history.toggle()
        self.cb_save_history.toggled.connect(self.cb_save_history_f)
        l_cb.addWidget(self.cb_save_history)
        # endregion

        # region -- Checkbox: Display plots showing dynamic evolution of the system as soon as experiment terminates
        self.cb_show_experiment_summary = QCheckBox('Show experiment summary',
                                                    self)
        if self.show_experiment_summary:
            self.cb_show_experiment_summary.toggle()
        self.cb_show_experiment_summary.toggled.connect(
            self.cb_show_experiment_summary_f)
        l_cb.addWidget(self.cb_show_experiment_summary)
        # endregion

        # region -- Checkbox: Block pole if it reaches +/-90 deg
        self.cb_stop_at_90_deg = QCheckBox('Stop-at-90-deg', self)
        if self.CartPoleInstance.stop_at_90:
            self.cb_stop_at_90_deg.toggle()
        self.cb_stop_at_90_deg.toggled.connect(self.cb_stop_at_90_deg_f)
        l_cb.addWidget(self.cb_stop_at_90_deg)
        # endregion

        # region -- Checkbox: Update slider on click/update slider while hoovering over it
        self.cb_slider_on_click = QCheckBox('Update slider on click', self)
        if self.slider_on_click:
            self.cb_slider_on_click.toggle()
        self.cb_slider_on_click.toggled.connect(self.cb_slider_on_click_f)
        l_cb.addWidget(self.cb_slider_on_click)

        # endregion

        # endregion

        # region - Radio buttons selecting simulator mode: user defined experiment, random experiment, replay

        # List available simulator modes - constant
        self.available_simulator_modes = [
            'Slider-Controlled Experiment', 'Random Experiment', 'Replay'
        ]
        self.rbs_simulator_mode = []
        for mode_name in self.available_simulator_modes:
            self.rbs_simulator_mode.append(QRadioButton(mode_name))

        # Ensures that radio buttons are exclusive
        self.simulator_mode_buttons_group = QButtonGroup()
        for button in self.rbs_simulator_mode:
            self.simulator_mode_buttons_group.addButton(button)

        lr_sm = QHBoxLayout()
        lr_sm.addStretch(1)
        lr_sm.addWidget(QLabel('Simulator mode:'))
        for rb in self.rbs_simulator_mode:
            rb.clicked.connect(self.RadioButtons_simulator_mode)
            lr_sm.addWidget(rb)
        lr_sm.addStretch(1)

        self.rbs_simulator_mode[self.available_simulator_modes.index(
            self.simulator_mode)].setChecked(True)

        l_cb.addStretch(1)
        l_cb.addLayout(lr_sm)
        l_cb.addStretch(1)

        # endregion

        # region - Add checkboxes to layout
        layout.addLayout(l_cb)
        # endregion

        # region - Create an instance of a GUI window
        w = QWidget()
        w.setLayout(layout)
        self.setCentralWidget(w)
        self.show()
        self.setWindowTitle('CartPole Simulator')

        # endregion

        # endregion

        # region Open controller-specific popup windows
        self.open_additional_controller_widget()
        # endregion

        # region Activate functions capturing mouse movements and clicks over the slider

        # This line links function capturing the mouse position on the canvas of the Figure
        self.canvas.mpl_connect("motion_notify_event", self.on_mouse_movement)
        # This line links function capturing the mouse position on the canvas of the Figure click
        self.canvas.mpl_connect("button_press_event", self.on_mouse_click)

        # endregion

        # region Introducing multithreading
        # To ensure smooth functioning of the app,
        # the calculations and redrawing of the figures have to be done in a different thread
        # than the one capturing the mouse position and running the animation
        self.threadpool = QThreadPool()
        # endregion

        # region Starts a thread repeatedly redrawing gauges (labels) of the GUI
        # It runs till the QUIT button is pressed
        worker_labels = Worker(self.set_labels_thread)
        self.threadpool.start(worker_labels)
        # endregion

        # region Start animation repeatedly redrawing changing elements of matplotlib figures (CartPole drawing and slider)
        # This animation runs ALWAYS when the GUI is open
        # The buttons of GUI only decide if new parameters are calculated or not
        self.anim = self.CartPoleInstance.run_animation(self.fig)
class MainWindow(QMainWindow):
    def __init__(self, *args, **kwargs):
        super(MainWindow, self).__init__(*args, **kwargs)

        # region Create CartPole instance and load initial settings

        # Create CartPole instance
        self.initial_state = create_cartpole_state()
        self.CartPoleInstance = CartPole(initial_state=self.initial_state)

        # Set timescales
        self.CartPoleInstance.dt_simulation = dt_simulation
        self.CartPoleInstance.dt_controller = controller_update_interval
        self.CartPoleInstance.dt_save = save_interval

        # set other settings
        self.CartPoleInstance.set_controller(controller_init)
        self.CartPoleInstance.stop_at_90 = stop_at_90_init
        self.set_random_experiment_generator_init_params()

        # endregion

        # region Decide whether to save the data in "CartPole memory" or not
        self.save_history = save_history_init
        self.show_experiment_summary = show_experiment_summary_init
        if self.save_history or self.show_experiment_summary:
            self.CartPoleInstance.save_data_in_cart = True
        else:
            self.CartPoleInstance.save_data_in_cart = False

        # endregion

        # region Other variables initial values as provided in gui_default_parameters.py

        # Start user controlled experiment/ start random experiment/ load and replay - on start button
        self.simulator_mode = simulator_mode_init
        self.slider_on_click = slider_on_click_init  # Update slider on click/update slider while hoovering over it
        self.speedup = speedup_init  # Default simulation speed-up

        # endregion

        # region Initialize loop-timer
        # This timer allows to relate the simulation time to user time
        # And (if your computer is fast enough) run simulation
        # slower or faster than real-time by predefined factor (speedup)
        self.looper = loop_timer(
            dt_target=(self.CartPoleInstance.dt_simulation / self.speedup))
        # endregion

        # region Variables controlling the state of various processes (DO NOT MODIFY)

        self.terminate_experiment_or_replay_thread = False  # True: gives signal causing thread to terminate
        self.pause_experiment_or_replay_thread = False  # True: gives signal causing the thread to pause

        self.run_set_labels_thread = True  # True if gauges (labels) keep being repeatedly updated
        # Stop threads by setting False

        # Flag indicating if the "START! / STOP!" button should act as start or as stop when pressed.
        # Can take values "START!" or "STOP!"
        self.start_or_stop_action = "START!"
        # Flag indicating whether the pause button should pause or unpause.
        self.pause_or_unpause_action = "PAUSE"

        # Flag indicating that saving of experiment recording to csv file has finished
        self.experiment_or_replay_thread_terminated = False

        self.user_time_counter = 0  # Measures the user time

        # Slider instant value (which is draw in GUI) differs from value saved in CartPole instance
        # if the option updating slider "on-click" is enabled.
        self.slider_instant_value = self.CartPoleInstance.slider_value

        self.noise = 'OFF'
        self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise

        # endregion

        # region Create GUI Layout

        # region - Create container for top level layout
        layout = QVBoxLayout()
        # endregion

        # region - Change geometry of the main window
        self.setGeometry(300, 300, 2500, 1000)
        # endregion

        # region - Matplotlib figures (CartPole drawing and Slider)
        # Draw Figure
        self.fig = Figure(
            figsize=(25, 10)
        )  # Regulates the size of Figure in inches, before scaling to window size.
        self.canvas = FigureCanvas(self.fig)
        self.fig.AxCart = self.canvas.figure.add_subplot(211)
        self.fig.AxSlider = self.canvas.figure.add_subplot(212)
        self.fig.AxSlider.set_ylim(0, 1)

        self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart,
                                                     self.fig.AxSlider)

        # Attach figure to the layout
        lf = QVBoxLayout()
        lf.addWidget(self.canvas)

        # endregion

        # region - Radio buttons selecting current controller
        self.rbs_controllers = []
        for controller_name in self.CartPoleInstance.controller_names:
            self.rbs_controllers.append(QRadioButton(controller_name))

        # Ensures that radio buttons are exclusive
        self.controllers_buttons_group = QButtonGroup()
        for button in self.rbs_controllers:
            self.controllers_buttons_group.addButton(button)

        lr_c = QVBoxLayout()
        lr_c.addStretch(1)
        for rb in self.rbs_controllers:
            rb.clicked.connect(self.RadioButtons_controller_selection)
            lr_c.addWidget(rb)
        lr_c.addStretch(1)

        self.rbs_controllers[self.CartPoleInstance.controller_idx].setChecked(
            True)

        # endregion

        # region - Create central part of the layout for figures and radio buttons and add it to the whole layout
        lc = QHBoxLayout()
        lc.addLayout(lf)
        lc.addLayout(lr_c)
        layout.addLayout(lc)

        # endregion

        # region - Gauges displaying current values of various states and parameters (time, velocity, angle,...)

        # First row
        ld = QHBoxLayout()
        # User time
        self.labTime = QLabel("User's time (s): ")
        self.timer = QTimer()
        self.timer.setInterval(100)  # Tick every 1/10 of the second
        self.timer.timeout.connect(self.set_user_time_label)
        self.timer.start()
        ld.addWidget(self.labTime)
        # Speed, angle, motor power (Q)
        self.labSpeed = QLabel('Speed (m/s):')
        self.labAngle = QLabel('Angle (deg):')
        self.labMotor = QLabel('')
        self.labTargetPosition = QLabel('')
        ld.addWidget(self.labSpeed)
        ld.addWidget(self.labAngle)
        ld.addWidget(self.labMotor)
        ld.addWidget(self.labTargetPosition)
        layout.addLayout(ld)

        # Second row of labels
        # Simulation time, Measured (real) speed-up, slider-value
        ld2 = QHBoxLayout()
        self.labTimeSim = QLabel('Simulation Time (s):')
        ld2.addWidget(self.labTimeSim)
        self.labSpeedUp = QLabel('Speed-up (measured):')
        ld2.addWidget(self.labSpeedUp)
        self.labSliderInstant = QLabel('')
        ld2.addWidget(self.labSliderInstant)
        layout.addLayout(ld2)

        # endregion

        # region - Buttons "START!" / "STOP!", "PAUSE", "QUIT"
        self.bss = QPushButton("START!")
        self.bss.pressed.connect(self.start_stop_button)
        self.bp = QPushButton("PAUSE")
        self.bp.pressed.connect(self.pause_unpause_button)
        bq = QPushButton("QUIT")
        bq.pressed.connect(self.quit_application)
        lspb = QHBoxLayout()  # Sub-Layout for Start/Stop and Pause Buttons
        lspb.addWidget(self.bss)
        lspb.addWidget(self.bp)

        # endregion

        # region - Sliders setting initial state and buttons for kicking the pole

        # Sliders setting initial position and angle
        lb = QVBoxLayout()  # Layout for buttons
        lb.addLayout(lspb)
        lb.addWidget(bq)
        ip = QHBoxLayout()  # Layout for initial position sliders
        self.initial_position_slider = QSlider(
            orientation=Qt.Orientation.Horizontal)
        self.initial_position_slider.setRange(
            -int(float(1000 * TrackHalfLength)),
            int(float(1000 * TrackHalfLength)))
        self.initial_position_slider.setValue(0)
        self.initial_position_slider.setSingleStep(1)
        self.initial_position_slider.valueChanged.connect(
            self.update_initial_position)
        self.initial_angle_slider = QSlider(
            orientation=Qt.Orientation.Horizontal)
        self.initial_angle_slider.setRange(-int(float(100 * np.pi)),
                                           int(float(100 * np.pi)))
        self.initial_angle_slider.setValue(0)
        self.initial_angle_slider.setSingleStep(1)
        self.initial_angle_slider.valueChanged.connect(
            self.update_initial_angle)
        ip.addWidget(QLabel("Initial position:"))
        ip.addWidget(self.initial_position_slider)
        ip.addWidget(QLabel("Initial angle:"))
        ip.addWidget(self.initial_angle_slider)
        ip.addStretch(0.01)

        # Slider setting latency
        self.LATENCY_SLIDER_RANGE_INT = 1000
        self.latency_slider = QSlider(orientation=Qt.Orientation.Horizontal)
        self.latency_slider.setRange(0, self.LATENCY_SLIDER_RANGE_INT)
        self.latency_slider.setValue(
            int(self.CartPoleInstance.LatencyAdderInstance.latency *
                self.LATENCY_SLIDER_RANGE_INT /
                self.CartPoleInstance.LatencyAdderInstance.max_latency))
        self.latency_slider.setSingleStep(1)
        self.latency_slider.valueChanged.connect(self.update_latency)
        ip.addWidget(QLabel("Latency:"))
        ip.addWidget(self.latency_slider)
        self.labLatency = QLabel('Latency (ms): {:.1f}'.format(
            self.CartPoleInstance.LatencyAdderInstance.latency * 1000))
        ip.addWidget(self.labLatency)

        # Buttons activating noise
        self.rbs_noise = []
        for mode_name in ['ON', 'OFF']:
            self.rbs_noise.append(QRadioButton(mode_name))

        # Ensures that radio buttons are exclusive
        self.noise_buttons_group = QButtonGroup()
        for button in self.rbs_noise:
            self.noise_buttons_group.addButton(button)

        lr_n = QHBoxLayout()
        lr_n.addWidget(QLabel('Noise:'))
        for rb in self.rbs_noise:
            rb.clicked.connect(self.RadioButtons_noise_on_off)
            lr_n.addWidget(rb)

        self.rbs_noise[1].setChecked(True)

        ip.addStretch(0.01)
        ip.addLayout(lr_n)
        ip.addStretch(0.01)

        # Buttons giving kick to the pole
        kick_label = QLabel("Kick pole:")
        kick_left_button = QPushButton()
        kick_left_button.setText("Left")
        kick_left_button.adjustSize()
        kick_left_button.clicked.connect(self.kick_pole)
        kick_right_button = QPushButton()
        kick_right_button.setText("Right")
        kick_right_button.adjustSize()
        kick_right_button.clicked.connect(self.kick_pole)
        ip.addWidget(kick_label)
        ip.addWidget(kick_left_button)
        ip.addWidget(kick_right_button)

        lb.addLayout(ip)
        layout.addLayout(lb)

        # endregion

        # region - Text boxes and Combobox to provide settings concerning generation of random experiment
        l_generate_trace = QHBoxLayout()
        l_generate_trace.addWidget(QLabel('Random experiment settings:'))
        l_generate_trace.addWidget(QLabel('Length (s):'))
        self.textbox_length = QLineEdit()
        l_generate_trace.addWidget(self.textbox_length)
        l_generate_trace.addWidget(QLabel('Turning Points (m):'))
        self.textbox_turning_points = QLineEdit()
        l_generate_trace.addWidget(self.textbox_turning_points)
        l_generate_trace.addWidget(QLabel('Interpolation:'))
        self.cb_interpolation = QComboBox()
        self.cb_interpolation.addItems(
            ['0-derivative-smooth', 'linear', 'previous'])
        self.cb_interpolation.currentIndexChanged.connect(
            self.cb_interpolation_selectionchange)
        self.cb_interpolation.setCurrentText(
            self.CartPoleInstance.interpolation_type)
        l_generate_trace.addWidget(self.cb_interpolation)

        layout.addLayout(l_generate_trace)

        # endregion

        # region - Textbox to provide csv file name for saving or loading data
        l_text = QHBoxLayout()
        textbox_title = QLabel('CSV file name:')
        self.textbox = QLineEdit()
        l_text.addWidget(textbox_title)
        l_text.addWidget(self.textbox)
        layout.addLayout(l_text)

        # endregion

        # region - Make strip of layout for checkboxes
        l_cb = QHBoxLayout()
        # endregion

        # region - Textbox to provide the target speed-up value
        l_text_speedup = QHBoxLayout()
        tx_speedup_title = QLabel('Speed-up (target):')
        self.tx_speedup = QLineEdit()
        l_text_speedup.addWidget(tx_speedup_title)
        l_text_speedup.addWidget(self.tx_speedup)
        self.tx_speedup.setText(str(self.speedup))
        l_cb.addLayout(l_text_speedup)

        self.wrong_speedup_msg = QMessageBox()
        self.wrong_speedup_msg.setWindowTitle("Speed-up value problem")
        self.wrong_speedup_msg.setIcon(QMessageBox.Icon.Critical)
        # endregion

        # region - Checkboxes

        # region -- Checkbox: Save/don't save experiment recording
        self.cb_save_history = QCheckBox('Save results', self)
        if self.save_history:
            self.cb_save_history.toggle()
        self.cb_save_history.toggled.connect(self.cb_save_history_f)
        l_cb.addWidget(self.cb_save_history)
        # endregion

        # region -- Checkbox: Display plots showing dynamic evolution of the system as soon as experiment terminates
        self.cb_show_experiment_summary = QCheckBox('Show experiment summary',
                                                    self)
        if self.show_experiment_summary:
            self.cb_show_experiment_summary.toggle()
        self.cb_show_experiment_summary.toggled.connect(
            self.cb_show_experiment_summary_f)
        l_cb.addWidget(self.cb_show_experiment_summary)
        # endregion

        # region -- Checkbox: Block pole if it reaches +/-90 deg
        self.cb_stop_at_90_deg = QCheckBox('Stop-at-90-deg', self)
        if self.CartPoleInstance.stop_at_90:
            self.cb_stop_at_90_deg.toggle()
        self.cb_stop_at_90_deg.toggled.connect(self.cb_stop_at_90_deg_f)
        l_cb.addWidget(self.cb_stop_at_90_deg)
        # endregion

        # region -- Checkbox: Update slider on click/update slider while hoovering over it
        self.cb_slider_on_click = QCheckBox('Update slider on click', self)
        if self.slider_on_click:
            self.cb_slider_on_click.toggle()
        self.cb_slider_on_click.toggled.connect(self.cb_slider_on_click_f)
        l_cb.addWidget(self.cb_slider_on_click)

        # endregion

        # endregion

        # region - Radio buttons selecting simulator mode: user defined experiment, random experiment, replay

        # List available simulator modes - constant
        self.available_simulator_modes = [
            'Slider-Controlled Experiment', 'Random Experiment', 'Replay'
        ]
        self.rbs_simulator_mode = []
        for mode_name in self.available_simulator_modes:
            self.rbs_simulator_mode.append(QRadioButton(mode_name))

        # Ensures that radio buttons are exclusive
        self.simulator_mode_buttons_group = QButtonGroup()
        for button in self.rbs_simulator_mode:
            self.simulator_mode_buttons_group.addButton(button)

        lr_sm = QHBoxLayout()
        lr_sm.addStretch(1)
        lr_sm.addWidget(QLabel('Simulator mode:'))
        for rb in self.rbs_simulator_mode:
            rb.clicked.connect(self.RadioButtons_simulator_mode)
            lr_sm.addWidget(rb)
        lr_sm.addStretch(1)

        self.rbs_simulator_mode[self.available_simulator_modes.index(
            self.simulator_mode)].setChecked(True)

        l_cb.addStretch(1)
        l_cb.addLayout(lr_sm)
        l_cb.addStretch(1)

        # endregion

        # region - Add checkboxes to layout
        layout.addLayout(l_cb)
        # endregion

        # region - Create an instance of a GUI window
        w = QWidget()
        w.setLayout(layout)
        self.setCentralWidget(w)
        self.show()
        self.setWindowTitle('CartPole Simulator')

        # endregion

        # endregion

        # region Open controller-specific popup windows
        self.open_additional_controller_widget()
        # endregion

        # region Activate functions capturing mouse movements and clicks over the slider

        # This line links function capturing the mouse position on the canvas of the Figure
        self.canvas.mpl_connect("motion_notify_event", self.on_mouse_movement)
        # This line links function capturing the mouse position on the canvas of the Figure click
        self.canvas.mpl_connect("button_press_event", self.on_mouse_click)

        # endregion

        # region Introducing multithreading
        # To ensure smooth functioning of the app,
        # the calculations and redrawing of the figures have to be done in a different thread
        # than the one capturing the mouse position and running the animation
        self.threadpool = QThreadPool()
        # endregion

        # region Starts a thread repeatedly redrawing gauges (labels) of the GUI
        # It runs till the QUIT button is pressed
        worker_labels = Worker(self.set_labels_thread)
        self.threadpool.start(worker_labels)
        # endregion

        # region Start animation repeatedly redrawing changing elements of matplotlib figures (CartPole drawing and slider)
        # This animation runs ALWAYS when the GUI is open
        # The buttons of GUI only decide if new parameters are calculated or not
        self.anim = self.CartPoleInstance.run_animation(self.fig)
        # endregion

    # region Thread performing CartPole experiment, slider-controlled or random
    # It iteratively updates  CartPole state and save data to a .csv file
    # It also put simulation time in relation to user time
    def experiment_thread(self):

        # Necessary only for debugging in Visual Studio Code IDE
        try:
            ptvsd.debug_this_thread()
        except:
            pass

        self.looper.start_loop()
        while not self.terminate_experiment_or_replay_thread:
            if self.pause_experiment_or_replay_thread:
                time.sleep(0.1)
            else:
                # Calculations of the Cart state in the next timestep
                self.CartPoleInstance.update_state()

                # Terminate thread if random experiment reached its maximal length
                if ((self.CartPoleInstance.use_pregenerated_target_position is
                     True) and (self.CartPoleInstance.time >=
                                self.CartPoleInstance.t_max_pre)):
                    self.terminate_experiment_or_replay_thread = True

                # FIXME: when Speedup empty in GUI I expected inf speedup but got error Loop timer was not initialized properly
                self.looper.sleep_leftover_time()

        # Save simulation history if user chose to do so at the end of the simulation
        if self.save_history:
            csv_name = self.textbox.text()
            self.CartPoleInstance.save_history_csv(
                csv_name=csv_name,
                mode='init',
                length_of_experiment=np.around(
                    self.CartPoleInstance.dict_history['time'][-1],
                    decimals=2))
            self.CartPoleInstance.save_history_csv(csv_name=csv_name,
                                                   mode='save offline')

        self.experiment_or_replay_thread_terminated = True

    # endregion

    # region Thread replaying a saved experiment recording
    def replay_thread(self):

        # Necessary only for debugging in Visual Studio Code IDE
        try:
            ptvsd.debug_this_thread()
        except:
            pass

        # Check what is in the csv textbox
        csv_name = self.textbox.text()

        # Load experiment history
        history_pd, filepath = self.CartPoleInstance.load_history_csv(
            csv_name=csv_name)

        # Set cartpole in the right mode (just to ensure slider behaves properly)
        with open(filepath, newline='') as f:
            reader = csv.reader(f)
            for line in reader:
                line = line[0]
                if line[:len('# Controller: ')] == '# Controller: ':
                    controller_set = self.CartPoleInstance.set_controller(
                        line[len('# Controller: '):].rstrip("\n"))
                    if controller_set:
                        self.rbs_controllers[self.CartPoleInstance.
                                             controller_idx].setChecked(True)
                    else:
                        self.rbs_controllers[1].setChecked(
                            True)  # Set first, but not manual stabilization
                    break

        # Augment the experiment history with simulation time step size
        dt = []
        row_iterator = history_pd.iterrows()
        _, last = next(row_iterator)  # take first item from row_iterator
        for i, row in row_iterator:
            dt.append(row['time'] - last['time'])
            last = row
        dt.append(dt[-1])
        history_pd['dt'] = np.array(dt)

        # Initialize loop timer (with arbitrary dt)
        replay_looper = loop_timer(dt_target=0.0)

        # Start looping over history
        replay_looper.start_loop()
        global L
        for index, row in history_pd.iterrows():
            self.CartPoleInstance.s[POSITION_IDX] = row['position']
            self.CartPoleInstance.s[POSITIOND_IDX] = row['positionD']
            self.CartPoleInstance.s[ANGLE_IDX] = row['angle']
            self.CartPoleInstance.time = row['time']
            self.CartPoleInstance.dt = row['dt']
            try:
                self.CartPoleInstance.u = row['u']
            except KeyError:
                pass
            self.CartPoleInstance.Q = row['Q']
            self.CartPoleInstance.target_position = row['target_position']
            if self.CartPoleInstance.controller_name == 'manual-stabilization':
                self.CartPoleInstance.slider_value = self.CartPoleInstance.Q
            else:
                self.CartPoleInstance.slider_value = self.CartPoleInstance.target_position / TrackHalfLength

            # TODO: Make it more general for all possible parameters
            try:
                L[...] = row['L']
            except KeyError:
                pass
            except:
                print('Error while assigning L')
                print("Unexpected error:", sys.exc_info()[0])
                print("Unexpected error:", sys.exc_info()[1])

            dt_target = (self.CartPoleInstance.dt / self.speedup)
            replay_looper.dt_target = dt_target

            replay_looper.sleep_leftover_time()

            if self.terminate_experiment_or_replay_thread:  # Means that stop button was pressed
                break

            while self.pause_experiment_or_replay_thread:  # Means that pause button was pressed
                time.sleep(0.1)

        if self.show_experiment_summary:
            self.CartPoleInstance.dict_history = history_pd.loc[:index].to_dict(
                orient='list')

        self.experiment_or_replay_thread_terminated = True

    # endregion

    # region "START! / STOP!" button -> run/stop slider-controlled experiment, random experiment or replay experiment recording

    # Actions to be taken when "START! / STOP!" button is clicked
    def start_stop_button(self):

        # If "START! / STOP!" button in "START!" mode...
        if self.start_or_stop_action == 'START!':
            self.bss.setText("STOP!")
            self.start_thread()

        # If "START! / STOP!" button in "STOP!" mode...
        elif self.start_or_stop_action == 'STOP!':
            self.bss.setText("START!")
            self.bp.setText("PAUSE")
            # This flag is periodically checked by thread. It terminates if set True.
            self.terminate_experiment_or_replay_thread = True
            # The stop_thread function is called automatically by the thread when it terminates
            # It is implemented this way, because thread my terminate not only due "STOP!" button
            # (e.g. replay thread when whole experiment is replayed)

    def pause_unpause_button(self):
        # Only Pause if experiment is running
        if self.pause_or_unpause_action == 'PAUSE' and self.start_or_stop_action == 'STOP!':
            self.pause_or_unpause_action = 'UNPAUSE'
            self.pause_experiment_or_replay_thread = True
            self.bp.setText("UNPAUSE")
        elif self.pause_or_unpause_action == 'UNPAUSE' and self.start_or_stop_action == 'STOP!':
            self.pause_or_unpause_action = 'PAUSE'
            self.pause_experiment_or_replay_thread = False
            self.bp.setText("PAUSE")

    # Run thread. works for all simulator modes.
    def start_thread(self):

        # Check if value provided in speed-up textbox makes sense
        # If not abort start
        speedup_updated = self.get_speedup()
        if not speedup_updated:
            return

        # Disable GUI elements for features which must not be changed in runtime
        # For other features changing in runtime may not cause errors, but will stay without effect for current run
        self.cb_save_history.setEnabled(False)
        for rb in self.rbs_simulator_mode:
            rb.setEnabled(False)
        for rb in self.rbs_controllers:
            rb.setEnabled(False)
        if self.simulator_mode != 'Replay':
            self.cb_show_experiment_summary.setEnabled(False)

        # Set user-provided initial values for state (or its part) of the CartPole
        # Search implementation for more detail
        # The following line is important as it let the user to set with the slider the starting target position
        # After the slider was reset at the end of last experiment
        # With the small sliders he can also adjust starting initial_state
        self.reset_variables(
            2,
            s=np.copy(self.initial_state),
            target_position=self.CartPoleInstance.target_position)

        if self.simulator_mode == 'Random Experiment':

            self.CartPoleInstance.use_pregenerated_target_position = True

            if self.textbox_length.text() == '':
                self.CartPoleInstance.length_of_experiment = length_of_experiment_init
            else:
                self.CartPoleInstance.length_of_experiment = float(
                    self.textbox_length.text())

            turning_points_list = []
            if self.textbox_turning_points.text() != '':
                for turning_point in self.textbox_turning_points.text().split(
                        ', '):
                    turning_points_list.append(float(turning_point))
            self.CartPoleInstance.turning_points = turning_points_list

            self.CartPoleInstance.setup_cartpole_random_experiment()

        self.looper.dt_target = self.CartPoleInstance.dt_simulation / self.speedup
        # Pass the function to execute
        if self.simulator_mode == "Replay":
            worker = Worker(self.replay_thread)
        elif self.simulator_mode == 'Slider-Controlled Experiment' or self.simulator_mode == 'Random Experiment':
            worker = Worker(self.experiment_thread)
        worker.signals.finished.connect(self.finish_thread)
        # Execute
        self.threadpool.start(worker)

        # Determine what should happen when "START! / STOP!" is pushed NEXT time
        self.start_or_stop_action = "STOP!"

    # finish_threads works for all simulation modes
    # Some lines mya be redundant for replay,
    # however as they do not take much computation time we leave them here
    # As it my code shorter, while hopefully still clear.
    # It is called automatically at the end of experiment_thread
    def finish_thread(self):

        self.CartPoleInstance.use_pregenerated_target_position = False
        self.initial_state = create_cartpole_state()
        self.initial_position_slider.setValue(0)
        self.initial_angle_slider.setValue(0)
        self.CartPoleInstance.s = self.initial_state

        # Some controllers may collect they own statistics about their usage and print it after experiment terminated
        if self.simulator_mode != 'Replay':
            try:
                self.CartPoleInstance.controller.controller_report()
            except:
                pass

        if self.show_experiment_summary:
            self.w_summary = SummaryWindow(
                summary_plots=self.CartPoleInstance.summary_plots)

        # Reset variables and redraw the figures
        self.reset_variables(0)

        # Draw figures
        self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart,
                                                     self.fig.AxSlider)
        self.canvas.draw()

        # Enable back all elements of GUI:
        self.cb_save_history.setEnabled(True)
        self.cb_show_experiment_summary.setEnabled(True)
        for rb in self.rbs_simulator_mode:
            rb.setEnabled(True)
        for rb in self.rbs_controllers:
            rb.setEnabled(True)

        self.start_or_stop_action = "START!"  # What should happen when "START! / STOP!" is pushed NEXT time

    # endregion

    # region Methods: "Get, set, reset, quit"

    # Set parameters from gui_default_parameters related to generating a random experiment target position
    def set_random_experiment_generator_init_params(self):
        self.CartPoleInstance.track_relative_complexity = track_relative_complexity_init
        self.CartPoleInstance.length_of_experiment = length_of_experiment_init
        self.CartPoleInstance.interpolation_type = interpolation_type_init
        self.CartPoleInstance.turning_points_period = turning_points_period_init
        self.CartPoleInstance.start_random_target_position_at = start_random_target_position_at_init
        self.CartPoleInstance.end_random_target_position_at = end_random_target_position_at_init
        self.CartPoleInstance.turning_points = turning_points_init

    # Method resetting variables which change during experimental run
    def reset_variables(self, reset_mode=1, s=None, target_position=None):
        self.CartPoleInstance.set_cartpole_state_at_t0(
            reset_mode, s=s, target_position=target_position)
        self.user_time_counter = 0
        # "Try" because this function is called for the first time during initialisation of the Window
        # when the timer label instance is not yer there.
        try:
            self.labt.setText("Time (s): " +
                              str(float(self.user_time_counter) / 10.0))
        except:
            pass
        self.experiment_or_replay_thread_terminated = False  # This is a flag informing thread terminated
        self.terminate_experiment_or_replay_thread = False  # This is a command to terminate a thread
        self.pause_experiment_or_replay_thread = False  # This is a command to pause a thread
        self.start_or_stop_action = "START!"
        self.pause_or_unpause_action = "PAUSE"
        self.looper.first_call_done = False

    ######################################################################################################

    # (Marcin) Below are methods with less critical functions.

    # A thread redrawing labels (except for timer, which has its own function) of GUI every 0.1 s
    def set_labels_thread(self):
        while (self.run_set_labels_thread):
            self.labSpeed.setText(
                "Speed (m/s): " +
                str(np.around(self.CartPoleInstance.s[POSITIOND_IDX], 2)))
            self.labAngle.setText("Angle (deg): " + str(
                np.around(
                    self.CartPoleInstance.s[ANGLE_IDX] * 360 /
                    (2 * np.pi), 2)))
            self.labMotor.setText("Motor power (Q): {:.3f}".format(
                np.around(self.CartPoleInstance.Q, 2)))
            if self.CartPoleInstance.controller_name == 'manual-stabilization':
                self.labTargetPosition.setText("")
            else:
                self.labTargetPosition.setText(
                    "Target position (m): " +
                    str(np.around(self.CartPoleInstance.target_position, 2)))

            if self.CartPoleInstance.controller_name == 'manual_stabilization':
                self.labSliderInstant.setText(
                    "Slider instant value (-): " +
                    str(np.around(self.slider_instant_value, 2)))
            else:
                self.labSliderInstant.setText(
                    "Slider instant value (m): " +
                    str(np.around(self.slider_instant_value, 2)))

            self.labTimeSim.setText('Simulation time (s): {:.2f}'.format(
                self.CartPoleInstance.time))

            mean_dt_real = np.mean(self.looper.circ_buffer_dt_real)
            if mean_dt_real > 0:
                self.labSpeedUp.setText('Speed-up (measured): x{:.2f}'.format(
                    self.CartPoleInstance.dt_simulation / mean_dt_real))
            sleep(0.1)

    # Function to measure the time of simulation as experienced by user
    # It corresponds to the time of simulation according to equations only if real time mode is on
    # TODO (Marcin) I just retained this function from some example being my starting point
    #   It seems it sometimes counting time to slow. Consider replacing in future
    def set_user_time_label(self):
        # "If": Increment time counter only if simulation is running
        if self.start_or_stop_action == "STOP!":  # indicates what start button was pressed and some process is running
            self.user_time_counter += 1
            # The updates are done smoother if the label is updated here
            # and not in the separate thread
            self.labTime.setText("Time (s): " +
                                 str(float(self.user_time_counter) / 10.0))

    # The actions which has to be taken to properly terminate the application
    # The method is evoked after QUIT button is pressed
    # TODO: Can we connect it somehow also the the default cross closing the application?
    def quit_application(self):
        # Stops animation (updating changing elements of the Figure)
        self.anim._stop()
        # Stops the two threads updating the GUI labels and updating the state of Cart instance
        self.run_set_labels_thread = False
        self.terminate_experiment_or_replay_thread = True
        self.pause_experiment_or_replay_thread = False
        # Closes the GUI window
        self.close()
        # The standard command
        # It seems however not to be working by its own
        # I don't know how it works
        QApplication.quit()

    # endregion

    # region Mouse interaction
    """
    These are some methods GUI uses to capture mouse effect while hoovering or clicking over/on the charts
    """

    # Function evoked at a mouse movement
    # If the mouse cursor is over the lower chart it reads the corresponding value
    # and updates the slider
    def on_mouse_movement(self, event):
        if self.simulator_mode == 'Slider-Controlled Experiment':
            if event.xdata == None or event.ydata == None:
                pass
            else:
                if event.inaxes == self.fig.AxSlider:
                    self.slider_instant_value = event.xdata
                    if not self.slider_on_click:
                        self.CartPoleInstance.update_slider(
                            mouse_position=event.xdata)

    # Function evoked at a mouse click
    # If the mouse cursor is over the lower chart it reads the corresponding value
    # and updates the slider
    def on_mouse_click(self, event):
        if self.simulator_mode == 'Slider-Controlled Experiment':
            if event.xdata == None or event.ydata == None:
                pass
            else:
                if event.inaxes == self.fig.AxSlider:
                    self.CartPoleInstance.update_slider(
                        mouse_position=event.xdata)

    # endregion

    # region Changing "static" options: radio buttons, text boxes, combo boxes, check boxes
    """
    This section collects methods used to change some ''static option'':
    e.g. change current controller, switch between saving and not saving etc.
    These are functions associated with radio buttons, check boxes, textfilds etc.
    The functions of "START! / STOP!" button is much more complex
    and we put them hence in a separate section.
    """

    # region - Radio buttons

    # Chose the controller method which should be used with the CartPole
    def RadioButtons_controller_selection(self):
        # Change the mode variable depending on the Radiobutton state
        for i in range(len(self.rbs_controllers)):
            if self.rbs_controllers[i].isChecked():
                self.CartPoleInstance.set_controller(controller_idx=i)

        # Reset the state of GUI and of the Cart instance after the mode has changed
        # TODO: Do I need the follwowing lines?
        self.reset_variables(0)
        self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart,
                                                     self.fig.AxSlider)
        self.canvas.draw()

        self.open_additional_controller_widget()

    # Chose the simulator mode - effect of start/stop button
    def RadioButtons_simulator_mode(self):
        # Change the mode variable depending on the Radiobutton state
        for i in range(len(self.rbs_simulator_mode)):
            sleep(0.001)
            if self.rbs_simulator_mode[i].isChecked():
                self.simulator_mode = self.available_simulator_modes[i]

        # Reset the state of GUI and of the Cart instance after the mode has changed
        # TODO: Do I need the follwowing lines?
        self.reset_variables(0)
        self.CartPoleInstance.draw_constant_elements(self.fig, self.fig.AxCart,
                                                     self.fig.AxSlider)
        self.canvas.draw()

    # Chose the noise mode - effect of start/stop button
    def RadioButtons_noise_on_off(self):
        # Change the mode variable depending on the Radiobutton state
        if self.rbs_noise[0].isChecked():
            self.noise = 'ON'
            self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise
        elif self.rbs_noise[1].isChecked():
            self.noise = 'OFF'
            self.CartPoleInstance.NoiseAdderInstance.noise_mode = self.noise
        else:
            raise Exception('Something wrong with ON/OFF button for noise')

        self.open_additional_noise_widget()

    # endregion

    # region - Text Boxes

    # Read speedup provided by user from appropriate GUI textbox
    def get_speedup(self):
        """
        Get speedup provided by user from appropriate textbox.
        Speed-up gives how many times faster or slower than real time the simulation or replay should run.
        The provided values may not always be reached due to computer speed limitation
        """
        speedup = self.tx_speedup.text()
        if speedup == '':
            self.speedup = np.inf
            return True
        else:
            try:
                speedup = float(speedup)
            except ValueError:
                self.wrong_speedup_msg.setText(
                    'You have provided the input for speed-up which is not convertible to a number'
                )
                x = self.wrong_speedup_msg.exec_()
                return False
            if speedup == 0.0:
                self.wrong_speedup_msg.setText(
                    'You cannot run an experiment with 0 speed-up (stopped time flow)'
                )
                x = self.wrong_speedup_msg.exec_()
                return False
            else:
                self.speedup = speedup
                return True

    # endregion

    # region - Combo Boxes

    # Select how to interpolate between turning points of randomly chosen target positions
    def cb_interpolation_selectionchange(self, i):
        """
        Select interpolation type for random target positions of randomly generated experiment
        """
        self.CartPoleInstance.interpolation_type = self.cb_interpolation.currentText(
        )

    # endregion

    # region - Check boxes

    # Action toggling between saving and not saving simulation results
    def cb_save_history_f(self, state):

        if state:
            self.save_history = 1
        else:
            self.save_history = 0

        if self.save_history or self.show_experiment_summary:
            self.CartPoleInstance.save_data_in_cart = True
        else:
            self.CartPoleInstance.save_data_in_cart = False

    # Action toggling between saving and not saving simulation results
    def cb_show_experiment_summary_f(self, state):

        if state:
            self.show_experiment_summary = 1
        else:
            self.show_experiment_summary = 0

        if self.save_history or self.show_experiment_summary:
            self.CartPoleInstance.save_data_in_cart = True
        else:
            self.CartPoleInstance.save_data_in_cart = False

    # Action toggling between stopping (or not) the pole if it reaches 90 deg
    def cb_stop_at_90_deg_f(self, state):

        if state:
            self.CartPoleInstance.stop_at_90 = True
        else:
            self.CartPoleInstance.stop_at_90 = False

    # Action toggling between updating CarPole slider value on click or by hoovering over it
    def cb_slider_on_click_f(self, state):

        if state:
            self.slider_on_click = True
        else:
            self.slider_on_click = False

    # endregion

    # region - Additional GUI Popups

    def open_additional_controller_widget(self):
        # Open up additional options widgets depending on the controller type
        if self.CartPoleInstance.controller_name == 'mppi':
            self.optionsControllerWidget = MPPIOptionsWindow()
        else:
            try:
                self.optionsControllerWidget.close()
            except:
                pass
            self.optionsControllerWidget = None

    def open_additional_noise_widget(self):
        # Open up additional options widgets depending on the controller type
        if self.noise == 'ON':
            self.optionsNoiseWidget = NoiseOptionsWindow()
        else:
            try:
                self.optionsNoiseWidget.close()
            except:
                pass
            self.optionsNoiseWidget = None

    # endregion

    # region - Sliders setting initial position and angle of the CartPole

    def update_initial_position(self, value: str):
        self.initial_state[POSITION_IDX] = float(value) / 1000.0

    def update_initial_angle(self, value: str):
        self.initial_state[ANGLE_IDX] = float(value) / 100.0

    # endregion

    # region - Slider setting latency of the controller

    def update_latency(self, value: str):
        latency_slider = float(value)
        latency = latency_slider * self.CartPoleInstance.LatencyAdderInstance.max_latency / self.LATENCY_SLIDER_RANGE_INT  # latency in seconds
        self.CartPoleInstance.LatencyAdderInstance.set_latency(latency)
        self.labLatency.setText('{:.1f} ms'.format(latency *
                                                   1000.0))  # latency in ms

    # endregion

    # region Buttons for providing a kick to the pole

    def kick_pole(self):
        if self.sender().text() == "Left":
            self.CartPoleInstance.s[ANGLED_IDX] += .6
        elif self.sender().text() == "Right":
            self.CartPoleInstance.s[ANGLED_IDX] -= .6
Beispiel #7
0
    def initUI(self):
        self.grid = QGridLayout(self)

        self.file_control_grid = QGridLayout()
        self.file_control_grid.setSpacing(3)
        self.grid.addLayout(self.file_control_grid, 0, 0)

        self.file_tree_grid = QGridLayout()
        self.file_tree_grid.setSpacing(3)
        self.grid.addLayout(self.file_tree_grid, 1, 0)

        self.group_control_grid = QGridLayout()
        self.group_control_grid.setSpacing(3)
        self.grid.addLayout(self.group_control_grid, 0, 1)

        self.attribute_grid = QGridLayout()
        self.attribute_grid.setSpacing(3)
        self.grid.addLayout(self.attribute_grid, 1, 1)

        self.roi_control_grid = QGridLayout()
        self.roi_control_grid.setSpacing(3)
        self.grid.addLayout(self.roi_control_grid, 0, 2)

        self.plot_grid = QGridLayout()
        self.plot_grid.setSpacing(3)
        self.grid.addLayout(self.plot_grid, 1, 2)

        # # # # File control browser: # # # # # # # # (0,0)
        loadButton = QPushButton("Load expt. file", self)
        loadButton.clicked.connect(self.selectDataFile)
        # Label with current expt file
        self.currentExperimentLabel = QLabel('')
        self.file_control_grid.addWidget(loadButton, 0, 0)
        self.file_control_grid.addWidget(self.currentExperimentLabel, 1, 0)

        directoryButton = QPushButton("Select data directory", self)
        directoryButton.clicked.connect(self.selectDataDirectory)
        self.file_control_grid.addWidget(directoryButton, 0, 1)
        self.data_directory_display = QLabel('')
        self.data_directory_display.setFont(QtGui.QFont('SansSerif', 8))
        self.file_control_grid.addWidget(self.data_directory_display, 1, 1)

        # Attach metadata to file
        attachDatabutton = QPushButton("Attach metadata to file", self)
        attachDatabutton.clicked.connect(self.attachData)
        self.file_control_grid.addWidget(attachDatabutton, 2, 0, 1, 2)

        # Select image data file
        selectImageDataFileButton = QPushButton("Select image data file", self)
        selectImageDataFileButton.clicked.connect(self.selectImageDataFile)
        self.file_control_grid.addWidget(selectImageDataFileButton, 3, 0, 1, 2)

        # # # # File tree: # # # # # # # #  (1,0)
        self.groupTree = QTreeWidget(self)
        self.groupTree.setHeaderHidden(True)
        self.groupTree.itemClicked.connect(self.onTreeItemClicked)
        self.file_tree_grid.addWidget(self.groupTree, 3, 0, 2, 7)

        # # # # Group control: # # # # # # # # (0, 1)
        deleteGroupButton = QPushButton("Delete selected group", self)
        deleteGroupButton.clicked.connect(self.deleteSelectedGroup)
        self.group_control_grid.addWidget(deleteGroupButton, 0, 0, 1, 2)

        # File name display
        self.currentImageFileNameLabel = QLabel('')
        self.group_control_grid.addWidget(self.currentImageFileNameLabel, 1, 0)

        # Channel drop down
        ch_label = QLabel('Channel:')
        self.ChannelComboBox = QComboBox(self)
        self.ChannelComboBox.addItem("1")
        self.ChannelComboBox.addItem("0")
        self.ChannelComboBox.activated.connect(self.selectChannel)
        self.group_control_grid.addWidget(ch_label, 2, 0)
        self.group_control_grid.addWidget(self.ChannelComboBox, 2, 1)

        # # # # Attribute table: # # # # # # # # (1, 1)
        self.tableAttributes = QTableWidget()
        self.tableAttributes.setStyleSheet("")
        self.tableAttributes.setColumnCount(2)
        self.tableAttributes.setObjectName("tableAttributes")
        self.tableAttributes.setRowCount(0)
        item = QTableWidgetItem()
        font = QtGui.QFont()
        font.setPointSize(10)
        item.setFont(font)
        item.setBackground(QtGui.QColor(121, 121, 121))
        brush = QtGui.QBrush(QtGui.QColor(91, 91, 91))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        item.setForeground(brush)
        self.tableAttributes.setHorizontalHeaderItem(0, item)
        item = QTableWidgetItem()
        item.setBackground(QtGui.QColor(123, 123, 123))
        brush = QtGui.QBrush(QtGui.QColor(91, 91, 91))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        item.setForeground(brush)
        self.tableAttributes.setHorizontalHeaderItem(1, item)
        self.tableAttributes.horizontalHeader().setCascadingSectionResizes(
            True)
        self.tableAttributes.horizontalHeader().setHighlightSections(False)
        self.tableAttributes.horizontalHeader().setSortIndicatorShown(True)
        self.tableAttributes.horizontalHeader().setStretchLastSection(True)
        self.tableAttributes.verticalHeader().setVisible(False)
        self.tableAttributes.verticalHeader().setHighlightSections(False)
        item = self.tableAttributes.horizontalHeaderItem(0)
        item.setText("Attribute")
        item = self.tableAttributes.horizontalHeaderItem(1)
        item.setText("Value")

        self.tableAttributes.itemChanged.connect(self.update_attrs_to_file)
        self.attribute_grid.addWidget(self.tableAttributes, 3, 0, 1, 8)

        # # # # Roi control # # # # # # # # (0, 2)
        # ROI type drop-down
        self.RoiTypeComboBox = QComboBox(self)
        self.RoiTypeComboBox.addItem("freehand")
        radii = [1, 2, 3, 4, 6, 8]
        for radius in radii:
            self.RoiTypeComboBox.addItem("circle:" + str(radius))
        self.RoiTypeComboBox.activated.connect(self.selectRoiType)
        self.roi_control_grid.addWidget(self.RoiTypeComboBox, 0, 0)

        # Clear all ROIs button
        self.clearROIsButton = QPushButton("Clear ROIs", self)
        self.clearROIsButton.clicked.connect(self.clearRois)
        self.roi_control_grid.addWidget(self.clearROIsButton, 0, 2)

        # Response display type dropdown
        self.RoiResponseTypeComboBox = QComboBox(self)

        self.RoiResponseTypeComboBox.addItem("RawTrace")
        self.RoiResponseTypeComboBox.addItem("TrialAverage")
        self.RoiResponseTypeComboBox.addItem("TrialResponses")
        self.RoiResponseTypeComboBox.addItem("TrialAverageDFF")
        self.roi_control_grid.addWidget(self.RoiResponseTypeComboBox, 2, 2)

        # ROIset file name line edit box
        self.defaultRoiSetName = "roi_set_name"
        self.le_roiSetName = QLineEdit(self.defaultRoiSetName)
        self.roi_control_grid.addWidget(self.le_roiSetName, 1, 1)

        # Save ROIs button
        self.saveROIsButton = QPushButton("Save ROIs", self)
        self.saveROIsButton.clicked.connect(self.saveRois)
        self.roi_control_grid.addWidget(self.saveROIsButton, 1, 0)

        # Load ROI set combobox
        self.loadROIsComboBox = QComboBox(self)
        self.loadROIsComboBox.addItem("(load existing ROI set)")
        self.loadROIsComboBox.activated.connect(self.selectedExistingRoiSet)
        self.roi_control_grid.addWidget(self.loadROIsComboBox, 1, 2)
        self.updateExistingRoiSetList()

        # Delete current roi button
        self.deleteROIButton = QPushButton("Delete ROI", self)
        self.deleteROIButton.clicked.connect(self.deleteRoi)
        self.roi_control_grid.addWidget(self.deleteROIButton, 2, 0)

        # Current roi slider
        self.roiSlider = QSlider(QtCore.Qt.Orientation.Horizontal, self)
        self.roiSlider.setMinimum(0)
        self.roiSlider.setMaximum(self.max_rois)
        self.roiSlider.valueChanged.connect(self.sliderUpdated)
        self.roi_control_grid.addWidget(self.roiSlider, 2, 1, 1, 1)

        ctx = plt.rc_context({
            'xtick.major.size': 1,
            'axes.spines.top': False,
            'axes.spines.right': False,
            'xtick.labelsize': 'xx-small',
            'ytick.labelsize': 'xx-small',
            'xtick.major.size': 1.0,
            'ytick.major.size': 1.0,
            'xtick.major.pad': 1.0,
            'ytick.major.pad': 1.0
        })
        with ctx:
            self.responseFig = plt.figure(frameon=False, layout='constrained')
            self.responsePlot = self.responseFig.add_subplot(111)
            self.responseCanvas = FigureCanvas(self.responseFig)
        self.responseCanvas.draw_idle()
        self.plot_grid.addWidget(self.responseCanvas, 0, 0)

        # # # # Image canvas # # # # # # # # (1, 2)
        self.roi_fig = plt.figure()
        self.roi_ax = self.roi_fig.add_subplot(111)
        self.roi_canvas = FigureCanvas(self.roi_fig)
        self.toolbar = NavigationToolbar(self.roi_canvas, self)
        self.roi_ax.set_aspect('equal')
        self.roi_ax.set_axis_off()
        self.plot_grid.addWidget(self.toolbar, 1, 0)
        self.plot_grid.addWidget(self.roi_canvas, 2, 0)
        self.plot_grid.setRowStretch(0, 1)
        self.plot_grid.setRowStretch(1, 3)
        self.plot_grid.setRowStretch(2, 3)

        # Current z slice slider
        self.zSlider = QSlider(QtCore.Qt.Orientation.Horizontal, self)
        self.zSlider.setMinimum(0)
        self.zSlider.setMaximum(50)
        self.zSlider.setValue(0)
        self.zSlider.valueChanged.connect(self.zSliderUpdated)
        self.plot_grid.addWidget(self.zSlider, 3, 0)

        self.roi_fig.tight_layout()

        self.setWindowTitle('Visanalysis')
        self.setGeometry(200, 200, 1200, 600)
        self.show()
Beispiel #8
0
class DataGUI(QWidget):
    def __init__(self):
        super().__init__()

        self.experiment_file_name = None
        self.experiment_file_directory = None
        self.data_directory = None
        self.max_rois = 50
        self.roi_type = 'freehand'
        self.roi_radius = None
        self.existing_roi_set_paths = {}

        self.current_roi_index = 0
        self.current_z_slice = 0
        self.current_channel = 1  # index
        self.image_series_name = ''
        self.series_number = None
        self.roi_response = []
        self.roi_mask = []
        self.roi_path = []
        self.roi_image = None
        self.roi_path_list = []

        self.blank_image = np.zeros((1, 1))

        self.colors = [
            mcolors.to_rgb(x) for x in list(mcolors.TABLEAU_COLORS)[:20]
        ]

        self.initUI()

    def initUI(self):
        self.grid = QGridLayout(self)

        self.file_control_grid = QGridLayout()
        self.file_control_grid.setSpacing(3)
        self.grid.addLayout(self.file_control_grid, 0, 0)

        self.file_tree_grid = QGridLayout()
        self.file_tree_grid.setSpacing(3)
        self.grid.addLayout(self.file_tree_grid, 1, 0)

        self.group_control_grid = QGridLayout()
        self.group_control_grid.setSpacing(3)
        self.grid.addLayout(self.group_control_grid, 0, 1)

        self.attribute_grid = QGridLayout()
        self.attribute_grid.setSpacing(3)
        self.grid.addLayout(self.attribute_grid, 1, 1)

        self.roi_control_grid = QGridLayout()
        self.roi_control_grid.setSpacing(3)
        self.grid.addLayout(self.roi_control_grid, 0, 2)

        self.plot_grid = QGridLayout()
        self.plot_grid.setSpacing(3)
        self.grid.addLayout(self.plot_grid, 1, 2)

        # # # # File control browser: # # # # # # # # (0,0)
        loadButton = QPushButton("Load expt. file", self)
        loadButton.clicked.connect(self.selectDataFile)
        # Label with current expt file
        self.currentExperimentLabel = QLabel('')
        self.file_control_grid.addWidget(loadButton, 0, 0)
        self.file_control_grid.addWidget(self.currentExperimentLabel, 1, 0)

        directoryButton = QPushButton("Select data directory", self)
        directoryButton.clicked.connect(self.selectDataDirectory)
        self.file_control_grid.addWidget(directoryButton, 0, 1)
        self.data_directory_display = QLabel('')
        self.data_directory_display.setFont(QtGui.QFont('SansSerif', 8))
        self.file_control_grid.addWidget(self.data_directory_display, 1, 1)

        # Attach metadata to file
        attachDatabutton = QPushButton("Attach metadata to file", self)
        attachDatabutton.clicked.connect(self.attachData)
        self.file_control_grid.addWidget(attachDatabutton, 2, 0, 1, 2)

        # Select image data file
        selectImageDataFileButton = QPushButton("Select image data file", self)
        selectImageDataFileButton.clicked.connect(self.selectImageDataFile)
        self.file_control_grid.addWidget(selectImageDataFileButton, 3, 0, 1, 2)

        # # # # File tree: # # # # # # # #  (1,0)
        self.groupTree = QTreeWidget(self)
        self.groupTree.setHeaderHidden(True)
        self.groupTree.itemClicked.connect(self.onTreeItemClicked)
        self.file_tree_grid.addWidget(self.groupTree, 3, 0, 2, 7)

        # # # # Group control: # # # # # # # # (0, 1)
        deleteGroupButton = QPushButton("Delete selected group", self)
        deleteGroupButton.clicked.connect(self.deleteSelectedGroup)
        self.group_control_grid.addWidget(deleteGroupButton, 0, 0, 1, 2)

        # File name display
        self.currentImageFileNameLabel = QLabel('')
        self.group_control_grid.addWidget(self.currentImageFileNameLabel, 1, 0)

        # Channel drop down
        ch_label = QLabel('Channel:')
        self.ChannelComboBox = QComboBox(self)
        self.ChannelComboBox.addItem("1")
        self.ChannelComboBox.addItem("0")
        self.ChannelComboBox.activated.connect(self.selectChannel)
        self.group_control_grid.addWidget(ch_label, 2, 0)
        self.group_control_grid.addWidget(self.ChannelComboBox, 2, 1)

        # # # # Attribute table: # # # # # # # # (1, 1)
        self.tableAttributes = QTableWidget()
        self.tableAttributes.setStyleSheet("")
        self.tableAttributes.setColumnCount(2)
        self.tableAttributes.setObjectName("tableAttributes")
        self.tableAttributes.setRowCount(0)
        item = QTableWidgetItem()
        font = QtGui.QFont()
        font.setPointSize(10)
        item.setFont(font)
        item.setBackground(QtGui.QColor(121, 121, 121))
        brush = QtGui.QBrush(QtGui.QColor(91, 91, 91))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        item.setForeground(brush)
        self.tableAttributes.setHorizontalHeaderItem(0, item)
        item = QTableWidgetItem()
        item.setBackground(QtGui.QColor(123, 123, 123))
        brush = QtGui.QBrush(QtGui.QColor(91, 91, 91))
        brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern)
        item.setForeground(brush)
        self.tableAttributes.setHorizontalHeaderItem(1, item)
        self.tableAttributes.horizontalHeader().setCascadingSectionResizes(
            True)
        self.tableAttributes.horizontalHeader().setHighlightSections(False)
        self.tableAttributes.horizontalHeader().setSortIndicatorShown(True)
        self.tableAttributes.horizontalHeader().setStretchLastSection(True)
        self.tableAttributes.verticalHeader().setVisible(False)
        self.tableAttributes.verticalHeader().setHighlightSections(False)
        item = self.tableAttributes.horizontalHeaderItem(0)
        item.setText("Attribute")
        item = self.tableAttributes.horizontalHeaderItem(1)
        item.setText("Value")

        self.tableAttributes.itemChanged.connect(self.update_attrs_to_file)
        self.attribute_grid.addWidget(self.tableAttributes, 3, 0, 1, 8)

        # # # # Roi control # # # # # # # # (0, 2)
        # ROI type drop-down
        self.RoiTypeComboBox = QComboBox(self)
        self.RoiTypeComboBox.addItem("freehand")
        radii = [1, 2, 3, 4, 6, 8]
        for radius in radii:
            self.RoiTypeComboBox.addItem("circle:" + str(radius))
        self.RoiTypeComboBox.activated.connect(self.selectRoiType)
        self.roi_control_grid.addWidget(self.RoiTypeComboBox, 0, 0)

        # Clear all ROIs button
        self.clearROIsButton = QPushButton("Clear ROIs", self)
        self.clearROIsButton.clicked.connect(self.clearRois)
        self.roi_control_grid.addWidget(self.clearROIsButton, 0, 2)

        # Response display type dropdown
        self.RoiResponseTypeComboBox = QComboBox(self)

        self.RoiResponseTypeComboBox.addItem("RawTrace")
        self.RoiResponseTypeComboBox.addItem("TrialAverage")
        self.RoiResponseTypeComboBox.addItem("TrialResponses")
        self.RoiResponseTypeComboBox.addItem("TrialAverageDFF")
        self.roi_control_grid.addWidget(self.RoiResponseTypeComboBox, 2, 2)

        # ROIset file name line edit box
        self.defaultRoiSetName = "roi_set_name"
        self.le_roiSetName = QLineEdit(self.defaultRoiSetName)
        self.roi_control_grid.addWidget(self.le_roiSetName, 1, 1)

        # Save ROIs button
        self.saveROIsButton = QPushButton("Save ROIs", self)
        self.saveROIsButton.clicked.connect(self.saveRois)
        self.roi_control_grid.addWidget(self.saveROIsButton, 1, 0)

        # Load ROI set combobox
        self.loadROIsComboBox = QComboBox(self)
        self.loadROIsComboBox.addItem("(load existing ROI set)")
        self.loadROIsComboBox.activated.connect(self.selectedExistingRoiSet)
        self.roi_control_grid.addWidget(self.loadROIsComboBox, 1, 2)
        self.updateExistingRoiSetList()

        # Delete current roi button
        self.deleteROIButton = QPushButton("Delete ROI", self)
        self.deleteROIButton.clicked.connect(self.deleteRoi)
        self.roi_control_grid.addWidget(self.deleteROIButton, 2, 0)

        # Current roi slider
        self.roiSlider = QSlider(QtCore.Qt.Orientation.Horizontal, self)
        self.roiSlider.setMinimum(0)
        self.roiSlider.setMaximum(self.max_rois)
        self.roiSlider.valueChanged.connect(self.sliderUpdated)
        self.roi_control_grid.addWidget(self.roiSlider, 2, 1, 1, 1)

        ctx = plt.rc_context({
            'xtick.major.size': 1,
            'axes.spines.top': False,
            'axes.spines.right': False,
            'xtick.labelsize': 'xx-small',
            'ytick.labelsize': 'xx-small',
            'xtick.major.size': 1.0,
            'ytick.major.size': 1.0,
            'xtick.major.pad': 1.0,
            'ytick.major.pad': 1.0
        })
        with ctx:
            self.responseFig = plt.figure(frameon=False, layout='constrained')
            self.responsePlot = self.responseFig.add_subplot(111)
            self.responseCanvas = FigureCanvas(self.responseFig)
        self.responseCanvas.draw_idle()
        self.plot_grid.addWidget(self.responseCanvas, 0, 0)

        # # # # Image canvas # # # # # # # # (1, 2)
        self.roi_fig = plt.figure()
        self.roi_ax = self.roi_fig.add_subplot(111)
        self.roi_canvas = FigureCanvas(self.roi_fig)
        self.toolbar = NavigationToolbar(self.roi_canvas, self)
        self.roi_ax.set_aspect('equal')
        self.roi_ax.set_axis_off()
        self.plot_grid.addWidget(self.toolbar, 1, 0)
        self.plot_grid.addWidget(self.roi_canvas, 2, 0)
        self.plot_grid.setRowStretch(0, 1)
        self.plot_grid.setRowStretch(1, 3)
        self.plot_grid.setRowStretch(2, 3)

        # Current z slice slider
        self.zSlider = QSlider(QtCore.Qt.Orientation.Horizontal, self)
        self.zSlider.setMinimum(0)
        self.zSlider.setMaximum(50)
        self.zSlider.setValue(0)
        self.zSlider.valueChanged.connect(self.zSliderUpdated)
        self.plot_grid.addWidget(self.zSlider, 3, 0)

        self.roi_fig.tight_layout()

        self.setWindowTitle('Visanalysis')
        self.setGeometry(200, 200, 1200, 600)
        self.show()

    def _populateTree(self, widget, dict):
        widget.clear()
        self.fill_item(widget.invisibleRootItem(), dict)

    def fill_item(self, item, value):
        item.setExpanded(True)
        if type(value) is dict:
            for key, val in sorted(value.items()):
                child = QTreeWidgetItem()
                child.setText(0, key)
                item.addChild(child)
                self.fill_item(child, val)
        elif type(value) is list:
            for val in value:
                child = QTreeWidgetItem()
                item.addChild(child)
                if type(val) is dict:
                    child.setText(0, '[dict]')
                    self.fill_item(child, val)
                elif type(val) is list:
                    child.setText(0, '[list]')
                    self.fill_item(child, val)
                else:
                    child.setText(0, val)
                child.setExpanded(True)
        else:
            child = QTreeWidgetItem()
            child.setText(0, value)
            item.addChild(child)

    def onTreeItemClicked(self, item, column):
        file_path = os.path.join(self.experiment_file_directory,
                                 self.experiment_file_name + '.hdf5')
        group_path = h5io.getPathFromTreeItem(
            self.groupTree.selectedItems()[0])
        self.clearRois()
        self.series_number = None
        if 'series_' in group_path:
            self.series_number = int(
                group_path.split('series_')[-1].split('/')[0])
            if self.plugin.dataIsAttached(file_path, self.series_number):
                self.plugin.updateImagingDataObject(
                    self.experiment_file_directory, self.experiment_file_name,
                    self.series_number)
            # look for image_file_name or ask user to select it
            if self.data_directory is not None:
                image_file_name = h5io.readImageFileName(
                    file_path, self.series_number)
                if image_file_name is None or image_file_name == '':
                    image_file_path, _ = QFileDialog.getOpenFileName(
                        self, "Select image file")
                    print('User selected image file at {}'.format(
                        image_file_path))
                    image_file_name = os.path.split(image_file_path)[-1]
                    self.data_directory = os.path.split(
                        image_file_path)[:-1][0]
                    h5io.attachImageFileName(file_path, self.series_number,
                                             image_file_name)
                    print('Attached image_file_name {} to series {}'.format(
                        image_file_name, self.series_number))
                    print('Data directory is {}'.format(self.data_directory))

                self.image_file_name = image_file_name
                self.currentImageFileNameLabel.setText(self.image_file_name)

        else:  # clicked part of the tree upstream of any series
            self.series_number = None

        if item.parent() is not None:
            if item.parent().text(
                    column) == 'rois':  # selected existing roi group
                roi_set_name = item.text(column)
                # print('Selected roi set {} from series {}'.format(roi_set_name, self.series_number))
                self.le_roiSetName.setText(roi_set_name)
                roi_set_path = h5io.getPathFromTreeItem(
                    self.groupTree.selectedItems()[0])
                self.loadRois(roi_set_path)
                self.redrawRoiTraces()

        if group_path != '':
            attr_dict = h5io.getAttributesFromGroup(file_path, group_path)
            editable_values = True  # user can edit metadata
            self.populate_attrs(attr_dict=attr_dict,
                                editable_values=editable_values)

        # show roi image
        if self.series_number is not None:  # Clicked on node of the tree associated with a single series
            if self.data_directory is not None:  # user has selected a raw data directory
                if self.plugin.dataIsAttached(file_path, self.series_number):
                    self.plugin.updateImageSeries(
                        data_directory=self.data_directory,
                        image_file_name=self.image_file_name,
                        series_number=self.series_number,
                        channel=self.current_channel)
                    self.roi_image = self.plugin.mean_brain
                    self.zSlider.setValue(0)
                    self.zSlider.setMaximum(self.roi_image.shape[2] - 1)
                    self.redrawRoiTraces()
                else:
                    print('Attach metadata to file before drawing rois')

            else:
                print('Select a data directory before drawing rois')

        # # # TEST # # #
        memory_usage = psutil.Process(os.getpid()).memory_info().rss * 10**-9
        print('Current Memory Usage: {:.2f}GB'.format(memory_usage))
        sys.stdout.flush()
        # # # TEST # # #

    def updateExistingRoiSetList(self):
        if self.experiment_file_name is not None:
            file_path = os.path.join(self.experiment_file_directory,
                                     self.experiment_file_name + '.hdf5')
            self.existing_roi_set_paths = self.plugin.getRoiSetPaths(
                file_path)  # dictionary of name: full path
            self.loadROIsComboBox.clear()
            for r_path in self.existing_roi_set_paths:
                self.loadROIsComboBox.addItem(r_path)

            self.show()

    def selectedExistingRoiSet(self):
        file_path = os.path.join(self.experiment_file_directory,
                                 self.experiment_file_name + '.hdf5')
        roi_set_key = self.loadROIsComboBox.currentText()
        roi_set_path = self.existing_roi_set_paths[roi_set_key]

        _, _, self.roi_path, self.roi_mask = self.plugin.loadRoiSet(
            file_path, roi_set_path)

        if self.series_number is not None:
            self.roi_response = []
            for new_path in self.roi_path:
                new_roi_resp = self.plugin.getRoiDataFromPath(
                    roi_path=new_path)
                self.roi_response.append(new_roi_resp)

            # update slider to show most recently drawn roi response
            self.current_roi_index = len(self.roi_response) - 1
            self.roiSlider.setValue(self.current_roi_index)

            # Update figures
            self.redrawRoiTraces()

    def selectDataFile(self):
        filePath, _ = QFileDialog.getOpenFileName(
            self, "Open experiment (hdf5) file")
        self.experiment_file_name = os.path.split(filePath)[1].split('.')[0]
        self.experiment_file_directory = os.path.split(filePath)[0]

        if self.experiment_file_name != '':
            self.currentExperimentLabel.setText(self.experiment_file_name)
            self.initializeDataAnalysis()
            self.populateGroups()
            self.updateExistingRoiSetList()

    def selectDataDirectory(self):
        filePath = str(
            QFileDialog.getExistingDirectory(self, "Select data directory"))
        self.data_directory = filePath
        self.data_directory_display.setText('..' + self.data_directory[-24:])

    def initializeDataAnalysis(self):
        file_path = os.path.join(self.experiment_file_directory,
                                 self.experiment_file_name + '.hdf5')
        data_type = h5io.getDataType(file_path)
        # Load plugin based on Rig name in hdf5 file
        if data_type == 'Bruker':
            from visanalysis.plugin import bruker
            self.plugin = bruker.BrukerPlugin()
        elif data_type == 'AODscope':
            from visanalysis.plugin import aodscope
            self.plugin = aodscope.AodScopePlugin()
        else:
            self.plugin = h5io.BasePlugin()

        self.plugin.parent_gui = self

        # # # TEST # # #
        memory_usage = psutil.Process(os.getpid()).memory_info().rss * 10**-9
        print('Current memory usage: {:.2f}GB'.format(memory_usage))
        sys.stdout.flush()
        # # # TEST # # #

    def attachData(self):
        if self.data_directory is not None:
            file_path = os.path.join(self.experiment_file_directory,
                                     self.experiment_file_name + '.hdf5')
            self.plugin.attachData(self.experiment_file_name, file_path,
                                   self.data_directory)
            print('Data attached')
        else:
            print('Select a data directory before attaching new data')

    def selectImageDataFile(self):
        file_path = os.path.join(self.experiment_file_directory,
                                 self.experiment_file_name + '.hdf5')

        image_file_path, _ = QFileDialog.getOpenFileName(
            self, "Select image file")
        print('User selected image file at {}'.format(image_file_path))
        self.image_file_name = os.path.split(image_file_path)[-1]
        self.data_directory = os.path.split(image_file_path)[:-1][0]
        h5io.attachImageFileName(file_path, self.series_number,
                                 self.image_file_name)
        print('Attached image_file_name {} to series {}'.format(
            self.image_file_name, self.series_number))
        print('Data directory is {}'.format(self.data_directory))

        self.currentImageFileNameLabel.setText(self.image_file_name)

        # show roi image
        if self.series_number is not None:
            if self.data_directory is not None:  # user has selected a raw data directory
                self.plugin.updateImageSeries(
                    data_directory=self.data_directory,
                    image_file_name=self.image_file_name,
                    series_number=self.series_number,
                    channel=self.current_channel)
                self.roi_image = self.plugin.mean_brain
                self.zSlider.setValue(0)
                self.zSlider.setMaximum(self.roi_image.shape[2] - 1)
                self.redrawRoiTraces()
            else:
                print('Select a data directory before drawing rois')

    def deleteSelectedGroup(self):
        file_path = os.path.join(self.experiment_file_directory,
                                 self.experiment_file_name + '.hdf5')
        group_path = h5io.getPathFromTreeItem(
            self.groupTree.selectedItems()[0])
        group_name = group_path.split('/')[-1]

        buttonReply = QMessageBox.question(
            self, 'Delete series',
            "Are you sure you want to delete group {}?".format(group_name),
            QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
            QMessageBox.StandardButton.No)
        if buttonReply == QMessageBox.StandardButton.Yes:
            h5io.deleteGroup(file_path=file_path, group_path=group_path)
            print('Deleted group {}'.format(group_name))
            self.updateExistingRoiSetList()
            self.populateGroups()
        else:
            print('Delete aborted')

    def populateGroups(self):
        file_path = os.path.join(self.experiment_file_directory,
                                 self.experiment_file_name + '.hdf5')
        self.group_dset_dict = h5io.getHierarchy(file_path)
        self._populateTree(self.groupTree, self.group_dset_dict)

    def populate_attrs(self, attr_dict=None, editable_values=False):
        """Populate attribute for currently selected group."""
        self.tableAttributes.blockSignals(
            True)  # block udpate signals for auto-filled forms
        self.tableAttributes.setRowCount(0)
        self.tableAttributes.setColumnCount(2)
        self.tableAttributes.setSortingEnabled(False)

        if attr_dict:
            for num, key in enumerate(attr_dict):
                self.tableAttributes.insertRow(self.tableAttributes.rowCount())
                key_item = QTableWidgetItem(key)
                key_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable
                                  | QtCore.Qt.ItemFlag.ItemIsEnabled)
                self.tableAttributes.setItem(num, 0, key_item)

                val_item = QTableWidgetItem(str(attr_dict[key]))
                if editable_values:
                    val_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable
                                      | QtCore.Qt.ItemFlag.ItemIsEditable
                                      | QtCore.Qt.ItemFlag.ItemIsEnabled)
                else:
                    val_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable
                                      | QtCore.Qt.ItemFlag.ItemIsEnabled)
                self.tableAttributes.setItem(num, 1, val_item)

        self.tableAttributes.blockSignals(False)

    def update_attrs_to_file(self, item):
        file_path = os.path.join(self.experiment_file_directory,
                                 self.experiment_file_name + '.hdf5')
        group_path = h5io.getPathFromTreeItem(
            self.groupTree.selectedItems()[0])

        attr_key = self.tableAttributes.item(item.row(), 0).text()
        attr_val = item.text()

        # update attr in file
        h5io.changeAttribute(file_path=file_path,
                             group_path=group_path,
                             attr_key=attr_key,
                             attr_val=attr_val)
        print('Changed attr {} to = {}'.format(attr_key, attr_val))

# %% # # # # # # # # ROI SELECTOR WIDGET # # # # # # # # # # # # # # # # # # #

    def refreshLassoWidget(self, keep_paths=False):
        self.roi_ax.clear()
        init_lasso = False
        if self.roi_image is not None:
            if len(self.roi_mask) > 0:
                newImage = plot_tools.overlayImage(
                    self.roi_image[:, :, self.current_z_slice],
                    self.roi_mask,
                    0.5,
                    self.colors,
                    z=self.current_z_slice)
            else:
                newImage = self.roi_image[:, :, self.current_z_slice]
            self.roi_ax.imshow(newImage, cmap=cm.gray)
            init_lasso = True
        else:
            self.roi_ax.imshow(self.blank_image)
        self.roi_ax.set_axis_off()

        self.roi_canvas.draw()

        if not keep_paths:
            self.roi_path_list = []

        if init_lasso:
            if self.roi_type == 'circle':
                self.lasso_1 = EllipseSelector(self.roi_ax,
                                               onselect=self.newEllipse,
                                               button=1)
            elif self.roi_type == 'freehand':
                self.lasso_1 = LassoSelector(self.roi_ax,
                                             onselect=self.newFreehand,
                                             button=1)
                self.lasso_2 = LassoSelector(self.roi_ax,
                                             onselect=self.appendFreehand,
                                             button=3)
            else:
                print(
                    'Warning ROI type not recognized. Choose circle or freehand'
                )

    def newFreehand(self, verts):
        new_roi_path = path.Path(verts)
        new_roi_path.z_level = self.zSlider.value()
        new_roi_path.channel = self.current_channel
        self.updateRoiSelection([new_roi_path])

    def appendFreehand(self, verts):
        print('Appending rois, hit Enter/Return to finish')
        new_roi_path = path.Path(verts)
        new_roi_path.z_level = self.zSlider.value()
        new_roi_path.channel = self.current_channel
        self.roi_path_list.append(new_roi_path)

    def keyPressEvent(self, event):
        if type(event) == QtGui.QKeyEvent:
            if np.any([
                    event.key() == QtCore.Qt.Key.Key_Return,
                    event.key() == QtCore.Qt.Key.Key_Enter
            ]):
                if len(self.roi_path_list) > 0:
                    event.accept()
                    self.updateRoiSelection(self.roi_path_list)
                else:
                    event.ignore()
            else:
                event.ignore()
        else:
            event.ignore()

    def newEllipse(self, pos1, pos2, definedRadius=None):
        x1 = np.round(pos1.xdata)
        x2 = np.round(pos2.xdata)
        y1 = np.round(pos1.ydata)
        y2 = np.round(pos2.ydata)

        radiusX = np.sqrt((x1 - x2)**2) / 2
        radiusY = np.sqrt((y1 - y2)**2) / 2
        if self.roi_radius is not None:
            radiusX = self.roi_radius

        center = (np.round((x1 + x2) / 2), np.round((y1 + y2) / 2))
        new_roi_path = path.Path.circle(center=center, radius=radiusX)
        new_roi_path.z_level = self.zSlider.value()
        new_roi_path.channel = self.current_channel
        self.updateRoiSelection([new_roi_path])

    def updateRoiSelection(self, new_roi_path):
        mask = self.plugin.getRoiMaskFromPath(new_roi_path)
        new_roi_resp = self.plugin.getRoiDataFromPath(roi_path=new_roi_path)
        if mask.sum() == 0:
            print('No pixels in the roi you just drew')
            return
        # update list of roi data
        self.roi_mask.append(mask)
        self.roi_path.append(new_roi_path)  # list of lists of paths
        self.roi_response.append(new_roi_resp)
        # update slider to show most recently drawn roi response
        self.current_roi_index = len(self.roi_response) - 1
        self.roiSlider.setValue(self.current_roi_index)

        # Update figures
        self.redrawRoiTraces()

    def sliderUpdated(self):
        self.current_roi_index = self.roiSlider.value()
        self.redrawRoiTraces()

    def zSliderUpdated(self):
        self.current_z_slice = self.zSlider.value()
        if self.roi_image is not None:
            self.refreshLassoWidget(keep_paths=True)

    def redrawRoiTraces(self):
        self.clearRoiArtists()
        if self.current_roi_index < len(self.roi_response):
            current_raw_trace = np.squeeze(
                self.roi_response[self.current_roi_index])
            fxn_name = self.RoiResponseTypeComboBox.currentText()
            display_trace = getattr(self.plugin,
                                    'getRoiResponse_{}'.format(fxn_name))(
                                        [current_raw_trace])
            self.responsePlot.plot(display_trace,
                                   color=self.colors[self.current_roi_index],
                                   linewidth=1,
                                   alpha=0.5)
            self.responsePlot.set_xlim([0, len(display_trace)])
            y_min = np.nanmin(display_trace)
            y_max = np.nanmax(display_trace)
            self.responsePlot.set_ylim([y_min, y_max])
        self.responseCanvas.draw()

        self.refreshLassoWidget(keep_paths=False)


# %% # # # # # # # # LOADING / SAVING / COMPUTING ROIS # # # # # # # # # # # # # # # # # # #

    def loadRois(self, roi_set_path):
        file_path = os.path.join(self.experiment_file_directory,
                                 self.experiment_file_name + '.hdf5')
        self.roi_response, self.roi_image, self.roi_path, self.roi_mask = self.plugin.loadRoiSet(
            file_path, roi_set_path)
        self.zSlider.setValue(0)
        self.zSlider.setMaximum(self.roi_image.shape[2] - 1)

    def saveRois(self):
        file_path = os.path.join(self.experiment_file_directory,
                                 self.experiment_file_name + '.hdf5')
        roi_set_name = self.le_roiSetName.text()
        if roi_set_name in h5io.getAvailableRoiSetNames(
                file_path, self.series_number):
            buttonReply = QMessageBox.question(
                self, 'Overwrite roi set',
                "Are you sure you want to overwrite roi set: {}?".format(
                    roi_set_name),
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No)
            if buttonReply == QMessageBox.StandardButton.Yes:
                self.plugin.saveRoiSet(file_path,
                                       series_number=self.series_number,
                                       roi_set_name=roi_set_name,
                                       roi_mask=self.roi_mask,
                                       roi_response=self.roi_response,
                                       roi_image=self.roi_image,
                                       roi_path=self.roi_path)
                print('Saved roi set {} to series {}'.format(
                    roi_set_name, self.series_number))
                self.populateGroups()
                self.updateExistingRoiSetList()
            else:
                print('Overwrite aborted - pick a unique roi set name')
        else:
            self.plugin.saveRoiSet(file_path,
                                   series_number=self.series_number,
                                   roi_set_name=roi_set_name,
                                   roi_mask=self.roi_mask,
                                   roi_response=self.roi_response,
                                   roi_image=self.roi_image,
                                   roi_path=self.roi_path)
            print('Saved roi set {} to series {}'.format(
                roi_set_name, self.series_number))
            self.populateGroups()
            self.updateExistingRoiSetList()

    def deleteRoi(self):
        if self.current_roi_index < len(self.roi_response):
            self.roi_mask.pop(self.current_roi_index)
            self.roi_response.pop(self.current_roi_index)
            self.roi_path.pop(self.current_roi_index)
            self.roiSlider.setValue(self.current_roi_index - 1)
            self.redrawRoiTraces()

    def clearRois(self):
        self.roi_mask = []
        self.roi_response = []
        self.roi_path = []
        self.roi_image = None
        self.clearRoiArtists()
        self.redrawRoiTraces()
        self.roi_ax.clear()

    def clearRoiArtists(self):
        for artist in self.responsePlot.lines + self.responsePlot.collections:
            artist.remove()

    def selectRoiType(self):
        self.roi_type = self.RoiTypeComboBox.currentText().split(':')[0]
        if 'circle' in self.RoiTypeComboBox.currentText():
            self.roi_radius = int(
                self.RoiTypeComboBox.currentText().split(':')[1])
        else:
            self.roi_radius = None
        self.redrawRoiTraces()

    def selectChannel(self):
        self.current_channel = int(self.ChannelComboBox.currentText())

        # show roi image
        if self.series_number is not None:
            if self.data_directory is not None:  # user has selected a raw data directory
                self.plugin.updateImageSeries(
                    data_directory=self.data_directory,
                    image_file_name=self.image_file_name,
                    series_number=self.series_number,
                    channel=self.current_channel)
                self.roi_image = self.plugin.mean_brain
                self.zSlider.setValue(0)
                self.zSlider.setMaximum(self.roi_image.shape[2] - 1)
                self.redrawRoiTraces()
            else:
                print('Select a data directory before drawing rois')
    def __init__(self):
        super(MPPIOptionsWindow, self).__init__()

        self.horizon_steps = controller_mppi.mpc_samples
        self.num_rollouts = controller_mppi.num_rollouts
        self.dd_weight = controller_mppi.dd_weight
        self.ep_weight = controller_mppi.ep_weight
        self.ekp_weight = controller_mppi.ekp_weight * 1.0e1
        self.ekc_weight = controller_mppi.ekc_weight * 1.0e-1
        self.cc_weight = controller_mppi.cc_weight * 1.0e-2
        self.ccrc_weight = controller_mppi.ccrc_weight * 1.0e-2
        self.R = controller_mppi.R  # How much to punish Q
        self.LBD = controller_mppi.LBD  # Cost parameter lambda
        self.NU = controller_mppi.NU  # Exploration variance

        layout = QVBoxLayout()

        ### Set Horizon Length
        horizon_options_layout = QVBoxLayout()

        self.horizon_label = QLabel("")
        self.horizon_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        horizon_options_layout.addWidget(self.horizon_label)

        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(10, 300)
        slider.setValue(self.horizon_steps)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(10)
        slider.setSingleStep(10)
        horizon_options_layout.addWidget(slider)

        slider.valueChanged.connect(self.horizon_length_changed)

        ### Set Number of Rollouts
        rollouts_options_layout = QVBoxLayout()

        self.rollouts_label = QLabel("")
        self.rollouts_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        rollouts_options_layout.addWidget(self.rollouts_label)

        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(10, 3000)
        slider.setValue(self.num_rollouts)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(10)
        slider.setSingleStep(10)
        rollouts_options_layout.addWidget(slider)

        slider.valueChanged.connect(self.num_rollouts_changed)

        ### Set Cost Weights
        cost_weight_layout = QVBoxLayout()

        # Distance difference cost
        self.dd_weight_label = QLabel("")
        self.dd_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.dd_weight_label)
        self.dd_label = QLabel("")
        self.dd_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.dd_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 990)
        slider.setValue(self.dd_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(10)
        slider.setSingleStep(10)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.dd_weight_changed)

        # Potential energy cost
        self.ep_weight_label = QLabel("")
        self.ep_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ep_weight_label)
        self.ep_label = QLabel("")
        self.ep_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ep_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 1e5 - 1e3)
        slider.setValue(self.ep_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1e3)
        slider.setSingleStep(1e3)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.ep_weight_changed)

        # Pole kinetic energy cost
        self.ekp_weight_label = QLabel("")
        self.ekp_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ekp_weight_label)
        self.ekp_label = QLabel("")
        self.ekp_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ekp_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 99)
        slider.setValue(self.ekp_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1)
        slider.setSingleStep(1)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.ekp_weight_changed)

        # Cart kinetic energy cost
        self.ekc_weight_label = QLabel("")
        self.ekc_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ekc_weight_label)
        self.ekc_label = QLabel("")
        self.ekc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ekc_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 99)
        slider.setValue(self.ekc_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1)
        slider.setSingleStep(1)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.ekc_weight_changed)

        # Control cost
        self.cc_weight_label = QLabel("")
        self.cc_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.cc_weight_label)
        self.cc_label = QLabel("")
        self.cc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.cc_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 99)
        slider.setValue(self.cc_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1)
        slider.setSingleStep(1)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.cc_weight_changed)

        # Control change rate cost
        self.ccrc_weight_label = QLabel("")
        self.ccrc_weight_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ccrc_weight_label)
        self.ccrc_label = QLabel("")
        self.ccrc_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
        cost_weight_layout.addWidget(self.ccrc_label)
        slider = QSlider(orientation=Qt.Orientation.Horizontal)
        slider.setRange(0, 99)
        slider.setValue(self.ccrc_weight)
        slider.setTickPosition(QSlider.TickPosition.TicksBelow)
        slider.setTickInterval(1)
        slider.setSingleStep(1)
        cost_weight_layout.addWidget(slider)
        slider.valueChanged.connect(self.ccrc_weight_changed)

        ### Set some more MPPI constants
        mppi_constants_layout = QVBoxLayout()

        # Quadratic cost penalty R
        textbox = QLineEdit()
        textbox.setText(str(self.R))
        textbox.textChanged.connect(self.R_changed)
        h_layout = QHBoxLayout()
        h_layout.addWidget(QLabel("Quadratic input cost penalty R ="))
        h_layout.addWidget(textbox)
        mppi_constants_layout.addLayout(h_layout)

        # Quadratic cost penalty LBD
        textbox = QLineEdit()
        textbox.setText(str(self.LBD))
        textbox.textChanged.connect(self.LBD_changed)
        h_layout = QHBoxLayout()
        h_layout.addWidget(QLabel("Importance of higher-cost rollouts LBD ="))
        h_layout.addWidget(textbox)
        mppi_constants_layout.addLayout(h_layout)

        # Quadratic cost penalty NU
        textbox = QLineEdit()
        textbox.setText(str(self.NU))
        textbox.textChanged.connect(self.NU_changed)
        h_layout = QHBoxLayout()
        h_layout.addWidget(QLabel("Exploration variance NU ="))
        h_layout.addWidget(textbox)
        mppi_constants_layout.addLayout(h_layout)

        # Sampling type
        h_layout = QHBoxLayout()
        btn1 = QRadioButton("iid")
        if btn1.text() == controller_mppi.SAMPLING_TYPE: btn1.setChecked(True)
        btn1.toggled.connect(lambda: self.toggle_button(btn1))
        h_layout.addWidget(btn1)
        btn2 = QRadioButton("random_walk")
        if btn2.text() == controller_mppi.SAMPLING_TYPE: btn2.setChecked(True)
        btn2.toggled.connect(lambda: self.toggle_button(btn2))
        h_layout.addWidget(btn2)
        btn3 = QRadioButton("uniform")
        if btn3.text() == controller_mppi.SAMPLING_TYPE: btn3.setChecked(True)
        btn3.toggled.connect(lambda: self.toggle_button(btn3))
        h_layout.addWidget(btn3)
        btn4 = QRadioButton("repeated")
        if btn4.text() == controller_mppi.SAMPLING_TYPE: btn4.setChecked(True)
        btn4.toggled.connect(lambda: self.toggle_button(btn4))
        h_layout.addWidget(btn4)
        btn5 = QRadioButton("interpolated")
        if btn5.text() == controller_mppi.SAMPLING_TYPE: btn5.setChecked(True)
        btn5.toggled.connect(lambda: self.toggle_button(btn5))
        h_layout.addWidget(btn5)
        mppi_constants_layout.addWidget(QLabel("Sampling type:"))
        mppi_constants_layout.addLayout(h_layout)

        ### Put together layout
        self.update_labels()
        self.update_slider_labels()
        layout.addLayout(horizon_options_layout)
        layout.addLayout(rollouts_options_layout)
        layout.addLayout(cost_weight_layout)
        layout.addLayout(mppi_constants_layout)

        self.setLayout(layout)
        self.setWindowFlags(self.windowFlags()
                            | Qt.WindowType.WindowStaysOnTopHint)
        self.setGeometry(0, 0, 400, 50)

        self.show()
        self.setWindowTitle("MPPI Options")

        self.timer = QTimer()
        self.timer.timeout.connect(self.update_labels)
        self.timer.start(100)
Beispiel #10
0
    def __init__(self):
        super().__init__()
        self.setFixedSize(800, 410)
        self.setWindowTitle("PyLX16A Servo Testing Software")

        self.port_selection_box = QComboBox(self)
        self.port_selection_box.setFixedSize(200, 27)
        self.port_selection_box.move(30, 65)
        port_selection_box_label = QLabel("Select Port:", self)
        port_selection_box_label.move(30, 35)

        self.port_selection_box_refresh_button = QPushButton("Refresh", self)
        self.port_selection_box_refresh_button.setFixedSize(60, 23)
        self.port_selection_box_refresh_button.move(170, 38)

        self.id_selection_box = QListWidget(self)
        self.id_selection_box.setFixedSize(200, 200)
        self.id_selection_box.move(30, 135)
        id_selection_box_label = QLabel("Connected Servos:", self)
        id_selection_box_label.setFixedWidth(200)
        id_selection_box_label.move(30, 105)

        self.id_selection_box_refresh_button = QPushButton("Refresh", self)
        self.id_selection_box_refresh_button.setFixedSize(60, 23)
        self.id_selection_box_refresh_button.move(170, 108)

        self.set_id_line_edit = QLineEdit(self)
        self.set_id_line_edit.setFixedSize(50, 27)
        self.set_id_line_edit.move(80, 355)
        set_id_line_edit_label = QLabel("Set ID:", self)
        set_id_line_edit_label.move(30, 355)
        set_id_line_edit_label.setFixedSize(50, 27)

        self.set_id_button = QPushButton("Change ID!", self)
        self.set_id_button.setFixedSize(85, 27)
        self.set_id_button.move(145, 355)

        self.position_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.position_slider.setMinimum(0)
        self.position_slider.setMaximum(240)
        self.position_slider.setFixedWidth(200)
        self.position_slider.move(300, 55)
        self.position_slider_readout = QLabel("0.00°", self)
        self.position_slider_readout.setFixedWidth(50)
        self.position_slider_readout.move(450, 30)
        self.position_slider_readout.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        position_slider_label = QLabel("Angle (degrees):", self)
        position_slider_label.move(300, 30)

        self.position_offset_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.position_offset_slider.setMinimum(-30)
        self.position_offset_slider.setMaximum(30)
        self.position_offset_slider.setFixedWidth(200)
        self.position_offset_slider.move(300, 125)
        self.position_offset_slider_readout = QLabel("0.00°", self)
        self.position_offset_slider_readout.setFixedWidth(50)
        self.position_offset_slider_readout.move(450, 100)
        self.position_offset_slider_readout.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        position_offset_slider_label = QLabel("Angle offset (degrees):", self)
        position_offset_slider_label.setFixedWidth(200)
        position_offset_slider_label.move(300, 100)

        self.angle_lower_limit_textentry = QLineEdit(self)
        self.angle_lower_limit_textentry.setFixedWidth(50)
        self.angle_lower_limit_textentry.move(450, 175)
        self.angle_lower_limit_textentry.setValidator(
            QIntValidator(0, 240, self))
        self.angle_upper_limit_textentry = QLineEdit(self)
        self.angle_upper_limit_textentry.setFixedWidth(50)
        self.angle_upper_limit_textentry.move(450, 210)
        self.angle_upper_limit_textentry.setValidator(
            QIntValidator(0, 240, self))
        self.angle_lower_limit_textentry_label = QLabel(
            "Lower Limit (degrees):", self)
        self.angle_lower_limit_textentry_label.move(300, 175)
        self.angle_lower_limit_textentry_label.setFixedWidth(150)
        self.angle_upper_limit_textentry_label = QLabel(
            "Upper Limit (degrees):", self)
        self.angle_upper_limit_textentry_label.move(300, 210)
        self.angle_upper_limit_textentry_label.setFixedWidth(150)

        self.vin_lower_limit_textentry = QLineEdit(self)
        self.vin_lower_limit_textentry.setFixedWidth(50)
        self.vin_lower_limit_textentry.move(450, 265)
        self.vin_lower_limit_textentry.setValidator(
            QIntValidator(4500, 12000, self))
        self.vin_upper_limit_textentry = QLineEdit(self)
        self.vin_upper_limit_textentry.setFixedWidth(50)
        self.vin_upper_limit_textentry.move(450, 300)
        self.vin_upper_limit_textentry.setValidator(
            QIntValidator(4500, 12000, self))
        self.vin_lower_limit_textentry_label = QLabel(
            "Voltage Lower Limit (mV):", self)
        self.vin_lower_limit_textentry_label.move(300, 265)
        self.vin_lower_limit_textentry_label.setFixedWidth(150)
        self.vin_upper_limit_textentry_label = QLabel(
            "Voltage Upper Limit (mV):", self)
        self.vin_upper_limit_textentry_label.move(300, 300)
        self.vin_upper_limit_textentry_label.setFixedWidth(150)

        self.temp_limit_textentry = QLineEdit(self)
        self.temp_limit_textentry.setFixedWidth(50)
        self.temp_limit_textentry.move(450, 355)
        self.temp_limit_textentry.setValidator(QIntValidator(50, 100, self))
        self.temp_limit_textentry_label = QLabel("Temp Limit (°C):", self)
        self.temp_limit_textentry_label.move(300, 355)
        self.temp_limit_textentry_label.setFixedWidth(150)

        self.servo_mode_radio_button = QRadioButton("Servo Mode", self)
        self.servo_mode_radio_button.move(565, 50)
        self.motor_mode_radio_button = QRadioButton("Motor Mode", self)
        self.motor_mode_radio_button.move(565, 75)

        self.motor_speed_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.motor_speed_slider.setMinimum(-1000)
        self.motor_speed_slider.setMaximum(1000)
        self.motor_speed_slider.setFixedWidth(200)
        self.motor_speed_slider.move(565, 125)
        motor_speed_slider_label = QLabel("Motor Speed:", self)
        motor_speed_slider_label.move(565, 100)

        self.torque_enabled_checkbox = QCheckBox("Torque Enabled", self)
        self.torque_enabled_checkbox.move(565, 175)
        self.torque_enabled_checkbox.setFixedWidth(200)

        self.led_enabled_checkbox = QCheckBox("LED Enabled", self)
        self.led_enabled_checkbox.move(565, 210)
        self.led_enabled_checkbox.setFixedWidth(200)

        self.led_over_temp_checkbox = QCheckBox("LED Over Temperature", self)
        self.led_over_temp_checkbox.move(565, 258)
        self.led_over_temp_checkbox.setFixedWidth(200)
        self.led_over_voltage_checkbox = QCheckBox("LED Over Voltage", self)
        self.led_over_voltage_checkbox.move(565, 283)
        self.led_over_voltage_checkbox.setFixedWidth(200)
        self.led_rotor_locked_checkbox = QCheckBox("LED Rotor Locked", self)
        self.led_rotor_locked_checkbox.move(565, 308)
        self.led_rotor_locked_checkbox.setFixedWidth(200)

        self.physical_position_readout = QLabel("--°", self)
        self.physical_position_readout.move(565, 367)
        self.physical_position_readout.setFixedWidth(200)
        self.physical_position_readout_label = QLabel("Position", self)
        self.physical_position_readout_label.move(565, 347)

        self.temperature_readout = QLabel("-- °C", self)
        self.temperature_readout.move(635, 367)
        self.temperature_readout.setFixedWidth(200)
        self.temperature_readout_label = QLabel("Temperature", self)
        self.temperature_readout_label.move(635, 347)

        self.voltage_readout = QLabel("-- V", self)
        self.voltage_readout.move(730, 367)
        self.voltage_readout.setFixedWidth(200)
        self.voltage_readout_label = QLabel("Voltage", self)
        self.voltage_readout_label.move(730, 347)

        self.readout_update_timer = QTimer(self)
        self.readout_update_timer.timeout.connect(self.update_readouts)
        self.readout_update_timer.start(250)

        self.active_servo: LX16A = None

        self.position_slider.setValue(0)
        self.position_offset_slider.setValue(0)
        self.motor_speed_slider.setValue(0)
        self.id_selection_box_refresh_button.setEnabled(False)
        self.disable_widgets()

        self.port_selection_box.currentTextChanged.connect(
            self.port_selection_box_changed)
        self.port_selection_box_refresh_button.clicked.connect(
            self.port_refresh_button_clicked)
        self.id_selection_box.currentTextChanged.connect(
            self.id_selection_box_changed)
        self.id_selection_box_refresh_button.clicked.connect(
            self.id_refresh_button_clicked)
        self.set_id_button.pressed.connect(self.id_updated)
        self.position_slider.sliderMoved.connect(self.position_slider_updated)
        self.position_offset_slider.sliderMoved.connect(
            self.position_offset_slider_updated)
        self.angle_lower_limit_textentry.textChanged.connect(
            self.angle_lower_limit_updated)
        self.angle_upper_limit_textentry.textChanged.connect(
            self.angle_upper_limit_updated)
        self.vin_lower_limit_textentry.textChanged.connect(
            self.vin_lower_limit_updated)
        self.vin_upper_limit_textentry.textChanged.connect(
            self.vin_upper_limit_updated)
        self.temp_limit_textentry.textChanged.connect(self.temp_limit_updated)
        self.servo_mode_radio_button.toggled.connect(
            self.servo_mode_radio_button_toggled)
        self.motor_mode_radio_button.toggled.connect(
            self.motor_mode_radio_button_toggled)
        self.motor_speed_slider.valueChanged.connect(
            self.motor_speed_slider_updated)
        self.torque_enabled_checkbox.stateChanged.connect(
            self.torque_enabled_checkbox_toggled)
        self.led_enabled_checkbox.stateChanged.connect(
            self.led_enabled_checkbox_toggled)
        self.led_over_temp_checkbox.stateChanged.connect(
            self.led_error_triggers_checkbox_toggled)
        self.led_over_voltage_checkbox.stateChanged.connect(
            self.led_error_triggers_checkbox_toggled)
        self.led_rotor_locked_checkbox.stateChanged.connect(
            self.led_error_triggers_checkbox_toggled)

        self.scan_for_ports()
Beispiel #11
0
    def __init__(self, parent=None):
        super(VideoWindow, self).__init__(parent)
        self.setWindowTitle("StudioProject")
        self.statusBar = QStatusBar()
        self.setStatusBar(self.statusBar)
        self.gScene = QGraphicsScene(self)
        self.gView = GraphicView(self.gScene, self)
        self.gView.viewport().setAttribute(
            Qt.WidgetAttribute.WA_AcceptTouchEvents, False)
        # self.gView.setBackgroundBrush(QBrush(Qt.black))

        self.videoStartDatetime = None
        self.videoCurrentDatetime = None

        self.projectFile = ''
        self.graphicsFile = ''
        self.videoFile = ''

        self.obsTb = ObsToolbox(self)

        # # ===================== Setting video item ==============================
        # self.videoItem = QGraphicsVideoItem()
        # self.videoItem.setAspectRatioMode(Qt.KeepAspectRatio)
        # self.gScene.addItem(self.videoItem)
        # self.videoItem.mouseMoveEvent = self.gView.mouseMoveEvent

        self.mediaPlayer = QMediaPlayer(self)
        # self.mediaPlayer.setVideoOutput(self.videoItem)
        self.mediaPlayer.playbackStateChanged.connect(self.mediaStateChanged)
        self.mediaPlayer.positionChanged.connect(self.positionChanged)
        self.mediaPlayer.durationChanged.connect(self.durationChanged)
        self.mediaPlayer.errorOccurred.connect(self.handleError)
        # self.mediaPlayer.setMuted(True)
        # self.mediaPlayer.setNotifyInterval(100)

        self.playButton = QPushButton()
        self.playButton.setEnabled(False)
        self.playButton.setIcon(self.style().standardIcon(
            QStyle.StandardPixmap.SP_MediaPlay))
        self.playButton.clicked.connect(self.play)

        self.changePlayRateBtn = QPushButton('1x')
        self.changePlayRateBtn.setFixedWidth(40)
        # self.incrPlayRateBtn.setEnabled(False)
        self.changePlayRateBtn.clicked.connect(self.changePlayRate)

        self.positionSlider = QSlider(Qt.Orientation.Horizontal)
        self.positionSlider.setRange(0, 0)
        self.positionSlider.sliderMoved.connect(self.setPosition)

        self.timerLabel = QLabel()
        self.timerLabel.setText('--:--:--')
        self.timerLabel.setFixedWidth(58)

        self.dateLabel = QLabel()
        self.dateLabel.setText('Video date: --')
        self.statusBar.addPermanentWidget(self.dateLabel)

        # Create open action
        self.openVideoAction = QAction(QIcon('icons/video-file.png'),
                                       'Open video', self)
        self.openVideoAction.setShortcut('Ctrl+O')
        self.openVideoAction.setStatusTip('Open video file')
        self.openVideoAction.triggered.connect(self.openVideoFile)

        # Create observation action
        obsTbAction = QAction(QIcon('icons/checklist.png'),
                              'Observation toolbox', self)
        obsTbAction.setStatusTip('Open observation toolbox')
        obsTbAction.triggered.connect(self.openObsToolbox)

        self.drawPointAction = QAction(QIcon('icons/drawPoint.png'),
                                       'Draw point', self)
        self.drawPointAction.setStatusTip('Draw point over the video')
        self.drawPointAction.setCheckable(True)
        self.drawPointAction.setEnabled(False)
        self.drawPointAction.triggered.connect(self.drawingClick)

        self.drawLineAction = QAction(QIcon('icons/drawLine.png'), 'Draw line',
                                      self)
        self.drawLineAction.setStatusTip('Draw line over the video')
        self.drawLineAction.setCheckable(True)
        self.drawLineAction.setEnabled(False)
        self.drawLineAction.triggered.connect(self.drawingClick)

        self.drawZoneAction = QAction(QIcon('icons/drawZone.png'), 'Draw zone',
                                      self)
        self.drawZoneAction.setStatusTip('Draw zone over the video')
        self.drawZoneAction.setCheckable(True)
        self.drawZoneAction.setEnabled(False)
        self.drawZoneAction.triggered.connect(self.drawingClick)

        self.maskGenAction = QAction(QIcon('icons/mask.png'),
                                     'Generate mask file', self)
        self.maskGenAction.setStatusTip(
            'Generate mask file for TrafficIntelligence')
        self.maskGenAction.setCheckable(True)
        self.maskGenAction.setEnabled(False)
        self.maskGenAction.triggered.connect(self.generateMask)

        actionGroup = QActionGroup(self)
        actionGroup.addAction(self.drawPointAction)
        actionGroup.addAction(self.drawLineAction)
        actionGroup.addAction(self.drawZoneAction)

        openProjectAction = QAction(QIcon('icons/open-project.png'),
                                    'Open project', self)
        openProjectAction.setStatusTip('Open project')
        openProjectAction.triggered.connect(self.openProject)

        self.saveProjectAction = QAction(QIcon('icons/save-project.png'),
                                         'Save project', self)
        self.saveProjectAction.setStatusTip('Save project')
        self.saveProjectAction.setEnabled(False)
        self.saveProjectAction.triggered.connect(self.saveProject)

        self.saveGraphAction = QAction(QIcon('icons/save-graphics.png'),
                                       'Save graphics', self)
        self.saveGraphAction.setStatusTip('Save graphics to database')
        self.saveGraphAction.setEnabled(False)
        self.saveGraphAction.triggered.connect(self.saveGraphics)

        self.loadGraphAction = QAction(QIcon('icons/folders.png'),
                                       'Load graphics', self)
        self.loadGraphAction.setStatusTip('Load graphics from database')
        self.loadGraphAction.setEnabled(False)
        self.loadGraphAction.triggered.connect(self.loadGraphics)

        # Create exit action
        exitAction = QAction(QIcon('icons/close.png'), 'Exit', self)
        exitAction.setShortcut('Ctrl+Q')
        exitAction.setStatusTip('Exit application')
        exitAction.triggered.connect(self.exitCall)  # self.exitCall

        # Create menu bar and add action
        # menuBar = self.menuBar()
        # menuBar.setNativeMenuBar(False)
        # fileMenu = menuBar.addMenu('&File')
        # fileMenu.addAction(openVideoAction)
        # fileMenu.addAction(obsTbAction)
        # fileMenu.addAction(exitAction)

        self.toolbar = self.addToolBar('Tools')
        self.toolbar.setIconSize(QSize(24, 24))
        self.toolbar.addAction(openProjectAction)
        self.toolbar.addAction(self.saveProjectAction)
        self.toolbar.addAction(self.openVideoAction)

        # self.toolbar.insertSeparator(self.loadGraphAction)
        # self.toolbar.addAction(self.loadGraphAction)
        # self.toolbar.addAction(self.saveGraphAction)
        # self.toolbar.addAction(self.drawPointAction)
        # self.toolbar.addAction(self.drawLineAction)
        # self.toolbar.addAction(self.drawZoneAction)
        self.toolbar.addAction(self.maskGenAction)
        # self.toolbar.insertSeparator(self.drawPointAction)

        self.toolbar.insertSeparator(obsTbAction)
        self.toolbar.addAction(obsTbAction)

        self.toolbar.insertSeparator(exitAction)
        self.toolbar.addAction(exitAction)

        # Create a widget for window contents
        wid = QWidget(self)
        self.setCentralWidget(wid)

        # Create layouts to place inside widget
        controlLayout = QHBoxLayout()
        controlLayout.setContentsMargins(0, 0, 0, 0)
        # controlLayout.addWidget(self.decrPlayRateBtn)
        controlLayout.addWidget(self.playButton)
        controlLayout.addWidget(self.changePlayRateBtn)
        controlLayout.addWidget(self.timerLabel)
        controlLayout.addWidget(self.positionSlider)
        # controlLayout.addWidget(self.durationLabel)

        layout = QVBoxLayout()
        layout.addWidget(self.gView)
        layout.addLayout(controlLayout)

        # Set widget to contain window contents
        wid.setLayout(layout)
Beispiel #12
0
        def make_slider(MAX_RANGE, INIT_VALUE):
            label = QLabel("")
            label.setAlignment(Qt.AlignmentFlag.AlignCenter)

            slider = QSlider(orientation=Qt.Orientation.Horizontal)
            slider.setRange(0, SLIDER_MAX_RANGE_INT)
            slider.setValue(int(SLIDER_MAX_RANGE_INT*(INIT_VALUE/MAX_RANGE)))
            slider.setTickPosition(QSlider.TickPosition.TicksBelow)
            slider.setTickInterval(int(SLIDER_MAX_RANGE_INT*0.1))
            slider.setSingleStep(1)

            return slider, label
Beispiel #13
0
    def createEditingBar(self):
        """Create dock widget for editing tools."""
        #TODO: Add a tab widget for the different editing tools
        self.editing_bar = QDockWidget("Tools")
        self.editing_bar.setAllowedAreas(
            Qt.DockWidgetAreas.LeftDockWidgetArea
            | Qt.DockWidgetAreas.RightDockWidgetArea)
        self.editing_bar.setMinimumWidth(90)

        # Create editing tool buttons
        filters_label = QLabel("Filters")

        convert_to_grayscale = QToolButton()
        convert_to_grayscale.setIcon(
            QIcon(os.path.join(icon_path, "grayscale.png")))
        convert_to_grayscale.clicked.connect(self.image_label.convertToGray)

        convert_to_RGB = QToolButton()
        convert_to_RGB.setIcon(QIcon(os.path.join(icon_path, "rgb.png")))
        convert_to_RGB.clicked.connect(self.image_label.convertToRGB)

        convert_to_sepia = QToolButton()
        convert_to_sepia.setIcon(QIcon(os.path.join(icon_path, "sepia.png")))
        convert_to_sepia.clicked.connect(self.image_label.convertToSepia)

        change_hue = QToolButton()
        change_hue.setIcon(QIcon(os.path.join(icon_path, "")))
        change_hue.clicked.connect(self.image_label.changeHue)

        brightness_label = QLabel("Brightness")
        self.brightness_slider = QSlider(Qt.Orientations.Horizontal)
        self.brightness_slider.setRange(-255, 255)
        self.brightness_slider.setTickInterval(35)
        self.brightness_slider.setTickPosition(QSlider.TickPosition.TicksAbove)
        self.brightness_slider.valueChanged.connect(
            self.image_label.changeBrighteness)

        contrast_label = QLabel("Contrast")
        self.contrast_slider = QSlider(Qt.Orientations.Horizontal)
        self.contrast_slider.setRange(-255, 255)
        self.contrast_slider.setTickInterval(35)
        self.contrast_slider.setTickPosition(QSlider.TickPosition.TicksAbove)
        self.contrast_slider.valueChanged.connect(
            self.image_label.changeContrast)

        # Set layout for dock widget
        editing_grid = QGridLayout()
        #editing_grid.addWidget(filters_label, 0, 0, 0, 2, Qt.AlignTop)
        editing_grid.addWidget(convert_to_grayscale, 1, 0)
        editing_grid.addWidget(convert_to_RGB, 1, 1)
        editing_grid.addWidget(convert_to_sepia, 2, 0)
        editing_grid.addWidget(change_hue, 2, 1)
        editing_grid.addWidget(brightness_label, 3, 0)
        editing_grid.addWidget(self.brightness_slider, 4, 0, 1, 0)
        editing_grid.addWidget(contrast_label, 5, 0)
        editing_grid.addWidget(self.contrast_slider, 6, 0, 1, 0)
        editing_grid.setRowStretch(7, 10)

        container = QWidget()
        container.setLayout(editing_grid)

        self.editing_bar.setWidget(container)

        self.addDockWidget(Qt.DockWidgetAreas.LeftDockWidgetArea,
                           self.editing_bar)

        self.tools_menu_act = self.editing_bar.toggleViewAction()
Beispiel #14
0
class PhotoEditorGUI(QMainWindow):
    def __init__(self):
        super().__init__()

        self.initializeUI()

        self.image = QImage()

    def initializeUI(self):
        self.setMinimumSize(300, 200)
        self.setWindowTitle("Photo Editor")
        self.showMaximized()

        self.zoom_factor = 1

        self.createMainLabel()
        self.createEditingBar()
        self.createMenu()
        self.createToolBar()

        self.show()

    def createMenu(self):
        """Set up the menubar."""
        # Actions for Photo Editor menu
        about_act = QAction('About', self)
        about_act.triggered.connect(self.aboutDialog)

        self.exit_act = QAction(QIcon(os.path.join(icon_path, "exit.png")),
                                'Quit Photo Editor', self)
        self.exit_act.setShortcut('Ctrl+Q')
        self.exit_act.triggered.connect(self.close)

        # Actions for File menu
        self.new_act = QAction(QIcon(os.path.join(icon_path, "new.png")),
                               'New...')

        self.open_act = QAction(QIcon(os.path.join(icon_path, "open.png")),
                                'Open...', self)
        self.open_act.setShortcut('Ctrl+O')
        self.open_act.triggered.connect(self.image_label.openImage)

        self.print_act = QAction(QIcon(os.path.join(icon_path, "print.png")),
                                 "Print...", self)
        self.print_act.setShortcut('Ctrl+P')
        #self.print_act.triggered.connect(self.printImage)
        self.print_act.setEnabled(False)

        self.save_act = QAction(QIcon(os.path.join(icon_path, "save.png")),
                                "Save...", self)
        self.save_act.setShortcut('Ctrl+S')
        self.save_act.triggered.connect(self.image_label.saveImage)
        self.save_act.setEnabled(False)

        # Actions for Edit menu
        self.revert_act = QAction("Revert to Original", self)
        self.revert_act.triggered.connect(self.image_label.revertToOriginal)
        self.revert_act.setEnabled(False)

        # Actions for Tools menu
        self.crop_act = QAction(QIcon(os.path.join(icon_path, "crop.png")),
                                "Crop", self)
        self.crop_act.setShortcut('Shift+X')
        self.crop_act.triggered.connect(self.image_label.cropImage)

        self.resize_act = QAction(QIcon(os.path.join(icon_path, "resize.png")),
                                  "Resize", self)
        self.resize_act.setShortcut('Shift+Z')
        self.resize_act.triggered.connect(self.image_label.resizeImage)

        self.rotate90_cw_act = QAction(
            QIcon(os.path.join(icon_path, "rotate90_cw.png")), 'Rotate 90º CW',
            self)
        self.rotate90_cw_act.triggered.connect(
            lambda: self.image_label.rotateImage90("cw"))

        self.rotate90_ccw_act = QAction(
            QIcon(os.path.join(icon_path, "rotate90_ccw.png")),
            'Rotate 90º CCW', self)
        self.rotate90_ccw_act.triggered.connect(
            lambda: self.image_label.rotateImage90("ccw"))

        self.flip_horizontal = QAction(
            QIcon(os.path.join(icon_path, "flip_horizontal.png")),
            'Flip Horizontal', self)
        self.flip_horizontal.triggered.connect(
            lambda: self.image_label.flipImage("horizontal"))

        self.flip_vertical = QAction(
            QIcon(os.path.join(icon_path, "flip_vertical.png")),
            'Flip Vertical', self)
        self.flip_vertical.triggered.connect(
            lambda: self.image_label.flipImage('vertical'))

        self.zoom_in_act = QAction(
            QIcon(os.path.join(icon_path, "zoom_in.png")), 'Zoom In', self)
        self.zoom_in_act.setShortcut('Ctrl++')
        self.zoom_in_act.triggered.connect(lambda: self.zoomOnImage(1.25))
        self.zoom_in_act.setEnabled(False)

        self.zoom_out_act = QAction(
            QIcon(os.path.join(icon_path, "zoom_out.png")), 'Zoom Out', self)
        self.zoom_out_act.setShortcut('Ctrl+-')
        self.zoom_out_act.triggered.connect(lambda: self.zoomOnImage(0.8))
        self.zoom_out_act.setEnabled(False)

        self.normal_size_Act = QAction("Normal Size", self)
        self.normal_size_Act.setShortcut('Ctrl+=')
        self.normal_size_Act.triggered.connect(self.normalSize)
        self.normal_size_Act.setEnabled(False)

        # Actions for Views menu
        #self.tools_menu_act = QAction(QIcon(os.path.join(icon_path, "edit.png")),'Tools View...', self, checkable=True)

        # Create menubar
        menu_bar = self.menuBar()
        menu_bar.setNativeMenuBar(False)

        # Create Photo Editor menu and add actions
        main_menu = menu_bar.addMenu('Photo Editor')
        main_menu.addAction(about_act)
        main_menu.addSeparator()
        main_menu.addAction(self.exit_act)

        # Create file menu and add actions
        file_menu = menu_bar.addMenu('File')
        file_menu.addAction(self.open_act)
        file_menu.addAction(self.save_act)
        file_menu.addSeparator()
        file_menu.addAction(self.print_act)

        edit_menu = menu_bar.addMenu('Edit')
        edit_menu.addAction(self.revert_act)

        tool_menu = menu_bar.addMenu('Tools')
        tool_menu.addAction(self.crop_act)
        tool_menu.addAction(self.resize_act)
        tool_menu.addSeparator()
        tool_menu.addAction(self.rotate90_cw_act)
        tool_menu.addAction(self.rotate90_ccw_act)
        tool_menu.addAction(self.flip_horizontal)
        tool_menu.addAction(self.flip_vertical)
        tool_menu.addSeparator()
        tool_menu.addAction(self.zoom_in_act)
        tool_menu.addAction(self.zoom_out_act)
        tool_menu.addAction(self.normal_size_Act)

        views_menu = menu_bar.addMenu('Views')
        views_menu.addAction(self.tools_menu_act)

    def createToolBar(self):
        """Set up the toolbar."""
        tool_bar = QToolBar("Main Toolbar")
        tool_bar.setIconSize(QSize(26, 26))
        self.addToolBar(tool_bar)

        # Add actions to the toolbar
        tool_bar.addAction(self.open_act)
        tool_bar.addAction(self.save_act)
        tool_bar.addAction(self.print_act)
        tool_bar.addAction(self.exit_act)
        tool_bar.addSeparator()
        tool_bar.addAction(self.crop_act)
        tool_bar.addAction(self.resize_act)
        tool_bar.addSeparator()
        tool_bar.addAction(self.rotate90_ccw_act)
        tool_bar.addAction(self.rotate90_cw_act)
        tool_bar.addAction(self.flip_horizontal)
        tool_bar.addAction(self.flip_vertical)
        tool_bar.addSeparator()
        tool_bar.addAction(self.zoom_in_act)
        tool_bar.addAction(self.zoom_out_act)

    def createEditingBar(self):
        """Create dock widget for editing tools."""
        #TODO: Add a tab widget for the different editing tools
        self.editing_bar = QDockWidget("Tools")
        self.editing_bar.setAllowedAreas(
            Qt.DockWidgetAreas.LeftDockWidgetArea
            | Qt.DockWidgetAreas.RightDockWidgetArea)
        self.editing_bar.setMinimumWidth(90)

        # Create editing tool buttons
        filters_label = QLabel("Filters")

        convert_to_grayscale = QToolButton()
        convert_to_grayscale.setIcon(
            QIcon(os.path.join(icon_path, "grayscale.png")))
        convert_to_grayscale.clicked.connect(self.image_label.convertToGray)

        convert_to_RGB = QToolButton()
        convert_to_RGB.setIcon(QIcon(os.path.join(icon_path, "rgb.png")))
        convert_to_RGB.clicked.connect(self.image_label.convertToRGB)

        convert_to_sepia = QToolButton()
        convert_to_sepia.setIcon(QIcon(os.path.join(icon_path, "sepia.png")))
        convert_to_sepia.clicked.connect(self.image_label.convertToSepia)

        change_hue = QToolButton()
        change_hue.setIcon(QIcon(os.path.join(icon_path, "")))
        change_hue.clicked.connect(self.image_label.changeHue)

        brightness_label = QLabel("Brightness")
        self.brightness_slider = QSlider(Qt.Orientations.Horizontal)
        self.brightness_slider.setRange(-255, 255)
        self.brightness_slider.setTickInterval(35)
        self.brightness_slider.setTickPosition(QSlider.TickPosition.TicksAbove)
        self.brightness_slider.valueChanged.connect(
            self.image_label.changeBrighteness)

        contrast_label = QLabel("Contrast")
        self.contrast_slider = QSlider(Qt.Orientations.Horizontal)
        self.contrast_slider.setRange(-255, 255)
        self.contrast_slider.setTickInterval(35)
        self.contrast_slider.setTickPosition(QSlider.TickPosition.TicksAbove)
        self.contrast_slider.valueChanged.connect(
            self.image_label.changeContrast)

        # Set layout for dock widget
        editing_grid = QGridLayout()
        #editing_grid.addWidget(filters_label, 0, 0, 0, 2, Qt.AlignTop)
        editing_grid.addWidget(convert_to_grayscale, 1, 0)
        editing_grid.addWidget(convert_to_RGB, 1, 1)
        editing_grid.addWidget(convert_to_sepia, 2, 0)
        editing_grid.addWidget(change_hue, 2, 1)
        editing_grid.addWidget(brightness_label, 3, 0)
        editing_grid.addWidget(self.brightness_slider, 4, 0, 1, 0)
        editing_grid.addWidget(contrast_label, 5, 0)
        editing_grid.addWidget(self.contrast_slider, 6, 0, 1, 0)
        editing_grid.setRowStretch(7, 10)

        container = QWidget()
        container.setLayout(editing_grid)

        self.editing_bar.setWidget(container)

        self.addDockWidget(Qt.DockWidgetAreas.LeftDockWidgetArea,
                           self.editing_bar)

        self.tools_menu_act = self.editing_bar.toggleViewAction()

    def createMainLabel(self):
        """Create an instance of the imageLabel class and set it 
           as the main window's central widget."""
        self.image_label = imageLabel(self)
        self.image_label.resize(self.image_label.pixmap().size())

        self.scroll_area = QScrollArea()
        self.scroll_area.setBackgroundRole(QPalette.ColorRole.Dark)
        self.scroll_area.setAlignment(Qt.Alignment.AlignCenter)
        #self.scroll_area.setWidgetResizable(False)
        #scroll_area.setMinimumSize(800, 800)

        self.scroll_area.setWidget(self.image_label)
        #self.scroll_area.setVisible(False)

        self.setCentralWidget(self.scroll_area)

        #self.resize(QApplication.primaryScreen().availableSize() * 3 / 5)

    def updateActions(self):
        """Update the values of menu and toolbar items when an image 
        is loaded."""
        self.save_act.setEnabled(True)
        self.revert_act.setEnabled(True)
        self.zoom_in_act.setEnabled(True)
        self.zoom_out_act.setEnabled(True)
        self.normal_size_Act.setEnabled(True)

    def zoomOnImage(self, zoom_value):
        """Zoom in and zoom out."""
        self.zoom_factor *= zoom_value
        self.image_label.resize(self.zoom_factor *
                                self.image_label.pixmap().size())

        self.adjustScrollBar(self.scroll_area.horizontalScrollBar(),
                             zoom_value)
        self.adjustScrollBar(self.scroll_area.verticalScrollBar(), zoom_value)

        self.zoom_in_act.setEnabled(self.zoom_factor < 4.0)
        self.zoom_out_act.setEnabled(self.zoom_factor > 0.333)

    def normalSize(self):
        """View image with its normal dimensions."""
        self.image_label.adjustSize()
        self.zoom_factor = 1.0

    def adjustScrollBar(self, scroll_bar, value):
        """Adjust the scrollbar when zooming in or out."""
        scroll_bar.setValue(
            int(value * scroll_bar.value()) +
            ((value - 1) * scroll_bar.pageStep() / 2))

    def aboutDialog(self):
        QMessageBox.about(
            self, "About Photo Editor",
            "Photo Editor\nVersion 0.2\n\nCreated by Joshua Willman")

    def keyPressEvent(self, event):
        """Handle key press events."""
        if event.key() == Qt.Key_Escape:
            self.close()
        if event.key() == Qt.Key_F1:  # fn + F1 on Mac
            if self.isMaximized():
                self.showNormal()
            else:
                self.showMaximized()

    def closeEvent(self, event):
        pass
Beispiel #15
0
class AppGUI(QtGui.QWidget):
    def __init__(self):
        super().__init__()

        self.X = pydicom.dcmread('images/.dcm').pixel_array

        self.z = self.X.shape[0] // 2
        self.y = self.X.shape[1] // 2
        self.x = self.X.shape[2] // 2


        self.init_ui()
        self.qt_connections()

    def init_ui(self):
        pg.setConfigOption('background', 'w')
        pg.setConfigOption('imageAxisOrder', 'row-major')

        self.zname = 'Head', 'Feet'
        self.yname = 'Face', 'Back'
        self.xname = 'Left Hand', 'Right Hand'


        self.layout = QVBoxLayout()

        self.setGeometry(0, 0, 1440, 900)
        self.setWindowTitle('DICOM Viewer')


        self.z_slice_label = QLabel(f'Z axis [{self.zname[0]} - {self.zname[1]}] Slice: {self.z + 1}/{self.X.shape[0]}')
        self.y_slice_label = QLabel(f'Y axis [{self.yname[0]} - {self.yname[1]}] Slice: {self.y + 1}/{self.X.shape[1]}')
        self.x_slice_label = QLabel(f'X axis [{self.xname[0]} - {self.xname[1]}] Slice: {self.x + 1}/{self.X.shape[2]}')


        # slices plots ----------------------------------------------------------------
        
        self.autolevels = True
        self.levels = (0, 100)
        self.glayout = pg.GraphicsLayoutWidget()
        self.glayout.ci.layout.setContentsMargins(0, 0, 0, 0)
        self.glayout.ci.layout.setSpacing(0)

        self.zi = pg.ImageItem(self.X[self.z, :     , :     ], autoLevels=self.autolevels, levels=self.levels, border=pg.mkPen(color='r', width=3))
        self.yi = pg.ImageItem(self.X[:     , self.y, :     ], autoLevels=self.autolevels, levels=self.levels, border=pg.mkPen(color='g', width=3))
        self.xi = pg.ImageItem(self.X[:     , :     , self.x], autoLevels=self.autolevels, levels=self.levels, border=pg.mkPen(color='b', width=3))
        self.zp = self.glayout.addPlot()
        self.yp = self.glayout.addPlot()
        self.xp = self.glayout.addPlot()
        # self.z_slice_plot.setTitle(f'Z axis [{self.z_axis_name[0]} - {self.z_axis_name[1]}]')
        # self.y_slice_plot.setTitle(f'Y axis [{self.y_axis_name[0]} - {self.y_axis_name[1]}]')
        # self.x_slice_plot.setTitle(f'X axis [{self.x_axis_name[0]} - {self.x_axis_name[1]}]')
        self.zp.setAspectLocked()
        self.yp.setAspectLocked()
        self.xp.setAspectLocked()

        self.zp.setMouseEnabled(x=False, y=False)
        self.yp.setMouseEnabled(x=False, y=False)
        self.xp.setMouseEnabled(x=False, y=False)

        self.z_slice_plot_y_helper1 = self.zp.plot([0        ,  self.X.shape[2]], [self.y    , self.y         ], pen='g')
        self.z_slice_plot_y_helper2 = self.zp.plot([0        ,  self.X.shape[2]], [self.y + 1, self.y + 1     ], pen='g')
        self.z_slice_plot_x_helper1 = self.zp.plot([self.x   ,  self.x         ], [0         , self.X.shape[1]], pen='b')
        self.z_slice_plot_x_helper2 = self.zp.plot([self.x + 1, self.x + 1     ], [0         , self.X.shape[1]], pen='b')
        self.y_slice_plot_z_helper1 = self.yp.plot([0        ,  self.X.shape[2]], [self.z    , self.z         ], pen='r')
        self.y_slice_plot_z_helper2 = self.yp.plot([0        ,  self.X.shape[2]], [self.z + 1, self.z + 1     ], pen='r')
        self.y_slice_plot_x_helper1 = self.yp.plot([self.x    , self.x         ], [0         , self.X.shape[0]], pen='b')
        self.y_slice_plot_x_helper2 = self.yp.plot([self.x + 1, self.x + 1     ], [0         , self.X.shape[0]], pen='b')
        self.x_slice_plot_z_helper1 = self.xp.plot([0        ,  self.X.shape[1]], [self.z    , self.z         ], pen='r')
        self.x_slice_plot_z_helper2 = self.xp.plot([0        ,  self.X.shape[1]], [self.z + 1, self.z + 1     ], pen='r')
        self.x_slice_plot_y_helper1 = self.xp.plot([self.y    , self.y         ], [0         , self.X.shape[0]], pen='g')
        self.x_slice_plot_y_helper2 = self.xp.plot([self.y + 1, self.y + 1     ], [0         , self.X.shape[0]], pen='g')

        self.zp.invertY(True)
        self.yp.invertY(True)
        self.xp.invertY(True)

        self.zp.setLabel('bottom', f'X axis [{self.xname[0]} - {self.xname[1]}]')
        self.yp.setLabel('bottom', f'X axis [{self.xname[0]} - {self.xname[1]}]')
        self.zp.setLabel('left'  , f'Y axis [{self.yname[1]} - {self.yname[0]}]')
        self.xp.setLabel('bottom', f'Y axis [{self.yname[0]} - {self.yname[1]}]')
        self.yp.setLabel('left'  , f'Z axis [{self.zname[1]} - {self.zname[0]}]')
        self.xp.setLabel('left'  , f'Z axis [{self.zname[1]} - {self.zname[0]}]')

        self.zp.addItem(self.zi)
        self.yp.addItem(self.yi)
        self.xp.addItem(self.xi)

        self.zi.setRect(pg.QtCore.QRectF(0, 0, self.X.shape[2], self.X.shape[1]))
        self.yi.setRect(pg.QtCore.QRectF(0, 0, self.X.shape[2], self.X.shape[0]))
        self.xi.setRect(pg.QtCore.QRectF(0, 0, self.X.shape[1], self.X.shape[0]))

        self.zi.setZValue(-1)
        self.yi.setZValue(-1)
        self.xi.setZValue(-1)


        self.zs = QSlider()
        self.ys = QSlider()
        self.xs = QSlider()
        self.zs.setStyleSheet('background-color: rgba(255, 0, 0, 0.2)')
        self.ys.setStyleSheet('background-color: rgba(0, 255, 0, 0.2)')
        self.xs.setStyleSheet('background-color: rgba(0, 0, 255, 0.2)')
        self.zs.setOrientation(Qt.Orientation.Horizontal)
        self.ys.setOrientation(Qt.Orientation.Horizontal)
        self.xs.setOrientation(Qt.Orientation.Horizontal)
        self.zs.setRange(0, self.X.shape[0] - 1)
        self.ys.setRange(0, self.X.shape[1] - 1)
        self.xs.setRange(0, self.X.shape[2] - 1)
        self.zs.setValue(self.z)
        self.ys.setValue(self.y)
        self.xs.setValue(self.x)
        self.zs.setTickPosition(QSlider.TickPosition.TicksBelow)
        self.ys.setTickPosition(QSlider.TickPosition.TicksBelow)
        self.xs.setTickPosition(QSlider.TickPosition.TicksBelow)
        self.zs.setTickInterval(1)
        self.ys.setTickInterval(1)
        self.xs.setTickInterval(1)
        self.layout.addWidget(self.zs)
        self.layout.addWidget(self.ys)
        self.layout.addWidget(self.xs)
        self.layout.addWidget(self.z_slice_label)
        self.layout.addWidget(self.y_slice_label)
        self.layout.addWidget(self.x_slice_label)
        self.layout.addWidget(self.glayout)
        self.setLayout(self.layout)

        self.show()


    def qt_connections(self):
        self.zs.valueChanged.connect(self.zs_changed)
        self.ys.valueChanged.connect(self.ys_changed)
        self.xs.valueChanged.connect(self.xs_changed)


    def wheelEvent(self, event):
        if self.zi.sceneBoundingRect().contains(self.glayout.mapFromParent(event.pos())):
            self.z = np.clip(self.z + np.sign(event.angleDelta().y()), 0, self.X.shape[0] - 1) # change bounds 0..N-1 => 1..N
            self.zs.setValue(self.z)
        elif self.yi.sceneBoundingRect().contains(self.glayout.mapFromParent(event.pos())):
            self.y = np.clip(self.y + np.sign(event.angleDelta().y()), 0, self.X.shape[1] - 1) # change bounds 0..N-1 => 1..N
            self.ys.setValue(self.y)
        elif self.xi.sceneBoundingRect().contains(self.glayout.mapFromParent(event.pos())):
            self.x = np.clip(self.x + np.sign(event.angleDelta().y()), 0, self.X.shape[2] - 1) # change bounds 0..N-1 => 1..N
            self.xs.setValue(self.x)


    def update_slice_helpers_lines(self):
        self.z_slice_plot_y_helper1.setData([0               , self.X.shape[2]], [self.y    , self.y         ])
        self.z_slice_plot_y_helper2.setData([0               , self.X.shape[2]], [self.y + 1, self.y + 1     ])
        self.z_slice_plot_x_helper1.setData([self.x          , self.x         ], [0         , self.X.shape[1]])
        self.z_slice_plot_x_helper2.setData([self.x + 1      , self.x + 1     ], [0         , self.X.shape[1]])
        self.y_slice_plot_z_helper1.setData([0               , self.X.shape[2]], [self.z    , self.z         ])
        self.y_slice_plot_z_helper2.setData([0               , self.X.shape[2]], [self.z + 1, self.z + 1     ])
        self.y_slice_plot_x_helper1.setData([self.x          , self.x         ], [0         , self.X.shape[0]])
        self.y_slice_plot_x_helper2.setData([self.x + 1      , self.x + 1     ], [0         , self.X.shape[0]])
        self.x_slice_plot_z_helper1.setData([0               , self.X.shape[1]], [self.z    , self.z         ])
        self.x_slice_plot_z_helper2.setData([0               , self.X.shape[1]], [self.z + 1, self.z + 1     ])
        self.x_slice_plot_y_helper1.setData([self.y          , self.y         ], [0         , self.X.shape[0]])
        self.x_slice_plot_y_helper2.setData([self.y + 1      , self.y + 1     ], [0         , self.X.shape[0]])

    def zs_changed(self):
        self.z = self.zs.value()
        self.z_slice_label.setText(f'Z axis [{self.zname[0]} - {self.zname[1]}] Slice: {self.z + 1}/{self.X.shape[0]}')
        self.zi.setImage(self.X[self.z, :, :])
        self.update_slice_helpers_lines()

    def ys_changed(self):
        self.y = self.ys.value()
        self.y_slice_label.setText(f'Y axis [{self.yname[0]} - {self.yname[1]}] Slice: {self.y + 1}/{self.X.shape[1]}')
        self.yi.setImage(self.X[:, self.y, :])
        self.update_slice_helpers_lines()

    def xs_changed(self):
        self.x = self.xs.value()
        self.x_slice_label.setText(f'X axis [{self.xname[0]} - {self.xname[1]}] Slice: {self.x + 1}/{self.X.shape[2]}')
        self.xi.setImage(self.X[:, :, self.x])
        self.update_slice_helpers_lines()
Beispiel #16
0
class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setFixedSize(800, 410)
        self.setWindowTitle("PyLX16A Servo Testing Software")

        self.port_selection_box = QComboBox(self)
        self.port_selection_box.setFixedSize(200, 27)
        self.port_selection_box.move(30, 65)
        port_selection_box_label = QLabel("Select Port:", self)
        port_selection_box_label.move(30, 35)

        self.port_selection_box_refresh_button = QPushButton("Refresh", self)
        self.port_selection_box_refresh_button.setFixedSize(60, 23)
        self.port_selection_box_refresh_button.move(170, 38)

        self.id_selection_box = QListWidget(self)
        self.id_selection_box.setFixedSize(200, 200)
        self.id_selection_box.move(30, 135)
        id_selection_box_label = QLabel("Connected Servos:", self)
        id_selection_box_label.setFixedWidth(200)
        id_selection_box_label.move(30, 105)

        self.id_selection_box_refresh_button = QPushButton("Refresh", self)
        self.id_selection_box_refresh_button.setFixedSize(60, 23)
        self.id_selection_box_refresh_button.move(170, 108)

        self.set_id_line_edit = QLineEdit(self)
        self.set_id_line_edit.setFixedSize(50, 27)
        self.set_id_line_edit.move(80, 355)
        set_id_line_edit_label = QLabel("Set ID:", self)
        set_id_line_edit_label.move(30, 355)
        set_id_line_edit_label.setFixedSize(50, 27)

        self.set_id_button = QPushButton("Change ID!", self)
        self.set_id_button.setFixedSize(85, 27)
        self.set_id_button.move(145, 355)

        self.position_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.position_slider.setMinimum(0)
        self.position_slider.setMaximum(240)
        self.position_slider.setFixedWidth(200)
        self.position_slider.move(300, 55)
        self.position_slider_readout = QLabel("0.00°", self)
        self.position_slider_readout.setFixedWidth(50)
        self.position_slider_readout.move(450, 30)
        self.position_slider_readout.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        position_slider_label = QLabel("Angle (degrees):", self)
        position_slider_label.move(300, 30)

        self.position_offset_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.position_offset_slider.setMinimum(-30)
        self.position_offset_slider.setMaximum(30)
        self.position_offset_slider.setFixedWidth(200)
        self.position_offset_slider.move(300, 125)
        self.position_offset_slider_readout = QLabel("0.00°", self)
        self.position_offset_slider_readout.setFixedWidth(50)
        self.position_offset_slider_readout.move(450, 100)
        self.position_offset_slider_readout.setAlignment(
            Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
        position_offset_slider_label = QLabel("Angle offset (degrees):", self)
        position_offset_slider_label.setFixedWidth(200)
        position_offset_slider_label.move(300, 100)

        self.angle_lower_limit_textentry = QLineEdit(self)
        self.angle_lower_limit_textentry.setFixedWidth(50)
        self.angle_lower_limit_textentry.move(450, 175)
        self.angle_lower_limit_textentry.setValidator(
            QIntValidator(0, 240, self))
        self.angle_upper_limit_textentry = QLineEdit(self)
        self.angle_upper_limit_textentry.setFixedWidth(50)
        self.angle_upper_limit_textentry.move(450, 210)
        self.angle_upper_limit_textentry.setValidator(
            QIntValidator(0, 240, self))
        self.angle_lower_limit_textentry_label = QLabel(
            "Lower Limit (degrees):", self)
        self.angle_lower_limit_textentry_label.move(300, 175)
        self.angle_lower_limit_textentry_label.setFixedWidth(150)
        self.angle_upper_limit_textentry_label = QLabel(
            "Upper Limit (degrees):", self)
        self.angle_upper_limit_textentry_label.move(300, 210)
        self.angle_upper_limit_textentry_label.setFixedWidth(150)

        self.vin_lower_limit_textentry = QLineEdit(self)
        self.vin_lower_limit_textentry.setFixedWidth(50)
        self.vin_lower_limit_textentry.move(450, 265)
        self.vin_lower_limit_textentry.setValidator(
            QIntValidator(4500, 12000, self))
        self.vin_upper_limit_textentry = QLineEdit(self)
        self.vin_upper_limit_textentry.setFixedWidth(50)
        self.vin_upper_limit_textentry.move(450, 300)
        self.vin_upper_limit_textentry.setValidator(
            QIntValidator(4500, 12000, self))
        self.vin_lower_limit_textentry_label = QLabel(
            "Voltage Lower Limit (mV):", self)
        self.vin_lower_limit_textentry_label.move(300, 265)
        self.vin_lower_limit_textentry_label.setFixedWidth(150)
        self.vin_upper_limit_textentry_label = QLabel(
            "Voltage Upper Limit (mV):", self)
        self.vin_upper_limit_textentry_label.move(300, 300)
        self.vin_upper_limit_textentry_label.setFixedWidth(150)

        self.temp_limit_textentry = QLineEdit(self)
        self.temp_limit_textentry.setFixedWidth(50)
        self.temp_limit_textentry.move(450, 355)
        self.temp_limit_textentry.setValidator(QIntValidator(50, 100, self))
        self.temp_limit_textentry_label = QLabel("Temp Limit (°C):", self)
        self.temp_limit_textentry_label.move(300, 355)
        self.temp_limit_textentry_label.setFixedWidth(150)

        self.servo_mode_radio_button = QRadioButton("Servo Mode", self)
        self.servo_mode_radio_button.move(565, 50)
        self.motor_mode_radio_button = QRadioButton("Motor Mode", self)
        self.motor_mode_radio_button.move(565, 75)

        self.motor_speed_slider = QSlider(Qt.Orientation.Horizontal, self)
        self.motor_speed_slider.setMinimum(-1000)
        self.motor_speed_slider.setMaximum(1000)
        self.motor_speed_slider.setFixedWidth(200)
        self.motor_speed_slider.move(565, 125)
        motor_speed_slider_label = QLabel("Motor Speed:", self)
        motor_speed_slider_label.move(565, 100)

        self.torque_enabled_checkbox = QCheckBox("Torque Enabled", self)
        self.torque_enabled_checkbox.move(565, 175)
        self.torque_enabled_checkbox.setFixedWidth(200)

        self.led_enabled_checkbox = QCheckBox("LED Enabled", self)
        self.led_enabled_checkbox.move(565, 210)
        self.led_enabled_checkbox.setFixedWidth(200)

        self.led_over_temp_checkbox = QCheckBox("LED Over Temperature", self)
        self.led_over_temp_checkbox.move(565, 258)
        self.led_over_temp_checkbox.setFixedWidth(200)
        self.led_over_voltage_checkbox = QCheckBox("LED Over Voltage", self)
        self.led_over_voltage_checkbox.move(565, 283)
        self.led_over_voltage_checkbox.setFixedWidth(200)
        self.led_rotor_locked_checkbox = QCheckBox("LED Rotor Locked", self)
        self.led_rotor_locked_checkbox.move(565, 308)
        self.led_rotor_locked_checkbox.setFixedWidth(200)

        self.physical_position_readout = QLabel("--°", self)
        self.physical_position_readout.move(565, 367)
        self.physical_position_readout.setFixedWidth(200)
        self.physical_position_readout_label = QLabel("Position", self)
        self.physical_position_readout_label.move(565, 347)

        self.temperature_readout = QLabel("-- °C", self)
        self.temperature_readout.move(635, 367)
        self.temperature_readout.setFixedWidth(200)
        self.temperature_readout_label = QLabel("Temperature", self)
        self.temperature_readout_label.move(635, 347)

        self.voltage_readout = QLabel("-- V", self)
        self.voltage_readout.move(730, 367)
        self.voltage_readout.setFixedWidth(200)
        self.voltage_readout_label = QLabel("Voltage", self)
        self.voltage_readout_label.move(730, 347)

        self.readout_update_timer = QTimer(self)
        self.readout_update_timer.timeout.connect(self.update_readouts)
        self.readout_update_timer.start(250)

        self.active_servo: LX16A = None

        self.position_slider.setValue(0)
        self.position_offset_slider.setValue(0)
        self.motor_speed_slider.setValue(0)
        self.id_selection_box_refresh_button.setEnabled(False)
        self.disable_widgets()

        self.port_selection_box.currentTextChanged.connect(
            self.port_selection_box_changed)
        self.port_selection_box_refresh_button.clicked.connect(
            self.port_refresh_button_clicked)
        self.id_selection_box.currentTextChanged.connect(
            self.id_selection_box_changed)
        self.id_selection_box_refresh_button.clicked.connect(
            self.id_refresh_button_clicked)
        self.set_id_button.pressed.connect(self.id_updated)
        self.position_slider.sliderMoved.connect(self.position_slider_updated)
        self.position_offset_slider.sliderMoved.connect(
            self.position_offset_slider_updated)
        self.angle_lower_limit_textentry.textChanged.connect(
            self.angle_lower_limit_updated)
        self.angle_upper_limit_textentry.textChanged.connect(
            self.angle_upper_limit_updated)
        self.vin_lower_limit_textentry.textChanged.connect(
            self.vin_lower_limit_updated)
        self.vin_upper_limit_textentry.textChanged.connect(
            self.vin_upper_limit_updated)
        self.temp_limit_textentry.textChanged.connect(self.temp_limit_updated)
        self.servo_mode_radio_button.toggled.connect(
            self.servo_mode_radio_button_toggled)
        self.motor_mode_radio_button.toggled.connect(
            self.motor_mode_radio_button_toggled)
        self.motor_speed_slider.valueChanged.connect(
            self.motor_speed_slider_updated)
        self.torque_enabled_checkbox.stateChanged.connect(
            self.torque_enabled_checkbox_toggled)
        self.led_enabled_checkbox.stateChanged.connect(
            self.led_enabled_checkbox_toggled)
        self.led_over_temp_checkbox.stateChanged.connect(
            self.led_error_triggers_checkbox_toggled)
        self.led_over_voltage_checkbox.stateChanged.connect(
            self.led_error_triggers_checkbox_toggled)
        self.led_rotor_locked_checkbox.stateChanged.connect(
            self.led_error_triggers_checkbox_toggled)

        self.scan_for_ports()

    def disable_widgets(self):
        self.set_id_line_edit.setEnabled(False)
        self.position_slider.setEnabled(False)
        self.position_offset_slider.setEnabled(False)
        self.angle_lower_limit_textentry.setEnabled(False)
        self.angle_upper_limit_textentry.setEnabled(False)
        self.vin_lower_limit_textentry.setEnabled(False)
        self.vin_upper_limit_textentry.setEnabled(False)
        self.temp_limit_textentry.setEnabled(False)
        self.servo_mode_radio_button.setEnabled(False)
        self.motor_mode_radio_button.setEnabled(False)
        self.motor_speed_slider.setEnabled(False)
        self.torque_enabled_checkbox.setEnabled(False)
        self.led_enabled_checkbox.setEnabled(False)
        self.led_over_temp_checkbox.setEnabled(False)
        self.led_over_voltage_checkbox.setEnabled(False)
        self.led_rotor_locked_checkbox.setEnabled(False)

    def enable_widgets(self):
        self.set_id_line_edit.setEnabled(True)
        self.position_slider.setEnabled(True)
        self.position_offset_slider.setEnabled(True)
        self.angle_lower_limit_textentry.setEnabled(True)
        self.angle_upper_limit_textentry.setEnabled(True)
        self.vin_lower_limit_textentry.setEnabled(True)
        self.vin_upper_limit_textentry.setEnabled(True)
        self.temp_limit_textentry.setEnabled(True)
        self.servo_mode_radio_button.setEnabled(True)
        self.motor_mode_radio_button.setEnabled(True)
        self.motor_speed_slider.setEnabled(True)
        self.torque_enabled_checkbox.setEnabled(True)
        self.led_enabled_checkbox.setEnabled(True)
        self.led_over_temp_checkbox.setEnabled(True)
        self.led_over_voltage_checkbox.setEnabled(True)
        self.led_rotor_locked_checkbox.setEnabled(True)

    def clear_servo(self):
        self.active_servo = None

    @catch_disconnection
    def set_servo_id(self, id_):
        if not id_.isdigit():
            return

        self.active_servo = LX16A(int(id_))
        self.active_servo.enable_torque()

        self.position_slider.setValue(
            int(self.active_servo.get_physical_angle()))
        self.position_slider_readout.setText(
            f"{int(self.active_servo.get_physical_angle() * 25 / 6) * 6 / 25:0.2f}°"
        )
        self.position_offset_slider.setValue(
            int(self.active_servo.get_angle_offset()))
        self.position_offset_slider_readout.setText(
            f"{int(self.active_servo.get_angle_offset() * 25 / 6) * 6 / 25:0.2f}°"
        )
        self.angle_lower_limit_textentry.setText(
            str(int(self.active_servo.get_angle_limits()[0])))
        self.angle_upper_limit_textentry.setText(
            str(int(self.active_servo.get_angle_limits()[1])))
        self.vin_lower_limit_textentry.setText(
            str(self.active_servo.get_vin_limits()[0]))
        self.vin_upper_limit_textentry.setText(
            str(self.active_servo.get_vin_limits()[1]))
        self.temp_limit_textentry.setText(
            str(self.active_servo.get_temp_limit()))
        self.motor_speed_slider.setValue(self.active_servo.get_motor_speed(
        ) if self.active_servo.is_motor_mode() else 0)
        if self.active_servo.is_motor_mode():
            self.motor_mode_radio_button.setChecked(True)
        else:
            self.servo_mode_radio_button.setChecked(True)
        self.motor_speed_slider.setEnabled(self.active_servo.is_motor_mode())
        self.torque_enabled_checkbox.setChecked(
            self.active_servo.is_torque_enabled())
        self.led_enabled_checkbox.setChecked(
            self.active_servo.is_led_power_on())
        self.led_over_temp_checkbox.setChecked(
            self.active_servo.get_led_error_triggers()[0])
        self.led_over_voltage_checkbox.setChecked(
            self.active_servo.get_led_error_triggers()[1])
        self.led_rotor_locked_checkbox.setChecked(
            self.active_servo.get_led_error_triggers()[2])

    @catch_disconnection
    def scan_for_servos(self, port):
        self.setCursor(Qt.CursorShape.WaitCursor)

        LX16A.initialize(port)

        self.id_selection_box.clear()

        for i in range(0, 254):
            try:
                servo = LX16A(i)
                self.id_selection_box.addItem(str(i))
            except:
                pass

        self.setCursor(Qt.CursorShape.ArrowCursor)

    @catch_disconnection
    def scan_for_ports(self):
        ports = serial.tools.list_ports.comports()
        for port in ports:
            self.port_selection_box.addItem(port.device)

    @catch_disconnection
    def update_readouts(self):
        if self.active_servo is None:
            return

        try:
            self.physical_position_readout.setText(
                f"{self.active_servo.get_physical_angle():0.2f}°")
            self.temperature_readout.setText(
                f"{self.active_servo.get_temp()} °C")
            self.voltage_readout.setText(
                f"{self.active_servo.get_vin() / 1000} V")
        except (ServoTimeoutError, ServoChecksumError):
            pass

    @catch_disconnection
    def id_updated(self):
        new_id = self.set_id_line_edit.text()

        try:
            servo = LX16A(int(new_id))
        except ServoTimeoutError:
            # Meaning this ID is not taken
            self.active_servo.set_id(int(new_id))
            self.id_selection_box.item(
                self.id_selection_box.currentRow()).setText(new_id)

            return

        QMessageBox.warning(None, "Error", "ID already taken")

    @catch_disconnection
    def position_slider_updated(self, pos):
        if float(self.voltage_readout.text()[:-2]) < 5:
            QMessageBox.warning(
                None,
                "Error",
                "The voltage going through the servo is too low. Is your battery powered on?",
            )

            return
        self.active_servo.move(pos)
        self.position_slider_readout.setText(
            f"{int(pos * 25 / 6) * 6 / 25:0.2f}°")

    @catch_disconnection
    def position_offset_slider_updated(self, pos):
        self.active_servo.set_angle_offset(pos)
        self.position_offset_slider_readout.setText(
            f"{int(pos * 25 / 6) * 6 / 25:0.2f}°")

    @catch_disconnection
    def angle_lower_limit_updated(self, text):
        if (QIntValidator(0, 240, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        if int(text) > int(self.angle_upper_limit_textentry.text()):
            return

        self.active_servo.set_angle_limits(
            int(text), int(self.angle_upper_limit_textentry.text()))

    @catch_disconnection
    def angle_upper_limit_updated(self, text):
        if (QIntValidator(0, 240, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        if int(text) < int(self.angle_lower_limit_textentry.text()):
            return

        self.active_servo.set_angle_limits(
            int(self.angle_lower_limit_textentry.text()), int(text))

    @catch_disconnection
    def vin_lower_limit_updated(self, text):
        if (QIntValidator(4500, 12000, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        if int(text) > int(self.vin_upper_limit_textentry.text()):
            return

        self.active_servo.set_vin_limits(
            int(text), int(self.vin_upper_limit_textentry.text()))

    @catch_disconnection
    def vin_upper_limit_updated(self, text):
        if (QIntValidator(4500, 12000, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        if int(text) < int(self.vin_lower_limit_textentry.text()):
            return

        self.active_servo.set_vin_limits(
            int(self.vin_lower_limit_textentry.text()), int(text))

    @catch_disconnection
    def temp_limit_updated(self, text):
        if (QIntValidator(50, 100, self).validate(text, 0) !=
                QIntValidator.State.Acceptable):
            return

        self.active_servo.set_temp_limit(int(text))

    @catch_disconnection
    def servo_mode_radio_button_toggled(self, checked):
        if checked:
            self.active_servo.servo_mode()
            self.motor_speed_slider.setEnabled(False)
            self.position_slider.setEnabled(True)
            self.position_offset_slider.setEnabled(True)
        else:
            self.active_servo.motor_mode(int(self.motor_speed_slider.value()))
            self.motor_speed_slider.setEnabled(True)
            self.position_slider.setEnabled(False)
            self.position_offset_slider.setEnabled(False)

    @catch_disconnection
    def motor_mode_radio_button_toggled(self, checked):
        if checked:
            self.active_servo.motor_mode(int(self.motor_speed_slider.value()))
            self.motor_speed_slider.setEnabled(True)
            self.position_slider.setEnabled(False)
            self.position_offset_slider.setEnabled(False)
        else:
            self.active_servo.servo_mode()
            self.motor_speed_slider.setEnabled(False)
            self.position_slider.setEnabled(True)
            self.position_offset_slider.setEnabled(True)

    @catch_disconnection
    def motor_speed_slider_updated(self, pos):
        self.active_servo.motor_mode(pos)

    @catch_disconnection
    def torque_enabled_checkbox_toggled(self, checked):
        if checked:
            self.active_servo.enable_torque()
        else:
            self.active_servo.disable_torque()

        self.position_slider.setEnabled(checked)
        self.position_offset_slider.setEnabled(checked)
        self.servo_mode_radio_button.setEnabled(checked)
        self.motor_mode_radio_button.setEnabled(checked)
        self.motor_speed_slider.setEnabled(checked)

    @catch_disconnection
    def led_enabled_checkbox_toggled(self, checked):
        if checked:
            self.active_servo.led_power_on()
        else:
            self.active_servo.led_power_off()

    @catch_disconnection
    def led_error_triggers_checkbox_toggled(self):
        self.active_servo.set_led_error_triggers(
            self.led_over_voltage_checkbox.isChecked(),
            self.led_over_temp_checkbox.isChecked(),
            self.led_rotor_locked_checkbox.isChecked(),
        )

    @catch_disconnection
    def port_refresh_button_clicked(self, value):
        self.id_selection_box_refresh_button.setEnabled(False)
        self.disable_widgets()
        self.port_selection_box.clear()
        self.id_selection_box.clear()
        self.scan_for_ports()

    @catch_disconnection
    def id_refresh_button_clicked(self, value):
        self.disable_widgets()
        self.id_selection_box.clear()
        self.scan_for_servos(self.port_selection_box.currentText())

    @catch_disconnection
    def port_selection_box_changed(self, text):
        if text == "":
            return

        self.id_selection_box_refresh_button.setEnabled(True)
        self.disable_widgets()
        self.id_selection_box.clear()
        self.clear_servo()
        self.scan_for_servos(text)

    @catch_disconnection
    def id_selection_box_changed(self, text):
        if text == "":
            return

        self.enable_widgets()
        self.set_servo_id(text)
Beispiel #17
0
    def init_ui(self):
        pg.setConfigOption('background', 'w')
        pg.setConfigOption('imageAxisOrder', 'row-major')

        self.zname = 'Head', 'Feet'
        self.yname = 'Face', 'Back'
        self.xname = 'Left Hand', 'Right Hand'


        self.layout = QVBoxLayout()

        self.setGeometry(0, 0, 1440, 900)
        self.setWindowTitle('DICOM Viewer')


        self.z_slice_label = QLabel(f'Z axis [{self.zname[0]} - {self.zname[1]}] Slice: {self.z + 1}/{self.X.shape[0]}')
        self.y_slice_label = QLabel(f'Y axis [{self.yname[0]} - {self.yname[1]}] Slice: {self.y + 1}/{self.X.shape[1]}')
        self.x_slice_label = QLabel(f'X axis [{self.xname[0]} - {self.xname[1]}] Slice: {self.x + 1}/{self.X.shape[2]}')


        # slices plots ----------------------------------------------------------------
        
        self.autolevels = True
        self.levels = (0, 100)
        self.glayout = pg.GraphicsLayoutWidget()
        self.glayout.ci.layout.setContentsMargins(0, 0, 0, 0)
        self.glayout.ci.layout.setSpacing(0)

        self.zi = pg.ImageItem(self.X[self.z, :     , :     ], autoLevels=self.autolevels, levels=self.levels, border=pg.mkPen(color='r', width=3))
        self.yi = pg.ImageItem(self.X[:     , self.y, :     ], autoLevels=self.autolevels, levels=self.levels, border=pg.mkPen(color='g', width=3))
        self.xi = pg.ImageItem(self.X[:     , :     , self.x], autoLevels=self.autolevels, levels=self.levels, border=pg.mkPen(color='b', width=3))
        self.zp = self.glayout.addPlot()
        self.yp = self.glayout.addPlot()
        self.xp = self.glayout.addPlot()
        # self.z_slice_plot.setTitle(f'Z axis [{self.z_axis_name[0]} - {self.z_axis_name[1]}]')
        # self.y_slice_plot.setTitle(f'Y axis [{self.y_axis_name[0]} - {self.y_axis_name[1]}]')
        # self.x_slice_plot.setTitle(f'X axis [{self.x_axis_name[0]} - {self.x_axis_name[1]}]')
        self.zp.setAspectLocked()
        self.yp.setAspectLocked()
        self.xp.setAspectLocked()

        self.zp.setMouseEnabled(x=False, y=False)
        self.yp.setMouseEnabled(x=False, y=False)
        self.xp.setMouseEnabled(x=False, y=False)

        self.z_slice_plot_y_helper1 = self.zp.plot([0        ,  self.X.shape[2]], [self.y    , self.y         ], pen='g')
        self.z_slice_plot_y_helper2 = self.zp.plot([0        ,  self.X.shape[2]], [self.y + 1, self.y + 1     ], pen='g')
        self.z_slice_plot_x_helper1 = self.zp.plot([self.x   ,  self.x         ], [0         , self.X.shape[1]], pen='b')
        self.z_slice_plot_x_helper2 = self.zp.plot([self.x + 1, self.x + 1     ], [0         , self.X.shape[1]], pen='b')
        self.y_slice_plot_z_helper1 = self.yp.plot([0        ,  self.X.shape[2]], [self.z    , self.z         ], pen='r')
        self.y_slice_plot_z_helper2 = self.yp.plot([0        ,  self.X.shape[2]], [self.z + 1, self.z + 1     ], pen='r')
        self.y_slice_plot_x_helper1 = self.yp.plot([self.x    , self.x         ], [0         , self.X.shape[0]], pen='b')
        self.y_slice_plot_x_helper2 = self.yp.plot([self.x + 1, self.x + 1     ], [0         , self.X.shape[0]], pen='b')
        self.x_slice_plot_z_helper1 = self.xp.plot([0        ,  self.X.shape[1]], [self.z    , self.z         ], pen='r')
        self.x_slice_plot_z_helper2 = self.xp.plot([0        ,  self.X.shape[1]], [self.z + 1, self.z + 1     ], pen='r')
        self.x_slice_plot_y_helper1 = self.xp.plot([self.y    , self.y         ], [0         , self.X.shape[0]], pen='g')
        self.x_slice_plot_y_helper2 = self.xp.plot([self.y + 1, self.y + 1     ], [0         , self.X.shape[0]], pen='g')

        self.zp.invertY(True)
        self.yp.invertY(True)
        self.xp.invertY(True)

        self.zp.setLabel('bottom', f'X axis [{self.xname[0]} - {self.xname[1]}]')
        self.yp.setLabel('bottom', f'X axis [{self.xname[0]} - {self.xname[1]}]')
        self.zp.setLabel('left'  , f'Y axis [{self.yname[1]} - {self.yname[0]}]')
        self.xp.setLabel('bottom', f'Y axis [{self.yname[0]} - {self.yname[1]}]')
        self.yp.setLabel('left'  , f'Z axis [{self.zname[1]} - {self.zname[0]}]')
        self.xp.setLabel('left'  , f'Z axis [{self.zname[1]} - {self.zname[0]}]')

        self.zp.addItem(self.zi)
        self.yp.addItem(self.yi)
        self.xp.addItem(self.xi)

        self.zi.setRect(pg.QtCore.QRectF(0, 0, self.X.shape[2], self.X.shape[1]))
        self.yi.setRect(pg.QtCore.QRectF(0, 0, self.X.shape[2], self.X.shape[0]))
        self.xi.setRect(pg.QtCore.QRectF(0, 0, self.X.shape[1], self.X.shape[0]))

        self.zi.setZValue(-1)
        self.yi.setZValue(-1)
        self.xi.setZValue(-1)


        self.zs = QSlider()
        self.ys = QSlider()
        self.xs = QSlider()
        self.zs.setStyleSheet('background-color: rgba(255, 0, 0, 0.2)')
        self.ys.setStyleSheet('background-color: rgba(0, 255, 0, 0.2)')
        self.xs.setStyleSheet('background-color: rgba(0, 0, 255, 0.2)')
        self.zs.setOrientation(Qt.Orientation.Horizontal)
        self.ys.setOrientation(Qt.Orientation.Horizontal)
        self.xs.setOrientation(Qt.Orientation.Horizontal)
        self.zs.setRange(0, self.X.shape[0] - 1)
        self.ys.setRange(0, self.X.shape[1] - 1)
        self.xs.setRange(0, self.X.shape[2] - 1)
        self.zs.setValue(self.z)
        self.ys.setValue(self.y)
        self.xs.setValue(self.x)
        self.zs.setTickPosition(QSlider.TickPosition.TicksBelow)
        self.ys.setTickPosition(QSlider.TickPosition.TicksBelow)
        self.xs.setTickPosition(QSlider.TickPosition.TicksBelow)
        self.zs.setTickInterval(1)
        self.ys.setTickInterval(1)
        self.xs.setTickInterval(1)
        self.layout.addWidget(self.zs)
        self.layout.addWidget(self.ys)
        self.layout.addWidget(self.xs)
        self.layout.addWidget(self.z_slice_label)
        self.layout.addWidget(self.y_slice_label)
        self.layout.addWidget(self.x_slice_label)
        self.layout.addWidget(self.glayout)
        self.setLayout(self.layout)

        self.show()
Beispiel #18
0
class VideoWindow(QMainWindow):
    def __init__(self, parent=None):
        super(VideoWindow, self).__init__(parent)
        self.setWindowTitle("StudioProject")
        self.statusBar = QStatusBar()
        self.setStatusBar(self.statusBar)
        self.gScene = QGraphicsScene(self)
        self.gView = GraphicView(self.gScene, self)
        self.gView.viewport().setAttribute(
            Qt.WidgetAttribute.WA_AcceptTouchEvents, False)
        # self.gView.setBackgroundBrush(QBrush(Qt.black))

        self.videoStartDatetime = None
        self.videoCurrentDatetime = None

        self.projectFile = ''
        self.graphicsFile = ''
        self.videoFile = ''

        self.obsTb = ObsToolbox(self)

        # # ===================== Setting video item ==============================
        # self.videoItem = QGraphicsVideoItem()
        # self.videoItem.setAspectRatioMode(Qt.KeepAspectRatio)
        # self.gScene.addItem(self.videoItem)
        # self.videoItem.mouseMoveEvent = self.gView.mouseMoveEvent

        self.mediaPlayer = QMediaPlayer(self)
        # self.mediaPlayer.setVideoOutput(self.videoItem)
        self.mediaPlayer.playbackStateChanged.connect(self.mediaStateChanged)
        self.mediaPlayer.positionChanged.connect(self.positionChanged)
        self.mediaPlayer.durationChanged.connect(self.durationChanged)
        self.mediaPlayer.errorOccurred.connect(self.handleError)
        # self.mediaPlayer.setMuted(True)
        # self.mediaPlayer.setNotifyInterval(100)

        self.playButton = QPushButton()
        self.playButton.setEnabled(False)
        self.playButton.setIcon(self.style().standardIcon(
            QStyle.StandardPixmap.SP_MediaPlay))
        self.playButton.clicked.connect(self.play)

        self.changePlayRateBtn = QPushButton('1x')
        self.changePlayRateBtn.setFixedWidth(40)
        # self.incrPlayRateBtn.setEnabled(False)
        self.changePlayRateBtn.clicked.connect(self.changePlayRate)

        self.positionSlider = QSlider(Qt.Orientation.Horizontal)
        self.positionSlider.setRange(0, 0)
        self.positionSlider.sliderMoved.connect(self.setPosition)

        self.timerLabel = QLabel()
        self.timerLabel.setText('--:--:--')
        self.timerLabel.setFixedWidth(58)

        self.dateLabel = QLabel()
        self.dateLabel.setText('Video date: --')
        self.statusBar.addPermanentWidget(self.dateLabel)

        # Create open action
        self.openVideoAction = QAction(QIcon('icons/video-file.png'),
                                       'Open video', self)
        self.openVideoAction.setShortcut('Ctrl+O')
        self.openVideoAction.setStatusTip('Open video file')
        self.openVideoAction.triggered.connect(self.openVideoFile)

        # Create observation action
        obsTbAction = QAction(QIcon('icons/checklist.png'),
                              'Observation toolbox', self)
        obsTbAction.setStatusTip('Open observation toolbox')
        obsTbAction.triggered.connect(self.openObsToolbox)

        self.drawPointAction = QAction(QIcon('icons/drawPoint.png'),
                                       'Draw point', self)
        self.drawPointAction.setStatusTip('Draw point over the video')
        self.drawPointAction.setCheckable(True)
        self.drawPointAction.setEnabled(False)
        self.drawPointAction.triggered.connect(self.drawingClick)

        self.drawLineAction = QAction(QIcon('icons/drawLine.png'), 'Draw line',
                                      self)
        self.drawLineAction.setStatusTip('Draw line over the video')
        self.drawLineAction.setCheckable(True)
        self.drawLineAction.setEnabled(False)
        self.drawLineAction.triggered.connect(self.drawingClick)

        self.drawZoneAction = QAction(QIcon('icons/drawZone.png'), 'Draw zone',
                                      self)
        self.drawZoneAction.setStatusTip('Draw zone over the video')
        self.drawZoneAction.setCheckable(True)
        self.drawZoneAction.setEnabled(False)
        self.drawZoneAction.triggered.connect(self.drawingClick)

        self.maskGenAction = QAction(QIcon('icons/mask.png'),
                                     'Generate mask file', self)
        self.maskGenAction.setStatusTip(
            'Generate mask file for TrafficIntelligence')
        self.maskGenAction.setCheckable(True)
        self.maskGenAction.setEnabled(False)
        self.maskGenAction.triggered.connect(self.generateMask)

        actionGroup = QActionGroup(self)
        actionGroup.addAction(self.drawPointAction)
        actionGroup.addAction(self.drawLineAction)
        actionGroup.addAction(self.drawZoneAction)

        openProjectAction = QAction(QIcon('icons/open-project.png'),
                                    'Open project', self)
        openProjectAction.setStatusTip('Open project')
        openProjectAction.triggered.connect(self.openProject)

        self.saveProjectAction = QAction(QIcon('icons/save-project.png'),
                                         'Save project', self)
        self.saveProjectAction.setStatusTip('Save project')
        self.saveProjectAction.setEnabled(False)
        self.saveProjectAction.triggered.connect(self.saveProject)

        self.saveGraphAction = QAction(QIcon('icons/save-graphics.png'),
                                       'Save graphics', self)
        self.saveGraphAction.setStatusTip('Save graphics to database')
        self.saveGraphAction.setEnabled(False)
        self.saveGraphAction.triggered.connect(self.saveGraphics)

        self.loadGraphAction = QAction(QIcon('icons/folders.png'),
                                       'Load graphics', self)
        self.loadGraphAction.setStatusTip('Load graphics from database')
        self.loadGraphAction.setEnabled(False)
        self.loadGraphAction.triggered.connect(self.loadGraphics)

        # Create exit action
        exitAction = QAction(QIcon('icons/close.png'), 'Exit', self)
        exitAction.setShortcut('Ctrl+Q')
        exitAction.setStatusTip('Exit application')
        exitAction.triggered.connect(self.exitCall)  # self.exitCall

        # Create menu bar and add action
        # menuBar = self.menuBar()
        # menuBar.setNativeMenuBar(False)
        # fileMenu = menuBar.addMenu('&File')
        # fileMenu.addAction(openVideoAction)
        # fileMenu.addAction(obsTbAction)
        # fileMenu.addAction(exitAction)

        self.toolbar = self.addToolBar('Tools')
        self.toolbar.setIconSize(QSize(24, 24))
        self.toolbar.addAction(openProjectAction)
        self.toolbar.addAction(self.saveProjectAction)
        self.toolbar.addAction(self.openVideoAction)

        # self.toolbar.insertSeparator(self.loadGraphAction)
        # self.toolbar.addAction(self.loadGraphAction)
        # self.toolbar.addAction(self.saveGraphAction)
        # self.toolbar.addAction(self.drawPointAction)
        # self.toolbar.addAction(self.drawLineAction)
        # self.toolbar.addAction(self.drawZoneAction)
        self.toolbar.addAction(self.maskGenAction)
        # self.toolbar.insertSeparator(self.drawPointAction)

        self.toolbar.insertSeparator(obsTbAction)
        self.toolbar.addAction(obsTbAction)

        self.toolbar.insertSeparator(exitAction)
        self.toolbar.addAction(exitAction)

        # Create a widget for window contents
        wid = QWidget(self)
        self.setCentralWidget(wid)

        # Create layouts to place inside widget
        controlLayout = QHBoxLayout()
        controlLayout.setContentsMargins(0, 0, 0, 0)
        # controlLayout.addWidget(self.decrPlayRateBtn)
        controlLayout.addWidget(self.playButton)
        controlLayout.addWidget(self.changePlayRateBtn)
        controlLayout.addWidget(self.timerLabel)
        controlLayout.addWidget(self.positionSlider)
        # controlLayout.addWidget(self.durationLabel)

        layout = QVBoxLayout()
        layout.addWidget(self.gView)
        layout.addLayout(controlLayout)

        # Set widget to contain window contents
        wid.setLayout(layout)

    # def showEvent(self, event):
    # self.gView.fitInView(self.videoItem, Qt.KeepAspectRatio)

    def openVideoFile(self):
        # self.mediaPlayer.setMedia(QMediaContent())
        if self.sender() == self.openVideoAction:
            self.videoFile, _ = QFileDialog.getOpenFileName(
                self, "Open video", QDir.homePath())
            # if self.videoFile != '':
            #     self.setWindowTitle('{} - {}'.format(os.path.basename(self.videoFile),
            #                                          os.path.basename(self.projectFile)))

        if self.videoFile != '':
            self.setWindowTitle('{} - {}'.format(
                os.path.basename(self.videoFile),
                os.path.basename(self.projectFile)))
            self.saveProjectAction.setEnabled(True)
            self.maskGenAction.setEnabled(True)
            # self.loadGraphAction.setEnabled(True)
            # self.saveGraphAction.setEnabled(True)
            # self.drawPointAction.setEnabled(True)
            # self.drawLineAction.setEnabled(True)
            # self.drawZoneAction.setEnabled(True)

            creation_datetime, width, height = getVideoMetadata(self.videoFile)
            self.videoStartDatetime = self.videoCurrentDatetime = creation_datetime
            self.dateLabel.setText(creation_datetime.strftime('%a, %b %d, %Y'))

            self.gView.setSceneRect(0, 0, width, height)

            self.videoItem = QGraphicsVideoItem()
            self.videoItem.setAspectRatioMode(
                Qt.AspectRatioMode.KeepAspectRatio)
            self.gScene.addItem(self.videoItem)
            self.videoItem.mouseMoveEvent = self.gView.mouseMoveEvent
            self.videoItem.setSize(QSizeF(width, height))

            self.mediaPlayer.setVideoOutput(self.videoItem)
            self.mediaPlayer.setSource(QUrl.fromLocalFile(self.videoFile))

            self.gView.labelSize = width / 50

            self.playButton.setEnabled(True)
            # self.gView.setViewport(QOpenGLWidget())
            self.mediaPlayer.pause()

    def exitCall(self):
        # sys.exit(app.exec())
        # self.mediaPlayer.pause()
        self.close()

    def play(self):
        # self.gView.fitInView(self.videoItem, Qt.KeepAspectRatio)

        if self.mediaPlayer.playbackState(
        ) == QMediaPlayer.PlaybackState.PlayingState:
            self.mediaPlayer.pause()

        else:
            self.mediaPlayer.play()

    def changePlayRate(self):
        if self.mediaPlayer.playbackRate() < 2:
            r = self.mediaPlayer.playbackRate() + 0.5
            self.mediaPlayer.setPlaybackRate(r)
            self.changePlayRateBtn.setText('{:g}x'.format(r))
            self.statusBar.showMessage('Play back rate = {:g}x'.format(r),
                                       2000)
        elif self.mediaPlayer.playbackRate() == 2:
            self.mediaPlayer.setPlaybackRate(1)
            self.changePlayRateBtn.setText('{}x'.format(1))
            self.statusBar.showMessage('Play back rate = {}x'.format(1), 2000)

    def mediaStateChanged(self, state):
        if self.mediaPlayer.playbackState(
        ) == QMediaPlayer.PlaybackState.PlayingState:
            self.playButton.setIcon(self.style().standardIcon(
                QStyle.StandardPixmap.SP_MediaPause))
        else:
            self.playButton.setIcon(self.style().standardIcon(
                QStyle.StandardPixmap.SP_MediaPlay))

    def positionChanged(self, position):
        self.positionSlider.setValue(position)
        s, m, h = self.convertMillis(position)
        self.videoCurrentDatetime = self.videoStartDatetime + \
                                    timedelta(hours=h, minutes=m, seconds=s)
        self.timerLabel.setText('{:02d}:{:02d}:{:02d}'.format(
            self.videoCurrentDatetime.time().hour,
            self.videoCurrentDatetime.time().minute,
            self.videoCurrentDatetime.time().second))

    def durationChanged(self, duration):
        self.positionSlider.setRange(0, duration)

        # s, m, h = self.convertMillis(duration)
        # self.durationLabel.setText('{:02d}:{:02d}'.format(m, s))

    def setPosition(self, position):
        self.mediaPlayer.setPosition(position)

    def handleError(self):
        self.playButton.setEnabled(False)
        # self.errorLabel.setText("Error: " + self.mediaPlayer.errorString())

    def openObsToolbox(self):
        if not self.obsTb.isVisible():
            self.obsTb.show()

    def drawingClick(self):
        # if self.sender() == self.drawLineAction:
        #     self.labelingAction.setChecked(False)
        # else:
        #     self.drawLineAction.setChecked(False)
        cursor = QCursor(Qt.CursorShape.CrossCursor)
        self.gView.setCursor(cursor)

    def generateMask(self):
        if not self.sender().isChecked():
            self.gView.unsetCursor()
            return
        cursor = QCursor(Qt.CursorShape.CrossCursor)
        self.gView.setCursor(cursor)

        # dbfilename = self.obsTb.dbFilename
        # if dbfilename != None:
        #     self.session = connectDatabase(dbfilename)
        # else:
        #     msg = QMessageBox()
        #     msg.setIcon(QMessageBox.Information)
        #     msg.setText('The database file is not defined.')
        #     msg.setInformativeText('In order to set the database file, open the Observation Toolbox')
        #     msg.setIcon(QMessageBox.Critical)
        #     msg.exec_()
        #     return

        # if self.gView.unsavedLines == [] and self.gView.unsavedZones == [] and \
        #         self.gView.unsavedPoints == []:
        #     QMessageBox.information(self, 'Save', 'There is no polygon to generate mask!')
        #     return

    def saveMaskFile(self):
        creation_datetime, width, height = getVideoMetadata(self.videoFile)
        item = self.gView.gPolyItem  #self.gView.unsavedZones[0]
        mask_polygon = item.polygon()
        xy = []
        for p in mask_polygon:
            xy.append((p.x(), p.y()))

        img = Image.new('RGB', (width, height), color='black')
        img1 = ImageDraw.Draw(img)
        img1.polygon(xy, fill="white", outline="white")

        fileName, _ = QFileDialog.getSaveFileName(self, "Open database file",
                                                  QDir.homePath(),
                                                  "PNG files (*.png)")
        if fileName != '':
            img.save(fileName)

        self.gView.scene().removeItem(item)
        self.gView.unsavedZones = []

    def openProject(self):
        self.projectFile, _ = QFileDialog.getOpenFileName(
            self, "Open project file", QDir.homePath(), "Project (*.prj)")

        if self.projectFile == '':
            return

        self.saveProjectAction.setEnabled(True)
        self.maskGenAction.setEnabled(True)
        # self.loadGraphAction.setEnabled(True)
        # self.saveGraphAction.setEnabled(True)
        # self.drawPointAction.setEnabled(True)
        # self.drawLineAction.setEnabled(True)
        # self.drawZoneAction.setEnabled(True)

        tree = ET.parse(self.projectFile)
        root = tree.getroot()
        gItems = []
        for elem in root:
            subEelTexts = {}
            for subelem in elem:
                subEelTexts[subelem.tag] = subelem.text
            gItems.append([elem.tag, subEelTexts])

        for key in gItems:
            if key[0] == 'database':
                item = key[1]
                if item['fileName'] is not None:
                    self.obsTb.dbFilename = item['fileName']
                    self.obsTb.opendbFile()

            elif key[0] == 'video':
                item = key[1]
                if item['fileName'] is not None:
                    self.videoFile = item['fileName']
                    self.openVideoFile()
                    self.mediaPlayer.setPosition(int(item['sliderValue']))
                    if item['fileName'] is not None:
                        self.loadGraphics()

            elif key[0] == 'trajectory':
                item = key[1]
                if item['metadata'] != None:
                    self.obsTb.mdbFileLedit.setText(item['metadata'])
                    self.obsTb.openMdbFile()
                    self.obsTb.siteNameCombobx.setCurrentIndex(
                        int(item['site']))
                    self.obsTb.camViewCombobx.setCurrentIndex(
                        int(item['cam_view']))
                    self.obsTb.trjDbCombobx.setCurrentIndex(
                        int(item['traj_db']))

            elif key[0] == 'window':
                item = key[1]
                x, y = item['mainWin_pos'].split(',')
                w, h = item['mainWin_size'].split(',')
                self.setGeometry(int(x), int(y), int(w), int(h))
                if item['obsTbx_open'] == 'True':
                    self.obsTb.show()
                    x, y = item['obsTbx_pos'].split(',')
                    w, h = item['obsTbx_size'].split(',')
                    self.obsTb.setGeometry(int(x), int(y), int(w), int(h))

        # self.setWindowTitle('{} - {}'.format(os.path.basename(self.videoFile),
        #                                      os.path.basename(self.projectFile)))

    def saveProject(self):

        if self.projectFile == '':
            fileDir = QDir.homePath()
        else:
            fileDir = self.projectFile

        self.projectFile, _ = QFileDialog.getSaveFileName(
            self, "Save project file", fileDir, "Project (*.prj)")
        # fileName = "/Users/Abbas/project.xml"
        if self.projectFile == '':
            return

        file = QFile(self.projectFile)
        if (not file.open(QIODevice.OpenModeFlag.WriteOnly
                          | QIODevice.OpenModeFlag.Text)):
            return

        xmlWriter = QXmlStreamWriter(file)
        xmlWriter.setAutoFormatting(True)
        xmlWriter.writeStartDocument()

        xmlWriter.writeStartElement('project')

        xmlWriter.writeStartElement('database')
        xmlWriter.writeTextElement("fileName", self.obsTb.dbFilename)
        xmlWriter.writeEndElement()

        xmlWriter.writeStartElement('video')
        xmlWriter.writeTextElement(
            "fileName",
            self.videoFile)  #mediaPlayer.media().canonicalUrl().path())
        xmlWriter.writeTextElement("sliderValue",
                                   str(self.mediaPlayer.position()))
        xmlWriter.writeEndElement()

        xmlWriter.writeStartElement('trajectory')
        xmlWriter.writeTextElement("metadata", self.obsTb.mdbFileLedit.text())
        xmlWriter.writeTextElement(
            "site", str(self.obsTb.siteNameCombobx.currentIndex()))
        xmlWriter.writeTextElement(
            "cam_view", str(self.obsTb.camViewCombobx.currentIndex()))
        xmlWriter.writeTextElement("traj_db",
                                   str(self.obsTb.trjDbCombobx.currentIndex()))
        xmlWriter.writeEndElement()

        xmlWriter.writeStartElement('window')
        xmlWriter.writeTextElement(
            "mainWin_size", "{},{}".format(int(self.width()),
                                           int(self.height())))
        xmlWriter.writeTextElement(
            "mainWin_pos", "{},{}".format(int(self.x()), int(self.y())))
        xmlWriter.writeTextElement("obsTbx_open", str(self.obsTb.isVisible()))
        xmlWriter.writeTextElement(
            "obsTbx_size", "{},{}".format(int(self.obsTb.width()),
                                          int(self.obsTb.height())))
        xmlWriter.writeTextElement(
            "obsTbx_pos", "{},{}".format(int(self.obsTb.x()),
                                         int(self.obsTb.y())))
        xmlWriter.writeEndElement()

        xmlWriter.writeEndElement()

        self.setWindowTitle('{} - {}'.format(
            os.path.basename(self.videoFile),
            os.path.basename(self.projectFile)))
        if self.obsTb.dbFilename != None:
            self.obsTb.setWindowTitle('{} - {}'.format(
                os.path.basename(self.obsTb.dbFilename),
                os.path.basename(self.projectFile)))

    def saveGraphics(self):
        dbfilename = self.obsTb.dbFilename
        if dbfilename != None:
            self.session = connectDatabase(dbfilename)
        else:
            msg = QMessageBox()
            msg.setIcon(QMessageBox.Information)
            msg.setText('The database file is not defined.')
            msg.setInformativeText(
                'In order to set the database file, open the Observation Toolbox'
            )
            msg.setIcon(QMessageBox.Critical)
            msg.exec_()
            return

        if self.gView.unsavedLines == [] and self.gView.unsavedZones == [] and \
                self.gView.unsavedPoints == []:
            QMessageBox.information(self, 'Save',
                                    'No new graphical items to save!')
            return

        for item in self.gView.unsavedLines:
            x1 = round(item.line().x1(), 2)
            y1 = round(item.line().y1(), 2)
            x2 = round(item.line().x2(), 2)
            y2 = round(item.line().y2(), 2)
            line = Line(None, None, x1, y1, x2, y2)
            self.session.add(line)
            self.session.flush()
            label = self.generate_itemGroup([x1, x2], [y1, y2], line.idx)
            self.gView.scene().addItem(label)

        for item in self.gView.unsavedZones:
            xs = []
            ys = []
            for p in item.polygon():
                xs.append(round(p.x(), 2))
                ys.append(round(p.y(), 2))
            zone = Zone(None, None, xs, ys)
            self.session.add(zone)
            self.session.flush()

            label = self.generate_itemGroup(xs, ys, zone.idx)
            self.gView.scene().addItem(label)

        for item in self.gView.unsavedPoints:
            x = round(item.rect().center().x(), 2)
            y = round(item.rect().center().y(), 2)

            point = Point(x, y)
            self.session.add(point)
            self.session.flush()

            label = self.generate_itemGroup([x], [y], point.idx)
            self.gView.scene().removeItem(item)
            self.gView.scene().addItem(label)

        QMessageBox.information(
            self, 'Save',
            '{} point(s), {} line(s) and {} zone(s) saved to database successfully!'
            .format(len(self.gView.unsavedPoints),
                    len(self.gView.unsavedLines),
                    len(self.gView.unsavedZones)))
        self.gView.unsavedLines = []
        self.gView.unsavedZones = []
        self.gView.unsavedPoints = []

        self.session.commit()

    def generate_itemGroup(self, xs, ys, label, type):
        gItemGroup = QGraphicsItemGroup()

        pointBbx = QRectF()
        pointBbx.setSize(QSizeF(self.gView.labelSize, self.gView.labelSize))

        textLabel = QGraphicsTextItem(label)

        if len(xs) == 1:
            pointBbx.moveCenter(QPointF(xs[0], ys[0]))
            textLabel.setPos(xs[0] - (textLabel.boundingRect().width() / 2),
                             ys[0] - (textLabel.boundingRect().height() / 2))

            pointShape = QGraphicsEllipseItem(pointBbx)
            shapeColor = Qt.GlobalColor.white
            textColor = Qt.GlobalColor.black
            tooltip = 'P{}:{}'
        elif len(xs) == 2:
            pointBbx.moveCenter(QPointF(xs[1], ys[1]))
            textLabel.setPos(xs[1] - (textLabel.boundingRect().width() / 2),
                             ys[1] - (textLabel.boundingRect().height() / 2))

            r, g, b = np.random.choice(range(256), size=3)
            line_item = QGraphicsLineItem(xs[0], ys[0], xs[1], ys[1])
            line_item.setPen(
                QPen(QColor(r, g, b, 128), self.gView.labelSize / 6))
            gItemGroup.addToGroup(line_item)

            # line_end = QGraphicsEllipseItem(xs[1], ys[1],
            #                                 int(self.gView.labelSize/3), int(self.gView.labelSize/3))
            # line_end.setPen(QPen(QColor(r, g, b), 0.5))
            # line_end.setBrush(QBrush(QColor(r, g, b)))
            # gItemGroup.addToGroup(line_end)

            pointShape = QGraphicsEllipseItem(pointBbx)
            shapeColor = QColor(r, g, b, 128)
            textColor = Qt.GlobalColor.black
            tooltip = 'L{}:{}'
            # textLabel.setRotation(np.arctan((ys[1] - ys[0])/(xs[1] - xs[0]))*(180/3.14))
        else:
            pointBbx.moveCenter(QPointF(np.mean(xs), np.mean(ys)))
            textLabel.setPos(
                np.mean(xs) - (textLabel.boundingRect().width() / 2),
                np.mean(ys) - (textLabel.boundingRect().height() / 2))

            points = [QPointF(x, y) for x, y in zip(xs, ys)]
            polygon = QPolygonF(points)
            r, g, b = np.random.choice(range(256), size=3)
            zone_item = QGraphicsPolygonItem(polygon)
            zone_item.setPen(QPen(QColor(r, g, b), self.gView.labelSize / 10))
            zone_item.setBrush(QBrush(QColor(r, g, b, 40)))
            gItemGroup.addToGroup(zone_item)

            pointShape = QGraphicsRectItem(pointBbx)
            shapeColor = Qt.GlobalColor.darkBlue
            textColor = Qt.GlobalColor.white
            tooltip = 'Z{}:{}'

        pointShape.setPen(QPen(Qt.GlobalColor.white, 0.5))
        pointShape.setBrush(QBrush(shapeColor))
        # self.gView.scene().addEllipse(pointBbx, QPen(Qt.white, 0.5), QBrush(Qt.black))
        gItemGroup.setToolTip(tooltip.format(label, type))
        gItemGroup.addToGroup(pointShape)

        labelFont = QFont()
        labelFont.setPointSize(round(self.gView.labelSize / 2))
        labelFont.setBold(True)

        textLabel.setFont(labelFont)
        textLabel.setDefaultTextColor(textColor)

        gItemGroup.addToGroup(textLabel)
        return gItemGroup

    def loadGraphics(self):
        dbfilename = self.obsTb.dbFilename
        if dbfilename != None:
            self.session = connectDatabase(dbfilename)
        else:
            msg = QMessageBox()
            # msg.setIcon(QMessageBox.Icon.Information)
            msg.setText('The database file is not defined.')
            msg.setInformativeText(
                'In order to set the database file, open the Observation Toolbox'
            )
            msg.setIcon(QMessageBox.Icon.Critical)
            msg.exec()
            return

        for gitem in self.gView.scene().items():
            if isinstance(gitem, QGraphicsItemGroup):
                self.gView.scene().removeItem(gitem)

        q_line = self.session.query(Line)
        q_zone = self.session.query(Zone)
        if q_line.all() == [] and q_zone.all() == []:
            QMessageBox.information(self, 'Warning!',
                                    'There is no graphics to load!')
            return

        line_items = []
        for line in q_line:
            p1 = line.points[0]
            p2 = line.points[1]

            if line.type != None:
                lineType = line.type.name
            else:
                lineType = None
            gItmGroup = self.generate_itemGroup([p1.x, p2.x], [p1.y, p2.y],
                                                str(line.idx), lineType)
            self.gScene.addItem(gItmGroup)

            line_items.append(str(line.idx))

        self.obsTb.line_list_wdgt.clear()
        self.obsTb.line_list_wdgt.addItems(line_items)
        self.obsTb.line_list_wdgt.setCurrentRow(0)

        self.obsTb.line_newRecButton.setEnabled(False)
        self.obsTb.line_saveButton.setEnabled(True)
        self.obsTb.line_saveButton.setText('Edit line(s)')
        self.obsTb.line_saveButton.setIcon(QIcon('icons/edit.png'))

        zone_items = []
        for zone in q_zone:
            if zone.type != None:
                zoneType = zone.type.name
            else:
                zoneType = None
            gItmGroup = self.generate_itemGroup(
                [point.x for point in zone.points],
                [point.y for point in zone.points], str(zone.idx), zoneType)
            self.gScene.addItem(gItmGroup)

            zone_items.append(str(zone.idx))

        self.obsTb.zone_list_wdgt.clear()
        self.obsTb.zone_list_wdgt.addItems(zone_items)
        self.obsTb.zone_list_wdgt.setCurrentRow(0)

        self.obsTb.zone_newRecButton.setEnabled(False)
        self.obsTb.zone_saveButton.setEnabled(True)
        self.obsTb.zone_saveButton.setText('Edit zone(s)')
        self.obsTb.zone_saveButton.setIcon(QIcon('icons/edit.png'))

    @staticmethod
    def convertMillis(millis):
        seconds = int(millis / 1000) % 60
        minutes = int(millis / (1000 * 60)) % 60
        hours = int(millis / (1000 * 60 * 60)) % 24
        return seconds, minutes, hours