Stable Diffusion进阶:ControlNet+LoRA实战
发布日期: 2026/07/21 阅读总量: 1

一、真实场景:一张图生成翻车了

上个月接了个电商项目,要求生成「穿着红色汉服、手持折扇、背景是江南水乡」的模特图。我用Stable Diffusion WebUI直接跑,结果惨不忍睹:

  • 人物手部扭曲,6根手指是常态
  • 汉服纹理糊成一团,扇子形状不对
  • 背景水乡变成了抽象色块

试了20次,只有2张勉强能用,废片率90%。

问题出在哪?基础模型(SD1.5)对特定风格(汉服、扇子)和空间关系(手部、背景)理解不够。这时候需要两个进阶工具:ControlNet(控制生成结构)和LoRA(微调特定风格)。

本文基于:
- Python 3.10.12
- diffusers 0.27.2
- torch 2.1.0+cu121
- ControlNet 1.1.224
- 硬件:RTX 4090 24GB / 32GB RAM

二、问题拆解:ControlNet和LoRA分别解决什么

2.1 ControlNet:控制生成的结构

ControlNet通过额外输入(如边缘图、深度图、姿态图)来约束生成图像的结构。比如:

  • Canny边缘:强制生成图像边缘与输入一致
  • Depth深度:保持物体空间关系
  • OpenPose姿态:控制人物骨骼姿势

适合场景:需要精确控制构图、人物姿势、物体形状。

2.2 LoRA:微调特定风格/物体

LoRA(Low-Rank Adaptation)是一种轻量级微调方法,通过训练少量参数(通常几MB到几十MB)让模型学会特定风格或物体。比如:

  • 汉服LoRA:让模型生成汉服纹理更精细
  • 扇子LoRA:让扇子形状正确
  • 水乡LoRA:让背景更真实

适合场景:需要固定风格、特定物体、角色一致性。

2.3 两者对比

维度ControlNetLoRA
控制粒度结构级(边缘/深度/姿态)风格级(纹理/颜色/物体)
训练成本无需训练,直接使用预训练模型需要训练,但参数少(通常10-100MB)
推理速度慢(额外UNet推理)快(仅权重融合)
显存占用高(约2-4GB额外)低(几乎无额外)
灵活性高(可组合多种控制)低(固定风格)

三、方案对比:单独使用 vs 组合使用

3.1 方案A:只用ControlNet

用Canny边缘控制构图,生成汉服模特。结果:结构正确,但汉服纹理还是糊。

3.2 方案B:只用LoRA

加载汉服LoRA,生成模特。结果:汉服纹理精细了,但手部还是扭曲,背景不对。

3.3 方案C:ControlNet + LoRA组合

先用OpenPose控制人物姿势,再用Canny控制构图,最后加载汉服LoRA和扇子LoRA。结果:手部正常、汉服纹理清晰、扇子形状正确、背景水乡真实。

最终方案:ControlNet(OpenPose + Canny)+ LoRA(汉服 + 扇子)

四、完整代码实现

4.1 环境准备

# 创建虚拟环境
python3 -m venv sd_env
source sd_env/bin/activate

# 安装依赖
pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu121
pip install diffusers==0.27.2 transformers==4.36.2 accelerate==0.25.0
pip install opencv-python==4.8.1.78 pillow==10.1.0
pip install controlnet-aux==0.0.7  # ControlNet预处理工具

4.2 加载模型

import torch
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel, DPMSolverMultistepScheduler
from diffusers.utils import load_image
from controlnet_aux import CannyDetector, OpenposeDetector
from PIL import Image
import numpy as np

# 加载基础模型
base_model_id = "runwayml/stable-diffusion-v1-5"

# 加载ControlNet模型(Canny和OpenPose)
controlnet_canny = ControlNetModel.from_pretrained(
    "lllyasviel/control_v11p_sd15_canny",
    torch_dtype=torch.float16
)
controlnet_openpose = ControlNetModel.from_pretrained(
    "lllyasviel/control_v11p_sd15_openpose",
    torch_dtype=torch.float16
)

# 创建pipeline,支持多个ControlNet
pipe = StableDiffusionControlNetPipeline.from_pretrained(
    base_model_id,
    controlnet=[controlnet_canny, controlnet_openpose],
    torch_dtype=torch.float16,
    safety_checker=None  # 禁用安全检查,加速
)
pipe.scheduler = DPMSolverMultistepScheduler.from_config(pipe.scheduler.config)
pipe.enable_model_cpu_offload()  # 节省显存
pipe.enable_vae_slicing()  # 减少显存占用

# 加载LoRA权重
pipe.load_lora_weights("path/to/hanfu_lora", weight_name="hanfu.safetensors")
pipe.load_lora_weights("path/to/fan_lora", weight_name="fan.safetensors")
# 设置LoRA权重缩放(默认1.0,可调整)
pipe.set_adapters(["hanfu", "fan"], adapter_weights=[0.8, 0.6])

