Pixels2GenAI
Path iii Generative
M 12 · 12.1.4 · conceptual

12.1.4 Pix2Pix Applications

Translate one image into another (label-to-photo, sketch-to-colour) with a conditional GAN: U-Net generator preserves spatial structure, PatchGAN discriminator enforces local realism, L1+adversarial loss balances accuracy against sharpness.

Duration35–45 min
Levelintermediate-advanced
Load4 core concepts
PrereqsLessons 12.1.1, 12.1.2, 12.1.3 (the GAN family); free Kaggle account for the optional from-scratch retrain
Fig. 1 Anime line-art → colourised pairs from a Pix2Pix model trained 100 epochs on the Kaggle Anime Sketch Colorization dataset. The output preserves every line of the input while adding plausible colour and shading.

Overview

DCGAN Lesson 12.1.2 generates fabric patterns from random noise. StyleGAN Lesson 12.1.3 generates them with controllable disentangled latents. Both are unconditional — the input is noise, and the only thing controlling the output is which random seed you picked.

Pix2Pix, introduced by Isola, Zhu, Zhou and Efros at CVPR 2017 [1], does something structurally different. The input is no longer noise — it’s another image. The model learns to translate input images into output images while preserving spatial structure. Feed it a black-and-white line drawing, get a colourised version. Feed it a building’s segmentation label, get a photorealistic facade. Feed it a daytime satellite image, get the night-time version. The same architecture handles all of these tasks; what changes is the paired training data.

This lesson works through two demonstrations on the same architecture: a pre-trained facades model (label-to-photo, ready to use) for Exercises 1 and 2, and an optional from-scratch retrain on the Anime Sketch Colorization Pair dataset from Kaggle [2] for Exercise 3. The underlying idea — conditional GAN, U-Net generator, PatchGAN discriminator, L1+adversarial loss — generalises to almost any image-to-image translation task you can find paired data for.

Learning objectives

  1. Distinguish conditional from unconditional GANs and explain what the conditioning input adds to the training signal.
  2. Build a U-Net generator and explain why the skip connections are non-negotiable for pixel-aligned output.
  3. Understand the PatchGAN discriminator’s patch-level real/fake scoring and predict when it outperforms a full-image discriminator.
  4. Combine adversarial loss with L1 reconstruction loss and tune λ for sharpness vs accuracy trade-off.

Quick start — translate a facade label into a building photo

The pre-trained facades_label2photo weights are on the GitHub Release. Place under checkpoints/, load with the helper script, run on one of the sample facade labels.

generator_weights.pth — U-Net generator (~208 MB, GitHub Release) discriminator_weights.pth — PatchGAN discriminator (~11 MB)
python · quick-start.py
import torch
from facades_generator import create_facades_generator
from PIL import Image
import numpy as np
import matplotlib.pyplot as plt

# Load pre-trained facades U-Net generator
generator = create_facades_generator('checkpoints/generator_weights.pth')
generator.eval()

# Load a CMP Facades segmentation label
label = Image.open('sample_facades/base/cmp_b0001.png').convert('RGB').resize((256, 256))
label_arr = np.array(label).astype(np.float32) / 255.0
x = torch.from_numpy((label_arr - 0.5) / 0.5).permute(2, 0, 1).unsqueeze(0)

with torch.no_grad():
    facade = generator(x)

facade = ((facade.squeeze().permute(1, 2, 0).numpy() + 1) / 2).clip(0, 1)
plt.imsave('quick_start_output.png', facade)

The input is a coloured segmentation label (different colours = walls, windows, doors, sky). The output is a photorealistic building photo that follows the label’s structure but fills it with realistic textures.

Fig. 2 Quick-start output. The input segmentation label is converted into a coherent building photo — the architectural layout comes from the input; the textures come from the model.

Core concepts

Concept 1 — Conditional vs unconditional generation

The structural shift from DCGAN to Pix2Pix is that the input vector is no longer arbitrary noise but a meaningful image. Mirza and Osindero’s 2014 paper [3] introduced the conditional GAN framework in the abstract — feed any conditioning information c alongside the noise — but Pix2Pix specialised it to a powerful case: when c is itself an image, you get image-to-image translation.

