在此项目中,我们将使用三个基于 OpenCV 和 TensorFlow 的 py 文件来实现石头剪刀布游戏,这些代码来源于磐怼怼大神,它们的作用分别是:
这篇文章会对这些代码进行详细讲解。
这个文件的作用是调用摄像头来获取图片数据的,这样就可以制作自己的数据集了。其中,所有的图片尺寸都是 300x300 的。
import numpy as np
import matplotlib.pyplot as plt
import cv2
import os
import sys
cap = cv2.VideoCapture(0) # 打开摄像头
shape = ['rock', 'paper', 'scissor'] # 三种手势
# 为每个手势建立一个文件夹,分别为 './Images/rock','./Images/paper','./Images/scissor'
dir_name = ['Images/'+shape[i] for i in range(len(shape))]
ct = 0 # 初始化计数器
maxCt = 4 # 每个手势保存 maxCt 张图片
print("Hit Space to Capture Image")
for i in range(3):
# 如果目录下没有此文件夹,则建立一个
try:
os.mkdir('./'+dir_name[i])
except FileExistsError:
pass
# 逐帧读取摄像头记录的视频
while True:
ret, frame = cap.read(0)
cv2.imshow('Get Data : '+dir_name[i], frame[50:350,100:400])
if cv2.waitKey(1) & 0xFF == ord(' '):
cv2.imwrite('./'+dir_name[i]+'/'+shape[i]+'{}.jpg'.format(ct), frame[50:350,100:400]) # 将此帧图片保存到相应文件夹
print(label[i]+'/'+shape[i]+'{}.jpg Captured'.format(ct))
ct+=1 # 计数器加一
if ct >= maxCt: # 如果计数达到 maxCt
ct = 0 # 重置计数器
break # 退出 while 循环
if i == 2: # 当三个手势的图片都保存完,退出 for 循环
break
cap.release() # 释放摄像头
cv2.destroyAllWindows() # 关闭窗口
运行 getData.py 后,在同一文件夹下将会出现一个新的文件夹 Images,在这个文件夹下又存在 3 个文件夹名为 rock,paper 和 scissor,分别存储着三种不同的手势图片。
此文件旨在对制作的数据集进行数据增强并用这些数据来训练模型。
import tensorflow as tf
import numpy as np
import cv2
import matplotlib.pyplot as plt
from skimage import io
import os
import pickle
import sys
import tensorflow as tf
from tensorflow.keras.models import Sequential,load_model
from tensorflow.keras.layers import Dense,MaxPool2D,Dropout,Flatten,Conv2D,GlobalAveragePooling2D,Activation
from tensorflow.keras.callbacks import ModelCheckpoint, EarlyStopping
from tensorflow.keras.optimizers import Adam
from tensorflow.keras.applications.densenet import DenseNet121
"""处理数据"""
DATA_PATH = './Images' # 含有所有图片的总文件夹
# shape_to_label 字典:键为不同手势,值为不同手势对应的独热编码
shape_to_label = {'rock':np.array([1.,0.,0.]),'paper':np.array([0.,1.,0.]),'scissor':np.array([0.,0.,1.])}
# arr_to_shape 字典:键为不同手势的代号,值为不同手势的名称 --> {0: 'rock', 1: 'paper', 2: 'scissor'}
arr_to_shape = {np.argmax(shape_to_label[x]):x for x in shape_to_label.keys()}
imgData = list() # 初始化图片特征
labels = list() # 初始化图片标签
for dr in os.listdir(DATA_PATH): # 遍历总文件夹下的所有子文件夹('rock','paper','scissor' 等)
if dr not in ['rock','paper','scissor']: # 不属于 'rock','paper','scissor' 的文件夹被跳过
continue
lb = shape_to_label[dr] # 判断该文件夹中图片对应的独热编码
i = 0 # 用 i 记录含有该手势的图片数量
for pic in os.listdir(os.path.join(DATA_PATH, dr)): # 遍历该文件夹中的所有图片
path = os.path.join(DATA_PATH, dr+'/'+pic) # 图片路径
img = cv2.imread(path) # 导入图片
imgData.append([img,lb]) # 原始图片+对应的独热编码
# 数据增强
imgData.append([cv2.flip(img, 1),lb]) # 水平翻转图片+对应的独热编码
imgData.append([cv2.resize(img[50:250,50:250],(300,300)),lb]) # 剪裁图片+对应的独热编码
i+=3 # 循环一次,imgData 中增加 3 张图片
imgData = list() # 初始化图片特征
labels = list() # 初始化图片标签
for dr in os.listdir(DATA_PATH): # 遍历总文件夹下的所有子文件夹('rock','paper','scissor' 等)
if dr not in ['rock','paper','scissor']: # 不属于 'rock','paper','scissor' 的文件夹被跳过
continue
lb = shape_to_label[dr] # 判断该文件夹中图片对应的独热编码
i = 0 # 用 i 记录含有该手势的图片数量
for pic in os.listdir(os.path.join(DATA_PATH, dr)): # 遍历该文件夹中的所有图片
path = os.path.join(DATA_PATH, dr+'/'+pic) # 图片路径
img = cv2.imread(path) # 导入图片
imgData.append([img,lb]) # 原始图片+对应的独热编码
# 数据增强
imgData.append([cv2.flip(img, 1),lb]) # 水平翻转图片+对应的独热编码
imgData.append([cv2.resize(img[50:250,50:250],(300,300)),lb]) # 剪裁图片+对应的独热编码
i+=3 # 循环一次,imgData 中增加 3 张图片
"""建立模型"""
# 迁移学习导入 DenseNet 模型
densenet = DenseNet121(include_top=False, weights='imagenet', classes=3,input_shape=(300,300,3))
densenet.trainable=True
def genericModel(base):
model = Sequential()
model.add(base)
model.add(MaxPool2D())
model.add(Flatten())
model.add(Dense(3,activation='softmax'))
model.compile(optimizer=Adam(),loss='categorical_crossentropy',metrics=['acc'])
return model
dnet = genericModel(densenet)
# 设立时间点用来保存权值
checkpoint = ModelCheckpoint(
'model.h5',
monitor='val_acc',
verbose=1,
save_best_only=True,
save_weights_only=True,
mode='auto'
)
es = EarlyStopping(patience = 3)
# 训练模型
history = dnet.fit(
x=imgData,
y=labels,
batch_size = 16,
epochs=30,
callbacks=[checkpoint,es],
validation_split=0.2
)
# 保存模型权值
dnet.save_weights('model.h5')
# 将模型写入 json 文件
with open("model.json", "w") as json_file:
json_file.write(dnet.to_json())
运行 getData.py 后,在同一文件夹下将会出现 model.h5 文件和 model.json 文件,其中包含着模型的权值。
此文件是最终的游戏文件,通过将 OpenCV 捕捉到的图片输入上一个文件建造的模型中来预测玩家手势,进而判断玩家和电脑的胜负情况。
from tensorflow.keras.models import model_from_json
import numpy as np
from skimage import io
import cv2
import random
# 将图片尺寸 resize 成 300x300,然后将其 reshape 成 (1,300,300,3)
def prepImg(pth):
return cv2.resize(pth,(300,300)).reshape(1,300,300,3)
# 通过判断玩家手势 play 和电脑手势 bplay 来更新胜负情况 p 和 b
def updateScore(play,bplay,p,b):
winRule = {'rock':'scissor','scissor':'paper','paper':'rock'}
if play == bplay: # 玩家和电脑所出手势相同
return p,b
elif bplay == winRule[play]: # 玩家胜
return p+1,b
else: # 电脑胜
return p,b+1
# 读取 json 文件和 h5 文件来获取模型权重(导入网络)
with open('model.json', 'r') as f:
loaded_model_json = f.read()
loaded_model = model_from_json(loaded_model_json)
loaded_model.load_weights("model.h5")
print("Loaded model from disk")
shape_to_label = {'rock':np.array([1.,0.,0.]),'paper':np.array([0.,1.,0.]),'scissor':np.array([0.,0.,1.])}
arr_to_shape = {np.argmax(shape_to_label[x]):x for x in shape_to_label.keys()}
options = ['rock','paper','scissor']
winRule = {'rock':'scissor','scissor':'paper','paper':'rock'}
rounds = 0 # 进行的轮数
botScore = 0 # 电脑的得分
playerScore = 0 # 玩家的得分
NUM_ROUNDS = 3 # 总轮数
cap = cv2.VideoCapture(0) # 打开摄像头
ret,frame = cap.read()
loaded_model.predict(prepImg(frame[50:350,100:400]))
bplay = ""
while True:
ret , frame = cap.read()
# 按空格进入游戏
frame = cv2.putText(frame,"Press Space to start",(160,200),cv2.FONT_HERSHEY_SIMPLEX,1,(250,250,0),2,cv2.LINE_AA)
cv2.imshow('Rock Paper Scissor',frame)
if cv2.waitKey(1) & 0xff == ord(' '):
break
for rounds in range(NUM_ROUNDS):
pred = ""
for i in range(90): # 每轮游戏在 90 次循环内完成
ret,frame = cap.read()
# 倒计时,此时玩家考虑出什么手势
if i//20 < 3 : # 循环执行次数 i < 60
frame = cv2.putText(frame,str(i//20+1),(320,100),cv2.FONT_HERSHEY_SIMPLEX,3,(250,250,0),2,cv2.LINE_AA)
# 预测玩家手势
elif i/20 < 3.5: # 60 =< 循环执行次数 i < 70
pred = arr_to_shape[np.argmax(loaded_model.predict(prepImg(frame[50:350,100:400])))]
# 得到电脑所出手势
elif i/20 == 3.5: # 循环执行次数 i = 70
bplay = random.choice(options)
print(pred,bplay) # 打印出玩家所出手势和电脑所出手势
# 更新玩家和电脑的得分
elif i//20 == 4: # 80 < 循环执行次数 i < 89
playerScore,botScore = updateScore(pred,bplay,playerScore,botScore)
break
cv2.rectangle(frame, (50, 100), (350, 400), (255, 255, 255), 2) # 玩家需要在这个矩形框中比出手势
frame = cv2.putText(frame,"Player : {} Bot : {}".format(playerScore,botScore),(120,400),cv2.FONT_HERSHEY_SIMPLEX,1,(250,250,0),2,cv2.LINE_AA) # 玩家得分,电脑得分
frame = cv2.putText(frame,pred,(150,140),cv2.FONT_HERSHEY_SIMPLEX,1,(250,250,0),2,cv2.LINE_AA) # 预测的玩家手势
frame = cv2.putText(frame,"Bot Played : {}".format(bplay),(300,140),cv2.FONT_HERSHEY_SIMPLEX,1,(250,250,0),2,cv2.LINE_AA) # 电脑手势
cv2.imshow('Rock Paper Scissor',frame)
if cv2.waitKey(1) & 0xff == ord('q'): # 按 q 退出游戏
break
if playerScore > botScore: # 玩家胜
winner = "You Won!!"
elif playerScore == botScore: # 平局
winner = "Its a Tie"
else: # 电脑胜
winner = "Bot Won.."
while True:
ret,frame = cap.read()
frame = cv2.putText(frame,winner,(230,150),cv2.FONT_HERSHEY_SIMPLEX,1,(250,250,0),2,cv2.LINE_AA) # 显示胜者
frame = cv2.putText(frame,"Press q to quit",(190,200),cv2.FONT_HERSHEY_SIMPLEX,1,(250,250,0),2,cv2.LINE_AA) # 显示'按 q 退出游戏'
frame = cv2.putText(frame,"Player : {} Bot : {}".format(playerScore,botScore),(120,400),cv2.FONT_HERSHEY_SIMPLEX,1,(250,250,0),2,cv2.LINE_AA) # 显示玩家得分和电脑得分
cv2.imshow('Rock Paper Scissor',frame)
if cv2.waitKey(1) & 0xff == ord('q'):
break
cap.release()
cv2.destroyAllWindows()