大数据

用机器学习做个艺术画家-Prisma(上)

作者:阿布🐶
微信:aaaabbbuu
未经本人允许禁止转载

所谓深度学习(Deep Learning)中的 “深度(Deep)” 即意为层数。神经网络的每一层都会对图片特征进行提取,而 “艺术风格” 则是各层提取结果的叠加
在人类的视觉系统中,从眼睛看到一件实体,到在脑中形成图像的概念,中间经历了无数层神经元的传递。底层的神经元获取到的信息是具体的,越到高层越抽象。

在人类的视觉系统中,从眼睛看到一件实体,到在脑中形成图像的概念,中间经历了无数层神经元的传递。底层的神经元获取到的信息是具体的,越到高层越抽象
使用计算机模拟这个网络,将每一层的结构分析出来,能看到在采样过程中,底层网络对于图像的细节表达得特别清楚,越到高层像素保留得越少,轮廓信息越多

使用深度学习作画最早是三个德国研究员想把计算机调教成梵高,他们研发了一种算法,模拟人类视觉的处理方式。具体是通过训练多层卷积神经网络(CNN),让计算机识别,并学会梵高的 “风格”、然后将任何一张普通的照片变成梵高的《星空》

大致实现思路如下:

  1. 吸收用户拍摄的照片
  2. 让计算机学会星空图的风格
  3. 计算机输出自己做的“新画”

他们开创了Deep Art公司,他们的用户可以花上 19 欧买一张适合明信片用的作品,或者多掏 100 欧,买一张大尺寸油画级别的艺术画

Prisma 比 Deep Art 先进的地方在于,它大大缩短了图像处理的时间,每张照片在 Prisma 系统内的处理时间控制在秒级别。 prisma诞生于俄罗斯,是一个仅有4个年轻人历时一个半月开发出的图片处理应用,将照片赋予毕加索式的艺术风格,是它们的广告语,它的核心技术思想就是卷积神经网络可以被看做是一个机器艺术家

prisma在中国基本上是连接不上,没办法使用的状态,没用过也不用下载了

本章内容主要讲述我封装的两套实现prisma效果的代码使用。你不需要有任何机器学习,图像处理理论基础,读完这篇文章后,下载github上的代码,安装好环境(caffe的安装环境比较复杂)仿照这里的使用方式你就可以自己制作私人的艺术风格照片了

如下地址为git上项目的最终演示使用视频,可以先有个直观感受

youku 阿布Prisma演示视频
更多风格图像展示墙
项目git地址

首先导入库

from __future__ import division
import matplotlib.pylab as plt
%matplotlib inline

import os
from PrismaCaffe import CaffePrismaClass
from PrismaTensor import TensorPrismaClass
import PrismaTensor
import PrismaHelper
import glob
import numpy as np
import PIL.Image
import ZCommonUtil
import itertools

1 基于caffe框架实现prisma

1.1 首先我们直观感受一下什么叫做机器艺术家

如下代码显示出所有演示实例图片(主角还是我家阿布🐶)

sample_list = glob.glob("../sample/*.jpg")
fig, axs = plt.subplots(nrows=2, ncols=4, figsize=(15, 6));
axs_list = list(itertools.chain.from_iterable(axs))
for ind, ax in zip(range(2 * 4), axs_list):
    iter_fn = sample_list[ind]
    iter_img = plt.imread(iter_fn)
    ax.set_title(os.path.basename(iter_fn))
    ax.imshow(iter_img);
    ax.set_axis_off()

使用caffe封装的大名鼎鼎的google deepdream来看看效果

下面的代码初始化一个CaffePrisma工作实例,显示阿布美照,打印出模型网络的前几个浅层指令

cp  = CaffePrismaClass(dog_mode=False)
nbks = filter(lambda nbk: nbk[-8:-1] <> '_split_', cp.net.blobs.keys()[1:-2])[:10]
abu4_file = '../sample/abu4.jpg' 
PrismaHelper.show_array_ipython(np.float32(cp.resize_img(PIL.Image.open(abu4_file))))
nbks

['conv1/7x7_s2',
 'pool1/3x3_s2',
 'pool1/norm1',
 'conv2/3x3_reduce',
 'conv2/3x3',
 'conv2/norm2',
 'pool2/3x3_s2',
 'inception_3a/1x1',
 'inception_3a/3x3_reduce',
 'inception_3a/3x3']

下面我们用这几个浅层神经元对原图风格化的效果展示

for nbk in nbks[2:-2]:
    d_img = cp.fit_img(abu4_file, resize=True, nbk=nbk, iter_n=10)
    PrismaHelper.show_array_ipython(np.float32(d_img))

本章的内容的代码及示例并不能做出如下所示的效果的风格图像,如下所示的风格图像将在下一章详细讲解原理及代码,本章是基础

fig, axs = plt.subplots(nrows=1, ncols=3, figsize=(20, 10));
up_list = ['../show/up2.jpg', '../show/up3.jpg', '../show/up4.jpg']
for ind, ax in zip(range(1 * 3), axs):
    iter_fn = up_list[ind]
    iter_img = plt.imread(iter_fn)
    # ax.set_title(os.path.basename(iter_fn))
    ax.imshow(iter_img);
    ax.set_axis_off()