# Unconditional GAN (DCGAN)
G(z) → image                    # z = noise, output is random

# Conditional GAN with image conditioning (Pix2Pix)
G(x) → image                    # x = input image, output is structured translation
Fig. 3 DCGAN (left) generates a random image from noise. Pix2Pix (right) translates an input image into an output image, with the spatial structure of the input preserved in the output.

The conditioning shifts what the generator has to learn. Instead of “produce something that looks like a real image,” the task becomes “produce the output image that corresponds to this input image.” Training data becomes paired — every input must have a known correct output — but the supervision is much stronger as a result. The model can converge faster and produce more controllable results, in exchange for needing paired data.

This same idea later evolved into the conditioning patterns used by Stable Diffusion (text-to-image, where the condition is a text embedding rather than an image) and ControlNet (Lesson 12.3.2, where structural conditioning is layered onto an existing text-to-image model).

Concept 2 — U-Net generator and the necessity of skip connections

Pix2Pix’s generator is a U-Net [5] — the same encoder-decoder-with-skip-connections architecture used in Lesson 12.3.1’s DDPM for noise prediction. For image-to-image translation, the skip connections are not a tuning choice; they’re a structural requirement.

Fig. 4 U-Net for image-to-image translation. Encoder (left) downsamples 256→128→...→1; decoder (right) upsamples back to 256; skip connections (pink) preserve fine spatial detail across the bottleneck.

Consider what happens without skip connections. The encoder downsamples 256×256 → 128 → 64 → 32 → 16 → 8 → 4 → 2 → 1 (a 1×1 bottleneck with all the input information compressed into one vector). The decoder then has to reconstruct a 256×256 output that aligns pixel-for-pixel with the input. Without skip connections, every spatial detail has to survive the bottleneck round-trip — and most of it doesn’t. The output ends up blurry and misaligned with the input lines.

Skip connections fix this by piping each encoder layer’s output directly to the corresponding decoder layer at the same resolution. Fine detail bypasses the bottleneck entirely; the bottleneck only has to encode the high-level transformation (this label means “wall”, this means “window”), while the skip connections carry the spatial layout that the output must respect.

python · U-Net forward pass with skip connections
def forward(self, x):
    # Encoder — store every output for skip connections
    d1 = self.down1(x)    # 256 → 128
    d2 = self.down2(d1)   # 128 → 64
    d3 = self.down3(d2)   # 64 → 32
    d4 = self.down4(d3)   # 32 → 16
    d5 = self.down5(d4)   # 16 → 8
    d6 = self.down6(d5)   # 8 → 4
    d7 = self.down7(d6)   # 4 → 2
    d8 = self.down8(d7)   # 2 → 1 (bottleneck)

    # Decoder — concatenate corresponding encoder feature at each step
    u1 = self.up1(d8, d7)
    u2 = self.up2(u1, d6)
    u3 = self.up3(u2, d5)
    u4 = self.up4(u3, d4)
    u5 = self.up5(u4, d3)
    u6 = self.up6(u5, d2)
    u7 = self.up7(u6, d1)

    return self.final(u7)  # → 256x256

Concept 3 — PatchGAN: discriminator that scores patches, not images

A standard GAN discriminator outputs a single scalar per image — real or fake. For high-resolution image-to-image translation, this single-scalar signal is too coarse: the discriminator can be fooled by a globally plausible image that has local artefacts (smeared windows, misaligned tiles, fuzzy edges).

Pix2Pix’s PatchGAN discriminator outputs a grid of real/fake predictions — typically 30×30 — where each prediction corresponds to a 70×70 pixel patch in the input. The generator is now penalised on every patch, not just the overall image. This concentrates training pressure on local texture quality, which is exactly where the L1 loss (Concept 4) is weakest.

Fig. 5 PatchGAN's structure. The discriminator outputs a 30×30 grid of real/fake scores, each corresponding to a 70×70 patch in the input. Training pressure is local; the generator is rewarded for *every* patch looking realistic.

The discriminator receives both the input condition and the output image, concatenated along the channel dimension. This lets it check not just “is this output realistic?” but “is this output a valid translation of the given input?”

