Русский
Русский
English
Статистика
Реклама

Как я научила свой компьютер играть в пары используя OpenCV и Глубокое обучение

Немного веселья с компьютерным зрением и CNN с маленькой базой данных.

Моё хобби это настольные игры и, поскольку я имею немного знаний о CNN, я решила сделать приложение, что может победить людей в карточной игре. Я хотела построить модель с нуля при помощи моей собственной базы данных, чтобы посмотреть, насколько хороша модель выйдет с нуля с маленькой базой данных. Было принято решение начать с не слишком сложной игры, Spot it! (она же, Пары).

В случае, если вы всё ещё не знаете об этой игре, вот короткое пояснение: Пары это простая игра на распознавание образов, в которой игроки пытаются найти изображения на двух карточках. В оригинальном Spot it!, на каждой карточке находятся по восемь картинок с разницей в размере между разными карточками. Любые две карточки имеют ровно по одной общей картинке. Если вы находите её первым, вы выигрываете карту. Как только колода из 55 карточек заканчивается, победа присуждается тому, у кого окажется больше карт.

Попробуйте сами: какой общий символ на карточках, показанных выше?Попробуйте сами: какой общий символ на карточках, показанных выше?

С чего начать?

Первым шагом в любом data science исследовании является сбор данных. Я сделала несколько фото на свой телефон, по шесть фото каждой карты. Итого у меня 330 картинок. Четыре из них показаны ниже. Вы можете подумать: а этого достаточно для создания полноценной Свёрточной Нейронной Сети (CNN)? Вернёмся к этому позже!

Обработка изображений

Хорошо, мы имеем данные, что дальше? Возможно, это самый важный пункт в пути к успеху: обработка изображений. Нам нужно обработать картинки, показанные на каждой карточке. Здесь возникает небольшие трудности. Вы можете видеть, что некоторые картинки достать сложнее: снеговик, привидение (третья картинка) и игла (четвёртая картинка) имеют яркий цвет, а пятна (вторая картинка) и восклицательный знак (четвёртая картинка) состоят из нескольких частей. После этого мы изменяем и сохраняем картинку.

Добавляем контраст

Мы используем цветовую систему Lab для изменения контраста. L означает яркость, a обозначает соотношение зелёного к фиолетовому, а b голубого к жёлтому. Мы легко можем извлечь эти компоненты при помощи OpenCV:

import cv2import imutilsimgname = 'picture1'image = cv2.imread(f{imgname}.jpg)lab = cv2.cvtColor(image, cv2.COLOR_BGR2LAB)l, a, b = cv2.split(lab)
Слева направо: оригинальное изображение, световая компонента, a компонента и b компонентаСлева направо: оригинальное изображение, световая компонента, a компонента и b компонента

Сейчас мы добавим контрастности к световой компоненте, сольём компоненты обратно и конвертируем картинку:

clahe = cv2.createCLAHE(clipLimit=3.0, tileGridSize=(8,8))cl = clahe.apply(l)limg = cv2.merge((cl,a,b))final = cv2.cvtColor(limg, cv2.COLOR_LAB2BGR)
Слева направо: оригинальное изображение, световая компонента, с увеличенным контрастом, конвертированная обратно в RGBСлева направо: оригинальное изображение, световая компонента, с увеличенным контрастом, конвертированная обратно в RGB

Масштабирование

Потом мы масштабируем и сохраняем картинку:

resized = cv2.resize(final, (800, 800))# сохраним изображениеcv2.imwrite(f'{imgname}processed.jpg', blurred)

Готово!

Обнаружение карточек и картинок

Сейчас картинки обработаны и мы можем начать с нахождения образов на фото. Можно найти их внешние контуры при помощи OpenCV. Затем надо будет конвертировать изображение в чёрно-белое, выбрать порог (в нашем случае, 190), чтобы найти контуры. В коде:

image = cv2.imread(f{imgname}processed.jpg)gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)thresh = cv2.threshold(gray, 190, 255, cv2.THRESH_BINARY)[1]# ищем контурыcnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)cnts = imutils.grab_contours(cnts)output = image.copy()# рисуем контуры на картинкеfor c in cnts:    cv2.drawContours(output, [c], -1, (255, 0, 0), 3)
Обрабатываемое изображение, конвертированное в чёрно-белое, разделённое по порогу и с контурамиОбрабатываемое изображение, конвертированное в чёрно-белое, разделённое по порогу и с контурами

После того, как мы отсортировали контуры по областям, мы можем найти контур с наибольшей площадью: это карточка. Мы можем создать белый фон, чтобы вытянуть картинки.

