This shows you the differences between two versions of the page.
pm:prj2025:apredescu:victor.mandescu [2025/05/18 18:02] victor.mandescu |
pm:prj2025:apredescu:victor.mandescu [2025/05/29 14:44] (current) victor.mandescu |
||
---|---|---|---|
Line 17: | Line 17: | ||
Sistemul EEG propus capteaza semnalul cerebral prin electrozi conectati la un amplificator si apoi la convertorul ADS1115. Semnalul este transmis catre ESP32, care il proceseaza in timp real. Datele apoi sunt afisate pe un ecran OLED si vor fi salvate pe un card microSD. Zgomotul de 50/60 Hz si altele sunt filtrate digital pentru a obtine o clasificare precisa in benzi cerebrale. | Sistemul EEG propus capteaza semnalul cerebral prin electrozi conectati la un amplificator si apoi la convertorul ADS1115. Semnalul este transmis catre ESP32, care il proceseaza in timp real. Datele apoi sunt afisate pe un ecran OLED si vor fi salvate pe un card microSD. Zgomotul de 50/60 Hz si altele sunt filtrate digital pentru a obtine o clasificare precisa in benzi cerebrale. | ||
- | {{victor.mandescu_bloc.png?800x700}} | + | {{victor_mandescu_bdiagram.jpg?800x700}} |
===== Hardware Design ===== | ===== Hardware Design ===== | ||
Line 28: | Line 28: | ||
* OLED SSD1306 | * OLED SSD1306 | ||
* Electrozi | * Electrozi | ||
- | * microSD Card Reader | ||
* Placa PCB pentru prototipare | * Placa PCB pentru prototipare | ||
* Condensatoare, rezistente, breadboard si power bank | * Condensatoare, rezistente, breadboard si power bank | ||
* Interfete Hardware: | * Interfete Hardware: | ||
* I2C cu dispozitivele conectate (oled si ads1115) are rolul pentru a transmite date intre microcontroler si senzori/afisaj. | * I2C cu dispozitivele conectate (oled si ads1115) are rolul pentru a transmite date intre microcontroler si senzori/afisaj. | ||
- | * SPI pentru microSD Card Reader. Are rolul de a salava masuratori EEG in fisiere CSV. | + | * USART pentru transmiterea datelor de pe esp32 catre laptop, apoi prelucrate in python. |
* ADC pentru citirea semnalului EEG. Am pornit de la faptul ca semnalul preluat de la electrozi este foarte slab, de ordinul microvoltilor. Ca sa-l pot duce intr-un interval masurabil, am folosit un amplificator de instrumentatie INA333, care amplifica diferenta de potential dintre cei 2 electrozi in zona milivoltilor. Dupa amplificare, semnalul trece printr-un filtru trece-jos de tip Sallen-Key, implementat cu amplificatorul MCP6002, configurat cu o frecventa de cut-off de 40 de Hz, pentru a elimina zgomotul de retea si alte interferente. Astfel, raman doar gamele dorite ale EEG-ului: Delta, Theta, Alpha, Beta, pe care ulterior le poate analiza convertorul ADC. | * ADC pentru citirea semnalului EEG. Am pornit de la faptul ca semnalul preluat de la electrozi este foarte slab, de ordinul microvoltilor. Ca sa-l pot duce intr-un interval masurabil, am folosit un amplificator de instrumentatie INA333, care amplifica diferenta de potential dintre cei 2 electrozi in zona milivoltilor. Dupa amplificare, semnalul trece printr-un filtru trece-jos de tip Sallen-Key, implementat cu amplificatorul MCP6002, configurat cu o frecventa de cut-off de 40 de Hz, pentru a elimina zgomotul de retea si alte interferente. Astfel, raman doar gamele dorite ale EEG-ului: Delta, Theta, Alpha, Beta, pe care ulterior le poate analiza convertorul ADC. | ||
{{victor.mandescu_schema-hw.jpg}} | {{victor.mandescu_schema-hw.jpg}} | ||
===== Software Design ===== | ===== Software Design ===== | ||
+ | Acest cod de Arduino are rolul de a afisa in timp real graficul evolutiei tensiunii EEG preluate de la convertorul analog-digital ADS1115. Tensiunea este citita de pe canalul A0 si transmisa atat catre ecranul OLED pentru vizualizare. Prelucrarea ulterioara a semnalului EEG (incadrarea in benzi de frecventa) este realizata de codul python. | ||
+ | <file ino eeg_final_code.ino> | ||
+ | #include <Wire.h> | ||
+ | #include <Adafruit_ADS1X15.h> | ||
+ | #include <Adafruit_GFX.h> | ||
+ | #include <Adafruit_SSD1306.h> | ||
+ | #define SCREEN_WIDTH 128 | ||
+ | #define SCREEN_HEIGHT 64 | ||
+ | #define GRAPH_WIDTH 128 | ||
+ | #define GRAPH_HEIGHT 40 | ||
+ | #define GRAPH_Y_OFFSET 20 | ||
- | <note tip> | + | Adafruit_ADS1115 ads; |
- | Descrierea codului aplicaţiei (firmware): | + | Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1); |
- | * mediu de dezvoltare (if any) (e.g. AVR Studio, CodeVisionAVR) | + | |
- | * librării şi surse 3rd-party (e.g. Procyon AVRlib) | + | float dataPoints[GRAPH_WIDTH]; |
- | * algoritmi şi structuri pe care plănuiţi să le implementaţi | + | int currentX = 0; |
- | * (etapa 3) surse şi funcţii implementate | + | float visualGain = 10.0; |
- | </note> | + | |
+ | unsigned long lastSample = 0; | ||
+ | unsigned long sampleCount = 0; | ||
+ | |||
+ | void setup() { | ||
+ | Serial.begin(115200); | ||
+ | Wire.begin(); | ||
+ | |||
+ | if (!ads.begin()) { | ||
+ | Serial.println("ADS1115 error!"); | ||
+ | while (1); | ||
+ | } | ||
+ | |||
+ | if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { | ||
+ | Serial.println("Display error!"); | ||
+ | while (1); | ||
+ | } | ||
+ | |||
+ | ads.setGain(GAIN_ONE); | ||
+ | ads.setDataRate(RATE_ADS1115_860SPS); | ||
+ | |||
+ | display.clearDisplay(); | ||
+ | display.setTextSize(1); | ||
+ | display.setTextColor(SSD1306_WHITE); | ||
+ | display.setCursor(0, 0); | ||
+ | display.println("EEG ready"); | ||
+ | display.display(); | ||
+ | |||
+ | Serial.println("timestamp,raw,voltage"); | ||
+ | } | ||
+ | |||
+ | void loop() { | ||
+ | unsigned long now = micros(); | ||
+ | |||
+ | if (now - lastSample >= 4000) { | ||
+ | lastSample = now; | ||
+ | sampleCount++; | ||
+ | |||
+ | unsigned long timestamp = millis(); | ||
+ | int16_t raw = ads.readADC_SingleEnded(0); | ||
+ | float voltage = raw * 4.096 / 32767.0; | ||
+ | |||
+ | Serial.print(timestamp); | ||
+ | Serial.print(","); | ||
+ | Serial.print(raw); | ||
+ | Serial.print(","); | ||
+ | Serial.println(voltage, 6); | ||
+ | |||
+ | float amplified = voltage * visualGain; | ||
+ | |||
+ | dataPoints[currentX] = amplified; | ||
+ | currentX = (currentX + 1) % GRAPH_WIDTH; | ||
+ | |||
+ | drawWaveform(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | void drawWaveform() { | ||
+ | display.clearDisplay(); | ||
+ | |||
+ | float minVal = dataPoints[0]; | ||
+ | float maxVal = dataPoints[0]; | ||
+ | for (int i = 1; i < GRAPH_WIDTH; i++) { | ||
+ | if (dataPoints[i] < minVal) minVal = dataPoints[i]; | ||
+ | if (dataPoints[i] > maxVal) maxVal = dataPoints[i]; | ||
+ | } | ||
+ | |||
+ | float range = maxVal - minVal; | ||
+ | if (range < 0.001) range = 0.001; | ||
+ | |||
+ | display.setTextSize(1); | ||
+ | display.setCursor(0, 0); | ||
+ | display.print("V:"); | ||
+ | display.print(dataPoints[(currentX - 1 + GRAPH_WIDTH) % GRAPH_WIDTH], 2); | ||
+ | display.setCursor(65, 0); | ||
+ | display.print("R:"); | ||
+ | display.print(range, 2); | ||
+ | |||
+ | int baselineY = GRAPH_Y_OFFSET + GRAPH_HEIGHT / 2; | ||
+ | for (int x = 0; x < GRAPH_WIDTH; x += 10) { | ||
+ | display.drawPixel(x, baselineY, SSD1306_WHITE); | ||
+ | } | ||
+ | |||
+ | for (int i = 0; i < GRAPH_WIDTH - 1; i++) { | ||
+ | int x1 = i; | ||
+ | int x2 = i + 1; | ||
+ | |||
+ | int y1 = GRAPH_Y_OFFSET + GRAPH_HEIGHT - ((dataPoints[i] - minVal) / range * GRAPH_HEIGHT); | ||
+ | int y2 = GRAPH_Y_OFFSET + GRAPH_HEIGHT - ((dataPoints[x2 % GRAPH_WIDTH] - minVal) / range * GRAPH_HEIGHT); | ||
+ | |||
+ | y1 = constrain(y1, GRAPH_Y_OFFSET, GRAPH_Y_OFFSET + GRAPH_HEIGHT); | ||
+ | y2 = constrain(y2, GRAPH_Y_OFFSET, GRAPH_Y_OFFSET + GRAPH_HEIGHT); | ||
+ | |||
+ | display.drawLine(x1, y1, x2, y2, SSD1306_WHITE); | ||
+ | } | ||
+ | |||
+ | int markerY = GRAPH_Y_OFFSET + GRAPH_HEIGHT - ((dataPoints[currentX] - minVal) / range * GRAPH_HEIGHT); | ||
+ | markerY = constrain(markerY, GRAPH_Y_OFFSET, GRAPH_Y_OFFSET + GRAPH_HEIGHT); | ||
+ | display.fillCircle(currentX, markerY, 1, SSD1306_WHITE); | ||
+ | |||
+ | display.display(); | ||
+ | } | ||
+ | |||
+ | </file> | ||
+ | In implementarea mea am folosit doua coduri de python. | ||
+ | |||
+ | Primul script de python are rolul de a colecta datele transmise de ESP32, folosind biblioteca pyserial. Prin aceasta metoda pot capta in timp real tensiunile masurate cu o rata de esantionare de 250 Hz. Aceasta valoare este cea mai potrivita pentru analiza EEG (teorema Nyquist). Datele apoi sunt salvate intr-un fisier CSV, pentru a fi procesate de o biblioteca cu metode numerice mai avansate comparativ cu Arduino, in special pentru prelucrarea semnalelor. | ||
+ | |||
+ | Al doilea cod realizeaza analiza de frecventa a semnalului EEG prin aplicarea Transformatei Fourier. Pe baza acesteia am incadrat fiecare valoare in banda ei specifica, apoi la final facandu-se o analiza prompta a tot ce s-a intamplat pe parcursul analizei. | ||
+ | Pentru observarea mai clara a semnalului, deoarece initial am fost pacalit si de osciloscop si de semnalul afisat pe oled, am crezut ca este zgomot, dar daca am amplificat artificial in codul arduino cu 10 semnalul, se poate observa foarte clar diferenta dintre zgomot si montajul electrozilor pe frunte si mastoida. | ||
+ | <file python get_data.py> | ||
+ | import serial | ||
+ | import time | ||
+ | import csv | ||
+ | |||
+ | def get_serial_port(serial_port, baud_rate, timeout=1): | ||
+ | ser = serial.Serial(serial_port, baud_rate, timeout=timeout) | ||
+ | time.sleep(2) | ||
+ | return ser | ||
+ | |||
+ | def write_to_csv(serial_conn, csv_file_path): | ||
+ | with open(csv_file_path, mode='w', newline='') as file: | ||
+ | writer = csv.writer(file) | ||
+ | writer.writerow(["timestamp", "raw", "voltage"]) | ||
+ | |||
+ | while True: | ||
+ | line = serial_conn.readline().decode(errors='ignore').strip() | ||
+ | if line.count(',') == 2: | ||
+ | timestamp, raw, voltage = line.split(",") | ||
+ | writer.writerow([timestamp.strip(), raw.strip(), voltage.strip()]) | ||
+ | print(timestamp, raw, voltage) | ||
+ | |||
+ | def main(): | ||
+ | serial_port = "COM5" | ||
+ | baud_rate = 115200 | ||
+ | csv_file = "eeg_data.csv" | ||
+ | |||
+ | ser = get_serial_port(serial_port, baud_rate) | ||
+ | write_to_csv(ser, csv_file) | ||
+ | |||
+ | if __name__ == "__main__": | ||
+ | main() | ||
+ | </file> | ||
+ | |||
+ | <file python processing_data.py> | ||
+ | import numpy as np | ||
+ | import csv | ||
+ | import matplotlib.pyplot as plt | ||
+ | import pandas as pd | ||
+ | from scipy.fft import rfft, rfftfreq | ||
+ | |||
+ | def read_csv(file_path): | ||
+ | timestamps = [] | ||
+ | raw_values = [] | ||
+ | voltage_values = [] | ||
+ | with open('eeg_data.csv', mode = 'r') as f: | ||
+ | reader = csv.reader(f) | ||
+ | next(reader) | ||
+ | for row in reader: | ||
+ | if len(row) == 3: | ||
+ | timestamps.append(float(row[0])) | ||
+ | raw_values.append(float(row[1])) | ||
+ | voltage_values.append(float(row[2])) | ||
+ | return timestamps, raw_values, voltage_values | ||
+ | |||
+ | def fourier_transform(voltage_values, samples, T): | ||
+ | signal = np.array(voltage_values) | ||
+ | fited_signal = signal * np.hamming(samples) | ||
+ | |||
+ | xf = rfftfreq(samples, T) | ||
+ | yf = rfft(fited_signal) | ||
+ | |||
+ | return xf, yf | ||
+ | |||
+ | def compute_band_powers(xf, yf, eeg_bands): | ||
+ | powers = {} | ||
+ | energy = np.abs(yf) ** 2 | ||
+ | for band_name, (f_low, f_high) in eeg_bands.items(): | ||
+ | band_power = 0 | ||
+ | for i in range(len(xf)): | ||
+ | if f_low <= xf[i] <= f_high: | ||
+ | band_power += energy[i] | ||
+ | powers[band_name] = band_power | ||
+ | return powers | ||
+ | |||
+ | def plot_eeg_bands_heatmap(band_powers): | ||
+ | plt.figure(figsize=(6, 1.5)) | ||
+ | band_labels = list(band_powers.keys()) | ||
+ | power_values = list(band_powers.values()) | ||
+ | |||
+ | plt.imshow([power_values], cmap='viridis', aspect='auto') | ||
+ | plt.xticks(ticks=np.arange(len(band_labels)), labels=band_labels) | ||
+ | plt.yticks([]) | ||
+ | plt.colorbar(label="Putere relativa") | ||
+ | plt.title("Putere pe benzi EEG") | ||
+ | plt.tight_layout() | ||
+ | plt.show() | ||
+ | def main(): | ||
+ | T = 1.0 / 250.0 | ||
+ | timestamps = [] | ||
+ | raw_values = [] | ||
+ | voltage_values = [] | ||
+ | |||
+ | timestamps, raw_values, voltage_values = read_csv('eeg_data.csv') | ||
+ | samples = len(voltage_values) | ||
+ | |||
+ | xf, yf = fourier_transform(voltage_values, samples, T) | ||
+ | |||
+ | eeg_bands = { | ||
+ | "Delta": (0.5, 4), | ||
+ | "Theta": (4, 8), | ||
+ | "Alpha": (8, 12), | ||
+ | "Beta": (12, 35), | ||
+ | "Gamma": (35, 50) | ||
+ | } | ||
+ | |||
+ | band_powers = compute_band_powers(xf, yf, eeg_bands) | ||
+ | magnitude = np.abs(yf) | ||
+ | |||
+ | print("\nPutere pe benzi EEG:") | ||
+ | for band, power in band_powers.items(): | ||
+ | print(f"{band}: {power:.2f}") | ||
+ | |||
+ | plot_eeg_bands_heatmap(band_powers) | ||
+ | |||
+ | if __name__ == "__main__": | ||
+ | main() | ||
+ | </file> | ||
===== Rezultate Obţinute ===== | ===== Rezultate Obţinute ===== | ||
+ | In urma experimentelor, se observa o diferenta clara intre zgomot si semnalul EEG valid, obtinut prin montarea corecta a electrozilor. | ||
- | <note tip> | + | De exemplu, atunci cand electrozii au fost lasati in aer, analiza in frecventa a aratat o valoare extrem de ridicata in banda Delta (~11400) semnaland un zgomot evident. |
- | Care au fost rezultatele obţinute în urma realizării proiectului vostru. | + | |
- | </note> | + | In schimb, in conditii reale, cu montajul plasat corect, s-au putut observa variatii semnificative in benzi, in functie de comportament (gama beta creste atunci cand vorbim sau gandim, alpha cand inchidem ochii si delta posibil un mic zgomot sau stare de oboseala). |
+ | |||
+ | Filmulet cu electrozii neconectati, zgomot: https://www.youtube.com/shorts/eSiJnOe5d7s | ||
+ | |||
+ | Mai jos imagini ale testarii efective cu electrozii conectati, dura foarte mult ca videoclip sa inregistrez o varietate de benzi. | ||
+ | |||
+ | {{victor.mandescu_electrozi_deschisi.jpg?400x300 }} | ||
+ | {{victor.mandescu_electrozi_inchisi.jpg?400x300 }} | ||
+ | {{ victor.mandescu_rezultate.jpg?400x300 }} | ||
===== Concluzii ===== | ===== Concluzii ===== | ||
+ | Chiar daca filtrarea hardware nu elimina complet toate sursele de zgomot, se vede clar diferenta intre semnalul zgomotos si semnalul real cu montajul aplicat corect. Atunci cand am testat, se observa variatii corelate cu activitate, de exemplue, Beta creste cand vorbesc sau ma concentrez, iar Alpha apare usor cand sunt relaxat cu ochii inchisi. Am incercat sa stau cat mai nemiscat si relaxat in timpul inregistrarii si am certitudinea ca valorile reflecta favorabil activitate cerebrala reala, nu zgomot sau miscari musculare. | ||
+ | |||
+ | Totusi, exista si niste cazuri care ne spun ca exista zgomot: componente mici din banda Gamma sau usoare variatii la Delta pot proveni din zgomot, interferente electromagnetice sau chiar miscari musculare subtile. Putem imbunatati aceste defecte printr-o filtrare hardware mai eficienta sau prelucrare digitala mai avansata. Zgomotul ramas mai poate fi redus si prin contactul bun al electroziilor cu pielea, mai ales la persoanele cu par, unde amplasarea electrozilor influenteaza mult calitatea semnalului. | ||
===== Download ===== | ===== Download ===== | ||