Here we go again...
Well, this is the fourth installment in the series, and I thank you for sticking
around this long. I am wanting to do some groundwork for a future
article, which will be involved enough that I didn't want to add bilinear
filtering to the mix at that stage, so I'm covering it here. In a nutshell,
bilinear filtering is a method of increasing the accuracy with which we can
select pixels, by allowing the selection of pixels in between the ones we
draw. Honest!!! To illustrate it's effect, we are going to write a
resize filter first, then we are going to add a bilinear filter to see the
effect it has on the final result.
A resize filter.
If you want to resize an image arbitrarily, the easiest way to do it is to
calculate a factor for the difference between the source and destination in
both x and y axes, then use that factor to figure out which pixel on the source
image maps to the colour being placed on the destination image. Note for
this filter I step through the destination image and calculate the source
pixels from there, this ensures that no pixels in the destination image are not
filled.
SetPixel ?
Before I show you the code, you'll notice that I've chosen to use Set/GetPixel
this time around instead of getting a pointer to my bitmap data. This
does two things for me, firstly it means my code is not 'unsafe', and secondly,
it makes the code a lot simpler, which will help when we add the bilinear
filter, which does enough work without my sample being cluttered by all the
pointer lookup code that would also be required, as you will see.
The code
Here then is my code for a function that resizes a bitmap, fills it with data
from a copy that was made prior, and then returns it. Note that unlike my
other functions, I found I had to return the new Bitmap
because
when I create one of a new size, it is no longer the same bitmap that is
referred to by the 'in' parameter, and therefore I am unable to return a
bool
to indicate success.
public static Bitmap Resize(Bitmap b, int nWidth, int nHeight, bool bBilinear)
{
Bitmap bTemp = (Bitmap)b.Clone();
b = new Bitmap(nWidth, nHeight, bTemp.PixelFormat);
double nXFactor = (double)bTemp.Width/(double)nWidth;
double nYFactor = (double)bTemp.Height/(double)nHeight;
if (bBilinear)
{
}
else
{
for (int x = 0; x < b.Width; ++x)
for (int y = 0; y < b.Height; ++y)
b.SetPixel(x, y, bTemp.GetPixel((int)(Math.Floor(x * nXFactor)),
(int)(Math.Floor(y * nYFactor))));
}
return b;
}
In order to highlight the artifacts we get from such a filter, I have taken an
image of Calvin and increased the width while decreasing the height ( both by
10 pixels ) several times, to get the following:
As you can see, things start to deteriorate fairly rapidly.
Bilinear Filtering
The problem we are having above is that we are not grabbing the pixels we want a
lot of the time. If we resize an image of 100 x 100 to 160 x 110, for
example, then our X scale is 100/160, or .625. In other words, to fill
column 43, we need to look up column (43 * .625), or 26.875. Obviously,
we are not able to look up such a value, we will end up with column 27.
In this case, the difference is slight, but we can obviously end up with
decimal values including .5, right in the middle between two existing
pixels. The image above shows how such small rounding of values can
accumulate to cause image quality to deteriorate. The solution is
obviously to look up the values without rounding. How do we look up a
pixel that does not exist ? We interpolate it from the values we can look
up. By reading the values of all the surrounding pixels, and then
weighting those values according to the decimal part of the pixel value, we can
construct the value of the sub pixel. For example, in the above example,
we would multiply the values of column 26 by .875, and the values of column 27
by .125 to find the exact value required.
In order to make the example clearer, I have used GetPixel
to read
the four pixels in the area surrounding the subpixel we want to find. In
a future example I will use direct pixel access, which will be faster, but also
a lot more complex. The variable names have been chosen to help clarify
what is going on. Here is the missing code from above, the code which is
executed when bBilinear = true
.
if (bBilinear)
{
double fraction_x, fraction_y, one_minus_x, one_minus_y;
int ceil_x, ceil_y, floor_x, floor_y;
Color c1 = new Color();
Color c2 = new Color();
Color c3 = new Color();
Color c4 = new Color();
byte red, green, blue;
byte b1, b2;
for (int x = 0; x < b.Width; ++x)
for (int y = 0; y < b.Height; ++y)
{
floor_x = (int)Math.Floor(x * nXFactor);
floor_y = (int)Math.Floor(y * nYFactor);
ceil_x = floor_x + 1;
if (ceil_x >= bTemp.Width) ceil_x = floor_x;
ceil_y = floor_y + 1;
if (ceil_y >= bTemp.Height) ceil_y = floor_y;
fraction_x = x * nXFactor - floor_x;
fraction_y = y * nYFactor - floor_y;
one_minus_x = 1.0 - fraction_x;
one_minus_y = 1.0 - fraction_y;
c1 = bTemp.GetPixel(floor_x, floor_y);
c2 = bTemp.GetPixel(ceil_x, floor_y);
c3 = bTemp.GetPixel(floor_x, ceil_y);
c4 = bTemp.GetPixel(ceil_x, ceil_y);
b1 = (byte)(one_minus_x * c1.B + fraction_x * c2.B);
b2 = (byte)(one_minus_x * c3.B + fraction_x * c4.B);
blue = (byte)(one_minus_y * (double)(b1) + fraction_y * (double)(b2));
b1 = (byte)(one_minus_x * c1.G + fraction_x * c2.G);
b2 = (byte)(one_minus_x * c3.G + fraction_x * c4.G);
green = (byte)(one_minus_y * (double)(b1) + fraction_y * (double)(b2));
b1 = (byte)(one_minus_x * c1.R + fraction_x * c2.R);
b2 = (byte)(one_minus_x * c3.R + fraction_x * c4.R);
red = (byte)(one_minus_y * (double)(b1) + fraction_y * (double)(b2));
b.SetPixel(x,y, System.Drawing.Color.FromArgb(255, red, green, blue));
}
}
The result is as follows:
As you can see, this is a much better result. It looks like it has gone
through a softening filter, but it looks much better than the one above.
It is possible to get a slightly better code by going through a much more
complex process called bicubic filtering, but I do not intend to cover it,
simply because I've never done it.
What's Next
As I said above, the whole point of this article was to illustrate how
bilinear filtering works. A bilinear filter will be employed in my next
article, which will talk about the process of writing a filter from scratch to
be optimised for a particular process instead of through the sort of generic
processes we've used so far. At that point we will reimpliment the
bilinear filter to be more optimised, but hopefully this version has helped you
to understand that part of the process, so we can focus on other aspects in the
next article.