Upscaling very low resolution videos for YouTube, using ffmpeg and interleaved gradient noise
Today, I wanted to help someone with upscaling videos for YouTube. In the days of AI and upscaling models, this seemed to be an easy task at first. Yet, it quickly became a bit more complicated.
The original videos are from a homemade DVD, with a ridiculously small resolution of 252x214 pixels. Upscaling this via AI will look like absolute garbage. So, I came come up with a different solution... that includes upscaling the video to 1440p and adding interleaved gradient noise.

Consider this a vague comparison since both versions are scaled to 480p. The following explanation will provide closure on what's happening and why.
Here's what I've done:
Why Upscale to 1440p?
Upscaling a 352x214 video to 1440p does not add new physical detail, but it fundamentally changes how YouTube handles the file.
If you upload a video below 1080p, YouTube processes it using the older AVC (H.264) codec at a very low bitrate. This aggressively compresses the file, destroying edges and turning low-resolution footage into a blurry, blocky mess.
Uploading at 1440p or 4K forces YouTube to assign the superior VP9 (or AV1) codec and allocates a significantly higher bitrate. This protects the mathematical upscaling and preserves the artificial noise layer, ensuring the video reaches the viewer looking as close as possible to what it did look like on your machine.
Why Use Interleaved Gradient Noise (IGN)?
Mathematical scalers like Lanczos do an excellent job of preserving edges, but they leave low-resolution surfaces looking perfectly smooth, flat, and sterile.
Perceptual Sharpness: The human eye interprets fine texture as "sharpness." By adding a subtle layer of noise, we trick the brain into perceiving detail that does not actually exist.
Encoder Forcing: Digital video encoders look for flat areas of color to compress. By introducing a constant, high-frequency noise floor (IGN), we force the encoder to work harder and allocate more data to the entire frame, preventing color banding and macro-blocking.
Why IGN specifically? Unlike pure white noise, IGN is a structured noise pattern specifically designed to be visually pleasing and less susceptible to causing severe compression artifacts.
How to generate interleaved gradient noise for this usecase
I use pre-calculated interleaved gradient noise via: ign-gen
The Script
:: Define directories and noise file here
SET "INPUT_DIR=PATH\TO\CDDASH"
SET "OUTPUT_DIR=PATH\TO\\CDDASH_UP"
SET "IGN=PATH\TO\ign_2560x1440.png"
:: Create the output directory if it doesn't exist
if not exist "%OUTPUT_DIR%" mkdir "%OUTPUT_DIR%"
:: Loop through every .mp4 file in the input directory
FOR %%F IN ("%INPUT_DIR%\*.mp4") DO (
echo Processing: %%~nxF
echo =======================================================
ffmpeg -i "%%F" -loop 1 -i "%IGN%" -filter_complex "[0:v]fps=60,scale=2560:1440:force_original_aspect_ratio=decrease:flags=lanczos,pad=2560:1440:(ow-iw)/2:(oh-ih)/2,format=yuv420p[upscaled];[1:v]format=yuv420p[ign_fmt];[upscaled][ign_fmt]lut2=c0='clip(x+(y-128)*0.15,0,255)':c1='x':c2='x':shortest=1[outv]" -map "[outv]" -map 0:a? -c:v hevc_nvenc -preset p6 -tune hq -rc vbr -cq 24 -b:v 0 -c:a aac -b:a 192k -shortest "%OUTPUT_DIR%\%%~nF_upscaled.mp4" -y
)
pause
Since the ffmpeg command is pretty long, let's take a closer look:
Inputs and Outputs
-i "%INPUT%": Reads the primary source video.-loop 1: Instructs FFmpeg to infinitely loop the following input (necessary because the noise file is a static image).-i "%IGN%": Reads the Interleaved Gradient Noise PNG file.-y: Automatically overwrites the output file without asking for confirmation.
Stream Mapping
-map "[outv]": Takes the final, processed video output from the filter_complex chain and maps it to the output file.-map 0:a?: Maps the audio from the first input (0) to the output. The?makes this optional, meaning the script won't crash if the source video has no audio track.-shortest: Tells FFmpeg to stop encoding when the shortest active input stream ends. Since the IGN image is looping infinitely, this ensures the encoding stops the exact millisecond the source video ends.
The Video Filtergraph
The filtergraph is the CPU-driven pipeline where the actual image processing happens. It is divided into two distinct stream preparations and a final merge.
Stream A ([0:v]): Preparing the Source Video
[0:v]scale=2560:1440:force_original_aspect_ratio=decrease:flags=lanczos,pad=2560:1440:(ow-iw)/2:(oh-ih)/2,format=yuv420p[upscaled];
scale=2560:1440: Sets the target resolution.force_original_aspect_ratio=decrease: Ensures the video scales up as much as possible to fit inside the 2560x1440 box without stretching or distorting the original geometry.flags=lanczos: Uses the Lanczos scaling algorithm. It uses a sinc function to calculate pixel values, resulting in much sharper edges than standard bilinear scaling.pad=2560:1440:(ow-iw)/2:(oh-ih)/2: Fills the remaining empty space with true black bars (pillarboxing or letterboxing). The formulas(ow-iw)/2(output width minus input width, divided by 2) mathematically center the video perfectly on both the X and Y axes.format=yuv420p: Forces the processed stream into the standard 8-bit YUV 4:2:0 color space.
Stream B ([1:v]): Preparing the Noise
[1:v]format=yuv420p[ign_fmt];
format=yuv420p: PNG images operate in RGB. This explicitly converts the noise image into the exact same YUV color space as the video, preventing chroma-drop errors during the merge.
The Merge ([lut2]): 2D Lookup Table
[upscaled]ign_fmt]lut2=c0='clip(x+(y-128)*0.15,0,255)':c1='x':c2='x':shortest=1[outv]
This filter merges the video (x) and the noise (y) using explicit pixel math.
c0(Luma / Brightness): Applies the noise using the formula .- By subtracting 128 from the 8-bit noise value (), we convert it into Zero-Mean Noise (ranging from -128 to +127).
- This ensures that when scaled by 15% () and added to the video (), it darkens as many pixels as it brightens, preserving the exact original contrast of the video.
- The
clipfunction acts as a safety limit, ensuring no pixel values drop below absolute black (0) or clip above absolute white (255).
c1andc2(Chroma / Color): Set to 'x', meaning the U and V color channels are copied exactly from the source video (x). The noise is blocked from affecting the color, preventing a magenta/purple tint.shortest=1: A stream-level command that tells the lut2 filter to terminate when the source video stream ends, preventing the infinite loop of the static noise image from freezing the pipeline.
Hardware Encoding Once the CPU finishes filtering a frame, it is sent to the NVIDIA GPU (NVENC) for highly efficient hardware encoding.
-c:v hevc_nvenc -preset p6 -tune hq -rc vbr -cq 24 -b:v 0 -c:a aac -b:a 192k
This is standard ffmpeg stuff and I don't want to explain the parameters here. This post is long enough...
Hope this will help someone in the future.
Enjoy!