"""方法F:轮廓候选匹配。 适用于 mark 的 RGB 颜色与大图目标不同、但 alpha 轮廓保持一致的样本。 流程: 1. 从大图生成边缘并提取外部轮廓; 2. 对 mark alpha 轮廓做缩放/旋转模板; 3. 用轮廓形状相似度 + 面积/边界覆盖率筛选候选。 输出:try/out/F/{*-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" / "F" OUT.mkdir(parents=True, exist_ok=True) SCALES = np.arange(0.35, 1.31, 0.05) ANGLES = np.arange(-180.0, 180.0, 15.0) TOP_K = 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 read_shapes(mark_path, big_path): try: mark = cv2.imread(str(mark_path), cv2.IMREAD_UNCHANGED) big = cv2.imread(str(big_path), cv2.IMREAD_COLOR) if mark is None or big 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) mask = (alpha > 128).astype(np.uint8) * 255 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None mark_contour = max(contours, key=cv2.contourArea) gray = cv2.cvtColor(big, cv2.COLOR_BGR2GRAY) # 目标通常与局部背景有明显亮度差;保留边缘并闭合轮廓。 edges = cv2.Canny(gray, 40, 120) edges = cv2.morphologyEx(edges, cv2.MORPH_CLOSE, np.ones((3, 3), np.uint8), iterations=2) return big, mark_contour, edges except Exception as e: print(f" 读取失败: {e}") return None def transform_contour(contour, scale, angle): try: pts = contour.reshape(-1, 2).astype(np.float32) center = pts.mean(axis=0) pts = (pts - center) * scale theta = math.radians(angle) rot = np.array([[math.cos(theta), -math.sin(theta)], [math.sin(theta), math.cos(theta)]], dtype=np.float32) return (pts @ rot.T).astype(np.float32) except Exception: return None def contour_score(mark_contour, candidate): try: # matchShapes 对平移/尺度基本不敏感,旋转也较稳定;用于形状初筛。 return float(cv2.matchShapes(mark_contour, candidate, cv2.CONTOURS_MATCH_I1, 0.0)) except cv2.error: return 1e9 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 = read_shapes(mark_path, big_path) if loaded is None: lines.append(f"{prefix}\tSKIP\t读取失败") return big, mark_contour, edges = loaded contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) mark_area = max(cv2.contourArea(mark_contour), 1.0) candidates = [] for contour in contours: area = cv2.contourArea(contour) if area < mark_area * 0.08 or area > mark_area * 20: continue score = contour_score(mark_contour, contour) x, y, w, h = cv2.boundingRect(contour) candidates.append((score, area, x, y, w, h)) candidates.sort(key=lambda item: item[0]) kept = [] for item in candidates: _, _, x, y, w, h = item 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.5 * max(w, h) for q in kept): kept.append(item) if len(kept) >= TOP_K: break if not kept: lines.append(f"{prefix}\tSKIP\t无轮廓候选") return best = kept[0] score, area, x, y, w, h = best print(f"{prefix} shape={score:.4f} area={area:.0f} offset=({x},{y}) size=({w},{h}) top={len(kept)}") lines.append(f"{prefix}\tshape={score:.4f}\tarea={area:.0f}\toffset=({x},{y})\t" f"size=({w},{h})\ttop={len(kept)}") vis = big.copy() for i, item in enumerate(kept): _, _, px, py, pw, ph = item color = (0, 0, 255) if i == 0 else (255, 0, 0) cv2.rectangle(vis, (px, py), (px + pw, py + ph), color, 2) cv2.putText(vis, str(i + 1), (px + 2, py + 16), cv2.FONT_HERSHEY_SIMPLEX, 0.55, color, 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()