程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式師世界 >> 編程語言 >  >> 更多編程語言 >> Python

從python圖像動漫化的設計和應用快速入門vue+python+深度學習+接口+部署

編輯:Python

今天,ofter將分享一個獨家出品的應用:圖像動漫化系統。原本ofter只是單純寫一個圖像處理的工具,但是當我寫完這個應用系統時,發現這是一個絕佳的學習案例:簡單、干淨又完整。此系統將前端、後端、深度學習、圖像處理完美地結合到了一起,這裡先列出我們可以學習到的內容:

  1. 前端Vue
  2. 接口(前端js+後端Python)
  3. 後端Flask Python
  4. 深度學習(tensorflow框架、opencv-python庫)
  5. 部署(本地、雲服務器部署)

通過這個實戰案例,我們完全可以入門vue、python、深度學習、接口、部署,絕對值得收藏、學習和使用。

完整資料下載地址見文末。

1、前端Vue

1.1 前端結構

#src
├── api/ #前端接口
├── assets/ #靜態圖片路徑
├── components/ #組件
├── anime.vue/ #圖片動漫化頁面
├── sort.vue/ #動態排序頁面
├── layouts/ #頁面布局組件
├── router/ #路由
├── utils/ #前端接口request
├── App.vue #App主頁面
├── main.js #主定義

1.2 前端頁面

1.3 前端功能

別看頁面簡單,前端主要包含了3個功能點:1)上傳圖片:獲取圖片鏈接;2)壓縮圖片;3)動漫化:通過接口傳送圖片給後端,並返回動漫化的結果。

1.3.1 上傳圖片

element-ui是比較簡單和經典的UI組件庫,我們可以看下如何實現。

<el-dialog
class="temp_dialog"
title="上傳圖片"
:visible.sync="uploadVisible"
>
<el-form
ref="ruleForm"
:model="form"
label-width="100px"
:hide-required-asterisk="true"
>
<el-upload
class="temp_upload"
ref="fileUpload"
drag
action="/api/images/"
:on-change="importPic"
:on-exceed="onFileExceed"
:on-remove="onFileRemove"
:auto-upload="false"
:limit="1"
:file-list="fileList"
multiple
>
<i class="el-icon-upload"/>
<div class="el-upload__text">
將文件拖到此處,或<em>點擊上傳</em>
</div>
<div slot="tip" class="el-upload__tip" >*注:只能上傳1張圖片,1分鐘內可轉換成功,5分鐘未轉換超時失敗!</div>
</el-upload>
</el-form>
<div
slot="footer"
class="dialog-footer"
>
<el-button
type="primary"
@click="uploadImages()"
>
{
{ '確認' }}
</el-button>
<el-button @click="uploadVisible = false">
{
{ '取消' }}
</el-button>
</div>
</el-dialog>

這裡我們看下上傳圖片最核心的代碼importPic():

