BiliSakura commited on
Commit
77c266c
·
verified ·
1 Parent(s): f103c11

Add files using upload-large-folder tool

Browse files
Files changed (36) hide show
  1. .gitattributes +2 -0
  2. README.md +192 -0
  3. demo/ir/demo.png +0 -0
  4. demo/opt/demo.png +0 -0
  5. demo/sar/demo.png +0 -0
  6. loras/ir/beach/pytorch_lora_weights.safetensors +3 -0
  7. loras/ir/bridge/pytorch_lora_weights.safetensors +3 -0
  8. loras/ir/desert/pytorch_lora_weights.safetensors +3 -0
  9. loras/ir/farmland/pytorch_lora_weights.safetensors +3 -0
  10. loras/ir/lake/pytorch_lora_weights.safetensors +3 -0
  11. loras/ir/mountain/pytorch_lora_weights.safetensors +3 -0
  12. loras/ir/residential/pytorch_lora_weights.safetensors +3 -0
  13. loras/ir/river/pytorch_lora_weights.safetensors +3 -0
  14. loras/ir/ship/pytorch_lora_weights.safetensors +3 -0
  15. loras/sar/beach/pytorch_lora_weights.safetensors +3 -0
  16. loras/sar/bridge/pytorch_lora_weights.safetensors +3 -0
  17. loras/sar/desert/pytorch_lora_weights.safetensors +3 -0
  18. loras/sar/farmland/pytorch_lora_weights.safetensors +3 -0
  19. loras/sar/lake/pytorch_lora_weights.safetensors +3 -0
  20. loras/sar/mountain/pytorch_lora_weights.safetensors +3 -0
  21. loras/sar/residential/pytorch_lora_weights.safetensors +3 -0
  22. loras/sar/river/pytorch_lora_weights.safetensors +3 -0
  23. loras/sar/ship/pytorch_lora_weights.safetensors +3 -0
  24. model_index.json +38 -0
  25. pipeline.py +1110 -0
  26. scheduler/scheduler_config.json +15 -0
  27. text_encoder/config.json +25 -0
  28. text_encoder/model.safetensors +3 -0
  29. tokenizer/merges.txt +0 -0
  30. tokenizer/special_tokens_map.json +24 -0
  31. tokenizer/tokenizer_config.json +30 -0
  32. tokenizer/vocab.json +0 -0
  33. unet/config.json +68 -0
  34. unet/diffusion_pytorch_model.safetensors +3 -0
  35. vae/config.json +38 -0
  36. vae/diffusion_pytorch_model.safetensors +3 -0
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ **/demo.png filter=lfs diff=lfs merge=lfs -text
37
+ *.png filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,192 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: diffusers
4
+ pipeline_tag: text-to-image
5
+ base_model: CompVis/stable-diffusion-v1-4
6
+ tags:
7
+ - diffusers
8
+ - remote-sensing
9
+ - optical
10
+ - sar
11
+ - infrared
12
+ - multimodal
13
+ - text-to-image
14
+ - lora
15
+ - stable-diffusion
16
+ inference: true
17
+ language:
18
+ - en
19
+ widget:
20
+ - text: There is a ship in the blue water on the shore.
21
+ output:
22
+ url: demo/opt/demo.png
23
+ - text: There is a ship in the blue water on the shore.
24
+ output:
25
+ url: demo/sar/demo.png
26
+ - text: There is a ship in the blue water on the shore.
27
+ output:
28
+ url: demo/ir/demo.png
29
+ ---
30
+
31
+ # BiliSakura/MMDiff-diffusers
32
+
33
+ Self-contained [Diffusers](https://github.com/huggingface/diffusers) checkpoint for **MMDiff**: text-driven generation of spatially consistent **optical (OPT)**, **synthetic aperture radar (SAR)**, and **infrared (IR)** remote-sensing images from a single prompt.
34
+
35
+ Converted from [`XinRan-Tang/MM-Diff`](https://huggingface.co/XinRan-Tang/MM-Diff) with the native custom pipeline in [MMDiff-diffusers](https://github.com/Bili-Sakura/MMDiff-diffusers). The optical UNet is a Stable Diffusion v1.4 backbone fine-tuned on optical image–text pairs; SAR and IR style are applied with scene LoRA adapters plus in-memory spatial feature transfer (no disk dumps under `features/`).
36
+
37
+ Paper: [MMDiff: Multi-modal remote sensing image generation via cross-modality spatial feature transfer](https://doi.org/10.1016/j.isprsjprs.2026.08.018) (ISPRS Journal of Photogrammetry and Remote Sensing, 2026).
38
+
39
+ ## Demo
40
+
41
+ Prompt: *"There is a ship in the blue water on the shore."* — scene `ship`, 256×256, 50 DDPM steps, `guidance_scale=7.5`, seed `2026`.
42
+
43
+ | Optical (OPT) | SAR | Infrared (IR) |
44
+ | --- | --- | --- |
45
+ | ![OPT](demo/opt/demo.png) | ![SAR](demo/sar/demo.png) | ![IR](demo/ir/demo.png) |
46
+
47
+ ## Model details
48
+
49
+ | Field | Value |
50
+ | --- | --- |
51
+ | Pipeline class | `MMDiffPipeline` (`pipeline.py`) |
52
+ | Backbone | Stable Diffusion v1.4 (`UNet2DConditionModel` + `AutoencoderKL` + CLIP ViT-L/14) |
53
+ | Scheduler | `DDPMScheduler`, 1000 training steps, `scaled_linear`, `prediction_type=epsilon` |
54
+ | Native resolution | 256×256 |
55
+ | Latent channels | 4 (VAE `scaling_factor=0.18215`) |
56
+ | SAR / IR adapters | PEFT LoRA under `loras/{sar,ir}/<scene>/` |
57
+ | Safety checker | Disabled (remote-sensing imagery) |
58
+ | License | Apache-2.0 |
59
+ | Training data | [`XinRan-Tang/Optical-SAR-Infrared`](https://huggingface.co/datasets/XinRan-Tang/Optical-SAR-Infrared) |
60
+
61
+ ### Scene LoRAs
62
+
63
+ `beach`, `bridge`, `desert`, `farmland`, `lake`, `mountain`, `residential`, `river`, `ship`.
64
+
65
+ SAR and IR images are decoded as single-channel (grayscale) to match the original sampling path. Optical images remain RGB.
66
+
67
+ ## Repo layout
68
+
69
+ ```text
70
+ BiliSakura/MMDiff-diffusers/
71
+ ├── README.md
72
+ ├── pipeline.py
73
+ ├── model_index.json
74
+ ├── demo/
75
+ │ ├── opt/demo.png
76
+ │ ├── sar/demo.png
77
+ │ └── ir/demo.png
78
+ ├── unet/
79
+ ├── vae/
80
+ ├── text_encoder/
81
+ ├── tokenizer/
82
+ ├── scheduler/
83
+ └── loras/
84
+ ├── sar/<scene>/pytorch_lora_weights.safetensors
85
+ └── ir/<scene>/pytorch_lora_weights.safetensors
86
+ ```
87
+
88
+ This folder is a complete Diffusers repo: load the **root**, not a nested variant.
89
+
90
+ ## Load from Hugging Face
91
+
92
+ ```python
93
+ import torch
94
+ from diffusers import DiffusionPipeline
95
+
96
+ pipe = DiffusionPipeline.from_pretrained(
97
+ "BiliSakura/MMDiff-diffusers",
98
+ trust_remote_code=True,
99
+ torch_dtype=torch.bfloat16,
100
+ ).to("cuda")
101
+
102
+ generator = torch.Generator(device="cpu").manual_seed(2026)
103
+ output = pipe(
104
+ "There is a ship in the blue water on the shore.",
105
+ scene="ship",
106
+ height=256,
107
+ width=256,
108
+ num_inference_steps=50,
109
+ guidance_scale=7.5,
110
+ generator=generator,
111
+ )
112
+ output.opt[0].save("opt.png")
113
+ output.sar[0].save("sar.png")
114
+ output.ir[0].save("ir.png")
115
+ ```
116
+
117
+ ## Load from a local clone
118
+
119
+ ```python
120
+ from pathlib import Path
121
+ import torch
122
+ from diffusers import DiffusionPipeline
123
+
124
+ model_dir = Path("./MMDiff-diffusers").resolve()
125
+ pipe = DiffusionPipeline.from_pretrained(
126
+ str(model_dir),
127
+ local_files_only=True,
128
+ custom_pipeline=str(model_dir / "pipeline.py"),
129
+ trust_remote_code=True,
130
+ torch_dtype=torch.bfloat16,
131
+ ).to("cuda")
132
+
133
+ generator = torch.Generator(device="cpu").manual_seed(2026)
134
+ output = pipe(
135
+ "There is a ship in the blue water on the shore.",
136
+ scene="ship",
137
+ modalities="all", # or ["opt", "sar", "ir"]
138
+ height=256,
139
+ width=256,
140
+ num_inference_steps=50,
141
+ guidance_scale=7.5,
142
+ generator=generator,
143
+ )
144
+ output.opt[0].save("demo/opt/demo.png")
145
+ output.sar[0].save("demo/sar/demo.png")
146
+ output.ir[0].save("demo/ir/demo.png")
147
+ ```
148
+
149
+ ## Recommended inference settings
150
+
151
+ | Setting | Value |
152
+ | --- | --- |
153
+ | Resolution | 256×256 |
154
+ | Steps | 50 |
155
+ | CFG (`guidance_scale`) | 7.5 |
156
+ | `torch_dtype` | `bfloat16` |
157
+ | Scheduler | `DDPMScheduler` (shipped) |
158
+ | Spatial transfer | attention layers `1..9`, ResNet layer `2`, `resnet_time=1.0` |
159
+
160
+ `modalities` accepts `"all"` or any subset of `"opt"`, `"sar"`, `"ir"`. SAR/IR generation requires OPT spatial features; the pipeline runs OPT first when they are not supplied via `spatial_features`.
161
+
162
+ Dependencies: `diffusers`, `transformers`, `accelerate`, `peft`, `safetensors`, `torch`, `pillow`.
163
+
164
+ ## Interface notes
165
+
166
+ - `output.images` is the first requested modality (Stable Diffusion convention); `output.opt` / `output.sar` / `output.ir` hold per-modality PIL lists.
167
+ - `scene` selects packaged SAR/IR LoRAs. Override with `sar_lora_path` / `ir_lora_path` if needed.
168
+ - Pass `return_spatial_features=True` to reuse captured OPT features in a later call.
169
+
170
+ ## Intended use
171
+
172
+ Research on text-driven multi-modal remote-sensing generation, cross-modality spatial transfer, multi-modal fusion, and downstream MMRS data augmentation. Not intended as a general-purpose photorealistic image generator.
173
+
174
+ ## Links
175
+
176
+ - Paper: [ISPRS Journal of Photogrammetry and Remote Sensing](https://www.sciencedirect.com/science/article/pii/S0924271626004089)
177
+ - Homepage: [MMDiff](https://xinr-tang.github.io/MMDiff-homepage/)
178
+ - Upstream weights: [`XinRan-Tang/MM-Diff`](https://huggingface.co/XinRan-Tang/MM-Diff)
179
+ - Dataset: [`XinRan-Tang/Optical-SAR-Infrared`](https://huggingface.co/datasets/XinRan-Tang/Optical-SAR-Infrared)
180
+ - Conversion / pipeline: [MMDiff-diffusers](https://github.com/Bili-Sakura/MMDiff-diffusers)
181
+
182
+ ## Citation
183
+
184
+ ```bibtex
185
+ @article{tang2026mmdiff,
186
+ title = {MMDiff: Multi-modal remote sensing image generation via cross-modality spatial feature transfer},
187
+ author = {Tang, Haojun and Zhao, Wenda and Cui, Hengshuai and Wang, Haipeng},
188
+ journal = {ISPRS Journal of Photogrammetry and Remote Sensing},
189
+ year = {2026},
190
+ doi = {10.1016/j.isprsjprs.2026.08.018}
191
+ }
192
+ ```
demo/ir/demo.png ADDED
demo/opt/demo.png ADDED
demo/sar/demo.png ADDED
loras/ir/beach/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:56a9c883d4e45f47c1c17cb5500c8ca36ae708acba816f9eb2626edda5bd6706
3
+ size 12792952
loras/ir/bridge/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3fb21c829971fc49a60eb7fd0cb6a8ae742cec3560fd1866556b362d0a7839e6
3
+ size 12792952
loras/ir/desert/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f53c30b3ae177d5afba9e46c566b9465d7d4647ebcb08d0387c6f4c10f39738e
3
+ size 12792952
loras/ir/farmland/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:aa818fecaef9f1cf6a0533e5696e450ea568563d158a9231eeaa2381d86ac716
3
+ size 12792952
loras/ir/lake/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:01cb83acbc97d43da35e137ea4bd570d19986a2486405e2ec0baa7b7b0903606
3
+ size 12792952
loras/ir/mountain/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9830c946dcb6d60411b2b6f2cf3ba4c93e8cd740406f9a425e328b62d72c9fc0
3
+ size 12792952
loras/ir/residential/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0cff96decb990fd6214048b9a508894bd30c6450bed41db6c509d9c0f521fb5f
3
+ size 12792952
loras/ir/river/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:71662b5d960e3ebf79e16831aad00bbe9b173ce7fc9f8c3179c3f89e94465a88
3
+ size 12792952
loras/ir/ship/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bdf52809a2baf4b250a18444b0031c51af4571b65e8eaa2c808d449ffcf9ad6d
3
+ size 12792952
loras/sar/beach/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:45e4efa97ec57352adfa818f25ee043ee2917b8921626614dfa83c4d0221e361
3
+ size 12792952
loras/sar/bridge/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f6b9bf4ddd42d62736f9b5db86201b35d9b44455a2e3af99ddf092d3727f071c
3
+ size 12792952
loras/sar/desert/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c2db5fcc87fb44db78f77961d426a7e87d206113455f58f9afd62a8af1e6c806
3
+ size 6414992
loras/sar/farmland/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c22d077d9e76332e2c2d6188647f6dd46c3a4732544c1ce30d726086dcd626b9
3
+ size 12792952
loras/sar/lake/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8641aad4a8718b56e2407fef16a98e13f1d5f034ce0bdf4cbb04d557f954cca7
3
+ size 12792952
loras/sar/mountain/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f49b61257705c01ff1a6013a486e4ded12a12eccee34086022f43ab5711ff3b1
3
+ size 12792952
loras/sar/residential/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c8669346f20ecfa8477f7c22a785df591d806ff9914dcddf6c595a39a3aef00f
3
+ size 12792952
loras/sar/river/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:146b839a06a518568619e07177336cf4030c76efdbb00f80f11f44f711867bb3
3
+ size 12792952
loras/sar/ship/pytorch_lora_weights.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:75c23a9f94465aec223ddb4289c03b8188c3e8b30696c6cacff17d596108aa69
3
+ size 12792952
model_index.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "MMDiffPipeline",
3
+ "_diffusers_version": "0.32.0",
4
+ "feature_extractor": [
5
+ null,
6
+ null
7
+ ],
8
+ "image_encoder": [
9
+ null,
10
+ null
11
+ ],
12
+ "lora_root": "loras",
13
+ "requires_safety_checker": false,
14
+ "safety_checker": [
15
+ null,
16
+ null
17
+ ],
18
+ "scheduler": [
19
+ "diffusers",
20
+ "DDPMScheduler"
21
+ ],
22
+ "text_encoder": [
23
+ "transformers",
24
+ "CLIPTextModel"
25
+ ],
26
+ "tokenizer": [
27
+ "transformers",
28
+ "CLIPTokenizer"
29
+ ],
30
+ "unet": [
31
+ "diffusers",
32
+ "UNet2DConditionModel"
33
+ ],
34
+ "vae": [
35
+ "diffusers",
36
+ "AutoencoderKL"
37
+ ]
38
+ }
pipeline.py ADDED
@@ -0,0 +1,1110 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2026 The HuggingFace Team. All rights reserved.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Native Diffusers pipeline for MMDiff multi-modal remote-sensing generation."""
16
+
17
+ from __future__ import annotations
18
+
19
+ from contextlib import contextmanager
20
+ from dataclasses import dataclass
21
+ from pathlib import Path
22
+ from typing import Any, Callable
23
+
24
+ import torch
25
+ from transformers import CLIPImageProcessor, CLIPTextModel, CLIPTokenizer, CLIPVisionModelWithProjection
26
+
27
+ from diffusers.image_processor import PipelineImageInput
28
+ from diffusers.loaders import FromSingleFileMixin, IPAdapterMixin, StableDiffusionLoraLoaderMixin, TextualInversionLoaderMixin
29
+ from diffusers.models import AutoencoderKL, UNet2DConditionModel
30
+ from diffusers.models.attention_processor import Attention, AttnProcessor
31
+ from diffusers.pipelines.pipeline_utils import DiffusionPipeline
32
+ from diffusers.pipelines.stable_diffusion.pipeline_output import StableDiffusionPipelineOutput
33
+ from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import (
34
+ StableDiffusionPipeline,
35
+ rescale_noise_cfg,
36
+ retrieve_timesteps,
37
+ )
38
+ from diffusers.pipelines.stable_diffusion.safety_checker import StableDiffusionSafetyChecker
39
+ from diffusers.schedulers import KarrasDiffusionSchedulers
40
+ from diffusers.utils import BaseOutput, logging, replace_example_docstring
41
+
42
+ logger = logging.get_logger(__name__)
43
+
44
+ DEFAULT_RESOLUTION = 256
45
+ DEFAULT_SCENE = "ship"
46
+ DEFAULT_ATTN_LAYERS = (1, 2, 3, 4, 5, 6, 7, 8, 9)
47
+ DEFAULT_RESNET_LAYERS = (2,)
48
+ SUPPORTED_MODALITIES = ("opt", "sar", "ir")
49
+ SUPPORTED_SCENES = (
50
+ "beach",
51
+ "bridge",
52
+ "desert",
53
+ "farmland",
54
+ "lake",
55
+ "mountain",
56
+ "residential",
57
+ "river",
58
+ "ship",
59
+ )
60
+
61
+ EXAMPLE_DOC_STRING = """
62
+ Examples:
63
+ ```py
64
+ >>> from pathlib import Path
65
+ >>> import torch
66
+ >>> from diffusers import DiffusionPipeline
67
+
68
+ >>> model_dir = Path("/path/to/mmdiff-diffusers")
69
+ >>> pipe = DiffusionPipeline.from_pretrained(
70
+ ... str(model_dir),
71
+ ... local_files_only=True,
72
+ ... custom_pipeline=str(model_dir / "pipeline.py"),
73
+ ... trust_remote_code=True,
74
+ ... torch_dtype=torch.bfloat16,
75
+ ... )
76
+ >>> pipe = pipe.to("cuda")
77
+
78
+ >>> generator = torch.Generator(device="cpu").manual_seed(2026)
79
+ >>> output = pipe(
80
+ ... "There is a ship in the blue water on the shore.",
81
+ ... scene="ship",
82
+ ... height=256,
83
+ ... width=256,
84
+ ... num_inference_steps=50,
85
+ ... generator=generator,
86
+ ... )
87
+ >>> output.opt[0].save("opt.png")
88
+ >>> output.sar[0].save("sar.png")
89
+ >>> output.ir[0].save("ir.png")
90
+
91
+ >>> # Hugging Face Hub style model id: UserID/RepoID
92
+ >>> # Example: "XinRan-Tang/MM-Diff" after conversion, or a packaged `mmdiff-diffusers` repo.
93
+ ```
94
+ """
95
+
96
+
97
+ def collect_up_self_attentions(unet: UNet2DConditionModel) -> list[Attention]:
98
+ r"""
99
+ Collect up-block self-attention modules in the same depth-first order used by the
100
+ original MMDiff hook registration.
101
+
102
+ Args:
103
+ unet (`UNet2DConditionModel`):
104
+ UNet whose `up_blocks` should be scanned.
105
+
106
+ Returns:
107
+ `list[Attention]`: Self-attention modules in 1-based transfer order.
108
+ """
109
+
110
+ modules: list[Attention] = []
111
+
112
+ def _recurse(module: torch.nn.Module) -> None:
113
+ if module.__class__.__name__ == "Attention":
114
+ if module.to_q.in_features == module.to_k.in_features:
115
+ modules.append(module)
116
+ return
117
+ for child in module.children():
118
+ _recurse(child)
119
+
120
+ _recurse(unet.up_blocks)
121
+ return modules
122
+
123
+
124
+ def collect_up_resnets(unet: UNet2DConditionModel) -> list[torch.nn.Module]:
125
+ r"""
126
+ Collect up-block `ResnetBlock2D` modules in the original 0-based transfer order.
127
+
128
+ Args:
129
+ unet (`UNet2DConditionModel`):
130
+ UNet whose `up_blocks` should be scanned.
131
+
132
+ Returns:
133
+ `list[torch.nn.Module]`: Residual blocks in injection-index order.
134
+ """
135
+
136
+ modules: list[torch.nn.Module] = []
137
+
138
+ def _recurse(module: torch.nn.Module) -> None:
139
+ if module.__class__.__name__ == "ResnetBlock2D":
140
+ modules.append(module)
141
+ return
142
+ for child in module.children():
143
+ _recurse(child)
144
+
145
+ _recurse(unet.up_blocks)
146
+ return modules
147
+
148
+
149
+ class SpatialFeatureStore:
150
+ r"""
151
+ In-memory store for OPT self-attention queries and up-block residual features.
152
+
153
+ Features are keyed by integer scheduler timestep, then by layer index. This replaces
154
+ the original disk dump under `features/visible_attn_maps` and `features/visible_resnet_maps`.
155
+ """
156
+
157
+ def __init__(self) -> None:
158
+ self.attn: dict[int, dict[int, torch.Tensor]] = {}
159
+ self.resnet: dict[int, dict[int, torch.Tensor]] = {}
160
+ self.current_timestep: int | None = None
161
+ self.mode: str = "off"
162
+ self.attn_layers: set[int] = set(DEFAULT_ATTN_LAYERS)
163
+ self.resnet_layers: set[int] = set(DEFAULT_RESNET_LAYERS)
164
+ self.inject_attn_timesteps: set[int] | None = None
165
+ self.inject_resnet_timesteps: set[int] | None = None
166
+
167
+ def reset(self) -> None:
168
+ r"""Clear captured features and timestep state without changing layer settings."""
169
+
170
+ self.attn = {}
171
+ self.resnet = {}
172
+ self.current_timestep = None
173
+
174
+ def set_timestep(self, timestep: int | torch.Tensor) -> None:
175
+ r"""
176
+ Record the scheduler timestep used by the current UNet forward.
177
+
178
+ Args:
179
+ timestep (`int` or `torch.Tensor`):
180
+ Scalar diffusion timestep. Tensors are stored as `int`.
181
+ """
182
+
183
+ self.current_timestep = int(timestep)
184
+
185
+ def _timestep_allowed(self, allowed: set[int] | None) -> bool:
186
+ if self.current_timestep is None:
187
+ return False
188
+ if allowed is None:
189
+ return True
190
+ return self.current_timestep in allowed
191
+
192
+ def save_attn(self, layer_idx: int, query: torch.Tensor) -> None:
193
+ r"""
194
+ Cache a self-attention query tensor for the current timestep.
195
+
196
+ Args:
197
+ layer_idx (`int`):
198
+ 1-based up-block self-attention index.
199
+ query (`torch.Tensor`):
200
+ Query tensor after `head_to_batch_dim`.
201
+ """
202
+
203
+ if self.mode != "save" or self.current_timestep is None:
204
+ return
205
+ if layer_idx not in self.attn_layers:
206
+ return
207
+ self.attn.setdefault(self.current_timestep, {})[layer_idx] = query.detach()
208
+
209
+ def get_attn(self, layer_idx: int) -> torch.Tensor | None:
210
+ r"""
211
+ Return the cached query for the current timestep and layer, if injection is active.
212
+
213
+ Args:
214
+ layer_idx (`int`):
215
+ 1-based up-block self-attention index.
216
+
217
+ Returns:
218
+ `torch.Tensor` or `None`: Cached query, or `None` when injection does not apply.
219
+ """
220
+
221
+ if self.mode != "inject" or not self._timestep_allowed(self.inject_attn_timesteps):
222
+ return None
223
+ if layer_idx not in self.attn_layers:
224
+ return None
225
+ return self.attn.get(self.current_timestep, {}).get(layer_idx)
226
+
227
+ def save_resnet(self, layer_idx: int, residual: torch.Tensor) -> None:
228
+ r"""
229
+ Cache an up-block residual tensor for the current timestep.
230
+
231
+ Args:
232
+ layer_idx (`int`):
233
+ 0-based up-block ResNet index.
234
+ residual (`torch.Tensor`):
235
+ `ResnetBlock2D` output.
236
+ """
237
+
238
+ if self.mode != "save" or self.current_timestep is None:
239
+ return
240
+ if layer_idx not in self.resnet_layers:
241
+ return
242
+ self.resnet.setdefault(self.current_timestep, {})[layer_idx] = residual.detach()
243
+
244
+ def get_resnet(self, layer_idx: int) -> torch.Tensor | None:
245
+ r"""
246
+ Return the cached residual for the current timestep and layer, if injection is active.
247
+
248
+ Args:
249
+ layer_idx (`int`):
250
+ 0-based up-block ResNet index.
251
+
252
+ Returns:
253
+ `torch.Tensor` or `None`: Cached residual, or `None` when injection does not apply.
254
+ """
255
+
256
+ if self.mode != "inject" or not self._timestep_allowed(self.inject_resnet_timesteps):
257
+ return None
258
+ if layer_idx not in self.resnet_layers:
259
+ return None
260
+ return self.resnet.get(self.current_timestep, {}).get(layer_idx)
261
+
262
+ def to_state(self) -> dict[str, Any]:
263
+ r"""
264
+ Export captured features for reuse in a later `__call__`.
265
+
266
+ Returns:
267
+ `dict`: Detached CPU tensors plus layer configuration.
268
+ """
269
+
270
+ def _cpu(store: dict[int, dict[int, torch.Tensor]]) -> dict[int, dict[int, torch.Tensor]]:
271
+ return {
272
+ timestep: {layer: tensor.detach().cpu() for layer, tensor in layers.items()}
273
+ for timestep, layers in store.items()
274
+ }
275
+
276
+ return {
277
+ "attn": _cpu(self.attn),
278
+ "resnet": _cpu(self.resnet),
279
+ "attn_layers": sorted(self.attn_layers),
280
+ "resnet_layers": sorted(self.resnet_layers),
281
+ }
282
+
283
+ def load_state(self, state: dict[str, Any]) -> None:
284
+ r"""
285
+ Restore features previously returned by `to_state`.
286
+
287
+ Args:
288
+ state (`dict`):
289
+ Mapping produced by `to_state`.
290
+ """
291
+
292
+ if not isinstance(state, dict) or "attn" not in state:
293
+ raise ValueError("`spatial_features` must be a dict created by MMDiffPipeline.")
294
+ self.attn = {int(t): {int(i): v for i, v in layers.items()} for t, layers in state["attn"].items()}
295
+ self.resnet = {
296
+ int(t): {int(i): v for i, v in layers.items()} for t, layers in state.get("resnet", {}).items()
297
+ }
298
+ if "attn_layers" in state:
299
+ self.attn_layers = set(int(i) for i in state["attn_layers"])
300
+ if "resnet_layers" in state:
301
+ self.resnet_layers = set(int(i) for i in state["resnet_layers"])
302
+
303
+
304
+ class MMDiffAttnProcessor(AttnProcessor):
305
+ r"""
306
+ Attention processor that records or replaces self-attention queries during spatial transfer.
307
+
308
+ Args:
309
+ layer_idx (`int`):
310
+ 1-based up-block self-attention index.
311
+ store (`SpatialFeatureStore`):
312
+ Shared feature store used by the current denoising loop.
313
+ """
314
+
315
+ def __init__(self, layer_idx: int, store: SpatialFeatureStore) -> None:
316
+ super().__init__()
317
+ self.layer_idx = layer_idx
318
+ self.store = store
319
+
320
+ def __call__(
321
+ self,
322
+ attn: Attention,
323
+ hidden_states: torch.Tensor,
324
+ encoder_hidden_states: torch.Tensor | None = None,
325
+ attention_mask: torch.Tensor | None = None,
326
+ temb: torch.Tensor | None = None,
327
+ *args: Any,
328
+ **kwargs: Any,
329
+ ) -> torch.Tensor:
330
+ residual = hidden_states
331
+ if attn.spatial_norm is not None:
332
+ hidden_states = attn.spatial_norm(hidden_states, temb)
333
+
334
+ input_ndim = hidden_states.ndim
335
+ if input_ndim == 4:
336
+ batch_size, channel, height, width = hidden_states.shape
337
+ hidden_states = hidden_states.view(batch_size, channel, height * width).transpose(1, 2)
338
+
339
+ batch_size, sequence_length, _ = (
340
+ hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape
341
+ )
342
+ attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size)
343
+
344
+ if attn.group_norm is not None:
345
+ hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2)
346
+
347
+ query = attn.to_q(hidden_states)
348
+ if encoder_hidden_states is None:
349
+ encoder_hidden_states = hidden_states
350
+ elif attn.norm_cross:
351
+ encoder_hidden_states = attn.norm_encoder_hidden_states(encoder_hidden_states)
352
+
353
+ key = attn.to_k(encoder_hidden_states)
354
+ value = attn.to_v(encoder_hidden_states)
355
+
356
+ query = attn.head_to_batch_dim(query)
357
+ key = attn.head_to_batch_dim(key)
358
+ value = attn.head_to_batch_dim(value)
359
+
360
+ injected = self.store.get_attn(self.layer_idx)
361
+ if injected is not None:
362
+ query = injected.to(device=query.device, dtype=query.dtype)
363
+
364
+ attention_probs = attn.get_attention_scores(query, key, attention_mask)
365
+ hidden_states = torch.bmm(attention_probs, value)
366
+ hidden_states = attn.batch_to_head_dim(hidden_states)
367
+ hidden_states = attn.to_out[0](hidden_states)
368
+ hidden_states = attn.to_out[1](hidden_states)
369
+
370
+ if input_ndim == 4:
371
+ hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width)
372
+
373
+ self.store.save_attn(self.layer_idx, query)
374
+
375
+ if attn.residual_connection:
376
+ hidden_states = hidden_states + residual
377
+ hidden_states = hidden_states / attn.rescale_output_factor
378
+ return hidden_states
379
+
380
+
381
+ @contextmanager
382
+ def spatial_transfer_hooks(unet: UNet2DConditionModel, store: SpatialFeatureStore):
383
+ r"""
384
+ Install native attention processors and ResNet wrappers for one denoising run.
385
+
386
+ Args:
387
+ unet (`UNet2DConditionModel`):
388
+ UNet to instrument.
389
+ store (`SpatialFeatureStore`):
390
+ Feature store read or written by the installed hooks.
391
+ """
392
+
393
+ attn_modules = collect_up_self_attentions(unet)
394
+ resnet_modules = collect_up_resnets(unet)
395
+ original_processors = [(module, module.processor) for module in attn_modules]
396
+ original_forwards = []
397
+
398
+ for layer_idx, module in enumerate(attn_modules, start=1):
399
+ module.set_processor(MMDiffAttnProcessor(layer_idx, store))
400
+
401
+ for layer_idx, module in enumerate(resnet_modules):
402
+ original_forward = module.forward
403
+
404
+ def _make_forward(orig: Callable, idx: int):
405
+ def wrapped(hidden_states: torch.Tensor, temb: torch.Tensor | None = None, *args: Any, **kwargs: Any):
406
+ output = orig(hidden_states, temb, *args, **kwargs)
407
+ if store.mode == "save":
408
+ store.save_resnet(idx, output)
409
+ elif store.mode == "inject":
410
+ injected = store.get_resnet(idx)
411
+ if injected is not None:
412
+ output = injected.to(device=output.device, dtype=output.dtype)
413
+ return output
414
+
415
+ return wrapped
416
+
417
+ original_forwards.append((module, original_forward))
418
+ module.forward = _make_forward(original_forward, layer_idx)
419
+
420
+ try:
421
+ yield store
422
+ finally:
423
+ for module, processor in original_processors:
424
+ module.set_processor(processor)
425
+ for module, original_forward in original_forwards:
426
+ module.forward = original_forward
427
+
428
+
429
+ def normalize_modalities(modalities: str | list[str] | tuple[str, ...]) -> list[str]:
430
+ r"""
431
+ Validate and normalize the modality list passed to the pipeline.
432
+
433
+ Args:
434
+ modalities (`str` or sequence of `str`):
435
+ `"all"` or any subset of `opt`, `sar`, and `ir`.
436
+
437
+ Returns:
438
+ `list[str]`: Deduplicated modalities in OPT → SAR → IR order.
439
+ """
440
+
441
+ if isinstance(modalities, str):
442
+ requested = list(SUPPORTED_MODALITIES) if modalities.lower() == "all" else [modalities.lower()]
443
+ else:
444
+ requested = [str(item).lower() for item in modalities]
445
+
446
+ unknown = [item for item in requested if item not in SUPPORTED_MODALITIES]
447
+ if unknown:
448
+ raise ValueError(
449
+ f"Unsupported modalities {unknown}. Expected a subset of {list(SUPPORTED_MODALITIES)} or 'all'."
450
+ )
451
+ if not requested:
452
+ raise ValueError("At least one modality must be requested.")
453
+
454
+ ordered = [item for item in SUPPORTED_MODALITIES if item in requested]
455
+ return ordered
456
+
457
+
458
+ @dataclass
459
+ class MMDiffPipelineOutput(BaseOutput):
460
+ r"""
461
+ Output of [`MMDiffPipeline`].
462
+
463
+ Args:
464
+ images (`list`):
465
+ Images for the first requested modality, matching the Stable Diffusion `images` convention.
466
+ opt (`list`, *optional*):
467
+ Optical images when that modality was generated.
468
+ sar (`list`, *optional*):
469
+ SAR images when that modality was generated.
470
+ ir (`list`, *optional*):
471
+ Infrared images when that modality was generated.
472
+ nsfw_content_detected (`list[bool]`, *optional*):
473
+ Safety-checker flags for the primary `images` batch, if a checker is enabled.
474
+ spatial_features (`dict`, *optional*):
475
+ In-memory OPT features when `return_spatial_features=True`.
476
+ """
477
+
478
+ images: list[Any]
479
+ opt: list[Any] | None = None
480
+ sar: list[Any] | None = None
481
+ ir: list[Any] | None = None
482
+ nsfw_content_detected: list[bool] | None = None
483
+ spatial_features: dict[str, Any] | None = None
484
+
485
+
486
+ class MMDiffPipeline(
487
+ StableDiffusionPipeline,
488
+ DiffusionPipeline,
489
+ TextualInversionLoaderMixin,
490
+ StableDiffusionLoraLoaderMixin,
491
+ IPAdapterMixin,
492
+ FromSingleFileMixin,
493
+ ):
494
+ r"""
495
+ Pipeline for jointly generating spatially consistent optical, SAR, and infrared images.
496
+
497
+ MMDiff fine-tunes a Stable Diffusion v1 UNet on optical remote-sensing pairs, then adapts
498
+ SAR and IR style with LoRA. During inference the OPT branch records up-block self-attention
499
+ queries and residual features; those features are injected into the SAR and IR branches.
500
+
501
+ Parameters:
502
+ vae ([`AutoencoderKL`]):
503
+ Variational Auto-Encoder used to decode latents into images.
504
+ text_encoder ([`CLIPTextModel`]):
505
+ Frozen CLIP text encoder.
506
+ tokenizer ([`CLIPTokenizer`]):
507
+ CLIP tokenizer paired with `text_encoder`.
508
+ unet ([`UNet2DConditionModel`]):
509
+ OPT-finetuned UNet. SAR/IR LoRA adapters are applied on top of this backbone.
510
+ scheduler ([`KarrasDiffusionSchedulers`]):
511
+ Denoising scheduler. The original sampling code uses [`DDPMScheduler`].
512
+ safety_checker ([`StableDiffusionSafetyChecker`], *optional*):
513
+ Optional safety checker. Disabled by default for remote-sensing imagery.
514
+ feature_extractor ([`CLIPImageProcessor`], *optional*):
515
+ Feature extractor used only when `safety_checker` is enabled.
516
+ image_encoder ([`CLIPVisionModelWithProjection`], *optional*):
517
+ Optional IP-Adapter image encoder.
518
+ requires_safety_checker (`bool`, *optional*, defaults to `False`):
519
+ Whether a missing safety checker should emit a warning.
520
+ lora_root (`str`, *optional*, defaults to `"loras"`):
521
+ Directory (relative to the model root) that contains `sar/<scene>` and `ir/<scene>` adapters.
522
+ """
523
+
524
+ model_cpu_offload_seq = "text_encoder->image_encoder->unet->vae"
525
+ _optional_components = ["safety_checker", "feature_extractor", "image_encoder"]
526
+ _exclude_from_cpu_offload = ["safety_checker"]
527
+ _callback_tensor_inputs = ["latents", "prompt_embeds", "negative_prompt_embeds"]
528
+
529
+ def __init__(
530
+ self,
531
+ vae: AutoencoderKL,
532
+ text_encoder: CLIPTextModel,
533
+ tokenizer: CLIPTokenizer,
534
+ unet: UNet2DConditionModel,
535
+ scheduler: KarrasDiffusionSchedulers,
536
+ safety_checker: StableDiffusionSafetyChecker | None = None,
537
+ feature_extractor: CLIPImageProcessor | None = None,
538
+ image_encoder: CLIPVisionModelWithProjection | None = None,
539
+ requires_safety_checker: bool = False,
540
+ lora_root: str = "loras",
541
+ ) -> None:
542
+ super().__init__(
543
+ vae=vae,
544
+ text_encoder=text_encoder,
545
+ tokenizer=tokenizer,
546
+ unet=unet,
547
+ scheduler=scheduler,
548
+ safety_checker=safety_checker,
549
+ feature_extractor=feature_extractor,
550
+ image_encoder=image_encoder,
551
+ requires_safety_checker=requires_safety_checker,
552
+ )
553
+ self.register_to_config(lora_root=lora_root, requires_safety_checker=requires_safety_checker)
554
+ self.spatial_store = SpatialFeatureStore()
555
+ self._loaded_scene: str | None = None
556
+
557
+ def check_inputs(
558
+ self,
559
+ prompt: str | list[str] | None,
560
+ height: int,
561
+ width: int,
562
+ callback_steps: int | None,
563
+ negative_prompt: str | list[str] | None = None,
564
+ prompt_embeds: torch.Tensor | None = None,
565
+ negative_prompt_embeds: torch.Tensor | None = None,
566
+ ip_adapter_image: PipelineImageInput | None = None,
567
+ ip_adapter_image_embeds: list[torch.Tensor] | None = None,
568
+ callback_on_step_end_tensor_inputs: list[str] | None = None,
569
+ modalities: str | list[str] | None = None,
570
+ scene: str | None = None,
571
+ spatial_features: dict[str, Any] | None = None,
572
+ ) -> None:
573
+ r"""
574
+ Validate standard Stable Diffusion arguments plus MMDiff modality options.
575
+
576
+ Args:
577
+ prompt (`str` or `list[str]`, *optional*):
578
+ Text prompt. Required unless `prompt_embeds` is provided.
579
+ height (`int`):
580
+ Output height in pixels. Must be divisible by the VAE scale factor.
581
+ width (`int`):
582
+ Output width in pixels. Must be divisible by the VAE scale factor.
583
+ callback_steps (`int`, *optional*):
584
+ Deprecated callback interval forwarded to the parent checker.
585
+ negative_prompt (`str` or `list[str]`, *optional*):
586
+ Negative prompt used for classifier-free guidance.
587
+ prompt_embeds (`torch.Tensor`, *optional*):
588
+ Precomputed prompt embeddings.
589
+ negative_prompt_embeds (`torch.Tensor`, *optional*):
590
+ Precomputed negative prompt embeddings.
591
+ ip_adapter_image (`PipelineImageInput`, *optional*):
592
+ Optional IP-Adapter image.
593
+ ip_adapter_image_embeds (`list[torch.Tensor]`, *optional*):
594
+ Optional precomputed IP-Adapter embeddings.
595
+ callback_on_step_end_tensor_inputs (`list[str]`, *optional*):
596
+ Tensor names forwarded to step-end callbacks.
597
+ modalities (`str` or `list[str]`, *optional*):
598
+ Requested modalities; validated by `normalize_modalities`.
599
+ scene (`str`, *optional*):
600
+ LoRA scene name used for SAR/IR adapters.
601
+ spatial_features (`dict`, *optional*):
602
+ Previously captured OPT features.
603
+ """
604
+
605
+ super().check_inputs(
606
+ prompt,
607
+ height,
608
+ width,
609
+ callback_steps,
610
+ negative_prompt,
611
+ prompt_embeds,
612
+ negative_prompt_embeds,
613
+ ip_adapter_image,
614
+ ip_adapter_image_embeds,
615
+ callback_on_step_end_tensor_inputs,
616
+ )
617
+ if modalities is not None:
618
+ normalize_modalities(modalities)
619
+ if scene is not None and not isinstance(scene, str):
620
+ raise TypeError(f"`scene` must be a string, got {type(scene)}.")
621
+ if spatial_features is not None and not isinstance(spatial_features, dict):
622
+ raise TypeError("`spatial_features` must be a dict produced by this pipeline.")
623
+
624
+ def decode_latents(self, latents: torch.Tensor, single_channel: bool = False) -> torch.Tensor:
625
+ r"""
626
+ Decode latents with the VAE, optionally collapsing RGB to a single SAR/IR channel.
627
+
628
+ Args:
629
+ latents (`torch.Tensor`):
630
+ Denoised latent tensor of shape `(batch, 4, h, w)`.
631
+ single_channel (`bool`, *optional*, defaults to `False`):
632
+ If `True`, average decoded RGB channels. This matches the original
633
+ single-channel VAE decoder used for SAR and IR.
634
+
635
+ Returns:
636
+ `torch.Tensor`: Decoded images in `[-1, 1]`.
637
+ """
638
+
639
+ latents = latents / self.vae.config.scaling_factor
640
+ image = self.vae.decode(latents, return_dict=False)[0]
641
+ if single_channel:
642
+ image = image.mean(dim=1, keepdim=True)
643
+ return image
644
+
645
+ def resolve_lora_dir(self, modality: str, scene: str, lora_path: str | Path | None = None) -> Path:
646
+ r"""
647
+ Resolve the directory that stores a scene-specific SAR or IR LoRA.
648
+
649
+ Args:
650
+ modality (`str`):
651
+ `"sar"` or `"ir"`.
652
+ scene (`str`):
653
+ Scene name such as `"ship"` or `"beach"`.
654
+ lora_path (`str` or `Path`, *optional*):
655
+ Explicit override. When omitted, `{model_root}/{lora_root}/{modality}/{scene}` is used.
656
+
657
+ Returns:
658
+ `Path`: Directory expected to contain `pytorch_lora_weights.safetensors`.
659
+ """
660
+
661
+ if lora_path is not None:
662
+ return Path(lora_path)
663
+ root = Path(self.config.lora_root)
664
+ if not root.is_absolute():
665
+ base = getattr(self, "name_or_path", None) or "."
666
+ root = Path(base) / root
667
+ return root / modality / scene
668
+
669
+ def load_scene_loras(
670
+ self,
671
+ scene: str,
672
+ sar_lora_path: str | Path | None = None,
673
+ ir_lora_path: str | Path | None = None,
674
+ ) -> None:
675
+ r"""
676
+ Load SAR and IR LoRA adapters for `scene` as named PEFT adapters.
677
+
678
+ Args:
679
+ scene (`str`):
680
+ Scene used to resolve default LoRA directories.
681
+ sar_lora_path (`str` or `Path`, *optional*):
682
+ Explicit SAR adapter directory or weight file.
683
+ ir_lora_path (`str` or `Path`, *optional*):
684
+ Explicit IR adapter directory or weight file.
685
+ """
686
+
687
+ if self._loaded_scene == scene and sar_lora_path is None and ir_lora_path is None:
688
+ return
689
+
690
+ if hasattr(self, "unload_lora_weights"):
691
+ try:
692
+ self.unload_lora_weights()
693
+ except Exception:
694
+ logger.debug("No previously loaded LoRA adapters to unload.")
695
+
696
+ loaded = False
697
+ for modality, path in (("sar", sar_lora_path), ("ir", ir_lora_path)):
698
+ adapter_dir = self.resolve_lora_dir(modality, scene, path)
699
+ weight_file = adapter_dir if adapter_dir.is_file() else adapter_dir / "pytorch_lora_weights.safetensors"
700
+ if not Path(weight_file).is_file() and not adapter_dir.is_dir():
701
+ logger.warning("Skipping %s LoRA for scene '%s'; missing path: %s", modality, scene, adapter_dir)
702
+ continue
703
+ load_target = adapter_dir if adapter_dir.is_dir() else adapter_dir.parent
704
+ self.load_lora_weights(str(load_target), adapter_name=modality)
705
+ loaded = True
706
+
707
+ if loaded:
708
+ self._loaded_scene = scene
709
+
710
+ def _set_modality_adapter(self, modality: str) -> None:
711
+ if modality == "opt":
712
+ if hasattr(self, "disable_lora"):
713
+ try:
714
+ self.disable_lora()
715
+ except Exception:
716
+ logger.debug("LoRA disable skipped; no adapters are active.")
717
+ return
718
+ if hasattr(self, "set_adapters"):
719
+ try:
720
+ self.set_adapters(modality)
721
+ except Exception as error:
722
+ logger.warning("Could not activate the '%s' LoRA adapter: %s", modality, error)
723
+
724
+ def _postprocess_image(
725
+ self,
726
+ image: torch.Tensor,
727
+ output_type: str,
728
+ single_channel: bool,
729
+ dtype: torch.dtype,
730
+ device: torch.device,
731
+ ) -> Any:
732
+ if output_type == "latent":
733
+ return image
734
+ if single_channel:
735
+ image, has_nsfw = self.run_safety_checker(image.repeat(1, 3, 1, 1) if image.shape[1] == 1 else image, device, dtype)
736
+ del has_nsfw
737
+ image = image.mean(dim=1, keepdim=True)
738
+ image = (image / 2 + 0.5).clamp(0, 1)
739
+ image_np = image.cpu().permute(0, 2, 3, 1).float().numpy()
740
+ if output_type == "np":
741
+ return image_np[..., 0]
742
+ if output_type == "pil":
743
+ return [
744
+ self.numpy_to_pil(frame)[0].convert("L") if frame.ndim == 3 else self.numpy_to_pil(frame[..., None])[0]
745
+ for frame in image_np
746
+ ]
747
+ raise ValueError(f"Unknown output_type '{output_type}'. Use 'pil', 'np', or 'latent'.")
748
+
749
+ image, has_nsfw_concept = self.run_safety_checker(image, device, dtype)
750
+ do_denormalize = [True] * image.shape[0] if has_nsfw_concept is None else [not flag for flag in has_nsfw_concept]
751
+ return self.image_processor.postprocess(image, output_type=output_type, do_denormalize=do_denormalize)
752
+
753
+ def _denoise(
754
+ self,
755
+ prompt_embeds: torch.Tensor,
756
+ timesteps: torch.Tensor,
757
+ latents: torch.Tensor,
758
+ extra_step_kwargs: dict[str, Any],
759
+ timestep_cond: torch.Tensor | None,
760
+ added_cond_kwargs: dict[str, Any] | None,
761
+ store_mode: str,
762
+ callback: Callable | None,
763
+ callback_steps: int | None,
764
+ callback_on_step_end: Callable | None,
765
+ callback_on_step_end_tensor_inputs: list[str],
766
+ num_inference_steps: int,
767
+ ) -> torch.Tensor:
768
+ store = self.spatial_store
769
+ store.mode = store_mode
770
+ num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order
771
+ self._num_timesteps = len(timesteps)
772
+
773
+ with spatial_transfer_hooks(self.unet, store), self.progress_bar(total=num_inference_steps) as progress_bar:
774
+ for i, t in enumerate(timesteps):
775
+ if self.interrupt:
776
+ continue
777
+ store.set_timestep(t)
778
+ latent_model_input = torch.cat([latents] * 2) if self.do_classifier_free_guidance else latents
779
+ if hasattr(self.scheduler, "scale_model_input"):
780
+ latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)
781
+
782
+ noise_pred = self.unet(
783
+ latent_model_input,
784
+ t,
785
+ encoder_hidden_states=prompt_embeds,
786
+ timestep_cond=timestep_cond,
787
+ cross_attention_kwargs=self.cross_attention_kwargs,
788
+ added_cond_kwargs=added_cond_kwargs,
789
+ return_dict=False,
790
+ )[0]
791
+
792
+ if self.do_classifier_free_guidance:
793
+ noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
794
+ noise_pred = noise_pred_uncond + self.guidance_scale * (noise_pred_text - noise_pred_uncond)
795
+ if self.guidance_rescale > 0.0:
796
+ noise_pred = rescale_noise_cfg(noise_pred, noise_pred_text, guidance_rescale=self.guidance_rescale)
797
+
798
+ latents = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs, return_dict=False)[0]
799
+
800
+ if callback_on_step_end is not None:
801
+ callback_kwargs = {name: locals()[name] for name in callback_on_step_end_tensor_inputs if name in locals()}
802
+ callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
803
+ latents = callback_outputs.pop("latents", latents)
804
+ prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
805
+
806
+ if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
807
+ progress_bar.update()
808
+ if callback is not None and callback_steps is not None and i % callback_steps == 0:
809
+ step_idx = i // getattr(self.scheduler, "order", 1)
810
+ callback(step_idx, t, latents)
811
+
812
+ store.mode = "off"
813
+ return latents
814
+
815
+ @torch.no_grad()
816
+ @replace_example_docstring(EXAMPLE_DOC_STRING)
817
+ def __call__(
818
+ self,
819
+ prompt: str | list[str] | None = None,
820
+ height: int | None = None,
821
+ width: int | None = None,
822
+ num_inference_steps: int = 50,
823
+ timesteps: list[int] | None = None,
824
+ sigmas: list[float] | None = None,
825
+ guidance_scale: float = 7.5,
826
+ negative_prompt: str | list[str] | None = None,
827
+ num_images_per_prompt: int | None = 1,
828
+ eta: float = 0.0,
829
+ generator: torch.Generator | list[torch.Generator] | None = None,
830
+ latents: torch.Tensor | None = None,
831
+ prompt_embeds: torch.Tensor | None = None,
832
+ negative_prompt_embeds: torch.Tensor | None = None,
833
+ ip_adapter_image: PipelineImageInput | None = None,
834
+ ip_adapter_image_embeds: list[torch.Tensor] | None = None,
835
+ output_type: str | None = "pil",
836
+ return_dict: bool = True,
837
+ cross_attention_kwargs: dict[str, Any] | None = None,
838
+ guidance_rescale: float = 0.0,
839
+ clip_skip: int | None = None,
840
+ callback_on_step_end: Callable[..., Any] | None = None,
841
+ callback_on_step_end_tensor_inputs: list[str] | None = None,
842
+ modalities: str | list[str] = "all",
843
+ scene: str = DEFAULT_SCENE,
844
+ sar_lora_path: str | Path | None = None,
845
+ ir_lora_path: str | Path | None = None,
846
+ attn_layers: list[int] | tuple[int, ...] | None = None,
847
+ resnet_layers: list[int] | tuple[int, ...] | None = None,
848
+ resnet_time: float = 1.0,
849
+ spatial_features: dict[str, Any] | None = None,
850
+ return_spatial_features: bool = False,
851
+ **kwargs: Any,
852
+ ) -> MMDiffPipelineOutput | tuple:
853
+ r"""
854
+ Generate optical, SAR, and/or infrared images from one text prompt.
855
+
856
+ The call follows the Stable Diffusion stage order: check inputs, define call
857
+ parameters, encode the prompt, prepare timesteps, prepare latents, prepare extra
858
+ step kwargs, then run the denoising loop. OPT is generated first so its spatial
859
+ features can be transferred into the SAR and IR branches.
860
+
861
+ Args:
862
+ prompt (`str` or `list[str]`, *optional*):
863
+ Text prompt that guides all requested modalities.
864
+ height (`int`, *optional*, defaults to `256`):
865
+ Output height in pixels. MMDiff was trained at 256×256.
866
+ width (`int`, *optional*, defaults to `256`):
867
+ Output width in pixels.
868
+ num_inference_steps (`int`, *optional*, defaults to `50`):
869
+ Number of denoising steps.
870
+ timesteps (`list[int]`, *optional*):
871
+ Custom descending timestep schedule.
872
+ sigmas (`list[float]`, *optional*):
873
+ Custom sigma schedule for compatible schedulers.
874
+ guidance_scale (`float`, *optional*, defaults to `7.5`):
875
+ Classifier-free guidance scale. Guidance is enabled when this value is `> 1`.
876
+ negative_prompt (`str` or `list[str]`, *optional*):
877
+ Prompt used for the unconditional branch. Defaults to empty strings.
878
+ num_images_per_prompt (`int`, *optional*, defaults to `1`):
879
+ Number of images drawn per prompt.
880
+ eta (`float`, *optional*, defaults to `0.0`):
881
+ DDIM eta. Ignored by schedulers that do not accept `eta`.
882
+ generator (`torch.Generator` or `list[torch.Generator]`, *optional*):
883
+ RNG used to sample the shared initial latents for every modality.
884
+ latents (`torch.Tensor`, *optional*):
885
+ Optional pre-sampled latents reused for every modality.
886
+ prompt_embeds (`torch.Tensor`, *optional*):
887
+ Precomputed prompt embeddings.
888
+ negative_prompt_embeds (`torch.Tensor`, *optional*):
889
+ Precomputed unconditional embeddings.
890
+ ip_adapter_image (`PipelineImageInput`, *optional*):
891
+ Optional IP-Adapter image condition.
892
+ ip_adapter_image_embeds (`list[torch.Tensor]`, *optional*):
893
+ Optional precomputed IP-Adapter embeddings.
894
+ output_type (`str`, *optional*, defaults to `"pil"`):
895
+ `"pil"`, `"np"`, or `"latent"`.
896
+ return_dict (`bool`, *optional*, defaults to `True`):
897
+ Whether to return [`MMDiffPipelineOutput`].
898
+ cross_attention_kwargs (`dict`, *optional*):
899
+ Extra kwargs forwarded to attention processors.
900
+ guidance_rescale (`float`, *optional*, defaults to `0.0`):
901
+ Optional guidance rescale factor.
902
+ clip_skip (`int`, *optional*):
903
+ Number of CLIP layers to skip when encoding prompts.
904
+ callback_on_step_end (`Callable`, *optional*):
905
+ Optional per-step callback.
906
+ callback_on_step_end_tensor_inputs (`list[str]`, *optional*):
907
+ Tensor names passed to `callback_on_step_end`.
908
+ modalities (`str` or `list[str]`, *optional*, defaults to `"all"`):
909
+ `"all"` or any subset of `"opt"`, `"sar"`, `"ir"`.
910
+ scene (`str`, *optional*, defaults to `"ship"`):
911
+ Scene used to resolve packaged SAR/IR LoRA adapters.
912
+ sar_lora_path (`str` or `Path`, *optional*):
913
+ Override for the SAR LoRA directory or weight file.
914
+ ir_lora_path (`str` or `Path`, *optional*):
915
+ Override for the IR LoRA directory or weight file.
916
+ attn_layers (`list[int]`, *optional*):
917
+ 1-based up-block self-attention layers to transfer. Defaults to `1..9`.
918
+ resnet_layers (`list[int]`, *optional*):
919
+ 0-based up-block ResNet layers to transfer. Defaults to `(2,)`.
920
+ resnet_time (`float`, *optional*, defaults to `1.0`):
921
+ Fraction of the early timestep schedule that receives ResNet injection.
922
+ spatial_features (`dict`, *optional*):
923
+ Features from a previous OPT run. When omitted, OPT is run first whenever
924
+ SAR or IR generation needs transfer features.
925
+ return_spatial_features (`bool`, *optional*, defaults to `False`):
926
+ If `True`, include the captured OPT features in the output.
927
+
928
+ Examples:
929
+
930
+ Returns:
931
+ [`MMDiffPipelineOutput`] or `tuple`:
932
+ Generated images grouped by modality. `images` is the first requested modality.
933
+ """
934
+
935
+ callback = kwargs.pop("callback", None)
936
+ callback_steps = kwargs.pop("callback_steps", None)
937
+ callback_on_step_end_tensor_inputs = callback_on_step_end_tensor_inputs or ["latents"]
938
+ requested = normalize_modalities(modalities)
939
+ attn_layers = tuple(DEFAULT_ATTN_LAYERS if attn_layers is None else attn_layers)
940
+ resnet_layers = tuple(DEFAULT_RESNET_LAYERS if resnet_layers is None else resnet_layers)
941
+
942
+ height = DEFAULT_RESOLUTION if height is None else height
943
+ width = DEFAULT_RESOLUTION if width is None else width
944
+
945
+ # 1. Check inputs
946
+ self.check_inputs(
947
+ prompt,
948
+ height,
949
+ width,
950
+ callback_steps,
951
+ negative_prompt,
952
+ prompt_embeds,
953
+ negative_prompt_embeds,
954
+ ip_adapter_image,
955
+ ip_adapter_image_embeds,
956
+ callback_on_step_end_tensor_inputs,
957
+ modalities=requested,
958
+ scene=scene,
959
+ spatial_features=spatial_features,
960
+ )
961
+
962
+ self._guidance_scale = guidance_scale
963
+ self._guidance_rescale = guidance_rescale
964
+ self._clip_skip = clip_skip
965
+ self._cross_attention_kwargs = cross_attention_kwargs
966
+ self._interrupt = False
967
+
968
+ # 2. Define call parameters
969
+ if prompt is not None and isinstance(prompt, str):
970
+ batch_size = 1
971
+ elif prompt is not None and isinstance(prompt, list):
972
+ batch_size = len(prompt)
973
+ else:
974
+ batch_size = prompt_embeds.shape[0]
975
+ num_images_per_prompt = 1 if num_images_per_prompt is None else num_images_per_prompt
976
+ device = self._execution_device
977
+
978
+ needs_transfer = any(modality in requested for modality in ("sar", "ir"))
979
+ run_opt = "opt" in requested or (needs_transfer and spatial_features is None)
980
+ if needs_transfer and not run_opt and spatial_features is None:
981
+ raise ValueError("SAR/IR generation requires OPT spatial features. Run OPT first or pass `spatial_features`.")
982
+
983
+ if any(modality in requested for modality in ("sar", "ir")):
984
+ self.load_scene_loras(scene, sar_lora_path=sar_lora_path, ir_lora_path=ir_lora_path)
985
+
986
+ self.spatial_store.reset()
987
+ self.spatial_store.attn_layers = set(attn_layers)
988
+ self.spatial_store.resnet_layers = set(resnet_layers)
989
+ if spatial_features is not None:
990
+ self.spatial_store.load_state(spatial_features)
991
+
992
+ # 3. Encode input condition
993
+ lora_scale = self.cross_attention_kwargs.get("scale", None) if self.cross_attention_kwargs is not None else None
994
+ prompt_embeds, negative_prompt_embeds = self.encode_prompt(
995
+ prompt,
996
+ device,
997
+ num_images_per_prompt,
998
+ self.do_classifier_free_guidance,
999
+ negative_prompt,
1000
+ prompt_embeds=prompt_embeds,
1001
+ negative_prompt_embeds=negative_prompt_embeds,
1002
+ lora_scale=lora_scale,
1003
+ clip_skip=self.clip_skip,
1004
+ )
1005
+ if self.do_classifier_free_guidance:
1006
+ prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])
1007
+
1008
+ added_cond_kwargs = None
1009
+ if ip_adapter_image is not None or ip_adapter_image_embeds is not None:
1010
+ image_embeds = self.prepare_ip_adapter_image_embeds(
1011
+ ip_adapter_image,
1012
+ ip_adapter_image_embeds,
1013
+ device,
1014
+ batch_size * num_images_per_prompt,
1015
+ self.do_classifier_free_guidance,
1016
+ )
1017
+ added_cond_kwargs = {"image_embeds": image_embeds}
1018
+
1019
+ # 4. Prepare timesteps
1020
+ timesteps, num_inference_steps = retrieve_timesteps(
1021
+ self.scheduler, num_inference_steps, device, timesteps, sigmas
1022
+ )
1023
+ timestep_values = [int(step) for step in timesteps]
1024
+ self.spatial_store.inject_attn_timesteps = set(timestep_values)
1025
+ cutoff = max(1, int(len(timestep_values) * resnet_time)) if resnet_time > 0 else 0
1026
+ self.spatial_store.inject_resnet_timesteps = set(timestep_values[:cutoff])
1027
+
1028
+ # 5. Prepare latent variables
1029
+ num_channels_latents = self.unet.config.in_channels
1030
+ init_latents = self.prepare_latents(
1031
+ batch_size * num_images_per_prompt,
1032
+ num_channels_latents,
1033
+ height,
1034
+ width,
1035
+ prompt_embeds.dtype,
1036
+ device,
1037
+ generator,
1038
+ latents,
1039
+ )
1040
+
1041
+ # 6. Prepare extra step kwargs
1042
+ extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta)
1043
+ timestep_cond = None
1044
+ if getattr(self.unet.config, "time_cond_proj_dim", None) is not None:
1045
+ guidance_scale_tensor = torch.tensor(self.guidance_scale - 1).repeat(batch_size * num_images_per_prompt)
1046
+ timestep_cond = self.get_guidance_scale_embedding(
1047
+ guidance_scale_tensor, embedding_dim=self.unet.config.time_cond_proj_dim
1048
+ ).to(device=device, dtype=init_latents.dtype)
1049
+
1050
+ # 7. Run denoising loop for each requested branch
1051
+ generated: dict[str, Any] = {}
1052
+ primary_nsfw = None
1053
+ for modality in (("opt",) if run_opt else ()) + tuple(item for item in requested if item != "opt"):
1054
+ self._set_modality_adapter(modality)
1055
+ store_mode = "save" if modality == "opt" else "inject"
1056
+ latents_in = init_latents.clone()
1057
+ latents_out = self._denoise(
1058
+ prompt_embeds=prompt_embeds,
1059
+ timesteps=timesteps,
1060
+ latents=latents_in,
1061
+ extra_step_kwargs=extra_step_kwargs,
1062
+ timestep_cond=timestep_cond,
1063
+ added_cond_kwargs=added_cond_kwargs,
1064
+ store_mode=store_mode,
1065
+ callback=callback,
1066
+ callback_steps=callback_steps,
1067
+ callback_on_step_end=callback_on_step_end,
1068
+ callback_on_step_end_tensor_inputs=callback_on_step_end_tensor_inputs,
1069
+ num_inference_steps=num_inference_steps,
1070
+ )
1071
+ if modality not in requested:
1072
+ continue
1073
+ if output_type == "latent":
1074
+ images = latents_out
1075
+ has_nsfw = None
1076
+ else:
1077
+ single_channel = modality in {"sar", "ir"}
1078
+ decoded = self.decode_latents(latents_out, single_channel=single_channel)
1079
+ if single_channel:
1080
+ images = self._postprocess_image(
1081
+ decoded, output_type=output_type, single_channel=True, dtype=prompt_embeds.dtype, device=device
1082
+ )
1083
+ has_nsfw = None
1084
+ else:
1085
+ images, has_nsfw = self.run_safety_checker(decoded, device, prompt_embeds.dtype)
1086
+ do_denormalize = [True] * images.shape[0] if has_nsfw is None else [not flag for flag in has_nsfw]
1087
+ images = self.image_processor.postprocess(
1088
+ images, output_type=output_type, do_denormalize=do_denormalize
1089
+ )
1090
+ generated[modality] = images
1091
+ if primary_nsfw is None:
1092
+ primary_nsfw = has_nsfw
1093
+
1094
+ self.maybe_free_model_hooks()
1095
+ feature_state = self.spatial_store.to_state() if return_spatial_features else None
1096
+ images = generated.get(requested[0])
1097
+ if not return_dict:
1098
+ return (images, primary_nsfw)
1099
+
1100
+ return MMDiffPipelineOutput(
1101
+ images=images,
1102
+ opt=generated.get("opt"),
1103
+ sar=generated.get("sar"),
1104
+ ir=generated.get("ir"),
1105
+ nsfw_content_detected=primary_nsfw,
1106
+ spatial_features=feature_state,
1107
+ )
1108
+
1109
+
1110
+ __all__ = ["MMDiffPipeline", "MMDiffPipelineOutput", "SpatialFeatureStore"]
scheduler/scheduler_config.json ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "DDPMScheduler",
3
+ "_diffusers_version": "0.32.0.dev0",
4
+ "beta_end": 0.012,
5
+ "beta_schedule": "scaled_linear",
6
+ "beta_start": 0.00085,
7
+ "clip_sample": false,
8
+ "num_train_timesteps": 1000,
9
+ "prediction_type": "epsilon",
10
+ "set_alpha_to_one": false,
11
+ "skip_prk_steps": true,
12
+ "steps_offset": 1,
13
+ "timestep_spacing": "leading",
14
+ "trained_betas": null
15
+ }
text_encoder/config.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_name_or_path": "/media/ubuntun/hdd/THj/model/stable-diffusion-v1-4",
3
+ "architectures": [
4
+ "CLIPTextModel"
5
+ ],
6
+ "attention_dropout": 0.0,
7
+ "bos_token_id": 0,
8
+ "dropout": 0.0,
9
+ "eos_token_id": 2,
10
+ "hidden_act": "quick_gelu",
11
+ "hidden_size": 768,
12
+ "initializer_factor": 1.0,
13
+ "initializer_range": 0.02,
14
+ "intermediate_size": 3072,
15
+ "layer_norm_eps": 1e-05,
16
+ "max_position_embeddings": 77,
17
+ "model_type": "clip_text_model",
18
+ "num_attention_heads": 12,
19
+ "num_hidden_layers": 12,
20
+ "pad_token_id": 1,
21
+ "projection_dim": 512,
22
+ "torch_dtype": "float16",
23
+ "transformers_version": "4.46.1",
24
+ "vocab_size": 49408
25
+ }
text_encoder/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:660c6f5b1abae9dc498ac2d21e1347d2abdb0cf6c0c0c8576cd796491d9a6cdd
3
+ size 246144152
tokenizer/merges.txt ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer/special_tokens_map.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "bos_token": {
3
+ "content": "<|startoftext|>",
4
+ "lstrip": false,
5
+ "normalized": true,
6
+ "rstrip": false,
7
+ "single_word": false
8
+ },
9
+ "eos_token": {
10
+ "content": "<|endoftext|>",
11
+ "lstrip": false,
12
+ "normalized": true,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ },
16
+ "pad_token": "<|endoftext|>",
17
+ "unk_token": {
18
+ "content": "<|endoftext|>",
19
+ "lstrip": false,
20
+ "normalized": true,
21
+ "rstrip": false,
22
+ "single_word": false
23
+ }
24
+ }
tokenizer/tokenizer_config.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "add_prefix_space": false,
3
+ "added_tokens_decoder": {
4
+ "49406": {
5
+ "content": "<|startoftext|>",
6
+ "lstrip": false,
7
+ "normalized": true,
8
+ "rstrip": false,
9
+ "single_word": false,
10
+ "special": true
11
+ },
12
+ "49407": {
13
+ "content": "<|endoftext|>",
14
+ "lstrip": false,
15
+ "normalized": true,
16
+ "rstrip": false,
17
+ "single_word": false,
18
+ "special": true
19
+ }
20
+ },
21
+ "bos_token": "<|startoftext|>",
22
+ "clean_up_tokenization_spaces": false,
23
+ "do_lower_case": true,
24
+ "eos_token": "<|endoftext|>",
25
+ "errors": "replace",
26
+ "model_max_length": 77,
27
+ "pad_token": "<|endoftext|>",
28
+ "tokenizer_class": "CLIPTokenizer",
29
+ "unk_token": "<|endoftext|>"
30
+ }
tokenizer/vocab.json ADDED
The diff for this file is too large to render. See raw diff
 
unet/config.json ADDED
@@ -0,0 +1,68 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "UNet2DConditionModel",
3
+ "_diffusers_version": "0.32.0.dev0",
4
+ "_name_or_path": "/media/ubuntun/hdd/THj/model/stable-diffusion-v1-4",
5
+ "act_fn": "silu",
6
+ "addition_embed_type": null,
7
+ "addition_embed_type_num_heads": 64,
8
+ "addition_time_embed_dim": null,
9
+ "attention_head_dim": 8,
10
+ "attention_type": "default",
11
+ "block_out_channels": [
12
+ 320,
13
+ 640,
14
+ 1280,
15
+ 1280
16
+ ],
17
+ "center_input_sample": false,
18
+ "class_embed_type": null,
19
+ "class_embeddings_concat": false,
20
+ "conv_in_kernel": 3,
21
+ "conv_out_kernel": 3,
22
+ "cross_attention_dim": 768,
23
+ "cross_attention_norm": null,
24
+ "down_block_types": [
25
+ "CrossAttnDownBlock2D",
26
+ "CrossAttnDownBlock2D",
27
+ "CrossAttnDownBlock2D",
28
+ "DownBlock2D"
29
+ ],
30
+ "downsample_padding": 1,
31
+ "dropout": 0.0,
32
+ "dual_cross_attention": false,
33
+ "encoder_hid_dim": null,
34
+ "encoder_hid_dim_type": null,
35
+ "flip_sin_to_cos": true,
36
+ "freq_shift": 0,
37
+ "in_channels": 4,
38
+ "layers_per_block": 2,
39
+ "mid_block_only_cross_attention": null,
40
+ "mid_block_scale_factor": 1,
41
+ "mid_block_type": "UNetMidBlock2DCrossAttn",
42
+ "norm_eps": 1e-05,
43
+ "norm_num_groups": 32,
44
+ "num_attention_heads": null,
45
+ "num_class_embeds": null,
46
+ "only_cross_attention": false,
47
+ "out_channels": 4,
48
+ "projection_class_embeddings_input_dim": null,
49
+ "resnet_out_scale_factor": 1.0,
50
+ "resnet_skip_time_act": false,
51
+ "resnet_time_scale_shift": "default",
52
+ "reverse_transformer_layers_per_block": null,
53
+ "sample_size": 64,
54
+ "time_cond_proj_dim": null,
55
+ "time_embedding_act_fn": null,
56
+ "time_embedding_dim": null,
57
+ "time_embedding_type": "positional",
58
+ "timestep_post_act": null,
59
+ "transformer_layers_per_block": 1,
60
+ "up_block_types": [
61
+ "UpBlock2D",
62
+ "CrossAttnUpBlock2D",
63
+ "CrossAttnUpBlock2D",
64
+ "CrossAttnUpBlock2D"
65
+ ],
66
+ "upcast_attention": false,
67
+ "use_linear_projection": false
68
+ }
unet/diffusion_pytorch_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6563affab036a21949693582693155259d70e390ec5d169fabc751def39a7012
3
+ size 3438167536
vae/config.json ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "AutoencoderKL",
3
+ "_diffusers_version": "0.32.0.dev0",
4
+ "_name_or_path": "/media/ubuntun/hdd/THj/model/stable-diffusion-v1-4",
5
+ "act_fn": "silu",
6
+ "block_out_channels": [
7
+ 128,
8
+ 256,
9
+ 512,
10
+ 512
11
+ ],
12
+ "down_block_types": [
13
+ "DownEncoderBlock2D",
14
+ "DownEncoderBlock2D",
15
+ "DownEncoderBlock2D",
16
+ "DownEncoderBlock2D"
17
+ ],
18
+ "force_upcast": true,
19
+ "in_channels": 3,
20
+ "latent_channels": 4,
21
+ "latents_mean": null,
22
+ "latents_std": null,
23
+ "layers_per_block": 2,
24
+ "mid_block_add_attention": true,
25
+ "norm_num_groups": 32,
26
+ "out_channels": 3,
27
+ "sample_size": 256,
28
+ "scaling_factor": 0.18215,
29
+ "shift_factor": null,
30
+ "up_block_types": [
31
+ "UpDecoderBlock2D",
32
+ "UpDecoderBlock2D",
33
+ "UpDecoderBlock2D",
34
+ "UpDecoderBlock2D"
35
+ ],
36
+ "use_post_quant_conv": true,
37
+ "use_quant_conv": true
38
+ }
vae/diffusion_pytorch_model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4fbcf0ebe55a0984f5a5e00d8c4521d52359af7229bb4d81890039d2aa16dd7c
3
+ size 167335342