Improving the Quality of ECGs Collected using Mobile Phones: The PhysioNet/Computing in Cardiology Challenge 2011 1.0.0
(16,058 bytes)
/****************************************************
* Physionet Challenge 2011 *
* Entry for event2 and 3 *
* *
* Author: Lars Johannesen <lj@halloffame.dk> *
* *
****************************************************/
package org.physionet.challenge2011;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
public class ChallengeEntry {
public static final String DEBUGTAG = ChallengeEntry.class.toString();
final static int FS= 500; //Sampling Frequency
final static int CH= 12;
final static int MAX_RT= 220; //Max expected beats in minutes
final static int WIN=FS*10;
// Because FS is fixed:
// Octave: b = fir1(5, 40/250);
final static short LFILT_ORDER = 10;
double [] LFILT = {
1.05384029057833e-04,
1.07692293618465e-02,
5.43189261717634e-02,
1.39011974550478e-01,
2.29255737951395e-01,
2.68492096154118e-01,
2.29255737951395e-01,
1.39011974550478e-01,
5.43189261717634e-02,
1.07692293618465e-02,
1.05384029057833e-04
};
final static short U3_N = 50; // 100ms
final static short MAXQRS=40;
int[] QRS=new int[MAXQRS*CH]; // Initial candidates
int[] QRSREAL=new int[MAXQRS]; // True complexes
int NQRS_REAL=0;
int[] NQRS=new int[CH];
final short QRSWIN = 100; // 200 ms
final short MINLEAD = 6; // QRS complexes must be present in 6 or more leads
final short QRS_RECONCILE = 80; // 80 ms
final short LEFT_MOVE=5; // 10 ms
final short MAX_LEFT=35; // 70 ms
int[] QRSON=new int[MAXQRS]; // Onsets
double[] RMSU3 = new double[CH];
final double RMSU3_THR = 0.7;
final double RMSU3_THR2 = 0.2;
final double ONSETTHR=0.15; // When value is below this relative to peak in each lead it is onset
final double CORTHR=0.70;
//Define Quality values (could also be defined as enum...)
final static int INIT=0;
final static int GOOD = 0;
final static int BAD = 1;
short[] data=new short[WIN*CH];
double[] data_filt=new double[WIN*CH];
double[] data_u3=new double[WIN*CH];
int [] starts = new int[MAXQRS];
int [] stops = new int[MAXQRS];
final static short PREFIX=150; // 300 ms
final static short POSTFIX=100; // 200 ms
final static short MAXLENGTH=2000; // 4000 ms
double [] medians = new double[MAXLENGTH*CH];
int [] REALQRS_LEAD = new int[CH];
double[] h = new double[MAXQRS];
double[] A = new double[MAXQRS];
double[] B = new double[MAXQRS];
double[] C = new double[MAXQRS];
double[] D = new double[MAXQRS];
double[] S = new double[MAXQRS];
double[] y = new double[MAXQRS];
double[] x = new double[MAXQRS];
final short PQWIN=3;
synchronized public int get_result(InputStream iFile, final ECG_MetaData m_MetaData) throws IOException {
ObjectInputStream in = new ObjectInputStream(iFile);
try {
data = (short[])in.readObject();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
int [] tmpflag = new int[CH];
for(int i = 0; i < CH; ++i) {
REALQRS_LEAD[i] = 0;
tmpflag[i] = 0;
}
// Step1: Detect LF
boolean leadfail = true;
for(int i = 0; i < CH; ++i) {
for(int j = 1; j < WIN; ++j) {
if(data[j*CH+i] != data[(j-1)*CH+i]) {
leadfail = false;
break;
}
}
if(!leadfail) { break; }
}
if(leadfail) {
iFile.close();
return BAD;
}
// Step2: Calculate HF noise / filter signal for U3
double allsum=0;
for(int i = 0; i < CH; ++i) {
// Transient handling
data_filt[0*CH+i] = LFILT[0]*data[0*CH+i];
double diff = (data_filt[0*CH+i] - data_filt[0*CH+i]);
double cursum = diff*diff;
for(int j=1; j < LFILT_ORDER+1; ++j) {
data_filt[j*CH+i] = 0;
for(int k = 0; k < j+1; ++k) {
data_filt[j*CH+i] = data_filt[j*CH+i] + LFILT[k] * data[(j-k)*CH+i];
}
diff = data_filt[j*CH+i]-data[j*CH+i];
cursum = cursum + (diff*diff);
}
// Filter rest
for(int j=LFILT_ORDER+1; j < WIN; ++j) {
data_filt[j*CH+i] = 0;
for(int k = 0; k < LFILT_ORDER+1; ++k) {
data_filt[j*CH+i] = data_filt[j*CH+i] + LFILT[k] * data[(j-k)*CH+i];
}
diff = data_filt[j*CH+i]-data[j*CH+i];
cursum = cursum + (diff*diff);
}
allsum = allsum + Math.sqrt(cursum/WIN);
}
double hfnoise = allsum / CH;
if(hfnoise > 100) {
iFile.close();
return BAD;
}
// Step3 : Deploy QRS detector
for(int i = 0; i < CH; ++i) {
double sum = 0;
double cursum = 0;
for(int j = 2; j < U3_N; ++j) {
sum = sum + Math.pow(data_filt[j*CH+i] -
data_filt[(j-2)*CH+i],2);
}
data_u3[0*CH+i] = sum;
cursum = sum * sum;
for(int j = 1; j < WIN-U3_N/2; ++j) {
double subterm = 0;
if(j > U3_N/2) {
subterm = Math.pow(data_filt[(j-U3_N/2)*CH+i]-
data_filt[(j-U3_N/2+2)*CH+i],2);
}
double addterm = Math.pow(data_filt[(j+U3_N/2)*CH+i]-
data_filt[(j+U3_N/2-2)*CH+i],2);
data_u3[j*CH+i] = data_u3[(j-1)*CH+i] + addterm - subterm;
cursum = cursum + (data_u3[j*CH+i]*data_u3[j*CH+i]);
}
RMSU3[i] = Math.sqrt(cursum/WIN);
NQRS[i] = 0;
boolean within = false; // Did we recently found a peak ?
int start = 0; // Start of search for peaks
int lastloc = 0; // Last location
int j = 1;
double thr = RMSU3[i] * RMSU3_THR;
while( j < WIN-1) {
if(data_u3[(j-1)*CH+i] < data_u3[j*CH+i] && data_u3[j*CH+i] > data_u3[(j+1)*CH+i] && data_u3[j*CH+i] > thr) {
if(!within) {
within = true;
start = j;
lastloc = j;
} else if(data_u3[i+CH*lastloc] < data_u3[j*CH+i]) {
lastloc = j;
}
}
if(within && (j - start) > U3_N) {
QRS[CH*NQRS[i]+i] = lastloc;
NQRS[i] = NQRS[i] + 1;
j = j + QRSWIN;
within = false;
} else {
++j;
}
}
}
// Step4 : Reconcile QRS complexes and determine noise peaks
int noisepeaks = 0;
int multinoise = 0;
NQRS_REAL = 0;
double tmpsum = 0;
int tmpn = 0;
for(int i = 0; i < CH; ++i) {
for(int iQRS = 0; iQRS < NQRS[i]; ++iQRS) {
if(QRS[i+CH*iQRS] != -1) {
tmpsum = QRS[i+CH*iQRS];
tmpn = 1;
for(int j = 0; j < CH; ++j) {
if(i != j) {
for(int jQRS = 0; jQRS < NQRS[j]; ++jQRS) {
if(QRS[j+CH*jQRS] != -1 && Math.abs(QRS[j+CH*jQRS]-QRS[i+CH*iQRS]) < QRS_RECONCILE ) {
tmpsum = tmpsum + QRS[j+CH*jQRS];
tmpn = tmpn + 1;
QRS[j+CH*jQRS] = -1;
tmpflag[j] = 1;
break;
}
}
}
}
QRS[i+CH*iQRS] = -1;
if(tmpn == 1) {
noisepeaks = noisepeaks + tmpn;
} else if(tmpn > MINLEAD) {
QRSREAL[NQRS_REAL] = (int) Math.round(tmpsum/tmpn);
NQRS_REAL = NQRS_REAL+1;
for(int j = 0; j < CH; ++j) {
if(tmpflag[j] == 1) {
REALQRS_LEAD[j] = REALQRS_LEAD[j] + 1;
}
}
} else {
multinoise = multinoise + tmpn;
}
for(int j = 0; j < CH; ++j) {
tmpflag[j] = 0;
}
}
}
}
// Sort
int n = NQRS_REAL;
while (n > 1) {
int newn = 0;
for(int i = 0; i < n-1; ++i) {
if(QRSREAL[i] > QRSREAL[i+1]) {
int tmp = QRSREAL[i];
QRSREAL[i] = QRSREAL[i+1];
QRSREAL[i+1] = tmp;
newn = i + 1;
}
}
n = newn;
}
// Look for missing beats
double rr = 0;
for(int i = 1; i < NQRS_REAL; ++i) {
rr = rr + QRSREAL[i]-QRSREAL[i-1];
}
rr = rr / (NQRS_REAL-1.0);
boolean update = false;
for(int i = 1; i < NQRS_REAL; ++i) {
int rra = QRSREAL[i]-QRSREAL[i-1];
// Did we really miss ?
if(Math.abs(rra - 2.0*rr) < Math.abs(rra - rr)) {
int start = QRSREAL[i-1] + (int) Math.round(0.39 * Math.sqrt(rr+0.04));
int stop = QRSREAL[i];
// Find by lead
for(int j = 0; j < CH; ++j) {
NQRS[j] = 0;
boolean within = false;
int startw = 0;
int lastloc = 0;
double thr = RMSU3[j]*RMSU3_THR2;
for(int k = start; k < stop; ++k) {
if(data_u3[(k-1)*CH+j] < data_u3[k*CH+j] && data_u3[k*CH+j] > data_u3[(k+1)*CH+j] && data_u3[k*CH+j] > thr) {
if(!within) {
within = true;
startw = j;
lastloc = j;
} else if(data_u3[k+CH*lastloc] < data_u3[k*CH+j]) {
lastloc = j;
}
}
if(within && (j - startw) > U3_N) {
QRS[CH*NQRS[j]+j] = lastloc;
NQRS[j] = NQRS[j] + 1;
break;
} else {
++k;
}
}
}
for(int j = 0; j < CH; ++j) {
tmpflag[j] = 0;
}
tmpsum = 0;
tmpn = 0;
for(int j = 0; j < CH; ++j) {
for(int jQRS = 0; jQRS < NQRS[j]; ++jQRS) {
if(QRS[j+CH*jQRS] != -1) {
tmpsum = QRS[j+CH*jQRS];
tmpn = 1;
for(int k = 0; k < CH; ++k) {
if(j != k) {
for(int kQRS = 0; kQRS < NQRS[k]; ++kQRS) {
if(QRS[k+CH*kQRS] != -1 && Math.abs(QRS[k+CH*kQRS]-QRS[j+CH*jQRS]) < QRS_RECONCILE) {
tmpsum = tmpsum + QRS[k+CH*kQRS];
tmpn = tmpn + 1;
QRS[k+CH*kQRS] = -1;
tmpflag[k] = 1;
break;
}
}
}
}
QRS[j+CH*jQRS] = -1;
if(tmpn == 1) {
noisepeaks = noisepeaks - tmpn;
} else if(tmpn > MINLEAD) {
QRSREAL[NQRS_REAL] = (int) Math.round(tmpsum/tmpn);
NQRS_REAL = NQRS_REAL+1;
update= true;
for(int l = 0; j < CH; ++j) {
if(tmpflag[l] == 1) {
REALQRS_LEAD[l] = REALQRS_LEAD[l] + 1;
}
}
}
}
}
}
}
}
// Re-sort list
if(update) {
n = NQRS_REAL;
while (n > 1) {
int newn = 0;
for(int j = 0; j < n-1; ++j) {
if(QRSREAL[j] > QRSREAL[j+1]) {
int tmp = QRSREAL[j];
QRSREAL[j] = QRSREAL[j+1];
QRSREAL[j+1] = tmp;
newn = j + 1;
}
}
n = newn;
}
}
// Step5: Valid NQRS ?
if(NQRS_REAL < 5 || NQRS_REAL > 37) {
iFile.close();
return BAD;
}
int nonleads = 0;
for(int i = 0; i < CH; ++i) {
if(REALQRS_LEAD[i] < 0.5*NQRS_REAL) {
nonleads = nonleads + 1;
}
}
// Step6: Not too leads that are not contributing ?
if(nonleads > 2) {
iFile.close();
return BAD;
}
// Step7: Detect PQ knots
for(int i = 0; i < NQRS_REAL; ++i) {
allsum = 0;
for(int j = 0; j < CH; ++j) {
int onset = QRSREAL[i];
double thrval = data_u3[j+CH*onset]*ONSETTHR;
int monset = onset;
while(onset > 0 && data_u3[j+CH*onset] > thrval) {
if(data_u3[j+CH*onset] < data_u3[j+CH*onset]) {
monset = onset;
}
if(QRSREAL[i] - onset > MAX_LEFT) {
onset = monset;
break;
}
--onset;
}
allsum = allsum + onset;
}
QRSON[i] = (int) (Math.round(allsum/CH)-LEFT_MOVE);
}
// Step8: Estimate BW noise and filt data
allsum = 0;
for(int i = 0; i < CH; ++i) {
double cursum = 0;
int NP=0;
// Calculate x,y
int ji = 0;
while(ji < NQRS_REAL) {
if(QRSON[ji]-PQWIN > 0 && QRSON[ji] + PQWIN < WIN) {
x[NP] = QRSON[ji];
double tmpsum2 = 0;
for(int k = QRSON[ji]-PQWIN; k <= QRSON[ji]+PQWIN; ++k) {
tmpsum2 = tmpsum2 + data_filt[i+CH*k];
}
y[NP] = tmpsum2 / (PQWIN*2+1);
NP = NP+1;
}
++ji;
}
// Calculate h
for(int j = 0; j < NP-1; ++j) {
h[j] = x[j+1]-x[j];
}
// Calculate A,B,C,D
for(int j = 1; j < NP-1; ++j) {
int k = j - 1;
D[k] = 2 * (h[j-1]+h[j]);
A[k] = h[j+1];
B[k] = h[j];
C[k] = 6 * ((y[j+1]-y[j])/h[j] - (y[j]-y[j-1])/h[j-1]);
}
// Solve matrix
for(int j = 1; j < NP-1; ++j) {
double m = 0;
if(D[j-1] != 0) {
m = B[j] / D[j-1];
}
D[j] = D[j] - m * A[j-1];
C[j] = C[j] - m * C[j-1];
}
if(D[NP-1] == 0) {
C[NP-1] = 0;
} else {
C[NP-1] = C[NP-1] / D[NP-1];
}
for(int j = NP-2; j >= 0; --j) {
if(D[j] == 0) {
C[j] = 0;
} else {
C[j] = (C[j]-A[j]*C[j+1]) / D[j];
}
}
// Calculate S
for(int j = 1; j < NP-2; ++j) {
int k = j - 1;
S[j] = C[k];
}
S[0] = 0;
S[NP-1] = 0;
// Calculate A,B,C,D
for(int j = 0; j < NP-1; ++j) {
A[j] = (S[j+1]-S[j]) / (6*h[j]);
B[j] = S[j]/2.0;
C[j] = (y[j+1]-y[j]) / h[j] - (2.0 * h[j] * S[j] + h[j] * S[j+1])/6.0;
D[j] = y[j];
}
int START = (int) x[0];
int STOP = (int) x[NP-1];
for(int num = 0; num < NP-1; ++num) {
for(int j = (int) x[num]; j < x[num+1]; ++j) {
double bw = A[num] * Math.pow(j-x[num],3) + B[num] * Math.pow(j-x[num],2) + C[num] * (j-x[num]) + D[num];
data_filt[i+CH*j] = data_filt[i+CH*j] - bw;
cursum = cursum + Math.pow(bw,2);
}
}
// Correct BW
allsum = allsum + Math.sqrt(cursum/(STOP-START));
}// End bw correct
double lf_noise = Math.sqrt(allsum / CH);
if(lf_noise > 35) {
iFile.close();
return BAD;
}
// Calculate median beat length
double lengths = 0;
int NBEATS = 0;
for(int i = 1; i < NQRS_REAL-1; ++i) {
if(QRSREAL[i]-PREFIX > 0) {
starts[NBEATS] = QRSREAL[i] - PREFIX;
stops[NBEATS] = QRSREAL[i+1] - POSTFIX;
lengths = lengths + (stops[NBEATS] - starts[NBEATS]);
NBEATS = NBEATS + 1;
}
}
lengths = lengths / (NBEATS);
if(lengths > MAXLENGTH) {
lengths = MAXLENGTH;
}
// Calculate median beat by lead
for(int i = 0; i < CH; ++i) {
// For each sample in median
for(int j = 0; j < lengths; ++j) {
double val = 0;
int n3 = 0;
// For each beat belonging to it
for(int k = 0; k < NBEATS; ++k) {
// Is the sample within ?
if(j < stops[k]-starts[k]) {
int curpos = starts[k]+j;
val = val + data_filt[curpos*CH+i];
n3 = n3 + 1;
}
}
medians[j*CH+i] = val/n3;
}
}
// Calculate RMS / SD in beats with good correlation
double allRMSsum = 0;
double allSDsum = 0;
int allN = 0;
for(int i = 0; i < CH; ++i) {
double ystd = 0;
double ymean = 0;
for(int j = 0; j < lengths; ++j) {
ymean = ymean + medians[j*CH+i];
ystd = ystd + (medians[j*CH+i]*medians[j*CH+i]);
}
for(int j = 0; j < NBEATS; ++j) {
int N = (int) lengths;
if(N > stops[j] - starts[j]) {
N = stops[j] - starts[j];
}
N = N - 1;
double cur_ystd = Math.sqrt((ystd - (ymean*ymean)/N) / (N-1));
double cur_ymean = ymean / N;
double xmean = 0;
double xstd = 0;
double xyprod = 0;
for(int k=0; k < N; ++k) {
xmean = xmean + data_filt[(starts[j]+k)*CH+i];
xstd = xstd + (data_filt[(starts[j]+k)*CH+i]*data_filt[(starts[j]+k)*CH+i]);
xyprod = xyprod + data_filt[(starts[j]+k)*CH+i] * medians[k*CH+i];
}
xstd = Math.sqrt((xstd - (xmean*xmean)/N)/(N-1));
xmean = xmean / N;
double corr = (xyprod - N*xmean*cur_ymean)/((N-1)*xstd*cur_ystd);
if(corr > CORTHR) {
double residuum = data_filt[(starts[j]+0)*CH+i] - medians[0*CH+i];
double resmean = residuum;
double resstd = residuum * residuum;
double diffsum = 0;
for(int k = 1; k < N; ++k) {
double nresiduum = data_filt[(starts[j]+k)*CH+i] - medians[k*CH+i];
resmean = resmean + nresiduum;
resstd = resstd + (nresiduum*nresiduum);
diffsum = diffsum + (nresiduum-residuum)*(nresiduum-residuum);
residuum = nresiduum;
}
resstd = Math.sqrt((resstd-(resmean*resmean)/N)/(N-1));
diffsum = Math.sqrt(diffsum / (N-1));
allRMSsum = allRMSsum + diffsum;
allSDsum = allSDsum + resstd;
allN = allN + 1;
} // End cor
} // End foreach beat
}// End calc RMS/SD
allRMSsum = allRMSsum / allN;
allSDsum = allSDsum / allN;
if(allSDsum > 50 || allRMSsum > 50) {
iFile.close();
return BAD;
}
iFile.close();
return GOOD;
}
}