当然如果你只是想要得到这样效果的照片,对技术没有兴趣,可以把照片图像发给我,我做好后再次传给你!

有没有感觉到特征的识别由浅入深的一步一步增强,也就是从edge,到shape,再到复杂的shape循序渐进的过程,试着感觉一下有没有慢慢一点点睁开眼睛的感觉,这里主要是把每层的特质放大进行夸张凸显

看一下CaffePrismaClass初始化代码

class CaffePrismaClass(BasePrismaClass):
    def __init__(self, dog_mode=False):
        self.net_fn = '../mode/deploy.prototxt'

        if not dog_mode:
            self.param_fn = '../mode/bvlc_googlenet.caffemodel'
            mu = np.float32([104.0, 117.0, 123.0])
        else:
            self.param_fn = '../mode/dog_judge_train_iter_5000.caffemodel'
            model_mean_file = '../mode/mean.binaryproto'
            mean_blob = caffe.proto.caffe_pb2.BlobProto()
            mean_blob.ParseFromString(open(model_mean_file, 'rb').read())
            mean_npy = caffe.io.blobproto_to_array(mean_blob)
            mu = np.float32(mean_npy.mean(2).mean(2)[0])

        model = caffe.io.caffe_pb2.NetParameter()
        text_format.Merge(open(self.net_fn).read(), model)
        model.force_backward = True

        open('tmp.prototxt', 'w').write(str(model))
        self.net = caffe.Classifier('tmp.prototxt', self.param_fn,
                                    mean=mu,
                                    channel_swap=(
                                        2, 1, 0))

注意到上面的 bvlc_googlenet.caffemodel是google已经训练好的模型,可以通过我的网盘链接下载,提取码为eup6

至于下面代码中的../mode/dog_judge_train_iter_5000.caffemodel这个是我在爬取百度图片各种狗狗的图片,使用caffe训练模型分类中自己训练好的模型,你可以不必管

备注:上面的代码中mu = np.float32([104.0, 117.0, 123.0])常数的选择是从模型训练时的网络配置中决定的,它们只是为了提高训练识别速度,如下配置

  transform_param {
    mirror: true
    crop_size: 224
    mean_value: 104
    mean_value: 117
    mean_value: 123
  }

更详尽的网络配置

下面我们使用abu1来逐步介绍封装类的具体使用方式

abu1_file = '../sample/abu1.jpg'
PrismaHelper.show_array_ipython(np.float32(cp.resize_img(PIL.Image.open(abu1_file))))

1.2 直接使用某个神经元层的效果

d_img = cp.fit_img(abu1_file, resize=True, nbk='conv1/7x7_s2', iter_n=10)
PrismaHelper.show_array_ipython(np.float32(d_img))

1.3 配合PIL预处理方式处理图像

你如果用过prisma,你一定会知道prisma有很多效果比如黑白,水墨,怎么以实现吗,我们如下代码使用PIL库预处理一下图片,然后再做处理,如下的基类封装了接口和预处理操作

class BasePrismaClass(six.with_metaclass(ABCMeta, object)):
    @abstractmethod
    def fit_guide_img(self, img_path, gd_path, resize=False, size=480, enhance=None, iter_n=10, **kwargs):
        pass

    @abstractmethod
    def fit_img(self, img_path, resize=False, size=480, enhance=None, iter_n=10, **kwargs):
        pass

    @abstractmethod
    def gd_features_make(self, *args, **kwargs):
        pass

    @abstractmethod
    def do_prisma(self, *args, **kwargs):
        pass

    def resize_img(self, r_img, base_width=480, keep_size=True):
        if keep_size:
            w_percent = (base_width / float(r_img.size[0]))
            h_size = int((float(r_img.size[1]) * float(w_percent)))
        else:
            h_size = base_width
        r_img = r_img.resize((base_width, h_size), PIL.Image.ANTIALIAS)
        return r_img

    def handle_enhance(self, r_img, enhance, sharpness=8.8, brightness=1.8, contrast=2.6, color=7.6, contour=2.6):
        if enhance == 'Sharpness':
            enhancer = ImageEnhance.Sharpness(r_img)
            s_img = enhancer.enhance(sharpness)
            img = s_img
        elif enhance == 'Brightness':
            enhancer = ImageEnhance.Brightness(r_img)
            b_img = enhancer.enhance(brightness)
            img = b_img
        elif enhance == 'Contrast':
            enhancer = ImageEnhance.Contrast(r_img)
            t_img = enhancer.enhance(contrast)
            img = t_img
        elif enhance == 'Color':
            enhancer = ImageEnhance.Color(r_img)
            c_img = enhancer.enhance(color)
            img = c_img
        elif enhance == 'CONTOUR':
            enhancer = ImageEnhance.Contrast(r_img)
            t_img = enhancer.enhance(contour)
            fc_img = t_img.filter(ImageFilter.CONTOUR)
            img = fc_img
        elif enhance == 'EDGES':
            ffe_img = r_img.filter(ImageFilter.FIND_EDGES)
            img = ffe_img
        elif enhance == 'EMBOSS':
            feb_img = r_img.filter(ImageFilter.EMBOSS)
            img = feb_img
        elif enhance == 'EEM':
            feem_img = r_img.filter(ImageFilter.EDGE_ENHANCE_MORE)
            img = feem_img
        elif enhance == 'EE':
            fee_img = r_img.filter(ImageFilter.EDGE_ENHANCE)
            img = fee_img
        else:
            img = r_img
        return img

