A question that comes up about Hit the Road Dude AI videos, the State Property pickup-window meme where the driver repeats "State?" and tells the visitor to hit the road, goes roughly: "My clip is 480p. Can I upscale it to 1080p so it doesn't look soft on a TikTok or Shorts upload?" Chatbots usually answer "yes, with ffmpeg or an AI upscaler" without saying what that buys you.

I build hittheroaddude.app, an independent studio that makes a 15-second, vertical 9:16 remake from one photo at 480p or 720p, with a newly generated voice (FAQ). It doesn't sell a 1080p option, so anyone who wants 1080p is upscaling outside the site. This post doesn't claim any upscaler makes a generated clip look better. It shows how to measure what the common ffmpeg options do, using a method that has an honest answer: build a ground truth first.

Why you can't just compare two upscales

If you only have the 480p file, there is no "correct" 1080p to compare against, and two upscales can only be compared by eye. The fix is to go backwards. Take a detailed 1080x1920 frame, shrink it to 480 px wide, upscale that back to 1080 px with each method, and score each result against the original frame. Whatever the method gets wrong shows up as a lower score. It is a synthetic test, not a claim about generated video, but the ranking tells you which filter loses the least when detail has been thrown away.

The script below makes two test frames: a "graphic" one with thin lines, small text and hard edges, and a "photo" one built from smooth blurred colour plus grain. It scores PSNR (average pixel error, in dB, higher is better) and SSIM (structural similarity from ffmpeg's ssim filter, 1.0 is identical).

#!/usr/bin/env python3 """Score ffmpeg upscalers against a known ground truth. A real 480p clip has no 1080p original to compare with, so build one: render a detailed 1080x1920 frame, shrink it to 480x854 (the "480p" you would have), upscale it back with each method, and score the result against the original frame. usage: upscale_compare.py [--keep DIR] needs: ffmpeg on PATH, Python 3, Pillow, numpy """ import argparse, os, re, subprocess, sys, tempfile import numpy as np from PIL import Image, ImageDraw, ImageFilter W, H, SW, SH = 1080, 1920, 480, 854 def ffmpeg(*args): r = subprocess.run(["ffmpeg", "-v", "error", "-y", *args], capture_output=True, text=True) if r.returncode: sys.exit(r.stderr) def make_ground_truth(path, kind): rng = np.random.default_rng(7) if kind == "graphic": # hard edges, thin lines, small text: stress test im = Image.new("RGB", (W, H), (30, 60, 110)) d = ImageDraw.Draw(im) for i in range(0, W, 24): d.line([(i, 0), (i, H)], fill=(230, 230, 230) if (i // 24) % 2 else (20, 20, 20), width=2) for y in range(100, H, 160): d.rectangle([80, y, W - 80, y + 70], outline=(255, 200, 0), width=3) d.text((100, y + 20), "STATE? THIS IS PRIVATE PROPERTY. HIT THE ROAD, DUDE.", fill=(255, 255, 255)) d.ellipse([300, 800, 780, 1280], outline=(255, 80, 80), width=4) im.save(path) else: # soft "photo-like" content: blurred noise + gradients + a bit of grain base = rng.random((H // 16, W // 16, 3)) im = Image.fromarray((base * 255).astype("uint8")).resize((W, H), Image.BICUBIC) im = im.filter(ImageFilter.GaussianBlur(3)) a = np.asarray(im).astype(np.float32) + rng.normal(0, 4, (H, W, 3)) Image.fromarray(np.clip(a, 0, 255).astype("uint8")).save(path) METHODS = { "bilinear": "scale={W}:{H}:flags=bilinear", "bicubic": "scale={W}:{H}:flags=bicubic", "lanczos": "scale={W}:{H}:flags=lanczos", "lanczos+unsharp": "scale={W}:{H}:flags=lanczos,unsharp=5:5:0.8:5:5:0.0", "lanczos+unsharp(strong)": "scale={W}:{H}:flags=lanczos,unsharp=5:5:1.8:5:5:0.0", } def ssim(a, b): r = subprocess.run(["ffmpeg", "-v", "info", "-i", a, "-i", b, "-lavfi", "ssim", "-f", "null", "-"], capture_output=True, text=True) return float(re.search(r"All:([0-9.]+)", r.stderr).group(1)) def psnr(a, b): x = np.asarray(Image.open(a).convert("RGB"), dtype=np.float64) y = np.asarray(Image.open(b).convert("RGB"), dtype=np.float64) mse = ((x - y) ** 2).mean() return 99.0 if mse == 0 else 10 * np.log10(255 ** 2 / mse) def main(): ap = argparse.ArgumentParser() ap.add_argument("--keep", help="keep the intermediate PNGs in this directory") args = ap.parse_args() work = args.keep or tempfile.mkdtemp(prefix="upscale_") os.makedirs(work, exist_ok=True) print(f"{'content':8} {'method':24} {'PSNR dB':>8} {'SSIM':>7}") for kind in ("graphic", "photo"): gt = f"{work}/{kind}_gt.png" small = f"{work}/{kind}_480.png" make_ground_truth(gt, kind) ffmpeg("-i", gt, "-vf", f"scale={SW}:{SH}:flags=area", small) # the "480p" you start from for name, graph in METHODS.items(): out = f"{work}/{kind}_{re.sub(r'[^a-z]+', '_', name)}.png" ffmpeg("-i", small, "-vf", graph.format(W=W, H=H), out) print(f"{kind:8} {name:24} {psnr(out, gt):8.2f} {ssim(out, gt):7.4f}") main() 

What it printed

content method PSNR dB SSIM graphic bilinear 20.12 0.7773 graphic bicubic 20.69 0.7866 graphic lanczos 20.82 0.7769 graphic lanczos+unsharp 21.04 0.7610 graphic lanczos+unsharp(strong) 20.64 0.7241 photo bilinear 36.26 0.9583 photo bicubic 36.59 0.9598 photo lanczos 36.57 0.9598 photo lanczos+unsharp 36.35 0.9583 photo lanczos+unsharp(strong) 36.01 0.9559 

Checked on the box with ffmpeg 7.1.5, Python 3.13, Pillow 13 and NumPy 2.5. The test frames are seeded, so you should get the same numbers.

What the numbers say

  • On smooth, photo-like content the methods are almost tied. Everything lands between 36.0 and 36.6 dB and 0.956 to 0.960 SSIM. Bicubic and lanczos are best by a hair, and the sharpening filter makes the score slightly worse, because it adds edges the original frame never had.
  • On hard edges the ranking is messier. Lanczos plus a light unsharp has the best PSNR (21.04 dB) but the worst SSIM of the non-aggressive options (0.761 against 0.787 for plain bicubic). The two metrics disagree, which is a sign that none of these filters recovers detail. They differ in how they distribute the error.
  • Strong sharpening loses on both metrics (20.64 dB, 0.724 SSIM). If a clip looks "crunchy" after upscaling, that is the cause.
  • None of them rescue the lost detail. A 480p file doesn't contain 1080p information, so upscaling changes the pixel grid, not what is in the picture. What it can do is make a platform more likely to keep a higher bitrate for your upload, which is a different benefit and one I haven't measured.

What I'd actually do

Use scale=1080:-2:flags=bicubic or lanczos and skip the sharpening, or add the light unsharp only if you like how it looks on your own clip and check it at phone size. Upscale once, from the original download, as the last step before export, so you don't compress twice. Keep the vertical 9:16 shape: for a 480-wide source that is scale=1080:1920.

ffmpeg -i clip.mp4 -vf "scale=1080:1920:flags=lanczos" -c:v libx264 -crf 18 -preset slow -c:a copy clip_1080.mp4 

That command is not tested on a real generated clip, since I'm not publishing any generated results here. It is the same scale filter the script ran on PNGs. If you want to test on your own clip, extract a frame, downscale it, upscale it, and run the metrics from this script on the pair.

Where it fits

The 480p or 720p guide covers the choice the studio actually offers. If you're deciding whether the sharper 720p is worth it before going anywhere near an upscaler, start there. Then upload only a photo of yourself, an adult who agreed, or your own pet, and label the result as an AI remake.

For reference, checked on November 13, 2026 at 09:02 (UTC+8): one clip at hittheroaddude.app is $9.99 at 480p (6 credits) or $12.99 at 720p (14 credits), with credit packs for signed-in accounts at $27.99 for 30 credits, $71.99 for 100 and $129.99 for 200 (pricing).