Files
杨豪 6902be04d4 feat(src): 交付物收敛为单文件 captcha_solver.py 并新增第三方对接文档
- 合并 matcher_core + solve 为自包含单文件 captcha_solver.py,
  对外仅暴露 solve_slide()(base64 进、JSON 出),内部实现细节不外泄
- 新增 README_3rd.md:接入步骤、返回字段、像素换算、鼠标操作规范、
  轨迹形态要求、踩坑清单与验收基线
- 删除内部 CLI 批量工具与调试字段(candidates/method),
  返回清洗后的 quality/alternatives
- try/live_solve.py 改用新模块路径;docs 同步更新引用
2026-09-16 11:44:35 +08:00

207 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 拖动重叠验证码:最终方案
> 历史实验过程与方法淘汰记录见 [experiments.md](experiments.md)
> 实验原始日志在 `try/findings.md`。
## 任务定义
给定「拖动重叠」验证码的背景大图(552×344)与 mark 小图(110×110 RGBA),
离线求出 mark 应拖动重叠的目标区域偏移。
背景中存在 ≥2 个与 mark 同形的区域(缩放/旋转生成的干扰项),
只有**无缩放、无旋转**(尺度≈1.0、旋转≈0°)的实例是目标。
## 核心发现(决定技术路线)
1. **mark 的 RGB 是"捐赠补丁",不是洞内内容。**
决定性实验:真掩码 NCC 全图任意位置最高仅 0.56241ac/ 0.464444d)。
若"洞 = 该处照片变暗、mark = 洞下原图"成立,真目标处 NCC 应接近 1.0
(NCC 对仿射亮度变换不变)。因此**一切基于 RGB/灰度的模板匹配在原理上不可行**。
2. **洞是叠加在照片上的实心暗色几何覆盖物**,边界 = mark 的 alpha 轮廓。
可靠信号只有三个:**alpha 轮廓形状、尺度≈1.0、旋转≈0°**。
3. **照片纹理会产生大量虚假贴合**:纯倒角距离会被草地等密纹理区域击败。
必须叠加方向约束与洞内部特征。
## 最终算法(`src/captcha_solver.py` 内部实现)
```text
粗扫:尺度 0.85–1.15(步长 0.025)× 全图位置
评分 = 方向感知倒角DT + 20×内部边缘密度 + 0.5×max(0, 内-环亮度差)
全图 NMS 取 top-5 候选
每候选局部旋转扫描:36 角(步长 10°)× ±3px 平移
→ rot*(近并列 ±0.15 内取离 0 最近者,兼容星形等旋转对称形状)
→ rot=0 拟合分 refined
选择:|rot*|≤12° 且 0.96≤s≤1.05 的候选中 refined 最低者
否则取 |rot*| 最小并标 AMBIGUOUS
```
三个评分项的物理含义:
| 项 | 含义 | 区分度(实测) |
| --- | --- | --- |
| 方向感知倒角DT | 边界点只匹配同法向桶(9 桶×20°)的 Canny 边缘 | 真洞 3–6,天空截断 8+ |
| 内部边缘密度 | 洞内部实心平坦(≈0),纹理区内部边缘多 | 真洞 ≤0.1,草地 0.36 |
| 内-环亮度差 | 洞比周围暗(半透明暗色覆盖) | 真洞 -40~-104,误检区 +6 |
Canny 阈值固定 `lo = max(40, 0.5×中位灰度)``hi = 3×lo`
阈值过低会让 JPEG 噪声在平坦区产生大量假边缘,污染"内部平坦"特征
(曾导致 4db9 星形样本漏检)。
## 判别力证据
所有样本的候选旋转曲线均能区分目标/干扰:
- 目标:`rot*≈0°` 且 rot=0 拟合分明显低于其他角度;
- 干扰项:`rot*≠0°`(如 41ac 左月牙 -10°、8727 竞争星 +20°、f4fc 竞品 -20°)。
三个独立真值锚点全部命中:
| 样本 | 独立真值来源 | 真值 | 本方案 | 偏差 |
| --- | --- | --- | --- | --- |
| 4db94793 | SIFT + RANSAC | (295.8, 142.1) | (297, 143) | ~2px |
| d7a7f80c | SIFT + RANSAC | (167.8, 146.4) | (171, 146) | ~4px |
| 444d0ea7 | 闭合轮廓分析(Hu≈0.003 | 左洞 rot≈0° | (183, 160) rot=0° | 一致 |
## API`src/captcha_solver.py`
```python
from captcha_solver import solve_slide
result = solve(big_image_b64, mark_image_b64) # 支持 dataURI 前缀
```
命令行:`python src/captcha_solver.py <大图路径或base64> <mark路径或base64>`
返回:
```json
{
"ok": true,
"distance_px": 297,
"x": 297, "y": 143,
"scale": 1.0, "rot": 0.0,
"score": 2.429,
"confidence": "OK",
"candidates": [...]
}
```
- `distance_px`mark 110×110 画布左上角应到达的 x 偏移;
若拖动 UI 的 mark 初始位置不在 x=0,用 `x` 减去初始 x。
- `confidence``OK`(满足 rot/尺度门限)或 `AMBIGUOUS`(降级选择,建议人工复核)。
- mark 图必须含 alpha 通道(PNG),否则返回 `ok=false`
## 结果验证方法
1. **叠加轮廓检查(最直接)**`python src/verify_result.py` 生成
`src/out/L/verify/`,把 alpha 轮廓按 offset 以绿色半透明+黄色描边叠回大图。
轮廓与暗色图形边缘完全重合 = 正确。
2. **候选对比**:看 `src/out/L/<前缀>-match.png`,红框目标应同形同大同向,
其余框应为旋转/缩放干扰项或被淘汰纹理。
3. **数值检查**`summary.txt` 中目标 rot≈0、scale∈[0.96,1.05]、精修分最低。
## 10 样本最终结果
| 样本 | offset | 尺度 | rot* | 精修分 | 备注 |
| --- | --- | --- | --- | --- | --- |
| 2a313202f58c | (370,114) | 1.025 | 0° | 4.04 | |
| 41ac8781f7e9 | (398,151) | 1.000 | 0° | 4.40 | 右月牙;bbox 88×106 与 alpha 完全一致 |
| 444d0ea7c8bb | (178,148) | 1.000 | 0° | 4.50 | 轮廓分析交叉验证 ✓ |
| 4db94793ec6d | (297,143) | 1.000 | 0° | 2.43 | SIFT 真值 ✓ |
| 632f84299d75 | (113,136) | 1.025 | 0° | 6.82 | 拟合最弱,置信度中 |
| 6a53f4529063 | (166,152) | 1.025 | 0° | 4.97 | |
| 87276de26640 | (153,134) | 1.025 | 0° | 3.94 | 星形,靠旋转扫描解决 |
| d6cd0ebb7eed | (200,55) | 1.000 | 0° | 5.82 | |
| d7a7f80c4554 | (171,146) | 1.025 | 0° | 3.11 | SIFT 真值 ✓ |
| f4fc5aa426ce | (298,68) | 1.050 | 0° | 4.79 | |
## 已知边界
- `632f` 精修分 6.82 明显高于其余样本(2.4–5.8),建议保留人工复核,
或积累更多样本校准"可信分数"阈值;
- 洞叠在深色照片上时"内-环亮度差"约束减弱,可提高方向倒角权重;
- 若验证码改用亮色覆盖物,需要把亮度差项改为双向(|内-环|)并重标权重。
## 线上增强:tip_y 锚定与 flatness 贴片检测(2026-05-15
线上实测发现题目分布漂移:覆盖物从离线样本的暗色洞变为**亮白半透明贴片**,
方法 L 分数从 1–3 恶化到 3.6–7.7,且出现假峰(chamfer 精修分 2.6 的雪枝纹理
压过真缺口 8.3)—— 边界/纹理特征在亮贴片雪景题上完全失灵。
两阶段解决:
### 1. 坐标系修正(bug 修复)
服务端 `question.tip_y×2` 是**画布坐标系**(mark 全图左上)缺口顶 y 真值,
而求解器内部候选 `e.y` 是 alpha-bbox 左上 y,两者相差 `bbox_y0 × scale`
(实测 ≈9px)。不换算会把真候选推出 ±8px 锚窗、错误候选反而入选。
换算关系(由离线 GT 实证):**canvas = bbox bbox_y0×s**。
### 2. flatness 贴片检测(线上新特征,决定性)
亮白半透明贴片的物理特征:**把盖住的背景“贴平”** —— 贴片区域内纹理方差
远低于周围。定义 flatness = 掩码内灰度 std / 环形外灰度 std:
- 线上真缺口 flatness 实测 **0.040.28**live_145400 GT 位置 0.056);
- 同图假峰(雪枝纹理)0.49–0.67,区分度巨大;
- 内部纹理 zNCC、亮度抬升等其它特征在此类题上全部失效(实测)。
求解流程(tip_y 提供时):各尺度在 y 条带内用 filter2D 向量化扫描 flatness
取画布 y 落在锚窗(±8px)内的最低值位置;flatness ≤ 0.30 判 OKmethod
= "flatness")。无匹配或未提供 tip_y → 回退 chamfer 路径(离线逻辑不变)。
**验证结果**
- 线上 batch 8/8 全 OK(此前 3 道 AMBIGUOUS 全修复),dy ≤ 8 全部满足;
- live_145400chamfer 假峰题)flatness 命中 (407,64),与 IoU 亚像素实证
GT (412,68) 差 (5,4)px
- 离线 10 题无 tip_y 走原逻辑,9/10 保持(4db94793 为历史已知边界)。
`solve(big, mark, tip_y=...)` 返回新增 `method` 字段;score 量纲随 method
变化(flatness 为 01 比值,chamfer 为 px 分)。
### 3. flatness 路径最终降级为禁用(2026-05-15 晚,真人 GT 实证)
flatness 在线批量指标全绿,但**位置真值校准暴露系统性偏差**。用真人手拖
(轨迹录制器采样 down→up 位移)拿到 4 个真值:
| 题 | flatness | chamfertip_y 锚定) | 真人 GT | 判定 |
| --- | --- | --- | --- | --- |
| live_165204 | — | 179 | 177 | chamfer ✓(真人 PASS |
| live_172458 | 433 (s=0.70) | 388 | 388 | flatness +45px ✗ |
| live_173321 | 209 (s=0.825) | 391 | 391 | flatness 182px ✗ |
| live_180227 | 266 (s=0.95) | 257 | (用户拖偏,不采信) | — |
结论:**chamfertip_y 锚定 + refined 排序)3/3 命中(dx=0/0/2),
flatness 每次都大幅偏移**。平坦区特征在雪景题上会吸到贴片内部/
天空等大平坦区,且可与同样为假的弱 chamfer 候选互相“掩护”
(交叉验证被穿透,live_173321 实测)。
处理:`FLAT_DISABLED = True` 禁用 flatness 选点(`flatness_scan`
函数保留供参考);主路径 = tip_y 锚定 chamfer。采题侧把
“flatness/chamfer 共识”作为加分项但不作硬依赖。
同时期的输入层排除(详见 experiments.md):CDP 合成指纹、事件指纹、
时序积压均被 xdotool 硬件输入 + 60Hz 样条加密重放排除——
**此前所有失败案例事后看都是位置错,不存在独立的“输入指纹拒”**
### 4. 尺度精修 + 首次机器 PASS2026-09-15 深夜)
SCALES 网格步长 0.025,真尺度落在步间时轮廓对齐可偏 5-15px —— 正是 VerifyErr
(社区资料确认其语义 = 缺口距离错,非轨迹问题)的量级。修法:对入选 OK 候选在
s±0.0375(步长 0.0125)重扫 + ±6px 窗口局部最优,取 rotation_scan refined 最低者。
- GT live_165204179 → 176(真人 177,Δ −1);离线回归 9/10 保持
- **机器 PASS**live_160548 x=255 **s=1.0375**(精修值,网格外),拖 157.1px → code=200
- 剩余 VerifyErr 案例视觉确证为候选整体选错(如 s=0.7 网格下边界假峰),
方向:多候选质量评估 / 不像就 refresh 换题
## 复现
```bash
.venv/bin/python src/verify_result.py # 叠加轮廓验证图
.venv/bin/python src/captcha_solver.py <大图> <mark> # 单样本 API
```