diff --git a/.readthedocs.yml b/.readthedocs.yml index 2ec2239..8168acb 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -4,6 +4,11 @@ # Required version: 2 +build: + os: ubuntu-22.04 + tools: + python: "3.11" + # Build documentation in the docs/ directory with Sphinx sphinx: configuration: docs/conf.py @@ -18,12 +23,11 @@ formats: # specify dependencies python: - version: 3.8 install: - requirements: docs/requirements.txt - #- method: pip - #path: . - #extra_requirements: - # - docs - # - gui + - method: pip + path: . + extra_requirements: + - docs + - gui diff --git a/README.md b/README.md index 0adfb48..918baac 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -[![Downloads](https://pepy.tech/badge/facemap)](https://pepy.tech/project/facemap) -[![Downloads](https://pepy.tech/badge/facemap/month)](https://pepy.tech/project/facemap) +[![Downloads](https://static.pepy.tech/badge/facemap)](https://pepy.tech/project/facemap) +[![Downloads](https://static.pepy.tech/badge/facemap/month)](https://pepy.tech/project/facemap) [![GitHub stars](https://badgen.net/github/stars/Mouseland/facemap)](https://github.com/MouseLand/facemap/stargazers) [![GitHub forks](https://badgen.net/github/forks/Mouseland/facemap)](https://github.com/MouseLand/facemap/network/members) [![](https://img.shields.io/github/license/MouseLand/facemap)](https://github.com/MouseLand/facemap/blob/main/LICENSE) @@ -7,7 +7,7 @@ [![Documentation Status](https://readthedocs.org/projects/ansicolortags/badge/?version=latest)](https://pypi.org/project/facemap/) [![GitHub open issues](https://badgen.net/github/open-issues/Mouseland/facemap)](https://github.com/MouseLand/facemap/issues) -# Facemap facemap +# Facemap facemap Facemap is a framework for predicting neural activity from mouse orofacial movements. It includes a pose estimation model for tracking distinct keypoints on the mouse face, a neural network model for predicting neural activity using the pose estimates, and also can be used compute the singular value decomposition (SVD) of behavioral videos. diff --git a/docs/api.rst b/docs/api.rst index 55f42d6..c0144de 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -1,7 +1,6 @@ Facemap API Guide =================================== - Pose estimation ~~~~~~~~~~~~~~~~ .. autoclass:: facemap.pose.pose.Pose diff --git a/docs/conf.py b/docs/conf.py index d534de3..66b3e67 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -13,10 +13,14 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) +import os +import sys +sys.path.insert(0, os.path.abspath('.')) +""" +Use the following command to generate documentation + sphinx-build -b html docs docs/build/html +""" # -- Project information ----------------------------------------------------- project = "Facemap" @@ -24,7 +28,7 @@ author = "Carsen Stringer & Atika Syeda" # The full version, including alpha/beta/rc tags -release = "1.0.1" +release = "1.0.2" # -- General configuration --------------------------------------------------- @@ -39,7 +43,7 @@ "sphinx.ext.mathjax", "sphinx.ext.viewcode", "sphinx.ext.napoleon", - "nbsphinx" + "nbsphinx", ] # extensions = ['sphinx.ext.autodoc', # 'sphinx.ext.mathjax', @@ -73,9 +77,10 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -""" + html_theme = 'sphinx_rtd_theme' html_theme_path = ["_themes"] +""" html_theme_options = { 'canonical_url': '', 'analytics_id': 'UA-XXXXXXX-1', # Provided by Google in your dashboard diff --git a/docs/index.rst b/docs/index.rst index 747b5ba..6d312f8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,18 +12,9 @@ For more details, please see our `paper `__ and `refining keypoints model `__. diff --git a/docs/requirements.txt b/docs/requirements.txt index 87d18ff..76c3b52 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,5 @@ ipykernel -nbsphinx \ No newline at end of file +nbsphinx +sphinx==5.3.0 +sphinx_rtd_theme==1.1.1 +readthedocs-sphinx-search==0.1.1 \ No newline at end of file diff --git a/facemap/__main__.py b/facemap/__main__.py index 0d635f4..f2bd05b 100644 --- a/facemap/__main__.py +++ b/facemap/__main__.py @@ -7,9 +7,9 @@ import numpy as np -from facemap import process +from facemap import process, version_str from facemap.gui import gui -from facemap import version_str + def tic(): return time.time() diff --git a/facemap/gui/gui.py b/facemap/gui/gui.py index 96a5ea6..fc95e62 100644 --- a/facemap/gui/gui.py +++ b/facemap/gui/gui.py @@ -20,6 +20,7 @@ QCheckBox, QComboBox, QDesktopWidget, + QFileDialog, QGridLayout, QGroupBox, QLabel, @@ -32,7 +33,6 @@ QStatusBar, QToolButton, QWidget, - QFileDialog ) from scipy.stats import skew, zscore @@ -43,8 +43,6 @@ istr = ["pupil", "motSVD", "blink", "running", "movSVD"] -# TODO: Add pose instructions for CLI commands - class MainW(QtWidgets.QMainWindow): def __init__( @@ -75,7 +73,7 @@ def __init__( QtCore.QCoreApplication.setApplicationName("Facemap") pg.setConfigOptions(imageAxisOrder="row-major") - self.setGeometry(55, 5, 1470, 800) + self.setGeometry(15, 5, 1470, 800)#(55, 5, 1470, 800) self.setWindowTitle("Facemap") self.setStyleSheet("QMainWindow {background: 'black';}") self.styleUnpressed = ( @@ -114,7 +112,6 @@ def __init__( menus.mainmenu(self) self.online_mode = False - # menus.onlinemenu(self) self.central_widget = QWidget(self) self.setCentralWidget(self.central_widget) @@ -125,19 +122,19 @@ def __init__( self.resize(self.sizeObject.width(), self.sizeObject.height()) self.video_window = pg.GraphicsLayoutWidget() - self.video_window.move(self.sizeObject.height(), self.sizeObject.width()) - self.video_window.resize(self.sizeObject.height(), self.sizeObject.width()) - self.scene_grid_layout.addWidget(self.video_window, 1, 2, 10, 7) + #self.video_window.move(self.sizeObject.height(), self.sizeObject.width()) + #self.video_window.resize(self.sizeObject.height(), self.sizeObject.width()) + self.scene_grid_layout.addWidget(self.video_window, 0, 2, 6, 5) # Create a window for embedding and ROI plot self.roi_embed_window = pg.GraphicsLayoutWidget() - self.roi_embed_window.move(self.sizeObject.height(), 0) - self.roi_embed_window.resize(self.sizeObject.height(), self.sizeObject.width()) - self.scene_grid_layout.addWidget(self.roi_embed_window, 14, 2, 7, 7) + #self.roi_embed_window.move(self.sizeObject.height(), 0) + #self.roi_embed_window.resize(self.sizeObject.height(), self.sizeObject.width()) + self.scene_grid_layout.addWidget(self.roi_embed_window, 6, 2, 3, 5) # Create a window for plots self.plots_window = pg.GraphicsLayoutWidget() - self.scene_grid_layout.addWidget(self.plots_window, 0, 9, 24, 8) + self.scene_grid_layout.addWidget(self.plots_window, 0, 7, 9, 8) # Add logo # icon_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "mouse.png") @@ -186,12 +183,10 @@ def __init__( qlabel = QLabel("Saturation:") qlabel.setStyleSheet("color: white;") - self.saturation_groupbox.layout().addWidget(qlabel, 0, 0, 1, 1) + self.saturation_groupbox.layout().addWidget(qlabel, 0, 0) video_saturation_slider = guiparts.Slider(0, self) self.saturation_sliders.append(video_saturation_slider) - self.saturation_groupbox.layout().addWidget( - self.saturation_sliders[0], 0, 1, 1, 4 - ) + self.saturation_groupbox.layout().addWidget(self.saturation_sliders[0], 0, 1) # Add label to indicate saturation level self.saturation_level_label = QLabel(str(self.saturation_sliders[0].value())) @@ -204,7 +199,8 @@ def __init__( self.reflector.clicked.connect(self.add_reflectROI) self.rROI = [] self.reflectors = [] - self.scene_grid_layout.addWidget(self.reflector, 0, 6, 1, 1) + self.saturation_groupbox.layout().addWidget(self.reflector, 0, 2) + #self.scene_grid_layout.addWidget(self.reflector, 0, 6, 1, 1) # roi Saturation groupbox self.roi_saturation_groupbox = QGroupBox() @@ -229,6 +225,7 @@ def __init__( self.saturation_sliders[1].valueChanged.connect(self.set_ROI_saturation_label) # Plots + # Add first plot self.keypoints_traces_plot = self.plots_window.addPlot( name="keypoints_traces_plot", row=0, col=1, title="Keypoints traces" ) @@ -254,6 +251,7 @@ def __init__( ["paw"], ] + # Add second plot self.svd_traces_plot = self.plots_window.addPlot( name="svd_traces_plot", row=1, col=1, title="SVD traces" ) @@ -393,8 +391,8 @@ def __init__( if savedir is not None: self.save_path = savedir self.output_folder_set = True - if len(savedir) > 20: - self.savelabel.setText("..." + savedir[-20:]) + if len(savedir) > 15: + self.savelabel.setText("..." + savedir[-15:]) else: self.savelabel.setText(savedir) if keypoints_file is not None: @@ -420,14 +418,14 @@ def make_buttons(self): # ~~~~~~~~~~~~~~~~~~~~~~~~ SVD variables ~~~~~~~~~~~~~~~~~~~~~~~~ self.svd_groupbox = QGroupBox("ROI settings:") self.svd_groupbox.setStyleSheet( - "QGroupBox { border: 1px solid white; border-style: outset; border-radius: 5px; color:white; padding: 20px 0px;}" - ) # padding: 5px -10px; + "QGroupBox { border: 1px solid white; border-style: outset; border-radius: 5px; color:white; padding: 15px 0px;}" + ) self.svd_groupbox.setLayout(QGridLayout()) # Create ROI features self.comboBox = QComboBox(self) # Set size of combobox - self.comboBox.setFixedWidth(int(0.05 * self.sizeObject.width())) + #self.comboBox.setFixedWidth(int(0.05 * self.sizeObject.width())) self.comboBox.addItem("Select ROI") self.comboBox.addItem("Pupil") self.comboBox.addItem("motion SVD") @@ -439,7 +437,7 @@ def make_buttons(self): self.svd_groupbox.layout().addWidget(self.comboBox, 0, 0) # self.comboBox.currentIndexChanged.connect(self.mode_change) self.addROI = QPushButton("Add ROI") - self.addROI.setFixedWidth(int(0.04 * self.sizeObject.width())) + #self.addROI.setFixedWidth(int(0.04 * self.sizeObject.width())) self.addROI.setFont(QFont("Arial", 10, QFont.Bold)) self.addROI.clicked.connect(lambda clicked: self.add_ROI()) self.addROI.setEnabled(False) @@ -453,7 +451,7 @@ def make_buttons(self): self.svdbin_spinbox = QSpinBox() self.svdbin_spinbox.setRange(1, 20) self.svdbin_spinbox.setValue(self.ops["sbin"]) - self.svdbin_spinbox.setFixedWidth(int(0.03 * self.sizeObject.width())) + self.svdbin_spinbox.setFixedWidth(int(0.05 * self.sizeObject.width())) self.svd_groupbox.layout().addWidget( self.svdbin_spinbox, 1, 1, alignment=QtCore.Qt.AlignRight ) @@ -464,7 +462,7 @@ def make_buttons(self): ) self.sigma_box = QLineEdit() self.sigma_box.setText(str(self.ops["pupil_sigma"])) - self.sigma_box.setFixedWidth(int(0.02 * self.sizeObject.width())) + self.sigma_box.setFixedWidth(int(0.05 * self.sizeObject.width())) self.pupil_sigma = float(self.sigma_box.text()) self.sigma_box.returnPressed.connect(self.pupil_sigma_change) self.svd_groupbox.layout().addWidget( @@ -474,7 +472,7 @@ def make_buttons(self): # ~~~~~~~~~~~~~~~~~~~~~~~~ Pose/keypoints variables ~~~~~~~~~~~~~~~~~~~~~~~~ self.pose_groupbox = QGroupBox("Pose settings:") self.pose_groupbox.setStyleSheet( - "QGroupBox { border: 1px solid white; border-style: outset; border-radius: 10px; color:white; padding: 25px 5px;}" + "QGroupBox { border: 1px solid white; border-style: outset; border-radius: 5px; color:white; padding: 5px 0px;}" ) self.pose_groupbox.setLayout(QGridLayout()) @@ -485,7 +483,7 @@ def make_buttons(self): self.pose_model_combobox = QComboBox(self) # Set size of combobox - self.pose_model_combobox.setFixedWidth(int(0.085 * self.sizeObject.width())) + #self.pose_model_combobox.setFixedWidth(int(0.085 * self.sizeObject.width())) # make combobox scrollable self.pose_model_combobox.view().setVerticalScrollBarPolicy( QtCore.Qt.ScrollBarAlwaysOn @@ -494,7 +492,7 @@ def make_buttons(self): self.pose_model_combobox.setStyleSheet("QComboBox { combobox-popup: 0; }") self.update_pose_model_combo_box() self.pose_groupbox.layout().addWidget( - self.pose_model_combobox, 0, 1, alignment=QtCore.Qt.AlignLeft + self.pose_model_combobox, 0, 1 ) # Add a QLabel and spinbox for selecting batch size @@ -504,7 +502,7 @@ def make_buttons(self): self.batch_size_spinbox = QSpinBox() self.batch_size_spinbox.setRange(1, 5000) self.batch_size_spinbox.setValue(4) - self.batch_size_spinbox.setFixedWidth(int(0.04 * self.sizeObject.width())) + #self.batch_size_spinbox.setFixedWidth(int(0.04 * self.sizeObject.width())) self.pose_groupbox.layout().addWidget( self.batch_size_spinbox, 1, 1, alignment=QtCore.Qt.AlignRight ) @@ -517,9 +515,9 @@ def make_buttons(self): self.keypoints_threshold_spinbox.setRange(0, 100) self.keypoints_threshold_spinbox.setValue(0) self.keypoints_threshold = self.keypoints_threshold_spinbox.value() - self.keypoints_threshold_spinbox.setFixedWidth( - int(0.04 * self.sizeObject.width()) - ) + #self.keypoints_threshold_spinbox.setFixedWidth( + # int(0.04 * self.sizeObject.width()) + #) self.keypoints_threshold_spinbox.valueChanged.connect( self.update_keypoints_threshold ) @@ -530,7 +528,7 @@ def make_buttons(self): # ~~~~~~~~~~~~~~~~~~~~~~~~ Process features ~~~~~~~~~~~~~~~~~~~~~~~~ self.process_groupbox = QGroupBox("Process settings:") self.process_groupbox.setStyleSheet( - "QGroupBox { border: 1px solid white; border-style: outset; border-radius: 10px; color:white; padding: 25px 5px;}" + "QGroupBox { border: 1px solid white; border-style: outset; border-radius: 5px; color:white; padding: 7px 0px;}" ) self.process_groupbox.setLayout(QGridLayout()) @@ -565,9 +563,9 @@ def make_buttons(self): self.process_groupbox.layout().addWidget(self.gpu_checkbox, 1, 2) # ~~~~~~~~~~~~~~~~~~~~~~~~ Process buttons ~~~~~~~~~~~~~~~~~~~~~~~~ - self.process_buttons_groupbox = QGroupBox("Process buttons:") + self.process_buttons_groupbox = QGroupBox() self.process_buttons_groupbox.setStyleSheet( - "QGroupBox { border: 0px solid white; border-style: outset;}" + "QGroupBox { border: 0px solid white; border-style: outset; border-radius: 0px; color:white; padding: 0px 0px;}" ) self.process_buttons_groupbox.setLayout(QGridLayout()) @@ -592,7 +590,7 @@ def make_buttons(self): # ~~~~~~~~~~~~~~~~~~~~~~~~ Labels ~~~~~~~~~~~~~~~~~~~~~~~~ self.labels_groupbox = QGroupBox() self.labels_groupbox.setStyleSheet( - "QGroupBox { border: 0px solid white; border-style: outset;}" + "QGroupBox { border: 0px solid white; border-style: outset; border-radius: 0px; color:white; padding: 0px 0px;}" ) self.labels_groupbox.setLayout(QGridLayout()) @@ -605,14 +603,21 @@ def make_buttons(self): self.labels_groupbox.layout().addWidget(self.savelabel, 0, 1) self.batchlist = [] + # Add show batch button to labels groupbox + self.show_batch_button = QPushButton("Show batch") + self.show_batch_button.setFont(QFont("Arial", 10, QFont.Bold)) + self.show_batch_button.clicked.connect(self.show_batch) + self.labels_groupbox.layout().addWidget(self.show_batch_button, 0, 2) + """ self.batchname = [] - # TODO: Change batchname to span 2 columns - for k in range(5): + # TODO: Change batchname to be displayed in a pop-up message box that lists the filenames!! + for k in range(1): self.batchname.append(QLabel("")) self.batchname[-1].setStyleSheet("color: white;") self.batchname[-1].setAlignment(QtCore.Qt.AlignCenter) self.labels_groupbox.layout().addWidget(self.batchname[-1], k + 1, 0, 1, 2) # self.scene_grid_layout.addWidget(self.batchname[-1], 6 + k, 0, 1, 4) + """ # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Video playback options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ self.current_frame_lineedit = QLineEdit() @@ -631,61 +636,14 @@ def make_buttons(self): self.frame_slider.setTracking(False) self.frame_slider.valueChanged.connect(self.go_to_frame) self.frameDelta = 10 - istretch = 15 - iplay = istretch + 10 iconSize = QtCore.QSize(20, 20) - # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Video playback options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - self.video_playback_groupbox = QGroupBox() - self.video_playback_groupbox.setStyleSheet( - "QGroupBox { border: 0px solid white; border-style: outset;}" - ) - self.video_playback_groupbox.setLayout(QGridLayout()) - - iconSize = QtCore.QSize(30, 30) - self.playButton = QToolButton() - self.playButton.setIcon( - self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay) - ) - self.playButton.setIconSize(iconSize) - self.playButton.setToolTip("Play") - self.playButton.setCheckable(True) - self.playButton.clicked.connect(self.start) - self.playButton.setFixedSize( - QtCore.QSize( - np.floor(self.sizeObject.width() * 0.025).astype(int), - np.floor(self.sizeObject.width() * 0.025).astype(int), - ) - ) - self.video_playback_groupbox.layout().addWidget(self.playButton, 0, 0) - - self.pauseButton = QToolButton() - self.pauseButton.setCheckable(True) - self.pauseButton.setIcon( - self.style().standardIcon(QtWidgets.QStyle.SP_MediaPause) - ) - self.pauseButton.setIconSize(iconSize) - self.pauseButton.setToolTip("Pause") - self.pauseButton.clicked.connect(self.pause) - self.pauseButton.setFixedSize( - QtCore.QSize( - np.floor(self.sizeObject.width() * 0.025).astype(int), - np.floor(self.sizeObject.width() * 0.025).astype(int), - ) - ) - self.video_playback_groupbox.layout().addWidget(self.pauseButton, 0, 1) - - btns = QButtonGroup(self) - btns.addButton(self.playButton, 0) - btns.addButton(self.pauseButton, 1) - btns.setExclusive(True) - # Add clustering analysis/visualization features self.roi_embed_combobox = QComboBox(self) self.roi_embed_combobox.addItem("--Select display--") self.roi_embed_combobox.addItem("ROI") - self.roi_embed_combobox.addItem("UMAP") - self.roi_embed_combobox.addItem("tSNE") + #self.roi_embed_combobox.addItem("UMAP") + #self.roi_embed_combobox.addItem("tSNE") self.roi_embed_combobox.currentIndexChanged.connect( self.vis_combobox_selection_changed ) @@ -727,52 +685,43 @@ def make_buttons(self): # ~~~~~~~~~~ motsvd/movsvd options ~~~~~~~~~~ self.scene_grid_layout.addWidget(self.svd_groupbox, 0, 0, 1, 2) # ~~~~~~~~~~ Pose features ~~~~~~~~~~ - self.scene_grid_layout.addWidget(self.pose_groupbox, 1, 0, 3, 2) + self.scene_grid_layout.addWidget(self.pose_groupbox, 1, 0, 2, 2) # ~~~~~~~~~~ Process features ~~~~~~~~~~ - self.scene_grid_layout.addWidget(self.process_groupbox, 5, 0, 1, 2) + self.scene_grid_layout.addWidget(self.process_groupbox, 3, 0, 1, 2) # ~~~~~~~~~~ Process buttons features ~~~~~~~~~~ - self.scene_grid_layout.addWidget(self.process_buttons_groupbox, 6, 0, 1, 2) + self.scene_grid_layout.addWidget(self.process_buttons_groupbox, 4, 0, 1, 2) # ~~~~~~~~~~ Save/file IO ~~~~~~~~~~ - self.scene_grid_layout.addWidget(self.labels_groupbox, 7, 0, 1, 2) - # ~~~~~~~~~~ Saturation ~~~~~~~~~~ - self.scene_grid_layout.addWidget(self.saturation_groupbox, 0, 2, 1, 3) - # ~~~~~~~~~~ embedding & ROI visualization window features - self.scene_grid_layout.addWidget(self.roi_saturation_groupbox, 11, 2, 1, 3) - self.scene_grid_layout.addWidget(self.roi_embed_combobox, 11, 6, 1, 1) - self.scene_grid_layout.addWidget(self.zoom_in_button, 11, 7, 1, 1) - self.scene_grid_layout.addWidget(self.zoom_out_button, 11, 8, 1, 1) - self.scene_grid_layout.addWidget(self.roi_display_combobox, 11, 7, 1, 1) - self.scene_grid_layout.addWidget(self.save_clustering_button, 12, 7, 1, 1) - # ~~~~~~~~~~ Video playback ~~~~~~~~~~ - self.scene_grid_layout.addWidget(self.video_playback_groupbox, iplay, 0, 1, 1) - self.playButton.setEnabled(False) - self.pauseButton.setEnabled(False) - self.pauseButton.setChecked(True) - self.scene_grid_layout.addWidget(QLabel(""), istretch, 0, 1, 3) - self.scene_grid_layout.setRowStretch(istretch, 1) - self.scene_grid_layout.addWidget( - self.current_frame_lineedit, istretch + 9, 0, 1, 1 + self.scene_grid_layout.addWidget(self.labels_groupbox, 5, 0, 1, 2) + + # ~~~~~~~~~~ Plot 1 and 2 features ~~~~~~~~~~ + self.keypoints_chckbox_groupbox = QGroupBox("Keypoints traces") + self.keypoints_chckbox_groupbox.setStyleSheet( + "QGroupBox { border: 1px solid white; border-style: outset; border-radius: 5px; color:white; padding: 10px 15px;}" ) - self.scene_grid_layout.addWidget(self.total_frames_label, istretch + 9, 1, 1, 1) - self.scene_grid_layout.addWidget(self.frame_slider, istretch + 10, 1, 1, 16) + self.keypoints_chckbox_groupbox.setLayout(QGridLayout()) - # Plot 1 and 2 features - plot_label = QLabel("Keypoints traces") - plot_label.setStyleSheet("color: gray;") - self.scene_grid_layout.addWidget(plot_label, istretch, 0, 1, 1) + self.svd_chckbox_groupbox = QGroupBox("SVD traces") + self.svd_chckbox_groupbox.setStyleSheet( + "QGroupBox { border: 1px solid white; border-style: outset; border-radius: 5px; color:white; padding: 10px 15px;}" + ) + self.svd_chckbox_groupbox.setLayout(QGridLayout()) + """ + #plot_label = QLabel("Keypoints traces") + #plot_label.setStyleSheet("color: gray;") + #self.scene_grid_layout.addWidget(plot_label, istretch, 0, 1, 1) plot_label = QLabel("SVD traces") plot_label.setStyleSheet("color: gray;") - self.scene_grid_layout.addWidget(plot_label, istretch, 1, 1, 1) - self.load_trace2_button = QPushButton("Load 1D data") - self.load_trace2_button.setFont(QFont("Arial", 12)) - self.load_trace2_button.clicked.connect( - lambda: self.load_1dtrace_button_clicked(2) - ) - self.load_trace2_button.setEnabled(False) - self.load_trace2_button.setFixedWidth(int(0.07 * self.sizeObject.width())) + self.scene_grid_layout.addWidget(plot_label, istretch, 1, 1, 1)""" + #self.load_trace2_button = QPushButton("Load 1D data") + #self.load_trace2_button.setFont(QFont("Arial", 12)) + #self.load_trace2_button.clicked.connect( + # lambda: self.load_1dtrace_button_clicked(2) + #) + #self.load_trace2_button.setEnabled(False) + #self.load_trace2_button.setFixedWidth(int(0.07 * self.sizeObject.width())) self.trace2_data_loaded = None self.trace2_legend = pg.LegendItem(labelTextSize="12pt", horSpacing=30) - self.scene_grid_layout.addWidget(self.load_trace2_button, istretch + 1, 1, 1, 1) + #self.scene_grid_layout.addWidget(self.load_trace2_button, istretch + 1, 1, 1, 1) self.plot1_checkboxes = [] self.plot2_checkboxes = [] self.lbls = [] @@ -784,22 +733,111 @@ def make_buttons(self): self.plot1_checkboxes[-1].toggled.connect( self.keypoint_subgroup_checkbox_toggled ) - self.scene_grid_layout.addWidget( - self.plot1_checkboxes[-1], istretch + 1 + i, 0, 1, 1 - ) + self.keypoints_chckbox_groupbox.layout().addWidget(self.plot1_checkboxes[-1], i, 0) + #self.scene_grid_layout.addWidget( + # self.plot1_checkboxes[-1], istretch + 1 + i, 0, 1, 1 + #) + self.scene_grid_layout.addWidget(self.keypoints_chckbox_groupbox, 6, 0, 3, 1) # Set plot 2 checkboxes - for k in range(7): + for k in range(5): self.plot2_checkboxes.append(QCheckBox("")) - self.scene_grid_layout.addWidget( - self.plot2_checkboxes[-1], istretch + 2 + k, 1, 1, 1 - ) + self.svd_chckbox_groupbox.layout().addWidget(self.plot2_checkboxes[-1], k, 1) + #self.scene_grid_layout.addWidget( + # self.plot2_checkboxes[-1], istretch + 1 + k, 1, 1, 1 + #) self.plot2_checkboxes[-1].toggled.connect(self.plot_processed) self.plot2_checkboxes[-1].setEnabled(False) self.plot2_checkboxes[k].setStyleSheet("color: gray;") self.lbls.append(QLabel("")) self.lbls[-1].setStyleSheet("color: white;") + self.scene_grid_layout.addWidget(self.svd_chckbox_groupbox, 6, 1, 3, 1) + + # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Video playback options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + self.video_playback_groupbox = QGroupBox() + self.video_playback_groupbox.setStyleSheet( + "QGroupBox { border: 0px solid white; border-style: outset;}" + ) + self.video_playback_groupbox.setLayout(QGridLayout()) + + iconSize = QtCore.QSize(30, 30) + self.playButton = QToolButton() + self.playButton.setIcon( + self.style().standardIcon(QtWidgets.QStyle.SP_MediaPlay) + ) + self.playButton.setIconSize(iconSize) + self.playButton.setToolTip("Play") + self.playButton.setCheckable(True) + self.playButton.clicked.connect(self.start) + self.playButton.setFixedSize( + QtCore.QSize( + np.floor(self.sizeObject.width() * 0.025).astype(int), + np.floor(self.sizeObject.width() * 0.025).astype(int), + ) + ) + self.video_playback_groupbox.layout().addWidget(self.playButton, 0, 0) + + self.pauseButton = QToolButton() + self.pauseButton.setCheckable(True) + self.pauseButton.setIcon( + self.style().standardIcon(QtWidgets.QStyle.SP_MediaPause) + ) + self.pauseButton.setIconSize(iconSize) + self.pauseButton.setToolTip("Pause") + self.pauseButton.clicked.connect(self.pause) + self.pauseButton.setFixedSize( + QtCore.QSize( + np.floor(self.sizeObject.width() * 0.025).astype(int), + np.floor(self.sizeObject.width() * 0.025).astype(int), + ) + ) + self.video_playback_groupbox.layout().addWidget(self.pauseButton, 0, 1) + + btns = QButtonGroup(self) + btns.addButton(self.playButton, 0) + btns.addButton(self.pauseButton, 1) + btns.setExclusive(True) + + self.playButton.setEnabled(False) + self.pauseButton.setEnabled(False) + self.pauseButton.setChecked(True) + + # Frame number labels + self.video_playback_groupbox.layout().addWidget(self.current_frame_lineedit, 0, 2) + self.video_playback_groupbox.layout().addWidget(self.total_frames_label, 0, 3) + self.scene_grid_layout.addWidget(self.video_playback_groupbox, 9, 0, 1, 2) + """ + self.scene_grid_layout.addWidget(QLabel(""), istretch, 0, 1, 3) + self.scene_grid_layout.setRowStretch(istretch, 1) + self.scene_grid_layout.addWidget( + self.current_frame_lineedit, istretch + 6, 0, 1, 1 + ) + self.scene_grid_layout.addWidget(self.total_frames_label, istretch + 6, 1, 1, 1) + """ + self.scene_grid_layout.addWidget(self.frame_slider, 9, 2, 1, 13) + # ~~~~~~~~~~ Saturation ~~~~~~~~~~ + self.scene_grid_layout.addWidget(self.saturation_groupbox, 0, 2, 1, 4) + # ~~~~~~~~~~ embedding & ROI visualization window features + self.scene_grid_layout.addWidget(self.roi_saturation_groupbox, 5, 2, 1, 3) + self.scene_grid_layout.addWidget(self.roi_embed_combobox, 5, 5, 1, 1) + #self.scene_grid_layout.addWidget(self.zoom_in_button, 4, 7, 1, 1) + #self.scene_grid_layout.addWidget(self.zoom_out_button, 4, 8, 1, 1) + self.scene_grid_layout.addWidget(self.roi_display_combobox, 5, 6, 1, 1) + #self.scene_grid_layout.addWidget(self.save_clustering_button, 5, 7, 1, 1) + + video_path_label = QLabel("Save path:") + video_path_label.setStyleSheet("color: gray;") + self.labels_groupbox.layout().addWidget(video_path_label, 0, 0) + self.update_frame_slider() + def show_batch(self): + if not self.batchlist: + QMessageBox.information(self, "Batch List", "No filenames in batchlist.") + else: + message = "List of filenames in batchlist:\n\n" + message += "\n".join(self.batchlist) # Items will be separated by new lines + QMessageBox.information(self, "Batch List", message) + def add_pose_model(self): # Open a file dialog to browse and select a pose model pose_model_path = QFileDialog.getOpenFileName( @@ -1177,7 +1215,7 @@ def update_buttons(self): self.saverois.setEnabled(True) self.multivideo_svd_checkbox.setChecked(True) self.save_mat.setChecked(True) - self.load_trace2_button.setEnabled(True) + #self.load_trace2_button.setEnabled(True) # Enable pose features for single video only self.keypoints_checkbox.setEnabled(True) @@ -2290,6 +2328,10 @@ def load_neural_data(self): neural_data_cancel_button = QtWidgets.QPushButton("Cancel") neural_data_cancel_button.clicked.connect(dialog.reject) neural_data_buttons_hbox.addWidget(neural_data_cancel_button) + # Add a help button + neural_data_help_button = QtWidgets.QPushButton("Help") + neural_data_help_button.clicked.connect(lambda clicked: self.load_neural_data_help_clicked(clicked, dialog)) + neural_data_buttons_hbox.addWidget(neural_data_help_button) # Add a done button neural_data_done_button = QtWidgets.QPushButton("Done") neural_data_done_button.clicked.connect( @@ -2407,7 +2449,7 @@ def show_run_neural_predictions_dialog(self): "QGroupBox {border: 1px solid gray; border-radius: 9px; margin-top: 1em;} " ) dialog.neural_model_hyperparameters_groupbox.setTitle( - "Keypoints network hyperparameters" + "Training hyperparameters" ) learning_rate_label = QtWidgets.QLabel("Learning rate:") dialog.learning_rate_line_edit = QtWidgets.QLineEdit() @@ -2585,6 +2627,9 @@ def update_hyperparameter_box(self, dialog): dialog.neural_model_hyperparameters_groupbox.hide() dialog.linear_regression_hyperparameters_groupbox.show() + def load_neural_data_help_clicked(self, clicked, dialog): + help_windows.LoadNeuralDataHelp(parent=dialog, window_size=self.sizeObject) + def neural_data_help_button_clicked(self, clicked, dialog): help_windows.NeuralModelTrainingWindow( parent=dialog, window_size=self.sizeObject diff --git a/facemap/gui/help_windows.py b/facemap/gui/help_windows.py index 332b5d2..46cecaf 100644 --- a/facemap/gui/help_windows.py +++ b/facemap/gui/help_windows.py @@ -2,6 +2,7 @@ Copright © 2023 Howard Hughes Medical Institute, Authored by Carsen Stringer and Atika Syeda. """ import os +import typing import numpy as np from PyQt5 import QtCore @@ -17,9 +18,11 @@ QPushButton, QScrollArea, QSizePolicy, + QTextEdit, QVBoxLayout, QWidget, ) + from ..version import version_str @@ -59,7 +62,7 @@ def __init__(self, parent=None, window_size=None): layout.addWidget(main_text, stretch=1) # Pose tracking section - pose_tracking_group = QGroupBox("Pose tracking", self) + pose_tracking_group = QGroupBox() pose_tracking_group.setStyleSheet( "QGroupBox {background: 'black'; border: 0px ;}" ) @@ -85,7 +88,7 @@ def __init__(self, parent=None, window_size=None): layout.addWidget(pose_tracking_group, stretch=1) # SVD section - svd_group = QGroupBox("SVD", self) + svd_group = QGroupBox() svd_group.setStyleSheet("QGroupBox {background: 'black'; border: 0px ;}") svd_group.setLayout(QVBoxLayout()) svd_text = """ @@ -112,13 +115,54 @@ def __init__(self, parent=None, window_size=None): self.show() - # TODO - Add instructions for filetypes accepted for different load buttons - # TODO - Add instructions for loading neural data +class LoadNeuralDataHelp(QDialog): + def __init__(self, window_size, parent=None): + super(LoadNeuralDataHelp, self).__init__(parent) + self.setWindowTitle("Help") + width, height = int(window_size.width() * 0.28), int( + window_size.height() * 0.25 + ) + self.resize(width, height) + self.win = QWidget(self) + layout = QVBoxLayout() + layout.setAlignment(QtCore.Qt.AlignCenter) + self.win.setLayout(layout) + + self.scrollArea = QScrollArea(self) + self.scrollArea.setFixedHeight(height) + self.scrollArea.setFixedWidth(width) + self.scrollArea.setWidgetResizable(True) + self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) + self.scrollArea.setWidget(self.win) + + text = """ +
    +
  1. Load neural data file (*.npy) containing an array of shape neurons x time.
  2. +
  3. Select whether to view neural data as heatmap or traces (for small number of neurons).
  4. +
  5. (Optional) Load neural timestamps file (*.npy) containing a 1D array.
  6. +
  7. (Optional) Load behavioral timestamps file (*.npy) containing a 1D array.
  8. +
  9. Note: the timestamps file are used for resampling behavioral data to neural timescale.
  10. +
