remove unnecessary scripts and update output to multiple images

This commit is contained in:
Denis Kochetov
2025-06-02 21:07:17 +00:00
parent 5311a5f0d5
commit 626a5ee60d
41 changed files with 41 additions and 8413 deletions
-23
View File
@@ -1,23 +0,0 @@
*.png
*.jpg
*.jpeg
*.gif
*.bmp
*.tiff
*.ico
*.webp
#glb
*.glb
*.gltf
*.obj
*.mtl
*.ply
*.stl
*.dae
#pycache
__pycache__
#checkpoints
checkpoints/
-201
View File
@@ -1,201 +0,0 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-471
View File
@@ -1,471 +0,0 @@
# MV-Adapter: Multi-view Consistent Image Generation Made Easy🚀
## 🏠 <a href="https://huanngzh.github.io/MV-Adapter-Page/" target="_blank">Project Page</a> | <a href="https://arxiv.org/abs/2412.03632" target="_blank">Paper</a> | <a href="https://huggingface.co/huanngzh/mv-adapter" target="_blank">Model Weights</a> | [Demo](https://huggingface.co/collections/huanngzh/mv-adapter-spaces-677e497578747fd734a1b999) | <a href="https://github.com/huanngzh/ComfyUI-MVAdapter" target="_blank">ComfyUI</a>
![teaser](assets/doc/teaser.jpg)
MV-Adapter is a **versatile plug-and-play adapter** that adapt T2I models and their derivatives to multi-view generators.
Highlight Features: Generate multi-view images
- with 768 Resolution using SDXL
- using personalized models (e.g. <a href="https://civitai.com/models/112902/dreamshaper-xl" target="_blank">DreamShaper</a>), distilled models (e.g. <a href="https://huggingface.co/docs/diffusers/api/pipelines/latent_consistency_models" target="_blank">LCM</a>), or extensions (e.g. <a href="https://github.com/lllyasviel/ControlNet" target="_blank">ControlNet</a>)
- from text or image condition
- can be guided by geometry for texture generation
## 🔥 Updates
* [2025-05-15] Release full pipeline for text-to-texture and image-to-texture generation. [See [guidelines](#usage-texture-generation)]
* [2025-04-23] Release dataset ([Objaverse-Ortho10View](https://huggingface.co/datasets/huanngzh/Objaverse-Ortho10View) and [Objaverse-Rand6View](https://huggingface.co/datasets/huanngzh/Objaverse-Rand6View)) and training code. [See [guidelines](#️-training)]
* [2025-03-31] Release text/image-conditioned 3D texture generation demos on [Text2Texture](https://huggingface.co/spaces/VAST-AI/MV-Adapter-Text2Texture) and [Image2Texture](https://huggingface.co/spaces/VAST-AI/MV-Adapter-Img2Texture). Feel free to try them!
* [2025-03-17] Release model weights for partial-image conditioned geometry-to-multiview generation, which can be used to generate textured 3D scenes combined with [MIDI](https://github.com/VAST-AI-Research/MIDI-3D). [See [guidelines](#partial-image--geometry-to-multiview)]
* [2025-03-07] Release model weights for geometry-guided multi-view generation. [See [guidelines](#text-geometry-to-multiview-generation)]
* [2024-12-27] Release model weights, gradio demo, inference scripts and comfyui of text-/image- to multi-view generation models.
## TOC
- [MV-Adapter: Multi-view Consistent Image Generation Made Easy🚀](#mv-adapter-multi-view-consistent-image-generation-made-easy)
- [🏠 Project Page | Paper | Model Weights | Demo | ComfyUI](#-project-page--paper--model-weights--demo--comfyui)
- [🔥 Updates](#-updates)
- [TOC](#toc)
- [Model Zoo \& Demos](#model-zoo--demos)
- [Installation](#installation)
- [Notes](#notes)
- [System Requirements](#system-requirements)
- [Usage: Multiview Generation](#usage-multiview-generation)
- [Launch Demo](#launch-demo)
- [Text to Multiview Generation](#text-to-multiview-generation)
- [Image to Multiview Generation](#image-to-multiview-generation)
- [Inference Scripts](#inference-scripts)
- [Text to Multiview Generation](#text-to-multiview-generation-1)
- [Image to Multiview Generation](#image-to-multiview-generation-1)
- [Text-Geometry to Multiview Generation](#text-geometry-to-multiview-generation)
- [Image-Geometry to Multiview Generation](#image-geometry-to-multiview-generation)
- [Partial Image + Geometry to Multiview](#partial-image--geometry-to-multiview)
- [ComfyUI](#comfyui)
- [Usage: Texture Generation](#usage-texture-generation)
- [Citation](#citation)
## Model Zoo & Demos
No need to download manually. Running the scripts will download model weights automatically.
Notes: Running MV-Adapter for SDXL may need higher GPU memory and more time, but produce higher-quality and higher-resolution results. On the other hand, running its SD2.1 variant needs lower computing cost, but shows a bit lower performance.
| Model | Base Model | HF Weights | Demo Link |
| :-------------------------: | :--------: | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------------: |
| Text-to-Multiview | SD2.1 | [mvadapter_t2mv_sd21.safetensors](https://huggingface.co/huanngzh/mv-adapter/resolve/main/mvadapter_t2mv_sd21.safetensors) | |
| Text-to-Multiview | SDXL | [mvadapter_t2mv_sdxl.safetensors](https://huggingface.co/huanngzh/mv-adapter/resolve/main/mvadapter_t2mv_sdxl.safetensors) | [General](https://huggingface.co/spaces/VAST-AI/MV-Adapter-T2MV-SDXL) / [Anime](https://huggingface.co/spaces/huanngzh/MV-Adapter-T2MV-Anime) |
| Image-to-Multiview | SD2.1 | [mvadapter_i2mv_sd21.safetensors](https://huggingface.co/huanngzh/mv-adapter/resolve/main/mvadapter_i2mv_sd21.safetensors) | |
| Image-to-Multiview | SDXL | [mvadapter_i2mv_sdxl.safetensors](https://huggingface.co/huanngzh/mv-adapter/resolve/main/mvadapter_i2mv_sdxl.safetensors) | [Demo](https://huggingface.co/spaces/VAST-AI/MV-Adapter-I2MV-SDXL) |
| Text-Geometry-to-Multiview | SDXL | [mvadapter_tg2mv_sdxl.safetensors](https://huggingface.co/huanngzh/mv-adapter/resolve/main/mvadapter_tg2mv_sdxl.safetensors) | [Demo](https://huggingface.co/spaces/VAST-AI/MV-Adapter-Text2Texture) |
| Image-Geometry-to-Multiview | SDXL | [mvadapter_ig2mv_sdxl.safetensors](https://huggingface.co/huanngzh/mv-adapter/resolve/main/mvadapter_ig2mv_sdxl.safetensors) / [mvadapter_ig2mv_partial_sdxl.safetensors](https://huggingface.co/huanngzh/mv-adapter/resolve/main/mvadapter_ig2mv_partial_sdxl.safetensors) | [Demo](https://huggingface.co/spaces/VAST-AI/MV-Adapter-Img2Texture) |
| Image-to-Arbitrary-Views | SDXL | | |
## Installation
Clone the repo first:
```Bash
git clone https://github.com/huanngzh/MV-Adapter.git
cd MV-Adapter
```
(Optional) Create a fresh conda env:
```Bash
conda create -n mvadapter python=3.10
conda activate mvadapter
```
Install necessary packages (torch > 2):
```Bash
# pytorch (select correct CUDA version)
pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118
# other dependencies
pip install -r requirements.txt
```
For texture generation, you need to install `CV-CUDA` according to [CVCUDA/CV-CUDA](https://github.com/CVCUDA/CV-CUDA?tab=readme-ov-file#installation).
## Notes
### System Requirements
In the model zoo of MV-Adapter, running image-to-multiview generation has the highest system requirements, which requires about 14G GPU memory.
## Usage: Multiview Generation
### Launch Demo
#### Text to Multiview Generation
**With SDXL:**
```Bash
python -m scripts.gradio_demo_t2mv --base_model "stabilityai/stable-diffusion-xl-base-1.0"
```
![demo_t2mv_1](assets/doc/demo_t2mv_1.png)
![demo_t2mv_2](assets/doc/demo_t2mv_2.png)
> Reminder: When switching the demo to another base model, delete the `gradio_cached_examples` directory, otherwise it will affect the examples results of the next demo.
**With anime-themed <a href="https://huggingface.co/cagliostrolab/animagine-xl-3.1" target="_blank">Animagine XL 3.1</a>:**
```Bash
python -m scripts.gradio_demo_t2mv --base_model "cagliostrolab/animagine-xl-3.1"
```
![demo_t2mv_anime_1](assets/doc/demo_t2mv_anime_1.png)
![demo_t2mv_anime_2](assets/doc/demo_t2mv_anime_2.png)
**With general <a href="https://huggingface.co/Lykon/dreamshaper-xl-1-0" target="_blank">Dreamshaper</a>:**
```Bash
python -m scripts.gradio_demo_t2mv --base_model "Lykon/dreamshaper-xl-1-0" --scheduler ddpm
```
![demo_t2mv_dreamshaper_1](assets/doc/demo_t2mv_dreamshaper_1.png)
You can also specify a new diffusers-format text-to-image diffusion model using `--base_model`. Note that it should be the model name in huggingface, such as `stabilityai/stable-diffusion-xl-base-1.0`, or a local path refer to a text-to-image pipeline directory. Note that if you specify `latent-consistency/lcm-sdxl` to use latent consistency models, please add `--scheduler lcm` to the command.
#### Image to Multiview Generation
**With SDXL:**
```Bash
python -m scripts.gradio_demo_i2mv
```
![demo_i2mv_1](assets/doc/demo_i2mv_1.png)
![demo_i2mv_2](assets/doc/demo_i2mv_2.png)
### Inference Scripts
We recommend that experienced users check the files in the scripts directory to adjust the parameters appropriately to try the best "card drawing" results.
#### Text to Multiview Generation
Note that you can specify a diffusers-format text-to-image diffusion model as the base model using `--base_model xxx`. It should be the model name in huggingface, such as `stabilityai/stable-diffusion-xl-base-1.0`, or a local path refer to a text-to-image pipeline directory.
**With SDXL:**
```Bash
python -m scripts.inference_t2mv_sdxl --text "an astronaut riding a horse" \
--seed 42 \
--output output.png
```
**With personalized models:**
anime-themed <a href="https://huggingface.co/cagliostrolab/animagine-xl-3.1" target="_blank">Animagine XL 3.1</a>
```Bash
python -m scripts.inference_t2mv_sdxl --base_model "cagliostrolab/animagine-xl-3.1" \
--text "1girl, izayoi sakuya, touhou, solo, maid headdress, maid, apron, short sleeves, dress, closed mouth, white apron, serious face, upper body, masterpiece, best quality, very aesthetic, absurdres" \
--seed 0 \
--output output.png
```
general <a href="https://huggingface.co/Lykon/dreamshaper-xl-1-0" target="_blank">Dreamshaper</a>
```Bash
python -m scripts.inference_t2mv_sdxl --base_model "Lykon/dreamshaper-xl-1-0" \
--scheduler ddpm \
--text "the warrior Aragorn from Lord of the Rings, film grain, 8k hd" \
--seed 0 \
--output output.png
```
realistic <a href="https://huggingface.co/stablediffusionapi/real-dream-sdxl" target="_blank">real-dream-sdxl</a>
```Bash
python -m scripts.inference_t2mv_sdxl --base_model "stablediffusionapi/real-dream-sdxl" \
--scheduler ddpm \
--text "macro shot, parrot, colorful, dark shot, film grain, extremely detailed" \
--seed 42 \
--output output.png
```
**With <a href="https://huggingface.co/latent-consistency/lcm-sdxl" target="_blank">LCM</a>:**
```Bash
python -m scripts.inference_t2mv_sdxl --unet_model "latent-consistency/lcm-sdxl" \
--scheduler lcm \
--text "Samurai koala bear" \
--num_inference_steps 8 \
--seed 42 \
--output output.png
```
**With LoRA:**
stylized lora <a href="https://huggingface.co/goofyai/3d_render_style_xl" target="_blank">3d_render_style_xl</a>
```Bash
python -m scripts.inference_t2mv_sdxl --lora_model "goofyai/3d_render_style_xl/3d_render_style_xl.safetensors" \
--text "3d style, a fox with flowers around it" \
--seed 20 \
--lora_scale 1.0 \
--output output.png
```
**With ControlNet:**
Scribble to Multiview with <a href="https://huggingface.co/xinsir/controlnet-scribble-sdxl-1.0" target="_blank">controlnet-scribble-sdxl-1.0</a>
```Bash
python -m scripts.inference_scribble2mv_sdxl --text "A 3D model of Finn the Human from the animated television series Adventure Time. He is wearing his iconic blue shirt and green backpack and has a neutral expression on his face. He is standing in a relaxed pose with his left foot slightly forward and his right foot back. His arms are at his sides and his head is turned slightly to the right. The model is made up of simple shapes and has a stylized, cartoon-like appearance. It is textured to resemble the character's appearance in the show." \
--seed 0 \
--output output.png \
--guidance_scale 5.0 \
--controlnet_images "assets/demo/scribble2mv/color_0000.webp" "assets/demo/scribble2mv/color_0001.webp" "assets/demo/scribble2mv/color_0002.webp" "assets/demo/scribble2mv/color_0003.webp" "assets/demo/scribble2mv/color_0004.webp" "assets/demo/scribble2mv/color_0005.webp" \
--controlnet_conditioning_scale 0.7
```
**With SD2.1:**
> SD2.1 has lower demand for computing resources and higher inference speed, but a bit lower performance than SDXL.
> In our tests, ddpm scheduler works better than other schedulers here.
```Bash
python -m scripts.inference_t2mv_sd --text "a corgi puppy" \
--seed 42 --scheduler ddpm \
--output output.png
```
#### Image to Multiview Generation
**With SDXL:**
```Bash
python -m scripts.inference_i2mv_sdxl \
--image assets/demo/i2mv/A_decorative_figurine_of_a_young_anime-style_girl.png \
--text "A decorative figurine of a young anime-style girl" \
--seed 21 --output output.png --remove_bg
```
**With LCM:**
```Bash
python -m scripts.inference_i2mv_sdxl \
--unet_model "latent-consistency/lcm-sdxl" \
--scheduler lcm \
--image assets/demo/i2mv/A_juvenile_emperor_penguin_chick.png \
--text "A juvenile emperor penguin chick" \
--num_inference_steps 8 \
--seed 0 --output output.png --remove_bg
```
**With SD2.1:** (lower demand for computing resources and higher inference speed)
> In our tests, ddpm scheduler works better than other schedulers here.
```Bash
python -m scripts.inference_i2mv_sd \
--image assets/demo/i2mv/A_decorative_figurine_of_a_young_anime-style_girl.png \
--text "A decorative figurine of a young anime-style girl" \
--output output.png --remove_bg --scheduler ddpm
```
#### Text-Geometry to Multiview Generation
**Importantly**, when using geometry-condition generation, please make sure that the orientation of the mesh you provide is consistent with the following example. Otherwise, you need to adjust the angles in the scripts when rendering the view.
**With SDXL:**
```Bash
python -m scripts.inference_tg2mv_sdxl \
--mesh assets/demo/tg2mv/ac9d4e4f44f34775ad46878ba8fbfd86.glb \
--text "Mater, a rusty and beat-up tow truck from the 2006 Disney/Pixar animated film 'Cars', with a rusty brown exterior, big blue eyes."
```
![tg2mv_example_out](assets/demo/tg2mv/ac9d4e4f44f34775ad46878ba8fbfd86_mv.png)
```Bash
python -m scripts.inference_tg2mv_sdxl \
--mesh assets/demo/tg2mv/b5f0f0f33e3644d1ba73576ceb486d42.glb \
--text "Optimus Prime, a character from Transformers, with blue, red and gray colors, and has a flame-like pattern on the body"
```
#### Image-Geometry to Multiview Generation
**With SDXL:**
```Bash
python -m scripts.inference_ig2mv_sdxl \
--image assets/demo/ig2mv/1ccd5c1563ea4f5fb8152eac59dabd5c.jpeg \
--mesh assets/demo/ig2mv/1ccd5c1563ea4f5fb8152eac59dabd5c.glb \
--output output.png --remove_bg
```
![example_ig2mv_out](assets/demo/ig2mv/1ccd5c1563ea4f5fb8152eac59dabd5c_mv.png)
#### Partial Image + Geometry to Multiview
**With SDXL:**
```Bash
python -m scripts.inference_ig2mv_partial_sdxl \
--image assets/demo/ig2mv/cartoon_style_table.png \
--mesh assets/demo/ig2mv/cartoon_style_table.glb \
--output output.png
```
Example input:
<img src="assets/demo/ig2mv/cartoon_style_table.png" alt="partial input" style="width: 20%">
Example output:
![example_partial_ig2mv](assets/demo/ig2mv/cartoon_style_table_mv.png)
The above command will save a `*_transform.json` file in the output dir, which contains transformation information like this:
```json
{
"offset": [
0.7446051140100826,
-0.3421213991056582,
0.1360104325533671
],
"scale": 1.0086087120792668
}
```
You can use it to transform your mesh into the canonical space, map the generated multi-view images onto the mesh, and then re-transform the mesh back to the original spatial position.
### ComfyUI
Please check <a href="https://github.com/huanngzh/ComfyUI-MVAdapter" target="_blank">ComfyUI-MVAdapter Repo</a> for details.
**Text to Multiview Generation**
![comfyui_t2mv](assets/doc/comfyui_t2mv.png)
**Text to Multiview Generation with LoRA**
![comfyui_t2mv_lora](assets/doc/comfyui_t2mv_lora.png)
**Image to Multiview Generation**
![comfyui_i2mv](assets/doc/comfyui_i2mv.png)
## Usage: Texture Generation
**Prepare Models**
Download pre-trained [RealESRGAN](https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth) for upscaling images and [LaMa](https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt) for view in-painting.
```Bash
wget https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth -O ./checkpoints/RealESRGAN_x2plus.pth
wget https://github.com/Sanster/models/releases/download/add_big_lama/big-lama.pt -O ./checkpoints/big-lama.pt
```
**Usage**
> All in one script
Text-conditioned texture generation:
```Bash
python -m scripts.texture_t2tex \
--mesh assets/demo/tg2mv/ac9d4e4f44f34775ad46878ba8fbfd86.glb \
--text "Mater, a rusty and beat-up tow truck from the 2006 Disney/Pixar animated film 'Cars', with a rusty brown exterior, big blue eyes." \
--save_dir outputs --save_name t2tex_sample
```
Image-conditioned texture generation:
```Bash
python -m scripts.texture_i2tex \
--image assets/demo/ig2mv/1ccd5c1563ea4f5fb8152eac59dabd5c.jpeg \
--mesh assets/demo/ig2mv/1ccd5c1563ea4f5fb8152eac59dabd5c.glb \
--save_dir outputs --save_name i2tex_sample \
--remove_bg
```
It will save the textured model into `<save_dir>/<save_name>_shaded.glb`.
## 📊 Dataset
Our training dataset, rendered from [Objaverse](https://huggingface.co/datasets/allenai/objaverse), can be downloaded from [Objaverse-Ortho10View](https://huggingface.co/datasets/huanngzh/Objaverse-Ortho10View) and [Objaverse-Rand6View](https://huggingface.co/datasets/huanngzh/Objaverse-Rand6View).
* [Objaverse-Ortho10View](https://huggingface.co/datasets/huanngzh/Objaverse-Ortho10View) contains 10 orthographic views of 1024x1024 resolution, and is used as ground truth.
* [Objaverse-Rand6View](https://huggingface.co/datasets/huanngzh/Objaverse-Rand6View) contains 6 randomly distributed views, and is used as reference image conditions.
Please refer to their dataset cards to extract the data files, and organize them into the following structures:
```Bash
data
├── texture_ortho10view_easylight_objaverse # Objaverse-Ortho10View
│ ├── 00
│ │ ├── 00a4d2b0c4c240289ed456e87d8b9e02
│ ...
├── texture_rand_easylight_objaverse # Objaverse-Rand6View
│ ├── 00
│ │ ├── 00a4d2b0c4c240289ed456e87d8b9e02
│ ...
├── objaverse_list_6w.json # objaverse ids
└── objaverse_short_captions.json # id to captions
```
## 🏋️ Training
The key training code can be found in `mvadapter/systems`:
* `MVAdapterTextSDXLSystem` in `mvadapter_text_sdxl.py` is used for text or text+geometry conditioned multi-view generation.
* `MVAdapterImageSDXLSystem` in `mvadapter_image_sdxl.py` is used for image or image+geometry conditioned multi-view generation.
The specific training commands are as follows:
For text to 6 view generation:
```Bash
python launch.py --config configs/view-guidance/mvadapter_t2mv_sdxl.yaml --train --gpu 0,1,2,3,4,5,6,7
```
For single image to 6 view generation:
```Bash
python launch.py --config configs/view-guidance/mvadapter_i2mv_sdxl.yaml --train --gpu 0,1,2,3,4,5,6,7
```
For single image to 2/3/4/6 view generation:
```Bash
python launch.py --config configs/view-guidance/mvadapter_i2mv_sdxl_aug_quantity.yaml --train --gpu 0,1,2,3,4,5,6,7
```
For text + geometry to 6 view generation:
```Bash
python launch.py --config configs/geometry-guidance/mvadapter_tg2mv_sdxl.yaml --train --gpu 0,1,2,3,4,5,6,7
```
For single image + geometry to 6 view generation:
```Bash
python launch.py --config configs/geometry-guidance/mvadapter_ig2mv_sdxl.yaml --train --gpu 0,1,2,3,4,5,6,7
```
For single partial image + geometry to 6 view generation (used for texture generation conditioned on occluded image, for example, used in [MIDI-3D](https://github.com/VAST-AI-Research/MIDI-3D)):
```Bash
python launch.py --config configs/geometry-guidance/mvadapter_ig2mv_partialimg_sdxl.yaml --train --gpu 0,1,2,3,4,5,6,7
```
## Citation
```
@article{huang2024mvadapter,
title={MV-Adapter: Multi-view Consistent Image Generation Made Easy},
author={Huang, Zehuan and Guo, Yuanchen and Wang, Haoran and Yi, Ran and Ma, Lizhuang and Cao, Yan-Pei and Sheng, Lu},
journal={arXiv preprint arXiv:2412.03632},
year={2024}
}
```
-221
View File
@@ -1,221 +0,0 @@
import argparse
import contextlib
import logging
import os
import sys
class ColoredFilter(logging.Filter):
"""
A logging filter to add color to certain log levels.
"""
RESET = "\033[0m"
RED = "\033[31m"
GREEN = "\033[32m"
YELLOW = "\033[33m"
BLUE = "\033[34m"
MAGENTA = "\033[35m"
CYAN = "\033[36m"
COLORS = {
"WARNING": YELLOW,
"INFO": GREEN,
"DEBUG": BLUE,
"CRITICAL": MAGENTA,
"ERROR": RED,
}
RESET = "\x1b[0m"
def __init__(self):
super().__init__()
def filter(self, record):
if record.levelname in self.COLORS:
color_start = self.COLORS[record.levelname]
record.levelname = f"{color_start}[{record.levelname}]"
record.msg = f"{record.msg}{self.RESET}"
return True
def main(args, extras) -> None:
# set CUDA_VISIBLE_DEVICES if needed, then import pytorch-lightning
os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID"
env_gpus_str = os.environ.get("CUDA_VISIBLE_DEVICES", None)
env_gpus = list(env_gpus_str.split(",")) if env_gpus_str else []
selected_gpus = [0]
# Always rely on CUDA_VISIBLE_DEVICES if specific GPU ID(s) are specified.
# As far as Pytorch Lightning is concerned, we always use all available GPUs
# (possibly filtered by CUDA_VISIBLE_DEVICES).
devices = -1
if len(env_gpus) > 0:
# CUDA_VISIBLE_DEVICES was set already, e.g. within SLURM srun or higher-level script.
n_gpus = len(env_gpus)
else:
selected_gpus = list(args.gpu.split(","))
n_gpus = len(selected_gpus)
os.environ["CUDA_VISIBLE_DEVICES"] = args.gpu
import pytorch_lightning as pl
import torch
from pytorch_lightning import Trainer
from pytorch_lightning.callbacks import LearningRateMonitor, ModelCheckpoint
from pytorch_lightning.loggers import CSVLogger, TensorBoardLogger, WandbLogger
from pytorch_lightning.utilities.rank_zero import rank_zero_only
if args.typecheck:
from jaxtyping import install_import_hook
install_import_hook("mvadapter", "typeguard.typechecked")
from mvadapter.systems.base import BaseSystem
from mvadapter.utils.callbacks import (
CodeSnapshotCallback,
ConfigSnapshotCallback,
CustomProgressBar,
ProgressCallback,
)
from mvadapter.utils.config import ExperimentConfig, load_config
from mvadapter.utils.core import find
from mvadapter.utils.misc import get_rank, time_recorder
from mvadapter.utils.typing import Optional
logger = logging.getLogger("pytorch_lightning")
if args.verbose:
logger.setLevel(logging.DEBUG)
if args.benchmark:
time_recorder.enable(True)
for handler in logger.handlers:
if handler.stream == sys.stderr: # type: ignore
handler.setFormatter(logging.Formatter("%(levelname)s %(message)s"))
handler.addFilter(ColoredFilter())
# parse YAML config to OmegaConf
cfg: ExperimentConfig
cfg = load_config(args.config, cli_args=extras, n_gpus=n_gpus)
dm = find(cfg.data_cls)(cfg.data)
system: BaseSystem = find(cfg.system_cls)(
cfg.system, resumed=cfg.resume is not None
)
system.set_save_dir(os.path.join(cfg.trial_dir, "save"))
callbacks = []
if args.train:
callbacks += [
ModelCheckpoint(
dirpath=os.path.join(cfg.trial_dir, "ckpts"), **cfg.checkpoint
),
LearningRateMonitor(logging_interval="step"),
# CodeSnapshotCallback(
# os.path.join(cfg.trial_dir, "code"), use_version=False
# ),
ConfigSnapshotCallback(
args.config,
cfg,
os.path.join(cfg.trial_dir, "configs"),
use_version=False,
),
CustomProgressBar(refresh_rate=1),
]
def write_to_text(file, lines):
with open(file, "w") as f:
for line in lines:
f.write(line + "\n")
loggers = []
if args.train:
# make tensorboard logging dir to suppress warning
rank_zero_only(
lambda: os.makedirs(os.path.join(cfg.trial_dir, "tb_logs"), exist_ok=True)
)()
loggers += [
TensorBoardLogger(cfg.trial_dir, name="tb_logs"),
]
if args.wandb:
wandb_logger = WandbLogger(
project="MV-Adapter", name=f"{cfg.name}-{cfg.tag}"
)
system._wandb_logger = wandb_logger
loggers += [wandb_logger]
rank_zero_only(
lambda: write_to_text(
os.path.join(cfg.trial_dir, "cmd.txt"),
["python " + " ".join(sys.argv), str(args)],
)
)()
trainer = Trainer(
callbacks=callbacks,
logger=loggers,
inference_mode=False,
accelerator="gpu",
devices=devices,
**cfg.trainer,
)
# set a different seed for each device
# NOTE: use trainer.global_rank instead of get_rank() to avoid getting the local rank
pl.seed_everything(cfg.seed + trainer.global_rank, workers=True)
def set_system_status(system: BaseSystem, ckpt_path: Optional[str]):
if ckpt_path is None:
return
ckpt = torch.load(ckpt_path, map_location="cpu")
system.set_resume_status(ckpt["epoch"], ckpt["global_step"])
if args.train:
trainer.fit(system, datamodule=dm, ckpt_path=cfg.resume)
trainer.test(system, datamodule=dm)
elif args.validate:
# manually set epoch and global_step as they cannot be automatically resumed
set_system_status(system, cfg.resume)
trainer.validate(system, datamodule=dm, ckpt_path=cfg.resume)
elif args.test:
# manually set epoch and global_step as they cannot be automatically resumed
set_system_status(system, cfg.resume)
trainer.test(system, datamodule=dm, ckpt_path=cfg.resume)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--config", required=True, help="path to config file")
parser.add_argument(
"--gpu",
default="0",
help="GPU(s) to be used. 0 means use the 1st available GPU. "
"1,2 means use the 2nd and 3rd available GPU. "
"If CUDA_VISIBLE_DEVICES is set before calling `launch.py`, "
"this argument is ignored and all available GPUs are always used.",
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--train", action="store_true")
group.add_argument("--validate", action="store_true")
group.add_argument("--test", action="store_true")
parser.add_argument("--wandb", action="store_true", help="if true, log to wandb")
parser.add_argument(
"--verbose", action="store_true", help="if true, set logging level to DEBUG"
)
parser.add_argument(
"--benchmark",
action="store_true",
help="if true, set to benchmark mode to record running times",
)
parser.add_argument(
"--typecheck",
action="store_true",
help="whether to enable dynamic type checking",
)
args, extras = parser.parse_known_args()
main(args, extras)
@@ -1,561 +0,0 @@
import json
import os
import random
from dataclasses import dataclass, field
import cv2
import numpy as np
import pytorch_lightning as pl
import torch
import torch.nn.functional as F
from PIL import Image
from torch.utils.data import DataLoader, Dataset
from ..utils.config import parse_structured
from ..utils.geometry import (
get_plucker_embeds_from_cameras,
get_plucker_embeds_from_cameras_ortho,
get_position_map_from_depth,
get_position_map_from_depth_ortho,
)
from ..utils.typing import *
def _parse_scene_list_single(scene_list_path: str, root_data_dir: str):
all_scenes = []
if scene_list_path.endswith(".json"):
with open(scene_list_path) as f:
for p in json.loads(f.read()):
if "/" in p:
all_scenes.append(os.path.join(root_data_dir, p))
else:
all_scenes.append(os.path.join(root_data_dir, p[:2], p))
elif scene_list_path.endswith(".txt"):
with open(scene_list_path) as f:
for p in f.readlines():
p = p.strip()
if "/" in p:
all_scenes.append(os.path.join(root_data_dir, p))
else:
all_scenes.append(os.path.join(root_data_dir, p[:2], p))
else:
raise NotImplementedError
return all_scenes
def _parse_scene_list(
scene_list_path: Union[str, List[str]], root_data_dir: Union[str, List[str]]
):
all_scenes = []
if isinstance(scene_list_path, str):
scene_list_path = [scene_list_path]
if isinstance(root_data_dir, str):
root_data_dir = [root_data_dir]
for scene_list_path_, root_data_dir_ in zip(scene_list_path, root_data_dir):
all_scenes += _parse_scene_list_single(scene_list_path_, root_data_dir_)
return all_scenes
def _parse_reference_scene_list(reference_scenes: List[str], all_scenes: List[str]):
all_ids = set(scene.split("/")[-1] for scene in all_scenes)
ref_ids = set(scene.split("/")[-1] for scene in reference_scenes)
common_ids = ref_ids.intersection(all_ids)
all_scenes = [scene for scene in all_scenes if scene.split("/")[-1] in common_ids]
all_ids = {scene.split("/")[-1]: idx for idx, scene in enumerate(all_scenes)}
ref_scenes = [
scene for scene in reference_scenes if scene.split("/")[-1] in all_ids
]
sorted_ref_scenes = sorted(ref_scenes, key=lambda x: all_ids[x.split("/")[-1]])
scene2ref = {
scene: ref_scene for scene, ref_scene in zip(all_scenes, sorted_ref_scenes)
}
return all_scenes, scene2ref
@dataclass
class MultiviewDataModuleConfig:
root_dir: Any = ""
scene_list: Any = ""
image_suffix: str = "webp"
background_color: Union[str, float] = "gray"
image_names: List[str] = field(default_factory=lambda: [])
image_modality: str = "render"
num_views: int = 1
random_view_list: Optional[List[List[int]]] = None
prompt_db_path: Optional[str] = None
return_prompt: bool = False
use_empty_prompt: bool = False
prompt_prefix: Optional[Any] = None
return_one_prompt: bool = True
projection_type: str = "ORTHO"
# source conditions
source_image_modality: Any = "position"
use_camera_space_normal: bool = False
position_offset: float = 0.5
position_scale: float = 1.0
plucker_offset: float = 1.0
plucker_scale: float = 2.0
# reference image
reference_root_dir: Optional[Any] = None
reference_scene_list: Optional[Any] = None
reference_image_modality: str = "render"
reference_image_names: List[str] = field(default_factory=lambda: [])
reference_augment_resolutions: Optional[List[int]] = None
reference_mask_aug: bool = False
repeat: int = 1 # for debugging purpose
train_indices: Optional[Tuple[Any, Any]] = None
val_indices: Optional[Tuple[Any, Any]] = None
test_indices: Optional[Tuple[Any, Any]] = None
height: int = 768
width: int = 768
batch_size: int = 1
eval_batch_size: int = 1
num_workers: int = 16
class MultiviewDataset(Dataset):
def __init__(self, cfg: Any, split: str = "train") -> None:
super().__init__()
assert split in ["train", "val", "test"]
self.cfg: MultiviewDataModuleConfig = cfg
self.all_scenes = _parse_scene_list(self.cfg.scene_list, self.cfg.root_dir)
if (
self.cfg.reference_root_dir is not None
and self.cfg.reference_scene_list is not None
):
reference_scenes = _parse_scene_list(
self.cfg.reference_scene_list, self.cfg.reference_root_dir
)
self.all_scenes, self.reference_scenes = _parse_reference_scene_list(
reference_scenes, self.all_scenes
)
else:
self.reference_scenes = None
self.split = split
if self.split == "train" and self.cfg.train_indices is not None:
self.all_scenes = self.all_scenes[
self.cfg.train_indices[0] : self.cfg.train_indices[1]
]
self.all_scenes = self.all_scenes * self.cfg.repeat
elif self.split == "val" and self.cfg.val_indices is not None:
self.all_scenes = self.all_scenes[
self.cfg.val_indices[0] : self.cfg.val_indices[1]
]
elif self.split == "test" and self.cfg.test_indices is not None:
self.all_scenes = self.all_scenes[
self.cfg.test_indices[0] : self.cfg.test_indices[1]
]
if self.cfg.prompt_db_path is not None:
self.prompt_db = json.load(open(self.cfg.prompt_db_path))
else:
self.prompt_db = None
def __len__(self):
return len(self.all_scenes)
def get_bg_color(self, bg_color):
if bg_color == "white":
bg_color = np.array([1.0, 1.0, 1.0], dtype=np.float32)
elif bg_color == "black":
bg_color = np.array([0.0, 0.0, 0.0], dtype=np.float32)
elif bg_color == "gray":
bg_color = np.array([0.5, 0.5, 0.5], dtype=np.float32)
elif bg_color == "random":
bg_color = np.random.rand(3)
elif bg_color == "random_gray":
bg_color = random.uniform(0.3, 0.7)
bg_color = np.array([bg_color] * 3, dtype=np.float32)
elif isinstance(bg_color, float):
bg_color = np.array([bg_color] * 3, dtype=np.float32)
elif isinstance(bg_color, list) or isinstance(bg_color, tuple):
bg_color = np.array(bg_color, dtype=np.float32)
else:
raise NotImplementedError
return bg_color
def load_image(
self,
image: Union[str, Image.Image],
height: int,
width: int,
background_color: torch.Tensor,
rescale: bool = False,
mask_aug: bool = False,
):
if isinstance(image, str):
image = Image.open(image)
image = image.resize((width, height))
image = torch.from_numpy(np.array(image)).float() / 255.0
if mask_aug:
alpha = image[:, :, 3] # Extract alpha channel
h, w = alpha.shape
y_indices, x_indices = torch.where(alpha > 0.5)
if len(y_indices) > 0 and len(x_indices) > 0:
idx = torch.randint(len(y_indices), (1,)).item()
y_center = y_indices[idx].item()
x_center = x_indices[idx].item()
mask_h = random.randint(h // 8, h // 4)
mask_w = random.randint(w // 8, w // 4)
y1 = max(0, y_center - mask_h // 2)
y2 = min(h, y_center + mask_h // 2)
x1 = max(0, x_center - mask_w // 2)
x2 = min(w, x_center + mask_w // 2)
alpha[y1:y2, x1:x2] = 0.0
image[:, :, 3] = alpha
image = image[:, :, :3] * image[:, :, 3:4] + background_color * (
1 - image[:, :, 3:4]
)
if rescale:
image = image * 2.0 - 1.0
return image
def load_normal_image(
self,
path,
height,
width,
background_color,
camera_space: bool = False,
c2w: Optional[torch.FloatTensor] = None,
):
image = Image.open(path).resize((width, height), resample=Image.NEAREST)
image = torch.from_numpy(np.array(image)).float() / 255.0
alpha = image[:, :, 3:4]
image = image[:, :, :3]
if camera_space:
w2c = torch.linalg.inv(c2w)[:3, :3]
image = (
F.normalize(((image * 2 - 1)[:, :, None, :] * w2c).sum(-1), dim=-1)
* 0.5
+ 0.5
)
image = image * alpha + background_color * (1 - alpha)
return image
def load_depth(self, path, height, width):
depth = cv2.imread(path, cv2.IMREAD_UNCHANGED)
depth = cv2.resize(depth, (width, height), interpolation=cv2.INTER_NEAREST)
depth = torch.from_numpy(depth[..., 0:1]).float()
mask = torch.ones_like(depth)
mask[depth > 1000.0] = 0.0 # depth = 65535 is the invalid value
depth[~(mask > 0.5)] = 0.0
return depth, mask
def retrieve_prompt(self, scene_dir):
assert self.prompt_db is not None
source_id = os.path.basename(scene_dir)
return self.prompt_db.get(source_id, "")
def __getitem__(self, index):
background_color = torch.as_tensor(self.get_bg_color(self.cfg.background_color))
scene_dir = self.all_scenes[index]
with open(os.path.join(scene_dir, "meta.json")) as f:
meta = json.load(f)
name2loc = {loc["index"]: loc for loc in meta["locations"]}
# target multi-view images
image_paths = [
os.path.join(
scene_dir, f"{self.cfg.image_modality}_{f}.{self.cfg.image_suffix}"
)
for f in self.cfg.image_names
]
images = [
self.load_image(
p,
height=self.cfg.height,
width=self.cfg.width,
background_color=background_color,
)
for p in image_paths
]
images = torch.stack(images, dim=0).permute(0, 3, 1, 2)
# camera
c2w = [
torch.as_tensor(name2loc[name]["transform_matrix"])
for name in self.cfg.image_names
]
c2w = torch.stack(c2w, dim=0)
if self.cfg.projection_type == "PERSP":
camera_angle_x = (
meta.get("camera_angle_x", None)
or meta["locations"][0]["camera_angle_x"]
)
focal_length = 0.5 * self.cfg.width / np.tan(0.5 * camera_angle_x)
intrinsics = (
torch.as_tensor(
[
[focal_length, 0.0, 0.5 * self.cfg.width],
[0.0, focal_length, 0.5 * self.cfg.height],
[0.0, 0.0, 1.0],
]
)
.unsqueeze(0)
.float()
.repeat(len(self.cfg.image_names), 1, 1)
)
elif self.cfg.projection_type == "ORTHO":
ortho_scale = (
meta.get("ortho_scale", None) or meta["locations"][0]["ortho_scale"]
)
# source conditions
source_image_modality = self.cfg.source_image_modality
if isinstance(source_image_modality, str):
source_image_modality = [source_image_modality]
source_images = []
for modality in source_image_modality:
if modality == "position":
depth_masks = [
self.load_depth(
os.path.join(scene_dir, f"depth_{f}.exr"),
self.cfg.height,
self.cfg.width,
)
for f in self.cfg.image_names
]
depths = torch.stack([d for d, _ in depth_masks])
masks = torch.stack([m for _, m in depth_masks])
c2w_ = c2w.clone()
c2w_[:, :, 1:3] *= -1
if self.cfg.projection_type == "PERSP":
position_maps = get_position_map_from_depth(
depths,
masks,
intrinsics,
c2w_,
image_wh=(self.cfg.width, self.cfg.height),
)
elif self.cfg.projection_type == "ORTHO":
position_maps = get_position_map_from_depth_ortho(
depths,
masks,
c2w_,
ortho_scale,
image_wh=(self.cfg.width, self.cfg.height),
)
position_maps = (
(position_maps + self.cfg.position_offset) / self.cfg.position_scale
).clamp(0.0, 1.0)
source_images.append(position_maps)
elif modality == "normal":
normal_maps = [
self.load_normal_image(
os.path.join(
scene_dir, f"{modality}_{f}.{self.cfg.image_suffix}"
),
height=self.cfg.height,
width=self.cfg.width,
background_color=background_color,
camera_space=self.cfg.use_camera_space_normal,
c2w=c,
)
for c, f in zip(c2w, self.cfg.image_names)
]
source_images.append(torch.stack(normal_maps, dim=0))
elif modality == "plucker":
if self.cfg.projection_type == "ORTHO":
plucker_embed = get_plucker_embeds_from_cameras_ortho(
c2w, [ortho_scale] * len(c2w), self.cfg.width
)
elif self.cfg.projection_type == "PERSP":
plucker_embed = get_plucker_embeds_from_cameras(
c2w, [camera_angle_x] * len(c2w), self.cfg.width
)
else:
raise NotImplementedError
plucker_embed = plucker_embed.permute(0, 2, 3, 1)
plucker_embed = (
(plucker_embed + self.cfg.plucker_offset) / self.cfg.plucker_scale
).clamp(0.0, 1.0)
source_images.append(plucker_embed)
else:
raise NotImplementedError
source_images = torch.cat(source_images, dim=-1).permute(0, 3, 1, 2)
rv = {"rgb": images, "c2w": c2w, "source_rgb": source_images}
num_images = len(self.cfg.image_names)
# prompt
if self.cfg.return_prompt:
if self.cfg.use_empty_prompt:
prompt = ""
else:
prompt = self.retrieve_prompt(scene_dir)
prompts = [prompt] * num_images
if self.cfg.prompt_prefix is not None:
prompt_prefix = self.cfg.prompt_prefix
if isinstance(prompt_prefix, str):
prompt_prefix = [prompt_prefix] * num_images
for i, prompt in enumerate(prompts):
prompts[i] = f"{prompt_prefix[i]} {prompt}"
if self.cfg.return_one_prompt:
rv.update({"prompts": prompts[0]})
else:
rv.update({"prompts": prompts})
# reference image
if self.reference_scenes is not None:
reference_scene_dir = self.reference_scenes[scene_dir]
reference_image_paths = [
os.path.join(
reference_scene_dir,
f"{self.cfg.reference_image_modality}_{f}.{self.cfg.image_suffix}",
)
for f in self.cfg.reference_image_names
]
reference_image_path = random.choice(reference_image_paths)
if self.cfg.reference_augment_resolutions is None:
reference_image = self.load_image(
reference_image_path,
height=self.cfg.height,
width=self.cfg.width,
background_color=background_color,
mask_aug=self.cfg.reference_mask_aug,
).permute(2, 0, 1)
rv.update({"reference_rgb": reference_image})
else:
random_resolution = random.choice(
self.cfg.reference_augment_resolutions
)
reference_image_ = Image.open(reference_image_path).resize(
(random_resolution, random_resolution)
)
reference_image = self.load_image(
reference_image_,
height=self.cfg.height,
width=self.cfg.width,
background_color=background_color,
mask_aug=self.cfg.reference_mask_aug,
).permute(2, 0, 1)
rv.update({"reference_rgb": reference_image})
return rv
def collate(self, batch):
batch = torch.utils.data.default_collate(batch)
pack = lambda t: t.view(-1, *t.shape[2:])
if self.cfg.random_view_list is not None:
indices = random.choice(self.cfg.random_view_list)
else:
indices = list(range(self.cfg.num_views))
num_views = len(indices)
for k in batch.keys():
if k in ["rgb", "source_rgb", "c2w"]:
batch[k] = batch[k][:, indices]
batch[k] = pack(batch[k])
for k in ["prompts"]:
if not self.cfg.return_one_prompt:
batch[k] = [item for pair in zip(*batch[k]) for item in pair]
batch.update(
{
"num_views": num_views,
# For SDXL
"original_size": (self.cfg.height, self.cfg.width),
"target_size": (self.cfg.height, self.cfg.width),
"crops_coords_top_left": (0, 0),
}
)
return batch
class MultiviewDataModule(pl.LightningDataModule):
cfg: MultiviewDataModuleConfig
def __init__(self, cfg: Optional[Union[dict, DictConfig]] = None) -> None:
super().__init__()
self.cfg = parse_structured(MultiviewDataModuleConfig, cfg)
def setup(self, stage=None) -> None:
if stage in [None, "fit"]:
self.train_dataset = MultiviewDataset(self.cfg, "train")
if stage in [None, "fit", "validate"]:
self.val_dataset = MultiviewDataset(self.cfg, "val")
if stage in [None, "test", "predict"]:
self.test_dataset = MultiviewDataset(self.cfg, "test")
def prepare_data(self):
pass
def train_dataloader(self) -> DataLoader:
return DataLoader(
self.train_dataset,
batch_size=self.cfg.batch_size,
num_workers=self.cfg.num_workers,
shuffle=True,
collate_fn=self.train_dataset.collate,
)
def val_dataloader(self) -> DataLoader:
return DataLoader(
self.val_dataset,
batch_size=self.cfg.eval_batch_size,
num_workers=self.cfg.num_workers,
shuffle=False,
collate_fn=self.val_dataset.collate,
)
def test_dataloader(self) -> DataLoader:
return DataLoader(
self.test_dataset,
batch_size=self.cfg.eval_batch_size,
num_workers=self.cfg.num_workers,
shuffle=False,
collate_fn=self.test_dataset.collate,
)
def predict_dataloader(self) -> DataLoader:
return self.test_dataloader()
if __name__ == "__main__":
import torchvision
from omegaconf import OmegaConf
config_file = "configs/view-guidance/mvadapter_t2mv_flux.yaml"
data_cfg = OmegaConf.load(config_file)["data"]
cfg: MultiviewDataModuleConfig = MultiviewDataModuleConfig(**data_cfg)
data_module = MultiviewDataModule(cfg)
data_module.setup()
for batch in data_module.test_dataloader():
# ref_rgb = batch["reference_rgb"] # bchw
rgb = batch["rgb"]
source_rgb = batch["source_rgb"]
print(batch["prompts"])
print(rgb.shape, source_rgb.shape)
torchvision.utils.save_image(rgb[:, :3], "debug_rgb.png")
torchvision.utils.save_image(source_rgb[:, :3], "debug_source_rgb.png")
exit(0)
@@ -1,777 +0,0 @@
# Copyright 2024 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
from typing import Any, Callable, Dict, List, Optional, Union
import PIL
import torch
from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback
from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
from diffusers.models import AutoencoderKL, T2IAdapter, UNet2DConditionModel
from diffusers.pipelines.stable_diffusion.pipeline_output import (
StableDiffusionPipelineOutput,
)
from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import (
StableDiffusionPipeline,
rescale_noise_cfg,
retrieve_timesteps,
)
from diffusers.pipelines.stable_diffusion.safety_checker import (
StableDiffusionSafetyChecker,
)
from diffusers.schedulers import KarrasDiffusionSchedulers
from diffusers.utils import deprecate, is_torch_xla_available, logging
from diffusers.utils.torch_utils import randn_tensor
from transformers import (
CLIPImageProcessor,
CLIPTextModel,
CLIPTokenizer,
CLIPVisionModelWithProjection,
)
from ..loaders import CustomAdapterMixin
from ..models.attention_processor import (
DecoupledMVRowSelfAttnProcessor2_0,
set_unet_2d_condition_attn_processor,
)
if is_torch_xla_available():
import torch_xla.core.xla_model as xm
XLA_AVAILABLE = True
else:
XLA_AVAILABLE = False
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
def retrieve_latents(
encoder_output: torch.Tensor,
generator: Optional[torch.Generator] = None,
sample_mode: str = "sample",
):
if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
return encoder_output.latent_dist.sample(generator)
elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
return encoder_output.latent_dist.mode()
elif hasattr(encoder_output, "latents"):
return encoder_output.latents
else:
raise AttributeError("Could not access latents of provided encoder_output")
class MVAdapterI2MVSDPipeline(StableDiffusionPipeline, CustomAdapterMixin):
def __init__(
self,
vae: AutoencoderKL,
text_encoder: CLIPTextModel,
tokenizer: CLIPTokenizer,
unet: UNet2DConditionModel,
scheduler: KarrasDiffusionSchedulers,
safety_checker: StableDiffusionSafetyChecker,
feature_extractor: CLIPImageProcessor,
image_encoder: CLIPVisionModelWithProjection = None,
requires_safety_checker: bool = False,
):
super().__init__(
vae=vae,
text_encoder=text_encoder,
tokenizer=tokenizer,
unet=unet,
scheduler=scheduler,
safety_checker=safety_checker,
feature_extractor=feature_extractor,
image_encoder=image_encoder,
requires_safety_checker=requires_safety_checker,
)
self.control_image_processor = VaeImageProcessor(
vae_scale_factor=self.vae_scale_factor,
do_convert_rgb=True,
do_normalize=False,
)
# Copied from diffusers.pipelines.stable_diffusion_xl.pipeline_stable_diffusion_xl_img2img.prepare_latents
def prepare_image_latents(
self,
image,
timestep,
batch_size,
num_images_per_prompt,
dtype,
device,
generator=None,
add_noise=True,
):
if not isinstance(image, (torch.Tensor, PIL.Image.Image, list)):
raise ValueError(
f"`image` has to be of type `torch.Tensor`, `PIL.Image.Image` or list but is {type(image)}"
)
image = image.to(device=device, dtype=dtype)
batch_size = batch_size * num_images_per_prompt
if image.shape[1] == 4:
init_latents = image
else:
if isinstance(generator, list) and len(generator) != batch_size:
raise ValueError(
f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
f" size of {batch_size}. Make sure the batch size matches the length of the generators."
)
elif isinstance(generator, list):
init_latents = [
retrieve_latents(
self.vae.encode(image[i : i + 1]), generator=generator[i]
)
for i in range(batch_size)
]
init_latents = torch.cat(init_latents, dim=0)
else:
init_latents = retrieve_latents(
self.vae.encode(image), generator=generator
)
init_latents = self.vae.config.scaling_factor * init_latents
if (
batch_size > init_latents.shape[0]
and batch_size % init_latents.shape[0] == 0
):
# expand init_latents for batch_size
additional_image_per_prompt = batch_size // init_latents.shape[0]
init_latents = torch.cat(
[init_latents] * additional_image_per_prompt, dim=0
)
elif (
batch_size > init_latents.shape[0]
and batch_size % init_latents.shape[0] != 0
):
raise ValueError(
f"Cannot duplicate `image` of batch size {init_latents.shape[0]} to {batch_size} text prompts."
)
else:
init_latents = torch.cat([init_latents], dim=0)
if add_noise:
shape = init_latents.shape
noise = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
# get latents
init_latents = self.scheduler.add_noise(init_latents, noise, timestep)
latents = init_latents
return latents
def prepare_control_image(
self,
image,
width,
height,
batch_size,
num_images_per_prompt,
device,
dtype,
do_classifier_free_guidance=False,
num_empty_images=0, # for concat in batch like ImageDream
):
assert hasattr(
self, "control_image_processor"
), "control_image_processor is not initialized"
image = self.control_image_processor.preprocess(
image, height=height, width=width
).to(dtype=torch.float32)
if num_empty_images > 0:
image = torch.cat(
[image, torch.zeros_like(image[:num_empty_images])], dim=0
)
image_batch_size = image.shape[0]
if image_batch_size == 1:
repeat_by = batch_size
else:
# image batch size is the same as prompt batch size
repeat_by = num_images_per_prompt # always 1 for control image
image = image.repeat_interleave(repeat_by, dim=0)
image = image.to(device=device, dtype=dtype)
if do_classifier_free_guidance:
image = torch.cat([image] * 2)
return image
@torch.no_grad()
def __call__(
self,
prompt: Union[str, List[str]] = None,
height: Optional[int] = None,
width: Optional[int] = None,
num_inference_steps: int = 50,
timesteps: List[int] = None,
sigmas: List[float] = None,
guidance_scale: float = 7.5,
negative_prompt: Optional[Union[str, List[str]]] = None,
num_images_per_prompt: Optional[int] = 1,
eta: float = 0.0,
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
latents: Optional[torch.Tensor] = None,
prompt_embeds: Optional[torch.Tensor] = None,
negative_prompt_embeds: Optional[torch.Tensor] = None,
ip_adapter_image: Optional[PipelineImageInput] = None,
ip_adapter_image_embeds: Optional[List[torch.Tensor]] = None,
output_type: Optional[str] = "pil",
return_dict: bool = True,
cross_attention_kwargs: Optional[Dict[str, Any]] = None,
guidance_rescale: float = 0.0,
clip_skip: Optional[int] = None,
callback_on_step_end: Optional[
Union[
Callable[[int, int, Dict], None],
PipelineCallback,
MultiPipelineCallbacks,
]
] = None,
callback_on_step_end_tensor_inputs: List[str] = ["latents"],
# NEW
mv_scale: float = 1.0,
# Camera or geometry condition
control_image: Optional[PipelineImageInput] = None,
control_conditioning_scale: Optional[float] = 1.0,
control_conditioning_factor: float = 1.0,
# Image condition
reference_image: Optional[PipelineImageInput] = None,
reference_conditioning_scale: Optional[float] = 1.0,
# Optional. controlnet
controlnet_image: Optional[PipelineImageInput] = None,
controlnet_conditioning_scale: Optional[float] = 1.0,
**kwargs,
):
r"""
The call function to the pipeline for generation.
Args:
prompt (`str` or `List[str]`, *optional*):
The prompt or prompts to guide image generation. If not defined, you need to pass `prompt_embeds`.
height (`int`, *optional*, defaults to `self.unet.config.sample_size * self.vae_scale_factor`):
The height in pixels of the generated image.
width (`int`, *optional*, defaults to `self.unet.config.sample_size * self.vae_scale_factor`):
The width in pixels of the generated image.
num_inference_steps (`int`, *optional*, defaults to 50):
The number of denoising steps. More denoising steps usually lead to a higher quality image at the
expense of slower inference.
timesteps (`List[int]`, *optional*):
Custom timesteps to use for the denoising process with schedulers which support a `timesteps` argument
in their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is
passed will be used. Must be in descending order.
sigmas (`List[float]`, *optional*):
Custom sigmas to use for the denoising process with schedulers which support a `sigmas` argument in
their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is passed
will be used.
guidance_scale (`float`, *optional*, defaults to 7.5):
A higher guidance scale value encourages the model to generate images closely linked to the text
`prompt` at the expense of lower image quality. Guidance scale is enabled when `guidance_scale > 1`.
negative_prompt (`str` or `List[str]`, *optional*):
The prompt or prompts to guide what to not include in image generation. If not defined, you need to
pass `negative_prompt_embeds` instead. Ignored when not using guidance (`guidance_scale < 1`).
num_images_per_prompt (`int`, *optional*, defaults to 1):
The number of images to generate per prompt.
eta (`float`, *optional*, defaults to 0.0):
Corresponds to parameter eta (η) from the [DDIM](https://arxiv.org/abs/2010.02502) paper. Only applies
to the [`~schedulers.DDIMScheduler`], and is ignored in other schedulers.
generator (`torch.Generator` or `List[torch.Generator]`, *optional*):
A [`torch.Generator`](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make
generation deterministic.
latents (`torch.Tensor`, *optional*):
Pre-generated noisy latents sampled from a Gaussian distribution, to be used as inputs for image
generation. Can be used to tweak the same generation with different prompts. If not provided, a latents
tensor is generated by sampling using the supplied random `generator`.
prompt_embeds (`torch.Tensor`, *optional*):
Pre-generated text embeddings. Can be used to easily tweak text inputs (prompt weighting). If not
provided, text embeddings are generated from the `prompt` input argument.
negative_prompt_embeds (`torch.Tensor`, *optional*):
Pre-generated negative text embeddings. Can be used to easily tweak text inputs (prompt weighting). If
not provided, `negative_prompt_embeds` are generated from the `negative_prompt` input argument.
ip_adapter_image: (`PipelineImageInput`, *optional*): Optional image input to work with IP Adapters.
ip_adapter_image_embeds (`List[torch.Tensor]`, *optional*):
Pre-generated image embeddings for IP-Adapter. It should be a list of length same as number of
IP-adapters. Each element should be a tensor of shape `(batch_size, num_images, emb_dim)`. It should
contain the negative image embedding if `do_classifier_free_guidance` is set to `True`. If not
provided, embeddings are computed from the `ip_adapter_image` input argument.
output_type (`str`, *optional*, defaults to `"pil"`):
The output format of the generated image. Choose between `PIL.Image` or `np.array`.
return_dict (`bool`, *optional*, defaults to `True`):
Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a
plain tuple.
cross_attention_kwargs (`dict`, *optional*):
A kwargs dictionary that if specified is passed along to the [`AttentionProcessor`] as defined in
[`self.processor`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
guidance_rescale (`float`, *optional*, defaults to 0.0):
Guidance rescale factor from [Common Diffusion Noise Schedules and Sample Steps are
Flawed](https://arxiv.org/pdf/2305.08891.pdf). Guidance rescale factor should fix overexposure when
using zero terminal SNR.
clip_skip (`int`, *optional*):
Number of layers to be skipped from CLIP while computing the prompt embeddings. A value of 1 means that
the output of the pre-final layer will be used for computing the prompt embeddings.
callback_on_step_end (`Callable`, `PipelineCallback`, `MultiPipelineCallbacks`, *optional*):
A function or a subclass of `PipelineCallback` or `MultiPipelineCallbacks` that is called at the end of
each denoising step during the inference. with the following arguments: `callback_on_step_end(self:
DiffusionPipeline, step: int, timestep: int, callback_kwargs: Dict)`. `callback_kwargs` will include a
list of all tensors as specified by `callback_on_step_end_tensor_inputs`.
callback_on_step_end_tensor_inputs (`List`, *optional*):
The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list
will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the
`._callback_tensor_inputs` attribute of your pipeline class.
Examples:
Returns:
[`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`:
If `return_dict` is `True`, [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] is returned,
otherwise a `tuple` is returned where the first element is a list with the generated images and the
second element is a list of `bool`s indicating whether the corresponding generated image contains
"not-safe-for-work" (nsfw) content.
"""
callback = kwargs.pop("callback", None)
callback_steps = kwargs.pop("callback_steps", None)
if callback is not None:
deprecate(
"callback",
"1.0.0",
"Passing `callback` as an input argument to `__call__` is deprecated, consider using `callback_on_step_end`",
)
if callback_steps is not None:
deprecate(
"callback_steps",
"1.0.0",
"Passing `callback_steps` as an input argument to `__call__` is deprecated, consider using `callback_on_step_end`",
)
if isinstance(callback_on_step_end, (PipelineCallback, MultiPipelineCallbacks)):
callback_on_step_end_tensor_inputs = callback_on_step_end.tensor_inputs
# 0. Default height and width to unet
height = height or self.unet.config.sample_size * self.vae_scale_factor
width = width or self.unet.config.sample_size * self.vae_scale_factor
# to deal with lora scaling and other possible forward hooks
# 1. Check inputs. Raise error if not correct
self.check_inputs(
prompt,
height,
width,
callback_steps,
negative_prompt,
prompt_embeds,
negative_prompt_embeds,
ip_adapter_image,
ip_adapter_image_embeds,
callback_on_step_end_tensor_inputs,
)
self._guidance_scale = guidance_scale
self._guidance_rescale = guidance_rescale
self._clip_skip = clip_skip
self._cross_attention_kwargs = cross_attention_kwargs
self._interrupt = False
# 2. Define call parameters
if prompt is not None and isinstance(prompt, str):
batch_size = 1
elif prompt is not None and isinstance(prompt, list):
batch_size = len(prompt)
else:
batch_size = prompt_embeds.shape[0]
device = self._execution_device
# 3. Encode input prompt
lora_scale = (
self.cross_attention_kwargs.get("scale", None)
if self.cross_attention_kwargs is not None
else None
)
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
prompt,
device,
num_images_per_prompt,
self.do_classifier_free_guidance,
negative_prompt,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
lora_scale=lora_scale,
clip_skip=self.clip_skip,
)
# For classifier free guidance, we need to do two forward passes.
# Here we concatenate the unconditional and text embeddings into a single batch
# to avoid doing two forward passes
if self.do_classifier_free_guidance:
prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])
if ip_adapter_image is not None or ip_adapter_image_embeds is not None:
image_embeds = self.prepare_ip_adapter_image_embeds(
ip_adapter_image,
ip_adapter_image_embeds,
device,
batch_size * num_images_per_prompt,
self.do_classifier_free_guidance,
)
# 4. Prepare timesteps
timesteps, num_inference_steps = retrieve_timesteps(
self.scheduler, num_inference_steps, device, timesteps, sigmas
)
# 5. Prepare latent variables
num_channels_latents = self.unet.config.in_channels
latents = self.prepare_latents(
batch_size * num_images_per_prompt,
num_channels_latents,
height,
width,
prompt_embeds.dtype,
device,
generator,
latents,
)
# 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline
extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta)
# 6.1 Add image embeds for IP-Adapter
added_cond_kwargs = (
{"image_embeds": image_embeds}
if (ip_adapter_image is not None or ip_adapter_image_embeds is not None)
else None
)
# 6.2 Optionally get Guidance Scale Embedding
timestep_cond = None
if self.unet.config.time_cond_proj_dim is not None:
guidance_scale_tensor = torch.tensor(self.guidance_scale - 1).repeat(
batch_size * num_images_per_prompt
)
timestep_cond = self.get_guidance_scale_embedding(
guidance_scale_tensor, embedding_dim=self.unet.config.time_cond_proj_dim
).to(device=device, dtype=latents.dtype)
# Preprocess reference image
reference_image = self.image_processor.preprocess(reference_image)
reference_latents = self.prepare_image_latents(
reference_image,
timesteps[:1].repeat(batch_size * num_images_per_prompt), # no use
batch_size,
1,
prompt_embeds.dtype,
device,
generator,
add_noise=False,
)
ref_timesteps = torch.zeros_like(timesteps[0])
ref_hidden_states = {}
with torch.no_grad():
self.unet(
reference_latents,
ref_timesteps,
encoder_hidden_states=prompt_embeds[-1:],
cross_attention_kwargs={
"cache_hidden_states": ref_hidden_states,
"use_mv": False,
"use_ref": False,
},
return_dict=False,
)
ref_hidden_states = {
k: v.repeat_interleave(num_images_per_prompt, dim=0)
for k, v in ref_hidden_states.items()
}
if self.do_classifier_free_guidance:
ref_hidden_states = {
k: torch.cat([torch.zeros_like(v), v], dim=0)
for k, v in ref_hidden_states.items()
}
cross_attention_kwargs = {
"num_views": num_images_per_prompt,
"mv_scale": mv_scale,
"ref_hidden_states": {k: v.clone() for k, v in ref_hidden_states.items()},
"ref_scale": reference_conditioning_scale,
**(self.cross_attention_kwargs or {}),
}
# Preprocess control image
control_image_feature = self.prepare_control_image(
image=control_image,
width=width,
height=height,
batch_size=batch_size * num_images_per_prompt,
num_images_per_prompt=1, # NOTE: always 1 for control images
device=device,
dtype=latents.dtype,
do_classifier_free_guidance=self.do_classifier_free_guidance,
)
control_image_feature = control_image_feature.to(
device=device, dtype=latents.dtype
)
adapter_state = self.cond_encoder(control_image_feature)
for i, state in enumerate(adapter_state):
adapter_state[i] = state * control_conditioning_scale
# Preprocess controlnet image if provided
do_controlnet = controlnet_image is not None and hasattr(self, "controlnet")
if do_controlnet:
controlnet_image = self.prepare_control_image(
image=controlnet_image,
width=width,
height=height,
batch_size=batch_size * num_images_per_prompt,
num_images_per_prompt=1, # NOTE: always 1 for control images
device=device,
dtype=latents.dtype,
do_classifier_free_guidance=self.do_classifier_free_guidance,
)
controlnet_image = controlnet_image.to(device=device, dtype=latents.dtype)
# 7. Denoising loop
num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order
self._num_timesteps = len(timesteps)
with self.progress_bar(total=num_inference_steps) as progress_bar:
for i, t in enumerate(timesteps):
if self.interrupt:
continue
# expand the latents if we are doing classifier free guidance
latent_model_input = (
torch.cat([latents] * 2)
if self.do_classifier_free_guidance
else latents
)
latent_model_input = self.scheduler.scale_model_input(
latent_model_input, t
)
if i < int(num_inference_steps * control_conditioning_factor):
down_intrablock_additional_residuals = [
state.clone() for state in adapter_state
]
else:
down_intrablock_additional_residuals = None
unet_add_kwargs = {}
# Do controlnet if provided
if do_controlnet:
down_block_res_samples, mid_block_res_sample = self.controlnet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
controlnet_cond=controlnet_image,
conditioning_scale=controlnet_conditioning_scale,
guess_mode=False,
added_cond_kwargs=added_cond_kwargs,
return_dict=False,
)
unet_add_kwargs.update(
{
"down_block_additional_residuals": down_block_res_samples,
"mid_block_additional_residual": mid_block_res_sample,
}
)
# predict the noise residual
noise_pred = self.unet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
timestep_cond=timestep_cond,
cross_attention_kwargs=cross_attention_kwargs,
down_intrablock_additional_residuals=down_intrablock_additional_residuals,
added_cond_kwargs=added_cond_kwargs,
return_dict=False,
**unet_add_kwargs,
)[0]
# perform guidance
if self.do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
if self.do_classifier_free_guidance and self.guidance_rescale > 0.0:
# Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf
noise_pred = rescale_noise_cfg(
noise_pred,
noise_pred_text,
guidance_rescale=self.guidance_rescale,
)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(
noise_pred, t, latents, **extra_step_kwargs, return_dict=False
)[0]
if callback_on_step_end is not None:
callback_kwargs = {}
for k in callback_on_step_end_tensor_inputs:
callback_kwargs[k] = locals()[k]
callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
latents = callback_outputs.pop("latents", latents)
prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
negative_prompt_embeds = callback_outputs.pop(
"negative_prompt_embeds", negative_prompt_embeds
)
# call the callback, if provided
if i == len(timesteps) - 1 or (
(i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0
):
progress_bar.update()
if callback is not None and i % callback_steps == 0:
step_idx = i // getattr(self.scheduler, "order", 1)
callback(step_idx, t, latents)
if XLA_AVAILABLE:
xm.mark_step()
if not output_type == "latent":
image = self.vae.decode(
latents / self.vae.config.scaling_factor,
return_dict=False,
generator=generator,
)[0]
image, has_nsfw_concept = self.run_safety_checker(
image, device, prompt_embeds.dtype
)
else:
image = latents
has_nsfw_concept = None
if has_nsfw_concept is None:
do_denormalize = [True] * image.shape[0]
else:
do_denormalize = [not has_nsfw for has_nsfw in has_nsfw_concept]
image = self.image_processor.postprocess(
image, output_type=output_type, do_denormalize=do_denormalize
)
# Offload all models
self.maybe_free_model_hooks()
if not return_dict:
return (image, has_nsfw_concept)
return StableDiffusionPipelineOutput(
images=image, nsfw_content_detected=has_nsfw_concept
)
### NEW: adapters ###
def _init_custom_adapter(
self,
# Multi-view adapter
num_views: int = 1,
self_attn_processor: Any = DecoupledMVRowSelfAttnProcessor2_0,
# Condition encoder
cond_in_channels: int = 6,
# For training
copy_attn_weights: bool = True,
zero_init_module_keys: List[str] = [],
):
# Condition encoder
self.cond_encoder = T2IAdapter(
in_channels=cond_in_channels,
channels=self.unet.config.block_out_channels,
num_res_blocks=self.unet.config.layers_per_block,
downscale_factor=8,
)
# set custom attn processor for multi-view attention
self.unet: UNet2DConditionModel
set_unet_2d_condition_attn_processor(
self.unet,
set_self_attn_proc_func=lambda name, hs, cad, ap: self_attn_processor(
query_dim=hs,
inner_dim=hs,
num_views=num_views,
name=name,
use_mv=True,
use_ref=True,
),
set_cross_attn_proc_func=lambda name, hs, cad, ap: self_attn_processor(
query_dim=hs,
inner_dim=hs,
num_views=num_views,
name=name,
use_mv=False,
use_ref=False,
),
)
# copy decoupled attention weights from original unet
if copy_attn_weights:
state_dict = self.unet.state_dict()
for key in state_dict.keys():
if "_mv" in key:
compatible_key = key.replace("_mv", "").replace("processor.", "")
elif "_ref" in key:
compatible_key = key.replace("_ref", "").replace("processor.", "")
else:
compatible_key = key
is_zero_init_key = any([k in key for k in zero_init_module_keys])
if is_zero_init_key:
state_dict[key] = torch.zeros_like(state_dict[compatible_key])
else:
state_dict[key] = state_dict[compatible_key].clone()
self.unet.load_state_dict(state_dict)
def _load_custom_adapter(self, state_dict):
self.unet.load_state_dict(state_dict, strict=False)
self.cond_encoder.load_state_dict(state_dict, strict=False)
def _save_custom_adapter(
self,
include_keys: Optional[List[str]] = None,
exclude_keys: Optional[List[str]] = None,
):
def include_fn(k):
is_included = False
if include_keys is not None:
is_included = is_included or any([key in k for key in include_keys])
if exclude_keys is not None:
is_included = is_included and not any(
[key in k for key in exclude_keys]
)
return is_included
state_dict = {k: v for k, v in self.unet.state_dict().items() if include_fn(k)}
state_dict.update(self.cond_encoder.state_dict())
return state_dict
@@ -1,634 +0,0 @@
# Copyright 2024 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
from typing import Any, Callable, Dict, List, Optional, Union
import torch
from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback
from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
from diffusers.models import AutoencoderKL, T2IAdapter, UNet2DConditionModel
from diffusers.pipelines.stable_diffusion.pipeline_output import (
StableDiffusionPipelineOutput,
)
from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import (
StableDiffusionPipeline,
rescale_noise_cfg,
retrieve_timesteps,
)
from diffusers.pipelines.stable_diffusion.safety_checker import (
StableDiffusionSafetyChecker,
)
from diffusers.schedulers import KarrasDiffusionSchedulers
from diffusers.utils import deprecate, is_torch_xla_available, logging
from diffusers.utils.torch_utils import randn_tensor
from packaging import version
from transformers import (
CLIPImageProcessor,
CLIPTextModel,
CLIPTokenizer,
CLIPVisionModelWithProjection,
)
from ..loaders import CustomAdapterMixin
from ..models.attention_processor import (
DecoupledMVRowSelfAttnProcessor2_0,
set_unet_2d_condition_attn_processor,
)
if is_torch_xla_available():
import torch_xla.core.xla_model as xm
XLA_AVAILABLE = True
else:
XLA_AVAILABLE = False
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
class MVAdapterT2MVSDPipeline(StableDiffusionPipeline, CustomAdapterMixin):
def __init__(
self,
vae: AutoencoderKL,
text_encoder: CLIPTextModel,
tokenizer: CLIPTokenizer,
unet: UNet2DConditionModel,
scheduler: KarrasDiffusionSchedulers,
safety_checker: StableDiffusionSafetyChecker,
feature_extractor: CLIPImageProcessor,
image_encoder: CLIPVisionModelWithProjection = None,
requires_safety_checker: bool = True,
):
super().__init__(
vae=vae,
text_encoder=text_encoder,
tokenizer=tokenizer,
unet=unet,
scheduler=scheduler,
safety_checker=safety_checker,
feature_extractor=feature_extractor,
image_encoder=image_encoder,
requires_safety_checker=requires_safety_checker,
)
self.control_image_processor = VaeImageProcessor(
vae_scale_factor=self.vae_scale_factor,
do_convert_rgb=True,
do_normalize=False,
)
def prepare_control_image(
self,
image,
width,
height,
batch_size,
num_images_per_prompt,
device,
dtype,
do_classifier_free_guidance=False,
):
assert hasattr(
self, "control_image_processor"
), "control_image_processor is not initialized"
image = self.control_image_processor.preprocess(
image, height=height, width=width
).to(dtype=torch.float32)
image_batch_size = image.shape[0]
if image_batch_size == 1:
repeat_by = batch_size
else:
# image batch size is the same as prompt batch size
repeat_by = num_images_per_prompt # always 1 for control image
image = image.repeat_interleave(repeat_by, dim=0)
image = image.to(device=device, dtype=dtype)
if do_classifier_free_guidance:
image = torch.cat([image] * 2)
return image
@torch.no_grad()
def __call__(
self,
prompt: Union[str, List[str]] = None,
height: Optional[int] = None,
width: Optional[int] = None,
num_inference_steps: int = 50,
timesteps: List[int] = None,
sigmas: List[float] = None,
guidance_scale: float = 7.5,
negative_prompt: Optional[Union[str, List[str]]] = None,
num_images_per_prompt: Optional[int] = 1,
eta: float = 0.0,
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
latents: Optional[torch.Tensor] = None,
prompt_embeds: Optional[torch.Tensor] = None,
negative_prompt_embeds: Optional[torch.Tensor] = None,
ip_adapter_image: Optional[PipelineImageInput] = None,
ip_adapter_image_embeds: Optional[List[torch.Tensor]] = None,
output_type: Optional[str] = "pil",
return_dict: bool = True,
cross_attention_kwargs: Optional[Dict[str, Any]] = None,
guidance_rescale: float = 0.0,
clip_skip: Optional[int] = None,
callback_on_step_end: Optional[
Union[
Callable[[int, int, Dict], None],
PipelineCallback,
MultiPipelineCallbacks,
]
] = None,
callback_on_step_end_tensor_inputs: List[str] = ["latents"],
# NEW
mv_scale: float = 1.0,
# Camera or geometry condition
control_image: Optional[PipelineImageInput] = None,
control_conditioning_scale: Optional[float] = 1.0,
control_conditioning_factor: float = 1.0,
# Optional. controlnet
controlnet_image: Optional[PipelineImageInput] = None,
controlnet_conditioning_scale: Optional[float] = 1.0,
**kwargs,
):
r"""
The call function to the pipeline for generation.
Args:
prompt (`str` or `List[str]`, *optional*):
The prompt or prompts to guide image generation. If not defined, you need to pass `prompt_embeds`.
height (`int`, *optional*, defaults to `self.unet.config.sample_size * self.vae_scale_factor`):
The height in pixels of the generated image.
width (`int`, *optional*, defaults to `self.unet.config.sample_size * self.vae_scale_factor`):
The width in pixels of the generated image.
num_inference_steps (`int`, *optional*, defaults to 50):
The number of denoising steps. More denoising steps usually lead to a higher quality image at the
expense of slower inference.
timesteps (`List[int]`, *optional*):
Custom timesteps to use for the denoising process with schedulers which support a `timesteps` argument
in their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is
passed will be used. Must be in descending order.
sigmas (`List[float]`, *optional*):
Custom sigmas to use for the denoising process with schedulers which support a `sigmas` argument in
their `set_timesteps` method. If not defined, the default behavior when `num_inference_steps` is passed
will be used.
guidance_scale (`float`, *optional*, defaults to 7.5):
A higher guidance scale value encourages the model to generate images closely linked to the text
`prompt` at the expense of lower image quality. Guidance scale is enabled when `guidance_scale > 1`.
negative_prompt (`str` or `List[str]`, *optional*):
The prompt or prompts to guide what to not include in image generation. If not defined, you need to
pass `negative_prompt_embeds` instead. Ignored when not using guidance (`guidance_scale < 1`).
num_images_per_prompt (`int`, *optional*, defaults to 1):
The number of images to generate per prompt.
eta (`float`, *optional*, defaults to 0.0):
Corresponds to parameter eta (η) from the [DDIM](https://arxiv.org/abs/2010.02502) paper. Only applies
to the [`~schedulers.DDIMScheduler`], and is ignored in other schedulers.
generator (`torch.Generator` or `List[torch.Generator]`, *optional*):
A [`torch.Generator`](https://pytorch.org/docs/stable/generated/torch.Generator.html) to make
generation deterministic.
latents (`torch.Tensor`, *optional*):
Pre-generated noisy latents sampled from a Gaussian distribution, to be used as inputs for image
generation. Can be used to tweak the same generation with different prompts. If not provided, a latents
tensor is generated by sampling using the supplied random `generator`.
prompt_embeds (`torch.Tensor`, *optional*):
Pre-generated text embeddings. Can be used to easily tweak text inputs (prompt weighting). If not
provided, text embeddings are generated from the `prompt` input argument.
negative_prompt_embeds (`torch.Tensor`, *optional*):
Pre-generated negative text embeddings. Can be used to easily tweak text inputs (prompt weighting). If
not provided, `negative_prompt_embeds` are generated from the `negative_prompt` input argument.
ip_adapter_image: (`PipelineImageInput`, *optional*): Optional image input to work with IP Adapters.
ip_adapter_image_embeds (`List[torch.Tensor]`, *optional*):
Pre-generated image embeddings for IP-Adapter. It should be a list of length same as number of
IP-adapters. Each element should be a tensor of shape `(batch_size, num_images, emb_dim)`. It should
contain the negative image embedding if `do_classifier_free_guidance` is set to `True`. If not
provided, embeddings are computed from the `ip_adapter_image` input argument.
output_type (`str`, *optional*, defaults to `"pil"`):
The output format of the generated image. Choose between `PIL.Image` or `np.array`.
return_dict (`bool`, *optional*, defaults to `True`):
Whether or not to return a [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] instead of a
plain tuple.
cross_attention_kwargs (`dict`, *optional*):
A kwargs dictionary that if specified is passed along to the [`AttentionProcessor`] as defined in
[`self.processor`](https://github.com/huggingface/diffusers/blob/main/src/diffusers/models/attention_processor.py).
guidance_rescale (`float`, *optional*, defaults to 0.0):
Guidance rescale factor from [Common Diffusion Noise Schedules and Sample Steps are
Flawed](https://arxiv.org/pdf/2305.08891.pdf). Guidance rescale factor should fix overexposure when
using zero terminal SNR.
clip_skip (`int`, *optional*):
Number of layers to be skipped from CLIP while computing the prompt embeddings. A value of 1 means that
the output of the pre-final layer will be used for computing the prompt embeddings.
callback_on_step_end (`Callable`, `PipelineCallback`, `MultiPipelineCallbacks`, *optional*):
A function or a subclass of `PipelineCallback` or `MultiPipelineCallbacks` that is called at the end of
each denoising step during the inference. with the following arguments: `callback_on_step_end(self:
DiffusionPipeline, step: int, timestep: int, callback_kwargs: Dict)`. `callback_kwargs` will include a
list of all tensors as specified by `callback_on_step_end_tensor_inputs`.
callback_on_step_end_tensor_inputs (`List`, *optional*):
The list of tensor inputs for the `callback_on_step_end` function. The tensors specified in the list
will be passed as `callback_kwargs` argument. You will only be able to include variables listed in the
`._callback_tensor_inputs` attribute of your pipeline class.
Examples:
Returns:
[`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] or `tuple`:
If `return_dict` is `True`, [`~pipelines.stable_diffusion.StableDiffusionPipelineOutput`] is returned,
otherwise a `tuple` is returned where the first element is a list with the generated images and the
second element is a list of `bool`s indicating whether the corresponding generated image contains
"not-safe-for-work" (nsfw) content.
"""
callback = kwargs.pop("callback", None)
callback_steps = kwargs.pop("callback_steps", None)
if callback is not None:
deprecate(
"callback",
"1.0.0",
"Passing `callback` as an input argument to `__call__` is deprecated, consider using `callback_on_step_end`",
)
if callback_steps is not None:
deprecate(
"callback_steps",
"1.0.0",
"Passing `callback_steps` as an input argument to `__call__` is deprecated, consider using `callback_on_step_end`",
)
if isinstance(callback_on_step_end, (PipelineCallback, MultiPipelineCallbacks)):
callback_on_step_end_tensor_inputs = callback_on_step_end.tensor_inputs
# 0. Default height and width to unet
height = height or self.unet.config.sample_size * self.vae_scale_factor
width = width or self.unet.config.sample_size * self.vae_scale_factor
# to deal with lora scaling and other possible forward hooks
# 1. Check inputs. Raise error if not correct
self.check_inputs(
prompt,
height,
width,
callback_steps,
negative_prompt,
prompt_embeds,
negative_prompt_embeds,
ip_adapter_image,
ip_adapter_image_embeds,
callback_on_step_end_tensor_inputs,
)
self._guidance_scale = guidance_scale
self._guidance_rescale = guidance_rescale
self._clip_skip = clip_skip
self._cross_attention_kwargs = cross_attention_kwargs
self._interrupt = False
# 2. Define call parameters
if prompt is not None and isinstance(prompt, str):
batch_size = 1
elif prompt is not None and isinstance(prompt, list):
batch_size = len(prompt)
else:
batch_size = prompt_embeds.shape[0]
device = self._execution_device
# 3. Encode input prompt
lora_scale = (
self.cross_attention_kwargs.get("scale", None)
if self.cross_attention_kwargs is not None
else None
)
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
prompt,
device,
num_images_per_prompt,
self.do_classifier_free_guidance,
negative_prompt,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
lora_scale=lora_scale,
clip_skip=self.clip_skip,
)
# For classifier free guidance, we need to do two forward passes.
# Here we concatenate the unconditional and text embeddings into a single batch
# to avoid doing two forward passes
if self.do_classifier_free_guidance:
prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])
if ip_adapter_image is not None or ip_adapter_image_embeds is not None:
image_embeds = self.prepare_ip_adapter_image_embeds(
ip_adapter_image,
ip_adapter_image_embeds,
device,
batch_size * num_images_per_prompt,
self.do_classifier_free_guidance,
)
# 4. Prepare timesteps
timesteps, num_inference_steps = retrieve_timesteps(
self.scheduler, num_inference_steps, device, timesteps, sigmas
)
# 5. Prepare latent variables
num_channels_latents = self.unet.config.in_channels
latents = self.prepare_latents(
batch_size * num_images_per_prompt,
num_channels_latents,
height,
width,
prompt_embeds.dtype,
device,
generator,
latents,
)
# 6. Prepare extra step kwargs. TODO: Logic should ideally just be moved out of the pipeline
extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta)
# 6.1 Add image embeds for IP-Adapter
added_cond_kwargs = (
{"image_embeds": image_embeds}
if (ip_adapter_image is not None or ip_adapter_image_embeds is not None)
else None
)
# 6.2 Optionally get Guidance Scale Embedding
timestep_cond = None
if self.unet.config.time_cond_proj_dim is not None:
guidance_scale_tensor = torch.tensor(self.guidance_scale - 1).repeat(
batch_size * num_images_per_prompt
)
timestep_cond = self.get_guidance_scale_embedding(
guidance_scale_tensor, embedding_dim=self.unet.config.time_cond_proj_dim
).to(device=device, dtype=latents.dtype)
# Preprocess control image
control_image_feature = self.prepare_control_image(
image=control_image,
width=width,
height=height,
batch_size=batch_size * num_images_per_prompt,
num_images_per_prompt=1, # NOTE: always 1 for control images
device=device,
dtype=latents.dtype,
do_classifier_free_guidance=self.do_classifier_free_guidance,
)
control_image_feature = control_image_feature.to(
device=device, dtype=latents.dtype
)
adapter_state = self.cond_encoder(control_image_feature)
for i, state in enumerate(adapter_state):
adapter_state[i] = state * control_conditioning_scale
# Preprocess controlnet image if provided
do_controlnet = controlnet_image is not None and hasattr(self, "controlnet")
if do_controlnet:
controlnet_image = self.prepare_control_image(
image=controlnet_image,
width=width,
height=height,
batch_size=batch_size * num_images_per_prompt,
num_images_per_prompt=1, # NOTE: always 1 for control images
device=device,
dtype=latents.dtype,
do_classifier_free_guidance=self.do_classifier_free_guidance,
)
controlnet_image = controlnet_image.to(device=device, dtype=latents.dtype)
# 7. Denoising loop
num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order
self._num_timesteps = len(timesteps)
with self.progress_bar(total=num_inference_steps) as progress_bar:
for i, t in enumerate(timesteps):
if self.interrupt:
continue
# expand the latents if we are doing classifier free guidance
latent_model_input = (
torch.cat([latents] * 2)
if self.do_classifier_free_guidance
else latents
)
latent_model_input = self.scheduler.scale_model_input(
latent_model_input, t
)
if i < int(num_inference_steps * control_conditioning_factor):
down_intrablock_additional_residuals = [
state.clone() for state in adapter_state
]
else:
down_intrablock_additional_residuals = None
unet_add_kwargs = {}
# Do controlnet if provided
if do_controlnet:
down_block_res_samples, mid_block_res_sample = self.controlnet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
controlnet_cond=controlnet_image,
conditioning_scale=controlnet_conditioning_scale,
guess_mode=False,
added_cond_kwargs=added_cond_kwargs,
return_dict=False,
)
unet_add_kwargs.update(
{
"down_block_additional_residuals": down_block_res_samples,
"mid_block_additional_residual": mid_block_res_sample,
}
)
# predict the noise residual
noise_pred = self.unet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
timestep_cond=timestep_cond,
cross_attention_kwargs={
"mv_scale": mv_scale,
"num_views": num_images_per_prompt,
**(self.cross_attention_kwargs or {}),
},
down_intrablock_additional_residuals=down_intrablock_additional_residuals,
added_cond_kwargs=added_cond_kwargs,
return_dict=False,
**unet_add_kwargs,
)[0]
# perform guidance
if self.do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + self.guidance_scale * (
noise_pred_text - noise_pred_uncond
)
if self.do_classifier_free_guidance and self.guidance_rescale > 0.0:
# Based on 3.4. in https://arxiv.org/pdf/2305.08891.pdf
noise_pred = rescale_noise_cfg(
noise_pred,
noise_pred_text,
guidance_rescale=self.guidance_rescale,
)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(
noise_pred, t, latents, **extra_step_kwargs, return_dict=False
)[0]
if callback_on_step_end is not None:
callback_kwargs = {}
for k in callback_on_step_end_tensor_inputs:
callback_kwargs[k] = locals()[k]
callback_outputs = callback_on_step_end(self, i, t, callback_kwargs)
latents = callback_outputs.pop("latents", latents)
prompt_embeds = callback_outputs.pop("prompt_embeds", prompt_embeds)
negative_prompt_embeds = callback_outputs.pop(
"negative_prompt_embeds", negative_prompt_embeds
)
# call the callback, if provided
if i == len(timesteps) - 1 or (
(i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0
):
progress_bar.update()
if callback is not None and i % callback_steps == 0:
step_idx = i // getattr(self.scheduler, "order", 1)
callback(step_idx, t, latents)
if XLA_AVAILABLE:
xm.mark_step()
if not output_type == "latent":
image = self.vae.decode(
latents / self.vae.config.scaling_factor,
return_dict=False,
generator=generator,
)[0]
image, has_nsfw_concept = self.run_safety_checker(
image, device, prompt_embeds.dtype
)
else:
image = latents
has_nsfw_concept = None
if has_nsfw_concept is None:
do_denormalize = [True] * image.shape[0]
else:
do_denormalize = [not has_nsfw for has_nsfw in has_nsfw_concept]
image = self.image_processor.postprocess(
image, output_type=output_type, do_denormalize=do_denormalize
)
# Offload all models
self.maybe_free_model_hooks()
if not return_dict:
return (image, has_nsfw_concept)
return StableDiffusionPipelineOutput(
images=image, nsfw_content_detected=has_nsfw_concept
)
### NEW: adapters ###
def _init_custom_adapter(
self,
# Multi-view adapter
num_views: int = 1,
self_attn_processor: Any = DecoupledMVRowSelfAttnProcessor2_0,
# Condition encoder
cond_in_channels: int = 6,
# For training
copy_attn_weights: bool = True,
zero_init_module_keys: List[str] = [],
):
# Condition encoder
self.cond_encoder = T2IAdapter(
in_channels=cond_in_channels,
channels=self.unet.config.block_out_channels,
num_res_blocks=self.unet.config.layers_per_block,
downscale_factor=8,
)
# set custom attn processor for multi-view attention
self.unet: UNet2DConditionModel
set_unet_2d_condition_attn_processor(
self.unet,
set_self_attn_proc_func=lambda name, hs, cad, ap: self_attn_processor(
query_dim=hs,
inner_dim=hs,
num_views=num_views,
name=name,
use_mv=True,
use_ref=False,
),
set_cross_attn_proc_func=lambda name, hs, cad, ap: self_attn_processor(
query_dim=hs,
inner_dim=hs,
num_views=num_views,
name=name,
use_mv=False,
use_ref=False,
),
)
# copy decoupled attention weights from original unet
if copy_attn_weights:
state_dict = self.unet.state_dict()
for key in state_dict.keys():
if "_mv" in key:
compatible_key = key.replace("_mv", "").replace("processor.", "")
else:
compatible_key = key
is_zero_init_key = any([k in key for k in zero_init_module_keys])
if is_zero_init_key:
state_dict[key] = torch.zeros_like(state_dict[compatible_key])
else:
state_dict[key] = state_dict[compatible_key].clone()
self.unet.load_state_dict(state_dict)
def _load_custom_adapter(self, state_dict):
self.unet.load_state_dict(state_dict, strict=False)
self.cond_encoder.load_state_dict(state_dict, strict=False)
def _save_custom_adapter(
self,
include_keys: Optional[List[str]] = None,
exclude_keys: Optional[List[str]] = None,
):
def include_fn(k):
is_included = False
if include_keys is not None:
is_included = is_included or any([key in k for key in include_keys])
if exclude_keys is not None:
is_included = is_included and not any(
[key in k for key in exclude_keys]
)
return is_included
state_dict = {k: v for k, v in self.unet.state_dict().items() if include_fn(k)}
state_dict.update(self.cond_encoder.state_dict())
return state_dict
@@ -1,428 +0,0 @@
import os
from contextlib import contextmanager
from dataclasses import dataclass, field
from typing import List, Optional, Tuple, Union
import numpy as np
import torch
import torch.nn.functional as F
from PIL import Image
from spandrel import ModelLoader
from mvadapter.utils import image_to_tensor, make_image_grid, tensor_to_image
from mvadapter.utils.mesh_utils import (
Camera,
CameraProjection,
NVDiffRastContextWrapper,
SmartPainter,
TexturedMesh,
get_camera,
get_orthogonal_camera,
load_mesh,
render,
replace_mesh_texture_and_save,
)
from mvadapter.utils.mesh_utils.mesh_process import process_raw
def clear():
torch.cuda.empty_cache()
@contextmanager
def mesh_use_texture(mesh: TexturedMesh, texture: torch.FloatTensor):
texture_ = mesh.texture
mesh.texture = texture
try:
yield
finally:
mesh.texture = texture_
@dataclass
class ModProcessConfig:
view_upscale: bool = False
view_upscale_factor: int = 2
inpaint_mode: str = "uv" # in ["none", "uv", "view"]
view_inpaint_max_view_score_thresh: float = 0.02
view_inpaint_min_rounds: int = 4
view_inpaint_max_rounds: int = 8
view_inpaint_uv_padding_end: bool = True
@dataclass
class TexturePipelineOutput:
shaded_model_save_path: Optional[str] = None
pbr_model_save_path: Optional[str] = None
class TexturePipeline:
def __init__(self, upscaler_ckpt_path: str, inpaint_ckpt_path: str, device: str):
self.device = device
self.ctx = NVDiffRastContextWrapper(device=self.device)
self.cam_proj = CameraProjection(
pb_backend="torch-cuda", bg_remover=None, device=self.device
)
if upscaler_ckpt_path is not None:
self.upscaler = ModelLoader().load_from_file(upscaler_ckpt_path)
self.upscaler.to(self.device).eval().half()
if inpaint_ckpt_path is not None:
self.inpainter = ModelLoader().load_from_file(inpaint_ckpt_path)
self.inpainter.to(self.device).eval()
self.smart_painter = SmartPainter(self.device)
def load_packed_images(self, packed_image_path: Optional[str]) -> List[Image.Image]:
if packed_image_path is None:
return None
packed_image = Image.open(packed_image_path)
images = np.array_split(np.array(packed_image), 6, axis=1)
images = [Image.fromarray(im) for im in images]
return images
def maybe_upscale_image(
self,
tensor: Optional[torch.FloatTensor],
upscale: bool,
upscale_factor: int,
batched: bool = False,
) -> Optional[torch.FloatTensor]:
if upscale:
with torch.no_grad():
tensor = tensor.permute(0, 3, 1, 2)
if batched:
tensor = self.upscaler(tensor.half()).float()
else:
tensor = torch.concat(
[
self.upscaler(im.unsqueeze(0).half()).float()
for im in tensor
],
dim=0,
)
tensor = tensor.clamp(0, 1).permute(0, 2, 3, 1)
clear()
return tensor
def view_inpaint(
self,
mod_name: str,
mesh: TexturedMesh,
uv_proj: torch.FloatTensor,
uv_valid_mask: torch.BoolTensor,
config: ModProcessConfig,
debug_dir: Optional[str] = None,
) -> Tuple[torch.FloatTensor, torch.BoolTensor]:
def inpaint_func(
image: torch.FloatTensor, mask: torch.FloatTensor
) -> torch.FloatTensor:
with torch.no_grad():
return self.inpainter(image.to(torch.float32), mask.to(torch.float32))
return self.smart_painter(
mod_name,
mesh,
inpaint_func,
uv_proj,
~uv_valid_mask,
max_view_score_thresh=config.view_inpaint_max_view_score_thresh,
min_rounds=config.view_inpaint_min_rounds,
max_rounds=config.view_inpaint_max_rounds,
uv_padding_end=config.view_inpaint_uv_padding_end,
debug_dir=debug_dir,
debug_visualize_details=False,
)
def __call__(
self,
mesh_path: str,
save_dir: str,
save_name: str = "default",
# mesh load settings
move_to_center: bool = False,
front_x: bool = True,
# uv unwarp
uv_unwarp: bool = False,
preprocess_mesh: bool = False,
# projection
uv_size: int = 4096,
# modes
rgb_path: Optional[str] = None,
rgb_process_config: ModProcessConfig = ModProcessConfig(),
base_color_path: Optional[str] = None,
base_color_process_config: ModProcessConfig = ModProcessConfig(),
orm_path: Optional[str] = None,
orm_process_config: ModProcessConfig = ModProcessConfig(),
normal_path: Optional[str] = None,
normal_strength: float = 1.0,
normal_process_config: ModProcessConfig = ModProcessConfig(),
# inpaint
uv_inpaint_use_network: bool = False,
view_inpaint_include_occlusion_boundary: bool = False,
poisson_reprojection: bool = False,
# camera
camera_projection_type: str = "ORTHO",
camera_elevation_deg: List[float] = [0, 0, 0, 0, 89.99, -89.99],
camera_azimuth_deg: List[float] = [0, 90, 180, 270, 180, 180],
camera_distance: float = 1.0,
camera_ortho_scale: float = 1.1,
camera_fov_deg: float = 40,
# debug
debug_mode: bool = False,
):
clear()
if debug_mode:
debug_dir = os.path.join(save_dir, "debug")
os.makedirs(debug_dir, exist_ok=True)
if uv_unwarp:
file_suffix = os.path.splitext(mesh_path)[-1]
mesh_path_new = mesh_path.replace(file_suffix, f"_unwarp{file_suffix}")
process_raw(mesh_path, mesh_path_new, preprocess=preprocess_mesh)
mesh_path = mesh_path_new
mesh: TexturedMesh = load_mesh(
mesh_path,
rescale=True,
move_to_center=move_to_center,
front_x_to_y=front_x,
default_uv_size=uv_size,
device=self.device,
)
# projection
if camera_projection_type == "PERSP":
raise NotImplementedError
elif camera_projection_type == "ORTHO":
cameras = get_orthogonal_camera(
elevation_deg=camera_elevation_deg,
distance=[camera_distance] * 6,
left=-camera_ortho_scale / 2,
right=camera_ortho_scale / 2,
bottom=-camera_ortho_scale / 2,
top=camera_ortho_scale / 2,
azimuth_deg=[x - 90 for x in camera_azimuth_deg], # -y as front
device=self.device,
)
mod_kwargs = {
"rgb": (rgb_path, rgb_process_config),
"base_color": (base_color_path, base_color_process_config),
"orm": (orm_path, orm_process_config),
"normal": (normal_path, normal_process_config),
}
mod_uv_image, mod_uv_tensor = {}, {}
for mod_name, (mod_path, mod_process_config) in mod_kwargs.items():
if mod_path is None:
mod_uv_image[mod_name] = None
mod_uv_tensor[mod_name] = None
continue
mod_images = self.load_packed_images(mod_path)
mod_tensor = image_to_tensor(mod_images, device=self.device)
mod_tensor = self.maybe_upscale_image(
mod_tensor,
mod_process_config.view_upscale,
mod_process_config.view_upscale_factor,
)
if mod_process_config.view_upscale and debug_mode:
make_image_grid(tensor_to_image(mod_tensor, batched=True), rows=1).save(
os.path.join(debug_dir, f"{mod_name}_upscaled.jpg")
)
if mod_name == "normal":
_, height, width, _ = mod_tensor.shape
render_out = render(
self.ctx,
mesh,
cameras,
height=height,
width=width,
render_attr=False,
render_depth=False,
render_normal=True,
render_tangent=True,
)
# compute UV tangent space
vN = render_out.normal
vT = render_out.tangent
vB = torch.cross(vN, vT, dim=-1)
tang_space = F.normalize(torch.stack([vT, vB, vN], dim=-2), dim=-1)
# compute geometry tangent space
vGN = vN
vGT = torch.as_tensor(
[
[1, 0, 0],
[0, 1, 0],
[-1, 0, 0],
[0, -1, 0],
[-1, 0, 0],
[-1, 0, 0],
],
dtype=torch.float32,
device=self.device,
)[:, None, None, :]
vGB = torch.cross(vGN, vGT, dim=-1)
vGT = torch.cross(vGB, vGN, dim=-1)
geo_tang_space = F.normalize(
torch.stack([vGT, vGB, vGN], dim=-2), dim=-1
)
# restore world-space normal from geometry tangent space
mod_tensor = mod_tensor * 2 - 1
mod_tensor = F.normalize(
(
mod_tensor[:, :, :, None, :]
* geo_tang_space.permute(0, 1, 2, 4, 3)
).sum(-1),
dim=-1,
)
# bake world-space normal to UV tangent space
mod_tensor = F.normalize(
(mod_tensor[:, :, :, None, :] * tang_space).sum(-1), dim=-1
)
mod_tensor = (mod_tensor * 0.5 + 0.5).clamp(0, 1)
uv_proj, uv_valid_mask = self.cam_proj(
mod_tensor,
mesh,
cameras,
from_scratch=mod_process_config.inpaint_mode != "none",
poisson_blending=False,
depth_grad_dilation=5,
uv_exp_blend_alpha=3,
uv_exp_blend_view_weight=torch.as_tensor([1, 1, 1, 1, 1, 1]),
aoi_cos_valid_threshold=0.2,
uv_size=uv_size,
return_uv_projection_mask=True,
)
uv_proj[~uv_valid_mask] = torch.as_tensor([0.5, 0.5, 1]).to(uv_proj)
else:
# TODO: tweak depth_grad_dilation
cam_proj_out = self.cam_proj(
mod_tensor,
mesh,
cameras,
from_scratch=mod_process_config.inpaint_mode != "none",
poisson_blending=False,
depth_grad_dilation=5,
depth_grad_threshold=0.1,
uv_exp_blend_alpha=3,
uv_exp_blend_view_weight=torch.as_tensor([1, 1, 1, 1, 1, 1]),
aoi_cos_valid_threshold=0.2,
uv_size=uv_size,
uv_padding=not uv_inpaint_use_network,
return_dict=True,
)
uv_proj, uv_valid_mask, uv_depth_grad = (
cam_proj_out.uv_proj,
cam_proj_out.uv_proj_mask,
cam_proj_out.uv_depth_grad,
)
if uv_inpaint_use_network:
uv_inpaint_mask_input = 1 - uv_valid_mask[None, None].float()
uv_inpaint_image_input = uv_proj[None].permute(0, 3, 1, 2)
with torch.no_grad():
uv_inpaint_result = self.inpainter(
uv_inpaint_image_input, uv_inpaint_mask_input
)[0].permute(1, 2, 0)
clear()
if debug_mode:
make_image_grid(
[
tensor_to_image(uv_proj),
tensor_to_image(uv_valid_mask),
tensor_to_image(uv_inpaint_result),
]
).save(os.path.join(debug_dir, f"{mod_name}_uv_inpaint.jpg"))
uv_proj = uv_inpaint_result.contiguous()
if mod_process_config.inpaint_mode == "view":
if view_inpaint_include_occlusion_boundary:
uv_max_depth_grad = uv_depth_grad.max(dim=0)[0]
uv_valid_mask = uv_valid_mask & (uv_max_depth_grad < 0.1)
uv_proj, uv_valid_mask = self.view_inpaint(
mod_name,
mesh,
uv_proj,
uv_valid_mask,
mod_process_config,
debug_dir=debug_dir if debug_mode else None,
)
if poisson_reprojection:
# up and down
mesh.texture = uv_proj
uv_proj = self.cam_proj(
mod_tensor[4:5],
mesh,
cameras[4:5],
from_scratch=False,
poisson_blending=True,
pb_keep_original_border=True,
depth_grad_dilation=5,
uv_exp_blend_alpha=3,
uv_exp_blend_view_weight=torch.as_tensor([1, 1]),
aoi_cos_valid_threshold=0.2,
uv_size=uv_size,
uv_padding=True,
return_dict=False,
)
# front, sides and back
mesh.texture = uv_proj
uv_proj = self.cam_proj(
mod_tensor[0:4],
mesh,
cameras[0:4],
from_scratch=False,
poisson_blending=True,
pb_keep_original_border=True,
depth_grad_dilation=5,
uv_exp_blend_alpha=3,
uv_exp_blend_view_weight=torch.as_tensor([1, 1, 1, 1]),
aoi_cos_valid_threshold=0.2,
uv_size=uv_size,
uv_padding=True,
return_dict=False,
)
if mod_name == "orm":
uv_proj[:, :, 0] = 1.0
mod_uv_image[mod_name] = tensor_to_image(uv_proj)
mod_uv_tensor[mod_name] = uv_proj
clear()
shaded_model_save_path = None
if mod_uv_image["rgb"] is not None:
shaded_model_save_path = os.path.join(save_dir, f"{save_name}_shaded.glb")
replace_mesh_texture_and_save(
mesh_path,
shaded_model_save_path,
texture=mod_uv_image["rgb"],
backend="gltflib",
task_id=save_name,
)
pbr_model_save_path = None
if mod_uv_image["base_color"] is not None:
pbr_model_save_path = os.path.join(save_dir, f"{save_name}_pbr.glb")
replace_mesh_texture_and_save(
mesh_path,
pbr_model_save_path,
texture=mod_uv_image["base_color"],
metallic_roughness_texture=mod_uv_image["orm"],
normal_texture=mod_uv_image["normal"],
normal_strength=normal_strength,
backend="gltflib",
task_id=save_name,
)
clear()
return TexturePipelineOutput(
shaded_model_save_path=shaded_model_save_path,
pbr_model_save_path=pbr_model_save_path,
)
@@ -1,262 +0,0 @@
import os
from dataclasses import dataclass, field
import pytorch_lightning as pl
import torch
import torch.nn.functional as F
from ..utils.base import Updateable, update_end_if_possible, update_if_possible
from ..utils.config import parse_structured
from ..utils.core import debug, find, info, warn
from ..utils.misc import (
C,
cleanup,
get_device,
get_rank,
load_module_weights,
show_vram_usage,
)
from ..utils.saving import SaverMixin
from ..utils.typing import *
from .utils import parse_optimizer, parse_scheduler
class BaseSystem(pl.LightningModule, Updateable, SaverMixin):
@dataclass
class Config:
optimizer: dict = field(default_factory=dict)
scheduler: Optional[dict] = None
weights: Optional[str] = None
weights_ignore_modules: Optional[List[str]] = None
weights_mapping: Optional[List[Dict[str, str]]] = None
check_train_every_n_steps: int = 0
check_val_limit_rank: int = 8
cleanup_after_validation_step: bool = False
cleanup_after_test_step: bool = False
allow_tf32: bool = True
cfg: Config
def __init__(self, cfg, resumed=False) -> None:
super().__init__()
self.cfg = parse_structured(self.Config, cfg)
self._save_dir: Optional[str] = None
self._resumed: bool = resumed
self._resumed_eval: bool = False
self._resumed_eval_status: dict = {"global_step": 0, "current_epoch": 0}
# weird fix for extra VRAM usage on rank 0
# credit: https://discuss.pytorch.org/t/extra-10gb-memory-on-gpu-0-in-ddp-tutorial/118113
torch.cuda.set_device(get_rank())
torch.cuda.empty_cache()
torch.backends.cuda.matmul.allow_tf32 = self.cfg.allow_tf32
self.configure()
if self.cfg.weights is not None:
self.load_weights(
self.cfg.weights,
self.cfg.weights_ignore_modules,
self.cfg.weights_mapping,
)
self.post_configure()
def load_weights(
self,
weights: str,
ignore_modules: Optional[List[str]] = None,
mapping: Optional[List[Dict[str, str]]] = None,
):
state_dict, epoch, global_step = load_module_weights(
weights,
ignore_modules=ignore_modules,
mapping=mapping,
map_location="cpu",
)
self.load_state_dict(state_dict, strict=False)
# restore step-dependent states
self.do_update_step(epoch, global_step, on_load_weights=True)
def set_resume_status(self, current_epoch: int, global_step: int):
# restore correct epoch and global step in eval
self._resumed_eval = True
self._resumed_eval_status["current_epoch"] = current_epoch
self._resumed_eval_status["global_step"] = global_step
@property
def resumed(self):
# whether from resumed checkpoint
return self._resumed
@property
def true_global_step(self):
if self._resumed_eval:
return self._resumed_eval_status["global_step"]
else:
return self.global_step
@property
def true_current_epoch(self):
if self._resumed_eval:
return self._resumed_eval_status["current_epoch"]
else:
return self.current_epoch
def configure(self) -> None:
pass
def post_configure(self) -> None:
"""
executed after weights are loaded
"""
pass
def C(self, value: Any) -> float:
return C(value, self.true_current_epoch, self.true_global_step)
def configure_optimizers(self):
optim = parse_optimizer(self.cfg.optimizer, self)
ret = {
"optimizer": optim,
}
if self.cfg.scheduler is not None:
ret.update(
{
"lr_scheduler": parse_scheduler(self.cfg.scheduler, optim),
}
)
return ret
def on_fit_start(self) -> None:
if self._save_dir is not None:
info(f"Validation results will be saved to {self._save_dir}")
else:
warn(
f"Saving directory not set for the system, visualization results will not be saved"
)
def training_step(self, batch, batch_idx):
raise NotImplementedError
def check_train(self, batch, **kwargs):
if (
self.global_rank == 0
and self.cfg.check_train_every_n_steps > 0
and self.true_global_step % self.cfg.check_train_every_n_steps == 0
):
self.on_check_train(batch, **kwargs)
def on_check_train(self, batch, outputs, **kwargs):
pass
def validation_step(self, batch, batch_idx):
raise NotImplementedError
def on_validation_epoch_end(self):
pass
def test_step(self, batch, batch_idx):
raise NotImplementedError
def on_test_epoch_end(self):
pass
def on_test_end(self) -> None:
if self._save_dir is not None:
info(f"Test results saved to {self._save_dir}")
def on_predict_start(self) -> None:
pass
def predict_step(self, batch, batch_idx):
pass
def on_predict_epoch_end(self) -> None:
pass
def on_predict_end(self) -> None:
pass
def preprocess_data(self, batch, stage):
pass
"""
Implementing on_after_batch_transfer of DataModule does the same.
But on_after_batch_transfer does not support DP.
"""
def on_train_batch_start(self, batch, batch_idx, unused=0):
self.preprocess_data(batch, "train")
self.dataset = self.trainer.train_dataloader.dataset
update_if_possible(self.dataset, self.true_current_epoch, self.true_global_step)
self.do_update_step(self.true_current_epoch, self.true_global_step)
def on_validation_batch_start(self, batch, batch_idx, dataloader_idx=0):
self.preprocess_data(batch, "validation")
self.dataset = self.trainer.val_dataloaders.dataset
update_if_possible(self.dataset, self.true_current_epoch, self.true_global_step)
self.do_update_step(self.true_current_epoch, self.true_global_step)
def on_test_batch_start(self, batch, batch_idx, dataloader_idx=0):
self.preprocess_data(batch, "test")
self.dataset = self.trainer.test_dataloaders.dataset
update_if_possible(self.dataset, self.true_current_epoch, self.true_global_step)
self.do_update_step(self.true_current_epoch, self.true_global_step)
def on_predict_batch_start(self, batch, batch_idx, dataloader_idx=0):
self.preprocess_data(batch, "predict")
self.dataset = self.trainer.predict_dataloaders.dataset
update_if_possible(self.dataset, self.true_current_epoch, self.true_global_step)
self.do_update_step(self.true_current_epoch, self.true_global_step)
def on_train_batch_end(self, outputs, batch, batch_idx):
self.dataset = self.trainer.train_dataloader.dataset
update_end_if_possible(
self.dataset, self.true_current_epoch, self.true_global_step
)
self.do_update_step_end(self.true_current_epoch, self.true_global_step)
def on_validation_batch_end(self, outputs, batch, batch_idx):
self.dataset = self.trainer.val_dataloaders.dataset
update_end_if_possible(
self.dataset, self.true_current_epoch, self.true_global_step
)
self.do_update_step_end(self.true_current_epoch, self.true_global_step)
if self.cfg.cleanup_after_validation_step:
# cleanup to save vram
cleanup()
def on_test_batch_end(self, outputs, batch, batch_idx):
self.dataset = self.trainer.test_dataloaders.dataset
update_end_if_possible(
self.dataset, self.true_current_epoch, self.true_global_step
)
self.do_update_step_end(self.true_current_epoch, self.true_global_step)
if self.cfg.cleanup_after_test_step:
# cleanup to save vram
cleanup()
def on_predict_batch_end(self, outputs, batch, batch_idx):
self.dataset = self.trainer.predict_dataloaders.dataset
update_end_if_possible(
self.dataset, self.true_current_epoch, self.true_global_step
)
self.do_update_step_end(self.true_current_epoch, self.true_global_step)
if self.cfg.cleanup_after_test_step:
# cleanup to save vram
cleanup()
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
pass
def on_before_optimizer_step(self, optimizer):
"""
# some gradient-related debugging goes here, example:
from lightning.pytorch.utilities import grad_norm
norms = grad_norm(self.geometry, norm_type=2)
print(norms)
for name, p in self.named_parameters():
if p.grad is None:
info(f"{name} does not receive gradients!")
"""
pass
@@ -1,459 +0,0 @@
import os
import random
from dataclasses import dataclass, field
import torch
import torch.nn.functional as F
from diffusers import DDPMScheduler, UNet2DConditionModel
from diffusers.models import AutoencoderKL
from diffusers.training_utils import compute_snr
from einops import rearrange
from omegaconf import OmegaConf
from PIL import Image
from ..pipelines.pipeline_mvadapter_i2mv_sd import MVAdapterI2MVSDPipeline
from ..schedulers.scheduling_shift_snr import ShiftSNRScheduler
from ..utils.core import find
from ..utils.typing import *
from .base import BaseSystem
from .utils import encode_prompt, vae_encode
class MVAdapterImageSDSystem(BaseSystem):
@dataclass
class Config(BaseSystem.Config):
# Model / Adapter
pretrained_model_name_or_path: str = "stabilityai/stable-diffusion-2-1-base"
pretrained_vae_name_or_path: Optional[str] = None
pretrained_adapter_name_or_path: Optional[str] = None
pretrained_unet_name_or_path: Optional[str] = None
init_adapter_kwargs: Dict[str, Any] = field(default_factory=dict)
use_fp16_vae: bool = True
use_fp16_clip: bool = True
# Training
trainable_modules: List[str] = field(default_factory=list)
train_cond_encoder: bool = True
prompt_drop_prob: float = 0.0
image_drop_prob: float = 0.0
cond_drop_prob: float = 0.0
gradient_checkpointing: bool = False
# Noise sampler
noise_scheduler_kwargs: Dict[str, Any] = field(default_factory=dict)
noise_offset: float = 0.0
input_perturbation: float = 0.0
snr_gamma: Optional[float] = 5.0
prediction_type: Optional[str] = None
shift_noise: bool = False
shift_noise_mode: str = "interpolated"
shift_noise_scale: float = 1.0
# Evaluation
eval_seed: int = 0
eval_num_inference_steps: int = 30
eval_guidance_scale: float = 1.0
eval_height: int = 512
eval_width: int = 512
cfg: Config
def configure(self):
super().configure()
# Prepare pipeline
pipeline_kwargs = {}
if self.cfg.pretrained_vae_name_or_path is not None:
pipeline_kwargs["vae"] = AutoencoderKL.from_pretrained(
self.cfg.pretrained_vae_name_or_path
)
if self.cfg.pretrained_unet_name_or_path is not None:
pipeline_kwargs["unet"] = UNet2DConditionModel.from_pretrained(
self.cfg.pretrained_unet_name_or_path
)
pipeline: MVAdapterI2MVSDPipeline
pipeline = MVAdapterI2MVSDPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path, **pipeline_kwargs
)
init_adapter_kwargs = OmegaConf.to_container(self.cfg.init_adapter_kwargs)
if "self_attn_processor" in init_adapter_kwargs:
self_attn_processor = init_adapter_kwargs["self_attn_processor"]
if self_attn_processor is not None and isinstance(self_attn_processor, str):
self_attn_processor = find(self_attn_processor)
init_adapter_kwargs["self_attn_processor"] = self_attn_processor
pipeline.init_custom_adapter(**init_adapter_kwargs)
if self.cfg.pretrained_adapter_name_or_path:
pretrained_path = os.path.dirname(self.cfg.pretrained_adapter_name_or_path)
adapter_name = os.path.basename(self.cfg.pretrained_adapter_name_or_path)
pipeline.load_custom_adapter(pretrained_path, weight_name=adapter_name)
noise_scheduler = DDPMScheduler.from_config(
pipeline.scheduler.config, **self.cfg.noise_scheduler_kwargs
)
if self.cfg.shift_noise:
noise_scheduler = ShiftSNRScheduler.from_scheduler(
noise_scheduler,
shift_mode=self.cfg.shift_noise_mode,
shift_scale=self.cfg.shift_noise_scale,
scheduler_class=DDPMScheduler,
)
pipeline.scheduler = noise_scheduler
# Prepare models
self.pipeline: MVAdapterI2MVSDPipeline = pipeline
self.vae = self.pipeline.vae.to(
dtype=torch.float16 if self.cfg.use_fp16_vae else torch.float32
)
self.tokenizer = self.pipeline.tokenizer
self.text_encoder = self.pipeline.text_encoder.to(
dtype=torch.float16 if self.cfg.use_fp16_clip else torch.float32
)
self.feature_extractor = self.pipeline.feature_extractor
self.cond_encoder = self.pipeline.cond_encoder
self.unet = self.pipeline.unet
self.noise_scheduler = self.pipeline.scheduler
self.inference_scheduler = DDPMScheduler.from_config(
self.noise_scheduler.config
)
self.pipeline.scheduler = self.inference_scheduler
if self.cfg.prediction_type is not None:
self.noise_scheduler.register_to_config(
prediction_type=self.cfg.prediction_type
)
# Prepare trainable / non-trainable modules
trainable_modules = self.cfg.trainable_modules
if trainable_modules and len(trainable_modules) > 0:
self.unet.requires_grad_(False)
for name, module in self.unet.named_modules():
for trainable_module in trainable_modules:
if trainable_module in name:
module.requires_grad_(True)
else:
self.unet.requires_grad_(True)
self.cond_encoder.requires_grad_(self.cfg.train_cond_encoder)
self.vae.requires_grad_(False)
self.text_encoder.requires_grad_(False)
# Others
# Prepare gradient checkpointing
if self.cfg.gradient_checkpointing:
self.unet.enable_gradient_checkpointing()
def forward(
self,
noisy_latents: Tensor,
conditioning_pixel_values: Tensor,
timesteps: Tensor,
ref_latents: Tensor,
prompts: List[str],
num_views: int,
**kwargs,
) -> Dict[str, Any]:
bsz = noisy_latents.shape[0]
b_samples = bsz // num_views
num_batch_images = num_views
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
input_ids = self.tokenizer(
prompts,
max_length=self.tokenizer.model_max_length,
padding="max_length",
truncation=True,
return_tensors="pt",
)["input_ids"].to(noisy_latents.device)
encoder_hidden_states = self.text_encoder(input_ids, return_dict=False)[
0
].float()
prompt_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device)
< self.cfg.prompt_drop_prob
)
image_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device)
< self.cfg.image_drop_prob
)
cond_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device) < self.cfg.cond_drop_prob
)
prompt_drop_mask = prompt_drop_mask | cond_drop_mask
image_drop_mask = image_drop_mask | cond_drop_mask
encoder_hidden_states[prompt_drop_mask] = 0.0
# Process reference latents to obtain reference features
with torch.no_grad():
ref_timesteps = torch.zeros_like(timesteps[:b_samples])
ref_hidden_states = {}
self.unet(
ref_latents,
ref_timesteps,
encoder_hidden_states=encoder_hidden_states,
cross_attention_kwargs={
"cache_hidden_states": ref_hidden_states,
"use_mv": False,
"use_ref": False,
},
return_dict=False,
)
for k, v in ref_hidden_states.items():
v_ = v
v_[image_drop_mask] = 0.0
ref_hidden_states[k] = v_.repeat_interleave(num_batch_images, dim=0)
# Repeat additional embeddings for each image in the batch
encoder_hidden_states = encoder_hidden_states.repeat_interleave(
num_batch_images, dim=0
)
conditioning_features = self.cond_encoder(conditioning_pixel_values)
noise_pred = self.unet(
noisy_latents,
timesteps,
encoder_hidden_states=encoder_hidden_states,
down_intrablock_additional_residuals=conditioning_features,
cross_attention_kwargs={
"ref_hidden_states": ref_hidden_states,
"num_views": num_views,
},
).sample
return {"noise_pred": noise_pred}
def training_step(self, batch, batch_idx):
num_views = batch["num_views"]
vae_max_slice = 8
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
latents = []
for i in range(0, batch["rgb"].shape[0], vae_max_slice):
latents.append(
vae_encode(
self.vae,
batch["rgb"][i : i + vae_max_slice].to(self.vae.dtype) * 2 - 1,
sample=True,
apply_scale=True,
).float()
)
latents = torch.cat(latents, dim=0)
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
ref_latents = vae_encode(
self.vae,
batch["reference_rgb"].to(self.vae.dtype) * 2 - 1,
sample=True,
apply_scale=True,
).float()
bsz = latents.shape[0]
b_samples = bsz // num_views
noise = torch.randn_like(latents)
if self.cfg.noise_offset is not None:
# # https://www.crosslabs.org//blog/diffusion-with-offset-noise
noise += self.cfg.noise_offset * torch.randn(
(latents.shape[0], latents.shape[1], 1, 1), device=latents.device
)
noise_mask = (
batch["noise_mask"]
if "noise_mask" in batch
else torch.ones((bsz,), dtype=torch.bool, device=latents.device)
)
timesteps = torch.randint(
0,
self.noise_scheduler.config.num_train_timesteps,
(b_samples,),
device=latents.device,
dtype=torch.long,
)
timesteps = timesteps.repeat_interleave(num_views)
timesteps[~noise_mask] = 0
if self.cfg.input_perturbation is not None:
new_noise = noise + self.cfg.input_perturbation * torch.randn_like(noise)
noisy_latents = self.noise_scheduler.add_noise(
latents, new_noise, timesteps
)
else:
noisy_latents = self.noise_scheduler.add_noise(latents, noise, timesteps)
noisy_latents[~noise_mask] = latents[~noise_mask]
if self.noise_scheduler.config.prediction_type == "epsilon":
target = noise
elif self.noise_scheduler.config.prediction_type == "v_prediction":
target = self.noise_scheduler.get_velocity(latents, noise, timesteps)
else:
raise ValueError(
f"Unsupported prediction type {self.noise_scheduler.config.prediction_type}"
)
model_pred = self(
noisy_latents, batch["source_rgb"], timesteps, ref_latents, **batch
)["noise_pred"]
model_pred = model_pred[noise_mask]
target = target[noise_mask]
if self.cfg.snr_gamma is None:
loss = F.mse_loss(model_pred, target, reduction="mean")
else:
# Compute loss-weights as per Section 3.4 of https://arxiv.org/abs/2303.09556.
# Since we predict the noise instead of x_0, the original formulation is slightly changed.
# This is discussed in Section 4.2 of the same paper.
snr = compute_snr(self.noise_scheduler, timesteps)
if self.noise_scheduler.config.prediction_type == "v_prediction":
# Velocity objective requires that we add one to SNR values before we divide by them.
snr = snr + 1
mse_loss_weights = (
torch.stack(
[snr, self.cfg.snr_gamma * torch.ones_like(timesteps)], dim=1
).min(dim=1)[0]
/ snr
)
loss = F.mse_loss(model_pred, target, reduction="none")
loss = loss.mean(dim=list(range(1, len(loss.shape)))) * mse_loss_weights
loss = loss.mean()
self.log("train/loss", loss, prog_bar=True)
# will execute self.on_check_train every self.cfg.check_train_every_n_steps steps
self.check_train(batch)
return {"loss": loss}
def on_train_batch_end(self, outputs, batch, batch_idx):
pass
def get_input_visualizations(self, batch):
return [
{
"type": "rgb",
"img": rearrange(
batch["source_rgb"],
"(B N) C H W -> (B H) (N W) C",
N=batch["num_views"],
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(batch["reference_rgb"], "B C H W -> (B H) W C"),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
batch["rgb"], "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
]
def get_output_visualizations(self, batch, outputs):
images = [
{
"type": "rgb",
"img": rearrange(
batch["source_rgb"],
"(B N) C H W -> (B H) (N W) C",
N=batch["num_views"],
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
batch["rgb"], "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(batch["reference_rgb"], "B C H W -> (B H) W C"),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
outputs, "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
]
return images
def generate_images(self, batch, **kwargs):
return self.pipeline(
prompt=batch["prompts"],
control_image=batch["source_rgb"],
num_images_per_prompt=batch["num_views"],
generator=torch.Generator(device=self.device).manual_seed(
self.cfg.eval_seed
),
num_inference_steps=self.cfg.eval_num_inference_steps,
guidance_scale=self.cfg.eval_guidance_scale,
height=self.cfg.eval_height,
width=self.cfg.eval_width,
reference_image=batch["reference_rgb"],
output_type="pt",
).images
def on_save_checkpoint(self, checkpoint):
if self.global_rank == 0:
self.pipeline.save_custom_adapter(
os.path.dirname(self.get_save_dir()),
"custom_adapter.safetensors",
safe_serialization=True,
include_keys=self.cfg.trainable_modules,
)
def on_check_train(self, batch):
self.save_image_grid(
f"it{self.true_global_step}-train.jpg",
self.get_input_visualizations(batch),
name="train_step_input",
step=self.true_global_step,
)
def validation_step(self, batch, batch_idx):
out = self.generate_images(batch)
if (
self.cfg.check_val_limit_rank > 0
and self.global_rank < self.cfg.check_val_limit_rank
):
self.save_image_grid(
f"it{self.true_global_step}-validation-{self.global_rank}_{batch_idx}.jpg",
self.get_output_visualizations(batch, out),
name=f"validation_step_output_{self.global_rank}_{batch_idx}",
step=self.true_global_step,
)
def on_validation_epoch_end(self):
pass
def test_step(self, batch, batch_idx):
out = self.generate_images(batch)
self.save_image_grid(
f"it{self.true_global_step}-test-{self.global_rank}_{batch_idx}.jpg",
self.get_output_visualizations(batch, out),
name=f"test_step_output_{self.global_rank}_{batch_idx}",
step=self.true_global_step,
)
def on_test_end(self):
pass
@@ -1,506 +0,0 @@
import os
import random
from dataclasses import dataclass, field
import torch
import torch.nn.functional as F
from diffusers import DDPMScheduler, UNet2DConditionModel
from diffusers.models import AutoencoderKL
from diffusers.training_utils import compute_snr
from einops import rearrange
from omegaconf import OmegaConf
from PIL import Image
from ..pipelines.pipeline_mvadapter_i2mv_sdxl import MVAdapterI2MVSDXLPipeline
from ..schedulers.scheduling_shift_snr import ShiftSNRScheduler
from ..utils.core import find
from ..utils.typing import *
from .base import BaseSystem
from .utils import encode_prompt, vae_encode
def compute_embeddings(
prompt_batch,
empty_prompt_indices,
text_encoders,
tokenizers,
is_train=True,
**kwargs,
):
original_size = kwargs["original_size"]
target_size = kwargs["target_size"]
crops_coords_top_left = kwargs["crops_coords_top_left"]
for i in range(empty_prompt_indices.shape[0]):
if empty_prompt_indices[i]:
prompt_batch[i] = ""
prompt_embeds, pooled_prompt_embeds = encode_prompt(
prompt_batch, text_encoders, tokenizers, 0, is_train
)
add_text_embeds = pooled_prompt_embeds.to(
device=prompt_embeds.device, dtype=prompt_embeds.dtype
)
# Adapted from pipeline.StableDiffusionXLPipeline._get_add_time_ids
add_time_ids = list(original_size + crops_coords_top_left + target_size)
add_time_ids = torch.tensor([add_time_ids])
add_time_ids = add_time_ids.repeat(len(prompt_batch), 1)
add_time_ids = add_time_ids.to(
device=prompt_embeds.device, dtype=prompt_embeds.dtype
)
unet_added_cond_kwargs = {"text_embeds": add_text_embeds, "time_ids": add_time_ids}
return {"prompt_embeds": prompt_embeds, **unet_added_cond_kwargs}
class MVAdapterImageSDXLSystem(BaseSystem):
@dataclass
class Config(BaseSystem.Config):
# Model / Adapter
pretrained_model_name_or_path: str = "stabilityai/stable-diffusion-xl-base-1.0"
pretrained_vae_name_or_path: Optional[str] = "madebyollin/sdxl-vae-fp16-fix"
pretrained_adapter_name_or_path: Optional[str] = None
pretrained_unet_name_or_path: Optional[str] = None
init_adapter_kwargs: Dict[str, Any] = field(default_factory=dict)
use_fp16_vae: bool = True
use_fp16_clip: bool = True
# Training
trainable_modules: List[str] = field(default_factory=list)
train_cond_encoder: bool = True
prompt_drop_prob: float = 0.0
image_drop_prob: float = 0.0
cond_drop_prob: float = 0.0
gradient_checkpointing: bool = False
# Noise sampler
noise_scheduler_kwargs: Dict[str, Any] = field(default_factory=dict)
noise_offset: float = 0.0
input_perturbation: float = 0.0
snr_gamma: Optional[float] = 5.0
prediction_type: Optional[str] = None
shift_noise: bool = False
shift_noise_mode: str = "interpolated"
shift_noise_scale: float = 1.0
# Evaluation
eval_seed: int = 0
eval_num_inference_steps: int = 30
eval_guidance_scale: float = 1.0
eval_height: int = 512
eval_width: int = 512
cfg: Config
def configure(self):
super().configure()
# Prepare pipeline
pipeline_kwargs = {}
if self.cfg.pretrained_vae_name_or_path is not None:
pipeline_kwargs["vae"] = AutoencoderKL.from_pretrained(
self.cfg.pretrained_vae_name_or_path
)
if self.cfg.pretrained_unet_name_or_path is not None:
pipeline_kwargs["unet"] = UNet2DConditionModel.from_pretrained(
self.cfg.pretrained_unet_name_or_path
)
pipeline: MVAdapterI2MVSDXLPipeline
pipeline = MVAdapterI2MVSDXLPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path, **pipeline_kwargs
)
init_adapter_kwargs = OmegaConf.to_container(self.cfg.init_adapter_kwargs)
if "self_attn_processor" in init_adapter_kwargs:
self_attn_processor = init_adapter_kwargs["self_attn_processor"]
if self_attn_processor is not None and isinstance(self_attn_processor, str):
self_attn_processor = find(self_attn_processor)
init_adapter_kwargs["self_attn_processor"] = self_attn_processor
pipeline.init_custom_adapter(**init_adapter_kwargs)
if self.cfg.pretrained_adapter_name_or_path:
pretrained_path = os.path.dirname(self.cfg.pretrained_adapter_name_or_path)
adapter_name = os.path.basename(self.cfg.pretrained_adapter_name_or_path)
pipeline.load_custom_adapter(pretrained_path, weight_name=adapter_name)
noise_scheduler = DDPMScheduler.from_config(
pipeline.scheduler.config, **self.cfg.noise_scheduler_kwargs
)
if self.cfg.shift_noise:
noise_scheduler = ShiftSNRScheduler.from_scheduler(
noise_scheduler,
shift_mode=self.cfg.shift_noise_mode,
shift_scale=self.cfg.shift_noise_scale,
scheduler_class=DDPMScheduler,
)
pipeline.scheduler = noise_scheduler
# Prepare models
self.pipeline: MVAdapterI2MVSDXLPipeline = pipeline
self.vae = self.pipeline.vae.to(
dtype=torch.float16 if self.cfg.use_fp16_vae else torch.float32
)
self.tokenizer = self.pipeline.tokenizer
self.tokenizer_2 = self.pipeline.tokenizer_2
self.text_encoder = self.pipeline.text_encoder.to(
dtype=torch.float16 if self.cfg.use_fp16_clip else torch.float32
)
self.text_encoder_2 = self.pipeline.text_encoder_2.to(
dtype=torch.float16 if self.cfg.use_fp16_clip else torch.float32
)
self.feature_extractor = self.pipeline.feature_extractor
self.cond_encoder = self.pipeline.cond_encoder
self.unet = self.pipeline.unet
self.noise_scheduler = self.pipeline.scheduler
self.inference_scheduler = DDPMScheduler.from_config(
self.noise_scheduler.config
)
self.pipeline.scheduler = self.inference_scheduler
if self.cfg.prediction_type is not None:
self.noise_scheduler.register_to_config(
prediction_type=self.cfg.prediction_type
)
# Prepare trainable / non-trainable modules
trainable_modules = self.cfg.trainable_modules
if trainable_modules and len(trainable_modules) > 0:
self.unet.requires_grad_(False)
for name, module in self.unet.named_modules():
for trainable_module in trainable_modules:
if trainable_module in name:
module.requires_grad_(True)
else:
self.unet.requires_grad_(True)
self.cond_encoder.requires_grad_(self.cfg.train_cond_encoder)
self.vae.requires_grad_(False)
self.text_encoder.requires_grad_(False)
self.text_encoder_2.requires_grad_(False)
# Others
# Prepare gradient checkpointing
if self.cfg.gradient_checkpointing:
self.unet.enable_gradient_checkpointing()
def forward(
self,
noisy_latents: Tensor,
conditioning_pixel_values: Tensor,
timesteps: Tensor,
ref_latents: Tensor,
prompts: List[str],
num_views: int,
**kwargs,
) -> Dict[str, Any]:
bsz = noisy_latents.shape[0]
b_samples = bsz // num_views
num_batch_images = num_views
prompt_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device)
< self.cfg.prompt_drop_prob
)
image_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device)
< self.cfg.image_drop_prob
)
cond_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device) < self.cfg.cond_drop_prob
)
prompt_drop_mask = prompt_drop_mask | cond_drop_mask
image_drop_mask = image_drop_mask | cond_drop_mask
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
# Here, we compute not just the text embeddings but also the additional embeddings
# needed for the SD XL UNet to operate.
additional_embeds = compute_embeddings(
prompts,
prompt_drop_mask,
[self.text_encoder, self.text_encoder_2],
[self.tokenizer, self.tokenizer_2],
**kwargs,
)
# Process reference latents to obtain reference features
with torch.no_grad():
ref_timesteps = torch.zeros_like(timesteps[:b_samples])
ref_hidden_states = {}
self.unet(
ref_latents,
ref_timesteps,
encoder_hidden_states=additional_embeds["prompt_embeds"],
added_cond_kwargs={
"text_embeds": additional_embeds["text_embeds"],
"time_ids": additional_embeds["time_ids"],
},
cross_attention_kwargs={
"cache_hidden_states": ref_hidden_states,
"use_mv": False,
"use_ref": False,
},
return_dict=False,
)
for k, v in ref_hidden_states.items():
v_ = v
v_[image_drop_mask] = 0.0
ref_hidden_states[k] = v_.repeat_interleave(num_batch_images, dim=0)
# Repeat additional embeddings for each image in the batch
for key, value in additional_embeds.items():
kwargs[key] = value.repeat_interleave(num_batch_images, dim=0)
conditioning_features = self.cond_encoder(conditioning_pixel_values)
added_cond_kwargs = {
"text_embeds": kwargs["text_embeds"],
"time_ids": kwargs["time_ids"],
}
noise_pred = self.unet(
noisy_latents,
timesteps,
encoder_hidden_states=kwargs["prompt_embeds"],
added_cond_kwargs=added_cond_kwargs,
down_intrablock_additional_residuals=conditioning_features,
cross_attention_kwargs={
"ref_hidden_states": ref_hidden_states,
"num_views": num_views,
},
).sample
return {"noise_pred": noise_pred}
def training_step(self, batch, batch_idx):
num_views = batch["num_views"]
vae_max_slice = 8
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
latents = []
for i in range(0, batch["rgb"].shape[0], vae_max_slice):
latents.append(
vae_encode(
self.vae,
batch["rgb"][i : i + vae_max_slice].to(self.vae.dtype) * 2 - 1,
sample=True,
apply_scale=True,
).float()
)
latents = torch.cat(latents, dim=0)
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
ref_latents = vae_encode(
self.vae,
batch["reference_rgb"].to(self.vae.dtype) * 2 - 1,
sample=True,
apply_scale=True,
).float()
bsz = latents.shape[0]
b_samples = bsz // num_views
noise = torch.randn_like(latents)
if self.cfg.noise_offset is not None:
# # https://www.crosslabs.org//blog/diffusion-with-offset-noise
noise += self.cfg.noise_offset * torch.randn(
(latents.shape[0], latents.shape[1], 1, 1), device=latents.device
)
noise_mask = (
batch["noise_mask"]
if "noise_mask" in batch
else torch.ones((bsz,), dtype=torch.bool, device=latents.device)
)
timesteps = torch.randint(
0,
self.noise_scheduler.config.num_train_timesteps,
(b_samples,),
device=latents.device,
dtype=torch.long,
)
timesteps = timesteps.repeat_interleave(num_views)
timesteps[~noise_mask] = 0
if self.cfg.input_perturbation is not None:
new_noise = noise + self.cfg.input_perturbation * torch.randn_like(noise)
noisy_latents = self.noise_scheduler.add_noise(
latents, new_noise, timesteps
)
else:
noisy_latents = self.noise_scheduler.add_noise(latents, noise, timesteps)
noisy_latents[~noise_mask] = latents[~noise_mask]
if self.noise_scheduler.config.prediction_type == "epsilon":
target = noise
elif self.noise_scheduler.config.prediction_type == "v_prediction":
target = self.noise_scheduler.get_velocity(latents, noise, timesteps)
else:
raise ValueError(
f"Unsupported prediction type {self.noise_scheduler.config.prediction_type}"
)
model_pred = self(
noisy_latents, batch["source_rgb"], timesteps, ref_latents, **batch
)["noise_pred"]
model_pred = model_pred[noise_mask]
target = target[noise_mask]
if self.cfg.snr_gamma is None:
loss = F.mse_loss(model_pred, target, reduction="mean")
else:
# Compute loss-weights as per Section 3.4 of https://arxiv.org/abs/2303.09556.
# Since we predict the noise instead of x_0, the original formulation is slightly changed.
# This is discussed in Section 4.2 of the same paper.
snr = compute_snr(self.noise_scheduler, timesteps)
if self.noise_scheduler.config.prediction_type == "v_prediction":
# Velocity objective requires that we add one to SNR values before we divide by them.
snr = snr + 1
mse_loss_weights = (
torch.stack(
[snr, self.cfg.snr_gamma * torch.ones_like(timesteps)], dim=1
).min(dim=1)[0]
/ snr
)
loss = F.mse_loss(model_pred, target, reduction="none")
loss = loss.mean(dim=list(range(1, len(loss.shape)))) * mse_loss_weights
loss = loss.mean()
self.log("train/loss", loss, prog_bar=True)
# will execute self.on_check_train every self.cfg.check_train_every_n_steps steps
self.check_train(batch)
return {"loss": loss}
def on_train_batch_end(self, outputs, batch, batch_idx):
pass
def get_input_visualizations(self, batch):
return [
{
"type": "rgb",
"img": rearrange(
batch["source_rgb"],
"(B N) C H W -> (B H) (N W) C",
N=batch["num_views"],
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(batch["reference_rgb"], "B C H W -> (B H) W C"),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
batch["rgb"], "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
]
def get_output_visualizations(self, batch, outputs):
images = [
{
"type": "rgb",
"img": rearrange(
batch["source_rgb"],
"(B N) C H W -> (B H) (N W) C",
N=batch["num_views"],
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
batch["rgb"], "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(batch["reference_rgb"], "B C H W -> (B H) W C"),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
outputs, "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
]
return images
def generate_images(self, batch, **kwargs):
return self.pipeline(
prompt=batch["prompts"],
control_image=batch["source_rgb"],
num_images_per_prompt=batch["num_views"],
generator=torch.Generator(device=self.device).manual_seed(
self.cfg.eval_seed
),
num_inference_steps=self.cfg.eval_num_inference_steps,
guidance_scale=self.cfg.eval_guidance_scale,
height=self.cfg.eval_height,
width=self.cfg.eval_width,
reference_image=batch["reference_rgb"],
output_type="pt",
).images
def on_save_checkpoint(self, checkpoint):
if self.global_rank == 0:
self.pipeline.save_custom_adapter(
os.path.dirname(self.get_save_dir()),
"custom_adapter.safetensors",
safe_serialization=True,
include_keys=self.cfg.trainable_modules,
)
def on_check_train(self, batch):
self.save_image_grid(
f"it{self.true_global_step}-train.jpg",
self.get_input_visualizations(batch),
name="train_step_input",
step=self.true_global_step,
)
def validation_step(self, batch, batch_idx):
out = self.generate_images(batch)
if (
self.cfg.check_val_limit_rank > 0
and self.global_rank < self.cfg.check_val_limit_rank
):
self.save_image_grid(
f"it{self.true_global_step}-validation-{self.global_rank}_{batch_idx}.jpg",
self.get_output_visualizations(batch, out),
name=f"validation_step_output_{self.global_rank}_{batch_idx}",
step=self.true_global_step,
)
def on_validation_epoch_end(self):
pass
def test_step(self, batch, batch_idx):
out = self.generate_images(batch)
self.save_image_grid(
f"it{self.true_global_step}-test-{self.global_rank}_{batch_idx}.jpg",
self.get_output_visualizations(batch, out),
name=f"test_step_output_{self.global_rank}_{batch_idx}",
step=self.true_global_step,
)
def on_test_end(self):
pass
@@ -1,415 +0,0 @@
import os
import random
from dataclasses import dataclass, field
import torch
import torch.nn.functional as F
from diffusers import DDPMScheduler, UNet2DConditionModel
from diffusers.models import AutoencoderKL
from diffusers.training_utils import compute_snr
from einops import rearrange
from omegaconf import OmegaConf
from PIL import Image
from ..pipelines.pipeline_mvadapter_t2mv_sd import MVAdapterT2MVSDPipeline
from ..schedulers.scheduling_shift_snr import ShiftSNRScheduler
from ..utils.core import find
from ..utils.typing import *
from .base import BaseSystem
from .utils import encode_prompt, vae_encode
class MVAdapterTextSDSystem(BaseSystem):
@dataclass
class Config(BaseSystem.Config):
# Model / Adapter
pretrained_model_name_or_path: str = "stabilityai/stable-diffusion-2-1-base"
pretrained_vae_name_or_path: Optional[str] = None
pretrained_adapter_name_or_path: Optional[str] = None
pretrained_unet_name_or_path: Optional[str] = None
init_adapter_kwargs: Dict[str, Any] = field(default_factory=dict)
use_fp16_vae: bool = True
use_fp16_clip: bool = True
# Training
trainable_modules: List[str] = field(default_factory=list)
train_cond_encoder: bool = True
prompt_drop_prob: float = 0.0
image_drop_prob: float = 0.0
cond_drop_prob: float = 0.0
gradient_checkpointing: bool = False
# Noise sampler
noise_scheduler_kwargs: Dict[str, Any] = field(default_factory=dict)
noise_offset: float = 0.0
input_perturbation: float = 0.0
snr_gamma: Optional[float] = 5.0
prediction_type: Optional[str] = None
shift_noise: bool = False
shift_noise_mode: str = "interpolated"
shift_noise_scale: float = 1.0
# Evaluation
eval_seed: int = 0
eval_num_inference_steps: int = 30
eval_guidance_scale: float = 1.0
eval_height: int = 512
eval_width: int = 512
cfg: Config
def configure(self):
super().configure()
# Prepare pipeline
pipeline_kwargs = {}
if self.cfg.pretrained_vae_name_or_path is not None:
pipeline_kwargs["vae"] = AutoencoderKL.from_pretrained(
self.cfg.pretrained_vae_name_or_path
)
if self.cfg.pretrained_unet_name_or_path is not None:
pipeline_kwargs["unet"] = UNet2DConditionModel.from_pretrained(
self.cfg.pretrained_unet_name_or_path
)
pipeline: MVAdapterT2MVSDPipeline
pipeline = MVAdapterT2MVSDPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path, **pipeline_kwargs
)
init_adapter_kwargs = OmegaConf.to_container(self.cfg.init_adapter_kwargs)
if "self_attn_processor" in init_adapter_kwargs:
self_attn_processor = init_adapter_kwargs["self_attn_processor"]
if self_attn_processor is not None and isinstance(self_attn_processor, str):
self_attn_processor = find(self_attn_processor)
init_adapter_kwargs["self_attn_processor"] = self_attn_processor
pipeline.init_custom_adapter(**init_adapter_kwargs)
if self.cfg.pretrained_adapter_name_or_path:
pretrained_path = os.path.dirname(self.cfg.pretrained_adapter_name_or_path)
adapter_name = os.path.basename(self.cfg.pretrained_adapter_name_or_path)
pipeline.load_custom_adapter(pretrained_path, weight_name=adapter_name)
noise_scheduler = DDPMScheduler.from_config(
pipeline.scheduler.config, **self.cfg.noise_scheduler_kwargs
)
if self.cfg.shift_noise:
noise_scheduler = ShiftSNRScheduler.from_scheduler(
noise_scheduler,
shift_mode=self.cfg.shift_noise_mode,
shift_scale=self.cfg.shift_noise_scale,
scheduler_class=DDPMScheduler,
)
pipeline.scheduler = noise_scheduler
# Prepare models
self.pipeline: MVAdapterT2MVSDPipeline = pipeline
self.vae = self.pipeline.vae.to(
dtype=torch.float16 if self.cfg.use_fp16_vae else torch.float32
)
self.tokenizer = self.pipeline.tokenizer
self.text_encoder = self.pipeline.text_encoder.to(
dtype=torch.float16 if self.cfg.use_fp16_clip else torch.float32
)
self.feature_extractor = self.pipeline.feature_extractor
self.cond_encoder = self.pipeline.cond_encoder
self.unet = self.pipeline.unet
self.noise_scheduler = self.pipeline.scheduler
self.inference_scheduler = DDPMScheduler.from_config(
self.noise_scheduler.config
)
self.pipeline.scheduler = self.inference_scheduler
if self.cfg.prediction_type is not None:
self.noise_scheduler.register_to_config(
prediction_type=self.cfg.prediction_type
)
# Prepare trainable / non-trainable modules
trainable_modules = self.cfg.trainable_modules
if trainable_modules and len(trainable_modules) > 0:
self.unet.requires_grad_(False)
for name, module in self.unet.named_modules():
for trainable_module in trainable_modules:
if trainable_module in name:
module.requires_grad_(True)
else:
self.unet.requires_grad_(True)
self.cond_encoder.requires_grad_(self.cfg.train_cond_encoder)
self.vae.requires_grad_(False)
self.text_encoder.requires_grad_(False)
# Others
# Prepare gradient checkpointing
if self.cfg.gradient_checkpointing:
self.unet.enable_gradient_checkpointing()
def forward(
self,
noisy_latents: Tensor,
conditioning_pixel_values: Tensor,
timesteps: Tensor,
prompts: List[str],
num_views: int,
**kwargs,
) -> Dict[str, Any]:
bsz = noisy_latents.shape[0]
b_samples = bsz // num_views
num_batch_images = num_views
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
input_ids = self.tokenizer(
prompts,
max_length=self.tokenizer.model_max_length,
padding="max_length",
truncation=True,
return_tensors="pt",
)["input_ids"].to(noisy_latents.device)
encoder_hidden_states = self.text_encoder(input_ids, return_dict=False)[
0
].float()
prompt_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device)
< self.cfg.prompt_drop_prob
)
image_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device)
< self.cfg.image_drop_prob
)
cond_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device) < self.cfg.cond_drop_prob
)
prompt_drop_mask = prompt_drop_mask | cond_drop_mask
image_drop_mask = image_drop_mask | cond_drop_mask
encoder_hidden_states[prompt_drop_mask] = 0.0
# Repeat additional embeddings for each image in the batch
encoder_hidden_states = encoder_hidden_states.repeat_interleave(
num_batch_images, dim=0
)
conditioning_features = self.cond_encoder(conditioning_pixel_values)
noise_pred = self.unet(
noisy_latents,
timesteps,
encoder_hidden_states=encoder_hidden_states,
down_intrablock_additional_residuals=conditioning_features,
cross_attention_kwargs={"num_views": num_views},
).sample
return {"noise_pred": noise_pred}
def training_step(self, batch, batch_idx):
num_views = batch["num_views"]
vae_max_slice = 8
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
latents = []
for i in range(0, batch["rgb"].shape[0], vae_max_slice):
latents.append(
vae_encode(
self.vae,
batch["rgb"][i : i + vae_max_slice].to(self.vae.dtype) * 2 - 1,
sample=True,
apply_scale=True,
).float()
)
latents = torch.cat(latents, dim=0)
bsz = latents.shape[0]
b_samples = bsz // num_views
noise = torch.randn_like(latents)
if self.cfg.noise_offset is not None:
# # https://www.crosslabs.org//blog/diffusion-with-offset-noise
noise += self.cfg.noise_offset * torch.randn(
(latents.shape[0], latents.shape[1], 1, 1), device=latents.device
)
noise_mask = (
batch["noise_mask"]
if "noise_mask" in batch
else torch.ones((bsz,), dtype=torch.bool, device=latents.device)
)
timesteps = torch.randint(
0,
self.noise_scheduler.config.num_train_timesteps,
(b_samples,),
device=latents.device,
dtype=torch.long,
)
timesteps = timesteps.repeat_interleave(num_views)
timesteps[~noise_mask] = 0
if self.cfg.input_perturbation is not None:
new_noise = noise + self.cfg.input_perturbation * torch.randn_like(noise)
noisy_latents = self.noise_scheduler.add_noise(
latents, new_noise, timesteps
)
else:
noisy_latents = self.noise_scheduler.add_noise(latents, noise, timesteps)
noisy_latents[~noise_mask] = latents[~noise_mask]
if self.noise_scheduler.config.prediction_type == "epsilon":
target = noise
elif self.noise_scheduler.config.prediction_type == "v_prediction":
target = self.noise_scheduler.get_velocity(latents, noise, timesteps)
else:
raise ValueError(
f"Unsupported prediction type {self.noise_scheduler.config.prediction_type}"
)
model_pred = self(noisy_latents, batch["source_rgb"], timesteps, **batch)[
"noise_pred"
]
model_pred = model_pred[noise_mask]
target = target[noise_mask]
if self.cfg.snr_gamma is None:
loss = F.mse_loss(model_pred, target, reduction="mean")
else:
# Compute loss-weights as per Section 3.4 of https://arxiv.org/abs/2303.09556.
# Since we predict the noise instead of x_0, the original formulation is slightly changed.
# This is discussed in Section 4.2 of the same paper.
snr = compute_snr(self.noise_scheduler, timesteps)
if self.noise_scheduler.config.prediction_type == "v_prediction":
# Velocity objective requires that we add one to SNR values before we divide by them.
snr = snr + 1
mse_loss_weights = (
torch.stack(
[snr, self.cfg.snr_gamma * torch.ones_like(timesteps)], dim=1
).min(dim=1)[0]
/ snr
)
loss = F.mse_loss(model_pred, target, reduction="none")
loss = loss.mean(dim=list(range(1, len(loss.shape)))) * mse_loss_weights
loss = loss.mean()
self.log("train/loss", loss, prog_bar=True)
# will execute self.on_check_train every self.cfg.check_train_every_n_steps steps
self.check_train(batch)
return {"loss": loss}
def on_train_batch_end(self, outputs, batch, batch_idx):
pass
def get_input_visualizations(self, batch):
return [
{
"type": "rgb",
"img": rearrange(
batch["source_rgb"],
"(B N) C H W -> (B H) (N W) C",
N=batch["num_views"],
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
batch["rgb"], "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
]
def get_output_visualizations(self, batch, outputs):
images = [
{
"type": "rgb",
"img": rearrange(
batch["source_rgb"],
"(B N) C H W -> (B H) (N W) C",
N=batch["num_views"],
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
batch["rgb"], "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
outputs, "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
]
return images
def generate_images(self, batch, **kwargs):
return self.pipeline(
prompt=batch["prompts"],
control_image=batch["source_rgb"],
num_images_per_prompt=batch["num_views"],
generator=torch.Generator(device=self.device).manual_seed(
self.cfg.eval_seed
),
num_inference_steps=self.cfg.eval_num_inference_steps,
guidance_scale=self.cfg.eval_guidance_scale,
height=self.cfg.eval_height,
width=self.cfg.eval_width,
output_type="pt",
).images
def on_save_checkpoint(self, checkpoint):
if self.global_rank == 0:
self.pipeline.save_custom_adapter(
os.path.dirname(self.get_save_dir()),
"custom_adapter.safetensors",
safe_serialization=True,
include_keys=self.cfg.trainable_modules,
)
def on_check_train(self, batch):
self.save_image_grid(
f"it{self.true_global_step}-train.jpg",
self.get_input_visualizations(batch),
name="train_step_input",
step=self.true_global_step,
)
def validation_step(self, batch, batch_idx):
out = self.generate_images(batch)
if (
self.cfg.check_val_limit_rank > 0
and self.global_rank < self.cfg.check_val_limit_rank
):
self.save_image_grid(
f"it{self.true_global_step}-validation-{self.global_rank}_{batch_idx}.jpg",
self.get_output_visualizations(batch, out),
name=f"validation_step_output_{self.global_rank}_{batch_idx}",
step=self.true_global_step,
)
def on_validation_epoch_end(self):
pass
def test_step(self, batch, batch_idx):
out = self.generate_images(batch)
self.save_image_grid(
f"it{self.true_global_step}-test-{self.global_rank}_{batch_idx}.jpg",
self.get_output_visualizations(batch, out),
name=f"test_step_output_{self.global_rank}_{batch_idx}",
step=self.true_global_step,
)
def on_test_end(self):
pass
@@ -1,457 +0,0 @@
import os
import random
from dataclasses import dataclass, field
import torch
import torch.nn.functional as F
from diffusers import DDPMScheduler, UNet2DConditionModel
from diffusers.models import AutoencoderKL
from diffusers.training_utils import compute_snr
from einops import rearrange
from omegaconf import OmegaConf
from PIL import Image
from ..pipelines.pipeline_mvadapter_t2mv_sdxl import MVAdapterT2MVSDXLPipeline
from ..schedulers.scheduling_shift_snr import ShiftSNRScheduler
from ..utils.core import find
from ..utils.typing import *
from .base import BaseSystem
from .utils import encode_prompt, vae_encode
def compute_embeddings(
prompt_batch,
empty_prompt_indices,
text_encoders,
tokenizers,
is_train=True,
**kwargs,
):
original_size = kwargs["original_size"]
target_size = kwargs["target_size"]
crops_coords_top_left = kwargs["crops_coords_top_left"]
for i in range(empty_prompt_indices.shape[0]):
if empty_prompt_indices[i]:
prompt_batch[i] = ""
prompt_embeds, pooled_prompt_embeds = encode_prompt(
prompt_batch, text_encoders, tokenizers, 0, is_train
)
add_text_embeds = pooled_prompt_embeds.to(
device=prompt_embeds.device, dtype=prompt_embeds.dtype
)
# Adapted from pipeline.StableDiffusionXLPipeline._get_add_time_ids
add_time_ids = list(original_size + crops_coords_top_left + target_size)
add_time_ids = torch.tensor([add_time_ids])
add_time_ids = add_time_ids.repeat(len(prompt_batch), 1)
add_time_ids = add_time_ids.to(
device=prompt_embeds.device, dtype=prompt_embeds.dtype
)
unet_added_cond_kwargs = {"text_embeds": add_text_embeds, "time_ids": add_time_ids}
return {"prompt_embeds": prompt_embeds, **unet_added_cond_kwargs}
class MVAdapterTextSDXLSystem(BaseSystem):
@dataclass
class Config(BaseSystem.Config):
# Model / Adapter
pretrained_model_name_or_path: str = "stabilityai/stable-diffusion-xl-base-1.0"
pretrained_vae_name_or_path: Optional[str] = "madebyollin/sdxl-vae-fp16-fix"
pretrained_adapter_name_or_path: Optional[str] = None
pretrained_unet_name_or_path: Optional[str] = None
init_adapter_kwargs: Dict[str, Any] = field(default_factory=dict)
use_fp16_vae: bool = True
use_fp16_clip: bool = True
# Training
trainable_modules: List[str] = field(default_factory=list)
train_cond_encoder: bool = True
prompt_drop_prob: float = 0.0
image_drop_prob: float = 0.0
cond_drop_prob: float = 0.0
gradient_checkpointing: bool = False
# Noise sampler
noise_scheduler_kwargs: Dict[str, Any] = field(default_factory=dict)
noise_offset: float = 0.0
input_perturbation: float = 0.0
snr_gamma: Optional[float] = 5.0
prediction_type: Optional[str] = None
shift_noise: bool = False
shift_noise_mode: str = "interpolated"
shift_noise_scale: float = 1.0
# Evaluation
eval_seed: int = 0
eval_num_inference_steps: int = 30
eval_guidance_scale: float = 1.0
eval_height: int = 512
eval_width: int = 512
cfg: Config
def configure(self):
super().configure()
# Prepare pipeline
pipeline_kwargs = {}
if self.cfg.pretrained_vae_name_or_path is not None:
pipeline_kwargs["vae"] = AutoencoderKL.from_pretrained(
self.cfg.pretrained_vae_name_or_path
)
if self.cfg.pretrained_unet_name_or_path is not None:
pipeline_kwargs["unet"] = UNet2DConditionModel.from_pretrained(
self.cfg.pretrained_unet_name_or_path
)
pipeline: MVAdapterT2MVSDXLPipeline
pipeline = MVAdapterT2MVSDXLPipeline.from_pretrained(
self.cfg.pretrained_model_name_or_path, **pipeline_kwargs
)
init_adapter_kwargs = OmegaConf.to_container(self.cfg.init_adapter_kwargs)
if "self_attn_processor" in init_adapter_kwargs:
self_attn_processor = init_adapter_kwargs["self_attn_processor"]
if self_attn_processor is not None and isinstance(self_attn_processor, str):
self_attn_processor = find(self_attn_processor)
init_adapter_kwargs["self_attn_processor"] = self_attn_processor
pipeline.init_custom_adapter(**init_adapter_kwargs)
if self.cfg.pretrained_adapter_name_or_path:
pretrained_path = os.path.dirname(self.cfg.pretrained_adapter_name_or_path)
adapter_name = os.path.basename(self.cfg.pretrained_adapter_name_or_path)
pipeline.load_custom_adapter(pretrained_path, weight_name=adapter_name)
noise_scheduler = DDPMScheduler.from_config(
pipeline.scheduler.config, **self.cfg.noise_scheduler_kwargs
)
if self.cfg.shift_noise:
noise_scheduler = ShiftSNRScheduler.from_scheduler(
noise_scheduler,
shift_mode=self.cfg.shift_noise_mode,
shift_scale=self.cfg.shift_noise_scale,
scheduler_class=DDPMScheduler,
)
pipeline.scheduler = noise_scheduler
# Prepare models
self.pipeline: MVAdapterT2MVSDXLPipeline = pipeline
self.vae = self.pipeline.vae.to(
dtype=torch.float16 if self.cfg.use_fp16_vae else torch.float32
)
self.tokenizer = self.pipeline.tokenizer
self.tokenizer_2 = self.pipeline.tokenizer_2
self.text_encoder = self.pipeline.text_encoder.to(
dtype=torch.float16 if self.cfg.use_fp16_clip else torch.float32
)
self.text_encoder_2 = self.pipeline.text_encoder_2.to(
dtype=torch.float16 if self.cfg.use_fp16_clip else torch.float32
)
self.cond_encoder = self.pipeline.cond_encoder
self.unet = self.pipeline.unet
self.noise_scheduler = self.pipeline.scheduler
self.inference_scheduler = DDPMScheduler.from_config(
self.noise_scheduler.config
)
self.pipeline.scheduler = self.inference_scheduler
if self.cfg.prediction_type is not None:
self.noise_scheduler.register_to_config(
prediction_type=self.cfg.prediction_type
)
# Prepare trainable / non-trainable modules
trainable_modules = self.cfg.trainable_modules
if trainable_modules and len(trainable_modules) > 0:
self.unet.requires_grad_(False)
for name, module in self.unet.named_modules():
for trainable_module in trainable_modules:
if trainable_module in name:
module.requires_grad_(True)
else:
self.unet.requires_grad_(True)
self.cond_encoder.requires_grad_(self.cfg.train_cond_encoder)
self.vae.requires_grad_(False)
self.text_encoder.requires_grad_(False)
self.text_encoder_2.requires_grad_(False)
# Others
# Prepare gradient checkpointing
if self.cfg.gradient_checkpointing:
self.unet.enable_gradient_checkpointing()
def forward(
self,
noisy_latents: Tensor,
conditioning_pixel_values: Tensor,
timesteps: Tensor,
prompts: List[str],
num_views: int,
**kwargs,
) -> Dict[str, Any]:
bsz = noisy_latents.shape[0]
b_samples = bsz // num_views
num_batch_images = num_views
prompt_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device)
< self.cfg.prompt_drop_prob
)
image_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device)
< self.cfg.image_drop_prob
)
cond_drop_mask = (
torch.rand(b_samples, device=noisy_latents.device) < self.cfg.cond_drop_prob
)
prompt_drop_mask = prompt_drop_mask | cond_drop_mask
image_drop_mask = image_drop_mask | cond_drop_mask
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
# Here, we compute not just the text embeddings but also the additional embeddings
# needed for the SD XL UNet to operate.
additional_embeds = compute_embeddings(
prompts,
prompt_drop_mask,
[self.text_encoder, self.text_encoder_2],
[self.tokenizer, self.tokenizer_2],
**kwargs,
)
for key, value in additional_embeds.items():
kwargs[key] = value.repeat_interleave(num_batch_images, dim=0)
conditioning_features = self.cond_encoder(conditioning_pixel_values)
added_cond_kwargs = {
"text_embeds": kwargs["text_embeds"],
"time_ids": kwargs["time_ids"],
}
noise_pred = self.unet(
noisy_latents,
timesteps,
encoder_hidden_states=kwargs["prompt_embeds"],
added_cond_kwargs=added_cond_kwargs,
down_intrablock_additional_residuals=conditioning_features,
cross_attention_kwargs={"num_views": num_views},
).sample
return {"noise_pred": noise_pred}
def training_step(self, batch, batch_idx):
num_views = batch["num_views"]
vae_max_slice = 8
with torch.no_grad(), torch.cuda.amp.autocast(enabled=False):
latents = []
for i in range(0, batch["rgb"].shape[0], vae_max_slice):
latents.append(
vae_encode(
self.vae,
batch["rgb"][i : i + vae_max_slice].to(self.vae.dtype) * 2 - 1,
sample=True,
apply_scale=True,
).float()
)
latents = torch.cat(latents, dim=0)
bsz = latents.shape[0]
b_samples = bsz // num_views
noise = torch.randn_like(latents)
if self.cfg.noise_offset is not None:
# # https://www.crosslabs.org//blog/diffusion-with-offset-noise
noise += self.cfg.noise_offset * torch.randn(
(latents.shape[0], latents.shape[1], 1, 1), device=latents.device
)
noise_mask = (
batch["noise_mask"]
if "noise_mask" in batch
else torch.ones((bsz,), dtype=torch.bool, device=latents.device)
)
timesteps = torch.randint(
0,
self.noise_scheduler.config.num_train_timesteps,
(b_samples,),
device=latents.device,
dtype=torch.long,
)
timesteps = timesteps.repeat_interleave(num_views)
timesteps[~noise_mask] = 0
if self.cfg.input_perturbation is not None:
new_noise = noise + self.cfg.input_perturbation * torch.randn_like(noise)
noisy_latents = self.noise_scheduler.add_noise(
latents, new_noise, timesteps
)
else:
noisy_latents = self.noise_scheduler.add_noise(latents, noise, timesteps)
noisy_latents[~noise_mask] = latents[~noise_mask]
if self.noise_scheduler.config.prediction_type == "epsilon":
target = noise
elif self.noise_scheduler.config.prediction_type == "v_prediction":
target = self.noise_scheduler.get_velocity(latents, noise, timesteps)
else:
raise ValueError(
f"Unsupported prediction type {self.noise_scheduler.config.prediction_type}"
)
model_pred = self(noisy_latents, batch["source_rgb"], timesteps, **batch)[
"noise_pred"
]
model_pred = model_pred[noise_mask]
target = target[noise_mask]
if self.cfg.snr_gamma is None:
loss = F.mse_loss(model_pred, target, reduction="mean")
else:
# Compute loss-weights as per Section 3.4 of https://arxiv.org/abs/2303.09556.
# Since we predict the noise instead of x_0, the original formulation is slightly changed.
# This is discussed in Section 4.2 of the same paper.
snr = compute_snr(self.noise_scheduler, timesteps)
if self.noise_scheduler.config.prediction_type == "v_prediction":
# Velocity objective requires that we add one to SNR values before we divide by them.
snr = snr + 1
mse_loss_weights = (
torch.stack(
[snr, self.cfg.snr_gamma * torch.ones_like(timesteps)], dim=1
).min(dim=1)[0]
/ snr
)
loss = F.mse_loss(model_pred, target, reduction="none")
loss = loss.mean(dim=list(range(1, len(loss.shape)))) * mse_loss_weights
loss = loss.mean()
self.log("train/loss", loss, prog_bar=True)
# will execute self.on_check_train every self.cfg.check_train_every_n_steps steps
self.check_train(batch)
return {"loss": loss}
def on_train_batch_end(self, outputs, batch, batch_idx):
pass
def get_input_visualizations(self, batch):
return [
{
"type": "rgb",
"img": rearrange(
batch["source_rgb"],
"(B N) C H W -> (B H) (N W) C",
N=batch["num_views"],
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
batch["rgb"], "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
]
def get_output_visualizations(self, batch, outputs):
images = [
{
"type": "rgb",
"img": rearrange(
batch["source_rgb"],
"(B N) C H W -> (B H) (N W) C",
N=batch["num_views"],
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
batch["rgb"], "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
{
"type": "rgb",
"img": rearrange(
outputs, "(B N) C H W -> (B H) (N W) C", N=batch["num_views"]
),
"kwargs": {"data_format": "HWC"},
},
]
return images
def generate_images(self, batch, **kwargs):
return self.pipeline(
prompt=batch["prompts"],
control_image=batch["source_rgb"],
num_images_per_prompt=batch["num_views"],
generator=torch.Generator(device=self.device).manual_seed(
self.cfg.eval_seed
),
num_inference_steps=self.cfg.eval_num_inference_steps,
guidance_scale=self.cfg.eval_guidance_scale,
height=self.cfg.eval_height,
width=self.cfg.eval_width,
output_type="pt",
).images
def on_save_checkpoint(self, checkpoint):
if self.global_rank == 0:
self.pipeline.save_custom_adapter(
os.path.dirname(self.get_save_dir()),
"custom_adapter.safetensors",
safe_serialization=True,
include_keys=self.cfg.trainable_modules,
)
def on_check_train(self, batch):
self.save_image_grid(
f"it{self.true_global_step}-train.jpg",
self.get_input_visualizations(batch),
name="train_step_input",
step=self.true_global_step,
)
def validation_step(self, batch, batch_idx):
out = self.generate_images(batch)
if (
self.cfg.check_val_limit_rank > 0
and self.global_rank < self.cfg.check_val_limit_rank
):
self.save_image_grid(
f"it{self.true_global_step}-validation-{self.global_rank}_{batch_idx}.jpg",
self.get_output_visualizations(batch, out),
name=f"validation_step_output_{self.global_rank}_{batch_idx}",
step=self.true_global_step,
)
def on_validation_epoch_end(self):
pass
def test_step(self, batch, batch_idx):
out = self.generate_images(batch)
self.save_image_grid(
f"it{self.true_global_step}-test-{self.global_rank}_{batch_idx}.jpg",
self.get_output_visualizations(batch, out),
name=f"test_step_output_{self.global_rank}_{batch_idx}",
step=self.true_global_step,
)
def on_test_end(self):
pass
@@ -1,178 +0,0 @@
import random
import numpy as np
import torch
import torch.nn as nn
from diffusers import AutoencoderKL
from torch.optim import lr_scheduler
from ..utils.core import debug, find, info, warn
from ..utils.typing import *
"""Diffusers Model Utils"""
def vae_encode(
vae: AutoencoderKL,
pixel_values: Float[Tensor, "B 3 H W"],
sample: bool = True,
apply_scale: bool = True,
):
latent_dist = vae.encode(pixel_values).latent_dist
latents = latent_dist.sample() if sample else latent_dist.mode()
if apply_scale:
latents = latents * vae.config.scaling_factor
return latents
# Adapted from pipelines.StableDiffusionXLPipeline.encode_prompt
def encode_prompt(
prompt_batch, text_encoders, tokenizers, proportion_empty_prompts, is_train=True
):
prompt_embeds_list = []
captions = []
for caption in prompt_batch:
if random.random() < proportion_empty_prompts:
captions.append("")
elif isinstance(caption, str):
captions.append(caption)
elif isinstance(caption, (list, np.ndarray)):
# take a random caption if there are multiple
captions.append(random.choice(caption) if is_train else caption[0])
with torch.no_grad():
for tokenizer, text_encoder in zip(tokenizers, text_encoders):
text_inputs = tokenizer(
captions,
padding="max_length",
max_length=tokenizer.model_max_length,
truncation=True,
return_tensors="pt",
)
text_input_ids = text_inputs.input_ids
prompt_embeds = text_encoder(
text_input_ids.to(text_encoder.device),
output_hidden_states=True,
)
# We are only ALWAYS interested in the pooled output of the final text encoder
pooled_prompt_embeds = prompt_embeds[0]
prompt_embeds = prompt_embeds.hidden_states[-2]
bs_embed, seq_len, _ = prompt_embeds.shape
prompt_embeds = prompt_embeds.view(bs_embed, seq_len, -1)
prompt_embeds_list.append(prompt_embeds)
prompt_embeds = torch.concat(prompt_embeds_list, dim=-1)
pooled_prompt_embeds = pooled_prompt_embeds.view(bs_embed, -1)
return prompt_embeds, pooled_prompt_embeds
CLIP_INPUT_MEAN = torch.as_tensor(
[0.48145466, 0.4578275, 0.40821073], dtype=torch.float32
)[None, :, None, None]
CLIP_INPUT_STD = torch.as_tensor(
[0.26862954, 0.26130258, 0.27577711], dtype=torch.float32
)[None, :, None, None]
def normalize_image_for_clip(image: Float[Tensor, "B C H W"]):
return (image - CLIP_INPUT_MEAN.to(image)) / CLIP_INPUT_STD.to(image)
"""Training"""
def get_scheduler(name):
if hasattr(lr_scheduler, name):
return getattr(lr_scheduler, name)
else:
raise NotImplementedError
def getattr_recursive(m, attr):
for name in attr.split("."):
m = getattr(m, name)
return m
def get_parameters(model, name):
module = getattr_recursive(model, name)
if isinstance(module, nn.Module):
return module.parameters()
elif isinstance(module, nn.Parameter):
return module
return []
def parse_optimizer(config, model):
if hasattr(config, "params"):
params = [
{"params": get_parameters(model, name), "name": name, **args}
for name, args in config.params.items()
]
debug(f"Specify optimizer params: {config.params}")
else:
params = model.parameters()
if config.name in ["FusedAdam"]:
import apex
optim = getattr(apex.optimizers, config.name)(params, **config.args)
elif config.name in ["Adam8bit", "AdamW8bit"]:
import bitsandbytes as bnb
optim = bnb.optim.Adam8bit(params, **config.args)
else:
optim = getattr(torch.optim, config.name)(params, **config.args)
return optim
def parse_scheduler_to_instance(config, optimizer):
if config.name == "ChainedScheduler":
schedulers = [
parse_scheduler_to_instance(conf, optimizer) for conf in config.schedulers
]
scheduler = lr_scheduler.ChainedScheduler(schedulers)
elif config.name == "Sequential":
schedulers = [
parse_scheduler_to_instance(conf, optimizer) for conf in config.schedulers
]
scheduler = lr_scheduler.SequentialLR(
optimizer, schedulers, milestones=config.milestones
)
else:
scheduler = getattr(lr_scheduler, config.name)(optimizer, **config.args)
return scheduler
def parse_scheduler(config, optimizer):
interval = config.get("interval", "epoch")
assert interval in ["epoch", "step"]
if config.name == "SequentialLR":
scheduler = {
"scheduler": lr_scheduler.SequentialLR(
optimizer,
[
parse_scheduler(conf, optimizer)["scheduler"]
for conf in config.schedulers
],
milestones=config.milestones,
),
"interval": interval,
}
elif config.name == "ChainedScheduler":
scheduler = {
"scheduler": lr_scheduler.ChainedScheduler(
[
parse_scheduler(conf, optimizer)["scheduler"]
for conf in config.schedulers
]
),
"interval": interval,
}
else:
scheduler = {
"scheduler": get_scheduler(config.name)(optimizer, **config.args),
"interval": interval,
}
return scheduler
@@ -130,30 +130,30 @@ def prepare_pipeline(
return pipe
def create_bg_remover(device: str = "cuda"):
"""Create background removal function"""
birefnet = AutoModelForImageSegmentation.from_pretrained(
"ZhengPeng7/BiRefNet", trust_remote_code=True
)
birefnet.to(device)
transform_image = transforms.Compose([
transforms.Resize((1024, 1024)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
])
# def create_bg_remover(device: str = "cuda"):
# """Create background removal function"""
# birefnet = AutoModelForImageSegmentation.from_pretrained(
# "ZhengPeng7/BiRefNet", trust_remote_code=True
# )
# birefnet.to(device)
# transform_image = transforms.Compose([
# transforms.Resize((1024, 1024)),
# transforms.ToTensor(),
# transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
# ])
def remove_bg(image):
image_size = image.size
input_images = transform_image(image).unsqueeze(0).to(device)
with torch.no_grad():
preds = birefnet(input_images)[-1].sigmoid().cpu()
pred = preds[0].squeeze()
pred_pil = transforms.ToPILImage()(pred)
mask = pred_pil.resize(image_size)
image.putalpha(mask)
return image
# def remove_bg(image):
# image_size = image.size
# input_images = transform_image(image).unsqueeze(0).to(device)
# with torch.no_grad():
# preds = birefnet(input_images)[-1].sigmoid().cpu()
# pred = preds[0].squeeze()
# pred_pil = transforms.ToPILImage()(pred)
# mask = pred_pil.resize(image_size)
# image.putalpha(mask)
# return image
return remove_bg
# return remove_bg
def preprocess_image(image: Image.Image, height: int, width: int) -> Image.Image:
@@ -251,10 +251,10 @@ def run_pipeline(
# Prepare image
reference_image = Image.open(image) if isinstance(image, str) else image
if remove_bg_fn is not None:
reference_image = remove_bg_fn(reference_image)
reference_image = preprocess_image(reference_image, height, width)
elif reference_image.mode == "RGBA":
# if remove_bg_fn is not None:
# reference_image = remove_bg_fn(reference_image)
# reference_image = preprocess_image(reference_image, height, width)
if reference_image.mode == "RGBA":
reference_image = preprocess_image(reference_image, height, width)
pipe_kwargs = {}
@@ -1,28 +0,0 @@
controlnet_aux
diffusers
transformers
peft
numpy
huggingface_hub
accelerate
opencv-python
safetensors
pillow
omegaconf
trimesh
einops
gradio
timm
kornia
scikit-image
sentencepiece
git+https://github.com/NVlabs/nvdiffrast.git
# new added for training
pytorch-lightning
# new added for texturing
spandrel==0.4.1
open3d
pymeshlab
cvcuda_cu12
@@ -1,214 +0,0 @@
import argparse
import random
import gradio as gr
import numpy as np
# import spaces
import torch
from torchvision import transforms
from transformers import AutoModelForImageSegmentation
from .inference_i2mv_sdxl import prepare_pipeline, remove_bg, run_pipeline
# Device and dtype
dtype = torch.bfloat16
device = "cuda" if torch.cuda.is_available() else "cpu"
# Hyperparameters
NUM_VIEWS = 6
HEIGHT = 768
WIDTH = 768
MAX_SEED = np.iinfo(np.int32).max
pipe = prepare_pipeline(
base_model="stabilityai/stable-diffusion-xl-base-1.0",
vae_model="madebyollin/sdxl-vae-fp16-fix",
unet_model=None,
lora_model=None,
adapter_path="huanngzh/mv-adapter",
scheduler=None,
num_views=NUM_VIEWS,
device=device,
dtype=dtype,
)
# remove bg
birefnet = AutoModelForImageSegmentation.from_pretrained(
"ZhengPeng7/BiRefNet", trust_remote_code=True
)
birefnet.to(device)
transform_image = transforms.Compose(
[
transforms.Resize((1024, 1024)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
]
)
# @spaces.GPU()
def infer(
prompt,
image,
do_rembg=True,
seed=42,
randomize_seed=False,
guidance_scale=3.0,
num_inference_steps=50,
reference_conditioning_scale=1.0,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
progress=gr.Progress(track_tqdm=True),
):
if do_rembg:
remove_bg_fn = lambda x: remove_bg(x, birefnet, transform_image, device)
else:
remove_bg_fn = None
if randomize_seed:
seed = random.randint(0, MAX_SEED)
images, preprocessed_image = run_pipeline(
pipe,
num_views=NUM_VIEWS,
text=prompt,
image=image,
height=HEIGHT,
width=WIDTH,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
seed=seed,
remove_bg_fn=remove_bg_fn,
reference_conditioning_scale=reference_conditioning_scale,
negative_prompt=negative_prompt,
device=device,
)
return images, preprocessed_image, seed
examples = [
[
"A decorative figurine of a young anime-style girl",
"assets/demo/i2mv/A_decorative_figurine_of_a_young_anime-style_girl.png",
True,
21,
],
[
"A juvenile emperor penguin chick",
"assets/demo/i2mv/A_juvenile_emperor_penguin_chick.png",
True,
0,
],
[
"A striped tabby cat with white fur sitting upright",
"assets/demo/i2mv/A_striped_tabby_cat_with_white_fur_sitting_upright.png",
True,
0,
],
]
with gr.Blocks() as demo:
with gr.Row():
gr.Markdown(
f"""# MV-Adapter [Image-to-Multi-View]
Generate 768x768 multi-view images from a single image using SDXL <br>
[[page](https://huanngzh.github.io/MV-Adapter-Page/)] [[repo](https://github.com/huanngzh/MV-Adapter)]
"""
)
with gr.Row():
with gr.Column():
with gr.Row():
input_image = gr.Image(
label="Input Image",
sources=["upload", "webcam", "clipboard"],
type="pil",
)
preprocessed_image = gr.Image(label="Preprocessed Image", type="pil")
prompt = gr.Textbox(
label="Prompt", placeholder="Enter your prompt", value="high quality"
)
do_rembg = gr.Checkbox(label="Remove background", value=True)
run_button = gr.Button("Run")
with gr.Accordion("Advanced Settings", open=False):
seed = gr.Slider(
label="Seed",
minimum=0,
maximum=MAX_SEED,
step=1,
value=0,
)
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
with gr.Row():
num_inference_steps = gr.Slider(
label="Number of inference steps",
minimum=1,
maximum=50,
step=1,
value=50,
)
with gr.Row():
guidance_scale = gr.Slider(
label="CFG scale",
minimum=0.0,
maximum=10.0,
step=0.1,
value=3.0,
)
with gr.Row():
reference_conditioning_scale = gr.Slider(
label="Image conditioning scale",
minimum=0.0,
maximum=2.0,
step=0.1,
value=1.0,
)
with gr.Row():
negative_prompt = gr.Textbox(
label="Negative prompt",
placeholder="Enter your negative prompt",
value="watermark, ugly, deformed, noisy, blurry, low contrast",
)
with gr.Column():
result = gr.Gallery(
label="Result",
show_label=False,
columns=[3],
rows=[2],
object_fit="contain",
height="auto",
)
with gr.Row():
gr.Examples(
examples=examples,
fn=infer,
inputs=[prompt, input_image, do_rembg, seed],
outputs=[result, preprocessed_image, seed],
cache_examples=True,
)
gr.on(
triggers=[run_button.click, prompt.submit],
fn=infer,
inputs=[
prompt,
input_image,
do_rembg,
seed,
randomize_seed,
guidance_scale,
num_inference_steps,
reference_conditioning_scale,
negative_prompt,
],
outputs=[result, preprocessed_image, seed],
)
demo.launch()
@@ -1,195 +0,0 @@
import argparse
import random
import gradio as gr
import numpy as np
# import spaces
import torch
from .inference_t2mv_sdxl import prepare_pipeline, run_pipeline
# Base model
parser = argparse.ArgumentParser()
parser.add_argument(
"--base_model", type=str, default="stabilityai/stable-diffusion-xl-base-1.0"
)
parser.add_argument("--scheduler", type=str, default=None)
args = parser.parse_args()
base_model = args.base_model
scheduler = args.scheduler
# Device and dtype
dtype = torch.bfloat16
device = "cuda" if torch.cuda.is_available() else "cpu"
# Hyperparameters
NUM_VIEWS = 6
HEIGHT = 768
WIDTH = 768
MAX_SEED = np.iinfo(np.int32).max
pipe = prepare_pipeline(
base_model=base_model,
vae_model="madebyollin/sdxl-vae-fp16-fix",
unet_model=None,
lora_model=None,
adapter_path="huanngzh/mv-adapter",
scheduler=scheduler,
num_views=NUM_VIEWS,
device=device,
dtype=dtype,
)
# @spaces.GPU()
def infer(
prompt,
seed=42,
randomize_seed=False,
guidance_scale=7.0,
num_inference_steps=50,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
progress=gr.Progress(track_tqdm=True),
):
if randomize_seed:
seed = random.randint(0, MAX_SEED)
images = run_pipeline(
pipe,
num_views=NUM_VIEWS,
text=prompt,
height=HEIGHT,
width=WIDTH,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
seed=seed,
negative_prompt=negative_prompt,
device=device,
)
return images, seed
examples = {
"stabilityai/stable-diffusion-xl-base-1.0": [
["An astronaut riding a horse", 42],
["A DSLR photo of a frog wearing a sweater", 42],
],
"cagliostrolab/animagine-xl-3.1": [
[
"1girl, izayoi sakuya, touhou, solo, maid headdress, maid, apron, short sleeves, dress, closed mouth, white apron, serious face, upper body, masterpiece, best quality, very aesthetic, absurdres",
0,
],
[
"1boy, male focus, ikari shinji, neon genesis evangelion, solo, serious face,(masterpiece), (best quality), (ultra-detailed), very aesthetic, illustration, disheveled hair, moist skin, intricate details",
0,
],
[
"1girl, pink hair, pink shirts, smile, shy, masterpiece, anime",
0,
],
],
"Lykon/dreamshaper-xl-1-0": [
["the warrior Aragorn from Lord of the Rings, film grain, 8k hd", 0],
[
"Oil painting, masterpiece, regal, fancy. A well-dressed dog named Puproy Doggerson III wearing reading glasses types an important letter on a typewriter and enjoys a cup of coffee with the newspaper.",
42,
],
],
}
css = """
#col-container {
margin: 0 auto;
max-width: 600px;
}
"""
with gr.Blocks(css=css) as demo:
with gr.Column(elem_id="col-container"):
gr.Markdown(
f"""# MV-Adapter [Text-to-Multi-View]
Generate 768x768 multi-view images using {base_model} <br>
[[page](https://huanngzh.github.io/MV-Adapter-Page/)] [[repo](https://github.com/huanngzh/MV-Adapter)]
"""
)
with gr.Row():
prompt = gr.Text(
label="Prompt",
show_label=False,
max_lines=1,
placeholder="Enter your prompt",
container=False,
)
run_button = gr.Button("Run", scale=0)
result = gr.Gallery(
label="Result",
show_label=False,
columns=[3],
rows=[2],
object_fit="contain",
height="auto",
)
with gr.Accordion("Advanced Settings", open=False):
seed = gr.Slider(
label="Seed",
minimum=0,
maximum=MAX_SEED,
step=1,
value=0,
)
randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
with gr.Row():
num_inference_steps = gr.Slider(
label="Number of inference steps",
minimum=1,
maximum=50,
step=1,
value=50,
)
with gr.Row():
guidance_scale = gr.Slider(
label="CFG scale",
minimum=0.0,
maximum=10.0,
step=0.1,
value=7.0,
)
with gr.Row():
negative_prompt = gr.Textbox(
label="Negative prompt",
placeholder="Enter your negative prompt",
value="watermark, ugly, deformed, noisy, blurry, low contrast",
)
if base_model in examples:
gr.Examples(
examples=examples[base_model],
fn=infer,
inputs=[prompt, seed],
outputs=[result, seed],
cache_examples=True,
)
gr.on(
triggers=[run_button.click, prompt.submit],
fn=infer,
inputs=[
prompt,
seed,
randomize_seed,
guidance_scale,
num_inference_steps,
negative_prompt,
],
outputs=[result, seed],
)
demo.launch()
@@ -1,255 +0,0 @@
import argparse
import numpy as np
import torch
from diffusers import AutoencoderKL, DDPMScheduler, LCMScheduler, UNet2DConditionModel
from PIL import Image
from torchvision import transforms
from transformers import AutoModelForImageSegmentation
from mvadapter.pipelines.pipeline_mvadapter_i2mv_sd import MVAdapterI2MVSDPipeline
from mvadapter.schedulers.scheduling_shift_snr import ShiftSNRScheduler
from mvadapter.utils.mesh_utils import get_orthogonal_camera
from mvadapter.utils.geometry import get_plucker_embeds_from_cameras_ortho
from mvadapter.utils import make_image_grid
def prepare_pipeline(
base_model,
vae_model,
unet_model,
lora_model,
adapter_path,
scheduler,
num_views,
device,
dtype,
):
# Load vae and unet if provided
pipe_kwargs = {}
if vae_model is not None:
pipe_kwargs["vae"] = AutoencoderKL.from_pretrained(vae_model)
if unet_model is not None:
pipe_kwargs["unet"] = UNet2DConditionModel.from_pretrained(unet_model)
# Prepare pipeline
pipe: MVAdapterI2MVSDPipeline
pipe = MVAdapterI2MVSDPipeline.from_pretrained(base_model, **pipe_kwargs)
# Load scheduler if provided
scheduler_class = None
if scheduler == "ddpm":
scheduler_class = DDPMScheduler
elif scheduler == "lcm":
scheduler_class = LCMScheduler
pipe.scheduler = ShiftSNRScheduler.from_scheduler(
pipe.scheduler,
shift_mode="interpolated",
shift_scale=8.0,
scheduler_class=scheduler_class,
)
pipe.init_custom_adapter(num_views=num_views)
pipe.load_custom_adapter(
adapter_path, weight_name="mvadapter_i2mv_sd21.safetensors"
)
pipe.to(device=device, dtype=dtype)
pipe.cond_encoder.to(device=device, dtype=dtype)
# load lora if provided
if lora_model is not None:
model_, name_ = lora_model.rsplit("/", 1)
pipe.load_lora_weights(model_, weight_name=name_)
# vae slicing for lower memory usage
pipe.enable_vae_slicing()
return pipe
def remove_bg(image, net, transform, device):
image_size = image.size
input_images = transform(image).unsqueeze(0).to(device)
with torch.no_grad():
preds = net(input_images)[-1].sigmoid().cpu()
pred = preds[0].squeeze()
pred_pil = transforms.ToPILImage()(pred)
mask = pred_pil.resize(image_size)
image.putalpha(mask)
return image
def preprocess_image(image: Image.Image, height, width):
image = np.array(image)
alpha = image[..., 3] > 0
H, W = alpha.shape
# get the bounding box of alpha
y, x = np.where(alpha)
y0, y1 = max(y.min() - 1, 0), min(y.max() + 1, H)
x0, x1 = max(x.min() - 1, 0), min(x.max() + 1, W)
image_center = image[y0:y1, x0:x1]
# resize the longer side to H * 0.9
H, W, _ = image_center.shape
if H > W:
W = int(W * (height * 0.9) / H)
H = int(height * 0.9)
else:
H = int(H * (width * 0.9) / W)
W = int(width * 0.9)
image_center = np.array(Image.fromarray(image_center).resize((W, H)))
# pad to H, W
start_h = (height - H) // 2
start_w = (width - W) // 2
image = np.zeros((height, width, 4), dtype=np.uint8)
image[start_h : start_h + H, start_w : start_w + W] = image_center
image = image.astype(np.float32) / 255.0
image = image[:, :, :3] * image[:, :, 3:4] + (1 - image[:, :, 3:4]) * 0.5
image = (image * 255).clip(0, 255).astype(np.uint8)
image = Image.fromarray(image)
return image
def run_pipeline(
pipe,
num_views,
text,
image,
height,
width,
num_inference_steps,
guidance_scale,
seed,
remove_bg_fn=None,
reference_conditioning_scale=1.0,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
lora_scale=1.0,
device="cuda",
):
# Prepare cameras
cameras = get_orthogonal_camera(
elevation_deg=[0, 0, 0, 0, 0, 0],
distance=[1.8] * num_views,
left=-0.55,
right=0.55,
bottom=-0.55,
top=0.55,
azimuth_deg=[x - 90 for x in [0, 45, 90, 180, 270, 315]],
device=device,
)
plucker_embeds = get_plucker_embeds_from_cameras_ortho(
cameras.c2w, [1.1] * num_views, width
)
control_images = ((plucker_embeds + 1.0) / 2.0).clamp(0, 1)
# Prepare image
reference_image = Image.open(image) if isinstance(image, str) else image
if remove_bg_fn is not None:
reference_image = remove_bg_fn(reference_image)
reference_image = preprocess_image(reference_image, height, width)
elif reference_image.mode == "RGBA":
reference_image = preprocess_image(reference_image, height, width)
pipe_kwargs = {}
if seed != -1 and isinstance(seed, int):
pipe_kwargs["generator"] = torch.Generator(device=device).manual_seed(seed)
images = pipe(
text,
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_views,
control_image=control_images,
control_conditioning_scale=1.0,
reference_image=reference_image,
reference_conditioning_scale=reference_conditioning_scale,
negative_prompt=negative_prompt,
cross_attention_kwargs={"scale": lora_scale},
**pipe_kwargs,
).images
return images, reference_image
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Models
parser.add_argument(
"--base_model", type=str, default="stabilityai/stable-diffusion-2-1-base"
)
parser.add_argument("--vae_model", type=str, default=None)
parser.add_argument("--unet_model", type=str, default=None)
parser.add_argument("--scheduler", type=str, default=None)
parser.add_argument("--lora_model", type=str, default=None)
parser.add_argument("--adapter_path", type=str, default="huanngzh/mv-adapter")
parser.add_argument("--num_views", type=int, default=6)
# Device
parser.add_argument("--device", type=str, default="cuda")
# Inference
parser.add_argument("--image", type=str, required=True)
parser.add_argument("--text", type=str, default="high quality")
parser.add_argument("--num_inference_steps", type=int, default=50)
parser.add_argument("--guidance_scale", type=float, default=3.0)
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument("--lora_scale", type=float, default=1.0)
parser.add_argument("--reference_conditioning_scale", type=float, default=1.0)
parser.add_argument(
"--negative_prompt",
type=str,
default="watermark, ugly, deformed, noisy, blurry, low contrast",
)
parser.add_argument("--output", type=str, default="output.png")
# Extra
parser.add_argument("--remove_bg", action="store_true", help="Remove background")
args = parser.parse_args()
pipe = prepare_pipeline(
base_model=args.base_model,
vae_model=args.vae_model,
unet_model=args.unet_model,
lora_model=args.lora_model,
adapter_path=args.adapter_path,
scheduler=args.scheduler,
num_views=args.num_views,
device=args.device,
dtype=torch.float16,
)
if args.remove_bg:
birefnet = AutoModelForImageSegmentation.from_pretrained(
"ZhengPeng7/BiRefNet", trust_remote_code=True
)
birefnet.to(args.device)
transform_image = transforms.Compose(
[
transforms.Resize((1024, 1024)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
]
)
remove_bg_fn = lambda x: remove_bg(x, birefnet, transform_image, args.device)
else:
remove_bg_fn = None
images, reference_image = run_pipeline(
pipe,
num_views=args.num_views,
text=args.text,
image=args.image,
height=512,
width=512,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
seed=args.seed,
lora_scale=args.lora_scale,
reference_conditioning_scale=args.reference_conditioning_scale,
negative_prompt=args.negative_prompt,
device=args.device,
remove_bg_fn=remove_bg_fn,
)
make_image_grid(images, rows=1).save(args.output)
reference_image.save(args.output.rsplit(".", 1)[0] + "_reference.png")
@@ -1,267 +0,0 @@
import argparse
import numpy as np
import torch
from diffusers import AutoencoderKL, DDPMScheduler, LCMScheduler, UNet2DConditionModel
from PIL import Image
from torchvision import transforms
from tqdm import tqdm
from transformers import AutoModelForImageSegmentation
from mvadapter.pipelines.pipeline_mvadapter_i2mv_sdxl import MVAdapterI2MVSDXLPipeline
from mvadapter.schedulers.scheduling_shift_snr import ShiftSNRScheduler
from mvadapter.utils.mesh_utils import get_orthogonal_camera
from mvadapter.utils.geometry import get_plucker_embeds_from_cameras_ortho
from mvadapter.utils import make_image_grid
def prepare_pipeline(
base_model,
vae_model,
unet_model,
lora_model,
adapter_path,
scheduler,
num_views,
device,
dtype,
):
# Load vae and unet if provided
pipe_kwargs = {}
if vae_model is not None:
pipe_kwargs["vae"] = AutoencoderKL.from_pretrained(vae_model)
if unet_model is not None:
pipe_kwargs["unet"] = UNet2DConditionModel.from_pretrained(unet_model)
# Prepare pipeline
pipe: MVAdapterI2MVSDXLPipeline
pipe = MVAdapterI2MVSDXLPipeline.from_pretrained(base_model, **pipe_kwargs)
# Load scheduler if provided
scheduler_class = None
if scheduler == "ddpm":
scheduler_class = DDPMScheduler
elif scheduler == "lcm":
scheduler_class = LCMScheduler
pipe.scheduler = ShiftSNRScheduler.from_scheduler(
pipe.scheduler,
shift_mode="interpolated",
shift_scale=8.0,
scheduler_class=scheduler_class,
)
pipe.init_custom_adapter(num_views=num_views)
pipe.load_custom_adapter(
adapter_path, weight_name="mvadapter_i2mv_sdxl.safetensors"
)
pipe.to(device=device, dtype=dtype)
pipe.cond_encoder.to(device=device, dtype=dtype)
# load lora if provided
if lora_model is not None:
model_, name_ = lora_model.rsplit("/", 1)
pipe.load_lora_weights(model_, weight_name=name_)
# vae slicing for lower memory usage
pipe.enable_vae_slicing()
return pipe
def remove_bg(image, net, transform, device):
image_size = image.size
input_images = transform(image).unsqueeze(0).to(device)
with torch.no_grad():
preds = net(input_images)[-1].sigmoid().cpu()
pred = preds[0].squeeze()
pred_pil = transforms.ToPILImage()(pred)
mask = pred_pil.resize(image_size)
image.putalpha(mask)
return image
def preprocess_image(image: Image.Image, height, width):
image = np.array(image)
alpha = image[..., 3] > 0
H, W = alpha.shape
# get the bounding box of alpha
y, x = np.where(alpha)
y0, y1 = max(y.min() - 1, 0), min(y.max() + 1, H)
x0, x1 = max(x.min() - 1, 0), min(x.max() + 1, W)
image_center = image[y0:y1, x0:x1]
# resize the longer side to H * 0.9
H, W, _ = image_center.shape
if H > W:
W = int(W * (height * 0.9) / H)
H = int(height * 0.9)
else:
H = int(H * (width * 0.9) / W)
W = int(width * 0.9)
image_center = np.array(Image.fromarray(image_center).resize((W, H)))
# pad to H, W
start_h = (height - H) // 2
start_w = (width - W) // 2
image = np.zeros((height, width, 4), dtype=np.uint8)
image[start_h : start_h + H, start_w : start_w + W] = image_center
image = image.astype(np.float32) / 255.0
image = image[:, :, :3] * image[:, :, 3:4] + (1 - image[:, :, 3:4]) * 0.5
image = (image * 255).clip(0, 255).astype(np.uint8)
image = Image.fromarray(image)
return image
def run_pipeline(
pipe,
num_views,
text,
image,
height,
width,
num_inference_steps,
guidance_scale,
seed,
remove_bg_fn=None,
reference_conditioning_scale=1.0,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
lora_scale=1.0,
device="cuda",
azimuth_deg=None,
):
# Prepare cameras
if azimuth_deg is None:
azimuth_deg = [0, 45, 90, 180, 270, 315]
cameras = get_orthogonal_camera(
elevation_deg=[0] * num_views,
distance=[1.8] * num_views,
left=-0.55,
right=0.55,
bottom=-0.55,
top=0.55,
azimuth_deg=[x - 90 for x in azimuth_deg],
device=device,
)
plucker_embeds = get_plucker_embeds_from_cameras_ortho(
cameras.c2w, [1.1] * num_views, width
)
control_images = ((plucker_embeds + 1.0) / 2.0).clamp(0, 1)
# Prepare image
reference_image = Image.open(image) if isinstance(image, str) else image
if remove_bg_fn is not None:
reference_image = remove_bg_fn(reference_image)
reference_image = preprocess_image(reference_image, height, width)
elif reference_image.mode == "RGBA":
reference_image = preprocess_image(reference_image, height, width)
pipe_kwargs = {}
if seed != -1 and isinstance(seed, int):
pipe_kwargs["generator"] = torch.Generator(device=device).manual_seed(seed)
images = pipe(
text,
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_views,
control_image=control_images,
control_conditioning_scale=1.0,
reference_image=reference_image,
reference_conditioning_scale=reference_conditioning_scale,
negative_prompt=negative_prompt,
cross_attention_kwargs={"scale": lora_scale},
**pipe_kwargs,
).images
return images, reference_image
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Models
parser.add_argument(
"--base_model", type=str, default="stabilityai/stable-diffusion-xl-base-1.0"
)
parser.add_argument(
"--vae_model", type=str, default="madebyollin/sdxl-vae-fp16-fix"
)
parser.add_argument("--unet_model", type=str, default=None)
parser.add_argument("--scheduler", type=str, default=None)
parser.add_argument("--lora_model", type=str, default=None)
parser.add_argument("--adapter_path", type=str, default="huanngzh/mv-adapter")
# Device
parser.add_argument("--device", type=str, default="cuda")
# Inference
parser.add_argument("--num_views", type=int, default=6) # not used
parser.add_argument(
"--azimuth_deg", type=int, nargs="+", default=[0, 45, 90, 180, 270, 315]
)
parser.add_argument("--image", type=str, required=True)
parser.add_argument("--text", type=str, default="high quality")
parser.add_argument("--num_inference_steps", type=int, default=50)
parser.add_argument("--guidance_scale", type=float, default=3.0)
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument("--lora_scale", type=float, default=1.0)
parser.add_argument("--reference_conditioning_scale", type=float, default=1.0)
parser.add_argument(
"--negative_prompt",
type=str,
default="watermark, ugly, deformed, noisy, blurry, low contrast",
)
parser.add_argument("--output", type=str, default="output.png")
# Extra
parser.add_argument("--remove_bg", action="store_true", help="Remove background")
args = parser.parse_args()
num_views = len(args.azimuth_deg)
pipe = prepare_pipeline(
base_model=args.base_model,
vae_model=args.vae_model,
unet_model=args.unet_model,
lora_model=args.lora_model,
adapter_path=args.adapter_path,
scheduler=args.scheduler,
num_views=num_views,
device=args.device,
dtype=torch.float16,
)
if args.remove_bg:
birefnet = AutoModelForImageSegmentation.from_pretrained(
"ZhengPeng7/BiRefNet", trust_remote_code=True
)
birefnet.to(args.device)
transform_image = transforms.Compose(
[
transforms.Resize((1024, 1024)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
]
)
remove_bg_fn = lambda x: remove_bg(x, birefnet, transform_image, args.device)
else:
remove_bg_fn = None
images, reference_image = run_pipeline(
pipe,
num_views=num_views,
text=args.text,
image=args.image,
height=768,
width=768,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
seed=args.seed,
lora_scale=args.lora_scale,
reference_conditioning_scale=args.reference_conditioning_scale,
negative_prompt=args.negative_prompt,
device=args.device,
remove_bg_fn=remove_bg_fn,
azimuth_deg=args.azimuth_deg,
)
make_image_grid(images, rows=1).save(args.output)
reference_image.save(args.output.rsplit(".", 1)[0] + "_reference.png")
@@ -1,303 +0,0 @@
import argparse
import json
import numpy as np
import torch
from diffusers import AutoencoderKL, DDPMScheduler, LCMScheduler, UNet2DConditionModel
from PIL import Image
from torchvision import transforms
from tqdm import tqdm
from transformers import AutoModelForImageSegmentation
from mvadapter.models.attention_processor import DecoupledMVRowColSelfAttnProcessor2_0
from mvadapter.pipelines.pipeline_mvadapter_i2mv_sdxl import MVAdapterI2MVSDXLPipeline
from mvadapter.schedulers.scheduling_shift_snr import ShiftSNRScheduler
from mvadapter.utils import make_image_grid, tensor_to_image
from mvadapter.utils.mesh_utils import (
NVDiffRastContextWrapper,
get_orthogonal_camera,
load_mesh,
render,
)
def prepare_pipeline(
base_model,
vae_model,
unet_model,
lora_model,
adapter_path,
scheduler,
num_views,
device,
dtype,
):
# Load vae and unet if provided
pipe_kwargs = {}
if vae_model is not None:
pipe_kwargs["vae"] = AutoencoderKL.from_pretrained(vae_model)
if unet_model is not None:
pipe_kwargs["unet"] = UNet2DConditionModel.from_pretrained(unet_model)
# Prepare pipeline
pipe: MVAdapterI2MVSDXLPipeline
pipe = MVAdapterI2MVSDXLPipeline.from_pretrained(base_model, **pipe_kwargs)
# Load scheduler if provided
scheduler_class = None
if scheduler == "ddpm":
scheduler_class = DDPMScheduler
elif scheduler == "lcm":
scheduler_class = LCMScheduler
pipe.scheduler = ShiftSNRScheduler.from_scheduler(
pipe.scheduler,
shift_mode="interpolated",
shift_scale=8.0,
scheduler_class=scheduler_class,
)
pipe.init_custom_adapter(
num_views=num_views, self_attn_processor=DecoupledMVRowColSelfAttnProcessor2_0
)
pipe.load_custom_adapter(
adapter_path, weight_name="mvadapter_ig2mv_partial_sdxl.safetensors"
)
pipe.to(device=device, dtype=dtype)
pipe.cond_encoder.to(device=device, dtype=dtype)
# load lora if provided
if lora_model is not None:
model_, name_ = lora_model.rsplit("/", 1)
pipe.load_lora_weights(model_, weight_name=name_)
return pipe
def remove_bg(image, net, transform, device):
image_size = image.size
input_images = transform(image).unsqueeze(0).to(device)
with torch.no_grad():
preds = net(input_images)[-1].sigmoid().cpu()
pred = preds[0].squeeze()
pred_pil = transforms.ToPILImage()(pred)
mask = pred_pil.resize(image_size)
image.putalpha(mask)
return image
def preprocess_image(image: Image.Image, height, width):
image = np.array(image)
alpha = image[..., 3] > 0
H, W = alpha.shape
# get the bounding box of alpha
y, x = np.where(alpha)
y0, y1 = max(y.min() - 1, 0), min(y.max() + 1, H)
x0, x1 = max(x.min() - 1, 0), min(x.max() + 1, W)
image_center = image[y0:y1, x0:x1]
# resize the longer side to H * 0.9
H, W, _ = image_center.shape
if H > W:
W = int(W * (height * 0.9) / H)
H = int(height * 0.9)
else:
H = int(H * (width * 0.9) / W)
W = int(width * 0.9)
image_center = np.array(Image.fromarray(image_center).resize((W, H)))
# pad to H, W
start_h = (height - H) // 2
start_w = (width - W) // 2
image = np.zeros((height, width, 4), dtype=np.uint8)
image[start_h : start_h + H, start_w : start_w + W] = image_center
image = image.astype(np.float32) / 255.0
image = image[:, :, :3] * image[:, :, 3:4] + (1 - image[:, :, 3:4]) * 0.5
image = (image * 255).clip(0, 255).astype(np.uint8)
image = Image.fromarray(image)
return image
def run_pipeline(
pipe,
mesh_path,
num_views,
text,
image,
height,
width,
num_inference_steps,
guidance_scale,
seed,
remove_bg_fn=None,
reference_conditioning_scale=1.0,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
lora_scale=1.0,
device="cuda",
):
# Prepare cameras
cameras = get_orthogonal_camera(
elevation_deg=[0, 0, 0, 0, 89.99, -89.99],
distance=[1.8] * num_views,
left=-0.55,
right=0.55,
bottom=-0.55,
top=0.55,
azimuth_deg=[x - 90 for x in [0, 90, 180, 270, 180, 180]],
device=device,
)
ctx = NVDiffRastContextWrapper(device=device)
mesh, offset, scale = load_mesh(
mesh_path,
rescale=True,
move_to_center=True,
device=device,
return_transform=True,
)
transform_dict = {"offset": offset.tolist(), "scale": scale.tolist()}
render_out = render(
ctx,
mesh,
cameras,
height=height,
width=width,
render_attr=False,
normal_background=0.0,
)
pos_images = tensor_to_image((render_out.pos + 0.5).clamp(0, 1), batched=True)
normal_images = tensor_to_image(
(render_out.normal / 2 + 0.5).clamp(0, 1), batched=True
)
control_images = (
torch.cat(
[
(render_out.pos + 0.5).clamp(0, 1),
(render_out.normal / 2 + 0.5).clamp(0, 1),
],
dim=-1,
)
.permute(0, 3, 1, 2)
.to(device)
)
# Prepare image
reference_image = Image.open(image) if isinstance(image, str) else image
if remove_bg_fn is not None:
reference_image = remove_bg_fn(reference_image)
reference_image = preprocess_image(reference_image, height, width)
elif reference_image.mode == "RGBA":
reference_image = preprocess_image(reference_image, height, width)
pipe_kwargs = {}
if seed != -1 and isinstance(seed, int):
pipe_kwargs["generator"] = torch.Generator(device=device).manual_seed(seed)
images = pipe(
text,
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_views,
control_image=control_images,
control_conditioning_scale=1.0,
reference_image=reference_image,
reference_conditioning_scale=reference_conditioning_scale,
negative_prompt=negative_prompt,
cross_attention_kwargs={"scale": lora_scale},
**pipe_kwargs,
).images
return images, pos_images, normal_images, reference_image, transform_dict
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Models
parser.add_argument(
"--base_model", type=str, default="stabilityai/stable-diffusion-xl-base-1.0"
)
parser.add_argument(
"--vae_model", type=str, default="madebyollin/sdxl-vae-fp16-fix"
)
parser.add_argument("--unet_model", type=str, default=None)
parser.add_argument("--scheduler", type=str, default=None)
parser.add_argument("--lora_model", type=str, default=None)
parser.add_argument("--adapter_path", type=str, default="huanngzh/mv-adapter")
parser.add_argument("--num_views", type=int, default=6)
# Device
parser.add_argument("--device", type=str, default="cuda")
# Inference
parser.add_argument("--mesh", type=str, required=True)
parser.add_argument("--image", type=str, required=True)
parser.add_argument("--text", type=str, required=False, default="high quality")
parser.add_argument("--num_inference_steps", type=int, default=50)
parser.add_argument("--guidance_scale", type=float, default=3.0)
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument("--lora_scale", type=float, default=1.0)
parser.add_argument("--reference_conditioning_scale", type=float, default=1.0)
parser.add_argument(
"--negative_prompt",
type=str,
default="watermark, ugly, deformed, noisy, blurry, low contrast",
)
parser.add_argument("--output", type=str, default="output.png")
# Extra
parser.add_argument("--remove_bg", action="store_true", help="Remove background")
args = parser.parse_args()
pipe = prepare_pipeline(
base_model=args.base_model,
vae_model=args.vae_model,
unet_model=args.unet_model,
lora_model=args.lora_model,
adapter_path=args.adapter_path,
scheduler=args.scheduler,
num_views=args.num_views,
device=args.device,
dtype=torch.float16,
)
if args.remove_bg:
birefnet = AutoModelForImageSegmentation.from_pretrained(
"ZhengPeng7/BiRefNet", trust_remote_code=True
)
birefnet.to(args.device)
transform_image = transforms.Compose(
[
transforms.Resize((1024, 1024)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
]
)
remove_bg_fn = lambda x: remove_bg(x, birefnet, transform_image, args.device)
else:
remove_bg_fn = None
images, pos_images, normal_images, reference_image, transform_dict = run_pipeline(
pipe,
mesh_path=args.mesh,
num_views=args.num_views,
text=args.text,
image=args.image,
height=768,
width=768,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
seed=args.seed,
lora_scale=args.lora_scale,
reference_conditioning_scale=args.reference_conditioning_scale,
negative_prompt=args.negative_prompt,
device=args.device,
remove_bg_fn=remove_bg_fn,
)
make_image_grid(images, rows=1).save(args.output)
make_image_grid(pos_images, rows=1).save(args.output.rsplit(".", 1)[0] + "_pos.png")
make_image_grid(normal_images, rows=1).save(
args.output.rsplit(".", 1)[0] + "_nor.png"
)
reference_image.save(args.output.rsplit(".", 1)[0] + "_reference.png")
with open(args.output.rsplit(".", 1)[0] + "_transform.json", "w") as f:
json.dump(transform_dict, f, indent=4)
@@ -1,313 +0,0 @@
import argparse
import numpy as np
import torch
from diffusers import AutoencoderKL, DDPMScheduler, LCMScheduler, UNet2DConditionModel
from PIL import Image
from torchvision import transforms
from tqdm import tqdm
from transformers import AutoModelForImageSegmentation
from mmgp import offload, profile_type
from mvadapter.models.attention_processor import DecoupledMVRowColSelfAttnProcessor2_0
from mvadapter.pipelines.pipeline_mvadapter_i2mv_sdxl import MVAdapterI2MVSDXLPipeline
from mvadapter.schedulers.scheduling_shift_snr import ShiftSNRScheduler
from mvadapter.utils import make_image_grid, tensor_to_image
from mvadapter.utils.mesh_utils import (
NVDiffRastContextWrapper,
get_orthogonal_camera,
load_mesh,
render,
)
def prepare_pipeline(
base_model,
vae_model,
unet_model,
lora_model,
adapter_path,
scheduler,
num_views,
device,
dtype,
):
# Load vae and unet if provided
pipe_kwargs = {}
if vae_model is not None:
pipe_kwargs["vae"] = AutoencoderKL.from_pretrained(vae_model)
if unet_model is not None:
pipe_kwargs["unet"] = UNet2DConditionModel.from_pretrained(unet_model)
# Prepare pipeline
pipe: MVAdapterI2MVSDXLPipeline
pipe = MVAdapterI2MVSDXLPipeline.from_pretrained(base_model, **pipe_kwargs)
# Load scheduler if provided
scheduler_class = None
if scheduler == "ddpm":
scheduler_class = DDPMScheduler
elif scheduler == "lcm":
scheduler_class = LCMScheduler
pipe.scheduler = ShiftSNRScheduler.from_scheduler(
pipe.scheduler,
shift_mode="interpolated",
shift_scale=8.0,
scheduler_class=scheduler_class,
)
pipe.init_custom_adapter(
num_views=num_views, self_attn_processor=DecoupledMVRowColSelfAttnProcessor2_0
)
pipe.load_custom_adapter(
adapter_path, weight_name="mvadapter_ig2mv_sdxl.safetensors"
)
pipe.to(device=device, dtype=dtype)
pipe.cond_encoder.to(device=device, dtype=dtype)
# load lora if provided
if lora_model is not None:
model_, name_ = lora_model.rsplit("/", 1)
pipe.load_lora_weights(model_, weight_name=name_)
import gc
torch.cuda.empty_cache()
gc.collect()
all_models = {
"vae": pipe.vae,
"text_encoder": pipe.text_encoder,
"text_encoder_2": pipe.text_encoder_2,
"unet": pipe.unet,
"cond_encoder": pipe.cond_encoder,
}
if hasattr(pipe, 'image_encoder') and pipe.image_encoder is not None:
all_models["image_encoder"] = pipe.image_encoder
offload.profile(all_models, profile_type.HighRAM_LowVRAM)
print("mmgp profiling applied successfully.")
return pipe
def remove_bg(image, net, transform, device):
image_size = image.size
input_images = transform(image).unsqueeze(0).to(device)
with torch.no_grad():
preds = net(input_images)[-1].sigmoid().cpu()
pred = preds[0].squeeze()
pred_pil = transforms.ToPILImage()(pred)
mask = pred_pil.resize(image_size)
image.putalpha(mask)
return image
def preprocess_image(image: Image.Image, height, width):
image = np.array(image)
alpha = image[..., 3] > 0
H, W = alpha.shape
# get the bounding box of alpha
y, x = np.where(alpha)
y0, y1 = max(y.min() - 1, 0), min(y.max() + 1, H)
x0, x1 = max(x.min() - 1, 0), min(x.max() + 1, W)
image_center = image[y0:y1, x0:x1]
# resize the longer side to H * 0.9
H, W, _ = image_center.shape
if H > W:
W = int(W * (height * 0.9) / H)
H = int(height * 0.9)
else:
H = int(H * (width * 0.9) / W)
W = int(width * 0.9)
image_center = np.array(Image.fromarray(image_center).resize((W, H)))
# pad to H, W
start_h = (height - H) // 2
start_w = (width - W) // 2
image = np.zeros((height, width, 4), dtype=np.uint8)
image[start_h : start_h + H, start_w : start_w + W] = image_center
image = image.astype(np.float32) / 255.0
image = image[:, :, :3] * image[:, :, 3:4] + (1 - image[:, :, 3:4]) * 0.5
image = (image * 255).clip(0, 255).astype(np.uint8)
image = Image.fromarray(image)
return image
def run_pipeline(
pipe,
mesh_path,
num_views,
text,
image,
height,
width,
num_inference_steps,
guidance_scale,
seed,
remove_bg_fn=None,
reference_conditioning_scale=1.0,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
lora_scale=1.0,
device="cuda",
):
# Prepare cameras
cameras = get_orthogonal_camera(
elevation_deg=[0, 0, 0, 0, 89.99, -89.99],
distance=[1.8] * num_views,
left=-0.55,
right=0.55,
bottom=-0.55,
top=0.55,
azimuth_deg=[x - 90 for x in [0, 90, 180, 270, 180, 180]],
device=device,
)
ctx = NVDiffRastContextWrapper(device=device)
mesh = load_mesh(mesh_path, rescale=True, device=device)
render_out = render(
ctx,
mesh,
cameras,
height=height,
width=width,
render_attr=False,
normal_background=0.0,
)
pos_images = tensor_to_image((render_out.pos + 0.5).clamp(0, 1), batched=True)
normal_images = tensor_to_image(
(render_out.normal / 2 + 0.5).clamp(0, 1), batched=True
)
control_images = (
torch.cat(
[
(render_out.pos + 0.5).clamp(0, 1),
(render_out.normal / 2 + 0.5).clamp(0, 1),
],
dim=-1,
)
.permute(0, 3, 1, 2)
.to(device)
)
# Prepare image
reference_image = Image.open(image) if isinstance(image, str) else image
if remove_bg_fn is not None:
reference_image = remove_bg_fn(reference_image)
reference_image = preprocess_image(reference_image, height, width)
elif reference_image.mode == "RGBA":
reference_image = preprocess_image(reference_image, height, width)
pipe_kwargs = {}
if seed != -1 and isinstance(seed, int):
pipe_kwargs["generator"] = torch.Generator(device=device).manual_seed(seed)
images = pipe(
text,
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_views,
control_image=control_images,
control_conditioning_scale=1.0,
reference_image=reference_image,
reference_conditioning_scale=reference_conditioning_scale,
negative_prompt=negative_prompt,
cross_attention_kwargs={"scale": lora_scale},
**pipe_kwargs,
).images
return images, pos_images, normal_images, reference_image
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Models
parser.add_argument(
"--base_model", type=str, default="stabilityai/stable-diffusion-xl-base-1.0"
)
parser.add_argument(
"--vae_model", type=str, default="madebyollin/sdxl-vae-fp16-fix"
)
parser.add_argument("--unet_model", type=str, default=None)
parser.add_argument("--scheduler", type=str, default=None)
parser.add_argument("--lora_model", type=str, default=None)
parser.add_argument("--adapter_path", type=str, default="huanngzh/mv-adapter")
parser.add_argument("--num_views", type=int, default=6)
# Device
parser.add_argument("--device", type=str, default="cuda")
# Inference
parser.add_argument("--mesh", type=str, required=True)
parser.add_argument("--image", type=str, required=True)
parser.add_argument("--text", type=str, required=False, default="high quality")
parser.add_argument("--num_inference_steps", type=int, default=50)
parser.add_argument("--guidance_scale", type=float, default=3.0)
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument("--lora_scale", type=float, default=1.0)
parser.add_argument("--reference_conditioning_scale", type=float, default=1.0)
parser.add_argument(
"--negative_prompt",
type=str,
default="watermark, ugly, deformed, noisy, blurry, low contrast",
)
parser.add_argument("--output", type=str, default="output.png")
# Extra
parser.add_argument("--remove_bg", action="store_true", help="Remove background")
args = parser.parse_args()
pipe = prepare_pipeline(
base_model=args.base_model,
vae_model=args.vae_model,
unet_model=args.unet_model,
lora_model=args.lora_model,
adapter_path=args.adapter_path,
scheduler=args.scheduler,
num_views=args.num_views,
device=args.device,
dtype=torch.float16,
)
if args.remove_bg:
birefnet = AutoModelForImageSegmentation.from_pretrained(
"ZhengPeng7/BiRefNet", trust_remote_code=True
)
birefnet.to(args.device)
transform_image = transforms.Compose(
[
transforms.Resize((1024, 1024)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
]
)
remove_bg_fn = lambda x: remove_bg(x, birefnet, transform_image, args.device)
else:
remove_bg_fn = None
images, pos_images, normal_images, reference_image = run_pipeline(
pipe,
mesh_path=args.mesh,
num_views=args.num_views,
text=args.text,
image=args.image,
height=768,
width=768,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
seed=args.seed,
lora_scale=args.lora_scale,
reference_conditioning_scale=args.reference_conditioning_scale,
negative_prompt=args.negative_prompt,
device=args.device,
remove_bg_fn=remove_bg_fn,
)
make_image_grid(images, rows=1).save(args.output)
make_image_grid(pos_images, rows=1).save(args.output.rsplit(".", 1)[0] + "_pos.png")
make_image_grid(normal_images, rows=1).save(
args.output.rsplit(".", 1)[0] + "_nor.png"
)
reference_image.save(args.output.rsplit(".", 1)[0] + "_reference.png")
@@ -1,252 +0,0 @@
import argparse
import random
import cv2
import numpy as np
import torch
from controlnet_aux import HEDdetector, PidiNetDetector
from diffusers import (
AutoencoderKL,
ControlNetModel,
DDPMScheduler,
LCMScheduler,
UNet2DConditionModel,
)
from PIL import Image
from mvadapter.pipelines.pipeline_mvadapter_t2mv_sdxl import MVAdapterT2MVSDXLPipeline
from mvadapter.schedulers.scheduling_shift_snr import ShiftSNRScheduler
from mvadapter.utils.mesh_utils import get_orthogonal_camera
from mvadapter.utils.geometry import get_plucker_embeds_from_cameras_ortho
from mvadapter.utils import make_image_grid
def prepare_pipeline(
base_model,
vae_model,
unet_model,
lora_model,
adapter_path,
scheduler,
num_views,
device,
dtype,
):
# Load vae and unet if provided
pipe_kwargs = {}
if vae_model is not None:
pipe_kwargs["vae"] = AutoencoderKL.from_pretrained(vae_model)
if unet_model is not None:
pipe_kwargs["unet"] = UNet2DConditionModel.from_pretrained(unet_model)
# Prepare pipeline
pipe: MVAdapterT2MVSDXLPipeline
pipe = MVAdapterT2MVSDXLPipeline.from_pretrained(base_model, **pipe_kwargs)
# Load scheduler if provided
scheduler_class = None
if scheduler == "ddpm":
scheduler_class = DDPMScheduler
elif scheduler == "lcm":
scheduler_class = LCMScheduler
pipe.scheduler = ShiftSNRScheduler.from_scheduler(
pipe.scheduler,
shift_mode="interpolated",
shift_scale=8.0,
scheduler_class=scheduler_class,
)
pipe.init_custom_adapter(num_views=num_views)
pipe.load_custom_adapter(
adapter_path, weight_name="mvadapter_t2mv_sdxl.safetensors"
)
# ControlNet
pipe.controlnet = ControlNetModel.from_pretrained(
"xinsir/controlnet-scribble-sdxl-1.0"
)
pipe.to(device=device, dtype=dtype)
pipe.cond_encoder.to(device=device, dtype=dtype)
pipe.controlnet.to(device=device, dtype=dtype)
# load lora if provided
if lora_model is not None:
model_, name_ = lora_model.rsplit("/", 1)
pipe.load_lora_weights(model_, weight_name=name_)
# vae slicing for lower memory usage
pipe.enable_vae_slicing()
return pipe
def nms(x, t, s):
x = cv2.GaussianBlur(x.astype(np.float32), (0, 0), s)
f1 = np.array([[0, 0, 0], [1, 1, 1], [0, 0, 0]], dtype=np.uint8)
f2 = np.array([[0, 1, 0], [0, 1, 0], [0, 1, 0]], dtype=np.uint8)
f3 = np.array([[1, 0, 0], [0, 1, 0], [0, 0, 1]], dtype=np.uint8)
f4 = np.array([[0, 0, 1], [0, 1, 0], [1, 0, 0]], dtype=np.uint8)
y = np.zeros_like(x)
for f in [f1, f2, f3, f4]:
np.putmask(y, cv2.dilate(x, kernel=f) == x, x)
z = np.zeros_like(y, dtype=np.uint8)
z[y > t] = 255
return z
def preprocess_controlnet_image(image_path, height, width):
image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
image = cv2.resize(image, (width, height))
if image.shape[2] == 4:
alpha_channel = image[:, :, 3] / 255.0
rgb_channels = image[:, :, :3] / 255.0
gray_background = np.ones_like(rgb_channels) * 0.5
image = (
alpha_channel[..., None] * rgb_channels
+ (1 - alpha_channel[..., None]) * gray_background
)
image = (image * 255).astype(np.uint8)
processor = HEDdetector.from_pretrained("lllyasviel/Annotators")
image = processor(image, scribble=False)
# following is some processing to simulate human sketch draw, different threshold can generate different width of lines
image = np.array(image)
image = nms(image, 127, 3)
image = cv2.GaussianBlur(image, (0, 0), 3)
# higher threshold, thiner line
random_val = int(round(random.uniform(0.01, 0.10), 2) * 255)
image[image > random_val] = 255
image[image < 255] = 0
return Image.fromarray(image)
def run_pipeline(
pipe,
num_views,
text,
height,
width,
num_inference_steps,
guidance_scale,
seed,
controlnet_images,
controlnet_conditioning_scale,
lora_scale=1.0,
device="cuda",
):
# Prepare cameras
cameras = get_orthogonal_camera(
elevation_deg=[0, 0, 0, 0, 0, 0],
distance=[1.8] * num_views,
left=-0.55,
right=0.55,
bottom=-0.55,
top=0.55,
azimuth_deg=[x - 90 for x in [0, 45, 90, 180, 270, 315]],
device=device,
)
plucker_embeds = get_plucker_embeds_from_cameras_ortho(
cameras.c2w, [1.1] * num_views, width
)
control_images = ((plucker_embeds + 1.0) / 2.0).clamp(0, 1)
pipe_kwargs = {}
if seed != -1:
pipe_kwargs["generator"] = torch.Generator(device=device).manual_seed(seed)
# Prepare controlnet images
controlnet_image = [
preprocess_controlnet_image(path, height, width) for path in controlnet_images
]
pipe_kwargs.update(
{
"controlnet_image": controlnet_image,
"controlnet_conditioning_scale": controlnet_conditioning_scale,
}
)
images = pipe(
text,
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_views,
control_image=control_images,
control_conditioning_scale=1.0,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
cross_attention_kwargs={"scale": lora_scale},
**pipe_kwargs,
).images
return images, controlnet_image
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Models
parser.add_argument(
"--base_model", type=str, default="stabilityai/stable-diffusion-xl-base-1.0"
)
parser.add_argument(
"--vae_model", type=str, default="madebyollin/sdxl-vae-fp16-fix"
)
parser.add_argument("--unet_model", type=str, default=None)
parser.add_argument("--scheduler", type=str, default=None)
parser.add_argument("--lora_model", type=str, default=None)
parser.add_argument("--adapter_path", type=str, default="huanngzh/mv-adapter")
parser.add_argument("--num_views", type=int, default=6)
# Device
parser.add_argument("--device", type=str, default="cuda")
# Inference
parser.add_argument("--text", type=str, required=True)
parser.add_argument("--num_inference_steps", type=int, default=50)
parser.add_argument("--guidance_scale", type=float, default=7.0)
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument("--lora_scale", type=float, default=1.0)
parser.add_argument("--output", type=str, default="output.png")
parser.add_argument("--controlnet_images", type=str, nargs="+", required=True)
parser.add_argument("--controlnet_conditioning_scale", type=float, default=1.0)
args = parser.parse_args()
pipe = prepare_pipeline(
base_model=args.base_model,
vae_model=args.vae_model,
unet_model=args.unet_model,
lora_model=args.lora_model,
adapter_path=args.adapter_path,
scheduler=args.scheduler,
num_views=args.num_views,
device=args.device,
dtype=torch.float16,
)
images, controlnet_images = run_pipeline(
pipe,
num_views=args.num_views,
text=args.text,
height=768,
width=768,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
seed=args.seed,
controlnet_images=args.controlnet_images,
controlnet_conditioning_scale=args.controlnet_conditioning_scale,
lora_scale=args.lora_scale,
device=args.device,
)
make_image_grid(images, rows=1).save(args.output)
make_image_grid(controlnet_images, rows=1).save(
args.output.rsplit(".", 1)[0] + "_controlnet.png"
)
@@ -1,170 +0,0 @@
import argparse
import torch
from diffusers import AutoencoderKL, DDPMScheduler, LCMScheduler, UNet2DConditionModel
from mvadapter.pipelines.pipeline_mvadapter_t2mv_sd import MVAdapterT2MVSDPipeline
from mvadapter.schedulers.scheduling_shift_snr import ShiftSNRScheduler
from mvadapter.utils.mesh_utils import get_orthogonal_camera
from mvadapter.utils.geometry import get_plucker_embeds_from_cameras_ortho
from mvadapter.utils import make_image_grid
def prepare_pipeline(
base_model,
vae_model,
unet_model,
lora_model,
adapter_path,
scheduler,
num_views,
device,
dtype,
):
# Load vae and unet if provided
pipe_kwargs = {}
if vae_model is not None:
pipe_kwargs["vae"] = AutoencoderKL.from_pretrained(vae_model)
if unet_model is not None:
pipe_kwargs["unet"] = UNet2DConditionModel.from_pretrained(unet_model)
# Prepare pipeline
pipe: MVAdapterT2MVSDPipeline
pipe = MVAdapterT2MVSDPipeline.from_pretrained(base_model, **pipe_kwargs)
# Load scheduler if provided
scheduler_class = None
if scheduler == "ddpm":
scheduler_class = DDPMScheduler
elif scheduler == "lcm":
scheduler_class = LCMScheduler
pipe.scheduler = ShiftSNRScheduler.from_scheduler(
pipe.scheduler,
shift_mode="interpolated",
shift_scale=8.0,
scheduler_class=scheduler_class,
)
pipe.init_custom_adapter(num_views=num_views)
pipe.load_custom_adapter(
adapter_path, weight_name="mvadapter_t2mv_sd21.safetensors"
)
pipe.to(device=device, dtype=dtype)
pipe.cond_encoder.to(device=device, dtype=dtype)
# load lora if provided
if lora_model is not None:
model_, name_ = lora_model.rsplit("/", 1)
pipe.load_lora_weights(model_, weight_name=name_)
# vae slicing for lower memory usage
pipe.enable_vae_slicing()
return pipe
def run_pipeline(
pipe,
num_views,
text,
height,
width,
num_inference_steps,
guidance_scale,
seed,
negative_prompt,
lora_scale=1.0,
device="cuda",
):
# Prepare cameras
cameras = get_orthogonal_camera(
elevation_deg=[0, 0, 0, 0, 0, 0],
distance=[1.8] * num_views,
left=-0.55,
right=0.55,
bottom=-0.55,
top=0.55,
azimuth_deg=[x - 90 for x in [0, 45, 90, 180, 270, 315]],
device=device,
)
plucker_embeds = get_plucker_embeds_from_cameras_ortho(
cameras.c2w, [1.1] * num_views, width
)
control_images = ((plucker_embeds + 1.0) / 2.0).clamp(0, 1)
pipe_kwargs = {}
if seed != -1:
pipe_kwargs["generator"] = torch.Generator(device=device).manual_seed(seed)
images = pipe(
text,
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_views,
control_image=control_images,
control_conditioning_scale=1.0,
negative_prompt=negative_prompt,
cross_attention_kwargs={"scale": lora_scale},
**pipe_kwargs,
).images
return images
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Models
parser.add_argument(
"--base_model", type=str, default="stabilityai/stable-diffusion-2-1-base"
)
parser.add_argument("--vae_model", type=str, default=None)
parser.add_argument("--unet_model", type=str, default=None)
parser.add_argument("--scheduler", type=str, default=None)
parser.add_argument("--lora_model", type=str, default=None)
parser.add_argument("--adapter_path", type=str, default="huanngzh/mv-adapter")
parser.add_argument("--num_views", type=int, default=6)
# Device
parser.add_argument("--device", type=str, default="cuda")
# Inference
parser.add_argument("--text", type=str, required=True)
parser.add_argument("--num_inference_steps", type=int, default=50)
parser.add_argument("--guidance_scale", type=float, default=7.0)
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument(
"--negative_prompt",
type=str,
default="watermark, ugly, deformed, noisy, blurry, low contrast",
)
parser.add_argument("--lora_scale", type=float, default=1.0)
parser.add_argument("--output", type=str, default="output.png")
args = parser.parse_args()
pipe = prepare_pipeline(
base_model=args.base_model,
vae_model=args.vae_model,
unet_model=args.unet_model,
lora_model=args.lora_model,
adapter_path=args.adapter_path,
scheduler=args.scheduler,
num_views=args.num_views,
device=args.device,
dtype=torch.float16,
)
images = run_pipeline(
pipe,
num_views=args.num_views,
text=args.text,
height=512,
width=512,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
seed=args.seed,
negative_prompt=args.negative_prompt,
lora_scale=args.lora_scale,
device=args.device,
)
make_image_grid(images, rows=1).save(args.output)
@@ -1,201 +0,0 @@
import argparse
import torch
from diffusers import AutoencoderKL, DDPMScheduler, LCMScheduler, UNet2DConditionModel
from mmgp import offload, profile_type
from mvadapter.pipelines.pipeline_mvadapter_t2mv_sdxl import MVAdapterT2MVSDXLPipeline
from mvadapter.schedulers.scheduling_shift_snr import ShiftSNRScheduler
from mvadapter.utils.mesh_utils import get_orthogonal_camera
from mvadapter.utils.geometry import get_plucker_embeds_from_cameras_ortho
from mvadapter.utils import make_image_grid
def prepare_pipeline(
base_model,
vae_model,
unet_model,
lora_model,
adapter_path,
scheduler,
num_views,
device,
dtype,
):
# Load vae and unet if provided
pipe_kwargs = {}
if vae_model is not None:
pipe_kwargs["vae"] = AutoencoderKL.from_pretrained(vae_model)
if unet_model is not None:
pipe_kwargs["unet"] = UNet2DConditionModel.from_pretrained(unet_model)
# Prepare pipeline
pipe: MVAdapterT2MVSDXLPipeline
pipe = MVAdapterT2MVSDXLPipeline.from_pretrained(base_model, **pipe_kwargs)
# Load scheduler if provided
scheduler_class = None
if scheduler == "ddpm":
scheduler_class = DDPMScheduler
elif scheduler == "lcm":
scheduler_class = LCMScheduler
pipe.scheduler = ShiftSNRScheduler.from_scheduler(
pipe.scheduler,
shift_mode="interpolated",
shift_scale=8.0,
scheduler_class=scheduler_class,
)
pipe.init_custom_adapter(num_views=num_views)
pipe.load_custom_adapter(
adapter_path, weight_name="mvadapter_t2mv_sdxl.safetensors"
)
pipe.to(device=device, dtype=dtype)
pipe.cond_encoder.to(device=device, dtype=dtype)
# load lora if provided
if lora_model is not None:
model_, name_ = lora_model.rsplit("/", 1)
pipe.load_lora_weights(model_, weight_name=name_)
import gc
torch.cuda.empty_cache()
gc.collect()
all_models = {
"vae": pipe.vae,
"text_encoder": pipe.text_encoder,
"text_encoder_2": pipe.text_encoder_2,
"unet": pipe.unet,
"cond_encoder": pipe.cond_encoder,
}
if hasattr(pipe, 'image_encoder') and pipe.image_encoder is not None:
all_models["image_encoder"] = pipe.image_encoder
offload.profile(all_models, profile_type.HighRAM_LowVRAM)
print("mmgp profiling applied successfully.")
# vae slicing for lower memory usage
pipe.enable_vae_slicing()
return pipe
def run_pipeline(
pipe,
num_views,
text,
height,
width,
num_inference_steps,
guidance_scale,
seed,
negative_prompt,
lora_scale=1.0,
device="cuda",
azimuth_deg=None,
):
# Prepare cameras
if azimuth_deg is None:
azimuth_deg = [0, 45, 90, 180, 270, 315]
cameras = get_orthogonal_camera(
elevation_deg=[0] * num_views,
distance=[1.8] * num_views,
left=-0.55,
right=0.55,
bottom=-0.55,
top=0.55,
azimuth_deg=[x - 90 for x in azimuth_deg],
device=device,
)
plucker_embeds = get_plucker_embeds_from_cameras_ortho(
cameras.c2w, [1.1] * num_views, width
)
control_images = ((plucker_embeds + 1.0) / 2.0).clamp(0, 1)
pipe_kwargs = {}
if seed != -1:
pipe_kwargs["generator"] = torch.Generator(device=device).manual_seed(seed)
images = pipe(
text,
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_views,
control_image=control_images,
control_conditioning_scale=1.0,
negative_prompt=negative_prompt,
cross_attention_kwargs={"scale": lora_scale},
**pipe_kwargs,
).images
return images
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Models
parser.add_argument(
"--base_model", type=str, default="stabilityai/stable-diffusion-xl-base-1.0"
)
parser.add_argument(
"--vae_model", type=str, default="madebyollin/sdxl-vae-fp16-fix"
)
parser.add_argument("--unet_model", type=str, default=None)
parser.add_argument("--scheduler", type=str, default=None)
parser.add_argument("--lora_model", type=str, default=None)
parser.add_argument("--adapter_path", type=str, default="huanngzh/mv-adapter")
# Device
parser.add_argument("--device", type=str, default="cuda")
# Inference
parser.add_argument("--num_views", type=int, default=6)
parser.add_argument(
"--azimuth_deg", type=int, nargs="+", default=[0, 45, 90, 180, 270, 315]
)
parser.add_argument("--text", type=str, required=True)
parser.add_argument("--num_inference_steps", type=int, default=50)
parser.add_argument("--guidance_scale", type=float, default=7.0)
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument(
"--negative_prompt",
type=str,
default="watermark, ugly, deformed, noisy, blurry, low contrast",
)
parser.add_argument("--lora_scale", type=float, default=1.0)
parser.add_argument("--output", type=str, default="output.png")
args = parser.parse_args()
num_views = len(args.azimuth_deg)
pipe = prepare_pipeline(
base_model=args.base_model,
vae_model=args.vae_model,
unet_model=args.unet_model,
lora_model=args.lora_model,
adapter_path=args.adapter_path,
scheduler=args.scheduler,
num_views=num_views,
device=args.device,
dtype=torch.float16,
)
images = run_pipeline(
pipe,
num_views=num_views,
text=args.text,
height=768,
width=768,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
seed=args.seed,
negative_prompt=args.negative_prompt,
lora_scale=args.lora_scale,
device=args.device,
azimuth_deg=args.azimuth_deg,
)
make_image_grid(images, rows=1).save(args.output)
@@ -1,200 +0,0 @@
import argparse
import torch
from diffusers import AutoencoderKL, DDPMScheduler, LCMScheduler, UNet2DConditionModel
from mvadapter.models.attention_processor import DecoupledMVRowColSelfAttnProcessor2_0
from mvadapter.pipelines.pipeline_mvadapter_t2mv_sdxl import MVAdapterT2MVSDXLPipeline
from mvadapter.schedulers.scheduling_shift_snr import ShiftSNRScheduler
from mvadapter.utils import get_orthogonal_camera, make_image_grid, tensor_to_image
from mvadapter.utils.mesh_utils import NVDiffRastContextWrapper, load_mesh, render
def prepare_pipeline(
base_model,
vae_model,
unet_model,
lora_model,
adapter_path,
scheduler,
num_views,
device,
dtype,
):
# Load vae and unet if provided
pipe_kwargs = {}
if vae_model is not None:
pipe_kwargs["vae"] = AutoencoderKL.from_pretrained(vae_model)
if unet_model is not None:
pipe_kwargs["unet"] = UNet2DConditionModel.from_pretrained(unet_model)
# Prepare pipeline
pipe: MVAdapterT2MVSDXLPipeline
pipe = MVAdapterT2MVSDXLPipeline.from_pretrained(base_model, **pipe_kwargs)
# Load scheduler if provided
scheduler_class = None
if scheduler == "ddpm":
scheduler_class = DDPMScheduler
elif scheduler == "lcm":
scheduler_class = LCMScheduler
pipe.scheduler = ShiftSNRScheduler.from_scheduler(
pipe.scheduler,
shift_mode="interpolated",
shift_scale=8.0,
scheduler_class=scheduler_class,
)
pipe.init_custom_adapter(
num_views=num_views, self_attn_processor=DecoupledMVRowColSelfAttnProcessor2_0
)
pipe.load_custom_adapter(
adapter_path, weight_name="mvadapter_tg2mv_sdxl.safetensors"
)
pipe.to(device=device, dtype=dtype)
pipe.cond_encoder.to(device=device, dtype=dtype)
# load lora if provided
if lora_model is not None:
model_, name_ = lora_model.rsplit("/", 1)
pipe.load_lora_weights(model_, weight_name=name_)
return pipe
def run_pipeline(
pipe,
mesh_path,
num_views,
text,
height,
width,
num_inference_steps,
guidance_scale,
seed,
negative_prompt,
lora_scale=1.0,
device="cuda",
):
# Prepare cameras
cameras = get_orthogonal_camera(
elevation_deg=[0, 0, 0, 0, 89.99, -89.99],
distance=[1.8] * num_views,
left=-0.55,
right=0.55,
bottom=-0.55,
top=0.55,
azimuth_deg=[x - 90 for x in [0, 90, 180, 270, 180, 180]],
device=device,
)
ctx = NVDiffRastContextWrapper(device=device)
mesh = load_mesh(mesh_path, rescale=True, device=device)
render_out = render(
ctx,
mesh,
cameras,
height=height,
width=width,
render_attr=False,
normal_background=0.0,
)
pos_images = tensor_to_image((render_out.pos + 0.5).clamp(0, 1), batched=True)
normal_images = tensor_to_image(
(render_out.normal / 2 + 0.5).clamp(0, 1), batched=True
)
control_images = (
torch.cat(
[
(render_out.pos + 0.5).clamp(0, 1),
(render_out.normal / 2 + 0.5).clamp(0, 1),
],
dim=-1,
)
.permute(0, 3, 1, 2)
.to(device)
)
pipe_kwargs = {}
if seed != -1:
pipe_kwargs["generator"] = torch.Generator(device=device).manual_seed(seed)
images = pipe(
text,
height=height,
width=width,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
num_images_per_prompt=num_views,
control_image=control_images,
control_conditioning_scale=1.0,
negative_prompt=negative_prompt,
cross_attention_kwargs={"scale": lora_scale},
**pipe_kwargs,
).images
return images, pos_images, normal_images
if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Models
parser.add_argument(
"--base_model", type=str, default="stabilityai/stable-diffusion-xl-base-1.0"
)
parser.add_argument(
"--vae_model", type=str, default="madebyollin/sdxl-vae-fp16-fix"
)
parser.add_argument("--unet_model", type=str, default=None)
parser.add_argument("--scheduler", type=str, default=None)
parser.add_argument("--lora_model", type=str, default=None)
parser.add_argument("--adapter_path", type=str, default="huanngzh/mv-adapter")
parser.add_argument("--num_views", type=int, default=6)
# Device
parser.add_argument("--device", type=str, default="cuda")
# Inference
parser.add_argument("--mesh", type=str, required=True)
parser.add_argument("--text", type=str, required=True)
parser.add_argument("--num_inference_steps", type=int, default=50)
parser.add_argument("--guidance_scale", type=float, default=7.0)
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument(
"--negative_prompt",
type=str,
default="watermark, ugly, deformed, noisy, blurry, low contrast",
)
parser.add_argument("--lora_scale", type=float, default=1.0)
parser.add_argument("--output", type=str, default="output.png")
args = parser.parse_args()
pipe = prepare_pipeline(
base_model=args.base_model,
vae_model=args.vae_model,
unet_model=args.unet_model,
lora_model=args.lora_model,
adapter_path=args.adapter_path,
scheduler=args.scheduler,
num_views=args.num_views,
device=args.device,
dtype=torch.float16,
)
images, pos_images, normal_images = run_pipeline(
pipe,
mesh_path=args.mesh,
num_views=args.num_views,
text=args.text,
height=768,
width=768,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
seed=args.seed,
negative_prompt=args.negative_prompt,
lora_scale=args.lora_scale,
device=args.device,
)
make_image_grid(images, rows=1).save(args.output)
make_image_grid(pos_images, rows=1).save(args.output.rsplit(".", 1)[0] + "_pos.png")
make_image_grid(normal_images, rows=1).save(
args.output.rsplit(".", 1)[0] + "_nor.png"
)
@@ -1,104 +0,0 @@
import argparse
import os
import sys
import torch
from torchvision import transforms
from transformers import AutoModelForImageSegmentation
from mvadapter.pipelines.pipeline_texture import ModProcessConfig, TexturePipeline
from mvadapter.utils import make_image_grid
from .inference_ig2mv_sdxl import prepare_pipeline, remove_bg, run_pipeline
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--device", type=str, default="cuda")
# I/O
parser.add_argument("--mesh", type=str, required=True)
parser.add_argument("--image", type=str, required=True)
parser.add_argument("--text", type=str, default="high quality")
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument("--save_dir", type=str, default="./output")
parser.add_argument("--save_name", type=str, default="i2tex_sample")
# Extra
parser.add_argument("--reference_conditioning_scale", type=float, default=1.0)
parser.add_argument("--preprocess_mesh", action="store_true")
parser.add_argument("--remove_bg", action="store_true")
args = parser.parse_args()
device = args.device
num_views = 6
# Prepare pipelines
pipe = prepare_pipeline(
base_model="stabilityai/stable-diffusion-xl-base-1.0",
vae_model="madebyollin/sdxl-vae-fp16-fix",
unet_model=None,
lora_model=None,
adapter_path="huanngzh/mv-adapter",
scheduler=None,
num_views=num_views,
device=device,
dtype=torch.float16,
)
if args.remove_bg:
birefnet = AutoModelForImageSegmentation.from_pretrained(
"ZhengPeng7/BiRefNet", trust_remote_code=True
)
birefnet.to(args.device)
transform_image = transforms.Compose(
[
transforms.Resize((1024, 1024)),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
]
)
remove_bg_fn = lambda x: remove_bg(x, birefnet, transform_image, args.device)
else:
remove_bg_fn = None
texture_pipe = TexturePipeline(
upscaler_ckpt_path="./checkpoints/RealESRGAN_x2plus.pth",
inpaint_ckpt_path="./checkpoints/big-lama.pt",
device=device,
)
print("Pipeline ready.")
os.makedirs(args.save_dir, exist_ok=True)
# 1. run MV-Adapter to generate multi-view images
images, _, _, _ = run_pipeline(
pipe,
mesh_path=args.mesh,
num_views=num_views,
text=args.text,
image=args.image,
height=768,
width=768,
num_inference_steps=50,
guidance_scale=3.0,
seed=args.seed,
reference_conditioning_scale=args.reference_conditioning_scale,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
device=device,
remove_bg_fn=remove_bg_fn,
)
mv_path = os.path.join(args.save_dir, f"{args.save_name}.png")
make_image_grid(images, rows=1).save(mv_path)
torch.cuda.empty_cache()
# 2. un-project and complete texture
out = texture_pipe(
mesh_path=args.mesh,
save_dir=args.save_dir,
save_name=args.save_name,
uv_unwarp=True,
preprocess_mesh=args.preprocess_mesh,
uv_size=4096,
rgb_path=mv_path,
rgb_process_config=ModProcessConfig(view_upscale=True, inpaint_mode="view"),
camera_azimuth_deg=[x - 90 for x in [0, 90, 180, 270, 180, 180]],
)
print(f"Output saved to {out.shaded_model_save_path}")
@@ -1,80 +0,0 @@
import argparse
import os
import sys
import torch
from mvadapter.pipelines.pipeline_texture import ModProcessConfig, TexturePipeline
from mvadapter.utils import make_image_grid
from .inference_tg2mv_sdxl import prepare_pipeline, run_pipeline
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--device", type=str, default="cuda")
# I/O
parser.add_argument("--mesh", type=str, required=True)
parser.add_argument("--text", type=str, required=True)
parser.add_argument("--seed", type=int, default=-1)
parser.add_argument("--save_dir", type=str, default="./output")
parser.add_argument("--save_name", type=str, default="t2tex_sample")
# Extra
parser.add_argument("--preprocess_mesh", action="store_true")
args = parser.parse_args()
device = args.device
num_views = 6
# Prepare pipelines
pipe = prepare_pipeline(
base_model="stabilityai/stable-diffusion-xl-base-1.0",
vae_model="madebyollin/sdxl-vae-fp16-fix",
unet_model=None,
lora_model=None,
adapter_path="huanngzh/mv-adapter",
scheduler=None,
num_views=num_views,
device=device,
dtype=torch.float16,
)
texture_pipe = TexturePipeline(
upscaler_ckpt_path="./checkpoints/RealESRGAN_x2plus.pth",
inpaint_ckpt_path="./checkpoints/big-lama.pt",
device=device,
)
print("Pipeline ready.")
os.makedirs(args.save_dir, exist_ok=True)
# 1. run MV-Adapter to generate multi-view images
images, pos_images, normal_images = run_pipeline(
pipe,
mesh_path=args.mesh,
num_views=num_views,
text=args.text,
height=768,
width=768,
num_inference_steps=50,
guidance_scale=7.0,
seed=args.seed,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
device=device,
)
mv_path = os.path.join(args.save_dir, f"{args.save_name}.png")
make_image_grid(images, rows=1).save(mv_path)
torch.cuda.empty_cache()
# 2. un-project and complete texture
out = texture_pipe(
mesh_path=args.mesh,
save_dir=args.save_dir,
save_name=args.save_name,
uv_unwarp=True,
preprocess_mesh=args.preprocess_mesh,
uv_size=4096,
rgb_path=mv_path,
rgb_process_config=ModProcessConfig(view_upscale=True, inpaint_mode="view"),
camera_azimuth_deg=[x - 90 for x in [0, 90, 180, 270, 180, 180]],
)
print(f"Output saved to {out.shaded_model_save_path}")
-59
View File
@@ -1,59 +0,0 @@
from setuptools import find_packages, setup
setup(
name="mvadapter",
version="0.1.0",
description="MV-Adapter: A Multi-View Adapter for 3D Generation",
author="Zehuan Huang",
packages=[
"mvadapter",
"mvadapter.loaders",
"mvadapter.models",
"mvadapter.pipelines",
"mvadapter.schedulers",
"mvadapter.utils",
"mvadapter.utils.mesh_utils",
],
install_requires=[
"torch>=2.0.0",
"torchvision",
"controlnet_aux",
"diffusers",
"transformers",
"peft",
"numpy",
"huggingface_hub",
"accelerate",
"opencv-python",
"safetensors",
"pillow",
"omegaconf",
"trimesh",
"einops",
"gradio",
"timm",
"kornia",
"scikit-image",
"sentencepiece",
"spandrel",
"open3d",
"pymeshlab",
"cvcuda_cu12",
],
python_requires=">=3.8",
classifiers=[
"Development Status :: 3 - Alpha",
"Intended Audience :: Science/Research",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
],
long_description=open("README.md").read(),
long_description_content_type="text/markdown",
url="https://github.com/huanngzh/MV-Adapter",
project_urls={
"Project Page": "https://huanngzh.github.io/MV-Adapter-Page/",
"Paper": "https://arxiv.org/abs/2412.03632",
"Model Weights": "https://huggingface.co/huanngzh/mv-adapter",
},
)
-138
View File
@@ -1,138 +0,0 @@
#!/bin/bash
# Performance testing script for MV-Adapter - dual run comparison
echo "MV-Adapter Performance Testing - Dual Run"
# Function to get memory usage by process
get_memory_usage() {
local pid=$1
if [ -n "$pid" ] && ps -p $pid > /dev/null 2>&1; then
ps -p $pid -o rss= | awk '{print $1/1024}' 2>/dev/null || echo "0"
else
echo "0"
fi
}
# Function to get VRAM usage
get_vram_usage() {
if command -v nvidia-smi &> /dev/null; then
nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits | awk '{print $1}'
else
echo "0"
fi
}
# Function to run single test
run_test() {
local test_name=$1
local use_mmgp=$2
local output_suffix=$3
echo "========================================="
echo "Running $test_name"
echo "========================================="
# Memory monitoring setup
log_file="memory_${output_suffix}.log"
echo "timestamp,ram_mb,vram_mb" > $log_file
# Build command
cmd="python -m scripts.inference_ig2mv_sdxl \
--image assets/demo/ig2mv/1ccd5c1563ea4f5fb8152eac59dabd5c.jpeg \
--mesh assets/demo/ig2mv/1ccd5c1563ea4f5fb8152eac59dabd5c.glb \
--output output_${output_suffix}.png \
--remove_bg"
if [ "$use_mmgp" = "true" ]; then
cmd="$cmd --mmgp"
fi
# Start test
start_time=$(date +%s.%N)
$cmd &
python_pid=$!
# Monitor memory
max_ram=0
max_vram=0
while ps -p $python_pid > /dev/null 2>&1; do
current_time=$(date +%s.%N)
current_ram=$(get_memory_usage $python_pid)
current_vram=$(get_vram_usage)
echo "${current_time},${current_ram},${current_vram}" >> $log_file
if (( $(echo "$current_ram > $max_ram" | bc -l) )); then
max_ram=$current_ram
fi
if (( $current_vram > $max_vram )); then
max_vram=$current_vram
fi
sleep 1
done
# Wait for completion
wait $python_pid
exit_code=$?
# Calculate execution time
end_time=$(date +%s.%N)
execution_time=$(echo "$end_time - $start_time" | bc -l)
# Display results
echo "Results for $test_name:"
echo " Execution time: ${execution_time} seconds"
echo " Max RAM usage: ${max_ram} MB"
echo " Max VRAM usage: ${max_vram} MB"
echo " Exit code: ${exit_code}"
echo ""
# Save results
{
echo "Test: $test_name"
echo "Date: $(date)"
echo "Execution time: ${execution_time} seconds"
echo "Max RAM usage: ${max_ram} MB"
echo "Max VRAM usage: ${max_vram} MB"
echo "Exit code: ${exit_code}"
echo "Log file: ${log_file}"
echo "========================================="
} >> "dual_test_results.txt"
if [ $exit_code -eq 0 ]; then
echo "✓ $test_name completed successfully"
if [ -f "output_${output_suffix}.png" ]; then
ls -lh "output_${output_suffix}.png"
fi
else
echo "✗ $test_name failed with exit code: ${exit_code}"
fi
}
# Initialize results file
echo "Dual Performance Test Results" > "dual_test_results.txt"
echo "Generated: $(date)" >> "dual_test_results.txt"
echo "=========================================" >> "dual_test_results.txt"
# Run tests
echo "Starting dual performance tests..."
echo ""
# Test 1: Without mmgp
# run_test "Test without mmgp" "false" "no_mmgp"
# Small delay between tests
# sleep 5
# Test 2: With mmgp
run_test "Test with mmgp" "true" "with_mmgp"
echo "========================================="
echo "DUAL TEST COMPLETE"
echo "========================================="
echo "Results saved to: dual_test_results.txt"
echo "Memory logs saved to: memory_no_mmgp.log and memory_with_mmgp.log"
echo "Output images: output_no_mmgp.png and output_with_mmgp.png"
+15 -15
View File
@@ -108,7 +108,6 @@ from Stable3DGen.pipeline_builders import StableGenPipelineBuilder
from MV_Adapter.mvadapter_node_utils import (
prepare_pipeline as mvadapter_prepare_pipeline,
run_pipeline as mvadapter_run_pipeline,
create_bg_remover,
prepare_t2mv_pipeline as mvadapter_prepare_t2mv_pipeline,
run_t2mv_pipeline as mvadapter_run_t2mv_pipeline
)
@@ -4886,10 +4885,6 @@ class MVAdapter_Image_To_MultiView:
if not mesh_path or not os.path.exists(mesh_path):
raise ValueError(f"Mesh path does not exist: {mesh_path}")
remove_bg_fn = None
if remove_background:
remove_bg_fn = create_bg_remover(DEVICE_STR)
num_views = 6
images, pos_images, normal_images, processed_ref_image = mvadapter_run_pipeline(
pipe=mvadapter_pipe,
@@ -4902,19 +4897,22 @@ class MVAdapter_Image_To_MultiView:
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
seed=seed,
remove_bg_fn=remove_bg_fn,
reference_conditioning_scale=reference_conditioning_scale,
negative_prompt=negative_prompt,
lora_scale=lora_scale,
device=DEVICE_STR,
)
grid_image = make_image_grid(images, rows=1)
# grid_image = make_image_grid(images, rows=1)
grid_tensor = pils_to_torch_imgs([grid_image], device=DEVICE_STR)
# grid_tensor = pils_to_torch_imgs([grid_image], device=DEVICE_STR)
print(f"Generated multiview images: {grid_tensor.shape}")
return (grid_tensor,)
# print(f"Generated multiview images: {grid_tensor.shape}")
# return (grid_tensor,)
# return images
return_images = pils_to_torch_imgs(images, device=DEVICE_STR)
return (return_images,)
class Load_MVAdapter_T2MV_Pipeline:
@@ -5033,11 +5031,13 @@ class MVAdapter_Text_To_MultiView:
)
# Create image grid
grid_image = make_image_grid(images, rows=1)
# grid_image = make_image_grid(images, rows=1)
# Convert PIL image to torch tensor using shared utils
grid_tensor = pils_to_torch_imgs([grid_image], device=DEVICE_STR)
# # Convert PIL image to torch tensor using shared utils
# grid_tensor = pils_to_torch_imgs([grid_image], device=DEVICE_STR)
print(f"Generated T2MV multiview images: {grid_tensor.shape}")
return (grid_tensor,)
# print(f"Generated T2MV multiview images: {grid_tensor.shape}")
# return (grid_tensor,)
return_images = pils_to_torch_imgs(images, device=DEVICE_STR)
return (return_images,)