"""Small collection of image helper functions factored out of sfp_module. These are intentionally lightweight wrappers that avoid depending on the SfpConnectorModule instance state; callers should pass necessary parameters (e.g. normalize method) when required. """ from typing import Optional import os import logging try: import numpy as np except Exception: # pragma: no cover - best-effort np = None try: from PIL import Image, ImageOps except Exception: Image = None ImageOps = None # Try to import the local normalize_image if available try: from .image_processing import normalize_image except Exception: normalize_image = None logger = logging.getLogger("VideoReceiverSFP.image_helpers") def looks_like_encoded_image(payload: bytes) -> bool: if not payload: return False if payload.startswith(b"\x89PNG\r\n\x1a\n"): return True if payload.startswith(b"\xff\xd8\xff"): return True if payload.startswith(b"BM"): return True return False def extract_index_array(width: int, height: int, row_stride: int, pixel_data: bytes): if np is None: return None total_bytes = row_stride * height if len(pixel_data) < total_bytes: logger.warning("extract_index_array: pixel_data shorter than expected (got=%d need=%d)", len(pixel_data), total_bytes) return None try: arr = np.frombuffer(pixel_data[:total_bytes], dtype=np.uint8) row_pixels = width if row_stride == row_pixels: arr = arr.reshape((height, width)) else: arr = arr.reshape((height, row_stride))[:, :width] return arr.copy() except Exception: logger.exception("extract_index_array: failed to reshape pixel data") return None def build_grayscale_frame(width, height, row_stride, row_pixels, pixel_data, normalize_method: Optional[str] = None): if Image is None: return None try: if row_stride == row_pixels: img = Image.frombytes("L", (width, height), pixel_data) else: rows = [] for r in range(height): start = r * row_stride rows.append(pixel_data[start:start + row_pixels]) img = Image.frombytes("L", (width, height), b"".join(rows)) normalized_img = None if normalize_method == 'cp' and normalize_image is not None: try: if np is not None: arr = np.frombuffer(img.tobytes(), dtype=np.uint8).reshape((height, width)) norm = normalize_image(arr, target_type=np.uint8) if norm is not None: normalized_img = Image.fromarray(norm, mode='L') except Exception: logger.exception("build_grayscale_frame: controlpanel normalize_image failed") elif normalize_method == 'autocontrast': try: if ImageOps is not None: normalized_img = ImageOps.autocontrast(img) except Exception: normalized_img = None if normalized_img is not None: frame = normalized_img.convert("RGB") try: dumps_dir = os.path.join(os.getcwd(), "dumps") os.makedirs(dumps_dir, exist_ok=True) dbg_path = os.path.join(dumps_dir, "VideoReceiverSFP_last_normalized.png") frame.save(dbg_path) logger.info("build_grayscale_frame: saved normalized debug frame to %s", dbg_path) except Exception: pass return frame return img.convert("RGB") except Exception: logger.exception("build_grayscale_frame: failed to build grayscale frame") return None