python · PatchGAN forward pass
def forward(self, sketch, target):
    x = torch.cat([sketch, target], dim=1)   # 3 + 3 = 6 channels
    return self.model(x)                     # → (batch, 1, 30, 30)

Three practical advantages stack up: detail-awareness (more sensitive to local artefacts), denser training signal (one loss value per patch, not per image), and parameter efficiency (the discriminator can be smaller because it only ever processes a single patch’s receptive field at a time).

Concept 4 — L1 + adversarial loss: accuracy from L1, sharpness from the GAN

Pix2Pix’s loss combines two complementary terms:

L_total = L_GAN(G, D) + λ · L_L1(G)

Adversarial loss is the standard GAN objective: generator tries to fool the discriminator, discriminator tries to catch fakes. This term rewards perceptual realism — sharpness, plausible textures, no obvious artefacts.

L1 reconstruction loss is the pixel-wise absolute difference between the generator’s output and the ground-truth output: L_L1 = E[|y - G(x)|]. This term rewards literal pixel accuracy. The L1 norm (rather than L2) is chosen because it’s less sensitive to outliers and produces less blurry results than L2 would.

Why both? Each fixes the other’s weakness. Pure adversarial loss produces sharp but spatially-imprecise outputs — the generator can satisfy the discriminator with a realistic image that doesn’t match the input. Pure L1 loss produces blurry but spatially-precise outputs — the L1 minimum at any pixel is the average of all plausible colours, which is grey-ish. Combining the two with λ = 100 (the conventional default in the original paper) lets the L1 pin the output to the right location and the GAN sharpen the textures within that location.

Exercises

EXECUTE I.

Run the pre-trained facades model on sample labels

generator_weights.pth — 208 MB U-Net generator exercise1_observe.py — multi-facade comparison facades_generator.py — generator loader helper

Place the generator checkpoint at checkpoints/facades_generator.pth, then:

bash
python exercise1_observe.py

Output: exercise1_output.png — a grid showing several facade segmentation labels alongside their generated photo equivalents. The CMP Facades dataset [6] is also on Kaggle if you want more inputs to try.

Fig. 6 Each row is one example: segmentation label on the left, generated photo on the right. The model has learned a consistent visual style for each label colour — walls are textured plaster, windows are darker glass, doors are wooden.

Reflection

  1. The generated photos all share a particular visual style — warm beige walls, dark wooden doors, regular window grids. Where does that style come from?
  2. The same facade label decoded twice with the same model produces the same output. How does this differ from DCGAN, where the same noise vector also produces the same output?
  3. What would happen if you fed a label that wasn’t a facade (e.g. a random colour blob) into this generator?
  4. The PatchGAN discriminator (Concept 3) outputs a 30×30 grid of patch scores. How does this granularity affect what the generator learns to do?
MODIFY II.

Modify labels and see how output changes

exercise2_explore.py — label-modification explorations
bash
python exercise2_explore.py

Three explorations:

Goal 1 — Window count. Modify a facade label to add or remove windows; observe how the generator responds. The output should track the input’s structural changes.

Fig. 7 Window-count variations. The model faithfully tracks the structural change in the label — fewer windows in, fewer windows out.

Goal 2 — Colour palette injection. Try modifying the label colour-coding to see how strict the model is about mapping label colours to specific materials.

Fig. 8 The label-colour-to-material mapping is learned, not hard-coded. Off-distribution colours produce ambiguous textures.

Goal 3 — Partial labels. Provide a label with large unlabelled regions; observe how the model fills them in.

Fig. 9 Partial labels test the generator under ambiguity — when the input doesn't specify enough structure, the model defaults to its training-distribution average.
TRAIN III.

Train Pix2Pix from scratch on anime sketch colourisation

This exercise trains a full Pix2Pix from scratch on the Anime Sketch Colorization Pair dataset from Kaggle [2] — ~17,000 paired sketch/colour images.

exercise3_train.py — full Pix2Pix training (100 epochs, GPU) pix2pix_model.py — U-Net generator + PatchGAN discriminator preprocess_anime_sketches.py — Kaggle dataset preprocessor

