186 lines
6.7 KiB
Python
186 lines
6.7 KiB
Python
"""干净标定 + 精确求解 + 落点 ground-truth(中途截图)。
|
|
|
|
标定(慢 swipe 114->900, 6s; 中途 c1@2s c2@4s c3@5.5s):
|
|
- diff(c1,c2): 拼图块位移 / handle位移(262px) = ratio
|
|
- diff(ref,c1): home 簇(左) => home_center
|
|
- c3 近 max-reach: 看 piece 最远能到哪(验证轨道是否够长)
|
|
刷新后重测缺口 => 按 ratio 求 handle 目标 => swipe; 中途截图验证落点。
|
|
"""
|
|
from __future__ import annotations
|
|
import subprocess
|
|
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
|
|
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(img, x_min_local=230, col_thr=25):
|
|
reg = photo(img)
|
|
col = (reg < 90).sum(axis=0)
|
|
col_r = col[x_min_local:]
|
|
hot = np.where(col_r > col_thr)[0]
|
|
if len(hot) == 0:
|
|
return None
|
|
br = np.where(np.diff(hot) > 12)[0]
|
|
segs = [s for s in np.split(hot, br + 1) if len(s) > 10]
|
|
seg = max(segs, key=lambda s: col_r[s].sum())
|
|
segL = x_min_local + int(seg[0])
|
|
segR = x_min_local + int(seg[-1])
|
|
block = (reg[:, segL:segR + 1] < 95)
|
|
ys, xs = np.where(block)
|
|
if len(xs) == 0:
|
|
return None
|
|
cx = PHOTO_LEFT + int(round(np.average(xs + segL)))
|
|
return cx
|
|
|
|
|
|
def detect_handle(img):
|
|
band = img[925:980, :]
|
|
col = (band < 235).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 diff_centroids(a, b, thr=45):
|
|
"""帧差 -> 拼图块足迹簇的局部 cx 列表(按 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]))))
|
|
return sorted(out)
|
|
|
|
|
|
def main():
|
|
ref = cap()
|
|
hh = detect_handle(ref)
|
|
print(f"[init] handle_home={hh} notchA={detect_notch(ref)}", flush=True)
|
|
|
|
DH = 900 - hh
|
|
p = subprocess.Popen(["adb", "shell", "input", "swipe",
|
|
str(hh), str(TRACK_Y), "900", str(TRACK_Y), "6000"])
|
|
time.sleep(2.0); c1 = cap()
|
|
time.sleep(2.0); c2 = cap()
|
|
time.sleep(1.5); c3 = cap()
|
|
p.wait()
|
|
time.sleep(2.2) # 释放 => refresh
|
|
|
|
# ratio: c1->c2 (handle disp = DH*2/6)
|
|
dh_c1c2 = DH * 2.0 / 6.0
|
|
b12 = diff_centroids(photo(c1), photo(c2))
|
|
ratio = None
|
|
if len(b12) >= 2:
|
|
shift = b12[-1] - b12[0]
|
|
ratio = shift / dh_c1c2
|
|
print(f"[cal] c1->c2 blobs_local={b12} shift={shift} handle_disp={dh_c1c2:.0f} "
|
|
f"=> ratio={ratio:.3f}", flush=True)
|
|
# home: ref->c1 (handle disp = DH*2/6)
|
|
b01 = diff_centroids(photo(ref), photo(c1))
|
|
home_center = None
|
|
if len(b01) >= 2:
|
|
home_center = PHOTO_LEFT + b01[0]
|
|
if ratio is None:
|
|
ratio = (b01[-1] - b01[0]) / dh_c1c2
|
|
print(f"[cal] ref->c1 blobs_local={b01} home_center={home_center} "
|
|
f"(cross-ratio={(b01[-1]-b01[0])/dh_c1c2:.3f})", flush=True)
|
|
# max-reach: where is piece in c3? diff(ref,c3) rightmost blob
|
|
b03 = diff_centroids(photo(ref), photo(c3))
|
|
if b03:
|
|
maxreach = PHOTO_LEFT + b03[-1]
|
|
print(f"[cal] c3(ref->c3) blobs_local={b03} piece_rightmost@screen={maxreach} "
|
|
f"(handle disp~{DH*5.5/6:.0f})", flush=True)
|
|
print(f"[cal] => home_center={home_center} ratio={ratio}", flush=True)
|
|
|
|
if home_center is None or ratio is None or ratio <= 0:
|
|
print("[!] 标定失败", flush=True); return 2
|
|
|
|
fresh = cap()
|
|
notch_cx = detect_notch(fresh)
|
|
print(f"[fresh] notchB={notch_cx}", flush=True)
|
|
if notch_cx is None:
|
|
print("[!] 刷新后无缺口", flush=True); return 2
|
|
|
|
delta_piece = notch_cx - home_center
|
|
delta_handle = delta_piece / ratio
|
|
handle_target = int(round(hh + delta_handle))
|
|
print(f"[plan] notch_cx={notch_cx} home={home_center} ratio={ratio:.3f} "
|
|
f"=> Δpiece={delta_piece:.0f} Δhandle={delta_handle:.0f} "
|
|
f"swipe {hh}->{handle_target}", flush=True)
|
|
|
|
adb_text("logcat", "-c")
|
|
p2 = subprocess.Popen(["adb", "shell", "input", "swipe", str(hh), str(TRACK_Y),
|
|
str(handle_target), str(TRACK_Y), "1600"])
|
|
time.sleep(0.9)
|
|
mid = cap() # 落点 ground truth
|
|
p2.wait()
|
|
time.sleep(3.5)
|
|
|
|
aft = cap()
|
|
top = adb_text("shell", "dumpsys", "activity", "activities")
|
|
after_top = next((l.strip() for l in top.splitlines() if "topResumedActivity" in l), "(none)")
|
|
closed = "KwaiWebViewActivity" not in after_top
|
|
photo_changed = not np.array_equal(photo(fresh), photo(aft))
|
|
|
|
# 落点: mid 时 piece 位置 (diff fresh,mid 右簇)
|
|
landing = None
|
|
bm = diff_centroids(photo(fresh), photo(mid))
|
|
if bm:
|
|
landing = PHOTO_LEFT + bm[-1]
|
|
print(f"[post] top={after_top[:72]}", flush=True)
|
|
print(f"[post] closed={closed} photo_changed={photo_changed} "
|
|
f"piece_landing@screen={landing} (notch={notch_cx}, Δ={landing-notch_cx if landing else '?'})", flush=True)
|
|
if closed:
|
|
print(">>> VERDICT: 活动关闭 => result=1, 真机指纹过 350014! 路径=真机 WebView/模拟器。", flush=True)
|
|
elif photo_changed and landing is not None and abs(landing - notch_cx) <= 20:
|
|
print(">>> VERDICT: 落点命中缺口(±20)却刷新 => 350014(号码被永久标记, 真机也过不去)。", flush=True)
|
|
elif photo_changed:
|
|
print(f">>> VERDICT: 刷新但落点偏 {landing-notch_cx:+.0f}px => 缺口错(350002), 调整再试。", flush=True)
|
|
else:
|
|
print(">>> VERDICT: 未刷新 => 滑动未生效。", flush=True)
|
|
|
|
print("\n===== LOGCAT =====", flush=True)
|
|
for ln in adb_text("logcat", "-d").splitlines():
|
|
low = ln.lower()
|
|
if any(k in low for k in ("captcha", "verify", "3500", "ksecret", "result\"", "hardetect")):
|
|
print(ln[:190], flush=True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|