想打造一个神经网络,自动给黑白照片上色?这儿有一份超详细教程

王小新 编译自 FloydHub Blog 量子位 出品 | 公众号 QbitAI

昨天,你可能惊喜地看到了Adobe做了个给人像上色的软件,然后伤心地发现只能搞定人脸,而且还没正式推出,现在能看到的只有一篇论文……

没关系,咱们自己做一个。

深度学习云平台FloydHub最近在官方博客上发了一篇教你搭建神经网络,来给黑白照片上色的教程,在Twitter和Reddit论坛上都广受好评。

FloydHub是个YC孵化的创业公司,号称要做深度学习领域的Heroku。它在GPU系统上预装了TensorFlow和很多其他的机器学习工具,用户可以按时长租用,训练自己的机器学习模型。

这份教程是基于FloydHub平台写的,这个平台号称深度学习领域的Heroku,在GPU系统上预装了TensorFlow和很多其他的机器学习工具,用户可以按时长租用,训练自己的机器学习模型。免费版支持1个项目、每月20小时GPU时长、10G存储空间,用上色项目练个手足够了。

进入正题~

以下内容编译自FloydHub官方博客:

我将分三个步骤展示如何打造你自己的着色神经网络。

第一部分介绍核心逻辑。我们将用40行代码来构建一个简单的神经网络,叫做“Alpha”版本着色机器人。这个代码里没有太多技巧,但是这有助于熟悉语法。

下一步是创建一个具备泛化能力的神经网络,叫做“Beta”版本,它能够对以前没见过的图像进行着色。

最后,将神经网络与分类器相结合,得到最终版本。Inception Resnet V2是训练120万张图像后得到的神经网络,我们使用了该模型。为了使着色效果更加吸引人,我们使用了来自素材网站Unsplash的人像集,来训练这个神经网络。

Alpha版本机器人的Jupyter Notebook代码、上述三个版本实现代码的FloydHub和GitHub地址,以及在FloydHub的GPU云上运行的所有实验代码,都在文末。

核心逻辑

在本节中,我将概述如何渲染图像、数字颜色的基础知识以及神经网络的主要逻辑。

黑白图像可以在像素网格中表示。每个像素有对应于其亮度的值,取值范围为0 - 255,从黑色到白色。

图像与数字的对应

彩色图像可以分为三层,分别是红色层、绿色层和蓝色层。直观上,你可能会认为植物只存在于绿色层,这可能与你的直觉相反。想象一下,将白色背景上的绿叶分成三个图层。

如下图所示,叶子在三个图层中都存在。这些层不仅可以确定颜色,还确定了亮度。

三个通道的绿叶

例如,为了得到白色这个颜色,你需要将所有颜色均匀分布。添加等量红色和蓝色后,绿色会变得更亮。因此,彩色图像使用三个通道来编码颜色和对比度:

RGB调色

就像黑白图像一样,彩色图像中每个图层值的范围也是0 – 255,值为0意味着该图层中没有颜色。如果在所有颜色图层中该值都为0,则该图像像素为黑色。

神经网络能建立输入和输出之间的关系。更准确地说,着色任务就是让网络找到连接灰度图像与彩色图像的特征。

因此,着色神经网络,就是要寻找将灰度值网格连接到三色网格的特征。

Alpha版本

我们从简单版本开始,建立一个能给女性脸部着色的神经网络。这样,当添加新特征时,你可以熟悉已有模型的核心语法。

只需要40行代码,就能实现下图所示的转换。中间图像是用神经网络完成的,右边图像是原始的彩色照片。这个网络使用了相同图像做训练和测试,在beta版本中还会再讲这一点。

颜色空间

首先,使用一种能改变颜色通道的算法,从RGB到Lab。其中,L表示亮度,a和b分别表示颜色光谱,绿-红和蓝-黄。

如下所示,Lab编码的图像具有一个灰度层,并将三个颜色层压成两层,这意味着在最终预测中可以使用原始灰度图像。此外,我们只需预测两个通道。

Lab编码图像

科学研究表明,人类眼睛中有94%细胞确定着亮度,只有6%细胞是用来感受颜色的。如上图所示,灰度图像比彩色层更加清晰。这是我们在最终预测中保留灰度图像的另一个原因。

从黑白到彩色

最终预测应该是这样的:向网络输入灰度层(L),然后预测Lab中的两个颜色层ab。要创建最终输出的彩色图像,我们需要把输入的灰度(L)图像和输出的a、b层加在一起,创建一个Lab图像。

图像转化函数

