Image Generation and Restoration
Image-to-image translation
Turning one kind of picture into another kind, either from matched pairs with pix2pix or from two unmatched piles with CycleGAN.
- 14 min read
- 3 reading levels
- Updated
Read these first
On this page 8
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
The short answer
Image-to-image translation takes a picture in one style and rewrites it in another, keeping the content.
The analogy
Think about a tailor copying a shirt. You hand over a shirt in cotton and ask for the same shirt in silk.
The tailor keeps every measurement. Collar, sleeve, shoulder, all identical. Only the material changes.
That is the job here. Keep the content, change the appearance. A satellite photo becomes a road map. A rough sketch becomes a finished drawing. A daytime street becomes the same street at night.
Two very different situations
You have matched pairs. For every input picture you also have the correct output picture. A building photo and the architect's line drawing of the same building.
This is the easier case, and it has a name: pix2pix. The model is shown the input, produces an output, and is told exactly how far off it was.
You have two piles and no pairs. A folder of horse photos and a folder of zebra photos. No horse has a matching zebra.
This is the harder case, and it has a name: CycleGAN. Nobody can tell the model what the right answer is, because nobody has it.
PAIRED (pix2pix) UNPAIRED (CycleGAN)
sketch -> model -> photo horses zebras
| | (500) (500)
+--- compare with ----+ | |
the real photo +--- no pairing --+How CycleGAN escapes having no answer key
The trick is a round trip. Turn the horse into a zebra. Then turn that zebra back into a horse.
You should get the original horse back. You do have the original horse, so you can check.
That single rule, called cycle consistency, is enough to stop the model from throwing the content away.
The honest problem with the round trip
Getting back where you started proves the model kept the information. It does not prove the model kept the meaning.
A model could map every horse to some zebra and back, in scrambled order. It would pass the test perfectly. Nothing in the round trip says horse number one must become the zebra standing in the same pose.
In practice models often do learn the sensible mapping, because the sensible mapping is the easiest one to find. But it is a tendency, not a guarantee, and you should treat it that way.
There is a stranger version of the same problem. Researchers found CycleGAN hiding the original picture inside the generated one. The pattern is far too faint for a human to see. The round trip then succeeds by reading the hidden copy.
Where you have seen this
- Phone camera modes that turn a photo into a watercolour or pencil sketch.
- Map apps that generate road maps from satellite imagery.
- Old black-and-white films released in colour.
- Medical tools that convert one scan type into another for comparison.
Remember this
- Paired data gives you a direct answer key, and a much easier problem.
- Unpaired data forces a round-trip rule called cycle consistency.
- Cycle consistency guarantees information is kept, not that the meaning is kept.
What to learn next
- The denoising U-Net — the network that took over this job from GANs.
- Image segmentation — where the U-Net architecture came from.
- Diffusion models — the generative family behind Palette and InstructPix2Pix.
Developer — Code and libraries.
Setup
pip install torch==2.5.1The failure that cycle consistency exists to prevent
Strip the problem to its skeleton. Eight distinct pieces of content in domain A, eight in domain B, no pairing. The adversarial loss can only insist that outputs look like B. It says nothing about which input becomes which output.
import torch, torch.nn as nn, torch.nn.functional as F
N = 8 # eight "classes" of content, e.g. eight horse poses that should map to eight zebra poses
def net(): return nn.Sequential(nn.Linear(N, 64), nn.ReLU(), nn.Linear(64, N))
def onehot(idx): return F.one_hot(idx, N).float()
def train(use_cycle, steps=1500, bs=64, seed=0):
torch.manual_seed(seed)
G, Fw, D = net(), net(), nn.Sequential(nn.Linear(N,64), nn.ReLU(), nn.Linear(64,1))
oG = torch.optim.Adam(list(G.parameters())+list(Fw.parameters()), lr=2e-3)
oD = torch.optim.Adam(D.parameters(), lr=2e-3)
bce = nn.BCEWithLogitsLoss()
for _ in range(steps):
a = onehot(torch.randint(0, N, (bs,)))
b = onehot(torch.randint(0, N, (bs,))) # UNPAIRED: b has nothing to do with a
fake = torch.softmax(G(a), -1)
oD.zero_grad()
(bce(D(b), torch.ones(bs,1)) + bce(D(fake.detach()), torch.zeros(bs,1))).backward(); oD.step()
oG.zero_grad()
fake = torch.softmax(G(a), -1)
loss = bce(D(fake), torch.ones(bs,1))
if use_cycle: # ||F(G(a)) - a||_1, the CycleGAN term
loss = loss + 10.0 * (torch.softmax(Fw(fake), -1) - a).abs().sum(-1).mean()
loss.backward(); oG.step()
with torch.no_grad():
eye = torch.eye(N)
mapped = G(eye).argmax(-1)
back = Fw(torch.softmax(G(eye), -1)).argmax(-1)
return mapped.tolist(), (back == torch.arange(N)).float().mean().item(), len(set(mapped.tolist()))
for use_cycle in (False, True):
m, cyc, distinct = train(use_cycle)
print(f"cycle loss {str(use_cycle):5s} A->B mapping {m} "
f"distinct outputs {distinct}/8 round-trip F(G(a))==a {cyc:5.1%}")
print("\nsame cycle-consistent model, three different random seeds:")
for s in (0, 1, 2):
m, cyc, distinct = train(True, seed=s)
print(f" seed {s}: {m} round-trip {cyc:5.1%}")cycle loss False A->B mapping [3, 3, 3, 3, 3, 3, 3, 3] distinct outputs 1/8 round-trip F(G(a))==a 12.5% cycle loss True A->B mapping [2, 0, 4, 6, 3, 1, 5, 7] distinct outputs 8/8 round-trip F(G(a))==a 100.0% same cycle-consistent model, three different random seeds: seed 0: [2, 0, 4, 6, 3, 1, 5, 7] round-trip 100.0% seed 1: [1, 6, 0, 3, 5, 4, 2, 7] round-trip 100.0% seed 2: [4, 7, 6, 5, 3, 7, 1, 2] round-trip 100.0%
Exact numbers depend on your PyTorch build. The three behaviours below reproduce.
Reading the output
Without cycle loss, total collapse. Every one of the eight inputs maps to output 3. The adversarial loss is satisfied, because output 3 is a perfectly valid member of domain B. Round-trip accuracy is 12.5%, which is one in eight, which is chance.
With cycle loss, a clean bijection. All eight outputs distinct, round trip 100%. The content survived the journey.
The bijection is arbitrary. Seeds 0, 1 and 2 give three different permutations, all scoring 100%. Cycle consistency bought invertibility, not correctness. Nothing here made input 0 map to output 0.
Seed 2 is the most interesting row. Its mapping [4, 7, 6, 5, 3, 7, 1, 2] contains 7 twice. At the level of the strongest output it is not even a bijection. Yet the round trip still scores 100%. The generator is hiding the input identity in the weaker components that argmax discards.
That is not an artefact of this toy. Chu, Zhmoginov and Sandler (2017), CycleGAN, a Master of Steganography, showed the same thing on images. CycleGAN hides a low-amplitude, high-frequency copy of the source in its output. The reverse generator reads it back. If you evaluate CycleGAN by cycle reconstruction error, you may be measuring steganography rather than translation.
The discriminator: PatchGAN
pix2pix and CycleGAN both score patches, not whole images. The discriminator emits a grid of real/fake scores, each covering a fixed window.
import torch, torch.nn as nn
# The pix2pix / CycleGAN discriminator: a small conv net that outputs a GRID of
# real/fake scores, one per overlapping patch, instead of one score per image.
D = nn.Sequential(
nn.Conv2d(3, 64, 4, 2, 1), nn.LeakyReLU(0.2), # C64
nn.Conv2d(64, 128, 4, 2, 1), nn.LeakyReLU(0.2), # C128
nn.Conv2d(128,256, 4, 2, 1), nn.LeakyReLU(0.2), # C256
nn.Conv2d(256,512, 4, 1, 1), nn.LeakyReLU(0.2), # C512, stride 1
nn.Conv2d(512, 1, 4, 1, 1), # one score per patch
)
x = torch.zeros(1, 3, 256, 256)
print("output grid:", tuple(D(x).shape), " -> one real/fake score per cell")
# Receptive field of one output cell, walked backwards through the layers.
rf, jump = 1, 1
for layer in reversed([m for m in D if isinstance(m, nn.Conv2d)]):
k, s = layer.kernel_size[0], layer.stride[0]
rf = rf * s + (k - s)
jump *= s
print(f"each score sees a {rf}x{rf} pixel patch, centres {jump} pixels apart")
print("parameters:", f"{sum(p.numel() for p in D.parameters()):,}")output grid: (1, 1, 30, 30) -> one real/fake score per cell each score sees a 70x70 pixel patch, centres 8 pixels apart parameters: 2,764,737
This is the exact "70x70 PatchGAN" from the pix2pix paper, reproduced from the layer definitions. Two consequences follow.
It is fully convolutional, so the same discriminator works at any input size. A 512x512 image produces a larger grid, not an error.
It cannot see global structure. A patch discriminator judges texture and local realism only. That is why pix2pix pairs it with an L1 term. The L1 loss handles low-frequency correctness: is the roof in the right place. The discriminator handles high-frequency realism: does the roof look like tiles.
The loss you should actually write
For pix2pix:
loss_G = adversarial(D(fake), real_label) + 100.0 * (fake - target).abs().mean()The weight of 100 on the L1 term is from the paper and is not a typo. Drop it to 1 and outputs get sharper and wrong. Use L1 rather than L2, because L2 averages over plausible answers and produces blur.
For CycleGAN, the full objective is four terms:
loss = adv_A2B + adv_B2A \
+ 10.0 * (F(G(a)) - a).abs().mean() \
+ 10.0 * (G(F(b)) - b).abs().mean() \
+ 5.0 * (G(b) - b).abs().mean() # identity loss: G must leave real B aloneThe identity term is what stops CycleGAN from shifting colours globally. Remove it and translated photos drift blue or orange.
Common mistakes
Using batch-norm with batch size 1. pix2pix trains with batch size 1, where batch-norm degenerates into instance normalisation with unstable statistics. Use nn.InstanceNorm2d, which is what the reference implementations do.
Forgetting the image buffer. CycleGAN trains the discriminator on a pool of the last 50 generated images, not only the newest. Without it the two networks oscillate.
Trusting cycle reconstruction as a quality metric. As above, it can be satisfied by hiding data. Judge on the forward output, with FID against the target domain and with human inspection.
Reaching for CycleGAN when you could get pairs. Paired data is dramatically better. Before committing to unpaired training, ask whether you can synthesise pairs by degrading the target domain. Every modern super-resolution pipeline does exactly that.
Try it yourself
Set the cycle weight to 0.5 instead of 10.0 and rerun. Find the weight at which the mapping stops being a bijection. That number is the real tuning parameter in every CycleGAN project.
What to learn next
- The denoising U-Net — the network that took over this job from GANs.
- Image segmentation — where the U-Net architecture came from.
- Diffusion models — the generative family behind Palette and InstructPix2Pix.
Researcher — Mathematics and papers.
Paired translation
Isola et al. (2017), Image-to-Image Translation with Conditional Adversarial Networks, formulate the task as a conditional GAN:
$$ \mathcal{L}{cGAN}(G, D) = \mathbb{E}{x,y}[\log D(x, y)] + \mathbb{E}_{x,z}[\log(1 - D(x, G(x, z)))] $$
$x$ is the input image, $y$ the paired target, $z$ a noise vector. Both $G$ and $D$ are conditioned on $x$, which is what distinguishes this from an unconditional GAN. The full objective adds a reconstruction term:
$$ G^* = \arg\min_G \max_D \; \mathcal{L}{cGAN}(G, D) + \lambda \,\mathbb{E}{x,y,z}\big[|y - G(x,z)|_1\big], \qquad \lambda = 100 $$
Two design findings from the paper are worth stating precisely.
The noise $z$ is nearly ignored. The authors report that the generator learns to disregard added noise. They supply stochasticity only through dropout at test time. The output remains close to deterministic. Paired translation with an $L1$ term is effectively a one-to-one map, which matters when the true mapping is one-to-many.
$L1$ is chosen over $L2$ deliberately. Both encourage blur, since the minimiser of an expected pixel loss is a conditional mean, but $L1$ blurs less. The adversarial term then supplies the high frequencies the reconstruction loss cannot.
The generator is a U-Net with skip connections between mirrored layers. For translation tasks the input and output share structure, and the skips let low-level information bypass the bottleneck.
Unpaired translation
Zhu et al. (2017), Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks, learn $G: X \to Y$ and $F: Y \to X$ with two discriminators and:
$$ \mathcal{L}{cyc}(G, F) = \mathbb{E}{x}\big[|F(G(x)) - x|1\big] + \mathbb{E}{y}\big[|G(F(y)) - y|_1\big] $$
$$ \mathcal{L} = \mathcal{L}_{GAN}(G, D_Y, X, Y) + \mathcal{L}_{GAN}(F, D_X, Y, X) + \lambda \mathcal{L}_{cyc}(G, F), \qquad \lambda = 10 $$
The adversarial losses use the least-squares form of Mao et al. (2017) rather than log-likelihood, which the authors report as more stable.
The identifiability problem is real and acknowledged in the paper. With sufficient capacity, $G$ can map $X$ to any permutation of $Y$ matching the target distribution. And $F$ can invert it. Cycle consistency constrains the composition $F \circ G$ to the identity, not $G$ itself. The prior that makes CycleGAN work in practice is architectural and optimisational, not a property of the objective.
Chu et al. (2017), CycleGAN, a Master of Steganography (NIPS 2017 Workshop on Machine Deception), demonstrate the sharp version of this. The generator embeds a nearly imperceptible high-frequency encoding of the source into its output, which the reverse generator decodes. They connect the phenomenon to adversarial examples and argue the cycle loss makes CycleGAN unusually vulnerable to adversarial attack. The practical consequence: cycle reconstruction error is not evidence of semantic correctness.
Beyond the two originals
| Method | Year | Contribution |
|---|---|---|
| CoGAN | 2016 | Weight sharing across two GANs induces a joint distribution |
| UNIT | 2017 | Shared-latent-space assumption, VAE-GAN per domain |
| MUNIT / DRIT | 2018 | Splits content and style codes, giving multimodal outputs |
| StarGAN / StarGAN v2 | 2018 / 2020 | One generator across many domains via a domain label |
| SPADE | 2019 | Spatially-adaptive normalisation for semantic-map inputs |
| CUT | 2020 | Replaces the cycle with patchwise contrastive loss, one generator |
| Palette | 2022 | Conditional diffusion; a single recipe across four translation tasks |
| InstructPix2Pix | 2023 | Text-instruction editing, trained on synthetic pairs from a text-to-image model |
CUT (Park et al., 2020) is the most important entry for practitioners moving off CycleGAN. It maximises mutual information between corresponding input and output patches, using a PatchNCE loss. That eliminates the reverse generator, halves memory, and removes the steganography channel by construction.
Palette (Saharia et al., 2022) marks the handover to diffusion. A single conditional diffusion model, with no task-specific architecture and no adversarial loss. It matches or beats specialised GANs on colourisation, inpainting, uncropping and JPEG restoration. It also restores genuine one-to-many behaviour, which pix2pix had lost.
Evaluation
Pixel metrics are close to meaningless here, because many outputs are correct. The standard battery:
- FID against the target domain, for realism and coverage.
- A pretrained task network on the output, the "FCN-score" of Isola et al. Translate a semantic map to a photo, then run a segmentation network on the result. Check that it recovers the map. This measures whether content survived, and is the closest thing to an objective correctness test for unpaired translation.
- Human study with a two-alternative forced choice against real images.
The FCN-score idea generalises well. Suppose you have an off-the-shelf model that reads the content you claim to preserve. Run it on the output and report the number.
Papers
- Isola et al., Image-to-Image Translation with Conditional Adversarial Networks, 2017 — arxiv.org/abs/1611.07004
- Zhu et al., Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks, 2017 — arxiv.org/abs/1703.10593
- Chu et al., CycleGAN, a Master of Steganography, 2017 — arxiv.org/abs/1712.02950
- Huang et al., Multimodal Unsupervised Image-to-Image Translation (MUNIT), 2018 — arxiv.org/abs/1804.04732
- Park et al., Semantic Image Synthesis with Spatially-Adaptive Normalization (SPADE), 2019 — arxiv.org/abs/1903.07291
- Park et al., Contrastive Learning for Unpaired Image-to-Image Translation (CUT), 2020 — arxiv.org/abs/2007.15651
- Saharia et al., Palette: Image-to-Image Diffusion Models, 2022 — arxiv.org/abs/2111.05826
- Brooks et al., InstructPix2Pix, 2023 — arxiv.org/abs/2211.09800
What to learn next
- The denoising U-Net — the network that took over this job from GANs.
- Image segmentation — where the U-Net architecture came from.
- Diffusion models — the generative family behind Palette and InstructPix2Pix.