Comfy-Org/ComfyUI · error · ValueError

Procrustes denominator collapsed (degenerate source).

Error message

Procrustes denominator collapsed (degenerate source).

What it means

Raised by the weighted Procrustes solver in the MediaPipe face-geometry port when the denominator used to compute the optimal scale is effectively zero. The denominator is the weighted sum of squared distances of the source points from their weighted centroid, so it collapses only when all landmark points are identical (or all weights are zero). This mirrors geometry_pipeline.cc::EstimateScale behavior where a degenerate canonical point set cannot define a rigid transform.

Source

Thrown at comfy_extras/mediapipe/face_geometry.py:30

    `target ≈ M @ homogeneous(source)` in the weighted LS sense. fp64 for
    SVD stability. Port of procrustes_solver.cc."""
    sqrt_w = np.sqrt(weights.astype(np.float64))
    w_total = float((sqrt_w ** 2).sum())
    ws = src.astype(np.float64) * sqrt_w
    wt = tgt.astype(np.float64) * sqrt_w

    c_w = (ws @ sqrt_w) / w_total
    centered = ws - np.outer(c_w, sqrt_w)
    U, _S, Vt = np.linalg.svd(wt @ centered.T, full_matrices=True)
    # Disallow reflection: flip the least-significant axis when det(U)·det(V)<0.
    post, pre = U.copy(), Vt.T.copy()
    if np.linalg.det(post) * np.linalg.det(pre) < 0:
        post[:, 2] *= -1.0
    R = post @ pre.T

    denom = float((centered * ws).sum())
    if denom < 1e-12:
        raise ValueError("Procrustes denominator collapsed (degenerate source).")
    scale = float((R @ centered * wt).sum()) / denom
    translation = ((wt - scale * (R @ ws)) @ sqrt_w) / w_total

    M = np.eye(4, dtype=np.float64)
    M[:3, :3] = scale * R
    M[:3, 3] = translation
    return M


def _estimate_scale(canonical: np.ndarray, runtime: np.ndarray, weights: np.ndarray) -> float:
    """scale = ‖first column of M[:3]‖ per geometry_pipeline.cc::EstimateScale."""
    return float(np.linalg.norm(_solve_weighted_orthogonal_problem(canonical, runtime, weights)[:3, 0]))


def solve_facial_transformation_matrix(
    landmarks_normalized: np.ndarray,
    canonical_vertices: np.ndarray,
    procrustes_indices: np.ndarray,

View on GitHub (pinned to 1c6d8d45b3)

Solutions

  1. Inspect the landmark tensor fed into the solver; if all points are identical, the face detector produced no real landmarks — feed frames where a face is actually detected
  2. If weights are user-supplied, verify they are positive and normalized instead of all zeros
  3. Add an upstream validity check (e.g. landmark variance > epsilon) before calling the Procrustes solve, and skip/interpolate the frame instead of crashing
  4. If a constant template is intentional (initialization), perturb it or bypass the solver for that frame

Example fix

// before
M = _solve_weighted_orthogonal_problem(canonical, runtime, weights)

// after
if np.linalg.norm(canonical - canonical.mean(axis=0)) < 1e-9:
    raise SkipFrame("degenerate landmarks")
M = _solve_weighted_orthogonal_problem(canonical, runtime, weights)
Defensive patterns

Strategy: validation

Validate before calling

import numpy as np

def landmarks_solvable(pts: np.ndarray, weights: np.ndarray, eps: float = 1e-9) -> bool:
    if pts.ndim != 2 or pts.shape[1] != 3 or pts.shape[0] < 3:
        return False
    centered = pts - pts.mean(axis=0)
    if np.linalg.norm(centered) < eps:
        return False  # all points identical -> denominator collapses
    if weights.sum() < eps or (weights < 0).any():
        return False
    return True

Try / catch

try:
    M = _solve_weighted_orthogonal_problem(src, dst, w)
except ValueError as e:
    if 'denominator collapsed' in str(e):
        use_previous_frame_transform(M_prev)  # or skip frame
    else:
        raise

Prevention

When it happens

Trigger: Calling _estimate_scale/_solve_weighted_procrustes with a canonical landmark array where every 3D point is the same coordinate, or with an all-zero weight vector (w_total ~ 0 makes centered ~ 0). Happens when a face-landmark model outputs constant/garbage landmarks for a frame with no detectable face.

Common situations: Running the face geometry pipeline on blank frames, fully occluded faces, or a mis-loaded landmark model that emits a constant template. Also when upstream code passes an uninitialized (zeros) landmark tensor or zero weights into the solver.


AI-assisted analysis of Comfy-Org/ComfyUI@1c6d8d45b3 (2026-08-14). Data as JSON: /api/errors/5cdd97597e27aca1. Report an issue: GitHub.