ecg-data-processor
Version:
A JavaScript library for processing and analyzing ECG data, including noise filtering, PQRST wave detection, and clinical indicator calculations.
1 lines • 18.9 kB
JavaScript
function ecg_data_process_v1(ecgData,frequency,options){function resultConstructor(ecgData,frequency,options){this._option=options;if(options.useDirectData){this._data=ecgData}else{this._data=ecgData.map((val,idx)=>[idx*1e3/frequency,val])}this._frequency=frequency;this._result={summary:{hr:0,st:0,pr:0,qrs:0},segments:[]};this._originalData=ecgData;this._originalFrequency=frequency;this.filterResult=function(result){let resultCopy=JSON.parse(JSON.stringify(result));if(this._option.debug){return resultCopy}for(const segment of resultCopy.segments){delete segment._data;for(const beat of segment.beats){delete beat.p._data;delete beat.q._data;delete beat.r._data;delete beat.s._data;delete beat.t._data;delete beat._data;delete beat._baseline;delete beat._cut_start;delete beat._cut_end}}return resultCopy};this.getResult=function(){return this.filterResult(this._result)};this.getOriginalData=function(){return this._originalData};this.getOriginalFrequency=function(){return this._originalFrequency}}function splitData(ecgData){let data=[];let segments=[];for(let i=0;i<ecgData.length;i++){if(isNaN(ecgData[i][1])||ecgData[i][1]===null){if(data.length>0){segments.push(data);data=[]}}else{data.push(ecgData[i])}}if(data.length>0){segments.push(data)}return segments}function aggregation(data,groupSize=5){let result={max:-65535,min:65535,middle:0,data:[]};let dataArr=data||[];let dataGroup=[];for(let i=0;i<dataArr.length;i++){const voltage=dataArr[i];const prevVoltage=dataArr[i-1]||null;let valid=true;if(typeof prevVoltage==="number"){if(Math.abs(voltage-prevVoltage)>2){valid=false}}if(valid){result.max=Math.max(result.max,voltage);result.min=Math.min(result.min,voltage);dataGroup.push(voltage)}if(dataGroup.length>=groupSize){let first=dataGroup[0],last=dataGroup[dataGroup.length-1],maxVal=Math.max.apply(null,dataGroup),minVal=Math.min.apply(null,dataGroup);if(maxVal>first&&maxVal>last){result.data.push(maxVal);dataGroup=[]}else if(minVal<first&&minVal<last){result.data.push(minVal);dataGroup=[]}else{let sum=0;for(let j=0;j<dataGroup.length;j++){sum+=dataGroup[j]}let avg=sum/dataGroup.length;result.data.push(avg);dataGroup=[]}}}if(result.max<1.5&&result.min>-1.5){result.middle=0}else{result.middle=(result.max+result.min)/2;for(let i=0;i<result.data.length;i++){let value=result.data[i];result.data[i]=value-result.middle}}return result.data}function smoothSignal(data,windowSize=5){return data.map((val,idx)=>{const start=Math.max(0,idx-Math.floor(windowSize/2));const end=Math.min(data.length,idx+Math.ceil(windowSize/2));const window=data.slice(start,end);return[val[0],window.reduce((sum,val)=>sum+val[1],0)/window.length]})}function medianFilter(data,windowSize=20){const result=[];const halfWindow=Math.floor(windowSize/2);for(let i=0;i<data.length;i++){const window=[];for(let j=Math.max(0,i-halfWindow);j<=Math.min(data.length-1,i+halfWindow);j++){window.push(data[j][1])}window.sort((a,b)=>a-b);result.push([data[i][0],window[Math.floor(window.length/2)]])}return result}function meanFilter(data,windowSize=20){const result=[];const halfWindow=Math.floor(windowSize/2);for(let i=0;i<data.length;i++){let sum=0;let count=0;for(let j=Math.max(0,i-halfWindow);j<=Math.min(data.length-1,i+halfWindow);j++){sum+=data[j][1];count++}result.push([data[i][0],sum/count])}return result}function lowPassFilter(data,alpha=.15){const result=[];let previous=data[0][1];for(let i=0;i<data.length;i++){let filtered=alpha*data[i][1]+(1-alpha)*previous;result.push([data[i][0],filtered]);previous=filtered}return result}function findPeaks(ecgData,direction=-1,minDistance=10,minPeakHeight=.1){let peaks=[];let through=null;if(direction<0){for(let i=ecgData.length-2;i>=1;i--){if(typeof through!=="number"){through=ecgData[i][1]}else{through=Math.min(through,ecgData[i][1])}if(ecgData[i][1]>ecgData[i+1][1]){let j=i-1;while(j>=0&&ecgData[j][1]===ecgData[i][1]){j--}if(j>=0&&ecgData[j][1]<ecgData[i][1]){let peak=Math.round((i+j)/2);let peakValue=ecgData[peak][1];if(peakValue-through>=minPeakHeight){if(peaks.length>0){if(Math.abs(peak-peaks[peaks.length-1])>minDistance){peaks.push(peak);through=null}}else{peaks.push(peak);through=null}}}}}}else{for(let i=1;i<ecgData.length-1;i++){if(typeof through!=="number"){through=ecgData[i][1]}else{through=Math.min(through,ecgData[i][1])}if(ecgData[i][1]>ecgData[i-1][1]){let j=i+1;while(j<ecgData.length&&ecgData[j][1]===ecgData[i][1]){j++}if(j<ecgData.length&&ecgData[j][1]<ecgData[i][1]){let peak=Math.round((i+j)/2);let peakValue=ecgData[peak][1];if(peakValue-through>=minPeakHeight){if(peaks.length>0){if(Math.abs(peak-peaks[peaks.length-1])>minDistance){peaks.push(peak);through=null}}else{peaks.push(peak);through=null}}}}}}return peaks}function calculateSlope(signal){const slope=[];for(let i=1;i<signal.length;i++){slope.push(signal[i][1]-signal[i-1][1])}return slope}function getRPeaks(seg,options){let rPeaks=[];let smoothed=smoothSignal(seg,options.smoothWindowSize);const slope=calculateSlope(smoothed);let isPositiveSlope=false;for(let i=1;i<slope.length;i++){if(slope[i]>options.rPeakSlopeThreshold&&!isPositiveSlope){isPositiveSlope=true}else if(slope[i]<-options.rPeakSlopeThreshold&&isPositiveSlope){isPositiveSlope=false;let j=i;while(j>0&&smoothed[j][1]<smoothed[j-1][1]){j--}if(rPeaks.length===0||j-rPeaks[rPeaks.length-1][0]>options.rPeakMinDistance){rPeaks.push([j,seg[j]])}}}return rPeaks.map(val=>val[1])}function getBeats(rPeaks,seg,frequency,options){let beats=[];let lastBeat=null;for(let i=0;i<rPeaks.length;i++){let peak=rPeaks[i];let start=i===0?0:rPeaks[i-1][0];if(lastBeat&&lastBeat.t.end_time>0){start=lastBeat.t.end_time}let end=rPeaks[i+1]?rPeaks[i+1][0]:-1;let left=seg.filter(val=>val[0]>=start&&val[0]<peak[0]);let right=seg.filter(val=>val[0]>peak[0]&&(end>0&&val[0]<=end||end<0));let leftSlope=calculateSlope(left);let rightSlope=calculateSlope(right);let qWaveStartIndex=-1;for(let j=leftSlope.length-1;j>=0;j--){if(Math.abs(leftSlope[j])*frequency<2.5){qWaveStartIndex=j;break}}let leftStartIdx=0;let smoothLeft=smoothSignal(left,options.smoothWindowSize);let leftPeaks=findPeaks(smoothLeft,1,options.throughMinDistance,options.minPWaveHeight);if(leftPeaks.length>0){for(let j=leftPeaks[0];j>0;j--){let slope=(smoothLeft[j][1]-smoothLeft[j-1][1])*frequency;if(slope>=-.05&&Math.abs(slope)<2.5){leftStartIdx=j;break}}}if(leftStartIdx>0){left=left.slice(leftStartIdx);qWaveStartIndex=qWaveStartIndex-leftStartIdx}let smoothRight=smoothSignal(right,options.smoothWindowSize);let rightEndIdx=-1;let rWaveEndIdx=-1;for(let j=0;j<rightSlope.length;j++){if(Math.abs(rightSlope[j])*frequency<2.5){rWaveEndIdx=j;break}}let rightPeaks=findPeaks(smoothRight,1,options.throughMinDistance,options.minTWaveHeight);let tWaveEndTime=-1;if(rightPeaks.length>0){let peakIdx=0;while(rightPeaks[peakIdx]&&rightPeaks[peakIdx]<rWaveEndIdx){peakIdx++}for(let j=rightPeaks[peakIdx];j<right.length-1;j++){let slope=(smoothRight[j+1][1]-smoothRight[j][1])*frequency;if(slope>=-.05&&Math.abs(slope)<2.5){rightEndIdx=j;tWaveEndTime=right[j][0];break}}}if(rightEndIdx>0){right=right.slice(0,rightEndIdx)}else{tWaveEndTime=right[right.length-1][0]}let beat={_data:[].concat(left||[],[peak],right||[]),valid:false,start_time:left&&left.length?left[0][0]:null,end_time:right.length>0?right[right.length-1][0]:null,p:{start_time:left.length>0?left[0][0]:0,end_time:0,peak_time:0,peak_voltage:0},q:{start_time:qWaveStartIndex>0?left[qWaveStartIndex-1][0]:0,end_time:0,peak_time:0,peak_voltage:0},r:{start_time:0,end_time:0,peak_time:peak[0],peak_voltage:peak[1]},s:{start_time:0,end_time:rWaveEndIdx>=0&&right[rWaveEndIdx+1]?right[rWaveEndIdx+1][0]:0,peak_time:0,peak_voltage:0},t:{start_time:0,end_time:tWaveEndTime>=0?tWaveEndTime:0,peak_time:0,peak_voltage:0}};beats.push(beat);lastBeat=beat}return beats}function updateWaves(beats,options){for(let beat of beats){let rawData=beat._data;let extendedRawData=Array.from(rawData);const extension=20;for(let i=0;i<extension;i++){extendedRawData.unshift(rawData[0]);extendedRawData.push(rawData[rawData.length-1])}let smooth=smoothSignal(extendedRawData,options.smoothWindowSize);let lastVal=null;let baseData=smooth.map(val=>{if(val[0]>=beat.q.start_time&&val[0]<=beat.s.end_time){return[val[0],lastVal]}else{if(typeof lastVal!=="number"){lastVal=val[1]}return val}});let baseline,baselineWithoutQRS;if(options.baselineFilter==="LOWPASS"){baselineWithoutQRS=lowPassFilter(baseData,options.baselineFilterOptions?.alpha);baseline=lowPassFilter(smooth,options.baselineFilterOptions?.alpha)}else if(options.baselineFilter==="MEAN"){baselineWithoutQRS=meanFilter(baseData,options.baselineFilterOptions?.windowSize);baseline=meanFilter(smooth,options.baselineFilterOptions?.windowSize)}else if(options.baselineFilter==="MEDIAN"){baselineWithoutQRS=medianFilter(baseData,options.baselineFilterOptions?.windowSize);baseline=medianFilter(smooth,options.baselineFilterOptions?.windowSize)}else{throw new Error("Invalid baseline filter",typeof options.baselineFilter,options.baselineFilter,"is not a valid filter function")}smooth.splice(0,extension);smooth.splice(smooth.length-extension,extension);baseline.splice(0,extension);baseline.splice(baseline.length-extension,extension);baselineWithoutQRS.splice(0,extension);baselineWithoutQRS.splice(baselineWithoutQRS.length-extension,extension);beat._baseline=baseline;beat._baselineWithoutQRS=baselineWithoutQRS;let rPeakIndex=rawData.findIndex(val=>val[0]===beat.r.peak_time);let left=rawData.slice(0,rPeakIndex+1);let right=rawData.slice(rPeakIndex);let smoothLeft=smooth.slice(0,rPeakIndex+1);let smoothRight=smooth.slice(rPeakIndex);let qWaveStartIndex=left.findIndex(val=>val[0]===beat.q.start_time);let sWaveEndIndex=right.findIndex(val=>val[0]===beat.s.end_time);let baselineLeft=[];let baselineRight=[];let baselineWithoutQRSLeft=[];let baselineWithoutQRSRight=[];baselineLeft=baseline.slice(0,rPeakIndex+1);baselineRight=baseline.slice(rPeakIndex);baselineWithoutQRSLeft=baselineWithoutQRS.slice(0,rPeakIndex+1);baselineWithoutQRSRight=baselineWithoutQRS.slice(rPeakIndex);const threshold=1/30;let pWaveEndIndex=-1,pWaveStartIndex=-1,pWaveProbablyStartIndex=rawData.findIndex(val=>val[0]===beat.p.start_time);for(let i=pWaveProbablyStartIndex;i<qWaveStartIndex;i++){if(smoothLeft[i]&&smoothLeft[i-1]&&smoothLeft[i][1]>smoothLeft[i-1][1]&&smoothLeft[i-1][1]<=baselineLeft[i-1][1]+threshold&&smoothLeft[i][1]>=baselineLeft[i][1]+threshold){pWaveStartIndex=i;break}}for(let i=qWaveStartIndex;i>0;i--){if(smoothLeft[i]&&smoothLeft[i+1]&&smoothLeft[i][1]>smoothLeft[i+1][1]){if(smoothLeft[i][1]>=baselineLeft[i][1]&&smoothLeft[i][1]<=baselineWithoutQRSLeft[i][1]+threshold&&smoothLeft[i+1][1]<=baselineWithoutQRSLeft[i+1][1]+threshold){pWaveEndIndex=i;break}}}options.debug&&console.log("pWaveStartIndex:",pWaveStartIndex,"pWaveEndIndex:",pWaveEndIndex);if(pWaveEndIndex>=0&&pWaveStartIndex>=0&&pWaveStartIndex<pWaveEndIndex){beat.p.start_time=left[pWaveStartIndex][0];beat.p.end_time=left[pWaveEndIndex][0];beat.p._data=left.slice(pWaveStartIndex,pWaveEndIndex);let pWavePeakValue=Math.max.apply(null,smoothLeft.slice(pWaveStartIndex,pWaveEndIndex).map(val=>val[1]));let pWavePeakIndex=smoothLeft.findIndex(val=>val[1]===pWavePeakValue);options.debug&&console.log("pWavePeakIndex:",pWavePeakIndex,pWavePeakValue,smoothLeft.slice(pWaveStartIndex,pWaveEndIndex).map(val=>val[1]));beat.p.peak_time=left[pWavePeakIndex][0];beat.p.peak_voltage=left[pWavePeakIndex][1]}let qWavePeakIndex=-1;let qWaveEndIndex=-1;for(let i=smoothLeft.length-1;i>=qWaveStartIndex;i--){if(smoothLeft[i]&&smoothLeft[i+1]&&smoothLeft[i][1]<smoothLeft[i+1][1]){if(smoothLeft[i][1]<=baselineLeft[i][1]+threshold&&smoothLeft[i+1][1]>=baselineLeft[i+1][1]+threshold){qWaveEndIndex=i;break}}}let qWaveThroughValue=Math.min.apply(null,smoothLeft.slice(qWaveStartIndex,qWaveEndIndex).map(val=>val[1]));qWavePeakIndex=smoothLeft.findLastIndex(val=>val[1]===qWaveThroughValue);options.debug&&console.log("qWaveStartIndex:",qWaveStartIndex,"qWavePeakIndex:",qWavePeakIndex,"qWaveEndIndex:",qWaveEndIndex);if(qWaveEndIndex>=0&&qWaveStartIndex>=0&&qWaveStartIndex<=qWavePeakIndex){beat.q.start_time=left[qWaveStartIndex][0];beat.q.end_time=left[qWaveEndIndex][0];beat.q._data=left.slice(qWaveStartIndex,qWaveEndIndex);beat.q.peak_time=left[qWavePeakIndex][0];beat.q.peak_voltage=left[qWavePeakIndex][1]}let rWaveStartIndex=qWaveEndIndex;let rWaveEndIndex=-1;for(let i=1;i<sWaveEndIndex;i++){console.log("rWaveEndIndex:",i,smoothRight[i][1],smoothRight[i-1][1],baselineRight[i][1],threshold,baselineRight[i-1][1],baselineWithoutQRSRight[i][1],baselineWithoutQRSRight[i-1][1]);console.log("beat",beat);if(smoothRight[i]&&smoothRight[i-1]&&smoothRight[i-1][1]>=smoothRight[i][1]&&smoothRight[i][1]<=baselineRight[i][1]+threshold){rWaveEndIndex=i;break}}options.debug&&console.log("rWaveStartIndex:",rWaveStartIndex,"rWaveEndIndex:",rWaveEndIndex);if(rWaveStartIndex>=0&&rWaveEndIndex>=0&&rWaveEndIndex<sWaveEndIndex){beat.r.start_time=left[rWaveStartIndex][0];beat.r.end_time=right[rWaveEndIndex][0];beat.r._data=[].concat(left.slice(rWaveStartIndex),right.slice(0,rWaveEndIndex))}let sWaveStartIndex=rWaveEndIndex;let sWavePeakIndex=-1;let sWaveThroughValue=Math.min.apply(null,smoothRight.slice(sWaveStartIndex,sWaveEndIndex).map(val=>val[1]));sWavePeakIndex=smoothRight.findIndex(val=>val[1]===sWaveThroughValue);options.debug&&console.log("sWaveStartIndex:",sWaveStartIndex,"sWavePeakIndex:",sWavePeakIndex,"sWaveEndIndex:",sWaveEndIndex);if(sWaveStartIndex>=0&&sWaveEndIndex>=0&&sWavePeakIndex>=0&&sWaveEndIndex>=sWavePeakIndex){beat.s.start_time=right[sWaveStartIndex][0];beat.s.end_time=right[sWaveEndIndex][0];beat.s._data=right.slice(sWaveStartIndex,sWaveEndIndex);beat.s.peak_time=right[sWavePeakIndex][0];beat.s.peak_voltage=right[sWavePeakIndex][1]}let tWaveStartIndex=-1;let tWavePeakIndex=null;let tWaveEndIndex=right.findIndex(val=>val[0]===beat.t.end_time);if(tWaveEndIndex<0){tWaveEndIndex=right.length-1}for(let i=sWaveEndIndex;i<right.length;i++){if(smoothRight[i]&&smoothRight[i-1]&&smoothRight[i][1]>smoothRight[i-1][1]&&smoothRight[i][1]>=baselineWithoutQRSRight[i][1]+threshold){tWaveStartIndex=i;break}}options.debug&&console.log("tWaveStartIndex:",tWaveStartIndex,"tWavePeakIndex:",tWavePeakIndex,"tWaveEndIndex:",tWaveEndIndex,beat.t.end_time,smoothRight,baselineRight);if(tWaveStartIndex>=0&&tWaveEndIndex>=0&&tWaveStartIndex<tWaveEndIndex){beat.t.start_time=right[tWaveStartIndex][0];beat.t.end_time=right[tWaveEndIndex][0];beat.t._data=right.slice(tWaveStartIndex,tWaveEndIndex);let tWavePeakValue=Math.max.apply(null,right.slice(tWaveStartIndex,tWaveEndIndex).map(val=>val[1]));let tWavePeakIndex=right.findLastIndex(val=>val[1]>=tWavePeakValue);options.debug&&console.log("tWavePeakIndex:",tWavePeakIndex,tWavePeakValue,right);beat.t.peak_time=right[tWavePeakIndex][0];beat.t.peak_voltage=right[tWavePeakIndex][1]}}return beats}function testBeats(beats){for(let beat of beats){let p,q,r,s,t;p=beat.p.start_time>=0&&beat.p.end_time>=0&&beat.p.peak_time>0;q=beat.q.start_time>=0&&beat.q.end_time>=0&&beat.q.peak_time>0;r=beat.r.start_time>=0&&beat.r.end_time>=0&&beat.r.peak_time>0;s=beat.s.start_time>=0&&beat.s.end_time>=0&&beat.s.peak_time>0;t=beat.t.start_time>=0&&beat.t.end_time>=0&&beat.t.peak_time>0;beat.valid=p&&q&&r&&s&&t}}function getSummary(beats,frequency=100,options){let summary={hr:null,st:0,pr:0,qrs:0};if(beats.length>=2){let duration=beats[beats.length-1].r.peak_time-beats[0].r.peak_time;if(duration>0){summary.hr_duration=duration;if(options.useDirectData){summary.hr_duration=duration*(1e3/frequency)}summary.hr=Math.round(60/(duration/1e3/(beats.length-1)))}summary.hr_beats=beats.length-1}let st=[],pr=[],qrs=[];let validBeats=0;for(let beat of beats){if(beat.valid){let stValid=false;if(beat.s.end_time>0){let jPointIdx=beat._data.findIndex(val=>val[0]===beat.s.end_time);let tStartIdx=beat._data.findIndex(val=>val[0]===beat.t.start_time);let jPointVoltage=beat._data[jPointIdx][1];if(tStartIdx>jPointIdx+1&&beat._data[jPointIdx+1]&&beat._data[jPointIdx+1][1]>jPointVoltage){jPointVoltage=beat._data[jPointIdx+1][1]}let _80msAfterJPoint=Math.min(jPointIdx+Math.floor(.08*frequency),tStartIdx);if(beat._data[_80msAfterJPoint]){let _80msVoltage=beat._data[_80msAfterJPoint][1];st.push(_80msVoltage-jPointVoltage);stValid=true}}if(stValid){validBeats++;let pr_time=beat.r.start_time-beat.p.start_time;let qrs_time=beat.s.end_time-beat.q.start_time;if(options.useDirectData){pr_time=pr_time*1e3/frequency;qrs_time=qrs_time*1e3/frequency}pr.push(pr_time);qrs.push(qrs_time)}else{beat.valid=false}}}if(st.length>0){summary.st=st.reduce((a,b)=>a+b)/st.length}else{summary.st=null}if(pr.length>0){summary.pr=pr.reduce((a,b)=>a+b)/pr.length}if(qrs.length>0){summary.qrs=qrs.reduce((a,b)=>a+b)/qrs.length}summary.validBeats=validBeats;return summary}function getSummaryFromSegments(segments){let summary={hr:null,st:0,pr:0,qrs:0};let hrSum=0,hrBeats=0;let stSum=0,prSum=0,qrsSum=0;let validBeats=0,stBeats=0;for(let seg of segments){if(seg.summary.hr_duration){hrSum+=seg.summary.hr_duration;hrBeats+=seg.summary.hr_beats}if(typeof seg.summary.st==="number"){stSum+=seg.summary.st*seg.summary.validBeats;stBeats++}if(seg.summary.pr){prSum+=seg.summary.pr*seg.summary.validBeats}if(seg.summary.qrs){qrsSum+=seg.summary.qrs*seg.summary.validBeats}validBeats+=seg.summary.validBeats}if(hrBeats>0){summary.hr=Math.round(60/(hrSum/1e3/hrBeats))}if(validBeats>0){summary.pr=prSum/validBeats;summary.qrs=qrsSum/validBeats}if(stBeats>0){summary.st=stSum/stBeats}else{summary.st=null}return summary}options=Object.assign({debug:false,aggregation:null,rPeakSlopeThreshold:.05,rPeakMinDistance:30,smoothWindowSize:5,baselineFilter:"LOWPASS",baselineFilterOptions:{alpha:.05,windowSize:20},throughMinDistance:10,minPWaveHeight:0,minTWaveHeight:.1,useDirectData:false},options||{});frequency=frequency||100;let aggregationRatio=options.aggregation||Math.floor(frequency/100);if(!options.useDirectData){ecgData=aggregation(ecgData,aggregationRatio);frequency=frequency/aggregationRatio}let resultObj=new resultConstructor(ecgData,frequency,options);ecgData=resultObj._data;let segments=splitData(ecgData);for(let seg of segments){let rPeaks=getRPeaks(seg,options);let beats=getBeats(rPeaks,seg,frequency,options);updateWaves(beats,options);testBeats(beats);let summary=getSummary(beats,frequency,options);resultObj._result.segments.push({start_time:seg[0][0],end_time:seg[seg.length-1][0],summary:summary,beats:beats})}resultObj._result.summary=getSummaryFromSegments(resultObj._result.segments);return resultObj}try{module.exports={filters:{LOWPASS:"LOWPASS",MEAN:"MEAN",MEDIAN:"MEDIAN"},process:ecg_data_process_v1}}catch(e){console.error("module.exports is not defined");if(typeof global==="undefined"){var global=window}global.ecg_data_process={filters:{LOWPASS:"LOWPASS",MEAN:"MEAN",MEDIAN:"MEDIAN"},process:ecg_data_process_v1}}