OpenCV + Aim Lab Gridshot 打到 22W : TSP is all you need
项目地址:https://github.com/ruali-dev/aimlab-gridshot-with-opencv-python
免责声明:本文仅用于学习与研究演示。自动化工具可能违反游戏条款,请自行承担风险。
序言
之前就一直想做一个用 OpenCV 控制 Aim Lab 打小球的项目:目标足够“干净”,规则足够简单,很适合用传统视觉 + 自动化把整条链路跑通。但一直没时间,考研结束后才抽空把它做出来。
开发过程中我用了 antigravity 辅助写代码(Google 的 Vibe Coding IDE)。整体体验是:我把问题描述清楚,它能很快给出可运行的方案,帮我把卡住的环节先往前推。
这篇文章不打算把重点放在「HSV 阈值怎么调」「WinAPI 怎么调用」这种网上到处都有的内容,我更想讲下我在路径优化这个点上做的一点小巧思

问题定义
Gridshot 更像一个“视觉 → 决策 → 控制”的实时闭环:
- 识别:屏幕里哪些是目标?中心点在哪?
- 决策:先打哪个、再打哪个?
- 控制:鼠标怎么以低延迟、低抖动的方式移动过去并点击?
在这个任务里,“每帧能识别到目标”只是入场券;真正影响分数的,是鼠标每一秒走了多少冤枉路。
架构
整体实现拆成三段:
- 捕获:锁定游戏窗口,高速截取 ROI(dxcam + win32gui)
- 视觉:HSV 颜色分割 + 轮廓筛选,得到目标中心点
- 决策 + 控制:mini-TSP 选访问顺序 + Win32
mouse_event插值移动并点击
这套结构的好处:三段可以分别计时,瓶颈在哪一眼就知道。
补充一个踩坑:我一开始遇到「鼠标移动距离 ≠ 游戏里准星移动距离」的问题(本质上是灵敏度、加速、分辨率/ROI、以及输入注入方式叠加导致的非线性)。后面我把它拆成两个问题:
- 先用 ROI offset 把“看到的中心”和“准星中心”对齐
- 再用 灵敏度标定 把“移动多少像素会让准星走多少距离”标出来
这两步做完,开环控制才能稳定工作。
视觉
我只检测一种颜色目标(青色球体),所以没上深度学习:在这个任务里传统视觉足够稳定,而且深度学习带来的推理延迟/部署复杂度不划算。
经典流程:RGB → HSV → 阈值分割 → 轮廓(contour)→ 过滤 → 中心点。
这里的建议很简单:视觉做到“稳定出点”就停手,把精力留给路径与控制。
def find_targets(self, frame):
"""
Returns list of tuples: (cx, cy, area)
"""
if frame is None:
return [], None
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
mask = cv2.inRange(hsv, config.BALL_COLOR_LOWER, config.BALL_COLOR_UPPER)
# Gridshot is clean, but a small dilate can help solidify the ball shape
# for better centroid calculation.
# mask = cv2.dilate(mask, self.kernel, iterations=1)
contours, _ = cv2.findContours(mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
valid_targets = []
for cnt in contours:
area = cv2.contourArea(cnt)
if area < config.MIN_CONTOUR_AREA:
continue
x, y, w, h = cv2.boundingRect(cnt)
if h <= 0: continue
ratio = w / h
if ratio < config.MIN_ASPECT_RATIO or ratio > config.MAX_ASPECT_RATIO:
continue
M = cv2.moments(cnt)
if M["m00"] != 0:
cx = int(M["m10"] / M["m00"])
cy = int(M["m01"] / M["m00"])
# Return Area for debug
valid_targets.append((cx, cy, area))
return valid_targets, mask
TSP
同屏一般 1~3 个球,偶尔更多。很多实现会用“最近邻贪心”:
每次打离鼠标最近的那个目标。
它在多数时候能用,但会出现一个典型问题:局部最优不等于整体最优 —— 你可能每一步都走得最短,但走完整轮并不最短。Gridshot 这种高频场景里,哪怕每轮多走一点点,累积起来就是分数差。

这个例子里,贪心因为“先吃最近的 A”,导致最后从 B 去 C 多走了 100 像素;TSP 允许一开始多走一点(S→B 比 S→A 多 63.7),换来最后少走 100,总路程反而更短(约省 36.3,≈3.1%)。
我做的 TSP
我做的是一个“工程版 mini-TSP”:
- 起点:当前鼠标位置
- 目标集合:当前帧检测到的所有目标点
- 做法:枚举所有访问顺序,选欧氏距离总和最小的那条
- 规模:N≤4 直接暴力枚举(排列数很小),实时性完全够
def solve_tsp_best_next(start_x, start_y, targets):
"""Returns the target that starts the optimal path."""
n = len(targets)
if n == 0: return None
if n == 1: return targets[0]
if n > 4:
targets.sort(key=lambda t: t[4])
subset = targets[:4]
else:
subset = targets
min_total_dist = float('inf')
best_first_target = None
for perm in itertools.permutations(subset):
current_dist = 0
curr_x, curr_y = start_x, start_y
for t in perm:
tx, ty = t[2], t[3]
dist = math.sqrt((tx - curr_x)**2 + (ty - curr_y)**2)
current_dist += dist
curr_x, curr_y = tx, ty
if current_dist < min_total_dist:
min_total_dist = current_dist
best_first_target = perm[0]
return best_first_target
性能分析:TSP 提升的是什么
在这个规模里,TSP 的计算开销几乎可以忽略;它的收益主要来自:
- 减少鼠标移动的总距离 → 减少无效时间 → 提高单位时间击杀数
用一个统一口径来描述收益会更清晰:
- 记
L_greedy为贪心策略在某一轮的总移动距离 - 记
L_tsp为 TSP 最短路径的总移动距离 - 距离节省率:
(L_greedy - L_tsp) / L_greedy
直观理解:哪怕单轮只省 3%~5%,在 Gridshot 这种“几十秒内重复无数轮”的场景里,累积就是实打实的分数差。
我自己跑下来大概能稳定在 22W 左右。对比我看到的一些实现(不少还是 C++ 版本),在识别精度差不多的前提下,得分差距往往不是“识别更聪明”,而是路径策略更省路:同样 60 秒,你能多打出几轮,就会被直接反映到分数上。
控制
控制上我用 Win32 的 mouse_event 来做移动和点击,并把一次大移动拆成多段插值,避免高 DPI 下抖动/过冲。
class Controller:
def __init__(self):
self.user32 = ctypes.windll.user32
if not self.is_admin():
print("WARNING: Not running as Admin. Input may fail.")
def is_admin(self):
try:
return ctypes.windll.shell32.IsUserAnAdmin()
except:
return False
def is_key_down(self, key_code):
return self.user32.GetAsyncKeyState(key_code) & 0x8000
def move_mouse(self, dx, dy):
if dx == 0 and dy == 0: return
self.user32.mouse_event(MOUSEEVENTF_MOVE, int(dx), int(dy), 0, 0)
def click(self):
self.user32.mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0)
import time
time.sleep(0.015) # 15ms hold
self.user32.mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0)
校准
跑之前我做了两件事,保证“看到的目标”和“打到的准星”一致:
ROI offset
用 tools/roi_calibrator.py 调整 ROI 偏移,让脚本的绿色准星对齐游戏准星,然后把值写回配置。
灵敏度标定
用 tools/auto_sensitivity_calibrator.py 自动标定 sensitivity multiplier。
详细操作见Github仓库README
补一句真实体验:这部分基本是我提出需求(比如“我需要一个脚本能自动把灵敏度标出来”),antigravity 很快给出一个可以跑的方案,我再根据效果把参数和边界情况补齐。对这种“工程工具脚本”,AI 的确很省时间。
总结
这个项目算是一次小尝试:把「屏幕捕获 → 传统视觉识别 → 路径规划 → 输入控制 → 校准」整条链路跑通,并且把重点放在 TSP 带来的路径长度节省 上。
最后在排行榜上最高打到过全球第四。

这项目效果看着还挺直观:原理不复杂,但把“实时闭环”里最关键的那一环(路径)捋顺了,分数就上去了。
如果你觉得这个开源项目有帮助,欢迎 B 站三连 / GitHub 点个 Star,也欢迎来交流。