|

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.
| You must Sign In to use this message board. |
|
| | Msgs 1 to 25 of 67 (Total in Forum: 67) (Refresh) | FirstPrevNext |
|
 |
|
|
I just had an error when trying to resize a GIF and an exception was thrown because it is impossible to call SetPixel method on an indexed color image.
To fix this, just update a line in the Resize method: b = new Bitmap(nWidth, nHeight, bTemp.PixelFormat); You can use any PixelFormat enumeration value which doesnt stand for an indexed format. For instance: b = new Bitmap(nWidth, nHeight, PixelFormat.Format24bppRgb);
______________________ Mathieu Gardère wam@mathieugardere.co.uk
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
A great Article from a great Author. Thanks Christian Graus. I read many articles that you wrote, and they really rock!!
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I have been struggling to use GDI+ for bilinear resizing of 16 bit greyscale bitmaps, as used often for terrain heightmaps, but have given up because it is so buggy (I understand now that MS have admitted the greyscale handling was never completed and wasn't intended to be shipped). I tried your code (adjusted for 16bit pixel data) and it worked great! 5 from me.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hello, I have read your artical about GDI+ in www.codeproject.com And I learn many from your articals.Thank you. Now I have a question .Can you help me?
In your artical "Image Processing for Dummies with C# and GDI+ Part 5 - Displacement filters, including swirl" , there is a sphere effect . Now I want to know how to make the concave effect, which is opposed to sphere effect. Can you help me?
niannina
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
The sphere effect, and most of these effects, where really the result of my playing with the framework I came up with, and so this does not represent my trying to write the best sphere filter in the world.
What you're talking about is texture mapping, you want to map the texture to the inside of a sphere. A google on texture mapping may give you some help in this area, also a barrel filter goes some of the way to doing something like this, that may be another term that will help you. I'm sorry not to give a more detailed answer, but right now I am really flooded with work. If you still need help in the new year, feel free to ask for more.
Christian Graus - Microsoft MVP - C++
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi,
your Articles about Image Processing are very good, but i need some help about dithering Images in C#.
I mean something like reducing Images to 1bit Bmps with dithers like Floyd-Steinberg or Bayer 4x4...
Maybe you can do another article about dithering 
Greets
LordK
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Good idea. I didn't think anyone used that stuff anymore, I thought we were all on 24 bit displays and looking forward to HDR I'll consider doing something sometime soonish.
Christian
I have several lifelong friends that are New Yorkers but I have always gravitated toward the weirdo's. - Richard Stringer
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Thanks a lot 
I need 1bit bmps for a 5,2" monochrom Graphic LCD, which is external connected to my PC via USB. I also found the algorythms for the dithers from the cxImage Project, but only in C++ and my programming skill in c++ / c# is not high enough to convert it to c#.
So if you could help a bit, it would be very nice 
greetings
LordK
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
I know this post was made quite some time ago, but I thought I'd reply anyway:
Dithering is actually still necessary from time to time, even in 24 bit. For example, if your program draws a gradient and you want to avoid banding artifacts, there is no way around it, especially if the colors you are blending are very similar.
Even with 16 bit per channel and HDR, it is still an issue. For example, whenever an application displays a 16-Bit or HDR image on an 8-bit-per-channel screen (or saves an 8-bit JPEG for that matter), it should do dithering in order to get the highest quality possible. As far as I know Photoshop handles things that way.
Peter
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi,
please look at the following images created by the GDI. The jpg has the quality set to 100 and is created using the getPixel setPixel method here http://www.codeproject.com/cs/media/imageprocessing4.asp
www.zona9.com/test/s.png www.zona9.com/test/f.jpg
This is created using the save for web function of photoshop www.zona9.com/test/p.jpg
Can someone please give me some pointers of how to create images close to the photoshop quality? Or where I can find better algorithms? I need them in JPG because they are used in flash.
Thanks
David
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
The only point of this article is to show how bilinear filters work. If you just want to resize a bitmap, just get GDI+ to do it for you on it's best filtering quality setting.
The file format is irrelevant. You always deal with BMP format images in memory, then encode them to save them.
Christian
I have several lifelong friends that are New Yorkers but I have always gravitated toward the weirdo's. - Richard Stringer
|
| Sign In·View Thread·PermaLink | 4.00/5 (1 vote) |
|
|
|
 |
|
|
Hi Christian, I agree with what you are saying, but I am not resizing. I am trying to add text to an image, but the quality of the text is poor. Thats why I decided to try creating a filter. You seem quite knowledgeable about filters and I thought you might know a way of improving the quality of the GDI generated JPG's.
Another quick question, I think i'm going to write a JPEG codec. Where do you find your information on algorithms etc and do you know where I can find a JPG file specification
David
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
You're going to write a JPEG codec ? Well, there already is one, called jpeglib, from memory.
The quality of the text is poor before or after you save it ? There are modes you can set in GDI+ to improve text quality, from memory.
Christian
I have several lifelong friends that are New Yorkers but I have always gravitated toward the weirdo's. - Richard Stringer
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
In general, JPG isn't very good for small text, due to its algorithms' target for photographic images. PNG is good because it doesn't use the same type of lossy image compression. (It just uses a different kind, if at all, I think)
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi,
Ok, you just wrote this article to demonstrate the bilinear filtering, but I was telling myself that you should inverse your "for" loops, to bring the "y" BEFORE "x" in the looping process. That's the way image is stored in memory.
Just do :
for (int y = 0; y < b.Height; ++y) for (int x = 0; x < b.Width; ++x)
instead of :
for (int x = 0; x < b.Width; ++x) for (int y = 0; y < b.Height; ++y)
and so the buffer will be read in a linear way...
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Well, seeing as I use Get/SetPixel to simplify the code, and make the bilinear filtering code clearer, how on earth would it make any difference ?
Christian
I have several lifelong friends that are New Yorkers but I have always gravitated toward the weirdo's. - Richard Stringer
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
 |
|
|
I'm not sure what you're trying to say ? GetPixel is slow, I note that in the article, and all my other articles do not use it for that reason. I also note in this article that I used it to simplify the other code, to make the code that does the filtering stand out. Because of this, the order in which I iterate through pixels is totally irrelevant, it will always be the same speed, and slower than direct pixel access.
Christian
I have several lifelong friends that are New Yorkers but I have always gravitated toward the weirdo's. - Richard Stringer
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
The order is actually relevant...
GetPixel is slow because it has out of bounds protection code. Accessing images against scanline order is slow because of the way modern processors work. They read a chunk of memory (slow) into a cache close to the CPU (fast). Next time you try to read from memory your computer first looks in the cache. If it finds the memory address cached it doesn't have to access the slow memory, thus everything becomes a lot faster. Depending on the size of the cache (typically between 512kb and 2mb for different level caches) they can fit more or less data into it. If you read in scanorder (row-by-row) you guarantee that the cache needs to be refreshed the least number of times. When you read against scanorder (column-by-column) it can happen that the cache needs to be refreshed every couple of pixels. This is very bad for performance.
For a demo article it might not matter much, but it is always good practice to keep these things in mind, and, all other things being equal (as you said: why should it matter), do it row-by-row instead.
|
| Sign In·View Thread·PermaLink | 3.63/5 (6 votes) |
|
|
|
 |
|
|
Should this code handle downsizing of images correctly? I don't think it does...let me explain: If you (for example) want to rescale an image from 640*480 to 320*240, nXFactor and nYFactor are both 2. They have no fractional part. This leads to fraction_x and fraction_y to be always 0 and one_minus_x and _y to be always 1. This way, only b1 counts for the color components and that means: no filtering at all. Am i right? I've implemented your algorithm in Java and that's exactly the problem i'm getting. But i really don't know what to do to handle this problem.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
This code was written to show how bilinear filters work, and not to replace the resizing that GDI+ does by itself. You obviously want to make a fraction value of .5 when downsizing to an exact factor as you are. The code as it is written will probably not do that, but it should not be hard to change.
Christian
I have several lifelong friends that are New Yorkers but I have always gravitated toward the weirdo's. - Richard Stringer
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Thanks for the quick reply. That's what i came up with too, but it's still not what i want. But after some more thinking, i ralized that bilinear filtering is not the way to go in this case. I've compared it Java's way of resampling images and i wanted to achieve the same at a faster speed and i thought they were using simple bilinear...but they don't...
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi Christian
Thanks for this great series of articles on GDI+. As a result of these articles I quickly learnt the mechanics of GDI+ (which is new to me, as is C#). I am now happily applying my newfound GDI+ skills. I've used them for a bit of rapid prototyping.
Thanks,
William Irwin
Are your IE6 web page printouts cut off on the right hand side? - Get PrintPunk.
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
Hi Christian,
What books, if any, have you used to learn about GDI+?
Jon SagaraIf you've ever watched 6-year-olds playing soccer, that's what the mainstream media is like. -- Jon Stewart My Articles
|
| Sign In·View Thread·PermaLink | 2.00/5 (1 vote) |
|
|
|
 |
|
|
No, I learned mostly by experimentation and by reading the SDK docs ( which at the time mostly said 'to be added later' ). Charles Petzold has a book on Windows programming for C#, which is about 50% GDI+ information. I'd say that's a good place to look.
Christian
I have drunk the cool-aid and found it wan and bitter. - Chris Maunder
|
| Sign In·View Thread·PermaLink | |
|
|
|
 |
|
|
General News Question Answer Joke Rant Admin
|