欢迎光临
我们一直在努力

强化学习实战-用强化学习打跑酷游戏 GreatWallRun 第一节 感知层构建

博主是很早接触了长城run这款极简跑酷游戏,当时觉得这款游戏很无聊,但是,现在来看,不失为一份极佳的强化学习项目。

接下来,我们以长城run这款极简跑酷游戏为例,首先使用yolo与固定策略,先实现能运行起来。之后再接入StableBase3使用PPO来完成。

逆向工程怎么做

通俗且片面地说,逆向工程的目的就是自动化打游戏,无论是基于规则的自动化,还是引入AI来自动化。

当然,实际上是指用CE反向抓基址、判断哪里断call、通过数据结构判断共享代码的双方如何分辨等等,这些都是方法,最终是要运用在自动化打游戏的目的之上的。

所有的逆向工程,都包含三个层次,

观察层:状态获取

这一层需要通过CV来获取游戏的状态。

第一点,是有哪些类型的状态?理论上如何获取?

一般而言,

无数值状态:使用简单的检色即可。(比如血条,只用检测绿色和黑色的比例)

有数值状态:使用OCR,个人而言,EasyOCR非常好用。其次是Tesseract的OCR。都是免费的。当然,对于一些色彩很奇怪的文字,OCR不那么好用的时候,引入yolo对10个数字做训练也是一种办法。

图像状态(比如怪物类型、标志物、物品栏识别):一般使用yolov8训练。选择Object Detection任务进行训练。

位置状态:非联网游戏用CE搜基址。联网游戏分几种情况:

对于有坐标的,那直接用CE的断call法,去尝试各个位置是否触发中断。

对于有小地图的,可以用二值图拼接+特征匹配。

对于都没有的,只能用我独家的光流法定位。这个我会在DeadMaze游戏的逆向中讲解。

第二点,是实战上如何获取?

对于CE能搜索到的,我们不做讨论。

问题是如何获取实时画面?

我们常用的方案会根据设备不同选择。

对于手游,我们会用模拟器,比如MuMu模拟器就会提供API来截图。

但是MuMu模拟器的截图速度是很低的,实测最高仅3fps,对于时间高精度要求的游戏是很糟糕的。

因此我们常用OBS Studio来实时获取游戏的视频流。可以达到60fps,对于所有游戏都是适用的。

电脑游戏一般也可以用OBS Studio来获取实时视频流。

算法层:

这一层就是如何做。

通常有两种模式,一种是基于规则的算法,是固定的。适用于简单,规则容易归纳的游戏(比如消消乐、可推理的扫雷、较为固定的跑酷)。要求动作空间小,观测空间小的游戏。

另一种是智能模式,是随动的,我常用强化学习。使用于较为复杂的,规则难以描述的游戏(大型MOBA、吃鸡、MC)。对于动作空间大、观测空间大的游戏。

操作层:

这一层就是具体如何控制游戏了。

对于手机游戏,一般都能用模拟器进行操作,我常用MuMu模拟器。当然也有一些游戏不支持模拟器,比如《我是市长》这类。之后的一切逆向工程,都会用MuMu的API来操作,你可以阅读

对于电脑游戏,基本都能调用win32的API进行后台控制。

实战

那么根据上述思路,我们就能做出如下架构

我们使用mumu模拟器的ADB来实现截图、模拟点击、滑动的Action。

首先要采集数据,也就是游戏中的图片,主角、敌人、以及障碍物,尽可能做到类别均衡,尤其是游戏中主角和敌人骑马的剪影很近似,最好拍300+以上便于区分。

使用下面的采集脚本,将ADB_EXE改为你的MuMu模拟器路径,TOTAL_IMAGES是截图的数量,你可以调大一些。

import os
import subprocess
from datetime import datetime
import time

# ========== 配置 ==========
PROJECT_DIR = r"E:\\Project\\GreatWallRun"
PIC_DIR = os.path.join(PROJECT_DIR, "pic")
ADB_EXE = r"D:\\工程\\MuMu Player 12\\nx_main\\adb.exe"
ADB_DEVICE = "emulator-5554"
TOTAL_IMAGES = 200

# 创建pic文件夹
os.makedirs(PIC_DIR, exist_ok=True)
print(f"图片将保存到: {PIC_DIR}")
print(f"目标采集数量: {TOTAL_IMAGES} 张")
print()

# ========== 截图函数 ==========
def capture_image(index):
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S_%f")[:-3]
# 文件名包含序号,方便排序
filename = f"img_{index:04d}_{timestamp}.png"
filepath = os.path.join(PIC_DIR, filename)

cmd = f'"{ADB_EXE}" -s {ADB_DEVICE} exec-out screencap -p > "{filepath}"'
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)

if result.returncode == 0:
return True
else:
print(f"❌ 截图失败: {result.stderr}")
return False

# ========== 主采集循环 ==========
def main():
print("=== 开始采集 200 张训练图片 ===")
print("💡 操作提示:")
print("1. 手动控制游戏,让画面出现不同场景")
print("2. 遇到障碍物、金币、烽火台时多停留")
print("3. 故意撞死几次,采集死亡画面")
print("4. 采集过程中不要关闭模拟器")
print("=" * 50)
print()

success_count = 0
fail_count = 0

for i in range(1, TOTAL_IMAGES + 1):
print(f"📸 正在采集第 {i}/{TOTAL_IMAGES} 张…", end=" ")

if capture_image(i):
success_count += 1
print("✅")
else:
fail_count += 1
print("❌")

# 每0.3秒截一张,给你留出时间手动操作游戏
time.sleep(0.3)

# 每10张显示一次进度
if i % 10 == 0:
print(f"📊 进度: {i}/{TOTAL_IMAGES} ({success_count}张成功, {fail_count}张失败)")
print("-" * 30)

print()
print("=" * 50)
print(f"🎉 采集完成!")
print(f"✅ 成功: {success_count} 张")
print(f"❌ 失败: {fail_count} 张")
print(f"📁 保存位置: {PIC_DIR}")
print("\\n💡 下一步:打开 labelImg 开始标注数据")

if __name__ == "__main__":
main()

