search icon search icon ВЕРСИЯ ДЛЯ СЛАБОВИДЯЩИХ

Инженерный тур. 3 этап

Общая информация

Участникам третьего этапа предлагается разработать программно-аппаратный комплекс для интерактивного управления внешними объектами через моторные действия оператора. Такая технология может быть применена при работе в средах, где физических возможностей человека недостаточно и необходимо их увеличить — например, экзоскелетом или устройством с похожими функциями. Кроме того, она может использоваться для реабилитации и абилитации людей парезами и параличами различной этиологии.

Легенда задачи

Человек исследует глубины океана. Для работы в этой среде собственных физических возможностей человека недостаточно. Необходима расширить его возможности через «дополнение» его физики внешним устройством, которое сможет осуществлять манипуляции в среде высокой плотности и экстремальных температур, где действовать самостоятельно человек не в состоянии. Участникам предлагается создать такое устройство, которое по активности мозга и мышц человека сможет осуществлять сложные манипуляции внешними устройствами.

Требования к команде и компетенциям участников

Количество участников в команде: 3–4.

Компетенции, которыми должны обладать члены команды: биология, психология, программирование, машинное обучение, сбор данных с микроконтроллеров, конструирование устройств для регистрации биосигналов.

Роли, которые должны быть представлены в команде:

  1. Биолог.
  2. Программист.
  3. Инженер.
Оборудование и программное обеспечение
Наименование Описание
Электроэнцефалограф, электромиограф Сбор биосигналов
Персональный компьютер Обработка биосигналов, программирование визуальной среды
Визуальная среда на ПК Интерфейс оператора для управления разрабатываемым программно-аппаратным комплексом
BiTronics Studio Базовая регистрация биосигналов, отладка оборудования для регистрации, синхронизированной с визуальной средой
Python 3.X с набором расширений PsychoPy, SciPy, Scikit-Learn и др. Конструирование визуальной среды оператора, построение и отладка классификатора, сбор биосигналов синхронно с работой визуальной среды оператора
Описание задачи

Для решения задачи среди разных мышц или областей головного мозга следует выбрать определенные точки регистрации биосигнала. На основании этого разрабатываются процедуры:

  • обработки биосигнала;
  • извлечения управляющих команд.

Для мышц и мозга паттерны сигнала, содержащие управляющие команды, будут существенно разниться, что потребует различного программно-алгоритмического инструментария для извлечения этих команд из потока биосигналов.

В соответствии с выбранными управляющими командами необходимо определиться со способом обучения оператора работе с разрабатываемым программно-аппаратным комплексом. Для разного типа управляющих команд оператор должен будет совершать различные действия.

Основные этапы работы:

  1. Собрать установку для получения сигнала. Результат: скриншот с ПО, подключенным к установке.
  2. Получить базовый вид сигнала. Результат: скриншот с записью типовых видов ЭЭГ и ЭМГ с функциональными пробами.
  3. Извлечь базовые параметры сигнала. Результат: программный код, выполняющий базовую обработку регистрируемого сигнала и извлекающий из сигнала необходимые метрики.
  4. Построить классификатор на полученных данных. Результат: программный код, строящий классификатор на собранных данных, и сам классификатор.
  5. Объединить в единый комплекс визуальную среду для оператора, модуль сбора биосигналов и классификатор для определения действий/намерений оператора. Результат: программный код, реализующий совместную работу визуального модуля, модуля для сбора данных, а также применение к данным классификатора. Демонстрация работы полученного комплекса.
Этап 1 / Подзадача 1

