ksjsb/tools/phone_calibrate.py
2026-07-30 20:25:56 +08:00

214 lines
7.7 KiB
Python

"""真机标定 + 精确求解。
轨道(640px)比图片(947px)窄 => handle↔piece 非 1:1, 需标定 ratio。
标定: 慢速 swipe + 帧差(拼图块在静态背景上移动 => 差分干净)。
- diff(ref_home, cap1): 拼图块 home 足迹(左)+ 移动后足迹(右) => home_center
- diff(cap1, cap2): 0.7s 内拼图块位移 shift => ratio = shift / (handle位移)
然后刷新后重新检测缺口 => 按 ratio 换算 handle 位移, 精确 swipe。
"""
from __future__ import annotations
import subprocess
import sys
import time
from io import BytesIO
import numpy as np
from PIL import Image
PHOTO_LEFT, PHOTO_RIGHT = 66, 1013
PHOTO_TOP, PHOTO_BOT = 267, 819
PHOTO_W = PHOTO_RIGHT - PHOTO_LEFT # 947
HANDLE_HOME_X = 114 # 初值, 下面重检测
TRACK_Y = 950
def adb_text(*a):
r = subprocess.run(["adb", *a], capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=60)
return r.stdout
def cap():
d = subprocess.run(["adb", "exec-out", "screencap", "-p"],
capture_output=True, timeout=30).stdout
return np.asarray(Image.open(BytesIO(d)).convert("L"))
def photo(img):
return img[PHOTO_TOP:PHOTO_BOT, PHOTO_LEFT:PHOTO_RIGHT]
def detect_notch_center(img):
reg = photo(img)
dark = (reg < 95)
rowcnt = dark.sum(axis=1)
thresh = np.percentile(rowcnt[rowcnt > 30], 60) if (rowcnt > 30).any() else 30
body_rows = np.where(rowcnt >= thresh)[0]
if len(body_rows) == 0:
return None
col = dark[body_rows].sum(axis=0)
hot = np.where(col > 15)[0]
br = np.where(np.diff(hot) > 10)[0]
segs = [s for s in np.split(hot, br + 1) if len(s) > 12]
if not segs:
return None
seg = max(segs, key=lambda s: col[s].sum())
cx = int(round(np.average(seg, weights=col[seg])))
return PHOTO_LEFT + cx, PHOTO_LEFT + int(seg[0]), PHOTO_LEFT + int(seg[-1])
def detect_handle(img):
band = img[925:980, :]
nw = (band < 235)
col = nw.sum(axis=0)
hot = np.where(col > 20)[0]
if len(hot) == 0:
return HANDLE_HOME_X
br = np.where(np.diff(hot) > 8)[0]
segs = [s for s in np.split(hot, br + 1) if s[0] < 320]
if not segs:
segs = np.split(hot, br + 1)
seg = max(segs, key=lambda s: col[s].sum())
return int(round(np.average(seg, weights=col[seg])))
def detect_track_right(img):
band = img[938:962, :]
nw = (band < 230)
col = nw.sum(axis=0)
hot = np.where(col > 8)[0]
return int(hot[-1]) if len(hot) else 1000
def diff_blobs(a, b, thr=45):
"""返回 [(cx_local, seg)] 拼图块足迹簇(局部 x)。"""
d = np.abs(b.astype(np.int16) - a.astype(np.int16))
mask = d > thr
col = mask.sum(axis=0)
hot = np.where(col > 6)[0]
if len(hot) == 0:
return []
w = np.where(col > 6, col, 0)
br = np.where(np.diff(hot) > 12)[0]
out = []
for s in np.split(hot, br + 1):
if len(s) > 12:
out.append((int(round(np.average(s, weights=w[s]))), s))
return out
def main():
ref = cap()
notchA = detect_notch_center(ref)
handle_home = detect_handle(ref)
track_right = detect_track_right(ref)
print(f"[init] notchA(当前验证码)={notchA} handle_home={handle_home} "
f"track_right={track_right}", flush=True)
# 慢速 swipe: handle_home -> handle_home+600, 5000ms; 2 个中途帧
DH = 600
x1 = handle_home
x2 = handle_home + DH
p = subprocess.Popen(["adb", "shell", "input", "swipe",
str(x1), str(TRACK_Y), str(x2), str(TRACK_Y), "5000"])
time.sleep(2.2)
c1 = cap()
t1 = time.monotonic()
time.sleep(0.7)
c2 = cap()
t2 = time.monotonic()
p.wait()
time.sleep(2.2) # swipe 完成+释放 => verify(错位) => 刷新
# home_center: diff(ref, c1) 的左簇
b1 = diff_blobs(photo(ref), photo(c1))
b1.sort()
print(f"[cal] diff(ref,c1) blobs(local cx)= {[c for c, _ in b1]}", flush=True)
if len(b1) >= 2:
home_local = b1[0][0]
moved1_local = b1[-1][0]
elif len(b1) == 1:
home_local, moved1_local = None, b1[0][0]
else:
home_local = moved1_local = None
home_center = (PHOTO_LEFT + home_local) if home_local else None
# ratio: diff(c1,c2) 两个簇位移 / handle 在(t2-t1)位移
dt = t2 - t1
handle_disp_dt = DH * (dt / 5.0)
b2 = diff_blobs(photo(c1), photo(c2))
b2.sort()
print(f"[cal] diff(c1,c2) blobs(local cx)= {[c for c,_ in b2]} "
f"dt={dt:.2f}s handle_disp~{handle_disp_dt:.0f}px", flush=True)
ratio = None
if len(b2) >= 2:
shift = b2[-1][0] - b2[0][0]
ratio = shift / handle_disp_dt if handle_disp_dt else None
print(f"[cal] c1->c2 piece shift={shift}px => ratio(piece/handle)={ratio:.3f}", flush=True)
# 另算 ratio: (moved1 - home)/handle在2.2s位移
if home_local is not None and moved1_local is not None:
hd22 = DH * (2.2 / 5.0)
ratio2 = (moved1_local - home_local) / hd22
print(f"[cal] cross-check ratio(ref->c1)= {ratio2:.3f} "
f"(moved1={moved1_local} home={home_local} hd2.2={hd22:.0f})", flush=True)
if ratio is None:
ratio = ratio2
print(f"[cal] => home_center={home_center} ratio={ratio}", flush=True)
# 刷新后新缺口
fresh = cap()
notchB = detect_notch_center(fresh)
print(f"[fresh] notchB(刷新后)={notchB}", flush=True)
if notchB is None:
print("[!] 刷新后未检测到缺口, 退出。", flush=True)
return 2
notch_cx = notchB[0]
if home_center is None or ratio is None or ratio <= 0:
print("[!] 标定失败, 无法精确 swipe。", flush=True)
return 2
delta_handle = (notch_cx - home_center) / ratio
handle_target = int(round(handle_home + delta_handle))
print(f"[plan] notch_cx={notch_cx} home_center={home_center} ratio={ratio:.3f} "
f"=> Δpiece={notch_cx-home_center:.0f} Δhandle={delta_handle:.0f} "
f"handle {handle_home}->{handle_target} (track_right={track_right})", flush=True)
if handle_target > track_right - 20:
print(f"[!] handle_target {handle_target} 超轨道右端 {track_right}!", flush=True)
# 执行精确 swipe
adb_text("logcat", "-c")
print(f"[act] adb input swipe {handle_home} {TRACK_Y} {handle_target} {TRACK_Y} 600", flush=True)
subprocess.run(["adb", "shell", "input", "swipe", str(handle_home), str(TRACK_Y),
str(handle_target), str(TRACK_Y), "600"], timeout=30)
time.sleep(4.0)
aft = cap()
after_act = adb_text("shell", "dumpsys", "activity", "activities")
after_top = next((l.strip() for l in after_act.splitlines()
if "topResumedActivity" in l), "(none)")
closed = "KwaiWebViewActivity" not in after_top
photo_changed = not np.array_equal(photo(fresh), photo(aft))
print(f"[post] top={after_top[:75]}", flush=True)
print(f"[post] closed={closed} photo_changed={photo_changed}", flush=True)
if closed:
print(">>> VERDICT: 活动关闭 => result=1, 真机指纹过 350014! 真机 WebView 路径可行。", flush=True)
elif photo_changed:
print(">>> VERDICT: 图片刷新+活动仍在 => FAIL。落点精确(已按 ratio 换算居中) => 极可能 350014(号码被标记)。", flush=True)
else:
print(">>> VERDICT: 图片未变 => 滑动未触发 verify 或未刷新。", flush=True)
print("\n===== LOGCAT(net/result) =====", flush=True)
lc = adb_text("logcat", "-d")
for ln in lc.splitlines():
low = ln.lower()
if any(k in low for k in ("captcha", "verify", "3500", "ksecret", "result", "okhttp")):
print(ln[:190], flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())