Наверняка все вы хоть раз в своей жизни играли в игру "камень, ножницы, бумага", в которой каждый из игроков с помощью своей руки изображает один из этих предметов. Правила этой игры очень просты:
- бумага побеждает камень;
- ножницы побеждают бумагу;
- камень побеждает ножницы.
В данной статье мы рассмотрим как играть в игру "камень, ножницы, бумага" с помощью платы Raspberry Pi и библиотек OpenCV и Tensorflow. Используя данные библиотеки мы будем распознавать жесты пользователя и, таким образом, идентифицировать какой из трех предметов он выбрал. Наш проект будет состоять из трех основных этапов:
- сбор данных;
- обучение (тренировка модели);
- распознавание жестов.
На первом этапе мы соберем изображения, на которых показаны камень, ножницы, бумага (с помощью жестов рук) и изображение без жеста (пустое). Пустое изображение нужно для того, чтобы плата Raspberry Pi не делала ненужных действий. Набор данных для нашего проекта будет состоять из 800 изображений, относящихся к 4-м классам. На втором этапе мы будем тренировать (обучать) наш распознаватель (Recognizer) обнаруживать (распознавать) жесты, сделанные пользователем. После распознавания жеста пользователя плата Raspberry Pi случайным образом выбирает один из этих предметов (камень, ножницы, бумага), после чего сравнивает свой предмет с предметом пользователя и выбирает победителя (она или пользователь).
Также на нашем сайте вы можете посмотреть аналогичные проекты, в которых использовалась обработка изображений с помощью платы Raspberry Pi и библиотеки OpenCV:
- определение социальной дистанции с помощью Raspberry Pi и OpenCV;
- обнаружение движения на видео с помощью Raspberry Pi и OpenCV;
- определение пола и возраста людей с помощью Raspberry Pi и OpenCV;
- обнаружение масок на лицах людей с помощью Raspberry Pi и OpenCV;
- распознавание лиц с помощью Raspberry Pi и библиотеки OpenCV.
Необходимые компоненты
- Плата Raspberry Pi (купить на AliExpress).
- Камера для Raspberry Pi (купить на AliExpress).
Установка OpenCV и других необходимых пакетов
Перед установкой библиотеки OpenCV на плату Raspberry Pi обновите ее программное обеспечение до последней версии с помощью команды:
1 |
sudo apt-get update |
Затем установим ряд дополнений, которые будут необходимы для последующей установки OpenCV.
1 2 3 4 5 6 |
sudo apt-get install libhdf5-dev -y sudo apt-get install libhdf5-serial-dev –y sudo apt-get install libatlas-base-dev –y sudo apt-get install libjasper-dev -y sudo apt-get install libqtgui4 –y sudo apt-get install libqt4-test –y |
После этого установим библиотеку OpenCV на плату Raspberry Pi с помощью команды:
1 |
pip3 install opencv-contrib-python==4.1.0.25 |
Затем установим пакет imutils, с помощью которого можно выполнять различные функции обработки изображений, например, перевод, вращение, изменение размера, скелетонизация. Также imutils упрощает отображение изображений Matplotlib. Для установки данного пакета используйте команду:
1 |
pip3 install imutils |
Далее установим пакет Tensorflow с помощью команды:
1 |
sudo pip3 install https://github.com/lhelontra/tensorflow-on-arm/releases/download/v2.2.0/tensorflow-2.2.0-cp37-none-linux_armv7l.whl |
Также нам будет необходима библиотека sklearn (Scikit-learn), написанная на python. Она обеспечивает широкий набор контролируемых и неконтролируемых обучающих алгоритмов с помощью совместимого с Python интерфейса. В нашем проекте мы будем использовать ее для построения модели машинного обучения (machine learning) для распознавания жестов руки. Для ее установки используйте следующую команду:
1 |
pip3 install sklearn |
Дополнительно в нашем проекте нам еще понадобится модель глубокой нейронной сети (deep neural network) SqueezeNet для технологии компьютерного зрения (computer vision). SqueezeNet представляет собой модель небольшой нейронной сети, которая работает на основе фреймворка глубокого обучения Caffe. Мы будем использовать SqueezeNet поскольку она отличается от других подобных инструментов маленьким размером и точностью работы. Благодаря маленькому размеру она хорошо подходит для установки на аппаратные средства с ограниченным объемом памяти. Установить keras_squeezenet можно с помощью команды:
1 |
pip3 install keras_squeezenet |
Объяснение программы распознавания жестов руки для Raspberry Pi
Полный код всех программ приведен в конце статьи, здесь же мы кратко рассмотрим их основные фрагменты.
Как мы уже отмечали ранее, наш проект будет состоять из 3-х этапов: на первом этапе будет выполняться сбор информации, на втором – обучение (тренировка) модели, а на третьем – распознавание жестов руки пользователя. Каталог нашего проекта будет включать следующие элементы:
- набор данных (Dataset): он будет содержать изображения камня, ножницы, бумаги и изображение "пустого" жеста;
- image.py: небольшой скрипт на python для сбора изображений, необходимых для построения набора данных;
- training.py: этот скрипт будет считывать наш набор данных и производить точную настройку Squeezenet чтобы создать модель распознавания жестов (game-model.h5);
- game.py: скрипт, который на основе нашей обученной модели и набора данных будет распознавать жесты пользователя (камень, ножницы, бумага) и осуществлять случайный выбор одного из этих предметов платой Raspberry Pi.
Весь каталог для данного проекта можно скачать по следующей ссылке.
Рассмотрим основные этапы работы нашего проекта более подробно.
1. Сбор данных
На первом этапе нашего проекта мы создадим набор данных, содержащий по 200 изображений для каждого класса: камень, ножницы, бумага и пустой жест. Image.py – это простой скрипт на python, который использует OpenCV для сбора изображений жестов. Откройте файл image.py в каталоге Game и вставьте в него ниже приведенный (в конце статьи) фрагмент кода. Затем начните сбор изображений с помощью команды:
1 |
python3 image.py 200 |
Число 200 в этой команде означает число изображений, которые мы будем захватывать. После запуска скрипта нажмите клавишу ‘r’ чтобы захватить изображения с жестом камня (Rock gesture) и затем нажмите ‘a’ чтобы начать процесс захвата изображений. Далее вам необходимо проделать аналогичные операции для захвата изображений бумаги (нажать ‘p’, paper), ножниц (нажать ‘s’, scissors) и пустого жеста (нажать ‘n’, nothing).
Рассмотрим ключевые фрагменты кода скрипта image.py. Вначале в нем нам необходимо подключить используемые пакеты.
1 2 3 |
import cv2 import os import sys |
Далее мы задаем число отсчетов, которые мы хотим собрать.
1 |
num_samples = int(sys.argv[1]) |
Затем укажем путь, по которому будут сохраняться все изображения.
1 |
IMG_SAVE_PATH = 'images' |
После этого создадим каталог, в котором будут сохраняться изображения.
1 2 3 4 |
try: os.mkdir(IMG_SAVE_PATH) except FileExistsError: pass |
Затем создадим прямоугольник с помощью функции cv2.rectangle. После запуска скрипта на выполнение вам необходимо будет поместить свои руки внутрь этого созданного прямоугольника.
1 |
cv2.rectangle(frame, (10, 30), (310, 330), (0, 255, 0), 2) |
В следующем фрагменте кода мы будем обрабатывать нажатия клавиш. Если пользователь нажимает клавишу ‘r’, то мы имени метки (label name) будем присваивать значение ‘rock’ и создавать каталог с именем rock внутри каталога image. Если пользователь нажимает клавишу ‘p’, то имени метки присваивается значение ‘paper’ и т.д.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
k = cv2.waitKey (1) if k == ord('r'): name = 'rock' IMG_CLASS_PATH = os.path.join(IMG_SAVE_PATH, name) os.mkdir(IMG_CLASS_PATH) if k == ord('p'): name = 'paper' IMG_CLASS_PATH = os.path.join(IMG_SAVE_PATH, name) os.mkdir(IMG_CLASS_PATH) if k == ord('s'): name = 'scissors' IMG_CLASS_PATH = os.path.join(IMG_SAVE_PATH, name) os.mkdir(IMG_CLASS_PATH) if k == ord('n'): name = 'nothing' IMG_CLASS_PATH = os.path.join(IMG_SAVE_PATH, name) os.mkdir(IMG_CLASS_PATH) |
Если нажата кнопка начала процесса (start button), то мы создаем интересующую нас область (Region of Interest, ROI) вокруг прямоугольника, который мы создали ранее, и сохраняем все изображения внутри каталога используя путь определенный ранее.
1 2 3 4 5 6 7 8 9 |
roi = frame[25:335, 8:315] save_path = os.path.join(IMG_CLASS_PATH, '{}.jpg'.format(counter + 1)) print(save_path) cv2.imwrite(save_path, roi) counter += 1 font = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(frame,"Collecting {}".format(counter), (10, 20), font, 0.7, (0, 255, 255), 2, cv2.LINE_AA) cv2.imshow("Collecting images", frame) |
2. Обучение (тренировка) модели
Теперь, когда мы собрали необходимые нам изображения, нам нужно передать их в нейронную сеть и начать процесс ее обучения чтобы в дальнейшем автоматически распознавать жесты пользователя. Для этого откройте файл training.py в каталоге Game и вставьте в него фрагмент кода, приведенный в конце данной статьи. Затем начните процесс обучения с помощью команды:
1 |
python3 training.py |
В скрипте training.py мы первым делом подключим (импортируем) необходимые библиотеки. Далее мы укажем путь к каталогу, в котором находятся необходимые нам изображения, и определим класс (class map), с которым мы далее будем работать в программе.
1 2 3 4 5 6 7 |
IMG_SAVE_PATH = 'images' CLASS_MAP = { "rock": 0, "paper": 1, "scissors": 2, "nothing": 3 } |
В следующих строках кода мы сконструируем головную часть модели, которая будет помещена на верх базовой модели. Функция (слой) AveragePooling2D рассчитывает среднее выходное значение каждой характеристической карты (feature map) изображения на предыдущем слое. Чтобы избежать переполнения мы используем 50% процент отсева.
1 2 3 4 5 6 7 8 9 |
def get_model(): model = Sequential([ SqueezeNet(input_shape=(227, 227, 3), include_top=False), Dropout(0.5), Convolution2D(NUM_CLASSES, (1, 1), padding='valid'), Activation('relu'), GlobalAveragePooling2D(), Activation('softmax') ]) |
Далее в цикле мы будем считывать все изображения, находящиеся в нашем каталоге image. После считывания изображений мы можем начать обучение модели. Но вначале необходимо выполнить шаги предварительной обработки изображений: конвертирование их из формата BGR в формат RGB, изменение их размера до 227×227 пикселов, преобразование их в формат массива (array format).
1 2 3 4 5 6 7 8 9 10 11 |
for directory in os.listdir(IMG_SAVE_PATH): path = os.path.join(IMG_SAVE_PATH, directory) if not os.path.isdir(path): continue for item in os.listdir(path): if item.startswith("."): continue img = cv2.imread(os.path.join(path, item)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (227, 227)) dataset.append([img, directory]) |
После того как наша модель распознавания будет готова мы будем компилировать ее с помощью оптимизатора Adam.
1 2 3 4 5 6 |
model = get_model() model.compile( optimizer=Adam(lr=0.0001), loss='categorical_crossentropy', metrics=['accuracy'] ) |
Затем мы начнем тренировку (обучение) модели и после того как обучение будет закончено, мы сохраним модель под именем “game-model.h5”.
1 2 |
model.fit(np.array(data), np.array(labels), epochs=15) model.save("game-model.h5") |
3. Распознавание жестов рук
В заключительной части нашего проекта мы будем использовать тренировочные данные для распознавания каждого жеста рук из видеопотока реального времени. Для этого откройте файл game.py в каталоге Game и вставьте в него фрагмент кода, приведенный в конце статьи.
В начале скрипта game.py мы подключим все необходимые пакеты. Затем создадим необходимый нам класс.
1 2 3 4 5 6 |
REV_CLASS_MAP = { 0: "rock", 1: "paper", 2: "scissors", 3: "nothing" } |
Функция calculate_winner в качестве входных аргументов будет использовать ход пользователя (то есть какой из трех предметов он показал с помощью жеста – камень, ножницы или бумага) и ход платы Raspberry Pi и затем на основании этого определять победителя – пользователя или плату.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
def calculate_winner(user_move, Pi_move): if user_move == Pi_move: return "Tie" elif user_move == "rock" and Pi_move == "scissors": return "You" elif user_move == "rock" and Pi_move == "paper": return "Pi" elif user_move == "scissors" and Pi_move == "rock": return "Pi" elif user_move == "scissors" and Pi_move == "paper": return "You" elif user_move == "paper" and Pi_move == "rock": return "You" elif user_move == "paper" and Pi_move == "scissors": return "Pi" |
В следующих строках кода мы будем считывать обученную модель и начнем захват видео потока реального времени.
1 2 |
model = load_model("game-model.h5") cap = cv2.VideoCapture(0) |
Затем внутри цикла мы будем создавать два прямоугольника – на левой и правой стороне кадра. В прямоугольнике на левой стороне будет отображаться ход пользователя, а в прямоугольнике на правой стороне – ход платы Raspberry Pi. После этого мы будем извлекать часть изображения кадра, находящуюся в прямоугольнике с ходом пользователя, преобразовывать ее в формат RGB и сжимать ее до размера 227×227.
1 2 3 4 5 |
cv2.rectangle(frame, (10, 70), (300, 340), (0, 255, 0), 2) cv2.rectangle(frame, (330, 70), (630, 370), (255, 0, 0), 2) roi = frame[70:300, 10:340] img = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (227, 227)) |
После этого мы будем использовать модель, которую натренировали ранее, и распознавать жест руки. Затем мы будем сравнивать полученный код с кодом в нашем классе (class map) и, таким образом, получать название жеста пользователя.
1 2 3 |
pred = model.predict(np.array([img])) move_code = np.argmax(pred[0]) user_move_name = mapper(move_code) |
В следующем фрагменте кода мы будем проверять сделал ли пользователь ход или нет. Если он сделал ход, то мы будем делать случайный ход платой Raspberry Pi и затем определять победителя. Если пользователь не сделал ход (то есть его ход равен ‘nothing’), то мы будем ждать пока пользователь сделает ход.
1 2 3 4 5 6 7 8 |
if prev_move != user_move_name: if user_move_name != "nothing": computer_move_name = choice(['rock', 'paper', 'scissors']) winner = calculate_winner(user_move_name, computer_move_name) else: computer_move_name = "nothing" winner = "Waiting..." prev_move = user_move_name |
Далее мы будем использовать функцию cv2.putText чтобы отобразить ходы пользователя и платы на экране, а также отобразить победителя.
1 2 3 4 5 6 7 |
font = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(frame, "Your Move: " + user_move_name, (10, 50), font, 1, (255, 255, 255), 2, cv2.LINE_AA) cv2.putText(frame, "Pi's Move: " + computer_move_name, (330, 50), font, 1, (255, 255, 255), 2, cv2.LINE_AA) cv2.putText(frame, "Winner: " + winner, (100, 450), font, 2, (0, 255, 0), 4, cv2.LINE_AA) |
Мы будем отображать ход платы Raspberry Pi внутри прямоугольника, который мы создали ранее. Плата будет случайным образом выбирать изображение, хранящееся в каталоге ‘test_img’.
1 2 3 4 5 |
if computer_move_name != "nothing": icon = cv2.imread( "test_img/{}.png".format(computer_move_name)) icon = cv2.resize(icon, (300, 300)) frame[70:370, 330:630] = icon |
Тестирование работы проекта
Перед тем, как запускать программу проекта на выполнение, подключите к плате Raspberry Pi модуль камеры как показано на следующем рисунке.
После этого запустите на выполнение скрипт game.py. Спустя несколько секунд после этого вы должны увидеть на экране всплывающее окно с двумя прямоугольниками. В левом прямоугольнике будет отображаться ход пользователя, а в правом – ход платы. Отрегулируйте положение своей руки внутри прямоугольника и сделайте ход. После того ка плата распознает ваш жест, она сделает свой ход и определит победителя, сравнив ваш ход и свой.
Более подробно работу проекта вы можете посмотреть на видео, приведенном в конце статьи.
Исходный код программ проекта на Python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
from keras.models import load_model import cv2 import numpy as np from random import choice REV_CLASS_MAP = { 0: "rock", 1: "paper", 2: "scissors", 3: "nothing" } def mapper(val): return REV_CLASS_MAP[val] def calculate_winner(user_move, Pi_move): if user_move == Pi_move: return "Tie" elif user_move == "rock" and Pi_move == "scissors": return "You" elif user_move == "rock" and Pi_move == "paper": return "Pi" elif user_move == "scissors" and Pi_move == "rock": return "Pi" elif user_move == "scissors" and Pi_move == "paper": return "You" elif user_move == "paper" and Pi_move == "rock": return "You" elif user_move == "paper" and Pi_move == "scissors": return "Pi" model = load_model("game-model.h5") cap = cv2.VideoCapture(0) prev_move = None while True: ret, frame = cap.read() if not ret: continue cv2.rectangle(frame, (10, 70), (300, 340), (0, 255, 0), 2) cv2.rectangle(frame, (330, 70), (630, 370), (255, 0, 0), 2) # извлекаем область изображения внутри прямоугольника пользователя roi = frame[70:300, 10:340] img = cv2.cvtColor(roi, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (227, 227)) # определяем сделанный ход pred = model.predict(np.array([img])) move_code = np.argmax(pred[0]) user_move_name = mapper(move_code) # определяем победителя if prev_move != user_move_name: if user_move_name != "nothing": computer_move_name = choice(['rock', 'paper', 'scissors']) winner = calculate_winner(user_move_name, computer_move_name) else: computer_move_name = "nothing" winner = "Waiting..." prev_move = user_move_name # отображаем информацию на экране font = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(frame, "Your Move: " + user_move_name, (10, 50), font, 1, (255, 255, 255), 2, cv2.LINE_AA) cv2.putText(frame, "Pi's Move: " + computer_move_name, (330, 50), font, 1, (255, 255, 255), 2, cv2.LINE_AA) cv2.putText(frame, "Winner: " + winner, (100, 450), font, 2, (0, 255, 0), 4, cv2.LINE_AA) if computer_move_name != "nothing": icon = cv2.imread( "test_img/{}.png".format(computer_move_name)) icon = cv2.resize(icon, (300, 300)) frame[70:370, 330:630] = icon cv2.imshow("Rock Paper Scissors", frame) k = cv2.waitKey(10) if k == ord('q'): break cap.release() cv2.destroyAllWindows() import cv2 import os import sys cam = cv2.VideoCapture(0) start = False counter = 0 num_samples = int(sys.argv[1]) IMG_SAVE_PATH = 'images' try: os.mkdir(IMG_SAVE_PATH) except FileExistsError: pass while True: ret, frame = cam.read() if not ret: print("failed to grab frame") break if counter == num_samples: break cv2.rectangle(frame, (10, 30), (310, 330), (0, 255, 0), 2) k = cv2.waitKey(1) if k == ord('r'): name = 'rock' IMG_CLASS_PATH = os.path.join(IMG_SAVE_PATH, name) os.mkdir(IMG_CLASS_PATH) if k == ord('p'): name = 'paper' IMG_CLASS_PATH = os.path.join(IMG_SAVE_PATH, name) os.mkdir(IMG_CLASS_PATH) if k == ord('s'): name = 'scissors' IMG_CLASS_PATH = os.path.join(IMG_SAVE_PATH, name) os.mkdir(IMG_CLASS_PATH) if k == ord('n'): name = 'nothing' IMG_CLASS_PATH = os.path.join(IMG_SAVE_PATH, name) os.mkdir(IMG_CLASS_PATH) if start: roi = frame[25:335, 8:315] save_path = os.path.join(IMG_CLASS_PATH, '{}.jpg'.format(counter + 1)) print(save_path) cv2.imwrite(save_path, roi) counter += 1 font = cv2.FONT_HERSHEY_SIMPLEX cv2.putText(frame,"Collecting {}".format(counter), (10, 20), font, 0.7, (0, 255, 255), 2, cv2.LINE_AA) cv2.imshow("Collecting images", frame) if k == ord('a'): start = not start if k == ord('q'): break print("\n{} image(s) saved to {}".format(counter, IMG_CLASS_PATH)) cam.release() cv2.destroyAllWindows() import cv2 import numpy as np from keras_squeezenet import SqueezeNet from keras.optimizers import Adam from keras.utils import np_utils from keras.layers import Activation, Dropout, Convolution2D, GlobalAveragePooling2D from keras.models import Sequential import tensorflow as tf import os IMG_SAVE_PATH = 'images' CLASS_MAP = { "rock": 0, "paper": 1, "scissors": 2, "nothing": 3 } NUM_CLASSES = len(CLASS_MAP) def mapper(val): return CLASS_MAP[val] def get_model(): model = Sequential([ SqueezeNet(input_shape=(227, 227, 3), include_top=False), Dropout(0.5), Convolution2D(NUM_CLASSES, (1, 1), padding='valid'), Activation('relu'), GlobalAveragePooling2D(), Activation('softmax') ]) return model # считываем изображения из каталога dataset = [] for directory in os.listdir(IMG_SAVE_PATH): path = os.path.join(IMG_SAVE_PATH, directory) if not os.path.isdir(path): continue for item in os.listdir(path): # убеждаемся что на нашем пути нет скрытых файлов if item.startswith("."): continue img = cv2.imread(os.path.join(path, item)) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = cv2.resize(img, (227, 227)) dataset.append([img, directory]) data, labels = zip(*dataset) labels = list(map(mapper, labels)) # one hot encode the labels labels = np_utils.to_categorical(labels) # определяем (получаем) модель model = get_model() model.compile( optimizer=Adam(lr=0.0001), loss='categorical_crossentropy', metrics=['accuracy'] ) # начинаем обучение модели model.fit(np.array(data), np.array(labels), epochs=15) # сохраняем модель для последующего использования model.save("game-model.h5") |
Добрый день!
При импорте keras_squeezenet выдает ошибку:
ImportError: cannot import name 'get_config' from 'tensorflow.python.eager.context' (/usr/local/lib/python3.7/dist-packages/tensorflow/python/eager/context.py)
Не знаете ли вы, как её разрешить.
Заранее спасибо.
Добрый вечер, пока нахожусь в отпуске, к сожалению, не могу помочь вам в вашем вопросе
Добрый день! Можно задать вопрос? У вас разрешилась эта проблема? Я сейчас пытаюсь ее решить