☰
360 Hz、11位ECG绘图:量纲还原、注释解析与抽稀避坑
2026/10/29 23:03:57 网站建设 项目流程

简介:面向心电信号处理入门者与科研人员的一套小型实践资料,围绕MIT-BIH ECG的一分钟记录与MATLAB绘图分析展开,可用于理解数据加载、采样率含义、波形可视化及P波、QRS复合波、T波等关键特征的观察。压缩包共4个文件,包含MAT数据、M脚本、ASV备份与INFO说明,整体约20KB,结构精简,便于在MATLAB环境中直接对照运行与修改。已有7603人学习,说明其在ECG入门与绘图实操场景中具有一定参考价值。读者可借此熟悉心电数据的读取流程、绘图函数调用方式,并以此为起点扩展滤波、分段、RR间期与心率变异性等分析,适合课程实验、自学练习和课题预研时快速上手。

1. 360 Hz 与 11 位量化:一份"很小"的 ECG 数据,为什么画起来处处是坑

一份 30 分钟的双通道 ECG 记录,解开来不过二十几兆,采样率 360 Hz,11 位量化,比一张手机照片还寒酸。可真正动手做过 ECG 绘图的人,大多撞过同一堵墙:数据读进来了,绘图命令一跑,出来的波形要么是一层台阶状的锯齿,要么上下颠倒,要么 R 峰标记整体偏了半拍。

问题几乎从来不在绘图函数上。真正决定这张图可不可信的,是"整数样本如何还原成毫伏""注释里的采样点编号从哪里起算""抽稀到什么程度才不骗眼睛"这三层。量纲错了,图画得再漂亮也是错的;抽样错了,窄而尖的 QRS 会被悄悄抹平。把这三层拆开看,ECG 绘图才从"调包"变成一件有依据的事。

2. 从整数到毫伏:gain、adc_zero 与 11 位量化的量纲还原

2.1 头文件是一份量纲契约,四个字段决定图上每一个刻度

这类心电数据集通常一个记录配三个文件:一个纯文本头文件、一个二进制数据文件、一个二进制注释文件。头文件短得可以一眼看完,但它承担的是"量纲契约"的角色——没有它,数据文件里那堆整数毫无意义。

典型的头文件长这样:

rec01 2 360 650000 rec01.dat 212 200 11 1024 995 0 0 MLII rec01.dat 212 200 11 1024 1011 0 0 V1

第一行是记录级的元信息:记录名、导联数、采样率、采样点数。第二行起每行描述一个通道,字段依次是文件名、打包格式、增益、ADC 分辨率、ADC 零点、首样本值、校验和、块大小、导联描述。新手最常犯的错就是只读了第一行采样率,把后面这几个字段当成可选的装饰。

它们一个都不能省。212决定你怎么拆字节,200决定一个整数代表多少毫伏,11决定有效位宽,1024决定零点在哪里。四个字段凑齐,一个整数才第一次有了物理含义。少读一个,图上纵轴就只能标"采样值",而标着"采样值"的心电图,严格说不算心电图。

这里有个血泪经验:多段记录的第一行采样点数是所有段的总和,实际数据分散在多个.dat里,段与段之间还可能存在极小的重叠。如果只按第一段去读,后面的数据会整体错位,而错位在图上表现为"波形突然被拉平一截"——非常像电极脱落,非常容易被误判成信号质量问题。

2.2 零点为什么是 1024:单极性 ADC 的偏置与动态范围

第一次看到原始数组的人几乎都会问一句:为什么所有数都在一千上下浮动,而不是围着 0 转?

因为前端是单极性采集。模拟前端的输出被抬到一个固定的直流电平上再送进 ADC,这样负半周也不会被削掉。1024 就是那个抬升量,也就是adc_zero。11 位量化的有效码值范围是 0 到 2047,以 1024 为中枢,正负各约 1024 个码值。配合增益 200 units/mV,可测范围就是大约 ±5.12 mV,而正常心电的 QRS 幅度在 1 mV 上下,余量充足。

换算公式只有一行:

physical_mV = (digital - adc_zero) / gain

这行公式里有两个容易翻车的细节。第一,gain可能是负数。某些记录的导联极性是反接的,头文件里写的就是-200。此时不要画蛇添足加abs()——除以负数会自然把极性翻回来,加了abs反而让波形上下颠倒。第二,换算必须在double上做。原始数据是整数类型,先减零点再除以增益,中间只要有一处停留在整型,小数部分就被截断了,画出来的波形会呈现明显的量化台阶,看着像"信号被压扁"。

