Немного веселья с компьютерным зрением и 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 компонента
Сейчас мы добавим контрастности к световой компоненте, сольём компоненты обратно и конвертируем картинку:
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
Масштабирование
Потом мы масштабируем и сохраняем картинку:
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%-ную модель действительно сложно. Это может быть сделано, например, с использованием трансферного обучения. Чтобы понять, что делает модель, мы можем визуализировать слои для тестового изображения. Что попробовать в следующий раз!
Надеюсь, вам понравилось читать этот пост!