Pixels2GenAI
Path iii Generative
M 12 · 12.3.2 · conceptual

12.3.2 ControlNet Guided Generation

Add spatial control to a frozen diffusion model with ControlNet's trainable encoder copy and zero-convolution skip path; combine with LoRA for content+style control over generation.

Duration45–55 min
Leveladvanced
Load4 core concepts
PrereqsLessons 12.3.1 (DDPM) and 12.5.1 (DreamBooth); diffusers + transformers; GPU strongly recommended
Fig. 1 ControlNet morphing colour while holding shape constant. The circle outline is the structural control input; the prompt and seed change frame to frame; the output respects the structure unconditionally.

Overview

Lesson 12.3.1 trained a DDPM that generates from pure noise — controllable by nothing other than the random seed. Lesson 12.5.1 added text conditioning to a pre-trained Stable Diffusion via DreamBooth: now the model responds to prompts but the structure of generated images is still up to the model’s prior.

ControlNet, introduced by Zhang, Rao and Agrawala at ICCV 2023 [1], adds the missing axis: structural conditioning. Feed the model a Canny edge map, a depth map, a pose skeleton, or any other spatial signal alongside the text prompt, and the output respects that structure while filling in everything else from the text. The headlining demo: take an edge map of a horse, prompt “a photo of a horse in a desert at sunset,” get a photorealistic horse in a desert at sunset with exactly the outline of the input edge map.

The architectural trick that makes this work without retraining Stable Diffusion is the zero-convolution initialisation, which lets a trainable conditioning branch fade in from zero contribution during training — guaranteeing the original model’s capabilities are preserved on day one of fine-tuning. This lesson works through the architecture, demonstrates control with the Fill50k dataset, and combines ControlNet (structure) with a LoRA (style) for maximum-flexibility generation.

Learning objectives

  1. Distinguish DDPM’s unconditional generation, DreamBooth’s text-conditional generation, and ControlNet’s spatial-conditional generation; predict which to use for which task.
  2. Explain ControlNet’s “clone the encoder, freeze the original, link with zero convolutions” architecture and why each piece is necessary.
  3. Apply different control types (Canny edges, line art, depth) and predict which control suits which use case.
  4. Combine ControlNet’s structural control with a LoRA’s style control to achieve simultaneous content and style direction.

Quick start — generate fabric guided by edges

The pre-trained ControlNet checkpoint for the Fill50k dataset is on the GitHub Release. The Stable Diffusion v1.5 base model is downloaded automatically on first run via diffusers.

controlnet_fill50k.pt — pre-trained ControlNet (~1.38 GB, GitHub Release) lora_african_fabrics.safetensors — companion style LoRA (~3 MB)
python · quick-start.py
import torch
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel
from PIL import Image
import cv2, numpy as np

# Load ControlNet + base SD pipeline
controlnet = ControlNetModel.from_pretrained("./models/controlnet_fill50k")
pipe = StableDiffusionControlNetPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5", controlnet=controlnet,
    torch_dtype=torch.float16,
).to("cuda")

# Build a Canny edge map from any image
image = cv2.imread("sample_inputs/circle.png")
edges = cv2.Canny(image, 100, 200)
edge_image = Image.fromarray(np.stack([edges]*3, axis=-1))

result = pipe(
    prompt="a vibrant african fabric pattern",
    image=edge_image,
    num_inference_steps=30, guidance_scale=7.5,
).images[0]
result.save("controlnet_output.png")

The output respects the edge map’s spatial structure (a circle, say) while filling in the colour and texture according to the prompt.

Core concepts

Concept 1 — Three axes of control: noise, text, structure

Place ControlNet in context with the previous generative-AI lessons:

ModelInputOutput controlled by
DDPM (12.3.1)noise + timesteprandom seed only
DreamBooth on SD (12.5.1)noise + text promptseed + text
ControlNet on SDnoise + text + spatial mapseed + text + structure

Each step adds an axis without removing the previous ones. ControlNet doesn’t replace text conditioning — it layers spatial conditioning on top. The base Stable Diffusion model still produces text-conditioned output; ControlNet adds a constraint that the output’s spatial structure must follow the input control image.

The mechanical question is: how do you add this new conditioning to a 860M-parameter pre-trained model without retraining it from scratch and without breaking what it already does well? Zhang et al.’s answer [1] is the architecture in Concept 2.

Fig. 2 ControlNet architecture. Frozen base Stable Diffusion (left) keeps its full pre-trained behaviour. Trainable encoder copy (right) processes the control input; zero-convolution layers merge its features back into the frozen decoder.

Concept 2 — Clone, freeze, zero-init

ControlNet’s three structural moves:

1. Clone the encoder. The U-Net inside Stable Diffusion has an encoder (downsampling) and decoder (upsampling). ControlNet creates a trainable copy of the encoder. This copy starts with the same weights as the frozen original, so it processes inputs identically to start with; only training will make them diverge.

