affaan-m/ECC · error · ValueError

cannot read overlay image

Error message

cannot read overlay image: {image}

What it means

overlay() reads the overlay image with OpenCV (cv2.imread with IMREAD_UNCHANGED) before building the ffmpeg filtergraph; imread returns None for unreadable files, which this code converts into ValueError('cannot read overlay image: ...'). The read also supplies the source image dimensions used to scale the overlay element.

Solutions

  1. Check the path in the message exists and is a readable image file (ls -l / file <path>).
  2. Convert the image to PNG or JPEG (ffmpeg -i in.webp out.png) if the format is unsupported by the installed OpenCV.
  3. Fix the path to be absolute or ensure the script's working directory matches the asset location.
  4. Regenerate the overlay asset if the producing step failed and left no file.
  5. Pre-validate in the caller: assert Path(image).is_file() before calling overlay().

Example fix

// before
assemble.overlay(video, "lgoo.png", ...)  # typo -> ValueError

// after
from pathlib import Path
img = Path("logo.png")
if not img.is_file():
    raise SystemExit(f"overlay asset missing: {img}")
assemble.overlay(video, str(img), ...)
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path
import cv2
img = cv2.imread(str(overlay_path), cv2.IMREAD_UNCHANGED)
if img is None:
    raise SystemExit(f"overlay not readable: {overlay_path}")

Type guard

def is_readable_image(path) -> bool:
    import cv2
    from pathlib import Path
    p = Path(path)
    return p.is_file() and cv2.imread(str(p), cv2.IMREAD_UNCHANGED) is not None

Try / catch

try:
    assemble.overlay(video, logo_path, ...)
except ValueError as exc:
    if str(exc).startswith("cannot read overlay image"):
        log.error("bad overlay asset: %s", exc)
    raise

Prevention

When it happens

Trigger: overlay(...) called with an image path that does not exist, is a directory, has an unsupported/corrupt format (e.g. WEBP with alpha on an old OpenCV), or has unreadable permissions — cv2.imread returns None and the raise fires.

Common situations: Typo in the logo/watermark path; asset generated by a previous step failed silently so file is missing; image is in a format the installed OpenCV build can't decode (missing codec support); relative path resolved from the wrong working directory.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/bd66a2643eaecd05. Report an issue: GitHub.

Appendix: source

Thrown at skills/taste-distillation/scripts/taste/assemble.py:113

    the same mark in the same place every time.

    ``screen`` is the default because plates are premultiplied against black,
    so screen drops their blacks for free and no matte is needed.
    """
    clip, image, dst = Path(clip), Path(image), Path(dst)
    if width is None or height is None:
        from . import frames as _fm
        info = _fm.probe(clip)
        width, height = info.width, info.height

    # Resolve the element's pixel size here rather than in ffmpeg expressions.
    # pad() rejects a negative offset and cannot pad to a size smaller than its
    # input, so an element that lands oversized or off-frame kills the whole
    # filtergraph - which it did on the first attempt.
    import cv2 as _cv2
    _im = _cv2.imread(str(image), _cv2.IMREAD_UNCHANGED)
    if _im is None:
        raise ValueError(f"cannot read overlay image: {image}")
    ih0, iw0 = _im.shape[:2]
    ew = max(2, int(width * max(0.02, min(1.0, scale))))
    eh = max(2, int(ew * ih0 / max(1, iw0)))
    if eh > height:  # fit tall elements to the frame instead of overflowing
        eh = height
        ew = max(2, int(eh * iw0 / max(1, ih0)))
    ew, eh = min(ew, width), min(eh, height)
    if isinstance(position, tuple):
        px = int(width * position[0])
        py = int(height * position[1])
    else:
        anchors = {
            "center": (0.5, 0.5), "top": (0.5, 0.12), "bottom": (0.5, 0.88),
            "left": (0.14, 0.5), "right": (0.86, 0.5),
            "topleft": (0.16, 0.16), "topright": (0.84, 0.16),
            "bottomleft": (0.16, 0.84), "bottomright": (0.84, 0.84),
        }
        ax, ay = anchors.get(position, (0.5, 0.5))

View on GitHub (pinned to 8321021c54)