Выделение событий в сигнале

Выделение событий в сигнале

Допустим, вам прислали для сравнения две записи ЭКГ. Специалист, получивший эти записи, работал в MATLAB, где он создал инфраструктуру для манипуляции множеством записей. Две из них в формате struct он сохранил в формате MATLAB и передал Вам.

Функция для загрузки файлов в формате MATLAB есть в пакете scipy.

import scipy.io
import scipy.signal
u=r'd/ecg.mat'
o=scipy.io.loadmat(u)
o.keys()
dict_keys(['__header__', '__version__', '__globals__', 'ecg1', 'ecg2'])

Поля, начинающиеся с двойных подчеркиваний, содержат метаданные файла.

o['__header__']
b'MATLAB 5.0 MAT-file, Platform: PCWIN64, Created on: Wed Nov 27 19:56:09 2013'

Кроме метаданных в файле две переменные: „ecg1“ и „ecg2“.

o['ecg1'].dtype
dtype([('data', 'O'), ('name', 'O'), ('name_col', 'O'), ('type', 'O'), ('units', 'O'), ('freq', 'O'), ('source', 'O'), ('date', 'O'), ('begin_time', 'O'), ('version', 'O'), ('data_quality', 'O'), ('history', 'O')])

По-умолчанию, переменная формата struct - структура, которая загружается в формате записи numpy: 1 структура - 1 запись.

o['ecg1'].shape
(1, 1)
o['ecg1'].dtype.fields
mappingproxy({'data': (dtype('O'), 0),
              'name': (dtype('O'), 8),
              'name_col': (dtype('O'), 16),
              'type': (dtype('O'), 24),
              'units': (dtype('O'), 32),
              'freq': (dtype('O'), 40),
              'source': (dtype('O'), 48),
              'date': (dtype('O'), 56),
              'begin_time': (dtype('O'), 64),
              'version': (dtype('O'), 72),
              'data_quality': (dtype('O'), 80),
              'history': (dtype('O'), 88)})

Преобразуем структуру в серию pandas.

