Instructions to use kr-cen/Qwen-Image-MICo with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use kr-cen/Qwen-Image-MICo with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("kr-cen/Qwen-Image-MICo", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Upload 3 files
Browse files- README.md +97 -0
- model_index.json +28 -0
- modeling_qwen_image.py +628 -0
README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
base_model:
|
| 4 |
+
- Qwen-Image-Edit
|
| 5 |
+
pipeline_tag: any-to-any
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
# 🔥 Qwen-Image-MICo • Qwen-Image-Edit-Variant finetuned on Multi-Image Composition Dataset (MICo-150K)
|
| 9 |
+
|
| 10 |
+
[](https://www.arxiv.org/pdf/2512.07348)
|
| 11 |
+
[](https://www.arxiv.org/abs/2512.07348)
|
| 12 |
+
[](https://github.com/A113N-W3I/MICo-150K)
|
| 13 |
+
[](https://mico-150k.github.io/)
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
<!-- <p align="left">
|
| 17 |
+
<a href="https://mico-150k.github.io/">
|
| 18 |
+
<img
|
| 19 |
+
src="https://img.shields.io/badge/MICo-Website-0A66C2?logo=safari&logoColor=white" style="display: inline-block; vertical-align: middle;"
|
| 20 |
+
alt="MICo Website"
|
| 21 |
+
/>
|
| 22 |
+
</a>
|
| 23 |
+
<a href="https://www.arxiv.org/abs/2512.07348">
|
| 24 |
+
<img
|
| 25 |
+
src="https://img.shields.io/badge/MICo-Paper-red?logo=arxiv&logoColor=red" style="display: inline-block; vertical-align: middle;"
|
| 26 |
+
alt="MICo Paper on arXiv"
|
| 27 |
+
/>
|
| 28 |
+
</a>
|
| 29 |
+
<a href="https://github.com/A113N-W3I/MICo-150K" target="_blank" style="margin: 2px;">
|
| 30 |
+
<img
|
| 31 |
+
alt="Github" src="https://img.shields.io/badge/MICo-Bench-536af5?color=536af5&logo=github" style="display: inline-block; vertical-align: middle;"
|
| 32 |
+
alt="MICo Bench"
|
| 33 |
+
/>
|
| 34 |
+
</a>
|
| 35 |
+
|
| 36 |
+
</p> -->
|
| 37 |
+
|
| 38 |
+
## 🎮 Demo
|
| 39 |
+
|
| 40 |
+
🔥🔥🔥 Please visit our [huggingface space](https://huggingface.co/spaces/kr-cen/Qwen-Image-MICo), the model is implemented on huggingface ZeroGPU.
|
| 41 |
+
|
| 42 |
+
## 🎨 Gallery
|
| 43 |
+
|
| 44 |
+

|
| 45 |
+
|
| 46 |
+

|
| 47 |
+
|
| 48 |
+

|
| 49 |
+
|
| 50 |
+

|
| 51 |
+
|
| 52 |
+

|
| 53 |
+
|
| 54 |
+

|
| 55 |
+
|
| 56 |
+

|
| 57 |
+
|
| 58 |
+

|
| 59 |
+
|
| 60 |
+

|
| 61 |
+
|
| 62 |
+
## 🌋 Emergent Capabilities
|
| 63 |
+
|
| 64 |
+
Hint: The following examples are editing types that are NOT present in our MICo-150K dataset, but these editing capabilities emerged in Qwen-Image-MICo during training.
|
| 65 |
+
|
| 66 |
+
Thus, this capability may be unstable, and the following examples are for reference only.
|
| 67 |
+
|
| 68 |
+

|
| 69 |
+
|
| 70 |
+

|
| 71 |
+
|
| 72 |
+

|
| 73 |
+
|
| 74 |
+

|
| 75 |
+
|
| 76 |
+

|
| 77 |
+
|
| 78 |
+

|
| 79 |
+
|
| 80 |
+

|
| 81 |
+
|
| 82 |
+
## ✍️ Citation
|
| 83 |
+
|
| 84 |
+
If you find this work useful, please cite:
|
| 85 |
+
|
| 86 |
+
~~~
|
| 87 |
+
@article{wei2025mico,
|
| 88 |
+
title={MICo-150K: A Comprehensive Dataset Advancing Multi-Image Composition},
|
| 89 |
+
author={Wei, Xinyu and Cen, Kangrui and Wei, Hongyang and Guo, Zhen and Li, Bairui and Wang, Zeqing and Zhang, Jinrui and Zhang, Lei},
|
| 90 |
+
journal={arXiv preprint arXiv:2512.07348},
|
| 91 |
+
year={2025}
|
| 92 |
+
}
|
| 93 |
+
~~~
|
| 94 |
+
|
| 95 |
+
## License
|
| 96 |
+
|
| 97 |
+
Qwen-Image-MICo is licensed under the Apache 2.0 license.
|
model_index.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_class_name": "QwenImageEditPipeline",
|
| 3 |
+
"_diffusers_version": "0.35.0.dev0",
|
| 4 |
+
"processor": [
|
| 5 |
+
"transformers",
|
| 6 |
+
"Qwen2VLProcessor"
|
| 7 |
+
],
|
| 8 |
+
"scheduler": [
|
| 9 |
+
"diffusers",
|
| 10 |
+
"FlowMatchEulerDiscreteScheduler"
|
| 11 |
+
],
|
| 12 |
+
"text_encoder": [
|
| 13 |
+
"transformers",
|
| 14 |
+
"Qwen2_5_VLForConditionalGeneration"
|
| 15 |
+
],
|
| 16 |
+
"tokenizer": [
|
| 17 |
+
"transformers",
|
| 18 |
+
"Qwen2Tokenizer"
|
| 19 |
+
],
|
| 20 |
+
"transformer": [
|
| 21 |
+
"diffusers",
|
| 22 |
+
"QwenImageTransformer2DModel"
|
| 23 |
+
],
|
| 24 |
+
"vae": [
|
| 25 |
+
"diffusers",
|
| 26 |
+
"AutoencoderKLQwenImage"
|
| 27 |
+
]
|
| 28 |
+
}
|
modeling_qwen_image.py
ADDED
|
@@ -0,0 +1,628 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Copyright 2025 Qwen-Image Team and The HuggingFace Team. All rights reserved.
|
| 2 |
+
# Independently adapted and refactored by the MICo Team.
|
| 3 |
+
#
|
| 4 |
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
| 5 |
+
# you may not use this file except in compliance with the License.
|
| 6 |
+
# You may obtain a copy of the License at
|
| 7 |
+
#
|
| 8 |
+
# http://www.apache.org/licenses/LICENSE-2.0
|
| 9 |
+
#
|
| 10 |
+
# Unless required by applicable law or agreed to in writing, software
|
| 11 |
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
| 12 |
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
| 13 |
+
# See the License for the specific language governing permissions and
|
| 14 |
+
# limitations under the License.
|
| 15 |
+
|
| 16 |
+
import inspect
|
| 17 |
+
import math
|
| 18 |
+
from typing import Any, Callable, Dict, List, Optional, Union, Tuple
|
| 19 |
+
|
| 20 |
+
import numpy as np
|
| 21 |
+
import torch
|
| 22 |
+
from PIL import Image
|
| 23 |
+
|
| 24 |
+
from transformers import Qwen2_5_VLForConditionalGeneration, Qwen2Tokenizer, Qwen2VLProcessor, PretrainedConfig
|
| 25 |
+
from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
|
| 26 |
+
from diffusers.loaders import QwenImageLoraLoaderMixin
|
| 27 |
+
from diffusers.models import AutoencoderKLQwenImage, QwenImageTransformer2DModel
|
| 28 |
+
from diffusers.schedulers import FlowMatchEulerDiscreteScheduler
|
| 29 |
+
from diffusers.utils import is_torch_xla_available, logging, replace_example_docstring
|
| 30 |
+
from diffusers.utils.torch_utils import randn_tensor
|
| 31 |
+
from diffusers.pipelines.pipeline_utils import DiffusionPipeline
|
| 32 |
+
from diffusers.pipelines.qwenimage.pipeline_output import QwenImagePipelineOutput
|
| 33 |
+
|
| 34 |
+
if is_torch_xla_available():
|
| 35 |
+
import torch_xla.core.xla_model as xm
|
| 36 |
+
XLA_AVAILABLE = True
|
| 37 |
+
else:
|
| 38 |
+
XLA_AVAILABLE = False
|
| 39 |
+
|
| 40 |
+
logger = logging.get_logger(__name__)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class _MICoPipelineUtils:
|
| 44 |
+
"""
|
| 45 |
+
Internal utility class grouping static computational methods for the diffusion pipeline.
|
| 46 |
+
Maintains clean namespace and modularizes mathematical operations.
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
@staticmethod
|
| 50 |
+
def compute_temporal_shift(
|
| 51 |
+
image_seq_len: int,
|
| 52 |
+
base_seq_len: int = 256,
|
| 53 |
+
max_seq_len: int = 4096,
|
| 54 |
+
base_shift: float = 0.5,
|
| 55 |
+
max_shift: float = 1.15,
|
| 56 |
+
) -> float:
|
| 57 |
+
"""Calculates the sequence shift parameter based on sequence length ratios."""
|
| 58 |
+
slope = (max_shift - base_shift) / (max_seq_len - base_seq_len)
|
| 59 |
+
intercept = base_shift - slope * base_seq_len
|
| 60 |
+
return image_seq_len * slope + intercept
|
| 61 |
+
|
| 62 |
+
@staticmethod
|
| 63 |
+
def fetch_timesteps(
|
| 64 |
+
scheduler,
|
| 65 |
+
num_inference_steps: Optional[int] = None,
|
| 66 |
+
device: Optional[Union[str, torch.device]] = None,
|
| 67 |
+
timesteps: Optional[List[int]] = None,
|
| 68 |
+
sigmas: Optional[List[float]] = None,
|
| 69 |
+
**kwargs,
|
| 70 |
+
) -> Tuple[torch.Tensor, int]:
|
| 71 |
+
"""Resolves custom timesteps or sigmas from the given scheduler instance."""
|
| 72 |
+
if timesteps is not None and sigmas is not None:
|
| 73 |
+
raise ValueError("Configuration Error: Define either `timesteps` or `sigmas`, but not both.")
|
| 74 |
+
|
| 75 |
+
if timesteps is not None:
|
| 76 |
+
if "timesteps" not in inspect.signature(scheduler.set_timesteps).parameters:
|
| 77 |
+
raise ValueError(f"Scheduler {scheduler.__class__.__name__} rejects custom timesteps.")
|
| 78 |
+
scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
|
| 79 |
+
elif sigmas is not None:
|
| 80 |
+
if "sigmas" not in inspect.signature(scheduler.set_timesteps).parameters:
|
| 81 |
+
raise ValueError(f"Scheduler {scheduler.__class__.__name__} rejects custom sigmas.")
|
| 82 |
+
scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
|
| 83 |
+
else:
|
| 84 |
+
scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
|
| 85 |
+
|
| 86 |
+
return scheduler.timesteps, len(scheduler.timesteps)
|
| 87 |
+
|
| 88 |
+
@staticmethod
|
| 89 |
+
def extract_latent_distribution(
|
| 90 |
+
encoder_output: Any, generator: Optional[torch.Generator] = None, mode: str = "sample"
|
| 91 |
+
) -> torch.Tensor:
|
| 92 |
+
"""Safely extracts tensors from VAE distributions."""
|
| 93 |
+
if hasattr(encoder_output, "latent_dist"):
|
| 94 |
+
return encoder_output.latent_dist.sample(generator) if mode == "sample" else encoder_output.latent_dist.mode()
|
| 95 |
+
elif hasattr(encoder_output, "latents"):
|
| 96 |
+
return encoder_output.latents
|
| 97 |
+
raise AttributeError("Failed to extract latents: Unsupported encoder output format.")
|
| 98 |
+
|
| 99 |
+
@staticmethod
|
| 100 |
+
def pad_image_dimensions(image: Image.Image, multiple_of: int = 32) -> Image.Image:
|
| 101 |
+
"""Resizes a PIL image ensuring its dimensions are strict multiples of the given integer."""
|
| 102 |
+
w, h = image.size
|
| 103 |
+
aligned_w = round(w / multiple_of) * multiple_of
|
| 104 |
+
aligned_h = round(h / multiple_of) * multiple_of
|
| 105 |
+
return image.resize((aligned_w, aligned_h))
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
class QwenImageConfig(PretrainedConfig):
|
| 109 |
+
"""Configuration class for parameterized model tracking."""
|
| 110 |
+
model_type = "qwen_image_transformer"
|
| 111 |
+
|
| 112 |
+
def __init__(
|
| 113 |
+
self,
|
| 114 |
+
attention_head_dim=128,
|
| 115 |
+
num_attention_heads=24,
|
| 116 |
+
num_layers=60,
|
| 117 |
+
in_channels=64,
|
| 118 |
+
out_channels=16,
|
| 119 |
+
patch_size=2,
|
| 120 |
+
joint_attention_dim=3584,
|
| 121 |
+
axes_dims_rope=[16, 56, 56],
|
| 122 |
+
guidance_embeds=False,
|
| 123 |
+
**kwargs,
|
| 124 |
+
):
|
| 125 |
+
self.attention_head_dim = attention_head_dim
|
| 126 |
+
self.num_attention_heads = num_attention_heads
|
| 127 |
+
self.num_layers = num_layers
|
| 128 |
+
self.in_channels = in_channels
|
| 129 |
+
self.out_channels = out_channels
|
| 130 |
+
self.patch_size = patch_size
|
| 131 |
+
self.joint_attention_dim = joint_attention_dim
|
| 132 |
+
self.axes_dims_rope = axes_dims_rope
|
| 133 |
+
self.guidance_embeds = guidance_embeds
|
| 134 |
+
super().__init__(**kwargs)
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
MICO_EXAMPLE_DOCS = """
|
| 138 |
+
Examples:
|
| 139 |
+
```py
|
| 140 |
+
>>> import torch
|
| 141 |
+
>>> from PIL import Image
|
| 142 |
+
>>> from diffusers import QwenImageEditPipeline
|
| 143 |
+
>>> from diffusers.utils import load_image
|
| 144 |
+
|
| 145 |
+
>>> # Initialize the MICo-adapted Multi-Modal Pipeline
|
| 146 |
+
>>> pipeline = QwenImageEditPipeline.from_pretrained("Qwen/Qwen-Image-Edit", torch_dtype=torch.bfloat16)
|
| 147 |
+
>>> pipeline.to("cuda")
|
| 148 |
+
|
| 149 |
+
>>> # Fetch source vision data
|
| 150 |
+
>>> source_vision = load_image("https://.../sample_workspace.png").convert("RGB")
|
| 151 |
+
|
| 152 |
+
>>> # Define agent-based instruction for visual modification
|
| 153 |
+
>>> task_instruction = (
|
| 154 |
+
... "Modify the scene into a futuristic computer vision lab, "
|
| 155 |
+
... "featuring an autonomous agent analyzing a complex LaTeX manuscript, highly detailed."
|
| 156 |
+
... )
|
| 157 |
+
|
| 158 |
+
>>> generated_output = pipeline(source_vision, task_instruction, num_inference_steps=50).images[0]
|
| 159 |
+
>>> generated_output.save("mico_agent_output.png")
|
| 160 |
+
```
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
class QwenImageEditPipeline(DiffusionPipeline, QwenImageLoraLoaderMixin):
|
| 165 |
+
r"""
|
| 166 |
+
Advanced Pipeline Architecture adapted for dynamic image editing via multi-modal instructions.
|
| 167 |
+
|
| 168 |
+
Architecture Components:
|
| 169 |
+
transformer: The core MMDiT architecture handling conditional denoising.
|
| 170 |
+
scheduler: Flow-matching scheduler controlling the noise traversal sequence.
|
| 171 |
+
vae: Handles transition between pixel space and latent dimensional space.
|
| 172 |
+
text_encoder: Qwen2.5-VL instruction encoder framework.
|
| 173 |
+
tokenizer: Linguistic boundary mapping utility.
|
| 174 |
+
"""
|
| 175 |
+
|
| 176 |
+
model_cpu_offload_seq = "text_encoder->transformer->vae"
|
| 177 |
+
_callback_tensor_inputs = ["latents", "prompt_embeds"]
|
| 178 |
+
|
| 179 |
+
def __init__(
|
| 180 |
+
self,
|
| 181 |
+
scheduler: FlowMatchEulerDiscreteScheduler,
|
| 182 |
+
vae: AutoencoderKLQwenImage,
|
| 183 |
+
text_encoder: Qwen2_5_VLForConditionalGeneration,
|
| 184 |
+
tokenizer: Qwen2Tokenizer,
|
| 185 |
+
processor: Qwen2VLProcessor,
|
| 186 |
+
transformer: QwenImageTransformer2DModel,
|
| 187 |
+
):
|
| 188 |
+
super().__init__()
|
| 189 |
+
self.register_modules(
|
| 190 |
+
vae=vae,
|
| 191 |
+
text_encoder=text_encoder,
|
| 192 |
+
tokenizer=tokenizer,
|
| 193 |
+
processor=processor,
|
| 194 |
+
transformer=transformer,
|
| 195 |
+
scheduler=scheduler,
|
| 196 |
+
)
|
| 197 |
+
|
| 198 |
+
self.vae_scale_factor = 2 ** len(self.vae.temperal_downsample) if getattr(self, "vae", None) else 8
|
| 199 |
+
self.latent_channels = self.vae.config.z_dim if getattr(self, "vae", None) else 16
|
| 200 |
+
self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor * 2)
|
| 201 |
+
self.vl_processor = processor
|
| 202 |
+
self.tokenizer_max_length = 1024
|
| 203 |
+
|
| 204 |
+
self.system_message = (
|
| 205 |
+
"Describe the key features of the input image (color, shape, size, texture, objects, background), "
|
| 206 |
+
"then explain how the user's text instruction should alter or modify the image. Generate a new "
|
| 207 |
+
"image that meets the user's requirements while maintaining consistency with the original input where appropriate."
|
| 208 |
+
)
|
| 209 |
+
self.prompt_template_encode_start_idx = 64
|
| 210 |
+
self.default_sample_size = 128
|
| 211 |
+
|
| 212 |
+
def _extract_masked_hidden(self, hidden_states: torch.Tensor, mask: torch.Tensor):
|
| 213 |
+
"""Isolates valid computational vectors from padded hidden states."""
|
| 214 |
+
bool_mask = mask.bool()
|
| 215 |
+
valid_lengths = bool_mask.sum(dim=1)
|
| 216 |
+
selected_states = hidden_states[bool_mask]
|
| 217 |
+
return torch.split(selected_states, valid_lengths.tolist(), dim=0)
|
| 218 |
+
|
| 219 |
+
def _get_qwen_prompt_embeds(
|
| 220 |
+
self,
|
| 221 |
+
prompts: Union[str, List[str]] = None,
|
| 222 |
+
images: List[List[Image.Image]] = None,
|
| 223 |
+
device: Optional[torch.device] = None,
|
| 224 |
+
dtype: Optional[torch.dtype] = None,
|
| 225 |
+
):
|
| 226 |
+
"""Processes and extracts embeddings via the visual-language processor framework."""
|
| 227 |
+
device = device or self._execution_device
|
| 228 |
+
dtype = dtype or self.text_encoder.dtype
|
| 229 |
+
|
| 230 |
+
prompts = [prompts] if isinstance(prompts, str) else prompts
|
| 231 |
+
|
| 232 |
+
# Standardize nested image lists
|
| 233 |
+
if isinstance(images, Image.Image):
|
| 234 |
+
images = [[images]]
|
| 235 |
+
elif isinstance(images[0], Image.Image):
|
| 236 |
+
images = [images]
|
| 237 |
+
|
| 238 |
+
assert len(prompts) == len(images), "Data integrity error: Prompt and vision input batch sizes mismatch."
|
| 239 |
+
|
| 240 |
+
compiled_texts = []
|
| 241 |
+
for prompt, image_list in zip(prompts, images):
|
| 242 |
+
chat_payload = [
|
| 243 |
+
{"role": "system", "content": self.system_message},
|
| 244 |
+
{
|
| 245 |
+
"role": "user",
|
| 246 |
+
"content": [{"type": "image", "image": img} for img in image_list] + [{"type": "text", "text": prompt}],
|
| 247 |
+
},
|
| 248 |
+
]
|
| 249 |
+
compiled_texts.append(self.processor.apply_chat_template(chat_payload, tokenize=False, add_generation_prompt=True))
|
| 250 |
+
|
| 251 |
+
model_inputs = self.processor(
|
| 252 |
+
text=compiled_texts,
|
| 253 |
+
images=images,
|
| 254 |
+
do_resize=False,
|
| 255 |
+
padding=True,
|
| 256 |
+
return_tensors="pt"
|
| 257 |
+
).to(self.device)
|
| 258 |
+
|
| 259 |
+
drop_idx = self.prompt_template_encode_start_idx
|
| 260 |
+
outputs = self.text_encoder(
|
| 261 |
+
input_ids=model_inputs.input_ids,
|
| 262 |
+
attention_mask=model_inputs.attention_mask,
|
| 263 |
+
pixel_values=model_inputs.pixel_values,
|
| 264 |
+
image_grid_thw=model_inputs.image_grid_thw,
|
| 265 |
+
output_hidden_states=True,
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
+
hidden_states = outputs.hidden_states[-1]
|
| 269 |
+
split_states = self._extract_masked_hidden(hidden_states, model_inputs.attention_mask)
|
| 270 |
+
split_states = [state[drop_idx:] for state in split_states]
|
| 271 |
+
|
| 272 |
+
attn_mask_list = [torch.ones(state.size(0), dtype=torch.long, device=state.device) for state in split_states]
|
| 273 |
+
max_seq_len = max([state.size(0) for state in split_states])
|
| 274 |
+
|
| 275 |
+
prompt_embeds = torch.stack(
|
| 276 |
+
[torch.cat([state, state.new_zeros(max_seq_len - state.size(0), state.size(1))]) for state in split_states]
|
| 277 |
+
).to(dtype=dtype, device=device)
|
| 278 |
+
|
| 279 |
+
encoder_attention_mask = torch.stack(
|
| 280 |
+
[torch.cat([mask, mask.new_zeros(max_seq_len - mask.size(0))]) for mask in attn_mask_list]
|
| 281 |
+
)
|
| 282 |
+
|
| 283 |
+
return prompt_embeds, encoder_attention_mask
|
| 284 |
+
|
| 285 |
+
def encode_prompt(
|
| 286 |
+
self,
|
| 287 |
+
prompt: Union[str, List[str]],
|
| 288 |
+
images: List[Image.Image] = None,
|
| 289 |
+
device: Optional[torch.device] = None,
|
| 290 |
+
num_images_per_prompt: int = 1,
|
| 291 |
+
prompt_embeds: Optional[torch.Tensor] = None,
|
| 292 |
+
prompt_embeds_mask: Optional[torch.Tensor] = None,
|
| 293 |
+
max_sequence_length: int = 1024,
|
| 294 |
+
):
|
| 295 |
+
"""Constructs and duplicates conditional multi-modal latents for execution batches."""
|
| 296 |
+
device = device or self._execution_device
|
| 297 |
+
prompt = [prompt] if isinstance(prompt, str) else prompt
|
| 298 |
+
batch_size = len(prompt) if prompt_embeds is None else prompt_embeds.shape[0]
|
| 299 |
+
|
| 300 |
+
if prompt_embeds is None:
|
| 301 |
+
prompt_embeds, prompt_embeds_mask = self._get_qwen_prompt_embeds(prompt, images, device)
|
| 302 |
+
|
| 303 |
+
_, seq_len, _ = prompt_embeds.shape
|
| 304 |
+
prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1).view(batch_size * num_images_per_prompt, seq_len, -1)
|
| 305 |
+
prompt_embeds_mask = prompt_embeds_mask.repeat(1, num_images_per_prompt, 1).view(batch_size * num_images_per_prompt, seq_len)
|
| 306 |
+
|
| 307 |
+
return prompt_embeds, prompt_embeds_mask
|
| 308 |
+
|
| 309 |
+
def check_inputs(
|
| 310 |
+
self,
|
| 311 |
+
prompt,
|
| 312 |
+
height,
|
| 313 |
+
width,
|
| 314 |
+
negative_prompt=None,
|
| 315 |
+
prompt_embeds=None,
|
| 316 |
+
negative_prompt_embeds=None,
|
| 317 |
+
prompt_embeds_mask=None,
|
| 318 |
+
negative_prompt_embeds_mask=None,
|
| 319 |
+
callback_on_step_end_tensor_inputs=None,
|
| 320 |
+
max_sequence_length=None,
|
| 321 |
+
):
|
| 322 |
+
"""Performs pre-flight integrity checks on all pipeline parameters."""
|
| 323 |
+
spatial_constraint = self.vae_scale_factor * 2
|
| 324 |
+
if height % spatial_constraint != 0 or width % spatial_constraint != 0:
|
| 325 |
+
logger.warning(f"Resolution ({width}x{height}) is not divisible by {spatial_constraint}. Auto-resizing will trigger.")
|
| 326 |
+
|
| 327 |
+
if callback_on_step_end_tensor_inputs:
|
| 328 |
+
invalid_cbs = [k for k in callback_on_step_end_tensor_inputs if k not in self._callback_tensor_inputs]
|
| 329 |
+
if invalid_cbs:
|
| 330 |
+
raise ValueError(f"Invalid callback tensors: {invalid_cbs}. Permitted: {self._callback_tensor_inputs}")
|
| 331 |
+
|
| 332 |
+
if bool(prompt is not None) == bool(prompt_embeds is not None):
|
| 333 |
+
raise ValueError("Configuration logic trap: Define strictly one of `prompt` OR `prompt_embeds`.")
|
| 334 |
+
|
| 335 |
+
if prompt is not None and not isinstance(prompt, (str, list)):
|
| 336 |
+
raise ValueError(f"`prompt` validation failed. Expected str/list, got {type(prompt)}")
|
| 337 |
+
|
| 338 |
+
if negative_prompt is not None and negative_prompt_embeds is not None:
|
| 339 |
+
raise ValueError("Conflict: Both `negative_prompt` and `negative_prompt_embeds` were supplied.")
|
| 340 |
+
|
| 341 |
+
if prompt_embeds is not None and prompt_embeds_mask is None:
|
| 342 |
+
raise ValueError("Masking integrity error: `prompt_embeds_mask` is mandatory when injecting `prompt_embeds`.")
|
| 343 |
+
|
| 344 |
+
if negative_prompt_embeds is not None and negative_prompt_embeds_mask is None:
|
| 345 |
+
raise ValueError("Masking integrity error: `negative_prompt_embeds_mask` is mandatory alongside `negative_prompt_embeds`.")
|
| 346 |
+
|
| 347 |
+
if max_sequence_length is not None and max_sequence_length > 1024:
|
| 348 |
+
raise ValueError(f"Sequence cap exceeded: {max_sequence_length} > 1024 limit.")
|
| 349 |
+
|
| 350 |
+
@staticmethod
|
| 351 |
+
def _pack_latents(latents, batch_size, num_channels_latents, height, width):
|
| 352 |
+
"""Transforms standard latent layouts into packed multi-patch configurations."""
|
| 353 |
+
latents = latents.view(batch_size, num_channels_latents, height // 2, 2, width // 2, 2)
|
| 354 |
+
latents = latents.permute(0, 2, 4, 1, 3, 5)
|
| 355 |
+
return latents.reshape(batch_size, (height // 2) * (width // 2), num_channels_latents * 4)
|
| 356 |
+
|
| 357 |
+
@staticmethod
|
| 358 |
+
def _unpack_latents(latents, height, width, vae_scale_factor):
|
| 359 |
+
"""Reverses multi-patch configurations back to classical latent layouts."""
|
| 360 |
+
batch_size, _, channels = latents.shape
|
| 361 |
+
h_adjusted = 2 * (int(height) // (vae_scale_factor * 2))
|
| 362 |
+
w_adjusted = 2 * (int(width) // (vae_scale_factor * 2))
|
| 363 |
+
|
| 364 |
+
latents = latents.view(batch_size, h_adjusted // 2, w_adjusted // 2, channels // 4, 2, 2)
|
| 365 |
+
latents = latents.permute(0, 3, 1, 4, 2, 5)
|
| 366 |
+
return latents.reshape(batch_size, channels // 4, 1, h_adjusted, w_adjusted)
|
| 367 |
+
|
| 368 |
+
def _encode_vae_image(self, image: torch.Tensor, generator: torch.Generator):
|
| 369 |
+
"""Converts raw pixel space into initial VAE distributions."""
|
| 370 |
+
if isinstance(generator, list):
|
| 371 |
+
latents_cache = [
|
| 372 |
+
_MICoPipelineUtils.extract_latent_distribution(self.vae.encode(image[i : i + 1]), generator=generator[i], mode="argmax")
|
| 373 |
+
for i in range(image.shape[0])
|
| 374 |
+
]
|
| 375 |
+
image_latents = torch.cat(latents_cache, dim=0)
|
| 376 |
+
else:
|
| 377 |
+
image_latents = _MICoPipelineUtils.extract_latent_distribution(self.vae.encode(image), generator=generator, mode="argmax")
|
| 378 |
+
|
| 379 |
+
latents_mean = torch.tensor(self.vae.config.latents_mean).view(1, self.latent_channels, 1, 1, 1).to(image_latents.device, image_latents.dtype)
|
| 380 |
+
latents_std = torch.tensor(self.vae.config.latents_std).view(1, self.latent_channels, 1, 1, 1).to(image_latents.device, image_latents.dtype)
|
| 381 |
+
|
| 382 |
+
return (image_latents - latents_mean) / latents_std
|
| 383 |
+
|
| 384 |
+
def enable_vae_slicing(self): self.vae.enable_slicing()
|
| 385 |
+
def disable_vae_slicing(self): self.vae.disable_slicing()
|
| 386 |
+
def enable_vae_tiling(self): self.vae.enable_tiling()
|
| 387 |
+
def disable_vae_tiling(self): self.vae.disable_tiling()
|
| 388 |
+
|
| 389 |
+
def prepare_latents(
|
| 390 |
+
self, images, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None
|
| 391 |
+
):
|
| 392 |
+
"""Allocates and standardizes initial noise vectors and vision encodings for the step loop."""
|
| 393 |
+
h_adjusted = 2 * (int(height) // (self.vae_scale_factor * 2))
|
| 394 |
+
w_adjusted = 2 * (int(width) // (self.vae_scale_factor * 2))
|
| 395 |
+
noise_shape = (batch_size, 1, num_channels_latents, h_adjusted, w_adjusted)
|
| 396 |
+
|
| 397 |
+
processed_img_latents = []
|
| 398 |
+
for img_tensor in images:
|
| 399 |
+
img_tensor = img_tensor.to(device=device, dtype=dtype)
|
| 400 |
+
current_latents = self._encode_vae_image(img_tensor, generator) if img_tensor.shape[1] != self.latent_channels else img_tensor
|
| 401 |
+
|
| 402 |
+
if batch_size > current_latents.shape[0] and batch_size % current_latents.shape[0] == 0:
|
| 403 |
+
duplication_factor = batch_size // current_latents.shape[0]
|
| 404 |
+
current_latents = torch.cat([current_latents] * duplication_factor, dim=0)
|
| 405 |
+
elif batch_size > current_latents.shape[0]:
|
| 406 |
+
raise ValueError(f"Batch misalignment: Cannot broadcast {current_latents.shape[0]} inputs to size {batch_size}.")
|
| 407 |
+
|
| 408 |
+
img_h, img_w = current_latents.shape[3:]
|
| 409 |
+
packed_img = self._pack_latents(current_latents, batch_size, num_channels_latents, img_h, img_w)
|
| 410 |
+
processed_img_latents.append(packed_img)
|
| 411 |
+
|
| 412 |
+
if isinstance(generator, list) and len(generator) != batch_size:
|
| 413 |
+
raise ValueError(f"Generator constraint failed. Passed {len(generator)} generators for batch size {batch_size}.")
|
| 414 |
+
|
| 415 |
+
if latents is None:
|
| 416 |
+
latents = randn_tensor(noise_shape, generator=generator, device=device, dtype=dtype)
|
| 417 |
+
latents = self._pack_latents(latents, batch_size, num_channels_latents, h_adjusted, w_adjusted)
|
| 418 |
+
else:
|
| 419 |
+
latents = latents.to(device=device, dtype=dtype)
|
| 420 |
+
|
| 421 |
+
return latents, processed_img_latents
|
| 422 |
+
|
| 423 |
+
@property
|
| 424 |
+
def guidance_scale(self): return self._guidance_scale
|
| 425 |
+
|
| 426 |
+
@property
|
| 427 |
+
def attention_kwargs(self): return self._attention_kwargs
|
| 428 |
+
|
| 429 |
+
@property
|
| 430 |
+
def num_timesteps(self): return self._num_timesteps
|
| 431 |
+
|
| 432 |
+
@property
|
| 433 |
+
def current_timestep(self): return self._current_timestep
|
| 434 |
+
|
| 435 |
+
@property
|
| 436 |
+
def interrupt(self): return self._interrupt
|
| 437 |
+
|
| 438 |
+
def _prepare_vision_inputs(self, images: List[PipelineImageInput]) -> Tuple[List[Image.Image], int, int]:
|
| 439 |
+
"""Calculates area-constrained ratios and standardizes spatial constraints."""
|
| 440 |
+
total_pixels = sum(math.prod(img.size) for img in images)
|
| 441 |
+
ratio = 1024 / (total_pixels ** 0.5)
|
| 442 |
+
|
| 443 |
+
resized_imgs = [img.resize(size=(round(img.width * ratio), round(img.height * ratio))) for img in images]
|
| 444 |
+
aligned_imgs = [_MICoPipelineUtils.pad_image_dimensions(img, multiple_of=32) for img in resized_imgs]
|
| 445 |
+
|
| 446 |
+
return aligned_imgs, aligned_imgs[0].width, aligned_imgs[0].height
|
| 447 |
+
|
| 448 |
+
def _decode_latents_to_image(self, latents, height, width, output_type):
|
| 449 |
+
"""Transforms structural noise output back to human-interpretable pixels."""
|
| 450 |
+
if output_type == "latent":
|
| 451 |
+
return latents
|
| 452 |
+
|
| 453 |
+
unpacked_latents = self._unpack_latents(latents, height, width, self.vae_scale_factor).to(self.vae.dtype)
|
| 454 |
+
|
| 455 |
+
l_mean = torch.tensor(self.vae.config.latents_mean).view(1, self.vae.config.z_dim, 1, 1, 1).to(unpacked_latents.device, unpacked_latents.dtype)
|
| 456 |
+
l_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(unpacked_latents.device, unpacked_latents.dtype)
|
| 457 |
+
|
| 458 |
+
normalized_latents = unpacked_latents / l_std + l_mean
|
| 459 |
+
decoded_pixels = self.vae.decode(normalized_latents, return_dict=False)[0][:, :, 0]
|
| 460 |
+
|
| 461 |
+
return self.image_processor.postprocess(decoded_pixels, output_type=output_type)
|
| 462 |
+
|
| 463 |
+
@torch.no_grad()
|
| 464 |
+
@replace_example_docstring(MICO_EXAMPLE_DOCS)
|
| 465 |
+
def __call__(
|
| 466 |
+
self,
|
| 467 |
+
images: List[PipelineImageInput] = None,
|
| 468 |
+
prompt: Union[str, List[str]] = None,
|
| 469 |
+
negative_prompt: Union[str, List[str]] = None,
|
| 470 |
+
true_cfg_scale: float = 4.0,
|
| 471 |
+
height: Optional[int] = None,
|
| 472 |
+
width: Optional[int] = None,
|
| 473 |
+
num_inference_steps: int = 50,
|
| 474 |
+
sigmas: Optional[List[float]] = None,
|
| 475 |
+
guidance_scale: float = 1.0,
|
| 476 |
+
num_images_per_prompt: int = 1,
|
| 477 |
+
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
|
| 478 |
+
latents: Optional[torch.Tensor] = None,
|
| 479 |
+
prompt_embeds: Optional[torch.Tensor] = None,
|
| 480 |
+
prompt_embeds_mask: Optional[torch.Tensor] = None,
|
| 481 |
+
negative_prompt_embeds: Optional[torch.Tensor] = None,
|
| 482 |
+
negative_prompt_embeds_mask: Optional[torch.Tensor] = None,
|
| 483 |
+
output_type: Optional[str] = "pil",
|
| 484 |
+
return_dict: bool = True,
|
| 485 |
+
attention_kwargs: Optional[Dict[str, Any]] = None,
|
| 486 |
+
callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = None,
|
| 487 |
+
callback_on_step_end_tensor_inputs: List[str] = ["latents"],
|
| 488 |
+
max_sequence_length: int = 512,
|
| 489 |
+
):
|
| 490 |
+
r"""
|
| 491 |
+
Executes the multi-modal editing sequence based on the provided vision inputs and textual instructions.
|
| 492 |
+
|
| 493 |
+
Args:
|
| 494 |
+
images: Source vision data to be modified.
|
| 495 |
+
prompt: Textual instructions dictating the structural or stylistic changes.
|
| 496 |
+
negative_prompt: Elements or styles to actively suppress during generation.
|
| 497 |
+
num_inference_steps: Number of denoising iterations (default: 50).
|
| 498 |
+
guidance_scale: Modifier for classifier-free guidance adherence.
|
| 499 |
+
output_type: Format of the returned vision data ('pil' or 'latent').
|
| 500 |
+
return_dict: Whether to wrap outputs in a pipeline standard dictionary.
|
| 501 |
+
|
| 502 |
+
Examples:
|
| 503 |
+
|
| 504 |
+
Returns:
|
| 505 |
+
`QwenImagePipelineOutput` or `tuple`: Contains the generated vision data.
|
| 506 |
+
"""
|
| 507 |
+
# 1. Image preparation & Alignment
|
| 508 |
+
images = [images] if not isinstance(images, (list, tuple)) else images
|
| 509 |
+
images, derived_w, derived_h = self._prepare_vision_inputs(images)
|
| 510 |
+
|
| 511 |
+
width, height = width or derived_w, height or derived_h
|
| 512 |
+
padding_base = self.vae_scale_factor * 2
|
| 513 |
+
width = (width // padding_base) * padding_base
|
| 514 |
+
height = (height // padding_base) * padding_base
|
| 515 |
+
|
| 516 |
+
# 2. Input Sanity Checks
|
| 517 |
+
self.check_inputs(
|
| 518 |
+
prompt, height, width, negative_prompt, prompt_embeds,
|
| 519 |
+
negative_prompt_embeds, prompt_embeds_mask, negative_prompt_embeds_mask,
|
| 520 |
+
callback_on_step_end_tensor_inputs, max_sequence_length
|
| 521 |
+
)
|
| 522 |
+
|
| 523 |
+
self._guidance_scale = guidance_scale
|
| 524 |
+
self._attention_kwargs = attention_kwargs or {}
|
| 525 |
+
self._current_timestep = None
|
| 526 |
+
self._interrupt = False
|
| 527 |
+
|
| 528 |
+
batch_size = len(prompt) if isinstance(prompt, list) else (prompt_embeds.shape[0] if prompt_embeds is not None else 1)
|
| 529 |
+
device = self._execution_device
|
| 530 |
+
|
| 531 |
+
# 3. Instruction Encoding
|
| 532 |
+
scaled_vision = [img.resize((round(img.width * 28 / 32), round(img.height * 28 / 32))) for img in images]
|
| 533 |
+
processed_images = [self.image_processor.preprocess(img, img.height, img.width).unsqueeze(2) for img in images]
|
| 534 |
+
|
| 535 |
+
cfg_eligible = true_cfg_scale > 1.0 and (negative_prompt is not None or negative_prompt_embeds is not None)
|
| 536 |
+
|
| 537 |
+
prompt_embeds, prompt_embeds_mask = self.encode_prompt(
|
| 538 |
+
images=scaled_vision, prompt=prompt, prompt_embeds=prompt_embeds,
|
| 539 |
+
prompt_embeds_mask=prompt_embeds_mask, device=device,
|
| 540 |
+
num_images_per_prompt=num_images_per_prompt, max_sequence_length=max_sequence_length,
|
| 541 |
+
)
|
| 542 |
+
|
| 543 |
+
if cfg_eligible:
|
| 544 |
+
negative_prompt_embeds, negative_prompt_embeds_mask = self.encode_prompt(
|
| 545 |
+
images=scaled_vision, prompt=negative_prompt, prompt_embeds=negative_prompt_embeds,
|
| 546 |
+
prompt_embeds_mask=negative_prompt_embeds_mask, device=device,
|
| 547 |
+
num_images_per_prompt=num_images_per_prompt, max_sequence_length=max_sequence_length,
|
| 548 |
+
)
|
| 549 |
+
|
| 550 |
+
# 4. Latent Space Injection
|
| 551 |
+
v_channels = self.transformer.config.in_channels // 4
|
| 552 |
+
latents, img_latents = self.prepare_latents(
|
| 553 |
+
processed_images, batch_size * num_images_per_prompt, v_channels,
|
| 554 |
+
height, width, prompt_embeds.dtype, device, generator, latents
|
| 555 |
+
)
|
| 556 |
+
|
| 557 |
+
img_shapes = [[(1, height // padding_base, width // padding_base)] + [(1, img.shape[-2] // padding_base, img.shape[-1] // padding_base) for img in processed_images]] * batch_size
|
| 558 |
+
|
| 559 |
+
# 5. Scheduling Alignment
|
| 560 |
+
sigmas = np.linspace(1.0, 1 / num_inference_steps, num_inference_steps) if sigmas is None else sigmas
|
| 561 |
+
shift_mu = _MICoPipelineUtils.compute_temporal_shift(
|
| 562 |
+
latents.shape[1],
|
| 563 |
+
self.scheduler.config.get("base_image_seq_len", 256),
|
| 564 |
+
self.scheduler.config.get("max_image_seq_len", 4096),
|
| 565 |
+
self.scheduler.config.get("base_shift", 0.5),
|
| 566 |
+
self.scheduler.config.get("max_shift", 1.15),
|
| 567 |
+
)
|
| 568 |
+
timesteps, num_inference_steps = _MICoPipelineUtils.fetch_timesteps(self.scheduler, num_inference_steps, device, sigmas=sigmas, mu=shift_mu)
|
| 569 |
+
num_warmup_steps = max(len(timesteps) - num_inference_steps * self.scheduler.order, 0)
|
| 570 |
+
self._num_timesteps = len(timesteps)
|
| 571 |
+
|
| 572 |
+
guidance_vec = torch.full([1], guidance_scale, device=device, dtype=torch.float32).expand(latents.shape[0]) if self.transformer.config.guidance_embeds else None
|
| 573 |
+
|
| 574 |
+
txt_lens = prompt_embeds_mask.sum(dim=1).tolist() if prompt_embeds_mask is not None else None
|
| 575 |
+
neg_txt_lens = negative_prompt_embeds_mask.sum(dim=1).tolist() if negative_prompt_embeds_mask is not None else None
|
| 576 |
+
|
| 577 |
+
# 6. Target Denoising Loop execution
|
| 578 |
+
self.scheduler.set_begin_index(0)
|
| 579 |
+
with self.progress_bar(total=num_inference_steps) as progress_bar:
|
| 580 |
+
for i, t in enumerate(timesteps):
|
| 581 |
+
if self.interrupt: continue
|
| 582 |
+
self._current_timestep = t
|
| 583 |
+
t_expanded = t.expand(latents.shape[0]).to(latents.dtype)
|
| 584 |
+
combined_states = torch.cat([latents] + img_latents, dim=1)
|
| 585 |
+
|
| 586 |
+
with self.transformer.cache_context("cond"):
|
| 587 |
+
pred_noise = self.transformer(
|
| 588 |
+
hidden_states=combined_states, timestep=t_expanded / 1000, guidance=guidance_vec,
|
| 589 |
+
encoder_hidden_states_mask=prompt_embeds_mask, encoder_hidden_states=prompt_embeds,
|
| 590 |
+
img_shapes=img_shapes, txt_seq_lens=txt_lens, attention_kwargs=self.attention_kwargs, return_dict=False
|
| 591 |
+
)[0][:, : latents.size(1)]
|
| 592 |
+
|
| 593 |
+
if cfg_eligible:
|
| 594 |
+
with self.transformer.cache_context("uncond"):
|
| 595 |
+
neg_pred_noise = self.transformer(
|
| 596 |
+
hidden_states=combined_states, timestep=t_expanded / 1000, guidance=guidance_vec,
|
| 597 |
+
encoder_hidden_states_mask=negative_prompt_embeds_mask, encoder_hidden_states=negative_prompt_embeds,
|
| 598 |
+
img_shapes=img_shapes, txt_seq_lens=neg_txt_lens, attention_kwargs=self.attention_kwargs, return_dict=False
|
| 599 |
+
)[0][:, : latents.size(1)]
|
| 600 |
+
|
| 601 |
+
blended_pred = neg_pred_noise + true_cfg_scale * (pred_noise - neg_pred_noise)
|
| 602 |
+
cond_norm, blend_norm = torch.norm(pred_noise, dim=-1, keepdim=True), torch.norm(blended_pred, dim=-1, keepdim=True)
|
| 603 |
+
pred_noise = blended_pred * (cond_norm / blend_norm)
|
| 604 |
+
|
| 605 |
+
base_dtype = latents.dtype
|
| 606 |
+
latents = self.scheduler.step(pred_noise, t, latents, return_dict=False)[0]
|
| 607 |
+
|
| 608 |
+
if latents.dtype != base_dtype:
|
| 609 |
+
latents = latents.to(base_dtype) if torch.backends.mps.is_available() else latents
|
| 610 |
+
|
| 611 |
+
if callback_on_step_end:
|
| 612 |
+
cb_kwargs = {k: locals()[k] for k in callback_on_step_end_tensor_inputs}
|
| 613 |
+
cb_outputs = callback_on_step_end(self, i, t, cb_kwargs)
|
| 614 |
+
latents = cb_outputs.pop("latents", latents)
|
| 615 |
+
prompt_embeds = cb_outputs.pop("prompt_embeds", prompt_embeds)
|
| 616 |
+
|
| 617 |
+
if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
|
| 618 |
+
progress_bar.update()
|
| 619 |
+
|
| 620 |
+
if XLA_AVAILABLE: xm.mark_step()
|
| 621 |
+
|
| 622 |
+
self._current_timestep = None
|
| 623 |
+
|
| 624 |
+
# 7. Final Output Resolution
|
| 625 |
+
final_image = self._decode_latents_to_image(latents, height, width, output_type)
|
| 626 |
+
self.maybe_free_model_hooks()
|
| 627 |
+
|
| 628 |
+
return QwenImagePipelineOutput(images=final_image) if return_dict else (final_image,)
|