Module 8 — Upscaling and face restoration
BoisClair's marketing agency asks for the walnut chair on a large poster — A2 at 300 dots per inch, i.e. 4961 by 7016 pixels. The 1024 by 1024 hero shot from module 3 is a tenth of that. This module is about going from a native-resolution generation to a print-ready image, and about restoring faces when the catalog includes a lifestyle shot with a person in it. Two families of tools, two failure modes to avoid.
Two families of upscalers
Dedicated super-resolution networks — Real-ESRGAN, ESRGAN, SwinIR — take an image in and return a bigger image. They are fast (fractions of a second per megapixel on a consumer GPU), they preserve the original faithfully, and they cannot invent detail that was not implied by the original pixels. Real-ESRGAN with the x4plus model is the pragmatic default; x4plus_anime exists for illustrated content.
Diffusion upscalers — the SDXL upscaler, sd-x4-upscaler for SD 1.5, or the "hi-res fix" workflow — encode the input into latent space, add noise, and denoise at the target resolution with the U-Net. They can hallucinate plausible detail that was never in the original: skin pores, wood grain fibers, individual threads in a linen. They are slower and can drift the identity of the subject.
Which family to pick depends on whether you want a faithful enlargement (Real-ESRGAN) or a detail-enhanced enlargement (diffusion). For product photography, faithful is safer. For creative posters, hallucinated detail can look magnificent when it does not go wrong.
Real-ESRGAN: the pragmatic path
from realesrgan import RealESRGANer
from basicsr.archs.rrdbnet_arch import RRDBNet
from PIL import Image
import numpy as np
model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4)
upsampler = RealESRGANer(
scale=4,
model_path="RealESRGAN_x4plus.pth",
model=model,
half=True,
)
image = Image.open("boisclair_walnut.png")
np_img = np.array(image)
output, _ = upsampler.enhance(np_img, outscale=4) # 4096 x 4096
Image.fromarray(output).save("boisclair_walnut_x4.png")
Three things worth knowing.
Fixed scale factors (typically x2, x4). Use outscale to reach an intermediate factor via internal resizing; the visible quality peak is at the trained factor.
Half precision. half=True cuts memory and roughly halves the time on a GPU with tensor cores. Rounding errors are invisible at print sizes.
Tile size on large inputs. Very large images run out of VRAM; RealESRGANer(tile=256, tile_pad=10) splits the image into tiles that fit and stitches them back. Padding between tiles avoids visible seams.
Diffusion upscaler with the "generate, then refine" pattern
For hero shots on the BoisClair project, a two-step pattern beats a single-step upscale.
- Generate the hero at 1024 with SDXL base.
- Refine it with SDXL Refiner OR with the SDXL image-to-image pipeline at 1536 or 2048 with a low denoising strength (0.2 to 0.35). The pipeline adds plausible detail without drifting the composition.
- Optionally, run Real-ESRGAN x2 on top of the refined image to reach print resolution.
from diffusers import StableDiffusionXLImg2ImgPipeline
pipe = StableDiffusionXLImg2ImgPipeline.from_pretrained(
"stabilityai/stable-diffusion-xl-base-1.0",
torch_dtype=torch.float16, variant="fp16",
).to("cuda")
hero = Image.open("boisclair_hero_1024.png").resize((2048, 2048))
refined = pipe(
prompt="walnut mid-century dining chair, sharp focus, editorial catalog",
image=hero,
strength=0.25,
num_inference_steps=25,
guidance_scale=6.0,
).images[0]
The low strength is the whole trick. At 0.25 the model does not reinvent the chair — it only decides what the fine detail should look like at the new resolution.
Artifacts to watch
Three failure modes recur across projects.
Waxy skin. Faces upscaled with a diffusion upscaler often look plasticised — pores are gone, edges too smooth. The fix is a dedicated face-restoration pass (below), not a stronger diffusion pass.
Cartoon lines on natural textures. Real-ESRGAN with the anime model on a photograph looks like a badly traced illustration. Match the model to the content type.
Hallucinated detail that reads as wrong. A diffusion upscaler can invent wood grain that flows against the direction of the plank, or fabric threads that do not weave together. Read the print at 100 % scale before shipping — the eye catches these at scale, not on a screen preview.
Face restoration
For catalog shots featuring a person, both diffusion upscalers and Real-ESRGAN often mangle the face — one of the most difficult things for a super-resolution network to reproduce is a coherent identity. Two dedicated tools solve this cleanly.
GFPGAN and CodeFormer are face-specific networks trained to restore high-fidelity faces from degraded inputs. They detect faces in the image, restore each face separately, and blend the result back. CodeFormer has a w parameter that trades fidelity for quality (w=0.7 is a good default: recognizable identity, cleaned-up features).
from gfpgan import GFPGANer
restorer = GFPGANer(
model_path="GFPGANv1.4.pth",
upscale=2,
arch="clean",
channel_multiplier=2,
)
_, _, restored = restorer.enhance(np_img, has_aligned=False, only_center_face=False, paste_back=True)
The order of operations matters. Restore faces on the native-resolution image first, then upscale the whole image with Real-ESRGAN. The reverse order sometimes works but is more prone to identity drift on very small faces.
Print resolution and the file that leaves your machine
Three practical facts for the BoisClair poster.
Effective PPI at viewing distance. A poster viewed at two meters does not need 300 pixels per inch — 150 is fine. A brochure held at 30 centimeters needs the full 300. Print resolution is a distance-dependent number, not a universal target.
Save in the right format. 8-bit PNG for web, 16-bit TIFF for print at professional printers, JPEG at quality 95+ for social media. Never let a JPEG at quality 80 be your archive — recompression on the next edit accumulates.
Embed the ICC color profile. sRGB for screen, Adobe RGB or the printer's specific profile for print. Without a profile, the printer guesses, and the walnut turns orange.
Native SDXL 1024 → image-to-image at 2048 with strength=0.25 → Real-ESRGAN x2 to 4096 → optional face-restoration pass on any human present → export as 16-bit TIFF for the print run. Total time: about a minute on a consumer GPU, per hero image.
In summary
- Real-ESRGAN and its family produce faithful enlargements at high speed; diffusion upscalers hallucinate plausible detail but can drift subject and identity.
- The reliable pattern for hero shots is native generation → image-to-image at higher resolution with low strength (0.25) → Real-ESRGAN x2 to reach print pixels.
- Face restoration (GFPGAN, CodeFormer at
w=0.7) is a dedicated step; run it before the final upscale, not after, to avoid identity drift on small faces. - Print resolution is viewing-distance dependent; embed the correct ICC profile and archive in a lossless format (16-bit TIFF or high-quality PNG).
Next module: keeping all this affordable — half precision, attention slicing, CPU offload, resolution versus memory, and time per image on modest hardware.