Understanding Annotations

library(EGM)

1 Electrocardiogram Annotations

1.1 WFDB Annotation Standards

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.

1.1.1 Annotation Table Structure

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:

  • annotator: Name of the annotation function or creator (e.g., “sqrs”, “ecgpuwave”, “manual”)
  • time: Time stamp constructed from sample number and sampling frequency (format: HH:MM:SS.mmm)
  • sample: Integer index of the annotation position in the signal
  • type: Single character symbol describing the annotation type (see annotation types below)
  • subtype: Single character providing additional classification
  • channel: Integer indicating which signal channel the annotation applies to
  • number: Additional numeric qualifier providing context-specific information
# 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      0

1.2 Standard ECG Annotation Types

The WFDB standard defines 41 annotation types for surface electrocardiograms. These annotations can be broadly grouped into several categories:

1.2.1 Beat Annotations

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

  • N (NORMAL): Normal sinus beat
  • V (PVC): Premature ventricular contraction
  • A (APC): Atrial premature contraction
  • L/R (LBBB/RBBB): Bundle branch block beats
  • F (FUSION): Fusion of ventricular and normal beat
  • / (PACE): Paced beat

1.2.2 Waveform Boundary Annotations

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

1.2.3 Rhythm and Signal Quality Annotations

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

1.2.4 Specialized Annotations

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

1.3 Common Annotation File Types

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 algorithms

1.4 Accessing All Standard Annotations

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

1.5 Example: Working with ECG Annotations

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 end

2 Working with Multiple Annotators

A 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.

2.1 Reading Multiple Annotators

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$atr

2.2 Reading Complete Records with Multiple Annotators

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

2.3 Helper Functions for Multi-Annotator Workflows

2.3.1 List Available Annotators

# Get names of all annotators in an egm object
annotators <- list_annotators(record)
# [1] "i"  "ii"

2.3.2 Extract Specific Annotators

# 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!

2.3.3 Merge Multiple Annotators

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)

3 Intracardiac Electrogram Annotations

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.

4 Examples

4.1 ecgpuwave

One of the common annotators used, called ecgpuwave, demonstrates how the annotation system is leveraged.

The standard labels are used:

  • p (PWAVE): P-wave peak
  • t (TWAVE): T-wave peak
  • ( (WFON): Waveform onset
  • ) (WFOFF): Waveform end

When using waveform boundary annotations like ( and ), the number column specifies which waveform:

  • 0 = P wave
  • 1 = QRS complex
  • 2 = T wave

For T-wave annotations (t), the number column describes morphology:

  • 0 = Normal
  • 1 = Inverted
  • 2 = Positive
  • 3 = Negative
  • 4 = Biphasic (negative-positive)
  • 5 = Biphasic (positive-negative)

5 Registration

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.