further variable changes
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# Version 1.2.0
|
# Version 1.2.0
|
||||||
|
|
||||||
- Added new parameters to the right side of the screen
|
- Added new parameters to the right side of the screen
|
||||||
- These parameters include SHOW_OPTODE_NAMES, SECONDS_TO_STRIP_HR, MAX_LOW_HR, MAX_HIGH_HR, SMOTHING_WINDOW_HR, HEART_RATE_WINDOW, BAD_CHANNELS_HANDLING, MAX_DIST, MIN_NEIGHBORS, L_TRANS_BANDWIDTH, H_TRANS_BANDWIDTH, RESAMPLE, RESAMPLE_FREQ, STIM_DUR, HRF_MODEL, HIGH_PASS, DRIFT_ORDER, FIR_DELAYS, MIN_ONSET, OVERSAMPLING, SHORT_CHANNEL_REGRESSION, NOISE_MODEL, and BINS.
|
- These parameters include SHOW_OPTODE_NAMES, SECONDS_TO_STRIP_HR, MAX_LOW_HR, MAX_HIGH_HR, SMOOTHING_WINDOW_HR, HEART_RATE_WINDOW, BAD_CHANNELS_HANDLING, MAX_DIST, MIN_NEIGHBORS, L_TRANS_BANDWIDTH, H_TRANS_BANDWIDTH, RESAMPLE, RESAMPLE_FREQ, STIM_DUR, HRF_MODEL, HIGH_PASS, DRIFT_ORDER, FIR_DELAYS, MIN_ONSET, OVERSAMPLING, SHORT_CHANNEL_REGRESSION, NOISE_MODEL, BINS, and VERBOSITY.
|
||||||
- All the new parameters have default values matching the underlying values in version 1.1.7
|
- All the new parameters have default values matching the underlying values in version 1.1.7
|
||||||
- The order of the parameters have changed to match the order that the code runs when the Process button is clicked
|
- The order of the parameters have changed to match the order that the code runs when the Process button is clicked
|
||||||
- Moved TIME_WINDOW_START and TIME_WINDOW_END to the 'Other' category
|
- Moved TIME_WINDOW_START and TIME_WINDOW_END to the 'Other' category
|
||||||
@@ -12,6 +13,8 @@
|
|||||||
- Fixed a crash when attempting to fOLD channels without the fOLD dataset installed
|
- Fixed a crash when attempting to fOLD channels without the fOLD dataset installed
|
||||||
- Lowered the number of rectangles in the progress bar to 24 after combining some actions
|
- Lowered the number of rectangles in the progress bar to 24 after combining some actions
|
||||||
- Fixed the User Guide window to properly display information about the 24 stages and added a link to the Git wiki page
|
- Fixed the User Guide window to properly display information about the 24 stages and added a link to the Git wiki page
|
||||||
|
- MAX_WORKERS should now properly repect the value set
|
||||||
|
- Added a new CSV export option to be used by other applications
|
||||||
|
|
||||||
|
|
||||||
# Version 1.1.7
|
# Version 1.1.7
|
||||||
|
|||||||
21
flares.py
21
flares.py
@@ -90,7 +90,7 @@ from mne_nirs.io.fold import fold_channel_specificity # type: ignore
|
|||||||
from mne_nirs.preprocessing import peak_power # type: ignore
|
from mne_nirs.preprocessing import peak_power # type: ignore
|
||||||
from mne_nirs.statistics._glm_level_first import RegressionResults # type: ignore
|
from mne_nirs.statistics._glm_level_first import RegressionResults # type: ignore
|
||||||
|
|
||||||
# Needs to be set for men
|
# Needs to be set for mne
|
||||||
os.environ["SUBJECTS_DIR"] = str(data_path()) + "/subjects" # type: ignore
|
os.environ["SUBJECTS_DIR"] = str(data_path()) + "/subjects" # type: ignore
|
||||||
|
|
||||||
# TODO: Tidy this up
|
# TODO: Tidy this up
|
||||||
@@ -167,6 +167,8 @@ H_FREQ: float
|
|||||||
L_TRANS_BANDWIDTH: float
|
L_TRANS_BANDWIDTH: float
|
||||||
H_TRANS_BANDWIDTH: float
|
H_TRANS_BANDWIDTH: float
|
||||||
|
|
||||||
|
RESAMPLE: bool
|
||||||
|
RESAMPLE_FREQ: int
|
||||||
STIM_DUR: float
|
STIM_DUR: float
|
||||||
HRF_MODEL: str
|
HRF_MODEL: str
|
||||||
DRIFT_MODEL: str
|
DRIFT_MODEL: str
|
||||||
@@ -184,8 +186,8 @@ N_JOBS: int
|
|||||||
|
|
||||||
TIME_WINDOW_START: int
|
TIME_WINDOW_START: int
|
||||||
TIME_WINDOW_END: int
|
TIME_WINDOW_END: int
|
||||||
|
MAX_WORKERS: int
|
||||||
VERBOSITY = True
|
VERBOSITY: bool
|
||||||
|
|
||||||
AGE = 25 # Assume 25 if not set from the GUI. This will result in a reasonable PPF
|
AGE = 25 # Assume 25 if not set from the GUI. This will result in a reasonable PPF
|
||||||
GENDER = ""
|
GENDER = ""
|
||||||
@@ -1252,7 +1254,7 @@ def epochs_calculations(raw_haemo, events, event_dict):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
data = evoked.data[picks_idx, :].mean(axis=0)
|
data = evoked.data[picks_idx, :].mean(axis=0)
|
||||||
t_start, t_end = 0, 15
|
t_start, t_end = 0, 15 #TODO: Is this in seconds? or is it 1hz input that makes it 15s?
|
||||||
times_mask = (evoked.times >= t_start) & (evoked.times <= t_end)
|
times_mask = (evoked.times >= t_start) & (evoked.times <= t_end)
|
||||||
data_segment = data[times_mask]
|
data_segment = data[times_mask]
|
||||||
times_segment = evoked.times[times_mask]
|
times_segment = evoked.times[times_mask]
|
||||||
@@ -1314,11 +1316,16 @@ def make_design_matrix(raw_haemo, short_chans):
|
|||||||
# Set the new annotations
|
# Set the new annotations
|
||||||
raw_haemo.set_annotations(new_annot)
|
raw_haemo.set_annotations(new_annot)
|
||||||
|
|
||||||
raw_haemo.resample(1, npad="auto")
|
if RESAMPLE:
|
||||||
|
raw_haemo.resample(RESAMPLE_FREQ, npad="auto")
|
||||||
raw_haemo._data = raw_haemo._data * 1e6
|
raw_haemo._data = raw_haemo._data * 1e6
|
||||||
|
try:
|
||||||
|
short_chans.resample(RESAMPLE_FREQ)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
# 2) Create design matrix
|
# 2) Create design matrix
|
||||||
if SHORT_CHANNEL:
|
if SHORT_CHANNEL_REGRESSION:
|
||||||
short_chans.resample(1)
|
|
||||||
design_matrix = make_first_level_design_matrix(
|
design_matrix = make_first_level_design_matrix(
|
||||||
raw=raw_haemo,
|
raw=raw_haemo,
|
||||||
stim_dur=STIM_DUR,
|
stim_dur=STIM_DUR,
|
||||||
|
|||||||
222
main.py
222
main.py
@@ -22,6 +22,7 @@ import subprocess
|
|||||||
from pathlib import Path, PurePosixPath
|
from pathlib import Path, PurePosixPath
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from multiprocessing import Process, current_process, freeze_support, Manager
|
from multiprocessing import Process, current_process, freeze_support, Manager
|
||||||
|
from enum import Enum, auto
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -215,9 +216,10 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Other",
|
"title": "Other",
|
||||||
"params": [
|
"params": [
|
||||||
{"name": "TIME_WINDOW_START", "default": "0", "type": int, "help": "Where to start averaging the fir model bins. Only affects the significance and contrast images."},
|
{"name": "TIME_WINDOW_START", "default": 0, "type": int, "help": "Where to start averaging the fir model bins. Only affects the significance and contrast images."},
|
||||||
{"name": "TIME_WINDOW_END", "default": "15", "type": int, "help": "Where to end averaging the fir model bins. Only affects the significance and contrast images."},
|
{"name": "TIME_WINDOW_END", "default": 15, "type": int, "help": "Where to end averaging the fir model bins. Only affects the significance and contrast images."},
|
||||||
{"name": "MAX_WORKERS", "default": 4, "type": int, "help": "Number of files to be processed at once. Lowering this value may help on underpowered systems."},
|
{"name": "MAX_WORKERS", "default": 4, "type": str, "help": "Number of files to be processed at once. Setting this to a small integer value may help on underpowered systems. Remove the value to use an automatic amount."},
|
||||||
|
{"name": "VERBOSITY", "default": False, "type": bool, "help": "True will log lots of debugging information to the log file. False will only log required data."},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -766,15 +768,23 @@ class UpdateOptodesWindow(QWidget):
|
|||||||
write_raw_snirf(raw, save_path)
|
write_raw_snirf(raw, save_path)
|
||||||
|
|
||||||
|
|
||||||
|
class EventUpdateMode(Enum):
|
||||||
|
WRITE_SNIRF = auto() # destructive
|
||||||
|
WRITE_JSON = auto() # non-destructive
|
||||||
|
|
||||||
|
|
||||||
class UpdateEventsWindow(QWidget):
|
class UpdateEventsWindow(QWidget):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None, mode=EventUpdateMode.WRITE_SNIRF, caller=None):
|
||||||
super().__init__(parent, Qt.WindowType.Window)
|
super().__init__(parent, Qt.WindowType.Window)
|
||||||
|
|
||||||
|
self.mode = mode
|
||||||
|
self.caller = caller or self.__class__.__name__
|
||||||
self.setWindowTitle("Update event markers")
|
self.setWindowTitle("Update event markers")
|
||||||
self.resize(760, 200)
|
self.resize(760, 200)
|
||||||
|
|
||||||
|
print("INIT MODE:", mode)
|
||||||
|
|
||||||
self.label_file_a = QLabel("SNIRF file:")
|
self.label_file_a = QLabel("SNIRF file:")
|
||||||
self.line_edit_file_a = QLineEdit()
|
self.line_edit_file_a = QLineEdit()
|
||||||
self.line_edit_file_a.setReadOnly(True)
|
self.line_edit_file_a.setReadOnly(True)
|
||||||
@@ -1075,46 +1085,55 @@ class UpdateEventsWindow(QWidget):
|
|||||||
QMessageBox.warning(self, "No SNIRF file", "Please select a SNIRF file.")
|
QMessageBox.warning(self, "No SNIRF file", "Please select a SNIRF file.")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
|
boris_obs = self.boris_data["observations"][selected_obs]
|
||||||
|
|
||||||
|
# --- Extract videos + delays ---
|
||||||
|
files = boris_obs.get("file", {})
|
||||||
|
offsets = boris_obs.get("media_info", {}).get("offset", {})
|
||||||
|
|
||||||
|
videos = {}
|
||||||
|
for key, path in files.items():
|
||||||
|
if path: # only include videos that exist
|
||||||
|
delay = offsets.get(key, 0.0) # default 0 if missing
|
||||||
|
videos[key] = {"file": path, "delay": delay}
|
||||||
|
|
||||||
|
base_name = os.path.splitext(os.path.basename(file_a))[0]
|
||||||
|
|
||||||
|
if self.mode == EventUpdateMode.WRITE_SNIRF:
|
||||||
|
# Open save dialog for SNIRF
|
||||||
base_name = os.path.splitext(os.path.basename(file_a))[0]
|
base_name = os.path.splitext(os.path.basename(file_a))[0]
|
||||||
suggested_name = f"{base_name}_{suffix}.snirf"
|
suggested_name = f"{base_name}_{suffix}.snirf"
|
||||||
|
|
||||||
# Open save dialog
|
|
||||||
save_path, _ = QFileDialog.getSaveFileName(
|
save_path, _ = QFileDialog.getSaveFileName(
|
||||||
self,
|
self,
|
||||||
"Save SNIRF File As",
|
"Save SNIRF File As",
|
||||||
suggested_name,
|
suggested_name,
|
||||||
"SNIRF Files (*.snirf)"
|
"SNIRF Files (*.snirf)"
|
||||||
)
|
)
|
||||||
|
|
||||||
if not save_path:
|
if not save_path:
|
||||||
print("Save cancelled.")
|
print("SNIRF save cancelled.")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not save_path.lower().endswith(".snirf"):
|
if not save_path.lower().endswith(".snirf"):
|
||||||
save_path += ".snirf"
|
save_path += ".snirf"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw = read_raw_snirf(snirf_path, preload=True)
|
raw = read_raw_snirf(file_a, preload=True)
|
||||||
|
|
||||||
onsets = []
|
|
||||||
durations = []
|
|
||||||
descriptions = []
|
|
||||||
|
|
||||||
|
# --- Align BORIS events to SNIRF ---
|
||||||
|
boris_events = boris_obs.get("events", [])
|
||||||
|
onsets, durations, descriptions = [], [], []
|
||||||
open_events = {} # label -> list of start times
|
open_events = {} # label -> list of start times
|
||||||
label_counts = {}
|
label_counts = {}
|
||||||
|
|
||||||
used_times = set()
|
used_times = set()
|
||||||
sfreq = raw.info['sfreq'] # sampling frequency in Hz
|
sfreq = raw.info['sfreq']
|
||||||
min_shift = 1.0 / sfreq
|
min_shift = 1.0 / sfreq
|
||||||
max_attempts = 10
|
max_attempts = 10
|
||||||
|
|
||||||
for event in boris_events:
|
for event in boris_events:
|
||||||
if not isinstance(event, list) or len(event) < 3:
|
if not isinstance(event, list) or len(event) < 3:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
event_time = event[0]
|
event_time = event[0]
|
||||||
label = event[2]
|
label = event[2]
|
||||||
|
|
||||||
count = label_counts.get(label, 0) + 1
|
count = label_counts.get(label, 0) + 1
|
||||||
label_counts[label] = count
|
label_counts[label] = count
|
||||||
|
|
||||||
@@ -1122,74 +1141,84 @@ class UpdateEventsWindow(QWidget):
|
|||||||
open_events[label] = []
|
open_events[label] = []
|
||||||
|
|
||||||
if count % 2 == 1:
|
if count % 2 == 1:
|
||||||
# Odd occurrence = start event
|
|
||||||
open_events[label].append(event_time)
|
open_events[label].append(event_time)
|
||||||
else:
|
else:
|
||||||
# Even occurrence = end event
|
|
||||||
if open_events[label]:
|
if open_events[label]:
|
||||||
matched_start = open_events[label].pop(0)
|
start_time = open_events[label].pop(0)
|
||||||
duration = event_time - matched_start
|
duration = event_time - start_time
|
||||||
|
|
||||||
if duration <= 0:
|
if duration <= 0:
|
||||||
print(f"Warning: Duration for {label} is non-positive ({duration}). Skipping.")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
shifted_start = matched_start + time_shift
|
adjusted_time = start_time + time_shift
|
||||||
|
|
||||||
adjusted_time = shifted_start
|
|
||||||
attempts = 0
|
attempts = 0
|
||||||
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
|
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
|
||||||
adjusted_time += min_shift
|
adjusted_time += min_shift
|
||||||
attempts += 1
|
attempts += 1
|
||||||
|
|
||||||
if attempts == max_attempts:
|
if attempts == max_attempts:
|
||||||
print(f"Warning: Couldn't find unique time for {label} @ {matched_start}s. Skipping.")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
adjusted_time = round(adjusted_time, 6)
|
adjusted_time = round(adjusted_time, 6)
|
||||||
used_times.add(adjusted_time)
|
used_times.add(adjusted_time)
|
||||||
|
|
||||||
print(f"Adding event: {label} @ {adjusted_time:.3f}s for {duration:.3f}s")
|
|
||||||
|
|
||||||
onsets.append(adjusted_time)
|
onsets.append(adjusted_time)
|
||||||
durations.append(duration)
|
durations.append(duration)
|
||||||
descriptions.append(label)
|
descriptions.append(label)
|
||||||
else:
|
# Handle unmatched starts
|
||||||
print(f"Warning: Unmatched end for label '{label}' at {event_time:.3f}s. Skipping.")
|
|
||||||
|
|
||||||
# Optionally warn about any unmatched starts left open
|
|
||||||
for label, starts in open_events.items():
|
for label, starts in open_events.items():
|
||||||
for start_time in starts:
|
for start_time in starts:
|
||||||
shifted_start = start_time + time_shift
|
adjusted_time = start_time + time_shift
|
||||||
|
|
||||||
adjusted_time = shifted_start
|
|
||||||
attempts = 0
|
attempts = 0
|
||||||
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
|
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
|
||||||
adjusted_time += min_shift
|
adjusted_time += min_shift
|
||||||
attempts += 1
|
attempts += 1
|
||||||
|
|
||||||
if attempts == max_attempts:
|
if attempts == max_attempts:
|
||||||
print(f"Warning: Couldn't find unique time for unmatched start {label} @ {start_time}s. Skipping.")
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
adjusted_time = round(adjusted_time, 6)
|
adjusted_time = round(adjusted_time, 6)
|
||||||
used_times.add(adjusted_time)
|
used_times.add(adjusted_time)
|
||||||
|
|
||||||
print(f"Warning: Unmatched start for label '{label}' at {start_time:.3f}s. Adding with duration 0.")
|
|
||||||
onsets.append(adjusted_time)
|
onsets.append(adjusted_time)
|
||||||
durations.append(0.0)
|
durations.append(0.0)
|
||||||
descriptions.append(label)
|
descriptions.append(label)
|
||||||
|
|
||||||
new_annotations = Annotations(onset=onsets, duration=durations, description=descriptions)
|
new_annotations = Annotations(onset=onsets, duration=durations, description=descriptions)
|
||||||
|
|
||||||
raw.set_annotations(new_annotations)
|
raw.set_annotations(new_annotations)
|
||||||
write_raw_snirf(raw, save_path)
|
write_raw_snirf(raw, save_path)
|
||||||
|
|
||||||
QMessageBox.information(self, "Success", "SNIRF file updated with aligned BORIS events.")
|
QMessageBox.information(self, "Success", "SNIRF file updated with aligned BORIS events.")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
||||||
|
|
||||||
|
elif self.mode == EventUpdateMode.WRITE_JSON:
|
||||||
|
# Open save dialog for JSON
|
||||||
|
base_name = os.path.splitext(os.path.basename(file_a))[0]
|
||||||
|
suggested_name = f"{base_name}_{suffix}_alignment.json"
|
||||||
|
save_path, _ = QFileDialog.getSaveFileName(
|
||||||
|
self,
|
||||||
|
"Save Event Alignment JSON As",
|
||||||
|
suggested_name,
|
||||||
|
"JSON Files (*.json)"
|
||||||
|
)
|
||||||
|
if not save_path:
|
||||||
|
print("JSON save cancelled.")
|
||||||
|
return
|
||||||
|
if not save_path.lower().endswith(".json"):
|
||||||
|
save_path += ".json"
|
||||||
|
|
||||||
|
# Build JSON dict
|
||||||
|
json_data = {
|
||||||
|
"observation": selected_obs,
|
||||||
|
"snirf_anchor": {"label": snirf_label, "time": snirf_anchor_time},
|
||||||
|
"boris_anchor": {"label": boris_label, "time": boris_anchor_time},
|
||||||
|
"time_shift": time_shift,
|
||||||
|
"videos": videos
|
||||||
|
}
|
||||||
|
|
||||||
|
# Write JSON
|
||||||
|
try:
|
||||||
|
with open(save_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(json_data, f, indent=4)
|
||||||
|
QMessageBox.information(self, "Success", f"Event alignment saved to:\n{save_path}")
|
||||||
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Error", f"Failed to write JSON:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
def update_optode_positions(self, file_a, file_b, save_path):
|
def update_optode_positions(self, file_a, file_b, save_path):
|
||||||
|
|
||||||
@@ -1221,6 +1250,47 @@ class UpdateEventsWindow(QWidget):
|
|||||||
write_raw_snirf(raw, save_path)
|
write_raw_snirf(raw, save_path)
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_events_to_snirf(self, raw, new_annotations, save_path):
|
||||||
|
raw.set_annotations(new_annotations)
|
||||||
|
write_raw_snirf(raw, save_path)
|
||||||
|
|
||||||
|
def _write_event_mapping_json(
|
||||||
|
self,
|
||||||
|
file_a,
|
||||||
|
file_b,
|
||||||
|
selected_obs,
|
||||||
|
snirf_anchor,
|
||||||
|
boris_anchor,
|
||||||
|
time_shift,
|
||||||
|
mapped_events,
|
||||||
|
save_path
|
||||||
|
):
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
import os
|
||||||
|
|
||||||
|
payload = {
|
||||||
|
"source": {
|
||||||
|
"called_from": self.caller,
|
||||||
|
"snirf_file": os.path.basename(file_a),
|
||||||
|
"boris_file": os.path.basename(file_b),
|
||||||
|
"observation": selected_obs
|
||||||
|
},
|
||||||
|
"alignment": {
|
||||||
|
"snirf_anchor": snirf_anchor,
|
||||||
|
"boris_anchor": boris_anchor,
|
||||||
|
"time_shift_seconds": time_shift
|
||||||
|
},
|
||||||
|
"events": mapped_events,
|
||||||
|
"created_at": datetime.utcnow().isoformat() + "Z"
|
||||||
|
}
|
||||||
|
|
||||||
|
with open(save_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(payload, f, indent=2)
|
||||||
|
|
||||||
|
return save_path
|
||||||
|
|
||||||
|
|
||||||
class ProgressBubble(QWidget):
|
class ProgressBubble(QWidget):
|
||||||
"""
|
"""
|
||||||
A clickable widget displaying a progress bar made of colored rectangles and a label.
|
A clickable widget displaying a progress bar made of colored rectangles and a label.
|
||||||
@@ -2506,7 +2576,7 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
|||||||
|
|
||||||
self.index_texts = [
|
self.index_texts = [
|
||||||
"0 (Export Data to CSV)",
|
"0 (Export Data to CSV)",
|
||||||
# "1 (second image)",
|
"1 (CSV for SPARKS)",
|
||||||
# "2 (third image)",
|
# "2 (third image)",
|
||||||
# "3 (fourth image)",
|
# "3 (fourth image)",
|
||||||
]
|
]
|
||||||
@@ -2658,7 +2728,6 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
|||||||
# Pass the necessary arguments to each method
|
# Pass the necessary arguments to each method
|
||||||
for file_path in selected_file_paths:
|
for file_path in selected_file_paths:
|
||||||
haemo_obj = self.haemo_dict.get(file_path)
|
haemo_obj = self.haemo_dict.get(file_path)
|
||||||
|
|
||||||
if haemo_obj is None:
|
if haemo_obj is None:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -2692,10 +2761,63 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
|||||||
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
|
elif idx == 1:
|
||||||
|
try:
|
||||||
|
suggested_name = f"{file_path}_sparks.csv"
|
||||||
|
|
||||||
|
# Open save dialog
|
||||||
|
save_path, _ = QFileDialog.getSaveFileName(
|
||||||
|
self,
|
||||||
|
"Save SNIRF 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 SNIRF file:\n{e}")
|
||||||
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print(f"No method defined for index {idx}")
|
print(f"No method defined for index {idx}")
|
||||||
|
|
||||||
|
|
||||||
class ClickableLabel(QLabel):
|
class ClickableLabel(QLabel):
|
||||||
def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap):
|
def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
@@ -4321,7 +4443,7 @@ class MainApplication(QMainWindow):
|
|||||||
|
|
||||||
def update_event_markers(self):
|
def update_event_markers(self):
|
||||||
if self.events is None or not self.events.isVisible():
|
if self.events is None or not self.events.isVisible():
|
||||||
self.events = UpdateEventsWindow(self)
|
self.events = UpdateEventsWindow(self, EventUpdateMode.WRITE_SNIRF, "Manual SNIRF Edit")
|
||||||
self.events.show()
|
self.events.show()
|
||||||
|
|
||||||
def open_file_dialog(self):
|
def open_file_dialog(self):
|
||||||
|
|||||||
Reference in New Issue
Block a user