495 lines
16 KiB
Python
495 lines
16 KiB
Python
import os
|
|
import shutil
|
|
import warnings
|
|
import sys
|
|
|
|
from aiohttp import web
|
|
from server import PromptServer
|
|
from scipy.spatial import KDTree
|
|
|
|
warnings.simplefilter("ignore", category=UserWarning)
|
|
warnings.simplefilter("ignore", category=FutureWarning)
|
|
warnings.simplefilter("ignore", category=DeprecationWarning)
|
|
from folder_paths import (
|
|
get_filename_list,
|
|
get_full_path,
|
|
get_save_image_path,
|
|
get_output_directory,
|
|
)
|
|
from huggingface_hub import snapshot_download
|
|
import torch
|
|
import numpy as np
|
|
from rembg import remove, new_session
|
|
import mcubes
|
|
import cv2
|
|
from einops import rearrange
|
|
from PIL import Image, ImageSequence, ImageOps, ImageColor
|
|
import io
|
|
import base64
|
|
import ipywidgets as widgets
|
|
from IPython.display import display
|
|
import trimesh
|
|
import torch.nn.functional as F
|
|
from typing import Tuple
|
|
import folder_paths
|
|
from os import path
|
|
from .infer import Removebg, Image2Views, Views2Mesh
|
|
|
|
# 添加当前目录到 sys.path
|
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
if current_dir not in sys.path:
|
|
sys.path.append(current_dir)
|
|
sys.path.insert(0, path.dirname(__file__))
|
|
|
|
# 设置访问路径
|
|
base64_data_Array = {""}
|
|
|
|
|
|
@PromptServer.instance.routes.get("/get_obj_Base64")
|
|
async def get_obj(request):
|
|
return web.json_response(base64_data_Array)
|
|
|
|
|
|
def barycentric_interpolate(v0, v1, v2, c0, c1, c2, p):
|
|
v0v1 = v1 - v0
|
|
v0v2 = v2 - v0
|
|
v0p = p - v0
|
|
d00 = np.dot(v0v1, v0v1)
|
|
d01 = np.dot(v0v1, v0v2)
|
|
d11 = np.dot(v0v2, v0v2)
|
|
d20 = np.dot(v0p, v0v1)
|
|
d21 = np.dot(v0p, v0v2)
|
|
denom = d00 * d11 - d01 * d01
|
|
if abs(denom) < 1e-8:
|
|
return (c0 + c1 + c2) / 3
|
|
v = (d11 * d20 - d01 * d21) / denom
|
|
w = (d00 * d21 - d01 * d20) / denom
|
|
u = 1.0 - v - w
|
|
u = np.clip(u, 0, 1)
|
|
v = np.clip(v, 0, 1)
|
|
w = np.clip(w, 0, 1)
|
|
interpolate_color = u * c0 + v * c1 + w * c2
|
|
return np.clip(interpolate_color, 0, 255)
|
|
|
|
|
|
def is_point_in_triangle(p, v0, v1, v2):
|
|
def sign(p1, p2, p3):
|
|
return (p1[0] - p3[0]) * (p2[1] - p3[1]) - (p2[0] - p3[0]) * (p1[1] - p3[1])
|
|
|
|
d1 = sign(p, v0, v1)
|
|
d2 = sign(p, v1, v2)
|
|
d3 = sign(p, v2, v0)
|
|
|
|
has_neg = (d1 < 0) or (d2 < 0) or (d3 < 0)
|
|
has_pos = (d1 > 0) or (d2 > 0) or (d3 > 0)
|
|
|
|
return not (has_neg and has_pos)
|
|
|
|
|
|
def pil2tensor(image: Image) -> torch.Tensor:
|
|
return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0)
|
|
|
|
|
|
def tensor2pil(t_image: torch.Tensor) -> Image:
|
|
return Image.fromarray(
|
|
np.clip(255.0 * t_image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)
|
|
)
|
|
|
|
|
|
class RemoveBackground:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"session": (
|
|
"STRING",
|
|
{
|
|
"multiline": False,
|
|
"default": "u2net",
|
|
"lazy": True,
|
|
},
|
|
),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("image",)
|
|
FUNCTION = "make_six_views"
|
|
CATEGORY = "EX"
|
|
|
|
def __init__(self):
|
|
self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
self.worker_i2v = None
|
|
|
|
def make_six_views(
|
|
self,
|
|
image,
|
|
session,
|
|
):
|
|
if not isinstance(image, Image.Image):
|
|
if isinstance(image, torch.Tensor):
|
|
print(f"Original tensor shape: {image.shape}")
|
|
if image.dim() == 4 and image.shape[0] == 1:
|
|
image = image.squeeze(0)
|
|
print(f"Squeezed tensor shape: {image.shape}")
|
|
if image.dim() == 3:
|
|
if image.shape[2] in [1, 3, 4]:
|
|
print(f"Image is in [H, W, C] format with shape: {image.shape}")
|
|
pass
|
|
elif image.shape[0] in [1, 3, 4]:
|
|
print(f"Image is in [C, H, W] format with shape: {image.shape}")
|
|
image = image.permute(1, 2, 0)
|
|
print(f"Transposed image shape: {image.shape}")
|
|
else:
|
|
raise ValueError(f"Unsupported image shape: {image.shape}")
|
|
else:
|
|
raise ValueError(f"Unsupported image dimensions: {image.dim()}")
|
|
image = image.cpu().numpy()
|
|
print(
|
|
f"Converted to numpy array, shape: {image.shape}, dtype: {image.dtype}"
|
|
)
|
|
if image.dtype in [np.float32, np.float64]:
|
|
image = np.clip(image * 255, 0, 255).astype(np.uint8)
|
|
print(f"Scaled image to uint8, dtype: {image.dtype}")
|
|
elif image.dtype == np.uint8:
|
|
print(f"Image is already uint8, dtype: {image.dtype}")
|
|
else:
|
|
image = image.astype(np.uint8)
|
|
print(f"Converted image to uint8, dtype: {image.dtype}")
|
|
if image.ndim != 3 or image.shape[2] not in [1, 3, 4]:
|
|
raise ValueError(
|
|
f"Invalid image shape after processing: {image.shape}"
|
|
)
|
|
image = Image.fromarray(image)
|
|
print(f"Converted to PIL Image, size: {image.size}, mode: {image.mode}")
|
|
else:
|
|
raise TypeError(f"Unsupported image type: {type(image)}")
|
|
else:
|
|
print(
|
|
f"Input image is already a PIL Image, size: {image.size}, mode: {image.mode}"
|
|
)
|
|
rgba = remove(image, session=new_session(session))
|
|
rgba_img_tensor = pil2tensor(rgba)
|
|
return (rgba_img_tensor,)
|
|
|
|
|
|
class GenerateSixViews:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"rgba": ("IMAGE",),
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
|
|
"step": ("INT", {"default": 50}),
|
|
"use_lite": ("BOOLEAN", {"default": False}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (
|
|
"IMAGE",
|
|
"IMAGE",
|
|
)
|
|
RETURN_NAMES = (
|
|
"sixImages",
|
|
"originalImage",
|
|
)
|
|
FUNCTION = "make_six_views"
|
|
CATEGORY = "EX"
|
|
|
|
def __init__(self):
|
|
self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
self.worker_i2v = None
|
|
|
|
def make_six_views(self, rgba, seed, step, use_lite):
|
|
rgba = tensor2pil(rgba)
|
|
if rgba.mode != "RGBA":
|
|
raise ValueError("Input image is not in RGBA format.")
|
|
if self.worker_i2v is None:
|
|
self.worker_i2v = Image2Views(use_lite=use_lite, device=self.device)
|
|
res_img, pils = self.worker_i2v(rgba, seed, step)
|
|
res_multi_img_tensor = pil2tensor(res_img[0])
|
|
use_image_tensor = pil2tensor(res_img[1])
|
|
return (
|
|
res_multi_img_tensor,
|
|
use_image_tensor,
|
|
)
|
|
|
|
|
|
class Hunyuan3DNode:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"sixImages": ("IMAGE",),
|
|
"originalImage": ("IMAGE",),
|
|
"seed": ("INT", {"default": 0, "min": 0, "max": 0xFFFFFFFFFFFFFFFF}),
|
|
"mesh_size": (
|
|
"INT",
|
|
{
|
|
"default": 500,
|
|
"min": 0,
|
|
"max": 1024,
|
|
"step": 1,
|
|
"display": "number",
|
|
},
|
|
),
|
|
"max_number_of_faces": (
|
|
"INT",
|
|
{
|
|
"default": 90000,
|
|
"min": 1000,
|
|
"max": 1000000,
|
|
"step": 100,
|
|
"display": "number",
|
|
},
|
|
),
|
|
"use_lite": ("BOOLEAN", {"default": False}),
|
|
"is_simplify": ("BOOLEAN", {"default": False}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("MESH",)
|
|
RETURN_NAMES = ("mesh",)
|
|
FUNCTION = "imgTo3D"
|
|
CATEGORY = "EX"
|
|
|
|
def __init__(self):
|
|
self.device = "cuda:0" if torch.cuda.is_available() else "cpu"
|
|
self.worker_v23 = None
|
|
|
|
@staticmethod
|
|
def download_model_files(repo_id: str, local_dir: str):
|
|
"""
|
|
从 Huggingface 仓库下载所有文件到指定的本地目录。
|
|
|
|
:param repo_id: Huggingface 仓库的 ID,例如 "tencent/Hunyuan3D-1"
|
|
:param local_dir: 本地目标目录,例如 "weights"
|
|
"""
|
|
print(
|
|
f"模型文件未找到,正在从 Huggingface 仓库 {repo_id} 下载所有文件到 {local_dir} ..."
|
|
)
|
|
try:
|
|
snapshot_download(
|
|
repo_id=repo_id,
|
|
local_dir=local_dir,
|
|
repo_type="model",
|
|
ignore_patterns=["*.gitignore"],
|
|
)
|
|
print("模型文件下载完成。")
|
|
except Exception as e:
|
|
print(f"下载模型文件时出错: {e}")
|
|
raise
|
|
|
|
def imgTo3D(
|
|
self,
|
|
sixImages,
|
|
originalImage,
|
|
seed,
|
|
mesh_size,
|
|
max_number_of_faces,
|
|
use_lite,
|
|
is_simplify,
|
|
):
|
|
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
weights_dir = os.path.join(base_dir, "weights")
|
|
svrm_dir = os.path.join(weights_dir, "svrm")
|
|
weights_path = os.path.join(svrm_dir, "svrm.safetensors")
|
|
|
|
# 检查模型权重文件是否存在
|
|
if not os.path.exists(weights_path):
|
|
print(f"未找到模型文件 {weights_path},开始下载模型文件...")
|
|
repo_id = "tencent/Hunyuan3D-1" # Huggingface 仓库 ID
|
|
self.download_model_files(repo_id=repo_id, local_dir=weights_dir)
|
|
# 检查下载后是否存在模型文件
|
|
if not os.path.exists(weights_path):
|
|
raise FileNotFoundError(
|
|
f"下载后仍未找到模型文件 {weights_path}。请检查仓库 ID 或网络连接。"
|
|
)
|
|
|
|
config_path = os.path.join(base_dir, "svrm/configs/svrm.yaml")
|
|
weights_path = os.path.join(base_dir, "weights/svrm/svrm.safetensors")
|
|
|
|
if self.worker_v23 is None:
|
|
self.worker_v23 = Views2Mesh(
|
|
config_path,
|
|
weights_path,
|
|
use_lite=use_lite,
|
|
device=self.device,
|
|
)
|
|
|
|
# 从视图生成网格
|
|
mesh = self.worker_v23(
|
|
tensor2pil(sixImages),
|
|
tensor2pil(originalImage),
|
|
seed=seed,
|
|
mesh_size=mesh_size,
|
|
)
|
|
mesh.apply_transform(
|
|
np.array([[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]])
|
|
)
|
|
|
|
# 简化模型
|
|
if is_simplify:
|
|
simplified_mesh = mesh.simplify_quadric_decimation(
|
|
face_count=max_number_of_faces
|
|
)
|
|
# 映射颜色:使用KD树找到新顶点的最近原始顶点
|
|
tree = KDTree(mesh.vertices)
|
|
_, closest_indices = tree.query(simplified_mesh.vertices)
|
|
|
|
# 根据最近点的颜色重新赋值
|
|
simplified_colors = mesh.visual.vertex_colors[closest_indices]
|
|
simplified_mesh.visual.vertex_colors = simplified_colors
|
|
|
|
mesh = simplified_mesh
|
|
|
|
# gltf材质贴图
|
|
# texture_image = mesh.visual.to_texture().material.image.copy()
|
|
# texture_image_tensor = pil2tensor(
|
|
# texture_image.resize((1024, 1024), Image.BILINEAR)
|
|
# )
|
|
|
|
return (mesh,)
|
|
|
|
|
|
class TriMeshViewer:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"mesh": ("MESH",),
|
|
"saveName": (
|
|
"STRING",
|
|
{
|
|
"multiline": False,
|
|
"default": "mesh",
|
|
},
|
|
),
|
|
"saveToOutputFolder": ("BOOLEAN", {"default": False}),
|
|
},
|
|
"hidden": {"display": "DISPLAY"},
|
|
}
|
|
|
|
RETURN_TYPES = ()
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "display"
|
|
CATEGORY = "EX"
|
|
|
|
def display(self, mesh, saveName, saveToOutputFolder):
|
|
saved = list()
|
|
full_output_folder, filename, counter, subfolder, filename_prefix = (
|
|
get_save_image_path(saveName, get_output_directory())
|
|
)
|
|
|
|
filename_with_batch_num = filename.replace("%batch_num%", str(1))
|
|
file = f"{filename_with_batch_num}_{counter:05}_.glb"
|
|
buffer = io.BytesIO()
|
|
mesh.export(buffer, "glb")
|
|
buffer.seek(0)
|
|
binary_data = buffer.getvalue()
|
|
base64_data = base64.b64encode(binary_data).decode("utf-8")
|
|
|
|
# 全局变量修改
|
|
global base64_data_Array
|
|
base64_data_Array = {"base64": {"file": file, "data": base64_data}}
|
|
# 是否保存到输出目录
|
|
if saveToOutputFolder:
|
|
mesh.export(path.join(full_output_folder, file))
|
|
saved.append({"filename": file, "type": "output", "subfolder": subfolder})
|
|
return {"ui": {"mesh": file}}
|
|
|
|
|
|
class SquareImage:
|
|
upscale_methods = ["nearest-exact", "bilinear", "area", "bicubic", "lanczos"]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"image": ("IMAGE",),
|
|
"resolution": (
|
|
"INT",
|
|
{"default": 1024, "min": 8, "max": 8096, "step": 16},
|
|
),
|
|
"upscale_method": (cls.upscale_methods,),
|
|
"padding_color": ("COLOR", {"default": (255, 255, 255)}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("Square_Image",)
|
|
FUNCTION = "make_square"
|
|
CATEGORY = "EX"
|
|
|
|
def make_square(
|
|
self, image, resolution, upscale_method, padding_color=(255, 255, 255)
|
|
):
|
|
ret_images = []
|
|
|
|
# 映射用户选择的缩放方法到 PIL 的 resample 方法
|
|
resample_methods = {
|
|
"nearest-exact": Image.NEAREST,
|
|
"bilinear": Image.BILINEAR,
|
|
"area": Image.BOX,
|
|
"bicubic": Image.BICUBIC,
|
|
"lanczos": Image.LANCZOS,
|
|
}
|
|
|
|
resample = resample_methods.get(upscale_method, Image.NEAREST)
|
|
|
|
for img_tensor in image:
|
|
# 将张量转换为 PIL 图像
|
|
pil_image = tensor2pil(img_tensor)
|
|
|
|
# 获取原始尺寸
|
|
original_width, original_height = pil_image.size
|
|
|
|
# 判断是否已经是正方形
|
|
if original_width == original_height:
|
|
square_image = pil_image
|
|
else:
|
|
# 计算填充尺寸
|
|
max_side = max(original_width, original_height)
|
|
delta_w = max_side - original_width
|
|
delta_h = max_side - original_height
|
|
padding = (
|
|
delta_w // 2,
|
|
delta_h // 2,
|
|
delta_w - (delta_w // 2),
|
|
delta_h - (delta_h // 2),
|
|
)
|
|
|
|
# 填充图像
|
|
square_image = ImageOps.expand(pil_image, padding, fill=padding_color)
|
|
|
|
# 缩放到指定分辨率
|
|
square_image = square_image.resize(
|
|
(resolution, resolution), resample=resample
|
|
)
|
|
|
|
# 转换回张量
|
|
tensor_img = pil2tensor(square_image)
|
|
ret_images.append(tensor_img)
|
|
|
|
# 将处理后的图像张量堆叠起来
|
|
return (torch.cat(ret_images, dim=0),)
|
|
|
|
|
|
# Register the node
|
|
NODE_CLASS_MAPPINGS = {
|
|
"Hunyuan3DNode": Hunyuan3DNode,
|
|
"GenerateSixViews": GenerateSixViews,
|
|
"SquareImage": SquareImage,
|
|
"RemoveBackground": RemoveBackground,
|
|
"TriMeshViewer": TriMeshViewer,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"Hunyuan3DNode": "EX_Hunyuan3DNode",
|
|
"GenerateSixViews": "EX_GenerateSixViews",
|
|
"SquareImage": "EX_SquareImage",
|
|
"RemoveBackground": "EX_RemoveBackground",
|
|
"TriMeshViewer": "EX_TriMeshViewer",
|
|
}
|