{"record":{"id":"806a3a190b51655a","repo":"affaan-m/ECC","slug":"expected-n-3-pixels-got-pixels-shape-grade","errorCode":null,"errorMessage":"expected (N, 3) pixels, got {pixels.shape}","messagePattern":"expected \\(N, 3\\) pixels, got (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"skills/taste-distillation/scripts/taste/grade.py","lineNumber":615,"sourceCode":"\ndef load_stats(path: str | Path) -> GradeStats:\n    return GradeStats.from_dict(json.loads(Path(path).read_text(encoding=\"utf-8\")))\n\n\ndef analyze_pixels(\n    pixels: np.ndarray,\n    noise_frames: list[np.ndarray] | None = None,\n    palette_pixels: np.ndarray | None = None,\n) -> GradeStats:\n    \"\"\"Same statistics as :func:`analyze`, but from a flat (N, 3) pixel array.\n\n    This is the masked path: callers pool only the pixels that survived\n    content masking, across references of differing frame sizes, and pass\n    them here. Grain still needs 2-D neighbourhoods, so ``noise_frames``\n    carries a handful of cropped frames purely for that estimate.\n    \"\"\"\n    if pixels.ndim != 2 or pixels.shape[1] != 3:\n        raise ValueError(f\"expected (N, 3) pixels, got {pixels.shape}\")\n\n    lab = _to_lab(np.ascontiguousarray(pixels.reshape(1, -1, 3), np.float32)).reshape(-1, 3)\n    L, a, b = lab[:, 0], lab[:, 1], lab[:, 2]\n    chroma = np.sqrt(a.astype(np.float64) ** 2 + b.astype(np.float64) ** 2)\n\n    pal_src = palette_pixels if palette_pixels is not None else pixels\n    pal = _palette([pal_src.reshape(1, -1, 3)])\n\n    return GradeStats(\n        zones=_zone_stats(L, a, b),\n        lab_mean=[float(L.mean()), float(a.mean()), float(b.mean())],\n        lab_std=[float(L.std()), float(a.std()), float(b.std())],\n        l_cdf=[float(v) for v in _cdf_of_l(L)],\n        black_point=float(np.percentile(L, 1)),\n        white_point=float(np.percentile(L, 99)),\n        contrast=float(L.std()),\n        saturation=float(chroma.mean()),\n        warmth=float(b.mean()),","sourceCodeStart":597,"sourceCodeEnd":633,"githubUrl":"https://github.com/affaan-m/ECC/blob/8321021c54d670126ce3b2969d5deb880b4b0c2a/skills/taste-distillation/scripts/taste/grade.py#L597-L633","documentation":"analyze_pixels() is the masked-pixel variant of the analysis and expects a 2-D (N, 3) array of pooled RGB pixels. It validates the shape up front because grain estimation, Lab conversion, and reshaping all assume exactly that layout; anything else (1-D, (N,4) RGBA, (H,W,3)) is rejected.","triggerScenarios":"Passing an RGBA frame's pixels (4 channels), a single 1-D pixel array, a (H, W, 3) image instead of flattened pixels, or an empty (0, 3) array produced by a mask that kept no pixels.","commonSituations":"Forgetting to slice [:, :3] off RGBA arrays; calling analyze_pixels with a raw frame instead of pooled pixel matrix; over-aggressive content masks leaving zero surviving pixels; float/uint8 confusion doesn't trigger this but often accompanies it.","solutions":["Reshape/flatten your pixels to (N, 3) before calling: pixels.reshape(-1, 3)","Strip the alpha channel: arr[..., :3] for RGBA inputs","Check the mask kept pixels: if pixels.shape[0] == 0, widen the mask or skip the clip","Verify you are not passing a full frame; use analyze() for frame lists instead"],"exampleFix":"// before\nstats = analyze_pixels(rgba_frame, ...)  # (H, W, 4)\n// after\npixels = rgba_frame[..., :3].reshape(-1, 3).astype(np.float32)\nstats = analyze_pixels(pixels, ...)","handlingStrategy":"type-guard","validationCode":"import numpy as np\nif not (isinstance(pixels, np.ndarray) and pixels.ndim == 2 and pixels.shape[1] == 3):\n    raise TypeError(\"analyze_pixels expects (N, 3) float array\")","typeGuard":"def is_pixel_matrix(x) -> bool:\n    return isinstance(x, np.ndarray) and x.ndim == 2 and x.shape[1] == 3 and x.shape[0] > 0","tryCatchPattern":"try:\n    stats = analyze_pixels(pixels, ...)\nexcept ValueError as e:\n    if \"expected (N, 3) pixels\" in str(e):\n        pixels = np.asarray(pixels)[..., :3].reshape(-1, 3)\n        stats = analyze_pixels(pixels, ...)\n    else: raise","preventionTips":["Always flatten frames with arr[..., :3].reshape(-1, 3) before pooling","Strip alpha channels before pixel pooling","Check the mask kept >0 pixels before calling","Use analyze() for frame lists and analyze_pixels() only for pooled (N,3) matrices"],"tags":["numpy","shape-validation","video"],"backgroundTag":"tensor-shape-mismatch","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}