Step 1 — Get the dataset. Visit kaggle.com/datasets/ktaebum/anime-sketch-colorization-pair, download (~14 GB compressed; free Kaggle account required), extract to anime_sketch_dataset/, then run:

bash
python preprocess_anime_sketches.py

This splits paired images into anime_processed/sketches/ and anime_processed/colors/ so the training script can find them.

Step 2 — Train.

bash
python exercise3_train.py

Configuration:

HyperparameterValue
Image size256×256
Batch size1 (the original paper’s choice)
GeneratorU-Net with 8 down/up blocks
DiscriminatorPatchGAN, 70×70 receptive field
OptimiserAdam, lr=2e-4, β₁=0.5
Loss weightλ_L1 = 100
Epochs100
Estimated time~3-5 hours on RTX 5070Ti

Sample images saved every epoch in training_results/. Loss curves and visual progression help diagnose training health.

Fig. 10 Training dynamics over 100 epochs. Adversarial losses oscillate (healthy); L1 loss decreases monotonically (also healthy).
Fig. 11 Epoch 25 — basic colour assignment learned, fine details still rough.
Fig. 12 Epoch 75 — colour palette consistent, shading and shadows starting to look natural.

Make it your own

After training, run the model on sketches you draw yourself. The model has seen ~17,000 anime sketches and generalises reasonably well to similar styles. For very different sketch styles (e.g., realistic line drawings), expect degraded results — that’s the limit of supervised translation: it knows the distribution it was trained on, not the universe of possible sketches.

Summary

Common pitfalls

  • Forgetting that paired data is required. Pix2Pix supervises pixel-by-pixel — without aligned ground truth, the L1 loss is undefined and the model can’t train.
  • Resizing input and output to different dimensions. The U-Net expects matching input/output resolutions; mismatch breaks the skip-connection alignment.
  • Setting λ_L1 = 0 to try “pure adversarial” — the result is sharp but spatially garbage. The L1 term is what enforces the correspondence.
  • Training with a batch size that’s too large. Pix2Pix’s original paper used batch size 1; PatchGAN works best when each forward pass sees one example fully rather than averaging across a batch.
  • Using a single-scalar discriminator. Without PatchGAN’s patch-level scoring, local artefacts go unpenalised and outputs look smeared up close.

References

  1. [1] Isola, P., Zhu, J.-Y., Zhou, T. & Efros, A. A. (2017). Image-to-Image Translation with Conditional Adversarial Networks (Pix2Pix). IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR). arxiv:1611.07004
  2. [2] Kaggle dataset. (2018). Anime Sketch Colorization Pair. kaggle.com/datasets/ktaebum/anime-sketch-colorization-pair
  3. [3] Mirza, M. & Osindero, S. (2014). Conditional Generative Adversarial Nets. arXiv preprint. arxiv:1411.1784
  4. [4] Zhu, J.-Y., Park, T., Isola, P. & Efros, A. A. (2017). Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks (CycleGAN). IEEE International Conference on Computer Vision (ICCV). arxiv:1703.10593
  5. [5] Ronneberger, O., Fischer, P. & Brox, T. (2015). U-Net: Convolutional Networks for Biomedical Image Segmentation. MICCAI. arxiv:1505.04597
  6. [6] Tyleček, R. & Šára, R. (2013). Spatial Pattern Templates for Recognition of Objects with Regular Structure. German Conference on Pattern Recognition (GCPR). CMP Facades dataset. cmp.felk.cvut.cz/~tylecr1/facade
  7. [7] Goodfellow, I., Pouget-Abadie, J., Mirza, M., Xu, B., Warde-Farley, D., Ozair, S., Courville, A. & Bengio, Y. (2014). Generative Adversarial Nets. NeurIPS. arxiv:1406.2661
  8. [8] Wang, T.-C., Liu, M.-Y., Zhu, J.-Y., Tao, A., Kautz, J. & Catanzaro, B. (2018). High-Resolution Image Synthesis and Semantic Manipulation with Conditional GANs (Pix2PixHD). CVPR. arxiv:1711.11585