Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ECG Visualization and Arrhythmia Detection - the MIT-BIH Database

Data source - the PhysioNet MIT-BIH Arrhythmia Database

The MIT-BIH Arrhythmia Database is a publicly available collection of electrocardiogram recordings published by PhysioNet at:

https://physionet.org/content/mitdb/1.0.0/

It contains 48 half-hour, two-channel recordings collected from 47 patients at Beth Israel Hospital in Boston between 1975 and 1979. The recordings were manually annotated by at least two independent cardiologists and are widely used in research on automatic arrhythmia detection.

Record 205

The example uses record 205 - a recording from a 59-year-old man treated with Digoxin and Quinaglute. The record contains episodes of ventricular tachycardia (VT) and is often cited in the literature as diagnostically challenging, due to two morphologically distinct forms of premature ventricular contractions (PVC).

Recording parameters:

ParameterValue
Duration≈ 30 min
Sampling rate360 Hz
Number of samples650,000
Channel 1 (MLII)Modified limb lead II
Channel 2 (V1)Precordial lead V1
Resolution12 bits, gain 200 LSB/mV, zero point 1024

Raw values are stored as unitless integers (so-called ADC values). Conversion to millivolts:

\[\text{mV} = \frac{\text{ADC} - 1024}{200}\]

The range of actual values in the rec205 file falls between 589–1315 (MLII) and 718–1106 (V1), corresponding to a signal amplitude of roughly ±1.5 mV.

Data preparation

The original recording files (205.hea, 205.dat, 205.atr) are provided in the MIT-BIH format and need to be converted into a binary format recognized by RetractorDB.

The MIT-BIH format 212

The signal in the 205.dat file is packed 12-bit in format 212: every three bytes store two consecutive samples of both channels according to the scheme:

[B0][B1][B2] → MLII = B0  | ((B1 & 0x0F) << 8)
               V1   = B2  | ((B1 >>  4)  << 8)

The values are 12-bit signed (range –2048..2047).

Converting to the RetractorDB format

The script examples/ecg/mitbih2rdb.py reads the 205.hea header, decodes the sample pairs, and writes them as little-endian int32 records into the rec205 file:

650,000 records × 2 fields × 4 bytes = 5,200,000 bytes

At the same time, the script generates an RQL script that plays back the signal (rec205-replay.rql). The descriptor file rec205.desc is created by build.sh.

The whole preparation process is run with a single command from the project’s root directory:

bash examples/ecg/build.sh

The result is three files in the examples/ecg/rec205/ directory:

FileGenerated byDescription
rec205mitbih2rdb.pyBinary data (int32 LE)
rec205.descbuild.shStream descriptor
rec205-replay.rqlmitbih2rdb.pyRQL playback script

The RQL query

The file rec205-replay.rql defines two streams:

DECLARE MLII INTEGER, V1 INTEGER STREAM ecg, 1/360 BINFILE 'rec205'

SELECT ecg.MLII, ecg.V1 STREAM s205out FROM ecg VOLATILE

The STREAM ecg, 1/360 clause sets the time interval of a single sample to 1/360 s, matching the actual sampling rate of 360 Hz. The BINFILE keyword (TYPE BINFILE in the descriptor) causes the rec205 file to be read as raw binary records, sequentially in a loop (after the last sample, reading returns to the beginning), enabling continuous playback of the recording.

The output stream s205out is declared VOLATILE, so it is not written to disk - the data only reaches the consumer process (xqry).

On-screen visualization

The ecg target in the build system is used to display the chart in real time. It runs the script scripts/xplot.sh, which starts xretractor in the background, and then pipes the data stream through xqry into gnuplot.

# from build/Debug
ninja ecg

The invocation CMake expands this into:

scripts/xplot.sh s205out rec205-replay.rql 720,560,1360 --gnuplot-rtl

Meaning of the parameters:

ParameterMeaning
s205outThe name of the output stream
rec205-replay.rqlThe query file
720The data window width (samples visible at once)
560,1360The Y-axis range (ADC values matching the actual signal)
--gnuplot-rtlNewest samples on the right, chart scrolls right to left

