Skip to content

Commit

Permalink
Analysis Page: Display fft and 1/f summaries after a recording comple…
Browse files Browse the repository at this point in the history
…tes (#236)


* add: display fooof results after dataset upload
  • Loading branch information
oreHGA committed Aug 23, 2024
1 parent 9190b9a commit 94938d5
Show file tree
Hide file tree
Showing 7 changed files with 258 additions and 54 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/staging_fusion-staging.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ jobs:
echo "NEXT_PUBLIC_FUSION_RELAY_URL=${{ secrets.NEXT_PUBLIC_FUSION_RELAY_URL }}" >> .env.production
echo "NEXT_PUBLIC_FUSION_NOSTR_PUBLIC_KEY=${{ secrets.NEXT_PUBLIC_FUSION_NOSTR_PUBLIC_KEY }}" >> .env.production
echo "NEXT_PUBLIC_NEUROFUSION_BACKEND_URL=${{ secrets.NEXT_PUBLIC_NEUROFUSION_BACKEND_URL }}" >> .env.production
echo "NEXT_PUBLIC_ANALYSIS_SERVER_URL=${{ secrets.NEXT_PUBLIC_ANALYSIS_SERVER_URL }}" >> .env.production
echo "NEXT_PUBLIC_APP_INSIGHTS_KEY=${{ secrets.NEXT_PUBLIC_APP_INSIGHTS_KEY }}" >> .env.production
npm run build --if-present
npm run test --if-present
Expand Down
9 changes: 4 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,21 @@ We build tools to accelerate the adoption of neurotechnology and behavior resear
- [x] Resting state (Eyes Closed/Eyes Open)
- [x] Stroop Task
- [x] Auditory Oddball
- [x] Visual Oddball
- [x] Flappy Bird game (Detecting intent & frustration)

- [ ] Analysis of collected data https://usefusion.ai/analysis

- [x] Chart of steady state frequency power across recordings
- [ ] Periodic vs. aperiodic frequency evaluation using [fooof package](https://fooof-tools.github.io/fooof/)
- [x] Periodic vs. aperiodic frequency evaluation using [fooof package](https://fooof-tools.github.io/fooof/)
- [ ] If applicable event related potential analysis

- [ ] Running Distributed Studies with people (Quests) https://usefusion.ai/blog/quests

- [x] A set of prompts people respond to at intervals on a topic related to you
- [x] Connecting Apple Health - steps, sleep, heart-rate
- [ ] Support for cognitive experiments
- [ ] Quest Dashboard - view submissions, analyze and publish results

- [ ] Design and upload custom EEG experiment protocols to participants' devices
- [x] Quest Dashboard - view submissions, analyze and publish results
- [ ] Design and upload custom EEG experiment protocols to participants' devices

- [ ] Connecting Other Sources
- [ ] Connect your screentime events & productivity metrics using [ActivityWatch](https://activitywatch.net)
Expand Down
3 changes: 2 additions & 1 deletion analysis_api/.gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
temp_unzip
powerComparisons.png
powerDistributions.png
.env
.env
fooof_outputs
119 changes: 98 additions & 21 deletions analysis_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,40 +95,117 @@ def process_eeg():
return jsonify({'error': str(e)}), 500

# TODO: endpoint for ERP analysis
@app.route('/api/v1/process_eeg_erp', methods=['POST'])
def process_eeg_erp():
try:
return jsonify({'response': "works perfect"}), 200
except Exception as e:
return jsonify({'error': 'error processing', 'message': e}), 500

@app.route('/api/v1/process_eeg_fooof', methods=['POST'])
def process_eeg_fooof():
"""
When a person uploads an EEG file, we need to process it and return the FOOOF results
as images"""
try:
# Check if the POST request contains a file with the key 'file'
if 'file' not in request.files:
return jsonify({'error': 'No file part'}), 400
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
from fooof import FOOOFGroup
import mne
import io
import base64
import time

if 'fileTimestamp' not in request.form:
return jsonify({'error': 'No file timestamp'}), 400
# Check if the POST request contains a file with the key 'file'
if 'eegFile' not in request.files:
return jsonify({'error': 'No EEG file submitted for processing'}), 400

file = request.files['file']
eegFile = request.files['eegFile']
samplingFrequency = int(request.form['samplingFrequency'])

fileTimestamp = request.form['fileTimestamp']
print("eegFile", eegFile)

# Check if the file has a filename
if file.filename == '':
return jsonify({'error': 'No selected file'}), 400
if eegFile.filename == '':
return jsonify({'error': 'No selected EEG file'}), 400

if eegFile.filename.endswith('.csv'):
# Read the CSV file into a pandas DataFrame
df = pd.read_csv(eegFile)
df.drop(columns=['index'], inplace=True)
sfreq = samplingFrequency
info = mne.create_info(ch_names=list(df.columns[1:]), sfreq=sfreq, ch_types='eeg')

# transpose data
df = df.values[:, 1:].T
df *= 1e-6 # convert from uV to V
raw = mne.io.RawArray(df, info)
raw.set_montage('standard_1020')

# Check if the file is a ZIP file
if file.filename.endswith('.zip'):
# Create a temporary directory to store the unzipped files
temp_dir = 'temp_unzip' # +`` "_" + str(int(time.time()))
os.makedirs(temp_dir, exist_ok=True)
events = mne.make_fixed_length_events(raw, duration=5)
epochs = mne.Epochs(raw, events, tmin=0, tmax=0.5, baseline=None)

# Save the ZIP file to the temporary directory
zip_file_path = os.path.join(temp_dir, file.filename)
file.save(zip_file_path)
epochsSpectrum = epochs.compute_psd(fmin=1, fmax=40, method='welch', verbose=False)

fg = FOOOFGroup(peak_width_limits=[1.0, 8.0], min_peak_height=0.1, peak_threshold=2.)
fg.fit(epochsSpectrum.freqs, epochsSpectrum.average().get_data(), freq_range=[1, 40])

# Unzip the file
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
zip_ref.extractall(temp_dir + "/raw_files")
temp_dir = 'fooof_outputs'
os.makedirs(temp_dir, exist_ok=True)
file_name = f"fooof_results_{int(time.time())}.png"
fg.save_report(file_name, file_path=temp_dir)

images = [{
"key": "FOOOF - Group Results",
"value": "data:image/png;base64," + encode_image_to_base64(temp_dir + "/" + file_name)
}]

# Get the FOOOF results for each channel
ch_names = raw.info['ch_names']
for i in range(len(ch_names)):
result = fg.get_fooof(i)

# Print the result
results = result.get_results()
results_str = (
f" FOOOF - POWER SPECTRUM MODEL\n\n"
f"The model was run on the frequency range {result.freq_range[0]:.2f} - {result.freq_range[1]:.2f} Hz\n"
f"Frequency Resolution is {result.freq_res:.2f} Hz\n\n"
f"Aperiodic Parameters (offset, exponent):\n"
f"{results.aperiodic_params[0]:.4f}, {results.aperiodic_params[1]:.4f}\n\n"
f"{len(results.peak_params)} peaks were found:\n"
)
for peak in results.peak_params:
results_str += f"CF: {peak[0]:6.2f}, PW: {peak[1]:6.3f}, BW: {peak[2]:6.2f}\n"
results_str += (
f"\nGoodness of fit metrics:\n"
f"R^2 of model fit is {results.r_squared:.4f}\n"
f"Error of the fit is {results.error:.4f}"
)
fig, ax = plt.subplots()

# Plot the result
result.plot(ax=ax, show=False)

# Save the plot to a BytesIO object
img_buffer = io.BytesIO()
fig.savefig(img_buffer, format='png')
img_buffer.seek(0)
plt.close(fig)

# Encode the image to base64
img_str = base64.b64encode(img_buffer.getvalue()).decode()

# Add the image to the list
images.append({
"key": f"FOOOF Results for {ch_names[i]}",
"value": f"data:image/png;base64,{img_str}",
"summary": results_str
})

print("file directory", temp_dir)
return jsonify({"images": images, "summary": "FOOOF Results"}), 200

except Exception as e:
print("error", e)
return jsonify({'error': str(e)}), 500
Expand Down
3 changes: 2 additions & 1 deletion analysis_api/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ scipy
fooof
nltk
python-dotenv
requests
requests
mne
2 changes: 2 additions & 0 deletions frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,7 @@ module.exports = {
NEXT_PUBLIC_FUSION_RELAY_URL: process.env.NEXT_PUBLIC_FUSION_RELAY_URL,
NEXT_PUBLIC_FUSION_NOSTR_PUBLIC_KEY: process.env.NEXT_PUBLIC_FUSION_NOSTR_PUBLIC_KEY,
NEXT_PUBLIC_NEUROFUSION_BACKEND_URL: process.env.NEXT_PUBLIC_NEUROFUSION_BACKEND_URL,
NEXT_PUBLIC_ANALYSIS_SERVER_URL: process.env.NEXT_PUBLIC_ANALYSIS_SERVER_URL,
NEXT_PUBLIC_APP_INSIGHTS_KEY: process.env.NEXT_PUBLIC_APP_INSIGHTS_KEY,
},
};
Loading

0 comments on commit 94938d5

Please sign in to comment.