Doc_code
 
Cargando...
Buscando...
Nada coincide
main.py
Ir a la documentación de este archivo.
1import sys # Para acceder a argumentos del sistema o finalizar la app si hace falta
2import random # Para aleatorizar condiciones o estímulos
3import time # Para pausas o cálculos de tiempo
4from matplotlib.pyplot import margins # (Parece innecesaria, no se usa en el script)
5import pygame # Para gestionar eventos de teclado y control del flujo durante el test
6from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QLabel, QMessageBox # Elementos de la interfaz gráfica
7from PyQt6.QtCore import Qt # Para definir tipos de alineación y eventos en Qt
8from PyQt6.QtGui import QFont # Para modificar fuentes en elementos de la interfaz
9
10# Importaciones de PyGaze
11from pygaze import eyetracker, libinput, libscreen, liblog
12from pygaze import libtime
13
14import tobii_research as tr # Librería oficial para interactuar con dispositivos Tobii
15
16import constants # Módulo personalizado que debe contener parámetros como DISPSIZE = (1920, 1080)
17import pandas as pd # Para trabajar con datos y guardar los resultados en Excel
18import subprocess # Para abrir archivos con el programa predeterminado del sistema operativo
19import os # Para comprobar si existen archivos o rutas
20
21
22# ============ FUNCIÓN DE CALIBRACIÓN ============
24 try:
25 print("Buscando dispositivo...")
26 found_eyes = tr.find_all_eyetrackers() # Busca todos los eye trackers conectados
27
28 if not found_eyes:
29 raise Exception("No se encontró ningún dispositivo Tobii Pro.")
30
31 eye_tracker = found_eyes[0] # Selecciona el primer dispositivo encontrado
32 print(f"Dispositivo encontrado: {eye_tracker}")
33
34 self.disp = libscreen.Display(bgcolor=(0, 0, 0)) # Crea una pantalla negra
35 self.tracker = eyetracker.EyeTracker(self.disp) # Crea el objeto de seguimiento ocular
36
37 instruction_screen = libscreen.Screen() # Crea una pantalla para mostrar instrucciones
38 instruction_screen.clear(colour=(0, 0, 0)) # Fondo negro
39 instruction_screen.draw_text("Presiona espacio para calibrar", fontsize=24, colour=(255, 255, 255)) # Texto en blanco
40 self.disp.fill(instruction_screen) # Muestra las instrucciones
41 self.disp.show()
42
43 print("Iniciando calibración...")
44 self.tracker.calibrate() # Ejecuta el proceso de calibración
45 print("Calibración completada.")
46
47 return True # Devuelve True si fue exitoso
48 except Exception as e:
49 print(f"Error durante calibración: {e}")
50 return False # Devuelve False si hubo error
51
52# ============ TEST GO/NOGO ======================
53
55 pygame.init() # Inicializa Pygame
56
57 log = liblog.Logfile() # Crea un archivo de log
58 log.write(["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"]) # Define las columnas del log
59
60 SCREEN_WIDTH, SCREEN_HEIGHT = constants.DISPSIZE # Tamaño de la pantalla
61 CENTER = (SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2) # Coordenadas del centro
62 TARGET_OFFSET = 382 # Distancia desde el centro hacia los lados para colocar estímulos
63
64 left_pos = (CENTER[0] - TARGET_OFFSET, CENTER[1]) # Posición del estímulo izquierdo
65 right_pos = (CENTER[0] + TARGET_OFFSET, CENTER[1]) # Posición del estímulo derecho
66
67 blackscreen = libscreen.Screen() # Pantalla negra para limpiar entre estímulos
68 blackscreen.clear(colour=(0, 0, 0))
69
70 # Pantalla de instrucciones
71 inscreen = libscreen.Screen()
72 inscreen.clear(colour=(0, 0, 0)) # Fondo negro
73 ins_text = (
74 "Mira la cruz central y presiona 'espacio'.\n\n"
75 "El color indicará la acción:\n"
76 "- Verde: mira al círculo blanco y negro (Pro-sácadico)\n"
77 "- Rojo: mira al estímulo blanco (Anti-sácadico)\n"
78 "- Naranja: NO mires a ningún estímulo (NoGo)\n\n"
79 "Presiona 'espacio' para comenzar.\n"
80 "Pulsa 'E' en cualquier momento para salir."
81 )
82 inscreen.draw_text(text=ins_text, fontsize=30, pos=CENTER, colour=(255, 255, 255), center=True)
83
84 # Pantalla con cruz de fijación
85 fixscreen = libscreen.Screen()
86 fixscreen.clear(colour=(0, 0, 0))
87 fixscreen.draw_fixation(fixtype='cross', diameter=57, pos=CENTER, colour=(255, 255, 255)) # Cruz blanca
88
89 # Pantallas para cada condición de color (verde, rojo, naranja)
90 signal_screens = {
91 'PS': libscreen.Screen(),
92 'AS': libscreen.Screen(),
93 'NoGo': libscreen.Screen()
94 }
95 for key, color in [('PS', (0, 255, 0)), ('AS', (255, 0, 0)), ('NoGo', (255, 165, 0))]:
96 signal_screens[key].clear(colour=(0, 0, 0))
97 signal_screens[key].draw_circle(pos=CENTER, r=38, fill=True, colour=color) # Círculo central de señal
98
99 # Pantallas para feedback
100 feedbackscreens = {
101 1: libscreen.Screen(),
102 0: libscreen.Screen()
103 }
104 feedbackscreens[1].clear(colour=(0, 0, 0))
105 feedbackscreens[1].draw_text(text='Correcto', fontsize=30, pos=CENTER, colour=(0, 255, 0), center=True)
106 feedbackscreens[0].clear(colour=(0, 0, 0))
107 feedbackscreens[0].draw_text(text='Incorrecto', fontsize=30, pos=CENTER, colour=(255, 0, 0), center=True)
108
109 self.disp.fill(inscreen) # Muestra pantalla de instrucciones
110 self.disp.show()
111
112 # Espera a que el usuario presione espacio o E para salir
113 waiting = True
114 while waiting:
115 for event in pygame.event.get():
116 if event.type == pygame.KEYDOWN:
117 if event.key == pygame.K_SPACE:
118 waiting = False
119 elif event.key == pygame.K_e:
120 pygame.quit()
121 return
122
123 max_blocks = 30 # Número de bloques de ensayo
124 results = [] # Lista para guardar resultados
125
126 try:
127 for block in range(max_blocks):
128 trials = ['PS', 'AS', 'NoGo'] # Tipos de ensayo
129 random.shuffle(trials) # Aleatoriza el orden
130
131 for trialnr, trialtype in enumerate(trials, start=1): # Recorre los tres ensayos del bloque
132 for event in pygame.event.get():
133 if event.type == pygame.KEYDOWN and event.key == pygame.K_e:
134 raise KeyboardInterrupt # Permite salir del test
135
136 self.disp.fill(fixscreen)
137 self.disp.show()
138 libtime.pause(random.randint(750, 1250)) # Tiempo aleatorio de fijación
139
140 waiting = True
141 while waiting:
142 for event in pygame.event.get():
143 if event.type == pygame.KEYDOWN:
144 if event.key == pygame.K_SPACE:
145 waiting = False
146 elif event.key == pygame.K_e:
147 raise KeyboardInterrupt
148
149 time.sleep(0.2) # Pequeña pausa antes de comenzar a grabar
150 self.tracker.start_recording() # Comienza a registrar movimientos oculares
151
152 self.disp.fill(signal_screens[trialtype]) # Muestra señal central
153 self.disp.show()
154 time.sleep(0.8) # Se muestra durante 0.8 segundos
155
156 self.disp.fill(blackscreen) # Limpia pantalla
157 self.disp.show()
158 time.sleep(0.2)
159
160 targetside = random.choice(['left', 'right']) # Aleatoriza la posición del objetivo
161 target_pos = left_pos if targetside == 'left' else right_pos
162 nontarget_pos = right_pos if targetside == 'left' else left_pos
163
164 dualstim_screen = libscreen.Screen()
165 dualstim_screen.clear(colour=(0, 0, 0))
166 dualstim_screen.draw_circle(pos=nontarget_pos, r=28, fill=True, colour=(255, 255, 255)) # Non-target
167 dualstim_screen.draw_circle(pos=target_pos, r=38, fill=True, colour=(255, 255, 255)) # Círculo externo
168 dualstim_screen.draw_circle(pos=target_pos, r=32, fill=True, colour=(0, 0, 0)) # Círculo medio
169 dualstim_screen.draw_circle(pos=target_pos, r=28, fill=True, colour=(255, 255, 255)) # Círculo interno
170
171 self.disp.fill(dualstim_screen)
172 self.disp.show()
173
174 t0 = pygame.time.get_ticks() # Tiempo inicial
175 self.tracker.log(f"target {targetside}") # Escribe en el log
176
177 t1, startpos = self.tracker.wait_for_saccade_start() # Espera el inicio de la sacada
178 endtime, startpos, endpos = self.tracker.wait_for_saccade_end() # Obtiene datos del final de la sacada
179
180 latency = (t1 - t0) / 1000 # Latencia en segundos
181 self.tracker.stop_recording() # Detiene el registro de datos
182
183 margin = 100 # Margen de tolerancia (no usado aún)
184
185 # Creamos una condición para indicar cuándo el movimiento prosacádico, antisacádico o NoGo es correcto o incorrecto
186 if trialtype == 'PS': # Si el círculo central es verde → condición Pro-saccádica
187 correct = int(
188 # Es correcto si el participante mira hacia el lado donde aparece el target:
189 # si el target está a la izquierda y la mirada termina en la mitad izquierda de la pantalla,
190 # o si el target está a la derecha y la mirada termina en la mitad derecha
191 (targetside == 'left' and endpos[0] <= constants.DISPSIZE[0]/2) or
192 (targetside == 'right' and endpos[0] >= constants.DISPSIZE[0]/2)
193 )
194 print(correct) # Imprime 1 si es correcto, 0 si es incorrecto
195
196 elif trialtype == 'AS': # Si el círculo central es rojo → condición Anti-saccádica
197 correct = int(
198 # Es correcto si el participante mira hacia el lado opuesto al target (al non-target):
199 # si el target está a la izquierda y la mirada termina en la mitad derecha,
200 # o si el target está a la derecha y la mirada termina en la mitad izquierda
201 (targetside == 'left' and endpos[0] >= constants.DISPSIZE[0]/2) or
202 (targetside == 'right' and endpos[0] <= constants.DISPSIZE[0]/2)
203 )
204
205 elif trialtype == 'NoGo': # Si el círculo central es naranja → condición NoGo
206 # En NoGo no debe haber movimiento ocular (sacada)
207 try:
208 # Espera hasta 900 ms para detectar el inicio de una sacada
209 t1, _ = self.tracker.wait_for_saccade_start(timeout=900)
210 correct = 0 # Si hay una sacada, se considera incorrecto
211 latency = (t1 - t0) / 1000 # Calcula la latencia en segundos
212 print("Incorrecto")
213 except Exception:
214 # Si no se detecta ninguna sacada en ese tiempo, se considera correcto
215 correct = 1
216 latency = 0 # No hay latencia porque no hubo movimiento
217 print("Correcto")
218
219 # correct toma valor 1 si es correcto, 0 si es incorrecto
220
221 if correct >= 0:
222 dualstim_screen.clear(colour=(0, 0, 0)) # Limpia la pantalla
223 self.disp.show() # Actualiza la pantalla para reflejar los cambios
224
225 self.disp.fill(feedbackscreens[correct]) # Muestra la pantalla de feedback correspondiente (correcta o incorrecta)
226 self.disp.show() # Refresca la pantalla para mostrar el feedback
227 time.sleep(0.5) # Muestra el feedback durante 0.5 segundos
228
229 # Almacena los resultados del ensayo en una lista para análisis posterior
230 results.append([trialnr, trialtype, targetside, endpos, round(latency, 3), correct])
231
232 except KeyboardInterrupt:
233 print("Test interrumpido por el usuario con la tecla 'E'.") # Si el usuario presiona 'E', se detiene el test
234
235 finally:
236 log.close() # Cierra el archivo de log
237 self.tracker.close() # Cierra la conexión con el eyetracker
238 self.disp.close() # Cierra la pantalla de presentación de estímulos
239 print("Test Go/NoGo finalizado.")
240
241 # Crea un DataFrame con los resultados recogidos y lo guarda en un archivo Excel
242 df = pd.DataFrame(results, columns=["trialnr", "trialtype", "targetside", "gaze_endpos", "latency", "correct"])
243 df.to_excel("gonogo_results.xlsx", index=False) # Guarda el archivo sin incluir los índices
244 print("Resultados guardados en 'gonogo_results.xlsx'.") # Informa que los datos han sido guardados correctamente
245
246
247# ============ INTERFAZ PYQT ===================
248
249class EyeTrackingApp(QWidget): # Define una clase que hereda de QWidget para crear una ventana principal
250 def __init__(self): # Método constructor
251 super().__init__() # Llama al constructor de la clase padre (QWidget)
252 self.setWindowTitle("Test Go/NoGo con Tobii") # Establece el título de la ventana
253 self.setGeometry(100, 100, 400, 300) # Define posición (x=100, y=100) y tamaño (400x300) de la ventana
254 self.calibrated = False # Variable para controlar si el eye tracker ha sido calibrado
255 self.setup_ui() # Llama a la función que crea los componentes de la interfaz
256 self.disp = None # Inicializa el objeto de pantalla (se asignará más adelante)
257 self.tracker = None # Inicializa el objeto del eye tracker (se asignará más adelante)
258
259 def setup_ui(self): # Método que crea y organiza los elementos visuales
260 self.setStyleSheet("background-color: #e6f0ff;") # Cambia el color de fondo de la ventana
261
262 layout = QVBoxLayout() # Crea un layout vertical para organizar los elementos de arriba hacia abajo
263
264 title = QLabel("Test Go/NoGo con Tobii") # Crea una etiqueta de título
265 title.setFont(QFont("Arial", 18, QFont.Weight.Bold)) # Establece la fuente, tamaño y negrita
266 title.setAlignment(Qt.AlignmentFlag.AlignCenter) # Centra el texto horizontalmente
267 layout.addWidget(title) # Añade la etiqueta al layout
268
269 # Botón para calibrar el eye tracker
270 self.btn_calibrate = QPushButton("Calibrar")
271 self.btn_calibrate.setFont(QFont("Arial", 14)) # Fuente y tamaño
272 self.btn_calibrate.setStyleSheet("background-color: #3399ff; color: white; padding: 10px; border-radius: 5px;")
273 self.btn_calibrate.clicked.connect(self.calibrate) # Conecta el clic del botón a la función de calibración
274 layout.addWidget(self.btn_calibrate) # Añade el botón al layout
275
276 # Botón para iniciar el test Go/NoGo
277 self.btn_test = QPushButton("Iniciar test Go/NoGo")
278 self.btn_test.setFont(QFont("Arial", 14))
279 self.btn_test.setStyleSheet("background-color: #3399ff; color: white; padding: 10px; border-radius: 5px;")
280 self.btn_test.clicked.connect(self.start_test) # Conecta a la función que lanza el test
281 layout.addWidget(self.btn_test)
282
283 # Botón para consultar los resultados del test
284 self.btn_results = QPushButton("Consultar resultados")
285 self.btn_results.setFont(QFont("Arial", 14))
286 self.btn_results.setStyleSheet("background-color: #3399ff; color: white; padding: 10px; border-radius: 5px;")
287 self.btn_results.clicked.connect(self.view_results) # Conecta al método para visualizar resultados
288 layout.addWidget(self.btn_results)
289
290 self.setLayout(layout) # Establece el layout creado como layout principal de la ventana
291
292 def calibrate(self):
293 result = calibrate_tobii(self)
294 if result:
295 self.calibrated = True
296 QMessageBox.information(self, "Calibración", "Calibración completada con éxito.")
297 else:
298 QMessageBox.critical(self, "Error", "La calibración ha fallado.")
299
300 def start_test(self): # Método que inicia el test
301 # Código comentado para forzar calibración antes del test:
302 # if not self.calibrated:
303 # QMessageBox.warning(self, "Calibración requerida", "Debes calibrar antes de realizar el test.")
304 # return
305 run_gonogo_test(self) # Llama a la función que ejecuta el test Go/NoGo
306
307
308 def view_results(self):
309 file_path = "gonogo_results.xlsx"
310
311 # Verificamos si el archivo existe antes de intentar abrirlo
312 if os.path.exists(file_path):
313 try:
314 # Abrimos el archivo con el programa predeterminado (Windows)
315 subprocess.Popen([file_path], shell=True)
316 except Exception as e:
317 QMessageBox.critical(self, "Error", f"No se pudo abrir el archivo Excel. {e}")
318 else:
319 QMessageBox.warning(self, "Archivo no encontrado", "El archivo de resultados no existe.")
320
321
322if __name__ == "__main__": # Si este archivo se ejecuta directamente
323 app = QApplication(sys.argv) # Crea la aplicación Qt
324 window = EyeTrackingApp() # Crea una instancia de la ventana principal
325 window.show() # Muestra la ventana
326 sys.exit(app.exec()) # Inicia el bucle de eventos y cierra correctamente al salir
run_gonogo_test(self)
Definition main.py:54
calibrate_tobii(self)
Definition main.py:23