Files
douyin-captcha/docs/experiments.md
T

18 KiB
Raw Blame History

实验演进记录(A → L

最终方案与使用说明见 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. 闭合轮廓 + matchShapes444d 上): 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 序列里 buttonsbutton 状态矛盾(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 #2live_160548x=255s=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 路线