Files
wTechArtist-ComfyUI_VVL_Vid…/vggt_native_interface.py

325 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
VGGT Native Interface
原生VGGT功能接口封装 - 基于官方API
"""
import torch
import numpy as np
from typing import Dict, Optional, Tuple, List
import logging
logger = logging.getLogger('vggt_native_interface')
class VGGTNativeInterface:
"""VGGT原生功能接口封装 - 基于官方API"""
def __init__(self, model: torch.nn.Module, device: torch.device):
self.model = model # 原生VGGT模型实例
self.device = device
def full_inference_pipeline(self, images: torch.Tensor, query_points: torch.Tensor = None) -> Dict:
"""完整的VGGT推理管道 - 基于官方最新API"""
with torch.no_grad():
with torch.cuda.amp.autocast(dtype=torch.float16):
# 确保输入是tensor格式,并添加batch维度如果需要
if isinstance(images, np.ndarray):
images = torch.from_numpy(images).to(self.device)
if images.dim() == 4: # (N, C, H, W)
images = images[None] # (1, N, C, H, W)
# 使用官方的forward方法,获得标准的predictions字典
predictions = self.model.forward(images, query_points)
# 添加原始图像数据(官方predictions_to_glb需要)
# 转换图像格式:从(1, N, C, H, W)到(N, H, W, C)
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)
predictions['images'] = images_formatted
# 直接返回标准的VGGT输出格式,添加相机解码结果
if 'pose_enc' in predictions:
try:
extrinsic, intrinsic = self._decode_camera_params(
predictions['pose_enc'], images.shape[-2:]
)
predictions['cameras'] = {'extrinsic': extrinsic, 'intrinsic': intrinsic}
except Exception as e:
logger.warning(f"Failed to decode camera parameters: {e}")
# 从深度图构建3D点云(如果可能)
if 'depth' in predictions and 'cameras' in predictions:
try:
point_map_unprojected = self._unproject_depth_to_points(
predictions['depth'].squeeze(0),
predictions['cameras']['extrinsic'].squeeze(0),
predictions['cameras']['intrinsic'].squeeze(0)
)
predictions['points_from_depth'] = point_map_unprojected
except Exception as e:
logger.warning(f"Failed to unproject depth to points: {e}")
return predictions
def selective_inference(self, images: torch.Tensor, branches: List[str], **kwargs) -> Dict:
"""选择性推理:只执行指定的分支"""
with torch.no_grad():
with torch.cuda.amp.autocast(dtype=torch.float16):
# 添加batch维度如果需要
if images.dim() == 4: # (N, C, H, W)
images = images[None] # (1, N, C, H, W)
# 1. 特征聚合 (必须步骤)
aggregated_tokens_list, ps_idx = self.model.aggregator(images)
results = {}
# 2. 根据需要执行不同分支
if 'cameras' in branches:
pose_enc = self.model.camera_head(aggregated_tokens_list)[-1]
extrinsic, intrinsic = self._decode_camera_params(pose_enc, images.shape[-2:])
results['cameras'] = {'extrinsic': extrinsic, 'intrinsic': intrinsic}
if 'depth' in branches:
depth_map, depth_conf = self.model.depth_head(aggregated_tokens_list, images, ps_idx)
results['depth'] = {'depth_map': depth_map, 'confidence': depth_conf}
if 'points' in branches:
point_map, point_conf = self.model.point_head(aggregated_tokens_list, images, ps_idx)
results['points'] = {'point_map': point_map, 'confidence': point_conf}
if 'points_from_depth' in branches and 'cameras' in results and 'depth' in results:
point_map_unprojected = self._unproject_depth_to_points(
results['depth']['depth_map'].squeeze(0),
results['cameras']['extrinsic'].squeeze(0),
results['cameras']['intrinsic'].squeeze(0)
)
results['points_from_depth'] = point_map_unprojected
if 'tracks' in branches and 'query_points' in kwargs:
track_list, vis_score, conf_score = self.model.track_head(
aggregated_tokens_list, images, ps_idx,
query_points=kwargs['query_points'][None]
)
results['tracks'] = {
'track_list': track_list,
'visibility_score': vis_score,
'confidence_score': conf_score
}
return results
def _decode_camera_params(self, pose_enc: torch.Tensor, image_shape: Tuple[int, int]) -> Tuple[torch.Tensor, torch.Tensor]:
"""解码相机参数 - 使用VGGT官方工具函数"""
try:
from vggt.utils.pose_enc import pose_encoding_to_extri_intri
return pose_encoding_to_extri_intri(pose_enc, image_shape)
except ImportError:
logger.error("VGGT pose_enc utils not available")
raise
def _unproject_depth_to_points(self, depth_map: torch.Tensor, extrinsic: torch.Tensor,
intrinsic: torch.Tensor) -> torch.Tensor:
"""从深度图反投影生成3D点 - 使用VGGT官方工具函数"""
try:
from vggt.utils.geometry import unproject_depth_map_to_point_map
return unproject_depth_map_to_point_map(depth_map, extrinsic, intrinsic)
except ImportError:
logger.error("VGGT geometry utils not available")
raise
class VGGTImageProcessor:
"""VGGT图像预处理器 - 基于官方load_and_preprocess_images函数"""
@staticmethod
def preprocess_images(images: List[np.ndarray], target_size: Tuple[int, int] = (336, 336)) -> torch.Tensor:
"""
图像预处理规范 - 遵循VGGT官方标准:
1. 尺寸调整到336x336(VGGT标准输入尺寸)
2. 归一化到[0,1]范围
3. 转换为torch.Tensor格式
4. 通道顺序RGB
5. 支持masking(设置像素值为0或1来屏蔽不需要的区域)
"""
try:
# 使用VGGT官方预处理函数
from vggt.utils.load_fn import load_and_preprocess_images
# 如果输入是numpy数组列表,需要先保存为临时文件
import tempfile
import os
from PIL import Image
temp_paths = []
temp_dir = tempfile.mkdtemp()
try:
# 保存图像到临时文件
for i, img_array in enumerate(images):
if isinstance(img_array, np.ndarray):
# 确保是RGB格式
if img_array.shape[-1] == 3:
img_pil = Image.fromarray(img_array.astype(np.uint8))
else:
img_pil = Image.fromarray(img_array.astype(np.uint8)).convert('RGB')
else:
img_pil = img_array
temp_path = os.path.join(temp_dir, f"temp_{i:04d}.jpg")
img_pil.save(temp_path)
temp_paths.append(temp_path)
# 使用官方预处理函数
processed_images = load_and_preprocess_images(temp_paths)
return processed_images
finally:
# 清理临时文件
for temp_path in temp_paths:
if os.path.exists(temp_path):
os.remove(temp_path)
os.rmdir(temp_dir)
except ImportError:
# 如果官方函数不可用,使用备用方案
return VGGTImageProcessor._fallback_preprocess(images, target_size)
@staticmethod
def _fallback_preprocess(images: List[np.ndarray], target_size: Tuple[int, int]) -> torch.Tensor:
"""备用图像预处理方案"""
import torch
import torchvision.transforms as transforms
from PIL import Image
transform = transforms.Compose([
transforms.Resize(target_size),
transforms.ToTensor(), # 自动归一化到[0,1]
])
processed = []
for img_array in images:
if isinstance(img_array, np.ndarray):
# 转换为PIL Image
if img_array.dtype != np.uint8:
img_array = (img_array * 255).astype(np.uint8)
pil_image = Image.fromarray(img_array).convert('RGB')
else:
pil_image = img_array
tensor_img = transform(pil_image)
processed.append(tensor_img)
return torch.stack(processed)
@staticmethod
def apply_masking(images: torch.Tensor, mask_type: str = None) -> torch.Tensor:
"""
应用遮罩 - VGGT支持简单的像素遮罩
mask_type: 'black_bg', 'white_bg', 'sky' 等
"""
if mask_type is None:
return images
masked_images = images.clone()
if mask_type == 'black_bg':
# 将黑色像素(接近0的像素)设置为0
black_mask = (images.sum(dim=1, keepdim=True) < 0.1) # RGB总和小于0.1
masked_images[black_mask.expand_as(images)] = 0.0
elif mask_type == 'white_bg':
# 将白色像素(接近1的像素)设置为1
white_mask = (images.sum(dim=1, keepdim=True) > 2.7) # RGB总和大于2.7
masked_images[white_mask.expand_as(images)] = 1.0
# 可以添加更多遮罩类型
return masked_images
class VGGTResultProcessor:
"""VGGT结果后处理器"""
@staticmethod
def format_camera_results(extrinsic: torch.Tensor, intrinsic: torch.Tensor) -> Dict:
"""相机参数结果格式化,同时保留原生Tensor输出"""
# 确保输入是tensor格式
if isinstance(extrinsic, np.ndarray):
extrinsic = torch.from_numpy(extrinsic)
if isinstance(intrinsic, np.ndarray):
intrinsic = torch.from_numpy(intrinsic)
return {
"raw": {
"extrinsic": extrinsic, # 原生Tensor
"intrinsic": intrinsic,
},
"json": {
"extrinsic_matrices": extrinsic.cpu().numpy().tolist(),
"intrinsic_matrices": intrinsic.cpu().numpy().tolist(),
"format": "opencv_convention",
"coordinate_system": "camera_from_world",
}
}
@staticmethod
def format_depth_results(depth_map: torch.Tensor, confidence: torch.Tensor) -> Dict:
"""深度结果格式化,同时保留原生Tensor输出"""
# 确保输入是tensor格式
if isinstance(depth_map, np.ndarray):
depth_map = torch.from_numpy(depth_map)
if isinstance(confidence, np.ndarray):
confidence = torch.from_numpy(confidence)
return {
"raw": {
"depth_map": depth_map,
"confidence": confidence,
},
"json": {
"depth_shape": list(depth_map.shape),
"depth_range": [float(depth_map.min()), float(depth_map.max())],
"confidence_range": [float(confidence.min()), float(confidence.max())],
}
}
@staticmethod
def format_point_cloud(points: torch.Tensor, colors: torch.Tensor = None, confidence: torch.Tensor = None) -> Dict:
"""点云结果格式化,同时保留原生Tensor输出"""
# 确保输入是tensor格式
if isinstance(points, np.ndarray):
points = torch.from_numpy(points)
if colors is not None and isinstance(colors, np.ndarray):
colors = torch.from_numpy(colors)
if confidence is not None and isinstance(confidence, np.ndarray):
confidence = torch.from_numpy(confidence)
result = {
"raw": {
"points": points,
"colors": colors,
"confidence": confidence,
},
"json": {
"num_points": int(points.shape[0]) if points.dim() > 1 else int(points.numel() // 3),
"point_shape": list(points.shape),
"bounds": {
"min": points.min(dim=0)[0].cpu().numpy().tolist() if points.numel() > 0 else [0, 0, 0],
"max": points.max(dim=0)[0].cpu().numpy().tolist() if points.numel() > 0 else [0, 0, 0],
}
}
}
if colors is not None:
result["json"]["has_colors"] = True
result["json"]["color_shape"] = list(colors.shape)
if confidence is not None:
result["json"]["has_confidence"] = True
result["json"]["confidence_range"] = [float(confidence.min()), float(confidence.max())]
return result