4.3 预处理:生成ControlNet输入

def preprocess_control_images(input_image_path):
    """生成Canny边缘图和OpenPose姿态图"""
    image = load_image(input_image_path)
    
    # Canny边缘检测
    canny_detector = CannyDetector()
    canny_image = canny_detector(image, low_threshold=100, high_threshold=200)
    
    # OpenPose姿态检测
    openpose_detector = OpenposeDetector.from_pretrained("lllyasviel/control_v11p_sd15_openpose")
    openpose_image = openpose_detector(image)
    
    return canny_image, openpose_image

# 示例:用一张参考图生成控制信号
canny_img, pose_img = preprocess_control_images("reference_pose.jpg")
canny_img.save("canny_input.png")
pose_img.save("pose_input.png")

4.4 生成图像

def generate_image(prompt, negative_prompt, canny_image, pose_image, num_inference_steps=30):
    """使用ControlNet + LoRA生成图像"""
    # 控制条件列表,顺序与加载的controlnet一致
    control_images = [canny_image, pose_image]
    
    # 生成
    with torch.autocast("cuda"):
        image = pipe(
            prompt=prompt,
            negative_prompt=negative_prompt,
            image=control_images,
            num_inference_steps=num_inference_steps,
            guidance_scale=7.5,
            controlnet_conditioning_scale=[0.8, 0.6],  # 分别控制Canny和OpenPose的强度
            cross_attention_kwargs={"scale": 0.8},  # LoRA权重缩放
            generator=torch.Generator(device="cuda").manual_seed(42)
        ).images[0]
    
    return image

# 执行生成
prompt = "a beautiful Chinese woman wearing red Hanfu, holding a folding fan, standing by a river in Jiangnan water town, photorealistic, high detail, 8k"
negative_prompt = "ugly, deformed, blurry, low quality, extra fingers, bad anatomy"

result = generate_image(prompt, negative_prompt, canny_img, pose_img)
result.save("output_hanfu_model.png")

4.5 批量生成与性能测试

import time

def batch_generate(prompts, canny_img, pose_img, batch_size=4):
    """批量生成,测试性能"""
    results = []
    start_time = time.time()
    
    for i in range(0, len(prompts), batch_size):
        batch = prompts[i:i+batch_size]
        for p in batch:
            img = generate_image(p, negative_prompt, canny_img, pose_img)
            results.append(img)
    
    total_time = time.time() - start_time
    avg_time = total_time / len(prompts)
    print(f"Total time: {total_time:.2f}s, Avg per image: {avg_time:.2f}s")
    return results

# 测试不同配置
test_prompts = [
    "red Hanfu with gold embroidery",
    "blue Hanfu with cloud pattern",
    "green Hanfu with flower design"
]

# 测试ControlNet + LoRA
results_cn_lora = batch_generate(test_prompts, canny_img, pose_img)

# 测试仅LoRA(无ControlNet)
pipe.controlnet = None  # 禁用ControlNet
results_lora_only = batch_generate(test_prompts, None, None)

# 测试仅ControlNet(无LoRA)
pipe.controlnet = [controlnet_canny, controlnet_openpose]
pipe.unload_lora_weights()  # 卸载LoRA
results_cn_only = batch_generate(test_prompts, canny_img, pose_img)

五、效果数据

5.1 生成质量对比

方案手部正确率汉服纹理清晰度扇子形状正确率背景匹配度用户评分(1-5)
基础SD1.510%2/1015%3/101.5
仅ControlNet70%4/1060%8/103.0
仅LoRA20%9/1080%4/103.5
ControlNet+LoRA85%9/1090%9/104.8

数据说明:每方案生成50张图,由3人独立评分取均值。手部正确率指手指数量正确且姿态自然。

5.2 性能数据(RTX 4090)

配置单张耗时显存占用峰值显存
基础SD1.51.2s4.2GB5.1GB
仅ControlNet(Canny)2.1s6.8GB8.3GB
仅ControlNet(OpenPose)2.3s7.1GB8.7GB
ControlNet(Canny+OpenPose)2.8s9.5GB11.2GB
仅LoRA(2个权重)1.2s4.3GB5.2GB
ControlNet+LoRA2.8s9.6GB11.3GB

测试条件:steps=30, 分辨率512x512, batch_size=1, fp16。LoRA几乎不增加额外耗时和显存。

5.3 不同ControlNet模式对比

模式结构控制强度风格自由度适用场景
Canny高(边缘强制)建筑、产品图
Depth中(空间关系)室内设计、3D场景
OpenPose高(姿态强制)人物姿势控制
SoftEdge中(软边缘)艺术风格迁移

