4. ControlNet 原理与使用:ControlNet 架构(零卷积、副本权重),Canny 边缘检测与深度图控制,多 ControlNet 组合使用
好,咱们进入 ControlNet 的世界。说实话,这是我觉得 Stable Diffusion 生态里最酷的组件之一。没有它,你只能靠 prompt 去「猜」画面结构;有了它,你可以直接告诉模型:「骨架在这儿,照着画!」
4.1 ControlNet 架构:零卷积与副本权重
先聊聊 ControlNet 到底是怎么「插」进 Stable Diffusion 的。你想想看,Stable Diffusion 本身是一个已经训练好的大模型,我们不想动它的原始权重,但又想给它加一个「遥控器」——这就是 ControlNet 的设计初衷。
核心思路:复制一份 U-Net 的编码器权重,作为「副本」。这个副本不参与原始模型的推理,而是专门用来处理你输入的控制信号(比如边缘图、深度图)。
这里有个关键设计叫 零卷积(Zero Convolution)。我第一次看到这个名词时也愣了一下,什么叫「零卷积」?说白了,就是一个卷积层,权重初始化为零,偏置也初始化为零。训练开始时,它什么都不做,输出全是零。随着训练进行,权重慢慢从零开始学习。
为什么要用零卷积?
因为我们要保证:在训练刚开始时,ControlNet 对原始模型的影响为零。这样不会破坏预训练模型已经学好的能力。随着训练推进,零卷积逐渐「解锁」,控制信号才开始生效。我在项目中遇到过,如果不用零卷积而直接用随机初始化,模型很容易崩掉,生成一些乱七八糟的东西。
整个流程可以这样理解:
- 原始 U-Net 编码器负责处理图像特征
- 副本 U-Net 编码器接收控制条件(如 Canny 边缘图)
- 副本的输出通过零卷积层,逐层注入到原始 U-Net 的对应层中
- 训练时只更新副本和零卷积的权重,原始模型冻结
嗯,这里要注意:副本权重和原始权重是独立的。训练完成后,你可以把 ControlNet 单独保存为一个文件,使用时再加载。这样非常灵活,一个基础模型可以搭配多个不同的 ControlNet。
4.2 Canny 边缘检测与深度图控制
ControlNet 支持很多种控制条件,但最常用的两个,我个人觉得是 Canny 边缘检测和深度图。咱们一个一个说。
Canny 边缘检测控制
Canny 边缘检测,说白了就是把一张图的轮廓提取出来。你给它一张照片,它返回一张黑白线条图,白线是边缘,黑色是背景。然后 ControlNet 就拿着这张线条图去指导生成。
我习惯用 OpenCV 来做 Canny 检测,代码很简单:
import cv2
image = cv2.imread('input.jpg')
gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
edges = cv2.Canny(gray, threshold1=100, threshold2=200)
cv2.imwrite('canny_output.jpg', edges)
这里有两个阈值参数 threshold1 和 threshold2。它们控制边缘检测的敏感度。数值越小,检测到的边缘越多越细;数值越大,只保留最明显的轮廓。
我的经验:对于建筑、机械等硬边物体,用较高的阈值(比如 100, 200)效果更好。对于人物、动物等柔边物体,用较低的阈值(比如 50, 150)能保留更多细节。我曾经在一个项目里生成产品渲染图,阈值调太高导致边缘丢失太多细节,生成结果跟原图完全不像——后来把阈值降下来就好了。
使用 Canny ControlNet 时,你只需要把边缘图作为控制输入,然后写 prompt。比如你有一张猫的轮廓图,prompt 写 "a cute cat, detailed fur, realistic",模型就会在轮廓内填充出猫的细节。
深度图控制
深度图控制又是另一回事了。它不关心边缘,它关心的是「远近关系」。深度图中,越近的物体越亮(白色),越远的物体越暗(黑色)。
为什么要用深度图?举个例子:你想生成一张室内场景图,要求沙发在前面,窗户在后面。如果只用 prompt,模型可能把沙发和窗户放在同一平面上。但如果你提供一张深度图,模型就知道哪里该近、哪里该远。
生成深度图通常用 MiDaS 模型:
import torch
import cv2
from PIL import Image
# 加载 MiDaS 模型
model_type = "DPT_Large"
midas = torch.hub.load("intel-isl/MiDaS", model_type)
# 读取图像并生成深度图
img = cv2.imread('input.jpg')
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
input_batch = transform(img_rgb).to(device)
with torch.no_grad():
prediction = midas(input_batch)
depth_map = prediction.squeeze().cpu().numpy()
# 保存深度图
cv2.imwrite('depth_output.jpg', depth_map)
注意:深度图的质量直接影响生成效果。如果原图本身景深不明显,生成的深度图可能很「平」,控制效果就会打折扣。我建议尽量选择有明显前后景差异的图片作为输入。
4.3 多 ControlNet 组合使用
单个 ControlNet 已经很强了,但有时候我们需要更精细的控制。比如,既要保持人物的姿势(用 OpenPose ControlNet),又要保持背景的深度关系(用 Depth ControlNet),还要保持物体的轮廓(用 Canny ControlNet)。这时候就需要多 ControlNet 组合。
多 ControlNet 的原理其实很简单:每个 ControlNet 独立处理自己的控制信号,然后把各自的输出加权求和,再注入到原始 U-Net 中。权重可以调节,比如让 Canny 的权重高一些,深度图的权重低一些。
在代码层面,使用多 ControlNet 通常是这样:
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel
import torch
# 加载多个 ControlNet
controlnet_canny = ControlNetModel.from_pretrained("lllyasviel/sd-controlnet-canny")
controlnet_depth = ControlNetModel.from_pretrained("lllyasviel/sd-controlnet-depth")
# 组合成一个列表
controlnet = [controlnet_canny, controlnet_depth]
# 创建 pipeline
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
controlnet=controlnet,
torch_dtype=torch.float16
)
# 准备多个控制图像
canny_image = ... # Canny 边缘图
depth_image = ... # 深度图
# 生成时传入列表
image = pipe(
prompt="a beautiful landscape, mountains and lake",
image=[canny_image, depth_image],
controlnet_conditioning_scale=[0.8, 0.5] # 分别控制权重
).images[0]
这里 controlnet_conditioning_scale 参数很关键。它控制每个 ControlNet 的影响力。数值越大,该控制条件越强。我一般会把主要控制条件设为 0.8-1.0,辅助控制条件设为 0.3-0.5。
避坑指南:我曾经同时用了 4 个 ControlNet(Canny、Depth、OpenPose、Normal),结果生成速度慢了 4 倍,而且控制信号互相冲突,生成结果反而变差了。后来我总结出一个原则:能用 2 个就别用 3 个。多 ControlNet 不是越多越好,而是越精准越好。
另外,不同 ControlNet 的输入图像尺寸必须一致。如果 Canny 图是 512x512,深度图是 768x768,pipeline 会报错。所以预处理时一定要统一尺寸。
小结
ControlNet 的核心就三件事:
- 架构上:副本权重 + 零卷积,保证不破坏原始模型
- 控制方式上:Canny 管轮廓,深度图管远近,各有各的用处
- 组合使用上:可以多 ControlNet 一起上,但要控制数量和权重
说实话,ControlNet 是我在 Stable Diffusion 生态里最常用的工具之一。它把「可控生成」这件事做到了极致。你只要把控制信号准备好,模型就能按你的意图生成。下一章咱们会讲怎么用 ControlNet 做具体的实战项目,到时候再细聊。