library(EGM)library(EGM)The WFDB (Waveform Database) software package provides a standardized system for annotating cardiac signals, predominately surface electrocardiogram (ECG) data. Annotations are polymorphic, meaning multiple annotation sets can be applied to a single signal dataset. The limitation is that these are constrained to physiological events associated with surface ECGs.
All WFDB-compatible annotations in the {EGM} package are stored as annotation_table objects. The columns are type-specific, which allows them to be easily stored. These tables contain the following required columns:
# Create annotations for detected R-peaks
# Example
ann <- annotation_table(
annotator = "qrs",
sample = c(100, 350, 600, 850),
type = "N",
frequency = 250,
channel = 0
)
# Print
ann
#> <annotation_table: 4 `qrs` annotations>
#> time sample type subtype channel number aux
#> <char> <num> <char> <char> <num> <int> <char>
#> 1: 00:00:00.4 100 N 0 0
#> 2: 00:00:01.4 350 N 0 0
#> 3: 00:00:02.4 600 N 0 0
#> 4: 00:00:03.4 850 N 0 0The WFDB standard defines 41 annotation types for surface electrocardiograms. These annotations can be broadly grouped into several categories:
Beat annotations mark individual cardiac cycles and their characteristics:
# Load internal annotation data
beat_symbols <- c("N", "L", "R", "a", "V", "F", "J", "A", "S", "E", "j", "/", "Q", "~")
beat_labels <- EGM:::.surface_annotations[EGM:::.surface_annotations$symbol %in% beat_symbols, ]
knitr::kable(
beat_labels[, c("symbol", "mnemonic", "description")],
col.names = c("Symbol", "Mnemonic", "Description"),
row.names = FALSE,
caption = "Beat Annotation Types"
)| Symbol | Mnemonic | Description |
|---|---|---|
| N | NORMAL | Normal beat |
| L | LBBB | Left bundle branch block beat |
| R | RBBB | Right bundle branch block beat |
| a | ABERR | Aberrated atrial premature beat |
| V | PVC | Premature ventricular contraction |
| F | FUSION | Fusion of ventricular and normal beat |
| J | NPC | Nodal (junctional) premature beat |
| A | APC | Atrial premature contraction |
| S | SVPB | Premature or ectopic supraventricular beat |
| E | VESC | Ventricular escape beat |
| j | NESC | Nodal (junctional) escape beat |
| / | PACE | Paced beat |
| Q | UNKNOWN | Unclassifiable beat |
| ~ | NOISE | Signal quality change |
Key beat types:
These mark the beginning, peak, and end of ECG waveforms:
wave_symbols <- c("p", "t", "u", "(", ")")
wave_labels <- EGM:::.surface_annotations[EGM:::.surface_annotations$symbol %in% wave_symbols, ]
knitr::kable(
wave_labels[, c("symbol", "mnemonic", "description")],
col.names = c("Symbol", "Mnemonic", "Description"),
row.names = FALSE,
caption = "Waveform Boundary Annotations"
)| Symbol | Mnemonic | Description |
|---|---|---|
| p | PWAVE | P-wave peak |
| t | TWAVE | T-wave peak |
| u | UWAVE | U-wave peak |
| ( | WFON | Waveform onset |
| ) | WFOFF | Waveform end |
rhythm_symbols <- c("+", "|", "s", "T", "~", "x")
rhythm_labels <- EGM:::.surface_annotations[EGM:::.surface_annotations$symbol %in% rhythm_symbols, ]
knitr::kable(
rhythm_labels[, c("symbol", "mnemonic", "description")],
col.names = c("Symbol", "Mnemonic", "Description"),
row.names = FALSE,
caption = "Rhythm and Signal Quality Annotations"
)| Symbol | Mnemonic | Description |
|---|---|---|
| ~ | NOISE | Signal quality change |
| | | ARFCT | Isolated QRS-like artifact |
| s | STCH | ST change |
| T | TCH | T-wave change |
| + | RHYTHM | Rhythm change |
| x | NAPC | Non-conducted P-wave (blocked APB) |
special_symbols <- c("*", "D", "\"", "=", "!", "[", "]", "@", "r", "^", "B", "e", "n", "f")
special_labels <- EGM:::.surface_annotations[EGM:::.surface_annotations$symbol %in% special_symbols, ]
knitr::kable(
special_labels[, c("symbol", "mnemonic", "description")],
col.names = c("Symbol", "Mnemonic", "Description"),
row.names = FALSE,
caption = "Specialized Annotations"
)| Symbol | Mnemonic | Description |
|---|---|---|
| * | SYSTOLE | Systole |
| D | DIASTOLE | Diastole |
| ” | NOTE | Comment annotation |
| = | MEASURE | Measurement annotation |
| B | BBB | Left or right bundle branch block |
| ^ | PACESP | Non-conducted pacer spike |
| ! | FLWAV | Ventricular flutter wave |
| [ | VFON | Start of ventricular flutter/fibrillation |
| ] | VFOFF | End of ventricular flutter/fibrillation |
| e | AESC | Atrial escape beat |
| n | SVESC | Supraventricular escape beat |
| @ | LINK | Link to external data (aux_note contains URL) |
| f | PFUS | Fusion of paced and normal beat |
| r | RONT | R-on-T premature ventricular contraction |
When working with WFDB files, annotations are stored with specific file extensions that indicate the annotator used:
.atr: Manually reviewed and corrected reference annotations.ann: General annotation file.ecgpuwave: Surface ECG wave boundaries (P, QRS, T waves) generated by the ecgpuwave algorithm.sqrs, .wqrs, .gqrs: R-peak detections from different QRS detection algorithmsThe complete list of standard WFDB annotations can be accessed using:
# View all standard ECG annotation types
wfdb_annotation_labels()
# Filter for specific symbols
wfdb_annotation_labels(symbol = c("N", "V", "A"))
# Decode annotations in an existing annotation table
ann <- annotation_table(
annotator = "example",
sample = c(100, 200),
type = c("N", "V")
)
# Add human-readable descriptions
wfdb_annotation_decode(ann)Here’s a practical example of reading, interpreting, and visualizing ECG annotations:
# Read an ECG with annotations
record_path <- system.file('extdata', package = 'EGM')
ecg <- read_wfdb(
record = "muse-sinus",
record_dir = record_path
)
# Read associated annotations (if they exist)
ann <- read_annotation(
record = "muse-sinus",
annotator = "ecgpuwave",
record_dir = record_path
)
# Decode annotation types
ann_decoded <- wfdb_annotation_decode(ann)
head(ann_decoded)
#> <annotation_table: 6 annotations>
#> type time sample subtype channel number aux label_store
#> <char> <char> <int> <int> <int> <int> <char> <int>
#> 1: ( 00:00:00.676 338 0 0 0 39
#> 2: p 00:00:00.710 355 0 0 0 24
#> 3: ) 00:00:00.748 374 0 0 0 40
#> 4: ( 00:00:00.812 406 0 0 1 39
#> 5: N 00:00:00.850 425 0 0 0 1
#> 6: ) 00:00:00.900 450 0 0 1 40
#> mnemonic description
#> <char> <char>
#> 1: WFON Waveform onset
#> 2: PWAVE P-wave peak
#> 3: WFOFF Waveform end
#> 4: WFON Waveform onset
#> 5: NORMAL Normal beat
#> 6: WFOFF Waveform endA key feature of the WFDB system is the ability to apply multiple independent annotation sets to a single recording. This is essential for comparing different automated algorithms, validating manual annotations, or analyzing different physiological features simultaneously.
The {EGM} package supports reading multiple annotators in a single operation:
# Read a single annotator (traditional approach)
ann_single <- read_annotation(
record = "300",
record_dir = "path/to/data",
annotator = "ecgpuwave"
)
# Read multiple annotators simultaneously
ann_multiple <- read_annotation(
record = "300",
record_dir = "path/to/data",
annotator = c("ecgpuwave", "atr", "wqrs")
)
# Returns a named list of annotation_table objects
names(ann_multiple)
# [1] "i" "ii" "wqrs"
# Access individual annotators
ann_multiple$ecgpuwave
ann_multiple$atrYou can also read an entire WFDB record with multiple annotations in one call:
# Read signal, header, and multiple annotations together
record <- read_wfdb(
record = "ludb-ecg",
record_dir = system.file("extdata", package = "EGM"),
annotator = c("i", "ii")
)
# The egm object now contains multiple annotators
print(record)
#> <Electrogram>
#> -------------------
#> Recording Duration: 10 seconds
#> Recording frequency 500 hz
#> Number of channels: 12
#> Channel Names: i ii iii avr avl avf v1 v2 v3 v4 v5 v6
#> Annotators: i (48 annotations), ii (48 annotations)# Get names of all annotators in an egm object
annotators <- list_annotators(record)
# [1] "i" "ii"# Extract a specific annotator from an egm object
ecg_ann <- get_annotation(record, "i")
# Without a name: the table itself when there is one annotator, a named list of
# them when there are several - the same shape `read_annotation()` returns
all_ann <- get_annotation(record)
# This will error if the annotator doesn't exist
# get_annotation(record, "nonexistent") # Error!For comparative analysis, you may want to combine multiple annotators into a single table:
# Merge all annotators into one table
merged <- merge_annotations(record)
# The result includes an 'annotator' column to identify the source
head(merged)
#> time sample type subtype channel number aux annotator
#> <char> <int> <char> <int> <int> <int> <char> <char>
#> 1: 00:00:01.282 641 ( 0 0 0 i
#> 2: 00:00:01.288 644 ( 0 0 0 ii
#> 3: 00:00:01.324 662 N 0 0 0 ii
#> 4: 00:00:01.328 664 N 0 0 0 i
#> 5: 00:00:01.364 682 ) 0 0 0 ii
#> 6: 00:00:01.380 690 ) 0 0 0 i
# Merge only specific annotators
merged_subset <- merge_annotations(record, annotators = c("i"))
# You can also merge from a list of annotation_table objects
ann_list <- read_annotation(
record = "ludb-ecg",
record_dir = system.file("extdata", package = "EGM"),
annotator = c("i", "ii")
)
merged_from_list <- merge_annotations(ann_list)This is a work in progress, where we aim to define and implement a comprehensive set of annotations for intracardiac electrograms that are compatible with the standard WFDB annotation system.
ecgpuwaveOne of the common annotators used, called ecgpuwave, demonstrates how the annotation system is leveraged.
The standard labels are used:
When using waveform boundary annotations like ( and ), the number column specifies which waveform:
For T-wave annotations (t), the number column describes morphology:
When reviewing annotations, the {EGM} system allows for all annotation types to be included, whether or not they conform with the WFDB annotator standards. The annotation_table class allows any annotator type to be read in.
However the {EGM} package would like to allow for editing of annotations and writing annotations, which requires some level of standardization (particularly when introducing a new annotation labels to intracardiac data).
To allow for editing on writing annotations (either through an algorithm approach or manual edits), several rules are being developed.