AnyMath 文档
Notebook

检测ECG信号上的R峰

该项目考虑了用于检测R峰的ECG信号处理。 加载信号,过滤网络干扰,准备检测信号并在心电信号上显示发现的R峰。

显示设置

功能 engee.clear() 清理工作区:

In [ ]:
engee.clear()

两种图形渲染模式可用于可视化结果。:

*对于静态,快速渲染,使用 gr().

*对于交互式,具有缩放和查看值的能力的动态可视化,使用 plotlyjs().

通过注释掉第二个命令来选择其中一个命令。 两个后端是互斥的,最后一个被调用的后端将被激活。

In [ ]:
# gr()
plotlyjs()
Out[0]:
Plots.PlotlyJSBackend()

我们将使用函数进行连接 include 的"read"文件。jl"用于读取文件:

In [ ]:
include("$(@__DIR__)/read.jl")
Out[0]:
safe_read_ann (generic function with 1 method)

安装必要的库

在这个阶段,必要的Julia库将被检查并自动安装,如果它们在工作环境中不可用。 有问题的示例使用库 Statistics 在心率变异性分析中执行基本统计计算所必需的,包括处理RR间隔、计算平均值和估计数据的分布。

In [ ]:
let
    installed_packages = collect(x.name for (_, x) in Pkg.dependencies() if x.is_direct_dep)
    list_packages = ["Statistics"]
    for pack in list_packages
        pack in installed_packages || Pkg.add(pack)
    end
end
using Statistics

图书馆正在激活 EngeeDSP 并导入了数字心电信号处理所必需的功能,包括滤波、频谱分析、峰值搜索和卷积。

In [ ]:
import EngeeDSP.Functions: fft, butter, filter, findpeaks, conv, freqz

心电信号处理与分析

本节讨论ECG信号处理和分析的顺序,包括数据加载,频谱分析,信号滤波,R峰值检测和rr间隔的计算。 然后将获得的结果用于使用RR转速表,rr间隔分布的直方图和散射图评估心率变异性。

信号频谱计算功能

正在创建函数 calc_fft,设计用于使用离散傅立叶变换计算信号的幅度谱。 该函数生成单侧频谱,计算相应的频率轴,并在线性和对数尺度上确定幅度频谱。 然后将获得的数据用于ECG信号的频谱分析和其频率组成的评估。

In [ ]:
function calc_fft(x, fs)
    len = length(x)
    S = fft(x)  # DFT
    
    # 单向频谱
    K = len ÷ 2
    S = S[1:K]      
    freq = (0:K-1) .* fs ./ len     # 频率轴从0到fs/2

    # 单侧振幅谱
    S_amp = 2 .* abs.(S) ./ len
    S_amp[1] = 0.0

    # 单侧振幅谱,单位为dB
    S_dB = 20 .* log10.(S_amp .+ 1e-12)

    return freq, S_amp, S_dB
end
Out[0]:
calc_fft (generic function with 1 method)

上传和准备数据

从MIT-BIH数据库加载ECG信号的选定片段。 基于来自头文件的信息,确定记录的采样频率,之后设置分析的信号段的开始和持续时间。 接下来,读取心电记录的相应时间片段,选择第二标准心电导联对应的第一信号通道,广泛应用于心率分析和R峰检测。

In [ ]:
data = "$(@__DIR__)/mitdb/100"

# 读取头文件并获取采样率
hdr = read_header(data)
fs = Float64(hdr.fs)

# 选择用于分析的信号片段
t_start = 5.0 * 60.0        # 片段从5分钟开始。
t_dur   = 5.0  * 60.0      # 片段的持续时间为5分钟。

# 将时间从秒转换为倒计时数字
sampfrom = Int(round(t_start * fs))
sampto   = sampfrom + Int(round(t_dur * fs))

# 读取ECG信号的选定片段
_, raw, phys = read_dat_212(data; sampfrom=sampfrom, sampto=sampto)

