"""方法H:受尺度约束的 alpha 轮廓 + 局部对比度匹配。 前面的 C/D/E 实验暴露出两个系统性误差: - 小模板会被背景纹理误选; - mark 的 RGB 颜色不是大图目标的真实颜色。 本方法只使用 mark 的 alpha 轮廓,并把尺度限制在 0.75~1.25(样本中目标 物体与 110px mark 近似同尺寸)。候选分数由两部分组成: 1. alpha 轮廓在大图边缘距离图上的贴合度; 2. alpha 内部与外部环带之间的局部对比度。 输出:try/out/H/{*-match.png,summary.txt} """ from pathlib import Path import math import cv2 import numpy as np ROOT = Path(__file__).resolve().parent.parent CAP = ROOT / "captchas" OUT = ROOT / "try" / "out" / "H" OUT.mkdir(parents=True, exist_ok=True) SCALES = np.arange(0.75, 1.26, 0.025) TOP_K = 5 RING = 5 def files_for(prefix): bigs = [p for p in CAP.glob(f"{prefix}*.jpeg") if "-mark" not in p.stem] if not bigs: return None, None big = bigs[0] return big, CAP / f"{big.stem}-mark.png" def load_data(big_path, mark_path): try: big = cv2.imread(str(big_path), cv2.IMREAD_GRAYSCALE) mark = cv2.imread(str(mark_path), cv2.IMREAD_UNCHANGED) if big is None or mark is None: return None alpha = mark[..., 3] if mark.ndim == 3 and mark.shape[2] == 4 else np.full(mark.shape[:2], 255, np.uint8) shape = (alpha > 128).astype(np.uint8) edge = cv2.morphologyEx(shape * 255, cv2.MORPH_GRADIENT, np.ones((3, 3), np.uint8)) > 0 outer = cv2.dilate(shape, np.ones((RING * 2 + 1, RING * 2 + 1), np.uint8)) > 0 ring = outer & ~shape return big, shape, edge, ring except Exception as e: print(f" 读取失败: {e}") return None def resize_binary(mask, scale): try: h, w = mask.shape size = (max(12, round(w * scale)), max(12, round(h * scale))) return cv2.resize(mask.astype(np.uint8), size, interpolation=cv2.INTER_NEAREST) > 0 except cv2.error: return None def distance_fit(distance, edge): try: pts = edge.astype(np.float32) n = float(pts.sum()) h, w = edge.shape if n < 10 or h >= distance.shape[0] or w >= distance.shape[1]: return None # 每个候选框内,模板边缘点到背景边缘的平均距离 cost = cv2.matchTemplate(distance.astype(np.float32), pts, cv2.TM_CCORR) / n return 1.0 / (1.0 + cost) except cv2.error: return None def contrast_map(gray, shape, ring): try: inside = shape.astype(np.float32) outside = ring.astype(np.float32) ni, no = float(inside.sum()), float(outside.sum()) if ni < 10 or no < 10: return None mean_i = cv2.matchTemplate(gray.astype(np.float32), inside, cv2.TM_CCORR) / ni mean_o = cv2.matchTemplate(gray.astype(np.float32), outside, cv2.TM_CCORR) / no return np.abs(mean_i - mean_o) except cv2.error: return None def normalize_map(values): try: lo, hi = np.percentile(values, (10, 99)) if hi <= lo: return np.zeros_like(values, dtype=np.float32) return np.clip((values - lo) / (hi - lo), 0.0, 1.0).astype(np.float32) except Exception: return np.zeros_like(values, dtype=np.float32) def peaks(score_map, w, h): result = [] try: r = score_map.copy() for _ in range(TOP_K): _, score, _, loc = cv2.minMaxLoc(r) if score <= 0: break result.append((float(score), int(loc[0]), int(loc[1]))) x0 = max(0, loc[0] - int(w * 0.65)) y0 = max(0, loc[1] - int(h * 0.65)) x1 = min(r.shape[1], loc[0] + int(w * 0.65)) y1 = min(r.shape[0], loc[1] + int(h * 0.65)) r[y0:y1, x0:x1] = 0 except Exception as e: print(f" 峰提取失败: {e}") return result def process(prefix, lines): try: big_path, mark_path = files_for(prefix) if big_path is None: lines.append(f"{prefix}\tSKIP\t文件缺失") return loaded = load_data(big_path, mark_path) if loaded is None: lines.append(f"{prefix}\tSKIP\t读取失败") return big, shape0, edge0, ring0 = loaded bg_edges = cv2.Canny(big, 80, 180) distance = cv2.distanceTransform((255 - bg_edges).astype(np.uint8), cv2.DIST_L2, 3) candidates = [] for scale in SCALES: shape = resize_binary(shape0, float(scale)) edge = resize_binary(edge0, float(scale)) ring = resize_binary(ring0, float(scale)) if shape is None or edge is None or ring is None: continue fit = distance_fit(distance, edge) contrast = contrast_map(big, shape, ring) if fit is None or contrast is None or fit.shape != contrast.shape: continue score_map = 0.55 * normalize_map(fit) + 0.45 * normalize_map(contrast) for score, x, y in peaks(score_map, shape.shape[1], shape.shape[0]): candidates.append((score, float(scale), x, y, shape.shape[1], shape.shape[0], float(fit[y, x]), float(contrast[y, x]))) if not candidates: lines.append(f"{prefix}\tSKIP\t无候选") return candidates.sort(key=lambda c: -c[0]) kept = [] for c in candidates: _, _, x, y, w, h, _, _ = c cx, cy = x + w / 2.0, y + h / 2.0 if all(math.hypot(cx - (q[2] + q[4] / 2), cy - (q[3] + q[5] / 2)) > 0.6 * max(w, h) for q in kept): kept.append(c) if len(kept) >= TOP_K: break best = kept[0] score, scale, x, y, w, h, fit_score, contrast_score = best print(f"{prefix} score={score:.3f} scale={scale:.3f} offset=({x},{y}) " f"edgeFit={fit_score:.2f} contrast={contrast_score:.2f}") lines.append(f"{prefix}\tscore={score:.3f}\tscale={scale:.3f}\toffset=({x},{y})\t" f"edgeFit={fit_score:.2f}\tcontrast={contrast_score:.2f}") vis = cv2.cvtColor(big, cv2.COLOR_GRAY2BGR) colors = [(0, 0, 255), (0, 165, 255), (255, 0, 0), (0, 255, 0), (255, 0, 255)] for i, item in enumerate(kept): _, _, px, py, pw, ph, _, _ = item cv2.rectangle(vis, (px, py), (px + pw, py + ph), colors[i], 2) cv2.putText(vis, str(i + 1), (px + 2, py + 16), cv2.FONT_HERSHEY_SIMPLEX, 0.55, colors[i], 2) cv2.imwrite(str(OUT / f"{prefix}-match.png"), vis) except Exception as e: print(f"{prefix} 处理失败: {e}") lines.append(f"{prefix}\tERROR\t{e}") def main(): lines = [] prefixes = sorted({p.name.split("~", 1)[0] for p in CAP.glob("*.jpeg") if "-mark" not in p.stem}) for prefix in prefixes: process(prefix, lines) (OUT / "summary.txt").write_text("\n".join(lines) + "\n", encoding="utf-8") print(f"\n结果已写入 {OUT}/") if __name__ == "__main__": main()