Digital Signal Processing DSP
Digital Signal Processing DSP
Preface
1. Dsp00095-Preface to Digital Signal Processing - DSP
2. Time Series
1. Dsp00100-Periodic Motion and Sinusoids
2. Dsp00104-Sampled Time Series
3. Dsp00108-Averaging Time Series
3. Plotting Data
1. Java1468-Plotting Engineering and Scientific Data using
Java
2. Java1489-Plotting 3D Surfaces using Java
3. Java1492-Plotting Large Quantities of Data using Java
4. Fourier Analysis
1. Java1478-Fun with Java, How and Why Spectral Analysis
Works
2. Java1482-Spectrum Analysis using Java, Sampling
Frequency, Folding Frequency, and the FFT Algorithm
3. Java1483-Spectrum Analysis using Java, Frequency
Resolution versus Data Length
4. Java1484-Spectrum Analysis using Java, Complex
Spectrum and Phase Angle
5. Java1485-Spectrum Analysis using Java, Forward and
Inverse Transforms, Filtering in the Frequency Domain
6. Java1486-Fun with Java, Understanding the Fast Fourier
Transform (FFT) Algorithm
7. Java1490-2D Fourier Transforms using Java, Part 1
8. Java1491-2D Fourier Transforms using Java, Part 2
5. Convolution and Frequency Filtering
1. Java1487-Convolution and Frequency Filtering in Java
2. Java1488-Convolution and Matched Filtering in Java
6. Adaptive Processing
1. Java2350-Adaptive Filtering in Java, Getting Started
2. Java2352-An Adaptive Whitening Filter in Java
3. Java2354-A General-Purpose LMS Adaptive Engine in
Java
4. Java2356-An Adaptive Line Tracker in Java
5. Java2358-Adaptive Identification and Inverse Filtering
using Java
6. Java2360-Adaptive Noise Cancellation using Java
7. Java2362-Adaptive Prediction using Java
7. Data Compression and the Discrete Cosine Transform
1. Java2440 Understanding the Lempel-Ziv Data
Compression Algorithm in Java
2. Java2442 Understanding the Huffman Data Compression
Algorithm in Java
3. Java2444 Understanding the Discrete Cosine Transform in
Java
4. Java2446 Understanding the 2D Discrete Cosine
Transform in Java
5. Java2448 Understanding the 2D Discrete Cosine
Transform in Java, Part 2
Dsp00095-Preface to Digital Signal Processing - DSP
This module is a preface to the collection titled Digital Signal Processing-
DSP
Table of contents
Preface
Legacy versus openstax presentation format
Miscellaneous
Preface
Over the years, I have published a large number of DSP tutorials on various
websites. This collection, which is a work in process, gathers the more
significant of those tutorials into a common location to make them more
readily available for Connexions users.
Some of the tutorials were originally published ten or more years ago.
However, you need not be concerned about the material in those tutorials
becoming obsolete. The concepts and algorithms involved in DSP (such as
convolution, correlation, the discrete Fourier transform and the Fast
Fourier Transform) are essentially the same today as they were when they
first became practical for use on digital computers around 1960. (However,
the hardware used to implement those algorithms has become much smaller
and much faster.)
As I have time available, I am converting the tutorials from their original
HTML format into the Openstax format that you are accustomed to seeing.
You will find that some of the tutorials are available in Openstax format,
and others are available in HTML or PDF format as described below.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF versions of the tutorials
using the PDF links that are provided.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the
tutorials in their original HTML format. (The HTML format is more reliable
than the PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF versions or the HTML versions directly from the openstax
presentation page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Dsp00100-Periodic Motion and Sinusoids
Baldwin kicks off a new miniseries on DSP. He discusses periodic motion
and sinusoids. He introduces time series analysis, sine and cosine functions,
and frequency decomposition. He discusses composition, and provides
examples for square and triangular waveforms.
Table of contents
Preface
Viewing tip
Figures
Preview
Discussion and sample code
Periodic motion
Motion of a pendulum
A bag of sand
Harmonic motion
Sinusoids
So what?
A practical example
Summary
Miscellaneous
Preface
This module is the first in a series of modules designed to teach you about
Digital Signal Processing (DSP) using Java. The purpose of the miniseries
is to present the concepts of DSP in a way that can be understood by
persons having no prior DSP experience. However, some experience in Java
programming would be useful. Whenever it is necessary for me to write a
program to illustrate a point, I will write it in Java.
Viewing tip
Figures
Preview
Many physical devices (and electronic circuits as well) exhibit a
characteristic commonly referred to as periodic motion .
periodic motion,
harmonic motion, and
sinusoids.
I will introduce you to sine and cosine functions and the Java methods that
can be used to calculate their values.
I will introduce you to the concepts of period and frequency for sinusoids.
Periodic motion
The most difficult decision that I must make for this series is to decide
where to begin. I need to begin at a sufficiently elementary level that you
will understand everything that you read. So, before getting into the actual
topic of digital signal processing, I'm going to take you back to some
elementary physics and mathematical concepts and discuss periodic motion
.
Motion of a pendulum
For example, consider the pendulum on a grandfather clock. Once you start
the pendulum swinging, it will swing for a long time (actually, the spring in
the clock gives it a little kick during each repetition, so it will continue to
swing until the spring runs down).
The most important characteristic of the motion of the pendulum is that
every repetition takes almost exactly the same amount of time. In other
words, the period of time during which the pendulum swings from one side
to the other is very constant. That is the source of the term periodic motion .
A bag of sand
As the bag of sand swings back and forth, a little bit of sand will leak out of
the hole and land on the floor below. The sand will trace out a line, which is
parallel to the direction of motion of the bag of sand.
Now assume that you carefully drag a carpet across the floor under the bag
at a uniform rate, perpendicular to the direction of motion of the bag. This
will cause the sand that leaks from the bag to trace out a zigzag pattern on
the carpet very similar to the curved line shown in Figure 1 .
A sinusoidal function
In this assumed scenario, the bag is swinging back and forth along the
vertical axis in Figure 1 , and the carpet is being dragged along the
horizontal axis. The motion of the bag causes the sand to be leaked in a
back-and-forth zigzag pattern. The motion of the carpet causes that pattern
to be elongated along the horizontal axis.
Math.cos(2*pi*x/50) + Math.sin(2*pi*x/50)
Math in the above expression is the name of a Java library that provides
methods for computing sine, cosine, and various other mathematical
functions.
The asterisk (*) in the above expression means multiplication.
The reference to pi in the above expression is a reference to the
mathematical constant with the same name.
The image in Figure 1 was produced using a Java program named
Graph03 , which I will describe in a future module.
Harmonic motion
Other devices that exhibit periodic motion may exhibit more complex
harmonic motion. For example, Figure 2 illustrates a more complex form of
harmonic motion.
As you can see, the positive excursions are greater than the negative
excursions in Figure 2 . In addition, the negative excursions exhibit some
ripple that is not exhibited by the positive excursions.
Math.cos(2*pi*x/50)
+ Math.sin(2*pi*x/50)
+ Math.sin(2*pi*x/25);
Sinusoids
Figure 1 shows a curve whose shape is commonly referred to as a sinusoid.
Eliminating the syntax required by the Java programming language, the
expression used to produce the data plotted in Figure 1 was:
As you can see, this function is composed of two components, one cosine
component and one sine component.
Figure 3 shows separate plots of the two components that were added
together to produce the plot in Figure 1 .
f(x) = cos(2*pi*x/50)
The blue curve shows the sine function produced by evaluating and plotting
the following equation:
g(x) = sin(2*pi*x/50)
The point-by-point sum of these two curves would produce the sinusoidal
curve shown in Figure 1 .
Many processes produce functions whose values vary over time. For
example, the temperature in your office is a function that varies
continuously with time.
Sampled data
If you were to measure and record the temperature in your office once each
minute, you could plot the recorded values in the manner shown in Figure 1
through Figure 3 . You could plot the temperature along the vertical axis
against time (or sample number) along the horizontal axis.
A common name for sampled data of this type is time series. That name
reflects the fact that your data is a recording of the temperature values for a
series of measurements that occur over time. (I will have quite a lot more to
say about time series in future modules.)
Each of the curves in Figure 3 has the same period, where the period is the
horizontal distance from one peak to the next.
The black curve in Figure 4 was produced by evaluating and plotting the
following equation:
f(x) = cos(2*pi*x/25)
The blue curve in Figure 4 was produced by evaluating and plotting the
following equation:
g(x) = sin(2*pi*x/50
Note that the periods (frequencies) for the black cosine curve and the blue
sine curve in Figure 4 are not the same. The period (time interval between
peaks) for the black curve is one-half that of the blue curve. Stated
differently, the frequency of the black curve is twice that of the blue curve.
Modified arguments
This difference exists because I modified the argument in the equation for
the cosine function relative to the sine function. In particular, I halved the
value in the denominator in the cosine argument relative to the denominator
in the sine argument.
This caused the period of the cosine function to be only half as long as the
period for the sine function. Stated differently, this caused the frequency of
the cosine function to be twice the frequency of the sine function.
Most programming languages provide methods for computing the sine and
the cosine values for an angle given in radians. That is true for Java, and it
is also true for the plotting software used to produce these graphs. (These
graphs were produced using a Java program.)
(For example, this is the number of degrees that the big hand on
a clock must traverse to begin at the 12 and make one complete
cycle around the clock face and back to the 12.)
Beyond one cycle, the values of the sine and cosine functions repeat.
The horizontal scale in Figure 4 extends from minus 100 units to plus 100
units with a value of 0 in the center.
A tic mark appears every ten units. For x equal to 25, the argument for the
cosine function equals 2*pi radians, or 360 degrees. A close examination of
Figure 4 shows that the cosine curve goes through one full cycle between an
x value of 0 and an x value of 25. Beyond that, the cosine function simply
repeats.
Similarly, for x equal to 50, the argument for the sine function equals 2*pi
radians, or 360 degrees. Again, a close examination of Figure 4 shows that
the sine curve goes through one full cycle between an x value of 0 and an x
value of 50. Beyond that, the sine function simply repeats.
2*pi(radians)*(x )*(1/n )
Thus, with a fixed value of n , for each value of x , the argument represents
an angle in radians, which is what is required for use with the functions of
the Java Math library.
The value of the sine of an angle goes through zero at every integer
multiple of pi radians. This explanation will probably make more sense if
you refer back to Figure 3 .
The curve in Figure 3 was calculated and plotted for n equal to 50. The sine
curve has a zero crossing for every value of x such that x is a multiple of
n/2, or 25.
Where are the peaks in the cosine function ?
Similarly, the peaks in the cosine curve in Figure 3 occur for every value of
x such that x is a multiple of n/2, or 25.
Successive approximations
cos(2*pi*x/50)
- cos(2*pi*x*3/50)/3
+ cos(2*pi*x*5/50)/5
- cos(2*pi*x*7/50)/7
+ cos(2*pi*x*9/50)/9
- cos(2*pi*x*11/50)/11
+ cos(2*pi*x*13/50)/13
- cos(2*pi*x*15/50)/15
+ cos(2*pi*x*17/50)/17
- cos(2*pi*x*19/50)/19
The top curve in Figure 5 is a plot of only the first sinusoidal term shown
above. It is a pure cosine curve.
Each successive plot, moving down the page in Figure 5 and Figure 6 adds
another term to the expression being plotted, until all ten terms are included
in the bottom curve in Figure 6 .
If you start at the top of Figure 5 and examine the successive curves, you
will see that the approximation to a square wave improves as each new
sinusoidal term is added.
Figure 7 shows individual plots of the first five sinusoidal terms required to
approximate the square wave.
(You may have noticed that the sign applied to every other term
in the expression is negative, causing every other term to plot
upside down in Figure 7 .)
The bottom curve in Figure 5 is the point-by-point sum of the five curves
shown in Figure 7 .
A side-by-side comparison
If you view Figure 7 side-by-side with Figure 5 , you should be able to see
how the sinusoidal terms add and subtract to produce the desired result. For
example, the subtraction of the second sinusoidal term from the first
sinusoidal term knocks the peaks off the first term and produces a
noticeable shift from a cosine towards a square wave.
Five sinusoids
The time series at the bottom of Figure 8 was created by adding together
five cosine waves, each having the amplitude and frequency values shown
in the following expression:
f(x) = cos(2*pi*x/50)
+ cos(2*pi*x*3/50)/9
+ cos(2*pi*x*5/50)/25
+ cos(2*pi*x*7/50)/49
+ cos(2*pi*x*9/50)/81
Intermediate waveforms
The top waveform in Figure 8 is a plot of the cosine curve created from the
sinusoidal first term in the expression shown above.
The second waveform from the top in Figure 8 is the sum of the first two
terms in the above expression.
The third waveform is the sum of the first three terms. By this point, the
plot has begun to resemble a triangular waveform in a significant way.
The fourth waveform is the sum of the first four terms, and the fifth
waveform is the sum of all five terms.
By examining the waveforms from top to bottom in Figure 8 , you can see
how the addition of each successive term causes the resulting waveform to
more closely approximate the desired triangular shape.
Figure 8 shows that only five terms are required to produce a fairly good
approximation to a triangular waveform.
A comparison of Figure 8 with Figure 5 shows that five terms are much
more effective in approximating a triangular waveform than were the five
terms in approximating a square waveform. The triangular waveform is
easier to approximate because it doesn't have a flat top and vertical sides.
So what ?
By now, you are may be saying "So what?" What in the world does DSP
have to do with bags of sand with holes in the bottom? The answer is
everything.
Almost everything that we will discuss in the area of DSP is based on the
premise that every time series, whether generated by sand leaking from a
bag onto a moving carpet, or acoustic waves generated by your favorite
rock band, can be decomposed into a large (possibly infinite) number of
sine and cosine waves, each having its own amplitude and frequency.
A practical example
You have probably seen, the kind of stereo music component commonly
known as an equalizer. An equalizer typically has about a dozen adjacent
slider switches that can be moved up and down to cause the music that you
hear to be more pleasing. This is a crude form of a frequency filter .
Many equalizers also have a set of vertical display lights that dance up and
down as your music is playing. This is a crude form of a frequency
spectrum analyzer .
Summary
Many physical devices (and electronic circuits as well) exhibit a
characteristic commonly referred to as periodic motion.
I introduced you to sine and cosine functions and the Java methods that can
be used to calculate their values.
I told you that almost everything we will discuss in this series on DSP is
based on the premise that every time series can be decomposed into a large
number of sinusoids, each having its own amplitude and frequency.
I introduced you to the concept of composition, where any time series can
be created by adding together the correct (possibly very large) set of
sinusoids, each having its own frequency and amplitude.
Miscellaneous
This section contains a variety of miscellaneous information.
-end-
Dsp00104-Sampled Time Series
Baldwin explains the meaning of sampling, and identifies some of the
problems that arise when sampling and processing analog signals. He
explains the concept of the Nyquist folding frequency and illustrates the
folding phenomena by plotting time series data as well as spectral data.
Table of contents
Preface
Viewing tip
Figures
Preview
Discussion
Some numbers
Comparison of frequencies with center frequency
Reduce the sampling frequency
A different approach
Introduce a sampling problem
Back to the case with no problems
Back to the case with the sampling problem
Summary
Miscellaneous
Preface
This is one in a series of modules designed to teach you about Digital
Signal Processing (DSP) using Java. The purpose of the miniseries is to
present the concepts of DSP in a way that can be understood by persons
having no prior DSP experience. However, some experience in Java
programming would be useful. Whenever it is necessary for me to write a
program to illustrate a point, I will write it in Java.
Viewing tip
Preview
I will explain the meaning of sampling, and will explain some of the
problems that arise when sampling and processing analog signals. Those
problems generally relate to the relationship between the sampling
frequency and the high-frequency components contained in the analog
signal.
I will explain the concept of the Nyquist folding frequency , which is half
the sampling frequency (more commonly called the sampling rate) .
Discussion
I also introduced the concept of composition , where any time series can be
created by adding together the correct set of sinusoids, each having its own
amplitude and frequency.
Once the signal has been sampled and converted to digital form, there is
often no interest in reconstructing the analog signal from the samples.
While this eliminates the difficulty of reconstruction, it doesn't eliminate the
potential problems caused by having the sampling frequency be less than
twice the highest frequency component in the signal.
If the analog signal contains frequency components that are greater than
half the sampling frequency, those components will appear to be at a
different frequency in the sampled data.
The frequency that is equal to half the sampling frequency is often referred
to as the Nyquist folding frequency , or simply the folding frequency. The
folding frequency is half the sampling frequency. I will provide examples
later to illustrate where this frequency gets its name.
A brief description
In other words, the entire frequency spectrum appears to fold around the
folding frequency such that all frequency components that are above the
folding frequency fold down to a similar position on the lower side of the
folding frequency. Those frequency components above the folding
frequency produce a mirror image below the folding frequency.
(If a frequency component in the analog signal is greater than the
sampling frequency, folding still occurs, but in a more
complicated way.)
Some specific numbers may make this easier to understand. Assume that
the sampling frequency is 2000 samples per second, giving a folding
frequency of 1000 cycles per second.
A frequency component at 1600 cycles per second in the analog signal will
fold down and appear at 400 cycles per second in the sampled signal.
First we need to think about what we really have when we have a sampled
time series. All that we really have is a set of values taken at specific times.
In reality, we know nothing about the values that actually existed for the
analog signal in-between the samples.
Sampled sinusoids
Figure 1 shows the values for samples taken from five different sinusoids
(the height of each vertical bar represents the value of a sample).
All five sinusoids were sampled at the same sampling frequency. The
sinusoid in the center was sampled twenty times per cycle (not necessarily
twenty times per second).
The two sinusoids above the center had higher frequencies than the sinusoid
in the center, with the sinusoid at the top having the highest frequency. For
a fixed sampling frequency, the sinusoids above the center had fewer
samples per cycle than the sinusoid in the center. The sinusoid at the top
had the fewest number of samples per cycle.
The two sinusoids below the center had lower frequencies, than the sinusoid
in the center, with the sinusoid at the bottom having the lowest frequency.
The two sinusoids below the center had more samples per cycle than the
sinusoid in the center. The sinusoid at the bottom had the most samples per
cycle.
In the final analysis, what really counts is not the number of samples per
second of the sampling frequency, or the number of cycles per second of the
signal frequency. What really counts is the number of samples per cycle of
the highest frequency component. This value is established by the
combination of the signal frequency and the sampling frequency.
Now consider the graphical treatment for the same five sinusoids shown in
Figure 3 .
Figure 3. Trapezoidal representations of samples from five
sinusoids.
As you can see in these figures, regardless of which graphical treatment you
use, the sampling frequency relative to the signal frequency is sufficiently
high to present a respectable view of the sinusoidal signals. Now I'm going
to show you what happens when the sampling frequency is reduced without
changing the frequency of the sinusoids.
Figure 5 shows the same five sinusoids as above, except that they are
plotted across a longer period of time. (The presentation in Figure 5 treats
each sample as a rectangle.)
In particular, you should note the obvious frequency difference between the
top sinusoid and the bottom sinusoid. Also note the frequency difference
between the two sinusoids immediately above and immediately below the
center sinusoid.
Some numbers
The most important thing to note about these frequency values is how the
four outer frequencies relate to the center frequency. The top and bottom
frequency values differ from the center frequency by 0.75 cycles per
second. In other words, the frequency of the top sinusoid is 0.75 cycles per
second above the frequency of the center sinusoid, and the frequency of the
bottom sinusoid is 0.75 cycles per second below the frequency of the center
sinusoid.
Similarly, the second and fourth frequency values differ from the center
frequency by 0.50 cycles per second. Again, one is above and the other is
below.
I will re-plot the data for each sinusoid across the same period of time as in
Figure 5 . The results are shown in Figure 6 . It would probably be useful
for you to view Figure 5 and Figure 6 side-by-side in separate browser
windows.
First, you will probably notice that the plot for the center sinusoid no longer
looks much like a sinusoid. Rather, it looks like a square wave. This is the
result of having exactly two samples per cycle of the sinusoid. One sample
is taken from the positive lobe of the sinusoid, and the next sample is taken
from the negative lobe of the sinusoid. This pattern repeats, producing
something that looks like a square wave. (A different graphical treatment
would make it look like a triangular wave.)
The top two sinusoids
More important, however, is to note what has happened to the top two
sinusoids. Because the frequencies of the top two sinusoids are above the
folding frequency, they no longer have a minimum of two samples per
cycle. Thus, the apparent frequency of these two sinusoids has folded
around the folding frequency and appears as a lower frequency.
In fact, the plot of the top sinusoid now looks exactly like the plot of the
bottom sinusoid at a frequency of 0.25 cycles per second. This means that
the energy in the top sinusoid at 1.75 cycles per second has folded into a
new frequency of 0.25 cycles per second.
The plot of the sinusoid immediately above the center looks exactly like the
plot of the sinusoid immediately below the center at a frequency of 0.50
cycles per second. This means that the energy in that sinusoid at 1.5 cycles
per second has folded into a new frequency of 0.5 cycles per second.
The plots of the center sinusoid and the two sinusoids below the center are
still correct (although not very well sampled). However, the frequency
information embodied in the top two sinusoids has been lost. The top two
sinusoids appear to be at a different frequency than the actual frequency of
the corresponding analog signals. The fact that the frequencies of these two
sinusoids were originally 1.75 and 1.50 cycles per second is now lost in the
sampled data.
A different approach
Now I'm going to illustrate the same folding phenomena from a different
perspective using spectral analyses. First I will show you a case having no
sampling problems. Then I will introduce a sampling problem and show
you the impact that the problem has on the final results.
The sampling frequency for the data in Figure 7 was four samples per
second, giving a folding frequency of two cycles per second. Thus, the
horizontal scale on each plot represents the frequencies from zero on the
left to two cycles per second on the right.
Starting at the top, each of the five plots represents the frequency spectrum
of a sinusoid having the amplitude and frequency shown in the following
table.
The height of each spectral peak in Figure 7 is consistent with the amplitude
of the corresponding sinusoid given in the table.
The location of the spectral peak in the fifth plot corresponds to a frequency
of 1.75 cycles per second within a total frequency range extending from
zero to two cycles per second. This matches the information given in the
above table for the fifth sinusoid.
The location of the peak in each of the three plots between the first and the
last are correct for the frequency of the sinusoid involved.
In this case, the sampling frequency was two samples per second, giving a
folding frequency of one cycle per second. Therefore, the horizontal scale
on each plot represents the frequencies from zero on the left to one cycle
per second on the right.
The heights of the spectral peaks
Once again, the height of each spectral peak is consistent with the
amplitude of the sinusoid.
As before, the spectral peaks in the first three plots appear where you would
expect to see them. The peak in the first plot is about twenty-five percent of
the way across the total spectrum, corresponding to 0.25 cycles per second.
The spectral peak in the second plot is at the center, corresponding to 0.5
cycles per second. The third peak is in the correct location for 0.75 cycles
per second.
However, a problem exists with the spectral peaks in the last two plots.
The spectral peak in the fourth plot also appears about midway between
zero and one cycle per second. This indicates that the corresponding
sinusoid had a frequency of 0.5 cycles per second.
However, the frequency of the sinusoid for the fourth plot was 1.50 cycles
per second, not 0.5 cycles per second as indicated. Thus, that spectral peak
should have been off the scale on the right-hand side of the plot.
The folding frequency
Recall, however, that the right edge of the plot is the folding frequency.
Therefore, any spectral components that should appear to the right of the
folding frequency fold around and appear to the left of the folding
frequency. Therefore, the spectral peak in the fourth plot, which should
appear at 0.50 cycles per second above the folding frequency, appears
instead at 0.50 cycles per second below the folding frequency.
Similarly, the frequency of the sinusoid for the fifth plot was 1.75 cycles per
second. The peak for this sinusoid should have appeared 0.75 cycles per
second above the folding frequency, but appeared instead 0.75 cycles per
second below the folding frequency. In other words, the spectrum folded
around the folding frequency so that this peak appeared below the folding
frequency.
I am going to show you two more views of the spectra of these sinusoids to
help you better understand the folding phenomena.
Let's go back and examine another view of the case that has no sampling
problems. This view is shown in Figure 9 .
This is the case where all five sinusoids are sampled at a sampling
frequency of four samples per second, resulting in a folding frequency of
two cycles per second. If you compare Figure 9 with Figure 7 , you will see
that the left half of Figure 9 is very similar to Figure 7 .
Thus, the total frequency range for Figure 9 is twice the frequency range for
Figure 7 .
Hopefully the display in Figure 9 will explain why the frequency that is half
the sampling frequency is called the folding frequency. The computed
spectrum folds around that frequency. Everything to the right of the folding
frequency is a mirror image of everything to the left of the folding
frequency.
All the peaks to the left of center in Figure 9 are valid spectral peaks
associated with the corresponding sinusoids. However, all the peaks to the
right of center, which I marked with red ovals, are artifacts of the sampling
process. Those peaks do not exist in the true spectrum of the original raw
data. They were created by the sampling process.
Normally don't compute the mirror image
Normally we don't worry about this mirror image above the folding
frequency when doing spectral analyses. We know it is there and we simply
ignore it.
In fact, for reasons of economy, when doing spectral analyses using discrete
Fourier transforms, we usually don't even compute the spectrum at
frequencies above the folding frequency. Since it is always a mirror image
of the spectrum below the folding frequency, we know what it looks like
without even computing it.
Although it's not obvious at this plotting scale, there are zero-valued points
between the side lobes on the peaks in Figure 7 .
The points in the spectral display of Figure 9 simply missed the side lobes
and hit the zeros between the side lobes. I will have a lot more to say about
this in a future module discussing spectral analysis.
As with the previous case, each of the plotted spectra in Figure 10 shows
the frequency range from zero frequency to the sampling frequency of two
samples per second. The folding frequency of one cycle per second appears
in the center of each plot.
A mirror image
I have identified the artifacts created by the sampling process with a red
oval in Figure 10 .
The problem, as you will recall, is that the frequency of the sinusoids
corresponding to the two bottom plots in Figure 10 is above the folding
frequency. Thus the peaks to the right of center in the bottom two plots of
Figure 10 actually represent the frequencies of the corresponding sinusoids.
Unfortunately, these two peaks appear to the right of the folding frequency,
which the area of the spectra that we normally ignore.
I am able to identify these two peaks as artifacts only because I know the
true frequency makeup of the raw data. In most real-world situations with
unknown data, there would be no way for me to identify these particular
peaks to the left of the folding frequency as artifacts.
Hopefully this illustration will make the concept of the folding frequency
easier for you to understand. The folding frequency is one-half the sampling
frequency. The entire spectrum below the folding frequency folds around
the folding frequency and the peaks in that spectrum appear in mirror-image
format above the folding frequency.
The frequency information for all frequency components above the folding
frequency is lost when the signal is sampled. In addition, the energy
associated with those components will fold around and can corrupt the
information for frequency components that are below the folding frequency.
The bottom line is that you must be very careful when sampling analog
signals for later processing using DSP. In order to avoid erroneous results,
you must sample sufficiently fast to ensure that your sampling rate is
greater than twice the highest frequency components contained in the
analog signal.
On the other hand, the greater your sampling rate, the more computer-
intensive will be most of the DSP techniques that you apply to the data
later. For economy reasons, therefore, you don't want your sampling
frequency to be excessively high.
Digital re-sampling
Summary
I explained the meaning of sampling, and explained some of the problems
that arise when sampling and processing analog signals.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it
possible for you to purchase a pre-printed version of the PDF file, you
should be aware that some of the HTML elements in this module may not
translate well into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales
nor do I know who does receive compensation. If you purchase such a
book, please be aware that it is a copy of a module that is freely available
on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Dsp00108-Averaging Time Series
Baldwin begins with a discussion of averaging time series, and ends with a
discussion of spectral resolution, covering several related topics in between.
Table of contents
Preface
Figures
Preview
Discussion
Spectral analysis
Summary
Miscellaneous
Preface
This is one in a series of modules designed to teach you about Digital
Signal Processing (DSP) using Java. The purpose of the miniseries is to
present the concepts of DSP in a way that can be understood by persons
having no prior DSP experience. However, some experience in Java
programming would be useful. Whenever it is necessary for me to write a
program to illustrate a point, I will write it in Java.
I told you that to sample an analog signal means to measure and record its
amplitude at a series of points in time. The values that you record constitute
a sampled time series intended to represent the analog signal.
I told you that to avoid problems, the sampling frequency must be a least
twice as great as the highest frequency component contained in the analog
signal, and as a practical matter, should probably be somewhat higher.
I introduced you to sinusoids, taught you about sine and cosine functions,
and introduced the concepts of period and frequency for sinusoids.
I told you that almost everything we will discuss in this series on DSP is
based on the premise that every time series can be decomposed into a large
number of sinusoids, each having its own amplitude and frequency.
I told you that DSP is based on the notion that signals in nature can be
sampled and converted into a series of numbers. The numbers can be fed
into some sort of digital device, which can process the numbers to achieve
some desired objective.
Viewing tip
Figures
Preview
This is a broad-ranging module. It begins with a discussion of averaging
time series, ends with a discussion of spectral resolution, and covers several
related topics in between. Don't be alarmed, however, at the range of the
module. The topics of time-series averaging and spectral resolution are very
strongly related.
I will discuss why we frequently need to average sampled time series, and
explain some of the issues involved in that process.
I will also show you the impact of those averaging issues on DSP, using
spectrum analysis as an example.
Discussion
It never ceases to amaze me how something as mathematically complex as
DSP can be distilled down to the simplest of computational processes.
Which screw to turn ...
DSP reminds me of the old story about the customer who complained about
the bill at the auto repair shop being too high. According to the customer,
all the mechanic did to fix the problem was turn one screw, and the bill was
too high for the labor involved. The mechanic responded that he didn't
charge for turning the screw. Instead, he charged for knowing which screw
to turn, and knowing which way and how far to turn it.
This module, in conjunction with the earlier module titled Sampled Time
Series may be the most important module in the entire collection because it
provides a practical pseudo-mathematical framework for almost everything
that follows.
1. Multiply one time series by another time series, to produce a third time
series.
2. Compute the average value of the third time series.
In many cases, it is the average value of the third time series that provides
the answer you are seeking.
The challenge is in knowing what the average value means, and how to
interpret it.
Almost everything that we will discuss in this series on DSP is based on the
premise that every time series can be decomposed into a (potentially large)
number of sinusoids, each having its own amplitude and frequency.
Suppose, for example, that we have two time series, each of which is
composed of two sinusoidal components as follows:
h(x) = f(x)*g(x)
= (cos(ax) + cos (bx)) * (cos(cx) + cos(dx))
where the asterisk (*) means multiplication.
h(x) = cos(ax)*cos(cx)
+ cos(ax)*cos(dx)
+ cos(bx)*cos(cx)
+ cos(bx)*cos(dx)
Thus, the time series produced by multiplying any two time series consists
of the sum of a (potentially large) number of terms, each of which is the
product of two sinusoids.
We probably need to learn a little about the product of two sinusoids. I will
discuss this topic with a little more mathematical rigor in a future module.
In this module, however, I will simply illustrate the topic using graphs.
The frequencies of the new sinusoids are different from the frequencies of
the original sinusoids. Furthermore, the frequency of one of the new
sinusoids may be zero.
Note: What is a sinusoid with zero frequency?
As a practical matter, a sinusoid with zero frequency is simply a constant
value. It plots as a horizontal straight line versus time.
Think of it this way. As the frequency of the sinusoid approaches zero, the
period, (which is the reciprocal of frequency), approaches infinity. Thus,
the width of the first lobe of the sinusoid widens, causing every value in
that lobe to be the same as the first value.
This will become a very important concept as we pursue DSP operations.
More specifically, when you multiply two sinusoids, the frequency of one
of the sinusoids in the new time series is the sum of the frequencies of the
two sinusoids that were multiplied together. The frequency of the other
sinusoid in the new time series is the difference between the frequencies of
the two sinusoids that were multiplied together.
For the special case where the two original sinusoids have the same
frequency, the difference frequency is zero and one of the sinusoids in the
new time series has a frequency of zero. It is this special case that makes
digital filtering and digital spectrum analysis possible.
When we multiply two time series and compute the average of the resulting
time series, we are in effect computing the average of the products of all the
individual sinusoidal components contained in the two time series. That is,
the new time series contains the products of (potentially many) individual
sinusoids contained in the two original time series. In the end, it all comes
down to computing the average value of products of sinusoids.
Product of sinusoids with same frequency
The product of any pair of sinusoids that have the same frequency will
produce a time series containing the sum of two sinusoids. One of the
sinusoids will have a frequency of zero (hence it will have a constant
value). The other sinusoid will have a frequency that is double the
frequency of the original sinusoids.
Ideally, the average value of the new time series will be equal to the
constant value of the sinusoid with zero frequency. This is because, ideally,
the average value of the other sinusoid will be zero.
The product of any pair of sinusoids that do not have the same frequency
will produce a new time series containing the sum of two sinusoids. One of
the new sinusoids will have a frequency that is the sum of the frequencies
of the two original sinusoids. The other sinusoid will have a frequency that
is the difference between the frequencies of the two original sinusoids.
Ideally, the average value of the new time series in this case will be equal to
zero, because ideally the average value of each of the sinusoids that make
up the time series will be zero.
Oops!
The third plot down from the top in Figure 1 shows the product of these two
sinusoids, which have the same frequency. If you examine the third plot,
you will notice several important characteristics.
A double-frequency sinusoid
By matching the peaks, you can determine that the frequency of the
sinusoid in the third plot is double the frequency of each of the top two
plots. (This is the sum of the frequencies of the two sinusoids that were
multiplied together.)
Next, you will notice that the amplitude of the sinusoid in the third plot is
half that of each of the first two plots. In addition, the entire sinusoid in the
third plot is in the positive value range.
The third plot is actually the sum of two sinusoids. One of the sinusoids has
a frequency of zero, giving a constant value of 0.5. This constant value of
0.5 is added to all the values in the other sinusoid, causing it to be plotted in
the positive value region.
Later on, we will compute the average value of the time series in the third
plot. Ideally, that average value will be the constant value produced by the
zero-frequency sinusoid.
Product of sinusoids with different frequencies
Now consider the bottom two plots in Figure 1 . The fourth plot down from
the top is a cosine function whose frequency is almost, but not quite the
same as the frequency of the sinusoid in the top plot. The sinusoid in the top
plot has 32 samples per cycle while the sinusoid in the fourth plot has 31
samples per cycle.
The time series in the bottom plot is the product of the time series in the
first and fourth plots.
Once again, this time series is the sum of two sinusoids. The frequency of
one is the difference between the two original frequencies. The frequency of
the other is the sum of the two original frequencies.
As you can see, the low-frequency component in the bottom plot in Figure
1 appears to be the beginning of a cosine function whose period is much
greater than the width of the plot (400 points).
Figure 2 shows another view of the bottom two plots from Figure 1 .
Figure 2. Products of sinusoids.
The difference between Figure 2 and Figure 1 is that while Figure 1 shows
only 400 points along the x-axis, Figure 2 shows 1200 points along the x-
axis. Thus, the horizontal scale in Figure 2 is significantly compressed
relative to the horizontal scale in Figure 1 .
Figure 2 lets you see a little more than one full cycle of the low-frequency
component of the time series produced by multiplying the two sinusoids.
The time series in the third plot down from the top is the product of the time
series in the top two plots. Again, this time series is composed of two new
sinusoids whose frequencies are the sum of and difference between the two
original frequencies.
A greater frequency difference
Because the frequency difference between the first two plots in Figure 3 is
considerably greater than was the case for the bottom plot of Figure 1 , the
frequency of the low frequency component of the third plot in Figure 3 is
considerably greater than was the case in Figure 1 .
Later on, we will compute the average value of the third plot in Figure 3 .
Ideally, the average value will be zero.
The fourth plot in Figure 3 shows a sinusoid having 16 samples per cycle.
The frequency of this sinusoid is double the frequency of the sinusoid in the
top plot.
The bottom plot in Figure 3 shows the product of the first and fourth plots.
As usual, this time series consists of the sum of two sinusoids whose
frequencies are the sum and the difference of the original frequencies.
We will also compute the average value of the bottom plot later on. Ideally,
the average value will be zero.
We may be tempted to say that the average value of a sinusoid is zero. After
all, the positive lobes of the sinusoid are shaped exactly like the negative
lobes. Therefore, every positive value is offset by a corresponding negative
value.
Next we will take a look at the computed average values of the time series
from Figures 1 and 3 that were produced by multiplying sinusoids.
[missing_resource: file:///M:/Baldwin/AA-
School/Connexions/Digital%20Signal%20Processing%20-
%20DSP/2-Time%20Series/3-Dsp00108-
Averaging%20Time%20Series/dsp00108fige.gif]
The red curve in Figure 5 shows the computed average value as a function
of the number of points included in the average. In other words, a particular
point on the red curve in Figure 5 represents the average value of all the
points on the black curve to the left of and including that point on the black
curve.
The blue horizontal line if Figure 5 shows the ideal average value for this
situation.
As more and more points are included in the average, the values of the
positive and negative peaks on the red curve approach the ideal blue line
asymptotically (except for a slight positive bias, which is the result of the
sampling process).
An expanded view
Figure 6 shows a greatly expanded view of the red average values in Figure
5.
The ideal value for this average is 0.5, and that is the value represented by
the blue line. The plot in Figure 6 shows the same horizontal scale as Figure
5 . However, the entire vertical plotting area in Figure 6 represents the
values from 0.48 to 0.52.
As you can see, the ideal value is never reached in Figure 6 except at
isolated points where the red curve crosses the horizontal line. Even if I
extended the horizontal axis to 1200 or more points, that would continue to
be the case.
A more serious case
Figure 7 computes and displays the average value of the bottom plot in
Figure 2 (recall that this plot shows 1200 points on the horizontal axis,
whereas Figure 5 shows only 400 points on the horizontal axis). Recall also
that this time series was produced by multiplying two sinusoids having
nearly the same but not exactly the same frequency.
As before, the black curve in Figure 7 shows the time series, and the red
curve shows the computed average value as a function of the number of
points included in the average.
(In this case, I didn't even bother to show the short axis
containing only 400 points. The horizontal axis in Figure 7
contains 1200 points, the same as in Figure 2 .)
In this case, the ideal average value is zero, as indicated by the green
horizontal axis. As you can see, even for a 1200-point averaging window,
the average value deviates significantly from the ideal. We will see the
detrimental impact of this problem later when I perform spectral analysis in
an attempt to separate two closely-spaced peaks in the frequency spectrum.
Figure 8 computes and displays the average value of the third plot down
from the top in Figure 3 . This plot was produced by multiplying the two
sinusoids in the top two plots in Figure 3 .
As before, the black curve in Figure 8 represents the time series, and the red
curve represents the computed average value of the time series as a function
of the number of points included in the average.
For this case also, the ideal average value is zero, as represented by the
green horizontal axis. The positive and negative peaks in the red average
value can be seen to approach the ideal value asymptotically within the 400
horizontal points plotted in Figure 8 .
Figure 9 computes and displays the average value of the bottom plot in
Figure 3 . This time series was produced by multiplying the top plot in
Figure 3 by the fourth plot in Figure 3 .
Figure 9. Computed average value of a time series.
Once again, the black curve in Figure 9 represents the time series, and the
red curve represents the computed average value of the time series as a
function of the number of points included in the average. In this case, the
average converges on zero rather nicely within the 400 points included on
the horizontal axis.
Hopefully, by this point, you understand how multiplying two time series
produces a new time series composed of the sum of all the products of the
individual sinusoids in the two original time series.
When each pair of sinusoids is multiplied together, they produce a new time
series consisting of two other sinusoids whose frequencies are the sum and
difference of the original pair of frequencies.
When an average is computed for a fixed number of points on the new time
series, the error in the average tends to be greater for cases where the
original frequency values were close together. This is because the period of
one of the new sinusoids becomes longer as the original frequencies
become closer. In general, the longer the period of the sinusoid, the more
points are required to get a good estimate of its average value.
There are many operations in DSP where this matters a lot. As mentioned
earlier, the computational requirements for DSP frequently boil down to
nothing more than multiplying a pair of time series and computing the
average of the product. You will see many examples of this as you continue
studying the modules in this series of tutorials on DSP.
Spectral analysis
(This example will illustrate and explain the results using graphs.
Future modules will provide more technical details on the DSP
operations involved.)
Several steps are involved
First, I will show you spectral data for several time series, each consisting
of a single sinusoid. The time series will have different lengths but the
individual sinusoids will have the same frequency. This will serve as
baseline data for the experiments that follow.
Then I will show you spectral data for several time series, each composed
of the sum of two sinusoids. These time series will have different lengths.
The sinusoids in each time series will have the same frequencies. I will
show you two cases that fall under this description. The frequency
difference for the two sinusoids in each time series will be small in one
case, and greater in another case.
Finally, I will show you spectral data for several time series, each composed
of the sum of two sinusoids. These time series will be different lengths, and
the sinusoids in each time series will have different frequencies. In
particular, the frequency difference between the two sinusoids in each time
series will be equal to the theoretical frequency resolution for a time series
of that particular length.
Keeping it simple
To keep this explanation as simple as possible, I will stipulate that all of the
sinusoids contained in the time series are cosine functions. There are no
sine functions in the time series.
(If the time series did contain sine functions, the process would
still work, but the explanation would be more complicated.)
Before I get into the results, I will provide a very brief description of how I
performed the Fourier transform for these experiments.
If the time series was shorter than 400 points, extend it to 400 points
by appending zero-valued points at the end.
Select the next frequency of interest.
Generate a cosine function, 400 samples in length, at that frequency.
Multiply the cosine function by the time series.
Compute the average value of the time series produced by multiplying
the cosine function by the time series.
Save the average value. Call it the real value for later reference.
Generate a sine function, 400 samples in length, at the same frequency.
Multiply the sine function by the time series.
Compute the average value of the time series produced by multiplying
the sine function by the time series.
Save the average value. Call it the imaginary value for later reference.
Compute the square root of the sum of the squares of the real and
imaginary values. This is the value of interest. Plot it.
The computed average value of this time series will converge on the value
of the constant with the quality of the estimate depending on the number of
points included in the average.
The computed average of this time series will converge on zero with the
quality of the estimate depending on the number of points in the average.
(As mentioned earlier, this process would work even if the time
series contained sinusoids other than cosine functions. However,
the explanation would be more complicated.)
The frequency of one of the sinusoids in the new time series will be the sum
of the frequencies of the sinusoidal component and the sine or cosine
function. The frequency of the other sinusoid will be the difference in the
frequencies between the sinusoidal component and the sine or cosine
function.
As you saw earlier, when this difference is very small, the frequency of the
new sinusoid will be very near to zero.
Ideally, the average value of the product should be zero when the frequency
of the original sinusoidal component is different from the sine or cosine
function by which it is multiplied. The computed average of this time series
will converge on zero with the quality of the estimate depending on the
number of points in the average.
Measurement error
However, (and this is very important), when the frequency of the original
sinusoid is very close to the frequency of the sine or cosine function, the
convergence on zero will be poor even for a large number of points in the
average.
With that as a preface, lets look at some graphs ( Figure 10 and Figure 11 )
resulting from spectral analyses. (These two figures show two different
views of the same data.)
Starting at the top in Figure 10 , the lengths of the five sinusoids were 80,
160, 240, 320, and 400 samples. (The lengths of the five sinusoids were
multiples of 80 samples.)
Extend to 400 samples for computation
As mentioned earlier, for the cases where the actual length of the sinusoid
was less than 400 samples, the length was extended to 400 samples by
appending an appropriate number of samples having zero values.
(This made it easy to compute and plot the spectrum for every
sinusoid over the same frequency range with the same number of
points in each plot.)
The spectrum was computed and plotted for each sinusoid at 400 individual
frequency points between zero and the folding frequency.
Even though the Fourier transform program averaged across 400 samples in
all cases, the effective averaging length was equal to the length of the
sinusoid. All product points outside that length had a value of zero and
contributed nothing to the average one way or the other.
A horizontally-expanded plot
As you can see in Figure 10 , there isn't much in the spectra to the right of
about 50 spectral points. That is as it should be since the single sinusoid in
each time series was at the low end of the spectrum.
Figure 11 shows the same data as Figure 10 with only the first 50 frequency
points plotted on the horizontal axis. The remaining 350 frequency points
were simply ignored. This provides a much better view of the structure of
the peaks in the different spectra.
I will begin the discussion with the bottom plot in Figure 11 , which is the
computed spectrum for the single sinusoid having a length of 400 samples.
A spectral line
Ideally, since the time series was a single sinusoid, the spectrum should
consist of a single non-zero value at the frequency of the sinusoid, (often
referred to as a spectral line) and every other value in the spectrum should
be zero.
(The bottom plot in Figure 11 has a large peak in the center with
every second point to the left and right of center having a zero
value. I will explain this structure in more detail later.)
Moving from the bottom to the top in Figure 11 , each individual plot shows
the result of shorter and shorter averaging windows. As a result, the
measurement error increases and the peak broadens for each successive plot
going from the bottom to the top in Figure 11 . The plot at the top, with an
averaging window of only 80 samples, exhibits the most measurement error
and the broadest peak.
(It should be noted, however, that even the spectra for the shorter
averaging windows have some zero-valued points. Once you
understand the reason for the zero-valued points, you can
correlate the positions of those points to the length of the
averaging windows in Figure 11 .)
Two spectral lines
Now I'm going to show you the detrimental impact of such spectral
measurement errors. In particular, the failure of the average to converge on
zero for short averaging windows limits the spectral resolution of the
Fourier transform.
I will create five new time series, each consisting of the sum of two
sinusoids with fairly closely-spaced frequencies. One sinusoid has 32
samples per cycle as in Figures 10 and 11. The other sinusoid has 26
samples per cycle.
As before, the lengths of the individual time series will be 80, 160, 240,
320, and 400 samples respectively.
Once again, let's begin with the plot at the bottom of Figure 12 . As you can
see, this spectrum shows two very distinct spectral peaks. Thus, for this
amount of frequency separation and a length of 400 samples, the Fourier
transform did a good job of separating the two peaks.
Before leaving this topic, there are a few more things that I want to show
you. If you go back and look at the bottom plot in Figure 11 , you will note
an interesting characteristic of that plot. In particular, starting at the peak
and moving outward in both directions, every second plotted value is zero.
I'm going to explain the reason for and the significance of this
characteristic.
(As I mentioned earlier, there are also zero-valued points in the
spectra of the time series with the shorter averaging windows.
Once you understand the reason for the zero-valued points, you
can correlate the positions of those points to the length of the
averaging window.)
To begin with, the Fourier transform program that was used to compute this
spectrum computed 400 values at equally spaced points between zero and
the folding frequency (only the first 50 values are shown in Figure 11 ).
Thus, each of the side-by-side rectangles in Figure 11 represents the
spectral value computed at one of the 400 frequency points.
The sinusoid that was used as the target for this spectral analysis had 32
samples per cycle. Since this sinusoid was generated mathematically
instead of being the result of sampling an analog signal, we can consider the
sampling frequency to be anything that we want.
For simplicity, let's assume that the sampling frequency was one sample per
second. This causes the sinusoid to have a period of 32 seconds and a
frequency of 0.03125 cycles per second.
At a sampling rate of one sample per second, the folding frequency occurs
at 0.5 cycles per second.
The top plot in Figure 14 shows the result of multiplying a cosine function
having a frequency of 0.03125 cycles per second (the frequency of the
sinusoid in the previous spectral analysis experiment) by a sine function
having a frequency of 0.02875 cycles per second.
The difference between the frequencies of the cosine function and the sine
function is 0.00250 cycles per second.
The second plot in Figure 14 shows the average value of the time series in
the first plot versus the number of samples included in the averaging
window.
It is very important to note that this average plot goes to zero when 400
samples are included in the average.
Similarly, the third plot in Figure 14 shows the product of the same cosine
function as above and another cosine function having the same frequency as
the sine function described above.
The fourth plot in Figure 14 shows the average value of the time series in
the third plot.
The first point at which both average plots go to zero at the same point on
the horizontal axis is at an averaging window of 400 samples.
(Both the real and imaginary values must go to zero in order for
the spectral value produced by the Fourier transform to go to
zero.)
(Note however, that you may not see the zero-valued points in the
spectrum if you don't compute the spectral values at exactly those
frequency values. This is the case for some of the plots in Figure
11 .)
The length of the time series for the bottom plot was 400 samples. Thus, the
separation of the two sinusoids matched the frequency resolution available
by performing a Fourier transform on that time series.
As you can see, the two peaks in the spectrum were resolved by the bottom
plot in Figure 15 .
Insufficient frequency resolution
The other four time series were shorter, having lengths of 80, 160, 240, and
320 samples respectively, from top to bottom.
However, even though the spectrum analysis on the 320-sample time series
hinted at a separation of the peaks, none of the spectrum analyses on the
time series that were shorter than 400 samples successfully separated the
peaks.
I'm going to show you one more picture and then call it a wrap for this
module. Figure 16 is similar to Figure 15 with one major difference.
As before, the five plots in Figure 16 show the first 50 points produced by
performing a Fourier transform on five different time series. Starting at the
top, the lengths of the time series were 80, 160, 240, 320, and 400 samples.
Also as before, each time series was the sum of two sinusoids with closely-
spaced frequencies. However, in Figure 16 , the difference between the
sinusoidal frequencies was different from one time series to the next.
The frequency of the lower-frequency peak was the same in all five cases.
Therefore, this peak should line up vertically for the five plots in Figure 16 .
If you examine Figure 16 , you will see that the peaks corresponding to the
two sinusoids were resolved for all five time series.
As would be expected, the peaks appear to be broader for the shorter time
series having the lower frequency resolution. The peaks are also separated
in all five cases. However, the peaks for the lower-frequency sinusoid don't
exactly line up vertically. Thus we see a small amount of measurement error
in the positions of the peaks
Summary
This module has presented a pseudo-mathematical discussion of issues
involving the averaging of time series, and the impact of those issues on
spectrum analysis.
Those averaging issues have an impact on many other areas of DSP as well,
but the detrimental effect is probably more obvious in spectrum analysis
than in other areas.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Housekeeping material
Baldwin begins with a discussion of averaging time series, and ends with a
discussion of spectral resolution, covering several related topics in
between.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it
possible for you to purchase a pre-printed version of the PDF file, you
should be aware that some of the HTML elements in this module may not
translate well into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales
nor do I know who does receive compensation. If you purchase such a
book, please be aware that it is a copy of a module that is freely available
on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Java1468-Plotting Engineering and Scientific Data using Java
Baldwin shows you how write a generalized plotting program that can be
used to plot engineering and scientific data produced by any object that
implements a very simple interface.
Table of contents
Preface
Viewing tip
Figures
Listings
Preview
A DSP example
Beginning of the class named Dsp002
Create data arrays
Beginning of the constructor
Create the convolution operator
Apply the convolution operator
Compute spectrum of each of two traces
The getNmbr method
The method named f1
Methods f2 through f5
Preface
Excellent language for engineering computations
However, there is a downside to this happy story. When doing this sort of
work, it is often very important to see the results of the experiments in the
form of graphs or plots. Unfortunately, the programming required to
produce graphical output from simple engineering and scientific
computational experiments cannot be accomplished using rudimentary
programming techniques. Rather, to do that job right requires considerable
expertise in Java programming.
Viewing tip
Figures
Figure 1 . Sample Display.
Figure 2 . Sample Display for Same Data with Different Plotting
Parameters.
Figure 3 . A Digital Signal Processing (DSP) Example.
Figure 4 . Graphic Display for Self-Test Class.
Figure 5 . Sample output format from Graph02.
Listings
Preview
Figure 1 shows a typical display produced by one of the plotting programs
that I will develop in this module. (The other program superimposes all of
the curves on the same set of axes instead of spacing them vertically as
shown in Figure 1 .)
While the plotting program itself is quite complex, the code required to
produce the data to be plotted can be very simple. For example, because of
the use of the Java Math library, only fourteen lines of simple Java code
were required to produce the data plotted in Figure 1 .
I will explain everything that you will need to know to cause the output
from your own engineering and scientific programs to be displayed by the
plotting program.
However, for those of you who may be interested, I will also discuss and
explain the plotting program later in this module.
Plotting format
As you can see in Figure 1 , the plotting program allows for plotting up to
five independent functions stacked vertically, each with the same vertical
and horizontal axes. This vertical stacking format makes it easy to compare
up to five plots at the same points on the horizontal axes.
If you need more than five functions, the number of functions can easily be
increased with a few minor changes to the program.
(I will also provide, but will not discuss, another version of the
program, named Graph02 , which superimposes up to five plots
on the same coordinate system. In some cases, that is a more
useful form of display. You will find a complete listing of this
program in Listing 40 near the end of the module.)
Plotting parameters
As you can also see in Figure 1 , a set of text fields and a button on the
bottom of the frame make it possible for the user to modify the plotting
parameters and to re-plot the same data with an entirely new set of plotting
parameters.
Figure 2 shows the same data as in Figure 1 , but plotted with a different set
of plotting parameters.
In the case of Figure 2 , the origin was moved to the left, the total expanse
of the horizontal axis was increased, and the space between the tic marks on
the vertical axis was increased from 20 units to 50 units.
(It will help you to see the differences if you will position two
browser windows side-by-side while viewing one display in one
browser window and viewing the other display in the other
browser window.)
At this point, you should be able to execute the program named Graph01 in
self-test mode by entering the following command at the command prompt
in the same directory where you compiled the program:
java Graph01
To use the plotting program with your own data generator program, do the
following:
Assume that your data-generator class is named MyData , and that you
have successfully compiled it in the same directory as the compiled version
of Graph01.
The next step is to enter the following command at the command prompt in
the same directory. (Note that this command differs from the command
given earlier. This command provides the name of your class as a
command-line argument following the name of the plotting program.)
When you do this, the plotting program should start pulling the necessary
data from your data-generator program and plotting that data in the format
shown in Figure 1 .
xMin and xMax - The values of the left and right ends of all horizontal
axes.
yMin and yMax - The values of the bottom and top of the vertical axis
in each plotting area. (Note that the different plotting areas are
identified by alternating white and gray backgrounds.)
xTicInt - The distance between tic marks on the x-axis.
yTicInt - The distance between tic marks on the y-axis.
xCalcInc - The distance between the points on the x-axis where values
for y are computed. (Unless your data-generator program is taking too
long to run, you should probably leave this set to 1.0 in order to get the
best quality plots.)
Each x-axis has a label at the left end and the right end. Similarly, each y-
axis has a label at the bottom and the top. These labels represent the values
at the extreme ends of the axes. For example in Figure 2 , the label 800
appears at the right end of each x-axis. This is value of the x-axis where the
axis intersects the border of the frame.
When adjusting the plotting parameters, keep in mind that the total width of
each of the plotting areas is slightly less than 400 pixels.
(You can easily increase this to full screen width by changing one
value in the Graph01 program and recompiling the program.
However, I had to keep it narrow in order to publish the images
in this publication format.)
While you can theoretically make the horizontal expanse of the x-axes as
wide as you wish, because of the pixel limitation, you cannot see details
that require a resolution greater than the number of pixels along the x-axis.
(This might be a good reason for you to modify the Graph01 program as
described above).
Implementing GraphIntfc01
On several occasions in this module, I have stated that the plotting program
can plot up to five functions. However, it doesn't have to plot all five
functions. The plotting program can be used (without modification) to plot
any number of functions from one to five.
The method named getNmbr , that you must define in your data-generator
program, must return an integer value between 1 and 5 that specifies the
number of functions to be plotted. The plotting program uses that value to
divide the total plotting surface into the specified number of plotting areas,
and plots each of the functions named f1 through fn in one of those plotting
areas.
As you can see in Listing 1 , each of these methods receives a double value
as an incoming parameter and returns a double value. In essence, each of
these methods receives a value for x and returns the corresponding value for
y.
One plotting area per method
Each of these methods provides the data to be plotted in one plotting area.
The method named f1 provides the data for the top plotting area, the
method named f2 provides the data for the first plotting area down from the
top, and so forth.
Each plotting area contains a horizontal axis. The plotting program moves
across the horizontal axis in each plotting area one step at a time (moving in
incremental steps equal to the plotting parameter named xCalcInc ) .
At each step along the way, the plotting program calls the method
associated with that plotting area, ( f1 , f2 , etc.) , passing the horizontal
position as a parameter to the method.
The plotting program doesn't know, and doesn't care how the method
decides on the value to return for each value that it receives as an incoming
parameter. The plotting program simply calls the methods to get the data,
and then plots the data.
Computed "on the fly"
For example, the returned values could be computed and returned "on the
fly" as is the case in the sample program named Graph01Demo , which we
will look at shortly.
On the other hand, the values could have been computed earlier and saved
in an array, as will be the case in the sample program named Dsp002
example that we will look at later.
The returned values could be read from a disk file, obtained from a database
on another computer, or obtained from any other source such as another
computer on the internet. All that matters is that when the plotting program
named Graph01 calls one of the five methods named f1 through f5 ,
passing a double value as a parameter, it expects to receive a double value
as a return value, and it will plot the value that it receives.
It is up to you
Listing 2 shows the beginning of the class definition, which names the class
Graph01Demo , and specifies that the class implements the interface
named GraphIntfc01 .
Recall from the earlier discussion that the method named getNmbr must
return an integer value between 1 and 5, which tells the plotting program
named Graph01 how many functions to plot.
Listing 4 shows the entire method named f1 . The output from this method
is plotted in the topmost plotting area of the display in Figure 1 . (This is the
area at the top with the white background.)
The curve plotted in the top-most plotting area with the gray background in
Figure 1 is produced by the method named f2 , which is shown in its
entirety in Listing 5 .
This is a simple cosine curve, which is computed on the fly. Each time the
method is called, the incoming parameter named x is used to calculate the
cosine of an angle in radians given by one-tenth the value of x . The cosine
of that angle is multiplied by 100 and returned.
The body of the method named f4 is similar to the body of the method
named f3 , except that f4 computes and returns sine values instead of cosine
values. Also, the value of x is used differently so that the period of the
curve produced by f4 is twice the period of the curve produced by f3 .
Finally, the bottom white plotting area in Figure 1 shows the output
produced by the method named f5 , which is shown in Listing 8 .
This method computes and returns the product of sine and cosine functions
identical to those discussed above.
The end of the class definition
Listing 8 also shows the closing curly brace that signifies the end of the
class definition for the class named Graph01Demo .
That's really all that you need to know to be able to make effective use of
the generalized plotting program named Graph01 . If you can define the
methods named f1 through f5 , which will return the required values for
your computational experiment, then you can make use of this program to
plot your data.
Lest you go away believing that this is all too trivial to be interesting, I am
going to show you another example that is far from trivial. In the next
example, I will demonstrate two of the most important operations in the
field commonly referred to as digital signal processing, or DSP for short.
Because many of you are unlikely to be familiar with the techniques and
terminology involved, the discussion will of necessity be fairly shallow.
However, I do want to show at least one example of how you can perform
substantive computational experiments using this approach.
A DSP example
In the field of DSP, the five individual plots shown in the plotting areas of
Figure 3 are commonly referred to as traces. I will use that terminology in
this discussion.
The top trace in the area with the white background shows about 256
samples of white random noise. When we get to the code, we will see that
this data was created using a Java pseudo-random number generator.
A convolution filter
The second trace from the top shows a 33-point narrow-band convolution
filter, which is simply a chunk taken out of a sinusoid whose frequency is
one-fourth the sampling frequency. In other words, the sinusoid is
represented by four samples per cycle.
The middle trace shows the result of applying the narrow-band convolution
filter to the white noise. The output from the convolution process was
amplified to bring it back into an appropriate amplitude range for visual
analysis.
If you compare the middle trace with the top trace, you will notice that
much of the high-frequency energy and much of the low-frequency energy
has been removed. Most of the energy in the middle trace appears to be
about the same frequency as the design frequency of the convolution filter
(which is what we would expect) .
The top three traces represent information in the time domain. The bottom
two traces represent information in the frequency domain.
The trace in the gray area immediately below the center is an estimate of
the spectral distribution of the white noise in the top trace. The spectrum
analysis was performed across the frequency range from zero frequency to
the sampling frequency.
While not perfectly flat, as would be the case for perfectly white noise, you
can see that the energy appears to be distributed across that entire range.
If you examine this trace carefully, you might notice that there is a point of
near symmetry in the middle. The values that you see above that point (the
folding frequency) are a mirror-image of the values that you see below that
point. (I will have more to say about this later.)
Unlike the spectral analysis of the white noise, this spectral analysis shows
two obvious peaks. One peak appears at one-fourth the sampling frequency,
and the other peak appears at three-fourths the sampling frequency.
Without getting into a lot of detail at this point, the point of symmetry that I
identified above is known as the Nyquist folding frequency. (See the earlier
module titled Dsp00104-Sampled Time Series .)
In order to be able to identify the frequency of a sine wave, you must have
at least two samples per cycle of the sine wave. The Nyquist folding
frequency is the frequency at which you have exactly two samples per
cycle.
As the frequency of the sine wave continues to increase beyond that point,
without a corresponding change in the sampling frequency, it is impossible
to determine from the samples so obtained whether the frequency is
increasing or decreasing.
The class used to produce the data displayed in Figure 3 is named Dsp002 .
A complete listing of this class definition is shown in Listing 38 near the
end of the module.
I will break this class down into fragments and briefly discuss it to show
how you can define significant classes and easily connect them to the
generalized plotting program named Graph01 .
As before, having compiled the class named Dsp002 , you would exercise it
by entering the following at a command prompt:
In this new class named Dsp002 , all the data is generated and stored in
array objects when an object of the class named Dsp002 is instantiated.
When the methods named f1 through f5 are called later, they simply retrieve
the data from the array objects and return that data to the plotting program.
The input noise, the filter, the filtered output, and the two spectra are
deposited in five arrays for later retrieval and display. The data in the five
arrays are returned by the methods named f1 , f2 , f3 , f4 , and f5
respectively.
The values that are returned by the methods are scaled for appropriate
display in the plotting areas provided by the program named Graph01 .
The code in Listing 9 establishes the data lengths for the white noise, the
convolution filter, the filtered output, and the spectrum.
Listing 9. Beginning of the class named Dsp002.
The code in Listing 10 creates the array objects that will be used to store the
data until it is retrieved by the methods named f1 through f5 .
The code in Listing 11 shows the beginning of the constructor for the class.
This code generates and saves the white noise in the array object named
data .
public Dsp002(){//constructor
Random generator = new Random(
new Date().getTime());
for(int cnt=0;cnt < data.length;
cnt++){
//Get data, scale it, remove the
// dc offset, and save it.
data[cnt] = 100*generator.
nextDouble()-50;
}//end for loop
Note that by virtue of the way this white noise is being generated, a
different seed is passed to the constructor for the Random class each time
an object of the Dsp002 class is instantiated. Thus, each new object
presents different random noise.
(In some cases, this may not be desirable and it may be
preferable to use the same seed each time an object is
instantiated.)
Note that the constant value of 4 in the denominator of the argument to the
cos method specifies the frequency of the cosine wave relative to the
sampling frequency. (In this case, the frequency of the cosine wave is one-
fourth the sampling frequency.)
Convolve01.convolve(data,dataLen,
operator,operatorLen,output);
The code in Listing 14 calls a static method named dft of a class named
Dft01 twice in succession to compute the spectra for the white noise and the
filtered noise, and to save those spectra in the appropriate arrays.
Dft01.dft(data,spectrumPts,
spectrumA);
Dft01.dft(output,spectrumPts,
spectrumB);
}//end constructor
All results have been computed and saved
That is the end of the constructor. At this point, all the results have been
computed and saved in the appropriate arrays for later retrieval by the
methods named f1 through f5 .
When the constructor finishes execution, the new object of the class named
Dsp002 has been created and occupies memory. The array objects
contained in the new object have been populated with five different types of
data. That data is available to be retrieved and plotted.
The class definition for the class named Dsp002 contains several more
methods that I need to explain. For example, the getNmbr method for this
class is exactly the same as for the class discussed earlier. As before, it
returns the integer value 5, telling the plotting program that there are five
plots to be generated. This method is shown in Listing 15 .
In all five cases, the purpose of the method is to fetch and return a value
from an array, where the incoming parameter will be converted to an array
index.
Following this, the method applies some logic to confirm that the index
value is within the bounds of the array. If not, the method returns the value
0.
If the index is within the array bounds, the method retrieves and returns the
value stored at that index location in the array.
Methods f2 through f5
Except for the scale factors applied to the data before returning it, the
behavior of the methods named f2 through f5 is essentially the same as the
behavior of the method named f1 . In each case, the method retrieves,
scales, and returns a value previously stored in an array. Therefore, I won't
discuss these other methods. You can view them in Listing 38 near the end
of the module.
If you already understand convolution, you will probably find the code in
this class straightforward. If not, the code will probably still be
straightforward, but the reason for the code may be obscure.
Listing 17. The class named Convolve01.
class Convolve01{
public static void convolve(
double[] data,
int dataLen,
double[] operator,
int operatorLen,
double[] output){
//Apply the operator to the data,
// dealing with the index
// reversal required by
// convolution.
for(int i=0;
i < dataLen-operatorLen;i++){
output[i] = 0;
for(int j=operatorLen-1;j>=0;
j--){
output[i] +=
data[i+j]*operator[j];
}//end inner loop
}//end outer loop
}//end convolve method
}//end Class Convolve01
To make a long story short, the class named Convolve01 provides a static
method named convolve , which applies an incoming convolution operator
to an incoming set of data and deposits the filtered data in an output array
whose reference is received as an incoming parameter.
This class could easily be broken out and put in a library as a stand-alone
class, or the convolve method could be added to a class containing a variety
of DSP methods.
The discrete Fourier transform (DFT)
class Dft01{
public static void dft(
double[] data,
int dataLen,
double[] spectrum){
//Set the frequency increment to
// the reciprocal of the data
// length. This is convenience
// only, and is not a requirement
// of the DFT algorithm.
double delF = 1.0/dataLen;
//Outer loop iterates on frequency
// values.
for(int i=0; i < dataLen;i++){
double freq = i*delF;
double real = 0;
double imag = 0;
//Inner loop iterates on time-
// series points.
for(int j=0; j < dataLen; j++){
real += data[j]*Math.cos(
2*Math.PI*freq*j);
imag += data[j]*Math.sin(
2*Math.PI*freq*j);
spectrum[i] = Math.sqrt(
real*real + imag*imag);
}//end inner loop
}//end outer loop
}//end dft
}//end Dft01
Once again, to make a long story short, this class provides a static method
named dft , which computes and returns the amplitude spectrum of an
incoming time series.
The amplitude spectrum is computed as the square root of the sum of the
squares of the real and imaginary parts.
As with convolution, this class could easily be broken out and put in a
library as a stand-alone class, or the dft method could be added to a class
containing a variety of DSP methods.
Now that you have examined the sample data-generator programs, some of
you may be interested in an explanation of the plotting program itself.
If you are interested only in how to use the plotting programs, and are not
interested in the programming details of the plotting programs, skip ahead
to the section titled Run the Program .
If you are interested in learning how the plotting programs do what they do,
keep reading.
Two very similar plotting programs are shown in the listings near the end of
the module. The program named Graph01 , shown in Listing 39 , can be
used to plot as many as five separate functions, each in its own plotting
area. Examples of the display produce by this program are shown in Figure
1 , Figure 2 , Figure 3 , and Figure 4 . I will briefly discuss this program in
the paragraphs that follow.
The program named Graph02 , shown in Listing 40 , can also be used to
plot as many as five separate functions. In this case, however, the graphs
produced by the functions are superimposed in the same plotting area. This
is simply an alternative display format. I won't discuss any of the particulars
of this program, but if you understand the program named Graph01 , you
will have no difficulty understanding this program named Graph02 as well.
As you learned in the earlier discussion, the class containing the functions
must also define a static method named getNmbr . This method takes no
parameters and returns the number of functions to be plotted. If this method
returns a value greater than 5, a NoSuchMethodException will be thrown.
The overall plotting surface is divided into the required number of equally
sized plotting areas. One function is plotted on Cartesian coordinates in
each plotting area.
If the getNmbr method returns a value less than 5, then the methods that
will not be called begin with f5 and work down toward f1 . For example, if
the value returned by getNmbr is 3, then the program will call the methods
named f1 , f2 , and f3 . While the methods named f4 and f5 must exist in
order to satisfy the interface, they won't be called. Therefore, it doesn't
matter what those methods return as long as it is type double.
As shown in Figure 1 , the plotting areas have alternating white and gray
backgrounds to make them easy to separate visually.
All curves are plotted in black. A Cartesian coordinate system with axes, tic
marks, and labels is drawn in red in each plotting area.
The Cartesian coordinate system in each plotting area has the same
horizontal and vertical scale, as well as the same tic marks and labels on the
axes.
The labels displayed on the axes, correspond to the values of the extreme
edges of the plotting area.
A test class
The program also compiles a test class named junk , which contains the
five required methods plus the method named getNmbr . This makes it
easy to compile and test the program in a stand-alone mode.
Usage instructions
Plotting parameters
This program provides the following text fields for user input, along with a
button labeled Graph . This allows the user to adjust the parameters and re-
plot the graph as many times as needed with as many different plotting
scales as may be needed:
The user can modify any of these parameters and then press the Graph
button to cause the five functions to be re-plotted according to the new
parameters.
A new object
Whenever the Graph button is pressed, the event handler for that button
instantiates a new object of the class that implements the GraphIntfc01
interface.
Depending on the nature of that class, this may be redundant in some cases.
However, it is useful in those cases where it is necessary to refresh the
values of instance variables defined in the class (such as a counter, for
example) .
(I will show you how to eliminate this feature from the plotting
program if you decide that it is unnecessary for your data.)
This program uses constants that were first defined in the Color class of
v1.4.0. Therefore, the program requires v1.4.0 or later to compile and
execute correctly.
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
class Graph01{
public static void main(
String[] args)
throws NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
if(args.length == 1){
//pass command-line parameter
new GUI(args[0]);
}else{
//no command-line parameter given
new GUI(null);
}//end else
}// end main
}//end class Graph01 definition
The beginning of the constructor for the GUI class is shown in Listing 21 .
GUI(String args)throws
NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
if(args != null){
//Save for use later in the
// ActionEvent handler
this.args = args;
//Instantiate an object of the
// target class using the String
// name of the class.
data = (GraphIntfc01)
Class.forName(args).
newInstance();
}else{
//Instantiate an object of the
// test class named junk.
data = new junk();
}//end else
The main purpose of the code in Listing 21 is to instantiated the object that
will provide the data to be plotted. If the user provided the name of a class
as a command-line argument, an attempt will be made to create a
newInstance of that class.
(In case you are unfamiliar with this approach, this is one way to
create an object of a class whose name is specified as a String at
runtime.)
Otherwise, the code in Listing 21 will instantiate an object of the test class
named junk (to be discussed later) .
pan1.add(new JLabel("xMax"));
pan1.add(xMaxTxt);
pan2.add(new JLabel("yMin"));
pan2.add(yMinTxt);
pan3.add(new JLabel("yMax"));
pan3.add(yMaxTxt);
pan4.add(new JLabel("xTicInt"));
pan4.add(xTicIntTxt);
pan5.add(new JLabel("yTicInt"));
pan5.add(yTicIntTxt);
pan6.add(new JLabel("xCalcInc"));
pan6.add(xCalcIncTxt);
Because of the routine nature of the code in Listing 23 , I will let the
comments suffice as an explanation.
The Canvas objects
If you refer back to Figure 1 , you will see that from one to five Canvas
objects are stacked vertically in the center of a frame.
Creates the JPanel object, and sets its layout property to GridLayout.
Creates the requisite number of objects of the MyCanvas class (which
extends Canvas), setting the background colors of the panels
alternately to white and gray.
Adds the MyCanvas objects to the cells in the grid. (Note that the
constructor for each MyCanvas object receives an integer that specifies
its position in the stack of MyCanvas objects. We will see how that
information is used later.)
Once again, I will let the comments serve as the explanation for this code.
setBounds(0,0,frmWidth,frmHeight);
setTitle("Graph01, " +
"Copyright 2002, " +
"Richard G. Baldwin");
setVisible(true);
Force a repaint
As you will see later, the actual plotting behavior of this program is defined
by the code in an overridden version of the paint method in the MyCanvas
class. I will discuss that code in some detail later.
One way to cause the code in the overridden paint method to be executed is
to call the repaint method on a reference to a MyCanvas object.
The code in Listing 26 calls the repaint method on each MyCanvas object
in sequence, to guarantee that they are properly painted when the GUI
object first becomes visible.
}//end constructor
Similar code will be used again later to cause the graphs to be repainted
each time the user presses the Graph button in the bottom right corner of
Figure 1 .
The code in Listing 26 also ends the constructor for the GUI object. When
the constructor finishes execution, the GUI appears on the screen with all
plotting areas properly painted.
shows the beginning of the event handler that is registered on the button to
cause the functions to be re-plotted.
Beginning of the re-plot code.
However, the code in Listing 27 goes beyond that. In particular, the code in
Listing 27 creates a new object from which to get the data that is to be
plotted.
In some cases, this may be required, depending on the nature of the class
from which that object is instantiated. In other cases, it may not be
necessary, and could slow down the re-plotting process.
If your class doesn't contain counters or other variables that need to be re-
initialized whenever you re-plot, you could probably safely remove or
disable the code in Listing 27 . This will make the program run faster,
although you may not be able to see the difference.
}//end actionPerformed
This code is very straightforward. It performs the following actions:
That brings us to the most interesting part of the program, the extended
Canvas class.
The class named MyCanvas is an inner class of the GUI class, which is
used to override the paint method in each of the plotting areas shown in
Figure 1 .
A simple constructor
The code in Listing 29 also defines the constructor, whose only purpose is
to save an integer identifying the position of this object in the vertical stack
of MyCanvas objects.
The beginning of the overridden paint method for the MyCanvas class is
shown in Listing 30 .
Listing 30. Beginning of the overridden paint method.
Calculates and saves the scale factors for converting from double
coordinate values to integer values in pixels.
Moves the plotting origin to the correct location.
Calls a method to draw the axes (in red) on the MyCanvas object.
Sets the color to black for the remainder of the plotting activity on the
object.
The code in Listing 31 initializes the beginning point for the plot. The
initial value for the x-coordinate is the left edge of the plotting area.
Listing 31. Get old coordinate values.
The initial value for the y-coordinate depends on which function is being
plotted on the MyCanvas object. Recall that each MyCanvas object
contains an instance variable that identifies its position in the vertical stack
of MyCanvas objects. The switch statement in Listing 31 uses that
information to call one of the five methods named f1 through f5 . This gets
the correct value for the y-coordinate based on the value of the x-coordinate
for each MyCanvas object.
The methods named getTheX and getTheY called by the code in Listing 31
convert the coordinate values from type double to integer values in pixels.
The method named getTheY also changes the sign on the data so that
positive y-values go up the screen rather than down the screen.
As it turns out, it is more difficult to draw and label the axes with tic marks
than it is to plot the actual data.
g.drawLine(getTheX(0.0),
getTheY(yMin),
getTheX(0.0),
getTheY(yMax));
Listing 34 shows the methods called from the code in Listing 33 to actually
draw the tic marks on the axes.
}//end xTics
//---------------------------------//
}//end yTics
Listing 35 also marks the end of the inner class named MyCanvas and the
end of the class named GUI .
Copy the code for the interface from Listing 1 into a Java source file named
GraphIntfc01.java .
Compile and run the program named Graph01 with no command-line
arguments. This should use the internal test class named junk discussed
earlier to produce a display similar to that shown in Figure 4 .
Once you have the display on your screen, make changes to the plotting
parameters in the text fields at the bottom and press the button labeled
Graph . When you do, you should see the same functions being re-plotted
with different plotting parameters.
Once that is successful, copy the code in Listing 37 into a file named
Graph01Demo.java . Copy the code in Listing 38 into a file named
Dsp002.java .
Compile these two files. Rerun the plotting program named Graph01
providing Graph01Demo as a command-line argument. Also rerun the
plotting program providing Dsp002 as a command-line argument. This
should produce displays similar to Figure 1 and Figure 3 . (You may need to
adjust some of the plotting parameters at the bottom to make them match.
Also remember that Figure 3 was produced using random data so it won't
be possible to match it exactly.)
You must be running Java version 1.4 or later to successfully compile and
execute this program.
Summary
I provided two generalized plotting programs in this module. One of the
programs plots up to five functions in a vertical stack. The other program
superimposes the plots for up to five functions on the same Cartesian
coordinate system.
/* File Graph01Demo.java
Copyright 2002, R.G.Baldwin
/* File Dsp002.java
Copyright 2002, R.G.Baldwin
public Dsp002(){//constructor
//Generate and save some wide-band
// random noise. Seed with a
// different value each time the
// object is constructed.
Random generator = new Random(
new Date().getTime());
for(int cnt=0;cnt < data.length;
cnt++){
//Get data, scale it, remove the
// dc offset, and save it.
data[cnt] = 100*generator.
nextDouble()-50;
}//end for loop
}//end constructor
//---------------------------------//
//The following six methods are
// required by the interface named
// GraphIntfc01.
public int getNmbr(){
//Return number of functions to
// process. Must not exceed 5.
return 5;
}//end getNmbr
//---------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
//This version of this method
// returns the random noise data.
// Be careful to stay within the
// array bounds.
if(index < 0 ||
index > data.length-1){
return 0;
Listing 38. Dsp002.java,
}else{
return data[index];
}//end else
}//end f1
//---------------------------------//
public double f2(double x){
//Return the convolution operator
int index = (int)Math.round(x);
if(index < 0 ||
index > operator.length-1){
return 0;
}else{
//Scale for good visibility in
// the plot
return operator[index] * 50;
}//end else
}//end f2
//---------------------------------//
public double f3(double x){
//Return filtered output
int index = (int)Math.round(x);
if(index < 0 ||
index > output.length-1){
return 0;
}else{
//Scale to approx same p-p as
// input data
return output[index]/6;
}//end else
}//end f3
//---------------------------------//
public double f4(double x){
//Return spectrum of raw data
int index = (int)Math.round(x);
if(index < 0 ||
Listing 38. Dsp002.java,
index > spectrumA.length-1){
return 0;
}else{
//Scale for good visibility in
// the plot.
return spectrumA[index]/10;
}//end else
}//end f4
//---------------------------------//
public double f5(double x){
//Return the spectrum of the
// filtered data.
int index = (int)Math.round(x);
if(index < 0 ||
index > spectrumB.length-1){
return 0;
}else{
//Scale for good visibility in
// the plot.
return spectrumB[index]/100;
}//end else
}//end f5
}//end Dft01
//===================================//
/* File Graph01.java
Copyright 2002, R.G.Baldwin
class Graph01{
public static void main(
String[] args)
throws NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
if(args.length == 1){
//pass command-line parameter
new GUI(args[0]);
}else{
//no command-line parameter given
new GUI(null);
}//end else
}// end main
}//end class Graph01 definition
//===================================//
//Constructor
GUI(String args)throws
NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
if(args != null){
//Save for use later in the
// ActionEvent handler
this.args = args;
//Instantiate an object of the
// target class using the String
// name of the class.
data = (GraphIntfc01)
Class.forName(args).
newInstance();
}else{
//Instantiate an object of the
Listing 39. Graph01.java.
// test class named junk.
data = new junk();
}//end else
pan1.add(new JLabel("xMax"));
pan1.add(xMaxTxt);
pan2.add(new JLabel("yMin"));
pan2.add(yMinTxt);
pan3.add(new JLabel("yMax"));
pan3.add(yMaxTxt);
pan4.add(new JLabel("xTicInt"));
pan4.add(xTicIntTxt);
pan5.add(new JLabel("yTicInt"));
pan5.add(yTicIntTxt);
pan6.add(new JLabel("xCalcInc"));
pan6.add(xCalcIncTxt);
setBounds(0,0,frmWidth,frmHeight);
setTitle("Graph01, " +
"Copyright 2002, " +
"Richard G. Baldwin");
setVisible(true);
}//end constructor
//---------------------------------//
}//end actionPerformed
//---------------------------------//
g.drawLine(getTheX(0.0),
getTheY(yMin),
getTheX(0.0),
getTheY(yMax));
Listing 39. Graph01.java.
//Draw the tic marks on axes
xTics(g);
yTics(g);
}//end drawAxes
//---------------------------------//
}//end xTics
//---------------------------------//
}//end yTics
//---------------------------------//
/* File Graph02.java
Copyright 2002, R.G.Baldwin
f1: BLACK
f2: BLUE
f3: RED
f4: MAGENTA
f5: CYAN
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
class Graph02{
public static void main(
String[] args)
throws NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
Listing 40. Graph02.java.
if(args.length == 1){
//pass command-line parameter
new GUI(args[0]);
}else{
//no command-line parameter given
new GUI(null);
}//end else
}// end main
}//end class Graph02 definition
//===================================//
//Constructor
GUI(String args)throws
NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
if(args != null){
//Save for use later in the
// ActionEvent handler
this.args = args;
//Instantiate an object of the
// target class using the String
// name of the class.
data = (GraphIntfc01)
Class.forName(args).
newInstance();
}else{
//Instantiate an object of the
// test class named junk.
data = new junk();
}//end else
pan1.add(new JLabel("xMax"));
pan1.add(xMaxTxt);
pan2.add(new JLabel("yMin"));
pan2.add(yMinTxt);
pan3.add(new JLabel("yMax"));
pan3.add(yMaxTxt);
pan4.add(new JLabel("xTicInt"));
pan4.add(xTicIntTxt);
pan5.add(new JLabel("yTicInt"));
pan5.add(yTicIntTxt);
pan6.add(new JLabel("xCalcInc"));
Listing 40. Graph02.java.
pan6.add(xCalcIncTxt);
setBounds(0,0,frmWidth,frmHeight);
setTitle("Graph02, " +
"Copyright 2002, " +
"Richard G. Baldwin");
setVisible(true);
}//end constructor
//---------------------------------//
}//end actionPerformed
//---------------------------------//
g.drawLine(getTheX(0.0),
getTheY(yMin),
getTheX(0.0),
getTheY(yMax));
//---------------------------------//
}//end xTics
//---------------------------------//
}//end yTics
//---------------------------------//
Miscellaneous
This section contains a variety of miscellaneous information.
Baldwin shows you how write a generalized plotting program that can be
used to plot engineering and scientific data produced by any object that
implements a very simple interface.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it
possible for you to purchase a pre-printed version of the PDF file, you
should be aware that some of the HTML elements in this module may not
translate well into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales
nor do I know who does receive compensation. If you purchase such a
book, please be aware that it is a copy of a module that is freely available
on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Java1489-Plotting 3D Surfaces using Java
Learn how to write a Java class that uses color to plot 3D surfaces in six
different formats and a wide range of sizes. The class is extremely easy to use.
You can incorporate the 3D plotting capability into your own programs by
inserting a single statement into your programs.
Table of contents
Preface
Viewing tip
Figures
Listings
General Discussion
Grayscale plot
Color Shift plot
Color Contour plot
A logarithmic conversion
Extremely easy to use
Preview
Constructor
Parameters
A calibration scale
Normalization
A 3D parabola
Local variables
A 3D parabolic surface
Display six surface images
The constructor
Overridden paint method for CanvasType0surface class
Instantiate a Color object
Set colors and draw squares
Draw the optional red axes
Beginning of the class named CanvasType0scale
The overridden paint method
Preface
In one of my earlier modules titled Plotting Engineering and Scientific Data
using Java , I published a generalized 2D plotting program that makes it easy
to cause other programs to display their outputs in 2D Cartesian coordinates .
I have used that plotting program in numerous modules since I originally
published it several years ago. Hopefully, some of my readers have been using
it as well.
In this module, I will present and explain a 3D surface plotting program that is
also very easy to use.
Numerous Java graphics libraries are available from various locations on the
web. Some are of high quality, and some are not. Unfortunately, many of
those libraries have a rather substantial learning curve.
The purpose of this program
It is not the purpose of the class that I will provide in this module to compete
with those graphics libraries. Rather, this class is intended to make it possible
for an experienced Java programmer to incorporate 3D surface plotting
capability into a Java program with a learning curve of three minutes or less.
(If you are an experienced Java programmer, you can start your
three-minute learning-curve clock right now. If you are not an
experienced Java programmer, it may take a little longer but should
still be easy. If you need to work on your Java programming skills,
see Object-Oriented Programming (OOP) with Java .)
All that's necessary to use this class to plot your own 3D surfaces is to copy
and compile the source code in Listing 29 near the end of this module. Then
include a statement similar to the following in your program:
new ImgMod29(data,blockSize,true,0);
The second parameter named blockSize specifies the size of one side of a
square array of colored pixels in the final plot that will represent each
elevation point on your 3D surface. Set this to 0 if you are unsure as to what
size square you need.
(If you look very carefully, you may be able to see a small white
square at the center of the middle image in Figure 1 . This is a nine-
pixel square produced by a blockSize value of 3.)
The third parameter specifies whether or not you want to have optional axes
drawn on the plot. (See Figure 3 for examples of plots with and without the
axes.) A parameter value of true causes the axes to be drawn. A value of false
causes the axes to be omitted.
If you are unsure as to which format would be best for your application, just
start with a value of 0. Then try all six formats to see which one works best
for you.
On the other hand, if you would like to learn how the class does what it does,
and perhaps use your programming skills to improve it, keep reading.
Hopefully, once you have finished the module, you will have learned quite a
lot about plotting 3D surfaces using color in Java.
Viewing tip
I recommend that you open another copy of this module in a separate browser
window and use the following links to easily find and view the Figures and
Listings while you are reading about them.
Figures
Listings
General Discussion
Displaying 3D data can be fairly difficult
(At least that was true before the advent of 3D printers. However,
as of October 2015, a 3D printer is not readily available for routine
use by most people.)
Grayscale plots
Color Shift plots
Color Contour plots
Using light and shadows to render the surface in ways that simulate a
photograph
Labeled Numeric Contour plots
Isometric Drawings
A 3D plotting program
I will provide and explain a program in this module that makes it very easy to
display a 3D surface as a Grayscale plot, a Color Shift plot, or a Color
Contour plot. The program supports six different plotting formats. Three of
those plotting formats are illustrated in Figure 1 .
(The images shown in Figure 1 are different views of the wave-
number spectrum resulting from performing a 2D Fourier
Transform on a box in the space domain. The use of 2D Fourier
Transforms will be the main topic of a future module.)
Figure 1 shows the same 3D surface plotted using three different plotting
formats. Going from left to right, Figure 1 shows:
Grayscale plot
Color Shift plot
Color Contour plot
Grayscale plot
The plot on the left in Figure 1 is the old standby method in which the
elevation of each point on the surface is represented by a shade of gray with
the highest elevation being white and the lowest elevation being black.
A calibration scale
For example, the scale on the Grayscale image shows a smooth gradient from
black to white going from left to right. The shade of gray shown at the
midpoint on the calibration scale represents the elevation that is halfway
between the lowest elevation and the highest elevation.
The image in the center in Figure 1 shows the same surface plotted using a
smooth gradient from blue at the low end through aqua, green, and yellow to
red at the high end. In addition, this plotting format sets the lowest elevation
to black and the highest elevation to white so that these two elevations are
obvious in the plot.
(Many years ago, when I was in the SONAR business, the general
rule of thumb was that a typical human can discern only about
seven shades of gray from black to white inclusive. Obviously a
typical human can discern more than seven different colors.)
By using black to indicate the lowest elevation, (in addition to using shades of
blue to indicate low elevations) , it is easy to determine the exact locations of
the lowest elevations in the Color Shift plot in the center of Figure 1 . On the
other hand, the locations of the lowest elevations are not discernable in the
Grayscale plot at the left Figure 1 .
Also, it is obvious from the Color Shift plot that the surface has four minor
peaks at the edges of the plot. Although the Grayscale plot has a slight hint of
those minor peaks, they are certainly not obvious.
By comparing the color at the top of the minor peaks to the color scale below
the Color Shift surface, it is possible to estimate that the elevation of the
minor peaks is probably somewhere between twenty-five and fifty percent of
the elevation of the main central peak. It is clearly impossible to glean that
kind of information from the Grayscale plot of the same surface.
While the highest elevation is pretty well indicated in the Grayscale plot also,
if the central peak were not symmetric in all four quadrants, there might be
some uncertainty as to the exact location of the highest elevation.
The ranges of the surface elevations colored red, green, and blue can also be
estimated but with less certainty.
The Color Contour plot at the right in Figure 1 is similar to a contour map
without labels on the contours. Each color traces out a constant elevation on
the surface. The elevation indicated by a given color on the 3D surface can be
determined by the position of that color in the calibration scale at the bottom
of the image.
(This program quantizes the range from the lowest to the highest
elevation into 23 levels. Therefore, the accuracy of an elevation
estimate is good to only about one 23rd of that total range.
However, it would be an easy matter to increase the number of
quantization levels used in this program, thereby improving the
accuracy of elevation estimates.)
For example, the blue contour that surrounds the central peak traces out the
shape of an elevation that is about three levels up from the lowest elevation
(as seen on the calibration scale) . The red contour that surrounds the central
peak traces out the shape of an elevation that is about five levels down from
the highest elevation. The aqua at the center of each of four minor peaks
establishes their peak elevation to be about thirty-five percent of the elevation
of the central peak (based on the position of aqua in the calibration scale) .
More minor peaks
This plotting format also exposes four more minor peaks at the corners of the
plot. The light gray color indicates that the level of these peaks is about two
levels up from the lowest elevation. These four peaks are barely visible in the
Color Shift plot in the center, and their elevation is clearly not quantifiable in
that plotting format. They are not visible at all in the Grayscale plot on the left
end of Figure 1 .
To determine the elevation associated with a particular color, all you need to
do is to locate that color on the calibration scale and determine its position
relative to the colors at the ends. That will tell you the elevation associated
with that color relative to the highest elevation and the lowest elevation. For
example, the color at the exact center of the calibration scale represents an
elevation that is half way between the lowest elevation and the highest
elevation.
Once again, this image shows that the elevations of the four minor peaks on
the edges match the color aqua on the calibration scale. Judging from the
position of the color aqua on the calibration scale, the elevation of each of the
four minor peaks is about thirty-five percent of the elevation of the major
peak in the center.
This information is clearly not available from the Grayscale plot. It is also not
available with this degree of accuracy from the Color Shift plot.
(All that we can tell from the Color Shift plot is that the minor
peaks are some shade of green, which represents a rather large
range of possible elevations.)
The Color Shift plot does a better job of identifying the locations of the lowest
elevations than does the Color Contour plot. This is because the color black
was dedicated to that purpose in the Color Shift plot, but was used to
represent a range of elevations in the Color Contour plot.
(The color black could also be dedicated to identifying the lowest
elevation in the design of the Color Contour plot, in which case,
both schemes would be equal in this regard.)
Both plots show that while the valleys between the central and minor peaks
are very deep, they aren't quite as deep as the lowest elevation. They are blue
in the Color Shift plot and gray in the Color Contour plot. Because the gray
color represents a somewhat smaller elevation range in the Color Contour plot
than the blue represents in the Color Shift plot, the elevation of the valley is
defined more accurately in the Color Contour plot.
A logarithmic conversion
Sometimes when plotting data, it is useful to plot the logarithm of the data
values instead of the raw values.
The three images in the top row of Figure 2 are reproductions of the three
images in Figure 1 . They were included in Figure 2 for comparison with the
bottom three images in Figure 2 .
The bottom three images in Figure 2 were produced in exactly the same way
as the top three images except that prior to creating the image the elevation
values for the surface were converted to the log base 10 of the raw elevation
values.
Thus, Figure 2 shows the same 3D surface plotted using six different plotting
formats. Going from left to right and top to bottom, the six images illustrate:
Grayscale (linear)
Color Shift (linear)
Color Contour (linear)
Grayscale with logarithmic data flattening
Color Shift with logarithmic data flattening
Color Contour with logarithmic data flattening
A similar discussion holds regarding the two middle plots in Figure 2 . The
log version on the bottom identifies the location of the minima more closely
than does the linear plot at the top.
You can also see from the bottom right image of Figure 2 that the central peak
of the log of the surface is much broader than the central peak for the raw
surface at the top right. This is indicated by the width of the white and yellow
areas in the log version as compared to the white and yellow areas in the raw
version.
In addition, the elevations of the log data for the minor peaks at the four sides
are almost as high as the elevation of the central peak, as indicated by the
orange or yellow color at the top of the minor peaks.
The elevations of the tops of the four minor peaks at the corners are perhaps
seventy-five percent of the elevation of the central peak as indicated by the
pinkish color of those minor peaks in the log version.
(Recall that the elevation of the minor peaks at the corners is only
about two levels up from the lowest elevation in the raw surface
data.)
All of this flattening was caused by converting the raw surface elevations to
the log of the surface elevations before producing the surface plot.
A plotting format that works best for one surface doesn't necessarily work
best for all surfaces. Therefore, it is useful for the program to be able to plot
the same surface using different plotting formats.
One of the main objectives in the development of this class was to make it
very easy to use. No fancy programming is required to use the class and
produce the plots. All that is required is to instantiate an object of the class
named ImgMod29 , passing an array of data to be plotted along with a few
other parameters to the constructor. Basically the parameters (in addition to
the data array) specify:
Which of the three main formats to use, Grayscale, Color Shift, or Color
Contour.
Whether or not to convert to logarithmic values before plotting.
Whether or not to draw the red axes shown in Figure 1 and Figure 2 .
How many pixels in the final output should be used to represent a single
point on the surface. (For example, the plots in Figure 1 and Figure 2
use a square of nine pixels to represent each point on the 3D surface.)
If multiple plots in different formats are needed for a given set of data, all that
is required is to instantiate multiple objects of the class named ImgMod29
passing the same data array with different parameters to the constructor for
each plot. Be aware, however, that all of the plots will be produced in a stack
in the upper left corner of the screen. You must physically move the plots on
the top in order to be able to view the plots lower down in the stack.
Preview
The purpose of the program that I will present and explain in this module is to
display a 3D surface using color (or shades of gray) to represent the elevation
of each point on a 3D surface.
Constructor
When an object of the class is constructed, it plots the 3D surface using one of
six possible formats representing the elevation of each point on the surface
with a color or a shade of gray.
Parameters
double[][] dataIn
int blockSize
boolean axis
int display
blockSize
The value of the parameter named blockSize defines the size of a colored
square in the final display that represents a single input surface elevation
value. For example, if blockSize is 1, each input surface value is represented
by a single pixel in the display. If blockSize is 5, each input surface value is
represented by a colored square having 5 pixels on each side (25 pixels in a
square) .
The test code in the main method displays a surface having 59 values along
the horizontal axis and 59 values along the vertical axis. Each elevation value
on the surface is represented in the final display by a colored square that is 2
pixels on each side.
axis
The parameter named axis specifies whether optional red axes will be drawn
on the display with the origin at the center as shown in Figure 1 .
display
The parameter named display specifies one of six possible display formats.
The value of display must be between 0 and 5 inclusive.
display = 0
This value for the display parameter specifies a Grayscale plot with a smooth
gradient from black at the minimum to white at the maximum.
display = 1
This value for the display parameter specifies a Color Shift plot with a smooth
gradient from blue at the low end through aqua, green, and yellow to red at
the high end. The minimum elevation is colored black. The maximum
elevation is colored white.
display = 2
This value for the display parameter specifies a Color Contour plot. The
surface is subdivided into 23 levels and each of the 23 levels is represented by
one of the following colors in order from lowest to highest elevation:
Color.BLACK
Color.GRAY
Color.LIGHT_GRAY
Color.BLUE
new Color(100,100,255)
new Color(140,140,255)
new Color(175,175,255)
Color.CYAN
new Color(140,255,255)
Color.GREEN
new Color(140,255,140)
new Color(200,255,200)
Color.PINK
new Color(255,140,255)
Color.MAGENTA
new Color(255,0,140)
Color.RED
new Color(255,100,0)
Color.ORANGE
new Color(255,225,0)
Color.YELLOW
new Color(255,255,150)
Color.WHITE
Note that some of the colors in the above list refer to named color constants in
the Color class. Others refer to new Color objects constructed by mixing the
specified levels of red, green, and blue.
display = 3, 4, and 5
These values for the display parameter specify that the surface is to be plotted
in the same format as for display values 1, 2, and 3, except that the surface
elevation values are rectified (made positive) and converted to log base 10
before being represented by a color and plotted.
A calibration scale
Normalization
Regardless of whether the surface elevation values are converted to log values
or not, the surface values are normalized to cause them to extend from 0 to
255 before converting the elevation values to color and plotting them. The
lowest elevation ends up with a value of 0. The highest elevation ends up with
a value of 255.
This is a Grayscale plot or a log Grayscale plot as shown at the left side of
Figure 2 . The highest normalized elevation with a value of 255 is painted
white. The lowest normalized elevation with a value of 0 is painted black. The
surface is represented using shades of gray.
The shade changes from black to white in a uniform gradient as the
normalized surface elevation values progress from 0 to 255.
This is a Color Shift plot or log Color Shift plot as shown in the center of
Figure 2 . The lowest normalized elevation is painted black and the highest
normalized elevation is painted white. (Black and white overwrite blue and
red for these two elevation values.)
The color changes from blue through aqua, green, and yellow to red in a
smooth gradient as the normalized surface values progress from 1 to 254.
(Values of 0 and 255 would be pure blue and pure red if they were not painted
black and white.)
This is a Color Contour plot or log Color Contour plot as shown at the right
side of Figure 2 . The highest normalized elevation with a value of 255 is
painted white. The lowest normalized elevation with a value of 0 is painted
black.
This display format is similar to a contour map where each distinct color
traces out a constant elevation level on the normalized surface being plotted.
When the program is executed, the six surfaces are stacked in the upper left
corner of the screen. (You must physically move the images on the top to see
the images on the bottom.) The stacking order of the surfaces from bottom to
top is based on the values of the display parameter in the order 0, 1, 2, 3, 4, 5.
Some of the surfaces show axes and some do not. This is controlled by the
value of the constructor parameter named axis . A true value for axis causes
the axes to be drawn.
A window listener
The program was tested using J2SE 8.0 and Win 7. Because the program uses
Java features that were introduced in J2SE 5.0, it may not compile
successfully with earlier versions of Java.
Listing 1 also defines a value of 2 for the blockSize parameter. This variable
will be passed to the constructor for the ImgMod29 class, causing each
elevation value to be plotted as a small square of four pixels, two pixels on
each side of the square.
A 3D parabolic surface
I will allow you to evaluate this code on your own. It creates a 3D surface
with the lowest elevation at the upper left corner and the highest elevation at
the lower right corner. The surface is a one-quarter section of a 3D parabola,
as shown in Figure 3 .
Listing 3 shows the code that causes the 3D surface elevation data to be
displayed as six independent images (each statement in Listing 3 produces
one output image) .
new ImgMod29(data,blockSize,true,0);
new ImgMod29(data,blockSize,false,1);
new ImgMod29(data,blockSize,true,2);
new ImgMod29(data,blockSize,true,3);
new ImgMod29(data,blockSize,false,4);
new ImgMod29(data,blockSize,true,5);
All that is necessary for another program to incorporate this class to display a
3D surface is to instantiate an object of the class named ImgMod29 passing
four parameters to the constructor.
The parameters
The second parameter is the blockSize . This int value specifies the size of
one side of a square of pixels, (all of the same color) that will be used to
represent each surface elevation value in the final display. As mentioned
earlier, this value was set to 2 in the sample displays produced by the main
method. However, it could have been any reasonable value such as 5, 10, or
15 for example.
The third parameter is true if you want the red axes to be drawn and is false
otherwise (as shown in Figure 3 ) .
0 - Grayscale (linear)
1 - Color Shift (linear)
2 - Color Contour (linear)
3 - Grayscale with logarithmic data conversion before plotting
4 - Color Shift with logarithmic data conversion before plotting
5 - Color Contour with logarithmic data conversion before plotting
Listing 3 instantiates six such objects to display the same 3D surface, one for
each plotting format as shown in Figure 3 . Some of the objects display the
axes and others do not. All use a blockSize value of 2.
Each display object appears in the upper-left corner of the screen. Thus, when
two or more such objects are instantiated, they appear as a stack. It is
necessary to physically move those on top of the stack to see those further
down.
Listing 4 shows the beginning of the class named ImgMod29 and the
beginning of the constructor for the class.
The meaning and purpose of each of the constructor parameters was explained
earlier, so I won't repeat that explanation here. The code in Listing 4 is
straightforward and should not require further explanation.
The code in Listing 5 uses the incoming value of the display parameter to
establish the display format if the value of display is 3, 4, or 5.
Listing 5. Establish display format for log conversion.
if(display == 3){
displayType = 0;
logPlot = true;
}else if(display == 4){
displayType = 1;
logPlot = true;
}else if(display == 5){
displayType = 2;
logPlot = true;
}else if((display > 5) || (display < 0)){
System.out.println(
"DisplayType input error,
terminating");
System.exit(0);
}//end if
The default display format is one of the three basic types without log
conversion of the surface elevation data. (The value of logPlot is set to false
in Listing 4 .) If the incoming parameter value is 3, 4, or 5, the code in Listing
5 establishes the display format as one of the three basic types with log
conversion of the surface elevation data prior to plotting.
(Note that the code in Listing 5 sets the value of the variable named
logPlot to true. The value stored in this variable will be used later
to determine if log conversion of the elevation data is required.)
These three basic types without log data conversion are shown from left to
right in Figure 1 . The three basic types are shown with log conversion from
left to right in the bottom rows of Figure 2 and Figure 3 .
Listing 6 makes a working copy of the input data to avoid damaging the
original data. This is done to protect the data belonging to the program that
instantiates an object of the class ImgMod29 .
The code in Listing 7 uses the log10 method of the Math class to perform a
log conversion of the surface elevation data if the value of logPlot is true.
Listing 7. Convert to log data if required
Here are a couple of restrictions taken from Oracle's documentation that apply
to the use of the method named log10 :
If the argument is NaN or less than zero, then the result is NaN.
If the argument is positive zero or negative zero, then the result is
negative infinity.
Because of the first restriction, the code in Listing 7 converts all negative
values into positive values before performing the conversion. Because of the
second restriction, no attempt is made to compute the log of elevation values
of zero.
After converting the elevation data to log form, (or not converting as the case
may be) , the code in Listing 8 calls the method named scaleTheSurfaceData
to normalize the elevation data by squeezing it into the integer range between
0 and 255 inclusive. When this method returns, the lowest elevation has a
value of 0 and the highest elevation has a value of 255.
scaleTheSurfaceData();
The elevation data is normalized to this range to make it easier later to form
relationships between the elevation values and allowable color values.
double min;
double max;
//This method is used to scale the surface
data
// to force it to fit in the range from 0 to
// 255.
void scaleTheSurfaceData(){
//Find the minimum surface value.
min = Double.MAX_VALUE;
for(int row = 0;row < dataHeight;row++){
for(int col = 0;col < dataWidth;col++){
if(data[row][col] < min)
min = data[row][col];
}//end col loop
}//end row loop
Return now to the discussion of the constructor for the ImgMod29 class.
The code in Listing 10 uses the value of displayType to make a decision and
to instantiate a pair of objects from two of six different inner classes, each of
which extends the class named Canvas . One of the objects in the pair is used
to display the 3D surface according to a specified format. The other object in
the pair is used to display the calibration strip below the surface display.
Listing 10. Create an appropriate pair of Canvas objects.
The default layout manager for a Frame object is BorderLayout . The code
in Listing 11 adds one of the above-instantiated objects to the center location
of the Frame , and adds the other object to the South location of the Frame .
This produces the format with the surface plot above the calibration scale as
shown by any of the images in Figure 1 through Figure 3 .
After adding the two objects to the Frame , Listing 11 calls the pack method
on the Frame to cause the size of the Frame to close in around the two
objects.
Finally, Listing 11 sets the title on the Frame and makes the s visible.
Register an anonymous WindowListener object
addWindowListener(new WindowAdapter(){
public void windowClosing(WindowEvent e)
{
System.exit(0);
}//end windowClosing
}//end class definition
);//end addWindowListener
}//end constructor
If you are unfamiliar with the use of anonymous inner classes, you can learn
about such topics in my other publications.
Listing 12 also signals the end of the constructor for the class named
ImgMod29 .
We have finally gotten to the fun part of the program. This is the part where
we write the code that determines how the surface elevations and the
calibration scale values are displayed. This part of the program consists of six
different inner classes.
The fact that the classes are inner classes makes it possible for methods in the
class to access instance variables and methods of the containing object of type
ImgMod29 . This makes the programming somewhat easier than would be
the case if they were all top-level classes.
Each of the six inner classes is a subclass of the class named Canvas and each
of the classes overrides the paint method. The code in the overridden paint
methods for the classes that display the 3D surfaces
Associated directly with the three above classes are three other inner classes
that are used to display the calibration scale immediately below the plot of the
3D surface. The names and behaviors of those three classes are:
Before getting into the details of these inner classes, however, I will present
and briefly discuss a method named getCenter , which is called by the
constructor for each of the surface plotting classes.
The getCenter method is used to find the horizontal and vertical center of the
surface. These values are used to position the optional red axes that may be
drawn on the surface. This method is shown in its entirety in Listing 13 .
int horizCenter;
int vertCenter;
void getCenter(){
if(dataWidth%2 == 0){//even
horizCenter =
dataWidth * blockSize/2 +
blockSize/2;
}else{//odd
horizCenter = dataWidth * blockSize/2;
}//end else
if(dataHeight%2 == 0){//even
vertCenter =
dataHeight * blockSize/2 +
blockSize/2;
}else{//odd
vertCenter = dataHeight * blockSize/2;
}//end else
}//end getCenter
Note that the returned values depend on whether the dimensions of the surface
are even or odd.
(For example, the center of a string of five blocks of pixels is the
third block whereas the center of a string of six blocks of pixels is
half way between the third and fourth blocks.)
Now that you know about the difference between even and odd surface
dimensions, the code in Listing 13 should be straightforward and should not
require further discussion.
The constructor also calls the getCenter method to get the coordinates of the
center of the surface in order to be able to draw the optional red axes in the
correct position later.
The real work is done by the overridden paint method, which begins in
Listing 15 . Of the three classes used to plot the 3D surface, this is the
simplest.
The purpose of this overridden paint method is to convert the elevation value
of each point on the 3D surface into an appropriate shade of gray, and to paint
a square of pixels on the screen at that elevation value's location where every
pixel in the square is the same shade of gray.
In order to produce a gray pixel in a 24-bit color system, you need to set the
color values for red, green, and blue to the same value. If all three color values
are zero, the pixel color is black. If all three color values are 255, the pixel
color is white. If all three color values fall somewhere in between zero and
255, the pixel color will be some shade of gray.
Recall that the elevation values were earlier normalized to fall in the range
from zero to 255. The code in Listing 15 uses a nested for loop to access
every elevation value in the array that describes the 3D surface. Then it sets
the red, green, and blue color values to the normalized surface value for each
point on the 3D surface.
Continuing inside the nested for loops, the code in Listing 16 instantiates a
new object of type Color based on the current red, green, and blue color
values.
The code in Listing 17 calls the setColor method of the Graphics class to set
the current drawing color to the color described by the Color object referred
to by the reference variable named color .
Finally the code in Listing 17 calls the fillRect method of the Graphics class
to paint a square of pixels of the specified size at the specified location in the
specified color.
This process is repeated for every elevation point on the 3D surface data,
producing an output similar to the leftmost image in Figure 1 .
Listing 17 signals the end of the nested for loops. When the code in Listing
17 finishes execution, the 3D surface has been plotted, but it does not yet
contain the optional red axes.
Listing 18 tests to see if the value of the axis parameter is true. If so, it uses
the information obtained earlier from the getCenter method, along with the
setColor and drawLine methods of the Graphics class to draw the optional
red axes shown in the images in Figure 1 . These axes always intersect at the
center of the image.
if(axis){
g.setColor(Color.RED);
g.drawLine(0,vertCenter,2*horizCenter,
vertCenter);
g.drawLine(horizCenter,0,horizCenter,
2*vertCenter);
}//end if
}//end paint
}//end inner class CanvasType0surface
Listing 18 also signals the end of the overridden paint method and the end of
the inner class named CanvasType0Surface .
CanvasType0scale(){//constructor
//Set the size of the Canvas based on the
// width of the surface and the size of
the
// square used to represent each value on
// the surface.
setSize(dataWidth *
blockSize,scaleHeight);
}//end constructor
How it works
Basically this class (as well as the other two classes that create calibration
scales) operates by constructing an artificial surface, (which is like a long thin
board) , positioned such that one end has an elevation of 0 and the other end
has an elevation of 255. The length of this long thin surface is equal to the
width of the surface plot for the Grayscale plot format.
The same Grayscale color algorithm is applied to this artificial surface that is
applied to the real surface. The result is a linear representation of the colors
produced by the color algorithm from the lowest elevation at 0 to the highest
elevation at 255. This result is displayed immediately below the real surface
with the lowest elevation at the left end and the highest elevation at the right
end. An example is shown in the leftmost image in Figure 1 .
The code in Listing 19 establishes the size of the calibration scale surface.
Listing 20 shows the overridden paint method that is used to plot the
calibration scale for the Grayscale plot format.
Because you already understand the color algorithm for the Grayscale plot
format, the code in Listing 20 should not require further explanation. This
code establishes the elevation level for each point on the calibration surface
and paints the box that represents that elevation in the appropriate color.
The beginning of the class and the constructor for the class named
CanvasType1surface are shown in Listing 21 .
CanvasType1surface(){//constructor
//Set the size of the Canvas based on the
// size of the surface and the size of the
// square used to represent each value on
// the surface.
setSize(dataWidth * blockSize,
dataHeight * blockSize);
getCenter();
}//end constructor
The beginning of each of the three classes that produce the three plotting
formats is essentially the same. Therefore, the code in Listing 21 is essentially
the same as the code in Listing 14 and should not require further explanation.
The significant differences between the three classes lie in their overridden
paint methods.
The overridden paint method for this class, which begins in Listing 22 , is
probably the most complex of the three.
The paint method for this class begins by setting up a pair of nested for loops
that will be used to process each elevation point on the surface, and by
initializing the color values for red, green, and blue to 0 in the innermost loop.
If the elevation value is equal to 255, color values are set to cause that
elevation to be painted white. If the elevation value is equal to 0, color values
are set to cause that elevation to be painted black.
Listing 23 sets the color values to cause the extreme values of 0 and 255 to be
painted black and white. Note that the code in Listing 23 is the beginning of a
series of if-else constructs.
Listing 23. Set white and black for max and min values.
if((int)data[row][col] == 255){
red = green = blue = 255;//white
}else if((int)data[row][col] == 0 ){
red = green = blue = 0;//black
If the elevation is not one of the extreme values of 0 or 255, control passes to
code that subdivides the total elevation range from 1 to 254 into the following
four ranges and then sets the color values for each range separately:
Listing 24 shows the code that is used to process the lowest range of
elevations between 1 and 63.
Listing 24. Process elevations from 1 to 63 inclusive.
What we are shooting for here is to produce color values that will result in a
smooth gradient of color from blue at the low end to aqua at the high end of
the range.
(See the leftmost one-fourth of the calibration scale for the middle
image in Figure 1 .)
The color aqua is produced by mixing equal amounts of blue and green.
Listing 24 holds the value of blue constant at 255 and increases the value of
green in proportion to the elevation value. Thus, at the lower end of the range,
blue has a value of 255 and green has a value of 4. (This is almost pure blue.)
At the upper end of the range, blue still has a value of 255 and green has a
value of 252. (This is almost the pure secondary color aqua.)
In all cases, the value of red is 0 within this range. These color values (blue
and temp) will be used later to instantiate a Color object, which will be used
to control the plotting color for that portion of the display.
Now that you know the basic scheme, you shouldn't have any difficulty
understanding the code for processing the other three ranges shown in Listing
25 .
}//end else
Gradient from aqua to green
The second range produces a smooth gradient from aqua to green. In this
range, the green color value is held constant at 255 and the blue color value is
caused to decrease in inverse proportion to the normalized color value.
The third range produces a smooth gradient from green to yellow. (Yellow is
produced by mixing equal amounts of red and green.) Within this range, green
is held constant at a value of 255 and the value of red increases in direct
proportion to the normalized elevation value.
The fourth range produces a smooth gradient from yellow to red. Within this
range, the value of red is held constant at 255 and the value of green decreases
in inverse proportion to the normalized elevation value.
A homework assignment
Subdivide the total range into eight sub ranges instead of four as I
did. Choose four additional colors that you can produce by mixing
various levels of red, green, and blue. Modify the code to cause the
colors to vary with a smooth gradient through those eight colors in
succession.
The overridden paint method for this class replicates the color algorithm in
the overridden paint method for the CanvasType1surface class.
Except for the difference in the overridden paint method, the structure of this
class is the same as the class named CanvasType0scale , which I discussed
earlier beginning with Listing 19 . Therefore, I won't repeat that discussion
here. You can view the class in Listing 29 near the end of the module.
Before getting into the details of the class, I will explain the color palette. The
color palette is produced by a method named getColorPalette , shown in its
entirety in Listing 26 . This is a utility method that is called by the inner
classes named CanvasType2surface and CanvasType2scale .
Color[] getColorPalette(){
//Note that the following is an initialized
// 1D array of type Color.
Color[] colorPalette = {
Color.BLACK,// 0, 0, 0
Color.GRAY,// 128,128,128
Color.LIGHT_GRAY,// 192,192,192
Color.BLUE,// 0, 0,255
new Color(100,100,255),//100,100,255
new Color(140,140,255),//140,140,255
new Color(175,175,255),//175,175,255
Color.CYAN,// 0,255,255
new Color(140,255,255),//140,255,255
Color.GREEN,// 0,255, 0
new Color(140,255,140),//140,255,140
new Color(200,255,200),//200,255,200
Color.PINK,// 255,175,175
new Color(255,140,255),//255,140,255
Color.MAGENTA,// 255, 0,255
new Color(255,0,140), //255, 0,140
Color.RED,// 255, 0, 0
new Color(255,100,0),// 255,100, 0
Color.ORANGE,// 255,200, 0
new Color(255,225,0),// 255,225, 0
Color.YELLOW,// 255,255, 0
new Color(255,255,150),//255,255,150
Color.WHITE};// 255,255,255
return colorPalette;
}//end getColorPalette
As you can see, some of the elements in the array refer to Color objects
defined as named constants (public final variables) in the Color class. Other
elements in the array refer to Color objects that are instantiated using red,
green, and blue color values of my own choosing. The actual colors
represented by these objects, going from top to bottom, match the colors
shown in the calibration scale for the rightmost image in Figure 1 .
If you would like to do so, you can rearrange the colors in the array. This will
result in different colors being adjacent to one another in the calibration scale.
Also if you would like to do so, you can remove colors from the array or add
new colors of your own choosing to the array. The overridden paint methods
in the classes named CanvasType2surface and CanvasType2scale are
designed to take such changes into account.
You can view the entire class named CanvasType2surface in Listing 29 near
the end of the module. Because of the similarity of this class to others that I
have previously discussed, I will limit my discussion to the portions of the
overridden paint method that distinguish this class from the others.
As it turns out, this is perhaps the simplest of the three overridden paint
methods. The method begins in Listing 27 where the getColorPalette method
is called to get a reference to the color palette discussed above.
Then a pair of nested for loops is set up to process every elevation value on
the 3D surface.
Listing 27. Beginning of overridden paint method.
The code in Listing 27 quantizes the elevation levels into a set of integer
values ranging from 0 to one less than the number of elements in the color
palette. As written, this redefines the normalized elevation values as extending
from 0 to 22, instead of from 0 to 255.
(If you change the length of the color palette, the number of ranges
will change accordingly.)
The code in Listing 28 uses the quantized elevation value to index into the
color palette and retrieve a reference to a Color object. This reference is
passed to the setColor method setting the current plotting color to the color
represented by that index value.
Listing 28. Set the color value.
g.setColor(colorPalette[quantizedData]);
Having set the current plotting color, as in the other two cases discussed
earlier, the method goes on to paint a square of pixels in that color at the
correct location. Then it draws the optional axes if specified. The code to
accomplish these operations is the same as code discussed previously, so I
won't repeat that discussion here. You can view the code in Listing 29 near the
end of the module.
This inner class is used to construct a color scale that matches the color
algorithm used in the class named CanvasType2surface . Except for the
difference in the overridden paint method, this class is essentially the same as
the other two classes used to construct color scale objects. Therefore, I won't
repeat that discussion.
You can view the class in its entirety in Listing 29 near the end of the module.
You can view the graphic output produced by this class in the calibration scale
for the image at the rightmost end of Figure 1 .
See if you can come up with a better color scheme than the color schemes that
I used in my version of the program. For example, you might add new colors
to the color palette used for the Color Contour plot. That will be very easy to
do. All you need to do is add them to the array.
(The hard part will be to identify new colors that are visually
separable from the colors that are already being used.)
You might also add new colors to the color algorithm for the Color Shift plot.
This will be somewhat more difficult in that additional coding will be required
to incorporate those new colors.
You might also want to modify the code in the main method to cause it to
create different test surfaces. You could even write new independent programs
that create surfaces and use this class named ImgMod29 to plot those
surfaces. Remember, all that's necessary to use this class to plot your own 3D
surface is to include a statement similar to the following in your code:
new ImgMod29(data,blockSize,true,0);
It was necessary for me to keep the images in this module small in order to
force them to fit into this publication format. As you are experimenting, make
your test surfaces larger and your blockSize smaller. This will result in
smoother edges where different colors meet.
The second parameter named blockSize specifies the size of one side of the
square of pixels in the final plot that you want to use to represent each
elevation point on your 3D surface. Set this to 0 if you are unsure as to what
size square you need.
The third parameter specifies whether or not you want to have the optional red
axes drawn. A value of true causes the axes to be drawn. A value of false
causes the axes to be omitted.
0 - Grayscale (linear)
1 - Color Shift (linear)
2 - Color Contour (linear)
3 - Grayscale with logarithmic data conversion
4 - Color Shift with logarithmic data conversion
5 - Color Contour with logarithmic data conversion
Above all, have fun and learn as much as you can in the process.
Summary
In this module, I explained and illustrated a program for using Java and color
to plot 3D surfaces. The program is extremely easy to use and makes it easy to
plot your surface using six different plotting formats in a wide range of sizes.
/*File ImgMod29.java
Copyright 2005, R.G.Baldwin
Listing 29. Source code for ImgMod29.java.
double[][] dataIn
int blockSize
boolean axis
int display
}//end constructor
//-------------------------------------------//
double min;
double max;
//This method is used to scale the surface data
// to force it to fit in the range from 0 to
Listing 29. Source code for ImgMod29.java.
// 255.
void scaleTheSurfaceData(){
//Find the minimum surface value.
min = Double.MAX_VALUE;
for(int row = 0;row < dataHeight;row++){
for(int col = 0;col < dataWidth;col++){
if(data[row][col] < min)
min = data[row][col];
}//end col loop
}//end row loop
int horizCenter;
int vertCenter;
//This helper method is used to find the
// horizontal and vertical center of the
// surface. These values are used to locate
// the red axes that are drawn on the surface.
// Note that the returned values depend on
// whether the dimensions of the surface are
// odd or even.
void getCenter(){
if(dataWidth%2 == 0){//even
horizCenter =
dataWidth * blockSize/2 + blockSize/2;
}else{//odd
horizCenter = dataWidth * blockSize/2;
}//end else
if(dataHeight%2 == 0){//even
vertCenter =
dataHeight * blockSize/2 + blockSize/2;
}else{//odd
vertCenter = dataHeight * blockSize/2;
}//end else
}//end getCenter
//-------------------------------------------//
CanvasType0scale(){//constructor
//Set the size of the Canvas based on the
// width of the surface and the size of the
// square used to represent each value on
Listing 29. Source code for ImgMod29.java.
// the surface.
setSize(dataWidth * blockSize,scaleHeight);
}//end constructor
CanvasType1surface(){//constructor
//Set the size of the Canvas based on the
// size of the surface and the size of the
// square used to represent each value on
// the surface.
setSize(dataWidth * blockSize,
dataHeight * blockSize);
getCenter();
}//end constructor
}else{//impossible condition
System.out.println(
"Should not reach here.");
Listing 29. Source code for ImgMod29.java.
System.exit(0);
}//end else
CanvasType1scale(){//constructor
//Set the size of the Canvas based on the
// width of the surface and the size of the
// square used to represent each value on
// the surface.
setSize(dataWidth * blockSize,scaleHeight);
}//end constructor
}else{//impossible condition
System.out.println(
"Should not reach here.");
System.exit(0);
}//end else
return colorPalette;
}//end getColorPalette
//===========================================//
CanvasType2surface(){//constructor
//Set the size of the Canvas based on the
// size of the surface and the size of the
// square used to represent each value on
// the surface.
setSize(dataWidth * blockSize,
dataHeight * blockSize);
getCenter();
}//end constructor
CanvasType2scale(){//constructor
//Set the size of the Canvas based on the
// width of the surface and the size of the
// square used to represent each value on
// the surface.
setSize(dataWidth * blockSize,scaleHeight);
}//end constructor
Miscellaneous
This section contains a variety of miscellaneous information.
Learn how to write a Java class that uses color to plot 3D surfaces in six
different formats and a wide range of sizes. The class is extremely easy to
use. You can incorporate the 3D plotting capability into your own programs
by inserting a single statement into your programs.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it possible
for you to purchase a pre-printed version of the PDF file, you should be
aware that some of the HTML elements in this module may not translate well
into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales nor
do I know who does receive compensation. If you purchase such a book,
please be aware that it is a copy of a module that is freely available on
cnx.org and that it was made and published without my prior knowledge.
Affiliation : I am a professor of Computer Information Technology at Austin
Community College in Austin, TX.
-end-
Java1492-Plotting Large Quantities of Data using Java
Learn how to use Java to plot millions of multi-channel data values in an
easy-to-view format with very little programming effort.
Table of contents
Preface
Viewing tip
Figures
Listings
Preview
Plotting format
On to the next page
Superimposed data
Sample programs
Preface
In one of my earlier modules titled Plotting Engineering and Scientific Data
using Java , I published a generalized 2D plotting class that makes it easy to
cause other programs to display their outputs in 2D Cartesian coordinates . I
have used that plotting class in numerous modules since I published it.
Hopefully, some of you have been using it as well.
All that's necessary to use these classes to plot large quantities of data is to:
It couldn't be easier
The choice among the four classes listed above depends on whether you need
to plot one, two, or three channels of data, and the format in which you want
to plot the data. The class named PlotALot01 is used to plot single-channel
data. The classes named PlotALot02 and PlotALot03 are used to plot two-
channel data in two different formats. The class named PlotALot04 is used to
plot three-channel data.
If you arrived at this page seeking a free Java class for plotting your data, you
are in luck. Just copy the source code for the classes in Listing 35 through
Listing 38 near the end of this module and feel free to use them as described
in the comments in the source code.
On the other hand, if you would like to learn how the classes do what they do,
and perhaps use your programming skills to improve them, keep reading.
Hopefully, once you have finished the module, you will have learned quite a
lot about plotting large quantities of data using Java.
Viewing tip
I recommend that you open another copy of this module in a separate browser
window and use the following links to easily find and view the Figures and
Listings while you are reading about them.
Figures
Listings
Preview
The four classes that I will present and explain in this module are designed to
make it easy for you to plot and examine large quantities of data.
The first class named PlotALot01 is designed for the plotting of large
quantities of single-channel data. Figure 1 shows an example of the plotted
output from this class when used to plot a small amount of data.
To use the class named PlotALot01 to plot data, you first instantiate an object
of the class, and then you feed data to it as the data becomes available within
your program.
When all of the data has been fed to the plotting object, you call a method
named plotData on the object. This causes the object to produce one or more
pages of plotted data in a stack on the screen. The page containing the earliest
data is on the top of the stack and the page containing the latest data on the
bottom of the stack.
Plotting format
The first data sample is plotted on the left end of the top trace on the top page
(titled Page: 0) . Successive data values are plotted from left to right across
the page. When the data for the first trace reaches the right end of the trace,
the next data sample is plotted at the left end of a new trace that is created
below the current trace. Hence, the chronological order of the data is from left
to right, top to bottom.
A horizontal axis is drawn for each trace. Positive data values are plotted
above the axis and negative values are plotted below the axis.
When the bottom trace on a page is filled, a new page is created automatically.
The next data sample is plotted on the left end of the top trace on the new
page and the process described above is repeated until that page also become
full. Then a new page is created, etc.
Other than the amount of memory that is available to the Java virtual machine,
(and perhaps some limit on the number of Page objects allowed by the
operating system) , there is almost no limit to the number of pages that can be
produced and the amount of data that can be plotted.
I have successfully plotted two million data values in 141 full screen pages on
a modest laptop computer with no difficulty whatsoever. When I pushed that
total up to eight million data values in 563 full screen pages, the plotting
process slowed down, but I was still able to display and examine the plots.
The practical limit on my computer seems to be somewhere between two
million and eight million data values.
Figure 1 shows two pages that were physically removed from the stack and
arranged with the page containing the earliest data above the page containing
the latest data for publication in this module.
Two overloaded constructors are provided for the class. One constructor plots
the data using a set of default plotting parameters. This constructor is
provided for extreme ease of use. The only information that you must provide
to this constructor is a string that becomes part of the title for each page.
I coded the values of the default plotting parameters to make the results
suitable for use in this narrow publication format. If you don't like my choice
of default plotting parameters, you can change them to values that you find
more useful. For example, you could cause the default size of the Frame
object to fill your screen, allowing you to plot quite a lot of data on each page.
The other overloaded constructor takes seven parameters that allow you to
control all aspects of the plotting format including:
Page title
Frame width and hence plotted data width
Frame height, spacing between traces, and hence the number of traces
per page
Spacing between samples, number of traces per page, and hence the
number of samples per page
Width and height of an oval that is used to mark each sample on the plot
The plotted sample values are connected by a straight line. Each sample is
marked with an oval. You can specify the width and the height of the oval in
pixels. If you set the width and height to zero, the oval simply disappears
from the plot.
The plots in Figure 1 were produced using the constructor that applies default
plotting parameters. For example, the data was plotted using the default value
of two pixels per sample. Hence the lines connecting the sample values in
Figure 1 are very short.
The ovals in Figure 1 had a default width and height of two pixels each. At
this small size, the ovals end up looking more like plus characters than ovals.
Title: B
Frame width: 400
Frame height: 410
Page width: 392
Page height: 383
Trace spacing: 50
Sample spacing: 2
Traces per page: 7
Samples per page: 1372
Explanation of terms
The Page width and Page height are the width and height of a Canvas object
contained in the Frame object, upon which the plotting is performed. The
width of the Canvas actually controls the number of samples that can be
plotted in each trace.
The Trace spacing is the number of pixels that separate each of the horizontal
axes on a page in Figure 1 .
The Sample spacing specifies the number of pixels that are dedicated to each
sample horizontally. In Figure 1 , that value is 2. This means that every other
black pixel in Figure 1 indicates the value of a data sample. The pixels in
between are fillers.
The Traces per page specifies the number of horizontal axes on each page
against which the data values are plotted.
The Samples per page gives the actual number of data values that are plotted
on each page. This is determined from the values of Traces per page ,
Sample spacing , and Page width .
There are also two overloaded versions of the method named plotData . One
version lets you specify the location of the upper left corner of the stack of
pages relative to the upper left corner of the screen. The other version simply
places the stack of pages in the upper left corner of the screen by default.
The class named PlotALot01 is designed for the plotting of large quantities of
data from a single channel as described above. The classes named
PlotALot02 and PlotALot03 are each designed to plot two channels of data.
These two classes plot the two-channel data in different formats.
Superimposed data
The class named PlotALot02 provides all of the features described above for
the class named PlotALot01 , such as overloaded constructors, overloaded
plotData methods, etc. In addition, it provides the capability to superimpose
two sets of data on the same axes with one set being plotted in black and the
other being plotted in red. This is illustrated in Figure 2 .
Figure 2. Sample output for PlotALot02 class.
Same data, different sign
The plots in Figure 2 were produced by plotting two versions of the same
data. The algebraic sign of each of the data values was inverted in one set of
data relative to the other. Thus, the red plot in Figure 2 is an upside down
version of the black plot. This makes it easy to confirm that both plotting
processes are behaving the same way.
The plots in Figure 2 were produced using the version of the constructor that
allows the user to control the plotting parameters. The overall plotting
parameters for Figure 2 are shown below:
Title: A
Frame width: 158
Frame height: 237
Page width: 150
Page height: 210
Trace spacing: 36
Sample spacing: 5
Traces per page: 5
Samples per page: 150
Larger ovals
As you can see, the ovals that were used to mark the sample values in Figure
2 were larger than in Figure 1 . With a height and a width of four pixels, each
oval turned out to be a circle centered on the sample value.
Also, the horizontal scaling in Figure 2 was five pixels per sample as opposed
to two pixels per sample in Figure 1 . As a result, the circles marking the
samples were further apart, and the straight lines connecting the circles are
often visible.
Sample output for PlotALot03 class
The classes named PlotALot02 and PlotALot03 are each designed to plot
two channels of data. These two classes plot the two-channel data in different
formats. Whereas PlotALot02 superimposes the two sets of data on the same
horizontal axes using color to provide visual separation, PlotALot03 plots the
two sets of data on alternating horizontal axes as shown in Figure 3 .
PlotALot03 also uses color to provide visual separation between the two sets
of data. One set is plotted on the odd numbered axes in black. The other set is
plotted on the even numbered axes in red.
The class named PlotALot03 also provides all of the general capabilities
described earlier for the class named PlotALot01 that are appropriate for a
two-channel plotting system.
The two sets of data plotted in Figure 3 consisted of exactly the same values.
Thus, the plots on the even numbered axes look just like the plots on the odd
numbered axes except that one plot is red and the other is black. Using the
same values for each set of data makes it easy to confirm that both plotting
processes are behaving the same way.
Title: A
Frame width: 158
Frame height: 270
Page width: 150
Page height: 243
Trace spacing: 36
Sample spacing: 5
Traces per page: 6
Samples per page: 90
Because PlotALot03 doesn't superimpose the two sets of data, twice as many
pages would be required for PlotALot03 to plot a given amount of data as
would be required by PlotALot02 for the same Page size.
PlotALot03 will refuse to plot data for a set of plotting parameters that result
in an odd number of traces on the page.
The class named PlotALot04 plots three sets of data on separate horizontal
axes as shown in Figure 4 . The first set of data is plotted in black. The second
set of data is plotted in red. The third set of data is plotted in blue. This class
is particularly useful for displaying the input, output, and error signals
involved in adaptive signal processing, for example.
The class named PlotALot04 also provides all of the general capabilities
described earlier for the class named PlotALot01 that are appropriate for a
three-channel plotting system.
The three sets of data plotted in Figure 4 consisted of exactly the same values.
Thus, the plots on the three different axes look just alike except that the first
plot is black, the second plot is red and the third is blue. Using the same
values for each set of data makes it easy to confirm that all three plotting
processes are behaving the same way.
Title: A
Frame width: 158
Frame height: 270
Page width: 150
Page height: 243
Trace spacing: 36
Sample spacing: 5
Traces per page: 6
Samples per page: 60
PlotALot04 will terminate if the number of traces per page is not evenly
divisible by 3
Sample programs
Now that you know where we are heading, it's time to examine these four
classes in detail. I will begin with the class named PlotALot01 .
Usage information
The class provides a main method so that the class can be run as an
application to test itself. The main method also illustrates how to use the
class.
There are three steps involved in the use of this class for plotting large
quantities of data:
A program that uses this class for plotting can instantiate as many different
plotting objects as are needed to plot all of the different sets of data that need
to be plotted independently of one another.
(For example, a program that uses this class could instantiate one
plotting object to plot time series data and a different plotting
object to plot spectral data.)
Can plot a large number of data values
Each plotting object can be used to plot as many data values as needed (unless
the program runs out of memory) .
A plotting object of type PlotALot01 owns one or more Page objects that
extend the Frame class. The plotting object can own as many Page objects as
are necessary to plot all of the data that is fed to the plotting object.
(The Page objects on the top of the stack must be physically moved
in order to see the Page objects further down in the stack.)
When the right end of an axis is reached, the next data value is plotted on the
left end of the axis immediately below it (sometimes referred to as wrap
around) . When the right end of the last axis on the Page is reached, a new
Page object is automatically created and the next data value is plotted at the
left end of the top axis on the new Page object.
There are two overloaded versions of the constructor for the PlotALot01
class. One overloaded version accepts several incoming parameters allowing
the user to control various aspects of the plotting format. (An example of the
use of this constructor is shown in Figure 5 .) A second overloaded version
accepts a title string only and sets all of the plotting parameters to default
values. (An example of the use of this constructor is shown in Figure 1 .)
(You can easily modify the default values and recompile the class if
you prefer different default values.)
Constructor parameters
The parameters for the version of the constructor that accepts plotting
parameters are:
String title: Title for the Frame object. This title is concatenated with the
page number and the result appears in the banner at the top of the Page as
shown in Figure 1 .
int frameWidth: The Frame width in pixels.
int frameHeight: The Frame height in pixels.
int traceSpacing: Distance between trace axes in pixels.
int sampSpace: Number of pixels dedicated to each data sample in pixels
per sample. (Must be 1 or greater.)
int ovalWidth: Width of an oval that is used to mark the sample value on
the plot. (See Figure 5 for a good example of the ovals. Set the oval width
and height parameters to zero to eliminate the ovals altogether.)
int ovalHeight: Height of an oval that is used to mark the sample value
on the plot.
For self-test purposes, the main method instantiates and feeds two
independent plotting objects. Plotting parameters are specified for the first
plotting object and the stack of pages for this plotting object is located 401
pixels to the right of the upper left corner of the screen. The output produced
by this plotting object is shown in Figure 5 below. (The two pages in the
screen shot in Figure 5 were manually relocated and positioned for reasons
that I will explain later.)
Default plotting parameters are used for the second plotting object and the
stack of pages is located in the default location at the upper left corner of the
screen. The output produced by this plotting object was shown earlier in
Figure 1 .
The data to be plotted
Most of the data that is fed to each plotting object is white random noise
produced by a random noise generator. However, fifteen of the data values fed
to the first plotting object are not random.
Eight of the data values for the first plotting object are set to
0,0,20,20,-20,-20,0,0. The result can be seen at the end of the first trace and
the beginning of the second trace in Page 0 in Figure 5 . Note that the last four
plotted points for the first trace have values of 0,0,20, and 20. Then note that
the first four plotted points on the second trace have values of -20, -20, 0, and
0. This confirms the proper transition from one trace to the next on the same
page with no loss of data values in the transition.
Seven of the values for the first plotting object are set to values of
0,0,25,-25,25,0,0. The result can be seen at the end of the last trace on Page 0
and the beginning of the first trace on Page 1 in Figure 5 . Note that the last
three plotted points in the last trace on Page 0 have values of 0, 0, and 25.
Then note that the first four plotted points in the first trace on Page 1 have
values of -25, 25, 0, and 0. This confirms the proper transition from one page
to the next with no loss of data in the transition.
Information about the plotting parameters for each plotting object is displayed
on the command line screen when this class is used for plotting. The values
shown below result from the execution of the main method of the PlotALot01
class for self-test purposes. One of the plotting objects instantiated by the
main method is titled "A" and the other is titled "B" .
null
The graphic output produced for the object titled "A" is shown in Figure 5 .
This output was based on plotting format parameters that were passed to the
constructor. The graphic output produced for the object titled "B" is shown in
Figure 1 . This output was based on default plotting parameters.
There are two overloaded versions of the plotData method. One version
allows the user to specify the location on the screen where the stack of plotted
pages will appear. This version requires two parameters, which are coordinate
values in pixels. The first parameter specifies the horizontal coordinate of the
upper left corner of the stack of pages relative to the upper left corner of the
screen. The second parameter specifies the vertical coordinate of the upper
left corner of the stack of pages relative to the upper left corner of the screen.
The other overloaded version of plotData places the stack of pages in the
upper left corner of the screen by default.
Each page has a WindowListener that will terminate the program if the user
clicks the close button on the Frame (the X-button in the upper-right corner) .
The class was tested using J2SE 5.0 and WinXP. J2SE 5.0 is required because
the class uses generics with an ArrayList object. (More recently, it was re-
tested using java version "1.8.0_60" under Windows 7.)
I will present and explain this class in fragments. A complete listing of the
class is provided in Listing 35 near the end of the module.
As mentioned earlier, this class contains a main method. The main method is
provided so that the class can be run as an application for self-test purposes,
which is common practice in Java programming. The main method also
illustrates the proper use of the class.
The beginning of the class and the beginning of the main method are shown
in Listing 1 .
Listing 2 contains a for loop that feeds 275 values to the plotting object titled
"A". Most of the code in Listing 2 is required to set fifteen specific values to
test for proper transitions as described earlier. This code is straightforward and
shouldn't require further explanation.
The final statement in Listing 2 uses a random number generator to feed white
random noise to the plotting object for all data values other than the fifteen
data values specified in the preceding statements. You can see the random
values plotted and marked by round ovals in Figure 5 .
The statement in Listing 3 calls the overloaded plotData method to cause all
of the pages belonging to the plotting object titled "A" to be stacked in a
location where the upper left corner of the stack is 401 pixels to the right of
the upper left corner of the screen.
plotObjectA.plotData(401,0);
As described earlier, page 0 containing the earliest data fed to the plotting
object is on the top of the stack. Figure 1 shows the two pages belonging to
this plotting object after they have been manually rearranged to make them
both visible.
Listing 4 feeds 2600 random white noise values to the object titled "B" and
displays the pages in the default location in the upper left corner of the screen.
Listing 4 also signals the end of the main method.
}//end main
Listing 4 (plus one of the statements in Listing 1 ) is much more typical of the
amount of code required to use this plotting class than was the case with
Listing 2 .
(Almost all of the code in Listing 2 was required to set the special
data values used to test the transitions discussed earlier.)
To recap, the three steps required to use this class for plotting nearly unlimited
amounts of data are:
String title;
int frameWidth;
int frameHeight;
int traceSpacing;//pixels between traces
int sampSpacing;//pixels between samples
int ovalWidth;//width of sample marking oval
int ovalHeight;//height of sample marking oval
int tracesPerPage;
int samplesPerPage;
int pageCounter = 0;
int sampleCounter = 0;
ArrayList <Page> pageLinks =
new ArrayList<Page>
();
The purpose of each of these instance variables is indicated by the name of the
variable, and in some cases by the comments following the variable
declaration. In addition, I will have more to say about some of these variables
later when I discuss the code that uses them.
As mentioned earlier, there are two overloaded versions of the constructor for
this class. The overloaded version that begins in Listing 6 accepts several
incoming parameters allowing the user to control various aspects of the
plotting format.
Listing 6 shows the signature for this overloaded version of the constructor.
The comments should make the code self explanatory.
Save the parameter values
With one exception, the code in Listing 7 simply saves the incoming
parameter values in instance variables to make those values available to other
members of the class.
this.title = title;
this.frameWidth = frameWidth;
this.frameHeight = frameHeight;
this.traceSpacing = traceSpacing;
//Convert to pixels between samples.
this.sampSpacing = sampSpace - 1;
this.ovalWidth = ovalWidth;
this.ovalHeight = ovalHeight;
The exception
The code in Listing 8 instantiates a temporary Page object solely for the
purpose of obtaining information about the width and the height of the
Canvas object. This information is used later to compute a variety of other
important parameter values.
tempPage.canvas.getHeight();
Once the width and height of the Canvas has been determined, the temporary
Page object is no longer needed. Listing 10 disposes of that object freeing all
of the resources dedicated to the object.
tempPage.dispose();
Having determined the width and height of the Canvas , Listing 11 computes
and displays the remaining plotting parameters. The expressions used to
compute these values are straightforward and shouldn't require further
explanation.
Listing 11. Compute and display the remaining plotting parameters.
tracesPerPage =
(canvasHeight -
traceSpacing/2)/
traceSpacing;
System.out.println("Traces per page: "
+
tracesPerPage);
if(tracesPerPage == 0){
System.out.println("Terminating program");
System.exit(0);
}//end if
samplesPerPage = canvasWidth *
tracesPerPage/
(sampSpacing +
1);
System.out.println("Samples per page: "
+
samplesPerPage);
Listing 12 instantiates the first usable Page object. (Recall that a temporary
Page object was instantiated and disposed of earlier.) This Page object will be
titled title Page: 0 as indicated in Figure 1 and Figure 5 .
pageLinks.add(new Page(title));
}//end constructor
Note that a reference to this Page object (and all subsequently instantiated
Page objects) is saved in an object of type ArrayList referred to by
pageLinks .
Listing 12 also signals the end of this constructor for the PlotALot01 class.
A second overloaded constructor is provided for those who don't want to have
to think about plotting parameters. This constructor, which is shown in its
entirety in Listing 13 , establishes a set of default plotting parameters.
(In case you are unfamiliar with the use of the keyword this to
cause one constructor to call another constructor of the same class,
you can learn about that topic in my module titled The Essence of
OOP using Java, The this and super Keywords .)
Listing 13. The other overloaded constructor .
PlotALot01(String title){
this(title,400,410,50,2,2,2);
}//end overloaded constructor
This is where the default plotting parameters are specified as having the
following values:
frameWidth: 400
frameHeight: 410
traceSpacing: 50
sampSpace: 2
ovalWidth: 2
ovalHeight: 2
The feedData method must be called on the plotting object once for each data
value that is to be plotted. This method is shown in its entirety in Listing 14 .
Listing 14. The feedData method.
val,sampleCounter);
sampleCounter++;
}//end feedData
The feedData method receives an incoming data value of type double . This
is probably a good time to point out that the data must be properly scaled for
plotting before it is passed to this method.
The incoming double value will later be cast to type int . As you should
already know, if the double value is too large to fit in type int , the value
resulting from the cast will be indeterminate.
All of the data values are stored in array objects of type double as they are fed
to the plotting object. Later, when it is time to display the plotted version of
the data, an overridden paint method accesses that data and produces the plot.
The MyCanvas class overrides the paint method to cause it to plot the data
stored in the array whenever the overridden version of the paint method is
called.
(If you are familiar with graphics in Java, you will already know
that the overridden paint method can be called for a variety of
reasons, such as covering and later uncovering the page. If you are
not familiar with graphics in Java, I discuss the overriding of the
paint method in numerous earlier modules including several
modules on animation in Java.)
I will have much more to say about the class named MyCanvas later.
Finally, the feedData method increments the sample counter and returns to
await being called to receive the next data sample.
The plotData method must be called once when all of the data has been fed to
the plotting object by way of the feedData method. The purpose of the
plotData method is to rearrange the Page objects in a stack on the screen with
page 0 (containing the earliest data) on the top of the stack.
Having rearranged the Page objects, the plotData method causes the object
on the top of the stack to become visible. This, in turn, causes its overridden
paint method belonging to that object to be called, thus causing the data to be
plotted as shown in Figure 1 and Figure 5 .
There are two overloaded versions of the plotData method. One version
allows the user to specify the location on the screen where the stack of plotted
pages will appear. The other version places the stack in the upper left corner
of the screen by default.
The version of the plotData method that allows the user to specify the
location begins in Listing 15 . This version receives two incoming parameters.
These parameters specify the coordinates of the upper left corner of the stack
of Page objects relative to the upper left corner of the screen. The first
parameter specifies the horizontal coordinate and the second parameter
specifies the vertical coordinate, with positive vertical values going down the
screen.
As you will see later, each of the pages are displayed on the screen as they are
produced. It is possible that this method could be called before the operating
system has completed the process of making the last page visible. The
plotData method uses a while loop to delay until the last page has become
visible on the screen.
At this point, the pages appear on the screen with the last page on the top of
the stack. This order needs to be reversed to cause the first page to be on the
top of the stack.
Make all pages invisible
The reversal of the order of the pages in the stack is accomplished by first
making every page invisible, and then making them visible again in reverse
order.
The code in Listing 17 iterates on the ArrayList object again, accessing the
references to the Page objects in reverse order and setting the value of the
visible property for each Page object to true. This results in page 0 (the page
with the earliest data) being on the top of the stack.
Listing 17. Make the pages visible in reverse order.
In addition, the code in Listing 17 sets the location property of each Page
object to the coordinate values received as incoming parameters to the
plotData method. This causes the stack of Page objects to appear in the
specified location on the screen.
The other overloaded version of the plotData method is shown in its entirety
in Listing 18 .
void plotData(){
plotData(0,0);//call overloaded version
}//end plotData()
The Page class is a member class of the class named PlotALot01 . As such,
methods belonging to objects of the Page class have direct access to all of the
other members of the enclosing PlotALot01 object, including instance
variables belonging to the PlotALot01 object.
(If you are unfamiliar with member classes, see the module titled
The Essence of OOP using Java, Member Classes .)
A PlotALot01 object may own as many Page objects as are required to plot
all of the data values that are fed to it.
Page(String title){//constructor
canvas = new MyCanvas();
add(canvas);
setSize(frameWidth,frameHeight);
setTitle(title + " Page: " + pageCounter);
setVisible(true);
The Page class begins by declaring two instance variables. The instance
variable named canvas will hold a reference to an object of the MyCanvas
class upon which the data will actually be plotted.
The other instance variable will hold the value of a sample counter.
Then the constructor accesses the title and pageCounter variables belonging
to the enclosing PlotALot01 object and uses those values to set the title for
the Page object.
Finally, the code in Listing 19 causes the Page object to become visible on the
screen.
(In case you are unfamiliar with anonymous inner classes, see my
module titled The Essence of OOP using Java, Anonymous Classes
.)
addWindowListener(
new WindowAdapter(){
public void windowClosing(
WindowEvent e)
{
System.exit(0);//terminate program
}//end windowClosing()
}//end WindowAdapter
);//end addWindowListener
}//end constructor
Listing 20 also signals the end of the constructor for the Page class.
This class definition begins by creating a new array object of type double that
will be used to store the data values belonging to the page. The size of the
array is specified by the value of samplesPerPage .
Listing 24 shows the beginning of the code that is used to plot the data values
stored in the array that was created in Listing 22 .
this.getWidth())*traceSpacing;
Listing 24 begins by testing the value of the sample counter to make certain
that there are some points to be plotted. If so, it enters a for loop to plot each
data value stored in the array. Because it uses the value of the sample counter
to terminate the for loop, only those data values that have been stored in the
array object will be plotted, even if the array object isn't full.
A vertical offset
The data values in the array are to be plotted on one or more horizontal axes
on the page. Therefore, it is necessary to first determine where on the page
each data value is to be plotted. The code in Listing 24 uses various pieces of
information to determine the vertical location of the axis against which each
data value will be plotted.
Draw an oval
The code in Listing 25 draws an oval centered on the sample value to mark
the sample on the plot. It is best if the dimensions of the oval are evenly
divisible by 2 for centering purposes. Otherwise, the ovals may appear to be a
little off center.
g.drawOval(cnt*(sampSpacing + 1)%
this.getWidth() -
ovalWidth/2,
yOffset - (int)data[cnt] -
ovalHeight/2,
ovalWidth,
ovalHeight);
The code in Listing 26 connects the sample values with straight lines. Care is
taken to avoid drawing a line from the last sample value on one trace to the
first sample value on the next trace. That would really be ugly.
if(cnt*(sampSpacing + 1)%
this.getWidth()
>=
sampSpacing + 1)
{
g.drawLine(
(cnt - 1)*(sampSpacing + 1)%
this.getWidth(),
yOffset - (int)data[cnt-1],
cnt*(sampSpacing + 1)%
this.getWidth(),
yOffset - (int)data[cnt]);
}//end if
}//end for loop
}//end if for sampleCounter > 0
}//end overridden paint method
}//end inner class MyCanvas
}//end inner class Page
}//end class PlotALot01
Listing 26 also signals the end of the overridden paint method, the end of the
MyCanvas class, the end of the Page class, and the end of the PlotALot01
class. In short, Listing 26 signals the end of the class under discussion.
This class is an update to the class named PlotALot01 . This class is designed
to plot large amounts of data for two channels on the same axes as shown in
Figure 2 . One set of data is plotted using the color black. The other set of data
is plotted using the color red.
As is the case for the class named PlotALot01 , this class provides a main
method so that the class can be run as an application in self-test mode.
As before, there are three steps involved in the use of this class for plotting
data:
For test purposes, the main method instantiates a single plotting object and
feeds two data sets to that plotting object. As before, the data that is fed to the
plotting object is white random noise. One of the data sets is the sequence of
values obtained from a random number generator. The other data set is the
same as the first except that the sign of each data values is reversed.
Also as before, and for the same reason, fifteen of the data values for each
data set are not random. The non-random data values are the same as in the
main method for the class named PlotALot01 . Figure 2 illustrates how these
fifteen specific values are used to confirm the proper transition from the end
of one trace to the beginning of the next trace, and also to confirm the proper
transition from the end of one page to the beginning of the next page.
The beginning of the class and an abbreviated version of the main method is
provided in Listing 27 . Much of the code has been deleted from Listing 27
for brevity.
Listing 27. The class named PlotALot02 and the main method.
Listing 27. The class named PlotALot02 and the main method.
}else{
plotObjectA.feedData(valBlack,valRed);
}//end else
}//end for loop
//Cause the data to be plotted in the
default
// screen location.
plotObjectA.plotData();
}//end main
valBlack,valRed,sampleCounter);
sampleCounter++;
}//end feedData
The most significant things to note about the modified version of the
feedData method are:
The class begins by creating two different array objects in which incoming
data is stored instead of just one array object. These are the objects that are
populated by the putData method in Listing 29 .
The most significant thing in Listing 31 is the call to the setColor method of
the Graphics class to set the drawing color to black. Otherwise, the code is
essentially the same as the code in the overridden paint method in
PlotALot01 . The code in Listing 31 draws the black traces shown in Figure 2
.
New code in the overridden paint method
The drawing color has been set to red instead of the default color of
black.
The data being plotted is the second set of data. This data is stored in the
array object referred to by redData .
Otherwise, this code is essentially the same as the code that was used to plot
the single data set in the overridden paint method in the class named
PlotALot01 .
this.getWidth(),
yOffset - (int)redData[cnt-1],
cnt*(sampSpacing + 1)%
this.getWidth(),
yOffset - (int)redData[cnt]);
}//end if
}//end for loop
}//end if for sampleCounter > 0
}//end overridden paint method
}//end inner class MyCanvas
}//end inner class Page
}//end class PlotALot02
Listing 32 signals the end of the overridden paint method, the MyCanvas
class, the Page class, and the PlotALot02 class.
The class named PlotALot03
Much of the code in the class named PlotALot03 is very similar to the code
in PlotALot02 . Therefore, this discussion will be brief, simply highlighting
the differences between the two classes.
This class is an update to the class named PlotALot02 . This class is designed
to plot large amounts of data for two channels on alternating horizontal axes.
One set of data is plotted using the color black. The other set of data is plotted
using the color red.
As before, there are three steps involved in the use of this class for plotting
two-channel data:
Each Page object contains two or more horizontal axes on which the data is
plotted. The class will terminate if the number of axes on the page is an odd
number.
Alternating axes
The two data sets are plotted on alternating axes as shown in Figure 3 with the
data from one data set being plotted in black on one axis and the data from the
other data set being plotted in red on the axis below that axis.
The earliest data is plotted on the pair of axes nearest the top of the page
moving from left to right across the page. Positive data values are plotted
above the axis and negative values are plotted below the axis.
When the right end of an axis is reached, the next data value is plotted on the
left end of the second axis below it skipping one axis in the process. When the
right end of the last pair of axes on the page is reached, a new Page object is
created and the next pair of data values are plotted at the left end of the top
pair of axes on that new page.
For self-test purposes, the main method instantiates a single plotting object
and feeds two data sets to that plotting object. The data that is fed to the
plotting object is white random noise. One of the data sets is the sequence of
values obtained from a random number generator. The other data set is the
same as the first. Thus, the pairs of black and red data sets that are plotted
should have the same shape making it easy to confirm that the process of
plotting the two data sets is behaving the same in both cases.
The first code that I will highlight as being different from the code in the class
named PlotALot02 is shown in Listing 33 . This code appears in the modified
constructor for the PlotALot03 class.
if((tracesPerPage == 0) ||
(tracesPerPage%2 != 0) )
{
System.out.println("Terminating program");
System.exit(0);
}//end if
samplesPerPage = canvasWidth *
tracesPerPage/
(sampSpacing +
1)/2;
The if statement in Listing 33 confirms that the number of traces per page is
evenly divisible by two. If not, the program terminates.
//Draw an oval
//...code deleted for brevity
//Connect the sample values with
// straight lines.
//...code deleted for brevity
this.getWidth())*2*traceSpacing;
Most of the code in the overridden paint method is the same as the code that I
discussed earlier and was deleted from Listing 34 for brevity.
The code that is different is the code that computes the vertical offset values
to locate the black data on the odd numbered axes and to locate the red data
on the even numbered axes as shown in Figure 3 . I will let you work through
the expressions in Listing 34 on your own and convince yourself that the code
is correct.
Listing 34 signals the end of the overridden paint method, the MyCanvas
class, the Page class, and the PlotALot03 class.
This class is an update to the class named PlotALot03 . This class is designed
to plot large amounts of three-channel data on separate horizontal axes. One
set of data is plotted using the color black. The second set of data is plotted
using the color red. The third set of data is plotted using the color blue.
The class provides a main method so that the class can be run as an
application to test itself.
There are three steps involved in the use of this class for plotting data:
Each Page object contains three or more horizontal axes on which the data is
plotted. The class will terminate if the number of axes on the page is not
evenly divisible by 3.
The three data sets are plotted on separate axes as shown in Figure 4 with the
data from one data set being plotted in black on one axis, the data from the
second data set being plotted in red on the axis below that axis, and the data
from the third data set being plotted in blue on the axis below that axis.
For test purposes, the main method instantiates a single plotting object and
feeds three data sets to that plotting object producing the graphic output
shown in Figure 4 .
The code in this class is so similar to the code in the class named PlotALot03
that I'm not going to discuss the code. You will find a complete listing of the
class in Listing 38 near the end of the module.
Modify the programs and experiment with them in order to learn as much as
you can about the use of Java for plotting large quantities of data. For
example, you might want to modify the default plotting parameters to a
different set of plotting parameters that are more to your liking. One
possibility is to cause the default Page size to fill the entire screen on your
computer.
Another good exercise would be for you to convert this class to Swing using a
look and feel that is independent of the operating system.
Summary
In this module, I presented and explained four self-testing classes for plotting
large quantities of data. One class plots a nearly unlimited amount of single-
channel data using multiple traces on multiple pages.
A third class also plots a large quantity of two-channel data, but with this
class, the two sets of data are plotted on alternating horizontal axes. Again,
one set of data is colored black and the other set is colored red.
A fourth class plots a large quantity of three-channel data on separate axes. In
this case, one set is colored black, the second set is colored red, and the third
set is colored blue.
/*File PlotALot01.java
Copyright 2005, R.G.Baldwin
This program is designed to plot large amounts of
time-series data for a single channel. See
PlotALot02.java for a two-channel program.
Title: A
Frame width: 158
Frame height: 237
Page width: 150
Page height: 210
Trace spacing: 36
Sample spacing: 5
Traces per page: 5
Samples per page: 150
Title: B
Frame width: 400
Frame height: 410
Page width: 392
Page height: 383
Trace spacing: 50
Sample spacing: 2
Traces per page: 7
Samples per page: 1372
import java.awt.*;
import java.awt.event.*;
import java.util.*;
}//end main
//-------------------------------------------//
String title;
int frameWidth;
int frameHeight;
int traceSpacing;//pixels between traces
Listing 35. PlotALot01.java.
int sampSpacing;//pixels between samples
int ovalWidth;//width of sample marking oval
int ovalHeight;//height of sample marking oval
int tracesPerPage;
int samplesPerPage;
int pageCounter = 0;
int sampleCounter = 0;
ArrayList <Page> pageLinks =
new ArrayList<Page>();
PlotALot01(String title){
//call the other overloaded constructor
// passing default values for all but the
// title.
this(title,400,410,50,2,2,2);
}//end overloaded constructor
//-------------------------------------------//
Page(String title){//constructor
canvas = new MyCanvas();
add(canvas);
setSize(frameWidth,frameHeight);
setTitle(title + " Page: " + pageCounter);
setVisible(true);
//---------------------------------------//
//Anonymous inner class to terminate the
// program when the user clicks the close
// button on the Frame.
addWindowListener(
new WindowAdapter(){
public void windowClosing(
WindowEvent e){
System.exit(0);//terminate program
}//end windowClosing()
}//end WindowAdapter
);//end addWindowListener
//---------------------------------------//
}//end constructor
//=========================================//
//=========================================//
//Inner class
class MyCanvas extends Canvas{
double [] data =
new double[samplesPerPage];
/*File PlotALot02.java
Copyright 2005, R.G.Baldwin
This program is an update to the program named
PlotALot01. This program is designed to plot
large amounts of time-series data for two
channels on the same axes. One set of data is
plotted using the color black. The other set of
data is plotted using the color red. See
PlotALot01.java for a one-channel program.
Title: A
Frame width: 158
Frame height: 237
Page width: 150
Page height: 210
Trace spacing: 36
Listing 36. PlotALot02.java.
Sample spacing: 5
Traces per page: 5
Samples per page: 150
import java.awt.*;
import java.awt.event.*;
Listing 36. PlotALot02.java.
import java.util.*;
String title;
int frameWidth;
int frameHeight;
int traceSpacing;//pixels between traces
Listing 36. PlotALot02.java.
int sampSpacing;//pixels between samples
int ovalWidth;//width of sample marking oval
int ovalHeight;//height of sample marking oval
int tracesPerPage;
int samplesPerPage;
int pageCounter = 0;
int sampleCounter = 0;
ArrayList <Page> pageLinks =
new ArrayList<Page>();
PlotALot02(String title){
//call the other overloaded constructor
// passing default values for all but the
// title.
this(title,400,410,50,2,2,2);
}//end overloaded constructor
//-------------------------------------------//
Page(String title){//constructor
canvas = new MyCanvas();
add(canvas);
setSize(frameWidth,frameHeight);
setTitle(title + " Page: " + pageCounter);
setVisible(true);
//---------------------------------------//
//Anonymous inner class to terminate the
// program when the user clicks the close
// button on the Frame.
addWindowListener(
new WindowAdapter(){
public void windowClosing(
WindowEvent e){
System.exit(0);//terminate program
}//end windowClosing()
}//end WindowAdapter
);//end addWindowListener
//---------------------------------------//
}//end constructor
//=========================================//
//=========================================//
//Inner class
class MyCanvas extends Canvas{
double [] blackData =
new double[samplesPerPage];
double [] redData =
new double[samplesPerPage];
}//end if
}//end for loop
}//end if for sampleCounter > 0
}//end overridden paint method
}//end inner class MyCanvas
Listing 36. PlotALot02.java.
}//end inner class Page
}//end class PlotALot02
//=============================================//
/*File PlotALot03.java
Copyright 2005, R.G.Baldwin
This program is an update to the program named
PlotALot02. This program is designed to plot
large amounts of time-series data for two
channels on alternating horizontal axes. One set
of data is plotted using the color black. The
other set of data is plotted using the color red.
import java.awt.*;
import java.awt.event.*;
import java.util.*;
String title;
int frameWidth;
int frameHeight;
int traceSpacing;//pixels between traces
int sampSpacing;//pixels between samples
int ovalWidth;//width of sample marking oval
int ovalHeight;//height of sample marking oval
int tracesPerPage;
int samplesPerPage;
int pageCounter = 0;
int sampleCounter = 0;
ArrayList <Page> pageLinks =
new ArrayList<Page>();
PlotALot03(String title){
//call the other overloaded constructor
// passing default values for all but the
// title.
this(title,400,410,50,2,2,2);
}//end overloaded constructor
//-------------------------------------------//
Page(String title){//constructor
canvas = new MyCanvas();
add(canvas);
setSize(frameWidth,frameHeight);
setTitle(title + " Page: " + pageCounter);
setVisible(true);
//---------------------------------------//
//Anonymous inner class to terminate the
// program when the user clicks the close
// button on the Frame.
addWindowListener(
new WindowAdapter(){
public void windowClosing(
WindowEvent e){
System.exit(0);//terminate program
}//end windowClosing()
}//end WindowAdapter
);//end addWindowListener
//---------------------------------------//
}//end constructor
Listing 37. PlotALot03.java.
//=========================================//
//=========================================//
//Inner class
class MyCanvas extends Canvas{
double [] blackData =
new double[samplesPerPage];
double [] redData =
new double[samplesPerPage];
g.drawOval(cnt*(sampSpacing + 1)%
this.getWidth() - ovalWidth/2,
yOffset - (int)blackData[cnt]
- ovalHeight/2,
ovalWidth,
Listing 37. PlotALot03.java.
ovalHeight);
}//end if
}//end for loop
}//end if for sampleCounter > 0
}//end overridden paint method
}//end inner class MyCanvas
}//end inner class Page
}//end class PlotALot03
//=============================================//
/*File PlotALot04.java
Copyright 2005, R.G.Baldwin
This program is an update to the program named
PlotALot03. This program is designed to plot
large amounts of time-series data for three
Listing 38. PlotALot04.java.
channels on separate horizontal axes. One set
of data is plotted using the color black. The
second set of data is plotted using the color
red. The third set of data is plotted using the
color blue.
Title: A
Frame width: 158
Frame height: 270
Page width: 150
Page height: 243
Trace spacing: 36
Sample spacing: 5
Traces per page: 6
Samples per page: 60
import java.awt.*;
import java.awt.event.*;
import java.util.*;
String title;
int frameWidth;
int frameHeight;
int traceSpacing;//pixels between traces
int sampSpacing;//pixels between samples
int ovalWidth;//width of sample marking oval
int ovalHeight;//height of sample marking oval
Listing 38. PlotALot04.java.
int tracesPerPage;
int samplesPerPage;
int pageCounter = 0;
int sampleCounter = 0;
ArrayList <Page> pageLinks =
new ArrayList<Page>();
PlotALot04(String title){
//call the other overloaded constructor
// passing default values for all but the
// title.
this(title,400,410,50,2,2,2);
}//end overloaded constructor
//-------------------------------------------//
Page(String title){//constructor
canvas = new MyCanvas();
add(canvas);
setSize(frameWidth,frameHeight);
setTitle(title + " Page: " + pageCounter);
setVisible(true);
//---------------------------------------//
//Anonymous inner class to terminate the
// program when the user clicks the close
// button on the Frame.
addWindowListener(
new WindowAdapter(){
public void windowClosing(
WindowEvent e){
System.exit(0);//terminate program
}//end windowClosing()
}//end WindowAdapter
);//end addWindowListener
//---------------------------------------//
}//end constructor
//=========================================//
//=========================================//
//Inner class
class MyCanvas extends Canvas{
double [] blackData =
new double[samplesPerPage];
double [] redData =
new double[samplesPerPage];
double [] blueData =
new double[samplesPerPage];
g.drawOval(cnt*(sampSpacing + 1)%
this.getWidth() - ovalWidth/2,
yOffset - (int)blackData[cnt]
- ovalHeight/2,
ovalWidth,
Listing 38. PlotALot04.java.
ovalHeight);
}//end if
Miscellaneous
This section contains a variety of miscellaneous information.
-end-
Java1478-Fun with Java, How and Why Spectral Analysis Works
Baldwin explains how the Fourier transform can be used to determine the
spectral content of a signal in the time domain.
Table of contents
Table of contents
Preface
Viewing tip
Figures
Multiply-add operations
Typical notation
A sum-of-products operation
An alternative notation
Preview
Summary
What's next?
Miscellaneous
Preface
Programming in Java doesn't have to be dull and boring. In fact, it's
possible to have a lot of fun while programming in Java. This module is one
in a series that concentrates on having fun while programming in Java.
Viewing tip
Figures
Multiply-add operations
This module deals with a topic commonly know as Digital Signal
Processing, (DSP for short) .
Typical notation
N-1
__
\
z =(1/N) * / x(n) * y(n)
--
n = 0
A sum-of-products operation
This notation means that a new value for z is calculated by multiplying the
first N corresponding samples from each of two numeric series (x(n) and
y(n)), calculating the sum of the products, and dividing the sum by N.
(In this module, I will be dealing primarily with numeric series
that represent samples from a continuous function taken over
time. Therefore, I will often refer to the numeric series as a time
series.)
An alternative notation
The above notation requires about six lines of text to construct, and
therefore could easily become scrambled during the HTML publishing
process. I have invented an alternative notation that means exactly the same
thing, but is less likely to be damaged during the publishing process. My
new notation is shown in Figure 2 . You should be able to compare this
notation with Figure 1 and correlate the terms in the notation to the verbal
description of the operation given above.
Preview
If I compute the sum of the individual values in the series w(n), and then
divide that sum by the number of samples, this is nothing more than the
calculation of the mean or average value of the time series named w(n).
Most DSP operations boil down to nothing more complicated than
calculating the average value of the product of two time series.
The real trick in DSP is knowing what to multiply, when to multiply it, and
why to multiply it.
Some DSP algorithms are very complex. For example, the Fast Fourier
Transform (FFT) algorithm, involves nothing more than a lot of multiply-
add operations under the control of an extremely complex and efficient
control structure.
A periodic example
A periodic time series is one in which a set of sample values repeats over
time, provided that you record enough samples to include one or more
periods. Figure 4 shows a plot of a periodic time series. You can see that the
same set of values repeats as you move from left to right on the curve
plotted in Figure 4 .
Periodic curves can often be viewed as the sum of two curves. One of the
curves is the periodic component having a zero net area under the curve
when measured across an even number of cycles. The other component is a
constant bias offset that is added to every value of the periodic curve.
Each of the solid dark blobs in Figure 4 is a sample value. The horizontal
line represents a sample value of zero. (The empty circle is the sample value
half way through the sampling interval. The only reason it is different is to
mark the mid point.)
What is the net area under the curve in Figure 4 ? Can you examine the
curve and come up with a good estimate. As it turns out, the net area under
the curve in Figure 4 is very close to zero (at least it is as close to zero as I
was able to draw it) .
Now take a look at Figure 5 . What is the net area under the curve in Figure
5?
Each of these curves describes the same periodic shape (although Figure 4
has a larger peak-to-peak amplitude, meaning simply that every value in
Figure 4 has been multiplied by the same scale factor) .
The curve in Figure 5 can be considered to consist of the sum of two parts.
One part is a straight horizontal line on the positive side of the horizontal
axis. The other part is the periodic curve from Figure 4 , added to that
positive bias.
The net area contributed by the periodic part of the curve in Figure 5
continues to be zero. In effect, the non-zero net area under the curve in
Figure 5 is the amount of the positive bias multiplied by the number of
points. In other words, because I captured an even number of cycles of the
periodic portion of the curve in my calculation of net area, only the bias
contributes a non-zero value to the net area under the total curve.
Part of a cycle
As you will see in this module, in doing frequency spectral analysis, we will
form a product between a target time series and a sinusoid. The purpose is
to measure the power contained in the time series at the frequency of the
sinusoid.
The product of the target time series and the sinusoid will produce the sum
of a potentially infinite number of periodic functions, some of which may
be riding on a positive or negative bias. We will measure the amount of bias
by computing the average value of the product time series. The amount of
bias will be our estimate of the power contained in the target time series at
the frequency of the sinusoid.
Usually when dealing with the time domain and the frequency domain, the
values that make up the samples in the time domain are purely real while
the values that make up the samples in the frequency domain are complex
containing both real and imaginary parts.
Time domain and frequency domain
If you were to draw a graph of the voltage impinging on the speaker coils
on your stereo system over time, that would be a time series, which is a
member of the time domain.
If you were to observe the lights dancing up and down on the front of your
equalizer while the music is playing, you would be observing the same
information presented in the frequency domain. Typically the lights on the
left represent low frequencies or bass while the lights on the right side
represent high frequencies or treble. Often there is a slider associated with
each vertical group of lights that allows you to apply filters to emphasize
certain parts of the frequency spectrum and to de-emphasize other parts of
the frequency spectrum.
There are two very similar forms of the Fourier transform. The forward
transform is typically used to transform information from the time domain
into the frequency domain. The inverse transform is typically used to
transform information from the frequency domain back into the time
domain.
Sampled time series
Because the DFT algorithm is somewhat easier to understand than the FFT
algorithm, and also more general, I will concentrate on the DFT algorithm
to explain how and why the Fourier transform accomplishes what it
accomplishes.
Real(F) = S(n=0,N-1)[x(n)*cos(2Pi*F*n)]
Imag(F) = S(n=0,N-1)[x(n)*sin(2Pi*F*n)]
Create one new time series, cos(n), which is a cosine function with the
frequency F.
Create another new time series, sin(n), which is a sine function with
the frequency F. (The methods needed to create the cosine and sine
time series are available in the Math class in the standard Java
library.)
Multiply x(n) by cos(n) and compute the sum of the products. Save
this value, calling it Real(F). This is an estimate of the amplitude, if
any, of the cosine component with the matching frequency contained
in the time series x(n).
Multiply x(n) by sin(n) and compute the sum of the products. Save this
value, calling it Imag(F). This is an estimate of the amplitude, if any,
of the sine component with the matching frequency contained in the
time series x(n).
Consider the values for Real(F) and Imag(F) to be the real and
imaginary parts of a complex number.
Consider the sum of the squares of the real and imaginary parts to
represent the power at that frequency in the time series.
That's all there is to it. For each frequency of interest, you can use this
process to compute a complex number, Real(F) - jImag(F), which
represents the component of that frequency in the target time series.
(This is typically the value that you would see being displayed by
one of the dancing vertical bars on the front of the equalizer on
your stereo system.)
(This would produce the set of values that you would likely see
being displayed by all of the dancing vertical bars on the font of
the equalizer on your stereo system.)
1. sin(a)*sin(b)=(1/2)*(cos(a-b)-cos(a+b))
2. cos(a)*cos(b)=(1/2)*(cos(a-b)+cos(a+b))
3. sin(a)*cos(b)=(1/2)*(sin(a+b)+sin(a-b))
Although these identities apply to the products of sine and cosine values for
single angles a and b , it is a simple matter to extend them to represent the
products of time series consisting of sine and cosine functions. Such an
extension is shown in Figure 8 .
In each of the three cases shown in Figure 8 , the function f(n) is a time
series produced by multiplying two other time series, which are either sine
functions or cosine functions.
1. f(n) = sin(a*n)*sin(b*n) =
(1/2)*(cos((a-b)*n)-cos((a+b)*n))
2. f(n) = cos(a*n)*cos(b*n) =
(1/2)*(cos((a-b)*n)+cos((a+b)*n))
3. f(n) = sin(a*n)*cos(b*n) =
(1/2)*(sin((a+b)*n)+sin((a-b)*n))
Rewrite and simplify
Figure 9 rewrites and simplifies these three functions for the special case
where a=b , taking into account the fact that cos(0) =1 and sin(0) = 0.
First you need to recall that the average of the values describing any true
sinusoid is zero when the average is computed over an even number of
cycles of the sinusoid.
If a time series consists of the sum of two true sinusoids, then the average
of the values describing that time series will be zero if the average is
computed over an even number of cycles of both sinusoids, and very close
to zero if the average is computed over a period that is not an even number
of cycles for either or both sinusoids.
(The average will approach zero as the length of data over which
the average is computed increases.)
Let's apply this knowledge to the three cases shown above for a=b .
Consider the time series for case 1 in Figure 9 . This case is the product of
two sine functions having the same frequency. The result of multiplying the
two sine functions is shown graphically in Figure 10 .
If you sum the values of the black curve over an even number of cycles, the
sum will not be zero. Rather, it will be a positive, non-zero value.
Now consider the time series for case 2 in Figure 9 . This case is the
product of two cosine functions having the same frequency. The result of
multiplying two cosine functions having the same frequency is shown
graphically in Figure 11 .
The red curve in Figure 11 shows the function cos(x), and the black curve
shows the function produced by multiplying cos(x) by cos(x).
If you sum the values of the black curve in Figure 11 over an even number
of cycles, the sum will not be zero. Rather, it will be a positive, non-zero
value.
Now refer back to the expression for Real(F) in Figure 6 . The real part of
the transform is computed by multiplying the time series by a cosine
function having a particular frequency and computing the sum of products.
If that time series contains a cosine component with the same frequency as
the cosine function, that component will contribute a non-zero value to the
sum of products. Thus, the real part of the transform at that frequency will
not be zero.
The red curve in Figure 12 shows the function cos(x), and the green curve
shows the function sin(x). The black curve shows the function produced by
multiplying sin(x) by cos(x).
If you sum the values of the black curve over an even number of cycles, the
sum will be zero.
In reality, the sinusoidal components that make up a time series will not
usually be sine functions or cosine functions. Rather, they will be sinusoidal
components having the same shape as a sine or cosine, but not having the
same value at zero as either a sine function or a cosine function. However, it
can be shown that a general sinusoidal function can always be represented
by the sum of a sine function and a cosine function having different
amplitudes and the same frequency.
The answer is not very much. Referring once more to the functional forms
produced by multiplying sine and cosine functions (repeated in Figure 13
for convenience) , we see that for any values of a and b , where a is not
equal to b , the function produced by multiplying two sinusoids will be the
sum of two other sinusoids. The sum of the values for any sinusoid
computed over an even number of cycles of the sinusoid will always be
zero, and these sinusoids are no exception to that rule.
1. f(n) = sin(a*n)*sin(b*n) =
(1/2)*(cos((a-b)*n)-cos((a+b)*n))
2. f(n) = cos(a*n)*cos(b*n) =
(1/2)*(cos((a-b)*n)+cos((a+b)*n))
3. f(n) = sin(a*n)*cos(b*n)
=(1/2)*(sin((a+b)*n)+sin((a-b)*n))
For any pair of arbitrary values for a and b , the frequencies of the sinusoids
in the resulting functions will be given by a+b and a-b .
(These are often referred to as the sum and difference
frequencies. Note that as a approaches b , the difference
frequency approaches zero. This is what produces the constant
values of 1/2 in Figure 9 and the positive bias on the black curve
in Figures 10 and 11.)
However, the summation would include less than one complete cycle of the
low frequency component. Therefore, the low frequency component would
contribute some output to the summation in the form of a measurement
error.
Summary
In this module, I have provided a quasi-theoretical basis for frequency
spectrum analysis.
What's next?
The next module in this series will reduce much of what I have discussed in
this module to practice. I will present and explain a program that
implements a DFT algorithm for performing frequency spectrum analysis.
In addition, I will present the results of several interesting experiments in
frequency spectrum analysis using that algorithm.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Housekeeping material
Baldwin explains how the Fourier transform can be used to determine the
spectral content of a signal in the time domain.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it
possible for you to purchase a pre-printed version of the PDF file, you
should be aware that some of the HTML elements in this module may not
translate well into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales
nor do I know who does receive compensation. If you purchase such a
book, please be aware that it is a copy of a module that is freely available
on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Java1482-Spectrum Analysis using Java, Sampling Frequency, Folding
Frequency, and the FFT Algorithm
Baldwin explains several different programs used for spectral analysis
including a DFT program and an FFT program. He also explains the impact of
the sampling frequency and the Nyquist folding frequency on spectral
analysis.
Table of contents
Table of contents
Preface
Viewing tip
Figures
Listings
Preview
Discussion and sample code
Display sinusoids
The plotting program named Graph06
The Nyquist folding frequency
One more example
Beginning of the class named Dsp029
The getNmbr method
The method named f1
The program named Graph06
Preface
The how and the why of spectral analysis
A previous module titled Fun with Java, How and Why Spectral Analysis
Works explained how and why spectral analysis works. An understanding of
that module is a prerequisite to understanding this module.
In this module I will provide and explain different programs used for
performing spectral analysis. The first program is a very general program that
implements a Discrete Fourier Transform (DFT) algorithm. I will explain this
program in detail.
The second program is a less general, but much faster program that
implements a Fast Fourier Transform (FFT) algorithm. I will defer an
explanation of this program until a future module. I am providing it here so
that you can use it and compare it with the DFT program in terms of speed
and flexibility.
I will also provide and explain a program that produces a visual illustration of
the impact of the sampling frequency and the Nyquist folding frequency.
Plotting programs
Finally, I will provide, but will not explain two different programs used for
display purposes. These are newer versions of graphics display programs that
I explained in the module titled Plotting Engineering and Scientific Data using
Java .
Viewing tip
I recommend that you open another copy of this module in a separate browser
window and use the following links to easily find and view the Figures and
Listings while you are reading about them.
Figures
Listings
Preview
Before I get into the technical details, here is a preview of the programs and
their purposes that I will present and explain in this module:
Display sinusoids
This program generates and displays up to five sinusoids having the same
sampling frequency but having different sinusoidal frequencies and
amplitudes. The program provides a visual illustration of the way in which
frequencies above one-half the sampling frequency fold back into the area
bounded by zero and one-half the sampling frequency.
Input parameters
The program gets its input parameters from a file named Dsp029.txt . If that
file doesn't exist in the current directory, the program uses a set of default
parameters.
Each parameter value must be stored as characters on a separate line in the file
named Dsp029.txt . The required parameters are shown in Figure 1 .
The number of values in each of the lists must match the value for the number
of sinusoids. Also, you must not allow blank lines at the end of the data in the
file.
Figure 2 shows the contents of the file named Dsp029.txt that I used to
produce the output shown in Figure 3 . I will discuss that output later.
Figure 2. Actual parameter values.
50.0
5
0.03125
0.0625
0.125
0.25
0.5
90
90
90
90
90
The plotting program that is used to plot the output data from this program
requires that the program implement GraphIntfc01 . I discussed that interface
in the module titled Plotting Engineering and Scientific Data using Java . For
example, the plotting program named Graph06 can be used to plot the data
produced by this program. When it is used, the program is executed and its
output is plotted by entering the following at the command line prompt:
Program output
Figure 3 shows the output produced by running the program named Dsp029
with the parameters shown in Figure 2 and then adjusting the xMax parameter
in the textbox at the bottom of the display.
Figure 3. Program output for five sinusoids.
Each of the five horizontal plots in Figure 3 shows a sampled sinusoid. Each
of the vertical bars represents one sample value for a given sinusoid.
If you examine the frequency values in Figure 2 carefully, you will see that
they represent the sampling frequency divided by the factors 32, 16, 8, 4, and
2. Thus, the last frequency value is the Nyquist folding frequency and the first
four frequency values are related to that frequency by multiples of two.
The horizontal plot at the top of Figure 3 is a reasonably well defined cosine
wave. The horizontal plot at the bottom of Figure 3 shows the result of having
exactly two samples per cycle of the sinusoid. Using this plotting scheme, the
sampled sinusoid is represented as a square wave. Using another plotting
scheme (such as that used in the program named Graph03 ) the sampled
sinusoid at the bottom would be represented as a triangular wave.
Now I will show you why the frequency at one-half the sampling frequency is
referred to as the folding frequency using the new set of frequency values
shown in Figure 4 .
( Figure 4 shows the values read from the file named Dsp029.txt
and displayed in an improved format.)
In this case, the third frequency in the list is one-half the sampling frequency,
which is the folding frequency. The two frequencies on either side of that one
have values that are symmetrical about the folding frequency.
Data length: 50
Number sinusoids: 5
Frequencies
0.125
0.25
0.5
0.75
0.875
Amplitudes
90.0
90.0
90.0
90.0
90.0
The five horizontal plots in Figure 5 show the result of running the program
named Dsp029 with the frequencies shown in Figure 4 .
The top two plots in Figure 5 are obviously the sampled representations of the
two lower frequencies specified by the first two frequencies in Figure 4 .
However, it is not so obvious that the bottom two plots in Figure 5 are the
sampled representations of the two higher frequencies specified by the last
two frequencies in Figure 4 . They look exactly like the top two plots but in
reverse order.
Unable to distinguish ...
Let's look at one more example of plotted sinusoids. Consider the frequency
values shown in Figure 6 . The second and third frequencies are symmetrical
about the sampling frequency. The fourth and fifth frequencies are
symmetrical about twice the sampling frequency. The first frequency value is
the same distance from zero as the other four frequencies are from the
sampling frequency and twice the sampling frequency.
Data length: 50
Number sinusoids: 5
Frequencies
0.1
0.9
1.1
1.9
2.1
Amplitudes
90.0
90.0
90.0
90.0
90.0
Figure 7 shows the output produced by running the program named Dsp029
with the frequency parameters specified by Figure 6 .
Although the actual frequencies of the five cosine functions are significantly
different, once they are sampled, they are indistinguishable. The sampling
process converts the actual frequencies to new frequencies that not only fold
around one-half the sampling frequency, they also fold around all multiples of
one-half the sampling frequency.
The code in Listing 2 creates five array objects that will be populated with
sinusoidal data.
Listing 2. Create array objects to hold sinusoidal data.
The constructor begins in Listing 3 . The code in this fragment calls the
method named getParameters to read the parameters from the file named
Dsp029.txt .
public Dsp029(){//constructor
if(new File("Dsp029.txt").exists()){
getParameters();
}//end if
Before calling the getParameters method, however, the program calls the
exists method of the File class to confirm that the file actually exists. If the
file doesn't exist, the call to getParameters is skipped, causing the default
parameters defined in Listing 1 to be used instead.
The getParameters method
In addition, the getParameters method displays the values read from the disk
file in the format shown in Figure 4 and Figure 6 .
For simplicity, this program always generates five sinusoids, even if fewer
than five were requested as the input parameter value for numberSinusoids .
In that case, the extra sinusoids are generated using default values and are
simply ignored when the sinusoids are plotted.
The code fragment in Listing 4 creates the sinusoidal data for each of the five
specified frequencies and saves that data in the array objects that were created
in Listing 2 .
}//end constructor
Listing 4 also signals the end of the constructor. When the constructor
terminates, an object of the Dsp029 class has been instantiated. The five
arrays shown in Listing 4 have been populated with sinusoidal data according
to the parameters read from the file named Dsp029.txt or according to the
default values of the parameters shown in Listing 1 .
The plotting program named Graph06 can be used to plot the sinusoidal data
as follows:
In this case, the constructor for the Dsp029 class populates the five array
objects with sinusoidal data. The subsequent call to the interface methods by
the program named Graph06 causes that sinusoidal data to be retrieved and
plotted by Graph06.
The method named getNmbr must return an integer value between 1 and 5
that specifies the number of functions to be plotted. The plotting program uses
that value to divide the total plotting surface into the specified number of
plotting areas, and plots each of the functions named f1 through fn in one of
those plotting areas.
As you can see in Listing 5 , each of these methods receives a double value as
an incoming parameter and returns a double value. In essence, each of these
methods receives a value for the horizontal coordinate x and returns the
corresponding value for the vertical coordinate y.
Each plotting area contains a horizontal axis. The plotting program moves
across the horizontal axis in each plotting area one step at a time (moving in
incremental steps equal to the plotting parameter named xCalcInc , which
you will find if you examine the code for Graph06 ) .
At each step along the way, the plotting program calls the method associated
with that plotting area, ( f1 , f2 , etc.) , passing the horizontal position as a
parameter to the method.
The plotting program doesn't know, and doesn't care how the interface method
decides on the value to return for each value that it receives as an incoming
parameter. The plotting program simply calls the methods to get the data, and
then plots the returned values.
On the other hand, the values could have been computed earlier and saved in
an array. That is the case with all the programs that I will explain in this
module.
The returned values could be read from a disk file, obtained from a database
on another computer, or obtained from any other source such as another
computer on the Internet.
All that matters is that when the plotting program calls one of the five
methods named f1 through f5 , passing a double value as a parameter, it
expects to receive a double value in return, and it will plot the value that it
receives.
The getNmbr method for the class named Dsp029 is shown in Listing 6 .
This is a very simple method. It returns the value stored in the variable named
numberSinusoids . This variable may contain the default value established
by Listing 1 , or may contain the value read from the file named Dsp029.txt
by the method named getParameters .
If the index value is outside the bounds of the array, the method simply
returns a value of zero. Otherwise, it uses the index value to return the value
stored in the array object at that index.
The remaining four interface methods are identical to the method named f1 ,
except that each method returns data values stored in a different array object.
Therefore, I won't discuss those methods.
That's about it for the program named Dsp029 . If you understand this
program, you are well ahead of the game. The overall structure for the
programs named Dsp028 and Dsp030 are very similar to the structure for
Dsp029 . The big difference is the manner in which they populate the array
objects with the data that is to be plotted. Instead of simply plotting sinusoids,
they perform spectral analysis on sinusoids and provide the results of the
spectral analysis to be plotted.
Using the interface named GraphIntfc01
As you learned earlier, this is a very simple interface. However, because the
class named Dsp029 implements the interface, the interface definition file
must be in the same directory as the source file for Dsp029 in order to
successfully compile Dsp029 . Therefore, I have provided a complete listing
of GraphIntfc01 in Listing 17 near the end of the module.
As you saw in Figure 3 and other previous figures, this program provides the
following text fields for user input, along with a button labeled Graph :
You can modify any of these parameters and then click the Graph button to
cause the five functions to be re-plotted according to the new plotting
parameters.
Now that you have a good idea where we are heading, it's time to start doing
some spectral analysis.
Although more parameters are required to perform spectral analysis than are
required to simply generate and plot the sinusoids, the number of sinusoids,
the frequencies of the sinusoids, and the amplitudes of the sinusoids in Figure
8 are the same as in Figure 2 .
Five separate spectral analyses were performed and the results of those five
spectral analyses are shown in Figure 9 . Each of the horizontal lines in Figure
9 is the horizontal axis used to display the result of performing a spectral
analysis on a different sinusoid. In other words, Figure 9 contains five
separate graphs moving from the top to the bottom of the display. The
individual graphs have alternating white and gray backgrounds to make them
easier to separate visually.
The top graph in Figure 9 shows the result of performing a spectral analysis
on the top sinusoid in Figure 3 . Moving down the page, each graph in Figure
9 shows the result of performing a spectral analysis on the corresponding
sinusoid in Figure 3 .
The horizontal axes in Figure 9 represent the frequency range from zero to the
sampling frequency.
Because the right-most end of each horizontal axis in Figure 9 represents the
sampling frequency, the center of each horizontal axis represents one-half the
sampling frequency, or the Nyquist folding frequency. Thus, the frequency
represented by the center of each horizontal axis represents the frequency
specified by a value of 0.5 in Figure 8 .
You can see a large peak in energy at the folding frequency of the bottom
graph in Figure 9 . That peak corresponds to the frequency of the fifth
sinusoid specified in the parameters shown in Figure 8 . (This also
corresponds to the spectrum of the bottom graph in Figure 3 .)
Knowing that, you should be able to correlate each of the peaks to the left of
center in Figure 9 with the frequencies of the sinusoids specified in Figure 8
and with the individual sinusoids plotted in Figure 3 .
Figure 9 clearly shows the frequency folding effect of the sampling process
illustrated earlier. As you can see, the peaks in the various graphs to the right
of the folding frequency are mirror images of the peaks to the left of the
folding frequency. In other words, given a set of samples of a sinusoid, the
spectral analysis process is unable to determine whether the peak is above or
below the folding frequency, so the energy is equally distributed between two
peaks on opposite sides of the folding frequency.
Note that the peak in the bottom graph is approximately twice the height of
the peaks in the other graphs. This is because the peak at the folding
frequency has no mirror-image partner, and all the energy is concentrated in
that single peak.
You may also have noticed that the peaks in the top graph are shorter and
wider than the peaks in the other graphs. This may be because the actual
frequency of the sinusoid for the top graph is about half way between the
values of the twelfth and thirteenth bins for which spectral energy was
computed. Thus, the energy in the sinusoid was spread between the bins on
either side of the actual frequency.
It is also instructive to plot these spectra with a data length of 400 using the
program named Graph06 . This will show you how the energy is distributed
between the frequency bins. This is most effective when the graph is
expanded as described in the next section.
The broadening of the peak in the top graph may also have to do with the
requirement to map the peaks in the spectrum to the locations of the actual
pixels on the screen. If the location of the peak falls between the positions of
two pixels, the plotting program must interpolate the energy in the peak so as
to display that energy in actual pixel locations.
This effect can be minimized by plotting the same number of spectral values
across a wider area of the screen. When you run this program later, click the
maximize button on the Frame to cause the display to occupy the entire
screen. That will give you a much better look at the actual shape of each of
the peaks. Do this using both Graph03 and Graph06 to plot the results.
(Note: When switching between the plotting programs, you may
need to delete the class files from the old program and compile the
new program to avoid having class files with the same names from
the two programs becoming intermingled in the same directory.)
This next example is designed to illustrate the following features of the DFT
algorithm which don't generally apply to an FFT algorithm:
As mentioned earlier, the DFT algorithm is much more flexible while the FFT
algorithm is much faster, particularly for large data lengths.
The input parameters are shown in Figure 10 . Note in particular the values
for the following parameters:
As you can see, the data length for this experiment is different from the data
length of 400 used earlier. In addition, neither data length is a power of two.
Computational frequency bounds
As you can also see, the lower and upper frequency bounds are not 0.0 and 1.0
as in the earlier cases. In this case, the frequency bounds describe a much
narrower range centered on the folding frequency.
The output from the spectral analysis for each of the five sinusoids is shown
in Figure 11 . (Another interesting view of the same results is shown later in
Figure 14 .)
The spectral peaks shown in Figure 11 are symmetrical about the folding
frequency, which in turn is centered horizontally in each of the graphs. As you
already know, the peaks are always symmetrical about the folding frequency
due to the frequency folding at that frequency.
The folding frequency is centered horizontally due to the way that I defined
the lower and upper frequency bounds, and the way that I adjusted the plotting
parameters.
Peaks are well defined and wider than before
The peaks are well defined because I computed the spectral energy at 200
points across the specified frequency range from 0.4 to 0.6. Thus, the
frequency bins at which I computed spectral energy were much narrower than
before.
The peaks are wider because I displayed a much smaller slice of the entire
frequency spectrum in the same physical screen space.
As the frequency of each sinusoid approaches the folding frequency, the two
mirror-image peaks corresponding to that sinusoid merge into a single peak
with twice the height at the folding frequency. This agrees with what you saw
in Figure 9 , but on a much more detailed basis.
In addition, I will present, but will not explain the plotting program named
Graph03 .
The program named Dsp029 simply populates those array objects with five
sinusoidal functions. The program named Dsp028 also creates five sinusoidal
functions. However, it passes those functions to a static method named
transform belonging to the ForwardRealToComplex01 class to perform
spectral analysis on those functions. The results of the spectral analysis are
used to populate the five array objects whose contents are plotted by the
plotting program.
The program named Dsp028 computes and displays the magnitude of the
spectral content for up to five sinusoids having different frequencies and
amplitudes.
Input parameters
The program gets input parameters from a file named Dsp028.txt . If that file
doesn't exist in the current directory, the program uses a set of default
parameters. As with the program named Dsp029 , each parameter value must
be stored as characters on a separate line in the file named Dsp028.txt . The
required parameters are shown in Figure 12 .
Figure 12. Required input parameters for Dsp028.
Don't allow blank lines at the end of the data in the file.
The number of values in each of the lists must match the value for the number
of spectra.
Figure 13 shows the contents of the file named Dsp028.txt that represent the
parameters shown in Figure 10 .
Figure 13. Contents of Dsp028.txt file.
200
0
0.4
0.6
5
0.492
0.494
0.496
0.498
0.5
90
90
90
90
90
The class definition begins in Listing 8 . For reasons that you already
understand, this class implements the interface named GraphIntfc01.
The code in Listing 8 defines a set of default parameter values that are used in
the event that a file named Dsp028.txt does not exist in the current directory.
The constructor
The constructor for the class begins in Listing 10 . The constructor begins by
getting the parameters from a file named Dsp028.txt . If that file doesn't exist
in the current directory, default parameters are used.
public Dsp028(){//constructor
if(new File("Dsp028.txt").exists()){
getParameters();
}//end if
For simplicity, this program always processes five sinusoids, even if fewer
than five were requested as the input parameter for numberSpectra . In that
case, the extra sinusoids are processed using default values and simply
ignored when the results are plotted.
The code in Listing 11 instantiates array objects and creates the sinusoidal
data upon which spectral analysis will be performed.
Listing 11. Create the raw sinusoidal data.
The code in Listing 12 creates array objects to receive the results and calls the
static transform method of the forwardRealToComplex01 class five times
in succession to perform the spectral analysis on each of the five sinusoids.
imag,angle,magnitude1,zeroTime,lowF,highF);
imag,angle,magnitude2,zeroTime,lowF,highF);
imag,angle,magnitude3,zeroTime,lowF,highF);
imag,angle,magnitude4,zeroTime,lowF,highF);
imag,angle,magnitude5,zeroTime,lowF,highF);
}//end constructor
Note that the magnitude results are saved in the array objects referred to by
magnitude1 , magnitude2 , etc. This will be important later when I discuss
the interface methods defined by Dsp028 .
Listing 12 also signals the end of the constructor. When the constructor
terminates, the object has been instantiated and populated with spectral
analysis results for five sinusoids using the parameters specified by the file
named Dsp028.tx t.
The getParameters method used in this program is the same as that used in
Dsp029 , so I won't discuss it further.
The interface methods
The Dsp028 class must define the same six interface methods as the Dsp029
class, which I discussed earlier. The only difference in the interface methods
is the identification of the array objects from which the methods return data
when the methods are called.
The plots in Figure 9 and Figure 11 were produced by entering the following
at the command line prompt:
That brings us to the heart of this module, which is the method that actually
implements the DFT algorithm and performs the spectral analysis. This is a
method named transform , which is a static method of the class named
ForwardRealToComplex01 . You saw this method being called five times in
the code in Listing 12 .
A brief description
For those of you who don't have the time to go back and study that module in
detail, a brief description of the DFT algorithm follows.
Using a notation that I described in the earlier module, the expressions that
you must evaluate to determine the frequency spectral content of a target time
series at a frequency F are shown in Figure 15 .
Figure 15. Spectral transform expressions.
Real(F) = S(n=0,N-1)[x(n)*cos(2Pi*F*n)]
Imag(F) = S(n=0,N-1)[x(n)*sin(2Pi*F*n)]
Before you panic, let me explain what this means in layman's terms. Given a
time series, x(n), you can determine if that time series contains a cosine
component or a sine component at a given frequency, F, by doing the
following:
Create one new time series, cos(n), which is a cosine function with the
frequency F.
Create another new time series, sin(n), which is a sine function with the
frequency F.
Multiply x(n) by cos(n) on a point by point basis and compute the sum of
the products. Save this value, calling it Real(F). This is an estimate of the
amplitude, if any, of the cosine component with the matching frequency
contained in the time series x(n).
Multiply x(n) by sin(n) on a point by point basis and compute the sum of
the products. Save this value, calling it Imag(f). This is an estimate of the
amplitude, if any, of the sine component with the matching frequency
contained in the time series x(n).
Consider the values for Real(F) and Imag(F) to be the real and imaginary
parts of a complex number.
Consider the sum of the squares of the real and imaginary parts to
represent the power at that frequency in the time series.
Consider the square root of the power to be the amplitude at that
frequency in the time series. (This is the value that is plotted in Figure 9 ,
Figure 11 , and Figure 14 .)
That is all there is to it. For each frequency of interest, you can use this
process to compute a complex number, Real(F)-jImag(F), which represents
the complex energy corresponding to that frequency in the target time series.
Similarly, you can compute the sum of the squares of the real and imaginary
parts and consider that to be a measure of the power at that frequency in the
time series. The square root of the power is the amplitude of the energy at that
frequency.
The magnitude or amplitude is computed as the square root of the sum of the
squares of the real and imaginary parts. This value is divided by the incoming
data length, which is given by data.length .
The method returns a number of points in the frequency domain equal to the
incoming data length regardless of the high and low frequency limits.
The class and the transform method begin in Listing 14 . The code in Listing
14 is described above.
Listing 14. The beginning of the transform method.
The nested for loops discussed above are included in the code shown in
Listing 15 . As suggested above, the outer loop iterates on frequency while the
inner loop iterates on the values that make up the incoming samples. The code
in the inner loop computes the sum of the product of the time series and the
reference cosine and sine functions.
realOut[i] = real/dataLen;
imagOut[i] = imag/dataLen;
magnitude[i] = (Math.sqrt(
real*real +
imag*imag))/dataLen;
At the end of each iteration of the inner loop, code in the outer loop deposits
the real, imaginary, magnitude, and phase angle results in the output array
objects. To accomplish this, the code:
Now you know about the DFT algorithm. You also know about some of the
fundamental aspects of spectral analysis involving the sampling frequency
and the folding frequency.
At this point, I will present a similar spectral analysis program that uses an
FFT algorithm. I will present this program with very little discussion. I am
providing it in this module for two primary purposes:
This program uses an FFT algorithm to compute and display the magnitude of
the spectral content for up to five sinusoids having different frequencies and
amplitudes. (See the program named Dsp028 for a program that does not use
an FFT algorithm.)
The program gets input parameters from a file named Dsp030.txt . If that file
doesn't exist in the current directory, the program uses a set of default
parameters.
Each parameter value must be stored as characters on a separate line in the file
named Dsp030.txt . The required input parameters are shown in Figure 16 .
(Contrast this with the required input parameters for Dsp028 shown in Figure
12 .)
Note that in contrast with Figure 12 , the required input parameters for
Dsp030 do not include the sample number representing zero time, the lower
frequency bound for computation of the spectra, and the upper frequency
bound for computation of the spectra.
As with Dsp028 , the number of values in each of the lists must match the
value for the number of spectra.
Figure 17 shows the parameters used to produce the spectral analysis plots
shown later in Figure 18 .
256
5
0.1
0.2
0.3
0.5
0.005
90
90
90
90
90
The plotting program that is used to plot the output data from this program
requires that the program implement GraphIntfc01 . For example, the
plotting program named Graph03 can be used to plot the data produced by
this program. This requires that you enter the following at the command line
prompt after everything is compiled:
The plotting program named Graph06 can also be used to plot the data
produced by this program, requiring that you enter the following at the
command line prompt:
The output produced by running Dsp030 using the input parameters shown in
Figure 17 is shown in Figure 18 .
Figure 19 shows the parameters required for the program named Dsp028 to
perform a DFT spectral analysis producing the same results as those produced
by the FFT analysis shown in Figure 18 . Note that the data length has been
set to 256 and the computational frequency range extends from zero to the
sampling frequency in Figure 19 .
The DFT output produced by running Dsp028 with the parameters shown in
Figure 19 is shown in Figure 20 .
Figure 20. DFT of five sinusoids.
Hopefully you noticed that Figure 20 looks almost exactly like Figure 18 .
This is how it should be. The DFT algorithm and the FFT algorithm are
simply two different algorithms for computing the same results. However, the
DFT algorithm is much more flexible than the FFT algorithm while the FFT
algorithm is much faster than the DFT algorithm.
I recommend that you repeat these two experiments several times increasing
the data length to a higher power of two each time you run the experiments.
If what you need is speed for long data lengths, the FFT is your best approach.
On the other hand, if you need more flexibility than the FFT provides and the
data length is not too long, then the DFT may be your best approach.
Fortunately, you don't have to understand the mechanics of the FFT algorithm
works to be able to use it.
I suggest that you begin by compiling and running the following files to
confirm that everything is working correctly on your machine before
attempting to compile and run the spectral analysis programs:
Dsp029.java
GraphIntfc01.java
Graph06.java
Make sure that you create an appropriate file named Dsp029.txt , as described
in Figure 2 . You should be able to reproduce my results if everything is
working correctly.
Once you confirm that things are working correctly, copy, compile, and run
the spectral analysis programs. Experiment with the parameters and try to
understand the result of making changes to the parameters. Confirm the
flexibility of the DFT algorithm and the speed of the FFT algorithm.
Summary
In this module I have provided and explained programs that illustrate the
impact of sampling and the Nyquist folding frequency.
I have also provided and explained several different programs used for
performing spectral analysis. The first program was a very general program
that implements a Discrete Fourier Transform (DFT) algorithm. I explained
this program in detail.
The second program was a less general, but much faster program that
implements a Fast Fourier Transform (FFT) algorithm. I will defer an
explanation of this program until a future module. I provided it in this module
so that you can use it and compare it with the DFT program in terms of speed
and flexibility.
What's next?
Future modules will discuss other aspects of spectral analysis including:
/* File Dsp029.java
Copyright 2004, R.G.Baldwin
Rev 5/6/04
400.0
5
0.1
0.9
1.1
1.9
2.1
90
90
90
90
90
public Dsp029(){//constructor
}//end constructor
//-------------------------------------------//
}//end getParameters
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01. The plotting
// program pulls the data values to be plotted
// by calling these methods.
public int getNmbr(){
//Return number of functions to
// process. Must not exceed 5.
return numberSinusoids;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > data1.length-1){
return 0;
}else{
return data1[index];
}//end else
}//end function
//-------------------------------------------//
Listing 16. Dsp029.java.
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > data2.length-1){
return 0;
}else{
return data2[index];
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > data3.length-1){
return 0;
}else{
return data3[index];
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > data4.length-1){
return 0;
}else{
return data4[index];
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > data5.length-1){
return 0;
}else{
Listing 16. Dsp029.java.
return data5[index];
}//end else
}//end function
//-------------------------------------------//
/* File GraphIntfc01.java
Copyright 2004, R.G.Baldwin
Rev 5/14/04
/* File Graph06.java
Copyright 2002, R.G.Baldwin
Revised 5/15/04
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
class Graph06{
public static void main(
String[] args)
throws NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
if(args.length == 1){
//pass command-line paramater
Listing 18. Graph06.java.
new GUI(args[0]);
}else{
//no command-line parameter given
new GUI(null);
}//end else
}// end main
}//end class Graph06 definition
//===================================//
//Constructor
Listing 18. Graph06.java.
GUI(String args)throws
NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
if(args != null){
//Save for use later in the
// ActionEvent handler
this.args = args;
//Instantiate an object of the
// target class using the String
// name of the class.
data = (GraphIntfc01)
Class.forName(args).
newInstance();
}else{
//Instantiate an object of the
// test class named junk.
data = new junk();
}//end else
pan1.add(new JLabel("xMax"));
pan1.add(xMaxTxt);
pan2.add(new JLabel("yMin"));
pan2.add(yMinTxt);
pan3.add(new JLabel("yMax"));
pan3.add(yMaxTxt);
pan4.add(new JLabel("xTicInt"));
pan4.add(xTicIntTxt);
pan5.add(new JLabel("yTicInt"));
pan5.add(yTicIntTxt);
pan6.add(new JLabel("xCalcInc"));
Listing 18. Graph06.java.
pan6.add(xCalcIncTxt);
setBounds(0,0,frmWidth,frmHeight);
Listing 18. Graph06.java.
if(args == null){
setTitle("Graph06, " +
"Copyright 2002, " +
"Richard G. Baldwin");
}else{
setTitle("Graph06/" + args +
" Copyright 2002, " +
"R. G. Baldwin");
}//end else
setVisible(true);
}//end constructor
//---------------------------------//
}//end actionPerformed
//---------------------------------//
int x = getTheX(xVal+xCalcInc/2);
g.drawLine(oldX,yVal,x,yVal);
g.drawLine(getTheX(0.0),
getTheY(yMin),
getTheX(0.0),
getTheY(yMax));
//---------------------------------//
}//end xTics
//---------------------------------//
}//end yTics
//---------------------------------//
/* File Dsp028.java
Copyright 2004, R.G.Baldwin
Rev 5/14/04
400
0
0.0
1.0
Listing 19. Dsp028.java.
5
0.1
0.2
0.3
0.4
0.45
60
70
80
90
100
public Dsp028(){//constructor
}//end getParameters
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01. The plotting
// program pulls the data values to be plotted
// by calling these methods.
public int getNmbr(){
//Return number of functions to
// process. Must not exceed 5.
return numberSpectra;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude1.length-1){
return 0;
}else{
return magnitude1[index];
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 ||
Listing 19. Dsp028.java.
index > magnitude2.length-1){
return 0;
}else{
return magnitude2[index];
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude3.length-1){
return 0;
}else{
return magnitude3[index];
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude4.length-1){
return 0;
}else{
return magnitude4[index];
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude5.length-1){
return 0;
}else{
return magnitude5[index];
}//end else
}//end function
Listing 19. Dsp028.java.
//-------------------------------------------//
/* File Graph03.java
Copyright 2002, R.G.Baldwin
import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import javax.swing.*;
import javax.swing.border.*;
Listing 20. Graph03.java.
class Graph03{
public static void main(
String[] args)
throws NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
if(args.length == 1){
//pass command-line paramater
new GUI(args[0]);
}else{
//no command-line parameter given
new GUI(null);
}//end else
}// end main
}//end class Graph03 definition
//===================================//
//Constructor
GUI(String args)throws
NoSuchMethodException,
ClassNotFoundException,
InstantiationException,
IllegalAccessException{
if(args != null){
//Save for use later in the
// ActionEvent handler
this.args = args;
//Instantiate an object of the
// target class using the String
// name of the class.
data = (GraphIntfc01)
Class.forName(args).
newInstance();
}else{
//Instantiate an object of the
// test class named junk.
data = new junk();
}//end else
pan1.add(new JLabel("xMax"));
pan1.add(xMaxTxt);
pan2.add(new JLabel("yMin"));
pan2.add(yMinTxt);
Listing 20. Graph03.java.
pan3.add(new JLabel("yMax"));
pan3.add(yMaxTxt);
pan4.add(new JLabel("xTicInt"));
pan4.add(xTicIntTxt);
pan5.add(new JLabel("yTicInt"));
pan5.add(yTicIntTxt);
pan6.add(new JLabel("xCalcInc"));
pan6.add(xCalcIncTxt);
setBounds(0,0,frmWidth,frmHeight);
if(args == null){
setTitle("Graph03, " +
"Copyright 2002, " +
"Richard G. Baldwin");
}else{
setTitle("Graph03/" + args +
" Copyright 2002, " +
"R. G. Baldwin");
}//end else
setVisible(true);
}//end constructor
//---------------------------------//
Listing 20. Graph03.java.
//This event handler is registered
// on the JButton to cause the
// functions to be replotted.
public void actionPerformed(
ActionEvent evt){
//Re-instantiate the object that
// provides the data
try{
if(args != null){
data = (GraphIntfc01)Class.
forName(args).newInstance();
}else{
data = new junk();
}//end else
}catch(Exception e){
//Known to be safe at this point.
// Otherwise would have aborted
// earlier.
}//end catch
}//end actionPerformed
//---------------------------------//
g.drawLine(getTheX(0.0),
getTheY(yMin),
getTheX(0.0),
getTheY(yMax));
//---------------------------------//
}//end yTics
//---------------------------------//
/*File ForwardRealToComplex01.java
Copyright 2004, R.G.Baldwin
Rev 5/14/04
/* File Dsp030.java
Copyright 2004, R.G.Baldwin
Rev 5/14/04
128.0
Listing 22. Dsp030.java.
5
0.1
0.2
0.3
0.4
0.45
60
70
80
90
100
public Dsp030(){//constructor
}//end getParameters
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01. The plotting
// program pulls the data values to be plotted
// by calling these methods.
public int getNmbr(){
//Return number of functions to
Listing 22. Dsp030.java.
// process. Must not exceed 5.
return numberSpectra;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude1.length-1){
return 0;
}else{
return magnitude1[index];
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude2.length-1){
return 0;
}else{
return magnitude2[index];
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude3.length-1){
return 0;
}else{
return magnitude3[index];
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
Listing 22. Dsp030.java.
if(index < 0 ||
index > magnitude4.length-1){
return 0;
}else{
return magnitude4[index];
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude5.length-1){
return 0;
}else{
return magnitude5[index];
}//end else
}//end function
//-------------------------------------------//
/*File ForwardRealToComplexFFT01.java
Copyright 2004, R.G.Baldwin
Rev 5/14/04
if(imagOut[cnt] == 0.0
&& realOut[cnt] == 0.0){
angleOut[cnt] = 0.0;
Listing 23. ForwardRealToComplexFFT01.java.
}//end if
else{
angleOut[cnt] = Math.atan(
imagOut[cnt]/realOut[cnt])*180.0/pi;
}//end else
int right = 0;
for (int left = spectraCnt;
left < len;left += stepSize){
right = left + maxSpectraForStage;
double tempReal =
realCorrection*real[right]
- imagCorrection*imag[right];
double tempImag =
realCorrection*imag[right]
+ imagCorrection*real[right];
real[right] = real[left]-tempReal;
imag[right] = imag[left]-tempImag;
real[left] += tempReal;
imag[left] += tempImag;
}//end for loop
}//end for loop for individual spectra
maxSpectraForStage = stepSize;
}//end for loop for stages
}//end complexToComplex method
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it possible
for you to purchase a pre-printed version of the PDF file, you should be
aware that some of the HTML elements in this module may not translate well
into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales nor
do I know who does receive compensation. If you purchase such a book,
please be aware that it is a copy of a module that is freely available on
cnx.org and that it was made and published without my prior knowledge.
Affiliation ::: I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Java1483-Spectrum Analysis using Java, Frequency Resolution versus Data
Length
Baldwin provides the code and explains the requirements for using spectral
analysis to resolve spectral peaks for pulses containing closely spaced
truncated sinusoids.
Table of contents
Table of contents
Preface
Viewing tip
Figures
Listings
Preview
Discussion and sample code
Description
Uses a DFT algorithm
Beginning of the class named Dsp031
Perform the spectral analysis
Preface
The how and the why of spectral analysis
A previous module titled Fun with Java, How and Why Spectral Analysis
Works explained some of the fundamentals regarding spectral analysis. An
understanding of that module is a prerequisite to an understanding of this
module.
Another previous module titled Spectrum Analysis using Java, Sampling
Frequency, Folding Frequency, and the FFT Algorithm presented and
explained several Java programs for doing spectral analysis. In that module, I
used a DFT program to illustrate several aspects of spectral analysis that
center around the sampling frequency and the Nyquist folding frequency.
I also used and briefly explained two different plotting programs that were
originally explained in the earlier module titled Plotting Engineering and
Scientific Data using Java .
In this module I will use similar programs to explain and illustrate the manner
in which spectral frequency resolution behaves with respect to data length.
A hypothetical situation
You are aware that the enemy submarine contains a device that operates
occasionally in short bursts. You are also aware that this device contains two
rotating machines that rotate at almost but not quite the same speed.
During an operating burst of the device, each of the two machines contained
in the device will emit acoustic energy that may appear as a peak in your
spectral analysis output. (Note that I said, "may appear" and did not say, "will
appear.") If you can identify the two peaks, you can conclusively identify the
acoustic source as an enemy submarine.
How long must the operating bursts of this device be in order for you to
resolve the peaks and identify the enemy submarine under ideal conditions?
That is the question that I will attempt to answer in this module by teaching
you about the relationship between frequency resolution and data length.
Viewing tip
I recommend that you open another copy of this module in a separate browser
window and use the following links to easily find and view the Figures and
Listings while you are reading about them.
Figures
Listings
Preview
Before I get into the technical details, here is a preview of the programs and
their purposes that I will present and explain in this module:
In addition, I will use the following programs that I explained in the module
titled Spectrum Analysis using Java, Sampling Frequency, Folding Frequency,
and the FFT Algorithm .
Let's begin by looking at the time series data that will be used as input to the
first spectral analysis experiment. Figure 1 shows five pulses in the time
domain. Figure 2 and Figure 3 show the result of performing a spectral
analysis on each of these pulses.
If you examine Figure 1 carefully, you will see that each pulse is twice as long
as the pulse above it. (There is a tick mark on the horizontal axes every
twenty-five samples.) The bottom pulse is 400 samples long while the top
pulse is 25 samples long.
Truncated sinusoids
Each pulse consists of a cosine wave that has been truncated at a different
length. The frequency of the cosine wave is the same for every pulse. As you
will see when we examine the code, the frequency of the cosine wave is
0.0625 times the sampling frequency. If you do the arithmetic, you will
conclude that this results in 16 samples per cycle of the cosine wave.
In all five cases, the length of the time series upon which spectral analysis will
be performed is 400 samples. For those four cases where the length of the
pulse is less than 400 samples, the remaining samples in the time series have a
value of zero.
The plots in Figure 1 were produced using the program named Graph03 .
Other plots in this module will be produced using the program named
Graph06 . I explained those programs in earlier modules, and I provided the
source code for both programs in the previous module titled Spectrum
Analysis using Java, Sampling Frequency, Folding Frequency, and the FFT
Algorithm . Therefore, I won't repeat those explanations or provide the source
code for those programs in this module.
The program named Dsp031a creates and displays five separate time series,
each 400 samples in length. Each time series contains a pulse and the pulses
are different lengths.
Each pulse consists of a truncated sinusoid. The frequency of the sinusoid for
each of the pulses is the same.
25 samples
50 samples
100 samples
200 samples
400 samples
Beginning of the class named Dsp031a
The beginning of the class, along with the declaration and initialization of
several variables is shown in Listing 1 . The names of the variables along with
the embedded comments should make the code self explanatory.
Listing 2 shows the constructor, which creates the raw sinusoidal data and
stores that data in the array objects created in Listing 1 .
(Recall that all element values in the array objects are initialized
with a value of zero. Therefore, the code in Listing 2 only needs to
store the non-zero values in the array objects.)
public Dsp031a(){//constructor
}//end constructor
The code in the conditional clause of each of the for loops in Listing 2
controls the length of each of the sinusoidal pulses.
As you can see in Listing 1 , the class implements the interface named
GraphIntfc01 . I introduced this interface in the earlier module titled Plotting
Engineering and Scientific Data using Java and also discussed it in the
previous module titled Spectrum Analysis using Java, Sampling Frequency,
Folding Frequency, and the FFT Algorithm .
The remaining code for the class named Dsp031a consists of the methods
necessary to satisfy the interface. These methods are called by the plotting
programs named Graph03 and Graph06 to obtain and plot the data returned
by the methods. As implemented in Dsp031a , these interface methods return
the values stored in the array objects referred to by data1 through data5 .
Thus, the values stored in those array objects are plotted in Figure 1 .
Figure 2 shows the result of using the program named Dsp031 to perform a
spectral analysis on each of the five pulses shown in Figure 1 . These results
were plotted using the program named Graph06 . With this plotting program,
each data value is plotted as a vertical bar. However, in this case, the sides of
each of the bars are so close together that the area under the spectral curve
appears to be solid black.
(When you run this program, you can expand the display to full
screen and see the individual vertical bars. However, I can't do that
and maintain the narrow publication format required for this
module.)
Before I get into the interpretation, I need to point out that I normalized the
data plotted in Figure 2 to cause each spectral peak to have approximately the
same value. Otherwise, the spectral analysis result values for the short pulses
would have been too small to be visible in this plotting format.
Therefore, the fact that the area under the curve in the top plot is greater than
the area under the curve in the bottom plot doesn't indicate that the first pulse
contains more energy than the last pulse. It simply means that I normalized
the data for best results in plotting.
Spectrum of an ideal sinusoid
That having been said, different people will probably interpret these results in
different ways. Let's begin by stating that the theoretical spectrum for a
sinusoid of infinite length in the absence of noise is a single vertical line
having zero width and infinite height.
Two viewpoints
There are at least two ways to think of the pulses shown in Figure 1 .
The way that you interpret the results shown in Figure 2 depends on your
viewpoint regarding the pulses.
If your viewpoint is that each pulse is a signal having a definite planned start
and stop time, then the widths and the shape of each of the peaks describes the
full range of frequency components required to physically generate such a
pulse. This is the viewpoint that is consistent with the hypothetical situation
involving a device on a submarine that I described earlier in this module.
A simplified hypothetical situation
Assume for the moment that the hypothetical device on the submarine
contains only one rotating machine and that this device is turned on and off
occasionally in short bursts. Because of the rotating machine, when the device
is turned on, it will emit acoustic energy whose frequency matches the
rotating speed of the machine.
Assume that you have a recording window of 400 samples, and that you are
able to record five such bursts within each of five separate recording
windows. Further assume that the lengths of the individual bursts match the
time periods indicated by the pulses in Figure 1 .
Figure 3 shows the left one-fourth of the spectral results from Figure 2 plotted
in the same horizontal space. In other words, Figure 3 discards the upper
three-fourths of the spectral results from Figure 2 and shows only the lower
one-fourth of the spectral results on an expanded scale. Figure 3 also provides
tick marks that make it convenient to perform measurements on the plots.
Also, whereas Figure 2 was plotted using the program named Graph06 ,
Figure 3 was plotted using the program named Graph03 . Thus, Figure 3 uses
a different plotting format than Figure 2
Figure 3. Expanded spectral analyses of five pulses.
The curves in Figure 3 are spread out to the point that we can pick some
approximate numeric values off the plot, and from this, we can draw a very
significant conclusion.
If you reduce the length of the pulse by a factor of two, you must double the
bandwidth of a transmission system designed to reliably transmit a pulse of
that length.
This will also be an important conclusion regarding our ability to separate and
identify the two spectral peaks in the burst of acoustic energy described in our
original hypothetical situation .
The generation of the signals and the spectral analysis for the results presented
in Figure 2 and Figure 3 were performed using the program named Dsp031 .
A complete listing of the program is shown in Listing 10 near the end of the
module.
Description
This program performs spectral analyses on five separate time series, each
400 samples in length.
Each time series contains a pulse and the pulses are different lengths. (The
lengths of the individual pulses match that shown in Figure 1 .) Each pulse
consists of a truncated sinusoid. The frequency of the sinusoid for each pulse
is the same.
25 samples
50 samples
100 samples
200 samples
400 samples
If this sounds familiar, it is because the pulses are identical to those displayed
in Figure 1 and discussed under Dsp031a above.
The spectral analysis process uses a DFT algorithm and computes the
amplitude of the spectral energy at 400 equally spaced frequencies between
zero and the folding frequency.
The results of the spectral analysis are multiplied by the reciprocal of the
lengths of the individual pulses to normalize all five plots to the same peak
value. Otherwise, the results for the short pulses would be too small to see on
the plots.
Will discuss in fragments
The code in Listing 3 declares and initializes some variables and creates the
array objects that will contain the sinusoidal pulses.
In addition, the code in Listing 3 declares reference variables that will be used
to refer to array objects containing results of the spectral analysis process that
are not used in this program.
Given the names of the variables, the comments, and what you learned in the
earlier modules, the code in Listing 3 should be self explanatory.
The constructor
The constructor begins in Listing 4 . The code in Listing 4 is identical to that
shown earlier in Listing 2 . This code generates the five sinusoidal pulses and
stores the data representing those pulses in the arrays referred to by data1
through data5 . So far, except for the declaration of some extra variables, this
program isn't much different from the program named Dsp031a discussed
earlier in this module.
public Dsp031(){//constructor
The remainder of the constructor is shown in Listing 5 . This code calls the
transform method of the ForwardRealToComplex01 class five times in
succession to perform the spectral analysis on each of the five pulses shown in
Figure 1 .
}//end constructor
Each time the transform method is called, it computes the magnitude spectrum
for the incoming data and saves it in the output array.
(Note that the real, imag, and angle arrays are not used later, so
they are discarded each time a new spectral analysis is performed.)
The Dsp031 class also implements the interface named GraphIntfc01 . The
remaining code in the program consists of the methods required to satisfy that
interface. Except for the identification of the arrays from which the methods
extract data to be returned for plotting, these methods are identical to those
defined in the earlier class named Dsp031a . Therefore, I won't discuss them
further.
So far, the main things that we have learned is that shorter pulses require
greater bandwidth, and the bandwidth required to faithfully represent a
truncated sinusoidal pulse is the reciprocal of the length of the pulse.
Now we will look at the issues involved in using spectral analysis to separate
and identify the frequencies of two closely-spaced spectral peaks for a pulse
composed of the sum of two sinusoids. Once again, we will begin by looking
at some results and then discuss the code that produced those results.
The five pulses that we will be working with in this example are shown in
Figure 4 . As you can see, these pulses are a little more ugly than the pulses
shown in Figure 1 . As you can also see, as was the case in Figure 1 , each
pulse appears to be a shorter or longer version of the other pulses in terms of
its waveform.
Produced by Dsp032a
The plots in Figure 4 were produced by the program named Dsp032a , which
I will briefly discuss later. (A complete listing of the program is shown in
Listing 11 near the end of the module.) This program creates and displays
pulses identical to those processed by the program named Dsp032 , which I
will also briefly discuss later. (A complete listing of the program named
Dsp32 is presented in Listing 12 .)
Each pulse consists of the sum of two sinusoids at closely spaced frequencies.
The frequencies of the two sinusoids for all pulses are the same.
25 samples
50 samples
100 samples
200 samples
400 samples
The only new code in this program is the code in the constructor that creates
the pulses and stores them in the data arrays. This code consists of five
separate for loops, one for each pulse. The code for the first for loop, which is
typical of the five, is shown in Listing 6 .
public Dsp032a(){//constructor
}//end constructor
As you can see from Listing 6 , the values that make up the pulse are
produced by adding together the values of two different cosine functions
having different frequencies. The values for freq1 and freq2 are as described
above.
Each of the peaks in the third, fourth, and fifth plots in Figure 5 corresponds
to the frequency of one of the two sinusoids that were added together to
produce the pulses shown in Figure 4 .
Clearly for the ideal condition of recording the bursts in the total absence of
noise, you cannot resolve the peaks from the top two plots in Figure 5 . For
those two pulses, the spectral peaks simply merge together to form a single
broad peak. Therefore, for this amount of separation between the frequencies
of the two sinusoids, the lengths of the first two pulses in Figure 4 are
insufficient to allow for separation and identification of the separate peaks.
We must be in luck
We seem to have the problem bracketed. (Were we really lucky, or did I plan it
this way?) Under the ideal conditions of this experiment, the peaks are
separable in the middle plot of Figure 5 . Thus, for the amount of separation
between the frequencies of the two sinusoids, the length of the third pulse is
Figure 4 is sufficient to allow for separation and identification of the separate
peaks.
If you were to add a nominal amount of wide-band noise to the mix, it would
become more difficult to resolve the peaks for the bottom three plots in Figure
5 because the peaks would be growing out of a bed of weeds.
(If you add enough wide-band noise, you couldn't resolve the peaks
using any of the plots, because the peaks would be completely "lost
in the noise.")
Since we have concluded that the middle pulse in Figure 4 is sufficiently long
to allow us to resolve the two peaks, let's see what we can learn from the
parameters that describe that pulse.
What about the frequency separation of the two sinusoids? Recall that the
frequency of one sinusoid is (0.0625 - 2.0/len) times the sampling frequency,
where len is the length of the time series containing the pulse. The frequency
of the other sinusoid is (0.0625 + 2.0/len) times the sampling frequency.
Thus, total separation between the two frequencies is 4/len, or 4/400. Dividing
through by 4 we see that the separation between the two frequencies is 1/100.
Eureka, we have found it
For the third pulse, the frequency separation is the reciprocal of the length of
the pulse . Also, the length of the third pulse is barely sufficient to allow for
separation and identification of the two peaks in the spectrum.
Thus, the two spectral peaks are separable in the absence of noise if the
frequency separation is the reciprocal of the pulse length. (That is too good to
be a coincidence. I must have planned that way.)
There is no single answer to the question "how long must the operating bursts
of this device be in order for you to resolve the peaks and identify the enemy
submarine under ideal conditions?"
The answer depends on the frequency separation. The general answer is that
the length of the bursts must be at least as long as the reciprocal of the
frequency separation for the two sinusoids. If the separation is large, the pulse
length may be short. If the separation is small, the pulse length must be long.
As I indicated earlier, the plots shown in Figure 5 were the result of running
the program named Dsp032 and displaying the data with the program named
Graph03 .
The only thing that is new in this program is the code that generates the five
pulses and saves them in their respective data arrays. Even that code is not
really new, because it is identical to the code shown in Listing 6 . Therefore, I
won't discuss this program further in this module.
As you can surmise from the conclusions reached above, in order to be able to
resolve the two peaks in the spectrum, you can either keep the pulse length the
same and increase the frequency separation, or you can keep the frequency
separation the same and increase the pulse length.
Let's examine an example where we keep the pulse lengths the same as before
and adjust the frequency separation between the two sinusoids to make it
barely possible to resolve the peaks for each of the five pulses.
We will need to increase the frequency separation for the first two pulses, and
we can decrease the frequency separation for the fourth and fifth pulses. We
will leave the frequency separation the same as before for the third pulse since
it already seems to have the optimum relationship between pulse length and
frequency separation.
Unlike in the previous two cases shown in Figure 1 and Figure 4 , each of
these pulses has a different shape from the others. In other words, in the
previous two cases, each pulse simply looked like a longer or shorter version
of the other pulses. That is not the case in this example.
(Note however that the third pulse in Figure 6 looks just like the
third pulse in Figure 4 . They were created using the same
parameters. However, none of the other pulses in Figure 6 look like
the corresponding pulses in Figure 4 , and none of the pulses in
Figure 6 look like the pulses in Figure 1 .)
Spectral analysis results
Figure 7 shows the result of performing spectral analysis on the five time
series containing the pulses shown in Figure 6 .
When we examine the code, you will see that the frequency separation for the
first two pulses has been increased to the reciprocal of the pulse length in each
case. This results in the two peaks in the spectrum for each of the first two
pulses being resolvable in Figure 7 .
The spectrum for the third pulse shown in Figure 7 is almost identical to the
spectrum for the third pulse shown in Figure 5 . The only difference is that I
had to decrease the vertical scaling on all of the plots in Figure 5 to keep the
peak in the top plot within the bounds of the plot.
When we examine the code, you will also see that the frequency separation
for the last two pulses has been decreased to the reciprocal of the pulse length
in each case. This results in the two peaks in the spectrum for each of the last
two pulses being closer than before in Figure 7 .
The peaks in the bottom two plots in Figure 7 appear to be resolvable, but we
can't be absolutely certain because they are so close together, particularly for
the last plot.
(If you expand the Frame to full screen when you run this program,
you will see that the two peaks are resolvable, but I can't do that
and stay within this narrow publication format.)
Figure 8 shows that the two peaks are barely resolvable for all five of the
pulses shown in Figure 6 .
The plots in Figure 7 and Figure 8 were produced by running the program
named Dsp033 and plotting the results with the program named Graph03 .
This program is the same as Dsp032 except that the separation between the
frequencies of the two sinusoids is the reciprocal of the length of the pulse in
each case.
The program performs spectral analysis on five separate time series, each 400
samples in length. Each time series contains a pulse and the pulses are
different lengths.
Each pulse consists of the sum of two sinusoids at closely spaced frequencies.
The frequencies of the two sinusoids are equidistant from a center frequency
of 0.0625 times the sampling frequency. The total separation between the
frequencies of the two sinusoids is the reciprocal of the length of the pulse.
25 samples
50 samples
100 samples
200 samples
400 samples
The spectral analysis computes the spectra at 400 equally spaced frequencies
between zero and the folding frequency (one-half the sampling frequency) .
The results of the spectral analysis are multiplied by the reciprocal of the
lengths of the individual pulses to normalize the five plots. Otherwise, the
results for the short pulses would be too small to see on the plots.
The code in Listing 8 uses those frequency values to create the data for the
pulses and to store that data in the arrays used to hold the pulses.
Other than the code shown in Listing 7 and Listing 8 , the program named
Dsp033 is the same as the programs that were previously explained, and I
won't discuss it further.
Create more complex experiments. For example, you could create pulses
containing three or more sinusoids at closely spaced frequencies, and you
could cause the amplitudes of the sinusoids to be different. See what it takes
to cause the peaks in the spectra of those pulses to be separable and
identifiable.
If you really want to get fancy, you could create a pulse consisting of a
sinusoid whose frequency changes with time from the beginning to the end of
the pulse. (A pulse of this type is often referred to as a frequency modulated
sweep signal.) See what you can conclude from doing spectral analysis on a
pulse of this type.
Try using the random number generator of the Math class to add some
random noise to every value in the 400-sample time series. See what this does
to your spectral analysis results.
Move the center frequency up and down the frequency axis. See if you can
explain what happens as the center frequency approaches zero and as the
center frequency approaches the folding frequency.
Summary
This program provides the code for three spectral analysis experiments of
increasing complexity.
The third experiment also performs spectral analyses on five pulses consisting
of the sum of two truncated sinusoids having closely spaced frequencies. In
this case, the frequency separation for each pulse is the reciprocal of the
length of the pulse. The results of the spectral analysis reinforce the
conclusions drawn in the second experiment.
What's next?
So far, the modules in this series have ignored the complex nature of the
results of spectral analysis. The complex results have been converted into real
results by computing the square root of the sum of the squares of the real and
imaginary parts.
The next module in the series will meet the issue of complex spectral results
head on and will explain the concept of phase angle. In addition, the module
will explain the behavior of the phase angle with respect to time shifts in the
input time series.
Listing 9. Dsp031a.java.
/* File Dsp031a.java
Copyright 2004, R.G.Baldwin
Revised 5/17/2004
public Dsp031a(){//constructor
}//end constructor
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of functions to process.
// Must not exceed 5.
return 5;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data1.length-1){
return 0;
}else{
//Scale the amplitude of the pulses to make
// them compatible with the default
// plotting amplitude of 100.0.
return data1[index]*90.0/amp;
}//end else
}//end function
Listing 9. Dsp031a.java.
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data2.length-1){
return 0;
}else{
return data2[index]*90.0/amp;
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data3.length-1){
return 0;
}else{
return data3[index]*90.0/amp;
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data4.length-1){
return 0;
}else{
return data4[index]*90.0/amp;
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data5.length-1){
return 0;
}else{
return data5[index]*90.0/amp;
}//end else
}//end function
Listing 9. Dsp031a.java.
/* File Dsp031.java
Copyright 2004, R.G.Baldwin
Revised 5/17/2004
25 samples
50 samples
Listing 10. Dsp031.java.
100 samples
200 samples
400 samples
public Dsp031(){//constructor
}//end constructor
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of functions to process.
// Must not exceed 5.
return 5;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag1.length-1){
return 0;
}else{
//Scale the magnitude data by the
// reciprocal of the length of the sinusoid
// to normalize the five plots to the same
// peak value.
Listing 10. Dsp031.java.
return mag1[index]*16.0;
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag2.length-1){
return 0;
}else{
return mag2[index]*8.0;
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag3.length-1){
return 0;
}else{
return mag3[index]*4.0;
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag4.length-1){
return 0;
}else{
return mag4[index]*2.0;
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag5.length-1){
return 0;
}else{
Listing 10. Dsp031.java.
return mag5[index]*1.0;
}//end else
}//end function
/* File Dsp032a.java
Copyright 2004, R.G.Baldwin
Revised 5/17/2004
25 samples
50 samples
100 samples
200 samples
400 samples
public Dsp032a(){//constructor
}//end constructor
//-------------------------------------------//
//The following six methods are required by the
Listing 11. Dsp032a.java.
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of functions to process.
// Must not exceed 5.
return 5;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data1.length-1){
return 0;
}else{
//Scale the amplitude of the pulses to make
// them compatible with the default
// plotting amplitude of 100.0.
return data1[index]*40.0/amp;
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data2.length-1){
return 0;
}else{
return data2[index]*40.0/amp;
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data3.length-1){
return 0;
}else{
return data3[index]*40.0/amp;
}//end else
}//end function
Listing 11. Dsp032a.java.
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data4.length-1){
return 0;
}else{
return data4[index]*40.0/amp;
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data5.length-1){
return 0;
}else{
return data5[index]*40.0/amp;
}//end else
}//end function
/* File Dsp032.java
Copyright 2004, R.G.Baldwin
Revised 5/17/2004
25 samples
50 samples
100 samples
200 samples
400 samples
public Dsp032(){//constructor
}//end constructor
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of functions to process.
// Must not exceed 5.
return 5;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag1.length-1){
return 0;
}else{
//Scale the magnitude data by the
// reciprocal of the length of the sinusoid
// to normalize the five plots to the same
// peak value.
return mag1[index]*16.0;
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
Listing 12. File Dsp032.java.
int index = (int)Math.round(x);
if(index < 0 || index > mag2.length-1){
return 0;
}else{
return mag2[index]*8.0;
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag3.length-1){
return 0;
}else{
return mag3[index]*4.0;
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag4.length-1){
return 0;
}else{
return mag4[index]*2.0;
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag5.length-1){
return 0;
}else{
return mag5[index]*1.0;
}//end else
}//end function
/* File Dsp033a.java
Copyright 2004, R.G.Baldwin
Revised 5/17/2004
25 samples
50 samples
100 samples
Listing 13. Dsp033a.java.
200 samples
400 samples
public Dsp033a(){//constructor
}//end constructor
Listing 13. Dsp033a.java.
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of functions to process.
// Must not exceed 5.
return 5;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data1.length-1){
return 0;
}else{
//Scale the amplitude of the pulses to make
// them compatible with the default
// plotting amplitude of 100.0.
return data1[index]*40.0/amp;
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data2.length-1){
return 0;
}else{
return data2[index]*40.0/amp;
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data3.length-1){
return 0;
}else{
return data3[index]*40.0/amp;
Listing 13. Dsp033a.java.
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data4.length-1){
return 0;
}else{
return data4[index]*40.0/amp;
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data5.length-1){
return 0;
}else{
return data5[index]*40.0/amp;
}//end else
}//end function
/* File Dsp033.java
Copyright 2004, R.G.Baldwin
Revised 5/17/2004
Listing 14. File Dsp033.java.
Same as Dsp032 except that the separation between
the frequencies of the two sinusoids is the
reciprocal of the length of the pulse.
25 samples
50 samples
100 samples
200 samples
400 samples
public Dsp033(){//constructor
}//end constructor
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of functions to process.
// Must not exceed 5.
Listing 14. File Dsp033.java.
return 5;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag1.length-1){
return 0;
}else{
//Scale the magnitude data by the
// reciprocal of the length of the sinusoid
// to normalize the five plots to the same
// peak value.
return mag1[index]*16.0;
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag2.length-1){
return 0;
}else{
return mag2[index]*8.0;
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag3.length-1){
return 0;
}else{
return mag3[index]*4.0;
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
Listing 14. File Dsp033.java.
if(index < 0 || index > mag4.length-1){
return 0;
}else{
return mag4[index]*2.0;
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag5.length-1){
return 0;
}else{
return mag5[index]*1.0;
}//end else
}//end function
Miscellaneous
This section contains a variety of miscellaneous information.
Baldwin provides the code and explains the requirements for using spectral
analysis to resolve spectral peaks for pulses containing closely spaced
truncated sinusoids.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it possible
for you to purchase a pre-printed version of the PDF file, you should be
aware that some of the HTML elements in this module may not translate well
into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales nor
do I know who does receive compensation. If you purchase such a book,
please be aware that it is a copy of a module that is freely available on
cnx.org and that it was made and published without my prior knowledge.
Affiliation : I am a professor of Computer Information Technology at Austin
Community College in Austin, TX.
-end-
Java1484-Spectrum Analysis using Java, Complex Spectrum and Phase Angle
Baldwin discusses the complex spectrum and explains the relationship
between the phase angle and shifts in the time domain.
Table of contents
Table of contents
Preface
Viewing tip
Figures
Listings
Preview
Discussion and sample code
Preface
Spectral analysis
A previous module titled Fun with Java, How and Why Spectral Analysis
Works explained some of the fundamentals regarding spectral analysis.
In this module, I will deal with issues involving the complex spectrum, the
phase angle, and shifts in the time domain.
Viewing tip
I recommend that you open another copy of this module in a separate browser
window and use the following links to easily find and view the Figures and
Listings while you are reading about them.
Figures
Listings
Preview
In this module, I will present and explain a program named Dsp034 . This
program will be used to illustrate the behavior of the complex spectrum and
the phase angle for several different scenarios.
In addition, I will use the following programs that I explained in the module
titled Spectrum Analysis using Java, Sampling Frequency, Folding Frequency,
and the FFT Algorithm .
When the data is plotted (see Figure 2 ) using the programs Graph03 or
Graph06 , the order of the plots from top to bottom in the display is:
The pulse
The amplitude spectrum
The real spectrum
The imaginary spectrum
The phase angle in degrees
The number of sample values for the pulse must match the value for the pulse
length.
Figure 1 provides a set of sample parameter values that can be used to test the
program. This sample data describes a triangular pulse. Be careful when you
create the file containing these values. Don't allow blank lines at the end of
the data in the file.
400
11
0
0.0
0.5
0
0
0
45
90
135
90
45
0
0
0
The plotting program that is used to plot the output data from this program
requires that the program implement GraphIntfc01 .
For example, the plotting program named Graph03 can be used to plot the
data produced by this program. When it is used, the following should be
entered at the command-line prompt:
java Graph03 Dsp034
Spectral analysis
The method named transform does not implement an FFT algorithm. Rather,
it implements a DFT algorithm, which is more general than, but much slower
than an FFT algorithm. (See the program named Dsp030 in the module titled
Spectrum Analysis using Java, Sampling Frequency, Folding Frequency, and
the FFT Algorithm for the use of an FFT algorithm.)
Results
Before getting into the technical details of the program, let's take a look at
some results. Figure 2 shows the results produced by using the program
named Graph06 to plot the output for the program named Dsp034 using
default parameters.
(The file named Dsp034.txt did not exist in the current directory
when the results shown in Figure 2 were generated.)
Figure 2. Spectral analysis of a damped pulse.
First consider the input pulse shown in the top plot of Figure 2 . This is the
sort of pulse that you would get if you hung a mass on a spring, gave it a swift
downward kick, and then allowed the mass to come to rest in an unimpeded
fashion.
The horizontal axis represents time moving from left to right. The values
above and below the axis represent the position of the mass over time relative
to its position at rest.
Potential energy in the spring
When the mass is at the most extreme positions and getting ready to reverse
directions, the spring is extended. Thus, potential energy is stored in the
spring. At these points in time, there is no kinetic energy stored in the mass.
As the mass goes through the rest position heading towards the other side, the
spring is no longer extended, and there is no potential energy stored in the
spring. However, at that point in time, the mass is moving causing it to have
kinetic energy.
An exchange of energy
Now that our physics lesson for the day is over, let's get back to programming
and digital signal processing.
Assume that you use an analog to digital converter to capture the position of
the mass at a set of uniformly spaced intervals in time and then you plot those
samples along a time axis. You should see something resembling the top plot
in Figure 2 .
In the module titled Fun with Java, How and Why Spectral Analysis Works , I
explained that you could compute the Fourier transform of an input time
series at any given frequency F by evaluating the first two expressions in
Figure 3 . (The notation used in Figure 3 was also explained in that module.)
Real(F) = S(n=0,N-1)[x(n)*cos(2Pi*F*n)]
Imag(F) = S(n=0,N-1)[x(n)*sin(2Pi*F*n)]
I also explained that the Fourier transform of the input time series at that
frequency can be viewed as a complex number having a real part and an
imaginary part as in the third expression in Figure 3 .
(In this display format, which was produced by the program named
Graph06, each sample value is represented by a vertical bar whose
height is proportional to the value of the sample.)
These four plots show the values of the Fourier transform output at a set of
uniformly spaced frequencies ranging from zero to 0.25 times the sampling
frequency.
The second plot from the top in Figure 2 shows the value of the amplitude
spectrum. This is the Fourier transform output that we have been using in the
previous modules in this series.
As you can see, the amplitude spectrum peaks at a frequency equal to 0.0625
times the sampling frequency. The reason for this will become clear when we
examine the code that produced the pulse shown in the first plot.
The real part of the transform is shown in the third plot and the imaginary part
of the transform is shown in the fourth plot. (I believe that this is the first time
that I have presented the real and imaginary parts of the spectrum in this
series of modules.)
The phase angle in degrees
The phase angle in degrees is shown in the bottom plot. There are a variety of
different ways to display phase angles. This program displays the phase angle
as values that range from -180 degrees to +180 degrees.
Basically, the phase angle is the angle that you get when you compute the arc
tangent of the ratio of the imaginary part to the real part of the complex
spectrum at a particular frequency. However, beyond computing the arc
tangent, you must do some additional work to take the quadrant into account.
To begin with, you should ignore the result of phase angle computations at
those frequencies for which there is insignificant energy. It is always possible
to form a ratio of the values of the real and imaginary parts of the complex
Fourier transform at any frequency. However, if the real and imaginary values
produced by the Fourier transform at that frequency are both very small, the
phase angle resulting from that ratio is of no practical significance. In fact, the
angle can be corrupted by arithmetic errors resulting from performing
arithmetic on very small values.
Many combinations
The phase angle produced by performing a Fourier transform on a pulse of a
given waveform is not unique. There are an infinite number of combinations
of real and imaginary parts that can result from performing a Fourier
transform on a given waveform, depending on how you define the origin of
time. This means that there are also an infinite number of phase angle curves
that can be produced from the ratio of those real and imaginary parts. I will
explain this in more detail later using simpler pulses.
In the case shown in Figure 2 , the frequency band of primary interest lies
approximately between the first and the third tick marks. Most of the energy
can be seen to lie between those limits on the basis of the amplitude plot.
The phase angle curve goes from a little more than zero degrees to a little less
than 180 degrees across this frequency interval. However, it is significant to
note that the phase angle is not linear across this frequency interval. Rather
the shape of the curve is more like an elongated S sloping to the right.
What is the significance of the nonlinear phase angle? If this plot represented
the frequency response of your audio system, the existence of the nonlinear
phase angle would be bad news. In particular, it would mean that the system
would introduce phase distortion into your favorite music.
The code in the transform method that computes the phase angle for a
particular frequency is shown in Listing 1 . At this point in the execution of
the transform method, the values of the real part (real) and the imaginary
part (imag) of the Fourier transform at a particular frequency have been
computed. Those values are used to compute the phase angle at that
frequency.
If both the real and imaginary parts are not zero, then the ratio of the
imaginary value to the real value is formed and passed as a parameter to the
atan method of the Math class.
The atan method returns the angle in radians whose tangent matches the value
received as a parameter. The angle that is returned is in the range from -pi/2 to
+pi/2 (-90 degrees to +90 degrees) . The code in Listing 1 multiplies that
angle in radians by 180.0/pi to convert the angle from radians to degrees.
For example, a positive ratio can result from a positive imaginary value and a
positive real value, or from a negative imaginary value and a negative real
value. Both of these would be reported by the atan method as being between
0 and 90 degrees when in fact, the negative imaginary value and the negative
real value means that the angle is actually between -90 degrees and -180
degrees.
The second and fourth quadrants
Similarly, a negative ratio can result from a negative imaginary value and a
positive real value or from a positive imaginary value and a negative real
value. Both of these would be reported by the atan method as being between
0 and -90 degrees when in fact, the positive imaginary value and the negative
real value means that the angle is actually between 90 degrees and 180
degrees.
I will leave it as an exercise for the reader to work through the remaining code
in Listing 1 to see how this code determines the proper quadrant and adjusts
the angle appropriately, all the while maintaining the angle between -180
degrees and +180 degrees.
The beginning of the class, along with the declaration of several variables is
shown in Listing 2 .
The constructor
The constructor begins in Listing 3 . The code in the constructor either calls
the getParameters method to get the operating parameters from the file named
Dsp034.txt, or creates a set of operating parameters on the fly if that file does
not exist in the current directory. In either case, many of the variables declared
in Listing 2 are populated as a result of that action.
public Dsp034(){//constructor
scale*Math.sin(2*pi*cnt*0.0625);
}//end for loop
//End default parameters
}//end else
At this point in the process, the array referred to by the reference variable
named pulse contains a set of samples that constitutes a pulse. The code in
Listing 4 creates a data array containing the data upon which spectral analysis
will be performed and stores the pulse in that array.
(All elements in the data array other than those elements occupied
by values of the pulse have a value of zero.)
Following this, code in the constructor prints the parameters and the values of
the samples that constitute the pulse. You can view that code in Listing 6 near
the end of the module.
Finally, the code in Listing 5 creates the array objects that will receive the
results of the spectral analysis, and calls the transform method of the
ForwardRealToComplex01 class to perform the spectral analysis.
}//end constructor
Listing 5 also signals the end of the constructor. At this point, the object has
been instantiated and it's array objects have been populated with the input and
output data from the spectral analysis process. This data is ready to be handed
over to the plotting program to be plotted, as shown in Figure 2 .
The one thing that you might want to pay attention to in these methods is the
scaling that is applied to the data before it is returned. This is an attempt to
cause all of the curves to plot reasonably well within a value range of -180 to
+180. This range is dictated by the fact that this is the range of values for the
phase angle data.
Now that you understand the inner workings of the program, let's look at
some more examples, this time getting the input data from the file named
Dsp034.txt .
More examples
The simplest pulse that you can create is a single non-zero valued sample
among a bunch of zero-valued samples. This simple pulse, commonly called
an impulse in digital signal processing, is an extremely important type of
signal. It is used for a variety of purposes in testing both digital and analog
signal processing systems.
Let's examine the result of performing spectral analysis on an impulse.
The data that you see in Figure 4 is the screen output from the method named
getParameters . To orient you with this format, the second item in Figure 4
indicates that the entire length of the data upon which spectral analysis was
performed was 400 samples. All 400 samples had a value of zero except for
the values of the sample in the pulse that occurred at the beginning. In this
program, setting the total data length to 400 causes the spectral analysis to be
performed at 400 individual frequencies.
(By now, you may be suspecting that I have a particular affinity for
a data length of 400 samples. If so, you are correct. This is not a
technical affinity. Rather, it is a visual one. These figures are
formatted such that the plotted data occupies an area of the screen
containing approximately 400 pixels. By matching the plotted
points to the positions of the pixels, it is possible to avoid, or at
least minimize, the distortion that can occur when attempting to
map from sample points to pixel locations when there is a mismatch
between the two.
As you can see in Figure 4 , the length of the pulse for this experiment was 11
samples, all but one of which had a value of zero.
(In this case, I could have made the pulse length 1 but for
simplicity, I will keep it at 11 for several different experiments.)
Defining the origin of time
As you may have discovered by playing video games, we can do things with a
computer that we can't do in the real world. For example, the Fourier
transform program allows me to specify which sample I regard as
representing zero time. Samples to the left of that sample represent negative
time (history) and samples to the right of that one represent positive time (the
future) .
In this case, I specified that the first sample (sample number 0) represents zero
time. As you will see later, this has a significant impact on the distribution of
energy between the real and imaginary parts of the transform results, and as
such, has a significant impact on the phase angle.
I used the program named Graph03 to plot the output from this program, and
the results are shown in Figure 5 .
(Note that rather than plotting each sample value as a vertical bar,
Graph03 plots each sample as a dot and connects the dots with
straight line segments.)
Now take a look at the amplitude spectrum in the second plot from the top.
What you should see is a straight black line extending from zero to the folding
frequency on the right. This is because such an impulse (theoretically)
contains an equal distribution of energy at every frequency from zero to
infinity.
That is one of the things that make the impulse so useful. It is often used for
various testing purposes in both the analog world and the digital world.
Now look at the real spectrum in the second plot from the top. As you can see,
it looks exactly like the amplitude spectrum. This is because the impulse
appears at zero time. We will change this in the next experiment so that you
can see the impact of a time delay on the complex spectrum.
Moving on down the page, the imaginary part of the spectrum is a flat line
with a value of zero across the entire frequency range. Once again, this is
because the impulse appears at zero time.
Because the imaginary value is zero everywhere, the ratio of the imaginary
value to the real value is also zero everywhere. Thus, the phase angle is also
zero at all frequencies within the range.
Now we are going to introduce a one-sample time delay in the location of the
impulse relative to the time origin. We will keep the zero time reference at the
first sample and cause the impulse to appear as the second sample in the
eleven-sample sequence.
The new parameters are shown in Figure 6 . The only change is the move of
the impulse from the first sample in the eleven-sample pulse to the second
sample in the eleven-sample pulse.
The result of performing the spectral analysis on this new time series is shown
in Figure 7 .
Once again, if you strain your eyes, you may be able to see the impulse on the
left end of the top plot. It has been shifted one sample to the right relative to
that shown in Figure 5 .
The amplitude spectrum in the second plot looks exactly like it looked in
Figure 5 . That is as it should be. The spectral content of a pulse is determined
by its waveform, not by its location in time. Simply moving the impulse by
one sample into the future doesn't change its spectral content.
The real and imaginary parts of the spectrum
However, moving the impulse one sample into the future (a time delay) did
change the values of the real and imaginary parts of the complex spectrum. As
you can see from the third plot in Figure 7 , the real part of the spectrum is no
longer a replica of the amplitude spectrum. Also the imaginary part of the
spectrum in the fourth plot is no longer zero across the frequency range from
zero to the folding frequency.
However, the real and imaginary parts cannot change in arbitrary ways
relative to one another. Recall that the amplitude spectrum at each individual
frequency is the square root of the sum of the squares of the real and
imaginary parts. In order for the amplitude spectrum to stay the same, changes
to the real part of the spectrum must be accompanied by changes to the
imaginary part that will maintain that relationship.
Finally, the phase angle shown in the bottom plot is no longer zero. Rather it
is a straight line with a value of zero at zero frequency and a value of 180
degrees at the folding frequency.
An important conclusion
Once again, consider your audio system. If your audio system introduces a
phase shift across the frequency band of interest, you would probably like for
that phase shift to be linear with frequency. That will simply cause the music
to be delayed in time. In other words, all frequency components in the music
will be delayed an equal amount of time.
On the other hand, if the phase shift is not linear with frequency, some
frequencies will delayed more than other frequencies. This sometimes results
in noticeable phase distortion in your music.
Now let's move the impulse to the center of the eleven-sample pulse and
observe the result. The new parameters are shown in Figure 8 .
Figure 9 shows the result of performing a spectral analysis on the time series
containing this new time-delayed impulse.
Finally, you can probably see the impulse on the left side of the top plot in
Figure 9 without straining your eyes too much.
Although it wasn't apparent in Figure 7 , Figure 9 shows that the real part of
the spectrum takes on the shape of a cosine wave, while the imaginary part of
the spectrum takes on the shape of a sine wave as a result of the time delay of
the impulse.
The phase shift is still linear across frequency as would be expected, but the
slope is now five times greater than the slope of the phase shift in Figure 7 .
(Note that the time delay is five times greater in Figure 9 . Note also
that the plot of the phase angle wraps around from +180 degrees to
-180 degrees each time the phase angle reaches +180 degrees. This
produces the saw tooth effect shown in the bottom plot in Figure 9
.)
A boxcar pulse
The spectral analysis output for the eleven-sample boxcar pulse is shown in
Figure 11 .
The pulse itself is relatively easy to see on the leftmost end of the top plot.
Without getting into the technical details, I will simply tell you that the
location of the point where it goes to zero is related to the reciprocal of the
pulse width. If that sounds familiar, it is because we encountered similar
situations involving bandwidth in the module titled Spectrum Analysis using
Java, Frequency Resolution versus Data Length .
In fact, the shape of the amplitude spectrum is a familiar (sin x)/x curve with
the negative lobes flipped up and turned into positive lobes instead.
The phase angle is still linear with frequency although it now shows some
discontinuities at those frequencies where the amplitude spectrum touches
zero.
When working with (recorded non real-time) digital time series, it is not only
possible to physically shift pulses forward or backward in time, it is also
possible to leave the pulses where they are and redefine the underlying time
base. For the next experiment, I will leave everything else the same and
redefine the location of the origin of time. I will place the time origin at the
middle of the boxcar pulse.
The new parameters for this experiment are shown in Figure 12 . Note that the
only significant difference between Figure 12 and Figure 10 is the redefinition
of the sample that represents zero time. I redefined the time origin from
sample 0 to sample 5. This causes the boxcar pulse to be centered on zero
time. Five of the samples in the boxcar pulse occur in negative (history) time.
One sample occurs exactly at zero time. The other five samples occur in
positive (future) time .
(I did make one other change. This change was to add a tiny spike
to one of the samples near the center of the pulse. This creates a
tiny amount of wide-band energy and tends to stabilize the
computation of the phase angle. It prevents the imaginary part of
the spectrum from switching back and forth between very small
positive and negative values due to arithmetic errors.)
The output from the spectral analysis is shown in Figure 13 . The magnitude
spectrum hasn't changed. The real part of the spectrum has changed
significantly. It is now a true (sin x)/x curve with both positive and negative
lobes.
The imaginary part of the spectrum is zero or nearly zero at every frequency.
(It would be zero in the absence of arithmetic errors.)
The phase angle is zero across the entire main energy lobe of the spectrum. It
is -180 degrees in those frequency areas where the real part of the spectrum is
negative, and is zero in those frequency areas where the real part of the
spectrum is positive. There is no linear phase shift because the boxcar pulse is
centered on the time origin.
And that is probably more than you ever wanted to know about the complex
spectrum, phase angles, and time shifts. I will stop writing and leave it at that.
Create more complex experiments. For example, you could create pulses of
different lengths with complex shapes and examine the complex spectra and
phase angles for those pulses.
If you really want to get fancy, you could create a pulse consisting of a
sinusoid whose frequency changes with time from the beginning to the end of
the pulse. (A pulse of this type is often referred to as a frequency modulated
sweep signal.) See what you can conclude from doing spectral analysis on a
pulse of this type. Pay particular attention to the phase angle across the
frequency band containing most of the energy.
Summary
The default pulse for the Dsp034 program is a damped sinusoid. This is a
pulse whose shape is commonly found in mechanical and electronic systems
in the real world. The phase angle in the complex spectrum for a pulse of this
shape is nonlinear. Among other things, nonlinear phase angles introduce
phase distortion into audio systems.
The simplest pulse of all is a single impulse. A pulse of this type has an
infinite bandwidth (theoretically) and a linear phase angle. The slope of the
phase angle depends on the location of the pulse relative to the time origin.
What's next?
The next module in this series will introduce the inverse Fourier transform (as
opposed to the forward Fourier transform) and will explain the reversible
nature of the Fourier transform.
Listing 6. Dsp034.java.
/* File Dsp034.java
Copyright 2004, R.G.Baldwin
Rev 5/21/04
400
Listing 6. Dsp034.java.
11
0
0.0
0.5
0
0
0
45
90
135
90
45
0
0
0
public Dsp034(){//constructor
}//end constructor
//-------------------------------------------//
int cnt = 0;
//Temporary holding area for strings. Allow
// space for a few blank lines at the end
// of the data in the file.
String[] data = new String[20];
try{
//Open an input stream.
BufferedReader inData =
new BufferedReader(new FileReader(
"Dsp034.txt"));
//Read and save the strings from each of
Listing 6. Dsp034.java.
// the lines in the file. Be careful to
// avoid having blank lines at the end,
// which may cause an ArrayIndexOutOfBounds
// exception to be thrown.
while((data[cnt] =
inData.readLine()) != null){
cnt++;
}//end while
inData.close();
}catch(IOException e){}
}//end getParameters
Listing 6. Dsp034.java.
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01. The plotting
// program pulls the data values to be plotted
// by calling these methods.
public int getNmbr(){
//Return number of functions to process.
// Must not exceed 5.
return 5;
}//end getNmbr
//-------------------------------------------//
//Provide the input data for plotting.
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > data.length-1){
return 0;
}else{
return data[index];
}//end else
}//end function
//-------------------------------------------//
//Provide the amplitude spectral data for
// plotting. Attempt to scale it so that it
// will plot well in the range 0 to 180.
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > mag.length-1){
return 0;
}else{
return (4*dataLen/pulseLen)*mag[index];
}//end else
}//end function
//-------------------------------------------//
//Provide the real spectral data for
// plotting. Attempt to scale it so that it
// will plot well in the range -180 to 180.
Listing 6. Dsp034.java.
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > real.length-1){
return 0;
}else{
//Scale for convenient display
return (4*dataLen/pulseLen)*real[index];
}//end else
}//end function
//-------------------------------------------//
//Provide the imaginary spectral data for
// plotting. Attempt to scale it so that it
// will plot well in the range -180 to 180.
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 || index > imag.length-1){
return 0;
}else{
//Scale for convenient display
return (4*dataLen/pulseLen)*imag[index];
}//end else
}//end function
//-------------------------------------------//
//Provide the phase angle data for plotting.
// The angle ranges from -180 degrees to +180
// degrees. This is thing that drives the
// attempt to cause the other curves to plot
// well in the range -180 to +180.
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 || index > angle.length-1){
return 0;
}else{
return angle[index];
}//end else
}//end function
Listing 6. Dsp034.java.
//-------------------------------------------//
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it possible
for you to purchase a pre-printed version of the PDF file, you should be
aware that some of the HTML elements in this module may not translate well
into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales nor
do I know who does receive compensation. If you purchase such a book,
please be aware that it is a copy of a module that is freely available on
cnx.org and that it was made and published without my prior knowledge.
Affiliation :: I am a professor of Computer Information Technology at Austin
Community College in Austin, TX.
-end-
Java1485-Spectrum Analysis using Java, Forward and Inverse Transforms,
Filtering in the Frequency Domain
Baldwin illustrates and explains forward and inverse Fourier transforms using
both DFT and FFT algorithms. He also illustrates and explains the
implementation of frequency filtering by modifying the complex spectrum in
the frequency domain and transforming the modified complex spectrum back
into the time domain.
Table of contents
Table of contents
Preface
Viewing tip
Figures
Listings
Preview
Discussion and sample code
Preface
A previous module titled Fun with Java, How and Why Spectral Analysis
Works explained some of the fundamentals regarding spectral analysis.
The module titled Spectrum Analysis using Java, Sampling Frequency,
Folding Frequency, and the FFT Algorithm presented and explained several
Java programs for doing spectral analysis, including both DFT programs and
FFT programs. That module illustrated the fundamental aspects of spectral
analysis that center around the sampling frequency and the Nyquist folding
frequency.
The module titled Spectrum Analysis using Java, Complex Spectrum and
Phase Angle explained issues involving the complex spectrum, the phase
angle, and shifts in the time domain.
This module will illustrate and explain forward and inverse Fourier
transforms using both DFT and FFT algorithms. I will also illustrate and
explain the implementation of frequency filtering by modifying the complex
spectrum in the frequency domain and then transforming the modified
complex spectra back into the time domain.
Viewing tip
I recommend that you open another copy of this module in a separate browser
window and use the following links to easily find and view the Figures and
Listings while you are reading about them.
Figures
Preview
In this module, I will present and explain the following new programs:
In addition, I will use the following programs that I explained in the module
titled Spectrum Analysis using Java, Sampling Frequency, Folding Frequency,
and the FFT Algorithm and other previous modules.
This program can be run with either Graph03 or Graph06 in order to plot the
results. Enter the following at the command-line prompt to run the program
with Graph03 after everything is compiled:
java Graph03 Dsp035
When the data is plotted (see Figure 1 ) using the programs Graph03 or
Graph06 , the plots appear in the following order from top to bottom:
There were 256 values plotted horizontally in each section. I plotted the
values on a grid that is 270 units wide to make it easier to view the plots on
the rightmost end. This leaves some blank space on the rightmost end to
contain the numbers, preventing the numbers from being mixed in with the
plotted values. The last actual data value coincides with the rightmost tick
mark on each plot.
(See the program named Dsp036 later in the module for the use of
an FFT algorithm.)
Results
Before getting into the technical details of the program, let's take a look at the
results shown in Figure 1 .
The top plot in Figure 1 shows the input time series used in this experiment.
The time series is 256 samples long. Although the DFT algorithm can
accommodate time series of arbitrary lengths, I set the length of this time
series to a power of two so that I can compare the results with results
produced by an FFT algorithm later in the module.
As you can see, the input time series consists of three concatenated pulses
separated by blank spaces. The pulse on the leftmost end consists simply of
some values that I entered into the time series to create a pulse with an
interesting shape.
The objective
The real part of the complex spectrum is shown in the second plot from the
top in Figure 1 . It will become important later to note that the real part of the
spectrum is symmetrical about the folding frequency near the center of the
plot (at the eighth tick mark) .
Without attempting to explain why, I will simply tell you that the real part of
the Fourier transform of a complex series whose imaginary part is all zeros
(like a typical sampled time series for real-world data) is always symmetrical
about the folding frequency.
The imaginary part of the complex spectrum is shown in the third plot from
the top. Again, it will become important later to note that the imaginary part
of the spectrum is asymmetrical about the folding frequency.
Once again, without attempting to explain why, the imaginary part of the
Fourier transform of a complex series whose imaginary part is all zeros (like a
typical sampled time series for real-world data) is always asymmetrical about
the folding frequency.
It is also true that the values of the imaginary part of the Fourier transform of
a complex spectrum whose real part is symmetrical about the folding
frequency and whose imaginary part is asymmetrical about the folding
frequency will all be zero. I will take advantage of these facts later to simplify
the computing and plotting process.
The amplitude spectrum is shown in the fourth plot down from the top. Recall
from previous modules that the amplitude values are always positive,
consisting of the square root of the sum of the squares of the real and
imaginary parts.
The beginning of the class for Dsp035 , including the declaration of some
variables and the creation of some array objects is shown in Listing 1 . This
code is straightforward.
The constructor begins in Listing 2 . The code in Listing 2 creates the input
time series data shown in the top plot of Figure 1 .
Listing 2. Beginning of the constructor.
public Dsp035(){//constructor
Note that I deleted much of the code from Listing 2 for brevity. You can view
the missing code in Listing 14 near the end of the module.
ForwardRealToComplex01.transform(timeDataIn,
realSpect,
imagSpect,
angle,
magnitude,
zero,
0.0,
1.0);
The angle results returned by the transform program are not used in this
module.
One of the parameters (zero) establishes that the first sample in the time series
array referred to by timeDataIn represents the zero time origin.
InverseComplexToReal01.inverseTransform(
realSpect,
imagSpect,
timeDataOut);
}//end constructor
Listing 4 also signals the end of the constructor. Once the constructor has
completed executing, an object of the Dsp035 class exists. The array objects
belonging to the object have been populated with the original time series, the
complex spectrum of the original time series, and the output time series
produced by performing an inverse Fourier transform on that complex
spectrum. This data is ready for plotting.
All of the remaining code in Dsp035 consists of the six methods necessary to
satisfy the interface named GraphIntfc01 . Those methods are required to
provide data to the plotting program, as explained in earlier modules in this
series.
If you have studied the earlier modules in this series, you probably don't want
to hear any more about those methods, so I won't discuss them further. You
can view the six interface methods in Listing 14 near the end of the modules.
There are more efficient ways to write this method taking known symmetry
and asymmetry conditions into account. However, I wrote the method the way
that I did because I wanted it to mimic the behavior of an FFT algorithm.
Therefore, the complex input must extend from zero to the sampling
frequency.
The method considers the data length to be realIn.length , and considers the
computational time increment to be 1.0/realIn.length .
Assumptions
The method returns a number of points equal to the data length. It assumes
that the real input consists of positive frequency points for a symmetric real
frequency function. That is, the real input is assumed to be symmetric about
the folding frequency. The method does not test this assumption.
The method assumes that the imaginary input consists of positive frequency
points for an asymmetric imaginary frequency function. That is, the imaginary
input is assumed to be asymmetric about the folding frequency. Once again,
the method does not test this assumption.
A real output
A symmetric real part and an asymmetric imaginary part guarantee that the
imaginary output will be all zero values. Having made that assumption, the
program makes no attempt to compute an imaginary output. If the
assumptions described above are not valid, the results won't be valid.
The beginning of the class and the beginning of the static inverseTransform
method is shown in Listing 5 .
Listing 5 declares and initializes some variables that will be used later.
Listing 6 contains a pair of nested for loops that perform the actual inverse
transform computation.
Math.sin(2*Math.PI*time*j);
}//end inner loop
realOut[i] = real;
}//end outer loop
}//end inverseTransform
If you have been studying the earlier modules in this series, you should be
able to understand the code in Listing 6 without further explanation. Pay
particular attention to the comments that describe the two for loops.
The program named Dsp036 replicates the behavior of the program named
Dsp035 , except that it uses an FFT algorithm to perform the inverse Fourier
transform instead of using a DFT algorithm as in Dsp035 .
Compare Figure 2 with Figure 1 . The two should be identical. The program
named Dsp036 was designed to use an FFT algorithm for the inverse Fourier
transform and to replicate the behavior of the program named Dsp035 , which
uses a DFT algorithm for the inverse Fourier transform. In addition, the same
plotting parameters were used for both figures.
Some code from Dsp036
I'm only going to show you one short code fragment from the program named
Dsp036 . Listing 7 shows the code that calls the methods to perform the
forward and inverse Fourier transforms using the FFT algorithm. A complete
listing of the program named Dsp036 is shown in Listing 16 near the end of
the module.
inverseTransform(
realSpect,
imagSpect,
timeOut);
I'm not going to discuss this method in detail either, because it is very similar
to the method named InverseComplexToReal01 discussed earlier in
conjunction with Listing 4 and the listings following that one.
The transform method and the inverseTransform method each call a method
named complexToComplex to actually perform the Fourier transform. This
method implements a classical FFT algorithm accepting complex input data
and producing complex output data. The restriction of real-to-complex and
complex-to-real is imposed in this program by the methods named transform
and inverseTransform .
Although I didn't include the code in this module, (because it was shown in an
earlier module) , the transform method in Figure 7 passes a value of +1 to
the complexToComplex method to cause it to perform a forward Fourier
transform.
The impulse is shown as the input time series in the topmost plot in Figure 4 .
(Although I didn't show the complex spectrum of the impulse, we
know that the magnitude of the spectrum of an impulse is constant
across all frequencies. In other words, the magnitude spectrum of
an impulse is a flat line from zero to the sampling frequency and
above.)
Then the program eliminates all energy between one-sixth and five-sixths of
the sampling frequency by setting the real and imaginary parts of the FFT
output to zero.
The second, third, and fourth plots in Figure 4 show the real part, imaginary
part, and amplitude respectively of the modified complex spectrum.
(The two boxes in the fourth plot in Figure 4 show what's left of the
spectral energy after the energy in the middle of the band has been
eliminated.)
The folding frequency in these three plots is near the center of the plot at the
eighth tick mark.
The input data length was 256 samples. All but one of the input data values
was set to zero resulting in a single impulse in the input time series near the
second tick mark in Figure 4 .
(The real and complex parts of the frequency spectrum were
computed at 256 frequencies between zero and the sampling
frequency.)
There were 256 values plotted horizontally in each separate plot. Once again,
to make it easier to view the plots on the rightmost end, I plotted the values on
a grid that is 270 units wide. This leaves some blank space on the rightmost
end to contain the numbers, thus preventing the numbers from being mixed in
with the plotted values. The last actual data value coincides with the rightmost
tick mark on each plot.
The filtered impulse is shown as the bottom plot in Figure 4 . As you can see,
the pulse is smeared out in time relative to the input pulse in the top plot. This
is the typical result of reducing the bandwidth of a pulse.
Listing 8 shows the beginning of the class definition for the program named
Dsp037.
Listing 8 simply declares and initializes some variables that will be used later.
public Dsp037(){//constructor
timeDataIn[32] = 90;
Listing 9 creates the raw pulse data shown in the topmost plot in Figure 4 .
When the array object referred to by timeDataIn is created, the values of all
array elements are set to zero by default. Listing 9 modifies one of the
elements to have a value of 90. This results in a single impulse at an index of
32.
Continuing with the constructor, the code in Listing 10 uses an FFT algorithm
in the method named transform (discussed earlier) to compute the Fourier
transform of the impulse.
The results of the Fourier transform are stored in the array objects referred to
by realSpect , imagSpect , and magnitude .
Listing 11 applies the filter by setting sample values in a portion of the real
and imaginary parts of the complex spectrum to zero.
This code eliminates all energy between one-sixth and five-sixths of the
sampling frequency. The modified data for the real and imaginary parts of the
complex spectrum are shown in the second and third plots in Figure 4 .
Listing 12 re-computes the magnitude values for the modified real and
imaginary values of the complex spectrum.
InverseComplexToRealFFT01.inverseTransform(
realSpect,
imagSpect,
timeOut);
}//end constructor
The results of the inverse transform are shown in the bottom plot in Figure 4 .
Once the constructor returns, all of the data that is to be plotted has been
stored in the various array objects. The remaining code in the program
consists of the definition of the six methods required by the interface named
GraphIntfc01. These methods are required to make it possible to use the
program named Graph03 to plot the results as shown in Figure 4 .
While discussing the program named Dsp037 , I told you that performing a
different modification on the complex spectrum would result in a different
waveform for the filtered impulse. The program named Dsp038 applies a
different modification to the complex spectrum, but is otherwise the same as
Dsp037.
The first difference to note between the two figures is that I moved the
impulse in the input time series in the topmost plot sixteen samples further to
the right in Dsp038 .
(This has no impact on the final result, which you can verify by
modifying the program to move the impulse to a different position
and then compiling and running the modified program.)
Finally, note the waveforms of the two filtered impulses. The overall
amplitude of the filtered impulse in Figure 5 is less than in Figure 4 , simply
because it contains less total energy. In addition, the filtered impulse in Figure
5 is broader than the filtered impulse in Figure 4 . This is because it has a
narrower bandwidth.
Create more complex experiments. For example, use more complex input time
series when experimenting with frequency filtering. Apply different
modifications to the complex spectrum when experimenting with frequency
filtering.
Summary
This module illustrates and explains forward and inverse Fourier transforms
using both DFT and FFT algorithms.
Listings for other programs mentioned in the module, such as Graph03 and
Graph06 , are provided in other modules. Those modules are identified in the
text of this module.
import java.util.*;
public Dsp035(){//constructor
timeDataIn[240] = 80;
timeDataIn[241] = 80;
timeDataIn[242] = 80;
timeDataIn[243] = 80;
timeDataIn[244] = -80;
timeDataIn[245] = -80;
timeDataIn[246] = -80;
timeDataIn[247] = -80;
timeDataIn[248] = 80;
timeDataIn[249] = 80;
timeDataIn[250] = 80;
timeDataIn[251] = 80;
timeDataIn[252] = -80;
timeDataIn[253] = -80;
timeDataIn[254] = -80;
timeDataIn[255] = -80;
//------------------------------------------
-//
//The following six methods are required by
the
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of curves to plot. Must not
// exceed 5.
return 5;
}//end getNmbr
//------------------------------------------
-//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > timeDataIn.length-1)
{
return 0;
}else{
return timeDataIn[index];
}//end else
}//end function
//------------------------------------------
-//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > realSpect.length-1){
Listing 14. Dsp035.java.
return 0;
}else{
//scale for convenient viewing
return 5*realSpect[index];
}//end else
}//end function
//------------------------------------------
-//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > imagSpect.length-1){
return 0;
}else{
//scale for convenient viewing
return 5*imagSpect[index];
}//end else
}//end function
//------------------------------------------
-//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 || index > magnitude.length-1){
return 0;
}else{
//scale for convenient viewing
return 5*magnitude[index];
}//end else
}//end function
//------------------------------------------
-//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > timeDataOut.length-1)
{
return 0;
Listing 14. Dsp035.java.
}else{
return timeDataOut[index];
}//end else
}//end function
/*File InverseComplexToReal01.java
Copyright 2004, R.G.Baldwin
Rev 5/24/04
public Dsp036(){//constructor
timeDataIn[240] = 80;
timeDataIn[241] = 80;
timeDataIn[242] = 80;
timeDataIn[243] = 80;
timeDataIn[244] = -80;
timeDataIn[245] = -80;
timeDataIn[246] = -80;
timeDataIn[247] = -80;
timeDataIn[248] = 80;
timeDataIn[249] = 80;
timeDataIn[250] = 80;
timeDataIn[251] = 80;
Listing 16. Dsp036.java.
timeDataIn[252] = -80;
timeDataIn[253] = -80;
timeDataIn[254] = -80;
timeDataIn[255] = -80;
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of curves to plot. Must not
// exceed 5.
return 5;
}//end getNmbr
Listing 16. Dsp036.java.
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > timeDataIn.length-1){
return 0;
}else{
return timeDataIn[index];
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > realSpect.length-1){
return 0;
}else{
//scale for convenient viewing
return 5*realSpect[index]/len;
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > imagSpect.length-1){
return 0;
}else{
//scale for convenient viewing
return 5*imagSpect[index]/len;
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude.length-1){
return 0;
}else{
Listing 16. Dsp036.java.
//scale for convenient viewing
return 5*magnitude[index];
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > timeOut.length-1){
return 0;
}else{
//scale for convenient viewing
return timeOut[index]/len;
}//end else
}//end function
/*File InverseComplexToRealFFT01.java
Copyright 2004, R.G.Baldwin
Rev 5/24/04
int right = 0;
for (int left = spectraCnt;
left < len;left += stepSize){
right = left + maxSpectraForStage;
double tempReal =
realCorrection*real[right]
- imagCorrection*imag[right];
double tempImag =
realCorrection*imag[right]
+ imagCorrection*real[right];
real[right] = real[left]-tempReal;
imag[right] = imag[left]-tempImag;
real[left] += tempReal;
imag[left] += tempImag;
}//end for loop
}//end for loop for individual spectra
maxSpectraForStage = stepSize;
}//end for loop for stages
}//end complexToComplex method
/* File Dsp037.java
Copyright 2004, R.G.Baldwin
Revised 5/24/04
public Dsp037(){//constructor
//-------------------------------------------//
//The following six methods are required by the
Listing 18. Dsp037.java.
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of curves to plot. Must not
// exceed 5.
return 5;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > timeDataIn.length-1){
return 0;
}else{
return timeDataIn[index];
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > realSpect.length-1){
return 0;
}else{
return realSpect[index];
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > imagSpect.length-1){
return 0;
}else{
return imagSpect[index];
}//end else
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
Listing 18. Dsp037.java.
if(index < 0 ||
index > magnitude.length-1){
return 0;
}else{
//scale for convenient viewing
return len*magnitude[index];
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > timeOut.length-1){
return 0;
}else{
//scale for convenient viewing
return 3.0*timeOut[index]/len;
}//end else
}//end function
/* File Dsp038.java
Copyright 2004, R.G.Baldwin
Revised 5/24/04
public Dsp038(){//constructor
//-------------------------------------------//
//The following six methods are required by the
// interface named GraphIntfc01.
public int getNmbr(){
//Return number of curves to plot. Must not
// exceed 5.
return 5;
}//end getNmbr
//-------------------------------------------//
public double f1(double x){
int index = (int)Math.round(x);
if(index < 0 || index > timeDataIn.length-1){
return 0;
}else{
return timeDataIn[index];
}//end else
}//end function
//-------------------------------------------//
public double f2(double x){
int index = (int)Math.round(x);
if(index < 0 || index > realSpect.length-1){
return 0;
}else{
return realSpect[index];
}//end else
}//end function
//-------------------------------------------//
public double f3(double x){
int index = (int)Math.round(x);
if(index < 0 || index > imagSpect.length-1){
return 0;
}else{
return imagSpect[index];
}//end else
Listing 19. Dsp038.java.
}//end function
//-------------------------------------------//
public double f4(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > magnitude.length-1){
return 0;
}else{
//scale for convenient viewing
return len*magnitude[index];
}//end else
}//end function
//-------------------------------------------//
public double f5(double x){
int index = (int)Math.round(x);
if(index < 0 ||
index > timeOut.length-1){
return 0;
}else{
//scale for convenient viewing
return 3.0*timeOut[index]/len;
}//end else
}//end function
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it possible
for you to purchase a pre-printed version of the PDF file, you should be
aware that some of the HTML elements in this module may not translate well
into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales nor
do I know who does receive compensation. If you purchase such a book,
please be aware that it is a copy of a module that is freely available on
cnx.org and that it was made and published without my prior knowledge.
Affiliation : I am a professor of Computer Information Technology at Austin
Community College in Austin, TX.
-end-
Java1486-Fun with Java, Understanding the Fast Fourier Transform (FFT)
Algorithm
Baldwin explains the underlying signal processing concepts that make the
Fast Fourier Transform (FFT) algorithm possible.
Table of contents
Table of contents
Preface
Viewing tip
Figures
Listings
General discussion
A general-purpose transform
Transforming from space domain to wave number domain
A sample program
Preface
Programming in Java doesn't have to be dull and boring. In fact, it's possible
to have a lot of fun while programming in Java. This module was taken from
a series that concentrates on having fun while programming in Java.
Viewing tip
I recommend that you open another copy of this module in a separate browser
window and use the following links to easily find and view the Figures and
Listings while you are reading about them.
Figures
Listings
General discussion
The purpose of this module is to help you to understand how the Fast Fourier
Transform (FFT) algorithm works. In order to understand the FFT, you must
first understand the Discrete Fourier Transform (DFT). I explained how the
DFT works in an earlier module titled Fun with Java, How and Why Spectral
Analysis Works .
There are several different FFT algorithms in common use. In addition, there
are many sites on the web where you can find explanations of the mechanics
of FFT algorithm. I won't replicate those explanations. Rather, I will explain
the underlying concepts that make the FFT possible and illustrate those
concepts using a simple program. Hopefully, once you understand the
underlying concepts, one or more of the explanations of the mechanics that
you find on other sites will make sense to you.
A general-purpose transform
For example, my first job after earning a BSEE degree in 1962 was in the
Seismic Research Department of Texas Instruments. That is where I had my
first encounter with Digital Signal Processing (DSP) . In that job, I did a lot of
work with Fourier transforms involving the time domain and the frequency
domain. I also did a lot of work with Fourier transforms involving the space
domain and the wave-number domain.
(Those familiar with the subject will know that while compression
waves will propagate through water and air, those media won't
support shear waves.)
For example, one of the things that we did was to compute two-dimensional
Fourier transforms on diagrams representing weighted points in two-
dimensional space. We would transform the weighted points in the space
domain into points in the wave-number domain.
The weighted points in the space domain represented the locations and
amplifications of seismometers in a two-dimensional array on the surface of
the earth. Each seismometer was amplified by a different gain factor and
polarity. The amplified outputs of the seismometers were added together in
various and complex ways intended to enhance signals and suppress noise.
In this case, the wave number was the reciprocal of the wave length of seismic
waves propagating across the array. By plotting the results of the
transformation in the wave-number domain, we could estimate which seismic
waves would be enhanced and which seismic waves would be suppressed by
the processing being applied to the seismometer outputs.
I mention all of this simply to illustrate the general nature of the Fourier
transform. Once again, the Fourier transform is simply a mathematical process
that can be used to transform a set of complex values in one domain into a set
of complex values in a different domain.
Before getting into the details of this discussion, I want to refer you to a
couple of excellent references on the FFT. Of course, you can find many more
by performing a Google search for the keyword FFT.
Many of the images that you will see in this module were produced using an
applet named FftLab that I originally downloaded from a website named
sepwww.stanford.edu/oldsep/hale/FftLab.html. (As of October 2015, that
website no longer exists. However, you can now download the applet from
http://sepwww.stanford.edu/data/media/public/oldsep/hale/FftLab.java . Also,
as of October 2015, you can learn more about the applet's author, Dave Hale
here .)
In order to use the applet to create the illustrations for this document, I
changed the name of the applet class to cause it to fit into my file-naming
scheme. I also made a couple of minor modifications to the code to force the
applet's output to fit into this narrow publication format. Otherwise, I used the
applet in its original form. This applet is extremely useful in performing FFT
experiments very quickly and easily. I strongly recommend that you become
familiar with it.
Hopefully after reading my explanation of the basic concepts, you will be able
to understand the explanation of the mechanics of the algorithm provided by
others.
A linear transform
One of those facts is that the Fourier transform is a linear transform. By this, I
mean that the transform of the sum of two or more input series is equal to the
sum of the transforms of the individual input series. I will attempt to illustrate
this in Figure 1 , Figure 2 , and Figure 3 .
This is an interactive applet with the ability to transform the complex samples
represented by f(x) into complex samples represented by F(k). Alternatively,
the applet can be used to transform complex samples represented by F(k) into
complex samples represented by f(x).
Each section contains two boxes, one labeled Real and the other labeled
Imaginary. One box contains a visual representation of a set of real samples
and the other box contains a visual representation of a set of imaginary
samples.
With one exception, each sample is represented by a black circle. In each box,
one of the samples is represented by an empty circle. The empty circle
represents an index value of zero. Samples to the right of the sample with the
empty circle are samples at positive indices, and samples to the left of the
sample with the empty circle are samples at negative indices.
A complex sample
A pair of values, one taken from the Real box and one taken from the
Imaginary box, represents a complex sample.
Any of the circles can be interactively moved up or down with the mouse. The
value of each sample is represented by the distance of the corresponding circle
from the horizontal line.
When a change is made to the value of any sample belonging to either f(x) for
F(k), the transformation is recomputed and the display of the other function is
modified accordingly. If you modify the value of a sample in f(x), the values
in F(k) are automatically modified to show the Fourier transform of f(x). If
you modify the value of a sample in F(k), the values in f(x) are automatically
modified to show the inverse Fourier transform of F(k).
Powers of two
Many and perhaps most FFT algorithms require the input series to contain a
number of complex samples that is a power of two such as 2, 4, 8, 16, 32, etc.
Most FFT algorithms also produce the same number of complex samples in
the output as are provided in the input. The FFT algorithm used in this applet
is no exception to those rules.
A pull-down list at the bottom of the applet lets the user specify 16, 32, or 64
complex samples for both the input and the output. All of the examples in this
module use 16 complex samples for input and output.
The applet also provides a check box that allows the user to cause the origin
(the empty circle at index value zero) to either be centered or placed at the left
end. The display in Figure 1 has the origin centered. Other displays that I will
use later have the origin at the left end.
The other pull-down list and the button at the bottom of the applet provide
other control features that don't need to be discussed here. I strongly urge you
to download this applet and experiment with it. The results can be very
enlightening.
Having discussed the features of the interactive FFT tool that I used to
produce many of the images in this module, it is time to get back to the
discussion of the Fourier transform as a linear transform. The fact that the
Fourier transform is a linear transform is illustrated in Figure 1 , Figure 2 ,
and Figure 3 .
In these three figures, the input series is shown in the real area in the upper
left. For simplification, the values of the imaginary part of the input series
shown in the upper right are all zero.
Also, for simplification, the zero origin is shown in the center by the value
with the empty circle.
The real and imaginary parts of the transform output are shown in the bottom
of each figure.
Figure 1 shows an input series consisting of a pulse that starts with a high
value at the origin and extends down and to the right for five samples, ending
in a large negative value.
This input series produces a rather complicated transform output series, as can
be seen in the bottom two boxes in Figure 1 . I will come back to a discussion
of the transform output later.
A mirror-image pulse
Figure 2 shown an input series consisting of a pulse that begins with a large
negative value four samples to the left of the origin and extends up and to the
right ending with a large positive value at the origin. The input series in
Figure 2 is the mirror image of the input series in Figure 1 relative to the
origin.
Once again, the output from the transform of the input series is shown in the
bottom two boxes of Figure 2 .
A comparison of the real part of each of the transforms for Figure 1 and
Figure 2 shows that the real parts are the same, at least insofar as I was able to
control the input by interactively adjusting the locations of the circles using
the mouse.
A comparison of the imaginary part of each of the transforms shows that the
imaginary parts are the same except for the algebraic sign of each of the
values in the imaginary part. The algebraic sign of each of the values in
Figure 2 is the reverse of the algebraic sign of each of the values in Figure 1 .
Figure 3 shows an input series that is the sum of the individual input series
from Figure 1 and Figure 2 . This produces a pulse that is symmetric around
the origin indicated by the value with the empty circle.
Normalized output
Note that the display of the transform values produced by this applet is
normalized so as to keep them in a reasonable range for plotting. As a result,
absolute values don't have much meaning. Only relative values have meaning.
The real part is the same
The real part of the transform of the input series in Figure 3 has the same
shape as the real parts of the transforms of the input series in Figure 1 and
Figure 2 . This is what would be produced by adding the real parts of the
transforms of the pulses in Figure 1 and Figure 2 , and then normalizing the
result.
The imaginary part of the transform of the input series in Figure 3 is zero at
all sample values. This is what would be produced by adding the imaginary
parts of the transforms of the input series in Figure 1 and Figure 2 .
(Recall that the values in the imaginary parts of the two earlier
transforms had the same magnitude but opposite signs).
Thus, Figure 1 , Figure 2 , and Figure 3 demonstrate that the transform of the
sum of two or more input series is equal to the sum of the transforms of the
individual input series. The Fourier transform is a linear transform.
The real part of the transform of a single real sample with a shift relative to
the origin has the shape of a cosine curve with a period that is proportional to
the reciprocal of the shift. Negative sample values produce cosine curves with
negative amplitudes.
The magnitude of the transform is the square root of the sum of the squares of
the real and imaginary parts at each output sample point. For the case of a
single input sample with a shift, that magnitude is constant for all output
sample points and is proportional to the absolute value of the sample.
The above facts are illustrated in Figure 4 , Figure 5 , Figure 6 , and Figure 7 .
A shift of zero
Figure 4 shows the transform of an impulse with a shift of zero relative to the
origin.
(Note that in this series of figures, the origin was moved from the
center to the left end. Once again, the sample with the empty circle
represents the origin.)
Although it isn't obvious, the real part of the transform in Figure 4 has the
shape of a cosine curve with a period that is the reciprocal of the shift.
Because the shift is zero, the period of the cosine curve is infinite, producing
real values that are constant at all output sample values.
Similarly, the imaginary part of the transform in Figure 4 has a shape that is a
sine curve with an infinite period. Thus, it is zero at all output sample values.
Figure 5 shows the transform of an impulse with a negative value and a shift
of one sample interval relative to the origin.
The shape of the real part of the transform output is an upside down cosine
curve. It is upside down because it has a negative amplitude. This is caused by
the fact that the input sample has a negative value.
The shape of the imaginary part of the transform is an upside down sine
curve.
In this case, I set the applet up to accept sixteen input samples and to produce
sixteen output samples.
For the moment, lets think in terms of time and frequency. Assume that the
input series f(x) is a time series and the output series F(k) is a frequency
spectrum.
To make the arithmetic easy, let's assume that the sampling interval for the
input time series in the upper left box of Figure 5 is one second. This gives a
sampling frequency of one sample per second, and a total elapsed time of
sixteen seconds.
The sine and cosine curves in Figure 5 each go through one complete period
between a frequency of zero and the sampling frequency, which one sample
per second. Thus, the period of the sine and cosine curves along the frequency
axis is one sample per second. This is the reciprocal of the time shift of one
sample interval at a sampling frequency of one sample per second.
Stated differently, the number of periods of the sine and cosine curves in the
real and imaginary parts of the transform between a frequency of zero and a
frequency equal to the sampling frequency is equal to the shift in sample
intervals. A shift of one sample interval produces sine and cosine curves
having one period in the frequency range from zero to the sampling
frequency. A shift of two sample intervals produces sine and cosine curves
having two periods in the frequency range from zero to the sampling
frequency, etc. This is illustrated by Figure 6 .
The real part of the transform has the shape of a cosine curve with two
complete periods between zero and an output index equal to the sampling
frequency.
The imaginary part of the transform has the shape of a sine curve with two
complete periods within the same output interval. This agrees with the
conclusions stated in the previous section.
Finally, Figure 7 shows the transform of an impulse with a shift equal to four
sample intervals.
The cosine and sine curves that represent the real and imaginary parts of the
transform each have four complete periods between zero and an output index
equal to the sampling frequency.
In this case the cosine and sine curves are very sparsely sampled.
Those equations are simple sine and cosine equations as a function of the
units of the output domain. This is an important concept that contributes
greatly to the implementation of the FFT algorithm.
If the complex part of the input series f(x) is not zero, things get somewhat
more complicated. For example, the real and imaginary parts of the transform
of an impulse having both real and imaginary parts are not necessarily cosine
and sine curves. This is illustrated in Figure 8 .
Figure 8 shows the results of transforming an impulse having both real and
imaginary parts and a shift of two sample intervals.
Although both the real and imaginary parts of the transformed result have the
shape of a sinusoid, neither is a cosine curve and neither is a sine curve. Both
of the curves are sinusoidal curves that have been shifted along the horizontal
output axis moving their peaks and zero crossings away from the origin.
Even for a complex input series, if you know the values of the
real and imaginary parts of a sample and you know the value of
the shift associated with that sample, you can write equations
that describe the real part and the imaginary part of the
transform results.
Can produce the transform of a time series by the adding transforms of the individual samples
That brings us to the crux of the matter. Given an input series consisting of a
set of sequential samples taken at uniform sampling intervals, we know how
to write equations for the real and imaginary parts that would be produced by
performing a Fourier transform on each of those samples individually.
We know that we can consider the input series to consist of the sum of the
individual samples, each having a specified value and a different shift. We
know that the Fourier transform is a linear transform. Therefore, the Fourier
transform of an input series is the sum of the transforms of the individual
samples.
In truth, there are several different forms of the FFT algorithm, and the
mechanics of each may be slightly different. At least one, and probably many
of the algorithms operate by performing the following steps:
A sample program
I want to emphasize at the outset that this program DOES NOT implement an
FFT algorithm. Rather, this program illustrates the underlying signal
processing concepts that make the FFT possible in a form that is more easily
understood than is normally the case with an actual FFT algorithm.
This program performs each of the processes listed above. However, it does
not perform those processes in the special order used by an FFT algorithm
that causes the FFT algorithm to be able to perform those processes at very
high speed.
The decomposition process in this program takes the complex samples in the
order that they appear in the input complex series. The transform of each
complex sample is simply the sample itself. This is the result that would be
obtained by actually computing the transform of the complex sample if the
sample were the first sample in the series.
The transform result for each complex sample (the sample itself) is then
corrected for position by applying sine and cosine curves to reflect the actual
position of the complex sample within the original complex series.
In order to accomplish the recombination of the corrected transform results,
the real and imaginary parts of the corrected transform are added to
accumulators. These accumulators are used to accumulate the corrected real
and imaginary parts from the corrected transforms for all of the individual
complex samples.
Once the real and imaginary parts have been accumulated for all of the
complex samples, the real part of the accumulator represents the real part of
the transform of the original complex series. The imaginary part of the
accumulator represents the imaginary part of the transform of the original
complex series. However, an actual transform was never performed on the
original complex series.
This program creates three separate complex series, applies the processes
listed above to each of those series, and displays the results on the screen.
class Fft02{
I will put the main method on the back burner for the moment and explain the
class named Transform .
class Transform{
The doIt method receives five incoming parameters. The first two parameters
are references to two array objects of type double containing the real and
imaginary parts of the input series.
The third parameter is a scale factor that is applied to the transform output in
an attempt to keep the values in a range suitable for plotting if desired.
The last two parameters are references to array objects of type double . The
results of performing the transform are used to populate these two arrays. This
is the mechanism by which the object returns the transform results to the
calling program. It is assumed that all of the elements in these two array
objects contain values of zero upon entry to the doIt method.
The body of the doIt method is presented in Listing 3 . The code in Listing 3
iterates on the input arrays, passing each complex sample contained in those
two arrays to a method named correctAndRecombine .
Listing 3. Performing the transform.
Each complex value in the incoming arrays represents both a complex sample
and the transform of that complex sample under the assumption that the
complex sample appears at the origin of the input series.
(2.0*Math.PI*cnt/length)*position;
realSample*Math.cos(angle)/scale;
imagOut[cnt] +=
realSample*Math.sin(angle)/scale;
imagSample*Math.sin(angle)/scale;
Listing 4. The correctAndRecombine method.
imagOut[cnt] +=
imagSample*Math.cos(angle)/scale;
}//end for loop
}//end correctAndRecombine
This method accepts an incoming complex sample value and the position in
the series associated with that sample. The method corrects the real and
imaginary transform values for that complex sample to reflect the specified
position in the input series.
After correcting the transform values for the sample on the basis of position,
the method updates the corresponding real and imaginary values contained in
array objects that are used to accumulate the real and imaginary values for all
of the samples.
The incoming parameter named length specifies the number of output samples
that are to be produced.
Hopefully this explanation will make it possible for you to understand the
code in Listing 4 .
Note in particular the use of the Math.cos and Math.sin methods to apply the
cosine and sine curves in the correction of the transforms of the individual
complex samples. This is used to produce results similar to those shown in
Figure 5 through Figure 7 .
A real FFT program would probably compute the cosine and sine
values only once, put them in a table and extract them from the
table when needed.
Note the use of the position and length parameters in the computation of the
angle that is passed as an argument to the Math.cos and Math.sin methods.
Also note how the correction is made separately on the real and imaginary
parts of the input. This produces results similar to those shown in Figure 7
after those results are added in the accumulators.
Returning now to the main method, the code in Listing 5 prepares the input
data and the output arrays for the first case that we will look at. This case is
labeled as Case A.
System.out.println("Case A");
double[] realInA =
{0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1};
double[] imagInA =
{0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
imagOutA);
display(realOutA,imagOutA);
Note that for Case A, the input complex series contains non-zero values only
in the real part. Also, most of the values in the real part are zero.
Case A is shown in graphic form in Figure 9 . As you can see, the input series
consists of two non-zero values in the real part. All the values in the
imaginary part are zero.
Figure 9. Case A. Transform of a real sample with two non-zero
values.
The real part of the transform of the complex input series looks like one cycle
of a cosine curve. All of the values in the imaginary part of the transform
result are zero.
As you saw in Listing 5 , the code in the main method calls a method named
display to display the complex transform output in numeric form on the
screen. The output produced by Listing 5 is shown in Figure 10 . (Note that I
manually inserted line breaks to force the material to fit in this narrow
publication format.)
Case A
Real:
1.0 0.923 0.707 0.382 0.0 -0.382 -0.707 -0.923
-1.0 -0.923 -0.707 -0.382 0.0 0.382 0.707 0.923
imag:
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
If you plot the real and imaginary values in Figure 10 , you will see that they
match the transform output shown in graphic form in Figure 9 .
Case B code
The code from the main method for Case B is shown in Listing 6 . Note that
the input complex series contains non-zero values in both the real and
imaginary parts.
System.out.println("\nCase B");
double[] realInB =
{0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1};
double[] imagInB =
{0,-1,0,0,0,0,0,0,0,0,0,0,0,0,0,-1};
transform.doIt(realInB,imagInB,2.0,realOutB,
imagOutB);
display(realOutB,imagOutB);
Case B
Real:
1.0 0.923 0.707 0.382 0.0 -0.382 -0.707 -0.923
-0.999 -0.923 -0.707 -0.382 0.0 0.382 0.707
0.923
imag:
-1.0 -0.923 -0.707 -0.382 0.0 0.382 0.707 0.923
1.0 0.923 0.707 0.382 0.0 -0.382 -0.707 -0.923
If you plot the values for the real and imaginary parts from Figure 12 , you
will see that they match the real and imaginary output shown in Figure 11 .
Case C code
The code extracted from the main method for Case C is shown in Listing 7 .
System.out.println("\nCase C");
double[] realInC =
{1.0,0.923,0.707,0.382,0.0,-0.382,-0.707,
-0.923,-1.0,-0.923,-0.707,-0.382,0.0,
0.382,0.707,0.923};
double[] imagInC =
{0.0,-0.382,-0.707,-0.923,-1.0,-0.923,
-0.707,-0.382,0.0,0.382,0.707,0.923,
1.0,0.923,0.707,0.382};
transform.doIt(realInC,imagInC,16.0,realOutC,
imagOutC);
display(realOutC,imagOutC);
The complex input series for Case C is a little more complicated than that for
either of the previous two cases. Note in particular that the input complex
series contains non-zero values in both the real and imaginary parts. In
addition, very few of the values in the complex series have a value of zero.
One of the interesting things to note about Figure 13 is the similarity of Figure
13 and Figure 5 . These two figures illustrate the reversible nature of the
Fourier transform.
If I had used a positive input real value instead of a negative input real value
in Figure 5 , the input of Figure 5 would look exactly like the output in Figure
13 , and the output of Figure 5 would look exactly like the input of Figure 13 .
With that as a hint, you should now be able to figure out how I used
a mouse and drew the perfect sine and cosine curves in Figure 13 .
In fact, I didn't draw them at all. Rather, I used my mouse and drew
the output, and the applet gave me the corresponding input
automatically.
Case C
Real:
0.0 0.999 0.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
imag:
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0
If you plot the real and imaginary input values from Listing 7 , you will see
that they match the input values in Figure 13 . If you plot the real and
imaginary output values in Figure 14 , you will see that they match the output
values shown in Figure 13 .
Listing 8 shows the code for a simple method named display . The purpose of
the display method is to display a real series and an imaginary series, each
contained in an incoming array object of type double . The double values are
truncated to no more than four digits before displaying them. Then they are
displayed on a single line.
System.out.println("imag: ");
for(int cnt=0;cnt < imag.length;cnt++){
System.out.print(((int)(1000.0*imag[cnt]))
/1000.0 + " ");
}//end for loop
System.out.println();
}//end display
Listing 8 also signals the end of the controlling class named Fft02 .
Run the program
I encourage you to copy and compile the program that you will find in Listing
9 . Experiment with different complex input series.
Finally, I encourage you to examine the source code for the applet.
Concentrate on that portion of the source code that performs the FFT.
Hopefully, what you have learned in this module will make it easier for you to
understand the source code for the FFT.
Summary
In this module, I have explained some of the underlying signal processing
concepts that make the FFT possible. I illustrated those concepts in a program
designed specifically to be as simple as possible while still illustrating the
concepts.
Now that you understand those concepts, you should be able to better
understand explanations of the mechanics of the FFT algorithm that appear on
various websites.
Listing 9. Fft02.java.
Listing 9. Fft02.java.
/*File Fft02.java Copyright 2004, R.G.Baldwin
Rev 4/30/04
class Fft02{
transform.doIt(realInB,imagInB,2.0,realOutB,
imagOutB);
display(realOutB,imagOutB);
transform.doIt(realInC,imagInC,16.0,realOutC,
imagOutC);
display(realOutC,imagOutC);
}//end main
//===========================================//
System.out.println("imag: ");
for(int cnt=0;cnt < imag.length;cnt++){
System.out.print(((int)(1000.0*imag[cnt]))
/1000.0 + " ");
}//end for loop
System.out.println();
}//end display
//===========================================//
Baldwin explains the underlying signal processing concepts that make the
Fast Fourier Transform (FFT) algorithm possible.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it possible
for you to purchase a pre-printed version of the PDF file, you should be
aware that some of the HTML elements in this module may not translate well
into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales nor
do I know who does receive compensation. If you purchase such a book,
please be aware that it is a copy of a module that is freely available on
cnx.org and that it was made and published without my prior knowledge.
Affiliation : I am a professor of Computer Information Technology at Austin
Community College in Austin, TX.
-end-
Java1490-2D Fourier Transforms using Java, Part 1
Learn how the space domain and the wavenumber domain in two-
dimensional analysis are analogous to the time domain and the frequency
domain in one-dimensional analysis. Learn about some practical examples
showing how 2D Fourier transforms and wavenumber spectra can be useful
in solving engineering problems involving antenna arrays.
Table of contents
Table of contents
Preface
Viewing tip
Figures
General discussion
A two-dimensional array
Radio astronomy
Seismology
Sonar
Radar
Petroleum exploration
Image processing
Summary
What's next?
Miscellaneous
Preface
This is the first module of a two-part series. In this module, I will:
In Part 2 of this series, I will present and explain two separate programs.
One program consists of a single class named ImgMod30 . The purpose of
this class is to satisfy the computational requirements for forward and
inverse 2D Fourier transforms. This class also provides a method for
rearranging the spectral data into a more useful format for plotting. The
second program named ImgMod31 will be used to test the 2D Fourier
transform class, and also to illustrate the use of 2D Fourier transforms for
some well known sample surfaces.
This and the following module will cover some technically difficult
material in the general area of Digital Signal Processing, or DSP for short.
As usual, the better prepared you are, the more likely you are to understand
the material. For example, it would be well for you to already understand
the one-dimensional Fourier transform before tackling the 2D Fourier
transform. If you don't already have that knowledge, you can learn about
one-dimensional Fourier transforms by studying the following modules :
1478 Fun with Java, How and Why Spectral Analysis Works
1482 Spectrum Analysis using Java, Sampling Frequency, Folding
Frequency, and the FFT Algorithm
1483 Spectrum Analysis using Java, Frequency Resolution versus Data
Length
1484 Spectrum Analysis using Java, Complex Spectrum and Phase
Angle
1485 Spectrum Analysis using Java, Forward and Inverse Transforms,
Filtering in the Frequency Domain
1486 Fun with Java, Understanding the Fast Fourier Transform (FFT)
Algorithm
The 2D Fourier transform has many uses. I will use the 2D Fourier
transform in several future modules involving such diverse topics as:
Viewing tip
Figures
General discussion
(In the computer world, we can make it appear that time can also
go backwards, but this still constitutes only one dimension.)
You learned that with enough computational power, you can easily
transform a given set of data back and forth between these two domains.
This makes it possible to use the domain of your choice to perform a given
signal processing operation, even if the results need to be delivered in the
other domain.
In this module, we will extend the concept of the Fourier transform from
the time domain into the space domain. In making this extension, we will
encounter some significant additional complexity. For example, while time
is one-dimensional, space is three-dimensional. While you can only move
forward and backwards in time, you can move up, down, forward,
backward, and from side to side in space.
We will consider the space domain to be analogous to the time domain, with
the stipulation that the space domain has two dimensions. The unit of
measure in the time domain is usually seconds, or some derivative thereof.
The unit of measure in space is usually meters, or some derivative thereof.
As with the time domain, we will assume that all space domain surfaces are
purely real (as opposed to being complex) . This will allow us to simplify
our computations when performing the 2D Fourier transform to transform
our data from the space domain into the wavenumber domain.
With all of this as background, I will begin by discussing some real world
engineering problems for which the solution lies in an understanding of the
wavenumber domain. I will use these examples to show some of the
practical uses of 2D Fourier transforms.
Following that (in Part 2 of this series) , I will present and explain a class
that you can copy and use to perform 2D Fourier transforms. Then I will
present and explain a program that exercises and tests the 2D Fourier
transform class for some common 3D surfaces.
Assume that you have just acquired an FCC license to build and operate a
new commercial radio station in a small town in west Texas. As is
frequently the case in west Texas, your town is situated at the intersection of
two highways. One highway runs northeast and southwest. The other
highway runs northwest and southwest. The two highways are generally
perpendicular to one another. Like many highways in west Texas, each of
these highways is straight as an arrow with very few curves.
Your town has a small business district at the intersection of the two
highways. Beyond that, most of the people who live in your town (and who
will listen to your radio station ) live along the two highways. Thus most of
the population lives in the directions of northeast, southwest, northwest, and
southeast from the center of town. There are very few people living in the
directions of north, south, east, and west. That real estate is mostly
populated by cows and cotton fields.
Your new FCC license places a limit on the amount of power that you will
be allowed to transmit. You would like to use that available power to reach
a many human listeners as possible. If you simply construct an
omnidirectional transmitting antenna and start broadcasting, approximately
half of the power that you transmit will be available mostly to cows and
cotton plants. As a result, the amount of power, and hence the reach of the
power that you transmit to human listeners will be less than you would like
for it to be.
While the FCC won't allow you to increase the amount of power that you
transmit, they will allow you to control the directions in which you choose
to transmit that power. You will probably hire an expert in the transmission
of radio signals to design a directional transmitting antenna system, which
will broadcast most of the available power in the directions where the
people live. Ideally, the antenna system will transmit very little of the
available power in the directions of the cows and the cotton fields.
The antenna designer will have many tools at her disposal. Whether or not
she uses wavenumber terminology, many of the calculations that she
performs will depend on wavenumber concepts. She will be concerned
about the reciprocal wavenumber (wavelength) of the radio signals that will
be broadcast. She will be concerned with the lengths of the active elements
in the antenna system, and the distance between active elements if she
chooses to use an array of active elements.
Basic concepts
I will leave the radio station scenario at this point and discuss some more
basic concepts. I will return to the radio station scenario later. We will need
to start with simpler things and work our way up to the radio station
scenario.
A one-dimensional space
Just to get us started down the right path, we will temporarily constrain
space to have only one dimension. We will discuss the propagation of
waves along a taut wire, as well as the measurement of the waves
propagating along that wire.
Assume that a wire is fastened at both ends, is fairly taut, and is suspended
between two walls so that it is free to move up and down only. Assume that
we attach two sensors to the wire, one meter apart, and that each of these
sensors is capable of generating an electrical signal that is proportional to
the vertical displacement of the wire at the point where the sensor is
attached. If the wire goes up, the sensor generates a positive signal. If the
wire goes down, the sensor generates a negative signal.
Standing waves
There are two ways that we can approach this analysis. If the wire is very
long, we can think in terms of a deformation pulse that propagates along the
wire passing by our sensors once and only once. This would fall into the
category of transient analysis .
On the other hand, if the wire is shorter, we can think in terms of vibrating
one end of the wire in such a way that a standing wave will develop on the
wire. This is probably the easier of the two approaches to understand
because you may have created standing waves on a rope as a child.
On the other hand, if the two sensors were exactly one-half wavelength
apart, one would be going up when the other is going down, and the
electrical output from one would cancel the electrical output from the other.
In space processing terminology, this would be referred to as a null point .
Now consider what would happen if you were to leave the two sensors in
the same locations as before and do something to the wire to change the
wavelength of the standing wave. The output from the sum of the two
sensors would range from zero to maximum as the wavelength relative to
the sensor separation varies from one-half wavelength to one full
wavelength. For a one-half wavelength separation, the output would be
zero. For a full wavelength separation, the output would be at its maximum.
A two-element array
We could refer to our two sensors as a two-element array , and we could
refer to the output produced by the sum of the two sensors as the response
of the array. We could plot the response versus wavelength. However, in the
same sense that it is more common to plot the response of electrical filters
versus frequency (instead of period) , it is probably most common to plot
the response of arrays versus wavenumber (instead of wavelength) .
Therefore, in this module, we will plot the response of the array versus
wavenumber.
This is where Fourier transforms come into play. We could compute the
response of our one-dimensional array for waves propagating along the
length of the wire by treating the elements of the array as samples in space
and performing a one-dimensional Fourier transform on the elements of the
array.
To set the stage for what we will be seeing later, Figure 2 depicts three
different two-element arrays with different spacing in the three (black and
white) images across the top of the figure. The wavenumber response for
each of the three arrays is shown in the three images in the bottom row of
images in Figure 2 .
(In Figure 2 , the array elements are represented by the white
dots on the black background.)
Plots of 3D surfaces
The images shown in Figure 2 were produced using the class named
ImgMod29 , which I explained earlier in the module titled Plotting 3D
Surfaces using Java . You can refer back to that module for a detailed
explanation of the display format. Briefly, each of the six individual images
in Figure 2 is a plot of a 3D surface, with the elevation of the surface at any
particular point being indicated by the color at that point based on the colors
on the calibration scale below each plot.
The three images in the top row of Figure 2 with the white dots
on the black background represent the array elements for each of
the three arrays. The three images in the bottom row of Figure 2
represent the wavenumber response of the corresponding arrays
in the top row.)
The lowest (algebraic) elevation in the plot is colored black. Hence the
backgrounds are black in the top three plots. The highest elevation is
colored white. The array elements are white in the top three plots.
Between black and white, the elevation is given by the color scale below
the plot with the lowest elevation on the left of the scale and the highest
elevation on the right. Thus, a green elevation is about half way between the
lowest and highest elevations. Blue elevations are near the low end. Red
elevations are near the high end. Cyan and yellow elevations fall in between
as shown by the calibration scale.
All three wavenumber plots have a maximum response at the center, which
is the zero wavenumber origin. In effect, this corresponds to infinite
wavelength. If the wavelength is infinite, it doesn't matter what the
separation between the elements is, they will all move up and down in
unison and their electrical outputs will add constructively to produce a
maximum output.
This same pair of images shows high responses at either end as indicated by
the red areas. This is the wavenumber for which the element spacing is an
exact multiple of one wavelength.
If this were a frequency spectrum analysis, we would say that the end points
are at the Nyquist folding frequency, which is one-half of the sampling
frequency. Thus, we can say that in the wavenumber domain, the end points
on the two leftmost plots are at the Nyquist folding wavenumber, which is
one-half the sampling wavenumber.
(If you are already familiar with this sort of thing, you may have
figured out that the separation between our elements in this
example is twice the sampling distance that determines the
location of the folding wavenumber.)
This array response also shows the two black areas mentioned earlier. You
can use the orange, yellow, green, and cyan locations on the calibration
scale to estimate the response of the array to different wavenumber values
between maximum and minimum.
Now consider the two images in the center of Figure 2 where the
wavenumber range is the same as the wavenumber plot on the left. The
elements for this array are separated more than the elements in the leftmost
pair of images. Again, the wavenumber response for this array has a
maximum value at the origin, which is at the center of the wavenumber
response in the lower image. In addition, this array response has a high
(red) response at four other wavenumber zones (for a total of five) , whereas
the array on the left had only three red zones. Similarly, this array has a low
response (black and blue) at four different wavenumber zones whereas the
array on the left has a low response at only two wavenumber zones.
Finally, consider the pair of images on the right where the elements are even
further apart. This array response has even more peaks and valleys than the
other two.
A wavenumber filter
The sum of the outputs from an array of sensor elements represents a form
of wavenumber filter (much as the correct combination of resistors,
capacitors, and inductors represents a frequency filter) . If we need to pass
signals having one wavenumber and to suppress signals having a different
wavenumber, we may be able to adjust the separation between the elements
so as to put a peak on the desirable wavenumber and to put a null point on
the undesirable wavenumbers.
Of course, with only two elements, we don't have very many degrees of
freedom to work with. We could exercise more control over our
wavenumber filter if we had more elements. We could do even better if we
had the ability to give each element a different weight (including a negative
weight) when the signals from all the elements are added together. Finally,
we could do even better still if we had the ability to insert a programmable
time delay (phase shift) into the output from each of the elements before
adding them together.
Now let's modify our scenario and see what we can learn in the process. We
are going to increase the size of the array from two to three elements. We
are also going to assume that we can apply amplification, sign reversal, or
both to the element output signals before adding those signals together. The
results are shown in Figure 3 . We will compare the results in Figure 3 with
the results discussed earlier in Figure 2 , so this may be a good time for you
to open another copy of this module in a separate browser window if you
haven't already done so.
All three elements in Figure 3 are weighted equally prior to summation. The
separation between the left and center elements is the same as in Figure 2 .
The separation between the center and right elements is the same as the
separation between the left and center elements.
The wavenumber response
The most noticeable thing about the wavenumber response for this three-
element array is that the central peak is narrower than the central peak for
the two-element array at the left of Figure 2 . In addition, the trough
between the central peak and the peaks at the ends is deeper, broader, and
probably flatter (although the degree of flatness is hard to determine from
this plotting format) .
Although I won't demonstrate it, I can tell you that if I were to continue
adding elements in this manner to increase the length of the array, the
central peak and the peaks at the folding wavenumbers would continue
getting narrower, and the trough between the peaks would continue getting
deeper and probably flatter.
Continuing with our three-element array scenario, let's take a look at the
center pair of images in Figure 3 and compare them with the leftmost
images in Figure 3 .
For this case, the array still contains three elements with the same spacing
as before. However, the electrical output from the center element is
amplified to make it twice as strong as the outputs from the other two
elements before the three electrical signals are added together.
Note that the array elements are no longer shown as being white
against a black background in the very top of the top-center
image. Instead, the center element is white but the other two
elements are greenish indicating different weights.
It is a little hard to tell what this does to the central peak in the wavenumber
response, but it definitely changes the shape of the response in the trough
between the peaks. Whether or not this would be a beneficial change would
depend on the problem being addressed.
Finally, take a look at the rightmost pair of images in Figure 3 . Once again,
the array contains three elements and the center element is weighted twice
as heavily as the other two. In addition, the sign of the electrical signals
from the two outer elements is inverted before the three are added together.
Note that the black color represents the smallest algebraic value.
Negative values are smaller than positive values. Therefore, the
background (which represents a weigh of zero) has changed from
black to something between green and blue. The two elements
with the negative weights are shown as black and the element
with the positive weight is shown as white.
If you consider the peaks at the ends of the wavenumber response for the
leftmost and center images in Figure 3 to each represent only half a peak
(with the other half being off the scale to the left and the right) , all three
scenarios have two complete peaks in their wavenumber responses.
However, the locations of the two peaks for the rightmost array are at
completely different wavenumber values than are the peaks for the other
two arrays. The two peaks exhibited by the rightmost array are in the
locations of the two nulls for the center array. Similarly, the null points for
the rightmost array are in the same locations as the two peaks for the center
array.
Now let's complicate things a bit by extending our array analysis into two
dimensions. Up to this point, we have assumed that our sensors were
attached to a wire that was free to move up and down only. As such, waves
impinging on the array were constrained to approach the array from one end
or the other. In this case the wavenumber was completely determined by the
wavelength of the wave.
Let's move our array of sensors from the wire to a large sheet of metal on
the top of a table. For the time being, we will still place the elements in a
line with uniform spacing. However, we will now assume that a wave can
impinge on the array from any direction along the surface of the sheet of
metal.
Imagine a piece of corrugated sheet metal or fiber glass. (Material like this
is sometimes used to build a roof on a patio.) When you look at it from one
end, it looks something like the sine wave in Figure 1 . However, if you
keep it at eye level and slowly turn it in the horizontal plane, the distance
between the peaks will appear to become shorter and shorter until finally
you don't see any peaks at all. What you see at that point is something that
appears to have the same thickness from one end to the other. This is the
view that one of our sensors sees as the wavefront of an impinging wave.
Infinite wavelength
For example, if the wave impinges on the array from a broadside direction,
all of the sensors will move up and down in unison regardless of their
separation and regardless of the actual wavelength of the wave. For this
case, the wave will appear to the array to have infinite wavelength or zero
wavenumber.
(A linear array has no ability to filter on the basis of
wavenumber for waves that impinge on the array from the
broadside direction. All waves from that direction appear to have
zero wavenumber. This will lead us later to consider the use of a
two-dimensional array.)
(In this case, I placed all five elements on adjacent points on the
space sampling grid with no spaces in between. This places them
so close together that you can't visually separate them in the
image and they appear as a white line in a black background.)
If you were to draw a circle centered on the crosshairs (axes) in the center
of the wavenumber response, the points on that circle would represent a
fixed wavenumber for a wave arriving from any direction. The value of the
response at any particular point on the circle would indicate the response of
the array to a wave having that wavenumber from that direction.
Symmetry
You would also notice quite a lot of symmetry. For example, the maximum
response occurs in two directions that are 180 degrees apart. In fact, if you
pick any direction and a given wavenumber, the response is the same for
that direction and for the direction that is 180 degrees around from that
direction.
This wouldn't be a very good design for the radio station that I described
earlier. If one of the broadside directions of the array faces northeast and the
other faces southwest, then the people who live in the northwest and
southeast directions wouldn't receive a very good signal from your
transmitter. You need a design that maximizes the power in the four
directions where the people live, and that minimizes the power in the other
directions. To accomplish that, we will need a two-dimensional array in
place of our one-dimensional linear array.
A two-dimensional array
We will achieve the desired array response by using an array having thirteen
elements in the form of a cross with very specific weighting applied to each
element prior to summation. The weighted array and the wavenumber
response of the array are shown in Figure 5 .
Figure 5. Wavenumber response of a two-dimensional array.
The center element in the array was weighted by -4.5 before summation.
(The signal from this element was amplified by a factor of 4.5 and the sign
of the signal was inverted prior to summation.)
The twelve remaining elements were weighted by a factor of 1.0 before
summation.
Once again, we can determine the wavenumber response of the array for a
given wavenumber from any direction by drawing a circle on the response
plot, centered at the origin, with the radius of the circle equal to the specific
wavenumber of interest.
Assume a wavenumber
Assume that the wavenumber of interest in our case is exactly equal to the
distance from the origin to the white spot in the upper right quadrant of the
wavenumber response plot. This white spot represents the maximum
response of the array.
Draw a circle centered on the origin having a radius that causes the circle to
go through the white spot.
The circle passes through yellow, red, and white (indicating a high
response) in the general directions of northeast, southeast, southwest, and
northwest. This means that a strong RF signal will be transmitted to the
people living along the highways in those four directions.
The circle passes through green, cyan, and blue (indicating a lower
response) in the general directions of north, south, east, and west. This
means that very little of the precious RF energy will be transmitted to the
cotton plants and the cows that live in those directions.
In any event, you have now seen one possible practical example of the use
of a 2D Fourier transform.
Radio astronomy
Perhaps the application that is most familiar to the general public (due to
widespread publicity and a very popular movie) is the Paul Allen radio
telescope used in the Search for Extraterrestrial Intelligence (SETI) .
In the past, much of this work has been done using a very large dish antenna
known as the Arecibo Radio Telescope in Puerto Rico. Efforts are now
underway involving an alternative approach that uses a large array of small
dishes instead of one large dish.
Seismology
The design and analysis of such array systems use 2D (and sometimes 3D)
Fourier transforms. Because the weights that are applied are produced by
complex frequency filters, the transform programs that are used must treat
both the space domain data and the wavenumber data as complex (instead
of being purely real as in the examples in this module) .
Sonar
A Fourier transform program used with these arrays would normally have
to be a 3D Fourier transform program capable of transforming from
complex space functions to complex wavenumber functions.
Radar
One of the reasons that sonar is typically processed using arrays has to do
with the wavelength of the signals and the operating environment. It is
usually not practical to physically move a sonar sensor large enough to do
the job in order to cause it to look in different directions. Thus arrays of
small sensors are used with the ability to steer beams electronically in order
to look in different directions.
Petroleum exploration
Image processing
While the examples described above are interesting, they are beyond the
scope of anything that I can demonstrate online. However, there are several
interesting applications using 2D Fourier transforms that I can demonstrate
online. One of those applications is image processing.
Future modules will show how to use 2D Fourier transforms for such
purposes as softening images, sharpening images, doing edge detection on
images, etc. For this application, it is satisfactory to use a 2D Fourier
transform program that assumes that the space domain data is purely real.
Therefore, the program that I will present and explain in Part 2 of this
module will make that assumption.
The deepest part of the trough in the center is relatively narrow with respect
to the folding wave numbers at the edges. However, it is somewhat broader
than the peak in the spectrum at the lower left.
The image in the upper right shows the result of convolving the space
domain surface in the upper left with the convolution operator in the upper
center. This output space domain surface has a green square area in the
center that is at the same level as the green background. In this case, green
represents an elevation of 0, which is about midway between the lowest
elevation (black) and the highest elevation (white).
Surrounding the green square is a yellow and white fence representing very
high elevations. Surrounding that fence is a black and blue fence,
representing very low elevations consisting of large negative values.
Thus, as you move from the outside to the inside of the square in the output
surface, the elevation goes from a background level of zero, to a large
negative value, followed immediately by a large positive value, followed by
zero.
Edge detection
This is one form of edge detection. The edges of the square in the input
surface have been emphasized and the flat portion of the input surface has
been deemphasized in the convolution output.
If you are familiar with digital signal processing, you will know that in
order for a space (or time) function to contain very rapid changes in value
(such as the elevation changes at the fences described above) the function
must contain significant high wavenumber (or frequency) components. That
appears to be the case here indicated by the red areas on the four sides of
the wavenumber spectrum.
Thus, the same results could have been produced using multiplication in the
wavenumber domain followed by an inverse Fourier transform to produce
the space domain result. Convolution in the space domain is equivalent to
multiplication in the wavenumber domain and vice versa.
Hidden watermarks and trademarks
Summary
I began by explaining how the space domain and the wavenumber domain
in two-dimensional analysis are analogous to the time domain and the
frequency domain in one-dimensional analysis.
What's next?
In Part 2 of this two-part series, I will provide and explain a Java class that
can be used to perform forward and inverse 2D Fourier transforms, and can
also be used to shift the wavenumber origin from the upper left to the center
for a more pleasing plot of the wavenumber spectrum.
Test the forward and inverse 2D Fourier transforms to confirm that the
code is correct and that the transformations behave as they should
Produce wavenumber spectra for simple surfaces to help the student
gain a feel for the relationship that exists between the space domain
and the wavenumber domain
Miscellaneous
This section contains a variety of miscellaneous information.
Learn how the space domain and the wavenumber domain in two-
dimensional analysis are analogous to the time domain and the frequency
domain in one-dimensional analysis. Learn about some practical examples
showing how 2D Fourier transforms and wavenumber spectra can be
useful in solving engineering problems involving antenna arrays.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it
possible for you to purchase a pre-printed version of the PDF file, you
should be aware that some of the HTML elements in this module may not
translate well into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales
nor do I know who does receive compensation. If you purchase such a
book, please be aware that it is a copy of a module that is freely available
on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Java1491-2D Fourier Transforms using Java, Part 2
Examine the code for a Java class that can be used to perform forward and
inverse 2D Fourier transforms on 3D surfaces in the space domain. Learn how
the 2D Fourier transform behaves for a variety of different sample surfaces in
the space domain.
Table of contents
Table of contents
Preface
Viewing tip
Figures
Listings
General discussion
Preview
Sample programs
Case 0
Case 1
Case 2
Case 3
Case 4
Case 5
Case 6
Case 7
Case 8
Case 9
Case 10
Case 11
Case 12
Case 13
Preface
This is the second module in a two-part series. The first part published earlier
was titled Java1490-2D Fourier Transforms using Java, Part 1 . In this
module, I will teach you how to perform two-dimensional (2D) Fourier
transforms using Java. I will
I will present and explain two separate programs. One program consists of a
single class named ImgMod30 . The purpose of this class is to satisfy the
computational requirements for forward and inverse 2D Fourier transforms.
This class also provides a method for rearranging the spectral data into a more
useful format for plotting. The second program named ImgMod31 will be
used to test the 2D Fourier transform class, and also to illustrate the use of 2D
Fourier transforms for some well known sample surfaces.
ImgMod30.xform2D(spatialData,realSpect,
imagSpect,amplitudeSpect);
The first parameter in the above statement is a reference to an array object
containing the data to be transformed. The other three parameters refer to
array objects that will be populated with the results of the transform.
ImgMod30.inverseXform2D(realSpect,imagSpect,
recoveredSpatialData);
The first two parameters in the above statement refer to array objects
containing the complex spectral data to be transformed. The third parameter
refers to an array that will be populated with the results of the inverse
transform.
To rearrange the spectral data for plotting, execute a statement similar to the
following where the parameter refers to an array object containing the spectral
data to be rearranged.
double[][] shiftedRealSpect =
ImgMod30.shiftOrigin(realSpect);
This module will cover some technically difficult material in the general area
of Digital Signal Processing, or DSP for short. As usual, the better prepared
you are, the more likely you are to understand the material. For example, it
would be well for you to already understand the one-dimensional Fourier
transform before tackling the 2D Fourier transform. If you don't already have
that knowledge, you can learn about one-dimensional Fourier transforms by
studying the following modules :
Java1478 Fun with Java, How and Why Spectral Analysis Works
Java1482 Spectrum Analysis using Java, Sampling Frequency, Folding
Frequency, and the FFT Algorithm
Java1483 Spectrum Analysis using Java, Frequency Resolution versus
Data Length
Java1484 Spectrum Analysis using Java, Complex Spectrum and Phase
Angle
Java1485 Spectrum Analysis using Java, Forward and Inverse
Transforms, Filtering in the Frequency Domain
Java1486 Fun with Java, Understanding the Fast Fourier Transform
(FFT) Algorithm
The 2D Fourier transform has many uses. I will use the 2D Fourier transform
in several future modules involving such diverse topics as:
Viewing tip
I recommend that you open another copy of this module in a separate browser
window and use the following links to easily find and view the Figures and
Listings while you are reading about them.
Figures
Listings
General discussion
Although the space domain can be (and often is) complex, many interesting
problems, (such as photographic image processing) can be dealt with under
the assumption that the space domain is purely real. We will make that
assumption in this module. This assumption will allow us to simplify our
computations when performing the 2D Fourier transform to transform our
data from the space domain into the wavenumber domain.
Preview
I will present and explain two complete Java programs in this module. The
first program is a single class named ImgMod30 , which provides the
capability to perform forward and inverse Fourier transforms on three-
dimensional surfaces. In addition, the class provides a method that can be
used to reformat the wavenumber spectrum to make it more suitable for
display.
This class provides 2D Fourier transform capability that can be used for image
processing and other purposes. The class provides three static methods:
The class was tested using JDK 1.8 and Windows 7. The class uses the static
import capability that was introduced in J2SE 5.0. Therefore, it should not
compile using earlier versions of the compiler.
The beginning of the class and the beginning of the static method named
xform2D is shown in Listing 1 .
This method computes a forward 2D Fourier transform from the space domain
into the wavenumber domain. The number of points produced for the
wavenumber domain matches the number of points received for the space
domain in both dimensions. Note that the input data must be purely real. In
other words, the program assumes that there are no imaginary values in the
space domain. Therefore, this is not a general purpose 2D complex-to-
complex transform.
class ImgMod30{
static void xform2D(double[][] inputData,
double[][] realOut,
double[][] imagOut,
double[][] amplitudeOut){
Parameters
I won't bore you with the details as to how and why the 2D Fourier transform
does what it does. Neither will I bore you with the details of the code that
implements the 2D Fourier transform. If you understand the material that I
have previously published on Fourier transforms in one dimension, this code
and these concepts should be a straightforward extension from one dimension
to two dimensions.
imagOut[yWave][xWave ] -=
(inputData[ySpace][xSpace]*sin(2*PI*
((1.0*xWave*
xSpace/width) + (1.0*yWave*ySpace/height))))
/sqrt(width*height);
amplitudeOut[yWave][xWave] =
sqrt(
realOut[yWave][xWave] * realOut[yWave][xWave]
+
imagOut[yWave][xWave] * imagOut[yWave]
[xWave]);
}//end xSpace loop
}//end ySpace loop
}//end xWave loop
}//end yWave loop
}//end xform2D method
This method assumes that the inverse transform will produce purely real
values in the space domain. Therefore, in the interest of computational
efficiency, the method does not compute the imaginary output values.
Therefore, this is not a general purpose 2D complex-to-complex transform.
For correct results, the input complex data must match that obtained by
performing a forward transform on purely real data in the space domain.
Once again it was necessary to sacrifice indentation to force this very long
equation to be compatible with this narrow publication format and still be
readable.
Parameters
This method requires three parameters. The first two parameters are
references to 2D arrays containing the real and imaginary parts of the complex
wavenumber spectrum that is to be transformed into a surface in the space
domain.
The third parameter is a reference to a 2D array of the same size that will be
populated with the transform results.
This method deserves some explanation. The reason that this method is
needed is illustrated by Figure 1 .
The top image shows how the wavenumber spectrum is actually computed.
Knowing that the area of wavenumber space shown in the top image of Figure
1 covers one complete period of a periodic surface, the shiftOrigin method
rearranges the results (for display purposes only) to that shown in the bottom
image in Figure 1 . The origin is at the center in the bottom image of Figure 1
. The edges of the lower image in Figure 1 are the Nyquist folding wave
numbers.
double[][] output =
new double[numberOfRows]
[numberOfCols];
if(numberOfRows%2 != 0){//odd
newRows = numberOfRows +
(numberOfRows +
1)/2;
}else{//even
newRows = numberOfRows + numberOfRows/2;
}//end else
if(numberOfCols%2 != 0){//odd
newCols = numberOfCols +
(numberOfCols +
1)/2;
}else{//even
newCols = numberOfCols + numberOfCols/2;
}//end else
Listing 4. The shiftOrigin method code.
return output;
}//end shiftOrigin method
Listing 4 also signals the end of the class definition for the class named
ImgMod30.
The main method in this class reads two command line parameters and uses
them to select:
Fourteen cases
There are 14 different cases built into the program with case numbers ranging
from 0 to 13 inclusive. Each of the cases is designed such that the results of
the analysis should be known in advance by a person familiar with 2D Fourier
analysis and the wavenumber domain. Thus, these cases can be used to
confirm that the transform code was properly written.
The cases are also designed to illustrate the impact of various space domain
characteristics on the wavenumber spectrum. This information will be useful
later when analyzing the results of performing 2D transforms on photographic
images for example.
To view the images near the bottom of the stack, you must physically move
those on top to get them out of the way.
Numeric output
In addition, the program produces some numeric output on the command line
screen that may be useful in confirming the validity of the forward and
inverse transform processes. Figure 2 shows an example of the numeric
output.
height = 41
width = 41
height = 41
width = 41
2.0 1.9999999999999916
0.5000000000000002 0.49999999999999845
0.49999999999999956 0.4999999999999923
1.7071067811865475 1.7071067811865526
0.2071067811865478 0.20710678118654233
0.20710678118654713 0.20710678118655435
1.0 1.0000000000000064
-0.4999999999999997 -0.49999999999999484
-0.5000000000000003 -0.4999999999999965
The first two lines of numeric output in Figure 2 show the size of the spatial
surface for the forward transform. The second two lines show the size of the
wavenumber surface for the inverse transform.
The remaining nine lines indicate something about the quality of the forward
and inverse transforms in terms of the ability of the inverse transform to
replicate the original spatial surface. These lines also indicate something
about the correctness of the overall scaling from original input to final output.
Each of the last nine lines contains a pair of values. The first value is a sample
from the original spatial surface. The second value is a sample from the
corresponding location on the spatial surface produced by performing an
inverse transform on the wavenumber spectrum. The two values in each pair
of values should match. If they match, this indicates the probability of a valid
result.
(Note however that this is a very small sampling of the values that
make up the original and replicated spatial data and problems
could arise in areas that are not included in this small sample.)
The match is very good in the example shown above. This example is from
Case #12.
Usage:
See ImgMod29 in the earlier module titled Plotting 3D Surfaces using Java
for a definition of DisplayType .
You can terminate the program by clicking on the close button on any of the
display frames produced by the program.
The beginning of the class and the beginning of the main method is shown in
Listing 5 .
Listing 5. Beginning of the class named ImgMod31.
class ImgMod31{
The code in Listing 5 gets the input parameters and uses them to set the case
and the display format. A default case and a default display format are used if
this information is not provided by the user.
Create and save the test surface
Listing 6 calls the method named getSpatialData to create a test surface that
matches the specified case. This surface will be used for testing the transform
process.
double[][] spatialData =
getSpatialData(switchCase,rows,cols);
I will discuss the method named getSpatialData in detail later. For now, just
assume that the 2D array object referred to by spatialData contains the test
surface when this method returns.
new
ImgMod29(spatialData,3,false,displayType);
The value of false in the third parameter indicates that the axes should not be
displayed.
Figure 4 shows the results for a test surface switchCase value of 2. I will
discuss the particulars of this case in detail later.
Listing 8 performs the forward Fourier transform to transform the test surface
into the wavenumber domain.
Listing 8. Perform the forward Fourier transform.
ImgMod30.xform2D(spatialData,realSpect,
imagSpect,amplitudeSpect);
Then Listing 8 calls the static xform2D method of the ImgMod30 class to
perform the forward transform, returning the results by way of the parameters
to the method.
This image also shows the result of passing true as the third parameter causing
the red axes to be plotted on top of the spectral data.
new ImgMod29(amplitudeSpect,3,true,
displayType);
The top-right image in Figure 4 is in a format that is not particularly good for
viewing. In particular, the origin is at the top-left corner. The horizontal
Nyquist folding wavenumber is near the horizontal center of the plot. The
vertical Nyquist folding wave number is near the vertical center of the plot. It
is much easier for most people to understand the plot when the wavenumber
origin is shifted to the center of the plot with the Nyquist folding wave
numbers at the edges of the plot.
The method named shiftOrigin can be used to rearrange the data and shift the
origin to the center of the plot.
Listing 10 shifts the origin to the center of the plot and displays:
double[][] shiftedRealSpect =
ImgMod30.shiftOrigin(realSpect);
new ImgMod29(shiftedRealSpect,3,true,
displayType);
double[][] shiftedImagSpect =
ImgMod30.shiftOrigin(imagSpect);
new ImgMod29(shiftedImagSpect,3,true,
displayType);
double[][] shiftedAmplitudeSpect =
ImgMod30.shiftOrigin(amplitudeSpect);
new ImgMod29(shiftedAmplitudeSpect,3,true,
displayType);
Example displays
The real part of the shifted wavenumber spectrum is shown in the image in the
top-center of Figure 4 . The imaginary part of the shifted wavenumber
spectrum is shown in the bottom-center of Figure 4 .
The unshifted amplitude spectrum is shown in the top-right in Figure 4 and
the shifted amplitude spectrum is shown in the bottom-right image in Figure 4
. The origin has been shifted to the center in the three cases shown in the top-
center, bottom-center, and bottom-right.
double[][] recoveredSpatialData =
new double[rows]
[cols];
ImgMod30.inverseXform2D(realSpect,imagSpect,
recoveredSpatialData);
Prepare an array object to store the results
(Note that these are the original real and imaginary parts of the
complex wavenumber spectrum. They are not the versions for which
the origin has been shifted for display purposes.)
new ImgMod29(recoveredSpatialData,3,false,
displayType);
Each line of output text contains two values, and ideally the two values should
be exactly the same. Realistically, because of small computational errors in
the transform and inverse transform process, it is unlikely that the two values
will be exactly the same except in computationally trivial cases. Unless the
two values are very close, however, something probably went wrong in the
transform process and the results should not be trusted.
Now we know how to use the ImgMod30 class and the ImgMod29 class to:
Transform a purely real 3D surface from the space domain into the
wavenumber domain
Transform a complex wavenumber spectrum into a purely real surface in
the space domain
Shift the origin of the real, imaginary, and amplitude wavenumber
spectral parts to convert the data into a format that is more suitable for
plotting
Plot 3D surfaces in both domains
It is time to for us to take a look at the method named getSpatialData that can
be used to create any of fourteen standard surfaces in the space domain.
The other two input parameters specify the size of the surface that will be
produced in units of rows and columns.
double[][] spatialData =
new double[rows]
[cols];
switch(switchCase){
Listing 14 begins by creating a 2D array object of type double in which to
store the surface.
Then Listing 14 shows the beginning of a switch statement that will be used
to select the code to create a surface that matches the value of the incoming
parameter named switchCase .
Case 0
Listing 15 shows the code that is executed for a value of switchCase equal to
0.
case 0:
spatialData[0][0] = 1;
break;
This case places a single non-zero point at the origin in the space domain. The
origin is at the top-left corner. The surface produced by this case is shown in
the leftmost image in Figure 5 . The non-zero value can be seen as the small
white square in the top-left corner. In signal processing terminology, this point
can be viewed as an impulse in space. It is well known that such an impulse
produces a flat spectrum in wavenumber space.
Figure 5. An impulse in space.
You can see the impulse as the small white square in the top-left corner of
both images.
The numeric output shows that the final output surface matches the input
surface to within an error that is less than about one part in ten to the
fourteenth power. The program produces the expected results for this test
case.
If you were to go back to the equations in Listing 2 and Listing 3 and work
this case out by hand, you would soon discover that the computational
requirements are almost trivial. Most of the computation involves doing
arithmetic using values of 1 and 0. Thus, there isn't a lot of opportunity for
computational errors in this case.
Case 1
Now we are going to see a case that is more significant from a computational
viewpoint. The input surface in this case will consist of a single impulse that
is not located at the origin in the space domain. Rather, it is displaced from the
origin.
Regardless of the fact that the real and imaginary parts are not flat, the square
root of the sum of the squares of the real and imaginary parts (the amplitude)
should be the same for every point in wavenumber space for this case. Thus,
the real and imaginary parts are related in a very special way.
case 1:
spatialData[2][2] = 1;
break;
This case places a single impulse close to but not at the origin in space. This
produces a flat amplitude spectrum in wavenumber space just like in Case 0.
However, the real and imaginary parts of the spectrum are different from Case
0. They are not flat. The computations are probably more subject to errors in
this case than for Case 0.
Figure 6 shows the six output images produced by the program for a
switchCase value of 1.
The input and output surfaces showing the single impulse are in the two
leftmost images in Figure 6 . From a visual viewpoint, the output at the
bottom appears to be an exact match for the input at the top.
The real and imaginary parts
The real and imaginary parts of the wavenumber spectrum are shown in the
two center images. The real part is at the top and the imaginary part is at the
bottom.
For a single impulse in the space domain, we would expect each of these
surfaces to be a 3D sinusoidal wave (similar to a piece of corrugated sheet
metal) . That appears to be what we are seeing, with almost two full cycles of
the sinusoidal wave between the origin and the bottom right corner of the
image.
Symmetry
We know that the real part of a wavenumber spectrum resulting from the
Fourier transform of a real space function is symmetric about the origin. We
also know that the imaginary part is asymmetric about the origin.
The imaginary part is asymmetric about the origin (the centers of the
red/white and the blue/black bands appear to be equidistant from and on
opposite sides of the origin) .
The amplitude spectrum is ugly
This normalization is applied even when the distance between the highest and
lowest elevation is very small. As a result of computational errors, the
amplitude spectrum is not perfectly flat. Rather there are very small variations
from one point to the next. As a result, the colors used to plot the surface
switch among the full range of available colors even for tiny deviations from
perfect flatness.
The total error for this case is very small. The numeric output shows that the
final output surface matches the input surface to within an error that is less
than about one part in ten to the thirteenth power. The program produces the
expected results for this test case.
Case 2
Now we are going to take a look at another case for which we know in
advance generally what the outcome should be. This will allow us to compare
the outcome with our expectations to confirm proper operation of the
program.
A box on the diagonal in space
This case places a box that is one unit tall on the diagonal near the origin in
the space domain as shown in the top-left image in Figure 7 .
On the basis of prior experience, we know that the amplitude spectrum of this
surface along the horizontal and vertical axes of the wavenumber spectrum
should have a rectified sin(x)/x shape (all negative values are converted to
positive values) . We know that the peak in this amplitude spectrum should
appear at the origin in wavenumber space, and that the width of the peak
should be inversely proportional to the size of the box.
The code that constructs the space domain surface for this case is shown in
Listing 17 .
case 2:
spatialData[3][3] = 1;
spatialData[3][4] = 1;
spatialData[3][5] = 1;
spatialData[4][3] = 1;
spatialData[4][4] = 1;
spatialData[4][5] = 1;
spatialData[5][3] = 1;
spatialData[5][4] = 1;
spatialData[5][5] = 1;
break;
There isn't a lot that I can tell you about what to expect regarding the real and
imaginary parts of this spectrum, other than that they should exhibit the same
symmetry and asymmetry conditions that I described earlier for the real and
imaginary parts in general. These requirements appear to be satisfied by the
real part at the top center of Figure 7 and the imaginary part at the bottom
center of Figure 7 .
Otherwise, the shape of the real and imaginary wavenumber spectra will
depend on the location of the box in space and the size and orientation of the
box.
Note that the plotting color scheme that I used for Figure 7 is different from
any of the plots previously shown in this module. This color scheme is what I
refer to as the Color Contour scheme in the module titled Plotting 3D Surfaces
using Java .
This scheme quantizes the range from the lowest to the highest elevation into
23 levels, coloring the lowest elevation black, the highest elevation white, and
assigning very specific colors to the 21 levels in between. The colors and the
levels that they represent are shown in the calibration scales under the plots in
Figure 7 . The lowest elevation is on the left end of the calibration scale. The
highest elevation is on the right end of the calibration scale.
As before, the wavenumber amplitude spectrum with the origin in the top-left
corner is shown in the top-right image in Figure 7 . The amplitude spectrum
with the origin shifted to the center is shown in the lower right image in
Figure 7 .
If you were to use the calibration scale to convert the colors along the
horizontal and vertical axes in the lower right image into numeric values, you
would find that they approximate a rectified sin(x)/x shape as expected.
The numeric output for this case isn't very useful because none of the samples
for which numeric data is provided fall within the square. However, because
the real and imaginary parts exhibit the correct symmetry , , the shape of the
amplitude spectrum is generally what we expect, and the output from the
inverse Fourier transform appears to match the original input causes us to
conclude that the program is working properly in this case.
Case 3
This case places a raised box at the top near the origin in the space domain,
but the box is not on the diagonal as it was in Case 2.
(See the top-left image in Figure 8 for the new location of the box.)
Figure 8. Graphic output for Case 3.
As long as the size and the orientation of the box doesn't change, the
wavenumber amplitude spectrum should be the same as Case 2 regardless of
the location of the box in space. Since the size and orientation of this box is
the same as in Case 2, the amplitude spectrum for this case should be the
same as for Case 2.
The real and imaginary parts of the spectrum may change
However, the real and imaginary parts (or the phase) change as the location of
the box changes relative to the origin in space.
A hypothetical example
If you compare Figure 8 with Figure 7 , you will see that the amplitude
spectrum is the same for both surfaces despite the fact that the box is in a
different location in each of the two surfaces. However, the real and imaginary
parts of the spectrum in Figure 8 are considerably different from the real and
imaginary parts of the spectrum in Figure 7 .
The code that was used to create the surface for this case is straightforward.
You can view that code in Listing 22 near the end of the module.
Case 4
This case draws a short line containing eight points along the diagonal from
top-left to lower right in the space domain. You can view this surface in the
top-left image in Figure 9 . You can view the code that generated this surface
in Listing 22 near the end of the module.
We would expect the amplitude spectrum when viewed along any line in
wavenumber space perpendicular to the line in space to have a constant value.
The shape of the amplitude spectrum shown in the lower right image in Figure
9 agrees with our expectations. Although not shown here, if we were to make
the line of points longer, the width of the peak in the rectified sin(x)/x would
become narrower. If we were to make the line of points shorter, the peak
would become wider, as we will demonstrate in Case 5.
Our expectations regarding symmetry and asymmetry for the real and
imaginary parts shown in the center images of Figure 9 are borne out. The real
part is at the top center and the imaginary part is at the bottom center.
The output from the inverse Fourier transform shown in the bottom left of
Figure 9 matches the original space domain surface in the top left of Figure 9 .
Case 5
This case draws a short line consisting of only four points perpendicular to the
diagonal from top-left to lower right. This line of points is perpendicular to
the direction of the line of points in Case 4.
You can view the surface for this case in the top-left image of Figure 10 . You
can view the code that generated this surface in Listing 22 near the end of the
module.
Figure 10. Graphic output for Case 5.
If you compare Figure 10 with Figure 9 , you will see that the spectral result is
rotated ninety degrees relative to that shown for Case 4 where the line was
along the diagonal. In other words, rotating the line of points by ninety
degrees also rotated the structure in the wavenumber spectrum by ninety
degrees.
A wider peak
In addition, the line of points for Case 5 is shorter than the line of points for
Case 4 resulting in a wider peak in the rectified sin(x)/x shape for Case 5.
While the real and imaginary parts of the spectrum shown in the center of
Figure 10 are considerably different from anything that we have seen prior to
this, they still satisfy the symmetry and asymmetry conditions that we expect
for the real and imaginary parts.
The output from the inverse Fourier transform in the bottom left image in
Figure 10 matches the input surface in the top left image in Figure 10 .
Case 6
This case is considerably more complicated than the previous cases. You can
view the surface for this case in the top-left image in Figure 11 . You can view
the code that generated this surface in Listing 22 near the end of the module.
This case draws horizontal lines, vertical lines, and lines on both diagonals.
Each individual point on each line is given a value of either +1 or -1. The
weights of the individual points are adjusted so that the sum of all the weights
is 0. The weight at the point where the lines intersect is also 0.
Black is -1, white is +1
The small black squares in the top-left image in Figure 11 represent points
with a weight of -1. The small white squares represent points with a weight of
+1. The green background color represents a value of 0.
Because the sum of all the points is 0, the value of the wavenumber spectrum
at the origin must also be zero. This is indicated by the black square at the
origin in the lower right image.
This amplitude spectrum has major peaks at the folding wave number on each
of the 45-degree axes. In addition, there are minor peaks at various other
points in the spectrum.
As expected, the real and imaginary parts of the spectrum, shown in the center
of Figure 11 exhibit the required symmetry and asymmetry that I discussed
earlier.
The final output
Case 7
Now we are going to make a major change in direction. All of the surfaces
from cases 0 through 6 consisted of a few individual points located in specific
geometries in the space domain. All of the remaining points on the surface
had a value of zero. This resulted in continuous (but sampled) surfaces in the
wavenumber domain.
Now we are going to generate continuous (but sampled) surfaces in the space
domain. We will generate these surfaces as sinusoidal surfaces (similar to a
sheet of corrugated sheet metal) or the sums of sinusoidal surfaces.
In order to make these amplitude spectra easier to view, I have modified the
program to cause the square representing each point in the amplitude
spectrum to be five pixels on each side instead of three pixels on each side. To
keep the overall size of the images under control, I reduced the width and the
height of the surfaces from 41 points to 23 points.
I suspect that you have seen all the real parts, imaginary parts, and unshifted
amplitude spectra that you want to see. Therefore, at this point, I will begin
displaying only the input surface, the amplitude spectrum, and the output
surface that results from performing an inverse Fourier transform on the
complex spectrum.
The first example in this category is shown in Figure 12 . The input surface
for this example is a sinusoidal wave with a frequency of zero. This results in
a perfectly flat surface in the space domain as shown in the leftmost image in
Figure 12 . This surface is perfectly flat and featureless.
case 7:
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
spatialData[row][col] = 1.0;
}//end inner loop
}//end outer loop
break;
Once again, the total error is very small. The numeric output shows that the
final output surface matches the input surface to within an error that is less
than about one part in ten to the thirteenth power. Thus, the program produces
the expected results for this test case.
Case 8
This case draws a sinusoidal surface along the horizontal axis with one sample
per cycle.
The code that was used to produce this surface is shown in Listing 19 . This
code is typical of the code that I will be using to produce the remaining
surfaces in this module. This code is straightforward and shouldn't require
further explanation.
case 8:
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
spatialData[row][col] =
cos(2*PI*col/1);
}//end inner loop
}//end outer loop
break;
The Fourier transform of this surface produces a single peak at the origin in
the wavenumber spectrum just like in Figure 12 . I didn't provide a display of
the graphic output for this case because it looks just like the graphic output
shown for the zero frequency sinusoid in Figure 12 .
Case 9
This case draws a sinusoidal surface along the horizontal axis with two
samples per cycle as shown in the leftmost image in Figure 13 . This
corresponds to the Nyquist folding wavenumber.
The center image in Figure 13 shows the wavenumber amplitude spectrum for
this surface. The wavenumber spectrum has white peak values at the positive
and negative folding wave numbers on the right and left edges of the imaged.
The colors in between these two peaks are green, blue, and gray indicating
very low values.
Figure 13. Graphic output for Case 9.
The output from the inverse Fourier transform performed on the complex
wavenumber spectrum for this case is shown in the rightmost image in Figure
13 . The output is a good match for the input shown on the left.
You can view the code that was used to create this surface in Listing 22 near
the end of the module.
Case 10
This case draws a sinusoidal surface along the vertical axis with two samples
per cycle. Again, this is the Nyquist folding wave number but the sinusoid
appears along the vertical axis instead of appearing along the horizontal axis.
If you run this case and view the results, you will see that it replicates the
results from Case 9 except that everything is rotated by ninety degrees in both
the space domain and the wavenumber domain.
Case 11
This case draws a sinusoidal surface along the horizontal axis with eight
samples per cycle as shown in the leftmost image of Figure 14 .
For a sinusoidal surface with eight samples per cycle, we would expect the
peaks to occur in the wavenumber spectrum about one-fourth of the distance
from the origin to the folding wavenumber. Figure 14 meets that expectation.
The peaks are surrounded on both sides by blue and cyan colors, indicating
very low values.
The output from the inverse Fourier transformed performed on the complex
spectrum is shown in the rightmost image in Figure 14 . This output compares
very favorably with the input surface shown in the leftmost image. The
difference between the two is that the input has white vertical bands whereas
the output has red vertical bands (with a single white spot) . The above
explanation of white versus red applies here also.
You can view the code that created this surface in Listing 22 near the end of
the module.
Case 12
This case draws a sinusoidal surface on the horizontal axis with three samples
per cycle plus a sinusoidal surface on the vertical axis with eight samples per
cycle as shown by the leftmost image in Figure 15 .
Figure 15. Graphic output for Case 12.
The peaks on the vertical axis should be about one-fourth of the way between
the origin and the folding wavenumber. This appears to be the case. The peaks
on the horizontal axis should be about two-thirds of the way between the
origin and the folding wavenumber, which they also appear to be.
Inverse Fourier transform output
You can view the code that created this surface in Listing 22 near the end of
the module.
Case 13
You can view the code that created this surface in Listing 22 near the end of
the module.
Listing 20 shows the end of the method named getSpatialData and the end of
the class named ImgMod31 .
default:
System.out.println("Case must be " +
"between 0 and 13
inclusive.");
System.out.println(
"Terminating
program.");
System.exit(0);
}//end switch statement
return spatialData;
}//end getSpatialData
}//end class ImgMod31
A default case is provided in the switch statement to deal with the possibility
that the user may specify a case that is not included in the allowable limits of
0 through 13 inclusive.
The method ends by returning a reference to the array object containing the
3D surface that was created by the selected case in the switch statement.
Create some different test cases and work with them until you understand why
they produce the results that they do.
Summary
I began Part 1 of this two-part series by explaining how the space domain and
the wavenumber domain in two-dimensional analysis are analogous to the
time domain and the frequency domain in one-dimensional analysis.
In this module, I provided and explained a class that can be used to perform
forward and inverse 2D Fourier transforms, and can also be used to shift the
wavenumber origin from the top-left to the center for a more pleasing plot of
the wavenumber spectral data.
Test the forward and inverse 2D Fourier transforms to confirm that the
code is correct and that the transforms behave as they should
Produce wavenumber spectra for simple surfaces to help the student gain
a feel for the relationships that exist between the space domain and the
wavenumber domain
Listings for other programs mentioned in the module, such as Dsp029 , are
provided in other modules. Those modules are identified in the text of this
module.
Listing 21. ImgMod30.java.
/*File ImgMod30.java
Copyright 2005, R.G.Baldwin
class ImgMod30{
imagOut[yWave][xWave ] -=
(inputData[ySpace][xSpace]*sin(2*PI*((1.0*xWave*
xSpace/width) + (1.0*yWave*ySpace/height))))
/sqrt(width*height);
amplitudeOut[yWave][xWave] =
sqrt(
realOut[yWave][xWave] * realOut[yWave][xWave] +
imagOut[yWave][xWave] * imagOut[yWave][xWave]);
}//end xSpace loop
}//end ySpace loop
}//end xWave loop
}//end yWave loop
}//end xform2D method
//-------------------------------------------//
double[][] output =
new double[numberOfRows][numberOfCols];
if(numberOfRows%2 != 0){//odd
newRows = numberOfRows +
(numberOfRows + 1)/2;
}else{//even
newRows = numberOfRows + numberOfRows/2;
}//end else
if(numberOfCols%2 != 0){//odd
newCols = numberOfCols +
(numberOfCols + 1)/2;
}else{//even
Listing 21. ImgMod30.java.
newCols = numberOfCols + numberOfCols/2;
}//end else
return output;
}//end shiftOrigin method
/*File ImgMod31.java
Copyright 2005, R.G.Baldwin
height = 41
width = 41
height = 41
width = 41
2.0 1.9999999999999916
0.5000000000000002 0.49999999999999845
0.49999999999999956 0.4999999999999923
1.7071067811865475 1.7071067811865526
0.2071067811865478 0.20710678118654233
0.20710678118654713 0.20710678118655435
1.0 1.0000000000000064
-0.4999999999999997 -0.49999999999999484
-0.5000000000000003 -0.4999999999999965
class ImgMod31{
double[][] shiftedImagSpect =
ImgMod30.shiftOrigin(imagSpect);
new ImgMod29(shiftedImagSpect,3,true,
displayType);
double[][] shiftedAmplitudeSpect =
ImgMod30.shiftOrigin(amplitudeSpect);
new ImgMod29(shiftedAmplitudeSpect,3,true,
displayType);
case 1:
//This case places a single non-zero
// point near but not at the origin in
// space. This produces a flat spectrum
// in wavenumber space as in case 0.
// However, the real and imaginary parts
// of the transform are different from
// case 0 and the result is subject to
// arithmetic accuracy issues. The
// plotted flat spectrum doesn't look
// very good because the color switches
// back and forth between three values
// that are very close to together. This
// is the result of the display program
// normalizing the surface values based
// on the maximum and minimum values,
// which in this case are very close
// together.
spatialData[2][2] = 1;
break;
Listing 22. ImgMod31.java.
case 2:
//This case places a box on the diagonal
// near the origin. This produces a
// sin(x)/x shape to the spectrum with
// its peak at the origin in wavenumber
// space.
spatialData[3][3] = 1;
spatialData[3][4] = 1;
spatialData[3][5] = 1;
spatialData[4][3] = 1;
spatialData[4][4] = 1;
spatialData[4][5] = 1;
spatialData[5][3] = 1;
spatialData[5][4] = 1;
spatialData[5][5] = 1;
break;
case 3:
case 4:
//This case draws a short line along the
// diagonal from top-left to lower
// right. This results in a spectrum with
// a sin(x)/x shape along that axis and a
// constant along the axis that is
// perpendicular to that axis
spatialData[0][0] = 1;
spatialData[1][1] = 1;
spatialData[2][2] = 1;
spatialData[3][3] = 1;
spatialData[4][4] = 1;
spatialData[5][5] = 1;
spatialData[6][6] = 1;
spatialData[7][7] = 1;
break;
case 5:
//This case draws a short line
// perpendicular to the diagonal from
// top-left to lower right. The
// spectral result is shifted 90 degrees
// relative to that shown for case 4
// where the line was along the diagonal.
// In addition, the line is shorter
// resulting in wider lobes in the
// spectrum.
spatialData[0][3] = 1;
spatialData[1][2] = 1;
spatialData[2][1] = 1;
spatialData[3][0] = 1;
break;
case 6:
Listing 22. ImgMod31.java.
//This case draws horizontal lines,
// vertical lines, and lines on both
// diagonals. The weights of the
// individual points is such that the
// average of all the weights is 0.
// The weight at the point where the
// lines intersect is also 0. This
// produces a spectrum that is
// symmetrical across the axes at 0,
// 45, and 90 degrees. The value of
// the spectrum at the origin is zero
// with major peaks at the folding
// wavenumbers on the 45-degree axes.
// In addition, there are minor peaks
// at various other points as well.
spatialData[0][0] = -1;
spatialData[1][1] = 1;
spatialData[2][2] = -1;
spatialData[3][3] = 0;
spatialData[4][4] = -1;
spatialData[5][5] = 1;
spatialData[6][6] = -1;
spatialData[6][0] = -1;
spatialData[5][1] = 1;
spatialData[4][2] = -1;
spatialData[3][3] = 0;
spatialData[2][4] = -1;
spatialData[1][5] = 1;
spatialData[0][6] = -1;
spatialData[3][0] = 1;
spatialData[3][1] = -1;
spatialData[3][2] = 1;
spatialData[3][3] = 0;
spatialData[3][4] = 1;
Listing 22. ImgMod31.java.
spatialData[3][5] = -1;
spatialData[3][6] = 1;
spatialData[0][3] = 1;
spatialData[1][3] = -1;
spatialData[2][3] = 1;
spatialData[3][3] = 0;
spatialData[4][3] = 1;
spatialData[5][3] = -1;
spatialData[6][3] = 1;
break;
case 7:
//This case draws a zero-frequency
// sinusoid (DC) on the surface with an
// infinite number of samples per cycle.
// This causes a single peak to appear in
// the spectrum at the wavenumber
// origin. This origin is the top-left
// corner for the raw spectrum, and is
// at the center cross hairs after the
// origin has been shifted to the
// center for better viewing.
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
spatialData[row][col] = 1.0;
}//end inner loop
}//end outer loop
break;
case 8:
//This case draws a sinusoidal surface
// along the horizontal axis with one
// sample per cycle. This function is
// under-sampled by a factor of 2.
// This produces a single peak in the
Listing 22. ImgMod31.java.
// spectrum at the wave number origin.
// The result is the same as if the
// sinusoidal surface had zero frequency
// as in case 7..
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
spatialData[row][col] =
cos(2*PI*col/1);
}//end inner loop
}//end outer loop
break;
case 9:
//This case draws a sinusoidal surface on
// the horizontal axis with 2 samples per
// cycle. This is the Nyquist folding
// wave number. This causes a single
// peak to appear in the spectrum at the
// negative folding wave number on the
// horizontal axis. A peak would also
// appear at the positive folding wave
// number if it were visible, but it is
// one unit outside the boundary of the
// plot.
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
spatialData[row][col] =
cos(2*PI*col/2);
}//end inner loop
}//end outer loop
break;
case 10:
//This case draws a sinusoidal surface on
// the vertical axis with 2 samples per
// cycle. Again, this is the Nyquist
Listing 22. ImgMod31.java.
// folding wave number but the sinusoid
// appears along a different axis. This
// causes a single peak to appear in the
// spectrum at the negative folding wave
// number on the vertical axis. A peak
// would also appear at the positive
// folding wave number if it were
// visible, but it is one unit outside
// the boundary of the plot.
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
spatialData[row][col] =
cos(2*PI*row/2);
}//end inner loop
}//end outer loop
break;
case 11:
//This case draws a sinusoidal surface on
// the horizontal axis with 8 samples per
// cycle. You might think of this surface
// as resembling a sheet of corrugated
// roofing material. This produces
// symmetrical peaks on the horizontal
// axis on either side of the wave-
// number origin.
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
spatialData[row][col] =
cos(2*PI*col/8);
}//end inner loop
}//end outer loop
break;
case 12:
//This case draws a sinusoidal surface on
Listing 22. ImgMod31.java.
// the horizontal axis with 3 samples per
// cycle plus a sinusoidal surface on the
// vertical axis with 8 samples per
// cycle. This produces symmetrical peaks
// on the horizontal and vertical axes on
// all four sides of the wave number
// origin.
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
spatialData[row][col] =
cos(2*PI*row/8) + cos(2*PI*col/3);
}//end inner loop
}//end outer loop
break;
case 13:
//This case draws a sinusoidal surface at
// an angle of approximately 45 degrees
// relative to the horizontal. This
// produces a pair of peaks in the
// wavenumber spectrum that are
// symmetrical about the origin at
// approximately 45 degrees relative to
// the horizontal axis.
double phase = 0;
for(int row = 0; row < rows; row++){
for(int col = 0; col < cols; col++){
spatialData[row][col] =
cos(2.0*PI*col/8 - phase);
}//end inner loop
//Increase phase for next row
phase += .8;
}//end outer loop
break;
default:
Listing 22. ImgMod31.java.
System.out.println("Case must be " +
"between 0 and 13 inclusive.");
System.out.println(
"Terminating program.");
System.exit(0);
}//end switch statement
return spatialData;
}//end getSpatialData
}//end class ImgMod31
Miscellaneous
This section contains a variety of miscellaneous information.
Examine the code for a Java class that can be used to perform forward and
inverse 2D Fourier transforms on 3D surfaces in the space domain. Learn
how the 2D Fourier transform behaves for a variety of different sample
surfaces in the space domain.
Note: Disclaimers:
Financial : Although the Connexions site makes it possible for you to
download a PDF file for this module at no charge, and also makes it possible
for you to purchase a pre-printed version of the PDF file, you should be
aware that some of the HTML elements in this module may not translate well
into PDF.
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the PDF version of the module.
In the past, unknown individuals have copied my modules from cnx.org,
converted them to Kindle books, and placed them for sale on Amazon.com
showing me as the author. I neither receive compensation for those sales nor
do I know who does receive compensation. If you purchase such a book,
please be aware that it is a copy of a module that is freely available on
cnx.org and that it was made and published without my prior knowledge.
Affiliation : I am a professor of Computer Information Technology at Austin
Community College in Austin, TX.
-end-
Java1487-Convolution and Frequency Filtering in Java
Learn how to take advantage of time-domain convolution for frequency
filtering using Java.
Table of contents
Preface
Tutorial links
Legacy content and references
Legacy versus openstax presentation format
Miscellaneous
Preface
This is a cover page for a tutorial named Convolution and Frequency
Filtering in Java that was published by Prof. Baldwin around 2005. Some
things don't change with time and the contents of the tutorial are as relevant
today as when the tutorial was originally published.
Tutorial Links
You can access the tutorial in either HTML or PDF format by clicking on
one of the links below:
HTML
PDF
(See the caution below regarding HTML and the openstax presentation
format.)
The tutorial may contain internal links to other tutorials that Prof. Baldwin
has written and published somewhere on the web. Those links may, or may
not still be good. In any event, if you search cnx.org for the referenced
tutorial by title or by topic, you will probably find a clean copy of the
referenced tutorial on cnx.org.
By publishing the tutorial on the openstax CNX site, the tutorial is being
licensed under a Creative Commons Attribution 4.0 License even though
the copyright notice on the original tutorial document may be more
restrictive.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF version of the tutorial
using the PDF link provided above.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the tutorial
in its original HTML format. (The HTML format is more reliable than the
PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF version or the HTML version directly from the openstax presentation
page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Java1488-Convolution and Matched Filtering in Java
Learn how to perform matched filtering in Java. Learn how matched
filtering often makes it possible to detect properly designed signals in noisy
environments where simple frequency filtering alone cannot do the job.
Table of contents
Preface
Tutorial links
Legacy content and references
Legacy versus openstax presentation format
Miscellaneous
Preface
This is a cover page for a tutorial named Convolution and Matched
Filtering in Java that was published by Prof. Baldwin around 2005. Some
things don't change with time and the contents of the tutorial are as relevant
today as when the tutorial was originally published.
Tutorial Links
You can access the tutorial in either HTML or PDF format by clicking on
one of the links below:
HTML
PDF
(See the caution below regarding HTML and the openstax presentation
format.)
The tutorial may contain internal links to other tutorials that Prof. Baldwin
has written and published somewhere on the web. Those links may, or may
not still be good. In any event, if you search cnx.org for the referenced
tutorial by title or by topic, you will probably find a clean copy of the
referenced tutorial on cnx.org.
By publishing the tutorial on the openstax CNX site, the tutorial is being
licensed under a Creative Commons Attribution 4.0 License even though
the copyright notice on the original tutorial document may be more
restrictive.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF version of the tutorial
using the PDF link provided above.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the tutorial
in its original HTML format. (The HTML format is more reliable than the
PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF version or the HTML version directly from the openstax presentation
page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Java2350-Adaptive Filtering in Java, Getting Started
Learn how to write a Java program to adaptively design a time-delay
convolution filter with a flat amplitude response and a linear phase response
using an LMS adaptive algorithm.
Table of contents
Preface
Tutorial links
Legacy content and references
Legacy versus openstax presentation format
Miscellaneous
Preface
This is a cover page for a tutorial named Adaptive Filtering in Java,
Getting Started that was published by Prof. Baldwin around 2005. Some
things don't change with time and the contents of the tutorial are as relevant
today as when the tutorial was originally published.
Tutorial Links
You can access the tutorial in either HTML or PDF format by clicking on
one of the links below:
HTML
PDF
(See the caution below regarding HTML and the openstax presentation
format.)
The tutorial may contain internal links to other tutorials that Prof. Baldwin
has written and published somewhere on the web. Those links may, or may
not still be good. In any event, if you search cnx.org for the referenced
tutorial by title or by topic, you will probably find a clean copy of the
referenced tutorial on cnx.org.
By publishing the tutorial on the openstax CNX site, the tutorial is being
licensed under a Creative Commons Attribution 4.0 License even though
the copyright notice on the original tutorial document may be more
restrictive.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF version of the tutorial
using the PDF link provided above.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the tutorial
in its original HTML format. (The HTML format is more reliable than the
PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF version or the HTML version directly from the openstax presentation
page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Java2352-An Adaptive Whitening Filter in Java
Learn how to write an adaptive whitening filter program in Java. Also learn
how to use the whitening filter to extract wide-band signal that is corrupted
by one or more components of narrow-band noise.
Table of contents
Preface
Tutorial links
Legacy content and references
Legacy versus openstax presentation format
Miscellaneous
Preface
This is a cover page for a tutorial named An Adaptive Whitening Filter in
Java that was published by Prof. Baldwin around 2005. Some things don't
change with time and the contents of the tutorial are as relevant today as
when the tutorial was originally published.
Tutorial Links
You can access the tutorial in either HTML or PDF format by clicking on
one of the links below:
HTML
PDF
(See the caution below regarding HTML and the openstax presentation
format.)
The tutorial may contain internal links to other tutorials that Prof. Baldwin
has written and published somewhere on the web. Those links may, or may
not still be good. In any event, if you search cnx.org for the referenced
tutorial by title or by topic, you will probably find a clean copy of the
referenced tutorial on cnx.org.
By publishing the tutorial on the openstax CNX site, the tutorial is being
licensed under a Creative Commons Attribution 4.0 License even though
the copyright notice on the original tutorial document may be more
restrictive.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF version of the tutorial
using the PDF link provided above.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the tutorial
in its original HTML format. (The HTML format is more reliable than the
PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF version or the HTML version directly from the openstax presentation
page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Java2354-A General-Purpose LMS Adaptive Engine in Java
Learn how to write a general-purpose LMS adaptive engine in Java, and
how to demonstrate the use of the engine for three different adaptive
programs of increasing complexity.
Table of contents
Preface
Tutorial links
Legacy content and references
Legacy versus openstax presentation format
Miscellaneous
Preface
This is a cover page for a tutorial named A General-Purpose LMS
Adaptive Engine in Java that was published by Prof. Baldwin around 2005.
Some things don't change with time and the contents of the tutorial are as
relevant today as when the tutorial was originally published.
Tutorial Links
You can access the tutorial in either HTML or PDF format by clicking on
one of the links below:
HTML
PDF
(See the caution below regarding HTML and the openstax presentation
format.)
The tutorial may contain internal links to other tutorials that Prof. Baldwin
has written and published somewhere on the web. Those links may, or may
not still be good. In any event, if you search cnx.org for the referenced
tutorial by title or by topic, you will probably find a clean copy of the
referenced tutorial on cnx.org.
By publishing the tutorial on the openstax CNX site, the tutorial is being
licensed under a Creative Commons Attribution 4.0 License even though
the copyright notice on the original tutorial document may be more
restrictive.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF version of the tutorial
using the PDF link provided above.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the tutorial
in its original HTML format. (The HTML format is more reliable than the
PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF version or the HTML version directly from the openstax presentation
page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Java2356-An Adaptive Line Tracker in Java
Learn how to use a general-purpose LMS adaptive engine to write an
adaptive spectral line tracker in Java.
Table of contents
Preface
Tutorial links
Legacy content and references
Legacy versus openstax presentation format
Miscellaneous
Preface
This is a cover page for a tutorial named An Adaptive Line Tracker in Java
that was published by Prof. Baldwin around 2005. Some things don't
change with time and the contents of the tutorial are as relevant today as
when the tutorial was originally published.
Tutorial Links
You can access the tutorial in either HTML or PDF format by clicking on
one of the links below:
HTML
PDF
(See the caution below regarding HTML and the openstax presentation
format.)
The tutorial may contain internal links to other tutorials that Prof. Baldwin
has written and published somewhere on the web. Those links may, or may
not still be good. In any event, if you search cnx.org for the referenced
tutorial by title or by topic, you will probably find a clean copy of the
referenced tutorial on cnx.org.
By publishing the tutorial on the openstax CNX site, the tutorial is being
licensed under a Creative Commons Attribution 4.0 License even though
the copyright notice on the original tutorial document may be more
restrictive.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF version of the tutorial
using the PDF link provided above.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the tutorial
in its original HTML format. (The HTML format is more reliable than the
PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF version or the HTML version directly from the openstax presentation
page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Java2358-Adaptive Identification and Inverse Filtering using Java
Learn how to write a Java program that illustrates adaptive identification
filtering and adaptive inverse filtering. Exercise the program for different
scenarios.
Table of contents
Preface
Tutorial links
Legacy content and references
Legacy versus openstax presentation format
Miscellaneous
Preface
This is a cover page for a tutorial named Adaptive Identification and
Inverse Filtering using Java that was published by Prof. Baldwin around
2006. Some things don't change with time and the contents of the tutorial
are as relevant today as when the tutorial was originally published.
Tutorial Links
You can access the tutorial in either HTML or PDF format by clicking on
one of the links below:
HTML
PDF
(See the caution below regarding HTML and the openstax presentation
format.)
The tutorial may contain internal links to other tutorials that Prof. Baldwin
has written and published somewhere on the web. Those links may, or may
not still be good. In any event, if you search cnx.org for the referenced
tutorial by title or by topic, you will probably find a clean copy of the
referenced tutorial on cnx.org.
By publishing the tutorial on the openstax CNX site, the tutorial is being
licensed under a Creative Commons Attribution 4.0 License even though
the copyright notice on the original tutorial document may be more
restrictive.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF version of the tutorial
using the PDF link provided above.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the tutorial
in its original HTML format. (The HTML format is more reliable than the
PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF version or the HTML version directly from the openstax presentation
page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Java2360-Adaptive Noise Cancellation using Java
Learn how to use a general-purpose LMS adaptive engine to write a Java
program that illustrates the use of adaptive filtering for noise cancellation.
Table of contents
Preface
Tutorial links
Legacy content and references
Legacy versus openstax presentation format
Miscellaneous
Preface
This is a cover page for a tutorial named Adaptive Noise Cancellation
using Java that was published by Prof. Baldwin around 2006. Some things
don't change with time and the contents of the tutorial are as relevant today
as when the tutorial was originally published.
Tutorial Links
You can access the tutorial in either HTML or PDF format by clicking on
one of the links below:
HTML
PDF
(See the caution below regarding HTML and the openstax presentation
format.)
The tutorial may contain internal links to other tutorials that Prof. Baldwin
has written and published somewhere on the web. Those links may, or may
not still be good. In any event, if you search cnx.org for the referenced
tutorial by title or by topic, you will probably find a clean copy of the
referenced tutorial on cnx.org.
By publishing the tutorial on the openstax CNX site, the tutorial is being
licensed under a Creative Commons Attribution 4.0 License even though
the copyright notice on the original tutorial document may be more
restrictive.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF version of the tutorial
using the PDF link provided above.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the tutorial
in its original HTML format. (The HTML format is more reliable than the
PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF version or the HTML version directly from the openstax presentation
page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Java2362-Adaptive Prediction using Java
Learn how to use a Java adaptive filter to predict future values in a time
series. Discover the relationship between the properties of the time series
and the quality of the prediction.
Table of contents
Preface
Tutorial links
Legacy content and references
Legacy versus openstax presentation format
Miscellaneous
Preface
This is a cover page for a tutorial named Adaptive Prediction using Java
that was published by Prof. Baldwin around 2006. Some things don't
change with time and the contents of the tutorial are as relevant today as
when the tutorial was originally published.
Tutorial Links
You can access the tutorial in either HTML or PDF format by clicking on
one of the links below:
HTML
PDF
(See the caution below regarding HTML and the openstax presentation
format.)
The tutorial may contain internal links to other tutorials that Prof. Baldwin
has written and published somewhere on the web. Those links may, or may
not still be good. In any event, if you search cnx.org for the referenced
tutorial by title or by topic, you will probably find a clean copy of the
referenced tutorial on cnx.org.
By publishing the tutorial on the openstax CNX site, the tutorial is being
licensed under a Creative Commons Attribution 4.0 License even though
the copyright notice on the original tutorial document may be more
restrictive.
This issue should be resolved at some point in the future. In the meantime,
one of your options is to select and view the PDF version of the tutorial
using the PDF link provided above.
A second option is to click the Legacy Site link at the top of this page
(assuming that you are not already on the Legacy Site) and view the tutorial
in its original HTML format. (The HTML format is more reliable than the
PDF format, particularly with regard to source code listings.)
Later, when the issue mentioned above is resolved, you can select either the
PDF version or the HTML version directly from the openstax presentation
page, whichever you prefer.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the openstax CNX site makes it possible for you to
download a PDF file for the collection that contains this module at no
charge, and also makes it possible for you to purchase a pre-printed version
of the PDF file, you should be aware that some of the HTML elements in
this module may not translate well into PDF.
You also need to know that Prof. Baldwin receives no financial
compensation from openstax CNX even if you purchase the PDF version
of the collection.
In the past, unknown individuals have copied Prof. Baldwin's modules
from cnx.org, converted them to Kindle books, and placed them for sale on
Amazon.com showing Prof. Baldwin as the author. Prof. Baldwin neither
receive compensation for those sales nor does he know who does receive
compensation. If you purchase such a book, please be aware that it is a
copy of a collection that is freely available on openstax CNX and that it
was made and published without the prior knowledge of Prof. Baldwin.
Affiliation : Prof. Baldwin is a professor of Computer Information
Technology at Austin Community College in Austin, TX.
-end-
Java2440 Understanding the Lempel-Ziv Data Compression Algorithm in
Java
Learn how to write a Java program that illustrates lossless data compression
according to the Lempel-Ziv Compression Algorithm commonly known as
LZ77. Also learn about the characteristics of the algorithm that result in
data compression.
Table of contents
Preface
Tutorial and code links
Miscellaneous
Preface
Over the years, I have published a large number of tutorials in the areas of
computer programming and digital signal processing (DSP). As I have time
available, I am converting the more significant of those tutorials into cnxml
code and re-publishing them at cnx.org .
In the meantime, this is one of the pages in a book titled Digital Signal
Processing - DSP that presents PDF versions of the original tutorials to
make them readily available for Connexions users. When I have time
available, I plan to update this tutorial and to re-publish it as a standard
page at cnx.org .
This tutorial may contain internal links to other tutorials that I have written
and published somewhere on the web. Those links may, or may not still be
good. In any event, if you search cnx.org for the tutorial by title or by topic,
you will probably find a clean copy of the referenced tutorial at cnx.org . If
not, you can probably use a Google Advanced Search to find a copy
somewhere on the web.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the Connexions website makes it possible for you to
purchase a pre-printed version of the book containing this page, please be
aware that the pre-printed version probably won't contain the contents of
the PDF file referenced above .
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the pre-printed version of the
book.
In the past, unknown individuals have copied my materials from cnx.org,
converted them to Kindle books, and have placed them for sale on
Amazon.com showing me as the author. I neither receive compensation for
those sales nor do I know who does receive compensation. If you purchase
such a book, please be aware that it is a copy of material that is freely
available on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Java2442 Understanding the Huffman Data Compression Algorithm in Java
Learn how to write a Java program that exposes the inner workings of the
Huffman lossless data compression algorithm. Apply the algorithm to
different test messages.
Table of contents
Preface
Tutorial and code links
Miscellaneous
Preface
Over the years, I have published a large number of tutorials in the areas of
computer programming and digital signal processing (DSP). As I have time
available, I am converting the more significant of those tutorials into cnxml
code and re-publishing them at cnx.org .
In the meantime, this is one of the pages in a book titled Digital Signal
Processing - DSP that presents PDF versions of the original tutorials to
make them readily available for Connexions users. When I have time
available, I plan to update this tutorial and to re-publish it as a standard
page at cnx.org .
This tutorial may contain internal links to other tutorials that I have written
and published somewhere on the web. Those links may, or may not still be
good. In any event, if you search cnx.org for the tutorial by title or by topic,
you will probably find a clean copy of the referenced tutorial at cnx.org . If
not, you can probably use a Google Advanced Search to find a copy
somewhere on the web.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the Connexions website makes it possible for you to
purchase a pre-printed version of the book containing this page, please be
aware that the pre-printed version probably won't contain the contents of
the PDF file referenced above .
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the pre-printed version of the
book.
In the past, unknown individuals have copied my materials from cnx.org,
converted them to Kindle books, and have placed them for sale on
Amazon.com showing me as the author. I neither receive compensation for
those sales nor do I know who does receive compensation. If you purchase
such a book, please be aware that it is a copy of material that is freely
available on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Java2444 Understanding the Discrete Cosine Transform in Java
Learn the basics of the Discrete Cosine Transform, which is used in many
applications, including JPEG image compression.
Table of contents
Preface
Tutorial and code links
Miscellaneous
Preface
Over the years, I have published a large number of tutorials in the areas of
computer programming and digital signal processing (DSP). As I have time
available, I am converting the more significant of those tutorials into cnxml
code and re-publishing them at cnx.org .
In the meantime, this is one of the pages in a book titled Digital Signal
Processing - DSP that presents PDF versions of the original tutorials to
make them readily available for Connexions users. When I have time
available, I plan to update this tutorial and to re-publish it as a standard
page at cnx.org .
This tutorial may contain internal links to other tutorials that I have written
and published somewhere on the web. Those links may, or may not still be
good. In any event, if you search cnx.org for the tutorial by title or by topic,
you will probably find a clean copy of the referenced tutorial at cnx.org . If
not, you can probably use a Google Advanced Search to find a copy
somewhere on the web.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the Connexions website makes it possible for you to
purchase a pre-printed version of the book containing this page, please be
aware that the pre-printed version probably won't contain the contents of
the PDF file referenced above .
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the pre-printed version of the
book.
In the past, unknown individuals have copied my materials from cnx.org,
converted them to Kindle books, and have placed them for sale on
Amazon.com showing me as the author. I neither receive compensation for
those sales nor do I know who does receive compensation. If you purchase
such a book, please be aware that it is a copy of material that is freely
available on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Java2446 Understanding the 2D Discrete Cosine Transform in Java
Learn how to use the forward two-dimensional Discrete Cosine Transform
(2D-DCT) to compute and display the wave-number spectrum of an image.
Also learn how to apply the inverse 2D-DCT to the spectral data to
reconstruct and display a replica of the original image.
Table of contents
Preface
Tutorial and code links
Miscellaneous
Preface
Over the years, I have published a large number of tutorials in the areas of
computer programming and digital signal processing (DSP). As I have time
available, I am converting the more significant of those tutorials into cnxml
code and re-publishing them at cnx.org .
In the meantime, this is one of the pages in a book titled Digital Signal
Processing - DSP that presents PDF versions of the original tutorials to
make them readily available for Connexions users. When I have time
available, I plan to update this tutorial and to re-publish it as a standard
page at cnx.org .
This tutorial may contain internal links to other tutorials that I have written
and published somewhere on the web. Those links may, or may not still be
good. In any event, if you search cnx.org for the tutorial by title or by topic,
you will probably find a clean copy of the referenced tutorial at cnx.org . If
not, you can probably use a Google Advanced Search to find a copy
somewhere on the web.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the Connexions website makes it possible for you to
purchase a pre-printed version of the book containing this page, please be
aware that the pre-printed version probably won't contain the contents of
the PDF file referenced above .
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the pre-printed version of the
book.
In the past, unknown individuals have copied my materials from cnx.org,
converted them to Kindle books, and have placed them for sale on
Amazon.com showing me as the author. I neither receive compensation for
those sales nor do I know who does receive compensation. If you purchase
such a book, please be aware that it is a copy of material that is freely
available on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-
Java2448 Understanding the 2D Discrete Cosine Transform in Java, Part 2
Learn how to sub-divide an image before applying a forward and inverse
2D-Discrete Cosine Transform similar to the way it is done in the JPEG
image compression algorithm. Also learn some of the theory behind and
some of the reasons for sub-dividing images, such as improved speed.
Table of contents
Preface
Tutorial and code links
Miscellaneous
Preface
Over the years, I have published a large number of tutorials in the areas of
computer programming and digital signal processing (DSP). As I have time
available, I am converting the more significant of those tutorials into cnxml
code and re-publishing them at cnx.org .
In the meantime, this is one of the pages in a book titled Digital Signal
Processing - DSP that presents PDF versions of the original tutorials to
make them readily available for Connexions users. When I have time
available, I plan to update this tutorial and to re-publish it as a standard
page at cnx.org .
This tutorial may contain internal links to other tutorials that I have written
and published somewhere on the web. Those links may, or may not still be
good. In any event, if you search cnx.org for the tutorial by title or by topic,
you will probably find a clean copy of the referenced tutorial at cnx.org . If
not, you can probably use a Google Advanced Search to find a copy
somewhere on the web.
Miscellaneous
This section contains a variety of miscellaneous information.
Note: Disclaimers:
Financial : Although the Connexions website makes it possible for you to
purchase a pre-printed version of the book containing this page, please be
aware that the pre-printed version probably won't contain the contents of
the PDF file referenced above .
I also want you to know that, I receive no financial compensation from the
Connexions website even if you purchase the pre-printed version of the
book.
In the past, unknown individuals have copied my materials from cnx.org,
converted them to Kindle books, and have placed them for sale on
Amazon.com showing me as the author. I neither receive compensation for
those sales nor do I know who does receive compensation. If you purchase
such a book, please be aware that it is a copy of material that is freely
available on cnx.org and that it was made and published without my prior
knowledge.
Affiliation : I am a professor of Computer Information Technology at
Austin Community College in Austin, TX.
-end-