NBA球星知识大挑战:基于 PyQt5 的球星认识小游戏
代码详见:https://github.com/xiaozhou-alt/NBA_Players_Recognition
文章目录
- NBA球星知识大挑战:基于 PyQt5 的球星认识小游戏
- 一、项目介绍
- 二、文件夹结构
- 三、项目实现
- 1. 自定义动画按钮(AnimatedButton)
- 2. 渐变背景组件(GradientWidget)
- 3. 主应用类(NBAApp)
- 4. 加载资源
- 5. 加载球员图像
- 6. 创建主菜单页面
- 7. 图像上传与识别功能
- 8. 球星识别页面
- 9. 球星认识小游戏界面
- 10. 游戏问题界面
- 11. 游戏结果页面
- 四、结果展示
关于识别球星的模型训练和数据集获取与讲解请详见:当库里遇上卷积神经网络:基于 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("请先上传图片或等待模型加载完成")returntry:# 添加加载提示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转RGBimg_array = img_array[..., :3]img_array = img_array.astype('float32') / 255.0return 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球星识别系统的演示视频如下所示:
如果你喜欢我的文章,不妨给小周一个免费的点赞和关注吧!