# 选择用于分析的引导通道
ecg = phys === nothing ? Float64.(raw[:,1]) : Float64.(phys[:,1])

# 计算计数数并形成时间轴
N = length(ecg)
t = collect(0:N-1) ./ fs
Out[0]:
108000-element Vector{Float64}:
   0.0
   0.002777777777777778
   0.005555555555555556
   0.008333333333333333
   0.011111111111111112
   0.013888888888888888
   0.016666666666666666
   0.019444444444444445
   0.022222222222222223
   0.025
   0.027777777777777776
   0.030555555555555555
   0.03333333333333333
   ⋮
 299.96666666666664
 299.96944444444443
 299.97222222222223
 299.975
 299.97777777777776
 299.98055555555555
 299.98333333333335
 299.9861111111111
 299.9888888888889
 299.9916666666667
 299.99444444444447
 299.9972222222222

显示原始心电信号

心电信号的原始片段在时域中显示。

In [ ]:
p1 = plot(t, ecg,
    xlabel="时间,从",
    ylabel="振幅,mV",
    label="原始信号"
)
Out[0]:

初始心电信号的频谱分析

使用先前创建的函数 calc_fft 心电信号谱进行计算,构建单侧幅值谱。 由此产生的图形使我们能够估计信号幅度的频率分布并确定ECG信号的主要频谱分量。

In [ ]:
freq0, S0_amp, _ = calc_fft(ecg, fs)

p2 = plot(freq0, S0_amp,
    xlabel="频率,赫兹",
    ylabel="振幅,mV",
    label="原始信号",
    xlim=(0, fs/2)
)
Out[0]:

从获得的ECG信号频谱中,可以观察到60Hz区域的明显频谱分量,对应于网络干扰。 该部件的存在是由于电气网络和信号记录设备的影响。 为减少网络干扰的影响,接下来将对心电信号进行数字滤波。

心电信号滤波

为了抑制网络干扰,合成了巴特沃斯陷波滤波器。 选择60Hz的频率作为中心抑制频率,对应于先前通过对信号进行频谱分析检测到的网络干扰的频谱分量。

In [ ]:
f_center = 60.0     # 谐振频率,Hz
bw = 6.0        # 滤波器带宽,Hz
f1 = f_center - bw/2      # 下限频率,Hz
f2 = f_center + bw/2      # 上限频率,Hz

# 归一化频率相对于采样率
wn1 = f1 / (fs / 2)
wn2 = f2 / (fs / 2)

order = 6      # 投影滤波器的顺序

b, a = butter(order, [wn1, wn2], "stop")    # 巴特沃斯陷波滤波器的合成
Out[0]:
2-element Vector{Any}:
 [0.8167515449979387 -4.907234464272076 … -4.907234464272074 0.8167515449979387]
 [1.0 -5.805671517442012 … -4.14311738707587 0.6670830862565154]

为了分析合成滤波器的性质,计算并构造了其幅频响应。 使用函数 freqz 确定滤波器的复透射系数,之后将角频率转换为赫兹并以对数标度计算频率响应。

In [ ]:
# 滤波器幅频响应(AFC)的计算
h, w = freqz(b, a, 2048)
f_resp = w .* fs ./ (2π)    # 将角频率转换为Hz

# 频率响应的计算(以dB为单位)
amp_resp = 20 .* log10.(abs.(h) .+ 1e-12)

plot(f_resp, amp_resp,
    xlabel="频率,赫兹",
    ylabel="频率响应,dB",
    legend=false,
    xlim=(0, fs/2)
)
Out[0]:

根据得到的曲线图,能够观察到频率60Hz附近的信号抑制的区域,对应于网络干扰。

使用先前合成的陷波滤波器对ECG信号进行滤波。 在处理过程中,60Hz区域中的频谱分量被衰减,对应于网络干扰。 这使得可以在进一步检测R峰和计算RR间隔之前减少外部电干扰的影响并改善ECG信号的质量。