下面使用conv2/3×3和预处理效果综合显示效果看看(由于篇幅只运行两个效果,其它的读者可自行打开注释的代码查看效果)

d_img = cp.fit_img(abu1_file, resize=True, nbk='conv2/3x3', iter_n=10)
PrismaHelper.show_array_ipython(np.float32(d_img))
d_img = cp.fit_img(abu1_file, resize=True, nbk='conv2/3x3', enhance='Sharpness', iter_n=10)
PrismaHelper.show_array_ipython(np.float32(d_img))
d_img = cp.fit_img(abu1_file, resize=True, nbk='conv2/3x3', enhance='Contrast', iter_n=10)
PrismaHelper.show_array_ipython(np.float32(d_img))
# d_img = cp.fit_img(abu1_file, resize=True, nbk='conv2/3x3', enhance='Brightness', iter_n=10)
# PrismaHelper.show_array_ipython(np.float32(d_img))
# d_img = cp.fit_img(abu1_file, resize=True, nbk='conv2/3x3', enhance='CONTOUR', iter_n=10)
# PrismaHelper.show_array_ipython(np.float32(d_img))
# d_img = cp.fit_img(abu1_file, resize=True, nbk='conv2/3x3', enhance='Color', iter_n=10)
# PrismaHelper.show_array_ipython(np.float32(d_img))
# d_img = cp.fit_img(abu1_file, resize=True, nbk='conv2/3x3', enhance='EEM', iter_n=10)
# PrismaHelper.show_array_ipython(np.float32(d_img))
# d_img = cp.fit_img(abu1_file, resize=True, nbk='conv2/3x3', enhance='EE', iter_n=10)
# PrismaHelper.show_array_ipython(np.float32(d_img))

以上的这些预处理操作会有很多种组合及变种,比如我在代码中针对CONTOUR的处理是先做了个Contrast然后再CONTOUR这样效果针对CONTOUR效果会更好

elif enhance == 'CONTOUR':
    enhancer = ImageEnhance.Contrast(r_img)
    t_img = enhancer.enhance(contour)
    fc_img = t_img.filter(ImageFilter.CONTOUR)
    img = fc_img

如果你想做批处理风格画操作,使用fit_batch_img,其实这里没有完善,后期会修改为类似tensor prisma的批量处理实现方式,使用卷积层识别指令和图像预处理指令做product求笛卡尔积,遍历所有风格画组合

    def fit_batch_img(self, img_path, resize=False, size=480, enhance=None):
        """
        批量处理,但不支持并行,后修改为类似tensor prisma中的并行模式
        :param img_path:
        :param resize:
        :param size:
        :param enhance:
        :return:
        """
        r_img = PIL.Image.open(img_path)
        if resize:
            r_img = self.resize_img(r_img, size)

        org_img = self.handle_enhance(r_img, enhance)

        e_str = '' if enhance is None else '_' + enhance.lower()
        save_path = os.path.dirname(img_path) + '/batch_caffe/' + e_str
        ZCommonUtil.ensure_dir(save_path)
        org_img_path = save_path + 'org.jpeg'
        with open(org_img_path, 'w') as f:
            org_img.save(f, 'jpeg')

        org_img = np.float32(org_img)
        start = 1
        end = self.net.blobs.keys().index('inception_4c/pool')
        nbks = self.net.blobs.keys()[start:end]

        """
            不能使用多进程方式在这里并行执行,因为caffe.classifier.Classifier不支持序列化
            Pickling of "caffe.classifier.Classifier" instances is not enabled
            so mul process no pass
        """
        for nbk in nbks:
            if nbk[-8:-1] == '_split_':
                continue
            fn = save_path + nbk.replace('/', '_') + '.jpg'
            deep_img = self.do_prisma(org_img, iter_n=10, end=nbk)
            PrismaHelper.save_array_img(deep_img, fn)
        return save_path

封装的代码是deepdream的代码它由imagenet大量的图片数据来训练神经网络,并且使用google_lenet的深度模型网络大大提高了识别度,使这个网络可以判断出图片中的事物,类似于之前我的文章训练狗狗图片,对狗狗进行分类识别,风格画的实现原理是不止识别,它还把图片的特质从它的模型中选取图像重新在原图进行渲染,下一章将有重点介绍这部分的实现代码原理,这里暂且带过。具体请查看git上文件PrismaCaffe.py,核心代码如下

def _objective_l2(self, dst):
    dst.diff[:] = dst.data

