no more cp1252 problems

This commit is contained in:
PozzettiAndrea
2026-06-07 21:03:15 +02:00
parent e6819a1c6a
commit 65b63841fa
11 changed files with 139 additions and 104 deletions
+6 -6
View File
@@ -106,10 +106,10 @@ def split_panorama_image_gpu(image: np.ndarray, extrinsics: np.ndarray, intrinsi
grid_t = torch.from_numpy(grid).to(device) # (N, R, R, 2) fp32
# Image to (1, C, H, W) → broadcast to (N, C, H, W) for batched sample.
# Image to (1, C, H, W) -> broadcast to (N, C, H, W) for batched sample.
img_chw = torch.from_numpy(np.ascontiguousarray(image)).to(device)
if img_chw.ndim == 2:
img_chw = img_chw.unsqueeze(-1) # (H, W) → (H, W, 1)
img_chw = img_chw.unsqueeze(-1) # (H, W) -> (H, W, 1)
img_chw = img_chw.permute(2, 0, 1).contiguous().to(torch.float32) # (C, H, W)
img_batched = img_chw.unsqueeze(0).expand(N, -1, -1, -1) # (N, C, H, W)
@@ -185,7 +185,7 @@ def grad_equation(width: int, height: int, wrap_x: bool = False, wrap_y: bool =
def merge_panorama_depth(width: int, height: int, distance_maps: List[np.ndarray], pred_masks: List[np.ndarray], extrinsics: List[np.ndarray], intrinsics: List[np.ndarray], *, pbar=None, _top_size=None):
"""Recursive multi-resolution merge of N rectilinear face depth maps → equirect depth.
"""Recursive multi-resolution merge of N rectilinear face depth maps -> equirect depth.
Optional `pbar` is a ComfyUI ProgressBar-like object (any obj with
`update_absolute(value, total)`). Progress is weighted by `pixels^1.5`
@@ -209,7 +209,7 @@ def merge_panorama_depth(width: int, height: int, distance_maps: List[np.ndarray
if max(w_, h_) <= 256:
break
w_, h_ = w_ // 2, h_ // 2
levels.reverse() # smallest → largest, matches recursion unwind order
levels.reverse() # smallest -> largest, matches recursion unwind order
weights = np.array([(lw * lh) ** 1.5 for lw, lh in levels], dtype=np.float64)
weights = weights / weights.sum()
pbar._wn_levels = levels
@@ -238,10 +238,10 @@ def merge_panorama_depth(width: int, height: int, distance_maps: List[np.ndarray
width // 2, height // 2, distance_maps, pred_masks, extrinsics, intrinsics,
pbar=pbar, _top_size=_top_size,
)
_p(f"recursion returned; upsampling {width // 2}x{height // 2} → {width}x{height}")
_p(f"recursion returned; upsampling {width // 2}x{height // 2} -> {width}x{height}")
panorama_depth_init = cv2.resize(panorama_depth_init, (width, height), cv2.INTER_LINEAR)
else:
_p(f"base case (≤256 on each side) → no recursion, no init x0 for LSMR")
_p(f"base case (<=256 on each side) -> no recursion, no init x0 for LSMR")
panorama_depth_init = None
_bump(0.05)
+24 -24
View File
@@ -61,7 +61,7 @@ def subdivide_icosahedron(subdivisions: int = 1) -> np.ndarray:
Returns:
vertices: (N, 3) Subdivided vertex coordinates on the unit sphere.
"""
# utils3d renamed `icosahedron()` → `create_icosahedron_mesh()`.
# utils3d renamed `icosahedron()` -> `create_icosahedron_mesh()`.
vertices, faces = utils3d.numpy.create_icosahedron_mesh()
# Convert to a list so new vertices can be appended dynamically.
@@ -316,10 +316,10 @@ def solve_lsmr_gpu(A, b, x0=None, atol=1e-5, btol=1e-5, conlim=0.0,
scipy.sparse.linalg.lsmr (Fong & Saunders 2011), but the entire iteration
runs through torch tensors — the big work vectors u, v, h, hbar, x live
on the GPU; only the ~6 scalar Givens reductions per iteration come back
to the host. Eliminates the per-iteration host↔device round-trip that
the old `_TorchSparseLinearOperator` paid (matvec → .cpu().numpy() →
scipy numpy ops → as_tensor → next matvec). For a 1920×960 panorama
solve that's ~600 iterations × 4 transfers × 7 MB ≈ 30 GB of avoided
to the host. Eliminates the per-iteration host<->device round-trip that
the old `_TorchSparseLinearOperator` paid (matvec -> .cpu().numpy() ->
scipy numpy ops -> as_tensor -> next matvec). For a 1920×960 panorama
solve that's ~600 iterations × 4 transfers × 7 MB ~= 30 GB of avoided
PCIe traffic.
Numerical behaviour vs scipy fp64 CPU: SpMV runs in fp32 on cuSPARSE
@@ -384,10 +384,10 @@ def solve_lsmr_gpu(A, b, x0=None, atol=1e-5, btol=1e-5, conlim=0.0,
size=AT_csr.shape,
)
def _matvec(vec): # vec: (n,) → (m,)
def _matvec(vec): # vec: (n,) -> (m,)
return torch.sparse.mm(A_gpu, vec.unsqueeze(-1)).squeeze(-1)
def _rmatvec(vec): # vec: (m,) → (n,)
def _rmatvec(vec): # vec: (m,) -> (n,)
return torch.sparse.mm(AT_gpu, vec.unsqueeze(-1)).squeeze(-1)
# --- Upload b (and x0 if given). ---
@@ -614,7 +614,7 @@ def merge_panorama_depth_gpu(width: int, height: int, distance_maps: List[np.nda
ext_gpu = torch.from_numpy(np.stack(extrinsics).astype(np.float32)).to(device) # (N, 4, 4)
intr_gpu = torch.from_numpy(np.stack(intrinsics).astype(np.float32)).to(device) # (N, 3, 3)
# --- Batched projection: equirect rays → per-face image plane. ---
# --- Batched projection: equirect rays -> per-face image plane. ---
# p_cam = R @ dirs + t. Then K @ p_cam, perspective divide.
R = ext_gpu[:, :3, :3] # (N, 3, 3)
t = ext_gpu[:, :3, 3] # (N, 3)
@@ -718,7 +718,7 @@ def merge_panorama_depth_gpu(width: int, height: int, distance_maps: List[np.nda
any_mask_lap = mask_laplacian.any(0) # (H, W)
panorama_pred_mask_final = panorama_pred_mask_per_face.any(0) # (H, W)
# --- Single device→host transfer of the data we need on CPU for the
# --- Single device->host transfer of the data we need on CPU for the
# sparse-system build below. ---
avg_grad_x_np = avg_grad_x.cpu().numpy()
avg_grad_y_np = avg_grad_y.cpu().numpy()
@@ -836,7 +836,7 @@ def pred_pano_depth(model, image: Image.Image, scale=1.0, resize_to=1920, remove
else:
skip_ratio_info = f"{skipped_count / (pred_count + skipped_count):.2%}"
print(f"\t 🔍 Predicted {pred_count} splitted images, skipped {skipped_count} splitted images. Skip ratio: {skip_ratio_info}")
print(f"\t Predicted {pred_count} splitted images, skipped {skipped_count} splitted images. Skip ratio: {skip_ratio_info}")
# merge moge depth
merging_width, merging_height = width, height
@@ -896,7 +896,7 @@ def compute_spherical_ray_derivatives(spherical_directions: np.ndarray):
ray = spherical_directions
# Azimuth theta and elevation phi.
# ray = (cos(φ)sin(θ), sin(φ), cos(φ)cos(θ))
# ray = (cos(phi)sin(theta), sin(phi), cos(phi)cos(theta))
# Or adapt to the coordinate-system definition in use.
# Method 1: numerical computation, which is more robust.
@@ -904,15 +904,15 @@ def compute_spherical_ray_derivatives(spherical_directions: np.ndarray):
v = (np.arange(H) + 0.5) / H # [0, 1]
u_grid, v_grid = np.meshgrid(u, v)
theta = (u_grid - 0.5) * 2 * np.pi # [-π, π]
phi = (0.5 - v_grid) * np.pi # [π/2, -π/2]
theta = (u_grid - 0.5) * 2 * np.pi # [-pi, pi]
phi = (0.5 - v_grid) * np.pi # [pi/2, -pi/2]
cos_phi = np.cos(phi)
sin_phi = np.sin(phi)
cos_theta = np.cos(theta)
sin_theta = np.sin(theta)
# ∂ray/∂θ, assuming standard equirectangular mapping.
# dray/dtheta, assuming standard equirectangular mapping.
# Adjust according to how spherical_directions is actually computed.
dray_dtheta = np.stack([
cos_phi * cos_theta,
@@ -920,7 +920,7 @@ def compute_spherical_ray_derivatives(spherical_directions: np.ndarray):
-cos_phi * sin_theta
], axis=-1)
# ∂ray/∂φ
# dray/dphi
dray_dphi = np.stack([
-sin_phi * sin_theta,
cos_phi,
@@ -941,9 +941,9 @@ def normal_to_log_distance_gradient(
Derivation:
Surface point P = d * ray
Tangent vector ∂P/∂θ = (∂d/∂θ) * ray + d * (∂ray/∂θ)
The normal is perpendicular to the tangent: n · ∂P/∂θ = 0
=> ∂(log d)/∂θ = -(n · ∂ray/∂θ) / (n · ray)
Tangent vector dP/dtheta = (dd/dtheta) * ray + d * (dray/dtheta)
The normal is perpendicular to the tangent: n · dP/dtheta = 0
=> d(log d)/dtheta = -(n · dray/dtheta) / (n · ray)
Args:
panorama_normal: (H, W, 3) Panorama normal map in world coordinates.
@@ -973,14 +973,14 @@ def normal_to_log_distance_gradient(
n_dot_ray_safe = np.where(valid_mask, n_dot_ray, 1.0)
# Continuous-space log(d) gradients with respect to angles.
# ∂(log d)/∂θ = -(n · ∂ray/∂θ) / (n · ray)
# d(log d)/dtheta = -(n · dray/dtheta) / (n · ray)
dlogd_dtheta = -n_dot_dray_dtheta / n_dot_ray_safe # (H, W)
dlogd_dphi = -n_dot_dray_dphi / n_dot_ray_safe # (H, W)
# Convert to discrete pixel gradients.
# In this code, grad_x = log_d[j] - log_d[j+1] = -∂(log d)/∂θ * Δθ.
# Δθ = 2π / W, the θ change per pixel.
# grad_y = log_d[i] - log_d[i+1] = -∂(log d)/∂φ * Δφ = ∂(log d)/∂φ * (π/H)
# In this code, grad_x = log_d[j] - log_d[j+1] = -d(log d)/dtheta * Deltatheta.
# Deltatheta = 2pi / W, the theta change per pixel.
# grad_y = log_d[i] - log_d[i+1] = -d(log d)/dphi * Deltaphi = d(log d)/dphi * (pi/H)
delta_theta = 2 * np.pi / W
delta_phi = np.pi / H
@@ -1257,7 +1257,7 @@ def convert_rgbd2pcd_multi_scale_panorama(
else:
interval_mask = ((median_distance * depth_intervals[i - 1]) < distance_nchw) & (distance_nchw <= (median_distance * depth_intervals[i]))
# pointclouds number ∝ depth^2
# pointclouds number prop depth^2
resize_scale = depth_intervals[i]
if interval_mask.sum() == 0:
continue
@@ -2075,7 +2075,7 @@ def erp_distance_ray_to_normal(distance_map, ray_directions,
normal_map = np.stack([normal_x, normal_y, normal_z], axis=-1)
# 7. Convert to an RGB visualization.
# [-1, 1] → [0, 255]
# [-1, 1] -> [0, 255]
normal_rgb = ((normal_map + 1.0) / 2.0 * 255).astype(np.uint8)
return normal_map, normal_rgb
+8 -8
View File
@@ -9,7 +9,7 @@ Channel order is fixed by `SharpPredictGaussianAttrs.GAUSS_ATTR_CHANNEL_NAMES`:
0: position_x (NDC)
1: position_y (NDC)
2: position_z (NDC / camera-space z)
3: scale_x (Σ-singular value, world units after unprojection)
3: scale_x (Sigma-singular value, world units after unprojection)
4: scale_y
5: scale_z
6: quaternion_w
@@ -24,7 +24,7 @@ Channel order is fixed by `SharpPredictGaussianAttrs.GAUSS_ATTR_CHANNEL_NAMES`:
We rebuild a `Gaussians3D` namedtuple in NDC, then call SHARP's
`unproject_gaussians` to transform to world coordinates using the
per-face extrinsics + intrinsics. Output convention matches SharpPredict:
single batch → one .ply; N>1 batch → folder of NNN.ply.
single batch -> one .ply; N>1 batch -> folder of NNN.ply.
"""
import logging
@@ -61,7 +61,7 @@ OPACITY = 13
class SharpImageAttrsToPLY(io.ComfyNode):
"""MULTIBAND_IMAGE of gaussian attrs + camera → PLY file."""
"""MULTIBAND_IMAGE of gaussian attrs + camera -> PLY file."""
@classmethod
def define_schema(cls):
@@ -171,7 +171,7 @@ class SharpImageAttrsToPLY(io.ComfyNode):
[0.0, 0.0, 1.0],
], dtype=torch.float32)
intr = K_default.unsqueeze(0).repeat(B, 1, 1)
_p(f"intrinsics not wired → using identity-style K (fx=fy={f_px_default:.1f}, "
_p(f"intrinsics not wired -> using identity-style K (fx=fy={f_px_default:.1f}, "
f"cx={W/2:.1f}, cy={H/2:.1f}). Pass `intrinsics` from "
f"SharpPredictGaussianAttrs for correct geometry.")
else:
@@ -212,11 +212,11 @@ class SharpImageAttrsToPLY(io.ComfyNode):
t_start = time.time()
for b in range(B):
# ---- extract per-pixel attrs → Gaussians3D in NDC ------------
# ---- extract per-pixel attrs -> Gaussians3D in NDC ------------
s = samples[b] # [14, H, W]
n_pixels = H * W
# Multiband stores [C, H, W]; flatten spatial dims → [C, N], then
# Multiband stores [C, H, W]; flatten spatial dims -> [C, N], then
# transpose to [N, C] for the per-channel slices below.
flat = s.reshape(C, n_pixels).T # [N, C]
mean_ndc = flat[:, POS].contiguous() # [N, 3]
@@ -252,7 +252,7 @@ class SharpImageAttrsToPLY(io.ComfyNode):
# for (H, W) (per the rescale done in SharpPredictGaussianAttrs).
image_shape = (H, W)
# ---- NDC → world ---------------------------------------------
# ---- NDC -> world ---------------------------------------------
gaussians_world = unproject_gaussians(
gaussians_ndc, ext_b, K_b, image_shape,
)
@@ -278,7 +278,7 @@ class SharpImageAttrsToPLY(io.ComfyNode):
loc = output_folder if is_batch else output_path
total_n = B * H * W
_p(
f"{B} multiband(s) {H}×{W} → {total_n/1e6:.2f}M gaussians; "
f"{B} multiband(s) {H}×{W} -> {total_n/1e6:.2f}M gaussians; "
f"saved to {loc}; {elapsed:.1f}s"
)
+3 -3
View File
@@ -305,11 +305,11 @@ class MergeGaussians(io.ComfyNode):
elapsed = time.time() - t_start
_p(
f"merged {len(ply_files)} file(s) → {num_gaussians/1e6:.2f}M gaussians "
f"→ {output_path}{size_str}; "
f"merged {len(ply_files)} file(s) -> {num_gaussians/1e6:.2f}M gaussians "
f"-> {output_path}{size_str}; "
f"save {save_elapsed:.1f}s, total {elapsed:.1f}s"
)
_p(f"field PLY (positions + face_id only) → {field_output_path}")
_p(f"field PLY (positions + face_id only) -> {field_output_path}")
return io.NodeOutput(
str(ply_folder_in), str(output_path),
+2 -2
View File
@@ -193,9 +193,9 @@ class SharpPredict(io.ComfyNode):
# E` chain both want an UNBATCHED matrix.
if img_intrinsics.dim() == 3 and img_intrinsics.shape[0] == 1:
img_intrinsics = img_intrinsics[0]
# Detect NORMALIZED K (fx, fy ∈ [0, 1]) and rescale to
# Detect NORMALIZED K (fx, fy in [0, 1]) and rescale to
# pixel-K at this image's resolution. PanoramaSplit emits
# normalized K by default (fx≈0.5 for a 90° FOV face);
# normalized K by default (fx~=0.5 for a 90° FOV face);
# SHARP's downstream `intrinsics_resized = K * (internal /
# width)` plus `f_px = K[0, 0].item()` both assume pixel-K
# at the INPUT image's resolution. Same sniff pattern HYWM2
+1 -1
View File
@@ -6,7 +6,7 @@ practical). Two selection modes:
- **closest** (default): per pixel, keep the gaussian with the smaller
world-space z (closer to camera). SHARP's monodepth output IS sorted
near→far at the head (`model.py:1122-1125`: `disparity.max` for layer
near->far at the head (`model.py:1122-1125`: `disparity.max` for layer
0, `disparity.min` for layer 1), but the gaussian decoder adds an
unconstrained delta_z per layer — so layer 0 isn't reliably the front
post-decode. `closest` looks at the actual z and picks per-pixel,
+11 -11
View File
@@ -36,7 +36,7 @@ Multiband channel order (per layer):
1: position_y (ndc, in [-1, 1])
2: position_z (ndc / camera z, same as depth_refined output;
duplicated here so multiband is self-contained)
3: scale_x (Σ-singular value)
3: scale_x (Sigma-singular value)
4: scale_y
5: scale_z
6: quaternion_w
@@ -254,7 +254,7 @@ class SharpPredictGaussianAttrs(io.ComfyNode):
"depth map — wire with `extrinsics_mdepth + "
"intrinsics_mdepth` into SharpDepthMerge for a "
"seam-free LSMR merge that matches MoGe2-style "
"smoothness. Memory: 42 faces × 1536² fp32 ≈ 395 MB."),
"smoothness. Memory: 42 faces × 1536² fp32 ~= 395 MB."),
io.Custom("EXTRINSICS").Output(
display_name="extrinsics_mdepth",
tooltip="Pass-through of input extrinsics. Extrinsics are "
@@ -338,7 +338,7 @@ class SharpPredictGaussianAttrs(io.ComfyNode):
intrinsics = _K_default.unsqueeze(0).repeat(B, 1, 1)
_fov_deg = 2 * _math.degrees(_math.atan((_img_H / 2.0) / _f_px_default))
_p(
f"intrinsics not wired → using identity-style K "
f"intrinsics not wired -> using identity-style K "
f"(focal={_f_px_default:.1f}px, image={_img_W}×{_img_H}, "
f"~{_fov_deg:.1f}° FOV); pass intrinsics from "
f"SharpPanoramaIcosahedronSplit for accurate geometry."
@@ -406,10 +406,10 @@ class SharpPredictGaussianAttrs(io.ComfyNode):
# produces the same gaussians.
#
# Convention sniff: PanoPack's PanoramaSplit emits NORMALIZED K
# (fx≈0.5, cx≈0.5 for 90° fov via utils3d.np.intrinsics_from_fov,
# (fx~=0.5, cx~=0.5 for 90° fov via utils3d.np.intrinsics_from_fov,
# units in [0,1]) whereas Sharp's predict path assumes pixel-K
# (fx in the hundreds). Rescale to pixel-K once before computing
# f_px so the disparity → depth math doesn't collapse to ~0.
# f_px so the disparity -> depth math doesn't collapse to ~0.
if intrinsics is not None:
intr_b = intrinsics[b] if intrinsics.dim() == 3 else intrinsics
if float(intr_b[0, 0]) < 2.0:
@@ -492,7 +492,7 @@ class SharpPredictGaussianAttrs(io.ComfyNode):
f"{'layer0 rgb':>22} | {'layer1 rgb':>22} | {'drgb (l1-l0)':>22}"
)
_dzs = []
_drgbs_l1 = [] # ‖rgb1 - rgb0‖₁ per sampled pixel
_drgbs_l1 = [] # ||rgb1 - rgb0||1 per sampled pixel
for _y, _x in zip(_ys, _xs):
_p0 = attrs0_batch[0, _y, _x, 0:3].tolist() # position xyz (NDC)
_p1 = attrs1_batch[0, _y, _x, 0:3].tolist()
@@ -514,8 +514,8 @@ class SharpPredictGaussianAttrs(io.ComfyNode):
_dz_med = float(sorted(_dzs)[len(_dzs)//2])
_drgb_med = float(sorted(_drgbs_l1)[len(_drgbs_l1)//2])
_verdict_z = "layer0 in front" if _dz_med > 0 else "layer1 in front (decoder swapped at the sampled pixels)"
_p(f" median dz = {_dz_med:+.4f} → {_verdict_z}")
_p(f" median ‖rgb1 - rgb0‖₁ = {_drgb_med:+.4f} (sum of abs RGB deltas; 0 = layers share color)")
_p(f" median dz = {_dz_med:+.4f} -> {_verdict_z}")
_p(f" median ||rgb1 - rgb0||1 = {_drgb_med:+.4f} (sum of abs RGB deltas; 0 = layers share color)")
except Exception as _e:
_p(f"layer-slice sanity check failed: {_e!r}")
@@ -561,7 +561,7 @@ class SharpPredictGaussianAttrs(io.ComfyNode):
# Rescale intrinsics so pixel-K matches the emitted depth grid for
# each output pair. The downstream merger normalizes K by the depth
# tensor's W; if K's native res doesn't match that W, the effective
# FOV is wrong → visible seams. We emit TWO K versions:
# FOV is wrong -> visible seams. We emit TWO K versions:
# - intrinsics : K rescaled to (H_grid, W_grid) = 768²,
# pairs with gaussian-z outputs.
# - intrinsics_mdepth: K rescaled to (1536, 1536),
@@ -606,11 +606,11 @@ class SharpPredictGaussianAttrs(io.ComfyNode):
d1_med = float(d1_batch.median())
m_med = float(metric_batch.median())
k_str = (
f", K_{H_grid} fx→{k_fx_dbg:.1f} K_{internal_shape[0]} fx→{k_fx_md_dbg:.1f}"
f", K_{H_grid} fx->{k_fx_dbg:.1f} K_{internal_shape[0]} fx->{k_fx_md_dbg:.1f}"
if k_fx_dbg is not None else ""
)
_p(
f"{B} face(s) → {n_gaussians_total/1e6:.2f}M gaussians "
f"{B} face(s) -> {n_gaussians_total/1e6:.2f}M gaussians "
f"({B}×{num_layers}×{H_grid}²); "
f"metric/layer0/layer1 depth median={m_med:.2f}m/{d0_med:.2f}m/{d1_med:.2f}m"
f"{k_str}; {elapsed:.1f}s"
+70 -35
View File
@@ -1,4 +1,4 @@
"""SharpPredictGaussiansFromMetricDepth — image + external depth → PLY paths.
"""SharpPredictGaussiansFromMetricDepth — image + external depth -> PLY paths.
Like SharpPredict, but **directly overrides SHARP's monodepth** with an
externally-provided depth (e.g. from SharpDepthMerge after LSMR-merging
@@ -14,27 +14,27 @@ panorama-merging workflows where SHARP's per-face monodepth has scale
ambiguity that the depth_alignment module wasn't trained to handle.
Output convention matches SharpPredict:
- Single image → single PLY file at OUTPUT_DIR/{prefix}_{ts}.ply
- Batch (>1) → folder OUTPUT_DIR/{prefix}_{ts}/ with NNN.ply files
- Single image -> single PLY file at OUTPUT_DIR/{prefix}_{ts}.ply
- Batch (>1) -> folder OUTPUT_DIR/{prefix}_{ts}/ with NNN.ply files
Wire the batch folder straight into `MergeGaussians (PLY Files)` for a
single merged scene PLY.
Typical pipeline:
1. SharpPanoramaIcosahedronSplit → face images
2. SharpPredictMetricDepth (per face) → raw per-face metric depths
3. SharpDepthMerge → seam-free equirect depth (LSMR)
1. SharpPanoramaIcosahedronSplit -> face images
2. SharpPredictMetricDepth (per face) -> raw per-face metric depths
3. SharpDepthMerge -> seam-free equirect depth (LSMR)
4. SharpPanoramaIcosahedronSplit (on the merged depth IMAGE)
→ re-cropped face depths in
-> re-cropped face depths in
equirect ray-distance convention
5. SharpPredictGaussiansFromMetricDepth(face_images, consistent_face_depths,
depth_convention="ray_distance")
→ folder of seam-aligned PLYs
6. MergeGaussians → final unified PLY
-> folder of seam-aligned PLYs
6. MergeGaussians -> final unified PLY
When step 4 returns ray-distance depth (the equirect convention), set
`depth_convention="ray_distance"` so the node applies the
ray-distance → planar-Z cos-map conversion using the per-face intrinsics.
ray-distance -> planar-Z cos-map conversion using the per-face intrinsics.
For depths that are already in SHARP's native planar-Z convention
(e.g. straight from `SharpPredictMetricDepth` on a single face),
leave it at the default `"planar_z"`.
@@ -71,7 +71,7 @@ def _p(msg: str) -> None:
class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
"""SHARP gaussian decoder driven by external depth → PLY paths."""
"""SHARP gaussian decoder driven by external depth -> PLY paths."""
@classmethod
def define_schema(cls):
@@ -85,7 +85,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
"becomes the geometric scaffold for `init_model`'s gaussian "
"base values, so gaussians share a consistent surface across "
"multiple views — fixes per-face seams.\n\n"
"Saves PLYs (single image → one .ply; batch → folder of "
"Saves PLYs (single image -> one .ply; batch -> folder of "
"NNN.ply). Pipe into MergeGaussians to combine into one PLY."
),
is_output_node=True,
@@ -145,7 +145,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
)),
io.Combo.Input(
"snap_to_external_depth",
options=["none", "z_only", "xyz_full"],
options=["none", "z_only", "z_only_firstlayer", "xyz_full", "xyz_full_firstlayer"],
default="none",
tooltip=(
"Force gaussian positions to exactly match the external "
@@ -156,11 +156,22 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
"the prediction head's sub-pixel xy drift survives — "
"layer 1 still varies in appearance from layer 0), but "
"depth is forced to match external.\n\n"
" z_only_firstlayer: snap ONLY layer 0 to the external "
"depth (same proportional z rescale as z_only); leave "
"layer 1 (and any further layers) at the decoder's "
"predicted z. The front surface is pinned to your depth "
"while the back gaussians stay where the model put them "
"-- preserves SHARP's hallucinated occlusion backplate.\n\n"
" xyz_full: reset xy to the pixel-grid identity AND "
"set depth to external. Every gaussian sits exactly "
"on the back-projected ray of its source pixel at the "
"external depth — no learned drift at all. Most rigid; "
"layer 0 and layer 1 collapse to identical positions.\n\n"
" xyz_full_firstlayer: apply the xyz_full reset to "
"layer 0 only (front surface sits exactly on the "
"pixel-grid ray at the external depth); leave layer 1+ "
"at the decoder's predicted position (occlusion "
"backplate preserved).\n\n"
" none: skip the snap entirely — trust the gaussian "
"decoder's predicted z (the direct-monodepth-override "
"behavior is in effect but not enforced)."
@@ -180,7 +191,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
" ray_distance: Euclidean distance from the camera "
"center along each pixel's ray — the equirect "
"convention. Pick this when the depth was extracted "
"via SharpDepthMerge → SharpPanoramaIcosahedronSplit, "
"via SharpDepthMerge -> SharpPanoramaIcosahedronSplit, "
"i.e. when each face was cut out of an equirect "
"depth panorama. The node multiplies by the per-face "
"cos-map `1 / sqrt(((u-cx)/fx)² + ((v-cy)/fy)² + 1)` "
@@ -293,10 +304,10 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
f"metric_depth_layer1 batch {metric_depth_l1.shape[0]} != "
f"image batch {B}"
)
_p("metric_depth_layer1 wired → layer-0 + layer-1 will get "
_p("metric_depth_layer1 wired -> layer-0 + layer-1 will get "
"DIFFERENT depths through init_model")
else:
_p("metric_depth_layer1 unwired → layer 1 will duplicate layer 0 "
_p("metric_depth_layer1 unwired -> layer 1 will duplicate layer 0 "
"(legacy behavior)")
# Sanity-check the alignment is wired through the model.
@@ -332,7 +343,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
# Per-face denormalized PIXEL-K, captured so the `intrinsics`
# output socket emits the same convention SharpPredict does
# (pixel-K), regardless of whether the input was normalized
# (PanoramaSplit, fx≈0.5) or already pixel-K.
# (PanoramaSplit, fx~=0.5) or already pixel-K.
all_intr_px = []
pbar = comfy.utils.ProgressBar(B)
@@ -396,10 +407,10 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
# intr_b -- the f_px formula below, the cos_map for ray_distance,
# and the unprojection K -- sees the same convention.
# `intr_b_px` is the per-batch K in PIXEL-K convention. We
# reuse it for f_px, the ray→planar cos_map, AND the
# reuse it for f_px, the ray->planar cos_map, AND the
# unprojection K further down — previously the unprojection
# step re-fetched from the raw `intrinsics` tensor, which
# silently bypassed the normalized→pixel correction and
# silently bypassed the normalized->pixel correction and
# produced garbage when PanoramaSplit's normalized K was wired.
intr_b_px = None
if intrinsics is not None:
@@ -424,13 +435,13 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
f_px = float(_cfl(width, height, max(0.1, float(focal_length_mm or 30.0))))
disparity_factor = torch.tensor([f_px / width]).float().to(device)
# External depth → 1536², broadcast to BOTH layers.
# External depth -> 1536², broadcast to BOTH layers.
md_b = metric_depth[b]
if md_b.dim() == 3:
md_b = md_b[..., 0]
md_b = md_b.to(device).float().unsqueeze(0).unsqueeze(0)
# Optional ray-distance → planar-Z conversion. Apply at the
# Optional ray-distance -> planar-Z conversion. Apply at the
# depth's native resolution (before the resize to 1536²) so the
# convention boundary doesn't get smeared by bilinear interp.
# cos_map = 1 / sqrt(((u-cx)/fx)² + ((v-cy)/fy)² + 1), built from
@@ -463,7 +474,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
cos_map = 1.0 / torch.sqrt(x_cam * x_cam + y_cam * y_cam + 1.0)
md_b = md_b * cos_map.unsqueeze(0).unsqueeze(0)
if b == 0:
_p(f"depth_convention=ray_distance → applying cos_map "
_p(f"depth_convention=ray_distance -> applying cos_map "
f"(min={float(cos_map.min()):.4f} "
f"max={float(cos_map.max()):.4f}) per face")
@@ -472,7 +483,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
)
# Layer-1 path. When wired, apply the SAME conversions
# (ray-distance → planar-Z if requested) to keep both layers
# (ray-distance -> planar-Z if requested) to keep both layers
# in the convention the model expects.
if metric_depth_l1 is not None:
md_b1 = metric_depth_l1[b]
@@ -509,12 +520,12 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
# Layer 0 = your front depth (e.g. plane-snapped HYWM2),
# Layer 1 = your back depth (e.g. SHARP's own predicted
# layer 1, or a +offset back-plate). Prediction head still
# adds per-layer Δs on top of these distinct base surfaces.
# adds per-layer Deltas on top of these distinct base surfaces.
monodepth_override = torch.cat(
[external_depth, external_depth_l1], dim=1,
) # [1, 2, 1536, 1536] with layer 0 ≠ layer 1
) # [1, 2, 1536, 1536] with layer 0 != layer 1
else:
# Legacy behavior: both monodepth layers = external depth →
# Legacy behavior: both monodepth layers = external depth ->
# no back-surface hallucination. Layer 1's gaussians collapse
# onto the same surface as layer 0; the prediction head's
# per-layer deltas can still drift them sub-pixel in xy and
@@ -526,8 +537,8 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
# map UNet). The paper's recommended inference path passes
# depth=None, leaving alignment as identity and using SHARP's
# own monodepth. Here we instead replace monodepth entirely
# with the user's external depth — same code path init_model →
# feature_model → prediction_head → gaussian_composer that the
# with the user's external depth — same code path init_model ->
# feature_model -> prediction_head -> gaussian_composer that the
# official `decode()` runs, just with external monodepth
# instead of `disparity_factor / disparity`.
init_output = predictor.init_model(image_resized_pt, monodepth_override)
@@ -646,12 +657,26 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
if snap_to_external_depth == "z_only":
# Proportional rescale: mean_vectors *= target_z/current_z.
# (xx*z, yy*z, z) → (xx*target_z, yy*target_z, target_z).
# (xx*z, yy*z, z) -> (xx*target_z, yy*target_z, target_z).
scale = (target_z_flat / current_z).unsqueeze(0).unsqueeze(-1)
new_mean_vectors = gaussians_ndc.mean_vectors * scale
gaussians_ndc = gaussians_ndc._replace(
mean_vectors=new_mean_vectors,
)
elif snap_to_external_depth == "z_only_firstlayer":
# Snap ONLY layer 0 to the external depth; leave layer 1+ at the
# decoder's predicted z (the hallucinated occlusion backplate).
# Gaussians are flattened (layer, h, w), so layer 0 is the first
# H_grid*W_grid entries. Per-gaussian scale = 1.0 everywhere
# except layer 0, where it is target_z/current_z.
per_scale = torch.ones_like(current_z)
L0 = H_grid * W_grid
per_scale[:L0] = target_z_flat[:L0] / current_z[:L0]
scale = per_scale.unsqueeze(0).unsqueeze(-1)
new_mean_vectors = gaussians_ndc.mean_vectors * scale
gaussians_ndc = gaussians_ndc._replace(
mean_vectors=new_mean_vectors,
)
elif snap_to_external_depth == "xyz_full":
# Reset xy to pixel-grid identity AND set z to target.
new_mean_vectors = torch.stack(
@@ -665,7 +690,17 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
gaussians_ndc = gaussians_ndc._replace(
mean_vectors=new_mean_vectors,
)
# else: "none" → leave gaussians_ndc as-is.
elif snap_to_external_depth == "xyz_full_firstlayer":
# xyz_full applied to layer 0 only; layer 1+ left untouched.
new_mean_vectors = gaussians_ndc.mean_vectors.clone()
L0 = H_grid * W_grid
new_mean_vectors[0, :L0, 0] = base_xx_flat[:L0] * target_z_flat[:L0]
new_mean_vectors[0, :L0, 1] = base_yy_flat[:L0] * target_z_flat[:L0]
new_mean_vectors[0, :L0, 2] = target_z_flat[:L0]
gaussians_ndc = gaussians_ndc._replace(
mean_vectors=new_mean_vectors,
)
# else: "none" -> leave gaussians_ndc as-is.
# Build extrinsics + intrinsics_resized for unprojection.
if extrinsics is not None:
@@ -677,7 +712,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
if intrinsics is not None:
# Reuse the per-batch PIXEL-K computed at the top of the
# loop. Re-fetching from `intrinsics` here would silently
# bypass the normalized→pixel correction and give garbage
# bypass the normalized->pixel correction and give garbage
# `intrinsics_resized` (fx_resized = 0.5 * internal/width).
K = intr_b_px.to(device).float().clone()
# Promote to 4x4 if it came as 3x3 (unproject_gaussians wants 4x4).
@@ -752,7 +787,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
n_after = int(gaussians.mean_vectors.shape[1])
if b == 0: # log once per batch to avoid spam on 42 faces
_p(
f"edge_crop={edge_crop:.3f} → crop_h={crop_h} crop_w={crop_w} "
f"edge_crop={edge_crop:.3f} -> crop_h={crop_h} crop_w={crop_w} "
f"per side; kept {n_after}/{n_before} gaussians "
f"({100.0 * n_after / max(n_before, 1):.1f}%) per face"
)
@@ -783,7 +818,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
)
n_after_mask = int(gaussians.mean_vectors.shape[1])
if b == 0:
_p(f"mask → kept {n_after_mask}/{n_before_mask} gaussians "
_p(f"mask -> kept {n_after_mask}/{n_before_mask} gaussians "
f"({100.0 * n_after_mask / max(n_before_mask, 1):.1f}%)")
# Save PLY.
@@ -805,7 +840,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
if edge_crop and edge_crop > 0.0:
layer_str += f", edge_crop={edge_crop:.3f}"
_p(
f"{B} face(s) → {n_gaussians_total/1e6:.2f}M gaussians total "
f"{B} face(s) -> {n_gaussians_total/1e6:.2f}M gaussians total "
f"({layer_str}, snap={snap_to_external_depth}); "
f"saved to {loc}; {elapsed:.1f}s"
)
@@ -822,7 +857,7 @@ class SharpPredictGaussiansFromMetricDepth(io.ComfyNode):
#
# - intrinsics: forward the DENORMALIZED pixel-K we captured
# per face (`all_intr_px`), NOT the raw input. PanoramaSplit
# emits normalized K (fx≈0.5); SharpPredict's `intrinsics`
# emits normalized K (fx~=0.5); SharpPredict's `intrinsics`
# output is pixel-K (fx in the hundreds) because it stores
# the denormalized form in `metadata["intrinsic"]`. Matching
# that convention keeps downstream consumers (preview
+7 -7
View File
@@ -1,4 +1,4 @@
"""SharpPredictMetricDepth — image → disparity-head metric depth only.
"""SharpPredictMetricDepth — image -> disparity-head metric depth only.
Tap SHARP's encoder + disparity head; skip the gaussian decoder entirely.
Fastest path to a clean per-pixel depth map at SHARP's native 1536²
@@ -56,7 +56,7 @@ def _rescale_K(intr_in, target_w, target_h, src_w, src_h):
class SharpPredictMetricDepth(io.ComfyNode):
"""SHARP encoder + disparity head only → per-pixel metric depth at 1536²."""
"""SHARP encoder + disparity head only -> per-pixel metric depth at 1536²."""
@classmethod
def define_schema(cls):
@@ -99,7 +99,7 @@ class SharpPredictMetricDepth(io.ComfyNode):
tooltip="[B, 1536, 1536, 3] LAYER-1 (back/occluded) metric "
"depth — SHARP's hallucinated backplate surface. "
"Typically tracks layer 0 in flat regions and "
"diverges at occlusion boundaries (column edges → "
"diverges at occlusion boundaries (column edges -> "
"sky depth behind, etc.). Feeds layer-1 gaussian "
"base values."),
io.Custom("EXTRINSICS").Output(
@@ -169,17 +169,17 @@ class SharpPredictMetricDepth(io.ComfyNode):
)
intrinsics = _K_default.unsqueeze(0).repeat(B, 1, 1)
_p(
f"intrinsics not wired → using identity-style K "
f"intrinsics not wired -> using identity-style K "
f"(focal={_f_px_default:.1f}px @ image {_img_W}×{_img_H}, "
f"35mm-equiv {focal_length_mm:.1f}mm). Pass intrinsics from "
f"PanoramaSplit for per-face accurate K."
)
else:
# Convention sniff: PanoPack's PanoramaSplit emits NORMALIZED K
# (fx≈0.5, cx≈0.5 for 90° fov via utils3d.np.intrinsics_from_fov,
# (fx~=0.5, cx~=0.5 for 90° fov via utils3d.np.intrinsics_from_fov,
# units in [0,1]), whereas Sharp's predict path assumes pixel-K
# (fx in the hundreds). Without this conversion, `f_px = K[0,0] *
# (1536/width)` resolves to 0.5 instead of ~768, the disparity →
# (1536/width)` resolves to 0.5 instead of ~768, the disparity ->
# depth math collapses, and every face comes out as a blank (~0)
# depth map. Detect normalized (fx < 2.0) and rescale once to
# pixel-K for the face image's (width, height). All downstream
@@ -314,7 +314,7 @@ class SharpPredictMetricDepth(io.ComfyNode):
pts_min = float(pts_norm.min())
pts_max = float(pts_norm.max())
elapsed = time.time() - t_start
k_str = f", K_{internal_shape[0]} fx→{k_fx_md_dbg:.1f}" if k_fx_md_dbg is not None else ""
k_str = f", K_{internal_shape[0]} fx->{k_fx_md_dbg:.1f}" if k_fx_md_dbg is not None else ""
_p(
f"{B} face(s) @ {internal_shape[0]}²; "
f"layer0/layer1 depth median={m_med_l0:.2f}/{m_med_l1:.2f}m{k_str}; "
+6 -6
View File
@@ -25,8 +25,8 @@ Inputs:
extrinsics (EXTRINSICS) — pass-through (not used in the math)
intrinsics (INTRINSICS) — per-face K; needed to build the cos-map.
Accepts normalized K (PanoPack convention,
fx≈0.5) or pixel K (Sharp convention,
fx≈hundreds) — auto-detected and rescaled.
fx~=0.5) or pixel K (Sharp convention,
fx~=hundreds) — auto-detected and rescaled.
Outputs:
image (IMAGE) — per-face depth in planar-Z convention
@@ -55,7 +55,7 @@ class SharpRayToPlanarDepth(io.ComfyNode):
def define_schema(cls):
return io.Schema(
node_id="SharpRayToPlanarDepth",
display_name="SHARP Ray-Distance → Planar Depth",
display_name="SHARP Ray-Distance -> Planar Depth",
category="SHARP",
description=(
"Per-face depth convention converter. Multiplies "
@@ -79,7 +79,7 @@ class SharpRayToPlanarDepth(io.ComfyNode):
io.Custom("INTRINSICS").Input(
"intrinsics",
tooltip="Per-face camera intrinsics. Accepts either "
"normalized K (fx≈0.5 for 90° fov, PanoPack "
"normalized K (fx~=0.5 for 90° fov, PanoPack "
"convention) or pixel K (fx in the hundreds, "
"Sharp convention) — auto-detected. The "
"output intrinsics socket emits pixel-K so "
@@ -188,7 +188,7 @@ class SharpRayToPlanarDepth(io.ComfyNode):
planar = depth_2d_batch * cos_map.unsqueeze(0)
cos_min_log = f"min={float(cos_map.min()):.4f} max={float(cos_map.max()):.4f}"
_p(f"ray→planar: B={B} {H}×{W}, cos_map {cos_min_log}, "
_p(f"ray->planar: B={B} {H}×{W}, cos_map {cos_min_log}, "
f"intrinsics{'_rescaled' if rescaled else ''} fx={fx:.1f} cx={cx:.1f}")
# Restore channel dim if input had one.
@@ -199,4 +199,4 @@ class SharpRayToPlanarDepth(io.ComfyNode):
NODE_CLASS_MAPPINGS = {"SharpRayToPlanarDepth": SharpRayToPlanarDepth}
NODE_DISPLAY_NAME_MAPPINGS = {"SharpRayToPlanarDepth": "SHARP Ray-Distance → Planar Depth"}
NODE_DISPLAY_NAME_MAPPINGS = {"SharpRayToPlanarDepth": "SHARP Ray-Distance -> Planar Depth"}
+1 -1
View File
@@ -72,7 +72,7 @@ class ScaleImageValues(io.ComfyNode):
_p(
f"{B} image(s) {tuple(image.shape[1:])}: "
f"in [{float(img_min.min()):.4f}, {float(img_max.max()):.4f}] "
f"→ out [0, 1]"
f"-> out [0, 1]"
)
return io.NodeOutput(scaled)