+ """ + + label = QLabel(text, self) + label.setStyleSheet( + "font-size: 12pt; font-family: Arial; text-align: center; " + ) + label.setWordWrap(True) + layout.addWidget(label, stretch=1) + + # Add a ok button to close the window + self.ok_button = QPushButton("Ok") + self.ok_button.clicked.connect(self.close) + layout.addWidget(self.ok_button, alignment=QtCore.Qt.AlignCenter) + + self.show() class AboutWindow(QDialog): - def __init__(self, parent=None, window_size=None): + def __init__(self, parent, window_size): super(AboutWindow, self).__init__(parent) width, height = int(window_size.width() * 0.28), int( window_size.height() * 0.42 @@ -155,7 +199,7 @@ def __init__(self, parent=None, window_size=None): text = """

- Pose tracking of mouse face from different camera views (python only) and svd processing of videos (python and MATLAB). + Framework for predicting neural activity from mouse orofacial movements tracked using a pose estimation model. Package also includes singular value decomposition (SVD) of behavioral videos.

Authors: Carsen Stringer & Atika Syeda @@ -169,15 +213,15 @@ def __init__(self, parent=None, window_size=None): Version: {version}

- Visit our github page for more information. + Visit our GitHub page for more information.

""".format(version=version_str) - text = QLabel(text, self) - text.setStyleSheet( - "font-size: 12pt; font-family: Arial; color: white; text-align: center; " + text = QTextEdit(text, self) + text.setStyleSheet( + "font-size: 12pt; color: white; background-color: #000000;" ) - text.setWordWrap(True) - text.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + text.setReadOnly(True) + text.setFixedSize(width*0.98, height) layout.addWidget(text, stretch=1) self.show() @@ -187,7 +231,7 @@ class PoseRefinementStep2HelpWindow(QDialog): def __init__(self, parent=None, window_size=None): super(PoseRefinementStep2HelpWindow, self).__init__(parent) width, height = int(window_size.width() * 0.3), int(window_size.height() * 0.3) - self.resize(width, height) + self.resize(width * 0.95, height * 0.75) self.setWindowTitle("Help") self.win = QWidget(self) layout = QVBoxLayout() @@ -196,16 +240,19 @@ def __init__(self, parent=None, window_size=None): text = """
    -
  1. Select the initial/base model to use for further finetuning.
  2. -
  3. Set the name of output model after refinement.
  4. -
  5. (If applicable) Select data files containing refined keypoints from previous training to include during current model training.
  6. -
  7. Select 'Refine current video' to refine predicted keypoints for a subset of frames from the current video after selecting the number of frames. Note: the suggested number of frames for each new animal are 20-25.
  8. -
  9. Select '+' to set the hyperparameters for training.
  10. +
  11. Select base model for finetuning.
  12. +
  13. Set name of finetuned model.
  14. +
  15. Select whether to 'Refine current video' and the number of frames to refine from current video.
  16. +
  17. Set proportion of random frames to include during training. Rest of the frames are selected based on keypoint values that lie above likelihood threshold.
  18. +
  19. (Optional) Select whether to use refined keypoints from previous training.
  20. +
  21. Select '+' to change hyperparameter settings for training.
