Compare commits
7 Commits
v1.1.7
...
7007478c3b
| Author | SHA1 | Date | |
|---|---|---|---|
| 7007478c3b | |||
| fb728d5033 | |||
| 1b78f1904d | |||
| 9779a63a9c | |||
| 2ecd357aca | |||
| fe4e8904b4 | |||
| 473c945563 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -174,3 +174,5 @@ cython_debug/
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
/individual_images
|
||||
*.xlsx
|
||||
19
changelog.md
19
changelog.md
@@ -1,3 +1,22 @@
|
||||
# Version 1.2.0
|
||||
|
||||
- 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, 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
|
||||
- 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
|
||||
- Fixed a bug causing SCI to not work when HEART_RATE was set to False
|
||||
- Bad channels can now be dealt with by taking no action, removing them completely, or interpolating them based on their neighbours. Interpolation remains the default option
|
||||
- Fixed an underlying deprecation warning
|
||||
- Fixed an issue causing some overlay elements to not render on the brain for certain devices
|
||||
- Fixed a crash when rendering some Inter-Group images with only one participant in a group
|
||||
- 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
|
||||
- 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
|
||||
|
||||
- Fixed a bug where having both a L_FREQ and H_FREQ would cause only the L_FREQ to be used
|
||||
|
||||
349
flares.py
349
flares.py
@@ -21,6 +21,7 @@ import os.path as op
|
||||
import re
|
||||
import traceback
|
||||
from concurrent.futures import ProcessPoolExecutor, as_completed
|
||||
from queue import Empty
|
||||
|
||||
# External library imports
|
||||
import matplotlib.pyplot as plt
|
||||
@@ -53,10 +54,10 @@ from scipy.signal import welch, butter, filtfilt # type: ignore
|
||||
import pywt # type: ignore
|
||||
import neurokit2 as nk # type: ignore
|
||||
|
||||
# Backen visualization needed to be defined for pyinstaller
|
||||
# Backend visualization needed to be defined for pyinstaller
|
||||
import pyvistaqt # type: ignore
|
||||
#import vtkmodules.util.data_model
|
||||
#import vtkmodules.util.execution_model
|
||||
import vtkmodules.util.data_model
|
||||
import vtkmodules.util.execution_model
|
||||
|
||||
# External library imports for mne
|
||||
from mne import (
|
||||
@@ -89,9 +90,10 @@ from mne_nirs.io.fold import fold_channel_specificity # type: ignore
|
||||
from mne_nirs.preprocessing import peak_power # type: ignore
|
||||
from mne_nirs.statistics._glm_level_first import RegressionResults # type: ignore
|
||||
|
||||
|
||||
# Needs to be set for mne
|
||||
os.environ["SUBJECTS_DIR"] = str(data_path()) + "/subjects" # type: ignore
|
||||
|
||||
# TODO: Tidy this up
|
||||
FIXED_CATEGORY_COLORS = {
|
||||
"SCI only": "skyblue",
|
||||
"PSP only": "salmon",
|
||||
@@ -112,10 +114,6 @@ FIXED_CATEGORY_COLORS = {
|
||||
}
|
||||
|
||||
|
||||
AGE: float
|
||||
GENDER: str
|
||||
|
||||
# SECONDS_TO_STRIP: int
|
||||
DOWNSAMPLE: bool
|
||||
DOWNSAMPLE_FREQUENCY: int
|
||||
|
||||
@@ -123,21 +121,37 @@ TRIM: bool
|
||||
SECONDS_TO_KEEP: float
|
||||
|
||||
OPTODE_PLACEMENT: bool
|
||||
SHOW_OPTODE_NAMES: bool
|
||||
|
||||
HEART_RATE: bool
|
||||
|
||||
SHORT_CHANNEL: bool
|
||||
SHORT_CHANNEL_THRESH: float
|
||||
LONG_CHANNEL_THRESH: float
|
||||
|
||||
HEART_RATE: bool
|
||||
SECONDS_TO_STRIP_HR: int
|
||||
MAX_LOW_HR: int
|
||||
MAX_HIGH_HR: int
|
||||
SMOOTHING_WINDOW_HR: int
|
||||
HEART_RATE_WINDOW: int
|
||||
|
||||
SCI: bool
|
||||
SCI_TIME_WINDOW: int
|
||||
SCI_THRESHOLD: float
|
||||
|
||||
SNR: bool
|
||||
# SNR_TIME_WINDOW : int
|
||||
# SNR_TIME_WINDOW : int #TODO: is this needed?
|
||||
SNR_THRESHOLD: float
|
||||
|
||||
PSP: bool
|
||||
PSP_TIME_WINDOW: int
|
||||
PSP_THRESHOLD: float
|
||||
|
||||
BAD_CHANNELS_HANDLING: str
|
||||
MAX_DIST: float
|
||||
MIN_NEIGHBORS: int
|
||||
|
||||
TDDR: bool
|
||||
|
||||
WAVELET: bool
|
||||
@@ -145,57 +159,41 @@ IQR: float
|
||||
WAVELET_TYPE: str
|
||||
WAVELET_LEVEL: int
|
||||
|
||||
HEART_RATE = True # True if heart rate should be calculated. This helps the SCI, PSP, and SNR methods to be more accurate.
|
||||
SECONDS_TO_STRIP_HR =5 # Amount of seconds to temporarily strip from the data to calculate heart rate more effectively. Useful if participant removed cap while still recording.
|
||||
MAX_LOW_HR = 40 # Any heart rate values lower than this will be set to this value.
|
||||
MAX_HIGH_HR = 200 # Any heart rate values higher than this will be set to this value.
|
||||
SMOOTHING_WINDOW_HR = 100 # Heart rate will be calculated as a rolling average over this many amount of samples.
|
||||
HEART_RATE_WINDOW = 25 # Amount of BPM above and below the calculated average to use for a range of resting BPM.
|
||||
|
||||
ENHANCE_NEGATIVE_CORRELATION: bool
|
||||
|
||||
FILTER: bool
|
||||
L_FREQ: float
|
||||
H_FREQ: float
|
||||
L_TRANS_BANDWIDTH: float
|
||||
H_TRANS_BANDWIDTH: float
|
||||
|
||||
SHORT_CHANNEL: bool
|
||||
SHORT_CHANNEL_THRESH: float
|
||||
LONG_CHANNEL_THRESH: float
|
||||
|
||||
RESAMPLE: bool
|
||||
RESAMPLE_FREQ: int
|
||||
STIM_DUR: float
|
||||
HRF_MODEL: str
|
||||
DRIFT_MODEL: str
|
||||
HIGH_PASS: float
|
||||
DRIFT_ORDER: int
|
||||
FIR_DELAYS: range
|
||||
MIN_ONSET: int
|
||||
OVERSAMPLING: int
|
||||
REMOVE_EVENTS: list
|
||||
SHORT_CHANNEL_REGRESSION: bool
|
||||
|
||||
NOISE_MODEL: str
|
||||
BINS: int
|
||||
N_JOBS: int
|
||||
|
||||
TIME_WINDOW_START: int
|
||||
TIME_WINDOW_END: int
|
||||
MAX_WORKERS: int
|
||||
VERBOSITY: bool
|
||||
|
||||
DRIFT_MODEL: str
|
||||
|
||||
VERBOSITY = True
|
||||
|
||||
# FIXME: Shouldn't need each ordering - just order it before checking
|
||||
FIXED_CATEGORY_COLORS = {
|
||||
"SCI only": "skyblue",
|
||||
"PSP only": "salmon",
|
||||
"SNR only": "lightgreen",
|
||||
"PSP + SCI": "orange",
|
||||
"SCI + SNR": "violet",
|
||||
"PSP + SNR": "gold",
|
||||
"SCI + PSP": "orange",
|
||||
"SNR + SCI": "violet",
|
||||
"SNR + PSP": "gold",
|
||||
"PSP + SNR + SCI": "gray",
|
||||
"SCI + PSP + SNR": "gray",
|
||||
"SCI + SNR + PSP": "gray",
|
||||
"PSP + SCI + SNR": "gray",
|
||||
"PSP + SNR + SCI": "gray",
|
||||
"SNR + SCI + PSP": "gray",
|
||||
"SNR + PSP + SCI": "gray",
|
||||
}
|
||||
|
||||
|
||||
AGE = 25
|
||||
AGE = 25 # Assume 25 if not set from the GUI. This will result in a reasonable PPF
|
||||
GENDER = ""
|
||||
GROUP = "Default"
|
||||
|
||||
# These are parameters that are required for the analysis
|
||||
REQUIRED_KEYS: dict[str, Any] = {
|
||||
|
||||
# "SECONDS_TO_STRIP": int,
|
||||
@@ -262,7 +260,7 @@ PLATFORM_NAME = platform.system().lower()
|
||||
# Configure logging to file with timestamps and realtime flush
|
||||
if PLATFORM_NAME == 'darwin':
|
||||
logging.basicConfig(
|
||||
filename=os.path.join(os.path.dirname(sys.executable), "../../../fnirs_analysis.log"),
|
||||
filename=os.path.join(os.path.dirname(sys.executable), "../../../fnirs_analysis.log"), # Needed to get out of the bundled application
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(processName)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S',
|
||||
@@ -320,8 +318,6 @@ def set_metadata(file_path, metadata: dict[str, Any]) -> None:
|
||||
val = file_metadata.get(key, None)
|
||||
if val not in (None, '', [], {}, ()): # check for "empty" values
|
||||
globals()[key] = val
|
||||
from queue import Empty # This works with multiprocessing.Manager().Queue()
|
||||
|
||||
|
||||
def gui_entry(config: dict[str, Any], gui_queue: Queue, progress_queue: Queue) -> None:
|
||||
def forward_progress():
|
||||
@@ -825,7 +821,7 @@ def get_hbo_hbr_picks(raw):
|
||||
return hbo_picks, hbr_picks, hbo_wl, hbr_wl
|
||||
|
||||
|
||||
def interpolate_fNIRS_bads_weighted_average(raw, bad_channels, max_dist=0.03, min_neighbors=2):
|
||||
def interpolate_fNIRS_bads_weighted_average(raw, max_dist=0.03, min_neighbors=2):
|
||||
"""
|
||||
Interpolate bad fNIRS channels using a distance-weighted average of nearby good channels.
|
||||
|
||||
@@ -1117,17 +1113,17 @@ def mark_bads(raw, bad_sci, bad_snr, bad_psp):
|
||||
|
||||
|
||||
def filter_the_data(raw_haemo):
|
||||
# --- STEP 5: Filtering (0.01–0.2 Hz bandpass) ---
|
||||
# --- STEP 5: Filtering (0.01-0.2 Hz bandpass) ---
|
||||
fig_filter = raw_haemo.compute_psd(fmax=3).plot(
|
||||
average=True, color="r", show=False, amplitude=True
|
||||
)
|
||||
|
||||
if L_FREQ == 0 and H_FREQ != 0:
|
||||
raw_haemo = raw_haemo.filter(l_freq=None, h_freq=H_FREQ, h_trans_bandwidth=0.02)
|
||||
raw_haemo = raw_haemo.filter(l_freq=None, h_freq=H_FREQ, h_trans_bandwidth=H_TRANS_BANDWIDTH)
|
||||
elif L_FREQ != 0 and H_FREQ == 0:
|
||||
raw_haemo = raw_haemo.filter(l_freq=L_FREQ, h_freq=None, l_trans_bandwidth=0.002)
|
||||
raw_haemo = raw_haemo.filter(l_freq=L_FREQ, h_freq=None, l_trans_bandwidth=L_TRANS_BANDWIDTH)
|
||||
elif L_FREQ != 0 and H_FREQ != 0:
|
||||
raw_haemo = raw_haemo.filter(l_freq=L_FREQ, h_freq=H_FREQ, l_trans_bandwidth=0.002, h_trans_bandwidth=0.02)
|
||||
raw_haemo = raw_haemo.filter(l_freq=L_FREQ, h_freq=H_FREQ, l_trans_bandwidth=L_TRANS_BANDWIDTH, h_trans_bandwidth=H_TRANS_BANDWIDTH)
|
||||
else:
|
||||
print("No filter")
|
||||
#raw_haemo = raw_haemo.filter(l_freq=None, h_freq=0.4, h_trans_bandwidth=0.2)
|
||||
@@ -1258,7 +1254,7 @@ def epochs_calculations(raw_haemo, events, event_dict):
|
||||
continue
|
||||
|
||||
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)
|
||||
data_segment = data[times_mask]
|
||||
times_segment = evoked.times[times_mask]
|
||||
@@ -1307,33 +1303,53 @@ def epochs_calculations(raw_haemo, events, event_dict):
|
||||
|
||||
def make_design_matrix(raw_haemo, short_chans):
|
||||
|
||||
raw_haemo.resample(1, npad="auto")
|
||||
events_to_remove = REMOVE_EVENTS
|
||||
|
||||
filtered_annotations = [ann for ann in raw_haemo.annotations if ann['description'] not in events_to_remove]
|
||||
|
||||
new_annot = Annotations(
|
||||
onset=[ann['onset'] for ann in filtered_annotations],
|
||||
duration=[ann['duration'] for ann in filtered_annotations],
|
||||
description=[ann['description'] for ann in filtered_annotations]
|
||||
)
|
||||
|
||||
# Set the new annotations
|
||||
raw_haemo.set_annotations(new_annot)
|
||||
|
||||
if RESAMPLE:
|
||||
raw_haemo.resample(RESAMPLE_FREQ, npad="auto")
|
||||
raw_haemo._data = raw_haemo._data * 1e6
|
||||
try:
|
||||
short_chans.resample(RESAMPLE_FREQ)
|
||||
except:
|
||||
pass
|
||||
|
||||
# 2) Create design matrix
|
||||
if SHORT_CHANNEL:
|
||||
short_chans.resample(1)
|
||||
if SHORT_CHANNEL_REGRESSION:
|
||||
design_matrix = make_first_level_design_matrix(
|
||||
raw=raw_haemo,
|
||||
hrf_model='fir',
|
||||
stim_dur=0.5,
|
||||
fir_delays=range(15),
|
||||
stim_dur=STIM_DUR,
|
||||
hrf_model=HRF_MODEL,
|
||||
drift_model=DRIFT_MODEL,
|
||||
high_pass=0.01,
|
||||
oversampling=1,
|
||||
min_onset=-125,
|
||||
high_pass=HIGH_PASS,
|
||||
drift_order=DRIFT_ORDER,
|
||||
fir_delays=range(15),
|
||||
add_regs=short_chans.get_data().T,
|
||||
add_reg_names=short_chans.ch_names
|
||||
add_reg_names=short_chans.ch_names,
|
||||
min_onset=MIN_ONSET,
|
||||
oversampling=OVERSAMPLING
|
||||
)
|
||||
else:
|
||||
design_matrix = make_first_level_design_matrix(
|
||||
raw=raw_haemo,
|
||||
hrf_model='fir',
|
||||
stim_dur=0.5,
|
||||
fir_delays=range(15),
|
||||
stim_dur=STIM_DUR,
|
||||
hrf_model=HRF_MODEL,
|
||||
drift_model=DRIFT_MODEL,
|
||||
high_pass=0.01,
|
||||
oversampling=1,
|
||||
min_onset=-125,
|
||||
high_pass=HIGH_PASS,
|
||||
drift_order=DRIFT_ORDER,
|
||||
fir_delays=range(15),
|
||||
min_onset=MIN_ONSET,
|
||||
oversampling=OVERSAMPLING
|
||||
)
|
||||
|
||||
print(design_matrix.head())
|
||||
@@ -2569,7 +2585,10 @@ def plot_fir_model_results(df, raw_haemo, dm, selected_event, l_bound, u_bound):
|
||||
dm_cols_activity = np.where([f"{selected_event}" in c for c in dm.columns])[0]
|
||||
dm = dm[[dm.columns[i] for i in dm_cols_activity]]
|
||||
|
||||
try:
|
||||
lme = smf.mixedlm("theta ~ -1 + delay:TidyCond:Chroma", df, groups=df["ID"]).fit()
|
||||
except:
|
||||
lme = smf.ols("theta ~ -1 + delay:TidyCond:Chroma", df, groups=df["ID"]).fit() # type: ignore
|
||||
|
||||
df_sum = statsmodels_to_results(lme)
|
||||
df_sum["delay"] = [int(n) for n in df_sum["delay"]]
|
||||
@@ -3310,18 +3329,20 @@ def hr_calc(raw):
|
||||
return fig, hr1, hr2, low, high
|
||||
|
||||
|
||||
|
||||
|
||||
def process_participant(file_path, progress_callback=None):
|
||||
|
||||
fig_individual: dict[str, Figure] = {}
|
||||
|
||||
# Step 1: Load
|
||||
# Step 1: Preprocessing
|
||||
raw = load_snirf(file_path)
|
||||
fig_raw = raw.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="Loaded Raw", show=False)
|
||||
fig_individual["Loaded Raw"] = fig_raw
|
||||
if progress_callback: progress_callback(1)
|
||||
logger.info("1")
|
||||
|
||||
logger.info("Step 1 Completed.")
|
||||
|
||||
# Step 2: Trimming
|
||||
if TRIM:
|
||||
if hasattr(raw, 'annotations') and len(raw.annotations) > 0:
|
||||
# Get time of first event
|
||||
@@ -3329,17 +3350,16 @@ def process_participant(file_path, progress_callback=None):
|
||||
trim_time = max(0, first_event_time - SECONDS_TO_KEEP) # Ensure we don't go negative
|
||||
raw.crop(tmin=trim_time)
|
||||
# Shift annotation onsets to match new t=0
|
||||
import mne
|
||||
|
||||
ann = raw.annotations
|
||||
ann_shifted = mne.Annotations(
|
||||
ann_shifted = Annotations(
|
||||
onset=ann.onset - trim_time, # shift to start at zero
|
||||
duration=ann.duration,
|
||||
description=ann.description
|
||||
)
|
||||
data = raw.get_data()
|
||||
info = raw.info.copy()
|
||||
raw = mne.io.RawArray(data, info)
|
||||
raw = RawArray(data, info)
|
||||
raw.set_annotations(ann_shifted)
|
||||
|
||||
logger.info(f"Trimmed raw data: start at {trim_time}s (5s before first event), t=0 at new start")
|
||||
@@ -3349,185 +3369,178 @@ def process_participant(file_path, progress_callback=None):
|
||||
fig_trimmed = raw.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="Trimmed Raw", show=False)
|
||||
fig_individual["Trimmed Raw"] = fig_trimmed
|
||||
if progress_callback: progress_callback(2)
|
||||
logger.info("2")
|
||||
logger.info("Step 2 Completed.")
|
||||
|
||||
# Step 1.5: Verify optode positions
|
||||
# Step 3: Verify Optode Placement
|
||||
if OPTODE_PLACEMENT:
|
||||
fig_optodes = raw.plot_sensors(show_names=True, to_sphere=True, show=False) # type: ignore
|
||||
fig_optodes = raw.plot_sensors(show_names=SHOW_OPTODE_NAMES, to_sphere=True, show=False) # type: ignore
|
||||
fig_individual["Plot Sensors"] = fig_optodes
|
||||
if progress_callback: progress_callback(3)
|
||||
logger.info("3")
|
||||
logger.info("Step 3 Completed.")
|
||||
|
||||
# Step 2: Bad from SCI
|
||||
# Step 4: Short/Long Channels
|
||||
if SHORT_CHANNEL:
|
||||
short_chans = get_short_channels(raw, max_dist=SHORT_CHANNEL_THRESH)
|
||||
fig_short_chans = short_chans.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="Short Channels Only", show=False)
|
||||
fig_individual["short"] = fig_short_chans
|
||||
else:
|
||||
short_chans = None
|
||||
get_long_channels(raw, min_dist=SHORT_CHANNEL_THRESH, max_dist=LONG_CHANNEL_THRESH) # Don't update the existing raw
|
||||
if progress_callback: progress_callback(4)
|
||||
logger.info("Step 4 Completed.")
|
||||
|
||||
# Step 5: Heart Rate
|
||||
if HEART_RATE:
|
||||
fig, hr1, hr2, low, high = hr_calc(raw)
|
||||
fig_individual["PSD"] = fig
|
||||
fig_individual['HeartRate_PSD'] = hr1
|
||||
fig_individual['HeartRate_Time'] = hr2
|
||||
if progress_callback: progress_callback(4)
|
||||
logger.info("4")
|
||||
if progress_callback: progress_callback(5)
|
||||
logger.info("Step 5 Completed.")
|
||||
|
||||
# Step 6: Scalp Coupling Index
|
||||
bad_sci = []
|
||||
if SCI:
|
||||
if HEART_RATE:
|
||||
bad_sci, fig_sci_1, fig_sci_2 = calculate_scalp_coupling(raw, low, high)
|
||||
else:
|
||||
bad_sci, fig_sci_1, fig_sci_2 = calculate_scalp_coupling(raw)
|
||||
fig_individual["SCI1"] = fig_sci_1
|
||||
fig_individual["SCI2"] = fig_sci_2
|
||||
if progress_callback: progress_callback(5)
|
||||
logger.info("5")
|
||||
if progress_callback: progress_callback(6)
|
||||
logger.info("Step 6 Completed.")
|
||||
|
||||
# Step 2: Bad from SNR
|
||||
# Step 7: Signal to Noise Ratio
|
||||
bad_snr = []
|
||||
if SNR:
|
||||
bad_snr, fig_snr = calculate_signal_noise_ratio(raw)
|
||||
fig_individual["SNR1"] = fig_snr
|
||||
if progress_callback: progress_callback(6)
|
||||
logger.info("6")
|
||||
if progress_callback: progress_callback(7)
|
||||
logger.info("Step 7 Completed.")
|
||||
|
||||
# Step 3: Bad from PSP
|
||||
# Step 8: Peak Spectral Power
|
||||
bad_psp = []
|
||||
if PSP:
|
||||
bad_psp, fig_psp1, fig_psp2 = calculate_peak_power(raw)
|
||||
fig_individual["PSP1"] = fig_psp1
|
||||
fig_individual["PSP2"] = fig_psp2
|
||||
if progress_callback: progress_callback(7)
|
||||
logger.info("7")
|
||||
if progress_callback: progress_callback(8)
|
||||
logger.info("Step 8 Completed.")
|
||||
|
||||
# Step 4: Mark the bad channels
|
||||
# Step 9: Bad Channels Handling
|
||||
if BAD_CHANNELS_HANDLING != "None":
|
||||
raw, fig_dropped, fig_raw_before, bad_channels = mark_bads(raw, bad_sci, bad_snr, bad_psp)
|
||||
if fig_dropped and fig_raw_before is not None:
|
||||
fig_individual["fig2"] = fig_dropped
|
||||
fig_individual["fig3"] = fig_raw_before
|
||||
if progress_callback: progress_callback(8)
|
||||
logger.info("8")
|
||||
|
||||
# Step 5: Interpolate the bad channels
|
||||
if bad_channels:
|
||||
raw, fig_raw_after = interpolate_fNIRS_bads_weighted_average(raw, bad_channels)
|
||||
if BAD_CHANNELS_HANDLING == "Interpolate":
|
||||
raw, fig_raw_after = interpolate_fNIRS_bads_weighted_average(raw, max_dist=MAX_DIST, min_neighbors=MIN_NEIGHBORS)
|
||||
fig_individual["fig4"] = fig_raw_after
|
||||
if progress_callback: progress_callback(9)
|
||||
logger.info("9")
|
||||
elif BAD_CHANNELS_HANDLING == "Remove":
|
||||
pass
|
||||
#TODO: Is there more needed here?
|
||||
|
||||
# Step 6: Optical Density
|
||||
if progress_callback: progress_callback(9)
|
||||
logger.info("Step 9 Completed.")
|
||||
|
||||
# Step 10: Optical Density
|
||||
raw_od = optical_density(raw)
|
||||
fig_raw_od = raw_od.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="Optical Density", show=False)
|
||||
fig_individual["Optical Density"] = fig_raw_od
|
||||
if progress_callback: progress_callback(10)
|
||||
logger.info("10")
|
||||
logger.info("Step 10 Completed.")
|
||||
|
||||
# Step 7: TDDR
|
||||
# Step 11: Temporal Derivative Distribution Repair Filtering
|
||||
if TDDR:
|
||||
raw_od = temporal_derivative_distribution_repair(raw_od)
|
||||
fig_raw_od_tddr = raw_od.plot(duration=raw.times[-1], n_channels=raw.info['nchan'], title="After TDDR (Motion Correction)", show=False)
|
||||
fig_individual["TDDR"] = fig_raw_od_tddr
|
||||
if progress_callback: progress_callback(11)
|
||||
logger.info("11")
|
||||
|
||||
logger.info("Step 11 Completed.")
|
||||
|
||||
# Step 12: Wavelet Filtering
|
||||
if WAVELET:
|
||||
raw_od, fig = calculate_and_apply_wavelet(raw_od)
|
||||
fig_individual["Wavelet"] = fig
|
||||
if progress_callback: progress_callback(12)
|
||||
logger.info("12")
|
||||
logger.info("Step 12 Completed.")
|
||||
|
||||
|
||||
# Step 8: BLL
|
||||
# Step 13: Haemoglobin Concentration
|
||||
raw_haemo = beer_lambert_law(raw_od, ppf=calculate_dpf(file_path))
|
||||
fig_raw_haemo_bll = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="HbO and HbR Signals", show=False)
|
||||
fig_individual["BLL"] = fig_raw_haemo_bll
|
||||
if progress_callback: progress_callback(13)
|
||||
logger.info("13")
|
||||
logger.info("Step 13 Completed.")
|
||||
|
||||
# Step 9: ENC
|
||||
# Step 14: Enhance Negative Correlation
|
||||
if ENHANCE_NEGATIVE_CORRELATION:
|
||||
raw_haemo = enhance_negative_correlation(raw_haemo)
|
||||
fig_raw_haemo_enc = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="HbO and HbR Signals", show=False)
|
||||
fig_raw_haemo_enc = raw_haemo.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="Enhance Negative Correlation", show=False)
|
||||
fig_individual["ENC"] = fig_raw_haemo_enc
|
||||
if progress_callback: progress_callback(14)
|
||||
logger.info("14")
|
||||
logger.info("Step 14 Completed.")
|
||||
|
||||
# Step 10: Filter
|
||||
# Step 15: Filter
|
||||
if FILTER:
|
||||
raw_haemo, fig_filter, fig_raw_haemo_filter = filter_the_data(raw_haemo)
|
||||
fig_individual["filter1"] = fig_filter
|
||||
fig_individual["filter2"] = fig_raw_haemo_filter
|
||||
if progress_callback: progress_callback(15)
|
||||
logger.info("15")
|
||||
logger.info("Step 15 Completed.")
|
||||
|
||||
# Step 11: Get short / long channels
|
||||
if SHORT_CHANNEL:
|
||||
short_chans = get_short_channels(raw_haemo, max_dist=SHORT_CHANNEL_THRESH)
|
||||
fig_short_chans = short_chans.plot(duration=raw_haemo.times[-1], n_channels=raw_haemo.info['nchan'], title="Short Channels Only", show=False)
|
||||
fig_individual["short"] = fig_short_chans
|
||||
else:
|
||||
short_chans = None
|
||||
raw_haemo = get_long_channels(raw_haemo, min_dist=SHORT_CHANNEL_THRESH, max_dist=LONG_CHANNEL_THRESH)
|
||||
if progress_callback: progress_callback(16)
|
||||
logger.info("16")
|
||||
|
||||
# Step 12: Events from annotations
|
||||
# Step 16: Extracting Events
|
||||
events, event_dict = events_from_annotations(raw_haemo)
|
||||
fig_events = plot_events(events, event_id=event_dict, sfreq=raw_haemo.info["sfreq"], show=False)
|
||||
fig_individual["events"] = fig_events
|
||||
if progress_callback: progress_callback(17)
|
||||
logger.info("17")
|
||||
if progress_callback: progress_callback(16)
|
||||
logger.info("Step 16 Completed.")
|
||||
|
||||
# Step 13: Epoch calculations
|
||||
# Step 17: Epoch Calculations
|
||||
epochs, fig_epochs = epochs_calculations(raw_haemo, events, event_dict)
|
||||
for name, fig in fig_epochs: # Unpack the tuple here
|
||||
fig_individual[f"epochs_{name}"] = fig # Store only the figure, not the name
|
||||
if progress_callback: progress_callback(18)
|
||||
logger.info("18")
|
||||
|
||||
# Step 14: Design Matrix
|
||||
events_to_remove = REMOVE_EVENTS
|
||||
|
||||
filtered_annotations = [ann for ann in raw.annotations if ann['description'] not in events_to_remove]
|
||||
|
||||
new_annot = Annotations(
|
||||
onset=[ann['onset'] for ann in filtered_annotations],
|
||||
duration=[ann['duration'] for ann in filtered_annotations],
|
||||
description=[ann['description'] for ann in filtered_annotations]
|
||||
)
|
||||
|
||||
# Set the new annotations
|
||||
raw_haemo.set_annotations(new_annot)
|
||||
for name, fig in fig_epochs:
|
||||
fig_individual[f"epochs_{name}"] = fig
|
||||
if progress_callback: progress_callback(17)
|
||||
logger.info("Step 17 Completed.")
|
||||
|
||||
# Step 18: Design Matrix
|
||||
design_matrix, fig_design_matrix = make_design_matrix(raw_haemo, short_chans)
|
||||
fig_individual["Design Matrix"] = fig_design_matrix
|
||||
if progress_callback: progress_callback(19)
|
||||
logger.info("19")
|
||||
if progress_callback: progress_callback(18)
|
||||
logger.info("Step 18 Completed.")
|
||||
|
||||
# Step 15: Run GLM
|
||||
glm_est = run_glm(raw_haemo, design_matrix)
|
||||
|
||||
# Step 19: Run GLM
|
||||
glm_est = run_glm(raw_haemo, design_matrix, noise_model=NOISE_MODEL, bins=BINS, n_jobs=N_JOBS, verbose=VERBOSITY)
|
||||
# Not used AppData\Local\Packages\PythonSoftwareFoundation.Python.3.13_qbz5n2kfra8p0\LocalCache\local-packages\Python313\site-packages\nilearn\glm\contrasts.py
|
||||
# Yes used AppData\Local\Packages\PythonSoftwareFoundation.Python.3.13_qbz5n2kfra8p0\LocalCache\local-packages\Python313\site-packages\mne_nirs\utils\_io.py
|
||||
|
||||
# The p-value is calculated from this t-statistic using the Student’s t-distribution with appropriate degrees of freedom.
|
||||
# The p-value is calculated from this t-statistic using the Student's t-distribution with appropriate degrees of freedom.
|
||||
# p_value = 2 * stats.t.cdf(-abs(t_statistic), df)
|
||||
# It is a two-tailed p-value.
|
||||
# It says how likely it is to observe the effect you did (or something more extreme) if the true effect was zero (null hypothesis).
|
||||
# A small p-value (e.g., < 0.05) suggests the effect is unlikely to be zero — it’s "statistically significant."
|
||||
# A small p-value (e.g., < 0.05) suggests the effect is unlikely to be zero — it's "statistically significant."
|
||||
# A large p-value means the data do not provide strong evidence that the effect is different from zero.
|
||||
|
||||
|
||||
if progress_callback: progress_callback(20)
|
||||
logger.info("20")
|
||||
if progress_callback: progress_callback(19)
|
||||
logger.info("19")
|
||||
|
||||
# Step 16: Plot GLM results
|
||||
# Step 20: Generate GLM Results
|
||||
fig_glm_result = plot_glm_results(file_path, raw_haemo, glm_est, design_matrix)
|
||||
for name, fig in fig_glm_result:
|
||||
fig_individual[f"GLM {name}"] = fig
|
||||
if progress_callback: progress_callback(21)
|
||||
logger.info("21")
|
||||
if progress_callback: progress_callback(20)
|
||||
logger.info("20")
|
||||
|
||||
# Step 17: Plot channel significance
|
||||
# Step 21: Generate Channel Significance
|
||||
fig_significance = individual_significance(raw_haemo, glm_est)
|
||||
for name, fig in fig_significance:
|
||||
fig_individual[f"Significance {name}"] = fig
|
||||
if progress_callback: progress_callback(22)
|
||||
logger.info("22")
|
||||
if progress_callback: progress_callback(21)
|
||||
logger.info("21")
|
||||
|
||||
# Step 18: cha, con, roi
|
||||
# Step 22: Generate Channel, Region of Interest, and Contrast Results
|
||||
cha = glm_est.to_dataframe()
|
||||
|
||||
# HACK: Comment out line 588 (self._renderer.show()) in _brain.py from MNE
|
||||
@@ -3580,10 +3593,10 @@ def process_participant(file_path, progress_callback=None):
|
||||
|
||||
contrast_dict[condition] = contrast_vector
|
||||
|
||||
if progress_callback: progress_callback(23)
|
||||
logger.info("23")
|
||||
if progress_callback: progress_callback(22)
|
||||
logger.info("22")
|
||||
|
||||
# Compute contrast results
|
||||
# Step 23: Compute Contrast Results
|
||||
contrast_results = {}
|
||||
|
||||
for cond, contrast_vector in contrast_dict.items():
|
||||
@@ -3594,10 +3607,10 @@ def process_participant(file_path, progress_callback=None):
|
||||
|
||||
cha["ID"] = file_path
|
||||
|
||||
if progress_callback: progress_callback(24)
|
||||
logger.info("24")
|
||||
|
||||
if progress_callback: progress_callback(23)
|
||||
logger.info("23")
|
||||
|
||||
# Step 24: Finishing Up
|
||||
fig_bytes = convert_fig_dict_to_png_bytes(fig_individual)
|
||||
|
||||
sanitize_paths_for_pickle(raw_haemo, epochs)
|
||||
|
||||
426
main.py
426
main.py
@@ -22,11 +22,11 @@ import subprocess
|
||||
from pathlib import Path, PurePosixPath
|
||||
from datetime import datetime
|
||||
from multiprocessing import Process, current_process, freeze_support, Manager
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from enum import Enum, auto
|
||||
|
||||
# External library imports
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import psutil
|
||||
import requests
|
||||
|
||||
@@ -46,7 +46,7 @@ from PySide6.QtGui import QAction, QKeySequence, QIcon, QIntValidator, QDoubleVa
|
||||
from PySide6.QtSvgWidgets import QSvgWidget # needed to show svgs when app is not frozen
|
||||
|
||||
|
||||
CURRENT_VERSION = "1.0.0"
|
||||
CURRENT_VERSION = "1.2.0"
|
||||
|
||||
API_URL = "https://git.research.dezeeuw.ca/api/v1/repos/tyler/flares/releases"
|
||||
API_URL_SECONDARY = "https://git.research2.dezeeuw.ca/api/v1/repos/tyler/flares/releases"
|
||||
@@ -58,7 +58,6 @@ SECTIONS = [
|
||||
{
|
||||
"title": "Preprocessing",
|
||||
"params": [
|
||||
# {"name": "SECONDS_TO_STRIP", "default": 0, "type": int, "help": "Seconds to remove from beginning of all loaded snirf files. Setting this to 0 will remove nothing from the files."},
|
||||
{"name": "DOWNSAMPLE", "default": True, "type": bool, "help": "Should the snirf files be downsampled? If this is set to True, DOWNSAMPLE_FREQUENCY will be used as the target frequency to downsample to."},
|
||||
{"name": "DOWNSAMPLE_FREQUENCY", "default": 25, "type": int, "help": "Frequency (Hz) to downsample to. If this is set higher than the input data, new data will be interpolated. Only used if DOWNSAMPLE is set to True"},
|
||||
]
|
||||
@@ -74,12 +73,26 @@ SECTIONS = [
|
||||
"title": "Verify Optode Placement",
|
||||
"params": [
|
||||
{"name": "OPTODE_PLACEMENT", "default": True, "type": bool, "help": "Generate an image for each participant outlining their optode placement."},
|
||||
{"name": "SHOW_OPTODE_NAMES", "default": True, "type": bool, "help": "Should the optode names be written next to their location or not."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Short/Long Channels",
|
||||
"params": [
|
||||
{"name": "SHORT_CHANNEL", "default": True, "type": bool, "help": "This should be set to True if the data has a short channel present in the data."},
|
||||
{"name": "SHORT_CHANNEL_THRESH", "default": 0.015, "type": float, "help": "The maximum distance the short channel can be in metres."},
|
||||
{"name": "LONG_CHANNEL_THRESH", "default": 0.045, "type": float, "help": "The maximum distance the long channel can be in metres."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Heart Rate",
|
||||
"params": [
|
||||
{"name": "HEART_RATE", "default": True, "type": bool, "help": "Attempt to calculate the participants heart rate."},
|
||||
{"name": "SECONDS_TO_STRIP_HR", "default": 5, "type": int, "help": "Will remove this many seconds from the start and end of the file. Useful if recording before cap is firmly placed, or participant removes cap while still recording."},
|
||||
{"name": "MAX_LOW_HR", "default": 40, "type": int, "help": "Any heart rate windows that average below this value will be rounded up to this value."},
|
||||
{"name": "MAX_HIGH_HR", "default": 200, "type": int, "help": "Any heart rate windows that average above this value will be rounded down to this value."},
|
||||
{"name": "SMOOTHING_WINDOW_HR", "default": 100, "type": int, "help": "How many individual data points to smooth into a single window."},
|
||||
{"name": "HEART_RATE_WINDOW", "default": 25, "type": int, "help": "Used for visualization. Shows the range of the calculated heart rate +- this value."},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -94,14 +107,12 @@ SECTIONS = [
|
||||
"title": "Signal to Noise Ratio",
|
||||
"params": [
|
||||
{"name": "SNR", "default": True, "type": bool, "help": "Calculate and mark channels bad based on their Signal to Noise Ratio. This metric calculates how much of the observed signal was noise versus how much of it was a useful signal."},
|
||||
# {"name": "SNR_TIME_WINDOW", "default": -1, "type": int, "help": "SNR time window."},
|
||||
{"name": "SNR_THRESHOLD", "default": 5.0, "type": float, "help": "SNR threshold (dB). A typical scale would be 0-25, but it is possible for values to be both above and below this range. Higher values correspond to a better signal. If SNR is True, any channels lower than this value will be marked as bad."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Peak Spectral Power",
|
||||
"params": [
|
||||
|
||||
{"name": "PSP", "default": True, "type": bool, "help": "Calculate and mark channels bad based on their Peak Spectral Power. This metric calculates the amplitude or strength of a frequency component that is most prominent in a particular frequency range or spectrum."},
|
||||
{"name": "PSP_TIME_WINDOW", "default": 3, "type": int, "help": "Independent PSP calculations will be perfomed in a time window for the duration of the value provided, until the end of the file is reached."},
|
||||
{"name": "PSP_THRESHOLD", "default": 0.1, "type": float, "help": "PSP threshold. A typical scale would be 0-0.5, but it is possible for values to be above this range. Higher values correspond to a better signal. If PSP is True, any channels lower than this value will be marked as bad."},
|
||||
@@ -110,15 +121,15 @@ SECTIONS = [
|
||||
{
|
||||
"title": "Bad Channels Handling",
|
||||
"params": [
|
||||
# {"name": "NOT_IMPLEMENTED", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
||||
# {"name": "NOT_IMPLEMENTED", "default": 3, "type": int, "help": "PSP time window."},
|
||||
# {"name": "NOT_IMPLEMENTED", "default": 0.1, "type": float, "help": "PSP threshold."},
|
||||
{"name": "BAD_CHANNELS_HANDLING", "default": [], "type": list, "options": ["Interpolate", "Remove", "None"], "exclusive": True, "help": "How should we deal with the bad channels that occurred? Note: Some analysis options will only work when this is set to 'Interpolate'."},
|
||||
{"name": "MAX_DIST", "default": 0.03, "type": float, "help": "The maximum distance to look for neighbours when interpolating. Used only when BAD_CHANNELS_HANDLING is set to 'Interpolate'."},
|
||||
{"name": "MIN_NEIGHBORS", "default": 2, "type": int, "help": "The minimumn amount of neighbours needed within the MAX_DIST parameter. Used only when BAD_CHANNELS_HANDLING is set to 'Interpolate'."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Optical Density",
|
||||
"params": [
|
||||
# Intentionally empty (TODO)
|
||||
# NOTE: Intentionally empty
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -139,7 +150,7 @@ SECTIONS = [
|
||||
{
|
||||
"title": "Haemoglobin Concentration",
|
||||
"params": [
|
||||
# Intentionally empty (TODO)
|
||||
# NOTE: Intentionally empty
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -154,24 +165,18 @@ SECTIONS = [
|
||||
{"name": "FILTER", "default": True, "type": bool, "help": "Filter the data."},
|
||||
{"name": "L_FREQ", "default": 0.005, "type": float, "help": "Any frequencies lower than this value will be removed."},
|
||||
{"name": "H_FREQ", "default": 0.3, "type": float, "help": "Any frequencies higher than this value will be removed."},
|
||||
{"name": "L_TRANS_BANDWIDTH", "default": 0.002, "type": float, "help": "How wide the transitional period should be so the data doesn't just drop off on the lower bound."},
|
||||
{"name": "H_TRANS_BANDWIDTH", "default": 0.002, "type": float, "help": "How wide the transitional period should be so the data doesn't just drop off on the upper bound."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Short/Long Channels",
|
||||
"params": [
|
||||
{"name": "SHORT_CHANNEL", "default": True, "type": bool, "help": "This should be set to True if the data has a short channel present in the data."},
|
||||
{"name": "SHORT_CHANNEL_THRESH", "default": 0.015, "type": float, "help": "The maximum distance the short channel can be in metres."},
|
||||
{"name": "LONG_CHANNEL_THRESH", "default": 0.045, "type": float, "help": "The maximum distance the long channel can be in metres."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Extracting Events",
|
||||
"title": "Extracting Events*",
|
||||
"params": [
|
||||
#{"name": "EVENTS", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Epoch Calculations",
|
||||
"title": "Epoch Calculations*",
|
||||
"params": [
|
||||
#{"name": "EVENTS", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
||||
]
|
||||
@@ -179,18 +184,27 @@ SECTIONS = [
|
||||
{
|
||||
"title": "Design Matrix",
|
||||
"params": [
|
||||
{"name": "RESAMPLE", "default": True, "type": bool, "help": "The length of your stimulus."},
|
||||
{"name": "RESAMPLE_FREQ", "default": 1, "type": int, "help": "The length of your stimulus."},
|
||||
|
||||
{"name": "STIM_DUR", "default": 0.5, "type": float, "help": "The length of your stimulus."},
|
||||
{"name": "HRF_MODEL", "default": "fir", "type": str, "help": "Specifies the hemodynamic response function."},
|
||||
{"name": "DRIFT_MODEL", "default": "cosine", "type": str, "help": "Specifies the desired drift model."},
|
||||
{"name": "HIGH_PASS", "default": 0.01, "type": float, "help": "High-pass frequency in case of a cosine model (in Hz)."},
|
||||
{"name": "DRIFT_ORDER", "default": 1, "type": int, "help": "Order of the drift model (in case it is polynomial)"},
|
||||
{"name": "FIR_DELAYS", "default": "None", "type": range, "help": "In case of FIR design, yields the array of delays used in the FIR model (in scans)."},
|
||||
{"name": "MIN_ONSET", "default": -24, "type": int, "help": "Minimal onset relative to frame times (in seconds)"},
|
||||
{"name": "OVERSAMPLING", "default": 50, "type": int, "help": "Oversampling factor used in temporal convolutions."},
|
||||
{"name": "REMOVE_EVENTS", "default": "None", "type": list, "help": "Remove events matching the names provided before generating the Design Matrix"},
|
||||
{"name": "DRIFT_MODEL", "default": "cosine", "type": str, "help": "Drift model for GLM."},
|
||||
# {"name": "DURATION_BETWEEN_ACTIVITIES", "default": 35, "type": int, "help": "Time between activities (s)."},
|
||||
# {"name": "SHORT_CHANNEL_REGRESSION", "default": True, "type": bool, "help": "Use short channel regression."},
|
||||
{"name": "SHORT_CHANNEL_REGRESSION", "default": True, "type": bool, "help": "Whether to use short channel regression and regress out the short channels. Requires SHORT_CHANNELS to be True and at least one short channel to be found."},
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "General Linear Model",
|
||||
"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_END", "default": "15", "type": int, "help": "Where to end averaging the fir model bins. Only affects the significance and contrast images."},
|
||||
#{"name": "N_JOBS", "default": 1, "type": int, "help": "Number of jobs for GLM processing."},
|
||||
{"name": "NOISE_MODEL", "default": "ar1", "type": str, "help": "Number of jobs for GLM processing."},
|
||||
{"name": "BINS", "default": 0, "type": int, "help": "Number of jobs for GLM processing."},
|
||||
{"name": "N_JOBS", "default": 1, "type": int, "help": "Number of jobs for GLM processing."},
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -202,7 +216,10 @@ SECTIONS = [
|
||||
{
|
||||
"title": "Other",
|
||||
"params": [
|
||||
{"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": "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": "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."},
|
||||
]
|
||||
},
|
||||
]
|
||||
@@ -483,29 +500,38 @@ class UserGuideWindow(QWidget):
|
||||
|
||||
layout = QVBoxLayout()
|
||||
label = QLabel("Progress Bar Stages:", self)
|
||||
label2 = QLabel("Stage 1: Load the snirf file\n"
|
||||
"Stage 2: Check the optode positions\n"
|
||||
"Stage 3: Scalp Coupling Index\n"
|
||||
"Stage 4: Signal to Noise Ratio\n"
|
||||
"Stage 5: Peak Spectral Power\n"
|
||||
"Stage 6: Identify bad channels\n"
|
||||
"Stage 7: Interpolate bad channels\n"
|
||||
"Stage 8: Optical Density\n"
|
||||
"Stage 9: Temporal Derivative Distribution Repair\n"
|
||||
"Stage 10: Beer Lambert Law\n"
|
||||
"Stage 11: Heart Rate Filtering\n"
|
||||
"Stage 12: Get Short/Long Channels\n"
|
||||
"Stage 13: Calculate Events from Annotations\n"
|
||||
"Stage 14: Epoch Calculations\n"
|
||||
"Stage 15: Design Matrix\n"
|
||||
"Stage 16: General Linear Model\n"
|
||||
"Stage 17: Generate Plots from the GLM\n"
|
||||
"Stage 18: Individual Significance\n"
|
||||
"Stage 19: Channel, Region of Interest, and Contrast Results\n"
|
||||
"Stage 20: Image Conversion\n", self)
|
||||
label2 = QLabel("Stage 1: Preprocessing\n"
|
||||
"Stage 2: Trimming\n"
|
||||
"Stage 3: Verify Optode Placement\n"
|
||||
"Stage 4: Short/Long Cannels\n"
|
||||
"Stage 5: Heart Rate\n"
|
||||
"Stage 6: Scalp Coupling Index\n"
|
||||
"Stage 7: Signal to Noise Ratio\n"
|
||||
"Stage 8: Peak Spectral Power\n"
|
||||
"Stage 9: Bad Channels Handling\n"
|
||||
"Stage 10: Optical Density\n"
|
||||
"Stage 11: Temporal Derivative Distribution Repair Filtering\n"
|
||||
"Stage 12: Wavelet Filtering\n"
|
||||
"Stage 13: Haemoglobin Concentration\n"
|
||||
"Stage 14: Enhance Negative Correlation\n"
|
||||
"Stage 15: Filter\n"
|
||||
"Stage 16: Extracting Events\n"
|
||||
"Stage 17: Epoch Calculations\n"
|
||||
"Stage 18: Design Matrix\n"
|
||||
"Stage 19: General Linear Model\n"
|
||||
"Stage 20: Generate GLM Results\n"
|
||||
"Stage 21: Generate Channel Significance\n"
|
||||
"Stage 22: Generate Channel, Region of Interest, and Contrast Results\n"
|
||||
"Stage 23: Compute Contrast Results\n"
|
||||
"Stage 24: Finishing Up\n", self)
|
||||
|
||||
label3 = QLabel("For more information, visit the Git wiki page <a href='https://git.research.dezeeuw.ca/tyler/flares/wiki'>here</a>.", self)
|
||||
label3.setTextFormat(Qt.TextFormat.RichText)
|
||||
label3.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
label3.setOpenExternalLinks(True)
|
||||
layout.addWidget(label)
|
||||
layout.addWidget(label2)
|
||||
layout.addWidget(label3)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
@@ -524,10 +550,10 @@ class UpdateOptodesWindow(QWidget):
|
||||
self.btn_browse_a = QPushButton("Browse .snirf")
|
||||
self.btn_browse_a.clicked.connect(self.browse_file_a)
|
||||
|
||||
self.label_file_b = QLabel("TXT file:")
|
||||
self.label_file_b = QLabel("Text file:")
|
||||
self.line_edit_file_b = QLineEdit()
|
||||
self.line_edit_file_b.setReadOnly(True)
|
||||
self.btn_browse_b = QPushButton("Browse .txt")
|
||||
self.btn_browse_b = QPushButton("Browse .txt/.xlsx")
|
||||
self.btn_browse_b.clicked.connect(self.browse_file_b)
|
||||
|
||||
self.label_suffix = QLabel("Suffix to append to filename:")
|
||||
@@ -548,9 +574,10 @@ class UpdateOptodesWindow(QWidget):
|
||||
|
||||
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>"
|
||||
"Using a .txt or .xlsx 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>.")
|
||||
"An example format of what a digitization text file should look like can be found <a href='custom_link'>by clicking here</a>. Currently only .xlsx files directly exported from a<br>"
|
||||
"Polhemus system are supported.")
|
||||
|
||||
self.description.linkActivated.connect(self.handle_link_click)
|
||||
layout.addWidget(self.description)
|
||||
@@ -579,8 +606,7 @@ class UpdateOptodesWindow(QWidget):
|
||||
file_a_layout.addWidget(file_a_container)
|
||||
layout.addLayout(file_a_layout)
|
||||
|
||||
|
||||
help_text_b = "Provide a .txt file with labeled optodes (e.g., nz, rpa, lpa, d1, s1) and their x, y, z coordinates."
|
||||
help_text_b = "Provide a .txt file with labeled optodes (e.g., nz, rpa, lpa, d1, s1) and their x, y, z coordinates, or a .xlsx file from a Polhemius system."
|
||||
|
||||
file_b_layout = QHBoxLayout()
|
||||
|
||||
@@ -663,7 +689,7 @@ class UpdateOptodesWindow(QWidget):
|
||||
self.line_edit_file_a.setText(file_path)
|
||||
|
||||
def browse_file_b(self):
|
||||
file_path, _ = QFileDialog.getOpenFileName(self, "Select TXT File", "", "Text Files (*.txt)")
|
||||
file_path, _ = QFileDialog.getOpenFileName(self, "Select File", "", "Text Files (*.txt), Excel Files (*.xlsx)")
|
||||
if file_path:
|
||||
self.line_edit_file_b.setText(file_path)
|
||||
|
||||
@@ -717,7 +743,10 @@ class UpdateOptodesWindow(QWidget):
|
||||
fiducials = {}
|
||||
ch_positions = {}
|
||||
|
||||
extension = Path(file_b).suffix
|
||||
|
||||
# Read the lines from the optode file
|
||||
if extension == '.txt':
|
||||
with open(file_b, 'r') as f:
|
||||
for line in f:
|
||||
if line.strip():
|
||||
@@ -733,6 +762,49 @@ class UpdateOptodesWindow(QWidget):
|
||||
else:
|
||||
ch_positions[ch_name.upper()] = coords
|
||||
|
||||
elif extension == '.xlsx':
|
||||
|
||||
df = pd.read_excel(file_b, sheet_name='Sheet1')
|
||||
|
||||
def _get_block_data(df, block_id, row_mapping, scale=0.001):
|
||||
"""Isolates a block, cleans numeric data, and returns a scaled dictionary."""
|
||||
# 1. Isolate and clean
|
||||
block = df[df['block_id'] == block_id].iloc[:, [1, 2, 3]].copy()
|
||||
block = block.apply(pd.to_numeric, errors='coerce')
|
||||
|
||||
# 2. Extract into dictionary based on mapping
|
||||
result = {}
|
||||
|
||||
# If row_mapping is a dict (like {0: 'nz'}), use it directly
|
||||
if isinstance(row_mapping, dict):
|
||||
for row_idx, key in row_mapping.items():
|
||||
if row_idx < len(block):
|
||||
result[key] = block.iloc[row_idx].to_numpy(dtype=float) * scale
|
||||
|
||||
# If row_mapping is a string prefix (like 'D' or 'S'), auto-generate keys
|
||||
elif isinstance(row_mapping, str):
|
||||
for i in range(len(block)):
|
||||
result[f"{row_mapping}{i+1}"] = block.iloc[i].to_numpy(dtype=float) * scale
|
||||
|
||||
return result
|
||||
|
||||
|
||||
# Identify blocks
|
||||
is_empty = df.isnull().all(axis=1)
|
||||
df['block_id'] = is_empty.cumsum()
|
||||
clean_df = df[~is_empty].copy()
|
||||
|
||||
# Process Block 2: Landmarks
|
||||
fiducials = _get_block_data(clean_df, 2, {0: 'nz', 2: 'rpa', 3: 'lpa'})
|
||||
|
||||
# Process Block 3: D-Points
|
||||
d_points = _get_block_data(clean_df, 3, 'D')
|
||||
|
||||
# Process Block 4: S-Points
|
||||
s_points = _get_block_data(clean_df, 4, 'S')
|
||||
|
||||
ch_positions = {**d_points, **s_points}
|
||||
|
||||
# Create montage with updated coords in head space
|
||||
initial_montage = make_dig_montage(ch_pos=ch_positions, nasion=fiducials.get('nz'), lpa=fiducials.get('lpa'), rpa=fiducials.get('rpa'), coord_frame='head') # type: ignore
|
||||
|
||||
@@ -742,15 +814,23 @@ class UpdateOptodesWindow(QWidget):
|
||||
write_raw_snirf(raw, save_path)
|
||||
|
||||
|
||||
class EventUpdateMode(Enum):
|
||||
WRITE_SNIRF = auto() # destructive
|
||||
WRITE_JSON = auto() # non-destructive
|
||||
|
||||
|
||||
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)
|
||||
|
||||
self.mode = mode
|
||||
self.caller = caller or self.__class__.__name__
|
||||
self.setWindowTitle("Update event markers")
|
||||
self.resize(760, 200)
|
||||
|
||||
print("INIT MODE:", mode)
|
||||
|
||||
self.label_file_a = QLabel("SNIRF file:")
|
||||
self.line_edit_file_a = QLineEdit()
|
||||
self.line_edit_file_a.setReadOnly(True)
|
||||
@@ -1051,46 +1131,55 @@ class UpdateEventsWindow(QWidget):
|
||||
QMessageBox.warning(self, "No SNIRF file", "Please select a SNIRF file.")
|
||||
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]
|
||||
suggested_name = f"{base_name}_{suffix}.snirf"
|
||||
|
||||
# Open save dialog
|
||||
save_path, _ = QFileDialog.getSaveFileName(
|
||||
self,
|
||||
"Save SNIRF File As",
|
||||
suggested_name,
|
||||
"SNIRF Files (*.snirf)"
|
||||
)
|
||||
|
||||
if not save_path:
|
||||
print("Save cancelled.")
|
||||
print("SNIRF save cancelled.")
|
||||
return
|
||||
|
||||
if not save_path.lower().endswith(".snirf"):
|
||||
save_path += ".snirf"
|
||||
|
||||
try:
|
||||
raw = read_raw_snirf(snirf_path, preload=True)
|
||||
|
||||
onsets = []
|
||||
durations = []
|
||||
descriptions = []
|
||||
raw = read_raw_snirf(file_a, preload=True)
|
||||
|
||||
# --- Align BORIS events to SNIRF ---
|
||||
boris_events = boris_obs.get("events", [])
|
||||
onsets, durations, descriptions = [], [], []
|
||||
open_events = {} # label -> list of start times
|
||||
label_counts = {}
|
||||
|
||||
used_times = set()
|
||||
sfreq = raw.info['sfreq'] # sampling frequency in Hz
|
||||
sfreq = raw.info['sfreq']
|
||||
min_shift = 1.0 / sfreq
|
||||
max_attempts = 10
|
||||
|
||||
for event in boris_events:
|
||||
if not isinstance(event, list) or len(event) < 3:
|
||||
continue
|
||||
|
||||
event_time = event[0]
|
||||
label = event[2]
|
||||
|
||||
count = label_counts.get(label, 0) + 1
|
||||
label_counts[label] = count
|
||||
|
||||
@@ -1098,74 +1187,84 @@ class UpdateEventsWindow(QWidget):
|
||||
open_events[label] = []
|
||||
|
||||
if count % 2 == 1:
|
||||
# Odd occurrence = start event
|
||||
open_events[label].append(event_time)
|
||||
else:
|
||||
# Even occurrence = end event
|
||||
if open_events[label]:
|
||||
matched_start = open_events[label].pop(0)
|
||||
duration = event_time - matched_start
|
||||
|
||||
start_time = open_events[label].pop(0)
|
||||
duration = event_time - start_time
|
||||
if duration <= 0:
|
||||
print(f"Warning: Duration for {label} is non-positive ({duration}). Skipping.")
|
||||
continue
|
||||
|
||||
shifted_start = matched_start + time_shift
|
||||
|
||||
adjusted_time = shifted_start
|
||||
adjusted_time = start_time + time_shift
|
||||
attempts = 0
|
||||
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
|
||||
adjusted_time += min_shift
|
||||
attempts += 1
|
||||
|
||||
if attempts == max_attempts:
|
||||
print(f"Warning: Couldn't find unique time for {label} @ {matched_start}s. Skipping.")
|
||||
continue
|
||||
|
||||
adjusted_time = round(adjusted_time, 6)
|
||||
used_times.add(adjusted_time)
|
||||
|
||||
print(f"Adding event: {label} @ {adjusted_time:.3f}s for {duration:.3f}s")
|
||||
|
||||
onsets.append(adjusted_time)
|
||||
durations.append(duration)
|
||||
descriptions.append(label)
|
||||
else:
|
||||
print(f"Warning: Unmatched end for label '{label}' at {event_time:.3f}s. Skipping.")
|
||||
|
||||
# Optionally warn about any unmatched starts left open
|
||||
# Handle unmatched starts
|
||||
for label, starts in open_events.items():
|
||||
for start_time in starts:
|
||||
shifted_start = start_time + time_shift
|
||||
|
||||
adjusted_time = shifted_start
|
||||
adjusted_time = start_time + time_shift
|
||||
attempts = 0
|
||||
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
|
||||
adjusted_time += min_shift
|
||||
attempts += 1
|
||||
|
||||
if attempts == max_attempts:
|
||||
print(f"Warning: Couldn't find unique time for unmatched start {label} @ {start_time}s. Skipping.")
|
||||
continue
|
||||
|
||||
adjusted_time = round(adjusted_time, 6)
|
||||
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)
|
||||
durations.append(0.0)
|
||||
descriptions.append(label)
|
||||
|
||||
new_annotations = Annotations(onset=onsets, duration=durations, description=descriptions)
|
||||
|
||||
raw.set_annotations(new_annotations)
|
||||
write_raw_snirf(raw, save_path)
|
||||
|
||||
QMessageBox.information(self, "Success", "SNIRF file updated with aligned BORIS events.")
|
||||
|
||||
except Exception as 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):
|
||||
|
||||
@@ -1197,6 +1296,47 @@ class UpdateEventsWindow(QWidget):
|
||||
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):
|
||||
"""
|
||||
A clickable widget displaying a progress bar made of colored rectangles and a label.
|
||||
@@ -1229,7 +1369,7 @@ class ProgressBubble(QWidget):
|
||||
self.progress_layout = QHBoxLayout()
|
||||
|
||||
self.rects = []
|
||||
for _ in range(25):
|
||||
for _ in range(24):
|
||||
rect = QFrame()
|
||||
rect.setFixedSize(10, 18)
|
||||
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
|
||||
@@ -1358,6 +1498,11 @@ class ParamSection(QWidget):
|
||||
widget.setValidator(QDoubleValidator())
|
||||
widget.setText(str(param["default"]))
|
||||
elif param["type"] == list:
|
||||
if param.get("exclusive", True):
|
||||
widget = QComboBox()
|
||||
widget.addItems(param.get("options", []))
|
||||
widget.setCurrentText(str(param.get("default", "<None Selected>")))
|
||||
else:
|
||||
widget = self._create_multiselect_dropdown(None)
|
||||
else:
|
||||
widget = QLineEdit()
|
||||
@@ -1466,7 +1611,10 @@ class ParamSection(QWidget):
|
||||
if expected_type == bool:
|
||||
values[name] = widget.currentText() == "True"
|
||||
elif expected_type == list:
|
||||
if isinstance(widget, FullClickComboBox):
|
||||
values[name] = [x.strip() for x in widget.lineEdit().text().split(",") if x.strip()]
|
||||
elif isinstance(widget, QComboBox):
|
||||
values[name] = widget.currentText()
|
||||
else:
|
||||
raw_text = widget.text()
|
||||
try:
|
||||
@@ -2422,9 +2570,23 @@ class ParticipantFoldChannelsWidget(QWidget):
|
||||
|
||||
for idx in selected_indexes:
|
||||
if idx == 0:
|
||||
|
||||
try:
|
||||
flares.fold_channels(haemo_obj)
|
||||
|
||||
except:
|
||||
msg_box = QMessageBox()
|
||||
msg_box.setIcon(QMessageBox.Icon.Critical)
|
||||
msg_box.setWindowTitle("Something went wrong!")
|
||||
message = (
|
||||
"Unable to locate the fOLD files!<br><br>"
|
||||
f"Please download the 'Supplementary' folder from <a href='https://github.com/nirx/fOLD-public'>here</a>. "
|
||||
"Once the folder is downloaded, place it in C:/Users/your username/mne_data/fOLD/fOLD-public-master/Supplementary.<br><br>"
|
||||
"If you are not using Windows, please go to the FLARES Git page for more information."
|
||||
)
|
||||
msg_box.setTextFormat(Qt.TextFormat.RichText)
|
||||
msg_box.setText(message)
|
||||
msg_box.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction)
|
||||
msg_box.setStandardButtons(QMessageBox.StandardButton.Ok)
|
||||
msg_box.exec()
|
||||
else:
|
||||
print(f"No method defined for index {idx}")
|
||||
|
||||
@@ -2460,7 +2622,7 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
||||
|
||||
self.index_texts = [
|
||||
"0 (Export Data to CSV)",
|
||||
# "1 (second image)",
|
||||
"1 (CSV for SPARKS)",
|
||||
# "2 (third image)",
|
||||
# "3 (fourth image)",
|
||||
]
|
||||
@@ -2612,7 +2774,6 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
||||
# Pass the necessary arguments to each method
|
||||
for file_path in selected_file_paths:
|
||||
haemo_obj = self.haemo_dict.get(file_path)
|
||||
|
||||
if haemo_obj is None:
|
||||
continue
|
||||
|
||||
@@ -2646,10 +2807,63 @@ class ExportDataAsCSVViewerWidget(QWidget):
|
||||
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:
|
||||
print(f"No method defined for index {idx}")
|
||||
|
||||
|
||||
class ClickableLabel(QLabel):
|
||||
def __init__(self, full_pixmap: QPixmap, thumbnail_pixmap: QPixmap):
|
||||
super().__init__()
|
||||
@@ -4275,7 +4489,7 @@ class MainApplication(QMainWindow):
|
||||
|
||||
def update_event_markers(self):
|
||||
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()
|
||||
|
||||
def open_file_dialog(self):
|
||||
|
||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
||||
import numpy as np
|
||||
from scipy import linalg
|
||||
from scipy.spatial.distance import cdist
|
||||
from scipy.special import sph_harm
|
||||
from scipy.special import sph_harm_y
|
||||
|
||||
from ._fiff.constants import FIFF
|
||||
from ._fiff.open import fiff_open
|
||||
|
||||
@@ -1025,7 +1025,7 @@ def _handle_sensor_types(meg, eeg, fnirs):
|
||||
fnirs=dict(channels="fnirs", pairs="fnirs_pairs"),
|
||||
)
|
||||
sensor_alpha = {
|
||||
key: dict(meg_helmet=0.25, meg=0.25).get(key, 0.8)
|
||||
key: dict(meg_helmet=0.25, meg=0.25).get(key, 1.0)
|
||||
for ch_dict in alpha_map.values()
|
||||
for key in ch_dict.values()
|
||||
}
|
||||
|
||||
@@ -586,7 +586,7 @@ class _PyVistaRenderer(_AbstractRenderer):
|
||||
color = None
|
||||
else:
|
||||
scalars = None
|
||||
tube = line.tube(radius, n_sides=self.tube_n_sides)
|
||||
tube = line.tube(radius=radius, n_sides=self.tube_n_sides)
|
||||
actor = _add_mesh(
|
||||
plotter=self.plotter,
|
||||
mesh=tube,
|
||||
|
||||
@@ -18,7 +18,7 @@ VSVersionInfo(
|
||||
StringStruct('FileDescription', 'FLARES main application'),
|
||||
StringStruct('FileVersion', '1.0.0.0'),
|
||||
StringStruct('InternalName', 'flares.exe'),
|
||||
StringStruct('LegalCopyright', '© 2025 Tyler de Zeeuw'),
|
||||
StringStruct('LegalCopyright', '© 2025-2026 Tyler de Zeeuw'),
|
||||
StringStruct('OriginalFilename', 'flares.exe'),
|
||||
StringStruct('ProductName', 'FLARES'),
|
||||
StringStruct('ProductVersion', '1.0.0.0')])
|
||||
|
||||
@@ -18,7 +18,7 @@ VSVersionInfo(
|
||||
StringStruct('FileDescription', 'FLARES updater application'),
|
||||
StringStruct('FileVersion', '1.0.0.0'),
|
||||
StringStruct('InternalName', 'main.exe'),
|
||||
StringStruct('LegalCopyright', '© 2025 Tyler de Zeeuw'),
|
||||
StringStruct('LegalCopyright', '© 2025-2026 Tyler de Zeeuw'),
|
||||
StringStruct('OriginalFilename', 'flares_updater.exe'),
|
||||
StringStruct('ProductName', 'FLARES Updater'),
|
||||
StringStruct('ProductVersion', '1.0.0.0')])
|
||||
|
||||
Reference in New Issue
Block a user