diff --git a/nodes/_vendor/moge_panorama.py b/nodes/_vendor/moge_panorama.py index 6241ae7..b7e6a33 100644 --- a/nodes/_vendor/moge_panorama.py +++ b/nodes/_vendor/moge_panorama.py @@ -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) diff --git a/nodes/_vendor/worldgen/src/panorama_utils.py b/nodes/_vendor/worldgen/src/panorama_utils.py index 663bcc2..9ac0ddb 100644 --- a/nodes/_vendor/worldgen/src/panorama_utils.py +++ b/nodes/_vendor/worldgen/src/panorama_utils.py @@ -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 diff --git a/nodes/image_attrs_to_ply.py b/nodes/image_attrs_to_ply.py index 1813f14..cfcc03c 100644 --- a/nodes/image_attrs_to_ply.py +++ b/nodes/image_attrs_to_ply.py @@ -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" ) diff --git a/nodes/merge_gaussians.py b/nodes/merge_gaussians.py index aed80fb..50dca4d 100644 --- a/nodes/merge_gaussians.py +++ b/nodes/merge_gaussians.py @@ -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), diff --git a/nodes/predict.py b/nodes/predict.py index e9ed0e7..0fda785 100644 --- a/nodes/predict.py +++ b/nodes/predict.py @@ -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 diff --git a/nodes/predict_foreground.py b/nodes/predict_foreground.py index e07493e..d70c499 100644 --- a/nodes/predict_foreground.py +++ b/nodes/predict_foreground.py @@ -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, diff --git a/nodes/predict_gaussian_attrs.py b/nodes/predict_gaussian_attrs.py index bca8252..90d21b4 100644 --- a/nodes/predict_gaussian_attrs.py +++ b/nodes/predict_gaussian_attrs.py @@ -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" diff --git a/nodes/predict_gaussians_from_depth.py b/nodes/predict_gaussians_from_depth.py index 4489ef8..d62aabe 100644 --- a/nodes/predict_gaussians_from_depth.py +++ b/nodes/predict_gaussians_from_depth.py @@ -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 diff --git a/nodes/predict_metric_depth.py b/nodes/predict_metric_depth.py index 65f2a27..63839c9 100644 --- a/nodes/predict_metric_depth.py +++ b/nodes/predict_metric_depth.py @@ -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}; " diff --git a/nodes/ray_to_planar_depth.py b/nodes/ray_to_planar_depth.py index 1e3a462..0a86016 100644 --- a/nodes/ray_to_planar_depth.py +++ b/nodes/ray_to_planar_depth.py @@ -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"} diff --git a/nodes/scale_image_values.py b/nodes/scale_image_values.py index 4b71f0b..c5aa12e 100644 --- a/nodes/scale_image_values.py +++ b/nodes/scale_image_values.py @@ -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)