/* file: wqrs.c Wei Zong 23 October 1998 Last revised: 9 April 2010 (by G. Moody) ----------------------------------------------------------------------------- wqrs: Single-lead QRS detector based on length transform Copyright (C) 1998-2010 Wei Zong This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . You may contact the author by e-mail (wzong@mit.edu) or postal mail (MIT Room E25-505, Cambridge, MA 02139, USA). For updates to this software, please visit PhysioNet (http://www.physionet.org/). ------------------------------------------------------------------------------ This program analyzes an ECG signal, detecting QRS onsets and J-points, using a nonlinearly-scaled ECG curve length feature. This version has been optimized for ECGs sampled at 125 Hz, but it can analyze ECGs sampled at any frequency using on-the-fly resampling provided by the WFDB library. `wqrs' can process records containing any number of signals, but it uses only one signal for QRS detection (signal 0 by default; this can be changed using the `-s' option, see below). 'wqrs' has been optimized for adult human ECGs. For other ECGs, it may be necessary to experiment with the input sampling frequency and the time constants indicated below. To compile this program under GNU/Linux, MacOS/X, MS-Windows, or Unix, use gcc: gcc -o wqrs wqrs.c -lwfdb -lm You must have installed the WFDB library, available at http://www.physionet.org/physiotools/wfdb.shtml gcc is standard with GNU/Linux and is available for other platforms from: http://www.gnu.org/ (sources and Unix binaries) http://fink.sourceforge.net (Mac OS/X only) http://www.cygwin.com/ (MS-Windows only) For a usage summary, see the help text at the end of this file. The input record may be in any of the formats readable by the WFDB library, and it may be anywhere in the WFDB path (in a local directory or on a remote web or ftp server). The output of 'wqrs' is an annotation file named RECORD.wqrs (where RECORD is replaced by the name of the input record). Within the output annotation file, the time of each NORMAL annotation marks a QRS onset; if the '-j' option is used, additional JPT annotations mark the J points (the ends of the QRS complexes). For example, to mark QRS complexes in record 100 beginning 5 minutes from the start, ending 10 minutes and 35 seconds from the start, and using signal 1, use the command: wqrs -r 100 -f 5:0 -t 10:35 -s 1 The output may be read using (for example): rdann -a wqrs -r 100 To evaluate the performance of this program, run it on the entire record, by: wqrs -r 100 and then compare its output with the reference annotations by: bxb -r 100 -a atr wqrs */ #include #include #include #include #include #define BUFLN 16384 /* must be a power of 2, see ltsamp() */ #define EYE_CLS 0.25 /* eye-closing period is set to 0.25 sec (250 ms) */ #define MaxQRSw 0.13 /* maximum QRS width (130ms) */ #define NDP 2.5 /* adjust threshold if no QRS found in NDP seconds */ #define PWFreqDEF 60 /* power line (mains) frequency, in Hz (default) */ #define TmDEF 100 /* minimum threshold value (default) */ char *pname; /* the name by which this program was invoked */ double lfsc; /* length function scale constant */ int *ebuf; int nsig; /* number of input signals */ int LPn, LP2n; /* filter parameters (dependent on sampling rate) */ int LTwindow; /* LT window size */ int PWFreq = PWFreqDEF; /* power line (mains) frequency, in Hz */ int sig = -1; /* signal number of signal to be analyzed */ int Tm = TmDEF; /* minimum threshold value */ WFDB_Sample *lbuf = NULL; /* ltsamp() returns a sample of the length transform of the input at time t. Since this program analyzes only one signal, ltsamp() does not have an input argument for specifying a signal number; rather, it always filters and returns samples from the signal designated by the global variable 'sig'. The caller must never "rewind" by more than BUFLN samples (the length of ltsamp()'s buffers). */ WFDB_Sample ltsamp(WFDB_Time t) { int dy; static int Yn, Yn1, Yn2; static WFDB_Time tt = (WFDB_Time)-1L; if (lbuf == NULL) { lbuf = (WFDB_Sample *)malloc((unsigned)BUFLN*sizeof(WFDB_Sample)); ebuf = (int *)malloc((unsigned)BUFLN * sizeof(int)); if (lbuf && ebuf) { for (ebuf[0] = sqrt(lfsc), tt = 1L; tt < BUFLN; tt++) ebuf[tt] = ebuf[0]; if (t > BUFLN) tt = (WFDB_Time)(t - BUFLN); else tt = (WFDB_Time)-1L; Yn = Yn1 = Yn2 = 0; } else { (void)fprintf(stderr, "%s: insufficient memory\n", pname); exit(2); } } if (t < tt - BUFLN) { fprintf(stderr, "%s: ltsamp buffer too short\n", pname); exit(2); } while (t > tt) { static int aet = 0, et; WFDB_Sample v0, v1, v2; Yn2 = Yn1; Yn1 = Yn; if ((v0 = sample(sig, tt)) != WFDB_INVALID_SAMPLE && (v1 = sample(sig, tt-LPn)) != WFDB_INVALID_SAMPLE && (v2 = sample(sig, tt-LP2n)) != WFDB_INVALID_SAMPLE) Yn = 2*Yn1 - Yn2 + v0 - 2*v1 + v2; dy = (Yn - Yn1) / LP2n; /* lowpass derivative of input */ et = ebuf[(++tt)&(BUFLN-1)] = sqrt(lfsc +dy*dy); /* length transform */ lbuf[(tt)&(BUFLN-1)] = aet += et - ebuf[(tt-LTwindow)&(BUFLN-1)]; /* lbuf contains the average of the length-transformed samples over the interval from tt-LTwindow+1 to tt */ } return (lbuf[t&(BUFLN-1)]); } main(int argc, char **argv) { char *p; char *record = NULL; /* input record name */ float sps; /* sampling frequency, in Hz (SR) */ float samplingInterval; /* sampling interval, in milliseconds */ int i, max, min, minutes = 0, onset, timer, vflag = 0; int dflag = 0; /* if non-zero, dump raw and filtered samples only; do not run detector */ int jflag = 0; /* if non-zero, annotate J-points */ int Rflag = 0; /* if non-zero, resample at 120 or 150 Hz */ int EyeClosing; /* eye-closing period, related to SR */ int ExpectPeriod; /* if no QRS is detected over this period, the threshold is automatically reduced to a minimum value; the threshold is restored upon a detection */ double Ta, T0; /* high and low detection thresholds */ WFDB_Anninfo a; WFDB_Annotation annot; WFDB_Gain gain; WFDB_Siginfo *s; WFDB_Time from = 0L, next_minute, spm, t, tj, tpq, to = 0L, tt, t1; static int gvmode = WFDB_GVPAD | WFDB_LOWRES; char *prog_name(); void help(); pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { case 'd': /* dump filter data */ dflag = 1; break; case 'f': /* starting time */ if (++i >= argc) { (void)fprintf(stderr, "%s: time must follow -f\n", pname); exit(1); } from = i; break; case 'h': /* help requested */ help(); exit(0); break; case 'H': /* operate in WFDB_HIGHRES mode */ gvmode = WFDB_GVPAD | WFDB_HIGHRES; break; case 'j': /* annotate J-points (ends of QRS complexes) */ jflag = 1; break; case 'm': /* threshold */ if (++i >= argc || (Tm = atoi(argv[i])) <= 0) { (void)fprintf(stderr, "%s: threshold ( > 0) must follow -m\n", pname); exit(1); } break; case 'p': /* specify power line (mains) frequency */ if (++i >= argc || (PWFreq = atoi(argv[i])) <= 0) { (void)fprintf(stderr, "%s: power line frequency ( > 0) must follow -p\n", pname); exit(1); } break; case 'r': /* record name */ if (++i >= argc) { (void)fprintf(stderr, "%s: input record name must follow -r\n", pname); exit(1); } record = argv[i]; break; case 'R': /* resample */ Rflag = 1; break; case 's': /* signal */ if (++i >= argc) { (void)fprintf(stderr, "%s: signal number or name must follow -s\n", pname); exit(1); } sig = i; /* remember the argument until the record is open */ break; case 't': /* end time */ if (++i >= argc) { (void)fprintf(stderr, "%s: time must follow -t\n", pname); exit(1); } to = i; break; case 'v': /* verbose mode */ vflag = 1; break; default: (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); exit(1); } else { (void)fprintf(stderr, "%s: unrecognized argument %s\n", pname, argv[i]); exit(1); } } if (record == NULL) { help(); exit(1); } if (gvmode == 0 && (p = getenv("WFDBGVMODE"))) gvmode = atoi(p); setgvmode(gvmode|WFDB_GVPAD); if ((nsig = isigopen(record, NULL, 0)) < 1) exit(2); if ((s = (WFDB_Siginfo *)malloc(nsig * sizeof(WFDB_Siginfo))) == NULL) { (void)fprintf(stderr, "%s: insufficient memory\n", pname); exit(2); } if ((nsig = isigopen(record, s, nsig)) < 1) exit(2); sps = sampfreq((char *)NULL); if (sps < PWFreq) { (void)fprintf(stderr, "%s: sampling frequency (%g Hz) is too low%s", pname, sps, (gvmode & WFDB_HIGHRES) ? "\n" : ", try -H option\n"); exit(3); } if (gvmode & WFDB_HIGHRES) setafreq(sampfreq((char *)NULL)); a.name = "wqrs"; a.stat = WFDB_WRITE; if (annopen(record, &a, 1) < 0) exit(2); if (sig >= 0) sig = findsig(argv[sig]); if (sig < 0 || sig >= nsig) sig = 0; if ((gain = s[sig].gain) == 0.0) gain = WFDB_DEFGAIN; if (Rflag) { if (PWFreq == 60.0) setifreq(sps = 120.); else setifreq(sps = 150.); } if (from > 0L) { if ((from = strtim(argv[from])) < 0L) from = -from; } if (to > 0L) { if ((to = strtim(argv[to])) < 0L) to = -to; } else to = strtim("e"); annot.subtyp = annot.num = 0; annot.chan = sig; annot.aux = NULL; Tm = muvadu((unsigned)sig, Tm); samplingInterval = 1000.0/sps; lfsc = 1.25*gain*gain/sps; /* length function scale constant */ spm = 60 * sps; next_minute = from + spm; LPn = sps/PWFreq; /* The LP filter will have a notch at the power line (mains) frequency */ if (LPn > 8) LPn = 8; /* avoid filtering too agressively */ LP2n = 2 * LPn; EyeClosing = sps * EYE_CLS; /* set eye-closing period */ ExpectPeriod = sps * NDP; /* maximum expected RR interval */ LTwindow = sps * MaxQRSw; /* length transform window size */ (void)sample(sig, 0L); if (dflag) { for (t = from; t < to || (to == 0L && sample_valid()); t++) printf("%6d\t%6d\n", sample(sig, t), ltsamp(t)); exit(0); } if (vflag) { printf("\n------------------------------------------------------\n"); printf("Record Name: %s\n", record); printf("Total Signals: %d (", nsig); for (i = 0; i < nsig - 1; i++) printf("%d, ", i); printf("%d)\n", nsig - 1); printf("Sampling Frequency: %.1f Hz\n", sps); printf("Sampling Interval: %.3f ms\n", samplingInterval); printf("Signal channel used for detection: %d\n", sig); printf("Eye-closing period: %d samples (%.0f ms)\n", EyeClosing, EyeClosing*samplingInterval); printf("Minimum threshold: %d A/D units (%d microvolts)\n", Tm, adumuv(sig, Tm)); printf("Power line frequency: %d Hz\n", PWFreq); printf("\n------------------------------------------------------\n\n"); printf("Processing:\n"); } /* Average the first 8 seconds of the length-transformed samples to determine the initial thresholds Ta and T0. The number of samples in the average is limited to half of the ltsamp buffer if the sampling frequency exceeds about 2 KHz. */ if ((t1 = strtim("8")) > BUFLN*0.9) t1 = BUFLN/2; t1 += from; for (T0 = 0, t = from; t < t1 && sample_valid(); t++) T0 += ltsamp(t); T0 /= t1 - from; Ta = 3 * T0; /* Main loop */ for (t = from; t < to || (to == 0L && sample_valid()); t++) { static int learning = 1, T1; if (learning) { if (t > t1) { learning = 0; T1 = T0; t = from; /* start over */ } else T1 = 2*T0; } /* Compare a length-transformed sample against T1. */ if (ltsamp(t) > T1) { /* found a possible QRS near t */ timer = 0; /* used for counting the time after previous QRS */ max = min = ltsamp(t); for (tt = t+1; tt < t + EyeClosing/2; tt++) if (ltsamp(tt) > max) max = ltsamp(tt); for (tt = t-1; tt > t - EyeClosing/2; tt--) if (ltsamp(tt) < min) min = ltsamp(tt); if (max > min+10) { /* There is a QRS near tt */ /* Find the QRS onset (PQ junction) */ onset = max/100 + 2; tpq = t - 5; for (tt = t; tt > t - EyeClosing/2; tt--) { if (ltsamp(tt) - ltsamp(tt-1) < onset && ltsamp(tt-1) - ltsamp(tt-2) < onset && ltsamp(tt-2) - ltsamp(tt-3) < onset && ltsamp(tt-3) - ltsamp(tt-4) < onset) { tpq = tt - LP2n; /* account for phase shift */ break; } } if (!learning) { /* Check that we haven't reached the end of the record. */ (void)sample(sig, tpq); if (sample_valid() == 0) break; /* Record an annotation at the QRS onset */ annot.time = tpq; annot.anntyp = NORMAL; if (putann(0, &annot) < 0) { /* write the annotation */ wfdbquit(); /* close files if an error occurred */ exit(1); } if (jflag) { /* Find the end of the QRS */ for (tt = t, tj = t + 5; tt < t + EyeClosing/2; tt++) { if (ltsamp(tt) > max - (max/10)) { tj = tt; break; } } (void)sample(sig, tj); if (sample_valid() == 0) break; /* Record an annotation at the J-point */ annot.time = tj; annot.anntyp = JPT; if (putann(0, &annot) < 0) { wfdbquit(); exit(1); } } } /* Adjust thresholds */ Ta += (max - Ta)/10; T1 = Ta / 3; /* Lock out further detections during the eye-closing period */ t += EyeClosing; } } else if (!learning) { /* Once past the learning period, decrease threshold if no QRS was detected recently. */ if (++timer > ExpectPeriod && Ta > Tm) { Ta--; T1 = Ta / 3; } } /* Keep track of progress by printing a dot for each minute analyzed */ if (t >= next_minute) { next_minute += spm; (void)fprintf(stderr, "."); (void)fflush(stderr); if (++minutes >= 60) { (void)fprintf(stderr, " %s\n", timstr(t)); minutes = 0; } } } if (minutes) (void)fprintf(stderr, " %s\n", timstr(t)); (void)free(lbuf); (void)free(ebuf); wfdbquit(); /* close WFDB files */ fprintf(stderr, "\n"); if (vflag) { printf("\n\nDone! \n\nResulting annotation file: %s.wqrs\n\n\n", record); } exit(0); } char *prog_name(s) char *s; { char *p = s + strlen(s); #ifdef MSDOS while (p >= s && *p != '\\' && *p != ':') { if (*p == '.') *p = '\0'; /* strip off extension */ if ('A' <= *p && *p <= 'Z') *p += 'a' - 'A'; /* convert to lower case */ p--; } #else while (p >= s && *p != '/') p--; #endif return (p+1); } static char *help_strings[] = { "usage: %s -r RECORD [OPTIONS ...]\n", "where RECORD is the name of the record to be analyzed, and OPTIONS may", "include any of:", " -d dump unfiltered and filtered samples on standard output;", " do not annotate", " -f TIME begin at specified time (default: beginning of the record)", " -h print this usage summary", " -H read multifrequency signals in high resolution mode", " -j find and annotate J-points (QRS ends) as well as QRS onsets", " -m THRESH set detector threshold to THRESH (default: 100)", /* TmDEF */ " -p FREQ specify power line (mains) frequency (default: 60)", /* PWFreqDEF */ " -R resample input at 120 or 150 Hz, depending on power line", " frequency (default: do not resample)", " -s SIGNAL analyze specified signal (default: 0)", " -t TIME stop at specified time (default: end of the record)", " -v verbose mode", "If too many beats are missed, decrease THRESH; if there are too many extra", "detections, increase THRESH.", NULL }; void help() { int i; (void)fprintf(stderr, help_strings[0], pname); for (i = 1; help_strings[i] != NULL; i++) (void)fprintf(stderr, "%s\n", help_strings[i]); }