"""方法I:背景自适应的目标连通域检测 + alpha 轮廓评分。 与 G 的固定暗度阈值不同:对每张图扫描多个局部对比度阈值,收集尺寸合理的 暗色/高对比度连通域;通过候选轮廓的形状、填充率和局部边缘完整度综合评分。 该方法的目标是先得到可解释的物体级候选,而不是在整张照片纹理上做 NCC。 输出:try/out/I/{*-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" / "I" OUT.mkdir(parents=True, exist_ok=True) THRESHOLDS = (10, 15, 20, 25, 30, 40, 50, 60) MIN_SIZE, MAX_SIZE = 25, 180 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 mark_info(path): try: mark = cv2.imread(str(path), cv2.IMREAD_UNCHANGED) if 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) mask = (alpha > 128).astype(np.uint8) cs, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not cs: return None c = max(cs, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(c) return mask, c, max(float(cv2.contourArea(c)), 1.0), (x, y, w, h) except Exception as e: print(f" mark 读取失败: {e}") return None def candidate_mask(gray, threshold): try: blur = cv2.GaussianBlur(gray, (0, 0), 21) delta = np.clip(blur.astype(np.int16) - gray.astype(np.int16), 0, 255).astype(np.uint8) m = (delta > threshold).astype(np.uint8) * 255 m = cv2.morphologyEx(m, cv2.MORPH_CLOSE, np.ones((7, 7), np.uint8), iterations=2) return cv2.morphologyEx(m, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8)) except cv2.error: return None def overlap(a, b): try: x1, y1, w1, h1 = a x2, y2, w2, h2 = b ix = max(0, min(x1 + w1, x2 + w2) - max(x1, x2)) iy = max(0, min(y1 + h1, y2 + h2) - max(y1, y2)) inter = ix * iy union = w1 * h1 + w2 * h2 - inter return inter / union if union else 0.0 except Exception: return 0.0 def score_contour(mark_c, mark_area, contour, gray): try: x, y, w, h = cv2.boundingRect(contour) area = float(cv2.contourArea(contour)) shape_score = float(cv2.matchShapes(mark_c, contour, cv2.CONTOURS_MATCH_I1, 0.0)) perimeter = max(float(cv2.arcLength(contour, True)), 1.0) compact = min(1.0, 4.0 * math.pi * area / (perimeter * perimeter)) contrast = float(gray[y:y + h, x:x + w].std()) # 越小越好;外部排序转换为负分 return shape_score, area, compact, contrast, (x, y, w, h) except cv2.error: return None def process(prefix, lines): try: big_path, mark_path = files_for(prefix) if big_path is None: return big = cv2.imread(str(big_path), cv2.IMREAD_COLOR) info = mark_info(mark_path) if big is None or info is None: lines.append(f"{prefix}\tSKIP\t读取失败") return _, mark_c, mark_area, mark_box = info gray = cv2.cvtColor(big, cv2.COLOR_BGR2GRAY) candidates = [] for threshold in THRESHOLDS: mask = candidate_mask(gray, threshold) if mask is None: continue contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for contour in contours: x, y, w, h = cv2.boundingRect(contour) area = cv2.contourArea(contour) if min(w, h) < MIN_SIZE or max(w, h) > MAX_SIZE or area < mark_area * 0.05: continue item = score_contour(mark_c, mark_area, contour, gray) if item is None: continue shape_score, area, compact, contrast, box = item if any(overlap(box, old[5]) > 0.6 for old in candidates): continue candidates.append((shape_score, -compact, -contrast, threshold, area, box)) candidates.sort(key=lambda z: (z[0], z[1], z[2])) candidates = candidates[:TOP_K] if not candidates: lines.append(f"{prefix}\tSKIP\t无候选") return best = candidates[0] shape_score, neg_compact, neg_contrast, threshold, area, (x, y, w, h) = best print(f"{prefix} shape={shape_score:.4f} compact={-neg_compact:.3f} " f"contrast={-neg_contrast:.1f} threshold={threshold} offset=({x},{y}) size=({w},{h})") lines.append(f"{prefix}\tshape={shape_score:.4f}\tcompact={-neg_compact:.3f}\t" f"contrast={-neg_contrast:.1f}\tthreshold={threshold}\toffset=({x},{y})\t" f"size=({w},{h})") vis = big.copy() for i, item in enumerate(candidates): _, _, _, _, _, (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, .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()