Anyone who has worked at a print shop (percetakan) knows this customer. They want a 1-metre banner for their warung, a framed photo for a wedding, or a poster for an event, and the only file they have is a photo someone sent them on WhatsApp. It looks fine on a phone. Printed at full size, it’s a blurry, blocky mess.
The usual options are bad. Print it anyway and get a complaint. Tell the customer to “find the original”, which they can’t. Or spend 20 minutes in Photoshop trying to make it look less bad.
AI upscaling is one of the few AI tools that does this job well enough to charge for, as long as you know where it fails. In this guide you’ll set up Real-ESRGAN, a free, open-source upscaling model that runs on your own computer, and wrap it in a short Python script that:
- works out how many pixels a print size actually needs (e.g. 100 cm wide at 150 DPI)
- skips the AI entirely when the photo is already big enough
- upscales 4× with Real-ESRGAN when it isn’t, then resizes to the exact print size and saves the DPI in the file
- makes a before/after comparison you can send the customer before you print
Software cost: $0. No API, no subscription, no per-image fee, and the customer’s photos never leave your machine. Setup takes about 30 minutes.
What you need
| Item | Cost | Notes |
|---|---|---|
| A computer with a Vulkan-capable GPU | What you already have | Most Intel/AMD integrated graphics from the last ~8 years, any recent NVIDIA/AMD card, Apple Silicon Macs |
realesrgan-ncnn-vulkan | $0 | Portable build from github.com/xinntao/Real-ESRGAN (BSD-3-Clause licence) |
| Python 3.10+ and Pillow | $0 | pip install pillow |
| Upscayl (optional) | $0 | Free desktop app (upscayl.org) built on the same family of models, if you want a GUI for one-off jobs |
| A way to show customers results | $0 | WhatsApp is fine |
Why this specific tool: the ncnn-vulkan build of Real-ESRGAN is a single executable plus a models folder. No CUDA, no PyTorch, no gigabytes of Python dependencies. It runs on a cheap laptop with integrated graphics, just slower.
The maths that decides every job
Print quality comes down to pixels per inch at the final size. The formula:
pixels needed = (size in cm / 2.54) × DPI
How much DPI you need depends on viewing distance, not on what the customer thinks “HD” means:
| Product | Viewing distance | DPI that looks fine |
|---|---|---|
| Photo print, A4 poster, flyer | Held in hand | 300 |
| A2/A1 poster on a wall | 1–2 m | 150 |
| Shop banner, spanduk, X-banner | 3 m+ | 72–100 |
Now apply it to a typical WhatsApp image. With standard quality, WhatsApp commonly shrinks photos to around 1,600 px on the long side (the newer “HD” option keeps more, but most people don’t use it, and forwarded images are often smaller still).
- 1,600 px at 300 DPI = 13.5 cm. Roughly a 4R (10×15 cm) photo print, and that’s the limit.
- 1,600 px at 150 DPI = 27 cm. Not enough for an A2 poster (42 cm wide).
- After 4× upscaling: 6,400 px at 150 DPI = 108 cm. Enough for a 1 m banner.
So the honest pitch is: AI upscaling turns a phone-sized image into something that holds up on a wall or a banner. It does not turn a thumbnail into a gallery print. Keep that sentence in mind when you quote a customer.
Step 1: Install Real-ESRGAN (ncnn-vulkan build)
Go to the Releases page of github.com/xinntao/Real-ESRGAN and, under release v0.2.5.0, download the realesrgan-ncnn-vulkan-… zip for your OS (Windows, Ubuntu, or macOS). Unzip it into a project folder.
Linux:
mkdir ~/upscale && cd ~/upscale
unzip ~/Downloads/realesrgan-ncnn-vulkan-*-ubuntu.zip -d esrgan
chmod +x esrgan/realesrgan-ncnn-vulkan
# Make sure Vulkan works (Ubuntu/Debian)
sudo apt install mesa-vulkan-drivers vulkan-tools
vulkaninfo --summary | grep deviceName
If vulkaninfo lists your GPU, you’re set. NVIDIA users need the proprietary driver installed; it ships its own Vulkan support. If the zip unpacks into a subfolder, move its contents up so the binary sits at esrgan/realesrgan-ncnn-vulkan next to esrgan/models/.
macOS: after unzipping, macOS will block the unsigned binary. Clear the quarantine flag once:
xattr -d com.apple.quarantine esrgan/realesrgan-ncnn-vulkan
chmod +x esrgan/realesrgan-ncnn-vulkan
Windows: unzip into C:\upscale\esrgan. The file is realesrgan-ncnn-vulkan.exe. Update your GPU driver from Intel/AMD/NVIDIA if the first run fails.
Test it on any photo:
./esrgan/realesrgan-ncnn-vulkan -i test.jpg -o test_x4.png -n realesrgan-x4plus -m esrgan/models
The zip includes a few models. The two you’ll use:
realesrgan-x4plus— general photos. This is the default for print work.realesrgan-x4plus-anime— illustrations, flat-colour artwork, cartoons. Often better on simple drawings and graphics too.
Time the test run. That number, multiplied by your weekly job count, tells you whether your current machine is good enough. On a decent discrete GPU it’s a few seconds per photo. On older integrated graphics it can take a minute or more for a large image. Both are workable for a print shop doing a handful of jobs a day.
Step 2: Install Python and Pillow
cd ~/upscale
python3 -m venv .venv && source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install pillow pillow-heif
mkdir inbox out
pillow-heif lets the script open iPhone .heic photos, which customers will send you.
Step 3: The print-prep script
Save this as prep.py in ~/upscale:
import argparse, os, subprocess, sys, tempfile
from pathlib import Path
from PIL import Image, ImageOps
try: # iPhone photos are HEIC by default
from pillow_heif import register_heif_opener
register_heif_opener()
except ImportError:
pass
Image.MAX_IMAGE_PIXELS = None # print files are big; these are our own files
BIN = Path(os.environ.get("ESRGAN_BIN", "esrgan/realesrgan-ncnn-vulkan"))
EXTS = {".jpg", ".jpeg", ".png", ".webp", ".heic"}
def needed_px(cm: float, dpi: int) -> int:
return round(cm / 2.54 * dpi)
def esrgan_x4(src: Path, dst: Path, model: str, tile: int) -> None:
cmd = [str(BIN), "-i", str(src), "-o", str(dst), "-n", model,
"-s", "4", "-m", str(BIN.parent / "models"), "-f", "png"]
if tile:
cmd += ["-t", str(tile)] # smaller tiles = less GPU memory
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0 or not dst.exists():
sys.exit(f"Real-ESRGAN failed on {src.name}:\n{result.stderr[-800:]}")
def comparison(original: Image.Image, final: Image.Image, path: Path) -> None:
"""Side by side: plain enlargement vs AI, same 800x800 crop from the centre."""
plain = original.resize(final.size, Image.LANCZOS)
box_w, box_h = min(800, final.width), min(800, final.height)
left, top = (final.width - box_w) // 2, (final.height - box_h) // 2
box = (left, top, left + box_w, top + box_h)
sheet = Image.new("RGB", (box_w * 2 + 20, box_h), "white")
sheet.paste(plain.crop(box), (0, 0))
sheet.paste(final.crop(box), (box_w + 20, 0))
sheet.save(path, quality=90)
def process(path: Path, args) -> None:
img = ImageOps.exif_transpose(Image.open(path)).convert("RGB")
target_w = needed_px(args.width_cm, args.dpi)
target_h = round(img.height * target_w / img.width)
factor = target_w / img.width
print(f"{path.name}: {img.width}x{img.height}px -> needs {target_w}x{target_h}px "
f"({factor:.1f}x) for {args.width_cm:g} cm @ {args.dpi} DPI")
if factor <= 1:
source, note = img, "already big enough, no AI used"
else:
if factor > 4:
print(f" WARNING: needs {factor:.1f}x. AI gives 4x; the rest is plain "
f"resizing. Max honest size at {args.dpi} DPI: "
f"{img.width * 4 / args.dpi * 2.54:.0f} cm wide.")
with tempfile.TemporaryDirectory() as tmp:
clean = Path(tmp) / "in.png" # rotation fixed, HEIC converted
img.save(clean)
up = Path(tmp) / "up.png"
esrgan_x4(clean, up, args.model, args.tile)
source = Image.open(up).convert("RGB")
note = f"AI 4x with {args.model}"
final = source.resize((target_w, target_h), Image.LANCZOS)
out = Path("out") / f"{path.stem}_{args.width_cm:g}cm_{args.dpi}dpi.jpg"
final.save(out, quality=95, dpi=(args.dpi, args.dpi), subsampling=0)
if factor > 1:
comparison(img, final, Path("out") / f"{path.stem}_compare.jpg")
print(f" -> {out} ({note})")
if __name__ == "__main__":
p = argparse.ArgumentParser(description="Prepare photos for print at a given size")
p.add_argument("--width-cm", type=float, required=True)
p.add_argument("--dpi", type=int, default=150)
p.add_argument("--model", default="realesrgan-x4plus")
p.add_argument("--tile", type=int, default=0, help="e.g. 256 if you run out of GPU memory")
args = p.parse_args()
files = sorted(f for f in Path("inbox").iterdir() if f.suffix.lower() in EXTS)
if not files:
sys.exit("No images in inbox/")
for f in files:
process(f, args)
A few deliberate choices in there:
- The script fixes rotation first. Phone photos store orientation in EXIF. Pillow’s
exif_transposeapplies it, and the upscaler gets a clean, upright PNG. Skip this and you’ll eventually print a sideways banner. - AI is only used when needed. If the customer sent a 6,000 px camera original, there’s nothing to fix. Running AI on it anyway just adds the risk of artefacts.
- One 4× pass, then normal resizing. Running Real-ESRGAN twice for 16× looks impressive on screen and bad in print: the second pass “enhances” the first pass’s invented textures. The script warns you instead.
- DPI is written into the JPEG. Many layout and RIP programs read it, so the file opens at the right physical size.
subsampling=0keeps full colour detail in the JPEG, which matters for sharp edges.
Step 4: Run a real job
Drop the customer’s photo into inbox/, then:
# 100 cm wide banner, viewed from a few metres
python prep.py --width-cm 100 --dpi 100
# A3 poster (29.7 cm wide) viewed up close
python prep.py --width-cm 29.7 --dpi 300
# Illustration or cartoon-style artwork
python prep.py --width-cm 60 --dpi 150 --model realesrgan-x4plus-anime
Example output:
warung_menu.jpg: 1280x960px -> needs 3937x2953px (3.1x) for 100 cm @ 100 DPI
-> out/warung_menu_100cm_100dpi.jpg (AI 4x with realesrgan-x4plus)
Open out/…_compare.jpg. The left half is a plain enlargement, the right half is the AI version, same crop at 100%. Send that image to the customer before printing. It sets expectations, it shows the value of what you did, and it gives them the chance to say “the face looks weird” before the banner is printed instead of after.
If the binary crashes or produces a black image on a large photo, your GPU ran out of memory. Re-run with --tile 256 (or 128). It’s slower but uses much less memory.
On Windows, set the binary path before running:
$env:ESRGAN_BIN="esrgan\realesrgan-ncnn-vulkan.exe"
python prep.py --width-cm 100 --dpi 100
Turning it into income
There are three realistic ways people get paid for this. None of them is passive.
1. Add it to an existing print shop’s workflow. This is the strongest use. The shop already has the customers with bad files. Instead of turning jobs away or printing blur, they offer a “perbaikan resolusi” add-on. You either run it in-house, or you sell the setup to a shop owner: install the tool, set up the script, and teach the staff the DPI table and the comparison step.
2. Do it as a service for nearby print shops and event organisers. They send files on WhatsApp, you return print-ready files plus the comparison image. Turnaround is the selling point.
3. Online marketplaces. Fiverr has plenty of “image upscaling” and “photo enhancement” gigs. Its minimum gig price is $5 and Fiverr keeps 20% of each order, so a $5 gig pays you $4. That market is crowded because anyone can install Upscayl. You only win there if you sell a result (“print-ready file for your 2 m banner, checked”) rather than “4× upscale”.
Honest cost and time breakdown
| Item | Cost |
|---|---|
| Software (Real-ESRGAN, Python, Pillow) | $0 |
| Per-image fee | $0 (runs locally) |
| Electricity | Negligible for a few minutes of GPU time per job |
| Hardware | The computer you have; a used GPU only if your run times annoy you |
| Your time per job | About 3–10 minutes: receive file, choose size/DPI, run, check the comparison, reply |
So almost all the cost is your time, and the price should reflect that plus the risk you take on (reprints aren’t free). As an example only: if you charge Rp 25,000 for a print-prep job and it takes 6 minutes, that’s an effective Rp 250,000 per hour of work. You won’t get ten jobs every hour, though. A realistic picture for one print shop is a few such files a day, which makes this a useful add-on that saves reprints and lost customers, not a business by itself. Check what shops near you already charge for design fixes before you set a number.
Reality check: what does NOT work
Logos and text. This is the big one. Real-ESRGAN is trained on photos. It makes blurry text smoother, not correct. Small letters come out wobbly or melted, and a logo pulled from a WhatsApp profile picture will never look professional on a 2 m banner. For logos, the right answer is redrawing or vectorising (Inkscape’s Path → Trace Bitmap is free and works well on simple logos), or getting the original file from whoever designed it. The -anime model gives a quick improvement on flat graphics, but don’t promise a “sharp logo”.
Tiny faces. If a face is 40 px wide in the original, the AI has to invent the eyes, teeth and skin. The result looks waxy or like a slightly different person. Customers notice this immediately on a memorial photo or a wedding print, and it’s the fastest way to lose their trust. That’s why the comparison image exists. Face-restoration models such as GFPGAN go further, but they change faces even more, so treat them with the same caution, and never use any of this on ID, passport or legal photos.
Heavy JPEG damage and screenshots of screenshots. The x4plus model removes some compression blockiness. It can’t recover detail that was destroyed three forwards ago. Ask the customer for the original first: in WhatsApp, sending a photo as a Document skips compression. That one tip fixes more jobs than any AI model.
Promising a size the pixels can’t support. The script prints the maximum honest width. Respect it. If the customer insists on bigger, write down that they were told, and let them decide.
Upscaling images nobody has rights to. A photo found on Google, or a brand’s logo for a “lookalike” banner, is a copyright and trademark problem. Upscaling doesn’t change who owns it. Make it a shop policy to ask.
Colour expectations. The script outputs RGB. Most print workflows convert to CMYK, and bright screen colours (neon greens, electric blue) will look duller on paper either way. That’s a printing issue, not an AI one, but the customer will blame whoever touched the file last. Mention it up front.
Running it on images that are already fine. Over-processing a good camera photo gives grass, hair and fabric a slightly painted look. The script skips AI when the factor is 1× or less. Don’t override that because “AI makes it better”. It doesn’t.
Selling “AI upscaling” as the product. Free apps like Upscayl do the same upscale with one click. What people pay for is the judgement: the right size, the right DPI, the honest “this will look soft”, and a file that prints correctly the first time.
Final checklist
-
vulkaninfo(or a test run) confirms the GPU works - Test run timed on your machine
- DPI table saved on your phone or stuck next to the counter
- Customers asked to send photos as a WhatsApp Document first
- Comparison image sent and approved before every print
- Logos and text routed to vectorising or redesign, not upscaling
- Faces checked at 100% before printing
- Max honest print size respected, or the customer’s choice written down
Start with your own old phone photos: run one at 30 cm and 300 DPI and one at 100 cm and 100 DPI, and print both. Once you’ve seen where it holds up and where it falls apart on paper, you’ll know exactly what to promise customers.
For another way to turn AI image work into recurring clients, see AI for Real Estate Agents: Listing Descriptions and Photo Enhancement as a Monthly Retainer.