Compare commits
2 Commits
87073fb218
...
3e0f70ea49
| Author | SHA1 | Date | |
|---|---|---|---|
| 3e0f70ea49 | |||
| d6c71e0ab2 |
19
changelog.md
19
changelog.md
@@ -1,3 +1,22 @@
|
||||
# Version 1.1.3
|
||||
|
||||
- Added back the ability to use the fOLD dataset. Fixes [Issue 23](https://git.research.dezeeuw.ca/tyler/flares/issues/23)
|
||||
- 5th option has been added under Analysis to get to fOLD channels per participant
|
||||
- Added an option to cancel the running process. Fixes [Issue 15](https://git.research.dezeeuw.ca/tyler/flares/issues/15)
|
||||
- Prevented graph images from showing when participants are being processed. Fixes [Issue 24](https://git.research.dezeeuw.ca/tyler/flares/issues/24)
|
||||
- Allow the option to remove all events of a type from all loaded snirfs. Fixes [Issue 25](https://git.research.dezeeuw.ca/tyler/flares/issues/25)
|
||||
- Added new icons in the menu bar
|
||||
- Added a terminal to interact with the app in a more command-like form
|
||||
- Currently the terminal has no functionality but some features for batch operations will be coming soon!
|
||||
- Inter-Group viewer now has the option to visualize the average response on the brain of all participants in the group. Fixes [Issue 26](https://git.research.dezeeuw.ca/tyler/flares/issues/24)
|
||||
|
||||
|
||||
# Version 1.1.2
|
||||
|
||||
- Fixed incorrect colormaps being applied
|
||||
- Added functionality to utilize external event markers from a file. Fixes [Issue 6](https://git.research.dezeeuw.ca/tyler/flares/issues/6)
|
||||
|
||||
|
||||
# Version 1.1.1
|
||||
|
||||
- Fixed the number of rectangles in the progress bar to 19
|
||||
|
||||
@@ -1077,7 +1077,7 @@ def epochs_calculations(raw_haemo, events, event_dict):
|
||||
|
||||
# Plot drop log
|
||||
# TODO: Why show this if we never use epochs2?
|
||||
fig_epochs_dropped = epochs2.plot_drop_log()
|
||||
fig_epochs_dropped = epochs2.plot_drop_log(show=False)
|
||||
fig_epochs.append(("fig_epochs_dropped", fig_epochs_dropped))
|
||||
|
||||
# Plot for each condition
|
||||
|
||||
1
icons/terminal_24dp_1F1F1F.svg
Normal file
1
icons/terminal_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M160-160q-33 0-56.5-23.5T80-240v-480q0-33 23.5-56.5T160-800h640q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm0-80h640v-400H160v400Zm140-40-56-56 103-104-104-104 57-56 160 160-160 160Zm180 0v-80h240v80H480Z"/></svg>
|
||||
|
After Width: | Height: | Size: 340 B |
1
icons/upgrade_24dp_1F1F1F.svg
Normal file
1
icons/upgrade_24dp_1F1F1F.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#1f1f1f"><path d="M280-160v-80h400v80H280Zm160-160v-327L336-544l-56-56 200-200 200 200-56 56-104-103v327h-80Z"/></svg>
|
||||
|
After Width: | Height: | Size: 216 B |
189
main.py
189
main.py
@@ -42,6 +42,7 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
from PySide6.QtCore import QThread, Signal, Qt, QTimer, QEvent, QSize
|
||||
from PySide6.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleValidator, QPixmap, QStandardItemModel, QStandardItem
|
||||
from PySide6.QtSvgWidgets import QSvgWidget # needed to show svgs when app is not frozen
|
||||
|
||||
|
||||
CURRENT_VERSION = "1.0.1"
|
||||
@@ -173,10 +174,10 @@ SECTIONS = [
|
||||
|
||||
|
||||
|
||||
class CommandConsole(QWidget):
|
||||
class TerminalWindow(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent, Qt.WindowType.Window)
|
||||
self.setWindowTitle("Custom Console")
|
||||
self.setWindowTitle("Terminal - FLARES")
|
||||
|
||||
self.output_area = QTextEdit()
|
||||
self.output_area.setReadOnly(True)
|
||||
@@ -189,10 +190,8 @@ class CommandConsole(QWidget):
|
||||
layout.addWidget(self.input_line)
|
||||
self.setLayout(layout)
|
||||
|
||||
# Define your commands
|
||||
self.commands = {
|
||||
"hello": self.cmd_hello,
|
||||
"add": self.cmd_add,
|
||||
"help": self.cmd_help
|
||||
}
|
||||
|
||||
@@ -219,12 +218,9 @@ class CommandConsole(QWidget):
|
||||
else:
|
||||
self.output_area.append(f"[Unknown command] '{command_name}'")
|
||||
|
||||
# Example commands
|
||||
def cmd_hello(self, *args):
|
||||
return "Hello from the console!"
|
||||
|
||||
def cmd_add(self, a, b):
|
||||
return f"Result: {int(a) + int(b)}"
|
||||
def cmd_hello(self, *args):
|
||||
return "Hello from the terminal!"
|
||||
|
||||
def cmd_help(self, *args):
|
||||
return f"Available commands: {', '.join(self.commands.keys())}"
|
||||
@@ -734,13 +730,12 @@ class UpdateEventsWindow(QWidget):
|
||||
self.description = QLabel()
|
||||
self.description.setTextFormat(Qt.TextFormat.RichText)
|
||||
self.description.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
self.description.setOpenExternalLinks(False) # Handle the click internally
|
||||
self.description.setOpenExternalLinks(True)
|
||||
|
||||
self.description.setText("Some software when creating snirf files will insert a template of optode positions as the correct position of the optodes for the participant.<br>"
|
||||
"This is rarely correct as each head differs slightly in shape or size, and a lot of calculations require the optodes to be in the correct location.<br>"
|
||||
"Using a .txt file, we can update the positions in the snirf file to match those of a digitization system such as one from Polhemus or elsewhere.<br>"
|
||||
"The .txt file should have the fiducials, detectors, and sources clearly labeled, followed by the x, y, and z coordinates seperated by a space.<br>"
|
||||
"An example format of what a digitization text file should look like can be found <a href='custom_link'>by clicking here</a>.")
|
||||
self.description.setText("The events that are present in a snirf file may not be the events that are to be studied and examined.<br>"
|
||||
"Utilizing different software and video recordings, it is easy enough to see when an action actually occured in a file.<br>"
|
||||
"The software <a href='https://www.boris.unito.it/'>BORIS</a> is used to add these events to video files, and these events can be applied to the snirf file <br>"
|
||||
"selected below by selecting the correct BORIS observation and time syncing it to an event that it shares with the snirf file.")
|
||||
|
||||
layout.addWidget(self.description)
|
||||
|
||||
@@ -1179,6 +1174,11 @@ class ProgressBubble(QWidget):
|
||||
else:
|
||||
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
|
||||
|
||||
def mark_cancelled(self):
|
||||
if 0 <= self.current_step < len(self.rects):
|
||||
rect = self.rects[self.current_step]
|
||||
rect.setStyleSheet("background-color: red; border: 1px solid gray;")
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
self.clicked.emit(self)
|
||||
super().mousePressEvent(event)
|
||||
@@ -2191,12 +2191,13 @@ class ParticipantFoldChannelsWidget(QWidget):
|
||||
self.image_index_dropdown.currentIndexChanged.connect(self.update_image_index_dropdown_label)
|
||||
|
||||
self.submit_button = QPushButton("Submit")
|
||||
self.submit_button.clicked.connect(self.show_brain_images)
|
||||
self.submit_button.clicked.connect(self.show_fold_images)
|
||||
|
||||
self.top_bar.addWidget(QLabel("Participants:"))
|
||||
self.top_bar.addWidget(self.participant_dropdown)
|
||||
self.top_bar.addWidget(QLabel("Fold Type:"))
|
||||
self.top_bar.addWidget(self.image_index_dropdown)
|
||||
self.top_bar.addWidget(QLabel("This will cause the app to hang for ~30s!"))
|
||||
self.top_bar.addWidget(self.submit_button)
|
||||
|
||||
self.scroll = QScrollArea()
|
||||
@@ -2316,7 +2317,7 @@ class ParticipantFoldChannelsWidget(QWidget):
|
||||
index_labels = [s.split(" ")[0] for s in selected]
|
||||
self.image_index_dropdown.lineEdit().setText(", ".join(index_labels))
|
||||
|
||||
def show_brain_images(self):
|
||||
def show_fold_images(self):
|
||||
import flares
|
||||
|
||||
selected_display_names = self._get_checked_items(self.participant_dropdown)
|
||||
@@ -2332,42 +2333,6 @@ class ParticipantFoldChannelsWidget(QWidget):
|
||||
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
|
||||
]
|
||||
|
||||
|
||||
parameterized_indexes = {
|
||||
0: [
|
||||
{
|
||||
"key": "show_optodes",
|
||||
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
|
||||
"default": "all",
|
||||
"type": str,
|
||||
},
|
||||
{
|
||||
"key": "show_brodmann",
|
||||
"label": "Show common brodmann areas on the brain.",
|
||||
"default": "True",
|
||||
"type": bool,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Inject full_text from index_texts
|
||||
for idx, params_list in parameterized_indexes.items():
|
||||
full_text = self.index_texts[idx] if idx < len(self.index_texts) else f"{idx} (No label found)"
|
||||
for param_info in params_list:
|
||||
param_info["full_text"] = full_text
|
||||
|
||||
indexes_needing_params = {idx: parameterized_indexes[idx] for idx in selected_indexes if idx in parameterized_indexes}
|
||||
|
||||
param_values = {}
|
||||
if indexes_needing_params:
|
||||
dialog = ParameterInputDialog(indexes_needing_params, parent=self)
|
||||
if dialog.exec_() == QDialog.Accepted:
|
||||
param_values = dialog.get_values()
|
||||
if param_values is None:
|
||||
return
|
||||
else:
|
||||
return
|
||||
|
||||
# Pass the necessary arguments to each method
|
||||
for file_path in selected_file_paths:
|
||||
haemo_obj = self.haemo_dict.get(file_path)
|
||||
@@ -2380,14 +2345,6 @@ class ParticipantFoldChannelsWidget(QWidget):
|
||||
for idx in selected_indexes:
|
||||
if idx == 0:
|
||||
|
||||
params = param_values.get(idx, {})
|
||||
show_optodes = params.get("show_optodes", None)
|
||||
show_brodmann = params.get("show_brodmann", None)
|
||||
|
||||
if show_optodes is None or show_brodmann is None:
|
||||
print(f"Missing parameters for index {idx}, skipping.")
|
||||
continue
|
||||
|
||||
flares.fold_channels(haemo_obj)
|
||||
|
||||
else:
|
||||
@@ -2600,7 +2557,7 @@ class GroupViewerWidget(QWidget):
|
||||
self.index_texts = [
|
||||
"0 (GLM Results)",
|
||||
"1 (Significance)",
|
||||
# "2 (third_image)",
|
||||
"2 (Brain Activity Visualization)",
|
||||
# "3 (fourth image)",
|
||||
]
|
||||
|
||||
@@ -2903,6 +2860,32 @@ class GroupViewerWidget(QWidget):
|
||||
"type": float,
|
||||
}
|
||||
],
|
||||
2: [
|
||||
{
|
||||
"key": "show_optodes",
|
||||
"label": "Determine what is rendered above the brain. Valid values are 'sensors', 'labels', 'none', 'all'.",
|
||||
"default": "all",
|
||||
"type": str,
|
||||
},
|
||||
{
|
||||
"key": "t_or_theta",
|
||||
"label": "Specify if t values or theta values should be plotted. Valid values are 't', 'theta'",
|
||||
"default": "theta",
|
||||
"type": str,
|
||||
},
|
||||
{
|
||||
"key": "show_text",
|
||||
"label": "Display informative text on the top left corner. THIS DOES NOT WORK AND SHOULD BE LEFT AT FALSE",
|
||||
"default": "False",
|
||||
"type": bool,
|
||||
},
|
||||
{
|
||||
"key": "brain_bounds",
|
||||
"label": "Graph Upper/Lower Limit",
|
||||
"default": "1.0",
|
||||
"type": float,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
# Inject full_text from index_texts
|
||||
@@ -2925,8 +2908,13 @@ class GroupViewerWidget(QWidget):
|
||||
|
||||
|
||||
all_cha = pd.DataFrame()
|
||||
for fp in selected_file_paths:
|
||||
cha_df = self.cha.get(fp)
|
||||
for file_path in selected_file_paths:
|
||||
haemo_obj = self.haemo_dict.get(file_path)
|
||||
|
||||
if haemo_obj is None:
|
||||
continue
|
||||
|
||||
cha_df = self.cha.get(file_path)
|
||||
if cha_df is not None:
|
||||
all_cha = pd.concat([all_cha, cha_df], ignore_index=True)
|
||||
|
||||
@@ -2981,6 +2969,20 @@ class GroupViewerWidget(QWidget):
|
||||
df_contrasts = pd.concat(all_contrasts, ignore_index=True)
|
||||
flares.run_second_level_analysis(df_contrasts, p_haemo, p_val, graph_bounds)
|
||||
|
||||
elif idx == 2:
|
||||
params = param_values.get(idx, {})
|
||||
show_optodes = params.get("show_optodes", None)
|
||||
t_or_theta = params.get("t_or_theta", None)
|
||||
show_text = params.get("show_text", None)
|
||||
brain_bounds = params.get("brain_bounds", None)
|
||||
|
||||
if show_optodes is None or t_or_theta is None or show_text is None or brain_bounds is None:
|
||||
print(f"Missing parameters for index {idx}, skipping.")
|
||||
continue
|
||||
|
||||
flares.brain_3d_visualization(haemo_obj, all_cha, selected_event, t_or_theta=t_or_theta, show_optodes=show_optodes, show_text=show_text, brain_bounds=brain_bounds)
|
||||
|
||||
|
||||
elif idx == 3:
|
||||
pass
|
||||
|
||||
@@ -3606,6 +3608,7 @@ class MainApplication(QMainWindow):
|
||||
self.param_sections = []
|
||||
self.folder_paths = []
|
||||
self.section_widget = None
|
||||
self.first_run = True
|
||||
|
||||
self.init_ui()
|
||||
self.create_menu_bar()
|
||||
@@ -3834,8 +3837,8 @@ class MainApplication(QMainWindow):
|
||||
options_actions = [
|
||||
("User Guide", "F1", self.user_guide, resource_path("icons/help_24dp_1F1F1F.svg")),
|
||||
("Check for Updates", "F5", self.manual_check_for_updates, resource_path("icons/update_24dp_1F1F1F.svg")),
|
||||
("Update optodes in snirf file...", "F6", self.update_optode_positions, resource_path("icons/update_24dp_1F1F1F.svg")),
|
||||
("Update events in snirf file...", "F7", self.update_event_markers, resource_path("icons/update_24dp_1F1F1F.svg")),
|
||||
("Update optodes in snirf file...", "F6", self.update_optode_positions, resource_path("icons/upgrade_24dp_1F1F1F.svg")),
|
||||
("Update events in snirf file...", "F7", self.update_event_markers, resource_path("icons/upgrade_24dp_1F1F1F.svg")),
|
||||
("About", "F12", self.about_window, resource_path("icons/info_24dp_1F1F1F.svg"))
|
||||
]
|
||||
|
||||
@@ -3846,9 +3849,7 @@ class MainApplication(QMainWindow):
|
||||
|
||||
terminal_menu = menu_bar.addMenu("Terminal")
|
||||
terminal_actions = [
|
||||
("Cut", "Ctrl+X", self.terminal_gui, resource_path("icons/content_cut_24dp_1F1F1F.svg")),
|
||||
("Copy", "Ctrl+C", self.terminal_gui, resource_path("icons/content_copy_24dp_1F1F1F.svg")),
|
||||
("Paste", "Ctrl+V", self.terminal_gui, resource_path("icons/content_paste_24dp_1F1F1F.svg"))
|
||||
("New Terminal", "Ctrl+Alt+T", self.terminal_gui, resource_path("icons/terminal_24dp_1F1F1F.svg")),
|
||||
]
|
||||
for name, shortcut, slot, icon in terminal_actions:
|
||||
terminal_menu.addAction(make_action(name, shortcut, slot, icon=icon))
|
||||
@@ -3875,6 +3876,8 @@ class MainApplication(QMainWindow):
|
||||
|
||||
def clear_all(self):
|
||||
|
||||
self.cancel_task()
|
||||
|
||||
self.right_column_widget.hide()
|
||||
|
||||
# Clear the bubble layout
|
||||
@@ -3937,7 +3940,7 @@ class MainApplication(QMainWindow):
|
||||
|
||||
def terminal_gui(self):
|
||||
if self.terminal is None or not self.terminal.isVisible():
|
||||
self.terminal = CommandConsole(self)
|
||||
self.terminal = TerminalWindow(self)
|
||||
self.terminal.show()
|
||||
|
||||
def update_optode_positions(self):
|
||||
@@ -4284,8 +4287,52 @@ class MainApplication(QMainWindow):
|
||||
return self.file_metadata
|
||||
|
||||
|
||||
def cancel_task(self):
|
||||
self.button1.clicked.disconnect(self.cancel_task)
|
||||
self.button1.setText("Stopping...")
|
||||
|
||||
if hasattr(self, "result_process") and self.result_process.is_alive():
|
||||
parent = psutil.Process(self.result_process.pid)
|
||||
children = parent.children(recursive=True)
|
||||
for child in children:
|
||||
try:
|
||||
child.kill()
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
self.result_process.terminate()
|
||||
self.result_process.join()
|
||||
|
||||
if hasattr(self, "result_timer") and self.result_timer.isActive():
|
||||
self.result_timer.stop()
|
||||
|
||||
# if hasattr(self, "result_process") and self.result_process.is_alive():
|
||||
# self.result_process.terminate() # Forcefully terminate the process
|
||||
# self.result_process.join() # Wait for it to properly close
|
||||
|
||||
# # Stop the QTimer if running
|
||||
# if hasattr(self, "result_timer") and self.result_timer.isActive():
|
||||
# self.result_timer.stop()
|
||||
|
||||
for bubble in self.bubble_widgets.values():
|
||||
bubble.mark_cancelled()
|
||||
|
||||
self.statusbar.showMessage("Processing cancelled.")
|
||||
self.button1.clicked.connect(self.on_run_task)
|
||||
self.button1.setText("Process")
|
||||
|
||||
|
||||
'''MODULE FILE'''
|
||||
def on_run_task(self):
|
||||
|
||||
self.button1.clicked.disconnect(self.on_run_task)
|
||||
self.button1.setText("Cancel")
|
||||
self.button1.clicked.connect(self.cancel_task)
|
||||
|
||||
if not self.first_run:
|
||||
for bubble in self.bubble_widgets.values():
|
||||
bubble.mark_cancelled()
|
||||
self.first_run = False
|
||||
|
||||
# Collect all selected snirf files in a flat list
|
||||
snirf_files = []
|
||||
|
||||
@@ -4813,7 +4860,7 @@ def resource_path(relative_path):
|
||||
# PyInstaller bundle path
|
||||
base_path = sys._MEIPASS
|
||||
else:
|
||||
base_path = os.path.abspath(".")
|
||||
base_path = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
return os.path.join(base_path, relative_path)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user