""" - label = QLabel(text) - label.setStyleSheet("font-size: 12pt; font-family: Arial; color: white;") - label.setWordWrap(True) + label = QTextEdit(text) #QLabel(text) + label.setReadOnly(True) # Make the text area read-only + label.setStyleSheet("font-size: 12pt; color: white; background-color: black;") + label.setHtml(text) + label.setFixedSize(width * 0.9, height * 0.6) layout.addWidget(label, alignment=QtCore.Qt.AlignCenter) # Add a ok button to close the window @@ -246,83 +293,84 @@ def __init__(self, parent=None, window_size=None):

Refinement keys

-

Labelling instructions

+

Guidelines for Labeling

- Keypoints for different facial regions are labelled as shown above in the side view and top view. Detailed instructions for each region are given below for different views: + Keypoints representing various facial regions are annotated according to the illustrations provided for the side view and top view. For each viewpoint, detailed instructions for labeling different regions are outlined below:

Eye

Nose

Whiskers

- To label whiskers, find a set of 3 whiskers in the triangular configuration as shown above. The easiest way to do this is to identify most prominent whiskers that are easily identifiable across frames. Whiskers are labeled in clockwise order (C1->D1-C3) when viewed from the right side and in counterclockwise order (C1->D1-C3) when viewed from the top/left view. + For whisker labeling, identify a set of three whiskers forming a triangular pattern as shown. Look for prominent whiskers consistently recognizable across frames. Label whiskers in a clockwise order (I->II-III) when viewed from the right side, or counterclockwise order (I->II-III) when viewed from the top/left view.

Paw

Mouth

+ Please follow these instructions to accurately label the keypoints for each facial region in the provided illustrations. """ - label = QLabel(text) + label = QTextEdit(text) label.setStyleSheet( "font-size: 12pt; font-family: Arial; color: white; text-align: center; padding: 15;" ) - label.setWordWrap(True) - label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) + label.setReadOnly(True) + label.setFixedSize(width * 0.9, height * 0.6) layout.addWidget(label, alignment=QtCore.Qt.AlignLeft, stretch=1) # Add ok button to close the window @@ -336,13 +384,10 @@ def __init__(self, parent=None, window_size=None): self.show() -# TODO: Update help button with correct instructions about keypoints labels (specially whiskers) - - class NeuralModelTrainingWindow(QDialog): def __init__(self, parent=None, window_size=None): super(NeuralModelTrainingWindow, self).__init__(parent) - width, height = int(window_size.width() * 0.4), int(window_size.height() * 0.45) + width, height = int(window_size.width() * 0.3), int(window_size.height() * 0.45) self.resize(width, height) self.setWindowTitle("Help - Neural Model Training") @@ -356,32 +401,36 @@ def __init__(self, parent=None, window_size=None): self.scrollArea.setFixedWidth(width) self.scrollArea.setWidgetResizable(True) self.scrollArea.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOn) - self.scrollArea.setStyleSheet("background: 'black'; ") self.scrollArea.setWidget(self.win) # Add a list of hyperparameters and their descriptions with recommended values main_text = """ -

