轻量级生理信号处理工具BioSPPy中的ECG(3)

1 简介

前面介绍了使用BioSPPy中提供的R波检测算法以及基于此的心拍截取方法。除了这些,还有3个API需要细讲一下,见下表:

API 用途
biosppy.signals.ecg.compare_segmentation 对比算法检测的R峰位置与标准的R波位置,给出评估结果。
biosppy.signals.ecg.correct_rpeaks 在一定范围内校准算法检测的R波位置,使其位于极大值处。
biosppy.signals.ecg.ecg ecg模块的集成,一个函数完成滤波,R波检测和校正,心拍截取,心率计算,并绘制图形。

下面结合代码示例讲解这三个API。

2 R波检测评估

BioSPPy不仅提供了R波检测算法,也提供了一个可以很方便地对比算法检测结果和标准结果的函数接口,即biosppy.signals.ecg.compare_segmentation,先对其有个大概的了解,它的参数比较多:

biosppy.signals.ecg.compare_segmentation(reference=None, test=None, sampling_rate=1000.0, 
                                         offset=0, minRR=None, tol=0.05)
'''
	入参:* reference(array):标准R峰位置数组作为参考.
    	 * test(array):用于测试评估的R峰位置数组.
         * sampling_rate(int,float,可选参数):信号采样率,默认为1000.0 Hz.
         * offset(int,可选参数): 测试R峰与标准R峰的固定先验偏移长度,单位为采样点个数, 默认为0.
         * minRR(float,可选参数): 可容许的最小RR间期,默认为None.
         * tol(float, 可选参数): 算法检测到的R峰位置与其对应标准R峰位置的宽容限度, 默认为0.05, 
         						单位为秒.
                                
    返回:* TP(int):True Positive, 正确检测到的R峰个数.
    	 * FP(int): False Positive, 错检到的R峰个数(注意与漏检区分).
         * performance(float):测试表现指数,计算方式为 TP/标准R峰个数.
         * acc(float):准确率,计算方式为TP/(TP+FP).
         * err(float):错误率,计算方式为FP/(TP+FP).
         * match(list): 与标准R峰正确对应的测试R峰在上述test数组中的索引.
         * deviation(array): 正确对应的R峰与标准R峰之间的偏差.
         * mean_deviation(float): 平均偏差,即上述deviation的平均值.
         * std_deviation(float): 偏差的标准差,即上述deviation的标准差.
         * mean_ref_ibi(float): 根据标准R峰位置,心拍的平均间隔,单位为秒.
         * std_ref_ibi(float): 根据标准R峰位置,心拍平均间隔的标准差, 单位为秒.
         * mean_test_ibi(float): 根据测试R峰位置, 心拍的平均间隔,单位为秒.
         * std_test_ibi(float): 根据测试R峰位置,心拍平均间隔的标准差, 单位为秒.
'''

出现这么多参数,如果之前没有经验,应该很难很快理清。下面拣一些我认为对于小白,理解起来会稍有曲折的来通俗地说一下,看了一点源码并且基于个人理解,有不当之处还请提出——
offset:这个参数其实是告诉biosppy,使用算法检测到的R峰与标准的R峰会有一个固定的距离差,在评估准不准确时需要考虑进去,把这个固定的偏差忽略掉。比如,你使用的R波检测算法里面用了FIR滤波器,有一个固定的相位延迟有d个点(滤波器阶数的一半),而R波检测算法本身又未对其进行校正,这时你对比检测出来的R峰与标准R峰就必须忽略掉这个相位差,就可以在用compare_segmentation把offset设定为d。
minRR:这个参数是告诉biosppy在评估时,两个相邻R波不能靠的太近,否则会把靠的太近的两个相邻R波中的后一个当做错检。这个也是有道理的,因为医学上有“不应期”的概念,两次心脏的除极化(对应QRS波)必然要相隔一段时间,在这段时间内,无论怎么刺激心脏都不可能产生新一次的除极。所以算法检测到的两个R波如果靠得太近,肯定是出错了。这个“不应期”不同的文献宣称的值不太一样,我看到的基本是0.24s左右。
tol:这个参数是指,如果算法检测到的R波和标准R波之间的偏差在tol之内,那就算正确检测,否则就算错误。
match:入参中,reference和test都是numpy array,经过评估之后,有正确检测出的R峰。而match就是记录在test数组中,哪几个是正确的检测项。比如在test中,第0项,第3项,第5项与reference中的R峰位置成功匹配,那match的值就是[0,3,5]。

