Compare commits
9 Commits
v1.1.5
...
1b78f1904d
| Author | SHA1 | Date | |
|---|---|---|---|
| 1b78f1904d | |||
| 9779a63a9c | |||
| 2ecd357aca | |||
| fe4e8904b4 | |||
| 473c945563 | |||
| 64ed6d2e87 | |||
| 1aa2402d09 | |||
| 45c6176dba | |||
| a4bbdb90c8 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -174,3 +174,4 @@ cython_debug/
|
|||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
/individual_images
|
||||||
43
changelog.md
43
changelog.md
@@ -1,4 +1,44 @@
|
|||||||
# Next Release
|
# 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
|
||||||
|
- Changed the default H_FREQ from 0.7 to 0.3
|
||||||
|
- Added a PSD graph, along with 2 heart rate images to the individual participant viewer
|
||||||
|
- The PSD graph is used to help calculate the heart rate, whereas the other 2 are currently just for show
|
||||||
|
- SCI is now done using a .6hz window around the calculated heart rate compared to a window around an average heart rate
|
||||||
|
- Fixed an issue with some epochs figures not showing under the participant analysis
|
||||||
|
- Removed SECONDS_TO_STRIP from the preprocessing options
|
||||||
|
- Added new parameters to the right side of the screen
|
||||||
|
- These parameters include TRIM, SECONDS_TO_KEEP, OPTODE_PLACEMENT, HEART_RATE, WAVELET, IQR, WAVELET_TYPE, WAVELET_LEVEL, ENHANCE_NEGATIVE_CORRELATION, SHORT_CHANNEL_THRESH, LONG_CHANNEL_THRESH, and DRIFT_MODEL
|
||||||
|
- Changed number of rectangles in the progress bar to 25 to account for the new options
|
||||||
|
|
||||||
|
|
||||||
|
# Version 1.1.6
|
||||||
|
|
||||||
|
- Fixed Process button from appearing when no files are selected
|
||||||
|
- Fixed a bug that would cause an instant child process crash on Windows
|
||||||
|
- Added L_FREQ and H_FREQ parameters for more user control over low and high pass filtering
|
||||||
|
|
||||||
|
|
||||||
|
# Version 1.1.5
|
||||||
|
|
||||||
- Fixed Windows saves not being able to be opened by a Mac (hopefully the other way too!)
|
- Fixed Windows saves not being able to be opened by a Mac (hopefully the other way too!)
|
||||||
- Added the option to right click loaded snirf files to reveal them in a file browser or delete them if they are no longer desired
|
- Added the option to right click loaded snirf files to reveal them in a file browser or delete them if they are no longer desired
|
||||||
@@ -14,6 +54,7 @@
|
|||||||
- Added a clickable link below the selected file's metadata explaining the independent parameters and why they are useful
|
- Added a clickable link below the selected file's metadata explaining the independent parameters and why they are useful
|
||||||
- Updated some tooltips to provide better, more accurate information
|
- Updated some tooltips to provide better, more accurate information
|
||||||
- Added details about the processing steps and their order into the user guide
|
- Added details about the processing steps and their order into the user guide
|
||||||
|
- Changed the default bandpass filtering parameters
|
||||||
|
|
||||||
|
|
||||||
# Version 1.1.4
|
# Version 1.1.4
|
||||||
|
|||||||
502
main.py
502
main.py
@@ -22,11 +22,11 @@ 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
|
||||||
import numpy as np
|
|
||||||
import pandas as pd
|
|
||||||
|
|
||||||
# External library imports
|
# External library imports
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
import psutil
|
import psutil
|
||||||
import requests
|
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
|
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 = "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"
|
API_URL_SECONDARY = "https://git.research2.dezeeuw.ca/api/v1/repos/tyler/flares/releases"
|
||||||
@@ -58,11 +58,43 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Preprocessing",
|
"title": "Preprocessing",
|
||||||
"params": [
|
"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", "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"},
|
{"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"},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Trimming",
|
||||||
|
"params": [
|
||||||
|
{"name": "TRIM", "default": True, "type": bool, "help": "Trim the file start."},
|
||||||
|
{"name": "SECONDS_TO_KEEP", "default": 5, "type": float, "help": "Seconds to keep at the beginning of all loaded snirf files before the first annotation/event occurs. Calculation is done seperatly on all loaded snirf files. Setting this to 0 will have the first annotation/event be at time point 0."},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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."},
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Scalp Coupling Index",
|
"title": "Scalp Coupling Index",
|
||||||
"params": [
|
"params": [
|
||||||
@@ -75,14 +107,12 @@ SECTIONS = [
|
|||||||
"title": "Signal to Noise Ratio",
|
"title": "Signal to Noise Ratio",
|
||||||
"params": [
|
"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", "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."},
|
{"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",
|
"title": "Peak Spectral Power",
|
||||||
"params": [
|
"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", "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_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."},
|
{"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."},
|
||||||
@@ -91,15 +121,15 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Bad Channels Handling",
|
"title": "Bad Channels Handling",
|
||||||
"params": [
|
"params": [
|
||||||
# {"name": "NOT_IMPLEMENTED", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
{"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": "NOT_IMPLEMENTED", "default": 3, "type": int, "help": "PSP time window."},
|
{"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": "NOT_IMPLEMENTED", "default": 0.1, "type": float, "help": "PSP threshold."},
|
{"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",
|
"title": "Optical Density",
|
||||||
"params": [
|
"params": [
|
||||||
# Intentionally empty (TODO)
|
# NOTE: Intentionally empty
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -108,38 +138,45 @@ SECTIONS = [
|
|||||||
{"name": "TDDR", "default": True, "type": bool, "help": "Apply Temporal Derivitave Distribution Repair filtering - a method that removes baseline shift and spike artifacts from the data."},
|
{"name": "TDDR", "default": True, "type": bool, "help": "Apply Temporal Derivitave Distribution Repair filtering - a method that removes baseline shift and spike artifacts from the data."},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"title": "Wavelet filtering",
|
||||||
|
"params": [
|
||||||
|
{"name": "WAVELET", "default": True, "type": bool, "help": "Apply Wavelet filtering."},
|
||||||
|
{"name": "IQR", "default": 1.5, "type": float, "help": "Inter-Quartile Range."},
|
||||||
|
{"name": "WAVELET_TYPE", "default": "db4", "type": str, "help": "Wavelet type."},
|
||||||
|
{"name": "WAVELET_LEVEL", "default": 3, "type": int, "help": "Wavelet level."},
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"title": "Haemoglobin Concentration",
|
"title": "Haemoglobin Concentration",
|
||||||
"params": [
|
"params": [
|
||||||
# Intentionally empty (TODO)
|
# NOTE: Intentionally empty
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Enhance Negative Correlation",
|
"title": "Enhance Negative Correlation",
|
||||||
"params": [
|
"params": [
|
||||||
#{"name": "ENHANCE_NEGATIVE_CORRELATION", "default": False, "type": bool, "help": "Calculate Peak Spectral Power."},
|
{"name": "ENHANCE_NEGATIVE_CORRELATION", "default": False, "type": bool, "help": "Apply Enhance Negative Correlation."},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Filtering",
|
"title": "Filtering",
|
||||||
"params": [
|
"params": [
|
||||||
#{"name": "FILTER", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
{"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 Channels",
|
"title": "Extracting Events*",
|
||||||
"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."},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"title": "Extracting Events",
|
|
||||||
"params": [
|
"params": [
|
||||||
#{"name": "EVENTS", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
#{"name": "EVENTS", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Epoch Calculations",
|
"title": "Epoch Calculations*",
|
||||||
"params": [
|
"params": [
|
||||||
#{"name": "EVENTS", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
#{"name": "EVENTS", "default": True, "type": bool, "help": "Calculate Peak Spectral Power."},
|
||||||
]
|
]
|
||||||
@@ -147,18 +184,27 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Design Matrix",
|
"title": "Design Matrix",
|
||||||
"params": [
|
"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": "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": "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."},
|
||||||
# {"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."},
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "General Linear Model",
|
"title": "General Linear Model",
|
||||||
"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": "NOISE_MODEL", "default": "ar1", "type": str, "help": "Number of jobs for GLM processing."},
|
||||||
{"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": "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."},
|
{"name": "N_JOBS", "default": 1, "type": int, "help": "Number of jobs for GLM processing."},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -170,7 +216,10 @@ SECTIONS = [
|
|||||||
{
|
{
|
||||||
"title": "Other",
|
"title": "Other",
|
||||||
"params": [
|
"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."},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
@@ -451,29 +500,38 @@ class UserGuideWindow(QWidget):
|
|||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
label = QLabel("Progress Bar Stages:", self)
|
label = QLabel("Progress Bar Stages:", self)
|
||||||
label2 = QLabel("Stage 1: Load the snirf file\n"
|
label2 = QLabel("Stage 1: Preprocessing\n"
|
||||||
"Stage 2: Check the optode positions\n"
|
"Stage 2: Trimming\n"
|
||||||
"Stage 3: Scalp Coupling Index\n"
|
"Stage 3: Verify Optode Placement\n"
|
||||||
"Stage 4: Signal to Noise Ratio\n"
|
"Stage 4: Short/Long Cannels\n"
|
||||||
"Stage 5: Peak Spectral Power\n"
|
"Stage 5: Heart Rate\n"
|
||||||
"Stage 6: Identify bad channels\n"
|
"Stage 6: Scalp Coupling Index\n"
|
||||||
"Stage 7: Interpolate bad channels\n"
|
"Stage 7: Signal to Noise Ratio\n"
|
||||||
"Stage 8: Optical Density\n"
|
"Stage 8: Peak Spectral Power\n"
|
||||||
"Stage 9: Temporal Derivative Distribution Repair\n"
|
"Stage 9: Bad Channels Handling\n"
|
||||||
"Stage 10: Beer Lambert Law\n"
|
"Stage 10: Optical Density\n"
|
||||||
"Stage 11: Heart Rate Filtering\n"
|
"Stage 11: Temporal Derivative Distribution Repair Filtering\n"
|
||||||
"Stage 12: Get Short/Long Channels\n"
|
"Stage 12: Wavelet Filtering\n"
|
||||||
"Stage 13: Calculate Events from Annotations\n"
|
"Stage 13: Haemoglobin Concentration\n"
|
||||||
"Stage 14: Epoch Calculations\n"
|
"Stage 14: Enhance Negative Correlation\n"
|
||||||
"Stage 15: Design Matrix\n"
|
"Stage 15: Filter\n"
|
||||||
"Stage 16: General Linear Model\n"
|
"Stage 16: Extracting Events\n"
|
||||||
"Stage 17: Generate Plots from the GLM\n"
|
"Stage 17: Epoch Calculations\n"
|
||||||
"Stage 18: Individual Significance\n"
|
"Stage 18: Design Matrix\n"
|
||||||
"Stage 19: Channel, Region of Interest, and Contrast Results\n"
|
"Stage 19: General Linear Model\n"
|
||||||
"Stage 20: Image Conversion\n", self)
|
"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(label)
|
||||||
layout.addWidget(label2)
|
layout.addWidget(label2)
|
||||||
|
layout.addWidget(label3)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
@@ -710,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)
|
||||||
@@ -1019,120 +1085,139 @@ 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]
|
base_name = os.path.splitext(os.path.basename(file_a))[0]
|
||||||
suggested_name = f"{base_name}_{suffix}.snirf"
|
|
||||||
|
|
||||||
# Open save dialog
|
if self.mode == EventUpdateMode.WRITE_SNIRF:
|
||||||
save_path, _ = QFileDialog.getSaveFileName(
|
# Open save dialog for SNIRF
|
||||||
self,
|
base_name = os.path.splitext(os.path.basename(file_a))[0]
|
||||||
"Save SNIRF File As",
|
suggested_name = f"{base_name}_{suffix}.snirf"
|
||||||
suggested_name,
|
save_path, _ = QFileDialog.getSaveFileName(
|
||||||
"SNIRF Files (*.snirf)"
|
self,
|
||||||
)
|
"Save SNIRF File As",
|
||||||
|
suggested_name,
|
||||||
|
"SNIRF Files (*.snirf)"
|
||||||
|
)
|
||||||
|
if not save_path:
|
||||||
|
print("SNIRF save cancelled.")
|
||||||
|
return
|
||||||
|
if not save_path.lower().endswith(".snirf"):
|
||||||
|
save_path += ".snirf"
|
||||||
|
|
||||||
if not save_path:
|
try:
|
||||||
print("Save cancelled.")
|
raw = read_raw_snirf(file_a, preload=True)
|
||||||
return
|
|
||||||
|
|
||||||
if not save_path.lower().endswith(".snirf"):
|
# --- Align BORIS events to SNIRF ---
|
||||||
save_path += ".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']
|
||||||
|
min_shift = 1.0 / sfreq
|
||||||
|
max_attempts = 10
|
||||||
|
|
||||||
try:
|
for event in boris_events:
|
||||||
raw = read_raw_snirf(snirf_path, preload=True)
|
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
|
||||||
|
|
||||||
onsets = []
|
if label not in open_events:
|
||||||
durations = []
|
open_events[label] = []
|
||||||
descriptions = []
|
|
||||||
|
|
||||||
open_events = {} # label -> list of start times
|
if count % 2 == 1:
|
||||||
label_counts = {}
|
open_events[label].append(event_time)
|
||||||
|
else:
|
||||||
|
if open_events[label]:
|
||||||
|
start_time = open_events[label].pop(0)
|
||||||
|
duration = event_time - start_time
|
||||||
|
if duration <= 0:
|
||||||
|
continue
|
||||||
|
|
||||||
used_times = set()
|
adjusted_time = start_time + time_shift
|
||||||
sfreq = raw.info['sfreq'] # sampling frequency in Hz
|
attempts = 0
|
||||||
min_shift = 1.0 / sfreq
|
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
|
||||||
max_attempts = 10
|
adjusted_time += min_shift
|
||||||
|
attempts += 1
|
||||||
|
if attempts == max_attempts:
|
||||||
|
continue
|
||||||
|
|
||||||
for event in boris_events:
|
adjusted_time = round(adjusted_time, 6)
|
||||||
if not isinstance(event, list) or len(event) < 3:
|
used_times.add(adjusted_time)
|
||||||
continue
|
onsets.append(adjusted_time)
|
||||||
|
durations.append(duration)
|
||||||
event_time = event[0]
|
descriptions.append(label)
|
||||||
label = event[2]
|
# Handle unmatched starts
|
||||||
|
for label, starts in open_events.items():
|
||||||
count = label_counts.get(label, 0) + 1
|
for start_time in starts:
|
||||||
label_counts[label] = count
|
adjusted_time = start_time + time_shift
|
||||||
|
|
||||||
if label not in open_events:
|
|
||||||
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
|
|
||||||
|
|
||||||
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
|
|
||||||
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(0.0)
|
||||||
descriptions.append(label)
|
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
|
new_annotations = Annotations(onset=onsets, duration=durations, description=descriptions)
|
||||||
for label, starts in open_events.items():
|
raw.set_annotations(new_annotations)
|
||||||
for start_time in starts:
|
write_raw_snirf(raw, save_path)
|
||||||
shifted_start = start_time + time_shift
|
QMessageBox.information(self, "Success", "SNIRF file updated with aligned BORIS events.")
|
||||||
|
|
||||||
adjusted_time = shifted_start
|
except Exception as e:
|
||||||
attempts = 0
|
QMessageBox.critical(self, "Error", f"Failed to update SNIRF file:\n{e}")
|
||||||
while round(adjusted_time, 6) in used_times and attempts < max_attempts:
|
|
||||||
adjusted_time += min_shift
|
|
||||||
attempts += 1
|
|
||||||
|
|
||||||
if attempts == max_attempts:
|
elif self.mode == EventUpdateMode.WRITE_JSON:
|
||||||
print(f"Warning: Couldn't find unique time for unmatched start {label} @ {start_time}s. Skipping.")
|
# Open save dialog for JSON
|
||||||
continue
|
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"
|
||||||
|
|
||||||
adjusted_time = round(adjusted_time, 6)
|
# Build JSON dict
|
||||||
used_times.add(adjusted_time)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
print(f"Warning: Unmatched start for label '{label}' at {start_time:.3f}s. Adding with duration 0.")
|
# Write JSON
|
||||||
onsets.append(adjusted_time)
|
try:
|
||||||
durations.append(0.0)
|
with open(save_path, "w", encoding="utf-8") as f:
|
||||||
descriptions.append(label)
|
json.dump(json_data, f, indent=4)
|
||||||
|
QMessageBox.information(self, "Success", f"Event alignment saved to:\n{save_path}")
|
||||||
new_annotations = Annotations(onset=onsets, duration=durations, description=descriptions)
|
except Exception as e:
|
||||||
|
QMessageBox.critical(self, "Error", f"Failed to write JSON:\n{e}")
|
||||||
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}")
|
|
||||||
|
|
||||||
|
|
||||||
def update_optode_positions(self, file_a, file_b, save_path):
|
def update_optode_positions(self, file_a, file_b, save_path):
|
||||||
@@ -1165,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.
|
||||||
@@ -1197,7 +1323,7 @@ class ProgressBubble(QWidget):
|
|||||||
self.progress_layout = QHBoxLayout()
|
self.progress_layout = QHBoxLayout()
|
||||||
|
|
||||||
self.rects = []
|
self.rects = []
|
||||||
for _ in range(20):
|
for _ in range(24):
|
||||||
rect = QFrame()
|
rect = QFrame()
|
||||||
rect.setFixedSize(10, 18)
|
rect.setFixedSize(10, 18)
|
||||||
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
|
rect.setStyleSheet("background-color: white; border: 1px solid gray;")
|
||||||
@@ -1326,7 +1452,12 @@ class ParamSection(QWidget):
|
|||||||
widget.setValidator(QDoubleValidator())
|
widget.setValidator(QDoubleValidator())
|
||||||
widget.setText(str(param["default"]))
|
widget.setText(str(param["default"]))
|
||||||
elif param["type"] == list:
|
elif param["type"] == list:
|
||||||
widget = self._create_multiselect_dropdown(None)
|
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:
|
else:
|
||||||
widget = QLineEdit()
|
widget = QLineEdit()
|
||||||
widget.setText(str(param["default"]))
|
widget.setText(str(param["default"]))
|
||||||
@@ -1434,7 +1565,10 @@ class ParamSection(QWidget):
|
|||||||
if expected_type == bool:
|
if expected_type == bool:
|
||||||
values[name] = widget.currentText() == "True"
|
values[name] = widget.currentText() == "True"
|
||||||
elif expected_type == list:
|
elif expected_type == list:
|
||||||
values[name] = [x.strip() for x in widget.lineEdit().text().split(",") if x.strip()]
|
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:
|
else:
|
||||||
raw_text = widget.text()
|
raw_text = widget.text()
|
||||||
try:
|
try:
|
||||||
@@ -2390,9 +2524,23 @@ class ParticipantFoldChannelsWidget(QWidget):
|
|||||||
|
|
||||||
for idx in selected_indexes:
|
for idx in selected_indexes:
|
||||||
if idx == 0:
|
if idx == 0:
|
||||||
|
try:
|
||||||
flares.fold_channels(haemo_obj)
|
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:
|
else:
|
||||||
print(f"No method defined for index {idx}")
|
print(f"No method defined for index {idx}")
|
||||||
|
|
||||||
@@ -2428,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)",
|
||||||
]
|
]
|
||||||
@@ -2580,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
|
||||||
|
|
||||||
@@ -2614,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__()
|
||||||
@@ -4044,7 +4244,7 @@ class MainApplication(QMainWindow):
|
|||||||
self.button2.setMinimumSize(100, 40)
|
self.button2.setMinimumSize(100, 40)
|
||||||
self.button3.setMinimumSize(100, 40)
|
self.button3.setMinimumSize(100, 40)
|
||||||
|
|
||||||
# self.button1.setVisible(False)
|
self.button1.setVisible(False)
|
||||||
self.button3.setVisible(False)
|
self.button3.setVisible(False)
|
||||||
|
|
||||||
self.button1.clicked.connect(self.on_run_task)
|
self.button1.clicked.connect(self.on_run_task)
|
||||||
@@ -4243,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):
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from pathlib import Path
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
from scipy import linalg
|
from scipy import linalg
|
||||||
from scipy.spatial.distance import cdist
|
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.constants import FIFF
|
||||||
from ._fiff.open import fiff_open
|
from ._fiff.open import fiff_open
|
||||||
|
|||||||
@@ -1025,7 +1025,7 @@ def _handle_sensor_types(meg, eeg, fnirs):
|
|||||||
fnirs=dict(channels="fnirs", pairs="fnirs_pairs"),
|
fnirs=dict(channels="fnirs", pairs="fnirs_pairs"),
|
||||||
)
|
)
|
||||||
sensor_alpha = {
|
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 ch_dict in alpha_map.values()
|
||||||
for key in ch_dict.values()
|
for key in ch_dict.values()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ class _PyVistaRenderer(_AbstractRenderer):
|
|||||||
color = None
|
color = None
|
||||||
else:
|
else:
|
||||||
scalars = None
|
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(
|
actor = _add_mesh(
|
||||||
plotter=self.plotter,
|
plotter=self.plotter,
|
||||||
mesh=tube,
|
mesh=tube,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ VSVersionInfo(
|
|||||||
StringStruct('FileDescription', 'FLARES main application'),
|
StringStruct('FileDescription', 'FLARES main application'),
|
||||||
StringStruct('FileVersion', '1.0.0.0'),
|
StringStruct('FileVersion', '1.0.0.0'),
|
||||||
StringStruct('InternalName', 'flares.exe'),
|
StringStruct('InternalName', 'flares.exe'),
|
||||||
StringStruct('LegalCopyright', '© 2025 Tyler de Zeeuw'),
|
StringStruct('LegalCopyright', '© 2025-2026 Tyler de Zeeuw'),
|
||||||
StringStruct('OriginalFilename', 'flares.exe'),
|
StringStruct('OriginalFilename', 'flares.exe'),
|
||||||
StringStruct('ProductName', 'FLARES'),
|
StringStruct('ProductName', 'FLARES'),
|
||||||
StringStruct('ProductVersion', '1.0.0.0')])
|
StringStruct('ProductVersion', '1.0.0.0')])
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ VSVersionInfo(
|
|||||||
StringStruct('FileDescription', 'FLARES updater application'),
|
StringStruct('FileDescription', 'FLARES updater application'),
|
||||||
StringStruct('FileVersion', '1.0.0.0'),
|
StringStruct('FileVersion', '1.0.0.0'),
|
||||||
StringStruct('InternalName', 'main.exe'),
|
StringStruct('InternalName', 'main.exe'),
|
||||||
StringStruct('LegalCopyright', '© 2025 Tyler de Zeeuw'),
|
StringStruct('LegalCopyright', '© 2025-2026 Tyler de Zeeuw'),
|
||||||
StringStruct('OriginalFilename', 'flares_updater.exe'),
|
StringStruct('OriginalFilename', 'flares_updater.exe'),
|
||||||
StringStruct('ProductName', 'FLARES Updater'),
|
StringStruct('ProductName', 'FLARES Updater'),
|
||||||
StringStruct('ProductVersion', '1.0.0.0')])
|
StringStruct('ProductVersion', '1.0.0.0')])
|
||||||
|
|||||||
Reference in New Issue
Block a user