- src/: 最终交付物(solve.py base64 API、method_l_shape.py 核心算法、verify_result.py 验证工具) - docs/: 方案文档与实验演进记录 - try/: 历史实验脚本(A~K 方法) - 10/10 样本求解成功,3 个独立真值锚点偏差 <=4px
141 lines
5.1 KiB
Python
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()
|