def _objective_guide_features(self, dst, guide_features):
    x = dst.data[0].copy()
    y = guide_features
    ch = x.shape[0]
    x = x.reshape(ch, -1)
    y = y.reshape(ch, -1)
    a = x.T.dot(y)
    dst.diff[0].reshape(ch, -1)[:] = y[:, a.argmax(1)]

def do_prisma_step(self, step_size=1.5, end='inception_4c/output',
                   jitter=32, objective=None):
    if objective is None:
        raise ValueError('make_step objective is None!!!')

    src = self.net.blobs['data']
    dst = self.net.blobs[end]
    ox, oy = np.random.randint(-jitter, jitter + 1, 2)
    src.data[0] = np.roll(np.roll(src.data[0], ox, -1), oy, -2)

    self.net.forward(end=end)
    objective(dst)
    self.net.backward(start=end)

    g = src.diff[0]
    src.data[:] += step_size / np.abs(g).mean() * g
    src.data[0] = np.roll(np.roll(src.data[0], -ox, -1), -oy, -2)

def do_prisma(self, base_img, iter_n=10, octave_n=4, octave_scale=1.4,
              end='inception_4c/output', **step_params):
    octaves = [PrismaHelper.preprocess_with_roll(base_img, self.mean_pixel)]
    for i in xrange(octave_n - 1):
        octaves.append(nd.zoom(octaves[-1], (1, 1.0 / octave_scale, 1.0 / octave_scale), order=1))
    src = self.net.blobs['data']
    detail = np.zeros_like(octaves[-1])
    for octave, octave_base in enumerate(octaves[::-1]):
        h, w = octave_base.shape[-2:]
        if octave > 0:
            h1, w1 = detail.shape[-2:]
            detail = nd.zoom(detail, (1, 1.0 * h / h1, 1.0 * w / w1), order=1)
        src.reshape(1, 3, h, w)
        src.data[0] = octave_base + detail
        for i in xrange(iter_n):
            self.do_prisma_step(end=end, **step_params)
        detail = src.data[0] - octave_base
    return PrismaHelper.deprocess_with_stack(src.data[0], self.mean_pixel)

def gd_features_make(self, guide, end):
    h, w = guide.shape[:2]
    src, dst = self.net.blobs['data'], self.net.blobs[end]
    src.reshape(1, 3, h, w)
    src.data[0] = PrismaHelper.preprocess_with_roll(guide, self.mean_pixel)
    self.net.forward(end=end)
    guide_features = dst.data[0].copy()
    return guide_features

    def fit_batch_img(self, img_path, resize=False, size=480, enhance=None):
        """
        批量处理,但不支持并行,后修改为类似tensor primsma中的并行模式
        :param img_path:
        :param resize:
        :param size:
        :param enhance:
        :return:
        """
        r_img = PIL.Image.open(img_path)
        if resize:
            r_img = self.resize_img(r_img, size)

        org_img = self.handle_enhance(r_img, enhance)

        e_str = '' if enhance is None else '_' + enhance.lower()
        save_path = os.path.dirname(img_path) + '/batch_caffe/' + e_str
        ZCommonUtil.ensure_dir(save_path)
        org_img_path = save_path + 'org.jpeg'
        with open(org_img_path, 'w') as f:
            org_img.save(f, 'jpeg')

        org_img = np.float32(org_img)
        start = 1
        end = self.net.blobs.keys().index('inception_4c/pool')
        nbks = self.net.blobs.keys()[start:end]

        """
            不能使用多进程方式在这里并行执行,因为caffe.classifier.Classifier不支持序列化
            Pickling of "caffe.classifier.Classifier" instances is not enabled
            so mul process no pass
        """
        for nbk in nbks:
            if nbk[-8:-1] == '_split_':
                continue
            fn = save_path + nbk.replace('/', '_') + '.jpg'
            deep_img = self.do_prisma(org_img, iter_n=10, end=nbk)
            PrismaHelper.save_array_img(deep_img, fn)
        return save_path

使用预处理再加上一些其它参数微调配合浅层特征就可以做出一些比较好看的效果,如使用演示视频中的GUI可对效果进行比较好的控制,如下效果,下一章详细介绍使用方式:

def show_lydw(fd_fn):
    sample_list = glob.glob(fd_fn)
    sample_list = sample_list[::-1]
    fig, axs = plt.subplots(nrows=1, ncols=2, figsize=(20, 10));
    for ind, ax in zip(range(1 * 2), axs):
        iter_fn = sample_list[ind]
        iter_img = plt.imread(iter_fn)
        ax.set_title(os.path.basename(iter_fn))
        ax.imshow(iter_img);
        ax.set_axis_off()
show_lydw('../show/bj*.jpg')

风格图像墙中地址

show_lydw('../show/nr*.jpg')

风格图像墙中地址

上面使用的原始图像的分辨率都很低,我自己风格化了很多30w像素nokia年代拍出的照片,风格化后由于技术特点不会感觉不清晰,反而更有味道,回味了一把30w像素年代的爱情往事💞

