Skip to content

Commit eba7e7a

Browse files
fabioriganoyiyixuxusayakpaul
authored
IP-Adapter attention masking (huggingface#6847)
* Add attention masking to attn processors * Update tensor conversion --------- Co-authored-by: YiYi Xu <yixu310@gmail.com> Co-authored-by: Sayak Paul <spsayakpaul@gmail.com>
1 parent 31de879 commit eba7e7a

4 files changed

Lines changed: 322 additions & 17 deletions

File tree

docs/source/en/using-diffusers/ip_adapter.md

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,3 +468,83 @@ image
468468
<div class="flex justify-center">
469469
    <img src="https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/ipa-controlnet-out.png" />
470470
</div>
471+
472+
### IP-Adapter masking
473+
474+
Binary masks can be used to specify which portion of the output image should be assigned to an IP-Adapter.
475+
For each input IP-Adapter image, a binary mask and an IP-Adapter must be provided.
476+
477+
Before passing the masks to the pipeline, it's essential to preprocess them using [`IPAdapterMaskProcessor.preprocess()`].
478+
479+
> [!TIP]
480+
> For optimal results, provide the output height and width to [`IPAdapterMaskProcessor.preprocess()`]. This ensures that masks with differing aspect ratios are appropriately stretched. If the input masks already match the aspect ratio of the generated image, specifying height and width can be omitted.
481+
482+
Here an example with two masks:
483+
484+
```py
485+
from diffusers.image_processor import IPAdapterMaskProcessor
486+
487+
mask1 = load_image("https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/ip_mask_mask1.png")
488+
mask2 = load_image("https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/ip_mask_mask2.png")
489+
490+
output_height = 1024
491+
output_width = 1024
492+
493+
processor = IPAdapterMaskProcessor()
494+
masks = processor.preprocess([mask1, mask2], height=output_height, width=output_width)
495+
```
496+
497+
<div class="flex flex-row gap-4">
498+
<div class="flex-1">
499+
<img class="rounded-xl" src="https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/ip_mask_mask1.png"/>
500+
<figcaption class="mt-2 text-center text-sm text-gray-500">mask one</figcaption>
501+
</div>
502+
<div class="flex-1">
503+
<img class="rounded-xl" src="https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/ip_mask_mask2.png"/>
504+
<figcaption class="mt-2 text-center text-sm text-gray-500">mask two</figcaption>
505+
</div>
506+
</div>
507+
508+
If you have more than one IP-Adapter image, load them into a list, ensuring each image is assigned to a different IP-Adapter.
509+
510+
```py
511+
face_image1 = load_image("https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/ip_mask_girl1.png")
512+
face_image2 = load_image("https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/ip_mask_girl2.png")
513+
514+
ip_images =[[image1], [image2]]
515+
516+
```
517+
518+
<div class="flex flex-row gap-4">
519+
<div class="flex-1">
520+
<img class="rounded-xl" src="https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/ip_mask_girl1.png"/>
521+
<figcaption class="mt-2 text-center text-sm text-gray-500">ip adapter image one</figcaption>
522+
</div>
523+
<div class="flex-1">
524+
<img class="rounded-xl" src="https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/ip_mask_girl2.png"/>
525+
<figcaption class="mt-2 text-center text-sm text-gray-500">ip adapter image two</figcaption>
526+
</div>
527+
</div>
528+
529+
Pass preprocessed masks to the pipeline using `cross_attention_kwargs` as shown below:
530+
531+
```py
532+
533+
pipeline.load_ip_adapter("h94/IP-Adapter", subfolder="sdxl_models", weight_name=["ip-adapter-plus-face_sdxl_vit-h.safetensors"] * 2)
534+
pipeline.set_ip_adapter_scale([0.7] * 2)
535+
generator = torch.Generator(device="cpu").manual_seed(0)
536+
num_images=1
537+
538+
image = pipeline(
539+
prompt="2 girls",
540+
ip_adapter_image=ip_images,
541+
negative_prompt="monochrome, lowres, bad anatomy, worst quality, low quality",
542+
num_inference_steps=20, num_images_per_prompt=num_images,
543+
generator=generator, cross_attention_kwargs={"ip_adapter_masks": masks}
544+
).images[0]
545+
```
546+
547+
<div class="flex justify-center">
548+
    <img src="https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/ip_adapter_attention_mask_result_seed_0.png" />
549+
<figcaption class="mt-2 text-center text-sm text-gray-500">output image</figcaption>
550+
</div>

src/diffusers/image_processor.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import math
1516
import warnings
1617
from typing import List, Optional, Tuple, Union
1718

1819
import numpy as np
1920
import PIL.Image
2021
import torch
22+
import torch.nn.functional as F
2123
from PIL import Image, ImageFilter, ImageOps
2224

2325
from .configuration_utils import ConfigMixin, register_to_config
@@ -882,3 +884,107 @@ def preprocess(
882884
depth = self.binarize(depth)
883885

884886
return rgb, depth
887+
888+
889+
class IPAdapterMaskProcessor(VaeImageProcessor):
890+
"""
891+
Image processor for IP Adapter image masks.
892+
893+
Args:
894+
do_resize (`bool`, *optional*, defaults to `True`):
895+
Whether to downscale the image's (height, width) dimensions to multiples of `vae_scale_factor`.
896+
vae_scale_factor (`int`, *optional*, defaults to `8`):
897+
VAE scale factor. If `do_resize` is `True`, the image is automatically resized to multiples of this factor.
898+
resample (`str`, *optional*, defaults to `lanczos`):
899+
Resampling filter to use when resizing the image.
900+
do_normalize (`bool`, *optional*, defaults to `False`):
901+
Whether to normalize the image to [-1,1].
902+
do_binarize (`bool`, *optional*, defaults to `True`):
903+
Whether to binarize the image to 0/1.
904+
do_convert_grayscale (`bool`, *optional*, defaults to be `True`):
905+
Whether to convert the images to grayscale format.
906+
907+
"""
908+
909+
config_name = CONFIG_NAME
910+
911+
@register_to_config
912+
def __init__(
913+
self,
914+
do_resize: bool = True,
915+
vae_scale_factor: int = 8,
916+
resample: str = "lanczos",
917+
do_normalize: bool = False,
918+
do_binarize: bool = True,
919+
do_convert_grayscale: bool = True,
920+
):
921+
super().__init__(
922+
do_resize=do_resize,
923+
vae_scale_factor=vae_scale_factor,
924+
resample=resample,
925+
do_normalize=do_normalize,
926+
do_binarize=do_binarize,
927+
do_convert_grayscale=do_convert_grayscale,
928+
)
929+
930+
@staticmethod
931+
def downsample(mask: torch.FloatTensor, batch_size: int, num_queries: int, value_embed_dim: int):
932+
"""
933+
Downsamples the provided mask tensor to match the expected dimensions for scaled dot-product attention.
934+
If the aspect ratio of the mask does not match the aspect ratio of the output image, a warning is issued.
935+
936+
Args:
937+
mask (`torch.FloatTensor`):
938+
The input mask tensor generated with `IPAdapterMaskProcessor.preprocess()`.
939+
batch_size (`int`):
940+
The batch size.
941+
num_queries (`int`):
942+
The number of queries.
943+
value_embed_dim (`int`):
944+
The dimensionality of the value embeddings.
945+
946+
Returns:
947+
`torch.FloatTensor`:
948+
The downsampled mask tensor.
949+
950+
"""
951+
o_h = mask.shape[1]
952+
o_w = mask.shape[2]
953+
ratio = o_w / o_h
954+
mask_h = int(math.sqrt(num_queries / ratio))
955+
mask_h = int(mask_h) + int((num_queries % int(mask_h)) != 0)
956+
mask_w = num_queries // mask_h
957+
958+
mask_downsample = F.interpolate(mask.unsqueeze(0), size=(mask_h, mask_w), mode="bicubic").squeeze(0)
959+
960+
# Repeat batch_size times
961+
if mask_downsample.shape[0] < batch_size:
962+
mask_downsample = mask_downsample.repeat(batch_size, 1, 1)
963+
964+
mask_downsample = mask_downsample.view(mask_downsample.shape[0], -1)
965+
966+
downsampled_area = mask_h * mask_w
967+
# If the output image and the mask do not have the same aspect ratio, tensor shapes will not match
968+
# Pad tensor if downsampled_mask.shape[1] is smaller than num_queries
969+
if downsampled_area < num_queries:
970+
warnings.warn(
971+
"The aspect ratio of the mask does not match the aspect ratio of the output image. "
972+
"Please update your masks or adjust the output size for optimal performance.",
973+
UserWarning,
974+
)
975+
mask_downsample = F.pad(mask_downsample, (0, num_queries - mask_downsample.shape[1]), value=0.0)
976+
# Discard last embeddings if downsampled_mask.shape[1] is bigger than num_queries
977+
if downsampled_area > num_queries:
978+
warnings.warn(
979+
"The aspect ratio of the mask does not match the aspect ratio of the output image. "
980+
"Please update your masks or adjust the output size for optimal performance.",
981+
UserWarning,
982+
)
983+
mask_downsample = mask_downsample[:, :num_queries]
984+
985+
# Repeat last dimension to match SDPA output shape
986+
mask_downsample = mask_downsample.view(mask_downsample.shape[0], mask_downsample.shape[1], 1).repeat(
987+
1, 1, value_embed_dim
988+
)
989+
990+
return mask_downsample

src/diffusers/models/attention_processor.py

Lines changed: 63 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import torch.nn.functional as F
2020
from torch import nn
2121

22+
from ..image_processor import IPAdapterMaskProcessor
2223
from ..utils import USE_PEFT_BACKEND, deprecate, logging
2324
from ..utils.import_utils import is_xformers_available
2425
from ..utils.torch_utils import maybe_allow_in_graph
@@ -2107,12 +2108,13 @@ def __init__(self, hidden_size, cross_attention_dim=None, num_tokens=(4,), scale
21072108

21082109
def __call__(
21092110
self,
2110-
attn,
2111-
hidden_states,
2112-
encoder_hidden_states=None,
2113-
attention_mask=None,
2114-
temb=None,
2115-
scale=1.0,
2111+
attn: Attention,
2112+
hidden_states: torch.FloatTensor,
2113+
encoder_hidden_states: Optional[torch.FloatTensor] = None,
2114+
attention_mask: Optional[torch.FloatTensor] = None,
2115+
temb: Optional[torch.FloatTensor] = None,
2116+
scale: float = 1.0,
2117+
ip_adapter_masks: Optional[torch.FloatTensor] = None,
21162118
):
21172119
residual = hidden_states
21182120

@@ -2167,9 +2169,22 @@ def __call__(
21672169
hidden_states = torch.bmm(attention_probs, value)
21682170
hidden_states = attn.batch_to_head_dim(hidden_states)
21692171

2172+
if ip_adapter_masks is not None:
2173+
if not isinstance(ip_adapter_masks, torch.Tensor) or ip_adapter_masks.ndim != 4:
2174+
raise ValueError(
2175+
" ip_adapter_mask should be a tensor with shape [num_ip_adapter, 1, height, width]."
2176+
" Please use `IPAdapterMaskProcessor` to preprocess your mask"
2177+
)
2178+
if len(ip_adapter_masks) != len(self.scale):
2179+
raise ValueError(
2180+
f"Number of ip_adapter_masks ({len(ip_adapter_masks)}) must match number of IP-Adapters ({len(self.scale)})"
2181+
)
2182+
else:
2183+
ip_adapter_masks = [None] * len(self.scale)
2184+
21702185
# for ip-adapter
2171-
for current_ip_hidden_states, scale, to_k_ip, to_v_ip in zip(
2172-
ip_hidden_states, self.scale, self.to_k_ip, self.to_v_ip
2186+
for current_ip_hidden_states, scale, to_k_ip, to_v_ip, mask in zip(
2187+
ip_hidden_states, self.scale, self.to_k_ip, self.to_v_ip, ip_adapter_masks
21732188
):
21742189
ip_key = to_k_ip(current_ip_hidden_states)
21752190
ip_value = to_v_ip(current_ip_hidden_states)
@@ -2181,6 +2196,15 @@ def __call__(
21812196
current_ip_hidden_states = torch.bmm(ip_attention_probs, ip_value)
21822197
current_ip_hidden_states = attn.batch_to_head_dim(current_ip_hidden_states)
21832198

2199+
if mask is not None:
2200+
mask_downsample = IPAdapterMaskProcessor.downsample(
2201+
mask, batch_size, current_ip_hidden_states.shape[1], current_ip_hidden_states.shape[2]
2202+
)
2203+
2204+
mask_downsample = mask_downsample.to(dtype=query.dtype, device=query.device)
2205+
2206+
current_ip_hidden_states = current_ip_hidden_states * mask_downsample
2207+
21842208
hidden_states = hidden_states + scale * current_ip_hidden_states
21852209

21862210
# linear proj
@@ -2244,12 +2268,13 @@ def __init__(self, hidden_size, cross_attention_dim=None, num_tokens=(4,), scale
22442268

22452269
def __call__(
22462270
self,
2247-
attn,
2248-
hidden_states,
2249-
encoder_hidden_states=None,
2250-
attention_mask=None,
2251-
temb=None,
2252-
scale=1.0,
2271+
attn: Attention,
2272+
hidden_states: torch.FloatTensor,
2273+
encoder_hidden_states: Optional[torch.FloatTensor] = None,
2274+
attention_mask: Optional[torch.FloatTensor] = None,
2275+
temb: Optional[torch.FloatTensor] = None,
2276+
scale: float = 1.0,
2277+
ip_adapter_masks: Optional[torch.FloatTensor] = None,
22532278
):
22542279
residual = hidden_states
22552280

@@ -2318,9 +2343,22 @@ def __call__(
23182343
hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, attn.heads * head_dim)
23192344
hidden_states = hidden_states.to(query.dtype)
23202345

2346+
if ip_adapter_masks is not None:
2347+
if not isinstance(ip_adapter_masks, torch.Tensor) or ip_adapter_masks.ndim != 4:
2348+
raise ValueError(
2349+
" ip_adapter_mask should be a tensor with shape [num_ip_adapter, 1, height, width]."
2350+
" Please use `IPAdapterMaskProcessor` to preprocess your mask"
2351+
)
2352+
if len(ip_adapter_masks) != len(self.scale):
2353+
raise ValueError(
2354+
f"Number of ip_adapter_masks ({len(ip_adapter_masks)}) must match number of IP-Adapters ({len(self.scale)})"
2355+
)
2356+
else:
2357+
ip_adapter_masks = [None] * len(self.scale)
2358+
23212359
# for ip-adapter
2322-
for current_ip_hidden_states, scale, to_k_ip, to_v_ip in zip(
2323-
ip_hidden_states, self.scale, self.to_k_ip, self.to_v_ip
2360+
for current_ip_hidden_states, scale, to_k_ip, to_v_ip, mask in zip(
2361+
ip_hidden_states, self.scale, self.to_k_ip, self.to_v_ip, ip_adapter_masks
23242362
):
23252363
ip_key = to_k_ip(current_ip_hidden_states)
23262364
ip_value = to_v_ip(current_ip_hidden_states)
@@ -2339,6 +2377,15 @@ def __call__(
23392377
)
23402378
current_ip_hidden_states = current_ip_hidden_states.to(query.dtype)
23412379

2380+
if mask is not None:
2381+
mask_downsample = IPAdapterMaskProcessor.downsample(
2382+
mask, batch_size, current_ip_hidden_states.shape[1], current_ip_hidden_states.shape[2]
2383+
)
2384+
2385+
mask_downsample = mask_downsample.to(dtype=query.dtype, device=query.device)
2386+
2387+
current_ip_hidden_states = current_ip_hidden_states * mask_downsample
2388+
23422389
hidden_states = hidden_states + scale * current_ip_hidden_states
23432390

23442391
# linear proj

0 commit comments

Comments
 (0)