In [ ]:
ecg_filt = filter(b, a, ecg)        # 心电信号滤波
Out[0]:
108000-element Vector{Float64}:
 -0.2613604943993404
 -0.21250240167607903
 -0.26573066807757795
 -0.36614738177362005
 -0.4161172656214138
 -0.39324744502909514
 -0.32258636917030514
 -0.2986311375931025
 -0.3313726245576049
 -0.3962893283973373
 -0.40059139122729925
 -0.3890850840151229
 -0.36649244512364276
  ⋮
 -0.34206831520401354
 -0.3327934850479681
 -0.3324339179822744
 -0.347472287278352
 -0.3435299337450428
 -0.3383511370436336
 -0.33223499162723336
 -0.33419189908414015
 -0.31892828917862315
 -0.3344302495989344
 -0.320462905333482
 -0.3240091843406425

滤波后的ECG信号的频谱被计算并显示在先前构造的图形上。 这使您可以直观地比较滤波前后信号的频谱组成,以及评估60Hz区域内网络干扰抑制的有效性。

In [ ]:
freq1, S1_amp, _ = calc_fft(ecg_filt, fs)

plot!(p2, freq1, S1_amp, label="过滤后")
Out[0]:
In [ ]:
plot!(p1, t, ecg_filt, label="过滤后")
Out[0]:

应用陷波滤波器后,频率为60Hz的频谱分量被明显抑制。 心电信号的主要能量保持集中在低频区域,因此保留了有用的波形。 这表明所选择的滤波器有效地减少了网络干扰,并且不会显着扭曲ECG信号的信息部分。

R峰的检测

为了增加QRS复合物和R峰的严重程度,计算ECG信号导数。 该导数使得能够识别QRS复区的信号特性的急剧变化。 应用函数后 diff 样本数量减少一个,因此使用函数 vcat 一个零值被添加到数组的开头,这允许您保存信号的原始长度。

In [ ]:
d_ecg = vcat(0.0, diff(ecg_filt))   # 心电信号导数的计算

plot(t, d_ecg,
    xlabel="时间,从",
    ylabel="导数的幅度,rel。 单位",
    title="信号的导数",
    legend=false
)
Out[0]:

在计算导数之后,信号被平方。 该操作使得能够放大具有对应于QRS复合物和R峰值的大振幅的区域,以及附加地抑制信号的低振幅分量。 此外,在平方之后,所有信号值变为正,这简化了R峰的进一步处理和检测。

In [ ]:
sq_ecg = d_ecg .^ 2     # 平方信号的导数

plot(t, sq_ecg,
    xlabel="时间,从",
    ylabel="振幅,相对单位",
    title="导数的平方",
    legend=false
)
Out[0]:

对导数平方后,使用移动平均方法对信号进行平滑处理。 为此,设置了一个约20ms的窗口,将其转换为采样数量,同时考虑到采样频率。 接下来,形成一组相同的平均系数,其总和等于一。 这样的窗口对信号的应用是使用函数来执行的 conv 来自EngeeDSP库,它实现了卷积操作。

In [ ]:
# 使用移动平均方法平滑信号
win = max(3, Int(round(0.010 * fs)))
kernel = ones(win) ./ win

det_sig = conv(sq_ecg, kernel)  # 使用卷积的信号平滑
det_sig = det_sig[1:N]  # 将信号长度减小到原始大小

plot(t, det_sig,
    xlabel="时间,从",
    ylabel="振幅,相对单位",
    title="用于检测R峰的信号",
    legend=false
)
Out[0]:

制备的信号用于检测R-峰。 det_sig,其中QRS复合体的区域变得更加明显。 检测阈值自适应地计算为平均信号值和1.5个标准偏差的总和。 这种方法比从最大值开始的固定阈值更好,因为它对单次发射的依赖性更小,并且考虑了整体信号强度。

参数 MinPeakDistance 设置找到的相邻峰之间的最小距离。 这是必要的,以便在单个QRS复合体中不会检测到多个极大值。 对于正常节律,心动过缓和心动过速的分析,使用约0.30s的距离,因为0.50s的值限制了频率高于120次/分钟的节律的检测,并且可导致心动过速中缺失峰

