7:56:59 pm on 5/17/12

Menu
» Home
» About Scott
» VD Labs
» QRSS VD
» Old Stuff
» Archive
» Publications
» Contact

Categories
» C/C++
» Circuitry
» DIY ECG
» General
» high altitude balloon
» Linux
» Microcontrollers
» Molecular Biology
» My Website
» PHP
» Prime Numbers
» Python
» Radio
» UCF Lab
» Everything
» RF Links

Writings
» MD Labels
» Streamrip
» AIM Thoughts
» WindowsXP?
» Partitioning
» CD/DVD Repair
» Monitor Info
» CRT Deflection
» Venomcrack
» Flash Thing
» Heart/Brain
» Diabetes
» Triops
» Biomed

Friends
» Mike
» Fred
» Kyle W
» Nick
» Louis
» Tom
» Kyle H




Archives
» August 2011
» July 2011
» June 2011
» March 2011
» February 2011
» January 2011
» December 2010
» November 2010
» September 2010
» August 2010
» July 2010
» June 2010
» May 2010
» April 2010
» March 2010
» February 2010
» January 2010
» December 2009
» September 2009
» August 2009
» July 2009
» June 2009
» May 2009
» April 2009
» March 2009
» February 2009
» January 2009
» December 2008
» November 2008
» October 2008
» September 2008
» September 2007
» December 2006
» August 2006
» January 2006
» August 2005
» July 2005
» June 2005
» May 2005
» April 2005
» March 2005
» February 2005
» January 2005
» December 2004
» November 2004
» October 2004
» September 2004
» August 2004
» July 2004
» June 2004
» May 2004
» April 2004
» March 2004
» February 2004
» January 2004
» December 2003
» November 2003
» October 2003
» September 2003
» August 2003
» July 2003
» June 2003
» May 2003
» April 2003
» March 2003
» February 2003
» January 2003
» December 2002
» November 2002
» October 2002
» September 2002
» June 2001

You are currently browsing the archives for the DIY ECG category.

Archive for the 'DIY ECG' Category

« Previous Entries
Next Entries »


DIY ECG Improvements
Posted by
Scott January 20th, 2009 | 5,253 words | 2 Comments »


Scott was 23.32 years old when he wrote this!

No 3-day weekend would be complete without a project that’s, well, virtually useless. I present to you my new and improved ECG machine! Instead of using a single op-amp circuit like the previous entries which gave me decent but staticky traces, I decided to build a more advanced ECG circuit documented by Jason Nguyen which boasted 6 op amps! (I’d only been using one) Luckily I picked up a couple LM 324 quad op amp chips at radioshack for about $1.40 each, so I had everything I needed. I’ll skip to the results. In short, they’re gorgeous. Noise is almost nothing, so true details of the trace are visible. I can now clearly see the P-Q-R-S-T features in the wave (before the P was invisible). I’ll detail how I did this in a later entry. For now, here are some photos of the little device and a video I uploaded to YouTube. It’s not fancy.

UPDATE: Upon analyzing ~20 minutes of heartbeat data I found a peculiarity. Technically this could be some kind of noise (a ‘pop’ in the microphone signal due to the shuffling of wires or a momentary disconnect from the electrodes or perhaps even a static shock to my body from something), but because this peculiarity happened only once in 20 minutes I’m not ruling out the possibility that this is the first irregular heartbeat I captured with my DIY ECG. Note that single-beat irregularities are common, and that this does not alarm me so much as fascinates me. Below is the section of the data which contains this irregular beat.



DIY ECG Progress
Posted by
Scott January 16th, 2009 | 5,253 words | No Comments »


Scott was 23.31 years old when he wrote this!

