AI 工具

用 Stable Diffusion Turbo 做图生图:比较 strength 并量化画面变化

图生图(img2img)在已有图片的基础上按提示词改画。strength 决定改动多少。 这篇做 img2img.py:把 A 篇生成的山村图改成雪景,比较 4 档 strength 的效果,并量化每档与原图的差异;参数不合法时先给出明确提示。

BINBHO · 图文教程

图生图(img2img)在已有图片的基础上按提示词改画。strength 决定改动多少。 这篇做 img2img.py:把 A 篇生成的山村图改成雪景,比较 4 档 strength 的效果,并量化每档与原图的差异;参数不合法时先给出明确提示。

先看清成品

  • strength 0.25 / 0.5 / 0.75 / 1.0 与原图的平均像素差分别为 9.3 / 20.4 / 43.2 / 66.2。
  • 实测 steps=1、strength=0.5 时 diffusers 报 “cannot reshape tensor of 0 elements…”;脚本现在提前检查 steps × strength ≥ 1。
  • pytest 2 项通过(其中 1 项真实运行模型)。

合规提醒:sd-turbo 使用 Stability AI Community License:研究、非商业用途,以及年收入低于 100 万美元的个人或组织的商业用途免费;超过的需要企业许可,以模型页面和 stability.ai/license 为准。sd-turbo 仓库不含安全检查器(diffusers 加载时会提示):不要把未经过滤的生成服务公开给他人使用。不生成真实人物的误导性图像,不对他人照片做未经同意的修改;图生图只处理你有权使用的图片。模型从 Hugging Face 官方仓库下载,不使用来路不明的模型文件。

运行说明:本篇在 box 上真实运行了 Stable Diffusion:stabilityai/sd-turbo(revision b261bac6),diffusers 0.41.0,torch 2.14.1(CPU 版),8 核 CPU、无显卡。512×512、1 步生成一张约 4~5 秒。文中的图片都是这次运行生成的。没有测试 GPU,也没有运行 ComfyUI 和 SD 3.5(SD 3.5 需要在 Hugging Face 上同意许可后才能下载,且 CPU 上太慢)。

做完之后你会得到什么?

成果 说明
img2img.py 图生图和 strength 对比
village.png 输入图(A 篇用种子 42 生成)
examples/strengths.jpg 本文生成的对比图
test_img2img.py 2 项测试

需要准备哪些环境?

项目 说明
Python 3.10 以上
PyTorch CPU:pip install torch --index-url https://download.pytorch.org/whl/cpu;有 NVIDIA 显卡时按 PyTorch 官网选择 CUDA 版本
diffusers 等 pip install diffusers transformers accelerate safetensors pillow pytest
磁盘 sd-turbo 的 fp16 权重约 2.5 GB
内存 CPU 运行时建议 8 GB 以上(本文机器 15 GB)

关键事实

事实 来源
图生图时 num_inference_steps × strength 必须 ≥ 1;实际执行 int(num_inference_steps × strength) 步 sd-turbo 模型页面
官方示例:num_inference_steps=2, strength=0.5, guidance_scale=0.0 同上

文件结构

75-stable-diffusion-C-src/
├── img2img.py
├── village.png
├── test_img2img.py
└── examples/
    └── strengths.jpg

第 1 步:脚本

"""图生图:在你自己的图片(或自己生成的图)上按提示词改画,并比较不同 strength 的效果。
- strength 越大,改动越大、越不像原图
- sd-turbo 实际执行的步数 = int(steps × strength),必须 ≥ 1;否则 diffusers 报一个看不懂的 RuntimeError(调试时实测)
只处理你有权使用的图片;不要用他人照片做换脸或误导性内容。"""
import argparse, sys
import numpy as np, torch
from diffusers import AutoPipelineForImage2Image
from PIL import Image, ImageDraw

MODEL = "stabilityai/sd-turbo"

def check_params(steps, strength):
    if not 0 < strength <= 1: raise ValueError(f"strength 必须在 (0, 1] 之间,现在是 {strength}")
    if int(steps * strength) < 1:
        need = int(np.ceil(1 / strength))
        raise ValueError(f"steps × strength = {steps} × {strength} = {steps * strength:g} < 1,实际一步都不会执行;请把 steps 提高到至少 {need}")

def load(model=MODEL):
    pipe = AutoPipelineForImage2Image.from_pretrained(model, dtype=torch.float32, variant="fp16"); pipe.set_progress_bar_config(disable=True)
    return pipe

def edit(pipe, image, prompt, strength, steps=2, seed=1):
    check_params(steps, strength)
    img = image.convert("RGB").resize((512, 512))
    return pipe(prompt, image=img, num_inference_steps=steps, strength=strength, guidance_scale=0.0, generator=torch.Generator().manual_seed(seed)).images[0]

def change(a, b):
    """与原图的平均像素差(0~255),用来量化“改了多少”。"""
    return round(float(np.abs(np.asarray(a.resize((512, 512)), dtype=np.int16) - np.asarray(b, dtype=np.int16)).mean()), 1)