//:on-change="importPic"
methods: {
importPic (file, fileList) {
const imgUrl = []
let dtUrl = []
let typeDis = true
let that = this
fileList.forEach(function (value, index) {
const types = value.name.split('.')[1]
const fileType = ['jpg', 'JPG', 'png', 'PNG', 'jpeg', 'JPEG'].some(
item => item === types
)
if (fileType === false) {
typeDis = false
} else {
// imgUrl[index] = URL.createObjectURL(value.raw) // 賦值圖片的url,用於圖片回顯功能
let reader = new FileReader()
reader.readAsDataURL(value.raw)
reader.onload = (e) => {
let result = e.target.result
let img = new Image()
img.src = result.toString()
console.log('*******原圖片大小*******')
console.log(result.length / 1024)
// const temp = reader.result
that.compress(img).then((value) => {
dtUrl[index] = value
})
}
}
})
if (typeDis === false) {
this.$message.error('格式錯誤!請重新選擇!')
this.form.data = []
this.$refs['fileUpload'].clearFiles()
this.fileList = []
this.dataUrl = []
} else {
this.fileList = fileList
this.imageUrl = imgUrl
this.dataUrl = dtUrl
}
}

1.3.1.1 遍歷圖片

雖然我們限制只能上傳1張圖片(為了防止上傳太多圖片而導致等待時間過長),但是我們的方法是按照上傳多張圖片來寫的。

fileList.forEach(function (value, index) {}

1.3.1.2 檢驗文件格式

我們必須提前篩選格式,避免後端做無謂的操作

const types = value.name.split('.')[1]
const fileType = ['jpg', 'JPG', 'png', 'PNG', 'jpeg', 'JPEG'].some(
item => item === types
)
if (fileType === false) {
typeDis = false
} else {}

1.3.1.3 獲取圖片鏈接

最關鍵的我們需要獲取通過el-upload上傳的圖片鏈接,然後傳遞給後端。這裡我們將多張圖片鏈接組合成一個數組dtUrl[]進行接口傳遞,當然我們也可以組成json格式。

 let reader = new FileReader()
reader.readAsDataURL(value.raw)
reader.onload = (e) => {
let result = e.target.result
let img = new Image()
img.src = result.toString()
console.log('*******原圖片大小*******')
console.log(result.length / 1024)
// const temp = reader.result
that.compress(img).then((value) => {
dtUrl[index] = value
})
}

獲取圖片鏈接主要有兩種方式:a)blob鏈接;b)base64鏈接。我們這裡采用的是b方式,而that.compress(img)即獲取壓縮後圖片的方法。

1.3.2 壓縮圖片

壓縮圖片也是重要的一環,一般圖片動不動就幾mb,這需要耗費後端更長的時間和資源。對於用戶來說,也需要等待更長的時間,等待是丟失用戶的殺手。

壓縮圖片方法compress():

compress (img) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
let that = this
return new Promise(function (resolve, reject) {
img.onload = setTimeout(() => {
// 圖片原始尺寸
let originWidth = img.width
let originHeight = img.height
// 最大尺寸限制,可通過設置寬高來實現圖片壓縮程度
let maxWidth = 1200
let maxHeight = 1200
// 目標尺寸
let targetWidth = originWidth
let targetHeight = originHeight
// 圖片尺寸超過限制
if (originWidth > maxWidth || originHeight > maxHeight) {
if (originWidth / originHeight > maxWidth / maxHeight) {
// 更寬,按照寬度限定尺寸
targetWidth = maxWidth
targetHeight = Math.round(maxWidth * (originHeight / originWidth))
} else {
targetHeight = maxHeight
targetWidth = Math.round(maxHeight * (originWidth / originHeight))
}
}
// canvas對圖片進行縮放
canvas.width = targetWidth
canvas.height = targetHeight
// 清除畫布
ctx.clearRect(0, 0, targetWidth, targetHeight)
// 圖片壓縮
ctx.drawImage(img, 0, 0, targetWidth, targetHeight)
// 進行最小壓縮
that.result = canvas.toDataURL('image/jpeg', 0.7)
resolve(that.result)
console.log('*******壓縮後的圖片大小*******')
console.log(that.result.length / 1024)
}, 1000)
})
}

1.3.2.1 promise方法

網上有很多類似的方法,但是你會發現經常獲取不到圖片,因為異步的原因,我們最好使用promise方法,通過resolve來保存圖片數據。

