nexatech-ltd/meditech-cardiomera-decoder
GitHub: nexatech-ltd/meditech-cardiomera-decoder
逆向工程并开源了 Meditech CardioMera (FC1) Holter 动态心电记录仪的专有文件格式规范,提供跨平台 Python 解码器将原始 ECG 数据导出为标准 EDF+ 格式。
Stars: 0 | Forks: 0
# meditech-cardiomera-decoder
[](LICENSE)
[](https://www.python.org/)
[](https://github.com/nexatech-ltd/meditech-cardiomera-decoder/actions/workflows/ci.yml)
一个开源的解码器和**逆向工程格式规范**,用于处理由 **Meditech CardioMera (FC1)** 动态记录仪(Meditech Kft, 布达佩斯)生成的动态心电图(Holter ECG)记录。它读取原始 SD 卡文件 `ECG.DAT` 和 `FC1_PRG.DAT`,重建多通道 ECG,并导出可在任何标准 ECG 查看器(EDFbrowser、WFDB 工具等)中打开的 **EDF+** 文件。

*重建的 3 导联 ECG。此样本由**合成**数据(`tests/synthetic.py`)生成——本仓库的任何位置均未包含患者数据。*
厂商自带的软件(**CardioVisions**)仅支持 Windows,并且**没有关于卡上文件格式的公开文档**。本项目记录了该格式,并提供了一个跨平台的 Python 读取器,从而使数据不再被锁定在单一应用程序中。
## ⚠️ 免责声明
**医疗。**本软件**不是医疗器械**,且不提供任何**诊断**。可选的心律失常脚本仅提供粗略的、基于心律的评估(它*不*分析 QRS 形态,也无法区分室性与室上性异位搏动或伪影)。在进行任何临床决策时,请始终依靠临床医生和经过认证的 Holter 分析软件。
**隐私 (PHI)。**`FC1_PRG.DAT` 以**明文形式存储患者姓名和 ID**,而 `ECG.DAT` 是患者的心电图。请将两者均视为受保护的健康信息。提供的 `.gitignore` 会将所有 `*.DAT`、`*.edf`、`*.npz` 以及图像/PDF 导出文件排除在版本库之外。**请勿提交真实记录。**本 README 中的所有示例均已脱敏或为合成数据。
## 功能
- 完整性报告(已记录块数与预分配块数对比,损坏块检查)。
- 从存储的差值中无损重建积分信号。
- 使用可插拔策略处理基线漂移(默认为漏泄积分器;记录了 Butterworth 高通/移动平均替代方案)。
- 以真实**微伏**为单位导出 EDF+(使用制造商提供的 4 µV/count 增益)。
- 可选的基于心率的异位搏动负荷评估,包含每小时的细分数据。
## 安装
```
python3 -m pip install -r requirements.txt
# numpy, scipy, pyEDFlib, matplotlib
```
## 用法
将 `ECG.DAT` 和 `FC1_PRG.DAT` 放入工作目录,然后执行:
```
# 解码 + 导出 EDF+ (cardiomera.edf) 并附带完整性报告
python3 meditech_decode.py
# 基于节律的异位负荷粗略估计(写入 arrhythmia_events.npz)
python3 meditech_arrhythmia.py
```
这两个工具都支持 CLI 标志,因此它们适用于任何 CardioMera 记录,而不仅仅是默认的 3 通道 / 300 Hz 布局:
```
python3 meditech_decode.py path/to/ECG.DAT path/to/FC1_PRG.DAT \
-o out.edf --channels 3 --rate 300 --gain 4.0 --baseline butter
python3 meditech_decode.py --report-only # metadata + integrity only
python3 meditech_arrhythmia.py path/to/ECG.DAT --rate 300 --detect-channel 2
python3 meditech_decode.py -h # full option list
```
运行测试套件(使用合成数据——无需真实记录):
```
python3 tests/test_smoke.py # or: pytest -q
```
编程式用法:
```
import meditech_decode as m
meta = m.parse_prg("FC1_PRG.DAT") # patient/device/timestamps
integ = m.analyze_integrity("ECG.DAT") # block accounting
sig = m.reconstruct("ECG.DAT") # (samples, 3) int32 ADC counts
sig = m.correct_baseline(sig) # remove slow drift for int16/EDF
m.export_edf(sig, meta, "cardiomera.edf") # EDF+ in microvolts
```
## 文件格式规范
以下所有内容均来自对真实记录的检查,并通过重建干净的 ECG 得到证实;请将其视为社区规范,而非厂商文档。所有多字节整数均为**小端序**。
### 1. 卡片上的文件
已编程的 CardioMera 卡片包含(至少)两个文件,这些文件列在 `FC1_PRG.DAT` 内部嵌入的 FAT 样式目录中:
| 文件 | 大小(示例) | 用途 |
|---------------|--------------------|------------------------------------------|
| `FC1_PRG.DAT` | 2048 字节 | 编程/配置 + 患者数据 |
| `ECG.DAT` | N × 1024 字节 | 原始多通道 ECG |
`FC1` 前缀是 CardioMera 的型号代码(根据 Meditech 设备手册),因此 `FC1_PRG.DAT` = "FC1 编程文件"。
### 2. `ECG.DAT` —— 信号容器
`ECG.DAT` 是一个由 **1024 字节块**组成的扁平数组。该文件在开始记录时被**预分配**为固定数量的块;当记录仪断电时,剩余的块将保持**全零**。因此:
```
file size = TOTAL_BLOCKS * 1024
recorded = blocks [0 .. last non-zero block]
unused tail= all-zero blocks (NOT lost data — never written)
```
**请勿**将全零的尾部视为损坏;这是正常的未使用缓冲区。
#### 2.1 块布局 (1024 字节)
```
offset size field
------ ---- ---------------------------------------------------------------
0 1 tag block type / flags: 0x1d, 0x19 or 0x0d
(does NOT change the data layout — data always @13)
1 1 0x00 constant
2 3 counter 24-bit LE, increments by +20 per block
(a device clock/index; NOT a sample count)
5 8 reserved housekeeping; typically 01 02 00 .. (varies slightly)
13 1011 payload 3 interleaved channels of int8 deltas:
c0,c1,c2, c0,c1,c2, ... (337 samples per channel)
```
关键事实及其原因:
- **每个块的头部均为 13 字节。**无论 `tag` 值如何,数据都从第 13 字节开始。这一点已得到经验证明:对于 `0x1d`、`0x19` 和 `0x0d` 块,使用偏移量 13 都能产生干净的 ECG,并且 `1024 − 13 = 1011 = 3 × 337` 可以被 3 个通道整除,因此交错相位在每个块边界处都能得到精确保留。
- **`tag` 字节是一个类型/标志,而不是头部长度。**(作为数字时,`0x1d`=29,`0x19`=25,`0x0d`=13,但在所有情况下头部均为 13。)其确切含义尚未完全解码;它不会影响样本提取。
- **计数器每块递增 +20**,是一个设备时钟/索引——它**不**等于样本数(每个块容纳 337 个样本/通道)。少数块在数据段边界处显示出不同的步长;这些是无害的,并且会相互抵消。
#### 2.2 样本编码 —— int8 差值
每个有效载荷字节都是一个**有符号 8 位差值** (`int8`)。重建后的信号是每个通道的累积总和:
```
deltas = payload.reshape(-1, 3).astype(int8) # (337*nblocks, 3)
signal = cumsum(deltas, axis=0) # ADC counts
```
这一点通过直方图得到了证实,它是一个以 0 为中心的干净拉普拉斯分布(值 0 最常见,然后是 ±1、±2……对称分布)——这是差分/增量编码的标志,而不是原始样本。(我们测试并否决了原始 `int16` 的解释:它噪声很大并且溢出了范围。)
**基线漂移。**全局累积总和虽然是精确的,但会累积一个微小的直流偏置(约 +36 计数/块),因此在多日的记录中,它会漂移到数百万,并超过 EDF 要求的 `int16` 范围。诊断内容存在于交流分量中,并且被完全保留;可以使用任何标准的 ECG 高通滤波器去除缓慢的漂移。`correct_baseline()` 默认实现了一个漏泄积分器(`y[n]=α·y[n−1]+Δ`,α≈0.999,≈0.05 Hz);0.05 Hz 的 Butterworth 高通能提供最佳的 ST/T 波保真度。
#### 2.3 计算示例 —— 第一个块
真实 `ECG.DAT` 的头部及第一个有效载荷的开头(有效载荷字节仅为 ECG 差值,不包含识别信息):
```
00000000: 1d 00 be 14 00 00 01 3f 02 00 00 00 45 1e 12 00 dd 1f f7 03
^^ ^^ ^^^^^^^^ ^^ ^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^ ...
| | | | | └ payload: int8 deltas
| | | | └ reserved / housekeeping (bytes 5..12)
| | | └ counter byte 3 (high)
| | └ counter = 0x0014be = 5310 (LE, bytes 2..4)
| └ 0x00
└ tag = 0x1d
```
解码:字节 `13..1023` → `int8` → 重塑为 `(337, 3)` → `cumsum` → 3 个通道中每个通道的前 337 个样本。
### 3. `FC1_PRG.DAT` —— 配置和患者数据
固定的 2048 字节结构。值得注意的字段(偏移量来自真实的、**已脱敏**的示例;PHI 字段的字节值被替换为 `JOHN DOE` / `0` / `X`):
| 偏移量 | 类型 / 长度 | 字段 |
|---------|-----------------------|--------------------------------------------------|
| `0x100` | 打包日期 (7 字节) | 年 `u16`,然后是月、日、时、分、秒 (`u8`) |
| `0x201` | 长度前缀字符串 | 软件名称,例如 `CardioClip01`(长度字节 `0x0c`) |
| `0x20d` | 打包的类日期 | 次要时间戳块 |
| `0x243` | 长度前缀字符串 | **患者姓名**(前面有长度字节,例如 `0x0d`) |
| `0x28b` | 长度前缀字符串 | **患者姓名**(第二份副本) |
| `0x2af` | 长度前缀字符串 | **患者 ID**(长度字节 `0x09`) |
| `0x2cf` | `double` (TDateTime) | Delphi 时间戳(自 1899-12-30 以来的天数) |
| `0x2d7` | `double` (TDateTime) | Delphi 时间戳 |
| `0x400` | 8.3 目录条目 | FAT 样式目录:`CARDIOMERA`, `FC1_PRG DAT`, `ECG DAT` |
| `0x610` | ASCII 字符串 | **设备序列号**,例如 `2019FCxxxxxxx` |
| ~`0x158`| ASCII | `ATDT` 调制解调器拨号字符串(遗留遥测) |
**时间戳。**存在两种编码:
- 位于 `0x100` 的**打包**日期:`EA 07 07 01 0A 14 26` → 年 `0x07EA`=2026,月 `07`,日 `01`,时 `0x0A`=10,分 `0x14`=20,秒 `0x26`=38 → `2026-07-01 10:20:38`。
- **Delphi `TDateTime`** 双精度浮点数(自 1899-12-30 以来的天数)。它们位于 `0x2cf`/`0x2d7` 附近,提供记录/设置时间。请注意,这些是在**编程时**写入的,捕获的是开始时间;这里**没有明确的结束时间戳**——根据解码出的长度计算结束时间(`recorded_blocks × 337 / sample_rate`)。
关键区域的脱敏转储:
```
00000100: ea 07 07 01 0a 14 26 00 ... packed date 2026-07-01 10:20:38
00000200: 0c 43 61 72 64 69 6f 43 6c 69 70 30 31 ea 07 07 .CardioClip01...
00000240: 00 00 0d 4a 4f 48 4e 20 44 4f 45 00 ... patient name (REDACTED)
000002a0: ... 09 30 30 30 30 30 30 30 30 30 ... patient ID (REDACTED)
000002c0: 00 .. 3a 58 26 ae 8d 8f e6 40 70 e7 38 9b 8d 8f e6 40 two TDateTime doubles
00000400: 43 41 52 44 49 4f 4d 45 52 41 20 ... CARDIOMERA (FAT-style directory)
00000420: 46 43 31 5f 50 52 47 20 44 41 54 20 ... FC1_PRG DAT
00000440: 45 43 47 20 20 20 20 20 44 41 54 20 ... ECG DAT
00000610: 32 30 31 39 46 43 58 58 58 58 58 58 58 2019FCXXXXXXX (serial, REDACTED)
```
### 4. 采集参数与校准
已由 Meditech 设备手册确认(参见[参考资料](#references)):
| 参数 | 值 |
|------------------|--------------------------------------------------------------|
| ECG 通道 | 最多 **3 个双极**(或 5 个单极);本记录:**3** |
| A/D 分辨率 | **12 位** |
| 采样(采集) | 1200 Hz 或 600 Hz |
| 存储速率 | **600 / 300 / 150 Hz**(可选);本记录:**300 Hz** |
| 动态范围 | 16 mV 峰谷值 |
| 灵敏度 | **4 µV** → **≈ 4 µV / ADC 计数** (16 mV / 4096 = 2¹²) |
| 介质 / 持续时间 | SD/MMC 卡,最长 96 小时 |
**微伏校准。**`physical_µV ≈ ADC_count × 4 µV`。解码器使用 `ADC_GAIN_UV = 4.0` 并以 µV 为单位写入 EDF。*注意事项:*手册给出的是硬件最低有效位 (LSB);`ECG.DAT` 存储的究竟是原始计数(还是缩放后的值),最好能通过记录中存在的已知 1 mV 校准脉冲来进行验证。
**特定记录的活动通道数和存储速率**是可选的,并且几乎可以肯定被编码在 `FC1_PRG.DAT` 中的某个位置;确切的字段尚未解码,因此本工具使用的是经验确定的值(3 通道,300 Hz)。300 Hz 这个数值在生理学上得到了证实(只有当频率为 300 Hz 时,65–80 bpm 的静息心率才合理;600 Hz 意味着约 150 bpm,150 Hz 意味着约 38 bpm)。
### 5. 已知的未知项 / 寻求帮助
- 块 `tag` 字节(`0x1d` / `0x19` / `0x0d`)的确切语义。
- 每块 +20 计数器的单位含义以及保留的头部字节 5–12 的含义。
- `FC1_PRG.DAT` 内部通道数和存储速率字段的位置。
- 确认样本是否为原始计数(需对照已知脉冲进行校准)。
- 其他 CardioMera 型号代码(`(X)plore`、`CardiUp`)是否共享此块布局。
## 验证
该格式经过了端到端的验证,而不仅仅是断言:
- **差分编码**通过拉普拉斯字节直方图得到证实 (0 ≫ ±1 ≫ ±2 …)。
- **3 通道交错**通过平滑度指标得到证实:重建为 3 通道可产生平滑的各通道波形;而 1/2/4 通道则不能。
- **生理 ECG** 成功恢复——具有清晰的 QRS 波群和可见的 P 波和 T 波,并且在 300 Hz 下静息心率为 65–80 bpm。
- **完整性**:在样本记录中,每个已记录的块都有有效的标签,并且已记录区域内没有出现零值/丢失的块。
## 转换 / 查看
导出的 `cardiomera.edf` 是标准的 **EDF+** 文件,可在以下软件中打开:
- [EDFbrowser](https://www.teuniz.net/edfbrowser/)(跨平台查看器)
- [WFDB / wfdb-python](https://wfdb.readthedocs.io/)(`edf2mit` 等)
- 大多数研究级 ECG 工具包(NeuroKit2 等)
## 参考资料
- Meditech CardioMera —— 产品页面和规格:
- Meditech 设备手册(型号代码包括 **`FC1 for CardioMera`**;**12 位**,**1200/600 Hz** 采集,**600/300/150 Hz** 存储,**16 mV p-v / 4 µV** 灵敏度,最多 3 个双极/5 个单极通道):
- CardioVisions 软件 (Windows) 和卡处理:
·
## 许可证
[MIT](LICENSE)。不附属于 Meditech Kft,也未获得其认可。所有商标均归其各自所有者所有。
标签:ECG心电数据, Python, 医疗数据解析, 文件格式逆向, 无后门, 格式转换, 逆向工具