# motion-sensing-game-fruit **Repository Path**: watsonhuang_admin/motion-sensing-game-fruit ## Basic Information - **Project Name**: motion-sensing-game-fruit - **Description**: 体感切水果游戏,使用USB摄像头识别出手指,进行游戏交互,游戏使用PyGame构建 - **Primary Language**: Unknown - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-11-28 - **Last Updated**: 2025-11-28 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## 基于摄像头的体感切水果游戏 --- * 体感切水果游戏,通过USB摄像头识别左右手,产生(食指坐标)模拟鼠标按键事件进行游戏交互,使用多线程处理,反应速度快。 * USB摄像头分辨率为 640x480 ,游戏屏幕尺寸同此,减少图像帧缩放的处理的时间。 --- * ![移步B站观看游戏视频](src/game.png) * [移步B站观看游戏视频](https://www.bilibili.com/video/BV1c6SuBDEcu/) --- * 采用 `OpenCV` 读取USB摄像头图像,进行图像转码、镜像; * 采用 `mediapipe` 识别图像中的手部,识别出手的特征点并且绘制; * 采用 `pygame` 构建切水果游戏的主要逻辑和界面渲染。 ### 依赖的库 ```shell # opencv 库 pip install opencv-python # 体感识别库 pip install mediapipe # python游戏库 pip install pygame ``` ### 一、 USB 摄像头处理 使用Opencv打开USB摄像头,并读取视频帧,将原始图像转换成RGB图像(显示),并进行水平镜像(前置镜像方便操作), 之后进行调用手部识别程序进行识别,识别后将处理过的帧和结果放入`self.frame_objs`列表中,由另一个帧处理线程处理转发。 #### 1. 摄像头帧读取线程 ```python # 打开默认的第一个usb摄像头 cap = cv2.VideoCapture(0, cv2.CAP_DSHOW) while True: ret, frame = self.cap.read()# 读取结果、图像帧 if not ret: continue ... frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 图像预处理(BGR→RGB) frame_rgb = cv2.flip(frame_rgb, 1)# 水平镜像 frame_rgb.flags.writeable = False results = self.hands.process(frame_rgb) # 手部识别处理 frame_rgb.flags.writeable = True self.frame_objs.append(FrameObject(frame_rgb, results.multi_hand_landmarks))# 添加进队列 self.frame_semaphore.release()# 发送信号量通知帧处理线程操作 ... ``` #### 2. 图像帧处理方法 `self.hands.process` 会将手部特征点识别结果坐标返回,可以进行手部特征绘制,生成互动触发事件: * `cv2.circle` 绘制圆圈,`cv2.putText` 绘制文本 * `self.event_counter` 防止事件发送过快,`self.mouse_update_cb`将食指坐标作为事件进行回调 ```python self.event_counter += 1 if multi_hand_landmarks: for hand_landmarks in multi_hand_landmarks: # 绘制手部骨架(连接关键点) self.mp_drawing.draw_landmarks( frame, hand_landmarks, self.mp_hands.HAND_CONNECTIONS, # 手部关键点连接关系 landmark_drawing_spec=self.drawing_spec, connection_drawing_spec=self.connection_spec ) # 标记指尖(可选:突出显示食指指尖) h, w, _ = frame.shape index_finger_tip = ( int(hand_landmarks.landmark[8].x * w), # 食指指尖是第8号关键点 int(hand_landmarks.landmark[8].y * h) ) cv2.circle(frame, index_finger_tip, 8, (0, 0, 255), -1) # 红色指尖标记 cv2.putText(frame, "Finger Tip", (index_finger_tip[0] + 10, index_finger_tip[1]), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 2) # 防止快速发送事件 if self.event_counter % self.event_interval == 0: # 使用指尖作为事件 mouse_x, mouse_y = index_finger_tip[0] + 10, index_finger_tip[1] if callable(self.mouse_update_cb): self.mouse_update_cb(mouse_x, mouse_y) self.event_counter = 0 # 重置计数器 ``` #### 3. 图像帧处理发送线程 将图像进行绘制和转发至游戏线程,通过设置回调函数`self.frame_update_cb`实现 ```python def frame_process(self): while self.running: if self.frame_semaphore.acquire(True,None) is True: # 阻塞接收到信号量 while len(self.frame_objs) > 0: fo = self.frame_objs.pop(0) # 取首帧 if fo: if callable(self.frame_update_cb): self.frame_draw(fo.frame, fo.multi_hand_landmarks)# 绘制识别的帧 self.frame_update_cb(fo.frame)# 回调显示 ``` ### 二、 游戏线程处理 游戏是使用坐标渲染水果,并且与接收图像处理线程返回的特征点坐标进行比对, 在水果渲染区域内,则判断成功,该水果消失,并随机产生一个新的水果落下。 接收图像处理线程返回的图像作为游戏背景。 当前游戏区域内的水果由 `self.fruits` 保存。 #### 1. 初始化水果元素,从素材库中加载水果图片,并添加到 `self.fruits_image` ```python imgs = ["apple.png", "strawberry.png"] for img in imgs: pyimg = pygame.image.load(f"src/{img}").convert_alpha() #读取图像,保留透明通道 pyimg = pygame.transform.scale(pyimg, (self.fruit_img_size, self.fruit_img_size))# 强制水果元素的缩放尺寸 self.fruits_image.append(pyimg) # 保留到全局,供后续抽取使用 ``` #### 2. 加载随机水果元素,随机抽取的水果,随机产生的坐标,`FruitObject` 包含坐标和图片元素 ```python self.fruits.remove(old) #去除旧的元素,可能是碰底,也可能是手指触碰到 new = FruitObject( random.randrange(0, self.screen_width - self.fruit_img_size), -500, #随机x下落坐标 random.choice(self.fruits_image) #随机从水果图片内抽取1个元素 ) self.fruits.append(new) #添加新生成的元素 ``` #### 3. 当识别线程回调回来时,产生一个 pygame 能够接收的事件 模拟鼠标左键点击事件,当前点击坐标为食指识别出来的坐标 ```python def customer_event(self, mouse_x, mouse_y): event = pygame.event.Event(pygame.MOUSEBUTTONDOWN, pos=(mouse_x, mouse_y), button=pygame.BUTTON_LEFT) pygame.event.post(event) ``` #### 4. 游戏界面渲染 图像处理线程返回的图像帧,图像帧作为游戏的背景,保存在游戏的`self.frame_queue`内, 当游戏界面渲染时,从中出队,并赋值至 `self.background`,使用互斥锁防止竞争 `self.background`。 ```python frame = self.frame_queue.get_nowait() # 出队 if frame: #获取背景互斥锁 if self.background_lock.acquire_lock(False): self.background = frame #将帧图像复制给背景 self.background_lock.release_lock() # 释放锁 ``` #### 5. pygame 的基本方法 游戏的初始化,初始化游戏渲染所需要的屏幕尺寸 ```python pygame.init() # 初始化 pygame.display.set_caption("切水果(多线程稳定版)") # 窗体名称 self.screen_width, self.screen_height = 640, 480 # 窗口尺寸 # 双缓冲+硬件加速(抗闪烁+提升性能) # self.screen 作为游戏屏幕,之后渲染都在此基础上 self.screen = pygame.display.set_mode((self.screen_width, self.screen_height), pygame.DOUBLEBUF) ``` 使用 `self.screen.blit(渲染的图像,渲染起始坐标(x,y))` 分别渲染: * 游戏分数(pygame.font 提供的文字渲染功能)、 * 水果(下坠更新 Y 轴的值 , 溢出屏幕高度时,生成新的节点,扣分)、 * 背景(获取互斥锁,将背景帧渲染整个屏幕)。 ```python def draw_animation(self): # 绘制背景 if self.background: if self.background_lock.acquire_lock(False): self.screen.blit(self.background, (0, 0)) # 渲染背景 self.background_lock.release_lock() else: self.screen.fill(self.black) # 初始无帧时用黑色 wait_node = [] # 绘制水果 for fruit in self.fruits: if fruit.y < self.screen_height: self.screen.blit(fruit.img, (fruit.x, fruit.y)) # 渲染水果 fruit.y += self.speed # 下坠 Y + , X 不变 if fruit.y > self.screen_height: # 溢出 wait_node.append(fruit) self.score_sub() for fruit in wait_node: self.node_replace(fruit) # 绘制分数 score_text = self.font.render(f"{self.score}", True, self.white) self.screen.blit(score_text, (self.screen_width / 2, 10)) pygame.display.flip() # 缓冲刷新 ```