124 lines
4.7 KiB
Python
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
|