Skip to content
Merged
2 changes: 1 addition & 1 deletion .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,4 +90,4 @@ jobs:
run: |
git tag
python setup.py sdist bdist_wheel
twine upload dist/*
twine upload dist/*
58 changes: 56 additions & 2 deletions facemap/gui/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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())
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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
Expand Down
18 changes: 18 additions & 0 deletions facemap/gui/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions facemap/gui/menus.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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")
Expand Down