如何用100行神经网络代码为黑白图片着色
编者按:如果给你一张黑白图片,你会怎样对它进行着色处理?相信大多数人会给出Photoshop这个答案。的确,技术发展到今日,PS这类美术工具大大简化了传统的绘画方式,给予用户更多便利,但如果涉及对图片着色,尤其是要求有各种环境、历史背景下的自然过渡,即便是有深厚美术功底的人,他们也要耗费数周乃至一个月的时间来层层渲染。那普通的开发者和想偷懒的设计师是不是和高效高质量着色没有缘分了呢?近日,博主Emil Wallnér授权论智向大家分享一篇初学者教程,只用100行代码就能帮你解决这个问题。
以下是由论智编译的原文:
今年早些时候,Amir Avni在社区介绍了一个能为历史黑白图片着色的机器人程序,通过深度学习神经网络,它能在短短几秒内就完成设计师长达一个月的手工劳动。我对Amir的网络十分着迷,于是下载了相关资源并做了一系列测试。
现在图像着色一般是在Photoshop中进行的,如果是对历史图片着色,工作人员可能需要进行长期调研,从政府、报社获得大量与当时历史背景相关的文字资料,并根据图片中的光线、场景进行长达一个月的手动劳作。光是一张脸,他们都需要添加多达20余层的粉色、绿色和蓝色,来使它恰到好处。
鉴于文章面对的是初学者,如果你对一些专业术语还没有深刻的了解,你可以先阅读我之前的两篇博文:深度学习第一周和用代码编写深度学习历史。结合实践,我会在这篇文章中介绍我是如何建立着色神经网络的,它主要分为3个部分。
第一部分是分解核心逻辑。我将构建一个40行的神经网络作为着色机器人的α测版本,这一版不会涉及太多技巧,只作为熟悉语法的过程。
第二部分则是创建一个真正的β测神经网络。我将让它实现对一些从没见过的图片进行着色。
第三部分,也就是在最后的final版中,我会把神经网络和分类器结合起来。我使用的是已经在120万张图像上训练过的谷歌Inception Resnet V2,以及图片网站Unsplash上的肖像图(文末附公开数据集地址)。
核心逻辑
在本节中,我将概述渲染图像、颜色数值表示的基础知识和神经网络的主要逻辑。
黑白图像可以用像素网格来表示,每一个像素都有一个对应其亮度的值,数值0—225代表的正是从黑色到白色。
而彩色图像则可被分为3层:红色层、绿色层和蓝色层。这可能与我们的直观感受相悖,想象一下,如果把一幅白底色绿叶图分为3个调色通道,从直觉上说,我们可能会觉得它只存在于绿色层。
事实上,正如你所看到的,绿叶在三个调色通道中都存在,颜色层决定的不仅仅是颜色,还有亮度。
例如,如果你想调出白色,你就需要平均分配这三种颜色,在绿色中加入等量的红色和蓝色,使得绿色更亮。这种利用三原色的色光以不同的比例相加,以产生多种多样的色光的方法即是RGB模型,在彩色图像中,它使用红绿蓝三层来编码颜色和对比度:
就像黑白图片一样,彩色图像中的每一层都有一个0—225的值,值0表示它在这一层没有颜色,如果所有调色通道中的值都是0,那图像的像素就是黑色。
正如我们所知道的,神经网络会在输入和输出之间建立联系。为了更精确地实现我们的着色任务,神经网络需要找到将黑白图像于彩色图像连接起来的特征。
总之,我们正在寻找将灰度值网格连接到三色网格的方法。
Alpha版本
我们先从一个简单版本的神经网络开始,为一张普通女性面部图像着色。通过这种方式,你可以在添加功能的同时熟悉模型的核心语法。
只用40行代码,我们可以实现如下图的转换。其中右侧为原始彩色图像,左侧为黑白图像,中间的图片是由神经网络生成的。该网络在相同的图片上进行了训练和测试,之后我们会提到这一点。
色彩空间
首先,我们将用一种算法来改变调色通道,把它由RGB模型转为Lab模型。这是一种描述颜色显示方式的模型,其中L表示Luminosity,即亮度,a和b分别表示从洋红色至绿色的范围和从黄色至蓝色的范围。
正如你在下图中看到的,Lab模型编码的图像具有一个灰度涂层,并且将原来的3个颜色层分重新划分成了2个。这就意味着我们的最终测试版可以直接转换黑白图片,并且我们只需要对两种调色通道进行预测。
同时,科学研究证实,人眼中有高达94%的细胞决定我们看到的亮度,而作为颜色“传感器”的细胞只有剩下的6%。如上图所示,灰度图像比彩色图像锐利得多,这是我们将灰度图像保留下来的另一个原因。
从黑白到彩色
我们的最终想法是这样的:输入灰度层,之后预测Lab模型中a、b两个通道的颜色层。在下文中,我们用L表示输入的黑白图像,而输出则是Lab图像。
因为涉及把一个图层转换为两个图层,我们使用卷积filter(滤波器),这相当于3D眼镜使用的滤镜,它可以删除或提取原图中的部分信息,这在一定程度上决定了图像中可以被看到的的内容。有了它,神经网络能用一个filter创建图像,或将几个filter的内容组合在一起,形成一张新图。
对于卷积神经网络(CNN),每个filter都会自动调整以帮助实现预期效果,所以我们使用的方法是在a通道和b通道内堆叠数百个filter。
在介绍其他工作原理前,先让我们来运行一下代码。
在FloydHub上部署代码
如果你没有FloydHub,你可以先上官网去看一下它的两分钟安装教程,或者是我之前提到的的博客深度学习第一周,这是在云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仪表盘会在浏览器中打开,之后系统会提示你创建一个FloydHub的新项目colornet。完成后,返回终端并运行相同的init命令。
floyd init colornet
之后,让我们开始干活:
floyd run --data emilwallner/datasets/colornet/2:data --mode jupyter --tensorboard
这里先插播一则简要说明:
我已经在FloydHub上挂了一个公开数据集,放在目录中有
--dataemilwallner/datasets/colornet/2:data
的data里。具体可以访问FloydHub查看。
我使用了Tensorboard--tensorboard
我使用了Jupyter Notebook模式--mode jupyter
如果还有GPU credits,你可以把GPU的flag--gpu添加到命令中,这大约能使模型运行速度加快50倍。
进入Jupyter笔记本,在FloydHub网站的工作标签下,点击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)
从epochs=1开始,我们把它慢慢增加到10、100、500、1000、3000。epoch数值表示神经网络学习的次数,而网络训练后的图像img_result.png可以在主文件夹中找到。
Alpha版代码:
# 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)
# 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(canvas))
imsave("img_gray_scale.png", rgb2gray(lab2rgb(canvas)))
用FloydHub命令运行神经网络:
floyd run --data emilwallner/datasets/colornet/2:data --mode jupyter --tensorboard
技术说明
总而言之,输入是表示灰度的像素网格,它对应的输出是两个带有颜色数值的网格,在输入和输出间,我们用filter把它们连接在一起,这就形成一个卷积神经网络。
当我们训练网络时,我们使用了彩色图像,并把RGB色彩空间转换成了Lab色彩空间。如下图所示,黑白图层是我们的输入,两个彩色图层是输出。
在上图左侧,我们可以看到B&W输入、一些filter以及模型的预测。
第三幅图和第四幅图分别是相同间隔内预测值和实际值的情况,为了形成对比,我们在模型中使用了tanh激活函数,将间隔范围控制在-1到1之间。同时,由于Lab默认的色彩空间在-128到128之间,我们将它除以128,这样范围也就落入-1到1的区间内。这种“标准化”能使我们更简单直观地发现错误。
计算获得错误后,神经网络会通过更新filter减少误差,之后继续反复循环,直至错误率尽可能最低。
让我来解释其中的一些代码语法。
X = rgb2lab(1.0/255*image)[:,:,0]
Y = rgb2lab(1.0/255*image)[:,:,1:]
1.0/255表示我们使用的是24位RGB色彩空间,这意味着每个调色通道的值是0—255之间的数字,共有1670万种颜色组合。
由于人眼只能感知两百万至一千万种颜色,使用这么大的色彩空间并没有多大意义。
Y = Y / 128
Lab的色彩空间与RGB相比范围不同,a、b两条通道的彩色光谱范围是-128到128,这就意味着通过将输出层中的所有值除以128,我们就能把它的范围控制在-1和1之间。
而这正好与我们的神经网络相匹配,它的返回值也在-1和1之间。
用函数rgb2lab()转换色彩空间后,我们选择灰度图层[ : , : , 0]——神经网络的输入;再用[ : , : , 1: ]把它分为两个颜色图层:绿—红、蓝—黄。
训练了神经网络后,我们得到最终预测,并将其转换成图片。
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,0,0)黑色RGB画布,把测试图像中的灰度图层数值复制进去,再把Lab中的两个颜色层数值添加进去,这个形成的像素值数组就能转换成最终的输出图片。
Alpha版总结
阅读论文是一件颇具挑战性的事。我发现当我总结了某篇论文的核心要素后,就很容易忽视文章本身的。因此我在上下文中增加了许多细节描述;
从一个简单项目开始是成功的关键。我在网上找到的实现方法都要2000至1万行代码,这对于掌握这个问题的核心逻辑十分不利。但是一旦我找到了一个barebones版本,无论是阅读代码还是阅读论文,这些都变得很轻松;
探索公共项目。为了大致了解代码的内容,我在Github上参与了50—100个着色项目;
事情不总是按预期进行。刚开始的时候,模型只能生成黄色和红色,因为我使用了Relu激活函数,而它不能生成负数,也就是绿色和蓝色。最后我终于找到了解决办法——使用tanh激活函数并映射Y值;
理解>操作。我发现许多项目实现很快,很难参与合作,因此比起加快码代码的速度,我把重心放在了创新速度上。
β测试版
如果你想了解Alpha版的缺点,我建议你按我说的建一个模型,并用数据集以外的图片试一试。你会发现它记住的只是训练集中的数据信息,并不能把经验延伸到从未见过的图像上。这正是我们要在β测试版种要做的:教神经网络推广所学经验。
和上一版一样,这一次我用的还是自己创建的数据集,包括9500张训练图像和500张测试图像。以下是新版的着色效果。
特征提取器
在上一版中,我们的神经网络已经发现了黑白图像和彩色图像的对应方式。但是试想一下,如果你正在为一张黑白图片着色,但一次只能看到9个像素,无论怎么扭转方位观察,你真的能准确预测每个像素的真正颜色吗?
我们可以举个例子,上图是介绍Alpha版时使用的女性肖像上的一处鼻孔边缘,你可以想象得到,在这种情况下要实现完美着色几乎是不可能的,所以我们需要把它分解为几个步骤。
首先,你需要在图像中发现一些简单图案,如对角线、全黑像素等。你要找出具有这些相同特征的像素并把它们归为一类。如果你有64个filter,那就意味着你能得到64张全新的图像。
之后,如果你再次扫描图像,你会得到一些检测到的相同小图案,为了更好地理解它们,我们对它们的尺寸做多次减半处理。
当图案缩小到9个像素后,这时你还有一个3x3的低水平filter,通过它们的结合,你可以检测到一些更复杂的图案,如一个像素组合可能会形成一个半圆、一个小点或一条线。再一次,如果你从图片中反复提取相同的小图案,你就会得到128个全新的过滤图像,它们可能长这样:
正如之前提到的,你会从一些低水平的特征开始,比如说边缘。输出层附近的层会将它们组合成模式,然后合并细节,最终形成一张人脸。
这个过程与图像处理使用的神经网络类似,也就是卷积神经网络。在CNN中,你可以组合多个过滤图像来了解图像中的上下文。
从特征提取到颜色提取
神经网络是以试错的方式运行的。它首先对每个像素进行随机预测,基于误差,它又通过反向调整来改进特征提取。当它需要纠正最大的错误时,它通常采取的方式是判断颜色对不对,及如何定位不同的对象。
当面对一张输入图像时,神经网络会先把所有对象着色为褐色,这是和其他颜色最相近的颜色,也就是说,误差最小。当然,由于大多数训练数据十分相似,网络有时会难以分清各个对象,这一点我会在完整版中提及。
以下是β测试版的代码:
# 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命令:
floyd run --data emilwallner/datasets/colornet/2:data --mode jupyter --tensorboard
技术说明
这个网络和其他图像识别神经网络的主要区别在于像素位置的重要性。在我们的着色网络中,整个图像的大小的比例是保持不变的,但在其他网络中,图像越接近最后一层,它就越容易出现畸变。出现这个现象的原因是分类网络中的最大池化层只重视信息而不重视图像布局,当它增加信息密度时,图像变形随之发生。
因此,我们的着色网络使用了一个步幅为2的卷积层,将图像的宽、高缩小为原来的二分之一。这种做法增加了信息密度,但不会扭曲图像。
此外,它们还存在两个区别,一是上采样(upsampling)层,二是图像比例。
分类网络只关心最后的分类情况,因此它会在网络中不断降低图像的大小和画质;而着色网络比例恒定,如上图所示,它会添加可视化白色填充。利用*padding='same'*参数,它能防止每个卷积层都切割图像。
而为了使图像大小加倍,着色网络使用了上采样层。
for filename in os.listdir('/Color_300/Train/'):
X.append(img_to_array(load_img('/Color_300/Test'+filename)))
这个for-loop首先计算目录中的所有文件名。然后,它遍历图像目录并将图像转换为像素数组。最后,它再将它们组合成一个巨大的向量。
datagen = ImageDataGenerator(
shear_range=0.2,
zoom_range=0.2,
rotation_range=20,
horizontal_flip=True)
通过ImageDataGenerator,我们可以调整我们这个图片生成器的设置。其中shear表示图片左右翻转,zoom表示放大缩小比例,rotation则代表图片旋转。这可以保证我们生成的每一张图片都各不相同,从而提高了网络的学习率。
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越强,处理的图片就越多。利用上述设置,你可以处理约50—100张图像,其中steps_per_epoch是训练图片数量和batch size的商。例如,如果我们有100张图片,它的batch size是50,那它的steps_per_epoch就是2。神经网络的训练次数(epoch)取决于你想让它训练多少次,通常1万张图像如果要在Tesla K80 GPU上训练21次,它大约需要11小时。
β测试版小贴士
在进行大批量运行前,先做许多小批量测试。在实验中,即便已经跑了二三十次,我还是发现了一些错误。神经网络能运行不意味着它不会出错,它的错误往往比传统编程错误更微妙,比如我就曾遭遇Adam突然上蹿的情况。
数据集种类越多,生成的图像越偏褐色。如果使用很相似的图片训练神经网络,你也许能获得一个不错的结果,而不用建立复杂架构。神经网络的“权衡技巧”有时会让它在通用化上的表现令人失望。
形状,形状,形状。重要的事情说三遍,每个图像的大小和比例必须是精确、固定的。一开始我用的时大小为300的图片,三次减半后,我分别的到了150、75、35.5,这导致的直接结果就是损失了半个像素,出现了一溜“黑客”。之后我才意识到用2的幂会更好。
创建数据集:1)禁用 .DS_Store文件,它快把我逼疯了;2)保持创新,为了下载. c文件,我用了Chrome控制台脚本和一个扩展程序;3)为原始文件备份,并构建一个清理脚本。
Final完整版
最终版的着色神经网络有4个组件,我们把之前两个网络分成编码器和解码器,并在中间插入一个fusion layer。如果你对分类网络不太理解,可以先去学习这篇教程(http://cs231n.github.io/classification/)。
和编码器相对应的,我们为输入图像也选择了一款分类器——Inception ResNet v2。这是一个在120万张图像上经过训练的神经网络,堪称当今世界上最强大的分类器之一。我们提取了分类层并把它和编码器结合在一起。
更多图像化细节可以点击https://github.com/baldassarreFe/deep-koalarization查看。
通过把分类器添加进着色神经网络,我们的网络能了解图像内容,从而将对象和着色方案匹配在一起。
下图是一些验证图像,我在训练时只用了20张图片。
如图所示,大多数图像很糟糕,但从中我也能找出几张正常的,因为我引入了一个包含2500张图像的验证集(validation set)。在更多图像上经过训练后,神经网络得到了更加一致的结果,但是它把其中的大部分都着色成了褐色。
对于这个问题,以下是前人在着色研究中提出的几种最常见的架构:
在图片中手动添加有色小点来引导神经网络(论文:http://www.cs.huji.ac.il/~yweiss/Colorization/);
找到一个匹配的图像并转移着色(论文1:https://dl.acm.org/citation.cfm?id=2393402,论文2:https://arxiv.org/abs/1505.05192);
残差编码器(Residual encoder)和合并分类层(论文:http://tinyclouds.org/colorize/);
合并分类网络超列(hypercolumns)(论文1:https://arxiv.org/pdf/1603.08511.pdf,论文2:https://arxiv.org/pdf/1603.06668.pdf);
合并编码器和解码器的最终分类(论文:http://hi.cs.waseda.ac.jp/~iizuka/projects/colorization/data/colorization_sig2016.pdf)。
色彩空间:Lab、YUV、HSV和LUV。
Loss:均方误差、分类、加权分类。
我选择了fusion layer架构,即第五种“合并编码器和解码器的最终分类”。它产生的结果更好,在Keras中也容易理解、复制。坦诚地说,这并不是最强的着色网络设计,但它非常适合初学者,尤其是在了解着色问题动态演变方面,堪称一个伟大的体系结构。
我沿用了Federico Baldassarre等人的神经网络设计,并把它们改编、应用到Keras中,具体代码如下所示。值得注意的是,在下文的代码中,我已经把Keras的Sequential模型转成了相应的功能API。
# 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功能API的情况还是很理想的。
首先,我们下载Inception ResNet v2神经网络并赋予权重,由于需要并行使用两个模型,我们得指定一个正在使用的模型,这可以在后端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上大约需要耗费1个小时。如果内存宽裕,事实上它最多可以一次生成50张。
yield ([X_batch, create_inception_embedding(grayscaled_rgb)], Y_batch)
这与我们的colornet模型格式相匹配。
model = Model(inputs=[encoder_input, embed_input], outputs=decoder_output)
编码器模型接收到输入encoder_input,之后它的输出和embed_input在fusion layer中合并,这个合并的输出将作为新的输入进入解码器模型,在那里,解码器返回最终的输出,也就是decoder_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)
在fusion layer中,我们将这1000个分类层乘以1024(32×32),这样Inception模型的最后一层就在1024行。
最后,我们把它从2D变为3D,即一个有1000根分类立柱的32×32网格,然后再把它们和编码器模型的输出连接在一起。在fusion layer输出最终值前,它们还要经过一个包含了254个1X1卷积核(filter)的卷积网络。
Final完整版小贴士
研究专业术语是一项艰巨的工程。为了理解如何在Keras上实现“fusion model”,我Google了3天,因为它听起来太复杂了,我不想自己看。事实证明,这种找捷径的想法才真正误导了我;
我曾在网上提问。我之前在Keras的slack频道提了一个问题,然后被Stack Overflow删除了。既然依靠公众力量分解难题这条路走不通,我想我就只能迫使自己把这个错误孤立起来,去更好的接近最后的解决方案;
给人发邮件。虽然论坛上可能冷冷清清,但是如果你直接和本人联系,他们还是会关注你的,我就在Skype上和相关研究人员聊过色彩空间;
在我决定先搁置fusion后,我先把其他组件都准备好,并组合在一起。
当我觉得一件事可行,我往往会犹豫。虽然我知道那样做的核心逻辑的可行的,但是在事实出来前,我什么都不信。我曾在思考了一段时间后尝试了一种新方法,模型跑完第一行就报错了,我用了4天时间修改了几百个bug,在谷歌上搜索了上千次,之后我的模型下就出现了“Epoch 1/22”。
下一步
为图像着色是一件非常吸引人的事情,与其说这是一个艺术问题,它更像一个科学难题。为了使感兴趣的读者能更快地入门,我撰写了这篇文章,并给予你们以下几点建议:
用一个事先训练好的模型来实现它;
尝试不同的数据集;
用更多的图片数量来提高准确率;
在RGB色彩空间内建立一个放大器。为着色网络创建一个类似的模型,将颜色过度饱和的彩色图像作为输入,并将正确的彩色图像作为输出;
使用加权分类;
把模型用于视频。不用太在意着色的问题,你可以重点关注不同帧的图像在着色上是否一致。你也可以在大小图像着色是否一致上做一些尝试。
如果你懒得自己做,也可以用FloydHub,直接搬走我的3个着色模型为黑白图片着色,它们的一些使用注意事项是:
对于alpha版本,你只需将woman.jpg替换成你文件的名字即可(大小为400x400);
对于β测试版和最终版,你需要在运行FloydHub命令之前把图片添加到Test文件夹中,或者在notebook运行的时候把它们直接放进Notebook的Test里。请注意,图片大小必须是256x256。此外,上传图片颜色不限,因为无论是什么样的图片,这个模型都会把它先转成黑白。
如果你在建立模型的过程中遇到问题卡住了,欢迎在twitter上联系我:emilwallner,我也很想看看你建的东西。
关于作者
这是Emil Wallnér深度学习教程系列的第三部分内容。作者花了10年时间探索人类学习,他曾在牛津商学院工作,投资过教育初创公司,建立了教育技术公司。去年,他入职编程教育学校Ecole 42,正式开始将他在人类学习和机器学习上的知识传播出去。
原文地址1(博客):blog.floydhub.com/colorizing-b&w-photos-with-neural-networks/
原文地址2(Medium):medium.freecodecamp.org/colorize-b-w-photos-with-a-100-line-neural-network-53d9b4449f8d
FloydHub公开资源:www.floydhub.com/emilwallner/datasets/cifar-10/1/cifar-10-batches-py
Github参考知识:github.com/baldassarreFe/deep-koalarization
基础知识补充
深度学习第一周:blog.floydhub.com/my-first-weekend-of-deep-learning/
用代码编写深度学习历史:blog.floydhub.com/coding-the-history-of-deep-learning/
分类网络简介:cs231n.github.io/classification/
编者的话:首先再次感谢作者的热心分享~鉴于原文过长,论智君在编译过程中可能会出现错字、漏字,如发现错误,或对原文有什么不理解,欢迎读者在文下留言或前往twitter关注作者,小编将不胜感谢!