Skip to content

Commit 4102a83

Browse files
authored
Output stim positions, capture screenshots, more cleanup (#303)
* stimuli position export + capture matrix grid * update offline analysis and gaze visualization * remove unneeded class variable * dsi-flex and better starting parameters * update the calibration tests to mock open and prevent testing issues on Windows * revert parameter change * update to workflow to fix Windows build * revert actions change, lock a pyqt6 dep * remove unneeded assertions and save the current task bar index before a screen capture * add more info to output, update CHANGELOG
1 parent b03dcd2 commit 4102a83

25 files changed

+909
-270
lines changed

.coveragerc

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ omit =
66
*tests*
77
*test*
88
*demo*
9+
*exceptions.py
910
*/gui/*
1011

1112
[report]

CHANGELOG.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ Our last release candidate before the official 2.0 release!
1010
- Model
1111
- Offline analysis to support multimodal fusion. Initial release of GazeModel, GazeReshaper, and Gaze Visualization #294
1212
- Stimuli
13-
- Updates to ensure stimuli are presented at the same frequency #287
13+
- Updates to ensure stimuli are presented at the same frequency #287 Output stimuli position, screen capture and monitor information after Matrix tasks #303
1414
- Dynamic Selection Window
1515
- Updated trial_length to trial_window to allow for greater control of window used after stimulus presentations #291
1616
- Parameters

bcipy/acquisition/tests/test_devices.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ def test_default_supported_devices(self):
2727
dsi = supported['DSI-24']
2828
self.assertEqual('EEG', dsi.content_type)
2929

30-
self.assertEqual(len(devices.with_content_type('EEG')), 3)
30+
self.assertEqual(len(devices.with_content_type('EEG')), 4)
3131

3232
def test_load_from_config(self):
3333
"""Should be able to load a list of supported devices from a

bcipy/config.py

+4
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,17 @@
3737
DEFAULT_FIXATION_PATH = f'{STATIC_IMAGES_PATH}/main/PLUS.png'
3838
DEFAULT_TEXT_FIXATION = '+'
3939

40+
MATRIX_IMAGE_FILENAME = 'matrix.png'
41+
DEFAULT_GAZE_IMAGE_PATH = f'{STATIC_IMAGES_PATH}/main/{MATRIX_IMAGE_FILENAME}'
42+
4043
# core data configuration
4144
RAW_DATA_FILENAME = 'raw_data'
4245
EYE_TRACKER_FILENAME_PREFIX = 'eyetracker_data'
4346
TRIGGER_FILENAME = 'triggers.txt'
4447
SESSION_DATA_FILENAME = 'session.json'
4548
SESSION_SUMMARY_FILENAME = 'session.xlsx'
4649
LOG_FILENAME = 'bcipy_system_log.txt'
50+
STIMULI_POSITIONS_FILENAME = 'stimuli_positions.json'
4751

4852
# misc configuration
4953
WAIT_SCREEN_MESSAGE = 'Press Space to start or Esc to exit'

bcipy/display/paradigm/matrix/display.py

+22-5
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from psychopy import core, visual
66

77
import bcipy.display.components.layout as layout
8+
from bcipy.config import MATRIX_IMAGE_FILENAME
89
from bcipy.display import (BCIPY_LOGO_PATH, Display, InformationProperties,
910
StimuliProperties)
1011
from bcipy.display.components.task_bar import TaskBar
@@ -127,6 +128,23 @@ def _stim_positions(self) -> Dict[str, Tuple[float, float]]:
127128
for sym, stim in self.stim_registry.items()
128129
}
129130

131+
def capture_grid_screenshot(self, file_path: str) -> None:
132+
"""Capture Grid Screenshot.
133+
134+
Capture a screenshot of the current display and save it to the specified filename.
135+
"""
136+
# draw the grid and flip the window
137+
self.draw_grid(opacity=self.full_grid_opacity)
138+
tmp_task_bar = self.task_bar.current_index
139+
self.task_bar.current_index = 0
140+
self.draw_static()
141+
self.window.flip()
142+
143+
# capture the screenshot and save it to the specified file path
144+
capture = self.window.getMovieFrame()
145+
capture.save(f'{file_path}/{MATRIX_IMAGE_FILENAME}')
146+
self.task_bar.current_index = tmp_task_bar
147+
130148
def schedule_to(self, stimuli: list, timing: list, colors: list) -> None:
131149
"""Schedule stimuli elements (works as a buffer).
132150
@@ -158,13 +176,13 @@ def add_timing(self, stimuli: str):
158176
159177
Useful as a callback function to register a marker at the time it is
160178
first displayed."""
161-
self._timing.append([stimuli, self.experiment_clock.getTime()])
179+
self._timing.append((stimuli, self.experiment_clock.getTime()))
162180

163181
def reset_timing(self):
164182
"""Reset the trigger timing."""
165183
self._timing = []
166184

167-
def do_inquiry(self) -> List[float]:
185+
def do_inquiry(self) -> List[Tuple[str, float]]:
168186
"""Animates an inquiry of stimuli and returns a list of stimuli trigger timing."""
169187
self.reset_timing()
170188
symbol_durations = self.symbol_durations()
@@ -187,11 +205,12 @@ def build_grid(self) -> Dict[str, visual.TextStim]:
187205
grid = {}
188206
for sym in self.symbol_set:
189207
pos_index = self.sort_order(sym)
208+
pos = self.positions[pos_index]
190209
grid[sym] = visual.TextStim(win=self.window,
191210
text=sym,
192211
color=self.grid_color,
193212
opacity=self.start_opacity,
194-
pos=self.positions[pos_index],
213+
pos=pos,
195214
height=self.grid_stimuli_height)
196215
return grid
197216

@@ -337,8 +356,6 @@ def update_task_bar(self, text: str = ''):
337356
PARAMETERS:
338357
339358
text: text for task
340-
color_list: list of the colors for each stimuli
341-
pos: position of task
342359
"""
343360
if self.task_bar:
344361
self.task_bar.update(text)

bcipy/feedback/demo/demo_level_feedback.py

-38
This file was deleted.

bcipy/feedback/visual/level_feedback.py

-130
This file was deleted.

bcipy/helpers/save.py

+44-7
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@
66
from pathlib import Path
77
from shutil import copyfile
88
from time import localtime, strftime
9-
from typing import Any, List, Union
9+
from typing import Any, Dict, List, Tuple, Union
1010

1111
from bcipy.acquisition.devices import DeviceSpec
12-
from bcipy.config import (DEFAULT_ENCODING, DEFAULT_EXPERIMENT_ID,
12+
from bcipy.config import (DEFAULT_ENCODING,
13+
DEFAULT_EXPERIMENT_ID,
1314
DEFAULT_LM_PARAMETERS_FILENAME,
1415
DEFAULT_LM_PARAMETERS_PATH,
15-
DEFAULT_PARAMETER_FILENAME, SIGNAL_MODEL_FILE_SUFFIX)
16+
DEFAULT_PARAMETER_FILENAME,
17+
SIGNAL_MODEL_FILE_SUFFIX,
18+
STIMULI_POSITIONS_FILENAME)
1619
from bcipy.helpers.validate import validate_experiments
1720
from bcipy.signal.model.base_model import SignalModel
1821

@@ -33,16 +36,26 @@ def save_json_data(data: Any, location: str, name: str) -> str:
3336
return str(path)
3437

3538

36-
def save_experiment_data(experiments, fields, location, name) -> str:
39+
def save_experiment_data(
40+
experiments: dict,
41+
fields: dict,
42+
location: str,
43+
name: str) -> str:
3744
validate_experiments(experiments, fields)
3845
return save_json_data(experiments, location, name)
3946

4047

41-
def save_field_data(fields, location, name) -> str:
48+
def save_field_data(
49+
fields: dict,
50+
location: str,
51+
name: str) -> str:
4252
return save_json_data(fields, location, name)
4353

4454

45-
def save_experiment_field_data(data, location, name) -> str:
55+
def save_experiment_field_data(
56+
data: dict,
57+
location: str,
58+
name: str) -> str:
4659
return save_json_data(data, location, name)
4760

4861

@@ -154,7 +167,7 @@ def _save_session_related_data(save_path: str, session_dictionary: dict) -> Any:
154167
return file
155168

156169

157-
def save_model(model: SignalModel, path: Union[Path, str]):
170+
def save_model(model: SignalModel, path: Union[Path, str]) -> None:
158171
"""Save model weights (e.g. after training) to `path`
159172
160173
Parameters
@@ -169,3 +182,27 @@ def save_model(model: SignalModel, path: Union[Path, str]):
169182
# It supports very large objects and some data format optimizations
170183
# making it appropriate for signal models.
171184
pickle.dump(model, file, protocol=4)
185+
186+
187+
def save_stimuli_position_info(
188+
stimuli_position_info: Dict[str, Tuple[float, float]],
189+
path: Union[Path, str],
190+
screen_info: Dict[str, Any]) -> str:
191+
"""Save stimuli positions and screen info to `path`
192+
193+
stimuli_position_info: {'A': (0, 0)}
194+
screen_info: {'screen_size_pixels': [1920, 1080], 'screen_refresh': 160}
195+
196+
Parameters
197+
----------
198+
stimuli_position_info - stimuli position info to save to json
199+
path - path to the file which will be created.
200+
screen_info - screen info to save to json
201+
"""
202+
# assert that screen_info is a dict with at least the key 'screen_resolution'
203+
assert 'screen_size_pixels' in screen_info.keys(), \
204+
'screen_size_pixels must be a key in screen_info'
205+
206+
# combine the dicts
207+
all_data = {**stimuli_position_info, **screen_info}
208+
return save_json_data(all_data, path, STIMULI_POSITIONS_FILENAME)

0 commit comments

Comments
 (0)