Training instructions

+

Instructions for Training

-

+
    +
  1. Choose the input data to be used for neural activity prediction.
  2. +
  3. Select the output of the neural activity prediction model (neural principal components or neurons).
  4. +
  5. Configure the hyperparameters for training the model.
  6. +
  7. Indicate whether to save the output of the neural activity prediction model as yes/no.
  8. +

-

Hyperparameters

+

Output of the Model

""" main_text = QLabel(main_text, self) main_text.setStyleSheet( - "font-size: 12pt; font-family: Arial; color: white; text-align: center; " + "font-size: 12pt; font-family: Arial; text-align: center; " ) main_text.setWordWrap(True) layout.addWidget(main_text, stretch=1) @@ -390,7 +439,7 @@ def __init__(self, parent=None, window_size=None): self.ok_button = QPushButton("Ok") self.ok_button.clicked.connect(self.close) self.ok_button.setStyleSheet( - "background: 'black'; color: 'white'; font-size: 12pt; font-family: Arial; " + "font-size: 12pt; font-family: Arial; " ) layout.addWidget(self.ok_button, alignment=QtCore.Qt.AlignCenter) diff --git a/facemap/gui/io.py b/facemap/gui/io.py index 7243fb1..286d79a 100644 --- a/facemap/gui/io.py +++ b/facemap/gui/io.py @@ -234,7 +234,7 @@ def open_proc(parent, file_name=None): int(parent.saturation[parent.iROI] * 100 / 255) ) parent.ROIs[parent.iROI].plot(parent) - if parent.processed and k <= 7: + if parent.processed and k <= 5: parent.plot2_checkboxes[k].setText( "%s%d" % (parent.typestr[r["rind"]], kt[r["rind"]]) ) @@ -380,8 +380,8 @@ def save_folder(parent): if folderName: parent.save_path = folderName parent.output_folder_set = True - if len(folderName) > 30: - parent.savelabel.setText("..." + folderName[-30:]) + if len(folderName) > 15: + parent.savelabel.setText("..." + folderName[-15:]) else: parent.savelabel.setText(folderName) diff --git a/facemap/gui/menus.py b/facemap/gui/menus.py index 1544748..a6986b3 100644 --- a/facemap/gui/menus.py +++ b/facemap/gui/menus.py @@ -8,7 +8,6 @@ def mainmenu(parent): # --------------- MENU BAR -------------------------- - # run suite2p from scratch open_file = QAction("Load video", parent) open_file.setShortcut("Ctrl+L") open_file.triggered.connect(lambda: io.open_file(parent)) diff --git a/facemap/gui/ops_user.npy b/facemap/gui/ops_user.npy index dcebfb2..a5f4c34 100755 Binary files a/facemap/gui/ops_user.npy and b/facemap/gui/ops_user.npy differ diff --git a/facemap/mouse.png b/facemap/mouse.png index fcf1e2e..7d8987e 100644 Binary files a/facemap/mouse.png and b/facemap/mouse.png differ diff --git a/facemap/mouse_big.png b/facemap/mouse_big.png new file mode 100644 index 0000000..3dbb287 Binary files /dev/null and b/facemap/mouse_big.png differ diff --git a/facemap/pose/pose_helper_functions.py b/facemap/pose/pose_helper_functions.py index a6995ce..13a7636 100644 --- a/facemap/pose/pose_helper_functions.py +++ b/facemap/pose/pose_helper_functions.py @@ -1,15 +1,15 @@ """ Copright © 2023 Howard Hughes Medical Institute, Authored by Carsen Stringer and Atika Syeda. """ -# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Import packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -import numpy as np - import random from platform import python_version import cv2 # opencv import matplotlib import matplotlib.pyplot as plt + +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Import packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +import numpy as np import pyqtgraph as pg import torch # pytorch from PyQt5 import QtWidgets diff --git a/facemap/pose/refine_pose.py b/facemap/pose/refine_pose.py index 7e657f2..3a4db93 100644 --- a/facemap/pose/refine_pose.py +++ b/facemap/pose/refine_pose.py @@ -305,7 +305,7 @@ def show_choose_training_files(self): self.spinbox_nframes = QSpinBox(self) self.spinbox_nframes.setRange(1, self.gui.cumframes[-1]) - self.spinbox_nframes.setValue(25) + self.spinbox_nframes.setValue(15) self.spinbox_nframes.setStyleSheet("QSpinBox {color: 'black';}") self.get_num_frames_groupbox.layout().addWidget(self.spinbox_nframes) diff --git a/facemap/version.py b/facemap/version.py index f9b23d4..50515d4 100644 --- a/facemap/version.py +++ b/facemap/version.py @@ -2,10 +2,12 @@ Copright © 2023 Howard Hughes Medical Institute, Authored by Carsen Stringer and Atika Syeda. """ +import sys from importlib.metadata import PackageNotFoundError, version -import sys from platform import python_version -import torch, numpy + +import numpy +import torch try: version = version("facemap") diff --git a/paper/fig1.py b/paper/fig1.py index 7710046..2e05ace 100644 --- a/paper/fig1.py +++ b/paper/fig1.py @@ -1,12 +1,14 @@ """ Copright © 2023 Howard Hughes Medical Institute, Authored by Carsen Stringer and Atika Syeda. """ -import matplotlib +import cv2 +import matplotlib import matplotlib.pyplot as plt +from fig_utils import * from scipy.stats import wilcoxon -import cv2 + from facemap import keypoints -from fig_utils import * + def panel_percentile_error(ax, data_path): errors = np.load(f'{data_path}net_results/facemap_benchmark_distances.npy', allow_pickle=True).item()['test_distances'] diff --git a/paper/neuralpred.py b/paper/neuralpred.py index ab8a2de..8a279c3 100644 --- a/paper/neuralpred.py +++ b/paper/neuralpred.py @@ -10,7 +10,7 @@ from scipy.interpolate import interp1d from scipy.linalg import eigh from scipy.ndimage import gaussian_filter1d -from scipy.stats import zscore +from scipy.stats import zscore from sklearn.decomposition import PCA from torch import nn from torch.nn import functional as F diff --git a/paper/suppfigs.py b/paper/suppfigs.py index 0fd1350..e860a3c 100644 --- a/paper/suppfigs.py +++ b/paper/suppfigs.py @@ -10,6 +10,7 @@ from facemap.utils import bin1d + def varexp_ranks(data_path, dbs, evals=None, save_fig=False): colors = [[0.5, 0.5, 0.5], [0.75, 0.75, 0.25]] lbls = ["keypoints", "movie PCs"]