""" Sliding-window color-space comparison of the "image minus vase" region (whole subject photo, background already removed upstream, with just the SAM3 "vase" mask subtracted out -- see pipeline/sam3_client.py) between an upload and its matched template. Both crops are resized (stretched, not aspect-preserving -- only color is being compared here, never shape) to the same fixed canonical size, so a window at index i means the same relative horizontal position in both images regardless of how differently sized or cropped the two original photos were. Purely informational, opt-in, its own section -- never feeds into any score. """ import cv2 import numpy as np import config from pipeline import color as color_module def canonical_masked_crop(bgr, keep_mask_bool): """Crops to the given keep-mask's own bounding box (tight, no padding), blacks out pixels outside it, then resizes (stretched) to the fixed canonical size shared by both sides of the comparison. Returns (canonical_bgr, canonical_mask_bool), or (None, None) if the mask is empty.""" if keep_mask_bool is None or not keep_mask_bool.any(): return None, None ys, xs = np.nonzero(keep_mask_bool) y0, y1, x0, x1 = ys.min(), ys.max() + 1, xs.min(), xs.max() + 1 crop = bgr[y0:y1, x0:x1].copy() mask_crop = keep_mask_bool[y0:y1, x0:x1] crop[~mask_crop] = 0 w, h = config.WINDOW_COMPARE_CANONICAL_WIDTH, config.WINDOW_COMPARE_CANONICAL_HEIGHT canonical_bgr = cv2.resize(crop, (w, h), interpolation=cv2.INTER_AREA) canonical_mask = cv2.resize(mask_crop.astype(np.uint8), (w, h), interpolation=cv2.INTER_NEAREST) > 0 return canonical_bgr, canonical_mask def slide_and_compare(bgr_a, mask_a, bgr_b, mask_b, window_width): """Slides a window of window_width (clamped to the configured min/max) left-to-right across both canonical (already same-size) images, full height per window, and reports a color-space match % per window -- same HS-histogram-intersection formula as pipeline/color.py's own Color-space section -- plus an overall summary. Windows where either side has too little real (non-background) content are reported with match_pct=None rather than a number computed mostly from noise.""" width = max(config.WINDOW_COMPARE_MIN_WIDTH, min(config.WINDOW_COMPARE_MAX_WIDTH, window_width)) canvas_w = config.WINDOW_COMPARE_CANONICAL_WIDTH canvas_h = config.WINDOW_COMPARE_CANONICAL_HEIGHT windows = [] x = 0 idx = 0 while x < canvas_w: x_end = min(canvas_w, x + width) slice_a_mask = mask_a[:, x:x_end] slice_b_mask = mask_b[:, x:x_end] window_area = (x_end - x) * canvas_h frac_a = slice_a_mask.sum() / window_area if window_area else 0 frac_b = slice_b_mask.sum() / window_area if window_area else 0 if (frac_a < config.WINDOW_COMPARE_MIN_FOREGROUND_FRAC or frac_b < config.WINDOW_COMPARE_MIN_FOREGROUND_FRAC): match_pct = None else: slice_a_bgr = bgr_a[:, x:x_end] slice_b_bgr = bgr_b[:, x:x_end] hist_a = color_module.compute_hs_histogram( slice_a_bgr, slice_a_mask.astype(np.uint8) * 255) hist_b = color_module.compute_hs_histogram( slice_b_bgr, slice_b_mask.astype(np.uint8) * 255) match_pct = color_module.histogram_match_pct(hist_a, hist_b) windows.append({ "index": idx, "x_start": x, "x_end": x_end, "match_pct": match_pct, }) x += width idx += 1 valid = [w["match_pct"] for w in windows if w["match_pct"] is not None] summary_pct = round(sum(valid) / len(valid), 1) if valid else None return { "window_width": width, "windows": windows, "valid_window_count": len(valid), "total_window_count": len(windows), "summary_pct": summary_pct, } _LINE_COLOR = (210, 200, 180) _GOOD_COLOR = (150, 210, 150) _BAD_COLOR = (120, 120, 230) _NA_COLOR = (160, 160, 160) def render_windows_visual(canonical_bgr, windows): """Draws vertical grid lines at each window boundary plus the per- window percentage (color-coded) on a copy of the canonical image.""" img = canonical_bgr.copy() h = img.shape[0] for w in windows: cv2.line(img, (w["x_start"], 0), (w["x_start"], h), _LINE_COLOR, 1) if w["match_pct"] is None: label, color = "n/a", _NA_COLOR else: label = f"{w['match_pct']}%" color = _GOOD_COLOR if w["match_pct"] >= 60 else _BAD_COLOR cv2.putText(img, label, (w["x_start"] + 4, h - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.42, color, 1, cv2.LINE_AA) return img