adc_zero也不永远等于 1024。它是随记录而变的,有的记录用 0 作零点,有的用 512。凡是把零点写死在代码里的脚本,换一份数据就崩,这是最典型的"跑通了 demo、跑不通项目"。

提示:读进来第一件事,先把adc_zero、gain、adc_res、n_samp四个数打印出来。数值和你预期对不上,后面所有分析都是白费。

2.3 格式 212 的位打包,以及一次必须写对的拆包

212这三个数字的含义是"每 3 个字节装 2 个样本,每个样本 12 位"。这是为了省空间做的位打包——12 位不是整字节,硬存要浪费 4 位,打包之后每样本只占 1.5 字节。数据文件里没有分隔符、没有文件头,就是一路排下去的字节流,所以拆包逻辑错一位,整条记录就全废。

拆分规则是高位在前:第 1 个样本 = 第 1 字节左移 4 位,加上第 2 字节的高 4 位;第 2 个样本 = 第 2 字节的低 4 位左移 8 位,加上第 3 字节。

function x = unpack212(bytes, n_samp) % 格式 212:每 3 字节打包 2 个 12 位样本,高位在前 % bytes : uint8 行向量,n_samp: 该通道应有的样本总数 b = double(reshape(bytes(1:3*ceil(n_samp/2)), 3, [])); % 必须转 double 再移位 w1 = bitshift(b(1,:), 4) + bitshift(b(2,:), -4); % 样本 1:b1 全 8 位 + b2 高 4 位 w2 = bitshift(bitand(b(2,:), 15), 8) + b(3,:); % 样本 2:b2 低 4 位 + b3 全 8 位 x = zeros(1, 2*numel(w1)); x(1:2:end) = w1; x(2:2:end) = w2; x = x(1:n_samp); % 奇数样本数时丢掉最后补的半个 x(x >= 2048) = x(x >= 2048) - 4096; % 12 位补码,负数还原 end

函数里最关键的一行是double(...)。uint8上做bitshift,左移 4 位会直接溢出被截断,得到的结果不会报错,只会安静地错。另一个关键是最后的补码还原:12 位有符号数把 2048 到 4095 解释成负数。这一步和adc_res = 11并不矛盾——打包宽度是 12 位,有效位宽是 11 位,两个概念不要混。

拆完包,再做量纲还原:

raw = unpack212(fread(fid, inf, 'uint8=>uint8'), n_samp); mv = (raw(:) - adc_zero) / gain; % 一行定生死 t = (0:numel(mv)-1) / fs; % 时间轴:索引用 0 起算

注意t的构造方式。写成linspace(0, n/fs, n)是错的:linspace把首尾都当成端点,实际步长变成n/(n-1)/fs,最后累积出接近一个采样的偏差。在图上这点偏差不可见,但只要你把注释点的样本号按t去对齐,就会看到 R 峰标记一路往左漂,越到记录尾部偏得越多。索引就用(0:n-1)/fs,没有别的写法。

3. 注释文件:一条按采样点编号的状态机

3.1 两个 16 位字拼出一个事件:增量编码与 SKIP

注释文件不是"标签数组",它是增量编码的事件流。每个事件占 2 个字节,拼成一个 16 位字之后从中间劈开:低 10 位是这个事件距离上一个事件的时间差(单位是采样点),高 6 位是事件类型码。第一个事件的时间差就是从记录起点算起的绝对位置。

之所以用增量编码,是因为心拍事件是稀疏的——30 分钟里大概两千多个心拍,如果按数组逐样本存标签,99.9% 都是空的。增量编码让整个注释文件只有几 KB。

低 10 位最多表示 1023 个采样点的时间差。遇到超过 1023 的间隔(比如长时间噪声段、或者记录中段有空白),就用特殊码 59 表示"跳过":这个字的低 10 位被忽略,紧跟其后的 4 个字节组成一个 32 位整数,才是真正的时间差。同理,码 63 表示"辅助文本",后面跟 1 个字节的长度和相应数量的 ASCII 字符,读的时候要整段跳过而不是当事件处理。码 0 是文件结束标记。

