I recently wrote one of these, and became very frustrated with the lack of straightforward code to do so. So, here's my code, hopefully to save someone else the headache.
First things first-- you need to define where pixels are located. One convention has the pixels at the intersections of lines on a grid. This convention is most commonly used in math applications, solutions of PDEs on grids, and so forth. Another convention holds that the pixels are in the space inside the intersections of the pixels; that is, the grid designates the borders of the pixels, and the interior space of the grid is the location of the intensity value of a given pixel. This convention more commonly reflects the actual nature of a CCD or CMOS imager (those used in digital cameras). The flux of light across a certain element is measured in a process that converts photons into electrons, and those electrons are then converted to a digital signal via an analog/digital converter. Thus, this second convention more accurately reflects the physical reality from which the pixel grid was derived.
The implications of the selection of convention have to do with how many pixels you will sample to determine the value of points along the line. If you choose the first convention, many common formulae for the interpolation of data on a grid (nearest neighbor, linear, bicubi, Lanczos) are all available to you in their conventional forms. If you choose the second convention, while perhaps more accurately reflecting reality, the formulae all have to be converted into this new convention. Thus, I opt for the first, if only so that I can keep with mathematical convention and not rewrite everyone's formulae.
So, once that decision is out of the way, the line itself must be drawn. Presumably, the user provides start and end points, and it is up to you to sample along the line. You may be tempted to use a Bresenham approach, especially if you come from a graphics background. However, remember that we're trying to get numbers that reflect reality, we are not drawing and creating our own reality. The Bresenham approach is useful for drawing, but not for sampling, because you lose the actual floating-space locations of pixels along the line. Not only that, the division of values by the delta-Y term requires the use of a special case when the line is drawn entirely vertically, and avoiding special cases makes programming easier.
The code to sample along the line, then, will depend on using the length of the hypotenuse to determine the change in x and y that should be performed for each pixel. The code looks like:
float dxPrime = (mCurrEndPoint.X - mCurrStartPoint.X);
float dyPrime = (mCurrEndPoint.Y - mCurrStartPoint.Y);
float hyp = (float)Math.Sqrt(dxPrime * dxPrime + dyPrime * dyPrime);
if (hyp > 0) {
float dx = dxPrime / hyp;
float dy = dyPrime / hyp;
float[] xcoord = new float[(int)Math.Ceiling(hyp)];
float[] ycoord = new float[(int)Math.Ceiling(hyp)];
xcoord[0] = (int)mCurrStartPoint.X;
ycoord[0] = (int)mCurrStartPoint.Y;
int i = 1;
for (i = 1; i < hyp; i++) {
xcoord[i] = xcoord[i - 1] + dx;
ycoord[i] = ycoord[i - 1] + dy;
}
}
Now that you've sampled, you need to get intensity values along those lines. There are three approaches.
Nearest neighbor (just choose the closest pixel as the value to use, cheapest in time and writing, worst accuracy):
mLineVals = new float[(int)Math.Ceiling(hyp)];
int xval, yval;
float a, b;
float c, d, e, f, g, h; //for bicubic
for (i = 0; i < hyp; i++) {
xval = (int)Math.Floor(xcoord[i]+0.5f);
yval = (int)Math.Floor(ycoord[i]+0.5f);
mLineVals[i] = inImage.GetPixelAt(xval, yval);
}
Linear Interpolation (chooses the four closest pixels as the values to use, second cheapest in time and a bit more accurate):
mLineVals = new float[(int)Math.Ceiling(hyp)];
int xval, yval;
float a, b;
float c, d, e, f, g, h; //for bicubic
for (i = 0; i < hyp; i++) {
xval = (int)Math.Floor(xcoord[i]);
yval = (int)Math.Floor(ycoord[i]);
a = xcoord[i] - (float)xval;
b = ycoord[i] - (float)yval;
if (xval >= 0 && xval+1 < inImage.XSize &&
yval >= 0 && yval+1 < inImage.YSize) {
mLineVals[i] =
(1 - a) * (1 - b) * inImage.GetPixelAt(xval, yval) +
a * (1 - b) * inImage.GetPixelAt(xval + 1, yval) +
(1 - a) * b * inImage.GetPixelAt(xval, yval + 1) +
a * b * inImage.GetPixelAt(xval + 1, yval + 1);
}
}
Bicubic interpolation (chooses the 16 closest pixels as the values to use, most expensive in time but still fast enough on modern hardware, and the smoothest of all, and what I use):
mLineVals = new float[(int)Math.Ceiling(hyp)];
int xval, yval;
float a, b;
float c, d, e, f, g, h; //for bicubic
for (i = 0; i < hyp; i++) {
xval = (int)Math.Floor(xcoord[i]);
yval = (int)Math.Floor(ycoord[i]);
a = xcoord[i] - (float)xval;
b = ycoord[i] - (float)yval;
c = 1.0f - a;
d = 1.0f - b;
e = a + 1.0f;//backwards distance is a + 1 pixel
f = b + 1.0f;
g = 2.0f - a;//forwards distance is 2 pixels - a;
h = 2.0f - b;
if (xval-1 >= 0 && xval + 2 < inImage.XSize &&
yval-1 >= 0 && yval + 2 < inImage.YSize) {
mLineVals[i] = //inImage.GetPixelAt(xval, yval);
(1 - 2 * a * a + a * a * a) * (1 - 2 * b * b + b * b * b) * inImage.GetPixelAt(xval, yval) +
(1 - 2 * c * c + c * c * c) * (1 - 2 * b * b + b * b * b) * inImage.GetPixelAt(xval + 1, yval) +
(1 - 2 * a * a + a * a * a) * (1 - 2 * d * d + d * d * d) * inImage.GetPixelAt(xval, yval + 1) +
(1 - 2 * c * c + c * c * c) * (1 - 2 * d * d + d * d * d) * inImage.GetPixelAt(xval + 1, yval + 1) +
//top line
(4 - 8 * e + 5 * e * e - e * e * e) * (4 - 8 * f + 5 * f * f - f * f * f) * inImage.GetPixelAt(xval - 1, yval - 1) +
(1 - 2 * a * a + a * a * a) * (4 - 8 * f + 5 * f * f - f * f * f) * inImage.GetPixelAt(xval, yval - 1) +
(1 - 2 * c * c + c * c * c) * (4 - 8 * f + 5 * f * f - f * f * f) * inImage.GetPixelAt(xval + 1, yval - 1) +
(4 - 8 * g + 5 * g * g - g * g * g) * (4 - 8 * f + 5 * f * f - f * f * f) * inImage.GetPixelAt(xval + 2, yval - 1) +
//bottom line
(4 - 8 * e + 5 * e * e - e * e * e) * (4 - 8 * h + 5 * h * h - h * h * h) * inImage.GetPixelAt(xval - 1, yval + 2) +
(1 - 2 * a * a + a * a * a) * (4 - 8 * h + 5 * h * h - h * h * h) * inImage.GetPixelAt(xval, yval + 2) +
(1 - 2 * c * c + c * c * c) * (4 - 8 * h + 5 * h * h - h * h * h) * inImage.GetPixelAt(xval + 1, yval + 2) +
(4 - 8 * g + 5 * g * g - g * g * g) * (4 - 8 * h + 5 * h * h - h * h * h) * inImage.GetPixelAt(xval + 2, yval + 2) +
//left side
(4 - 8 * e + 5 * e * e - e * e * e) * (1 - 2 * b * b + b * b * b) * inImage.GetPixelAt(xval - 1, yval) +
(4 - 8 * e + 5 * e * e - e * e * e) * (1 - 2 * d * d + d * d * d) * inImage.GetPixelAt(xval - 1, yval + 1) +
//right side
(4 - 8 * g + 5 * g * g - g * g * g) * (1 - 2 * b * b + b * b * b) * inImage.GetPixelAt(xval + 2, yval) +
(4 - 8 * g + 5 * g * g - g * g * g) * (1 - 2 * d * d + d * d * d) * inImage.GetPixelAt(xval + 2, yval + 1);
}
}
(there's a fourth, Lanczos, but I'm ignoring it, because bicubic is good enough, I think).
And there you have it. From there, you need to be able to draw the line profile. However, that drawing code is specific to each platform. To do it in C#, I would have a class that inherits from PictureBox and overwrites the draw routine, like so:
private void JustPaint() {
//start from 1% of the edge and go to 99% of the edge
//round down for the start, up for the end, but want to make sure leave some space
if (mValues == null) return; //no drawing yet
if (mValues.Length == 0) return; //no drawing yet
if (Image == null || mOldWidth != Width || mOldHeight != Height)
Image = new Bitmap(Width, Height);
Graphics g = Graphics.FromImage(Image);
mOldWidth = Width;
mOldHeight = Height;
int theStart = (int)((float)Width * 0.01f);
int theEnd = (int)((float)Width * 0.99f + 0.5f);
int theMaxHeight = (int)((float)Height * 0.01f);
int theMinHeight = (int)((float)Height * 0.99f);
float theHeightRange = (int)(theMinHeight - theMaxHeight);
g.Clear(Color.White);
//rescale the values to be between 0 and 1
//note that mRescale is set with mLineVals, so that when this code
//is called during redraws that are not relevant to line moving, less work is done
//the x step is from theStart to theEnd, scaled by the length of the line.
//clearly, we drop some points for long lines.
//so, we have to get a sampling rate from mRescaled.length vs theEnd-theStart
float theRange = theEnd - theStart;
float theSample = 1;
float theDrawStep = 1;
if (theRange > mValues.Length) {
theSample = 1.0f;
theDrawStep = (float)theRange / (float)mValues.Length;
} else {
theSample = (float)mValues.Length / (float)theRange;
theDrawStep = 1.0f; //every pixel gets hit
}
float i = theSample;
float theDrawStart = theStart;
float theDrawSpot = (theStart) + (theDrawStep);
int intDrawStart = (int)(theDrawStart + 0.5f);
int intDrawSpot = (int)(theDrawSpot + 0.5f);
while ((int)(i+0.5f) < mValues.Length){
g.DrawLine(mBluePen,
new Point((int)(theDrawStart + 0.5f),
theMinHeight - (int)(mValues[(int)((i - theSample)+0.5f)] * theHeightRange + 0.5f)),
new Point((int)(theDrawSpot + 0.5f),
theMinHeight - (int)(mValues[(int)(i+0.5f)] * theHeightRange + 0.5f)));
theDrawStart = theDrawSpot;
theDrawSpot += theDrawStep;
i += theSample;
}
}
There you have it. Assuming you can draw a line, you can now display a graph of that line.

No comments:
Post a Comment