diff --git a/.github/workflows/test_and_deploy.yml b/.github/workflows/test_and_deploy.yml index cc14a13..ef1584c 100755 --- a/.github/workflows/test_and_deploy.yml +++ b/.github/workflows/test_and_deploy.yml @@ -90,4 +90,4 @@ jobs: run: | git tag python setup.py sdist bdist_wheel - twine upload dist/* + twine upload dist/* \ No newline at end of file diff --git a/facemap/gui/gui.py b/facemap/gui/gui.py index 4d4b90c..ee60f65 100644 --- a/facemap/gui/gui.py +++ b/facemap/gui/gui.py @@ -30,9 +30,9 @@ QStatusBar, QToolButton, QWidget, + QGraphicsItemGroup ) from scipy.stats import skew, zscore - from facemap import process, roi, utils from facemap.gui import ( guiparts, @@ -265,6 +265,7 @@ def __init__( self.cframe = 0 self.traces1 = None self.traces2 = None + self.saccade_data = None ## Pose plot self.pose_scatterplot = pg.ScatterPlotItem(hover=True) @@ -939,6 +940,7 @@ def reset(self): self.keypoints_vtick = None self.svd_plot_vtick = None self.neural_win = None + self.saccade_data = None def pupil_sigma_change(self): self.pupil_sigma = float(self.sigma_box.text()) @@ -1286,7 +1288,9 @@ def process_ROIs(self): savepath = self.save_path else: savepath = None - if self.motSVD_checkbox.isChecked() or self.movSVD_checkbox.isChecked(): + print("Checking if ROIs are set") + if self.motSVD_checkbox.isChecked() or self.movSVD_checkbox.isChecked() or len(self.ROIs) > 0: + print("Processing ROIs") savename = process.run( self.filenames, GUIobject=QtWidgets, parent=self, savepath=savepath ) @@ -2009,9 +2013,59 @@ def plot_trace(self, wplot, proctype, wroi, color, keypoints_group_selected=None pen=pg.mkPen(color=(255, 255, 255), width=2, movable=True), ) selected_plot.addItem(self.keypoints_vtick) + if self.saccade_data is not None: + self.plot_saccade_data() selected_plot.setLimits(xMin=0, xMax=self.nframes) return tr + def plot_saccade_data(self): + """ + Plot saccade data on the SVD traces plot as a single toggleable item. + """ + if self.saccade_data is not None: + # Extract saccade data + saccade = self.saccade_data['Saccade'][0, 0].squeeze() + + # Check if saccade data matches the number of frames + if saccade.shape[0] != self.nframes: + print("Saccade data shape does not match the number of frames.") + return + + # Find start and end indices for saccades + sac_start_idx = np.where(np.diff(saccade) == 1)[0] + 1 + sac_end_idx = np.where(np.diff(saccade) == -1)[0] + 1 + + # Remove any existing saccade overlays to avoid duplication + if hasattr(self, 'saccade_vspan_items') and self.saccade_vspan_items: + for item in self.saccade_vspan_items: + self.svd_traces_plot.removeItem(item) + + # Create a list to store the saccade region items + self.saccade_vspan_items = [] + + # Add vertical spans for each saccade + for start, end in zip(sac_start_idx, sac_end_idx): + if start < end and end < self.nframes: + # Create a LinearRegionItem for the saccade + vspan = pg.LinearRegionItem( + values=(start, end), # Start and end positions + brush=pg.mkBrush(255, 255, 255, 50), # Semi-transparent white + movable=False, # Non-movable + ) + + # Add the region to the plot + self.svd_traces_plot.addItem(vspan) + + # Keep track of added items for later removal + self.saccade_vspan_items.append(vspan) + else: + print("No saccade data available.") + + def toggle_saccade_vspans(self, show): + # Toggles visibility of the saccade vspans group + if hasattr(self, 'saccade_vspan_group'): # Check if the group exists + self.saccade_vspan_group.setVisible(show) + def on_click_svd_plot(self, event): """ Update vtick position of svd plot when user clicks diff --git a/facemap/gui/io.py b/facemap/gui/io.py index 38a4953..ba54401 100644 --- a/facemap/gui/io.py +++ b/facemap/gui/io.py @@ -354,6 +354,24 @@ def load_movies(parent, filelist=None): parent.load_keypoints_from_videodir() return good +def load_saccade(parent): + path = QFileDialog.getOpenFileName(parent, "Select a file", filter="MAT (*.mat)") + # Check if path exists + if path[0]: + try: + import scipy.io + + saccade_data = scipy.io.loadmat(path[0]) + parent.saccade_data = saccade_data['eye_movement_data'] + parent.update_status_bar("Saccade data loaded") + parent.plot_saccade_data() + except Exception as e: + msg = QMessageBox(parent) + msg.setIcon(QMessageBox.Icon.Warning) + msg.setText("Error loading saccade data: " + str(e)) + msg.setStandardButtons(QMessageBox.StandardButton.Ok) + msg.exec_() + def load_npy_file(parent, allow_mat=False): # Open a file dialog to select a folder diff --git a/facemap/gui/menus.py b/facemap/gui/menus.py index 3d5e8af..6291705 100644 --- a/facemap/gui/menus.py +++ b/facemap/gui/menus.py @@ -23,6 +23,11 @@ def mainmenu(parent): load_proc.triggered.connect(lambda: io.open_proc(parent)) parent.addAction(load_proc) + # Load saccade data + load_saccade = QAction("Load saccade data", parent) + load_saccade.triggered.connect(lambda: io.load_saccade(parent)) + parent.addAction(load_saccade) + # Set output folder set_output_folder = QAction("Set output folder", parent) set_output_folder.setShortcut("Ctrl+S") @@ -95,6 +100,7 @@ def mainmenu(parent): file_menu.addAction(open_file) file_menu.addAction(open_folder) file_menu.addAction(load_proc) + file_menu.addAction(load_saccade) file_menu.addAction(set_output_folder) pose_menu = main_menu.addMenu("Pose")