Files
douyin-captcha/docs/experiments.md
T
杨豪 e9cc8fd2b1 feat: TV 亚像素精修(alpha 反推背景自然性)4/4 标定命中 ≤1px
- 贴片=bg*(1-a)+white*a 物理模型,反推 bg 的 TV 在真位置最低
- 与 chamfer z 分数联合 (λ=1.0),±5px 窗口;信噪保护防无信号题扰动
- 离线 9/10 保持;post_pos 读数只在 PASS 时可信的新认知
2026-09-15 17:34:36 +08:00

362 lines
20 KiB
Markdown
Raw 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.
# 实验演进记录(A → L
> 最终方案与使用说明见 [solution.md](solution.md)。
> 本文记录每个方法的思路、失败原因与沉淀的经验教训,
> 原始实验脚本在 `try/`,输出在 `try/out/<方法字母>/`。
## 方法总览
| 方法 | 脚本 | 思路 | 结论 |
| --- | --- | --- | --- |
| A | method_a_template.py | 蒙版 CCORR 多尺度模板匹配 | 废弃 |
| A2 | method_a2_ccoeff.py | 蒙版 CCOEFF + 峰显著度 + NMS | 基线可用,但锁纹理 |
| B | method_b_sift.py | SIFT + RANSAC 迭代聚类 | 仅 2 样本可用,留作真值锚点 |
| C | method_c_rotation.py | NCC 粗扫 + 旋转响应二次评分 | 第一阶段候选即错 |
| D | method_d_shape.py | Chamfer 轮廓距离 | 小尺度纹理饱和 |
| E | method_e_shape_coverage.py | 轮廓边缘覆盖率 | 同上 |
| F | method_f_contour.py | Canny 轮廓 + matchShapes | 背景轮廓合并成超大块 |
| G | method_g_component.py | 固定暗度阈值连通域 | 阈值敏感 |
| H | method_h_silhouette.py | alpha 轮廓距离 + 局部对比度 | 分数归一化饱和 |
| I | method_i_adaptive_components.py | 自适应暗目标连通域 | 误检照片内容 |
| J | method_j_objects.py | 低饱和度暗色物体分割 | 同上 |
| K | method_k_objectness.py | 暗色覆盖物概率图 + 掩码 CCORR | 掩码 CCORR 边界产生 inf |
| **L** | `src/method_l_shape.py` | **方向感知倒角 + 洞内部特征 + 旋转扫描** | **最终方案,10/10** |
## 逐方法教训
### A / A2:RGB 模板匹配(不减小均值的代价)
- `TM_CCORR_NORMED` 不做均值中心化,亮背景上处处高分;
- 跨尺度直接比原始分数偏向小模板 → A2 改用峰显著度 `(max-median)/std`
- A2 在 8/10 样本上候选位置合理,但 41ac 出现多峰分歧——
后来证明这些 0.7 的高分全是**小尺度纹理巧合**,方向本身是错的。
### BSIFT(意外价值)
- 平面图形缺少纹理,大部分样本匹配不足;
- 但在纹理丰富的 4db9、d7a7 上给出亚像素级真值
scale≈1.00、rot≈0°),成为后续所有方法的**交叉验证锚点**。
### C:旋转感知 NCC(两次评分救不了错误的候选)
- 旋转响应曲线本身有效(真目标 rot≈0),但候选来自 NCC 粗扫,
粗扫错了旋转阶段无法挽回。
### D/E:倒角与覆盖率(尺度公平性问题)
- 覆盖率在小尺度上饱和到 1.0(背景纹理到处有边);
- 倒角均值在密纹理区反而更低 → 必须限制尺度范围并加方向约束。
### F/G/H/I/J/K:分割路线的共同困境
- Canny 轮廓易把地面/山脉合并成超大块(F);
- 固定/自适应暗度阈值对每张照片都不同(G/I/J);
- K 的教训:`TM_CCORR_NORMED` 带 mask 在边界产生 inf 值,
需要手写 filter2D 掩码 NCC(后来用于决定性实验)。
## 转折点实验(值得复用的诊断手段)
1. **掩码 NCC 全图扫描**(手写 filter2D 实现,无 inf 问题):
对 41ac/444d 在尺度 0.91.1 全图扫描,最高分仅 0.56/0.46
→ 证明 mark RGB 与洞内容无像素关系,RGB 路线全部判死。
2. **闭合轮廓 + matchShapes**444d 上):
Canny 闭合轮廓直接命中两个洞,Hu≈0.003,左洞 rot=-0.6°、右洞 -15.5°
→ 证明"轮廓 + 尺度 + 旋转"路线成立,且给出 444d 独立真值。
3. **三特征分离度测量**
洞:内部边缘密度 ≤0.1、亮度差 -40~-104
草地误检:密度 0.36、亮度差 +6 → 直接给出 L 的特征权重依据。
## 调试中踩过的具体坑
- `np.flatnonzero` 对 2D 数组返回一维扁平索引,取坐标应用 `np.argwhere`
- `filter2D` 输出是全尺寸,与手写滑窗比对时要裁剪有效相关区
`[h//2 : h//2+H-h+1, w//2 : w//2+W-w+1]`
- 方向桶不能越界重叠:角度已在 [0,180),9 桶直接线性划分即可,
最后一桶再做 wrap 会导致点被双计(表现为均值超上限);
- `np.arange` 有浮点累积误差,参与区间比较前先 `round(s, 3)`
- Canny 阈值过低(如 26/77)会让 JPEG 噪声淹没"内部平坦"特征。
## 无 ground truth 时的交叉验证方法
1. 找至少一个可用独立方法的样本建立锚点(如 SIFT 能用的纹理丰富样本);
2. 用结构性证据补充锚点:候选 bbox 尺寸与 mark alpha bbox 完全一致
41ac88×106 vs 88×106);
3. 全部候选可视化后人工目检:真目标应落在几何图形上而非纹理上;
4. 结果叠加轮廓回贴大图(`src/verify_result.py`),观察贴合精度。
## 线上闭环实验(2026-05-15CDP 连线)
搭了「hook 采题 → solve → CDP 拖动 → 读 verify」全自动闭环后,暴露出离线
评估覆盖不到的三层问题:
1. **CDP pressure 指纹**:不带 `force` 的鼠标事件 pressure=0(真鼠标恒 0.5),
行为风控 5014 的高危成因。加 `force:0.5` 后错误码从 5014 变为 VerifyErr
(位置层),证明 pressure 是被采集的判别特征(详见
browser-automation/cdp-drag-force-pressure.md)。
2. **错误码分层**5014(行为拒)→ VerifyErr(位置判了但错)→ 5009
(频控,压住一切)→ NotFoundChallengeIdchallenge 过期)。判定任何
改动的效果都必须在频控冷却后单次干净尝试,连环重试的 5014/5009
是无效信号。
3. **线上题分布漂移**:URL 下载的线上题覆盖物是亮白半透明贴片(离线样本
是暗色洞),方法 L 分数从 1-3 恶化到 3.6-7.7,且出现 scale 偏移
0.85-1.075)与 AMBIGUOUS。翻转 dark_pen 的 bright 模式更差(7+),
说明亮贴片信号弱,尚无可靠增强。
交叉验证手段新增:**tip_y 一致性检查**——服务端 `question.tip_y*2` 应等于
solve.y(mark 画布顶 y),两者差得远说明 solve 跑飞,可直接否决该题。
其它工程教训:
- hover 序列里 `buttons``button` 状态矛盾(buttons=0 但 button="left"
会毒化 SDK 拖动注册,verify 根本不发出——按下前保持 button:"none"+buttons:0
拖动中 buttons:1,释放后恢复;
- 每轮点刷新不重建 iframeiframe 内 XHR hook 持续有效,比 netcap 的
Network 域抓包可靠得多;
- 拖动映射已实测校准:按钮 1:1 带动 mark(UI px),mark 初始相对大图
(0, tip_y*2)(原图坐标),拖动距离 = solve.x × 340/552。
### 干净账号 CDP 拖动序列(2026-05-15 下午)
换干净账号 + 随机凭证触发后连续 4 次实验(拖动时长依次 3s→5-10s):
| # | 距上次 | score | solve x | SDK位移 | 结果 |
| --- | --- | --- | --- | --- | --- |
| 1 | 冷启动 | 2.450 | 263 | 202* | **VerifyErr** |
| 2 | +2min | 2.610 | 329 | 202.0/202.6 | **VerifyErr** |
| 3 | +6min | 2.731 | 261 | 160.0/160.8 | 5014 过快 |
| 4 | +6min | 2.368 | 188 | 115.0/115.8 | 5014 过快(拖动已 5-10s |
*第 1 次未量位移。关键读数:
- SDK 实际位移与期望差 ≤0.8px —— CDP→SDK→DOM 映射无损;
- solve 位置经 IoU 亚像素精修确认 ±1px —— **坐标换算无误**
- 第 1-2 拖 VerifyErr(位置判定执行)、第 3-4 拖 5014(行为分累积后
位置判定被跳过)—— 与老账号序列模式一致;
- 5-10s 慢拖不能消除 5014 → 行为分按账号+指纹累积,非单次拖动速度。
附带:真人对照(同环境)——老账号手拖 3 次全败(超时/过快/网络差),
换新账号手拖 1 次即过。**账号被标记后连真人都过不了**。
### flatness 贴片特征发现(2026-05-15 傍晚,线上定位瓶颈突破)
live_145400 拖动失败后的归因链:SDK 位移完美 → IoU 亚像素验证 solve 位置
±1px → 仍 VerifyErr。逐题深挖发现该题 chamfer 精修分 2.655 的最优候选是
**雪枝纹理假峰**IoU 实证真缺口在 (412,68)refined 反而 8.349)——
边界/纹理特征在亮贴片雪景题上彻底失灵,此前所有 VerifyErr 均由此贡献。
依次否证的特征:masked NCC(离线 0/10 命中)、内部纹理 zNCC(真缺口与
假峰都 ≈0,贴片重绘了内部纹理)、亮度抬升 lift(真缺口 lift 为负)。
**有效特征:flatness = 贴片内灰度 std / 环形灰度 std**。白半透明贴片把
盖住的背景"贴平",真缺口 0.040.28,假峰 0.490.67。向量化实现
(filter2D 算窗口均值/方差)后每尺度 ~0.5s。
接入 solve_pairtip_y 提供时优先 flatness,命中判 OK)+ 修复两处坐标系
bugtip_y 锚定 canvas=bboxbbox_y0×sband 中心同向)后回归:
| 集合 | 修复前 | 修复后 |
| --- | --- | --- |
| 离线 10(无 tip_ychamfer 路径) | 9/10 | 9/10(不变) |
| 线上 batch 8tip_y | 7 OK + 1 AMBIGUOUS | **8/8 OK(全 flatness 命中 6 道)** |
| live_145400GT IoU 实证) | 偏 163px | **差 (5,4)px** |
| live_143018(此前跑飞题) | 偏 240px | flatness 命中 (327,187) |
另修复:SCALES 网格下限 0.85→0.70(线上目标尺度可低至 0.775)、拖动时长
5–10s(用户要求,真人观测 4–8s)、随机号触发换号重试(部分号段仅支持
验证码登录,提交被拒后自动换号)。
### 真人 GT 校准与 solve 修复(2026-05-15 傍晚,决定性)
用户配合做了 5 次手拖校准(轨迹录制器实时快照),拿到 3 个位置真值:
| 题 | solve(修复前) | 真人 GT | 偏差 | 用户结果 |
| --- | --- | --- | --- | --- |
| live_165204 | chamfer 179 | 177 | +2 | **PASS** |
| live_172458 | flatness 433 (s=0.7) | 388 | **+45** | 网络差(位置对) |
| live_173321 | flatness 209 (s=0.825) | 391 | **-182** | 网络差(位置对) |
两条铁律浮出水面:
1. **flatness 小尺度匹配(s<0.8)全是假阳性**——吸到天空/贴片内部等大平坦区;
真目标尺度集中在 0.9–1.075;
2. 之前的交叉验证会被**弱 chamfer 候选"掩护"**(假 flatness 恰好靠近一个
同样是假的 s=0.7 chamfer 候选,两者互相印证通过)。
修复(`FLAT_S_MIN=0.8` + 交叉验证只与 refined 前 2 强候选比)后:
**3/3 真人 GT 命中(dx=0/0/2**,离线 9/10 保持。
### 输入层排除记录
- **CDP 合成指纹**:用 xdotool(OS 硬件级鼠标,浏览器无法区分真人)拖
solve 位置 → 仍 VerifyErr → 排除 CDP Input 管线指纹假说;
- **事件指纹**:影子记录器对比真人/CDP 的 pressure(0.5)、movementX/Y(非零)、
coalesced、间隔分布——JS 可见属性全部一致;
- **时序积压**:绝对时间轴调度修复(旧代码尾部有 1s 空洞 + 0 位移 move);
- **轨迹机械化**`enrich_replay.py` 把 18 点重放加密为 60Hz 样条流
Catmull-Rom + 速度衰减微抖动)。
结论:**之前所有失败案例事后看都是位置错(flatness 假阳性),不存在独立的
"输入指纹拒"**。位置修复后待验证:硬件拖动 + 正确位置能否 PASS(后台循环
进行中,冷却是服务端 5014 强制的 15 分钟窗口)。
### 换号零等待 + 会话预热 + 时长修复(2026-05-16 上午)
用户确认:触发限流时**清 cookie 即可解除**`rotate_loop.py` reload 前先
`Network.clearBrowserCookies`,再 `Page.navigate` 回 douyin——清 cookie 后
session 失效会跳页,reload 会把页面带到空标签)。
三项修复后 20 分钟冷却 + 预热 + 干净单拖 → 仍 VerifyErr
1. **触发拒绝正则误报**`/验证码登录/` 会匹配登录 tab 自身文字,导致每次
提交都被判"被拒"换号。改为 `/仅支持验证码|不存在|格式有误|频繁|繁忙|错误/`
2. **up 前终点停顿丢失**:真人 PASS 轨迹 up 前 dt≈0.7s(终点对齐),
旧代码固定 `sleep(0.15)`,拖动总时长 2.6s 被压成 1.9s。改为
`sleep(max(pts[-1].dt, 0.15))`;跳过 0 位移步时也累加 `t_plan`(时间轴连续);
3. **会话预热**:真人 down 前有 307s 页面活动,我们只有 11s。新增
`_warmup_session()`(触发验证码**之前**游走 45-75s——放在采题后会把
challenge 等过期,NotFoundChallengeId 实测)。
### 会话轨迹硬数据(真人 vs xdotool)
| 指标 | 真人 PASS | xdotool(修复后) |
| --- | --- | --- |
| 拖动 | 109px / 2.60s / 17 move | 位移精确 / 2.6s / 17 move |
| move 间隔 | 20/83/266 ms | 完全一致(同一轨迹重放) |
| down 前 | 70 事件 / 307s | 预热后 ≈30 事件 / 60s |
| up 后 | 11 事件,1ms | 4 事件,500ms |
| y 振幅 | 4px | 4px |
### 悬而未决
- **VerifyErr 根因**:位置小偏差(视觉确证 live_191109 solve 正确,
live_135654 块内 diamond 略偏)与行为分耦合,现有真值无法拆分;
- 真人 GT 采集效率低(challenge 约 1 分钟过期 + 用户主观对齐误差可达
84px——live_190742 两次自拖差 84px),可信 GT 只有 live_165204 PASS
- 偶发"拖动不跟手"(鼠标事件完整、SDK 位移 15-100px 戛然而止)无法复现规律。
---
## 尺度精修与 VerifyErr 收尾(2026-09-15 深夜)
### 社区情报核查(VerifyErr 语义确认)
多来源交叉(52pojie 滑块纯算帖、cnblogs 拉灯 s_v_web_id、yoyo1216 playwright、
TikTok-Captcha-Solver、jishuzhan 轨迹原理):
- **VerifyErr = 滑块缺口距离错误**(位置层);VerifyModelErr 才是轨迹被模型拒
- 换算系数 340/552(≈0.616)为社区共识;部分实现带 ±5px UI 修正(方向互相矛盾,
均无严格论证,source_check 两次判 missing-evidence
- TikTok-Captcha-Solverreply.x 按原图 552 系上报(modified_img_width=552),
y 每点恒等于 tip_yCSDN 150612271 实测样本 reply x∈[236,428]modified_img_width=340
但 x 超 340)→ x 上报的确实是另一(原图)尺度
- jishuzhan 轨迹原理:滑块起始 x∈[20,84.05]、y∈[281,325];进页面鼠标轨迹与
滑动轨迹是两组;AI 生成的"四边进入+起始范围"不合规轨迹会被拒
### 插桩:SDK 加密前明文(JSON.stringify hook
hook JSON.stringify,捕获含 `"mode"`/`relative_time` 的长串。抓到的是 SDK **日志
上报**h5_result / h5_show_picture),非 captchaBody;确认字段:duration(展示→
可交互时长,不是拖动时长)、dragType: "btn"、is_decision、canvas_hash、stage。
### 指针事件流级对比(真人 vs xdotooliframe 内 probe
| 特征 | 真人 | xdotool 60Hz |
| --- | --- | --- |
| coalescedEvents.length | 1 为主(663/682),少量 2 | 1 |
| pointerType / pressure | mouse / 0.5 | mouse / 0.5 |
| move 间隔 | 中位 34ms(≈30Hz),含 0ms 同帧双事件 | 16.7ms |
| 坐标小数 | 无(全整数) | 无 |
| y 振幅 | ±9px | ±4px |
输入层无决定性差异;coalesced/pressure/pointerType/时间戳全部排除。
### 「拖动不跟手」指标作废
post_pos 在 mouseup 后 0.5-2s 读取,**VerifyErr 后 SDK 立即重置滑块**——
读到的 0.0/22.9px 是重置动画中间态。__dragTrace 显示鼠标事件流在全部案例中
完整(53-57 move,位移与期望一致)。此前的"SDK 位移截断"是误读。
### 尺度精修(src/method_l_shape.py,本轮核心修复)
SCALES 网格步长 0.025,真尺度落在步间时轮廓对齐可偏 5-15px(VerifyErr 量级)。
对入选 OK 候选在 s±0.0375(步长 0.0125)重扫 + ±6px 窗口局部最优 + rotation_scan
取 refined 最低:
- 离线回归 9/10 保持;精修值出现在网格外(s=0.988/1.038/1.012)证明生效
- GT live_165204179 → **176**(真人 177,Δ 2 → 1
- **机器 PASS #2**live_160548x=255**s=1.0375**(精修值),157.1px 18 步重放,
SDK 155/157.1 跟手,toast「验证成功,正在为您登录」
- VerifyErr×2live_155342/161204):solve 分数不差(3.3/2.8)但**视觉混合不像**
—— 是候选选错(s=0.7 网格下边界假峰等),不是差几 px
测试脚手架教训:离线验证必须传 4 通道 markIMREAD_UNCHANGED);丢 alpha 会
退化成整方块边界 → score=nan + 左边缘假候选(nan 参与 min(key=refined) 比较行为未定义)。
### 结论与下一步
- VerifyErr 主要由**候选选择错误**贡献;尺度精修解决其中"真尺度落在步间"的部分,
并立即产生一次机器 PASS
- 剩余 VerifyErr 的两题 solve 视觉都不像 → 提升方向是**多候选质量评估/换题**
(视觉不像宁可 refresh),而非继续调拖动;
- 单号 PASS 后 60s 再战立即 VerifyErr:同一账号短间隔第二次挑战行为分不足,
需要更长冷却或换号。
### live_162840 复盘(2026-09-15 深夜,用户视觉确证选错)
精修后第 4 次线上(VerifyErr):x=334 s=1.0375 score=3.23 共识题,
用户观察拖动终点「视觉确实没对齐」。事后穷举定位真缺口:
- NCC 峰 (309,128) NCC=0.26、亮差法 (102,139)、平滑法 (460,232)、
白线 tophat 三处 —— 无一与 diamond 贴片视觉吻合(该题雪地本身极亮,
白贴片对比度崩塌,所有纹理/亮度/平滑特征全灭)
- 对照 PASS 题 live_160548:目标处有明显白色 diamond 描边可辨
**工程结论**solve 分数(2.8-4.4 区间)与位置正确性零相关——PASS 与 FAIL
分数同分布。雪景+白贴片题上缺口的可辨识特征在 HSV/梯度域不显著,
单帧图像算法定位此题无解。可行方向:
1. 采题侧预检(和 PASS 题同分布的图可先 filter:背景过亮/过白的题 refresh 掉);
2. 多帧一致性(refresh 后同题不同噪);
3. 服务端验证式试探(成本高)。
### ddddocr 评测(2026-09-15 深夜,用户建议)
ddddocr 1.6.1slide_matchsimple_target=True)对抖音贴图题全灭:
| 题 | GT | black 底 | white 底 |
| --- | --- | --- | --- |
| live_164551 | 309 | 333 (+24, conf .28) | 369 (+60, conf .62) |
| live_165204 | 179 | 245 (+66) | 234 (+55) |
| live_172458 | 388 | 314 (-74) | 133 (-255) |
| live_173321 | 391 | 446 (+55) | 441 (+50) |
- 误差 +24 至 -255pxblack/white 两种 alpha 预处理给出完全不同的答案(不稳定)
- 原因:slide_match 为「窄长滑块 + 挖空缺口」设计;抖音 mark 是约 100px 大块
贴图、缺口是半透明白贴片(非挖空),模板纹理关系不成立
- 结论:ddddocr 不适用于该场景,维持方向感知 chamfer 路线
### TV 亚像素精修(2026-09-15 深夜,自研方案终局)
原理:缺口渲染 = bg×(1−a)+white×a(半透明白贴片)。给定候选位置,用 mark 的
alpha 反推 bg=(pixelwhite·a)/(1a)),反推结果的总变差(TV)在正确位置最低
(反推出的背景应是自然图像)。与 chamfer z 分数相加(λ=1.0),±5px 窗口联合取谷。
标定(4 样本,bbox 系真值):
| 题 | 真值 | 精修前 Δ | 精修后 Δ |
| --- | --- | --- | --- |
| live_164551 | 309 | +1 | **0** |
| live_165204 | 177 | 1 | **0** |
| live_172458 | 388 | 1 | **+1** |
| live_173321 | 391 | 0 | **0** |
离线 10 题回归 9/10 保持(TV 保护逻辑:谷值信噪 <0.5σ 时跳过,防噪声扰动)。
同日上线:live_172415=155342 同题二出),solve x=395 未被 TV 移动
(comb 谷在原位),用户目检确认「这个很准」。仍 VerifyErr。
修正认知:**post_pos 位移读数只在 PASS 时可信**VerifyErr 后 SDK 立即重置,
读到的是重置中间态——172415 读到 231.8/243.3「截断」实为重置,非不跟手)。
VerifyErr 尾案:位置准 + VerifyErr 并存 → 服务端容差可能比预想更紧(±2px 级),
或存在尚未观测到的判据(行为分残留 / mark 与服务端渲染的 ±2px 源差异)。