Image Processing 8. Image scaling

2 min read June 23, 2025 #image processing #javascript

Let's explore a highly relevant topic in image processing: image scaling. You've likely encountered this task and may have even heard about interpolation algorithms used for scaling. This article will focus on that.

logo

Nearest-neighbor Interpolation#

When scaling using this method, we only manipulate pixels without needing to obtain the values of individual color channels. This allows images to scale very quickly. However, when downscaling - many details are lost, and when upscaling - pixels turn into large squares or rectangles.

const dx = width / newWidth;
const dy = height / newHeight;
for (let y = 0; y < newHeight; y++) {
  let srcY = Math.floor(y * dy);
  for (let x = 0; x < newWidth; x++) {
    let srcX = Math.floor(x * dx);
    dst[y * newWidth + x] = src[srcY * width + srcX];
  }
}

The original image of size width × height is scaled to newWidth × newHeight. We calculate the increments dx and dy, then for each new pixel, we find the corresponding position in the original image and assign the value.

Original (64×64) in the center, downscaled copy (32×32) on the left, and upscaled version (200×200) on the right: all-nearest.png

Bilinear Interpolation#

For each point on the image, values of four neighboring pixels are sampled. The value for each color component is calculated using a formula and assigned in the current position.

bilinear.png

const xMax = (width - 1);
const yMax = (height - 1);
const dx = (xMax + 0.5) / newWidth;
const dy = (yMax + 0.5) / newHeight;
let dstOffset = 0;
for (let i = 0; i < newHeight; i++) {
  for (let j = 0; j < newWidth; j++) {
    const x = Math.floor(dx * j);
    const y = Math.floor(dy * i);
    const xDiff = (dx * j) - x;
    const yDiff = (dy * i) - y;
    const index = y * width + x;

    const a = src[index];
    const b = (x >= xMax) ? a : src[index + 1];
    const c = (y >= yMax) ? a : src[index + width];
    const d = (y >= yMax) ? b : ((x >= xMax) ? c : (src[index + width + 1]));

    const red = interpolate(
        a & 0xff, b & 0xff,
        c & 0xff, d & 0xff,
        xDiff, yDiff);
    const green = interpolate(
        (a >> 8) & 0xff, (b >> 8) & 0xff,
        (c >> 8) & 0xff, (d >> 8) & 0xff,
        xDiff, yDiff);
    const blue = interpolate(
        (a >> 16) & 0xff, (b >> 16) & 0xff,
        (c >> 16) & 0xff, (d >> 16) & 0xff,
        xDiff, yDiff);
    const alpha = interpolate(
        (a >> 24) & 0xff, (b >> 24) & 0xff,
        (c >> 24) & 0xff, (d >> 24) & 0xff,
        xDiff, yDiff);
    
    dst[dstOffset++] = (alpha << 24) | (blue << 16) | (green << 8) | red;
  }
}

function interpolate(a, b, c, d, width, height) {
  return a * (1 - width) * (1 - height)
      + b * width * (1 - height)
      + c * (1 - width) * height
      + d * width * height;
}

Similarly, we calculate the increment and the distance between old and new positions (xDiff, yDiff), then apply the interpolate function for each color component.

all-bilinear.png

Lanczos Interpolation#

Finally, let's discuss interpolation using the Lanczos filter. This method provides even smoother results, but significantly increases processing time.

The filter can be applied with different kernel sizes (convolution kernels will be covered in a separate article). While bilinear interpolation uses 4 source pixels per new pixel, Lanczos may use 9 (3×3), 25 (5×5), 49 (7×7), etc. Sizes 2 and 3 offer the best balance of quality and speed.

lanczos.png

Result for filterSize=2:
all-lanczos.png

Finally, a Pikachu downscaled to 20×20 and then upscaled:
pika.png