Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions experiment/Speller/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from .stimulus import SSVEPStimulus
from .grid import SpellerGrid
65 changes: 65 additions & 0 deletions experiment/Speller/grid.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import pygame
from .stimulus import SSVEPStimulus

class SpellerGrid:
def __init__(self, screen_res, frequencies, labels):
self.width, self.height = screen_res
self.stimuli = []

# 1. ------------------------------------ Struktura Obiektu ------------------------------------

# (3 kolumny, 2 rzędy = 6 kafelków)
cols = 3
rows = 2

# Odstęp między kafelkami
margin = 80

# 2. ------------------------------------ Symetryczne ustawianie siatki ------------------------------------


# 2. Obliczamy maksymalny możliwy rozmiar kafelka, który wejdzie na ekran
available_w = self.width - (cols + 1) * margin
available_h = self.height - (rows + 1) * margin

tile_w = available_w // cols
tile_h = available_h // rows

# Rozmiar boku kwadratu (wybieramy mniejszy, by zachować proporcje 1:1)
size = min(tile_w, tile_h)

# Całkowita szerokość i wysokość całego boxa gridu
total_grid_width = (cols * size) + ((cols - 1) * margin)
total_grid_height = (rows * size) + ((rows - 1) * margin)


start_x = (self.width - total_grid_width) // 2
start_y = (self.height - total_grid_height) // 2

# 5. ------------------------------------ Konstrukcja kafelków z uwzględnieniem offsetu ------------------------------------
for i in range(len(labels)):
col = i % cols
row = i // cols

# Nowa pozycja z uwzględnieniem start_x i start_y
x = start_x + col * (size + margin)
y = start_y + row * (size + margin)

s = SSVEPStimulus(x, y, size, frequencies[i], label=labels[i])
self.stimuli.append(s)


def update(self):
for s in self.stimuli:
s.update()


def draw(self, surface):
for s in self.stimuli:
s.draw(surface)


def set_labels(self, new_labels):
for i, label in enumerate(new_labels):
if i < len(self.stimuli):
self.stimuli[i].label = label
24 changes: 24 additions & 0 deletions experiment/Speller/menu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import pygame
import config
from Speller import SSVEPStimulus

def draw_menu(screen):
screen.fill((0, 0, 0))

btn_offline = SSVEPStimulus(x=config.WIDTH//2 - 350, y=config.HEIGHT//2 - 100,
size=300, freq=0, label="OFFLINE (Calibration)")

btn_online = SSVEPStimulus(x=config.WIDTH//2 + 50, y=config.HEIGHT//2 - 100,
size=300, freq=0, label="ONLINE\n(Speller)")

btn_offline.current_color = (0, 130, 0)
btn_online.current_color = (130, 0, 0)

btn_offline.draw(screen)
btn_online.draw(screen)

