Files
douyin-captcha/try/method_f_contour.py
杨豪 d15b3c5b43 feat: 拖动重叠验证码离线求解(方向感知倒角+alpha轮廓+旋转扫描)
- src/: 最终交付物(solve.py base64 API、method_l_shape.py 核心算法、verify_result.py 验证工具)
- docs/: 方案文档与实验演进记录
- try/: 历史实验脚本(A~K 方法)
- 10/10 样本求解成功,3 个独立真值锚点偏差 <=4px
2026-09-07 20:13:22 +08:00

141 lines
5.1 KiB
Python

"""方法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()