Merge pull request #501 from DenisKochetov/hun-2.1

add hunyuan 2.1
This commit is contained in:
Mr. For Example
2025-06-19 12:10:11 +01:00
committed by GitHub
127 changed files with 72260 additions and 3 deletions
@@ -0,0 +1,82 @@
model:
target: hy3dshape.models.denoisers.hunyuandit.HunYuanDiTPlain
params:
input_size: &num_latents 4096
in_channels: 64
hidden_size: 2048
context_dim: 1024
depth: 21
num_heads: 16
qk_norm: true
text_len: 1370
with_decoupled_ca: false
use_attention_pooling: false
qk_norm_type: 'rms'
qkv_bias: false
use_pos_emb: false
num_moe_layers: 6
num_experts: 8
moe_top_k: 2
vae:
target: hy3dshape.models.autoencoders.ShapeVAE
params:
num_latents: *num_latents
embed_dim: 64
num_freqs: 8
include_pi: false
heads: 16
width: 1024
num_encoder_layers: 8
num_decoder_layers: 16
qkv_bias: false
qk_norm: true
scale_factor: 1.0039506158752403
geo_decoder_mlp_expand_ratio: 4
geo_decoder_downsample_ratio: 1
geo_decoder_ln_post: true
point_feats: 4
pc_size: 81920
pc_sharpedge_size: 0
conditioner:
target: hy3dshape.models.conditioner.SingleImageEncoder
params:
main_image_encoder:
type: DinoImageEncoder # dino large
kwargs:
config:
attention_probs_dropout_prob: 0.0
drop_path_rate: 0.0
hidden_act: gelu
hidden_dropout_prob: 0.0
hidden_size: 1024
image_size: 518
initializer_range: 0.02
layer_norm_eps: 1.e-6
layerscale_value: 1.0
mlp_ratio: 4
model_type: dinov2
num_attention_heads: 16
num_channels: 3
num_hidden_layers: 24
patch_size: 14
qkv_bias: true
torch_dtype: float32
use_swiglu_ffn: false
image_size: 518
use_cls_token: true
scheduler:
target: hy3dshape.schedulers.FlowMatchEulerDiscreteScheduler
params:
num_train_timesteps: 1000
image_processor:
target: hy3dshape.preprocessors.ImageProcessorV2
params:
size: 512
border_ratio: 0.15
pipeline:
target: hy3dshape.pipelines.Hunyuan3DDiTFlowMatchingPipeline
@@ -0,0 +1,20 @@
{
"crop_size": 224,
"do_center_crop": true,
"do_convert_rgb": true,
"do_normalize": true,
"do_resize": true,
"feature_extractor_type": "CLIPFeatureExtractor",
"image_mean": [
0.48145466,
0.4578275,
0.40821073
],
"image_std": [
0.26862954,
0.26130258,
0.27577711
],
"resample": 3,
"size": 224
}
@@ -0,0 +1,23 @@
{
"_name_or_path": "vision_encoder",
"architectures": [
"CLIPVisionModelWithProjection"
],
"attention_dropout": 0.0,
"dropout": 0.0,
"hidden_act": "gelu",
"hidden_size": 1280,
"image_size": 224,
"initializer_factor": 1.0,
"initializer_range": 0.02,
"intermediate_size": 5120,
"layer_norm_eps": 1e-05,
"model_type": "clip_vision_model",
"num_attention_heads": 16,
"num_channels": 3,
"num_hidden_layers": 32,
"patch_size": 14,
"projection_dim": 1024,
"torch_dtype": "float16",
"transformers_version": "4.36.0"
}
@@ -0,0 +1,37 @@
{
"_class_name": "HunyuanPaintPipeline",
"_diffusers_version": "0.24.0",
"feature_extractor": [
"transformers",
"CLIPImageProcessor"
],
"requires_safety_checker": false,
"safety_checker": [
null,
null
],
"scheduler": [
"diffusers",
"DDIMScheduler"
],
"text_encoder": [
"transformers",
"CLIPTextModel"
],
"tokenizer": [
"transformers",
"CLIPTokenizer"
],
"unet": [
"modules",
"UNet2p5DConditionModel"
],
"vae": [
"diffusers",
"AutoencoderKL"
],
"image_encoder": [
"transformers",
"CLIPVisionModelWithProjection"
]
}
@@ -0,0 +1,15 @@
{
"_class_name": "DDIMScheduler",
"_diffusers_version": "0.23.1",
"beta_end": 0.012,
"beta_schedule": "scaled_linear",
"beta_start": 0.00085,
"clip_sample": false,
"num_train_timesteps": 1000,
"prediction_type": "v_prediction",
"set_alpha_to_one": true,
"steps_offset": 1,
"trained_betas": null,
"timestep_spacing": "trailing",
"rescale_betas_zero_snr": true
}
@@ -0,0 +1,25 @@
{
"_name_or_path": "stabilityai/stable-diffusion-2",
"architectures": [
"CLIPTextModel"
],
"attention_dropout": 0.0,
"bos_token_id": 0,
"dropout": 0.0,
"eos_token_id": 2,
"hidden_act": "gelu",
"hidden_size": 1024,
"initializer_factor": 1.0,
"initializer_range": 0.02,
"intermediate_size": 4096,
"layer_norm_eps": 1e-05,
"max_position_embeddings": 77,
"model_type": "clip_text_model",
"num_attention_heads": 16,
"num_hidden_layers": 23,
"pad_token_id": 1,
"projection_dim": 512,
"torch_dtype": "float32",
"transformers_version": "4.25.0.dev0",
"vocab_size": 49408
}
@@ -0,0 +1,24 @@
{
"bos_token": {
"content": "<|startoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
},
"eos_token": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
},
"pad_token": "!",
"unk_token": {
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
}
}
@@ -0,0 +1,34 @@
{
"add_prefix_space": false,
"bos_token": {
"__type": "AddedToken",
"content": "<|startoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
},
"do_lower_case": true,
"eos_token": {
"__type": "AddedToken",
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
},
"errors": "replace",
"model_max_length": 77,
"name_or_path": "stabilityai/stable-diffusion-2",
"pad_token": "<|endoftext|>",
"special_tokens_map_file": "./special_tokens_map.json",
"tokenizer_class": "CLIPTokenizer",
"unk_token": {
"__type": "AddedToken",
"content": "<|endoftext|>",
"lstrip": false,
"normalized": true,
"rstrip": false,
"single_word": false
}
}
@@ -0,0 +1,45 @@
{
"_class_name": "UNet2DConditionModel",
"_diffusers_version": "0.10.0.dev0",
"act_fn": "silu",
"attention_head_dim": [
5,
10,
20,
20
],
"block_out_channels": [
320,
640,
1280,
1280
],
"center_input_sample": false,
"cross_attention_dim": 1024,
"down_block_types": [
"CrossAttnDownBlock2D",
"CrossAttnDownBlock2D",
"CrossAttnDownBlock2D",
"DownBlock2D"
],
"downsample_padding": 1,
"dual_cross_attention": false,
"flip_sin_to_cos": true,
"freq_shift": 0,
"in_channels": 4,
"layers_per_block": 2,
"mid_block_scale_factor": 1,
"norm_eps": 1e-05,
"norm_num_groups": 32,
"num_class_embeds": null,
"only_cross_attention": false,
"out_channels": 4,
"sample_size": 64,
"up_block_types": [
"UpBlock2D",
"CrossAttnUpBlock2D",
"CrossAttnUpBlock2D",
"CrossAttnUpBlock2D"
],
"use_linear_projection": true
}
@@ -0,0 +1,29 @@
{
"_class_name": "AutoencoderKL",
"_diffusers_version": "0.10.0.dev0",
"act_fn": "silu",
"block_out_channels": [
128,
256,
512,
512
],
"down_block_types": [
"DownEncoderBlock2D",
"DownEncoderBlock2D",
"DownEncoderBlock2D",
"DownEncoderBlock2D"
],
"in_channels": 3,
"latent_channels": 4,
"layers_per_block": 2,
"norm_num_groups": 32,
"out_channels": 3,
"sample_size": 768,
"up_block_types": [
"UpDecoderBlock2D",
"UpDecoderBlock2D",
"UpDecoderBlock2D",
"UpDecoderBlock2D"
]
}
@@ -0,0 +1,19 @@
target: hy3dshape.models.ShapeVAE
params:
num_latents: 4096
embed_dim: 64
num_freqs: 8
include_pi: false
heads: 16
width: 1024
num_encoder_layers: 8
num_decoder_layers: 16
qkv_bias: false
qk_norm: true
scale_factor: 1.0039506158752403
geo_decoder_mlp_expand_ratio: 4
geo_decoder_downsample_ratio: 1
geo_decoder_ln_post: true
point_feats: 4
pc_size: 81920
pc_sharpedge_size: 0
+185
View File
@@ -0,0 +1,185 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
!hy3dpaint/custom_rasterizer/lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
.DS_Store
# Cython debug symbols
cython_debug/
gradio_cache/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
hy3dshape/tools/cmd*.sh
hy3dshape/output_folder/
demo.glb
demo_textured.glb
demo_textured.jpg
demo_textured.mtl
demo_textured_metallic.jpg
demo_textured_roughness.jpg
demo_untextured.glb
white_mesh_remesh.obj
hy3dpaint/ckpt/
+73
View File
@@ -0,0 +1,73 @@
# __init__.py for Hunyuan3D-2.1
import os
import sys
import torch
import logging
import platform
# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger('ComfyUI-Hunyuan3D-2.1')
# Add parent directory to Python path
current_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.dirname(current_dir)
# Add paths for Hunyuan3D-2.1 modules
hy3dshape_path = os.path.join(current_dir, "hy3dshape")
hy3dpaint_path = os.path.join(current_dir, "hy3dpaint")
# Add all necessary paths
paths_to_add = [
current_dir,
hy3dshape_path,
hy3dpaint_path,
]
for path in paths_to_add:
if os.path.exists(path) and path not in sys.path:
sys.path.insert(0, path)
logger.info(f"Added Hunyuan3D-2.1 path to sys.path: {path}")
# Apply torchvision fix before other imports
try:
from hy3dpaint.utils.torchvision_fix import apply_fix
apply_fix()
logger.info("Torchvision fix applied for Hunyuan3D-2.1")
except Exception as e:
logger.error(f"Warning: Failed to apply torchvision fix: {e}")
# Import mmgp for memory management
try:
from mmgp import offload, profile_type
logger.info("mmgp available for memory management")
except ImportError:
logger.warning("Warning: mmgp module not found")
except Exception as e:
logger.error(f"Warning: Failed to import mmgp: {e}")
# Import key modules with aliases to avoid conflicts with old Hunyuan3D
from hy3dshape import FaceReducer as FaceReducer_2_1
from hy3dshape import FloaterRemover as FloaterRemover_2_1
from hy3dshape import DegenerateFaceRemover as DegenerateFaceRemover_2_1
from hy3dshape.pipelines import Hunyuan3DDiTFlowMatchingPipeline as Hunyuan3DDiTFlowMatchingPipeline_2_1
from hy3dshape.pipelines import export_to_trimesh as export_to_trimesh_2_1
from hy3dshape.rembg import BackgroundRemover as BackgroundRemover_2_1
from hy3dpaint.textureGenPipeline import Hunyuan3DPaintPipeline as Hunyuan3DPaintPipeline_2_1
from hy3dpaint.textureGenPipeline import Hunyuan3DPaintConfig as Hunyuan3DPaintConfig_2_1
from hy3dpaint.convert_utils import create_glb_with_pbr_materials as create_glb_with_pbr_materials_2_1
logger.info("Hunyuan3D-2.1 modules loaded successfully")
# Export modules with safe aliases
__all__ = [
'FaceReducer_2_1',
'FloaterRemover_2_1',
'DegenerateFaceRemover_2_1',
'Hunyuan3DDiTFlowMatchingPipeline_2_1',
'export_to_trimesh_2_1',
'BackgroundRemover_2_1',
'Hunyuan3DPaintPipeline_2_1',
'Hunyuan3DPaintConfig_2_1',
'create_glb_with_pbr_materials_2_1',
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,11 @@
# Try to import C++ version first, fallback to Python version
try:
from .mesh_inpaint_processor import meshVerticeInpaint
print("[INFO] Using compiled C++ mesh_inpaint_processor (fast)")
except ImportError:
try:
from .mesh_inpaint_processor_fallback import meshVerticeInpaint
print("[WARNING] Using Python fallback mesh_inpaint_processor (slower)")
print("[INFO] To use faster C++ version, run: compile_mesh_painter.bat")
except ImportError:
print("[ERROR] Neither C++ nor Python mesh_inpaint_processor found!")
@@ -0,0 +1,107 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import math
import numpy as np
import torch
def transform_pos(mtx, pos, keepdim=False):
t_mtx = torch.from_numpy(mtx).to(pos.device) if isinstance(mtx, np.ndarray) else mtx
if pos.shape[-1] == 3:
posw = torch.cat([pos, torch.ones([pos.shape[0], 1]).to(pos.device)], axis=1)
else:
posw = pos
if keepdim:
return torch.matmul(posw, t_mtx.t())[...]
else:
return torch.matmul(posw, t_mtx.t())[None, ...]
def get_mv_matrix(elev, azim, camera_distance, center=None):
elev = -elev
azim += 90
elev_rad = math.radians(elev)
azim_rad = math.radians(azim)
camera_position = np.array(
[
camera_distance * math.cos(elev_rad) * math.cos(azim_rad),
camera_distance * math.cos(elev_rad) * math.sin(azim_rad),
camera_distance * math.sin(elev_rad),
]
)
if center is None:
center = np.array([0, 0, 0])
else:
center = np.array(center)
lookat = center - camera_position
lookat = lookat / np.linalg.norm(lookat)
up = np.array([0, 0, 1.0])
right = np.cross(lookat, up)
right = right / np.linalg.norm(right)
up = np.cross(right, lookat)
up = up / np.linalg.norm(up)
c2w = np.concatenate([np.stack([right, up, -lookat], axis=-1), camera_position[:, None]], axis=-1)
w2c = np.zeros((4, 4))
w2c[:3, :3] = np.transpose(c2w[:3, :3], (1, 0))
w2c[:3, 3:] = -np.matmul(np.transpose(c2w[:3, :3], (1, 0)), c2w[:3, 3:])
w2c[3, 3] = 1.0
return w2c.astype(np.float32)
def get_orthographic_projection_matrix(left=-1, right=1, bottom=-1, top=1, near=0, far=2):
"""
计算正交投影矩阵。
参数:
left (float): 投影区域左侧边界。
right (float): 投影区域右侧边界。
bottom (float): 投影区域底部边界。
top (float): 投影区域顶部边界。
near (float): 投影区域近裁剪面距离。
far (float): 投影区域远裁剪面距离。
返回:
numpy.ndarray: 正交投影矩阵。
"""
ortho_matrix = np.eye(4, dtype=np.float32)
ortho_matrix[0, 0] = 2 / (right - left)
ortho_matrix[1, 1] = 2 / (top - bottom)
ortho_matrix[2, 2] = -2 / (far - near)
ortho_matrix[0, 3] = -(right + left) / (right - left)
ortho_matrix[1, 3] = -(top + bottom) / (top - bottom)
ortho_matrix[2, 3] = -(far + near) / (far - near)
return ortho_matrix
def get_perspective_projection_matrix(fovy, aspect_wh, near, far):
fovy_rad = math.radians(fovy)
return np.array(
[
[1.0 / (math.tan(fovy_rad / 2.0) * aspect_wh), 0, 0, 0],
[0, 1.0 / math.tan(fovy_rad / 2.0), 0, 0],
[0, 0, -(far + near) / (far - near), -2.0 * far * near / (far - near)],
[0, 0, -1, 0],
]
).astype(np.float32)
@@ -0,0 +1,5 @@
cl /O2 /LD /EHsc /MD %INCLUDE1% %INCLUDE2% mesh_inpaint_processor.cpp %LIB% ^
/link /OUT:%OUTFILE% ^
/LIBPATH:"C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\VC\Tools\MSVC\14.29.30133\lib\x64" ^
/LIBPATH:"C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\um\x64" ^
/LIBPATH:"C:\Program Files (x86)\Windows Kits\10\Lib\10.0.19041.0\ucrt\x64"
@@ -0,0 +1 @@
c++ -O3 -Wall -shared -std=c++11 -fPIC `python -m pybind11 --includes` mesh_inpaint_processor.cpp -o mesh_inpaint_processor`python3-config --extension-suffix`
@@ -0,0 +1,395 @@
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>
#include <pybind11/stl.h>
#include <algorithm>
#include <cmath>
#include <queue>
#include <vector>
#include <functional>
namespace py = pybind11;
using namespace std;
namespace {
// 内部数据结构,避免重复的buffer获取和指针设置
struct MeshData {
int texture_height, texture_width, texture_channel;
int vtx_num;
float* texture_ptr;
uint8_t* mask_ptr;
float* vtx_pos_ptr;
float* vtx_uv_ptr;
int* pos_idx_ptr;
int* uv_idx_ptr;
// 存储buffer以防止被销毁
py::buffer_info texture_buf, mask_buf, vtx_pos_buf, vtx_uv_buf, pos_idx_buf, uv_idx_buf;
MeshData(py::array_t<float>& texture, py::array_t<uint8_t>& mask,
py::array_t<float>& vtx_pos, py::array_t<float>& vtx_uv,
py::array_t<int>& pos_idx, py::array_t<int>& uv_idx) {
texture_buf = texture.request();
mask_buf = mask.request();
vtx_pos_buf = vtx_pos.request();
vtx_uv_buf = vtx_uv.request();
pos_idx_buf = pos_idx.request();
uv_idx_buf = uv_idx.request();
texture_height = texture_buf.shape[0];
texture_width = texture_buf.shape[1];
texture_channel = texture_buf.shape[2];
texture_ptr = static_cast<float*>(texture_buf.ptr);
mask_ptr = static_cast<uint8_t*>(mask_buf.ptr);
vtx_num = vtx_pos_buf.shape[0];
vtx_pos_ptr = static_cast<float*>(vtx_pos_buf.ptr);
vtx_uv_ptr = static_cast<float*>(vtx_uv_buf.ptr);
pos_idx_ptr = static_cast<int*>(pos_idx_buf.ptr);
uv_idx_ptr = static_cast<int*>(uv_idx_buf.ptr);
}
};
// 公共函数:计算UV坐标
pair<int, int> calculateUVCoordinates(int vtx_uv_idx, const MeshData& data) {
int uv_v = round(data.vtx_uv_ptr[vtx_uv_idx * 2] * (data.texture_width - 1));
int uv_u = round((1.0 - data.vtx_uv_ptr[vtx_uv_idx * 2 + 1]) * (data.texture_height - 1));
return make_pair(uv_u, uv_v);
}
// 公共函数:计算距离权重
float calculateDistanceWeight(const array<float, 3>& vtx_0, const array<float, 3>& vtx1) {
float dist_weight = 1.0f / max(
sqrt(
pow(vtx_0[0] - vtx1[0], 2) +
pow(vtx_0[1] - vtx1[1], 2) +
pow(vtx_0[2] - vtx1[2], 2)
), 1E-4);
return dist_weight * dist_weight;
}
// 公共函数:获取顶点位置
array<float, 3> getVertexPosition(int vtx_idx, const MeshData& data) {
return {data.vtx_pos_ptr[vtx_idx * 3],
data.vtx_pos_ptr[vtx_idx * 3 + 1],
data.vtx_pos_ptr[vtx_idx * 3 + 2]};
}
// 公共函数:构建图结构
void buildGraph(vector<vector<int>>& G, const MeshData& data) {
G.resize(data.vtx_num);
for(int i = 0; i < data.uv_idx_buf.shape[0]; ++i) {
for(int k = 0; k < 3; ++k) {
G[data.pos_idx_ptr[i * 3 + k]].push_back(data.pos_idx_ptr[i * 3 + (k + 1) % 3]);
}
}
}
// 通用初始化函数:处理两种掩码类型(float和int)
template<typename MaskType>
void initializeVertexDataGeneric(const MeshData& data, vector<MaskType>& vtx_mask,
vector<vector<float>>& vtx_color, vector<int>* uncolored_vtxs = nullptr,
MaskType mask_value = static_cast<MaskType>(1)) {
vtx_mask.assign(data.vtx_num, static_cast<MaskType>(0));
vtx_color.assign(data.vtx_num, vector<float>(data.texture_channel, 0.0f));
if(uncolored_vtxs) {
uncolored_vtxs->clear();
}
for(int i = 0; i < data.uv_idx_buf.shape[0]; ++i) {
for(int k = 0; k < 3; ++k) {
int vtx_uv_idx = data.uv_idx_ptr[i * 3 + k];
int vtx_idx = data.pos_idx_ptr[i * 3 + k];
auto uv_coords = calculateUVCoordinates(vtx_uv_idx, data);
if(data.mask_ptr[uv_coords.first * data.texture_width + uv_coords.second] > 0) {
vtx_mask[vtx_idx] = mask_value;
for(int c = 0; c < data.texture_channel; ++c) {
vtx_color[vtx_idx][c] = data.texture_ptr[(uv_coords.first * data.texture_width +
uv_coords.second) * data.texture_channel + c];
}
} else if(uncolored_vtxs) {
uncolored_vtxs->push_back(vtx_idx);
}
}
}
}
// 通用平滑算法:支持不同的掩码类型和检查函数
template<typename MaskType>
void performSmoothingAlgorithm(const MeshData& data, const vector<vector<int>>& G,
vector<MaskType>& vtx_mask, vector<vector<float>>& vtx_color,
const vector<int>& uncolored_vtxs,
function<bool(MaskType)> is_colored_func,
function<void(MaskType&)> set_colored_func) {
int smooth_count = 2;
int last_uncolored_vtx_count = 0;
while(smooth_count > 0) {
int uncolored_vtx_count = 0;
for(int vtx_idx : uncolored_vtxs) {
vector<float> sum_color(data.texture_channel, 0.0f);
float total_weight = 0.0f;
array<float, 3> vtx_0 = getVertexPosition(vtx_idx, data);
for(int connected_idx : G[vtx_idx]) {
if(is_colored_func(vtx_mask[connected_idx])) {
array<float, 3> vtx1 = getVertexPosition(connected_idx, data);
float dist_weight = calculateDistanceWeight(vtx_0, vtx1);
for(int c = 0; c < data.texture_channel; ++c) {
sum_color[c] += vtx_color[connected_idx][c] * dist_weight;
}
total_weight += dist_weight;
}
}
if(total_weight > 0.0f) {
for(int c = 0; c < data.texture_channel; ++c) {
vtx_color[vtx_idx][c] = sum_color[c] / total_weight;
}
set_colored_func(vtx_mask[vtx_idx]);
} else {
uncolored_vtx_count++;
}
}
if(last_uncolored_vtx_count == uncolored_vtx_count) {
smooth_count--;
} else {
smooth_count++;
}
last_uncolored_vtx_count = uncolored_vtx_count;
}
}
// 前向传播算法的通用实现
void performForwardPropagation(const MeshData& data, const vector<vector<int>>& G,
vector<float>& vtx_mask, vector<vector<float>>& vtx_color,
queue<int>& active_vtxs) {
while(!active_vtxs.empty()) {
queue<int> pending_active_vtxs;
while(!active_vtxs.empty()) {
int vtx_idx = active_vtxs.front();
active_vtxs.pop();
array<float, 3> vtx_0 = getVertexPosition(vtx_idx, data);
for(int connected_idx : G[vtx_idx]) {
if(vtx_mask[connected_idx] > 0) continue;
array<float, 3> vtx1 = getVertexPosition(connected_idx, data);
float dist_weight = calculateDistanceWeight(vtx_0, vtx1);
for(int c = 0; c < data.texture_channel; ++c) {
vtx_color[connected_idx][c] += vtx_color[vtx_idx][c] * dist_weight;
}
if(vtx_mask[connected_idx] == 0) {
pending_active_vtxs.push(connected_idx);
}
vtx_mask[connected_idx] -= dist_weight;
}
}
while(!pending_active_vtxs.empty()) {
int vtx_idx = pending_active_vtxs.front();
pending_active_vtxs.pop();
for(int c = 0; c < data.texture_channel; ++c) {
vtx_color[vtx_idx][c] /= -vtx_mask[vtx_idx];
}
vtx_mask[vtx_idx] = 1.0f;
active_vtxs.push(vtx_idx);
}
}
}
// 公共函数:创建输出数组
pair<py::array_t<float>, py::array_t<uint8_t>> createOutputArrays(
const MeshData& data, const vector<float>& vtx_mask,
const vector<vector<float>>& vtx_color) {
py::array_t<float> new_texture(data.texture_buf.size);
py::array_t<uint8_t> new_mask(data.mask_buf.size);
auto new_texture_buf = new_texture.request();
auto new_mask_buf = new_mask.request();
float* new_texture_ptr = static_cast<float*>(new_texture_buf.ptr);
uint8_t* new_mask_ptr = static_cast<uint8_t*>(new_mask_buf.ptr);
// Copy original texture and mask to new arrays
copy(data.texture_ptr, data.texture_ptr + data.texture_buf.size, new_texture_ptr);
copy(data.mask_ptr, data.mask_ptr + data.mask_buf.size, new_mask_ptr);
for(int face_idx = 0; face_idx < data.uv_idx_buf.shape[0]; ++face_idx) {
for(int k = 0; k < 3; ++k) {
int vtx_uv_idx = data.uv_idx_ptr[face_idx * 3 + k];
int vtx_idx = data.pos_idx_ptr[face_idx * 3 + k];
if(vtx_mask[vtx_idx] == 1.0f) {
auto uv_coords = calculateUVCoordinates(vtx_uv_idx, data);
for(int c = 0; c < data.texture_channel; ++c) {
new_texture_ptr[
(uv_coords.first * data.texture_width + uv_coords.second) *
data.texture_channel + c
] = vtx_color[vtx_idx][c];
}
new_mask_ptr[uv_coords.first * data.texture_width + uv_coords.second] = 255;
}
}
}
// Reshape the new arrays to match the original texture and mask shapes
new_texture.resize({data.texture_height, data.texture_width, 3});
new_mask.resize({data.texture_height, data.texture_width});
return make_pair(new_texture, new_mask);
}
// 创建顶点颜色输出数组的专用函数
pair<py::array_t<float>, py::array_t<uint8_t>> createVertexColorOutput(
const MeshData& data, const vector<int>& vtx_mask,
const vector<vector<float>>& vtx_color) {
py::array_t<float> py_vtx_color({data.vtx_num, data.texture_channel});
py::array_t<uint8_t> py_vtx_mask({data.vtx_num});
auto py_vtx_color_buf = py_vtx_color.request();
auto py_vtx_mask_buf = py_vtx_mask.request();
float* py_vtx_color_ptr = static_cast<float*>(py_vtx_color_buf.ptr);
uint8_t* py_vtx_mask_ptr = static_cast<uint8_t*>(py_vtx_mask_buf.ptr);
for(int i = 0; i < data.vtx_num; ++i) {
py_vtx_mask_ptr[i] = vtx_mask[i];
for(int c = 0; c < data.texture_channel; ++c) {
py_vtx_color_ptr[i * data.texture_channel + c] = vtx_color[i][c];
}
}
return make_pair(py_vtx_color, py_vtx_mask);
}
} // anonymous namespace
// 重构后的 meshVerticeInpaint_smooth 函数
pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeInpaint_smooth(
py::array_t<float> texture, py::array_t<uint8_t> mask, py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
py::array_t<int> pos_idx, py::array_t<int> uv_idx) {
MeshData data(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
vector<float> vtx_mask;
vector<vector<float>> vtx_color;
vector<int> uncolored_vtxs;
vector<vector<int>> G;
initializeVertexDataGeneric(data, vtx_mask, vtx_color, &uncolored_vtxs, 1.0f);
buildGraph(G, data);
// 使用通用平滑算法
performSmoothingAlgorithm<float>(data, G, vtx_mask, vtx_color, uncolored_vtxs,
[](float mask_val) { return mask_val > 0; }, // 检查是否着色
[](float& mask_val) { mask_val = 1.0f; } // 设置为已着色
);
return createOutputArrays(data, vtx_mask, vtx_color);
}
// 重构后的 meshVerticeInpaint_forward 函数
pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeInpaint_forward(
py::array_t<float> texture, py::array_t<uint8_t> mask, py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
py::array_t<int> pos_idx, py::array_t<int> uv_idx) {
MeshData data(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
vector<float> vtx_mask;
vector<vector<float>> vtx_color;
vector<vector<int>> G;
queue<int> active_vtxs;
// 使用通用初始化(不需要 uncolored_vtxs)
initializeVertexDataGeneric(data, vtx_mask, vtx_color, nullptr, 1.0f);
buildGraph(G, data);
// 收集活跃顶点
for(int i = 0; i < data.vtx_num; ++i) {
if(vtx_mask[i] == 1.0f) {
active_vtxs.push(i);
}
}
// 使用通用前向传播算法
performForwardPropagation(data, G, vtx_mask, vtx_color, active_vtxs);
return createOutputArrays(data, vtx_mask, vtx_color);
}
// 主接口函数
pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeInpaint(
py::array_t<float> texture, py::array_t<uint8_t> mask, py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
py::array_t<int> pos_idx, py::array_t<int> uv_idx, const string& method = "smooth") {
if(method == "smooth") {
return meshVerticeInpaint_smooth(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
} else if(method == "forward") {
return meshVerticeInpaint_forward(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
} else {
throw invalid_argument("Invalid method. Use 'smooth' or 'forward'.");
}
}
//============================
// 重构后的 meshVerticeColor_smooth 函数
pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeColor_smooth(
py::array_t<float> texture, py::array_t<uint8_t> mask, py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
py::array_t<int> pos_idx, py::array_t<int> uv_idx) {
MeshData data(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
vector<int> vtx_mask;
vector<vector<float>> vtx_color;
vector<int> uncolored_vtxs;
vector<vector<int>> G;
initializeVertexDataGeneric(data, vtx_mask, vtx_color, &uncolored_vtxs, 1);
buildGraph(G, data);
// 使用通用平滑算法
performSmoothingAlgorithm<int>(data, G, vtx_mask, vtx_color, uncolored_vtxs,
[](int mask_val) { return mask_val > 0; }, // 检查是否着色
[](int& mask_val) { mask_val = 2; } // 设置为已着色(值为2)
);
return createVertexColorOutput(data, vtx_mask, vtx_color);
}
// meshVerticeColor 主接口函数
pair<py::array_t<float>, py::array_t<uint8_t>> meshVerticeColor(
py::array_t<float> texture, py::array_t<uint8_t> mask, py::array_t<float> vtx_pos, py::array_t<float> vtx_uv,
py::array_t<int> pos_idx, py::array_t<int> uv_idx, const string& method = "smooth") {
if(method == "smooth") {
return meshVerticeColor_smooth(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx);
} else {
throw invalid_argument("Invalid method. Use 'smooth' or 'forward'.");
}
}
// Python绑定
PYBIND11_MODULE(mesh_inpaint_processor, m) {
m.def("meshVerticeInpaint", &meshVerticeInpaint, "A function to process mesh",
py::arg("texture"), py::arg("mask"), py::arg("vtx_pos"), py::arg("vtx_uv"),
py::arg("pos_idx"), py::arg("uv_idx"), py::arg("method") = "smooth");
m.def("meshVerticeColor", &meshVerticeColor, "A function to process mesh",
py::arg("texture"), py::arg("mask"), py::arg("vtx_pos"), py::arg("vtx_uv"),
py::arg("pos_idx"), py::arg("uv_idx"), py::arg("method") = "smooth");
}
@@ -0,0 +1,84 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import numpy as np
def meshVerticeInpaint_smooth(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx):
texture_height, texture_width, texture_channel = texture.shape
vtx_num = vtx_pos.shape[0]
vtx_mask = np.zeros(vtx_num, dtype=np.float32)
vtx_color = [np.zeros(texture_channel, dtype=np.float32) for _ in range(vtx_num)]
uncolored_vtxs = []
G = [[] for _ in range(vtx_num)]
for i in range(uv_idx.shape[0]):
for k in range(3):
vtx_uv_idx = uv_idx[i, k]
vtx_idx = pos_idx[i, k]
uv_v = int(round(vtx_uv[vtx_uv_idx, 0] * (texture_width - 1)))
uv_u = int(round((1.0 - vtx_uv[vtx_uv_idx, 1]) * (texture_height - 1)))
if mask[uv_u, uv_v] > 0:
vtx_mask[vtx_idx] = 1.0
vtx_color[vtx_idx] = texture[uv_u, uv_v]
else:
uncolored_vtxs.append(vtx_idx)
G[pos_idx[i, k]].append(pos_idx[i, (k + 1) % 3])
smooth_count = 2
last_uncolored_vtx_count = 0
while smooth_count > 0:
uncolored_vtx_count = 0
for vtx_idx in uncolored_vtxs:
sum_color = np.zeros(texture_channel, dtype=np.float32)
total_weight = 0.0
vtx_0 = vtx_pos[vtx_idx]
for connected_idx in G[vtx_idx]:
if vtx_mask[connected_idx] > 0:
vtx1 = vtx_pos[connected_idx]
dist = np.sqrt(np.sum((vtx_0 - vtx1) ** 2))
dist_weight = 1.0 / max(dist, 1e-4)
dist_weight *= dist_weight
sum_color += vtx_color[connected_idx] * dist_weight
total_weight += dist_weight
if total_weight > 0:
vtx_color[vtx_idx] = sum_color / total_weight
vtx_mask[vtx_idx] = 1.0
else:
uncolored_vtx_count += 1
if last_uncolored_vtx_count == uncolored_vtx_count:
smooth_count -= 1
else:
smooth_count += 1
last_uncolored_vtx_count = uncolored_vtx_count
new_texture = texture.copy()
new_mask = mask.copy()
for face_idx in range(uv_idx.shape[0]):
for k in range(3):
vtx_uv_idx = uv_idx[face_idx, k]
vtx_idx = pos_idx[face_idx, k]
if vtx_mask[vtx_idx] == 1.0:
uv_v = int(round(vtx_uv[vtx_uv_idx, 0] * (texture_width - 1)))
uv_u = int(round((1.0 - vtx_uv[vtx_uv_idx, 1]) * (texture_height - 1)))
new_texture[uv_u, uv_v] = vtx_color[vtx_idx]
new_mask[uv_u, uv_v] = 255
return new_texture, new_mask
def meshVerticeInpaint(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx, method="smooth"):
if method == "smooth":
return meshVerticeInpaint_smooth(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx)
else:
raise ValueError("Invalid method. Use 'smooth' or 'forward'.")
@@ -0,0 +1,480 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
import cv2
# import bpy # Blender Python API - commented out
import math
import numpy as np
from io import StringIO
from typing import Optional, Tuple, Dict, Any
import pymeshlab as pml
def _safe_extract_attribute(obj: Any, attr_path: str, default: Any = None) -> Any:
"""Extract nested attribute safely from object."""
try:
for attr in attr_path.split("."):
obj = getattr(obj, attr)
return obj
except AttributeError:
return default
def _convert_to_numpy(data: Any, dtype: np.dtype) -> Optional[np.ndarray]:
"""Convert data to numpy array with specified dtype, handling None values."""
if data is None:
return None
return np.asarray(data, dtype=dtype)
def load_mesh(mesh):
"""Load mesh data including vertices, faces, UV coordinates and texture."""
# Extract vertex positions and face indices
vtx_pos = _safe_extract_attribute(mesh, "vertices")
pos_idx = _safe_extract_attribute(mesh, "faces")
# Extract UV coordinates (reusing face indices for UV indices)
vtx_uv = _safe_extract_attribute(mesh, "visual.uv")
uv_idx = pos_idx # Reuse face indices for UV mapping
# Convert to numpy arrays with appropriate dtypes
vtx_pos = _convert_to_numpy(vtx_pos, np.float32)
pos_idx = _convert_to_numpy(pos_idx, np.int32)
vtx_uv = _convert_to_numpy(vtx_uv, np.float32)
uv_idx = _convert_to_numpy(uv_idx, np.int32)
texture_data = None
return vtx_pos, pos_idx, vtx_uv, uv_idx, texture_data
def _get_base_path_and_name(mesh_path: str) -> Tuple[str, str]:
"""Get base path without extension and mesh name."""
base_path = os.path.splitext(mesh_path)[0]
name = os.path.basename(base_path)
return base_path, name
def _save_texture_map(
texture: np.ndarray,
base_path: str,
suffix: str = "",
image_format: str = ".jpg",
color_convert: Optional[int] = None,
) -> str:
"""Save texture map with optional color conversion."""
path = f"{base_path}{suffix}{image_format}"
processed_texture = (texture * 255).astype(np.uint8)
if color_convert is not None:
processed_texture = cv2.cvtColor(processed_texture, color_convert)
cv2.imwrite(path, processed_texture)
else:
cv2.imwrite(path, processed_texture[..., ::-1]) # RGB to BGR
return os.path.basename(path)
def _write_mtl_properties(f, properties: Dict[str, Any]):
"""Write material properties to MTL file."""
for key, value in properties.items():
if isinstance(value, (list, tuple)):
f.write(f"{key} {' '.join(map(str, value))}\n")
else:
f.write(f"{key} {value}\n")
def _create_obj_content(
vtx_pos: np.ndarray, vtx_uv: np.ndarray, pos_idx: np.ndarray, uv_idx: np.ndarray, name: str
) -> str:
"""Create OBJ file content."""
buffer = StringIO()
# Write header and vertices
buffer.write(f"mtllib {name}.mtl\no {name}\n")
np.savetxt(buffer, vtx_pos, fmt="v %.6f %.6f %.6f")
np.savetxt(buffer, vtx_uv, fmt="vt %.6f %.6f")
buffer.write("s 0\nusemtl Material\n")
# Write faces
pos_idx_plus1 = pos_idx + 1
uv_idx_plus1 = uv_idx + 1
face_format = np.frompyfunc(lambda *x: f"{int(x[0])}/{int(x[1])}", 2, 1)
faces = face_format(pos_idx_plus1, uv_idx_plus1)
face_strings = [f"f {' '.join(face)}" for face in faces]
buffer.write("\n".join(face_strings) + "\n")
return buffer.getvalue()
def save_obj_mesh(mesh_path, vtx_pos, pos_idx, vtx_uv, uv_idx, texture, metallic=None, roughness=None, normal=None):
"""Save mesh as OBJ file with textures and material."""
# Convert inputs to numpy arrays
vtx_pos = _convert_to_numpy(vtx_pos, np.float32)
vtx_uv = _convert_to_numpy(vtx_uv, np.float32)
pos_idx = _convert_to_numpy(pos_idx, np.int32)
uv_idx = _convert_to_numpy(uv_idx, np.int32)
base_path, name = _get_base_path_and_name(mesh_path)
# Create and save OBJ content
obj_content = _create_obj_content(vtx_pos, vtx_uv, pos_idx, uv_idx, name)
with open(mesh_path, "w") as obj_file:
obj_file.write(obj_content)
# Save texture maps
texture_maps = {}
texture_maps["diffuse"] = _save_texture_map(texture, base_path)
if metallic is not None:
texture_maps["metallic"] = _save_texture_map(metallic, base_path, "_metallic", color_convert=cv2.COLOR_RGB2GRAY)
if roughness is not None:
texture_maps["roughness"] = _save_texture_map(
roughness, base_path, "_roughness", color_convert=cv2.COLOR_RGB2GRAY
)
if normal is not None:
texture_maps["normal"] = _save_texture_map(normal, base_path, "_normal")
# Create MTL file
_create_mtl_file(base_path, texture_maps, metallic is not None)
def _create_mtl_file(base_path: str, texture_maps: Dict[str, str], is_pbr: bool):
"""Create MTL material file."""
mtl_path = f"{base_path}.mtl"
with open(mtl_path, "w") as f:
f.write("newmtl Material\n")
if is_pbr:
# PBR material properties
properties = {
"Kd": [0.800, 0.800, 0.800],
"Ke": [0.000, 0.000, 0.000], # 鐜鍏夐伄钄�
"Ni": 1.500, # 鎶樺皠绯绘暟
"d": 1.0, # 閫忔槑搴�
"illum": 2, # 鍏夌収妯″瀷
"map_Kd": texture_maps["diffuse"],
}
_write_mtl_properties(f, properties)
# Additional PBR maps
map_configs = [("metallic", "map_Pm"), ("roughness", "map_Pr"), ("normal", "map_Bump -bm 1.0")]
for texture_key, mtl_key in map_configs:
if texture_key in texture_maps:
f.write(f"{mtl_key} {texture_maps[texture_key]}\n")
else:
# Standard material properties
properties = {
"Ns": 250.000000,
"Ka": [0.200, 0.200, 0.200],
"Kd": [0.800, 0.800, 0.800],
"Ks": [0.500, 0.500, 0.500],
"Ke": [0.000, 0.000, 0.000],
"Ni": 1.500,
"d": 1.0,
"illum": 3,
"map_Kd": texture_maps["diffuse"],
}
_write_mtl_properties(f, properties)
def save_mesh(mesh_path, vtx_pos, pos_idx, vtx_uv, uv_idx, texture, metallic=None, roughness=None, normal=None):
"""Save mesh using OBJ format."""
save_obj_mesh(
mesh_path, vtx_pos, pos_idx, vtx_uv, uv_idx, texture, metallic=metallic, roughness=roughness, normal=normal
)
def convert_mesh(
input_path: str,
output_path: str,
shade_type: str = "SMOOTH",
auto_smooth_angle: float = 60,
merge_vertices: bool = False,
fix_mesh: bool = True
) -> bool:
"""
Universal mesh converter that handles multiple formats.
Args:
input_path: Path to input mesh file
output_path: Path to output mesh file
shade_type: Type of shading ("SMOOTH", "FLAT", "AUTO_SMOOTH")
auto_smooth_angle: Angle for auto smooth in degrees
merge_vertices: Whether to merge duplicate vertices
fix_mesh: Whether to fix mesh issues automatically
Returns:
bool: True if conversion successful, False otherwise
"""
try:
import trimesh
print(f"Converting {input_path} -> {output_path}")
# Check if input file exists
if not os.path.exists(input_path):
print(f"Error: Input file does not exist: {input_path}")
return False
# Get file extensions
input_ext = os.path.splitext(input_path)[1].lower()
output_ext = os.path.splitext(output_path)[1].lower()
# If same format and same file, nothing to do
if input_path == output_path:
print("Input and output are the same file, no conversion needed")
return True
# If same format but different paths, copy file
if input_ext == output_ext:
import shutil
shutil.copy2(input_path, output_path)
print(f"Copied {input_ext.upper()} file from {input_path} to {output_path}")
return True
# Load mesh based on input format
print(f"Loading {input_ext.upper()} file...")
# Special handling for different formats
if input_ext in ['.glb', '.gltf']:
# Load GLTF/GLB
scene = trimesh.load(input_path)
if hasattr(scene, 'geometry') and scene.geometry:
mesh = list(scene.geometry.values())[0]
else:
mesh = scene
elif input_ext == '.obj':
# Load OBJ with materials if possible
try:
scene = trimesh.load(input_path, force='scene')
if hasattr(scene, 'geometry') and scene.geometry:
mesh = list(scene.geometry.values())[0]
else:
mesh = trimesh.load_mesh(input_path)
except:
mesh = trimesh.load_mesh(input_path)
else:
# Load other formats (PLY, STL, etc.)
mesh = trimesh.load_mesh(input_path)
print(f"Mesh loaded: {len(mesh.vertices)} vertices, {len(mesh.faces)} faces")
# Fix mesh if requested
if fix_mesh:
print("Fixing mesh issues...")
# Check if mesh has is_valid attribute, if not - check basic properties
try:
is_mesh_valid = hasattr(mesh, 'is_valid') and mesh.is_valid
except:
# Fallback: check basic mesh properties
is_mesh_valid = len(mesh.vertices) > 0 and len(mesh.faces) > 0
if not is_mesh_valid:
print("Mesh needs fixing...")
try:
mesh.fix_normals()
mesh.remove_degenerate_faces()
mesh.remove_duplicate_faces()
except Exception as e:
print(f"Warning: Some mesh fixes failed: {e}")
else:
print("Mesh appears to be valid")
# Merge vertices if requested
if merge_vertices:
print("Merging duplicate vertices...")
try:
mesh.merge_vertices()
except Exception as e:
print(f"Warning: Vertex merging failed: {e}")
# Apply shading
if shade_type == "SMOOTH":
print("Applying smooth shading...")
try:
mesh.fix_normals()
except Exception as e:
print(f"Warning: Normal fixing failed: {e}")
elif shade_type == "FLAT":
print("Using flat shading...")
pass
elif shade_type == "AUTO_SMOOTH":
print("Applying auto smooth...")
try:
mesh.fix_normals()
except Exception as e:
print(f"Warning: Auto smooth failed: {e}")
# Create output directory if needed
os.makedirs(os.path.dirname(output_path), exist_ok=True)
# Export mesh
print(f"Exporting to {output_ext.upper()}...")
# Try different export methods
export_success = False
for method_num, export_func in enumerate([
lambda: mesh.export(output_path, file_type=output_ext[1:]), # Remove dot from extension
lambda: mesh.export(output_path), # Auto-detect format
lambda: trimesh.Scene([mesh]).export(output_path) # Export as scene
], 1):
try:
export_func()
export_success = True
print(f"Export successful using method {method_num}")
break
except Exception as e:
print(f"Export method {method_num} failed: {e}")
if method_num == 3: # Last method
raise e
if not export_success:
print("All export methods failed")
return False
print(f"Conversion completed: {output_path}")
return True
except Exception as e:
print(f"Error converting {input_path} to {output_path}: {str(e)}")
import traceback
traceback.print_exc()
return False
def get_supported_formats():
"""Get list of supported mesh formats."""
return {
'input': ['.obj', '.glb', '.gltf', '.ply', '.stl', '.off', '.3mf'],
'output': ['.obj', '.glb', '.gltf', '.ply', '.stl', '.off']
}
def is_mesh_file(file_path: str) -> bool:
"""Check if file is a supported mesh format."""
ext = os.path.splitext(file_path)[1].lower()
supported = get_supported_formats()
return ext in supported['input']
# def _setup_blender_scene():
# """Setup Blender scene for conversion."""
# if "convert" not in bpy.data.scenes:
# bpy.data.scenes.new("convert")
# bpy.context.window.scene = bpy.data.scenes["convert"]
# def _clear_scene_objects():
# """Clear all objects from current Blender scene."""
# for obj in bpy.context.scene.objects:
# obj.select_set(True)
# bpy.data.objects.remove(obj, do_unlink=True)
# def _select_mesh_objects():
# """Select all mesh objects in scene."""
# bpy.ops.object.select_all(action="DESELECT")
# for obj in bpy.context.scene.objects:
# if obj.type == "MESH":
# obj.select_set(True)
# def _merge_vertices_if_needed(merge_vertices: bool):
# """Merge duplicate vertices if requested."""
# if not merge_vertices:
# return
# for obj in bpy.context.selected_objects:
# if obj.type == "MESH":
# bpy.context.view_layer.objects.active = obj
# bpy.ops.object.mode_set(mode="EDIT")
# bpy.ops.mesh.select_all(action="SELECT")
# bpy.ops.mesh.remove_doubles()
# bpy.ops.object.mode_set(mode="OBJECT")
# def _apply_shading(shade_type: str, auto_smooth_angle: float):
# """Apply shading to selected objects."""
# shading_ops = {
# "SMOOTH": lambda: bpy.ops.object.shade_smooth(),
# "FLAT": lambda: bpy.ops.object.shade_flat(),
# "AUTO_SMOOTH": lambda: _apply_auto_smooth(auto_smooth_angle),
# }
# if shade_type in shading_ops:
# shading_ops[shade_type]()
# def _apply_auto_smooth(auto_smooth_angle: float):
# """Apply auto smooth based on Blender version."""
# angle_rad = math.radians(auto_smooth_angle)
# if bpy.app.version < (4, 1, 0):
# bpy.ops.object.shade_smooth(use_auto_smooth=True, auto_smooth_angle=angle_rad)
# elif bpy.app.version < (4, 2, 0):
# bpy.ops.object.shade_smooth_by_angle(angle=angle_rad)
# else:
# bpy.ops.object.shade_auto_smooth(angle=angle_rad)
# def convert_obj_to_glb(
# obj_path: str,
# glb_path: str,
# shade_type: str = "SMOOTH",
# auto_smooth_angle: float = 60,
# merge_vertices: bool = False,
# ) -> bool:
# """Convert OBJ file to GLB format using Blender."""
# try:
# _setup_blender_scene()
# _clear_scene_objects()
# # Import OBJ file
# bpy.ops.wm.obj_import(filepath=obj_path)
# _select_mesh_objects()
# # Process meshes
# _merge_vertices_if_needed(merge_vertices)
# _apply_shading(shade_type, auto_smooth_angle)
# # Export to GLB
# bpy.ops.export_scene.gltf(filepath=glb_path, use_active_scene=True)
# return True
# except Exception:
# return False
# Backward compatibility alias
def convert_obj_to_glb(
obj_path: str,
glb_path: str,
shade_type: str = "SMOOTH",
auto_smooth_angle: float = 60,
merge_vertices: bool = False,
) -> bool:
"""
Convert OBJ file to GLB format using trimesh.
This is a backward compatibility wrapper for convert_mesh function.
"""
return convert_mesh(
input_path=obj_path,
output_path=glb_path,
shade_type=shade_type,
auto_smooth_angle=auto_smooth_angle,
merge_vertices=merge_vertices,
fix_mesh=True
)
@@ -0,0 +1,81 @@
TENCENT HUNYUAN 3D 2.1 COMMUNITY LICENSE AGREEMENT
Tencent Hunyuan 3D 2.1 Release Date: June 13, 2025
THIS LICENSE AGREEMENT DOES NOT APPLY IN THE EUROPEAN UNION, UNITED KINGDOM AND SOUTH KOREA AND IS EXPRESSLY LIMITED TO THE TERRITORY, AS DEFINED BELOW.
By clicking to agree or by using, reproducing, modifying, distributing, performing or displaying any portion or element of the Tencent Hunyuan 3D 2.1 Works, including via any Hosted Service, You will be deemed to have recognized and accepted the content of this Agreement, which is effective immediately.
1. DEFINITIONS.
a. “Acceptable Use Policy” shall mean the policy made available by Tencent as set forth in the Exhibit A.
b. “Agreement” shall mean the terms and conditions for use, reproduction, distribution, modification, performance and displaying of Tencent Hunyuan 3D 2.1 Works or any portion or element thereof set forth herein.
c. “Documentation” shall mean the specifications, manuals and documentation for Tencent Hunyuan 3D 2.1 made publicly available by Tencent.
d. “Hosted Service” shall mean a hosted service offered via an application programming interface (API), web access, or any other electronic or remote means.
e. “Licensee,” “You” or “Your” shall mean a natural person or legal entity exercising the rights granted by this Agreement and/or using the Tencent Hunyuan 3D 2.1 Works for any purpose and in any field of use.
f. “Materials” shall mean, collectively, Tencent’s proprietary Tencent Hunyuan 3D 2.1 and Documentation (and any portion thereof) as made available by Tencent under this Agreement.
g. “Model Derivatives” shall mean all: (i) modifications to Tencent Hunyuan 3D 2.1 or any Model Derivative of Tencent Hunyuan 3D 2.1; (ii) works based on Tencent Hunyuan 3D 2.1 or any Model Derivative of Tencent Hunyuan 3D 2.1; or (iii) any other machine learning model which is created by transfer of patterns of the weights, parameters, operations, or Output of Tencent Hunyuan 3D 2.1 or any Model Derivative of Tencent Hunyuan 3D 2.1, to that model in order to cause that model to perform similarly to Tencent Hunyuan 3D 2.1 or a Model Derivative of Tencent Hunyuan 3D 2.1, including distillation methods, methods that use intermediate data representations, or methods based on the generation of synthetic data Outputs by Tencent Hunyuan 3D 2.1 or a Model Derivative of Tencent Hunyuan 3D 2.1 for training that model. For clarity, Outputs by themselves are not deemed Model Derivatives.
h. “Output” shall mean the information and/or content output of Tencent Hunyuan 3D 2.1 or a Model Derivative that results from operating or otherwise using Tencent Hunyuan 3D 2.1 or a Model Derivative, including via a Hosted Service.
i. “Tencent,” “We” or “Us” shall mean THL Q Limited.
j. “Tencent Hunyuan 3D 2.1” shall mean the 3D generation models and their software and algorithms, including trained model weights, parameters (including optimizer states), machine-learning model code, inference-enabling code, training-enabling code, fine-tuning enabling code and other elements of the foregoing made publicly available by Us at [ https://github.com/Tencent-Hunyuan/Hunyuan3D-2.1].
k. “Tencent Hunyuan 3D 2.1 Works” shall mean: (i) the Materials; (ii) Model Derivatives; and (iii) all derivative works thereof.
l. “Territory” shall mean the worldwide territory, excluding the territory of the European Union, United Kingdom and South Korea.
m. “Third Party” or “Third Parties” shall mean individuals or legal entities that are not under common control with Us or You.
n. “including” shall mean including but not limited to.
2. GRANT OF RIGHTS.
We grant You, for the Territory only, a non-exclusive, non-transferable and royalty-free limited license under Tencent’s intellectual property or other rights owned by Us embodied in or utilized by the Materials to use, reproduce, distribute, create derivative works of (including Model Derivatives), and make modifications to the Materials, only in accordance with the terms of this Agreement and the Acceptable Use Policy, and You must not violate (or encourage or permit anyone else to violate) any term of this Agreement or the Acceptable Use Policy.
3. DISTRIBUTION.
You may, subject to Your compliance with this Agreement, distribute or make available to Third Parties the Tencent Hunyuan 3D 2.1 Works, exclusively in the Territory, provided that You meet all of the following conditions:
a. You must provide all such Third Party recipients of the Tencent Hunyuan 3D 2.1 Works or products or services using them a copy of this Agreement;
b. You must cause any modified files to carry prominent notices stating that You changed the files;
c. You are encouraged to: (i) publish at least one technology introduction blogpost or one public statement expressing Your experience of using the Tencent Hunyuan 3D 2.1 Works; and (ii) mark the products or services developed by using the Tencent Hunyuan 3D 2.1 Works to indicate that the product/service is “Powered by Tencent Hunyuan”; and
d. All distributions to Third Parties (other than through a Hosted Service) must be accompanied by a “Notice” text file that contains the following notice: “Tencent Hunyuan 3D 2.1 is licensed under the Tencent Hunyuan 3D 2.1 Community License Agreement, Copyright © 2025 Tencent. All Rights Reserved. The trademark rights of “Tencent Hunyuan” are owned by Tencent or its affiliate.”
You may add Your own copyright statement to Your modifications and, except as set forth in this Section and in Section 5, may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Model Derivatives as a whole, provided Your use, reproduction, modification, distribution, performance and display of the work otherwise complies with the terms and conditions of this Agreement (including as regards the Territory). If You receive Tencent Hunyuan 3D 2.1 Works from a Licensee as part of an integrated end user product, then this Section 3 of this Agreement will not apply to You.
4. ADDITIONAL COMMERCIAL TERMS.
If, on the Tencent Hunyuan 3D 2.1 version release date, the monthly active users of all products or services made available by or for Licensee is greater than 1 million monthly active users in the preceding calendar month, You must request a license from Tencent, which Tencent may grant to You in its sole discretion, and You are not authorized to exercise any of the rights under this Agreement unless or until Tencent otherwise expressly grants You such rights.
Subject to Tencent's written approval, you may request a license for the use of Tencent Hunyuan 3D 2.1 by submitting the following information to hunyuan3d@tencent.com:
a. Your company’s name and associated business sector that plans to use Tencent Hunyuan 3D 2.1.
b. Your intended use case and the purpose of using Tencent Hunyuan 3D 2.1.
c. Your plans to modify Tencent Hunyuan 3D 2.1 or create Model Derivatives.
5. RULES OF USE.
a. Your use of the Tencent Hunyuan 3D 2.1 Works must comply with applicable laws and regulations (including trade compliance laws and regulations) and adhere to the Acceptable Use Policy for the Tencent Hunyuan 3D 2.1 Works, which is hereby incorporated by reference into this Agreement. You must include the use restrictions referenced in these Sections 5(a) and 5(b) as an enforceable provision in any agreement (e.g., license agreement, terms of use, etc.) governing the use and/or distribution of Tencent Hunyuan 3D 2.1 Works and You must provide notice to subsequent users to whom You distribute that Tencent Hunyuan 3D 2.1 Works are subject to the use restrictions in these Sections 5(a) and 5(b).
b. You must not use the Tencent Hunyuan 3D 2.1 Works or any Output or results of the Tencent Hunyuan 3D 2.1 Works to improve any other AI model (other than Tencent Hunyuan 3D 2.1 or Model Derivatives thereof).
c. You must not use, reproduce, modify, distribute, or display the Tencent Hunyuan 3D 2.1 Works, Output or results of the Tencent Hunyuan 3D 2.1 Works outside the Territory. Any such use outside the Territory is unlicensed and unauthorized under this Agreement.
6. INTELLECTUAL PROPERTY.
a. Subject to Tencent’s ownership of Tencent Hunyuan 3D 2.1 Works made by or for Tencent and intellectual property rights therein, conditioned upon Your compliance with the terms and conditions of this Agreement, as between You and Tencent, You will be the owner of any derivative works and modifications of the Materials and any Model Derivatives that are made by or for You.
b. No trademark licenses are granted under this Agreement, and in connection with the Tencent Hunyuan 3D 2.1 Works, Licensee may not use any name or mark owned by or associated with Tencent or any of its affiliates, except as required for reasonable and customary use in describing and distributing the Tencent Hunyuan 3D 2.1 Works. Tencent hereby grants You a license to use “Tencent Hunyuan” (the “Mark”) in the Territory solely as required to comply with the provisions of Section 3(c), provided that You comply with any applicable laws related to trademark protection. All goodwill arising out of Your use of the Mark will inure to the benefit of Tencent.
c. If You commence a lawsuit or other proceedings (including a cross-claim or counterclaim in a lawsuit) against Us or any person or entity alleging that the Materials or any Output, or any portion of any of the foregoing, infringe any intellectual property or other right owned or licensable by You, then all licenses granted to You under this Agreement shall terminate as of the date such lawsuit or other proceeding is filed. You will defend, indemnify and hold harmless Us from and against any claim by any Third Party arising out of or related to Your or the Third Party’s use or distribution of the Tencent Hunyuan 3D 2.1 Works.
d. Tencent claims no rights in Outputs You generate. You and Your users are solely responsible for Outputs and their subsequent uses.
7. DISCLAIMERS OF WARRANTY AND LIMITATIONS OF LIABILITY.
a. We are not obligated to support, update, provide training for, or develop any further version of the Tencent Hunyuan 3D 2.1 Works or to grant any license thereto.
b. UNLESS AND ONLY TO THE EXTENT REQUIRED BY APPLICABLE LAW, THE TENCENT HUNYUAN 3D 2.1 WORKS AND ANY OUTPUT AND RESULTS THEREFROM ARE PROVIDED “AS IS” WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES OF ANY KIND INCLUDING ANY WARRANTIES OF TITLE, MERCHANTABILITY, NONINFRINGEMENT, COURSE OF DEALING, USAGE OF TRADE, OR FITNESS FOR A PARTICULAR PURPOSE. YOU ARE SOLELY RESPONSIBLE FOR DETERMINING THE APPROPRIATENESS OF USING, REPRODUCING, MODIFYING, PERFORMING, DISPLAYING OR DISTRIBUTING ANY OF THE TENCENT HUNYUAN 3D 2.1 WORKS OR OUTPUTS AND ASSUME ANY AND ALL RISKS ASSOCIATED WITH YOUR OR A THIRD PARTY’S USE OR DISTRIBUTION OF ANY OF THE TENCENT HUNYUAN 3D 2.1 WORKS OR OUTPUTS AND YOUR EXERCISE OF RIGHTS AND PERMISSIONS UNDER THIS AGREEMENT.
c. TO THE FULLEST EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL TENCENT OR ITS AFFILIATES BE LIABLE UNDER ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, TORT, NEGLIGENCE, PRODUCTS LIABILITY, OR OTHERWISE, FOR ANY DAMAGES, INCLUDING ANY DIRECT, INDIRECT, SPECIAL, INCIDENTAL, EXEMPLARY, CONSEQUENTIAL OR PUNITIVE DAMAGES, OR LOST PROFITS OF ANY KIND ARISING FROM THIS AGREEMENT OR RELATED TO ANY OF THE TENCENT HUNYUAN 3D 2.1 WORKS OR OUTPUTS, EVEN IF TENCENT OR ITS AFFILIATES HAVE BEEN ADVISED OF THE POSSIBILITY OF ANY OF THE FOREGOING.
8. SURVIVAL AND TERMINATION.
a. The term of this Agreement shall commence upon Your acceptance of this Agreement or access to the Materials and will continue in full force and effect until terminated in accordance with the terms and conditions herein.
b. We may terminate this Agreement if You breach any of the terms or conditions of this Agreement. Upon termination of this Agreement, You must promptly delete and cease use of the Tencent Hunyuan 3D 2.1 Works. Sections 6(a), 6(c), 7 and 9 shall survive the termination of this Agreement.
9. GOVERNING LAW AND JURISDICTION.
a. This Agreement and any dispute arising out of or relating to it will be governed by the laws of the Hong Kong Special Administrative Region of the People’s Republic of China, without regard to conflict of law principles, and the UN Convention on Contracts for the International Sale of Goods does not apply to this Agreement.
b. Exclusive jurisdiction and venue for any dispute arising out of or relating to this Agreement will be a court of competent jurisdiction in the Hong Kong Special Administrative Region of the People’s Republic of China, and Tencent and Licensee consent to the exclusive jurisdiction of such court with respect to any such dispute.
 
EXHIBIT A
ACCEPTABLE USE POLICY
Tencent reserves the right to update this Acceptable Use Policy from time to time.
Last modified: November 5, 2024
Tencent endeavors to promote safe and fair use of its tools and features, including Tencent Hunyuan 3D 2.1. You agree not to use Tencent Hunyuan 3D 2.1 or Model Derivatives:
1. Outside the Territory;
2. In any way that violates any applicable national, federal, state, local, international or any other law or regulation;
3. To harm Yourself or others;
4. To repurpose or distribute output from Tencent Hunyuan 3D 2.1 or any Model Derivatives to harm Yourself or others;
5. To override or circumvent the safety guardrails and safeguards We have put in place;
6. For the purpose of exploiting, harming or attempting to exploit or harm minors in any way;
7. To generate or disseminate verifiably false information and/or content with the purpose of harming others or influencing elections;
8. To generate or facilitate false online engagement, including fake reviews and other means of fake online engagement;
9. To intentionally defame, disparage or otherwise harass others;
10. To generate and/or disseminate malware (including ransomware) or any other content to be used for the purpose of harming electronic systems;
11. To generate or disseminate personal identifiable information with the purpose of harming others;
12. To generate or disseminate information (including images, code, posts, articles), and place the information in any public context (including –through the use of bot generated tweets), without expressly and conspicuously identifying that the information and/or content is machine generated;
13. To impersonate another individual without consent, authorization, or legal right;
14. To make high-stakes automated decisions in domains that affect an individual’s safety, rights or wellbeing (e.g., law enforcement, migration, medicine/health, management of critical infrastructure, safety components of products, essential services, credit, employment, housing, education, social scoring, or insurance);
15. In a manner that violates or disrespects the social ethics and moral standards of other countries or regions;
16. To perform, facilitate, threaten, incite, plan, promote or encourage violent extremism or terrorism;
17. For any use intended to discriminate against or harm individuals or groups based on protected characteristics or categories, online or offline social behavior or known or predicted personal or personality characteristics;
18. To intentionally exploit any of the vulnerabilities of a specific group of persons based on their age, social, physical or mental characteristics, in order to materially distort the behavior of a person pertaining to that group in a manner that causes or is likely to cause that person or another person physical or psychological harm;
19. For military purposes;
20. To engage in the unauthorized or unlicensed practice of any profession including, but not limited to, financial, legal, medical/health, or other professional practices.
@@ -0,0 +1,96 @@
# Hunyuan3D-Paint 2.1
Hunyuan3D-Paint 2.1 is a high quality PBR texture generation model for 3D meshes, powered by [RomanTex](https://github.com/oakshy/RomanTex) and [MaterialMVP](https://github.com/ZebinHe/MaterialMVP/).
## Quick Inference
You need to manually download the RealESRGAN weight to the `ckpt` folder using the following command:
```bash
wget https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth -P ckpt
```
Given a 3D mesh `mesh.glb` and a reference image `image.png`, you can run inference using the following code. The result will be saved as `textured_mesh.glb`.
```bash
python3 demo.py
```
**Optional arguments in `demo.py`:**
- `max_num_view` : Maximum number of views, adaptively selected by the model (integer between 6 to 9)
- `resolution` : Resolution for generated PBR textures (512 or 768)
**Memory Recommendation:** For `max_num_view=6` and `resolution=512`, we recommend using a GPU with at least **21GB VRAM**.
## Training
### Data Prepare
We provide a piece of data in `train_examples` for the overfitting training test. The data structure should be organized as follows:
```
train_examples/
├── examples.json
└── 001/
├── render_tex/ # Rendered generated PBR images
│ ├── 000.png # Rendered views (RGB images)
│ ├── 000_albedo.png # Albedo maps for each view
│ ├── 000_mr.png # Metallic-Roughness maps for each view, R and G channels
│ ├── 000_normal.png # Normal maps
│ ├── 000_normal.png # Normal maps
│ ├── 000_pos.png # Position maps
│ ├── 000_pos.png # Position maps
│ ├── 001.png # Additional views...
│ ├── 001_albedo.png
│ ├── 001_mr.png
│ ├── 001_normal.png
│ ├── 001_pos.png
│ └── ... # More views (002, 003, 004, 005, ...)
└── render_cond/ # Rendered reference images (at least two light conditions should be rendered to facilitate consistency loss)
├── 000_light_AL.png # Light condition 1 (Area Light)
├── 000_light_ENVMAP.png # Light condition 2 (Environment map)
├── 000_light_PL.png # Light condition 3 (Point lighting)
├── 001_light_AL.png
├── 001_light_ENVMAP.png
├── 001_light_PL.png
└── ... # More lighting conditions (002-005, ...)
```
Each training example contains:
- **render_tex/**: Multi-view renderings with PBR material properties
- Main RGB images (`XXX.png`)
- Albedo maps (`XXX_albedo.png`)
- Metallic-Roughness maps (`XXX_mr.png`)
- Normal maps (`XXX_normal.png/jpg`)
- Position maps (`XXX_pos.png/jpg`)
- Camera transforms (`transforms.json`)
- **render_cond/**: Lighting condition maps for each view
- Ambient lighting (`XXX_light_AL.png`)
- Environment map lighting (`XXX_light_ENVMAP.png`)
- Point lighting (`XXX_light_PL.png`)
### Launch Training
```bash
python3 train.py --base 'cfgs/hunyuan-paint-pbr.yaml' --name overfit --logdir logs/
```
## BibTeX
If you found Hunyuan3D-Paint 2.1 helpful, please cite our papers:
```bibtex
@article{feng2025romantex,
title={RomanTex: Decoupling 3D-aware Rotary Positional Embedded Multi-Attention Network for Texture Synthesis},
author={Feng, Yifei and Yang, Mingxin and Yang, Shuhui and Zhang, Sheng and Yu, Jiaao and Zhao, Zibo and Liu, Yuhong and Jiang, Jie and Guo, Chunchao},
journal={arXiv preprint arXiv:2503.19011},
year={2025}
}
@article{he2025materialmvp,
title={MaterialMVP: Illumination-Invariant Material Generation via Multi-view PBR Diffusion},
author={He, Zebin and Yang, Mingxin and Yang, Shuhui and Tang, Yixuan and Wang, Tao and Zhang, Kaihao and Chen, Guanying and Liu, Yuhong and Jiang, Jie and Guo, Chunchao and Luo, Wenhan},
journal={arXiv preprint arXiv:2503.10289},
year={2025}
}
```
@@ -0,0 +1,4 @@
import os
import sys
ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT_PATH, 'Gen_3D_Modules/Hunyuan3D_2_1/hy3dpaint'))
@@ -0,0 +1,52 @@
model:
base_learning_rate: 5.0e-05
target: hunyuanpaintpbr.model.HunyuanPaint
params:
num_view: 6
view_size: 512
drop_cond_prob: 0.1
noise_in_channels: 12
stable_diffusion_config:
pretrained_model_name_or_path: stabilityai/stable-diffusion-2-1
custom_pipeline: ./hunyuanpaintpbr
data:
target: src.data.objaverse_hunyuan.DataModuleFromConfig
params:
batch_size: 1
num_workers: 4
train:
-
target: src.data.dataloader.objaverse_loader_forTexturePBR.TextureDataset
params:
num_view: 6
json_path: train_examples/examples.json
validation:
-
target: src.data.dataloader.objaverse_loader_forTexturePBR.TextureDataset
params:
num_view: 6
json_path: train_examples/examples.json
lightning:
modelcheckpoint:
params:
every_n_train_steps: 10000
save_top_k: -1
save_last: true
callbacks: {}
trainer:
benchmark: true
max_epochs: -1
gradient_clip_val: 1.0
val_check_interval: 1000
num_sanity_val_steps: 0
accumulate_grad_batches: 1
check_val_every_n_epoch: null # if not set this, validation does not run
init_control_from:
resume_from:
@@ -0,0 +1,160 @@
import trimesh
import pygltflib
import numpy as np
from PIL import Image
import base64
import io
import tempfile
import os
def combine_metallic_roughness(metallic_path, roughness_path, output_path):
"""
将metallic和roughness贴图合并为一张贴图
GLB格式要求metallic在B通道,roughness在G通道
"""
# 加载贴图
metallic_img = Image.open(metallic_path).convert("L") # 转为灰度
roughness_img = Image.open(roughness_path).convert("L") # 转为灰度
# 确保尺寸一致
if metallic_img.size != roughness_img.size:
roughness_img = roughness_img.resize(metallic_img.size)
# 创建RGB图像
width, height = metallic_img.size
combined = Image.new("RGB", (width, height))
# 转为numpy数组便于操作
metallic_array = np.array(metallic_img)
roughness_array = np.array(roughness_img)
# 创建合并的数组 (R, G, B) = (AO, Roughness, Metallic)
combined_array = np.zeros((height, width, 3), dtype=np.uint8)
combined_array[:, :, 0] = 255 # R通道:AO (如果没有AO贴图,设为白色)
combined_array[:, :, 1] = roughness_array # G通道:Roughness
combined_array[:, :, 2] = metallic_array # B通道:Metallic
# 转回PIL图像并保存
combined = Image.fromarray(combined_array)
combined.save(output_path)
return output_path
def create_glb_with_pbr_materials(obj_path, textures_dict, output_path):
"""
使用pygltflib创建包含完整PBR材质的GLB文件
textures_dict = {
'albedo': 'path/to/albedo.png',
'metallic': 'path/to/metallic.png',
'roughness': 'path/to/roughness.png',
'normal': 'path/to/normal.png', # 可选
'ao': 'path/to/ao.png' # 可选
}
"""
temp_files = []
try:
# 1. 加载OBJ文件
mesh = trimesh.load(obj_path)
# 2. 先导出为临时GLB
with tempfile.NamedTemporaryFile(suffix='.glb', delete=False) as temp_glb_file:
temp_glb = temp_glb_file.name
temp_files.append(temp_glb)
mesh.export(temp_glb)
# 3. 加载GLB文件进行材质编辑
gltf = pygltflib.GLTF2().load(temp_glb)
# 4. 准备纹理数据
def image_to_data_uri(image_path):
"""将图像转换为data URI"""
with open(image_path, "rb") as f:
image_data = f.read()
encoded = base64.b64encode(image_data).decode()
return f"data:image/png;base64,{encoded}"
# 5. 合并metallic和roughness
mr_combined_path = None
if "metallic" in textures_dict and "roughness" in textures_dict:
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_mr_file:
mr_combined_path = temp_mr_file.name
temp_files.append(mr_combined_path)
combine_metallic_roughness(textures_dict["metallic"], textures_dict["roughness"], mr_combined_path)
textures_dict["metallicRoughness"] = mr_combined_path
# 6. 添加图像到GLTF
images = []
textures = []
texture_mapping = {
"albedo": "baseColorTexture",
"metallicRoughness": "metallicRoughnessTexture",
"normal": "normalTexture",
"ao": "occlusionTexture",
}
for tex_type, tex_path in textures_dict.items():
if tex_type in texture_mapping and tex_path and os.path.exists(tex_path):
# 添加图像
image = pygltflib.Image(uri=image_to_data_uri(tex_path))
images.append(image)
# 添加纹理
texture = pygltflib.Texture(source=len(images) - 1)
textures.append(texture)
# 7. 创建PBR材质
pbr_metallic_roughness = pygltflib.PbrMetallicRoughness(
baseColorFactor=[1.0, 1.0, 1.0, 1.0], metallicFactor=1.0, roughnessFactor=1.0
)
# 设置纹理索引
texture_index = 0
if "albedo" in textures_dict and os.path.exists(textures_dict["albedo"]):
pbr_metallic_roughness.baseColorTexture = pygltflib.TextureInfo(index=texture_index)
texture_index += 1
if "metallicRoughness" in textures_dict and os.path.exists(textures_dict["metallicRoughness"]):
pbr_metallic_roughness.metallicRoughnessTexture = pygltflib.TextureInfo(index=texture_index)
texture_index += 1
# 创建材质
material = pygltflib.Material(name="PBR_Material", pbrMetallicRoughness=pbr_metallic_roughness)
# 添加法线贴图
if "normal" in textures_dict and os.path.exists(textures_dict["normal"]):
material.normalTexture = pygltflib.NormalTextureInfo(index=texture_index)
texture_index += 1
# 添加AO贴图
if "ao" in textures_dict and os.path.exists(textures_dict["ao"]):
material.occlusionTexture = pygltflib.OcclusionTextureInfo(index=texture_index)
# 8. 更新GLTF
gltf.images = images
gltf.textures = textures
gltf.materials = [material]
# 确保mesh使用材质
if gltf.meshes:
for primitive in gltf.meshes[0].primitives:
primitive.material = 0
# 9. 保存最终GLB
gltf.save(output_path)
print(f"PBR GLB file saved: {output_path}")
except Exception as e:
print(f"Error creating GLB with PBR materials: {e}")
raise e
finally:
for temp_file in temp_files:
try:
if os.path.exists(temp_file):
os.remove(temp_file)
except Exception as e:
print(f"Warning: Failed to remove temporary file {temp_file}: {e}")
@@ -0,0 +1,4 @@
"""
from .render import rasterize, interpolate
"""
from .render import *
@@ -0,0 +1,32 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import custom_rasterizer_kernel
import torch
def rasterize(pos, tri, resolution, clamp_depth=torch.zeros(0), use_depth_prior=0):
assert pos.device == tri.device
findices, barycentric = custom_rasterizer_kernel.rasterize_image(
pos[0], tri, clamp_depth, resolution[1], resolution[0], 1e-6, use_depth_prior
)
return findices, barycentric
def interpolate(col, findices, barycentric, tri):
f = findices - 1 + (findices == 0)
vcol = col[0, tri.long()[f.long()]]
result = barycentric.view(*barycentric.shape, 1) * vcol
result = torch.sum(result, axis=-2)
return result.view(1, *result.shape)
@@ -0,0 +1,574 @@
#include "rasterizer.h"
#include <fstream>
inline int pos2key(float* p, int resolution) {
int x = (p[0] * 0.5 + 0.5) * resolution;
int y = (p[1] * 0.5 + 0.5) * resolution;
int z = (p[2] * 0.5 + 0.5) * resolution;
return (x * resolution + y) * resolution + z;
}
inline void key2pos(int key, int resolution, float* p) {
int x = key / resolution / resolution;
int y = key / resolution % resolution;
int z = key % resolution;
p[0] = ((x + 0.5) / resolution - 0.5) * 2;
p[1] = ((y + 0.5) / resolution - 0.5) * 2;
p[2] = ((z + 0.5) / resolution - 0.5) * 2;
}
inline void key2cornerpos(int key, int resolution, float* p) {
int x = key / resolution / resolution;
int y = key / resolution % resolution;
int z = key % resolution;
p[0] = ((x + 0.75) / resolution - 0.5) * 2;
p[1] = ((y + 0.25) / resolution - 0.5) * 2;
p[2] = ((z + 0.75) / resolution - 0.5) * 2;
}
inline float* pos_ptr(int l, int i, int j, torch::Tensor t) {
float* pdata = t.data_ptr<float>();
int height = t.size(1);
int width = t.size(2);
return &pdata[((l * height + i) * width + j) * 4];
}
struct Grid
{
std::vector<int> seq2oddcorner;
std::vector<int> seq2evencorner;
std::vector<int> seq2grid;
std::vector<int> seq2normal;
std::vector<int> seq2neighbor;
std::unordered_map<int, int> grid2seq;
std::vector<int> downsample_seq;
int num_origin_seq;
int resolution;
int stride;
};
inline void pos_from_seq(Grid& grid, int seq, float* p) {
auto k = grid.seq2grid[seq];
key2pos(k, grid.resolution, p);
}
inline int fetch_seq(Grid& grid, int l, int i, int j, torch::Tensor pdata) {
float* p = pos_ptr(l, i, j, pdata);
if (p[3] == 0)
return -1;
auto key = pos2key(p, grid.resolution);
int seq = grid.grid2seq[key];
return seq;
}
inline int fetch_last_seq(Grid& grid, int i, int j, torch::Tensor pdata) {
int num_layers = pdata.size(0);
int l = 0;
int idx = fetch_seq(grid, l, i, j, pdata);
while (l < num_layers - 1) {
l += 1;
int new_idx = fetch_seq(grid, l, i, j, pdata);
if (new_idx == -1)
break;
idx = new_idx;
}
return idx;
}
inline int fetch_nearest_seq(Grid& grid, int i, int j, int dim, float d, torch::Tensor pdata) {
float p[3];
float max_dist = 1e10;
int best_idx = -1;
int num_layers = pdata.size(0);
for (int l = 0; l < num_layers; ++l) {
int idx = fetch_seq(grid, l, i, j, pdata);
if (idx == -1)
break;
pos_from_seq(grid, idx, p);
float dist = std::abs(d - p[(dim + 2) % 3]);
if (dist < max_dist) {
max_dist = dist;
best_idx = idx;
}
}
return best_idx;
}
inline int fetch_nearest_seq_layer(Grid& grid, int i, int j, int dim, float d, torch::Tensor pdata) {
float p[3];
float max_dist = 1e10;
int best_layer = -1;
int num_layers = pdata.size(0);
for (int l = 0; l < num_layers; ++l) {
int idx = fetch_seq(grid, l, i, j, pdata);
if (idx == -1)
break;
pos_from_seq(grid, idx, p);
float dist = std::abs(d - p[(dim + 2) % 3]);
if (dist < max_dist) {
max_dist = dist;
best_layer = l;
}
}
return best_layer;
}
void FetchNeighbor(Grid& grid, int seq, float* pos, int dim, int boundary_info, std::vector<torch::Tensor>& view_layer_positions,
int* output_indices)
{
auto t = view_layer_positions[dim];
int height = t.size(1);
int width = t.size(2);
int top = 0;
int ci = 0;
int cj = 0;
if (dim == 0) {
ci = (pos[1]/2+0.5)*height;
cj = (pos[0]/2+0.5)*width;
}
else if (dim == 1) {
ci = (pos[1]/2+0.5)*height;
cj = (pos[2]/2+0.5)*width;
}
else {
ci = (-pos[2]/2+0.5)*height;
cj = (pos[0]/2+0.5)*width;
}
int stride = grid.stride;
for (int ni = ci + stride; ni >= ci - stride; ni -= stride) {
for (int nj = cj - stride; nj <= cj + stride; nj += stride) {
int idx = -1;
if (ni == ci && nj == cj)
idx = seq;
else if (!(ni < 0 || ni >= height || nj < 0 || nj >= width)) {
if (boundary_info == -1)
idx = fetch_seq(grid, 0, ni, nj, t);
else if (boundary_info == 1)
idx = fetch_last_seq(grid, ni, nj, t);
else
idx = fetch_nearest_seq(grid, ni, nj, dim, pos[(dim + 2) % 3], t);
}
output_indices[top] = idx;
top += 1;
}
}
}
void DownsampleGrid(Grid& src, Grid& tar)
{
src.downsample_seq.resize(src.seq2grid.size(), -1);
tar.resolution = src.resolution / 2;
tar.stride = src.stride * 2;
float pos[3];
std::vector<int> seq2normal_count;
for (int i = 0; i < src.seq2grid.size(); ++i) {
key2pos(src.seq2grid[i], src.resolution, pos);
int k = pos2key(pos, tar.resolution);
int s = seq2normal_count.size();
if (!tar.grid2seq.count(k)) {
tar.grid2seq[k] = tar.seq2grid.size();
tar.seq2grid.emplace_back(k);
seq2normal_count.emplace_back(0);
seq2normal_count.emplace_back(0);
seq2normal_count.emplace_back(0);
//tar.seq2normal.emplace_back(src.seq2normal[i]);
} else {
s = tar.grid2seq[k] * 3;
}
seq2normal_count[s + src.seq2normal[i]] += 1;
src.downsample_seq[i] = tar.grid2seq[k];
}
tar.seq2normal.resize(seq2normal_count.size() / 3);
for (int i = 0; i < seq2normal_count.size(); i += 3) {
int t = 0;
for (int j = 1; j < 3; ++j) {
if (seq2normal_count[i + j] > seq2normal_count[i + t])
t = j;
}
tar.seq2normal[i / 3] = t;
}
}
void NeighborGrid(Grid& grid, std::vector<torch::Tensor> view_layer_positions, int v)
{
grid.seq2evencorner.resize(grid.seq2grid.size(), 0);
grid.seq2oddcorner.resize(grid.seq2grid.size(), 0);
std::unordered_set<int> visited_seq;
for (int vd = 0; vd < 3; ++vd) {
auto t = view_layer_positions[vd];
auto t0 = view_layer_positions[v];
int height = t.size(1);
int width = t.size(2);
int num_layers = t.size(0);
int num_view_layers = t0.size(0);
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
for (int l = 0; l < num_layers; ++l) {
int seq = fetch_seq(grid, l, i, j, t);
if (seq == -1)
break;
int dim = grid.seq2normal[seq];
if (dim != v)
continue;
float pos[3];
pos_from_seq(grid, seq, pos);
int ci = 0;
int cj = 0;
if (dim == 0) {
ci = (pos[1]/2+0.5)*height;
cj = (pos[0]/2+0.5)*width;
}
else if (dim == 1) {
ci = (pos[1]/2+0.5)*height;
cj = (pos[2]/2+0.5)*width;
}
else {
ci = (-pos[2]/2+0.5)*height;
cj = (pos[0]/2+0.5)*width;
}
if ((ci % (grid.stride * 2) < grid.stride) && (cj % (grid.stride * 2) >= grid.stride))
grid.seq2evencorner[seq] = 1;
if ((ci % (grid.stride * 2) >= grid.stride) && (cj % (grid.stride * 2) < grid.stride))
grid.seq2oddcorner[seq] = 1;
bool is_boundary = false;
if (vd == v) {
if (l == 0 || l == num_layers - 1)
is_boundary = true;
else {
int seq_new = fetch_seq(grid, l + 1, i, j, t);
if (seq_new == -1)
is_boundary = true;
}
}
int boundary_info = 0;
if (is_boundary && (l == 0))
boundary_info = -1;
else if (is_boundary)
boundary_info = 1;
if (visited_seq.count(seq))
continue;
visited_seq.insert(seq);
FetchNeighbor(grid, seq, pos, dim, boundary_info, view_layer_positions, &grid.seq2neighbor[seq * 9]);
}
}
}
}
}
void PadGrid(Grid& src, Grid& tar, std::vector<torch::Tensor>& view_layer_positions) {
auto& downsample_seq = src.downsample_seq;
auto& seq2evencorner = src.seq2evencorner;
auto& seq2oddcorner = src.seq2oddcorner;
int indices[9];
std::vector<int> mapped_even_corners(tar.seq2grid.size(), 0);
std::vector<int> mapped_odd_corners(tar.seq2grid.size(), 0);
for (int i = 0; i < downsample_seq.size(); ++i) {
if (seq2evencorner[i] > 0) {
mapped_even_corners[downsample_seq[i]] = 1;
}
if (seq2oddcorner[i] > 0) {
mapped_odd_corners[downsample_seq[i]] = 1;
}
}
auto& tar_seq2normal = tar.seq2normal;
auto& tar_seq2grid = tar.seq2grid;
for (int i = 0; i < tar_seq2grid.size(); ++i) {
if (mapped_even_corners[i] == 1 && mapped_odd_corners[i] == 1)
continue;
auto k = tar_seq2grid[i];
float p[3];
key2cornerpos(k, tar.resolution, p);
int src_key = pos2key(p, src.resolution);
if (!src.grid2seq.count(src_key)) {
int seq = src.seq2grid.size();
src.grid2seq[src_key] = seq;
src.seq2evencorner.emplace_back((mapped_even_corners[i] == 0));
src.seq2oddcorner.emplace_back((mapped_odd_corners[i] == 0));
src.seq2grid.emplace_back(src_key);
src.seq2normal.emplace_back(tar_seq2normal[i]);
FetchNeighbor(src, seq, p, tar_seq2normal[i], 0, view_layer_positions, indices);
for (int j = 0; j < 9; ++j) {
src.seq2neighbor.emplace_back(indices[j]);
}
src.downsample_seq.emplace_back(i);
} else {
int seq = src.grid2seq[src_key];
if (mapped_even_corners[i] == 0)
src.seq2evencorner[seq] = 1;
if (mapped_odd_corners[i] == 0)
src.seq2oddcorner[seq] = 1;
}
}
}
std::vector<std::vector<torch::Tensor>> build_hierarchy(std::vector<torch::Tensor> view_layer_positions,
std::vector<torch::Tensor> view_layer_normals, int num_level, int resolution)
{
if (view_layer_positions.size() != 3 || num_level < 1) {
printf("Alert! We require 3 layers and at least 1 level! (%d %d)\n", view_layer_positions.size(), num_level);
return {{},{},{},{}};
}
std::vector<Grid> grids;
grids.resize(num_level);
std::vector<float> seq2pos;
auto& seq2grid = grids[0].seq2grid;
auto& seq2normal = grids[0].seq2normal;
auto& grid2seq = grids[0].grid2seq;
grids[0].resolution = resolution;
grids[0].stride = 1;
auto int64_options = torch::TensorOptions().dtype(torch::kInt64).requires_grad(false);
auto float_options = torch::TensorOptions().dtype(torch::kFloat32).requires_grad(false);
for (int v = 0; v < 3; ++v) {
int num_layers = view_layer_positions[v].size(0);
int height = view_layer_positions[v].size(1);
int width = view_layer_positions[v].size(2);
float* data = view_layer_positions[v].data_ptr<float>();
float* data_normal = view_layer_normals[v].data_ptr<float>();
for (int l = 0; l < num_layers; ++l) {
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
float* p = &data[(i * width + j) * 4];
float* n = &data_normal[(i * width + j) * 3];
if (p[3] == 0)
continue;
auto k = pos2key(p, resolution);
if (!grid2seq.count(k)) {
int dim = 0;
for (int d = 0; d < 3; ++d) {
if (std::abs(n[d]) > std::abs(n[dim]))
dim = d;
}
dim = (dim + 1) % 3;
grid2seq[k] = seq2grid.size();
seq2grid.emplace_back(k);
seq2pos.push_back(p[0]);
seq2pos.push_back(p[1]);
seq2pos.push_back(p[2]);
seq2normal.emplace_back(dim);
}
}
}
data += (height * width * 4);
data_normal += (height * width * 3);
}
}
for (int i = 0; i < num_level - 1; ++i) {
DownsampleGrid(grids[i], grids[i + 1]);
}
for (int l = 0; l < num_level; ++l) {
grids[l].seq2neighbor.resize(grids[l].seq2grid.size() * 9, -1);
grids[l].num_origin_seq = grids[l].seq2grid.size();
for (int d = 0; d < 3; ++d) {
NeighborGrid(grids[l], view_layer_positions, d);
}
}
for (int i = num_level - 2; i >= 0; --i) {
PadGrid(grids[i], grids[i + 1], view_layer_positions);
}
for (int i = grids[0].num_origin_seq; i < grids[0].seq2grid.size(); ++i) {
int k = grids[0].seq2grid[i];
float p[3];
key2pos(k, grids[0].resolution, p);
seq2pos.push_back(p[0]);
seq2pos.push_back(p[1]);
seq2pos.push_back(p[2]);
}
std::vector<torch::Tensor> texture_positions(2);
std::vector<torch::Tensor> grid_neighbors(grids.size());
std::vector<torch::Tensor> grid_downsamples(grids.size() - 1);
std::vector<torch::Tensor> grid_evencorners(grids.size());
std::vector<torch::Tensor> grid_oddcorners(grids.size());
texture_positions[0] = torch::zeros({seq2pos.size() / 3, 3}, float_options);
texture_positions[1] = torch::zeros({seq2pos.size() / 3}, float_options);
float* positions_out_ptr = texture_positions[0].data_ptr<float>();
memcpy(positions_out_ptr, seq2pos.data(), sizeof(float) * seq2pos.size());
positions_out_ptr = texture_positions[1].data_ptr<float>();
for (int i = 0; i < grids[0].seq2grid.size(); ++i) {
positions_out_ptr[i] = (i < grids[0].num_origin_seq);
}
for (int i = 0; i < grids.size(); ++i) {
grid_neighbors[i] = torch::zeros({grids[i].seq2grid.size(), 9}, int64_options);
long* nptr = grid_neighbors[i].data_ptr<long>();
for (int j = 0; j < grids[i].seq2neighbor.size(); ++j) {
nptr[j] = grids[i].seq2neighbor[j];
}
grid_evencorners[i] = torch::zeros({grids[i].seq2evencorner.size()}, int64_options);
grid_oddcorners[i] = torch::zeros({grids[i].seq2oddcorner.size()}, int64_options);
long* dptr = grid_evencorners[i].data_ptr<long>();
for (int j = 0; j < grids[i].seq2evencorner.size(); ++j) {
dptr[j] = grids[i].seq2evencorner[j];
}
dptr = grid_oddcorners[i].data_ptr<long>();
for (int j = 0; j < grids[i].seq2oddcorner.size(); ++j) {
dptr[j] = grids[i].seq2oddcorner[j];
}
if (i + 1 < grids.size()) {
grid_downsamples[i] = torch::zeros({grids[i].downsample_seq.size()}, int64_options);
long* dptr = grid_downsamples[i].data_ptr<long>();
for (int j = 0; j < grids[i].downsample_seq.size(); ++j) {
dptr[j] = grids[i].downsample_seq[j];
}
}
}
return {texture_positions, grid_neighbors, grid_downsamples, grid_evencorners, grid_oddcorners};
}
std::vector<std::vector<torch::Tensor>> build_hierarchy_with_feat(
std::vector<torch::Tensor> view_layer_positions,
std::vector<torch::Tensor> view_layer_normals,
std::vector<torch::Tensor> view_layer_feats,
int num_level, int resolution)
{
if (view_layer_positions.size() != 3 || num_level < 1) {
printf("Alert! We require 3 layers and at least 1 level! (%d %d)\n", view_layer_positions.size(), num_level);
return {{},{},{},{}};
}
std::vector<Grid> grids;
grids.resize(num_level);
std::vector<float> seq2pos;
std::vector<float> seq2feat;
auto& seq2grid = grids[0].seq2grid;
auto& seq2normal = grids[0].seq2normal;
auto& grid2seq = grids[0].grid2seq;
grids[0].resolution = resolution;
grids[0].stride = 1;
auto int64_options = torch::TensorOptions().dtype(torch::kInt64).requires_grad(false);
auto float_options = torch::TensorOptions().dtype(torch::kFloat32).requires_grad(false);
int feat_channel = 3;
for (int v = 0; v < 3; ++v) {
int num_layers = view_layer_positions[v].size(0);
int height = view_layer_positions[v].size(1);
int width = view_layer_positions[v].size(2);
float* data = view_layer_positions[v].data_ptr<float>();
float* data_normal = view_layer_normals[v].data_ptr<float>();
float* data_feat = view_layer_feats[v].data_ptr<float>();
feat_channel = view_layer_feats[v].size(3);
for (int l = 0; l < num_layers; ++l) {
for (int i = 0; i < height; ++i) {
for (int j = 0; j < width; ++j) {
float* p = &data[(i * width + j) * 4];
float* n = &data_normal[(i * width + j) * 3];
float* f = &data_feat[(i * width + j) * feat_channel];
if (p[3] == 0)
continue;
auto k = pos2key(p, resolution);
if (!grid2seq.count(k)) {
int dim = 0;
for (int d = 0; d < 3; ++d) {
if (std::abs(n[d]) > std::abs(n[dim]))
dim = d;
}
dim = (dim + 1) % 3;
grid2seq[k] = seq2grid.size();
seq2grid.emplace_back(k);
seq2pos.push_back(p[0]);
seq2pos.push_back(p[1]);
seq2pos.push_back(p[2]);
for (int c = 0; c < feat_channel; ++c) {
seq2feat.emplace_back(f[c]);
}
seq2normal.emplace_back(dim);
}
}
}
data += (height * width * 4);
data_normal += (height * width * 3);
data_feat += (height * width * feat_channel);
}
}
for (int i = 0; i < num_level - 1; ++i) {
DownsampleGrid(grids[i], grids[i + 1]);
}
for (int l = 0; l < num_level; ++l) {
grids[l].seq2neighbor.resize(grids[l].seq2grid.size() * 9, -1);
grids[l].num_origin_seq = grids[l].seq2grid.size();
for (int d = 0; d < 3; ++d) {
NeighborGrid(grids[l], view_layer_positions, d);
}
}
for (int i = num_level - 2; i >= 0; --i) {
PadGrid(grids[i], grids[i + 1], view_layer_positions);
}
for (int i = grids[0].num_origin_seq; i < grids[0].seq2grid.size(); ++i) {
int k = grids[0].seq2grid[i];
float p[3];
key2pos(k, grids[0].resolution, p);
seq2pos.push_back(p[0]);
seq2pos.push_back(p[1]);
seq2pos.push_back(p[2]);
for (int c = 0; c < feat_channel; ++c) {
seq2feat.emplace_back(0.5);
}
}
std::vector<torch::Tensor> texture_positions(2);
std::vector<torch::Tensor> texture_feats(1);
std::vector<torch::Tensor> grid_neighbors(grids.size());
std::vector<torch::Tensor> grid_downsamples(grids.size() - 1);
std::vector<torch::Tensor> grid_evencorners(grids.size());
std::vector<torch::Tensor> grid_oddcorners(grids.size());
texture_positions[0] = torch::zeros({seq2pos.size() / 3, 3}, float_options);
texture_positions[1] = torch::zeros({seq2pos.size() / 3}, float_options);
texture_feats[0] = torch::zeros({seq2feat.size() / feat_channel, feat_channel}, float_options);
float* positions_out_ptr = texture_positions[0].data_ptr<float>();
memcpy(positions_out_ptr, seq2pos.data(), sizeof(float) * seq2pos.size());
positions_out_ptr = texture_positions[1].data_ptr<float>();
for (int i = 0; i < grids[0].seq2grid.size(); ++i) {
positions_out_ptr[i] = (i < grids[0].num_origin_seq);
}
float* feats_out_ptr = texture_feats[0].data_ptr<float>();
memcpy(feats_out_ptr, seq2feat.data(), sizeof(float) * seq2feat.size());
for (int i = 0; i < grids.size(); ++i) {
grid_neighbors[i] = torch::zeros({grids[i].seq2grid.size(), 9}, int64_options);
long* nptr = grid_neighbors[i].data_ptr<long>();
for (int j = 0; j < grids[i].seq2neighbor.size(); ++j) {
nptr[j] = grids[i].seq2neighbor[j];
}
grid_evencorners[i] = torch::zeros({grids[i].seq2evencorner.size()}, int64_options);
grid_oddcorners[i] = torch::zeros({grids[i].seq2oddcorner.size()}, int64_options);
long* dptr = grid_evencorners[i].data_ptr<long>();
for (int j = 0; j < grids[i].seq2evencorner.size(); ++j) {
dptr[j] = grids[i].seq2evencorner[j];
}
dptr = grid_oddcorners[i].data_ptr<long>();
for (int j = 0; j < grids[i].seq2oddcorner.size(); ++j) {
dptr[j] = grids[i].seq2oddcorner[j];
}
if (i + 1 < grids.size()) {
grid_downsamples[i] = torch::zeros({grids[i].downsample_seq.size()}, int64_options);
long* dptr = grid_downsamples[i].data_ptr<long>();
for (int j = 0; j < grids[i].downsample_seq.size(); ++j) {
dptr[j] = grids[i].downsample_seq[j];
}
}
}
return {texture_positions, texture_feats, grid_neighbors, grid_downsamples, grid_evencorners, grid_oddcorners};
}
@@ -0,0 +1,139 @@
#include "rasterizer.h"
void rasterizeTriangleCPU(int idx, float* vt0, float* vt1, float* vt2, int width, int height, INT64* zbuffer, float* d, float occlusion_truncation) {
float x_min = std::min(vt0[0], std::min(vt1[0],vt2[0]));
float x_max = std::max(vt0[0], std::max(vt1[0],vt2[0]));
float y_min = std::min(vt0[1], std::min(vt1[1],vt2[1]));
float y_max = std::max(vt0[1], std::max(vt1[1],vt2[1]));
for (int px = x_min; px < x_max + 1; ++px) {
if (px < 0 || px >= width)
continue;
for (int py = y_min; py < y_max + 1; ++py) {
if (py < 0 || py >= height)
continue;
float vt[2] = {px + 0.5, py + 0.5};
float baryCentricCoordinate[3];
calculateBarycentricCoordinate(vt0, vt1, vt2, vt, baryCentricCoordinate);
if (isBarycentricCoordInBounds(baryCentricCoordinate)) {
int pixel = py * width + px;
if (zbuffer == 0) {
zbuffer[pixel] = (INT64)(idx + 1);
continue;
}
float depth = baryCentricCoordinate[0] * vt0[2] + baryCentricCoordinate[1] * vt1[2] + baryCentricCoordinate[2] * vt2[2];
float depth_thres = 0;
if (d) {
depth_thres = d[pixel] * 0.49999f + 0.5f + occlusion_truncation;
}
int z_quantize = depth * (2<<17);
INT64 token = (INT64)z_quantize * MAXINT + (INT64)(idx + 1);
if (depth < depth_thres)
continue;
zbuffer[pixel] = std::min(zbuffer[pixel], token);
}
}
}
}
void barycentricFromImgcoordCPU(float* V, int* F, int* findices, INT64* zbuffer, int width, int height, int num_vertices, int num_faces,
float* barycentric_map, int pix)
{
INT64 f = zbuffer[pix] % MAXINT;
if (f == (MAXINT-1)) {
findices[pix] = 0;
barycentric_map[pix * 3] = 0;
barycentric_map[pix * 3 + 1] = 0;
barycentric_map[pix * 3 + 2] = 0;
return;
}
findices[pix] = f;
f -= 1;
float barycentric[3] = {0, 0, 0};
if (f >= 0) {
float vt[2] = {float(pix % width) + 0.5f, float(pix / width) + 0.5f};
float* vt0_ptr = V + (F[f * 3] * 4);
float* vt1_ptr = V + (F[f * 3 + 1] * 4);
float* vt2_ptr = V + (F[f * 3 + 2] * 4);
float vt0[2] = {(vt0_ptr[0] / vt0_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt0_ptr[1] / vt0_ptr[3]) * (height - 1) + 0.5f};
float vt1[2] = {(vt1_ptr[0] / vt1_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt1_ptr[1] / vt1_ptr[3]) * (height - 1) + 0.5f};
float vt2[2] = {(vt2_ptr[0] / vt2_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt2_ptr[1] / vt2_ptr[3]) * (height - 1) + 0.5f};
calculateBarycentricCoordinate(vt0, vt1, vt2, vt, barycentric);
barycentric[0] = barycentric[0] / vt0_ptr[3];
barycentric[1] = barycentric[1] / vt1_ptr[3];
barycentric[2] = barycentric[2] / vt2_ptr[3];
float w = 1.0f / (barycentric[0] + barycentric[1] + barycentric[2]);
barycentric[0] *= w;
barycentric[1] *= w;
barycentric[2] *= w;
}
barycentric_map[pix * 3] = barycentric[0];
barycentric_map[pix * 3 + 1] = barycentric[1];
barycentric_map[pix * 3 + 2] = barycentric[2];
}
void rasterizeImagecoordsKernelCPU(float* V, int* F, float* d, INT64* zbuffer, float occlusion_trunc, int width, int height, int num_vertices, int num_faces, int f)
{
float* vt0_ptr = V + (F[f * 3] * 4);
float* vt1_ptr = V + (F[f * 3 + 1] * 4);
float* vt2_ptr = V + (F[f * 3 + 2] * 4);
float vt0[3] = {(vt0_ptr[0] / vt0_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt0_ptr[1] / vt0_ptr[3]) * (height - 1) + 0.5f, vt0_ptr[2] / vt0_ptr[3] * 0.49999f + 0.5f};
float vt1[3] = {(vt1_ptr[0] / vt1_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt1_ptr[1] / vt1_ptr[3]) * (height - 1) + 0.5f, vt1_ptr[2] / vt1_ptr[3] * 0.49999f + 0.5f};
float vt2[3] = {(vt2_ptr[0] / vt2_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt2_ptr[1] / vt2_ptr[3]) * (height - 1) + 0.5f, vt2_ptr[2] / vt2_ptr[3] * 0.49999f + 0.5f};
rasterizeTriangleCPU(f, vt0, vt1, vt2, width, height, zbuffer, d, occlusion_trunc);
}
std::vector<torch::Tensor> rasterize_image_cpu(torch::Tensor V, torch::Tensor F, torch::Tensor D,
int width, int height, float occlusion_truncation, int use_depth_prior)
{
int num_faces = F.size(0);
int num_vertices = V.size(0);
auto options = torch::TensorOptions().dtype(torch::kInt32).requires_grad(false);
auto INT64_options = torch::TensorOptions().dtype(torch::kInt64).requires_grad(false);
auto findices = torch::zeros({height, width}, options);
INT64 maxint = (INT64)MAXINT * (INT64)MAXINT + (MAXINT - 1);
auto z_min = torch::ones({height, width}, INT64_options) * (long)maxint;
if (!use_depth_prior) {
for (int i = 0; i < num_faces; ++i) {
rasterizeImagecoordsKernelCPU(V.data_ptr<float>(), F.data_ptr<int>(), 0,
(INT64*)z_min.data_ptr<long>(), occlusion_truncation, width, height, num_vertices, num_faces, i);
}
} else {
for (int i = 0; i < num_faces; ++i)
rasterizeImagecoordsKernelCPU(V.data_ptr<float>(), F.data_ptr<int>(), D.data_ptr<float>(),
(INT64*)z_min.data_ptr<long>(), occlusion_truncation, width, height, num_vertices, num_faces, i);
}
auto float_options = torch::TensorOptions().dtype(torch::kFloat32).requires_grad(false);
auto barycentric = torch::zeros({height, width, 3}, float_options);
for (int i = 0; i < width * height; ++i)
barycentricFromImgcoordCPU(V.data_ptr<float>(), F.data_ptr<int>(),
findices.data_ptr<int>(), (INT64*)z_min.data_ptr<long>(), width, height, num_vertices, num_faces, barycentric.data_ptr<float>(), i);
return {findices, barycentric};
}
std::vector<torch::Tensor> rasterize_image(torch::Tensor V, torch::Tensor F, torch::Tensor D,
int width, int height, float occlusion_truncation, int use_depth_prior)
{
int device_id = V.get_device();
if (device_id == -1)
return rasterize_image_cpu(V, F, D, width, height, occlusion_truncation, use_depth_prior);
else
return rasterize_image_gpu(V, F, D, width, height, occlusion_truncation, use_depth_prior);
}
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("rasterize_image", &rasterize_image, "Custom image rasterization");
m.def("build_hierarchy", &build_hierarchy, "Custom image rasterization");
m.def("build_hierarchy_with_feat", &build_hierarchy_with_feat, "Custom image rasterization");
}
@@ -0,0 +1,54 @@
#ifndef RASTERIZER_H_
#define RASTERIZER_H_
#include <torch/extension.h>
#include <vector>
#include <ATen/ATen.h>
#include <ATen/cuda/CUDAContext.h> // For CUDA context
#define INT64 unsigned long long
#define MAXINT 2147483647
__host__ __device__ inline float calculateSignedArea2(float* a, float* b, float* c) {
return ((c[0] - a[0]) * (b[1] - a[1]) - (b[0] - a[0]) * (c[1] - a[1]));
}
__host__ __device__ inline void calculateBarycentricCoordinate(float* a, float* b, float* c, float* p,
float* barycentric)
{
float beta_tri = calculateSignedArea2(a, p, c);
float gamma_tri = calculateSignedArea2(a, b, p);
float area = calculateSignedArea2(a, b, c);
if (area == 0) {
barycentric[0] = -1.0;
barycentric[1] = -1.0;
barycentric[2] = -1.0;
return;
}
float tri_inv = 1.0 / area;
float beta = beta_tri * tri_inv;
float gamma = gamma_tri * tri_inv;
float alpha = 1.0 - beta - gamma;
barycentric[0] = alpha;
barycentric[1] = beta;
barycentric[2] = gamma;
}
__host__ __device__ inline bool isBarycentricCoordInBounds(float* barycentricCoord) {
return barycentricCoord[0] >= 0.0 && barycentricCoord[0] <= 1.0 &&
barycentricCoord[1] >= 0.0 && barycentricCoord[1] <= 1.0 &&
barycentricCoord[2] >= 0.0 && barycentricCoord[2] <= 1.0;
}
std::vector<torch::Tensor> rasterize_image_gpu(torch::Tensor V, torch::Tensor F, torch::Tensor D,
int width, int height, float occlusion_truncation, int use_depth_prior);
std::vector<std::vector<torch::Tensor>> build_hierarchy(std::vector<torch::Tensor> view_layer_positions, std::vector<torch::Tensor> view_layer_normals, int num_level, int resolution);
std::vector<std::vector<torch::Tensor>> build_hierarchy_with_feat(
std::vector<torch::Tensor> view_layer_positions,
std::vector<torch::Tensor> view_layer_normals,
std::vector<torch::Tensor> view_layer_feats,
int num_level, int resolution);
#endif
@@ -0,0 +1,127 @@
#include "rasterizer.h"
__device__ void rasterizeTriangleGPU(int idx, float* vt0, float* vt1, float* vt2, int width, int height, INT64* zbuffer, float* d, float occlusion_truncation) {
float x_min = std::min(vt0[0], std::min(vt1[0],vt2[0]));
float x_max = std::max(vt0[0], std::max(vt1[0],vt2[0]));
float y_min = std::min(vt0[1], std::min(vt1[1],vt2[1]));
float y_max = std::max(vt0[1], std::max(vt1[1],vt2[1]));
for (int px = x_min; px < x_max + 1; ++px) {
if (px < 0 || px >= width)
continue;
for (int py = y_min; py < y_max + 1; ++py) {
if (py < 0 || py >= height)
continue;
float vt[2] = {px + 0.5f, py + 0.5f};
float baryCentricCoordinate[3];
calculateBarycentricCoordinate(vt0, vt1, vt2, vt, baryCentricCoordinate);
if (isBarycentricCoordInBounds(baryCentricCoordinate)) {
int pixel = py * width + px;
if (zbuffer == 0) {
atomicExch(&zbuffer[pixel], (INT64)(idx + 1));
continue;
}
float depth = baryCentricCoordinate[0] * vt0[2] + baryCentricCoordinate[1] * vt1[2] + baryCentricCoordinate[2] * vt2[2];
float depth_thres = 0;
if (d) {
depth_thres = d[pixel] * 0.49999f + 0.5f + occlusion_truncation;
}
int z_quantize = depth * (2<<17);
INT64 token = (INT64)z_quantize * MAXINT + (INT64)(idx + 1);
if (depth < depth_thres)
continue;
atomicMin(&zbuffer[pixel], token);
}
}
}
}
__global__ void barycentricFromImgcoordGPU(float* V, int* F, int* findices, INT64* zbuffer, int width, int height, int num_vertices, int num_faces,
float* barycentric_map)
{
int pix = blockIdx.x * blockDim.x + threadIdx.x;
if (pix >= width * height)
return;
INT64 f = zbuffer[pix] % MAXINT;
if (f == (MAXINT-1)) {
findices[pix] = 0;
barycentric_map[pix * 3] = 0;
barycentric_map[pix * 3 + 1] = 0;
barycentric_map[pix * 3 + 2] = 0;
return;
}
findices[pix] = f;
f -= 1;
float barycentric[3] = {0, 0, 0};
if (f >= 0) {
float vt[2] = {float(pix % width) + 0.5f, float(pix / width) + 0.5f};
float* vt0_ptr = V + (F[f * 3] * 4);
float* vt1_ptr = V + (F[f * 3 + 1] * 4);
float* vt2_ptr = V + (F[f * 3 + 2] * 4);
float vt0[2] = {(vt0_ptr[0] / vt0_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt0_ptr[1] / vt0_ptr[3]) * (height - 1) + 0.5f};
float vt1[2] = {(vt1_ptr[0] / vt1_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt1_ptr[1] / vt1_ptr[3]) * (height - 1) + 0.5f};
float vt2[2] = {(vt2_ptr[0] / vt2_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt2_ptr[1] / vt2_ptr[3]) * (height - 1) + 0.5f};
calculateBarycentricCoordinate(vt0, vt1, vt2, vt, barycentric);
barycentric[0] = barycentric[0] / vt0_ptr[3];
barycentric[1] = barycentric[1] / vt1_ptr[3];
barycentric[2] = barycentric[2] / vt2_ptr[3];
float w = 1.0f / (barycentric[0] + barycentric[1] + barycentric[2]);
barycentric[0] *= w;
barycentric[1] *= w;
barycentric[2] *= w;
}
barycentric_map[pix * 3] = barycentric[0];
barycentric_map[pix * 3 + 1] = barycentric[1];
barycentric_map[pix * 3 + 2] = barycentric[2];
}
__global__ void rasterizeImagecoordsKernelGPU(float* V, int* F, float* d, INT64* zbuffer, float occlusion_trunc, int width, int height, int num_vertices, int num_faces)
{
int f = blockIdx.x * blockDim.x + threadIdx.x;
if (f >= num_faces)
return;
float* vt0_ptr = V + (F[f * 3] * 4);
float* vt1_ptr = V + (F[f * 3 + 1] * 4);
float* vt2_ptr = V + (F[f * 3 + 2] * 4);
float vt0[3] = {(vt0_ptr[0] / vt0_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt0_ptr[1] / vt0_ptr[3]) * (height - 1) + 0.5f, vt0_ptr[2] / vt0_ptr[3] * 0.49999f + 0.5f};
float vt1[3] = {(vt1_ptr[0] / vt1_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt1_ptr[1] / vt1_ptr[3]) * (height - 1) + 0.5f, vt1_ptr[2] / vt1_ptr[3] * 0.49999f + 0.5f};
float vt2[3] = {(vt2_ptr[0] / vt2_ptr[3] * 0.5f + 0.5f) * (width - 1) + 0.5f, (0.5f + 0.5f * vt2_ptr[1] / vt2_ptr[3]) * (height - 1) + 0.5f, vt2_ptr[2] / vt2_ptr[3] * 0.49999f + 0.5f};
rasterizeTriangleGPU(f, vt0, vt1, vt2, width, height, zbuffer, d, occlusion_trunc);
}
std::vector<torch::Tensor> rasterize_image_gpu(torch::Tensor V, torch::Tensor F, torch::Tensor D,
int width, int height, float occlusion_truncation, int use_depth_prior)
{
int device_id = V.get_device();
cudaSetDevice(device_id);
int num_faces = F.size(0);
int num_vertices = V.size(0);
auto options = torch::TensorOptions().dtype(torch::kInt32).device(torch::kCUDA, device_id).requires_grad(false);
auto INT64_options = torch::TensorOptions().dtype(torch::kInt64).device(torch::kCUDA, device_id).requires_grad(false);
auto findices = torch::zeros({height, width}, options);
INT64 maxint = (INT64)MAXINT * (INT64)MAXINT + (MAXINT - 1);
auto z_min = torch::ones({height, width}, INT64_options) * (long)maxint;
if (!use_depth_prior) {
rasterizeImagecoordsKernelGPU<<<(num_faces+255)/256,256,0,at::cuda::getCurrentCUDAStream()>>>(V.data_ptr<float>(), F.data_ptr<int>(), 0,
(INT64*)z_min.data_ptr<long>(), occlusion_truncation, width, height, num_vertices, num_faces);
} else {
rasterizeImagecoordsKernelGPU<<<(num_faces+255)/256,256,0,at::cuda::getCurrentCUDAStream()>>>(V.data_ptr<float>(), F.data_ptr<int>(), D.data_ptr<float>(),
(INT64*)z_min.data_ptr<long>(), occlusion_truncation, width, height, num_vertices, num_faces);
}
auto float_options = torch::TensorOptions().dtype(torch::kFloat32).device(torch::kCUDA, device_id).requires_grad(false);
auto barycentric = torch::zeros({height, width, 3}, float_options);
barycentricFromImgcoordGPU<<<(width * height + 255)/256, 256>>>(V.data_ptr<float>(), F.data_ptr<int>(),
findices.data_ptr<int>(), (INT64*)z_min.data_ptr<long>(), width, height, num_vertices, num_faces, barycentric.data_ptr<float>());
return {findices, barycentric};
}
@@ -0,0 +1,40 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from setuptools import setup, find_packages
import torch
from torch.utils.cpp_extension import BuildExtension, CUDAExtension, CppExtension
# build custom rasterizer
custom_rasterizer_module = CUDAExtension(
"custom_rasterizer_kernel",
[
"lib/custom_rasterizer_kernel/rasterizer.cpp",
"lib/custom_rasterizer_kernel/grid_neighbor.cpp",
"lib/custom_rasterizer_kernel/rasterizer_gpu.cu",
],
)
setup(
packages=find_packages(),
version="0.1",
name="custom_rasterizer",
include_package_data=True,
package_dir={"": "."},
ext_modules=[
custom_rasterizer_module,
],
cmdclass={"build_ext": BuildExtension},
)
@@ -0,0 +1,35 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from textureGenPipeline import Hunyuan3DPaintPipeline, Hunyuan3DPaintConfig
try:
from utils.torchvision_fix import apply_fix
apply_fix()
except ImportError:
print("Warning: torchvision_fix module not found, proceeding without compatibility fix")
except Exception as e:
print(f"Warning: Failed to apply torchvision fix: {e}")
if __name__ == "__main__":
max_num_view = 6 # can be 6 to 9
resolution = 512 # can be 768 or 512
conf = Hunyuan3DPaintConfig(max_num_view, resolution)
paint_pipeline = Hunyuan3DPaintPipeline(conf)
output_mesh_path = paint_pipeline(mesh_path="./assets/case_1/mesh.glb", image_path="./assets/case_1/image.png")
print(f"Output mesh path: {output_mesh_path}")
@@ -0,0 +1,39 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from .pipeline import HunyuanPaintPipeline
from .unet.model import HunyuanPaint
from .unet.modules import (
Dino_v2,
Basic2p5DTransformerBlock,
ImageProjModel,
UNet2p5DConditionModel,
)
from .unet.attn_processor import (
PoseRoPEAttnProcessor2_0,
SelfAttnProcessor2_0,
RefAttnProcessor2_0,
)
__all__ = [
'HunyuanPaintPipeline',
'HunyuanPaint',
'Dino_v2',
'Basic2p5DTransformerBlock',
'ImageProjModel',
'UNet2p5DConditionModel',
'PoseRoPEAttnProcessor2_0',
'SelfAttnProcessor2_0',
'RefAttnProcessor2_0',
]
@@ -0,0 +1,736 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from typing import Any, Dict, Optional
from diffusers.models import AutoencoderKL, UNet2DConditionModel
from diffusers.schedulers import KarrasDiffusionSchedulers
import numpy
import torch
import torch.utils.checkpoint
import torch.distributed
import numpy as np
import transformers
from PIL import Image
from einops import rearrange
from transformers import CLIPImageProcessor, CLIPTextModel, CLIPTokenizer, CLIPVisionModelWithProjection
from typing import Any, Callable, Dict, List, Optional, Union, Tuple
import diffusers
from diffusers import (
AutoencoderKL,
DiffusionPipeline,
UNet2DConditionModel,
)
from diffusers.image_processor import VaeImageProcessor
from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion import (
StableDiffusionPipeline,
retrieve_timesteps,
rescale_noise_cfg,
)
from diffusers.utils import deprecate
from diffusers.callbacks import MultiPipelineCallbacks, PipelineCallback
from diffusers.image_processor import PipelineImageInput
from diffusers.pipelines.stable_diffusion.pipeline_output import StableDiffusionPipelineOutput
from .unet.modules import UNet2p5DConditionModel
from .unet.attn_processor import SelfAttnProcessor2_0, RefAttnProcessor2_0, PoseRoPEAttnProcessor2_0
__all__ = [
"HunyuanPaintPipeline",
"UNet2p5DConditionModel",
"SelfAttnProcessor2_0",
"RefAttnProcessor2_0",
"PoseRoPEAttnProcessor2_0",
]
def to_rgb_image(maybe_rgba: Image.Image):
if maybe_rgba.mode == "RGB":
return maybe_rgba
elif maybe_rgba.mode == "RGBA":
rgba = maybe_rgba
img = numpy.random.randint(127, 128, size=[rgba.size[1], rgba.size[0], 3], dtype=numpy.uint8)
img = Image.fromarray(img, "RGB")
img.paste(rgba, mask=rgba.getchannel("A"))
return img
else:
raise ValueError("Unsupported image type.", maybe_rgba.mode)
class HunyuanPaintPipeline(StableDiffusionPipeline):
"""Custom pipeline for multiview PBR texture generation.
Extends Stable Diffusion with:
- Material-specific conditioning
- Multiview processing
- Position-aware attention
- 2.5D UNet integration
"""
def __init__(
self,
vae: AutoencoderKL,
text_encoder: CLIPTextModel,
tokenizer: CLIPTokenizer,
unet: UNet2DConditionModel,
scheduler: KarrasDiffusionSchedulers,
feature_extractor: CLIPImageProcessor,
safety_checker=None,
use_torch_compile=False,
):
DiffusionPipeline.__init__(self)
safety_checker = None
self.register_modules(
vae=torch.compile(vae) if use_torch_compile else vae,
text_encoder=text_encoder,
tokenizer=tokenizer,
unet=unet,
scheduler=scheduler,
safety_checker=safety_checker,
feature_extractor=torch.compile(feature_extractor) if use_torch_compile else feature_extractor,
)
self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)
self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor)
if isinstance(self.unet, UNet2DConditionModel):
self.unet = UNet2p5DConditionModel(self.unet, None, self.scheduler)
def eval(self):
self.unet.eval()
self.vae.eval()
def set_pbr_settings(self, pbr_settings: List[str]):
self.pbr_settings = pbr_settings
def set_learned_parameters(self):
"""Configures parameter freezing strategy.
Freezes:
- Standard attention layers
- Dual-stream reference UNet
Unfreezes:
- Material-specific parameters
- DINO integration components
"""
freezed_names = ["attn1", "unet_dual"]
added_learned_names = ["albedo", "mr", "dino"]
for name, params in self.unet.named_parameters():
if any(freeze_name in name for freeze_name in freezed_names) and all(
learned_name not in name for learned_name in added_learned_names
):
params.requires_grad = False
else:
params.requires_grad = True
def prepare(self):
if isinstance(self.unet, UNet2DConditionModel):
self.unet = UNet2p5DConditionModel(self.unet, None, self.scheduler).eval()
@torch.no_grad()
def encode_images(self, images):
"""Encodes multiview image batches into latent space.
Args:
images: Input images [B, N_views, C, H, W]
Returns:
torch.Tensor: Latent representations [B, N_views, C, H_latent, W_latent]
"""
B = images.shape[0]
images = rearrange(images, "b n c h w -> (b n) c h w")
dtype = next(self.vae.parameters()).dtype
images = (images - 0.5) * 2.0
posterior = self.vae.encode(images.to(dtype)).latent_dist
latents = posterior.sample() * self.vae.config.scaling_factor
latents = rearrange(latents, "(b n) c h w -> b n c h w", b=B)
return latents
@torch.no_grad()
def __call__(
self,
images=None,
prompt=None,
negative_prompt="watermark, ugly, deformed, noisy, blurry, low contrast",
*args,
num_images_per_prompt: Optional[int] = 1,
guidance_scale=3.0,
output_type: Optional[str] = "pil",
width=512,
height=512,
num_inference_steps=15,
return_dict=True,
sync_condition=None,
**cached_condition,
):
"""Main generation method for multiview PBR textures.
Steps:
1. Input validation and preparation
2. Reference image encoding
3. Condition processing (normal/position maps)
4. Prompt embedding setup
5. Classifier-free guidance preparation
6. Diffusion sampling loop
Args:
images: List of reference PIL images
prompt: Text prompt (overridden by learned embeddings)
cached_condition: Dictionary containing:
- images_normal: Normal maps (PIL or tensor)
- images_position: Position maps (PIL or tensor)
Returns:
List[PIL.Image]: Generated multiview PBR textures
"""
self.prepare()
if images is None:
raise ValueError("Inputting embeddings not supported for this pipeline. Please pass an image.")
assert not isinstance(images, torch.Tensor)
if not isinstance(images, List):
images = [images]
images = [to_rgb_image(image) for image in images]
images_vae = [torch.tensor(np.array(image) / 255.0) for image in images]
images_vae = [image_vae.unsqueeze(0).permute(0, 3, 1, 2).unsqueeze(0) for image_vae in images_vae]
images_vae = torch.cat(images_vae, dim=1)
images_vae = images_vae.to(device=self.vae.device, dtype=self.unet.dtype)
batch_size = images_vae.shape[0]
N_ref = images_vae.shape[1]
assert batch_size == 1
assert num_images_per_prompt == 1
if self.unet.use_ra:
ref_latents = self.encode_images(images_vae)
cached_condition["ref_latents"] = ref_latents
def convert_pil_list_to_tensor(images):
bg_c = [1.0, 1.0, 1.0]
images_tensor = []
for batch_imgs in images:
view_imgs = []
for pil_img in batch_imgs:
img = numpy.asarray(pil_img, dtype=numpy.float32) / 255.0
if img.shape[2] > 3:
alpha = img[:, :, 3:]
img = img[:, :, :3] * alpha + bg_c * (1 - alpha)
img = torch.from_numpy(img).permute(2, 0, 1).unsqueeze(0).contiguous().half().to("cuda")
view_imgs.append(img)
view_imgs = torch.cat(view_imgs, dim=0)
images_tensor.append(view_imgs.unsqueeze(0))
images_tensor = torch.cat(images_tensor, dim=0)
return images_tensor
if "images_normal" in cached_condition:
if isinstance(cached_condition["images_normal"], List):
cached_condition["images_normal"] = convert_pil_list_to_tensor(cached_condition["images_normal"])
cached_condition["embeds_normal"] = self.encode_images(cached_condition["images_normal"])
if "images_position" in cached_condition:
if isinstance(cached_condition["images_position"], List):
cached_condition["images_position"] = convert_pil_list_to_tensor(cached_condition["images_position"])
cached_condition["position_maps"] = cached_condition["images_position"]
cached_condition["embeds_position"] = self.encode_images(cached_condition["images_position"])
if self.unet.use_learned_text_clip:
all_shading_tokens = []
for token in self.unet.pbr_setting:
all_shading_tokens.append(
getattr(self.unet, f"learned_text_clip_{token}").unsqueeze(dim=0).repeat(batch_size, 1, 1)
)
prompt_embeds = torch.stack(all_shading_tokens, dim=1)
negative_prompt_embeds = torch.stack(all_shading_tokens, dim=1)
# negative_prompt_embeds = torch.zeros_like(prompt_embeds)
else:
if prompt is None:
prompt = "high quality"
if isinstance(prompt, str):
prompt = [prompt for _ in range(batch_size)]
device = self._execution_device
prompt_embeds, _ = self.encode_prompt(
prompt, device=device, num_images_per_prompt=num_images_per_prompt, do_classifier_free_guidance=False
)
if isinstance(negative_prompt, str):
negative_prompt = [negative_prompt for _ in range(batch_size)]
if negative_prompt is not None:
negative_prompt_embeds, _ = self.encode_prompt(
negative_prompt,
device=device,
num_images_per_prompt=num_images_per_prompt,
do_classifier_free_guidance=False,
)
else:
negative_prompt_embeds = torch.zeros_like(prompt_embeds)
if guidance_scale > 1:
if self.unet.use_ra:
cached_condition["ref_latents"] = cached_condition["ref_latents"].repeat(
3, *([1] * (cached_condition["ref_latents"].dim() - 1))
)
cached_condition["ref_scale"] = torch.as_tensor([0.0, 1.0, 1.0]).to(cached_condition["ref_latents"])
if self.unet.use_dino:
zero_states = torch.zeros_like(cached_condition["dino_hidden_states"])
cached_condition["dino_hidden_states"] = torch.cat(
[zero_states, zero_states, cached_condition["dino_hidden_states"]]
)
del zero_states
if "embeds_normal" in cached_condition:
cached_condition["embeds_normal"] = cached_condition["embeds_normal"].repeat(
3, *([1] * (cached_condition["embeds_normal"].dim() - 1))
)
if "embeds_position" in cached_condition:
cached_condition["embeds_position"] = cached_condition["embeds_position"].repeat(
3, *([1] * (cached_condition["embeds_position"].dim() - 1))
)
if "position_maps" in cached_condition:
cached_condition["position_maps"] = cached_condition["position_maps"].repeat(
3, *([1] * (cached_condition["position_maps"].dim() - 1))
)
images = self.denoise(
None,
*args,
cross_attention_kwargs=None,
guidance_scale=guidance_scale,
num_images_per_prompt=num_images_per_prompt,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
num_inference_steps=num_inference_steps,
output_type=output_type,
width=width,
height=height,
return_dict=return_dict,
**cached_condition,
)
return images
def denoise(
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"],
**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.
Core denoising procedure for multiview PBR texture generation.
Handles the complete diffusion process including:
- Input validation and preparation
- Timestep scheduling
- Latent noise initialization
- Iterative denoising with specialized guidance
- Output decoding and post-processing
Key innovations:
1. Triple-batch classifier-free guidance:
- Negative (unconditional)
- Reference-conditioned
- Full-conditioned
2. View-dependent guidance scaling:
- Adjusts influence based on camera azimuth
3. PBR-aware latent organization:
- Maintains material/view separation throughout
4. Optimized VRAM management:
- Selective tensor reshaping
Processing Stages:
1. Setup & Validation: Configures pipeline components and validates inputs
2. Prompt Encoding: Processes text/material conditioning
3. Latent Initialization: Prepares noise for denoising process
4. Iterative Denoising:
a) Scales and organizes latent variables
b) Predicts noise at current timestep
c) Applies view-dependent guidance
d) Computes previous latent state
5. Output Decoding: Converts latents to final images
6. Cleanup: Releases resources and formats output
"""
callback = kwargs.pop("callback", None)
callback_steps = kwargs.pop("callback_steps", None)
# open cache
kwargs["cache"] = {}
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` 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])
prompt_embeds = torch.cat([negative_prompt_embeds, 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
)
assert num_images_per_prompt == 1
# 5. Prepare latent variables
n_pbr = len(self.unet.pbr_setting)
num_channels_latents = self.unet.config.in_channels
latents = self.prepare_latents(
batch_size * kwargs["num_in_batch"] * n_pbr, # 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)
# 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
latents = rearrange(
latents, "(b n_pbr n) c h w -> b n_pbr n c h w", n=kwargs["num_in_batch"], n_pbr=n_pbr
)
# latent_model_input = torch.cat([latents] * 3) if self.do_classifier_free_guidance else latents
latent_model_input = latents.repeat(3, 1, 1, 1, 1, 1) if self.do_classifier_free_guidance else latents
latent_model_input = rearrange(latent_model_input, "b n_pbr n c h w -> (b n_pbr n) c h w")
latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)
latent_model_input = rearrange(
latent_model_input, "(b n_pbr n) c h w ->b n_pbr n c h w", n=kwargs["num_in_batch"], n_pbr=n_pbr
)
# predict the noise residual
noise_pred = self.unet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
timestep_cond=timestep_cond,
cross_attention_kwargs=self.cross_attention_kwargs,
added_cond_kwargs=added_cond_kwargs,
return_dict=False,
**kwargs,
)[0]
latents = rearrange(latents, "b n_pbr n c h w -> (b n_pbr n) c h w")
# 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)
noise_pred_uncond, noise_pred_ref, noise_pred_full = noise_pred.chunk(3)
if "camera_azims" in kwargs.keys():
camera_azims = kwargs["camera_azims"]
else:
camera_azims = [0] * kwargs["num_in_batch"]
def cam_mapping(azim):
if azim < 90 and azim >= 0:
return float(azim) / 90.0 + 1
elif azim >= 90 and azim < 330:
return 2.0
else:
return -float(azim) / 90.0 + 5.0
view_scale_tensor = (
torch.from_numpy(np.asarray([cam_mapping(azim) for azim in camera_azims]))
.unsqueeze(0)
.repeat(n_pbr, 1)
.view(-1)
.to(noise_pred_uncond)[:, None, None, None]
)
noise_pred = noise_pred_uncond + self.guidance_scale * view_scale_tensor * (
noise_pred_ref - noise_pred_uncond
)
noise_pred += self.guidance_scale * view_scale_tensor * (noise_pred_full - noise_pred_ref)
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_ref, guidance_rescale=self.guidance_rescale)
# compute the previous noisy sample x_t -> x_t-1
latents = self.scheduler.step(
noise_pred, t, latents[:, :num_channels_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 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)
@@ -0,0 +1,839 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import Optional, Dict, Tuple, Union, Literal, List, Callable
from einops import rearrange
from diffusers.utils import deprecate
from diffusers.models.attention_processor import Attention, AttnProcessor
class AttnUtils:
"""
Shared utility functions for attention processing.
This class provides common operations used across different attention processors
to eliminate code duplication and improve maintainability.
"""
@staticmethod
def check_pytorch_compatibility():
"""
Check PyTorch compatibility for scaled_dot_product_attention.
Raises:
ImportError: If PyTorch version doesn't support scaled_dot_product_attention
"""
if not hasattr(F, "scaled_dot_product_attention"):
raise ImportError("AttnProcessor2_0 requires PyTorch 2.0, to use it, please upgrade PyTorch to 2.0.")
@staticmethod
def handle_deprecation_warning(args, kwargs):
"""
Handle deprecation warning for the 'scale' argument.
Args:
args: Positional arguments passed to attention processor
kwargs: Keyword arguments passed to attention processor
"""
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)
@staticmethod
def prepare_hidden_states(
hidden_states, attn, temb, spatial_norm_attr="spatial_norm", group_norm_attr="group_norm"
):
"""
Common preprocessing of hidden states for attention computation.
Args:
hidden_states: Input hidden states tensor
attn: Attention module instance
temb: Optional temporal embedding tensor
spatial_norm_attr: Attribute name for spatial normalization
group_norm_attr: Attribute name for group normalization
Returns:
Tuple of (processed_hidden_states, residual, input_ndim, shape_info)
"""
residual = hidden_states
spatial_norm = getattr(attn, spatial_norm_attr, None)
if spatial_norm is not None:
hidden_states = 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)
else:
batch_size, channel, height, width = None, None, None, None
group_norm = getattr(attn, group_norm_attr, None)
if group_norm is not None:
hidden_states = group_norm(hidden_states.transpose(1, 2)).transpose(1, 2)
return hidden_states, residual, input_ndim, (batch_size, channel, height, width)
@staticmethod
def prepare_attention_mask(attention_mask, attn, sequence_length, batch_size):
"""
Prepare attention mask for scaled_dot_product_attention.
Args:
attention_mask: Input attention mask tensor or None
attn: Attention module instance
sequence_length: Length of the sequence
batch_size: Batch size
Returns:
Prepared attention mask tensor reshaped for multi-head attention
"""
if attention_mask is not None:
attention_mask = attn.prepare_attention_mask(attention_mask, sequence_length, batch_size)
attention_mask = attention_mask.view(batch_size, attn.heads, -1, attention_mask.shape[-1])
return attention_mask
@staticmethod
def reshape_qkv_for_attention(tensor, batch_size, attn_heads, head_dim):
"""
Reshape Q/K/V tensors for multi-head attention computation.
Args:
tensor: Input tensor to reshape
batch_size: Batch size
attn_heads: Number of attention heads
head_dim: Dimension per attention head
Returns:
Reshaped tensor with shape [batch_size, attn_heads, seq_len, head_dim]
"""
return tensor.view(batch_size, -1, attn_heads, head_dim).transpose(1, 2)
@staticmethod
def apply_norms(query, key, norm_q, norm_k):
"""
Apply Q/K normalization layers if available.
Args:
query: Query tensor
key: Key tensor
norm_q: Query normalization layer (optional)
norm_k: Key normalization layer (optional)
Returns:
Tuple of (normalized_query, normalized_key)
"""
if norm_q is not None:
query = norm_q(query)
if norm_k is not None:
key = norm_k(key)
return query, key
@staticmethod
def finalize_output(hidden_states, input_ndim, shape_info, attn, residual, to_out):
"""
Common output processing including projection, dropout, reshaping, and residual connection.
Args:
hidden_states: Processed hidden states from attention
input_ndim: Original input tensor dimensions
shape_info: Tuple containing original shape information
attn: Attention module instance
residual: Residual connection tensor
to_out: Output projection layers [linear, dropout]
Returns:
Final output tensor after all processing steps
"""
batch_size, channel, height, width = shape_info
# Apply output projection and dropout
hidden_states = to_out[0](hidden_states)
hidden_states = to_out[1](hidden_states)
# Reshape back if needed
if input_ndim == 4:
hidden_states = hidden_states.transpose(-1, -2).reshape(batch_size, channel, height, width)
# Apply residual connection
if attn.residual_connection:
hidden_states = hidden_states + residual
# Apply rescaling
hidden_states = hidden_states / attn.rescale_output_factor
return hidden_states
# Base class for attention processors (eliminating initialization duplication)
class BaseAttnProcessor(nn.Module):
"""
Base class for attention processors with common initialization.
This base class provides shared parameter initialization and module registration
functionality to reduce code duplication across different attention processor types.
"""
def __init__(
self,
query_dim: int,
pbr_setting: List[str] = ["albedo", "mr"],
cross_attention_dim: Optional[int] = None,
heads: int = 8,
kv_heads: Optional[int] = None,
dim_head: int = 64,
dropout: float = 0.0,
bias: bool = False,
upcast_attention: bool = False,
upcast_softmax: bool = False,
cross_attention_norm: Optional[str] = None,
cross_attention_norm_num_groups: int = 32,
qk_norm: Optional[str] = None,
added_kv_proj_dim: Optional[int] = None,
added_proj_bias: Optional[bool] = True,
norm_num_groups: Optional[int] = None,
spatial_norm_dim: Optional[int] = None,
out_bias: bool = True,
scale_qk: bool = True,
only_cross_attention: bool = False,
eps: float = 1e-5,
rescale_output_factor: float = 1.0,
residual_connection: bool = False,
_from_deprecated_attn_block: bool = False,
processor: Optional["AttnProcessor"] = None,
out_dim: int = None,
out_context_dim: int = None,
context_pre_only=None,
pre_only=False,
elementwise_affine: bool = True,
is_causal: bool = False,
**kwargs,
):
"""
Initialize base attention processor with common parameters.
Args:
query_dim: Dimension of query features
pbr_setting: List of PBR material types to process (e.g., ["albedo", "mr"])
cross_attention_dim: Dimension of cross-attention features (optional)
heads: Number of attention heads
kv_heads: Number of key-value heads for grouped query attention (optional)
dim_head: Dimension per attention head
dropout: Dropout rate
bias: Whether to use bias in linear projections
upcast_attention: Whether to upcast attention computation to float32
upcast_softmax: Whether to upcast softmax computation to float32
cross_attention_norm: Type of cross-attention normalization (optional)
cross_attention_norm_num_groups: Number of groups for cross-attention norm
qk_norm: Type of query-key normalization (optional)
added_kv_proj_dim: Dimension for additional key-value projections (optional)
added_proj_bias: Whether to use bias in additional projections
norm_num_groups: Number of groups for normalization (optional)
spatial_norm_dim: Dimension for spatial normalization (optional)
out_bias: Whether to use bias in output projection
scale_qk: Whether to scale query-key products
only_cross_attention: Whether to only perform cross-attention
eps: Small epsilon value for numerical stability
rescale_output_factor: Factor to rescale output values
residual_connection: Whether to use residual connections
_from_deprecated_attn_block: Flag for deprecated attention blocks
processor: Optional attention processor instance
out_dim: Output dimension (optional)
out_context_dim: Output context dimension (optional)
context_pre_only: Whether to only process context in pre-processing
pre_only: Whether to only perform pre-processing
elementwise_affine: Whether to use element-wise affine transformations
is_causal: Whether to use causal attention masking
**kwargs: Additional keyword arguments
"""
super().__init__()
AttnUtils.check_pytorch_compatibility()
# Store common attributes
self.pbr_setting = pbr_setting
self.n_pbr_tokens = len(self.pbr_setting)
self.inner_dim = out_dim if out_dim is not None else dim_head * heads
self.inner_kv_dim = self.inner_dim if kv_heads is None else dim_head * kv_heads
self.query_dim = query_dim
self.use_bias = bias
self.is_cross_attention = cross_attention_dim is not None
self.cross_attention_dim = cross_attention_dim if cross_attention_dim is not None else query_dim
self.upcast_attention = upcast_attention
self.upcast_softmax = upcast_softmax
self.rescale_output_factor = rescale_output_factor
self.residual_connection = residual_connection
self.dropout = dropout
self.fused_projections = False
self.out_dim = out_dim if out_dim is not None else query_dim
self.out_context_dim = out_context_dim if out_context_dim is not None else query_dim
self.context_pre_only = context_pre_only
self.pre_only = pre_only
self.is_causal = is_causal
self._from_deprecated_attn_block = _from_deprecated_attn_block
self.scale_qk = scale_qk
self.scale = dim_head**-0.5 if self.scale_qk else 1.0
self.heads = out_dim // dim_head if out_dim is not None else heads
self.sliceable_head_dim = heads
self.added_kv_proj_dim = added_kv_proj_dim
self.only_cross_attention = only_cross_attention
self.added_proj_bias = added_proj_bias
# Validation
if self.added_kv_proj_dim is None and self.only_cross_attention:
raise ValueError(
"`only_cross_attention` can only be set to True if `added_kv_proj_dim` is not None."
"Make sure to set either `only_cross_attention=False` or define `added_kv_proj_dim`."
)
def register_pbr_modules(self, module_types: List[str], **kwargs):
"""
Generic PBR module registration to eliminate code repetition.
Dynamically registers PyTorch modules for different PBR material types
based on the specified module types and PBR settings.
Args:
module_types: List of module types to register ("qkv", "v_only", "out", "add_kv")
**kwargs: Additional arguments for module configuration
"""
for pbr_token in self.pbr_setting:
if pbr_token == "albedo":
continue
for module_type in module_types:
if module_type == "qkv":
self.register_module(
f"to_q_{pbr_token}", nn.Linear(self.query_dim, self.inner_dim, bias=self.use_bias)
)
self.register_module(
f"to_k_{pbr_token}", nn.Linear(self.cross_attention_dim, self.inner_dim, bias=self.use_bias)
)
self.register_module(
f"to_v_{pbr_token}", nn.Linear(self.cross_attention_dim, self.inner_dim, bias=self.use_bias)
)
elif module_type == "v_only":
self.register_module(
f"to_v_{pbr_token}", nn.Linear(self.cross_attention_dim, self.inner_dim, bias=self.use_bias)
)
elif module_type == "out":
if not self.pre_only:
self.register_module(
f"to_out_{pbr_token}",
nn.ModuleList(
[
nn.Linear(self.inner_dim, self.out_dim, bias=kwargs.get("out_bias", True)),
nn.Dropout(self.dropout),
]
),
)
else:
self.register_module(f"to_out_{pbr_token}", None)
elif module_type == "add_kv":
if self.added_kv_proj_dim is not None:
self.register_module(
f"add_k_proj_{pbr_token}",
nn.Linear(self.added_kv_proj_dim, self.inner_kv_dim, bias=self.added_proj_bias),
)
self.register_module(
f"add_v_proj_{pbr_token}",
nn.Linear(self.added_kv_proj_dim, self.inner_kv_dim, bias=self.added_proj_bias),
)
else:
self.register_module(f"add_k_proj_{pbr_token}", None)
self.register_module(f"add_v_proj_{pbr_token}", None)
# Rotary Position Embedding utilities (specialized for PoseRoPE)
class RotaryEmbedding:
"""
Rotary position embedding utilities for 3D spatial attention.
Provides functions to compute and apply rotary position embeddings (RoPE)
for 1D, 3D spatial coordinates used in 3D-aware attention mechanisms.
"""
@staticmethod
def get_1d_rotary_pos_embed(dim: int, pos: torch.Tensor, theta: float = 10000.0, linear_factor=1.0, ntk_factor=1.0):
"""
Compute 1D rotary position embeddings.
Args:
dim: Embedding dimension (must be even)
pos: Position tensor
theta: Base frequency for rotary embeddings
linear_factor: Linear scaling factor
ntk_factor: NTK (Neural Tangent Kernel) scaling factor
Returns:
Tuple of (cos_embeddings, sin_embeddings)
"""
assert dim % 2 == 0
theta = theta * ntk_factor
freqs = (
1.0
/ (theta ** (torch.arange(0, dim, 2, dtype=pos.dtype, device=pos.device)[: (dim // 2)] / dim))
/ linear_factor
)
freqs = torch.outer(pos, freqs)
freqs_cos = freqs.cos().repeat_interleave(2, dim=1).float()
freqs_sin = freqs.sin().repeat_interleave(2, dim=1).float()
return freqs_cos, freqs_sin
@staticmethod
def get_3d_rotary_pos_embed(position, embed_dim, voxel_resolution, theta: int = 10000):
"""
Compute 3D rotary position embeddings for spatial coordinates.
Args:
position: 3D position tensor with shape [..., 3]
embed_dim: Embedding dimension
voxel_resolution: Resolution of the voxel grid
theta: Base frequency for rotary embeddings
Returns:
Tuple of (cos_embeddings, sin_embeddings) for 3D positions
"""
assert position.shape[-1] == 3
dim_xy = embed_dim // 8 * 3
dim_z = embed_dim // 8 * 2
grid = torch.arange(voxel_resolution, dtype=torch.float32, device=position.device)
freqs_xy = RotaryEmbedding.get_1d_rotary_pos_embed(dim_xy, grid, theta=theta)
freqs_z = RotaryEmbedding.get_1d_rotary_pos_embed(dim_z, grid, theta=theta)
xy_cos, xy_sin = freqs_xy
z_cos, z_sin = freqs_z
embed_flattn = position.view(-1, position.shape[-1])
x_cos = xy_cos[embed_flattn[:, 0], :]
x_sin = xy_sin[embed_flattn[:, 0], :]
y_cos = xy_cos[embed_flattn[:, 1], :]
y_sin = xy_sin[embed_flattn[:, 1], :]
z_cos = z_cos[embed_flattn[:, 2], :]
z_sin = z_sin[embed_flattn[:, 2], :]
cos = torch.cat((x_cos, y_cos, z_cos), dim=-1)
sin = torch.cat((x_sin, y_sin, z_sin), dim=-1)
cos = cos.view(*position.shape[:-1], embed_dim)
sin = sin.view(*position.shape[:-1], embed_dim)
return cos, sin
@staticmethod
def apply_rotary_emb(x: torch.Tensor, freqs_cis: Union[torch.Tensor, Tuple[torch.Tensor]]):
"""
Apply rotary position embeddings to input tensor.
Args:
x: Input tensor to apply rotary embeddings to
freqs_cis: Tuple of (cos_embeddings, sin_embeddings) or single tensor
Returns:
Tensor with rotary position embeddings applied
"""
cos, sin = freqs_cis
cos, sin = cos.to(x.device), sin.to(x.device)
cos = cos.unsqueeze(1)
sin = sin.unsqueeze(1)
x_real, x_imag = x.reshape(*x.shape[:-1], -1, 2).unbind(-1)
x_rotated = torch.stack([-x_imag, x_real], dim=-1).flatten(3)
out = (x.float() * cos + x_rotated.float() * sin).to(x.dtype)
return out
# Core attention processing logic (eliminating major duplication)
class AttnCore:
"""
Core attention processing logic shared across processors.
This class provides the fundamental attention computation pipeline
that can be reused across different attention processor implementations.
"""
@staticmethod
def process_attention_base(
attn: Attention,
hidden_states: torch.Tensor,
encoder_hidden_states: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
temb: Optional[torch.Tensor] = None,
get_qkv_fn: Callable = None,
apply_rope_fn: Optional[Callable] = None,
**kwargs,
):
"""
Generic attention processing core shared across different processors.
This function implements the common attention computation pipeline including:
1. Hidden state preprocessing
2. Attention mask preparation
3. Q/K/V computation via provided function
4. Tensor reshaping for multi-head attention
5. Optional normalization and RoPE application
6. Scaled dot-product attention computation
Args:
attn: Attention module instance
hidden_states: Input hidden states tensor
encoder_hidden_states: Optional encoder hidden states for cross-attention
attention_mask: Optional attention mask tensor
temb: Optional temporal embedding tensor
get_qkv_fn: Function to compute Q, K, V tensors
apply_rope_fn: Optional function to apply rotary position embeddings
**kwargs: Additional keyword arguments passed to subfunctions
Returns:
Tuple containing (attention_output, residual, input_ndim, shape_info,
batch_size, num_heads, head_dim)
"""
# Prepare hidden states
hidden_states, residual, input_ndim, shape_info = AttnUtils.prepare_hidden_states(hidden_states, attn, temb)
batch_size, sequence_length, _ = (
hidden_states.shape if encoder_hidden_states is None else encoder_hidden_states.shape
)
# Prepare attention mask
attention_mask = AttnUtils.prepare_attention_mask(attention_mask, attn, sequence_length, batch_size)
# Get Q, K, V
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)
query, key, value = get_qkv_fn(attn, hidden_states, encoder_hidden_states, **kwargs)
# Reshape for attention
inner_dim = key.shape[-1]
head_dim = inner_dim // attn.heads
query = AttnUtils.reshape_qkv_for_attention(query, batch_size, attn.heads, head_dim)
key = AttnUtils.reshape_qkv_for_attention(key, batch_size, attn.heads, head_dim)
value = AttnUtils.reshape_qkv_for_attention(value, batch_size, attn.heads, value.shape[-1] // attn.heads)
# Apply normalization
query, key = AttnUtils.apply_norms(query, key, getattr(attn, "norm_q", None), getattr(attn, "norm_k", None))
# Apply RoPE if provided
if apply_rope_fn is not None:
query, key = apply_rope_fn(query, key, head_dim, **kwargs)
# Compute attention
hidden_states = F.scaled_dot_product_attention(
query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False
)
return hidden_states, residual, input_ndim, shape_info, batch_size, attn.heads, head_dim
# Specific processor implementations (minimal unique code)
class PoseRoPEAttnProcessor2_0:
"""
Attention processor with Rotary Position Encoding (RoPE) for 3D spatial awareness.
This processor extends standard attention with 3D rotary position embeddings
to provide spatial awareness for 3D scene understanding tasks.
"""
def __init__(self):
"""Initialize the RoPE attention processor."""
AttnUtils.check_pytorch_compatibility()
def __call__(
self,
attn: Attention,
hidden_states: torch.Tensor,
encoder_hidden_states: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_indices: Dict = None,
temb: Optional[torch.Tensor] = None,
n_pbrs=1,
*args,
**kwargs,
) -> torch.Tensor:
"""
Apply RoPE-enhanced attention computation.
Args:
attn: Attention module instance
hidden_states: Input hidden states tensor
encoder_hidden_states: Optional encoder hidden states for cross-attention
attention_mask: Optional attention mask tensor
position_indices: Dictionary containing 3D position information for RoPE
temb: Optional temporal embedding tensor
n_pbrs: Number of PBR material types
*args: Additional positional arguments
**kwargs: Additional keyword arguments
Returns:
Attention output tensor with applied rotary position encodings
"""
AttnUtils.handle_deprecation_warning(args, kwargs)
def get_qkv(attn, hidden_states, encoder_hidden_states, **kwargs):
return attn.to_q(hidden_states), attn.to_k(encoder_hidden_states), attn.to_v(encoder_hidden_states)
def apply_rope(query, key, head_dim, **kwargs):
if position_indices is not None:
if head_dim in position_indices:
image_rotary_emb = position_indices[head_dim]
else:
image_rotary_emb = RotaryEmbedding.get_3d_rotary_pos_embed(
rearrange(
position_indices["voxel_indices"].unsqueeze(1).repeat(1, n_pbrs, 1, 1),
"b n_pbrs l c -> (b n_pbrs) l c",
),
head_dim,
voxel_resolution=position_indices["voxel_resolution"],
)
position_indices[head_dim] = image_rotary_emb
query = RotaryEmbedding.apply_rotary_emb(query, image_rotary_emb)
key = RotaryEmbedding.apply_rotary_emb(key, image_rotary_emb)
return query, key
# Core attention processing
hidden_states, residual, input_ndim, shape_info, batch_size, heads, head_dim = AttnCore.process_attention_base(
attn,
hidden_states,
encoder_hidden_states,
attention_mask,
temb,
get_qkv_fn=get_qkv,
apply_rope_fn=apply_rope,
position_indices=position_indices,
n_pbrs=n_pbrs,
)
# Finalize output
hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, heads * head_dim)
hidden_states = hidden_states.to(hidden_states.dtype)
return AttnUtils.finalize_output(hidden_states, input_ndim, shape_info, attn, residual, attn.to_out)
class SelfAttnProcessor2_0(BaseAttnProcessor):
"""
Self-attention processor with PBR (Physically Based Rendering) material support.
This processor handles multiple PBR material types (e.g., albedo, metallic-roughness)
with separate attention computation paths for each material type.
"""
def __init__(self, **kwargs):
"""
Initialize self-attention processor with PBR support.
Args:
**kwargs: Arguments passed to BaseAttnProcessor initialization
"""
super().__init__(**kwargs)
self.register_pbr_modules(["qkv", "out", "add_kv"], **kwargs)
def process_single(
self,
attn: Attention,
hidden_states: torch.Tensor,
encoder_hidden_states: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
temb: Optional[torch.Tensor] = None,
token: Literal["albedo", "mr"] = "albedo",
multiple_devices=False,
*args,
**kwargs,
):
"""
Process attention for a single PBR material type.
Args:
attn: Attention module instance
hidden_states: Input hidden states tensor
encoder_hidden_states: Optional encoder hidden states for cross-attention
attention_mask: Optional attention mask tensor
temb: Optional temporal embedding tensor
token: PBR material type to process ("albedo", "mr", etc.)
multiple_devices: Whether to use multiple GPU devices
*args: Additional positional arguments
**kwargs: Additional keyword arguments
Returns:
Processed attention output for the specified PBR material type
"""
target = attn if token == "albedo" else attn.processor
token_suffix = "" if token == "albedo" else "_" + token
# Device management (if needed)
if multiple_devices:
device = torch.device("cuda:0") if token == "albedo" else torch.device("cuda:1")
for attr in [f"to_q{token_suffix}", f"to_k{token_suffix}", f"to_v{token_suffix}", f"to_out{token_suffix}"]:
getattr(target, attr).to(device)
def get_qkv(attn, hidden_states, encoder_hidden_states, **kwargs):
return (
getattr(target, f"to_q{token_suffix}")(hidden_states),
getattr(target, f"to_k{token_suffix}")(encoder_hidden_states),
getattr(target, f"to_v{token_suffix}")(encoder_hidden_states),
)
# Core processing using shared logic
hidden_states, residual, input_ndim, shape_info, batch_size, heads, head_dim = AttnCore.process_attention_base(
attn, hidden_states, encoder_hidden_states, attention_mask, temb, get_qkv_fn=get_qkv
)
# Finalize
hidden_states = hidden_states.transpose(1, 2).reshape(batch_size, -1, heads * head_dim)
hidden_states = hidden_states.to(hidden_states.dtype)
return AttnUtils.finalize_output(
hidden_states, input_ndim, shape_info, attn, residual, getattr(target, f"to_out{token_suffix}")
)
def __call__(
self,
attn: Attention,
hidden_states: torch.Tensor,
encoder_hidden_states: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
temb: Optional[torch.Tensor] = None,
*args,
**kwargs,
) -> torch.Tensor:
"""
Apply self-attention with PBR material processing.
Processes multiple PBR material types sequentially, applying attention
computation for each material type separately and combining results.
Args:
attn: Attention module instance
hidden_states: Input hidden states tensor with PBR dimension
encoder_hidden_states: Optional encoder hidden states for cross-attention
attention_mask: Optional attention mask tensor
temb: Optional temporal embedding tensor
*args: Additional positional arguments
**kwargs: Additional keyword arguments
Returns:
Combined attention output for all PBR material types
"""
AttnUtils.handle_deprecation_warning(args, kwargs)
B = hidden_states.size(0)
pbr_hidden_states = torch.split(hidden_states, 1, dim=1)
# Process each PBR setting
results = []
for token, pbr_hs in zip(self.pbr_setting, pbr_hidden_states):
processed_hs = rearrange(pbr_hs, "b n_pbrs n l c -> (b n_pbrs n) l c").to("cuda:0")
result = self.process_single(attn, processed_hs, None, attention_mask, temb, token, False)
results.append(result)
outputs = [rearrange(result, "(b n_pbrs n) l c -> b n_pbrs n l c", b=B, n_pbrs=1) for result in results]
return torch.cat(outputs, dim=1)
class RefAttnProcessor2_0(BaseAttnProcessor):
"""
Reference attention processor with shared value computation across PBR materials.
This processor computes query and key once, but uses separate value projections
for different PBR material types, enabling efficient multi-material processing.
"""
def __init__(self, **kwargs):
"""
Initialize reference attention processor.
Args:
**kwargs: Arguments passed to BaseAttnProcessor initialization
"""
super().__init__(**kwargs)
self.pbr_settings = self.pbr_setting # Alias for compatibility
self.register_pbr_modules(["v_only", "out"], **kwargs)
def __call__(
self,
attn: Attention,
hidden_states: torch.Tensor,
encoder_hidden_states: Optional[torch.Tensor] = None,
attention_mask: Optional[torch.Tensor] = None,
temb: Optional[torch.Tensor] = None,
*args,
**kwargs,
) -> torch.Tensor:
"""
Apply reference attention with shared Q/K and separate V projections.
This method computes query and key tensors once and reuses them across
all PBR material types, while using separate value projections for each
material type to maintain material-specific information.
Args:
attn: Attention module instance
hidden_states: Input hidden states tensor
encoder_hidden_states: Optional encoder hidden states for cross-attention
attention_mask: Optional attention mask tensor
temb: Optional temporal embedding tensor
*args: Additional positional arguments
**kwargs: Additional keyword arguments
Returns:
Stacked attention output for all PBR material types
"""
AttnUtils.handle_deprecation_warning(args, kwargs)
def get_qkv(attn, hidden_states, encoder_hidden_states, **kwargs):
query = attn.to_q(hidden_states)
key = attn.to_k(encoder_hidden_states)
# Concatenate values from all PBR settings
value_list = [attn.to_v(encoder_hidden_states)]
for token in ["_" + token for token in self.pbr_settings if token != "albedo"]:
value_list.append(getattr(attn.processor, f"to_v{token}")(encoder_hidden_states))
value = torch.cat(value_list, dim=-1)
return query, key, value
# Core processing
hidden_states, residual, input_ndim, shape_info, batch_size, heads, head_dim = AttnCore.process_attention_base(
attn, hidden_states, encoder_hidden_states, attention_mask, temb, get_qkv_fn=get_qkv
)
# Split and process each PBR setting output
hidden_states_list = torch.split(hidden_states, head_dim, dim=-1)
output_hidden_states_list = []
for i, hs in enumerate(hidden_states_list):
hs = hs.transpose(1, 2).reshape(batch_size, -1, heads * head_dim).to(hs.dtype)
token_suffix = "_" + self.pbr_settings[i] if self.pbr_settings[i] != "albedo" else ""
target = attn if self.pbr_settings[i] == "albedo" else attn.processor
hs = AttnUtils.finalize_output(
hs, input_ndim, shape_info, attn, residual, getattr(target, f"to_out{token_suffix}")
)
output_hidden_states_list.append(hs)
return torch.stack(output_hidden_states_list, dim=1)
@@ -0,0 +1,622 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
# import ipdb
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import pytorch_lightning as pl
from tqdm import tqdm
from torchvision.transforms import v2
from torchvision.utils import make_grid, save_image
from einops import rearrange
from diffusers import (
DiffusionPipeline,
EulerAncestralDiscreteScheduler,
DDPMScheduler,
UNet2DConditionModel,
ControlNetModel,
)
from .modules import Dino_v2, UNet2p5DConditionModel
import math
def extract_into_tensor(a, t, x_shape):
b, *_ = t.shape
out = a.gather(-1, t)
return out.reshape(b, *((1,) * (len(x_shape) - 1)))
class HunyuanPaint(pl.LightningModule):
def __init__(
self,
stable_diffusion_config,
control_net_config=None,
num_view=6,
view_size=320,
drop_cond_prob=0.1,
with_normal_map=None,
with_position_map=None,
pbr_settings=["albedo", "mr"],
**kwargs,
):
"""Initializes the HunyuanPaint Lightning Module.
Args:
stable_diffusion_config: Configuration for loading the Stable Diffusion pipeline
control_net_config: Configuration for ControlNet (optional)
num_view: Number of views to process
view_size: Size of input views (height/width)
drop_cond_prob: Probability of dropping conditioning input during training
with_normal_map: Flag indicating whether normal maps are used
with_position_map: Flag indicating whether position maps are used
pbr_settings: List of PBR materials to generate (e.g., albedo, metallic-roughness)
**kwargs: Additional keyword arguments
"""
super(HunyuanPaint, self).__init__()
self.num_view = num_view
self.view_size = view_size
self.drop_cond_prob = drop_cond_prob
self.pbr_settings = pbr_settings
# init modules
pipeline = DiffusionPipeline.from_pretrained(**stable_diffusion_config)
pipeline.set_pbr_settings(self.pbr_settings)
pipeline.scheduler = EulerAncestralDiscreteScheduler.from_config(
pipeline.scheduler.config, timestep_spacing="trailing"
)
self.with_normal_map = with_normal_map
self.with_position_map = with_position_map
self.pipeline = pipeline
self.pipeline.vae.use_slicing = True
train_sched = DDPMScheduler.from_config(self.pipeline.scheduler.config)
if isinstance(self.pipeline.unet, UNet2DConditionModel):
self.pipeline.unet = UNet2p5DConditionModel(
self.pipeline.unet, train_sched, self.pipeline.scheduler, self.pbr_settings
)
self.train_scheduler = train_sched # use ddpm scheduler during training
self.register_schedule()
pipeline.set_learned_parameters()
if control_net_config is not None:
pipeline.unet = pipeline.unet.bfloat16().requires_grad_(control_net_config.train_unet)
self.pipeline.add_controlnet(
ControlNetModel.from_pretrained(control_net_config.pretrained_model_name_or_path),
conditioning_scale=0.75,
)
self.unet = pipeline.unet
self.pipeline.set_progress_bar_config(disable=True)
self.pipeline.vae = self.pipeline.vae.bfloat16()
self.pipeline.text_encoder = self.pipeline.text_encoder.bfloat16()
if self.unet.use_dino:
self.dino_v2 = Dino_v2("facebook/dinov2-giant")
self.dino_v2 = self.dino_v2.bfloat16()
self.validation_step_outputs = []
def register_schedule(self):
self.num_timesteps = self.train_scheduler.config.num_train_timesteps
betas = self.train_scheduler.betas.detach().cpu()
alphas = 1.0 - betas
alphas_cumprod = torch.cumprod(alphas, dim=0)
alphas_cumprod_prev = torch.cat([torch.ones(1, dtype=torch.float64), alphas_cumprod[:-1]], 0)
self.register_buffer("betas", betas.float())
self.register_buffer("alphas_cumprod", alphas_cumprod.float())
self.register_buffer("alphas_cumprod_prev", alphas_cumprod_prev.float())
# calculations for diffusion q(x_t | x_{t-1}) and others
self.register_buffer("sqrt_alphas_cumprod", torch.sqrt(alphas_cumprod).float())
self.register_buffer("sqrt_one_minus_alphas_cumprod", torch.sqrt(1 - alphas_cumprod).float())
self.register_buffer("sqrt_recip_alphas_cumprod", torch.sqrt(1.0 / alphas_cumprod).float())
self.register_buffer("sqrt_recipm1_alphas_cumprod", torch.sqrt(1.0 / alphas_cumprod - 1).float())
def on_fit_start(self):
device = torch.device(f"cuda:{self.local_rank}")
self.pipeline.to(device)
if self.global_rank == 0:
os.makedirs(os.path.join(self.logdir, "images_val"), exist_ok=True)
def prepare_batch_data(self, batch):
"""Preprocesses a batch of input data for training/inference.
Args:
batch: Raw input batch dictionary
Returns:
tuple: Contains:
- cond_imgs: Primary conditioning images (B, 1, C, H, W)
- cond_imgs_another: Secondary conditioning images (B, 1, C, H, W)
- target_imgs: Dictionary of target PBR images resized and clamped
- images_normal: Preprocessed normal maps (if available)
- images_position: Preprocessed position maps (if available)
"""
images_cond = batch["images_cond"].to(self.device) # (B, M, C, H, W), where M is the number of reference images
cond_imgs, cond_imgs_another = images_cond[:, 0:1, ...], images_cond[:, 1:2, ...]
cond_size = self.view_size
cond_imgs = v2.functional.resize(cond_imgs, cond_size, interpolation=3, antialias=True).clamp(0, 1)
cond_imgs_another = v2.functional.resize(cond_imgs_another, cond_size, interpolation=3, antialias=True).clamp(
0, 1
)
target_imgs = {}
for pbr_token in self.pbr_settings:
target_imgs[pbr_token] = batch[f"images_{pbr_token}"].to(self.device)
target_imgs[pbr_token] = v2.functional.resize(
target_imgs[pbr_token], self.view_size, interpolation=3, antialias=True
).clamp(0, 1)
images_normal = None
if "images_normal" in batch:
images_normal = batch["images_normal"] # (B, N, C, H, W)
images_normal = v2.functional.resize(images_normal, self.view_size, interpolation=3, antialias=True).clamp(
0, 1
)
images_normal = [images_normal]
images_position = None
if "images_position" in batch:
images_position = batch["images_position"] # (B, N, C, H, W)
images_position = v2.functional.resize(
images_position, self.view_size, interpolation=3, antialias=True
).clamp(0, 1)
images_position = [images_position]
return cond_imgs, cond_imgs_another, target_imgs, images_normal, images_position
@torch.no_grad()
def forward_text_encoder(self, prompts):
device = next(self.pipeline.vae.parameters()).device
text_embeds = self.pipeline.encode_prompt(prompts, device, 1, False)[0]
return text_embeds
@torch.no_grad()
def encode_images(self, images):
"""Encodes input images into latent representations using the VAE.
Handles both standard input (B, N, C, H, W) and PBR input (B, N_pbrs, N, C, H, W)
Maintains original batch structure in output latents.
Args:
images: Input images tensor
Returns:
torch.Tensor: Latent representations with original batch dimensions preserved
"""
B = images.shape[0]
image_ndims = images.ndim
if image_ndims != 5:
N_pbrs, N = images.shape[1:3]
images = (
rearrange(images, "b n c h w -> (b n) c h w")
if image_ndims == 5
else rearrange(images, "b n_pbrs n c h w -> (b n_pbrs n) c h w")
)
dtype = next(self.pipeline.vae.parameters()).dtype
images = (images - 0.5) * 2.0
posterior = self.pipeline.vae.encode(images.to(dtype)).latent_dist
latents = posterior.sample() * self.pipeline.vae.config.scaling_factor
latents = (
rearrange(latents, "(b n) c h w -> b n c h w", b=B)
if image_ndims == 5
else rearrange(latents, "(b n_pbrs n) c h w -> b n_pbrs n c h w", b=B, n_pbrs=N_pbrs)
)
return latents
def forward_unet(self, latents, t, **cached_condition):
"""Runs the UNet model to predict noise/latent residuals.
Args:
latents: Noisy latent representations (B, C, H, W)
t: Timestep tensor (B,)
**cached_condition: Dictionary of conditioning inputs (text embeds, reference images, etc)
Returns:
torch.Tensor: UNet output (predicted noise or velocity)
"""
dtype = next(self.unet.parameters()).dtype
latents = latents.to(dtype)
shading_embeds = cached_condition["shading_embeds"]
pred_noise = self.pipeline.unet(latents, t, encoder_hidden_states=shading_embeds, **cached_condition)
return pred_noise[0]
def predict_start_from_z_and_v(self, x_t, t, v):
"""
Predicts clean image (x0) from noisy latents (x_t) and
velocity prediction (v) using the v-prediction formula.
Args:
x_t: Noisy latents at timestep t
t: Current timestep
v: Predicted velocity (v) from UNet
Returns:
torch.Tensor: Predicted clean image (x0)
"""
return (
extract_into_tensor(self.sqrt_alphas_cumprod, t, x_t.shape) * x_t
- extract_into_tensor(self.sqrt_one_minus_alphas_cumprod, t, x_t.shape) * v
)
def get_v(self, x, noise, t):
"""Computes the target velocity (v) for v-prediction training.
Args:
x: Clean latents (x0)
noise: Added noise
t: Current timestep
Returns:
torch.Tensor: Target velocity
"""
return (
extract_into_tensor(self.sqrt_alphas_cumprod, t, x.shape) * noise
- extract_into_tensor(self.sqrt_one_minus_alphas_cumprod, t, x.shape) * x
)
def training_step(self, batch, batch_idx):
"""Performs a single training step with both conditioning paths.
Implements:
1. Dual-conditioning path training (main ref + secondary ref)
2. Velocity-prediction with consistency loss
3. Conditional dropout for robust learning
4. PBR-specific losses (albedo/metallic-roughness)
Args:
batch: Input batch from dataloader
batch_idx: Index of current batch
Returns:
torch.Tensor: Combined loss value
"""
cond_imgs, cond_imgs_another, target_imgs, normal_imgs, position_imgs = self.prepare_batch_data(batch)
B, N_ref = cond_imgs.shape[:2]
_, N_gen, _, H, W = target_imgs["albedo"].shape
N_pbrs = len(self.pbr_settings)
t = torch.randint(0, self.num_timesteps, size=(B,)).long().to(self.device)
t = t.unsqueeze(-1).repeat(1, N_pbrs, N_gen)
t = rearrange(t, "b n_pbrs n -> (b n_pbrs n)")
all_target_pbrs = []
for pbr_token in self.pbr_settings:
all_target_pbrs.append(target_imgs[pbr_token])
all_target_pbrs = torch.stack(all_target_pbrs, dim=0).transpose(1, 0)
gen_latents = self.encode_images(all_target_pbrs) #! B, N_pbrs N C H W
ref_latents = self.encode_images(cond_imgs) #! B, M, C, H, W
ref_latents_another = self.encode_images(cond_imgs_another) #! B, M, C, H, W
all_shading_tokens = []
for token in self.pbr_settings:
if token in ["albedo", "mr"]:
all_shading_tokens.append(
getattr(self.unet, f"learned_text_clip_{token}").unsqueeze(dim=0).repeat(B, 1, 1)
)
shading_embeds = torch.stack(all_shading_tokens, dim=1)
if self.unet.use_dino:
dino_hidden_states = self.dino_v2(cond_imgs[:, :1, ...])
dino_hidden_states_another = self.dino_v2(cond_imgs_another[:, :1, ...])
gen_latents = rearrange(gen_latents, "b n_pbrs n c h w -> (b n_pbrs n) c h w")
noise = torch.randn_like(gen_latents).to(self.device)
latents_noisy = self.train_scheduler.add_noise(gen_latents, noise, t).to(self.device)
latents_noisy = rearrange(latents_noisy, "(b n_pbrs n) c h w -> b n_pbrs n c h w", b=B, n_pbrs=N_pbrs)
cached_condition = {}
if normal_imgs is not None:
normal_embeds = self.encode_images(normal_imgs[0])
cached_condition["embeds_normal"] = normal_embeds #! B, N, C, H, W
if position_imgs is not None:
position_embeds = self.encode_images(position_imgs[0])
cached_condition["embeds_position"] = position_embeds #! B, N, C, H, W
cached_condition["position_maps"] = position_imgs[0] #! B, N, C, H, W
for b in range(B):
prob = np.random.rand()
if prob < self.drop_cond_prob:
if "normal_imgs" in cached_condition:
cached_condition["embeds_normal"][b, ...] = torch.zeros_like(
cached_condition["embeds_normal"][b, ...]
)
if "position_imgs" in cached_condition:
cached_condition["embeds_position"][b, ...] = torch.zeros_like(
cached_condition["embeds_position"][b, ...]
)
prob = np.random.rand()
if prob < self.drop_cond_prob:
if "position_maps" in cached_condition:
cached_condition["position_maps"][b, ...] = torch.zeros_like(
cached_condition["position_maps"][b, ...]
)
prob = np.random.rand()
if prob < self.drop_cond_prob:
dino_hidden_states[b, ...] = torch.zeros_like(dino_hidden_states[b, ...])
prob = np.random.rand()
if prob < self.drop_cond_prob:
dino_hidden_states_another[b, ...] = torch.zeros_like(dino_hidden_states_another[b, ...])
# MVA & Ref Attention
prob = np.random.rand()
cached_condition["mva_scale"] = 1.0
cached_condition["ref_scale"] = 1.0
if prob < self.drop_cond_prob:
cached_condition["mva_scale"] = 0.0
cached_condition["ref_scale"] = 0.0
elif prob > 1.0 - self.drop_cond_prob:
prob = np.random.rand()
if prob < 0.5:
cached_condition["mva_scale"] = 0.0
else:
cached_condition["ref_scale"] = 0.0
else:
pass
if self.train_scheduler.config.prediction_type == "v_prediction":
cached_condition["shading_embeds"] = shading_embeds
cached_condition["ref_latents"] = ref_latents
cached_condition["dino_hidden_states"] = dino_hidden_states
v_pred = self.forward_unet(latents_noisy, t, **cached_condition)
v_pred_albedo, v_pred_mr = torch.split(
rearrange(
v_pred, "(b n_pbr n) c h w -> b n_pbr n c h w", n_pbr=len(self.pbr_settings), n=self.num_view
),
1,
dim=1,
)
v_target = self.get_v(gen_latents, noise, t)
v_target_albedo, v_target_mr = torch.split(
rearrange(
v_target, "(b n_pbr n) c h w -> b n_pbr n c h w", n_pbr=len(self.pbr_settings), n=self.num_view
),
1,
dim=1,
)
albedo_loss_1, _ = self.compute_loss(v_pred_albedo, v_target_albedo)
mr_loss_1, _ = self.compute_loss(v_pred_mr, v_target_mr)
cached_condition["ref_latents"] = ref_latents_another
cached_condition["dino_hidden_states"] = dino_hidden_states_another
v_pred_another = self.forward_unet(latents_noisy, t, **cached_condition)
v_pred_another_albedo, v_pred_another_mr = torch.split(
rearrange(
v_pred_another,
"(b n_pbr n) c h w -> b n_pbr n c h w",
n_pbr=len(self.pbr_settings),
n=self.num_view,
),
1,
dim=1,
)
albedo_loss_2, _ = self.compute_loss(v_pred_another_albedo, v_target_albedo)
mr_loss_2, _ = self.compute_loss(v_pred_another_mr, v_target_mr)
consistency_loss, _ = self.compute_loss(v_pred_another, v_pred)
albedo_loss = (albedo_loss_1 + albedo_loss_2) * 0.5
mr_loss = (mr_loss_1 + mr_loss_2) * 0.5
log_loss_dict = {}
log_loss_dict.update({f"train/albedo_loss": albedo_loss})
log_loss_dict.update({f"train/mr_loss": mr_loss})
log_loss_dict.update({f"train/cons_loss": consistency_loss})
loss_dict = log_loss_dict
elif self.train_scheduler.config.prediction_type == "epsilon":
e_pred = self.forward_unet(latents_noisy, t, **cached_condition)
loss, loss_dict = self.compute_loss(e_pred, noise)
else:
raise f"No {self.train_scheduler.config.prediction_type}"
# logging
self.log_dict(loss_dict, prog_bar=True, logger=True, on_step=True, on_epoch=True)
self.log("global_step", self.global_step, prog_bar=True, logger=True, on_step=True, on_epoch=False)
lr = self.optimizers().param_groups[0]["lr"]
self.log("lr_abs", lr, prog_bar=True, logger=True, on_step=True, on_epoch=False)
return 0.85 * (albedo_loss + mr_loss) + 0.15 * consistency_loss
def compute_loss(self, noise_pred, noise_gt):
loss = F.mse_loss(noise_pred, noise_gt)
prefix = "train"
loss_dict = {}
loss_dict.update({f"{prefix}/loss": loss})
return loss, loss_dict
@torch.no_grad()
def validation_step(self, batch, batch_idx):
"""Performs validation on a single batch.
Generates predicted images using:
1. Reference conditioning images
2. Optional normal/position maps
3. Frozen DINO features (if enabled)
4. Text prompt conditioning
Compares predictions against ground truth targets and prepares visualization.
Stores results for epoch-level aggregation.
Args:
batch: Input batch from validation dataloader
batch_idx: Index of current batch
"""
# [Validation image generation and comparison logic...]
# Key steps:
# 1. Preprocess conditioning images to PIL format
# 2. Set up conditioning inputs (normal maps, position maps, DINO features)
# 3. Run pipeline inference with fixed prompt ("high quality")
# 4. Decode latent outputs to image space
# 5. Arrange predictions and ground truths for visualization
cond_imgs_tensor, _, target_imgs, normal_imgs, position_imgs = self.prepare_batch_data(batch)
resolution = self.view_size
image_pils = []
for i in range(cond_imgs_tensor.shape[0]):
image_pils.append([])
for j in range(cond_imgs_tensor.shape[1]):
image_pils[-1].append(v2.functional.to_pil_image(cond_imgs_tensor[i, j, ...]))
outputs, gts = [], []
for idx in range(len(image_pils)):
cond_imgs = image_pils[idx]
cached_condition = dict(num_in_batch=self.num_view, N_pbrs=len(self.pbr_settings))
if normal_imgs is not None:
cached_condition["images_normal"] = normal_imgs[0][idx, ...].unsqueeze(0)
if position_imgs is not None:
cached_condition["images_position"] = position_imgs[0][idx, ...].unsqueeze(0)
if self.pipeline.unet.use_dino:
dino_hidden_states = self.dino_v2([cond_imgs][0])
cached_condition["dino_hidden_states"] = dino_hidden_states
latent = self.pipeline(
cond_imgs,
prompt="high quality",
num_inference_steps=30,
output_type="latent",
height=resolution,
width=resolution,
**cached_condition,
).images
image = self.pipeline.vae.decode(latent / self.pipeline.vae.config.scaling_factor, return_dict=False)[
0
] # [-1, 1]
image = (image * 0.5 + 0.5).clamp(0, 1)
image = rearrange(
image, "(b n_pbr n) c h w -> b n_pbr n c h w", n_pbr=len(self.pbr_settings), n=self.num_view
)
image = torch.cat((torch.ones_like(image[:, :, :1, ...]) * 0.5, image), dim=2)
image = rearrange(image, "b n_pbr n c h w -> (b n_pbr n) c h w")
image = rearrange(
image,
"(b n_pbr n) c h w -> b c (n_pbr h) (n w)",
b=1,
n_pbr=len(self.pbr_settings),
n=self.num_view + 1,
)
outputs.append(image)
all_target_pbrs = []
for pbr_token in self.pbr_settings:
all_target_pbrs.append(target_imgs[pbr_token])
all_target_pbrs = torch.stack(all_target_pbrs, dim=0).transpose(1, 0)
all_target_pbrs = torch.cat(
(cond_imgs_tensor.unsqueeze(1).repeat(1, len(self.pbr_settings), 1, 1, 1, 1), all_target_pbrs), dim=2
)
all_target_pbrs = rearrange(all_target_pbrs, "b n_pbrs n c h w -> b c (n_pbrs h) (n w)")
gts = all_target_pbrs
outputs = torch.cat(outputs, dim=0).to(self.device)
images = torch.cat([gts, outputs], dim=-2)
self.validation_step_outputs.append(images)
@torch.no_grad()
def on_validation_epoch_end(self):
"""Aggregates validation results at epoch end.
Gathers outputs from all GPUs (if distributed training),
creates a unified visualization grid, and saves to disk.
Only rank 0 process performs saving.
"""
# [Result aggregation and visualization...]
# Key steps:
# 1. Gather validation outputs from all processes
# 2. Create image grid combining ground truths and predictions
# 3. Save visualization with step-numbered filename
# 4. Clear memory for next validation cycle
images = torch.cat(self.validation_step_outputs, dim=0)
all_images = self.all_gather(images)
all_images = rearrange(all_images, "r b c h w -> (r b) c h w")
if self.global_rank == 0:
grid = make_grid(all_images, nrow=8, normalize=True, value_range=(0, 1))
save_image(grid, os.path.join(self.logdir, "images_val", f"val_{self.global_step:07d}.png"))
self.validation_step_outputs.clear() # free memory
def configure_optimizers(self):
lr = self.learning_rate
optimizer = torch.optim.AdamW(self.unet.parameters(), lr=lr)
def lr_lambda(step):
warm_up_step = 1000
T_step = 9000
gamma = 0.9
min_lr = 0.1 if step >= warm_up_step else 0.0
max_lr = 1.0
normalized_step = step % (warm_up_step + T_step)
current_max_lr = max_lr * gamma ** (step // (warm_up_step + T_step))
if current_max_lr < min_lr:
current_max_lr = min_lr
if normalized_step < warm_up_step:
lr_step = min_lr + (normalized_step / warm_up_step) * (current_max_lr - min_lr)
else:
step_wc_wp = normalized_step - warm_up_step
ratio = step_wc_wp / T_step
lr_step = min_lr + 0.5 * (current_max_lr - min_lr) * (1 + math.cos(math.pi * ratio))
return lr_step
lr_scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda)
lr_scheduler_config = {
"scheduler": lr_scheduler,
"interval": "step",
"frequency": 1,
"monitor": "val_loss",
"strict": False,
"name": None,
}
return {"optimizer": optimizer, "lr_scheduler": lr_scheduler_config}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,6 @@
include README.md
include pyproject.toml
recursive-include mesh_inpaint_processor *.pyd
recursive-include mesh_inpaint_processor *.so
recursive-include mesh_inpaint_processor *.py
recursive-include mesh_inpaint_processor *.txt
@@ -0,0 +1,11 @@
# Try to import C++ version first, fallback to Python version
try:
from .mesh_inpaint_processor import meshVerticeInpaint
print("[INFO] Using compiled C++ mesh_inpaint_processor (fast)")
except ImportError:
try:
from .mesh_inpaint_processor_fallback import meshVerticeInpaint
print("[WARNING] Using Python fallback mesh_inpaint_processor (slower)")
print("[INFO] To use faster C++ version, run: compile_mesh_painter.bat")
except ImportError:
print("[ERROR] Neither C++ nor Python mesh_inpaint_processor found!")
@@ -0,0 +1,84 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import numpy as np
def meshVerticeInpaint_smooth(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx):
texture_height, texture_width, texture_channel = texture.shape
vtx_num = vtx_pos.shape[0]
vtx_mask = np.zeros(vtx_num, dtype=np.float32)
vtx_color = [np.zeros(texture_channel, dtype=np.float32) for _ in range(vtx_num)]
uncolored_vtxs = []
G = [[] for _ in range(vtx_num)]
for i in range(uv_idx.shape[0]):
for k in range(3):
vtx_uv_idx = uv_idx[i, k]
vtx_idx = pos_idx[i, k]
uv_v = int(round(vtx_uv[vtx_uv_idx, 0] * (texture_width - 1)))
uv_u = int(round((1.0 - vtx_uv[vtx_uv_idx, 1]) * (texture_height - 1)))
if mask[uv_u, uv_v] > 0:
vtx_mask[vtx_idx] = 1.0
vtx_color[vtx_idx] = texture[uv_u, uv_v]
else:
uncolored_vtxs.append(vtx_idx)
G[pos_idx[i, k]].append(pos_idx[i, (k + 1) % 3])
smooth_count = 2
last_uncolored_vtx_count = 0
while smooth_count > 0:
uncolored_vtx_count = 0
for vtx_idx in uncolored_vtxs:
sum_color = np.zeros(texture_channel, dtype=np.float32)
total_weight = 0.0
vtx_0 = vtx_pos[vtx_idx]
for connected_idx in G[vtx_idx]:
if vtx_mask[connected_idx] > 0:
vtx1 = vtx_pos[connected_idx]
dist = np.sqrt(np.sum((vtx_0 - vtx1) ** 2))
dist_weight = 1.0 / max(dist, 1e-4)
dist_weight *= dist_weight
sum_color += vtx_color[connected_idx] * dist_weight
total_weight += dist_weight
if total_weight > 0:
vtx_color[vtx_idx] = sum_color / total_weight
vtx_mask[vtx_idx] = 1.0
else:
uncolored_vtx_count += 1
if last_uncolored_vtx_count == uncolored_vtx_count:
smooth_count -= 1
else:
smooth_count += 1
last_uncolored_vtx_count = uncolored_vtx_count
new_texture = texture.copy()
new_mask = mask.copy()
for face_idx in range(uv_idx.shape[0]):
for k in range(3):
vtx_uv_idx = uv_idx[face_idx, k]
vtx_idx = pos_idx[face_idx, k]
if vtx_mask[vtx_idx] == 1.0:
uv_v = int(round(vtx_uv[vtx_uv_idx, 0] * (texture_width - 1)))
uv_u = int(round((1.0 - vtx_uv[vtx_uv_idx, 1]) * (texture_height - 1)))
new_texture[uv_u, uv_v] = vtx_color[vtx_idx]
new_mask[uv_u, uv_v] = 255
return new_texture, new_mask
def meshVerticeInpaint(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx, method="smooth"):
if method == "smooth":
return meshVerticeInpaint_smooth(texture, mask, vtx_pos, vtx_uv, pos_idx, uv_idx)
else:
raise ValueError("Invalid method. Use 'smooth' or 'forward'.")
@@ -0,0 +1,39 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools-scm[toml]>=6.2"]
build-backend = "setuptools.build_meta"
[project]
name = "mesh_inpaint_processor"
version = "1.0.0"
description = "Fast C++ mesh inpainting processor with Python fallback"
readme = "README.md"
requires-python = ">=3.8"
license = {text = "TENCENT HUNYUAN NON-COMMERCIAL LICENSE"}
authors = [
{name = "Hunyuan3D Team"},
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",
]
dependencies = [
"numpy>=1.19.0",
]
[project.urls]
Homepage = "https://github.com/Tencent/Hunyuan3D"
[tool.setuptools.packages.find]
where = ["."]
[tool.setuptools.package-data]
mesh_inpaint_processor = ["*.pyd", "*.so", "*.py"]
@@ -0,0 +1,47 @@
from setuptools import setup, find_packages
import platform
# Определяем платформу для правильного именования wheel
if platform.system() == "Windows":
plat_name = "win_amd64"
elif platform.system() == "Darwin":
plat_name = "macosx_10_9_x86_64"
else:
plat_name = "linux_x86_64"
setup(
name="mesh_inpaint_processor",
version="1.0.0",
description="Fast C++ mesh inpainting processor with Python fallback",
long_description="Компилированный модуль для быстрой обработки инпейнтинга мешей",
author="Hunyuan3D Team",
packages=find_packages(),
package_data={
'mesh_inpaint_processor': [
'*.pyd', # Включаем .pyd файлы
'*.so', # На случай Linux версии
'*.py' # Python fallback файлы
]
},
include_package_data=True,
python_requires=">=3.8",
install_requires=[
"numpy>=1.19.0",
],
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Operating System :: Microsoft :: Windows",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS",
],
# Указываем что это binary distribution
zip_safe=False,
has_ext_modules=lambda: True, # Указывает что есть нативные модули
)
+13
View File
@@ -0,0 +1,13 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
@@ -0,0 +1,13 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
@@ -0,0 +1,219 @@
#!/usr/bin/env python3
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
import cv2
import json
import random
import numpy as np
import torch
from torch.utils.data import Dataset
from PIL import Image, ImageOps, ImageChops
class BaseDataset(Dataset):
def __init__(self, json_path, num_view=4, image_size=512):
self.data = list()
self.num_view = num_view
self.image_size = image_size
if isinstance(json_path, str):
json_path = [json_path]
for jp in json_path:
with open(jp) as f:
self.data.extend(json.load(f))
print("============= length of dataset %d =============" % len(self.data))
def __len__(self):
return len(self.data)
def load_image(self, pil_img, color, image_size=None):
if image_size is None:
image_size = self.image_size
if isinstance(pil_img, str):
pil_img = Image.open(pil_img)
else:
pil_img = pil_img
if pil_img.mode == "L":
pil_img = pil_img.convert("RGB")
pil_img = pil_img.resize((image_size, image_size))
image = np.asarray(pil_img, dtype=np.float32) / 255.0
if image.shape[2] == 3:
image = image[:, :, :3]
alpha = np.ones_like(image)
else:
alpha = image[:, :, 3:]
image = image[:, :, :3] * alpha + color * (1 - alpha)
image = torch.from_numpy(image).permute(2, 0, 1).contiguous().float()
alpha = torch.from_numpy(alpha).permute(2, 0, 1).contiguous().float()
return image, alpha
def _apply_scaling(self, image, scale_factor, width, height, bg_color, scale_width=True):
"""Apply scaling to image with proper cropping or padding."""
if scale_width:
new_width = int(width * scale_factor)
new_height = height
else:
new_width = width
new_height = int(height * scale_factor)
image = image.resize((new_width, new_height), resample=Image.BILINEAR)
if scale_factor > 1.0:
# Crop to original size
left = (new_width - width) // 2
top = (new_height - height) // 2
image = image.crop((left, top, left + width, top + height))
else:
# Pad to original size
pad_width = (width - new_width) // 2
pad_height = (height - new_height) // 2
image = ImageOps.expand(
image,
(
pad_width,
pad_height,
width - new_width - pad_width,
height - new_height - pad_height,
),
fill=bg_color,
)
return image
def _apply_rotation(self, image, bg_color):
"""Apply random rotation to image."""
original_size = image.size
angle = random.uniform(-30, 30)
image = image.convert("RGBA")
rotated_image = image.rotate(angle, resample=Image.BILINEAR, expand=True)
# Create background with bg_color
background = Image.new("RGBA", rotated_image.size, (bg_color[0], bg_color[1], bg_color[2], 255))
background.paste(rotated_image, (0, 0), rotated_image)
image = background.convert("RGB")
# Crop to original size
left = (image.width - original_size[0]) // 2
top = (image.height - original_size[1]) // 2
right = left + original_size[0]
bottom = top + original_size[1]
return image.crop((left, top, right, bottom))
def _apply_translation(self, image, bg_color):
"""Apply random translation to image."""
max_dx = 0.1 * image.size[0]
max_dy = 0.1 * image.size[1]
dx = int(random.uniform(-max_dx, max_dx))
dy = int(random.uniform(-max_dy, max_dy))
image = ImageChops.offset(image, dx, dy)
# Fill edges
width, height = image.size
if dx > 0:
image.paste(bg_color, (0, 0, dx, height))
elif dx < 0:
image.paste(bg_color, (width + dx, 0, width, height))
if dy > 0:
image.paste(bg_color, (0, 0, width, dy))
elif dy < 0:
image.paste(bg_color, (0, height + dy, width, height))
return image
def _apply_perspective(self, image, bg_color):
"""Apply random perspective transformation to image."""
image_np = np.array(image)
height, width = image_np.shape[:2]
# Define original and new points
original_points = np.float32([[0, 0], [width, 0], [width, height], [0, height]])
perspective_scale = 0.2
new_points = np.float32(
[
[random.uniform(0, width * perspective_scale), random.uniform(0, height * perspective_scale)],
[random.uniform(width * (1 - perspective_scale), width), random.uniform(0, height * perspective_scale)],
[
random.uniform(width * (1 - perspective_scale), width),
random.uniform(height * (1 - perspective_scale), height),
],
[
random.uniform(0, width * perspective_scale),
random.uniform(height * (1 - perspective_scale), height),
],
]
)
matrix = cv2.getPerspectiveTransform(original_points, new_points)
image_np = cv2.warpPerspective(
image_np, matrix, (width, height), borderMode=cv2.BORDER_CONSTANT, borderValue=bg_color
)
return Image.fromarray(image_np)
def augment_image(
self,
image,
bg_color,
identity_prob=0.5,
rotate_prob=0.3,
scale_prob=0.5,
translate_prob=0.5,
perspective_prob=0.3,
):
if random.random() < identity_prob:
return image
# Convert torch tensors back to PIL images for augmentation
image = Image.fromarray((image.permute(1, 2, 0).cpu().numpy() * 255).astype(np.uint8))
bg_color = (int(bg_color[0] * 255), int(bg_color[1] * 255), int(bg_color[2] * 255))
# Random rotation
if random.random() < rotate_prob:
image = self._apply_rotation(image, bg_color)
# Random scaling
if random.random() < scale_prob:
width, height = image.size
scale_factor = random.uniform(0.8, 1.2)
if random.random() < 0.5:
# Scale both dimensions proportionally
image = self._apply_scaling(image, scale_factor, width, height, bg_color, scale_width=True)
image = self._apply_scaling(image, scale_factor, width, height, bg_color, scale_width=False)
else:
# Scale width then height independently
scale_factor_w = random.uniform(0.8, 1.2)
scale_factor_h = random.uniform(0.8, 1.2)
image = self._apply_scaling(image, scale_factor_w, width, height, bg_color, scale_width=True)
image = self._apply_scaling(image, scale_factor_h, width, height, bg_color, scale_width=False)
# Random translation
if random.random() < translate_prob:
image = self._apply_translation(image, bg_color)
# Random perspective
if random.random() < perspective_prob:
image = self._apply_perspective(image, bg_color)
# Convert back to torch tensors
image = image.convert("RGB")
image = np.asarray(image, dtype=np.float32) / 255.0
image = torch.from_numpy(image).permute(2, 0, 1).contiguous().float()
return image
@@ -0,0 +1,146 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
import time
import glob
import json
import random
import numpy as np
import torch
from .loader_util import BaseDataset
class TextureDataset(BaseDataset):
def __init__(
self, json_path, num_view=6, image_size=512, lighting_suffix_pool=["light_PL", "light_AL", "light_ENVMAP"]
):
self.data = list()
self.num_view = num_view
self.image_size = image_size
self.lighting_suffix_pool = lighting_suffix_pool
if isinstance(json_path, str):
json_path = [json_path]
for jp in json_path:
with open(jp) as f:
self.data.extend(json.load(f))
print("============= length of dataset %d =============" % len(self.data))
def __getitem__(self, index):
try_sleep_interval = 20
total_try_num = 100
cnt = try_sleep_interval * total_try_num
# try:
images_ref = list()
images_albedo = list()
images_mr = list()
images_normal = list()
images_position = list()
bg_white = [1.0, 1.0, 1.0]
bg_black = [0.0, 0.0, 0.0]
bg_gray = [127 / 255.0, 127 / 255.0, 127 / 255.0]
dirx = self.data[index]
condition_dict = {}
# 6view
fix_num_view = self.num_view
available_views = []
for ext in ["*_albedo.png", "*_albedo.jpg", "*_albedo.jpeg"]:
available_views.extend(glob.glob(os.path.join(dirx, "render_tex", ext)))
cond_images = (
glob.glob(os.path.join(dirx, "render_cond", "*.png"))
+ glob.glob(os.path.join(dirx, "render_cond", "*.jpg"))
+ glob.glob(os.path.join(dirx, "render_cond", "*.jpeg"))
)
# 确保有足够的样本
if len(available_views) < fix_num_view:
print(
f"Warning: Only {len(available_views)} views available, but {fix_num_view} requested."
"Using all available views."
)
images_gen = available_views
else:
images_gen = random.sample(available_views, fix_num_view)
if not cond_images:
raise ValueError(f"No condition images found in {os.path.join(dirx, 'render_cond')}")
ref_image_path = random.choice(cond_images)
light_suffix = None
for suffix in self.lighting_suffix_pool:
if suffix in ref_image_path:
light_suffix = suffix
break
if light_suffix is None:
raise ValueError(f"light suffix not found in {ref_image_path}")
ref_image_diff_light_path = random.choice(
[
ref_image_path.replace(light_suffix, tar_suffix)
for tar_suffix in self.lighting_suffix_pool
if tar_suffix != light_suffix
]
)
images_ref_paths = [ref_image_path, ref_image_diff_light_path]
# Data aug
bg_c_record = None
for i, image_ref in enumerate(images_ref_paths):
if random.random() < 0.6:
bg_c = bg_gray
else:
if random.random() < 0.5:
bg_c = bg_black
else:
bg_c = bg_white
if i == 0:
bg_c_record = bg_c
image, alpha = self.load_image(image_ref, bg_c_record)
image = self.augment_image(image, bg_c_record).float()
images_ref.append(image)
condition_dict["images_cond"] = torch.stack(images_ref, dim=0).float()
for i, image_gen in enumerate(images_gen):
images_albedo.append(self.augment_image(self.load_image(image_gen, bg_gray)[0], bg_gray))
images_mr.append(
self.augment_image(self.load_image(image_gen.replace("_albedo", "_mr"), bg_gray)[0], bg_gray)
)
images_normal.append(
self.augment_image(self.load_image(image_gen.replace("_albedo", "_normal"), bg_gray)[0], bg_gray)
)
images_position.append(
self.augment_image(self.load_image(image_gen.replace("_albedo", "_pos"), bg_gray)[0], bg_gray)
)
condition_dict["images_albedo"] = torch.stack(images_albedo, dim=0).float()
condition_dict["images_mr"] = torch.stack(images_mr, dim=0).float()
condition_dict["images_normal"] = torch.stack(images_normal, dim=0).float()
condition_dict["images_position"] = torch.stack(images_position, dim=0).float()
condition_dict["name"] = dirx # .replace('/', '_')
return condition_dict # (N, 3, H, W)
# except Exception as e:
# print(e, self.data[index])
# # exit()
if __name__ == "__main__":
dataset = TextureDataset(json_path=["../../../train_examples/examples.json"])
print("images_cond", dataset[0]["images_cond"].shape)
print("images_albedo", dataset[0]["images_albedo"].shape)
print("images_mr", dataset[0]["images_mr"].shape)
print("images_normal", dataset[0]["images_normal"].shape)
print("images_position", dataset[0]["images_position"].shape)
print("name", dataset[0]["name"])
@@ -0,0 +1,10 @@
+-----------------+----------------------------------+
| Key | Value |
+-----------------+----------------------------------+
| images_cond | torch.Size([2, 2, 3, 512, 512]) |
| images_albedo | torch.Size([2, 6, 3, 512, 512]) |
| images_mr | torch.Size([2, 6, 3, 512, 512]) |
| images_normal | torch.Size([2, 6, 3, 512, 512]) |
| images_position | torch.Size([2, 6, 3, 512, 512]) |
| caption | ['high quality', 'high quality'] |
+-----------------+----------------------------------+
@@ -0,0 +1,79 @@
#!/usr/bin/env python3
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import pytorch_lightning as pl
from torch.utils.data import Dataset, ConcatDataset, DataLoader
from torch.utils.data.distributed import DistributedSampler
class DataModuleFromConfig(pl.LightningDataModule):
def __init__(
self,
batch_size=8,
num_workers=4,
train=None,
validation=None,
test=None,
**kwargs,
):
super().__init__()
self.batch_size = batch_size
self.num_workers = num_workers
self.dataset_configs = dict()
if train is not None:
self.dataset_configs["train"] = train
if validation is not None:
self.dataset_configs["validation"] = validation
if test is not None:
self.dataset_configs["test"] = test
def setup(self, stage):
from src.utils.train_util import instantiate_from_config
if stage in ["fit"]:
dataset_dict = {}
for k in self.dataset_configs:
dataset_dict[k] = []
for loader in self.dataset_configs[k]:
dataset_dict[k].append(instantiate_from_config(loader))
self.datasets = dataset_dict
print(self.datasets)
else:
raise NotImplementedError
def train_dataloader(self):
datasets = ConcatDataset(self.datasets["train"])
sampler = DistributedSampler(datasets)
return DataLoader(
datasets,
batch_size=self.batch_size,
num_workers=self.num_workers,
shuffle=False,
sampler=sampler,
prefetch_factor=2,
pin_memory=True,
)
def val_dataloader(self):
datasets = ConcatDataset(self.datasets["validation"])
sampler = DistributedSampler(datasets)
return DataLoader(datasets, batch_size=4, num_workers=self.num_workers, shuffle=False, sampler=sampler)
def test_dataloader(self):
datasets = ConcatDataset(self.datasets["test"])
return DataLoader(datasets, batch_size=self.batch_size, num_workers=self.num_workers, shuffle=False)
@@ -0,0 +1,13 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
@@ -0,0 +1,40 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import importlib
def count_params(model, verbose=False):
total_params = sum(p.numel() for p in model.parameters())
if verbose:
print(f"{model.__class__.__name__} has {total_params*1.e-6:.2f} M params.")
return total_params
def instantiate_from_config(config):
if not "target" in config:
if config == "__is_first_stage__":
return None
elif config == "__is_unconditional__":
return None
raise KeyError("Expected key `target` to instantiate.")
return get_obj_from_str(config["target"])(**config.get("params", dict()))
def get_obj_from_str(string, reload=False):
module, cls = string.rsplit(".", 1)
if reload:
module_imp = importlib.import_module(module)
importlib.reload(module_imp)
return getattr(importlib.import_module(module, package=None), cls)
@@ -0,0 +1,219 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
import torch
import copy
import trimesh
import numpy as np
from PIL import Image
from typing import List
from .DifferentiableRenderer.MeshRender import MeshRender
import sys
current_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.append(current_dir)
from .utils.simplify_mesh_utils import remesh_mesh
from .utils.multiview_utils import multiviewDiffusionNet
from .utils.pipeline_utils import ViewProcessor
from .utils.image_super_utils import imageSuperNet
from .utils.uvwrap_utils import mesh_uv_wrap
from .DifferentiableRenderer.mesh_utils import convert_obj_to_glb
from .convert_utils import create_glb_with_pbr_materials
import warnings
warnings.filterwarnings("ignore")
from diffusers.utils import logging as diffusers_logging
diffusers_logging.set_verbosity(50)
class Hunyuan3DPaintConfig:
def __init__(self, max_num_view, resolution):
self.device = "cuda"
self.multiview_cfg_path = "cfgs/hunyuan-paint-pbr.yaml"
self.custom_pipeline = "hunyuanpaintpbr"
self.multiview_pretrained_path = "tencent/Hunyuan3D-2.1"
self.dino_ckpt_path = "facebook/dinov2-giant"
self.realesrgan_ckpt_path = "/home/ubuntu/oldrigger/AutoRigger/auto_rigger/models/ComfyUI/custom_nodes/ComfyUI-3D-Pack/Checkpoints/Diffusers/tencent/Hunyuan3D-2.1/RealESRGAN_x4plus.pth"
self.raster_mode = "cr"
self.bake_mode = "back_sample"
self.render_size = 1024 * 2
self.texture_size = 1024 * 4
self.max_selected_view_num = max_num_view
self.resolution = resolution
self.bake_exp = 4
self.merge_method = "fast"
# view selection
self.candidate_camera_azims = [0, 90, 180, 270, 0, 180]
self.candidate_camera_elevs = [0, 0, 0, 0, 90, -90]
self.candidate_view_weights = [1, 0.1, 0.5, 0.1, 0.05, 0.05]
for azim in range(0, 360, 30):
self.candidate_camera_azims.append(azim)
self.candidate_camera_elevs.append(20)
self.candidate_view_weights.append(0.01)
self.candidate_camera_azims.append(azim)
self.candidate_camera_elevs.append(-20)
self.candidate_view_weights.append(0.01)
class Hunyuan3DPaintPipeline:
def __init__(self, config=None) -> None:
self.config = config if config is not None else Hunyuan3DPaintConfig()
self.models = {}
self.stats_logs = {}
self.render = MeshRender(
default_resolution=self.config.render_size,
texture_size=self.config.texture_size,
bake_mode=self.config.bake_mode,
raster_mode=self.config.raster_mode,
)
self.view_processor = ViewProcessor(self.config, self.render)
self.load_models()
def load_models(self):
torch.cuda.empty_cache()
self.models["super_model"] = imageSuperNet(self.config)
self.models["multiview_model"] = multiviewDiffusionNet(self.config)
print("Models Loaded.")
@torch.no_grad()
def __call__(self, mesh_path=None, image_path=None, output_mesh_path=None, use_remesh=True, save_glb=True):
"""Generate texture for 3D mesh using multiview diffusion"""
# Ensure image_prompt is a list
if isinstance(image_path, str):
image_prompt = Image.open(image_path)
elif isinstance(image_path, Image.Image):
image_prompt = image_path
if not isinstance(image_prompt, List):
image_prompt = [image_prompt]
else:
image_prompt = image_path
# Process mesh
path = os.path.dirname(mesh_path)
if use_remesh:
processed_mesh_path = os.path.join(path, "white_mesh_remesh.obj")
remesh_mesh(mesh_path, processed_mesh_path)
else:
processed_mesh_path = mesh_path
# Output path
if output_mesh_path is None:
output_mesh_path = os.path.join(path, f"textured_mesh.obj")
# Load mesh
mesh = trimesh.load(processed_mesh_path)
mesh = mesh_uv_wrap(mesh)
self.render.load_mesh(mesh=mesh)
########### View Selection #########
selected_camera_elevs, selected_camera_azims, selected_view_weights = self.view_processor.bake_view_selection(
self.config.candidate_camera_elevs,
self.config.candidate_camera_azims,
self.config.candidate_view_weights,
self.config.max_selected_view_num,
)
normal_maps = self.view_processor.render_normal_multiview(
selected_camera_elevs, selected_camera_azims, use_abs_coor=True
)
position_maps = self.view_processor.render_position_multiview(selected_camera_elevs, selected_camera_azims)
########## Style ###########
image_caption = "high quality"
image_style = []
for image in image_prompt:
image = image.resize((512, 512))
if image.mode == "RGBA":
white_bg = Image.new("RGB", image.size, (255, 255, 255))
white_bg.paste(image, mask=image.getchannel("A"))
image = white_bg
image_style.append(image)
image_style = [image.convert("RGB") for image in image_style]
########### Multiview ##########
multiviews_pbr = self.models["multiview_model"](
image_style,
normal_maps + position_maps,
prompt=image_caption,
custom_view_size=self.config.resolution,
resize_input=True,
)
########### Enhance ##########
enhance_images = {}
enhance_images["albedo"] = copy.deepcopy(multiviews_pbr["albedo"])
enhance_images["mr"] = copy.deepcopy(multiviews_pbr["mr"])
for i in range(len(enhance_images["albedo"])):
enhance_images["albedo"][i] = self.models["super_model"](enhance_images["albedo"][i])
enhance_images["mr"][i] = self.models["super_model"](enhance_images["mr"][i])
########### Bake ##########
for i in range(len(enhance_images)):
enhance_images["albedo"][i] = enhance_images["albedo"][i].resize(
(self.config.render_size, self.config.render_size)
)
enhance_images["mr"][i] = enhance_images["mr"][i].resize((self.config.render_size, self.config.render_size))
texture, mask = self.view_processor.bake_from_multiview(
enhance_images["albedo"], selected_camera_elevs, selected_camera_azims, selected_view_weights
)
mask_np = (mask.squeeze(-1).cpu().numpy() * 255).astype(np.uint8)
texture_mr, mask_mr = self.view_processor.bake_from_multiview(
enhance_images["mr"], selected_camera_elevs, selected_camera_azims, selected_view_weights
)
mask_mr_np = (mask_mr.squeeze(-1).cpu().numpy() * 255).astype(np.uint8)
########## inpaint ###########
texture = self.view_processor.texture_inpaint(texture, mask_np)
self.render.set_texture(texture, force_set=True)
if "mr" in enhance_images:
texture_mr = self.view_processor.texture_inpaint(texture_mr, mask_mr_np)
self.render.set_texture_mr(texture_mr)
self.render.save_mesh(output_mesh_path, downsample=True)
if save_glb:
output_glb_path = output_mesh_path.replace(".obj", ".glb")
base_path = os.path.splitext(output_mesh_path)[0]
textures_dict = {
'albedo': f"{base_path}.jpg",
}
metallic_path = f"{base_path}_metallic.jpg"
roughness_path = f"{base_path}_roughness.jpg"
normal_path = f"{base_path}_normal.jpg"
if os.path.exists(metallic_path):
textures_dict['metallic'] = metallic_path
if os.path.exists(roughness_path):
textures_dict['roughness'] = roughness_path
if os.path.exists(normal_path):
textures_dict['normal'] = normal_path
try:
create_glb_with_pbr_materials(output_mesh_path, textures_dict, output_glb_path)
print(f"Created GLB with full PBR materials: {output_glb_path}")
except Exception as e:
print(f"Warning: Failed to create GLB with PBR materials: {e}")
convert_obj_to_glb(output_mesh_path, output_glb_path)
print(f"Created GLB with basic conversion: {output_glb_path}")
return output_mesh_path
@@ -0,0 +1,401 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import torch
import os, sys
import argparse
import shutil
import subprocess
from omegaconf import OmegaConf
from pytorch_lightning import seed_everything
from pytorch_lightning.trainer import Trainer
from pytorch_lightning.strategies import DDPStrategy
from pytorch_lightning.callbacks import Callback
from pytorch_lightning.utilities import rank_zero_only, rank_zero_warn
from src.utils.train_util import instantiate_from_config
import warnings
warnings.filterwarnings("ignore")
from diffusers.utils import logging as diffusers_logging
diffusers_logging.set_verbosity(50)
@rank_zero_only
def rank_zero_print(*args):
print(*args)
def get_parser(**parser_kwargs):
def str2bool(v):
if isinstance(v, bool):
return v
if v.lower() in ("yes", "true", "t", "y", "1"):
return True
elif v.lower() in ("no", "false", "f", "n", "0"):
return False
else:
raise argparse.ArgumentTypeError("Boolean value expected.")
parser = argparse.ArgumentParser(**parser_kwargs)
parser.add_argument(
"-r",
"--resume",
type=str,
default=None,
help="resume from checkpoint",
)
parser.add_argument(
"--resume_weights_only",
action="store_true",
help="only resume model weights",
)
parser.add_argument(
"-b",
"--base",
type=str,
default="base_config.yaml",
help="path to base configs",
)
parser.add_argument(
"-n",
"--name",
type=str,
default="",
help="experiment name",
)
parser.add_argument(
"--num_nodes",
type=int,
default=1,
help="number of nodes to use",
)
parser.add_argument(
"--gpus",
type=str,
default="0,",
help="gpu ids to use",
)
parser.add_argument(
"-s",
"--seed",
type=int,
default=42,
help="seed for seed_everything",
)
parser.add_argument(
"-l",
"--logdir",
type=str,
default="logs",
help="directory for logging data",
)
return parser
class SetupCallback(Callback):
def __init__(self, resume, logdir, ckptdir, cfgdir, config):
super().__init__()
self.resume = resume
self.logdir = logdir
self.ckptdir = ckptdir
self.cfgdir = cfgdir
self.config = config
def on_fit_start(self, trainer, pl_module):
if trainer.global_rank == 0:
# Create logdirs and save configs
os.makedirs(self.logdir, exist_ok=True)
os.makedirs(self.ckptdir, exist_ok=True)
os.makedirs(self.cfgdir, exist_ok=True)
rank_zero_print("Project config")
rank_zero_print(OmegaConf.to_yaml(self.config))
OmegaConf.save(self.config, os.path.join(self.cfgdir, "project.yaml"))
class CodeSnapshot(Callback):
"""
Modified from https://github.com/threestudio-project/threestudio/blob/main/threestudio/utils/callbacks.py#L60
"""
def __init__(self, savedir):
self.savedir = savedir
def get_file_list(self):
return [
b.decode()
for b in set(subprocess.check_output('git ls-files -- ":!:configs/*"', 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."
)
if __name__ == "__main__":
# add cwd for convenience and to make classes in this file available when
# running as `python main.py`
sys.path.append(os.getcwd())
torch.set_float32_matmul_precision("medium")
parser = get_parser()
opt, unknown = parser.parse_known_args()
cfg_fname = os.path.split(opt.base)[-1]
cfg_name = os.path.splitext(cfg_fname)[0]
exp_name = "-" + opt.name if opt.name != "" else ""
logdir = os.path.join(opt.logdir, cfg_name + exp_name)
# assert not os.path.exists(logdir) or 'test' in logdir, logdir
if os.path.exists(logdir) and opt.resume is None:
auto_resume_path = os.path.join(logdir, "checkpoints", "last.ckpt")
if os.path.exists(auto_resume_path):
opt.resume = auto_resume_path
print(f"Auto set resume ckpt {opt.resume}")
ckptdir = os.path.join(logdir, "checkpoints")
cfgdir = os.path.join(logdir, "configs")
codedir = os.path.join(logdir, "code")
node_rank = int(os.environ.get("NODE_RANK", 0)) # 当前节点的编号
local_rank = int(os.environ.get("LOCAL_RANK", 0)) # 当前节点上的 GPU 编号
num_gpus_per_node = torch.cuda.device_count() # 每个节点上的 GPU 数量
global_rank = node_rank * num_gpus_per_node + local_rank
seed_everything(opt.seed + global_rank)
# init configs
config = OmegaConf.load(opt.base)
lightning_config = config.lightning
trainer_config = lightning_config.trainer
trainer_config["accelerator"] = "gpu"
rank_zero_print(f"Running on GPUs {opt.gpus}")
try:
ngpu = int(opt.gpus)
except:
ngpu = len(opt.gpus.strip(",").split(","))
trainer_config["devices"] = ngpu
trainer_opt = argparse.Namespace(**trainer_config)
lightning_config.trainer = trainer_config
# model
model = instantiate_from_config(config.model)
model_unet = model.unet.unet
model_unet_prefix = "unet.unet."
if hasattr(model_unet, "unet"):
model_unet = model_unet.unet
model_unet_prefix += "unet."
if getattr(config, "init_unet_from", None):
unet_ckpt_path = config.init_unet_from
sd = torch.load(unet_ckpt_path, map_location="cpu")
model_unet.load_state_dict(sd, strict=True)
if getattr(config, "init_vae_from", None):
vae_ckpt_path = config.init_vae_from
sd_vae = torch.load(vae_ckpt_path, map_location="cpu")
def replace_key(key_str):
replace_pairs = [("key", "to_k"), ("query", "to_q"), ("value", "to_v"), ("proj_attn", "to_out.0")]
for replace_pair in replace_pairs:
key_str = key_str.replace(replace_pair[0], replace_pair[1])
return key_str
sd_vae = {replace_key(k): v for k, v in sd_vae.items()}
model.pipeline.vae.load_state_dict(sd_vae, strict=True)
if hasattr(model.unet, "controlnet"):
if getattr(config, "init_control_from", None):
unet_ckpt_path = config.init_control_from
sd_control = torch.load(unet_ckpt_path, map_location="cpu")
model.unet.controlnet.load(sd_control, strict=True)
noise_in_channels = config.model.params.get("noise_in_channels", None)
if noise_in_channels is not None:
with torch.no_grad():
new_conv_in = torch.nn.Conv2d(
noise_in_channels,
model_unet.conv_in.out_channels,
model_unet.conv_in.kernel_size,
model_unet.conv_in.stride,
model_unet.conv_in.padding,
)
new_conv_in.weight.zero_()
new_conv_in.weight[:, : model_unet.conv_in.in_channels, :, :].copy_(model_unet.conv_in.weight)
new_conv_in.bias.zero_()
new_conv_in.bias[: model_unet.conv_in.bias.size(0)].copy_(model_unet.conv_in.bias)
model_unet.conv_in = new_conv_in
if hasattr(model.unet, "controlnet"):
if config.model.params.get("control_in_channels", None):
control_in_channels = config.model.params.control_in_channels
model.unet.controlnet.config["conditioning_channels"] = control_in_channels
condition_conv_in = model.unet.controlnet.controlnet_cond_embedding.conv_in
new_condition_conv_in = torch.nn.Conv2d(
control_in_channels,
condition_conv_in.out_channels,
kernel_size=condition_conv_in.kernel_size,
stride=condition_conv_in.stride,
padding=condition_conv_in.padding,
)
with torch.no_grad():
new_condition_conv_in.weight[:, : condition_conv_in.in_channels, :, :] = condition_conv_in.weight
if condition_conv_in.bias is not None:
new_condition_conv_in.bias = condition_conv_in.bias
model.unet.controlnet.controlnet_cond_embedding.conv_in = new_condition_conv_in
rank_zero_print(f"Loaded Init ...")
if getattr(config, "resume_from", None):
cnet_ckpt_path = config.resume_from
sds = torch.load(cnet_ckpt_path, map_location="cpu")["state_dict"]
sd0 = {k[len(model_unet_prefix) :]: v for k, v in sds.items() if model_unet_prefix in k}
# model.unet.unet.unet.load_state_dict(sd0, strict=True)
model_unet.load_state_dict(sd0, strict=True)
if hasattr(model.unet, "controlnet"):
sd1 = {k[16:]: v for k, v in sds.items() if "unet.controlnet." in k}
model.unet.controlnet.load_state_dict(sd1, strict=True)
rank_zero_print(f"Loaded {cnet_ckpt_path} ...")
if opt.resume and opt.resume_weights_only:
model = model.__class__.load_from_checkpoint(opt.resume, **config.model.params)
model.logdir = logdir
# trainer and callbacks
trainer_kwargs = dict()
# logger
default_logger_cfg = {
"target": "pytorch_lightning.loggers.TensorBoardLogger",
"params": {
"name": "tensorboard",
"save_dir": logdir,
"version": "0",
},
}
logger_cfg = OmegaConf.merge(default_logger_cfg)
trainer_kwargs["logger"] = instantiate_from_config(logger_cfg)
# model checkpoint
default_modelckpt_cfg = {
"target": "pytorch_lightning.callbacks.ModelCheckpoint",
"params": {
"dirpath": ckptdir,
"filename": "{step:08}",
"verbose": True,
"save_last": True,
"every_n_train_steps": 5000,
"save_top_k": -1, # save all checkpoints
},
}
if "modelcheckpoint" in lightning_config:
modelckpt_cfg = lightning_config.modelcheckpoint
else:
modelckpt_cfg = OmegaConf.create()
modelckpt_cfg = OmegaConf.merge(default_modelckpt_cfg, modelckpt_cfg)
# callbacks
default_callbacks_cfg = {
"setup_callback": {
"target": "train.SetupCallback",
"params": {
"resume": opt.resume,
"logdir": logdir,
"ckptdir": ckptdir,
"cfgdir": cfgdir,
"config": config,
},
},
"learning_rate_logger": {
"target": "pytorch_lightning.callbacks.LearningRateMonitor",
"params": {
"logging_interval": "step",
},
},
"code_snapshot": {
"target": "train.CodeSnapshot",
"params": {
"savedir": codedir,
},
},
}
default_callbacks_cfg["checkpoint_callback"] = modelckpt_cfg
if "callbacks" in lightning_config:
callbacks_cfg = lightning_config.callbacks
else:
callbacks_cfg = OmegaConf.create()
callbacks_cfg = OmegaConf.merge(default_callbacks_cfg, callbacks_cfg)
trainer_kwargs["callbacks"] = [instantiate_from_config(callbacks_cfg[k]) for k in callbacks_cfg]
trainer_kwargs["precision"] = "bf16"
trainer_kwargs["strategy"] = DDPStrategy(find_unused_parameters=False)
# trainer
trainer = Trainer(**trainer_config, **trainer_kwargs, num_nodes=opt.num_nodes, inference_mode=False)
trainer.logdir = logdir
# data
data = instantiate_from_config(config.data)
data.prepare_data()
data.setup("fit")
# configure learning rate
base_lr = config.model.base_learning_rate
if "accumulate_grad_batches" in lightning_config.trainer:
accumulate_grad_batches = lightning_config.trainer.accumulate_grad_batches
else:
accumulate_grad_batches = 1
rank_zero_print(f"accumulate_grad_batches = {accumulate_grad_batches}")
lightning_config.trainer.accumulate_grad_batches = accumulate_grad_batches
model.learning_rate = base_lr
rank_zero_print("++++ NOT USING LR SCALING ++++")
rank_zero_print(f"Setting learning rate to {model.learning_rate:.2e}")
# run training loop
if opt.resume and not opt.resume_weights_only:
trainer.fit(model, data, ckpt_path=opt.resume)
else:
trainer.fit(model, data)
@@ -0,0 +1,13 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
@@ -0,0 +1,41 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import numpy as np
from PIL import Image
class imageSuperNet:
def __init__(self, config) -> None:
from realesrgan import RealESRGANer
from basicsr.archs.rrdbnet_arch import RRDBNet
model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4)
upsampler = RealESRGANer(
scale=4,
model_path=config.realesrgan_ckpt_path,
dni_weight=None,
model=model,
tile=0,
tile_pad=10,
pre_pad=0,
half=True,
gpu_id=None,
)
self.upsampler = upsampler
def __call__(self, image):
output, _ = self.upsampler.enhance(np.array(image))
output = Image.fromarray(output)
return output
@@ -0,0 +1,128 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
import torch
import random
import numpy as np
from PIL import Image
from typing import List
import huggingface_hub
from omegaconf import OmegaConf
from diffusers import DiffusionPipeline
from diffusers import EulerAncestralDiscreteScheduler, DDIMScheduler, UniPCMultistepScheduler
class multiviewDiffusionNet:
def __init__(self, config) -> None:
self.device = config.device
cfg_path = config.multiview_cfg_path
custom_pipeline = config.custom_pipeline
cfg = OmegaConf.load(cfg_path)
self.cfg = cfg
self.mode = self.cfg.model.params.stable_diffusion_config.custom_pipeline[2:]
model_path = huggingface_hub.snapshot_download(
repo_id=config.multiview_pretrained_path,
allow_patterns=["hunyuan3d-paintpbr-v2-1/*"],
)
model_path = os.path.join(model_path, "hunyuan3d-paintpbr-v2-1")
pipeline = DiffusionPipeline.from_pretrained(
model_path,
custom_pipeline=custom_pipeline,
torch_dtype=torch.float16
)
pipeline.scheduler = UniPCMultistepScheduler.from_config(pipeline.scheduler.config, timestep_spacing="trailing")
pipeline.set_progress_bar_config(disable=True)
pipeline.eval()
setattr(pipeline, "view_size", cfg.model.params.get("view_size", 320))
self.pipeline = pipeline.to(self.device)
if hasattr(self.pipeline.unet, "use_dino") and self.pipeline.unet.use_dino:
from hunyuanpaintpbr.unet.modules import Dino_v2
self.dino_v2 = Dino_v2(config.dino_ckpt_path).to(torch.float16)
self.dino_v2 = self.dino_v2.to(self.device)
def seed_everything(self, seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
os.environ["PL_GLOBAL_SEED"] = str(seed)
@torch.no_grad()
def __call__(self, images, conditions, prompt=None, custom_view_size=None, resize_input=False):
pils = self.forward_one(
images, conditions, prompt=prompt, custom_view_size=custom_view_size, resize_input=resize_input
)
return pils
def forward_one(self, input_images, control_images, prompt=None, custom_view_size=None, resize_input=False):
self.seed_everything(0)
custom_view_size = custom_view_size if custom_view_size is not None else self.pipeline.view_size
if not isinstance(input_images, List):
input_images = [input_images]
if not resize_input:
input_images = [
input_image.resize((self.pipeline.view_size, self.pipeline.view_size)) for input_image in input_images
]
else:
input_images = [input_image.resize((custom_view_size, custom_view_size)) for input_image in input_images]
for i in range(len(control_images)):
control_images[i] = control_images[i].resize((custom_view_size, custom_view_size))
if control_images[i].mode == "L":
control_images[i] = control_images[i].point(lambda x: 255 if x > 1 else 0, mode="1")
kwargs = dict(generator=torch.Generator(device=self.pipeline.device).manual_seed(0))
num_view = len(control_images) // 2
normal_image = [[control_images[i] for i in range(num_view)]]
position_image = [[control_images[i + num_view] for i in range(num_view)]]
kwargs["width"] = custom_view_size
kwargs["height"] = custom_view_size
kwargs["num_in_batch"] = num_view
kwargs["images_normal"] = normal_image
kwargs["images_position"] = position_image
if hasattr(self.pipeline.unet, "use_dino") and self.pipeline.unet.use_dino:
dino_hidden_states = self.dino_v2(input_images[0])
kwargs["dino_hidden_states"] = dino_hidden_states
sync_condition = None
infer_steps_dict = {
"EulerAncestralDiscreteScheduler": 30,
"UniPCMultistepScheduler": 15,
"DDIMScheduler": 50,
"ShiftSNRScheduler": 15,
}
mvd_image = self.pipeline(
input_images[0:1],
num_inference_steps=infer_steps_dict[self.pipeline.scheduler.__class__.__name__],
prompt=prompt,
sync_condition=sync_condition,
guidance_scale=3.0,
**kwargs,
).images
if "pbr" in self.mode:
mvd_image = {"albedo": mvd_image[:num_view], "mr": mvd_image[num_view:]}
# mvd_image = {'albedo':mvd_image[:num_view]}
else:
mvd_image = {"hdr": mvd_image}
return mvd_image
@@ -0,0 +1,135 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import torch
import numpy as np
class ViewProcessor:
def __init__(self, config, render):
self.config = config
self.render = render
def render_normal_multiview(self, camera_elevs, camera_azims, use_abs_coor=True):
normal_maps = []
for elev, azim in zip(camera_elevs, camera_azims):
normal_map = self.render.render_normal(elev, azim, use_abs_coor=use_abs_coor, return_type="pl")
normal_maps.append(normal_map)
return normal_maps
def render_position_multiview(self, camera_elevs, camera_azims):
position_maps = []
for elev, azim in zip(camera_elevs, camera_azims):
position_map = self.render.render_position(elev, azim, return_type="pl")
position_maps.append(position_map)
return position_maps
def bake_view_selection(
self, candidate_camera_elevs, candidate_camera_azims, candidate_view_weights, max_selected_view_num
):
original_resolution = self.render.default_resolution
self.render.set_default_render_resolution(1024)
selected_camera_elevs = []
selected_camera_azims = []
selected_view_weights = []
selected_alpha_maps = []
viewed_tri_idxs = []
viewed_masks = []
# 计算每个三角片的面积
face_areas = self.render.get_face_areas(from_one_index=True)
total_area = face_areas.sum()
face_area_ratios = face_areas / total_area
candidate_view_num = len(candidate_camera_elevs)
self.render.set_boundary_unreliable_scale(2)
for elev, azim in zip(candidate_camera_elevs, candidate_camera_azims):
viewed_tri_idx = self.render.render_alpha(elev, azim, return_type="np")
viewed_tri_idxs.append(set(np.unique(viewed_tri_idx.flatten())))
viewed_masks.append(viewed_tri_idx[0, :, :, 0] > 0)
is_selected = [False for _ in range(candidate_view_num)]
total_viewed_tri_idxs = set()
total_viewed_area = 0.0
for idx in range(6):
selected_camera_elevs.append(candidate_camera_elevs[idx])
selected_camera_azims.append(candidate_camera_azims[idx])
selected_view_weights.append(candidate_view_weights[idx])
selected_alpha_maps.append(viewed_masks[idx])
is_selected[idx] = True
total_viewed_tri_idxs.update(viewed_tri_idxs[idx])
total_viewed_area = face_area_ratios[list(total_viewed_tri_idxs)].sum()
for iter in range(max_selected_view_num - len(selected_view_weights)):
max_inc = 0
max_idx = -1
for idx, (elev, azim, weight) in enumerate(
zip(candidate_camera_elevs, candidate_camera_azims, candidate_view_weights)
):
if is_selected[idx]:
continue
new_tri_idxs = viewed_tri_idxs[idx] - total_viewed_tri_idxs
new_inc_area = face_area_ratios[list(new_tri_idxs)].sum()
if new_inc_area > max_inc:
max_inc = new_inc_area
max_idx = idx
if max_inc > 0.01:
is_selected[max_idx] = True
selected_camera_elevs.append(candidate_camera_elevs[max_idx])
selected_camera_azims.append(candidate_camera_azims[max_idx])
selected_view_weights.append(candidate_view_weights[max_idx])
selected_alpha_maps.append(viewed_masks[max_idx])
total_viewed_tri_idxs = total_viewed_tri_idxs.union(viewed_tri_idxs[max_idx])
total_viewed_area += max_inc
else:
break
self.render.set_default_render_resolution(original_resolution)
return selected_camera_elevs, selected_camera_azims, selected_view_weights
def bake_from_multiview(self, views, camera_elevs, camera_azims, view_weights):
project_textures, project_weighted_cos_maps = [], []
project_boundary_maps = []
for view, camera_elev, camera_azim, weight in zip(views, camera_elevs, camera_azims, view_weights):
project_texture, project_cos_map, project_boundary_map = self.render.back_project(
view, camera_elev, camera_azim
)
project_cos_map = weight * (project_cos_map**self.config.bake_exp)
project_textures.append(project_texture)
project_weighted_cos_maps.append(project_cos_map)
project_boundary_maps.append(project_boundary_map)
texture, ori_trust_map = self.render.fast_bake_texture(project_textures, project_weighted_cos_maps)
return texture, ori_trust_map > 1e-8
def texture_inpaint(self, texture, mask, defualt=None):
if defualt is not None:
mask = mask.astype(bool)
inpaint_value = torch.tensor(defualt, dtype=texture.dtype, device=texture.device)
texture[~mask] = inpaint_value
else:
texture_np = self.render.uv_inpaint(texture, mask)
texture = torch.tensor(texture_np / 255).float().to(texture.device)
return texture
@@ -0,0 +1,32 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import trimesh
import pymeshlab
def remesh_mesh(mesh_path, remesh_path):
mesh = mesh_simplify_trimesh(mesh_path, remesh_path)
def mesh_simplify_trimesh(inputpath, outputpath, target_count=40000):
ms = pymeshlab.MeshSet()
if inputpath.endswith(".glb"):
ms.load_new_mesh(inputpath, load_in_a_single_layer=True)
else:
ms.load_new_mesh(inputpath)
ms.meshing_decimation_quadric_edge_collapse(targetfacenum=target_count)
obj_path = outputpath.replace(".glb", ".obj")
ms.save_current_mesh(obj_path, save_textures=False)
mesh = trimesh.load(obj_path, force="mesh")
mesh.export(outputpath)
@@ -0,0 +1,111 @@
# Torchvision compatibility fix for functional_tensor module
# This file helps resolve compatibility issues between different torchvision versions
import sys
import torch
import torchvision
def fix_torchvision_functional_tensor():
"""
Fix torchvision.transforms.functional_tensor import issue
"""
try:
# Check if the module exists in the expected location
import torchvision.transforms.functional_tensor
print("torchvision.transforms.functional_tensor is available")
return True
except ImportError:
print("torchvision.transforms.functional_tensor not found, applying compatibility fix...")
try:
# Create a mock functional_tensor module with the required functions
import torchvision.transforms.functional as F
class FunctionalTensorMock:
"""Mock module to replace functional_tensor"""
@staticmethod
def _get_grayscale_weights(img):
"""Helper to create grayscale weights based on image dimensions"""
weights = torch.tensor([0.299, 0.587, 0.114], device=img.device, dtype=img.dtype)
return weights.view(1, 3, 1, 1) if len(img.shape) == 4 else weights.view(3, 1, 1)
@staticmethod
def _try_import_fallback(module_names, attr_name):
"""Helper to try importing from multiple modules"""
for module_name in module_names:
try:
module = __import__(module_name, fromlist=[attr_name])
if hasattr(module, attr_name):
return getattr(module, attr_name)
except ImportError:
continue
return None
@staticmethod
def rgb_to_grayscale(img, num_output_channels=1):
"""Convert RGB image to grayscale"""
if hasattr(F, 'rgb_to_grayscale'):
return F.rgb_to_grayscale(img, num_output_channels)
# Fallback implementation
weights = FunctionalTensorMock._get_grayscale_weights(img)
grayscale = torch.sum(img * weights, dim=-3, keepdim=True)
if num_output_channels == 3:
repeat_dims = (1, 3, 1, 1) if len(img.shape) == 4 else (3, 1, 1)
grayscale = grayscale.repeat(*repeat_dims)
return grayscale
@staticmethod
def resize(img, size, interpolation=2, antialias=None):
"""Resize function wrapper"""
# Try v2.functional first, then regular functional, then torch.nn.functional
resize_func = FunctionalTensorMock._try_import_fallback([
'torchvision.transforms.v2.functional',
'torchvision.transforms.functional'
], 'resize')
if resize_func:
try:
return resize_func(img, size, interpolation=interpolation, antialias=antialias)
except TypeError:
# Fallback for older versions without antialias parameter
return resize_func(img, size, interpolation=interpolation)
# Final fallback using torch.nn.functional
import torch.nn.functional as torch_F
size = (size, size) if isinstance(size, int) else size
img_input = img.unsqueeze(0) if len(img.shape) == 3 else img
return torch_F.interpolate(img_input, size=size, mode='bilinear', align_corners=False)
def __getattr__(self, name):
"""Fallback to regular functional module"""
func = self._try_import_fallback([
'torchvision.transforms.functional',
'torchvision.transforms.v2.functional'
], name)
if func:
return func
raise AttributeError(f"'{name}' not found in functional_tensor mock")
# Create the mock module instance and monkey patch
sys.modules['torchvision.transforms.functional_tensor'] = FunctionalTensorMock()
print("Applied compatibility fix: created functional_tensor mock module")
return True
except Exception as e:
print(f"Failed to create functional_tensor mock: {e}")
return False
def apply_fix():
"""Apply the torchvision compatibility fix"""
print(f"Torchvision version: {torchvision.__version__}")
return fix_torchvision_functional_tensor()
if __name__ == "__main__":
apply_fix()
@@ -0,0 +1,32 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import trimesh
import xatlas
def mesh_uv_wrap(mesh):
if isinstance(mesh, trimesh.Scene):
mesh = mesh.dump(concatenate=True)
if len(mesh.faces) > 500000000:
raise ValueError("The mesh has more than 500,000,000 faces, which is not supported.")
vmapping, indices, uvs = xatlas.parametrize(mesh.vertices, mesh.faces)
mesh.vertices = mesh.vertices[vmapping]
mesh.faces = indices
mesh.visual.uv = uvs
return mesh
@@ -0,0 +1,169 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
!hy3dgen/texgen/custom_rasterizer/lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
.DS_Store
# Cython debug symbols
cython_debug/
gradio_cache/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
@@ -0,0 +1,174 @@
name: "DiT: Flux large flowmatching; VAE: 1024 token length; ImageEncoder: DINO Giant; ImageSize: 518"
training:
steps: 10_0000_0000
use_amp: true
amp_type: "bf16"
base_lr: 1.e-5
gradient_clip_val: 1.0
gradient_clip_algorithm: "norm"
every_n_train_steps: 2000 # 5000
val_check_interval: 50 # 4096
limit_val_batches: 16
dataset:
target: hy3dshape.data.dit_asl.AlignedShapeLatentModule
params:
#! Base setting
batch_size: 4
num_workers: 8
val_num_workers: 4
# Data
train_data_list: tools/mini_trainset/preprocessed
val_data_list: tools/mini_trainset/preprocessed
#! Image loading
cond_stage_key: "image" # image / text / image_text
image_size: 518
mean: &mean [0.5, 0.5, 0.5]
std: &std [0.5, 0.5, 0.5]
#! Point cloud sampling
pc_size: &pc_size 30720
pc_sharpedge_size: &pc_sharpedge_size 30720
sharpedge_label: &sharpedge_label true
return_normal: true
#! Augmentation
padding: true
model:
target: hy3dshape.models.diffusion.flow_matching_sit.Diffuser
params:
first_stage_key: "surface"
cond_stage_key: "image"
scale_by_std: false
z_scale_factor: &z_scale_factor 0.9990943042622529 # 1 / 1.0009065167661184
torch_compile: false
# ema_config:
# ema_model: LitEma
# ema_decay: 0.999
# ema_inference: false
first_stage_config:
target: hy3dshape.models.autoencoders.ShapeVAE
from_pretrained: tencent/Hunyuan3D-2.1
params:
num_latents: &num_latents 512
embed_dim: 64
num_freqs: 8
include_pi: false
heads: 16
width: 1024
point_feats: 4
num_decoder_layers: 16
pc_size: *pc_size
pc_sharpedge_size: *pc_sharpedge_size
qkv_bias: false
qk_norm: true
scale_factor: *z_scale_factor
geo_decoder_mlp_expand_ratio: 4
geo_decoder_downsample_ratio: 1
geo_decoder_ln_post: true
cond_stage_config:
target: hy3dshape.models.conditioner.SingleImageEncoder
params:
main_image_encoder:
type: DinoImageEncoder # dino giant
kwargs:
config:
attention_probs_dropout_prob: 0.0
drop_path_rate: 0.0
hidden_act: gelu
hidden_dropout_prob: 0.0
hidden_size: 1536
image_size: 518
initializer_range: 0.02
layer_norm_eps: 1.e-6
layerscale_value: 1.0
mlp_ratio: 4
model_type: dinov2
num_attention_heads: 24
num_channels: 3
num_hidden_layers: 40
patch_size: 14
qkv_bias: true
torch_dtype: float32
use_swiglu_ffn: true
image_size: 518
denoiser_cfg:
target: hy3dshape.models.denoisers.hunyuan3ddit.Hunyuan3DDiT
params:
ckpt_path: ~/.cache/hy3dgen/tencent/Hunyuan3D-2-1-Shape/dit/model.fp16.ckpt
input_size: *num_latents
context_in_dim: 1536
hidden_size: 1024
mlp_ratio: 4.0
num_heads: 16
depth: 16
depth_single_blocks: 32
axes_dim: [64]
theta: 10000
qkv_bias: true
use_pe: false
force_norm_fp32: true
scheduler_cfg:
transport:
target: hy3dshape.models.diffusion.transport.create_transport
params:
path_type: Linear
prediction: velocity
sampler:
target: hy3dshape.models.diffusion.transport.Sampler
params: {}
ode_params:
sampling_method: euler # dopri5 ...
num_steps: &num_steps 50
optimizer_cfg:
optimizer:
target: torch.optim.AdamW
params:
betas: [0.9, 0.99]
eps: 1.e-6
weight_decay: 1.e-2
scheduler:
target: hy3dshape.utils.trainings.lr_scheduler.LambdaWarmUpCosineFactorScheduler
params:
warm_up_steps: 50 # 5000
f_start: 1.e-6
f_min: 1.e-3
f_max: 1.0
pipeline_cfg:
target: hy3dshape.pipelines.Hunyuan3DDiTFlowMatchingPipeline
image_processor_cfg:
target: hy3dshape.preprocessors.ImageProcessorV2
params: {}
callbacks:
logger:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalASLDiffuserLogger
params:
step_frequency: 100 # 10000
num_samples: 1
sample_times: 1
mean: *mean
std: *std
bounds: [-1.01, -1.01, -1.01, 1.01, 1.01, 1.01]
octree_depth: 8
num_chunks: 50000
mc_level: 0.0
file_loggers:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalFixASLDiffuserLogger
params:
step_frequency: 50 # 5000
test_data_path: "tools/mini_testset/images.json"
@@ -0,0 +1,173 @@
name: "DiT: Flux large flowmatching; VAE: 1024 token length; ImageEncoder: DINO Giant; ImageSize: 518"
training:
steps: 10_0000_0000
use_amp: true
amp_type: "bf16"
base_lr: 1e-4
gradient_clip_val: 1.0
gradient_clip_algorithm: "norm"
every_n_train_steps: 2000 # 5000
val_check_interval: 50 # 4096
limit_val_batches: 16
dataset:
target: hy3dshape.data.dit_asl.AlignedShapeLatentModule
params:
#! Base setting
batch_size: 2
num_workers: 8
val_num_workers: 4
# Data
train_data_list: tools/mini_trainset/preprocessed
val_data_list: tools/mini_trainset/preprocessed
#! Image loading
cond_stage_key: "image" # image / text / image_text
image_size: 518
mean: &mean [0.5, 0.5, 0.5]
std: &std [0.5, 0.5, 0.5]
#! Point cloud sampling
pc_size: &pc_size 10240
pc_sharpedge_size: &pc_sharpedge_size 10240
sharpedge_label: &sharpedge_label true
return_normal: true
#! Augmentation
padding: true
model:
target: hy3dshape.models.diffusion.flow_matching_sit.Diffuser
params:
first_stage_key: "surface"
cond_stage_key: "image"
scale_by_std: false
z_scale_factor: &z_scale_factor 0.9990943042622529 # 1 / 1.0009065167661184
torch_compile: false
# ema_config:
# ema_model: LitEma
# ema_decay: 0.999
# ema_inference: false
first_stage_config:
target: hy3dshape.models.autoencoders.ShapeVAE
from_pretrained: tencent/Hunyuan3D-2.1
params:
num_latents: &num_latents 512
embed_dim: 64
num_freqs: 8
include_pi: false
heads: 16
width: 1024
point_feats: 4
num_decoder_layers: 16
pc_size: *pc_size
pc_sharpedge_size: *pc_sharpedge_size
qkv_bias: false
qk_norm: true
scale_factor: *z_scale_factor
geo_decoder_mlp_expand_ratio: 4
geo_decoder_downsample_ratio: 1
geo_decoder_ln_post: true
cond_stage_config:
target: hy3dshape.models.conditioner.SingleImageEncoder
params:
main_image_encoder:
type: DinoImageEncoder # dino giant
kwargs:
config:
attention_probs_dropout_prob: 0.0
drop_path_rate: 0.0
hidden_act: gelu
hidden_dropout_prob: 0.0
hidden_size: 1536
image_size: 518
initializer_range: 0.02
layer_norm_eps: 1.e-6
layerscale_value: 1.0
mlp_ratio: 4
model_type: dinov2
num_attention_heads: 24
num_channels: 3
num_hidden_layers: 40
patch_size: 14
qkv_bias: true
torch_dtype: float32
use_swiglu_ffn: true
image_size: 518
denoiser_cfg:
target: hy3dshape.models.denoisers.hunyuan3ddit.Hunyuan3DDiT
params:
input_size: *num_latents
context_in_dim: 1536
hidden_size: 1024
mlp_ratio: 4.0
num_heads: 16
depth: 8
depth_single_blocks: 16
axes_dim: [64]
theta: 10000
qkv_bias: true
use_pe: false
force_norm_fp32: true
scheduler_cfg:
transport:
target: hy3dshape.models.diffusion.transport.create_transport
params:
path_type: Linear
prediction: velocity
sampler:
target: hy3dshape.models.diffusion.transport.Sampler
params: {}
ode_params:
sampling_method: euler # dopri5 ...
num_steps: &num_steps 50
optimizer_cfg:
optimizer:
target: torch.optim.AdamW
params:
betas: [0.9, 0.99]
eps: 1.e-6
weight_decay: 1.e-2
scheduler:
target: hy3dshape.utils.trainings.lr_scheduler.LambdaWarmUpCosineFactorScheduler
params:
warm_up_steps: 50 # 5000
f_start: 1.e-6
f_min: 1.e-3
f_max: 1.0
pipeline_cfg:
target: hy3dshape.pipelines.Hunyuan3DDiTFlowMatchingPipeline
image_processor_cfg:
target: hy3dshape.preprocessors.ImageProcessorV2
params: {}
callbacks:
logger:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalASLDiffuserLogger
params:
step_frequency: 100 # 10000
num_samples: 1
sample_times: 1
mean: *mean
std: *std
bounds: [-1.01, -1.01, -1.01, 1.01, 1.01, 1.01]
octree_depth: 8
num_chunks: 50000
mc_level: 0.0
file_loggers:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalFixASLDiffuserLogger
params:
step_frequency: 50 # 5000
test_data_path: "tools/mini_testset/images.json"
@@ -0,0 +1,180 @@
name: "DiT: Flux large flowmatching; VAE: 1024 token length; ImageEncoder: DINO Giant; ImageSize: 518"
training:
steps: 10_0000_0000
use_amp: true
amp_type: "bf16"
base_lr: 1e-5
gradient_clip_val: 1.0
gradient_clip_algorithm: "norm"
every_n_train_steps: 2000 # 5000
val_check_interval: 50 # 4096
limit_val_batches: 16
dataset:
target: hy3dshape.data.dit_asl.AlignedShapeLatentModule
params:
#! Base setting
batch_size: 4
num_workers: 8
val_num_workers: 4
# Data
train_data_list: tools/mini_trainset/preprocessed
val_data_list: tools/mini_trainset/preprocessed
#! Image loading
cond_stage_key: "image" # image / text / image_text
image_size: 518
mean: &mean [0.5, 0.5, 0.5]
std: &std [0.5, 0.5, 0.5]
#! Point cloud sampling
pc_size: &pc_size 81920
pc_sharpedge_size: &pc_sharpedge_size 0
sharpedge_label: &sharpedge_label true
return_normal: true
#! Augmentation
padding: true
model:
target: hy3dshape.models.diffusion.flow_matching_sit.Diffuser
params:
first_stage_key: "surface"
cond_stage_key: "image"
scale_by_std: false
z_scale_factor: &z_scale_factor 1.0039506158752403
torch_compile: false
# ema_config:
# ema_model: LitEma
# ema_decay: 0.999
# ema_inference: false
first_stage_config:
target: hy3dshape.models.autoencoders.ShapeVAE
from_pretrained: tencent/Hunyuan3D-2.1
params:
num_latents: &num_latents 4096
embed_dim: 64
num_freqs: 8
include_pi: false
heads: 16
width: 1024
num_encoder_layers: 8
num_decoder_layers: 16
qkv_bias: false
qk_norm: true
scale_factor: *z_scale_factor
geo_decoder_mlp_expand_ratio: 4
geo_decoder_downsample_ratio: 1
geo_decoder_ln_post: true
point_feats: 4
pc_size: *pc_size
pc_sharpedge_size: *pc_sharpedge_size
cond_stage_config:
target: hy3dshape.models.conditioner.SingleImageEncoder
params:
main_image_encoder:
type: DinoImageEncoder # dino large
kwargs:
config:
attention_probs_dropout_prob: 0.0
drop_path_rate: 0.0
hidden_act: gelu
hidden_dropout_prob: 0.0
hidden_size: 1024
image_size: 518
initializer_range: 0.02
layer_norm_eps: 1.e-6
layerscale_value: 1.0
mlp_ratio: 4
model_type: dinov2
num_attention_heads: 16
num_channels: 3
num_hidden_layers: 24
patch_size: 14
qkv_bias: true
torch_dtype: float32
use_swiglu_ffn: false
image_size: 518
use_cls_token: true
denoiser_cfg:
target: hy3dshape.models.denoisers.hunyuandit.HunYuanDiTPlain
params:
input_size: *num_latents
in_channels: 64
hidden_size: 2048
context_dim: 1024
depth: 21
num_heads: 16
qk_norm: true
text_len: 1370
with_decoupled_ca: false
use_attention_pooling: false
qk_norm_type: 'rms'
qkv_bias: false
use_pos_emb: false
num_moe_layers: 6
num_experts: 8
moe_top_k: 2
scheduler_cfg:
transport:
target: hy3dshape.models.diffusion.transport.create_transport
params:
path_type: Linear
prediction: velocity
sampler:
target: hy3dshape.models.diffusion.transport.Sampler
params: {}
ode_params:
sampling_method: euler # dopri5 ...
num_steps: &num_steps 50
optimizer_cfg:
optimizer:
target: torch.optim.AdamW
params:
betas: [0.9, 0.99]
eps: 1.e-6
weight_decay: 1.e-2
scheduler:
target: hy3dshape.utils.trainings.lr_scheduler.LambdaWarmUpCosineFactorScheduler
params:
warm_up_steps: 50 # 5000
f_start: 1.e-6
f_min: 1.e-3
f_max: 1.0
pipeline_cfg:
target: hy3dshape.pipelines.Hunyuan3DDiTFlowMatchingPipeline
image_processor_cfg:
target: hy3dshape.preprocessors.ImageProcessorV2
params: {}
callbacks:
logger:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalASLDiffuserLogger
params:
step_frequency: 100 # 10000
num_samples: 1
sample_times: 1
mean: *mean
std: *std
bounds: [-1.01, -1.01, -1.01, 1.01, 1.01, 1.01]
octree_depth: 8
num_chunks: 50000
mc_level: 0.0
file_loggers:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalFixASLDiffuserLogger
params:
step_frequency: 50 # 5000
test_data_path: "tools/mini_testset/images.json"
@@ -0,0 +1,180 @@
name: "DiT: Flux large flowmatching; VAE: 1024 token length; ImageEncoder: DINO Giant; ImageSize: 518"
training:
steps: 10_0000_0000
use_amp: true
amp_type: "bf16"
base_lr: 1e-4
gradient_clip_val: 1.0
gradient_clip_algorithm: "norm"
every_n_train_steps: 2000 # 5000
val_check_interval: 50 # 4096
limit_val_batches: 16
dataset:
target: hy3dshape.data.dit_asl.AlignedShapeLatentModule
params:
#! Base setting
batch_size: 2
num_workers: 8
val_num_workers: 4
# Data
train_data_list: tools/mini_trainset/preprocessed
val_data_list: tools/mini_trainset/preprocessed
#! Image loading
cond_stage_key: "image" # image / text / image_text
image_size: 518
mean: &mean [0.5, 0.5, 0.5]
std: &std [0.5, 0.5, 0.5]
#! Point cloud sampling
pc_size: &pc_size 81920
pc_sharpedge_size: &pc_sharpedge_size 0
sharpedge_label: &sharpedge_label true
return_normal: true
#! Augmentation
padding: true
model:
target: hy3dshape.models.diffusion.flow_matching_sit.Diffuser
params:
first_stage_key: "surface"
cond_stage_key: "image"
scale_by_std: false
z_scale_factor: &z_scale_factor 1.0039506158752403
torch_compile: false
# ema_config:
# ema_model: LitEma
# ema_decay: 0.999
# ema_inference: false
first_stage_config:
target: hy3dshape.models.autoencoders.ShapeVAE
from_pretrained: tencent/Hunyuan3D-2.1
params:
num_latents: &num_latents 4096
embed_dim: 64
num_freqs: 8
include_pi: false
heads: 16
width: 1024
num_encoder_layers: 8
num_decoder_layers: 16
qkv_bias: false
qk_norm: true
scale_factor: *z_scale_factor
geo_decoder_mlp_expand_ratio: 4
geo_decoder_downsample_ratio: 1
geo_decoder_ln_post: true
point_feats: 4
pc_size: *pc_size
pc_sharpedge_size: *pc_sharpedge_size
cond_stage_config:
target: hy3dshape.models.conditioner.SingleImageEncoder
params:
main_image_encoder:
type: DinoImageEncoder # dino large
kwargs:
config:
attention_probs_dropout_prob: 0.0
drop_path_rate: 0.0
hidden_act: gelu
hidden_dropout_prob: 0.0
hidden_size: 1024
image_size: 518
initializer_range: 0.02
layer_norm_eps: 1.e-6
layerscale_value: 1.0
mlp_ratio: 4
model_type: dinov2
num_attention_heads: 16
num_channels: 3
num_hidden_layers: 24
patch_size: 14
qkv_bias: true
torch_dtype: float32
use_swiglu_ffn: false
image_size: 518
use_cls_token: true
denoiser_cfg:
target: hy3dshape.models.denoisers.hunyuandit.HunYuanDiTPlain
params:
input_size: *num_latents
in_channels: 64
hidden_size: 2048
context_dim: 1024
depth: 11
num_heads: 16
qk_norm: true
text_len: 1370
with_decoupled_ca: false
use_attention_pooling: false
qk_norm_type: 'rms'
qkv_bias: false
use_pos_emb: false
num_moe_layers: 6
num_experts: 8
moe_top_k: 2
scheduler_cfg:
transport:
target: hy3dshape.models.diffusion.transport.create_transport
params:
path_type: Linear
prediction: velocity
sampler:
target: hy3dshape.models.diffusion.transport.Sampler
params: {}
ode_params:
sampling_method: euler # dopri5 ...
num_steps: &num_steps 50
optimizer_cfg:
optimizer:
target: torch.optim.AdamW
params:
betas: [0.9, 0.99]
eps: 1.e-6
weight_decay: 1.e-2
scheduler:
target: hy3dshape.utils.trainings.lr_scheduler.LambdaWarmUpCosineFactorScheduler
params:
warm_up_steps: 50 # 5000
f_start: 1.e-6
f_min: 1.e-3
f_max: 1.0
pipeline_cfg:
target: hy3dshape.pipelines.Hunyuan3DDiTFlowMatchingPipeline
image_processor_cfg:
target: hy3dshape.preprocessors.ImageProcessorV2
params: {}
callbacks:
logger:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalASLDiffuserLogger
params:
step_frequency: 100 # 10000
num_samples: 1
sample_times: 1
mean: *mean
std: *std
bounds: [-1.01, -1.01, -1.01, 1.01, 1.01, 1.01]
octree_depth: 8
num_chunks: 50000
mc_level: 0.0
file_loggers:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalFixASLDiffuserLogger
params:
step_frequency: 50 # 5000
test_data_path: "tools/mini_testset/images.json"
@@ -0,0 +1,180 @@
name: "DiT: Flux large flowmatching; VAE: 1024 token length; ImageEncoder: DINO Giant; ImageSize: 518"
training:
steps: 10_0000_0000
use_amp: true
amp_type: "bf16"
base_lr: 1e-4
gradient_clip_val: 1.0
gradient_clip_algorithm: "norm"
every_n_train_steps: 2000 # 5000
val_check_interval: 50 # 4096
limit_val_batches: 16
dataset:
target: hy3dshape.data.dit_asl.AlignedShapeLatentModule
params:
#! Base setting
batch_size: 2
num_workers: 8
val_num_workers: 4
# Data
train_data_list: tools/mini_trainset/preprocessed
val_data_list: tools/mini_trainset/preprocessed
#! Image loading
cond_stage_key: "image" # image / text / image_text
image_size: 518
mean: &mean [0.5, 0.5, 0.5]
std: &std [0.5, 0.5, 0.5]
#! Point cloud sampling
pc_size: &pc_size 81920
pc_sharpedge_size: &pc_sharpedge_size 0
sharpedge_label: &sharpedge_label true
return_normal: true
#! Augmentation
padding: true
model:
target: hy3dshape.models.diffusion.flow_matching_sit.Diffuser
params:
first_stage_key: "surface"
cond_stage_key: "image"
scale_by_std: false
z_scale_factor: &z_scale_factor 1.0039506158752403
torch_compile: false
# ema_config:
# ema_model: LitEma
# ema_decay: 0.999
# ema_inference: false
first_stage_config:
target: hy3dshape.models.autoencoders.ShapeVAE
from_pretrained: tencent/Hunyuan3D-2.1
params:
num_latents: &num_latents 512
embed_dim: 64
num_freqs: 8
include_pi: false
heads: 16
width: 1024
num_encoder_layers: 8
num_decoder_layers: 16
qkv_bias: false
qk_norm: true
scale_factor: *z_scale_factor
geo_decoder_mlp_expand_ratio: 4
geo_decoder_downsample_ratio: 1
geo_decoder_ln_post: true
point_feats: 4
pc_size: *pc_size
pc_sharpedge_size: *pc_sharpedge_size
cond_stage_config:
target: hy3dshape.models.conditioner.SingleImageEncoder
params:
main_image_encoder:
type: DinoImageEncoder # dino large
kwargs:
config:
attention_probs_dropout_prob: 0.0
drop_path_rate: 0.0
hidden_act: gelu
hidden_dropout_prob: 0.0
hidden_size: 1024
image_size: 518
initializer_range: 0.02
layer_norm_eps: 1.e-6
layerscale_value: 1.0
mlp_ratio: 4
model_type: dinov2
num_attention_heads: 16
num_channels: 3
num_hidden_layers: 24
patch_size: 14
qkv_bias: true
torch_dtype: float32
use_swiglu_ffn: false
image_size: 518
use_cls_token: true
denoiser_cfg:
target: hy3dshape.models.denoisers.hunyuandit.HunYuanDiTPlain
params:
input_size: *num_latents
in_channels: 64
hidden_size: 768
context_dim: 1024
depth: 6
num_heads: 12
qk_norm: true
text_len: 1370
with_decoupled_ca: false
use_attention_pooling: false
qk_norm_type: 'rms'
qkv_bias: false
use_pos_emb: false
num_moe_layers: 3
num_experts: 4
moe_top_k: 2
scheduler_cfg:
transport:
target: hy3dshape.models.diffusion.transport.create_transport
params:
path_type: Linear
prediction: velocity
sampler:
target: hy3dshape.models.diffusion.transport.Sampler
params: {}
ode_params:
sampling_method: euler # dopri5 ...
num_steps: &num_steps 50
optimizer_cfg:
optimizer:
target: torch.optim.AdamW
params:
betas: [0.9, 0.99]
eps: 1.e-6
weight_decay: 1.e-2
scheduler:
target: hy3dshape.utils.trainings.lr_scheduler.LambdaWarmUpCosineFactorScheduler
params:
warm_up_steps: 50 # 5000
f_start: 1.e-6
f_min: 1.e-3
f_max: 1.0
pipeline_cfg:
target: hy3dshape.pipelines.Hunyuan3DDiTFlowMatchingPipeline
image_processor_cfg:
target: hy3dshape.preprocessors.ImageProcessorV2
params: {}
callbacks:
logger:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalASLDiffuserLogger
params:
step_frequency: 100 # 10000
num_samples: 1
sample_times: 1
mean: *mean
std: *std
bounds: [-1.01, -1.01, -1.01, 1.01, 1.01, 1.01]
octree_depth: 8
num_chunks: 50000
mc_level: 0.0
file_loggers:
target: hy3dshape.utils.trainings.mesh_log_callback.ImageConditionalFixASLDiffuserLogger
params:
step_frequency: 50 # 5000
test_data_path: "tools/mini_testset/images.json"
@@ -0,0 +1,21 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from .pipelines import Hunyuan3DDiTPipeline, Hunyuan3DDiTFlowMatchingPipeline
from .postprocessors import FaceReducer, FloaterRemover, DegenerateFaceRemover, MeshSimplifier
from .preprocessors import ImageProcessorV2, IMAGE_PROCESSORS, DEFAULT_IMAGEPROCESSOR
import os
import sys
ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, os.path.join(ROOT_PATH, 'Gen_3D_Modules/Hunyuan3D_2_1/hy3dshape'))
@@ -0,0 +1,384 @@
# -*- coding: utf-8 -*-
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
import io
import sys
import time
import random
import traceback
from typing import Optional, Union, List, Tuple, Dict
import json
import glob
import cv2
import numpy as np
import trimesh
import torch
import torchvision.transforms as transforms
from pytorch_lightning import LightningDataModule
from pytorch_lightning.utilities import rank_zero_info
from .utils import worker_init_fn, pytorch_worker_seed, make_seed
class ResampledShards(torch.utils.data.dataset.IterableDataset):
def __init__(self, datalist, nshards=sys.maxsize, worker_seed=None, deterministic=False):
super().__init__()
self.datalist = datalist
self.nshards = nshards
# If no worker_seed provided, use pytorch_worker_seed function; else use given seed
self.worker_seed = pytorch_worker_seed if worker_seed is None else worker_seed
self.deterministic = deterministic
self.epoch = -1
def __iter__(self):
self.epoch += 1
if self.deterministic:
seed = make_seed(self.worker_seed(), self.epoch)
else:
seed = make_seed(self.worker_seed(), self.epoch,
os.getpid(), time.time_ns(), os.urandom(4))
self.rng = random.Random(seed)
for _ in range(self.nshards):
index = self.rng.randint(0, len(self.datalist) - 1)
yield self.datalist[index]
def read_npz(data):
# Load a numpy .npz file from a file path or file-like object
# The commented line shows how to load from bytes in memory
# return np.load(io.BytesIO(data))
return np.load(data)
def read_json(path):
# Read and parse a JSON file from the given file path
with open(path, 'r', encoding='utf-8') as file:
data = json.load(file)
return data
def padding(image, mask, center=True, padding_ratio_range=[1.15, 1.15]):
"""
Pad the input image and mask to a square shape with padding ratio.
Args:
image (np.ndarray): Input image array of shape (H, W, C).
mask (np.ndarray): Corresponding mask array of shape (H, W).
center (bool): Whether to center the original image in the padded output.
padding_ratio_range (list): Range [min, max] to randomly select padding ratio.
Returns:
newimg (np.ndarray): Padded image of shape (resize_side, resize_side, 3).
newmask (np.ndarray): Padded mask of shape (resize_side, resize_side).
"""
h, w = image.shape[:2]
max_side = max(h, w)
# Select padding ratio either fixed or randomly within the given range
if padding_ratio_range[0] == padding_ratio_range[1]:
padding_ratio = padding_ratio_range[0]
else:
padding_ratio = random.uniform(padding_ratio_range[0], padding_ratio_range[1])
resize_side = int(max_side * padding_ratio)
# resize_side = int(max_side * 1.15)
pad_h = resize_side - h
pad_w = resize_side - w
if center:
start_h = pad_h // 2
else:
start_h = pad_h - resize_side // 20
start_w = pad_w // 2
# Create new white image and black mask with padded size
newimg = np.ones((resize_side, resize_side, 3), dtype=np.uint8) * 255
newmask = np.zeros((resize_side, resize_side), dtype=np.uint8)
# Place original image and mask into the padded canvas
newimg[start_h:start_h + h, start_w:start_w + w] = image
newmask[start_h:start_h + h, start_w:start_w + w] = mask
return newimg, newmask
def viz_pc(surface, normal, image_input, name):
image_input = image_input.cpu().numpy()
image_input = image_input.transpose(1, 2, 0) * 0.5 + 0.5
image_input = (image_input * 255).astype(np.uint8)
cv2.imwrite(name + '.png', cv2.cvtColor(image_input, cv2.COLOR_RGB2BGR))
surface = surface.cpu().numpy()
normal = normal.cpu().numpy()
surface_mesh = trimesh.Trimesh(surface, vertex_colors=(normal + 1) / 2)
surface_mesh.export(name + '.obj')
class AlignedShapeLatentDataset(torch.utils.data.dataset.IterableDataset):
def __init__(
self,
data_list: str = None,
cond_stage_key: str = "image",
image_transform = None,
pc_size: int = 2048,
pc_sharpedge_size: int = 2048,
sharpedge_label: bool = False,
return_normal: bool = False,
deterministic = False,
worker_seed = None,
padding = True,
padding_ratio_range=[1.15, 1.15]
):
super().__init__()
if isinstance(data_list, str) and data_list.endswith('.json'):
self.data_list = read_json(data_list_json)
elif isinstance(data_list, str) and os.path.isdir(data_list):
self.data_list = glob.glob(data_list + '/*')
else:
self.data_list = data_list
assert isinstance(self.data_list, list)
self.rng = random.Random(0)
self.cond_stage_key = cond_stage_key
self.image_transform = image_transform
self.pc_size = pc_size
self.pc_sharpedge_size = pc_sharpedge_size
self.sharpedge_label = sharpedge_label
self.return_normal = return_normal
self.padding = padding
self.padding_ratio_range = padding_ratio_range
rank_zero_info(f'*' * 50)
rank_zero_info(f'Dataset Infos:')
rank_zero_info(f'# of 3D file: {len(self.data_list)}')
rank_zero_info(f'# of Surface Points: {self.pc_size}')
rank_zero_info(f'# of Sharpedge Surface Points: {self.pc_sharpedge_size}')
rank_zero_info(f'Using sharp edge label: {self.sharpedge_label}')
rank_zero_info(f'*' * 50)
def load_surface_sdf_points(self, rng, random_surface, sharpedge_surface):
surface_normal = []
if self.pc_size > 0:
ind = rng.choice(random_surface.shape[0], self.pc_size, replace=False)
random_surface = random_surface[ind]
if self.sharpedge_label:
sharpedge_label = np.zeros((self.pc_size, 1))
random_surface = np.concatenate((random_surface, sharpedge_label), axis=1)
surface_normal.append(random_surface)
if self.pc_sharpedge_size > 0:
ind_sharpedge = rng.choice(sharpedge_surface.shape[0], self.pc_sharpedge_size, replace=False)
sharpedge_surface = sharpedge_surface[ind_sharpedge]
if self.sharpedge_label:
sharpedge_label = np.ones((self.pc_sharpedge_size, 1))
sharpedge_surface = np.concatenate((sharpedge_surface, sharpedge_label), axis=1)
surface_normal.append(sharpedge_surface)
surface_normal = np.concatenate(surface_normal, axis=0)
surface_normal = torch.FloatTensor(surface_normal)
surface = surface_normal[:, 0:3]
normal = surface_normal[:, 3:6]
assert surface.shape[0] == self.pc_size + self.pc_sharpedge_size
geo_points = 0.0
normal = torch.nn.functional.normalize(normal, p=2, dim=1)
if self.return_normal:
surface = torch.cat([surface, normal], dim=-1)
if self.sharpedge_label:
surface = torch.cat([surface, surface_normal[:, -1:]], dim=-1)
return surface, geo_points
def load_render(self, imgs_path):
imgs_choice = self.rng.sample(imgs_path, 1)
images, masks = [], []
for image_path in imgs_choice:
image = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
assert image.shape[2] == 4
alpha = image[:, :, 3:4].astype(np.float32) / 255
forground = image[:, :, :3]
background = np.ones_like(forground) * 255
img_new = forground * alpha + background * (1 - alpha)
image = img_new.astype(np.uint8)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
mask = (alpha[:, :, 0] * 255).astype(np.uint8)
if self.padding:
h, w = image.shape[:2]
binary = mask > 0.3
non_zero_coords = np.argwhere(binary)
x_min, y_min = non_zero_coords.min(axis=0)
x_max, y_max = non_zero_coords.max(axis=0)
image, mask = padding(
image[max(x_min - 5, 0):min(x_max + 5, h), max(y_min - 5, 0):min(y_max + 5, w)],
mask[max(x_min - 5, 0):min(x_max + 5, h), max(y_min - 5, 0):min(y_max + 5, w)],
center=True, padding_ratio_range=self.padding_ratio_range)
if self.image_transform:
image = self.image_transform(image)
mask = np.stack((mask, mask, mask), axis=-1)
mask = self.image_transform(mask)
images.append(image)
masks.append(mask)
images = torch.cat(images, dim=0)
masks = torch.cat(masks, dim=0)[:1, ...]
return images, masks
def decode(self, item):
uid = item.split('/')[-1]
render_img_paths = [os.path.join(item, f'render_cond/{i:03d}.png') for i in range(24)]
# transforms_json_path = os.path.join(item, 'render_cond/transforms.json')
surface_npz_path = os.path.join(item, f'geo_data/{uid}_surface.npz')
# sdf_npz_path = os.path.join(item, f'geo_data/{uid}_sdf.npz')
# watertight_obj_path = os.path.join(item, f'geo_data/{uid}_watertight.obj')
sample = {}
sample["image"] = render_img_paths
surface_data = read_npz(surface_npz_path)
sample["random_surface"] = surface_data['random_surface']
sample["sharpedge_surface"] = surface_data['sharp_surface']
return sample
def transform(self, sample):
rng = np.random.default_rng()
random_surface = sample.get("random_surface", 0)
sharpedge_surface = sample.get("sharpedge_surface", 0)
image_input, mask_input = self.load_render(sample['image'])
surface, geo_points = self.load_surface_sdf_points(rng, random_surface, sharpedge_surface)
sample = {
"surface": surface,
"geo_points": geo_points,
"image": image_input,
"mask": mask_input,
}
return sample
def __iter__(self):
total_num = 0
failed_num = 0
for data in ResampledShards(self.data_list):
total_num += 1
if total_num % 1000 == 0:
print(f"Current failure rate of data loading:")
print(f"{failed_num}/{total_num}={failed_num/total_num}")
try:
sample = self.decode(data)
sample = self.transform(sample)
except Exception as err:
print(err)
failed_num += 1
continue
yield sample
class AlignedShapeLatentModule(LightningDataModule):
def __init__(
self,
batch_size: int = 1,
num_workers: int = 4,
val_num_workers: int = 2,
train_data_list: str = None,
val_data_list: str = None,
cond_stage_key: str = "all",
image_size: int = 224,
mean: Union[List[float], Tuple[float]] = (0.485, 0.456, 0.406),
std: Union[List[float], Tuple[float]] = (0.229, 0.224, 0.225),
pc_size: int = 2048,
pc_sharpedge_size: int = 2048,
sharpedge_label: bool = False,
return_normal: bool = False,
padding = True,
padding_ratio_range=[1.15, 1.15]
):
super().__init__()
self.batch_size = batch_size
self.num_workers = num_workers
self.val_num_workers = val_num_workers
self.train_data_list = train_data_list
self.val_data_list = val_data_list
self.cond_stage_key = cond_stage_key
self.image_size = image_size
self.mean = mean
self.std = std
self.train_image_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize(self.image_size),
transforms.Normalize(mean=self.mean, std=self.std)])
self.val_image_transform = transforms.Compose([
transforms.ToTensor(),
transforms.Resize(self.image_size),
transforms.Normalize(mean=self.mean, std=self.std)])
self.pc_size = pc_size
self.pc_sharpedge_size = pc_sharpedge_size
self.sharpedge_label = sharpedge_label
self.return_normal = return_normal
self.padding = padding
self.padding_ratio_range = padding_ratio_range
def train_dataloader(self):
asl_params = {
"data_list": self.train_data_list,
"cond_stage_key": self.cond_stage_key,
"image_transform": self.train_image_transform,
"pc_size": self.pc_size,
"pc_sharpedge_size": self.pc_sharpedge_size,
"sharpedge_label": self.sharpedge_label,
"return_normal": self.return_normal,
"padding": self.padding,
"padding_ratio_range": self.padding_ratio_range
}
dataset = AlignedShapeLatentDataset(**asl_params)
return torch.utils.data.DataLoader(
dataset,
batch_size=self.batch_size,
num_workers=self.num_workers,
pin_memory=True,
drop_last=True,
worker_init_fn=worker_init_fn,
)
def val_dataloader(self):
asl_params = {
"data_list": self.val_data_list,
"cond_stage_key": self.cond_stage_key,
"image_transform": self.val_image_transform,
"pc_size": self.pc_size,
"pc_sharpedge_size": self.pc_sharpedge_size,
"sharpedge_label": self.sharpedge_label,
"return_normal": self.return_normal,
"padding": self.padding,
"padding_ratio_range": self.padding_ratio_range
}
dataset = AlignedShapeLatentDataset(**asl_params)
return torch.utils.data.DataLoader(
dataset,
batch_size=self.batch_size,
num_workers=self.val_num_workers,
pin_memory=True,
drop_last=True,
worker_init_fn=worker_init_fn,
)
@@ -0,0 +1,186 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2017-2021 NVIDIA CORPORATION. All rights reserved.
# This file is part of the WebDataset library.
# See the LICENSE file for licensing terms (BSD-style).
"""Miscellaneous utility functions."""
import importlib
import itertools as itt
import os
import re
import sys
from typing import Any, Callable, Iterator, Union
import torch
import numpy as np
def make_seed(*args):
seed = 0
for arg in args:
seed = (seed * 31 + hash(arg)) & 0x7FFFFFFF
return seed
class PipelineStage:
def invoke(self, *args, **kw):
raise NotImplementedError
def identity(x: Any) -> Any:
"""Return the argument as is."""
return x
def safe_eval(s: str, expr: str = "{}"):
"""Evaluate the given expression more safely."""
if re.sub("[^A-Za-z0-9_]", "", s) != s:
raise ValueError(f"safe_eval: illegal characters in: '{s}'")
return eval(expr.format(s))
def lookup_sym(sym: str, modules: list):
"""Look up a symbol in a list of modules."""
for mname in modules:
module = importlib.import_module(mname, package="webdataset")
result = getattr(module, sym, None)
if result is not None:
return result
return None
def repeatedly0(
loader: Iterator, nepochs: int = sys.maxsize, nbatches: int = sys.maxsize
):
"""Repeatedly returns batches from a DataLoader."""
for _ in range(nepochs):
yield from itt.islice(loader, nbatches)
def guess_batchsize(batch: Union[tuple, list]):
"""Guess the batch size by looking at the length of the first element in a tuple."""
return len(batch[0])
def repeatedly(
source: Iterator,
nepochs: int = None,
nbatches: int = None,
nsamples: int = None,
batchsize: Callable[..., int] = guess_batchsize,
):
"""Repeatedly yield samples from an iterator."""
epoch = 0
batch = 0
total = 0
while True:
for sample in source:
yield sample
batch += 1
if nbatches is not None and batch >= nbatches:
return
if nsamples is not None:
total += guess_batchsize(sample)
if total >= nsamples:
return
epoch += 1
if nepochs is not None and epoch >= nepochs:
return
def pytorch_worker_info(group=None): # sourcery skip: use-contextlib-suppress
"""Return node and worker info for PyTorch and some distributed environments."""
rank = 0
world_size = 1
worker = 0
num_workers = 1
if "RANK" in os.environ and "WORLD_SIZE" in os.environ:
rank = int(os.environ["RANK"])
world_size = int(os.environ["WORLD_SIZE"])
else:
try:
import torch.distributed
if torch.distributed.is_available() and torch.distributed.is_initialized():
group = group or torch.distributed.group.WORLD
rank = torch.distributed.get_rank(group=group)
world_size = torch.distributed.get_world_size(group=group)
except ModuleNotFoundError:
pass
if "WORKER" in os.environ and "NUM_WORKERS" in os.environ:
worker = int(os.environ["WORKER"])
num_workers = int(os.environ["NUM_WORKERS"])
else:
try:
import torch.utils.data
worker_info = torch.utils.data.get_worker_info()
if worker_info is not None:
worker = worker_info.id
num_workers = worker_info.num_workers
except ModuleNotFoundError:
pass
return rank, world_size, worker, num_workers
def pytorch_worker_seed(group=None):
"""Compute a distinct, deterministic RNG seed for each worker and node."""
rank, world_size, worker, num_workers = pytorch_worker_info(group=group)
return rank * 1000 + worker
def worker_init_fn(_):
worker_info = torch.utils.data.get_worker_info()
worker_id = worker_info.id
# dataset = worker_info.dataset
# split_size = dataset.num_records // worker_info.num_workers
# # reset num_records to the true number to retain reliable length information
# dataset.sample_ids = dataset.valid_ids[worker_id * split_size:(worker_id + 1) * split_size]
# current_id = np.random.choice(len(np.random.get_state()[1]), 1)
# return np.random.seed(np.random.get_state()[1][current_id] + worker_id)
return np.random.seed(np.random.get_state()[1][0] + worker_id)
def collation_fn(samples, combine_tensors=True, combine_scalars=True):
"""
Args:
samples (list[dict]):
combine_tensors:
combine_scalars:
Returns:
"""
result = {}
keys = samples[0].keys()
for key in keys:
result[key] = []
for sample in samples:
for key in keys:
val = sample[key]
result[key].append(val)
for key in keys:
val_list = result[key]
if isinstance(val_list[0], (int, float)):
if combine_scalars:
result[key] = np.array(result[key])
elif isinstance(val_list[0], torch.Tensor):
if combine_tensors:
result[key] = torch.stack(val_list)
elif isinstance(val_list[0], np.ndarray):
if combine_tensors:
result[key] = np.stack(val_list)
return result
@@ -0,0 +1,28 @@
# Open Source Model Licensed under the Apache License Version 2.0
# and Other Licenses of the Third-Party Components therein:
# The below Model in this distribution may have been modified by THL A29 Limited
# ("Tencent Modifications"). All Tencent Modifications are Copyright (C) 2024 THL A29 Limited.
# Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved.
# The below software and/or models in this distribution may have been
# modified by THL A29 Limited ("Tencent Modifications").
# All Tencent Modifications are Copyright (C) THL A29 Limited.
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from .autoencoders import ShapeVAE
from .conditioner import DualImageEncoder, SingleImageEncoder, DinoImageEncoder, CLIPImageEncoder
from .denoisers import Hunyuan3DDiT
@@ -0,0 +1,20 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from .attention_blocks import CrossAttentionDecoder
from .attention_processors import FlashVDMCrossAttentionProcessor, CrossAttentionProcessor, \
FlashVDMTopMCrossAttentionProcessor
from .model import ShapeVAE, VectsetVAE
from .surface_extractors import SurfaceExtractors, MCSurfaceExtractor, DMCSurfaceExtractor, Latent2MeshOutput
from .volume_decoders import HierarchicalVolumeDecoding, FlashVDMVolumeDecoding, VanillaVolumeDecoder
@@ -0,0 +1,716 @@
# Open Source Model Licensed under the Apache License Version 2.0
# and Other Licenses of the Third-Party Components therein:
# The below Model in this distribution may have been modified by THL A29 Limited
# ("Tencent Modifications"). All Tencent Modifications are Copyright (C) 2024 THL A29 Limited.
# Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved.
# The below software and/or models in this distribution may have been
# modified by THL A29 Limited ("Tencent Modifications").
# All Tencent Modifications are Copyright (C) THL A29 Limited.
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
from typing import Optional, Union, List
import torch
import torch.nn as nn
from einops import rearrange
from torch import Tensor
from .attention_processors import CrossAttentionProcessor
from ...utils import logger
scaled_dot_product_attention = nn.functional.scaled_dot_product_attention
if os.environ.get('USE_SAGEATTN', '0') == '1':
try:
from sageattention import sageattn
except ImportError:
raise ImportError('Please install the package "sageattention" to use this USE_SAGEATTN.')
scaled_dot_product_attention = sageattn
class FourierEmbedder(nn.Module):
"""The sin/cosine positional embedding. Given an input tensor `x` of shape [n_batch, ..., c_dim], it converts
each feature dimension of `x[..., i]` into:
[
sin(x[..., i]),
sin(f_1*x[..., i]),
sin(f_2*x[..., i]),
...
sin(f_N * x[..., i]),
cos(x[..., i]),
cos(f_1*x[..., i]),
cos(f_2*x[..., i]),
...
cos(f_N * x[..., i]),
x[..., i] # only present if include_input is True.
], here f_i is the frequency.
Denote the space is [0 / num_freqs, 1 / num_freqs, 2 / num_freqs, 3 / num_freqs, ..., (num_freqs - 1) / num_freqs].
If logspace is True, then the frequency f_i is [2^(0 / num_freqs), ..., 2^(i / num_freqs), ...];
Otherwise, the frequencies are linearly spaced between [1.0, 2^(num_freqs - 1)].
Args:
num_freqs (int): the number of frequencies, default is 6;
logspace (bool): If logspace is True, then the frequency f_i is [..., 2^(i / num_freqs), ...],
otherwise, the frequencies are linearly spaced between [1.0, 2^(num_freqs - 1)];
input_dim (int): the input dimension, default is 3;
include_input (bool): include the input tensor or not, default is True.
Attributes:
frequencies (torch.Tensor): If logspace is True, then the frequency f_i is [..., 2^(i / num_freqs), ...],
otherwise, the frequencies are linearly spaced between [1.0, 2^(num_freqs - 1);
out_dim (int): the embedding size, if include_input is True, it is input_dim * (num_freqs * 2 + 1),
otherwise, it is input_dim * num_freqs * 2.
"""
def __init__(self,
num_freqs: int = 6,
logspace: bool = True,
input_dim: int = 3,
include_input: bool = True,
include_pi: bool = True) -> None:
"""The initialization"""
super().__init__()
if logspace:
frequencies = 2.0 ** torch.arange(
num_freqs,
dtype=torch.float32
)
else:
frequencies = torch.linspace(
1.0,
2.0 ** (num_freqs - 1),
num_freqs,
dtype=torch.float32
)
if include_pi:
frequencies *= torch.pi
self.register_buffer("frequencies", frequencies, persistent=False)
self.include_input = include_input
self.num_freqs = num_freqs
self.out_dim = self.get_dims(input_dim)
def get_dims(self, input_dim):
temp = 1 if self.include_input or self.num_freqs == 0 else 0
out_dim = input_dim * (self.num_freqs * 2 + temp)
return out_dim
def forward(self, x: torch.Tensor) -> torch.Tensor:
""" Forward process.
Args:
x: tensor of shape [..., dim]
Returns:
embedding: an embedding of `x` of shape [..., dim * (num_freqs * 2 + temp)]
where temp is 1 if include_input is True and 0 otherwise.
"""
if self.num_freqs > 0:
embed = (x[..., None].contiguous() * self.frequencies).view(*x.shape[:-1], -1)
if self.include_input:
return torch.cat((x, embed.sin(), embed.cos()), dim=-1)
else:
return torch.cat((embed.sin(), embed.cos()), dim=-1)
else:
return x
class DropPath(nn.Module):
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
"""
def __init__(self, drop_prob: float = 0., scale_by_keep: bool = True):
super(DropPath, self).__init__()
self.drop_prob = drop_prob
self.scale_by_keep = scale_by_keep
def forward(self, x):
"""Drop paths (Stochastic Depth) per sample (when applied in main path of residual blocks).
This is the same as the DropConnect impl I created for EfficientNet, etc networks, however,
the original name is misleading as 'Drop Connect' is a different form of dropout in a separate paper...
See discussion: https://github.com/tensorflow/tpu/issues/494#issuecomment-532968956 ... I've opted for
changing the layer and argument names to 'drop path' rather than mix DropConnect as a layer name and use
'survival rate' as the argument.
"""
if self.drop_prob == 0. or not self.training:
return x
keep_prob = 1 - self.drop_prob
shape = (x.shape[0],) + (1,) * (x.ndim - 1) # work with diff dim tensors, not just 2D ConvNets
random_tensor = x.new_empty(shape).bernoulli_(keep_prob)
if keep_prob > 0.0 and self.scale_by_keep:
random_tensor.div_(keep_prob)
return x * random_tensor
def extra_repr(self):
return f'drop_prob={round(self.drop_prob, 3):0.3f}'
class MLP(nn.Module):
def __init__(
self, *,
width: int,
expand_ratio: int = 4,
output_width: int = None,
drop_path_rate: float = 0.0
):
super().__init__()
self.width = width
self.c_fc = nn.Linear(width, width * expand_ratio)
self.c_proj = nn.Linear(width * expand_ratio, output_width if output_width is not None else width)
self.gelu = nn.GELU()
self.drop_path = DropPath(drop_path_rate) if drop_path_rate > 0. else nn.Identity()
def forward(self, x):
return self.drop_path(self.c_proj(self.gelu(self.c_fc(x))))
class QKVMultiheadCrossAttention(nn.Module):
def __init__(
self,
*,
heads: int,
n_data: Optional[int] = None,
width=None,
qk_norm=False,
norm_layer=nn.LayerNorm
):
super().__init__()
self.heads = heads
self.n_data = n_data
self.q_norm = norm_layer(width // heads, elementwise_affine=True, eps=1e-6) if qk_norm else nn.Identity()
self.k_norm = norm_layer(width // heads, elementwise_affine=True, eps=1e-6) if qk_norm else nn.Identity()
self.attn_processor = CrossAttentionProcessor()
def forward(self, q, kv):
_, n_ctx, _ = q.shape
bs, n_data, width = kv.shape
attn_ch = width // self.heads // 2
q = q.view(bs, n_ctx, self.heads, -1)
kv = kv.view(bs, n_data, self.heads, -1)
k, v = torch.split(kv, attn_ch, dim=-1)
q = self.q_norm(q)
k = self.k_norm(k)
q, k, v = map(lambda t: rearrange(t, 'b n h d -> b h n d', h=self.heads), (q, k, v))
out = self.attn_processor(self, q, k, v)
out = out.transpose(1, 2).reshape(bs, n_ctx, -1)
return out
class MultiheadCrossAttention(nn.Module):
def __init__(
self,
*,
width: int,
heads: int,
qkv_bias: bool = True,
n_data: Optional[int] = None,
data_width: Optional[int] = None,
norm_layer=nn.LayerNorm,
qk_norm: bool = False,
kv_cache: bool = False,
):
super().__init__()
self.n_data = n_data
self.width = width
self.heads = heads
self.data_width = width if data_width is None else data_width
self.c_q = nn.Linear(width, width, bias=qkv_bias)
self.c_kv = nn.Linear(self.data_width, width * 2, bias=qkv_bias)
self.c_proj = nn.Linear(width, width)
self.attention = QKVMultiheadCrossAttention(
heads=heads,
n_data=n_data,
width=width,
norm_layer=norm_layer,
qk_norm=qk_norm
)
self.kv_cache = kv_cache
self.data = None
def forward(self, x, data):
x = self.c_q(x)
if self.kv_cache:
if self.data is None:
self.data = self.c_kv(data)
logger.info('Save kv cache,this should be called only once for one mesh')
data = self.data
else:
data = self.c_kv(data)
x = self.attention(x, data)
x = self.c_proj(x)
return x
class ResidualCrossAttentionBlock(nn.Module):
def __init__(
self,
*,
n_data: Optional[int] = None,
width: int,
heads: int,
mlp_expand_ratio: int = 4,
data_width: Optional[int] = None,
qkv_bias: bool = True,
norm_layer=nn.LayerNorm,
qk_norm: bool = False
):
super().__init__()
if data_width is None:
data_width = width
self.attn = MultiheadCrossAttention(
n_data=n_data,
width=width,
heads=heads,
data_width=data_width,
qkv_bias=qkv_bias,
norm_layer=norm_layer,
qk_norm=qk_norm
)
self.ln_1 = norm_layer(width, elementwise_affine=True, eps=1e-6)
self.ln_2 = norm_layer(data_width, elementwise_affine=True, eps=1e-6)
self.ln_3 = norm_layer(width, elementwise_affine=True, eps=1e-6)
self.mlp = MLP(width=width, expand_ratio=mlp_expand_ratio)
def forward(self, x: torch.Tensor, data: torch.Tensor):
x = x + self.attn(self.ln_1(x), self.ln_2(data))
x = x + self.mlp(self.ln_3(x))
return x
class QKVMultiheadAttention(nn.Module):
def __init__(
self,
*,
heads: int,
n_ctx: int,
width=None,
qk_norm=False,
norm_layer=nn.LayerNorm
):
super().__init__()
self.heads = heads
self.n_ctx = n_ctx
self.q_norm = norm_layer(width // heads, elementwise_affine=True, eps=1e-6) if qk_norm else nn.Identity()
self.k_norm = norm_layer(width // heads, elementwise_affine=True, eps=1e-6) if qk_norm else nn.Identity()
def forward(self, qkv):
bs, n_ctx, width = qkv.shape
attn_ch = width // self.heads // 3
qkv = qkv.view(bs, n_ctx, self.heads, -1)
q, k, v = torch.split(qkv, attn_ch, dim=-1)
q = self.q_norm(q)
k = self.k_norm(k)
q, k, v = map(lambda t: rearrange(t, 'b n h d -> b h n d', h=self.heads), (q, k, v))
out = scaled_dot_product_attention(q, k, v).transpose(1, 2).reshape(bs, n_ctx, -1)
return out
class MultiheadAttention(nn.Module):
def __init__(
self,
*,
n_ctx: int,
width: int,
heads: int,
qkv_bias: bool,
norm_layer=nn.LayerNorm,
qk_norm: bool = False,
drop_path_rate: float = 0.0
):
super().__init__()
self.n_ctx = n_ctx
self.width = width
self.heads = heads
self.c_qkv = nn.Linear(width, width * 3, bias=qkv_bias)
self.c_proj = nn.Linear(width, width)
self.attention = QKVMultiheadAttention(
heads=heads,
n_ctx=n_ctx,
width=width,
norm_layer=norm_layer,
qk_norm=qk_norm
)
self.drop_path = DropPath(drop_path_rate) if drop_path_rate > 0. else nn.Identity()
def forward(self, x):
x = self.c_qkv(x)
x = self.attention(x)
x = self.drop_path(self.c_proj(x))
return x
class ResidualAttentionBlock(nn.Module):
def __init__(
self,
*,
n_ctx: int,
width: int,
heads: int,
qkv_bias: bool = True,
norm_layer=nn.LayerNorm,
qk_norm: bool = False,
drop_path_rate: float = 0.0,
):
super().__init__()
self.attn = MultiheadAttention(
n_ctx=n_ctx,
width=width,
heads=heads,
qkv_bias=qkv_bias,
norm_layer=norm_layer,
qk_norm=qk_norm,
drop_path_rate=drop_path_rate
)
self.ln_1 = norm_layer(width, elementwise_affine=True, eps=1e-6)
self.mlp = MLP(width=width, drop_path_rate=drop_path_rate)
self.ln_2 = norm_layer(width, elementwise_affine=True, eps=1e-6)
def forward(self, x: torch.Tensor):
x = x + self.attn(self.ln_1(x))
x = x + self.mlp(self.ln_2(x))
return x
class Transformer(nn.Module):
def __init__(
self,
*,
n_ctx: int,
width: int,
layers: int,
heads: int,
qkv_bias: bool = True,
norm_layer=nn.LayerNorm,
qk_norm: bool = False,
drop_path_rate: float = 0.0
):
super().__init__()
self.n_ctx = n_ctx
self.width = width
self.layers = layers
self.resblocks = nn.ModuleList(
[
ResidualAttentionBlock(
n_ctx=n_ctx,
width=width,
heads=heads,
qkv_bias=qkv_bias,
norm_layer=norm_layer,
qk_norm=qk_norm,
drop_path_rate=drop_path_rate
)
for _ in range(layers)
]
)
def forward(self, x: torch.Tensor):
for block in self.resblocks:
x = block(x)
return x
class CrossAttentionDecoder(nn.Module):
def __init__(
self,
*,
num_latents: int,
out_channels: int,
fourier_embedder: FourierEmbedder,
width: int,
heads: int,
mlp_expand_ratio: int = 4,
downsample_ratio: int = 1,
enable_ln_post: bool = True,
qkv_bias: bool = True,
qk_norm: bool = False,
label_type: str = "binary"
):
super().__init__()
self.enable_ln_post = enable_ln_post
self.fourier_embedder = fourier_embedder
self.downsample_ratio = downsample_ratio
self.query_proj = nn.Linear(self.fourier_embedder.out_dim, width)
if self.downsample_ratio != 1:
self.latents_proj = nn.Linear(width * downsample_ratio, width)
if self.enable_ln_post == False:
qk_norm = False
self.cross_attn_decoder = ResidualCrossAttentionBlock(
n_data=num_latents,
width=width,
mlp_expand_ratio=mlp_expand_ratio,
heads=heads,
qkv_bias=qkv_bias,
qk_norm=qk_norm
)
if self.enable_ln_post:
self.ln_post = nn.LayerNorm(width)
self.output_proj = nn.Linear(width, out_channels)
self.label_type = label_type
self.count = 0
def set_cross_attention_processor(self, processor):
self.cross_attn_decoder.attn.attention.attn_processor = processor
def set_default_cross_attention_processor(self):
self.cross_attn_decoder.attn.attention.attn_processor = CrossAttentionProcessor
def forward(self, queries=None, query_embeddings=None, latents=None):
if query_embeddings is None:
query_embeddings = self.query_proj(self.fourier_embedder(queries).to(latents.dtype))
self.count += query_embeddings.shape[1]
if self.downsample_ratio != 1:
latents = self.latents_proj(latents)
x = self.cross_attn_decoder(query_embeddings, latents)
if self.enable_ln_post:
x = self.ln_post(x)
occ = self.output_proj(x)
return occ
def fps(
src: torch.Tensor,
batch: Optional[Tensor] = None,
ratio: Optional[Union[Tensor, float]] = None,
random_start: bool = True,
batch_size: Optional[int] = None,
ptr: Optional[Union[Tensor, List[int]]] = None,
):
src = src.float()
from torch_cluster import fps as fps_fn
output = fps_fn(src, batch, ratio, random_start, batch_size, ptr)
return output
class PointCrossAttentionEncoder(nn.Module):
def __init__(
self, *,
num_latents: int,
downsample_ratio: float,
pc_size: int,
pc_sharpedge_size: int,
fourier_embedder: FourierEmbedder,
point_feats: int,
width: int,
heads: int,
layers: int,
normal_pe: bool = False,
qkv_bias: bool = True,
use_ln_post: bool = False,
use_checkpoint: bool = False,
qk_norm: bool = False
):
super().__init__()
self.use_checkpoint = use_checkpoint
self.num_latents = num_latents
self.downsample_ratio = downsample_ratio
self.point_feats = point_feats
self.normal_pe = normal_pe
if pc_sharpedge_size == 0:
print(
f'PointCrossAttentionEncoder INFO: pc_sharpedge_size is not given, using pc_size as pc_sharpedge_size')
else:
print(
f'PointCrossAttentionEncoder INFO: pc_sharpedge_size is given, using pc_size={pc_size}, pc_sharpedge_size={pc_sharpedge_size}')
self.pc_size = pc_size
self.pc_sharpedge_size = pc_sharpedge_size
self.fourier_embedder = fourier_embedder
self.input_proj = nn.Linear(self.fourier_embedder.out_dim + point_feats, width)
self.cross_attn = ResidualCrossAttentionBlock(
width=width,
heads=heads,
qkv_bias=qkv_bias,
qk_norm=qk_norm
)
self.self_attn = None
if layers > 0:
self.self_attn = Transformer(
n_ctx=num_latents,
width=width,
layers=layers,
heads=heads,
qkv_bias=qkv_bias,
qk_norm=qk_norm
)
if use_ln_post:
self.ln_post = nn.LayerNorm(width)
else:
self.ln_post = None
def sample_points_and_latents(self, pc: torch.FloatTensor, feats: Optional[torch.FloatTensor] = None):
B, N, D = pc.shape
num_pts = self.num_latents * self.downsample_ratio
# Compute number of latents
num_latents = int(num_pts / self.downsample_ratio)
# Compute the number of random and sharpedge latents
num_random_query = self.pc_size / (self.pc_size + self.pc_sharpedge_size) * num_latents
num_sharpedge_query = num_latents - num_random_query
# Split random and sharpedge surface points
random_pc, sharpedge_pc = torch.split(pc, [self.pc_size, self.pc_sharpedge_size], dim=1)
assert random_pc.shape[1] <= self.pc_size, "Random surface points size must be less than or equal to pc_size"
assert sharpedge_pc.shape[
1] <= self.pc_sharpedge_size, "Sharpedge surface points size must be less than or equal to pc_sharpedge_size"
# Randomly select random surface points and random query points
input_random_pc_size = int(num_random_query * self.downsample_ratio)
random_query_ratio = num_random_query / input_random_pc_size
idx_random_pc = torch.randperm(random_pc.shape[1], device=random_pc.device)[:input_random_pc_size]
input_random_pc = random_pc[:, idx_random_pc, :]
flatten_input_random_pc = input_random_pc.view(B * input_random_pc_size, D)
N_down = int(flatten_input_random_pc.shape[0] / B)
batch_down = torch.arange(B).to(pc.device)
batch_down = torch.repeat_interleave(batch_down, N_down)
idx_query_random = fps(flatten_input_random_pc, batch_down, ratio=random_query_ratio)
query_random_pc = flatten_input_random_pc[idx_query_random].view(B, -1, D)
# Randomly select sharpedge surface points and sharpedge query points
input_sharpedge_pc_size = int(num_sharpedge_query * self.downsample_ratio)
if input_sharpedge_pc_size == 0:
input_sharpedge_pc = torch.zeros(B, 0, D, dtype=input_random_pc.dtype).to(pc.device)
query_sharpedge_pc = torch.zeros(B, 0, D, dtype=query_random_pc.dtype).to(pc.device)
else:
sharpedge_query_ratio = num_sharpedge_query / input_sharpedge_pc_size
idx_sharpedge_pc = torch.randperm(sharpedge_pc.shape[1], device=sharpedge_pc.device)[
:input_sharpedge_pc_size]
input_sharpedge_pc = sharpedge_pc[:, idx_sharpedge_pc, :]
flatten_input_sharpedge_surface_points = input_sharpedge_pc.view(B * input_sharpedge_pc_size, D)
N_down = int(flatten_input_sharpedge_surface_points.shape[0] / B)
batch_down = torch.arange(B).to(pc.device)
batch_down = torch.repeat_interleave(batch_down, N_down)
idx_query_sharpedge = fps(flatten_input_sharpedge_surface_points, batch_down, ratio=sharpedge_query_ratio)
query_sharpedge_pc = flatten_input_sharpedge_surface_points[idx_query_sharpedge].view(B, -1, D)
# Concatenate random and sharpedge surface points and query points
query_pc = torch.cat([query_random_pc, query_sharpedge_pc], dim=1)
input_pc = torch.cat([input_random_pc, input_sharpedge_pc], dim=1)
# PE
query = self.fourier_embedder(query_pc)
data = self.fourier_embedder(input_pc)
# Concat normal if given
if self.point_feats != 0:
random_surface_feats, sharpedge_surface_feats = torch.split(feats, [self.pc_size, self.pc_sharpedge_size],
dim=1)
input_random_surface_feats = random_surface_feats[:, idx_random_pc, :]
flatten_input_random_surface_feats = input_random_surface_feats.view(B * input_random_pc_size, -1)
query_random_feats = flatten_input_random_surface_feats[idx_query_random].view(B, -1,
flatten_input_random_surface_feats.shape[
-1])
if input_sharpedge_pc_size == 0:
input_sharpedge_surface_feats = torch.zeros(B, 0, self.point_feats,
dtype=input_random_surface_feats.dtype).to(pc.device)
query_sharpedge_feats = torch.zeros(B, 0, self.point_feats, dtype=query_random_feats.dtype).to(
pc.device)
else:
input_sharpedge_surface_feats = sharpedge_surface_feats[:, idx_sharpedge_pc, :]
flatten_input_sharpedge_surface_feats = input_sharpedge_surface_feats.view(B * input_sharpedge_pc_size,
-1)
query_sharpedge_feats = flatten_input_sharpedge_surface_feats[idx_query_sharpedge].view(B, -1,
flatten_input_sharpedge_surface_feats.shape[
-1])
query_feats = torch.cat([query_random_feats, query_sharpedge_feats], dim=1)
input_feats = torch.cat([input_random_surface_feats, input_sharpedge_surface_feats], dim=1)
if self.normal_pe:
query_normal_pe = self.fourier_embedder(query_feats[..., :3])
input_normal_pe = self.fourier_embedder(input_feats[..., :3])
query_feats = torch.cat([query_normal_pe, query_feats[..., 3:]], dim=-1)
input_feats = torch.cat([input_normal_pe, input_feats[..., 3:]], dim=-1)
query = torch.cat([query, query_feats], dim=-1)
data = torch.cat([data, input_feats], dim=-1)
if input_sharpedge_pc_size == 0:
query_sharpedge_pc = torch.zeros(B, 1, D).to(pc.device)
input_sharpedge_pc = torch.zeros(B, 1, D).to(pc.device)
# print(f'query_pc: {query_pc.shape}')
# print(f'input_pc: {input_pc.shape}')
# print(f'query_random_pc: {query_random_pc.shape}')
# print(f'input_random_pc: {input_random_pc.shape}')
# print(f'query_sharpedge_pc: {query_sharpedge_pc.shape}')
# print(f'input_sharpedge_pc: {input_sharpedge_pc.shape}')
return query.view(B, -1, query.shape[-1]), data.view(B, -1, data.shape[-1]), [query_pc, input_pc,
query_random_pc, input_random_pc,
query_sharpedge_pc,
input_sharpedge_pc]
def forward(self, pc, feats):
"""
Args:
pc (torch.FloatTensor): [B, N, 3]
feats (torch.FloatTensor or None): [B, N, C]
Returns:
"""
query, data, pc_infos = self.sample_points_and_latents(pc, feats)
query = self.input_proj(query)
query = query
data = self.input_proj(data)
data = data
latents = self.cross_attn(query, data)
if self.self_attn is not None:
latents = self.self_attn(latents)
if self.ln_post is not None:
latents = self.ln_post(latents)
return latents, pc_infos
@@ -0,0 +1,96 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
import torch
import torch.nn.functional as F
scaled_dot_product_attention = F.scaled_dot_product_attention
if os.environ.get('CA_USE_SAGEATTN', '0') == '1':
try:
from sageattention import sageattn
except ImportError:
raise ImportError('Please install the package "sageattention" to use this USE_SAGEATTN.')
scaled_dot_product_attention = sageattn
class CrossAttentionProcessor:
def __call__(self, attn, q, k, v):
out = scaled_dot_product_attention(q, k, v)
return out
class FlashVDMCrossAttentionProcessor:
def __init__(self, topk=None):
self.topk = topk
def __call__(self, attn, q, k, v):
if k.shape[-2] == 3072:
topk = 1024
elif k.shape[-2] == 512:
topk = 256
else:
topk = k.shape[-2] // 3
if self.topk is True:
q1 = q[:, :, ::100, :]
sim = q1 @ k.transpose(-1, -2)
sim = torch.mean(sim, -2)
topk_ind = torch.topk(sim, dim=-1, k=topk).indices.squeeze(-2).unsqueeze(-1)
topk_ind = topk_ind.expand(-1, -1, -1, v.shape[-1])
v0 = torch.gather(v, dim=-2, index=topk_ind)
k0 = torch.gather(k, dim=-2, index=topk_ind)
out = scaled_dot_product_attention(q, k0, v0)
elif self.topk is False:
out = scaled_dot_product_attention(q, k, v)
else:
idx, counts = self.topk
start = 0
outs = []
for grid_coord, count in zip(idx, counts):
end = start + count
q_chunk = q[:, :, start:end, :]
k0, v0 = self.select_topkv(q_chunk, k, v, topk)
out = scaled_dot_product_attention(q_chunk, k0, v0)
outs.append(out)
start += count
out = torch.cat(outs, dim=-2)
self.topk = False
return out
def select_topkv(self, q_chunk, k, v, topk):
q1 = q_chunk[:, :, ::50, :]
sim = q1 @ k.transpose(-1, -2)
sim = torch.mean(sim, -2)
topk_ind = torch.topk(sim, dim=-1, k=topk).indices.squeeze(-2).unsqueeze(-1)
topk_ind = topk_ind.expand(-1, -1, -1, v.shape[-1])
v0 = torch.gather(v, dim=-2, index=topk_ind)
k0 = torch.gather(k, dim=-2, index=topk_ind)
return k0, v0
class FlashVDMTopMCrossAttentionProcessor(FlashVDMCrossAttentionProcessor):
def select_topkv(self, q_chunk, k, v, topk):
q1 = q_chunk[:, :, ::30, :]
sim = q1 @ k.transpose(-1, -2)
# sim = sim.to(torch.float32)
sim = sim.softmax(-1)
sim = torch.mean(sim, 1)
activated_token = torch.where(sim > 1e-6)[2]
index = torch.unique(activated_token, return_counts=True)[0].unsqueeze(0).unsqueeze(0).unsqueeze(-1)
index = index.expand(-1, v.shape[1], -1, v.shape[-1])
v0 = torch.gather(v, dim=-2, index=index)
k0 = torch.gather(k, dim=-2, index=index)
return k0, v0
@@ -0,0 +1,339 @@
# Open Source Model Licensed under the Apache License Version 2.0
# and Other Licenses of the Third-Party Components therein:
# The below Model in this distribution may have been modified by THL A29 Limited
# ("Tencent Modifications"). All Tencent Modifications are Copyright (C) 2024 THL A29 Limited.
# Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved.
# The below software and/or models in this distribution may have been
# modified by THL A29 Limited ("Tencent Modifications").
# All Tencent Modifications are Copyright (C) THL A29 Limited.
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
from typing import Union, List
import numpy as np
import torch
import torch.nn as nn
import yaml
from .attention_blocks import FourierEmbedder, Transformer, CrossAttentionDecoder, PointCrossAttentionEncoder
from .surface_extractors import MCSurfaceExtractor, SurfaceExtractors
from .volume_decoders import VanillaVolumeDecoder, FlashVDMVolumeDecoding, HierarchicalVolumeDecoding
from ...utils import logger, synchronize_timer, smart_load_model
class DiagonalGaussianDistribution(object):
def __init__(self, parameters: Union[torch.Tensor, List[torch.Tensor]], deterministic=False, feat_dim=1):
"""
Initialize a diagonal Gaussian distribution with mean and log-variance parameters.
Args:
parameters (Union[torch.Tensor, List[torch.Tensor]]):
Either a single tensor containing concatenated mean and log-variance along `feat_dim`,
or a list of two tensors [mean, logvar].
deterministic (bool, optional): If True, the distribution is deterministic (zero variance).
Default is False. feat_dim (int, optional): Dimension along which mean and logvar are
concatenated if parameters is a single tensor. Default is 1.
"""
self.feat_dim = feat_dim
self.parameters = parameters
if isinstance(parameters, list):
self.mean = parameters[0]
self.logvar = parameters[1]
else:
self.mean, self.logvar = torch.chunk(parameters, 2, dim=feat_dim)
self.logvar = torch.clamp(self.logvar, -30.0, 20.0)
self.deterministic = deterministic
self.std = torch.exp(0.5 * self.logvar)
self.var = torch.exp(self.logvar)
if self.deterministic:
self.var = self.std = torch.zeros_like(self.mean)
def sample(self):
"""
Sample from the diagonal Gaussian distribution.
Returns:
torch.Tensor: A sample tensor with the same shape as the mean.
"""
x = self.mean + self.std * torch.randn_like(self.mean)
return x
def kl(self, other=None, dims=(1, 2, 3)):
"""
Compute the Kullback-Leibler (KL) divergence between this distribution and another.
If `other` is None, compute KL divergence to a standard normal distribution N(0, I).
Args:
other (DiagonalGaussianDistribution, optional): Another diagonal Gaussian distribution.
dims (tuple, optional): Dimensions along which to compute the mean KL divergence.
Default is (1, 2, 3).
Returns:
torch.Tensor: The mean KL divergence value.
"""
if self.deterministic:
return torch.Tensor([0.])
else:
if other is None:
return 0.5 * torch.mean(torch.pow(self.mean, 2)
+ self.var - 1.0 - self.logvar,
dim=dims)
else:
return 0.5 * torch.mean(
torch.pow(self.mean - other.mean, 2) / other.var
+ self.var / other.var - 1.0 - self.logvar + other.logvar,
dim=dims)
def nll(self, sample, dims=(1, 2, 3)):
if self.deterministic:
return torch.Tensor([0.])
logtwopi = np.log(2.0 * np.pi)
return 0.5 * torch.sum(
logtwopi + self.logvar + torch.pow(sample - self.mean, 2) / self.var,
dim=dims)
def mode(self):
return self.mean
class VectsetVAE(nn.Module):
@classmethod
@synchronize_timer('VectsetVAE Model Loading')
def from_single_file(
cls,
ckpt_path,
config_path,
device='cuda',
dtype=torch.float16,
use_safetensors=None,
**kwargs,
):
# load config
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
# load ckpt
if use_safetensors:
ckpt_path = ckpt_path.replace('.ckpt', '.safetensors')
if not os.path.exists(ckpt_path):
raise FileNotFoundError(f"Model file {ckpt_path} not found")
# logger.info(f"Loading model from {ckpt_path}")
if use_safetensors:
import safetensors.torch
ckpt = safetensors.torch.load_file(ckpt_path, device='cpu')
else:
ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=True)
model_kwargs = config['params']
model_kwargs.update(kwargs)
model = cls(**model_kwargs)
model.load_state_dict(ckpt)
model.to(device=device, dtype=dtype)
return model
@classmethod
def from_pretrained(
cls,
model_path,
device='cuda',
dtype=torch.float16,
use_safetensors=False,
variant='fp16',
subfolder='hunyuan3d-vae-v2-1',
**kwargs,
):
config_path, ckpt_path = smart_load_model(
model_path,
subfolder=subfolder,
use_safetensors=use_safetensors,
variant=variant
)
return cls.from_single_file(
ckpt_path,
config_path,
device=device,
dtype=dtype,
use_safetensors=use_safetensors,
**kwargs
)
def init_from_ckpt(self, path, ignore_keys=()):
state_dict = torch.load(path, map_location="cpu")
state_dict = state_dict.get("state_dict", state_dict)
keys = list(state_dict.keys())
for k in keys:
for ik in ignore_keys:
if k.startswith(ik):
print("Deleting key {} from state_dict.".format(k))
del state_dict[k]
missing, unexpected = self.load_state_dict(state_dict, strict=False)
print(f"Restored from {path} with {len(missing)} missing and {len(unexpected)} unexpected keys")
if len(missing) > 0:
print(f"Missing Keys: {missing}")
print(f"Unexpected Keys: {unexpected}")
def __init__(
self,
volume_decoder=None,
surface_extractor=None
):
super().__init__()
if volume_decoder is None:
volume_decoder = VanillaVolumeDecoder()
if surface_extractor is None:
surface_extractor = MCSurfaceExtractor()
self.volume_decoder = volume_decoder
self.surface_extractor = surface_extractor
def latents2mesh(self, latents: torch.FloatTensor, **kwargs):
with synchronize_timer('Volume decoding'):
grid_logits = self.volume_decoder(latents, self.geo_decoder, **kwargs)
with synchronize_timer('Surface extraction'):
outputs = self.surface_extractor(grid_logits, **kwargs)
return outputs
def enable_flashvdm_decoder(
self,
enabled: bool = True,
adaptive_kv_selection=True,
topk_mode='mean',
mc_algo='dmc',
):
if enabled:
if adaptive_kv_selection:
self.volume_decoder = FlashVDMVolumeDecoding(topk_mode)
else:
self.volume_decoder = HierarchicalVolumeDecoding()
if mc_algo not in SurfaceExtractors.keys():
raise ValueError(f'Unsupported mc_algo {mc_algo}, available:{list(SurfaceExtractors.keys())}')
self.surface_extractor = SurfaceExtractors[mc_algo]()
else:
self.volume_decoder = VanillaVolumeDecoder()
self.surface_extractor = MCSurfaceExtractor()
class ShapeVAE(VectsetVAE):
def __init__(
self,
*,
num_latents: int,
embed_dim: int,
width: int,
heads: int,
num_decoder_layers: int,
num_encoder_layers: int = 8,
pc_size: int = 5120,
pc_sharpedge_size: int = 5120,
point_feats: int = 3,
downsample_ratio: int = 20,
geo_decoder_downsample_ratio: int = 1,
geo_decoder_mlp_expand_ratio: int = 4,
geo_decoder_ln_post: bool = True,
num_freqs: int = 8,
include_pi: bool = True,
qkv_bias: bool = True,
qk_norm: bool = False,
label_type: str = "binary",
drop_path_rate: float = 0.0,
scale_factor: float = 1.0,
use_ln_post: bool = True,
ckpt_path = None
):
super().__init__()
self.geo_decoder_ln_post = geo_decoder_ln_post
self.downsample_ratio = downsample_ratio
self.fourier_embedder = FourierEmbedder(num_freqs=num_freqs, include_pi=include_pi)
self.encoder = PointCrossAttentionEncoder(
fourier_embedder=self.fourier_embedder,
num_latents=num_latents,
downsample_ratio=self.downsample_ratio,
pc_size=pc_size,
pc_sharpedge_size=pc_sharpedge_size,
point_feats=point_feats,
width=width,
heads=heads,
layers=num_encoder_layers,
qkv_bias=qkv_bias,
use_ln_post=use_ln_post,
qk_norm=qk_norm
)
self.pre_kl = nn.Linear(width, embed_dim * 2)
self.post_kl = nn.Linear(embed_dim, width)
self.transformer = Transformer(
n_ctx=num_latents,
width=width,
layers=num_decoder_layers,
heads=heads,
qkv_bias=qkv_bias,
qk_norm=qk_norm,
drop_path_rate=drop_path_rate
)
self.geo_decoder = CrossAttentionDecoder(
fourier_embedder=self.fourier_embedder,
out_channels=1,
num_latents=num_latents,
mlp_expand_ratio=geo_decoder_mlp_expand_ratio,
downsample_ratio=geo_decoder_downsample_ratio,
enable_ln_post=self.geo_decoder_ln_post,
width=width // geo_decoder_downsample_ratio,
heads=heads // geo_decoder_downsample_ratio,
qkv_bias=qkv_bias,
qk_norm=qk_norm,
label_type=label_type,
)
self.scale_factor = scale_factor
self.latent_shape = (num_latents, embed_dim)
if ckpt_path is not None:
self.init_from_ckpt(ckpt_path)
def forward(self, latents):
latents = self.post_kl(latents)
latents = self.transformer(latents)
return latents
def encode(self, surface, sample_posterior=True):
pc, feats = surface[:, :, :3], surface[:, :, 3:]
latents, _ = self.encoder(pc, feats)
# print(latents.shape, self.pre_kl.weight.shape)
moments = self.pre_kl(latents)
posterior = DiagonalGaussianDistribution(moments, feat_dim=-1)
if sample_posterior:
latents = posterior.sample()
else:
latents = posterior.mode()
return latents
def decode(self, latents):
latents = self.post_kl(latents)
latents = self.transformer(latents)
return latents
@@ -0,0 +1,164 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from typing import Union, Tuple, List
import numpy as np
import torch
from skimage import measure
class Latent2MeshOutput:
def __init__(self, mesh_v=None, mesh_f=None):
self.mesh_v = mesh_v
self.mesh_f = mesh_f
def center_vertices(vertices):
"""Translate the vertices so that bounding box is centered at zero."""
vert_min = vertices.min(dim=0)[0]
vert_max = vertices.max(dim=0)[0]
vert_center = 0.5 * (vert_min + vert_max)
return vertices - vert_center
class SurfaceExtractor:
def _compute_box_stat(self, bounds: Union[Tuple[float], List[float], float], octree_resolution: int):
"""
Compute grid size, bounding box minimum coordinates, and bounding box size based on input
bounds and resolution.
Args:
bounds (Union[Tuple[float], List[float], float]): Bounding box coordinates or a single
float representing half side length.
If float, bounds are assumed symmetric around zero in all axes.
Expected format if list/tuple: [xmin, ymin, zmin, xmax, ymax, zmax].
octree_resolution (int): Resolution of the octree grid.
Returns:
grid_size (List[int]): Grid size along each axis (x, y, z), each equal to octree_resolution + 1.
bbox_min (np.ndarray): Minimum coordinates of the bounding box (xmin, ymin, zmin).
bbox_size (np.ndarray): Size of the bounding box along each axis (xmax - xmin, etc.).
"""
if isinstance(bounds, float):
bounds = [-bounds, -bounds, -bounds, bounds, bounds, bounds]
bbox_min, bbox_max = np.array(bounds[0:3]), np.array(bounds[3:6])
bbox_size = bbox_max - bbox_min
grid_size = [int(octree_resolution) + 1, int(octree_resolution) + 1, int(octree_resolution) + 1]
return grid_size, bbox_min, bbox_size
def run(self, *args, **kwargs):
"""
Abstract method to extract surface mesh from grid logits.
This method should be implemented by subclasses.
Raises:
NotImplementedError: Always, since this is an abstract method.
"""
return NotImplementedError
def __call__(self, grid_logits, **kwargs):
"""
Process a batch of grid logits to extract surface meshes.
Args:
grid_logits (torch.Tensor): Batch of grid logits with shape (batch_size, ...).
**kwargs: Additional keyword arguments passed to the `run` method.
Returns:
List[Optional[Latent2MeshOutput]]: List of mesh outputs for each grid in the batch.
If extraction fails for a grid, None is appended at that position.
"""
outputs = []
for i in range(grid_logits.shape[0]):
try:
vertices, faces = self.run(grid_logits[i], **kwargs)
vertices = vertices.astype(np.float32)
faces = np.ascontiguousarray(faces)
outputs.append(Latent2MeshOutput(mesh_v=vertices, mesh_f=faces))
except Exception:
import traceback
traceback.print_exc()
outputs.append(None)
return outputs
class MCSurfaceExtractor(SurfaceExtractor):
def run(self, grid_logit, *, mc_level, bounds, octree_resolution, **kwargs):
"""
Extract surface mesh using the Marching Cubes algorithm.
Args:
grid_logit (torch.Tensor): 3D grid logits tensor representing the scalar field.
mc_level (float): The level (iso-value) at which to extract the surface.
bounds (Union[Tuple[float], List[float], float]): Bounding box coordinates or half side length.
octree_resolution (int): Resolution of the octree grid.
**kwargs: Additional keyword arguments (ignored).
Returns:
Tuple[np.ndarray, np.ndarray]: Tuple containing:
- vertices (np.ndarray): Extracted mesh vertices, scaled and translated to bounding
box coordinates.
- faces (np.ndarray): Extracted mesh faces (triangles).
"""
vertices, faces, normals, _ = measure.marching_cubes(grid_logit.cpu().numpy(),
mc_level,
method="lewiner")
grid_size, bbox_min, bbox_size = self._compute_box_stat(bounds, octree_resolution)
vertices = vertices / grid_size * bbox_size + bbox_min
return vertices, faces
class DMCSurfaceExtractor(SurfaceExtractor):
def run(self, grid_logit, *, octree_resolution, **kwargs):
"""
Extract surface mesh using Differentiable Marching Cubes (DMC) algorithm.
Args:
grid_logit (torch.Tensor): 3D grid logits tensor representing the scalar field.
octree_resolution (int): Resolution of the octree grid.
**kwargs: Additional keyword arguments (ignored).
Returns:
Tuple[np.ndarray, np.ndarray]: Tuple containing:
- vertices (np.ndarray): Extracted mesh vertices, centered and converted to numpy.
- faces (np.ndarray): Extracted mesh faces (triangles), with reversed vertex order.
Raises:
ImportError: If the 'diso' package is not installed.
"""
device = grid_logit.device
if not hasattr(self, 'dmc'):
try:
from diso import DiffDMC
self.dmc = DiffDMC(dtype=torch.float32).to(device)
except:
raise ImportError("Please install diso via `pip install diso`, or set mc_algo to 'mc'")
sdf = -grid_logit / octree_resolution
sdf = sdf.to(torch.float32).contiguous()
verts, faces = self.dmc(sdf, deform=None, return_quads=False, normalize=True)
verts = center_vertices(verts)
vertices = verts.detach().cpu().numpy()
faces = faces.detach().cpu().numpy()[:, ::-1]
return vertices, faces
SurfaceExtractors = {
'mc': MCSurfaceExtractor,
'dmc': DMCSurfaceExtractor,
}
@@ -0,0 +1,435 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from typing import Union, Tuple, List, Callable
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import repeat
from tqdm import tqdm
from .attention_blocks import CrossAttentionDecoder
from .attention_processors import FlashVDMCrossAttentionProcessor, FlashVDMTopMCrossAttentionProcessor
from ...utils import logger
def extract_near_surface_volume_fn(input_tensor: torch.Tensor, alpha: float):
device = input_tensor.device
D = input_tensor.shape[0]
signed_val = 0.0
# 添加偏移并处理无效值
val = input_tensor + alpha
valid_mask = val > -9000 # 假设-9000是无效值
# 改进的邻居获取函数(保持维度一致)
def get_neighbor(t, shift, axis):
"""根据指定轴进行位移并保持维度一致"""
if shift == 0:
return t.clone()
# 确定填充轴(输入为[D, D, D]对应z,y,x轴)
pad_dims = [0, 0, 0, 0, 0, 0] # 格式:[x前,x后,y前,y后,z前,z后]
# 根据轴类型设置填充
if axis == 0: # x轴(最后一个维度)
pad_idx = 0 if shift > 0 else 1
pad_dims[pad_idx] = abs(shift)
elif axis == 1: # y轴(中间维度)
pad_idx = 2 if shift > 0 else 3
pad_dims[pad_idx] = abs(shift)
elif axis == 2: # z轴(第一个维度)
pad_idx = 4 if shift > 0 else 5
pad_dims[pad_idx] = abs(shift)
# 执行填充(添加batch和channel维度适配F.pad)
padded = F.pad(t.unsqueeze(0).unsqueeze(0), pad_dims[::-1], mode='replicate') # 反转顺序适配F.pad
# 构建动态切片索引
slice_dims = [slice(None)] * 3 # 初始化为全切片
if axis == 0: # x轴(dim=2)
if shift > 0:
slice_dims[0] = slice(shift, None)
else:
slice_dims[0] = slice(None, shift)
elif axis == 1: # y轴(dim=1)
if shift > 0:
slice_dims[1] = slice(shift, None)
else:
slice_dims[1] = slice(None, shift)
elif axis == 2: # z轴(dim=0)
if shift > 0:
slice_dims[2] = slice(shift, None)
else:
slice_dims[2] = slice(None, shift)
# 应用切片并恢复维度
padded = padded.squeeze(0).squeeze(0)
sliced = padded[slice_dims]
return sliced
# 获取各方向邻居(确保维度一致)
left = get_neighbor(val, 1, axis=0) # x方向
right = get_neighbor(val, -1, axis=0)
back = get_neighbor(val, 1, axis=1) # y方向
front = get_neighbor(val, -1, axis=1)
down = get_neighbor(val, 1, axis=2) # z方向
up = get_neighbor(val, -1, axis=2)
# 处理边界无效值(使用where保持维度一致)
def safe_where(neighbor):
return torch.where(neighbor > -9000, neighbor, val)
left = safe_where(left)
right = safe_where(right)
back = safe_where(back)
front = safe_where(front)
down = safe_where(down)
up = safe_where(up)
# 计算符号一致性(转换为float32确保精度)
sign = torch.sign(val.to(torch.float32))
neighbors_sign = torch.stack([
torch.sign(left.to(torch.float32)),
torch.sign(right.to(torch.float32)),
torch.sign(back.to(torch.float32)),
torch.sign(front.to(torch.float32)),
torch.sign(down.to(torch.float32)),
torch.sign(up.to(torch.float32))
], dim=0)
# 检查所有符号是否一致
same_sign = torch.all(neighbors_sign == sign, dim=0)
# 生成最终掩码
mask = (~same_sign).to(torch.int32)
return mask * valid_mask.to(torch.int32)
def generate_dense_grid_points(
bbox_min: np.ndarray,
bbox_max: np.ndarray,
octree_resolution: int,
indexing: str = "ij",
):
length = bbox_max - bbox_min
num_cells = octree_resolution
x = np.linspace(bbox_min[0], bbox_max[0], int(num_cells) + 1, dtype=np.float32)
y = np.linspace(bbox_min[1], bbox_max[1], int(num_cells) + 1, dtype=np.float32)
z = np.linspace(bbox_min[2], bbox_max[2], int(num_cells) + 1, dtype=np.float32)
[xs, ys, zs] = np.meshgrid(x, y, z, indexing=indexing)
xyz = np.stack((xs, ys, zs), axis=-1)
grid_size = [int(num_cells) + 1, int(num_cells) + 1, int(num_cells) + 1]
return xyz, grid_size, length
class VanillaVolumeDecoder:
@torch.no_grad()
def __call__(
self,
latents: torch.FloatTensor,
geo_decoder: Callable,
bounds: Union[Tuple[float], List[float], float] = 1.01,
num_chunks: int = 10000,
octree_resolution: int = None,
enable_pbar: bool = True,
**kwargs,
):
device = latents.device
dtype = latents.dtype
batch_size = latents.shape[0]
# 1. generate query points
if isinstance(bounds, float):
bounds = [-bounds, -bounds, -bounds, bounds, bounds, bounds]
bbox_min, bbox_max = np.array(bounds[0:3]), np.array(bounds[3:6])
xyz_samples, grid_size, length = generate_dense_grid_points(
bbox_min=bbox_min,
bbox_max=bbox_max,
octree_resolution=octree_resolution,
indexing="ij"
)
xyz_samples = torch.from_numpy(xyz_samples).to(device, dtype=dtype).contiguous().reshape(-1, 3)
# 2. latents to 3d volume
batch_logits = []
for start in tqdm(range(0, xyz_samples.shape[0], num_chunks), desc=f"Volume Decoding",
disable=not enable_pbar):
chunk_queries = xyz_samples[start: start + num_chunks, :]
chunk_queries = repeat(chunk_queries, "p c -> b p c", b=batch_size)
logits = geo_decoder(queries=chunk_queries, latents=latents)
batch_logits.append(logits)
grid_logits = torch.cat(batch_logits, dim=1)
grid_logits = grid_logits.view((batch_size, *grid_size)).float()
return grid_logits
class HierarchicalVolumeDecoding:
@torch.no_grad()
def __call__(
self,
latents: torch.FloatTensor,
geo_decoder: Callable,
bounds: Union[Tuple[float], List[float], float] = 1.01,
num_chunks: int = 10000,
mc_level: float = 0.0,
octree_resolution: int = None,
min_resolution: int = 63,
enable_pbar: bool = True,
**kwargs,
):
device = latents.device
dtype = latents.dtype
resolutions = []
if octree_resolution < min_resolution:
resolutions.append(octree_resolution)
while octree_resolution >= min_resolution:
resolutions.append(octree_resolution)
octree_resolution = octree_resolution // 2
resolutions.reverse()
# 1. generate query points
if isinstance(bounds, float):
bounds = [-bounds, -bounds, -bounds, bounds, bounds, bounds]
bbox_min = np.array(bounds[0:3])
bbox_max = np.array(bounds[3:6])
bbox_size = bbox_max - bbox_min
xyz_samples, grid_size, length = generate_dense_grid_points(
bbox_min=bbox_min,
bbox_max=bbox_max,
octree_resolution=resolutions[0],
indexing="ij"
)
dilate = nn.Conv3d(1, 1, 3, padding=1, bias=False, device=device, dtype=dtype)
dilate.weight = torch.nn.Parameter(torch.ones(dilate.weight.shape, dtype=dtype, device=device))
grid_size = np.array(grid_size)
xyz_samples = torch.from_numpy(xyz_samples).to(device, dtype=dtype).contiguous().reshape(-1, 3)
# 2. latents to 3d volume
batch_logits = []
batch_size = latents.shape[0]
for start in tqdm(range(0, xyz_samples.shape[0], num_chunks),
desc=f"Hierarchical Volume Decoding [r{resolutions[0] + 1}]"):
queries = xyz_samples[start: start + num_chunks, :]
batch_queries = repeat(queries, "p c -> b p c", b=batch_size)
logits = geo_decoder(queries=batch_queries, latents=latents)
batch_logits.append(logits)
grid_logits = torch.cat(batch_logits, dim=1).view((batch_size, grid_size[0], grid_size[1], grid_size[2]))
for octree_depth_now in resolutions[1:]:
grid_size = np.array([octree_depth_now + 1] * 3)
resolution = bbox_size / octree_depth_now
next_index = torch.zeros(tuple(grid_size), dtype=dtype, device=device)
next_logits = torch.full(next_index.shape, -10000., dtype=dtype, device=device)
curr_points = extract_near_surface_volume_fn(grid_logits.squeeze(0), mc_level)
curr_points += grid_logits.squeeze(0).abs() < 0.95
if octree_depth_now == resolutions[-1]:
expand_num = 0
else:
expand_num = 1
for i in range(expand_num):
curr_points = dilate(curr_points.unsqueeze(0).to(dtype)).squeeze(0)
(cidx_x, cidx_y, cidx_z) = torch.where(curr_points > 0)
next_index[cidx_x * 2, cidx_y * 2, cidx_z * 2] = 1
for i in range(2 - expand_num):
next_index = dilate(next_index.unsqueeze(0)).squeeze(0)
nidx = torch.where(next_index > 0)
next_points = torch.stack(nidx, dim=1)
next_points = (next_points * torch.tensor(resolution, dtype=next_points.dtype, device=device) +
torch.tensor(bbox_min, dtype=next_points.dtype, device=device))
batch_logits = []
for start in tqdm(range(0, next_points.shape[0], num_chunks),
desc=f"Hierarchical Volume Decoding [r{octree_depth_now + 1}]"):
queries = next_points[start: start + num_chunks, :]
batch_queries = repeat(queries, "p c -> b p c", b=batch_size)
logits = geo_decoder(queries=batch_queries.to(latents.dtype), latents=latents)
batch_logits.append(logits)
grid_logits = torch.cat(batch_logits, dim=1)
next_logits[nidx] = grid_logits[0, ..., 0]
grid_logits = next_logits.unsqueeze(0)
grid_logits[grid_logits == -10000.] = float('nan')
return grid_logits
class FlashVDMVolumeDecoding:
def __init__(self, topk_mode='mean'):
if topk_mode not in ['mean', 'merge']:
raise ValueError(f'Unsupported topk_mode {topk_mode}, available: {["mean", "merge"]}')
if topk_mode == 'mean':
self.processor = FlashVDMCrossAttentionProcessor()
else:
self.processor = FlashVDMTopMCrossAttentionProcessor()
@torch.no_grad()
def __call__(
self,
latents: torch.FloatTensor,
geo_decoder: CrossAttentionDecoder,
bounds: Union[Tuple[float], List[float], float] = 1.01,
num_chunks: int = 10000,
mc_level: float = 0.0,
octree_resolution: int = None,
min_resolution: int = 63,
mini_grid_num: int = 4,
enable_pbar: bool = True,
**kwargs,
):
processor = self.processor
geo_decoder.set_cross_attention_processor(processor)
device = latents.device
dtype = latents.dtype
resolutions = []
if octree_resolution < min_resolution:
resolutions.append(octree_resolution)
while octree_resolution >= min_resolution:
resolutions.append(octree_resolution)
octree_resolution = octree_resolution // 2
resolutions.reverse()
resolutions[0] = round(resolutions[0] / mini_grid_num) * mini_grid_num - 1
for i, resolution in enumerate(resolutions[1:]):
resolutions[i + 1] = resolutions[0] * 2 ** (i + 1)
logger.info(f"FlashVDMVolumeDecoding Resolution: {resolutions}")
# 1. generate query points
if isinstance(bounds, float):
bounds = [-bounds, -bounds, -bounds, bounds, bounds, bounds]
bbox_min = np.array(bounds[0:3])
bbox_max = np.array(bounds[3:6])
bbox_size = bbox_max - bbox_min
xyz_samples, grid_size, length = generate_dense_grid_points(
bbox_min=bbox_min,
bbox_max=bbox_max,
octree_resolution=resolutions[0],
indexing="ij"
)
dilate = nn.Conv3d(1, 1, 3, padding=1, bias=False, device=device, dtype=dtype)
dilate.weight = torch.nn.Parameter(torch.ones(dilate.weight.shape, dtype=dtype, device=device))
grid_size = np.array(grid_size)
# 2. latents to 3d volume
xyz_samples = torch.from_numpy(xyz_samples).to(device, dtype=dtype)
batch_size = latents.shape[0]
mini_grid_size = xyz_samples.shape[0] // mini_grid_num
xyz_samples = xyz_samples.view(
mini_grid_num, mini_grid_size,
mini_grid_num, mini_grid_size,
mini_grid_num, mini_grid_size, 3
).permute(
0, 2, 4, 1, 3, 5, 6
).reshape(
-1, mini_grid_size * mini_grid_size * mini_grid_size, 3
)
batch_logits = []
num_batchs = max(num_chunks // xyz_samples.shape[1], 1)
for start in tqdm(range(0, xyz_samples.shape[0], num_batchs),
desc=f"FlashVDM Volume Decoding", disable=not enable_pbar):
queries = xyz_samples[start: start + num_batchs, :]
batch = queries.shape[0]
batch_latents = repeat(latents.squeeze(0), "p c -> b p c", b=batch)
processor.topk = True
logits = geo_decoder(queries=queries, latents=batch_latents)
batch_logits.append(logits)
grid_logits = torch.cat(batch_logits, dim=0).reshape(
mini_grid_num, mini_grid_num, mini_grid_num,
mini_grid_size, mini_grid_size,
mini_grid_size
).permute(0, 3, 1, 4, 2, 5).contiguous().view(
(batch_size, grid_size[0], grid_size[1], grid_size[2])
)
for octree_depth_now in resolutions[1:]:
grid_size = np.array([octree_depth_now + 1] * 3)
resolution = bbox_size / octree_depth_now
next_index = torch.zeros(tuple(grid_size), dtype=dtype, device=device)
next_logits = torch.full(next_index.shape, -10000., dtype=dtype, device=device)
curr_points = extract_near_surface_volume_fn(grid_logits.squeeze(0), mc_level)
curr_points += grid_logits.squeeze(0).abs() < 0.95
if octree_depth_now == resolutions[-1]:
expand_num = 0
else:
expand_num = 1
for i in range(expand_num):
curr_points = dilate(curr_points.unsqueeze(0).to(dtype)).squeeze(0)
(cidx_x, cidx_y, cidx_z) = torch.where(curr_points > 0)
next_index[cidx_x * 2, cidx_y * 2, cidx_z * 2] = 1
for i in range(2 - expand_num):
next_index = dilate(next_index.unsqueeze(0)).squeeze(0)
nidx = torch.where(next_index > 0)
next_points = torch.stack(nidx, dim=1)
next_points = (next_points * torch.tensor(resolution, dtype=torch.float32, device=device) +
torch.tensor(bbox_min, dtype=torch.float32, device=device))
query_grid_num = 6
min_val = next_points.min(axis=0).values
max_val = next_points.max(axis=0).values
vol_queries_index = (next_points - min_val) / (max_val - min_val) * (query_grid_num - 0.001)
index = torch.floor(vol_queries_index).long()
index = index[..., 0] * (query_grid_num ** 2) + index[..., 1] * query_grid_num + index[..., 2]
index = index.sort()
next_points = next_points[index.indices].unsqueeze(0).contiguous()
unique_values = torch.unique(index.values, return_counts=True)
grid_logits = torch.zeros((next_points.shape[1]), dtype=latents.dtype, device=latents.device)
input_grid = [[], []]
logits_grid_list = []
start_num = 0
sum_num = 0
for grid_index, count in zip(unique_values[0].cpu().tolist(), unique_values[1].cpu().tolist()):
if sum_num + count < num_chunks or sum_num == 0:
sum_num += count
input_grid[0].append(grid_index)
input_grid[1].append(count)
else:
processor.topk = input_grid
logits_grid = geo_decoder(queries=next_points[:, start_num:start_num + sum_num], latents=latents)
start_num = start_num + sum_num
logits_grid_list.append(logits_grid)
input_grid = [[grid_index], [count]]
sum_num = count
if sum_num > 0:
processor.topk = input_grid
logits_grid = geo_decoder(queries=next_points[:, start_num:start_num + sum_num], latents=latents)
logits_grid_list.append(logits_grid)
logits_grid = torch.cat(logits_grid_list, dim=1)
grid_logits[index.indices] = logits_grid.squeeze(0).squeeze(-1)
next_logits[nidx] = grid_logits
grid_logits = next_logits.unsqueeze(0)
grid_logits[grid_logits == -10000.] = float('nan')
return grid_logits
@@ -0,0 +1,257 @@
# Open Source Model Licensed under the Apache License Version 2.0
# and Other Licenses of the Third-Party Components therein:
# The below Model in this distribution may have been modified by THL A29 Limited
# ("Tencent Modifications"). All Tencent Modifications are Copyright (C) 2024 THL A29 Limited.
# Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved.
# The below software and/or models in this distribution may have been
# modified by THL A29 Limited ("Tencent Modifications").
# All Tencent Modifications are Copyright (C) THL A29 Limited.
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import numpy as np
import torch
import torch.nn as nn
from torchvision import transforms
from transformers import (
CLIPVisionModelWithProjection,
CLIPVisionConfig,
Dinov2Model,
Dinov2Config,
)
def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
"""
embed_dim: output dimension for each position
pos: a list of positions to be encoded: size (M,)
out: (M, D)
"""
assert embed_dim % 2 == 0
omega = np.arange(embed_dim // 2, dtype=np.float64)
omega /= embed_dim / 2.
omega = 1. / 10000 ** omega # (D/2,)
pos = pos.reshape(-1) # (M,)
out = np.einsum('m,d->md', pos, omega) # (M, D/2), outer product
emb_sin = np.sin(out) # (M, D/2)
emb_cos = np.cos(out) # (M, D/2)
return np.concatenate([emb_sin, emb_cos], axis=1)
class ImageEncoder(nn.Module):
def __init__(
self,
version=None,
config=None,
use_cls_token=True,
image_size=224,
**kwargs,
):
super().__init__()
if config is None:
self.model = self.MODEL_CLASS.from_pretrained(version)
else:
self.model = self.MODEL_CLASS(self.MODEL_CONFIG_CLASS.from_dict(config))
self.model.eval()
self.model.requires_grad_(False)
self.use_cls_token = use_cls_token
self.size = image_size // 14
self.num_patches = (image_size // 14) ** 2
if self.use_cls_token:
self.num_patches += 1
self.transform = transforms.Compose(
[
transforms.Resize(image_size, transforms.InterpolationMode.BILINEAR, antialias=True),
transforms.CenterCrop(image_size),
transforms.Normalize(
mean=self.mean,
std=self.std,
),
]
)
def forward(self, image, mask=None, value_range=(-1, 1), **kwargs):
if value_range is not None:
low, high = value_range
image = (image - low) / (high - low)
image = image.to(self.model.device, dtype=self.model.dtype)
inputs = self.transform(image)
outputs = self.model(inputs)
last_hidden_state = outputs.last_hidden_state
if not self.use_cls_token:
last_hidden_state = last_hidden_state[:, 1:, :]
return last_hidden_state
def unconditional_embedding(self, batch_size, **kwargs):
device = next(self.model.parameters()).device
dtype = next(self.model.parameters()).dtype
zero = torch.zeros(
batch_size,
self.num_patches,
self.model.config.hidden_size,
device=device,
dtype=dtype,
)
return zero
class CLIPImageEncoder(ImageEncoder):
MODEL_CLASS = CLIPVisionModelWithProjection
MODEL_CONFIG_CLASS = CLIPVisionConfig
mean = [0.48145466, 0.4578275, 0.40821073]
std = [0.26862954, 0.26130258, 0.27577711]
class DinoImageEncoder(ImageEncoder):
MODEL_CLASS = Dinov2Model
MODEL_CONFIG_CLASS = Dinov2Config
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
class DinoImageEncoderMV(DinoImageEncoder):
def __init__(
self,
version=None,
config=None,
use_cls_token=True,
image_size=224,
view_num=4,
**kwargs,
):
super().__init__(version, config, use_cls_token, image_size, **kwargs)
self.view_num = view_num
self.num_patches = self.num_patches
pos = np.arange(self.view_num, dtype=np.float32)
view_embedding = torch.from_numpy(
get_1d_sincos_pos_embed_from_grid(self.model.config.hidden_size, pos)).float()
view_embedding = view_embedding.unsqueeze(1).repeat(1, self.num_patches, 1)
self.view_embed = view_embedding.unsqueeze(0)
def forward(self, image, mask=None, value_range=(-1, 1), view_idxs=None):
if value_range is not None:
low, high = value_range
image = (image - low) / (high - low)
image = image.to(self.model.device, dtype=self.model.dtype)
bs, num_views, c, h, w = image.shape
image = image.view(bs * num_views, c, h, w)
inputs = self.transform(image)
outputs = self.model(inputs)
last_hidden_state = outputs.last_hidden_state
last_hidden_state = last_hidden_state.view(
bs, num_views, last_hidden_state.shape[-2],
last_hidden_state.shape[-1]
)
view_embedding = self.view_embed.to(last_hidden_state.dtype).to(last_hidden_state.device)
if view_idxs is not None:
assert len(view_idxs) == bs
view_embeddings = []
for i in range(bs):
view_idx = view_idxs[i]
assert num_views == len(view_idx)
view_embeddings.append(self.view_embed[:, view_idx, ...])
view_embedding = torch.cat(view_embeddings, 0).to(last_hidden_state.dtype).to(last_hidden_state.device)
if num_views != self.view_num:
view_embedding = view_embedding[:, :num_views, ...]
last_hidden_state = last_hidden_state + view_embedding
last_hidden_state = last_hidden_state.view(bs, num_views * last_hidden_state.shape[-2],
last_hidden_state.shape[-1])
return last_hidden_state
def unconditional_embedding(self, batch_size, view_idxs=None, **kwargs):
device = next(self.model.parameters()).device
dtype = next(self.model.parameters()).dtype
zero = torch.zeros(
batch_size,
self.num_patches * len(view_idxs[0]),
self.model.config.hidden_size,
device=device,
dtype=dtype,
)
return zero
def build_image_encoder(config):
if config['type'] == 'CLIPImageEncoder':
return CLIPImageEncoder(**config['kwargs'])
elif config['type'] == 'DinoImageEncoder':
return DinoImageEncoder(**config['kwargs'])
elif config['type'] == 'DinoImageEncoderMV':
return DinoImageEncoderMV(**config['kwargs'])
else:
raise ValueError(f'Unknown image encoder type: {config["type"]}')
class DualImageEncoder(nn.Module):
def __init__(
self,
main_image_encoder,
additional_image_encoder,
):
super().__init__()
self.main_image_encoder = build_image_encoder(main_image_encoder)
self.additional_image_encoder = build_image_encoder(additional_image_encoder)
def forward(self, image, mask=None, **kwargs):
outputs = {
'main': self.main_image_encoder(image, mask=mask, **kwargs),
'additional': self.additional_image_encoder(image, mask=mask, **kwargs),
}
return outputs
def unconditional_embedding(self, batch_size, **kwargs):
outputs = {
'main': self.main_image_encoder.unconditional_embedding(batch_size, **kwargs),
'additional': self.additional_image_encoder.unconditional_embedding(batch_size, **kwargs),
}
return outputs
class SingleImageEncoder(nn.Module):
def __init__(
self,
main_image_encoder,
):
super().__init__()
self.main_image_encoder = build_image_encoder(main_image_encoder)
def forward(self, image, mask=None, **kwargs):
outputs = {
'main': self.main_image_encoder(image, mask=mask, **kwargs),
}
return outputs
def unconditional_embedding(self, batch_size, **kwargs):
outputs = {
'main': self.main_image_encoder.unconditional_embedding(batch_size, **kwargs),
}
return outputs
@@ -0,0 +1,15 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from .hunyuan3ddit import Hunyuan3DDiT
@@ -0,0 +1,404 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
import math
from dataclasses import dataclass
from typing import List, Tuple, Optional
import torch
from torch import Tensor, nn
from einops import rearrange
# set up attention backend
scaled_dot_product_attention = nn.functional.scaled_dot_product_attention
if os.environ.get('USE_SAGEATTN', '0') == '1':
try:
from sageattention import sageattn
except ImportError:
raise ImportError('Please install the package "sageattention" to use this USE_SAGEATTN.')
scaled_dot_product_attention = sageattn
def attention(q: Tensor, k: Tensor, v: Tensor, **kwargs) -> Tensor:
x = scaled_dot_product_attention(q, k, v)
x = rearrange(x, "B H L D -> B L (H D)")
return x
def timestep_embedding(t: Tensor, dim, max_period=10000, time_factor: float = 1000.0):
"""
Create sinusoidal timestep embeddings.
:param t: a 1-D Tensor of N indices, one per batch element.
These may be fractional.
:param dim: the dimension of the output.
:param max_period: controls the minimum frequency of the embeddings.
:return: an (N, D) Tensor of positional embeddings.
"""
t = time_factor * t
half = dim // 2
freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half)
freqs = freqs.to(t.device)
args = t[:, None].float() * freqs[None]
embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
if dim % 2:
embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
if torch.is_floating_point(t):
embedding = embedding.to(t)
return embedding
class GELU(nn.Module):
def __init__(self, approximate='tanh'):
super().__init__()
self.approximate = approximate
def forward(self, x: Tensor) -> Tensor:
return nn.functional.gelu(x.contiguous(), approximate=self.approximate)
class MLPEmbedder(nn.Module):
def __init__(self, in_dim: int, hidden_dim: int):
super().__init__()
self.in_layer = nn.Linear(in_dim, hidden_dim, bias=True)
self.silu = nn.SiLU()
self.out_layer = nn.Linear(hidden_dim, hidden_dim, bias=True)
def forward(self, x: Tensor) -> Tensor:
return self.out_layer(self.silu(self.in_layer(x)))
class RMSNorm(torch.nn.Module):
def __init__(self, dim: int):
super().__init__()
self.scale = nn.Parameter(torch.ones(dim))
def forward(self, x: Tensor):
x_dtype = x.dtype
x = x.float()
rrms = torch.rsqrt(torch.mean(x ** 2, dim=-1, keepdim=True) + 1e-6)
return (x * rrms).to(dtype=x_dtype) * self.scale
class QKNorm(torch.nn.Module):
def __init__(self, dim: int):
super().__init__()
self.query_norm = RMSNorm(dim)
self.key_norm = RMSNorm(dim)
def forward(self, q: Tensor, k: Tensor, v: Tensor) -> Tuple[Tensor, Tensor]:
q = self.query_norm(q)
k = self.key_norm(k)
return q.to(v), k.to(v)
class SelfAttention(nn.Module):
def __init__(
self,
dim: int,
num_heads: int = 8,
qkv_bias: bool = False,
):
super().__init__()
self.num_heads = num_heads
head_dim = dim // num_heads
self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
self.norm = QKNorm(head_dim)
self.proj = nn.Linear(dim, dim)
def forward(self, x: Tensor, pe: Tensor) -> Tensor:
qkv = self.qkv(x)
q, k, v = rearrange(qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads)
q, k = self.norm(q, k, v)
x = attention(q, k, v, pe=pe)
x = self.proj(x)
return x
@dataclass
class ModulationOut:
shift: Tensor
scale: Tensor
gate: Tensor
class Modulation(nn.Module):
def __init__(self, dim: int, double: bool):
super().__init__()
self.is_double = double
self.multiplier = 6 if double else 3
self.lin = nn.Linear(dim, self.multiplier * dim, bias=True)
def forward(self, vec: Tensor) -> Tuple[ModulationOut, Optional[ModulationOut]]:
out = self.lin(nn.functional.silu(vec))[:, None, :]
out = out.chunk(self.multiplier, dim=-1)
return (
ModulationOut(*out[:3]),
ModulationOut(*out[3:]) if self.is_double else None,
)
class DoubleStreamBlock(nn.Module):
def __init__(
self,
hidden_size: int,
num_heads: int,
mlp_ratio: float,
qkv_bias: bool = False,
):
super().__init__()
mlp_hidden_dim = int(hidden_size * mlp_ratio)
self.num_heads = num_heads
self.hidden_size = hidden_size
self.img_mod = Modulation(hidden_size, double=True)
self.img_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.img_attn = SelfAttention(dim=hidden_size, num_heads=num_heads, qkv_bias=qkv_bias)
self.img_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.img_mlp = nn.Sequential(
nn.Linear(hidden_size, mlp_hidden_dim, bias=True),
GELU(approximate="tanh"),
nn.Linear(mlp_hidden_dim, hidden_size, bias=True),
)
self.txt_mod = Modulation(hidden_size, double=True)
self.txt_norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.txt_attn = SelfAttention(dim=hidden_size, num_heads=num_heads, qkv_bias=qkv_bias)
self.txt_norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.txt_mlp = nn.Sequential(
nn.Linear(hidden_size, mlp_hidden_dim, bias=True),
GELU(approximate="tanh"),
nn.Linear(mlp_hidden_dim, hidden_size, bias=True),
)
def forward(self, img: Tensor, txt: Tensor, vec: Tensor, pe: Tensor) -> Tuple[Tensor, Tensor]:
img_mod1, img_mod2 = self.img_mod(vec)
txt_mod1, txt_mod2 = self.txt_mod(vec)
img_modulated = self.img_norm1(img)
img_modulated = (1 + img_mod1.scale) * img_modulated + img_mod1.shift
img_qkv = self.img_attn.qkv(img_modulated)
img_q, img_k, img_v = rearrange(img_qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads)
img_q, img_k = self.img_attn.norm(img_q, img_k, img_v)
txt_modulated = self.txt_norm1(txt)
txt_modulated = (1 + txt_mod1.scale) * txt_modulated + txt_mod1.shift
txt_qkv = self.txt_attn.qkv(txt_modulated)
txt_q, txt_k, txt_v = rearrange(txt_qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads)
txt_q, txt_k = self.txt_attn.norm(txt_q, txt_k, txt_v)
q = torch.cat((txt_q, img_q), dim=2)
k = torch.cat((txt_k, img_k), dim=2)
v = torch.cat((txt_v, img_v), dim=2)
attn = attention(q, k, v, pe=pe)
txt_attn, img_attn = attn[:, : txt.shape[1]], attn[:, txt.shape[1]:]
img = img + img_mod1.gate * self.img_attn.proj(img_attn)
img = img + img_mod2.gate * self.img_mlp((1 + img_mod2.scale) * self.img_norm2(img) + img_mod2.shift)
txt = txt + txt_mod1.gate * self.txt_attn.proj(txt_attn)
txt = txt + txt_mod2.gate * self.txt_mlp((1 + txt_mod2.scale) * self.txt_norm2(txt) + txt_mod2.shift)
return img, txt
class SingleStreamBlock(nn.Module):
"""
A DiT block with parallel linear layers as described in
https://arxiv.org/abs/2302.05442 and adapted modulation interface.
"""
def __init__(
self,
hidden_size: int,
num_heads: int,
mlp_ratio: float = 4.0,
qk_scale: Optional[float] = None,
):
super().__init__()
self.hidden_dim = hidden_size
self.num_heads = num_heads
head_dim = hidden_size // num_heads
self.scale = qk_scale or head_dim ** -0.5
self.mlp_hidden_dim = int(hidden_size * mlp_ratio)
# qkv and mlp_in
self.linear1 = nn.Linear(hidden_size, hidden_size * 3 + self.mlp_hidden_dim)
# proj and mlp_out
self.linear2 = nn.Linear(hidden_size + self.mlp_hidden_dim, hidden_size)
self.norm = QKNorm(head_dim)
self.hidden_size = hidden_size
self.pre_norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.mlp_act = GELU(approximate="tanh")
self.modulation = Modulation(hidden_size, double=False)
def forward(self, x: Tensor, vec: Tensor, pe: Tensor) -> Tensor:
mod, _ = self.modulation(vec)
x_mod = (1 + mod.scale) * self.pre_norm(x) + mod.shift
qkv, mlp = torch.split(self.linear1(x_mod), [3 * self.hidden_size, self.mlp_hidden_dim], dim=-1)
q, k, v = rearrange(qkv, "B L (K H D) -> K B H L D", K=3, H=self.num_heads)
q, k = self.norm(q, k, v)
# compute attention
attn = attention(q, k, v, pe=pe)
# compute activation in mlp stream, cat again and run second linear layer
output = self.linear2(torch.cat((attn, self.mlp_act(mlp)), 2))
return x + mod.gate * output
class LastLayer(nn.Module):
def __init__(self, hidden_size: int, patch_size: int, out_channels: int):
super().__init__()
self.norm_final = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True)
self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True))
def forward(self, x: Tensor, vec: Tensor) -> Tensor:
shift, scale = self.adaLN_modulation(vec).chunk(2, dim=1)
x = (1 + scale[:, None, :]) * self.norm_final(x) + shift[:, None, :]
x = self.linear(x)
return x
class Hunyuan3DDiT(nn.Module):
def __init__(
self,
in_channels: int = 64,
context_in_dim: int = 1536,
hidden_size: int = 1024,
mlp_ratio: float = 4.0,
num_heads: int = 16,
depth: int = 16,
depth_single_blocks: int = 32,
axes_dim: List[int] = [64],
theta: int = 10_000,
qkv_bias: bool = True,
time_factor: float = 1000,
guidance_embed: bool = False,
ckpt_path: Optional[str] = None,
**kwargs,
):
super().__init__()
self.in_channels = in_channels
self.context_in_dim = context_in_dim
self.hidden_size = hidden_size
self.mlp_ratio = mlp_ratio
self.num_heads = num_heads
self.depth = depth
self.depth_single_blocks = depth_single_blocks
self.axes_dim = axes_dim
self.theta = theta
self.qkv_bias = qkv_bias
self.time_factor = time_factor
self.out_channels = self.in_channels
self.guidance_embed = guidance_embed
if hidden_size % num_heads != 0:
raise ValueError(
f"Hidden size {hidden_size} must be divisible by num_heads {num_heads}"
)
pe_dim = hidden_size // num_heads
if sum(axes_dim) != pe_dim:
raise ValueError(f"Got {axes_dim} but expected positional dim {pe_dim}")
self.hidden_size = hidden_size
self.num_heads = num_heads
self.latent_in = nn.Linear(self.in_channels, self.hidden_size, bias=True)
self.time_in = MLPEmbedder(in_dim=256, hidden_dim=self.hidden_size)
self.cond_in = nn.Linear(context_in_dim, self.hidden_size)
self.guidance_in = (
MLPEmbedder(in_dim=256, hidden_dim=self.hidden_size) if guidance_embed else nn.Identity()
)
self.double_blocks = nn.ModuleList(
[
DoubleStreamBlock(
self.hidden_size,
self.num_heads,
mlp_ratio=mlp_ratio,
qkv_bias=qkv_bias,
)
for _ in range(depth)
]
)
self.single_blocks = nn.ModuleList(
[
SingleStreamBlock(
self.hidden_size,
self.num_heads,
mlp_ratio=mlp_ratio,
)
for _ in range(depth_single_blocks)
]
)
self.final_layer = LastLayer(self.hidden_size, 1, self.out_channels)
if ckpt_path is not None:
print('restored denoiser ckpt', ckpt_path)
ckpt = torch.load(ckpt_path, map_location="cpu")
if 'state_dict' not in ckpt:
# deepspeed ckpt
state_dict = {}
for k in ckpt.keys():
new_k = k.replace('_forward_module.', '')
state_dict[new_k] = ckpt[k]
else:
state_dict = ckpt["state_dict"]
final_state_dict = {}
for k, v in state_dict.items():
if k.startswith('model.'):
final_state_dict[k.replace('model.', '')] = v
else:
final_state_dict[k] = v
missing, unexpected = self.load_state_dict(final_state_dict, strict=False)
print('unexpected keys:', unexpected)
print('missing keys:', missing)
def forward(self, x, t, contexts, **kwargs) -> Tensor:
cond = contexts['main']
latent = self.latent_in(x)
vec = self.time_in(timestep_embedding(t, 256, self.time_factor).to(dtype=latent.dtype))
if self.guidance_embed:
guidance = kwargs.get('guidance', None)
if guidance is None:
raise ValueError("Didn't get guidance strength for guidance distilled model.")
vec = vec + self.guidance_in(timestep_embedding(guidance, 256, self.time_factor))
cond = self.cond_in(cond)
pe = None
for block in self.double_blocks:
latent, cond = block(img=latent, txt=cond, vec=vec, pe=pe)
latent = torch.cat((cond, latent), 1)
for block in self.single_blocks:
latent = block(latent, vec=vec, pe=pe)
latent = latent[:, cond.shape[1]:, ...]
latent = self.final_layer(latent, vec)
return latent
@@ -0,0 +1,596 @@
# Open Source Model Licensed under the Apache License Version 2.0
# and Other Licenses of the Third-Party Components therein:
# The below Model in this distribution may have been modified by THL A29 Limited
# ("Tencent Modifications"). All Tencent Modifications are Copyright (C) 2024 THL A29 Limited.
# Copyright (C) 2024 THL A29 Limited, a Tencent company. All rights reserved.
# The below software and/or models in this distribution may have been
# modified by THL A29 Limited ("Tencent Modifications").
# All Tencent Modifications are Copyright (C) THL A29 Limited.
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import math
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
from .moe_layers import MoEBlock
def modulate(x, shift, scale):
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
"""
embed_dim: output dimension for each position
pos: a list of positions to be encoded: size (M,)
out: (M, D)
"""
assert embed_dim % 2 == 0
omega = np.arange(embed_dim // 2, dtype=np.float64)
omega /= embed_dim / 2.
omega = 1. / 10000 ** omega # (D/2,)
pos = pos.reshape(-1) # (M,)
out = np.einsum('m,d->md', pos, omega) # (M, D/2), outer product
emb_sin = np.sin(out) # (M, D/2)
emb_cos = np.cos(out) # (M, D/2)
return np.concatenate([emb_sin, emb_cos], axis=1)
class Timesteps(nn.Module):
def __init__(self,
num_channels: int,
downscale_freq_shift: float = 0.0,
scale: int = 1,
max_period: int = 10000
):
super().__init__()
self.num_channels = num_channels
self.downscale_freq_shift = downscale_freq_shift
self.scale = scale
self.max_period = max_period
def forward(self, timesteps):
assert len(timesteps.shape) == 1, "Timesteps should be a 1d-array"
embedding_dim = self.num_channels
half_dim = embedding_dim // 2
exponent = -math.log(self.max_period) * torch.arange(
start=0, end=half_dim, dtype=torch.float32, device=timesteps.device)
exponent = exponent / (half_dim - self.downscale_freq_shift)
emb = torch.exp(exponent)
emb = timesteps[:, None].float() * emb[None, :]
emb = self.scale * emb
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
if embedding_dim % 2 == 1:
emb = torch.nn.functional.pad(emb, (0, 1, 0, 0))
return emb
class TimestepEmbedder(nn.Module):
"""
Embeds scalar timesteps into vector representations.
"""
def __init__(self, hidden_size, frequency_embedding_size=256, cond_proj_dim=None, out_size=None):
super().__init__()
if out_size is None:
out_size = hidden_size
self.mlp = nn.Sequential(
nn.Linear(hidden_size, frequency_embedding_size, bias=True),
nn.GELU(),
nn.Linear(frequency_embedding_size, out_size, bias=True),
)
self.frequency_embedding_size = frequency_embedding_size
if cond_proj_dim is not None:
self.cond_proj = nn.Linear(cond_proj_dim, frequency_embedding_size, bias=False)
self.time_embed = Timesteps(hidden_size)
def forward(self, t, condition):
t_freq = self.time_embed(t).type(self.mlp[0].weight.dtype)
# t_freq = timestep_embedding(t, self.frequency_embedding_size).type(self.mlp[0].weight.dtype)
if condition is not None:
t_freq = t_freq + self.cond_proj(condition)
t = self.mlp(t_freq)
t = t.unsqueeze(dim=1)
return t
class MLP(nn.Module):
def __init__(self, *, width: int):
super().__init__()
self.width = width
self.fc1 = nn.Linear(width, width * 4)
self.fc2 = nn.Linear(width * 4, width)
self.gelu = nn.GELU()
def forward(self, x):
return self.fc2(self.gelu(self.fc1(x)))
class CrossAttention(nn.Module):
def __init__(
self,
qdim,
kdim,
num_heads,
qkv_bias=True,
qk_norm=False,
norm_layer=nn.LayerNorm,
with_decoupled_ca=False,
decoupled_ca_dim=16,
decoupled_ca_weight=1.0,
**kwargs,
):
super().__init__()
self.qdim = qdim
self.kdim = kdim
self.num_heads = num_heads
assert self.qdim % num_heads == 0, "self.qdim must be divisible by num_heads"
self.head_dim = self.qdim // num_heads
assert self.head_dim % 8 == 0 and self.head_dim <= 128, "Only support head_dim <= 128 and divisible by 8"
self.scale = self.head_dim ** -0.5
self.to_q = nn.Linear(qdim, qdim, bias=qkv_bias)
self.to_k = nn.Linear(kdim, qdim, bias=qkv_bias)
self.to_v = nn.Linear(kdim, qdim, bias=qkv_bias)
# TODO: eps should be 1 / 65530 if using fp16
self.q_norm = norm_layer(self.head_dim, elementwise_affine=True, eps=1e-6) if qk_norm else nn.Identity()
self.k_norm = norm_layer(self.head_dim, elementwise_affine=True, eps=1e-6) if qk_norm else nn.Identity()
self.out_proj = nn.Linear(qdim, qdim, bias=True)
self.with_dca = with_decoupled_ca
if self.with_dca:
self.kv_proj_dca = nn.Linear(kdim, 2 * qdim, bias=qkv_bias)
self.k_norm_dca = norm_layer(self.head_dim, elementwise_affine=True, eps=1e-6) if qk_norm else nn.Identity()
self.dca_dim = decoupled_ca_dim
self.dca_weight = decoupled_ca_weight
def forward(self, x, y):
"""
Parameters
----------
x: torch.Tensor
(batch, seqlen1, hidden_dim) (where hidden_dim = num heads * head dim)
y: torch.Tensor
(batch, seqlen2, hidden_dim2)
freqs_cis_img: torch.Tensor
(batch, hidden_dim // 2), RoPE for image
"""
b, s1, c = x.shape # [b, s1, D]
if self.with_dca:
token_len = y.shape[1]
context_dca = y[:, -self.dca_dim:, :]
kv_dca = self.kv_proj_dca(context_dca).view(b, self.dca_dim, 2, self.num_heads, self.head_dim)
k_dca, v_dca = kv_dca.unbind(dim=2) # [b, s, h, d]
k_dca = self.k_norm_dca(k_dca)
y = y[:, :(token_len - self.dca_dim), :]
_, s2, c = y.shape # [b, s2, 1024]
q = self.to_q(x)
k = self.to_k(y)
v = self.to_v(y)
kv = torch.cat((k, v), dim=-1)
split_size = kv.shape[-1] // self.num_heads // 2
kv = kv.view(1, -1, self.num_heads, split_size * 2)
k, v = torch.split(kv, split_size, dim=-1)
q = q.view(b, s1, self.num_heads, self.head_dim) # [b, s1, h, d]
k = k.view(b, s2, self.num_heads, self.head_dim) # [b, s2, h, d]
v = v.view(b, s2, self.num_heads, self.head_dim) # [b, s2, h, d]
q = self.q_norm(q)
k = self.k_norm(k)
with torch.backends.cuda.sdp_kernel(
enable_flash=True,
enable_math=False,
enable_mem_efficient=True
):
q, k, v = map(lambda t: rearrange(t, 'b n h d -> b h n d', h=self.num_heads), (q, k, v))
context = F.scaled_dot_product_attention(
q, k, v
).transpose(1, 2).reshape(b, s1, -1)
if self.with_dca:
with torch.backends.cuda.sdp_kernel(
enable_flash=True,
enable_math=False,
enable_mem_efficient=True
):
k_dca, v_dca = map(lambda t: rearrange(t, 'b n h d -> b h n d', h=self.num_heads),
(k_dca, v_dca))
context_dca = F.scaled_dot_product_attention(
q, k_dca, v_dca).transpose(1, 2).reshape(b, s1, -1)
context = context + self.dca_weight * context_dca
out = self.out_proj(context) # context.reshape - B, L1, -1
return out
class Attention(nn.Module):
"""
We rename some layer names to align with flash attention
"""
def __init__(
self,
dim,
num_heads,
qkv_bias=True,
qk_norm=False,
norm_layer=nn.LayerNorm,
):
super().__init__()
self.dim = dim
self.num_heads = num_heads
assert self.dim % num_heads == 0, 'dim should be divisible by num_heads'
self.head_dim = self.dim // num_heads
# This assertion is aligned with flash attention
assert self.head_dim % 8 == 0 and self.head_dim <= 128, "Only support head_dim <= 128 and divisible by 8"
self.scale = self.head_dim ** -0.5
self.to_q = nn.Linear(dim, dim, bias=qkv_bias)
self.to_k = nn.Linear(dim, dim, bias=qkv_bias)
self.to_v = nn.Linear(dim, dim, bias=qkv_bias)
# TODO: eps should be 1 / 65530 if using fp16
self.q_norm = norm_layer(self.head_dim, elementwise_affine=True, eps=1e-6) if qk_norm else nn.Identity()
self.k_norm = norm_layer(self.head_dim, elementwise_affine=True, eps=1e-6) if qk_norm else nn.Identity()
self.out_proj = nn.Linear(dim, dim)
def forward(self, x):
B, N, C = x.shape
q = self.to_q(x)
k = self.to_k(x)
v = self.to_v(x)
qkv = torch.cat((q, k, v), dim=-1)
split_size = qkv.shape[-1] // self.num_heads // 3
qkv = qkv.view(1, -1, self.num_heads, split_size * 3)
q, k, v = torch.split(qkv, split_size, dim=-1)
q = q.reshape(B, N, self.num_heads, self.head_dim).transpose(1, 2) # [b, h, s, d]
k = k.reshape(B, N, self.num_heads, self.head_dim).transpose(1, 2) # [b, h, s, d]
v = v.reshape(B, N, self.num_heads, self.head_dim).transpose(1, 2)
q = self.q_norm(q) # [b, h, s, d]
k = self.k_norm(k) # [b, h, s, d]
with torch.backends.cuda.sdp_kernel(
enable_flash=True,
enable_math=False,
enable_mem_efficient=True
):
x = F.scaled_dot_product_attention(q, k, v)
x = x.transpose(1, 2).reshape(B, N, -1)
x = self.out_proj(x)
return x
class HunYuanDiTBlock(nn.Module):
def __init__(
self,
hidden_size,
c_emb_size,
num_heads,
text_states_dim=1024,
use_flash_attn=False,
qk_norm=False,
norm_layer=nn.LayerNorm,
qk_norm_layer=nn.RMSNorm,
with_decoupled_ca=False,
decoupled_ca_dim=16,
decoupled_ca_weight=1.0,
init_scale=1.0,
qkv_bias=True,
skip_connection=True,
timested_modulate=False,
use_moe: bool = False,
num_experts: int = 8,
moe_top_k: int = 2,
**kwargs,
):
super().__init__()
self.use_flash_attn = use_flash_attn
use_ele_affine = True
# ========================= Self-Attention =========================
self.norm1 = norm_layer(hidden_size, elementwise_affine=use_ele_affine, eps=1e-6)
self.attn1 = Attention(hidden_size, num_heads=num_heads, qkv_bias=qkv_bias, qk_norm=qk_norm,
norm_layer=qk_norm_layer)
# ========================= FFN =========================
self.norm2 = norm_layer(hidden_size, elementwise_affine=use_ele_affine, eps=1e-6)
# ========================= Add =========================
# Simply use add like SDXL.
self.timested_modulate = timested_modulate
if self.timested_modulate:
self.default_modulation = nn.Sequential(
nn.SiLU(),
nn.Linear(c_emb_size, hidden_size, bias=True)
)
# ========================= Cross-Attention =========================
self.attn2 = CrossAttention(hidden_size, text_states_dim, num_heads=num_heads, qkv_bias=qkv_bias,
qk_norm=qk_norm, norm_layer=qk_norm_layer,
with_decoupled_ca=with_decoupled_ca, decoupled_ca_dim=decoupled_ca_dim,
decoupled_ca_weight=decoupled_ca_weight, init_scale=init_scale,
)
self.norm3 = norm_layer(hidden_size, elementwise_affine=True, eps=1e-6)
if skip_connection:
self.skip_norm = norm_layer(hidden_size, elementwise_affine=True, eps=1e-6)
self.skip_linear = nn.Linear(2 * hidden_size, hidden_size)
else:
self.skip_linear = None
self.use_moe = use_moe
if self.use_moe:
# print("using moe")
self.moe = MoEBlock(
hidden_size,
num_experts=num_experts,
moe_top_k=moe_top_k,
dropout=0.0,
activation_fn="gelu",
final_dropout=False,
ff_inner_dim=int(hidden_size * 4.0),
ff_bias=True,
)
else:
self.mlp = MLP(width=hidden_size)
def forward(self, x, c=None, text_states=None, skip_value=None):
if self.skip_linear is not None:
cat = torch.cat([skip_value, x], dim=-1)
x = self.skip_linear(cat)
x = self.skip_norm(x)
# Self-Attention
if self.timested_modulate:
shift_msa = self.default_modulation(c).unsqueeze(dim=1)
x = x + shift_msa
attn_out = self.attn1(self.norm1(x))
x = x + attn_out
# Cross-Attention
x = x + self.attn2(self.norm2(x), text_states)
# FFN Layer
mlp_inputs = self.norm3(x)
if self.use_moe:
x = x + self.moe(mlp_inputs)
else:
x = x + self.mlp(mlp_inputs)
return x
class AttentionPool(nn.Module):
def __init__(self, spacial_dim: int, embed_dim: int, num_heads: int, output_dim: int = None):
super().__init__()
self.positional_embedding = nn.Parameter(torch.randn(spacial_dim + 1, embed_dim) / embed_dim ** 0.5)
self.k_proj = nn.Linear(embed_dim, embed_dim)
self.q_proj = nn.Linear(embed_dim, embed_dim)
self.v_proj = nn.Linear(embed_dim, embed_dim)
self.c_proj = nn.Linear(embed_dim, output_dim or embed_dim)
self.num_heads = num_heads
def forward(self, x, attention_mask=None):
x = x.permute(1, 0, 2) # NLC -> LNC
if attention_mask is not None:
attention_mask = attention_mask.unsqueeze(-1).permute(1, 0, 2)
global_emb = (x * attention_mask).sum(dim=0) / attention_mask.sum(dim=0)
x = torch.cat([global_emb[None,], x], dim=0)
else:
x = torch.cat([x.mean(dim=0, keepdim=True), x], dim=0) # (L+1)NC
x = x + self.positional_embedding[:, None, :].to(x.dtype) # (L+1)NC
x, _ = F.multi_head_attention_forward(
query=x[:1], key=x, value=x,
embed_dim_to_check=x.shape[-1],
num_heads=self.num_heads,
q_proj_weight=self.q_proj.weight,
k_proj_weight=self.k_proj.weight,
v_proj_weight=self.v_proj.weight,
in_proj_weight=None,
in_proj_bias=torch.cat([self.q_proj.bias, self.k_proj.bias, self.v_proj.bias]),
bias_k=None,
bias_v=None,
add_zero_attn=False,
dropout_p=0,
out_proj_weight=self.c_proj.weight,
out_proj_bias=self.c_proj.bias,
use_separate_proj_weight=True,
training=self.training,
need_weights=False
)
return x.squeeze(0)
class FinalLayer(nn.Module):
"""
The final layer of HunYuanDiT.
"""
def __init__(self, final_hidden_size, out_channels):
super().__init__()
self.final_hidden_size = final_hidden_size
self.norm_final = nn.LayerNorm(final_hidden_size, elementwise_affine=True, eps=1e-6)
self.linear = nn.Linear(final_hidden_size, out_channels, bias=True)
def forward(self, x):
x = self.norm_final(x)
x = x[:, 1:]
x = self.linear(x)
return x
class HunYuanDiTPlain(nn.Module):
def __init__(
self,
input_size=1024,
in_channels=4,
hidden_size=1024,
context_dim=1024,
depth=24,
num_heads=16,
mlp_ratio=4.0,
norm_type='layer',
qk_norm_type='rms',
qk_norm=False,
text_len=257,
with_decoupled_ca=False,
additional_cond_hidden_state=768,
decoupled_ca_dim=16,
decoupled_ca_weight=1.0,
use_pos_emb=False,
use_attention_pooling=True,
guidance_cond_proj_dim=None,
qkv_bias=True,
num_moe_layers: int = 6,
num_experts: int = 8,
moe_top_k: int = 2,
**kwargs
):
super().__init__()
self.input_size = input_size
self.depth = depth
self.in_channels = in_channels
self.out_channels = in_channels
self.num_heads = num_heads
self.hidden_size = hidden_size
self.norm = nn.LayerNorm if norm_type == 'layer' else nn.RMSNorm
self.qk_norm = nn.RMSNorm if qk_norm_type == 'rms' else nn.LayerNorm
self.context_dim = context_dim
self.with_decoupled_ca = with_decoupled_ca
self.decoupled_ca_dim = decoupled_ca_dim
self.decoupled_ca_weight = decoupled_ca_weight
self.use_pos_emb = use_pos_emb
self.use_attention_pooling = use_attention_pooling
self.guidance_cond_proj_dim = guidance_cond_proj_dim
self.text_len = text_len
self.x_embedder = nn.Linear(in_channels, hidden_size, bias=True)
self.t_embedder = TimestepEmbedder(hidden_size, hidden_size * 4, cond_proj_dim=guidance_cond_proj_dim)
# Will use fixed sin-cos embedding:
if self.use_pos_emb:
self.register_buffer("pos_embed", torch.zeros(1, input_size, hidden_size))
pos = np.arange(self.input_size, dtype=np.float32)
pos_embed = get_1d_sincos_pos_embed_from_grid(self.pos_embed.shape[-1], pos)
self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float().unsqueeze(0))
self.use_attention_pooling = use_attention_pooling
if use_attention_pooling:
self.pooler = AttentionPool(self.text_len, context_dim, num_heads=8, output_dim=1024)
self.extra_embedder = nn.Sequential(
nn.Linear(1024, hidden_size * 4),
nn.SiLU(),
nn.Linear(hidden_size * 4, hidden_size, bias=True),
)
if with_decoupled_ca:
self.additional_cond_hidden_state = additional_cond_hidden_state
self.additional_cond_proj = nn.Sequential(
nn.Linear(additional_cond_hidden_state, hidden_size * 4),
nn.SiLU(),
nn.Linear(hidden_size * 4, 1024, bias=True),
)
# HUnYuanDiT Blocks
self.blocks = nn.ModuleList([
HunYuanDiTBlock(hidden_size=hidden_size,
c_emb_size=hidden_size,
num_heads=num_heads,
mlp_ratio=mlp_ratio,
text_states_dim=context_dim,
qk_norm=qk_norm,
norm_layer=self.norm,
qk_norm_layer=self.qk_norm,
skip_connection=layer > depth // 2,
with_decoupled_ca=with_decoupled_ca,
decoupled_ca_dim=decoupled_ca_dim,
decoupled_ca_weight=decoupled_ca_weight,
qkv_bias=qkv_bias,
use_moe=True if depth - layer <= num_moe_layers else False,
num_experts=num_experts,
moe_top_k=moe_top_k
)
for layer in range(depth)
])
self.depth = depth
self.final_layer = FinalLayer(hidden_size, self.out_channels)
def forward(self, x, t, contexts, **kwargs):
cond = contexts['main']
t = self.t_embedder(t, condition=kwargs.get('guidance_cond'))
x = self.x_embedder(x)
if self.use_pos_emb:
pos_embed = self.pos_embed.to(x.dtype)
x = x + pos_embed
if self.use_attention_pooling:
extra_vec = self.pooler(cond, None)
c = t + self.extra_embedder(extra_vec) # [B, D]
else:
c = t
if self.with_decoupled_ca:
additional_cond = self.additional_cond_proj(contexts['additional'])
cond = torch.cat([cond, additional_cond], dim=1)
x = torch.cat([c, x], dim=1)
skip_value_list = []
for layer, block in enumerate(self.blocks):
skip_value = None if layer <= self.depth // 2 else skip_value_list.pop()
x = block(x, c, cond, skip_value=skip_value)
if layer < self.depth // 2:
skip_value_list.append(x)
x = self.final_layer(x)
return x
@@ -0,0 +1,177 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import torch
import torch.nn as nn
import numpy as np
import math
from timm.models.vision_transformer import PatchEmbed, Attention, Mlp
import torch.nn.functional as F
from diffusers.models.attention import FeedForward
class AddAuxiliaryLoss(torch.autograd.Function):
"""
The trick function of adding auxiliary (aux) loss,
which includes the gradient of the aux loss during backpropagation.
"""
@staticmethod
def forward(ctx, x, loss):
assert loss.numel() == 1
ctx.dtype = loss.dtype
ctx.required_aux_loss = loss.requires_grad
return x
@staticmethod
def backward(ctx, grad_output):
grad_loss = None
if ctx.required_aux_loss:
grad_loss = torch.ones(1, dtype=ctx.dtype, device=grad_output.device)
return grad_output, grad_loss
class MoEGate(nn.Module):
def __init__(self, embed_dim, num_experts=16, num_experts_per_tok=2, aux_loss_alpha=0.01):
super().__init__()
self.top_k = num_experts_per_tok
self.n_routed_experts = num_experts
self.scoring_func = 'softmax'
self.alpha = aux_loss_alpha
self.seq_aux = False
# topk selection algorithm
self.norm_topk_prob = False
self.gating_dim = embed_dim
self.weight = nn.Parameter(torch.empty((self.n_routed_experts, self.gating_dim)))
self.reset_parameters()
def reset_parameters(self) -> None:
import torch.nn.init as init
init.kaiming_uniform_(self.weight, a=math.sqrt(5))
def forward(self, hidden_states):
bsz, seq_len, h = hidden_states.shape
# print(bsz, seq_len, h)
### compute gating score
hidden_states = hidden_states.view(-1, h)
logits = F.linear(hidden_states, self.weight, None)
if self.scoring_func == 'softmax':
scores = logits.softmax(dim=-1)
else:
raise NotImplementedError(f'insupportable scoring function for MoE gating: {self.scoring_func}')
### select top-k experts
topk_weight, topk_idx = torch.topk(scores, k=self.top_k, dim=-1, sorted=False)
### norm gate to sum 1
if self.top_k > 1 and self.norm_topk_prob:
denominator = topk_weight.sum(dim=-1, keepdim=True) + 1e-20
topk_weight = topk_weight / denominator
### expert-level computation auxiliary loss
if self.training and self.alpha > 0.0:
scores_for_aux = scores
aux_topk = self.top_k
# always compute aux loss based on the naive greedy topk method
topk_idx_for_aux_loss = topk_idx.view(bsz, -1)
if self.seq_aux:
scores_for_seq_aux = scores_for_aux.view(bsz, seq_len, -1)
ce = torch.zeros(bsz, self.n_routed_experts, device=hidden_states.device)
ce.scatter_add_(
1,
topk_idx_for_aux_loss,
torch.ones(
bsz, seq_len * aux_topk,
device=hidden_states.device
)
).div_(seq_len * aux_topk / self.n_routed_experts)
aux_loss = (ce * scores_for_seq_aux.mean(dim = 1)).sum(dim = 1).mean()
aux_loss = aux_loss * self.alpha
else:
mask_ce = F.one_hot(topk_idx_for_aux_loss.view(-1),
num_classes=self.n_routed_experts)
ce = mask_ce.float().mean(0)
Pi = scores_for_aux.mean(0)
fi = ce * self.n_routed_experts
aux_loss = (Pi * fi).sum() * self.alpha
else:
aux_loss = None
return topk_idx, topk_weight, aux_loss
class MoEBlock(nn.Module):
def __init__(self, dim, num_experts=8, moe_top_k=2,
activation_fn = "gelu", dropout=0.0, final_dropout = False,
ff_inner_dim = None, ff_bias = True):
super().__init__()
self.moe_top_k = moe_top_k
self.experts = nn.ModuleList([
FeedForward(dim,dropout=dropout,
activation_fn=activation_fn,
final_dropout=final_dropout,
inner_dim=ff_inner_dim,
bias=ff_bias)
for i in range(num_experts)])
self.gate = MoEGate(embed_dim=dim, num_experts=num_experts, num_experts_per_tok=moe_top_k)
self.shared_experts = FeedForward(dim,dropout=dropout, activation_fn=activation_fn,
final_dropout=final_dropout, inner_dim=ff_inner_dim,
bias=ff_bias)
def initialize_weight(self):
pass
def forward(self, hidden_states):
identity = hidden_states
orig_shape = hidden_states.shape
topk_idx, topk_weight, aux_loss = self.gate(hidden_states)
hidden_states = hidden_states.view(-1, hidden_states.shape[-1])
flat_topk_idx = topk_idx.view(-1)
if self.training:
hidden_states = hidden_states.repeat_interleave(self.moe_top_k, dim=0)
y = torch.empty_like(hidden_states, dtype=hidden_states.dtype)
for i, expert in enumerate(self.experts):
tmp = expert(hidden_states[flat_topk_idx == i])
y[flat_topk_idx == i] = tmp.to(hidden_states.dtype)
y = (y.view(*topk_weight.shape, -1) * topk_weight.unsqueeze(-1)).sum(dim=1)
y = y.view(*orig_shape)
y = AddAuxiliaryLoss.apply(y, aux_loss)
else:
y = self.moe_infer(hidden_states, flat_topk_idx, topk_weight.view(-1, 1)).view(*orig_shape)
y = y + self.shared_experts(identity)
return y
@torch.no_grad()
def moe_infer(self, x, flat_expert_indices, flat_expert_weights):
expert_cache = torch.zeros_like(x)
idxs = flat_expert_indices.argsort()
tokens_per_expert = flat_expert_indices.bincount().cpu().numpy().cumsum(0)
token_idxs = idxs // self.moe_top_k
for i, end_idx in enumerate(tokens_per_expert):
start_idx = 0 if i == 0 else tokens_per_expert[i-1]
if start_idx == end_idx:
continue
expert = self.experts[i]
exp_token_idx = token_idxs[start_idx:end_idx]
expert_tokens = x[exp_token_idx]
expert_out = expert(expert_tokens)
expert_out.mul_(flat_expert_weights[idxs[start_idx:end_idx]])
# for fp16 and other dtype
expert_cache = expert_cache.to(expert_out.dtype)
expert_cache.scatter_reduce_(0, exp_token_idx.view(-1, 1).repeat(1, x.shape[-1]),
expert_out,
reduce='sum')
return expert_cache
@@ -0,0 +1,354 @@
import os
from contextlib import contextmanager
from typing import List, Tuple, Optional, Union
import torch
import torch.nn as nn
from torch.optim import lr_scheduler
import pytorch_lightning as pl
from pytorch_lightning.utilities import rank_zero_info
from pytorch_lightning.utilities import rank_zero_only
from ...utils.ema import LitEma
from ...utils.misc import instantiate_from_config, instantiate_non_trainable_model
class Diffuser(pl.LightningModule):
def __init__(
self,
*,
first_stage_config,
cond_stage_config,
denoiser_cfg,
scheduler_cfg,
optimizer_cfg,
pipeline_cfg=None,
image_processor_cfg=None,
lora_config=None,
ema_config=None,
first_stage_key: str = "surface",
cond_stage_key: str = "image",
scale_by_std: bool = False,
z_scale_factor: float = 1.0,
ckpt_path: Optional[str] = None,
ignore_keys: Union[Tuple[str], List[str]] = (),
torch_compile: bool = False,
):
super().__init__()
self.first_stage_key = first_stage_key
self.cond_stage_key = cond_stage_key
# ========= init optimizer config ========= #
self.optimizer_cfg = optimizer_cfg
# ========= init diffusion scheduler ========= #
self.scheduler_cfg = scheduler_cfg
self.sampler = None
if 'transport' in scheduler_cfg:
self.transport = instantiate_from_config(scheduler_cfg.transport)
self.sampler = instantiate_from_config(scheduler_cfg.sampler, transport=self.transport)
self.sample_fn = self.sampler.sample_ode(**scheduler_cfg.sampler.ode_params)
# ========= init the model ========= #
self.denoiser_cfg = denoiser_cfg
self.model = instantiate_from_config(denoiser_cfg, device=None, dtype=None)
self.cond_stage_model = instantiate_from_config(cond_stage_config)
self.ckpt_path = ckpt_path
if ckpt_path is not None:
self.init_from_ckpt(ckpt_path, ignore_keys=ignore_keys)
# ========= config lora model ========= #
if lora_config is not None:
from peft import LoraConfig, get_peft_model
loraconfig = LoraConfig(
r=lora_config.rank,
lora_alpha=lora_config.rank,
target_modules=lora_config.get('target_modules')
)
self.model = get_peft_model(self.model, loraconfig)
# ========= config ema model ========= #
self.ema_config = ema_config
if self.ema_config is not None:
if self.ema_config.ema_model == 'DSEma':
# from michelangelo.models.modules.ema_deepspeed import DSEma
from ..utils.ema_deepspeed import DSEma
self.model_ema = DSEma(self.model, decay=self.ema_config.ema_decay)
else:
self.model_ema = LitEma(self.model, decay=self.ema_config.ema_decay)
#do not initilize EMA weight from ckpt path, since I need to change moe layers
if ckpt_path is not None:
self.init_from_ckpt(ckpt_path, ignore_keys=ignore_keys)
print(f"Keeping EMAs of {len(list(self.model_ema.buffers()))}.")
# ========= init vae at last to prevent it is overridden by loaded ckpt ========= #
self.first_stage_model = instantiate_non_trainable_model(first_stage_config)
self.scale_by_std = scale_by_std
if scale_by_std:
self.register_buffer("z_scale_factor", torch.tensor(z_scale_factor))
else:
self.z_scale_factor = z_scale_factor
# ========= init pipeline for inference ========= #
self.image_processor_cfg = image_processor_cfg
self.image_processor = None
if self.image_processor_cfg is not None:
self.image_processor = instantiate_from_config(self.image_processor_cfg)
self.pipeline_cfg = pipeline_cfg
from ...schedulers import FlowMatchEulerDiscreteScheduler
scheduler = FlowMatchEulerDiscreteScheduler(num_train_timesteps=1000)
self.pipeline = instantiate_from_config(
pipeline_cfg,
vae=self.first_stage_model,
model=self.model,
scheduler=scheduler, # self.sampler,
conditioner=self.cond_stage_model,
image_processor=self.image_processor,
)
# ========= torch compile to accelerate ========= #
self.torch_compile = torch_compile
if self.torch_compile:
torch.nn.Module.compile(self.model)
torch.nn.Module.compile(self.first_stage_model)
torch.nn.Module.compile(self.cond_stage_model)
print(f'*' * 100)
print(f'Compile model for acceleration')
print(f'*' * 100)
@contextmanager
def ema_scope(self, context=None):
if self.ema_config is not None and self.ema_config.get('ema_inference', False):
self.model_ema.store(self.model)
self.model_ema.copy_to(self.model)
if context is not None:
print(f"{context}: Switched to EMA weights")
try:
yield None
finally:
if self.ema_config is not None and self.ema_config.get('ema_inference', False):
self.model_ema.restore(self.model)
if context is not None:
print(f"{context}: Restored training weights")
def init_from_ckpt(self, path, ignore_keys=()):
ckpt = torch.load(path, map_location="cpu")
if 'state_dict' not in ckpt:
# deepspeed ckpt
state_dict = {}
for k in ckpt.keys():
new_k = k.replace('_forward_module.', '')
state_dict[new_k] = ckpt[k]
else:
state_dict = ckpt["state_dict"]
keys = list(state_dict.keys())
for k in keys:
for ik in ignore_keys:
if ik in k:
print("Deleting key {} from state_dict.".format(k))
del state_dict[k]
missing, unexpected = self.load_state_dict(state_dict, strict=False)
print(f"Restored from {path} with {len(missing)} missing and {len(unexpected)} unexpected keys")
if len(missing) > 0:
print(f"Missing Keys: {missing}")
print(f"Unexpected Keys: {unexpected}")
def on_load_checkpoint(self, checkpoint):
"""
The pt_model is trained separately, so we already have access to its
checkpoint and load it separately with `self.set_pt_model`.
However, the PL Trainer is strict about
checkpoint loading (not configurable), so it expects the loaded state_dict
to match exactly the keys in the model state_dict.
So, when loading the checkpoint, before matching keys, we add all pt_model keys
from self.state_dict() to the checkpoint state dict, so that they match
"""
for key in self.state_dict().keys():
if key.startswith("model_ema") and key not in checkpoint["state_dict"]:
checkpoint["state_dict"][key] = self.state_dict()[key]
def configure_optimizers(self) -> Tuple[List, List]:
lr = self.learning_rate
params_list = []
trainable_parameters = list(self.model.parameters())
params_list.append({'params': trainable_parameters, 'lr': lr})
no_decay = ['bias', 'norm.weight', 'norm.bias', 'norm1.weight', 'norm1.bias', 'norm2.weight', 'norm2.bias']
if self.optimizer_cfg.get('train_image_encoder', False):
image_encoder_parameters = list(self.cond_stage_model.named_parameters())
image_encoder_parameters_decay = [param for name, param in image_encoder_parameters if
not any((no_decay_name in name) for no_decay_name in no_decay)]
image_encoder_parameters_nodecay = [param for name, param in image_encoder_parameters if
any((no_decay_name in name) for no_decay_name in no_decay)]
# filter trainable params
image_encoder_parameters_decay = [param for param in image_encoder_parameters_decay if
param.requires_grad]
image_encoder_parameters_nodecay = [param for param in image_encoder_parameters_nodecay if
param.requires_grad]
print(f"Image Encoder Params: {len(image_encoder_parameters_decay)} decay, ")
print(f"Image Encoder Params: {len(image_encoder_parameters_nodecay)} nodecay, ")
image_encoder_lr = self.optimizer_cfg['image_encoder_lr']
image_encoder_lr_multiply = self.optimizer_cfg.get('image_encoder_lr_multiply', 1.0)
image_encoder_lr = image_encoder_lr if image_encoder_lr is not None else lr * image_encoder_lr_multiply
params_list.append(
{'params': image_encoder_parameters_decay, 'lr': image_encoder_lr,
'weight_decay': 0.05})
params_list.append(
{'params': image_encoder_parameters_nodecay, 'lr': image_encoder_lr,
'weight_decay': 0.})
optimizer = instantiate_from_config(self.optimizer_cfg.optimizer, params=params_list, lr=lr)
if hasattr(self.optimizer_cfg, 'scheduler'):
scheduler_func = instantiate_from_config(
self.optimizer_cfg.scheduler,
max_decay_steps=self.trainer.max_steps,
lr_max=lr
)
scheduler = {
"scheduler": lr_scheduler.LambdaLR(optimizer, lr_lambda=scheduler_func.schedule),
"interval": "step",
"frequency": 1
}
schedulers = [scheduler]
else:
schedulers = []
optimizers = [optimizer]
return optimizers, schedulers
@rank_zero_only
@torch.no_grad()
def on_train_batch_start(self, batch, batch_idx):
# only for very first batch
if self.scale_by_std and self.current_epoch == 0 and self.global_step == 0 \
and batch_idx == 0 and self.ckpt_path is None:
# set rescale weight to 1./std of encodings
print("### USING STD-RESCALING ###")
z_q = self.encode_first_stage(batch[self.first_stage_key])
z = z_q.detach()
del self.z_scale_factor
self.register_buffer("z_scale_factor", 1. / z.flatten().std())
print(f"setting self.z_scale_factor to {self.z_scale_factor}")
print("### USING STD-RESCALING ###")
def on_train_batch_end(self, *args, **kwargs):
if self.ema_config is not None:
self.model_ema(self.model)
def on_train_epoch_start(self) -> None:
pl.seed_everything(self.trainer.global_rank)
def forward(self, batch):
with torch.autocast(device_type="cuda", dtype=torch.bfloat16): #float32 for text
contexts = self.cond_stage_model(image=batch.get('image'), text=batch.get('text'), mask=batch.get('mask'))
# t5_text = contexts['t5_text']['prompt_embeds']
# nan_count = torch.isnan(t5_text).sum()
# if nan_count > 0:
# print("t5_text has %d NaN values"%(nan_count))
with torch.autocast(device_type="cuda", dtype=torch.float16):
with torch.no_grad():
latents = self.first_stage_model.encode(batch[self.first_stage_key], sample_posterior=True)
latents = self.z_scale_factor * latents
# print(latents.shape)
# check vae encode and decode is ok? answer is ok !
# import time
# from hy3dshape.pipelines import export_to_trimesh
# latents = 1. / self.z_scale_factor * latents
# latents = self.first_stage_model(latents)
# outputs = self.first_stage_model.latents2mesh(
# latents,
# bounds=1.01,
# mc_level=0.0,
# num_chunks=20000,
# octree_resolution=256,
# mc_algo='mc',
# enable_pbar=True
# )
# mesh = export_to_trimesh(outputs)
# if isinstance(mesh, list):
# for midx, m in enumerate(mesh):
# m.export(f"check_{midx}_{time.time()}.glb")
# else:
# mesh.export(f"check_{time.time()}.glb")
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
loss = self.transport.training_losses(self.model, latents, dict(contexts=contexts))["loss"].mean()
return loss
def training_step(self, batch, batch_idx, optimizer_idx=0):
loss = self.forward(batch)
split = 'train'
loss_dict = {
f"{split}/simple": loss.detach(),
f"{split}/total_loss": loss.detach(),
f"{split}/lr_abs": self.optimizers().param_groups[0]['lr'],
}
self.log_dict(loss_dict, prog_bar=True, logger=True, sync_dist=False, rank_zero_only=True)
return loss
def validation_step(self, batch, batch_idx, optimizer_idx=0):
loss = self.forward(batch)
split = 'val'
loss_dict = {
f"{split}/simple": loss.detach(),
f"{split}/total_loss": loss.detach(),
f"{split}/lr_abs": self.optimizers().param_groups[0]['lr'],
}
self.log_dict(loss_dict, prog_bar=True, logger=True, sync_dist=False, rank_zero_only=True)
return loss
@torch.no_grad()
def sample(self, batch, output_type='trimesh', **kwargs):
self.cond_stage_model.disable_drop = True
generator = torch.Generator().manual_seed(0)
with self.ema_scope("Sample"):
with torch.amp.autocast(device_type='cuda'):
try:
self.pipeline.device = self.device
self.pipeline.dtype = self.dtype
print("### USING PIPELINE ###")
print(f'device: {self.device} dtype : {self.dtype}')
additional_params = {'output_type':output_type}
image = batch.get("image", None)
mask = batch.get('mask', None)
# if not isinstance(image, torch.Tensor): print(image.shape)
# if isinstance(mask, torch.Tensor): print(mask.shape)
outputs = self.pipeline(image=image,
mask=mask,
generator=generator,
**additional_params)
except Exception as e:
import traceback
traceback.print_exc()
print(f"Unexpected {e=}, {type(e)=}")
with open("error.txt", "a") as f:
f.write(str(e))
f.write(traceback.format_exc())
f.write("\n")
outputs = [None]
self.cond_stage_model.disable_drop = False
return [outputs]
@@ -0,0 +1,97 @@
# This file includes code derived from the SiT project (https://github.com/willisma/SiT),
# which is licensed under the MIT License.
#
# MIT License
#
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
from .transport import Transport, ModelType, WeightType, PathType, Sampler
def create_transport(
path_type='Linear',
prediction="velocity",
loss_weight=None,
train_eps=None,
sample_eps=None,
train_sample_type="uniform",
mean = 0.0,
std = 1.0,
shift_scale = 1.0,
):
"""function for creating Transport object
**Note**: model prediction defaults to velocity
Args:
- path_type: type of path to use; default to linear
- learn_score: set model prediction to score
- learn_noise: set model prediction to noise
- velocity_weighted: weight loss by velocity weight
- likelihood_weighted: weight loss by likelihood weight
- train_eps: small epsilon for avoiding instability during training
- sample_eps: small epsilon for avoiding instability during sampling
"""
if prediction == "noise":
model_type = ModelType.NOISE
elif prediction == "score":
model_type = ModelType.SCORE
else:
model_type = ModelType.VELOCITY
if loss_weight == "velocity":
loss_type = WeightType.VELOCITY
elif loss_weight == "likelihood":
loss_type = WeightType.LIKELIHOOD
else:
loss_type = WeightType.NONE
path_choice = {
"Linear": PathType.LINEAR,
"GVP": PathType.GVP,
"VP": PathType.VP,
}
path_type = path_choice[path_type]
if (path_type in [PathType.VP]):
train_eps = 1e-5 if train_eps is None else train_eps
sample_eps = 1e-3 if train_eps is None else sample_eps
elif (path_type in [PathType.GVP, PathType.LINEAR] and model_type != ModelType.VELOCITY):
train_eps = 1e-3 if train_eps is None else train_eps
sample_eps = 1e-3 if train_eps is None else sample_eps
else: # velocity & [GVP, LINEAR] is stable everywhere
train_eps = 0
sample_eps = 0
# create flow state
state = Transport(
model_type=model_type,
path_type=path_type,
loss_type=loss_type,
train_eps=train_eps,
sample_eps=sample_eps,
train_sample_type=train_sample_type,
mean=mean,
std=std,
shift_scale =shift_scale,
)
return state
@@ -0,0 +1,142 @@
# This file includes code derived from the SiT project (https://github.com/willisma/SiT),
# which is licensed under the MIT License.
#
# MIT License
#
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import numpy as np
import torch as th
import torch.nn as nn
from torchdiffeq import odeint
from functools import partial
from tqdm import tqdm
class sde:
"""SDE solver class"""
def __init__(
self,
drift,
diffusion,
*,
t0,
t1,
num_steps,
sampler_type,
):
assert t0 < t1, "SDE sampler has to be in forward time"
self.num_timesteps = num_steps
self.t = th.linspace(t0, t1, num_steps)
self.dt = self.t[1] - self.t[0]
self.drift = drift
self.diffusion = diffusion
self.sampler_type = sampler_type
def __Euler_Maruyama_step(self, x, mean_x, t, model, **model_kwargs):
w_cur = th.randn(x.size()).to(x)
t = th.ones(x.size(0)).to(x) * t
dw = w_cur * th.sqrt(self.dt)
drift = self.drift(x, t, model, **model_kwargs)
diffusion = self.diffusion(x, t)
mean_x = x + drift * self.dt
x = mean_x + th.sqrt(2 * diffusion) * dw
return x, mean_x
def __Heun_step(self, x, _, t, model, **model_kwargs):
w_cur = th.randn(x.size()).to(x)
dw = w_cur * th.sqrt(self.dt)
t_cur = th.ones(x.size(0)).to(x) * t
diffusion = self.diffusion(x, t_cur)
xhat = x + th.sqrt(2 * diffusion) * dw
K1 = self.drift(xhat, t_cur, model, **model_kwargs)
xp = xhat + self.dt * K1
K2 = self.drift(xp, t_cur + self.dt, model, **model_kwargs)
return xhat + 0.5 * self.dt * (K1 + K2), xhat # at last time point we do not perform the heun step
def __forward_fn(self):
"""TODO: generalize here by adding all private functions ending with steps to it"""
sampler_dict = {
"Euler": self.__Euler_Maruyama_step,
"Heun": self.__Heun_step,
}
try:
sampler = sampler_dict[self.sampler_type]
except:
raise NotImplementedError("Smapler type not implemented.")
return sampler
def sample(self, init, model, **model_kwargs):
"""forward loop of sde"""
x = init
mean_x = init
samples = []
sampler = self.__forward_fn()
for ti in self.t[:-1]:
with th.no_grad():
x, mean_x = sampler(x, mean_x, ti, model, **model_kwargs)
samples.append(x)
return samples
class ode:
"""ODE solver class"""
def __init__(
self,
drift,
*,
t0,
t1,
sampler_type,
num_steps,
atol,
rtol,
):
assert t0 < t1, "ODE sampler has to be in forward time"
self.drift = drift
self.t = th.linspace(t0, t1, num_steps)
self.atol = atol
self.rtol = rtol
self.sampler_type = sampler_type
def sample(self, x, model, **model_kwargs):
device = x[0].device if isinstance(x, tuple) else x.device
def _fn(t, x):
t = th.ones(x[0].size(0)).to(device) * t if isinstance(x, tuple) else th.ones(x.size(0)).to(device) * t
model_output = self.drift(x, t, model, **model_kwargs)
return model_output
t = self.t.to(device)
atol = [self.atol] * len(x) if isinstance(x, tuple) else [self.atol]
rtol = [self.rtol] * len(x) if isinstance(x, tuple) else [self.rtol]
samples = odeint(
_fn,
x,
t,
method=self.sampler_type,
atol=atol,
rtol=rtol
)
return samples
@@ -0,0 +1,220 @@
# This file includes code derived from the SiT project (https://github.com/willisma/SiT),
# which is licensed under the MIT License.
#
# MIT License
#
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import torch as th
import numpy as np
from functools import partial
def expand_t_like_x(t, x):
"""Function to reshape time t to broadcastable dimension of x
Args:
t: [batch_dim,], time vector
x: [batch_dim,...], data point
"""
dims = [1] * (len(x.size()) - 1)
t = t.view(t.size(0), *dims)
return t
#################### Coupling Plans ####################
class ICPlan:
"""Linear Coupling Plan"""
def __init__(self, sigma=0.0):
self.sigma = sigma
def compute_alpha_t(self, t):
"""Compute the data coefficient along the path"""
return t, 1
def compute_sigma_t(self, t):
"""Compute the noise coefficient along the path"""
return 1 - t, -1
def compute_d_alpha_alpha_ratio_t(self, t):
"""Compute the ratio between d_alpha and alpha"""
return 1 / t
def compute_drift(self, x, t):
"""We always output sde according to score parametrization; """
t = expand_t_like_x(t, x)
alpha_ratio = self.compute_d_alpha_alpha_ratio_t(t)
sigma_t, d_sigma_t = self.compute_sigma_t(t)
drift = alpha_ratio * x
diffusion = alpha_ratio * (sigma_t ** 2) - sigma_t * d_sigma_t
return -drift, diffusion
def compute_diffusion(self, x, t, form="constant", norm=1.0):
"""Compute the diffusion term of the SDE
Args:
x: [batch_dim, ...], data point
t: [batch_dim,], time vector
form: str, form of the diffusion term
norm: float, norm of the diffusion term
"""
t = expand_t_like_x(t, x)
choices = {
"constant": norm,
"SBDM": norm * self.compute_drift(x, t)[1],
"sigma": norm * self.compute_sigma_t(t)[0],
"linear": norm * (1 - t),
"decreasing": 0.25 * (norm * th.cos(np.pi * t) + 1) ** 2,
"inccreasing-decreasing": norm * th.sin(np.pi * t) ** 2,
}
try:
diffusion = choices[form]
except KeyError:
raise NotImplementedError(f"Diffusion form {form} not implemented")
return diffusion
def get_score_from_velocity(self, velocity, x, t):
"""Wrapper function: transfrom velocity prediction model to score
Args:
velocity: [batch_dim, ...] shaped tensor; velocity model output
x: [batch_dim, ...] shaped tensor; x_t data point
t: [batch_dim,] time tensor
"""
t = expand_t_like_x(t, x)
alpha_t, d_alpha_t = self.compute_alpha_t(t)
sigma_t, d_sigma_t = self.compute_sigma_t(t)
mean = x
reverse_alpha_ratio = alpha_t / d_alpha_t
var = sigma_t**2 - reverse_alpha_ratio * d_sigma_t * sigma_t
score = (reverse_alpha_ratio * velocity - mean) / var
return score
def get_noise_from_velocity(self, velocity, x, t):
"""Wrapper function: transfrom velocity prediction model to denoiser
Args:
velocity: [batch_dim, ...] shaped tensor; velocity model output
x: [batch_dim, ...] shaped tensor; x_t data point
t: [batch_dim,] time tensor
"""
t = expand_t_like_x(t, x)
alpha_t, d_alpha_t = self.compute_alpha_t(t)
sigma_t, d_sigma_t = self.compute_sigma_t(t)
mean = x
reverse_alpha_ratio = alpha_t / d_alpha_t
var = reverse_alpha_ratio * d_sigma_t - sigma_t
noise = (reverse_alpha_ratio * velocity - mean) / var
return noise
def get_velocity_from_score(self, score, x, t):
"""Wrapper function: transfrom score prediction model to velocity
Args:
score: [batch_dim, ...] shaped tensor; score model output
x: [batch_dim, ...] shaped tensor; x_t data point
t: [batch_dim,] time tensor
"""
t = expand_t_like_x(t, x)
drift, var = self.compute_drift(x, t)
velocity = var * score - drift
return velocity
def compute_mu_t(self, t, x0, x1):
"""Compute the mean of time-dependent density p_t"""
t = expand_t_like_x(t, x1)
alpha_t, _ = self.compute_alpha_t(t)
sigma_t, _ = self.compute_sigma_t(t)
# t*x1 + (1-t)*x0 ; t=0 x0; t=1 x1
return alpha_t * x1 + sigma_t * x0
def compute_xt(self, t, x0, x1):
"""Sample xt from time-dependent density p_t; rng is required"""
xt = self.compute_mu_t(t, x0, x1)
return xt
def compute_ut(self, t, x0, x1, xt):
"""Compute the vector field corresponding to p_t"""
t = expand_t_like_x(t, x1)
_, d_alpha_t = self.compute_alpha_t(t)
_, d_sigma_t = self.compute_sigma_t(t)
return d_alpha_t * x1 + d_sigma_t * x0
def plan(self, t, x0, x1):
xt = self.compute_xt(t, x0, x1)
ut = self.compute_ut(t, x0, x1, xt)
return t, xt, ut
class VPCPlan(ICPlan):
"""class for VP path flow matching"""
def __init__(self, sigma_min=0.1, sigma_max=20.0):
self.sigma_min = sigma_min
self.sigma_max = sigma_max
self.log_mean_coeff = lambda t: -0.25 * ((1 - t) ** 2) * \
(self.sigma_max - self.sigma_min) - 0.5 * (1 - t) * self.sigma_min
self.d_log_mean_coeff = lambda t: 0.5 * (1 - t) * \
(self.sigma_max - self.sigma_min) + 0.5 * self.sigma_min
def compute_alpha_t(self, t):
"""Compute coefficient of x1"""
alpha_t = self.log_mean_coeff(t)
alpha_t = th.exp(alpha_t)
d_alpha_t = alpha_t * self.d_log_mean_coeff(t)
return alpha_t, d_alpha_t
def compute_sigma_t(self, t):
"""Compute coefficient of x0"""
p_sigma_t = 2 * self.log_mean_coeff(t)
sigma_t = th.sqrt(1 - th.exp(p_sigma_t))
d_sigma_t = th.exp(p_sigma_t) * (2 * self.d_log_mean_coeff(t)) / (-2 * sigma_t)
return sigma_t, d_sigma_t
def compute_d_alpha_alpha_ratio_t(self, t):
"""Special purposed function for computing numerical stabled d_alpha_t / alpha_t"""
return self.d_log_mean_coeff(t)
def compute_drift(self, x, t):
"""Compute the drift term of the SDE"""
t = expand_t_like_x(t, x)
beta_t = self.sigma_min + (1 - t) * (self.sigma_max - self.sigma_min)
return -0.5 * beta_t * x, beta_t / 2
class GVPCPlan(ICPlan):
def __init__(self, sigma=0.0):
super().__init__(sigma)
def compute_alpha_t(self, t):
"""Compute coefficient of x1"""
alpha_t = th.sin(t * np.pi / 2)
d_alpha_t = np.pi / 2 * th.cos(t * np.pi / 2)
return alpha_t, d_alpha_t
def compute_sigma_t(self, t):
"""Compute coefficient of x0"""
sigma_t = th.cos(t * np.pi / 2)
d_sigma_t = -np.pi / 2 * th.sin(t * np.pi / 2)
return sigma_t, d_sigma_t
def compute_d_alpha_alpha_ratio_t(self, t):
"""Special purposed function for computing numerical stabled d_alpha_t / alpha_t"""
return np.pi / (2 * th.tan(t * np.pi / 2))
@@ -0,0 +1,534 @@
# This file includes code derived from the SiT project (https://github.com/willisma/SiT),
# which is licensed under the MIT License.
#
# MIT License
#
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import torch as th
import numpy as np
import logging
import enum
from . import path
from .utils import EasyDict, log_state, mean_flat
from .integrators import ode, sde
class ModelType(enum.Enum):
"""
Which type of output the model predicts.
"""
NOISE = enum.auto() # the model predicts epsilon
SCORE = enum.auto() # the model predicts \nabla \log p(x)
VELOCITY = enum.auto() # the model predicts v(x)
class PathType(enum.Enum):
"""
Which type of path to use.
"""
LINEAR = enum.auto()
GVP = enum.auto()
VP = enum.auto()
class WeightType(enum.Enum):
"""
Which type of weighting to use.
"""
NONE = enum.auto()
VELOCITY = enum.auto()
LIKELIHOOD = enum.auto()
class Transport:
def __init__(
self,
*,
model_type,
path_type,
loss_type,
train_eps,
sample_eps,
train_sample_type = "uniform",
**kwargs,
):
path_options = {
PathType.LINEAR: path.ICPlan,
PathType.GVP: path.GVPCPlan,
PathType.VP: path.VPCPlan,
}
self.loss_type = loss_type
self.model_type = model_type
self.path_sampler = path_options[path_type]()
self.train_eps = train_eps
self.sample_eps = sample_eps
self.train_sample_type = train_sample_type
if self.train_sample_type == "logit_normal":
self.mean = kwargs['mean']
self.std = kwargs['std']
self.shift_scale = kwargs['shift_scale']
print(f"using logit normal sample, shift scale is {self.shift_scale}")
def prior_logp(self, z):
'''
Standard multivariate normal prior
Assume z is batched
'''
shape = th.tensor(z.size())
N = th.prod(shape[1:])
_fn = lambda x: -N / 2. * np.log(2 * np.pi) - th.sum(x ** 2) / 2.
return th.vmap(_fn)(z)
def check_interval(
self,
train_eps,
sample_eps,
*,
diffusion_form="SBDM",
sde=False,
reverse=False,
eval=False,
last_step_size=0.0,
):
t0 = 0
t1 = 1
eps = train_eps if not eval else sample_eps
if (type(self.path_sampler) in [path.VPCPlan]):
t1 = 1 - eps if (not sde or last_step_size == 0) else 1 - last_step_size
elif (type(self.path_sampler) in [path.ICPlan, path.GVPCPlan]) \
and (
self.model_type != ModelType.VELOCITY or sde): # avoid numerical issue by taking a first semi-implicit step
t0 = eps if (diffusion_form == "SBDM" and sde) or self.model_type != ModelType.VELOCITY else 0
t1 = 1 - eps if (not sde or last_step_size == 0) else 1 - last_step_size
if reverse:
t0, t1 = 1 - t0, 1 - t1
return t0, t1
def sample(self, x1):
"""Sampling x0 & t based on shape of x1 (if needed)
Args:
x1 - data point; [batch, *dim]
"""
x0 = th.randn_like(x1)
if self.train_sample_type=="uniform":
t0, t1 = self.check_interval(self.train_eps, self.sample_eps)
t = th.rand((x1.shape[0],)) * (t1 - t0) + t0
t = t.to(x1)
elif self.train_sample_type=="logit_normal":
t = th.randn((x1.shape[0],)) * self.std + self.mean
t = t.to(x1)
t = 1/(1+th.exp(-t))
t = np.sqrt(self.shift_scale)*t/(1+(np.sqrt(self.shift_scale)-1)*t)
return t, x0, x1
def training_losses(
self,
model,
x1,
model_kwargs=None
):
"""Loss for training the score model
Args:
- model: backbone model; could be score, noise, or velocity
- x1: datapoint
- model_kwargs: additional arguments for the model
"""
if model_kwargs == None:
model_kwargs = {}
t, x0, x1 = self.sample(x1)
t, xt, ut = self.path_sampler.plan(t, x0, x1)
model_output = model(xt, t, **model_kwargs)
B, *_, C = xt.shape
assert model_output.size() == (B, *xt.size()[1:-1], C)
terms = {}
terms['pred'] = model_output
if self.model_type == ModelType.VELOCITY:
terms['loss'] = mean_flat(((model_output - ut) ** 2))
else:
_, drift_var = self.path_sampler.compute_drift(xt, t)
sigma_t, _ = self.path_sampler.compute_sigma_t(path.expand_t_like_x(t, xt))
if self.loss_type in [WeightType.VELOCITY]:
weight = (drift_var / sigma_t) ** 2
elif self.loss_type in [WeightType.LIKELIHOOD]:
weight = drift_var / (sigma_t ** 2)
elif self.loss_type in [WeightType.NONE]:
weight = 1
else:
raise NotImplementedError()
if self.model_type == ModelType.NOISE:
terms['loss'] = mean_flat(weight * ((model_output - x0) ** 2))
else:
terms['loss'] = mean_flat(weight * ((model_output * sigma_t + x0) ** 2))
return terms
def get_drift(
self
):
"""member function for obtaining the drift of the probability flow ODE"""
def score_ode(x, t, model, **model_kwargs):
drift_mean, drift_var = self.path_sampler.compute_drift(x, t)
model_output = model(x, t, **model_kwargs)
return (-drift_mean + drift_var * model_output) # by change of variable
def noise_ode(x, t, model, **model_kwargs):
drift_mean, drift_var = self.path_sampler.compute_drift(x, t)
sigma_t, _ = self.path_sampler.compute_sigma_t(path.expand_t_like_x(t, x))
model_output = model(x, t, **model_kwargs)
score = model_output / -sigma_t
return (-drift_mean + drift_var * score)
def velocity_ode(x, t, model, **model_kwargs):
model_output = model(x, t, **model_kwargs)
return model_output
if self.model_type == ModelType.NOISE:
drift_fn = noise_ode
elif self.model_type == ModelType.SCORE:
drift_fn = score_ode
else:
drift_fn = velocity_ode
def body_fn(x, t, model, **model_kwargs):
model_output = drift_fn(x, t, model, **model_kwargs)
assert model_output.shape == x.shape, "Output shape from ODE solver must match input shape"
return model_output
return body_fn
def get_score(
self,
):
"""member function for obtaining score of
x_t = alpha_t * x + sigma_t * eps"""
if self.model_type == ModelType.NOISE:
score_fn = lambda x, t, model, **kwargs: model(x, t, **kwargs) / - \
self.path_sampler.compute_sigma_t(path.expand_t_like_x(t, x))[0]
elif self.model_type == ModelType.SCORE:
score_fn = lambda x, t, model, **kwagrs: model(x, t, **kwagrs)
elif self.model_type == ModelType.VELOCITY:
score_fn = lambda x, t, model, **kwargs: self.path_sampler.get_score_from_velocity(model(x, t, **kwargs), x,
t)
else:
raise NotImplementedError()
return score_fn
class Sampler:
"""Sampler class for the transport model"""
def __init__(
self,
transport,
):
"""Constructor for a general sampler; supporting different sampling methods
Args:
- transport: an tranport object specify model prediction & interpolant type
"""
self.transport = transport
self.drift = self.transport.get_drift()
self.score = self.transport.get_score()
def __get_sde_diffusion_and_drift(
self,
*,
diffusion_form="SBDM",
diffusion_norm=1.0,
):
def diffusion_fn(x, t):
diffusion = self.transport.path_sampler.compute_diffusion(x, t, form=diffusion_form, norm=diffusion_norm)
return diffusion
sde_drift = \
lambda x, t, model, **kwargs: \
self.drift(x, t, model, **kwargs) + diffusion_fn(x, t) * self.score(x, t, model, **kwargs)
sde_diffusion = diffusion_fn
return sde_drift, sde_diffusion
def __get_last_step(
self,
sde_drift,
*,
last_step,
last_step_size,
):
"""Get the last step function of the SDE solver"""
if last_step is None:
last_step_fn = \
lambda x, t, model, **model_kwargs: \
x
elif last_step == "Mean":
last_step_fn = \
lambda x, t, model, **model_kwargs: \
x + sde_drift(x, t, model, **model_kwargs) * last_step_size
elif last_step == "Tweedie":
alpha = self.transport.path_sampler.compute_alpha_t # simple aliasing; the original name was too long
sigma = self.transport.path_sampler.compute_sigma_t
last_step_fn = \
lambda x, t, model, **model_kwargs: \
x / alpha(t)[0][0] + (sigma(t)[0][0] ** 2) / alpha(t)[0][0] * self.score(x, t, model,
**model_kwargs)
elif last_step == "Euler":
last_step_fn = \
lambda x, t, model, **model_kwargs: \
x + self.drift(x, t, model, **model_kwargs) * last_step_size
else:
raise NotImplementedError()
return last_step_fn
def sample_sde(
self,
*,
sampling_method="Euler",
diffusion_form="SBDM",
diffusion_norm=1.0,
last_step="Mean",
last_step_size=0.04,
num_steps=250,
):
"""returns a sampling function with given SDE settings
Args:
- sampling_method: type of sampler used in solving the SDE; default to be Euler-Maruyama
- diffusion_form: function form of diffusion coefficient; default to be matching SBDM
- diffusion_norm: function magnitude of diffusion coefficient; default to 1
- last_step: type of the last step; default to identity
- last_step_size: size of the last step; default to match the stride of 250 steps over [0,1]
- num_steps: total integration step of SDE
"""
if last_step is None:
last_step_size = 0.0
sde_drift, sde_diffusion = self.__get_sde_diffusion_and_drift(
diffusion_form=diffusion_form,
diffusion_norm=diffusion_norm,
)
t0, t1 = self.transport.check_interval(
self.transport.train_eps,
self.transport.sample_eps,
diffusion_form=diffusion_form,
sde=True,
eval=True,
reverse=False,
last_step_size=last_step_size,
)
_sde = sde(
sde_drift,
sde_diffusion,
t0=t0,
t1=t1,
num_steps=num_steps,
sampler_type=sampling_method
)
last_step_fn = self.__get_last_step(sde_drift, last_step=last_step, last_step_size=last_step_size)
def _sample(init, model, **model_kwargs):
xs = _sde.sample(init, model, **model_kwargs)
ts = th.ones(init.size(0), device=init.device) * t1
x = last_step_fn(xs[-1], ts, model, **model_kwargs)
xs.append(x)
assert len(xs) == num_steps, "Samples does not match the number of steps"
return xs
return _sample
def sample_ode(
self,
*,
sampling_method="dopri5",
num_steps=50,
atol=1e-6,
rtol=1e-3,
reverse=False,
):
"""returns a sampling function with given ODE settings
Args:
- sampling_method: type of sampler used in solving the ODE; default to be Dopri5
- num_steps:
- fixed solver (Euler, Heun): the actual number of integration steps performed
- adaptive solver (Dopri5): the number of datapoints saved during integration; produced by interpolation
- atol: absolute error tolerance for the solver
- rtol: relative error tolerance for the solver
- reverse: whether solving the ODE in reverse (data to noise); default to False
"""
if reverse:
drift = lambda x, t, model, **kwargs: self.drift(x, th.ones_like(t) * (1 - t), model, **kwargs)
else:
drift = self.drift
t0, t1 = self.transport.check_interval(
self.transport.train_eps,
self.transport.sample_eps,
sde=False,
eval=True,
reverse=reverse,
last_step_size=0.0,
)
_ode = ode(
drift=drift,
t0=t0,
t1=t1,
sampler_type=sampling_method,
num_steps=num_steps,
atol=atol,
rtol=rtol,
)
return _ode.sample
def sample_ode_intermediate(
self,
*,
sampling_method="dopri5",
num_steps=50,
atol=1e-6,
rtol=1e-3,
t=0.5,
reverse=False,
):
"""returns a sampling function with given ODE settings
Args:
- sampling_method: type of sampler used in solving the ODE; default to be Dopri5
- num_steps:
- fixed solver (Euler, Heun): the actual number of integration steps performed
- adaptive solver (Dopri5): the number of datapoints saved during integration; produced by interpolation
- atol: absolute error tolerance for the solver
- rtol: relative error tolerance for the solver
- reverse: whether solving the ODE in reverse (data to noise); default to False
"""
if reverse:
drift = lambda x, t, model, **kwargs: self.drift(x, th.ones_like(t) * (1 - t), model, **kwargs)
else:
drift = self.drift
t0, t1 = self.transport.check_interval(
self.transport.train_eps,
self.transport.sample_eps,
sde=False,
eval=True,
reverse=reverse,
last_step_size=0.0,
)
_ode = ode(
drift=drift,
t0=t,
t1=t1,
sampler_type=sampling_method,
num_steps=num_steps,
atol=atol,
rtol=rtol,
)
return _ode.sample
def sample_ode_likelihood(
self,
*,
sampling_method="dopri5",
num_steps=50,
atol=1e-6,
rtol=1e-3,
):
"""returns a sampling function for calculating likelihood with given ODE settings
Args:
- sampling_method: type of sampler used in solving the ODE; default to be Dopri5
- num_steps:
- fixed solver (Euler, Heun): the actual number of integration steps performed
- adaptive solver (Dopri5): the number of datapoints saved during integration; produced by interpolation
- atol: absolute error tolerance for the solver
- rtol: relative error tolerance for the solver
"""
def _likelihood_drift(x, t, model, **model_kwargs):
x, _ = x
eps = th.randint(2, x.size(), dtype=th.float, device=x.device) * 2 - 1
t = th.ones_like(t) * (1 - t)
with th.enable_grad():
x.requires_grad = True
grad = th.autograd.grad(th.sum(self.drift(x, t, model, **model_kwargs) * eps), x)[0]
logp_grad = th.sum(grad * eps, dim=tuple(range(1, len(x.size()))))
drift = self.drift(x, t, model, **model_kwargs)
return (-drift, logp_grad)
t0, t1 = self.transport.check_interval(
self.transport.train_eps,
self.transport.sample_eps,
sde=False,
eval=True,
reverse=False,
last_step_size=0.0,
)
_ode = ode(
drift=_likelihood_drift,
t0=t0,
t1=t1,
sampler_type=sampling_method,
num_steps=num_steps,
atol=atol,
rtol=rtol,
)
def _sample_fn(x, model, **model_kwargs):
init_logp = th.zeros(x.size(0)).to(x)
input = (x, init_logp)
drift, delta_logp = _ode.sample(input, model, **model_kwargs)
drift, delta_logp = drift[-1], delta_logp[-1]
prior_logp = self.transport.prior_logp(drift)
logp = prior_logp - delta_logp
return logp, drift
return _sample_fn
@@ -0,0 +1,54 @@
# This file includes code derived from the SiT project (https://github.com/willisma/SiT),
# which is licensed under the MIT License.
#
# MIT License
#
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import torch as th
class EasyDict:
def __init__(self, sub_dict):
for k, v in sub_dict.items():
setattr(self, k, v)
def __getitem__(self, key):
return getattr(self, key)
def mean_flat(x):
"""
Take the mean over all non-batch dimensions.
"""
return th.mean(x, dim=list(range(1, len(x.size()))))
def log_state(state):
result = []
sorted_state = dict(sorted(state.items()))
for key, value in sorted_state.items():
# Check if the value is an instance of a class
if "<object" in str(value) or "object at" in str(value):
result.append(f"{key}: [{value.__class__.__name__}]")
else:
result.append(f"{key}: {value}")
return '\n'.join(result)
@@ -0,0 +1,783 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import copy
import importlib
import inspect
import os
from typing import List, Optional, Union
import numpy as np
import torch
import trimesh
import yaml
from PIL import Image
from diffusers.utils.torch_utils import randn_tensor
from diffusers.utils.import_utils import is_accelerate_version, is_accelerate_available
from tqdm import tqdm
from .models.autoencoders import ShapeVAE
from .models.autoencoders import SurfaceExtractors
from .utils import logger, synchronize_timer, smart_load_model
def retrieve_timesteps(
scheduler,
num_inference_steps: Optional[int] = None,
device: Optional[Union[str, torch.device]] = None,
timesteps: Optional[List[int]] = None,
sigmas: Optional[List[float]] = None,
**kwargs,
):
"""
Calls the scheduler's `set_timesteps` method and retrieves timesteps from the scheduler after the call. Handles
custom timesteps. Any kwargs will be supplied to `scheduler.set_timesteps`.
Args:
scheduler (`SchedulerMixin`):
The scheduler to get timesteps from.
num_inference_steps (`int`):
The number of diffusion steps used when generating samples with a pre-trained model. If used, `timesteps`
must be `None`.
device (`str` or `torch.device`, *optional*):
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
timesteps (`List[int]`, *optional*):
Custom timesteps used to override the timestep spacing strategy of the scheduler. If `timesteps` is passed,
`num_inference_steps` and `sigmas` must be `None`.
sigmas (`List[float]`, *optional*):
Custom sigmas used to override the timestep spacing strategy of the scheduler. If `sigmas` is passed,
`num_inference_steps` and `timesteps` must be `None`.
Returns:
`Tuple[torch.Tensor, int]`: A tuple where the first element is the timestep schedule from the scheduler and the
second element is the number of inference steps.
"""
if timesteps is not None and sigmas is not None:
raise ValueError("Only one of `timesteps` or `sigmas` can be passed. Please choose one to set custom values")
if timesteps is not None:
accepts_timesteps = "timesteps" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
if not accepts_timesteps:
raise ValueError(
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
f" timestep schedules. Please check whether you are using the correct scheduler."
)
scheduler.set_timesteps(timesteps=timesteps, device=device, **kwargs)
timesteps = scheduler.timesteps
num_inference_steps = len(timesteps)
elif sigmas is not None:
accept_sigmas = "sigmas" in set(inspect.signature(scheduler.set_timesteps).parameters.keys())
if not accept_sigmas:
raise ValueError(
f"The current scheduler class {scheduler.__class__}'s `set_timesteps` does not support custom"
f" sigmas schedules. Please check whether you are using the correct scheduler."
)
scheduler.set_timesteps(sigmas=sigmas, device=device, **kwargs)
timesteps = scheduler.timesteps
num_inference_steps = len(timesteps)
else:
scheduler.set_timesteps(num_inference_steps, device=device, **kwargs)
timesteps = scheduler.timesteps
return timesteps, num_inference_steps
@synchronize_timer('Export to trimesh')
def export_to_trimesh(mesh_output):
if isinstance(mesh_output, list):
outputs = []
for mesh in mesh_output:
if mesh is None:
outputs.append(None)
else:
mesh.mesh_f = mesh.mesh_f[:, ::-1]
mesh_output = trimesh.Trimesh(mesh.mesh_v, mesh.mesh_f)
outputs.append(mesh_output)
return outputs
else:
mesh_output.mesh_f = mesh_output.mesh_f[:, ::-1]
mesh_output = trimesh.Trimesh(mesh_output.mesh_v, mesh_output.mesh_f)
return mesh_output
def get_obj_from_str(string, reload=False):
module, cls = string.rsplit(".", 1)
if reload:
module_imp = importlib.import_module(module)
importlib.reload(module_imp)
return getattr(importlib.import_module(module, package=None), cls)
def instantiate_from_config(config, **kwargs):
if "target" not in config:
raise KeyError("Expected key `target` to instantiate.")
cls = get_obj_from_str(config["target"])
params = config.get("params", dict())
kwargs.update(params)
instance = cls(**kwargs)
return instance
class Hunyuan3DDiTPipeline:
model_cpu_offload_seq = "conditioner->model->vae"
_exclude_from_cpu_offload = []
@classmethod
@synchronize_timer('Hunyuan3DDiTPipeline Model Loading')
def from_single_file(
cls,
ckpt_path,
config_path,
device='cuda',
dtype=torch.float16,
use_safetensors=None,
**kwargs,
):
# load config
with open(config_path, 'r') as f:
config = yaml.safe_load(f)
# load ckpt
if use_safetensors:
ckpt_path = ckpt_path.replace('.ckpt', '.safetensors')
if not os.path.exists(ckpt_path):
raise FileNotFoundError(f"Model file {ckpt_path} not found")
# logger.info(f"Loading model from {ckpt_path}")
if use_safetensors:
# parse safetensors
import safetensors.torch
safetensors_ckpt = safetensors.torch.load_file(ckpt_path, device='cpu')
ckpt = {}
for key, value in safetensors_ckpt.items():
model_name = key.split('.')[0]
new_key = key[len(model_name) + 1:]
if model_name not in ckpt:
ckpt[model_name] = {}
ckpt[model_name][new_key] = value
else:
ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=True)
# load model
model = instantiate_from_config(config['model'])
model.load_state_dict(ckpt['model'])
vae = instantiate_from_config(config['vae'])
vae.load_state_dict(ckpt['vae'], strict=False)
conditioner = instantiate_from_config(config['conditioner'])
if 'conditioner' in ckpt:
conditioner.load_state_dict(ckpt['conditioner'])
image_processor = instantiate_from_config(config['image_processor'])
scheduler = instantiate_from_config(config['scheduler'])
model_kwargs = dict(
vae=vae,
model=model,
scheduler=scheduler,
conditioner=conditioner,
image_processor=image_processor,
device=device,
dtype=dtype,
)
model_kwargs.update(kwargs)
return cls(
**model_kwargs
)
@classmethod
def from_pretrained(
cls,
model_path,
device='cuda',
dtype=torch.float16,
use_safetensors=False,
variant='fp16',
subfolder='hunyuan3d-dit-v2-1',
**kwargs,
):
kwargs['from_pretrained_kwargs'] = dict(
model_path=model_path,
subfolder=subfolder,
use_safetensors=use_safetensors,
variant=variant,
dtype=dtype,
device=device,
)
config_path, ckpt_path = smart_load_model(
model_path,
subfolder=subfolder,
use_safetensors=use_safetensors,
variant=variant
)
return cls.from_single_file(
ckpt_path,
config_path,
device=device,
dtype=dtype,
use_safetensors=use_safetensors,
**kwargs
)
def __init__(
self,
vae,
model,
scheduler,
conditioner,
image_processor,
device='cuda',
dtype=torch.float16,
**kwargs
):
self.vae = vae
self.model = model
self.scheduler = scheduler
self.conditioner = conditioner
self.image_processor = image_processor
self.kwargs = kwargs
self.to(device, dtype)
def compile(self):
self.vae = torch.compile(self.vae)
self.model = torch.compile(self.model)
self.conditioner = torch.compile(self.conditioner)
def enable_flashvdm(
self,
enabled: bool = True,
adaptive_kv_selection=True,
topk_mode='mean',
mc_algo='mc',
replace_vae=True,
):
if enabled:
model_path = self.kwargs['from_pretrained_kwargs']['model_path']
turbo_vae_mapping = {
'Hunyuan3D-2': ('tencent/Hunyuan3D-2', 'hunyuan3d-vae-v2-0-turbo'),
'Hunyuan3D-2mv': ('tencent/Hunyuan3D-2', 'hunyuan3d-vae-v2-0-turbo'),
'Hunyuan3D-2mini': ('tencent/Hunyuan3D-2mini', 'hunyuan3d-vae-v2-mini-turbo'),
}
model_name = model_path.split('/')[-1]
if replace_vae and model_name in turbo_vae_mapping:
model_path, subfolder = turbo_vae_mapping[model_name]
self.vae = ShapeVAE.from_pretrained(
model_path, subfolder=subfolder,
use_safetensors=self.kwargs['from_pretrained_kwargs']['use_safetensors'],
device=self.device,
)
self.vae.enable_flashvdm_decoder(
enabled=enabled,
adaptive_kv_selection=adaptive_kv_selection,
topk_mode=topk_mode,
mc_algo=mc_algo
)
else:
model_path = self.kwargs['from_pretrained_kwargs']['model_path']
vae_mapping = {
'Hunyuan3D-2': ('tencent/Hunyuan3D-2', 'hunyuan3d-vae-v2-0'),
'Hunyuan3D-2mv': ('tencent/Hunyuan3D-2', 'hunyuan3d-vae-v2-0'),
'Hunyuan3D-2mini': ('tencent/Hunyuan3D-2mini', 'hunyuan3d-vae-v2-mini'),
}
model_name = model_path.split('/')[-1]
if model_name in vae_mapping:
model_path, subfolder = vae_mapping[model_name]
self.vae = ShapeVAE.from_pretrained(model_path, subfolder=subfolder)
self.vae.enable_flashvdm_decoder(enabled=False)
def to(self, device=None, dtype=None):
if dtype is not None:
self.dtype = dtype
self.vae.to(dtype=dtype)
self.model.to(dtype=dtype)
self.conditioner.to(dtype=dtype)
if device is not None:
self.device = torch.device(device)
self.vae.to(device)
self.model.to(device)
self.conditioner.to(device)
@property
def _execution_device(self):
r"""
Returns the device on which the pipeline's models will be executed. After calling
[`~DiffusionPipeline.enable_sequential_cpu_offload`] the execution device can only be inferred from
Accelerate's module hooks.
"""
for name, model in self.components.items():
if not isinstance(model, torch.nn.Module) or name in self._exclude_from_cpu_offload:
continue
if not hasattr(model, "_hf_hook"):
return self.device
for module in model.modules():
if (
hasattr(module, "_hf_hook")
and hasattr(module._hf_hook, "execution_device")
and module._hf_hook.execution_device is not None
):
return torch.device(module._hf_hook.execution_device)
return self.device
def enable_model_cpu_offload(self, gpu_id: Optional[int] = None, device: Union[torch.device, str] = "cuda"):
r"""
Offloads all models to CPU using accelerate, reducing memory usage with a low impact on performance. Compared
to `enable_sequential_cpu_offload`, this method moves one whole model at a time to the GPU when its `forward`
method is called, and the model remains in GPU until the next model runs. Memory savings are lower than with
`enable_sequential_cpu_offload`, but performance is much better due to the iterative execution of the `unet`.
Arguments:
gpu_id (`int`, *optional*):
The ID of the accelerator that shall be used in inference. If not specified, it will default to 0.
device (`torch.Device` or `str`, *optional*, defaults to "cuda"):
The PyTorch device type of the accelerator that shall be used in inference. If not specified, it will
default to "cuda".
"""
if self.model_cpu_offload_seq is None:
raise ValueError(
"Model CPU offload cannot be enabled because no `model_cpu_offload_seq` class attribute is set."
)
if is_accelerate_available() and is_accelerate_version(">=", "0.17.0.dev0"):
from accelerate import cpu_offload_with_hook
else:
raise ImportError("`enable_model_cpu_offload` requires `accelerate v0.17.0` or higher.")
torch_device = torch.device(device)
device_index = torch_device.index
if gpu_id is not None and device_index is not None:
raise ValueError(
f"You have passed both `gpu_id`={gpu_id} and an index as part of the passed device `device`={device}"
f"Cannot pass both. Please make sure to either not define `gpu_id` or not pass the index as part of "
f"the device: `device`={torch_device.type}"
)
# _offload_gpu_id should be set to passed gpu_id (or id in passed `device`)
# or default to previously set id or default to 0
self._offload_gpu_id = gpu_id or torch_device.index or getattr(self, "_offload_gpu_id", 0)
device_type = torch_device.type
device = torch.device(f"{device_type}:{self._offload_gpu_id}")
if self.device.type != "cpu":
self.to("cpu")
device_mod = getattr(torch, self.device.type, None)
if hasattr(device_mod, "empty_cache") and device_mod.is_available():
device_mod.empty_cache()
# otherwise we don't see the memory savings (but they probably exist)
all_model_components = {k: v for k, v in self.components.items() if isinstance(v, torch.nn.Module)}
self._all_hooks = []
hook = None
for model_str in self.model_cpu_offload_seq.split("->"):
model = all_model_components.pop(model_str, None)
if not isinstance(model, torch.nn.Module):
continue
_, hook = cpu_offload_with_hook(model, device, prev_module_hook=hook)
self._all_hooks.append(hook)
# CPU offload models that are not in the seq chain unless they are explicitly excluded
# these models will stay on CPU until maybe_free_model_hooks is called
# some models cannot be in the seq chain because they are iteratively called,
# such as controlnet
for name, model in all_model_components.items():
if not isinstance(model, torch.nn.Module):
continue
if name in self._exclude_from_cpu_offload:
model.to(device)
else:
_, hook = cpu_offload_with_hook(model, device)
self._all_hooks.append(hook)
def maybe_free_model_hooks(self):
r"""
Function that offloads all components, removes all model hooks that were added when using
`enable_model_cpu_offload` and then applies them again. In case the model has not been offloaded this function
is a no-op. Make sure to add this function to the end of the `__call__` function of your pipeline so that it
functions correctly when applying enable_model_cpu_offload.
"""
if not hasattr(self, "_all_hooks") or len(self._all_hooks) == 0:
# `enable_model_cpu_offload` has not be called, so silently do nothing
return
for hook in self._all_hooks:
# offload model and remove hook from model
hook.offload()
hook.remove()
# make sure the model is in the same state as before calling it
self.enable_model_cpu_offload()
@synchronize_timer('Encode cond')
def encode_cond(self, image, additional_cond_inputs, do_classifier_free_guidance, dual_guidance):
bsz = image.shape[0]
cond = self.conditioner(image=image, **additional_cond_inputs)
if do_classifier_free_guidance:
un_cond = self.conditioner.unconditional_embedding(bsz, **additional_cond_inputs)
if dual_guidance:
un_cond_drop_main = copy.deepcopy(un_cond)
un_cond_drop_main['additional'] = cond['additional']
def cat_recursive(a, b, c):
if isinstance(a, torch.Tensor):
return torch.cat([a, b, c], dim=0).to(self.dtype)
out = {}
for k in a.keys():
out[k] = cat_recursive(a[k], b[k], c[k])
return out
cond = cat_recursive(cond, un_cond_drop_main, un_cond)
else:
def cat_recursive(a, b):
if isinstance(a, torch.Tensor):
return torch.cat([a, b], dim=0).to(self.dtype)
out = {}
for k in a.keys():
out[k] = cat_recursive(a[k], b[k])
return out
cond = cat_recursive(cond, un_cond)
return cond
def prepare_extra_step_kwargs(self, generator, eta):
# prepare extra kwargs for the scheduler step, since not all schedulers have the same signature
# eta (η) is only used with the DDIMScheduler, it will be ignored for other schedulers.
# eta corresponds to η in DDIM paper: https://arxiv.org/abs/2010.02502
# and should be between [0, 1]
accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys())
extra_step_kwargs = {}
if accepts_eta:
extra_step_kwargs["eta"] = eta
# check if the scheduler accepts generator
accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys())
if accepts_generator:
extra_step_kwargs["generator"] = generator
return extra_step_kwargs
def prepare_latents(self, batch_size, dtype, device, generator, latents=None):
shape = (batch_size, *self.vae.latent_shape)
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."
)
if latents is None:
latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
else:
latents = latents.to(device)
# scale the initial noise by the standard deviation required by the scheduler
latents = latents * getattr(self.scheduler, 'init_noise_sigma', 1.0)
return latents
def prepare_image(self, image, mask=None) -> dict:
if isinstance(image, torch.Tensor) and isinstance(mask, torch.Tensor):
outputs = {
'image': image,
'mask': mask
}
return outputs
if isinstance(image, str) and not os.path.exists(image):
raise FileNotFoundError(f"Couldn't find image at path {image}")
if not isinstance(image, list):
image = [image]
outputs = []
for img in image:
output = self.image_processor(img)
outputs.append(output)
cond_input = {k: [] for k in outputs[0].keys()}
for output in outputs:
for key, value in output.items():
cond_input[key].append(value)
for key, value in cond_input.items():
if isinstance(value[0], torch.Tensor):
cond_input[key] = torch.cat(value, dim=0)
return cond_input
def get_guidance_scale_embedding(self, w, embedding_dim=512, dtype=torch.float32):
"""
See https://github.com/google-research/vdm/blob/dc27b98a554f65cdc654b800da5aa1846545d41b/model_vdm.py#L298
Args:
timesteps (`torch.Tensor`):
generate embedding vectors at these timesteps
embedding_dim (`int`, *optional*, defaults to 512):
dimension of the embeddings to generate
dtype:
data type of the generated embeddings
Returns:
`torch.FloatTensor`: Embedding vectors with shape `(len(timesteps), embedding_dim)`
"""
assert len(w.shape) == 1
w = w * 1000.0
half_dim = embedding_dim // 2
emb = torch.log(torch.tensor(10000.0)) / (half_dim - 1)
emb = torch.exp(torch.arange(half_dim, dtype=dtype) * -emb)
emb = w.to(dtype)[:, None] * emb[None, :]
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=1)
if embedding_dim % 2 == 1: # zero pad
emb = torch.nn.functional.pad(emb, (0, 1))
assert emb.shape == (w.shape[0], embedding_dim)
return emb
def set_surface_extractor(self, mc_algo):
if mc_algo is None:
return
logger.info('The parameters `mc_algo` is deprecated, and will be removed in future versions.\n'
'Please use: \n'
'from hy3dshape.models.autoencoders import SurfaceExtractors\n'
'pipeline.vae.surface_extractor = SurfaceExtractors[mc_algo]() instead\n')
if mc_algo not in SurfaceExtractors.keys():
raise ValueError(f"Unknown mc_algo {mc_algo}")
self.vae.surface_extractor = SurfaceExtractors[mc_algo]()
@torch.no_grad()
def __call__(
self,
image: Union[str, List[str], Image.Image] = None,
num_inference_steps: int = 50,
timesteps: List[int] = None,
sigmas: List[float] = None,
eta: float = 0.0,
guidance_scale: float = 7.5,
dual_guidance_scale: float = 10.5,
dual_guidance: bool = True,
generator=None,
box_v=1.01,
octree_resolution=384,
mc_level=-1 / 512,
num_chunks=8000,
mc_algo=None,
output_type: Optional[str] = "trimesh",
enable_pbar=True,
**kwargs,
) -> List[List[trimesh.Trimesh]]:
callback = kwargs.pop("callback", None)
callback_steps = kwargs.pop("callback_steps", None)
self.set_surface_extractor(mc_algo)
device = self.device
dtype = self.dtype
do_classifier_free_guidance = guidance_scale >= 0 and \
getattr(self.model, 'guidance_cond_proj_dim', None) is None
dual_guidance = dual_guidance_scale >= 0 and dual_guidance
if isinstance(image, torch.Tensor):
pass
else:
cond_inputs = self.prepare_image(image)
image = cond_inputs.pop('image')
cond = self.encode_cond(
image=image,
additional_cond_inputs=cond_inputs,
do_classifier_free_guidance=do_classifier_free_guidance,
dual_guidance=False,
)
batch_size = image.shape[0]
t_dtype = torch.long
timesteps, num_inference_steps = retrieve_timesteps(
self.scheduler, num_inference_steps, device, timesteps, sigmas)
latents = self.prepare_latents(batch_size, dtype, device, generator)
extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta)
guidance_cond = None
if getattr(self.model, 'guidance_cond_proj_dim', None) is not None:
logger.info('Using lcm guidance scale')
guidance_scale_tensor = torch.tensor(guidance_scale - 1).repeat(batch_size)
guidance_cond = self.get_guidance_scale_embedding(
guidance_scale_tensor, embedding_dim=self.model.guidance_cond_proj_dim
).to(device=device, dtype=latents.dtype)
with synchronize_timer('Diffusion Sampling'):
for i, t in enumerate(tqdm(timesteps, disable=not enable_pbar, desc="Diffusion Sampling:", leave=False)):
# expand the latents if we are doing classifier free guidance
if do_classifier_free_guidance:
latent_model_input = torch.cat([latents] * (3 if dual_guidance else 2))
else:
latent_model_input = latents
latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)
# predict the noise residual
timestep_tensor = torch.tensor([t], dtype=t_dtype, device=device)
timestep_tensor = timestep_tensor.expand(latent_model_input.shape[0])
noise_pred = self.model(latent_model_input, timestep_tensor, cond, guidance_cond=guidance_cond)
# no drop, drop clip, all drop
if do_classifier_free_guidance:
if dual_guidance:
noise_pred_clip, noise_pred_dino, noise_pred_uncond = noise_pred.chunk(3)
noise_pred = (
noise_pred_uncond
+ guidance_scale * (noise_pred_clip - noise_pred_dino)
+ dual_guidance_scale * (noise_pred_dino - noise_pred_uncond)
)
else:
noise_pred_cond, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_cond - noise_pred_uncond)
# compute the previous noisy sample x_t -> x_t-1
outputs = self.scheduler.step(noise_pred, t, latents, **extra_step_kwargs)
latents = outputs.prev_sample
if callback is not None and i % callback_steps == 0:
step_idx = i // getattr(self.scheduler, "order", 1)
callback(step_idx, t, outputs)
return self._export(
latents,
output_type,
box_v, mc_level, num_chunks, octree_resolution, mc_algo,
)
def _export(
self,
latents,
output_type='trimesh',
box_v=1.01,
mc_level=0.0,
num_chunks=20000,
octree_resolution=256,
mc_algo='mc',
enable_pbar=True
):
if not output_type == "latent":
latents = 1. / self.vae.scale_factor * latents
latents = self.vae(latents)
outputs = self.vae.latents2mesh(
latents,
bounds=box_v,
mc_level=mc_level,
num_chunks=num_chunks,
octree_resolution=octree_resolution,
mc_algo=mc_algo,
enable_pbar=enable_pbar,
)
else:
outputs = latents
if output_type == 'trimesh':
outputs = export_to_trimesh(outputs)
return outputs
class Hunyuan3DDiTFlowMatchingPipeline(Hunyuan3DDiTPipeline):
@torch.inference_mode()
def __call__(
self,
image: Union[str, List[str], Image.Image, dict, List[dict], torch.Tensor] = None,
num_inference_steps: int = 50,
timesteps: List[int] = None,
sigmas: List[float] = None,
eta: float = 0.0,
guidance_scale: float = 5.0,
generator=None,
box_v=1.01,
octree_resolution=384,
mc_level=0.0,
mc_algo=None,
num_chunks=8000,
output_type: Optional[str] = "trimesh",
enable_pbar=True,
mask = None,
**kwargs,
) -> List[List[trimesh.Trimesh]]:
callback = kwargs.pop("callback", None)
callback_steps = kwargs.pop("callback_steps", None)
self.set_surface_extractor(mc_algo)
device = self.device
dtype = self.dtype
do_classifier_free_guidance = guidance_scale >= 0 and not (
hasattr(self.model, 'guidance_embed') and
self.model.guidance_embed is True
)
# print('image', type(image), 'mask', type(mask))
cond_inputs = self.prepare_image(image, mask)
image = cond_inputs.pop('image')
cond = self.encode_cond(
image=image,
additional_cond_inputs=cond_inputs,
do_classifier_free_guidance=do_classifier_free_guidance,
dual_guidance=False,
)
batch_size = image.shape[0]
# 5. Prepare timesteps
# NOTE: this is slightly different from common usage, we start from 0.
sigmas = np.linspace(0, 1, num_inference_steps) if sigmas is None else sigmas
timesteps, num_inference_steps = retrieve_timesteps(
self.scheduler,
num_inference_steps,
device,
sigmas=sigmas,
)
latents = self.prepare_latents(batch_size, dtype, device, generator)
guidance = None
if hasattr(self.model, 'guidance_embed') and \
self.model.guidance_embed is True:
guidance = torch.tensor([guidance_scale] * batch_size, device=device, dtype=dtype)
# logger.info(f'Using guidance embed with scale {guidance_scale}')
with synchronize_timer('Diffusion Sampling'):
for i, t in enumerate(tqdm(timesteps, disable=not enable_pbar, desc="Diffusion Sampling:")):
# expand the latents if we are doing classifier free guidance
if do_classifier_free_guidance:
latent_model_input = torch.cat([latents] * 2)
else:
latent_model_input = latents
# NOTE: we assume model get timesteps ranged from 0 to 1
timestep = t.expand(latent_model_input.shape[0]).to(latents.dtype)
timestep = timestep / self.scheduler.config.num_train_timesteps
noise_pred = self.model(latent_model_input, timestep, cond, guidance=guidance)
if do_classifier_free_guidance:
noise_pred_cond, noise_pred_uncond = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_cond - noise_pred_uncond)
# compute the previous noisy sample x_t -> x_t-1
outputs = self.scheduler.step(noise_pred, t, latents)
latents = outputs.prev_sample
if callback is not None and i % callback_steps == 0:
step_idx = i // getattr(self.scheduler, "order", 1)
callback(step_idx, t, outputs)
return self._export(
latents,
output_type,
box_v, mc_level, num_chunks, octree_resolution, mc_algo,
enable_pbar=enable_pbar,
)
@@ -0,0 +1,202 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import os
import tempfile
from typing import Union
import numpy as np
import pymeshlab
import torch
import trimesh
from .models.autoencoders import Latent2MeshOutput
from .utils import synchronize_timer
def load_mesh(path):
if path.endswith(".glb"):
mesh = trimesh.load(path)
else:
mesh = pymeshlab.MeshSet()
mesh.load_new_mesh(path)
return mesh
def reduce_face(mesh: pymeshlab.MeshSet, max_facenum: int = 200000):
if max_facenum > mesh.current_mesh().face_number():
return mesh
mesh.apply_filter(
"meshing_decimation_quadric_edge_collapse",
targetfacenum=max_facenum,
qualitythr=1.0,
preserveboundary=True,
boundaryweight=3,
preservenormal=True,
preservetopology=True,
autoclean=True
)
return mesh
def remove_floater(mesh: pymeshlab.MeshSet):
mesh.apply_filter("compute_selection_by_small_disconnected_components_per_face",
nbfaceratio=0.005)
mesh.apply_filter("compute_selection_transfer_face_to_vertex", inclusive=False)
mesh.apply_filter("meshing_remove_selected_vertices_and_faces")
return mesh
def pymeshlab2trimesh(mesh: pymeshlab.MeshSet):
with tempfile.NamedTemporaryFile(suffix='.ply', delete=False) as temp_file:
mesh.save_current_mesh(temp_file.name)
mesh = trimesh.load(temp_file.name)
# 检查加载的对象类型
if isinstance(mesh, trimesh.Scene):
combined_mesh = trimesh.Trimesh()
# 如果是Scene,遍历所有的geometry并合并
for geom in mesh.geometry.values():
combined_mesh = trimesh.util.concatenate([combined_mesh, geom])
mesh = combined_mesh
return mesh
def trimesh2pymeshlab(mesh: trimesh.Trimesh):
with tempfile.NamedTemporaryFile(suffix='.ply', delete=False) as temp_file:
if isinstance(mesh, trimesh.scene.Scene):
for idx, obj in enumerate(mesh.geometry.values()):
if idx == 0:
temp_mesh = obj
else:
temp_mesh = temp_mesh + obj
mesh = temp_mesh
mesh.export(temp_file.name)
mesh = pymeshlab.MeshSet()
mesh.load_new_mesh(temp_file.name)
return mesh
def export_mesh(input, output):
if isinstance(input, pymeshlab.MeshSet):
mesh = output
elif isinstance(input, Latent2MeshOutput):
output = Latent2MeshOutput()
output.mesh_v = output.current_mesh().vertex_matrix()
output.mesh_f = output.current_mesh().face_matrix()
mesh = output
else:
mesh = pymeshlab2trimesh(output)
return mesh
def import_mesh(mesh: Union[pymeshlab.MeshSet, trimesh.Trimesh, Latent2MeshOutput, str]) -> pymeshlab.MeshSet:
if isinstance(mesh, str):
mesh = load_mesh(mesh)
elif isinstance(mesh, Latent2MeshOutput):
mesh = pymeshlab.MeshSet()
mesh_pymeshlab = pymeshlab.Mesh(vertex_matrix=mesh.mesh_v, face_matrix=mesh.mesh_f)
mesh.add_mesh(mesh_pymeshlab, "converted_mesh")
if isinstance(mesh, (trimesh.Trimesh, trimesh.scene.Scene)):
mesh = trimesh2pymeshlab(mesh)
return mesh
class FaceReducer:
@synchronize_timer('FaceReducer')
def __call__(
self,
mesh: Union[pymeshlab.MeshSet, trimesh.Trimesh, Latent2MeshOutput, str],
max_facenum: int = 40000
) -> Union[pymeshlab.MeshSet, trimesh.Trimesh]:
ms = import_mesh(mesh)
ms = reduce_face(ms, max_facenum=max_facenum)
mesh = export_mesh(mesh, ms)
return mesh
class FloaterRemover:
@synchronize_timer('FloaterRemover')
def __call__(
self,
mesh: Union[pymeshlab.MeshSet, trimesh.Trimesh, Latent2MeshOutput, str],
) -> Union[pymeshlab.MeshSet, trimesh.Trimesh, Latent2MeshOutput]:
ms = import_mesh(mesh)
ms = remove_floater(ms)
mesh = export_mesh(mesh, ms)
return mesh
class DegenerateFaceRemover:
@synchronize_timer('DegenerateFaceRemover')
def __call__(
self,
mesh: Union[pymeshlab.MeshSet, trimesh.Trimesh, Latent2MeshOutput, str],
) -> Union[pymeshlab.MeshSet, trimesh.Trimesh, Latent2MeshOutput]:
ms = import_mesh(mesh)
with tempfile.NamedTemporaryFile(suffix='.ply', delete=False) as temp_file:
ms.save_current_mesh(temp_file.name)
ms = pymeshlab.MeshSet()
ms.load_new_mesh(temp_file.name)
mesh = export_mesh(mesh, ms)
return mesh
def mesh_normalize(mesh):
"""
Normalize mesh vertices to sphere
"""
scale_factor = 1.2
vtx_pos = np.asarray(mesh.vertices)
max_bb = (vtx_pos - 0).max(0)[0]
min_bb = (vtx_pos - 0).min(0)[0]
center = (max_bb + min_bb) / 2
scale = torch.norm(torch.tensor(vtx_pos - center, dtype=torch.float32), dim=1).max() * 2.0
vtx_pos = (vtx_pos - center) * (scale_factor / float(scale))
mesh.vertices = vtx_pos
return mesh
class MeshSimplifier:
def __init__(self, executable: str = None):
if executable is None:
CURRENT_DIR = os.path.dirname(os.path.abspath(__file__))
executable = os.path.join(CURRENT_DIR, "mesh_simplifier.bin")
self.executable = executable
@synchronize_timer('MeshSimplifier')
def __call__(
self,
mesh: Union[trimesh.Trimesh],
) -> Union[trimesh.Trimesh]:
with tempfile.NamedTemporaryFile(suffix='.obj', delete=False) as temp_input:
with tempfile.NamedTemporaryFile(suffix='.obj', delete=False) as temp_output:
mesh.export(temp_input.name)
os.system(f'{self.executable} {temp_input.name} {temp_output.name}')
ms = trimesh.load(temp_output.name, process=False)
if isinstance(ms, trimesh.Scene):
combined_mesh = trimesh.Trimesh()
for geom in ms.geometry.values():
combined_mesh = trimesh.util.concatenate([combined_mesh, geom])
ms = combined_mesh
ms = mesh_normalize(ms)
return ms
@@ -0,0 +1,167 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import cv2
import numpy as np
import torch
from PIL import Image
from einops import repeat, rearrange
def array_to_tensor(np_array):
image_pt = torch.tensor(np_array).float()
image_pt = image_pt / 255 * 2 - 1
image_pt = rearrange(image_pt, "h w c -> c h w")
image_pts = repeat(image_pt, "c h w -> b c h w", b=1)
return image_pts
class ImageProcessorV2:
def __init__(self, size=512, border_ratio=None):
self.size = size
self.border_ratio = border_ratio
@staticmethod
def recenter(image, border_ratio: float = 0.2):
""" recenter an image to leave some empty space at the image border.
Args:
image (ndarray): input image, float/uint8 [H, W, 3/4]
mask (ndarray): alpha mask, bool [H, W]
border_ratio (float, optional): border ratio, image will be resized to (1 - border_ratio). Defaults to 0.2.
Returns:
ndarray: output image, float/uint8 [H, W, 3/4]
"""
if image.shape[-1] == 4:
mask = image[..., 3]
else:
mask = np.ones_like(image[..., 0:1]) * 255
image = np.concatenate([image, mask], axis=-1)
mask = mask[..., 0]
H, W, C = image.shape
size = max(H, W)
result = np.zeros((size, size, C), dtype=np.uint8)
coords = np.nonzero(mask)
x_min, x_max = coords[0].min(), coords[0].max()
y_min, y_max = coords[1].min(), coords[1].max()
h = x_max - x_min
w = y_max - y_min
if h == 0 or w == 0:
raise ValueError('input image is empty')
desired_size = int(size * (1 - border_ratio))
scale = desired_size / max(h, w)
h2 = int(h * scale)
w2 = int(w * scale)
x2_min = (size - h2) // 2
x2_max = x2_min + h2
y2_min = (size - w2) // 2
y2_max = y2_min + w2
result[x2_min:x2_max, y2_min:y2_max] = cv2.resize(image[x_min:x_max, y_min:y_max], (w2, h2),
interpolation=cv2.INTER_AREA)
bg = np.ones((result.shape[0], result.shape[1], 3), dtype=np.uint8) * 255
mask = result[..., 3:].astype(np.float32) / 255
result = result[..., :3] * mask + bg * (1 - mask)
mask = mask * 255
result = result.clip(0, 255).astype(np.uint8)
mask = mask.clip(0, 255).astype(np.uint8)
return result, mask
def load_image(self, image, border_ratio=0.15, to_tensor=True):
if isinstance(image, str):
image = cv2.imread(image, cv2.IMREAD_UNCHANGED)
image, mask = self.recenter(image, border_ratio=border_ratio)
image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
elif isinstance(image, Image.Image):
image = image.convert("RGBA")
image = np.asarray(image)
image, mask = self.recenter(image, border_ratio=border_ratio)
image = cv2.resize(image, (self.size, self.size), interpolation=cv2.INTER_CUBIC)
mask = cv2.resize(mask, (self.size, self.size), interpolation=cv2.INTER_NEAREST)
mask = mask[..., np.newaxis]
if to_tensor:
image = array_to_tensor(image)
mask = array_to_tensor(mask)
return image, mask
def __call__(self, image, border_ratio=0.15, to_tensor=True, **kwargs):
if self.border_ratio is not None:
border_ratio = self.border_ratio
image, mask = self.load_image(image, border_ratio=border_ratio, to_tensor=to_tensor)
outputs = {
'image': image,
'mask': mask
}
return outputs
class MVImageProcessorV2(ImageProcessorV2):
"""
view order: front, front clockwise 90, back, front clockwise 270
"""
return_view_idx = True
def __init__(self, size=512, border_ratio=None):
super().__init__(size, border_ratio)
self.view2idx = {
'front': 0,
'left': 1,
'back': 2,
'right': 3
}
def __call__(self, image_dict, border_ratio=0.15, to_tensor=True, **kwargs):
if self.border_ratio is not None:
border_ratio = self.border_ratio
images = []
masks = []
view_idxs = []
for idx, (view_tag, image) in enumerate(image_dict.items()):
view_idxs.append(self.view2idx[view_tag])
image, mask = self.load_image(image, border_ratio=border_ratio, to_tensor=to_tensor)
images.append(image)
masks.append(mask)
zipped_lists = zip(view_idxs, images, masks)
sorted_zipped_lists = sorted(zipped_lists)
view_idxs, images, masks = zip(*sorted_zipped_lists)
image = torch.cat(images, 0).unsqueeze(0)
mask = torch.cat(masks, 0).unsqueeze(0)
outputs = {
'image': image,
'mask': mask,
'view_idxs': view_idxs
}
return outputs
IMAGE_PROCESSORS = {
"v2": ImageProcessorV2,
'mv_v2': MVImageProcessorV2,
}
DEFAULT_IMAGEPROCESSOR = 'v2'
@@ -0,0 +1,25 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
from PIL import Image
from rembg import remove, new_session
class BackgroundRemover():
def __init__(self):
self.session = new_session()
def __call__(self, image: Image.Image):
output = remove(image, session=self.session, bgcolor=[255, 255, 255, 0])
return output
@@ -0,0 +1,480 @@
# Copyright 2024 Stability AI, Katherine Crowson and 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.
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import math
from dataclasses import dataclass
from typing import List, Optional, Tuple, Union
import numpy as np
import torch
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.schedulers.scheduling_utils import SchedulerMixin
from diffusers.utils import BaseOutput, logging
logger = logging.get_logger(__name__) # pylint: disable=invalid-name
@dataclass
class FlowMatchEulerDiscreteSchedulerOutput(BaseOutput):
"""
Output class for the scheduler's `step` function output.
Args:
prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images):
Computed sample `(x_{t-1})` of previous timestep. `prev_sample` should be used as next model input in the
denoising loop.
"""
prev_sample: torch.FloatTensor
class FlowMatchEulerDiscreteScheduler(SchedulerMixin, ConfigMixin):
"""
NOTE: this is very similar to diffusers.FlowMatchEulerDiscreteScheduler. Except our timesteps are reversed
Euler scheduler.
This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic
methods the library implements for all schedulers such as loading and saving.
Args:
num_train_timesteps (`int`, defaults to 1000):
The number of diffusion steps to train the model.
timestep_spacing (`str`, defaults to `"linspace"`):
The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and
Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information.
shift (`float`, defaults to 1.0):
The shift value for the timestep schedule.
"""
_compatibles = []
order = 1
@register_to_config
def __init__(
self,
num_train_timesteps: int = 1000,
shift: float = 1.0,
use_dynamic_shifting=False,
):
timesteps = np.linspace(1, num_train_timesteps, num_train_timesteps, dtype=np.float32).copy()
timesteps = torch.from_numpy(timesteps).to(dtype=torch.float32)
sigmas = timesteps / num_train_timesteps
if not use_dynamic_shifting:
# when use_dynamic_shifting is True, we apply the timestep shifting on the fly based on the image resolution
sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
self.timesteps = sigmas * num_train_timesteps
self._step_index = None
self._begin_index = None
self.sigmas = sigmas.to("cpu") # to avoid too much CPU/GPU communication
self.sigma_min = self.sigmas[-1].item()
self.sigma_max = self.sigmas[0].item()
@property
def step_index(self):
"""
The index counter for current timestep. It will increase 1 after each scheduler step.
"""
return self._step_index
@property
def begin_index(self):
"""
The index for the first timestep. It should be set from pipeline with `set_begin_index` method.
"""
return self._begin_index
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index
def set_begin_index(self, begin_index: int = 0):
"""
Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
Args:
begin_index (`int`):
The begin index for the scheduler.
"""
self._begin_index = begin_index
def scale_noise(
self,
sample: torch.FloatTensor,
timestep: Union[float, torch.FloatTensor],
noise: Optional[torch.FloatTensor] = None,
) -> torch.FloatTensor:
"""
Forward process in flow-matching
Args:
sample (`torch.FloatTensor`):
The input sample.
timestep (`int`, *optional*):
The current timestep in the diffusion chain.
Returns:
`torch.FloatTensor`:
A scaled input sample.
"""
# Make sure sigmas and timesteps have the same device and dtype as original_samples
sigmas = self.sigmas.to(device=sample.device, dtype=sample.dtype)
if sample.device.type == "mps" and torch.is_floating_point(timestep):
# mps does not support float64
schedule_timesteps = self.timesteps.to(sample.device, dtype=torch.float32)
timestep = timestep.to(sample.device, dtype=torch.float32)
else:
schedule_timesteps = self.timesteps.to(sample.device)
timestep = timestep.to(sample.device)
# self.begin_index is None when scheduler is used for training, or pipeline does not implement set_begin_index
if self.begin_index is None:
step_indices = [self.index_for_timestep(t, schedule_timesteps) for t in timestep]
elif self.step_index is not None:
# add_noise is called after first denoising step (for inpainting)
step_indices = [self.step_index] * timestep.shape[0]
else:
# add noise is called before first denoising step to create initial latent(img2img)
step_indices = [self.begin_index] * timestep.shape[0]
sigma = sigmas[step_indices].flatten()
while len(sigma.shape) < len(sample.shape):
sigma = sigma.unsqueeze(-1)
sample = sigma * noise + (1.0 - sigma) * sample
return sample
def _sigma_to_t(self, sigma):
return sigma * self.config.num_train_timesteps
def time_shift(self, mu: float, sigma: float, t: torch.Tensor):
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
def set_timesteps(
self,
num_inference_steps: int = None,
device: Union[str, torch.device] = None,
sigmas: Optional[List[float]] = None,
mu: Optional[float] = None,
):
"""
Sets the discrete timesteps used for the diffusion chain (to be run before inference).
Args:
num_inference_steps (`int`):
The number of diffusion steps used when generating samples with a pre-trained model.
device (`str` or `torch.device`, *optional*):
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
"""
if self.config.use_dynamic_shifting and mu is None:
raise ValueError(" you have a pass a value for `mu` when `use_dynamic_shifting` is set to be `True`")
if sigmas is None:
self.num_inference_steps = num_inference_steps
timesteps = np.linspace(
self._sigma_to_t(self.sigma_max), self._sigma_to_t(self.sigma_min), num_inference_steps
)
sigmas = timesteps / self.config.num_train_timesteps
if self.config.use_dynamic_shifting:
sigmas = self.time_shift(mu, 1.0, sigmas)
else:
sigmas = self.config.shift * sigmas / (1 + (self.config.shift - 1) * sigmas)
sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32, device=device)
timesteps = sigmas * self.config.num_train_timesteps
self.timesteps = timesteps.to(device=device)
self.sigmas = torch.cat([sigmas, torch.ones(1, device=sigmas.device)])
self._step_index = None
self._begin_index = None
def index_for_timestep(self, timestep, schedule_timesteps=None):
if schedule_timesteps is None:
schedule_timesteps = self.timesteps
indices = (schedule_timesteps == timestep).nonzero()
# The sigma index that is taken for the **very** first `step`
# is always the second index (or the last index if there is only 1)
# This way we can ensure we don't accidentally skip a sigma in
# case we start in the middle of the denoising schedule (e.g. for image-to-image)
pos = 1 if len(indices) > 1 else 0
return indices[pos].item()
def _init_step_index(self, timestep):
if self.begin_index is None:
if isinstance(timestep, torch.Tensor):
timestep = timestep.to(self.timesteps.device)
self._step_index = self.index_for_timestep(timestep)
else:
self._step_index = self._begin_index
def step(
self,
model_output: torch.FloatTensor,
timestep: Union[float, torch.FloatTensor],
sample: torch.FloatTensor,
s_churn: float = 0.0,
s_tmin: float = 0.0,
s_tmax: float = float("inf"),
s_noise: float = 1.0,
generator: Optional[torch.Generator] = None,
return_dict: bool = True,
) -> Union[FlowMatchEulerDiscreteSchedulerOutput, Tuple]:
"""
Predict the sample from the previous timestep by reversing the SDE. This function propagates the diffusion
process from the learned model outputs (most often the predicted noise).
Args:
model_output (`torch.FloatTensor`):
The direct output from learned diffusion model.
timestep (`float`):
The current discrete timestep in the diffusion chain.
sample (`torch.FloatTensor`):
A current instance of a sample created by the diffusion process.
s_churn (`float`):
s_tmin (`float`):
s_tmax (`float`):
s_noise (`float`, defaults to 1.0):
Scaling factor for noise added to the sample.
generator (`torch.Generator`, *optional*):
A random number generator.
return_dict (`bool`):
Whether or not to return a [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or
tuple.
Returns:
[`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] or `tuple`:
If return_dict is `True`, [`~schedulers.scheduling_euler_discrete.EulerDiscreteSchedulerOutput`] is
returned, otherwise a tuple is returned where the first element is the sample tensor.
"""
if (
isinstance(timestep, int)
or isinstance(timestep, torch.IntTensor)
or isinstance(timestep, torch.LongTensor)
):
raise ValueError(
(
"Passing integer indices (e.g. from `enumerate(timesteps)`) as timesteps to"
" `EulerDiscreteScheduler.step()` is not supported. Make sure to pass"
" one of the `scheduler.timesteps` as a timestep."
),
)
if self.step_index is None:
self._init_step_index(timestep)
# Upcast to avoid precision issues when computing prev_sample
sample = sample.to(torch.float32)
sigma = self.sigmas[self.step_index]
sigma_next = self.sigmas[self.step_index + 1]
prev_sample = sample + (sigma_next - sigma) * model_output
# Cast sample back to model compatible dtype
prev_sample = prev_sample.to(model_output.dtype)
# upon completion increase step index by one
self._step_index += 1
if not return_dict:
return (prev_sample,)
return FlowMatchEulerDiscreteSchedulerOutput(prev_sample=prev_sample)
def __len__(self):
return self.config.num_train_timesteps
@dataclass
class ConsistencyFlowMatchEulerDiscreteSchedulerOutput(BaseOutput):
prev_sample: torch.FloatTensor
pred_original_sample: torch.FloatTensor
class ConsistencyFlowMatchEulerDiscreteScheduler(SchedulerMixin, ConfigMixin):
_compatibles = []
order = 1
@register_to_config
def __init__(
self,
num_train_timesteps: int = 1000,
pcm_timesteps: int = 50,
):
sigmas = np.linspace(0, 1, num_train_timesteps)
step_ratio = num_train_timesteps // pcm_timesteps
euler_timesteps = (np.arange(1, pcm_timesteps) * step_ratio).round().astype(np.int64) - 1
euler_timesteps = np.asarray([0] + euler_timesteps.tolist())
self.euler_timesteps = euler_timesteps
self.sigmas = sigmas[self.euler_timesteps]
self.sigmas = torch.from_numpy((self.sigmas.copy())).to(dtype=torch.float32)
self.timesteps = self.sigmas * num_train_timesteps
self._step_index = None
self._begin_index = None
self.sigmas = self.sigmas.to("cpu") # to avoid too much CPU/GPU communication
@property
def step_index(self):
"""
The index counter for current timestep. It will increase 1 after each scheduler step.
"""
return self._step_index
@property
def begin_index(self):
"""
The index for the first timestep. It should be set from pipeline with `set_begin_index` method.
"""
return self._begin_index
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index
def set_begin_index(self, begin_index: int = 0):
"""
Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
Args:
begin_index (`int`):
The begin index for the scheduler.
"""
self._begin_index = begin_index
def _sigma_to_t(self, sigma):
return sigma * self.config.num_train_timesteps
def set_timesteps(
self,
num_inference_steps: int = None,
device: Union[str, torch.device] = None,
sigmas: Optional[List[float]] = None,
):
"""
Sets the discrete timesteps used for the diffusion chain (to be run before inference).
Args:
num_inference_steps (`int`):
The number of diffusion steps used when generating samples with a pre-trained model.
device (`str` or `torch.device`, *optional*):
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
"""
self.num_inference_steps = num_inference_steps if num_inference_steps is not None else len(sigmas)
inference_indices = np.linspace(
0, self.config.pcm_timesteps, num=self.num_inference_steps, endpoint=False
)
inference_indices = np.floor(inference_indices).astype(np.int64)
inference_indices = torch.from_numpy(inference_indices).long()
self.sigmas_ = self.sigmas[inference_indices]
timesteps = self.sigmas_ * self.config.num_train_timesteps
self.timesteps = timesteps.to(device=device)
self.sigmas_ = torch.cat(
[self.sigmas_, torch.ones(1, device=self.sigmas_.device)]
)
self._step_index = None
self._begin_index = None
def index_for_timestep(self, timestep, schedule_timesteps=None):
if schedule_timesteps is None:
schedule_timesteps = self.timesteps
indices = (schedule_timesteps == timestep).nonzero()
# The sigma index that is taken for the **very** first `step`
# is always the second index (or the last index if there is only 1)
# This way we can ensure we don't accidentally skip a sigma in
# case we start in the middle of the denoising schedule (e.g. for image-to-image)
pos = 1 if len(indices) > 1 else 0
return indices[pos].item()
def _init_step_index(self, timestep):
if self.begin_index is None:
if isinstance(timestep, torch.Tensor):
timestep = timestep.to(self.timesteps.device)
self._step_index = self.index_for_timestep(timestep)
else:
self._step_index = self._begin_index
def step(
self,
model_output: torch.FloatTensor,
timestep: Union[float, torch.FloatTensor],
sample: torch.FloatTensor,
generator: Optional[torch.Generator] = None,
return_dict: bool = True,
) -> Union[ConsistencyFlowMatchEulerDiscreteSchedulerOutput, Tuple]:
if (
isinstance(timestep, int)
or isinstance(timestep, torch.IntTensor)
or isinstance(timestep, torch.LongTensor)
):
raise ValueError(
(
"Passing integer indices (e.g. from `enumerate(timesteps)`) as timesteps to"
" `EulerDiscreteScheduler.step()` is not supported. Make sure to pass"
" one of the `scheduler.timesteps` as a timestep."
),
)
if self.step_index is None:
self._init_step_index(timestep)
sample = sample.to(torch.float32)
sigma = self.sigmas_[self.step_index]
sigma_next = self.sigmas_[self.step_index + 1]
prev_sample = sample + (sigma_next - sigma) * model_output
prev_sample = prev_sample.to(model_output.dtype)
pred_original_sample = sample + (1.0 - sigma) * model_output
pred_original_sample = pred_original_sample.to(model_output.dtype)
self._step_index += 1
if not return_dict:
return (prev_sample,)
return ConsistencyFlowMatchEulerDiscreteSchedulerOutput(prev_sample=prev_sample,
pred_original_sample=pred_original_sample)
def __len__(self):
return self.config.num_train_timesteps
@@ -0,0 +1,234 @@
# Hunyuan 3D is licensed under the TENCENT HUNYUAN NON-COMMERCIAL LICENSE AGREEMENT
# except for the third-party components listed below.
# Hunyuan 3D does not impose any additional limitations beyond what is outlined
# in the repsective licenses of these third-party components.
# Users must comply with all terms and conditions of original licenses of these third-party
# components and must ensure that the usage of the third party components adheres to
# all relevant laws and regulations.
# For avoidance of doubts, Hunyuan 3D means the large language models and
# their software and algorithms, including trained model weights, parameters (including
# optimizer states), machine-learning model code, inference-enabling code, training-enabling code,
# fine-tuning enabling code and other elements of the foregoing made publicly available
# by Tencent in accordance with TENCENT HUNYUAN COMMUNITY LICENSE AGREEMENT.
import numpy as np
import torch
import trimesh
def normalize_mesh(mesh, scale=0.9999):
"""
Normalize the mesh to fit inside a centered cube with a specified scale.
The mesh is translated so that its bounding box center is at the origin,
then uniformly scaled so that the longest side of the bounding box fits within [-scale, scale].
Args:
mesh (trimesh.Trimesh): Input mesh to normalize.
scale (float, optional): Scaling factor to slightly shrink the mesh inside the unit cube. Default is 0.9999.
Returns:
trimesh.Trimesh: The normalized mesh with applied translation and scaling.
"""
bbox = mesh.bounds
center = (bbox[1] + bbox[0]) / 2
scale_ = (bbox[1] - bbox[0]).max()
mesh.apply_translation(-center)
mesh.apply_scale(1 / scale_ * 2 * scale)
return mesh
def sample_pointcloud(mesh, num=200000):
"""
Sample points uniformly from the surface of the mesh along with their corresponding face normals.
Args:
mesh (trimesh.Trimesh): Input mesh to sample from.
num (int, optional): Number of points to sample. Default is 200000.
Returns:
Tuple[torch.Tensor, torch.Tensor]:
- points: Sampled points as a float tensor of shape (num, 3).
- normals: Corresponding normals as a float tensor of shape (num, 3).
"""
points, face_idx = mesh.sample(num, return_index=True)
normals = mesh.face_normals[face_idx]
points = torch.from_numpy(points.astype(np.float32))
normals = torch.from_numpy(normals.astype(np.float32))
return points, normals
def load_surface(mesh, num_points=8192):
"""
Normalize the mesh, sample points and normals from its surface, and randomly select a subset.
Args:
mesh (trimesh.Trimesh): Input mesh to process.
num_points (int, optional): Number of points to randomly select
from the sampled surface points. Default is 8192.
Returns:
Tuple[torch.Tensor, trimesh.Trimesh]:
- surface: Tensor of shape (1, num_points, 6), concatenating points and normals.
- mesh: The normalized mesh.
"""
mesh = normalize_mesh(mesh, scale=0.98)
surface, normal = sample_pointcloud(mesh)
rng = np.random.default_rng(seed=0)
ind = rng.choice(surface.shape[0], num_points, replace=False)
surface = torch.FloatTensor(surface[ind])
normal = torch.FloatTensor(normal[ind])
surface = torch.cat([surface, normal], dim=-1).unsqueeze(0)
return surface, mesh
def sharp_sample_pointcloud(mesh, num=16384):
"""
Sample points and normals preferentially from sharp edges of the mesh.
Sharp edges are detected based on the angle between vertex normals and face normals.
Points are sampled along these edges proportionally to edge length.
Args:
mesh (trimesh.Trimesh): Input mesh to sample from.
num (int, optional): Number of points to sample from sharp edges. Default is 16384.
Returns:
Tuple[np.ndarray, np.ndarray]:
- samples: Sampled points along sharp edges, shape (num, 3).
- normals: Corresponding interpolated normals, shape (num, 3).
"""
V = mesh.vertices
N = mesh.face_normals
VN = mesh.vertex_normals
F = mesh.faces
VN2 = np.ones(V.shape[0])
for i in range(3):
dot = np.stack((VN2[F[:, i]], np.sum(VN[F[:, i]] * N, axis=-1)), axis=-1)
VN2[F[:, i]] = np.min(dot, axis=-1)
sharp_mask = VN2 < 0.985
# collect edge
edge_a = np.concatenate((F[:, 0], F[:, 1], F[:, 2]))
edge_b = np.concatenate((F[:, 1], F[:, 2], F[:, 0]))
sharp_edge = ((sharp_mask[edge_a] * sharp_mask[edge_b]))
edge_a = edge_a[sharp_edge > 0]
edge_b = edge_b[sharp_edge > 0]
sharp_verts_a = V[edge_a]
sharp_verts_b = V[edge_b]
sharp_verts_an = VN[edge_a]
sharp_verts_bn = VN[edge_b]
weights = np.linalg.norm(sharp_verts_b - sharp_verts_a, axis=-1)
weights /= np.sum(weights)
random_number = np.random.rand(num)
w = np.random.rand(num, 1)
index = np.searchsorted(weights.cumsum(), random_number)
samples = w * sharp_verts_a[index] + (1 - w) * sharp_verts_b[index]
normals = w * sharp_verts_an[index] + (1 - w) * sharp_verts_bn[index]
return samples, normals
def load_surface_sharpegde(mesh, num_points=4096, num_sharp_points=4096, sharpedge_flag=True):
try:
mesh_full = trimesh.util.concatenate(mesh.dump())
except Exception as err:
mesh_full = trimesh.util.concatenate(mesh)
mesh_full = normalize_mesh(mesh_full)
origin_num = mesh_full.faces.shape[0]
original_vertices = mesh_full.vertices
original_faces = mesh_full.faces
mesh = trimesh.Trimesh(vertices=original_vertices, faces=original_faces[:origin_num])
mesh_fill = trimesh.Trimesh(vertices=original_vertices, faces=original_faces[origin_num:])
area = mesh.area
area_fill = mesh_fill.area
sample_num = 499712 // 2
num_fill = int(sample_num * (area_fill / (area + area_fill)))
num = sample_num - num_fill
random_surface, random_normal = sample_pointcloud(mesh, num=num)
if num_fill == 0:
random_surface_fill, random_normal_fill = np.zeros((0, 3)), np.zeros((0, 3))
else:
random_surface_fill, random_normal_fill = sample_pointcloud(mesh_fill, num=num_fill)
random_sharp_surface, sharp_normal = sharp_sample_pointcloud(mesh, num=sample_num)
# save_surface
surface = np.concatenate((random_surface, random_normal), axis=1).astype(np.float16)
surface_fill = np.concatenate((random_surface_fill, random_normal_fill), axis=1).astype(np.float16)
sharp_surface = np.concatenate((random_sharp_surface, sharp_normal), axis=1).astype(np.float16)
surface = np.concatenate((surface, surface_fill), axis=0)
if sharpedge_flag:
sharpedge_label = np.zeros((surface.shape[0], 1))
surface = np.concatenate((surface, sharpedge_label), axis=1)
sharpedge_label = np.ones((sharp_surface.shape[0], 1))
sharp_surface = np.concatenate((sharp_surface, sharpedge_label), axis=1)
rng = np.random.default_rng()
ind = rng.choice(surface.shape[0], num_points, replace=False)
surface = torch.FloatTensor(surface[ind])
ind = rng.choice(sharp_surface.shape[0], num_sharp_points, replace=False)
sharp_surface = torch.FloatTensor(sharp_surface[ind])
return torch.cat([surface, sharp_surface], dim=0).unsqueeze(0), mesh_full
class SurfaceLoader:
def __init__(self, num_points=8192):
self.num_points = num_points
def __call__(self, mesh_or_mesh_path, num_points=None):
if num_points is None:
num_points = self.num_points
mesh = mesh_or_mesh_path
if isinstance(mesh, str):
mesh = trimesh.load(mesh, force="mesh", merge_primitives=True)
if isinstance(mesh, trimesh.scene.Scene):
for idx, obj in enumerate(mesh.geometry.values()):
if idx == 0:
temp_mesh = obj
else:
temp_mesh = temp_mesh + obj
mesh = temp_mesh
surface, mesh = load_surface(mesh, num_points=num_points)
return surface
class SharpEdgeSurfaceLoader:
def __init__(self, num_uniform_points=8192, num_sharp_points=8192, **kwargs):
self.num_uniform_points = num_uniform_points
self.num_sharp_points = num_sharp_points
self.num_points = num_uniform_points + num_sharp_points
def __call__(self, mesh_or_mesh_path, num_uniform_points=None, num_sharp_points=None):
if num_uniform_points is None:
num_uniform_points = self.num_uniform_points
if num_sharp_points is None:
num_sharp_points = self.num_sharp_points
mesh = mesh_or_mesh_path
if isinstance(mesh, str):
mesh = trimesh.load(mesh, force="mesh", merge_primitives=True)
if isinstance(mesh, trimesh.scene.Scene):
for idx, obj in enumerate(mesh.geometry.values()):
if idx == 0:
temp_mesh = obj
else:
temp_mesh = temp_mesh + obj
mesh = temp_mesh
surface, mesh = load_surface_sharpegde(mesh, num_points=num_uniform_points, num_sharp_points=num_sharp_points)
return surface
@@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
from .misc import get_config_from_file
from .misc import instantiate_from_config
from .utils import get_logger, logger, synchronize_timer, smart_load_model
@@ -0,0 +1,76 @@
import torch
from torch import nn
class LitEma(nn.Module):
def __init__(self, model, decay=0.9999, use_num_updates=True):
super().__init__()
if decay < 0.0 or decay > 1.0:
raise ValueError('Decay must be between 0 and 1')
self.m_name2s_name = {}
self.register_buffer('decay', torch.tensor(decay, dtype=torch.float32))
self.register_buffer('num_updates', torch.tensor(0, dtype=torch.int) if use_num_updates
else torch.tensor(-1, dtype=torch.int))
for name, p in model.named_parameters():
if p.requires_grad:
# remove as '.'-character is not allowed in buffers
s_name = name.replace('.', '_____')
self.m_name2s_name.update({name: s_name})
self.register_buffer(s_name, p.clone().detach().data)
self.collected_params = []
def forward(self, model):
decay = self.decay
if self.num_updates >= 0:
self.num_updates += 1
decay = min(self.decay, (1 + self.num_updates) / (10 + self.num_updates))
one_minus_decay = 1.0 - decay
with torch.no_grad():
m_param = dict(model.named_parameters())
shadow_params = dict(self.named_buffers())
for key in m_param:
if m_param[key].requires_grad:
sname = self.m_name2s_name[key]
shadow_params[sname] = shadow_params[sname].type_as(m_param[key])
shadow_params[sname].sub_(one_minus_decay * (shadow_params[sname] - m_param[key]))
else:
assert not key in self.m_name2s_name
def copy_to(self, model):
m_param = dict(model.named_parameters())
shadow_params = dict(self.named_buffers())
for key in m_param:
if m_param[key].requires_grad:
m_param[key].data.copy_(shadow_params[self.m_name2s_name[key]].data)
else:
assert not key in self.m_name2s_name
def store(self, model):
"""
Save the current parameters for restoring later.
Args:
parameters: Iterable of `torch.nn.Parameter`; the parameters to be
temporarily stored.
"""
self.collected_params = [param.clone() for param in model.parameters()]
def restore(self, model):
"""
Restore the parameters stored with the `store` method.
Useful to validate the model with EMA parameters without affecting the
original optimization process. Store the parameters before the
`copy_to` method. After validation (or model saving), use this to
restore the former parameters.
Args:
parameters: Iterable of `torch.nn.Parameter`; the parameters to be
updated with the stored parameters.
"""
for c_param, param in zip(self.collected_params, model.parameters()):
param.data.copy_(c_param.data)
@@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
import importlib
from omegaconf import OmegaConf, DictConfig, ListConfig
import torch
import torch.distributed as dist
from typing import Union
def get_config_from_file(config_file: str) -> Union[DictConfig, ListConfig]:
config_file = OmegaConf.load(config_file)
if 'base_config' in config_file.keys():
if config_file['base_config'] == "default_base":
base_config = OmegaConf.create()
# base_config = get_default_config()
elif config_file['base_config'].endswith(".yaml"):
base_config = get_config_from_file(config_file['base_config'])
else:
raise ValueError(f"{config_file} must be `.yaml` file or it contains `base_config` key.")
config_file = {key: value for key, value in config_file if key != "base_config"}
return OmegaConf.merge(base_config, config_file)
return config_file
def get_obj_from_str(string, reload=False):
module, cls = string.rsplit(".", 1)
if reload:
module_imp = importlib.import_module(module)
importlib.reload(module_imp)
return getattr(importlib.import_module(module, package=None), cls)
def get_obj_from_config(config):
if "target" not in config:
raise KeyError("Expected key `target` to instantiate.")
return get_obj_from_str(config["target"])
def instantiate_from_config(config, **kwargs):
if "target" not in config:
raise KeyError("Expected key `target` to instantiate.")
cls = get_obj_from_str(config["target"])
if config.get("from_pretrained", None):
return cls.from_pretrained(
config["from_pretrained"],
use_safetensors=config.get('use_safetensors', False),
variant=config.get('variant', 'fp16'))
params = config.get("params", dict())
# params.update(kwargs)
# instance = cls(**params)
kwargs.update(params)
instance = cls(**kwargs)
return instance
def disabled_train(self, mode=True):
"""Overwrite model.train with this function to make sure train/eval mode
does not change anymore."""
return self
def instantiate_non_trainable_model(config):
model = instantiate_from_config(config)
model = model.eval()
model.train = disabled_train
for param in model.parameters():
param.requires_grad = False
return model
def is_dist_avail_and_initialized():
if not dist.is_available():
return False
if not dist.is_initialized():
return False
return True
def get_rank():
if not is_dist_avail_and_initialized():
return 0
return dist.get_rank()
def get_world_size():
if not is_dist_avail_and_initialized():
return 1
return dist.get_world_size()
def all_gather_batch(tensors):
"""
Performs all_gather operation on the provided tensors.
"""
# Queue the gathered tensors
world_size = get_world_size()
# There is no need for reduction in the single-proc case
if world_size == 1:
return tensors
tensor_list = []
output_tensor = []
for tensor in tensors:
tensor_all = [torch.ones_like(tensor) for _ in range(world_size)]
dist.all_gather(
tensor_all,
tensor,
async_op=False # performance opt
)
tensor_list.append(tensor_all)
for tensor_all in tensor_list:
output_tensor.append(torch.cat(tensor_all, dim=0))
return output_tensor
@@ -0,0 +1 @@
# -*- coding: utf-8 -*-

Some files were not shown because too many files have changed in this diff Show More