Files
fws_code/dp_slider_verify.py

280 lines
8.9 KiB
Python
Raw Permalink Normal View History

2026-02-26 01:32:11 +08:00
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
根据背景图 + 拼图块图计算滑块目标偏移并可选使用 DrissionPage 执行拖动
默认读取当前项目根目录
- 下载 (1).png (背景图)
- 下载.png (拼图块RGBA 透明图)
"""
from __future__ import annotations
import argparse
import random
import time
from pathlib import Path
import numpy as np
from PIL import Image, ImageDraw
def _load_images(bg_path: Path, piece_path: Path) -> tuple[np.ndarray, np.ndarray]:
bg = np.array(Image.open(bg_path).convert("RGB"), dtype=np.int16)
piece_rgba = np.array(Image.open(piece_path).convert("RGBA"), dtype=np.int16)
return bg, piece_rgba
def _piece_bbox(alpha: np.ndarray, threshold: int = 12) -> tuple[int, int, int, int, np.ndarray]:
mask = alpha > threshold
if not mask.any():
raise ValueError("拼图块图片 alpha 全透明,无法匹配")
ys, xs = np.where(mask)
y0, y1 = int(ys.min()), int(ys.max()) + 1
x0, x1 = int(xs.min()), int(xs.max()) + 1
return x0, y0, x1, y1, mask
def calc_drag_distance(
bg_path: Path,
piece_path: Path,
alpha_threshold: int = 12,
) -> dict:
"""返回最佳匹配位置与建议拖动距离。"""
bg, piece_rgba = _load_images(bg_path, piece_path)
bh, bw = bg.shape[:2]
ph, pw = piece_rgba.shape[:2]
if ph != bh:
raise ValueError(f"图片高度不一致:背景={bh}, 拼图块={ph}")
alpha = piece_rgba[:, :, 3]
x0, y0, x1, y1, _ = _piece_bbox(alpha, threshold=alpha_threshold)
piece_crop = piece_rgba[y0:y1, x0:x1, :3]
mask = alpha[y0:y1, x0:x1] > alpha_threshold
ys, xs = np.where(mask)
piece_pixels = piece_crop[ys, xs]
patch_h, patch_w = piece_crop.shape[:2]
if patch_w > bw or patch_h > bh:
raise ValueError("拼图块裁剪后尺寸超过背景图")
max_x = bw - patch_w
best_x = 0
best_score = float("inf")
second_best = float("inf")
# 固定 y按 x 扫描。评分越小越好。
for x in range(max_x + 1):
patch_pixels = bg[y0 + ys, x + xs]
score = float(np.abs(patch_pixels - piece_pixels).mean())
if score < best_score:
second_best = best_score
best_score = score
best_x = x
elif score < second_best:
second_best = score
# 如果拼图初始从最左侧出发建议拖动距离可用best_x - x0
drag_distance = best_x - x0
confidence_ratio = (second_best / best_score) if best_score > 0 else float("inf")
return {
"target_x": int(best_x),
"piece_bbox_x0": int(x0),
"piece_bbox_y0": int(y0),
"piece_bbox_w": int(patch_w),
"piece_bbox_h": int(patch_h),
"drag_distance": int(drag_distance),
"best_score": best_score,
"second_best": second_best,
"confidence_ratio": confidence_ratio,
"bg_width": int(bw),
"bg_height": int(bh),
}
def save_debug_overlay(bg_path: Path, out_path: Path, match: dict) -> None:
img = Image.open(bg_path).convert("RGB")
draw = ImageDraw.Draw(img)
x = int(match["target_x"])
y = int(match["piece_bbox_y0"])
w = int(match["piece_bbox_w"])
h = int(match["piece_bbox_h"])
draw.rectangle([x, y, x + w, y + h], outline=(255, 0, 0), width=2)
img.save(out_path)
def build_human_track(distance: int) -> list[int]:
"""生成拟人拖动轨迹(整数步长列表)。"""
if distance == 0:
return []
direction = 1 if distance > 0 else -1
remaining = abs(int(round(distance)))
track: list[int] = []
moved = 0
velocity = 0.0
interval = 0.02
accelerate_until = remaining * 0.7
while moved < remaining:
if moved < accelerate_until:
acc = random.uniform(2.0, 4.0)
else:
acc = -random.uniform(3.0, 6.0)
step = max(1, int(velocity * interval + 0.5 * acc * interval * interval * 100))
moved += step
velocity = max(0.0, velocity + acc * interval)
track.append(step)
overflow = moved - remaining
if overflow > 0 and track:
track[-1] -= overflow
if track[-1] <= 0:
track.pop()
# 小幅回拉,增加拟人性
if remaining >= 30:
back = random.randint(1, 3)
track.extend([-back, back])
return [direction * s for s in track if s != 0]
def drag_slider_with_dp(page, slider_selector: str, distance: int) -> None:
slider = page.ele(slider_selector, timeout=8)
if not slider:
raise RuntimeError(f"未找到滑块元素:{slider_selector}")
# 若元素支持直接 drag优先用内置实现
if hasattr(slider, "drag"):
try:
slider.drag(offset_x=distance, offset_y=0, duration=0.8)
return
except Exception:
pass
actions = page.actions
actions.move_to(slider, duration=0.2)
hold_ok = False
for hold_call in (
lambda: actions.hold(slider),
lambda: actions.hold(),
):
try:
hold_call()
hold_ok = True
break
except Exception:
continue
if not hold_ok:
raise RuntimeError("无法按下滑块,请确认 DrissionPage 版本与页面状态")
for step in build_human_track(distance):
dy = random.randint(-1, 1)
moved = False
for move_call in (
lambda: actions.move(step, dy, duration=random.uniform(0.01, 0.04)),
lambda: actions.move(offset_x=step, offset_y=dy, duration=random.uniform(0.01, 0.04)),
lambda: actions.move(step, dy),
lambda: actions.move(offset_x=step, offset_y=dy),
):
try:
move_call()
moved = True
break
except Exception:
continue
if not moved:
raise RuntimeError("动作链 move 调用失败,请按当前 DrissionPage 版本调整 move 参数")
released = False
for release_call in (
lambda: actions.release(),
lambda: actions.release(on_ele=None),
):
try:
release_call()
released = True
break
except Exception:
continue
if not released:
raise RuntimeError("动作链 release 调用失败")
def maybe_run_dp_drag(args: argparse.Namespace, distance: int) -> None:
if not args.url or not args.slider:
return
from DrissionPage import ChromiumOptions, ChromiumPage
if args.port:
co = ChromiumOptions().set_local_port(port=args.port)
page = ChromiumPage(addr_or_opts=co)
else:
page = ChromiumPage()
page.get(args.url)
if args.wait_after_open > 0:
time.sleep(args.wait_after_open)
drag_slider_with_dp(page, args.slider, distance)
def parse_args() -> argparse.Namespace:
root = Path(__file__).resolve().parent
parser = argparse.ArgumentParser(description="DP 滑块验证辅助脚本")
parser.add_argument("--bg", type=Path, default=root / "下载 (1).png", help="背景图路径")
parser.add_argument("--piece", type=Path, default=root / "下载.png", help="拼图块图路径")
parser.add_argument("--alpha-threshold", type=int, default=12, help="透明阈值(0-255)")
parser.add_argument("--debug-out", type=Path, default=root / "slider_match_debug.png", help="匹配结果标注图")
# 以下参数用于真实页面拖动(可选)
parser.add_argument("--url", type=str, default="", help="验证码页面 URL")
parser.add_argument("--slider", type=str, default="", help="滑块元素选择器")
parser.add_argument("--port", type=int, default=0, help="连接已有浏览器的本地端口")
parser.add_argument("--distance-adjust", type=int, default=0, help="对计算结果追加修正像素")
parser.add_argument("--wait-after-open", type=float, default=1.0, help="打开页面后等待秒数")
return parser.parse_args()
def main() -> None:
args = parse_args()
if not args.bg.exists():
raise FileNotFoundError(f"背景图不存在:{args.bg}")
if not args.piece.exists():
raise FileNotFoundError(f"拼图块图不存在:{args.piece}")
match = calc_drag_distance(args.bg, args.piece, alpha_threshold=args.alpha_threshold)
save_debug_overlay(args.bg, args.debug_out, match)
distance = int(match["drag_distance"]) + int(args.distance_adjust)
print("匹配结果:")
print(f" target_x={match['target_x']}")
print(f" piece_bbox_x0={match['piece_bbox_x0']}")
print(f" 建议拖动距离 drag_distance={match['drag_distance']}")
print(f" 调整后拖动距离 distance={distance}")
print(f" best_score={match['best_score']:.4f}, second_best={match['second_best']:.4f}")
print(f" confidence_ratio={match['confidence_ratio']:.4f} (越大越好)")
print(f" 标注图已输出:{args.debug_out}")
if args.url and args.slider:
maybe_run_dp_drag(args, distance)
print("DP 拖动已执行。")
if __name__ == "__main__":
main()