{"record":{"id":"5cdd97597e27aca1","repo":"Comfy-Org/ComfyUI","slug":"procrustes-denominator-collapsed-degenerate-sourc","errorCode":null,"errorMessage":"Procrustes denominator collapsed (degenerate source).","messagePattern":"Procrustes denominator collapsed \\(degenerate source\\)\\.","errorType":"exception","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"comfy_extras/mediapipe/face_geometry.py","lineNumber":30,"sourceCode":"    `target ≈ M @ homogeneous(source)` in the weighted LS sense. fp64 for\n    SVD stability. Port of procrustes_solver.cc.\"\"\"\n    sqrt_w = np.sqrt(weights.astype(np.float64))\n    w_total = float((sqrt_w ** 2).sum())\n    ws = src.astype(np.float64) * sqrt_w\n    wt = tgt.astype(np.float64) * sqrt_w\n\n    c_w = (ws @ sqrt_w) / w_total\n    centered = ws - np.outer(c_w, sqrt_w)\n    U, _S, Vt = np.linalg.svd(wt @ centered.T, full_matrices=True)\n    # Disallow reflection: flip the least-significant axis when det(U)·det(V)<0.\n    post, pre = U.copy(), Vt.T.copy()\n    if np.linalg.det(post) * np.linalg.det(pre) < 0:\n        post[:, 2] *= -1.0\n    R = post @ pre.T\n\n    denom = float((centered * ws).sum())\n    if denom < 1e-12:\n        raise ValueError(\"Procrustes denominator collapsed (degenerate source).\")\n    scale = float((R @ centered * wt).sum()) / denom\n    translation = ((wt - scale * (R @ ws)) @ sqrt_w) / w_total\n\n    M = np.eye(4, dtype=np.float64)\n    M[:3, :3] = scale * R\n    M[:3, 3] = translation\n    return M\n\n\ndef _estimate_scale(canonical: np.ndarray, runtime: np.ndarray, weights: np.ndarray) -> float:\n    \"\"\"scale = ‖first column of M[:3]‖ per geometry_pipeline.cc::EstimateScale.\"\"\"\n    return float(np.linalg.norm(_solve_weighted_orthogonal_problem(canonical, runtime, weights)[:3, 0]))\n\n\ndef solve_facial_transformation_matrix(\n    landmarks_normalized: np.ndarray,\n    canonical_vertices: np.ndarray,\n    procrustes_indices: np.ndarray,","sourceCodeStart":12,"sourceCodeEnd":48,"githubUrl":"https://github.com/Comfy-Org/ComfyUI/blob/1c6d8d45b3693bfbb32385b410d813a7fd6be216/comfy_extras/mediapipe/face_geometry.py#L12-L48","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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","If weights are user-supplied, verify they are positive and normalized instead of all zeros","Add an upstream validity check (e.g. landmark variance > epsilon) before calling the Procrustes solve, and skip/interpolate the frame instead of crashing","If a constant template is intentional (initialization), perturb it or bypass the solver for that frame"],"exampleFix":"// before\nM = _solve_weighted_orthogonal_problem(canonical, runtime, weights)\n\n// after\nif np.linalg.norm(canonical - canonical.mean(axis=0)) < 1e-9:\n    raise SkipFrame(\"degenerate landmarks\")\nM = _solve_weighted_orthogonal_problem(canonical, runtime, weights)","handlingStrategy":"validation","validationCode":"import numpy as np\n\ndef landmarks_solvable(pts: np.ndarray, weights: np.ndarray, eps: float = 1e-9) -> bool:\n    if pts.ndim != 2 or pts.shape[1] != 3 or pts.shape[0] < 3:\n        return False\n    centered = pts - pts.mean(axis=0)\n    if np.linalg.norm(centered) < eps:\n        return False  # all points identical -> denominator collapses\n    if weights.sum() < eps or (weights < 0).any():\n        return False\n    return True","typeGuard":null,"tryCatchPattern":"try:\n    M = _solve_weighted_orthogonal_problem(src, dst, w)\nexcept ValueError as e:\n    if 'denominator collapsed' in str(e):\n        use_previous_frame_transform(M_prev)  # or skip frame\n    else:\n        raise","preventionTips":["Validate landmark spread (variance) per frame before the geometry solve","Skip or interpolate frames where the face detector reports low confidence","Keep weights strictly positive and normalized"],"tags":["numpy","procrustes","face-landmarks","degenerate-input"],"backgroundTag":null,"analyzedSha":"1c6d8d45b3693bfbb32385b410d813a7fd6be216","analyzedAt":"2026-08-14T19:37:18.893Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}