我们使用卷积过滤器将一层变成两层,可以把它们看作3D眼镜中的蓝色和红色滤镜。每个过滤器确定能从图片中看到的内容,可以突出或移除某些东西,来从图片中提取信息。这个网络可以从过滤器中创建新图像,也可以组合多个过滤器形成新图像。

卷积神经网络能自动调整每个滤波器,以达到预期结果。我们将从堆叠数百个滤波器开始,然后将它们缩小成两层,即a层和b层。

在详细介绍其工作原理之前,先介绍代码。

在FloydHub上部署代码

如果你刚接触FloydHub,请看这个2分钟安装教程(https://www.floydhub.com/),以及手把手教学指南(https://blog.floydhub.com/my-first-weekend-of-deep-learning/),然后就可以开始在GPU云上训练深度学习模型了。

Alpha版本

安装好FloydHub后,执行以下命令:

git clone https://github.com/emilwallner/Coloring-greyscale-images-in-Keras

打开文件夹并启动FloydHub。

cd Coloring-greyscale-images-in-Keras/floydhub
floyd init colornet

FloydHub Web控制台将在浏览器中打开,系统会提示你创建一个名为colornet的FloydHub新项目。完成后,返回终端并执行相同的初始化命令。

floyd init colornet

接下来执行本项目任务。

floyd run --data emilwallner/datasets/colornet/2:data --mode jupyter --tensorboard

对于这个任务的一些简单说明:

1. 我们在FloydHub的数据集目录(—data emilwallner / datasets / colornet / 2:data)中载入了一个公开数据集,你在FloydHub上查看并使用此数据集和许多其他公开数据集;

2. 启用了Tensorboard (—tensorboard);

3. 在Jupyter Notebook下运行代码 (—mode jupyter);

4. 如果能使用GPU,你还可以将GPU (—gpu)添加到命令中,这样运行速度能提高50倍。

在FloydHub网站上的选项“Jobs”下,点击Jupyter Notebook可链接到以下文件:floydhub / Alpha version / working_floyd_pink_light_full.ipynb,打开它并摁下shift+enter。

逐渐增大epoch值,体会神经网络是如何学习的。

model.fit(x=X, y=Y, batch_size=1, epochs=1)

开始时把epoch设置为1,逐渐增加到10、100、5500、1000和3000。epoch值表示神经网络从图像中学习的次数。在训练神经网络的过程中,你可以在主文件夹中找到这个图像img_result.png。

# Get images
image = img_to_array(load_img('woman.png'))
image = np.array(image, dtype=float)

# Import map images into the lab colorspace
X = rgb2lab(1.0/255*image)[:,:,0]
Y = rgb2lab(1.0/255*image)[:,:,1:]
Y = Y / 128
X = X.reshape(1, 400, 400, 1)
Y = Y.reshape(1, 400, 400, 2)

model = Sequential()
model.add(InputLayer(input_shape=(None, None, 1)))

# Building the neural network
model = Sequential()
model.add(InputLayer(input_shape=(None, None, 1)))
model.add(Conv2D(8, (3, 3), activation='relu', padding='same', strides=2))
model.add(Conv2D(8, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(16, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(16, (3, 3), activation='relu', padding='same', strides=2))
model.add(Conv2D(32, (3, 3), activation='relu', padding='same'))
model.add(Conv2D(32, (3, 3), activation='relu', padding='same', strides=2))
model.add(UpSampling2D((2, 2)))
model.add(Conv2D(32, (3, 3), activation='relu', padding='same'))
model.add(UpSampling2D((2, 2)))
model.add(Conv2D(16, (3, 3), activation='relu', padding='same'))
model.add(UpSampling2D((2, 2)))
model.add(Conv2D(2, (3, 3), activation='tanh', padding='same'))

# Finish model
model.compile(optimizer='rmsprop',loss='mse')

#Train the neural network
model.fit(x=X, y=Y, batch_size=1, epochs=3000)
print(model.evaluate(X, Y, batch_size=1))

# Output colorizations
output = model.predict(X)
output = output * 128
canvas = np.zeros((400, 400, 3))
canvas[:,:,0] = X[0][:,:,0]
canvas[:,:,1:] = output[0]
imsave("img_result.png", lab2rgb(cur))
imsave("img_gray_scale.png", rgb2gray(lab2rgb(cur)))

用FloydHub命令来运行网络:

floyd run --data emilwallner/datasets/colornet/2:data --mode jupyter --tensorboard

技术说明

总的来说,输入是一个黑白图像网格,输出两个具有颜色值的网格。在输入和输出之间,通过一个卷积神经网络,构建过滤器连接两者。

训练网络时使用的是彩色图像,并将RGB颜色转换成Lab颜色空间。网络输入为黑白图层,输出两个着色层。

在上图左侧,输入为黑白网格、过滤器和神经网络的预测结果。

在同一变化间距内,比较预测值和实际值的差异。其中,该间距为[-1, 1]。为了映射这些预测值,我们使用Tanh激活函数,因为Tanh函数的输入可为任意值,输出为-1到1。

实际上,颜色值的分布区间为[-128, 128],这也是Lab颜色空间的默认间距。除以128后这些值也落到区间[-1, 1]中,这样就能用来比较预测结果的误差。

计算得到最终误差后,网络会更新过滤器以减小全局误差。网络会留在这个循环,直到误差达到足够小。

下面解释下代码的一些语法。

X = rgb2lab(1.0/255*image)[:,:,0]
Y = rgb2lab(1.0/255*image)[:,:,1:]

1.0 / 255表示彩色图像使用的RGB颜色空间为24位,也就是说每个颜色通道值为整数,且从0到255变化。这是标准颜色范围,一共有1670万种颜色组合。由于人类只能感知到2 ~ 10万种颜色,因此使用更大的颜色空间没有太大意义。

Y = Y / 128

与RGB相比,Lab颜色空间的范围与其不同。Lab中色谱ab的取值范围是[-128, 128]。将输出层的所有值都除以128,它的范围变成[-1, 1]。神经网络的输出值范围也为[-1, 1],两者可进行匹配。

在用rgb2lab()函数转换颜色空间后,我们使用[:,…,0]来选择灰度层,这是神经网络的输入。同时[:,…,1:]表示选择两个颜色层,绿-红和蓝-黄。

训练好神经网络后,转换回Lab图像,作最终预测。

output = model.predict(X)
output = output * 128

其中,输入为灰度图像,经过训练好的神经网络后,其输出位于区间[-1, 1],再将其乘以128,得到Lab色谱中的合适颜色。

canvas = np.zeros((400, 400, 3))
canvas[:,:,0] = X[0][:,:,0]
canvas[:,:,1:] = output[0]

最后,用0填充三个图层得到一个全黑RGB画布,然后从测试图像中复制灰度图层,再把两个颜色层添加到RGB画布中,最后把这个像素数组转换回图像。

打造Alpha版本的一些心得

1. 阅读研究论文是很痛苦的。但只要你能总结出每篇论文的核心要点,就能轻松地阅读论文,也让你更关注于文中的前后关联。

2. 关键是要从简化版开始。网上大多数实现的代码行数可达2000行到10000行,这让我很难去理解问题的核心逻辑。只要有了一个准系统版本,之后就能更容易去阅读实现代码和研究论文。

3.探索开源项目。为了大体了解如何编程,我在Github上浏览了50-100个关于着色的项目。

4.事情并不总是像预期的那样工作。开始时,我的网络只能创建红色和黄色。最初,末层激活时用的是Relu激活函数。因为它只能将数字映射为正数,无法得到负值和蓝绿色谱。后来,我添加了一个Tanh激活函数来映射Y值,解决了这个问题。

5.理解比速度更重要。我看到,很多实现代码的运行速度都很快,但很难看懂。我选择优化创新速度而不是代码速度。

Beta版本

要了解Alpha版本的缺点,用该网络给未经训练的图像着色。尝试后你就会发现它的效果不好,因为网络只记住了已有的信息,还没有学会如何给未见过的图像着色。而这正是我们要在Beta版本中努力的方向,即提高网络的泛化能力。

以下是Beta版本神经网络在验证图像上的着色结果。

我在FloydHub上创建了一个公开的高质量图像数据集,而不是使用现有的Imagenet。Unsplash收集了大量专业摄影师的创意共享图片,本数据集图像来自于此,一共包括了9500张训练图像和500张验证图像。

特征提取器

我们所构建的神经网络能找到灰度图与其彩色版本相关联的有效特征。

试想一下,你必须给黑白图像着色,但是限制你每次只能看到九个像素。你只能从左上角到右下角来扫描每张图像,并尝试预测每个像素可能的颜色值。

例如,这九个像素就是上面那个女性的鼻孔边缘。想象得到,这很难得到一个良好的着色效果,所以可将它分解成多个步骤。

首先,你要寻找简单图案,如对角线和黑色像素等。你也可以在每个方块中寻找相同的确切图案,并删除不匹配的像素。具体做法是使用最开始的64个过滤器来生成64张新图像。

当再次扫描图像时,你会看到已经检测到的相同小图案。要获得对图像的更深入理解,你可以将图像分辨率减半。

你仍然只用了3×3过滤器来扫描每张图像。但是,通过将9个新像素与较低层次滤波器相结合,你可以检测更复杂的图案。几个像素组合起来,可能会形成一个半圆、一个小点或是一条线。此外,你可以反复从图像中提取相同图案,这样就能生成128个过滤后的新图像。

经过几次操作后,过滤后的新图像可能会像这样:

如上所述,先提取低层特征,如边缘等。越靠近输出的图层先组合成图案,再组合成局部细节,最终转化为面部。如果你觉得难以理解,可观看这个视频教程(https://www.youtube.com/watch?v=AgkfIQ4IGaM)。

这个过程与用于处理图像的神经网络相似,被称为卷积神经网络。卷积运算与单词组合相似,通过组合若干个过滤后的图像来理解图像中的前后关系。

从特征提取到着色

神经网络以误差传递的方式实现。首先,它对每个像素进行随机预测;然后基于每个像素的误差,通过反向传播,来改进特征提取效果。

开始时,它会先调整具有最大误差的位置。因此,最初只考虑两个问题,即是否要着色和是否定位不同物体;然后给所有物体涂上棕色,因为这是与其他颜色最相似的一种颜色,能产生最小的误差。

由于大多数训练数据非常相似,网络只能通过调整棕色的深浅程度来区分不同物体,但是不能产生更细致的颜色,这也是我们将在最终版本中探索的内容。

以下是beta版本的代码,及技术说明。

# Get images
X = []
for filename in os.listdir('../Train/'):
    X.append(img_to_array(load_img('../Train/'+filename)))
X = np.array(X, dtype=float)

# Set up training and test data
split = int(0.95*len(X))
Xtrain = X[:split]
Xtrain = 1.0/255*Xtrain

#Design the neural network
model = Sequential()
model.add(InputLayer(input_shape=(256, 256, 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)))

# Finish model
model.compile(optimizer='rmsprop', loss='mse')

# Image transformer
datagen = ImageDataGenerator(
        shear_range=0.2,
        zoom_range=0.2,
        rotation_range=20,
        horizontal_flip=True)

# Generate training data
batch_size = 50
def image_a_b_gen(batch_size):
    for batch in datagen.flow(Xtrain, batch_size=batch_size):
        lab_batch = rgb2lab(batch)
        X_batch = lab_batch[:,:,:,0]
        Y_batch = lab_batch[:,:,:,1:] / 128
        yield (X_batch.reshape(X_batch.shape+(1,)), Y_batch)

# Train model
TensorBoard(log_dir='/output')
model.fit_generator(image_a_b_gen(batch_size), steps_per_epoch=10000, epochs=1)
# Test images
Xtest = rgb2lab(1.0/255*X[split:])[:,:,:,0]
Xtest = Xtest.reshape(Xtest.shape+(1,))
Ytest = rgb2lab(1.0/255*X[split:])[:,:,:,1:]
Ytest = Ytest / 128
print model.evaluate(Xtest, Ytest, batch_size=batch_size)

# Load black and white images
color_me = []
for filename in os.listdir('../Test/'):
        color_me.append(img_to_array(load_img('../Test/'+filename)))
color_me = np.array(color_me, dtype=float)
color_me = rgb2lab(1.0/255*color_me)[:,:,:,0]
color_me = color_me.reshape(color_me.shape+(1,))

# Test model
output = model.predict(color_me)
output = output * 128

# Output colorizations
for i in range(len(output)):
        cur = np.zeros((256, 256, 3))
        cur[:,:,0] = color_me[i][:,:,0]
        cur[:,:,1:] = output[i]
        imsave("result/img_"+str(i)+".png", lab2rgb(cur))

用FloydHub命令来运行Beta版本神经网络:

floyd run --data emilwallner/datasets/colornet/2:data --mode jupyter --tensorboard

技术说明

与其他视觉网络不同,它的主要区别在于像素位置的重要性。在着色网络中,图像的分辨率或比例在整个网络中保持不变。而在其他网络中,越靠近最后一层,图像变得越扭曲。

分类网络中的最大池化层增加了信息密度,但也可能导致图像失真。它只对信息进行估值,而未考虑到图像布局。在着色网络中,我们将步幅改为2,每次运算使宽度和高度减半。这样,不但增加了信息密度,也不会使图像扭曲。

两者差异在于上采样层和图像比例保持方面。而分类网络只关心最终的分类正确率,因此随着网络深入,它会不断减小图像的分辨率和质量。

但是,着色网络的图像比例保持不变。这是通过添加空白填充来实现的,否则每个卷积层将削减图像,实现代码为:padding =’same’。

要使图像的分辨率加倍,着色网络使用了上采样层,更多信息见:https://keras.io/layers/convolutional/#upsampling2d。

for filename in os.listdir('/Color_300/Train/'):
    X.append(img_to_array(load_img('/Color_300/Test'+filename)))

这个for循环首先计算了目录中的所有文件名,然后遍历图像目录,并将图像转换为像素数组,最后将其组合成一个大型矢量。

datagen = ImageDataGenerator(
        shear_range=0.2,
        zoom_range=0.2,
        rotation_range=20,
        horizontal_flip=True)

在ImageDataGenerator中,我们还修改了图像生成器的参数。这样,图像永远不会相同,以改善学习效果。参数shear_range是指图像向左或向右的倾斜程度,其他参数不需要加以说明。

batch_size = 50
def image_a_b_gen(batch_size):
    for batch in datagen.flow(Xtrain, batch_size=batch_size):
        lab_batch = rgb2lab(batch)
        X_batch = lab_batch[:,:,:,0]
        Y_batch = lab_batch[:,:,:,1:] / 128
        yield (X_batch.reshape(X_batch.shape+(1,)), Y_batch)

我们使用了文件夹Xtrain中的图像,根据之前设置来生成图像。然后我们提取X_batch中的黑白层和两个颜色层的两种颜色。

model.fit_generator(image_a_b_gen(batch_size), steps_per_epoch=1, epochs=1000)

拥有越高性能的GPU,则可以设置越大的batch_size值。根据现有硬件,我们设置了每批次输入50-100张图像。参数steps_per_epoch是通过把训练图像的数量除以批次大小得出的。例如,有100张图像且批次大小为50,则steps_per_epoch值为2。参数epoch决定网络中所有图像的训练次数。在Tesla K80 GPU上,大约需要11小时才能完成对1万张图像的21次训练。

训练心得

1.先进行多次小批次实验,再尝试大批次实验。即使经过20-30次实验,我仍然发现很多错误。程序能运行并不意味着它能工作。神经网络中的问题通常比传统编程遇到的Bug更为麻烦。

2.多样化的数据集会使着色效果呈现棕色。如果数据集中图像比较相似,不需要设计很复杂的架构,就可以得到一个良好效果,但是其泛化能力十分糟糕。

3.重视图像形状的统一性。每张图像的分辨率必须是准确的,且在整个网络中保持成比例。开始时,所使用的图像分辨率为300,将它减半三次后分别得到150、75和35.5。因此,就丢失了半个像素,这导致了很多问题,后来才意识到应该使用4、8、16、32、64、256等这种能被2整除的数。

4.创建数据集。请禁用.DS_Store文件,不然会产生很多麻烦;大胆创新,最后我用Chrome控制台脚本和扩展程序来下载文件;备份原始文件并构建清理脚本。

最终版本

最终版本的着色神经网络有四个组成部分。我们将之前的网络拆分成编码器和解码器,在这两者之间还添加了一个融合层。关于分类网络的基本教程,可参考:https://cs231n.github.io/classification/。

Inception resnet v2是目前最强大的分类器之一,使用了120万张图像来训练该网络。我们提取了它的分类层,并将其与编码器的输出进行合并。因此,输入数据传给编码器的同时,也并行传输到resnet v2网络的分类层中。更多信息,请参考原论文:https://github.com/baldassarreFe/deep-koalarization。

信息并行传输示意图

这个网络通过将分类器的学习效果迁移到着色网络上,可更好理解图片中的内容。因此,这样使得网络能够把目标表征与着色方案相匹配。

以下是在一些验证图像上的着色效果,仅使用20张图像来训练网络。

大多数图像的着色效果不好,但是由于验证集中有2500张图像,我设法找到了一些良好的着色图像。在更多图像上进行训练可以获得更为稳定的结果,但是大部分都呈现棕色色调。这两个链接贴出了运行试验的完整列表,包括验证图像(https://www.floydhub.com/emilwallner/projects/color) (https://www.floydhub.com/emilwallner/projects/color_full)。

本文的神经网络设计参考了这篇论文(https://github.com/baldassarreFe/deep-koalarization/blob/master/report.pdf),以及我在Keras中的一些理解。

注意:在下面代码中,我把Keras的序列模型变换成相应的函数调用。

# Get images
X = []
for filename in os.listdir('/data/images/Train/'):
    X.append(img_to_array(load_img('/data/images/Train/'+filename)))
X = np.array(X, dtype=float)
Xtrain = 1.0/255*X

#Load weights
inception = InceptionResNetV2(weights=None, include_top=True)
inception.load_weights('/data/inception_resnet_v2_weights_tf_dim_ordering_tf_kernels.h5')
inception.graph = tf.get_default_graph()

embed_input = Input(shape=(1000,))

#Encoder
encoder_input = Input(shape=(256, 256, 1,))
encoder_output = Conv2D(64, (3,3), activation='relu', padding='same', strides=2)(encoder_input)
encoder_output = Conv2D(128, (3,3), activation='relu', padding='same')(encoder_output)
encoder_output = Conv2D(128, (3,3), activation='relu', padding='same', strides=2)(encoder_output)
encoder_output = Conv2D(256, (3,3), activation='relu', padding='same')(encoder_output)
encoder_output = Conv2D(256, (3,3), activation='relu', padding='same', strides=2)(encoder_output)
encoder_output = Conv2D(512, (3,3), activation='relu', padding='same')(encoder_output)
encoder_output = Conv2D(512, (3,3), activation='relu', padding='same')(encoder_output)
encoder_output = Conv2D(256, (3,3), activation='relu', padding='same')(encoder_output)

#Fusion
fusion_output = RepeatVector(32 * 32)(embed_input) 
fusion_output = Reshape(([32, 32, 1000]))(fusion_output)
fusion_output = concatenate([encoder_output, fusion_output], axis=3) 
fusion_output = Conv2D(256, (1, 1), activation='relu', padding='same')(fusion_output) 

#Decoder
decoder_output = Conv2D(128, (3,3), activation='relu', padding='same')(fusion_output)
decoder_output = UpSampling2D((2, 2))(decoder_output)
decoder_output = Conv2D(64, (3,3), activation='relu', padding='same')(decoder_output)
decoder_output = UpSampling2D((2, 2))(decoder_output)
decoder_output = Conv2D(32, (3,3), activation='relu', padding='same')(decoder_output)
decoder_output = Conv2D(16, (3,3), activation='relu', padding='same')(decoder_output)
decoder_output = Conv2D(2, (3, 3), activation='tanh', padding='same')(decoder_output)
decoder_output = UpSampling2D((2, 2))(decoder_output)

model = Model(inputs=[encoder_input, embed_input], outputs=decoder_output)

#Create embedding
def create_inception_embedding(grayscaled_rgb):
    grayscaled_rgb_resized = []
    for i in grayscaled_rgb:
        i = resize(i, (299, 299, 3), mode='constant')
        grayscaled_rgb_resized.append(i)
    grayscaled_rgb_resized = np.array(grayscaled_rgb_resized)
    grayscaled_rgb_resized = preprocess_input(grayscaled_rgb_resized)
    with inception.graph.as_default():
        embed = inception.predict(grayscaled_rgb_resized)
    return embed

# Image transformer
datagen = ImageDataGenerator(
        shear_range=0.4,
        zoom_range=0.4,
        rotation_range=40,
        horizontal_flip=True)

#Generate training data
batch_size = 20

def image_a_b_gen(batch_size):
    for batch in datagen.flow(Xtrain, batch_size=batch_size):
        grayscaled_rgb = gray2rgb(rgb2gray(batch))
        embed = create_inception_embedding(grayscaled_rgb)
        lab_batch = rgb2lab(batch)
        X_batch = lab_batch[:,:,:,0]
        X_batch = X_batch.reshape(X_batch.shape+(1,))
        Y_batch = lab_batch[:,:,:,1:] / 128
        yield ([X_batch, create_inception_embedding(grayscaled_rgb)], Y_batch)

#Train model      
tensorboard = TensorBoard(log_dir="/output")
model.compile(optimizer='adam', loss='mse')
model.fit_generator(image_a_b_gen(batch_size), callbacks=[tensorboard], epochs=1000, steps_per_epoch=20)

#Make a prediction on the unseen images
color_me = []
for filename in os.listdir('../Test/'):
    color_me.append(img_to_array(load_img('../Test/'+filename)))
color_me = np.array(color_me, dtype=float)
color_me = 1.0/255*color_me
color_me = gray2rgb(rgb2gray(color_me))
color_me_embed = create_inception_embedding(color_me)
color_me = rgb2lab(color_me)[:,:,:,0]
color_me = color_me.reshape(color_me.shape+(1,))

# Test model
output = model.predict([color_me, color_me_embed])
output = output * 128

# Output colorizations
for i in range(len(output)):
    cur = np.zeros((256, 256, 3))
    cur[:,:,0] = color_me[i][:,:,0]
    cur[:,:,1:] = output[i]
    imsave("result/img_"+str(i)+".png", lab2rgb(cur))

用FloydHub命令来运行最终版本神经网络:

floyd run --data emilwallner/datasets/colornet/2:data --mode jupyter --tensorboard

技术说明

当你要实现模型连接或模型融合时,Keras的函数调用功能是最佳选择。

模型融合层

首先,要下载inception resnet v2网络并加载权重。由于要并行使用两个模型,因此必须指定当前要使用哪个模型。这个可通过Keras的后端Tensorflow来完成。

inception = InceptionResNetV2(weights=None, include_top=True)
inception.load_weights('/data/inception_resnet_v2_weights_tf_dim_ordering_tf_kernels.h5')
inception.graph = tf.get_default_graph()

批处理方法中使用了调整后的图像,把它们变成黑白图像,并在inception resnet模型上运行。

grayscaled_rgb = gray2rgb(rgb2gray(batch))
embed = create_inception_embedding(grayscaled_rgb)

首先,要调整图像分辨率以适应inception模型;然后根据模型,使用预处理程序来规范化像素和颜色值;最后,在inception网络上运行并提取模型最后一层的权重。

def create_inception_embedding(grayscaled_rgb):
    grayscaled_rgb_resized = []
    for i in grayscaled_rgb:
        i = resize(i, (299, 299, 3), mode='constant')
        grayscaled_rgb_resized.append(i)
    grayscaled_rgb_resized = np.array(grayscaled_rgb_resized)
    grayscaled_rgb_resized = preprocess_input(grayscaled_rgb_resized)
    with inception.graph.as_default():
        embed = inception.predict(grayscaled_rgb_resized)
    return embed

再讲下生成器。对于每个批次,我们按照下列格式生成20张图像,在Tesla K80 GPU上大约要运行一个小时。基于该模型,每批次最多可输入50张图像,且不产生内存溢出。

yield ([X_batch, create_inception_embedding(grayscaled_rgb)], Y_batch)
代码23

这与本项目中着色模型的格式相匹配。

model = Model(inputs=[encoder_input, embed_input], outputs=decoder_output)

encoder_input会输入到编码器模型中;接着,编码器模型的输出会与融合层中的embed_input相融合;然后,这个融合输出会作为解码器模型的输入;最终,解码器模型会输出预测结果decode_output。

fusion_output = RepeatVector(32 * 32)(embed_input) 
fusion_output = Reshape(([32, 32, 1000]))(fusion_output)
fusion_output = concatenate([fusion_output, encoder_output], axis=3) 
fusion_output = Conv2D(256, (1, 1), activation='relu')(fusion_output)

在融合层中,首先将1000类输出层乘以1024(即32×32),这样就从inception模型的最后一层得到了1024行数据;接着,把2D重构成3D得到一个具有1000个类别对象的32x32网格;然后,将它与编码器模型的输出相连;最后,应用一个带有254个1X1内核的卷积网络,得到了融合层的最终输出。

一些思考

1.不要逃避难懂的术语。我花了三天时间去搜索在Keras该如何实现“融合层”。因为这听起来很复杂,我不想面对这个问题,而是试图找到现成代码。

2.多在网上请教他人。在Keras论坛中,我提出的问题没人回答,同时Stack Overflow删除了我的提问。但是,通过分解成小问题去请教他人,这迫使我进一步理解问题,并更快解决问题。

3.多发邮件请教。虽然论坛可能没人回应,人们关心你能否直接与他们联系。在Skype上与你不认识的研究人员一起探讨问题,这听起来很有趣。

4.在解决“融合层”问题之前,我决定构建出所有组件。以下是分解融合层的几个实验(https://www.floydhub.com/emilwallner/projects/color/24/code/Experiments/transfer-learning-examples)。

5.我以为某些代码能够起作用,但是在犹豫是否要运行。虽然我知道核心逻辑是行得通的,但我不认为它会奏效。经过一阵纠结,我还是选择运行。运行完模型的第一行代码后,就开始报错;四天后,一共产生几百个报错,我也做了数千个Google搜索,模型依旧停留在“Epoch 1/22”。

下一步计划

图像着色是一个极其有趣的问题。这既是一个科学问题,也是一个艺术问题。我写下这篇文章,希望你能从中有所启发,以加快在图像着色方面的研究。以下是一些建议:

1.微调另一个预训练好的模型;

2.使用不同的数据集;

3.添加更多图片来提高网络的正确率;

4.在RGB颜色空间中构建一个放大器。构建一个与着色网络类似的模型,将深色调的着色图像作为输入,它能微调颜色以输出合适的着色图像;

5.进行加权分类;

6.应用一个分类神经网络作为损失函数。网络中错误分类的图片有一个相应误差,探究每个像素对该误差的贡献度。

7.应用到视频中。不要太担心着色效果,而是要关注如何使图像切换保持协调。你也可以通过平铺多张小图像来处理大型图像。

当然,你也可以尝试用我贴在FloydHub上的三种着色神经网络,来给你的黑白图像着色。

1.对于Alpha版本,只需将你的图片分辨率调成400x400像素,把名称改为woman.jpg,并替换原有文件。

2.对于Beta版本和最终版本,在你运行FloydHub命令之前,要将你的图片放入Test文件夹。当Notebook运行时,你也可以通过命令行直接上传到Test文件夹。要注意,这些图像的分辨率必须是256x256像素。此外,你也可以上传彩色测试图像集,因为这个网络会自动把它们转换为黑白图像。

相关链接

1.Alpha版本机器人的Jupyter Notebook代码:

https://www.floydhub.com/emilwallner/projects/color/43/code/Alpha-version/alpha_version.ipynb

2.三个版本实现代码 - Floydhub传送门:

https://www.floydhub.com/emilwallner/projects/color/43/code

3.三个版本实现代码 - Github传送门:

https://github.com/emilwallner/Coloring-greyscale-images-in-Keras

4.所有实验代码:

https://www.floydhub.com/emilwallner/projects/color/jobs

5.作者Twitter:emilwallner https://twitter.com/EmilWallner

6.教程原文:https://blog.floydhub.com/colorizing-b&w-photos-with-neural-networks/

原文发布于微信公众号 - 量子位(QbitAI)

原文发表时间:2017-10-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏PPV课数据科学社区

【V课堂】R语言十八讲(十五)—-置换检验和自助法

不知道看到这里,读者有么有发现,前面讲了那么多方法,几大检验,回归分析,方差分析“都有一个共同的特点,那就是有一定的前提假设,只有满足这个假设时,模型才有较好的...

3296
来自专栏AI科技大本营的专栏

AI 技术讲座精选:Python中使用LSTM网络进行时间序列预测

长短记忆型递归神经网络拥有学习长观察值序列的潜力。 它似乎是实现时间序列预测的完美方法,事实上,它可能就是。 在此教程中,你将学习如何构建解决单步单变量时间序...

4694
来自专栏AI科技大本营的专栏

AI技术讲座精选:神经结构搜索和强化学习

摘 要 神经网络模型不仅功能强大,而且特别灵活,在许多困难的学习任务中均发挥着良好的作用,如图像、声音和自然语言的理解等。尽管神经网络获得了一系列的成功,但是...

33611
来自专栏机器学习算法与Python学习

干货|详解CNN五大经典模型:Lenet,Alexnet,Googlenet,VGG,DRL

文章来源:CSDN 作者:大饼博士X 关于卷积神经网络CNN,网络和文献中有非常多的资料,我在工作/研究中也用了好一段时间各种常见的model了,就想着简单整...

8276
来自专栏大数据挖掘DT机器学习

R语言与机器学习(分类算法)支持向量机

说到支持向量机,必须要提到july大神的《支持向量机通俗导论》,个人感觉再怎么写也不可能写得比他更好的了。这也正如青莲居士见到崔颢的黄鹤楼后也...

3534
来自专栏计算机视觉与深度学习基础

计算机视觉与图像处理学习笔记(一)

写在前面:因学习需要,本人根据章毓晋的《计算机视觉教程》和冈萨雷斯的《数字图像处理》两本书进行学习,中间会穿插相关实践,会有对opencv的学习,以此笔记记录学...

2416
来自专栏人工智能头条

SVM大解密(附代码和公式)

2342
来自专栏CVer

TensorFlow和深度学习入门教程

英文原文:https://codelabs.developers.google.com/codelabs/cloud-tensorflow-mnist/#0 C...

3888
来自专栏AI科技大本营的专栏

如何在Python中用LSTM网络进行时间序列预测

Matt MacGillivray 拍摄,保留部分权利 翻译 | AI科技大本营(rgznai100) 长短记忆型递归神经网络拥有学习长观察值序列的潜力。它似...

1K4
来自专栏ATYUN订阅号

只使用Numpy手动实现多层卷积神经网络(详解)

AiTechYun 编辑:yuxiangyu 在过去,我曾写过一篇关于“理解在最大池化层和转置卷积的反向传播”的文章。现在我想要使用这些知识做一个多层(或者说多...

3708

扫码关注云+社区

领取腾讯云代金券