Last night I finished building my DIY ECG as a prototype (I finally got the circuit off the breadboard and onto a plastic sheet). This is a similar circuit to the one used to record data from the last entry (resister values are now identical to the crude circuit described several posts ago). I left-in the crude band-pass filter (made by grounding my primary electrode sensor through a 0.1µF capacitor) because it seemed to help a great deal, and wasn’t hard to implement. I picked up all of my parts (including the LF324 quad-op-amp microchip) at RadioShack. Of note, the quad-op-amp is overkill because I’m only using one of the 4 op-amps. Theoretically I could add 3 more electrodes to this circuit (which would allow for multi-sensor recording, similar to the electrodes placed at http://en.wikipedia.org/wiki/File:Limb_leads.svg) but this would require multiple microphone jacks, which isn’t very common. I guess I could use 2 microphone jacks, and differentiate right/left channels. Anyway, here are some photos.


I made the prototype by drilling holes in a small rectangular piece of a non-conductive plastic-fiberish material. (I picked up a stack of these rectangular sections for a quarter at a local electrical surplus store and they’re perfect for prototyping). The two green wires coming out the left side attach to a ~5v power supply (either a plugged in AC->DC transformer, 3 or 4 AA batteries, or even a 9V should work). The blue wires on the right attach to the electrodes I put on my chest. The black wires go to a headphone-jack which I plug into the microphone hole of my PC to record the signal.


This is the back of the device which shows my crummy soldering technique. Let it slide folks, I’m a molecular biology not an electrical engineer. Anyhow, basic point-to-point contacts, nothing special. The white/yellow wires correspond to the left/right channels of the microphone connector. I only use the left one (white), but attached the right channel (yellow) to the op-amp just in case I decide to add another sensor later – this is not required.


Here’s the full shabang! You can clearly see the circuit (note its small size – easy to mount inside of a tictac box or something) with the green wires leading to a power supply, black cable being the microphone connector, and the blue wires leading to electrodes made… from… fanta… cans…? Yes, in the spirit of ghetto-rigging electronics (my specialty) I found that surprisingly nice chest electrodes can be made from aluminum soda cans! If you go this route, cut them delicately so you don’t get metal shards in your skin like I did at first. Also, note that you have to firmly scrape each side of the aluminum to get the insulating waxy-plastic stuff off or it just won’t work. (I guess it’s coated with something to prevent the soda from tasting like metal.) Aluminum rapidly transfers heat, so it’s nearly impossible to solder leads onto these pads, so I wrapped a wire (tipped with a bead of solder) with the edge of the aluminum several times and crushed the heck out of it with pliers and it seems to stay on well and make a good connection. Also, before taping these onto your skin, you have to put a highly-conductive goo on it to make the connection better. I chose to use a copious squirt of my wife’s skin moisturizer on each electrode (shh, don’t tell her!) and taped the gooey electrode directly onto my chest.

I recorded ~20 minutes of data last night with this rig and it looked pretty good. I went to analyze it with Python and it kept crashing! The python script I gave you previously loads the entire wave file into an array of numbers, but with a 20 minute wave file (at over 40,000 samples a second) this is just too big for memory. I wrote an updated wave loader function which loads large wave files in parts which is much more efficient. It also performs the condensation method at load time. Basically, it loads 100 data points (or whatever deg is set to), averages them, and adds this value to a list. The result is a smoothed trace with a resolution of ~400Hz instead of ~40,000Hz. I’d apply it to my wave file I recorded last night but it’s on my laptop which is in the car and I’ve got to get back to work. Here’s that function:

 def loadWav(fname,deg=100):  
     global hz  
     w=wave.open(fname)  
     nchannel, width, rate, length, comptype, compname = w.getparams()  
     print "[%s] 
 rate: %d Hz 
 frames: %d 
 length: %.02f sec" %  
           (fname, rate, length, float(length)/rate)  
     hz=rate/deg  
     chunks=int(length/deg)  
     data=[]  
     for i in range(chunks):  
         if i%7777==0:  
             print "processing chunk %d of %d (%0.02f%%)" %  
                   (i,chunks,100.0*i/chunks)  
         vals = struct.unpack("%sh" %deg,w.readframes(deg))  
         data.append(sum(vals)/float(deg))  
     print "complete!"  
     return data  
 


Circuits vs. Software
Posted by
Scott January 15th, 2009 | 5,253 words | 1 Comment »


Scott was 23.31 years old when he wrote this!

Would I rather design circuits or software? I’m a software guy (likely due to my lack of working knowledge of circuits) so I’d rather record noisy signals and write software to eliminate the noise, rather than assembling circuits to eliminate the noise for me. In the case of my DIY ECG machine, I’d say I’ve done a great job of eliminating noise via the software route. Most DIY ECG circuits on the net use multiple op-amps and diodes to do this, and have a hardware-based band-pass filter to eliminate frequencies around 60 Hz. Instead of all that fancy stuff, I made a super-crude circuit (a single op-amp and two resisters) to record my ECG. It was INCREDIBLY noisy! So, how did I clean it up with software? I’ll tell you.

The first step in removing electrical noise is classifying it. Most of the noise in my signal were overlapping sine waves caused by my electrodes picking up signals not from my body. This was determined by simply close-up observation of the original trace. Since this sine-type interference is consistant, power-spectral analysis could be applied to determine the frequencies of the noise so I could block them out. I used the fast Fourier transform algorithm (FFT) on the values from my wave file to generate a plot of noise level with respect to frequency (noise was seen as sharp peaks). I manually blocked-out certain regions of the FFT trace that I thought were noise-related (colored bands, all the values of which were set to zero) – a process known as band-pass filtering (something which is possible to do electronically with a more complicated circuit) – then performed an inverse FFT algorithm on the trace. The result was a trace with greatly reduced noise and after a moving window smoothing algorithm was applied the signal was better than ever. Below are some figures. Note that I recorded the raw WAV file with “sound recorder” (not GoldWave) and did all of the processing (including band-pass filtering) within Python.

What an awesome chart! On top we have the power spectral analysis with band-stop filters applied at the colored regions. Below is the trace of the original WAV file (light gray), the inverse-FFT-filtered trace (dark gray), and the smoothed inverse-FFT-filtered trace (black) – the final ECG signal I intend to use.

This is a magnified view of a few heartbeats after the inverse FFT filtering (multi-band-blocking) process was applied. Not bad! Not bad at all…

And, of course, the updated code:

 import wave, struct, numpy, pylab, scipy  

 fname='./success3.wav'  

 def readwave(wavfilename):  
     """load raw data directly from a WAV file."""  
     global rate  
     w=wave.open(wavfilename,'rb')  
     (nchannel, width, rate, length, comptype, compname) = w.getparams()  
     print "[%s] %d HZ (%0.2fsec)" %(wavfilename, rate, length/float(rate))  
     frames = w.readframes(length)  
     return numpy.array(struct.unpack("%sh" %length*nchannel,frames))  

 def shrink(data,deg=100):  
     """condense a linear data array by a multiple of [deg]."""  
     global rate  
     small=[]  
     print "starting with", len(data)  
     for i in range(len(data)/deg):  
         small.append(numpy.average(data[i*deg:(i+1)*deg]))  
     print "ending with", len(small)  
     rate = rate/deg  
     #return small[40000:50000]  
     return small  

 def normalize(data):  
     """make all data fit between -.5 and +.5"""  
     data=data-numpy.average(data)  
     big=float(max(data))  
     sml=float(min(data))  
     data=data/abs(big-sml)  
     data=data+float(abs(min(data)))-.47  
     return data  

 def smooth(data,deg=20,expand=False):  
     """moving window average (deg = window size)."""  
     for i in range(len(data)-deg):  
         if i==0: cur,smooth=sum(data[0:deg]),[]  
         smooth.append(cur/deg)  
         cur=cur-data[i]+data[i+deg]  
     if expand:  
         for i in range(deg):  
             smooth.append(smooth[-1])  
     return smooth  

 def smoothListGaussian(list,degree=10,expand=False):  
     window=degree*2-1  
     weight=numpy.array([1.0]*window)  
     weightGauss=[]  
     for i in range(window):  
         i=i-degree+1  
         frac=i/float(window)  
         gauss=1/(numpy.exp((4*(frac))**2))  
         weightGauss.append(gauss)  
     weight=numpy.array(weightGauss)*weight  
     smoothed=[0.0]*(len(list)-window)  
     for i in range(len(smoothed)):  
         smoothed[i]=sum(numpy.array(list[i:i+window])*weight)/sum(weight)  
     if expand:  
         for i in range((degree*2)-1):  
             smoothed.append(smoothed[-1])  
     return smoothed  

 def goodSmooth(data):  
     #data=smooth(fix,20,True)  
     data=smooth(fix,100,True)  
     #data=smooth(fix,20,True)  
     return data    

 def makeabs(data):  
     """center linear data to its average value."""  
     for i in range(len(data)): data[i]=abs(data[i])  
     return data  

 def invert(data):  
     """obviously."""  
     for i in range(len(data)): data[i]=-data[i]  
     return data  

 def loadwav(fname):  
     """a do-everything function to get usable, smoothed data from a WAV."""  
     wav=readwave(fname)  
     wav=shrink(wav)  
     wav=invert(wav)  
     wav=smooth(wav)  
     wav=smooth(wav,10)  
     wav=normalize(wav)  
     Xs=getXs(wav)  
     return Xs,wav  

 def getXs(datalen):  
     """calculate time positions based on WAV frequency resolution."""  
     Xs=[]  
     for i in range(len(datalen)):  
         Xs.append(i*(1/float(rate)))  
     print len(datalen), len(Xs)  
     return Xs  

 def integrate(data):  
     """integrate the function with respect to its order."""  
     inte=[]  
     for i in range(len(data)-1):  
         inte.append(abs(data[i]-data[i+1]))  
     inte.append(inte[-1])  
     return inte  

 def getPoints(Xs,data,res=10):  
     """return X,Y coordinates of R peaks and calculate R-R based heartrate."""  
     pXs,pYs,pHRs=[],[],[]  
     for i in range(res,len(data)-res):  
         if data[i]>data[i-res]+.1 and data[i]>data[i+res]+.1:  
             if data[i]>data[i-1] and data[i]>data[i+1]:  
                 pXs.append(Xs[i])  
                 pYs.append(data[i])  
                 if len(pXs)>1:   
                     pHRs.append((1.0/(pXs[-1]-pXs[-2]))*60.0)  
     pHRs.append(pHRs[-1])  
     return pXs,pYs,pHRs  

 def bandStop(fft,fftx,low,high,show=True):  
     lbl="%d-%d"%(low,high)  
     print "band-stopping:",lbl  
     if show:  
         col=pylab.cm.spectral(low/1200.)  
         pylab.axvspan(low,high,alpha=.4,ec='none',label=lbl,fc=col)  
         #pylab.axvspan(-low,-high,fc='r',alpha=.3)  
     for i in range(len(fft)):  
         if abs(fftx[i])>low and abs(fftx[i])<high :  
             fft[i]=0  
     return fft  

 def getXs(data):  
     xs=numpy.array(range(len(data)))  
     xs=xs*(1.0/rate)  
     return xs  

 def clip(x,deg=1000):  
     return numpy.array(x[deg:-deg])  

 pylab.figure(figsize=(12,8))  
 raw = invert(shrink(readwave(fname),10))  
 xs = getXs(raw)  
 fftr = numpy.fft.fft(raw)  
 fft = fftr[:]  
 fftx= numpy.fft.fftfreq(len(raw), d=(1.0/(rate)))  

 pylab.subplot(2,1,1)  
 pylab.plot(fftx,abs(fftr),'k')  

 fft=bandStop(fft,fftx,30,123)  
 fft=bandStop(fft,fftx,160,184)  
 fft=bandStop(fft,fftx,294,303)  
 fft=bandStop(fft,fftx,386,423)  
 fft=bandStop(fft,fftx,534,539)  
 fft=bandStop(fft,fftx,585,610)  
 fft=bandStop(fft,fftx,654,660)  
 fft=bandStop(fft,fftx,773,778)  
 fft=bandStop(fft,fftx,893,900)  
 fft=bandStop(fft,fftx,1100,max(fftx))  
 pylab.axis([0,1200,0,2*10**6])  
 pylab.legend()  
 pylab.title("Power Spectral Analysis",fontsize=28)  
 pylab.ylabel("Power",fontsize=20)  
 pylab.xlabel("Frequency (Hz)",fontsize=20)  

 pylab.subplot(2,1,2)  
 pylab.title("Original Trace",fontsize=28)  
 pylab.ylabel("Potential",fontsize=20)  
 pylab.xlabel("Time (sec)",fontsize=20)  
 pylab.plot(clip(xs),clip(raw),color='.8',label='1: raw')  

 fix = scipy.ifft(fft)  
 pylab.plot(clip(xs),clip(fix)+5000,color='.6',label='2: band-stop')  
 pylab.plot(clip(xs),clip(goodSmooth(fix))-5000,'k',label='3: smoothed')  
 pylab.legend()  
 pylab.title("Band-Stop Filtered Trace",fontsize=28)  
 pylab.ylabel("Potential",fontsize=20)  
 pylab.xlabel("Time (sec)",fontsize=20)  

 pylab.subplots_adjust(hspace=.5)  
 pylab.savefig('out.png',dpi=100)  
 pylab.show()  
 print "COMPLETE"  
 


DIY ECG Trial 1
Posted by
Scott January 15th, 2009 | 5,253 words | 2 Comments »


Scott was 26.65 years old when he wrote this!

I’ve succeeded in building my own electrocardiograph (ECG) to record the electrical activity of my own heart! Briefly, I built a micropotential amplifier using an op-amp and attached it to makeshift electrodes on my chest (pennies and shampoo lol), fed the amplified signal into my sound card, and recorded it as a WAV. The signal is INCREDIBLY noisy though. I was able to do a great job at removing this noise using band/frequency filters in GoldWave (audio editing software designed to handle WAV files). I band-blocked 50-70 Hz (which removed the oscillations from the 60 Hz AC lines running around my apartment). I then wrote the Python code (at the bottom of this entry) to load this WAV file as a single list of numbers (voltage potentials). I performed a data condensation algorithm (converting 100 points of raw WAV data into a single, averaged point, lessening my processing load by 100x), followed by two consecutative moving window averages (20-point window, performed on the condensed data). The result was a voltage reading that had most of the random interference oscillations removed and, behold, a BEAUTIFUL ECG signal!!! I also tossed in some code to determine the peak of the R wave, label it (yellow dot), and use the inverse R-R time distance to calculate heart rate. Nifty, huh?

I swear, this is legitimate data that I recorded myself tonight! This is my actual ECC signal as record by a circuit similar to the one in the previous entry, recorded through my sound card, and processed with the Python script below. Incredible, huh? You can clearly see the Q, R, S, and T spikes (described in the previous couple entries)! I can’t wait to solder-up a prototype (it’s currently breadboarded) and try to analyze hours of data rather than just a few seconds! I’ll take pictures of this device soon. Until then, here’s some real data!

This is a zoomed-in view of a representative QRST wave of mine. Amazing, huh? I’m proud of my ticker! =o)

