Home

Awesome

Finding the best methods for image scaling

There is no such thing as a universally best method or the best kernel function, or... Which methods and settings will be best for you will depend on your use case scenario. In this research, several scenarios will be tested using mpv player (https://mpv.io). Settings will be adjusted for the beset or close to best results on tested images (videos). I will try to restrain myself from making claims that this or that is the best or worst, I will try to base all claims on the test results, but do note that in different scenarios, you may get different results.

Testing methodology

All test images were created directly from the same illustration (https://www.freepik.com/free-vector/vector-illustration-mountain-landscape_1215613.htm#query=illustrations&position=11&from_view=keyword&track=sph) in Adobe Illustrator with added text as extra details, and then they were converted with ffmpeg to mp4 with this command ffmpeg -loop 1 -i input.png -c:v libx264 -t 60 -r 30 -pix_fmt yuv444p output.mp4. Videos were then upscaled from lower resolutions to 1080p and compared to screenshot taken of the original 1080p video, in case of upscaling. The oposite was done in case of downscaling. For comparation was used dssim (https://en.wikipedia.org/wiki/Structural_similarity) in ImageMagick (https://imagemagick.org) with this command magick compare -metric dssim org.png scaled.png x:. For taking screenshots Single Menu Screenshot (https://github.com/garamond13/SingleMenuScreenshot) was used. The shaders and the config used in testing were modified as needed.

Notes that there is no perfect methodology for testing. I went with this methodology because of past experiences. This methodology also avoids blurring, ringing, and aliasing artifacts that occur in the preparation of test images by conventional scaling of the original image. Also, note that this image may not be very suitable for testing downscaling as it won’t have serious issues with aliasing. And it's possible that errors ocour during testing.

Upscale (upsample)

Ringing and anti-ringing

Reference on ringing https://en.wikipedia.org/wiki/Ringing_artifacts
Let’s first establish whether we should use anti-ringing in further testing. Here we will test what amount of antiringing produces best results. The amount is in range [0.0, 1.0]. We will use altUpscaleHDR.glsl user shader with kernel-function=lanczos radius=2.0 blur=1.0. All tested resoultions are upscaled to 1080p.

anti-ringing amounts and corresponding dssim result

resolution0.00.750.991.0
7200.02385350.02354850.02355620.0234667
5400.03721750.03652130.03645340.0363607
3600.06469680.06379670.06366460.0636103

Based on these results, it should be safe to assume that the optimal setting for anti-ringing is 1.0. However, we will perform a few more tests. We will use altUpscaleHDR.glsl user shader with kernel-function=welch radius=2.0 blur=1.0.

anti-ringing amounts and corresponding dssim result

resolution0.991.0
7200.02298980.0228918
5400.03603430.0359319
3600.06325650.0631909

Now we will use altUpscaleHDR.glsl user shader with kernel-function=bicubic a=-0.5.

anti-ringing amounts and corresponding dssim result

resolution0.751.0
7200.02366750.0235908
5400.03662260.0364864
3600.06382640.0636606

Again we are getting consistent results where 1.0 looks to be most optimal. Based on these results, we will use an anti-ringing value of 1.0 for further testing.

Kernel functions

Reference on kernel https://en.wikipedia.org/wiki/Kernel_(image_processing)
Kernel functions are used for the calculation of kernel weights. We will test a few types of scaling filters and several kernel functions. The filters we are going to test are mpv's ewa or polar (mpv --vo=gpu-next), at the time of the research my experimental jinc (which is similar to mpv's ewa or polar), and orthogonal or separated (alt-scale shaders). Kernel functions we are going to use here will be windowed sinc or jinc, or their alternatives.

Let’s first have a look at windowed sinc and the following windows: cosine, welch, lanczos (sinc window), hann, and hamming. For all of these windows, it can be said that they are controlled by the kernel radius. We will test at what radius these windows are giving best results. We will use altUpscaleHDR.glsl user shader with blur=1.0 anti-ringing=1.0.

720p

windowradiusdssim
lanczos4.60.0221655
cosine4.50.0221147
welch4.50.0220882
hann6.60.0221957
hamming5.90.0221789

540p

windowradiusdssim
lanczos4.30.0354828
cosine4.30.035486
welch4.30.0354907
hann4.90.0354921
hamming4.60.035502

360p

windowradiusdssim
lanczos2.70.0628738
cosine2.60.0627836
welch2.50.0627289
hann3.40.0629509
hamming3.70.0628955

Here, we can notice that all windows can achieve similar results. However, one other thing to note is that hann and hamming usually need a larger radius to achieve those results, which makes them worse in terms of performance.

Now lets test mpv's ewa or polar with these settings added to base config scale=ewa_lanczos scaler-lut-size=10 scale-window= scale-radius= scale-cutoff=0.0 scale-blur=1.0. For antiringing we will use https://github.com/haasn/gentoo-conf/blob/xor/home/nand/.mpv/shaders/antiring.hook without chroma part (default settings).

Note: dont confuse ewa_lanczos and lanczos, ewa_lanczos is jinc windowed jinc and lanczos is sinc windowed sinc, but sinc window is also called lanczos window. Here we will only test sinc windowed jinc (under the name lanczos) also known as ewa_ginseng.

720p

windowradiusdssim
lanczos3.00.0228769
cosine2.90.0226756
welch2.80.0225573
hann5.30.0227386
hamming4.40.022792

540p

windowradiusdssim
lanczos2.90.036318
cosine2.90.036318
welch2.70.0359962
hann3.40.0366169
hamming4.00.0365035

360p

windowradiusdssim
lanczos2.90.0638985
cosine2.70.0635906
welch2.70.0634135
hann3.30.0641833
hamming4.10.0641346

The results are very consistent. For similar radiuses, lanczos, cosine, and welch achieve their best overall results at lower radiuses compared to hann and hamming. This is why I will only test lanczos welch and hann in my experimental jinc (usually if not mentioned assume antiringing=1.0 and blur=1.0).

720p

windowradiusdssim
lanczos3.00.0227903
welch2.80.0224323
hann5.30.0227661

540p

windowradiusdssim
lanczos2.90.0365359
welch2.70.036151
hann3.40.0368122

360p

windowradiusdssim
lanczos2.90.0642838
welch2.70.0638784
hann3.30.0645392

Again we are getting consitent results. Based on the last tests, we could conclude that windows controlled by radius can achieve around similar results, but some windows need higher radiuses to achieve those results.

So far, we have tested windows that don’t offer any control with free parameters. We only get to control them with radius. Now we will test windows with one free parameter blackman, power of cosine, and here I will present the new window with one free parameter.

w(x) = 1.0 - pow(abs(x) / R, n), where n is in the range (0.0, +inf] and R is the kernel radius
at n=1.0, w(x)=linear window
at n=2.0, w(x)=welch window
as n aproaches +inf, w(x)=box window
I will refer to it as the garamond window.

Taking into consideration the fact that at n=2.0, the garamond window is exactly the same as the welch window, we will use the previous results of welch and test them against the garamond window at the same radius, and possibly lower radius. We will use my experimental jinc.

720p

windowradiusdssim
welch2.80.0224323
garamond n=4.02.80.0221757
garamond n=3.72.70.0221628

Using the garamond window, we were able to achieve better result at the same radius, and we were able to lower the radius further while achieving even better results.

Lets now test power of cosine with free parameter n.
Few interisting facts about it:
at n=0.0, box window
at n=1.0, cosine window
at n=2.0, hann window

We will use my experimental jinc and compare the results to welch because it achieved the best result in the previous test.

540p

windowradiusdssim
welch2.70.036151
pow cosine n=0.62.70.0360422
pow cosine n=0.42.50.0357687

Again we were able to achive better resut at the same radius, and we were able to lower the radius further while achieving even better results.

Now lets take the best result from earlier test and test blackman against it. (at a=0.16, common blackman). We will use altUpscaleHDR.glsl.

720p

windowradiusdssim
welch4.50.0220882
blackman a=-0.84.50.0219042
blackman a=-0.73.60.0218609

Again, we are getting consistent results. Based on this, we could conclude that these windows, still controlled by radius, and now with one free parameter, can achieve better results and further reduce the needed radius for those results compared to windows controlled only by radius.

Now, we will test the generalized normal window (gnw) and said, windows with two free parameters. One thing to note about them is that they are not affected by radius directly, as previously tested windows. We will take the best results from earlier test and test both against. We will use altUpscaleHDR.glsl.

720p

windowradiusdssim
welch4.50.0220882
blackman a=-0.73.60.0218609
gnw s=4.9 n=3.53.90.0219217
said chi=0.16 eta=1.04.00.0220121

Here we can see that gnw and said can achive similar results. Now we will use my experimental jinc.

540p

windowradiusdssim
welch2.70.036151
pow cosine n=0.42.50.0357687
gnw s=3.0 n=2.92.50.0358475
said chi=0.17 eta=0.02.50.0358198

And we are getting consistent results. Based on this, we could conclude that we are able to get at least close to the best results compared to kernel functions with one free parameter (which are affected by radius, so we could count it as a second parameter as well).

Kernel blur

With the kernel blur we can control the kernel funcion's central lobe width. So far, we have conducted tests with a blur value of 1.0, which means neutral or off. Now, we will test whether we can improve results by adjusting the kernel blur. We will use altUpscaleHDR.glsl.

720p

windowradiusblurdssim
blackman a=-0.73.61.00.0218609
blackman a=-0.73.60.930.0215603

Now we will use my experimental jinc.

540p

windowradiusblurdssim
pow cosine n=0.42.51.00.0357687
pow cosine n=0.42.50.890.0346785

By adjusting the kernel blur, we were able to improve some of the best previous results.

Sigmoidal upscale

Reference https://legacy.imagemagick.org/Usage/color_mods/#sigmoidal
So far, tests were done in gamma light because of simplicity. Now, we will test upscaling in sigmoidal light. We control sigmoidal curve with contrast (c) and midpoint (m) parameters. Now we will compare gamma upscale (previous results altUpscaleHDR.glsl) and same settings in sigmoidal light using altUpscale.glsl.

720p

windowradiusblurdssim
blackman a=-0.7 (gamma)3.60.930.0215603
blackman a=-0.7 (c=6.0 m=0.6)3.60.930.0214525

And now, I will modify my experimental jinc to upscale in sigmoidal light. For simplicity, I will use the same settings for sigmoidal light control.

540p

windowradiusblurdssim
pow cosine n=0.4 (gamma)2.50.890.0346785
pow cosine n=0.4 (c=6.0 m=0.6)2.50.890.034432

By performing upscale in sigmoidal light, we were able to improve the results. Based on this, we could conclude that performing upscale in sigmoidal light can achieve better results. However, for most of the further testing, I will continue to use gamma light because of simplicity.

Post-upscale sharpening

Now, we will test post-upscale sharpening, essentially, can we further improve the results? We will use unsharp mask which is controled by sigma value (s), its kernel radius (r) and sharpening amount (a). We wil still use sigmoidal light altUpscale.glsl.

720p

windowradiusblurdssim
blackman a=-0.7 (c=6.0 m=0.6) no sharpening3.60.930.0214525
blackman a=-0.7 (c=6.0 m=0.6) s=1.1 r=2.0 a=0.13.60.930.0213019

And we are able to further iprove results by sharpening image after the upscale.

Alternative kernel functions

Alternative kernel functions are mostly designed as alternatives to sinc. However, in this case, we will only test a few adjustable alternatives, meaning we should be able to adjust them to perform well with jinc as well. We will test bicubic with one free parameter (a) and a fixed radius of 2.0, bc-spline with two parameters (b) and (c) and a fixed radius of 2.0. Additionally, I will present here the new modified fsr kernel based on https://github.com/GPUOpen-Effects/FidelityFX-FSR .The original fsr kernel has one parameter (b) and a fixed radius of 2.0. The new modified fsr kernel has two parameters (b) and (c). The c parameter will have a similar effect to the kernel blur in windowed sinc and windowed jinc kernel functions. It will also have a similar effect to the (b) parameter of bc-spline.

modified fsr kernel, fixed radius 2.0
b != 0.0 && b != 2.0 && c != 0.0
(1.0 / (2.0 * b - b * b) * (b / (c * c) * x * x - 1.0) * (b / (c * c) * x * x - 1.0) - (1.0 / (2.0 * b - b * b) - 1.0)) * (0.25 * x * x - 1.0) * (0.25 * x * x - 1.0)
at c=1.0, the original fsr kernel

Now we will test bicubic using altUpscaleHDR.glsl.

resolutionadssim
720-1.10.0225256
540-0.80.0359437
360-0.90.0631531

Now we will test bicubic using my experimental jinc.

resolutionadssim
720-0.70.0224556
540-0.70.0347427
360-0.60.0622983

Now we will test bc-spline using altUpscaleHDR.glsl.

resolutionb and cdssim
720b=-0.5, c=1.10.0219281
540b=-0.5 c=0.90.0346389
360b=-0.7 c=0.90.0619001

Now we will test bc-spline using my experimental jinc.

resolutionb and cdssim
720b=0.3 c=0.70.0218987
540b=0.2 c=0.70.0343549
360b=0.2 c=0.60.0621524

Now we will test modified fsr using altUpscaleHDR.glsl.

resolutionb and cdssim
720b=0.2, c=0.950.0218886
540b=0.2 c=0.880.034506
360b=0.2 c=0.940.0625743

Now we will test modified fsr using my experimental jinc.

resolutionb and cdssim
720b=0.43 c=1.060.0220508
540b=0.44 c=1.030.0344618
360b=0.44 c=1.040.0623959

Here, we are able to get results that are close to the windowed sinc and windowed jinc kernel functions. Based on these results, we could conclude that it is easier to achieve good results with bc-spline and modified fsr kernel compared to bicubic. This probably shouldn’t be surprising as bicubic has only one parameter compared to the two parameters in the previously mentioned kernel functions. Additionally, note that all three kernel functions use kernels with a radius of 2.0.

Downscale (downsample)

Downscaling will be kept a bit simpler because it’s more straightforward if done correctly. The correct way to do downscaling is to use linear light because otherwise, the image can be significantly darkened (reference http://www.ericbrasseur.org/gamma.html). The kernel has to be scaled appropriately, and antiringing is generally bad because it can increase aliasing. These rules can be broken and yield better results on some images, but generally, for video with a lot of different frames (images), these rules should probably be enforced.

We will skip testing of many windows since we have already established that more adjustable windows can be adjusted for better results. We will test the power of the cosine window because for n=1 it’s a cosine window, and for n=2, it’s a hann window. We will also test the garamond window because for n=1, it’s a linear window, and for n=2, it’s a Welch window. Now we will use altDownscale.glsl.

540p

windowradiusdssim
lanczos2.20.0184956
pow cosine n=0.82.00.0185596
garamond n=0.12.40.0173727
said chi=0.4 eta=0.02.00.0183955

720p

windowradiusdssim
lanczos2.10.0137789
pow cosine n=1.32.00.0140686
garamond n=0.12.40.0132567
said chi=0.5 eta=0.02.10.0138891

Based on these results we could conclude that the garamond window can be adjusted for the best results. Now we will use my experimental jinc.

540p

windowradiusdssim
lanczos2.50.0204201
pow cosine n=0.62.10.0206342
garamond n=0.12.70.0192115
said chi=0.4 eta=0.02.20.0202307

And we are getting consistent results. Now we will test can we improve the results by adjusting the kernel blur. First we will use altDownscale.glsl.

540p

windowradiusblurdssim
garamond n=0.12.41.00.0173727
garamond n=0.12.40.750.0153649

Now we will use my experimental jinc.

540p

windowradiusblurdssim
garamond n=0.12.71.00.0192115
garamond n=0.12.70.690.0155021

Based on these results, we could conclude that the results can be improved by adjusting the kernel blur.

Now we will test alternative kernel functions. We will test only bc-spline and the modified fsr kernel. We will use altDownscale.glsl.

540p

windowparametersdssim
bcsplineb=-0.5 c=0.60.0169073
modified fsrb=0.45 c=0.940.0177153

And now we will use my experimental jinc.

540p

windowparametersdssim
bcsplineb=-0.1 c=0.20.016042
modified fsrb=0.53 c=0.940.0164078

These results may not be too impressive, but note that these kernel functions should be significantly faster and, on top of that, use a radius of 2.0.

Finally, I’m going to just mention that downscaling could be further improved by using pre-filtering (gaussian blur), which will reduce aliasing, and we could use post-scale sharpening for obvious reasons.