Downscaling using interpolation causes aliasing (jagged edges), and can be fixed using a specific type of blur on the source.
Interpolation works fine when upscaling an image, but when reducing resolution, it creates a problem because interpolation only uses the nearest two or three pixels from the larger source image. This can leave some source pixels unused in the down-scale.
Removing aliasing is also good for video compression. Sharp edges lower overall quality due to the additional bits needed to compress them. Aliasing also draws people’s attention away from the content to the edges.
The simplest case is when the original resolution is an integer multiple of the new resolution, for example, scaling from 1280×720 to 640×360. In this case, a box filter can be used.
A box filter takes all of the pixels in a “box” and averages them together.
If the target resolution is 1/2 of the original resolution, a 2×2 box filter can be used. In general, if the target resolution is 1/N the resolution of the source, then an NxN box filter can be used.
Here’s an example of an unoptimized box filter. In practice, you will want to implement this on a GPU or using a CPU’s SIMD instructions.
/*
* rgbImage and scaledImage have each pixel packed into 3 bytes.
*
* scaledImage has a width of srcWidth / N and a height of
* srcHeight / N.
*/
void boxFilter(uint8_t* rgbImage,
int srcWidth,
int srcHeight,
int N,
uint8_t* scaledImage)
{
int dstWidth = srcWidth / N;
int dstHeight = srcHeight / N;
for(int dstY = 0; dstY < dstHeight; dstY++){
for(int dstX = 0; dstX < dstWidth; dstX++){
int redSum = 0;
int greenSum = 0;
int blueSum = 0;
for(int boxX = 0; boxX < N; boxX++){
for(int boxY = 0; boxY < N; boxY++){
int srcX = dstX * N + boxX;
int srcY = dstY * N + boxY;
int srcOffset = srcY * srcWidth * 3 + srcX * 3;
uint8_t red = rgbImage[srcOffset];
uint8_t green = rgbImage[srcOffset + 1];
uint8_t blue = rgbImage[srcOffset + 2];
redSum += red;
greenSum += green;
blueSum += blue;
}
}
int dstOffset = dstY * dstWidth * 3 + dstX * 3;
int pixelCount = N * N;
scaledImage[dstOffset] = redSum / pixelCount;
scaledImage[dstOffset + 1] = greenSum / pixelCount;
scaledImage[dstOffset + 2] = blueSum / pixelCount;
}
}
}