NBA球星知识大挑战:基于 PyQt5 的球星认识小游戏
代码详见:https://github.com/xiaozhou-alt/NBA_Players_Recognition
文章目录
关于识别球星的模型训练和数据集获取与讲解请详见:当库里遇上卷积神经网络:基于 EfficientNetV2 的NBA球星分类
一、项目介绍
项目使用深度学习技术对NBA球星图像进行分类。项目基于TensorFlow实现,采用迁移学习策略,使用预训练的 EfficientNetV2L 模型作为基础架构,并添加了自定义的通道注意力机制。整个训练过程分为两个阶段:第一阶段冻结基础模型训练分类头,第二阶段解冻顶层进行微调优化;同时结合UI界面,将球星识别系统设计了一个球星识别小游戏,测试用户对NBA球星的了解程度
项目亮点:
- 实现加权Top-3准确率评估指标,更符合实际应用场景
- 采用两阶段训练策略提高模型性能
- 集成通道注意力机制增强特征提取能力
- 使用余弦衰减+预热的学习率调度策略
- 现代化UI设计:采用渐变背景、动画按钮和流畅交互
- 资源优化:支持相对路径访问,便于打包分发
- 跨平台兼容:可在Windows、macOS和Linux系统运行
此项目承接自:当库里遇上卷积神经网络:基于 EfficientNetV2 的NBA球星分类,数据集的获取方法详见:NBA 60位全明星球员图片数据集(ScienceDB)
二、文件夹结构
NBA/
├── assets/ # 静态资源文件夹
├── data/ # 球员图片数据集
└── Allen_Iverson/ # 艾弗森图片(示例,共60位NBA球星)
├── 1.png # 球员图片(命名格式为数字)
└── ... # 每个球员约300-400张图片
├── log/ # 日志目录
├── output/ # 输出目录
├── model/ # 训练好的模型
└── pic/ # 生成的图片
├── README.md
├── build.spec # PyInstaller打包配置
├── class.txt # 分类标签
├── data.ipynb
├── demo.py # 主程序(带GUI的识别系统)
├── demo.mp4 # 演示视频
├── predict.py # 预测脚本
├── requirements.txt
└── train.py # 训练脚本
三、项目实现
1. 自定义动画按钮(AnimatedButton)
这个自定义按钮实现了:
- 设置按钮的初始样式(深蓝色背景)
- 鼠标 悬停 时改变为悬停样式(浅蓝色背景)
- 使用属性动画实现 高度变化 的动画效果
- 设置鼠标指针为手形,增强用户体验
class AnimatedButton(QPushButton):
"""带有悬停动画的按钮"""
def __init__(self, text, parent=None):
super().__init__(text, parent)
self.setFont(QFont("Arial", 14, QFont.Bold))
self.setMinimumHeight(50)
self.setCursor(Qt.PointingHandCursor)
# 初始样式
self.normal_style = """
background-color: #1e3c72;
color: white;
border: 2px solid #2a5298;
border-radius: 25px;
padding: 10px 20px;
"""
self.hover_style = """
background-color: #2a5298;
color: white;
border: 2px solid #3a6bc4;
border-radius: 25px;
padding: 10px 20px;
"""
self.setStyleSheet(self.normal_style)
def enterEvent(self, event):
# 鼠标进入时动画
...
def leaveEvent(self, event):
# 鼠标离开时动画
...
def animate_size(self, start, end):
# 创建尺寸动画
...
如下是一个简单按键动画展示:
2. 渐变背景组件(GradientWidget)
这个组件实现了:
- 定义四种颜色(深蓝、中蓝、浅蓝和红色)
- 在
paintEvent
中创建线性渐变 - 使用四种颜色创建从左上到右下的 渐变 效果
- 填充整个组件区域,为应用提供美观的背景
class GradientWidget(QWidget):
"""带有渐变背景的组件"""
def __init__(self, parent=None):
super().__init__(parent)
self.color1 = QColor(30, 60, 114) # 深蓝色
self.color2 = QColor(42, 82, 152) # 中蓝色
self.color3 = QColor(58, 107, 196) # 浅蓝色
self.color4 = QColor(142, 45, 65) # 红色(NBA主题)
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# 创建渐变
...
3. 主应用类(NBAApp)
这是应用的主类:
- 继承自
QMainWindow
,作为主窗口 - 设置全局字体和应用标题、大小
- 初始化球员图像字典
- 创建堆叠窗口管理多个页面
- 初始化所有页面(主菜单、识别页、游戏页等)
- 加载模型和资源
- 显示主菜单页面
class NBAApp(QMainWindow):
def __init__(self):
super().__init__()
# 设置全局字体
font = QFont("Times New Roman", 12)
QApplication.setFont(font)
# 应用设置
self.setWindowTitle("NBA球星识别系统")
self.setGeometry(100, 100, 1000, 750)
# 初始化球员图像字典
self.player_images = {} # 修复:添加初始化
# 创建主堆叠窗口
self.stacked_widget = QStackedWidget()
self.setCentralWidget(self.stacked_widget)
# 创建各页面
self.main_menu = self.create_main_menu()
...
# 加载模型和类别映射
self.load_resources()
# 显示主菜单
self.stacked_widget.setCurrentIndex(0)
4. 加载资源
- 加载预训练的深度学习模型(包含自定义注意力层)
- 加载类别映射文件(JSON 格式)
- 从类别映射中提取球员名称列表(替换下划线为空格)
- 调用
load_player_images
方法加载球员图像
def load_resources(self):
"""加载模型和类别映射"""
try:
# 加载模型 - 使用相对路径
model_path = self.resource_path("output/model/best_model_phase2.h5")
if os.path.exists(model_path):
self.model = tf.keras.models.load_model(
model_path,
custom_objects={'ChannelAttention': ChannelAttention},
compile=False
)
print("✅ 模型加载成功")
else:
print(f"❌ 模型文件不存在: {model_path}")
# 加载类别映射
mapping_path = self.resource_path("output/class_mapping.json")
if os.path.exists(mapping_path):
with open(mapping_path, 'r') as f:
self.class_mapping = json.load(f)
print("✅ 类别映射加载成功")
# 获取球员列表(将下划线替换为空格)
self.player_names = [
name.replace('_', ' ')
for name in self.class_mapping['class_to_index'].keys()
]
else:
print(f"❌ 类别映射文件不存在: {mapping_path}")
# 加载球员图像(用于小游戏)
self.load_player_images()
except Exception as e:
print(f"❌ 资源加载失败: {e}")
球员姓名映射文件(class_mapping.json
):
{
“class_to_index”: {
“Allen_Iverson”: 0,
…
“Wilt_Chamberlain”: 59
},
“index_to_class”: {
“0”: “Allen_Iverson”,
…
“59”: “Wilt_Chamberlain”
}
}
5. 加载球员图像
这个方法加载球员图像用于小游戏:
- 检查球员名称列表是否有效
- 获取
data
文件夹路径 - 遍历
data
文件夹下的每个球员文件夹 - 提取球员名称(替换下划线为空格)
- 收集该球员的所有图像文件路径
- 将球员名称和图像路径列表存储到字典中
def load_player_images(self):
"""加载球员图像(用于小游戏)"""
if not hasattr(self, 'player_names') or not self.player_names:
print("⚠️ 球员名称列表未初始化或为空")
return
# 球员文件夹路径
nba_dir = self.resource_path("data")
if not os.path.exists(nba_dir):
print(f"❌ data文件夹不存在: {nba_dir}")
return
# 确保 player_images 字典已初始化
if not hasattr(self, 'player_images'):
self.player_images = {}
print("ℹ️ player_images 字典已初始化")
for player_folder in os.listdir(nba_dir):
player_path = os.path.join(nba_dir, player_folder)
if os.path.isdir(player_path):
# 获取球员名称(替换下划线)
player_name = player_folder.replace('_', ' ')
# 获取该球员的所有图像
images = [
os.path.join(player_path, img)
for img in os.listdir(player_path)
if img.lower().endswith(('.png', '.jpg', '.jpeg'))
]
if images:
self.player_images[player_name] = images
球员图片文件夹格式样例:
6. 创建主菜单页面
- 使用渐变背景组件
- 创建标题和副标题
- 添加三个功能按钮(球星识别、小游戏、退出)
- 添加篮球装饰图片
- 使用垂直布局组织所有元素
- 通过
Stretch
实现元素居中效果
def create_main_menu(self):
"""创建主菜单页面"""
widget = GradientWidget()
layout = QVBoxLayout(widget)
# ... (布局设置)
# 标题区域
title_frame = QFrame()
# ... (样式设置)
title_layout = QVBoxLayout(title_frame)
# 标题
title = QLabel("NBA球星识别系统")
# ... (字体和样式设置)
# 副标题
subtitle = QLabel("探索篮球传奇,认识超级球星")
# ... (样式设置)
title_layout.addWidget(title)
title_layout.addWidget(subtitle)
# 按钮容器
button_frame = QFrame()
# ... (样式设置)
button_layout = QVBoxLayout(button_frame)
# 按钮
btn_recognition = AnimatedButton("球星识别")
...
# 添加篮球装饰
basketball_label = QLabel()
pixmap = QPixmap(self.resource_path("assets/basketball.png"))
# ... (加载和缩放图片)
# 添加组件
layout.addStretch(1)
...
return widget
最终的主界面布局如下所示:
7. 图像上传与识别功能
upload_image
:打开文件对话框选择图片,显示在界面上preprocess_image
:预处理图像(调整大小、处理通道、归一化)recognize_player
:使用模型进行预测,显示 T o p 3 Top3 Top3 结果
def upload_image(self):
"""上传图片"""
file_path, _ = QFileDialog.getOpenFileName(
self, "选择图片", "",
"图片文件 (*.png *.jpg *.jpeg)"
)
if file_path:
# 显示图片
pixmap = QPixmap(file_path)
if not pixmap.isNull():
# 缩放图片以适应标签
...
def recognize_player(self):
"""识别球星"""
if not hasattr(self, 'current_image_path') or not self.model:
self.recog_result_label.setText("请先上传图片或等待模型加载完成")
return
try:
# 添加加载提示
self.recog_result_label.setText("正在识别中,请稍后...(第一次加载需要较长时间哦)")
QApplication.processEvents() # 强制刷新UI
# 预处理图像
processed_img, original_img = self.preprocess_image(self.current_image_path)
# 进行预测
predictions = self.model.predict(processed_img)[0]
# 获取top-3预测结果
top_indices = np.argsort(predictions)[::-1][:3]
top_indices = [int(idx) for idx in top_indices]
# 获取球员名称和概率
top_players = [
self.class_mapping['index_to_class'][str(idx)].replace('_', ' ')
for idx in top_indices
]
top_probs = predictions[top_indices]
# 构建结果字符串
result_text = "🏀 识别结果:\n\n"
for i, (player, prob) in enumerate(zip(top_players, top_probs)):
result_text += f"{i+1}. {player}: {prob*100:.2f}%\n"
self.recog_result_label.setText(result_text)
except Exception as e:
self.recog_result_label.setText(f"⚠️ 识别失败: {str(e)}")
def preprocess_image(self, image_path, target_size=(300, 300)):
"""预处理图像用于模型预测"""
img = Image.open(image_path)
# 保留原始图像用于显示
original_img = img.copy()
# 调整大小为模型输入尺寸
img = img.resize(target_size)
img_array = np.array(img)
# 处理图像通道
if len(img_array.shape) == 2: # 灰度图
img_array = np.stack((img_array,) * 3, axis=-1)
elif img_array.shape[2] == 4: # RGBA转RGB
img_array = img_array[..., :3]
img_array = img_array.astype('float32') / 255.0
return np.expand_dims(img_array, axis=0), original_img
8. 球星识别页面
添加返回按钮和页面标题、创建图像显示区域、添加上传和识别按钮、设置结果标签用于显示识别结果、使用垂直布局组织所有元素、
def create_recognition_page(self):
"""创建球星识别页面"""
widget = GradientWidget()
# ... (布局设置)
# 标题栏
header = QWidget()
header_layout = QHBoxLayout(header)
# 返回按钮
btn_back = AnimatedButton("返回")
...
# 标题
title = QLabel("球星识别")
# ... (样式设置)
# 主内容区域
content = QWidget()
content_layout = QVBoxLayout(content)
# 图像显示区域
self.recog_image_label = QLabel()
# ... (样式设置)
# 按钮容器
button_container = QWidget()
button_layout = QHBoxLayout(button_container)
# 上传按钮
btn_upload = AnimatedButton("上传图片")
...
# 识别按钮
btn_recognize = AnimatedButton("识别球星")
...
# 结果区域
self.recog_result_label = QLabel("上传图片后点击识别按钮")
# ... (样式设置)
# 添加装饰
decoration = QLabel()
pixmap = QPixmap(self.resource_path("assets/nba_logo.png"))
# ... (加载和缩放图片)
# 添加组件
...
return widget
最终的球星识别页面布局如下所示:
9. 球星认识小游戏界面
- 添加返回按钮和页面标题
- 创建游戏说明标签
- 添加难度选择单选按钮(简单(5)、中等(10)、困难(20))
- 使用垂直布局组织所有元素
def create_game_page(self):
"""创建小游戏页面"""
widget = GradientWidget()
# ... (布局设置)
# 标题栏
header = QWidget()
header_layout = QHBoxLayout(header)
# 返回按钮
btn_back = AnimatedButton("返回")
btn_back.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(0))
# 标题
title = QLabel("球星认识小游戏")
# ... (样式设置)
header_layout.addWidget(btn_back)
header_layout.addWidget(title)
header_layout.addStretch()
# 主内容区域
content = QWidget()
content_layout = QVBoxLayout(content)
# 游戏说明
instruction = QLabel("测试你对NBA球星的了解程度!\n选择难度级别,开始挑战吧!")
# ... (样式设置)
# 游戏选项区域
group = QGroupBox("选择游戏难度")
# ... (样式设置)
group_layout = QHBoxLayout(group)
...
# 设置单选按钮样式
# ... (样式设置)
...
# 开始游戏按钮
btn_start = AnimatedButton("开始游戏")
btn_start.setIcon(QIcon(self.resource_path("assets/start_icon.png")))
btn_start.clicked.connect(self.start_game)
# 添加装饰
trophy_label = QLabel()
pixmap = QPixmap(self.resource_path("assets/trophy.png"))
# ... (加载和缩放图片)
# 添加内容
...
# 添加组件
...
return widget
最终小游戏页面布局如下所示:
10. 游戏问题界面
- 添加返回按钮和动态标题(显示当前问题)
- 创建图像显示区域(带边框)
- 添加四个选项按钮(单选)
- 添加提交答案按钮
- 添加进度标签(显示当前得分)
def create_game_question_page(self):
"""创建游戏问题页面"""
widget = GradientWidget()
# ... (布局设置)
# 标题栏
header = QWidget()
header_layout = QHBoxLayout(header)
# 返回按钮
btn_back = AnimatedButton("返回")
btn_back.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(2))
# 标题(显示当前问题)
self.game_title = QLabel()
# ... (样式设置)
...
# 主内容区域
content = QWidget()
content_layout = QVBoxLayout(content)
# 图像显示区域
self.game_image_frame = QFrame()
# ... (样式设置)
image_layout = QVBoxLayout(self.game_image_frame)
self.game_image_label = QLabel()
# ... (设置)
image_layout.addWidget(self.game_image_label)
# 选项组
options_group = QGroupBox("请选择正确的球星名字")
# ... (样式设置)
option_layout = QVBoxLayout(options_group)
self.option_group = QButtonGroup()
self.option_buttons = [] # 存储选项按钮
# 创建选项按钮
for i in range(4):
...
# 提交答案按钮
btn_submit = AnimatedButton("提交答案")
btn_submit.setIcon(QIcon(self.resource_path("assets/submit_icon.png")))
btn_submit.clicked.connect(self.check_answer)
# 进度标签
self.progress_label = QLabel()
# ... (样式设置)
# 添加内容
...
# 添加组件
layout.addWidget(header)
layout.addWidget(content, 1)
return widget
最终问题界面布局如下所示:
11. 游戏结果页面
- 创建结果标签(显示评价)
- 添加分数标签(显示得分)
- 添加动画标签(显示GIF动画)
- 添加两个按钮(再玩一次和返回主菜单)
- 使用垂直布局组织所有元素
- 通过
Stretch
实现居中效果
def create_result_page(self):
"""创建游戏结果页面"""
widget = GradientWidget()
# ... (布局设置)
# 标题
title = QLabel("游戏结果")
# ... (样式设置)
# 结果容器
result_container = QWidget()
result_layout = QVBoxLayout(result_container)
# 结果标签
self.result_label = QLabel()
# ... (样式设置)
# 分数标签
self.score_label = QLabel()
# ... (样式设置)
# 动画标签
self.animation_label = QLabel()
# 按钮容器
button_container = QWidget()
button_layout = QHBoxLayout(button_container)
# 再玩一次按钮
btn_restart = AnimatedButton("再玩一次")
btn_restart.setIcon(QIcon(self.resource_path("assets/restart_icon.png")))
btn_restart.clicked.connect(lambda: self.stacked_widget.setCurrentIndex(2))
# 返回主菜单按钮
btn_menu = AnimatedButton("返回主菜单")
...
# 添加内容
...
# 添加组件
layout.addStretch(1)
...
return widget
最终游戏结果画面布局如下所示:
四、结果展示
NBA球星识别系统的演示视频如下所示:
如果你喜欢我的文章,不妨给小周一个免费的点赞和关注吧!