# сортируем по площади, берём наибольшуюcnts = sorted(cnts, key=cv2.contourArea, reverse=True)[0]# создаём маску по наибольшему контуруmask = np.zeros(gray.shape,np.uint8)mask = cv2.drawContours(mask, [cnts], -1, 255, cv2.FILLED)# карточку на передний планfg_masked = cv2.bitwise_and(image, image, mask=mask)# белый фон (используем инвертированную маску)mask = cv2.bitwise_not(mask)bk = np.full(image.shape, 255, dtype=np.uint8)bk_masked = cv2.bitwise_and(bk, bk, mask=mask)# сливаем фон и передний планfinal = cv2.bitwise_or(fg_masked, bk_masked)
Маска, фон, передний план, объединённоеМаска, фон, передний план, объединённое

А сейчас пора выделять картинки! Мы можем использовать предидущее изображение, чтобы вновь определить внешние контуры они и есть картинки. Если мы выделим площадь вокруг каждой картинки, мы сможем извлечь её. В этот раз код немножко длиннее:

# прямо как и в предидущем случае (с удалением карточки)gray = cv2.cvtColor(final, cv2.COLOR_RGB2GRAY)thresh = cv2.threshold(gray, 195, 255, cv2.THRESH_BINARY)[1]thresh = cv2.bitwise_not(thresh)cnts = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)cnts = imutils.grab_contours(cnts)cnts = sorted(cnts, key=cv2.contourArea, reverse=True)[:10]# обрабатываем каждый контурi = 0for c in cnts:    if cv2.contourArea(c) > 1000:        # рисуем маску, оставляем контур        mask = np.zeros(gray.shape, np.uint8)        mask = cv2.drawContours(mask, [c], -1, 255, cv2.FILLED)        # белый фон        fg_masked = cv2.bitwise_and(image, image, mask=mask)        mask = cv2.bitwise_not(mask)        bk = np.full(image.shape, 255, dtype=np.uint8)        bk_masked = cv2.bitwise_and(bk, bk, mask=mask)        finalcont = cv2.bitwise_or(fg_masked, bk_masked)        # ограничивающая область по контуру        output = finalcont.copy()        x,y,w,h = cv2.boundingRect(c)        # squares io rectangles        if w < h:            x += int((w-h)/2)            w = h        else:            y += int((h-w)/2)            h = w        # вырезаем область с картинкой        roi = finalcont[y:y+h, x:x+w]        roi = cv2.resize(roi, (400,400))        # сохраняем картинку        cv2.imwrite(f"{imgname}_icon{i}.jpg", roi)        i += 1
Разделённое по порогу, с определёнными контурами, картинки призрака и сердца (вырезанные по маске)Разделённое по порогу, с определёнными контурами, картинки призрака и сердца (вырезанные по маске)

Сортировка картинок

Сейчас начинается скучная часть! Время сортировки картинок. Нам нужны папки теста, трейна и валидации, содержащие по 57 подпапок каждая (у нас 57 различных картинок). Структура каталога выглядит так:

symbols  test     anchor     apple       ...     zebra  train     anchor     apple       ...     zebra  validation      anchor      apple        ...      zebra

Потребуется время, Чтобы поместить все извлечённые картинки в нужные каталоги (более 2500)! У меня есть код для создания подпапок, набор тестов и проверок на GitHub. Может, в следующий раз будет лучше провести сортировку алгоритмом кластеризации

Обучение Свёрточной Нейронной Сети (CNN)

После скучной части наступает крутая часть. Давайте сделаем и обучим CNN. Вы можете найти информацию о CNN в этом посте.

Архитектура модели

Это задача многоклассовой классификации с одной меткой. Нам нужна одна метка для каждого символа. Вот почему необходимо выбрать softmax активации последнего уровня с 57 нейронами и категориальной функцией потерь перекрёстной энтропии.

Архитектура итоговой модели выглядит так:

# импортfrom keras import layersfrom keras import modelsfrom keras import optimizersfrom keras.preprocessing.image import ImageDataGeneratorimport matplotlib.pyplot as plt# слои, активационный слой с 57 нейронами (по одному на каждый символ)model = models.Sequential()model.add(layers.Conv2D(32, (3, 3), activation='relu', input_shape=(400, 400, 3)))model.add(layers.MaxPooling2D((2, 2)))  model.add(layers.Conv2D(64, (3, 3), activation='relu'))model.add(layers.MaxPooling2D((2, 2)))model.add(layers.Conv2D(128, (3, 3), activation='relu'))model.add(layers.MaxPooling2D((2, 2)))model.add(layers.Conv2D(256, (3, 3), activation='relu'))model.add(layers.MaxPooling2D((2, 2)))model.add(layers.Conv2D(256, (3, 3), activation='relu'))model.add(layers.MaxPooling2D((2, 2)))model.add(layers.Conv2D(128, (3, 3), activation='relu'))model.add(layers.Flatten())model.add(layers.Dropout(0.5)) model.add(layers.Dense(512, activation='relu'))model.add(layers.Dense(57, activation='softmax'))model.compile(loss='categorical_crossentropy',       optimizer=optimizers.RMSprop(lr=1e-4), metrics=['acc'])