功能 findpeaks 从EngeeDSP库中,它搜索在峰之间的高度和距离上满足指定条件的局部极大值。 结果,形成了所发现的峰的振幅的阵列。 pks 和他们的位置的数组 locs ,然后用于计算RR间隔。

In [ ]:
thr = mean(det_sig) + 1.5 * std(det_sig)    # 自适应检测阈值
min_dist = Int(round(0.5 * fs))   # R峰之间的最小距离

# 搜索R峰
pks, locs = findpeaks(
    det_sig, out=:data,
    MinPeakDistance = min_dist,
    MinPeakHeight  = thr
)
Out[0]:
(Ypk = [0.07705275825787196, 0.0916773825278909, 0.10834663376541775, 0.09481633031944532, 0.09963496404605718, 0.10460719300050016, 0.08650497487378789, 0.08319442366179106, 0.09247478104048162, 0.11295427417467355  …  0.1004942866529982, 0.08518680051583521, 0.08025327798569036, 0.08981339449670496, 0.0967951803374813, 0.08915943080450855, 0.08684800494412531, 0.08676418960626395, 0.09614281275354113, 0.08352697256693856], Xpk = [52, 349, 650, 932, 1206, 1493, 1780, 2083, 2381, 2680  …  105334, 105614, 105901, 106188, 106472, 106744, 107012, 107288, 107570, 107857], Wpk = [4.3850150608365865, 4.442932153811, 4.291772746874244, 4.367091061929614, 4.328827355369185, 4.38801284823262, 4.733936150335694, 4.452001752022625, 4.403056166576334, 4.284659123849451  …  4.387227236977196, 4.42038689427136, 4.44097986187262, 4.375775133899879, 4.294981876926613, 4.350528891707654, 4.336650176890544, 4.633688054411323, 4.2678951177804265, 4.363150334407692], Ppk = [0.07704879058713884, 0.09167628535186181, 0.10834571777219215, 0.09481181465597753, 0.09963224995512014, 0.10460611620966644, 0.08650225879249397, 0.0831901731489352, 0.09247192121249741, 0.11295348704301397  …  0.10049394311552719, 0.0851814030130436, 0.08024837323306237, 0.08981076469939231, 0.09679261413663849, 0.08915510722090965, 0.08684034685887856, 0.08675754683512554, 0.09613799667239774, 0.08351632852143341])

显示初始检测R峰的结果

In [ ]:
plot(t, det_sig,
    xlabel="时间,从",
    ylabel="振幅,相对单位",
    title="发现R峰",
    legend=false
)
r_times = (locs .- 1) ./ fs   # 将找到的峰值的指数转换为秒

scatter!(r_times, pks, markersize=3)  # 在检测信号上显示发现的峰值
Out[0]:

结果的视觉验证表明,准备的信号适合搜索R峰:发现的最大值与QRS复合物的区域重合,并且选择的阈值和最小距离参数有助于避免在同一心动周期内

由于初级搜索不是由ECG信号本身执行的,而是由信号执行的 det_sig 所发现的最大值的位置可以相对于实际的R峰稍微偏移。 因此,进一步细化坐标:在每个找到的位置附近分析经过滤波的ECG信号的一小部分,并将其局部最大值作为R峰值。

In [ ]:
# 澄清R峰的位置
halfwin = Int(round(0.05 * fs))   # 搜索框
r_locs = Int[]

for idx in locs
    # 找到的峰周围区域的边界
    l = max(1, idx - halfwin)
    r = min(N, idx + halfwin)

     # 搜索选定区域内的最大值
    seg = ecg_filt[l:r]
    _, imax = findmax(seg)

    push!(r_locs, l + imax - 1)
end

r_locs = unique(sort(r_locs))   # 删除重复索引

r_times = (r_locs .- 1) ./ fs   # 发现的R峰的时间
r_vals = ecg_filt[r_locs]       # 发现的R峰的振幅

plot(t, ecg_filt,
    xlabel="时间,从",
    ylabel="振幅,mV",
    title="发现R峰",
    legend=false
)

scatter!(r_times, r_vals, markersize=3)
Out[0]:

结果表明,r峰的细化位置正确地对应于滤波ECG信号的最大值。 这种方法使得可以将基于准备好的信号的初级检测与ECG的实际形状联系起来,并获得更精确的R峰的时间坐标。

RR间隔的计算

找到的R峰值的时间坐标用于计算RR间隔。 首先,确定相邻R峰之间的差异,即以秒为单位的每个心动周期的持续时间。 然后将获得的值转换为毫秒,因为这个维度更便于分析心率。 此外,瞬时心率计算为RR间隔的倒数。

In [ ]:
rr = diff(r_times)  # 相邻R峰之间的差异,具有

rr_ms = rr.* 1e3  # 将RR间隔转换为ms

rr_time = r_times[2:end]    # RR间隔的时间轴

heart_rate = 60.0 ./ rr # 计算心率、心跳/分钟

plot(rr_time, rr_ms,
    xlabel="时间,从",
    ylabel="RR-间隔,ms",
    title="RR间隔",
    legend=false
)
Out[0]:

该图表显示了RR间隔随时间的变化。 它可用于评估心率的均匀性,并查看心动周期的持续时间在ECG信号的选定片段上的变化程度。

构建心率随时间变化的曲线图。 心率值使用RR间隔计算:相邻R峰值之间的距离越小,心率越高,反之亦然。

In [ ]:
plot(rr_time, heart_rate,
    xlabel="时间,从",
    ylabel="心率,心跳/分钟",
    title="心率",
    legend=false
)
Out[0]:

由此产生的图表允许您评估心电信号的选定片段上的心率动态。 节奏相对稳定,心率值变化平稳,处于近距离。 图形上的突然跳跃可以与心率的真实变化相关联,以及与检测信号中的R峰或伪影的错误相关联。

使用的材料

这项工作使用了从开源PhysioNet获得的真实实验心电图记录。 将[MIT-BIH心律失常数据库]作为源数据(https://physionet.org/content/mitdb/1.0.0 /)。 在此示例中,使用该条目 100,以文件表示 100.hea100.dat.

结论

在该示例中,考虑了使用EngeeDSP库检测R峰值然后计算RR间隔和心率的数字ECG信号处理。

示例的工作包括以下主要步骤:

***数据下载。**从开放数据库PhysioNet MIT-BIH心律失常数据库获得初始ECG数据。 记录片段用于分析。 100 含真实的实验心电信号。
***光谱分析。**为了估计原始信号的频率组成,构建了单侧振幅谱。 频谱显示在60Hz区域有一个明显的分量,对应于网络干扰。
*信号滤波。合成了巴特沃斯陷波滤波器来抑制网络干扰。 使用函数 butter, freqz, filter 来自EngeeDSP图书馆。
*准备信号进行检测。为了分离QRS复合物的区域,计算滤波的ECG信号的导数,使用移动平均方法对导数进行平方和平滑。 平滑是使用函数实现的 conv 来自EngeeDSP图书馆。
*检测R峰。该函数用于搜索局部最大值 findpeaks 来自EngeeDSP图书馆。 在考虑自适应阈值和相邻峰之间的最小距离的情况下执行检测,这使得可以避免在同一心动周期内重复触发。
***澄清R峰的位置。**在初始检测后,在过滤的ECG信号上澄清了R峰的位置。 这使得可以将准备好的信号的搜索结果与ECG信号的实际形状联系起来。
***计算RR间隔和心率。**根据发现的R-峰值的时间坐标,计算RR间隔和瞬时心率。 获得的图表使我们能够估计心动周期持续时间的变化以及所选记录片段上的心率动态。

作为示例的结果,实施了从加载源数据到检测R峰、计算RR间隔和心率的ECG信号处理的序列。