add mv-adapter ig2mv
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
*.png
|
||||
*.jpg
|
||||
*.jpeg
|
||||
*.gif
|
||||
*.bmp
|
||||
*.tiff
|
||||
*.ico
|
||||
*.webp
|
||||
|
||||
#glb
|
||||
*.glb
|
||||
*.gltf
|
||||
*.obj
|
||||
*.mtl
|
||||
*.ply
|
||||
*.stl
|
||||
*.dae
|
||||
|
||||
#pycache
|
||||
__pycache__
|
||||
|
||||
#checkpoints
|
||||
checkpoints/
|
||||
@@ -0,0 +1,201 @@
|
||||
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.
|
||||
@@ -0,0 +1,471 @@
|
||||
# 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>
|
||||
|
||||

|
||||
|
||||
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"
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
> 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"
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
**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
|
||||
```
|
||||
|
||||

|
||||
|
||||
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
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### 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."
|
||||
```
|
||||
|
||||

|
||||
|
||||
```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
|
||||
```
|
||||
|
||||

|
||||
|
||||
#### 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:
|
||||

|
||||
|
||||
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**
|
||||
|
||||

|
||||
|
||||
**Text to Multiview Generation with LoRA**
|
||||
|
||||

|
||||
|
||||
**Image to Multiview Generation**
|
||||
|
||||

|
||||
|
||||
|
||||
## 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}
|
||||
}
|
||||
```
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
name: ig2mv
|
||||
tag: "r768-ortho-nv6-ig2mv-partialimg-dcrowcol-sdxl"
|
||||
exp_root_dir: "outputs"
|
||||
seed: 42
|
||||
|
||||
data_cls: mvadapter.data.multiview.MultiviewDataModule
|
||||
data:
|
||||
root_dir: data/texture_ortho6view_easylight
|
||||
scene_list: data/objaverse_list_6w.json
|
||||
background_color: gray
|
||||
image_names: ["0000", "0001", "0002", "0003", "0008", "0009"]
|
||||
image_modality: color
|
||||
num_views: 6
|
||||
|
||||
prompt_db_path: data/objaverse_short_captions.json
|
||||
return_prompt: true
|
||||
|
||||
projection_type: ORTHO
|
||||
|
||||
source_image_modality: ["position", "normal"]
|
||||
position_offset: 0.5
|
||||
position_scale: 1.0
|
||||
|
||||
reference_root_dir: ["data/texture_rand_easylight_objaverse"]
|
||||
reference_scene_list: ["data/objaverse_list_6w.json"]
|
||||
reference_image_modality: color
|
||||
reference_image_names: ["0000", "0001", "0002", "0003", "0004"]
|
||||
reference_mask_aug: true
|
||||
|
||||
train_indices: [0, -8]
|
||||
val_indices: [-8, null]
|
||||
test_indices: [-8, null]
|
||||
|
||||
height: 768
|
||||
width: 768
|
||||
|
||||
batch_size: 1
|
||||
num_workers: 16
|
||||
|
||||
system_cls: mvadapter.systems.mvadapter_image_sdxl.MVAdapterImageSDXLSystem
|
||||
system:
|
||||
check_train_every_n_steps: 1000
|
||||
cleanup_after_validation_step: true
|
||||
cleanup_after_test_step: true
|
||||
|
||||
# Model / Adapter
|
||||
pretrained_model_name_or_path: "stabilityai/stable-diffusion-xl-base-1.0"
|
||||
pretrained_vae_name_or_path: "madebyollin/sdxl-vae-fp16-fix"
|
||||
pretrained_adapter_name_or_path: null
|
||||
init_adapter_kwargs:
|
||||
# Multi-view adapter
|
||||
self_attn_processor: "mvadapter.models.attention_processor.DecoupledMVRowColSelfAttnProcessor2_0"
|
||||
# Condition encoder
|
||||
cond_in_channels: 6
|
||||
# For training
|
||||
copy_attn_weights: true
|
||||
zero_init_module_keys: ["to_out_mv", "to_out_ref"]
|
||||
|
||||
# Training
|
||||
train_cond_encoder: true
|
||||
trainable_modules: ["_mv", "_ref"]
|
||||
prompt_drop_prob: 0.1
|
||||
image_drop_prob: 0.1
|
||||
cond_drop_prob: 0.1
|
||||
|
||||
# Noise sampler
|
||||
shift_noise: true
|
||||
shift_noise_mode: interpolated
|
||||
shift_noise_scale: 8
|
||||
|
||||
# Evaluation
|
||||
eval_seed: 42
|
||||
eval_num_inference_steps: 30
|
||||
eval_guidance_scale: 3.0
|
||||
eval_height: ${data.height}
|
||||
eval_width: ${data.width}
|
||||
|
||||
# optimizer definition
|
||||
# you can set different learning rates separately for each group of parameters, but note that if you do this you should specify EVERY trainable parameters
|
||||
optimizer:
|
||||
name: AdamW
|
||||
args:
|
||||
lr: 5e-5
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
params:
|
||||
cond_encoder:
|
||||
lr: 5e-5
|
||||
unet:
|
||||
lr: 5e-5
|
||||
|
||||
scheduler:
|
||||
name: SequentialLR
|
||||
interval: step
|
||||
schedulers:
|
||||
- name: LinearLR
|
||||
interval: step
|
||||
args:
|
||||
start_factor: 1e-6
|
||||
end_factor: 1.0
|
||||
total_iters: 2000
|
||||
- name: ConstantLR
|
||||
interval: step
|
||||
args:
|
||||
factor: 1.0
|
||||
total_iters: 9999999
|
||||
milestones: [2000]
|
||||
|
||||
trainer:
|
||||
max_epochs: 10
|
||||
log_every_n_steps: 10
|
||||
num_sanity_val_steps: 1
|
||||
val_check_interval: 2000
|
||||
enable_progress_bar: true
|
||||
precision: bf16-mixed
|
||||
gradient_clip_val: 1.0
|
||||
strategy: ddp
|
||||
accumulate_grad_batches: 1
|
||||
|
||||
checkpoint:
|
||||
save_last: true # whether to save at each validation time
|
||||
save_top_k: -1
|
||||
every_n_epochs: 9999 # do not save at all for debug purpose
|
||||
@@ -0,0 +1,122 @@
|
||||
name: ig2mv
|
||||
tag: "r768-ortho-nv6-ig2mv-dcrowcol-sdxl"
|
||||
exp_root_dir: "outputs"
|
||||
seed: 42
|
||||
|
||||
data_cls: mvadapter.data.multiview.MultiviewDataModule
|
||||
data:
|
||||
root_dir: data/texture_ortho6view_easylight
|
||||
scene_list: data/objaverse_list_6w.json
|
||||
background_color: gray
|
||||
image_names: ["0000", "0001", "0002", "0003", "0008", "0009"]
|
||||
image_modality: color
|
||||
num_views: 6
|
||||
|
||||
prompt_db_path: data/objaverse_short_captions.json
|
||||
return_prompt: true
|
||||
|
||||
projection_type: ORTHO
|
||||
|
||||
source_image_modality: ["position", "normal"]
|
||||
position_offset: 0.5
|
||||
position_scale: 1.0
|
||||
|
||||
reference_root_dir: ["data/texture_rand_easylight_objaverse"]
|
||||
reference_scene_list: ["data/objaverse_list_6w.json"]
|
||||
reference_image_modality: color
|
||||
reference_image_names: ["0000", "0001", "0002", "0003", "0004"]
|
||||
|
||||
train_indices: [0, -8]
|
||||
val_indices: [-8, null]
|
||||
test_indices: [-8, null]
|
||||
|
||||
height: 768
|
||||
width: 768
|
||||
|
||||
batch_size: 1
|
||||
num_workers: 16
|
||||
|
||||
system_cls: mvadapter.systems.mvadapter_image_sdxl.MVAdapterImageSDXLSystem
|
||||
system:
|
||||
check_train_every_n_steps: 1000
|
||||
cleanup_after_validation_step: true
|
||||
cleanup_after_test_step: true
|
||||
|
||||
# Model / Adapter
|
||||
pretrained_model_name_or_path: "stabilityai/stable-diffusion-xl-base-1.0"
|
||||
pretrained_vae_name_or_path: "madebyollin/sdxl-vae-fp16-fix"
|
||||
pretrained_adapter_name_or_path: null
|
||||
init_adapter_kwargs:
|
||||
# Multi-view adapter
|
||||
self_attn_processor: "mvadapter.models.attention_processor.DecoupledMVRowColSelfAttnProcessor2_0"
|
||||
# Condition encoder
|
||||
cond_in_channels: 6
|
||||
# For training
|
||||
copy_attn_weights: true
|
||||
zero_init_module_keys: ["to_out_mv", "to_out_ref"]
|
||||
|
||||
# Training
|
||||
train_cond_encoder: true
|
||||
trainable_modules: ["_mv", "_ref"]
|
||||
prompt_drop_prob: 0.1
|
||||
image_drop_prob: 0.1
|
||||
cond_drop_prob: 0.1
|
||||
|
||||
# Noise sampler
|
||||
shift_noise: true
|
||||
shift_noise_mode: interpolated
|
||||
shift_noise_scale: 8
|
||||
|
||||
# Evaluation
|
||||
eval_seed: 42
|
||||
eval_num_inference_steps: 30
|
||||
eval_guidance_scale: 3.0
|
||||
eval_height: ${data.height}
|
||||
eval_width: ${data.width}
|
||||
|
||||
# optimizer definition
|
||||
# you can set different learning rates separately for each group of parameters, but note that if you do this you should specify EVERY trainable parameters
|
||||
optimizer:
|
||||
name: AdamW
|
||||
args:
|
||||
lr: 5e-5
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
params:
|
||||
cond_encoder:
|
||||
lr: 5e-5
|
||||
unet:
|
||||
lr: 5e-5
|
||||
|
||||
scheduler:
|
||||
name: SequentialLR
|
||||
interval: step
|
||||
schedulers:
|
||||
- name: LinearLR
|
||||
interval: step
|
||||
args:
|
||||
start_factor: 1e-6
|
||||
end_factor: 1.0
|
||||
total_iters: 2000
|
||||
- name: ConstantLR
|
||||
interval: step
|
||||
args:
|
||||
factor: 1.0
|
||||
total_iters: 9999999
|
||||
milestones: [2000]
|
||||
|
||||
trainer:
|
||||
max_epochs: 10
|
||||
log_every_n_steps: 10
|
||||
num_sanity_val_steps: 1
|
||||
val_check_interval: 2000
|
||||
enable_progress_bar: true
|
||||
precision: bf16-mixed
|
||||
gradient_clip_val: 1.0
|
||||
strategy: ddp
|
||||
accumulate_grad_batches: 1
|
||||
|
||||
checkpoint:
|
||||
save_last: true # whether to save at each validation time
|
||||
save_top_k: -1
|
||||
every_n_epochs: 9999 # do not save at all for debug purpose
|
||||
@@ -0,0 +1,117 @@
|
||||
name: tg2mv
|
||||
tag: "r768-ortho-nv6-tg2mv-dcrowcol-sdxl"
|
||||
exp_root_dir: "outputs"
|
||||
seed: 42
|
||||
|
||||
data_cls: mvadapter.data.multiview.MultiviewDataModule
|
||||
data:
|
||||
root_dir: data/texture_ortho10view_easylight_objaverse
|
||||
scene_list: data/objaverse_list_6w.json
|
||||
background_color: gray
|
||||
image_names: ["0000", "0001", "0002", "0003", "0008", "0009"]
|
||||
image_modality: color
|
||||
num_views: 6
|
||||
|
||||
prompt_db_path: data/objaverse_short_captions.json
|
||||
return_prompt: true
|
||||
|
||||
projection_type: ORTHO
|
||||
|
||||
source_image_modality: ["position", "normal"]
|
||||
position_offset: 0.5
|
||||
position_scale: 1.0
|
||||
|
||||
train_indices: [0, -8]
|
||||
val_indices: [-8, null]
|
||||
test_indices: [-8, null]
|
||||
|
||||
height: 768
|
||||
width: 768
|
||||
|
||||
batch_size: 1
|
||||
num_workers: 16
|
||||
|
||||
system_cls: mvadapter.systems.mvadapter_text_sdxl.MVAdapterTextSDXLSystem
|
||||
system:
|
||||
check_train_every_n_steps: 1000
|
||||
cleanup_after_validation_step: true
|
||||
cleanup_after_test_step: true
|
||||
|
||||
# Model / Adapter
|
||||
pretrained_model_name_or_path: "stabilityai/stable-diffusion-xl-base-1.0"
|
||||
pretrained_vae_name_or_path: "madebyollin/sdxl-vae-fp16-fix"
|
||||
pretrained_adapter_name_or_path: null
|
||||
init_adapter_kwargs:
|
||||
# Multi-view adapter
|
||||
self_attn_processor: "mvadapter.models.attention_processor.DecoupledMVRowColSelfAttnProcessor2_0"
|
||||
# Condition encoder
|
||||
cond_in_channels: 6
|
||||
# For training
|
||||
copy_attn_weights: true
|
||||
zero_init_module_keys: ["to_out_mv"]
|
||||
|
||||
# Training
|
||||
train_cond_encoder: true
|
||||
trainable_modules: ["_mv"]
|
||||
prompt_drop_prob: 0.1
|
||||
image_drop_prob: 0.1
|
||||
cond_drop_prob: 0.1
|
||||
|
||||
# Noise sampler
|
||||
shift_noise: true
|
||||
shift_noise_mode: interpolated
|
||||
shift_noise_scale: 8
|
||||
|
||||
# Evaluation
|
||||
eval_seed: 42
|
||||
eval_num_inference_steps: 30
|
||||
eval_guidance_scale: 3.0
|
||||
eval_height: ${data.height}
|
||||
eval_width: ${data.width}
|
||||
|
||||
# optimizer definition
|
||||
# you can set different learning rates separately for each group of parameters, but note that if you do this you should specify EVERY trainable parameters
|
||||
optimizer:
|
||||
name: AdamW
|
||||
args:
|
||||
lr: 5e-5
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
params:
|
||||
cond_encoder:
|
||||
lr: 5e-5
|
||||
unet:
|
||||
lr: 5e-5
|
||||
|
||||
scheduler:
|
||||
name: SequentialLR
|
||||
interval: step
|
||||
schedulers:
|
||||
- name: LinearLR
|
||||
interval: step
|
||||
args:
|
||||
start_factor: 1e-6
|
||||
end_factor: 1.0
|
||||
total_iters: 2000
|
||||
- name: ConstantLR
|
||||
interval: step
|
||||
args:
|
||||
factor: 1.0
|
||||
total_iters: 9999999
|
||||
milestones: [2000]
|
||||
|
||||
trainer:
|
||||
max_epochs: 10
|
||||
log_every_n_steps: 10
|
||||
num_sanity_val_steps: 1
|
||||
val_check_interval: 2000
|
||||
enable_progress_bar: true
|
||||
precision: bf16-mixed
|
||||
gradient_clip_val: 1.0
|
||||
strategy: ddp
|
||||
accumulate_grad_batches: 1
|
||||
|
||||
checkpoint:
|
||||
save_last: true # whether to save at each validation time
|
||||
save_top_k: -1
|
||||
every_n_epochs: 9999 # do not save at all for debug purpose
|
||||
@@ -0,0 +1,122 @@
|
||||
name: i2mv
|
||||
tag: "r512-ortho-nv6-ele0-sd21"
|
||||
exp_root_dir: "outputs"
|
||||
seed: 42
|
||||
|
||||
data_cls: mvadapter.data.multiview.MultiviewDataModule
|
||||
data:
|
||||
root_dir: "data/texture_ortho10view_easylight_objaverse"
|
||||
scene_list: "data/objaverse_list_6w.json"
|
||||
background_color: gray
|
||||
image_names: ["0000", "0004", "0001", "0002", "0003", "0005"]
|
||||
image_modality: color
|
||||
num_views: 6
|
||||
|
||||
prompt_db_path: data/objaverse_short_captions.json
|
||||
return_prompt: true
|
||||
|
||||
projection_type: ORTHO
|
||||
|
||||
source_image_modality: ["plucker"]
|
||||
plucker_offset: 1.0
|
||||
plucker_scale: 2.0
|
||||
|
||||
reference_root_dir: ["data/texture_rand_easylight_objaverse"]
|
||||
reference_scene_list: ["data/objaverse_list_6w.json"]
|
||||
reference_image_modality: color
|
||||
reference_image_names: ["0000", "0001", "0002", "0003", "0004"]
|
||||
|
||||
train_indices: [0, -8]
|
||||
val_indices: [-8, null]
|
||||
test_indices: [-8, null]
|
||||
|
||||
height: 512
|
||||
width: 512
|
||||
|
||||
batch_size: 1
|
||||
num_workers: 16
|
||||
|
||||
system_cls: mvadapter.systems.mvadapter_image_sd.MVAdapterImageSDSystem
|
||||
system:
|
||||
check_train_every_n_steps: 1000
|
||||
cleanup_after_validation_step: true
|
||||
cleanup_after_test_step: true
|
||||
|
||||
# Model / Adapter
|
||||
pretrained_model_name_or_path: "stabilityai/stable-diffusion-2-1-base"
|
||||
pretrained_vae_name_or_path: null
|
||||
pretrained_adapter_name_or_path: null
|
||||
init_adapter_kwargs:
|
||||
# Multi-view adapter
|
||||
self_attn_processor: "mvadapter.models.attention_processor.DecoupledMVRowSelfAttnProcessor2_0"
|
||||
# Condition encoder
|
||||
cond_in_channels: 6
|
||||
# For training
|
||||
copy_attn_weights: true
|
||||
zero_init_module_keys: ["to_out_mv", "to_out_ref"]
|
||||
|
||||
# Training
|
||||
train_cond_encoder: true
|
||||
trainable_modules: ["_mv", "_ref"]
|
||||
prompt_drop_prob: 0.1
|
||||
image_drop_prob: 0.1
|
||||
cond_drop_prob: 0.1
|
||||
|
||||
# Noise sampler
|
||||
shift_noise: true
|
||||
shift_noise_mode: interpolated
|
||||
shift_noise_scale: 8
|
||||
|
||||
# Evaluation
|
||||
eval_seed: 42
|
||||
eval_num_inference_steps: 30
|
||||
eval_guidance_scale: 3.0
|
||||
eval_height: ${data.height}
|
||||
eval_width: ${data.width}
|
||||
|
||||
# optimizer definition
|
||||
# you can set different learning rates separately for each group of parameters, but note that if you do this you should specify EVERY trainable parameters
|
||||
optimizer:
|
||||
name: AdamW
|
||||
args:
|
||||
lr: 5e-5
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
params:
|
||||
cond_encoder:
|
||||
lr: 5e-5
|
||||
unet:
|
||||
lr: 5e-5
|
||||
|
||||
scheduler:
|
||||
name: SequentialLR
|
||||
interval: step
|
||||
schedulers:
|
||||
- name: LinearLR
|
||||
interval: step
|
||||
args:
|
||||
start_factor: 1e-6
|
||||
end_factor: 1.0
|
||||
total_iters: 2000
|
||||
- name: ConstantLR
|
||||
interval: step
|
||||
args:
|
||||
factor: 1.0
|
||||
total_iters: 9999999
|
||||
milestones: [2000]
|
||||
|
||||
trainer:
|
||||
max_epochs: 5
|
||||
log_every_n_steps: 10
|
||||
num_sanity_val_steps: 1
|
||||
val_check_interval: 2000
|
||||
enable_progress_bar: true
|
||||
precision: bf16-mixed
|
||||
gradient_clip_val: 1.0
|
||||
strategy: ddp
|
||||
accumulate_grad_batches: 1
|
||||
|
||||
checkpoint:
|
||||
save_last: true # whether to save at each validation time
|
||||
save_top_k: -1
|
||||
every_n_epochs: 9999 # do not save at all for debug purpose
|
||||
@@ -0,0 +1,122 @@
|
||||
name: i2mv
|
||||
tag: "r768-ortho-nv6-ele0-sdxl"
|
||||
exp_root_dir: "outputs"
|
||||
seed: 42
|
||||
|
||||
data_cls: mvadapter.data.multiview.MultiviewDataModule
|
||||
data:
|
||||
root_dir: "data/texture_ortho10view_easylight_objaverse"
|
||||
scene_list: "data/objaverse_list_6w.json"
|
||||
background_color: gray
|
||||
image_names: ["0000", "0004", "0001", "0002", "0003", "0005"]
|
||||
image_modality: color
|
||||
num_views: 6
|
||||
|
||||
prompt_db_path: data/objaverse_short_captions.json
|
||||
return_prompt: true
|
||||
|
||||
projection_type: ORTHO
|
||||
|
||||
source_image_modality: ["plucker"]
|
||||
plucker_offset: 1.0
|
||||
plucker_scale: 2.0
|
||||
|
||||
reference_root_dir: ["data/texture_rand_easylight_objaverse"]
|
||||
reference_scene_list: ["data/objaverse_list_6w.json"]
|
||||
reference_image_modality: color
|
||||
reference_image_names: ["0000", "0001", "0002", "0003", "0004"]
|
||||
|
||||
train_indices: [0, -8]
|
||||
val_indices: [-8, null]
|
||||
test_indices: [-8, null]
|
||||
|
||||
height: 768
|
||||
width: 768
|
||||
|
||||
batch_size: 1
|
||||
num_workers: 16
|
||||
|
||||
system_cls: mvadapter.systems.mvadapter_image_sdxl.MVAdapterImageSDXLSystem
|
||||
system:
|
||||
check_train_every_n_steps: 1000
|
||||
cleanup_after_validation_step: true
|
||||
cleanup_after_test_step: true
|
||||
|
||||
# Model / Adapter
|
||||
pretrained_model_name_or_path: "stabilityai/stable-diffusion-xl-base-1.0"
|
||||
pretrained_vae_name_or_path: "madebyollin/sdxl-vae-fp16-fix"
|
||||
pretrained_adapter_name_or_path: null
|
||||
init_adapter_kwargs:
|
||||
# Multi-view adapter
|
||||
self_attn_processor: "mvadapter.models.attention_processor.DecoupledMVRowSelfAttnProcessor2_0"
|
||||
# Condition encoder
|
||||
cond_in_channels: 6
|
||||
# For training
|
||||
copy_attn_weights: true
|
||||
zero_init_module_keys: ["to_out_mv", "to_out_ref"]
|
||||
|
||||
# Training
|
||||
train_cond_encoder: true
|
||||
trainable_modules: ["_mv", "_ref"]
|
||||
prompt_drop_prob: 0.1
|
||||
image_drop_prob: 0.1
|
||||
cond_drop_prob: 0.1
|
||||
|
||||
# Noise sampler
|
||||
shift_noise: true
|
||||
shift_noise_mode: interpolated
|
||||
shift_noise_scale: 8
|
||||
|
||||
# Evaluation
|
||||
eval_seed: 42
|
||||
eval_num_inference_steps: 30
|
||||
eval_guidance_scale: 3.0
|
||||
eval_height: ${data.height}
|
||||
eval_width: ${data.width}
|
||||
|
||||
# optimizer definition
|
||||
# you can set different learning rates separately for each group of parameters, but note that if you do this you should specify EVERY trainable parameters
|
||||
optimizer:
|
||||
name: AdamW
|
||||
args:
|
||||
lr: 5e-5
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
params:
|
||||
cond_encoder:
|
||||
lr: 5e-5
|
||||
unet:
|
||||
lr: 5e-5
|
||||
|
||||
scheduler:
|
||||
name: SequentialLR
|
||||
interval: step
|
||||
schedulers:
|
||||
- name: LinearLR
|
||||
interval: step
|
||||
args:
|
||||
start_factor: 1e-6
|
||||
end_factor: 1.0
|
||||
total_iters: 2000
|
||||
- name: ConstantLR
|
||||
interval: step
|
||||
args:
|
||||
factor: 1.0
|
||||
total_iters: 9999999
|
||||
milestones: [2000]
|
||||
|
||||
trainer:
|
||||
max_epochs: 10
|
||||
log_every_n_steps: 10
|
||||
num_sanity_val_steps: 1
|
||||
val_check_interval: 2000
|
||||
enable_progress_bar: true
|
||||
precision: bf16-mixed
|
||||
gradient_clip_val: 1.0
|
||||
strategy: ddp
|
||||
accumulate_grad_batches: 1
|
||||
|
||||
checkpoint:
|
||||
save_last: true # whether to save at each validation time
|
||||
save_top_k: -1
|
||||
every_n_epochs: 9999 # do not save at all for debug purpose
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
name: i2mv
|
||||
tag: "r768-ortho-nv6-ele0-sdxl-aug-quantity"
|
||||
exp_root_dir: "outputs"
|
||||
seed: 42
|
||||
|
||||
data_cls: mvadapter.data.multiview.MultiviewDataModule
|
||||
data:
|
||||
root_dir: "data/texture_ortho10view_easylight_objaverse"
|
||||
scene_list: "data/objaverse_list_6w.json"
|
||||
background_color: gray
|
||||
image_names: ["0000", "0004", "0001", "0002", "0003", "0005"]
|
||||
image_modality: color
|
||||
num_views: 6
|
||||
random_view_list: [[0, 1, 2, 3, 4, 5], [0, 2, 3, 4], [0, 2, 3], [0, 3]]
|
||||
|
||||
prompt_db_path: data/objaverse_short_captions.json
|
||||
return_prompt: true
|
||||
|
||||
projection_type: ORTHO
|
||||
|
||||
source_image_modality: ["plucker"]
|
||||
plucker_offset: 1.0
|
||||
plucker_scale: 2.0
|
||||
|
||||
reference_root_dir: ["data/texture_rand_easylight_objaverse"]
|
||||
reference_scene_list: ["data/objaverse_list_6w.json"]
|
||||
reference_image_modality: color
|
||||
reference_image_names: ["0000", "0001", "0002", "0003", "0004"]
|
||||
|
||||
train_indices: [0, -8]
|
||||
val_indices: [-8, null]
|
||||
test_indices: [-8, null]
|
||||
|
||||
height: 768
|
||||
width: 768
|
||||
|
||||
batch_size: 1
|
||||
num_workers: 16
|
||||
|
||||
system_cls: mvadapter.systems.mvadapter_image_sdxl.MVAdapterImageSDXLSystem
|
||||
system:
|
||||
check_train_every_n_steps: 1000
|
||||
cleanup_after_validation_step: true
|
||||
cleanup_after_test_step: true
|
||||
|
||||
# Model / Adapter
|
||||
pretrained_model_name_or_path: "stabilityai/stable-diffusion-xl-base-1.0"
|
||||
pretrained_vae_name_or_path: "madebyollin/sdxl-vae-fp16-fix"
|
||||
pretrained_adapter_name_or_path: null
|
||||
init_adapter_kwargs:
|
||||
# Multi-view adapter
|
||||
self_attn_processor: "mvadapter.models.attention_processor.DecoupledMVRowSelfAttnProcessor2_0"
|
||||
# Condition encoder
|
||||
cond_in_channels: 6
|
||||
# For training
|
||||
copy_attn_weights: true
|
||||
zero_init_module_keys: ["to_out_mv", "to_out_ref"]
|
||||
|
||||
# Training
|
||||
train_cond_encoder: true
|
||||
trainable_modules: ["_mv", "_ref"]
|
||||
prompt_drop_prob: 0.1
|
||||
image_drop_prob: 0.1
|
||||
cond_drop_prob: 0.1
|
||||
|
||||
# Noise sampler
|
||||
shift_noise: true
|
||||
shift_noise_mode: interpolated
|
||||
shift_noise_scale: 8
|
||||
|
||||
# Evaluation
|
||||
eval_seed: 42
|
||||
eval_num_inference_steps: 30
|
||||
eval_guidance_scale: 3.0
|
||||
eval_height: ${data.height}
|
||||
eval_width: ${data.width}
|
||||
|
||||
# optimizer definition
|
||||
# you can set different learning rates separately for each group of parameters, but note that if you do this you should specify EVERY trainable parameters
|
||||
optimizer:
|
||||
name: AdamW
|
||||
args:
|
||||
lr: 5e-5
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
params:
|
||||
cond_encoder:
|
||||
lr: 5e-5
|
||||
unet:
|
||||
lr: 5e-5
|
||||
|
||||
scheduler:
|
||||
name: SequentialLR
|
||||
interval: step
|
||||
schedulers:
|
||||
- name: LinearLR
|
||||
interval: step
|
||||
args:
|
||||
start_factor: 1e-6
|
||||
end_factor: 1.0
|
||||
total_iters: 2000
|
||||
- name: ConstantLR
|
||||
interval: step
|
||||
args:
|
||||
factor: 1.0
|
||||
total_iters: 9999999
|
||||
milestones: [2000]
|
||||
|
||||
trainer:
|
||||
max_epochs: 10
|
||||
log_every_n_steps: 10
|
||||
num_sanity_val_steps: 1
|
||||
val_check_interval: 2000
|
||||
enable_progress_bar: true
|
||||
precision: bf16-mixed
|
||||
gradient_clip_val: 1.0
|
||||
strategy: ddp
|
||||
accumulate_grad_batches: 1
|
||||
|
||||
checkpoint:
|
||||
save_last: true # whether to save at each validation time
|
||||
save_top_k: -1
|
||||
every_n_epochs: 9999 # do not save at all for debug purpose
|
||||
@@ -0,0 +1,117 @@
|
||||
name: t2mv
|
||||
tag: "r512-ortho-nv6-ele0-sd"
|
||||
exp_root_dir: "outputs"
|
||||
seed: 42
|
||||
|
||||
data_cls: mvadapter.data.multiview.MultiviewDataModule
|
||||
data:
|
||||
root_dir: "data/texture_ortho10view_easylight_objaverse"
|
||||
scene_list: "data/objaverse_list_6w.json"
|
||||
background_color: gray
|
||||
image_names: ["0000", "0004", "0001", "0002", "0003", "0005"]
|
||||
image_modality: color
|
||||
num_views: 6
|
||||
|
||||
prompt_db_path: data/objaverse_short_captions.json
|
||||
return_prompt: true
|
||||
|
||||
projection_type: ORTHO
|
||||
|
||||
source_image_modality: ["plucker"]
|
||||
plucker_offset: 1.0
|
||||
plucker_scale: 2.0
|
||||
|
||||
train_indices: [0, -8]
|
||||
val_indices: [-8, null]
|
||||
test_indices: [-8, null]
|
||||
|
||||
height: 512
|
||||
width: 512
|
||||
|
||||
batch_size: 1
|
||||
num_workers: 16
|
||||
|
||||
system_cls: mvadapter.systems.mvadapter_text_sd.MVAdapterTextSDSystem
|
||||
system:
|
||||
check_train_every_n_steps: 1000
|
||||
cleanup_after_validation_step: true
|
||||
cleanup_after_test_step: true
|
||||
|
||||
# Model / Adapter
|
||||
pretrained_model_name_or_path: "stabilityai/stable-diffusion-2-1-base"
|
||||
pretrained_vae_name_or_path: null
|
||||
pretrained_adapter_name_or_path: null
|
||||
init_adapter_kwargs:
|
||||
# Multi-view adapter
|
||||
self_attn_processor: "mvadapter.models.attention_processor.DecoupledMVRowSelfAttnProcessor2_0"
|
||||
# Condition encoder
|
||||
cond_in_channels: 6
|
||||
# For training
|
||||
copy_attn_weights: true
|
||||
zero_init_module_keys: ["to_out_mv"]
|
||||
|
||||
# Training
|
||||
train_cond_encoder: true
|
||||
trainable_modules: ["_mv"]
|
||||
prompt_drop_prob: 0.1
|
||||
image_drop_prob: 0.1
|
||||
cond_drop_prob: 0.1
|
||||
|
||||
# Noise sampler
|
||||
shift_noise: true
|
||||
shift_noise_mode: interpolated
|
||||
shift_noise_scale: 8
|
||||
|
||||
# Evaluation
|
||||
eval_seed: 42
|
||||
eval_num_inference_steps: 30
|
||||
eval_guidance_scale: 3.0
|
||||
eval_height: ${data.height}
|
||||
eval_width: ${data.width}
|
||||
|
||||
# optimizer definition
|
||||
# you can set different learning rates separately for each group of parameters, but note that if you do this you should specify EVERY trainable parameters
|
||||
optimizer:
|
||||
name: AdamW
|
||||
args:
|
||||
lr: 5e-5
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
params:
|
||||
cond_encoder:
|
||||
lr: 5e-5
|
||||
unet:
|
||||
lr: 5e-5
|
||||
|
||||
scheduler:
|
||||
name: SequentialLR
|
||||
interval: step
|
||||
schedulers:
|
||||
- name: LinearLR
|
||||
interval: step
|
||||
args:
|
||||
start_factor: 1e-6
|
||||
end_factor: 1.0
|
||||
total_iters: 2000
|
||||
- name: ConstantLR
|
||||
interval: step
|
||||
args:
|
||||
factor: 1.0
|
||||
total_iters: 9999999
|
||||
milestones: [2000]
|
||||
|
||||
trainer:
|
||||
max_epochs: 10
|
||||
log_every_n_steps: 10
|
||||
num_sanity_val_steps: 1
|
||||
val_check_interval: 2000
|
||||
enable_progress_bar: true
|
||||
precision: bf16-mixed
|
||||
gradient_clip_val: 1.0
|
||||
strategy: ddp
|
||||
accumulate_grad_batches: 1
|
||||
|
||||
checkpoint:
|
||||
save_last: true # whether to save at each validation time
|
||||
save_top_k: -1
|
||||
every_n_epochs: 9999 # do not save at all for debug purpose
|
||||
@@ -0,0 +1,117 @@
|
||||
name: t2mv
|
||||
tag: "r768-ortho-nv6-ele0-sdxl"
|
||||
exp_root_dir: "outputs"
|
||||
seed: 42
|
||||
|
||||
data_cls: mvadapter.data.multiview.MultiviewDataModule
|
||||
data:
|
||||
root_dir: "data/texture_ortho10view_easylight_objaverse"
|
||||
scene_list: "data/objaverse_list_6w.json"
|
||||
background_color: gray
|
||||
image_names: ["0000", "0004", "0001", "0002", "0003", "0005"]
|
||||
image_modality: color
|
||||
num_views: 6
|
||||
|
||||
prompt_db_path: data/objaverse_short_captions.json
|
||||
return_prompt: true
|
||||
|
||||
projection_type: ORTHO
|
||||
|
||||
source_image_modality: ["plucker"]
|
||||
plucker_offset: 1.0
|
||||
plucker_scale: 2.0
|
||||
|
||||
train_indices: [0, -8]
|
||||
val_indices: [-8, null]
|
||||
test_indices: [-8, null]
|
||||
|
||||
height: 768
|
||||
width: 768
|
||||
|
||||
batch_size: 1
|
||||
num_workers: 16
|
||||
|
||||
system_cls: mvadapter.systems.mvadapter_text_sdxl.MVAdapterTextSDXLSystem
|
||||
system:
|
||||
check_train_every_n_steps: 1000
|
||||
cleanup_after_validation_step: true
|
||||
cleanup_after_test_step: true
|
||||
|
||||
# Model / Adapter
|
||||
pretrained_model_name_or_path: "stabilityai/stable-diffusion-xl-base-1.0"
|
||||
pretrained_vae_name_or_path: "madebyollin/sdxl-vae-fp16-fix"
|
||||
pretrained_adapter_name_or_path: null
|
||||
init_adapter_kwargs:
|
||||
# Multi-view adapter
|
||||
self_attn_processor: "mvadapter.models.attention_processor.DecoupledMVRowSelfAttnProcessor2_0"
|
||||
# Condition encoder
|
||||
cond_in_channels: 6
|
||||
# For training
|
||||
copy_attn_weights: true
|
||||
zero_init_module_keys: ["to_out_mv"]
|
||||
|
||||
# Training
|
||||
train_cond_encoder: true
|
||||
trainable_modules: ["_mv"]
|
||||
prompt_drop_prob: 0.1
|
||||
image_drop_prob: 0.1
|
||||
cond_drop_prob: 0.1
|
||||
|
||||
# Noise sampler
|
||||
shift_noise: true
|
||||
shift_noise_mode: interpolated
|
||||
shift_noise_scale: 8
|
||||
|
||||
# Evaluation
|
||||
eval_seed: 42
|
||||
eval_num_inference_steps: 30
|
||||
eval_guidance_scale: 3.0
|
||||
eval_height: ${data.height}
|
||||
eval_width: ${data.width}
|
||||
|
||||
# optimizer definition
|
||||
# you can set different learning rates separately for each group of parameters, but note that if you do this you should specify EVERY trainable parameters
|
||||
optimizer:
|
||||
name: AdamW
|
||||
args:
|
||||
lr: 5e-5
|
||||
betas: [0.9, 0.999]
|
||||
weight_decay: 0.01
|
||||
params:
|
||||
cond_encoder:
|
||||
lr: 5e-5
|
||||
unet:
|
||||
lr: 5e-5
|
||||
|
||||
scheduler:
|
||||
name: SequentialLR
|
||||
interval: step
|
||||
schedulers:
|
||||
- name: LinearLR
|
||||
interval: step
|
||||
args:
|
||||
start_factor: 1e-6
|
||||
end_factor: 1.0
|
||||
total_iters: 2000
|
||||
- name: ConstantLR
|
||||
interval: step
|
||||
args:
|
||||
factor: 1.0
|
||||
total_iters: 9999999
|
||||
milestones: [2000]
|
||||
|
||||
trainer:
|
||||
max_epochs: 10
|
||||
log_every_n_steps: 10
|
||||
num_sanity_val_steps: 1
|
||||
val_check_interval: 2000
|
||||
enable_progress_bar: true
|
||||
precision: bf16-mixed
|
||||
gradient_clip_val: 1.0
|
||||
strategy: ddp
|
||||
accumulate_grad_batches: 1
|
||||
|
||||
checkpoint:
|
||||
save_last: true # whether to save at each validation time
|
||||
save_top_k: -1
|
||||
every_n_epochs: 9999 # do not save at all for debug purpose
|
||||
@@ -0,0 +1,221 @@
|
||||
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)
|
||||
@@ -0,0 +1,561 @@
|
||||
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)
|
||||
@@ -0,0 +1 @@
|
||||
from .custom_adapter import CustomAdapterMixin
|
||||
@@ -0,0 +1,132 @@
|
||||
import os
|
||||
from typing import Dict, Optional, Union
|
||||
|
||||
import safetensors
|
||||
import torch
|
||||
from diffusers.utils import _get_model_file, logging
|
||||
from safetensors import safe_open
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class CustomAdapterMixin:
|
||||
def init_custom_adapter(self, *args, **kwargs):
|
||||
self._init_custom_adapter(*args, **kwargs)
|
||||
|
||||
def _init_custom_adapter(self, *args, **kwargs):
|
||||
raise NotImplementedError
|
||||
|
||||
def load_custom_adapter(
|
||||
self,
|
||||
pretrained_model_name_or_path_or_dict: Union[str, Dict[str, torch.Tensor]],
|
||||
weight_name: str,
|
||||
subfolder: Optional[str] = None,
|
||||
local_cache_dir: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
# Load the main state dict first.
|
||||
cache_dir = kwargs.pop("cache_dir", None)
|
||||
force_download = kwargs.pop("force_download", False)
|
||||
proxies = kwargs.pop("proxies", None)
|
||||
local_files_only = kwargs.pop("local_files_only", None)
|
||||
token = kwargs.pop("token", None)
|
||||
revision = kwargs.pop("revision", None)
|
||||
|
||||
user_agent = {
|
||||
"file_type": "attn_procs_weights",
|
||||
"framework": "pytorch",
|
||||
}
|
||||
|
||||
if not isinstance(pretrained_model_name_or_path_or_dict, dict):
|
||||
# First try to load from local cache if provided
|
||||
if local_cache_dir and os.path.exists(os.path.join(local_cache_dir, weight_name)):
|
||||
model_file = os.path.join(local_cache_dir, weight_name)
|
||||
logger.info(f"Loading adapter from local cache: {model_file}")
|
||||
else:
|
||||
try:
|
||||
model_file = _get_model_file(
|
||||
pretrained_model_name_or_path_or_dict,
|
||||
weights_name=weight_name,
|
||||
subfolder=subfolder,
|
||||
cache_dir=cache_dir,
|
||||
force_download=force_download,
|
||||
proxies=proxies,
|
||||
local_files_only=local_files_only,
|
||||
token=token,
|
||||
revision=revision,
|
||||
user_agent=user_agent,
|
||||
)
|
||||
except (OSError, EnvironmentError) as e:
|
||||
# If file not found and we have a custom cache dir, try to download
|
||||
if local_cache_dir:
|
||||
logger.info(f"Adapter not found. Downloading {pretrained_model_name_or_path_or_dict} to {local_cache_dir}")
|
||||
try:
|
||||
# Create directory if it doesn't exist
|
||||
os.makedirs(local_cache_dir, exist_ok=True)
|
||||
|
||||
# Download the repository to local cache
|
||||
snapshot_download(
|
||||
repo_id=pretrained_model_name_or_path_or_dict,
|
||||
local_dir=local_cache_dir,
|
||||
ignore_patterns=["*.yaml", "*.json", "*.py", ".png", ".jpg", ".gif"]
|
||||
)
|
||||
|
||||
# Now try to load from the local cache
|
||||
model_file = os.path.join(local_cache_dir, weight_name)
|
||||
if not os.path.exists(model_file):
|
||||
raise EnvironmentError(f"File {weight_name} not found even after download to {local_cache_dir}")
|
||||
|
||||
except Exception as download_error:
|
||||
logger.error(f"Failed to download adapter: {download_error}")
|
||||
raise e # Re-raise original error
|
||||
else:
|
||||
raise e # Re-raise original error
|
||||
|
||||
if weight_name.endswith(".safetensors"):
|
||||
state_dict = {}
|
||||
with safe_open(model_file, framework="pt", device="cpu") as f:
|
||||
for key in f.keys():
|
||||
state_dict[key] = f.get_tensor(key)
|
||||
else:
|
||||
state_dict = torch.load(model_file, map_location="cpu")
|
||||
else:
|
||||
state_dict = pretrained_model_name_or_path_or_dict
|
||||
|
||||
self._load_custom_adapter(state_dict)
|
||||
|
||||
def _load_custom_adapter(self, state_dict):
|
||||
raise NotImplementedError
|
||||
|
||||
def save_custom_adapter(
|
||||
self,
|
||||
save_directory: Union[str, os.PathLike],
|
||||
weight_name: str,
|
||||
safe_serialization: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
if os.path.isfile(save_directory):
|
||||
logger.error(
|
||||
f"Provided path ({save_directory}) should be a directory, not a file"
|
||||
)
|
||||
return
|
||||
|
||||
if safe_serialization:
|
||||
|
||||
def save_function(weights, filename):
|
||||
return safetensors.torch.save_file(
|
||||
weights, filename, metadata={"format": "pt"}
|
||||
)
|
||||
|
||||
else:
|
||||
save_function = torch.save
|
||||
|
||||
# Save the model
|
||||
state_dict = self._save_custom_adapter(**kwargs)
|
||||
save_function(state_dict, os.path.join(save_directory, weight_name))
|
||||
logger.info(
|
||||
f"Custom adapter weights saved in {os.path.join(save_directory, weight_name)}"
|
||||
)
|
||||
|
||||
def _save_custom_adapter(self):
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,743 @@
|
||||
import math
|
||||
from typing import Callable, List, Optional, Union
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from diffusers.models.attention_processor import Attention
|
||||
from diffusers.models.unets import UNet2DConditionModel
|
||||
from diffusers.utils import deprecate, logging
|
||||
from diffusers.utils.import_utils import is_torch_npu_available, is_xformers_available
|
||||
from einops import rearrange, repeat
|
||||
from torch import nn
|
||||
|
||||
|
||||
def default_set_attn_proc_func(
|
||||
name: str,
|
||||
hidden_size: int,
|
||||
cross_attention_dim: Optional[int],
|
||||
ori_attn_proc: object,
|
||||
) -> object:
|
||||
return ori_attn_proc
|
||||
|
||||
|
||||
def set_unet_2d_condition_attn_processor(
|
||||
unet: UNet2DConditionModel,
|
||||
set_self_attn_proc_func: Callable = default_set_attn_proc_func,
|
||||
set_cross_attn_proc_func: Callable = default_set_attn_proc_func,
|
||||
set_custom_attn_proc_func: Callable = default_set_attn_proc_func,
|
||||
set_self_attn_module_names: Optional[List[str]] = None,
|
||||
set_cross_attn_module_names: Optional[List[str]] = None,
|
||||
set_custom_attn_module_names: Optional[List[str]] = None,
|
||||
) -> None:
|
||||
do_set_processor = lambda name, module_names: (
|
||||
any([name.startswith(module_name) for module_name in module_names])
|
||||
if module_names is not None
|
||||
else True
|
||||
) # prefix match
|
||||
|
||||
attn_procs = {}
|
||||
for name, attn_processor in unet.attn_processors.items():
|
||||
# set attn_processor by default, if module_names is None
|
||||
set_self_attn_processor = do_set_processor(name, set_self_attn_module_names)
|
||||
set_cross_attn_processor = do_set_processor(name, set_cross_attn_module_names)
|
||||
set_custom_attn_processor = do_set_processor(name, set_custom_attn_module_names)
|
||||
|
||||
if name.startswith("mid_block"):
|
||||
hidden_size = unet.config.block_out_channels[-1]
|
||||
elif name.startswith("up_blocks"):
|
||||
block_id = int(name[len("up_blocks.")])
|
||||
hidden_size = list(reversed(unet.config.block_out_channels))[block_id]
|
||||
elif name.startswith("down_blocks"):
|
||||
block_id = int(name[len("down_blocks.")])
|
||||
hidden_size = unet.config.block_out_channels[block_id]
|
||||
|
||||
is_custom = "attn_mid_blocks" in name or "attn_post_blocks" in name
|
||||
if is_custom:
|
||||
attn_procs[name] = (
|
||||
set_custom_attn_proc_func(name, hidden_size, None, attn_processor)
|
||||
if set_custom_attn_processor
|
||||
else attn_processor
|
||||
)
|
||||
else:
|
||||
cross_attention_dim = (
|
||||
None
|
||||
if name.endswith("attn1.processor")
|
||||
else unet.config.cross_attention_dim
|
||||
)
|
||||
if cross_attention_dim is None or "motion_modules" in name:
|
||||
# self attention
|
||||
attn_procs[name] = (
|
||||
set_self_attn_proc_func(
|
||||
name, hidden_size, cross_attention_dim, attn_processor
|
||||
)
|
||||
if set_self_attn_processor
|
||||
else attn_processor
|
||||
)
|
||||
else:
|
||||
# cross attention
|
||||
attn_procs[name] = (
|
||||
set_cross_attn_proc_func(
|
||||
name, hidden_size, cross_attention_dim, attn_processor
|
||||
)
|
||||
if set_cross_attn_processor
|
||||
else attn_processor
|
||||
)
|
||||
|
||||
unet.set_attn_processor(attn_procs)
|
||||
|
||||
|
||||
class DecoupledMVRowSelfAttnProcessor2_0(torch.nn.Module):
|
||||
r"""
|
||||
Attention processor for Decoupled Row-wise Self-Attention and Image Cross-Attention for PyTorch 2.0.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_dim: int,
|
||||
inner_dim: int,
|
||||
num_views: int = 1,
|
||||
name: Optional[str] = None,
|
||||
use_mv: bool = True,
|
||||
use_ref: bool = False,
|
||||
):
|
||||
if not hasattr(F, "scaled_dot_product_attention"):
|
||||
raise ImportError(
|
||||
"DecoupledMVRowSelfAttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0."
|
||||
)
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.num_views = num_views
|
||||
self.name = name # NOTE: need for image cross-attention
|
||||
self.use_mv = use_mv
|
||||
self.use_ref = use_ref
|
||||
|
||||
if self.use_mv:
|
||||
self.to_q_mv = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_k_mv = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_v_mv = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_out_mv = nn.ModuleList(
|
||||
[
|
||||
nn.Linear(in_features=inner_dim, out_features=query_dim, bias=True),
|
||||
nn.Dropout(0.0),
|
||||
]
|
||||
)
|
||||
|
||||
if self.use_ref:
|
||||
self.to_q_ref = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_k_ref = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_v_ref = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_out_ref = nn.ModuleList(
|
||||
[
|
||||
nn.Linear(in_features=inner_dim, out_features=query_dim, bias=True),
|
||||
nn.Dropout(0.0),
|
||||
]
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
attn: Attention,
|
||||
hidden_states: torch.FloatTensor,
|
||||
encoder_hidden_states: Optional[torch.FloatTensor] = None,
|
||||
attention_mask: Optional[torch.FloatTensor] = None,
|
||||
temb: Optional[torch.FloatTensor] = None,
|
||||
mv_scale: float = 1.0,
|
||||
ref_hidden_states: Optional[torch.FloatTensor] = None,
|
||||
ref_scale: float = 1.0,
|
||||
cache_hidden_states: Optional[List[torch.FloatTensor]] = None,
|
||||
use_mv: bool = True,
|
||||
use_ref: bool = True,
|
||||
num_views: Optional[int] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> torch.FloatTensor:
|
||||
"""
|
||||
New args:
|
||||
mv_scale (float): scale for multi-view self-attention.
|
||||
ref_hidden_states (torch.FloatTensor): reference encoder hidden states for image cross-attention.
|
||||
ref_scale (float): scale for image cross-attention.
|
||||
cache_hidden_states (List[torch.FloatTensor]): cache hidden states from reference unet.
|
||||
|
||||
"""
|
||||
if len(args) > 0 or kwargs.get("scale", None) is not None:
|
||||
deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`."
|
||||
deprecate("scale", "1.0.0", deprecation_message)
|
||||
|
||||
if num_views is not None:
|
||||
self.num_views = num_views
|
||||
|
||||
# NEW: cache hidden states for reference unet
|
||||
if cache_hidden_states is not None:
|
||||
cache_hidden_states[self.name] = hidden_states.clone()
|
||||
|
||||
# NEW: whether to use multi-view attention and image cross-attention
|
||||
use_mv = self.use_mv and use_mv
|
||||
use_ref = self.use_ref and use_ref
|
||||
|
||||
residual = hidden_states
|
||||
if attn.spatial_norm is not None:
|
||||
hidden_states = attn.spatial_norm(hidden_states, temb)
|
||||
|
||||
input_ndim = hidden_states.ndim
|
||||
|
||||
if input_ndim == 4:
|
||||
batch_size, channel, height, width = hidden_states.shape
|
||||
hidden_states = hidden_states.view(
|
||||
batch_size, channel, height * width
|
||||
).transpose(1, 2)
|
||||
|
||||
batch_size, sequence_length, _ = (
|
||||
hidden_states.shape
|
||||
if encoder_hidden_states is None
|
||||
else encoder_hidden_states.shape
|
||||
)
|
||||
|
||||
if attention_mask is not None:
|
||||
attention_mask = attn.prepare_attention_mask(
|
||||
attention_mask, sequence_length, batch_size
|
||||
)
|
||||
# scaled_dot_product_attention expects attention_mask shape to be
|
||||
# (batch, heads, source_length, target_length)
|
||||
attention_mask = attention_mask.view(
|
||||
batch_size, attn.heads, -1, attention_mask.shape[-1]
|
||||
)
|
||||
|
||||
if attn.group_norm is not None:
|
||||
hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(
|
||||
1, 2
|
||||
)
|
||||
|
||||
query = attn.to_q(hidden_states)
|
||||
|
||||
# NEW: for decoupled multi-view attention
|
||||
if use_mv:
|
||||
query_mv = self.to_q_mv(hidden_states)
|
||||
|
||||
# NEW: for decoupled reference cross attention
|
||||
if use_ref:
|
||||
query_ref = self.to_q_ref(hidden_states)
|
||||
|
||||
if encoder_hidden_states is None:
|
||||
encoder_hidden_states = hidden_states
|
||||
elif attn.norm_cross:
|
||||
encoder_hidden_states = attn.norm_encoder_hidden_states(
|
||||
encoder_hidden_states
|
||||
)
|
||||
|
||||
key = attn.to_k(encoder_hidden_states)
|
||||
value = attn.to_v(encoder_hidden_states)
|
||||
|
||||
inner_dim = key.shape[-1]
|
||||
head_dim = inner_dim // attn.heads
|
||||
|
||||
query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
|
||||
key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
|
||||
# the output of sdp = (batch, num_heads, seq_len, head_dim)
|
||||
# TODO: add support for attn.scale when we move to Torch 2.1
|
||||
hidden_states = F.scaled_dot_product_attention(
|
||||
query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False
|
||||
)
|
||||
|
||||
hidden_states = hidden_states.transpose(1, 2).reshape(
|
||||
batch_size, -1, attn.heads * head_dim
|
||||
)
|
||||
hidden_states = hidden_states.to(query.dtype)
|
||||
|
||||
####### Decoupled multi-view self-attention ########
|
||||
if use_mv:
|
||||
key_mv = self.to_k_mv(encoder_hidden_states)
|
||||
value_mv = self.to_v_mv(encoder_hidden_states)
|
||||
|
||||
query_mv = query_mv.view(batch_size, -1, attn.heads, head_dim)
|
||||
key_mv = key_mv.view(batch_size, -1, attn.heads, head_dim)
|
||||
value_mv = value_mv.view(batch_size, -1, attn.heads, head_dim)
|
||||
|
||||
height = width = math.isqrt(sequence_length)
|
||||
|
||||
# row self-attention
|
||||
query_mv = rearrange(
|
||||
query_mv,
|
||||
"(b nv) (ih iw) h c -> (b nv ih) iw h c",
|
||||
nv=self.num_views,
|
||||
ih=height,
|
||||
iw=width,
|
||||
).transpose(1, 2)
|
||||
key_mv = rearrange(
|
||||
key_mv,
|
||||
"(b nv) (ih iw) h c -> b ih (nv iw) h c",
|
||||
nv=self.num_views,
|
||||
ih=height,
|
||||
iw=width,
|
||||
)
|
||||
key_mv = (
|
||||
key_mv.repeat_interleave(self.num_views, dim=0)
|
||||
.view(batch_size * height, -1, attn.heads, head_dim)
|
||||
.transpose(1, 2)
|
||||
)
|
||||
value_mv = rearrange(
|
||||
value_mv,
|
||||
"(b nv) (ih iw) h c -> b ih (nv iw) h c",
|
||||
nv=self.num_views,
|
||||
ih=height,
|
||||
iw=width,
|
||||
)
|
||||
value_mv = (
|
||||
value_mv.repeat_interleave(self.num_views, dim=0)
|
||||
.view(batch_size * height, -1, attn.heads, head_dim)
|
||||
.transpose(1, 2)
|
||||
)
|
||||
|
||||
hidden_states_mv = F.scaled_dot_product_attention(
|
||||
query_mv,
|
||||
key_mv,
|
||||
value_mv,
|
||||
dropout_p=0.0,
|
||||
is_causal=False,
|
||||
)
|
||||
hidden_states_mv = rearrange(
|
||||
hidden_states_mv,
|
||||
"(b nv ih) h iw c -> (b nv) (ih iw) (h c)",
|
||||
nv=self.num_views,
|
||||
ih=height,
|
||||
)
|
||||
hidden_states_mv = hidden_states_mv.to(query.dtype)
|
||||
|
||||
# linear proj
|
||||
hidden_states_mv = self.to_out_mv[0](hidden_states_mv)
|
||||
# dropout
|
||||
hidden_states_mv = self.to_out_mv[1](hidden_states_mv)
|
||||
|
||||
if use_ref:
|
||||
reference_hidden_states = ref_hidden_states[self.name]
|
||||
|
||||
key_ref = self.to_k_ref(reference_hidden_states)
|
||||
value_ref = self.to_v_ref(reference_hidden_states)
|
||||
|
||||
query_ref = query_ref.view(batch_size, -1, attn.heads, head_dim).transpose(
|
||||
1, 2
|
||||
)
|
||||
key_ref = key_ref.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
value_ref = value_ref.view(batch_size, -1, attn.heads, head_dim).transpose(
|
||||
1, 2
|
||||
)
|
||||
|
||||
hidden_states_ref = F.scaled_dot_product_attention(
|
||||
query_ref, key_ref, value_ref, dropout_p=0.0, is_causal=False
|
||||
)
|
||||
|
||||
hidden_states_ref = hidden_states_ref.transpose(1, 2).reshape(
|
||||
batch_size, -1, attn.heads * head_dim
|
||||
)
|
||||
hidden_states_ref = hidden_states_ref.to(query.dtype)
|
||||
|
||||
# linear proj
|
||||
hidden_states_ref = self.to_out_ref[0](hidden_states_ref)
|
||||
# dropout
|
||||
hidden_states_ref = self.to_out_ref[1](hidden_states_ref)
|
||||
|
||||
# linear proj
|
||||
hidden_states = attn.to_out[0](hidden_states)
|
||||
# dropout
|
||||
hidden_states = attn.to_out[1](hidden_states)
|
||||
|
||||
if use_mv:
|
||||
hidden_states = hidden_states + hidden_states_mv * mv_scale
|
||||
|
||||
if use_ref:
|
||||
hidden_states = hidden_states + hidden_states_ref * ref_scale
|
||||
|
||||
if input_ndim == 4:
|
||||
hidden_states = hidden_states.transpose(-1, -2).reshape(
|
||||
batch_size, channel, height, width
|
||||
)
|
||||
|
||||
if attn.residual_connection:
|
||||
hidden_states = hidden_states + residual
|
||||
|
||||
hidden_states = hidden_states / attn.rescale_output_factor
|
||||
|
||||
return hidden_states
|
||||
|
||||
def set_num_views(self, num_views: int) -> None:
|
||||
self.num_views = num_views
|
||||
|
||||
|
||||
class DecoupledMVRowColSelfAttnProcessor2_0(torch.nn.Module):
|
||||
r"""
|
||||
Attention processor for Decoupled Row-wise Self-Attention and Image Cross-Attention for PyTorch 2.0.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_dim: int,
|
||||
inner_dim: int,
|
||||
num_views: int = 1,
|
||||
name: Optional[str] = None,
|
||||
use_mv: bool = True,
|
||||
use_ref: bool = False,
|
||||
):
|
||||
if not hasattr(F, "scaled_dot_product_attention"):
|
||||
raise ImportError(
|
||||
"DecoupledMVRowSelfAttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0."
|
||||
)
|
||||
|
||||
super().__init__()
|
||||
|
||||
self.num_views = num_views
|
||||
self.name = name # NOTE: need for image cross-attention
|
||||
self.use_mv = use_mv
|
||||
self.use_ref = use_ref
|
||||
|
||||
if self.use_mv:
|
||||
self.to_q_mv = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_k_mv = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_v_mv = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_out_mv = nn.ModuleList(
|
||||
[
|
||||
nn.Linear(in_features=inner_dim, out_features=query_dim, bias=True),
|
||||
nn.Dropout(0.0),
|
||||
]
|
||||
)
|
||||
|
||||
if self.use_ref:
|
||||
self.to_q_ref = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_k_ref = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_v_ref = nn.Linear(
|
||||
in_features=query_dim, out_features=inner_dim, bias=False
|
||||
)
|
||||
self.to_out_ref = nn.ModuleList(
|
||||
[
|
||||
nn.Linear(in_features=inner_dim, out_features=query_dim, bias=True),
|
||||
nn.Dropout(0.0),
|
||||
]
|
||||
)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
attn: Attention,
|
||||
hidden_states: torch.FloatTensor,
|
||||
encoder_hidden_states: Optional[torch.FloatTensor] = None,
|
||||
attention_mask: Optional[torch.FloatTensor] = None,
|
||||
temb: Optional[torch.FloatTensor] = None,
|
||||
mv_scale: float = 1.0,
|
||||
ref_hidden_states: Optional[torch.FloatTensor] = None,
|
||||
ref_scale: float = 1.0,
|
||||
cache_hidden_states: Optional[List[torch.FloatTensor]] = None,
|
||||
use_mv: bool = True,
|
||||
use_ref: bool = True,
|
||||
num_views: Optional[int] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
) -> torch.FloatTensor:
|
||||
"""
|
||||
New args:
|
||||
mv_scale (float): scale for multi-view self-attention.
|
||||
ref_hidden_states (torch.FloatTensor): reference encoder hidden states for image cross-attention.
|
||||
ref_scale (float): scale for image cross-attention.
|
||||
cache_hidden_states (List[torch.FloatTensor]): cache hidden states from reference unet.
|
||||
|
||||
"""
|
||||
if len(args) > 0 or kwargs.get("scale", None) is not None:
|
||||
deprecation_message = "The `scale` argument is deprecated and will be ignored. Please remove it, as passing it will raise an error in the future. `scale` should directly be passed while calling the underlying pipeline component i.e., via `cross_attention_kwargs`."
|
||||
deprecate("scale", "1.0.0", deprecation_message)
|
||||
|
||||
if num_views is not None:
|
||||
self.num_views = num_views
|
||||
|
||||
# NEW: cache hidden states for reference unet
|
||||
if cache_hidden_states is not None:
|
||||
cache_hidden_states[self.name] = hidden_states.clone()
|
||||
|
||||
# NEW: whether to use multi-view attention and image cross-attention
|
||||
use_mv = self.use_mv and use_mv
|
||||
use_ref = self.use_ref and use_ref
|
||||
|
||||
residual = hidden_states
|
||||
if attn.spatial_norm is not None:
|
||||
hidden_states = attn.spatial_norm(hidden_states, temb)
|
||||
|
||||
input_ndim = hidden_states.ndim
|
||||
|
||||
if input_ndim == 4:
|
||||
batch_size, channel, height, width = hidden_states.shape
|
||||
hidden_states = hidden_states.view(
|
||||
batch_size, channel, height * width
|
||||
).transpose(1, 2)
|
||||
|
||||
batch_size, sequence_length, _ = (
|
||||
hidden_states.shape
|
||||
if encoder_hidden_states is None
|
||||
else encoder_hidden_states.shape
|
||||
)
|
||||
|
||||
if attention_mask is not None:
|
||||
attention_mask = attn.prepare_attention_mask(
|
||||
attention_mask, sequence_length, batch_size
|
||||
)
|
||||
# scaled_dot_product_attention expects attention_mask shape to be
|
||||
# (batch, heads, source_length, target_length)
|
||||
attention_mask = attention_mask.view(
|
||||
batch_size, attn.heads, -1, attention_mask.shape[-1]
|
||||
)
|
||||
|
||||
if attn.group_norm is not None:
|
||||
hidden_states = attn.group_norm(hidden_states.transpose(1, 2)).transpose(
|
||||
1, 2
|
||||
)
|
||||
|
||||
query = attn.to_q(hidden_states)
|
||||
|
||||
# NEW: for decoupled multi-view attention
|
||||
if use_mv:
|
||||
query_mv = self.to_q_mv(hidden_states)
|
||||
|
||||
# NEW: for decoupled reference cross attention
|
||||
if use_ref:
|
||||
query_ref = self.to_q_ref(hidden_states)
|
||||
|
||||
if encoder_hidden_states is None:
|
||||
encoder_hidden_states = hidden_states
|
||||
elif attn.norm_cross:
|
||||
encoder_hidden_states = attn.norm_encoder_hidden_states(
|
||||
encoder_hidden_states
|
||||
)
|
||||
|
||||
key = attn.to_k(encoder_hidden_states)
|
||||
value = attn.to_v(encoder_hidden_states)
|
||||
|
||||
inner_dim = key.shape[-1]
|
||||
head_dim = inner_dim // attn.heads
|
||||
|
||||
query = query.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
|
||||
key = key.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
value = value.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
|
||||
# the output of sdp = (batch, num_heads, seq_len, head_dim)
|
||||
# TODO: add support for attn.scale when we move to Torch 2.1
|
||||
hidden_states = F.scaled_dot_product_attention(
|
||||
query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False
|
||||
)
|
||||
|
||||
hidden_states = hidden_states.transpose(1, 2).reshape(
|
||||
batch_size, -1, attn.heads * head_dim
|
||||
)
|
||||
hidden_states = hidden_states.to(query.dtype)
|
||||
|
||||
####### Decoupled multi-view self-attention ########
|
||||
if use_mv:
|
||||
key_mv = self.to_k_mv(encoder_hidden_states)
|
||||
value_mv = self.to_v_mv(encoder_hidden_states)
|
||||
|
||||
query_mv = query_mv.view(batch_size, -1, attn.heads, head_dim)
|
||||
key_mv = key_mv.view(batch_size, -1, attn.heads, head_dim)
|
||||
value_mv = value_mv.view(batch_size, -1, attn.heads, head_dim)
|
||||
|
||||
height = width = math.isqrt(sequence_length)
|
||||
|
||||
query_mv = rearrange(
|
||||
query_mv,
|
||||
"(b nv) (ih iw) h c -> b nv ih iw h c",
|
||||
nv=self.num_views,
|
||||
ih=height,
|
||||
iw=width,
|
||||
)
|
||||
key_mv = rearrange(
|
||||
key_mv,
|
||||
"(b nv) (ih iw) h c -> b nv ih iw h c",
|
||||
nv=self.num_views,
|
||||
ih=height,
|
||||
iw=width,
|
||||
)
|
||||
value_mv = rearrange(
|
||||
value_mv,
|
||||
"(b nv) (ih iw) h c -> b nv ih iw h c",
|
||||
nv=self.num_views,
|
||||
ih=height,
|
||||
iw=width,
|
||||
)
|
||||
|
||||
# row-wise attention for view 0123 (front, right, back, left)
|
||||
query_mv_0123 = rearrange(
|
||||
query_mv[:, 0:4], "b nv ih iw h c -> (b ih) h (nv iw) c"
|
||||
)
|
||||
key_mv_0123 = rearrange(
|
||||
key_mv[:, 0:4], "b nv ih iw h c -> (b ih) h (nv iw) c"
|
||||
)
|
||||
value_mv_0123 = rearrange(
|
||||
value_mv[:, 0:4], "b nv ih iw h c -> (b ih) h (nv iw) c"
|
||||
)
|
||||
hidden_states_mv_0123 = F.scaled_dot_product_attention(
|
||||
query_mv_0123,
|
||||
key_mv_0123,
|
||||
value_mv_0123,
|
||||
dropout_p=0.0,
|
||||
is_causal=False,
|
||||
)
|
||||
hidden_states_mv_0123 = rearrange(
|
||||
hidden_states_mv_0123,
|
||||
"(b ih) h (nv iw) c -> b nv (ih iw) (h c)",
|
||||
ih=height,
|
||||
iw=height,
|
||||
)
|
||||
|
||||
# col-wise attention for view 0245 (front, back, top, bottom)
|
||||
# flip first
|
||||
query_mv_0245 = torch.cat(
|
||||
[
|
||||
torch.flip(query_mv[:, [0]], [3]), # horizontal flip
|
||||
query_mv[:, [2, 4, 5]],
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
key_mv_0245 = torch.cat(
|
||||
[
|
||||
torch.flip(key_mv[:, [0]], [3]), # horizontal flip
|
||||
key_mv[:, [2, 4, 5]],
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
value_mv_0245 = torch.cat(
|
||||
[
|
||||
torch.flip(value_mv[:, [0]], [3]), # horizontal flip
|
||||
value_mv[:, [2, 4, 5]],
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
# attention
|
||||
query_mv_0245 = rearrange(
|
||||
query_mv_0245, "b nv ih iw h c -> (b iw) h (nv ih) c"
|
||||
)
|
||||
key_mv_0245 = rearrange(key_mv_0245, "b nv ih iw h c -> (b iw) h (nv ih) c")
|
||||
value_mv_0245 = rearrange(
|
||||
value_mv_0245, "b nv ih iw h c -> (b iw) h (nv ih) c"
|
||||
)
|
||||
hidden_states_mv_0245 = F.scaled_dot_product_attention(
|
||||
query_mv_0245,
|
||||
key_mv_0245,
|
||||
value_mv_0245,
|
||||
dropout_p=0.0,
|
||||
is_causal=False,
|
||||
)
|
||||
# flip back
|
||||
hidden_states_mv_0245 = rearrange(
|
||||
hidden_states_mv_0245,
|
||||
"(b iw) h (nv ih) c -> b nv ih iw (h c)",
|
||||
ih=height,
|
||||
iw=height,
|
||||
)
|
||||
hidden_states_mv_0245 = torch.cat(
|
||||
[
|
||||
torch.flip(hidden_states_mv_0245[:, [0]], [3]), # horizontal flip
|
||||
hidden_states_mv_0245[:, [1, 2, 3]],
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
hidden_states_mv_0245 = hidden_states_mv_0245.view(
|
||||
hidden_states_mv_0245.shape[0],
|
||||
hidden_states_mv_0245.shape[1],
|
||||
-1,
|
||||
hidden_states_mv_0245.shape[-1],
|
||||
)
|
||||
|
||||
# combine row and col
|
||||
hidden_states_mv = torch.stack(
|
||||
[
|
||||
(hidden_states_mv_0123[:, 0] + hidden_states_mv_0245[:, 0]) / 2,
|
||||
hidden_states_mv_0123[:, 1],
|
||||
(hidden_states_mv_0123[:, 2] + hidden_states_mv_0245[:, 1]) / 2,
|
||||
hidden_states_mv_0123[:, 3],
|
||||
hidden_states_mv_0245[:, 2],
|
||||
hidden_states_mv_0245[:, 3],
|
||||
],
|
||||
dim=1,
|
||||
)
|
||||
|
||||
hidden_states_mv = hidden_states_mv.view(
|
||||
-1, hidden_states_mv.shape[-2], hidden_states_mv.shape[-1]
|
||||
)
|
||||
hidden_states_mv = hidden_states_mv.to(query.dtype)
|
||||
|
||||
# linear proj
|
||||
hidden_states_mv = self.to_out_mv[0](hidden_states_mv)
|
||||
# dropout
|
||||
hidden_states_mv = self.to_out_mv[1](hidden_states_mv)
|
||||
|
||||
if use_ref:
|
||||
reference_hidden_states = ref_hidden_states[self.name]
|
||||
|
||||
key_ref = self.to_k_ref(reference_hidden_states)
|
||||
value_ref = self.to_v_ref(reference_hidden_states)
|
||||
|
||||
query_ref = query_ref.view(batch_size, -1, attn.heads, head_dim).transpose(
|
||||
1, 2
|
||||
)
|
||||
key_ref = key_ref.view(batch_size, -1, attn.heads, head_dim).transpose(1, 2)
|
||||
value_ref = value_ref.view(batch_size, -1, attn.heads, head_dim).transpose(
|
||||
1, 2
|
||||
)
|
||||
|
||||
hidden_states_ref = F.scaled_dot_product_attention(
|
||||
query_ref, key_ref, value_ref, dropout_p=0.0, is_causal=False
|
||||
)
|
||||
|
||||
hidden_states_ref = hidden_states_ref.transpose(1, 2).reshape(
|
||||
batch_size, -1, attn.heads * head_dim
|
||||
)
|
||||
hidden_states_ref = hidden_states_ref.to(query.dtype)
|
||||
|
||||
# linear proj
|
||||
hidden_states_ref = self.to_out_ref[0](hidden_states_ref)
|
||||
# dropout
|
||||
hidden_states_ref = self.to_out_ref[1](hidden_states_ref)
|
||||
|
||||
# linear proj
|
||||
hidden_states = attn.to_out[0](hidden_states)
|
||||
# dropout
|
||||
hidden_states = attn.to_out[1](hidden_states)
|
||||
|
||||
if use_mv:
|
||||
hidden_states = hidden_states + hidden_states_mv * mv_scale
|
||||
|
||||
if use_ref:
|
||||
hidden_states = hidden_states + hidden_states_ref * ref_scale
|
||||
|
||||
if input_ndim == 4:
|
||||
hidden_states = hidden_states.transpose(-1, -2).reshape(
|
||||
batch_size, channel, height, width
|
||||
)
|
||||
|
||||
if attn.residual_connection:
|
||||
hidden_states = hidden_states + residual
|
||||
|
||||
hidden_states = hidden_states / attn.rescale_output_factor
|
||||
|
||||
return hidden_states
|
||||
|
||||
def set_num_views(self, num_views: int) -> None:
|
||||
self.num_views = num_views
|
||||
@@ -0,0 +1,777 @@
|
||||
# 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
|
||||
@@ -0,0 +1,962 @@
|
||||
# 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, Tuple, Union
|
||||
|
||||
import numpy as np
|
||||
import PIL
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
|
||||
from diffusers.models import (
|
||||
AutoencoderKL,
|
||||
ImageProjection,
|
||||
T2IAdapter,
|
||||
UNet2DConditionModel,
|
||||
)
|
||||
from diffusers.pipelines.stable_diffusion_xl.pipeline_output import (
|
||||
StableDiffusionXLPipelineOutput,
|
||||
)
|
||||
from diffusers.pipelines.stable_diffusion_xl.pipeline_stable_diffusion_xl import (
|
||||
StableDiffusionXLPipeline,
|
||||
rescale_noise_cfg,
|
||||
retrieve_timesteps,
|
||||
)
|
||||
from diffusers.schedulers import KarrasDiffusionSchedulers
|
||||
from diffusers.utils import deprecate, logging
|
||||
from diffusers.utils.torch_utils import randn_tensor
|
||||
from einops import rearrange
|
||||
from transformers import (
|
||||
CLIPImageProcessor,
|
||||
CLIPTextModel,
|
||||
CLIPTextModelWithProjection,
|
||||
CLIPTokenizer,
|
||||
CLIPVisionModelWithProjection,
|
||||
)
|
||||
|
||||
from ..loaders import CustomAdapterMixin
|
||||
from ..models.attention_processor import (
|
||||
DecoupledMVRowSelfAttnProcessor2_0,
|
||||
set_unet_2d_condition_attn_processor,
|
||||
)
|
||||
|
||||
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 MVAdapterI2MVSDXLPipeline(StableDiffusionXLPipeline, CustomAdapterMixin):
|
||||
def __init__(
|
||||
self,
|
||||
vae: AutoencoderKL,
|
||||
text_encoder: CLIPTextModel,
|
||||
text_encoder_2: CLIPTextModelWithProjection,
|
||||
tokenizer: CLIPTokenizer,
|
||||
tokenizer_2: CLIPTokenizer,
|
||||
unet: UNet2DConditionModel,
|
||||
scheduler: KarrasDiffusionSchedulers,
|
||||
image_encoder: CLIPVisionModelWithProjection = None,
|
||||
feature_extractor: CLIPImageProcessor = None,
|
||||
force_zeros_for_empty_prompt: bool = True,
|
||||
add_watermarker: Optional[bool] = None,
|
||||
):
|
||||
super().__init__(
|
||||
vae=vae,
|
||||
text_encoder=text_encoder,
|
||||
text_encoder_2=text_encoder_2,
|
||||
tokenizer=tokenizer,
|
||||
tokenizer_2=tokenizer_2,
|
||||
unet=unet,
|
||||
scheduler=scheduler,
|
||||
image_encoder=image_encoder,
|
||||
feature_extractor=feature_extractor,
|
||||
force_zeros_for_empty_prompt=force_zeros_for_empty_prompt,
|
||||
add_watermarker=add_watermarker,
|
||||
)
|
||||
|
||||
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)}"
|
||||
)
|
||||
|
||||
latents_mean = latents_std = None
|
||||
if (
|
||||
hasattr(self.vae.config, "latents_mean")
|
||||
and self.vae.config.latents_mean is not None
|
||||
):
|
||||
latents_mean = torch.tensor(self.vae.config.latents_mean).view(1, 4, 1, 1)
|
||||
if (
|
||||
hasattr(self.vae.config, "latents_std")
|
||||
and self.vae.config.latents_std is not None
|
||||
):
|
||||
latents_std = torch.tensor(self.vae.config.latents_std).view(1, 4, 1, 1)
|
||||
|
||||
# Offload text encoder if `enable_model_cpu_offload` was enabled
|
||||
if hasattr(self, "final_offload_hook") and self.final_offload_hook is not None:
|
||||
self.text_encoder_2.to("cpu")
|
||||
torch.cuda.empty_cache()
|
||||
|
||||
image = image.to(device=device, dtype=dtype)
|
||||
|
||||
batch_size = batch_size * num_images_per_prompt
|
||||
|
||||
if image.shape[1] == 4:
|
||||
init_latents = image
|
||||
|
||||
else:
|
||||
# make sure the VAE is in float32 mode, as it overflows in float16
|
||||
if self.vae.config.force_upcast:
|
||||
image = image.float()
|
||||
self.vae.to(dtype=torch.float32)
|
||||
|
||||
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):
|
||||
if image.shape[0] < batch_size and batch_size % image.shape[0] == 0:
|
||||
image = torch.cat([image] * (batch_size // image.shape[0]), dim=0)
|
||||
elif image.shape[0] < batch_size and batch_size % image.shape[0] != 0:
|
||||
raise ValueError(
|
||||
f"Cannot duplicate `image` of batch size {image.shape[0]} to effective batch_size {batch_size} "
|
||||
)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
if self.vae.config.force_upcast:
|
||||
self.vae.to(dtype)
|
||||
|
||||
init_latents = init_latents.to(dtype)
|
||||
if latents_mean is not None and latents_std is not None:
|
||||
latents_mean = latents_mean.to(device=device, dtype=dtype)
|
||||
latents_std = latents_std.to(device=device, dtype=dtype)
|
||||
init_latents = (
|
||||
(init_latents - latents_mean)
|
||||
* self.vae.config.scaling_factor
|
||||
/ latents_std
|
||||
)
|
||||
else:
|
||||
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,
|
||||
prompt_2: Optional[Union[str, List[str]]] = None,
|
||||
height: Optional[int] = None,
|
||||
width: Optional[int] = None,
|
||||
num_inference_steps: int = 50,
|
||||
timesteps: List[int] = None,
|
||||
denoising_end: Optional[float] = None,
|
||||
guidance_scale: float = 5.0,
|
||||
negative_prompt: Optional[Union[str, List[str]]] = None,
|
||||
negative_prompt_2: 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.FloatTensor] = None,
|
||||
prompt_embeds: Optional[torch.FloatTensor] = None,
|
||||
negative_prompt_embeds: Optional[torch.FloatTensor] = None,
|
||||
pooled_prompt_embeds: Optional[torch.FloatTensor] = None,
|
||||
negative_pooled_prompt_embeds: Optional[torch.FloatTensor] = None,
|
||||
ip_adapter_image: Optional[PipelineImageInput] = None,
|
||||
ip_adapter_image_embeds: Optional[List[torch.FloatTensor]] = None,
|
||||
output_type: Optional[str] = "pil",
|
||||
return_dict: bool = True,
|
||||
cross_attention_kwargs: Optional[Dict[str, Any]] = None,
|
||||
guidance_rescale: float = 0.0,
|
||||
original_size: Optional[Tuple[int, int]] = None,
|
||||
crops_coords_top_left: Tuple[int, int] = (0, 0),
|
||||
target_size: Optional[Tuple[int, int]] = None,
|
||||
negative_original_size: Optional[Tuple[int, int]] = None,
|
||||
negative_crops_coords_top_left: Tuple[int, int] = (0, 0),
|
||||
negative_target_size: Optional[Tuple[int, int]] = None,
|
||||
clip_skip: Optional[int] = None,
|
||||
callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = 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,
|
||||
**kwargs,
|
||||
):
|
||||
r"""
|
||||
Function invoked when calling the pipeline for generation.
|
||||
|
||||
Args:
|
||||
prompt (`str` or `List[str]`, *optional*):
|
||||
The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`.
|
||||
instead.
|
||||
prompt_2 (`str` or `List[str]`, *optional*):
|
||||
The prompt or prompts to be sent to the `tokenizer_2` and `text_encoder_2`. If not defined, `prompt` is
|
||||
used in both text-encoders
|
||||
height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
|
||||
The height in pixels of the generated image. This is set to 1024 by default for the best results.
|
||||
Anything below 512 pixels won't work well for
|
||||
[stabilityai/stable-diffusion-xl-base-1.0](https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0)
|
||||
and checkpoints that are not specifically fine-tuned on low resolutions.
|
||||
width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
|
||||
The width in pixels of the generated image. This is set to 1024 by default for the best results.
|
||||
Anything below 512 pixels won't work well for
|
||||
[stabilityai/stable-diffusion-xl-base-1.0](https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0)
|
||||
and checkpoints that are not specifically fine-tuned on low resolutions.
|
||||
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.
|
||||
denoising_end (`float`, *optional*):
|
||||
When specified, determines the fraction (between 0.0 and 1.0) of the total denoising process to be
|
||||
completed before it is intentionally prematurely terminated. As a result, the returned sample will
|
||||
still retain a substantial amount of noise as determined by the discrete timesteps selected by the
|
||||
scheduler. The denoising_end parameter should ideally be utilized when this pipeline forms a part of a
|
||||
"Mixture of Denoisers" multi-pipeline setup, as elaborated in [**Refining the Image
|
||||
Output**](https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion/stable_diffusion_xl#refining-the-image-output)
|
||||
guidance_scale (`float`, *optional*, defaults to 5.0):
|
||||
Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).
|
||||
`guidance_scale` is defined as `w` of equation 2. of [Imagen
|
||||
Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >
|
||||
1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`,
|
||||
usually at the expense of lower image quality.
|
||||
negative_prompt (`str` or `List[str]`, *optional*):
|
||||
The prompt or prompts not to guide the image generation. If not defined, one has to pass
|
||||
`negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is
|
||||
less than `1`).
|
||||
negative_prompt_2 (`str` or `List[str]`, *optional*):
|
||||
The prompt or prompts not to guide the image generation to be sent to `tokenizer_2` and
|
||||
`text_encoder_2`. If not defined, `negative_prompt` is used in both text-encoders
|
||||
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 (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to
|
||||
[`schedulers.DDIMScheduler`], will be ignored for others.
|
||||
generator (`torch.Generator` or `List[torch.Generator]`, *optional*):
|
||||
One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)
|
||||
to make generation deterministic.
|
||||
latents (`torch.FloatTensor`, *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 will ge generated by sampling using the supplied random `generator`.
|
||||
prompt_embeds (`torch.FloatTensor`, *optional*):
|
||||
Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
|
||||
provided, text embeddings will be generated from `prompt` input argument.
|
||||
negative_prompt_embeds (`torch.FloatTensor`, *optional*):
|
||||
Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
|
||||
weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
|
||||
argument.
|
||||
pooled_prompt_embeds (`torch.FloatTensor`, *optional*):
|
||||
Pre-generated pooled text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting.
|
||||
If not provided, pooled text embeddings will be generated from `prompt` input argument.
|
||||
negative_pooled_prompt_embeds (`torch.FloatTensor`, *optional*):
|
||||
Pre-generated negative pooled text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
|
||||
weighting. If not provided, pooled negative_prompt_embeds will be generated from `negative_prompt`
|
||||
input argument.
|
||||
ip_adapter_image: (`PipelineImageInput`, *optional*): Optional image input to work with IP Adapters.
|
||||
ip_adapter_image_embeds (`List[torch.FloatTensor]`, *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 generate image. Choose between
|
||||
[PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`.
|
||||
return_dict (`bool`, *optional*, defaults to `True`):
|
||||
Whether or not to return a [`~pipelines.stable_diffusion_xl.StableDiffusionXLPipelineOutput`] instead
|
||||
of a plain tuple.
|
||||
cross_attention_kwargs (`dict`, *optional*):
|
||||
A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
|
||||
`self.processor` in
|
||||
[diffusers.models.attention_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 proposed by [Common Diffusion Noise Schedules and Sample Steps are
|
||||
Flawed](https://arxiv.org/pdf/2305.08891.pdf) `guidance_scale` is defined as `φ` in equation 16. of
|
||||
[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.
|
||||
original_size (`Tuple[int]`, *optional*, defaults to (1024, 1024)):
|
||||
If `original_size` is not the same as `target_size` the image will appear to be down- or upsampled.
|
||||
`original_size` defaults to `(height, width)` if not specified. Part of SDXL's micro-conditioning as
|
||||
explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952).
|
||||
crops_coords_top_left (`Tuple[int]`, *optional*, defaults to (0, 0)):
|
||||
`crops_coords_top_left` can be used to generate an image that appears to be "cropped" from the position
|
||||
`crops_coords_top_left` downwards. Favorable, well-centered images are usually achieved by setting
|
||||
`crops_coords_top_left` to (0, 0). Part of SDXL's micro-conditioning as explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952).
|
||||
target_size (`Tuple[int]`, *optional*, defaults to (1024, 1024)):
|
||||
For most cases, `target_size` should be set to the desired height and width of the generated image. If
|
||||
not specified it will default to `(height, width)`. Part of SDXL's micro-conditioning as explained in
|
||||
section 2.2 of [https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952).
|
||||
negative_original_size (`Tuple[int]`, *optional*, defaults to (1024, 1024)):
|
||||
To negatively condition the generation process based on a specific image resolution. Part of SDXL's
|
||||
micro-conditioning as explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952). For more
|
||||
information, refer to this issue thread: https://github.com/huggingface/diffusers/issues/4208.
|
||||
negative_crops_coords_top_left (`Tuple[int]`, *optional*, defaults to (0, 0)):
|
||||
To negatively condition the generation process based on a specific crop coordinates. Part of SDXL's
|
||||
micro-conditioning as explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952). For more
|
||||
information, refer to this issue thread: https://github.com/huggingface/diffusers/issues/4208.
|
||||
negative_target_size (`Tuple[int]`, *optional*, defaults to (1024, 1024)):
|
||||
To negatively condition the generation process based on a target image resolution. It should be as same
|
||||
as the `target_size` for most cases. Part of SDXL's micro-conditioning as explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952). For more
|
||||
information, refer to this issue thread: https://github.com/huggingface/diffusers/issues/4208.
|
||||
callback_on_step_end (`Callable`, *optional*):
|
||||
A function that calls at the end of each denoising steps during the inference. The function is called
|
||||
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_xl.StableDiffusionXLPipelineOutput`] or `tuple`:
|
||||
[`~pipelines.stable_diffusion_xl.StableDiffusionXLPipelineOutput`] if `return_dict` is True, otherwise a
|
||||
`tuple`. When returning a tuple, the first element is a list with the generated images.
|
||||
"""
|
||||
|
||||
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 use `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 use `callback_on_step_end`",
|
||||
)
|
||||
|
||||
# 0. Default height and width to unet
|
||||
height = height or self.default_sample_size * self.vae_scale_factor
|
||||
width = width or self.default_sample_size * self.vae_scale_factor
|
||||
|
||||
original_size = original_size or (height, width)
|
||||
target_size = target_size or (height, width)
|
||||
|
||||
# 1. Check inputs. Raise error if not correct
|
||||
self.check_inputs(
|
||||
prompt,
|
||||
prompt_2,
|
||||
height,
|
||||
width,
|
||||
callback_steps,
|
||||
negative_prompt,
|
||||
negative_prompt_2,
|
||||
prompt_embeds,
|
||||
negative_prompt_embeds,
|
||||
pooled_prompt_embeds,
|
||||
negative_pooled_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._denoising_end = denoising_end
|
||||
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,
|
||||
pooled_prompt_embeds,
|
||||
negative_pooled_prompt_embeds,
|
||||
) = self.encode_prompt(
|
||||
prompt=prompt,
|
||||
prompt_2=prompt_2,
|
||||
device=device,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
do_classifier_free_guidance=self.do_classifier_free_guidance,
|
||||
negative_prompt=negative_prompt,
|
||||
negative_prompt_2=negative_prompt_2,
|
||||
prompt_embeds=prompt_embeds,
|
||||
negative_prompt_embeds=negative_prompt_embeds,
|
||||
pooled_prompt_embeds=pooled_prompt_embeds,
|
||||
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
|
||||
lora_scale=lora_scale,
|
||||
clip_skip=self.clip_skip,
|
||||
)
|
||||
|
||||
# 4. Prepare timesteps
|
||||
timesteps, num_inference_steps = retrieve_timesteps(
|
||||
self.scheduler, num_inference_steps, device, timesteps
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
# 7. Prepare added time ids & embeddings
|
||||
add_text_embeds = pooled_prompt_embeds
|
||||
if self.text_encoder_2 is None:
|
||||
text_encoder_projection_dim = int(pooled_prompt_embeds.shape[-1])
|
||||
else:
|
||||
text_encoder_projection_dim = self.text_encoder_2.config.projection_dim
|
||||
|
||||
add_time_ids = self._get_add_time_ids(
|
||||
original_size,
|
||||
crops_coords_top_left,
|
||||
target_size,
|
||||
dtype=prompt_embeds.dtype,
|
||||
text_encoder_projection_dim=text_encoder_projection_dim,
|
||||
)
|
||||
if negative_original_size is not None and negative_target_size is not None:
|
||||
negative_add_time_ids = self._get_add_time_ids(
|
||||
negative_original_size,
|
||||
negative_crops_coords_top_left,
|
||||
negative_target_size,
|
||||
dtype=prompt_embeds.dtype,
|
||||
text_encoder_projection_dim=text_encoder_projection_dim,
|
||||
)
|
||||
else:
|
||||
negative_add_time_ids = add_time_ids
|
||||
|
||||
if self.do_classifier_free_guidance:
|
||||
prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds], dim=0)
|
||||
add_text_embeds = torch.cat(
|
||||
[negative_pooled_prompt_embeds, add_text_embeds], dim=0
|
||||
)
|
||||
add_time_ids = torch.cat([negative_add_time_ids, add_time_ids], dim=0)
|
||||
|
||||
prompt_embeds = prompt_embeds.to(device)
|
||||
add_text_embeds = add_text_embeds.to(device)
|
||||
add_time_ids = add_time_ids.to(device).repeat(
|
||||
batch_size * num_images_per_prompt, 1
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
# 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,
|
||||
)
|
||||
|
||||
with torch.no_grad():
|
||||
ref_timesteps = torch.zeros_like(timesteps[0])
|
||||
ref_hidden_states = {}
|
||||
|
||||
self.unet(
|
||||
reference_latents,
|
||||
ref_timesteps,
|
||||
encoder_hidden_states=prompt_embeds[-1:],
|
||||
added_cond_kwargs={
|
||||
"text_embeds": add_text_embeds[-1:],
|
||||
"time_ids": add_time_ids[-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 = {
|
||||
"mv_scale": mv_scale,
|
||||
"ref_hidden_states": {k: v.clone() for k, v in ref_hidden_states.items()},
|
||||
"ref_scale": reference_conditioning_scale,
|
||||
"num_views": num_images_per_prompt,
|
||||
**(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
|
||||
|
||||
# 8. Denoising loop
|
||||
num_warmup_steps = max(
|
||||
len(timesteps) - num_inference_steps * self.scheduler.order, 0
|
||||
)
|
||||
|
||||
# 8.1 Apply denoising_end
|
||||
if (
|
||||
self.denoising_end is not None
|
||||
and isinstance(self.denoising_end, float)
|
||||
and self.denoising_end > 0
|
||||
and self.denoising_end < 1
|
||||
):
|
||||
discrete_timestep_cutoff = int(
|
||||
round(
|
||||
self.scheduler.config.num_train_timesteps
|
||||
- (self.denoising_end * self.scheduler.config.num_train_timesteps)
|
||||
)
|
||||
)
|
||||
num_inference_steps = len(
|
||||
list(filter(lambda ts: ts >= discrete_timestep_cutoff, timesteps))
|
||||
)
|
||||
timesteps = timesteps[:num_inference_steps]
|
||||
|
||||
# 9. 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)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
added_cond_kwargs = {
|
||||
"text_embeds": add_text_embeds,
|
||||
"time_ids": add_time_ids,
|
||||
}
|
||||
if ip_adapter_image is not None or ip_adapter_image_embeds is not None:
|
||||
added_cond_kwargs["image_embeds"] = image_embeds
|
||||
|
||||
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
|
||||
|
||||
# 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,
|
||||
)[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_dtype = latents.dtype
|
||||
latents = self.scheduler.step(
|
||||
noise_pred, t, latents, **extra_step_kwargs, return_dict=False
|
||||
)[0]
|
||||
if latents.dtype != latents_dtype:
|
||||
if torch.backends.mps.is_available():
|
||||
# some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
|
||||
latents = latents.to(latents_dtype)
|
||||
|
||||
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
|
||||
)
|
||||
add_text_embeds = callback_outputs.pop(
|
||||
"add_text_embeds", add_text_embeds
|
||||
)
|
||||
negative_pooled_prompt_embeds = callback_outputs.pop(
|
||||
"negative_pooled_prompt_embeds", negative_pooled_prompt_embeds
|
||||
)
|
||||
add_time_ids = callback_outputs.pop("add_time_ids", add_time_ids)
|
||||
negative_add_time_ids = callback_outputs.pop(
|
||||
"negative_add_time_ids", negative_add_time_ids
|
||||
)
|
||||
|
||||
# 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 not output_type == "latent":
|
||||
# make sure the VAE is in float32 mode, as it overflows in float16
|
||||
needs_upcasting = (
|
||||
self.vae.dtype == torch.float16 and self.vae.config.force_upcast
|
||||
)
|
||||
|
||||
if needs_upcasting:
|
||||
self.upcast_vae()
|
||||
latents = latents.to(
|
||||
next(iter(self.vae.post_quant_conv.parameters())).dtype
|
||||
)
|
||||
elif latents.dtype != self.vae.dtype:
|
||||
if torch.backends.mps.is_available():
|
||||
# some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
|
||||
self.vae = self.vae.to(latents.dtype)
|
||||
|
||||
# unscale/denormalize the latents
|
||||
# denormalize with the mean and std if available and not None
|
||||
has_latents_mean = (
|
||||
hasattr(self.vae.config, "latents_mean")
|
||||
and self.vae.config.latents_mean is not None
|
||||
)
|
||||
has_latents_std = (
|
||||
hasattr(self.vae.config, "latents_std")
|
||||
and self.vae.config.latents_std is not None
|
||||
)
|
||||
if has_latents_mean and has_latents_std:
|
||||
latents_mean = (
|
||||
torch.tensor(self.vae.config.latents_mean)
|
||||
.view(1, 4, 1, 1)
|
||||
.to(latents.device, latents.dtype)
|
||||
)
|
||||
latents_std = (
|
||||
torch.tensor(self.vae.config.latents_std)
|
||||
.view(1, 4, 1, 1)
|
||||
.to(latents.device, latents.dtype)
|
||||
)
|
||||
latents = (
|
||||
latents * latents_std / self.vae.config.scaling_factor
|
||||
+ latents_mean
|
||||
)
|
||||
else:
|
||||
latents = latents / self.vae.config.scaling_factor
|
||||
|
||||
image = self.vae.decode(latents, return_dict=False)[0]
|
||||
|
||||
# cast back to fp16 if needed
|
||||
if needs_upcasting:
|
||||
self.vae.to(dtype=torch.float16)
|
||||
else:
|
||||
image = latents
|
||||
|
||||
if not output_type == "latent":
|
||||
# apply watermark if available
|
||||
if self.watermark is not None:
|
||||
image = self.watermark.apply_watermark(image)
|
||||
|
||||
image = self.image_processor.postprocess(image, output_type=output_type)
|
||||
|
||||
# Offload all models
|
||||
self.maybe_free_model_hooks()
|
||||
|
||||
if not return_dict:
|
||||
return (image,)
|
||||
|
||||
return StableDiffusionXLPipelineOutput(images=image)
|
||||
|
||||
### 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=(320, 640, 1280, 1280),
|
||||
num_res_blocks=2,
|
||||
downscale_factor=16,
|
||||
adapter_type="full_adapter_xl",
|
||||
)
|
||||
|
||||
# set custom attn processor for multi-view attention and image cross-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
|
||||
@@ -0,0 +1,634 @@
|
||||
# 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
|
||||
@@ -0,0 +1,801 @@
|
||||
# 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.
|
||||
|
||||
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
||||
|
||||
import torch
|
||||
from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
|
||||
from diffusers.models import AutoencoderKL, T2IAdapter, UNet2DConditionModel
|
||||
from diffusers.pipelines.stable_diffusion_xl.pipeline_output import (
|
||||
StableDiffusionXLPipelineOutput,
|
||||
)
|
||||
from diffusers.pipelines.stable_diffusion_xl.pipeline_stable_diffusion_xl import (
|
||||
StableDiffusionXLPipeline,
|
||||
rescale_noise_cfg,
|
||||
retrieve_timesteps,
|
||||
)
|
||||
from diffusers.schedulers import KarrasDiffusionSchedulers
|
||||
from diffusers.utils import deprecate, logging
|
||||
from transformers import (
|
||||
CLIPImageProcessor,
|
||||
CLIPTextModel,
|
||||
CLIPTextModelWithProjection,
|
||||
CLIPTokenizer,
|
||||
CLIPVisionModelWithProjection,
|
||||
)
|
||||
|
||||
from ..loaders import CustomAdapterMixin
|
||||
from ..models.attention_processor import (
|
||||
DecoupledMVRowSelfAttnProcessor2_0,
|
||||
set_unet_2d_condition_attn_processor,
|
||||
)
|
||||
|
||||
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
|
||||
|
||||
|
||||
class MVAdapterT2MVSDXLPipeline(StableDiffusionXLPipeline, CustomAdapterMixin):
|
||||
def __init__(
|
||||
self,
|
||||
vae: AutoencoderKL,
|
||||
text_encoder: CLIPTextModel,
|
||||
text_encoder_2: CLIPTextModelWithProjection,
|
||||
tokenizer: CLIPTokenizer,
|
||||
tokenizer_2: CLIPTokenizer,
|
||||
unet: UNet2DConditionModel,
|
||||
scheduler: KarrasDiffusionSchedulers,
|
||||
image_encoder: CLIPVisionModelWithProjection = None,
|
||||
feature_extractor: CLIPImageProcessor = None,
|
||||
force_zeros_for_empty_prompt: bool = True,
|
||||
add_watermarker: Optional[bool] = None,
|
||||
):
|
||||
super().__init__(
|
||||
vae=vae,
|
||||
text_encoder=text_encoder,
|
||||
text_encoder_2=text_encoder_2,
|
||||
tokenizer=tokenizer,
|
||||
tokenizer_2=tokenizer_2,
|
||||
unet=unet,
|
||||
scheduler=scheduler,
|
||||
image_encoder=image_encoder,
|
||||
feature_extractor=feature_extractor,
|
||||
force_zeros_for_empty_prompt=force_zeros_for_empty_prompt,
|
||||
add_watermarker=add_watermarker,
|
||||
)
|
||||
|
||||
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,
|
||||
prompt_2: Optional[Union[str, List[str]]] = None,
|
||||
height: Optional[int] = None,
|
||||
width: Optional[int] = None,
|
||||
num_inference_steps: int = 50,
|
||||
timesteps: List[int] = None,
|
||||
denoising_end: Optional[float] = None,
|
||||
guidance_scale: float = 5.0,
|
||||
negative_prompt: Optional[Union[str, List[str]]] = None,
|
||||
negative_prompt_2: 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.FloatTensor] = None,
|
||||
prompt_embeds: Optional[torch.FloatTensor] = None,
|
||||
negative_prompt_embeds: Optional[torch.FloatTensor] = None,
|
||||
pooled_prompt_embeds: Optional[torch.FloatTensor] = None,
|
||||
negative_pooled_prompt_embeds: Optional[torch.FloatTensor] = None,
|
||||
ip_adapter_image: Optional[PipelineImageInput] = None,
|
||||
ip_adapter_image_embeds: Optional[List[torch.FloatTensor]] = None,
|
||||
output_type: Optional[str] = "pil",
|
||||
return_dict: bool = True,
|
||||
cross_attention_kwargs: Optional[Dict[str, Any]] = None,
|
||||
guidance_rescale: float = 0.0,
|
||||
original_size: Optional[Tuple[int, int]] = None,
|
||||
crops_coords_top_left: Tuple[int, int] = (0, 0),
|
||||
target_size: Optional[Tuple[int, int]] = None,
|
||||
negative_original_size: Optional[Tuple[int, int]] = None,
|
||||
negative_crops_coords_top_left: Tuple[int, int] = (0, 0),
|
||||
negative_target_size: Optional[Tuple[int, int]] = None,
|
||||
clip_skip: Optional[int] = None,
|
||||
callback_on_step_end: Optional[Callable[[int, int, Dict], None]] = 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"""
|
||||
Function invoked when calling the pipeline for generation.
|
||||
|
||||
Args:
|
||||
prompt (`str` or `List[str]`, *optional*):
|
||||
The prompt or prompts to guide the image generation. If not defined, one has to pass `prompt_embeds`.
|
||||
instead.
|
||||
prompt_2 (`str` or `List[str]`, *optional*):
|
||||
The prompt or prompts to be sent to the `tokenizer_2` and `text_encoder_2`. If not defined, `prompt` is
|
||||
used in both text-encoders
|
||||
height (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
|
||||
The height in pixels of the generated image. This is set to 1024 by default for the best results.
|
||||
Anything below 512 pixels won't work well for
|
||||
[stabilityai/stable-diffusion-xl-base-1.0](https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0)
|
||||
and checkpoints that are not specifically fine-tuned on low resolutions.
|
||||
width (`int`, *optional*, defaults to self.unet.config.sample_size * self.vae_scale_factor):
|
||||
The width in pixels of the generated image. This is set to 1024 by default for the best results.
|
||||
Anything below 512 pixels won't work well for
|
||||
[stabilityai/stable-diffusion-xl-base-1.0](https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0)
|
||||
and checkpoints that are not specifically fine-tuned on low resolutions.
|
||||
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.
|
||||
denoising_end (`float`, *optional*):
|
||||
When specified, determines the fraction (between 0.0 and 1.0) of the total denoising process to be
|
||||
completed before it is intentionally prematurely terminated. As a result, the returned sample will
|
||||
still retain a substantial amount of noise as determined by the discrete timesteps selected by the
|
||||
scheduler. The denoising_end parameter should ideally be utilized when this pipeline forms a part of a
|
||||
"Mixture of Denoisers" multi-pipeline setup, as elaborated in [**Refining the Image
|
||||
Output**](https://huggingface.co/docs/diffusers/api/pipelines/stable_diffusion/stable_diffusion_xl#refining-the-image-output)
|
||||
guidance_scale (`float`, *optional*, defaults to 5.0):
|
||||
Guidance scale as defined in [Classifier-Free Diffusion Guidance](https://arxiv.org/abs/2207.12598).
|
||||
`guidance_scale` is defined as `w` of equation 2. of [Imagen
|
||||
Paper](https://arxiv.org/pdf/2205.11487.pdf). Guidance scale is enabled by setting `guidance_scale >
|
||||
1`. Higher guidance scale encourages to generate images that are closely linked to the text `prompt`,
|
||||
usually at the expense of lower image quality.
|
||||
negative_prompt (`str` or `List[str]`, *optional*):
|
||||
The prompt or prompts not to guide the image generation. If not defined, one has to pass
|
||||
`negative_prompt_embeds` instead. Ignored when not using guidance (i.e., ignored if `guidance_scale` is
|
||||
less than `1`).
|
||||
negative_prompt_2 (`str` or `List[str]`, *optional*):
|
||||
The prompt or prompts not to guide the image generation to be sent to `tokenizer_2` and
|
||||
`text_encoder_2`. If not defined, `negative_prompt` is used in both text-encoders
|
||||
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 (η) in the DDIM paper: https://arxiv.org/abs/2010.02502. Only applies to
|
||||
[`schedulers.DDIMScheduler`], will be ignored for others.
|
||||
generator (`torch.Generator` or `List[torch.Generator]`, *optional*):
|
||||
One or a list of [torch generator(s)](https://pytorch.org/docs/stable/generated/torch.Generator.html)
|
||||
to make generation deterministic.
|
||||
latents (`torch.FloatTensor`, *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 will ge generated by sampling using the supplied random `generator`.
|
||||
prompt_embeds (`torch.FloatTensor`, *optional*):
|
||||
Pre-generated text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting. If not
|
||||
provided, text embeddings will be generated from `prompt` input argument.
|
||||
negative_prompt_embeds (`torch.FloatTensor`, *optional*):
|
||||
Pre-generated negative text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
|
||||
weighting. If not provided, negative_prompt_embeds will be generated from `negative_prompt` input
|
||||
argument.
|
||||
pooled_prompt_embeds (`torch.FloatTensor`, *optional*):
|
||||
Pre-generated pooled text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt weighting.
|
||||
If not provided, pooled text embeddings will be generated from `prompt` input argument.
|
||||
negative_pooled_prompt_embeds (`torch.FloatTensor`, *optional*):
|
||||
Pre-generated negative pooled text embeddings. Can be used to easily tweak text inputs, *e.g.* prompt
|
||||
weighting. If not provided, pooled negative_prompt_embeds will be generated from `negative_prompt`
|
||||
input argument.
|
||||
ip_adapter_image: (`PipelineImageInput`, *optional*): Optional image input to work with IP Adapters.
|
||||
ip_adapter_image_embeds (`List[torch.FloatTensor]`, *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 generate image. Choose between
|
||||
[PIL](https://pillow.readthedocs.io/en/stable/): `PIL.Image.Image` or `np.array`.
|
||||
return_dict (`bool`, *optional*, defaults to `True`):
|
||||
Whether or not to return a [`~pipelines.stable_diffusion_xl.StableDiffusionXLPipelineOutput`] instead
|
||||
of a plain tuple.
|
||||
cross_attention_kwargs (`dict`, *optional*):
|
||||
A kwargs dictionary that if specified is passed along to the `AttentionProcessor` as defined under
|
||||
`self.processor` in
|
||||
[diffusers.models.attention_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 proposed by [Common Diffusion Noise Schedules and Sample Steps are
|
||||
Flawed](https://arxiv.org/pdf/2305.08891.pdf) `guidance_scale` is defined as `φ` in equation 16. of
|
||||
[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.
|
||||
original_size (`Tuple[int]`, *optional*, defaults to (1024, 1024)):
|
||||
If `original_size` is not the same as `target_size` the image will appear to be down- or upsampled.
|
||||
`original_size` defaults to `(height, width)` if not specified. Part of SDXL's micro-conditioning as
|
||||
explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952).
|
||||
crops_coords_top_left (`Tuple[int]`, *optional*, defaults to (0, 0)):
|
||||
`crops_coords_top_left` can be used to generate an image that appears to be "cropped" from the position
|
||||
`crops_coords_top_left` downwards. Favorable, well-centered images are usually achieved by setting
|
||||
`crops_coords_top_left` to (0, 0). Part of SDXL's micro-conditioning as explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952).
|
||||
target_size (`Tuple[int]`, *optional*, defaults to (1024, 1024)):
|
||||
For most cases, `target_size` should be set to the desired height and width of the generated image. If
|
||||
not specified it will default to `(height, width)`. Part of SDXL's micro-conditioning as explained in
|
||||
section 2.2 of [https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952).
|
||||
negative_original_size (`Tuple[int]`, *optional*, defaults to (1024, 1024)):
|
||||
To negatively condition the generation process based on a specific image resolution. Part of SDXL's
|
||||
micro-conditioning as explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952). For more
|
||||
information, refer to this issue thread: https://github.com/huggingface/diffusers/issues/4208.
|
||||
negative_crops_coords_top_left (`Tuple[int]`, *optional*, defaults to (0, 0)):
|
||||
To negatively condition the generation process based on a specific crop coordinates. Part of SDXL's
|
||||
micro-conditioning as explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952). For more
|
||||
information, refer to this issue thread: https://github.com/huggingface/diffusers/issues/4208.
|
||||
negative_target_size (`Tuple[int]`, *optional*, defaults to (1024, 1024)):
|
||||
To negatively condition the generation process based on a target image resolution. It should be as same
|
||||
as the `target_size` for most cases. Part of SDXL's micro-conditioning as explained in section 2.2 of
|
||||
[https://huggingface.co/papers/2307.01952](https://huggingface.co/papers/2307.01952). For more
|
||||
information, refer to this issue thread: https://github.com/huggingface/diffusers/issues/4208.
|
||||
callback_on_step_end (`Callable`, *optional*):
|
||||
A function that calls at the end of each denoising steps during the inference. The function is called
|
||||
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_xl.StableDiffusionXLPipelineOutput`] or `tuple`:
|
||||
[`~pipelines.stable_diffusion_xl.StableDiffusionXLPipelineOutput`] if `return_dict` is True, otherwise a
|
||||
`tuple`. When returning a tuple, the first element is a list with the generated images.
|
||||
"""
|
||||
|
||||
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 use `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 use `callback_on_step_end`",
|
||||
)
|
||||
|
||||
# 0. Default height and width to unet
|
||||
height = height or self.default_sample_size * self.vae_scale_factor
|
||||
width = width or self.default_sample_size * self.vae_scale_factor
|
||||
|
||||
original_size = original_size or (height, width)
|
||||
target_size = target_size or (height, width)
|
||||
|
||||
# 1. Check inputs. Raise error if not correct
|
||||
self.check_inputs(
|
||||
prompt,
|
||||
prompt_2,
|
||||
height,
|
||||
width,
|
||||
callback_steps,
|
||||
negative_prompt,
|
||||
negative_prompt_2,
|
||||
prompt_embeds,
|
||||
negative_prompt_embeds,
|
||||
pooled_prompt_embeds,
|
||||
negative_pooled_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._denoising_end = denoising_end
|
||||
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,
|
||||
pooled_prompt_embeds,
|
||||
negative_pooled_prompt_embeds,
|
||||
) = self.encode_prompt(
|
||||
prompt=prompt,
|
||||
prompt_2=prompt_2,
|
||||
device=device,
|
||||
num_images_per_prompt=num_images_per_prompt,
|
||||
do_classifier_free_guidance=self.do_classifier_free_guidance,
|
||||
negative_prompt=negative_prompt,
|
||||
negative_prompt_2=negative_prompt_2,
|
||||
prompt_embeds=prompt_embeds,
|
||||
negative_prompt_embeds=negative_prompt_embeds,
|
||||
pooled_prompt_embeds=pooled_prompt_embeds,
|
||||
negative_pooled_prompt_embeds=negative_pooled_prompt_embeds,
|
||||
lora_scale=lora_scale,
|
||||
clip_skip=self.clip_skip,
|
||||
)
|
||||
|
||||
# 4. Prepare timesteps
|
||||
timesteps, num_inference_steps = retrieve_timesteps(
|
||||
self.scheduler, num_inference_steps, device, timesteps
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
# 7. Prepare added time ids & embeddings
|
||||
add_text_embeds = pooled_prompt_embeds
|
||||
if self.text_encoder_2 is None:
|
||||
text_encoder_projection_dim = int(pooled_prompt_embeds.shape[-1])
|
||||
else:
|
||||
text_encoder_projection_dim = self.text_encoder_2.config.projection_dim
|
||||
|
||||
add_time_ids = self._get_add_time_ids(
|
||||
original_size,
|
||||
crops_coords_top_left,
|
||||
target_size,
|
||||
dtype=prompt_embeds.dtype,
|
||||
text_encoder_projection_dim=text_encoder_projection_dim,
|
||||
)
|
||||
if negative_original_size is not None and negative_target_size is not None:
|
||||
negative_add_time_ids = self._get_add_time_ids(
|
||||
negative_original_size,
|
||||
negative_crops_coords_top_left,
|
||||
negative_target_size,
|
||||
dtype=prompt_embeds.dtype,
|
||||
text_encoder_projection_dim=text_encoder_projection_dim,
|
||||
)
|
||||
else:
|
||||
negative_add_time_ids = add_time_ids
|
||||
|
||||
if self.do_classifier_free_guidance:
|
||||
prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds], dim=0)
|
||||
add_text_embeds = torch.cat(
|
||||
[negative_pooled_prompt_embeds, add_text_embeds], dim=0
|
||||
)
|
||||
add_time_ids = torch.cat([negative_add_time_ids, add_time_ids], dim=0)
|
||||
|
||||
prompt_embeds = prompt_embeds.to(device)
|
||||
add_text_embeds = add_text_embeds.to(device)
|
||||
add_time_ids = add_time_ids.to(device).repeat(
|
||||
batch_size * num_images_per_prompt, 1
|
||||
)
|
||||
|
||||
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,
|
||||
)
|
||||
|
||||
# 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)
|
||||
|
||||
# 8. Denoising loop
|
||||
num_warmup_steps = max(
|
||||
len(timesteps) - num_inference_steps * self.scheduler.order, 0
|
||||
)
|
||||
|
||||
# 8.1 Apply denoising_end
|
||||
if (
|
||||
self.denoising_end is not None
|
||||
and isinstance(self.denoising_end, float)
|
||||
and self.denoising_end > 0
|
||||
and self.denoising_end < 1
|
||||
):
|
||||
discrete_timestep_cutoff = int(
|
||||
round(
|
||||
self.scheduler.config.num_train_timesteps
|
||||
- (self.denoising_end * self.scheduler.config.num_train_timesteps)
|
||||
)
|
||||
)
|
||||
num_inference_steps = len(
|
||||
list(filter(lambda ts: ts >= discrete_timestep_cutoff, timesteps))
|
||||
)
|
||||
timesteps = timesteps[:num_inference_steps]
|
||||
|
||||
# 9. 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)
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
added_cond_kwargs = {
|
||||
"text_embeds": add_text_embeds,
|
||||
"time_ids": add_time_ids,
|
||||
}
|
||||
if ip_adapter_image is not None or ip_adapter_image_embeds is not None:
|
||||
added_cond_kwargs["image_embeds"] = image_embeds
|
||||
|
||||
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_dtype = latents.dtype
|
||||
latents = self.scheduler.step(
|
||||
noise_pred, t, latents, **extra_step_kwargs, return_dict=False
|
||||
)[0]
|
||||
if latents.dtype != latents_dtype:
|
||||
if torch.backends.mps.is_available():
|
||||
# some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
|
||||
latents = latents.to(latents_dtype)
|
||||
|
||||
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
|
||||
)
|
||||
add_text_embeds = callback_outputs.pop(
|
||||
"add_text_embeds", add_text_embeds
|
||||
)
|
||||
negative_pooled_prompt_embeds = callback_outputs.pop(
|
||||
"negative_pooled_prompt_embeds", negative_pooled_prompt_embeds
|
||||
)
|
||||
add_time_ids = callback_outputs.pop("add_time_ids", add_time_ids)
|
||||
negative_add_time_ids = callback_outputs.pop(
|
||||
"negative_add_time_ids", negative_add_time_ids
|
||||
)
|
||||
|
||||
# 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 not output_type == "latent":
|
||||
# make sure the VAE is in float32 mode, as it overflows in float16
|
||||
needs_upcasting = (
|
||||
self.vae.dtype == torch.float16 and self.vae.config.force_upcast
|
||||
)
|
||||
|
||||
if needs_upcasting:
|
||||
self.upcast_vae()
|
||||
latents = latents.to(
|
||||
next(iter(self.vae.post_quant_conv.parameters())).dtype
|
||||
)
|
||||
elif latents.dtype != self.vae.dtype:
|
||||
if torch.backends.mps.is_available():
|
||||
# some platforms (eg. apple mps) misbehave due to a pytorch bug: https://github.com/pytorch/pytorch/pull/99272
|
||||
self.vae = self.vae.to(latents.dtype)
|
||||
|
||||
# unscale/denormalize the latents
|
||||
# denormalize with the mean and std if available and not None
|
||||
has_latents_mean = (
|
||||
hasattr(self.vae.config, "latents_mean")
|
||||
and self.vae.config.latents_mean is not None
|
||||
)
|
||||
has_latents_std = (
|
||||
hasattr(self.vae.config, "latents_std")
|
||||
and self.vae.config.latents_std is not None
|
||||
)
|
||||
if has_latents_mean and has_latents_std:
|
||||
latents_mean = (
|
||||
torch.tensor(self.vae.config.latents_mean)
|
||||
.view(1, 4, 1, 1)
|
||||
.to(latents.device, latents.dtype)
|
||||
)
|
||||
latents_std = (
|
||||
torch.tensor(self.vae.config.latents_std)
|
||||
.view(1, 4, 1, 1)
|
||||
.to(latents.device, latents.dtype)
|
||||
)
|
||||
latents = (
|
||||
latents * latents_std / self.vae.config.scaling_factor
|
||||
+ latents_mean
|
||||
)
|
||||
else:
|
||||
latents = latents / self.vae.config.scaling_factor
|
||||
|
||||
image = self.vae.decode(latents, return_dict=False)[0]
|
||||
|
||||
# cast back to fp16 if needed
|
||||
if needs_upcasting:
|
||||
self.vae.to(dtype=torch.float16)
|
||||
else:
|
||||
image = latents
|
||||
|
||||
if not output_type == "latent":
|
||||
# apply watermark if available
|
||||
if self.watermark is not None:
|
||||
image = self.watermark.apply_watermark(image)
|
||||
|
||||
image = self.image_processor.postprocess(image, output_type=output_type)
|
||||
|
||||
# Offload all models
|
||||
self.maybe_free_model_hooks()
|
||||
|
||||
if not return_dict:
|
||||
return (image,)
|
||||
|
||||
return StableDiffusionXLPipelineOutput(images=image)
|
||||
|
||||
### 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=(320, 640, 1280, 1280),
|
||||
num_res_blocks=2,
|
||||
downscale_factor=16,
|
||||
adapter_type="full_adapter_xl",
|
||||
)
|
||||
|
||||
# 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
|
||||
@@ -0,0 +1,428 @@
|
||||
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,
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
import torch
|
||||
|
||||
|
||||
def get_sigmas(noise_scheduler, timesteps, n_dim=4, dtype=torch.float32, device=None):
|
||||
sigmas = noise_scheduler.sigmas.to(device=device, dtype=dtype)
|
||||
schedule_timesteps = noise_scheduler.timesteps.to(device)
|
||||
timesteps = timesteps.to(device)
|
||||
step_indices = [(schedule_timesteps == t).nonzero().item() for t in timesteps]
|
||||
sigma = sigmas[step_indices].flatten()
|
||||
while len(sigma.shape) < n_dim:
|
||||
sigma = sigma.unsqueeze(-1)
|
||||
return sigma
|
||||
|
||||
|
||||
def SNR_to_betas(snr):
|
||||
"""
|
||||
Converts SNR to betas
|
||||
"""
|
||||
# alphas_cumprod = pass
|
||||
# snr = (alpha / ) ** 2
|
||||
# alpha_t^2 / (1 - alpha_t^2) = snr
|
||||
alpha_t = (snr / (1 + snr)) ** 0.5
|
||||
alphas_cumprod = alpha_t**2
|
||||
alphas = alphas_cumprod / torch.cat(
|
||||
[torch.ones(1, device=snr.device), alphas_cumprod[:-1]]
|
||||
)
|
||||
betas = 1 - alphas
|
||||
return betas
|
||||
|
||||
|
||||
def compute_snr(timesteps, noise_scheduler):
|
||||
"""
|
||||
Computes SNR as per Min-SNR-Diffusion-Training/guided_diffusion/gaussian_diffusion.py at 521b624bd70c67cee4bdf49225915f5
|
||||
"""
|
||||
alphas_cumprod = noise_scheduler.alphas_cumprod
|
||||
sqrt_alphas_cumprod = alphas_cumprod**0.5
|
||||
sqrt_one_minus_alphas_cumprod = (1.0 - alphas_cumprod) ** 0.5
|
||||
|
||||
# Expand the tensors.
|
||||
# Adapted from Min-SNR-Diffusion-Training/guided_diffusion/gaussian_diffusion.py at 521b624bd70c67cee4bdf49225915f5
|
||||
sqrt_alphas_cumprod = sqrt_alphas_cumprod.to(device=timesteps.device)[
|
||||
timesteps
|
||||
].float()
|
||||
while len(sqrt_alphas_cumprod.shape) < len(timesteps.shape):
|
||||
sqrt_alphas_cumprod = sqrt_alphas_cumprod[..., None]
|
||||
alpha = sqrt_alphas_cumprod.expand(timesteps.shape)
|
||||
|
||||
sqrt_one_minus_alphas_cumprod = sqrt_one_minus_alphas_cumprod.to(
|
||||
device=timesteps.device
|
||||
)[timesteps].float()
|
||||
while len(sqrt_one_minus_alphas_cumprod.shape) < len(timesteps.shape):
|
||||
sqrt_one_minus_alphas_cumprod = sqrt_one_minus_alphas_cumprod[..., None]
|
||||
sigma = sqrt_one_minus_alphas_cumprod.expand(timesteps.shape)
|
||||
|
||||
# Compute SNR.
|
||||
snr = (alpha / sigma) ** 2
|
||||
return snr
|
||||
|
||||
|
||||
def compute_alpha(timesteps, noise_scheduler):
|
||||
alphas_cumprod = noise_scheduler.alphas_cumprod
|
||||
sqrt_alphas_cumprod = alphas_cumprod**0.5
|
||||
sqrt_alphas_cumprod = sqrt_alphas_cumprod.to(device=timesteps.device)[
|
||||
timesteps
|
||||
].float()
|
||||
while len(sqrt_alphas_cumprod.shape) < len(timesteps.shape):
|
||||
sqrt_alphas_cumprod = sqrt_alphas_cumprod[..., None]
|
||||
alpha = sqrt_alphas_cumprod.expand(timesteps.shape)
|
||||
|
||||
return alpha
|
||||
@@ -0,0 +1,138 @@
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
|
||||
from .scheduler_utils import SNR_to_betas, compute_snr
|
||||
|
||||
|
||||
class ShiftSNRScheduler:
|
||||
def __init__(
|
||||
self,
|
||||
noise_scheduler: Any,
|
||||
timesteps: Any,
|
||||
shift_scale: float,
|
||||
scheduler_class: Any,
|
||||
):
|
||||
self.noise_scheduler = noise_scheduler
|
||||
self.timesteps = timesteps
|
||||
self.shift_scale = shift_scale
|
||||
self.scheduler_class = scheduler_class
|
||||
|
||||
def _get_shift_scheduler(self):
|
||||
"""
|
||||
Prepare scheduler for shifted betas.
|
||||
|
||||
:return: A scheduler object configured with shifted betas
|
||||
"""
|
||||
snr = compute_snr(self.timesteps, self.noise_scheduler)
|
||||
shifted_betas = SNR_to_betas(snr / self.shift_scale)
|
||||
|
||||
return self.scheduler_class.from_config(
|
||||
self.noise_scheduler.config, trained_betas=shifted_betas.numpy()
|
||||
)
|
||||
|
||||
def _get_interpolated_shift_scheduler(self):
|
||||
"""
|
||||
Prepare scheduler for shifted betas and interpolate with the original betas in log space.
|
||||
|
||||
:return: A scheduler object configured with interpolated shifted betas
|
||||
"""
|
||||
snr = compute_snr(self.timesteps, self.noise_scheduler)
|
||||
shifted_snr = snr / self.shift_scale
|
||||
|
||||
weighting = self.timesteps.float() / (
|
||||
self.noise_scheduler.config.num_train_timesteps - 1
|
||||
)
|
||||
interpolated_snr = torch.exp(
|
||||
torch.log(snr) * (1 - weighting) + torch.log(shifted_snr) * weighting
|
||||
)
|
||||
|
||||
shifted_betas = SNR_to_betas(interpolated_snr)
|
||||
|
||||
return self.scheduler_class.from_config(
|
||||
self.noise_scheduler.config, trained_betas=shifted_betas.numpy()
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def from_scheduler(
|
||||
cls,
|
||||
noise_scheduler: Any,
|
||||
shift_mode: str = "default",
|
||||
timesteps: Any = None,
|
||||
shift_scale: float = 1.0,
|
||||
scheduler_class: Any = None,
|
||||
):
|
||||
# Check input
|
||||
if timesteps is None:
|
||||
timesteps = torch.arange(0, noise_scheduler.config.num_train_timesteps)
|
||||
if scheduler_class is None:
|
||||
scheduler_class = noise_scheduler.__class__
|
||||
|
||||
# Create scheduler
|
||||
shift_scheduler = cls(
|
||||
noise_scheduler=noise_scheduler,
|
||||
timesteps=timesteps,
|
||||
shift_scale=shift_scale,
|
||||
scheduler_class=scheduler_class,
|
||||
)
|
||||
|
||||
if shift_mode == "default":
|
||||
return shift_scheduler._get_shift_scheduler()
|
||||
elif shift_mode == "interpolated":
|
||||
return shift_scheduler._get_interpolated_shift_scheduler()
|
||||
else:
|
||||
raise ValueError(f"Unknown shift_mode: {shift_mode}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
"""
|
||||
Compare the alpha values for different noise schedulers.
|
||||
"""
|
||||
import matplotlib.pyplot as plt
|
||||
from diffusers import DDPMScheduler
|
||||
|
||||
from .scheduler_utils import compute_alpha
|
||||
|
||||
# Base
|
||||
timesteps = torch.arange(0, 1000)
|
||||
noise_scheduler_base = DDPMScheduler.from_pretrained(
|
||||
"runwayml/stable-diffusion-v1-5", subfolder="scheduler"
|
||||
)
|
||||
alpha = compute_alpha(timesteps, noise_scheduler_base)
|
||||
plt.plot(timesteps.numpy(), alpha.numpy(), label="Base")
|
||||
|
||||
# Kolors
|
||||
num_train_timesteps_ = 1100
|
||||
timesteps_ = torch.arange(0, num_train_timesteps_)
|
||||
noise_kwargs = {"beta_end": 0.014, "num_train_timesteps": num_train_timesteps_}
|
||||
noise_scheduler_kolors = DDPMScheduler.from_config(
|
||||
noise_scheduler_base.config, **noise_kwargs
|
||||
)
|
||||
alpha = compute_alpha(timesteps_, noise_scheduler_kolors)
|
||||
plt.plot(timesteps_.numpy(), alpha.numpy(), label="Kolors")
|
||||
|
||||
# Shift betas
|
||||
shift_scale = 8.0
|
||||
noise_scheduler_shift = ShiftSNRScheduler.from_scheduler(
|
||||
noise_scheduler_base, shift_mode="default", shift_scale=shift_scale
|
||||
)
|
||||
alpha = compute_alpha(timesteps, noise_scheduler_shift)
|
||||
plt.plot(timesteps.numpy(), alpha.numpy(), label="Shift Noise (scale 8.0)")
|
||||
|
||||
# Shift betas (interpolated)
|
||||
noise_scheduler_inter = ShiftSNRScheduler.from_scheduler(
|
||||
noise_scheduler_base, shift_mode="interpolated", shift_scale=shift_scale
|
||||
)
|
||||
alpha = compute_alpha(timesteps, noise_scheduler_inter)
|
||||
plt.plot(timesteps.numpy(), alpha.numpy(), label="Interpolated (scale 8.0)")
|
||||
|
||||
# ZeroSNR
|
||||
noise_scheduler = DDPMScheduler.from_config(
|
||||
noise_scheduler_base.config, rescale_betas_zero_snr=True
|
||||
)
|
||||
alpha = compute_alpha(timesteps, noise_scheduler)
|
||||
plt.plot(timesteps.numpy(), alpha.numpy(), label="ZeroSNR")
|
||||
|
||||
plt.legend()
|
||||
plt.grid()
|
||||
plt.savefig("check_alpha.png")
|
||||
@@ -0,0 +1,262 @@
|
||||
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
|
||||
@@ -0,0 +1,459 @@
|
||||
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
|
||||
@@ -0,0 +1,506 @@
|
||||
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
|
||||
@@ -0,0 +1,415 @@
|
||||
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
|
||||
@@ -0,0 +1,457 @@
|
||||
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
|
||||
@@ -0,0 +1,178 @@
|
||||
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
|
||||
@@ -0,0 +1,3 @@
|
||||
from .mesh_utils.camera import get_camera, get_orthogonal_camera
|
||||
from .geometry import get_plucker_embeds_from_cameras_ortho
|
||||
from .saving import make_image_grid, tensor_to_image, image_to_tensor
|
||||
@@ -0,0 +1,123 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
|
||||
from .config import parse_structured
|
||||
from .misc import get_device, load_module_weights
|
||||
from .typing import *
|
||||
|
||||
|
||||
class Configurable:
|
||||
@dataclass
|
||||
class Config:
|
||||
pass
|
||||
|
||||
def __init__(self, cfg: Optional[dict] = None) -> None:
|
||||
super().__init__()
|
||||
self.cfg = parse_structured(self.Config, cfg)
|
||||
|
||||
|
||||
class Updateable:
|
||||
def do_update_step(
|
||||
self, epoch: int, global_step: int, on_load_weights: bool = False
|
||||
):
|
||||
for attr in self.__dir__():
|
||||
if attr.startswith("_"):
|
||||
continue
|
||||
try:
|
||||
module = getattr(self, attr)
|
||||
except:
|
||||
continue # ignore attributes like property, which can't be retrived using getattr?
|
||||
if isinstance(module, Updateable):
|
||||
module.do_update_step(
|
||||
epoch, global_step, on_load_weights=on_load_weights
|
||||
)
|
||||
self.update_step(epoch, global_step, on_load_weights=on_load_weights)
|
||||
|
||||
def do_update_step_end(self, epoch: int, global_step: int):
|
||||
for attr in self.__dir__():
|
||||
if attr.startswith("_"):
|
||||
continue
|
||||
try:
|
||||
module = getattr(self, attr)
|
||||
except:
|
||||
continue # ignore attributes like property, which can't be retrived using getattr?
|
||||
if isinstance(module, Updateable):
|
||||
module.do_update_step_end(epoch, global_step)
|
||||
self.update_step_end(epoch, global_step)
|
||||
|
||||
def update_step(self, epoch: int, global_step: int, on_load_weights: bool = False):
|
||||
# override this method to implement custom update logic
|
||||
# if on_load_weights is True, you should be careful doing things related to model evaluations,
|
||||
# as the models and tensors are not guarenteed to be on the same device
|
||||
pass
|
||||
|
||||
def update_step_end(self, epoch: int, global_step: int):
|
||||
pass
|
||||
|
||||
|
||||
def update_if_possible(module: Any, epoch: int, global_step: int) -> None:
|
||||
if isinstance(module, Updateable):
|
||||
module.do_update_step(epoch, global_step)
|
||||
|
||||
|
||||
def update_end_if_possible(module: Any, epoch: int, global_step: int) -> None:
|
||||
if isinstance(module, Updateable):
|
||||
module.do_update_step_end(epoch, global_step)
|
||||
|
||||
|
||||
class BaseObject(Updateable):
|
||||
@dataclass
|
||||
class Config:
|
||||
pass
|
||||
|
||||
cfg: Config # add this to every subclass of BaseObject to enable static type checking
|
||||
|
||||
def __init__(
|
||||
self, cfg: Optional[Union[dict, DictConfig]] = None, *args, **kwargs
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.cfg = parse_structured(self.Config, cfg)
|
||||
self.device = get_device()
|
||||
self.configure(*args, **kwargs)
|
||||
|
||||
def configure(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
|
||||
class BaseModule(nn.Module, Updateable):
|
||||
@dataclass
|
||||
class Config:
|
||||
weights: Optional[str] = None
|
||||
|
||||
cfg: Config # add this to every subclass of BaseModule to enable static type checking
|
||||
|
||||
def __init__(
|
||||
self, cfg: Optional[Union[dict, DictConfig]] = None, *args, **kwargs
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.cfg = parse_structured(self.Config, cfg)
|
||||
self.device = get_device()
|
||||
self._non_modules = {}
|
||||
self.configure(*args, **kwargs)
|
||||
if self.cfg.weights is not None:
|
||||
# format: path/to/weights:module_name
|
||||
weights_path, module_name = self.cfg.weights.split(":")
|
||||
state_dict, epoch, global_step = load_module_weights(
|
||||
weights_path, module_name=module_name, map_location="cpu"
|
||||
)
|
||||
self.load_state_dict(state_dict)
|
||||
self.do_update_step(
|
||||
epoch, global_step, on_load_weights=True
|
||||
) # restore states
|
||||
|
||||
def configure(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
def register_non_module(self, name: str, module: nn.Module) -> None:
|
||||
# non-modules won't be treated as model parameters
|
||||
self._non_modules[name] = module
|
||||
|
||||
def non_module(self, name: str):
|
||||
return self._non_modules.get(name, None)
|
||||
@@ -0,0 +1,158 @@
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
import pytorch_lightning
|
||||
|
||||
from .config import dump_config
|
||||
from .misc import parse_version
|
||||
|
||||
if parse_version(pytorch_lightning.__version__) > parse_version("1.8"):
|
||||
from pytorch_lightning.callbacks import Callback
|
||||
else:
|
||||
from pytorch_lightning.callbacks.base import Callback
|
||||
|
||||
from pytorch_lightning.callbacks.progress import TQDMProgressBar
|
||||
from pytorch_lightning.utilities.rank_zero import rank_zero_only, rank_zero_warn
|
||||
|
||||
|
||||
class VersionedCallback(Callback):
|
||||
def __init__(self, save_root, version=None, use_version=True):
|
||||
self.save_root = save_root
|
||||
self._version = version
|
||||
self.use_version = use_version
|
||||
|
||||
@property
|
||||
def version(self) -> int:
|
||||
"""Get the experiment version.
|
||||
|
||||
Returns:
|
||||
The experiment version if specified else the next version.
|
||||
"""
|
||||
if self._version is None:
|
||||
self._version = self._get_next_version()
|
||||
return self._version
|
||||
|
||||
def _get_next_version(self):
|
||||
existing_versions = []
|
||||
if os.path.isdir(self.save_root):
|
||||
for f in os.listdir(self.save_root):
|
||||
bn = os.path.basename(f)
|
||||
if bn.startswith("version_"):
|
||||
dir_ver = os.path.splitext(bn)[0].split("_")[1].replace("/", "")
|
||||
existing_versions.append(int(dir_ver))
|
||||
if len(existing_versions) == 0:
|
||||
return 0
|
||||
return max(existing_versions) + 1
|
||||
|
||||
@property
|
||||
def savedir(self):
|
||||
if not self.use_version:
|
||||
return self.save_root
|
||||
return os.path.join(
|
||||
self.save_root,
|
||||
(
|
||||
self.version
|
||||
if isinstance(self.version, str)
|
||||
else f"version_{self.version}"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class CodeSnapshotCallback(VersionedCallback):
|
||||
def __init__(self, save_root, version=None, use_version=True):
|
||||
super().__init__(save_root, version, use_version)
|
||||
|
||||
def get_file_list(self):
|
||||
return [
|
||||
b.decode()
|
||||
for b in set(
|
||||
subprocess.check_output(
|
||||
'git ls-files -- ":!:load/*"', shell=True
|
||||
).splitlines()
|
||||
)
|
||||
| set( # hard code, TODO: use config to exclude folders or files
|
||||
subprocess.check_output(
|
||||
"git ls-files --others --exclude-standard", shell=True
|
||||
).splitlines()
|
||||
)
|
||||
]
|
||||
|
||||
@rank_zero_only
|
||||
def save_code_snapshot(self):
|
||||
os.makedirs(self.savedir, exist_ok=True)
|
||||
for f in self.get_file_list():
|
||||
if not os.path.exists(f) or os.path.isdir(f):
|
||||
continue
|
||||
os.makedirs(os.path.join(self.savedir, os.path.dirname(f)), exist_ok=True)
|
||||
shutil.copyfile(f, os.path.join(self.savedir, f))
|
||||
|
||||
def on_fit_start(self, trainer, pl_module):
|
||||
try:
|
||||
self.save_code_snapshot()
|
||||
except:
|
||||
rank_zero_warn(
|
||||
"Code snapshot is not saved. Please make sure you have git installed and are in a git repository."
|
||||
)
|
||||
|
||||
|
||||
class ConfigSnapshotCallback(VersionedCallback):
|
||||
def __init__(self, config_path, config, save_root, version=None, use_version=True):
|
||||
super().__init__(save_root, version, use_version)
|
||||
self.config_path = config_path
|
||||
self.config = config
|
||||
|
||||
@rank_zero_only
|
||||
def save_config_snapshot(self):
|
||||
os.makedirs(self.savedir, exist_ok=True)
|
||||
dump_config(os.path.join(self.savedir, "parsed.yaml"), self.config)
|
||||
shutil.copyfile(self.config_path, os.path.join(self.savedir, "raw.yaml"))
|
||||
|
||||
def on_fit_start(self, trainer, pl_module):
|
||||
self.save_config_snapshot()
|
||||
|
||||
|
||||
class CustomProgressBar(TQDMProgressBar):
|
||||
def get_metrics(self, *args, **kwargs):
|
||||
# don't show the version number
|
||||
items = super().get_metrics(*args, **kwargs)
|
||||
items.pop("v_num", None)
|
||||
return items
|
||||
|
||||
|
||||
class ProgressCallback(Callback):
|
||||
def __init__(self, save_path):
|
||||
super().__init__()
|
||||
self.save_path = save_path
|
||||
self._file_handle = None
|
||||
|
||||
@property
|
||||
def file_handle(self):
|
||||
if self._file_handle is None:
|
||||
self._file_handle = open(self.save_path, "w")
|
||||
return self._file_handle
|
||||
|
||||
@rank_zero_only
|
||||
def write(self, msg: str) -> None:
|
||||
self.file_handle.seek(0)
|
||||
self.file_handle.truncate()
|
||||
self.file_handle.write(msg)
|
||||
self.file_handle.flush()
|
||||
|
||||
@rank_zero_only
|
||||
def on_train_batch_end(self, trainer, pl_module, *args, **kwargs):
|
||||
self.write(
|
||||
f"Generation progress: {pl_module.true_global_step / trainer.max_steps * 100:.2f}%"
|
||||
)
|
||||
|
||||
@rank_zero_only
|
||||
def on_validation_start(self, trainer, pl_module):
|
||||
self.write(f"Rendering validation image ...")
|
||||
|
||||
@rank_zero_only
|
||||
def on_test_start(self, trainer, pl_module):
|
||||
self.write(f"Rendering video ...")
|
||||
|
||||
@rank_zero_only
|
||||
def on_predict_start(self, trainer, pl_module):
|
||||
self.write(f"Exporting mesh assets ...")
|
||||
@@ -0,0 +1,140 @@
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
|
||||
from omegaconf import OmegaConf
|
||||
|
||||
from .core import debug, find, info, warn
|
||||
from .typing import *
|
||||
|
||||
# ============ Register OmegaConf Resolvers ============= #
|
||||
OmegaConf.register_new_resolver(
|
||||
"calc_exp_lr_decay_rate", lambda factor, n: factor ** (1.0 / n)
|
||||
)
|
||||
OmegaConf.register_new_resolver("add", lambda a, b: a + b)
|
||||
OmegaConf.register_new_resolver("sub", lambda a, b: a - b)
|
||||
OmegaConf.register_new_resolver("mul", lambda a, b: a * b)
|
||||
OmegaConf.register_new_resolver("div", lambda a, b: a / b)
|
||||
OmegaConf.register_new_resolver("idiv", lambda a, b: a // b)
|
||||
OmegaConf.register_new_resolver("basename", lambda p: os.path.basename(p))
|
||||
OmegaConf.register_new_resolver("rmspace", lambda s, sub: s.replace(" ", sub))
|
||||
OmegaConf.register_new_resolver("tuple2", lambda s: [float(s), float(s)])
|
||||
OmegaConf.register_new_resolver("gt0", lambda s: s > 0)
|
||||
OmegaConf.register_new_resolver("not", lambda s: not s)
|
||||
|
||||
|
||||
def calc_num_train_steps(num_data, batch_size, max_epochs, num_nodes, num_cards=8):
|
||||
return int(num_data / (num_nodes * num_cards * batch_size)) * max_epochs
|
||||
|
||||
|
||||
OmegaConf.register_new_resolver("calc_num_train_steps", calc_num_train_steps)
|
||||
|
||||
# ======================================================= #
|
||||
|
||||
|
||||
# ============== Automatic Name Resolvers =============== #
|
||||
def get_naming_convention(cfg):
|
||||
# TODO
|
||||
name = f"lrm_{cfg.system.backbone.num_layers}"
|
||||
return name
|
||||
|
||||
|
||||
# ======================================================= #
|
||||
|
||||
|
||||
@dataclass
|
||||
class ExperimentConfig:
|
||||
name: str = "default"
|
||||
description: str = ""
|
||||
tag: str = ""
|
||||
seed: int = 0
|
||||
use_timestamp: bool = True
|
||||
timestamp: Optional[str] = None
|
||||
exp_root_dir: str = "outputs"
|
||||
|
||||
### these shouldn't be set manually
|
||||
exp_dir: str = "outputs/default"
|
||||
trial_name: str = "exp"
|
||||
trial_dir: str = "outputs/default/exp"
|
||||
n_gpus: int = 1
|
||||
###
|
||||
|
||||
resume: Optional[str] = None
|
||||
|
||||
data_cls: str = ""
|
||||
data: dict = field(default_factory=dict)
|
||||
|
||||
system_cls: str = ""
|
||||
system: dict = field(default_factory=dict)
|
||||
|
||||
# accept pytorch-lightning trainer parameters
|
||||
# see https://lightning.ai/docs/pytorch/stable/common/trainer.html#trainer-class-api
|
||||
trainer: dict = field(default_factory=dict)
|
||||
|
||||
# accept pytorch-lightning checkpoint callback parameters
|
||||
# see https://lightning.ai/docs/pytorch/stable/api/lightning.pytorch.callbacks.ModelCheckpoint.html#modelcheckpoint
|
||||
checkpoint: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
def load_config(
|
||||
*yamls: str, cli_args: list = [], from_string=False, makedirs=True, **kwargs
|
||||
) -> Any:
|
||||
if from_string:
|
||||
parse_func = OmegaConf.create
|
||||
else:
|
||||
parse_func = OmegaConf.load
|
||||
yaml_confs = []
|
||||
for y in yamls:
|
||||
conf = parse_func(y)
|
||||
extends = conf.pop("extends", None)
|
||||
if extends:
|
||||
assert os.path.exists(extends), f"File {extends} does not exist."
|
||||
yaml_confs.append(OmegaConf.load(extends))
|
||||
yaml_confs.append(conf)
|
||||
cli_conf = OmegaConf.from_cli(cli_args)
|
||||
cfg = OmegaConf.merge(*yaml_confs, cli_conf, kwargs)
|
||||
OmegaConf.resolve(cfg)
|
||||
assert isinstance(cfg, DictConfig)
|
||||
scfg: ExperimentConfig = parse_structured(ExperimentConfig, cfg)
|
||||
|
||||
# post processing
|
||||
# auto naming
|
||||
if scfg.name == "auto":
|
||||
scfg.name = get_naming_convention(scfg)
|
||||
# add timestamp
|
||||
if not scfg.tag and not scfg.use_timestamp:
|
||||
raise ValueError("Either tag is specified or use_timestamp is True.")
|
||||
scfg.trial_name = scfg.tag
|
||||
# if resume from an existing config, scfg.timestamp should not be None
|
||||
if scfg.timestamp is None:
|
||||
scfg.timestamp = ""
|
||||
if scfg.use_timestamp:
|
||||
if scfg.n_gpus > 1:
|
||||
warn(
|
||||
"Timestamp is disabled when using multiple GPUs, please make sure you have a unique tag."
|
||||
)
|
||||
else:
|
||||
scfg.timestamp = datetime.now().strftime("@%Y%m%d-%H%M%S")
|
||||
# make directories
|
||||
scfg.trial_name += scfg.timestamp
|
||||
scfg.exp_dir = os.path.join(scfg.exp_root_dir, scfg.name)
|
||||
scfg.trial_dir = os.path.join(scfg.exp_dir, scfg.trial_name)
|
||||
|
||||
if makedirs:
|
||||
os.makedirs(scfg.trial_dir, exist_ok=True)
|
||||
|
||||
return scfg
|
||||
|
||||
|
||||
def config_to_primitive(config, resolve: bool = True) -> Any:
|
||||
return OmegaConf.to_container(config, resolve=resolve)
|
||||
|
||||
|
||||
def dump_config(path: str, config) -> None:
|
||||
with open(path, "w") as fp:
|
||||
OmegaConf.save(config=config, f=fp)
|
||||
|
||||
|
||||
def parse_structured(fields: Any, cfg: Optional[Union[dict, DictConfig]] = None) -> Any:
|
||||
scfg = OmegaConf.merge(OmegaConf.structured(fields), cfg)
|
||||
return scfg
|
||||
@@ -0,0 +1,29 @@
|
||||
import importlib
|
||||
|
||||
### grammar sugar for logging utilities ###
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger("pytorch_lightning")
|
||||
|
||||
from pytorch_lightning.utilities.rank_zero import (
|
||||
rank_zero_debug,
|
||||
rank_zero_info,
|
||||
rank_zero_only,
|
||||
)
|
||||
|
||||
|
||||
def find(cls_string):
|
||||
module_string = ".".join(cls_string.split(".")[:-1])
|
||||
cls_name = cls_string.split(".")[-1]
|
||||
module = importlib.import_module(module_string, package=None)
|
||||
cls = getattr(module, cls_name)
|
||||
return cls
|
||||
|
||||
|
||||
debug = rank_zero_debug
|
||||
info = rank_zero_info
|
||||
|
||||
|
||||
@rank_zero_only
|
||||
def warn(*args, **kwargs):
|
||||
logger.warn(*args, **kwargs)
|
||||
@@ -0,0 +1,253 @@
|
||||
from typing import List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from torch.nn import functional as F
|
||||
|
||||
|
||||
def get_position_map_from_depth(depth, mask, intrinsics, extrinsics, image_wh=None):
|
||||
"""Compute the position map from the depth map and the camera parameters for a batch of views.
|
||||
|
||||
Args:
|
||||
depth (torch.Tensor): The depth maps with the shape (B, H, W, 1).
|
||||
mask (torch.Tensor): The masks with the shape (B, H, W, 1).
|
||||
intrinsics (torch.Tensor): The camera intrinsics matrices with the shape (B, 3, 3).
|
||||
extrinsics (torch.Tensor): The camera extrinsics matrices with the shape (B, 4, 4).
|
||||
image_wh (Tuple[int, int]): The image width and height.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: The position maps with the shape (B, H, W, 3).
|
||||
"""
|
||||
if image_wh is None:
|
||||
image_wh = depth.shape[2], depth.shape[1]
|
||||
|
||||
B, H, W, _ = depth.shape
|
||||
depth = depth.squeeze(-1)
|
||||
|
||||
u_coord, v_coord = torch.meshgrid(
|
||||
torch.arange(image_wh[0]), torch.arange(image_wh[1]), indexing="xy"
|
||||
)
|
||||
u_coord = u_coord.type_as(depth).unsqueeze(0).expand(B, -1, -1)
|
||||
v_coord = v_coord.type_as(depth).unsqueeze(0).expand(B, -1, -1)
|
||||
|
||||
# Compute the position map by back-projecting depth pixels to 3D space
|
||||
x = (
|
||||
(u_coord - intrinsics[:, 0, 2].unsqueeze(-1).unsqueeze(-1))
|
||||
* depth
|
||||
/ intrinsics[:, 0, 0].unsqueeze(-1).unsqueeze(-1)
|
||||
)
|
||||
y = (
|
||||
(v_coord - intrinsics[:, 1, 2].unsqueeze(-1).unsqueeze(-1))
|
||||
* depth
|
||||
/ intrinsics[:, 1, 1].unsqueeze(-1).unsqueeze(-1)
|
||||
)
|
||||
z = depth
|
||||
|
||||
# Concatenate to form the 3D coordinates in the camera frame
|
||||
camera_coords = torch.stack([x, y, z], dim=-1)
|
||||
|
||||
# Apply the extrinsic matrix to get coordinates in the world frame
|
||||
coords_homogeneous = torch.nn.functional.pad(
|
||||
camera_coords, (0, 1), "constant", 1.0
|
||||
) # Add a homogeneous coordinate
|
||||
world_coords = torch.matmul(
|
||||
coords_homogeneous.view(B, -1, 4), extrinsics.transpose(1, 2)
|
||||
).view(B, H, W, 4)
|
||||
|
||||
# Apply the mask to the position map
|
||||
position_map = world_coords[..., :3] * mask
|
||||
|
||||
return position_map
|
||||
|
||||
|
||||
def get_position_map_from_depth_ortho(
|
||||
depth, mask, extrinsics, ortho_scale, image_wh=None
|
||||
):
|
||||
"""Compute the position map from the depth map and the camera parameters for a batch of views
|
||||
using orthographic projection with a given ortho_scale.
|
||||
|
||||
Args:
|
||||
depth (torch.Tensor): The depth maps with the shape (B, H, W, 1).
|
||||
mask (torch.Tensor): The masks with the shape (B, H, W, 1).
|
||||
extrinsics (torch.Tensor): The camera extrinsics matrices with the shape (B, 4, 4).
|
||||
ortho_scale (torch.Tensor): The scaling factor for the orthographic projection with the shape (B, 1, 1, 1).
|
||||
image_wh (Tuple[int, int]): Optional. The image width and height.
|
||||
|
||||
Returns:
|
||||
torch.Tensor: The position maps with the shape (B, H, W, 3).
|
||||
"""
|
||||
if image_wh is None:
|
||||
image_wh = depth.shape[2], depth.shape[1]
|
||||
|
||||
B, H, W, _ = depth.shape
|
||||
depth = depth.squeeze(-1)
|
||||
|
||||
# Generating grid of coordinates in the image space
|
||||
u_coord, v_coord = torch.meshgrid(
|
||||
torch.arange(0, image_wh[0]), torch.arange(0, image_wh[1]), indexing="xy"
|
||||
)
|
||||
u_coord = u_coord.type_as(depth).unsqueeze(0).expand(B, -1, -1)
|
||||
v_coord = v_coord.type_as(depth).unsqueeze(0).expand(B, -1, -1)
|
||||
|
||||
# Compute the position map using orthographic projection with ortho_scale
|
||||
x = (u_coord - image_wh[0] / 2) * ortho_scale / image_wh[0]
|
||||
y = (v_coord - image_wh[1] / 2) * ortho_scale / image_wh[1]
|
||||
z = depth
|
||||
|
||||
# Concatenate to form the 3D coordinates in the camera frame
|
||||
camera_coords = torch.stack([x, y, z], dim=-1)
|
||||
|
||||
# Apply the extrinsic matrix to get coordinates in the world frame
|
||||
coords_homogeneous = torch.nn.functional.pad(
|
||||
camera_coords, (0, 1), "constant", 1.0
|
||||
) # Add a homogeneous coordinate
|
||||
world_coords = torch.matmul(
|
||||
coords_homogeneous.view(B, -1, 4), extrinsics.transpose(1, 2)
|
||||
).view(B, H, W, 4)
|
||||
|
||||
# Apply the mask to the position map
|
||||
position_map = world_coords[..., :3] * mask
|
||||
|
||||
return position_map
|
||||
|
||||
|
||||
def get_opencv_from_blender(matrix_world, fov=None, image_size=None):
|
||||
# convert matrix_world to opencv format extrinsics
|
||||
opencv_world_to_cam = matrix_world.inverse()
|
||||
opencv_world_to_cam[1, :] *= -1
|
||||
opencv_world_to_cam[2, :] *= -1
|
||||
R, T = opencv_world_to_cam[:3, :3], opencv_world_to_cam[:3, 3]
|
||||
|
||||
if fov is None: # orthographic camera
|
||||
return R, T
|
||||
|
||||
R, T = R.unsqueeze(0), T.unsqueeze(0)
|
||||
# convert fov to opencv format intrinsics
|
||||
focal = 1 / np.tan(fov / 2)
|
||||
intrinsics = np.diag(np.array([focal, focal, 1])).astype(np.float32)
|
||||
opencv_cam_matrix = (
|
||||
torch.from_numpy(intrinsics).unsqueeze(0).float().to(matrix_world.device)
|
||||
)
|
||||
opencv_cam_matrix[:, :2, -1] += torch.tensor([image_size / 2, image_size / 2]).to(
|
||||
matrix_world.device
|
||||
)
|
||||
opencv_cam_matrix[:, [0, 1], [0, 1]] *= image_size / 2
|
||||
|
||||
return R, T, opencv_cam_matrix
|
||||
|
||||
|
||||
def get_ray_directions(
|
||||
H: int,
|
||||
W: int,
|
||||
focal: float,
|
||||
principal: Optional[Tuple[float, float]] = None,
|
||||
use_pixel_centers: bool = True,
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Get ray directions for all pixels in camera coordinate.
|
||||
Args:
|
||||
H, W, focal, principal, use_pixel_centers: image height, width, focal length, principal point and whether use pixel centers
|
||||
Outputs:
|
||||
directions: (H, W, 3), the direction of the rays in camera coordinate
|
||||
"""
|
||||
pixel_center = 0.5 if use_pixel_centers else 0
|
||||
cx, cy = W / 2, H / 2 if principal is None else principal
|
||||
i, j = torch.meshgrid(
|
||||
torch.arange(W, dtype=torch.float32) + pixel_center,
|
||||
torch.arange(H, dtype=torch.float32) + pixel_center,
|
||||
indexing="xy",
|
||||
)
|
||||
directions = torch.stack(
|
||||
[(i - cx) / focal, -(j - cy) / focal, -torch.ones_like(i)], -1
|
||||
)
|
||||
return F.normalize(directions, dim=-1)
|
||||
|
||||
|
||||
def get_rays(
|
||||
directions: torch.Tensor, c2w: torch.Tensor
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
"""
|
||||
Get ray origins and directions from camera coordinates to world coordinates
|
||||
Args:
|
||||
directions: (H, W, 3) ray directions in camera coordinates
|
||||
c2w: (4, 4) camera-to-world transformation matrix
|
||||
Outputs:
|
||||
rays_o, rays_d: (H, W, 3) ray origins and directions in world coordinates
|
||||
"""
|
||||
# Rotate ray directions from camera coordinate to the world coordinate
|
||||
rays_d = directions @ c2w[:3, :3].T
|
||||
rays_o = c2w[:3, 3].expand(rays_d.shape)
|
||||
return rays_o, rays_d
|
||||
|
||||
|
||||
def compute_plucker_embed(
|
||||
c2w: torch.Tensor, image_width: int, image_height: int, focal: float
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Computes Plucker coordinates for a camera.
|
||||
Args:
|
||||
c2w: (4, 4) camera-to-world transformation matrix
|
||||
image_width: Image width
|
||||
image_height: Image height
|
||||
focal: Focal length of the camera
|
||||
Returns:
|
||||
plucker: (6, H, W) Plucker embedding
|
||||
"""
|
||||
directions = get_ray_directions(image_height, image_width, focal)
|
||||
rays_o, rays_d = get_rays(directions, c2w)
|
||||
# Cross product to get Plucker coordinates
|
||||
cross = torch.cross(rays_o, rays_d, dim=-1)
|
||||
plucker = torch.cat((rays_d, cross), dim=-1)
|
||||
return plucker.permute(2, 0, 1)
|
||||
|
||||
|
||||
def get_plucker_embeds_from_cameras(
|
||||
c2w: List[torch.Tensor], fov: List[float], image_size: int
|
||||
) -> torch.Tensor:
|
||||
"""
|
||||
Given lists of camera transformations and fov, returns the batched plucker embeddings.
|
||||
Args:
|
||||
c2w: list of camera-to-world transformation matrices
|
||||
fov: list of field of view values
|
||||
image_size: size of the image
|
||||
Returns:
|
||||
plucker_embeds: (B, 6, H, W) batched plucker embeddings
|
||||
"""
|
||||
plucker_embeds = []
|
||||
for cam_matrix, cam_fov in zip(c2w, fov):
|
||||
focal = 0.5 * image_size / np.tan(0.5 * cam_fov)
|
||||
plucker = compute_plucker_embed(cam_matrix, image_size, image_size, focal)
|
||||
plucker_embeds.append(plucker)
|
||||
return torch.stack(plucker_embeds)
|
||||
|
||||
|
||||
def get_plucker_embeds_from_cameras_ortho(
|
||||
c2w: List[torch.Tensor], ortho_scale: List[float], image_size: int
|
||||
):
|
||||
"""
|
||||
Given lists of camera transformations and fov, returns the batched plucker embeddings.
|
||||
|
||||
Parameters:
|
||||
c2w: list of camera-to-world transformation matrices
|
||||
fov: list of field of view values
|
||||
image_size: size of the image
|
||||
|
||||
Returns:
|
||||
plucker_embeds: plucker embeddings (B, 6, H, W)
|
||||
"""
|
||||
plucker_embeds = []
|
||||
# compute pairwise mask and plucker embeddings
|
||||
for cam_matrix, scale in zip(c2w, ortho_scale):
|
||||
# blender to opencv to pytorch3d
|
||||
R, T = get_opencv_from_blender(cam_matrix)
|
||||
cam_pos = -R.T @ T
|
||||
view_dir = R.T @ torch.tensor([0, 0, 1]).float().to(cam_matrix.device)
|
||||
# normalize camera position
|
||||
cam_pos = F.normalize(cam_pos, dim=0)
|
||||
plucker = torch.concat([view_dir, cam_pos])
|
||||
plucker = plucker.unsqueeze(-1).unsqueeze(-1).repeat(1, image_size, image_size)
|
||||
plucker_embeds.append(plucker)
|
||||
|
||||
plucker_embeds = torch.stack(plucker_embeds)
|
||||
|
||||
return plucker_embeds
|
||||
@@ -0,0 +1,340 @@
|
||||
# coding=utf-8
|
||||
# Copyright 2024 Optuna, Hugging Face
|
||||
#
|
||||
# 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.
|
||||
""" Logging utilities."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from logging import CRITICAL # NOQA
|
||||
from logging import DEBUG # NOQA
|
||||
from logging import ERROR # NOQA
|
||||
from logging import FATAL # NOQA
|
||||
from logging import INFO # NOQA
|
||||
from logging import NOTSET # NOQA
|
||||
from logging import WARN # NOQA
|
||||
from logging import WARNING # NOQA
|
||||
from typing import Dict, Optional
|
||||
|
||||
from tqdm import auto as tqdm_lib
|
||||
|
||||
_lock = threading.Lock()
|
||||
_default_handler: Optional[logging.Handler] = None
|
||||
|
||||
log_levels = {
|
||||
"debug": logging.DEBUG,
|
||||
"info": logging.INFO,
|
||||
"warning": logging.WARNING,
|
||||
"error": logging.ERROR,
|
||||
"critical": logging.CRITICAL,
|
||||
}
|
||||
|
||||
_default_log_level = logging.INFO
|
||||
|
||||
_tqdm_active = True
|
||||
|
||||
|
||||
def _get_default_logging_level() -> int:
|
||||
"""
|
||||
If LATEXTURE_VERBOSITY env var is set to one of the valid choices return that as the new default level. If it is
|
||||
not - fall back to `_default_log_level`
|
||||
"""
|
||||
env_level_str = os.getenv("LATEXTURE_VERBOSITY", None)
|
||||
if env_level_str:
|
||||
if env_level_str in log_levels:
|
||||
return log_levels[env_level_str]
|
||||
else:
|
||||
logging.getLogger().warning(
|
||||
f"Unknown option LATEXTURE_VERBOSITY={env_level_str}, "
|
||||
f"has to be one of: { ', '.join(log_levels.keys()) }"
|
||||
)
|
||||
return _default_log_level
|
||||
|
||||
|
||||
def _get_library_name() -> str:
|
||||
return __name__.split(".")[0]
|
||||
|
||||
|
||||
def _get_library_root_logger() -> logging.Logger:
|
||||
return logging.getLogger(_get_library_name())
|
||||
|
||||
|
||||
def _configure_library_root_logger() -> None:
|
||||
global _default_handler
|
||||
|
||||
with _lock:
|
||||
if _default_handler:
|
||||
# This library has already configured the library root logger.
|
||||
return
|
||||
_default_handler = logging.StreamHandler() # Set sys.stderr as stream.
|
||||
_default_handler.flush = sys.stderr.flush
|
||||
|
||||
# Apply our default configuration to the library root logger.
|
||||
library_root_logger = _get_library_root_logger()
|
||||
library_root_logger.addHandler(_default_handler)
|
||||
library_root_logger.setLevel(_get_default_logging_level())
|
||||
library_root_logger.propagate = False
|
||||
|
||||
enable_explicit_format()
|
||||
|
||||
|
||||
def _reset_library_root_logger() -> None:
|
||||
global _default_handler
|
||||
|
||||
with _lock:
|
||||
if not _default_handler:
|
||||
return
|
||||
|
||||
library_root_logger = _get_library_root_logger()
|
||||
library_root_logger.removeHandler(_default_handler)
|
||||
library_root_logger.setLevel(logging.NOTSET)
|
||||
_default_handler = None
|
||||
|
||||
|
||||
def get_log_levels_dict() -> Dict[str, int]:
|
||||
return log_levels
|
||||
|
||||
|
||||
def get_logger(name: Optional[str] = None) -> logging.Logger:
|
||||
"""
|
||||
Return a logger with the specified name.
|
||||
|
||||
This function is not supposed to be directly accessed unless you are writing a custom diffusers module.
|
||||
"""
|
||||
|
||||
if name is None:
|
||||
name = _get_library_name()
|
||||
|
||||
_configure_library_root_logger()
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
def get_verbosity() -> int:
|
||||
"""
|
||||
Return the current level for the 🤗 Diffusers' root logger as an `int`.
|
||||
|
||||
Returns:
|
||||
`int`:
|
||||
Logging level integers which can be one of:
|
||||
|
||||
- `50`: `diffusers.logging.CRITICAL` or `diffusers.logging.FATAL`
|
||||
- `40`: `diffusers.logging.ERROR`
|
||||
- `30`: `diffusers.logging.WARNING` or `diffusers.logging.WARN`
|
||||
- `20`: `diffusers.logging.INFO`
|
||||
- `10`: `diffusers.logging.DEBUG`
|
||||
|
||||
"""
|
||||
|
||||
_configure_library_root_logger()
|
||||
return _get_library_root_logger().getEffectiveLevel()
|
||||
|
||||
|
||||
def set_verbosity(verbosity: int) -> None:
|
||||
"""
|
||||
Set the verbosity level for the 🤗 Diffusers' root logger.
|
||||
|
||||
Args:
|
||||
verbosity (`int`):
|
||||
Logging level which can be one of:
|
||||
|
||||
- `diffusers.logging.CRITICAL` or `diffusers.logging.FATAL`
|
||||
- `diffusers.logging.ERROR`
|
||||
- `diffusers.logging.WARNING` or `diffusers.logging.WARN`
|
||||
- `diffusers.logging.INFO`
|
||||
- `diffusers.logging.DEBUG`
|
||||
"""
|
||||
|
||||
_configure_library_root_logger()
|
||||
_get_library_root_logger().setLevel(verbosity)
|
||||
|
||||
|
||||
def set_verbosity_info() -> None:
|
||||
"""Set the verbosity to the `INFO` level."""
|
||||
return set_verbosity(INFO)
|
||||
|
||||
|
||||
def set_verbosity_warning() -> None:
|
||||
"""Set the verbosity to the `WARNING` level."""
|
||||
return set_verbosity(WARNING)
|
||||
|
||||
|
||||
def set_verbosity_debug() -> None:
|
||||
"""Set the verbosity to the `DEBUG` level."""
|
||||
return set_verbosity(DEBUG)
|
||||
|
||||
|
||||
def set_verbosity_error() -> None:
|
||||
"""Set the verbosity to the `ERROR` level."""
|
||||
return set_verbosity(ERROR)
|
||||
|
||||
|
||||
def disable_default_handler() -> None:
|
||||
"""Disable the default handler of the 🤗 Diffusers' root logger."""
|
||||
|
||||
_configure_library_root_logger()
|
||||
|
||||
assert _default_handler is not None
|
||||
_get_library_root_logger().removeHandler(_default_handler)
|
||||
|
||||
|
||||
def enable_default_handler() -> None:
|
||||
"""Enable the default handler of the 🤗 Diffusers' root logger."""
|
||||
|
||||
_configure_library_root_logger()
|
||||
|
||||
assert _default_handler is not None
|
||||
_get_library_root_logger().addHandler(_default_handler)
|
||||
|
||||
|
||||
def add_handler(handler: logging.Handler) -> None:
|
||||
"""adds a handler to the HuggingFace Diffusers' root logger."""
|
||||
|
||||
_configure_library_root_logger()
|
||||
|
||||
assert handler is not None
|
||||
_get_library_root_logger().addHandler(handler)
|
||||
|
||||
|
||||
def remove_handler(handler: logging.Handler) -> None:
|
||||
"""removes given handler from the HuggingFace Diffusers' root logger."""
|
||||
|
||||
_configure_library_root_logger()
|
||||
|
||||
assert handler is not None and handler in _get_library_root_logger().handlers
|
||||
_get_library_root_logger().removeHandler(handler)
|
||||
|
||||
|
||||
def disable_propagation() -> None:
|
||||
"""
|
||||
Disable propagation of the library log outputs. Note that log propagation is disabled by default.
|
||||
"""
|
||||
|
||||
_configure_library_root_logger()
|
||||
_get_library_root_logger().propagate = False
|
||||
|
||||
|
||||
def enable_propagation() -> None:
|
||||
"""
|
||||
Enable propagation of the library log outputs. Please disable the HuggingFace Diffusers' default handler to prevent
|
||||
double logging if the root logger has been configured.
|
||||
"""
|
||||
|
||||
_configure_library_root_logger()
|
||||
_get_library_root_logger().propagate = True
|
||||
|
||||
|
||||
def enable_explicit_format() -> None:
|
||||
"""
|
||||
Enable explicit formatting for every 🤗 Diffusers' logger. The explicit formatter is as follows:
|
||||
```
|
||||
[LEVELNAME|FILENAME|LINE NUMBER] TIME >> MESSAGE
|
||||
```
|
||||
All handlers currently bound to the root logger are affected by this method.
|
||||
"""
|
||||
handlers = _get_library_root_logger().handlers
|
||||
|
||||
for handler in handlers:
|
||||
formatter = logging.Formatter(
|
||||
"[%(levelname)s|%(filename)s:%(lineno)s] %(asctime)s >> %(message)s"
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
|
||||
def reset_format() -> None:
|
||||
"""
|
||||
Resets the formatting for 🤗 Diffusers' loggers.
|
||||
|
||||
All handlers currently bound to the root logger are affected by this method.
|
||||
"""
|
||||
handlers = _get_library_root_logger().handlers
|
||||
|
||||
for handler in handlers:
|
||||
handler.setFormatter(None)
|
||||
|
||||
|
||||
def warning_advice(self, *args, **kwargs) -> None:
|
||||
"""
|
||||
This method is identical to `logger.warning()`, but if env var LATEXTURE_NO_ADVISORY_WARNINGS=1 is set, this
|
||||
warning will not be printed
|
||||
"""
|
||||
no_advisory_warnings = os.getenv("LATEXTURE_NO_ADVISORY_WARNINGS", False)
|
||||
if no_advisory_warnings:
|
||||
return
|
||||
self.warning(*args, **kwargs)
|
||||
|
||||
|
||||
logging.Logger.warning_advice = warning_advice
|
||||
|
||||
|
||||
class EmptyTqdm:
|
||||
"""Dummy tqdm which doesn't do anything."""
|
||||
|
||||
def __init__(self, *args, **kwargs): # pylint: disable=unused-argument
|
||||
self._iterator = args[0] if args else None
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._iterator)
|
||||
|
||||
def __getattr__(self, _):
|
||||
"""Return empty function."""
|
||||
|
||||
def empty_fn(*args, **kwargs): # pylint: disable=unused-argument
|
||||
return
|
||||
|
||||
return empty_fn
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, type_, value, traceback):
|
||||
return
|
||||
|
||||
|
||||
class _tqdm_cls:
|
||||
def __call__(self, *args, **kwargs):
|
||||
if _tqdm_active:
|
||||
return tqdm_lib.tqdm(*args, **kwargs)
|
||||
else:
|
||||
return EmptyTqdm(*args, **kwargs)
|
||||
|
||||
def set_lock(self, *args, **kwargs):
|
||||
self._lock = None
|
||||
if _tqdm_active:
|
||||
return tqdm_lib.tqdm.set_lock(*args, **kwargs)
|
||||
|
||||
def get_lock(self):
|
||||
if _tqdm_active:
|
||||
return tqdm_lib.tqdm.get_lock()
|
||||
|
||||
|
||||
tqdm = _tqdm_cls()
|
||||
|
||||
|
||||
def is_progress_bar_enabled() -> bool:
|
||||
"""Return a boolean indicating whether tqdm progress bars are enabled."""
|
||||
global _tqdm_active
|
||||
return bool(_tqdm_active)
|
||||
|
||||
|
||||
def enable_progress_bar() -> None:
|
||||
"""Enable tqdm progress bar."""
|
||||
global _tqdm_active
|
||||
_tqdm_active = True
|
||||
|
||||
|
||||
def disable_progress_bar() -> None:
|
||||
"""Disable tqdm progress bar."""
|
||||
global _tqdm_active
|
||||
_tqdm_active = False
|
||||
@@ -0,0 +1,20 @@
|
||||
from .camera import (
|
||||
Camera,
|
||||
get_c2w,
|
||||
get_camera,
|
||||
get_orthogonal_camera,
|
||||
get_orthogonal_projection_matrix,
|
||||
get_projection_matrix,
|
||||
)
|
||||
from .mesh import TexturedMesh, load_mesh, replace_mesh_texture_and_save
|
||||
from .projection import CameraProjection, CameraProjectionOutput
|
||||
from .render import (
|
||||
DepthControlNetNormalization,
|
||||
DepthNormalizationStrategy,
|
||||
NVDiffRastContextWrapper,
|
||||
RenderOutput,
|
||||
SimpleNormalization,
|
||||
Zero123PlusPlusNormalization,
|
||||
render,
|
||||
)
|
||||
from .smart_paint import SmartPainter
|
||||
@@ -0,0 +1,324 @@
|
||||
import time
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import triton
|
||||
import triton.language as tl
|
||||
from torch.utils.cpp_extension import load_inline
|
||||
|
||||
from .utils import SINGLE_IMAGE_TYPE, image_to_tensor
|
||||
|
||||
pb_solver_cpp_source = """
|
||||
#include <torch/extension.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
// CUDA forward declarations
|
||||
|
||||
void pb_solver_run_cuda(
|
||||
torch::Tensor A,
|
||||
torch::Tensor X,
|
||||
torch::Tensor B,
|
||||
torch::Tensor Xbuf,
|
||||
int num_iters
|
||||
);
|
||||
|
||||
// C++ interface
|
||||
|
||||
#define CHECK_CUDA(x) TORCH_CHECK(x.device().is_cuda(), #x " must be a CUDA tensor")
|
||||
#define CHECK_CONTIGUOUS(x) TORCH_CHECK(x.is_contiguous(), #x " must be contiguous")
|
||||
#define CHECK_INPUT(x) CHECK_CUDA(x); CHECK_CONTIGUOUS(x)
|
||||
|
||||
void pb_solver_run(
|
||||
torch::Tensor A,
|
||||
torch::Tensor X,
|
||||
torch::Tensor B,
|
||||
torch::Tensor Xbuf,
|
||||
int num_iters
|
||||
) {
|
||||
CHECK_INPUT(A);
|
||||
CHECK_INPUT(X);
|
||||
CHECK_INPUT(B);
|
||||
CHECK_INPUT(Xbuf);
|
||||
|
||||
pb_solver_run_cuda(A, X, B, Xbuf, num_iters);
|
||||
return;
|
||||
}
|
||||
|
||||
"""
|
||||
|
||||
pb_solver_cuda_source = """
|
||||
#include <torch/extension.h>
|
||||
|
||||
#include <cuda.h>
|
||||
#include <cuda_runtime.h>
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
|
||||
__global__ void pb_solver_run_cuda_kernel(
|
||||
torch::PackedTensorAccessor32<int64_t,2,torch::RestrictPtrTraits> A,
|
||||
torch::PackedTensorAccessor32<float,1,torch::RestrictPtrTraits> X,
|
||||
torch::PackedTensorAccessor32<float,1,torch::RestrictPtrTraits> B,
|
||||
torch::PackedTensorAccessor32<float,1,torch::RestrictPtrTraits> Xbuf
|
||||
) {
|
||||
const int index = blockIdx.x * blockDim.x + threadIdx.x;
|
||||
|
||||
if (index < A.size(0)) {
|
||||
Xbuf[index] = (X[A[index][0]] + X[A[index][1]] + X[A[index][2]] + X[A[index][3]] + B[index]) / 4.;
|
||||
}
|
||||
}
|
||||
|
||||
void pb_solver_run_cuda(
|
||||
torch::Tensor A,
|
||||
torch::Tensor X,
|
||||
torch::Tensor B,
|
||||
torch::Tensor Xbuf,
|
||||
int num_iters
|
||||
) {
|
||||
int batch_size = A.size(0);
|
||||
|
||||
const int threads = 1024;
|
||||
const dim3 blocks((batch_size + threads - 1) / threads);
|
||||
|
||||
auto A_ptr = A.packed_accessor32<int64_t,2,torch::RestrictPtrTraits>();
|
||||
auto X_ptr = X.packed_accessor32<float,1,torch::RestrictPtrTraits>();
|
||||
auto B_ptr = B.packed_accessor32<float,1,torch::RestrictPtrTraits>();
|
||||
auto Xbuf_ptr = Xbuf.packed_accessor32<float,1,torch::RestrictPtrTraits>();
|
||||
|
||||
for (int i = 0; i < num_iters; ++i) {
|
||||
pb_solver_run_cuda_kernel<<<blocks, threads>>>(
|
||||
A_ptr,
|
||||
X_ptr,
|
||||
B_ptr,
|
||||
Xbuf_ptr
|
||||
);
|
||||
cudaDeviceSynchronize();
|
||||
std::swap(X_ptr, Xbuf_ptr);
|
||||
}
|
||||
// we may waste an iteration here, but it's fine
|
||||
return;
|
||||
}
|
||||
"""
|
||||
|
||||
|
||||
class PBBackend(ABC):
|
||||
def __init__(self, *args, **kwargs) -> None:
|
||||
pass
|
||||
|
||||
def solve(self, num_iters, A, X, B, Xbuf) -> None:
|
||||
pass
|
||||
|
||||
|
||||
try:
|
||||
|
||||
@triton.jit
|
||||
def pb_triton_step_kernel(
|
||||
A_ptr,
|
||||
X_ptr,
|
||||
B_ptr,
|
||||
Xbuf_ptr,
|
||||
A_row_stride: tl.constexpr,
|
||||
n_elements: tl.constexpr,
|
||||
BLOCK_SIZE: tl.constexpr,
|
||||
):
|
||||
pid = tl.program_id(axis=0) # We use a 1D launch grid so axis is 0.
|
||||
block_start = pid * BLOCK_SIZE
|
||||
offsets = tl.arange(0, BLOCK_SIZE) + block_start
|
||||
|
||||
A_start_ptr = A_ptr + block_start * A_row_stride
|
||||
A_ptrs = A_start_ptr + (
|
||||
tl.arange(0, BLOCK_SIZE)[:, None] * A_row_stride
|
||||
+ tl.arange(0, A_row_stride)[None, :]
|
||||
)
|
||||
B_ptrs = B_ptr + offsets
|
||||
Xbuf_ptrs = Xbuf_ptr + offsets
|
||||
|
||||
mask = offsets < n_elements
|
||||
|
||||
A = tl.load(A_ptrs, mask=mask[:, None], other=0)
|
||||
X = tl.load(X_ptr + A)
|
||||
B = tl.load(B_ptrs, mask=mask, other=0.0)
|
||||
|
||||
Xout = (tl.sum(X, axis=1) + B) / 4
|
||||
tl.store(Xbuf_ptrs, Xout, mask=mask)
|
||||
|
||||
except:
|
||||
pb_triton_step_kernel = None
|
||||
|
||||
|
||||
class PBTorchCUDAKernelBackend(PBBackend):
|
||||
def __init__(self) -> None:
|
||||
self.kernel = load_inline(
|
||||
name="pb_solver",
|
||||
cpp_sources=[pb_solver_cpp_source],
|
||||
cuda_sources=[pb_solver_cuda_source],
|
||||
functions=["pb_solver_run"],
|
||||
verbose=True,
|
||||
)
|
||||
|
||||
def solve(self, num_iters, A, X, B, Xbuf) -> None:
|
||||
self.kernel.pb_solver_run(A, X, B, Xbuf, num_iters)
|
||||
|
||||
|
||||
class PBTritonBackend(PBBackend):
|
||||
def step(
|
||||
self, X: torch.Tensor, A: torch.Tensor, B: torch.Tensor, Xbuf: torch.Tensor
|
||||
) -> None:
|
||||
n_elements = X.shape[0]
|
||||
grid = lambda meta: (triton.cdiv(n_elements, meta["BLOCK_SIZE"]),)
|
||||
assert pb_triton_step_kernel is not None
|
||||
pb_triton_step_kernel[grid](A, X, B, Xbuf, A.stride(0), n_elements, 1024)
|
||||
|
||||
def solve(self, num_iters, A, X, B, Xbuf) -> None:
|
||||
for _ in range(num_iters):
|
||||
self.step(X, A, B, Xbuf)
|
||||
X, Xbuf = Xbuf, X
|
||||
|
||||
|
||||
class PBTorchNativeBackend(PBBackend):
|
||||
def step(
|
||||
self, X: torch.Tensor, A: torch.Tensor, B: torch.Tensor, Xbuf: torch.Tensor
|
||||
) -> None:
|
||||
X.copy_(X[A].sum(-1).add_(B).div_(4))
|
||||
|
||||
def solve(self, num_iters, A, X, B, Xbuf) -> None:
|
||||
for _ in range(num_iters):
|
||||
self.step(X, A, B, Xbuf)
|
||||
|
||||
|
||||
class PoissonBlendingSolver:
|
||||
def __init__(self, backend: str, device: str):
|
||||
self.backend = backend
|
||||
if backend == "torch-native":
|
||||
self.pb_solver = PBTorchNativeBackend()
|
||||
elif backend == "torch-cuda":
|
||||
self.pb_solver = PBTorchCUDAKernelBackend()
|
||||
elif backend == "triton":
|
||||
self.pb_solver = PBTritonBackend()
|
||||
else:
|
||||
raise ValueError(f"Unknown backend: {backend}")
|
||||
|
||||
self.device = device
|
||||
self.lap_kernel = torch.tensor(
|
||||
[[0, -1, 0], [-1, 4, -1], [0, -1, 0]], device=device, dtype=torch.float32
|
||||
).view(1, 1, 3, 3)
|
||||
self.lap_kernel4 = torch.tensor(
|
||||
[
|
||||
[[0, -1, 0], [0, 1, 0], [0, 0, 0]],
|
||||
[[0, 0, 0], [0, 1, 0], [0, -1, 0]],
|
||||
[[0, 0, 0], [-1, 1, 0], [0, 0, 0]],
|
||||
[[0, 0, 0], [0, 1, -1], [0, 0, 0]],
|
||||
],
|
||||
device=device,
|
||||
dtype=torch.float32,
|
||||
).view(4, 1, 3, 3)
|
||||
self.neighbor_kernel = torch.tensor(
|
||||
[[0, 1, 0], [1, 0, 1], [0, 1, 0]], device=device, dtype=torch.float32
|
||||
).view(1, 1, 3, 3)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
src: SINGLE_IMAGE_TYPE,
|
||||
mask: SINGLE_IMAGE_TYPE,
|
||||
tgt: SINGLE_IMAGE_TYPE,
|
||||
num_iters: int,
|
||||
inplace: bool = True,
|
||||
grad_mode: str = "src",
|
||||
):
|
||||
src = image_to_tensor(src, device=self.device)
|
||||
mask = image_to_tensor(mask, device=self.device)
|
||||
tgt = image_to_tensor(tgt, device=self.device)
|
||||
|
||||
assert src.ndim == 3 and tgt.ndim == 3 and mask.ndim in [2, 3]
|
||||
|
||||
if mask.ndim == 3:
|
||||
mask = mask.mean(-1) > 0.5
|
||||
else:
|
||||
mask = mask > 0.5
|
||||
mask[0, :] = 0
|
||||
mask[-1, :] = 0
|
||||
mask[:, 0] = 0
|
||||
mask[:, -1] = 0
|
||||
|
||||
tgt_masked = torch.where(mask[..., None], torch.zeros_like(tgt), tgt)
|
||||
|
||||
x, y = mask.nonzero(as_tuple=True)
|
||||
N = x.shape[0]
|
||||
index_map = torch.cumsum(mask.reshape(-1).long(), dim=-1).reshape(mask.shape)
|
||||
index_map[~mask] = 0
|
||||
|
||||
if grad_mode == "src":
|
||||
src_lap = F.conv2d(
|
||||
src.permute(2, 0, 1)[:, None],
|
||||
weight=self.lap_kernel,
|
||||
padding=1,
|
||||
)[:, 0].permute(1, 2, 0)
|
||||
lap = src_lap
|
||||
elif grad_mode == "max":
|
||||
src_lap = F.conv2d(
|
||||
src.permute(2, 0, 1)[:, None],
|
||||
weight=self.lap_kernel4,
|
||||
padding=1,
|
||||
)
|
||||
tgt_lap = F.conv2d(
|
||||
tgt.permute(2, 0, 1)[:, None],
|
||||
weight=self.lap_kernel4,
|
||||
padding=1,
|
||||
)
|
||||
lap = (
|
||||
torch.where(src_lap.abs() > tgt_lap.abs(), src_lap, tgt_lap)
|
||||
.sum(1)
|
||||
.permute(1, 2, 0)
|
||||
)
|
||||
elif grad_mode == "avg":
|
||||
src_lap = F.conv2d(
|
||||
src.permute(2, 0, 1)[:, None],
|
||||
weight=self.lap_kernel4,
|
||||
padding=1,
|
||||
)
|
||||
tgt_lap = F.conv2d(
|
||||
tgt.permute(2, 0, 1)[:, None],
|
||||
weight=self.lap_kernel4,
|
||||
padding=1,
|
||||
)
|
||||
lap = (src_lap + tgt_lap).mul(0.5).sum(1).permute(1, 2, 0)
|
||||
|
||||
fq_star = F.conv2d(
|
||||
tgt_masked.permute(2, 0, 1)[:, None],
|
||||
weight=self.neighbor_kernel,
|
||||
padding=1,
|
||||
)[:, 0].permute(1, 2, 0)
|
||||
|
||||
A = torch.zeros(N + 1, 4, device=self.device, dtype=torch.long)
|
||||
X = torch.zeros(N + 1, 3, device=self.device, dtype=torch.float32)
|
||||
B = torch.zeros(N + 1, 3, device=self.device, dtype=torch.float32)
|
||||
|
||||
A[1:] = torch.stack(
|
||||
[
|
||||
index_map[x - 1, y],
|
||||
index_map[x + 1, y],
|
||||
index_map[x, y - 1],
|
||||
index_map[x, y + 1],
|
||||
],
|
||||
dim=-1,
|
||||
)
|
||||
X[1:] = tgt[x, y]
|
||||
B[1:] = lap[x, y] + fq_star[x, y]
|
||||
|
||||
X_flatten = X.flatten()
|
||||
B_flatten = B.flatten()
|
||||
A_flatten = torch.stack([3 * A, 3 * A + 1, 3 * A + 2], dim=1).reshape(-1, 4)
|
||||
|
||||
buffer = torch.zeros_like(X_flatten)
|
||||
|
||||
self.pb_solver.solve(num_iters, A_flatten, X_flatten, B_flatten, buffer)
|
||||
|
||||
if inplace:
|
||||
tgt[x, y] = X_flatten.view(-1, 3)[1:].clamp(0.0, 1.0)
|
||||
else:
|
||||
tgt = tgt.clone()
|
||||
tgt[x, y] = X_flatten.view(-1, 3)[1:].clamp(0.0, 1.0)
|
||||
|
||||
return tgt
|
||||
@@ -0,0 +1,223 @@
|
||||
import math
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import trimesh
|
||||
from PIL import Image
|
||||
from torch import BoolTensor, FloatTensor
|
||||
|
||||
from .utils import LIST_TYPE
|
||||
|
||||
|
||||
def list_to_pt(
|
||||
x: LIST_TYPE, dtype: Optional[torch.dtype] = None, device: Optional[str] = None
|
||||
) -> torch.Tensor:
|
||||
if isinstance(x, list) or isinstance(x, np.ndarray):
|
||||
return torch.tensor(x, dtype=dtype, device=device)
|
||||
return x.to(dtype=dtype)
|
||||
|
||||
|
||||
def get_c2w(
|
||||
elevation_deg: LIST_TYPE,
|
||||
distance: LIST_TYPE,
|
||||
azimuth_deg: Optional[LIST_TYPE],
|
||||
num_views: Optional[int] = 1,
|
||||
device: Optional[str] = None,
|
||||
) -> torch.FloatTensor:
|
||||
if azimuth_deg is None:
|
||||
assert (
|
||||
num_views is not None
|
||||
), "num_views must be provided if azimuth_deg is None."
|
||||
azimuth_deg = torch.linspace(
|
||||
0, 360, num_views + 1, dtype=torch.float32, device=device
|
||||
)[:-1]
|
||||
else:
|
||||
num_views = len(azimuth_deg)
|
||||
azimuth_deg = list_to_pt(azimuth_deg, dtype=torch.float32, device=device)
|
||||
elevation_deg = list_to_pt(elevation_deg, dtype=torch.float32, device=device)
|
||||
camera_distances = list_to_pt(distance, dtype=torch.float32, device=device)
|
||||
elevation = elevation_deg * math.pi / 180
|
||||
azimuth = azimuth_deg * math.pi / 180
|
||||
camera_positions = torch.stack(
|
||||
[
|
||||
camera_distances * torch.cos(elevation) * torch.cos(azimuth),
|
||||
camera_distances * torch.cos(elevation) * torch.sin(azimuth),
|
||||
camera_distances * torch.sin(elevation),
|
||||
],
|
||||
dim=-1,
|
||||
)
|
||||
center = torch.zeros_like(camera_positions)
|
||||
up = torch.tensor([0, 0, 1], dtype=torch.float32, device=device)[None, :].repeat(
|
||||
num_views, 1
|
||||
)
|
||||
lookat = F.normalize(center - camera_positions, dim=-1)
|
||||
right = F.normalize(torch.cross(lookat, up, dim=-1), dim=-1)
|
||||
up = F.normalize(torch.cross(right, lookat, dim=-1), dim=-1)
|
||||
c2w3x4 = torch.cat(
|
||||
[torch.stack([right, up, -lookat], dim=-1), camera_positions[:, :, None]],
|
||||
dim=-1,
|
||||
)
|
||||
c2w = torch.cat([c2w3x4, torch.zeros_like(c2w3x4[:, :1])], dim=1)
|
||||
c2w[:, 3, 3] = 1.0
|
||||
return c2w
|
||||
|
||||
|
||||
def get_projection_matrix(
|
||||
fovy_deg: LIST_TYPE,
|
||||
aspect_wh: float = 1.0,
|
||||
near: float = 0.1,
|
||||
far: float = 100.0,
|
||||
device: Optional[str] = None,
|
||||
) -> torch.FloatTensor:
|
||||
fovy_deg = list_to_pt(fovy_deg, dtype=torch.float32, device=device)
|
||||
batch_size = fovy_deg.shape[0]
|
||||
fovy = fovy_deg * math.pi / 180
|
||||
tan_half_fovy = torch.tan(fovy / 2)
|
||||
projection_matrix = torch.zeros(
|
||||
batch_size, 4, 4, dtype=torch.float32, device=device
|
||||
)
|
||||
projection_matrix[:, 0, 0] = 1 / (aspect_wh * tan_half_fovy)
|
||||
projection_matrix[:, 1, 1] = -1 / tan_half_fovy
|
||||
projection_matrix[:, 2, 2] = -(far + near) / (far - near)
|
||||
projection_matrix[:, 2, 3] = -2 * far * near / (far - near)
|
||||
projection_matrix[:, 3, 2] = -1
|
||||
return projection_matrix
|
||||
|
||||
|
||||
def get_orthogonal_projection_matrix(
|
||||
batch_size: int,
|
||||
left: float,
|
||||
right: float,
|
||||
bottom: float,
|
||||
top: float,
|
||||
near: float = 0.1,
|
||||
far: float = 100.0,
|
||||
device: Optional[str] = None,
|
||||
) -> torch.FloatTensor:
|
||||
projection_matrix = torch.zeros(
|
||||
batch_size, 4, 4, dtype=torch.float32, device=device
|
||||
)
|
||||
projection_matrix[:, 0, 0] = 2 / (right - left)
|
||||
projection_matrix[:, 1, 1] = -2 / (top - bottom)
|
||||
projection_matrix[:, 2, 2] = -2 / (far - near)
|
||||
projection_matrix[:, 0, 3] = -(right + left) / (right - left)
|
||||
projection_matrix[:, 1, 3] = -(top + bottom) / (top - bottom)
|
||||
projection_matrix[:, 2, 3] = -(far + near) / (far - near)
|
||||
projection_matrix[:, 3, 3] = 1
|
||||
return projection_matrix
|
||||
|
||||
|
||||
@dataclass
|
||||
class Camera:
|
||||
c2w: Optional[torch.FloatTensor]
|
||||
w2c: torch.FloatTensor
|
||||
proj_mtx: torch.FloatTensor
|
||||
mvp_mtx: torch.FloatTensor
|
||||
cam_pos: Optional[torch.FloatTensor]
|
||||
|
||||
def __getitem__(self, index):
|
||||
if isinstance(index, int):
|
||||
sl = slice(index, index + 1)
|
||||
elif isinstance(index, slice):
|
||||
sl = index
|
||||
elif isinstance(index, list):
|
||||
sl = index
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
return Camera(
|
||||
c2w=self.c2w[sl] if self.c2w is not None else None,
|
||||
w2c=self.w2c[sl],
|
||||
proj_mtx=self.proj_mtx[sl],
|
||||
mvp_mtx=self.mvp_mtx[sl],
|
||||
cam_pos=self.cam_pos[sl] if self.cam_pos is not None else None,
|
||||
)
|
||||
|
||||
def to(self, device: Optional[str] = None):
|
||||
if self.c2w is not None:
|
||||
self.c2w = self.c2w.to(device)
|
||||
self.w2c = self.w2c.to(device)
|
||||
self.proj_mtx = self.proj_mtx.to(device)
|
||||
self.mvp_mtx = self.mvp_mtx.to(device)
|
||||
if self.cam_pos is not None:
|
||||
self.cam_pos = self.cam_pos.to(device)
|
||||
|
||||
def __len__(self):
|
||||
return self.c2w.shape[0]
|
||||
|
||||
|
||||
def get_camera(
|
||||
elevation_deg: Optional[LIST_TYPE] = None,
|
||||
distance: Optional[LIST_TYPE] = None,
|
||||
fovy_deg: Optional[LIST_TYPE] = None,
|
||||
azimuth_deg: Optional[LIST_TYPE] = None,
|
||||
num_views: Optional[int] = 1,
|
||||
c2w: Optional[torch.FloatTensor] = None,
|
||||
w2c: Optional[torch.FloatTensor] = None,
|
||||
proj_mtx: Optional[torch.FloatTensor] = None,
|
||||
aspect_wh: float = 1.0,
|
||||
near: float = 0.1,
|
||||
far: float = 100.0,
|
||||
perturb_camera_position: Optional[float] = None,
|
||||
device: Optional[str] = None,
|
||||
):
|
||||
if w2c is None:
|
||||
if c2w is None:
|
||||
c2w = get_c2w(elevation_deg, distance, azimuth_deg, num_views, device)
|
||||
if perturb_camera_position is not None:
|
||||
perturbed_pos = (
|
||||
c2w[:, :3, 3]
|
||||
+ torch.randn_like(c2w[:, :3, 3]) * perturb_camera_position
|
||||
)
|
||||
perturbed_pos = (
|
||||
F.normalize(perturbed_pos, dim=-1)
|
||||
* ((c2w[:, :3, 3] ** 2).sum(-1) ** 0.5)[:, None]
|
||||
)
|
||||
camera_positions = c2w[:, :3, 3]
|
||||
w2c = torch.linalg.inv(c2w)
|
||||
else:
|
||||
camera_positions = None
|
||||
c2w = None
|
||||
if proj_mtx is None:
|
||||
proj_mtx = get_projection_matrix(
|
||||
fovy_deg, aspect_wh=aspect_wh, near=near, far=far, device=device
|
||||
)
|
||||
mvp_mtx = proj_mtx @ w2c
|
||||
return Camera(
|
||||
c2w=c2w, w2c=w2c, proj_mtx=proj_mtx, mvp_mtx=mvp_mtx, cam_pos=camera_positions
|
||||
)
|
||||
|
||||
|
||||
def get_orthogonal_camera(
|
||||
elevation_deg: LIST_TYPE,
|
||||
distance: LIST_TYPE,
|
||||
left: float,
|
||||
right: float,
|
||||
bottom: float,
|
||||
top: float,
|
||||
azimuth_deg: Optional[LIST_TYPE] = None,
|
||||
num_views: Optional[int] = 1,
|
||||
near: float = 0.1,
|
||||
far: float = 100.0,
|
||||
device: Optional[str] = None,
|
||||
):
|
||||
c2w = get_c2w(elevation_deg, distance, azimuth_deg, num_views, device)
|
||||
camera_positions = c2w[:, :3, 3]
|
||||
w2c = torch.linalg.inv(c2w)
|
||||
proj_mtx = get_orthogonal_projection_matrix(
|
||||
batch_size=c2w.shape[0],
|
||||
left=left,
|
||||
right=right,
|
||||
bottom=bottom,
|
||||
top=top,
|
||||
near=near,
|
||||
far=far,
|
||||
device=device,
|
||||
)
|
||||
mvp_mtx = proj_mtx @ w2c
|
||||
return Camera(
|
||||
c2w=c2w, w2c=w2c, proj_mtx=proj_mtx, mvp_mtx=mvp_mtx, cam_pos=camera_positions
|
||||
)
|
||||
@@ -0,0 +1,191 @@
|
||||
from typing import Optional
|
||||
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
try:
|
||||
import cvcuda
|
||||
torch_to_cvc = lambda x, layout: cvcuda.as_tensor(x, layout)
|
||||
|
||||
cvc_to_torch = lambda x, device: torch.tensor(x.cuda(), device=device)
|
||||
except ImportError:
|
||||
cvcuda = None
|
||||
torch_to_cvc = None
|
||||
cvc_to_torch = None
|
||||
|
||||
|
||||
def inpaint_cvc(
|
||||
image: torch.Tensor,
|
||||
mask: torch.Tensor,
|
||||
padding_size: int,
|
||||
return_dtype: Optional[torch.dtype] = None,
|
||||
):
|
||||
input_dtype = image.dtype
|
||||
input_device = image.device
|
||||
|
||||
image = image.detach()
|
||||
mask = mask.detach()
|
||||
|
||||
if image.dtype != torch.uint8:
|
||||
image = (image * 255).to(torch.uint8)
|
||||
if mask.dtype != torch.uint8:
|
||||
mask = (mask * 255).to(torch.uint8)
|
||||
|
||||
image_cvc = torch_to_cvc(image, "HWC")
|
||||
mask_cvc = torch_to_cvc(mask, "HW")
|
||||
output_cvc = cvcuda.inpaint(image_cvc, mask_cvc, padding_size)
|
||||
output = cvc_to_torch(output_cvc, device=input_device)
|
||||
|
||||
if return_dtype == torch.uint8 or input_dtype == torch.uint8:
|
||||
return output
|
||||
return output.to(dtype=input_dtype) / 255.0
|
||||
|
||||
|
||||
def inpaint_torch(
|
||||
image: torch.Tensor,
|
||||
mask: torch.Tensor,
|
||||
padding_size: int,
|
||||
return_dtype: Optional[torch.dtype] = None,
|
||||
):
|
||||
"""
|
||||
Inpaint masked areas in an image using PyTorch operations.
|
||||
|
||||
Args:
|
||||
image: Input image tensor [H, W, C]
|
||||
mask: Binary mask tensor [H, W] where non-zero values indicate pixels to inpaint
|
||||
padding_size: Size of neighborhood to consider for inpainting
|
||||
return_dtype: Optional dtype for the output tensor
|
||||
|
||||
Returns:
|
||||
Inpainted image tensor
|
||||
"""
|
||||
input_dtype = image.dtype
|
||||
input_device = image.device
|
||||
|
||||
image = image.detach()
|
||||
mask = mask.detach()
|
||||
|
||||
if image.dtype != torch.uint8:
|
||||
image = (image * 255).to(torch.uint8)
|
||||
if mask.dtype != torch.uint8:
|
||||
mask = (mask * 255).to(torch.uint8)
|
||||
|
||||
# Convert to float for processing
|
||||
image_float = image.float() / 255.0
|
||||
mask_float = (mask > 0).float() # 1 for areas to inpaint, 0 for known areas
|
||||
|
||||
# Initialize output with original image
|
||||
output = image_float.clone()
|
||||
|
||||
# Create distance-based weights for neighborhood pixels
|
||||
kernel_size = min(2 * padding_size + 1, 31) # Limit kernel size to avoid memory issues
|
||||
y, x = torch.meshgrid(
|
||||
torch.arange(-(kernel_size // 2), kernel_size // 2 + 1, device=input_device),
|
||||
torch.arange(-(kernel_size // 2), kernel_size // 2 + 1, device=input_device),
|
||||
indexing="ij"
|
||||
)
|
||||
dist = torch.sqrt(x.float() ** 2 + y.float() ** 2)
|
||||
weights = 1.0 / (dist + 1e-6)
|
||||
weights = weights / weights.sum()
|
||||
weights_kernel = weights.unsqueeze(0).unsqueeze(0)
|
||||
|
||||
# For each channel, compute weighted average of non-masked pixels
|
||||
inpainted = []
|
||||
|
||||
for c in range(image_float.shape[-1]):
|
||||
channel = image_float[..., c]
|
||||
|
||||
# Compute weighted sum of valid pixels
|
||||
valid_values = channel * (1 - mask_float)
|
||||
weighted_sum = F.conv2d(
|
||||
valid_values.unsqueeze(0).unsqueeze(0),
|
||||
weights_kernel,
|
||||
padding=kernel_size // 2
|
||||
).squeeze(0).squeeze(0)
|
||||
|
||||
# Compute sum of weights for valid pixels
|
||||
valid_weights = F.conv2d(
|
||||
(1 - mask_float).unsqueeze(0).unsqueeze(0),
|
||||
weights_kernel,
|
||||
padding=kernel_size // 2
|
||||
).squeeze(0).squeeze(0)
|
||||
|
||||
# Avoid division by zero
|
||||
valid_weights = torch.clamp(valid_weights, min=1e-6)
|
||||
|
||||
# Compute weighted average
|
||||
inpainted_channel = weighted_sum / valid_weights
|
||||
|
||||
# Only update masked pixels
|
||||
inpainted_channel = torch.where(mask_float > 0, inpainted_channel, channel)
|
||||
inpainted.append(inpainted_channel)
|
||||
|
||||
# Combine channels
|
||||
output = torch.stack(inpainted, dim=-1)
|
||||
|
||||
# Convert back to appropriate dtype
|
||||
if return_dtype == torch.uint8 or input_dtype == torch.uint8:
|
||||
output = (output * 255).to(torch.uint8)
|
||||
else:
|
||||
output = output.to(dtype=input_dtype)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
def batch_inpaint_cvc(
|
||||
images: torch.Tensor,
|
||||
masks: torch.Tensor,
|
||||
padding_size: int,
|
||||
return_dtype: Optional[torch.dtype] = None,
|
||||
):
|
||||
output = torch.stack(
|
||||
[
|
||||
inpaint_cvc(image, mask, padding_size, return_dtype)
|
||||
for (image, mask) in zip(images, masks)
|
||||
],
|
||||
axis=0,
|
||||
)
|
||||
return output
|
||||
|
||||
|
||||
def batch_erode(
|
||||
masks: torch.Tensor, kernel_size: int, return_dtype: Optional[torch.dtype] = None
|
||||
):
|
||||
input_dtype = masks.dtype
|
||||
input_device = masks.device
|
||||
masks = masks.detach()
|
||||
if masks.dtype != torch.uint8:
|
||||
masks = (masks.float() * 255).to(torch.uint8)
|
||||
masks_cvc = torch_to_cvc(masks[..., None], "NHWC")
|
||||
masks_erode_cvc = cvcuda.morphology(
|
||||
masks_cvc,
|
||||
cvcuda.MorphologyType.ERODE,
|
||||
maskSize=(kernel_size, kernel_size),
|
||||
anchor=(-1, -1),
|
||||
)
|
||||
masks_erode = cvc_to_torch(masks_erode_cvc, device=input_device)[..., 0]
|
||||
if return_dtype == torch.uint8 or input_dtype == torch.uint8:
|
||||
return masks_erode
|
||||
return (masks_erode > 0).to(dtype=input_dtype)
|
||||
|
||||
|
||||
def batch_dilate(
|
||||
masks: torch.Tensor, kernel_size: int, return_dtype: Optional[torch.dtype] = None
|
||||
):
|
||||
input_dtype = masks.dtype
|
||||
input_device = masks.device
|
||||
masks = masks.detach()
|
||||
if masks.dtype != torch.uint8:
|
||||
masks = (masks.float() * 255).to(torch.uint8)
|
||||
masks_cvc = torch_to_cvc(masks[..., None], "NHWC")
|
||||
masks_dilate_cvc = cvcuda.morphology(
|
||||
masks_cvc,
|
||||
cvcuda.MorphologyType.DILATE,
|
||||
maskSize=(kernel_size, kernel_size),
|
||||
anchor=(-1, -1),
|
||||
)
|
||||
masks_dilate = cvc_to_torch(masks_dilate_cvc, device=input_device)[..., 0]
|
||||
if return_dtype == torch.uint8 or input_dtype == torch.uint8:
|
||||
return masks_dilate
|
||||
return (masks_dilate > 0).to(dtype=input_dtype)
|
||||
@@ -0,0 +1,522 @@
|
||||
import io
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import nvdiffrast.torch as dr
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import trimesh
|
||||
from PIL import Image
|
||||
from torch import BoolTensor, FloatTensor
|
||||
|
||||
from .utils import tensor_to_image
|
||||
|
||||
|
||||
def dot(x: torch.FloatTensor, y: torch.FloatTensor) -> torch.FloatTensor:
|
||||
return torch.sum(x * y, -1, keepdim=True)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TexturedMesh:
|
||||
v_pos: torch.FloatTensor
|
||||
t_pos_idx: torch.LongTensor
|
||||
|
||||
# texture coordinates
|
||||
v_tex: Optional[torch.FloatTensor] = None
|
||||
t_tex_idx: Optional[torch.LongTensor] = None
|
||||
|
||||
# texture map
|
||||
texture: Optional[torch.FloatTensor] = None
|
||||
|
||||
# vertices, faces after vertex merging
|
||||
_stitched_v_pos: Optional[torch.FloatTensor] = None
|
||||
_stitched_t_pos_idx: Optional[torch.LongTensor] = None
|
||||
|
||||
_v_nrm: Optional[torch.FloatTensor] = None
|
||||
_v_tang: Optional[torch.FloatTensor] = None
|
||||
|
||||
@property
|
||||
def v_nrm(self) -> torch.FloatTensor:
|
||||
if self._v_nrm is None:
|
||||
self._v_nrm = self._compute_vertex_normal()
|
||||
return self._v_nrm
|
||||
|
||||
@property
|
||||
def v_tang(self) -> torch.FloatTensor:
|
||||
if self._v_tang is None:
|
||||
self._v_tang = self._compute_tangent()
|
||||
return self._v_tang
|
||||
|
||||
def set_vertex_normal(self, v_nrm: torch.FloatTensor) -> None:
|
||||
assert v_nrm.shape == self.v_pos.shape
|
||||
self._v_nrm = v_nrm.to(self.v_pos)
|
||||
|
||||
def set_stitched_mesh(
|
||||
self, v_pos: torch.FloatTensor, t_pos_idx: torch.LongTensor
|
||||
) -> None:
|
||||
self._stitched_v_pos = v_pos
|
||||
self._stitched_t_pos_idx = t_pos_idx
|
||||
|
||||
@property
|
||||
def stitched_v_pos(self) -> torch.FloatTensor:
|
||||
if self._stitched_v_pos is None:
|
||||
print("Warning: Stitched vertices not available, using original vertices!")
|
||||
return self.v_pos
|
||||
return self._stitched_v_pos
|
||||
|
||||
@property
|
||||
def stitched_t_pos_idx(self) -> torch.LongTensor:
|
||||
if self._stitched_t_pos_idx is None:
|
||||
print("Warning: Stitched faces not available, using original faces!")
|
||||
return self.t_pos_idx
|
||||
return self._stitched_t_pos_idx
|
||||
|
||||
@property
|
||||
def uv_size(self) -> Optional[int]:
|
||||
if self.texture is None:
|
||||
return None
|
||||
return self.texture.shape[0]
|
||||
|
||||
def _compute_vertex_normal(self) -> torch.FloatTensor:
|
||||
if self._stitched_v_pos is None or self._stitched_t_pos_idx is None:
|
||||
print(
|
||||
"Warning: Stitched vertices and faces not available, computing vertex normals on original mesh, which can be erroneous!"
|
||||
)
|
||||
v_pos, t_pos_idx = self.v_pos, self.t_pos_idx
|
||||
else:
|
||||
v_pos, t_pos_idx = self._stitched_v_pos, self._stitched_t_pos_idx
|
||||
|
||||
i0 = t_pos_idx[:, 0]
|
||||
i1 = t_pos_idx[:, 1]
|
||||
i2 = t_pos_idx[:, 2]
|
||||
|
||||
v0 = v_pos[i0, :]
|
||||
v1 = v_pos[i1, :]
|
||||
v2 = v_pos[i2, :]
|
||||
|
||||
face_normals = torch.cross(v1 - v0, v2 - v0)
|
||||
|
||||
# Splat face normals to vertices
|
||||
v_nrm = torch.zeros_like(v_pos)
|
||||
v_nrm.scatter_add_(0, i0[:, None].repeat(1, 3), face_normals)
|
||||
v_nrm.scatter_add_(0, i1[:, None].repeat(1, 3), face_normals)
|
||||
v_nrm.scatter_add_(0, i2[:, None].repeat(1, 3), face_normals)
|
||||
|
||||
# Normalize, replace zero (degenerated) normals with some default value
|
||||
v_nrm = torch.where(
|
||||
dot(v_nrm, v_nrm) > 1e-20, v_nrm, torch.as_tensor([0.0, 0.0, 1.0]).to(v_nrm)
|
||||
)
|
||||
v_nrm = F.normalize(v_nrm, dim=1)
|
||||
|
||||
if torch.is_anomaly_enabled():
|
||||
assert torch.all(torch.isfinite(v_nrm))
|
||||
|
||||
return v_nrm
|
||||
|
||||
def _compute_tangent(self) -> torch.FloatTensor:
|
||||
vn_idx = [None] * 3
|
||||
pos = [None] * 3
|
||||
tex = [None] * 3
|
||||
for i in range(0, 3):
|
||||
pos[i] = self.v_pos[self.t_pos_idx[:, i]]
|
||||
tex[i] = self.v_tex[self.t_tex_idx[:, i]]
|
||||
vn_idx[i] = self.t_pos_idx[:, i]
|
||||
|
||||
tangents = torch.zeros_like(self._v_nrm)
|
||||
tansum = torch.zeros_like(self._v_nrm)
|
||||
|
||||
# Compute tangent space for each triangle
|
||||
uve1 = tex[1] - tex[0]
|
||||
uve2 = tex[2] - tex[0]
|
||||
pe1 = pos[1] - pos[0]
|
||||
pe2 = pos[2] - pos[0]
|
||||
|
||||
nom = pe1 * uve2[..., 1:2] - pe2 * uve1[..., 1:2]
|
||||
denom = uve1[..., 0:1] * uve2[..., 1:2] - uve1[..., 1:2] * uve2[..., 0:1]
|
||||
|
||||
# Avoid division by zero for degenerated texture coordinates
|
||||
tang = nom / torch.where(
|
||||
denom > 0.0, torch.clamp(denom, min=1e-6), torch.clamp(denom, max=-1e-6)
|
||||
)
|
||||
|
||||
# Update all 3 vertices
|
||||
for i in range(0, 3):
|
||||
idx = vn_idx[i][:, None].repeat(1, 3)
|
||||
tangents.scatter_add_(0, idx, tang) # tangents[n_i] = tangents[n_i] + tang
|
||||
tansum.scatter_add_(
|
||||
0, idx, torch.ones_like(tang)
|
||||
) # tansum[n_i] = tansum[n_i] + 1
|
||||
tangents = tangents / tansum
|
||||
|
||||
# Normalize and make sure tangent is perpendicular to normal
|
||||
tangents = F.normalize(tangents, dim=1)
|
||||
tangents = F.normalize(
|
||||
tangents - dot(tangents, self._v_nrm) * self._v_nrm, dim=1
|
||||
)
|
||||
|
||||
v_tng = tangents
|
||||
|
||||
if torch.is_anomaly_enabled():
|
||||
assert torch.all(torch.isfinite(v_tng))
|
||||
|
||||
return v_tng
|
||||
|
||||
def to(self, device: Optional[str] = None):
|
||||
self.v_pos = self.v_pos.to(device)
|
||||
self.t_pos_idx = self.t_pos_idx.to(device)
|
||||
if self.v_tex is not None:
|
||||
self.v_tex = self.v_tex.to(device)
|
||||
if self.t_tex_idx is not None:
|
||||
self.t_tex_idx = self.t_tex_idx.to(device)
|
||||
if self.texture is not None:
|
||||
self.texture = self.texture.to(device)
|
||||
if self._stitched_v_pos is not None:
|
||||
self._stitched_v_pos = self._stitched_v_pos.to(device)
|
||||
if self._stitched_t_pos_idx is not None:
|
||||
self._stitched_t_pos_idx = self._stitched_t_pos_idx.to(device)
|
||||
if self._v_nrm is not None:
|
||||
self._v_nrm = self._v_nrm.to(device)
|
||||
if self._v_tang is not None:
|
||||
self._v_tang = self._v_tang.to(device)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def mesh_use_texture(mesh: TexturedMesh, texture: torch.FloatTensor):
|
||||
texture_ = mesh.texture
|
||||
mesh.texture = texture
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
mesh.texture = texture_
|
||||
|
||||
|
||||
def load_mesh(
|
||||
mesh_path: str,
|
||||
rescale: bool = False,
|
||||
move_to_center: bool = False,
|
||||
scale: float = 0.5,
|
||||
flip_uv: bool = True,
|
||||
merge_vertices: bool = True,
|
||||
default_uv_size: Optional[int] = None,
|
||||
shape_init_mesh_up: str = "+y",
|
||||
shape_init_mesh_front: str = "+x",
|
||||
front_x_to_y: bool = False,
|
||||
device: Optional[str] = None,
|
||||
return_transform: bool = False,
|
||||
) -> TexturedMesh:
|
||||
if mesh_path.endswith(".npz"):
|
||||
|
||||
class Mesh:
|
||||
vertices = None
|
||||
faces = None
|
||||
|
||||
data = np.load(mesh_path)
|
||||
mesh = Mesh()
|
||||
mesh.vertices = data["vertices"]
|
||||
mesh.faces = data["faces"]
|
||||
merge_vertices = False
|
||||
else:
|
||||
scene = trimesh.load(mesh_path, force="mesh", process=False)
|
||||
if isinstance(scene, trimesh.Trimesh):
|
||||
mesh = scene
|
||||
elif isinstance(scene, trimesh.scene.Scene):
|
||||
mesh = trimesh.Trimesh()
|
||||
for obj in scene.geometry.values():
|
||||
mesh = trimesh.util.concatenate([mesh, obj])
|
||||
else:
|
||||
raise ValueError(f"Unknown mesh type at {mesh_path}.")
|
||||
|
||||
vertex_normals = getattr(mesh, "vertex_normals", None)
|
||||
|
||||
# move to center
|
||||
transform_offset = None
|
||||
if move_to_center:
|
||||
centroid = mesh.vertices.mean(0)
|
||||
mesh.vertices = mesh.vertices - centroid
|
||||
transform_offset = centroid # record the offset
|
||||
|
||||
# rescale
|
||||
transform_scale = None
|
||||
if rescale:
|
||||
max_scale = np.abs(mesh.vertices).max()
|
||||
mesh.vertices = mesh.vertices / max_scale * scale
|
||||
transform_scale = max_scale / scale
|
||||
|
||||
dirs = ["+x", "+y", "+z", "-x", "-y", "-z"]
|
||||
dir2vec = {
|
||||
"+x": np.array([1, 0, 0]),
|
||||
"+y": np.array([0, 1, 0]),
|
||||
"+z": np.array([0, 0, 1]),
|
||||
"-x": np.array([-1, 0, 0]),
|
||||
"-y": np.array([0, -1, 0]),
|
||||
"-z": np.array([0, 0, -1]),
|
||||
}
|
||||
if shape_init_mesh_up not in dirs or shape_init_mesh_front not in dirs:
|
||||
raise ValueError(
|
||||
f"shape_init_mesh_up and shape_init_mesh_front must be one of {dirs}."
|
||||
)
|
||||
if shape_init_mesh_up[1] == shape_init_mesh_front[1]:
|
||||
raise ValueError(
|
||||
"shape_init_mesh_up and shape_init_mesh_front must be orthogonal."
|
||||
)
|
||||
z_, x_ = (
|
||||
dir2vec[shape_init_mesh_up],
|
||||
dir2vec[shape_init_mesh_front],
|
||||
)
|
||||
y_ = np.cross(z_, x_)
|
||||
std2mesh = np.stack([x_, y_, z_], axis=0).T
|
||||
mesh2std = np.linalg.inv(std2mesh)
|
||||
mesh.vertices = np.dot(mesh2std, mesh.vertices.T).T
|
||||
if vertex_normals is not None:
|
||||
vertex_normals = np.dot(mesh2std, vertex_normals.T).T
|
||||
if front_x_to_y:
|
||||
x = mesh.vertices[:, 1].copy()
|
||||
y = -mesh.vertices[:, 0].copy()
|
||||
mesh.vertices[:, 0] = x
|
||||
mesh.vertices[:, 1] = y
|
||||
if vertex_normals is not None:
|
||||
vx = vertex_normals[:, 1].copy()
|
||||
vy = -vertex_normals[:, 0].copy()
|
||||
vertex_normals[:, 0] = vx
|
||||
vertex_normals[:, 1] = vy
|
||||
|
||||
v_pos = torch.tensor(mesh.vertices, dtype=torch.float32)
|
||||
t_pos_idx = torch.tensor(mesh.faces, dtype=torch.int64)
|
||||
|
||||
if hasattr(mesh, "visual") and hasattr(mesh.visual, "uv"):
|
||||
v_tex = torch.tensor(mesh.visual.uv, dtype=torch.float32)
|
||||
if flip_uv:
|
||||
v_tex[:, 1] = 1.0 - v_tex[:, 1]
|
||||
t_tex_idx = t_pos_idx.clone()
|
||||
if (
|
||||
default_uv_size is not None
|
||||
or getattr(mesh.visual.material, "baseColorTexture", None) is None
|
||||
):
|
||||
assert default_uv_size is not None
|
||||
texture = torch.zeros(
|
||||
(default_uv_size, default_uv_size, 3), dtype=torch.float32
|
||||
)
|
||||
else:
|
||||
texture = torch.tensor(
|
||||
np.array(mesh.visual.material.baseColorTexture) / 255.0,
|
||||
dtype=torch.float32,
|
||||
)[..., :3]
|
||||
else:
|
||||
v_tex = None
|
||||
t_tex_idx = None
|
||||
texture = None
|
||||
|
||||
textured_mesh = TexturedMesh(
|
||||
v_pos=v_pos,
|
||||
t_pos_idx=t_pos_idx,
|
||||
v_tex=v_tex,
|
||||
t_tex_idx=t_tex_idx,
|
||||
texture=texture,
|
||||
)
|
||||
|
||||
if vertex_normals is not None:
|
||||
v_nrm = F.normalize(torch.tensor(vertex_normals, dtype=torch.float32), dim=-1)
|
||||
textured_mesh.set_vertex_normal(v_nrm)
|
||||
|
||||
if merge_vertices and vertex_normals is None:
|
||||
# only merge vertices when vertex normals are not available
|
||||
mesh.merge_vertices(merge_tex=True)
|
||||
textured_mesh.set_stitched_mesh(
|
||||
torch.tensor(mesh.vertices, dtype=torch.float32),
|
||||
torch.tensor(mesh.faces, dtype=torch.int64),
|
||||
)
|
||||
else:
|
||||
textured_mesh.set_stitched_mesh(textured_mesh.v_pos, textured_mesh.t_pos_idx)
|
||||
|
||||
textured_mesh.to(device)
|
||||
|
||||
if return_transform:
|
||||
return textured_mesh, transform_offset, transform_scale
|
||||
else:
|
||||
return textured_mesh
|
||||
|
||||
|
||||
def replace_mesh_texture_and_save_trimesh(
|
||||
input_path: str,
|
||||
output_path: str,
|
||||
texture: Union[Image.Image, np.ndarray, torch.Tensor],
|
||||
metallic_roughness_texture: Optional[
|
||||
Union[Image.Image, np.ndarray, torch.Tensor]
|
||||
] = None,
|
||||
normal_texture: Optional[Union[Image.Image, np.ndarray, torch.Tensor]] = None,
|
||||
texture_format: str = "JPEG",
|
||||
task_id: str = "",
|
||||
**kwargs,
|
||||
) -> None:
|
||||
tmesh = trimesh.load(input_path, force="mesh", process=False)
|
||||
|
||||
def convert_image(tensor):
|
||||
texture = tensor_to_image(tensor)
|
||||
texture.format = texture_format
|
||||
return texture
|
||||
|
||||
texture = convert_image(texture)
|
||||
if metallic_roughness_texture is not None:
|
||||
metallic_roughness_texture = convert_image(metallic_roughness_texture)
|
||||
|
||||
if normal_texture is not None:
|
||||
normal_texture = convert_image(normal_texture)
|
||||
|
||||
if not isinstance(tmesh.visual.material, trimesh.visual.material.PBRMaterial):
|
||||
material = trimesh.visual.texture.PBRMaterial(
|
||||
baseColorFactor=[1.0, 1.0, 1.0, 1.0],
|
||||
roughnessFactor=0.9 if metallic_roughness_texture is None else 1.0,
|
||||
metallicFactor=0.0 if metallic_roughness_texture is None else 1.0,
|
||||
baseColorTexture=texture,
|
||||
metallicRoughnessTexture=metallic_roughness_texture,
|
||||
normalTexture=normal_texture,
|
||||
)
|
||||
tmesh.visual = trimesh.visual.TextureVisuals(
|
||||
uv=tmesh.visual.uv, image=texture, material=material
|
||||
)
|
||||
else:
|
||||
tmesh.visual.material.baseColorTexture = texture
|
||||
tmesh.visual.material.baseColorFactor = [1.0, 1.0, 1.0, 1.0]
|
||||
if metallic_roughness_texture is None:
|
||||
tmesh.visual.material.roughnessFactor = 0.9
|
||||
tmesh.visual.material.metallicFactor = 0.0
|
||||
else:
|
||||
tmesh.visual.material.roughnessFactor = 1.0
|
||||
tmesh.visual.material.metallicFactor = 1.0
|
||||
tmesh.visual.material.metallicRoughnessTexture = metallic_roughness_texture
|
||||
tmesh.visual.material.normalTexture = normal_texture
|
||||
|
||||
def p_func(tree, task_id_in=task_id):
|
||||
if "nodes" in tree and tree["nodes"][0]["name"].find("world") >= 0:
|
||||
tree["nodes"].pop(0)
|
||||
tree["asset"]["generator"] = "https://github.com/huanngzh/MV-Adapter"
|
||||
change_dict = {
|
||||
"nodes": "node",
|
||||
"meshes": "mesh",
|
||||
"materials": "mat",
|
||||
"images": "image",
|
||||
}
|
||||
for k, v in change_dict.items():
|
||||
if k in tree:
|
||||
for index in range(len(tree[k])):
|
||||
tree[k][index]["name"] = f"mvadapter_{v}_{task_id_in}"
|
||||
|
||||
tmesh.export(output_path, tree_postprocessor=p_func)
|
||||
|
||||
|
||||
def replace_mesh_texture_and_save_gltflib(
|
||||
input_path: str,
|
||||
output_path: str,
|
||||
texture: Union[Image.Image, np.ndarray, torch.Tensor],
|
||||
metallic_roughness_texture: Optional[
|
||||
Union[Image.Image, np.ndarray, torch.Tensor]
|
||||
] = None,
|
||||
normal_texture: Optional[Union[Image.Image, np.ndarray, torch.Tensor]] = None,
|
||||
texture_format: str = "JPEG",
|
||||
normal_strength: float = 1.0,
|
||||
task_id: str = "",
|
||||
**kwargs,
|
||||
) -> None:
|
||||
from gltflib import GLTF
|
||||
from gltflib import Image as gltfImage
|
||||
from gltflib import NormalTextureInfo, Texture, TextureInfo
|
||||
|
||||
def convert_image(tensor):
|
||||
texture = tensor_to_image(tensor)
|
||||
texture.format = texture_format
|
||||
return texture
|
||||
|
||||
def add_texture(gltf: GLTF, image: Image.Image) -> int:
|
||||
jpeg_buffer = io.BytesIO()
|
||||
image.save(jpeg_buffer, format=texture_format)
|
||||
new_texture_data = bytearray(jpeg_buffer.getvalue())
|
||||
_, offset, bytelen = gltf._create_or_extend_glb_resource(new_texture_data)
|
||||
bufferView = gltf._create_embedded_image_buffer_view(offset, bytelen)
|
||||
image = gltfImage(mimeType="image/jpeg", bufferView=bufferView)
|
||||
if gltf.model.images:
|
||||
gltf.model.images.append(image)
|
||||
else:
|
||||
gltf.model.images = [image]
|
||||
texture = Texture(source=len(gltf.model.images) - 1)
|
||||
if gltf.model.textures:
|
||||
gltf.model.textures.append(texture)
|
||||
else:
|
||||
gltf.model.textures = [texture]
|
||||
return len(gltf.model.textures) - 1
|
||||
|
||||
gltf = GLTF.load(input_path)
|
||||
|
||||
texture = convert_image(texture)
|
||||
if metallic_roughness_texture is not None:
|
||||
metallic_roughness_texture = convert_image(metallic_roughness_texture)
|
||||
|
||||
if normal_texture is not None:
|
||||
normal_texture = convert_image(normal_texture)
|
||||
|
||||
pbr_material = gltf.model.materials[0].pbrMetallicRoughness
|
||||
|
||||
pbr_material.baseColorTexture = TextureInfo(index=add_texture(gltf, texture))
|
||||
pbr_material.baseColorFactor = [1, 1, 1, 1]
|
||||
|
||||
if metallic_roughness_texture is not None:
|
||||
pbr_material.metallicRoughnessTexture = TextureInfo(
|
||||
index=add_texture(gltf, metallic_roughness_texture)
|
||||
)
|
||||
pbr_material.metallicFactor = 1.0
|
||||
pbr_material.roughnessFactor = 1.0
|
||||
else:
|
||||
pbr_material.metallicFactor = 0.0
|
||||
pbr_material.roughnessFactor = 0.9
|
||||
|
||||
if normal_texture is not None:
|
||||
gltf.model.materials[0].normalTexture = NormalTextureInfo(
|
||||
index=add_texture(gltf, normal_texture), scale=normal_strength
|
||||
)
|
||||
|
||||
gltf.model.nodes[0].name = f"mvadapter_node_{task_id}"
|
||||
gltf.model.meshes[0].name = f"mvadapter_mesh_{task_id}"
|
||||
gltf.model.materials[0].name = f"mvadapter_material_{task_id}"
|
||||
gltf.model.asset.generator = "https://github.com/huanngzh/MV-Adapter"
|
||||
gltf.export(output_path)
|
||||
|
||||
|
||||
def replace_mesh_texture_and_save(
|
||||
input_path: str,
|
||||
output_path: str,
|
||||
texture: Union[Image.Image, np.ndarray, torch.Tensor],
|
||||
metallic_roughness_texture: Optional[
|
||||
Union[Image.Image, np.ndarray, torch.Tensor]
|
||||
] = None,
|
||||
normal_texture: Optional[Union[Image.Image, np.ndarray, torch.Tensor]] = None,
|
||||
normal_strength: float = 1.0,
|
||||
texture_format: str = "JPEG",
|
||||
task_id: str = "",
|
||||
backend: str = "trimesh",
|
||||
) -> None:
|
||||
if backend == "trimesh":
|
||||
replace_mesh_texture_and_save_trimesh(
|
||||
input_path,
|
||||
output_path,
|
||||
texture,
|
||||
metallic_roughness_texture=metallic_roughness_texture,
|
||||
normal_texture=normal_texture,
|
||||
texture_format=texture_format,
|
||||
task_id=task_id,
|
||||
)
|
||||
elif backend == "gltflib":
|
||||
replace_mesh_texture_and_save_gltflib(
|
||||
input_path,
|
||||
output_path,
|
||||
texture,
|
||||
metallic_roughness_texture=metallic_roughness_texture,
|
||||
normal_texture=normal_texture,
|
||||
normal_strength=normal_strength,
|
||||
task_id=task_id,
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
@@ -0,0 +1,348 @@
|
||||
import numpy as np
|
||||
import open3d as o3d
|
||||
import pymeshlab
|
||||
import torch
|
||||
import trimesh
|
||||
from pymeshlab import PercentageValue
|
||||
|
||||
|
||||
### Mesh Utils ###
|
||||
##### read mesh
|
||||
def read_mesh_from_path(mesh_path):
|
||||
ms = pymeshlab.MeshSet()
|
||||
ms.load_new_mesh(mesh_path)
|
||||
return ms
|
||||
|
||||
|
||||
def mesh_to_meshlab(vertices, faces):
|
||||
mesh = pymeshlab.Mesh(vertex_matrix=vertices, face_matrix=faces)
|
||||
ms = pymeshlab.MeshSet()
|
||||
ms.add_mesh(mesh)
|
||||
return ms
|
||||
|
||||
|
||||
def meshlab_to_mesh(ms):
|
||||
m = ms.current_mesh()
|
||||
return m.vertex_matrix(), m.face_matrix(), m.vertex_normal_matrix()
|
||||
|
||||
|
||||
##### decimation
|
||||
def decimate_quadric_edge_collapse_with_texture(
|
||||
ms, targetfacenum=None, preservenormal=True, verbose=False
|
||||
):
|
||||
# targetfacenum: int, Target number of faces.
|
||||
# preservenormal: bool, Preserve the normals of the original mesh.
|
||||
if verbose:
|
||||
print("Starting decimation ... ")
|
||||
m = ms.current_mesh()
|
||||
if targetfacenum is None:
|
||||
targetfacenum = int(m.face_number() * 0.5)
|
||||
if verbose:
|
||||
print("... Initial face number is %d ... " % m.face_number())
|
||||
ms.meshing_decimation_quadric_edge_collapse_with_texture(
|
||||
targetfacenum=targetfacenum, preservenormal=preservenormal
|
||||
)
|
||||
if verbose:
|
||||
print("... Decimated face number is %d ... " % m.face_number())
|
||||
print("Decimation done!\n ")
|
||||
|
||||
|
||||
def decimate_quadric_edge_collapse(
|
||||
ms, targetfacenum=None, preservenormal=True, verbose=False
|
||||
):
|
||||
# targetfacenum: int, Target number of faces.
|
||||
# preservenormal: bool, Preserve the normals of the original mesh.
|
||||
if verbose:
|
||||
print("Starting decimation ... ")
|
||||
m = ms.current_mesh()
|
||||
if targetfacenum is None:
|
||||
targetfacenum = int(m.face_number() * 0.5)
|
||||
if verbose:
|
||||
print("... Initial face number is %d ... " % m.face_number())
|
||||
ms.meshing_decimation_quadric_edge_collapse(
|
||||
targetfacenum=targetfacenum, preservenormal=preservenormal
|
||||
)
|
||||
if verbose:
|
||||
print("... Decimated face number is %d ... " % m.face_number())
|
||||
print("Decimation done!\n ")
|
||||
|
||||
|
||||
##### vertex merge
|
||||
def merge_close_vertices(ms, threshold=0.0001, verbose=False):
|
||||
# threshold: float, Merge together all the vertices that are nearer than the specified threshold.
|
||||
if verbose:
|
||||
print("Starting merge vertices ... ")
|
||||
m = ms.current_mesh()
|
||||
if verbose:
|
||||
print("... Initial vertex number is %d ... " % m.vertex_number())
|
||||
ms.meshing_merge_close_vertices(threshold=PercentageValue(threshold * 100))
|
||||
if verbose:
|
||||
print("... Merged vertex number is %d ... " % m.vertex_number())
|
||||
print("Merge vertices done!\n ")
|
||||
|
||||
|
||||
##### Island Removal
|
||||
def remove_isolated_pieces(ms, mincomponentsize=25, diameter=None, verbose=False):
|
||||
# mincomponentsize: Delete isolated connected components composed by a limited number of triangles
|
||||
# diameter: Delete isolated connected components whose diameter is smaller than the specified constant
|
||||
if verbose:
|
||||
print("Starting remove isolated pieces ... ")
|
||||
m = ms.current_mesh()
|
||||
if verbose:
|
||||
print("... Initial face number is %d ... " % m.face_number())
|
||||
if diameter is None:
|
||||
ms.meshing_remove_connected_component_by_face_number(
|
||||
mincomponentsize=mincomponentsize, removeunref=True
|
||||
)
|
||||
else:
|
||||
ms.meshing_remove_connected_component_by_diameter(
|
||||
mincomponentdiag=PercentageValue(diameter), removeunref=True
|
||||
)
|
||||
if verbose:
|
||||
print("... Isolated removed face number is %d ... " % m.face_number())
|
||||
print("Remove isolated pieces done!\n ")
|
||||
|
||||
|
||||
##### hole filling
|
||||
def fix_hole(ms, maxholesize=30, verbose=False):
|
||||
# maxholesize: int, Maximum size of the hole to be filled.
|
||||
if verbose:
|
||||
print("Starting fix holes ... ")
|
||||
m = ms.current_mesh()
|
||||
if verbose:
|
||||
print("... Initial face number is %d ... " % m.face_number())
|
||||
ms.meshing_close_holes(maxholesize=maxholesize)
|
||||
if verbose:
|
||||
print("... Fixed hole face number is %d ... " % m.face_number())
|
||||
print("Fix holes done!\n ")
|
||||
|
||||
|
||||
##### repair non manifold edges
|
||||
def repair_non_manifold(ms, verbose=False):
|
||||
if verbose:
|
||||
print("Starting repair non manifold edges ... ")
|
||||
m = ms.current_mesh()
|
||||
if verbose:
|
||||
print("... Initial face number is %d ... " % m.face_number())
|
||||
ms.meshing_repair_non_manifold_edges()
|
||||
ms.meshing_repair_non_manifold_vertices(vertdispratio=0.1)
|
||||
ms.meshing_remove_duplicate_faces()
|
||||
if verbose:
|
||||
print("... Fixed non manifold edges face number is %d ... " % m.face_number())
|
||||
print("Repair non manifold edges done!\n ")
|
||||
|
||||
|
||||
##### laplacian_smooth
|
||||
def laplacian_smooth(ms, stepsmoothnum=3, verbose=False):
|
||||
# stepsmoothnum: int, Number of smoothing steps to be performed
|
||||
if verbose:
|
||||
print("Starting laplacian smooth ... ")
|
||||
m = ms.current_mesh()
|
||||
ms.apply_coord_laplacian_smoothing(stepsmoothnum=stepsmoothnum)
|
||||
if verbose:
|
||||
print("Laplacian smooth done!\n ")
|
||||
|
||||
|
||||
##### taubin_smooth
|
||||
def taubin_smooth(ms, stepsmoothnum=3, verbose=False):
|
||||
if verbose:
|
||||
print("Starting Taubin smooth ... ")
|
||||
m = ms.current_mesh()
|
||||
ms.apply_coord_taubin_smoothing(stepsmoothnum=stepsmoothnum)
|
||||
if verbose:
|
||||
print("Taubin smooth done!\n ")
|
||||
|
||||
|
||||
##### compute_normal
|
||||
def compute_normal(ms, weightmode="Simple Average", verbose=False):
|
||||
if verbose:
|
||||
print("Starting compute_normal_per_vertex ... ")
|
||||
m = ms.current_mesh()
|
||||
ms.compute_normal_per_vertex(weightmode=weightmode)
|
||||
if verbose:
|
||||
print("compute_normal_per_vertex done!\n ")
|
||||
|
||||
|
||||
### Pre-process Mesh ###
|
||||
def process_mesh(
|
||||
vertices,
|
||||
faces,
|
||||
threshold=0.0001,
|
||||
mincomponentRatio=0.02,
|
||||
targetfacenum=50000,
|
||||
maxholesize=30,
|
||||
stepsmoothnum=10,
|
||||
verbose=False,
|
||||
):
|
||||
ms = mesh_to_meshlab(vertices, faces)
|
||||
|
||||
### Vertex Merge
|
||||
merge_close_vertices(ms, threshold=threshold, verbose=verbose)
|
||||
|
||||
### Island Removal
|
||||
faces = ms.current_mesh().face_matrix()
|
||||
remove_isolated_pieces(
|
||||
ms, mincomponentsize=int(len(faces) * mincomponentRatio), verbose=verbose
|
||||
)
|
||||
|
||||
### Hole Filling
|
||||
repair_non_manifold(ms) # repair before fix hole
|
||||
fix_hole(ms, maxholesize=maxholesize, verbose=verbose)
|
||||
|
||||
### Taubin Smoothing
|
||||
taubin_smooth(ms, stepsmoothnum=stepsmoothnum, verbose=verbose)
|
||||
|
||||
vertices, faces, _ = meshlab_to_mesh(ms)
|
||||
if faces.shape[0] > targetfacenum:
|
||||
device = o3d.core.Device("CPU:0")
|
||||
dtype_f = o3d.core.float32
|
||||
dtype_i = o3d.core.int64
|
||||
mesh = o3d.t.geometry.TriangleMesh(device)
|
||||
mesh.vertex.positions = o3d.core.Tensor(
|
||||
vertices.astype(np.float32), dtype_f, device
|
||||
)
|
||||
mesh.triangle.indices = o3d.core.Tensor(faces.astype(np.int64), dtype_i, device)
|
||||
simplified_mesh = mesh.simplify_quadric_decimation(
|
||||
target_reduction=1.0 - float(targetfacenum) / faces.shape[0]
|
||||
)
|
||||
ms.clear()
|
||||
vertices = simplified_mesh.vertex.positions.numpy()
|
||||
faces = simplified_mesh.triangle.indices.numpy()
|
||||
mesh = pymeshlab.Mesh(vertex_matrix=vertices, face_matrix=faces)
|
||||
ms.add_mesh(mesh)
|
||||
|
||||
### Mesh Simplification/Decimation
|
||||
# decimate_quadric_edge_collapse(ms, targetfacenum=targetfacenum, verbose=verbose)
|
||||
taubin_smooth(ms, stepsmoothnum=stepsmoothnum, verbose=verbose)
|
||||
repair_non_manifold(ms, verbose=verbose)
|
||||
compute_normal(ms, verbose=verbose)
|
||||
return meshlab_to_mesh(ms)
|
||||
|
||||
|
||||
### UV Un-Warp ###
|
||||
def uv_parameterize_uvatlas(
|
||||
vertices,
|
||||
faces,
|
||||
size=1024,
|
||||
gutter=2.5,
|
||||
max_stretch=0.1666666716337204,
|
||||
parallel_partitions=16,
|
||||
nthreads=0,
|
||||
):
|
||||
device = o3d.core.Device("CPU:0")
|
||||
dtype_f = o3d.core.float32
|
||||
dtype_i = o3d.core.int64
|
||||
|
||||
mesh = o3d.t.geometry.TriangleMesh(device)
|
||||
|
||||
mesh.vertex.positions = o3d.core.Tensor(
|
||||
vertices.astype(np.float32), dtype_f, device
|
||||
)
|
||||
mesh.triangle.indices = o3d.core.Tensor(faces.astype(np.int64), dtype_i, device)
|
||||
|
||||
mesh.compute_uvatlas(
|
||||
size=size,
|
||||
gutter=gutter,
|
||||
max_stretch=max_stretch,
|
||||
parallel_partitions=parallel_partitions,
|
||||
nthreads=nthreads,
|
||||
)
|
||||
|
||||
return mesh.triangle.texture_uvs.numpy() # (#F, 3, 2)
|
||||
|
||||
|
||||
### Pack All ###
|
||||
def process_raw(mesh_path, save_path, preprocess=True, device="cpu"):
|
||||
scene = trimesh.load(mesh_path, force="mesh", process=False)
|
||||
if isinstance(scene, trimesh.Trimesh):
|
||||
mesh = scene
|
||||
elif isinstance(scene, trimesh.scene.Scene):
|
||||
mesh = trimesh.Trimesh()
|
||||
for obj in scene.geometry.values():
|
||||
mesh = trimesh.util.concatenate([mesh, obj])
|
||||
else:
|
||||
raise ValueError(f"Unknown mesh type at {mesh_path}.")
|
||||
|
||||
vertices = mesh.vertices
|
||||
faces = mesh.faces
|
||||
|
||||
mesh_post_process_options = {
|
||||
"mincomponentRatio": 0.02,
|
||||
"targetfacenum": 50000,
|
||||
"maxholesize": 100,
|
||||
"stepsmoothnum": 10,
|
||||
"verbose": False,
|
||||
}
|
||||
|
||||
if preprocess:
|
||||
v_pos, t_pos_idx, normals = process_mesh(
|
||||
vertices=vertices,
|
||||
faces=faces,
|
||||
**mesh_post_process_options,
|
||||
)
|
||||
else:
|
||||
v_pos, t_pos_idx, normals = vertices, faces, mesh.vertex_normals
|
||||
|
||||
v_tex_np = (
|
||||
uv_parameterize_uvatlas(v_pos, t_pos_idx).reshape(-1, 2).astype(np.float32)
|
||||
)
|
||||
|
||||
v_pos = torch.from_numpy(v_pos).to(device=device, dtype=torch.float32)
|
||||
t_pos_idx = torch.from_numpy(t_pos_idx).to(device=device, dtype=torch.long)
|
||||
v_tex = torch.from_numpy(v_tex_np).to(device=device, dtype=torch.float32)
|
||||
normals = torch.from_numpy(normals).to(device=device, dtype=torch.float32)
|
||||
assert v_tex.shape[0] == t_pos_idx.shape[0] * 3
|
||||
t_tex_idx = torch.arange(
|
||||
t_pos_idx.shape[0] * 3,
|
||||
device=device,
|
||||
dtype=torch.long,
|
||||
).reshape(-1, 3)
|
||||
# uv, index = torch.unique(v_tex, dim=0, return_inverse=True) # 这样实现是2毫秒
|
||||
# super efficient de-duplication
|
||||
v_tex_u_uint32 = v_tex_np[..., 0].view(np.uint32)
|
||||
v_tex_v_uint32 = v_tex_np[..., 1].view(np.uint32)
|
||||
v_hashed = (v_tex_u_uint32.astype(np.uint64) << 32) | v_tex_v_uint32
|
||||
v_hashed = torch.from_numpy(v_hashed.view(np.int64)).to(v_pos.device)
|
||||
|
||||
t_pos_idx_f3 = torch.arange(
|
||||
t_pos_idx.shape[0] * 3, device=t_pos_idx.device, dtype=torch.long
|
||||
).reshape(-1, 3)
|
||||
v_pos_f3 = v_pos[t_pos_idx].reshape(-1, 3)
|
||||
normals_f3 = normals[t_pos_idx].reshape(-1, 3)
|
||||
|
||||
v_hashed_dedup, inverse_indices = torch.unique(v_hashed, return_inverse=True)
|
||||
dedup_size, full_size = v_hashed_dedup.shape[0], inverse_indices.shape[0]
|
||||
indices = torch.scatter_reduce(
|
||||
torch.full(
|
||||
[dedup_size],
|
||||
fill_value=full_size,
|
||||
device=inverse_indices.device,
|
||||
dtype=torch.long,
|
||||
),
|
||||
index=inverse_indices,
|
||||
src=torch.arange(full_size, device=inverse_indices.device, dtype=torch.int64),
|
||||
dim=0,
|
||||
reduce="amin",
|
||||
)
|
||||
v_tex = v_tex[indices]
|
||||
t_tex_idx = inverse_indices.reshape(-1, 3)
|
||||
|
||||
v_pos = v_pos_f3[indices]
|
||||
normals = normals_f3[indices]
|
||||
|
||||
normals = normals.to(dtype=torch.float32, device=device)
|
||||
|
||||
# either flip uv or flip texture
|
||||
# here we flip uv
|
||||
uv_to_save = v_tex.clone()
|
||||
uv_to_save[:, 1] = 1.0 - uv_to_save[:, 1]
|
||||
|
||||
visual = trimesh.visual.TextureVisuals(uv=uv_to_save.cpu().numpy())
|
||||
tmesh = trimesh.Trimesh(
|
||||
vertices=v_pos.cpu().numpy(),
|
||||
faces=t_tex_idx.cpu().numpy(),
|
||||
vertex_normals=normals.cpu().numpy(),
|
||||
visual=visual,
|
||||
process=False,
|
||||
)
|
||||
tmesh.export(save_path)
|
||||
@@ -0,0 +1,204 @@
|
||||
import glob
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
from einops import rearrange
|
||||
from PIL import Image
|
||||
|
||||
from .blend import PoissonBlendingSolver
|
||||
from .camera import Camera, get_camera, get_orthogonal_camera
|
||||
from .mesh import TexturedMesh, load_mesh, replace_mesh_texture_and_save
|
||||
from .render import NVDiffRastContextWrapper, SimpleNormalization, render
|
||||
from .seg import SegmentationModel
|
||||
from .utils import (
|
||||
IMAGE_TYPE,
|
||||
LIST_TYPE,
|
||||
image_to_tensor,
|
||||
make_image_grid,
|
||||
tensor_to_image,
|
||||
)
|
||||
from .uv import (
|
||||
ExponentialBlend,
|
||||
SimpleUVValidityStrategy,
|
||||
uv_blend,
|
||||
uv_precompute,
|
||||
uv_render_attr,
|
||||
uv_render_geometry,
|
||||
)
|
||||
from .warp import compute_warp_field
|
||||
|
||||
|
||||
@dataclass
|
||||
class CameraProjectionOutput:
|
||||
uv_proj: torch.FloatTensor
|
||||
uv_proj_mask: torch.BoolTensor
|
||||
uv_depth_grad: Optional[torch.FloatTensor]
|
||||
uv_aoi_cos: Optional[torch.FloatTensor]
|
||||
|
||||
|
||||
class CameraProjection:
|
||||
def __init__(
|
||||
self,
|
||||
pb_backend: str,
|
||||
bg_remover: Optional[SegmentationModel],
|
||||
device: str,
|
||||
context_type: str = "gl",
|
||||
) -> None:
|
||||
self.pb_solver = PoissonBlendingSolver(pb_backend, device)
|
||||
self.ctx = NVDiffRastContextWrapper(device, context_type)
|
||||
self.bg_remover = bg_remover
|
||||
self.device = device
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
images: IMAGE_TYPE,
|
||||
mesh: TexturedMesh,
|
||||
cam: Optional[Camera] = None,
|
||||
fovy_deg: Optional[LIST_TYPE] = None,
|
||||
masks: Optional[IMAGE_TYPE] = None,
|
||||
remove_bg: bool = False,
|
||||
c2w: Optional[torch.FloatTensor] = None,
|
||||
elevation_deg: Optional[LIST_TYPE] = None,
|
||||
distance: Optional[LIST_TYPE] = None,
|
||||
azimuth_deg: Optional[LIST_TYPE] = None,
|
||||
num_views: Optional[int] = None,
|
||||
uv_size: int = 2048,
|
||||
warp_images: bool = False,
|
||||
images_background: Optional[float] = None,
|
||||
iou_rejection_threshold: Optional[float] = 0.8,
|
||||
aoi_cos_valid_threshold: float = 0.3,
|
||||
depth_grad_dilation: int = 5,
|
||||
depth_grad_threshold: float = 0.1,
|
||||
uv_exp_blend_alpha: float = 6,
|
||||
uv_exp_blend_view_weight: Optional[torch.FloatTensor] = None,
|
||||
poisson_blending: bool = True,
|
||||
pb_num_iters: int = 1000,
|
||||
pb_keep_original_border: bool = True,
|
||||
from_scratch: bool = False,
|
||||
uv_padding: bool = True,
|
||||
return_uv_projection_mask: bool = False,
|
||||
return_dict: bool = False,
|
||||
) -> Optional[torch.FloatTensor]:
|
||||
images_pt = image_to_tensor(images, device=self.device)
|
||||
assert images_pt.ndim == 4
|
||||
Nv, H, W, _ = images_pt.shape
|
||||
|
||||
if masks is not None:
|
||||
masks_pt = image_to_tensor(masks, device=self.device)
|
||||
else:
|
||||
if remove_bg:
|
||||
assert self.bg_remover is not None
|
||||
masks_pt = self.bg_remover(images_pt)
|
||||
else:
|
||||
masks_pt = None
|
||||
|
||||
if masks_pt is not None and masks_pt.ndim == 4:
|
||||
masks_pt = masks_pt.mean(-1)
|
||||
|
||||
if cam is None:
|
||||
cam = get_camera(
|
||||
elevation_deg,
|
||||
distance,
|
||||
fovy_deg,
|
||||
azimuth_deg,
|
||||
num_views,
|
||||
c2w,
|
||||
aspect_wh=W / H,
|
||||
device=self.device,
|
||||
)
|
||||
uv_precompute_output = uv_precompute(
|
||||
self.ctx, mesh, height=uv_size, width=uv_size
|
||||
)
|
||||
uv_render_geometry_output = uv_render_geometry(
|
||||
self.ctx,
|
||||
mesh,
|
||||
cam,
|
||||
view_height=H,
|
||||
view_width=W,
|
||||
uv_precompute_output=uv_precompute_output,
|
||||
compute_depth_grad=True,
|
||||
depth_grad_dilation=depth_grad_dilation,
|
||||
)
|
||||
|
||||
# IoU rejection
|
||||
if masks_pt is not None and iou_rejection_threshold is not None:
|
||||
given_masks = (masks_pt > 0.5).float()
|
||||
render_masks = uv_render_geometry_output.view_mask.float()
|
||||
intersection = given_masks * render_masks
|
||||
union = given_masks + render_masks - intersection
|
||||
iou = intersection.sum((1, 2)) / union.sum((1, 2))
|
||||
iou_min = iou.min()
|
||||
print(f"Debug: Per view IoU: {iou.tolist()}")
|
||||
if iou_min < iou_rejection_threshold:
|
||||
print(
|
||||
f"Warning: Minimum view IoU {iou_min} below threshold {iou_rejection_threshold}, skipping camera projection!"
|
||||
)
|
||||
return None
|
||||
|
||||
if warp_images:
|
||||
# TODO: clean code
|
||||
assert images_background is not None
|
||||
render_attr = render(
|
||||
self.ctx,
|
||||
mesh,
|
||||
cam,
|
||||
height=H,
|
||||
width=W,
|
||||
render_attr=True,
|
||||
attr_background=images_background,
|
||||
).attr
|
||||
images_pt = compute_warp_field(
|
||||
self.ctx.ctx,
|
||||
images_pt,
|
||||
render_attr,
|
||||
n_grid=10,
|
||||
optim_res=[64, 128],
|
||||
optim_step_per_res=20,
|
||||
lambda_reg=2.0,
|
||||
temp_dir="debug_warp",
|
||||
verbose=False,
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
uv_render_attr_output = uv_render_attr(
|
||||
images=images_pt,
|
||||
masks=masks_pt,
|
||||
uv_render_geometry_output=uv_render_geometry_output,
|
||||
)
|
||||
uv_blend_output = uv_blend(
|
||||
uv_precompute_output,
|
||||
uv_render_geometry_output,
|
||||
uv_render_attr_output,
|
||||
uv_validity_strategy=SimpleUVValidityStrategy(
|
||||
aoi_cos_thresh=aoi_cos_valid_threshold,
|
||||
depth_grad_thresh=depth_grad_threshold,
|
||||
),
|
||||
uv_blend_weight_strategy=ExponentialBlend(
|
||||
alpha=uv_exp_blend_alpha, view_weight=uv_exp_blend_view_weight
|
||||
),
|
||||
empty_value=1.0,
|
||||
do_uv_padding=uv_padding,
|
||||
pad_unseen_area=from_scratch,
|
||||
poisson_blending=poisson_blending,
|
||||
pb_solver=self.pb_solver,
|
||||
pb_num_iters=pb_num_iters,
|
||||
pb_keep_original_border=pb_keep_original_border,
|
||||
)
|
||||
|
||||
if return_dict:
|
||||
# recommonded new way to get return value
|
||||
return CameraProjectionOutput(
|
||||
uv_proj=uv_blend_output.uv_attr_blend,
|
||||
uv_proj_mask=uv_blend_output.uv_valid_mask_blend,
|
||||
uv_depth_grad=uv_render_geometry_output.uv_depth_grad,
|
||||
uv_aoi_cos=uv_render_geometry_output.uv_aoi_cos,
|
||||
)
|
||||
else:
|
||||
if return_uv_projection_mask:
|
||||
return (
|
||||
uv_blend_output.uv_attr_blend,
|
||||
uv_blend_output.uv_valid_mask_blend,
|
||||
)
|
||||
return uv_blend_output.uv_attr_blend
|
||||
@@ -0,0 +1,310 @@
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import nvdiffrast.torch as dr
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import trimesh
|
||||
from PIL import Image
|
||||
from torch import BoolTensor, FloatTensor
|
||||
|
||||
from .camera import Camera
|
||||
from .mesh import TexturedMesh
|
||||
from .utils import get_clip_space_position, transform_points_homo
|
||||
|
||||
|
||||
@dataclass
|
||||
class RenderOutput:
|
||||
attr: Optional[torch.FloatTensor] = None
|
||||
mask: Optional[torch.BoolTensor] = None
|
||||
depth: Optional[torch.FloatTensor] = None
|
||||
normal: Optional[torch.FloatTensor] = None
|
||||
tangent: Optional[torch.FloatTensor] = None
|
||||
pos: Optional[torch.FloatTensor] = None
|
||||
|
||||
|
||||
class NVDiffRastContextWrapper:
|
||||
def __init__(self, device: str, context_type: str = "gl"):
|
||||
if context_type == "gl":
|
||||
self.ctx = dr.RasterizeGLContext(device=device)
|
||||
elif context_type == "cuda":
|
||||
self.ctx = dr.RasterizeCudaContext(device=device)
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
def rasterize(self, pos, tri, resolution, ranges=None, grad_db=True):
|
||||
"""
|
||||
Rasterize triangles.
|
||||
|
||||
All input tensors must be contiguous and reside in GPU memory except for the ranges tensor that, if specified, has to reside in CPU memory. The output tensors will be contiguous and reside in GPU memory.
|
||||
|
||||
Arguments:
|
||||
glctx Rasterizer context of type RasterizeGLContext or RasterizeCudaContext.
|
||||
pos Vertex position tensor with dtype torch.float32. To enable range mode, this tensor should have a 2D shape [num_vertices, 4]. To enable instanced mode, use a 3D shape [minibatch_size, num_vertices, 4].
|
||||
tri Triangle tensor with shape [num_triangles, 3] and dtype torch.int32.
|
||||
resolution Output resolution as integer tuple (height, width).
|
||||
ranges In range mode, tensor with shape [minibatch_size, 2] and dtype torch.int32, specifying start indices and counts into tri. Ignored in instanced mode.
|
||||
grad_db Propagate gradients of image-space derivatives of barycentrics into pos in backward pass. Ignored if using an OpenGL context that was not configured to output image-space derivatives.
|
||||
Returns:
|
||||
A tuple of two tensors. The first output tensor has shape [minibatch_size, height, width, 4] and contains the main rasterizer output in order (u, v, z/w, triangle_id). If the OpenGL context was configured to output image-space derivatives of barycentrics, the second output tensor will also have shape [minibatch_size, height, width, 4] and contain said derivatives in order (du/dX, du/dY, dv/dX, dv/dY). Otherwise it will be an empty tensor with shape [minibatch_size, height, width, 0].
|
||||
"""
|
||||
return dr.rasterize(
|
||||
self.ctx,
|
||||
pos.float().contiguous(),
|
||||
tri.int().contiguous(),
|
||||
resolution,
|
||||
ranges,
|
||||
grad_db,
|
||||
)
|
||||
|
||||
def interpolate(self, attr, rast, tri, rast_db=None, diff_attrs=None):
|
||||
"""
|
||||
Interpolate vertex attributes.
|
||||
|
||||
All input tensors must be contiguous and reside in GPU memory. The output tensors will be contiguous and reside in GPU memory.
|
||||
|
||||
Arguments:
|
||||
attr Attribute tensor with dtype torch.float32. Shape is [num_vertices, num_attributes] in range mode, or [minibatch_size, num_vertices, num_attributes] in instanced mode. Broadcasting is supported along the minibatch axis.
|
||||
rast Main output tensor from rasterize().
|
||||
tri Triangle tensor with shape [num_triangles, 3] and dtype torch.int32.
|
||||
rast_db (Optional) Tensor containing image-space derivatives of barycentrics, i.e., the second output tensor from rasterize(). Enables computing image-space derivatives of attributes.
|
||||
diff_attrs (Optional) List of attribute indices for which image-space derivatives are to be computed. Special value 'all' is equivalent to list [0, 1, ..., num_attributes - 1].
|
||||
Returns:
|
||||
A tuple of two tensors. The first output tensor contains interpolated attributes and has shape [minibatch_size, height, width, num_attributes]. If rast_db and diff_attrs were specified, the second output tensor contains the image-space derivatives of the selected attributes and has shape [minibatch_size, height, width, 2 * len(diff_attrs)]. The derivatives of the first selected attribute A will be on channels 0 and 1 as (dA/dX, dA/dY), etc. Otherwise, the second output tensor will be an empty tensor with shape [minibatch_size, height, width, 0].
|
||||
"""
|
||||
return dr.interpolate(
|
||||
attr.float().contiguous(), rast, tri.int().contiguous(), rast_db, diff_attrs
|
||||
)
|
||||
|
||||
def texture(
|
||||
self,
|
||||
tex,
|
||||
uv,
|
||||
uv_da=None,
|
||||
mip_level_bias=None,
|
||||
mip=None,
|
||||
filter_mode="auto",
|
||||
boundary_mode="wrap",
|
||||
max_mip_level=None,
|
||||
):
|
||||
"""
|
||||
Perform texture sampling.
|
||||
|
||||
All input tensors must be contiguous and reside in GPU memory. The output tensor will be contiguous and reside in GPU memory.
|
||||
|
||||
Arguments:
|
||||
tex Texture tensor with dtype torch.float32. For 2D textures, must have shape [minibatch_size, tex_height, tex_width, tex_channels]. For cube map textures, must have shape [minibatch_size, 6, tex_height, tex_width, tex_channels] where tex_width and tex_height are equal. Note that boundary_mode must also be set to 'cube' to enable cube map mode. Broadcasting is supported along the minibatch axis.
|
||||
uv Tensor containing per-pixel texture coordinates. When sampling a 2D texture, must have shape [minibatch_size, height, width, 2]. When sampling a cube map texture, must have shape [minibatch_size, height, width, 3].
|
||||
uv_da (Optional) Tensor containing image-space derivatives of texture coordinates. Must have same shape as uv except for the last dimension that is to be twice as long.
|
||||
mip_level_bias (Optional) Per-pixel bias for mip level selection. If uv_da is omitted, determines mip level directly. Must have shape [minibatch_size, height, width].
|
||||
mip (Optional) Preconstructed mipmap stack from a texture_construct_mip() call, or a list of tensors specifying a custom mipmap stack. When specifying a custom mipmap stack, the tensors in the list must follow the same format as tex except for width and height that must follow the usual rules for mipmap sizes. The base level texture is still supplied in tex and must not be included in the list. Gradients of a custom mipmap stack are not automatically propagated to base texture but the mipmap tensors will receive gradients of their own. If a mipmap stack is not specified but the chosen filter mode requires it, the mipmap stack is constructed internally and discarded afterwards.
|
||||
filter_mode Texture filtering mode to be used. Valid values are 'auto', 'nearest', 'linear', 'linear-mipmap-nearest', and 'linear-mipmap-linear'. Mode 'auto' selects 'linear' if neither uv_da or mip_level_bias is specified, and 'linear-mipmap-linear' when at least one of them is specified, these being the highest-quality modes possible depending on the availability of the image-space derivatives of the texture coordinates or direct mip level information.
|
||||
boundary_mode Valid values are 'wrap', 'clamp', 'zero', and 'cube'. If tex defines a cube map, this must be set to 'cube'. The default mode 'wrap' takes fractional part of texture coordinates. Mode 'clamp' clamps texture coordinates to the centers of the boundary texels. Mode 'zero' virtually extends the texture with all-zero values in all directions.
|
||||
max_mip_level If specified, limits the number of mipmaps constructed and used in mipmap-based filter modes.
|
||||
Returns:
|
||||
A tensor containing the results of the texture sampling with shape [minibatch_size, height, width, tex_channels]. Cube map fetches with invalid uv coordinates (e.g., zero vectors) output all zeros and do not propagate gradients.
|
||||
"""
|
||||
return dr.texture(
|
||||
tex.float(),
|
||||
uv.float(),
|
||||
uv_da,
|
||||
mip_level_bias,
|
||||
mip,
|
||||
filter_mode,
|
||||
boundary_mode,
|
||||
max_mip_level,
|
||||
)
|
||||
|
||||
def antialias(
|
||||
self, color, rast, pos, tri, topology_hash=None, pos_gradient_boost=1.0
|
||||
):
|
||||
"""
|
||||
Perform antialiasing.
|
||||
|
||||
All input tensors must be contiguous and reside in GPU memory. The output tensor will be contiguous and reside in GPU memory.
|
||||
|
||||
Note that silhouette edge determination is based on vertex indices in the triangle tensor. For it to work properly, a vertex belonging to multiple triangles must be referred to using the same vertex index in each triangle. Otherwise, nvdiffrast will always classify the adjacent edges as silhouette edges, which leads to bad performance and potentially incorrect gradients. If you are unsure whether your data is good, check which pixels are modified by the antialias operation and compare to the example in the documentation.
|
||||
|
||||
Arguments:
|
||||
color Input image to antialias with shape [minibatch_size, height, width, num_channels].
|
||||
rast Main output tensor from rasterize().
|
||||
pos Vertex position tensor used in the rasterization operation.
|
||||
tri Triangle tensor used in the rasterization operation.
|
||||
topology_hash (Optional) Preconstructed topology hash for the triangle tensor. If not specified, the topology hash is constructed internally and discarded afterwards.
|
||||
pos_gradient_boost (Optional) Multiplier for gradients propagated to pos.
|
||||
Returns:
|
||||
A tensor containing the antialiased image with the same shape as color input tensor.
|
||||
"""
|
||||
return dr.antialias(
|
||||
color.float(),
|
||||
rast,
|
||||
pos.float(),
|
||||
tri.int(),
|
||||
topology_hash,
|
||||
pos_gradient_boost,
|
||||
)
|
||||
|
||||
|
||||
class DepthNormalizationStrategy(ABC):
|
||||
@abstractmethod
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __call__(
|
||||
self, depth: torch.FloatTensor, mask: torch.BoolTensor
|
||||
) -> torch.FloatTensor:
|
||||
pass
|
||||
|
||||
|
||||
class DepthControlNetNormalization(DepthNormalizationStrategy):
|
||||
def __init__(
|
||||
self, far_clip: float = 0.25, near_clip: float = 1.0, bg_value: float = 0.0
|
||||
):
|
||||
self.far_clip = far_clip
|
||||
self.near_clip = near_clip
|
||||
self.bg_value = bg_value
|
||||
|
||||
def __call__(
|
||||
self, depth: torch.FloatTensor, mask: torch.BoolTensor
|
||||
) -> torch.FloatTensor:
|
||||
batch_size = depth.shape[0]
|
||||
min_depth = depth.view(batch_size, -1).min(dim=-1)[0][:, None, None]
|
||||
max_depth = depth.view(batch_size, -1).max(dim=-1)[0][:, None, None]
|
||||
depth = 1.0 - ((depth - min_depth) / (max_depth - min_depth + 1e-5)).clamp(
|
||||
0.0, 1.0
|
||||
)
|
||||
depth = depth * (self.near_clip - self.far_clip) + self.far_clip
|
||||
depth[~mask] = self.bg_value
|
||||
return depth
|
||||
|
||||
|
||||
class Zero123PlusPlusNormalization(DepthNormalizationStrategy):
|
||||
def __init__(self, bg_value: float = 0.8):
|
||||
self.bg_value = bg_value
|
||||
|
||||
def __call__(self, depth: FloatTensor, mask: BoolTensor) -> FloatTensor:
|
||||
batch_size = depth.shape[0]
|
||||
min_depth = depth.view(batch_size, -1).min(dim=-1)[0][:, None, None]
|
||||
max_depth = depth.view(batch_size, -1).max(dim=-1)[0][:, None, None]
|
||||
depth = ((depth - min_depth) / (max_depth - min_depth + 1e-5)).clamp(0.0, 1.0)
|
||||
depth[~mask] = self.bg_value
|
||||
return depth
|
||||
|
||||
|
||||
class SimpleNormalization(DepthNormalizationStrategy):
|
||||
def __init__(
|
||||
self,
|
||||
scale: float = 1.0,
|
||||
offset: float = -1.0,
|
||||
clamp: bool = True,
|
||||
bg_value: float = 1.0,
|
||||
):
|
||||
self.scale = scale
|
||||
self.offset = offset
|
||||
self.clamp = clamp
|
||||
self.bg_value = bg_value
|
||||
|
||||
def __call__(self, depth: FloatTensor, mask: BoolTensor) -> FloatTensor:
|
||||
depth = depth * self.scale + self.offset
|
||||
if self.clamp:
|
||||
depth = depth.clamp(0.0, 1.0)
|
||||
depth[~mask] = self.bg_value
|
||||
return depth
|
||||
|
||||
|
||||
def render(
|
||||
ctx: NVDiffRastContextWrapper,
|
||||
mesh: TexturedMesh,
|
||||
cam: Camera,
|
||||
height: int,
|
||||
width: int,
|
||||
render_attr: bool = True,
|
||||
render_depth: bool = True,
|
||||
render_normal: bool = True,
|
||||
render_tangent: bool = False,
|
||||
depth_normalization_strategy: DepthNormalizationStrategy = DepthControlNetNormalization(),
|
||||
attr_background: Union[float, torch.FloatTensor] = 0.5,
|
||||
antialias_attr=False,
|
||||
normal_background: Union[float, torch.FloatTensor] = 0.0,
|
||||
tangent_background: Union[float, torch.FloatTensor] = 0.0,
|
||||
texture_override=None,
|
||||
texture_filter_mode: str = "linear",
|
||||
) -> RenderOutput:
|
||||
output_dict = {}
|
||||
|
||||
v_pos_clip = get_clip_space_position(mesh.v_pos, cam.mvp_mtx)
|
||||
rast, _ = ctx.rasterize(v_pos_clip, mesh.t_pos_idx, (height, width), grad_db=True)
|
||||
mask = rast[..., 3] > 0
|
||||
|
||||
gb_pos, _ = ctx.interpolate(mesh.v_pos[None], rast, mesh.t_pos_idx)
|
||||
output_dict.update({"mask": mask, "pos": gb_pos})
|
||||
|
||||
if render_depth:
|
||||
gb_pos_vs = transform_points_homo(gb_pos, cam.w2c)
|
||||
gb_depth = -gb_pos_vs[..., 2]
|
||||
# set background pixels to min depth value for correct min/max calculation
|
||||
gb_depth = torch.where(
|
||||
mask,
|
||||
gb_depth,
|
||||
gb_depth.view(gb_depth.shape[0], -1).min(dim=-1)[0][:, None, None],
|
||||
)
|
||||
gb_depth = depth_normalization_strategy(gb_depth, mask)
|
||||
output_dict["depth"] = gb_depth
|
||||
|
||||
if render_attr:
|
||||
tex_c, _ = ctx.interpolate(mesh.v_tex[None], rast, mesh.t_tex_idx)
|
||||
texture = (
|
||||
texture_override[None]
|
||||
if texture_override is not None
|
||||
else mesh.texture[None]
|
||||
)
|
||||
gb_rgb_fg = ctx.texture(texture, tex_c, filter_mode=texture_filter_mode)
|
||||
gb_rgb_bg = torch.ones_like(gb_rgb_fg) * attr_background
|
||||
gb_rgb = torch.where(mask[..., None], gb_rgb_fg, gb_rgb_bg)
|
||||
if antialias_attr:
|
||||
gb_rgb = ctx.antialias(gb_rgb, rast, v_pos_clip, mesh.t_pos_idx)
|
||||
output_dict["attr"] = gb_rgb
|
||||
|
||||
if render_normal:
|
||||
gb_nrm, _ = ctx.interpolate(mesh.v_nrm[None], rast, mesh.stitched_t_pos_idx)
|
||||
gb_nrm = F.normalize(gb_nrm, dim=-1, p=2)
|
||||
gb_nrm[~mask] = normal_background
|
||||
output_dict["normal"] = gb_nrm
|
||||
|
||||
if render_tangent:
|
||||
gb_tang, _ = ctx.interpolate(mesh.v_tang[None], rast, mesh.stitched_t_pos_idx)
|
||||
gb_tang = F.normalize(gb_tang, dim=-1, p=2)
|
||||
gb_tang[~mask] = tangent_background
|
||||
output_dict["tangent"] = gb_tang
|
||||
|
||||
return RenderOutput(**output_dict)
|
||||
|
||||
|
||||
def tensor_to_image(
|
||||
data: Union[Image.Image, torch.Tensor, np.ndarray],
|
||||
batched: bool = False,
|
||||
format: str = "HWC",
|
||||
) -> Union[Image.Image, List[Image.Image]]:
|
||||
if isinstance(data, Image.Image):
|
||||
return data
|
||||
if isinstance(data, torch.Tensor):
|
||||
data = data.detach().cpu().numpy()
|
||||
if data.dtype == np.float32 or data.dtype == np.float16:
|
||||
data = (data * 255).astype(np.uint8)
|
||||
elif data.dtype == np.bool_:
|
||||
data = data.astype(np.uint8) * 255
|
||||
assert data.dtype == np.uint8
|
||||
if format == "CHW":
|
||||
if batched and data.ndim == 4:
|
||||
data = data.transpose((0, 2, 3, 1))
|
||||
elif not batched and data.ndim == 3:
|
||||
data = data.transpose((1, 2, 0))
|
||||
|
||||
if batched:
|
||||
return [Image.fromarray(d) for d in data]
|
||||
return Image.fromarray(data)
|
||||
@@ -0,0 +1,38 @@
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import torch
|
||||
import transformers
|
||||
|
||||
from .utils import IMAGE_TYPE, image_to_tensor
|
||||
|
||||
|
||||
class SegmentationModel(ABC):
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __call__(self, images: IMAGE_TYPE) -> torch.FloatTensor:
|
||||
pass
|
||||
|
||||
|
||||
class RMBGModel(SegmentationModel):
|
||||
def __init__(self, pretrained_model_name_or_path: str, device: str):
|
||||
self.model = transformers.AutoModelForImageSegmentation.from_pretrained(
|
||||
pretrained_model_name_or_path, trust_remote_code=True
|
||||
).to(device)
|
||||
self.device = device
|
||||
|
||||
def __call__(self, images: IMAGE_TYPE) -> torch.FloatTensor:
|
||||
images = image_to_tensor(images, device=self.device)
|
||||
batched = True
|
||||
if images.ndim == 3:
|
||||
images = images.unsqueeze(0)
|
||||
batched = False
|
||||
|
||||
out = (
|
||||
self.model(images.permute(0, 3, 1, 2) - 0.5)[0][0]
|
||||
.clamp(0.0, 1.0)
|
||||
.permute(0, 2, 3, 1)
|
||||
)
|
||||
if not batched:
|
||||
out = out.squeeze(0)
|
||||
return out
|
||||
@@ -0,0 +1,334 @@
|
||||
import glob
|
||||
import os
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from itertools import product
|
||||
from typing import Any, Dict, List, Optional, Union
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from PIL import Image
|
||||
|
||||
from .camera import Camera, get_camera
|
||||
from .mesh import (
|
||||
TexturedMesh,
|
||||
load_mesh,
|
||||
mesh_use_texture,
|
||||
replace_mesh_texture_and_save,
|
||||
)
|
||||
from .projection import CameraProjection
|
||||
from .render import NVDiffRastContextWrapper, render
|
||||
from .utils import image_to_tensor, make_image_grid, tensor_to_image
|
||||
from .uv import (
|
||||
ExponentialBlend,
|
||||
NVDiffRastContextWrapper,
|
||||
PoissonBlendingSolver,
|
||||
SimpleUVValidityStrategy,
|
||||
uv_blend,
|
||||
uv_padding,
|
||||
uv_precompute,
|
||||
uv_render_attr,
|
||||
uv_render_geometry,
|
||||
)
|
||||
|
||||
|
||||
class SmartPainter:
|
||||
def __init__(self, device: str):
|
||||
self.device = device
|
||||
self.cam_proj = CameraProjection(
|
||||
pb_backend="torch-cuda", bg_remover=None, device=device
|
||||
)
|
||||
self.ctx = NVDiffRastContextWrapper(device=self.device)
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
mod_name: str,
|
||||
mesh: TexturedMesh,
|
||||
inpaint_func: callable,
|
||||
uv_texture: torch.FloatTensor,
|
||||
uv_inpaint_mask: torch.BoolTensor,
|
||||
max_view_score_thresh: float = 0.02,
|
||||
min_rounds: int = 3,
|
||||
max_rounds: int = 8,
|
||||
uv_padding_end: bool = True,
|
||||
debug_dir: Optional[str] = None,
|
||||
debug_visualize_details: bool = False,
|
||||
):
|
||||
def make_view_selection_cam_params():
|
||||
elevation_anchors = range(-60, 61, 15)
|
||||
azimuth_anchors = range(0, 360, 30)
|
||||
distance_anchors = [1.2]
|
||||
fovy_deg_anchors = [40]
|
||||
params = list(
|
||||
product(
|
||||
elevation_anchors,
|
||||
azimuth_anchors,
|
||||
distance_anchors,
|
||||
fovy_deg_anchors,
|
||||
)
|
||||
)
|
||||
params = list(zip(*params))
|
||||
return params
|
||||
|
||||
elevation_deg, azimuth_deg, distance, fovy_deg = (
|
||||
make_view_selection_cam_params()
|
||||
)
|
||||
view_selection_cams = get_camera(
|
||||
elevation_deg=list(elevation_deg),
|
||||
azimuth_deg=list(azimuth_deg),
|
||||
distance=list(distance),
|
||||
fovy_deg=list(fovy_deg),
|
||||
perturb_camera_position=0.1,
|
||||
device=self.device,
|
||||
)
|
||||
|
||||
texture_update = uv_texture.clone()
|
||||
uv_valid_mask_update = ~uv_inpaint_mask.clone()
|
||||
score_map_update = torch.zeros_like(uv_valid_mask_update, dtype=torch.float32)
|
||||
score_map_update[uv_valid_mask_update] = 1.0
|
||||
|
||||
max_view_score = 1
|
||||
i = 0
|
||||
while i < min_rounds or (
|
||||
max_view_score > max_view_score_thresh and i < max_rounds
|
||||
):
|
||||
score_map_image = score_map_update.float()[:, :, None].repeat_interleave(
|
||||
3, dim=-1
|
||||
)
|
||||
render_size = 256
|
||||
with mesh_use_texture(mesh, score_map_image):
|
||||
render_out = render(
|
||||
self.ctx,
|
||||
mesh,
|
||||
view_selection_cams,
|
||||
height=render_size,
|
||||
width=render_size,
|
||||
attr_background=1.0,
|
||||
texture_filter_mode="nearest",
|
||||
)
|
||||
|
||||
def get_view_aoi_cos(render_out, cameras):
|
||||
view_normal = render_out.normal
|
||||
view_normal_cs = (
|
||||
view_normal[:, :, :, None, :] * cameras.w2c[:, None, None, :3, :3]
|
||||
).sum(-1)
|
||||
view_normal_cs = F.normalize(view_normal_cs, dim=-1, p=2)
|
||||
view_normal_cs[~render_out.mask] = 0
|
||||
view_aoi_cos = (
|
||||
(
|
||||
view_normal_cs
|
||||
* torch.as_tensor(
|
||||
[0.0, 0.0, 1.0],
|
||||
dtype=view_normal_cs.dtype,
|
||||
device=view_normal_cs.device,
|
||||
)[None, None, None]
|
||||
)
|
||||
.sum(-1)
|
||||
.clamp(0, 1)
|
||||
)
|
||||
return view_aoi_cos
|
||||
|
||||
view_aoi_cos = get_view_aoi_cos(render_out, view_selection_cams)
|
||||
if debug_dir is not None and debug_visualize_details:
|
||||
make_image_grid(tensor_to_image(render_out.attr, batched=True)).save(
|
||||
os.path.join(debug_dir, f"{mod_name}_render_score_{i:02d}.jpg")
|
||||
)
|
||||
make_image_grid(tensor_to_image(view_aoi_cos, batched=True)).save(
|
||||
os.path.join(
|
||||
debug_dir, f"{mod_name}_render_view_aoi_cos_{i:02d}.jpg"
|
||||
)
|
||||
)
|
||||
view_score = [
|
||||
(
|
||||
((attr < 1e-3) & (aoi_cos > 0.1)).sum().item()
|
||||
+ (
|
||||
((attr > 1e-3) & (aoi_cos > 0.1)).float()
|
||||
* (aoi_cos - attr - 0.3).clamp_min(0)
|
||||
)
|
||||
.sum()
|
||||
.item()
|
||||
)
|
||||
/ render_size**2
|
||||
for attr, aoi_cos in zip(render_out.attr[..., 0], view_aoi_cos)
|
||||
]
|
||||
|
||||
max_view_score = np.max(view_score)
|
||||
best_view = np.argmax(view_score)
|
||||
|
||||
inpaint_render_size = 1024
|
||||
best_cam = view_selection_cams[best_view : best_view + 1]
|
||||
|
||||
def shrink_mask(mask: torch.BoolTensor, radius: int) -> torch.BoolTensor:
|
||||
return (
|
||||
(
|
||||
-torch.nn.functional.max_pool2d(
|
||||
-(mask[None, None].float()),
|
||||
kernel_size=radius * 2 + 1,
|
||||
stride=1,
|
||||
padding=radius,
|
||||
)
|
||||
)
|
||||
.squeeze()
|
||||
.bool()
|
||||
)
|
||||
|
||||
def enlarge_mask(mask: torch.BoolTensor, radius: int) -> torch.BoolTensor:
|
||||
return (
|
||||
torch.nn.functional.max_pool2d(
|
||||
mask[None, None].float(),
|
||||
kernel_size=radius * 2 + 1,
|
||||
stride=1,
|
||||
padding=radius,
|
||||
)
|
||||
.squeeze()
|
||||
.bool()
|
||||
)
|
||||
|
||||
def blur_mask(mask: torch.BoolTensor, radius: int) -> torch.FloatTensor:
|
||||
# Create a Gaussian kernel
|
||||
x = torch.arange(
|
||||
-radius, radius + 1, dtype=torch.float32, device=mask.device
|
||||
)
|
||||
kernel = torch.exp(-(x**2) / (2 * (radius / 2) ** 2))
|
||||
kernel = kernel[None, None, None]
|
||||
kernel = kernel * kernel.transpose(2, 3)
|
||||
kernel = kernel / kernel.sum()
|
||||
# Apply convolution to blur the mask
|
||||
blurred_mask = F.conv2d(
|
||||
mask[None, None].float(), kernel, padding=radius
|
||||
)
|
||||
return blurred_mask.squeeze()
|
||||
|
||||
def get_occulusion_boundary(
|
||||
view_depth: torch.FloatTensor, dilation: int, thresh: float
|
||||
) -> torch.BoolTensor:
|
||||
grad_x_kernel = torch.tensor(
|
||||
[[1, 0, -1], [2, 0, -2], [1, 0, -1]],
|
||||
device=view_depth.device,
|
||||
dtype=torch.float32,
|
||||
).view(1, 1, 3, 3)
|
||||
grad_y_kernel = torch.tensor(
|
||||
[[1, 2, 1], [0, 0, 0], [-1, -2, -1]],
|
||||
device=view_depth.device,
|
||||
dtype=torch.float32,
|
||||
).view(1, 1, 3, 3)
|
||||
view_depth_grad_x = F.conv2d(
|
||||
view_depth[None, None], grad_x_kernel, padding=1
|
||||
)
|
||||
view_depth_grad_y = F.conv2d(
|
||||
view_depth[None, None], grad_y_kernel, padding=1
|
||||
)
|
||||
view_depth_grad = (view_depth_grad_x**2 + view_depth_grad_y**2).sqrt()
|
||||
occ_boundary = (view_depth_grad > thresh)[0][0]
|
||||
if dilation > 0:
|
||||
occ_boundary = enlarge_mask(occ_boundary, dilation)
|
||||
return occ_boundary
|
||||
|
||||
with mesh_use_texture(mesh, score_map_image):
|
||||
render_out = render(
|
||||
self.ctx,
|
||||
mesh,
|
||||
best_cam,
|
||||
height=inpaint_render_size,
|
||||
width=inpaint_render_size,
|
||||
attr_background=1.0,
|
||||
texture_filter_mode="nearest",
|
||||
)
|
||||
|
||||
view_aoi_cos = get_view_aoi_cos(render_out, best_cam)
|
||||
inpaint_mask = (render_out.attr[0, :, :, 0] < 1e-3) | (
|
||||
view_aoi_cos[0] - render_out.attr[0, :, :, 0] > 0.3
|
||||
)
|
||||
occ_boundary = get_occulusion_boundary(
|
||||
render_out.depth[0], dilation=0, thresh=0.1
|
||||
)
|
||||
# shrink to remove uv seam
|
||||
# enlarge the mask gives better results
|
||||
# remove the occlusion boundary to avoid bleeding
|
||||
inpaint_mask = enlarge_mask(shrink_mask(inpaint_mask, 3), 5) & ~occ_boundary
|
||||
if debug_dir is not None and debug_visualize_details:
|
||||
tensor_to_image(occ_boundary).save(
|
||||
os.path.join(debug_dir, f"{mod_name}_occ_boundary_{i:02d}.jpg")
|
||||
)
|
||||
|
||||
with mesh_use_texture(mesh, texture_update):
|
||||
inpaint_image = render(
|
||||
self.ctx,
|
||||
mesh,
|
||||
best_cam,
|
||||
height=inpaint_render_size,
|
||||
width=inpaint_render_size,
|
||||
texture_filter_mode="linear",
|
||||
).attr[0]
|
||||
|
||||
# according to https://github.com/advimman/lama/issues/262
|
||||
# blur the sharp edge mask gives better results
|
||||
# but it gives worse results in our case
|
||||
# inpaint_mask = blur_mask(inpaint_mask, 1)
|
||||
|
||||
inpaint_mask_input = inpaint_mask.float()[None, None]
|
||||
inpaint_image_input = inpaint_image.permute(2, 0, 1)[None]
|
||||
inpaint_result = inpaint_func(inpaint_image_input, inpaint_mask_input)[
|
||||
0
|
||||
].permute(1, 2, 0)
|
||||
|
||||
if debug_dir is not None:
|
||||
make_image_grid(
|
||||
[
|
||||
tensor_to_image(inpaint_image),
|
||||
tensor_to_image(inpaint_mask),
|
||||
tensor_to_image(inpaint_result),
|
||||
],
|
||||
rows=1,
|
||||
).save(
|
||||
os.path.join(debug_dir, f"{mod_name}_inpaint_result_{i:02d}.jpg")
|
||||
)
|
||||
with mesh_use_texture(mesh, texture_update):
|
||||
proj_out = self.cam_proj(
|
||||
inpaint_result[None],
|
||||
mesh,
|
||||
best_cam,
|
||||
masks=inpaint_mask[None].float(),
|
||||
from_scratch=False,
|
||||
poisson_blending=False,
|
||||
depth_grad_dilation=3,
|
||||
uv_exp_blend_alpha=3,
|
||||
aoi_cos_valid_threshold=0.1,
|
||||
uv_size=mesh.uv_size,
|
||||
uv_padding=True,
|
||||
iou_rejection_threshold=None,
|
||||
return_dict=True,
|
||||
)
|
||||
uv_proj_inpaint, uv_valid_mask_inpaint = (
|
||||
proj_out.uv_proj,
|
||||
proj_out.uv_proj_mask,
|
||||
)
|
||||
if debug_dir is not None and debug_visualize_details:
|
||||
tensor_to_image(uv_valid_mask_inpaint).save(
|
||||
os.path.join(debug_dir, f"{mod_name}_uv_inpaint_mask_{i:02d}.jpg")
|
||||
)
|
||||
texture_update = uv_proj_inpaint
|
||||
uv_valid_mask_update = uv_valid_mask_inpaint | uv_valid_mask_update
|
||||
score_map_inpaint = torch.where(
|
||||
uv_valid_mask_inpaint,
|
||||
proj_out.uv_aoi_cos[0],
|
||||
torch.zeros_like(proj_out.uv_aoi_cos[0]),
|
||||
)
|
||||
score_map = torch.max(score_map_update, score_map_inpaint)
|
||||
if debug_dir is not None and debug_visualize_details:
|
||||
make_image_grid(
|
||||
[
|
||||
tensor_to_image(score_map_update),
|
||||
tensor_to_image(score_map_inpaint),
|
||||
tensor_to_image(score_map),
|
||||
]
|
||||
).save(os.path.join(debug_dir, f"{mod_name}_score_map_{i:02d}.jpg"))
|
||||
score_map_update = score_map
|
||||
i += 1
|
||||
|
||||
if uv_padding_end:
|
||||
texture_update = uv_padding(texture_update, uv_valid_mask_update, 3)
|
||||
|
||||
return texture_update, uv_valid_mask_update
|
||||
@@ -0,0 +1,139 @@
|
||||
import io
|
||||
import math
|
||||
from abc import ABC, abstractmethod
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import numpy as np
|
||||
import nvdiffrast.torch as dr
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import trimesh
|
||||
from PIL import Image
|
||||
from torch import BoolTensor, FloatTensor
|
||||
|
||||
LIST_TYPE = Union[list, np.ndarray, torch.Tensor]
|
||||
IMAGE_TYPE = Union[Image.Image, List[Image.Image], np.ndarray, torch.Tensor]
|
||||
SINGLE_IMAGE_TYPE = Union[Image.Image, np.ndarray, torch.Tensor]
|
||||
|
||||
|
||||
def tensor_to_image(
|
||||
data: Union[Image.Image, torch.Tensor, np.ndarray],
|
||||
batched: bool = False,
|
||||
format: str = "HWC",
|
||||
) -> Union[Image.Image, List[Image.Image]]:
|
||||
if isinstance(data, Image.Image):
|
||||
return data
|
||||
if isinstance(data, torch.Tensor):
|
||||
data = data.detach().cpu().numpy()
|
||||
if data.dtype == np.float32 or data.dtype == np.float16:
|
||||
data = (data * 255).astype(np.uint8)
|
||||
elif data.dtype == np.bool_:
|
||||
data = data.astype(np.uint8) * 255
|
||||
assert data.dtype == np.uint8
|
||||
if format == "CHW":
|
||||
if batched and data.ndim == 4:
|
||||
data = data.transpose((0, 2, 3, 1))
|
||||
elif not batched and data.ndim == 3:
|
||||
data = data.transpose((1, 2, 0))
|
||||
|
||||
if batched:
|
||||
return [Image.fromarray(d) for d in data]
|
||||
return Image.fromarray(data)
|
||||
|
||||
|
||||
def image_to_tensor(image: IMAGE_TYPE, return_type="pt", device: Optional[str] = None):
|
||||
assert return_type in ["np", "pt"]
|
||||
batched = True
|
||||
if isinstance(image, Image.Image):
|
||||
batched = False
|
||||
image = [image]
|
||||
if isinstance(image, list):
|
||||
image = np.stack([np.array(img) for img in image], axis=0)
|
||||
image = image.astype(np.float32) / 255.0
|
||||
if isinstance(image, np.ndarray) and return_type == "pt":
|
||||
image = torch.tensor(image, device=device)
|
||||
if isinstance(image, torch.Tensor):
|
||||
image = image.to(dtype=torch.float32, device=device)
|
||||
|
||||
if not batched:
|
||||
image = image[0]
|
||||
return image
|
||||
|
||||
|
||||
def largest_factor_near_sqrt(n: int) -> int:
|
||||
"""
|
||||
Finds the largest factor of n that is closest to the square root of n.
|
||||
|
||||
Args:
|
||||
n (int): The integer for which to find the largest factor near its square root.
|
||||
|
||||
Returns:
|
||||
int: The largest factor of n that is closest to the square root of n.
|
||||
"""
|
||||
sqrt_n = int(math.sqrt(n)) # Get the integer part of the square root
|
||||
|
||||
# First, check if the square root itself is a factor
|
||||
if sqrt_n * sqrt_n == n:
|
||||
return sqrt_n
|
||||
|
||||
# Otherwise, find the largest factor by iterating from sqrt_n downwards
|
||||
for i in range(sqrt_n, 0, -1):
|
||||
if n % i == 0:
|
||||
return i
|
||||
|
||||
# If n is 1, return 1
|
||||
return 1
|
||||
|
||||
|
||||
def make_image_grid(
|
||||
images: List[Image.Image],
|
||||
rows: Optional[int] = None,
|
||||
cols: Optional[int] = None,
|
||||
resize: Optional[int] = None,
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Prepares a single grid of images. Useful for visualization purposes.
|
||||
"""
|
||||
if rows is None and cols is not None:
|
||||
assert len(images) % cols == 0
|
||||
rows = len(images) // cols
|
||||
elif cols is None and rows is not None:
|
||||
assert len(images) % rows == 0
|
||||
cols = len(images) // rows
|
||||
elif rows is None and cols is None:
|
||||
rows = largest_factor_near_sqrt(len(images))
|
||||
cols = len(images) // rows
|
||||
|
||||
assert len(images) == rows * cols
|
||||
|
||||
if resize is not None:
|
||||
images = [img.resize((resize, resize)) for img in images]
|
||||
|
||||
w, h = images[0].size
|
||||
grid = Image.new("RGB", size=(cols * w, rows * h))
|
||||
|
||||
for i, img in enumerate(images):
|
||||
grid.paste(img, box=(i % cols * w, i // cols * h))
|
||||
return grid
|
||||
|
||||
|
||||
def get_current_timestamp(fmt: str = "%Y%m%d%H%M%S") -> str:
|
||||
return datetime.now().strftime(fmt)
|
||||
|
||||
|
||||
def get_clip_space_position(pos: torch.FloatTensor, mvp_mtx: torch.FloatTensor):
|
||||
pos_homo = torch.cat([pos, torch.ones([pos.shape[0], 1]).to(pos)], dim=-1)
|
||||
return torch.matmul(pos_homo, mvp_mtx.permute(0, 2, 1))
|
||||
|
||||
|
||||
def transform_points_homo(pos: torch.FloatTensor, mtx: torch.FloatTensor):
|
||||
batch_size = pos.shape[0]
|
||||
pos_shape = pos.shape[1:-1]
|
||||
pos = pos.reshape(batch_size, -1, 3)
|
||||
pos_homo = torch.cat([pos, torch.ones_like(pos[..., 0:1])], dim=-1)
|
||||
pos = (pos_homo.unsqueeze(2) * mtx.unsqueeze(1)).sum(-1)[..., :3]
|
||||
pos = pos.reshape(batch_size, *pos_shape, 3)
|
||||
return pos
|
||||
@@ -0,0 +1,468 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
|
||||
from .blend import PoissonBlendingSolver
|
||||
from .camera import Camera
|
||||
from .mesh import TexturedMesh
|
||||
from .render import NVDiffRastContextWrapper, SimpleNormalization, render
|
||||
from .utils import IMAGE_TYPE, get_clip_space_position, image_to_tensor
|
||||
|
||||
|
||||
@dataclass
|
||||
class UVPrecomputeOutput:
|
||||
height: int
|
||||
width: int
|
||||
uv_attr: torch.Tensor
|
||||
uv_mask: torch.Tensor
|
||||
uv_pos: torch.Tensor
|
||||
|
||||
|
||||
def uv_precompute(
|
||||
ctx: NVDiffRastContextWrapper,
|
||||
mesh: TexturedMesh,
|
||||
height: int,
|
||||
width: int,
|
||||
clone_attr: bool = False,
|
||||
) -> UVPrecomputeOutput:
|
||||
uv_clip = mesh.v_tex * 2.0 - 1.0
|
||||
uv_clip4 = torch.cat(
|
||||
(
|
||||
uv_clip,
|
||||
torch.zeros_like(uv_clip[..., 0:1]),
|
||||
torch.ones_like(uv_clip[..., 0:1]),
|
||||
),
|
||||
dim=-1,
|
||||
)
|
||||
rast, _ = ctx.rasterize(uv_clip4[None], mesh.t_tex_idx, (height, width))
|
||||
uv_mask = rast[0, :, :, 3] > 0
|
||||
|
||||
uv_pos, _ = ctx.interpolate(mesh.v_pos[None], rast, mesh.t_pos_idx)
|
||||
uv_pos = uv_pos[0]
|
||||
|
||||
uv_precompute_output = UVPrecomputeOutput(
|
||||
height=height,
|
||||
width=width,
|
||||
uv_attr=mesh.texture.clone() if clone_attr else mesh.texture,
|
||||
uv_mask=uv_mask,
|
||||
uv_pos=uv_pos,
|
||||
)
|
||||
return uv_precompute_output
|
||||
|
||||
|
||||
@dataclass
|
||||
class UVRenderGeometryOutput:
|
||||
uv_pos_proj: torch.Tensor
|
||||
uv_pos_error: torch.Tensor
|
||||
uv_aoi_cos: torch.Tensor
|
||||
uv_pos_ndc: torch.Tensor
|
||||
view_mask: torch.Tensor
|
||||
view_normal: torch.Tensor
|
||||
view_aoi_cos: torch.Tensor
|
||||
view_position: torch.Tensor
|
||||
view_depth: torch.Tensor
|
||||
view_depth_grad: Optional[torch.Tensor] = None
|
||||
uv_depth_grad: Optional[torch.Tensor] = None
|
||||
view_attr: Optional[torch.Tensor] = None
|
||||
|
||||
|
||||
def uv_render_geometry(
|
||||
ctx: NVDiffRastContextWrapper,
|
||||
mesh: TexturedMesh,
|
||||
cam: Camera,
|
||||
view_height: int,
|
||||
view_width: int,
|
||||
uv_precompute_output: UVPrecomputeOutput,
|
||||
grid_sample_mode="bilinear",
|
||||
compute_depth_grad: bool = False,
|
||||
depth_grad_dilation: int = 1,
|
||||
render_attr: bool = False,
|
||||
) -> UVRenderGeometryOutput:
|
||||
batch_size = cam.c2w.shape[0]
|
||||
|
||||
height, width, _ = uv_precompute_output.uv_pos.shape
|
||||
uv_pos_clip = get_clip_space_position(
|
||||
uv_precompute_output.uv_pos.view(-1, 3), cam.mvp_mtx
|
||||
).view(batch_size, height, width, 4)
|
||||
uv_pos_ndc = uv_pos_clip[..., :2] / uv_pos_clip[..., 3:4]
|
||||
|
||||
render_output = render(
|
||||
ctx,
|
||||
mesh,
|
||||
cam,
|
||||
view_height,
|
||||
view_width,
|
||||
render_attr=render_attr,
|
||||
render_depth=True,
|
||||
render_normal=True,
|
||||
depth_normalization_strategy=SimpleNormalization(
|
||||
scale=1.0, offset=0.0, clamp=False, bg_value=1e2
|
||||
),
|
||||
)
|
||||
view_position = render_output.pos
|
||||
view_mask = render_output.mask
|
||||
view_normal = render_output.normal
|
||||
view_normal_cs = (
|
||||
view_normal[:, :, :, None, :] * cam.w2c[:, None, None, :3, :3]
|
||||
).sum(-1)
|
||||
view_normal_cs = F.normalize(view_normal_cs, dim=-1, p=2)
|
||||
view_normal_cs[~render_output.mask] = render_output.normal[~render_output.mask]
|
||||
view_aoi_cos = (
|
||||
view_normal_cs
|
||||
* torch.as_tensor(
|
||||
[0.0, 0.0, 1.0], dtype=view_normal_cs.dtype, device=view_normal_cs.device
|
||||
)[None, None, None]
|
||||
).sum(-1)
|
||||
view_aoi_cos = view_aoi_cos.clamp(0.0, 1.0)
|
||||
|
||||
view_depth = render_output.depth
|
||||
if compute_depth_grad:
|
||||
grad_x_kernel = torch.tensor(
|
||||
[[1, 0, -1], [2, 0, -2], [1, 0, -1]],
|
||||
device=view_depth.device,
|
||||
dtype=torch.float32,
|
||||
).view(1, 1, 3, 3)
|
||||
grad_y_kernel = torch.tensor(
|
||||
[[1, 2, 1], [0, 0, 0], [-1, -2, -1]],
|
||||
device=view_depth.device,
|
||||
dtype=torch.float32,
|
||||
).view(1, 1, 3, 3)
|
||||
view_depth_grad_x = F.conv2d(view_depth[:, None], grad_x_kernel, padding=1)
|
||||
view_depth_grad_y = F.conv2d(view_depth[:, None], grad_y_kernel, padding=1)
|
||||
view_depth_grad = (view_depth_grad_x**2 + view_depth_grad_y**2).sqrt()
|
||||
view_depth_grad = F.max_pool2d(
|
||||
view_depth_grad,
|
||||
depth_grad_dilation,
|
||||
stride=1,
|
||||
padding=depth_grad_dilation // 2,
|
||||
)
|
||||
|
||||
uv_depth_grad = F.grid_sample(
|
||||
view_depth_grad, uv_pos_ndc, align_corners=False, mode=grid_sample_mode
|
||||
).permute(0, 2, 3, 1)[..., 0]
|
||||
else:
|
||||
view_depth_grad = None
|
||||
uv_depth_grad = None
|
||||
|
||||
if render_attr:
|
||||
view_attr = render_output.attr
|
||||
else:
|
||||
view_attr = None
|
||||
|
||||
uv_pos_proj = F.grid_sample(
|
||||
view_position.permute(0, 3, 1, 2),
|
||||
uv_pos_ndc,
|
||||
align_corners=False,
|
||||
mode=grid_sample_mode,
|
||||
).permute(0, 2, 3, 1)
|
||||
|
||||
uv_pos_error = ((uv_pos_proj - uv_precompute_output.uv_pos) ** 2).sum(-1).sqrt()
|
||||
|
||||
uv_aoi_cos = F.grid_sample(
|
||||
view_aoi_cos[..., None].permute(0, 3, 1, 2),
|
||||
uv_pos_ndc,
|
||||
align_corners=False,
|
||||
mode=grid_sample_mode,
|
||||
).permute(0, 2, 3, 1)[..., 0]
|
||||
|
||||
return UVRenderGeometryOutput(
|
||||
uv_pos_proj=uv_pos_proj,
|
||||
uv_pos_error=uv_pos_error,
|
||||
uv_aoi_cos=uv_aoi_cos,
|
||||
uv_pos_ndc=uv_pos_ndc,
|
||||
view_mask=view_mask,
|
||||
view_position=view_position,
|
||||
view_normal=view_normal,
|
||||
view_aoi_cos=view_aoi_cos,
|
||||
view_depth=view_depth,
|
||||
view_depth_grad=view_depth_grad,
|
||||
uv_depth_grad=uv_depth_grad,
|
||||
view_attr=view_attr,
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UVRenderAttrOutput:
|
||||
uv_attr_proj: torch.Tensor
|
||||
uv_mask_proj: Optional[torch.Tensor]
|
||||
|
||||
|
||||
def uv_render_attr(
|
||||
images: IMAGE_TYPE,
|
||||
uv_render_geometry_output: UVRenderGeometryOutput,
|
||||
masks: Optional[IMAGE_TYPE] = None,
|
||||
grid_sample_mode: str = "bilinear",
|
||||
) -> UVRenderAttrOutput:
|
||||
images = image_to_tensor(images, device=uv_render_geometry_output.uv_pos_ndc.device)
|
||||
uv_attr_proj = F.grid_sample(
|
||||
images.permute(0, 3, 1, 2),
|
||||
uv_render_geometry_output.uv_pos_ndc,
|
||||
align_corners=False,
|
||||
mode=grid_sample_mode,
|
||||
).permute(0, 2, 3, 1)
|
||||
|
||||
if masks is not None:
|
||||
masks = image_to_tensor(
|
||||
masks, device=uv_render_geometry_output.uv_pos_ndc.device
|
||||
)
|
||||
if masks.ndim == 4:
|
||||
masks = masks.mean(-1)
|
||||
uv_mask_proj = F.grid_sample(
|
||||
masks[..., None].permute(0, 3, 1, 2),
|
||||
uv_render_geometry_output.uv_pos_ndc,
|
||||
align_corners=False,
|
||||
mode=grid_sample_mode,
|
||||
).permute(0, 2, 3, 1)[..., 0]
|
||||
else:
|
||||
uv_mask_proj = None
|
||||
|
||||
return UVRenderAttrOutput(uv_attr_proj=uv_attr_proj, uv_mask_proj=uv_mask_proj)
|
||||
|
||||
|
||||
@dataclass
|
||||
class UVBlendOutput:
|
||||
uv_attr_blend: Optional[torch.Tensor]
|
||||
uv_valid_mask: torch.Tensor
|
||||
uv_valid_mask_blend: torch.Tensor
|
||||
uv_blend_weight: torch.Tensor
|
||||
|
||||
|
||||
class UVValidityStrategy(ABC):
|
||||
@abstractmethod
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __call__(
|
||||
self,
|
||||
uv_precompute_output: UVPrecomputeOutput,
|
||||
uv_render_geometry_output: UVRenderGeometryOutput,
|
||||
uv_render_attr_output: Optional[UVRenderAttrOutput],
|
||||
) -> torch.BoolTensor:
|
||||
pass
|
||||
|
||||
|
||||
class SimpleUVValidityStrategy(UVValidityStrategy):
|
||||
def __init__(
|
||||
self,
|
||||
pos_error_eps: float = 1e-3,
|
||||
aoi_cos_thresh: float = 0.1,
|
||||
mask_thresh: float = 0.9,
|
||||
depth_grad_thresh: Optional[float] = None,
|
||||
first_view_dominate: bool = False,
|
||||
):
|
||||
self.pos_error_eps = pos_error_eps
|
||||
self.aoi_cos_thresh = aoi_cos_thresh
|
||||
self.mask_thresh = mask_thresh
|
||||
self.depth_grad_thresh = depth_grad_thresh
|
||||
self.first_view_dominate = first_view_dominate
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
uv_precompute_output: UVPrecomputeOutput,
|
||||
uv_render_geometry_output: UVRenderGeometryOutput,
|
||||
uv_render_attr_output: Optional[UVRenderAttrOutput],
|
||||
) -> torch.BoolTensor:
|
||||
uv_pos_valid = uv_render_geometry_output.uv_pos_error < self.pos_error_eps
|
||||
uv_aoi_cos_valid = uv_render_geometry_output.uv_aoi_cos > self.aoi_cos_thresh
|
||||
uv_valid_mask = uv_pos_valid & uv_aoi_cos_valid
|
||||
|
||||
if self.depth_grad_thresh is not None:
|
||||
if uv_render_geometry_output.uv_depth_grad is None:
|
||||
print(
|
||||
"Warning: Depth gradient is not computed, depth gradient threshold is ignored."
|
||||
)
|
||||
else:
|
||||
uv_valid_mask &= (
|
||||
uv_render_geometry_output.uv_depth_grad < self.depth_grad_thresh
|
||||
)
|
||||
|
||||
uv_valid_mask &= uv_precompute_output.uv_mask
|
||||
|
||||
if (
|
||||
uv_render_attr_output is not None
|
||||
and uv_render_attr_output.uv_mask_proj is not None
|
||||
):
|
||||
uv_valid_mask &= uv_render_attr_output.uv_mask_proj > self.mask_thresh
|
||||
else:
|
||||
print("No view mask provided for UV blending, using all valid pixels")
|
||||
|
||||
if self.first_view_dominate:
|
||||
uv_valid_mask[1:][
|
||||
uv_valid_mask[0:1].expand(uv_valid_mask.shape[0] - 1, -1, -1)
|
||||
] = False
|
||||
|
||||
return uv_valid_mask
|
||||
|
||||
|
||||
class UVBlendWeightStrategy(ABC):
|
||||
@abstractmethod
|
||||
def __init__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def __call__(
|
||||
self,
|
||||
uv_precompute_output: UVPrecomputeOutput,
|
||||
uv_render_geometry_output: UVRenderGeometryOutput,
|
||||
uv_render_attr_output: Optional[UVRenderAttrOutput],
|
||||
uv_valid_mask: torch.BoolTensor,
|
||||
) -> torch.FloatTensor:
|
||||
pass
|
||||
|
||||
|
||||
class ExponentialBlend(UVBlendWeightStrategy):
|
||||
def __init__(
|
||||
self,
|
||||
alpha: float = 1.0,
|
||||
normalization: str = "linear",
|
||||
view_weight: Optional[torch.FloatTensor] = None,
|
||||
):
|
||||
self.alpha = alpha
|
||||
self.normalization = normalization
|
||||
self.view_weight = view_weight
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
uv_precompute_output: UVPrecomputeOutput,
|
||||
uv_render_geometry_output: UVRenderGeometryOutput,
|
||||
uv_render_attr_output: Optional[UVRenderAttrOutput],
|
||||
uv_valid_mask: torch.BoolTensor,
|
||||
) -> torch.FloatTensor:
|
||||
weight = uv_render_geometry_output.uv_aoi_cos
|
||||
weight = weight * uv_valid_mask.float()
|
||||
if self.view_weight is not None:
|
||||
weight = weight ** (self.alpha / self.view_weight[:, None, None].to(weight))
|
||||
else:
|
||||
weight = weight**self.alpha
|
||||
if self.normalization == "linear":
|
||||
weight_norm = (weight / weight.sum(axis=0, keepdim=True).clamp(1e-5)).clamp(
|
||||
0.0, 1.0
|
||||
)
|
||||
elif self.normalization == "softmax":
|
||||
weight[~uv_valid_mask] = -1e5
|
||||
weight_norm = F.softmax(weight, dim=0)
|
||||
return weight_norm
|
||||
|
||||
|
||||
class RandomChoiceBlend(UVBlendWeightStrategy):
|
||||
def __init__(self, alpha):
|
||||
self.alpha = alpha
|
||||
|
||||
def __call__(
|
||||
self,
|
||||
uv_precompute_output: UVPrecomputeOutput,
|
||||
uv_render_geometry_output: UVRenderGeometryOutput,
|
||||
uv_render_attr_output: Optional[UVRenderAttrOutput],
|
||||
uv_valid_mask: torch.BoolTensor,
|
||||
) -> torch.FloatTensor:
|
||||
weight = uv_render_geometry_output.uv_aoi_cos
|
||||
weight = weight * uv_valid_mask.float()
|
||||
weight[weight > 0] = torch.rand_like(weight[weight > 0])
|
||||
weight_norm = (
|
||||
F.one_hot(weight.max(dim=0).indices, num_classes=weight.shape[0])
|
||||
.float()
|
||||
.permute(2, 0, 1)
|
||||
)
|
||||
return weight_norm
|
||||
|
||||
|
||||
def uv_padding(attr: torch.FloatTensor, inside_mask: torch.BoolTensor, radius: int):
|
||||
if attr.min() < -1e-5 or attr.max() > 1 + 1e-5:
|
||||
print(
|
||||
"Warning: UV blending result is out of range [0, 1], force clamped before UV padding."
|
||||
)
|
||||
|
||||
from .cv_ops import inpaint_torch
|
||||
|
||||
attr_padded = inpaint_torch(attr.clamp(0.0, 1.0), ~inside_mask, radius)
|
||||
return attr_padded
|
||||
|
||||
|
||||
def uv_blend(
|
||||
uv_precompute_output: UVPrecomputeOutput,
|
||||
uv_render_geometry_output: UVRenderGeometryOutput,
|
||||
uv_render_attr_output: Optional[UVRenderAttrOutput],
|
||||
uv_validity_strategy: UVValidityStrategy = SimpleUVValidityStrategy(),
|
||||
uv_blend_weight_strategy: UVBlendWeightStrategy = ExponentialBlend(),
|
||||
empty_value: float = 0.0,
|
||||
do_uv_padding: bool = True,
|
||||
uv_padding_radius: int = 3,
|
||||
pad_unseen_area: bool = False,
|
||||
poisson_blending: bool = False,
|
||||
pb_solver: Optional[PoissonBlendingSolver] = None,
|
||||
pb_num_iters: int = 1000,
|
||||
pb_keep_original_border: bool = True,
|
||||
pb_inplace: bool = False,
|
||||
pb_grad_mode: str = "src",
|
||||
) -> UVBlendOutput:
|
||||
uv_valid_mask = uv_validity_strategy(
|
||||
uv_precompute_output, uv_render_geometry_output, uv_render_attr_output
|
||||
)
|
||||
uv_blend_weight = uv_blend_weight_strategy(
|
||||
uv_precompute_output,
|
||||
uv_render_geometry_output,
|
||||
uv_render_attr_output,
|
||||
uv_valid_mask,
|
||||
)
|
||||
uv_valid_mask_blend = uv_valid_mask.any(dim=0)
|
||||
|
||||
if uv_render_attr_output is None:
|
||||
return UVBlendOutput(
|
||||
uv_attr_blend=None,
|
||||
uv_valid_mask=uv_valid_mask,
|
||||
uv_valid_mask_blend=uv_valid_mask_blend,
|
||||
uv_blend_weight=uv_blend_weight,
|
||||
)
|
||||
|
||||
uv_attr_blend = (
|
||||
uv_render_attr_output.uv_attr_proj * uv_blend_weight[..., None]
|
||||
).sum(axis=0)
|
||||
if poisson_blending:
|
||||
assert do_uv_padding
|
||||
assert pb_solver is not None
|
||||
uv_attr_blend_padded = uv_padding(
|
||||
uv_attr_blend, uv_valid_mask_blend, uv_padding_radius
|
||||
)
|
||||
if pb_keep_original_border:
|
||||
pb_tgt = uv_precompute_output.uv_attr
|
||||
else:
|
||||
uv_attr_hard_stitch = (
|
||||
uv_attr_blend * uv_valid_mask_blend[..., None].float()
|
||||
+ uv_precompute_output.uv_attr
|
||||
* (~uv_valid_mask_blend)[..., None].float()
|
||||
)
|
||||
uv_attr_hard_stitch_padded = uv_padding(
|
||||
uv_attr_hard_stitch, uv_precompute_output.uv_mask, uv_padding_radius
|
||||
)
|
||||
pb_tgt = uv_attr_hard_stitch_padded
|
||||
|
||||
uv_attr_blend = pb_solver(
|
||||
uv_attr_blend_padded,
|
||||
uv_valid_mask_blend,
|
||||
pb_tgt,
|
||||
pb_num_iters,
|
||||
inplace=pb_inplace,
|
||||
grad_mode=pb_grad_mode,
|
||||
)
|
||||
else:
|
||||
uv_attr_blend = (
|
||||
uv_attr_blend * uv_valid_mask_blend[..., None].float()
|
||||
+ uv_precompute_output.uv_attr * (~uv_valid_mask_blend)[..., None].float()
|
||||
)
|
||||
|
||||
if do_uv_padding:
|
||||
content_mask = (
|
||||
uv_valid_mask_blend if pad_unseen_area else uv_precompute_output.uv_mask
|
||||
)
|
||||
uv_attr_blend = uv_padding(uv_attr_blend, content_mask, uv_padding_radius)
|
||||
|
||||
return UVBlendOutput(
|
||||
uv_attr_blend=uv_attr_blend,
|
||||
uv_valid_mask=uv_valid_mask,
|
||||
uv_valid_mask_blend=uv_valid_mask_blend,
|
||||
uv_blend_weight=uv_blend_weight,
|
||||
)
|
||||
@@ -0,0 +1,285 @@
|
||||
import os
|
||||
|
||||
import numpy as np
|
||||
import nvdiffrast.torch as dr
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
import trimesh
|
||||
from einops import rearrange
|
||||
from PIL import Image, ImageDraw
|
||||
from tqdm import tqdm
|
||||
|
||||
|
||||
def draw(img, lines):
|
||||
"""
|
||||
lines: n x 4, x1,y2,x2,y2
|
||||
"""
|
||||
if isinstance(img, torch.Tensor):
|
||||
img = img.detach().cpu().numpy()
|
||||
|
||||
if isinstance(img, np.ndarray):
|
||||
if img.dtype == np.float32 or img.dtype == np.float64:
|
||||
img = (img * 255.0).astype(np.uint8)
|
||||
img = Image.fromarray(img)
|
||||
|
||||
img = img.resize((512, 512), Image.Resampling.BICUBIC)
|
||||
w, h = img.size
|
||||
assert w == h
|
||||
if isinstance(lines, torch.Tensor):
|
||||
lines = lines.detach().cpu().numpy()
|
||||
lines = (lines + 1) / 2 * w
|
||||
|
||||
img1 = ImageDraw.Draw(img)
|
||||
for line in lines:
|
||||
img1.line(line.tolist(), fill="red", width=1)
|
||||
return img
|
||||
|
||||
|
||||
def construct_grid_mesh(n_grid):
|
||||
vertices = []
|
||||
vertices_movable_ids = []
|
||||
idx = 0
|
||||
for j in range(n_grid + 1):
|
||||
for i in range(n_grid + 1):
|
||||
# 3D fit in trimesh format
|
||||
if 0 < i < n_grid and 0 < j < n_grid:
|
||||
vertices_movable_ids.append(idx)
|
||||
vertices.append([i / n_grid, j / n_grid, 1.0 / 2])
|
||||
idx += 1
|
||||
|
||||
vertices = np.array(vertices)
|
||||
vertices = 2 * vertices - 1
|
||||
|
||||
faces = []
|
||||
for j in range(n_grid):
|
||||
for i in range(n_grid):
|
||||
# clockwise
|
||||
faces.append(
|
||||
[
|
||||
i + j * (n_grid + 1),
|
||||
i + 1 + j * (n_grid + 1),
|
||||
i + (j + 1) * (n_grid + 1),
|
||||
]
|
||||
)
|
||||
faces.append(
|
||||
[
|
||||
i + 1 + j * (n_grid + 1),
|
||||
i + 1 + (j + 1) * (n_grid + 1),
|
||||
i + (j + 1) * (n_grid + 1),
|
||||
]
|
||||
)
|
||||
faces = np.array(faces)
|
||||
vertices_movable_ids = np.array(vertices_movable_ids)
|
||||
|
||||
mesh = trimesh.Trimesh(vertices=vertices, faces=faces, process=False)
|
||||
return mesh, vertices_movable_ids, vertices
|
||||
|
||||
|
||||
def compute_warp_field(
|
||||
ctx,
|
||||
src_images_tensor,
|
||||
tgt_images_tensor,
|
||||
n_grid,
|
||||
optim_res,
|
||||
optim_step_per_res,
|
||||
lambda_reg,
|
||||
temp_dir,
|
||||
verbose,
|
||||
device,
|
||||
):
|
||||
"""
|
||||
src_images_tensor: 4 H W 4
|
||||
tgt_images_tensor: 4 H W 4
|
||||
return: 4 H W 4
|
||||
"""
|
||||
lam_reg = lambda_reg
|
||||
lam_mask = 2
|
||||
mesh, _, vertices_np = construct_grid_mesh(n_grid)
|
||||
|
||||
# vertices = mesh.vertices
|
||||
faces = mesh.faces
|
||||
edges = mesh.edges_unique
|
||||
|
||||
n_degree = torch.tensor(mesh.vertex_degree)
|
||||
vertices = torch.tensor(vertices_np, device=device, dtype=torch.float32)
|
||||
vertices_unopt = vertices.clone()
|
||||
faces = torch.tensor(faces, device=device)
|
||||
edges = torch.tensor(edges, device=device)
|
||||
|
||||
warped_images_tensor = []
|
||||
bs = src_images_tensor.shape[0]
|
||||
|
||||
for img_idx in range(bs):
|
||||
src_image_tensor = src_images_tensor[img_idx]
|
||||
tgt_image_tensor = tgt_images_tensor[img_idx]
|
||||
|
||||
if verbose and temp_dir is not None:
|
||||
vis_dir = os.path.join(temp_dir, f"{img_idx}")
|
||||
os.makedirs(vis_dir, exist_ok=True)
|
||||
# prepare for nvdiff rendering
|
||||
v_origin_homo = torch.cat(
|
||||
[vertices_unopt, torch.ones_like(vertices_unopt[..., :1])], dim=-1
|
||||
)
|
||||
|
||||
# prepare parameters
|
||||
v_move_indices = torch.where(n_degree == 6)[0]
|
||||
vertices_movable = vertices[v_move_indices].detach().clone()
|
||||
vertices_movable.requires_grad = True
|
||||
opt = torch.optim.Adam([vertices_movable], lr=0.02)
|
||||
|
||||
for resl in optim_res:
|
||||
rast, _ = dr.rasterize(
|
||||
ctx,
|
||||
v_origin_homo[None, ...].float(),
|
||||
faces.int(),
|
||||
(resl, resl),
|
||||
grad_db=True,
|
||||
)
|
||||
face_ids = rast[..., 3].long() - 1
|
||||
assert torch.all(face_ids >= 0)
|
||||
u = rast[..., 0]
|
||||
v = rast[..., 1]
|
||||
pixel_vertice_ids = faces[face_ids]
|
||||
pixel_bary_coords = torch.stack([u, v, 1 - u - v], dim=-1)
|
||||
|
||||
src_img = F.interpolate(
|
||||
rearrange(src_image_tensor[..., :3], "H W C -> () C H W"),
|
||||
size=(resl, resl),
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
antialias=True,
|
||||
)
|
||||
|
||||
tgt_img = F.interpolate(
|
||||
rearrange(tgt_image_tensor[..., :3], "H W C -> () C H W"),
|
||||
size=(resl, resl),
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
antialias=True,
|
||||
)
|
||||
|
||||
# src_img, tgt_img: 1 C H W
|
||||
if verbose:
|
||||
Image.fromarray(
|
||||
(tgt_img[0].permute(1, 2, 0).detach().cpu().numpy() * 255.0).astype(
|
||||
np.uint8
|
||||
)
|
||||
).save(os.path.join(vis_dir, f"target_{resl:04d}.png"))
|
||||
with tqdm(range(optim_step_per_res), disable=not verbose) as pbar:
|
||||
for i in pbar:
|
||||
opt.zero_grad()
|
||||
vertices_all = vertices_unopt.detach().clone()
|
||||
vertices_all[v_move_indices] = vertices_movable
|
||||
|
||||
pixel_vertices = vertices_all[
|
||||
pixel_vertice_ids
|
||||
] # 1 x 512 x 512 x 3 x 3
|
||||
pixel_coords_inter = torch.sum(
|
||||
pixel_vertices * pixel_bary_coords[..., None], dim=-2
|
||||
)
|
||||
src_img_warped = F.grid_sample(
|
||||
src_img,
|
||||
pixel_coords_inter[..., :2],
|
||||
mode="bilinear",
|
||||
align_corners=False,
|
||||
)
|
||||
|
||||
edge_vertices_all = vertices_all[edges]
|
||||
edge_vertices_unopt = vertices_unopt[edges]
|
||||
|
||||
edge_len_all = torch.linalg.norm(
|
||||
edge_vertices_all[:, 0, :2] - edge_vertices_all[:, 1, :2],
|
||||
dim=-1,
|
||||
)
|
||||
edge_len_unopt = torch.linalg.norm(
|
||||
edge_vertices_unopt[:, 0, :2] - edge_vertices_all[:, 1, :2],
|
||||
dim=-1,
|
||||
)
|
||||
|
||||
reg_loss = ((edge_len_all - edge_len_unopt) ** 2).mean()
|
||||
|
||||
# mask_loss = ((src_img_warped[:, 3:4, :, :] - tgt_img[:, 3:4, :, :])**2).mean()
|
||||
# rgb_loss = (((src_img_warped[:, :3, :, :] - tgt_img[:, :3, :, :]) * tgt_img[:, 3:4, :, :])**2).mean()
|
||||
# loss = rgb_loss + lam_reg * reg_loss + lam_mask * mask_loss
|
||||
img_loss = ((src_img_warped - tgt_img) ** 2).mean()
|
||||
loss = img_loss + lam_reg * reg_loss
|
||||
|
||||
loss.backward()
|
||||
opt.step()
|
||||
if verbose:
|
||||
print(img_loss.item(), reg_loss.item())
|
||||
|
||||
if verbose:
|
||||
draw(
|
||||
src_img[0].permute(1, 2, 0),
|
||||
edge_vertices_all[:, :, :2].reshape(-1, 4),
|
||||
).save(os.path.join(vis_dir, f"src_{resl:04d}_{i:03d}.png"))
|
||||
Image.fromarray(
|
||||
(
|
||||
torch.cat(
|
||||
[
|
||||
tgt_img,
|
||||
src_img_warped,
|
||||
(tgt_img - src_img_warped).abs(),
|
||||
],
|
||||
dim=-1,
|
||||
)[0]
|
||||
.permute(1, 2, 0)
|
||||
.detach()
|
||||
.cpu()
|
||||
.numpy()
|
||||
* 255.0
|
||||
).astype(np.uint8)
|
||||
).save(os.path.join(vis_dir, f"opt_{resl:04d}_{i:03d}.png"))
|
||||
warped_this_resl = Image.fromarray(
|
||||
(
|
||||
src_img_warped[0]
|
||||
.permute(1, 2, 0)
|
||||
.detach()
|
||||
.cpu()
|
||||
.numpy()
|
||||
* 255.0
|
||||
).astype(np.uint8)
|
||||
)
|
||||
warped_this_resl.save(
|
||||
os.path.join(vis_dir, f"warped_{resl:04d}_{i:03d}.png")
|
||||
)
|
||||
|
||||
# warp on 512 resolution
|
||||
resl = src_image_tensor.shape[1]
|
||||
with torch.no_grad():
|
||||
rast, _ = dr.rasterize(
|
||||
ctx,
|
||||
v_origin_homo[None, ...].float(),
|
||||
faces.int(),
|
||||
(resl, resl),
|
||||
grad_db=True,
|
||||
)
|
||||
face_ids = rast[..., 3].long() - 1
|
||||
assert torch.all(face_ids >= 0)
|
||||
u = rast[..., 0]
|
||||
v = rast[..., 1]
|
||||
pixel_vertice_ids = faces[face_ids]
|
||||
pixel_bary_coords = torch.stack([u, v, 1 - u - v], dim=-1)
|
||||
|
||||
vertices_all = vertices_unopt.detach().clone()
|
||||
vertices_all[v_move_indices] = vertices_movable
|
||||
|
||||
pixel_vertices = vertices_all[pixel_vertice_ids] # 1 x 512 x 512 x 3 x 3
|
||||
pixel_coords_inter = torch.sum(
|
||||
pixel_vertices * pixel_bary_coords[..., None], dim=-2
|
||||
)
|
||||
src_img_warped = rearrange(
|
||||
F.grid_sample(
|
||||
rearrange(src_image_tensor, "H W C -> () C H W"),
|
||||
pixel_coords_inter[..., :2],
|
||||
mode="bicubic",
|
||||
align_corners=False,
|
||||
),
|
||||
"() C H W -> H W C",
|
||||
).clamp(0, 1)
|
||||
warped_images_tensor.append(src_img_warped)
|
||||
|
||||
warped_images_tensor = torch.stack(warped_images_tensor, dim=0) # 4 H W 3
|
||||
|
||||
return warped_images_tensor
|
||||
@@ -0,0 +1,220 @@
|
||||
import gc
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
|
||||
import psutil
|
||||
import torch
|
||||
from packaging import version
|
||||
|
||||
from .config import config_to_primitive
|
||||
from .core import debug, find, info, warn
|
||||
from .typing import *
|
||||
|
||||
|
||||
def parse_version(ver: str):
|
||||
return version.parse(ver)
|
||||
|
||||
|
||||
def get_rank():
|
||||
# SLURM_PROCID can be set even if SLURM is not managing the multiprocessing,
|
||||
# therefore LOCAL_RANK needs to be checked first
|
||||
rank_keys = ("RANK", "LOCAL_RANK", "SLURM_PROCID", "JSM_NAMESPACE_RANK")
|
||||
for key in rank_keys:
|
||||
rank = os.environ.get(key)
|
||||
if rank is not None:
|
||||
return int(rank)
|
||||
return 0
|
||||
|
||||
|
||||
def get_device():
|
||||
return torch.device(f"cuda:{get_rank()}")
|
||||
|
||||
|
||||
def load_module_weights(
|
||||
path, module_name=None, ignore_modules=None, mapping=None, map_location=None
|
||||
) -> Tuple[dict, int, int]:
|
||||
if module_name is not None and ignore_modules is not None:
|
||||
raise ValueError("module_name and ignore_modules cannot be both set")
|
||||
if map_location is None:
|
||||
map_location = get_device()
|
||||
|
||||
ckpt = torch.load(path, map_location=map_location)
|
||||
state_dict = ckpt["state_dict"]
|
||||
|
||||
if mapping is not None:
|
||||
state_dict_to_load = {}
|
||||
for k, v in state_dict.items():
|
||||
if any([k.startswith(m["to"]) for m in mapping]):
|
||||
pass
|
||||
else:
|
||||
state_dict_to_load[k] = v
|
||||
for k, v in state_dict.items():
|
||||
for m in mapping:
|
||||
if k.startswith(m["from"]):
|
||||
k_dest = k.replace(m["from"], m["to"])
|
||||
info(f"Mapping {k} => {k_dest}")
|
||||
state_dict_to_load[k_dest] = v.clone()
|
||||
state_dict = state_dict_to_load
|
||||
|
||||
state_dict_to_load = state_dict
|
||||
|
||||
if ignore_modules is not None:
|
||||
state_dict_to_load = {}
|
||||
for k, v in state_dict.items():
|
||||
ignore = any(
|
||||
[k.startswith(ignore_module + ".") for ignore_module in ignore_modules]
|
||||
)
|
||||
if ignore:
|
||||
continue
|
||||
state_dict_to_load[k] = v
|
||||
|
||||
if module_name is not None:
|
||||
state_dict_to_load = {}
|
||||
for k, v in state_dict.items():
|
||||
m = re.match(rf"^{module_name}\.(.*)$", k)
|
||||
if m is None:
|
||||
continue
|
||||
state_dict_to_load[m.group(1)] = v
|
||||
|
||||
return state_dict_to_load, ckpt["epoch"], ckpt["global_step"]
|
||||
|
||||
|
||||
def C(value: Any, epoch: int, global_step: int) -> float:
|
||||
if isinstance(value, int) or isinstance(value, float):
|
||||
pass
|
||||
else:
|
||||
value = config_to_primitive(value)
|
||||
if not isinstance(value, list):
|
||||
raise TypeError("Scalar specification only supports list, got", type(value))
|
||||
if len(value) == 3:
|
||||
value = [0] + value
|
||||
assert len(value) == 4
|
||||
start_step, start_value, end_value, end_step = value
|
||||
if isinstance(end_step, int):
|
||||
current_step = global_step
|
||||
value = start_value + (end_value - start_value) * max(
|
||||
min(1.0, (current_step - start_step) / (end_step - start_step)), 0.0
|
||||
)
|
||||
elif isinstance(end_step, float):
|
||||
current_step = epoch
|
||||
value = start_value + (end_value - start_value) * max(
|
||||
min(1.0, (current_step - start_step) / (end_step - start_step)), 0.0
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def cleanup():
|
||||
gc.collect()
|
||||
torch.cuda.empty_cache()
|
||||
try:
|
||||
import tinycudann as tcnn
|
||||
|
||||
tcnn.free_temporary_memory()
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def finish_with_cleanup(func: Callable):
|
||||
def wrapper(*args, **kwargs):
|
||||
out = func(*args, **kwargs)
|
||||
cleanup()
|
||||
return out
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
def _distributed_available():
|
||||
return torch.distributed.is_available() and torch.distributed.is_initialized()
|
||||
|
||||
|
||||
def barrier():
|
||||
if not _distributed_available():
|
||||
return
|
||||
else:
|
||||
torch.distributed.barrier()
|
||||
|
||||
|
||||
def broadcast(tensor, src=0):
|
||||
if not _distributed_available():
|
||||
return tensor
|
||||
else:
|
||||
torch.distributed.broadcast(tensor, src=src)
|
||||
return tensor
|
||||
|
||||
|
||||
def enable_gradient(model, enabled: bool = True) -> None:
|
||||
for param in model.parameters():
|
||||
param.requires_grad_(enabled)
|
||||
|
||||
|
||||
class TimeRecorder:
|
||||
_instance = None
|
||||
|
||||
def __init__(self):
|
||||
self.items = {}
|
||||
self.accumulations = defaultdict(list)
|
||||
self.time_scale = 1000.0 # ms
|
||||
self.time_unit = "ms"
|
||||
self.enabled = False
|
||||
|
||||
def __new__(cls):
|
||||
# singleton
|
||||
if cls._instance is None:
|
||||
cls._instance = super(TimeRecorder, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
def enable(self, enabled: bool) -> None:
|
||||
self.enabled = enabled
|
||||
|
||||
def start(self, name: str) -> None:
|
||||
if not self.enabled:
|
||||
return
|
||||
torch.cuda.synchronize()
|
||||
self.items[name] = time.time()
|
||||
|
||||
def end(self, name: str, accumulate: bool = False) -> float:
|
||||
if not self.enabled or name not in self.items:
|
||||
return
|
||||
torch.cuda.synchronize()
|
||||
start_time = self.items.pop(name)
|
||||
delta = time.time() - start_time
|
||||
if accumulate:
|
||||
self.accumulations[name].append(delta)
|
||||
t = delta * self.time_scale
|
||||
info(f"{name}: {t:.2f}{self.time_unit}")
|
||||
|
||||
def get_accumulation(self, name: str, average: bool = False) -> float:
|
||||
if not self.enabled or name not in self.accumulations:
|
||||
return
|
||||
acc = self.accumulations.pop(name)
|
||||
total = sum(acc)
|
||||
if average:
|
||||
t = total / len(acc) * self.time_scale
|
||||
else:
|
||||
t = total * self.time_scale
|
||||
info(f"{name} for {len(acc)} times: {t:.2f}{self.time_unit}")
|
||||
|
||||
|
||||
### global time recorder
|
||||
time_recorder = TimeRecorder()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def time_recorder_enabled():
|
||||
enabled = time_recorder.enabled
|
||||
time_recorder.enable(enabled=True)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
time_recorder.enable(enabled=enabled)
|
||||
|
||||
|
||||
def show_vram_usage(name):
|
||||
available, total = torch.cuda.mem_get_info()
|
||||
used = total - available
|
||||
print(
|
||||
f"{name}: {used / 1024**2:.1f}MB, {psutil.Process(os.getpid()).memory_info().rss / 1024**2:.1f}MB"
|
||||
)
|
||||
@@ -0,0 +1,462 @@
|
||||
import math
|
||||
from collections import defaultdict
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from torch.autograd import Function
|
||||
from torch.cuda.amp import custom_bwd, custom_fwd
|
||||
|
||||
from .core import debug, find, info, warn
|
||||
from .typing import *
|
||||
|
||||
|
||||
def dot(x, y):
|
||||
return torch.sum(x * y, -1, keepdim=True)
|
||||
|
||||
|
||||
def reflect(x, n):
|
||||
return 2 * dot(x, n) * n - x
|
||||
|
||||
|
||||
ValidScale = Union[Tuple[float, float], Num[Tensor, "2 D"]]
|
||||
|
||||
|
||||
def scale_tensor(
|
||||
dat: Num[Tensor, "... D"], inp_scale: ValidScale, tgt_scale: ValidScale
|
||||
):
|
||||
if inp_scale is None:
|
||||
inp_scale = (0, 1)
|
||||
if tgt_scale is None:
|
||||
tgt_scale = (0, 1)
|
||||
if isinstance(tgt_scale, Tensor):
|
||||
assert dat.shape[-1] == tgt_scale.shape[-1]
|
||||
dat = (dat - inp_scale[0]) / (inp_scale[1] - inp_scale[0])
|
||||
dat = dat * (tgt_scale[1] - tgt_scale[0]) + tgt_scale[0]
|
||||
return dat
|
||||
|
||||
|
||||
class _TruncExp(Function): # pylint: disable=abstract-method
|
||||
# Implementation from torch-ngp:
|
||||
# https://github.com/ashawkey/torch-ngp/blob/93b08a0d4ec1cc6e69d85df7f0acdfb99603b628/activation.py
|
||||
@staticmethod
|
||||
@custom_fwd(cast_inputs=torch.float32)
|
||||
def forward(ctx, x): # pylint: disable=arguments-differ
|
||||
ctx.save_for_backward(x)
|
||||
return torch.exp(x)
|
||||
|
||||
@staticmethod
|
||||
@custom_bwd
|
||||
def backward(ctx, g): # pylint: disable=arguments-differ
|
||||
x = ctx.saved_tensors[0]
|
||||
return g * torch.exp(torch.clamp(x, max=15))
|
||||
|
||||
|
||||
trunc_exp = _TruncExp.apply
|
||||
|
||||
|
||||
def get_activation(name) -> Callable:
|
||||
if name is None:
|
||||
return lambda x: x
|
||||
name = name.lower()
|
||||
if name == "none":
|
||||
return lambda x: x
|
||||
elif name == "lin2srgb":
|
||||
return lambda x: torch.where(
|
||||
x > 0.0031308,
|
||||
torch.pow(torch.clamp(x, min=0.0031308), 1.0 / 2.4) * 1.055 - 0.055,
|
||||
12.92 * x,
|
||||
).clamp(0.0, 1.0)
|
||||
elif name == "exp":
|
||||
return lambda x: torch.exp(x)
|
||||
elif name == "shifted_exp":
|
||||
return lambda x: torch.exp(x - 1.0)
|
||||
elif name == "trunc_exp":
|
||||
return trunc_exp
|
||||
elif name == "shifted_trunc_exp":
|
||||
return lambda x: trunc_exp(x - 1.0)
|
||||
elif name == "sigmoid":
|
||||
return lambda x: torch.sigmoid(x)
|
||||
elif name == "tanh":
|
||||
return lambda x: torch.tanh(x)
|
||||
elif name == "shifted_softplus":
|
||||
return lambda x: F.softplus(x - 1.0)
|
||||
elif name == "scale_-11_01":
|
||||
return lambda x: x * 0.5 + 0.5
|
||||
elif name == "negative":
|
||||
return lambda x: -x
|
||||
else:
|
||||
try:
|
||||
return getattr(F, name)
|
||||
except AttributeError:
|
||||
raise ValueError(f"Unknown activation function: {name}")
|
||||
|
||||
|
||||
def chunk_batch(func: Callable, chunk_size: int, *args, **kwargs) -> Any:
|
||||
if chunk_size <= 0:
|
||||
return func(*args, **kwargs)
|
||||
B = None
|
||||
for arg in list(args) + list(kwargs.values()):
|
||||
if isinstance(arg, torch.Tensor):
|
||||
B = arg.shape[0]
|
||||
break
|
||||
assert (
|
||||
B is not None
|
||||
), "No tensor found in args or kwargs, cannot determine batch size."
|
||||
out = defaultdict(list)
|
||||
out_type = None
|
||||
# max(1, B) to support B == 0
|
||||
for i in range(0, max(1, B), chunk_size):
|
||||
out_chunk = func(
|
||||
*[
|
||||
arg[i : i + chunk_size] if isinstance(arg, torch.Tensor) else arg
|
||||
for arg in args
|
||||
],
|
||||
**{
|
||||
k: arg[i : i + chunk_size] if isinstance(arg, torch.Tensor) else arg
|
||||
for k, arg in kwargs.items()
|
||||
},
|
||||
)
|
||||
if out_chunk is None:
|
||||
continue
|
||||
out_type = type(out_chunk)
|
||||
if isinstance(out_chunk, torch.Tensor):
|
||||
out_chunk = {0: out_chunk}
|
||||
elif isinstance(out_chunk, tuple) or isinstance(out_chunk, list):
|
||||
chunk_length = len(out_chunk)
|
||||
out_chunk = {i: chunk for i, chunk in enumerate(out_chunk)}
|
||||
elif isinstance(out_chunk, dict):
|
||||
pass
|
||||
else:
|
||||
print(
|
||||
f"Return value of func must be in type [torch.Tensor, list, tuple, dict], get {type(out_chunk)}."
|
||||
)
|
||||
exit(1)
|
||||
for k, v in out_chunk.items():
|
||||
v = v if torch.is_grad_enabled() else v.detach()
|
||||
out[k].append(v)
|
||||
|
||||
if out_type is None:
|
||||
return None
|
||||
|
||||
out_merged: Dict[Any, Optional[torch.Tensor]] = {}
|
||||
for k, v in out.items():
|
||||
if all([vv is None for vv in v]):
|
||||
# allow None in return value
|
||||
out_merged[k] = None
|
||||
elif all([isinstance(vv, torch.Tensor) for vv in v]):
|
||||
out_merged[k] = torch.cat(v, dim=0)
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Unsupported types in return value of func: {[type(vv) for vv in v if not isinstance(vv, torch.Tensor)]}"
|
||||
)
|
||||
|
||||
if out_type is torch.Tensor:
|
||||
return out_merged[0]
|
||||
elif out_type in [tuple, list]:
|
||||
return out_type([out_merged[i] for i in range(chunk_length)])
|
||||
elif out_type is dict:
|
||||
return out_merged
|
||||
|
||||
|
||||
def get_ray_directions(
|
||||
H: int,
|
||||
W: int,
|
||||
focal: Union[float, Tuple[float, float]],
|
||||
principal: Optional[Tuple[float, float]] = None,
|
||||
use_pixel_centers: bool = True,
|
||||
normalize: bool = True,
|
||||
) -> Float[Tensor, "H W 3"]:
|
||||
"""
|
||||
Get ray directions for all pixels in camera coordinate.
|
||||
Reference: https://www.scratchapixel.com/lessons/3d-basic-rendering/
|
||||
ray-tracing-generating-camera-rays/standard-coordinate-systems
|
||||
|
||||
Inputs:
|
||||
H, W, focal, principal, use_pixel_centers: image height, width, focal length, principal point and whether use pixel centers
|
||||
Outputs:
|
||||
directions: (H, W, 3), the direction of the rays in camera coordinate
|
||||
"""
|
||||
pixel_center = 0.5 if use_pixel_centers else 0
|
||||
|
||||
if isinstance(focal, float):
|
||||
fx, fy = focal, focal
|
||||
cx, cy = W / 2, H / 2
|
||||
else:
|
||||
fx, fy = focal
|
||||
assert principal is not None
|
||||
cx, cy = principal
|
||||
|
||||
i, j = torch.meshgrid(
|
||||
torch.arange(W, dtype=torch.float32) + pixel_center,
|
||||
torch.arange(H, dtype=torch.float32) + pixel_center,
|
||||
indexing="xy",
|
||||
)
|
||||
|
||||
directions: Float[Tensor, "H W 3"] = torch.stack(
|
||||
[(i - cx) / fx, -(j - cy) / fy, -torch.ones_like(i)], -1
|
||||
)
|
||||
|
||||
if normalize:
|
||||
directions = F.normalize(directions, dim=-1)
|
||||
|
||||
return directions
|
||||
|
||||
|
||||
def get_rays(
|
||||
directions: Float[Tensor, "... 3"],
|
||||
c2w: Float[Tensor, "... 4 4"],
|
||||
keepdim=False,
|
||||
noise_scale=0.0,
|
||||
normalize=False,
|
||||
) -> Tuple[Float[Tensor, "... 3"], Float[Tensor, "... 3"]]:
|
||||
# Rotate ray directions from camera coordinate to the world coordinate
|
||||
assert directions.shape[-1] == 3
|
||||
|
||||
if directions.ndim == 2: # (N_rays, 3)
|
||||
if c2w.ndim == 2: # (4, 4)
|
||||
c2w = c2w[None, :, :]
|
||||
assert c2w.ndim == 3 # (N_rays, 4, 4) or (1, 4, 4)
|
||||
rays_d = (directions[:, None, :] * c2w[:, :3, :3]).sum(-1) # (N_rays, 3)
|
||||
rays_o = c2w[:, :3, 3].expand(rays_d.shape)
|
||||
elif directions.ndim == 3: # (H, W, 3)
|
||||
assert c2w.ndim in [2, 3]
|
||||
if c2w.ndim == 2: # (4, 4)
|
||||
rays_d = (directions[:, :, None, :] * c2w[None, None, :3, :3]).sum(
|
||||
-1
|
||||
) # (H, W, 3)
|
||||
rays_o = c2w[None, None, :3, 3].expand(rays_d.shape)
|
||||
elif c2w.ndim == 3: # (B, 4, 4)
|
||||
rays_d = (directions[None, :, :, None, :] * c2w[:, None, None, :3, :3]).sum(
|
||||
-1
|
||||
) # (B, H, W, 3)
|
||||
rays_o = c2w[:, None, None, :3, 3].expand(rays_d.shape)
|
||||
elif directions.ndim == 4: # (B, H, W, 3)
|
||||
assert c2w.ndim == 3 # (B, 4, 4)
|
||||
rays_d = (directions[:, :, :, None, :] * c2w[:, None, None, :3, :3]).sum(
|
||||
-1
|
||||
) # (B, H, W, 3)
|
||||
rays_o = c2w[:, None, None, :3, 3].expand(rays_d.shape)
|
||||
|
||||
# add camera noise to avoid grid-like artifect
|
||||
# https://github.com/ashawkey/stable-dreamfusion/blob/49c3d4fa01d68a4f027755acf94e1ff6020458cc/nerf/utils.py#L373
|
||||
if noise_scale > 0:
|
||||
rays_o = rays_o + torch.randn(3, device=rays_o.device) * noise_scale
|
||||
rays_d = rays_d + torch.randn(3, device=rays_d.device) * noise_scale
|
||||
|
||||
if normalize:
|
||||
rays_d = F.normalize(rays_d, dim=-1)
|
||||
if not keepdim:
|
||||
rays_o, rays_d = rays_o.reshape(-1, 3), rays_d.reshape(-1, 3)
|
||||
|
||||
return rays_o, rays_d
|
||||
|
||||
|
||||
def get_projection_matrix(
|
||||
fovy: Union[float, Float[Tensor, "B"]], aspect_wh: float, near: float, far: float
|
||||
) -> Float[Tensor, "*B 4 4"]:
|
||||
if isinstance(fovy, float):
|
||||
proj_mtx = torch.zeros(4, 4, dtype=torch.float32)
|
||||
proj_mtx[0, 0] = 1.0 / (math.tan(fovy / 2.0) * aspect_wh)
|
||||
proj_mtx[1, 1] = -1.0 / math.tan(
|
||||
fovy / 2.0
|
||||
) # add a negative sign here as the y axis is flipped in nvdiffrast output
|
||||
proj_mtx[2, 2] = -(far + near) / (far - near)
|
||||
proj_mtx[2, 3] = -2.0 * far * near / (far - near)
|
||||
proj_mtx[3, 2] = -1.0
|
||||
else:
|
||||
batch_size = fovy.shape[0]
|
||||
proj_mtx = torch.zeros(batch_size, 4, 4, dtype=torch.float32)
|
||||
proj_mtx[:, 0, 0] = 1.0 / (torch.tan(fovy / 2.0) * aspect_wh)
|
||||
proj_mtx[:, 1, 1] = -1.0 / torch.tan(
|
||||
fovy / 2.0
|
||||
) # add a negative sign here as the y axis is flipped in nvdiffrast output
|
||||
proj_mtx[:, 2, 2] = -(far + near) / (far - near)
|
||||
proj_mtx[:, 2, 3] = -2.0 * far * near / (far - near)
|
||||
proj_mtx[:, 3, 2] = -1.0
|
||||
return proj_mtx
|
||||
|
||||
|
||||
def get_mvp_matrix(
|
||||
c2w: Float[Tensor, "*B 4 4"], proj_mtx: Float[Tensor, "*B 4 4"]
|
||||
) -> Float[Tensor, "*B 4 4"]:
|
||||
# calculate w2c from c2w: R' = Rt, t' = -Rt * t
|
||||
# mathematically equivalent to (c2w)^-1
|
||||
if c2w.ndim == 2:
|
||||
assert proj_mtx.ndim == 2
|
||||
w2c: Float[Tensor, "4 4"] = torch.zeros(4, 4).to(c2w)
|
||||
w2c[:3, :3] = c2w[:3, :3].permute(1, 0)
|
||||
w2c[:3, 3:] = -c2w[:3, :3].permute(1, 0) @ c2w[:3, 3:]
|
||||
w2c[3, 3] = 1.0
|
||||
else:
|
||||
w2c: Float[Tensor, "B 4 4"] = torch.zeros(c2w.shape[0], 4, 4).to(c2w)
|
||||
w2c[:, :3, :3] = c2w[:, :3, :3].permute(0, 2, 1)
|
||||
w2c[:, :3, 3:] = -c2w[:, :3, :3].permute(0, 2, 1) @ c2w[:, :3, 3:]
|
||||
w2c[:, 3, 3] = 1.0
|
||||
# calculate mvp matrix by proj_mtx @ w2c (mv_mtx)
|
||||
mvp_mtx = proj_mtx @ w2c
|
||||
return mvp_mtx
|
||||
|
||||
|
||||
def get_intrinsic_from_fov(fov, H, W, bs=-1):
|
||||
focal_length = 0.5 * H / np.tan(0.5 * fov)
|
||||
intrinsic = np.identity(3, dtype=np.float32)
|
||||
intrinsic[0, 0] = focal_length
|
||||
intrinsic[1, 1] = focal_length
|
||||
intrinsic[0, 2] = W / 2.0
|
||||
intrinsic[1, 2] = H / 2.0
|
||||
|
||||
if bs > 0:
|
||||
intrinsic = intrinsic[None].repeat(bs, axis=0)
|
||||
|
||||
return torch.from_numpy(intrinsic)
|
||||
|
||||
|
||||
def binary_cross_entropy(input, target):
|
||||
"""
|
||||
F.binary_cross_entropy is not numerically stable in mixed-precision training.
|
||||
"""
|
||||
return -(target * torch.log(input) + (1 - target) * torch.log(1 - input)).mean()
|
||||
|
||||
|
||||
def tet_sdf_diff(
|
||||
vert_sdf: Float[Tensor, "Nv 1"], tet_edges: Integer[Tensor, "Ne 2"]
|
||||
) -> Float[Tensor, ""]:
|
||||
sdf_f1x6x2 = vert_sdf[:, 0][tet_edges.reshape(-1)].reshape(-1, 2)
|
||||
mask = torch.sign(sdf_f1x6x2[..., 0]) != torch.sign(sdf_f1x6x2[..., 1])
|
||||
sdf_f1x6x2 = sdf_f1x6x2[mask]
|
||||
sdf_diff = F.binary_cross_entropy_with_logits(
|
||||
sdf_f1x6x2[..., 0], (sdf_f1x6x2[..., 1] > 0).float()
|
||||
) + F.binary_cross_entropy_with_logits(
|
||||
sdf_f1x6x2[..., 1], (sdf_f1x6x2[..., 0] > 0).float()
|
||||
)
|
||||
return sdf_diff
|
||||
|
||||
|
||||
def validate_empty_rays(ray_indices, t_start, t_end):
|
||||
if ray_indices.nelement() == 0:
|
||||
warn("Empty rays_indices!")
|
||||
ray_indices = torch.LongTensor([0]).to(ray_indices)
|
||||
t_start = torch.Tensor([0]).to(ray_indices)
|
||||
t_end = torch.Tensor([0]).to(ray_indices)
|
||||
return ray_indices, t_start, t_end
|
||||
|
||||
|
||||
def rays_intersect_bbox(
|
||||
rays_o: Float[Tensor, "N 3"],
|
||||
rays_d: Float[Tensor, "N 3"],
|
||||
radius: Float,
|
||||
near: Float = 0.0,
|
||||
valid_thresh: Float = 0.01,
|
||||
background: bool = False,
|
||||
):
|
||||
input_shape = rays_o.shape[:-1]
|
||||
rays_o, rays_d = rays_o.view(-1, 3), rays_d.view(-1, 3)
|
||||
rays_d_valid = torch.where(
|
||||
rays_d.abs() < 1e-6, torch.full_like(rays_d, 1e-6), rays_d
|
||||
)
|
||||
if type(radius) in [int, float]:
|
||||
radius = torch.FloatTensor(
|
||||
[[-radius, radius], [-radius, radius], [-radius, radius]]
|
||||
).to(rays_o.device)
|
||||
radius = (
|
||||
1.0 - 1.0e-3
|
||||
) * radius # tighten the radius to make sure the intersection point lies in the bounding box
|
||||
interx0 = (radius[..., 1] - rays_o) / rays_d_valid
|
||||
interx1 = (radius[..., 0] - rays_o) / rays_d_valid
|
||||
t_near = torch.minimum(interx0, interx1).amax(dim=-1).clamp_min(near)
|
||||
t_far = torch.maximum(interx0, interx1).amin(dim=-1)
|
||||
|
||||
# check wheter a ray intersects the bbox or not
|
||||
rays_valid = t_far - t_near > valid_thresh
|
||||
|
||||
t_near_valid, t_far_valid = t_near[rays_valid], t_far[rays_valid]
|
||||
global_near = t_near_valid.min().item()
|
||||
global_far = t_far_valid.max().item()
|
||||
|
||||
t_near[torch.where(~rays_valid)] = 0.0
|
||||
t_far[torch.where(~rays_valid)] = 0.0
|
||||
|
||||
t_near = t_near.view(*input_shape, 1)
|
||||
t_far = t_far.view(*input_shape, 1)
|
||||
rays_valid = rays_valid.view(*input_shape)
|
||||
|
||||
return t_near, t_far, rays_valid
|
||||
|
||||
|
||||
def get_plucker_rays(
|
||||
rays_o: Float[Tensor, "*N 3"], rays_d: Float[Tensor, "*N 3"]
|
||||
) -> Float[Tensor, "*N 6"]:
|
||||
rays_o = F.normalize(rays_o, dim=-1)
|
||||
rays_d = F.normalize(rays_d, dim=-1)
|
||||
return torch.cat([rays_o.cross(rays_d), rays_d], dim=-1)
|
||||
|
||||
|
||||
def c2w_to_polar(c2w: Float[Tensor, "4 4"]) -> Tuple[float, float, float]:
|
||||
cam_pos = c2w[:3, 3]
|
||||
x, y, z = cam_pos.tolist()
|
||||
distance = cam_pos.norm().item()
|
||||
elevation = math.asin(z / distance)
|
||||
if abs(x) < 1.0e-5 and abs(y) < 1.0e-5:
|
||||
azimuth = 0
|
||||
else:
|
||||
azimuth = math.atan2(y, x)
|
||||
if azimuth < 0:
|
||||
azimuth += 2 * math.pi
|
||||
|
||||
return elevation, azimuth, distance
|
||||
|
||||
|
||||
def polar_to_c2w(
|
||||
elevation: float, azimuth: float, distance: float
|
||||
) -> Float[Tensor, "4 4"]:
|
||||
"""
|
||||
Compute L = p - C.
|
||||
Normalize L.
|
||||
Compute s = L x u. (cross product)
|
||||
Normalize s.
|
||||
Compute u' = s x L.
|
||||
rotation = [s, u, -l]
|
||||
"""
|
||||
z = distance * math.sin(elevation)
|
||||
x = distance * math.cos(elevation) * math.cos(azimuth)
|
||||
y = distance * math.cos(elevation) * math.sin(azimuth)
|
||||
l = -torch.as_tensor([x, y, z]).float()
|
||||
l = F.normalize(l, dim=0)
|
||||
u = torch.as_tensor([0.0, 0.0, 1.0]).float()
|
||||
s = l.cross(u)
|
||||
s = F.normalize(s, dim=0)
|
||||
u = s.cross(l)
|
||||
rot = torch.stack([s, u, -l], dim=0).T
|
||||
c2w = torch.zeros((4, 4), dtype=torch.float32)
|
||||
c2w[:3, :3] = rot
|
||||
c2w[:3, 3] = torch.as_tensor([x, y, z])
|
||||
c2w[3, 3] = 1
|
||||
return c2w
|
||||
|
||||
|
||||
def fourier_position_encoding(x, n_freq: int, dim: int):
|
||||
assert n_freq > 0
|
||||
input_shape = x.shape
|
||||
input_ndim = x.ndim
|
||||
if dim < 0:
|
||||
dim = input_ndim + dim
|
||||
bands = 2 ** torch.arange(n_freq, dtype=x.dtype, device=x.device)
|
||||
for i in range(dim + 1):
|
||||
bands = bands.unsqueeze(0)
|
||||
for i in range(input_ndim - dim - 1):
|
||||
bands = bands.unsqueeze(-1)
|
||||
x = x.view(*input_shape[: dim + 1], 1, *input_shape[dim + 1 :])
|
||||
x = torch.cat(
|
||||
[
|
||||
torch.sin(bands * x).reshape(
|
||||
*input_shape[:dim], -1, *input_shape[dim + 1 :]
|
||||
),
|
||||
torch.cos(bands * x).reshape(
|
||||
*input_shape[:dim], -1, *input_shape[dim + 1 :]
|
||||
),
|
||||
],
|
||||
dim=dim,
|
||||
)
|
||||
return x
|
||||
@@ -0,0 +1,544 @@
|
||||
import json
|
||||
import math
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
from typing import List, Optional, Union
|
||||
|
||||
import cv2
|
||||
import imageio
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
# import wandb
|
||||
from matplotlib import cm
|
||||
from matplotlib.colors import LinearSegmentedColormap
|
||||
from PIL import Image, ImageDraw
|
||||
|
||||
from .typing import *
|
||||
|
||||
|
||||
def image_to_tensor(
|
||||
images: List[Image.Image], device: str = "cuda"
|
||||
) -> torch.FloatTensor:
|
||||
tensors = []
|
||||
for image in images:
|
||||
tensor = torch.from_numpy(np.array(image)).float() / 255.0
|
||||
tensor = tensor.to(device)
|
||||
tensors.append(tensor)
|
||||
return torch.stack(tensors)
|
||||
|
||||
|
||||
def tensor_to_image(
|
||||
data: Union[Image.Image, torch.Tensor, np.ndarray],
|
||||
batched: bool = False,
|
||||
format: str = "HWC",
|
||||
) -> Union[Image.Image, List[Image.Image]]:
|
||||
if isinstance(data, Image.Image):
|
||||
return data
|
||||
if isinstance(data, torch.Tensor):
|
||||
data = data.detach().cpu().numpy()
|
||||
if data.dtype == np.float32 or data.dtype == np.float16:
|
||||
data = (data * 255).astype(np.uint8)
|
||||
elif data.dtype == np.bool_:
|
||||
data = data.astype(np.uint8) * 255
|
||||
assert data.dtype == np.uint8
|
||||
if format == "CHW":
|
||||
if batched and data.ndim == 4:
|
||||
data = data.transpose((0, 2, 3, 1))
|
||||
elif not batched and data.ndim == 3:
|
||||
data = data.transpose((1, 2, 0))
|
||||
|
||||
if batched:
|
||||
return [Image.fromarray(d) for d in data]
|
||||
return Image.fromarray(data)
|
||||
|
||||
|
||||
def largest_factor_near_sqrt(n: int) -> int:
|
||||
"""
|
||||
Finds the largest factor of n that is closest to the square root of n.
|
||||
|
||||
Args:
|
||||
n (int): The integer for which to find the largest factor near its square root.
|
||||
|
||||
Returns:
|
||||
int: The largest factor of n that is closest to the square root of n.
|
||||
"""
|
||||
sqrt_n = int(math.sqrt(n)) # Get the integer part of the square root
|
||||
|
||||
# First, check if the square root itself is a factor
|
||||
if sqrt_n * sqrt_n == n:
|
||||
return sqrt_n
|
||||
|
||||
# Otherwise, find the largest factor by iterating from sqrt_n downwards
|
||||
for i in range(sqrt_n, 0, -1):
|
||||
if n % i == 0:
|
||||
return i
|
||||
|
||||
# If n is 1, return 1
|
||||
return 1
|
||||
|
||||
|
||||
def make_image_grid(
|
||||
images: List[Image.Image],
|
||||
rows: Optional[int] = None,
|
||||
cols: Optional[int] = None,
|
||||
resize: Optional[int] = None,
|
||||
) -> Image.Image:
|
||||
"""
|
||||
Prepares a single grid of images. Useful for visualization purposes.
|
||||
"""
|
||||
if rows is None and cols is not None:
|
||||
assert len(images) % cols == 0
|
||||
rows = len(images) // cols
|
||||
elif cols is None and rows is not None:
|
||||
assert len(images) % rows == 0
|
||||
cols = len(images) // rows
|
||||
elif rows is None and cols is None:
|
||||
rows = largest_factor_near_sqrt(len(images))
|
||||
cols = len(images) // rows
|
||||
|
||||
assert len(images) == rows * cols
|
||||
|
||||
if resize is not None:
|
||||
images = [img.resize((resize, resize)) for img in images]
|
||||
|
||||
w, h = images[0].size
|
||||
grid = Image.new("RGB", size=(cols * w, rows * h))
|
||||
|
||||
for i, img in enumerate(images):
|
||||
grid.paste(img, box=(i % cols * w, i // cols * h))
|
||||
return grid
|
||||
|
||||
|
||||
class SaverMixin:
|
||||
_save_dir: Optional[str] = None
|
||||
_wandb_logger: Optional[Any] = None
|
||||
|
||||
def set_save_dir(self, save_dir: str):
|
||||
self._save_dir = save_dir
|
||||
|
||||
def get_save_dir(self):
|
||||
if self._save_dir is None:
|
||||
raise ValueError("Save dir is not set")
|
||||
return self._save_dir
|
||||
|
||||
def convert_data(self, data):
|
||||
if data is None:
|
||||
return None
|
||||
elif isinstance(data, np.ndarray):
|
||||
return data
|
||||
elif isinstance(data, torch.Tensor):
|
||||
if data.dtype in [torch.float16, torch.bfloat16]:
|
||||
data = data.float()
|
||||
return data.detach().cpu().numpy()
|
||||
elif isinstance(data, list):
|
||||
return [self.convert_data(d) for d in data]
|
||||
elif isinstance(data, dict):
|
||||
return {k: self.convert_data(v) for k, v in data.items()}
|
||||
else:
|
||||
raise TypeError(
|
||||
"Data must be in type numpy.ndarray, torch.Tensor, list or dict, getting",
|
||||
type(data),
|
||||
)
|
||||
|
||||
def get_save_path(self, filename):
|
||||
save_path = os.path.join(self.get_save_dir(), filename)
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
return save_path
|
||||
|
||||
DEFAULT_RGB_KWARGS = {"data_format": "HWC", "data_range": (0, 1)}
|
||||
DEFAULT_UV_KWARGS = {
|
||||
"data_format": "HWC",
|
||||
"data_range": (0, 1),
|
||||
"cmap": "checkerboard",
|
||||
}
|
||||
DEFAULT_GRAYSCALE_KWARGS = {"data_range": None, "cmap": "jet"}
|
||||
DEFAULT_GRID_KWARGS = {"align": "max"}
|
||||
|
||||
def get_rgb_image_(self, img, data_format, data_range, rgba=False):
|
||||
img = self.convert_data(img)
|
||||
assert data_format in ["CHW", "HWC"]
|
||||
if data_format == "CHW":
|
||||
img = img.transpose(1, 2, 0)
|
||||
if img.dtype != np.uint8:
|
||||
img = img.clip(min=data_range[0], max=data_range[1])
|
||||
img = (
|
||||
(img - data_range[0]) / (data_range[1] - data_range[0]) * 255.0
|
||||
).astype(np.uint8)
|
||||
nc = 4 if rgba else 3
|
||||
imgs = [img[..., start : start + nc] for start in range(0, img.shape[-1], nc)]
|
||||
imgs = [
|
||||
(
|
||||
img_
|
||||
if img_.shape[-1] == nc
|
||||
else np.concatenate(
|
||||
[
|
||||
img_,
|
||||
np.zeros(
|
||||
(img_.shape[0], img_.shape[1], nc - img_.shape[2]),
|
||||
dtype=img_.dtype,
|
||||
),
|
||||
],
|
||||
axis=-1,
|
||||
)
|
||||
)
|
||||
for img_ in imgs
|
||||
]
|
||||
img = np.concatenate(imgs, axis=1)
|
||||
if rgba:
|
||||
img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA)
|
||||
else:
|
||||
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
||||
return img
|
||||
|
||||
def _save_rgb_image(
|
||||
self,
|
||||
filename,
|
||||
img,
|
||||
data_format,
|
||||
data_range,
|
||||
name: Optional[str] = None,
|
||||
step: Optional[int] = None,
|
||||
):
|
||||
img = self.get_rgb_image_(img, data_format, data_range)
|
||||
cv2.imwrite(filename, img)
|
||||
if name and self._wandb_logger:
|
||||
self._wandb_logger.log_image(
|
||||
key=name, images=[self.get_save_path(filename)], step=step
|
||||
)
|
||||
|
||||
def save_rgb_image(
|
||||
self,
|
||||
filename,
|
||||
img,
|
||||
data_format=DEFAULT_RGB_KWARGS["data_format"],
|
||||
data_range=DEFAULT_RGB_KWARGS["data_range"],
|
||||
name: Optional[str] = None,
|
||||
step: Optional[int] = None,
|
||||
) -> str:
|
||||
save_path = self.get_save_path(filename)
|
||||
self._save_rgb_image(save_path, img, data_format, data_range, name, step)
|
||||
return save_path
|
||||
|
||||
def get_uv_image_(self, img, data_format, data_range, cmap):
|
||||
img = self.convert_data(img)
|
||||
assert data_format in ["CHW", "HWC"]
|
||||
if data_format == "CHW":
|
||||
img = img.transpose(1, 2, 0)
|
||||
img = img.clip(min=data_range[0], max=data_range[1])
|
||||
img = (img - data_range[0]) / (data_range[1] - data_range[0])
|
||||
assert cmap in ["checkerboard", "color"]
|
||||
if cmap == "checkerboard":
|
||||
n_grid = 64
|
||||
mask = (img * n_grid).astype(int)
|
||||
mask = (mask[..., 0] + mask[..., 1]) % 2 == 0
|
||||
img = np.ones((img.shape[0], img.shape[1], 3), dtype=np.uint8) * 255
|
||||
img[mask] = np.array([255, 0, 255], dtype=np.uint8)
|
||||
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
||||
elif cmap == "color":
|
||||
img_ = np.zeros((img.shape[0], img.shape[1], 3), dtype=np.uint8)
|
||||
img_[..., 0] = (img[..., 0] * 255).astype(np.uint8)
|
||||
img_[..., 1] = (img[..., 1] * 255).astype(np.uint8)
|
||||
img_ = cv2.cvtColor(img_, cv2.COLOR_RGB2BGR)
|
||||
img = img_
|
||||
return img
|
||||
|
||||
def save_uv_image(
|
||||
self,
|
||||
filename,
|
||||
img,
|
||||
data_format=DEFAULT_UV_KWARGS["data_format"],
|
||||
data_range=DEFAULT_UV_KWARGS["data_range"],
|
||||
cmap=DEFAULT_UV_KWARGS["cmap"],
|
||||
) -> str:
|
||||
save_path = self.get_save_path(filename)
|
||||
img = self.get_uv_image_(img, data_format, data_range, cmap)
|
||||
cv2.imwrite(save_path, img)
|
||||
return save_path
|
||||
|
||||
def get_grayscale_image_(self, img, data_range, cmap):
|
||||
img = self.convert_data(img)
|
||||
img = np.nan_to_num(img)
|
||||
if data_range is None:
|
||||
img = (img - img.min()) / (img.max() - img.min())
|
||||
else:
|
||||
img = img.clip(data_range[0], data_range[1])
|
||||
img = (img - data_range[0]) / (data_range[1] - data_range[0])
|
||||
assert cmap in [None, "jet", "magma", "spectral"]
|
||||
if cmap == None:
|
||||
img = (img * 255.0).astype(np.uint8)
|
||||
img = np.repeat(img[..., None], 3, axis=2)
|
||||
elif cmap == "jet":
|
||||
img = (img * 255.0).astype(np.uint8)
|
||||
img = cv2.applyColorMap(img, cv2.COLORMAP_JET)
|
||||
elif cmap == "magma":
|
||||
img = 1.0 - img
|
||||
base = cm.get_cmap("magma")
|
||||
num_bins = 256
|
||||
colormap = LinearSegmentedColormap.from_list(
|
||||
f"{base.name}{num_bins}", base(np.linspace(0, 1, num_bins)), num_bins
|
||||
)(np.linspace(0, 1, num_bins))[:, :3]
|
||||
a = np.floor(img * 255.0)
|
||||
b = (a + 1).clip(max=255.0)
|
||||
f = img * 255.0 - a
|
||||
a = a.astype(np.uint16).clip(0, 255)
|
||||
b = b.astype(np.uint16).clip(0, 255)
|
||||
img = colormap[a] + (colormap[b] - colormap[a]) * f[..., None]
|
||||
img = (img * 255.0).astype(np.uint8)
|
||||
elif cmap == "spectral":
|
||||
colormap = plt.get_cmap("Spectral")
|
||||
|
||||
def blend_rgba(image):
|
||||
image = image[..., :3] * image[..., -1:] + (
|
||||
1.0 - image[..., -1:]
|
||||
) # blend A to RGB
|
||||
return image
|
||||
|
||||
img = colormap(img)
|
||||
img = blend_rgba(img)
|
||||
img = (img * 255).astype(np.uint8)
|
||||
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
||||
return img
|
||||
|
||||
def _save_grayscale_image(
|
||||
self,
|
||||
filename,
|
||||
img,
|
||||
data_range,
|
||||
cmap,
|
||||
name: Optional[str] = None,
|
||||
step: Optional[int] = None,
|
||||
):
|
||||
img = self.get_grayscale_image_(img, data_range, cmap)
|
||||
cv2.imwrite(filename, img)
|
||||
if name and self._wandb_logger:
|
||||
self._wandb_logger.log_image(
|
||||
key=name, images=[self.get_save_path(filename)], step=step
|
||||
)
|
||||
|
||||
def save_grayscale_image(
|
||||
self,
|
||||
filename,
|
||||
img,
|
||||
data_range=DEFAULT_GRAYSCALE_KWARGS["data_range"],
|
||||
cmap=DEFAULT_GRAYSCALE_KWARGS["cmap"],
|
||||
name: Optional[str] = None,
|
||||
step: Optional[int] = None,
|
||||
) -> str:
|
||||
save_path = self.get_save_path(filename)
|
||||
self._save_grayscale_image(save_path, img, data_range, cmap, name, step)
|
||||
return save_path
|
||||
|
||||
def get_image_grid_(self, imgs, align):
|
||||
if isinstance(imgs[0], list):
|
||||
return np.concatenate(
|
||||
[self.get_image_grid_(row, align) for row in imgs], axis=0
|
||||
)
|
||||
cols = []
|
||||
for col in imgs:
|
||||
assert col["type"] in ["rgb", "uv", "grayscale"]
|
||||
if col["type"] == "rgb":
|
||||
rgb_kwargs = self.DEFAULT_RGB_KWARGS.copy()
|
||||
rgb_kwargs.update(col["kwargs"])
|
||||
cols.append(self.get_rgb_image_(col["img"], **rgb_kwargs))
|
||||
elif col["type"] == "uv":
|
||||
uv_kwargs = self.DEFAULT_UV_KWARGS.copy()
|
||||
uv_kwargs.update(col["kwargs"])
|
||||
cols.append(self.get_uv_image_(col["img"], **uv_kwargs))
|
||||
elif col["type"] == "grayscale":
|
||||
grayscale_kwargs = self.DEFAULT_GRAYSCALE_KWARGS.copy()
|
||||
grayscale_kwargs.update(col["kwargs"])
|
||||
cols.append(self.get_grayscale_image_(col["img"], **grayscale_kwargs))
|
||||
|
||||
if align == "max":
|
||||
h = max([col.shape[0] for col in cols])
|
||||
elif align == "min":
|
||||
h = min([col.shape[0] for col in cols])
|
||||
elif isinstance(align, int):
|
||||
h = align
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported image grid align: {align}, should be min, max, or int"
|
||||
)
|
||||
|
||||
for i in range(len(cols)):
|
||||
if cols[i].shape[0] != h:
|
||||
w = int(cols[i].shape[1] * h / cols[i].shape[0])
|
||||
cols[i] = cv2.resize(cols[i], (w, h), interpolation=cv2.INTER_CUBIC)
|
||||
return np.concatenate(cols, axis=1)
|
||||
|
||||
def save_image_grid(
|
||||
self,
|
||||
filename,
|
||||
imgs,
|
||||
align=DEFAULT_GRID_KWARGS["align"],
|
||||
name: Optional[str] = None,
|
||||
step: Optional[int] = None,
|
||||
texts: Optional[List[float]] = None,
|
||||
):
|
||||
save_path = self.get_save_path(filename)
|
||||
img = self.get_image_grid_(imgs, align=align)
|
||||
|
||||
if texts is not None:
|
||||
img = Image.fromarray(img)
|
||||
draw = ImageDraw.Draw(img)
|
||||
black, white = (0, 0, 0), (255, 255, 255)
|
||||
for i, text in enumerate(texts):
|
||||
draw.text((2, (img.size[1] // len(texts)) * i + 1), f"{text}", white)
|
||||
draw.text((0, (img.size[1] // len(texts)) * i + 1), f"{text}", white)
|
||||
draw.text((2, (img.size[1] // len(texts)) * i - 1), f"{text}", white)
|
||||
draw.text((0, (img.size[1] // len(texts)) * i - 1), f"{text}", white)
|
||||
draw.text((1, (img.size[1] // len(texts)) * i), f"{text}", black)
|
||||
img = np.asarray(img)
|
||||
|
||||
cv2.imwrite(save_path, img)
|
||||
if name and self._wandb_logger:
|
||||
self._wandb_logger.log_image(key=name, images=[save_path], step=step)
|
||||
return save_path
|
||||
|
||||
def save_image(self, filename, img) -> str:
|
||||
save_path = self.get_save_path(filename)
|
||||
img = self.convert_data(img)
|
||||
assert img.dtype == np.uint8 or img.dtype == np.uint16
|
||||
if img.ndim == 3 and img.shape[-1] == 3:
|
||||
img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR)
|
||||
elif img.ndim == 3 and img.shape[-1] == 4:
|
||||
img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGRA)
|
||||
cv2.imwrite(save_path, img)
|
||||
return save_path
|
||||
|
||||
def save_cubemap(self, filename, img, data_range=(0, 1), rgba=False) -> str:
|
||||
save_path = self.get_save_path(filename)
|
||||
img = self.convert_data(img)
|
||||
assert img.ndim == 4 and img.shape[0] == 6 and img.shape[1] == img.shape[2]
|
||||
|
||||
imgs_full = []
|
||||
for start in range(0, img.shape[-1], 3):
|
||||
img_ = img[..., start : start + 3]
|
||||
img_ = np.stack(
|
||||
[
|
||||
self.get_rgb_image_(img_[i], "HWC", data_range, rgba=rgba)
|
||||
for i in range(img_.shape[0])
|
||||
],
|
||||
axis=0,
|
||||
)
|
||||
size = img_.shape[1]
|
||||
placeholder = np.zeros((size, size, 3), dtype=np.float32)
|
||||
img_full = np.concatenate(
|
||||
[
|
||||
np.concatenate(
|
||||
[placeholder, img_[2], placeholder, placeholder], axis=1
|
||||
),
|
||||
np.concatenate([img_[1], img_[4], img_[0], img_[5]], axis=1),
|
||||
np.concatenate(
|
||||
[placeholder, img_[3], placeholder, placeholder], axis=1
|
||||
),
|
||||
],
|
||||
axis=0,
|
||||
)
|
||||
imgs_full.append(img_full)
|
||||
|
||||
imgs_full = np.concatenate(imgs_full, axis=1)
|
||||
cv2.imwrite(save_path, imgs_full)
|
||||
return save_path
|
||||
|
||||
def save_data(self, filename, data) -> str:
|
||||
data = self.convert_data(data)
|
||||
if isinstance(data, dict):
|
||||
if not filename.endswith(".npz"):
|
||||
filename += ".npz"
|
||||
save_path = self.get_save_path(filename)
|
||||
np.savez(save_path, **data)
|
||||
else:
|
||||
if not filename.endswith(".npy"):
|
||||
filename += ".npy"
|
||||
save_path = self.get_save_path(filename)
|
||||
np.save(save_path, data)
|
||||
return save_path
|
||||
|
||||
def save_state_dict(self, filename, data) -> str:
|
||||
save_path = self.get_save_path(filename)
|
||||
torch.save(data, save_path)
|
||||
return save_path
|
||||
|
||||
def save_img_sequence(
|
||||
self,
|
||||
filename,
|
||||
img_dir,
|
||||
matcher,
|
||||
save_format="mp4",
|
||||
fps=30,
|
||||
name: Optional[str] = None,
|
||||
step: Optional[int] = None,
|
||||
) -> str:
|
||||
assert save_format in ["gif", "mp4"]
|
||||
if not filename.endswith(save_format):
|
||||
filename += f".{save_format}"
|
||||
save_path = self.get_save_path(filename)
|
||||
matcher = re.compile(matcher)
|
||||
img_dir = os.path.join(self.get_save_dir(), img_dir)
|
||||
imgs = []
|
||||
for f in os.listdir(img_dir):
|
||||
if matcher.search(f):
|
||||
imgs.append(f)
|
||||
imgs = sorted(imgs, key=lambda f: int(matcher.search(f).groups()[0]))
|
||||
imgs = [cv2.imread(os.path.join(img_dir, f)) for f in imgs]
|
||||
|
||||
if save_format == "gif":
|
||||
imgs = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in imgs]
|
||||
imageio.mimsave(save_path, imgs, fps=fps, palettesize=256)
|
||||
elif save_format == "mp4":
|
||||
imgs = [cv2.cvtColor(i, cv2.COLOR_BGR2RGB) for i in imgs]
|
||||
imageio.mimsave(save_path, imgs, fps=fps)
|
||||
if name and self._wandb_logger:
|
||||
from .core import warn
|
||||
|
||||
warn("Wandb logger does not support video logging yet!")
|
||||
return save_path
|
||||
|
||||
def save_img_sequences(
|
||||
self,
|
||||
seq_dir,
|
||||
matcher,
|
||||
save_format="mp4",
|
||||
fps=30,
|
||||
delete=True,
|
||||
name: Optional[str] = None,
|
||||
step: Optional[int] = None,
|
||||
):
|
||||
seq_dir_ = os.path.join(self.get_save_dir(), seq_dir)
|
||||
for f in os.listdir(seq_dir_):
|
||||
img_dir_ = os.path.join(seq_dir_, f)
|
||||
if not os.path.isdir(img_dir_):
|
||||
continue
|
||||
try:
|
||||
self.save_img_sequence(
|
||||
os.path.join(seq_dir, f),
|
||||
os.path.join(seq_dir, f),
|
||||
matcher,
|
||||
save_format=save_format,
|
||||
fps=fps,
|
||||
name=f"{name}_{f}",
|
||||
step=step,
|
||||
)
|
||||
if delete:
|
||||
shutil.rmtree(img_dir_)
|
||||
except:
|
||||
from .core import warn
|
||||
|
||||
warn(f"Video saving for directory {seq_dir_} failed!")
|
||||
|
||||
def save_file(self, filename, src_path, delete=False) -> str:
|
||||
save_path = self.get_save_path(filename)
|
||||
shutil.copyfile(src_path, save_path)
|
||||
if delete:
|
||||
os.remove(src_path)
|
||||
return save_path
|
||||
|
||||
def save_json(self, filename, payload) -> str:
|
||||
save_path = self.get_save_path(filename)
|
||||
with open(save_path, "w") as f:
|
||||
f.write(json.dumps(payload))
|
||||
return save_path
|
||||
@@ -0,0 +1,40 @@
|
||||
"""
|
||||
This module contains type annotations for the project, using
|
||||
1. Python type hints (https://docs.python.org/3/library/typing.html) for Python objects
|
||||
2. jaxtyping (https://github.com/google/jaxtyping/blob/main/API.md) for PyTorch tensors
|
||||
|
||||
Two types of typing checking can be used:
|
||||
1. Static type checking with mypy (install with pip and enabled as the default linter in VSCode)
|
||||
2. Runtime type checking with typeguard (install with pip and triggered at runtime, mainly for tensor dtype and shape checking)
|
||||
"""
|
||||
|
||||
# Basic types
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
Iterable,
|
||||
List,
|
||||
Literal,
|
||||
NamedTuple,
|
||||
NewType,
|
||||
Optional,
|
||||
Sized,
|
||||
Tuple,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
# Tensor dtype
|
||||
# for jaxtyping usage, see https://github.com/google/jaxtyping/blob/main/API.md
|
||||
from jaxtyping import Bool, Complex, Float, Inexact, Int, Integer, Num, Shaped, UInt
|
||||
|
||||
# Config type
|
||||
from omegaconf import DictConfig, ListConfig
|
||||
|
||||
# PyTorch Tensor type
|
||||
from torch import Tensor
|
||||
|
||||
# Runtime type checking decorator
|
||||
from typeguard import typechecked as typechecker
|
||||
@@ -0,0 +1,302 @@
|
||||
"""
|
||||
Utilities for ComfyUI MV-Adapter nodes
|
||||
"""
|
||||
|
||||
import os
|
||||
import torch
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
from typing import Optional, Union, List
|
||||
from torchvision import transforms
|
||||
from transformers import AutoModelForImageSegmentation
|
||||
|
||||
from diffusers import AutoencoderKL, DDPMScheduler, LCMScheduler, UNet2DConditionModel
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
from .mvadapter.pipelines.pipeline_mvadapter_i2mv_sdxl import MVAdapterI2MVSDXLPipeline
|
||||
from .mvadapter.models.attention_processor import DecoupledMVRowColSelfAttnProcessor2_0
|
||||
from .mvadapter.schedulers.scheduling_shift_snr import ShiftSNRScheduler
|
||||
from .mvadapter.utils.mesh_utils import (
|
||||
NVDiffRastContextWrapper,
|
||||
get_orthogonal_camera,
|
||||
load_mesh,
|
||||
render,
|
||||
)
|
||||
from .mvadapter.utils import make_image_grid, tensor_to_image
|
||||
|
||||
try:
|
||||
from mmgp import offload, profile_type
|
||||
MMGP_AVAILABLE = True
|
||||
except ImportError:
|
||||
MMGP_AVAILABLE = False
|
||||
print("Warning: mmgp not available, memory optimization disabled")
|
||||
|
||||
|
||||
def prepare_pipeline(
|
||||
base_model: str,
|
||||
vae_model: Optional[str] = None,
|
||||
unet_model: Optional[str] = None,
|
||||
lora_model: Optional[str] = None,
|
||||
adapter_path: str = "huanngzh/mv-adapter",
|
||||
scheduler: str = "default",
|
||||
num_views: int = 6,
|
||||
device: str = "cuda",
|
||||
dtype: torch.dtype = torch.float16,
|
||||
use_mmgp: bool = False,
|
||||
adapter_local_path: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
Prepare MV-Adapter pipeline
|
||||
|
||||
Args:
|
||||
base_model: Base Stable Diffusion XL model
|
||||
vae_model: Custom VAE model (optional)
|
||||
unet_model: Custom UNet model (optional)
|
||||
lora_model: LoRA model (optional)
|
||||
adapter_path: Path to adapter weights
|
||||
scheduler: Scheduler type ("default", "ddpm", "lcm")
|
||||
num_views: Number of views to generate
|
||||
device: Device for computation
|
||||
dtype: Data type
|
||||
use_mmgp: Use mmgp for memory optimization
|
||||
adapter_local_path: Local path for adapter download
|
||||
"""
|
||||
# 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.from_pretrained(base_model, **pipe_kwargs)
|
||||
|
||||
# Load scheduler if provided
|
||||
if scheduler != "default":
|
||||
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
|
||||
)
|
||||
|
||||
# Load adapter weights
|
||||
weight_name = "mvadapter_ig2mv_sdxl.safetensors"
|
||||
pipe.load_custom_adapter(adapter_path, weight_name=weight_name, local_cache_dir=adapter_local_path)
|
||||
|
||||
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_)
|
||||
|
||||
# Apply mmgp optimization if available and requested
|
||||
if use_mmgp and MMGP_AVAILABLE:
|
||||
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.")
|
||||
|
||||
# Enable VAE slicing for memory efficiency
|
||||
pipe.enable_vae_slicing()
|
||||
|
||||
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 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
|
||||
|
||||
|
||||
def preprocess_image(image: Image.Image, height: int, width: int) -> Image.Image:
|
||||
"""Preprocess image for MV-Adapter"""
|
||||
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: str,
|
||||
num_views: int,
|
||||
text: str,
|
||||
image: Union[str, Image.Image],
|
||||
height: int,
|
||||
width: int,
|
||||
num_inference_steps: int,
|
||||
guidance_scale: float,
|
||||
seed: int,
|
||||
remove_bg_fn=None,
|
||||
reference_conditioning_scale: float = 1.0,
|
||||
negative_prompt: str = "watermark, ugly, deformed, noisy, blurry, low contrast",
|
||||
lora_scale: float = 1.0,
|
||||
device: str = "cuda",
|
||||
):
|
||||
"""
|
||||
Execute multi-view image generation
|
||||
"""
|
||||
# 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
|
||||
|
||||
|
||||
def create_image_grid(images: List[Image.Image], rows: int = 1) -> Image.Image:
|
||||
"""Создание сетки изображений"""
|
||||
return make_image_grid(images, rows=rows)
|
||||
|
||||
|
||||
def pil_to_torch_tensor(image: Image.Image) -> torch.Tensor:
|
||||
"""Конвертация PIL изображения в torch tensor"""
|
||||
import numpy as np
|
||||
array = np.array(image)
|
||||
tensor = torch.from_numpy(array).float() / 255.0
|
||||
|
||||
# Add batch dimension if needed
|
||||
if tensor.dim() == 3:
|
||||
tensor = tensor.unsqueeze(0)
|
||||
|
||||
return tensor
|
||||
|
||||
|
||||
def pils_to_torch_tensor(images: List[Image.Image]) -> torch.Tensor:
|
||||
"""Конвертация списка PIL изображений в torch tensor"""
|
||||
tensors = [pil_to_torch_tensor(img).squeeze(0) for img in images]
|
||||
return torch.stack(tensors, dim=0)
|
||||
@@ -0,0 +1,28 @@
|
||||
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
|
||||
@@ -0,0 +1,214 @@
|
||||
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()
|
||||
@@ -0,0 +1,195 @@
|
||||
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()
|
||||
@@ -0,0 +1,255 @@
|
||||
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")
|
||||
@@ -0,0 +1,267 @@
|
||||
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")
|
||||
@@ -0,0 +1,303 @@
|
||||
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)
|
||||
@@ -0,0 +1,313 @@
|
||||
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")
|
||||
@@ -0,0 +1,252 @@
|
||||
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"
|
||||
)
|
||||
@@ -0,0 +1,170 @@
|
||||
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)
|
||||
@@ -0,0 +1,201 @@
|
||||
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)
|
||||
@@ -0,0 +1,200 @@
|
||||
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"
|
||||
)
|
||||
+104
@@ -0,0 +1,104 @@
|
||||
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}")
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
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}")
|
||||
@@ -0,0 +1,59 @@
|
||||
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",
|
||||
},
|
||||
)
|
||||
@@ -0,0 +1,138 @@
|
||||
#!/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"
|
||||
@@ -105,6 +105,12 @@ from TripoSG.pipelines.pipeline_triposg import TripoSGPipeline
|
||||
from TripoSG.pipelines.pipeline_triposg_scribble import TripoSGScribblePipeline
|
||||
from Stable3DGen.trellis.backend_config import get_available_backends, get_available_sparse_backends
|
||||
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,
|
||||
)
|
||||
from MV_Adapter.mvadapter.utils import make_image_grid
|
||||
|
||||
os.environ['SPCONV_ALGO'] = 'native'
|
||||
|
||||
@@ -4781,3 +4787,130 @@ class StableGen_StableX_Process_Image:
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"[StableGen_StableX_Process_Image] Image processing failed: {str(e)}")
|
||||
|
||||
class Load_MVAdapter_Pipeline:
|
||||
"""Loader pipeline for MV-Adapter (Image to Multi-View)"""
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
RETURN_TYPES = ("DIFFUSERS_PIPE",)
|
||||
RETURN_NAMES = ("mvadapter_pipe",)
|
||||
FUNCTION = "load"
|
||||
|
||||
CKPT_MVADAPTER_PATH = os.path.join(CKPT_DIFFUSERS_PATH, "huanngzh", "MV-Adapter")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"base_model": (["stabilityai/stable-diffusion-xl-base-1.0"],
|
||||
{"default": "stabilityai/stable-diffusion-xl-base-1.0"}),
|
||||
"vae_model": (["madebyollin/sdxl-vae-fp16-fix", "None"],
|
||||
{"default": "madebyollin/sdxl-vae-fp16-fix"}),
|
||||
"adapter_path": (["huanngzh/mv-adapter"], {"default": "huanngzh/mv-adapter"}),
|
||||
"scheduler": (["default", "ddpm", "lcm"], {"default": "default"}),
|
||||
"num_views": ("INT", {"default": 6, "min": 1, "max": 16}),
|
||||
"use_fp16": ("BOOLEAN", {"default": True}),
|
||||
"use_mmgp": ("BOOLEAN", {"default": True}),
|
||||
},
|
||||
"optional": {
|
||||
"unet_model": ("STRING", {"default": ""}),
|
||||
"lora_model": ("STRING", {"default": ""}),
|
||||
}
|
||||
}
|
||||
@classmethod
|
||||
def load(cls, base_model, vae_model, adapter_path, scheduler, num_views,
|
||||
use_fp16, use_mmgp, unet_model="", lora_model=""):
|
||||
|
||||
|
||||
dtype = torch.float16 if use_fp16 else torch.float32
|
||||
vae_model = None if vae_model == "None" else vae_model
|
||||
unet_model = None if not unet_model else unet_model
|
||||
lora_model = None if not lora_model else lora_model
|
||||
|
||||
pipe = mvadapter_prepare_pipeline(
|
||||
base_model=base_model,
|
||||
vae_model=vae_model,
|
||||
unet_model=unet_model,
|
||||
lora_model=lora_model,
|
||||
adapter_path=adapter_path,
|
||||
scheduler=scheduler,
|
||||
num_views=num_views,
|
||||
device=DEVICE_STR,
|
||||
dtype=dtype,
|
||||
use_mmgp=use_mmgp,
|
||||
adapter_local_path=cls.CKPT_MVADAPTER_PATH
|
||||
)
|
||||
|
||||
print("MV-Adapter pipeline loaded successfully")
|
||||
return (pipe,)
|
||||
|
||||
|
||||
class MVAdapter_Image_To_MultiView:
|
||||
"""Generate multi-view images from single image and 3D mesh"""
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("multiview_images",)
|
||||
FUNCTION = "run"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mvadapter_pipe": ("DIFFUSERS_PIPE",),
|
||||
"mesh_path": ("STRING", {"default": ""}),
|
||||
"reference_image": ("IMAGE",),
|
||||
"prompt": ("STRING", {"default": "high quality", "multiline": True}),
|
||||
"negative_prompt": ("STRING", {"default": "watermark, ugly, deformed, noisy, blurry, low contrast", "multiline": True}),
|
||||
"num_inference_steps": ("INT", {"default": 50, "min": 1, "max": 200}),
|
||||
"guidance_scale": ("FLOAT", {"default": 3.0, "min": 0.0, "max": 20.0, "step": 0.1}),
|
||||
"reference_conditioning_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
||||
"height": ("INT", {"default": 768, "min": 256, "max": 2048, "step": 8}),
|
||||
"width": ("INT", {"default": 768, "min": 256, "max": 2048, "step": 8}),
|
||||
"seed": ("INT", {"default": -1, "min": -1, "max": 0xffffffffffffffff}),
|
||||
"remove_background": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"lora_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
|
||||
def run(self, mvadapter_pipe, mesh_path, reference_image, prompt, negative_prompt,
|
||||
num_inference_steps, guidance_scale, reference_conditioning_scale,
|
||||
height, width, seed, remove_background, lora_scale=1.0):
|
||||
|
||||
if isinstance(reference_image, torch.Tensor):
|
||||
reference_images = torch_imgs_to_pils(reference_image)
|
||||
reference_image = reference_images[0]
|
||||
|
||||
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,
|
||||
mesh_path=mesh_path,
|
||||
num_views=num_views,
|
||||
text=prompt,
|
||||
image=reference_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,
|
||||
lora_scale=lora_scale,
|
||||
device=DEVICE_STR,
|
||||
)
|
||||
|
||||
grid_image = make_image_grid(images, rows=1)
|
||||
|
||||
grid_tensor = pils_to_torch_imgs([grid_image], device=DEVICE_STR)
|
||||
|
||||
print(f"Generated multiview images: {grid_tensor.shape}")
|
||||
return (grid_tensor,)
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
import torch
|
||||
import os
|
||||
import trimesh
|
||||
import numpy as np
|
||||
from collections import OrderedDict
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
# Import StableGen modules
|
||||
try:
|
||||
from .Gen_3D_Modules.Stable3DGen.trellis.pipelines.trellis_image_to_3d import TrellisImageTo3DPipeline as Stable3DGenTrellisImageTo3DPipeline
|
||||
from .Gen_3D_Modules.Stable3DGen.stablex.pipeline_yoso import YosoPipeline
|
||||
from .Gen_3D_Modules.Stable3DGen.stablex.controlnetvae import ControlNetVAEModel
|
||||
|
||||
from .Gen_3D_Modules.Stable3DGen.pipeline_builders import StableGenPipelineBuilder
|
||||
except ImportError as e:
|
||||
print(f"Warning: Could not import StableGen modules: {e}")
|
||||
# Create dummy functions for fallback
|
||||
def get_available_backends():
|
||||
return {'flash_attn': True, 'sdpa': True}
|
||||
def get_available_sparse_backends():
|
||||
return {'spconv': True}
|
||||
def set_attention_backend(backend):
|
||||
return True
|
||||
def set_sparse_backend(backend, algo):
|
||||
return True
|
||||
|
||||
class StableGenPipelineBuilder:
|
||||
@staticmethod
|
||||
def build_trellis_pipeline(*args, **kwargs):
|
||||
raise ImportError("StableGen modules not available")
|
||||
@staticmethod
|
||||
def build_stablex_pipeline(*args, **kwargs):
|
||||
raise ImportError("StableGen modules not available")
|
||||
|
||||
# Import diffusers components for StableX
|
||||
try:
|
||||
from diffusers.models import AutoencoderKL, UNet2DConditionModel
|
||||
from accelerate import init_empty_weights
|
||||
from accelerate.utils import set_module_tensor_to_device
|
||||
from safetensors.torch import load_file
|
||||
except ImportError as e:
|
||||
print(f"Warning: Could not import diffusers/accelerate: {e}")
|
||||
|
||||
import json
|
||||
|
||||
from .shared_utils.mesh_utils import Mesh
|
||||
from .shared_utils.image_utils import torch_imgs_to_pils, pils_to_torch_imgs, pil_make_image_grid
|
||||
|
||||
ROOT_PATH = os.path.dirname(os.path.realpath(__file__))
|
||||
CKPT_ROOT_PATH = os.path.join(ROOT_PATH, "Checkpoints")
|
||||
CKPT_DIFFUSERS_PATH = os.path.join(CKPT_ROOT_PATH, "Diffusers")
|
||||
|
||||
WEIGHT_DTYPE = torch.float16
|
||||
DEVICE_STR = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
DEVICE = torch.device(DEVICE_STR)
|
||||
|
||||
HF_DOWNLOAD_IGNORE = ["*.yaml", "*.json", "*.py", ".png", ".jpg", ".gif"]
|
||||
|
||||
|
||||
class Load_StableGen_Trellis_Pipeline:
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
RETURN_TYPES = ("DIFFUSERS_PIPE",)
|
||||
RETURN_NAMES = ("trellis_pipe",)
|
||||
FUNCTION = "load"
|
||||
|
||||
_REPO_ID_BASE = "Stable-X"
|
||||
CKPT_STABLEGEN_PATH = os.path.join(CKPT_DIFFUSERS_PATH, "Stable3DGen")
|
||||
|
||||
_MODES = {
|
||||
"trellis-normal-v0-1": ("trellis-normal-v0-1", 12, 12), # (repo, ss_steps, slat_steps)
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
available_attn, available_sparse = StableGenPipelineBuilder.get_available_backends()
|
||||
|
||||
return {
|
||||
"required": {
|
||||
"model_name": (list(cls._MODES.keys()),),
|
||||
"dinov2_model": (["dinov2_vitl14_reg"],),
|
||||
"use_fp16": ("BOOLEAN", {"default": True}),
|
||||
"attn_backend": (available_attn,),
|
||||
"sparse_backend": (available_sparse,),
|
||||
"spconv_algo": (["implicit_gemm", "native", "auto"],),
|
||||
"smooth_k": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _build_pipe(cls, repo: str, dinov2_model: str, use_fp16: bool, attn_backend: str,
|
||||
sparse_backend: str, spconv_algo: str, smooth_k: bool):
|
||||
|
||||
return StableGenPipelineBuilder.build_trellis_pipeline(
|
||||
repo=repo,
|
||||
dinov2_model=dinov2_model,
|
||||
use_fp16=use_fp16,
|
||||
attn_backend=attn_backend,
|
||||
sparse_backend=sparse_backend,
|
||||
spconv_algo=spconv_algo,
|
||||
smooth_k=smooth_k,
|
||||
ckpt_path=cls.CKPT_STABLEGEN_PATH
|
||||
)
|
||||
|
||||
def load(self, model_name, dinov2_model, use_fp16, attn_backend, sparse_backend, spconv_algo, smooth_k):
|
||||
repo, ss_steps, slat_steps = self._MODES[model_name]
|
||||
|
||||
pipe = self.__class__._build_pipe(repo, dinov2_model, use_fp16, attn_backend, sparse_backend, spconv_algo, smooth_k)
|
||||
# Store default steps
|
||||
pipe.default_ss_steps = ss_steps
|
||||
pipe.default_slat_steps = slat_steps
|
||||
|
||||
return (pipe,)
|
||||
|
||||
|
||||
class Load_StableGen_StableX_Pipeline:
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
RETURN_TYPES = ("DIFFUSERS_PIPE",)
|
||||
RETURN_NAMES = ("stablex_pipe",)
|
||||
FUNCTION = "load"
|
||||
|
||||
_REPO_ID_BASE = "Stable-X"
|
||||
CKPT_STABLEGEN_PATH = os.path.join(CKPT_DIFFUSERS_PATH, "Stable3DGen")
|
||||
|
||||
_MODES = {
|
||||
"yoso-normal-v1-8-1": "yoso-normal-v1-8-1",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"model_name": (list(cls._MODES.keys()),),
|
||||
"use_fp16": ("BOOLEAN", {"default": True}),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _build_pipe(cls, repo: str, use_fp16: bool):
|
||||
return StableGenPipelineBuilder.build_stablex_pipeline(
|
||||
repo=repo,
|
||||
use_fp16=use_fp16,
|
||||
ckpt_path=cls.CKPT_STABLEGEN_PATH
|
||||
)
|
||||
|
||||
def load(self, model_name, use_fp16):
|
||||
repo = self._MODES[model_name]
|
||||
pipe = self.__class__._build_pipe(repo, use_fp16)
|
||||
return (pipe,)
|
||||
|
||||
|
||||
class StableGen_Trellis_Image_To_3D:
|
||||
"""
|
||||
3D generation pipeline using Trellis model.
|
||||
"""
|
||||
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
RETURN_TYPES = ("MESH",)
|
||||
RETURN_NAMES = ("mesh",)
|
||||
FUNCTION = "run"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"trellis_pipe": ("DIFFUSERS_PIPE",),
|
||||
"images": ("IMAGE", {"list": True}),
|
||||
"mode": (["single", "multi"], {"default": "single"}),
|
||||
"seed": ("INT", {"default": 1234, "min": 0, "max": 0xffffffffffffffff}),
|
||||
"ss_guidance_strength": ("FLOAT", {"default": 7.5, "min": 0.0, "step": 0.1}),
|
||||
"ss_sampling_steps": ("INT", {"default": 12, "min": 1}),
|
||||
"slat_guidance_strength": ("FLOAT", {"default": 3.0, "min": 0.0, "step": 0.1}),
|
||||
"slat_sampling_steps": ("INT", {"default": 12, "min": 1}),
|
||||
"mesh_simplify": ("FLOAT", {"default": 0.95, "min": 0.9, "max": 1.0, "step": 0.01}),
|
||||
}
|
||||
}
|
||||
|
||||
@torch.no_grad()
|
||||
def run(
|
||||
self,
|
||||
trellis_pipe,
|
||||
images,
|
||||
mode="single",
|
||||
seed=1234,
|
||||
ss_guidance_strength=7.5,
|
||||
ss_sampling_steps=12,
|
||||
slat_guidance_strength=3.0,
|
||||
slat_sampling_steps=12,
|
||||
mesh_simplify=0.95
|
||||
):
|
||||
if isinstance(images, torch.Tensor):
|
||||
images = torch_imgs_to_pils(images)
|
||||
|
||||
if not isinstance(images, list) or len(images) == 0:
|
||||
raise Exception(f"[StableGen_Trellis_Image_To_3D] 'images' must be a non-empty list of PIL images. Got type: {type(images)}, len: {len(images) if hasattr(images, '__len__') else 'no len'}")
|
||||
|
||||
# Use inference context like in working version
|
||||
with trellis_pipe.inference_context():
|
||||
# Prepare input based on mode
|
||||
if mode == "single":
|
||||
if len(images) > 1:
|
||||
print(f"Warning: Single mode selected but {len(images)} images provided. Using first image.")
|
||||
image_input = images[0]
|
||||
|
||||
# Set up generation parameters for single image
|
||||
pipeline_params = StableGenPipelineBuilder.create_trellis_pipeline_params(
|
||||
seed=seed,
|
||||
ss_sampling_steps=ss_sampling_steps,
|
||||
ss_guidance_strength=ss_guidance_strength,
|
||||
slat_sampling_steps=slat_sampling_steps,
|
||||
slat_guidance_strength=slat_guidance_strength,
|
||||
formats=["mesh"]
|
||||
)
|
||||
|
||||
# Generate 3D mesh using single image method
|
||||
outputs = trellis_pipe.run(
|
||||
image_input,
|
||||
**pipeline_params
|
||||
)
|
||||
else: # multi mode
|
||||
# Set up generation parameters for multi image
|
||||
pipeline_params = StableGenPipelineBuilder.create_trellis_pipeline_params(
|
||||
seed=seed,
|
||||
ss_sampling_steps=ss_sampling_steps,
|
||||
ss_guidance_strength=ss_guidance_strength,
|
||||
slat_sampling_steps=slat_sampling_steps,
|
||||
slat_guidance_strength=slat_guidance_strength,
|
||||
formats=["mesh"]
|
||||
)
|
||||
|
||||
outputs = trellis_pipe.run_multi_image(
|
||||
images,
|
||||
**pipeline_params
|
||||
)
|
||||
|
||||
try:
|
||||
mesh_output = outputs['mesh'][0]
|
||||
|
||||
vertices = mesh_output.vertices.cpu().numpy()
|
||||
faces = mesh_output.faces.cpu().numpy()
|
||||
|
||||
transformation_matrix = np.array([[1, 0, 0], [0, 0, -1], [0, 1, 0]])
|
||||
vertices = vertices @ transformation_matrix
|
||||
|
||||
tri_mesh = trimesh.Trimesh(vertices, faces)
|
||||
|
||||
if mesh_simplify < 1.0:
|
||||
try:
|
||||
target_faces = int(len(faces) * mesh_simplify)
|
||||
tri_mesh = tri_mesh.simplify_quadric_decimation(target_faces)
|
||||
except Exception as e:
|
||||
print(f"Warning: Mesh simplification failed: {e}. Using original mesh.")
|
||||
|
||||
# Convert to Mesh object
|
||||
mesh = Mesh.load_trimesh(given_mesh=tri_mesh)
|
||||
mesh.auto_normal()
|
||||
|
||||
return (mesh,)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"[StableGen_Trellis_Image_To_3D] 3D generation failed: {str(e)}")
|
||||
|
||||
|
||||
# Import MV-Adapter modules
|
||||
try:
|
||||
from .Gen_3D_Modules.MV_Adapter.mvadapter_node_utils import (
|
||||
prepare_pipeline as mvadapter_prepare_pipeline,
|
||||
run_pipeline as mvadapter_run_pipeline,
|
||||
create_bg_remover,
|
||||
)
|
||||
from .Gen_3D_Modules.MV_Adapter.mvadapter.utils import make_image_grid
|
||||
MVADAPTER_AVAILABLE = True
|
||||
except ImportError as e:
|
||||
print(f"Warning: Could not import MV-Adapter modules: {e}")
|
||||
MVADAPTER_AVAILABLE = False
|
||||
|
||||
|
||||
class Load_MVAdapter_Pipeline:
|
||||
"""Loader pipeline for MV-Adapter (Image to Multi-View)"""
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
RETURN_TYPES = ("DIFFUSERS_PIPE",)
|
||||
RETURN_NAMES = ("mvadapter_pipe",)
|
||||
FUNCTION = "load"
|
||||
|
||||
CKPT_MVADAPTER_PATH = os.path.join(CKPT_DIFFUSERS_PATH, "huanngzh", "MV-Adapter")
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"base_model": (["stabilityai/stable-diffusion-xl-base-1.0"],
|
||||
{"default": "stabilityai/stable-diffusion-xl-base-1.0"}),
|
||||
"vae_model": (["madebyollin/sdxl-vae-fp16-fix", "None"],
|
||||
{"default": "madebyollin/sdxl-vae-fp16-fix"}),
|
||||
"adapter_path": (["huanngzh/mv-adapter"], {"default": "huanngzh/mv-adapter"}),
|
||||
"scheduler": (["default", "ddpm", "lcm"], {"default": "default"}),
|
||||
"num_views": ("INT", {"default": 6, "min": 1, "max": 16}),
|
||||
"use_fp16": ("BOOLEAN", {"default": True}),
|
||||
"use_mmgp": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"unet_model": ("STRING", {"default": ""}),
|
||||
"lora_model": ("STRING", {"default": ""}),
|
||||
}
|
||||
}
|
||||
@classmethod
|
||||
def load(self, base_model, vae_model, adapter_path, scheduler, num_views,
|
||||
use_fp16, use_mmgp, unet_model="", lora_model=""):
|
||||
|
||||
|
||||
dtype = torch.float16 if use_fp16 else torch.float32
|
||||
vae_model = None if vae_model == "None" else vae_model
|
||||
unet_model = None if not unet_model else unet_model
|
||||
lora_model = None if not lora_model else lora_model
|
||||
|
||||
pipe = mvadapter_prepare_pipeline(
|
||||
base_model=base_model,
|
||||
vae_model=vae_model,
|
||||
unet_model=unet_model,
|
||||
lora_model=lora_model,
|
||||
adapter_path=adapter_path,
|
||||
scheduler=scheduler,
|
||||
num_views=num_views,
|
||||
device=DEVICE_STR,
|
||||
dtype=dtype,
|
||||
use_mmgp=use_mmgp,
|
||||
adapter_local_path=cls.CKPT_MVADAPTER_PATH
|
||||
)
|
||||
|
||||
print("MV-Adapter pipeline loaded successfully")
|
||||
return (pipe,)
|
||||
|
||||
|
||||
class MVAdapter_Image_To_MultiView:
|
||||
"""Generate multi-view images from single image and 3D mesh"""
|
||||
CATEGORY = "Comfy3D/Algorithm"
|
||||
RETURN_TYPES = ("IMAGE",)
|
||||
RETURN_NAMES = ("multiview_images",)
|
||||
FUNCTION = "run"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"mvadapter_pipe": ("DIFFUSERS_PIPE",),
|
||||
"mesh_path": ("STRING", {"default": ""}),
|
||||
"reference_image": ("IMAGE",),
|
||||
"prompt": ("STRING", {"default": "high quality", "multiline": True}),
|
||||
"negative_prompt": ("STRING", {"default": "watermark, ugly, deformed, noisy, blurry, low contrast", "multiline": True}),
|
||||
"num_inference_steps": ("INT", {"default": 50, "min": 1, "max": 200}),
|
||||
"guidance_scale": ("FLOAT", {"default": 3.0, "min": 0.0, "max": 20.0, "step": 0.1}),
|
||||
"reference_conditioning_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
||||
"height": ("INT", {"default": 768, "min": 256, "max": 2048, "step": 8}),
|
||||
"width": ("INT", {"default": 768, "min": 256, "max": 2048, "step": 8}),
|
||||
"seed": ("INT", {"default": -1, "min": -1, "max": 0xffffffffffffffff}),
|
||||
"remove_background": ("BOOLEAN", {"default": False}),
|
||||
},
|
||||
"optional": {
|
||||
"lora_scale": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 2.0, "step": 0.1}),
|
||||
}
|
||||
}
|
||||
|
||||
def run(self, mvadapter_pipe, mesh_path, reference_image, prompt, negative_prompt,
|
||||
num_inference_steps, guidance_scale, reference_conditioning_scale,
|
||||
height, width, seed, remove_background, lora_scale=1.0):
|
||||
|
||||
# Convert input image
|
||||
if isinstance(reference_image, torch.Tensor):
|
||||
reference_images = torch_imgs_to_pils(reference_image)
|
||||
reference_image = reference_images[0]
|
||||
|
||||
# Check if mesh_path exists
|
||||
if not mesh_path or not os.path.exists(mesh_path):
|
||||
raise ValueError(f"Mesh path does not exist: {mesh_path}")
|
||||
|
||||
# Create background removal function if needed
|
||||
remove_bg_fn = None
|
||||
if remove_background:
|
||||
remove_bg_fn = create_bg_remover(DEVICE_STR)
|
||||
|
||||
# Execute generation
|
||||
num_views = 6 # Standard number of views
|
||||
images, pos_images, normal_images, processed_ref_image = mvadapter_run_pipeline(
|
||||
pipe=mvadapter_pipe,
|
||||
mesh_path=mesh_path,
|
||||
num_views=num_views,
|
||||
text=prompt,
|
||||
image=reference_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,
|
||||
lora_scale=lora_scale,
|
||||
device=DEVICE_STR,
|
||||
)
|
||||
|
||||
# Create image grid
|
||||
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)
|
||||
|
||||
print(f"Generated multiview images: {grid_tensor.shape}")
|
||||
return (grid_tensor,)
|
||||
|
||||
Reference in New Issue
Block a user