update custom nodes
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
[submodule "chord"]
|
||||
path = chord
|
||||
url = https://github.com/ubisoft/ubisoft-laforge-chord.git
|
||||
@@ -0,0 +1,99 @@
|
||||
Ubisoft Machine Learning License (Research-Only - Copyleft)
|
||||
|
||||
This license governs the use, reproduction, and distribution of the Licensed
|
||||
Materials, including AI Models and associated source code for the sole purpose
|
||||
of scientific research. By accessing, downloading or using the Licensed
|
||||
Materials, you hereby accept to be bound by this [Ubisoft Machine Learning
|
||||
License (Research-Only - Copyleft)] agreement (hereinafter the “License”).
|
||||
|
||||
1. Licensed Materials
|
||||
|
||||
- AI Models
|
||||
- Source Code
|
||||
|
||||
2. Definitions
|
||||
|
||||
“Licensed Materials”: Refers to the AI Models and/or Source Code licensed under
|
||||
this agreement.
|
||||
"Source Code" means the preferred form of the work for making modifications to
|
||||
it corresponding to text written using human-readable programming language.
|
||||
"Object Code" means any non-source form of a work.
|
||||
“AI Model” means any machine learning based assembly or assemblies (including
|
||||
checkpoints), consisting of learnt weights, parameters (including optimizer
|
||||
states), corresponding to the model architecture as embodied in the Source Code.
|
||||
“Output” means the results of operating an AI Model as embodied in
|
||||
informational content resulting therefrom.
|
||||
“Derivative”: Any work derived from or based upon the Licensed Materials,
|
||||
including modifications.
|
||||
“Permitted Purpose”: Use for academic or research purposes only. Commercial
|
||||
use is strictly prohibited.
|
||||
“Distribution”: Any sharing of the Licensed Materials or Derivatives with third
|
||||
parties, including hosting as a service.
|
||||
“Licensor”: The rights holder or authorized entity granting this License.
|
||||
“You”: The individual or entity receiving and exercising rights under this
|
||||
License.
|
||||
|
||||
3. Grant of Rights
|
||||
|
||||
Subject to compliance with the terms of this License, You are granted a
|
||||
worldwide, royalty-free, non-exclusive License to use, study, reproduce,
|
||||
modify, and distribute the Licensed Materials and Derivatives solely for the
|
||||
Permitted Purpose. As between You and Licensor, Licensor claims no rights in
|
||||
the Outputs You generate using the AI Models used in accordance with the
|
||||
Permitted Purpose.
|
||||
|
||||
4. Distribution of Licensed Materials and Derivatives
|
||||
|
||||
Any Distribution of the Derivatives of the Licensed Materials, or the Licensed
|
||||
Materials shall be licensed under the same exact terms as this License.
|
||||
Redistribution shall include this License and retain all notices of author
|
||||
attribution and all modifications shall be clearly marked.
|
||||
|
||||
5. Use Restrictions
|
||||
|
||||
You shall not use the Licensed Materials or its Derivatives for:
|
||||
- any other purposes than the Permitted Purpose, including for commercial
|
||||
purposes such as using the Licensed Materials in any activity intended for
|
||||
commercial advantage or monetary compensation directly or indirectly;
|
||||
- weaponry, warfare, military applications, surveillance, or any activity that
|
||||
may cause harm or violate human rights;
|
||||
- engaging or enabling fully automated decision-making that may adversely
|
||||
impacts a natural person's legal rights;
|
||||
- providing medical advice or making clinical decisions;
|
||||
- generating content that promotes or incites hatred, violence, discrimination,
|
||||
or harm based on race, ethnicity, religion, gender, sexual orientation, or
|
||||
any other protected characteristic;
|
||||
- generating content that includes depictions of sexual abuse, sexual
|
||||
violence, explicit pornography, or any form of non-consensual acts and/or
|
||||
generating content that includes depictions of child nudity, child
|
||||
pornography, or any form of child exploitation;
|
||||
|
||||
6. Disclaimer of Warranty
|
||||
|
||||
THE LICENSED MATERIALS IS PROVIDED "AS IS" AND “AS AVAILABLE” WITHOUT
|
||||
WARRANTIES OF ANY KIND WHETHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE,
|
||||
NON-INFRINGEMENT, CORRECTNESS, ACCURACY, OR RELIABILITY. THE LICENSOR DISCLAIMS
|
||||
ALL LIABILITY FOR DAMAGES RESULTING FROM THE USE OR INABILITY TO USE THE
|
||||
LICENSED MATERIALS. THE USE OF THE LICENSED MATERIALS AND ANY OUTPUTS YOU MAY
|
||||
GENERATE SHALL BE AT YOUR OWN RISK.
|
||||
|
||||
7. Termination
|
||||
|
||||
This License terminates automatically if You violate any of its terms. Upon
|
||||
termination, You shall cease all use and distribution of the Licensed
|
||||
Materials and its Derivatives.
|
||||
|
||||
8. Governing Law
|
||||
|
||||
The validity of this Agreement and any of its terms and provisions, as well as
|
||||
the rights and duties of the parties hereunder, shall be governed, interpreted
|
||||
and enforced in accordance with the laws of France.
|
||||
|
||||
9. Miscellaneous
|
||||
|
||||
If any provision of this License is held to be invalid, illegal or
|
||||
unenforceable, the remaining provisions shall be unaffected thereby and remain
|
||||
valid as if such provision had not been set forth herein.
|
||||
|
||||
Copyright (C) 2025 UBISOFT ENTERTAINMENT. All Rights Reserved.
|
||||
@@ -1,2 +1,47 @@
|
||||
# ComfyUI-Chord
|
||||
ComfyUI Custom Node for Chord
|
||||
|
||||
<a href="https://arxiv.org/abs/2509.09952"><img src="https://img.shields.io/badge/arXiv-2509.09952-B31B1B?logo=arxiv&logoColor=white&style=flat-square" alt="arXiv"></a>
|
||||
<a href="https://ubisoft-laforge.github.io/world/chord/"><img src="https://img.shields.io/badge/Project-Page-brightgreen?logo=ubisoft&logoColor=white&style=flat-square" alt="Project Page"></a>
|
||||
<a href="https://github.com/ubisoft/ubisoft-laforge-chord"><img src="https://img.shields.io/badge/Github-Code-blue?logo=github&logoColor=white&style=flat-square" alt="Code"></a>
|
||||
|
||||
ComfyUI Custom Node for paper: **Chord: Chain of Rendering Decomposition for PBR Material Estimation from Generated Texture Images**
|
||||
|
||||
## Installation
|
||||
|
||||
1. Download and install ComfyUI. If you are new to ComfyUI, refer to the original [repository](https://github.com/comfyanonymous/ComfyUI) to get started. **Make sure you have the latest version.**
|
||||
|
||||
2. Download the pretrained model from [Hugging Face](Ubisoft/ubisoft-laforge-chord) and place the **chord_v1.ckpt** file into the **ComfyUI/models/checkpoints** folder.
|
||||
|
||||
3. Install the custom nodes from the [ComfyUI-Manager](https://github.com/Comfy-Org/ComfyUI-Manager) or by cloning this repository recursively in the custom nodes folder of ComfyUI:
|
||||
|
||||
```shell
|
||||
cd ./ComfyUI/custom_nodes
|
||||
git clone --recursive https://github.com/ubisoft/ComfyUI-Chord.git
|
||||
```
|
||||
## Example Workflow
|
||||
|
||||

|
||||
|
||||
You can load this workflow using the JSON file:
|
||||
`examples/chord_example.json`
|
||||
|
||||
## License
|
||||
|
||||
This project is released under the **Ubisoft Machine Learning License (Research-Only - Copyleft)**. See the full terms in the [LICENSE](LICENSE) file
|
||||
## Citation
|
||||
|
||||
If you find our work useful, please consider citing:
|
||||
|
||||
```
|
||||
@misc{ying2025chord,
|
||||
title={Chord: Chain of Rendering Decomposition for PBR Material Estimation from Generated Texture Images},
|
||||
author={Zhi Ying and Boxiang Rong and Jingyu Wang and Maoyuan Xu},
|
||||
year={2025},
|
||||
eprint={2509.09952},
|
||||
archivePrefix={arXiv},
|
||||
primaryClass={cs.GR},
|
||||
url={https://arxiv.org/abs/2509.09952},
|
||||
}
|
||||
```
|
||||
|
||||
© [2025] Ubisoft Entertainment. All Rights Reserved.
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import os,sys
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "chord"))
|
||||
|
||||
from .nodes import ChordLoadModel, ChordMaterialEstimation, ChordNormalToHeight
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ChordLoadModel": ChordLoadModel,
|
||||
"ChordMaterialEstimation": ChordMaterialEstimation,
|
||||
"ChordNormalToHeight": ChordNormalToHeight,
|
||||
}
|
||||
__all__ = ["NODE_CLASS_MAPPINGS"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Submodule
+1
Submodule chord added at 3ce835852e
@@ -0,0 +1,412 @@
|
||||
{
|
||||
"id": "39d23e25-903c-4f5f-88e1-3bbadf18b7df",
|
||||
"revision": 0,
|
||||
"last_node_id": 25,
|
||||
"last_link_id": 38,
|
||||
"nodes": [
|
||||
{
|
||||
"id": 25,
|
||||
"type": "ChordMaterialEstimation",
|
||||
"pos": [
|
||||
669.1222107554908,
|
||||
312.46323813855713
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
118
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "model",
|
||||
"type": "MODEL",
|
||||
"link": 32
|
||||
},
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 33
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
34
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
35,
|
||||
38
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
36
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
37
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "ubisoft/ComfyUI-Chord",
|
||||
"ver": "cfadf742e1838ded9990d85e43650d55a96e7391",
|
||||
"Node name for S&R": "ChordMaterialEstimation"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1035.9285017533348,
|
||||
125.65524984290931
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 34
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"title": "Basecolor",
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.70",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1183.84326933871,
|
||||
126.43252808866265
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 5,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 38
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"title": "Normal",
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.70",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 24,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1035.5098787324794,
|
||||
407.8680029468901
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
"order": 7,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 37
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"title": "Roughness",
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.70",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1180,
|
||||
410
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
246.00000000000006
|
||||
],
|
||||
"flags": {},
|
||||
"order": 6,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 36
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"title": "Metalness",
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.70",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 20,
|
||||
"type": "ChordNormalToHeight",
|
||||
"pos": [
|
||||
1333.731988297213,
|
||||
253.143266404242
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "normal",
|
||||
"type": "IMAGE",
|
||||
"link": 35
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
25
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "ubisoft/ComfyUI-Chord",
|
||||
"ver": "cfadf742e1838ded9990d85e43650d55a96e7391",
|
||||
"Node name for S&R": "ChordNormalToHeight"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 21,
|
||||
"type": "PreviewImage",
|
||||
"pos": [
|
||||
1455.691637213612,
|
||||
354.32688389208334
|
||||
],
|
||||
"size": [
|
||||
140,
|
||||
246.00000000000006
|
||||
],
|
||||
"flags": {},
|
||||
"order": 8,
|
||||
"mode": 0,
|
||||
"inputs": [
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 25
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"title": "Height",
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.70",
|
||||
"Node name for S&R": "PreviewImage"
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"type": "ChordLoadModel",
|
||||
"pos": [
|
||||
374.58235480468545,
|
||||
141.5289281758196
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
58
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "MODEL",
|
||||
"type": "MODEL",
|
||||
"links": [
|
||||
32
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"aux_id": "ubisoft/ComfyUI-Chord",
|
||||
"ver": "cfadf742e1838ded9990d85e43650d55a96e7391",
|
||||
"Node name for S&R": "ChordLoadModel"
|
||||
},
|
||||
"widgets_values": [
|
||||
[
|
||||
"chord_v1.ckpt"
|
||||
]
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 13,
|
||||
"type": "LoadImage",
|
||||
"pos": [
|
||||
370,
|
||||
330
|
||||
],
|
||||
"size": [
|
||||
270,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
"inputs": [],
|
||||
"outputs": [
|
||||
{
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
33
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
"type": "MASK",
|
||||
"links": null
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"cnr_id": "comfy-core",
|
||||
"ver": "0.3.70",
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"ref_render.png",
|
||||
"image"
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
25,
|
||||
20,
|
||||
0,
|
||||
21,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
32,
|
||||
11,
|
||||
0,
|
||||
25,
|
||||
0,
|
||||
"MODEL"
|
||||
],
|
||||
[
|
||||
33,
|
||||
13,
|
||||
0,
|
||||
25,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
34,
|
||||
25,
|
||||
0,
|
||||
14,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
35,
|
||||
25,
|
||||
1,
|
||||
20,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
36,
|
||||
25,
|
||||
2,
|
||||
15,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
37,
|
||||
25,
|
||||
3,
|
||||
24,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
38,
|
||||
25,
|
||||
1,
|
||||
16,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
],
|
||||
"groups": [],
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 1.1102851430715899,
|
||||
"offset": [
|
||||
-148.2607809733155,
|
||||
51.621946976768974
|
||||
]
|
||||
},
|
||||
"frontendVersion": "1.28.8"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 447 KiB |
@@ -0,0 +1,135 @@
|
||||
import os
|
||||
import torch
|
||||
from omegaconf import OmegaConf
|
||||
from torchvision.transforms import v2
|
||||
from .normal_to_height import normal_to_height
|
||||
|
||||
# Modules from ComfyUI
|
||||
import folder_paths
|
||||
from comfy_api.latest import ComfyExtension, io
|
||||
|
||||
# Modules from ComfyUI-Chord
|
||||
from chord import ChordModel
|
||||
from chord.io import read_image, save_maps
|
||||
|
||||
def apply_padding(model, mode):
|
||||
for layer in [layer for _, layer in model.named_modules() if isinstance(layer, torch.nn.Conv2d)]:
|
||||
layer.padding_mode = mode
|
||||
|
||||
def apply_circular_padding(model):
|
||||
if hasattr(model, 'sd'):
|
||||
apply_padding(model.sd.vae, 'circular')
|
||||
apply_padding(model.sd.unet, 'circular')
|
||||
else:
|
||||
apply_padding(model, 'circular')
|
||||
|
||||
class ChordLoadModel(io.ComfyNode):
|
||||
"""Node to load Chord Model"""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> io.Schema:
|
||||
return io.Schema(
|
||||
node_id="ChordLoadModel",
|
||||
display_name="Chord - Load Model",
|
||||
category="Chord",
|
||||
description="A node to load a Chord model.",
|
||||
inputs=[
|
||||
io.Combo.Input(
|
||||
"ckpt_name",
|
||||
options=([x for x in folder_paths.get_filename_list("checkpoints") if x.endswith("ckpt")], {})
|
||||
),
|
||||
],
|
||||
outputs=[io.Model.Output("model")],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, ckpt_name) -> io.NodeOutput:
|
||||
device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||
if type(ckpt_name) is list:
|
||||
ckpt_name = ckpt_name[0]
|
||||
ckpt_path = folder_paths.get_full_path("checkpoints", ckpt_name)
|
||||
config = OmegaConf.load(os.path.join(os.path.dirname(__file__), "chord/config/chord.yaml"))
|
||||
model = ChordModel(config)
|
||||
ckpt = torch.load(ckpt_path, map_location="cpu", weights_only=False)
|
||||
try:
|
||||
model.load_state_dict(ckpt["state_dict"])
|
||||
except RuntimeError as e:
|
||||
raise RuntimeError('Failed to load model, check if the checkpoint file is correct.\n{}'.format(repr(e)))
|
||||
model.to(device)
|
||||
model.eval()
|
||||
return io.NodeOutput(model)
|
||||
|
||||
class ChordMaterialEstimation(io.ComfyNode):
|
||||
"""Chord Material Estimation Node"""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> io.Schema:
|
||||
return io.Schema(
|
||||
node_id="ChordMaterialEstimation",
|
||||
display_name="Chord - Material Estimation",
|
||||
category="Chord",
|
||||
description="A node to estimate material maps from a texture image using the Chord model.",
|
||||
inputs=[
|
||||
io.Model.Input(
|
||||
"model",
|
||||
tooltip="The Chord model used to estimate material."
|
||||
),
|
||||
io.Image.Input(
|
||||
"image",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output("basecolor"),
|
||||
io.Image.Output("normal"),
|
||||
io.Image.Output("roughness"),
|
||||
io.Image.Output("metalness"),
|
||||
]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, model, image) -> io.NodeOutput:
|
||||
device = next(model.parameters()).device
|
||||
apply_circular_padding(model)
|
||||
image = image.permute(0,3,1,2).to(device)
|
||||
ori_h, ori_w = image.shape[-2:]
|
||||
x = v2.Resize(size=(1024, 1024), antialias=True)(image)
|
||||
with torch.no_grad() as no_grad, torch.autocast(device_type=device.type) as amp:
|
||||
output = model(x)
|
||||
for key in output.keys():
|
||||
output[key] = v2.Resize(size=(ori_h, ori_w), antialias=True)(output[key])
|
||||
if output[key].ndim == 4:
|
||||
output[key] = output[key].permute(0,2,3,1)
|
||||
return io.NodeOutput(output['basecolor'], output['normal'], output['roughness'], output['metalness'])
|
||||
|
||||
class ChordNormalToHeight(io.ComfyNode):
|
||||
"""Integrate normal map to height map using Poisson solver with overlapping subregions."""
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> io.Schema:
|
||||
return io.Schema(
|
||||
node_id="ChordNormalToHeight",
|
||||
display_name="Chord - Normal to Height",
|
||||
category="Chord",
|
||||
description="A node to convert normal map to height map using Poisson solver with overlapping subregions.",
|
||||
inputs=[
|
||||
io.Image.Input(
|
||||
"normal",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output("height"),
|
||||
],
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(cls, normal) -> io.NodeOutput:
|
||||
normal = normal.permute(0,3,1,2)
|
||||
height_var_threshold = 5e-4
|
||||
ori_h, ori_w = normal.shape[-2:]
|
||||
x = v2.Resize(size=(1024, 1024), antialias=True)(normal)
|
||||
height = normal_to_height(x)[None, None].squeeze(1)
|
||||
if height.var() < height_var_threshold and height.var() > 0:
|
||||
height = normal_to_height(x, skip_normalize_normal=True)[None, None].squeeze(1)
|
||||
|
||||
height = v2.Resize(size=(ori_h, ori_w), antialias=True)(height)
|
||||
return io.NodeOutput(height)
|
||||
@@ -0,0 +1,175 @@
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from torch.signal.windows import hann
|
||||
import torch.fft as fft_module
|
||||
|
||||
def compute_divergence(fx: torch.tensor, fy: torch.tensor) -> torch.tensor:
|
||||
div_x = F.pad(fx[:, :, 1:] - fx[:, :, :-1], (0, 1, 0, 0), mode='constant')
|
||||
div_y = F.pad(fy[:, 1:, :] - fy[:, :-1, :], (0, 0, 0, 1), mode='constant')
|
||||
return div_x + div_y
|
||||
|
||||
def solve_poisson_fft(div: torch.tensor, h: int, w: int) -> torch.tensor:
|
||||
fft_div = fft_module.fft2(div)
|
||||
|
||||
kx = fft_module.fftfreq(2*w, device=div.device) * 2 * torch.pi
|
||||
ky = fft_module.fftfreq(2*h, device=div.device) * 2 * torch.pi
|
||||
kx, ky = torch.meshgrid(kx, ky, indexing='xy')
|
||||
epsilon = 1e-9
|
||||
denom = 4 - 2 * torch.cos(kx) - 2 * torch.cos(ky)
|
||||
denom = torch.where(torch.abs(denom) > epsilon, denom, epsilon)
|
||||
|
||||
height_map_full = torch.real(fft_module.ifft2(fft_div / denom))
|
||||
return torch.nan_to_num(height_map_full[:, :h, :w])
|
||||
|
||||
def apply_window_function(gradient: torch.tensor) -> torch.tensor:
|
||||
hann_window = hann(gradient.shape[-2], device=gradient.device)[:, None] * hann(gradient.shape[-1], device=gradient.device)[None, :]
|
||||
return gradient * hann_window
|
||||
|
||||
def compute_height(normal_map: torch.tensor, epsilon: float = 1e-8) -> torch.tensor:
|
||||
h, w = normal_map.shape[-2:]
|
||||
nz = normal_map[:, 2]
|
||||
nz_safe = torch.where(torch.abs(nz) > epsilon, nz, epsilon)
|
||||
fx = normal_map[:, 0] / nz_safe
|
||||
fy = normal_map[:, 1] / nz_safe
|
||||
|
||||
fx = apply_window_function(fx)
|
||||
fy = apply_window_function(fy)
|
||||
|
||||
div = compute_divergence(fx, fy)
|
||||
div = F.pad(div, (0, w, 0, h), mode='constant')
|
||||
height_map = solve_poisson_fft(div, h, w)
|
||||
return height_map - torch.mean(height_map)
|
||||
|
||||
def extend_normal_map(normal_map: torch.tensor, region_size: int) -> torch.tensor:
|
||||
larger_normal_map = F.pad(normal_map, (region_size, region_size, region_size, region_size), mode='circular')
|
||||
return larger_normal_map
|
||||
|
||||
def define_subregions(
|
||||
h: int,
|
||||
w: int,
|
||||
min_region_size: int=128,
|
||||
overlap_factor: float = 0.5
|
||||
) -> list:
|
||||
step_size = int(min_region_size - min_region_size * overlap_factor)
|
||||
if step_size <= 0:
|
||||
step_size = min_region_size
|
||||
overlap_size = int(min_region_size * overlap_factor)
|
||||
subregions = []
|
||||
for y in range(0, h, step_size):
|
||||
for x in range(0, w, step_size):
|
||||
y_end = min(y + min_region_size + overlap_size, h)
|
||||
x_end = min(x + min_region_size + overlap_size, w)
|
||||
subregions.append((y, y_end, x, x_end))
|
||||
return subregions
|
||||
|
||||
def cosine_smoothing(x: torch.tensor) -> torch.tensor:
|
||||
return 0.5 * (1 - torch.cos(torch.pi * x))
|
||||
|
||||
def create_subregions(
|
||||
normal_map: torch.tensor,
|
||||
subregions: list,
|
||||
) -> list:
|
||||
results = []
|
||||
for region in subregions:
|
||||
y, y_end, x, x_end = region
|
||||
sub_map = normal_map[:, y:y_end, x:x_end]
|
||||
results.append(sub_map)
|
||||
return results
|
||||
|
||||
def cosine_blending_update_weight(
|
||||
sub_weight_map: torch.tensor,
|
||||
subregion,
|
||||
shape,
|
||||
region_size: int = 128,
|
||||
) -> None:
|
||||
h, w = sub_weight_map.shape[-2:]
|
||||
d = sub_weight_map.device
|
||||
y_start, y_end, x_start, x_end = subregion
|
||||
if y_start > 0:
|
||||
overlap = min(region_size, h)
|
||||
y_smooth = cosine_smoothing(torch.linspace(0, 1, overlap, device=d))[:, None]
|
||||
sub_weight_map[:overlap, :] *= y_smooth
|
||||
if y_end < shape[0]:
|
||||
overlap = min(region_size, h)
|
||||
y_smooth = cosine_smoothing(torch.linspace(1, 0, overlap, device=d))[:, None]
|
||||
sub_weight_map[-overlap:, :] *= y_smooth
|
||||
if x_start > 0:
|
||||
overlap = min(region_size, w)
|
||||
x_smooth = cosine_smoothing(torch.linspace(0, 1, overlap, device=d))
|
||||
sub_weight_map[:, :overlap] *= x_smooth
|
||||
if x_end < shape[1]:
|
||||
overlap = min(region_size, w)
|
||||
x_smooth = cosine_smoothing(torch.linspace(1, 0, overlap, device=d))
|
||||
sub_weight_map[:, -overlap:] *= x_smooth
|
||||
|
||||
def combine_sub_height_maps(
|
||||
height_maps: list,
|
||||
subregions: list,
|
||||
h: int,
|
||||
w: int,
|
||||
epsilon: float = 1e-8,
|
||||
) -> torch.tensor:
|
||||
height_map = torch.zeros((h, w), device=height_maps[0].device)
|
||||
weight_map = torch.zeros((h, w), device=height_maps[0].device)
|
||||
|
||||
for subregion, sub_height_map in zip(subregions, height_maps):
|
||||
y_start, y_end, x_start, x_end = subregion
|
||||
y_slice = slice(y_start, y_end)
|
||||
x_slice = slice(x_start, x_end)
|
||||
|
||||
sub_weight_map = torch.ones_like(sub_height_map)
|
||||
cosine_blending_update_weight(
|
||||
sub_weight_map,
|
||||
subregion,
|
||||
(h, w)
|
||||
)
|
||||
|
||||
height_map[y_slice, x_slice] += sub_height_map * sub_weight_map
|
||||
weight_map[y_slice, x_slice] += sub_weight_map
|
||||
|
||||
return height_map / (weight_map + epsilon)
|
||||
|
||||
def crop_height_map(height_map: torch.tensor, original_shape: tuple, region_size: int=128) -> torch.tensor:
|
||||
original_height, original_width = original_shape
|
||||
return height_map[
|
||||
region_size:region_size + original_height,
|
||||
region_size:region_size + original_width
|
||||
]
|
||||
|
||||
def normalize_height_map(data: torch.tensor, eps: float = 1e-8) -> torch.tensor:
|
||||
return (data - data.min()) / (data.max() - data.min() + eps)
|
||||
|
||||
def normal_to_height(
|
||||
normal_map: torch.tensor,
|
||||
subdivisions: int = 16,
|
||||
min_region_size: int = 128,
|
||||
skip_normalize_normal: bool = False,
|
||||
) -> torch.tensor:
|
||||
'''
|
||||
Args:
|
||||
normal_map: torch.tensor(1, 3, H, W)
|
||||
subdivisions: int, subdivision level at each edge
|
||||
min_region_size: int, minimal region size
|
||||
skip_normalize_normal: bool, if skip normalization of input normal map
|
||||
'''
|
||||
if normal_map.dim() == 4:
|
||||
try: assert normal_map.shape[0] == 1
|
||||
except: breakpoint()
|
||||
normal_map = normal_map.squeeze(0)
|
||||
h, w = normal_map.shape[-2:]
|
||||
if not skip_normalize_normal:
|
||||
normal_map = F.normalize(normal_map * 2.0 - 1.0, dim=0)
|
||||
region_size = min(
|
||||
max(min(h, w) // subdivisions, min_region_size),
|
||||
min(h, w)
|
||||
)
|
||||
larger_normal_map = extend_normal_map(normal_map, region_size)
|
||||
lh, lw = larger_normal_map.shape[-2:]
|
||||
subregions = define_subregions(lh, lw, region_size)
|
||||
map_batch = create_subregions(larger_normal_map, subregions)
|
||||
sub_height_maps = []
|
||||
for m in map_batch:
|
||||
sub_height_maps.append(compute_height(m.unsqueeze(0)).squeeze(0))
|
||||
height_combined = combine_sub_height_maps(sub_height_maps, subregions, lh, lw)
|
||||
height_cropped = crop_height_map(height_combined, (h, w), region_size)
|
||||
return normalize_height_map(height_cropped)
|
||||
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "comfyui-chord"
|
||||
description = "PBR material estimation custom nodes for paper: Chord: Chain of Rendering Decomposition for PBR Material Estimation from Generated Texture Images"
|
||||
version = "v1.0"
|
||||
license = { text = "Ubisoft Machine Learning License (Research-Only - Copyleft)" }
|
||||
dependencies = []
|
||||
|
||||
[project.urls]
|
||||
Repository = "https://github.com/ubisoft/ComfyUI-Chord"
|
||||
|
||||
[tool.comfy]
|
||||
PublisherId = "Ubisoft-LaForge"
|
||||
DisplayName = "ComfyUI-Chord"
|
||||
Icon = "https://www.ubisoft.com/favicon.ico"
|
||||
@@ -0,0 +1,7 @@
|
||||
huggingface_hub[hf_xet]
|
||||
diffusers
|
||||
transformers
|
||||
tokenizers
|
||||
typer
|
||||
omegaconf
|
||||
imageio
|
||||
Reference in New Issue
Block a user