六、避坑指南(我实际踩过的坑)

坑1:显存溢出(OOM)

现象:加载两个ControlNet后,生成512x512图像直接OOM。

原因:ControlNet每个模型约占用2-3GB显存,两个就是4-6GB,加上基础模型和VAE,24GB显存不够。

解决

  • 启用pipe.enable_model_cpu_offload(),将不用的模型卸载到CPU
  • 启用pipe.enable_vae_slicing(),分片处理VAE
  • 降低分辨率到384x384或使用--medvram参数
  • 如果还不行,换用diffusersStableDiffusionControlNetImg2ImgPipeline,减少显存

坑2:LoRA过拟合

现象:加载汉服LoRA后,生成的所有图像都带有相同的褶皱纹理,缺乏多样性。

原因:LoRA训练数据太少(只有20张图),导致过拟合。

解决

  • 降低LoRA权重缩放(从1.0降到0.6-0.8)
  • 混合多个LoRA(如汉服+水乡+扇子),分散权重
  • 增加训练数据到100张以上
  • 使用set_adapters调整每个LoRA的权重

坑3:ControlNet和LoRA权重冲突

现象:同时使用Canny ControlNet和汉服LoRA,生成图像中汉服纹理被Canny边缘破坏,出现断裂。

原因:ControlNet强制边缘,LoRA修改纹理,两者在UNet中产生冲突。

解决

  • 降低ControlNet的controlnet_conditioning_scale(从1.0降到0.6-0.8)
  • 调整LoRA权重(从1.0降到0.5-0.7)
  • 使用SoftEdge代替Canny,给LoRA更多自由度
  • 增加num_inference_steps到40-50步,让模型有更多时间协调

坑4:OpenPose检测不准

现象:参考图中人物侧身,OpenPose检测出的骨骼点错位,导致生成人物姿势扭曲。

原因:OpenPose对遮挡、侧面、复杂动作检测精度有限。

解决

  • 使用controlnet_auxOpenposeDetector时,设置include_body=True, include_hand=True, include_face=True,提高检测精度
  • 手动修正检测结果(用Photoshop或Python脚本调整关键点)
  • 换用DWPose(更准确的姿态检测器)
  • 如果姿势简单,直接用Canny控制轮廓

坑5:LoRA权重文件格式问题

现象:从CivitAI下载的LoRA文件(.safetensors)加载时报错KeyError: 'lora_unet_down_blocks_0_attentions_0_proj_in.lora_down.weight'

原因:LoRA权重命名与diffusers不兼容,需要转换。

解决

  • 使用scripts/convert_lora_safetensor_to_diffusers.py脚本转换
  • 或者用diffusersload_lora_weights时指定weight_name参数
  • 推荐从HuggingFace下载官方格式的LoRA

七、进阶技巧

7.1 多ControlNet组合策略

可以同时使用3-4个ControlNet,但要注意权重分配:

  • 主控制(如OpenPose):权重0.8-1.0
  • 辅助控制(如Canny):权重0.4-0.6
  • 细节控制(如Depth):权重0.2-0.4

示例:人物姿势(OpenPose 0.9)+ 构图边缘(Canny 0.5)+ 空间深度(Depth 0.3)

7.2 LoRA权重融合技巧

多个LoRA同时使用时,权重总和不要超过1.5,否则容易过拟合:

  • 主风格LoRA:0.6-0.8
  • 辅助物体LoRA:0.3-0.5
  • 背景LoRA:0.2-0.4

7.3 动态调整ControlNet强度

可以在推理过程中动态调整ControlNet强度,实现从结构控制到自由生成的过渡:

# 在pipe.callback中动态调整
class DynamicControlNetCallback:
    def __init__(self):
        self.step = 0
    
    def __call__(self, pipe, i, t, callback_kwargs):
        # 前10步强控制,后20步弱控制
        if i < 10:
            scale = 0.8
        else:
            scale = 0.8 * (1 - (i - 10) / 20)
        callback_kwargs["controlnet_conditioning_scale"] = [scale, scale * 0.75]
        self.step += 1
        return callback_kwargs

pipe.callback_on_step_end = DynamicControlNetCallback()

八、总结

ControlNet和LoRA是Stable Diffusion进阶的两大核心工具。ControlNet控制结构,LoRA控制风格,组合使用效果最佳。

关键参数速查:

  • ControlNet权重:0.6-0.9(太高会限制风格)
  • LoRA权重:0.5-0.8(太高会过拟合)
  • 推理步数:30-50(组合使用时建议40步)
  • 分辨率:512x512或768x768(更高需要更多显存)

最后提醒:不要盲目堆叠ControlNet和LoRA,先确定问题类型(结构问题用ControlNet,风格问题用LoRA),再逐步添加。