Аугментация данных

Для лучшей производительности я использовала аугментацию данных. Аугментация данных это процесс увеличения количества и разнообразия входных данных. Это возможно путем поворота, сдвига, масштабирования, обрезки и отражения существующих изображений. Аугментацию данных легко выполнить с Keras:

# определим папкиtrain_dir = 'symbols/train'validation_dir = 'symbols/validation'test_dir = 'symbols/test'# аугментация данных при помощи ImageDataGenerator из Keras (только для тренировки)train_datagen = ImageDataGenerator(rescale=1./255, rotation_range=40, width_shift_range=0.1, height_shift_range=0.1, shear_range=0.1, zoom_range=0.1, horizontal_flip=True, vertical_flip=True)test_datagen = ImageDataGenerator(rescale=1./255)train_generator = train_datagen.flow_from_directory(train_dir, target_size=(400,400), batch_size=20, class_mode='categorical')validation_generator = test_datagen.flow_from_directory(validation_dir, target_size=(400,400), batch_size=20, class_mode='categorical')

Если вам интересно, аугментирование привидения выглядит так:

Оригинальное привидение слева, на других изображениях примеры аугментирования данныхОригинальное привидение слева, на других изображениях примеры аугментирования данных

Подгон модели

Пора бы подогнать модель, сохранить ее для прогнозов и проверить результаты.

history = model.fit_generator(train_generator, steps_per_epoch=100, epochs=100, validation_data=validation_generator, validation_steps=50)# не забывайте сохранить вашу модельmodel.save('models/model.h5')

Полученные результаты

Базовая модель, которую я обучила, была без аугментации и исключения данных и имела меньше слоев. Эта модель дала следующие результаты:

Результаты базовой моделиРезультаты базовой модели

Вы можете ясно видеть, что эта модель обучается. Результаты итоговой модели (из кода в предыдущих абзацах) намного лучше. На изображении ниже вы можете увидеть точность и потери трейна и валидации.

Результаты итоговой моделиРезультаты итоговой модели

На тестовом наборе эта модель допустила только одну ошибку: она назвала каплю вместо бомбы. Я решила остановиться на модели, точность которой составила 0,995 на тестовом наборе.

Найдите общую картинку двух карточек

Теперь можно угадать общую картинку двух карточек. Мы можем взять два изображения, идентифицировать каждую картинку отдельно и использовать пересечение, чтобы увидеть, какая картинка есть на обеих карточках. У этого есть три возможных исхода:

  • Что-то пошло не так: не найдено общих картинок.

  • На ровно одна общая картинка (может быть правильной или неправильной).

  • Есть больше одной общей картинки. В данном случае я выбрала картинку с наибольшей вероятностью (среднее значение обоих прогнозов).

Код находится на GitHub для прогнозирования всех комбинаций двух изображений в каталоге, файле main.py.

Некоторые результаты:


Заключение

Это идеальная модель? К сожалению, нет! Когда я сделала новые снимки карточек и дала модели найти общий символ, у неё были некоторые проблемы со снеговиком. Иногда она называла снеговиком глаз или зебру! Это дает несколько странные результаты:

Снеговик? Где?Снеговик? Где?

Эта модель лучше людей? Это зависит от обстоятельств: люди могут делать это идеально, но модель работает быстрее! Я рассчитала при помощи компьютера: я дала ему колоду из 55 карт и спросила общий символ для каждой комбинации из двух карт. Всего 1485 комбинаций. Это заняло у компьютера менее 140 секунд. Компьютер допустил несколько ошибок, но по скорости он точно превзойдет любого человека!

Я не думаю, что создать 100%-ную модель действительно сложно. Это может быть сделано, например, с использованием трансферного обучения. Чтобы понять, что делает модель, мы можем визуализировать слои для тестового изображения. Что попробовать в следующий раз!


Надеюсь, вам понравилось читать этот пост!

Источник: habr.com
К списку статей
Опубликовано: 05.01.2021 14:11:03
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Занимательные задачки

Python

Машинное обучение

Data engineering

Deep learning

Opencv

Python3

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru