Exemplo n.º 1
0
class MainWindow(QMainWindow):
    def __init__(self, parent=None):
        super(MainWindow, self).__init__(parent)
        self.root_folder = os.path.expanduser("~")
        self.logger = logging.getLogger(name='MainWindow Logger')

        self.set_main_widget()

        #create menus
        self.file_menu = self.menuBar().addMenu('&File')
        self.view_menu = self.menuBar().addMenu('View')
        self.video_menu = self.menuBar().addMenu('Video')
        self.tracking_menu = self.menuBar().addMenu('Tracking')
        self.analysis_menu = self.menuBar().addMenu('Analysis')

        #create toolbar for navigating through videos
        self.video_toolbar = self.addToolBar('Video')
        self.video_trackbar = self.addToolBar('Video Track Bar')

        self.set_file_menu()
        self.set_video_menu()
        self.set_tracking_menu()
        self.set_analysis_menu()

        self.set_slider()
        self.set_status()
        self.set_file_explorer()
        self.set_statistic_toolbar()
        self.set_classification_toolbar()

        self.set_view_menu()

    def set_main_widget(self):
        self.video_player = MainVideoPlayer()
        self.setCentralWidget(self.video_player)

    def add_menu_action(self,
                        menu,
                        name,
                        connection,
                        status_tip,
                        shortcut=None,
                        icon=None,
                        tool_bar=None,
                        return_action=False):
        """Helper function for adding a QAction to a menu.

        Parameters
        ----------
        menu : QMenu
            Which menu to add new QAction to.

        name : string
            Name of new QAction. This is displayed as the QAction
            text in the QMenu.

        connection : function
            Which function should the QAction be connected to.

        status_tip : string
            Status tip to display on MouseOver.

        shortcut : QKeySequence or None (default = None)
            Shortcut to execute action.

        icon : QIcon or None (default = None)
            Icon to attach to QAction.

        tool_bar : QToolBar or None
            QToolBar to attach QAction to (in addition to the specified QMenu).

        return_action : bool (default = False)
            Whether or not to return the action.
        """
        if icon is None:
            action = QAction(name, self)
        else:
            action = QAction(icon, name, self)

        if shortcut is not None:
            action.setShortcut(shortcut)

        action.setStatusTip(status_tip)
        action.triggered.connect(connection)
        menu.addAction(action)

        if tool_bar is not None:
            tool_bar.addAction(action)

        if return_action:
            return action

    def set_file_menu(self):
        self.add_menu_action(menu=self.file_menu,
                             name='Open Directory',
                             connection=self.open_file_directory,
                             status_tip='Open a folder in the file explorer.',
                             shortcut=QKeySequence.Open)
        self.add_menu_action(menu=self.file_menu,
                             name='Open Video',
                             connection=self.open_video,
                             status_tip='Open an individual video.',
                             shortcut=None)
        self.add_menu_action(
            menu=self.file_menu,
            name='Open Tracking Summary',
            connection=self.open_tracking_summary,
            status_tip='Open a tracked video file (.fcts, .xlsx, etc)')

    def set_view_menu(self):
        """Sets up menu to allow users to select which DockWidgets/ToolBars
        are displayed."""
        #file explorer
        display_file_explorer_action = self.file_dock_widget.toggleViewAction()
        self.view_menu.addAction(display_file_explorer_action)

        #statistic widget
        display_statistic_widget_action = self.statistic_toolbar.toggleViewAction(
        )
        self.view_menu.addAction(display_statistic_widget_action)

        #video toolbar
        display_video_toolbar_action = self.video_toolbar.toggleViewAction()
        self.view_menu.addAction(display_video_toolbar_action)

        #slider toolbar
        display_slider_toolbar_action = self.video_trackbar.toggleViewAction()
        self.view_menu.addAction(display_slider_toolbar_action)

    def set_video_menu(self):
        self.previous_action = self.add_menu_action(
            menu=self.video_menu,
            name='Previous Frame',
            connection=self.video_player.previous,
            status_tip='Go to previous frame.',
            icon=QIcon(os.path.join(DIR, 'icons', 'prev_icon.png')),
            tool_bar=self.video_toolbar,
            return_action=True)
        self.previous_action.setEnabled(False)
        self.previous_action.setShortcut(QKeySequence(','))

        self.play_action = self.add_menu_action(
            menu=self.video_menu,
            name='Play',
            connection=self.video_player.on,
            status_tip='Play Video.',
            icon=QIcon(os.path.join(DIR, 'icons', 'play_icon.png')),
            tool_bar=self.video_toolbar,
            return_action=True)
        self.play_action.setEnabled(False)
        self.play_action.setShortcut(QKeySequence(']'))

        self.stop_action = self.add_menu_action(
            menu=self.video_menu,
            name='Stop',
            connection=self.video_player.off,
            status_tip='Stop Video.',
            icon=QIcon(os.path.join(DIR, 'icons', 'stop_icon.png')),
            tool_bar=self.video_toolbar,
            return_action=True)
        self.stop_action.setEnabled(False)
        self.stop_action.setShortcut(QKeySequence('['))

        self.next_action = self.add_menu_action(
            menu=self.video_menu,
            name='Next Frame',
            connection=self.video_player.next,
            status_tip='Go to next frame.',
            icon=QIcon(os.path.join(DIR, 'icons', 'next_icon.png')),
            tool_bar=self.video_toolbar,
            return_action=True)
        self.next_action.setEnabled(False)
        self.next_action.setShortcut(QKeySequence('.'))

        self.stop_start_action = self.add_menu_action(
            menu=self.video_menu,
            name='Pause/Play',
            connection=self.stop_start,
            status_tip='Start playing, pause, or resume playing video.',
            return_action=True)
        self.stop_start_action.setEnabled(False)
        self.stop_start_action.setShortcut(QKeySequence(Qt.Key_Space))

        self.video_zoom_action = self.add_menu_action(
            menu=self.video_menu,
            name='Zoom',
            connection=self.zoom,
            status_tip='Zoom in on a segment of video.',
            return_action=True)
        self.video_zoom_enabled = False
        self.video_zoom_action.setEnabled(False)
        self.video_zoom_action.setShortcut(QKeySequence('Z'))

        self.video_menu.addSeparator()

        # Actions to control the speed of the video.
        self.videoMenuSpeedMenu = self.video_menu.addMenu('Playback Speed')
        speedFunctions = [
            self.adjustSpeed1x, self.adjustSpeed1p5x, self.adjustSpeed2x,
            self.adjustSpeed3x, self.adjustSpeed5x, self.adjustSpeed10x
        ]
        speedFunctionNames = ['1x', '1.5x', '2x', '3x', '5x', '10x']
        speedActionGroup = QActionGroup(self)
        for i in xrange(len(speedFunctionNames)):
            action = self.add_menu_action(
                menu=self.videoMenuSpeedMenu,
                name=speedFunctionNames[i],
                connection=speedFunctions[i],
                status_tip='Adjust the playback speed of the current video.',
                return_action=True)
            action.setCheckable(True)
            speedActionGroup.addAction(action)
            if i == 0:
                action.setChecked(True)

    def set_tracking_menu(self):
        self.add_menu_action(menu=self.tracking_menu,
                             name='Batch Processing',
                             connection=self.track_batch,
                             status_tip='Open Batch Tracking Wizard.')

    def set_analysis_menu(self):
        self.add_menu_action(menu=self.analysis_menu,
                             name='Signal Processing',
                             connection=self.signal_processing,
                             status_tip='Open Signal Processing Dialog.')

    def open_file_directory(self):
        file_dialog = QFileDialog()
        directory_name = str(
            file_dialog.getExistingDirectory(caption='Open Folder',
                                             options=QFileDialog.ShowDirsOnly))

        if not os.path.isdir(directory_name):
            self.logger.warning(
                'Passed directory was not valid (in ' +
                'MainWindow.open_file_directory(), line ~282 of entry.py).')
            return

        self.file_explorer.set_path(directory_name)
        self.root_folder = directory_name

    def open_video(self, video_fname=None):
        """Opens an individual video for display in the central widget."""
        if not isinstance(video_fname, str):
            file_dialog = QFileDialog(self)
            video_fname = str(
                file_dialog.getOpenFileName(caption='Open Video File',
                                            filter='Video Files (*.fmf)',
                                            directory=self.root_folder)[0])

        if not os.path.isfile(video_fname):
            self.logger.warning(
                'Passed `video_fname` was not valid (in ' +
                'MainWindow.open_video(), line 290 of entry.py).')
            return

        self.video_player.set_video(video_fname)
        self.video_player.image_annotation = False
        self.video_player.tracking_summary = None
        self.enable_video_controls()

    def enable_video_controls(self):
        """Enables all video controls and connects signals/slots if necessary."""
        #connect video_player signals
        self.video_player.frame_changed.connect(self.update_frame_label)

        #enable video controls
        self.previous_action.setEnabled(True)
        self.next_action.setEnabled(True)
        self.play_action.setEnabled(True)
        self.stop_action.setEnabled(True)
        self.stop_start_action.setEnabled(True)
        self.video_zoom_action.setEnabled(True)

        #enable slider
        self.slider.setMaximum(self.video_player.video.get_n_frames() - 1)
        self.slider.setValue(0)
        self.slider.setEnabled(True)

        self.frame_label.setText('Frame: 0')
        self.video_label.setText('Current Video: {}'.format(
            self.video_player.video_file_name))

    def open_tracking_summary(self, fcts_fname=None, video_fname=None):
        """Opens a tracking summary file (.fcts, .xlsx, etc) for display."""
        if not isinstance(fcts_fname, str):
            file_dialog = QFileDialog(self)
            fcts_fname = str(
                file_dialog.getOpenFileName(
                    caption='Open Tracking Summary File',
                    filter='Tracking Summary File (*.fcts *.xlsx)',
                    directory=self.root_folder)[0])

        if not os.path.isfile(fcts_fname):
            self.logger.warning(
                'Passed `fcts_fname` was not valid (in ' +
                'MainWindow.open_tracking_summary(), line 332 of entry.py).')
            return

        file_type = fcts_fname.split('.')[-1]
        if file_type == 'fcts':
            with open(fcts_fname, 'rb') as f:
                tracking_summary = pickle.load(f)
        elif file_type == 'xlsx':
            tracking_summary = FCTS().from_xlsx(
                fcts_fname)  # TODO: Add from_xlsx() function
        else:
            return

        if os.path.exists(tracking_summary.video.filename):
            self.video_player.set_video(tracking_summary.video.filename)
        else:
            if not isinstance(video_fname, str):
                file_dialog = QFileDialog(self)
                video_fname = str(
                    file_dialog.getOpenFileName(
                        caption='Select Associated Video File',
                        filter='Video File (*.fmf)',
                        directory=self.root_folder)[0])
            self.video_player.set_video(video_fname)

        self.video_player.tracking_summary = tracking_summary
        self.video_player.image_annotation = True
        self.video_player.update_label()
        self.enable_video_controls()

        # setup statistic toolbar
        self.statistic_toolbar.setEnabled(True)
        # calculate statistics about fly held in current experiment.
        lwa, rwa = wing_stats.individual_wing_angles(tracking_summary.male)
        fwa = wing_stats.full_wing_angle(tracking_summary.male)
        wd = wing_stats.wing_distances(tracking_summary.male)
        nn = spatial_stats.nearest_neighbor_centroid(tracking_summary.male,
                                                     tracking_summary.female)
        n2e, t2e = spatial_stats.nose_and_tail_to_ellipse(
            tracking_summary.male, tracking_summary.female)
        vel = centroid_stats.centroid_velocity(tracking_summary.male)
        self.statistics_widget.statistics = {
            'LeftWingAngle': lwa,
            'RightWingAngle': rwa,
            'FullWingAngle': fwa,
            'WingDistance': wd,
            'Centroid-to-Centroid': nn,
            'Nose-to-Ellipse': n2e,
            'Rear-to-Ellipse': t2e,
            'CentroidVelocity': vel
        }
        self.statistics_widget.update_combobox()
        self.statistics_widget.update_stat_label(0)
        self.statistic_toolbar.show()

        self.classification_toolbar.setEnabled(True)
        self.classification_widget.set_tracking_summary(tracking_summary)
        self.classification_widget.statistics = \
            tracking_summary.male.get_all_behaviors_as_dict()
        self.classification_widget.update_combobox()
        self.classification_widget.update_stat_label(0)
        self.classification_toolbar.show()

    def set_slider(self):
        """Places slider widget in video track bar menu."""
        self.slider = QSlider(Qt.Horizontal)
        self.slider.setMinimum(0)
        self.video_trackbar.addWidget(self.slider)
        self.slider.valueChanged[int].connect(
            self.video_player.slider_update_label)
        self.video_player.frame_changed.connect(self.update_slider)
        self.slider.setEnabled(False)

    def set_status(self):
        """Annotates video statistics at bottom of main window."""
        status = self.statusBar()
        status.setSizeGripEnabled(True)

        self.frame_label = QLabel()
        self.frame_label.setFrameStyle(QFrame.StyledPanel | QFrame.Sunken)
        self.frame_label.setText('Frame: NA (00:00:00)')

        self.video_label = QLabel()
        self.video_label.setText('Current Video: NA')

        status.addPermanentWidget(self.frame_label)
        status.addWidget(self.video_label)

    def set_file_explorer(self):
        """Initializes the file explorer as a DockWidget."""
        self.file_explorer = FileExplorer()
        self.file_dock_widget = QDockWidget('File Explorer', self)
        self.file_dock_widget.setWidget(self.file_explorer)
        self.addDockWidget(Qt.RightDockWidgetArea, self.file_dock_widget)
        self.file_explorer.open_video.connect(self.load_video_from_file_exp)

    def set_statistic_toolbar(self):
        self.statistic_toolbar = QDockWidget('Tracking Statistics')
        self.statistics_widget = StatisticWindowWidget()
        self.statistic_toolbar.setWidget(self.statistics_widget)
        self.statistic_toolbar.setEnabled(False)
        self.statistic_toolbar.hide()
        self.video_player.frame_changed.connect(
            self.statistics_widget.update_stat_label)
        self.addDockWidget(Qt.TopDockWidgetArea, self.statistic_toolbar)

    def set_classification_toolbar(self):
        self.classification_toolbar = QDockWidget('Behavioral Classifications')
        self.classification_widget = ClassificationWindowWidget()
        self.classification_toolbar.setWidget(self.classification_widget)
        self.classification_toolbar.setEnabled(False)
        self.classification_toolbar.hide()
        self.video_player.frame_changed.connect(
            self.classification_widget.update_stat_label)
        self.addDockWidget(Qt.TopDockWidgetArea, self.classification_toolbar)
        self.tabifyDockWidget(self.statistic_toolbar,
                              self.classification_toolbar)

    def track_batch(self):
        dialog = BatchTrackingDialog(self.root_folder)
        if dialog.exec_():
            return

    def signal_processing(self):
        dialog = StatProcessing(root_folder=self.root_folder, parent=self)
        dialog.show()

    @pyqtSlot(int, str, int)
    def update_frame_label(self, ix, hms, frame_rate):
        self.frame_label.setText(
            'Frame: {} ({}) | FPS: {} | Playback Speed: {}x'.format(
                ix, hms, frame_rate, self.video_player.playback_speed))

    @pyqtSlot(str)
    def load_video_from_file_exp(self, fname):
        self.open_video(str(fname))

    @pyqtSlot(int)
    def update_slider(self, ix):
        self.slider.setValue(ix)

    @pyqtSlot()
    def stop_start(self):
        if self.video_player.is_playing:
            self.video_player.off()
        else:
            self.video_player.on()

    @pyqtSlot()
    def zoom(self):
        """Allows user to select a region of the video to zoom in on."""
        if self.video_zoom_enabled:
            self.video_zoom_enabled = False
            self.video_player.zoom_enabled = False
            non_zooming_cursor = QCursor(Qt.ArrowCursor)
            self.video_player.setCursor(non_zooming_cursor)
            self.video_player.close_zoom_window()
            return

        self.video_zoom_enabled = True
        zooming_cursor = QCursor(Qt.CrossCursor)
        self.video_player.setCursor(zooming_cursor)
        self.video_player.zoom_enabled = True

    # Playback speed adjustments
    @pyqtSlot()
    def adjustSpeed1x(self):
        self.video_player.playback_speed = 1

    @pyqtSlot()
    def adjustSpeed1p5x(self):
        self.video_player.playback_speed = 1.5

    @pyqtSlot()
    def adjustSpeed2x(self):
        self.video_player.playback_speed = 2

    @pyqtSlot()
    def adjustSpeed3x(self):
        self.video_player.playback_speed = 3

    @pyqtSlot()
    def adjustSpeed5x(self):
        self.video_player.playback_speed = 5

    @pyqtSlot()
    def adjustSpeed10x(self):
        self.video_player.playback_speed = 10