869 lines
35 KiB
Python
869 lines
35 KiB
Python
# VGGT Mask处理节点文件
|
|
|
|
import os
|
|
import json
|
|
import tempfile
|
|
from typing import List, Any, Dict
|
|
import logging
|
|
import struct
|
|
|
|
import cv2
|
|
import numpy as np
|
|
import torch
|
|
from PIL import Image
|
|
|
|
# 尝试导入 ComfyUI 的类型标记
|
|
try:
|
|
from comfy.comfy_types import IO
|
|
except ImportError:
|
|
class IO:
|
|
VIDEO = "VIDEO"
|
|
IMAGE = "IMAGE"
|
|
|
|
# 导入原生VGGT接口
|
|
try:
|
|
from .vggt_native_interface import VGGTNativeInterface, VGGTImageProcessor, VGGTResultProcessor
|
|
VGGT_NATIVE_AVAILABLE = True
|
|
except ImportError:
|
|
VGGTNativeInterface = None
|
|
VGGTImageProcessor = None
|
|
VGGTResultProcessor = None
|
|
VGGT_NATIVE_AVAILABLE = False
|
|
|
|
# 导入模型加载器
|
|
try:
|
|
from .vggt_model_loader import VVLVGGTLoader
|
|
MODEL_LOADER_AVAILABLE = True
|
|
except ImportError:
|
|
VVLVGGTLoader = None
|
|
MODEL_LOADER_AVAILABLE = False
|
|
|
|
# 导入ComfyUI的路径管理
|
|
try:
|
|
import folder_paths
|
|
FOLDER_PATHS_AVAILABLE = True
|
|
except ImportError:
|
|
folder_paths = None
|
|
FOLDER_PATHS_AVAILABLE = False
|
|
|
|
# 导入原节点的工具函数
|
|
try:
|
|
from .comfyui_vggt_nodes import (
|
|
_create_traj_preview,
|
|
_create_insufficient_data_image,
|
|
predictions_to_glb,
|
|
TRIMESH_AVAILABLE,
|
|
MATPLOTLIB_AVAILABLE,
|
|
SCIPY_AVAILABLE
|
|
)
|
|
VGGT_UTILS_AVAILABLE = True
|
|
except ImportError:
|
|
_create_traj_preview = None
|
|
_create_insufficient_data_image = None
|
|
predictions_to_glb = None
|
|
TRIMESH_AVAILABLE = False
|
|
MATPLOTLIB_AVAILABLE = False
|
|
SCIPY_AVAILABLE = False
|
|
VGGT_UTILS_AVAILABLE = False
|
|
|
|
# 配置日志
|
|
logger = logging.getLogger('vvl_vggt_native_nodes')
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 原生VGGT推理函数
|
|
# -----------------------------------------------------------------------------
|
|
|
|
def run_vggt_full_native_inference(images: torch.Tensor, model_instance, device,
|
|
enable_tracks: bool = True,
|
|
grid_size: int = 8,
|
|
query_region: float = 0.8) -> Dict:
|
|
"""使用原生VGGT接口进行完整推理"""
|
|
if not VGGT_NATIVE_AVAILABLE:
|
|
raise ImportError("VGGT Native Interface not available")
|
|
|
|
try:
|
|
# 创建原生接口
|
|
vggt_interface = VGGTNativeInterface(model_instance, device)
|
|
|
|
# 确定启用的分支
|
|
branches = ['cameras', 'depth', 'points', 'points_from_depth']
|
|
|
|
# 如果启用tracks,需要生成查询点
|
|
query_points = None
|
|
if enable_tracks:
|
|
branches.append('tracks')
|
|
# 自动生成查询点:在图像指定区域均匀采样点
|
|
B, S, C, H, W = images.shape if images.dim() == 5 else (1, *images.shape)
|
|
|
|
# 计算查询区域
|
|
margin_h = int(H * (1 - query_region) / 2)
|
|
margin_w = int(W * (1 - query_region) / 2)
|
|
|
|
# 生成网格查询点
|
|
y_coords = torch.linspace(margin_h, H - margin_h, grid_size)
|
|
x_coords = torch.linspace(margin_w, W - margin_w, grid_size)
|
|
grid_y, grid_x = torch.meshgrid(y_coords, x_coords, indexing='ij')
|
|
|
|
# 生成查询点坐标 (N_points, 2),格式为 (x, y)
|
|
query_points = torch.stack([grid_x.flatten(), grid_y.flatten()], dim=1).to(device)
|
|
logger.info(f"Generated {len(query_points)} query points ({grid_size}x{grid_size} grid)")
|
|
|
|
# 执行选择性推理以获取所有分支的输出
|
|
results = vggt_interface.selective_inference(
|
|
images,
|
|
branches=branches,
|
|
query_points=query_points
|
|
)
|
|
|
|
# 添加原始图像到结果中
|
|
if images.dim() == 5: # (1, N, C, H, W)
|
|
images_formatted = images.squeeze(0).permute(0, 2, 3, 1) # (N, H, W, C)
|
|
else: # (N, C, H, W)
|
|
images_formatted = images.permute(0, 2, 3, 1) # (N, H, W, C)
|
|
results['images'] = images_formatted.cpu().numpy()
|
|
|
|
# 添加查询点信息
|
|
if query_points is not None:
|
|
results['query_points'] = query_points.cpu().numpy()
|
|
|
|
logger.info(f"VGGT full native inference completed. Results keys: {list(results.keys())}")
|
|
return results
|
|
|
|
except Exception as e:
|
|
logger.error(f"VGGT full native inference failed: {e}")
|
|
raise
|
|
|
|
def preprocess_images_native(images_list: List[np.ndarray]) -> torch.Tensor:
|
|
"""使用原生VGGT预处理器处理图像"""
|
|
if not VGGT_NATIVE_AVAILABLE:
|
|
raise ImportError("VGGT Native Interface not available")
|
|
|
|
try:
|
|
return VGGTImageProcessor.preprocess_images(images_list)
|
|
except Exception as e:
|
|
logger.error(f"Native image preprocessing failed: {e}")
|
|
raise
|
|
|
|
def apply_comprehensive_filtering(raw_results: Dict, confidence_threshold: float,
|
|
mask_black_bg: bool, mask_white_bg: bool, mask_sky: bool) -> Dict:
|
|
"""对原生VGGT结果应用彻底的过滤"""
|
|
logger.info(f"应用彻底过滤: confidence_threshold={confidence_threshold}")
|
|
|
|
filtered_results = {}
|
|
|
|
# 1. 首先处理图像,应用背景遮罩
|
|
images = raw_results.get('images')
|
|
if images is not None and (mask_black_bg or mask_white_bg or mask_sky):
|
|
images = apply_image_masking(images, mask_black_bg, mask_white_bg, mask_sky)
|
|
filtered_results['images'] = images
|
|
|
|
# 2. 获取或创建置信度mask
|
|
confidence_mask = create_confidence_mask(raw_results, confidence_threshold)
|
|
|
|
# 3. 过滤所有点云数据
|
|
for key in ['points_from_depth', 'points']:
|
|
if key in raw_results:
|
|
filtered_results[key] = apply_confidence_filtering(raw_results[key], confidence_mask)
|
|
|
|
# 4. 过滤深度数据
|
|
if 'depth' in raw_results:
|
|
filtered_results['depth'] = apply_confidence_filtering(raw_results['depth'], confidence_mask)
|
|
|
|
# 5. 其他数据直接复制
|
|
for key in ['cameras', 'tracks', 'query_points']:
|
|
if key in raw_results:
|
|
filtered_results[key] = raw_results[key]
|
|
|
|
# 6. 保存过滤参数到结果中
|
|
filtered_results['_filtering_params'] = {
|
|
'confidence_threshold': confidence_threshold,
|
|
'mask_black_bg': mask_black_bg,
|
|
'mask_white_bg': mask_white_bg,
|
|
'mask_sky': mask_sky
|
|
}
|
|
|
|
logger.info("彻底过滤完成,所有点云数据已应用过滤参数")
|
|
return filtered_results
|
|
|
|
def create_confidence_mask(raw_results: Dict, confidence_threshold: float) -> np.ndarray:
|
|
"""创建置信度mask,用于过滤低置信度点"""
|
|
if confidence_threshold <= 0 or confidence_threshold >= 100:
|
|
return None
|
|
|
|
# 尝试从depth confidence获取
|
|
if 'depth' in raw_results:
|
|
depth_data = raw_results['depth']
|
|
if isinstance(depth_data, dict) and 'confidence' in depth_data:
|
|
conf = depth_data['confidence']
|
|
if isinstance(conf, torch.Tensor):
|
|
conf = conf.cpu().numpy()
|
|
|
|
# 去掉batch维度
|
|
if conf.ndim == 4 and conf.shape[0] == 1:
|
|
conf = np.squeeze(conf, axis=0)
|
|
|
|
threshold_val = confidence_threshold / 100.0
|
|
conf_mask = conf >= threshold_val
|
|
logger.info(f"从depth confidence创建mask,形状: {conf_mask.shape}")
|
|
return conf_mask
|
|
|
|
# 如果没有confidence数据,基于点云分布创建mask
|
|
points_data = raw_results.get('points_from_depth')
|
|
if points_data is None:
|
|
points_data = raw_results.get('points')
|
|
if points_data is not None:
|
|
if isinstance(points_data, dict):
|
|
points_data = points_data.get('point_map', points_data)
|
|
|
|
if isinstance(points_data, torch.Tensor):
|
|
points_np = points_data.cpu().numpy()
|
|
else:
|
|
points_np = points_data
|
|
|
|
# 基于距离分布创建confidence mask
|
|
if points_np.ndim >= 3:
|
|
original_shape = points_np.shape
|
|
points_flat = points_np.reshape(-1, 3) if points_np.shape[-1] == 3 else points_np.reshape(-1)
|
|
|
|
# 过滤无效点
|
|
valid_mask = ~(np.isnan(points_flat).any(axis=1) | np.isinf(points_flat).any(axis=1))
|
|
|
|
if np.any(valid_mask) and points_flat.shape[-1] == 3:
|
|
valid_points = points_flat[valid_mask]
|
|
center = np.mean(valid_points, axis=0)
|
|
distances = np.linalg.norm(valid_points - center, axis=1)
|
|
distance_threshold = np.percentile(distances, confidence_threshold)
|
|
|
|
# 创建完整的mask
|
|
full_mask = np.zeros(len(points_flat), dtype=bool)
|
|
full_mask[valid_mask] = distances <= distance_threshold
|
|
|
|
# 确保返回的mask形状与数据兼容
|
|
target_shape = original_shape[:-1] # 去掉最后的坐标维度
|
|
reshaped_mask = full_mask.reshape(target_shape)
|
|
logger.info(f"创建的confidence mask形状: {reshaped_mask.shape}, 目标数据形状: {original_shape}")
|
|
return reshaped_mask
|
|
|
|
logger.warning("无法创建置信度mask,将不进行置信度过滤")
|
|
return None
|
|
|
|
def apply_confidence_filtering(data, confidence_mask):
|
|
"""对数据应用置信度过滤"""
|
|
if confidence_mask is None:
|
|
return data
|
|
|
|
if isinstance(data, dict):
|
|
filtered_data = {}
|
|
for key, value in data.items():
|
|
filtered_data[key] = apply_confidence_filtering(value, confidence_mask)
|
|
return filtered_data
|
|
|
|
if data is None:
|
|
return data
|
|
|
|
try:
|
|
# 转换为numpy
|
|
if isinstance(data, torch.Tensor):
|
|
data_np = data.cpu().numpy()
|
|
was_tensor = True
|
|
original_device = data.device
|
|
original_dtype = data.dtype
|
|
else:
|
|
data_np = data
|
|
was_tensor = False
|
|
|
|
# 确保mask形状兼容
|
|
try:
|
|
if data_np.ndim > confidence_mask.ndim:
|
|
# 如果数据有额外维度(如坐标维度),扩展mask
|
|
for _ in range(data_np.ndim - confidence_mask.ndim):
|
|
confidence_mask = np.expand_dims(confidence_mask, axis=-1)
|
|
confidence_mask = np.broadcast_to(confidence_mask, data_np.shape)
|
|
elif data_np.ndim < confidence_mask.ndim:
|
|
# 如果mask维度更多,裁剪mask
|
|
confidence_mask = confidence_mask.reshape(data_np.shape[:confidence_mask.ndim])
|
|
except ValueError as e:
|
|
logger.warning(f"无法广播confidence_mask形状 {confidence_mask.shape} 到数据形状 {data_np.shape}: {e}")
|
|
return data
|
|
|
|
# 应用mask:不符合条件的点设为0(而不是NaN)
|
|
filtered_data = np.where(confidence_mask, data_np, 0.0)
|
|
|
|
# 转换回原格式
|
|
if was_tensor:
|
|
filtered_data = torch.from_numpy(filtered_data).to(original_device).to(original_dtype)
|
|
|
|
return filtered_data
|
|
|
|
except Exception as e:
|
|
logger.warning(f"置信度过滤失败: {e},返回原始数据")
|
|
return data
|
|
|
|
def apply_image_masking(images_data, mask_black_bg: bool, mask_white_bg: bool, mask_sky: bool):
|
|
"""对图像应用背景遮罩"""
|
|
if images_data is None or not (mask_black_bg or mask_white_bg or mask_sky):
|
|
return images_data
|
|
|
|
try:
|
|
if VGGT_NATIVE_AVAILABLE:
|
|
# 转换为tensor格式进行处理
|
|
if isinstance(images_data, np.ndarray):
|
|
if images_data.max() <= 1.0:
|
|
images_tensor = torch.from_numpy(images_data * 255).to(torch.uint8)
|
|
else:
|
|
images_tensor = torch.from_numpy(images_data).to(torch.uint8)
|
|
was_numpy = True
|
|
else:
|
|
images_tensor = images_data
|
|
was_numpy = False
|
|
|
|
# 确保tensor格式正确
|
|
if images_tensor.dim() == 4: # (S, H, W, 3) -> (S, 3, H, W)
|
|
if images_tensor.shape[-1] == 3:
|
|
images_tensor = images_tensor.permute(0, 3, 1, 2)
|
|
|
|
# 应用遮罩
|
|
if mask_black_bg:
|
|
images_tensor = VGGTImageProcessor.apply_masking(images_tensor, 'black_bg')
|
|
if mask_white_bg:
|
|
images_tensor = VGGTImageProcessor.apply_masking(images_tensor, 'white_bg')
|
|
if mask_sky:
|
|
images_tensor = VGGTImageProcessor.apply_masking(images_tensor, 'sky')
|
|
|
|
# 转换回原始格式
|
|
if images_tensor.dim() == 4 and images_tensor.shape[1] == 3: # (S, 3, H, W) -> (S, H, W, 3)
|
|
images_tensor = images_tensor.permute(0, 2, 3, 1)
|
|
|
|
if was_numpy:
|
|
return images_tensor.cpu().numpy()
|
|
else:
|
|
return images_tensor
|
|
else:
|
|
logger.warning("VGGT原生接口不可用,跳过图像遮罩")
|
|
return images_data
|
|
|
|
except Exception as e:
|
|
logger.warning(f"图像遮罩应用失败: {e},返回原始图像")
|
|
return images_data
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 完整原生输出节点
|
|
# -----------------------------------------------------------------------------
|
|
|
|
class VGGTNativeFullOutputNode:
|
|
"""VGGT 原生完整输出节点 - 输出彻底过滤后的数据"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"vggt_model": ("VVL_VGGT_MODEL", {
|
|
"tooltip": "来自VVLVGGTLoader的VGGT模型实例"
|
|
}),
|
|
"images": ("IMAGE", {
|
|
"tooltip": "图片序列输入(必需)- 支持1-200帧图像"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"enable_tracks": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "是否启用点追踪分支"
|
|
}),
|
|
"grid_size": ("INT", {
|
|
"default": 8, "min": 4, "max": 16, "step": 1,
|
|
"tooltip": "查询点网格大小"
|
|
}),
|
|
"query_region": ("FLOAT", {
|
|
"default": 0.8, "min": 0.1, "max": 1.0, "step": 0.05,
|
|
"tooltip": "查询点区域比例"
|
|
}),
|
|
"confidence_threshold": ("FLOAT", {
|
|
"default": 90.0, "min": 0.0, "max": 100.0, "step": 0.1,
|
|
"tooltip": "置信度阈值(%),过滤低置信度点"
|
|
}),
|
|
"show_cameras": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "是否在3D模型中显示相机位置"
|
|
}),
|
|
"mask_black_bg": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "是否过滤黑色背景点"
|
|
}),
|
|
"mask_white_bg": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "是否过滤白色背景点"
|
|
}),
|
|
"mask_sky": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "是否过滤天空点"
|
|
}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = (
|
|
"RAW_VGGT_RESULT", # 彻底过滤后的VGGT结果
|
|
"STRING", # 结果摘要JSON
|
|
"STRING", # Tracks JSON数据
|
|
"STRING", # 相机内参JSON
|
|
"STRING", # 相机姿态JSON
|
|
"IMAGE", # 轨迹可视化预览
|
|
"STRING", # 3D模型GLB文件路径
|
|
"STRING", # 过滤后点云PLY文件路径
|
|
)
|
|
RETURN_NAMES = (
|
|
"filtered_vggt_result",
|
|
"result_summary",
|
|
"tracks_json",
|
|
"intrinsics_json",
|
|
"poses_json",
|
|
"trajectory_preview",
|
|
"model_3d_glb_path",
|
|
"filtered_pointcloud_ply_path",
|
|
)
|
|
OUTPUT_TOOLTIPS = [
|
|
"彻底过滤后的VGGT结果(所有点云数据已应用置信度和背景过滤)",
|
|
"结果摘要信息(JSON格式)",
|
|
"点追踪数据(JSON格式)",
|
|
"相机内参数据(JSON格式)",
|
|
"相机位姿数据(JSON格式)",
|
|
"相机轨迹3D可视化预览图像",
|
|
"3D模型GLB文件路径",
|
|
"过滤后的点云PLY文件路径"
|
|
]
|
|
OUTPUT_NODE = True
|
|
FUNCTION = "get_filtered_output"
|
|
CATEGORY = "💃VVL/VGGT Native"
|
|
|
|
def get_filtered_output(self, vggt_model: Dict, images,
|
|
enable_tracks: bool = True,
|
|
grid_size: int = 8,
|
|
query_region: float = 0.8,
|
|
confidence_threshold: float = 90.0,
|
|
show_cameras: bool = True,
|
|
mask_black_bg: bool = False,
|
|
mask_white_bg: bool = False,
|
|
mask_sky: bool = False):
|
|
"""获取彻底过滤后的VGGT输出"""
|
|
logger.info("开始VGGT原生推理并彻底过滤")
|
|
|
|
# 获取模型实例和设备
|
|
model_instance = vggt_model.get("model")
|
|
device = vggt_model.get("device")
|
|
|
|
if model_instance is None:
|
|
raise ValueError("无效的VGGT模型实例")
|
|
|
|
# 处理图片序列输入
|
|
image_list = []
|
|
if isinstance(images, torch.Tensor):
|
|
images_np = images.cpu().numpy()
|
|
for i in range(images_np.shape[0]):
|
|
img = images_np[i]
|
|
if img.max() <= 1.0:
|
|
img = (img * 255).astype(np.uint8)
|
|
else:
|
|
img = img.astype(np.uint8)
|
|
image_list.append(img)
|
|
else:
|
|
image_list.extend(images)
|
|
|
|
if not image_list:
|
|
raise ValueError("未能提取到有效的图像数据")
|
|
|
|
logger.info(f"总共处理 {len(image_list)} 张图像")
|
|
|
|
# 限制图像数量
|
|
max_frames = 200
|
|
if len(image_list) > max_frames:
|
|
logger.warning(f"图像数量超过限制,采样到 {max_frames} 帧")
|
|
indices = np.linspace(0, len(image_list) - 1, max_frames, dtype=int)
|
|
image_list = [image_list[i] for i in indices]
|
|
|
|
try:
|
|
# 使用原生VGGT预处理
|
|
processed_images = preprocess_images_native(image_list)
|
|
processed_images = processed_images.to(device)
|
|
|
|
# 执行原生推理
|
|
raw_results = run_vggt_full_native_inference(
|
|
processed_images, model_instance, device,
|
|
enable_tracks, grid_size, query_region
|
|
)
|
|
|
|
# 🎯 关键:应用彻底的过滤
|
|
logger.info("应用彻底过滤到所有点云数据...")
|
|
filtered_results = apply_comprehensive_filtering(
|
|
raw_results, confidence_threshold, mask_black_bg, mask_white_bg, mask_sky
|
|
)
|
|
|
|
# 生成结果摘要
|
|
summary = self._generate_result_summary(filtered_results, {
|
|
"confidence_threshold": confidence_threshold,
|
|
"mask_black_bg": mask_black_bg,
|
|
"mask_white_bg": mask_white_bg,
|
|
"mask_sky": mask_sky
|
|
})
|
|
summary_json = json.dumps(summary, ensure_ascii=False, indent=2)
|
|
|
|
# 生成tracks JSON输出
|
|
tracks_json = self._generate_tracks_json(filtered_results)
|
|
|
|
# 生成相机参数JSON输出
|
|
intrinsics_json, poses_json = self._generate_camera_json(filtered_results)
|
|
|
|
# 生成轨迹预览
|
|
trajectory_preview = self._create_trajectory_preview(filtered_results)
|
|
|
|
# 生成3D模型GLB文件
|
|
model_3d_glb_path = self._generate_3d_model(filtered_results, confidence_threshold, show_cameras)
|
|
|
|
# 生成过滤后点云PLY文件
|
|
pointcloud_ply_path = self._export_filtered_pointcloud(filtered_results)
|
|
|
|
logger.info("VGGT彻底过滤输出完成")
|
|
return (filtered_results, summary_json, tracks_json, intrinsics_json, poses_json,
|
|
trajectory_preview, model_3d_glb_path, pointcloud_ply_path)
|
|
|
|
except Exception as e:
|
|
logger.error(f"VGGT推理失败: {e}")
|
|
error_msg = f"VGGT推理失败: {str(e)}"
|
|
empty_result = {"error": str(e)}
|
|
error_json = json.dumps({"error": str(e)})
|
|
error_image = self._create_error_image()
|
|
return (empty_result, error_msg, error_json, error_json, error_json,
|
|
error_image, "", "")
|
|
|
|
def _generate_result_summary(self, results: Dict, parameters: Dict = None) -> Dict:
|
|
"""生成结果摘要"""
|
|
summary = {
|
|
"available_branches": list(results.keys()),
|
|
"filtering_applied": True,
|
|
"metadata": {
|
|
"generated_by": "VGGTNativeFullOutputNode",
|
|
"description": "Comprehensively filtered VGGT results"
|
|
}
|
|
}
|
|
|
|
if parameters is not None:
|
|
summary["metadata"]["filtering_parameters"] = parameters
|
|
|
|
# 添加数据形状信息
|
|
for key, value in results.items():
|
|
if hasattr(value, 'shape'):
|
|
summary[f"{key}_shape"] = list(value.shape)
|
|
elif isinstance(value, dict):
|
|
summary[f"{key}_keys"] = list(value.keys())
|
|
|
|
return summary
|
|
|
|
def _generate_tracks_json(self, filtered_results: Dict) -> str:
|
|
"""生成tracks JSON数据"""
|
|
if 'tracks' not in filtered_results:
|
|
return json.dumps({"message": "No tracks data available"})
|
|
|
|
try:
|
|
track_data = filtered_results['tracks']
|
|
tracks_export = {}
|
|
|
|
for key, value in track_data.items():
|
|
if hasattr(value, 'cpu') and hasattr(value, 'numpy'):
|
|
tracks_export[key] = value.cpu().numpy().tolist()
|
|
elif isinstance(value, torch.Tensor):
|
|
tracks_export[key] = value.detach().cpu().numpy().tolist()
|
|
elif isinstance(value, np.ndarray):
|
|
tracks_export[key] = value.tolist()
|
|
elif isinstance(value, (list, tuple)):
|
|
# 递归处理列表中可能的tensor
|
|
converted_list = []
|
|
for item in value:
|
|
if hasattr(item, 'cpu'):
|
|
converted_list.append(item.cpu().numpy().tolist())
|
|
elif isinstance(item, torch.Tensor):
|
|
converted_list.append(item.detach().cpu().numpy().tolist())
|
|
else:
|
|
converted_list.append(item)
|
|
tracks_export[key] = converted_list
|
|
else:
|
|
tracks_export[key] = value
|
|
|
|
return json.dumps({"tracks_data": tracks_export}, ensure_ascii=False, indent=2)
|
|
except Exception as e:
|
|
logger.error(f"Failed to serialize tracks data: {e}")
|
|
return json.dumps({"error": f"Failed to serialize tracks: {str(e)}"})
|
|
|
|
def _generate_camera_json(self, filtered_results: Dict) -> tuple:
|
|
"""生成相机参数JSON"""
|
|
intrinsics_json = ""
|
|
poses_json = ""
|
|
|
|
if 'cameras' in filtered_results:
|
|
try:
|
|
cameras_data = filtered_results['cameras']
|
|
if isinstance(cameras_data, dict):
|
|
if 'intrinsic' in cameras_data:
|
|
intrinsic = cameras_data['intrinsic']
|
|
if hasattr(intrinsic, 'cpu'):
|
|
intrinsic = intrinsic.cpu().numpy()
|
|
intrinsics_json = json.dumps(intrinsic.tolist(), ensure_ascii=False, indent=2)
|
|
|
|
if 'extrinsic' in cameras_data:
|
|
extrinsic = cameras_data['extrinsic']
|
|
if hasattr(extrinsic, 'cpu'):
|
|
extrinsic = extrinsic.cpu().numpy()
|
|
poses_json = json.dumps(extrinsic.tolist(), ensure_ascii=False, indent=2)
|
|
except Exception as e:
|
|
logger.warning(f"Failed to generate camera JSON: {e}")
|
|
intrinsics_json = json.dumps({"error": f"Failed to extract intrinsics: {str(e)}"})
|
|
poses_json = json.dumps({"error": f"Failed to extract poses: {str(e)}"})
|
|
else:
|
|
intrinsics_json = json.dumps({"message": "No camera data available"})
|
|
poses_json = json.dumps({"message": "No camera data available"})
|
|
|
|
return intrinsics_json, poses_json
|
|
|
|
def _create_error_image(self) -> torch.Tensor:
|
|
"""创建错误提示图像"""
|
|
if VGGT_UTILS_AVAILABLE and _create_insufficient_data_image:
|
|
return _create_insufficient_data_image()
|
|
else:
|
|
canvas = np.ones((400, 600, 3), dtype=np.float32) * 0.9
|
|
return torch.from_numpy(canvas).unsqueeze(0)
|
|
|
|
def _create_trajectory_preview(self, filtered_results: Dict) -> torch.Tensor:
|
|
"""生成轨迹预览"""
|
|
if not VGGT_UTILS_AVAILABLE or not _create_traj_preview:
|
|
return self._create_error_image()
|
|
|
|
try:
|
|
if 'cameras' in filtered_results:
|
|
extrinsic_for_preview = filtered_results['cameras']['extrinsic']
|
|
|
|
if isinstance(extrinsic_for_preview, torch.Tensor):
|
|
if extrinsic_for_preview.ndim == 4 and extrinsic_for_preview.shape[0] == 1:
|
|
extrinsic_for_preview = extrinsic_for_preview.squeeze(0)
|
|
elif isinstance(extrinsic_for_preview, np.ndarray):
|
|
if extrinsic_for_preview.ndim == 4 and extrinsic_for_preview.shape[0] == 1:
|
|
extrinsic_for_preview = np.squeeze(extrinsic_for_preview, axis=0)
|
|
|
|
return _create_traj_preview(extrinsic_for_preview)
|
|
else:
|
|
return self._create_error_image()
|
|
except Exception as e:
|
|
logger.warning(f"轨迹预览生成失败: {e}")
|
|
return self._create_error_image()
|
|
|
|
def _generate_3d_model(self, filtered_results: Dict, confidence_threshold: float, show_cameras: bool) -> str:
|
|
"""生成3D模型GLB文件"""
|
|
if not VGGT_UTILS_AVAILABLE or not predictions_to_glb:
|
|
logger.warning("VGGT工具函数不可用,跳过3D模型生成")
|
|
return ""
|
|
|
|
try:
|
|
# 创建输出目录
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = os.path.join(folder_paths.get_output_directory(), "3d")
|
|
else:
|
|
output_dir = os.path.join("output", "3d")
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# 生成文件名
|
|
import time
|
|
timestamp = int(time.time())
|
|
model_path = os.path.join(output_dir, f"vggt_filtered_{timestamp}.glb")
|
|
|
|
logger.info(f"生成过滤后的3D模型: {model_path}")
|
|
|
|
# 准备预测数据
|
|
predictions_formatted = self._prepare_predictions_data(filtered_results)
|
|
|
|
# 使用官方VGGT的predictions_to_glb函数
|
|
scene_3d = predictions_to_glb(
|
|
predictions_formatted,
|
|
conf_thres=confidence_threshold,
|
|
filter_by_frames="all",
|
|
mask_black_bg=False, # 已经在原生过滤中处理
|
|
mask_white_bg=False,
|
|
show_cam=show_cameras,
|
|
mask_sky=False,
|
|
target_dir=None,
|
|
prediction_mode="Depthmap and Camera Branch"
|
|
)
|
|
|
|
# 导出为GLB文件
|
|
scene_3d.export(model_path)
|
|
|
|
logger.info(f"过滤后3D模型已保存: {model_path}")
|
|
return model_path
|
|
|
|
except Exception as e:
|
|
logger.error(f"生成3D模型失败: {e}")
|
|
return ""
|
|
|
|
def _export_filtered_pointcloud(self, filtered_results: Dict) -> str:
|
|
"""导出过滤后的点云PLY文件"""
|
|
try:
|
|
# 创建输出目录
|
|
if FOLDER_PATHS_AVAILABLE:
|
|
output_dir = os.path.join(folder_paths.get_output_directory(), "pointclouds")
|
|
else:
|
|
output_dir = os.path.join("output", "pointclouds")
|
|
os.makedirs(output_dir, exist_ok=True)
|
|
|
|
# 生成文件名
|
|
import time
|
|
timestamp = int(time.time())
|
|
ply_path = os.path.join(output_dir, f"vggt_filtered_{timestamp}.ply")
|
|
|
|
# 提取点云数据
|
|
points_data = filtered_results.get('points_from_depth') or filtered_results.get('points')
|
|
if points_data is None:
|
|
logger.warning("No point cloud data found")
|
|
return ""
|
|
|
|
if isinstance(points_data, dict):
|
|
points_data = points_data.get('point_map', points_data)
|
|
|
|
# 转换为numpy
|
|
if isinstance(points_data, torch.Tensor):
|
|
points_np = points_data.cpu().numpy()
|
|
else:
|
|
points_np = points_data
|
|
|
|
# 去掉batch维度
|
|
if points_np.ndim == 5 and points_np.shape[0] == 1:
|
|
points_np = np.squeeze(points_np, axis=0)
|
|
|
|
# 重塑点云数据
|
|
if points_np.ndim == 4: # (S, H, W, 3)
|
|
points_np = points_np.reshape(-1, 3)
|
|
|
|
# 过滤无效点(已经过滤但可能仍有0值)
|
|
valid_mask = ~(np.isnan(points_np).any(axis=1) | np.isinf(points_np).any(axis=1))
|
|
valid_mask = valid_mask & (np.linalg.norm(points_np, axis=1) > 1e-6) # 排除0点
|
|
|
|
if not np.any(valid_mask):
|
|
logger.warning("No valid points after filtering")
|
|
return ""
|
|
|
|
points_np = points_np[valid_mask]
|
|
|
|
# 获取颜色信息
|
|
colors_rgb = self._extract_colors(filtered_results, valid_mask)
|
|
|
|
# 写入PLY文件
|
|
self._write_ply_file(ply_path, points_np, colors_rgb)
|
|
|
|
logger.info(f"过滤后点云PLY已保存: {ply_path} ({len(points_np)} 个点)")
|
|
return ply_path
|
|
|
|
except Exception as e:
|
|
logger.error(f"导出过滤后点云失败: {e}")
|
|
return ""
|
|
|
|
def _prepare_predictions_data(self, filtered_results: Dict) -> Dict:
|
|
"""准备predictions_to_glb需要的数据格式"""
|
|
predictions = {}
|
|
|
|
# 处理点云数据
|
|
if 'points_from_depth' in filtered_results:
|
|
world_points = filtered_results['points_from_depth']
|
|
if isinstance(world_points, torch.Tensor) and world_points.ndim == 5:
|
|
world_points = world_points.squeeze(0)
|
|
predictions['world_points_from_depth'] = world_points
|
|
|
|
# 处理深度置信度
|
|
if 'depth' in filtered_results:
|
|
depth_data = filtered_results['depth']
|
|
if isinstance(depth_data, dict):
|
|
predictions['depth_conf'] = depth_data.get('confidence')
|
|
|
|
# 处理图像
|
|
if 'images' in filtered_results:
|
|
images = filtered_results['images']
|
|
if isinstance(images, np.ndarray) and images.ndim == 4:
|
|
predictions['images'] = images
|
|
|
|
# 处理相机外参
|
|
if 'cameras' in filtered_results:
|
|
cameras = filtered_results['cameras']
|
|
if isinstance(cameras, dict):
|
|
extrinsic = cameras.get('extrinsic')
|
|
if isinstance(extrinsic, torch.Tensor) and extrinsic.ndim == 4:
|
|
extrinsic = extrinsic.squeeze(0)
|
|
predictions['extrinsic'] = extrinsic
|
|
|
|
# 转换tensor为numpy
|
|
for key, value in predictions.items():
|
|
if isinstance(value, torch.Tensor):
|
|
predictions[key] = value.cpu().numpy()
|
|
|
|
return predictions
|
|
|
|
def _extract_colors(self, filtered_results: Dict, valid_mask: np.ndarray) -> np.ndarray:
|
|
"""提取点云颜色信息"""
|
|
try:
|
|
if 'images' in filtered_results:
|
|
images = filtered_results['images']
|
|
if isinstance(images, np.ndarray) and images.ndim == 4:
|
|
# 重塑图像为点云颜色
|
|
colors_flat = images.reshape(-1, 3)
|
|
colors_flat = colors_flat[valid_mask]
|
|
|
|
# 确保颜色值在0-255范围
|
|
if colors_flat.max() <= 1.0:
|
|
colors_flat = (colors_flat * 255).astype(np.uint8)
|
|
else:
|
|
colors_flat = colors_flat.astype(np.uint8)
|
|
|
|
return colors_flat
|
|
except Exception as e:
|
|
logger.warning(f"提取颜色失败: {e}")
|
|
|
|
# 使用默认灰色
|
|
return np.ones((np.sum(valid_mask), 3), dtype=np.uint8) * 128
|
|
|
|
def _write_ply_file(self, filepath: str, vertices: np.ndarray, colors: np.ndarray = None):
|
|
"""写入PLY格式点云文件"""
|
|
try:
|
|
with open(filepath, 'w') as f:
|
|
f.write("ply\n")
|
|
f.write("format ascii 1.0\n")
|
|
f.write(f"element vertex {len(vertices)}\n")
|
|
f.write("property float x\n")
|
|
f.write("property float y\n")
|
|
f.write("property float z\n")
|
|
|
|
if colors is not None:
|
|
f.write("property uchar red\n")
|
|
f.write("property uchar green\n")
|
|
f.write("property uchar blue\n")
|
|
|
|
f.write("end_header\n")
|
|
|
|
for i in range(len(vertices)):
|
|
x, y, z = vertices[i]
|
|
if colors is not None:
|
|
r, g, b = colors[i]
|
|
f.write(f"{x:.6f} {y:.6f} {z:.6f} {r} {g} {b}\n")
|
|
else:
|
|
f.write(f"{x:.6f} {y:.6f} {z:.6f}\n")
|
|
|
|
except Exception as e:
|
|
logger.error(f"写入PLY文件失败: {e}")
|
|
raise
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# 节点注册
|
|
# -----------------------------------------------------------------------------
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"VGGTNativeFullOutputNode": VGGTNativeFullOutputNode,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"VGGTNativeFullOutputNode": "🔬 VGGT Native Filtered Output",
|
|
}
|
|
|
|
if MODEL_LOADER_AVAILABLE:
|
|
NODE_CLASS_MAPPINGS["VVLVGGTLoader"] = VVLVGGTLoader
|
|
NODE_DISPLAY_NAME_MAPPINGS["VVLVGGTLoader"] = "VVL VGGT Model Loader" |