广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何在Android端准确测试ECG心电图分析功能?

做Android端ECG分析时,最容易把采集、解析、滤波、R波检测和波形绘制全部塞进蓝牙回调里。这样写出来的功能在真机上偶尔能跑通,但测试完全无从下手。要把心电图分析做到可验证、可回归,第一步一定是拆分数据链路,让每一层都只依赖明确的输入输出。

如何在Android端准确测试ECG心电图分析功能?

一、先拆开ECG数据处理链路

Android端的心电数据通常来自BLE设备,回调方法里拿到的是字节数组。这些字节并不直接等于采样值,设备协议可能是每包固定长度,也可能出现粘包或半包。解析层需要先把字节流还原成整数采样序列,同时处理好符号位和采样率。比如常见的协议是每包以0xAA 0x55开头,后面跟包序号、通道数和若干16位有符号采样值。解析时不能假设一次回调一定包含完整包,必须内部维护缓冲区。

数据解析完成后,不能直接把原始值画到屏幕上。ECG原始信号里包含基线漂移、50Hz工频干扰以及运动伪影。滤波层需要做高通、低通和陷波处理,至少把有效频段限制在0.5Hz到40Hz之间。滤波后的信号仍然不适合直接计算心率,因为QRS波群的斜率、幅度和宽度变化很大,需要专门的R波检测器来定位心跳。最后才是RR间期统计和界面绘制。把这条链路拆开,测试就能针对每一层独立构造输入、断言输出。

public class EcgStreamParser {
    private final ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    public int[] append(byte[] chunk) {
        buffer.write(chunk, 0, chunk.length);
        byte[] data = buffer.toByteArray();
        int[] samples = new int[0];
        int index = 0;
        while (index + 5 <= data.length) {
            if ((data[index] & 0xFF) != 0xAA || (data[index + 1] & 0xFF) != 0x55) {
                index++;
                continue;
            }
            int length = data[index + 2] & 0xFF;
            if (index + 5 + length * 2 > data.length) {
                break;
            }
            samples = new int[length];
            int pos = index + 3;
            for (int i = 0; i < length; i++) {
                int low = data[pos++] & 0xFF;
                int high = data[pos++] & 0xFF;
                samples[i] = (short) ((high << 8) | low);
            }
            index += 5 + length * 2;
        }
        byte[] remaining = new byte[data.length - index];
        System.arraycopy(data, index, remaining, 0, remaining.length);
        buffer.reset();
        buffer.write(remaining, 0, remaining.length);
        return samples;
    }
}

这个解析器不依赖任何Android组件,可以在JVM单元测试里直接跑。测试时重点验证三件事:单包完整数据能否正确还原;粘包数据能否一次解析多帧;半包数据是否会在下次追加后继续解析。只有解析层稳定了,后续滤波和检测才不会因为输入错位而出现伪差。

二、用模拟信号构造可重复测试

测试ECG分析算法时,直接拿真人信号有两个问题:一是每次心跳形态不完全一致,无法精确断言;二是测试环境不稳定,结果不可重复。更合适的做法是用程序生成模拟心电信号,把P波、QRS波群和T波按模板叠加到正弦基线上,再加入已知幅度和频率的工频干扰。这样输入信号完全确定,输出的心率值和R波位置也就可以精确预测。

模拟信号至少需要覆盖几种场景:正常窦性心率60次每分钟、心动过速120次每分钟、心动过缓45次每分钟,以及包含偶发早搏的波形。生成时需要注意采样率,Android端常见设备采样率有250Hz、360Hz和500Hz。测试用例如果只跑250Hz,可能掩盖采样率转换错误,因此模拟数据应支持按不同采样率生成。

public class SyntheticEcg {
    public static float[] generate(float heartRate, int sampleRate, int durationSeconds) {
        int total = sampleRate * durationSeconds;
        float[] ecg = new float[total];
        float beatPeriod = sampleRate * 60.0f / heartRate;
        for (int i = 0; i < total; i++) {
            float t = i / (float) sampleRate;
            float rr = (i % (int) beatPeriod) / beatPeriod;
            float qrs = 0.0f;
            if (rr < 0.06f) {
                qrs = 1.8f * (float) Math.sin(rr / 0.06f * Math.PI);
            } else if (rr < 0.12f) {
                qrs = 1.2f * (float) Math.sin((rr - 0.06f) / 0.06f * Math.PI);
            }
            float tWave = 0.0f;
            if (rr > 0.28f && rr < 0.38f) {
                tWave = 0.35f * (float) Math.sin((rr - 0.28f) / 0.10f * Math.PI);
            }
            ecg[i] = qrs + tWave + 0.05f * (float) Math.sin(2 * Math.PI * 0.8f * t);
        }
        return ecg;
    }
}