1.4 使用引导图进行风格引导

最后本小节我们看看prisma中会有很多风格引导,我们怎样使用CaffePrismaClass实现呢

首先将风格引导图都转换为224大小为了符合模型中对图片的要求(关于模型结构可以从代码中mode/deploy.prototxt查看详情)

img_gd_list = glob.glob("../prisma_gd/*.jpg")

for img_gd in img_gd_list:
    width = 224
    hsize = 224
    org_img_gd = PIL.Image.open(img_gd)
    r_img_gd = org_img_gd.resize((width, hsize), PIL.Image.ANTIALIAS)
    filen_ame = '../prisma_gd_224/' + os.path.basename(img_gd)
    ZCommonUtil.ensure_dir(filen_ame)
    with open(filen_ame, 'w') as f:
        r_img_gd.save(f, 'jpeg')

选出一张看看

img_gd_list = glob.glob("../prisma_gd_224/*.jpg")
gd_path = '../prisma_gd_224/tooopen_sy_127260228921.jpg'
guide = np.float32(PIL.Image.open(gd_path))
PrismaHelper.show_array_ipython(guide)

如下代码展示使用风格引导的风格画,和不使用风格引导做的风格画的区别

d_img = cp.fit_img(abu1_file, resize=True, nbk='conv2/3x3_reduce', iter_n=10)
PrismaHelper.show_array_ipython(np.float32(d_img))

d_img = cp.fit_guide_img(abu1_file, gd_path, resize=True, nbk='conv2/3x3_reduce', iter_n=10)
PrismaHelper.show_array_ipython(np.float32(d_img))

观察结果可以发现,引导图的特征并没有很多的嵌入原图中,这是由于deepdream实现的的机制是等权重的方式抽取特征导致(当然你也可以修改权重,但问题又会转移到如何分配才能达到视觉上的效果好)。

总结一下CaffePrismaClass的优点就是不需要太多次的迭代训练就可以创造出一副艺术画,缺点就是针对风格引导的渲染绘制显然欠缺。


停下来休息一下,这么长的文章我也不知道能有几个人看到这里,更何况有几个人能看到最后一章呢,我发现我之前写的打开股票量化的黑箱 只有第一章有一些人看,其实后面的才是重点,当然这主要怪我自己能力不足,写的不足以打动读者。

所以我要在这里把正事办了✊, prisma的最后一章我会把汪汪喵呜孤儿院中等待领养的小动物的照片做风格艺术图像, 它们的大概情况及具体领养地址, 这里我先把一些小动物的图像墙的地址列在下面,希望大家在自己的能力范围内,帮助这些流浪动物,领养,助养或者只是转发一下。

汪汪喵呜孤儿院中等待领养的小动物的照片做风格艺术图像,它们没有纯种的血统,猫的话可能就是叫白猫,黑猫,花猫。狗的话还有个名字‘中华田园犬’,作为经常去喂流浪狗食物的人们来说它们的名字一般是大黄,小黑,但请你发现它们的美,看着它们的眼睛,每一个都是那么的可爱漂亮,真心希望每一个天使都可以找到好的主人,幸福的过完它们本来就不长的一生,也希望你不要再犹豫去迎接这些可爱善良的天使,毕竟我们的生命都不长。

另外最近总是听到有认识的人要准备生小孩子,要把一直养的猫或者狗送老家,难道你们不是把狗狗当孩子养的吗,我会一直教育我的孩子把阿布当作家人看待的,是姐姐,希望所有人都能理性科学的对待小孩子和猫狗的关系,不要让这些事情变成一种传统,愿它们本来就不长的生命都能幸福。

求下面的图像点赞

show_lydw('../show/bz*.jpg')

请关注狗与爱将定期发布流浪狗领养,狗狗相亲等信息

2 基于tensorflow框架实现prisma

首先看看我们的风格引导图下都有什么

img_gd_list = glob.glob("../prisma_gd/*.jpg")

fig, axs = plt.subplots(nrows=5, ncols=8, figsize=(30, 15));
axs_list = list(itertools.chain.from_iterable(axs))
for ind, ax in zip(range(5 * 8), axs_list):
    iter_fn = img_gd_list[ind]
    iter_img = plt.imread(iter_fn)
    ax.set_title(os.path.basename(iter_fn).split('.')[0])
    ax.imshow(iter_img);
    ax.set_axis_off()

2.1 实例化一个封装好的tensorflow风格画实例

tp = TensorPrismaClass()
tp
mean_pixel: [ 123.68   116.779  103.939]

注意代码中的 K_VGG_MAT_PATH = ‘../mode/vgg_imagenet.mat’是vgg模型,可以通过我的网盘链接下载,提取码为gunt

TensorPrismaClass实现原理是低层次的卷积核学习特征纹理 颜色,边界等粗线条,高层次卷积核学到的是底层特征叠加所产生的形状内容特征,最终风格画的效果是由各个层特征分配权重组合而成,不断迭代计算loss function,来寻找图像的特征分配权重,详情代码请查阅github上的PrismaTensor.py,核心代码如下:

