2D桌球自动化脚本5-ZXQZQ

发布时间:2026/11/6 1:54:56
2D桌球自动化脚本5-ZXQZQ
importcv2importnumpyasnpimportpyautoguiimportwin32api,win32conimporttimeimporttkinterastkfromPILimportImageGrabimportos# ================= 配置区域 =================TEMPLATE_DIR="templates"POCKET_TEMPLATES={"top_left":"pocket_tl.png","top_mid":"pocket_tm.png","top_right":"pocket_tr.png","bottom_left":"pocket_bl.png","bottom_mid":"pocket_bm.png","bottom_right":"pocket_br.png",}POCKET_THRESHOLDS={"top_left":0.80,"top_mid":0.60,"top_right":0.80,"bottom_left":0.80,"bottom_mid":0.80,"bottom_right":0.80,}# 白球:V 下限从 160 放宽到 140,容忍光照WHITE_LOWER=np.array([0,0,140])WHITE_UPPER=np.array([180,40,255])RED_LOWER1=np.array([0,100,50])RED_UPPER1=np.array([10,255,255])RED_LOWER2=np.array([160,100,50])RED_UPPER2=np.array([180,255,255])BALL_RADIUS=11AIM_CLEARANCE=2.0POWER_HOLD_TIME=0.6SHOT_COOLDOWN=5.0# 兜底杆力度(轻推解围)RESCUE_HOLD_TIME=0.3WHITE_STUCK_LIMIT=5WHITE_STUCK_TOL=3# 几何门槛MIN_COS_ANGLE=0.15# 最小切角余弦(0.15 ≈ 81°)MIN_DOT_TOL=-0.1# 允许轻微反向NEAR_RAIL_DIST=30# 目标球离画面边缘 < 30px 视为贴库POCKET_SHRINK=0.03# 贴库时袋口向中心收缩比例# ============================================classSnookerBot:def__init__(self,region):self.region=region# (x, y, w, h)self.white_ball=Noneself.target_balls=[]self.pockets=[]# 袋口缓存self._pockets_cached=None# 卡死检测self._last_white=Noneself._white_stuck_count=0# ---------- 截图 ----------defcapture(self):img=ImageGrab.grab(bbox=(self.region[0],self.region[1],self.region[0]+self.region[2],self.region[1]+self.region[3]))frame=np.array(img)returncv2.cvtColor(frame,cv2.COLOR_RGB2BGR)# ---------- 袋口模板匹配(带缓存) ----------deffind_pockets_by_template(self,frame,force=False):# 已缓存 6 个袋口则复用,避免被球遮挡导致抖动ifself._pockets_cachedisnotNoneandnotforce:self.pockets=self._pockets_cachedreturnself.pockets=[]forname,filenameinPOCKET_TEMPLATES.items():template_path=os.path.join(TEMPLATE_DIR,filename)ifnotos.path.exists(template_path):print(f"警告: 找不到模板{template_path},请先截图保存!")continuetemplate=cv2.imread(template_path)iftemplateisNone:print(f"警告: 模板读取失败{template_path}")continueres=cv2.matchTemplate(frame,template,cv2.TM_CCOEFF_NORMED)_,max_val,_,max_loc=cv2.minMaxLoc(res)thr=POCKET_THRESHOLDS.get(name,0.8)ifmax_val>=thr:h,w=template.shape[:2]cx=max_loc[0]+w//2cy=max_loc[1]+h//2self.pockets.append((cx,cy))print(f"找到袋口{name}: ({cx},{cy}), 相似度:{max_val:.2f}")else:print(f"未找到袋口{name},最高相似度:{max_val:.2f}(阈值{thr})")iflen(self.pockets)==6:self._pockets_cached=list(self.pockets)print("[袋口] 已缓存 6 个袋口坐标,后续复用")# ---------- 球识别 ----------deffind_balls(self,frame):hsv=cv2.cvtColor(frame,cv2.COLOR_BGR2HSV)H,W=frame.shape[:2]kernel=np.ones((3,3),np.uint8)# 白球mask_white=cv2.inRange(hsv,WHITE_LOWER,WHITE_UPPER)mask_white=cv2.morphologyEx(mask_white,cv2.MORPH_OPEN,kernel)contours_w,_=cv2.findContours(mask_white,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)self.white_ball=Nonebest_r=0forcincontours_w:(x,y),r=cv2.minEnclosingCircle(c)ifr>8andr>best_r:best_r=r self.white_ball=(int(x),int(y))# 红球mask_red=cv2.bitwise_or(cv2.inRange(hsv,RED_LOWER1,RED_UPPER1),cv2.inRange(hsv,RED_LOWER2,RED_UPPER2),)mask_red=cv2.morphologyEx(mask_red,cv2.MORPH_OPEN,kernel)contours_r,_=cv2.findContours(mask_red,cv2.RETR_EXTERNAL,cv2.CHAIN_APPROX_SIMPLE)self.target_balls=[]forcincontours_r:(x,y),r=cv2.minEnclosingCircle(c)ifr<=8:continueifr>25:continue# 圆度校验area=cv2.contourArea(c)circle_area=np.pi*r*rifcircle_area<1e-6orarea/circle_area<0.55:continue# 贴边过滤ifx<30ory<30orx>W-30ory>H-30:continue# 袋口附近过滤near_pocket=any((x-px)**2+(y-py)**2<35**2for(px,py)inself.pockets)ifnear_pocket:continueself.target_balls.append((int(x),int(y)))# ---------- 假想球瞄准 ----------defcalculate_aim_point(self):ifnotself.white_ballornotself.target_ballsornotself.pockets:print("[AIM] 输入不完整:white=%s, targets=%d, pockets=%d"%(self.white_ball,len(self.target_balls),len(self.pockets)))returnNone,NoneW,H=self.region[2],self.region[3]CENTER=(W/2,H/2)candidates=[]fortinself.target_balls:# 目标球是否贴库near_rail=(t[0]<NEAR_RAIL_DISTort[0]>W-NEAR_RAIL_DISTort[1]<NEAR_RAIL_DISTort[1]>H-NEAR_RAIL_DIST)forpocketinself.pockets:px,py=pocket# 贴库时袋口向中心收缩ifnear_rail:px+=(CENTER[0]-px)*POCKET_SHRINK py+=(CENTER[1]-py)*POCKET_SHRINK v_tp=np.array([px-t[0],py-t[1]],dtype=float)n_tp=np.linalg.norm(v_tp)ifn_tp<1e-6:continuedir_tp=v_tp/n_tp aim=(t[0]-dir_tp[0]*BALL_RADIUS*AIM_CLEARANCE,t[1]-dir_tp[1]*BALL_RADIUS*AIM_CLEARANCE)v_wa=np.array([aim[0]-self.white_ball[0],aim[1]-self.white_ball[1]],dtype=float)v_at=np.array([t[0]-aim[0],t[1]-aim[1]],dtype=float)ifnp.linalg.norm(v_wa)<1e-6:continuedot=np.dot(v_wa,v_at)ifdot<=MIN_DOT_TOL:continuecos_angle=dot/(np.linalg.norm(v_wa)*np.linalg.norm(v_at)+1e-9)ifcos_angle<MIN_COS_ANGLE:continuecandidates.append({"target":t,"pocket":pocket,"aim":(int(round(aim[0])),int(round(aim[1]))),"dist":np.linalg.norm(v_wa),"cos":cos_angle,})ifnotcandidates:print("[AIM] 没有任何 红球×袋口 组合满足几何条件")returnNone,Nonebest=min(candidates,key=lambdac:c["dist"]-c["cos"]*50)print(f"[AIM] 白球={self.white_ball}目标={best['target']}"f"袋口={best['pocket']}假想点={best['aim']}"f"距离={best['dist']:.1f}cos={best['cos']:.2f}")returnbest["aim"],best["target"]# ---------- 击球 ----------defshoot(self,aim_point,hold_time=None):ifnotaim_point:returnhold=hold_timeifhold_timeisnotNoneelsePOWER_HOLD_TIME screen_x=self.region[0]+aim_point[0]screen_y=self.region[1]+aim_point[1]print(f"[SHOOT] 移动鼠标到 ({screen_x},{screen_y}) 并蓄力{hold}s")win32api.SetCursorPos((screen_x,screen_y))time.sleep(0.15)win32api.mouse_event(win32con.MOUSEEVENTF_LEFTDOWN,0,0,0,0)time.sleep(hold)win32api.mouse_event(win32con.MOUSEEVENTF_LEFTUP,0,0,0,0)time.sleep(SHOT_COOLDOWN)# ---------- 兜底:朝最近红球轻推 ----------defrescue_shot(self):ifnotself.white_ballornotself.target_balls:print("[兜底] 白球或红球缺失,无法解围")returnFalsenearest=min(self.target_balls,key=lambdat:(t[0]-self.white_ball[0])**2+(t[1]-self.white_ball[1])**2)# 稍微偏移,避免完全正碰(正碰容易形成"粘球")aim=(nearest[0]+4,nearest[1]-4)print(f"[兜底] 朝最近红球{nearest}轻推,目标点{aim}")self.shoot(aim,hold_time=RESCUE_HOLD_TIME)returnTrue# ---------- 卡死检测 ----------defcheck_white_stuck(self):ifself.white_ballisNone:returnFalseifself._last_whiteisNone:self._last_white=self.white_ball self._white_stuck_count=0returnFalsemoved=(abs(self.white_ball[0]-self._last_white[0])>WHITE_STUCK_TOLorabs(self.white_ball[1]-self._last_white[1])>WHITE_STUCK_TOL)self._last_white=self.white_ballifmoved:self._white_stuck_count=0returnFalseself._white_stuck_count+=1ifself._white_stuck_count>=WHITE_STUCK_LIMIT:print(f"[警告] 白球连续{self._white_stuck_count}轮未移动")self._white_stuck_count=0returnTruereturnFalse# ================= 主程序 =================defselect_region():print("请拖动鼠标框选游戏区域...")root=tk.Tk()root.attributes("-alpha",0.3)root.attributes("-fullscreen",True)root.config(cursor="cross")coords={}canvas=tk.Canvas(root,cursor="cross",bg="grey")canvas.pack(fill="both",expand=True)rect=Nonestart_x=start_y=0defon_button_press(event):nonlocalstart_x,start_y,rect start_x,start_y=event.x,event.y rect=canvas.create_rectangle(start_x,start_y,start_x,start_y,outline='red',width=2)defon_move_press(event):ifrectisnotNone:canvas.coords(rect,start_x,start_y,event.x,event.y)defon_button_release(event):end_x,end_y=event.x,event.y x=min(start_x,end_x)y=min(start_y,end_y)w=abs(end_x-start_x)h=abs(end_y-start_y)coords['region']=(x,y,w,h)root.quit()canvas.bind("<ButtonPress-1>",on_button_press)canvas.bind("<B1-Motion>",on_move_press)canvas.bind("<ButtonRelease-1>",on_button_release)root.mainloop()root.destroy()returncoords.get('region')if__name__=="__main__":game_region=select_region()ifnotgame_region:print("未选择区域,退出。")exit()print(f"游戏区域:{game_region}")bot=SnookerBot(game_region)round_no=0try:whileTrue:round_no+=1print(f"\n--- 第{round_no}轮识别 ---")frame=bot.capture()bot.find_pockets_by_template(frame)bot.find_balls(frame)print(f"[状态] 白球={bot.white_ball}"f"红球数={len(bot.target_balls)}袋口数={len(bot.pockets)}")ifbot.white_ballandbot.target_ballsandbot.pockets:aim,target=bot.calculate_aim_point()ifaim:bot.shoot(aim)else:# 兜底解围bot.rescue_shot()else:print("[跳过] 识别不完整,等待下一轮")ifbot.check_white_stuck():time.sleep(3)time.sleep(1)exceptKeyboardInterrupt:print("\n脚本已停止。")

python-2D桌球自动化脚本源代码.zip

(https://download.csdn.net/download/qq_32257509/93804898)
https://download.csdn.net/download/qq_32257509/93804898