Enlarging an image means inventing pixels. How you invent them is the difference between blocks and mush.
Overview
Enlarging invents; shrinking discards
Resizing is two different problems wearing one name.
Enlarging asks for pixels that were never measured. A 4×4 image stretched to 8×8 needs 64 values where 16 exist. The extra 48 have to come from a place, and wherever they come from, they're a guess. No interpolation method adds info — the choice is only about which plausible guess to make.
Shrinking has too much data rather than too little, and the risk is throwing away the wrong parts.
Resizing and Interpolation
This module needs JavaScript: the
images are computed in the page rather than downloaded.
Worth knowing
Enlarging cannot add information. It can only decide what to put between the pixels that exist.
Nearest neighbour copies the closest pixel. Fast, exact for pixel art, blocky for everything else.
Bilinear takes a weighted average of the four surrounding pixels. Smoother, and softer.
Shrinking has the opposite problem: sampling without averaging first produces aliasing, which is why area averaging exists.
Resizing and Interpolation
What happens between the pixels, and why the answer looks different each time.
Nearest neighbour
For each output pixel, work out where it lands in the input and copy whichever input pixel is closest. That is all.
It's the fastest possible method, and it's exact in one key sense: each output value is a value that truly appeared in the input. Nothing is averaged, so nothing is invented that wasn't there.
The cost is blockiness. Neighbouring output pixels that map to the same input pixel are the same, so enlarging makes flat squares with hard steps between them. Straight edges come out as staircases.
That is often just right. Pixel art must be scaled with nearest neighbour or it stops being pixel art. A segmentation mask where pixel value 3 means "road" must be scaled with nearest neighbour. Blending 3 and 5 gives 4, which might mean "building" — a label is a name, not a quantity, and names do not mean.
Bilinear
For each output pixel, find the four input pixels nearby its spot and take a weighted mean, with weights from how close the spot is to each. Linear in one direction, then linear in the other. Hence bilinear.
Edges become smooth and the staircase goes away. The cost is softness: a blend of nearby pixels is always softer than the originals, so sharp edges turn into ramps. Enlarge by a large factor and the result is blurry, not blocky.
Switch between the two above with the downscale factor set high. The same info is present in both. Only the guessing strategy differs, and it's easy to see that neither is simply better.
Bicubic extends the idea to sixteen nearby pixels with a cubic weight function. It's sharper than bilinear because the cubic curve overshoots slightly at edges, adding a small amount of local contrast. Photo editors default to it for that reason. The overshoot is also its failure mode: it can give faint halos beside high-contrast edges.
Method
Reads
Speed
Good for
Fails at
Nearest
1 pixel
fastest
pixel art, label masks
photographs
Bilinear
4 pixels
fast
general enlargement
large factors
Bicubic
16 pixels
slower
photographs
can halo
Area
a block
fast
shrinking
not for enlarging
Shrinking is a different failure
To halve an image, it's tempting to keep each second pixel. This is wrong, and wrong in a way that looks like a bug elsewhere in the program.
The fix is to mean before sampling. Area interpolation takes the mean of each input pixel that falls inside each output pixel's footprint, so nothing is silently dropped. Gaussian blurring before downsampling reaches the same end, and is what an image pyramid does at each level.
This matters for machine learning in detail. A dataset resized with a naive-sampling routine has aliased artefacts that vary with the first detail, and a network will happily learn them.
Bilinear, worked, and why masks forbid it
Bilinear interpolation reads the four input pixels around a fractional spot and takes a distance-weighted mean — linear across, then linear down. For an output point landing at fraction (0.25, 0.75) inside a cell whose corners are 10 (top-left), 20 (top-right), 30 (bottom-left), 40 (bottom-right):
step
value
top edge: 10·0.75 + 20·0.25
12.5
bottom edge: 30·0.75 + 40·0.25
32.5
down: 12.5·0.25 + 32.5·0.75
27.5
The output 27.5 is a value that never appeared in the input. That is the whole point for a photo: it smooths the staircase that nearest neighbour would leave.
But a *label mask* must never be resized this way. If 3 means "road" and 5 means "sky", a blend of 4 might be a class that is neither. Class labels are names, not amounts, and names cannot be mixed. Use nearest neighbour for masks.
Shrinking is a third case. Keeping each second pixel can alias — a striped pattern can vanish or flip — so you need to blur first (area method), not guess between samples.
Bicubic uses sixteen nearby pixels with a cubic weight that overshoots slightly at edges. That adds a little local contrast, which is why photo editors default to it. The overshoot can also leave faint halos beside sharp edges.
Where it goes wrong with Resizing and Interpolation
Bilinear on a label mask. Blending class indices makes indices that mean a thing else. Nearest neighbour, always.
Enlarging to fix a low-detail input. Upscaling before feeding a model adds no info and costs work. Learned super-detail is a different thing fully — it hallucinates plausible detail from a prior, which is handy for viewing and dangerous for reading.
Repeated resizes. Each interpolation softens. Resize once from the first rather than in steps.
Skipping aspect ratio. Stretching to a square distorts each shape in the frame. Pad to the target aspect ratio, then resize.
Questions people ask about Resizing and Interpolation
Which interpolation method should I use? Use INTER_AREA when shrinking, INTER_LINEAR as a general default when enlarging, and INTER_CUBIC for higher quality at a higher cost. Use nearest neighbour for labels and masks.
Why does a downscaled image look jagged or show moire? Dropping pixels without filtering causes aliasing. INTER_AREA, or a blur before subsampling, removes the detail that would otherwise fold into false patterns.
Why can't I use bilinear on a segmentation mask? It blends label values, producing classes that do not exist, such as 1.5 between class 1 and class 2. Nearest neighbour copies existing labels only.
Does upscaling add detail? No. Interpolation only estimates values between existing pixels, so the image gets bigger and smoother but not sharper. Recovering detail needs learned super-resolution.
How do I resize while keeping the aspect ratio? Compute one scale factor from the target width or height and apply it to both. Pad or crop afterwards if you need a fixed shape.
Does resizing affect model accuracy? Yes, so use the same method in training and inference. A mismatch between libraries, such as PIL and OpenCV, can shift predictions.
Nearest, bilinear, and why downscaling needs a blur first
Resizing asks for pixel values at coords that don't exist. Each method is a guess. This measures three guesses against a known ground truth. Then shows the failure that catches everyone -- that shrinking an image right isn't an interpolation problem at all.
The next block examines what nearest and bilinear actually produce per-pixel, showing how nearest repeats values while bilinear smooths between them.
example_03.pyNumPy
import numpy as np
def f(y, x):
return (128 + 90 * np.sin(x * 0.55) * np.cos(y * 0.42)
+ 20 * np.sin(x * 0.2 + y * 0.3))
H, W, SY, SX = 8, 10, 3, 3
OH, OW = H * SY, W * SX
src = f(*np.mgrid[0:H, 0:W])
def coords(o, s, scale):
return (np.arange(o) + 0.5) / scale - 0.5
def nearest(a, oh, ow):
yy = np.clip(np.round(coords(oh, a.shape[0], SY)).astype(int), 0, a.shape[0] - 1)
xx = np.clip(np.round(coords(ow, a.shape[1], SX)).astype(int), 0, a.shape[1] - 1)
return a[np.ix_(yy, xx)]
def bilinear(a, oh, ow):
cy = np.clip(coords(oh, a.shape[0], SY), 0, a.shape[0] - 1)
cx = np.clip(coords(ow, a.shape[1], SX), 0, a.shape[1] - 1)
y0 = np.floor(cy).astype(int); y1 = np.minimum(y0 + 1, a.shape[0] - 1)
x0 = np.floor(cx).astype(int); x1 = np.minimum(x0 + 1, a.shape[1] - 1)
wy = (cy - y0)[:, None]; wx = (cx - x0)[None, :]
top = a[np.ix_(y0, x0)] * (1 - wx) + a[np.ix_(y0, x1)] * wx
bot = a[np.ix_(y1, x0)] * (1 - wx) + a[np.ix_(y1, x1)] * wx
return top * (1 - wy) + bot * wy
truth = f(*np.meshgrid(coords(OH, H, SY), coords(OW, W, SX), indexing="ij"))
n_out = nearest(src, OH, OW)
b_out = bilinear(src, OH, OW)
print("WHAT NEAREST ACTUALLY DOES. one output row, upscaled 3x:")
print(" nearest: " + " ".join("%4.0f" % v for v in n_out[9][:12]))
print(" bilinear: " + " ".join("%4.0f" % v for v in b_out[9][:12]))
print(" truth: " + " ".join("%4.0f" % v for v in truth[9][:12]))
print(" nearest repeats each value 3 times -- those flat runs are the")
print(" blocky squares you see when a thumbnail is blown up.")
Output
The next block shows that bilinear interpolation is not truly linear along a diagonal -- it is linear in each axis separately, which makes a curved surface.
example_04.pyNumPy
import numpy as np
def f(y, x):
return (128 + 90 * np.sin(x * 0.55) * np.cos(y * 0.42)
+ 20 * np.sin(x * 0.2 + y * 0.3))
src = f(*np.mgrid[0:8, 0:10])
def bilerp_at(a, y, x):
y0, x0 = int(np.floor(y)), int(np.floor(x))
wy, wx = y - y0, x - x0
return ((a[y0, x0] * (1 - wx) + a[y0, x0 + 1] * wx) * (1 - wy)
+ (a[y0 + 1, x0] * (1 - wx) + a[y0 + 1, x0 + 1] * wx) * wy)
print("BILINEAR IS NOT LINEAR IN THE COORDINATES. walk the diagonal")
print("from corner (2,3) to corner (3,4):")
print("%8s %14s %14s %10s" % ("t", "bilinear", "straight line", "gap"))
for t in (0.0, 0.25, 0.5, 0.75, 1.0):
b = bilerp_at(src, 2 + t, 3 + t)
line = src[2, 3] * (1 - t) + src[3, 4] * t
print("%8.2f %14.3f %14.3f %10.3f" % (t, b, line, b - line))
print(" the endpoints agree and the middle does not. 'bilinear' means")
print(" linear in each variable SEPARATELY, which is a curved surface.")
Output
The next block demonstrates the downscaling failure: subsampling a stripe pattern causes aliasing, while averaging first preserves the correct brightness.
example_05.pyNumPy
import numpy as np
FH, FW = 6, 48
stripes = np.where((np.arange(FW) // 2) % 2 == 0, 220.0, 40.0)
stripes = np.tile(stripes, (FH, 1))
print("DOWNSCALING FAILURE. take a fine stripe pattern:")
print(" input row (every 2 columns alternate light/dark):")
print(" " + "".join("#" if v > 128 else "." for v in stripes[0]))
print(" the true average brightness is %.0f." % stripes.mean())
print()
print(" shrink 4x by just taking every 4th column -- what 'nearest' is:")
sub = stripes[:, ::4]
print(" " + "".join("#" if v > 128 else "." for v in sub[0]))
print(" mean %.0f, and every stripe is GONE. the pattern repeats every 4"
% sub.mean())
print(" columns and we sampled every 4 columns, so we hit the same phase")
print(" each time. that is ALIASING.")
print()
print(" now average each 4-wide block instead -- BLUR, THEN SAMPLE:")
area = stripes.reshape(FH, FW // 4, 4).mean(axis=2)
print(" " + " ".join("%3.0f" % v for v in area[0][:12]))
print(" mean %.0f, matching the input's %.0f."
% (area.mean(), stripes.mean()))
print()
print("%-34s %12s" % ("4x downscale of the stripes", "result mean"))
print("%-34s %12.1f" % ("true input mean", stripes.mean()))
print("%-34s %12.1f" % ("subsample (nearest)", sub.mean()))
print("%-34s %12.1f" % ("area-average (blur first)", area.mean()))
print()
print("SO THE RULE SPLITS IN TWO:")
print(" UPSCALING is an interpolation problem -- use bilinear or bicubic.")
print(" DOWNSCALING is a filtering problem -- low-pass first, THEN")
print(" sample. this is the single most common resizing bug, and it")
print(" silently corrupts training data.")
Output
Check yourself
0 of 3
Answer without scrolling back up.
Why must a segmentation mask be resized with nearest neighbour?
Averaging label 3 and label 5 gives 4, which is a different class entirely. Labels are categorical, so only copying is safe.
What causes aliasing when shrinking an image?
Detail finer than the new sampling grid is not lost gracefully - it beats against the grid and produces a pattern that was never in the scene. Averaging before sampling prevents it.
Why can bicubic interpolation produce halos near high-contrast edges?
The overshoot is what makes bicubic look sharper than bilinear. Beside a strong edge the same overshoot shows up as a faint bright or dark fringe.
Cheat sheet
Resizing and Interpolation
Resize an image with nearest, bilinear and bicubic interpolation side by side and see exactly where each method invents or drops detail.
Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.