155 lines
6.0 KiB
Python
155 lines
6.0 KiB
Python
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
import os
|
|
from .common import (
|
|
COMFYUI_INPUT_FOLDER,
|
|
COMFYUI_OUTPUT_FOLDER,
|
|
get_default_extrinsics,
|
|
get_default_intrinsics,
|
|
get_recommended_resolution,
|
|
)
|
|
from .load_gaussian_ply import LoadGaussianPLY
|
|
|
|
|
|
class LoadGaussianPLYPath:
|
|
"""Load a Gaussian splat PLY file by entering the path directly."""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"ply_path": ("STRING", {
|
|
"default": "",
|
|
"multiline": False,
|
|
"tooltip": "Path to .ply file (absolute or relative to ComfyUI input/output folders)",
|
|
}),
|
|
"fov_degrees": ("FLOAT", {
|
|
"default": 50.0,
|
|
"min": 10.0,
|
|
"max": 180.0,
|
|
"step": 1.0,
|
|
"tooltip": "Horizontal field of view in degrees (10-180°, supports wide-angle and fisheye)",
|
|
}),
|
|
"auto_resolution": (["disabled", "enabled"], {
|
|
"default": "enabled",
|
|
"tooltip": "Auto-calculate optimal resolution based on FOV and target scale",
|
|
}),
|
|
},
|
|
"optional": {
|
|
"target_scale": ("FLOAT", {
|
|
"default": 10.0,
|
|
"min": 1.0,
|
|
"max": 50.0,
|
|
"step": 1.0,
|
|
"tooltip": "Target gaussian scale in viewer (used when auto_resolution enabled)",
|
|
}),
|
|
"image_width": ("INT", {
|
|
"default": 512,
|
|
"min": 64,
|
|
"max": 8192,
|
|
"step": 1,
|
|
"tooltip": "Image width for camera intrinsics (used when auto_resolution disabled)",
|
|
}),
|
|
"image_height": ("INT", {
|
|
"default": 512,
|
|
"min": 64,
|
|
"max": 8192,
|
|
"step": 1,
|
|
"tooltip": "Image height for camera intrinsics (used when auto_resolution disabled)",
|
|
}),
|
|
"enable_opacity_filter": (["disabled", "enabled"], {
|
|
"default": "disabled",
|
|
"tooltip": "Filter out low-opacity Gaussians to reduce background noise",
|
|
}),
|
|
"opacity_threshold": ("FLOAT", {
|
|
"default": 0.1,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.01,
|
|
"tooltip": "Minimum opacity (0-1). Gaussians below this will be removed (used when filter enabled)",
|
|
}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "EXTRINSICS", "INTRINSICS")
|
|
RETURN_NAMES = ("ply_path", "extrinsics", "intrinsics")
|
|
FUNCTION = "load_ply"
|
|
CATEGORY = "PlyPreview"
|
|
|
|
@classmethod
|
|
def IS_CHANGED(cls, ply_path, **kwargs):
|
|
resolved = cls._resolve_path(ply_path)
|
|
if resolved and os.path.exists(resolved):
|
|
return os.path.getmtime(resolved)
|
|
return ply_path
|
|
|
|
@classmethod
|
|
def _resolve_path(cls, ply_path):
|
|
if not ply_path:
|
|
return None
|
|
candidate = ply_path.strip().strip('"')
|
|
if candidate == "":
|
|
return None
|
|
if os.path.isabs(candidate) and os.path.exists(candidate):
|
|
return candidate
|
|
if COMFYUI_OUTPUT_FOLDER is not None:
|
|
output_candidate = os.path.join(COMFYUI_OUTPUT_FOLDER, candidate)
|
|
if os.path.exists(output_candidate):
|
|
return output_candidate
|
|
if COMFYUI_INPUT_FOLDER is not None:
|
|
input_candidate = os.path.join(COMFYUI_INPUT_FOLDER, candidate)
|
|
if os.path.exists(input_candidate):
|
|
return input_candidate
|
|
input_3d_candidate = os.path.join(COMFYUI_INPUT_FOLDER, "3d", candidate)
|
|
if os.path.exists(input_3d_candidate):
|
|
return input_3d_candidate
|
|
if os.path.exists(candidate):
|
|
return candidate
|
|
return None
|
|
|
|
def load_ply(
|
|
self,
|
|
ply_path: str,
|
|
fov_degrees: float = 50.0,
|
|
auto_resolution: str = "enabled",
|
|
target_scale: float = 10.0,
|
|
image_width: int = 512,
|
|
image_height: int = 512,
|
|
enable_opacity_filter: str = "disabled",
|
|
opacity_threshold: float = 0.1,
|
|
):
|
|
if not ply_path or ply_path.strip() == "":
|
|
raise ValueError("PLY path cannot be empty")
|
|
|
|
resolved = self._resolve_path(ply_path)
|
|
if resolved is None:
|
|
searched = [ply_path]
|
|
if COMFYUI_OUTPUT_FOLDER:
|
|
searched.append(os.path.join(COMFYUI_OUTPUT_FOLDER, ply_path))
|
|
if COMFYUI_INPUT_FOLDER:
|
|
searched.append(os.path.join(COMFYUI_INPUT_FOLDER, ply_path))
|
|
searched.append(os.path.join(COMFYUI_INPUT_FOLDER, "3d", ply_path))
|
|
formatted = "\n".join(f" - {path}" for path in searched)
|
|
raise ValueError(f"PLY file not found. Searched in:\n{formatted}")
|
|
|
|
if not resolved.lower().endswith(".ply"):
|
|
raise ValueError("File must be a .ply Gaussian splat")
|
|
|
|
print(f"[LoadGaussianPLYPath] Using PLY file: {resolved}")
|
|
|
|
if auto_resolution == "enabled":
|
|
image_width, image_height = get_recommended_resolution(fov_degrees, target_scale)
|
|
print(f"[LoadGaussianPLYPath] Auto-resolution for FOV {fov_degrees}° @ scale {target_scale}: {image_width}x{image_height}")
|
|
|
|
output_path = resolved
|
|
if enable_opacity_filter == "enabled":
|
|
loader = LoadGaussianPLY()
|
|
output_path = loader._filter_by_opacity(resolved, opacity_threshold)
|
|
print(f"[LoadGaussianPLYPath] Filtered PLY saved to: {output_path}")
|
|
|
|
extrinsics = get_default_extrinsics()
|
|
intrinsics = get_default_intrinsics(image_width, image_height, fov_degrees)
|
|
print(f"[LoadGaussianPLYPath] Camera: FOV={fov_degrees}°, size={image_width}x{image_height}")
|
|
|
|
return (output_path, extrinsics, intrinsics)
|