return new Promise(function (resolve, reject) {
...
that.result = canvas.toDataURL('image/jpeg', 0.7)
resolve(that.result)
...
}

然後,我們可以回顧importPic()中獲取resolve保存的壓縮圖片的方法。

that.compress(img).then((value) => {
dtUrl[index] = value
})

這裡提醒一句,數據需要提前定義。

data () {
return {
result: ''
}
},

1.3.3 動漫化

當我們獲取到壓縮後的圖片,我們就開始動漫化了。

uploadImages () {
this.uploadVisible = false
this.loading = true
this.$message.warning('圖像越大可能需要的時間越長,ofter正在努力動漫化...')
let dtUrl = {}
this.dataUrl.forEach(function (value, index) {
dtUrl[index] = value
})
return getImages(dtUrl).then(
res => {
const {code, data} = res
if (code !== 200) {
this.$message.error('無法從後端獲取數據')
this.fileList = []
this.dataUrl = []
this.loading = false
} else {
this.fileList = []
this.imgList1 = data.Hayao
this.imgList2 = data.Paprika
this.imgList3 = data.Shinkai
this.loading = false
}
}
).catch(() => {
})
},

通過getImages()方法,我們就進入到了api接口部分,即將連接後端。

return getImages(dtUrl).then()

1.4 前端接口

api:

import request from '../utils/request'
export function getImages (data) {
return request({
url: '/connect/anime',
method: 'post',
data: data
})
}

request.js:

import axios from 'axios'
import { Message } from 'element-ui'
const service = axios.create({
baseURL: 'http://127.0.0.1:5000/', //連接後端的url
timeout: 300000 // request timeout
})
service.interceptors.response.use(
response => {
const res = response.data
return res
},
error => {
console.log('err' + error) // for debug
Message({
message: '動漫化出現問題,請稍後刷新再試!',
type: 'error',
duration: 300 * 1000
})
return Promise.reject(error)
}
)
export default service

是的,這就是所有的前端接口代碼,夠簡單吧!

2、後端Flask-Python

2.1 後端結構

├── checkpoint/ #生成器權重
├── dist/ #前端打包文件
├── net/ #網絡生成器
├── discriminator/ #圖片鑒別器
├── generator/ #圖片生成器
├── response/ #返回前端接口代碼
├── results/ #圖片生成結果保存路徑
├── tools2/ #工具函數
├── adjust_brightness.py/ #調整圖片亮度
├── base64_code.py/ #base64圖像格式轉換
...
├── app.py #運行程序
├── README.md #使用說明
├── requirements.txt #安裝庫文件
├── test.py #動漫化代碼
├── start.sh #啟動程序腳本
├── stop.sh #停止程序腳本

2.2 後端接口

為了與前端接口對接,flask輕量級後端框架是個不錯的選擇。代碼也很簡單,前端通過axios傳遞了3個參數:

 url: '/connect/anime',
method: 'post',
data: data

那麼在flask-python文件中,app.py:

@app.route('/connect/anime', methods=['POST'])
def upload_images():
data = request.get_data()
data = json.loads(data.decode("UTF-8"))
if data is None or data == '':
return response_fail(403, '未接收到任何圖片')
images = []
for i in range(len(data)):
images.append(data[str(i)])
Hayao = 'checkpoint/generator_Hayao_weight'
Paprika = 'checkpoint/generator_Paprika_weight'
Shinkai = 'checkpoint/generator_Shinkai_weight'
save_add = 'imgs/'
brightness = False
result_Hayao = test_anime(Hayao, save_add, images, brightness)
result_Paprika = test_anime(Paprika, save_add, images, brightness)
result_Shinkai = test_anime(Shinkai, save_add, images, brightness)
result_arr = {
'Hayao': result_Hayao,
'Paprika': result_Paprika,
'Shinkai': result_Shinkai
}
return response_success('success', result_arr)

其中獲取Post的數據有3種方式:a)params;b)form.data;c)data。我們這裡用了方式c:

data = request.get_data()

2.3 執行動漫化

我們稍微介紹下AnimeGanV2的網絡架構和實現。如果您並未了解過神經網絡,建議可以閱讀下ofter用最簡單的方式寫的關於卷積神經網絡的文章:

[5機器學習]計算機視覺的世界-卷積神經網絡(CNNs) - 知乎

2.3.1 卷積神經網絡

因為辨別器網絡只是為了識別圖片是不是Cartoon圖片,所以該網絡架構很簡單,我們主要看下生成器網絡的實現,我們把網絡架構分割成幾個部分。

with tf.compat.v1.variable_scope('A'):
inputs = Conv2DNormLReLU(inputs, 32, 7)
inputs = Conv2DNormLReLU(inputs, 64, strides=2)
inputs = Conv2DNormLReLU(inputs, 64)
with tf.compat.v1.variable_scope('B'):
inputs = Conv2DNormLReLU(inputs, 128, strides=2)
inputs = Conv2DNormLReLU(inputs, 128)
with tf.compat.v1.variable_scope('C'):
inputs = Conv2DNormLReLU(inputs, 128)
inputs = self.InvertedRes_block(inputs, 2, 256, 1, 'r1')
inputs = self.InvertedRes_block(inputs, 2, 256, 1, 'r2')
inputs = self.InvertedRes_block(inputs, 2, 256, 1, 'r3')
inputs = self.InvertedRes_block(inputs, 2, 256, 1, 'r4')
inputs = Conv2DNormLReLU(inputs, 128)
with tf.compat.v1.variable_scope('D'):
inputs = Unsample(inputs, 128)
inputs = Conv2DNormLReLU(inputs, 128)
with tf.compat.v1.variable_scope('E'):
inputs = Unsample(inputs, 64)
inputs = Conv2DNormLReLU(inputs, 64)
inputs = Conv2DNormLReLU(inputs, 32, 7)
with tf.compat.v1.variable_scope('out_layer'):
out = Conv2D(inputs, filters =3, kernel_size=1, strides=1)
self.fake = tf.tanh(out)

代碼實現與網絡架構一一對上了,當然tensorflow中沒有那麼便利的方法,而我們只需對每個方法再往下寫。

2.3.2 卷積層

Conv2DNormLReLU方法:

def Conv2DNormLReLU(inputs, filters, kernel_size=3, strides=1, padding='VALID', Use_bias = None):
x = Conv2D(inputs, filters, kernel_size, strides,padding=padding, Use_bias = Use_bias)
x = layer_norm(x,scope=None)
return lrelu(x)
def Conv2D(inputs, filters, kernel_size=3, strides=1, padding='VALID', Use_bias = None):
if kernel_size == 3 and strides == 1:
inputs = tf.pad(inputs, [[0, 0], [1, 1], [1, 1], [0, 0]], mode="REFLECT")
if kernel_size == 7 and strides == 1:
inputs = tf.pad(inputs, [[0, 0], [3, 3], [3, 3], [0, 0]], mode="REFLECT")
if strides == 2:
inputs = tf.pad(inputs, [[0, 0], [0, 1], [0, 1], [0, 0]], mode="REFLECT")
return tf_layers.conv2d(
inputs,
num_outputs=filters,
kernel_size=kernel_size,
stride=strides,
weights_initializer=tf_layers.variance_scaling_initializer(),
biases_initializer= Use_bias,
normalizer_fn=None,
activation_fn=None,
padding=padding)
def layer_norm(x, scope='layer_norm') :
return tf_layers.layer_norm(x, center=True, scale=True, scope=scope)
def lrelu(x, alpha=0.2):
return tf.nn.leaky_relu(x, alpha)

Unsample方法:

def Unsample(inputs, filters, kernel_size=3):
new_H, new_W = 2 * tf.shape(inputs)[1], 2 * tf.shape(inputs)[2]
inputs = tf.compat.v1.image.resize_images(inputs, [new_H, new_W])
return Conv2DNormLReLU(filters=filters, kernel_size=kernel_size, inputs=inputs)

為了避免丟失圖像的信息,我們盡量避免使用池化操作。

3、深度學習-Gan網絡對抗(模型訓練)

對於圖像風格遷移來說,最重要的一環是Gan網絡對抗。此節與我們的實際應用無關,是用於模型訓練的,既然我們今天的應用主題是圖像動漫化,那也稍微提一下。以生成器的損失為例:

# gan
c_loss, s_loss = con_sty_loss(self.vgg, self.real, self.anime_gray, self.generated)
tv_loss = self.tv_weight * total_variation_loss(self.generated)
t_loss = self.con_weight * c_loss + self.sty_weight * s_loss + color_loss(self.real,self.generated) * self.color_weight + tv_loss
g_loss = self.g_adv_weight * generator_loss(self.gan_type, generated_logit)
self.Generator_loss = t_loss + g_loss
G_vars = [var for var in t_vars if 'generator' in var.name]
self.G_optim = tf.train.AdamOptimizer(self.g_lr , beta1=0.5, beta2=0.999).minimize(self.Generator_loss, var_list=G_vars)

這裡我們在訓練過程中,使用AdamOptimizer優化算法使我們的生成器網絡損失最小化。

4、Opencv庫的使用

opencv是一個比較常用的圖像處理庫。

4.1 讀取和預處理圖片數據

我們先看個本地圖片的例子,我們知道計算機處理圖片其實是對矩陣數組的處理,那麼我們可以用cv2.imread獲取圖片的矩陣數組。

import cv2
image_path = 'D:/XXX.png'
img = cv2.imread(image_path).astype(np.float32)
print(img)

輸出結果:

[[255,255,255],[],[],...]

但本案例中ofter獲取到的是base64圖像,因此采用了另一種讀取圖像數據的函數:

cap = cv2.VideoCapture(image_path)
ret, frame = cap.read()

其中frame才是我們需要的圖像數據,我們看下我們加載圖像數據,進行預處理的方法:

def load_test_data(image_path, size):
# img = cv2.imread(image_path).astype(np.float32)
cap = cv2.VideoCapture(image_path)
ret, frame = cap.read()
img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
img = preprocessing(img, size)
img = np.expand_dims(img, axis=0)
return img
def preprocessing(img, size):
h, w = img.shape[:2]
if h <= size[0]:
h = size[0]
else:
x = h % 32
h = h - x
if w < size[1]:
w = size[1]
else:
y = w % 32
w = w - y
# the cv2 resize func : dsize format is (W ,H)
img = cv2.resize(img, (w, h))
return img/127.5 - 1.0

4.2 保存圖片數據

對圖像數據進行保存,我們使用cv2.imwrite()方法:

cv2.imwrite(path, cv2.cvtColor(images, cv2.COLOR_BGR2RGB))

4.3 返回圖片數據

當我們有了圖片的路徑,我們將其通過接口返回給前端。

返回base64圖片鏈接的方法:

#獲取本地圖片
def return_img_stream(img_local_path):
img_stream = ''
with open(img_local_path, 'rb') as img_f:
img_stream = img_f.read()
img_stream = str("data:;base64," + str(base64.b64encode(img_stream).decode('utf-8')))
return img_stream

return圖片鏈接數組:

result_arr=[]
result_arr.append(return_img_stream('./'+image_name))
return result_arr

5、運行和部署

5.1 本地後端運行(無需前端)

如果只需要本地測試,本案例提供了args的方法,在項目路徑下,執行如下:

python test.py --checkpoint_dir checkpoint/generator_Hayao_weight --test_dir dataset/pics --save_dir /imgs
# --checkpoint_dir 拉取生成器權重,目前有hayao/paprika/shinkai。
# --test_dir 需要動漫化圖片的路徑
# --save_dir 動漫化結果圖片保存的路徑

即可將圖片動漫化結果保存到指定路徑。

5.2 本地部署運行(需要前端)

將電腦當作服務器使用,使用前需先下載Nginx。

5.2.1 啟動flask後端程序

在項目路徑下,執行如下命令:

chmod u+x start.sh #授權腳本運行
./start.sh > result.log & #運行腳本
#停止腳本:./stop.sh

5.2.2 Nginx配置和運行

配置Nginx

#nginx.conf
server
{
listen 80;
server_name localhost;
location / {
index index.html index.htm;
root XX/dist; #dist路徑
}
# 接口
location /api {
proxy_pass http://127.0.0.1:5000/;
}
}

啟動Nginx

service nginx start #重啟: service nginx restart

5.2.3 浏覽器運行

在浏覽器上輸入localhost,即可運行。

5.3 雲服務器部署

雲服務器部署方式與本地類似,體驗地址如下:

http://139.159.233.237/#/anime

6、完整資料下載

https://www.jdmm.top/file/2707572/


  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式師世界 All Rights Reserved