Files
BIMer e88c7f9a67 Update node.py
Fixed the seed range
2024-12-10 01:50:22 +08:00

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",
}