Computer Vision Use OpenCV to make a table tennis gesture game

Effect

Operating environment

Anaconda 22.9.0

Visual Studio Code

Python 3.9.12

PyQT 5.2

Detailed process

Preparation

1. Create a folder and give it a random name to represent the name of the game.

2. Create another folder in this folder, and you can put the materials into the new folder

3. Create a py file and you’re ready to go

4. We give priority to the main part of the game, and then slowly improve it

1. Import the required libraries

import cv2
import time
import cvzone
from cvzone.HandTrackingModule import HandDetector
from winsound import Beep
import numpy as np
import random

2. Turn on the camera and set the page size

cap = cv2.VideoCapture(0)
cap.set(3, 1280)
cap.set(4, 720)

3. Read material

imgBackground = cv2.imread("picture\Background.png")#Background board
imgBall = cv2.imread("picture\Ball.png", cv2.IMREAD_UNCHANGED)#小ball
imgBat1 = cv2.imread("picture\Bat1.png", cv2.IMREAD_UNCHANGED)#bat
imgBat2 = cv2.imread("picture\Bat2.png", cv2.IMREAD_UNCHANGED)# racket

4.Initialization data

1. Initialize the ball and scores

# Random direction of the ball
speed = [-20,20]
# The initial position of the ball is in the center of the table
ballPos = [600, 320]
# Random initial speed of the ball
speedX = random.choice(speed)
speedY = random.choice(speed)
# Set game to start
gameOver = False
#Initial score
score = [0, 0]

2. Import hand recognition

detector = HandDetector(detectionCon=0.8, maxHands=2)
hands, img = detector.findHands(img)
img = cv2.flip(img, 1)
hands, img = detector.findHands(img, flipType=False)

5. Logic implementation

5.1. Two-hand mode

# If the hand is in the camera
if hands:
    for hand in hands:
       x, y, w, h = hand['bbox']
       # Return the height and width of the racket
       h1, w1 = imgBat1.shape[0:2]
       y1 = y - h1 // 2
       # Specify the upper and lower range of the racket
       y1 = np.clip(y1, 20, 520)
       # If detected as left hand
       if hand['type'] == "Left":
       # Place the board on the background, the abscissa does not move, and the ordinate moves up and down according to the position of the hand
           img = cvzone.overlayPNG(img, imgBat1, (60, y1))
           # If the ball hits the board
           if 70 < ballPos[0] < 70 + w1 and y1-h1//3 < ballPos[1] < y1 + h1:
           # Reverse the lateral speed, that is, rebound
           speedX = -speedX
              ballPos[0] + = 30
              score[0] + = 1
              #crash
              Beep(1046,50)
#If detected as right hand
       if hand['type'] == "Right":
      img = cvzone.overlayPNG(img, imgBat2, (1190, y1))
       if 1140 - w1 < ballPos[0] < 1140 and y1-h1//3 < ballPos[1] < y1 + h1:
          speedX = -speedX
          ballPos[0] -= 30
          score[1] + = 1
          Beep(1046,50)
if ballPos[0] < 40 or ballPos[0] > 1160:
gameOver = True

Real-time score display

cv2.putText(img,str(score[0]),(300,650),cv2.FONT_HERSHEY_COMPLEX,3,(255,255,255),5)
cv2.putText(img,str(score[1]),(900,650),cv2.FONT_HERSHEY_COMPLEX,3,(255,255, 255),5)

End interface display

