Log in

View Full Version : Comparing different scalers


GeoffreyA
12th February 2026, 09:09
A script I found helpful for comparing different scalers:

from vapoursynth import core
import vapoursynth as vs

src = core.bs.VideoSource(r"Calliphora.mp4").std.Trim(0, 0)

# Linear RGB (32-bit)
src = src.resize.Spline36(format=vs.RGBS, matrix_in_s="709", transfer_in_s="709", primaries_in_s="709", range_in_s="limited",
matrix_s="rgb", transfer_s="linear", primaries_s="709", range_s="full")

#src = src.fmtc.bitdepth(bits=16)

dst = src
scale = getattr(core.resize, "Spline36")
count = 100
factor = 2

# Scale up, then down
for i in range(count):
dst = scale(dst, dst.width * factor, dst.height * factor)
dst = scale(dst, dst.width / factor, dst.height / factor)

src = src.text.Text("Reference", alignment=4)
dst = dst.text.Text(scale.name + ": " + str(count) + " iterations", alignment=4)

out = core.std.Interleave([src, dst])
out = out.text.ClipInfo(alignment=6)

# Gamma YUV420 (8-bit)
out = out.resize.Spline36(format=vs.YUV420P8, matrix_s="709", transfer_s="709", primaries_s="709", range_s="limited", chromaloc=0, dither_type="error_diffusion")
#src = src.fmtc.bitdepth(bits=8)

out.set_output()

A useful source from poisondeathray: https://workupload.com/file/xT7Z2urrT3j

Comparison: https://slow.pics/s/uvSTX6Cc

Sunspark
12th February 2026, 14:56
I like the comparison slider, but I think there's something wrong with some of the output. Bicubic is not that blurry, Lanczos is much better than that unless you mean the first version nobody uses?

Should also specify the parameters used.. e.g. what kind of bicubic? catmull-rom is bicubic 50 for example.

For video use, it would also be good to see how they perform at downscaling where they have to scale to a non-integer factor vs upscaling where they need to create data.

Selur
12th February 2026, 15:03
So the conclusion is that if you plan to scale up and then down by a factor of two, a hundred times, stick with point resize. *gig*

GeoffreyA
12th February 2026, 16:04
I like the comparison slider, but I think there's something wrong with some of the output. Bicubic is not that blurry, Lanczos is much better than that unless you mean the first version nobody uses?

Should also specify the parameters used.. e.g. what kind of bicubic? catmull-rom is bicubic 50 for example.

For video use, it would also be good to see how they perform at downscaling where they have to scale to a non-integer factor vs upscaling where they need to create data.

It could be much improved. I couldn't tell the difference with a one-iteration roundtrip, say between Spline36 and 64, so I tried something like this to get an idea of the tendencies of each algorithm. But in practice, with less than ten scalings, it would be hard to tell the difference between most.

The scalers were from VapourSynth's resize (https://www.vapoursynth.com/doc/functions/video/resize.html) module, which uses zimg. I don't know Python, so I'll have to find out how to wrap the scalers to make them easy to change.

So the conclusion is that if you plan to scale up and then down by a factor of two, a hundred times, stick with point resize. *gig*

Exactly! :)

GeoffreyA
12th February 2026, 19:04
Sunspark, here is a new version of the script with more scalers. If you have access to VapourSynth and its Editor, you can easily adjust the iteration count and scaling factor.

from vapoursynth import core
import vapoursynth as vs


# =============================================================

def BT709ToLinearRGB(clip):
return core.resize.Spline36(clip, format=vs.RGBS, matrix_in_s="709", transfer_in_s="709", primaries_in_s="709", range_in_s="limited",
matrix_s="rgb", transfer_s="linear", primaries_s="709", range_s="full")

def LinearRGBToBT709(clip):
return core.resize.Spline36(clip, format=vs.YUV420P8, matrix_s="709", transfer_s="709", primaries_s="709",
range_s="limited", chromaloc=0, dither_type="error_diffusion")

def Bilinear(clip, w, h):
return core.resize.Bilinear(clip, w, h)

def Hermite(clip, w, h):
return core.resize.Bicubic(clip, w, h, filter_param_a=0, filter_param_b=0)

def Mitchell(clip, w, h):
return core.resize.Bicubic(clip, w, h, filter_param_a=0.333333, filter_param_b=0.333333)

def CatmullRom(clip, w, h):
return core.resize.Bicubic(clip, w, h, filter_param_a=0, filter_param_b=0.5)

def Box(clip, w, h):
return core.fmtc.resample(clip, w, h, kernel="box")

def Point(clip, w, h):
return core.resize.Point(clip, w, h)

