"""方法J:低饱和度暗色几何对象检测 + alpha 轮廓匹配。 样本中的 mark 不是用于 RGB 像素复制,而是一个透明几何图形;背景中对应 对象通常是低饱和度、较暗的半透明图形。先用多组局部暗度/饱和度阈值提取 物体级连通区域,再用 mark 的 alpha 外轮廓进行形状筛选。 """ 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" / "J" OUT.mkdir(parents=True, exist_ok=True) DARK_THRESHOLDS = (8, 12, 16, 20, 25, 30, 40, 50) SAT_THRESHOLDS = (50, 70, 90, 110, 140) TOP_K = 5 MIN_SIDE, MAX_SIDE = 25, 220 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_data(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) * 255 contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) if not contours: return None contour = max(contours, key=cv2.contourArea) x, y, w, h = cv2.boundingRect(contour) area = max(float(cv2.contourArea(contour)), 1.0) return contour, area, (x, y, w, h) except Exception as e: print(f" mark 读取失败: {e}") return None def segmentation_masks(bgr): gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) hsv = cv2.cvtColor(bgr, cv2.COLOR_BGR2HSV) local = cv2.GaussianBlur(gray, (0, 0), 21) dark_delta = np.clip(local.astype(np.int16) - gray.astype(np.int16), 0, 255).astype(np.uint8) for dt in DARK_THRESHOLDS: for st in SAT_THRESHOLDS: mask = ((dark_delta >= dt) & (hsv[..., 1] <= st)).astype(np.uint8) * 255 mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, np.ones((5, 5), np.uint8), iterations=2) yield mask, dt, st def contour_metrics(mark_contour, mark_area, contour, gray): try: x, y, w, h = cv2.boundingRect(contour) if min(w, h) < MIN_SIDE or max(w, h) > MAX_SIDE: return None area = float(cv2.contourArea(contour)) if area < mark_area * 0.04: return None shape = float(cv2.matchShapes(mark_contour, 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**2) hull = cv2.convexHull(contour) hull_area = max(float(cv2.contourArea(hull)), 1.0) solidity = min(1.0, area / hull_area) aspect = min(w, h) / max(w, h) # 几何图形一般轮廓完整、实心度较高;照片纹理多为细长碎片。 if compact < 0.08 or solidity < 0.25: return None inside = gray[y:y + h, x:x + w] contrast = float(inside.std()) if inside.size else 0.0 return shape, -compact, -solidity, -aspect, -contrast, (x, y, w, h) except cv2.error: return None def overlap(box1, box2): try: x1, y1, w1, h1 = box1 x2, y2, w2, h2 = box2 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 process(prefix, lines): try: big_path, mark_path = files_for(prefix) if big_path is None: return bgr = cv2.imread(str(big_path), cv2.IMREAD_COLOR) md = mark_data(mark_path) if bgr is None or md is None: lines.append(f"{prefix}\tSKIP\t读取失败") return mark_contour, mark_area, _ = md gray = cv2.cvtColor(bgr, cv2.COLOR_BGR2GRAY) candidates = [] for mask, dt, st in segmentation_masks(bgr): contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE) for contour in contours: metric = contour_metrics(mark_contour, mark_area, contour, gray) if metric is None: continue shape, neg_compact, neg_solidity, neg_aspect, neg_contrast, box = metric if any(overlap(box, old[6]) > 0.6 for old in candidates): continue candidates.append((shape, neg_compact, neg_solidity, neg_aspect, neg_contrast, (dt, st), box)) candidates.sort(key=lambda c: (c[0], c[1], c[2], c[3])) candidates = candidates[:TOP_K] if not candidates: lines.append(f"{prefix}\tSKIP\t无候选") return best = candidates[0] shape, neg_compact, neg_solidity, neg_aspect, neg_contrast, ts, box = best x, y, w, h = box print(f"{prefix} shape={shape:.4f} compact={-neg_compact:.3f} solidity={-neg_solidity:.3f} " f"aspect={-neg_aspect:.3f} offset=({x},{y}) size=({w},{h}) threshold={ts}") lines.append(f"{prefix}\tshape={shape:.4f}\tcompact={-neg_compact:.3f}\t" f"solidity={-neg_solidity:.3f}\taspect={-neg_aspect:.3f}\t" f"offset=({x},{y})\tsize=({w},{h})\tthreshold={ts}") vis = bgr.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()