Соберите комплект Arduino Mega с 8 каналами ЭЭГ и фотодиодом. Подключите полученный комплект к ПК. Загрузите прошивку (https://disk.yandex.ru/d/Pxcw2JtBCauPGA/EEG-8.ino) на Arduino (дополнительная нужная библиотека https://disk.yandex.ru/d/Pxcw2JtBCauPGA/TimerOne.zip) и проверьте соединение ПО Bitronics Studio EEG Edition (инструкция https://disk.yandex.ru/i/-BZRSNpW8FqOEQ) с платой Arduino. При корректном соединении можно видеть сигналы на аналоговых входах ЭЭГ и фотодиода, а также «сигнал» счетчика — пилу.

В качестве ответа представьте скриншот экрана с Bitronics Studio, где видны все 10 каналов данных (восемь каналов ЭЭГ, один канал фотодиода и один канал счетчика). На скриншоте укажите расположение каналов ЭЭГ, канала фотодиода и канала счетчика.

Решение задачи

Рис. 5.1.

Пояснение решения задачи

Необходимо собрать установку по инструкции, подключить ее к ПК и в программе BiTronics Studio отметить соответствующие каналы.

Критерии оценивания

Формат ответа: скриншот в формате .png или .jpg.

Скриншот с 10 каналами — 1 балл.

Верно отмеченные 8 каналов ЭЭГ — 1 балл.

Верно отмеченный канал фотодиода — 1 балл.

Верно отмеченный канал счетчика — 1 балл.

Количество баллов: 4.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 25.02.2025 г.

Этап 2 / Подзадача 2

Напишите скрипт с использованием PsychoPy, который будет:

  • Выводить на экран четыре стрелки, направленные вверх, вниз, вправо и влево. Каждая из стрелок расположена в соответствующей части экрана: стрелка «Влево» — в левой части экрана, стрелка «Вверх» — в верхней части экрана, стрелка «Вправо» — в правой части экрана, стрелка «Вниз» — в нижней части экрана. Стрелка состоит из трех объектов типа Линия, которые соединяются в одной точке — на кончике стрелки (на острие стрелки). Средняя линия в два раза длиннее, чем боковые.
  • По нажатию любой клавиши начинается последовательное изменение цвета стрелок. Стрелки меняют цвет по одной в случайном порядке. Стрелка меняет свое состояние (цвет) на 0,5 с, затем возвращается к исходному цвету. Через 0,5 с, в течение которых все стрелки находятся в исходном состоянии, следующая случайная стрелка меняет свой цвет на 0,5 с.
  • По следующему нажатию любой клавиши мигания стрелок останавливаются, и через 1,75 с окно PsychoPy закрывается.

Рекомендуем использовать Python 3.10. Он установлен на ПК, в нем установлен PsychoPy в виде модуля.

В качестве ответа пришлите скрипт, выполняющий вышеописанные процедуры.

Критерии оценивания

Формат ответа: скрипт в формате .py.

Вывод стрелок на экран — 1 балл за каждую стрелку в соответствующей области экрана.

Изменение цвета стрелок в случайном порядке после нажатия любой клавиши — 1,5 балла.

Завершение мигания стрелок и закрывание окна PsychoPy по нажатию любой клавиши черзе 1,75 с после остановки мигания стрелок — 0,5 балла.

Количество баллов: 6.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 25.02.2025 г.

Решение задачи
Python
from psychopy import visual, core, event
from time import sleep
from random import randint

win = visual.Window(size=(600, 600))
colors = ["red", "green", "blue", "yellow"]

def draw(colour1, colour2, colour3, colour4):
    line = visual.Line(win, start=(-0.8, 0), end=(-0.3, 0), lineWidth=5, lineColor=colour1)
    line.draw()
    line = visual.Line(win, start=(-0.8, 0), end=(-0.55, 0.25), lineWidth=5, lineColor=colour1)
    line.draw()
    line = visual.Line(win, start=(-0.8, 0), end=(-0.55, -0.25), lineWidth=5, lineColor=colour1)
    line.draw()

    line = visual.Line(win, start=(0.8, 0), end=(0.3, 0), lineWidth=5, lineColor=colour2)
    line.draw()
    line = visual.Line(win, start=(0.8, 0), end=(0.55, 0.25), lineWidth=5, lineColor=colour2)
    line.draw()
    line = visual.Line(win, start=(0.8, 0), end=(0.55, -0.25), lineWidth=5, lineColor=colour2)
    line.draw()

    line = visual.Line(win, start=(0, 0.8), end=(0, 0.3), lineWidth=5, lineColor=colour3)
    line.draw()
    line = visual.Line(win, start=(0, 0.8), end=(0.25, 0.55), lineWidth=5, lineColor=colour3)
    line.draw()
    line = visual.Line(win, start=(0, 0.8), end=(-0.25, 0.55), lineWidth=5, lineColor=colour3)
    line.draw()

    line = visual.Line(win, start=(0, -0.8), end=(0, -0.3), lineWidth=5, lineColor=colour4)
    line.draw()
    line = visual.Line(win, start=(0, -0.8), end=(-0.25, -0.55), lineWidth=5, lineColor=colour4)
    line.draw()
    line = visual.Line(win, start=(0, -0.8), end=(0.25, -0.55), lineWidth=5, lineColor=colour4)
    line.draw()
    win.flip()
'''
def draw_right(colour):
    line = visual.Line(win, start=(0.8, 0), end=(0.3, 0), lineWidth=5, lineColor=colour)
    line.draw()
    line = visual.Line(win, start=(0.8, 0), end=(0.55, 0.25), lineWidth=5, lineColor=colour)
    line.draw()
    line = visual.Line(win, start=(0.8, 0), end=(0.55, -0.25), lineWidth=5, lineColor=colour)
    line.draw()
    win.flip()

def draw_up(colour):
    line = visual.Line(win, start=(0, 0.8), end=(0, 0.3), lineWidth=5, lineColor=colour)
    line.draw()
    line = visual.Line(win, start=(0, 0.8), end=(0.25, 0.55), lineWidth=5, lineColor=colour)
    line.draw()
    line = visual.Line(win, start=(0, 0.8), end=(-0.25, 0.55), lineWidth=5, lineColor=colour)
    line.draw()
    win.flip()

def draw_down(colour):
    line = visual.Line(win, start=(0, -0.8), end=(0, -0.3), lineWidth=5, lineColor=colour)
    line.draw()
    line = visual.Line(win, start=(0, -0.8), end=(-0.25, -0.55), lineWidth=5, lineColor=colour)
    line.draw()
    line = visual.Line(win, start=(0, -0.8), end=(0.25, -0.55), lineWidth=5, lineColor=colour)
    line.draw()
    win.flip()
'''
draw('white', 'white', 'white', 'white')
keys = event.waitKeys()
# Ждем 2 секунды
core.wait(2)

while True:
    x = randint(0, 3)
    y = randint(0, 3)

    if (x == 0):
        draw(colors[y], 'white', 'white', 'white')
    elif x== 1:
        draw('white', colors[y], 'white', 'white')
    elif x==2:
        draw('white', 'white', colors[y], 'white')
    else:
        draw('white', 'white', 'white', colors[y])

    sleep(0.5)

# Закрываем окно
win.close()

Пояснение решения задачи

Пример скрипта с применение PsychoPy, реализующий требуемую процедуру.

Этап 3 / Подзадача 3

Сделайте запись ЭМГ. Для записи ЭМГ можно использовать Bitronics Studio EEG Edition, просто вместо датчика ЭЭГ подключить датчик ЭМГ. Запись ЭМГ будет осуществляться в соответствующие каналы, соответственно подключению ЭМГ-датчиков. Одну запись сделайте с расслабленных мышц предплечья, вторую — с напряженных мышц предплечья. Оба фрагмента можно записать непрерывно, не выключая установку.

Ответьте на вопрос: «Какие характерные различия в сигнале можно наблюдать для электромиограммы, снятой с расслабленных мышц, и для электромиограммы, снятой с напряженных мышц?» Интерпретируйте полученные различия, объясните наблюдаемые различия в амплитуде регистрируемого сигнала.

Аккуратно следуйте правилам электробезопасности (https://disk.yandex.ru/i/Ivnwg5kn1zwWPQ), плату Arduino подключайте к ПК через гальваноразвязку, питание Arduino осуществляйте при помощи аккумулятора (у каждой команды в наличии два заряженных аккумулятора).

Критерии оценивания

Формат ответа: скриншот в формате .png или .jpg, текстовые пояснения в текстовом файле.

Скриншот фрагмента записи ЭМГ с расслабленных мышц — 1 балл.

Скриншот фрагмента записи ЭМГ с напряженных мышц — 1 балл.

Верная интерпретация полученных различий в сигнале ЭМГ — 2 балла.

Количество баллов: 4.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 25.02.2025 г.

Решение задачи

Различия в сигнале для электромиограммы, снятой с расслабленных мышц, и для электромиограммы, снятой с напряженных, заключаются в амплитуде сигнала: при напряжении амплитуда больше, это связано с тем, что внутри мышцы при ее сокращении протекают крошечные токи, это и влияет на амплитуду (увеличивает) сигнала, см. рис. 5.2.

Пояснение решения задачи

Делается запись ЭМГ на расслабленной и напряженной мышце, объясняются различия в сигнале.

Рис. 5.2.

Этап 4 / Подзадача 4

Сделайте скрипт, который будет:

  • Выводить четыре стрелки, как в задаче № 2, и точно так же ими мигать, но время, на которое меняется состояние стрелки — 0,75 с.
  • Осуществлять сбор данных с Arduino Mega с 8 каналами ЭЭГ, одним каналом фотодиода и одним «каналом» счетчика.
  • После завершения работы скрипта (например, по нажатию клавиши Escape) сохраните полученные с Arduino данные в текстовый файл с расширением .csv.
  • После завершения работы скрипта выводится визуализация записанных данных.

Постарайтесь сделать скрипт таким образом, чтобы потом можно было легко менять расположение стрелок, время их модификации, время пребывания в исходном состоянии, а также параметры этих модификаций. Предусмотрите возможность выполнять несколько «кругов» модификаций по набору элементов на экране. Один круг — однократная подсветка всех одиночных элементов либо их наборов.

Рекомендуем выполнить задачу в среде Python 3.10.

Аккуратно следуйте правилам электробезопасности (https://disk.yandex.ru/i/Ivnwg5kn1zwWPQ), плату Arduino подключайте к ПК через гальваноразвязку, питание Arduino осуществляйте при помощи аккумулятора (у каждой команды в наличии два заряженных аккумулятора).

Критерии оценивания

Формат ответа: скрипт в формате .py, скриншоты визуализации в виде графического файла.

Вывод элементов и смена их состояния каждые 0,75 с — 1 балл.

Сбор и сохранение данных с Arduino одновременно с работой визуальной части — 2 балла.

Визуализация записанных данных — 2 балла.

Количество баллов: 5.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 26.02.2025 г.

Решение задачи
Python
from psychopy import visual, event, core
import time, socket, os
from random import randint, shuffle
import matplotlib.pyplot as plt
import pandas as pd
import serial
from serial import Serial
import numpy as np
hz = 20

arr = []
port = "COM4"
baudrate = 115200
ser = serial.Serial(port, baudrate, timeout=0.1)

win = visual.Window(size=(600, 600))
colors = ["red", "green", "blue", "yellow"]

def draw(colour1, colour2, colour3, colour4):
    line = visual.Line(win, start=(-0.8, 0), end=(-0.3, 0), lineWidth=5, lineColor=colour1)
    line.draw()
    line = visual.Line(win, start=(-0.8, 0), end=(-0.55, 0.25), lineWidth=5, lineColor=colour1)
    line.draw()
    line = visual.Line(win, start=(-0.8, 0), end=(-0.55, -0.25), lineWidth=5, lineColor=colour1)
    line.draw()

    line = visual.Line(win, start=(0.8, 0), end=(0.3, 0), lineWidth=5, lineColor=colour2)
    line.draw()
    line = visual.Line(win, start=(0.8, 0), end=(0.55, 0.25), lineWidth=5, lineColor=colour2)
    line.draw()
    line = visual.Line(win, start=(0.8, 0), end=(0.55, -0.25), lineWidth=5, lineColor=colour2)
    line.draw()

    line = visual.Line(win, start=(0, 0.8), end=(0, 0.3), lineWidth=5, lineColor=colour3)
    line.draw()
    line = visual.Line(win, start=(0, 0.8), end=(0.25, 0.55), lineWidth=5, lineColor=colour3)
    line.draw()
    line = visual.Line(win, start=(0, 0.8), end=(-0.25, 0.55), lineWidth=5, lineColor=colour3)
    line.draw()

    line = visual.Line(win, start=(0, -0.8), end=(0, -0.3), lineWidth=5, lineColor=colour4)
    line.draw()
    line = visual.Line(win, start=(0, -0.8), end=(-0.25, -0.55), lineWidth=5, lineColor=colour4)
    line.draw()
    line = visual.Line(win, start=(0, -0.8), end=(0.25, -0.55), lineWidth=5, lineColor=colour4)
    line.draw()
    win.flip()

draw('white', 'white', 'white', 'white')

def do_ardo(delay_time):
    global hz, res, ser

    while(ser.in_waiting >= 10):
        res.append([])
        for j in range(10):
            res[-1].append(ord(ser.read()))
    time.sleep(0.75)

state = 0
index = 0
res = []

ser.reset_input_buffer()
ser.reset_output_buffer()

while True:
    t = event.getKeys()
    x = randint(0, 3)
    y = randint(0, 3)

    if (x == 0):
        draw(colors[y], 'white', 'white', 'white')
    elif x== 1:
        draw('white', colors[y], 'white', 'white')
    elif x==2:
        draw('white', 'white', colors[y], 'white')
    else:
        draw('white', 'white', 'white', colors[y])

    do_ardo(0.75)
    if 'escape' in t:
        ser.close()
        names = ['A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8(photodiod)', 'Counter']
        df = pd.DataFrame(np.array(res), columns = ['A0', 'A1', 'A2', 'A3', 'A4', 'A5', 'A6', 'A7', 'A8(photodiod)', 'Counter'])
        df.to_csv("result.csv")
        print("Результат сохранен в result.csv")
        x = np.array([i / hz for i in range(len(df))])

        fig, axs = plt.subplots(5, 2, figsize=(6, 10))

        axs = axs.flatten()

        for i in range(10):
            y = np.array(df[names[i]])
            axs[i].set_ylim(0, 255)
            axs[i].plot(x, y)
            axs[i].set_title(names[i])
            axs[i].set_xlabel('Время, с')
            axs[i].set_ylabel('Значения')

        plt.tight_layout()

        plt.show()
        break
        
ser.close()
win.close()

Пояснение решения задачи

Код с применением модулей PsychoPy и Serial для создания визуальной среды и сбора данных с Arduino.

Этап 5 / Подзадача 5

Сделайте записи ЭМГ. Одну запись сделайте с расслабленных мышц предплечья, вторую — с напряженных мышц предплечья. Либо можете использовать, если сохранилась, запись ЭМГ предыдущего дня.

Постройте спектр мощности ЭМГ-сигнала. Ответьте на вопрос: «В каком диапазоне частот сильнее всего отличаются спектры для напряженной и расслабленной мышцы?» Интерпретируйте полученные различия.

Аккуратно следуйте правилам электробезопасности (https://disk.yandex.ru/i/Ivnwg5kn1zwWPQ), плату Arduino подключайте к ПК через гальваноразвязку, питание Arduino осуществляйте при помощи аккумулятора (у каждой команды в наличии два заряженных аккумулятора).

Критерии оценивания

Формат ответа: скриншот в формате .png или .jpg, текстовые пояснения в текстовом файле, файлы записанных фрагментов ЭМГ, скрипт в формате .py, вычисляющий и визуализирующий спектры двух фрагментов ЭМГ.

Два файла с данными ЭМГ — 1 балл.

Скрипт в формате .py (допускается .ipynb), вычисляющий и визуализирующий спектры двух фрагментов ЭМГ — 3 балла.

Верная интерпретация полученных различий в форме спектров ЭМГ — 2 балла.

Количество баллов: 6.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 26.02.2025 г.

Решение задачи

Код для построения спектров мощности ЭМГ при разном состоянии мышц см. ниже.

Python
import numpy as np
import matplotlib.pyplot as plt

data_relif = np.loadtxt('раслабленная.dat')
data_tension = np.loadtxt('напряженная.dat')

emg_signal_relif = data_relif[:, 0]
emg_signal_tension = data_tension[:, 0]

sampling_rate = 220

n_r = len(emg_signal_relif)
n_t = len(emg_signal_tension)

fft_result_r = np.fft.fft(emg_signal_relif)
fft_result_t = np.fft.fft(emg_signal_tension)

fft_freqs_r = np.fft.fftfreq(n_r, d=1/sampling_rate)
fft_freqs_t = np.fft.fftfreq(n_t, d=1/sampling_rate)

power_spectrum_r = (np.abs(fft_result_r))[1:]
power_spectrum_t = (np.abs(fft_result_t))[1:]

positive_freqs_r = fft_freqs_r[:n_r//2]
positive_power_spectrum_r = power_spectrum_r[:n_r//2]

positive_freqs_t = fft_freqs_t[:n_t//2]
positive_power_spectrum_t = power_spectrum_t[:n_t//2]

plt.figure(figsize=(10, 6))
plt.plot(positive_freqs_r, positive_power_spectrum_r)
plt.title('Спектр мощности расслабления ЭМГ сигнала')
plt.xlabel('Частота (Гц)')
plt.ylabel('Мощность')
plt.grid(True)
plt.show()

plt.figure(figsize=(10, 6))
plt.plot(positive_freqs_t, positive_power_spectrum_t)
plt.title('Спектр мощности напряжения ЭМГ сигнала')
plt.xlabel('Частота (Гц)')
plt.ylabel('Мощность')
plt.grid(True)
plt.show()

Пример верного пояснения

Во время расслабления мышцы мощность в сигнале существенно ниже, чем во время напряжения. Мощность на более высоких частотах (вплоть до 167 Гц) также заметно отличается.

Максимальный пик мощности во время напряжения мышцы приходится на частоту \(\approx\) 14 Гц, во время расслабления — \(\approx\) 3 Гц.

При расслаблении мышцы мощность в диапазоне 1–14 Гц выше, чем мощность в диапазоне 15–60 Гц (и дальше до 167 Гц). При напряжении мышцы мощность в диапазоне 1–14 Гц ниже, чем мощность в диапазоне 15–60 Гц (дальше мощность постепенно убывает, но все равно выше, чем при расслаблении).

Это связано с тем, что внутри мышцы при ее сокращении протекают крошечные токи, это влияет на амплитуду (увеличивает) сигнала, а амплитуда влияет на мощность спектра.

Рис. 5.3.

Рис. 5.4.

Этап 6 / Подзадача 6

Загрузите набор данных https://disk.yandex.ru/d/Pxcw2JtBCauPGA/2024_data_real_train_final.csv. В данном файле представлены записи ЭЭГ в шести отведениях: Cz, Pz, PO7, PO8, O1, O2. Частота оцифровки: 250 Гц. В файле представлены эпохи, соответствующие двум разным и типовым моментам в эксперименте: вспышка буквы на экране, за которой испытуемый следил, и вспышка буквы, за которой испытуемый не следил. Длительность эпох — 1 с, в данных находится 250 «блоков» по 6 сэмплов соответственно отведения ЭЭГ. В столбце class указан тип эпохи.

Постройте на предложенных данных классификатор и проверьте его работу на тестовых данных, которые можно скачать по ссылке https://disk.yandex.ru/d/Pxcw2JtBCauPGA/2024_data_real_test_final.csv.

В качестве ответа пришлите текстовый файл, содержащий последовательность 0 и 1, соответствующую классам эпох, находящимся в тестовом наборе данных. Пример оформления файла с ответом можно скачать по ссылке https://disk.yandex.ru/d/Pxcw2JtBCauPGA/data_answers_example.csv.

Балл за попытку вычисляется по формуле \(6 \cdot abs(1 - (1-x)\cdot 2)\), где \(x\) — доля совпадения присланного решения с верным ответом.

Критерии оценивания

Формат ответа: текстовый файл с набором меток 0 и 1.

Количество баллов: 6.

Количество попыток: 4.

Дисконт за попытки: 1, 0,95, 0,9, 0,85.

Срок сдачи: 18:30 по Мск, 26.02.2025 г.

Решение задачи

Пример кода для построения классификатора см. ниже.

Python
import pandas as pd
from catboost import CatBoostClassifier
from sklearn.preprocessing import StandardScaler

train_file = '2024_data_real_train_final.csv'
test_file = '2024_data_real_test_final.csv'
output_file = 'predictions_catboost_output.csv'

train_data = pd.read_csv(train_file, delimiter=';')
test_data = pd.read_csv(test_file, delimiter=';')

X_train = train_data.drop(columns=['class'])
y_train = train_data['class']

scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
test_data_scaled = scaler.transform(test_data)

model = CatBoostClassifier(iterations=1000, learning_rate=0.1, depth=6, random_seed=42, verbose=100)
model.fit(X_train_scaled, y_train)

test_predictions = model.predict(test_data_scaled).astype(int)

predictions_str = ''.join(map(str, test_predictions.flatten()))

with open(output_file, 'w') as f:
    f.write(predictions_str)

print(f"Предсказания на тестовой выборке сохранены в файл {output_file}")

Пример возможного ответа на задачу:

00010110000101000000001000001001001010100001010010

Этап 7 / Подзадача 7

Установите на добровольца (добровольцем может быть любой участник команды) 8 каналов ЭЭГ. Для получения хорошего сигнала электроды необходимо корректно установить, можете воспользоваться руководством https://disk.yandex.ru/i/OiBSO-23iK_ZhA (стр. 18–32, но и другие части руководства могут быть полезны и интересны).

Сделайте запись с открытыми и закрытыми глазами, по 1 мин в каждом варианте. Опишите наблюдаемые различия в ЭЭГ, записанной в различных областях головы. Поясните их.

Аккуратно следуйте правилам электробезопасности (https://disk.yandex.ru/i/Ivnwg5kn1zwWPQ), плату Arduino подключайте к ПК через гальваноразвязку, питание Arduino осуществляйте при помощи аккумулятора (у каждой команды в наличии два заряженных аккумулятора).

Критерии оценивания

Формат ответа: скриншот в формате .png или .jpg.

Скриншот с фрагментов ЭЭГ с открытыми глазами — 1 балл.

Скриншот с фрагментов ЭЭГ с закрытыми глазами — 1 балл.

Верное объяснение наблюдаемых различий в различных зонах головы между двумя полученными записями — 3 балла.

Количество баллов: 5.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 26.02.2025 г.

Решение задачи

Рис. 5.5.

Рис. 5.6.

Вариант правильного пояснения различий в ЭЭГ при открытых и закрытых глазах

Из ЭЭГ для закрытых и открытых глаз видим, что амплитуда при закрытых глазах больше, чем амплитуда при открытых глазах. Это прослеживается во всех каналах, но особенно в каналах с затылочной части головы.

Это происходит из-за того, что при закрытых глазах зрительная кора получает меньше информации, и мозг работает более ритмично. Также многие нейроны начинают работать синхронно, что создает высокую амплитуду. Когда же глаза открываются, мозг начинает активно обрабатывать зрительную информацию, это вызывает хаотичную быструю активность разных нейронов, что снижает амплитуду.

Во всех зонах, кроме затылочной, значительных изменений нет, поскольку они напрямую не связаны с обработкой зрительной информации.

Этап 8 / Подзадача 8

Напишите скрипт, который будет:

  • выводить на экран в PsychoPy два круга белого цвета, один находится в левой части экрана, другой — в правой;
  • осуществлять сбор ЭМГ-данных с двух рук (например, предплечья);
  • в соответствии с напряжением и расслаблением рук менять цвет кругов (с белого на красный), при напряжении правой руки меняет свой цвет правый круг, при напряжении левой руки меняет свой цвет левый круг.

Постарайтесь выполнить задачу в среде Python 3.10.

Аккуратно следуйте правилам электробезопасности (https://disk.yandex.ru/i/Ivnwg5kn1zwWPQ), плату Arduino подключайте к ПК через гальваноразвязку, питание Arduino осуществляйте при помощи аккумулятора (у каждой команды в наличии два заряженных аккумулятора).

Критерии оценивания

Формат ответа: скрипт, реализующий описанную процедуру. Видеозапись процесса работы разработанного скрипта с оператором, в кадре должны находиться руки оператора с ЭМГ-электродами, монитор с окном PsychoPy, клавиатура и мышка, работа мышкой и клавиатурой в ходе работы скрипта, когда по сигналу ЭМГ меняется цвет одного или обоих кругов, не допускается. На видео должны быть продемонстрированы три режима работы:

  1. изменение цвета одного из кругов по напряжению одной руки;
  2. изменение цвета обоих кругов при напряжении обеих рук;
  3. попеременное изменение цвета кругов при напряжении той или иной руки.

Видеофайл загружается на Гугл- или Яндекс-диск, ссылка на видео отправляется в теле письма, доступ к видео выставляется всем по ссылке. В случае некорректных настроек доступа к видео, в качестве ответа оно засчитано не будет.

Альтернативный вариант — сдать задачу свободному эксперту на площадке. В таком случае по почте высылается только скрипт. Однако не стоит всерьез надеяться на наличие свободного эксперта на площадке в последний час работы. Прием задачи экспертом не гарантируется при наличии у него очереди сдающих участников, занятости вопросами технического характера, истечению времени приема задачи и др. Невозможность приема задачи экспертом не является основанием для выдачи дополнительного времени на сдачу задачи.

Вывод двух белых кругов в окне PsychoPy — 1 балл.

Смена цвета одного из кругов при напряжении соответствующей кругу руке — 2 балла.

Одновременное изменение цвета обоих кругов при напряжении двух рук — 2 балла.

Попеременное изменение цвета двух кругов при попеременном напряжении рук — 3 балла.

Количество баллов: 8.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 26.02.2025 г.

Решение задачи
Код модуля на Python см. ниже.
Python
from psychopy import visual, core, event
import time, socket, os
from random import randint, shuffle
import matplotlib.pyplot as plt
import pandas as pd
import serial
from serial import Serial
import numpy as np

win = visual.Window(size=(600,600), color ='grey', units='pix')

port = "COM4"
baudrate = 115200
ser = serial.Serial(port, baudrate, timeout=0.1)

left_circle = visual.Circle(win, radius=50, fillColor='white', pos=(-200, 0))
right_circle = visual.Circle(win, radius=50, fillColor='white', pos=(200, 0))

ser.reset_input_buffer()
ser.reset_output_buffer()

def left(color):
    left_circle = visual.Circle(win, radius=50, fillColor=color, pos=(-200, 0))
    left_circle.draw()

def right(color):
    right_circle = visual.Circle(win, radius=50, fillColor=color, pos=(200, 0))
    right_circle.draw()

left('white')
right('white')
win.flip()

while True:
    keys = event.getKeys()

    while(ser.in_waiting >= 2):
        a = ord(ser.read())
        if a == 0:
            b = ord(ser.read())
            if b == 10:
                left('red')
            else:
                left('white')
            print(a, b)
        elif a == 1:
            b = ord(ser.read())
            if b == 11:
                right('red')
            else:
                right('white')
            print(a, b)
    win.flip()
    
    time.sleep(0.1)
    if 'escape' in keys:
        break

    #win.flip()

win.close()
core.quit()

 

Этап 9 / Подзадача 9

На основе сигнала ЭМГ от нескольких мышц (лучше от двух или трех) закодируйте активацию четырех кнопок: «Влево», «Вправо», «Вверх» и «Вниз». Объедините систему управления на основе ЭМГ с игрой «Пакман». Основной модуль «Пакмана» и пример его работы в PsychoPy можно скачать по ссылкам: основной модуль — https://disk.yandex.ru/d/Pxcw2JtBCauPGA/pacman.py, пример — https://disk.yandex.ru/d/Pxcw2JtBCauPGA/primer.py.

Сгенерируйте поле для Пакмана размером \(5\times 5\) ячеек и разместите на этом поле 4 цели. «Съешьте» все цели при помощи управления Пакманом от ЭМГ. Цели размещаются на рабочем поле случайным образом. Если все цели оказались в одной строке или одном столбце, программа перезапускается для получения нового расположения целей. В случае воспроизведения положения целей при разных запусках (цели оказываются в одних и тех же ячейках), модуль pacman.py заменяется на оригинальный по ссылке в задаче.

Рекомендуем выполнить задачу в среде Python 3.10. на стационарном ПК, который был выдан команде для работы.

Аккуратно следуйте правилам электробезопасности (https://disk.yandex.ru/i/Ivnwg5kn1zwWPQ), плату Arduino подключайте к ПК через гальваноразвязку, питание Arduino осуществляйте при помощи аккумулятора (у каждой команды в наличии два заряженных аккумулятора).

Критерии оценивания

Формат ответа: скрипт, реализующий описанную процедуру. Видеозапись процесса работы разработанного скрипта с оператором, в кадре должны находиться руки оператора с ЭМГ-электродами, монитор с окном PsychoPy, клавиатура и мышка, работа мышкой и клавиатурой в ходе работы скрипта, когда по сигналу ЭМГ Пакман делает ходы, не допускается. На видео должны быть хорошо видно «поедание» Пакманом целей на поле.

Видеофайл загружается на Гугл- или Яндекс-диск, ссылка на видео отправляется в теле письма, доступ к видео выставляется всем по ссылке. В случае некорректных настроек доступа к видео, в качестве ответа оно засчитано не будет. Максимальная длительность видеофайла — 3 мин.

Альтернативный вариант — сдать задачу свободному эксперту на площадке. В таком случае по почте высылается только скрипт. Однако не стоит всерьез надеяться на наличие свободного эксперта на площадке в последний час работы. Прием задачи экспертом не гарантируется при наличии у него очереди сдающих участников, занятости вопросами технического характера, истечению времени приема задачи и др. Невозможность приема задачи экспертом не является основанием для выдачи дополнительного времени на сдачу задачи. Максимальное время сдачи задачи эксперту — 3 мин.

Любой произвольный ход Пакмана по сигналу ЭМГ — 2 балла.

Поедание Пакманом каждой из целей за отведенное время (3 мин) — 2 балла за каждую цель.

Количество баллов: 10.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 27.02.2025 г.

Решение задачи

Пример решения задачи на Python, см. ниже.

Python
print("psychoimporting...")
from psychopy import visual, core, event
print("psychoimported!")
import numpy as np
import serial
import scipy.signal as sig
from pacman import Pacman
#coding:utf-8
from psychopy import visual, event
import numpy as np
from psychopy.visual.pie import Pie
import random

def colorchange(stim, color):
    stim.color = color
    stim.draw()

def arm_status(data: np.ndarray, threshold: float, left_arm_channel, right_arm_channel):
    stdleft = np.std(data[0])
    stdright = np.std(data[1])
    left_tense = stdleft > threshold
    right_tense = stdright > threshold
    # left_tense = ((data[0]**2).mean())**0.5 > threshold
    # right_tense = ((data[1]**2).mean())**0.5 > threshold
    return left_tense, right_tense

def get_move(left_tense, right_tense):
    if left_tense:
        if right_tense:
            return "up"
        else:
            return "left"
    else:
        if right_tense:
            return "right"
        else:
            return "down"

def read_arduino(n_channels):
    try:
        line = arduino.readline().decode('utf-8').strip()
        data = list(map(float, line.split(' ')))
        if len(data) != n_channels:
            raise ValueError(f"found only {len(data)} channels") # if not all  channels are present
        return data
    except Exception as e:
        print(f"Ошибка чтения данных с Arduino: {e}")

emg_threshold = 135 # values above count arm as tense
emg_std_threshold = 20
n_channels=10 # for arduino to work, we use only first two
left_arm_channel=0 # 0 if A0 and so on
right_arm_channel=4

print("Попытка соединения с Arduino")
try:
    arduino = serial.Serial('COM5', 115200)
    core.wait(2)
    print("Соединение с Arduino установлено.")
except Exception as e:
    print(f"Ошибка подключения к Arduino: {e}")
    core.quit()

# Create a window
win = visual.Window(size=(800, 600), color=(0, 0, 0))
running = True
data: list[list[int]] = [[],[]]
data_required = 200
pacman = Pacman(win, gridSize=5,nTyrgets=4)
while running:
    keys = event.getKeys()
    if 'escape' in keys:
        running = False
    for _ in range(data_required):
        received = read_arduino(n_channels)
        if received:
            data[0].append(received[left_arm_channel])
            data[1].append(received[right_arm_channel])
    left_tense, right_tense = arm_status(
        np.array(data),
        emg_std_threshold,
        left_arm_channel, 
        right_arm_channel
        )
    move = get_move(left_tense, right_tense)
    if move == "left":
        pacman.left()
    elif move == "up":
        pacman.up()
    elif move == "right":
        pacman.right()
    elif move == "down":
        pacman.down()  
    data = [[],[]]
    win.flip()

# Close the window
win.close()

 

Этап 10 / Подзадача 10

Загрузите набор данных https://disk.yandex.ru/d/Pxcw2JtBCauPGA/2024_data_real_train_final_2702.csv. В данном файле представлены записи ЭЭГ в шести отведениях: Cz, Pz, PO7, PO8, O1, O2. Частота оцифровки: 250 Гц. В файле представлены эпохи, соответствующие двум разным и типовым моментам в эксперименте: вспышка буквы на экране, за которой испытуемый следил, и вспышка буквы, за которой испытуемый не следил. Длительность эпох — 1 с, в данных находится 250 «блоков» по 6 сэмплов соответственно отведения ЭЭГ. В столбце class указан тип эпохи.

Разные эпохи, как целевые так и нецелевые, могут принадлежать разным людям (испытуемым).

Постройте на предложенных данных классификатор и проверьте его работу на тестовых данных, которые можно скачать по ссылке https://disk.yandex.ru/d/Pxcw2JtBCauPGA/2024_data_real_test_final_2702.csv.

В качестве ответа пришлите текстовый файл, содержащий последовательность 0 и 1, соответствующую классам эпох, находящимся в тестовом наборе данных. Пример оформления файла с ответом можно скачать по ссылке https://disk.yandex.ru/d/Pxcw2JtBCauPGA/data_answers_example2702.csv.

Балл за попытку вычисляется по формуле \(7 \cdot abs(1 - (1-x)\cdot 2)\), где \(x\) — доля совпадения присланного решения с верным ответом.

Критерии оценивания

Формат ответа: текстовый файл с набором меток 0 и 1.

Количество баллов: 7.

Количество попыток: 4.

Дисконт за попытки: 1, 0,95, 0,9, 0,85.

Срок сдачи: 18:30 по Мск, 27.02.2025 г.

Решение задачи
Пример решения задачи на Python см. ниже.
Python

# -*- coding: utf-8 -*-
"""Ульта228

Automatically generated by Colab.

Original file is located at
    https://colab.research.google.com/drive/1NaEsGWavydoTjyYugc30Z- UHUGQQPWR9
"""

pip install tensorflow

!pip install keras_tuner

import pandas as pd
import tensorflow.keras

import pandas as pd
import numpy as np
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Dropout
from tensorflow.keras.callbacks import EarlyStopping

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.utils.class_weight import compute_class_weight
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Conv1D, BatchNormalization, MaxPooling1D, Dropout, Flatten
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
import keras_tuner as kt

df = pd.read_csv('/content/drive/MyDrive/10/data10.csv', sep=';')
new = []
# time, epoch, class, values
for i, row in df.iterrows():
    for time, ind in enumerate(range(0, 1500, 6)):
        new.append((time, i, row['class'], *row.tolist()[ind:ind+6]))

df = pd.DataFrame(new, columns=['time', 'epoch', 'class', '1', '2', '3', '4', '5', '6'])

df

df.to_csv('/content/drive/MyDrive/10/new_df.csv')

def prepare_sequences(df):
    epochs = df['epoch'].unique()
    X = np.zeros((len(epochs), 250, 6))
    y = np.zeros(len(epochs))

    for i, epoch_num in enumerate(epochs):
        epoch_data = df[df['epoch'] == epoch_num].sort_values('time')
        X[i] = epoch_data[[str(i) for i in range(1, 7)]].values
        y[i] = epoch_data['class'].iloc[0]

    return X, y

def get_class_weights(y):
    class_weights = compute_class_weight(
        class_weight='balanced',
        classes=np.unique(y),
        y=y
    )
    return dict(enumerate(class_weights))

early_stopping = EarlyStopping(
    monitor='val_loss',
    patience=10,
    restore_best_weights=True,
    mode='min'
)

import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.utils.class_weight import compute_class_weight
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import LSTM, Dense, Conv1D, BatchNormalization, MaxPooling1D, Dropout, Flatten
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.callbacks import EarlyStopping
import keras_tuner as kt

class P300HyperModel(kt.HyperModel):
    def build(self, hp):
        model = Sequential()

        # Первый свёрточный блок
        model.add(Conv1D(
            hp.Int('conv1_filters', 16, 64, step=16),
            kernel_size=hp.Choice('conv1_kernel', values=[3, 5, 7]),
            padding='same',
            input_shape=(250, 6)
        ))
        model.add(BatchNormalization())
        model.add(MaxPooling1D(pool_size=2))
        model.add(Dropout(hp.Float('dropout1', 0.1, 0.5, step=0.1)))

        # Второй свёрточный блок
        model.add(Conv1D(
            hp.Int('conv2_filters', 32, 128, step=32),
            kernel_size=hp.Choice('conv2_kernel', values=[3, 5, 7]),
            padding='same'
        ))
        model.add(BatchNormalization())
        model.add(MaxPooling1D(pool_size=2))
        model.add(Dropout(hp.Float('dropout2', 0.1, 0.5, step=0.1)))

        # LSTM блок
        model.add(LSTM(
            hp.Int('lstm_units', 32, 128, step=32),
            return_sequences=True
        ))
        model.add(BatchNormalization())
        model.add(Dropout(hp.Float('dropout3', 0.1, 0.5, step=0.1)))

        # Выходной блок
        model.add(Flatten())
        model.add(Dense(
            hp.Int('dense_units', 32, 128, step=32),
            activation='relu'
        ))
        model.add(BatchNormalization())
        model.add(Dropout(hp.Float('dropout4', 0.1, 0.5, step=0.1)))
        model.add(Dense(1, activation='sigmoid'))

        # Оптимизатор
        learning_rate = hp.Float('learning_rate', 1e-4, 1e-2, sampling='log')
        optimizer = Adam(learning_rate=learning_rate)

        model.compile(
            optimizer=optimizer,
            loss='binary_crossentropy',
            metrics=['accuracy']
        )

        return model

def find_best_hyperparameters(X_train, y_train, X_val, y_val):
    # Создаем тюнер
    tuner = kt.Hyperband(
        P300HyperModel(),
        objective='val_accuracy',
        max_epochs=5,
        factor=3,
        directory='hyperparameter_tuning',
        project_name='p300_classification'
    )

    # Колбэк для ранней остановки
    early_stopping = EarlyStopping(
        monitor='val_loss',
        patience=5,
        restore_best_weights=True
    )

    # Поиск оптимальных гиперпараметров
    tuner.search(
        X_train, y_train,
        validation_data=(X_val, y_val),
        callbacks=[early_stopping],
        epochs=5
    )

    # Получаем лучшую модель
    best_hps = tuner.get_best_hyperparameters(num_trials=1)[0]

    return best_hps, tuner

X, y = prepare_sequences(df)
X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.1)
class_weights = get_class_weights(y_train)

# Поиск лучших гиперпараметров
best_hps, tuner = find_best_hyperparameters(X_train, y_train, X_val, y_val)

# Создание лучшей модели
best_model = tuner.hypermodel.build(best_hps)

# # Обучение лучшей модели
# history = best_model.fit(
#     X_train, y_train,
#     validation_data=(X_val, y_val),
#     epochs=100,
#     batch_size=32,
#     callbacks=[early_stopping],
#     class_weight=class_weights,
#     verbose=1
# )

# Обучение лучшей модели
history = best_model.fit(
    X_train, y_train,
    validation_data=(X_val, y_val),
    epochs=60,
    batch_size=32,
    callbacks=[early_stopping],
    class_weight=class_weights,
    verbose=1
)

def train_and_evaluate_model(X_train, X_val, X_test, y_train, y_val, y_test, best_hps, tuner):
    """
    Обучение оптимизированной модели и её оценка
    """

    # Оценка на тестовом наборе
    test_metrics = best_model.evaluate(X_test, y_test, verbose=0)
    print(f'Тестовая точность: {test_metrics[1]:.4f}')

    # Прогнозы для подробного анализа
    y_pred = best_model.predict(X_test)
    y_pred_classes = (y_pred > 0.5).astype(int)

    # Метрики для несбалансированных классов
    from sklearn.metrics import classification_report, confusion_matrix
    print("\nОтчёт по классификации:")
    print(classification_report(y_test, y_pred_classes))

    print("\nМатрица ошибок:")
    print(confusion_matrix(y_test, y_pred_classes))

    return best_model, history

# Пример использования:

# Разделение данных на три части
X, y = prepare_sequences(df)
# Сначала отделяем тестовую выборку
X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.1)
# Затем разделяем оставшиеся данные на обучающую и валидационную
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.2)

# Поиск лучших гиперпараметров
best_hps, tuner = find_best_hyperparameters(X_train, y_train, X_val, y_val)

# Обучение и оценка лучшей модели
best_model, history = train_and_evaluate_model(
    X_train, X_val, X_test,
    y_train, y_val, y_test,
    best_hps, tuner
)

best_model.save('/content/drive/MyDrive/10/my_model.keras')

df = pd.read_csv('/content/drive/MyDrive/10/test10.csv', sep=';')
new = []
# time, epoch, class, values
for i, row in df.iterrows():
    for time, ind in enumerate(range(0, 1500, 6)):
        new.append((time, i, *row.tolist()[ind:ind+6]))

df = pd.DataFrame(new, columns=['time', 'epoch', '1', '2', '3', '4', '5', '6'])

epochs = df['epoch'].unique()
X = np.zeros((len(epochs), 250, 6))

for i, epoch_num in enumerate(epochs):
    epoch_data = df[df['epoch'] == epoch_num].sort_values('time')
    X[i] = epoch_data[[str(i) for i in range(1, 7)]].values

y_pred = best_model.predict(X)
y_pred_classes = (y_pred > 0.5).astype(int)
y_pred_classes = y_pred_classes.flatten().tolist()
y_pred = y_pred.flatten().tolist()

import random
outliers = {}
values_dict = {index: value for index, value in enumerate(y_pred)}

for index, value in values_dict.items():
    if abs(value - 0) > 0.25 and abs(value - 1) > 0.25:
        outliers[index] = value

outliers_keys = list(outliers.keys())
num_to_invert = len(outliers_keys) // 2
keys_to_invert = random.sample(outliers_keys, num_to_invert)

for key in keys_to_invert:
    value = outliers[key]
    inverted_value = 1 - value
    outliers[key] = inverted_value

for index, new_value in outliers.items():
    y_pred[index] = new_value

for index in range(len(y_pred)):
    y_pred_classes[index] = 1 if y_pred[index] > 0.5 else 0

with open('/content/drive/MyDrive/10/output.csv', 'w') as file:
    file.write(''.join([str(s) for s in y_pred_classes]))

Пример ответа:

11100001101111100011001010110001000010100101111110

Этап 11 / Подзадача 11

Запишите ЭЭГ и визуализируйте потенциал П300 на основе работы оператора со стрелками, которые были реализованы на предыдущих задачах (воспользуйтесь инструкцией https://disk.yandex.ru/i/OiBSO-23iK_ZhA, в ней есть краткое пояснение о технике получения П300).

Задайте несколько (столько, сколько будет нужно) циклов подмигивания стрелками, чтобы собрать достаточное количество данных для качественного выделения компоненты П300 вызванного потенциала.

Визуализируйте вызванный потенциал во всех каналах ЭЭГ для целевых (за которыми оператор следил) и нецелевых (за которыми оператор не следил) подмигиваний стрелок.

Сделайте вывод об областях мозга оператора, в которых компонент П300 выражен наиболее ярко.

Рекомендуем выполнить задачу в среде Python 3.10. на стационарном ПК, который был выдан команде для работы.

Аккуратно следуйте правилам электробезопасности (https://disk.yandex.ru/i/Ivnwg5kn1zwWPQ), плату Arduino подключайте к ПК через гальваноразвязку, питание Arduino осуществляйте при помощи аккумулятора (у каждой команды в наличии два заряженных аккумулятора).

Критерии оценивания

Формат ответа:

  • скрипт в формате .py, при помощи которого осуществлялся сбор данных;
  • скрипт для получения вызванного потенциала и визуализация вызванного потенциала — скрипт в формате .py или .ipynb;
  • визуализация — в виде графического файла или внутри .ipynb;
  • вывод об областях мозга оператора, где наиболее выражен П300 на основе визуального анализа полученного вызванного потенциала.

Скрипт для подмигивания стрелками и сбора данных ЭЭГ — 1 балл.

Скрипт для вычисления и визуализации (вместе с самой визуализацией) вызванного потенциала для целевых и нецелевых подмигиваний стрелок — 3 балла.

Вывод об областях мозга оператора, где наиболее выражен компонент П300 вызванного потенциала — 2 балла.

Количество баллов: 6.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 27.02.2025 г.

Решение задачи

Пример скрипта для процедуры стимуляции.

Python
from psychopy import visual, event
import time
from random import randint
from math import sqrt
import serial
import threading
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
import csv

PORT = "COM11"
BAUDRATE = 115200
ser = serial.Serial(PORT, BAUDRATE, timeout=1)

EEG_data_watched = []
EEG_data_unwatched = []
snapshot = []
lock = threading.Lock()
stop_event = threading.Event()
watching_arr = 0


def start_window():
    # psychopy variables
    win = visual.Window(size=(800, 800))
    state = 0

    # длина стрелки, длина лепестков
    l_arrow = 0.15
    L = 2 * sqrt(2 * (l_arrow ** 2))

    # начальные координаты стрелок
    left_arrow_sc = -0.8
    right_arrow_sc = 0.8
    top_arrow_sc = 0.8
    bottom_arrow_sc = -0.8

    # creating arrows
    left_arrow = [
        visual.Line(win, start=(left_arrow_sc - 0.008, 0), end=(left_arrow_sc - 0.008 + L, 0), lineWidth=10,
                    colorSpace="rgb255"),
        visual.Line(win, start=(left_arrow_sc, 0), end=(left_arrow_sc + l_arrow, l_arrow), lineWidth=10,
                    colorSpace="rgb255"),
        visual.Line(win, start=(left_arrow_sc, 0), end=(left_arrow_sc + l_arrow, -l_arrow), lineWidth=10,
                    colorSpace="rgb255")]
    right_arrow = [
        visual.Line(win, start=(right_arrow_sc + 0.008, 0), end=(right_arrow_sc + 0.008 - L, 0), lineWidth=10,
                    colorSpace="rgb255"),
        visual.Line(win, start=(right_arrow_sc, 0), end=(right_arrow_sc - l_arrow, l_arrow), lineWidth=10,
                    colorSpace="rgb255"),
        visual.Line(win, start=(right_arrow_sc, 0), end=(right_arrow_sc - l_arrow, -l_arrow), lineWidth=10,
                    colorSpace="rgb255")]
    top_arrow = [
        visual.Line(win, start=(0, top_arrow_sc + 0.008), end=(0, top_arrow_sc + 0.008 - L), lineWidth=10,
                    colorSpace="rgb255"),
        visual.Line(win, start=(0, top_arrow_sc), end=(l_arrow, top_arrow_sc - l_arrow), lineWidth=10,
                    colorSpace="rgb255"),
        visual.Line(win, start=(0, top_arrow_sc), end=(-l_arrow, top_arrow_sc - l_arrow), lineWidth=10,
                    colorSpace="rgb255")]
    bottom_arrow = [
        visual.Line(win, start=(0, bottom_arrow_sc - 0.008), end=(0, bottom_arrow_sc - 0.008 + L), lineWidth=10,
                    colorSpace="rgb255"),
        visual.Line(win, start=(0, bottom_arrow_sc), end=(l_arrow, bottom_arrow_sc + l_arrow), lineWidth=10,
                    colorSpace="rgb255"),
        visual.Line(win, start=(0, bottom_arrow_sc), end=(-l_arrow, bottom_arrow_sc + l_arrow), lineWidth=10,
                    colorSpace="rgb255")]

    arrows = [left_arrow, top_arrow, right_arrow, bottom_arrow]

    for arrow in arrows:
        for arr in arrow:
            arr.color = [0, 191, 255]
            arr.draw()
    win.flip()
    return win, arrows, state

def add_snapshot(watched):
    global snapshot
    global EEG_data_watched
    global EEG_data_unwatched
    arr = []
    arr.extend(snapshot)
    if watched:
        EEG_data_watched.append(arr)
        print(1)
    else:
        EEG_data_unwatched.append(arr)
        print(2)
    snapshot=[]

def read_eeg():
    global snapshot
    global EEG_data_watched
    global EEG_data_unwatched
    while not stop_event.is_set():
        try:
            line = ser.readline().decode()
            val = line.strip().split(',')
            if len(val) == 10:
                with lock:
                    values=[]
                    for i in range(8):
                        values.append(int(val[i]))
                    snapshot += values
        except Exception as e:
            print("Ошибка чтения:", e)
    with open("EEG_watched.csv", "w", newline="") as file:
        writer = csv.writer(file)
        writer.writerows(EEG_data_watched)
    with open("EEG_unwatched.csv", "w", newline="") as file:
        writer = csv.writer(file)
        writer.writerows(EEG_data_unwatched)

def run_psychopy():
    global snapshot
    global watching_arr
    win, arrows, state = start_window()
    while not stop_event.is_set():
        t = event.getKeys()
        if t:
            if state == 0:
                state = 1
                read_eeg_strart()
                t.pop()
            else:
                state = 2
                break

        if state == 1:
            r = randint(0, len(arrows) - 1)
            cur_arr = arrows[r]
            snapshot=[]
            for element in cur_arr:
                element.color = [255, 105, 180]
            for arrow in arrows:
                for arr in arrow:
                    arr.draw()
            win.flip()
            time.sleep(0.75)

            for element in cur_arr:
                element.color = [0, 191, 255]
            for arrow in arrows:
                for arr in arrow:
                    arr.draw()
            win.flip()
            if r==watching_arr:
                add_snapshot(True)
            else:
                add_snapshot(False)
            snapshot=[]
            time.sleep(0.75)

    stop_event.set()
    time.sleep(1.75)
    win.close()

def read_eeg_strart():
    eeg_thread = threading.Thread(target=read_eeg, daemon=True)
    eeg_thread.start()

#psychopy_thread = threading.Thread(target=run_psychopy, daemon=True)
#psychopy_thread.start()

# ani = FuncAnimation(fig, update_plot, interval=1000)
# plt.show()
run_psychopy()

Пример скрипта для визуализации вызванных потенциалов.

Python
import pandas as pd
import numpy as np
import matplotlib
matplotlib.use("TkAgg")  # Используем стандартный бэкенд
import matplotlib.pyplot as plt
from scipy.ndimage import zoom
from mne.filter import filter_data

with open("EEG_watched.csv") as f:
    lines_watched = [line.strip().split(",") for line in f]
min_cols_watched = min(len(row) for row in lines_watched)
data_watched = np.array([row[:min_cols_watched] for row in lines_watched], dtype=float)
print(data_watched.shape)

with open("EEG_unwatched.csv") as f:
    lines_unwatched = [line.strip().split(",") for line in f]
min_cols_unwatched = min(len(row) for row in lines_unwatched)
data_unwatched = np.array([row[:min_cols_unwatched] for row in lines_unwatched], dtype=float)
print(data_unwatched.shape)

# data_watched=data_watched[4:5, :]
# data_unwatched=data_unwatched[4:5, :]

unwatched_channels = np.zeros((data_unwatched.shape[0], 8, data_unwatched.shape[1]//8))
for i in range(data_unwatched.shape[0]):
    for j in range(data_unwatched.shape[1]//8):
        for e in range(8):
            unwatched_channels[i][e][j] = data_unwatched[i][j*6+e]

for i in range(unwatched_channels.shape[0]):
    for e in range(unwatched_channels.shape[1]):
        unwatched_channels[i][e] = filter_data(unwatched_channels[i][e], sfreq=250, l_freq=1, h_freq=40)  # Применяем фильтр с 1 Гц до 40 Гц

watched_channels = np.zeros((data_watched.shape[0], 8, data_watched.shape[1]//8))
for i in range(data_watched.shape[0]):
    for j in range(data_watched.shape[1]//8):
        for e in range(8):
            watched_channels[i][e][j] = data_watched[i][j*6+e]
for i in range(watched_channels.shape[0]):
    for e in range(watched_channels.shape[1]):
        watched_channels[i][e] = filter_data(watched_channels[i][e], sfreq=250, l_freq=1, h_freq=40)

num_of_try = 4
fig, ax = plt.subplots()
colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', '#ff7f00']
plt.figure(figsize=(25, 15))
for i in range(8):
    plt.subplot(4, 2, i + 1)
    plt.plot(watched_channels[num_of_try][i], color=colors[i])
    plt.title(f"График для канала А{i}")
    plt.legend()
    plt.grid(True)
plt.suptitle("EEG сигналы целевые")
plt.savefig("eeg_watched_new.png", dpi=300)
plt.show()

fig, ax = plt.subplots()
colors = ['b', 'g', 'r', 'c', 'm', 'y', 'k', '#ff7f00']
plt.figure(figsize=(25, 15))
for i in range(8):
    plt.subplot(4, 2, i + 1)
    plt.plot(unwatched_channels[num_of_try][i], color=colors[i])
    plt.title(f"График для канала А{i}")
    plt.legend()
    plt.grid(True)
plt.suptitle("EEG сигналы нецелевые")
plt.savefig("eeg_unwatched_new.png", dpi=300)
plt.show()

Визуализации вызванного потенциала, см. на рис. 5.75.8.

Пример пояснения

Компонент P300 — это потенциал, возникающий при совпадении ожидаемого стимула с настоящим. Он проявляется в виде пика, который наблюдается примерно через 300 мс после целевого стимула (того, который совпал с ожидаемым). При анализе графиков за несколько целевых и нецелевых эпох приходим к выводу, что данный компонент в целевых эпохах проявился, а в нецелевых не наблюдался. Построением схемы монтирования электродов и анализа полученных графиков было выяснено, что он наиболее явно проявлялся в теменной области головного мозга.

Рис. 5.7.

Рис. 5.8.

Этап 12 / Подзадача 12

Разработайте интерфейс мозг-компьютер на волне П300, в котором оператор сможет через выбор стрелок «Вверх», «Вниз», «Влево» и «Вправо» управлять ходами Пакмана. Соберите данные для построения классификатора и обучите классификатор. Соедините классификатор с визуальной частью на PsychoPy со стрелками и Пакманом.

Задайте рабочее поле Пакмана — \(5\times 5\) ячеек. Количество целей — 4. Размер ячеек подберите на свой вкус.

Критерии оценивания

Формат ответа:

  • скрипт, реализующий работу визуальной части (стрелки с Пакманом) совместно со сбором данных и работой классификатора;
  • классификатор;
  • видеозапись процесса работы разработанного скрипта с оператором, в кадре должны находиться монитор с окном PsychoPy, клавиатура и мышка, работа мышкой и клавиатурой в ходе работы скрипта не допускается, кроме нажатий клавиши «пробел» для запуска очередного цикла подмигиваний для выбора следующего хода; на видео должны быть хорошо видно «поедание» Пакманом целей на поле.

Видеофайл загружается на Гугл- или Яндекс-диск, ссылка на видео отправляется в теле письма, доступ к видео выставляется всем по ссылке. В случае некорректных настроек доступа к видео в качестве ответа оно засчитано не будет. Максимальная длительность видеофайла — 3 мин.

Альтернативный вариант — сдать задачу свободному эксперту на площадке. В таком случае по почте высылается только скрипт. Однако не стоит всерьез надеяться на наличие свободного эксперта на площадке в последний час работы. Прием задачи экспертом не гарантируется при наличии у него очереди сдающих участников, занятости вопросами технического характера, истечению времени приема задачи и др. Невозможность приема задачи экспертом не является основанием для выдачи дополнительного времени на сдачу задачи. Максимальное время сдачи задачи эксперту — 3 мин.

Построение классификатора на собранных данных — 2 балла.

Поедание Пакманом каждой цели на поле — 2,5 балла.

Количество баллов: 12.

Количество попыток: 3.

Дисконт за попытки: 1, 0,95, 0,9.

Срок сдачи: 18:30 по Мск, 27.02.2025 г.

Решение задачи
Пример скрипта Python для решения задачи см. ниже.
Python
#coding:utf-8
import csv
import time

import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from psychopy import visual, event
import random
import serial
from time import sleep
from pacman import Pacman
from classification import g

win = visual.Window(fullscr=False)
gridSize = 5
cellSize = 0.2
nTyrgets = 4
pacman = Pacman(win, gridSize, cellSize, nTyrgets)
win.flip()

original_color = 'green'
active_color = 'red'
arrow_length = 0.4
side_length = 0.1535534   # катеты чтобы боковые были 50 пикселей
line_width = 10
time_to_sleep = 0.75

adata = {"leftx": [0], "rightx": [0], "upx": [0], "downx": [0], "lefty": [0], "righty": [0], "upy": [0], "downy": [0]}
arrowslist = ["up", "right", "down", "left"]

hz = 256
port = '/dev/ttyACM0'   # '/dev/ttyUSB0' for me
baudrate = 115200
ser = serial.Serial(port, baudrate=baudrate)
result = {"A0": [], "A4": []}
timer = []
ex = ["A0", "A4"]

def create_right(x, y):
    main = visual.Line(win, start=(x, y), end=(x - arrow_length, y), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    line1 = visual.Line(win, start=(x, y), end=(x - side_length, y + side_length / 2), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    line2 = visual.Line(win, start=(x, y), end=(x - side_length, y - side_length / 2), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    return [main, line1, line2]


def create_left(x, y):
    main = visual.Line(win, start=(x, y), end=(x + arrow_length, y), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    line1 = visual.Line(win, start=(x, y), end=(x + side_length, y + side_length / 2), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    line2 = visual.Line(win, start=(x, y), end=(x + side_length, y - side_length / 2), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    return [main, line1, line2]

def create_down(x, y):
    main = visual.Line(win, start=(x, y), end=(x, y + arrow_length), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    line1 = visual.Line(win, start=(x, y), end=(x + side_length / 2, y + side_length), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    line2 = visual.Line(win, start=(x, y), end=(x - side_length / 2, y + side_length), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    return [main, line1, line2]

def create_up(x, y):
    main = visual.Line(win, start=(x, y), end=(x, y - arrow_length), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    line1 = visual.Line(win, start=(x, y), end=(x + side_length / 2, y - side_length), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    line2 = visual.Line(win, start=(x, y), end=(x - side_length / 2, y - side_length), lineWidth=line_width, autoDraw=True, lineColor=original_color)
    return [main, line1, line2]

def recording():
    values = {"A0": -1, "A4": -1}
    while values["A0"] == -1 or values["A4"] == -1:
        first_byte = ser.read(1).decode('utf-8', errors="ignore")
        while first_byte != 'A':
            first_byte = ser.read(1).decode('utf-8', errors="ignore")
        header = "A" + ser.read(1).decode('utf-8', errors="ignore")
        value = int.from_bytes(ser.read(1), byteorder='big')
        if header in ex:
            values[header] = value
    return values["A0"], values["A4"]

def flusher():
    print(1)
    global result, timer, time_start
    result = {"A0": [], "A4": []}
    timer = []
    time_start = time.time()
    a0, a4 = [], []
    for line in arrows["up"]:
        line.color = original_color
    win.flip()
    for x in arrowslist:
        for line in arrows[x]:
            line.color = active_color
        win.flip()
        for i in range(200):
            r, l = recording()
            a0.append(l)
            a4.append(r)
            time.sleep(1/hz)
        for line in arrows[x]:
            line.color = original_color
        win.flip()
        for i in range(200):
            r, l = recording()
            a0.append(l)
            a4.append(r)
            time.sleep(1 / hz)

    data = zip(a0, a4)
    with open('res.csv', 'w', newline='') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(['A0', 'A4'])
        writer.writerows(data)

arrows = {
    'right': create_right(0.95, 0),
    'left': create_left(-0.95, 0),
    'down': create_down(0, -0.95),
    'up': create_up(0, 0.95)
}
win.flip()

flashing = False
active_arrow = None
is_flush = False

while True:
    keys = event.getKeys()
    if 'escape' in keys:
        print("Выход из программы...")
        break
    elif 'space' in keys:
        flusher()
        key = int(g("res.csv")[0])
        print(key, type(key))
        if key == 0:
            pacman.up()
        elif key == 1:
            pacman.right()
        elif key == 2:
            pacman.down()
        else:
            pacman.left()
        win.flip()

 

Этап 13 / Подзадача 13

Пройдите максимально быстро все целевые точки в «Пакмане», которые будут назначены случайным образом в тестовом программном модуле. Управление ходами Пакмана — через ЭМГ. Размер поля — \(8\times 8\), количество целей — 10. Обязательно наличие «нейтрального состояния» — после запуска работы программы (по клавише «пробел») при всех расслабленных мышцах Пакман стоит на месте. Если Пакман движется по полю при расслабленных мышцах оператора, попытка не засчитывается.

Один ход (действие оператора) — смещение Пакмана на одну ячейку. Пакман всегда стартует из верхнего левого угла (дефолтное состояние модуля).

Балл за точность начисляется по количеству съеденных целей — \(A\). Коэффициент за скорость вычисляется по формуле \[K_t = \frac{t_{min}}{t},\] где \(t\) — время выполнения задачи командой в секундах, а \(t_{min}\) — минимальное время (в секундах) выполнения задачи среди всех команд по данной задаче, коэффициент округляется до двух знаков после запятой. Итоговый балл команды вычисляется по формуле \(A \cdot K_t\).

Максимальное время выполнения — 5 мин. При досрочном завершении выполнения задачи (точки были собраны не все) время выполнения устанавливается максимально возможным — 5 мин.

Задача выполняется строго на компьютерах Кванториума. Компьютеры отключены от сети интернет. Из периферийных устройств к компьютеру подключен монитор, клавиатура, мышка и Arduino через гальваноразвязку. Иных периферийных устройств подключено быть не должно. После начала выполнения задачи по нажатию клавиши «пробел» запрещается нажимать какие-либо клавиши на клавиатуре и кнопки мыши. Перед началом сдачи задачи программа запускается несколько раз для подтверждения случайного характера расположения целей для Пакмана. Наличие любой «автоматики» в ходах Пакмана не принимается к оценке.

Время выполнения фиксируется от нажатия клавиши «пробел» для старта программы до достижения последней целевой точки.

Диапазон времени для сдачи задачи доступен по ссылке (https://disk.yandex.ru/i/NA2d78U04nteGg).

Критерии оценивания

Формат ответа: видеозапись процесса работы команды, протокол судейской коллегии.

Количество баллов: 10 (1 балл за каждую съеденную точку при \(t = t_{min}\)).

Количество попыток: 2.

Дисконт: отсутствует.

Срок сдачи: 16:00 по Мск, 28.02.2025 г.

Решение задачи
Пример скрипта Python для решения задачи см. ниже.
Python
print("psychoimporting...")
from psychopy import visual, core, event
print("psychoimported!")
import numpy as np
import serial
import scipy.signal as sig
from pacman import Pacman
#coding:utf-8
from psychopy import visual, event
import numpy as np
from psychopy.visual.pie import Pie
import random

def colorchange(stim, color):
    stim.color = color
    stim.draw()

def arm_status(data: np.ndarray, threshold: float, leg_threshold: float):
    stdleft = np.std(data[0])
    stdright = np.std(data[1])
    stdleg = np.std(data[2])
    print(f"{stdleft=} {stdright=} {stdleg=}")
    left_tense = stdleft > threshold
    right_tense = stdright > threshold
    leg_tense = stdleg > leg_threshold
    # left_tense = ((data[0]**2).mean())**0.5 > threshold
    # right_tense = ((data[1]**2).mean())**0.5 > threshold
    print(left_tense, right_tense, leg_tense)
    return left_tense, right_tense, leg_tense

def get_move(left_tense, right_tense, leg_tense):
    if left_tense and not right_tense:
        return "left"
    if right_tense and not left_tense:
        return "right"
    if right_tense and left_tense:
        return "up"
    if leg_tense:
        return "down"
    
def read_arduino(n_channels):
    try:
        line = arduino.readline().decode('utf-8').strip()
        data = list(map(float, line.split(' ')))
        if len(data) == n_channels:
            return data
        print(f"found {len(data)} channels instead of {n_channels}.")
    except Exception as e:
        print(f"Ошибка чтения данных с Arduino: {e}")
        return 
    
emg_std_threshold = 10
leg_std_threshold = 10
n_channels=10 # for arduino to work, we use only first two
left_arm_channel=1 # 0 if A0 and so on
right_arm_channel=4
leg_channel = 7
data_required = 8

print("Попытка соединения с Arduino")
try:
    arduino = serial.Serial('COM5', 115200)
    core.wait(2)
    print("Соединение с Arduino установлено.")
except Exception as e:
    print(f"Ошибка подключения к Arduino: {e}")
    core.quit()

# Create a window
win = visual.Window(size=(800, 600), color=(0, 0, 0))
running = True
data: list[list[int]] = [[],[],[]]
pacman = Pacman(win, gridSize=8,nTyrgets=10)
win.flip()
event.waitKeys(keyList=["space"])
clock = core.Clock()
start = clock.getTime()
clock.reset()
while running:
    keys = event.getKeys()
    if 'escape' in keys or pacman.activeTargets == 0:
        running = False
    for _ in range(data_required):
        received = read_arduino(n_channels)
        if received:
            data[0].append(received[left_arm_channel])
            data[1].append(received[right_arm_channel])
            data[2].append(received[leg_channel])
    left_tense, right_tense, leg_tense = arm_status(
        np.array(data),
        emg_std_threshold,
        leg_std_threshold
        )
    move = get_move(left_tense, right_tense, leg_tense)
    print(move)
    if move == "left":
        pacman.left()
    elif move == "up":
        pacman.up()
    elif move == "right":
        pacman.right()
    elif move == "down":
        pacman.down()  
    data = [[],[],[]]
    win.flip()

# Close the window
print("time:", clock.getTime(applyZero=True) - start)
win.close()

 

Этап 14 / Подзадача 14

Пройдите максимально быстро все целевые точки в «Пакмане», которые будут назначены случайным образом в тестовом программном модуле. Управление ходами Пакмана — через ЭЭГ (ИМК на П300). Размер поля — \(7\times 7\), количество целей — 10.

Один ход (действие оператора) — смещение Пакмана на одну ячейку. Пакман всегда стартует из верхнего левого угла (дефолтное состояние модуля).

Балл за точность начисляется по «количеству съеденных целей», умноженных на 1,1 (1,1 балла за 1 цель) = A. Коэффициент за скорость вычисляется по формуле \(K_t = t_{min}/t\), где \(t\) — время выполнения задачи командой в секундах, а \(t_{min}\) — минимальное время (в секундах) выполнения задачи среди всех команд по данной задаче, коэффициент округляется до двух знаков после запятой. Итоговый балл команды вычисляется по формуле \(A \cdot K_t\).

Максимальное время выполнения задачи — 5 мин. При досрочном завершении выполнения задачи (точки были собраны не все) время выполнения устанавливается максимально возможным — 5 мин.

Задача выполняется строго на компьютерах Кванториума. Компьютеры отключены от сети интернет. Из периферийных устройств к компьютеру подключен монитор, клавиатура, мышка и Arduino через гальваноразвязку. Иных периферийных устройств подключено быть не должно. После начала выполнения задачи разрешается нажимать только клавишу «пробел» для запуска очередного цикла подмигиваний стрелок, запрещается нажимать какие-либо другие клавиши на клавиатуре и кнопки мыши. Перед началом сдачи задачи программа запускается несколько раз для подтверждения случайного характера расположения целей для Пакмана. Наличие любой «автоматики» в ходах Пакмана не принимается к оценке.

Время выполнения фиксируется от нажатия клавиши «пробел» для старта программы до достижения последней целевой точки.

Диапазон времени для сдачи задачи доступен по ссылке (https://disk.yandex.ru/i/NA2d78U04nteGg).

Критерии оценивания

Формат ответа: видеозапись процесса работы команды, протокол судейской коллегии.

Количество баллов: 11 (1,1 балла за каждую съеденную точку при \(t = t_{min}\)).

Количество попыток: 2.

Дисконт: отсутствует.

Срок сдачи: 16:00 по Мск, 28.02.2025 г.

Решение задачи
Пример скрипта Python для решения задачи.
Python
from psychopy import visual, event
from math import sqrt
import numpy as np
import pandas as pd
from pacman import Pacman
import time
from random import randint, shuffle
import serial
import threading
import joblib
from mne.filter import filter_data
from sklearn.preprocessing import StandardScaler
from pyriemann.classification import MDM, FgMDM

n_epochs = 504
n_channels = 8
n_times = 172
pipeline = joblib.load('pipeline_model_2.pkl')
snapshot = []
lock = threading.Lock()
stop_event = threading.Event()

PORT = "COM3"
BAUDRATE = 115200
ser = serial.Serial(PORT, BAUDRATE, timeout=1)

def read_eeg():
    global snapshot
    while not stop_event.is_set():
        try:
            line = ser.readline().decode()
            val = line.strip().split(',')
            
            if len(val) == 10:
                with lock:
                    values = []
                    for i in range(8):
                        values.append(int(val[i]))
                    snapshot += values
            else:
                print(val)
        except Exception as e:
            print("Ошибка чтения:", e)


def start_window():
    # psychopy variables
    win = visual.Window(size=(800, 800))
    state = 0

    # длина стрелки, длина лепестков
    l_arrow = 0.15
    L = 2 * sqrt(2 * (l_arrow ** 2))
    w = 22

    # начальные координаты стрелок
    left_arrow_sc = -0.9
    right_arrow_sc = 0.9
    top_arrow_sc = 0.9
    bottom_arrow_sc = -0.9

    # creating arrows
    left_arrow = [
        visual.Line(win, start=(left_arrow_sc - 0.01, 0), end=(left_arrow_sc - 0.01 + L, 0), lineWidth=w,
                    colorSpace="rgb255"),
        visual.Line(win, start=(left_arrow_sc, 0), end=(left_arrow_sc + l_arrow, l_arrow), lineWidth=w,
                    colorSpace="rgb255"),
        visual.Line(win, start=(left_arrow_sc, 0), end=(left_arrow_sc + l_arrow, -l_arrow), lineWidth=w,
                    colorSpace="rgb255")]
    right_arrow = [
        visual.Line(win, start=(right_arrow_sc + 0.01, 0), end=(right_arrow_sc + 0.01 - L, 0), lineWidth=w,
                    colorSpace="rgb255"),
        visual.Line(win, start=(right_arrow_sc, 0), end=(right_arrow_sc - l_arrow, l_arrow), lineWidth=w,
                    colorSpace="rgb255"),
        visual.Line(win, start=(right_arrow_sc, 0), end=(right_arrow_sc - l_arrow, -l_arrow), lineWidth=w,
                    colorSpace="rgb255")]
    top_arrow = [
        visual.Line(win, start=(0, top_arrow_sc + 0.01), end=(0, top_arrow_sc + 0.01 - L), lineWidth=w,
                    colorSpace="rgb255"),
        visual.Line(win, start=(0, top_arrow_sc), end=(l_arrow, top_arrow_sc - l_arrow), lineWidth=w,
                    colorSpace="rgb255"),
        visual.Line(win, start=(0, top_arrow_sc), end=(-l_arrow, top_arrow_sc - l_arrow), lineWidth=w,
                    colorSpace="rgb255")]
    bottom_arrow = [
        visual.Line(win, start=(0, bottom_arrow_sc - 0.01), end=(0, bottom_arrow_sc - 0.01 + L), lineWidth=w,
                    colorSpace="rgb255"),
        visual.Line(win, start=(0, bottom_arrow_sc), end=(l_arrow, bottom_arrow_sc + l_arrow), lineWidth=w,
                    colorSpace="rgb255"),
        visual.Line(win, start=(0, bottom_arrow_sc), end=(-l_arrow, bottom_arrow_sc + l_arrow), lineWidth=w,
                    colorSpace="rgb255")]

    arrows = [left_arrow, top_arrow, right_arrow, bottom_arrow]

    for arrow in arrows:
        for arr in arrow:
            arr.color = [0, 0, 0]
            arr.draw()
    win.flip()
    return win, arrows, state

def run_psychopy():
    read_eeg_start()
    win, arrows, state = start_window()
    gridSize = 7
    cellSize = 0.1
    nTyrgets = 10
    pacman = Pacman(win, gridSize, cellSize, nTyrgets)

    que = [0, 1, 2, 3]
    i = 0
    shuffle(que)
    print(que)
    while True:
        t = event.getKeys()
        if t:
            if state == 0:
                state = 1
                t.pop()
            else:
                state = 2
                break

        if state == 1:
            r = que[i]
            i += 1
            if i == len(que):
                i = 0
                shuffle(que)
                print(que)
            cur_arr = arrows[r]
            # snapshot = []
            for element in cur_arr:
                element.color = [255, 255, 255]
            for arrow in arrows:
                for arr in arrow:
                    arr.draw()
            win.flip()
            time.sleep(0.5)

            for element in cur_arr:
                element.color = [0, 0, 0]
            for arrow in arrows:
                for arr in arrow:
                    arr.draw()
            win.flip()
            time.sleep(0.5)
            was = check_p300()
            snapshot = []
            if was and r == 0:
                pacman.left()
            elif was and r == 2:
                pacman.right()
            elif was and r == 1:
                pacman.up()
            elif was and r == 3:
                pacman.down()
            else:
                pass

            # if r==watching_arr:
            #     add_snapshot(True)
            # else:
            #     add_snapshot(False)
            # snapshot=[]
    time.sleep(1.75)
    win.close()


def check_p300():
    global snapshot
    cur = [snapshot[len(snapshot) - n_times * 8:]]
    
    
    X_splitted = np.zeros((1, 8, n_times))
    for i in range(1):
        for j in range(n_times):
            for e in range(n_channels):
                if(cur[i][j * 8 + e]):
                    X_splitted[i][e][j] = cur[i][j * 8 + e]
    
    
    X_splitted = X_splitted.reshape(1, 8, 43, 4)
    X_splitted = X_splitted.mean(axis=3)
#    
#    for i in range(X_splitted.shape[0]):
#        for e in range(n_channels):
#            X_splitted[i][e] = filter_data(X_splitted[i][e], sfreq=250, l_freq=1, h_freq=40)  # Применяем фильтр с 1 Гц до 40 Гц
#    

    # Предсказываю классы для тест данных
    predictions = pipeline.predict(X_splitted)
    # predictions_binary = (predictions > 0.5).astype(int)
    print(predictions, X_splitted.shape)
    return predictions[0]

def read_eeg_start():
    eeg_thread = threading.Thread(target=read_eeg, daemon=True)
    eeg_thread.start()

run_psychopy()

 

Материалы для подготовки
  1. http://www.bitronicslab.com/guide/.
  2. https://www.youtube.com/playlist?list=PLQu4ZlRw9NvtRA3OI9SabAgmoooGle2vL.
  3. http://edurobots.ru/kurs-arduino-dlya-nachinayushhix/.
  4. http://brainseminar.ru/?page_id=253.
  5. https://ntcontest.ru/upload/iblock/b9b/b9b6120243ba5603f9ccc71b60b87c77.pdf.
  6. https://drive.google.com/open?id=1_LoQASIySU23PxcvpIeV8UQgVDMo47Wf.
  7. https://stepik.org/course/67/promo.
  8. https://www.youtube.com/playlist?list=PLYw3n_vP4f8dOmHegKkYUjioE0BmqXA8x.
  9. https://www.youtube.com/playlist?list=PLZntC_OlEOglu6eXzidURAwT4wq4c6B7l.
  10. https://www.booksmed.com/fiziologiya/1777-vyzvannye-potencialy-mozga-v-norme-i-patologii-shagas-prakticheskoe-posobie.html.
  11. https://mitpress.mit.edu/books/introduction-event-related-potential-technique-second-edition.
text slider background image text slider background image
text slider background image text slider background image text slider background image text slider background image