Skip to content

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):

bash
pip install 'potato-annotation[signals]'

配置

下面的配置在一份四通道多导睡眠图上以 30 秒为一帧进行睡眠分期。第二个方案让标注者在同一张图上点击标记觉醒:

yaml
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 的信号文件路径:

json
{"id": "night-01", "note": "Night 1", "signal": "psg/night-01.edf"}

用于 EEG 审阅的自由模式

EEG 审阅使用自由模式。在自由模式下,标注者在图上拖动来标注一个区间,点击来标记一个点事件。下面这段部分配置替换上面配置中的 annotation_schemes:

yaml
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 相同的自由模式方案,就像上面的睡眠配置对觉醒所做的那样。两个方案要使用不同的标签按键。

信号文件

条目中所写文件的扩展名决定使用哪个读取器:

扩展名格式依赖
.edfEDF、EDF+(连续与不连续)无
.bdfBioSemi BDF、BDF+(24 位)无
.heaWFDB 记录(头文件,加上头文件列出的 .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 使用:

json
{"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 选项默认值含义
window30初始显示的秒数,也是前进和后退控件的步长
channels全部要显示的通道及其顺序,写成名称或 {name, label, range: [low, high], color}
layoutstackedstacked 为每个通道画一行,overlay 把所有通道画在同一个 y 轴上并附图例
drawlineline,或用于稀疏、不规则序列的 points
amplituderecordingrecording 按通道在整个文件上的范围(第 0.5 到第 99.5 百分位)缩放每一行,window 按屏幕上显示的内容重新缩放。通道的 range 优先于这两者
lane_height56堆叠布局中每行的像素高度,取值 20 到 400
height由通道行数决定图的像素高度,取值 80 到 4000。不设置时,高度为通道行数乘以 lane_height 再加 48,并限制在 160 到 1400 之间
time_formatautoelapsed(h:mm:ss)、clock(由 start_time 推算的挂钟时间)或 number。auto 在 time_unit 为 s 时表示 elapsed,否则表示 number
time_units时间列的单位,用于以天或 MJD 计时的 CSV 或 Parquet 数据
invert_yfalse在叠加布局中翻转 y 轴,用于天文星等
time_column自动CSV 或 Parquet 的时间列
annotation_extensionatr作为预标注读取的 WFDB 注释文件,null 表示不读取
max_file_mb2048可读取的最大信号文件,单位为 MB。更大的文件会被拒绝
overviewtrue在图下方显示一条概览条,包含整段记录、已标注的片段和当前窗口。点击或拖动概览条可以移动窗口

engine 选项只接受 uplot 一个值。由于信号文件总是在服务器上读取,这个显示类型拒绝 allow_remote。

方案选项

interval_label 选项默认值含义
signal_field必填要标注的 time_series 字段的 key
labels必填名称,或 {name, key_value, color, kind, tooltip}。kind 为 interval(默认)或 event
modefreefree 用于拖动和点击,epoch 用于按顺序标注固定窗口
epoch_length30每帧的秒数
channel_scopeallall 把标签作用于所有通道,channel 作用于拖动开始所在的通道行,either 显示一个开关
auto_advancetrue在分帧模式下,按下标签键后跳到下一帧
allow_overlap自由模式下为 true,分帧模式下为 false为 false 时,新区间会替换同一通道上其他区间被重叠的部分
next_key、previous_key]、[移动窗口的按键
pre_annotationstrue从文件自带的注释(EDF+、WFDB .atr 或内联)开始
preannotation_field无一个条目字段,存放 BIDS events.tsv 路径,或以存储格式写出的注释
preannotation_labels无把文件中的标签重命名为方案标签,映射到 null 的标签会被丢弃。重命名后的注释只有在方案以相同的 kind 声明了该标签时才会保留
require_all_epochsfalse在分帧模式下,每一帧都有标签之前,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 对象:

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_labelintervals.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 方案时,每个方案有自己的文件夹
bash
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 计算。

示例

bash
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,以了解格式和选项。

延伸阅读

有关实现细节,请参阅源文档。