def strip(images, labels, size=256):
    out = Image.new("RGB", (size * len(images), size + 22), "white"); d = ImageDraw.Draw(out)
    for i, (im, lb) in enumerate(zip(images, labels)):
        out.paste(im.resize((size, size)), (i * size, 22)); d.text((i * size + 4, 4), lb, fill="black")
    return out

def main(argv=None):
    ap = argparse.ArgumentParser(); ap.add_argument("image"); ap.add_argument("prompt"); ap.add_argument("--strengths", default="0.25,0.5,0.75,1.0")
    ap.add_argument("--steps", type=int, default=4); ap.add_argument("--seed", type=int, default=1); ap.add_argument("--out", default="strengths.jpg"); a = ap.parse_args(argv)
    strengths = [float(s) for s in a.strengths.split(",")]
    for s in strengths: check_params(a.steps, s)                # 先检查全部参数,避免跑到一半才失败
    src = Image.open(a.image).convert("RGB"); pipe = load(); imgs, labels = [src.resize((512, 512))], ["original"]
    for s in strengths:
        im = edit(pipe, src, a.prompt, s, a.steps, a.seed); imgs.append(im)
        labels.append(f"strength {s} (diff {change(src, im)})"); print(labels[-1])
    strip(imgs, labels).save(a.out, quality=88); print("→", a.out); return 0

if __name__ == "__main__":
    try: sys.exit(main())
    except ValueError as e: print("参数错误:", e); sys.exit(2)

第 2 步:测试

import pytest
from PIL import Image
import img2img as I

def test_params():
    # 调试时实测:steps=1、strength=0.5 时 diffusers 报 “cannot reshape tensor of 0 elements …”
    with pytest.raises(ValueError, match="至少 2"): I.check_params(1, 0.5)
    with pytest.raises(ValueError, match="至少 4"): I.check_params(1, 0.25)
    with pytest.raises(ValueError, match=r"\(0, 1\]"): I.check_params(4, 0)
    I.check_params(2, 0.5); I.check_params(1, 1.0)

@pytest.fixture(scope="module")
def pipe(): return I.load()

def test_strength_order(pipe):
    src = Image.open("village.png")
    lo, hi = I.edit(pipe, src, "the same mountain village in winter, deep snow, watercolor", 0.25, steps=4), \
             I.edit(pipe, src, "the same mountain village in winter, deep snow, watercolor", 1.0, steps=4)
    assert I.change(src, lo) < 15 < 40 < I.change(src, hi)          # 实测 9.3 和 66.2

第 3 步:运行

$ python img2img.py village.png "the same mountain village in winter, deep snow, watercolor" --steps 1
参数错误: steps × strength = 1 × 0.25 = 0.25 < 1,实际一步都不会执行;请把 steps 提高到至少 4
$ python img2img.py village.png "the same mountain village in winter, deep snow, watercolor"
strength 0.25 (diff 9.3)
strength 0.5 (diff 20.4)
strength 0.75 (diff 43.2)
strength 1.0 (diff 66.2)
→ strengths.jpg

strength 对比

$ python -m pytest -q
2 passed

怎么检查结果?

  • strength 越大,差异值越大,画面越不像原图(本例从“原图加一层薄雪”到“完全重新构图的雪山村”)。
  • 想保留构图:0.25~0.5;想换风格:0.5~0.75;1.0 基本等于重新生成。
  • 所有 strength 都在开始前检查一遍,不会跑到一半才失败。

调试示例

steps × strength < 1 时的报错看不懂。 我先用 steps=1、strength=0.5 运行(沿用文生图的 1 步),diffusers 报:

RuntimeError cannot reshape tensor of 0 elements into shape [0, -1, 1, 512] because the unspecified dimension size -1 can be any value and is ambiguous

从这句报错完全看不出是参数问题。原因是实际执行的步数是 int(1 × 0.5) = 0,模型页面中也写明了 steps × strength 必须 ≥ 1。改成 steps=2 后正常。脚本现在在加载模型前检查所有 strength,并直接给出需要的最少步数。

常见问题

现象 原因 处理
RuntimeError: cannot reshape tensor of 0 elements steps × strength < 1 提高 steps
改动太大 strength 太高 降到 0.25~0.5
输入图被拉伸 原图不是正方形 先裁剪成正方形再输入

模板规则

  • steps × strength ≥ 1。
  • 用 strength 控制改动程度。
  • 只处理你有权使用的图片。

项目资源

用途 地址
sd-turbo 模型页面(官方) https://huggingface.co/stabilityai/sd-turbo
sd-turbo 许可证 https://huggingface.co/stabilityai/sd-turbo/blob/main/LICENSE.md
Stability AI 许可说明 https://stability.ai/license
diffusers 文档 https://huggingface.co/docs/diffusers/index
diffusers GitHub https://github.com/huggingface/diffusers

strength 是一个旋钮:从“轻微修改”到“重新画一张”。