2025-03-01 02:42:23 +00:00
|
|
|
|
from concurrent.futures import thread
|
|
|
|
|
from threading import Thread
|
|
|
|
|
import threading
|
2025-02-28 16:55:58 +00:00
|
|
|
|
import cv2
|
|
|
|
|
import mediapipe as mp
|
|
|
|
|
import pygame
|
|
|
|
|
import time
|
|
|
|
|
import random
|
2025-03-01 02:42:23 +00:00
|
|
|
|
import os
|
|
|
|
|
import tkinter as tk
|
|
|
|
|
from PIL import Image, ImageTk
|
|
|
|
|
|
2025-02-28 16:55:58 +00:00
|
|
|
|
# 初始化游戏窗口
|
|
|
|
|
pygame.init()
|
|
|
|
|
# 字体初始化
|
|
|
|
|
font = pygame.font.Font(None, 36) # None使用默认字体,大小为36
|
|
|
|
|
screen = pygame.display.set_mode((600, 800))
|
|
|
|
|
ball_radius = 20
|
|
|
|
|
ball_color = (255, 105, 180) # 粉色
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 初始化摄像头
|
|
|
|
|
cap = cv2.VideoCapture(0)
|
|
|
|
|
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) # 设置宽度
|
|
|
|
|
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) # 设置高度
|
|
|
|
|
|
|
|
|
|
# 验证设置
|
|
|
|
|
actual_width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
|
|
|
actual_height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
|
|
|
print(f"Working Resolution: {actual_width}x{actual_height}")
|
|
|
|
|
# 创建自适应窗口
|
|
|
|
|
# cv2.namedWindow('Face Detection', cv2.WINDOW_NORMAL)
|
|
|
|
|
# cv2.resizeWindow('Face Detection', actual_width, actual_height)
|
|
|
|
|
|
|
|
|
|
# 初始化MediaPipe
|
|
|
|
|
mp_face = mp.solutions.face_detection # type: ignore
|
|
|
|
|
face_detection = mp_face.FaceDetection(min_detection_confidence=0.5)
|
|
|
|
|
# 获取实际分辨率
|
|
|
|
|
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
|
|
|
|
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
|
|
|
|
print(f"实际分辨率: {width}x{height}")
|
|
|
|
|
# 定义目标裁剪尺寸
|
|
|
|
|
CROP_SIZE = 720
|
|
|
|
|
|
|
|
|
|
# 计算裁剪区域坐标
|
|
|
|
|
x_start = (width - CROP_SIZE) // 2
|
|
|
|
|
y_start = (height - CROP_SIZE) // 2
|
|
|
|
|
x_end = x_start + CROP_SIZE
|
|
|
|
|
y_end = y_start + CROP_SIZE
|
|
|
|
|
|
|
|
|
|
clock = pygame.time.Clock() # 用于控制帧率
|
|
|
|
|
|
|
|
|
|
# 全局变量初始化 (请确保在主程序中进行初始化)
|
|
|
|
|
OBSTACLE_INTERVAL = 1400 # 毫秒,障碍物生成间隔
|
|
|
|
|
OBSTACLE_SPEED = 5 # 障碍物下移速度
|
|
|
|
|
start_time = time.time() # 游戏开始时间# 全局变量初始化
|
|
|
|
|
obstacles = [] # 存储当前障碍物的列表
|
|
|
|
|
obstacle_timer = 0 # 通用障碍物生成计时器
|
|
|
|
|
|
|
|
|
|
# 新增全局变量用于跟踪左右两侧障碍物的生成时间
|
|
|
|
|
last_obstacle_time_left = 0
|
|
|
|
|
last_obstacle_time_right = 0
|
|
|
|
|
MIN_OBSTACLE_INTERVAL = 2000 # 毫秒,同一侧障碍物最小生成间隔
|
|
|
|
|
|
|
|
|
|
start_time = time.time() # 游戏开始时间
|
2025-03-01 02:42:23 +00:00
|
|
|
|
|
|
|
|
|
def get_player_avatar():
|
|
|
|
|
global cap
|
|
|
|
|
"""获取当前游玩者的头像(人脸图像)。"""
|
|
|
|
|
mp_face = mp.solutions.face_detection # type: ignore
|
|
|
|
|
face_detection = mp_face.FaceDetection(min_detection_confidence=0.5)
|
|
|
|
|
|
|
|
|
|
ret, frame = cap.read()
|
|
|
|
|
if not ret:
|
|
|
|
|
return None # 如果未读取到帧,返回 None
|
|
|
|
|
|
|
|
|
|
frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
|
|
|
results = face_detection.process(frame_rgb)
|
|
|
|
|
|
|
|
|
|
if results.detections:
|
|
|
|
|
max_area = 0
|
|
|
|
|
best_detection = None
|
|
|
|
|
|
|
|
|
|
# 找到最大的面框
|
|
|
|
|
for detection in results.detections:
|
|
|
|
|
bbox = detection.location_data.relative_bounding_box
|
|
|
|
|
area = bbox.width * bbox.height
|
|
|
|
|
if area > max_area:
|
|
|
|
|
max_area = area
|
|
|
|
|
best_detection = bbox
|
|
|
|
|
|
|
|
|
|
if best_detection:
|
|
|
|
|
# 计算面框在原始帧中的坐标
|
|
|
|
|
h, w, _ = frame.shape
|
|
|
|
|
x_min = int(best_detection.xmin * w)
|
|
|
|
|
y_min = int(best_detection.ymin * h)
|
|
|
|
|
x_max = int((best_detection.xmin + best_detection.width) * w)
|
|
|
|
|
y_max = int((best_detection.ymin + best_detection.height) * h)
|
|
|
|
|
|
|
|
|
|
# 增加边界,确保完整性
|
|
|
|
|
border_x = int(0.1 * (x_max - x_min)) # 边界宽度为框宽度的10%
|
|
|
|
|
border_y = int(0.1 * (y_max - y_min)) # 边界高度为框高度的10%
|
|
|
|
|
|
|
|
|
|
# 更新坐标以增加边界,同时确保坐标在有效范围内
|
|
|
|
|
x_min = max(x_min - border_x, 0) # 确保不小于0
|
|
|
|
|
y_min = max(y_min - border_y, 0) # 确保不小于0
|
|
|
|
|
x_max = min(x_max + border_x, w) # 确保不超出图像宽度
|
|
|
|
|
y_max = min(y_max + border_y, h) # 确保不超出图像高度
|
|
|
|
|
|
|
|
|
|
# 裁剪出面框区域
|
|
|
|
|
avatar = frame[y_min:y_max, x_min:x_max]
|
|
|
|
|
return avatar
|
|
|
|
|
|
|
|
|
|
return None # 如果未检测到人脸,返回 None
|
|
|
|
|
|
2025-02-28 16:55:58 +00:00
|
|
|
|
def you_failed(elapsed_time):
|
|
|
|
|
global obstacles
|
|
|
|
|
obstacles.clear()
|
|
|
|
|
"""处理失败的函数,弹出消息或重置游戏等"""
|
|
|
|
|
global start_time
|
2025-03-01 02:42:23 +00:00
|
|
|
|
print(f"You failed! Elapsed dis: {elapsed_time:.2f} meters")
|
|
|
|
|
if not os.path.exists("faces"):
|
|
|
|
|
os.makedirs("faces")
|
|
|
|
|
file_name = os.path.join("faces",f"{elapsed_time:.2f}.png")
|
|
|
|
|
avatar = get_player_avatar()
|
|
|
|
|
if avatar is not None:
|
|
|
|
|
cv2.imwrite(file_name,avatar)
|
2025-02-28 16:55:58 +00:00
|
|
|
|
start_time=time.time()
|
|
|
|
|
# 在此处添加其他失败处理逻辑,例如重置游戏、显示消息等
|
|
|
|
|
|
|
|
|
|
def draw_ball(normalized_x, normalized_y):
|
|
|
|
|
global obstacles, obstacle_timer, last_obstacle_time_left, last_obstacle_time_right, start_time
|
|
|
|
|
screen.fill((0, 0, 0))
|
|
|
|
|
"""在Pygame窗口中绘制小球并处理障碍物"""
|
|
|
|
|
# 计算经过的时间
|
|
|
|
|
elapsed_time = time.time() - start_time
|
2025-03-01 02:42:23 +00:00
|
|
|
|
# elapsed_seconds = # 整数秒数
|
2025-02-28 16:55:58 +00:00
|
|
|
|
|
|
|
|
|
# 计算小球位置
|
|
|
|
|
ball_x = int(normalized_x * (screen.get_width() - ball_radius * 2)) + ball_radius
|
|
|
|
|
ball_y = int(0.6 * (screen.get_height() - ball_radius * 2)) + ball_radius
|
|
|
|
|
|
|
|
|
|
# # 检查小球是否碰到屏幕边界
|
|
|
|
|
# if ball_x - ball_radius <= 0 or ball_x + ball_radius >= screen.get_width():
|
|
|
|
|
# elapsed_time = time.time() - start_time
|
|
|
|
|
# you_failed(elapsed_time)
|
|
|
|
|
# return
|
|
|
|
|
|
|
|
|
|
# 绘制小球
|
|
|
|
|
pygame.draw.circle(screen, ball_color, (ball_x, ball_y), ball_radius)
|
|
|
|
|
|
|
|
|
|
# 生成障碍物
|
|
|
|
|
current_time = pygame.time.get_ticks()
|
|
|
|
|
if current_time - obstacle_timer > OBSTACLE_INTERVAL:
|
|
|
|
|
# 尝试随机选择左右两侧生成障碍物
|
|
|
|
|
sides = ['left', 'right']
|
|
|
|
|
random.shuffle(sides) # 随机打乱侧别顺序以增加随机性
|
|
|
|
|
# 障碍物宽度和高度
|
|
|
|
|
obstacle_width = random.randint(200, 500)
|
|
|
|
|
obstacle_height = random.randint(20, 100)
|
|
|
|
|
for side in sides:
|
|
|
|
|
if side == 'left':
|
|
|
|
|
# 检查左侧的障碍物生成间隔
|
|
|
|
|
if current_time - last_obstacle_time_left >= MIN_OBSTACLE_INTERVAL:
|
|
|
|
|
x_pos = 0
|
|
|
|
|
last_obstacle_time_left = current_time
|
|
|
|
|
else:
|
|
|
|
|
continue # 左侧间隔不够,尝试右侧
|
|
|
|
|
else:
|
|
|
|
|
# 检查右侧的障碍物生成间隔
|
|
|
|
|
if current_time - last_obstacle_time_right >= MIN_OBSTACLE_INTERVAL:
|
|
|
|
|
x_pos = screen.get_width() - obstacle_width
|
|
|
|
|
last_obstacle_time_right = current_time
|
|
|
|
|
else:
|
|
|
|
|
continue # 右侧间隔不够,尝试左侧S
|
|
|
|
|
# 确定x_pos根据侧别
|
|
|
|
|
if side == 'left':
|
|
|
|
|
x_pos = 0
|
|
|
|
|
else:
|
|
|
|
|
x_pos = screen.get_width() - obstacle_width
|
|
|
|
|
y_pos = -obstacle_height # 从屏幕上方外部生成
|
|
|
|
|
obstacles.append(pygame.Rect(x_pos, y_pos, obstacle_width, obstacle_height))
|
|
|
|
|
obstacle_timer = current_time # 重置生成计时器
|
|
|
|
|
break # 成功生成一个障碍物后退出循环
|
|
|
|
|
|
|
|
|
|
# 移动并绘制障碍物
|
|
|
|
|
for obstacle in obstacles[:]:
|
|
|
|
|
obstacle.y += OBSTACLE_SPEED
|
|
|
|
|
|
|
|
|
|
# 绘制障碍物
|
|
|
|
|
pygame.draw.rect(screen, (0, 255, 0), obstacle) # 绿色障碍物
|
|
|
|
|
|
|
|
|
|
# 检查是否与小球碰撞
|
|
|
|
|
if obstacle.colliderect(pygame.Rect(ball_x - ball_radius, ball_y - ball_radius, ball_radius * 2, ball_radius * 2)):
|
|
|
|
|
elapsed_time = time.time() - start_time
|
|
|
|
|
you_failed(elapsed_time)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# 移除已离开屏幕的障碍物
|
|
|
|
|
if obstacle.y > screen.get_height():
|
|
|
|
|
obstacles.remove(obstacle)
|
|
|
|
|
|
|
|
|
|
# 绘制计时器文本
|
2025-03-01 02:42:23 +00:00
|
|
|
|
timer_text = font.render(f"{elapsed_time:.2f} M", True, (255, 255, 255)) # 白色文本
|
2025-02-28 16:55:58 +00:00
|
|
|
|
screen.blit(timer_text, (10, 10)) # 在左上角绘制文本(10, 10)是坐标
|
|
|
|
|
pygame.display.flip()
|
|
|
|
|
clock.tick(60)
|
|
|
|
|
|
|
|
|
|
# 验证裁剪可行性
|
|
|
|
|
if x_start < 0 or y_start < 0:
|
|
|
|
|
raise ValueError("摄像头分辨率不足以裁剪720x720区域")
|
|
|
|
|
|
|
|
|
|
mp_drawing = mp.solutions.drawing_utils # type: ignore
|
|
|
|
|
|
|
|
|
|
while cap.isOpened():
|
|
|
|
|
ret, frame = cap.read()
|
|
|
|
|
if not ret: break
|
|
|
|
|
frame = cv2.flip(frame,1)
|
|
|
|
|
# 人脸检测
|
|
|
|
|
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
|
|
|
|
results = face_detection.process(rgb_frame)
|
|
|
|
|
if results.detections:
|
|
|
|
|
for detection in results.detections:
|
|
|
|
|
# 使用MediaPipe自带绘制方法
|
|
|
|
|
mp_drawing.draw_detection(frame, detection)
|
|
|
|
|
if results.detections:
|
|
|
|
|
detection = results.detections[0]
|
|
|
|
|
max_area = 0
|
|
|
|
|
|
|
|
|
|
for idetection in results.detections:
|
|
|
|
|
bbox = idetection.location_data.relative_bounding_box
|
|
|
|
|
area = bbox.width * bbox.height # 计算面框的面积
|
|
|
|
|
if area > max_area: # 找到最大的
|
|
|
|
|
max_area = area
|
|
|
|
|
detection = idetection
|
|
|
|
|
bbox = detection.location_data.relative_bounding_box
|
|
|
|
|
# 计算原始坐标(相对于完整帧)
|
|
|
|
|
raw_x = int(bbox.xmin * width)
|
|
|
|
|
raw_y = int(bbox.ymin * height)
|
|
|
|
|
raw_w = int(bbox.width * width)
|
|
|
|
|
raw_h = int(bbox.height * height)
|
|
|
|
|
center_x = raw_x + raw_w // 2
|
|
|
|
|
center_y = raw_y + raw_h // 2
|
|
|
|
|
|
|
|
|
|
# 转换为裁剪区域坐标
|
|
|
|
|
crop_x = center_x - x_start
|
|
|
|
|
crop_y = center_y - y_start
|
|
|
|
|
|
|
|
|
|
# 归一化到0-1范围(带边界保护)
|
|
|
|
|
normalized_x = max(0.0, min(1.0, crop_x / CROP_SIZE))
|
|
|
|
|
normalized_y = max(0.0, min(1.0, crop_y / CROP_SIZE))
|
|
|
|
|
draw_ball(normalized_x,normalized_y)
|
|
|
|
|
# print(f"归一化坐标: ({normalized_x:.2f}, {normalized_y:.2f})")
|
|
|
|
|
|
|
|
|
|
# 可视化裁剪区域
|
|
|
|
|
cv2.rectangle(frame,
|
|
|
|
|
(x_start, y_start),
|
|
|
|
|
(x_end, y_end),
|
|
|
|
|
(0,255,0), 2)
|
|
|
|
|
cv2.imshow('Preview', frame)
|
|
|
|
|
if cv2.waitKey(1) == ord('q'):
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
cap.release()
|
|
|
|
|
cv2.destroyAllWindows()
|