Files
Vase-Matcher/pipeline/window_compare.py
2026-08-06 00:04:50 +05:30

124 lines
4.7 KiB
Python

"""
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