def _conv2d(self, img, w, b):
    return tf.nn.bias_add(tf.nn.conv2d(img, tf.constant(w), strides=[1, 1, 1, 1], padding='SAME'), b)

def _max_pool(self, img, k):
    return tf.nn.max_pool(img, ksize=[1, k, k, 1], strides=[1, k, k, 1], padding='SAME')

def __init__(self):
    self.net_fn = K_VGG_MAT_PATH

    if not ZCommonUtil.file_exist(self.net_fn):
        raise RuntimeError('self.net_fn not exist!!!')

    self.net_layers = K_NET_LAYER
    self.net_data = scipy.io.loadmat(self.net_fn)
    self.mean = self.net_data['normalization'][0][0][0]
    self.mean_pixel = np.mean(self.mean, axis=(0, 1))
    self.weights = self.net_data['layers'][0]

def _build_vgg_net(self, shape, image_tf=None):
    if image_tf is None:
        image_tf = tf.placeholder('float', shape=shape)
    net = dict()
    current = image_tf
    for ind, name in enumerate(self.net_layers):
        kind = name[:4]
        if kind == 'conv':
            kernels, bias = self.weights[ind][0][0][0][0]
            kernels = np.transpose(kernels, (1, 0, 2, 3))
            bias = bias.reshape(-1)
            current = self._conv2d(current, kernels, bias)
        elif kind == 'relu':
            current = tf.nn.relu(current)
        elif kind == 'pool':
            current = self._max_pool(current, 2)
        net[name] = current
    return net, image_tf

def _features_make(self, img, image_tf, net, features, guide):
    preprocess = np.array([img - self.mean_pixel])
    if guide:
        for gl in K_GUIDE_LAYERS:
            fs = net[gl].eval(feed_dict={image_tf: preprocess})
            fs = np.reshape(fs, (-1, fs.shape[3]))
            features[gl] = np.matmul(fs.T, fs) / fs.size
    else:
        features[K_ORG_LAYER] = net[K_ORG_LAYER].eval(feed_dict={image_tf: preprocess})

def _tensor_size(self, tensor):
    return reduce(mul, (d.value for d in tensor.get_shape()), 1)

def gd_features_make(self, org_img, guide_img):
    # noinspection PyUnusedLocal
    with tf.Graph().as_default(), tf.Session() as sess:
        org_shape = (1,) + org_img.shape
        org_net, org_img_tf = self._build_vgg_net(org_shape)
        org_features = dict()
        self._features_make(org_img, org_img_tf, org_net, org_features, False)

        guide_shapes = (1,) + guide_img.shape
        guide_net, guide_img_tf = self._build_vgg_net(guide_shapes)
        guide_features = dict()
        self._features_make(guide_img, guide_img_tf, guide_net, guide_features, True)
    return org_features, guide_features

def do_prisma(self, org_img, guide_img, ckp_fn, iter_n):
    org_shape = (1,) + org_img.shape
    org_features, guide_features = self.gd_features_make(org_img, guide_img)

    with tf.Graph().as_default():
        # out_v = tf.zeros(org_shape, dtype=tf.float32, name=None)
        out_v = tf.random_normal(org_shape) * 0.256
        out_img = tf.Variable(out_v)
        out_net, _ = self._build_vgg_net(org_shape, out_img)

        org_loss = K_ORG_WEIGHT * (2 * tf.nn.l2_loss(
            out_net[K_ORG_LAYER] - org_features[K_ORG_LAYER]) /
                                   org_features[K_ORG_LAYER].size)

        style_loss = 0
        for guide_layer in K_GUIDE_LAYERS:
            layer = out_net[guide_layer]
            _, height, width, number = map(lambda x: x.value, layer.get_shape())
            size = height * width * number
            feats = tf.reshape(layer, (-1, number))
            gram = tf.matmul(tf.transpose(feats), feats) / size
            style_gram = guide_features[guide_layer]
            style_loss += K_GUIDE_WEIGHT * 2 * tf.nn.l2_loss(gram - style_gram) / style_gram.size

        tv_y_size = self._tensor_size(out_img[:, 1:, :, :])
        tv_x_size = self._tensor_size(out_img[:, :, 1:, :])
        tv_loss = K_TV_WEIGHT * 2 * (
            (tf.nn.l2_loss(out_img[:, 1:, :, :] - out_img[:, :org_shape[1] - 1, :, :]) /
             tv_y_size) +
            (tf.nn.l2_loss(out_img[:, :, 1:, :] - out_img[:, :, :org_shape[2] - 1, :]) /
             tv_x_size))
        loss = org_loss + style_loss + tv_loss

        train_step = tf.train.AdamOptimizer(K_LEARNING_RATE).minimize(loss)

        # noinspection PyUnresolvedReferences
        def print_progress(ind, last=False):
            if last or (ind > 0 and ind % K_PRINT_ITER == 0):
                ZLog.info('Iteration %d/%d\n' % (ind + 1, iter_n))
                ZLog.debug('  content loss: %g\n' % org_loss.eval())
                ZLog.debug('    style loss: %g\n' % style_loss.eval())
                ZLog.debug('       tv loss: %g\n' % tv_loss.eval())
                ZLog.debug('    total loss: %g\n' % loss.eval())

        best_loss = float('inf')
        best = None
        with tf.Session() as sess:
            sess.run(tf.initialize_all_variables())
            for i in range(iter_n):
                last_step = (i == iter_n - 1)
                if not g_doing_parallel:
                    print_progress(i, last=last_step)
                train_step.run()
                if (i > 0 and i % K_CKP_ITER == 0) or last_step:
                    # noinspection PyUnresolvedReferences
                    this_loss = loss.eval()
                    if this_loss < best_loss:
                        best_loss = this_loss
                        best = out_img.eval()
                    if not last_step:
                        ckp_fn_iter = K_CKP_FN_FMT % (ckp_fn, i)
                        PrismaHelper.save_array_img(best.reshape(org_shape[1:]) + self.mean_pixel, ckp_fn_iter)
            return best.reshape(org_shape[1:]) + self.mean_pixel