font = pygame.font.SysFont('Arial', 24)
text = font.render("Press 1 for Offline or 2 for Online", True, config.COLOR_WHITE)
screen.blit(text, (config.WIDTH//2 - text.get_width()//2, config.HEIGHT//2 + 250))

return btn_offline.rect, btn_online.rect
88 changes: 88 additions & 0 deletions experiment/Speller/stimulus.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import math
import pygame

class SSVEPStimulus:
def __init__(self, x, y, size, freq, refresh_rate=60, label=""):
self.rect = pygame.Rect(x, y, size, size)
self.freq = freq
self.refresh_rate = refresh_rate
self.label = label
self.frame_count = 0

# Kolory
self.current_color = (0, 0, 0)
self.text_color = (255, 255, 255)

pygame.font.init()
self.font = pygame.font.SysFont('Arial', int(40), bold=True)

def update(self):
t = self.frame_count / self.refresh_rate

sine_val = math.sin(2 * math.pi * self.freq * t)

intensity = int(127.5 * (sine_val * 0.5 + 1.0))

self.current_color = (intensity, 0, 0)

self.frame_count += 1

def draw(self, surface):
# Renderowanie boxa
pygame.draw.rect(surface, self.current_color, self.rect)

# Renderowanie liter w boxie
if self.label:
# Margines wewnętrzny
padding_percent = 0.05
target_width = int(self.rect.width * (1 - padding_percent * 2))

# --- Algorytm zawijania tekstu ---

explicit_segments = self.label.split('\n')
final_lines = []

line_height = self.font.get_linesize()

# Przetwarzamy każdy segment
for segment in explicit_segments:

words = segment.split(' ')
if not words or (len(words) == 1 and words[0] == ''):
final_lines.append("")
continue

current_line_words = []

for word in words:
test_line_str = ' '.join(current_line_words + [word])

width, height = self.font.size(test_line_str)

if width <= target_width:

current_line_words.append(word)
else:

final_lines.append(' '.join(current_line_words))
current_line_words = [word]

final_lines.append(' '.join(current_line_words))

# --- Renderowanie i wyśrodkowanie ---

total_text_height = len(final_lines) * line_height

start_y = self.rect.centery - (total_text_height // 2)

# Renderujemy każdą linię po kolei
for i, line_str in enumerate(final_lines):
if not line_str: continue

line_surf = self.font.render(line_str, True, self.text_color)

line_rect = line_surf.get_rect(centerx=self.rect.centerx)

line_rect.y = start_y + i * line_height

surface.blit(line_surf, line_rect)
57 changes: 57 additions & 0 deletions experiment/config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

# --- Ustawienia ekranu ---
WIDTH, HEIGHT = 1280, 720
REFRESH_RATE = 60

# --- Parametry SSVEP ---
# 6 częstotliwości
FREQS = [7.5, 8.57, 10.0, 12.0, 15.0, 8.0]

# --- Struktura Alfabetu (Drzewo) ---

ALPHABET_TREE = {
"root": [
"UNDO",
"START",
"A B C D\nE F G H\nI J K L\nM N O P",
"Q R S T\nU V W X\nY Z + -\n* / ( )",
"1 2 3 4\n5 6 7 8\n9 0",
"$ & @ \"\n. , ? !\n% : ; =\n~ # Del Blank"
],

# Poddrzewo Grupy 1 (Litery A-P)
"A B C D\nE F G H\nI J K L\nM N O P": [
"BACK", "MAIN",
"A B C D", "E F G H",
"I J K L", "M N O P"
],

# Poddrzewo Grupy 2 (Litery Q-Z i symbole)
"Q R S T\nU V W X\nY Z + -\n* / ( )": [
"BACK", "MAIN",
"Q R S T", "U V W X",
"Y Z + -", "* / ( )"
],

# Poddrzewo Grupy 3 (Cyfry)
"1 2 3 4\n5 6 7 8\n9 0": [
"BACK", "MAIN",
"1 2", "3 4",
"5 6", "7 8 9 0"
],

# Liście (konkretne litery) - przykład dla pierwszej grupy
"A B C D": ["BACK", "MAIN", "A", "B", "C", "D"],
"E F G H": ["BACK", "MAIN", "E", "F", "G", "H"],
"I J K L": ["BACK", "MAIN", "I", "J", "K", "L"],
"M N O P": ["BACK", "MAIN", "M", "N", "O", "P"],

# Przykład dla cyfr
"1 2": ["BACK", "MAIN", "1", "2", "-", "-"],
}

TILE_MARGIN = 40

MENU_OPTIONS = ["1. OFFLINE", "2. ONLINE"]
COLOR_WHITE = (255, 255, 255)
COLOR_GRAY = (50, 50, 50)
Empty file removed experiment/gui/__init__.py
Empty file.
68 changes: 68 additions & 0 deletions experiment/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import pygame
import sys
import gc

from config import *
from Speller import SSVEPStimulus, SpellerGrid
from Speller import menu


# 1. PyGame setup
pygame.init()

# 2. Ustawienia ekranu
screen = pygame.display.set_mode((WIDTH, HEIGHT), vsync=1)
pygame.display.set_caption("SSVEP Speller Experiment")

# 3. Zegar
clock = pygame.time.Clock()

# 4. Tworzymy obiekt
grid = SpellerGrid((WIDTH, HEIGHT), FREQS, ALPHABET_TREE["root"])

state = "MENU"

running = True

while running:
events = pygame.event.get()

for event in events:
gc.collect()

if event.type == pygame.QUIT:
running = False

if state == "MENU" and event.type == pygame.KEYDOWN:
if event.key == pygame.K_1:
gc.disable()

state = "OFFLINE"
print("Startujemy fazę kalibracji...")

if event.key == pygame.K_2:
gc.disable()

state = "ONLINE"
print("Startujemy Speller...")


screen.fill((0, 0, 0))

if state == "MENU":
menu.draw_menu(screen)

if state == "OFFLINE":
grid.update()
grid.draw(screen)

if state == "ONLINE":
grid.update()
grid.draw(screen)

pygame.display.flip()

clock.tick(60)

pygame.quit()
sys.exit()