这个生成器只能作为测试起点,真实心电波形远比正弦模板复杂。但它的好处是形态稳定、周期明确,适合做心率计算的基线回归。如果算法能正确识别这种理想波形,再逐步加入噪声、幅度变化和基线漂移,就能形成分层测试用例。测试数据也可以保存为文本文件,在CI环境中重复加载,避免每次生成引入随机差异。

三、滤波与R波检测算法的单元测试

心电信号经过Bluetooth传输后,往往会混入高频毛刺和低频漂移。滤波层的测试不能只看波形是否平滑,还要检查滤波器是否引入了群延迟。如果高通滤波器阶数过高,QRS波群会被明显展宽,导致R波定位偏移。对滤波器的单元测试应当输入一段已知频率成分的信号,然后检查输出幅度是否在期望频带内被抑制或保留。

R波检测算法是整条链路中最容易出错的部分。简单的固定阈值方法对幅度变化敏感,运动伪影稍大就会漏检或误检。更可靠的方案是使用滑动窗口自适应阈值,同时结合斜率变化和不应期限制。检测结果的测试需要同时断言TP、FP和FN三个指标,而不是只比较最终心率值。因为两个不同的错误检测可能产生相同的心率,但一个多检一个漏检,诊断含义完全不同。

public class RPeakDetector {
    private final int sampleRate;
    private final float[] window;
    private int index = 0;
    private float threshold = 0.5f;
    private int refractory = 0;
    public RPeakDetector(int sampleRate) {
        this.sampleRate = sampleRate;
        this.window = new float[sampleRate * 2];
    }
    public boolean detect(float sample) {
        window[index % window.length] = sample;
        int current = index;
        index++;
        if (refractory > 0) {
            refractory--;
            return false;
        }
        float sum = 0.0f;
        int count = 0;
        for (int i = 1; i <= 5 && current - i >= 0; i++) {
            sum += window[(current - i) % window.length];
            count++;
        }
        float avg = count > 0 ? sum / count : sample;
        if (sample > threshold && sample > avg * 1.4f) {
            threshold = Math.max(0.3f, sample * 0.6f);
            refractory = sampleRate / 4;
            return true;
        }
        return false;
    }
}

测试R波检测器时,用模拟信号生成精确周期,逐个样本喂给检测器,记录每次返回true的位置。然后计算相邻R波间隔,除以采样率得到RR间期,再换算心率。断言时不要只判断心率是否等于60,而应检查所有检测到的R波数量是否在预期误差范围内,同时确认没有在T波位置产生额外检测。

四、性能测试与设备兼容性验证

Android设备型号繁多,蓝牙芯片、系统版本和屏幕刷新率都会影响心电分析表现。性能测试要重点观察长时间运行后的内存占用和CPU使用情况。如果每收到一个数据包就创建新的数组、频繁触发UI刷新,低端设备上可能两小时内就出现卡顿或掉帧。对于实时心电图,绘制层应使用SurfaceView或自定义View配合硬件加速,避免在UI线程做复杂计算。

兼容性测试可以借助AndroidJUnitRunner在真机矩阵上执行,也可以把算法部分抽成纯Java模块在电脑端快速回归。测试用例应包含不同采样率、不同字节序、不同BLE MTU分包的组合。尤其要注意部分国产ROM对BLE回调节流更激进,数据可能一批到达很多包,解析器必须能承受突发负载而不丢包。

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>

最后还要测试算法对采样率变化的适应能力。有些设备标称250Hz采样率,实际在蓝牙拥塞时会降到200Hz以下。如果心率计算直接假设固定采样率,计算出的RR间期会系统性偏大。正确的做法是每次统计时根据实际时间戳修正,或者在数据包头中读取设备真实的采样计数。把这些边界条件纳入测试,Android端ECG分析才能真正达到医疗参考级别。

广告位 · 文章底部(后台-模块管理替换此区域)