EEG、ECG 与睡眠分期的时间序列标注
以 30 秒为一帧进行睡眠分期,或在 EDF+ 和 WFDB 记录上标记区间和事件,再把每位标注者的标签导出为 BIDS events.tsv 文件。
v2.13.0 新增
time_series 显示类型在浏览器中绘制多通道记录,interval_label 方案在记录上记录带标签的区间和点事件。睡眠研究者把这两者配合起来做睡眠分期。睡眠分期为多导睡眠图的每个 30 秒帧(epoch)判定一个 AASM 分期,即 W、N1、N2、N3 或 REM。脑电(EEG)研究者标记伪迹和癫痫发作,心脏病学研究者审阅来自 PhysioNet 的心电图(ECG)记录中的心搏和节律,可穿戴设备研究者在加速度计数据中标注活动片段,天文学家在光变曲线中标记凌星或耀发。关于一段记录的其他问题,例如质量评分或备注,放在图旁边的普通方案中。
欧洲数据格式(European Data Format,EDF)是交换和存储睡眠记录、EEG 记录等医学时间序列的标准格式,EDF+ 在此基础上增加了注释和带间断的记录。WFDB 是 PhysioNet 的记录格式。一条 WFDB 记录由一个 .hea 头文件、若干 .dat 信号文件以及可选的注释文件(例如 .atr)组成。
- 格式: EDF 和 EDF+(连续与不连续)、BDF 和 BDF+、WFDB 记录、带时间列的 CSV 或 TSV、Parquet,以及一个小型内联 JSON 对象。EDF 和 BDF 文件通过内存映射读取,不需要额外的库。
- 已有标签: EDF+ 注释、WFDB
.atr文件和脑成像数据结构(Brain Imaging Data Structure,BIDS)的events.tsv会作为预标注载入。标注者可以保留、编辑或丢弃这些预标注。 - 长记录: 浏览器从不下载整段记录。对于屏幕上显示的时间范围,服务器为每个通道的每个像素最多发送两个值(原始采样值,或每个像素内的最小值和最大值),因此 8 小时的记录打开起来和 30 秒的记录差不多快。
- 离线: 信号文件由 Potato 服务器从媒体目录或任务目录中读取。URL、绝对路径和
..越界路径一律拒绝。
WFDB 记录和 Parquet 文件需要 signals 可选依赖(wfdb 和 pyarrow):
pip install 'potato-annotation[signals]'配置
下面的配置在一份四通道多导睡眠图上以 30 秒为一帧进行睡眠分期。第二个方案让标注者在同一张图上点击标记觉醒:
annotation_task_name: "Sleep staging"
task_dir: .
media_directory: media
output_annotation_dir: annotation_output
data_files: [data/psg.json]
item_properties: {id_key: id, text_key: note}
instance_display:
fields:
- key: signal # item field holding the EDF file name
type: time_series
label: "PSG"
display_options:
window: 30
channels:
- {name: "EEG Fpz-Cz", range: [-120, 120]}
- {name: "EOG", range: [-150, 150]}
- "EMG chin"
- "Resp"
user_config:
allow_all_users: true
annotation_schemes:
- annotation_type: interval_label
name: stage
description: "Sleep stage of each 30-second epoch"
signal_field: signal
mode: epoch
epoch_length: 30
require_all_epochs: true # counted only while the scheme is required
label_requirement: {required: true}
labels:
- {name: W, key_value: "1"}
- {name: N1, key_value: "2"}
- {name: N2, key_value: "3"}
- {name: N3, key_value: "4"}
- {name: REM, key_value: "5"}
preannotation_labels: # EDF+ hypnogram names -> scheme labels
"Sleep stage W": W
"Sleep stage 1": N1
"Sleep stage 2": N2
"Sleep stage 3": N3
"Sleep stage R": REM
"Arousal": null
- annotation_type: interval_label
name: arousals
description: "Arousals (click on the plot)"
signal_field: signal
mode: free
pre_annotations: false
labels:
- {name: arousal, key_value: "a", kind: event}channels 列表决定显示哪些通道行以及它们的顺序,range 以通道自身的单位固定某一行的幅度刻度。固定的范围让 EEG 通道行在每一帧中保持相同的刻度。preannotation_labels 映射把 EDF+ 睡眠图(hypnogram)中的分期名称改为方案的标签,并丢弃文件中的觉醒注释。第二个方案关闭了 pre_annotations,标注者在这个方案中手动标记觉醒。require_all_epochs 与 label_requirement 同时设置时,每一帧都有分期之前,Next 一直不可用。每个方案的 signal_field 必须等于某个 time_series 字段的 key,两者不一致时服务器拒绝启动。
每个条目给出相对于 media_directory 的信号文件路径:
{"id": "night-01", "note": "Night 1", "signal": "psg/night-01.edf"}用于 EEG 审阅的自由模式
EEG 审阅使用自由模式。在自由模式下,标注者在图上拖动来标注一个区间,点击来标记一个点事件。下面这段部分配置替换上面配置中的 annotation_schemes:
annotation_schemes:
- annotation_type: interval_label
name: eeg_events
description: "Mark artifacts and seizures, and click each spike"
signal_field: signal
mode: free
channel_scope: either # all channels, or the lane the drag starts on
labels:
- {name: artifact, key_value: "1"}
- {name: seizure, key_value: "2"}
- {name: spike, key_value: "3", kind: event}设置 channel_scope: either 后,图上方会出现一个开关,用来选择新区间作用于所有通道还是某一个通道行。电极伪迹可能只出现在一个通道行上,而全面性癫痫发作横跨所有通道。若要在已分期的帧内标记呼吸暂停或觉醒,保留分帧方案,再添加一个 signal_field 相同的自由模式方案,就像上面的睡眠配置对觉醒所做的那样。两个方案要使用不同的标签按键。
信号文件
条目中所写文件的扩展名决定使用哪个读取器:
| 扩展名 | 格式 | 依赖 |
|---|---|---|
.edf | EDF、EDF+(连续与不连续) | 无 |
.bdf | BioSemi BDF、BDF+(24 位) | 无 |
.hea | WFDB 记录(头文件,加上头文件列出的 .dat 文件) | signals 可选依赖 |
.csv、.tsv | 一个时间列,加上每个通道一列 | 无 |
.parquet、.pq | 相同的布局,以 Parquet 存储 | signals 可选依赖 |
.json | 下文的内联对象,存为文件(最大 50 MB) | 无 |
- EDF+。 每条有时长的注释成为一个作用于所有通道的预标注区间,每条没有时长的注释成为一个点事件。在 EDF+D 文件中,每个数据记录放在其计时注释给出的起始时刻上,记录之间的空隙画成线条的断开。如果文件比头部声明的短,就读到最后一个完整的记录为止,并在图下方显示警告。
- WFDB。 用记录的
.hea文件来指定记录,信号文件放在同一目录下。<record>.atr中的心搏注释成为以其符号(N、V等)为标签的点事件,(AFIB之类的节律注释成为AFIB区间,一直延续到下一次节律变化。采样值按窗口逐段读取,长的 Holter 记录从不整体载入。读取器不支持多段记录,多段记录需要先拼接再载入。 - CSV 和 Parquet。 设置了
time_column时以它为时间列;否则取第一个名为time、t、timestamp、seconds、sec、time_s、datetime、mjd或jd的列;再否则取第一列。其余每个数值列都是一个通道,表头中的单位(ax (g)、ay [m/s^2])成为该通道的单位。如果一个序列的所有时间步都落在中位时间步的 0.1% 以内,就按等间隔采样处理,否则保留每个采样点自己的时间。CSV 和 Parquet 文件整体载入,上限为 256 MB。更大的记录请转换为 EDF。 - 内联 JSON。 小信号可以直接放在条目里,所有通道合计最多 500,000 个采样点。
内联对象为等间隔采样给出 sampling_rate,或为不规则序列(例如光变曲线)给出 time 列表,每个采样点一个时间。这两个键也可以按通道设置。t0 设置第一个采样点的时间,可以写在顶层,也可以按通道设置。data 中的 null 表示缺失的采样点,start_time 给出 t = 0 对应的挂钟时间,供 time_format: clock 使用:
{"id": "demo", "signal": {
"sampling_rate": 100,
"start_time": "2024-01-01T22:00:00",
"channels": [
{"name": "Fz", "unit": "uV", "data": [0.1, 0.4, 0.2]},
{"name": "Resp", "unit": "a.u.", "sampling_rate": 10, "data": [0.0, 0.3]}
]
}}对于 EDF、BDF、WFDB 以及带 sampling_rate 的内联数据,时间是从记录开始算起的秒数。对于 CSV 和 Parquet,时间就是时间列中写入的值(秒、天或 MJD),日期时间列会换算为距第一行的秒数。存储的标签使用与文件相同的时间基准。
显示选项
选项写在 display_options 下,或直接写在字段上:
time_series 选项 | 默认值 | 含义 |
|---|---|---|
window | 30 | 初始显示的秒数,也是前进和后退控件的步长 |
channels | 全部 | 要显示的通道及其顺序,写成名称或 {name, label, range: [low, high], color} |
layout | stacked | stacked 为每个通道画一行,overlay 把所有通道画在同一个 y 轴上并附图例 |
draw | line | line,或用于稀疏、不规则序列的 points |
amplitude | recording | recording 按通道在整个文件上的范围(第 0.5 到第 99.5 百分位)缩放每一行,window 按屏幕上显示的内容重新缩放。通道的 range 优先于这两者 |
lane_height | 56 | 堆叠布局中每行的像素高度,取值 20 到 400 |
height | 由通道行数决定 | 图的像素高度,取值 80 到 4000。不设置时,高度为通道行数乘以 lane_height 再加 48,并限制在 160 到 1400 之间 |
time_format | auto | elapsed(h:mm:ss)、clock(由 start_time 推算的挂钟时间)或 number。auto 在 time_unit 为 s 时表示 elapsed,否则表示 number |
time_unit | s | 时间列的单位,用于以天或 MJD 计时的 CSV 或 Parquet 数据 |
invert_y | false | 在叠加布局中翻转 y 轴,用于天文星等 |
time_column | 自动 | CSV 或 Parquet 的时间列 |
annotation_extension | atr | 作为预标注读取的 WFDB 注释文件,null 表示不读取 |
max_file_mb | 2048 | 可读取的最大信号文件,单位为 MB。更大的文件会被拒绝 |
overview | true | 在图下方显示一条概览条,包含整段记录、已标注的片段和当前窗口。点击或拖动概览条可以移动窗口 |
engine 选项只接受 uplot 一个值。由于信号文件总是在服务器上读取,这个显示类型拒绝 allow_remote。
方案选项
interval_label 选项 | 默认值 | 含义 |
|---|---|---|
signal_field | 必填 | 要标注的 time_series 字段的 key |
labels | 必填 | 名称,或 {name, key_value, color, kind, tooltip}。kind 为 interval(默认)或 event |
mode | free | free 用于拖动和点击,epoch 用于按顺序标注固定窗口 |
epoch_length | 30 | 每帧的秒数 |
channel_scope | all | all 把标签作用于所有通道,channel 作用于拖动开始所在的通道行,either 显示一个开关 |
auto_advance | true | 在分帧模式下,按下标签键后跳到下一帧 |
allow_overlap | 自由模式下为 true,分帧模式下为 false | 为 false 时,新区间会替换同一通道上其他区间被重叠的部分 |
next_key、previous_key | ]、[ | 移动窗口的按键 |
pre_annotations | true | 从文件自带的注释(EDF+、WFDB .atr 或内联)开始 |
preannotation_field | 无 | 一个条目字段,存放 BIDS events.tsv 路径,或以存储格式写出的注释 |
preannotation_labels | 无 | 把文件中的标签重命名为方案标签,映射到 null 的标签会被丢弃。重命名后的注释只有在方案以相同的 kind 声明了该标签时才会保留 |
require_all_epochs | false | 在分帧模式下,每一帧都有标签之前,Next 一直不可用。仅在方案为必填时生效 |
min_annotations | 无 | 条目的区间和事件合计达到给定数量之前,Next 一直不可用。仅在方案为必填时生效 |
instructions | 无 | 显示在标签上方的文本 |
没有 key_value 的标签依次获得按键 1 到 9,然后是 q 到 p。标签颜色默认使用 Okabe-Ito 调色板。Okabe-Ito 配色在常见的色觉缺陷下仍然可以区分。
分帧评分与标记区间
在分帧模式下,记录从开头起被切成长度为 epoch_length 的窗口,标注者一次看一帧。按分期键会标注当前帧并跳到下一帧,[ 和 ](或 Page Up 和 Page Down)只移动不标注,“Clear this epoch”清除当前帧的标签。概览条按分期给每个已评分的帧着色,点击概览条会把窗口移到点击处。
在自由模式下,标注者先按标签键或点击标签按钮,然后在图上拖动来标注一个区间;如果所选标签是事件标签,则点击来标记一个点。选中区间标签时,点击会选中指针下的标记,按标签键会为这个标记更换标签,Delete 删除这个标记,Escape 取消选择。所有标记都列在图下方,各带 Show 和 Delete 按钮,这个列表就是不用鼠标、只用键盘审阅标签的途径。
A− 和 A+ 按钮降低或提高所有通道行的幅度。在获得焦点的图上,左右方向键移动一个窗口,而不像 Potato 其他地方那样在条目之间切换;Shift 加方向键会把窗口平移十分之一。
条目没有已保存的答案时,文件中的注释以虚线叠加层显示,并附一条提示。标注者第一次改动时,或点击“Keep these”时,叠加层会被保存,“Discard them”则清除叠加层。在分帧模式下,文件中的一段睡眠图会被拆分到它覆盖至少一半的那些帧中。
存储的答案
每个方案为每个条目存储一个 JSON 对象:
{"intervals": [{"start": 30.0, "end": 60.0, "label": "N2", "channel": null},
{"start": 41.2, "end": 43.0, "label": "artifact", "channel": "EOG"}],
"events": [{"time": 52.25, "label": "arousal", "channel": null, "source": "file"}]}channel 是通道名称,null 表示所有通道。来自文件且未被改动的标记保留 "source": "file"。
导出
多名标注者可以为同一段记录评分,人数由 num_annotators_per_item 设置。两种信号导出都把每位标注者的标签分开保存:
| 格式 | 输出 |
|---|---|
interval_label | intervals.csv 和 intervals.jsonl,每个区间或事件一行,列为 user_id, instance_id, schema, kind, start, end, duration, label, channel, source。事件的 start 等于 end,时长为 0;标注者添加的标记 source 为 annotator |
bids_events | 每位标注者、每个条目一个 <annotator>/<instance>_events.tsv,列为 onset, duration, trial_type, channel(所有通道记为 n/a),另附一个 events.json 附属文件。有多个 interval_label 方案时,每个方案有自己的文件夹 |
python -m potato.export -c config.yaml -f bids_events -o exports/bids通用的 CSV 和 JSONL 导出也包含答案,每个方案占一个 JSON 单元格。在分帧模式下,每个已评分的帧是一个单独的区间,因此 intervals.csv 中每位标注者的每一帧各占一行。Potato 的一致性指标不覆盖 interval_label 方案。睡眠分期的逐帧统计量,例如 Cohen's kappa,请在导出后从 intervals.csv 计算。
示例
python potato/flask_server.py start examples/advanced/time-series-intervals/config-epochs.yaml -p 8000这个示例有两个条目,指向同一份合成的五分钟 EDF+ 多导睡眠图,用于以 30 秒为一帧评分。config-epochs.yaml 旁边的 config.yaml 在同一段记录以及一份两分钟的腕部加速度计 CSV(附 BIDS events.tsv)上运行自由模式。
用编程智能体搭建研究
potato-skill 1.2.0 面向 Potato 2.13.1,认得 interval_label 和 time_series 这两个名称,并指导研究设计这一侧,例如向标注者问什么、每段记录由几名标注者评分、培训和注意力检查,以及导出。这个技能目前还没有记录信号文件格式、显示选项和 signals 可选依赖。安装 potato-skill,把研究描述给你的智能体,再让智能体参考本页的 Markdown 副本 https://www.potatoannotator.com/docs/annotation-types/time-series.md,以了解格式和选项。
延伸阅读
- 实例显示 - 如何配置显示字段
- 频谱图标注 - 音频上的时频框
- 导出格式 - Potato 的导出格式和导出命令
- 你需要多少位标注者? - 为每个条目选择标注者人数
- 医疗健康与临床 - 用 Potato 做临床和生物医学标注
有关实现细节,请参阅源文档。