290 lines
9.3 KiB
Python
290 lines
9.3 KiB
Python
import math
|
|
import random
|
|
import cvzone
|
|
import cv2
|
|
from cvzone.HandTrackingModule import HandDetector
|
|
import time
|
|
|
|
def detectGesture(hand):
|
|
fingers = detector.fingersUp(hand)
|
|
lm = hand["lmList"]
|
|
|
|
# 获取关键点坐标
|
|
thumb_tip = lm[4]
|
|
index_tip = lm[8]
|
|
middle_tip = lm[12]
|
|
pinky_tip = lm[20]
|
|
wrist = lm[0]
|
|
|
|
# 计算手指高度
|
|
index_height = wrist[1] - index_tip[1]
|
|
middle_height = wrist[1] - middle_tip[1]
|
|
|
|
# 手势1: 比耶 → 暂停
|
|
if fingers == [0, 1, 1, 0, 0] and index_height > 30 and middle_height > 30:
|
|
return "STOP"
|
|
|
|
# 手势2: 蜘蛛侠 → 开始/继续
|
|
if fingers == [1, 1, 0, 0, 1] and index_height > 50:
|
|
return "START"
|
|
|
|
# 手势3: 摇滚手势 → 跳过食物
|
|
if fingers == [1, 0, 0, 0, 1]:
|
|
thumb_pinky_dist = math.dist(thumb_tip[:2], pinky_tip[:2])
|
|
base_dist = math.dist(wrist[:2], lm[9][:2])
|
|
|
|
if thumb_pinky_dist > base_dist * 0.7:
|
|
return "ROCK"
|
|
|
|
return "NONE"
|
|
|
|
class SnakeGame:
|
|
def __init__(self, food_path):
|
|
self.left_points = []
|
|
self.right_points = []
|
|
self.left_lengths = []
|
|
self.right_lengths = []
|
|
self.currentLeftLength = 0
|
|
self.currentRightLength = 0
|
|
self.allowedLength = 200
|
|
self.previousLeftHead = (0, 0)
|
|
self.previousRightHead = (0, 0)
|
|
|
|
self.imgFood = cv2.imread(food_path, cv2.IMREAD_UNCHANGED)
|
|
self.hFood, self.wFood, _ = self.imgFood.shape
|
|
self.foodPoint = (0, 0)
|
|
self.randomFoodLocation()
|
|
|
|
self.score = 0
|
|
self.startTime = time.time()
|
|
|
|
self.lastSkipTime = 0
|
|
self.skipCooldown = 1
|
|
self.trailDuration = 1.0
|
|
|
|
def randomFoodLocation(self):
|
|
self.foodPoint = (
|
|
random.randint(100, 1180),
|
|
random.randint(100, 620)
|
|
)
|
|
|
|
def update(self, imgMain, leftHeadPoint, rightHeadPoint):
|
|
current_time = time.time()
|
|
|
|
# 更新左手轨迹
|
|
if leftHeadPoint:
|
|
self._updateHandTrail(leftHeadPoint, current_time, is_left=True)
|
|
|
|
# 检测左手是否吃到食物
|
|
if self._checkFoodCollision(leftHeadPoint):
|
|
self._handleFoodEaten()
|
|
|
|
# 更新右手轨迹
|
|
if rightHeadPoint:
|
|
self._updateHandTrail(rightHeadPoint, current_time, is_left=False)
|
|
|
|
# 检测右手是否吃到食物
|
|
if self._checkFoodCollision(rightHeadPoint):
|
|
self._handleFoodEaten()
|
|
|
|
# 清理过期轨迹点
|
|
self._cleanOldPoints(current_time)
|
|
|
|
# 控制蛇身长度
|
|
self._controlSnakeLength()
|
|
|
|
# 绘制蛇身和蛇头
|
|
self._drawSnake(imgMain, current_time)
|
|
|
|
return imgMain
|
|
|
|
def _updateHandTrail(self, headPoint, current_time, is_left):
|
|
cx, cy = headPoint
|
|
points = self.left_points if is_left else self.right_points
|
|
lengths = self.left_lengths if is_left else self.right_lengths
|
|
prevHead = self.previousLeftHead if is_left else self.previousRightHead
|
|
|
|
px, py = prevHead
|
|
points.append([cx, cy, current_time])
|
|
dist = math.dist((cx, cy), (px, py))
|
|
lengths.append(dist)
|
|
|
|
if is_left:
|
|
self.currentLeftLength += dist
|
|
self.previousLeftHead = (cx, cy)
|
|
else:
|
|
self.currentRightLength += dist
|
|
self.previousRightHead = (cx, cy)
|
|
|
|
def _checkFoodCollision(self, headPoint):
|
|
cx, cy = headPoint
|
|
fx, fy = self.foodPoint
|
|
return (fx - self.wFood // 2 < cx < fx + self.wFood // 2 and
|
|
fy - self.hFood // 2 < cy < fy + self.hFood // 2)
|
|
|
|
def _handleFoodEaten(self):
|
|
self.randomFoodLocation()
|
|
self.allowedLength += 40
|
|
self.score += 1
|
|
|
|
def _cleanOldPoints(self, current_time):
|
|
# 清理左手过期点
|
|
while (self.left_points and
|
|
current_time - self.left_points[0][2] > self.trailDuration):
|
|
if self.left_lengths:
|
|
self.currentLeftLength -= self.left_lengths.pop(0)
|
|
self.left_points.pop(0)
|
|
|
|
# 清理右手过期点
|
|
while (self.right_points and
|
|
current_time - self.right_points[0][2] > self.trailDuration):
|
|
if self.right_lengths:
|
|
self.currentRightLength -= self.right_lengths.pop(0)
|
|
self.right_points.pop(0)
|
|
|
|
def _controlSnakeLength(self):
|
|
# 控制左手蛇长
|
|
while self.currentLeftLength > self.allowedLength and self.left_lengths:
|
|
self.currentLeftLength -= self.left_lengths.pop(0)
|
|
if self.left_points:
|
|
self.left_points.pop(0)
|
|
|
|
# 控制右手蛇长
|
|
while self.currentRightLength > self.allowedLength and self.right_lengths:
|
|
self.currentRightLength -= self.right_lengths.pop(0)
|
|
if self.right_points:
|
|
self.right_points.pop(0)
|
|
|
|
def _drawSnake(self, imgMain, current_time):
|
|
# 绘制左手蛇身(蓝色系)
|
|
for i in range(1, len(self.left_points)):
|
|
time_factor = 1.0 - (current_time - self.left_points[i][2]) / self.trailDuration
|
|
color_intensity = int(255 * time_factor)
|
|
color = (color_intensity, color_intensity // 2, 0) # 蓝到青
|
|
thickness = max(5, int(20 * time_factor))
|
|
|
|
cv2.line(imgMain, tuple(self.left_points[i-1][:2]),
|
|
tuple(self.left_points[i][:2]), color, thickness)
|
|
|
|
# 绘制右手蛇身(红色系)
|
|
for i in range(1, len(self.right_points)):
|
|
time_factor = 1.0 - (current_time - self.right_points[i][2]) / self.trailDuration
|
|
color_intensity = int(255 * time_factor)
|
|
color = (0, color_intensity // 2, color_intensity) # 红到橙
|
|
thickness = max(5, int(20 * time_factor))
|
|
|
|
cv2.line(imgMain, tuple(self.right_points[i-1][:2]),
|
|
tuple(self.right_points[i][:2]), color, thickness)
|
|
|
|
# 绘制蛇头
|
|
if self.left_points:
|
|
cv2.circle(imgMain, tuple(self.left_points[-1][:2]), 20, (255, 100, 100), cv2.FILLED)
|
|
cv2.circle(imgMain, tuple(self.left_points[-1][:2]), 20, (255, 255, 255), 2)
|
|
|
|
if self.right_points:
|
|
cv2.circle(imgMain, tuple(self.right_points[-1][:2]), 20, (100, 100, 255), cv2.FILLED)
|
|
cv2.circle(imgMain, tuple(self.right_points[-1][:2]), 20, (255, 255, 255), 2)
|
|
|
|
def skipFood(self):
|
|
currentTime = time.time()
|
|
if currentTime - self.lastSkipTime > self.skipCooldown:
|
|
self.randomFoodLocation()
|
|
self.lastSkipTime = currentTime
|
|
return True
|
|
return False
|
|
|
|
def drawFood(self, imgMain):
|
|
fx, fy = self.foodPoint
|
|
return cvzone.overlayPNG(imgMain, self.imgFood,
|
|
(fx - self.wFood // 2, fy - self.hFood // 2))
|
|
|
|
def drawUI(self, imgMain, gameRunning, skipMessageTime):
|
|
currentTime = time.time()
|
|
|
|
# 分数
|
|
cvzone.putTextRect(imgMain, f"Score: {self.score}", [50, 60], scale=2, thickness=3)
|
|
|
|
# 时间
|
|
elapsed = int(currentTime - self.startTime)
|
|
mm, ss = elapsed // 60, elapsed % 60
|
|
cvzone.putTextRect(imgMain, f"Time: {mm:02d}:{ss:02d}", [50, 120], scale=2, thickness=3)
|
|
|
|
# 状态提示
|
|
if not gameRunning:
|
|
cvzone.putTextRect(imgMain, "STOP", [50, 180], scale=2, thickness=2)
|
|
elif currentTime - skipMessageTime < 1.0:
|
|
cvzone.putTextRect(imgMain, "SKIP", [50, 180], scale=2, thickness=2)
|
|
|
|
return imgMain
|
|
|
|
# 初始化
|
|
cap = cv2.VideoCapture(0)
|
|
cap.set(3, 1280)
|
|
cap.set(4, 720)
|
|
|
|
detector = HandDetector(detectionCon=0.8, maxHands=2)
|
|
game = SnakeGame("donut.png")
|
|
|
|
gameRunning = True
|
|
skipMessageTime = 0
|
|
lastGesture = "NONE"
|
|
|
|
# 主循环
|
|
while True:
|
|
success, img = cap.read()
|
|
img = cv2.flip(img, 1)
|
|
|
|
hands, img = detector.findHands(img, flipType=False)
|
|
|
|
currentGesture = "NONE"
|
|
currentTime = time.time()
|
|
|
|
leftHandPoint = None
|
|
rightHandPoint = None
|
|
|
|
if hands:
|
|
for hand in hands:
|
|
handType = hand["type"]
|
|
index_tip = tuple(hand["lmList"][8][:2])
|
|
|
|
if handType == "Left":
|
|
leftHandPoint = index_tip
|
|
else:
|
|
rightHandPoint = index_tip
|
|
|
|
# 检测手势
|
|
gesture = detectGesture(hand)
|
|
if gesture != "NONE":
|
|
currentGesture = gesture
|
|
|
|
# 处理手势变化
|
|
if currentGesture != lastGesture:
|
|
if currentGesture == "START":
|
|
gameRunning = True
|
|
elif currentGesture == "STOP":
|
|
gameRunning = False
|
|
|
|
if gameRunning and currentGesture == "ROCK":
|
|
if game.skipFood():
|
|
skipMessageTime = currentTime
|
|
|
|
lastGesture = currentGesture
|
|
|
|
if gameRunning:
|
|
img = game.update(img, leftHandPoint, rightHandPoint)
|
|
else:
|
|
lastGesture = "NONE"
|
|
|
|
# 绘制食物和UI
|
|
img = game.drawFood(img)
|
|
img = game.drawUI(img, gameRunning, skipMessageTime)
|
|
|
|
# 显示当前手势
|
|
cvzone.putTextRect(img, f"Gesture: {currentGesture}", [50, 650], scale=1, thickness=2)
|
|
|
|
cv2.imshow("Snake Game", img)
|
|
if cv2.waitKey(1) & 0xFF == ord('q'):
|
|
break
|
|
|
|
cap.release()
|
|
cv2.destroyAllWindows() |