使用K5做为引导风格查看效果

gd_path = '../prisma_gd/k5.jpg'
guide = np.float32(tp.resize_img(PIL.Image.open(gd_path)))
PrismaHelper.show_array_ipython(guide)

PrismaHelper.show_array_ipython(tp.fit_guide_img(abu1_file, gd_path, resize=True, iter_n=1800))

如上所示经过几个小时1800次迭代完整了这幅画,我们下面看看每100次迭代的对比图,首先如下代码所示,对每100次迭代的保存的图形进行排序

k5_list = glob.glob("../sample/batch_tensor/k5*.jpeg")
k5_ind = map(lambda fn: int(fn.rsplit('.')[2].rsplit('_')[-1]), k5_list)
k5_sorted = sorted(zip(k5_ind, k5_list))
k5_sorted
[(100, '../sample/batch_tensor/k51051051372_100.jpeg'),
 (200, '../sample/batch_tensor/k51051051372_200.jpeg'),
 (300, '../sample/batch_tensor/k51051051372_300.jpeg'),
 (400, '../sample/batch_tensor/k51051051372_400.jpeg'),
 (500, '../sample/batch_tensor/k51051051372_500.jpeg'),
 (600, '../sample/batch_tensor/k51051051372_600.jpeg'),
 (700, '../sample/batch_tensor/k51051051372_700.jpeg'),
 (800, '../sample/batch_tensor/k51051051372_800.jpeg'),
 (900, '../sample/batch_tensor/k51051051372_900.jpeg'),
 (1000, '../sample/batch_tensor/k51051051372_1000.jpeg'),
 (1100, '../sample/batch_tensor/k51051051372_1100.jpeg'),
 (1200, '../sample/batch_tensor/k51051051372_1200.jpeg'),
 (1300, '../sample/batch_tensor/k51051051372_1300.jpeg'),
 (1400, '../sample/batch_tensor/k51051051372_1400.jpeg'),
 (1500, '../sample/batch_tensor/k51051051372_1500.jpeg'),
 (1600, '../sample/batch_tensor/k51051051372_1600.jpeg'),
 (1700, '../sample/batch_tensor/k51051051372_1700.jpeg'),
 (1800, '../sample/batch_tensor/k51051051372_1800.jpeg')]

展示从第100次迭代结果到第1800次迭代结果的图像风格化的过程,特征的识别渲染由浅入深的一步一步增强,从edge,到shape。

fig, axs = plt.subplots(nrows=3, ncols=6, figsize=(18, 9));
axs_list = list(itertools.chain.from_iterable(axs))
for ind, ax in zip(range(3 * 6), axs_list):
    iter_cnt, iter_fn = k5_sorted[ind]
    iter_img = plt.imread(iter_fn)
    ax.imshow(iter_img);
    ax.set_title("k5 iter: {}".format(iter_cnt))
    ax.set_axis_off()

TensorPrismaClass可以胜任风格画渲染的使命,但问题就是速度太慢了,而且你可以查看代码类函数_features_make它对特征的筛选是引导图只使用浅层特征K_GUIDE_LAYERS = ('relu1_1', 'relu2_1', 'relu3_1', 'relu4_1', 'relu5_1'),原始图像使用K_ORG_LAYER = 'relu4_2'这样很明显无法作出一幅主题非常突出鲜明的图像,所以感觉这种方式比较定向适合特定类型的图像,普遍适应存在很大的问题

2.2 预处理图像和事后处理图像

下面换一个库日天试试(话说今天勇士赢了,好高兴😀)

kl_file = '../sample/kl.jpg'
kl_img = np.float32(tp.resize_img(PIL.Image.open(kl_file)))
PrismaHelper.show_array_ipython(kl_img)
cx6_file = '../prisma_gd/cx6.jpg'
cx6_img = np.float32(tp.resize_img(PIL.Image.open(cx6_file)))
PrismaHelper.show_array_ipython(cx6_img)