function [sample, code] = read_atr(atr_path) % 逐字解析注释流:低 10 位时间差,高 6 位类型码 fid = fopen(atr_path, 'r'); b = double(fread(fid, inf, 'uint8=>uint8')); fclose(fid); i = 1; cur = 0; sample = []; code = []; while i + 1 <= numel(b) w = b(i) + 256*b(i+1); i = i + 2; dt = bitand(w, 1023); % 低 10 位:与上一事件的时间差 ty = bitshift(w, -10); % 高 6 位:类型码 if ty == 0, break; end % 0 号:文件结束 if ty == 59 % SKIP:后接 32 位大间隔,且不产生事件 dt = b(i) + 256*b(i+1) + 65536*b(i+2) + 16777216*b(i+3); i = i + 4; cur = cur + dt; continue; end if ty == 63 % AUX:1 字节长度 + 文本,整段跳过 n = b(i); i = i + 1 + n; continue; end cur = cur + dt; sample(end+1,1) = cur; % 单位:采样点,0 起算 code(end+1,1) = ty; end end

这段代码里continue的位置是重点。SKIP 会推进时间游标,但不产生事件;AUX 既不推进时间也不产生事件。把这两种情况混进事件数组,图上的 R 峰标记就会凭空多出几个,而且位置看起来"挺合理",很难察觉。

还有一个易被忽略的点:sample是从 0 起算的样本序号,MATLAB 的数组下标从 1 起算。要取波形值,必须写mv(sample + 1);要算时间,直接sample / fs,不要再加 1。这两种写法各自都对,混着用就偏一个样本。360 Hz 下一个样本是 2.8 ms,放在 25 mm/s 的走纸速度上约 0.07 mm,肉眼几乎看不出来——所以这个 bug 能活很久。

3.2 类型码到五分类:注释不是标签,而是节律状态

类型码不是简单的"正常/异常"二值。常见的码值大致是这样一组:

类型码符号含义五分类归并
1N正常心拍N
2 / 3L / R左 / 右束支阻滞N
4a房性早搏S
8A房性早搏(另一标记)S
5V室性早搏V
10E室性逸搏V
6F融合波F
13Q无法分类Q
14~信号质量变化标记非心拍,需剔除
16|孤立伪迹非心拍,需剔除

把 L、R 归进 N 类,是因为从检测角度看它们的 QRS 形态虽然宽,但节律是窦性的;把~和|单独拎出来,是因为它们根本不是心拍——如果直接当样本喂给分类器,标签噪声会非常严重。这类"看着像注释、其实不是心拍"的码值有好几种,写映射表时宁愿多写几个分支,也不要落到otherwise里一把兜住。

值得强调的是,同一份记录里,同一个心拍可能被标注多次(比如一个心拍既有形态标记又有节律标记)。工程上的做法是先按样本号排序、去重,再取每个位置上的主类型码,而不是简单地把所有事件都当成独立心拍。

3.3 用注释点反推 RR 间期与瞬时心率

拿到sample之后,RR 间期就是一次差分:

rr_ms = diff(sample) / fs * 1000; % 相邻心拍的间期,毫秒 hr = 60 * fs ./ diff(sample); % 瞬时心率,次/分 % 剔除标注质量段带来的离群值,避免心率曲线被几个异常点拉爆 ok = rr_ms > 250 & rr_ms < 3000; % 大致对应 20~240 次/分 rr_ms = rr_ms(ok); hr = hr(ok);

这里的上下界不是随便取的。生理上 RR 间期低于 250 ms 基本可以判定是误检或者标注重叠,高于 3000 ms 多半是漏检或者中间夹了噪声段。做可视化时如果不剔除,心率曲线上会出现几根冲到天上或者掉到零的尖刺,看图的人第一反应是"算法崩了",其实是标注本身的问题。

顺带说一句:RR 间期序列还有一个隐藏属性——它是非等间隔采样的。每个 RR 对应的是一个时间区间,画心率曲线时横轴应该用区间中点(sample(1:end-1)+sample(2:end))/2/fs,而不是等间隔的索引。用索引画出来的曲线在心率剧烈变化时会失真,这个细节在教科书里基本不提,但在做心率变异性分析时是硬伤。

4. 把 65 万个点画进 1200 像素:MATLAB 绘图的抽样与坐标轴

4.1 每像素 590 个样本:为什么全量 plot 会骗你的眼睛

