"""Pixel-content heuristics for detecting blank or near-uniform image regions. Used by both the layout predictor (drop hallucinated layout blocks over empty space) and the recognition predictor (drop hallucinated text blocks from full-page OCR, decide whether an empty full-page output is a correct blank-page read or a failure). Two signals, combined: * near-white fraction — most pixels have every RGB channel above a threshold * pixel-value standard deviation — the region is essentially one color (catches uniform-color fills that the white check misses) """ from __future__ import annotations import numpy as np from PIL import Image # Per-channel value at/above which a pixel is considered "near-white". # Tolerates the small noise typical of PDF renders at 96 DPI. BLANK_WHITE_THRESHOLD = 245 # Fraction of pixels that must be near-white for a region to count as blank. BLANK_PIXEL_FRACTION = 0.99 # Pixel-value std below which a region is "essentially one color" regardless # of what that color is (catches solid-fill rectangles, dark banners, etc.). UNIFORM_COLOR_STD = 8.0 def near_white_fraction( image: Image.Image, white_threshold: int = BLANK_WHITE_THRESHOLD ) -> float: """Fraction of pixels where every RGB channel ≥ ``white_threshold``.""" arr = np.asarray(image.convert("RGB")) if arr.size == 0: return 0.0 return float(np.all(arr >= white_threshold, axis=-1).mean()) def is_blank_region( image: Image.Image, *, white_threshold: int = BLANK_WHITE_THRESHOLD, blank_pixel_fraction: float = BLANK_PIXEL_FRACTION, uniform_color_std: float = UNIFORM_COLOR_STD, ) -> bool: """True iff the image is essentially blank — either mostly near-white or near-uniform color. Use this on a per-block crop or a whole page. Returns False for empty (0-pixel) crops so callers don't accidentally treat a degenerate bbox as blank. """ arr = np.asarray(image.convert("RGB")) if arr.size == 0: return False if np.all(arr >= white_threshold, axis=-1).mean() > blank_pixel_fraction: return True # Per-channel std — a uniform solid color (e.g., red banner with RGB=(200,50,50)) # has each channel constant across pixels, but mixing channels inflates the # aggregate std. Check each channel independently. per_channel_std = arr.reshape(-1, arr.shape[-1]).std(axis=0) if float(per_channel_std.max()) < uniform_color_std: return True return False