File=SoundAnalyze3.doc Sun 18-Jan-2004 T:=01:07
To analyze Holter data, use SoundAnalyze3 (SA3) program which allows one to chose the channel which has cleanest data. One of the bugs in SA3 is that it can only fit waveforms to the first channel and so the flip channels feature was added.
SA3 is not a particularly user friendly program. The introduction given here will allow you to take a WAV file of Holter data and convert it into HeartLink (HL) type graph, but before you start getting onto the programmer, look at the source code that comes with this program. If you don't like a feature, change it. I can live with the bugs because I wrote the program and, after a week of programming, getting Holter data analyzed was far more important to me than making sure that this program met all of the usability requirements that Apple first came up with for the MacIntosh (and which M$ very poorly copied). To make things a little bit easier for the novice user, I've put button names in bold whenever I remembered to do so. I'm assuming that this text will be read in conjunction with playing with the program.
First it is necessary to open a file. Click on OpenFile and a file selection list will come up and the use of this should be self evident to any user of windoze. Find the file you want and then open it. NOTE: this program does minimal error checking and if you click on a non-WAV file, I have no idea what the program will do. Once you've opened the file, click on SetParm and then GetData. If everything is working as it should, you should see 2 more or less straight lines in the upper display area unless your WAV file has data right at the beginning. Now using the scroll bar will let you move through the file and display a buffer full of data at a time. Initial buffer size is set at 2048 samples (for any given channel only Buffersize/2 samples are displayed), and for 44.1 KHz sampled Holter data, each screen represents 1.11 seconds of data. If you have a sound card that can sample at 48 KHz, then each screen represents 1.024 seconds of data.
Quite soon you will notice that there are a lot of overlapping waveforms on the display screen. Unless you want this effect, click on AutoScreenClear checkbox to clear the screen before new data is displayed. Sorry, black is the only color you can use to display data. All of the data that is considered in this writeup comes from a file called BH.WAV. This WAV file is not available online due to its size and is only on the CD distribution of this program. If you don't have the CD you have to either digitize your own Holter tape or use another WAV file.
To set the amplitude of the waveform, enter scaling factor in the text box labelled Full Scale. The data is multiplied by this number before being displayed. For the examples which follow, the value 2 is used. You can poke data into any of the text boxes in the program since I didn't bother making those which should be read only write protected. After looking at the SA3 screen today, I couldn't figure out what some of the text boxes are for and so feel free to experiment, but if you do things that I haven't mentioned here, it is at your own risk.
As you scroll through Holter file, you will first run into calibration pulses which look like:
I'm not sure what their amplitude is supposed to be. If you increase the amplitude of the pulses, you can see that they are bipolar:
They are actually differentiated pulses and feel free to code a software integrator to restore them to their original square wave shape and this will allow you to get at such data as ST segment depression which is lost in the AC coupled record one gets by simply playing a Holter tape in a cassette recorder into a computer soundcard. To change the amplitude of displayed waveform, enter the new amplitude in Full Scale textbox and then click on SetParm button. Once you scroll forwards or backwards, data with new scale is displayed.. The same considerations hold if you are changing displayed buffer size whose value is in Buffer Size textbox.
As you scroll forward in BH.WAV, you run into QRS complexes. To get an idea of how much variation there is, move the thumb of the scroll bar back and forth to look at the whole file (NOTE: a bug in SA3 allows you to look at only 32767 buffers in the file. For 2048 element buffer size, this means you are limited to the first ~67 Mb of WAV file. Don't blame me, blame the M$ designer who decided that an HScroll control would only have range -32768 to 32767).
SA3 works on the basis of template matching. This means you chose a representative QRS complex (or other waveform as this program isn't limited to just Holter data analysis) and indicate which parts of it you want to use as a template. Once you have chosen a template, the program runs through selected data point by point and calculates an error function (EF) which represents sqrt(sum(template(i) - data(k+i))**2) for all of selected data (I'll do a better description here RSN, I promise). Before we get to this, first need to show how we select the waveform.
Lets say you really like the above . Data from 624664 to 626711 and buffersize = 2048 and scale = 2. Note that flip channels is checked.
Right click in the imagianary rectangle enclosing the top trace and you will see the following happen:
Now, move the mouse pointer to the spot which you consider to be the start of the waveform you want to use as a template and press and hold the left mouse button. While continuing to hold the left mouse button, move the mouse pointer to the spot which represents the end of the template waveform and let go of the left mouse button. The result should look as follows:
To store this template, click on StoreTemplate and you will notice that number in textbox Template will increment. If you want to save this template to a file, first make sure that checkbox Save Selected to file is checked and then click on ListSelected button. A window with then appear entitled MultWindTest1 which tells you to enter the titlein text3 and then click OK. There is a bug in the program which prevents this from happening and the data is stored in the same directory as the WAV file with a filename comprised of the data/time the OK button was clicked. Once you click on the OK button, a text window overlying the waveform window will appear with the data stored for the template. First colum is index in waveform (note that all indices increment by 2 since 2 channels of interleaved data is assumed). You can click on vertical scroll bar in this window to examine data, but as soon as you click anywhere else in that window it vanishes. Don't worry, if you have checked Save selected to file, your selection has been stored to disk.
Anyone who feels ambitious can fix the bug noted above and also allow for saved templates to be read back into program. I plan on doing this RSN when I have time.
The chunk of data that I selected above is:
Right clicking again in the upper waveform bounding rectangle will make the blue areas disappear and the program will take itself out of waveform select mode. If the waveform select mode doesn't appear to be functioning as it should, simply scroll forward and back. There are some bugs in this code and fixing them is left as an exercise for the reader.
To match template to displayed data one next needs to:
Check Enable Template Match
Check Threshold Enable
Enter a number in Threshold textbox
Set Template Max textbox to desired value.
Threshold is not set by default, and this means VB sets it to 0. Unless EF has value of 0, nothing happens in this case. To find out range of EF, click on Match Template button (only works if Enable Template Match checked) and you will get an overlay numeric screen of value of EF for each point in displayed chunk of waveform. Usually the only point where EF is 0 is when one is matching chunk of data containing template. The minimum value of EF occurs when closest match to template exists. If EF value < Threshold, then a match is assumed to exist. Matches are indicated by red lines superimposed on data at point where match exists. Note that multiple points may satisfy the match condition, and if you specify too large a value for threshold, the whole upper waveform will be red. The material below should give you enough of an idea how to set threshold values to begin playing with your data.
Negative threshold values are treated differently. For positive threshold values, a match is considered to exist if EF < threshold. For negative threshold values a match is considered to exist if EF > |threshold|. (|number| = absolute value of a number if you're not familiar with this notation). To do simple amplitude thresholding of input data, find a blank section of data, chose a small chunk of baseline as template, and then match it with a negative threshold. The larger the length of baseline chosen, the greater the effective smoothing done on input waveform. This isn't discussed further but noted for people who might want to play with it.
Below is an example of matching template. Threshold = 200, template max = 2000 and this data may be found between 641048 and 643095. Note the EF display in bottom trace.
Next, to test for validity of template match, change buffersize to 16384. To get this change to happen, need to do the following (sorry, this was a very quickly written program).
Click close file
Click on open file and then click on cancel
Click on SetParm
Then can simply click on scroll bar to move along in file. While checking find that match isn't that good anymore: (Data from 5193752 to 5210135)
To find out what threshold value should be, click on MatchTemplate button and this will give a listing of the data being fitted along with fit values. Any waveforms that are matched are marked with *** (these are the same points that have red lines drawn at match point). Look for local minima between match point and use this to chose a better value of threshold. Note that once you click in the text window, it is minimized. The only place you should be clicking is in the window scrollbar.
After examining the data, a value of 300 looks like it might be better.
Note that points still being missed, so we go for 400 for threshold:
Now we get every QRS and at this point one should check rest of data. One can either use the brute force approach of running through whole file, or just check sections. What one does is a matter of personal preference.
Here we find some points are still being missed (data from 5603352 to 5619735)
Time to run through the process of looking at what kind of fit values are happening and change threshold again. Decision is made to change threshold to 480 and all points now matched below.
Note that the template matching procedure is insensitive to much noise as is seen in this screen (from 28688408 to 28704791)
However, some types of artifact just can't be dealt with as is seen in the very next screen.
The above artifact is most likely due to movement. Note the absent QRS on the first channel whereas a complex is seen on the second channel. This is followed by a greatly attenuated QRS and then normal QRS complexes. Such events are best dealt with manually and this is why Holter tapes are scanned in their entirety by a technician (very boring, but someone has to do it).
Everything appears to be going very well as can be seen by this frame from (54722584 to 54738967)
Then something happens (55148568 to 55164951)
and the remainder of the tape has such meaningfull stuff like (55296024 to 55312407)
What happened was that the patient found the geometric arrangment of the EKG electrodes aesthetically displeasing and reconfigured them into a pattern more to his liking, but not one which was usefull in picking up an EKG signal. He said nothing to the lab technician or me about this, and I had to phone the lab to talk to the technician who removed the holter to find out what had happened. This is put in to illustrate that patients can do really strange things and that one can never be too paranoid when the data doesn't seem to make sense.
Now that one has found an appropriate template and threshold value, one can run through whole Holter file to create HL type data. On closer inspection of BH.WAV, it was found that threshold should really be 500, not 480, something that will only be of importance to people attempting to exactly duplicate performace of this program.
Now need to setup analysis parameters which is done from Setup menu, Analysis option. This part of program was written last and is atrocious. Analysis start index and end index need to be entered, and one can either get this data from display, or use 1000 for start index and (current high file index - 32000)/2. There should be default values setup, but I left them blank since I needed to be able to analyze any chunk of data during program testing. For purposes of this analysis we use 1000 and 70,000,000 since most of file is junk. After setting up values, hit Accept/Exit button and this window will disappear.
Next, got to File menu and click on Analysis output start. Next go to Analyze menu and click on Begin Analysis. Data should now be read in and analyzed. How long this takes depends on the speed of your processor; for a 900 MHz Athlon processor a ~300 Mb WAV file takes about 3 minutes to be analyzed whereas on a 375 MHz AMD K6/2, the analysis time was on the order of 20-30 minutes. This program won't run on a 80486 or lower class of 80x86 processor (should be obvious when you try to run it). One way of monitoring progress of analysis is to look at the new *.anf file which has been creasted in same directory that WAV file is in. As long as this file is increasing in size, data is still being read in. RSN I plan on putting some type of progress indicator in program (another exercise for the reader). Other option, in W95 or W98 is to use system monitor and look at disk bytes read/second as this should be a steady value until data file has been read in.
When you've finally read in all data, next step is to compress *.anf file to a *.anc file. *.anc file contains every single point where EF < threshold. For some waveforms, one might find 20-200 points where this is the case and the reason that *.anf files are written is because I didn't have enough RAM in the system I developed the program on to store the *.anf file data in RAM. Really, this step should be done automatically by program, but it isn't. One justification I had for it was that various values for max heart rate could be used when compressing *.anf files, but no provision exists for changing this value except by recompiling program. *.anf files can be deleted once they have been compressed. When program is finished, go to Analyze menu and chose Compact HE and once this process has finished, click on HeartLink Graph (also in Analyze menu).
Once the data has been analyzed, go to Analyze menu and click on Compact HE and a window labelled MultWindTest1 will come up with a message similar to:
Input records = 467985 Output records = 43657
Click on OK and there is just one more step before you can take your HL format file and display it in the display program of your choice. Go to Analyze menu and click on HeartLink Graph. A window will pop up with message:
No *.anc file open. Open one?
Click on OK and then select the *.anc file you just made. Very shortly afterwards you should get an informative window with data on how many QRS complexes were found
Heartllink graph accumulated from 43658 points
When the program asks:
Dump data to text file
Click on yes, and you now have a *.txt file with HR data put into 1 minute bins which is precisely what a HL type record consists of.
What you do with this file is now up to you. I was originally going to put in a feature to display this data on screen in SA3, but I ran out of time. M$ Excel (Ugh!!) happened to be nearby and I imported the *.txt file into Excel and created a HL type graph. CricketGraph is a much nicer program, but it only runs on the Macintosh and my Mac emulation via Basilisk didn't pass data back and forth to Windoze in 2000, but I think that the 1987 version of CricketGraph is far better for this task than the bloated monstrosity of M$ Excel [rant mode off].
Note that you have to create a mapping between HL file indices and Holter times yourself. It would be trivial to write a program to do this, but I simply have the graphing program put horizontal divisions every 120 minutes and manually insert the times since 99% of the time this is sufficient to determine when sleep period occurred and do analysis. Email me if there are any questions or if I've made stupid errors in this writeup.