30 分钟、360 Hz,单导联就是 648000 个采样点。一条 1100 像素宽的图,平均每个像素要承接将近 590 个样本。这意味着图上任何一个像素点的颜色,本质上都是这 590 个样本经过某种"归约"之后的结果,而这个归约过程并不由你控制。

现代图形栈在 OpenGL 渲染器下会对超长折线做抽稀,抽稀通常是均匀取点。均匀取点的问题在于:QRS 复合波只占 50 到 80 ms,也就是不到 30 个采样点,宽度不到一个像素的十分之一。均匀抽稀很可能把这 30 个点里的大部分丢掉,只留下两三个,画出来的 R 峰高度就成了随机数——同一条记录换个窗口宽度重画,R 峰高度就变一次,看起来像"信号不稳",其实是渲染在耍你。

所以关键认知是:抽稀比不是固定参数,而是随视窗变的。看全记录总览(1800 秒铺满一屏),每像素对应 1.6 秒,需要的其实是包络而不是波形,抽到 10 到 20 Hz 都够;看 10 秒局部(诊断级观察),一屏 1100 像素对应 3600 个样本,每像素 3.3 个样本,这时候根本不该抽稀,全采样率直接画才是对的。

我一般的做法是写一个按视窗决定抽稀因子的函数:视窗跨度超过 60 秒就抽到 120 Hz,否则保持原采样率。这样既不会卡,也不会在局部放大时丢失形态细节。

4.2 decimate 的抗混叠与它的相位代价

抽稀不能直接用x(1:q:end)。那是降采样不是抽取,没有抗混叠,高于新奈奎斯特频率的成分会原样折叠回来。举个具体的例子:把 360 Hz 直接隔点取到 60 Hz,50 Hz 的工频干扰会折到 10 Hz,正好落在 T 波的频段里,图上会出现一串规律的波浪——你会以为是基线漂移,去调高通滤波器的截止频率,调半天也没用,因为干扰是"造"出来的,不在原始数据里。

正确的做法是用带抗混叠滤波的抽取:

q = 3; % 360 / 3 = 120 Hz,足够看清 QRS 包络 y = decimate(x, q, 'fir'); % 先低通再抽取,抑制折叠 t = (0:numel(y)-1) / (fs/q); % 时间轴必须用抽取后的采样率重建 % 对照:错误写法,会把高频折叠进低频 y_bad = x(1:q:end);

decimate的第三个参数'fir'值得说一下。默认是用 IIR(切比雪夫)滤波器,相位非线性,QRS 这种宽带脉冲经过之后波形会畸变,峰位也会移;换成'fir'之后是线性相位,且内部以零相位方式施加滤波,峰位基本不动。不同版本的实现细节略有差异,稳妥的做法是画完之后把注释点叠上去对一眼,峰位对不上就说明有残余时延。

q必须是整数,这是decimate的硬限制。360 除以 3 得 120,除以 4 得 90,都不是问题;想抽到 100 Hz 就得用resample。这一点在设计绘图函数时要想清楚:与其追求"每像素一个点"的完美比例,不如选一个整数因子,剩下的交给图形栈。

4.3 时间轴、导联偏置与坐标轴刻度

多导联心电图的标准画法是上下排列、共用时间轴、各自独立的纵向偏置。偏置量的选取有讲究:太小会导联之间重叠,太大会浪费画布高度。经验值是取全局峰峰值的 1.2 倍。

function plot_ecg(t, X, labels, ann_t, ann_code) % X: n×k,每列一个导联,单位 mV;ann_t: 注释点时间(秒) span = max(X(:)) - min(X(:)); offset = 1.2 * span * (0:size(X,2)-1); % 每导联的纵向抬升量 figure('Color','w','Position',[100 100 1100 460]); ax = axes; hold(ax,'on'); for k = 1:size(X,2) plot(ax, t, X(:,k) + offset(k), 'Color',[0.10 0.35 0.65], 'LineWidth',0.5); end % 注释点只画在第一个导联上,避免重复标记干扰阅读 yline_top = max(X(:,1)) + offset(1); idx = ann_code == 1 | ann_code == 5; % 只挑正常拍与室早做示意 plot(ax, ann_t(idx), yline_top*ones(sum(idx),1), 'v', ... 'MarkerSize',3, 'MarkerFaceColor',[0.85 0.33 0.10], 'MarkerEdgeColor','none'); yticks(ax, offset); yticklabels(ax, labels); % 刻度落在每个导联的基线上 ylabel(ax, 'mV(相对基线)'); xlabel(ax, '时间 / s'); xlim(ax, [t(1) t(end)]); box(ax, 'off'); end