2. Freeze the original. The pre-trained Stable Diffusion U-Net weights stay locked. Training only updates the cloned encoder + the linking layers. The full text-to-image capability the base model already has is preserved untouched.

3. Connect with zero convolutions. The cloned encoder’s outputs need to feed back into the frozen decoder. The connection points are 1×1 convolutions initialised with all weights and biases at zero — so at the start of training the cloned encoder contributes literally nothing, the model behaves exactly like base Stable Diffusion, and as training progresses the zero convolutions learn appropriate scales for the conditioning signal.

Fig. 3 Zero-convolution initialisation. At training step 0, the output is zero regardless of input — the conditioning branch has no effect. Gradients still flow back (the input isn't zero, just the weights), so the layer can learn appropriate non-zero values.

The brilliance of this design is what it guarantees: on day one of training, ControlNet behaves identically to base Stable Diffusion because the zero convolutions force the conditioning branch’s contribution to zero. The model can only get better than baseline from there — gradient descent will discover useful conditioning patterns and update the zero convolutions away from zero where doing so reduces loss. The pre-trained capability is preserved by construction, not by careful regularisation.

python · zero convolution
class ZeroConv2d(nn.Conv2d):
    def __init__(self, in_channels, out_channels):
        super().__init__(in_channels, out_channels, kernel_size=1)
        nn.init.zeros_(self.weight)
        nn.init.zeros_(self.bias)

# Step 0: output = input * 0 + 0 = 0 — no effect on the frozen decoder
# After training: output = input * learned_weights + learned_bias

Concept 3 — Control types: pick the spatial signal that matches your task

A trained ControlNet expects a specific control image format. The most common types, each with a corresponding pre-trained ControlNet checkpoint on Hugging Face:

Fig. 4 Four common control types. Each ControlNet is trained specifically for one input format — you can't feed Canny edges to a depth-trained ControlNet and expect it to work.
Control typeSpatial signalBest for
Canny edgessharp binary edgesarchitecture, geometric patterns, anything edge-defined
Line artclean artistic outlinesillustration, anime, hand-drawn references
Depth mapsper-pixel depth (0=near, 1=far)scenes with 3D structure, realistic compositions
PoseOpenPose skeleton keypointshuman figures, dance, character art
MLSD linesHough straight linesindoor scenes, architectural plans
Semantic segmentationper-pixel class labelsscenes built from semantic regions
Scribblerough freehand drawingsrapid prototyping, low-fidelity sketches

Picking the right control type is half the work. Use Canny edges when your task is “render this object’s outline in a new style.” Use depth when you care about 3D plausibility. Use pose for character work. The fabric demonstration in this lesson uses Canny edges on circular masks — the simplest possible control signal for the simplest possible structural constraint.

Concept 4 — Tuning conditioning strength + combining with LoRA

Two run-time parameters tune how strictly the model follows the control signal:

controlnet_conditioning_scale (typically 0.5–1.5) — the multiplicative weight applied to the ControlNet’s contributions. 1.0 is the default training value; 0.5 halves the conditioning strength (letting the prompt assert itself more); 1.5 increases it (overriding the prompt where they conflict). Tune down if outputs look too rigidly locked to the control; tune up if the model is ignoring the control.

guidance_scale (typically 7.5) — the classifier-free guidance from text conditioning, same parameter as DreamBooth. Independent of ControlNet’s contribution. Use both together: higher CFG for stronger prompt adherence, higher conditioning scale for stronger structural adherence.

For maximum flexibility, ControlNet can be combined with a LoRA (Lesson 12.5.1). The ControlNet handles structure; the LoRA handles style; the prompt handles content semantics. All three operate independently on the same generation step:

python · ControlNet + LoRA + text
pipe = StableDiffusionControlNetPipeline.from_pretrained(
    "runwayml/stable-diffusion-v1-5", controlnet=controlnet,
    torch_dtype=torch.float16,
).to("cuda")
pipe.load_lora_weights("./models/lora_african_fabrics")  # style

result = pipe(
    prompt="a sks african fabric pattern with geometric shapes",
    image=canny_edge_map,                                  # structure
    controlnet_conditioning_scale=1.0,
    guidance_scale=7.5,
    num_inference_steps=30,
).images[0]

Exercises

EXECUTE I.

Generate guided fabric patterns from Canny edge maps

controlnet_fill50k.pt — 1.38 GB ControlNet checkpoint exercise1_generate.py — Canny-edge-guided generation controlnet_utils.py — shared pipeline helpers

Place the checkpoint at models/controlnet_fill50k/, then:

bash
pip install diffusers transformers accelerate xformers opencv-python
python exercise1_generate.py

First run downloads Stable Diffusion v1.5 (~4 GB) from Hugging Face; subsequent runs are fast. Output: exercise1_output.png — a comparison grid showing the original sample, its Canny edge map, and the ControlNet-generated output that respects the edges.

Fig. 5 Each row: original image (used to extract edges), Canny edge map (the control input), and ControlNet-generated output that follows those edges while applying new colour/style from the prompt.

Reflection

  1. The generated output respects the edge map exactly but the colours and textures are completely different from the original. Which architectural property gives ControlNet this strict structural adherence?
  2. Why does the first run download ~4 GB of weights? What’s not in the 1.38 GB ControlNet checkpoint?
  3. The same edge map fed through the same model with two different text prompts produces structurally identical but stylistically different outputs. Which conditioning signal is doing which work?
  4. If you fed an edge map from a photograph of a chair into a ControlNet trained on Fill50k circles, what would happen?
MODIFY II.

Three explorations: control strength, prompt variation, comparison

exercise2_explore.py — three explorations
bash
python exercise2_explore.py

Goal 1 — Control strength sweep. Same prompt + same edges at controlnet_conditioning_scale ∈ {0.3, 0.6, 0.9, 1.2, 1.5}.

Fig. 6 Control strength sweep. At 0.3 the structural guide is suggestion-strength; at 1.0 it's enforced; at 1.5+ the model overrides prompt-driven details to fit the control.

Goal 2 — Prompt variation. Same control + same strength, different prompts. The structure stays; the content changes.

Fig. 7 Same structure, six prompts. The circle outline persists; everything inside it changes with the prompt.

Goal 3 — Direct control-type comparison. Run the same content through Canny, Line Art, and Scribble ControlNets to see how the same scene reacts to different structural signals.

Fig. 8 Canny (top) is strict — every edge is preserved. Line art (middle) is cleaner. Scribble (bottom) is the loosest — only the rough composition is enforced.
CREATE III.

Train ControlNet from scratch + LoRA style adaptation

Three scripts cover the full pipeline. Most learners should focus on Option C (combined generation) since it demonstrates the architectural payoff; Options A and B are deep dives for those with multi-hour GPU time available.

exercise3a_train_controlnet.py — train ControlNet from scratch (2-4h GPU) exercise3b_train_lora.py — train style LoRA on African fabrics (15-30 min) exercise3c_combined_generation.py — ControlNet + LoRA combined download_fill50k.py — fetches Fill50k dataset from Hugging Face

Make it your own

Try a control type the pre-trained ControlNet wasn’t trained for (e.g. feed a depth map to a Canny-trained ControlNet). The output will still be generated but the structural adherence will be unpredictable. This is a useful exercise in understanding the limits of control-type specificity.

Summary

Common pitfalls

  • Using a ControlNet trained for SD 1.5 with SDXL or SD 2.x — different U-Net architectures, incompatible weights.
  • Feeding the wrong control format for the ControlNet checkpoint (e.g. line art into a Canny-trained ControlNet). The model will produce something, but the structural adherence breaks.
  • controlnet_conditioning_scale = 0 (or near-zero) — the conditioning branch is effectively disabled and the ControlNet does nothing.
  • controlnet_conditioning_scale > 2.0 — the control overpowers the prompt to the point of degrading the output. Stay in 0.5–1.5 for clean outputs.
  • Combining a LoRA and a ControlNet trained against different base models. Both load, neither composes correctly, and the failure mode is subtle.

References

  1. [1] Zhang, L., Rao, A. & Agrawala, M. (2023). Adding Conditional Control to Text-to-Image Diffusion Models (ControlNet). IEEE International Conference on Computer Vision (ICCV). arxiv:2302.05543
  2. [2] Rombach, R., Blattmann, A., Lorenz, D., Esser, P. & Ommer, B. (2022). High-Resolution Image Synthesis with Latent Diffusion Models (Stable Diffusion). CVPR. arxiv:2112.10752
  3. [3] Ho, J., Jain, A. & Abbeel, P. (2020). Denoising Diffusion Probabilistic Models. NeurIPS. arxiv:2006.11239
  4. [4] Hu, E. J. et al. (2022). LoRA: Low-Rank Adaptation of Large Language Models. ICLR. arxiv:2106.09685
  5. [5] Ho, J. & Salimans, T. (2022). Classifier-Free Diffusion Guidance. NeurIPS Workshop. arxiv:2207.12598
  6. [6] Canny, J. (1986). A Computational Approach to Edge Detection. IEEE TPAMI, 8(6), 679–698. doi.org/10.1109/TPAMI.1986.4767851
  7. [7] Hugging Face. (2024). ControlNet in Diffusers. huggingface.co/docs/diffusers/using-diffusers/controlnet
  8. [8] Mou, C., Wang, X., Xie, L., Wu, Y., Zhang, J., Qi, Z. et al. (2024). T2I-Adapter: Learning Adapters to Dig out More Controllable Ability for Text-to-Image Diffusion Models. AAAI. arxiv:2302.08453