336 lines
14 KiB
Python
Executable File
336 lines
14 KiB
Python
Executable File
import os
|
|
import json
|
|
import tempfile
|
|
import logging
|
|
import struct
|
|
from typing import List, Any, Dict, Tuple
|
|
|
|
import numpy as np
|
|
import torch
|
|
|
|
# 导入ComfyUI的路径管理
|
|
try:
|
|
import folder_paths
|
|
FOLDER_PATHS_AVAILABLE = True
|
|
except ImportError:
|
|
folder_paths = None
|
|
FOLDER_PATHS_AVAILABLE = False
|
|
|
|
# 导入trimesh用于GLB文件处理
|
|
try:
|
|
import trimesh
|
|
TRIMESH_AVAILABLE = True
|
|
except ImportError:
|
|
TRIMESH_AVAILABLE = False
|
|
trimesh = None
|
|
|
|
# 配置日志
|
|
logger = logging.getLogger('glb_point_cloud_processor')
|
|
|
|
class GLBPointCloudProcessor:
|
|
"""GLB点云文件处理器 - 专门用于删除黑色点和暗色点优化"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"glb_file_path": ("STRING", {
|
|
"default": "",
|
|
"tooltip": "GLB点云文件路径:输入需要处理的GLB格式点云文件路径。支持绝对路径(如C:/path/to/file.glb)或相对于ComfyUI output目录的相对路径(如pointcloud.glb)。GLB文件应包含点云数据,支持带颜色信息的点云处理"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"black_threshold": ("INT", {
|
|
"default": 30, "min": 0, "max": 255, "step": 1,
|
|
"tooltip": "黑色阈值控制:RGB三色总和小于此值的点将被删除。0=仅删除纯黑点(0,0,0);30=删除深黑色点;60=删除较暗点;100=删除中等暗度点;255=删除所有点。调大=删除更多暗色点,点云更亮净;调小=保留更多暗色细节,但可能有噪点"
|
|
}),
|
|
"output_filename": ("STRING", {
|
|
"default": "clean_pointcloud.glb",
|
|
"tooltip": "输出GLB文件名:处理后点云的保存文件名,自动添加.glb扩展名。文件将保存到ComfyUI的output目录中。建议使用描述性名称如'clean_pointcloud.glb'、'filtered_scan.glb'等便于识别"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (
|
|
"STRING", # 处理后的GLB文件路径
|
|
"STRING", # 处理统计信息(JSON格式)
|
|
"STRING", # 处理日志信息
|
|
)
|
|
RETURN_NAMES = (
|
|
"processed_glb_path",
|
|
"processing_stats",
|
|
"processing_log",
|
|
)
|
|
OUTPUT_TOOLTIPS = [
|
|
"处理后的GLB文件完整路径",
|
|
"处理统计信息(JSON格式)- 包含删除的点数、剩余点数等",
|
|
"详细的处理日志信息"
|
|
]
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "process_point_cloud"
|
|
CATEGORY = "💃VVL/Point Cloud Cleaning"
|
|
|
|
def process_point_cloud(self,
|
|
glb_file_path: str,
|
|
black_threshold: int = 30,
|
|
output_filename: str = "clean_pointcloud.glb"):
|
|
"""
|
|
处理GLB点云文件,删除黑色点和暗色点
|
|
"""
|
|
|
|
processing_log = []
|
|
processing_log.append("开始GLB点云黑色点清理...")
|
|
|
|
# 检查依赖
|
|
if not TRIMESH_AVAILABLE:
|
|
error_msg = "trimesh库不可用,无法处理GLB文件"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return "", json.dumps({"error": error_msg}), "\n".join(processing_log)
|
|
|
|
# 验证输入文件路径
|
|
if not glb_file_path or not glb_file_path.strip():
|
|
error_msg = "GLB文件路径为空"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return "", json.dumps({"error": error_msg}), "\n".join(processing_log)
|
|
|
|
# 处理文件路径
|
|
input_path = self._resolve_file_path(glb_file_path.strip())
|
|
processing_log.append(f"输入文件路径: {input_path}")
|
|
|
|
if not os.path.exists(input_path):
|
|
error_msg = f"GLB文件不存在: {input_path}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return "", json.dumps({"error": error_msg}), "\n".join(processing_log)
|
|
|
|
try:
|
|
# 加载GLB文件
|
|
processing_log.append("正在加载GLB文件...")
|
|
scene = trimesh.load(input_path)
|
|
|
|
# 提取点云数据
|
|
point_clouds = []
|
|
other_geometries = []
|
|
|
|
if isinstance(scene, trimesh.Scene):
|
|
for name, geometry in scene.geometry.items():
|
|
if isinstance(geometry, trimesh.PointCloud):
|
|
point_clouds.append((name, geometry))
|
|
processing_log.append(f"发现点云: {name}, 点数: {len(geometry.vertices)}")
|
|
else:
|
|
other_geometries.append((name, geometry))
|
|
processing_log.append(f"发现其他几何体: {name}, 类型: {type(geometry).__name__}")
|
|
elif isinstance(scene, trimesh.PointCloud):
|
|
point_clouds.append(("main_pointcloud", scene))
|
|
processing_log.append(f"发现点云: main_pointcloud, 点数: {len(scene.vertices)}")
|
|
else:
|
|
# 尝试转换为点云
|
|
if hasattr(scene, 'vertices'):
|
|
pc = trimesh.PointCloud(vertices=scene.vertices, colors=getattr(scene.visual, 'vertex_colors', None))
|
|
point_clouds.append(("converted_pointcloud", pc))
|
|
processing_log.append(f"转换为点云: converted_pointcloud, 点数: {len(pc.vertices)}")
|
|
else:
|
|
error_msg = "GLB文件中未找到点云数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return "", json.dumps({"error": error_msg}), "\n".join(processing_log)
|
|
|
|
if not point_clouds:
|
|
error_msg = "GLB文件中没有点云数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return "", json.dumps({"error": error_msg}), "\n".join(processing_log)
|
|
|
|
# 处理每个点云
|
|
processed_point_clouds = []
|
|
total_original_points = 0
|
|
total_removed_points = 0
|
|
|
|
for name, point_cloud in point_clouds:
|
|
processing_log.append(f"\n处理点云: {name}")
|
|
original_count = len(point_cloud.vertices)
|
|
total_original_points += original_count
|
|
|
|
# 获取顶点和颜色
|
|
vertices = point_cloud.vertices.copy()
|
|
colors = None
|
|
|
|
if hasattr(point_cloud.visual, 'vertex_colors') and point_cloud.visual.vertex_colors is not None:
|
|
colors = point_cloud.visual.vertex_colors.copy()
|
|
elif hasattr(point_cloud, 'colors') and point_cloud.colors is not None:
|
|
colors = point_cloud.colors.copy()
|
|
|
|
# 初始化掩码(所有点都保留)
|
|
keep_mask = np.ones(len(vertices), dtype=bool)
|
|
|
|
# 删除暗色点(默认启用)
|
|
if colors is not None:
|
|
removed_count = self._remove_dark_points(vertices, colors, keep_mask, black_threshold, processing_log)
|
|
processing_log.append(f"删除黑色点: {removed_count} 个")
|
|
else:
|
|
processing_log.append("跳过黑色点过滤: 点云无颜色信息")
|
|
|
|
# 应用掩码
|
|
filtered_vertices = vertices[keep_mask]
|
|
filtered_colors = colors[keep_mask] if colors is not None else None
|
|
|
|
remaining_count = len(filtered_vertices)
|
|
removed_count = original_count - remaining_count
|
|
total_removed_points += removed_count
|
|
|
|
processing_log.append(f"点云 {name}: 原始 {original_count} -> 剩余 {remaining_count} (删除 {removed_count})")
|
|
|
|
# 创建处理后的点云
|
|
if remaining_count > 0:
|
|
processed_pc = trimesh.PointCloud(vertices=filtered_vertices, colors=filtered_colors)
|
|
processed_point_clouds.append((name, processed_pc))
|
|
else:
|
|
processing_log.append(f"警告: 点云 {name} 处理后没有剩余点")
|
|
|
|
# 创建新的场景
|
|
if processed_point_clouds:
|
|
new_scene = trimesh.Scene()
|
|
|
|
# 添加处理后的点云
|
|
for name, pc in processed_point_clouds:
|
|
new_scene.add_geometry(pc, node_name=name)
|
|
|
|
# 添加其他几何体(如相机模型)
|
|
for name, geometry in other_geometries:
|
|
new_scene.add_geometry(geometry, node_name=name)
|
|
processing_log.append(f"保留几何体: {name}")
|
|
|
|
# 生成输出路径
|
|
output_path = self._generate_output_path(output_filename)
|
|
processing_log.append(f"输出文件路径: {output_path}")
|
|
|
|
# 保存处理后的GLB文件
|
|
processing_log.append("正在保存处理后的GLB文件...")
|
|
new_scene.export(output_path)
|
|
|
|
# 验证文件是否保存成功
|
|
if os.path.exists(output_path):
|
|
file_size = os.path.getsize(output_path)
|
|
processing_log.append(f"GLB文件保存成功,文件大小: {file_size} bytes")
|
|
|
|
# 生成统计信息
|
|
stats = {
|
|
"original_points": total_original_points,
|
|
"remaining_points": total_original_points - total_removed_points,
|
|
"removed_points": total_removed_points,
|
|
"removal_percentage": (total_removed_points / total_original_points * 100) if total_original_points > 0 else 0,
|
|
"point_clouds_processed": len(point_clouds),
|
|
"other_geometries_preserved": len(other_geometries),
|
|
"output_file_size": file_size,
|
|
"settings": {
|
|
"black_threshold": black_threshold,
|
|
"remove_dark_points": True
|
|
}
|
|
}
|
|
|
|
processing_log.append("GLB点云黑色点清理完成!")
|
|
return output_path, json.dumps(stats, indent=2), "\n".join(processing_log)
|
|
else:
|
|
error_msg = "GLB文件保存失败"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return "", json.dumps({"error": error_msg}), "\n".join(processing_log)
|
|
else:
|
|
error_msg = "处理后没有剩余的点云数据"
|
|
processing_log.append(f"错误: {error_msg}")
|
|
return "", json.dumps({"error": error_msg}), "\n".join(processing_log)
|
|
|
|
except Exception as e:
|
|
error_msg = f"处理GLB文件时发生错误: {str(e)}"
|
|
logger.error(error_msg)
|
|
processing_log.append(f"错误: {error_msg}")
|
|
import traceback
|
|
traceback.print_exc()
|
|
return "", json.dumps({"error": error_msg}), "\n".join(processing_log)
|
|
|
|
def _resolve_file_path(self, file_path: str) -> str:
|
|
"""解析文件路径,支持绝对路径和相对路径"""
|
|
if os.path.isabs(file_path):
|
|
return file_path
|
|
|
|
# 尝试相对于ComfyUI输出目录
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = folder_paths.get_output_directory()
|
|
candidate_path = os.path.join(output_dir, file_path)
|
|
if os.path.exists(candidate_path):
|
|
return candidate_path
|
|
|
|
# 尝试相对于当前工作目录
|
|
if os.path.exists(file_path):
|
|
return os.path.abspath(file_path)
|
|
|
|
# 返回原始路径(让后续检查处理错误)
|
|
return file_path
|
|
|
|
def _generate_output_path(self, filename: str) -> str:
|
|
"""生成输出文件路径"""
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = folder_paths.get_output_directory()
|
|
else:
|
|
output_dir = "output"
|
|
|
|
# 确保输出目录存在
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# 处理文件名,确保是.glb扩展名
|
|
if not filename.lower().endswith('.glb'):
|
|
filename = f"{filename}.glb"
|
|
|
|
return os.path.join(output_dir, filename)
|
|
|
|
def _remove_dark_points(self, vertices: np.ndarray, colors: np.ndarray, keep_mask: np.ndarray,
|
|
black_threshold: int, processing_log: List[str]) -> int:
|
|
"""删除暗色点"""
|
|
removed_count = 0
|
|
|
|
# 确保颜色数据格式正确
|
|
if colors.shape[1] == 4: # RGBA
|
|
rgb_colors = colors[:, :3]
|
|
else: # RGB
|
|
rgb_colors = colors
|
|
|
|
# 如果颜色值在0-1范围内,转换为0-255
|
|
if rgb_colors.max() <= 1.0:
|
|
rgb_colors = (rgb_colors * 255).astype(np.uint8)
|
|
else:
|
|
rgb_colors = rgb_colors.astype(np.uint8)
|
|
|
|
# 计算RGB总和
|
|
rgb_sum = rgb_colors.sum(axis=1)
|
|
|
|
|
|
|
|
# 应用黑色阈值过滤
|
|
if black_threshold > 0:
|
|
dark_mask = rgb_sum >= black_threshold
|
|
before_count = keep_mask.sum()
|
|
keep_mask &= dark_mask
|
|
after_count = keep_mask.sum()
|
|
removed_by_black = before_count - after_count
|
|
removed_count += removed_by_black
|
|
processing_log.append(f" 按RGB总和阈值({black_threshold})删除: {removed_by_black} 个点")
|
|
|
|
return removed_count
|
|
|
|
|
|
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 节点注册 - 独立注册,ComfyUI会自动发现此文件
|
|
# -----------------------------------------------------------------------------
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"GLBPointCloudProcessor": GLBPointCloudProcessor,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"GLBPointCloudProcessor": "VVL GLB Point Cloud Processor",
|
|
}
|
|
|
|
# # 添加节点信息,帮助ComfyUI更好地识别
|
|
# WEB_DIRECTORY = "./web"
|
|
# __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] |