简介:这份资源面向人工智能、计算机科学与技术等相关专业的学生与开发者,提供一套使用OpenCV与TensorFlow实现游戏中车辆自动驾驶的完整项目源码,可用于毕业设计、课程作业或技术练手。项目围绕屏幕图像采集、道路线检测、数据收集与平衡、模型训练及测试等环节展开,目录中划分了数据采集、预处理、训练与测试等模块,便于理解自动驾驶从感知到决策的基本流程。压缩包共37个文件,以28个Python脚本为核心,辅以png图像、npy训练数据、txt说明、md文档及license等,整体约11.77MB,结构清晰、便于按模块查阅。目前已有79人学习关注。源码经过测试验证,下载后可先阅读README.md了解项目结构,适合作为计算机视觉与深度学习入门实践参考,仅限交流学习,请勿用于商业用途。
1. 从游戏画面到方向盘:OpenCV+TensorFlow 车辆自动驾驶到底在做什么
很多人第一次听到「用 OpenCV+TensorFlow 实现游戏中车辆的自动驾驶」,脑子里浮现的是 GTA5 里那辆自己跑高速的车。但真正动手做过的人知道,这件事的核心根本不是「自动驾驶」四个字,而是一条完整的感知-决策-控制链路:OpenCV 负责把屏幕画面变成模型能吃的数字,TensorFlow 负责从这些数字里学出「打多少方向」,最后再把预测结果送回游戏。它解决的是一个非常具体的问题——在虚拟环境里,让程序自己看着画面开车,不需要读内存、不需要注入进程、不需要游戏提供任何接口。
这套方案适合谁?适合想入门自动驾驶算法但买不起真车、搭不起 CARLA 集群的开发者;适合想学 OpenCV 图像处理和 TensorFlow 端到端回归的在校学生;也适合那些做游戏自动化测试、想用视觉方案替代固定脚本的工程师。它的门槛不高,一台带独显的普通电脑就能跑,但坑不少——分辨率、帧率、方向平滑、数据分布,每一个都能让你训练出来的模型在直道上画龙。下面我把这条链路拆开,从环境搭建到模型训练再到实机部署,把能复现的步骤和踩过的坑都讲清楚。
2. 环境搭建与数据采集:把游戏画面变成训练样本
2.1 OpenCV 和 TensorFlow 的安装选择
环境这一步就能劝退一批人。OpenCV 安装方式直接决定后面能不能正常读帧,TensorFlow 版本则决定你用的是 Keras 3 还是 tf.keras。我一般推荐下面这套组合,稳定且资料多:
# 创建独立环境,避免和系统 Python 冲突 conda create -n game_auto python=3.10 -y conda activate game_auto # 安装 OpenCV,用 opencv-python 而不是 headless # headless 版本没有 GUI,没法用 imshow 调试画面 pip install opencv-python==4.9.0.80 # 安装 TensorFlow,2.15 是最后一个原生支持 Keras 2 的稳定版 # 如果你用 2.16+,注意 tf.keras 和 Keras 3 的 API 差异 pip install tensorflow==2.15.0 # 采集阶段需要截屏和模拟按键 pip install mss pyautogui keyboard这里有几个参数值得说清楚。opencv-python和opencv-contrib-python的区别在于后者包含 SIFT、LSD 等专利算法,做车辆检测时如果要用特征点匹配就装 contrib 版。TensorFlow 2.15 之后tf.keras被独立成 Keras 3,很多旧教程里的model.fit(generator)写法会报错,新手建议锁 2.15。mss比 PIL.ImageGrab 快,采集 1080p 画面能到 60fps 以上,不会成为数据采集的瓶颈。
提示:如果你在 Windows 上遇到
ModuleNotFoundError: No module named 'cv2',九成是 conda 环境和 pip 装到了不同解释器,用python -c "import sys; print(sys.executable)"确认路径。
2.2 用 OpenCV 截取游戏画面并做预处理
采集脚本的核心逻辑是:截屏 → 裁剪感兴趣区域 → 缩放 → 存图并记录当前按键。不要直接把 1080p 原图喂给模型,显存吃不消,而且天空和 UI 区域全是噪声。
import cv2 import numpy as np import mss import keyboard import os import time SAVE_DIR = "dataset" os.makedirs(SAVE_DIR, exist_ok=True) # 感兴趣区域:只保留画面下方 40%,去掉天空和 HUD # 具体数值根据你的游戏调整,先用 mss 截全屏再手动量 ROI_TOP = 0.55 # 从画面 55% 高度开始 ROI_BOTTOM = 0.95 TARGET_W, TARGET_H = 200, 66 # 输入模型的尺寸 sct = mss.mss() monitor = sct.monitors[1] # 主显示器 frame_id = 0 while True: if keyboard.is_pressed('q'): break # 抓取全屏 img = np.array(sct.grab(monitor)) img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) h, w = img.shape[:2] # 裁剪 ROI roi = img[int(h * ROI_TOP):int(h * ROI_BOTTOM), :] # 缩放到模型输入尺寸 resized = cv2.resize(roi, (TARGET_W, TARGET_H)) # 转灰度可以减参数量,但会丢失车道线颜色信息 # 实测彩色输入在弯道表现更好,这里保留三通道 cv2.imwrite(f"{SAVE_DIR}/frame_{frame_id:06d}.jpg", resized) # 记录当前按键状态,作为标签 steer = 0 if keyboard.is_pressed('a'): steer = -1 elif keyboard.is_pressed('d'): steer = 1 with open(f"{SAVE_DIR}/labels.txt", "a") as f: f.write(f"frame_{frame_id:06d}.jpg {steer}\n") frame_id += 1 time.sleep(1 / 30) # 控制采集帧率,30fps 足够这段代码里ROI_TOP和ROI_BOTTOM是最需要调的参数。切太多,车道线被裁掉,模型学不到东西;切太少,天空和树木干扰训练。我的经验是先在游戏里截一张图,用画图工具量出路面占据的像素范围,再换算成比例。TARGET_W和TARGET_H建议保持 3:1 左右的宽高比,因为路面信息是横向分布的,压成正方形反而丢信息。
采集时还有一个血泪经验:不要只采直道。很多人图省事在直道上跑十分钟,结果模型一进弯就冲出去。正确做法是直道、左弯、右弯、减速、加速各采 3-5 分钟,且保证左右转向样本数量接近。如果左转样本是右转的三倍,模型会默认往左打方向,这就是典型的分布偏移。
2.3 数据增强与标签平衡
采完数据别急着训练,先看一眼标签分布。用下面这段脚本统计:
import collections labels = [] with open("dataset/labels.txt") as f: for line in f: _, steer = line.strip().split() labels.append(int(steer)) counter = collections.Counter(labels) print("左转:", counter[-1], "直行:", counter[0], "右转:", counter[1])如果某一类占比超过 60%,就得做增强。对图像做水平翻转的同时把标签取反,这是最自然的增强方式:
import cv2 def augment_flip(img_path, steer): img = cv2.imread(img_path) flipped = cv2.flip(img, 1) # 水平翻转 return flipped, -steer # 标签同步取反注意亮度增强和噪声增强对转向预测帮助不大,反而可能让模型学到无关特征。翻转是唯一不会引入语义错误的增强方式,因为路面左右对称。另外,采集时如果游戏有动态天气,尽量在晴天采集,雨天画面模糊会拉低模型上限。
3. 端到端模型设计:用 TensorFlow 从图像直接回归转向角
3.1 为什么选回归而不是分类
转向预测有两种建模方式:分类是把转向角离散成若干档,回归是直接输出连续值。分类的好处是训练稳定,坏处是输出不连续,车辆会一顿一顿地抖。回归输出平滑,但对损失函数和归一化更敏感。游戏自动驾驶场景里路面连续变化,我一般选回归,配合 MSE 损失。
网络结构不需要太深。NVIDIA 那篇端到端自动驾驶论文里的结构就够用:卷积层提取特征,全连接层回归转向值。输入 200x66x3,输出一个标量。
import tensorflow as tf from tensorflow.keras import layers, models def build_model(input_shape=(66, 200, 3)): model = models.Sequential([ # 归一化层,把 0-255 映射到 -1 到 1 layers.Rescaling(1./127.5, offset=-1, input_shape=input_shape), layers.Conv2D(24, (5, 5), strides=(2, 2), activation='relu'), layers.Conv2D(36, (5, 5), strides=(2, 2), activation='relu'), layers.Conv2D(48, (5, 5), strides=(2, 2), activation='relu'), layers.Conv2D(64, (3, 3), activation='relu'), layers.Conv2D(64, (3, 3), activation='relu'), layers.Flatten(), layers.Dropout(0.3), # 防止过拟合,游戏画面重复度高 layers.Dense(100, activation='relu'), layers.Dense(50, activation='relu'), layers.Dense(10, activation='relu'), layers.Dense(1) # 回归输出,不加激活函数 ]) model.compile(optimizer=tf.keras.optimizers.Adam(1e-3), loss='mse', metrics=['mae']) return model model = build_model() model.summary()几个关键参数说明。Rescaling层把像素归一化,比在数据管道里做更省事,而且会跟着模型一起保存,部署时不用再写预处理。Dropout(0.3)是必须的,因为游戏画面帧间差异小,模型很容易记住训练集。最后一层Dense(1)不能加激活函数,加了会把输出限制在固定范围,转向角就学不准了。
3.2 数据管道与训练参数
数据量上千张之后不能一次性读进内存,用tf.data做流式加载:
import tensorflow as tf IMG_SIZE = (66, 200) BATCH_SIZE = 32 def load_and_preprocess(img_path, steer): img = tf.io.read_file(img_path) img = tf.image.decode_jpeg(img, channels=3) img = tf.image.resize(img, IMG_SIZE) img = tf.cast(img, tf.float32) return img, steer def make_dataset(label_file, batch_size=BATCH_SIZE): paths, steers = [], [] with open(label_file) as f: for line in f: p, s = line.strip().split() paths.append("dataset/" + p) steers.append(float(s)) ds = tf.data.Dataset.from_tensor_slices((paths, steers)) ds = ds.map(load_and_preprocess, num_parallel_calls=tf.data.AUTOTUNE) ds = ds.shuffle(1000).batch(batch_size).prefetch(tf.data.AUTOTUNE) return ds train_ds = make_dataset("dataset/labels.txt") # 学习率调度:前 5 轮热身,之后余弦衰减 lr_schedule = tf.keras.optimizers.schedules.CosineDecay( initial_learning_rate=1e-3, decay_steps=10000, alpha=0.1 ) model.compile(optimizer=tf.keras.optimizers.Adam(lr_schedule), loss='mse', metrics=['mae']) history = model.fit(train_ds, epochs=30, validation_split=0.2) model.save("steer_model.h5")shuffle(1000)的缓冲区要大于批次大小,否则打乱不充分。prefetch(AUTOTUNE)让 CPU 预处理和 GPU 训练重叠,能提升 20% 左右的吞吐。学习率用余弦衰减比固定值好,前期快速下降,后期微调。训练轮数不用太多,30 轮足够,再多就过拟合了——判断标准是验证集 loss 开始上升。
注意:如果你用 TensorFlow 2.16+,
model.save("steer_model.h5")会警告 H5 格式过时,改用model.save("steer_model.keras")。加载时对应改tf.keras.models.load_model("steer_model.keras")。
3.3 推理部署:把预测结果送回游戏
训练完的模型要实时跑,推理速度必须跟上。用model.predict单帧调用会有几百毫秒延迟,正确做法是把模型转成 TensorFlow Lite 或者用tf.function固化计算图:
import cv2 import numpy as np import mss import pyautogui import tensorflow as tf model = tf.keras.models.load_model("steer_model.h5") # 用 tf.function 固化,第一次调用后速度提升明显 @tf.function(input_signature=[tf.TensorSpec(shape=[1, 66, 200, 3], dtype=tf.float32)]) def infer(x): return model(x, training=False) sct = mss.mss() monitor = sct.monitors[1] while True: img = np.array(sct.grab(monitor)) img = cv2.cvtColor(img, cv2.COLOR_BGRA2BGR) h, w = img.shape[:2] roi = img[int(h * 0.55):int(h * 0.95), :] resized = cv2.resize(roi, (200, 66)) inp = resized.astype(np.float32)[np.newaxis, ...] steer = infer(tf.constant(inp)).numpy()[0][0] # 死区:小于 0.1 的方向不按,避免直道画龙 if steer < -0.1: pyautogui.keyDown('a') pyautogui.keyUp('d') elif steer > 0.1: pyautogui.keyDown('d') pyautogui.keyUp('a') else: pyautogui.keyUp('a') pyautogui.keyUp('d')死区阈值 0.1 是调出来的。太小,模型输出的微小波动会让车左右抖;太大,弯道反应迟钝。不同游戏手感不一样,建议从 0.05 开始试。另外pyautogui的按键有延迟,追求极致可以用keyboard库或者直接调 Windows API,但一般游戏 30fps 下 pyautogui 够用。
4. 避坑与排查:模型训练和实机部署中最容易翻车的五件事
4.1 模型在直道上左右画龙
现象:车在直道上方向盘来回小幅摆动,坐起来像醉驾。
原因:训练数据里直道样本的转向标签不是精确的 0,而是人手操作时的 ±0.05 抖动,模型学到了这种噪声。另外推理时没有做输出平滑。
解决:训练前把 |steer| < 0.1 的样本标签统一置 0;推理时加滑动平均,用最近 5 帧的预测均值作为当前转向:
from collections import deque steer_history = deque(maxlen=5) steer_history.append(steer) smooth_steer = sum(steer_history) / len(steer_history)4.2 进弯道直接冲出赛道
现象:直道跑得好好的,一到弯道就直直撞墙。
原因:九成是训练集里弯道样本太少,模型没见过大转向角。少数情况是 ROI 裁剪把弯道内侧车道线裁掉了。
解决:先统计标签分布,如果 |steer| > 0.5 的样本占比低于 15%,回去补采弯道数据。同时检查 ROI 区域,确保画面里能看到至少两条车道线。补采时故意在弯道多跑几圈,让模型见过足够的极端情况。
4.3 推理帧率跟不上游戏帧率
现象:模型预测没问题,但车反应慢半拍,高速时尤其明显。
原因:model.predict每次调用都重建计算图,单帧耗时可能超过 100ms。或者截图用了 PIL.ImageGrab,1080p 下只有 10fps。
解决:用tf.function固化推理函数,截图换mss。如果还慢,把模型转成 TFLite:
converter = tf.lite.TFLiteConverter.from_keras_model(model) tflite_model = converter.convert() with open("steer_model.tflite", "wb") as f: f.write(tflite_model)TFLite 在 CPU 上也能跑到 60fps,代价是精度略降,一般掉 5% 以内。
4.4 换了分辨率或窗口模式就失效
现象:全屏采集训练的数据,换成窗口模式后模型完全不会开车。
原因:ROI 裁剪比例是按全屏算的,窗口模式下画面尺寸变了,裁出来的区域根本不是路面。
解决:采集和推理必须用同一种显示模式、同一分辨率。如果非要换,重新标定 ROI 比例,或者用 OpenCV 的边缘检测自动找路面区域。最省事的办法是训练和部署都锁死 1920x1080 全屏。
4.5 损失降到很低但实车表现很差
现象:验证集 MSE 只有 0.01,但一上路就撞。
原因:数据泄漏。采集时连续帧高度相似,随机划分训练集和验证集时,相邻帧被分到两边,验证集等于在考训练集见过的画面。
解决:按时间段划分,前 80% 时间采集的数据做训练,后 20% 做验证。或者每隔 10 帧取 1 帧做验证集,确保验证帧和训练帧在时间上隔开。这个坑最隐蔽,损失曲线好看得让你以为成了,实际上路就翻车。
5. 从能跑到好用:三个提升稳定性的进阶技巧
模型能跑起来只是第一步,真正让它稳定开完一圈,还得在细节上磨。第一个技巧是多尺度输入训练。游戏里车辆远近变化大,固定尺寸输入对远处小目标不敏感。可以在数据管道里随机把图像缩放到 0.8 到 1.2 倍再裁回原尺寸,让模型适应不同距离的路面。这个增强对弯道入弯判断提升明显,实测弯道冲出率能降三成。
第二个技巧是加入历史帧。单帧图像没有速度信息,模型不知道车开多快,过弯时容易转向不足。把最近 3 帧堆叠成 9 通道输入,模型就能从画面变化里推断速度。改法很简单,把Conv2D的input_shape改成(66, 200, 9),数据管道里用tf.data.Dataset.window做帧堆叠。代价是推理耗时增加约 40%,但稳定性提升值得。
第三个技巧是用 OpenCV 做车道线辅助校验。模型输出转向角的同时,用 Canny 边缘检测加霍夫变换提取车道线,如果模型预测的转向方向和车道线曲率矛盾,就以车道线为准。这不是替代模型,而是给模型加一层安全兜底。代码不复杂:
def lane_check(frame, model_steer): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) edges = cv2.Canny(gray, 50, 150) lines = cv2.HoughLinesP(edges, 1, np.pi/180, 50, minLineLength=50, maxLineGap=10) if lines is None: return model_steer # 没检测到车道线,信任模型 # 计算车道线平均斜率,判断弯道方向 slopes = [(y2-y1)/(x2-x1+1e-6) for x1,y1,x2,y2 in lines[:,0]] avg_slope = np.mean(slopes) if avg_slope > 0.3 and model_steer < 0: return 0.2 # 车道线说右弯,模型说左转,强制修正 return model_steer这个兜底逻辑在模型没见过的场景下救过我好几次。参数minLineLength和maxLineGap要根据分辨率调,1080p 下 50 和 10 比较合适,720p 下减半。
最后说一个习惯:每次改完模型或数据,先在训练集上跑一遍看 loss 能不能降到 0.01 以下。降不下去说明模型容量不够或者数据有问题,别急着上路。降下去了再在验证集上看,验证集 loss 是训练集的 1.5 倍以内才算没严重过拟合。这套流程帮我省了很多次「训练三小时,上路三秒撞」的后悔药。希望帮到你。
本文还有配套的精品资源,点击获取