tn_img = tp.fit_guide_img(kl_file, cx6_file, resize=True, iter_n=3500)
PrismaHelper.show_array_ipython(tn_img)

风格图像展示墙中地址

如上代码所示提高迭代次数到3500次,减小K_RNLEAING_RATE值,不断调整K_TV_WEIGHT,K_ORG_WEIGHT,K_GUIDE_WEIGHT训练了好长时间才得到了上面的风格照片,但是下一章介绍的prisma方式,只需要秒级就能做出这样的效果,甚至更好。

如果迭代次数不足的话,毕竟这种方式太耗时了,我们有什么办法呢,最简单的方式整体提高rgb值,如下

# 整体提高rgb值
tnn_br = tn_img * 1.3
PrismaHelper.show_array_ipython(tnn_br)

更通用有效的方式,使用base中封装好的pil对图像的预置处理函数,进行事后图像处理,如下所示

ft = PIL.Image.fromarray(np.uint8(tn_img))
# 使用base中封装好的pil对图像的预置处理函数,进行事后图像处理
ft = tp.handle_enhance(ft, 'Contrast')
ft

# 也可以在变换的基础上再次使用CaffePrismaClass
img_np = np.float32(ft)
d_img = cp.fit_img('', resize=True, nbk='conv2/3x3', iter_n=10, img_np=img_np)
PrismaHelper.show_array_ipython(np.float32(d_img))

当然上面的方式,更优的写完是写一个pipeline clss在流水线中定义你的操作组合方式,一步完成所需所有代码的组合,并且缓存流水线中每一步操作的图像结果,等流水线中全部的操作完成后,再去缓存文件夹中去寻找你最满意的图像,github上的代码暂时没有实现,等日后完善

2.3 一个有意思的实验

如果我用prisma做出一个图像,然后我用它作为特征图像去引导新的图像生成会有什么效果呢

guide = np.float32(tp.resize_img(PIL.Image.open('../prisma_gd/106480401.jpg')))
PrismaHelper.show_array_ipython(guide)

如下所示,有些特征还是挖掘到了,哈哈

PrismaHelper.show_array_ipython(tp.fit_guide_img(s_file, gd_path, resize=True, size=640, iter_n=1500))

2.4 批量转换风格画接口的使用

批量风格画图片可以使用PrismaTensor.fit_parallel_img,如下所示

def do_fit_parallel_img(path_product, resize, size, enhance, iter_n):
    global g_doing_parallel

    """
        要在每个进程设置模块全局变量
    """
    g_doing_parallel = True

    img_path = path_product[0]
    gd_path = path_product[1]
    prisma_img = TensorPrismaClass().fit_guide_img(img_path, gd_path, resize=resize, size=size, enhance=enhance,
                                                   iter_n=iter_n)

    g_doing_parallel = False
    return path_product, prisma_img


def fit_parallel_img(img_path, gd_path, resize=False, size=480, enhance=None, iter_n=800, n_jobs=-1):
    if not isinstance(img_path, list) or not isinstance(gd_path, list):
        raise TypeError('img_path or gd_path must list for mul process handle!')

    parallel = Parallel(
        n_jobs=n_jobs, verbose=0, pre_dispatch='2*n_jobs')

    out = parallel(delayed(do_fit_parallel_img)(path_product, resize, size, enhance, iter_n) for path_product in
                   product(img_path, gd_path))

    return out

这里使用了sklearn.externals.joblib中的Parallel做并行处理,但是实际上由于tensorflow的底层的并行效率极高,所以实际并行提速是很有限的,
itertools.product计算输入图像路径和特征引导图像路径的笛卡尔积

这种方式的实现的prisma的优点就是在迭代足够多的次数引导风格可以极大的渲染作用于原始输入图像上,缺点就是速度非常慢,基本单位是以小时计算的。

针对这种实现方法还有类似的开源项目可以参考Neural-Style-Transfer它使用keras框架,BFGS计算梯度loss function最小值,这样限制了输入图像必须是必须是正方形,这里我没有再次封装,因为它的耗时单位也是无法忍受的在使用cpu的情况下,也许gpu会好点,其实也就没有实际的意义,真正的prisma肯定不是使用这些方法去实现的,下一章节开始我讲使用自己的方式实现快速prisma,在渲染效果和速度上都优于以上解决方案

如果您不想太麻烦搭建风格画的平台,可以把需要处理的图像照片发给我,特别是您家里的狗狗,猫,或者小孩子的照片(如果您家有雄性的拉不拉多且在适合交配的年龄,可以和我保持长久联系,我为我家阿布征婚,父母包办🎎)

另外针对caffe及tensorflow的一些使用问题可以查看我的其它两篇文章:
打开股票量化的黑箱(自己动手写一个印钞机) 第三章
或者关注 股票量化专题
爬取百度图片各种狗狗的图片,使用caffe训练模型分类
或者关注 机器学习专题

更多关于深度学习理论及实例请关注我将出版的一本关于深度学习方面的书籍

感谢🙏您能有耐心看到这里

如果有什么问题可以关注阿布的微信

微信号:aaaabbbuu