Click here to Skip to main content
Email Password   helpLost your password?

Introduction

Even the GDI+ methods aren't the most comprehensive. A commonly used image resizing methods are e.g. Nearest Neighbor, Bilinear or Bicubic. These are part of some "standard", but experts and freaks know that these filters doesn't provide as nice images as other filters can. In some software products (especially specialized software for astronomical and medical imaging), we can see filters with strange names like Hermite, Mitchell or Lanczos. These filters works with reference to image's frequency spectra or edges. One of the most unpleasant artifacts is the Moir� effect which can appear while scaling detailed image down. In order to prevent these artifacts, enhance edges and reduce pixelation - more complex filters were designed.

In spite of GDI+, filters have big problems with Moir� patterns, they make some artifacts on image borders (it's not an error and can be eliminated, as will be discussed later). This article presents class for image resampling with more precise filters.

Background

The first mentoined artifact was the Moir� pattern which appears due to aliasing. This artifact can be seen when one shrinks images without blurring image before zooming, so the high frequencies in the image create a pattern. Samples taken from old images create new frequencies in the new image. This effect is most visible when using a filter with low radius (this filter includes too few adjacent pixels around sample from old image). These examples shows the aliasing artifact produced by a so called Nearest Neighbor filter (first image from left).

Much better quality can be obtained by using the GDI+ Low filter (second image - filter has radius 1). Even better quality is provided by the High Quality Bicubic filter (radius 2). Finally, there is the image output from filtering via Lanczos8 filter (radius 8) from the ResamplingService class. This filter has three times larger radius, so it provides highest quality image from these filters (roof tiles are more visible and sharper):

Screenshot - moire_nn.gifScreenshot - moire_low.gifScreenshot - moire_hqbic.gifScreenshot - moire_lanc.gif

The original roof image can be found in Wikipedia in the "Moir� pattern" article. It's hard to see, but that last image is sharper than the third one, but there is other more visible defect in the GDI+ resampled version:

Screenshot - defect.gif Screenshot - nodefect.gif

This problem can be easily solved using the ImageAttributes object in DrawImage method override (don't forget to dispose it):

System.Drawing.Imaging.ImageAttributes attr = 
    new System.Drawing.Imaging.ImageAttributes();

attr.SetWrapMode(System.Drawing.Drawing2D.WrapMode.TileFlipXY);
//...

attr.Dispose();

An alternative solution is to set the PixelOffsetMode property of the Graphics object to an appropriate value:

Graphics grfx = Graphics.FromImage(imgInput);

grfx.PixelOffsetMode = PixelOffsetMode.HighQuality;

Image Resampling with GDI+ Filters

Image resizing (or resampling) is one of the most common functions of every raster image processing tool. If you've decided for GDI+ to resize your images, you can choose from variety of filters. So called Nearest Neighbor (System.Drawing.Drawing2D.InterpolationMode.NearestNeighbor) filter is the fastest one, but it produces blocky artifacts on image stretching because this filter just copies samples from old image to a new image (no interpolation between them). On the contrary, when downsampling by factor 2, every second pixel is lost:

Screenshot - downsample_nearest_neighbor.gifScreenshot - original.gif Screenshot - upsample_nearest_neighbor.gif

Much better results can be achieved using some interpolation spline. When the image is scaled up, a space between pixels must be filled with some new pixel values. Value of the new pixel can be computed as weighted average of nearest pixel and its neighbors (weights are given by the filter). One of the simplest interpolation/resampling filter is Linear filter (InterpolationMode.Low). The weight of neighboring pixels is a linear function. If you want achieve the "best" quality, then use High Quality Bicubic (InterpolationMode.HighQualityBicubic). This interpolation includes more neighbors in every new pixel value and weights are distributed more effectively (the function is a cubic spline):

Screenshot - downsample_bicubic_gdi.gifScreenshot - original.gif Screenshot - upsample_bicubic_gdi.gif

The interpolation of the scaled image is done by image filtering, or convolution between image (intensity function) and convolution kernel. If you want closer explanation of how convolution works, see one of the excellent articles from Christian Grauss at this site. He also explains bilinear interpolation.

Image Resampling with Custom Filters

Now point our view to ResamplingService.cs. This code file contains few classes for precise resampling. The most important class is ResamplingService, whose method provides all the functionality.

There is one class representing common resampling filter:

public abstract class ResamplingFilter

When you run the resampling, a new instance of ResamplingFilter is created as well. The filter depends on the Filter property of the ResamplingService object.

A few people mentioned the fact that older version of this library could not resample images with alpha channel. So I've made it much more versatile after this feedback, primarily by using the ushort array (except GDI+ Bitmap). Benefits of this approach are listed below. To resample a GDI+ Bitmap, one must add some functionality to provide conversion between Bitmap and ushort[][,] types (the conversion routines are made for 32 bits per pixel with alpha, but can be easily extended to support arbitrary color formats).

Resampling using ResamplingService has a disadvantage in speed (it's not real-time), but you'll obtain fine results:

Screenshot - downsample_cc.gif Screenshot - original.gif Screenshot - upsample_cc.gif

In the extreme case, we can compare results on upsampling four times. The results are obtained using GDI+ HQ Bicubic filter (left), Paint Shop PRO 8 Smart Size (middle) and current implementation of Lanczos8 filter:

Screenshot - extreme_gdi.gif Screenshot - extreme_psp.gif Screenshot - extreme_lanc.gif

About the Filters

I've implemented a collection of different resampling filters. Some of them provide even better results than filters in professional imaging applications. ResamplingService supports these filters (you can add your own filter by inheriting the ResamplingFilter class and adding new filter creation in the Create method of the base class). Here is the brief description of ResamplingFilters enumeration members:

Box equivalent to Nearest Neighbor on upsampling, averages pixels on downsampling
Triangle equivalent to Low; the function can be called Tent function for its shape
Hermite use of the cubic spline from Hermite interpolation
Bell attempt to compromise between reducing block artifacts and blurring image
CubicBSpline most blurry filter (cubic Bezier spline) - known samples are just "magnets" for this curve
Lanczos3 windowed Sinc function (sin(x)/x) - promising quality, but ringing artifacts can appear
Mitchell another compromise, but excellent for upsampling
Cosine an attemp to replace curve of high order polynomial by cosine function (which is even)
CatmullRom Catmull-Rom curve, used in first image warping algorithm (did you see Terminator II ?)
Quadratic performance optimized filter - results are like with B-Splines, but using quadratic function only
QuadraticBSpline quadratic Bezier spline modification
CubicConvolution filter used in one example, its weight distribution enhances image edges
Lanczos8 also Sinc function, but with larger window, this function includes largest neighborhood

Using the Code

The project demo solution is extremely simple to understand. All you have to do is to use ResamplingService in your own project by copying ResamplingService.cs into your project directory and start using this with a few lines of code:

ResamplingService resamplingService = new ResamplingService();

resamplingService.Filter = ResamplingFilters.CubicBSpline;

ushort[][,] input = ConvertBitmapToArray((Bitmap)imgInput);

ushort[][,] output = resamplingService.Resample(input, nSize.Width, 
    nSize.Height);

Image imgCustom = (Image)ConvertArrayToBitmap(output);

This code will create a ResamplingService instance, sets its Filter property such that it will use the CubicBSpline filter, converts the input image to array, resamples the array bitmap into a new array, and finally converts the output array to image. Note that conversion methods in the sample code works only with bitmaps whose pixel format is 32bpp with alpha (4 planes).

Beyond Conventional Methods

None of the presented filters (GDI+ or other) can preserve edges, add artifcial detail or do something more on image. Somebody can ask: "And what about that expensive zooming software, that uses edge-enhancing diffusion, adaptive splines or fractals to achieve good visual quality?"

The truth is, that these technologies are licensed, but not unknown. Everyone can design some adaptive filter (actual kernel depends on current pixel neighborhood). But these filters are too slow for real-time applications (TV, DVD, games, ...). Yes, of course, it's very practical for photographers. They preserve quality before performance.

I'm not familiar with spline approaches, but I've done some investigation on fractals, because it can be used to add good-looking detail into natural images, not only keep image edges. This samples were made by using nontrivial fractal resolution enhancement technique:

Screenshot - original.gif Screenshot - upsample_fractal1.gif Screenshot - upsample_fractal2.gif

Many students on my faculty are interested in digital photography, so the fractal library will be freely available for academic purposes. This is a better choice than very expensive professional zooming software on the market. The only only difference between academic version and the professional is the ratio between quality and speed. PRO software is quick with compromise results, the scientific approach needs to sacrifice tens of minutes to zoom an average-sized image, but the quality gain is sensible (here in 400% zoom)...

Original crop of the wood in fall image:

Screenshot - fall_original.jpg

Photoshop's Bicubic filter:

Screenshot - fall_Photoshop.jpg

Genuine Fractals PrintPro - top zooming software available:

Screenshot - fall_GF.jpg

Our implementation of fractal-based zooming:

Screenshot - fall_ImagingShop.jpg

Conclusion

I hope you've got a brief look onto the resampling topic. We discussed a way how to work with your own filters. To get more information, try to look for some technical articles. You can find several other implementations of resampling methods, but be careful about its properties. I tested my class for many grotesque image sizes (like 1x2397 from 191x2 pixels) and eliminated all artifacts on edges, repairing wrong weight distributions I saw in other sources.

One programmer showed me his GUI specialized on resampling. I've discovered and removed a terrible problem, which was causing black and white stripes over all images when reducing its size by an odd factor - of course due to the wrong weight distribution. This was a week before his resampling app became commercial component. I think the presented class is OK, but if you find anything or if you have any ideas how to optimize this common filtering algorithm, I'll be happy to discuss it.

Changes in the Last Version

The code was modified and made more readable (more private access modifiers, global variables reduced, more blank lines, indentation and XML comments). The library works with ushort[][,] except Bitmap. Why ushort[][,] and why not something else like byte[,]? I've chosen this type because it can contain a bitmap with almost any thinkable color format (from black&white image to 16-bit per channel image plus alpha) and all the color formats can be handled the same way. The jagged array of ushort (ushort range is four bytes or 0-65535) can contain any number of planes. The reason for jagged array except only ushort[,] is that the resampling procedure precomputes pixel weights and then filters every plane with same weights, so this solution gives better performance on color images. The code is now 100% managed (no unsafe code blocks). The way the library is used was also changed and the filters were enumerated to be more transparent.

You must Sign In to use this message board.
 
 
Per page   
 FirstPrevNext
GeneralMath.Floor more appropriate?
kingsimba0511
2:54 3 Feb '10  
left = (int)Math.Floor(center - filter.defaultFilterRadius);
right = (int)Math.Ceiling(center + filter.defaultFilterRadius);

I think both of them should be Math.Floor

- Currahee! We stand alone together.

GeneralIt's useless for creating image thumbnails?
jossik
11:16 23 Apr '09  
I discovered that your program creates thumbnail (downsampling of image) from image even larger (about 5-10%) and worster quality (!?) than just this simple function:

private static byte[] ResizeImageFile(byte[] imageFile, int targetSize)
{
using (System.Drawing.Image oldImage = System.Drawing.Image.FromStream(new MemoryStream(imageFile)))
{
Size newSize = CalculateDimensions(oldImage.Size, targetSize);
using (Bitmap newImage = new Bitmap(newSize.Width, newSize.Height, PixelFormat.Format24bppRgb))
{
using (Graphics canvas = Graphics.FromImage(newImage))
{
canvas.SmoothingMode = SmoothingMode.AntiAlias;
canvas.InterpolationMode = InterpolationMode.HighQualityBicubic;
canvas.PixelOffsetMode = PixelOffsetMode.HighQuality;
canvas.DrawImage(oldImage, new Rectangle(new Point(0, 0), newSize));
MemoryStream m = new MemoryStream();
newImage.Save(m, ImageFormat.Jpeg);
return m.GetBuffer();
}
}
}
}

But this method is still worst for me. Thumbnails is still have a big size. Any idea?
GeneralRe: It's useless for creating image thumbnails?
Libor Tinka
12:31 23 Apr '09  
What do you mean by big size and quality? One thing is the image size and encoded file size is the other.

I think using GDI+ (Bitmap, Encoder) or WIC (BitmapFrame, BitmapEncoder) is one of the fastest way available on .NET. To obtain small thumbnails (in terms of file size), use Encoder class for JPEG format and set the Quality property to some reasonable value. Thus you can control thumbnail size.

If you want to create thumbnails from images really quickly, try FreeImage C# Wrapper or .NET WIC, where you can take advantage of loading image thumbnails directly from image metadata or from low-frequency components of JPEG/HDP (or formats providing progressive encoding) macroblocks and load image directly to smaller size. The overall quality using these methods is more than sufficient.
GeneralRe: It's useless for creating image thumbnails?
jossik
21:16 23 Apr '09  
Sorry, of course i mean a "file size". Ok, i have already found a solution. You are right i tried to use encoder class and got not bad results.

Thank you, Libor.
GeneralRe: It's useless for creating image thumbnails?
jossik
22:38 23 Apr '09  
Ok, that peace of code for creating thumbnails is perfect for me.

public static void SaveResizedImage(string physicalPath, int targetSize, int quality)
{
using (System.Drawing.Image oldImage = System.Drawing.Image.FromFile(physicalPath))
{
Size newSize = CalculateDimensions(oldImage.Size, targetSize);
using (Bitmap newImage = new Bitmap(newSize.Width, newSize.Height, PixelFormat.Format24bppRgb))
{
using (Graphics canvas = Graphics.FromImage(newImage))
{
canvas.SmoothingMode = SmoothingMode.AntiAlias;
canvas.InterpolationMode = InterpolationMode.HighQualityBicubic;
canvas.PixelOffsetMode = PixelOffsetMode.HighQuality;
canvas.DrawImage(oldImage, new Rectangle(new Point(0, 0), newSize));

ImageCodecInfo codecInfo = GetEncoderInfo("image/jpeg");
Encoder encoder = Encoder.Quality;
EncoderParameter encoderParam = new EncoderParameter(encoder, quality);
EncoderParameters encoderParams = new EncoderParameters(1);

encoderParams.Param[0] = encoderParam;

newImage.Save(physicalPath + "_thumb", ImageFormat.Jpeg);

}
}
}
}

private static ImageCodecInfo GetEncoderInfo(String mimeType)
{
int j;
ImageCodecInfo[] encoders;
encoders = ImageCodecInfo.GetImageEncoders();
for (j = 0; j < encoders.Length; ++j)
{
if (encoders[j].MimeType == mimeType)
return encoders[j];
}
return null;
}

private static Size CalculateDimensions(Size oldSize, int targetSize)
{
Size newSize = new Size();
if (oldSize.Height > oldSize.Width)
{
newSize.Width = (int)(oldSize.Width * ((float)targetSize / (float)oldSize.Height));
newSize.Height = targetSize;
}
else
{
newSize.Width = targetSize;
newSize.Height = (int)(oldSize.Height * ((float)targetSize / (float)oldSize.Width));
}
return newSize;
}


That method produces thumbnails with the best proportions of the quality, filesize and works much less slower. Or am i wrong?

Hopes it someones helpes.
Generalushort[][,]?
DooMer_MP3
12:37 19 Mar '09  
Hi, thanks for the code. I see you added a new version that handles alpha. However, I can't for the life of me figure out how to get my image data into the array you want. Can you explain this a bit? It seems like I would need to lock the bits, and go through and pull the data one pixel at a time to get it into the 2D array which would be slow. Am I missing something?
GeneralRe: ushort[][,]?
Libor Tinka
10:35 26 Mar '09  
You're right - you need LockBits (direct pixel access) to copy image into array. This is quite easy to manage. Check this tutorial:
http://www.bobpowell.net/lockingbits.htm[^]

The process is in fact very quick, but using direct pixel access for resizing is of course even faster (I've done this copying for several reasons - the one is the support for arbitrary number of color planes: e.g. CMYK and the other: simple code transition to support 96 bpp (HDR) images - both not natively supported by GDI+).

Hope this helps.
GeneralSolution for downsampling/intencity issue
Valery Karkachev
13:44 25 Feb '09  
weight = filter.GetValue((center - j - 0.5) * xScale) * xScale;

The symmecrical thing should be done for yScale.
Libor you kick ass! It's a mega-library, musthave one. Thumbs Up
GeneralI don't know if it is a mistake or not. [modified]
CPallini
1:42 5 Dec '07  
It seems you missed to divide the intensity term by the weighted sum factor contrib[i].wsum on

output[c][k, i] = (ushort)Math.Min(Math.Max(intensity, UInt16.MinValue), UInt16.MaxValue);

when filtering vertically.

I don't know if it is a mistake or a feature, since your code is my only reference about the algo.
Smile

P.S. of course a made a little test with modified code but, honestly Red faced, I cannot appreciate any difference on the rendered images.

[added]
A second test using a simpler, geometric image provided support to the bug hipothesys Sigh

Big Grin

If the Lord God Almighty had consulted me before embarking upon the Creation, I would have recommended something simpler.
-- Alfonso the Wise, 13th Century King of Castile.


modified on Thursday, December 06, 2007 5:33:23 AM

GeneralRe: I don't know if it is a mistake or not.
Libor Tinka
23:56 13 Mar '08  
Thanks alot. I'll take a look at it.
GeneralRe: I don't know if it is a mistake or not.
locklin
21:17 22 Jul '08  
I can second this bug(?).

I was frantically scanning the code to figure out what I messed up when my thumbnails were coming out with what seemed to be their color intensity bumped up 1000%. When I modified that line(line#307), the thumbnails were coming out as expected. Big Grin Thanks to Pallini for this fix.

A very big thanks to Libor for this great bit of code. Big Grin Great job!
GeneralRe: I don't know if it is a mistake or not.
CPallini
22:16 22 Jul '08  
locklin wrote:
Thanks to Pallini for this fix.

You're welcome. I'm pleased my post helped.

locklin wrote:
A very big thanks to Libor for this great bit of code. [Big Grin] Great job!

Indeed.
Smile

If the Lord God Almighty had consulted me before embarking upon the Creation, I would have recommended something simpler.
-- Alfonso the Wise, 13th Century King of Castile.

This is going on my arrogant assumptions. You may have a superb reason why I'm completely wrong.
-- Iain Clarke


[My articles]

GeneralRe: I don't know if it is a mistake or not.
kingsimba0511
2:39 3 Feb '10  
It's indeed a bug. I can verify it produce problems when scaling down.

- Currahee! We stand alone together.

GeneralVery nice work.
CPallini
6:45 30 Nov '07  
BTW There's a typo at the end of the article: ushort data type is 2 (not 4) bytes wide.
Smile

If the Lord God Almighty had consulted me before embarking upon the Creation, I would have recommended something simpler.
-- Alfonso the Wise, 13th Century King of Castile.

GeneralRe: Very nice work.
Libor Tinka
8:21 1 Dec '07  
Of course, thanks. I will correct it with addition of new articles in the near future.
QuestionImage Resize Script, for generating thumbnails??
uswebpro2
1:41 2 Nov '07  
Hello,
I'm looking for a nice Image Resize c# Script for generating thumbnails.
Could someone give me a link

thanks!

- aron

GeneralFractal Library for Testing
AndyHo
6:36 21 Aug '07  
Hello
Well done!
I have had problems when reducing images with the Thumbnail class (the quality of resizing is bad) and will test your classes soon.;)Mi interest lies in the fractal libraries (for test purposes)
Confused where can I get them to test (Academic version)
GeneralRe: Fractal Library for Testing
Libor Tinka
10:05 24 Aug '07  
Hello,

my thesis will cover another theme and I have only one finished version that is part of a digital imaging software called ImagingShop (you can download trial version on www.imagingshop.eu - fractal resolution enhancement is there present as a filter plugin).

If you are really interested in testing or improving this technology, send me an e-mail. I'm currently working on other projects, but I'm planning to reduce its time complexity.

Libor Tinka
GeneralRe: Fractal Library for Testing
Member 3719814
4:48 1 Jul '09  
might your link be broken ? I am interested in evaluating your fractal filter.

CT
GeneralHelp with aplha
Fabian von Romberg
22:02 13 Aug '07  
Hi guys, was anybody updated the code with alpha capabilities. Im tryng to accomplish it but i havent been able.

Your help will be much appreciated.

Thanks in advance,
Fabian
GeneralRe: Help with aplha
Libor Tinka
13:02 14 Aug '07  
Hi,

you pushed me to leave my projects and update this article (and code). It will take some time, before the article will be accepted, so you can see it here right now on my personal site. The download links should work.

Now it supports any standard color format you can imagine with almost zero programming effort Big Grin

I've simplified the core algorithm (it's a little bit more readable without that unsafe code stuff, a little faster and completely managed).

Use it as you wish, but don't forget about copyright and don't sell the library itself (making it part of some commercial product is not a problem).

Regards,
Libor Tinka
GeneralRe: Help with aplha
Fabian von Romberg
19:24 14 Aug '07  
Hi Libor, first of all thanks for you code. It is much appreciated. Please note that downsampling is not working properly. Seems there is some color shifting. Upsampling is working just great.

Thanks,
Fabian
GeneralRe: Help with aplha
Libor Tinka
16:41 16 Aug '07  
I'll check it asap (but at least in one week). It is most probably caused by newest changes in the code. Thanks for the remainder.

Libor
GeneralRe: Help with aplha
jeffpowers
16:58 14 Sep '07  
Libor,

This is a great library ... I'm planning to use your work in a Silverlight image resizing tool.

I'm currently trying to fix the downsample intensity-increase problem. The following edit fixes the color intensity, but doesn't apply the filters correctly and images end up looking like they were nearest-neighbor downsampled...

Change:
weight = filter.GetValue((center - j - 0.5) * xScale);
To:
weight = filter.GetValue(center - j - 0.5);

I'll keep experiementing.

Thanks for the library! Amazing work!

Jeff Powers
http://fluxcapacity.net
GeneralRe: Help with aplha
jeffpowers
17:09 14 Sep '07  
Here's a better way to hack the downsample code into working:

Change:
contrib[i].wsum += weight;

To:
contrib[i].wsum += (weight / xScale);

(and yScale for the vertical pass, accordingly)


Last Updated 16 Aug 2007 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010