# The score on the left is high
if score[0]>score[1]:
    cv2.putText(img, str('WIN'), (250, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255,0,255), 5)
    cv2.putText(img, str('LOSE'), (850, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (34, 148, 83), 5)
# The score on the right is high
elif score[1]>score[0]:
    cv2.putText(img, str('LOSE'), (250, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (34, 148, 83), 5)
    cv2.putText(img, str('WIN'), (850, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255,0,255), 5)
# Tie display
elif score[0]==score[1]:
cv2.putText(img, str('DRAW'), (500, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (192, 44, 56), 5)

5.2. One-handed mode

if hands:
    for hand in hands:
        x, y, w, h = hand['bbox']
        h1, w1 = imgBat1.shape[0:2]
        y1 = y - h1 // 2
        y1 = np.clip(y1, 20, 520)
        if hand['type'] == "Left":
            img = cvzone.overlayPNG(img, imgBat1, (60, y1))
            if 70 < ballPos[0] < 70 + w1 and y1-h1//3 < ballPos[1] < y1 + h1:
                speedX = -speedX
                ballPos[0] + = 30
                score[0] + = 1
                Beep(1046,50)
            # If the ball goes over the board
            if ballPos[0] < 40:
                # game over
                gameOver=True
            # If the ball hits the right wall
            if ballPos[0] >= 1200:
                speedX = -speedX
            # Display real-time results
cv2.putText(img,str(score[0]),(300,650),cv2.FONT_HERSHEY_COMPLEX,3,(255, 255, 255), 5)
        if hand['type'] == "Right":
            img = cvzone.overlayPNG(img, imgBat2, (1190, y1))
            if 1140 - w1 < ballPos[0] < 1140 and y1-h1//3 < ballPos[1] < y1 + h1:
                speedX = -speedX
                ballPos[0] -= 30
                score[1] + = 1
                Beep(1046,50)
            # If the ball goes over the board
            if ballPos[0] > 1160:
                # game over
                gameOver=True
            # If the ball hits the left wall
            if ballPos[0] <= 20:
                # bounce
                speedX = -speedX
cv2.putText(img,str(score[1]),(900,650),cv2.FONT_HERSHEY_COMPLEX,3,(255, 255, 255), 5)
# If no hands are detected and the ball exceeds the left and right boundaries (the core of seamlessly connecting left and right hands)
if not hands and (ballPos[0] < 40 or ballPos[0] > 1160):
    # game over
    gameOver = True

5.3 Game end interface

if gameOver:
    imgGameOver = cv2.imread("picture\Gameover.png")
    img = imgGameOver
    cvzone.putTextRect(img, f"'R' begin or 'ESC' end", (400,100))

5.4 frame rate screen

pTime = 0
cTime = 0
Write the calculation process and display into the while loop
cTime = time.time()
fps = 1/(cTime-pTime)
pTime = cTime
cv2.putText(img,f"FPS:{int(fps)}",(20,70),cv2.FONT_HERSHEY_PLAIN,3,(255, 255, 0),3)

5.5 Moving the ball

ballPos[0] + = speedX
ballPos[1] + = speedY
# Display the ball trajectory (background, ball, coordinates)
img = cvzone.overlayPNG(img, imgBall, ballPos)

5.6 Restart or exit the game

if key == ord('R'):
# Restore central position
    ballPos = [600, 320]
    # The ball returns to a random initial speed
    speedX = random.choice(speed)
    speedY = random.choice(speed)
    # Close game over screen
    gameOver = False
    #Restore initialization results
    score = [0, 0]
elif key == 27:
    # quit
    break

Complete code

UI.py

import sys
from PyQt5.QtWidgets import QApplication, QMainWindow
from UI_Set import Ui_Game
import game

class MyMainForm(QMainWindow,Ui_Game):
    def __init__(self, parent=None):
        super(MyMainForm, self).__init__(parent)
        self.setupUi(self)
        self.Button1.clicked.connect(game.One_hand)
        self.Button2.clicked.connect(game.Both_hands)
        self.setWindowTitle("Fun Table Tennis")
        
if __name__ == "__main__":
    app = QApplication(sys.argv)
    mygame = MyMainForm()
    mygame.setObjectName("MainWindow")
    mygame.show()
    sys.exit(app.exec_())

game.py

import cv2
import time
import cvzone
from cvzone.HandTrackingModule import HandDetector
from winsound import Beep
import numpy as np
import random

imgBackground = cv2.imread("picture\Background.png")
imgBall = cv2.imread("picture\Ball.png", cv2.IMREAD_UNCHANGED)
imgBat1 = cv2.imread("picture\Bat1.png", cv2.IMREAD_UNCHANGED)
imgBat2 = cv2.imread("picture\Bat2.png", cv2.IMREAD_UNCHANGED)

cap = cv2.VideoCapture(0)
cap.set(3, 1280)
cap.set(4, 720)

def Both_hands(self):

    detector = HandDetector(detectionCon=0.8, maxHands=2)
    pTime = 0
    cTime = 0
    speed = [-20,20]
    ballPos = [600, 320]
    speedX = random.choice(speed)
    speedY = random.choice(speed)
    gameOver = False
    score = [0, 0]

    while True:
        imgGameOver = cv2.imread("picture\Gameover.png")
        _, img = cap.read()
        img = cv2.flip(img, 1)
        cTime = time.time()
        fps = 1/(cTime-pTime)
        pTime = cTime
        cv2.putText(img,f"FPS:{int(fps)}",(20,70),cv2.FONT_HERSHEY_PLAIN,3,(255, 255, 0),3)

        hands, img = detector.findHands(img, flipType=False)
        img = cv2.addWeighted(img, 0.2, imgBackground, 0.8, 0)

        if hands:
            for hand in hands:
                x, y, w, h = hand['bbox']
                h1, w1 = imgBat1.shape[0:2]
                y1 = y - h1 // 2
                y1 = np.clip(y1, 20, 520)

                if hand['type'] == "Left":
                    img = cvzone.overlayPNG(img, imgBat1, (60, y1))
                    if 70 < ballPos[0] < 70 + w1 and y1-h1//3 < ballPos[1] < y1 + h1:
                        speedX = -speedX
                        ballPos[0] + = 30
                        score[0] + = 1
                        Beep(1046,50)

                if hand['type'] == "Right":
                    img = cvzone.overlayPNG(img, imgBat2, (1190, y1))
                    if 1140 - w1 < ballPos[0] < 1140 and y1-h1//3 < ballPos[1] < y1 + h1:
                        speedX = -speedX
                        ballPos[0] -= 30
                        score[1] + = 1
                        Beep(1046,50)

        if ballPos[0] < 40 or ballPos[0] > 1160:
            gameOver=True

        if gameOver:
            img = imgGameOver
            cvzone.putTextRect(img, f"'R' begin or 'ESC' end", (400,100))
            if score[0]>score[1]:
                cv2.putText(img, str('WIN'), (250, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255,0,255), 5)
                cv2.putText(img, str('LOSE'), (850, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (34, 148, 83), 5)
            elif score[1]>score[0]:
                cv2.putText(img, str('LOSE'), (250, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (34, 148, 83), 5)
                cv2.putText(img, str('WIN'), (850, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255,0,255), 5)
            elif score[0]==score[1]:
                cv2.putText(img, str('DRAW'), (500, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (192, 44, 56), 5)
            
        else:
            if ballPos[1] >= 600 or ballPos[1] <= 10:
                speedY = -speedY
            ballPos[0] + = speedX
            ballPos[1] + = speedY
            img = cvzone.overlayPNG(img, imgBall, ballPos)
            cv2.putText(img, str(score[0]), (300, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255, 255, 255), 5)
            cv2.putText(img, str(score[1]), (900, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255, 255, 255), 5)

        cv2.imshow("Both_hands", img)

        key = cv2.waitKey(1)
        if key == ord('R'):
            ballPos = [600, 320]
            speedX = random.choice(speed)
            speedY = random.choice(speed)
            gameOver = False
            score = [0, 0]

        elif key == 27:
            break

    cv2.destroyAllWindows()




def One_hand(self):

    detector = HandDetector(detectionCon=0.8, maxHands=1)
    pTime = 0
    cTime = 0
    speed = [-15,15]
    ballPos = [600, 320]
    speedX = random.choice(speed)
    speedY = random.choice(speed)
    gameOver = False
    score = [0, 0]

    while True:

        imgGameOver = cv2.imread("picture\Gameover.png")
        _, img = cap.read()
        img = cv2.flip(img, 1)
        cTime = time.time()
        fps = 1/(cTime-pTime)
        pTime = cTime
        cv2.putText(img,f"FPS:{int(fps)}",(20,70),cv2.FONT_HERSHEY_PLAIN,3,(255, 255, 0),3)
        
        hands, img = detector.findHands(img, flipType=False)
        img = cv2.addWeighted(img, 0.2, imgBackground, 0.8, 0)

        if hands:

            for hand in hands:
                x, y, w, h = hand['bbox']
                h1, w1 = imgBat1.shape[0:2]
                y1 = y - h1 // 2
                y1 = np.clip(y1, 20, 520)

                if hand['type'] == "Left":
                    img = cvzone.overlayPNG(img, imgBat1, (60, y1))
                    if 70 < ballPos[0] < 70 + w1 and y1-h1//3 < ballPos[1] < y1 + h1:
                        speedX = -speedX
                        ballPos[0] + = 30
                        score[0] + = 1
                        Beep(1046,50)
                    if ballPos[0] < 40:
                        gameOver=True
                    if ballPos[0] >= 1200:
                        speedX = -speedX
                    cv2.putText(img, str(score[0]), (300, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255, 255, 255), 5)

                if hand['type'] == "Right":
                    img = cvzone.overlayPNG(img, imgBat2, (1190, y1))
                    if 1140 - w1 < ballPos[0] < 1140 and y1-h1//3 < ballPos[1] < y1 + h1:
                        speedX = -speedX
                        ballPos[0] -= 30
                        score[1] + = 1
                        Beep(1046,50)
                    if ballPos[0] > 1160:
                        gameOver=True
                    if ballPos[0] <= 20:
                        speedX = -speedX
                    cv2.putText(img, str(score[1]), (900, 650), cv2.FONT_HERSHEY_COMPLEX, 3, (255, 255, 255), 5)

        if not hands and (ballPos[0] < 40 or ballPos[0] > 1160):
            gameOver=True

        if gameOver:
            img = imgGameOver
            cvzone.putTextRect(img, f"'R' begin or 'ESC' end", (400,100))
  
        else:
            if ballPos[1] >= 600 or ballPos[1] <= 10:
                speedY = -speedY
            ballPos[0] + = speedX
            ballPos[1] + = speedY
            img = cvzone.overlayPNG(img, imgBall, ballPos)

        cv2.imshow("One_hand", img)

        key = cv2.waitKey(1)

        if key == ord('R'):
            ballPos = [600, 320]
            speedX = random.choice(speed)
            speedY = random.choice(speed)
            gameOver = False
            score = [0, 0]
        elif key == 27:
            break

    cv2.destroyAllWindows()

UI_Set.py

# -*- coding: utf-8 -*-

# Form implementation generated from reading ui file 'e:\my_opencv\pingpong\pingpong5.0\First.ui'
#
# Created by: PyQt5 UI code generator 5.15.9
#
# WARNING: Any manual changes made to this file will be lost when pyuic5 is
# run again. Do not edit this file unless you know what you are doing.


from PyQt5 import QtCore, QtGui, QtWidgets


class Ui_Game(object):
    def setupUi(self, Game):
        Game.setObjectName("Game")
        Game.setEnabled(True)
        Game.resize(854, 716)
        Game.setCursor(QtGui.QCursor(QtCore.Qt.ArrowCursor))
        self.textEdit = QtWidgets.QTextEdit(Game)
        self.textEdit.setGeometry(QtCore.QRect(11, 11, 831, 511))
        self.textEdit.viewport().setProperty("cursor", QtGui.QCursor(QtCore.Qt.ArrowCursor))
        self.textEdit.setStyleSheet("background-color: rgb(255, 255, 255,60)")
        self.textEdit.setObjectName("textEdit")
        self.Button1 = QtWidgets.QPushButton(Game)
        self.Button1.setGeometry(QtCore.QRect(20, 550, 231, 131))
        self.Button1.setCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
        self.Button1.setStyleSheet("background-color: rgb(255, 255, 255,60)")
        self.Button1.setObjectName("Button1")
        self.Button2 = QtWidgets.QPushButton(Game)
        self.Button2.setGeometry(QtCore.QRect(590, 550, 241, 131))
        self.Button2.setCursor(QtGui.QCursor(QtCore.Qt.OpenHandCursor))
        self.Button2.setStyleSheet("background-color: rgb(255, 255, 255,60)")
        self.Button2.setObjectName("Button2")

        self.retranslateUi(Game)
        QtCore.QMetaObject.connectSlotsByName(Game)

    def retranslateUi(self, Game):
        _translate = QtCore.QCoreApplication.translate
        Game.setWindowTitle(_translate("Game", "Form"))
        self.textEdit.setHtml(_translate("Game", "<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC- html40/strict.dtd">\
"
"<html><head><meta name="qrichtext" content="1" /><style type="text/css">\
"
"p, li { white-space: pre-wrap; }\
"
"</style></head><body style=" font-family:'SimSun'; font-size:9pt; font-weight:400; font-style:normal;">\
"
"<p align="center" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt- block-indent:0; text-indent:0px;"><br /></p>\
"
"<p align="center" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt- block-indent:0; text-indent:0px;"><br /></p>\
"
"<p align="center" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt- block-indent:0; text-indent:0px; font-size:48pt; color:#ff0000;"><br /></p>\
"
"<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent :0px;"><span style=" font-size:48pt; color:#ff0000;">Fun table tennis</span></p>\
"
"<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent :0px;"><span style=" font-size:18pt; color:#0000ff;">Fried Chicken is a fun gesture decompression game, easy to use</span></p>\
"
"<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent :0px;"><span style=" font-size:20pt; color:#000000;">Game Rules</span><span style=" font-size:20pt; color:#550000;\ ">:</span><span style=" font-size:16pt; color:#550000;">The racket hits the ball to score points. The ball will bounce when it hits the upper and lower sides. The game ends when the ball goes out of bounds. Press </span>< span style=" font-size:16pt; color:#00ff00;">ESC to exit</span><span style=" font-size:16pt; color:#550000;">Game or press</span> span><span style=" font-size:16pt; color:#ff5500;">R restart</span><span style=" font-size:16pt; color:#550000;">Game </span></p>\
"
"<p align="center" style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt- block-indent:0; text-indent:0px; font-size:16pt; color:#550000;"><br /></p>\
"
"<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent :0px;"><span style=" font-size:20pt; color:#000000;"> One-handed mode:</span><span style=" font-size:16pt; color:#000000 ;">When one side of the racket appears, the opposite boundary can allow the ball to bounce</span></p>\
"
"<p align="center" style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent :0px;"><span style=" font-size:20pt; color:#000000;"> Two-handed mode:</span><span style=" font-size:16pt; color:#000000; ">Only the up and down bounces, the left and right rackets need to be controlled</span></p>\
"
"<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text -indent:0px; font-size:24pt; color:#550000;"><br /></p></body></html>"))
        self.Button1.setText(_translate("Game", "One-handed mode"))
        self.Button2.setText(_translate("Game", "Two-Handed Mode"))

Materials

End

This is my first time blogging, and to be honest, I quite like this game. In the first version, mistakes were often made, such as the racket failing to hit the ball, etc. The one-handed mode later came out, which was also quite fun. If you like it, you can click it to favorites and follow it. If there is anything wrong, please point it out.

Reference

https://blog.csdn.net/weixin_42506516/article/details/125393153