几个参数值得解释。LineWidth设 0.5 而不是默认的 0.5 以上,是因为 ECG 的波形密集,线一粗,相邻的 QRS 就糊成一片,在导出到 300 dpi 时尤其明显。yticks手动设成各导联的偏置值,是为了让纵轴刻度落在每条基线上,读图的人一眼就知道每条线的零点在哪;如果不设,MATLAB 会自动按合并后的数据范围生成刻度,写上一堆带偏置的绝对数字,毫无意义。

xlim显式固定也很重要。不固定的话,叠加注释点之后坐标范围会被撑开一点点,同一个函数画出来的两张图横轴对不齐,做前后对比时非常难受。

时间轴还有一处细节:样本i对应的时刻是(i-1)/fs而不是i/fs。差一个采样在 1800 秒的记录末端是 2.8 ms,图上完全看不出来,但只要做过任何"按时间窗截取"的操作,累积误差就会显形——比如把 30 分钟切成 30 段,每段末尾都会多带一个样本。

4.4 导出与渲染:为什么截图和 print 出来的线不一样粗

屏幕上看得好好的图,导出之后就变样,原因通常在渲染器。MATLAB 有三条路径:painters(矢量,内存开销随图元数量线性增长)、opengl(硬件加速,快,但导出的矢量文件里其实是位图)、以及自动选择。

65 万个点走painters会非常慢甚至爆内存,所以默认会落到opengl。这时候如果导出 PDF 或 EPS,得到的实际是一张嵌入的位图,放大就糊。要矢量又要快,只能先抽稀再导出——这又回到了 4.1 的结论:抽稀决策应该跟着用途走,不是跟着数据走。

位图导出用exportgraphics比老的print更可控:

exportgraphics(gcf, 'ecg_overview.png', 'Resolution', 300); % 位图,指定 dpi exportgraphics(gcf, 'ecg_overview.pdf', 'ContentType', 'vector'); % 矢量,点少时才用

Resolution只影响位图。300 dpi 下一根 0.5 pt 的线会变成约 2 个像素宽,比屏幕上看起来粗一档,这正是"截图和 print 不一样粗"的来源。要在论文里用,就把线宽和 dpi 一起调,别只调一个。

5. 避坑:ECG 绘图里最常见的五个翻车现场

5.1 波形呈规则台阶,像被量化了两次

现象:放大看波形,每一小段都是水平平台加垂直跳变,边缘呈锯齿状,尤其在低幅度的 P 波和 T 波上最明显。

原因:整型运算没有提前转double。常见写法是x = raw - adc_zero之后再除以gain,但raw还是整型,减完仍然是整型,除以 200 时小数被截断,等效于把分辨率从 11 位砍到只剩几档。另一种原因是读取时用了'uint16=>int16'之类不匹配的精度说明符,字节序一错,数值整体乱掉。

解决:读二进制时统一用'uint8=>uint8',拆包第一步就double(),之后全程浮点。改完再画一遍对比,台阶会立刻消失。

5.2 波形上下颠倒,但幅度完全正确

现象:QRS 主波方向反了,正常应该向上的 R 波变成向下的深 S,幅度、节律都对。

原因:头文件里的gain是负数,代码里加了abs()。反接导联在这类数据里不罕见,-200就是明确的极性声明。加abs等于把声明抹掉,波形自然翻转。另一种成因是把导联顺序读反了——两个通道的描述行搞混,MLII 画成了 V1。

解决:删掉所有对gain的取绝对值操作,直接相除。同时在图上标出导联名,用描述字段而不是列序号来定位。

5.3 注释点整体偏移,越到后面偏得越多

现象:R 峰标记和波形峰顶对不齐,开头偏一点,记录末尾偏出明显距离。

原因:时间轴用linspace(0, n/fs, n)构造,步长不是1/fs,累积误差随索引线性增长;或者注释点的 0 起算样本号直接当 MATLAB 索引用了mv(sample)而不是mv(sample+1)。

解决:时间轴一律写(0:n-1)/fs;索引访问统一+1,时间换算不+1。改完拿一个小窗口(比如 5 秒)放大核对,能对齐就说明整条都对。