d = {k:o['ecg1'][0][0][k].squeeze() for k in o['ecg1'].dtype.fields}  #.squeeze() чтобы убрать лишние измерения
d = pd.Series(d)
d
data            [-6669.0, -6743.0, -6810.0, -6729.0, -6636.0, ...
name                                          Электрокардиограмма
name_col                                                      ecg
type                                                            1
units                                                          mV
freq                                                          250
source                                                         []
date                                                  22-Apr-2010
begin_time                                                      0
version                                                      5.16
data_quality                                                    4
history                                 Created by eegdata2oECG 1
dtype: object

Очевидно, данные находятся в поле data.

y = (d['data'])
plot(y)
y.shape
(78000,)
_images/i_ecg_16_1.png

Посмотрим фрагмент подробнее.

plot(d['data'][17000:18000]);
_images/i_ecg_18_0.png

Да это же кардиограмма! Мы знаем, что сердце бьётся примерно один раз в секунду, однако числа по оси Ox не похожи ни на секунды, ни на миллисекунды.

В поле freq мы обнаруживаем частоту сигнала. Частота в Гц - это сколько отсчетов сигнала помещается в 1 с.

freq = float(d['freq'])
freq
250.0

Для второй записи извлечем данные напрямую, поскольку мы уже изучили структуру данных.

y2 = o['ecg2'][0][0]['data']
y2.shape
(150800, 1)

Наложим два сигнала, задавая попарно координаты x и y через запятую.

x=arange(0, len(y)/freq, 1/freq)
x2=arange(0, len(y2)/freq, 1/freq)
plot(x,y, x2,y2, alpha=.7)
xlabel('time, s'); ylabel('ecg, $\mu V$'); legend(['ecg1','ecg2']);
_images/i_ecg_25_0.png

Видно, что первая запись продолжалась около 5 мин, а вторая запись длиннее - около 10 мин. При этом в начале записи сильно отклоняются от нуля.

Чтобы убрать колебания изолинии - профильтруем сигнал фильтром низких частот.

# делаем фильтр с пропусканием выше 2 Гц
b, a = scipy.signal.firwin(int(freq/2), 2/(freq/2), pass_zero=False), 1
# запускаем фильтр без сдвига фазы
yf = scipy.signal.filtfilt(b,a,y)
# смотрим эффект на картинке
plot(x,y, x,yf,'r');
xlabel('time, s'); ylabel('ecg, $\mu V$'); legend(['ecg1','ecg1 filtered']);
_images/i_ecg_28_0.png

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

Меняйте начальную точку для отображения разных фрагментов.

t0 = 58 # начальная точка просмотра, попробуйте поменять
porog=1200  # порог

i0=int(t0*freq)
ii=slice(i0,i0+int(4*freq))
plot(x[ii],yf[ii],'r');

hlines(porog, x[ii][0], x[ii][-1], 'k','--');
_images/i_ecg_31_0.png

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

Конечно же существуют специальные пакеты для подсчета пульса по записи ЭКГ с учетом инверсии сигнала, помех и т.д. Данный алгоритм служит для демонстрации принципов анализа сигналов.

# находим участки сигнала выше порога

#вначале один
i=500
iStart = i + argmax(yf[i:] > porog)   # начало зубца - превышение порога
iEnd = iStart + argmin(yf[iStart:] > porog)  # конец зубца
ii=slice(iStart,iEnd)
plot(x[ii],yf[ii],'r');

iR= iStart + argmax(yf[ii])
plot(x[iR], yf[iR], 'bo');
_images/i_ecg_33_0.png
# теперь повторим это для всех участков
# введем мертвую зону 400 мс, чтобы не попадать на высокие Т-зубцы
nDead=int(0.4*freq)

rr=[]

i=0
while i< len(yf):  # двигаемся вдоль графика
    iStart = i + argmax(yf[i:] > porog)  # начало зубца - превышение порога
    if iStart == i:
        break
    iEnd = iStart + argmin(yf[iStart:] > porog) # конец зубца
    ii=slice(iStart,iEnd)
    iR= iStart + argmax(yf[ii])

    _r=[x[iR], yf[iR]]
    rr.append(_r)
    
    i=iEnd+nDead  #прыгаем через "мертвую зону"

rr.__len__()
376

Найдено 376 сокращений на 300-секундной записи.

rr = array(rr)
hist(rr[:,1]); xlabel('Амплитуда пиков, мкВ'); ylabel('Частота');
_images/i_ecg_36_0.png

Выделяются два вида зубцов - высокие и низкие (примерно в половину от амплитуды высоких).

Посмотрим как варьируют интервалы между найденными зубцами.

drr=diff(rr[:,0])
hist(drr); xlabel('Интервалы между сердечными сокращениями, с'); ylabel('Частота');
_images/i_ecg_38_0.png

Чаще всего встречались интервалы около 0.8 с (нормальное сердцебиение), также было значительное количество около 0.5 с, и еще одна группа около 1.2 с.

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

scatter(drr[1:], drr[:-1]);
xlabel('Текущий кардиоцикл'); ylabel('Предыдущий кардиоцикл');
_images/i_ecg_41_0.png

Если сердце бьётся нормально, то и текущий, и предыдущий интервалы равны примерно 0.8 с. Найдите это пересечение. Если человек расслабился, сердечный ритм замедлился, то интервал 0.9 по обеим осям - верхняя правая часть центрального облака. Облако вытянуто наискось, что означает, что частота сокращений изменяется плавно. Теперь найдем экстрасистолы с интервалом 0.5 с. Перед ними всегда был нормальный интервал около 0.8 с (левое облако точек). После экстрасистолы всегда возникала пауза (правое нижнее облако). И осталось последнее верхнее облако - после паузы больше секунды всегда следовало нормальное сокращение.

Мы видим четкие четыре обособленных группы значений! У этого человека серьезные проблемы с сердечной регуляцией. Это пример диагностики без использования статистических методов. Подробности см. Экстрасистолия