采集完后,你可以让AI生成一个标注脚本,他会在你本地创建一个Label Studio接口,点击后跳转Label Studio页面,准备标注。

Label Stuidio标注数据

创建标注任务

填写任务名

导入数据

选择所有刚刚截图得到的数据,按照99张一批次导入,否则会报错

这样就是上传成功了

点击标注预设,选择物体检测和框选任务,然后保存。

在任务界面在这个Add label names填写需要的分类

horse
obstacle_wood
obstacle_fire
obstacle_bridge
coin
beacon
enemy
bird
soul

点击Add,并删除默认的Airplane和Car

点击Label All Tasks,开始标注

开始标注

可以通过键盘1-9来快速选定要标注的类别,然后在图中框选出来。

标注完一张中的所有出现的物体就submit

标注质量要求

标准要求
框的贴合度 框要紧贴物体边缘,不要外扩太多
切割标注 如果目标只有部分在屏幕内,也要标注
不漏标 所有可见的目标都要标(除非太小)

导出yolo数据

注意,如果之后修改了类别数量/对应关系等重新训练,要删除这个labels.cache.

如果出现这个就是说明正在训练,你可以吃午饭了。

训练结束了,我们看下效果

先看下原始的混淆矩阵:

主对角线就是分类正确的样本,可以发现基本都是正确的,金币由于数量很多,可能会有漏选。

由于我们的样本采集不是很均匀,因此要进行归一化,发现还是很不错的。

各项数据也收敛很稳定

我们挑几张验证效果看看

非常的Amazing啊!

烽火台、主角与马、火堆障碍、奖励都能识别出来

面对多个敌人的情况也能识别出来。

而且主角与马、敌人与马长得很类似,也能达到90%以上的置信度。

4

更令人震惊的是,我在标数据的时候,特地没有把坐上的coin选入(这是累计的coin显示,并不是游戏过程中的奖励,但他们长得几乎一样)yolo8也能完成分辨。

我们用视频验证一下:(依然替换为你的emulator号)

# 1. 开始录制(录到 sdcard,按 Ctrl+C 结束)

adb -s emulator-5554 shell screenrecord /sdcard/test.mp4

打一把游戏,然后按ctrl+c结束录制。

# 2. 结束录制后,导出到你的新文件夹

adb -s emulator-5554 pull /sdcard/test.mp4 E:\\Project\\GreatWallRun\\valification\\test_video.mp4

# 3. 删除手机里的临时文件

adb -s emulator-5554 shell rm /sdcard/test.mp4

就是成功了。

运行推理代码:

import cv2
from ultralytics import YOLO
import os

# ========== 路径配置 ==========
VIDEO_PATH = r"E:\\Project\\GreatWallRun\\valification\\test_video.mp4"
MODEL_PATH = r"E:\\Project\\GreatWallRun\\runs\\detect\\runs\\greatwall_run\\weights\\best.pt"
OUTPUT_PATH = r"E:\\Project\\GreatWallRun\\valification\\test_result.mp4"

# 检查文件是否存在
if not os.path.exists(VIDEO_PATH):
print(f"❌ 找不到视频文件: {VIDEO_PATH}")
exit()
if not os.path.exists(MODEL_PATH):
print(f"❌ 找不到模型文件: {MODEL_PATH}")
exit()

print("✅ 加载 YOLO 模型中…")
model = YOLO(MODEL_PATH)

# 打开视频
cap = cv2.VideoCapture(VIDEO_PATH)
fps = cap.get(cv2.CAP_PROP_FPS)
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))

print(f"📹 视频信息: {width}x{height}, {fps:.1f} FPS, 共 {total_frames} 帧")

# 准备输出视频
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(OUTPUT_PATH, fourcc, fps, (width, height))

frame_count = 0
print("🚀 开始逐帧推理(这可能需要几十秒,取决于视频长度)…")

while True:
ret, frame = cap.read()
if not ret:
break

frame_count += 1

# YOLO 推理(不输出 verbose 日志)
results = model(frame, verbose=False)

# 在画面上画框
annotated_frame = results[0].plot()

# 写入输出视频
out.write(annotated_frame)