5.4 滤波之后 QRS 变矮、位置漂移

现象:做了带通之后,R 峰幅度掉了两三成,峰位相对注释点右移了几十毫秒,窄 QRS 尤其明显。

原因:用了因果filter,群延时没补偿;或者高通截止频率设得太高(比如 5 Hz),把 QRS 的低频成分削掉了。QRS 的主能量集中在 10 到 25 Hz,但它的基底部分在 1 到 5 Hz 有相当贡献,截止频率抬高会直接削顶。

解决:离线分析用零相位滤波(filtfilt),高通截止压到 0.5 Hz,低通留到 40 Hz 以上。做完滤波一定叠一次原始波形做目视比较,只看数值指标不够。

5.5 中值滤波去基线,把 ST 段一起抹平了

现象:基线漂移确实没了,但 ST 段的抬高/压低也跟着消失,波形看起来"过于干净",反而失真。

原因:中值滤波的窗口宽度选得太大。去基线用的中值滤波,窗口要明显大于 QRS 宽度(约 200 ms)又明显小于基线漂移的周期(通常几秒)。窗口取到一两秒,就会把 ST 段这种持续几百毫秒的缓慢变化当成漂移一起减掉,而 ST 段的形态恰恰是诊断心肌缺血的关键。

解决:两级中值滤波,第一级窗口约 200 ms 去掉 QRS,第二级约 600 ms 去掉 T 波,相减得到基线,再原信号减去基线。窗口不要超过 0.8 秒,且处理前后要做 ST 段的形态对比。

6. 设计取舍:360 Hz、11 位与"先滤波再检测"的边界

这份数据的参数选择,本身就是一组很清楚的工程取舍,理解了它,才知道哪些分析在这个数据上做得了、哪些做不了。

360 Hz 采样意味着 180 Hz 的奈奎斯特带宽。QRS 检测需要的主要信息在 40 Hz 以下,这个采样率绰绰有余;但它同时也意味着,任何依赖 100 Hz 以上成分的分析——高频 QRS、晚电位、心室晚电位的微伏级碎裂——在这个数据上都没有足够的时间分辨率。11 位量化配合 200 units/mV,一个 LSB 约 5 µV。对毫伏级的 QRS 来说精度富余,对微伏级的晚电位来说则完全不够。这不是数据的缺陷,而是它的定位:它服务的是节律与形态分析,不是高分辨率电生理。

第二个取舍更微妙:滤波到底放在检测之前还是之后。

维度因果滤波(filter)零相位滤波(filtfilt)
相位有群延时,QRS 整体后移无相移,峰位与原始对齐
幅频响应单次施加,过渡带较缓响应平方,过渡带更陡、通带更平
实时性可流式处理必须拿到整段数据
边界效应起点有短瞬态两端各有一段镜像引入的伪迹

看清楚这张表就会明白,为什么很多脚本在离线分析里用filtfilt,一搬到在线检测就崩:零相位滤波本质是"先正着滤一遍再倒着滤一遍",天然要求你提前知道整段数据。真正要上实时,只能退回因果滤波,然后用群延时补偿去对齐——而群延时在滤波器阶数变化时还会变,补偿量得跟着算。

还有一个经常被忽略的取舍:滤波会改变 QRS 的形态,而形态恰恰是室早和室上早的分界依据。带通下限设到 5 Hz,宽 QRS 的基底被削掉,本来 140 ms 的宽度测出来只剩 110 ms,室早可能就被判成了正常。所以做形态学分类时,滤波器要么足够保守(0.5 到 40 Hz),要么干脆在原始波形上做,滤波只用于检测不用于测量。

我最早做这类图的时候,习惯是先跑一遍滤波、再画图,图上一片干净,心里很踏实。后来有一次把原始波形和滤波后的波形叠在同一张图上,才发现 ST 段被悄悄抬高了 0.1 mV——那点变化在单看图时完全察觉不到。从那以后,我每画一张 ECG 图,都强制先把gain、adc_zero、n_samp打印出来核一遍,再把原始波形和滤波结果叠一次做目视对比,最后才去看检测结果。这三步走完,图才敢往报告里放。

资料里那套解析与绘图脚本,基本就是按这条链路拆开的:头文件解析、位打包拆解、注释流还原、按视窗抽稀、多导联排版,每一层都能单独拎出来改。对着自己的记录跑一遍,比读十遍格式说明管用得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询