Midjourney + Stable Diffusion 深度协同:从风格迁移到生产交付的技术实践
MJ 与 SD 的协同不是简单接力,而是一次**能力解耦**:MJ 作为"视觉语义引擎"提供高审美参考,SD 作为"可控渲染引擎"完成像素级交付。两者之间的桥梁,是 IP-Adapter、ControlNet 与 LoRA 三大适配器。理解它们的数学角色,才能构建稳定的混合管线。 ## 一、协同的底层逻辑 MJ 的输出是一张高审美图像,本质是"隐空间采样的一个样本";SD 要复用它的价值,有三条路径: 1. **结构复用**:Canny/Depth/OpenPose 提取几何,交给 ControlNet 约束生成。 1. **风格复用**:IP-Adapter 将参考图编码为图像提示,注入 Cross-Attention。 1. **分布复用**:img2img 以参考图为起点,用较低 `strength` 微调。 三者可叠加:ControlNet 锁结构,IP-Adapter 锁风格,img2img 保底图,形成"三重约束"。 ## 二、MJ 侧:稳定探索与参数化 MJ 无官方 API,生产走第三方网关。核心是**任务提交 → 轮询 → 结果落盘**,并保留提示词与种子以便复现。 ``` import os, time, requests, json, hashlib, pathlib BASE = "https://api.goapi.ai/mj/v2" H = {"X-API-Secret": os.getenv("MJ_KEY"), "Content-Type": "application/json"} def mj_run(prompt, aspect="1:1", version="6.1", mode="relax"): r = requests.post(f"{BASE}/imagine", headers=H, json={ "prompt": prompt, "aspect_ratio": aspect, "version": version, "process_mode": mode, }).json() tid = r["task_id"] while True: s = requests.get(f"{BASE}/fetch", headers=H, params={"task_id": tid}).json() if s["status"] == "finished": return s["task_result"]["image_urls"] if s["status"] == "failed": raise RuntimeError(s.get("message")) time.sleep(5) def cache_prompt(prompt, urls, out="mj_cache"): pathlib.Path(out).mkdir(exist_ok=True) key = hashlib.md5(prompt.encode()).hexdigest()[:10] for i, u in enumerate(urls): p = f"{out}/{key}_{i}.png" if not os.path.exists(p): open(p, "wb").write(requests.get(u).content) json.dump({"prompt": prompt, "urls": urls}, open(f"{out}/{key}.json", "w"), ensure_ascii=False) return f"{out}/{key}_0.png" ``` 要点:`--style raw` 降低默认美化,`--sref` 做风格迁移,`--cref` 做角色一致性。探索阶段一次出 4 张,快速筛方向。 ## 三、桥梁:IP-Adapter 风格注入 IP-Adapter 是 MJ→SD 协同的关键。它把参考图通过 CLIP 图像编码器转成 image embedding,拼接到文本 embedding 后送入 UNet,实现"图像即提示词"。 ``` import torch from PIL import Image from diffusers import StableDiffusionXLPipeline, DDIMScheduler from transformers import CLIPVisionModelWithProjection, CLIPImageProcessor pipe = StableDiffusionXLPipeline.from_pretrained( "stabilityai/stable-diffusion-xl-base-1.0", torch_dtype=torch.float16, image_encoder=CLIPVisionModelWithProjection.from_pretrained( "h94/IP-Adapter", subfolder="models/image_encoder", torch_dtype=torch.float16), ).to("cuda") pipe.scheduler = DDIMScheduler.from_config(pipe.scheduler.config) # 加载 IP-Adapter 权重 pipe.load_ip_adapter("h94/IP-Adapter", subfolder="sdxl_models", weight_name="ip-adapter_sdxl.bin") pipe.set_ip_adapter_scale(0.6) # 风格强度,0.4~0.7 为常用区间 ref = Image.open("mj_cache/style_0.png").convert("RGB") out = pipe( prompt="luxury perfume bottle, marble, studio lighting, 8k", negative_prompt="text, watermark, deformed", ip_adapter_image=ref, # MJ 图作为风格参考 num_inference_steps=30, guidance_scale=7.5, generator=torch.Generator("cuda").manual_seed(42), ).images[0] out.save("sd_styled.png") ``` `ip_adapter_scale` 是核心旋钮:过高会照抄参考图(包括不想要的元素),过低则风格漂移。0.6 是经验起点。 ## 四、结构约束:ControlNet 精修 风格有了,构图仍需精确控制。ControlNet 从 MJ 图提取 Canny 边缘或深度图,约束 SD 的空间结构。 ``` import cv2, numpy as np from diffusers import StableDiffusionXLControlNetPipeline, ControlNetModel from diffusers.utils import load_image cn = ControlNetModel.from_pretrained( "diffusers/controlnet-depth-sdxl-1.0", torch_dtype=torch.float16) pipe2 = StableDiffusionXLControlNetPipeline.from_pretrained( "stabilityai/stable-diffusion-xl-base-1.0", controlnet=cn, torch_dtype=torch.float16).to("cuda") pipe2.load_ip_adapter("h94/IP-Adapter", subfolder="sdxl_models", weight_name="ip-adapter_sdxl.bin") pipe2.set_ip_adapter_scale(0.5) # 用 MiDaS 或 Depth-Anything 生成深度图,这里以 Canny 简化演示 ref = np.array(load_image("mj_cache/style_0.png")) edges = cv2.Canny(ref, 100, 200) cond = np.stack([edges] * 3, axis=-1) final = pipe2( prompt="premium product shot, brand style, soft light", negative_prompt="cluttered, lowres", image=cond, ip_adapter_image=Image.fromarray(ref), controlnet_conditioning_scale=0.7, num_inference_steps=30, guidance_scale=7.5, generator=torch.Generator("cuda").manual_seed(7), ).images[0] final.save("sd_final.png") ``` **双适配器叠加**:IP-Adapter 管风格,ControlNet 管结构,两者权重需平衡——通常风格 0.5、结构 0.7,避免互相压制。 ## 五、生产管线:幂等与可追溯 ``` import hashlib from dataclasses import dataclass, asdict @dataclass class Job: mj_prompt: str sd_prompt: str ip_scale: float cn_scale: float seed: int def jid(j): return hashlib.md5(json.dumps(asdict(j), sort_keys=True).encode()).hexdigest()[:12] def pipeline(jobs, out="output"): pathlib.Path(out).mkdir(exist_ok=True) for j in jobs: k = jid(j) if os.path.exists(f"{out}/{k}.png"): continue # 幂等:已产出跳过 urls = mj_run(j.mj_prompt) ref_path = cache_prompt(j.mj_prompt, urls) img = sd_pipeline(ref_path, j.sd_prompt, j.ip_scale, j.cn_scale, j.seed) img.save(f"{out}/{k}.png") json.dump(asdict(j), open(f"{out}/{k}.json", "w"), ensure_ascii=False) ``` **幂等 + 元数据**是底线:任务可重跑不重复,参数可追溯可回滚。 ## 六、工程护栏 1. **成本分层**:MJ 只用 relax 探索,正式出图交给 SD。 1. **一致性**:MJ 用 `--cref`/`--sref`,SD 用 IP-Adapter + 固定种子。 1. **版权合规**:MJ 商用需确认订阅等级,SD 模型许可逐一核对。 1. **内容安全**:出图前过 NSFW 过滤,MJ 结果二次审核。 1. **显存优化**:`enable_model_cpu_offload()`、`vae_slicing` 降显存。 1. **版本锁定**:MJ 版本、SD 模型、LoRA、适配器权重全部固定。 ## 结语 MJ 与 SD 的深度协同,本质是把"审美"与"控制"解耦再重组:MJ 提供高维视觉语义,IP-Adapter 迁移风格,ControlNet 锁定结构,img2img 保底起点。当这套管线具备幂等、可追溯、可评估的工程属性,AI 绘画才真正从灵感工具升级为可交付的生产系统。