# 每 10% 打印一次进度
if total_frames > 0 and frame_count % (total_frames // 10) == 0:
progress = int(frame_count / total_frames * 100)
print(f"⏳ 进度: {progress}% ({frame_count}/{total_frames})")

cap.release()
out.release()
print(f"✅ 验证完成!结果已保存到: {OUTPUT_PATH}")
print("👉 请用播放器打开这个视频,检查 YOLO 框是否准确、稳定。")

查看输出视频:发现是没问题的。

环境描述 OBS视频流实时获取

我们刚刚使用的是录制的视频,但打游戏时肯定要求是实时的。

我亲试了用ADB的快速截屏,延迟特别大,完全用不了

📊 统计结果(共 30 张):
🔹 最快: 0.501 秒
🔹 最慢: 0.695 秒
🔹 平均: 0.560 秒
🔹 理论极限 FPS: 1.8 FPS
🔹 相邻帧平均间隔: 0.002 秒

而且MUMU不提供视频流ADB。

因此我们考虑用OBS来获取实时视频流。

Open Broadcaster Software | OBS

下载,解压,启动。

点击左下角源的加号

选择“窗口采集”

随便起个名字

选择MUMU安卓设备

然后将获取的画面调整至合适的大小

点击“启动虚拟摄像机”

非常有效!

由于CNN处理简化画面更方便,因此我们需要把复杂的图片转换为简化的可视图作为输入,送给CNN

我们利用这个代码,将复杂的游戏画面转为纯色像素点:

debug_vision_pro.py

import cv2
import numpy as np
from ultralytics import YOLO
import time

# ========== 配置 ==========
MODEL_PATH = r"E:\\Project\\GreatWallRun\\runs\\detect\\runs\\greatwall_run\\weights\\best.pt"
OBS_CAM_ID = 1 # 你的 OBS 虚拟摄像头的索引(通常是 1)

# ———- 简化渲染的颜色配置 ———-
COLOR_HORSE = (0, 255, 0) # 绿色
COLOR_OBSTACLE = (0, 0, 255) # 红色
COLOR_COIN = (0, 215, 255) # 金色
COLOR_BEACON = (255, 0, 255) # 紫色
COLOR_ENEMY = (0, 165, 255) # 橙色
COLOR_BIRD = (255, 255, 0) # 青色
COLOR_SOUL = (128, 0, 128) # 深紫色
COLOR_BRIDGE = (255, 0, 0) # 蓝色

# 类别 ID 映射(依据你之前训练出的索引)
CLASS_IDS = {
'horse': 4,
'obstacle_fire': 6,
'obstacle_wood': 7,
'coin': 2,
'beacon': 5,
'enemy': 3,
'bird': 1,
'soul': 8,
'obstacle_bridge': 0 # 假设 bridge 索引是 0 (如果你没标桥就忽略)
}

print("加载 YOLO 模型中…")
model = YOLO(MODEL_PATH)
print("✅ YOLO 模型加载完成!")

# ========== 打开 OBS 虚拟摄像头 ==========
cap = cv2.VideoCapture(OBS_CAM_ID)
if not cap.isOpened():
print("❌ 无法打开 OBS 虚拟摄像头!")
print("请确保:1. OBS已开 2. 虚拟摄像头已启动")
exit()
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
print("✅ OBS 虚拟摄像头连接成功!")

# ========== 简化渲染函数 ==========
def render_simplified_frame(original_shape, horse, obstacles, coins, beacons, enemies, birds, souls, bridges):
"""
创建一个纯黑底的画布,根据坐标绘制简化的几何图形。
"""
h, w = original_shape[:2]
canvas = np.zeros((h, w, 3), dtype=np.uint8) # 纯黑背景

# 1. 绘制主角(用一个大的绿色圆点表示)
if horse:
hx, hy = horse[0] + horse[2]//2, horse[1] + horse[3]//2
cv2.circle(canvas, (hx, hy), 20, COLOR_HORSE, -1) # -1 表示实心填充
cv2.putText(canvas, "Player", (hx – 30, hy – 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, COLOR_HORSE, 1)

# 2. 绘制障碍物(红色矩形)
for obs in obstacles:
x1, y1, x2, y2 = obs
cv2.rectangle(canvas, (x1, y1), (x2, y2), COLOR_OBSTACLE, 2)
# 画一个红色的叉叉表示危险
cv2.line(canvas, (x1, y1), (x2, y2), COLOR_OBSTACLE, 1)
cv2.line(canvas, (x2, y1), (x1, y2), COLOR_OBSTACLE, 1)

# 3. 绘制金币(金色小圆点)
for coin in coins:
cx, cy = coin[0] + coin[2]//2, coin[1] + coin[3]//2
cv2.circle(canvas, (cx, cy), 5, COLOR_COIN, -1)

# 4. 绘制烽火台(紫色大框)
for beacon in beacons:
x1, y1, x2, y2 = beacon
cv2.rectangle(canvas, (x1, y1), (x2, y2), COLOR_BEACON, 2)

# 5. 绘制敌人(橙色圆点)
for enemy in enemies:
ex, ey = enemy[0] + enemy[2]//2, enemy[1] + enemy[3]//2
cv2.circle(canvas, (ex, ey), 15, COLOR_ENEMY, -1)

# 6. 绘制飞鸟(青色小点)
for bird in birds:
bx, by = bird[0] + bird[2]//2, bird[1] + bird[3]//2
cv2.circle(canvas, (bx, by), 4, COLOR_BIRD, -1)

# 7. 绘制 Soul(深紫色)
for soul in souls:
sx, sy = soul[0] + soul[2]//2, soul[1] + soul[3]//2
cv2.circle(canvas, (sx, sy), 10, COLOR_SOUL, -1)

# 8. 绘制桥(蓝色长条)
for bridge in bridges:
x1, y1, x2, y2 = bridge
cv2.rectangle(canvas, (x1, y1), (x2, y2), COLOR_BRIDGE, 2)

return canvas

# ========== 主循环(双窗口) ==========
print("🔴 按 'q' 键退出。将同时显示原图窗口和简化窗口。")

# 创建两个窗口
cv2.namedWindow("YOLO Live (OBS)", cv2.WINDOW_NORMAL)
cv2.namedWindow("Simplified View", cv2.WINDOW_NORMAL)

frame_count = 0
while True:
start_time = time.time()

ret, frame = cap.read()
if not ret:
time.sleep(0.05)
continue

# 1. YOLO 推理
results = model(frame, verbose=False, conf=0.5)

# 2. 按类别解析目标
horse = None
obstacles = []
coins = []
beacons = []
enemies = []
birds = []
souls = []
bridges = []

for box in results[0].boxes:
cls = int(box.cls[0])
x1, y1, x2, y2 = map(int, box.xyxy[0])

if cls == CLASS_IDS['horse']:
horse = (x1, y1, x2, y2)
elif cls in [CLASS_IDS['obstacle_fire'], CLASS_IDS['obstacle_wood']]:
obstacles.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['coin']:
coins.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['beacon']:
beacons.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['enemy']:
enemies.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['bird']:
birds.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['soul']:
souls.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['obstacle_bridge']:
bridges.append((x1, y1, x2, y2))

# 3. 生成原图标注画面(带距离线)
annotated = results[0].plot()
if horse and obstacles:
hx, hy = horse[0] + horse[2]//2, horse[1] + horse[3]//2
nearest_obs = None
min_dist = float('inf')
for obs in obstacles:
ox, oy = obs[0] + obs[2]//2, obs[1] + obs[3]//2
dist = ox – hx
if dist > 0 and dist < min_dist:
min_dist = dist
nearest_obs = obs
if nearest_obs:
cv2.putText(annotated, f"Dist: {int(min_dist)} px", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
ox, oy = nearest_obs[0] + nearest_obs[2]//2, nearest_obs[1] + nearest_obs[3]//2
cv2.line(annotated, (hx, hy), (ox, oy), (0, 255, 255), 2)
if horse:
cv2.putText(annotated, f"Horse Y: {horse[1]}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

# 4. 生成简化渲染画面
simplified = render_simplified_frame(
frame.shape, horse, obstacles, coins, beacons,
enemies, birds, souls, bridges
)

# 5. 显示两个窗口
cv2.imshow("YOLO Live (OBS)", annotated)
cv2.imshow("Simplified View", simplified)

if cv2.waitKey(1) & 0xFF == ord('q'):
break

frame_count += 1
elapsed = time.time() – start_time
if elapsed < 0.033:
time.sleep(0.033 – elapsed)

cap.release()
cv2.destroyAllWindows()
print(f"✅ 退出。共处理 {frame_count} 帧")

然后我们还需要绘制一个“地面”,地面的颜色是(33,33,33)和(33,36,33),我们把这一块填充为灰色:

修改debug_vision_pro.py

import cv2
import numpy as np
from ultralytics import YOLO
import time

# ========== 配置 ==========
MODEL_PATH = r"E:\\Project\\GreatWallRun\\runs\\detect\\runs\\greatwall_run\\weights\\best.pt"
OBS_CAM_ID = 1 # 你的 OBS 虚拟摄像头索引

# ———- 简化渲染的颜色配置 ———-
COLOR_HORSE = (0, 255, 0) # 绿色
COLOR_OBSTACLE = (0, 0, 255) # 红色
COLOR_COIN = (0, 215, 255) # 金色
COLOR_BEACON = (255, 0, 255) # 紫色
COLOR_ENEMY = (0, 165, 255) # 橙色
COLOR_BIRD = (255, 255, 0) # 青色
COLOR_SOUL = (128, 0, 128) # 深紫色
COLOR_BRIDGE = (255, 0, 0) # 蓝色
COLOR_GROUND_LINE = (100, 100, 100) # 灰色(用于画地形分界线,如果不想画线可以改成背景色)

# 定义地面的颜色范围(BGR 格式)
GROUND_COLOR_1 = (33, 33, 33) # 212421
GROUND_COLOR_2 = (33, 36, 33) # 212021

# 类别 ID 映射
CLASS_IDS = {
'horse': 4,
'obstacle_fire': 6,
'obstacle_wood': 7,
'coin': 2,
'beacon': 5,
'enemy': 3,
'bird': 1,
'soul': 8,
'obstacle_bridge': 0
}

print("加载 YOLO 模型中…")
model = YOLO(MODEL_PATH)
print("✅ YOLO 模型加载完成!")

# ========== 打开 OBS 虚拟摄像头 ==========
cap = cv2.VideoCapture(OBS_CAM_ID)
if not cap.isOpened():
print("❌ 无法打开 OBS 虚拟摄像头!请检查设置。")
exit()
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
print("✅ OBS 虚拟摄像头连接成功!")

# ========== 简化渲染函数(带地形提取) ==========
def render_simplified_frame(original_frame, horse, obstacles, coins, beacons, enemies, birds, souls, bridges):
h, w = original_frame.shape[:2]
# 1. 创建纯黑背景
canvas = np.zeros((h, w, 3), dtype=np.uint8)

# 2. 【新增】提取并绘制地面/地形
# 创建一个掩码,找出所有符合两个地面颜色的像素
# 允许有小范围的容差 (tol=5),防止因为压缩或光线导致的微小色差
tol = 5
lower_1 = np.array([max(0, c – tol) for c in GROUND_COLOR_1])
upper_1 = np.array([min(255, c + tol) for c in GROUND_COLOR_1])

lower_2 = np.array([max(0, c – tol) for c in GROUND_COLOR_2])
upper_2 = np.array([min(255, c + tol) for c in GROUND_COLOR_2])

mask1 = cv2.inRange(original_frame, lower_1, upper_1)
mask2 = cv2.inRange(original_frame, lower_2, upper_2)
ground_mask = cv2.bitwise_or(mask1, mask2)

# 将地面颜色直接填充到简化画布上(或者你可以直接把原图的地面像素复制过来)
canvas[ground_mask > 0] = original_frame[ground_mask > 0]

# 可选:绘制地面分界线(比如山脉边缘)
# 如果你只想看填色,把下面这两行注释掉即可
# 使用 Canny 边缘检测找到地面边缘,并用灰色画出轮廓
# edges = cv2.Canny(ground_mask, 50, 150)
# canvas[edges > 0] = COLOR_GROUND_LINE

# ———- 以下是绘制物体图标 ———-
# 3. 绘制主角
if horse:
hx, hy = horse[0] + horse[2]//2, horse[1] + horse[3]//2
cv2.circle(canvas, (hx, hy), 20, COLOR_HORSE, -1)
cv2.putText(canvas, "Player", (hx – 30, hy – 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, COLOR_HORSE, 1)

# 4. 绘制障碍物(红色矩形+叉叉)
for obs in obstacles:
x1, y1, x2, y2 = obs
cv2.rectangle(canvas, (x1, y1), (x2, y2), COLOR_OBSTACLE, 2)
cv2.line(canvas, (x1, y1), (x2, y2), COLOR_OBSTACLE, 1)
cv2.line(canvas, (x2, y1), (x1, y2), COLOR_OBSTACLE, 1)

# 5. 绘制金币
for coin in coins:
cx, cy = coin[0] + coin[2]//2, coin[1] + coin[3]//2
cv2.circle(canvas, (cx, cy), 5, COLOR_COIN, -1)

# 6. 绘制烽火台
for beacon in beacons:
x1, y1, x2, y2 = beacon
cv2.rectangle(canvas, (x1, y1), (x2, y2), COLOR_BEACON, 2)

# 7. 绘制敌人
for enemy in enemies:
ex, ey = enemy[0] + enemy[2]//2, enemy[1] + enemy[3]//2
cv2.circle(canvas, (ex, ey), 15, COLOR_ENEMY, -1)

# 8. 绘制飞鸟
for bird in birds:
bx, by = bird[0] + bird[2]//2, bird[1] + bird[3]//2
cv2.circle(canvas, (bx, by), 4, COLOR_BIRD, -1)

# 9. 绘制 Soul
for soul in souls:
sx, sy = soul[0] + soul[2]//2, soul[1] + soul[3]//2
cv2.circle(canvas, (sx, sy), 10, COLOR_SOUL, -1)

# 10. 绘制桥
for bridge in bridges:
x1, y1, x2, y2 = bridge
cv2.rectangle(canvas, (x1, y1), (x2, y2), COLOR_BRIDGE, 2)

return canvas

# ========== 主循环(双窗口) ==========
print("🔴 按 'q' 键退出。")
cv2.namedWindow("YOLO Live (OBS)", cv2.WINDOW_NORMAL)
cv2.namedWindow("Simplified View", cv2.WINDOW_NORMAL)

frame_count = 0
while True:
start_time = time.time()

ret, frame = cap.read()
if not ret:
time.sleep(0.05)
continue

# 1. YOLO 推理
results = model(frame, verbose=False, conf=0.5)

# 2. 按类别解析目标
horse = None
obstacles = []
coins = []
beacons = []
enemies = []
birds = []
souls = []
bridges = []

for box in results[0].boxes:
cls = int(box.cls[0])
x1, y1, x2, y2 = map(int, box.xyxy[0])

if cls == CLASS_IDS['horse']:
horse = (x1, y1, x2, y2)
elif cls in [CLASS_IDS['obstacle_fire'], CLASS_IDS['obstacle_wood']]:
obstacles.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['coin']:
coins.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['beacon']:
beacons.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['enemy']:
enemies.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['bird']:
birds.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['soul']:
souls.append((x1, y1, x2, y2))
elif cls == CLASS_IDS['obstacle_bridge']:
bridges.append((x1, y1, x2, y2))

# 3. 生成原图标注画面
annotated = results[0].plot()
if horse and obstacles:
hx, hy = horse[0] + horse[2]//2, horse[1] + horse[3]//2
nearest_obs = None
min_dist = float('inf')
for obs in obstacles:
ox, oy = obs[0] + obs[2]//2, obs[1] + obs[3]//2
dist = ox – hx
if dist > 0 and dist < min_dist:
min_dist = dist
nearest_obs = obs
if nearest_obs:
cv2.putText(annotated, f"Dist: {int(min_dist)} px", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 255), 2)
ox, oy = nearest_obs[0] + nearest_obs[2]//2, nearest_obs[1] + nearest_obs[3]//2
cv2.line(annotated, (hx, hy), (ox, oy), (0, 255, 255), 2)
if horse:
cv2.putText(annotated, f"Horse Y: {horse[1]}", (10, 60), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2)

# 4. 生成带地形的简化渲染画面
simplified = render_simplified_frame(
frame, horse, obstacles, coins, beacons,
enemies, birds, souls, bridges
)

# 5. 显示两个窗口
cv2.imshow("YOLO Live (OBS)", annotated)
cv2.imshow("Simplified View", simplified)

if cv2.waitKey(1) & 0xFF == ord('q'):
break

frame_count += 1
elapsed = time.time() – start_time
if elapsed < 0.033:
time.sleep(0.033 – elapsed)

cap.release()
cv2.destroyAllWindows()
print(f"✅ 退出。共处理 {frame_count} 帧")

OCR状态获取

这个游戏还有生命数量,箭矢数量,金币数量(没有中文,只有数字和对应的图标,因此应该要用OCR处理)这些信息。也需要用OCR获取到。

pip install pytesseract

Release v5.4.0.20240606 · UB-Mannheim/tesseract · GitHub

安装的时候可以在Additional language data把中文包加上。

你可以通过这个代码,查看识别的区域对不对,不对的话,你可以调整region中的值

import cv2
import numpy as np
from ultralytics import YOLO
import time
import pytesseract

# ========== 核心配置 ==========
pytesseract.pytesseract.tesseract_cmd = r"E:\\Tools\\tesseract\\tesseract.exe"

MODEL_PATH = r"E:\\Project\\GreatWallRun\\runs\\detect\\runs\\greatwall_run\\weights\\best.pt"
OBS_CAM_ID = 1

# ———- 地形颜色 ———-
GROUND_COLOR_1 = (33, 33, 33)
GROUND_COLOR_2 = (33, 36, 33)
COLOR_TOLERANCE = 10

# ———- 简化颜色 ———-
COLOR_HORSE = (0, 255, 0)
COLOR_OBSTACLE = (0, 0, 255)
COLOR_COIN = (0, 215, 255)

# 类别 ID
CLASS_IDS = {'horse': 4, 'obstacle_fire': 6, 'obstacle_wood': 7, 'coin': 2}

print("加载 YOLO 模型中…")
model = YOLO(MODEL_PATH)
print("✅ YOLO 模型加载完成!")

# ========== 打开 OBS ==========
cap = cv2.VideoCapture(OBS_CAM_ID)
if not cap.isOpened():
print("❌ OBS 摄像头未打开!")
exit()
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
print("✅ OBS 虚拟摄像头连接成功!")

# ========== 带绿色框的调试版 OCR ==========
def debug_read_ui_numbers(frame):
h, w = frame.shape[:2]
scale_x = w / 1280.0
scale_y = h / 720.0

# ⭐ 这里是 4 个区域的坐标 (X, Y, W, H)
# 前 3 个是你调好的,第 4 个是右上角"距离"的初始猜测位置
regions = [
[80, 60, 40, 40], # 生命 (左)
[230, 60, 40, 40], # 箭矢 (中)
[330, 60, 60, 40], # 金币 (右)
[930, 60, 90, 40] # 🟢 跑酷距离 (初始放在右上角,需要你调)
]

img_copy = frame.copy()
results_text = []

for i, (rx, ry, rw, rh) in enumerate(regions):
x = int(rx * scale_x)
y = int(ry * scale_y)
w = int(rw * scale_x)
h = int(rh * scale_y)

roi = frame[y:y+h, x:x+w]

# OCR 识别
gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
config = r'–psm 10 -c tessedit_char_whitelist=0123456789'
text = pytesseract.image_to_string(thresh, config=config).strip()
if not text.isdigit():
text = "?"
results_text.append(text)

# 🟢 画绿框
cv2.rectangle(img_copy, (x, y), (x+w, y+h), (0, 255, 0), 2)
# 框上方写识别结果
cv2.putText(img_copy, f"{text}", (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
# 框下方写坐标 (方便你调试)
cv2.putText(img_copy, f"[{rx},{ry}]", (x, y+h+20), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 1)

return img_copy, results_text

# ========== 主循环 ==========
print("🔴 按 'q' 键退出。")
print("👀 请观察画面上的 4 个绿色方框。")
print("📝 第 4 个框在右上角,请调整 regions 里的坐标把它对准数字!")

cv2.namedWindow("Debug OCR", cv2.WINDOW_NORMAL)

while True:
ret, frame = cap.read()
if not ret:
continue

# 运行调试版 OCR
debug_frame, texts = debug_read_ui_numbers(frame)

# 显示
cv2.imshow("Debug OCR", debug_frame)

if cv2.waitKey(1) & 0xFF == ord('q'):
break

cap.release()
cv2.destroyAllWindows()
print("✅ 已退出。")

经过调整,我确定识别区域是:

regions = [
[80, 60, 40, 40], # 生命 (左)
[230, 60, 40, 40], # 箭矢 (中)
[330, 60, 60, 40], # 金币 (右)
[930, 60, 90, 40] # 🟢 跑酷距离 (初始放在右上角,需要你调)
]

同步更新简化图的状态展示。

我们打一局,可以发现,状态是正确识别的!

接下来要解决的是效率问题,由于OCR,我们只能达到3fps了,下面这个debug_vision_v3.py就包含了OCR和Yolo8,你可以通过ENABLE_OCR来对比一下使用OCR与否对帧率的影响。

import cv2
import numpy as np
from ultralytics import YOLO
import time
import pytesseract

# ========== 核心配置 ==========
pytesseract.pytesseract.tesseract_cmd = r"E:\\Tools\\tesseract\\tesseract.exe"

MODEL_PATH = r"E:\\Project\\GreatWallRun\\runs\\detect\\runs\\greatwall_run\\weights\\best.pt"
OBS_CAM_ID = 1
SIDEBAR_WIDTH = 220

# =====================================================
# 🟢 性能微调开关:
ENABLE_OCR = True # True=启用, False=彻底关闭
OCR_INTERVAL = 30 # 每 N 帧执行一次 OCR(默认 5 帧约等于 6 次/秒)
# =====================================================

# ———- 地形颜色 ———-
GROUND_COLOR_1 = (33, 33, 33)
GROUND_COLOR_2 = (33, 36, 33)
COLOR_TOLERANCE = 10

# ———- 简化颜色 ———-
COLOR_HORSE = (0, 255, 0)
COLOR_OBSTACLE = (0, 0, 255)
COLOR_COIN = (0, 215, 255)

CLASS_IDS = {'horse': 4, 'obstacle_fire': 6, 'obstacle_wood': 7, 'coin': 2}

print("加载 YOLO 模型中…")
model = YOLO(MODEL_PATH)
print("✅ YOLO 模型加载完成!")

# ========== 打开 OBS ==========
cap = cv2.VideoCapture(OBS_CAM_ID)
if not cap.isOpened():
print("❌ OBS 摄像头未打开!")
exit()
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
print("✅ OBS 虚拟摄像头连接成功!")

# ========== OCR 读取(带频率控制) ==========
_ocr_frame_counter = 0
_cached_lives = "0"
_cached_arrows = "0"
_cached_coins = "0"
_cached_distance = "0"

def read_ui_numbers(frame):
global _ocr_frame_counter, _cached_lives, _cached_arrows, _cached_coins, _cached_distance
_ocr_frame_counter += 1

# 彻底关闭模式
if not ENABLE_OCR:
return "0", "0", "0", "0"

# 频率控制:还没到间隔时间,直接返回缓存值
if _ocr_frame_counter % OCR_INTERVAL != 0:
return _cached_lives, _cached_arrows, _cached_coins, _cached_distance

# 到达间隔时间,执行真正的 OCR
h, w = frame.shape[:2]
scale_x = w / 1280.0
scale_y = h / 720.0

regions = [
[80, 60, 40, 40], [230, 60, 40, 40],
[330, 60, 60, 40], [930, 60, 90, 40]
]

texts = []
for (rx, ry, rw, rh) in regions:
x = int(rx * scale_x)
y = int(ry * scale_y)
w = int(rw * scale_x)
h = int(rh * scale_y)

roi = frame[y:y+h, x:x+w]
if roi.size == 0:
texts.append("0")
continue

gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
_, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
config = r'–psm 10 -c tessedit_char_whitelist=0123456789'
text = pytesseract.image_to_string(thresh, config=config).strip()
texts.append(text if text.isdigit() else "0")

# 更新缓存
_cached_lives, _cached_arrows, _cached_coins, _cached_distance = \\
texts[0], texts[1], texts[2], texts[3] if len(texts) > 3 else "0"

return _cached_lives, _cached_arrows, _cached_coins, _cached_distance

# ========== 简化渲染 + 侧边栏 ==========
def render_simplified_with_sidebar(original_frame, horse, obstacles, coins, lives, arrows, coins_num, distance):
small_frame = cv2.resize(original_frame, (640, 360))
h, w = small_frame.shape[:2]

# 提取地面
canvas = small_frame.copy()
tol = COLOR_TOLERANCE
mask1 = cv2.inRange(small_frame, np.array([33-tol, 33-tol, 33-tol]), np.array([33+tol, 33+tol, 33+tol]))
mask2 = cv2.inRange(small_frame, np.array([33-tol, 36-tol, 33-tol]), np.array([33+tol, 36+tol, 33+tol]))
ground_mask = cv2.bitwise_or(mask1, mask2)
canvas[ground_mask == 0] = [0, 0, 0]

# 画物体
scale_w = 640 / original_frame.shape[1]
scale_h = 360 / original_frame.shape[0]

if horse:
hx, hy = int((horse[0] + horse[2]//2) * scale_w), int((horse[1] + horse[3]//2) * scale_h)
cv2.circle(canvas, (hx, hy), 12, COLOR_HORSE, -1)
for obs in obstacles:
x1, y1, x2, y2 = int(obs[0]*scale_w), int(obs[1]*scale_h), int(obs[2]*scale_w), int(obs[3]*scale_h)
cv2.rectangle(canvas, (x1, y1), (x2, y2), COLOR_OBSTACLE, 2)
for coin in coins:
cx, cy = int((coin[0] + coin[2]//2) * scale_w), int((coin[1] + coin[3]//2) * scale_h)
cv2.circle(canvas, (cx, cy), 4, COLOR_COIN, -1)

# 右侧侧边栏
sidebar = np.zeros((h, int(SIDEBAR_WIDTH * 0.5), 3), dtype=np.uint8)
y_offset = 40
line_height = 30

cv2.putText(sidebar, "STATUS", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 2)
cv2.putText(sidebar, f"Lives : {lives}", (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 2)
y_offset += line_height
cv2.putText(sidebar, f"Arrows : {arrows}", (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 0), 2)
y_offset += line_height
cv2.putText(sidebar, f"Coins : {coins_num}", (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 215, 255), 2)
y_offset += line_height
cv2.putText(sidebar, f"Distance: {distance}", (10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 2)

return np.hstack((canvas, sidebar))

# ========== 主循环 ==========
print("🔴 按 'q' 键退出。")
print(f"📟 OCR 状态: {'✅ 已启用' if ENABLE_OCR else '⛔ 已禁用'}")
print(f"📟 OCR 频率: 每 {OCR_INTERVAL} 帧执行一次")

cv2.namedWindow("YOLO Live", cv2.WINDOW_NORMAL)
cv2.namedWindow("Simplified + Sidebar", cv2.WINDOW_NORMAL)

frame_count = 0
while True:
start_time = time.time()

ret, frame = cap.read()
if not ret:
continue

# 1. YOLO 推理
yolo_input = cv2.resize(frame, (640, 360))
results = model(yolo_input, verbose=False, conf=0.5)

horse, obstacles, coins = None, [], []
for box in results[0].boxes:
cls = int(box.cls[0])
x1, y1, x2, y2 = map(int, box.xyxy[0])
scale = frame.shape[1] / 640.0
x1, y1, x2, y2 = int(x1 * scale), int(y1 * scale), int(x2 * scale), int(y2 * scale)

if cls == 4: horse = (x1, y1, x2, y2)
elif cls in [6, 7]: obstacles.append((x1, y1, x2, y2))
elif cls == 2: coins.append((x1, y1, x2, y2))

# 2. 读取数据(受 ENABLE_OCR 和 OCR_INTERVAL 控制)
lives, arrows, coins_num, distance = read_ui_numbers(frame)

# 3. 渲染原图
annotated = results[0].plot()
cv2.imshow("YOLO Live", cv2.resize(annotated, (960, 540)))

# 4. 渲染简化窗口
simplified = render_simplified_with_sidebar(frame, horse, obstacles, coins, lives, arrows, coins_num, distance)
cv2.imshow("Simplified + Sidebar", simplified)

if cv2.waitKey(1) & 0xFF == ord('q'):
break

cap.release()
cv2.destroyAllWindows()
print("✅ 已退出。")

我们考虑,图片单独更新,状态单独更新,这样不要把状态的数字拼接到图片里,而是放侧边栏,这样视频流就很流畅,虽然可能状态会有些延迟(合一起也有延迟)

  • 视频流(画面):只做纯粹的图像处理和显示,追求高帧率(40~60 FPS)。

  • 状态(OCR数据):单独在一个低频率的循环里跑,通过共享变量传给显示线程。

  • “流水线”是怎么工作的:

    组件速度延迟对画面的影响
    画面主循环 (YOLO) 30 FPS 极低 丝滑流畅
    后台 OCR 线程 3 次/秒 0.3 秒 状态字体偶尔闪烁更新,但不会卡住画面

    这样我们的状态描述就解耦了,完全可行!

    import cv2
    import numpy as np
    from ultralytics import YOLO
    import time
    import pytesseract
    import threading

    # ========== 核心配置 ==========
    pytesseract.pytesseract.tesseract_cmd = r"E:\\Tools\\tesseract\\tesseract.exe"

    MODEL_PATH = r"E:\\Project\\GreatWallRun\\runs\\detect\\runs\\greatwall_run\\weights\\best.pt"
    OBS_CAM_ID = 1
    SIDEBAR_WIDTH = 220

    # =====================================================
    ENABLE_OCR = True # True=启用
    OCR_INTERVAL_SECONDS = 0.3 # 每 0.3 秒跑一次 OCR (完全独立于画面帧率)
    # =====================================================

    # ———- 地形颜色 ———-
    GROUND_COLOR_1 = (33, 33, 33)
    GROUND_COLOR_2 = (33, 36, 33)
    COLOR_TOLERANCE = 10

    # ———- 简化颜色 ———-
    COLOR_HORSE = (0, 255, 0)
    COLOR_OBSTACLE = (0, 0, 255)
    COLOR_COIN = (0, 215, 255)

    CLASS_IDS = {'horse': 4, 'obstacle_fire': 6, 'obstacle_wood': 7, 'coin': 2}

    print("加载 YOLO 模型中…")
    model = YOLO(MODEL_PATH)
    print("✅ YOLO 模型加载完成!")

    # ========== 共享变量(用于线程间传递数据) ==========
    shared_lives = "0"
    shared_arrows = "0"
    shared_coins = "0"
    shared_distance = "0"

    # ========== 打开 OBS ==========
    cap = cv2.VideoCapture(OBS_CAM_ID)
    if not cap.isOpened():
    print("❌ OBS 摄像头未打开!")
    exit()
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
    print("✅ OBS 虚拟摄像头连接成功!")

    # ========== 独立 OCR 后台线程 ==========
    def ocr_thread_loop():
    global shared_lives, shared_arrows, shared_coins, shared_distance
    if not ENABLE_OCR:
    return

    while True:
    # 从 OBS 获取最新帧(只用来读 OCR,不影响显示线程)
    ret, frame = cap.read()
    if not ret:
    time.sleep(0.05)
    continue

    h, w = frame.shape[:2]
    scale_x = w / 1280.0
    scale_y = h / 720.0

    regions = [
    [80, 60, 40, 40], [230, 60, 40, 40],
    [330, 60, 60, 40], [930, 60, 90, 40]
    ]

    texts = []
    for (rx, ry, rw, rh) in regions:
    x = int(rx * scale_x)
    y = int(ry * scale_y)
    w = int(rw * scale_x)
    h = int(rh * scale_y)

    roi = frame[y:y+h, x:x+w]
    if roi.size == 0:
    texts.append("0")
    continue

    gray = cv2.cvtColor(roi, cv2.COLOR_BGR2GRAY)
    _, thresh = cv2.threshold(gray, 150, 255, cv2.THRESH_BINARY)
    config = r'–psm 10 -c tessedit_char_whitelist=0123456789'
    text = pytesseract.image_to_string(thresh, config=config).strip()
    texts.append(text if text.isdigit() else "0")

    # 更新共享变量
    if len(texts) >= 4:
    shared_lives, shared_arrows, shared_coins, shared_distance = texts[0], texts[1], texts[2], texts[3]
    else:
    shared_lives, shared_arrows, shared_coins, shared_distance = "0", "0", "0", "0"

    # 按照 OCR_INTERVAL_SECONDS 频率休眠
    time.sleep(OCR_INTERVAL_SECONDS)

    # ========== 启动 OCR 后台线程 ==========
    if ENABLE_OCR:
    ocr_thread = threading.Thread(target=ocr_thread_loop, daemon=True)
    ocr_thread.start()
    print(f"✅ OCR 后台线程已启动,频率: {1/OCR_INTERVAL_SECONDS:.1f} 次/秒")
    else:
    print("⛔ OCR 已禁用")

    # ========== 简化渲染 + 侧边栏(无拼接,纯画图) ==========
    def render_simplified_with_sidebar(original_frame, horse, obstacles, coins):
    # 1. 缩小画面加速处理
    small_frame = cv2.resize(original_frame, (640, 360))
    h, w = small_frame.shape[:2]

    # 2. 提取地面
    canvas = small_frame.copy()
    tol = COLOR_TOLERANCE
    mask1 = cv2.inRange(small_frame, np.array([33-tol, 33-tol, 33-tol]), np.array([33+tol, 33+tol, 33+tol]))
    mask2 = cv2.inRange(small_frame, np.array([33-tol, 36-tol, 33-tol]), np.array([33+tol, 36+tol, 33+tol]))
    ground_mask = cv2.bitwise_or(mask1, mask2)
    canvas[ground_mask == 0] = [0, 0, 0]

    # 3. 画物体
    scale_w = 640 / original_frame.shape[1]
    scale_h = 360 / original_frame.shape[0]

    if horse:
    hx, hy = int((horse[0] + horse[2]//2) * scale_w), int((horse[1] + horse[3]//2) * scale_h)
    cv2.circle(canvas, (hx, hy), 12, COLOR_HORSE, -1)
    for obs in obstacles:
    x1, y1, x2, y2 = int(obs[0]*scale_w), int(obs[1]*scale_h), int(obs[2]*scale_w), int(obs[3]*scale_h)
    cv2.rectangle(canvas, (x1, y1), (x2, y2), COLOR_OBSTACLE, 2)
    for coin in coins:
    cx, cy = int((coin[0] + coin[2]//2) * scale_w), int((coin[1] + coin[3]//2) * scale_h)
    cv2.circle(canvas, (cx, cy), 4, COLOR_COIN, -1)

    # 4. 构建侧边栏(这里不再用 hstack,直接把画面和黑边合在一起)
    combined = np.zeros((h, w + int(SIDEBAR_WIDTH * 0.5), 3), dtype=np.uint8)
    combined[:, :w] = canvas # 左侧放简化游戏图

    # 5. 在右侧画文字(使用共享变量)
    y_offset = 40
    line_height = 30
    cv2.putText(combined, "STATUS", (w + 10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (200, 200, 200), 2)
    cv2.putText(combined, f"Lives : {shared_lives}", (w + 10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 255, 0), 2)
    y_offset += line_height
    cv2.putText(combined, f"Arrows : {shared_arrows}", (w + 10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 0), 2)
    y_offset += line_height
    cv2.putText(combined, f"Coins : {shared_coins}", (w + 10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (0, 215, 255), 2)
    y_offset += line_height
    cv2.putText(combined, f"Distance: {shared_distance}", (w + 10, y_offset), cv2.FONT_HERSHEY_SIMPLEX, 0.4, (255, 255, 255), 2)

    return combined

    # ========== 主循环(纯画面流) ==========
    print("🔴 按 'q' 键退出。")
    print("🚀 画面流与状态流已彻底分离。画面将极其丝滑!")

    cv2.namedWindow("YOLO Live", cv2.WINDOW_NORMAL)
    cv2.namedWindow("Simplified + Sidebar", cv2.WINDOW_NORMAL)

    while True:
    ret, frame = cap.read()
    if not ret:
    continue

    # 1. YOLO 推理(只做画面处理)
    yolo_input = cv2.resize(frame, (640, 360))
    results = model(yolo_input, verbose=False, conf=0.5)

    horse, obstacles, coins = None, [], []
    for box in results[0].boxes:
    cls = int(box.cls[0])
    x1, y1, x2, y2 = map(int, box.xyxy[0])
    scale = frame.shape[1] / 640.0
    x1, y1, x2, y2 = int(x1 * scale), int(y1 * scale), int(x2 * scale), int(y2 * scale)

    if cls == 4: horse = (x1, y1, x2, y2)
    elif cls in [6, 7]: obstacles.append((x1, y1, x2, y2))
    elif cls == 2: coins.append((x1, y1, x2, y2))

    # 2. 渲染原图
    annotated = results[0].plot()
    cv2.imshow("YOLO Live", cv2.resize(annotated, (960, 540)))

    # 3. 渲染简化窗口(侧边栏纯粹读共享变量,无等待)
    simplified = render_simplified_with_sidebar(frame, horse, obstacles, coins)
    cv2.imshow("Simplified + Sidebar", simplified)

    if cv2.waitKey(1) & 0xFF == ord('q'):
    break

    cap.release()
    cv2.destroyAllWindows()
    print("✅ 已退出。")

    赞(0)
    未经允许不得转载:171主机测评 » 强化学习实战-用强化学习打跑酷游戏 GreatWallRun 第一节 感知层构建
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址