接下来使用样例信号来展示一下compare_segmentation的用法和注意事项,首先载入信号本身和标准R波位置,然后使用hamilton_segmenter检测R波:

import time
import logging
import numpy as np
from biosppy.storage import load_txt
from biosppy.signals import ecg
import matplotlib.pyplot as plt

logging.basicConfig(level=logging.DEBUG, 
                    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)

signal_path = "./data/ecg_records_117.txt"
ann_path = "./data/ecg_ann_117.txt"
logging.info("--------------------------------------------------")
signal, _ = load_txt(signal_path)
logging.info("载入信号-%s, 长度 = %d. " % (signal_path, len(signal)))
ann, _ = load_txt(ann_path)
logging.info("载入R峰位置人工标记, 共 %d 个R峰." % (len(ann)))

fs = 360  # 信号采样率 360 Hz
logging.info("调用 hamilton_segmenter 进行R波检测 ...")
tic = time.time()
rpeaks = ecg.hamilton_segmenter(signal, sampling_rate=fs)
toc = time.time()
logging.info("完成. 用时: %f 秒. " % (toc - tic))
rpeaks = rpeaks[0]

日志输出:

2020-01-05 02:04:15,592 - root - INFO - --------------------------------------------------
2020-01-05 02:04:15,645 - root - INFO - 载入信号-./data/ecg_records_117.txt, 长度 = 10800. 
2020-01-05 02:04:15,646 - root - INFO - 载入R峰位置人工标记, 共 25 个R峰.
2020-01-05 02:04:15,647 - root - INFO - 调用 hamilton_segmenter 进行R波检测 ...
2020-01-05 02:04:15,667 - root - INFO - 完成. 用时: 0.018969 秒. 

使用compare_segmentation来评估。这里为了使算法产生错检,更完整地呈现结果,所以把tol参数设置的很低,为0.02s,然而实际中应用中可能没有这么严格的限制,在大部分R波检测的论文中,宽容度更是放到了0.15s(美国EC38标准),所以下面的结果只是为了展示用法,并不作为真正的客观性能:

logging.info("使用compare_segmentation对比算法结果与人工标记 ...")
tic = time.time()
eval_results = ecg.compare_segmentation(ann, rpeaks, fs, tol=0.02)
toc = time.time()
logging.info("完成. 用时: %f 秒. 返回结果类型为 %s ." % (toc - tic, str(type(eval_results))))

日志输出:

2020-01-05 02:04:15,668 - root - INFO - 使用compare_segmentation对比算法结果与人工标记 ...
2020-01-05 02:04:15,669 - root - INFO - 完成. 用时: 0.000000 秒. 
																				返回结果类型为 <class 'biosppy.utils.ReturnTuple'>.

首先,返回结果的类型是在上一篇中提到过的内置类biosppy.utils.ReturnTuple的对象。其实这个解答了一个疑问,那就是为什么biosppy为什么要引入这么一个内置类,直接用Python原生类型或numpy array返回结果不就好了吗,这样还省事。当返回结果的形式比较简单时,这么做当然简单省事,但是如果是compare_segmentation这样返回结果构成很复杂的情况,用biosppy.utils.ReturnTuple就可以很好地打包成一个整体。我们调用这个对象的as_dict()方法,使其变为一个OrderedDict:

dict_results = eval_results.as_dict()

然后观察一下它的构造(这里借助了Anaconda自带Spyder IDE的variable explorer):

TIM截图20200105234523.png

返回结果的key与API文档中的名称是一致的,也可以算是一目了然了。只要知道了各个参数的含义,就可以按需索取了,再一次证明了在返回值形式复杂时,使用biosppy.utils.ReturnTuple的优势,例如:

logging.info("********** 结果报告 *************")
logging.info("* 准确率(acc): %.3f *" % dict_results["acc"])
logging.info("* 总体表现(performance): %.3f *" % dict_results["performance"])
logging.info("*********************************")

输出:

