Diffusers JoyImageEditPlusPipeline 实战:基于 JoyAI-Image-Edit-Plus 的多参考图指令引导图像编辑
【免费下载链接】diffusers🤗 Diffusers: State-of-the-art diffusion models for image, video, and audio generation in PyTorch.项目地址: https://gitcode.com/GitHub_Trending/di/diffusers
JoyAI-Image-Edit-Plus 是京东开源 JoyAI-Image 系列中的多图指令引导编辑模型:它以多张参考图像 + 一条文本指令为输入,按照指令把多张参考图中的元素组合生成一张新图像,单个样本支持 1~5 张参考图。本文以 Hugging Face Diffusers 仓库中该功能的官方文档(docs/source/en/api/pipelines/joyimage_edit_plus.md)为主线,结合 pipeline_joyimage_edit_plus.py 与 transformer_joyimage_edit_plus.py 的源码实现,完整讲解从模型加载、参考图预处理到端到端调用的全部流程,并深入分析其"目标噪声 + 参考图 patch 拼接联合去噪"的底层原理与全部可调参数。
模型背景:JoyAI-Image 与 Edit-Plus
JoyAI-Image 是一个统一的跨模态基础模型,同时覆盖图像理解、文生图、指令引导图像编辑三类任务。其架构由两部分组成:一个 8B 参数的多模态大语言模型(MLLM)负责理解指令与图像内容,一个 16B 参数的多模态扩散 Transformer(MMDiT)负责去噪生成。
在 Diffusers 仓库中,该系列实现了两条流水线(见 pipelines/joyimage/init.py):
JoyImageEditPipeline:单参考图编辑 / 文生图,对应 pipeline_joyimage_edit.py;JoyImageEditPlusPipeline:多参考图指令引导编辑,对应 pipeline_joyimage_edit_plus.py,即本文核心。
| 模型 | 说明 |
|---|---|
| JoyAI-Image-Edit-Plus | 多图指令引导编辑,支持从多张参考图中组合元素(官方权重 ID 为jdopensource/JoyAI-Image-Edit-Plus-Diffusers) |
从源码结构看,Edit-Plus 与 Edit 共享同一套组件(Qwen3-VL 文本编码器、WAN VAE、FlowMatch Euler 调度器、JoyImage 图像处理器),核心区别在于 Edit-Plus 的 Transformer 采用"目标噪声 + 全部参考图"拼接的联合去噪方式,这是它能够跨图组合元素的关键。
快速上手:最小可用示例
官方文档给出的示例完整覆盖了"加载模型 → 传入参考图 → 按指令生成"三步。下面按仓库源码(pipeline_joyimage_edit_plus.py 的__call__参数定义)对参数逐一注释:
import torch from PIL import Image from diffusers import JoyImageEditPlusPipeline # 1. 加载官方权重,bfloat16 降低显存占用 pipeline = JoyImageEditPlusPipeline.from_pretrained( "jdopensource/JoyAI-Image-Edit-Plus-Diffusers", dtype=torch.bfloat16 ) pipeline.to("cuda") # 也支持 "mps"、"xpu"、"cpu" # 2. 准备参考图列表(此处 2 张;模型支持每个样本 1~5 张) images = [ Image.open("reference_0.png").convert("RGB"), Image.open("reference_1.png").convert("RGB"), ] # 3. 未显式指定分辨率时,以最后一张参考图为准确定输出尺寸 target_h, target_w = pipeline.image_processor.get_default_height_width(images[-1]) # 4. 执行多图编辑 output = pipeline( images=images, prompt="Combine the person from the second image with the scene from the first image.", negative_prompt="low quality, blurry, deformed", height=target_h, width=target_w, num_inference_steps=30, guidance_scale=4.0, generator=torch.Generator("cuda").manual_seed(42), ).images[0] output.save("joyimage_edit_plus_output.png")几点关键说明:
dtype=torch.bfloat16:模型由 8B MLLM 与 16B MMDiT 组成,体量较大,官方示例与文档一致推荐使用 bfloat16 半精度加载;images入参归一化:源码__call__中若检测到images[0]是单个PIL.Image.Image,会自动包成[images],即"一个样本、多张参考图";若传入list[list[Image.Image]]则按样本批量处理(参见 pipeline_joyimage_edit_plus.py);- 每个样本生成且仅生成一张图:测试用例 test_joyimage_edit_plus.py 明确指出每个样本绑定各自参考图集、不暴露
num_images_per_prompt参数; - 分辨率未指定时的兜底:若既未传
height/width也没有参考图,源码默认回退到1024×1024(pipeline_joyimage_edit_plus.py)。
分辨率桶机制:参考图如何被统一到合法尺寸
多图编辑允许每张参考图分辨率各不相同,但 Transformer 需要规整的 patch 输入。Diffusers 通过 image_processor.py 中的1024 分辨率桶表解决这个问题:
BUCKETS = { 1024: [ (512, 1792), (512, 1856), (512, 1920), (512, 1984), (512, 2048), (576, 1600), (576, 1664), (576, 1728), (576, 1792), (640, 1472), (640, 1536), (640, 1600), ... (1024, 960), (1024, 1024), ... (2048, 512), ], }处理链路分两步:
find_best_bucket(height, width, basesize):计算输入图宽高比,在桶表中选出宽高比最接近的(h, w)目标尺寸(image_processor.py);resize_center_crop(img, target_size):先按max(bh/h, bw/w)的缩放系数把图像放大到覆盖目标尺寸,再做中心裁剪,保证不拉伸变形(image_processor.py)。
该处理器继承自VaeImageProcessor,vae_scale_factor默认 8,basesize默认 1024,且默认执行归一化到[-1, 1]。值得注意的是:由于桶表只覆盖 1024 基数,测试套件在构造极小输入时通过 patchfind_best_bucket固定返回(32, 32)以规避放大(见 test_joyimage_edit_plus.py),这从侧面验证了分辨率桶是流水线不可或缺的一环。
深入源码:多图联合去噪的完整流程
JoyImageEditPlusPipeline.__call__(pipeline_joyimage_edit_plus.py)的执行链路可拆解为六个阶段:
1. 输入归一化与校验
- 统一
images为List[List[Image]](扁平列表自动视为单样本多参考); check_inputs校验height/width必须能被 VAE 空间缩放因子 8 整除,且prompt与prompt_embeds二选一、negative_prompt与negative_prompt_embeds二选一(pipeline_joyimage_edit_plus.py);- 所有参考图先经分辨率桶 + 中心裁剪预处理。
2. 提示词模板与多模态编码
流水线为每张参考图在 prompt 前插入<image>\n标记,再套用 ChatML 模板,其中内部把<image>\n替换为 Qwen3-VL 的视觉占位符<|vision_start|><|image_pad|><|vision_end|>(pipeline_joyimage_edit_plus.py)。随后由Qwen3VLProcessor一次性处理"文本 + 展平后的全部参考图"。
这里有一个非常值得注意的实现细节:_get_last_decoder_hidden_states通过forward hook 直接挂到 Qwen3-VL 最后一个 decoder 层,取回 pre-norm 的隐藏状态,而不是依赖outputs.hidden_states[-1]。源码注释解释了原因:不同 transformers 版本下hidden_states[-1]可能是 post-norm 结果,两者数值尺度相差约 10 倍(std ≈ 2 vs ≈ 21),会直接破坏推理;该 hook 绕开了版本差异,保证跨版本行为一致(pipeline_joyimage_edit_plus.py)。
3. 无分类器引导(CFG)分支
当guidance_scale > 1时启用 CFG:
- 若未提供
negative_prompt,源码自动构造"仅含图像标签、无文字"的空负向 prompt(pipeline_joyimage_edit_plus.py); - 正负向 embedding 通过
_pad_sequence对齐到相同序列长度后拼接为[negative, positive]; - 最终组合预测使用
noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond),并额外做norm rescaling(按文本条件预测的范数与组合预测范数之比缩放),这是 JoyImage 系模型保持 CFG 稳定性的细节(pipeline_joyimage_edit_plus.py)。
4. 目标噪声 + 参考图:prepare_latents的 6D patch 拼接
这是 Edit-Plus 最核心的机制(pipeline_joyimage_edit_plus.py):
- 为目标图采样随机噪声(
t=1帧,h/w为像素尺寸除以 VAE 缩放因子 8); - 每张参考图独立 VAE 编码,并用
latents_mean/latents_std做归一化(参考图 latent 取自latent_dist.mode()); - 按 Transformer 的
patch_size=[1, 2, 2]将每个组件沿(t, h, w)切分为 patch 序列; - 目标噪声 patch 与全部参考图 patch沿序列维度拼接,并生成布尔
target_mask(只有目标 patch 为 True); - 批次内按最大 patch 数做 0-padding,得到
[B, max_patches, C, pt, ph, pw]的 6D 张量。
5. 去噪循环:参考 patch 保持纯净
去噪循环中有一个关键设计(pipeline_joyimage_edit_plus.py):
- 进入循环前保存
clean_reference_backup; - 每一步开始时,用备份把
~target_mask处的参考图 patch 覆盖回"干净"状态(latents[~target_mask] = clean_reference_backup[~target_mask]); - 只有目标 patch 参与噪声更新,参考图信息以固定条件的形式持续注入注意力计算;
- CFG 时把模型输入沿 batch 维翻倍,
shape_list同步翻倍。
也就是说,参考图并非被"逐渐加噪再还原",而是始终以编码后的 clean latent 形式参与联合注意力,让模型在每一 denoising 步都能跨图建立元素对应关系。
6. 解码与后处理
循环结束后,将目标 patch 还原为[1, C, 1, H', W']形状的 latent,反归一化后由 WAN VAE 解码为图像(pipeline_joyimage_edit_plus.py)。output_type支持"pil"(默认)、"np"、"pt"与"latent"(跳过解码直接返回 latent)。
底层模型:JoyImageEditPlusTransformer3DModel
多图联合去噪的能力最终由 transformer_joyimage_edit_plus.py 中的JoyImageEditPlusTransformer3DModel承载,它是一个 3D 条件的 MMDiT,关键配置如下:
| 配置项 | 默认值 | 含义 |
|---|---|---|
patch_size | [1, 2, 2] | 沿(t, h, w)的 patch 尺寸 |
in_channels | 16 | 输入 latent 通道数(VAE 潜空间通道) |
hidden_size | 3072 | 隐藏表示维度 |
num_attention_heads | 24 | 注意力头数 |
text_dim | 4096 | 文本编码器输出维度(对齐 Qwen3-VL) |
mlp_width_ratio | 4.0 | MLP 隐藏维度与hidden_size之比 |
num_layers | 20 | 双流 Transformer 块数量 |
rope_dim_list | [16, 56, 56] | 3D 旋转位置编码沿(t, h, w)的维度 |
rope_type/theta | "rope"/256 | RoPE 类型与基频 |
从源码看有三个实现重点:
- 双流(double-stream)注意力:每个
JoyImageEditPlusTransformerBlock内图像流与文本流各自走 modulation + LayerNorm + FFN,再在JoyImageEditPlusAttention中拼接 Q/K/V 做联合注意力,输出按位置切分回两流(transformer_joyimage_edit_plus.py); - 支持批量 RoPE:
JoyImageEditPlusAttnProcessor与_apply_rotary_emb_batched让同一 batch 内每个样本、每个组件拥有各自的时间/空间位置编码;前向时按shape_list为每个组件生成带时间偏移的 RoPE(组件间current_t_offset递增),padding 区用cos=1, sin=0填充(transformer_joyimage_edit_plus.py); - Wan 风格调制:
JoyImageEditPlusModulate用可学习参数表叠加时间条件,输出 6 组 shift/scale/gate 向量,实现 DiT 式的自适应调制(transformer_joyimage_edit_plus.py)。
核心参数速查表
基于 pipeline_joyimage_edit_plus.py 的__call__文档,整理完整参数说明:
| 参数 | 默认值 | 说明 |
|---|---|---|
images | None | 参考图列表;扁平列表视为单样本多参考,嵌套列表按样本批量处理,每样本 1~5 张 |
prompt | None | 编辑指令;可为str或list[str],与prompt_embeds二选一 |
height/width | None | 输出像素尺寸;未指定时取最后一张参考图的分辨率桶,均需能被 8 整除 |
num_inference_steps | 30 | 去噪步数,越多质量越高但越慢 |
timesteps/sigmas | None | 自定义时间步 / sigma 调度,与num_inference_steps互斥(经retrieve_timesteps透传) |
guidance_scale | 4.0 | CFG 强度;大于 1 启用 CFG,越大越贴合 prompt、但可能降低画质 |
negative_prompt | None | 负向提示;缺省时自动使用"空文字 + 图像标签"模板 |
generator | None | torch.Generator或列表,用于确定性复现 |
latents | None | 预生成的目标噪声 latent,形状(B, C, 1, H', W') |
prompt_embeds/prompt_embeds_mask | None | 预计算的正向文本 embedding 与其注意力 mask |
negative_prompt_embeds/negative_prompt_embeds_mask | None | 预计算的负向 embedding 与其 mask |
output_type | "pil" | 输出格式:"pil"/"np"/"pt"/"latent" |
return_dict | True | 为True返回JoyImageEditPlusPipelineOutput,否则返回裸元组 |
callback_on_step_end | None | 每步去噪结束回调,可配合callback_on_step_end_tensor_inputs(默认["latents"],可选"prompt_embeds") |
max_sequence_length | 4096 | 文本编码最大序列长度 |
构造器层面还有两个默认值值得注意:text_token_max_length=2048控制文本编码最大 token 数;model_cpu_offload_seq = "text_encoder->transformer->vae"定义了顺序 CPU offload 的卸载顺序,显存紧张时可配合enable_model_cpu_offload()使用。
输出对象与测试验证
调用返回的JoyImageEditPlusPipelineOutput定义于 pipeline_output.py,是一个BaseOutput数据类,仅含一个字段images,类型为List[PIL.Image.Image]或np.ndarray(output_type="np"时)。
仓库自带的测试 test_joyimage_edit_plus.py 提供了组件的实际构造范例,可作为本地调试参考:
- Transformer 用极小配置(
hidden_size=32、num_attention_heads=2、num_layers=1、rope_dim_list=[4, 6, 6])即可跑通; - VAE 使用
AutoencoderKLWan(z_dim=16、dim_mult=[1,1,1,1]); - 文本侧用 tiny 版 Qwen3-VL(
huangfeice/tiny-random-Qwen3VLForConditionalGeneration); - 测试输入为两张 32×32 图 + prompt
"combine the two images",输出形状断言为(3, 32, 32)(RGB、单样本)。
这组配置对理解各组件间维度匹配关系(patch 尺寸、通道数、文本维度)非常有帮助,是官方文档之外最直接的补充资料。
常见问题与注意事项
- 显存不足:模型规模大(8B MLLM + 16B MMDiT),务必使用
torch.bfloat16;仍不够时可尝试enable_model_cpu_offload()(按text_encoder → transformer → vae顺序卸载)或enable_sequential_cpu_offload(); - 参考图数量:官方说明每样本支持 1~5 张参考图,超出范围的行为未在源码中显式限制,建议按官方能力使用;
- 分辨率合法性:
height/width必须能被 8 整除(VAE 空间缩放因子),否则check_inputs直接抛ValueError;传None时由最后一张参考图的分辨率桶自动决定; - 确定性复现:传入固定种子的
torch.Generator即可复现,多参考图时也可为每个样本传独立 generator(list[torch.Generator]); - transformers 版本兼容:若自行替换文本编码器或升级 transformers 大版本,注意
_get_last_decoder_hidden_states的 pre-norm hook 机制是保证结果正确的关键路径,不应绕过。
小结
JoyAI-Image-Edit-Plus 的多图编辑能力建立在一套清晰的技术栈之上:Qwen3-VL 负责把"多张图 + 指令"编码为统一的多模态条件,MMDiT 通过"目标噪声 patch 与参考图 clean latent 拼接 + 每步还原参考 patch"实现跨图元素组合,分辨率桶与中心裁剪则保证了变分辨率参考图的输入合法性。本文档对应的完整实现、图像处理器与模型定义均可直接在 src/diffusers/pipelines/joyimage/ 与 src/diffusers/models/transformers/transformer_joyimage_edit_plus.py 中继续深入阅读,测试用例 test_joyimage_edit_plus.py 则是理解各组件最小可用配置的最佳入口。
【免费下载链接】diffusers🤗 Diffusers: State-of-the-art diffusion models for image, video, and audio generation in PyTorch.项目地址: https://gitcode.com/GitHub_Trending/di/diffusers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考