2023兔年剪纸兔子 利用AI风格化实现剪纸兔
一、图像风格化简介
说起图像风格化(image stylization),你可能会感觉到陌生。尽管这项技术,已经深入你的生活很久了。
专业名词,有时候,沟通起来不方便。
记得高中时,我看见同桌带了一个书包。很特别。我就问他这是什么材料的。他说是PP的。我摇了摇头。他又说,就是聚丙烯。当时我很自卑,他说了两遍我依然不懂,感觉我知识太贫乏了。即便如此,我还是虚伪地点了点头。
多少年之后。我才了解到,原来聚丙烯的袋子从我出生时,我就见过了,就是下图这样的:
从那一刻起,我发誓,对于专业名词,我要做到尽量不提。
但是不提,同行又以为我不专业。因此,我现在就是,说完了通俗的,再总结专业的。我会说编织袋或者蛇皮袋,文雅一点可以称为:聚丙烯可延展包装容器。
而对于图像风格化,其实就类似你的照片加梵高的画作合成梵高风格的你。又或者你的照片直接转为动漫头像。
风格化需要有两个参数。一个叫 content 原内容,另一个叫 style 风格参照。两者经过模型,可以将原内容变为参照的风格。
举个例子。如果 content 是一只兔子,style 是上面的聚丙烯编织袋,那么两者融合会发生什么呢?
那肯定是一个带有编织袋风格的……兔子!
上面的风格融合,多少有点下里巴人。
我再来一个阳春白雪的。让兔子和康定斯基的抽象画做一次融合。
看着还不错,虽然没有抽象感,但是起码风格是有的。
大家想象一下,在兔年来临之际,如果兔子和年画、剪纸、烟花这类春节元素融合起来,会是怎样的效果呢?从技术上(不调用API接口)又该如何实现呢?
下面,跟随我的镜头,我们来一探究竟(我已经探完了,不然不能有上面的图)。
二、技术实现讲解首先说啊,咱们不调用网络API。其次,我们是基于开源项目。
调用第三方API,会实时依附于服务提供商。一般来说,它处于自主产品鄙视链的底层。
我了解一些大牛,尤其是领导,声称实现了很多高级功能。结果一深究,是调用了别家的能力。这类人,把购买接口的年租费用,称为“研发投入”。把忘记续费,归咎于“服务器故障”。
那么,鄙视链再上升一层。就是拿国外的开源项目,自己部署服务用。尽管这种行为依然不露脸。但是,这在国内已经算很棒的了。因为他们会把部署好的服务,再卖给上面的大牛领导,然后还鄙视他只能调API。
今天,我要使用的,就是从开源项目本地部署这条路。
因此,你学会了也不要骄傲,这并没有什么自主的知识产权。学不会也不用自卑,你还可以试试调用API。
我们选用的开源项目就是TensorFlow Hub。地址是 github/TensorFlow/hub 。
2.1 TensorFlow Hub库可以说我对 TensorFlow 很熟,而且是它的铁杆粉丝。铁到我的昵称“TF男孩”的TF指的就是 TensorFlow 。
TensorFlow 已经很简单和人性化了。简单到几十行代码,就可以实现数字识别的全流程(我都不好意写这个教程)。
但是,它依然不满足于此。代码调用已经够简单了。但是对于训练的样本数据、设备性能这些条件,仍然是限制普通人去涉足的门槛。
于是,TensorFlow 就推出了一个 TensorFlow Hub 来解决上面的问题。你可以利用它训练好的模型和权重,自己再做微调,以此适配成自己的成果。这节省了大量的人力和物力的投入。
Hub 是轮毂的意思。
这让我们很容易就联想到“重复造轮子”这个话题。但是,它又很明确,不是轮子,是轮毂。这说明,它把最硬的部件做好了,你只需要往上放轮胎就行。到这里,我开始感觉,虽然我很讨厌有些人说一句话,又是带中文,又是带英文的。但是,这个 Hub ,很难翻译,还是叫 TensorFlow Hub 更为贴切。
2.2 加载image-stylization模型如果你打算使用 hub 预训练好的风格化模型,自己不做任何改动的话,效果就像下面这样。这是我搞的一个梵高《星空》风格的兔子:
而代码其实很简单,也就是下面几行:
# 导入hub库
import tensorflow_hub as hub
# 加载训练好的风格化模型
hub_model = hub.load('tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/1')
# 将content和style传入
stylized_image = hub_model(content_image, style_image)[0]
# 获取风格化后的图片并打印出来
tensor_to_image(stylized_image)
这,看起来很简单。似乎人工智能的工作很容易干。
事实,并非如此。
我建议大家都来学习人工智能,利用成熟的代码或工具,解决生活中遇到的问题。
但是,我不建议你着急转行到人工智能的工作岗位中来。
因为在学习过程中,你会发现,相比于其他语言,人工智能具有更多的网络限制和基础学科要求。因此,作为使用体验者和制作开发者,会是两个不同的心境。
随着下面的讲解,上面的问题我们会逐个碰到。
首先,上面的 hub.load('tfhub.dev/……') 你就加载不下来。而这个地址,正是图像风格化的模型文件。咔,晴天霹雳啊,刚起头就是挫折。
其实 TensorFlow 是谷歌的开源项目。因此他们很多项目的资源是共享的。你可以替换 tfhub.dev 为 storage.googleapis/tfhub-modules 。并且在末尾加上后缀 .tar.gz 。
下载完成之后,解压文件,然后指定加载路径。其实这一步操作,也是框架的操作。它也是先下载到本地某处,然后从本地加载。
比如,我将 .tar.gz 解压到同级目录下。然后调用 hub.load('image-stylization-v1-256_1') 即可完成 hub 的加载。
这就是我说的网络限制。
相比较而言,Java 或者 Php 这类情况也会有,但是频率没有这么高。
下面,我们继续。还会有其他惊喜。
2.3 输入图片转为tensor格式hub_model = hub.load(……) 是加载模型。我们是加载了图像风格化的模型。
赋值的名称随便起就行,上面我起的名是 hub_model 。之所以说这句话,是因为我发现有些人感觉改个名字,代码就会运行不起来。其实,变一变,更有利于理解代码。而项目运行不起来,向上帝祈祷不起作用,是需要看报错信息的。
如果完全不更改 hub 预置模型的话,再一行代码就完工了。
这行代码就是 stylized_images = hub_model(content_image, style_image) 。
这行代码是把内容图片 content_image 和风格参照图片 style_image 传给加载好的模型 hub_model 。模型就会输出风格化后的结果图片stylized_images。
哇哦,瞬间感觉自己能卖API了。
但是,这个图片参数的格式,却并没有那么简单。
前面说了,TensorFlow Hub 是 TensorFlow 的轮毂,不是轮子,更不是自动驾驶。它的参数和返回值,都是一个 flow 流的形式。
TensorFlow 中的 flow 是什么?这很像《道德经》里的“道”是什么一样。它们只能在自己的语言体系里能说清楚。
但是在这里,你只需要知道调用一个 tf.constant(……) ,或者其他 tf 开头的函数,就可把一个字符、数组或者结构体,包装成为 tensor flow 的格式。
那么下面,我们就要把图片文件包装成这个格式。
先放代码:
import tensorflow as tf
# 根据路径加载图片,并缩小至512像素,转为tensor
max_dim = 512
img = tf.io.read_file(path_to_img)
img = tf.image.decode_image(img, channels=3)
img = tf.image.convert_image_dtype(img, tf.float32)
shape = tf.cast(tf.shape(img)[:-1], tf.float32)
long_dim = max(shape)
scale = max_dim / long_dim
new_shape = tf.cast(shape * scale, tf.int32)
img = tf.image.resize(img, new_shape)
img = img[tf.newaxis, :]
tf_img = tf.constant(img)
首先,模型在训练和预测时,是有固定尺寸的。比如,宽高统一是512像素。
然后,对于用户的输入,我们是不能限制的。比如,用户输入一个高度为863像素的图,这时我们不能让用户裁剪好了再上传。应该是用户上传后,我们来处理。
最后,要搞成tensorflow需要的格式。
上面的代码片段,把这三条都搞定了。
read_file 从路径读入文件。然后通过 decode_image 将文件解析成数组。
这时,如果打印img,具体如下:
shape=(434, 650, 3), dtype=uint8
array([[[219, 238, 245],
...,
[219, 238, 245]],
[[219, 238, 245],
...,
[219, 238, 245]]])
shape=(434, 650, 3) 说明这是一个三维数组,看数据这是一张650×434像素且具有RGB三个通道的图片。其中的array是具体像素的数值,在某个颜色通道内,255表示纯白,0表示纯黑。
接着 convert_image_dtype(img, tf.float32) 把img转成了float形式。
此时,img的信息为:
shape=(434, 650, 3), dtype=float32
array([[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]],
[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]]])
为啥要把int转为float呢?初学者往往会有这样的疑问。
因为他们发现,只要是计算,就要求搞成float类型。就算明明是9个分类,也不能用1、2、3、4来表示,也要转为一堆的小数点。
今天这个图片的像素,也是如此,255个色值多好辨认,为什么非要转为看不懂的小数呢?
别拦着我,我今天非要要解释一下。
这并不是算法没事找事,假装高级。其实,这是为了更好地对应到很多基础学科的知识。
比如,我在《详解激活函数》中讲过很多激活函数。激活函数是决定算法如何做决策的,可以说是算法的指导思想。
你看几个就知道了。不管是sigmoid还是tanh,它的值都是以0或者1为边界的。
也就是说你的模型做数字识别的时候,计算的结果并不是1、2、3、4,而是0到1之间的小数。最后,算法根据概率得出属于哪个分类。哎,你看概率的表示也是0到1之间的数。
除此之外,计算机的二进制也是0或者1。芯片的计算需要精度,整数类型不如小数精确。
各种原因,导致还是浮点型的小数更适合算法的计算。甚至,人工智能的体系中,还具有float64类型,也就是64位的小数。
变为小数之后,后面就是将图片数组做缩放。根据数据的shape,找到最长的边。然后缩放到512像素以内。
这就到了 resize(img, new_shape) 这行代码。
到这一步时,img的数据如下:
shape=(341, 512, 3), dtype=float32
array([[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]],
[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]]])
原来的 (434, 650, 3) 图片被重新定义成了 (341, 512, 3) 。依然是3通道的色彩,但是长宽尺寸经过计算,最大已经不超过512像素了。
为什么做缩放?除了模型要求,还要防止用户有可能上传一张1亿像素的图片,这时你的服务器就冒烟了。
(434, 650, 3) 代表的是一张图。但是纵观所有算法模型,不管是 model.fit(train_ds) 训练阶段,还是 model.predict(tf_imgs) 预测阶段。就没有处理单张图片的代码逻辑,全都是批量处理。
它不能处理单张图片的结构,你别说它不人性化,不用跟他杠。兄弟,模型要的只是一个数组结构,它并不关心里面图片的数量。一张图片可以是 ["a.png"] 这种形式。
说到这里,我又忍不住想谈谈关于接口设计的话题了。
我给业务方提供了一个算法接口能力,就是查询一张图上存在的特定目标信息。我也是返回多个结果的结构。尽管样本中只有一个目标。业务方非要返回一个。从长远来讲,谁也不敢保证以后场景中只有一个目标。我必须要如实返回,有一个返回一个,有两个返回两个,你可以只取第一个。但是,结构肯定是要支持多个的。
从成本和风险权衡的角度,从列表中取一条数据的成本,要远小于程序出错或者失灵的风险。但是业务方比较坚持返回一个就行。
后来,他们让我把图片的base64返回值带上 data:image/jpeg;base64, ,便于前端直接展示。那一刻,我就明白了,跟他们较这个真,是我冲动了。
而对于 TensorFlow 的要求,你必须要包装成批量的形式。我认为这很规范。
这句代码 img = img[tf.newaxis, :] 就是将维度上升一层。可以将 1 变为 [1] ,也可以将 [[1],[2]] 变为 [[[1],[2]]] 。
此时再打印img,它已经变为了如下结构:
shape=(1, 341, 512, 3), dtype=float32
array([[[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]],
[[0.8588236, 0.9333334, 0.9607844],
...,
[0.8588236, 0.9333334, 0.9607844]]]])
shape=(1, 341, 512, 3) 表示有1张512×341的彩图。那么,这个结构它也可以承载100张这样的图,那时就是shape=(100, 341, 512, 3)。这就做到了,以不变应万变。
最后一步的 tf_img = tf.constant(img) ,作用是通过 tf.constant 把图片数据,包装成 TensorFlow 需要的格式。
这个格式,就可以传给hub_model去处理了。
经过 stylized_images = hub_model(tf_img_content, tf_img_style) 这行代码的处理。它会将处理结果放到 stylized_images 中。你马上就可以看到融合结果了。
不过,好像也没有那么简单。这个结果的呈现,实际上是图片到tensor格式的逆向过程。
我们下面就来处理它。
2.4 tensor格式结果转为图片上一步经过 hub_model 转化,我们获取到了 stylized_images 。这是我们辛苦那么久的产物。你是否会好奇 stylized_images 到底是怎样的结构。
我们来打印一下:
[<tf.Tensor: shape=(1, 320, 512, 3), dtype=float32, numpy=
array([[[[0.31562978, 0.47748038, 0.7790847 ],
...,
[0.7430198 , 0.733053 , 0.6921962 ]],
[[0.76158 , 0.6912774 , 0.5468565 ],
...,
[0.69527835, 0.70888966, 0.6492392 ]]]], dtype=float32)>]
厉害了,它是一个 shape=(1, 320, 512, 3) 形状的 tf.Tensor 的数组。
不要和我说这些,我要把它转为图片看结果。
来,先上代码:
import numpy as np
import PIL.Image
tensor = stylized_images[0]
tensor = tensor*255
tensor_arr = np.array(tensor, dtype=np.uint8)
img_arr = tensor_arr[0]
img = PIL.Image.fromarray(img_arr)
img.save(n_path)
相信有了上面图片转 tensor 的过程,这个反着转化的过程,你很容易就能理解。
第1步:取结果中的第一个 stylized_images[0] ,那是 shape=(1, 320, 512, 3) 。
第2步:小数转为255色值的整数数组 tensor*255、 np.array(tensor, dtype=np.uint8) 。
第3步:取出 shape=(1, 320, 512, 3) 中的那个1,也就是512×320的那张图。
第4步:通过 fromarray(img_arr) 加载图片的数组数据,保存为图片文件。
我敢保证,后面的事情,你只管享受就好了。
源码在这里 github/hlwgy/image-stylization 。你可以亲自运行试验下效果。
不过,多数人还是会选择看完文章再试。
三、一切皆可兔图的效果春节就要到了,新的一年是兔年(抱歉,我好像说过了)。
下面,我就把兔子和一些春节元素,做一个风格融合。
3.1 年画兔当然,我只说我这个年龄段的春节场景。
年画,过年是必须贴的在我老家(倒装句暴露了家乡)。而且年画种类很丰富。
有这样的:
还有这样的:
它们的制作工艺不同,作用不同,贴的位置也大不相同。
我最喜欢贴的是门神。老家的门是木头门。搞一盆浆糊,拿扫帚往门上抹。然后把年画一放,就粘上了。纸的质量不是很好,浆糊又是湿的,浆糊融合着彩纸还会把染料扩散开来。估计现在的孩子很少再见到了。
我们看一下,可爱的小兔子遇到门神年画,会发生怎样的反应:
你们知道年画是怎么制作的吗?在没有印刷机的年代,年画的制作完全靠手工。
需要先雕刻模子,其作用类似于印章。有用木头雕刻的模具,印出来的就是木板年画。
好了,雕刻完了。最终的模子是这样的。
模板里放上不同的染料,然后印在纸上,年画就出来了。
如果兔子遇到这种木板模具,会是什么风格呢?我有点好奇,我们看一下:
我想,这个图,再结合3D打印机,是不是就不用工匠雕刻了。
3.2 剪纸兔剪纸,也是过春节的一项活动。
我老家有一种特殊的剪纸的工艺,叫“挂门笺”。当地叫“门吊子”,意思就是吊在门下的旗子。
其实这个习俗来源于南宋。那时候过年,大户人家都挂丝绸旗帜,以示喜庆。但是普通百姓买不起啊,就改成了彩纸。
跟年画比,这个工艺现在依然活跃,农村大集还有卖的。
如果小兔子遇到剪纸,会是什么风格呢?揭晓一下效果:
3.3 烟花兔
说起烟花,这就不是哪个年龄段的专利了。现如今,即便是小孩子,也很喜欢看烟花。
如果兔子遇到烟花,会产生什么样的融合呢?放图揭晓答案:
确实很美丽。
四、无限遐想上面的例子,我们是 hub.load 了 tfhub.dev/……image-stylization-v1-256/2 这个模型。
其实,你可以试试 tfhub.dev/……image-stylization-v1-256/1 这个模型,它是一个带有发光效果的模型。
嗨,技术人,不管前端还是后端,如果春节没事干,想跨界、想突破,试试这段人工智能的代码吧。搞个小程序,给亲友用一下,也挺好的。
我是ITF男孩,一位讲代码过程中,多少带点人文气息的编程表演艺术家。
,
免责声明:本文仅代表文章作者的个人观点,与本站无关。其原创性、真实性以及文中陈述文字和内容未经本站证实,对本文以及其中全部或者部分内容文字的真实性、完整性和原创性本站不作任何保证或承诺,请读者仅作参考,并自行核实相关内容。文章投诉邮箱:anhduc.ph@yahoo.com