def Spline16(clip, w, h):
return core.resize.Spline16(clip, w, h)

def Spline36(clip, w, h):
return core.resize.Spline36(clip, w, h)

def Spline64(clip, w, h):
return core.resize.Spline64(clip, w, h)

def Spline144(clip, w, h):
return core.fmtc.resample(clip, w, h, kernel="spline", taps=6)

def Lanczos3(clip, w, h):
return core.resize.Lanczos(clip, w, h, filter_param_a=3)

def Lanczos4(clip, w, h):
return core.resize.Lanczos(clip, w, h, filter_param_a=4)

def ScaleLoop(clip, scaler, count, factor):
for i in range(count):
clip = scaler(clip, clip.width * factor, clip.height * factor)
clip = scaler(clip, clip.width / factor, clip.height / factor)

return clip.text.Text(scaler.__name__ + "\nIterations = " + str(count) + "\nFactor = " + str(factor), alignment=7)


# =============================================================

src = core.bs.VideoSource(r"Calliphora.mp4").std.Trim(0, 0)
src = BT709ToLinearRGB(src)

count = 100
factor = 1.5

out = core.std.Interleave([src, ScaleLoop(src, Bilinear, count, factor),
ScaleLoop(src, Hermite, count, factor),
ScaleLoop(src, Mitchell, count, factor),
ScaleLoop(src, CatmullRom, count, factor),
ScaleLoop(src, Box, count, factor),
ScaleLoop(src, Point, count, factor),
ScaleLoop(src, Spline16, count, factor),
ScaleLoop(src, Spline36, count, factor),
ScaleLoop(src, Spline64, count, factor),
ScaleLoop(src, Spline144, count, factor),
ScaleLoop(src, Lanczos3, count, factor),
ScaleLoop(src, Lanczos4, count, factor)])

out = out.text.ClipInfo(alignment=6)
out = LinearRGBToBT709(out)

out.set_output()

microchip8
13th February 2026, 09:09
To me, Spline36 looks the best. Spline64 makes it slightly worse but in theory it should be slightly better than 36, but it isn't.

Is there something wrong with Lanczos? It shows a garbled/corrupt pic

(I use Lanczos 3-tap when downscaling my encodes from 4K/UHD to Full HD - it is slightly sharper than Spline36 and ringing is virtually unnoticeable)

GeoffreyA
13th February 2026, 11:22
I agree, preferring Spline36, which has alway given me good results, neither too sharp nor too soft.

I'm not sure. That slider comparison was using zimg's Lanczos, along with 100 iterations at 32-float linear RGB. I noticed that using a lower bit depth of 8, the artefacts are mostly gone. I'll make a new comparison with the updated script when I get home.

GeoffreyA
13th February 2026, 13:46
32-bit float linear-RGB processing, 1.5x factor

10 iterations: https://slow.pics/s/1vYdwUvV
100 iterations: https://slow.pics/c/zWqwAt5r

Selur
13th February 2026, 15:32
side note: instead of using:
dst = scale(dst, dst.width * factor, dst.height * factor)
dst = scale(dst, dst.width / factor, dst.height / factor)
wouldn't it make more sense to use:

dst = scale(dst, dst.width / factor, dst.height / factor)
dst = scale(dst, dst.width * factor, dst.height * factor)

? (with the first, point resize should always win)

Sunspark
13th February 2026, 15:43
I was watching a video last night and changed scalers afterward to Mitchell-Netravali AR for the rest of the series because the encoder or quality was a bit too grainy. Sharp scalers were accentuating the grain noise too much.

Unfortunately, there is no one perfect scaler to rule them all.

@microchip8 Try comparing with Bicubic150 for the downscaling. The load won't be worse and the results will be clean.

Z2697
13th February 2026, 15:47
side note: instead of using:
dst = scale(dst, dst.width * factor, dst.height * factor)
dst = scale(dst, dst.width / factor, dst.height / factor)
wouldn't it make more sense to use:

dst = scale(dst, dst.width / factor, dst.height / factor)
dst = scale(dst, dst.width * factor, dst.height * factor)

? (with the first, point resize should always win)

I think point resize will still win in some aspect in second case.
It's "lossless" after first iteration.

GeoffreyA
13th February 2026, 17:11
Using:

clip = scaler(clip, clip.width / factor, clip.height / factor)
clip = scaler(clip, clip.width * factor, clip.height * factor)

3 iterations: https://slow.pics/c/iKxOOx7s
100 iterations: https://slow.pics/c/SMepiWCu

I had to use a factor of 2 because of dimensional problems with 1.5.