qol improvements + optode location fix

This commit is contained in:
2026-04-15 16:15:15 -07:00
parent 8d922ecae9
commit 0f6434121f
3 changed files with 185 additions and 120 deletions
+107 -117
View File
@@ -1685,13 +1685,14 @@ class ProgressBubble(QWidget):
# Transition to a green checkmark
self.setSuffixText(" <span style='color: green;'>✔</span>")
def update_progress(self, step_index):
def update_progress(self, step_index, active=True):
self.current_step = step_index
for i, rect in enumerate(self.rects):
if i < step_index:
rect.setStyleSheet("background-color: green; border: 1px solid gray;")
elif i == step_index:
rect.setStyleSheet("background-color: yellow; border: 1px solid gray;")
color = "yellow" if active else "white"
rect.setStyleSheet(f"background-color: {color}; border: 1px solid gray;")
else:
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
@@ -3682,8 +3683,22 @@ class ExportDataAsCSVViewerWidget(FlaresBaseWidget):
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
if not selected_file_paths or not selected_indexes:
QMessageBox.warning(self, "Selection Missing", "Please select at least one participant and one export type.")
return
# 2. ASK ONCE: Select Output Directory
output_dir = QFileDialog.getExistingDirectory(self, "Select Output Folder for CSV Exports")
if not output_dir:
print("Export cancelled: No folder selected.")
return
success_count = 0
# Pass the necessary arguments to each method
for file_path in selected_file_paths:
base_filename = os.path.splitext(os.path.basename(file_path))[0]
haemo_obj = self.haemo_dict.get(file_path)
if haemo_obj is None:
continue
@@ -3691,90 +3706,57 @@ class ExportDataAsCSVViewerWidget(FlaresBaseWidget):
cha = self.cha_dict.get(file_path)
for idx in selected_indexes:
if idx == 0:
try:
suggested_name = f"{file_path}.csv"
try:
if idx == 0:
save_path = os.path.join(output_dir, f"{base_filename}_exported.csv")
if cha is not None:
cha.to_csv(save_path)
success_count += 1
# Open save dialog
save_path, _ = QFileDialog.getSaveFileName(
self,
"Save CSV File As",
suggested_name,
"CSV Files (*.csv)"
)
if not save_path:
print("Save cancelled.")
return
elif idx == 1:
# SPARKS Export
save_path = os.path.join(output_dir, f"{base_filename}_sparks.csv")
if haemo_obj is not None:
raw = haemo_obj
data, times = raw.get_data(return_times=True)
ann_col = np.full(times.shape, "", dtype=object)
if not save_path.lower().endswith(".csv"):
save_path += ".csv"
# Save the CSV here
if raw.annotations is not None and len(raw.annotations) > 0:
for onset, duration, desc in zip(
raw.annotations.onset,
raw.annotations.duration,
raw.annotations.description
):
mask = (times >= onset) & (times < onset + duration)
ann_col[mask] = desc
df = pd.DataFrame(data.T, columns=raw.ch_names)
df.insert(0, "annotation", ann_col)
df.insert(0, "time", times)
df.to_csv(save_path, index=False)
success_count += 1
cha.to_csv(save_path)
QMessageBox.information(self, "Success", "CSV file has been saved.")
else:
print(f"No method defined for index {idx}")
except Exception as e:
print(f"Failed to export {file_path} (Type {idx}): {e}")
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to update CSV file:\n{e}")
# 4. Final Notification
if success_count > 0:
QMessageBox.information(self, "Export Complete", f"Successfully saved {success_count} CSV files to:\n{output_dir}")
# # If SPARKS export was included, show the Event Window once at the end
# if 1 in selected_indexes:
# win = UpdateEventsWindow(
# parent=self,
# mode=EventUpdateMode.WRITE_JSON,
# caller="Video Alignment Tool"
# )
# win.show()
elif idx == 1:
try:
suggested_name = f"{file_path}_sparks.csv"
# Open save dialog
save_path, _ = QFileDialog.getSaveFileName(
self,
"Save CSV File As",
suggested_name,
"CSV Files (*.csv)"
)
if not save_path:
print("Save cancelled.")
return
if not save_path.lower().endswith(".csv"):
save_path += ".csv"
# Save the CSV here
raw = haemo_obj
data, times = raw.get_data(return_times=True)
ann_col = np.full(times.shape, "", dtype=object)
if raw.annotations is not None and len(raw.annotations) > 0:
for onset, duration, desc in zip(
raw.annotations.onset,
raw.annotations.duration,
raw.annotations.description
):
mask = (times >= onset) & (times < onset + duration)
ann_col[mask] = desc
df = pd.DataFrame(data.T, columns=raw.ch_names)
df.insert(0, "annotation", ann_col)
df.insert(0, "time", times)
df.to_csv(save_path, index=False)
QMessageBox.information(self, "Success", "CSV file has been saved.")
win = UpdateEventsWindow(
parent=self,
mode=EventUpdateMode.WRITE_JSON,
caller="Video Alignment Tool"
)
win.show()
except Exception as e:
QMessageBox.critical(self, "Error", f"Failed to update CSV file:\n{e}")
else:
print(f"No method defined for index {idx}")
class ClickableLabel(QLabel):
def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap):
super().__init__()
@@ -4204,9 +4186,16 @@ class GroupViewerWidget(FlaresBaseWidget):
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)
raw_list = [self.haemo_dict.get(fp) for fp in selected_file_paths]
if len(selected_file_paths) > 1:
print(f"Aggregating geometry for {len(selected_file_paths)} participants...")
processed_raw = flares.aggregate_fnirs_group_geometry(raw_list)
else:
processed_raw = raw_list[0].copy().pick(picks="hbo")
flares.brain_3d_visualization(processed_raw, 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
@@ -4383,6 +4372,11 @@ class GroupBrainViewerWidget(FlaresBaseWidget):
int(s.split(" ")[0]) for s in self._get_checked_items(self.image_index_dropdown)
]
all_selected_paths = list(set(file_paths_a + file_paths_b))
if not all_selected_paths:
print("No participants selected.")
return
parameterized_indexes = {
0: [
@@ -4463,14 +4457,12 @@ class GroupBrainViewerWidget(FlaresBaseWidget):
print("contrast_df_a empty?", contrast_df_a.empty)
print("contrast_df_b empty?", contrast_df_b.empty)
# Get one person for their layout
rep_raw = None
for fp in file_paths_a + file_paths_b:
rep_raw = self.haemo_dict.get(fp)
if rep_raw:
break
print(rep_raw)
all_raw_objs = [self.haemo_dict.get(fp) for fp in all_selected_paths if self.haemo_dict.get(fp)]
if len(all_raw_objs) > 1:
processed_raw = flares.aggregate_fnirs_group_geometry(all_raw_objs)
else:
processed_raw = all_raw_objs[0].copy().pick(picks="hbo")
# Visualizations
for idx in selected_indexes:
@@ -4486,12 +4478,12 @@ class GroupBrainViewerWidget(FlaresBaseWidget):
print(f"Missing parameters for index {idx}, skipping.")
continue
if not contrast_df_a.empty and not contrast_df_b.empty and rep_raw:
if not contrast_df_a.empty and not contrast_df_b.empty and processed_raw:
flares.plot_2d_3d_contrasts_between_groups(
contrast_df_a,
contrast_df_b,
raw_haemo=rep_raw,
raw_haemo=processed_raw,
group_a_name=self.group_a_dropdown.currentText(),
group_b_name=self.group_b_dropdown.currentText(),
is_3d=is_3d,
@@ -4713,6 +4705,7 @@ class MainApplication(QMainWindow):
label = QLabel(key.capitalize())
right_column_layout.addWidget(label)
right_column_layout.addWidget(field)
field.textChanged.connect(self.sync_bubble_data)
label_desc = QLabel('<a href="#">Why are these useful?</a>')
label_desc.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
@@ -5405,8 +5398,6 @@ class MainApplication(QMainWindow):
for rel_path, step in raw_progress.items()
}
self.show_files_as_bubbles_from_list(file_list, progress_states, filename)
for rel_path in data["file_list"]:
abs_path = str((project_dir / Path(rel_path)).resolve())
@@ -5425,14 +5416,7 @@ class MainApplication(QMainWindow):
# Scenario C: Empty default
self.file_metadata[abs_path] = {"AGE": "", "GENDER": "", "GROUP": ""}
if file_list:
self.current_file = file_list[0]
self.right_column_widget.show()
# Update Metadata fields (Age/Gender/Group) for the selected file
curr_meta = self.file_metadata.get(self.current_file, {"AGE": "", "GENDER": "", "GROUP": ""})
for key, field in self.meta_fields.items():
field.setText(curr_meta.get(key, ""))
self.show_files_as_bubbles_from_list(file_list, progress_states, filename)
if "current_ui_params" in data:
self.restore_sections_from_config(data["current_ui_params"])
@@ -5589,6 +5573,18 @@ class MainApplication(QMainWindow):
bubble.clicked.connect(self.on_bubble_clicked)
bubble.rightClicked.connect(self.on_bubble_right_clicked)
if hasattr(self, 'file_metadata') and file_path in self.file_metadata:
meta = self.file_metadata[file_path]
parts = []
for key in ["AGE", "GENDER", "GROUP"]:
value = meta.get(key, "").strip()
if value:
parts.append(f"{key}: {value}")
suffix = f"{', '.join(parts)}" if parts else ""
bubble.setSuffixText(suffix)
# Track it
self.bubble_widgets[file_path] = bubble
if file_path not in self.selected_paths:
@@ -5596,7 +5592,7 @@ class MainApplication(QMainWindow):
# Restore saved progress but keep loading state active
step = progress_states.get(file_path, 0)
bubble.update_progress(step)
bubble.update_progress(step, active=False)
# Add to layout
self.bubble_layout.addWidget(bubble, index, 1)
@@ -5757,21 +5753,15 @@ class MainApplication(QMainWindow):
if getattr(self, 'last_clicked_bubble', None) is bubble:
self.last_clicked_bubble = None
def eventFilter(self, watched, event):
if event.type() == QEvent.Type.MouseButtonPress:
widget = self.childAt(event.pos())
if isinstance(widget, ProgressBubble):
pass
else:
if self.last_clicked_bubble:
if not self.last_clicked_bubble.isAncestorOf(widget):
if self.current_file:
self.save_metadata(self.current_file)
suffix = self.get_suffix_from_meta_fields()
self.last_clicked_bubble.setSuffixText(suffix)
self.last_clicked_bubble = None
return super().eventFilter(watched, event)
def sync_bubble_data(self):
"""Refreshes the bubble and saves data in real-time."""
if self.current_file and self.last_clicked_bubble:
# Save the current state of all fields
self.save_metadata(self.current_file)
# Grab the updated suffix and apply it immediately
suffix = self.get_suffix_from_meta_fields()
self.last_clicked_bubble.setSuffixText(suffix)
def placeholder(self):
QMessageBox.information(self, "Placeholder", "This feature is not implemented yet.")