NBA球星知识大挑战:基于 PyQt5 的球星认识小游戏

发布于:2025-09-06 ⋅ 阅读:(20) ⋅ 点赞:(0)

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)

这个组件实现了:

  1. 定义四种颜色(深蓝、中蓝、浅蓝和红色)
  2. paintEvent中创建线性渐变
  3. 使用四种颜色创建从左上到右下的 渐变 效果
  4. 填充整个组件区域,为应用提供美观的背景
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. 加载资源

  1. 加载预训练的深度学习模型(包含自定义注意力层)
  2. 加载类别映射文件(JSON 格式)
  3. 从类别映射中提取球员名称列表(替换下划线为空格)
  4. 调用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球星识别系统的演示视频如下所示:

如果你喜欢我的文章,不妨给小周一个免费的点赞和关注吧!


网站公告

今日签到

点亮在社区的每一天
去签到