/* file: gqpost.c G. Moody 16 November 2006 Last revised: 15 January 2019 ------------------------------------------------------------------------------- gqpost: A post-processor for gqrs Copyright (C) 2006-2012 George B. Moody 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 (wfdb@physionet.org) or postal mail (MIT Room E25-505A, Cambridge, MA 02139 USA). For updates to this software, please visit PhysioNet (http://www.physionet.org/). _______________________________________________________________________________ The companion to this program, gqrs, is a QRS detector that is optimized for sensitivity. To reduce the number of false positives, gqpost can read the output annotation file produced by gqrs and can look for interpolated beat detections in it. Such events are identified by looking for pairs or longer groups of inter-beat intervals that sum to values close to the predicted interbeat interval (in other words, an interpolated event is one that occurs between a pair of beats that occur at their expected times). Although true beats can be interpolated, such beats are relatively rare in comparison with false detections that are almost always interpolated, so a high proportion of interpolated events is false, and a simple strategy for improving positive predictivity is to reject all interpolated events. To avoid rejecting (at least some of) the true interpolated beats, gqpost also looks at the size of each interpolated event as recorded by gqrs in the annotation 'num' field, and it rejects only those events that are smaller than a threshold value (which may be adjusted using the -m option). gqpost is most effective in the context of regular rhythms. During highly irregular rhythms, such as atrial fibrillation, the predicted time of the next beat is highly uncertain, and gqpost rejects very few if any events in such cases. gqpost is very fast since it operates entirely on the input annotation file, without reference to the raw signals. The output of gqpost is a complete copy of its input, except that the anntyp fields of any rejected events occurring between the start and end times (specified on the command line using the -f and -t options) are set to ARFCT. gqpost can accept its own output as input, which allows one to vary the rejection threshold if necessary by use of multiple post-processing passes. */ #include #include #include #include #define PBLEN 12 /* size of predictor array */ /* The `getconf' macro is used to check a line of input (already in `buf') for the string named by getconf's first argument. If the string is found, the value following the string (and an optional `:' or `=') is converted using sscanf and the format specifier supplied as getconf's second argument, and stored in the variable named by the first argument. */ #define getconf(a, fmt) if (p=strstr(buf,#a)){sscanf(p,#a "%*[=: \t]" fmt,&a);} void help(void); char *prog_name(char *p); void *gcalloc(size_t nmemb, size_t size); void cleanup(int status); char *pname; /* name of this program, used in messages */ char *record = NULL; /* name of input record */ main(int argc, char **argv) { FILE *config = NULL; int dt, i, meanrr, meanrrd, minrrd, state = 0, thresh = 20; static double HR, RR, RRdelta; WFDB_Anninfo a[2]; WFDB_Annotation annot; WFDB_Frequency sps; WFDB_Time from = (WFDB_Time)0, to = (WFDB_Time)0; a[0].name = "qrs"; a[0].stat = WFDB_READ; a[1].name = "gqp"; a[1].stat = WFDB_WRITE; pname = prog_name(argv[0]); for (i = 1; i < argc; i++) { if (*argv[i] == '-') switch (*(argv[i]+1)) { case 'a': /* input annotator name */ if (++i >= argc) { (void)fprintf(stderr, "%s: input annotator name must follow -a\n", pname); cleanup(1); } a[0].name = argv[i]; break; case 'c': /* configuration file */ if (++i >= argc) { (void)fprintf(stderr, "%s: name of configuration file must follow -c\n", pname); cleanup(1); } if ((config = fopen(argv[i], "rt")) == NULL) { (void)fprintf(stderr, "%s: can't read configuration file %s\n", pname, argv[i]); cleanup(1); } break; case 'f': /* starting time */ if (++i >= argc) { (void)fprintf(stderr, "%s: time must follow -f\n", pname); cleanup(1); } from = i; break; case 'h': /* help requested */ help(); cleanup(0); break; case 'm': /* threshold */ if (++i >= argc) { (void)fprintf(stderr, "%s: threshold must follow -m\n", pname); cleanup(1); } thresh = 10*atof(argv[i]) + 0.5; break; case 'o': /* output annotator name */ if (++i >= argc) { (void)fprintf(stderr, "%s: output annotator name must follow -o\n", pname); cleanup(1); } a[1].name = argv[i]; break; case 'r': /* record name */ if (++i >= argc) { (void)fprintf(stderr, "%s: input record name must follow -r\n", pname); cleanup(1); } record = argv[i]; break; case 't': /* end time */ if (++i >= argc) { (void)fprintf(stderr, "%s: time must follow -t\n", pname); cleanup(1); } to = i; break; default: (void)fprintf(stderr, "%s: unrecognized option %s\n", pname, argv[i]); cleanup(1); } else { (void)fprintf(stderr, "%s: unrecognized argument %s\n", pname, argv[i]); cleanup(1); } } if (record == NULL) { help(); cleanup(1); } /* Read a priori physiologic parameters from the configuration file if available. They can be adjusted in the configuration file for pediatric, fetal, or animal ECGs. */ if (config) { char buf[256], *p; /* Read the configuration file a line at a time. */ while (fgets(buf, sizeof(buf), config)) { /* Skip comments (empty lines and lines beginning with `#'). */ if (buf[0] == '#' || buf[0] == '\n') continue; /* Set parameters. Each `getconf' below is executed once for each non-comment line in the configuration file. */ getconf(HR, "%lf"); getconf(RR, "%lf"); getconf(RRdelta, "%lf"); } fclose(config); } /* If any a priori parameters were not specified in the configuration file, initialize them here (using values chosen for adult human ECGs). */ if (HR != 0.0) RR = 60.0/HR; if (RR == 0.0) RR = 0.8; if (RRdelta == 0.0) RRdelta = RR/4; sps = sampfreq(record); if (annopen(record, a, 2) < 0) cleanup(2); if (getiaorigfreq(0) > 0) { setifreq(sps = getiaorigfreq(0)); setiafreq(0, sps); } meanrr = RR * sps; meanrrd = RRdelta * sps; if ((minrrd = meanrrd/2) < 4) minrrd = 4; if ((dt = meanrr/80) < 1) dt = 1; if (from) { from = strtim(argv[from]); if (from < 0) from = -from; } if (to) { to = strtim(argv[to]); if (to < 0) to = -to; } while (getann(0, &annot) == 0) { switch (state) { case 0: if (annot.time < from) break; else state++; /* fall through to case 1 */ case 1: if (to > (WFDB_Time)0 && annot.time > to) state++; else { /* reject sub-threshold interpolated events */ static WFDB_Time tpred, tpredmin, tprev; static int rr, rrd; if (annot.time < tpredmin && isqrs(annot.anntyp) && annot.num < thresh) { /* event is small and early */ WFDB_Annotation nextann; if (getann(0, &nextann) == 0) { /* peek ahead */ if (nextann.time <= tpred) annot.anntyp = ARFCT; /* reject event */ ungetann(0, &nextann); } } if (isqrs(annot.anntyp)) { rr = annot.time - tprev; if ((rrd = rr - meanrr) < 0) rrd = -rrd; if (rr > meanrr + dt) meanrr += dt; else if (rr > meanrr - dt) meanrr = rr; else meanrr -= dt; if (rrd > meanrrd + dt) meanrrd += dt; else if (rrd > meanrrd - dt) meanrrd = rrd; else meanrrd -= dt; if (meanrrd < minrrd) meanrrd = minrrd; tpred = annot.time + meanrr + minrrd; tpredmin = annot.time + meanrr - meanrrd; tprev = annot.time; } } break; case 2: /* do nothing */ break; } putann(0, &annot); } cleanup(0); } /* prog_name() extracts this program's name from argv[0], for use in error and warning messages. */ char *prog_name(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); } /* help() prints a (very) concise summary of how to use this program. A more detailed summary is in the man page (gqpost.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:", " -a ANNOTATOR read annotations from the specified ANNOTATOR (default: qrs)", " -c FILE initialize parameters from the specified configuration FILE", " -f TIME begin processing at specified time", " -h print this usage summary", " -H read multifrequency signals in high resolution mode", " -m THRESH set interpolated event acceptance threshold to THRESH", " (default: 1)", " -o ANNOTATOR write annotations to the specified ANNOTATOR (default: gqp)", " -t TIME stop processing at specified time", "If too many true beats are rejected, decrease THRESH; if too many false", "detections are accepted, increase THRESH.", "Note that the output is a complete copy of the input (with rejected events", "flagged as ARFCT). The -f and -t options only limit the interval during", "which events may be rejected.", 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]); } /* gcalloc() is a wrapper for calloc() that handles errors and maintains a list of allocated buffers for automatic on-exit deallocation via cleanup(). */ size_t gcn = 0, gcnmax = 0; void **gclist = NULL; void *gcalloc(size_t nmemb, size_t size) { void *p = calloc(nmemb, size); if ((p == NULL) || ((gcn >= gcnmax) && (gclist = realloc(gclist, (gcnmax += 32)*(sizeof(void *)))) == NULL)) cleanup(3); /* could not allocate requested memory */ return (gclist[gcn++] = p); } void cleanup(int status) /* close files and free allocated memory */ { if (status == 3) fprintf(stderr, "%s: insufficient memory (%d)\n", pname, status); if (record) wfdbquit(); while (gcn-- > 0) if (gclist[gcn]) free(gclist[gcn]); exit(status); }