Commit 65c73dc7 by Laura Rodríguez

12 jun

parent c5ccffbe
import pygame
import random
import sys
import time
SCREEN_WIDTH, SCREEN_HEIGHT = 1920, 1080
CENTER = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
TARGET_OFFSET = 382
LEFT_POS = (CENTER[0] - TARGET_OFFSET, CENTER[1])
RIGHT_POS = (CENTER[0] + TARGET_OFFSET, CENTER[1])
ARROW_OFFSET_Y = 60 # altura flecha arriba target
RADIUS = 38
def draw_arrow(screen, position, direction):
color = (255, 255, 255)
x, y = position
y -= ARROW_OFFSET_Y
if direction == "left":
points = [(x, y), (x + 20, y + 10), (x + 20, y - 10)]
else: # right
points = [(x, y), (x - 20, y + 10), (x - 20, y - 10)]
pygame.draw.polygon(screen, color, points)
def wait_for_response(timeout=3000):
start = time.time()
while (time.time() - start) * 1000 < timeout:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key in [pygame.K_LEFT, pygame.K_RIGHT]:
return event.key
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.time.delay(10)
return None
def pro_saccade(screen):
# Target verde indica mirar al target (pro-saccade)
screen.fill((0,0,0))
side = random.choice(["left", "right"])
pos = LEFT_POS if side == "left" else RIGHT_POS
# Dibuja target verde en el lado correcto
pygame.draw.circle(screen, (0,255,0), pos, RADIUS)
# Dibuja target rojo en el lado contrario (no target)
other_pos = RIGHT_POS if side == "left" else LEFT_POS
pygame.draw.circle(screen, (255,0,0), other_pos, RADIUS)
# Flecha arriba en el target correcto
draw_arrow(screen, pos, side)
pygame.display.flip()
key = wait_for_response()
if key == pygame.K_LEFT:
resp = "left"
elif key == pygame.K_RIGHT:
resp = "right"
else:
resp = None
correcto = (resp == side)
return correcto, resp, side
def anti_saccade(screen):
# Target rojo indica mirar al lado contrario (anti-saccade)
screen.fill((0,0,0))
side = random.choice(["left", "right"])
pos = LEFT_POS if side == "left" else RIGHT_POS
# Dibuja target rojo en lado seleccionado
pygame.draw.circle(screen, (255,0,0), pos, RADIUS)
# Dibuja target verde en lado contrario
other_pos = RIGHT_POS if side == "left" else LEFT_POS
pygame.draw.circle(screen, (0,255,0), other_pos, RADIUS)
# Flecha arriba en el lado contrario al target rojo (donde debe mirar)
arrow_side = "right" if side == "left" else "left"
arrow_pos = LEFT_POS if arrow_side == "left" else RIGHT_POS
draw_arrow(screen, arrow_pos, arrow_side)
pygame.display.flip()
key = wait_for_response()
if key == pygame.K_LEFT:
resp = "left"
elif key == pygame.K_RIGHT:
resp = "right"
else:
resp = None
# Correcto si mira al lado contrario al target rojo
correcto = (resp == arrow_side)
return correcto, resp, arrow_side
def no_go(screen):
# Estímulo amarillo = No mover la mirada (NoGo)
screen.fill((0,0,0))
pygame.draw.circle(screen, (255, 255, 0), CENTER, RADIUS) # amarillo centro
pygame.display.flip()
key = wait_for_response(2000) # Espera respuesta pero no debe moverse
if key is None:
# Correcto si no responde (no se mueve)
correcto = True
resp = None
else:
# Incorrecto si responde (se mueve)
correcto = False
resp = "movimiento"
return correcto, resp, "centro"
def fixation(screen):
# Mantener fijación centro 2 seg
screen.fill((0,0,0))
pygame.draw.circle(screen, (255, 255, 255), CENTER, RADIUS)
pygame.display.flip()
start = time.time()
while (time.time() - start)*1000 < 2000:
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.time.delay(10)
return True, None, "centro"
def saccade_simple(screen):
# Aparace target verde a izquierda o derecha sin flecha, debe mirar ahí
screen.fill((0,0,0))
side = random.choice(["left", "right"])
pos = LEFT_POS if side == "left" else RIGHT_POS
other_pos = RIGHT_POS if side == "left" else LEFT_POS
pygame.draw.circle(screen, (0, 255, 0), pos, RADIUS)
pygame.draw.circle(screen, (255, 0, 0), other_pos, RADIUS)
pygame.display.flip()
key = wait_for_response()
if key == pygame.K_LEFT:
resp = "left"
elif key == pygame.K_RIGHT:
resp = "right"
else:
resp = None
correcto = (resp == side)
return correcto, resp, side
def main():
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Pruebas oficiales Pro/Anti/NoGo")
tests = [
("Pro-saccade", pro_saccade),
("Anti-saccade", anti_saccade),
("NoGo", no_go),
("Fijación", fixation),
("Saccade simple", saccade_simple),
]
for i, (name, func) in enumerate(tests, 1):
screen.fill((0, 0, 0))
font = pygame.font.SysFont(None, 48)
text = font.render(f"Prueba {i}: {name}", True, (255, 255, 255))
rect = text.get_rect(center=(CENTER[0], CENTER[1] - 200))
screen.blit(text, rect)
pygame.display.flip()
pygame.time.delay(1500)
result = func(screen)
correcto, resp, lado = result
print(f"Prueba {i} - {name}: Correcto? {correcto}, Respuesta: {resp}, Lado esperado: {lado}")
# Pausa entre pruebas
font_small = pygame.font.SysFont(None, 36)
text2 = font_small.render("Presiona ESPACIO para continuar...", True, (255, 255, 255))
rect2 = text2.get_rect(center=(CENTER[0], CENTER[1] + 200))
screen.blit(text2, rect2)
pygame.display.flip()
waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
waiting = False
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
pygame.quit()
if __name__ == "__main__":
main()
import sys
import random
import time
from matplotlib.pyplot import margins
from numpy import disp
import pygame
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
from pygaze import eyetracker, libinput, libscreen, liblog
from pygaze import libtime
import tobii_research as tr
import constants # Debe contener: DISPSIZE = (1920, 1080)
import pandas as pd # Importamos pandas para guardar los resultados en un archivo Excel
import subprocess # Para abrir el archivo con el programa predeterminado
import os # Para comprobar la existencia del archivo
from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QPushButton,
QLabel, QMessageBox
)
from PyQt6.QtGui import QFont
from PyQt6.QtCore import Qt
# ============ FUNCIÓN DE CALIBRACIÓN ============
def calibrate_tobii(self):
try:
print("Buscando dispositivo...")
found_eyes = tr.find_all_eyetrackers()
if not found_eyes:
raise Exception("No se encontró ningún dispositivo Tobii Pro.")
eye_tracker = found_eyes[0]
print(f"Dispositivo encontrado: {eye_tracker}")
self.disp = libscreen.Display(bgcolor=(0, 0, 0)) # Fondo negro
self.tracker = eyetracker.EyeTracker(self.disp)
instruction_screen = libscreen.Screen()
instruction_screen.clear(colour=(0, 0, 0))
instruction_screen.draw_text("Presiona espacio para calibrar", fontsize=24, colour=(255, 255, 255))
self.disp.fill(instruction_screen)
self.disp.show()
print("Iniciando calibración...")
self.tracker.calibrate()
print("Calibración completada.")
# self.tracker.close()
self.disp.close() #cierro display
return True
except Exception as e:
print(f"Error durante calibración: {e}")
return False
# ============ TEST GO/NOGO ======================
def run_bateria_test(self):
pygame.init()
self.disp = libscreen.Display(bgcolor=(0, 0, 0)) # abro display de nuevo
log = liblog.Logfile()
log.write(["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"])
SCREEN_WIDTH, SCREEN_HEIGHT = constants.DISPSIZE
CENTER = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
TARGET_OFFSET = 382
left_pos = (CENTER[0] - TARGET_OFFSET, CENTER[1])
right_pos = (CENTER[0] + TARGET_OFFSET, CENTER[1])
blackscreen = libscreen.Screen()
blackscreen.clear(colour=(0, 0, 0))
inscreen = libscreen.Screen()
inscreen.clear(colour=(0, 0, 0))
fixscreen = libscreen.Screen()
fixscreen.clear(colour=(0, 0, 0))
fixscreen.draw_fixation(fixtype='cross', diameter=57, pos=CENTER, colour=(255, 255, 255))
signal_screens = {
'PS': libscreen.Screen(),
'AS': libscreen.Screen(),
'NoGo': libscreen.Screen()
}
for key, color in [('PS', (0, 255, 0)), ('AS', (255, 0, 0)), ('NoGo', (255, 165, 0))]:
signal_screens[key].clear(colour=(0, 0, 0))
signal_screens[key].draw_circle(pos=CENTER, r=38, fill=True, colour=color)
feedbackscreens = {
1: libscreen.Screen(),
0: libscreen.Screen()
}
feedbackscreens[1].clear(colour=(0, 0, 0))
feedbackscreens[1].draw_text(text='Correcto', fontsize=30, pos=CENTER, colour=(0, 255, 0), center=True)
feedbackscreens[0].clear(colour=(0, 0, 0))
feedbackscreens[0].draw_text(text='Incorrecto', fontsize=30, pos=CENTER, colour=(255, 0, 0), center=True)
self.disp.fill(inscreen)
self.disp.show()
waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
waiting = False
elif event.key == pygame.K_e:
pygame.quit()
return
# Orden fijo de las pruebas:
fixed_trialtypes = ['PS', 'AS', 'NoGo', 'AS', 'NoGo']
results = []
try:
for trialnr, trialtype in enumerate(fixed_trialtypes, start=1):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN and event.key == pygame.K_e:
raise KeyboardInterrupt
# Mostrar instrucción previa según tipo de prueba
instruction_text = ""
if trialtype == 'PS':
instruction_text = "Mira al círculo GRANDE"
elif trialtype == 'AS':
instruction_text = "Mira al círculo PEQUEÑO"
elif trialtype == 'NoGo':
instruction_text = "NO mires ningún círculo\nMantente en el centro"
trial_instruction = libscreen.Screen()
trial_instruction.clear(colour=(0, 0, 0))
trial_instruction.draw_text(text=instruction_text, fontsize=40, pos=CENTER, colour=(255, 255, 255), center=True)
self.disp.fill(trial_instruction)
self.disp.show()
time.sleep(1.5)
# Mostrar cruz de fijación
self.disp.fill(fixscreen)
self.disp.show()
libtime.pause(random.randint(750, 1250))
# Esperar que el usuario presione espacio
waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
waiting = False
elif event.key == pygame.K_e:
raise KeyboardInterrupt
time.sleep(0.2)
self.tracker.start_recording()
# Mostrar señal central de color
self.disp.fill(signal_screens[trialtype])
self.disp.show()
time.sleep(0.8)
self.disp.fill(blackscreen)
self.disp.show()
time.sleep(0.2)
targetside = random.choice(['left', 'right'])
target_pos = left_pos if targetside == 'left' else right_pos
nontarget_pos = right_pos if targetside == 'left' else left_pos
dualstim_screen = libscreen.Screen()
dualstim_screen.clear(colour=(0, 0, 0))
dualstim_screen.draw_circle(pos=nontarget_pos, r=28, fill=True, colour=(255, 255, 255))
dualstim_screen.draw_circle(pos=target_pos, r=38, fill=True, colour=(255, 255, 255))
dualstim_screen.draw_circle(pos=target_pos, r=32, fill=True, colour=(0, 0, 0))
dualstim_screen.draw_circle(pos=target_pos, r=28, fill=True, colour=(255, 255, 255))
self.disp.fill(dualstim_screen)
self.disp.show()
t0 = pygame.time.get_ticks()
self.tracker.log(f"target {targetside}")
t1, startpos = self.tracker.wait_for_saccade_start()
endtime, startpos, endpos = self.tracker.wait_for_saccade_end()
latency = (t1 - t0) / 1000
self.tracker.stop_recording()
if trialtype == 'PS':
correct = int(
(targetside == 'left' and endpos[0] <= constants.DISPSIZE[0]/2) or
(targetside == 'right' and endpos[0] >= constants.DISPSIZE[0]/2)
)
elif trialtype == 'AS':
correct = int(
(targetside == 'left' and endpos[0] >= constants.DISPSIZE[0]/2) or
(targetside == 'right' and endpos[0] <= constants.DISPSIZE[0]/2)
)
elif trialtype == 'NoGo':
# Para NoGo: el correcto es no mover la mirada fuera del centro
correct = int(constants.DISPSIZE[0]/2 - 100 < endpos[0] < constants.DISPSIZE[0]/2 + 100)
if correct >= 0:
dualstim_screen.clear(colour=(0, 0, 0))
self.disp.show()
self.disp.fill(feedbackscreens[correct])
self.disp.show()
time.sleep(0.5)
results.append([trialnr, trialtype, targetside, endpos, round(latency, 3), correct])
except KeyboardInterrupt:
print("Test interrumpido por el usuario con la tecla 'E'.")
finally:
log.close()
self.tracker.close()
self.disp.close()
print("Test Go/NoGo finalizado.")
df = pd.DataFrame(results, columns=["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"])
df.to_excel("gonogo_results.xlsx", index=False)
print("Resultados guardados en 'gonogo_results.xlsx'.")
# ============ INTERFAZ PYQT ===================
class EyeTrackingApp(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Test Go/NoGo con Tobii")
self.setGeometry(100, 100, 400, 300)
self.calibrated = False
self.setup_ui()
self.disp = None
self.tracker = None
def setup_ui(self):
self.setStyleSheet("background-color: #e6f0ff;")
button_style = """
QPushButton {
background-color: #3399ff;
color: white;
padding: 10px;
border-radius: 5px;
font-weight: bold;
}
QPushButton:hover {
background-color: #1a8cff;
color: #e6f7ff;
}
QPushButton:pressed {
background-color: #006bb3;
color: #ccf2ff;
padding-top: 11px;
padding-bottom: 9px;
}
"""
layout = QVBoxLayout()
title = QLabel("Test Go/NoGo con Tobii")
title.setFont(QFont("Arial", 18, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title)
self.btn_calibrate = QPushButton("Calibrar")
self.btn_calibrate.setFont(QFont("Arial", 14))
self.btn_calibrate.setStyleSheet(button_style)
self.btn_calibrate.clicked.connect(self.calibrate)
layout.addWidget(self.btn_calibrate)
self.btn_test = QPushButton("Bateria pruebas")
self.btn_test.setFont(QFont("Arial", 14))
self.btn_test.setStyleSheet(button_style)
self.btn_test.clicked.connect(self.start_test)
layout.addWidget(self.btn_test)
self.btn_results = QPushButton("Consultar resultados")
self.btn_results.setFont(QFont("Arial", 14))
self.btn_results.setStyleSheet(button_style)
self.btn_results.clicked.connect(self.view_results)
layout.addWidget(self.btn_results)
self.setLayout(layout)
def calibrate(self):
result = calibrate_tobii(self)
if result:
self.calibrated = True
QMessageBox.information(self, "Calibración", "Calibración completada con éxito.")
else:
QMessageBox.critical(self, "Error", "La calibración ha fallado.")
def start_test(self):
# if not self.calibrated:
# QMessageBox.warning(self, "Calibración requerida", "Debes calibrar antes de realizar el test.")
# return
run_bateria_test(self)
def view_results(self):
file_path = "gonogo_results.xlsx"
if os.path.exists(file_path):
try:
subprocess.Popen([file_path], shell=True)
except Exception as e:
QMessageBox.critical(self, "Error", f"No se pudo abrir el archivo Excel. {e}")
else:
QMessageBox.warning(self, "Archivo no encontrado", "El archivo de resultados no existe.")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = EyeTrackingApp()
window.show()
sys.exit(app.exec())
\ No newline at end of file
import sys
import random
import time
import threading
import pygame
import pandas as pd
import os
import subprocess
from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QLabel, QPushButton
)
from PyQt6.QtGui import QFont, QColor, QPalette
from PyQt6.QtCore import Qt
from pygaze.display import Display
from pygaze.eyetracker import EyeTracker
# --- CONSTANTES DEL EXPERIMENTO ---
SCREEN_WIDTH, SCREEN_HEIGHT = 1920, 1080
CENTER = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
TARGET_OFFSET = 382
LEFT_POS = (CENTER[0] - TARGET_OFFSET, CENTER[1])
RIGHT_POS = (CENTER[0] + TARGET_OFFSET, CENTER[1])
RESULTS_FILENAME = "compelled_saccade_results.xlsx"
# Colores para feedback: verde si correcto, rojo si incorrecto
feedbackscreens = {
True: (0, 255, 0), # verde
False: (255, 0, 0) # rojo
}
def feedback_screen(disp: Display, correct: bool):
disp.fill(feedbackscreens[correct])
disp.show()
time.sleep(0.5) # mostrar 0.5 segundos
def wait_for_saccade_start(tracker, timeout=2000):
"""Espera hasta detectar inicio de sacada o timeout (ms)."""
start = time.time()
while (time.time() - start) * 1000 < timeout:
sample = tracker.sample()
if sample and hasattr(sample, "saccade") and sample.saccade:
return True
pygame.time.delay(5)
return False
def wait_for_saccade_end(tracker, timeout=2000):
"""Espera hasta fin de sacada y devuelve la muestra con info."""
start = time.time()
while (time.time() - start) * 1000 < timeout:
sample = tracker.sample()
if sample and hasattr(sample, "end_pos") and sample.end_pos is not None:
return sample
pygame.time.delay(5)
return None
def compelled_saccade_test(trials_per_block=30, n_blocks=3):
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Compelled Saccade Task")
font = pygame.font.SysFont(None, 36)
def draw_text(text, pos, color=(255, 255, 255)):
img = font.render(text, True, color)
rect = img.get_rect(center=pos)
screen.blit(img, rect)
def check_exit_key():
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN and event.key == pygame.K_e:
pygame.quit()
sys.exit()
def color_name(rgb):
if rgb == (0, 255, 0): return "Verde"
elif rgb == (255, 0, 0): return "Rojo"
elif rgb == (255, 255, 0): return "Amarillo"
else: return "Desconocido"
def pause_between_blocks(block_num, total_blocks):
screen.fill((0, 0, 0))
draw_text(f"Bloque {block_num}/{total_blocks} completado", (CENTER[0], CENTER[1] - 40))
draw_text("Presiona ESPACIO para continuar o E para salir", (CENTER[0], CENTER[1] + 20))
pygame.display.flip()
waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
waiting = False
elif event.key == pygame.K_e:
pygame.quit()
sys.exit()
disp = Display()
tracker = EyeTracker(disp)
# tracker.calibrate()
results = []
radius = 38
trial_counter = 1
for block in range(1, n_blocks + 1):
for trial in range(1, trials_per_block + 1):
check_exit_key()
# Estímulo central rojo
stim_color = (255, 0, 0)
screen.fill((0, 0, 0))
pygame.draw.circle(screen, stim_color, CENTER, radius)
pygame.display.flip()
pygame.time.delay(800)
# Estímulos amarillos anticipatorios
screen.fill((0, 0, 0))
pygame.draw.circle(screen, stim_color, CENTER, radius)
pygame.draw.circle(screen, (255, 255, 0), LEFT_POS, radius)
pygame.draw.circle(screen, (255, 255, 0), RIGHT_POS, radius)
pygame.display.flip()
pygame.time.delay(800)
# Colores laterales: uno verde, otro rojo aleatorio
if random.choice([True, False]):
left_color, right_color = (0, 255, 0), (255, 0, 0)
else:
left_color, right_color = (255, 0, 0), (0, 255, 0)
screen.fill((0, 0, 0))
pygame.draw.circle(screen, left_color, LEFT_POS, radius)
pygame.draw.circle(screen, right_color, RIGHT_POS, radius)
pygame.display.flip()
tracker.start_recording()
tracker.log("TARGETS_ON")
saccade = None
# Esperar inicio sacada
if wait_for_saccade_start(tracker):
# Esperar fin sacada y obtener datos
saccade = wait_for_saccade_end(tracker)
if saccade:
end_x = saccade.end_pos[0]
choice = 'left' if abs(end_x - LEFT_POS[0]) < abs(end_x - RIGHT_POS[0]) else 'right'
chosen_color = left_color if choice == 'left' else right_color
correct = int(chosen_color == (0, 255, 0)) # verde correcto
rt = saccade.latency * 1000
feedback_screen(disp, correct)
else:
# No sacada detectada (omisión)
choice = 'none'
chosen_color = "Ninguno"
correct = 0
rt = None
tracker.stop_recording()
trial_data = {
"trial": trial_counter,
"block": block,
"stim_color": color_name(stim_color),
"left_color": color_name(left_color),
"right_color": color_name(right_color),
"choice": choice,
"chosen_color": color_name(chosen_color) if isinstance(chosen_color, tuple) else chosen_color,
"RT_ms": rt,
"correct": correct
}
results.append(trial_data)
# Guardar resultados tras ensayo
df_trial = pd.DataFrame([trial_data])
try:
df_existing = pd.read_excel(RESULTS_FILENAME)
df_combined = pd.concat([df_existing, df_trial], ignore_index=True)
except FileNotFoundError:
df_combined = df_trial
df_combined.to_excel(RESULTS_FILENAME, index=False)
trial_counter += 1
if block < n_blocks:
pause_between_blocks(block, n_blocks)
pygame.quit()
print(f"Test finalizado. Resultados guardados en {RESULTS_FILENAME}")
def open_results_file():
if os.path.exists(RESULTS_FILENAME):
if sys.platform == "win32":
os.startfile(RESULTS_FILENAME)
elif sys.platform == "darwin":
subprocess.call(["open", RESULTS_FILENAME])
else:
subprocess.call(["xdg-open", RESULTS_FILENAME])
else:
print("No hay resultados guardados todavía.")
# --- EJECUCIÓN DIRECTA ---
if __name__ == "__main__":
compelled_saccade_test()
# # --- INTERFAZ PyQt6 ---
# class BlueWhiteInterface(QWidget):
# def __init__(self):
# super().__init__()
# self.setWindowTitle("Test de Elección Sacádica Urgente")
# self.setGeometry(100, 100, 500, 300)
# self.init_ui()
# def init_ui(self):
# self.setAutoFillBackground(True)
# palette = self.palette()
# palette.setColor(QPalette.ColorRole.Window, QColor("#FFFFFF"))
# self.setPalette(palette)
# layout = QVBoxLayout()
# title = QLabel("Compelled saccade test")
# title.setFont(QFont("Arial", 20, QFont.Weight.Bold))
# title.setStyleSheet("color: #1E90FF;")
# title.setAlignment(Qt.AlignmentFlag.AlignCenter)
# layout.addWidget(title)
# self.start_button = QPushButton("Iniciar test")
# self.start_button.setFont(QFont("Arial", 14))
# self.start_button.setStyleSheet("""
# QPushButton {
# background-color: #1E90FF;
# color: white;
# padding: 10px;
# border-radius: 10px;
# }
# QPushButton:hover {
# background-color: #187bcd;
# }
# """)
# self.start_button.clicked.connect(self.start_test)
# layout.addWidget(self.start_button, alignment=Qt.AlignmentFlag.AlignCenter)
# self.view_button = QPushButton("Ver resultados")
# self.view_button.setFont(QFont("Arial", 14))
# self.view_button.setStyleSheet("""
# QPushButton {
# background-color: #1E90FF;
# color: white;
# padding: 10px;
# border-radius: 10px;
# margin-top: 20px;
# }
# QPushButton:hover {
# background-color: #187bcd;
# }
# """)
# self.view_button.clicked.connect(open_results_file)
# layout.addWidget(self.view_button, alignment=Qt.AlignmentFlag.AlignCenter)
# self.setLayout(layout)
# def start_test(self):
# self.start_button.setEnabled(False)
# self.start_button.setText("Ejecutando...")
# thread = threading.Thread(target=self.run_test, daemon=True)
# thread.start()
# def run_test(self):
# compelled_saccade_test()
# # Volver a habilitar botón tras terminar el test
# self.start_button.setEnabled(True)
# self.start_button.setText("Iniciar test")
# # --- EJECUCIÓN ---
# if __name__ == "__main__":
# app = QApplication(sys.argv)
# window = BlueWhiteInterface()
# window.show()
# sys.exit(app.exec())
import sys
import random
import threading
import time
from matplotlib.pyplot import margins
from numpy import disp
import pygame
from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont
# from display import Display
from pygaze import eyetracker, libinput, libscreen, liblog
from pygaze import libtime
import tobii_research as tr
import constants # Debe contener: DISPSIZE = (1920, 1080)
import pandas as pd # Importamos pandas para guardar los resultados en un archivo Excel
import subprocess # Para abrir el archivo con el programa predeterminado
import os # Para comprobar la existencia del archivo
from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QPushButton,
QLabel, QMessageBox
)
from PyQt6.QtGui import QFont
from PyQt6.QtCore import Qt
from PyQt6.QtGui import QFont, QColor, QPalette
from PyQt6.QtCore import Qt
from pygaze.display import Display
from pygaze.keyboard import Keyboard
from PyQt6.QtWidgets import QGridLayout # asegúrate de importar esto arriba
# ============ LÍNEA DE CALIBRACIÓN ============
# # Crear display (pantalla)
# disp = Display()
# kb = Keyboard()
# # Obtener tamaño de pantalla
# SCREEN_WIDTH, SCREEN_HEIGHT = disp.dispsize
# center_y = SCREEN_HEIGHT // 2
# # Crear pantalla negra con línea blanca horizontal en el centro
# screen = libscreen.Screen()
# screen.clear(colour=(0, 0, 0)) # Fondo negro
# screen.draw_line(
# colour=(255, 255, 255), # Color blanco
# spos=(0, center_y), # Punto de inicio: lado izquierdo del centro
# epos=(SCREEN_WIDTH, center_y), # Punto final: lado derecho del centro
# pw=2 # Grosor de la línea
# )
# # Mostrar pantalla
# disp.fill(screen)
# disp.show()
# # Esperar hasta que se presione la tecla espacio
# while True:
# key, _ = kb.get_key()
# if key == 'space':
# break
# time.sleep(0.01)
# # Cerrar pantalla
# disp.close()
# ============ FUNCIÓN DE CALIBRACIÓN ============
def calibrate_tobii(self):
try:
print("Buscando dispositivo...")
found_eyes = tr.find_all_eyetrackers()
if not found_eyes:
raise Exception("No se encontró ningún dispositivo Tobii Pro.")
eye_tracker = found_eyes[0]
print(f"Dispositivo encontrado: {eye_tracker}")
self.disp = libscreen.Display(bgcolor=(0, 0, 0)) # Fondo negro
self.tracker = eyetracker.EyeTracker(self.disp)
instruction_screen = libscreen.Screen()
instruction_screen.clear(colour=(0, 0, 0))
instruction_screen.draw_text("Presiona espacio para calibrar", fontsize=24, colour=(255, 255, 255))
SCREEN_WIDTH, SCREEN_HEIGHT = constants.DISPSIZE
center_y = SCREEN_HEIGHT // 2
self.disp.fill(instruction_screen)
self.disp.show()
time.sleep(1)
print("Iniciando calibración...")
self.tracker.calibrate()
print("Calibración completada.")
self.disp.close() #cierro display
return True
except Exception as e:
print(f"Error durante calibración: {e}")
return False
# ============ TEST PRO/NOGO ======================
def run_pro_nogo_test(self):
pygame.init()
# self.test_window.disp = self.disp
self.disp = libscreen.Display(bgcolor=(0, 0, 0))#abro display de nuevo
# self.tracker = eyetracker.EyeTracker(self.disp)
log = liblog.Logfile()
log.write(["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"])
SCREEN_WIDTH, SCREEN_HEIGHT = constants.DISPSIZE
CENTER = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
TARGET_OFFSET = 382
left_pos = (CENTER[0] - TARGET_OFFSET, CENTER[1])
right_pos = (CENTER[0] + TARGET_OFFSET, CENTER[1])
blackscreen = libscreen.Screen()
blackscreen.clear(colour=(0, 0, 0))
inscreen = libscreen.Screen()
inscreen.clear(colour=(0, 0, 0))
ins_text = (
"Mira la cruz central y presiona 'espacio'.\n\n"
"El color indicará la acción:\n"
"- Verde: mira al círculo blanco grande)\n"
"- Naranja: NO mires a ningún estímulo (NoGo)\n\n"
"Presiona 'espacio' para comenzar.\n"
"Pulsa 'E' en cualquier momento para salir."
)
inscreen.draw_text(text=ins_text, fontsize=30, pos=CENTER, colour=(255, 255, 255), center=True)
fixscreen = libscreen.Screen()
fixscreen.clear(colour=(0, 0, 0))
fixscreen.draw_fixation(fixtype='cross', diameter=57, pos=CENTER, colour=(255, 255, 255))
signal_screens = {
'PS': libscreen.Screen(),
'NoGo': libscreen.Screen()
}
for key, color in [('PS', (0, 255, 0)), ('NoGo', (255, 165, 0))]:
signal_screens[key].clear(colour=(0, 0, 0))
signal_screens[key].draw_circle(pos=CENTER, r=38, fill=True, colour=color)
feedbackscreens = {
1: libscreen.Screen(),
0: libscreen.Screen()
}
feedbackscreens[1].clear(colour=(0, 0, 0))
feedbackscreens[1].draw_text(text='Correcto', fontsize=30, pos=CENTER, colour=(0, 255, 0), center=True)
feedbackscreens[0].clear(colour=(0, 0, 0))
feedbackscreens[0].draw_text(text='Incorrecto', fontsize=30, pos=CENTER, colour=(255, 0, 0), center=True)
self.disp.fill(inscreen)
self.disp.show()
# waiting = True
# while waiting:
# for event in pygame.event.get():
# if event.type == pygame.KEYDOWN:
# if event.key == pygame.K_SPACE:
# waiting = False
# elif event.key == pygame.K_e:
# pygame.quit()
# return
max_blocks = 30
results = []
try:
self.tracker.start_recording()
for block in range(max_blocks):
fixation_duration = random.choice([1000, 2000, 3000]) # milisegundos
trials = ['PS','NoGo']
# trials = ['PS', 'AS']
random.shuffle(trials)
for trialnr, trialtype in enumerate(trials, start=1):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN and event.key == pygame.K_e:
raise KeyboardInterrupt
# Mostrar cruz de fijación
self.disp.fill(fixscreen)
self.disp.show()
# libtime.pause(random.randint(750, 1250))
# libtime.pause(fixation_duration) # Usamos la duración fija del bloque
# Esperar que el usuario presione espacio
# waiting = True
# while waiting:
# for event in pygame.event.get():
# if event.type == pygame.KEYDOWN:
# if event.key == pygame.K_SPACE:
# waiting = False
# elif event.key == pygame.K_e:
# raise KeyboardInterrupt
time.sleep(0.2)
# Mostrar señal central de color
self.disp.fill(signal_screens[trialtype])
self.disp.show()
time.sleep(0.8)
self.disp.fill(blackscreen)
self.disp.show()
time.sleep(0.2)
targetside = random.choice(['left', 'right'])
target_pos = left_pos if targetside == 'left' else right_pos
nontarget_pos = right_pos if targetside == 'left' else left_pos
dualstim_screen = libscreen.Screen()
dualstim_screen.clear(colour=(0, 0, 0))
dualstim_screen.draw_circle(pos=nontarget_pos, r=28, fill=True, colour=(255, 255, 255))
dualstim_screen.draw_circle(pos=target_pos, r=38, fill=True, colour=(255, 255, 255))
dualstim_screen.draw_circle(pos=target_pos, r=32, fill=True, colour=(0, 0, 0))
dualstim_screen.draw_circle(pos=target_pos, r=28, fill=True, colour=(255, 255, 255))
self.disp.fill(dualstim_screen)
self.disp.show()
t0 = pygame.time.get_ticks()
self.tracker.log(f"target {targetside}")
# try:
t1, startpos = self.tracker.wait_for_saccade_start()
endtime, startpos, endpos = self.tracker.wait_for_saccade_end()
latency = (t1 - t0) / 1000
# midpoint = SCREEN_WIDTH / 2
# margin = 100
# endpos=None
if trialtype == 'PS':
correct = int(
(targetside == 'left' and endpos[0] <= constants.DISPSIZE[0] / 2) or
(targetside == 'right' and endpos[0] >= constants.DISPSIZE[0] / 2)
)
print(correct)
elif trialtype == 'NoGo':
# En NoGo no debe haber movimiento ocular (sacada)
try:
# Espera hasta 900 ms para detectar el inicio de una sacada
t1, _ = self.tracker.wait_for_saccade_start(latency=200)
correct = 0 # Si hay una sacada, se considera incorrecto
latency = (t1 - t0) / 1000 # Calcula la latencia en segundos
print("Incorrecto")
except Exception:
# Si no se detecta ninguna sacada en ese tiempo, se considera correcto
correct = 1
latency = 0 # No hay latencia porque no hubo movimiento
print("Correcto")
self.disp.fill(feedbackscreens[correct])
self.disp.show()
time.sleep(0.5)
results.append([trialnr, trialtype, targetside, endpos, round(latency, 3), correct])
self.tracker.stop_recording()
except KeyboardInterrupt:
print("Test interrumpido por el usuario con la tecla 'E'.")
finally:
log.close()
self.tracker.close()
self.disp.close()
print("Test Go/NoGo finalizado.")
df = pd.DataFrame(results, columns=["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"])
df.to_excel("gonogo_results.xlsx", index=False)
print("Resultados guardados en 'gonogo_results.xlsx'.")
# ============ TEST PR/AS ===================
def run_pr_as_test(self):
pygame.init()
# self.test_window.disp = self.disp
self.disp = libscreen.Display(bgcolor=(0, 0, 0))#abro display de nuevo
# self.tracker = eyetracker.EyeTracker(self.disp)
log = liblog.Logfile()
log.write(["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"])
SCREEN_WIDTH, SCREEN_HEIGHT = constants.DISPSIZE
CENTER = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
TARGET_OFFSET = 382
left_pos = (CENTER[0] - TARGET_OFFSET, CENTER[1])
right_pos = (CENTER[0] + TARGET_OFFSET, CENTER[1])
blackscreen = libscreen.Screen()
blackscreen.clear(colour=(0, 0, 0))
inscreen = libscreen.Screen()
inscreen.clear(colour=(0, 0, 0))
ins_text = (
"Mira la cruz central y presiona 'espacio'.\n\n"
"El color indicará la acción:\n"
"- Verde: mira al círculo blanco grande\n"
"- Rojo: mira al estímulo blanco pequeño)\n"
"Presiona 'espacio' para comenzar.\n"
"Pulsa 'E' en cualquier momento para salir."
)
inscreen.draw_text(text=ins_text, fontsize=30, pos=CENTER, colour=(255, 255, 255), center=True)
fixscreen = libscreen.Screen()
fixscreen.clear(colour=(0, 0, 0))
fixscreen.draw_fixation(fixtype='cross', diameter=57, pos=CENTER, colour=(255, 255, 255))
signal_screens = {
'PS': libscreen.Screen(),
'AS': libscreen.Screen(),
}
for key, color in [('PS', (0, 255, 0)), ('AS', (255, 0, 0))]:
signal_screens[key].clear(colour=(0, 0, 0))
signal_screens[key].draw_circle(pos=CENTER, r=38, fill=True, colour=color)
feedbackscreens = {
1: libscreen.Screen(),
0: libscreen.Screen()
}
feedbackscreens[1].clear(colour=(0, 0, 0))
feedbackscreens[1].draw_text(text='Correcto', fontsize=30, pos=CENTER, colour=(0, 255, 0), center=True)
feedbackscreens[0].clear(colour=(0, 0, 0))
feedbackscreens[0].draw_text(text='Incorrecto', fontsize=30, pos=CENTER, colour=(255, 0, 0), center=True)
self.disp.fill(inscreen)
self.disp.show()
# waiting = True
# while waiting:
# for event in pygame.event.get():
# if event.type == pygame.KEYDOWN:
# if event.key == pygame.K_SPACE:
# waiting = False
# elif event.key == pygame.K_e:
# pygame.quit()
# return
max_blocks = 30
results = []
try:
for block in range(max_blocks):
trials = ['PS', 'AS']
fixation_duration = random.choice([1000, 2000, 3000]) # milisegundos
# trials = ['PS', 'AS']
random.shuffle(trials)
for trialnr, trialtype in enumerate(trials, start=1):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN and event.key == pygame.K_e:
raise KeyboardInterrupt
# Mostrar cruz de fijación
self.disp.fill(fixscreen)
self.disp.show()
libtime.pause(random.randint(750, 1250))
libtime.pause(fixation_duration) # Usamos la duración fija del bloque
# # Esperar que el usuario presione espacio
# waiting = T rue
# while waiting:
# for event in pygame.event.get():
# if event.type == pygame.KEYDOWN:
# if event.key == pygame.K_SPACE:
# waiting = False
# elif event.key == pygame.K_e:
# raise KeyboardInterrupt
time.sleep(0.2)
self.tracker.start_recording()
# Mostrar señal central de color
self.disp.fill(signal_screens[trialtype])
self.disp.show()
time.sleep(0.8)
self.disp.fill(blackscreen)
self.disp.show()
time.sleep(0.2)
targetside = random.choice(['left', 'right'])
target_pos = left_pos if targetside == 'left' else right_pos
nontarget_pos = right_pos if targetside == 'left' else left_pos
dualstim_screen = libscreen.Screen()
dualstim_screen.clear(colour=(0, 0, 0))
dualstim_screen.draw_circle(pos=nontarget_pos, r=28, fill=True, colour=(255, 255, 255))
dualstim_screen.draw_circle(pos=target_pos, r=38, fill=True, colour=(255, 255, 255))
dualstim_screen.draw_circle(pos=target_pos, r=32, fill=True, colour=(0, 0, 0))
dualstim_screen.draw_circle(pos=target_pos, r=28, fill=True, colour=(255, 255, 255))
self.disp.fill(dualstim_screen)
self.disp.show()
t0 = pygame.time.get_ticks()
self.tracker.log(f"target {targetside}")
# try:
t1, startpos = self.tracker.wait_for_saccade_start()
endtime, startpos, endpos = self.tracker.wait_for_saccade_end()
latency = (t1 - t0) / 1000
self.tracker.stop_recording()
# midpoint = SCREEN_WIDTH / 2
# margin = 100
# endpos=None
if trialtype == 'PS':
correct = int(
(targetside == 'left' and endpos[0] <= constants.DISPSIZE[0] / 2) or
(targetside == 'right' and endpos[0] >= constants.DISPSIZE[0] / 2)
)
print(correct)
elif trialtype == 'AS':
correct = int(
(targetside == 'left' and endpos[0] >= constants.DISPSIZE[0] / 2) or
(targetside == 'right' and endpos[0] <= constants.DISPSIZE[0] / 2)
)
print(correct)
self.disp.fill(feedbackscreens[correct])
self.disp.show()
time.sleep(0.5)
results.append([trialnr, trialtype, targetside, endpos, round(latency, 3), correct])
except KeyboardInterrupt:
print("Test interrumpido por el usuario con la tecla 'E'.")
finally:
log.close()
self.tracker.close()
self.disp.close()
print("Test Pro/AS finalizado.")
df = pd.DataFrame(results, columns=["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"])
df.to_excel("gonogo_results.xlsx", index=False)
print("Resultados guardados en 'gonogo_results.xlsx'.")
# ============ TEST CS_TASK ===================
# --- CONSTANTES DEL EXPERIMENTO ---
SCREEN_WIDTH, SCREEN_HEIGHT = 1920, 1080
CENTER = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
TARGET_OFFSET = 382
LEFT_POS = (CENTER[0] - TARGET_OFFSET, CENTER[1])
RIGHT_POS = (CENTER[0] + TARGET_OFFSET, CENTER[1])
RESULTS_FILENAME = "compelled_saccade_results.xlsx"
# Colores para feedback: verde si correcto, rojo si incorrecto
feedbackscreens = {
True: (0, 255, 0), # verde
False: (255, 0, 0) # rojo
}
def feedback_screen(disp: Display, correct: bool):
disp.fill(feedbackscreens[correct])
disp.show()
time.sleep(0.5) # mostrar 0.5 segundos
def wait_for_saccade_start(tracker, timeout=2000):
"""Espera hasta detectar inicio de sacada o timeout (ms)."""
start = time.time()
while (time.time() - start) * 1000 < timeout:
sample = tracker.sample()
if sample and hasattr(sample, "saccade") and sample.saccade:
return True
pygame.time.delay(5)
return False
def wait_for_saccade_end(tracker, timeout=2000):
"""Espera hasta fin de sacada y devuelve la muestra con info."""
start = time.time()
while (time.time() - start) * 1000 < timeout:
sample = tracker.sample()
if sample and hasattr(sample, "end_pos") and sample.end_pos is not None:
return sample
pygame.time.delay(5)
return None
def compelled_saccade_test(trials_per_block=30, n_blocks=3):
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption("Compelled Saccade Task")
font = pygame.font.SysFont(None, 36)
def draw_text(text, pos, color=(255, 255, 255)):
img = font.render(text, True, color)
rect = img.get_rect(center=pos)
screen.blit(img, rect)
def check_exit_key():
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN and event.key == pygame.K_e:
pygame.quit()
sys.exit()
def color_name(rgb):
if rgb == (0, 255, 0): return "Verde"
elif rgb == (255, 0, 0): return "Rojo"
elif rgb == (255, 255, 0): return "Amarillo"
else: return "Desconocido"
def pause_between_blocks(block_num, total_blocks):
screen.fill((0, 0, 0))
draw_text(f"Bloque {block_num}/{total_blocks} completado", (CENTER[0], CENTER[1] - 40))
draw_text("Presiona ESPACIO para continuar o E para salir", (CENTER[0], CENTER[1] + 20))
pygame.display.flip()
waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
waiting = False
elif event.key == pygame.K_e:
pygame.quit()
sys.exit()
disp = Display()
tracker = eyetracker.EyeTracker(disp)
# tracker.calibrate()
results = []
radius = 38
trial_counter = 1
for block in range(1, n_blocks + 1):
for trial in range(1, trials_per_block + 1):
check_exit_key()
# Estímulo central rojo
stim_color = (255, 0, 0)
screen.fill((0, 0, 0))
pygame.draw.circle(screen, stim_color, CENTER, radius)
pygame.display.flip()
pygame.time.delay(800)
# Estímulos amarillos anticipatorios
screen.fill((0, 0, 0))
pygame.draw.circle(screen, stim_color, CENTER, radius)
pygame.draw.circle(screen, (255, 255, 0), LEFT_POS, radius)
pygame.draw.circle(screen, (255, 255, 0), RIGHT_POS, radius)
pygame.display.flip()
pygame.time.delay(800)
# Colores laterales: uno verde, otro rojo aleatorio
if random.choice([True, False]):
left_color, right_color = (0, 255, 0), (255, 0, 0)
else:
left_color, right_color = (255, 0, 0), (0, 255, 0)
screen.fill((0, 0, 0))
pygame.draw.circle(screen, left_color, LEFT_POS, radius)
pygame.draw.circle(screen, right_color, RIGHT_POS, radius)
pygame.display.flip()
tracker.start_recording()
tracker.log("TARGETS_ON")
saccade = None
# Esperar inicio sacada
if wait_for_saccade_start(tracker):
# Esperar fin sacada y obtener datos
saccade = wait_for_saccade_end(tracker)
if saccade:
end_x = saccade.end_pos[0]
choice = 'left' if abs(end_x - LEFT_POS[0]) < abs(end_x - RIGHT_POS[0]) else 'right'
chosen_color = left_color if choice == 'left' else right_color
correct = int(chosen_color == (0, 255, 0)) # verde correcto
rt = saccade.latency * 1000
feedback_screen(disp, correct)
else:
# No sacada detectada (omisión)
choice = 'none'
chosen_color = "Ninguno"
correct = 0
rt = None
tracker.stop_recording()
trial_data = {
"trial": trial_counter,
"block": block,
"stim_color": color_name(stim_color),
"left_color": color_name(left_color),
"right_color": color_name(right_color),
"choice": choice,
"chosen_color": color_name(chosen_color) if isinstance(chosen_color, tuple) else chosen_color,
"RT_ms": rt,
"correct": correct
}
results.append(trial_data)
# Guardar resultados tras ensayo
df_trial = pd.DataFrame([trial_data])
try:
df_existing = pd.read_excel(RESULTS_FILENAME)
df_combined = pd.concat([df_existing, df_trial], ignore_index=True)
except FileNotFoundError:
df_combined = df_trial
df_combined.to_excel(RESULTS_FILENAME, index=False)
trial_counter += 1
if block < n_blocks:
pause_between_blocks(block, n_blocks)
pygame.quit()
print(f"Test finalizado. Resultados guardados en {RESULTS_FILENAME}")
def open_results_file():
if os.path.exists(RESULTS_FILENAME):
if sys.platform == "win32":
os.startfile(RESULTS_FILENAME)
elif sys.platform == "darwin":
subprocess.call(["open", RESULTS_FILENAME])
else:
subprocess.call(["xdg-open", RESULTS_FILENAME])
else:
print("No hay resultados guardados todavía.")
# ============ TEST BATERIA ===================
def run_bateria_test(self):
pygame.init()
self.disp = libscreen.Display(bgcolor=(0, 0, 0)) # abro display de nuevo
log = liblog.Logfile()
log.write(["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"])
SCREEN_WIDTH, SCREEN_HEIGHT = constants.DISPSIZE
CENTER = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2)
TARGET_OFFSET = 382
left_pos = (CENTER[0] - TARGET_OFFSET, CENTER[1])
right_pos = (CENTER[0] + TARGET_OFFSET, CENTER[1])
blackscreen = libscreen.Screen()
blackscreen.clear(colour=(0, 0, 0))
inscreen = libscreen.Screen()
inscreen.clear(colour=(0, 0, 0))
fixscreen = libscreen.Screen()
fixscreen.clear(colour=(0, 0, 0))
fixscreen.draw_fixation(fixtype='cross', diameter=57, pos=CENTER, colour=(255, 255, 255))
signal_screens = {
'PS': libscreen.Screen(),
'AS': libscreen.Screen(),
'NoGo': libscreen.Screen()
}
for key, color in [('PS', (0, 255, 0)), ('AS', (255, 0, 0)), ('NoGo', (255, 165, 0))]:
signal_screens[key].clear(colour=(0, 0, 0))
signal_screens[key].draw_circle(pos=CENTER, r=38, fill=True, colour=color)
feedbackscreens = {
1: libscreen.Screen(),
0: libscreen.Screen()
}
feedbackscreens[1].clear(colour=(0, 0, 0))
feedbackscreens[1].draw_text(text='Correcto', fontsize=30, pos=CENTER, colour=(0, 255, 0), center=True)
feedbackscreens[0].clear(colour=(0, 0, 0))
feedbackscreens[0].draw_text(text='Incorrecto', fontsize=30, pos=CENTER, colour=(255, 0, 0), center=True)
self.disp.fill(inscreen)
self.disp.show()
waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
waiting = False
elif event.key == pygame.K_e:
pygame.quit()
return
# Orden fijo de las pruebas:
fixed_trialtypes = ['PS', 'AS', 'NoGo', 'AS', 'NoGo']
results = []
try:
for trialnr, trialtype in enumerate(fixed_trialtypes, start=1):
for event in pygame.event.get():
if event.type == pygame.KEYDOWN and event.key == pygame.K_e:
raise KeyboardInterrupt
# Mostrar instrucción previa según tipo de prueba
instruction_text = ""
if trialtype == 'PS':
instruction_text = "Mira al círculo GRANDE"
elif trialtype == 'AS':
instruction_text = "Mira al círculo PEQUEÑO"
elif trialtype == 'NoGo':
instruction_text = "NO mires ningún círculo\nMantente en el centro"
trial_instruction = libscreen.Screen()
trial_instruction.clear(colour=(0, 0, 0))
trial_instruction.draw_text(text=instruction_text, fontsize=40, pos=CENTER, colour=(255, 255, 255), center=True)
self.disp.fill(trial_instruction)
self.disp.show()
time.sleep(1.5)
# Mostrar cruz de fijación
self.disp.fill(fixscreen)
self.disp.show()
libtime.pause(random.randint(750, 1250))
# Esperar que el usuario presione espacio
waiting = True
while waiting:
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
waiting = False
elif event.key == pygame.K_e:
raise KeyboardInterrupt
time.sleep(0.2)
self.tracker.start_recording()
# Mostrar señal central de color
self.disp.fill(signal_screens[trialtype])
self.disp.show()
time.sleep(0.8)
self.disp.fill(blackscreen)
self.disp.show()
time.sleep(0.2)
targetside = random.choice(['left', 'right'])
target_pos = left_pos if targetside == 'left' else right_pos
nontarget_pos = right_pos if targetside == 'left' else left_pos
dualstim_screen = libscreen.Screen()
dualstim_screen.clear(colour=(0, 0, 0))
dualstim_screen.draw_circle(pos=nontarget_pos, r=28, fill=True, colour=(255, 255, 255))
dualstim_screen.draw_circle(pos=target_pos, r=38, fill=True, colour=(255, 255, 255))
dualstim_screen.draw_circle(pos=target_pos, r=32, fill=True, colour=(0, 0, 0))
dualstim_screen.draw_circle(pos=target_pos, r=28, fill=True, colour=(255, 255, 255))
self.disp.fill(dualstim_screen)
self.disp.show()
t0 = pygame.time.get_ticks()
self.tracker.log(f"target {targetside}")
t1, startpos = self.tracker.wait_for_saccade_start()
endtime, startpos, endpos = self.tracker.wait_for_saccade_end()
latency = (t1 - t0) / 1000
self.tracker.stop_recording()
if trialtype == 'PS':
correct = int(
(targetside == 'left' and endpos[0] <= constants.DISPSIZE[0]/2) or
(targetside == 'right' and endpos[0] >= constants.DISPSIZE[0]/2)
)
elif trialtype == 'AS':
correct = int(
(targetside == 'left' and endpos[0] >= constants.DISPSIZE[0]/2) or
(targetside == 'right' and endpos[0] <= constants.DISPSIZE[0]/2)
)
elif trialtype == 'NoGo':
# Para NoGo: el correcto es no mover la mirada fuera del centro
correct = int(constants.DISPSIZE[0]/2 - 100 < endpos[0] < constants.DISPSIZE[0]/2 + 100)
if correct >= 0:
dualstim_screen.clear(colour=(0, 0, 0))
self.disp.show()
self.disp.fill(feedbackscreens[correct])
self.disp.show()
time.sleep(0.5)
results.append([trialnr, trialtype, targetside, endpos, round(latency, 3), correct])
except KeyboardInterrupt:
print("Test interrumpido por el usuario con la tecla 'E'.")
finally:
log.close()
self.tracker.close()
self.disp.close()
print("Test Go/NoGo finalizado.")
df = pd.DataFrame(results, columns=["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"])
df.to_excel("gonogo_results.xlsx", index=False)
print("Resultados guardados en 'gonogo_results.xlsx'.")
# ============ INTERFAZ PYQT ===================
class EyeTrackingApp(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("EYETRACKING")
self.setGeometry(100, 100, 800, 600)
self.calibrated = False
self.setup_ui()
self.disp = None
self.tracker = None
def setup_ui(self):
self.setStyleSheet("background-color: #e6f0ff;")
blue_button_style = """
QPushButton {
background-color: #3399ff;
color: white;
padding: 10px;
border-radius: 5px;
font-weight: bold;
}
QPushButton:hover {
background-color: #1a8cff;
color: #e6f7ff;
}
QPushButton:pressed {
background-color: #006bb3;
color: #ccf2ff;
padding-top: 11px;
padding-bottom: 9px;
}
"""
green_button_style = """
QPushButton {
background-color: #33cc33;
color: white;
padding: 10px;
border-radius: 5px;
font-weight: bold;
}
QPushButton:hover {
background-color: #28a428;
color: #e6ffe6;
}
QPushButton:pressed {
background-color: #1f7a1f;
color: #ccffcc;
padding-top: 11px;
padding-bottom: 9px;
}
"""
# Añade un nuevo estilo para el botón calibrar en verde oscuro
dark_green_button_style = """
QPushButton {
background-color: #1f7a1f; /* Verde oscuro */
color: white;
padding: 10px;
border-radius: 5px;
font-weight: bold;
}
QPushButton:hover {
background-color: #145214; /* Verde aún más oscuro */
color: #e6ffe6;
}
QPushButton:pressed {
background-color: #0d3a0d; /* Verde muy oscuro */
color: #ccffcc;
padding-top: 11px;
padding-bottom: 9px;
}
"""
dark_yellow_button_style = """
QPushButton {
background-color: #b58900; /* Amarillo oscuro */
color: white;
padding: 10px;
border-radius: 5px;
font-weight: bold;
}
QPushButton:hover {
background-color: #8c6d00; /* Amarillo más oscuro */
color: #fffacd; /* Amarillo claro */
}
QPushButton:pressed {
background-color: #665200; /* Amarillo muy oscuro */
color: #fff8dc;
padding-top: 11px;
padding-bottom: 9px;
}
"""
half_green_red_style = """
QPushButton {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #33cc33, /* Verde desde el 0% */
stop: 0.5 #33cc33, /* Verde hasta el 50% */
stop: 0.5 #ff3333, /* Rojo desde el 50% */
stop: 1 #ff3333 /* Rojo hasta el 100% */
);
color: white;
padding: 10px;
border-radius: 5px;
font-weight: bold;
border: none;
}
QPushButton:hover {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #28a428,
stop: 0.5 #28a428,
stop: 0.5 #cc2929,
stop: 1 #cc2929
);
}
QPushButton:pressed {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #1f7a1f,
stop: 0.5 #1f7a1f,
stop: 0.5 #991f1f,
stop: 1 #991f1f
);
padding-top: 11px;
padding-bottom: 9px;
}
"""
green_yellow_button_style = """
QPushButton {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #33cc33, /* Verde */
stop: 0.5 #33cc33, /* Verde hasta mitad */
stop: 0.5 #ffcc00, /* Amarillo desde mitad */
stop: 1 #ffcc00 /* Amarillo */
);
color: black;
padding: 10px;
border-radius: 5px;
font-weight: bold;
border: none;
}
QPushButton:hover {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #39d339,
stop: 0.5 #39d339,
stop: 0.5 #ffd633,
stop: 1 #ffd633
);
color: black;
}
QPushButton:pressed {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #2b992b,
stop: 0.5 #2b992b,
stop: 0.5 #ccaa00,
stop: 1 #ccaa00
);
padding-top: 11px;
padding-bottom: 9px;
color: black;
}
"""
green_red_yellow_button_style = """
QPushButton {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #33cc33, /* Verde */
stop: 0.33 #33cc33, /* Verde hasta 33% */
stop: 0.33 #ff3333, /* Rojo desde 33% */
stop: 0.66 #ff3333, /* Rojo hasta 66% */
stop: 0.66 #ffcc00, /* Amarillo desde 66% */
stop: 1 #ffcc00 /* Amarillo */
);
color: black;
padding: 10px;
border-radius: 5px;
font-weight: bold;
border: none;
}
QPushButton:hover {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #39d339,
stop: 0.33 #39d339,
stop: 0.33 #ff4d4d,
stop: 0.66 #ff4d4d,
stop: 0.66 #ffd633,
stop: 1 #ffd633
);
color: black;
}
QPushButton:pressed {
background: qlineargradient(
x1: 0, y1: 0, x2: 1, y2: 0,
stop: 0 #2b992b,
stop: 0.33 #2b992b,
stop: 0.33 #cc2929,
stop: 0.66 #cc2929,
stop: 0.66 #ccaa00,
stop: 1 #ccaa00
);
padding-top: 11px;
padding-bottom: 9px;
color: black;
}
"""
main_layout = QVBoxLayout()
main_layout.setContentsMargins(20, 20, 20, 20) # margen alrededor del layout principal
# Título centrado
title = QLabel("EYETRACKING")
title.setFont(QFont("Arial", 44, QFont.Weight.Bold))
title.setAlignment(Qt.AlignmentFlag.AlignCenter)
main_layout.addWidget(title)
# Grid layout para botones (2 columnas)
grid = QGridLayout()
grid.setHorizontalSpacing(30) # espacio horizontal entre botones
grid.setVerticalSpacing(20) # espacio vertical entre botones
# Crear botones
btn_linea = QPushButton("Línea de calibración")
btn_linea.setFont(QFont("Arial", 14))
btn_linea.setStyleSheet(green_button_style)
btn_linea.clicked.connect(self.show_calibration_line)
btn_calibrate = QPushButton("Calibrar")
btn_calibrate.setFont(QFont("Arial", 14))
btn_calibrate.setStyleSheet(dark_green_button_style)
btn_calibrate.clicked.connect(self.calibrate)
btn_battery = QPushButton("Test batería pruebas")
btn_battery.setFont(QFont("Arial", 14))
btn_battery.setStyleSheet(blue_button_style)
btn_battery.clicked.connect(self.start_test0)
btn_pronogo = QPushButton("Test Go/NoGo")
btn_pronogo.setFont(QFont("Arial", 14))
btn_pronogo.setStyleSheet(green_yellow_button_style)
btn_pronogo.clicked.connect(self.start_test)
btn_proas = QPushButton("Test Ps/As")
btn_proas.setFont(QFont("Arial", 14))
btn_proas.setStyleSheet(half_green_red_style)
btn_proas.clicked.connect(self.start_test1)
btn_cs_task = QPushButton("Test CS_Task")
btn_cs_task.setFont(QFont("Arial", 14))
btn_cs_task.setStyleSheet(green_red_yellow_button_style)
btn_cs_task.clicked.connect(self.cs_task_test)
btn_results = QPushButton("Consultar resultados")
btn_results.setFont(QFont("Arial", 14))
btn_results.setStyleSheet( dark_yellow_button_style)
btn_results.clicked.connect(self.view_results)
botones = [btn_linea, btn_calibrate, btn_battery, btn_pronogo, btn_proas, btn_cs_task, btn_results]
for i, boton in enumerate(botones):
row = i // 2 # 2 columnas
col = i % 2
grid.addWidget(boton, row, col)
main_layout.addLayout(grid)
self.setLayout(main_layout)
def calibrate(self):
result = calibrate_tobii(self)
if result:
self.calibrated = True
QMessageBox.information(self, "Calibración", "Calibración completada con éxito.")
else:
QMessageBox.critical(self, "Error", "La calibración ha fallado.")
def show_calibration_line(self):
try:
disp = Display()
kb = Keyboard()
SCREEN_WIDTH, SCREEN_HEIGHT = disp.dispsize
center_y = SCREEN_HEIGHT // 2
screen = libscreen.Screen()
screen.clear(colour=(0, 0, 0)) # Negro
screen.draw_line(
colour=(255, 255, 255), # Blanco
spos=(0, center_y),
epos=(SCREEN_WIDTH, center_y),
pw=2
)
disp.fill(screen)
disp.show()
while True:
key, _ = kb.get_key()
if key == 'space':
break
time.sleep(0.01)
disp.close()
except Exception as e:
QMessageBox.critical(self, "Error", f"No se pudo mostrar la línea de calibración: {e}")
def start_test(self):
run_pro_nogo_test(self)
def start_test0(self):
run_bateria_test(self)
def start_test1(self):
run_pr_as_test(self)
def cs_task_test(self):
compelled_saccade_test(self)
def view_results(self):
file_path = "gonogo_results.xlsx"
if os.path.exists(file_path):
try:
subprocess.Popen([file_path], shell=True)
except Exception as e:
QMessageBox.critical(self, "Error", f"No se pudo abrir el archivo Excel. {e}")
else:
QMessageBox.warning(self, "Archivo no encontrado", "El archivo de resultados no existe.")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = EyeTrackingApp()
window.show()
sys.exit(app.exec())
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment