兔年了,利用AI風格化實現剪紙兔、年畫兔、煙花兔

語言: CN / TW / HK

theme: Chinese-red

我正在參加「兔了個兔」創意投稿大賽,詳情請看:「兔了個兔」創意投稿大賽

一、圖像風格化簡介

説起圖像風格化(image stylization),你可能會感覺到陌生。儘管這項技術,已經深入你的生活很久了。

專業名詞,有時候,溝通起來不方便。

記得高中時,我看見同桌帶了一個書包。很特別。我就問他這是什麼材料的。他説是PP的。我搖了搖頭。他又説,就是聚丙烯。當時我很自卑,他説了兩遍我依然不懂,感覺我知識太貧乏了。即便如此,我還是虛偽地點了點頭

多少年之後。我才瞭解到,原來聚丙烯的袋子從我出生時,我就見過了,就是下圖這樣的:

從那一刻起,我發誓,對於專業名詞,我要做到儘量不提。

但是不提,同行又以為我不專業。因此,我現在就是,説完了通俗的,再總結專業的。我會説編織袋或者蛇皮袋,文雅一點可以稱為:聚丙烯可延展包裝容器

而對於圖像風格化,其實就類似你的照片加梵高的畫作合成梵高風格的你。又或者你的照片直接轉為動漫頭像。

風格化需要有兩個參數。一個叫 content 原內容,另一個叫 style 風格參照。兩者經過模型,可以將原內容變為參照的風格。

舉個例子。如果 content 是一隻兔子,style 是上面的聚丙烯編織袋,那麼兩者融合會發生什麼呢?

那肯定是一個帶有編織袋風格的……兔子!

上面的風格融合,多少有點下里巴人。

我再來一個陽春白雪的。讓兔子和康定斯基的抽象畫做一次融合。

看着還不錯,雖然沒有抽象感,但是起碼風格是有的。

大家想象一下,在兔年來臨之際,如果兔子和年畫、剪紙、煙花這類春節元素融合起來,會是怎樣的效果呢?從技術上(不調用API接口)又該如何實現呢?

下面,跟隨我的鏡頭,我們來一探究竟(我已經探完了,不然不能有上面的圖)。

二、技術實現講解

首先説啊,咱們不調用網絡API。其次,我們是基於開源項目。

調用第三方API,會實時依附於服務提供商。一般來説,它處於自主產品鄙視鏈的底層。

我瞭解一些大牛,尤其是領導,聲稱實現了很多高級功能。結果一深究,是調用了別家的能力。這類人,把購買接口的年租費用,稱為“研發投入”。把忘記續費,歸咎於“服務器故障”。

那麼,鄙視鏈再上升一層。就是拿國外的開源項目,自己部署服務用。儘管這種行為依然不露臉。但是,這在國內已經算很棒的了。因為他們會把部署好的服務,再賣給上面的大牛領導,然後還鄙視他只能調API。

今天,我要使用的,就是從開源項目本地部署這條路。

因此,你學會了也不要驕傲,這並沒有什麼自主的知識產權。學不會也不用自卑,你還可以試試調用API。

我們選用的開源項目就是TensorFlow Hub。地址是 https://github.com/tensorflow/hub

2.1 TensorFlow Hub庫

可以説我對 TensorFlow 很熟,而且是它的鐵桿粉絲。鐵到我的暱稱“TF男孩”的TF指的就是 TensorFlow

TensorFlow 已經很簡單和人性化了。簡單到幾十行代碼,就可以實現數字識別的全流程(我都不好意寫這個教程)。

但是,它依然不滿足於此。代碼調用已經夠簡單了。但是對於訓練的樣本數據、設備性能這些條件,仍然是限制普通人去涉足的門檻。

於是,TensorFlow 就推出了一個 TensorFlow Hub 來解決上面的問題。你可以利用它訓練好的模型和權重,自己再做微調,以此適配成自己的成果。這節省了大量的人力和物力的投入。

Hub 是輪轂的意思

這讓我們很容易就聯想到“重複造輪子”這個話題。但是,它又很明確,不是輪子,是輪轂。這説明,它把最硬的部件做好了,你只需要往上放輪胎就行。到這裏,我開始感覺,雖然我很討厭有些人説一句話,又是帶中文,又是帶英文的。但是,這個 Hub ,很難翻譯,還是叫 TensorFlow Hub 更為貼切。

2.2 加載image-stylization模型

如果你打算使用 hub 預訓練好的風格化模型,自己不做任何改動的話,效果就像下面這樣。這是我搞的一個梵高《星空》風格的兔子:

而代碼其實很簡單,也就是下面幾行:

``` python

導入hub庫

import tensorflow_hub as hub

加載訓練好的風格化模型

hub_model = hub.load('https://tfhub.dev/google/magenta/arbitrary-image-stylization-v1-256/2')

將content和style傳入

stylized_image = hub_model(content_image, style_image)[0]

獲取風格化後的圖片並打印出來

tensor_to_image(stylized_image) ```

這,看起來很簡單。似乎人工智能的工作很容易幹。

事實,並非如此

我建議大家都來學習人工智能,利用成熟的代碼或工具,解決生活中遇到的問題。

但是,我不建議你着急轉行到人工智能的工作崗位中來。

因為在學習過程中,你會發現,相比於其他語言,人工智能具有更多的限制和基礎學科要求。因此,作為使用體驗者和製作開發者,會是兩個不同的心境。

隨着下面的講解,上面的問題我們會逐個碰到。

首先,上面的 hub.load('https://tfhub.dev/……') 你就加載不下來。而這個地址,正是圖像風格化的模型文件。咔,晴天霹靂啊,剛起頭就是挫折

其實TensorFlow 是谷歌的開源項目。因此他們很多項目的資源是共享的。你可以替換 tfhub.devstorage.googleapis.com/tfhub-modules 。並且在末尾加上後綴 .tar.gz

下載完成之後,解壓文件,然後指定加載路徑。其實這一步操作,也是框架的操作。它也是先下載到本地某處,然後從本地加載。

比如,我將 .tar.gz 解壓到同級目錄下。然後調用 hub.load('image-stylization-v1-256_2') 即可完成 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 HubTensorFlow 的輪轂,不是輪子,更不是自動駕駛。它的參數和返回值,都是一個 flow 流的形式。

TensorFlow 中的 flow 是什麼?這很像《道德經》裏的“道”是什麼一樣。它們只能在自己的語言體系裏能説清楚。

但是在這裏,你只需要知道調用一個 tf.constant(……) ,或者其他 tf 開頭的函數,就可把一個字符、數組或者結構體,包裝成為 tensor flow 的格式。

那麼下面,我們就要把圖片文件包裝成這個格式。

先放代碼:

``` python 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,具體如下:

python 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的信息為:

python 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的數據如下:

python 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,它已經變為了如下結構:

python 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 到底是怎樣的結構。

我們來打印一下:

python [<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 的數組。

不要和我説這些,我要把它轉為圖片看結果。

來,先上代碼:

``` python 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*255np.array(tensor, dtype=np.uint8)
  • 第3步:取出 shape=(1, 320, 512, 3) 中的那個1,也就是512×320的那張圖。
  • 第4步:通過 fromarray(img_arr) 加載圖片的數組數據,保存為圖片文件。

我敢保證,後面的事情,你只管享受就好了

源碼在這裏 https://github.com/hlwgy/image-stylization 。你可以親自運行試驗下效果。

不過,多數人還是會選擇看完文章再試。

三、一切皆可兔圖的效果

春節就要到了,新的一年是兔年(抱歉,我好像説過了)。

下面,我就把小兔子畫面和一些春節元素,做一個風格融合。

3.1 年畫兔

當然,我只説我這個年齡段的春節場景。

年畫,過年是必須貼的,在我老家(倒裝句暴露了家鄉)。而且年畫種類很豐富。

有這樣的:

還有這樣的:

它們的製作工藝不同,作用不同,貼的位置也大不相同。

我最喜歡貼的是門神。老家的門是木頭門。搞一盆漿糊,拿掃帚往門上抹。然後把年畫一放,就粘上了。紙的質量不是很好,漿糊又是濕的,漿糊融合着彩紙還會把染料擴散開來。估計現在的孩子很少再見到了。

我們看一下,可愛的小兔子遇到門神年畫,會發生怎樣的反應:

你們知道年畫是怎麼製作的嗎?在沒有印刷機的年代,年畫的製作完全靠手工。

需要先雕刻模子,其作用類似於印章。有用木頭雕刻的模具,印出來的就是木板年畫。

好了,雕刻完了。最終的模子是這樣的。

模板裏放上不同的染料,然後印在紙上,年畫就出來了。

如果兔子遇到這種木板模具,會是什麼風格呢?我有點好奇,我們看一下:

我想,這個圖,再結合3D打印機,是不是就不用工匠雕刻了。

3.2 剪紙兔

剪紙,也是過春節的一項民俗。

我老家有一種特殊的剪紙的工藝,叫“掛門箋”。當地叫“門吊子”,意思就是吊在門下的旗子。

其實這個習俗來源於南宋。那時候過年,大户人家都掛絲綢旗幟,以示喜慶。但是普通百姓買不起啊,就改成了彩紙。

跟年畫比,這個工藝現在依然活躍,農村大集還有賣的。

如果小兔子遇到剪紙,會是什麼風格呢?揭曉一下效果:

3.3 煙花兔

説起煙花,就不是哪個年齡段的專利了。現如今,即便是小孩子,也很喜歡看煙花。

如果兔子遇到煙花,會產生什麼樣的融合呢?放圖揭曉答案:

確實很美麗。

四、無限遐想

最後,我仍然意猶未盡。

我嘗試自己畫了個小兔子,和掘金的吉祥物們做了一個融合:

這……我感覺比較失落。

但是,轉念一想,其實作為抽象畫也可以,反正大多數人都看不懂。

我又融合了一張,裱起來,打上落款,好像也過得去。

上面的例子,我們是 hub.loadhttps://tfhub.dev/……image-stylization-v1-256/2 這個模型。

其實,你可以試試 https://tfhub.dev/……image-stylization-v1-256/1 這個模型,它是一個帶有發光效果的模型。

嗨,技術人,不管你是前端還是後端,如果春節沒事幹,想跨界、想突破,試試這個人工智能的項目吧。搞個小程序,給親友用一下,也挺好的。

我是掘金@TF男孩,一位講代碼過程中,多少帶點人文氣息的編程表演藝術家。