Курс по нейронным сетям: https://stepik.org/a/227582
На этом занятии
вы узнаете, как с помощью СНС делается раскраска изображений (из градаций
серого в цветное):
По-английски,
это называется:
Colorization
Начнем с того,
чем отличается на уровне пикселей цветное изображение от черно-белого (я
градации серого для простоты буду называть черно-белым). Черно-белое
представлено одной компонентой яркости со значениями пикселей в диапазоне от 0
до 255:
Полноцветные,
как правило, описываются тремя компонентами RGB:
Следовательно,
чтобы СНС раскрасила черно-белое изображение, на ее выходе должно формироваться
три цветовых канала:
Но это не лучшее
решение. Полноцветные изображения в данной задаче удобнее представлять в другом
цветовом пространстве, например:
Lab: (Light – слой яркости
(градации серого); a, b – цветовые слои)
Причем, пиксели
слоя L меняются в
диапазоне от 0 до 100, а пиксели слоев a и b – в диапазоне от
-128 до 127. В этом случае СНС достаточно сгенерировать только для канала: a и b, вместо трех RGB (третий канал у
нас уже есть – это исходное изображение в градациях серого):
Кроме того, 94%
рецепторов человеческого глаза настроены на восприятие яркости (компоненты L – градации
серого) и только 6% - на цветовые составляющие. Поэтому, если нейросеть немного
«напутает» в цветах – это не так сильно скажется на визуальном восприятии
картинки в целом. А вот путаница в яркостной составляющей – это уже критично.
Но мы ее и не будем формировать, а возьмем уже готовую. Это еще одно
преимущество цветового пространства Lab в рамках данной задачи.
Для
преобразования изображения из RGB в Lab в Python воспользуемся
пакетом skimage и импортируем
следующие методы:
import numpy as np
import matplotlib.pyplot as plt
from skimage.color import rgb2lab, lab2rgb
-
rgb2lab –
преобразовывает изображение из RGB в Lab;
-
lab2rgb –
преобразовывает изображение из Lab в RGB.
В качестве
тестовой реализации мы можем взять любое полноцветное изображение, из него
получить яркостную компоненту L (в градациях серого) и две цветовые
компоненты a и b. На вход сети
подадим изображение в градациях серого, а на выходе потребуем цветовые
составляющие a и b:
Для загрузки
изображения в коллаборатории google импортируем следующие модули:
from google.colab import files
from io import BytesIO
from PIL import Image
И, затем,
выполним строчки:
upl = files.upload()
names = list(upl.keys())
img = Image.open(BytesIO(upl[names[0]]))
Загруженное
изображение будет в RGB-формате. Для преобразования в пространство Lab определим
следующую функцию:
def processed_image(img):
image = img.resize( (256, 256), Image.BILINEAR)
image = np.array(image, dtype=float)
size = image.shape
lab = rgb2lab(1.0/255*image)
X, Y = lab[:,:,0], lab[:,:,1:]
Y /= 128 # нормируем выходные значение в диапазон от -1 до 1
X = X.reshape(1, size[0], size[1], 1)
Y = Y.reshape(1, size[0], size[1], 2)
return X, Y, size
Мы здесь сначала
изменяем размер изображения до 256х256 пикселей и преобразовываем его в массив numpy. Затем, делаем
преобразование в пространство Lab (обратите внимание, на вход функции
нужно передавать изображение с компонентами RGB и вещественными
значениями пикселей от 0 до 1, поэтому мы здесь добавляем нормирующий множитель
1/255). Далее, выделяем яркостную компоненту X и две цветовые
в Y. Цвета будут
использоваться как требуемые выходные значения НС, поэтому мы их нормируем до
диапазона [-1; 1]. Затем, формируем нужный формат размерностей для входных и
выходных данных НС.
Вызовем эту
функцию и получим следующий набор данных:
X, Y, size = processed_image(img)
Теперь у нас
есть что подавать на сеть и что требовать на ее выходах. Поэтому дальше нам
нужно построить модель СНС, представленной ранее на рисунке. С помощью Keras это можно
сделать так:
from keras.layers import Conv2D, UpSampling2D, InputLayer
from keras.models import Sequential
model = Sequential()
model.add(InputLayer(input_shape=(None, None, 1)))
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(64, (3, 3), activation='relu', padding='same', strides=2))
model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(128, (3, 3), activation='relu', padding='same', strides=2))
model.add(Conv2D(256, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(256, (3, 3), activation='relu', padding='same', strides=2))
model.add(Conv2D(512, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(256, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(128, (3, 3), activation='relu', padding='same'))
model.add(UpSampling2D((2, 2)))
model.add(Conv2D(64, (3, 3), activation='relu', padding='same'))
model.add(UpSampling2D((2, 2)))
model.add(Conv2D(32, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(2, (3, 3), activation='tanh', padding='same'))
model.add(UpSampling2D((2, 2)))
Чтобы не
изобретать велосипед, я взял эту модель из статьи, посвященной теме раскраски
изображений с помощью глубоких нейронных сетей:
https://github.com/baldassarreFe/deep-koalarization
Обратите
внимание на структуру слоев. Первый слой – это обычный сверточный слой,
состоящий из 64 фильтров и ядрами 3х3 пиксела. Следующая свертка имеет те же
параметры, но шаг смещения фильтров равен двум пикселям по каждой координате. Почему
здесь масштабирование признаков делается с помощью увеличения шага, а не
методом
MaxPooling
который
использовался при классификации и стилизации изображений? Дело в том, что слой MaxPooling хорошо
концентрирует значимую информацию об особенностях изображения, но несколько
искажает взаимное расположение пикселей на плоскости. В задачах колоризации
такое искажение представления изображения недопустимо. Поэтому и используется
сверточный слой с шагом 2.
Так продолжается
движение в глубину, пока не встретится слой UpSampling2D((2, 2)). Параметр
(2, 2) задает увеличение размера каждого элемента карты признаков. В Keras это работает
следующим образом:
Каждый элемент
карты признаков увеличивается до указанного размера (2, 2), причем при
масштабировании каждой ячейки (синяя рамка) значение просто копируется в
соседние, заполняя все свое пространство. И так каждый элемент. В результате
получается увеличенное грубое представление карт признаков на каждом канале.
На последнем
выходном слое имеем два канала (для двух цветовых компонент a и b), размеры
которых совпадают с размерами исходного (входного) изображения. В качестве
функции активации выбираем гиперболический тангенс, чтобы цветовые составляющие
имели диапазон [-1; 1]. Так сеть будет формировать цвета.
Давайте для
примера обучим эту НС на одном изображении, то есть, потребуем, чтобы она
выдавала строго определенные выходные цветовые компоненты:
model.compile(optimizer='adam', loss='mse')
model.fit(x=X, y=Y, batch_size=1, epochs=50)
Мы здесь делаем
оптимизацию по Adam, критерий качества – минимум среднего квадрата
рассогласования. На вход этой сети будем подавать черно-белое изображение, а на
выходе требовать заданные для него цветовые составляющие.
После обучения
прогоним через сеть изображение в градациях серого:
output = model.predict(X)
и посмотрим на
результат:
output *= 128
min_vals, max_vals = -128, 127
ab = np.clip(output[0], min_vals, max_vals)
cur = np.zeros((size[0], size[1], 3))
cur[:,:,0] = np.clip(X[0][:,:,0], 0, 100)
cur[:,:,1:] = ab
plt.subplot(1, 2, 1)
plt.imshow(img)
plt.subplot(1, 2, 2)
plt.imshow(lab2rgb(cur))
Как видите, это
одно конкретное изображение она раскрасила вполне приемлемо, правда, мы именно
на нем ее и обучали. Но этот простейший пример показывает, что такая операция,
в принципе, возможна и общая идея вроде бы рабочая. Но если взять другое
изображение, то эффект будет уже значительно хуже:
И это не
удивительно. Наша обучающая выборка состояла всего из одного изображения. Этого
явно недостаточно. Нужно хотя бы несколько тысяч.
Будет ли такое обучение?
В качестве
домашнего задания сформируйте выборку из тысяч цветных изображений кошек (или
собак, или пляжей и т.п. главное, чтобы класс изображений был каким-то одним,
иначе это негативно скажется на результате). И посмотрите, как это будет
работать.
А мы зададимся
вопросом: почему НС в принципе способна выполнять раскраску, как это работает? Давайте
посмотрим еще раз на структуру НС. Здесь первые два слоя образуют свертки с
ядром 3х3. Это приближенно заменяет свертку фильтра с ядром 5х5:
Получается, что
каждый признак парных слоев связан с областью 5х5 отсчетов. И если в эту
область часто попадают округлые очертания объекта темного объекта (например,
глаза котов), то сеть связывает такой признак с темным цветом:
Это, особенно
хорошо проявляется на глубоких слоях, где формируются более сложные объекты:
глаза, лапы, цветок, трава и т.п. В весовых коэффициентах, как бы, сохраняется
опыт и «знания» о соответствии элементов черно-белого изображения этим же
элементам, но в цвете. Примерно так можно воспринимать работу НС по раскраске
изображений.
Как я уже
отмечал, приведенный алгоритм колоризации изображений может неплохо работать на
однотипных данных, например, котов на фоне природы, или лица людей, или
фотографий морских пляжей и так далее. Но если все это смешать, то результаты
станут заметно хуже.
В 2016-м году ряд
японских исследователей:
Хатоши
Иизука, Эдгар Симо-Серра и Хироши Ишикава
http://hi.cs.waseda.ac.jp/~iizuka/projects/colorization/data/colorization_sig2016.pdf
предложили
интересную концепцию по улучшению раскраски изображений. Они к уже существующей
НС параллельно добавили еще одну, которая выполняет обычную классификацию, то есть,
определяет: к какому классу относится раскрашиваемое изображение. Как вы
понимаете, если наша НС будет дополнительно «знать» о типе входных данных
(изображение котов, пляжа, руин, лиц, машин и т.п.), то она сможет сохранить
специализацию по раскраске и при этом работать с любыми данными.
Для определения
характера изображения ученые предложили воспользоваться одной из известных и
уже обученных СНС, например, знакомой нам VGG19, которая на
выходе дает 1000 различных классов. Или же, можно взять более продвинутую сеть
Inception-ResNet-v2
которая также
имеет 1000 выходных классов. На рисунке, который я взял из статьи, показано как
добавляется классифицирующая сеть к раскрашивающей сети. Для этого японские
ученые добавили еще один, так называемый слой слияния (Fusion layer). Что это за
слой? В центре первой сверточной сети (с наименьшими размерами карт признаков)
создается дополнительный сверточный слой с 256 каналами и таким же размером
карт признаков, что и в предыдущем слое. Затем, признаки дополняются вектором
классификации от второй СНС, по следующей схеме:
Благодаря этому,
каждый признак в дальнейшем будет ассоциирован с установленной тематикой
входного изображения и качество раскраски значительно увеличивается.
Заключение
Итак, на
последних занятиях мы с вами увидели несколько примеров использования
сверточных НС для различных задач: классификации, стилизации, колоризации. В
каждой из них СНС используется по своему, подчас неожиданно, как в задаче
стилизации, где она применяется для вычисления критерия качества. Эти примеры
показывают, что нейронные сети – всего лишь инструмент и как мы будем его
использовать, зависит лишь от нашей фантазии.
Курс по нейронным сетям: https://stepik.org/a/227582