The --gnuplot-rtl option is an xqry parameter that reverses gnuplot’s X axis (set xrange [720:0]). The effect is that the freshest samples appear on the right side of the window, and older ones scroll to the left - similar to the classic ECG printout on paper tape.

The script gives the server a name derived from the working directory and passes it to every xqry --server invocation. Several plotting targets can therefore run concurrently without taking over another instance. An optional fifth argument supplies a different name; if that name is already in use, the script exits before removing the output directory.

View of the gnuplot window with the ECG signal being played back in RetractorDB

Fig. 60. View of the gnuplot window with the ECG signal being played back (record 205)

The window shown in Fig. 60 displays 720 samples, i.e. exactly 2 seconds of signal at 360 Hz, matching the typical width of a single ECG strip used in diagnostics.

QRS Detection and Arrhythmia Identification

Context - the Pan-Tompkins algorithm

QRS-complex detection is the foundation of automatic ECG analysis. The QRS complex represents ventricular depolarization and corresponds to every heartbeat, visible as a sharp spike in the signal. Knowing the positions of the QRS complexes in time, RR intervals can be computed, and from them, basic rhythm disturbances can be identified:

Measure derived from QRSUse
RR intervalsHeart rate (HR), VT, bradycardia
RR variability (HRV)Autonomic nervous system, event prediction
QRS morphologyDistinguishing PVC from a normal rhythm, APC
QRS durationBundle branch block (BBB)

The Pan-Tompkins algorithm (1985) is a classic, five-stage, pipelined digital-signal-processing algorithm implemented with FIR filters. RetractorDB implements it directly as an RQL query stream, without specialized DSP libraries.

Generating the signal filters (coef)

The algorithm requires two sets of FIR coefficients, stored as text files (bp_coef.txt, d_coef.txt). They are generated once, with Python scripts, before running detection.

The band-pass filter - gen_bp_coef.py

Step 1 of the algorithm requires a filter that cuts out noise and artifacts outside the QRS band. A passband of 5–15 Hz at fs = 360 Hz produces a response containing the QRS morphology, while attenuating baseline wander (< 5 Hz) and muscle noise (> 15 Hz).

The design method is a windowed sinc:

h_bp[n] = (h_lp2[n] − h_lp1[n]) · w[n]

where:

  • h_lp[n] = 2·fc·sinc(2·fc·(n−M)) - the ideal low-pass filter
  • w[n] = 0.54 − 0.46·cos(2πn/(N−1)) - the Hamming window, damping Gibbs effects
  • M = (N−1)/2 = 12 - the filter’s center point (group delay = 12 samples)

Parameters:

ParameterValue
Filter length N25 coefficients
Lower cutoff fc₁5 Hz (normalized 5/360)
Upper cutoff fc₂15 Hz (normalized 15/360)
Integer scale×1000 (divided /1000 in RQL)

Running the script:

cd examples/ecg/rec205
python3 gen_bp_coef.py
# Saved 25 coefficients to bp_coef.txt
# Coefficients: [-2, -2, -1, 0, 3, 8, 14, 23, 32, 41, 49, 54, 56, ...]
# Sum (DC gain): 5 / 1000 = 0.0050

The coefficients are symmetric about the center (n=12), confirming the filter’s linear phase - an essential property when analyzing ECG, since it guarantees no phase distortion of the QRS morphology.

The differentiating filter - gen_d_coef.py

Step 2 of the algorithm applies a filter that emphasizes the steep edges of the QRS. Pan and Tompkins proposed a 5-point derivative estimator:

y[n] = (1/8T) · (−x[n−4] − 2·x[n−3] + 2·x[n−1] + x[n])

Coefficients (from oldest to newest sample):

h = [−1, −2, 0, 2, 1]

Filter properties:

PropertyValue
Sum of coefficients0 (zero DC gain - eliminates offsets)
Maximum responsef ≈ 10–25 Hz (the QRS-edge range)
Scale factor (1/8T)360/8 = 45 Hz (ignored - does not affect detection)
cd examples/ecg/rec205
python3 gen_d_coef.py
# Saved 5 coefficients to d_coef.txt
# Coefficients: [-1, -2, 0, 2, 1]
# Sum (DC gain): 0  (should be 0)

Implementing the pipeline in RQL - rec205-detect.rql

The file rec205-detect.rql implements the complete five-stage pipeline for both ECG channels (MLII and V1):

# Keep windows extracted automatically from FROM in memory
SUBSTRAT 'memory'

DECLARE MLII INTEGER, V1 INTEGER STREAM ecg, 1/360 BINFILE 'rec205'
DECLARE bp_coef INTEGER[25] STREAM bpf, 1 TEXTFILE 'bp_coef.txt'
DECLARE d_coef INTEGER[5]   STREAM df,  1 TEXTFILE 'd_coef.txt'

# Extracting the channels
SELECT ecg.MLII            STREAM mlii    FROM ecg VOLATILE
SELECT ecg.V1              STREAM v1      FROM ecg VOLATILE

# 1. Band-pass filter (5-15 Hz) - 25-tap FIR convolution
SELECT mlii[_]*bpf[_]      STREAM bp_acc  FROM mlii@(1,25)+bpf VOLATILE
SELECT int(bp_acc[0]/1000) STREAM bp_out  FROM SUMC(bp_acc) VOLATILE

# 2. Differentiation - 5-tap FIR convolution
SELECT bp_out[_]*df[_]     STREAM d_acc   FROM bp_out@(1,5)+df VOLATILE
SELECT int(d_acc[0])       STREAM d_out   FROM SUMC(d_acc) VOLATILE

# 3. Squaring (/1000 prevents int32 overflow)
SELECT d_out[0]^2/1000     STREAM sq_out  FROM d_out VOLATILE

# 4. Moving-window integration over 30 samples (~83 ms)
SELECT int(sq_out[0])      STREAM mwi     FROM AVG(sq_out@(1,30)) VOLATILE

# 5. Adaptive threshold - 2x moving average over 180 samples (0.5 s)
SELECT int(mwi[0])         STREAM mwi_thr FROM AVG(mwi@(1,180)) VOLATILE

# Output: MLII centered, V1 centered, detection signal ×5
SELECT mlii[0]-900, v1[0]-900, (mwi[0]-mwi_thr[0]*2)*5 \
STREAM detect_out FROM mlii+v1+mwi+mwi_thr VOLATILE

Rationale for the parameters

The @(1,25) operator creates a 25-sample sliding window directly in FROM. The mlii[_] index expands according to the 25 slots contributed by this window to the input record, and SUMC adds the products with bpf[_]. The discrete convolution therefore does not require a separate mlii_win query. The same notation creates the five-element differentiating convolution - see Underscore Symbol Processing.

The compiler extracts windows and reducers from a compound FROM clause into compiler-generated substrates. VOLATILE applies to the stream named by a given SELECT, not to these automatic nodes. The SUBSTRAT 'memory' directive keeps the complete intermediate pipeline in memory; without it, the generated windows would use the default on-disk storage.

The bp_acc[0]/1000 division in step 1 compensates for the integer scale of the band-pass filter coefficients. The second /1000, after exponentiation in step 3, limits value growth; without it, d_out[0]^2 could exceed the int32 range (2,147,483,647) for typical ECG amplitudes.

The pipeline computes in integer arithmetic, so every reducer result returns to INTEGER through an explicit int(...) (short for to_integer). SUMC and AVG over an INTEGER field yield RATIONAL; without the cast the denominators grow from stage to stage (/1000, squaring, the 30-sample average) until boost::rational<int> overflows without warning.

The output expression (mwi[0]-mwi_thr[0]*2)*5 implements the adaptive threshold: the value is positive only when the MWI envelope exceeds twice the current moving average - indicating a detected QRS. The ×5 multiplier scales the detection signal to a range visually comparable to the raw ECG on the chart.

Running it - ninja ecg-detect-qrs

The process is started with a single command from the build/Debug directory:

cd build/Debug
ninja ecg-detect-qrs

CMake expands this target into the command:

scripts/xplot.sh detect_out rec205-detect.rql 720,-400,400 --gnuplot-rtl

Meaning of the parameters:

ParameterMeaning
detect_outThe name of the output stream (3 fields)
rec205-detect.rqlThe query file with the pipeline above
720Window width: 720 samples = 2 seconds at 360 Hz
−400,400Y-axis range in ADC units (≈ ±2 mV)
--gnuplot-rtlNewest samples on the right (right-to-left)

The xplot.sh script starts xretractor in the background (compiling and executing the queries), then pipes the detect_out stream through xqry into gnuplot in continuous mode. The gnuplot window refreshes with every new batch of samples.

Figure description - the gnuplot window

gnuplot QRS-detection window: MLII, V1, and the detection signal on record 205

Fig. 61. The gnuplot window from running ninja ecg-detect-qrs - MIT-BIH record 205, 720 samples (2 s), RTL

In Fig. 61, three signals are visible, corresponding to the three fields of the detect_out stream:

[detect-out-0] red line - MLII centered (mlii − 900)

The raw ECG signal from lead MLII, shifted by the 900 ADC baseline point so that the zero axis corresponds to the isoline. Two sharp spikes (amplitude ≈ 280 ADC ≈ 1.4 mV) around samples 520 and 350 from the right edge represent two consecutive QRS complexes. The clear QRS morphology, with a dominant R peak, confirms the band-pass filter is working correctly - noise has been suppressed while the peak retained its amplitude.

[detect-out-1] blue line - V1 centered (v1 − 900)

The signal from lead V1 of the same recording. QRS morphology in V1 is, as a rule, less pronounced than in MLII, which is visible in the figure - the blue signal shows a smaller R-peak amplitude at similar QRS time positions. Having both channels available at once allows distinguishing supraventricular contractions (APC) from ventricular ones (PVC), since ventricular QRS complexes show a distinct morphology in V1.

[detect-out-2] green line - the QRS detection signal ((mwi − 2·mwi_thr) × 5)

The algorithm’s output signal. A positive value means a detected QRS complex - the moving-window-integration envelope exceeded twice the adaptive threshold. In the figure, two clear positive pulses are visible, coinciding in time with the QRS peaks on the MLII channel. Between beats, the line stays close to zero or slightly below - confirming the detector’s specificity.

The gap between the two visible QRS complexes is approximately 170 samples, which at 360 Hz gives:

RR ≈ 170 / 360 ≈ 0.47 s  →  HR ≈ 127 bpm

This value falls within the range of ventricular tachycardia (VT, 79–216 bpm) recorded in record 205, suggesting that the visualized fragment of the recording comes from one of the 6 VT episodes noted by the MIT-BIH cardiologists.

Process flow diagram

The diagram below (Fig. 62) shows the complete data flow from the raw MIT-BIH recording to arrhythmia identification, indicating where RetractorDB carries out the Pan-Tompkins algorithm, and its relationship to classic arrhythmia-recognition methods:

Data-flow diagram for the QRS-detection and arrhythmia-identification process

Fig. 62. Data flow - from the MIT-BIH recording, through the Pan-Tompkins pipeline in RQL, to visualization and arrhythmia identification

The right branch of the diagram - Arrhythmia identification - represents classic post-QRS-detection analysis methods, which can be built as further RQL queries layered on top of the detect_out stream:

MethodDescriptionRelationship to QRS
RR intervalsTime between consecutive QRS complexes → HRdirectly from detection positions
HRV (variability)Standard deviation of RRRR-stream statistics
PVC classificationQRS width > 120 ms, V1 morphologywidth of the mwi window
VT detectionSequence of ≥ 3 PVCs with HR > 100 bpmRULE on the HR+PVC stream
APC detectionAn early, narrow QRS preceding a pauseMLII vs V1 morphology

RetractorDB provides RULE operators and windowed reducers (AVG, SUMC), which make it possible to implement the methods above in the same RQL query language, without leaving the system’s environment. QRS detection is the first and necessary step in this hierarchy.