2020-01-05 02:04:15,670 - root - INFO - ********** 结果报告 *************
2020-01-05 02:04:15,671 - root - INFO - * 准确率(acc): 0.895 *
2020-01-05 02:04:15,672 - root - INFO - * 总体表现(performance): 0.680 *
2020-01-05 02:04:15,673 - root - INFO - *********************************

3 R波检测校正

一般来说,R波检测算法就是要定位到R峰的位置,但由于信号情况复杂或是算法机制问题,并不是都能精准定位到R峰。这时可以采取一些修补的手段,比如这里要讲的correct_rpeaks,就是在一定范围内校准算法检测的R波位置,使其位于极大值处:

biosppy.signals.ecg.correct_rpeaks(signal=None, rpeaks=None, sampling_rate=1000.0,
                                   tol=0.05)
'''
	入参:* signal(array):ECG 信号.
    	 * rpeaks(array):需要校正的R峰位置序列.
         * sampling_rate(int,float,可选参数):信号采样率,默认为1000.0 Hz.
         * tol(int,float,可选参数): 校正的最大范围,单位为秒,默认为0.05.
         
    返回:* rpeaks(array):校正后的R峰位置序列.
    
'''

这里需要说明一下tol参数。这个参数的意思是说对于入参rpeaks中的每个位置R,寻找[R-tol, R+tol]区间内的最大值的位置作为校正后的R峰位置,但不保证找到的就是一个峰,因为这个区间也有可能是单调递增或递减,这样区间内的最大值就不能确保是一个峰。
接着上面的代码,使用correct_rpeaks校正R峰:

correct_tol = 0.05
logging.info("使用correct_rpeaks校正R波位置, 最大校正范围 %.3f 秒 ..." % correct_tol)
tic = time.time()
rpeaks_correct = ecg.correct_rpeaks(signal, rpeaks, fs, tol=correct_tol)
toc = time.time()
logging.info("完成. 用时: %f 秒. 返回结果类型为 %s ." % (toc - tic, 
                                             str(type(rpeaks_correct))))

输出:

2020-01-06 23:12:41,794 - root - INFO - 使用correct_rpeaks校正R波位置, 最大校正范围 0.050 秒 ...
2020-01-06 23:12:41,796 - root - INFO - 完成. 用时: 0.001022 秒. 
																				返回结果类型为 <class 'biosppy.utils.ReturnTuple'> .

返回结果的类型仍然是biosppy.utils.ReturnTuple对象,按照第一节讲的第二种取值方式取出校正后的R峰位置序列:

rpeaks_correct = rpeaks_correct.as_dict()["rpeaks"]

绘图来直观的看一下校正的结果:

logging.info("绘制部分R峰校正前后的位置 ...")
num_plot_samples = 3600
sig_plot = signal[:num_plot_samples]
rpeaks_plot = rpeaks[rpeaks <= num_plot_samples]
rpeaks_correct_plot = rpeaks_correct[rpeaks_correct <= num_plot_samples]
plt.figure()
plt.grid(True)
plt.plot(sig_plot)
plt.plot(rpeaks_plot, sig_plot[rpeaks_plot], "ro")
plt.plot(rpeaks_correct_plot, sig_plot[rpeaks_correct_plot], "b*")
plt.show()
logging.info("绘图完成.")

绘图如下:
figure_3.png

可以看到,没校正前(红点),有几个检测到了S波上,经过校正(蓝星)后,都精准定位到了R峰上。

4 ECG综合处理

上面讲了这么多函数或方法,它们的特点是各有作用,基本不重叠。而除此之外,biosppy还提供了ecg模块的“集大成者”——biosppy.signals.ecg.ecg。这一个方法中包含了对ECG的一整套流程处理,依次是滤波,R波检测和校正,心拍截取,心率计算,并绘制图形,先来看一下接口参数情况:

biosppy.signals.ecg.ecg(signal=None, sampling_rate=1000.0, show=True)
'''
	入参:* signal(array):ECG 信号.
    	 * sampling_rate(int,float,可选参数):信号采样率,默认为1000.0 Hz.
         * show(bool,可选参数): 是否画汇总图。默认为True,即画汇总图。
         
    返回:* ts(array):心电信号的时间轴刻度标示,单位为秒.
    	 * filtered(array): 滤波之后的ECG信号,默认为 3~45 Hz FIR带通滤波.
         * rpeaks(array): 检测到的R峰位置序列,默认使用hamilton_segmenter.
         * templates_ts(array): 截取到的心拍的时间轴刻度标示, 单位为秒.
         * templates(array): 截取到的心拍(基于滤波后的信号),为m*n的二维数组,m为心拍个数,n为心拍长度(n个采样点)
         * heart_rate_ts(array): 瞬时心率时间轴刻度标示, 单位为秒.
         * heart_rate(array): 瞬时心率.单位为bpm,即平均每分钟多少心拍.
         
    在该方法内部调用其他方法时,其他方法的可选参数均保持了默认值。
         
'''

对两个参数进行下说明:
ts:所谓时间轴刻度标示,就是给一个时间的参照。比如现在有一个采样率为250 Hz的信号(每0.004s 采样一个点),长度为1000个采样点,那给每个采样点的时间参照就是0.000 s,0.004s,0.008s,……,3.996 s,4.000 s,另外两个template_ts和heart_rate_ts同理,只不过应用对象不同。
heart_rate: 这里指的是瞬时心率,即计算每个RR间期对应的心率。例如,一共检测到了10个R峰,那么就可以计算9个RR间期的长度,根据每个RR间期的长度,计算一个心率,这样可以得到一共9个心率值。计算平均心率的方法前面一篇中讲过,瞬时心率无非就是把平均RR间期换成某个具体的RR间期。

接上面的代码调用一下biosppy.signals.ecg.ecg:

logging.info("使用biosppy.signals.ecg.ecg 综合处理 ...")
tic = time.time()
summary_result = ecg.ecg(sig_plot, fs, True)
toc = time.time()
logging.info("完成. 用时: %f 秒. 返回结果类型为 %s ." % (toc - tic,
                                             str(type(summary_result))))

输出有:

2020-01-07 23:33:49,970 - root - INFO - 使用biosppy.signals.ecg.ecg 综合处理 ...
2020-01-07 23:33:50,227 - root - INFO - 完成. 用时: 0.255050 秒. 
																				返回结果类型为 <class 'biosppy.utils.ReturnTuple'> .

果然,返回类型又是biosppy.utils.ReturnTuple,将它转化为有序字典:

summary_result = summary_result.as_dict()

然后观察一下它的构造(这里借助了Anaconda自带Spyder IDE的variable explorer):

TIM截图20200107233756.png

同样,只要知道了各个参数的含义,就可以根据key来取值。当然更重要的是该方法自己画出来的图:

figure_4.png

分为左右两个部分,右边这个很明显是截取到的心拍叠绘在了一起,而且是基于滤波后的信号截取的而非是原信号,注意横轴单位是时间,对应于上面说的template_ts。左边有三个图,由上到下:

  • 左1:ECG原信号波形图,横轴单位是时间(秒),对应于上述ts。
  • 左2:滤波后的ECG信号波形图和R峰位置指示图,垂直的线指示的是R峰的位置,横轴单位是时间(秒),仍然对应于上述ts。(3~45Hz的带通滤波对该信号的损伤有些大…)
  • 左3:瞬时心率图。横轴单位是时间(秒),对应于heart_rate_ts;纵轴单位是bpm,其值对应于heart_rate。

可以说biosppy.signals.ecg.ecg几乎囊括了前面说的各种ECG处理流程。在我看来,这个方法适合于想大体观测信号情况时使用,由于其内部方法参数都保持了默认,其对外参数的自定义程度不够,所以如果要细致研究的话还是要使用前面所讲的各种方法。

5 总结

这一篇讲解了除了R波识别和心拍截取之外的三个方法,本质上还是围绕了R波识别及后续处理。这三个方法需要注意的点就是它们的输入和返回参数相对复杂,需要对其含义有较好的了解。
同样本篇的所有代码已开源在github:
https://github.com/Aiwiscal/ECG-ML-DL-Algorithm-Python/blob/master/correct_eval_v1.0.py
如果感觉有帮助请给个star哦~

发布了30 篇原创文章 · 获赞 205 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_15746879/article/details/103899704