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