
直接把人物抠图丢进 AI 生成的场景里,大概率会翻车。常见问题有三个:透视对不上(人物站着,背景是俯视)、光照方向矛盾(人脸左光,场景右光)、景深不匹配(人物边缘锐利,背景虚化过度)。
原因在于:文本生成图像时,模型并不"知道"你后续要往里塞什么。它生成的是一个自洽的独立画面,不是一个"等待合成的背景板"。
正确的做法是把置景当作一个受约束的生成任务:先确定相机参数和光照方向,再用 ControlNet 把这些约束注入生成过程。
层 | 职责 | 核心工具 |
|---|---|---|
置景层 | 生成受控背景 | ControlNet + 深度/线稿 |
合成层 | 主体与背景对齐 | 透视变换 + 蒙版融合 |
精修层 | 统一光影与质感 | 重打光 + 频率分离 |
三层的顺序不能乱。先合成再精修,是因为精修的输入必须是已经对齐的图像;如果透视没对齐就开始调光影,等于在错误的基础上叠加修正,越修越乱。
置景的关键是给模型一个"骨架"。以下代码用深度图约束场景的空间结构:
import torch
from diffusers import StableDiffusionControlNetPipeline, ControlNetModel
from diffusers.utils import load_image
from controlnet_aux import MidasDetector
controlnet = ControlNetModel.from_pretrained(
"lllyasviel/control_v11f1p_sd15_depth",
torch_dtype=torch.float16)
pipe = StableDiffusionControlNetPipeline.from_pretrained(
"runwayml/stable-diffusion-v1-5",
controlnet=controlnet,
torch_dtype=torch.float16).to("cuda")
midas = MidasDetector.from_pretrained("lllyasviel/Annotators")
def build_scene(ref_image: str, prompt: str, seed: int = 42) -> tuple:
"""从参考图提取深度,生成透视一致的背景"""
src = load_image(ref_image)
depth = midas(src) # 提取深度图
generator = torch.Generator("cuda").manual_seed(seed)
out = pipe(
prompt=prompt,
image=depth,
num_inference_steps=30,
guidance_scale=7.5,
controlnet_conditioning_scale=0.9, # 深度约束强度
generator=generator).images[0]
return out, depthcontrolnet_conditioning_scale=0.9 是关键参数。设 1.0 会完全贴合深度图,背景容易僵硬;设 0.5 约束太弱,透视会漂。0.8 到 0.9 是常用区间。
生成的背景透视可能和主体有偏差,需要做透视校正再合成:
import cv2
import numpy as np
def align_perspective(subject: np.ndarray, bg: np.ndarray,
src_pts: np.ndarray, dst_pts: np.ndarray) -> np.ndarray:
"""把主体透视变换到背景的相机视角"""
h, w = bg.shape[:2]
M = cv2.getPerspectiveTransform(src_pts, dst_pts)
warped = cv2.warpPerspective(subject, M, (w, h))
return warped
def composite(subject: np.ndarray, bg: np.ndarray,
mask: np.ndarray, blur: int = 3) -> np.ndarray:
"""用羽化蒙版做无缝合成"""
mask = cv2.GaussianBlur(mask, (blur * 2 + 1,) * 2, 0)
mask = mask.astype(np.float32) / 255.0
mask = np.stack([mask] * 3, axis=-1)
result = subject * mask + bg * (1 - mask)
return result.astype(np.uint8)蒙版羽化是必须的。硬边蒙版会让人物边缘出现一圈"贴纸感",羽化 3 到 5 像素能显著改善过渡。但羽化过度会让边缘发虚,所以 blur 参数要控制在 5 以内。
合成后主体和背景的光照往往不统一,需要重新打光:
def relight(subject: np.ndarray, light_dir: tuple = (-1, -1),
intensity: float = 0.3) -> np.ndarray:
"""根据光照方向给主体叠加方向性光影"""
h, w = subject.shape[:2]
yy, xx = np.mgrid[0:h, 0:w]
lx, ly = light_dir
grad = (xx * lx + yy * ly).astype(np.float32)
grad = (grad - grad.min()) / (grad.max() - grad.min() + 1e-6)
shade = 1.0 - intensity * (1.0 - grad)
shade = np.stack([shade] * 3, axis=-1)
return np.clip(subject.astype(np.float32) * shade, 0, 255).astype(np.uint8)
def frequency_separate(img: np.ndarray, sigma: int = 15) -> tuple:
"""频率分离:低频管光影,高频管质感"""
low = cv2.GaussianBlur(img, (0, 0), sigma)
high = cv2.subtract(img.astype(np.int16),
low.astype(np.int16)) + 128
return low, high.astype(np.uint8)频率分离的价值在于分离控制:低频层调光影(改整体明暗和色调),高频层调质感(改皮肤纹理和细节)。调光影不会破坏纹理,调纹理不会影响明暗。这是精修环节最核心的工程手段。
三个环节用统一参数串起来:
def pipeline(subject_path: str, ref_scene: str,
prompt: str, light_dir: tuple) -> dict:
subject = cv2.imread(subject_path)
bg, depth = build_scene(ref_scene, prompt)
bg_np = np.array(bg)
mask = extract_mask(subject) # 主体蒙版
aligned = align_perspective(subject, bg_np, src_pts, dst_pts)
composed = composite(aligned, bg_np, mask)
final = relight(composed, light_dir=light_dir, intensity=0.3)
low, high = frequency_separate(final)
return {"final": final, "low": low, "high": high,
"depth": depth}light_dir 必须和置景提示词里的光照描述一致。如果置景提示词写了 "window light from left",light_dir 就该设成 (-1, -1) 这类左上方方向,不能随机填。
第一,置景先定相机再定内容。 用深度图或线稿给 ControlNet 一个明确的空间骨架,比纯文本描述可控得多。
第二,透视对齐用四点变换。 不用自动估计,手动选四个对应点,精度和可控性都更好。
第三,蒙版羽化宁小勿大。 3 到 5 像素够用,超过 8 像素边缘就会发虚。
第四,光照方向要全局一致。 置景提示词、重打光参数、精修低频调整,三处的光照方向必须统一,否则画面会"精神分裂"。
第五,精修分频处理。 低频改光影、高频改质感,两者独立调整,避免互相干扰。
AI 置景加合成精修的本质,是把"生成一张图"拆成"生成背景—对齐主体—统一光影"三个可独立调试的工程环节。每个环节的误差不会累积到下一步,出图质量才可控。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。