This is the output of the python script I wrote tonight

And here’s the code I used: note that it relies on the WAV file I recorded – if you want it just ask me for it and I’ll email it to you – it’s about 12 sec long. Oh yeah, not all of the functions in this script are used to create the image (such as integration calculations), but I left ‘em in because you may find them useful.

 import wave, struct, numpy, pylab, scipy  

 def readwave(wavfilename):  
     """load raw data directly from a WAV file."""  
     global rate  
     w=wave.open(wavfilename,'rb')  
     (nchannel, width, rate, length, comptype, compname) = w.getparams()  
     print "[%s] %d HZ (%0.2fsec)" %(wavfilename, rate, length/float(rate))  
     frames = w.readframes(length)  
     return numpy.array(struct.unpack("%sh" %length*nchannel,frames))  

 def shrink(data,deg=100):  
     """condense a linear data array by a multiple of [deg]."""  
     global rate  
     small=[]  
     print "starting with", len(data)  
     for i in range(len(data)/deg):  
         small.append(numpy.average(data[i*deg:(i+1)*deg]))  
     print "ending with", len(small)  
     rate = rate/deg  
     return small  

 def normalize(data):  
     """make all data fit between -.5 and +.5"""  
     data=data-numpy.average(data)  
     big=float(max(data))  
     sml=float(min(data))  
     data=data/abs(big-sml)  
     data=data+float(abs(min(data)))-.47  
     return data  

 def smooth(data,deg=20):  
     """moving window average (deg = window size)."""  
     for i in range(len(data)-deg):  
         if i==0: cur,smooth=sum(data[0:deg]),[]  
         smooth.append(cur/deg)  
         cur=cur-data[i]+data[i+deg]  
     return smooth  

 def makeabs(data):  
     """center linear data to its average value."""  
     for i in range(len(data)): data[i]=abs(data[i])  
     return data  

 def invert(data):  
     """obviously."""  
     for i in range(len(data)): data[i]=-data[i]  
     return data  

 def loadwav(fname='./wavs/bandpassed.wav'):  
     """a do-everything function to get usable, smoothed data from a WAV."""  
     wav=readwave(fname)  
     wav=shrink(wav)  
     wav=invert(wav)  
     wav=smooth(wav)  
     wav=smooth(wav,10)  
     wav=normalize(wav)  
     Xs=getXs(wav)  
     return Xs,wav  

 def getXs(datalen):  
     """calculate time positions based on WAV frequency resolution."""  
     Xs=[]  
     for i in range(len(datalen)):  
         Xs.append(i*(1/float(rate)))  
     print len(datalen), len(Xs)  
     return Xs  

 def integrate(data):  
     """integrate the function with respect to its order."""  
     inte=[]  
     for i in range(len(data)-1):  
         inte.append(abs(data[i]-data[i+1]))  
     inte.append(inte[-1])  
     return inte  

 def getPoints(Xs,data,res=10):  
     """return X,Y coordinates of R peaks and calculate R-R based heartrate."""  
     pXs,pYs,pHRs=[],[],[]  
     for i in range(res,len(data)-res):  
         if data[i]>data[i-res]+.1 and data[i]>data[i+res]+.1:  
             if data[i]>data[i-1] and data[i]>data[i+1]:  
                 pXs.append(Xs[i])  
                 pYs.append(data[i])  
                 if len(pXs)>1:   
                     pHRs.append((1.0/(pXs[-1]-pXs[-2]))*60.0)  
     pHRs.append(pHRs[-1])  
     return pXs,pYs,pHRs  

 Xs,Ys=loadwav()  
 px,py,pHR=getPoints(Xs,Ys)  

 pylab.figure(figsize=(12,6))  
 pylab.subplot(2,1,1)  
 #pylab.axhline(color='.4',linestyle=':')  
 pylab.plot(Xs,Ys,'b-')  
 pylab.plot(px,py,'y.')  
 pylab.axis([None,None,-.6,.6])  
 pylab.title("DIY Electrocardiogram - Trial 1",fontsize=26)  
 pylab.ylabel("Normalized Potential",fontsize=16)  
 #pylab.xlabel("Time (sec)")  
 ax=pylab.axis()  
 pylab.subplot(2,1,2)  
 pylab.plot(px,pHR,'k:')  
 pylab.plot(px,pHR,'b.')  
 pylab.axis([ax[0],ax[1],None,None])  
 pylab.ylabel("Heart Rate (BPM)",fontsize=16)  
 pylab.xlabel("Time (seconds)",fontsize=16)  
 pylab.savefig("test.png",dpi=120)  
 pylab.show()  
 print "DONE"  
 


ECG Success!
Posted by
Scott January 14th, 2009 | 5,253 words | 1 Comment »


Scott was 23.31 years old when he wrote this!

I kept working on my homemade ECG machine (I had to change the values of some of the resisters) and it looks like I’m getting some valid signals! By recording the potential using my sound card (microphone hole = a nice analog to digital converter that every PC has) I was able record my ECG with sound recording software, smooth it, and this is what it looks like. Pretty cool huh?

This was based on a circuit I made using a single op-amp (A LM324 from RadioShack $1.49). Basically the op-amp just amplifies micropotential generated by my heart and outputs it in such a way that I can connect it to a standard headphone jack to plug into my microphone hole. The signal is very noisy though. I’m thinking about making the intricate circuit (with 6 op-amps) to produce a better signal-to-noise ratio, but first I’ll try coding my way out of the noise.

« Previous EntriesNext Entries »
copyright © 2006 swharden@gmail.com