Images · Local AI

Upscale images locally with Real-ESRGAN

Turn one image into a 2x or 4x version on your own computer. The portable NCNN build uses Vulkan and needs no Python ML packages, CUDA, or cloud service.

AI upscaling reconstructs plausible detail; it cannot recover the exact original. Keep your source image, especially for archival or evidentiary work.

1. Download the engine and model

Choose the official Real-ESRGAN portable archive for your system. Each archive already contains the executable and its NCNN models, including realesrgan-x4plus.

Linux example:

mkdir real-esrgan && cd real-esrgan
curl -fLO https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesrgan-ncnn-vulkan-20220424-ubuntu.zip
unzip realesrgan-ncnn-vulkan-20220424-ubuntu.zip
chmod +x realesrgan-ncnn-vulkan

Keep the models/ directory beside the executable. The NCNN build uses paired .param and .bin files—not the PyTorch .pth model.

2. Add the Python wrapper

Download Python script

Save this file next to the executable and models/ directory, then make it executable with chmod +x upscale_image.py.

#!/usr/bin/env python3
from __future__ import annotations

import argparse
import os
import shutil
import subprocess
import sys
from pathlib import Path


class LocalUpscaler:
    supported_formats = frozenset({".jpg", ".jpeg", ".png", ".webp"})

    def __init__(self, binary: Path, models: Path) -> None:
        self.binary = binary.expanduser().resolve()
        self.models = models.expanduser().resolve()

    @staticmethod
    def default_output(input_path: Path, scale: int) -> Path:
        return input_path.with_name(f"{input_path.stem}-x{scale}.png")

    def upscale(
        self,
        input_path: Path,
        output_path: Path,
        scale: int,
        model: str,
    ) -> None:
        input_path = input_path.expanduser().resolve()
        output_path = output_path.expanduser().resolve()

        if not input_path.is_file():
            raise ValueError(f"input image does not exist: {input_path}")
        if input_path.suffix.lower() not in self.supported_formats:
            raise ValueError("input must be a JPG, PNG, or WebP image")
        if output_path.suffix.lower() not in self.supported_formats:
            raise ValueError("output must use .jpg, .jpeg, .png, or .webp")
        if input_path == output_path:
            raise ValueError("output must be different from the input image")
        if not self.binary.is_file():
            raise ValueError(f"Real-ESRGAN binary not found: {self.binary}")
        if not self.models.is_dir():
            raise ValueError(f"model directory not found: {self.models}")
        for suffix in (".param", ".bin"):
            model_file = self.models / f"{model}{suffix}"
            if not model_file.is_file():
                raise ValueError(f"model file not found: {model_file}")

        output_path.parent.mkdir(parents=True, exist_ok=True)
        output_format = output_path.suffix.lower().lstrip(".")
        if output_format == "jpeg":
            output_format = "jpg"
        subprocess.run(
            [
                str(self.binary),
                "-i",
                str(input_path),
                "-o",
                str(output_path),
                "-n",
                model,
                "-s",
                str(scale),
                "-m",
                str(self.models),
                "-f",
                output_format,
            ],
            check=True,
        )


def default_binary() -> Path:
    configured = os.environ.get("REALESRGAN_BIN")
    if configured:
        return Path(configured)
    executable = "realesrgan-ncnn-vulkan.exe" if os.name == "nt" else "realesrgan-ncnn-vulkan"
    installed = shutil.which(executable)
    return Path(installed) if installed else Path.cwd() / executable


def argument_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        description="Upscale one image locally with Real-ESRGAN NCNN Vulkan."
    )
    parser.add_argument("input", type=Path, help="JPG, PNG, or WebP input image")
    parser.add_argument("--scale", type=int, choices=(2, 4), default=2)
    parser.add_argument("--output", type=Path, help="Output path (default: NAME-xSCALE.png)")
    parser.add_argument("--binary", type=Path, help="Path to realesrgan-ncnn-vulkan")
    parser.add_argument("--models", type=Path, help="Path to the NCNN models directory")
    parser.add_argument("--model", default="realesrgan-x4plus", help="NCNN model name")
    return parser


def main() -> int:
    arguments = argument_parser().parse_args()
    binary = arguments.binary or default_binary()
    configured_models = os.environ.get("REALESRGAN_MODELS")
    models = arguments.models or (
        Path(configured_models) if configured_models else binary.parent / "models"
    )
    output = arguments.output or LocalUpscaler.default_output(
        arguments.input, arguments.scale
    )

    try:
        LocalUpscaler(binary, models).upscale(
            arguments.input,
            output,
            arguments.scale,
            arguments.model,
        )
    except (OSError, ValueError, subprocess.CalledProcessError) as error:
        print(f"upscale_image.py: {error}", file=sys.stderr)
        return 1

    print(output.expanduser().resolve())
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

3. Upscale an image

./upscale_image.py photo.jpg --scale 2
./upscale_image.py photo.jpg --scale 4 --output photo-large.png

If the wrapper lives elsewhere, point it to the extracted files:

REALESRGAN_BIN=/path/to/realesrgan-ncnn-vulkan \
REALESRGAN_MODELS=/path/to/models \
./upscale_image.py photo.jpg --scale 4

The default result is photo-x2.png or photo-x4.png. JPG, PNG, and WebP are supported. If the engine crashes, update your GPU driver and confirm that Vulkan is available.