• 从python图像动漫化的设计和应用快速入门vue+python+深度学习+接口+部署


    今天,ofter将分享一个独家出品的应用:图像动漫化系统。原本ofter只是单纯写一个图像处理的工具,但是当我写完这个应用系统时,发现这是一个绝佳的学习案例:简单、干净又完整。此系统将前端、后端、深度学习、图像处理完美地结合到了一起,这里先列出我们可以学习到的内容:

    1. 前端Vue
    2. 接口(前端js+后端Python)
    3. 后端Flask Python
    4. 深度学习(tensorflow框架、opencv-python库)
    5. 部署(本地、云服务器部署)

    通过这个实战案例,我们完全可以入门vue、python、深度学习、接口、部署,绝对值得收藏、学习和使用。

    完整资料下载地址见文末。

    1、前端Vue

    1.1 前端结构

    1. #src
    2. ├── api/ #前端接口
    3. ├── assets/ #静态图片路径
    4. ├── components/ #组件
    5. ├── anime.vue/ #图片动漫化页面
    6. ├── sort.vue/ #动态排序页面
    7. ├── layouts/ #页面布局组件
    8. ├── router/ #路由
    9. ├── utils/ #前端接口request
    10. ├── App.vue #App主页面
    11. ├── main.js #主定义

    1.2 前端页面

    1.3 前端功能

    别看页面简单,前端主要包含了3个功能点:1)上传图片:获取图片链接;2)压缩图片;3)动漫化:通过接口传送图片给后端,并返回动漫化的结果。

    1.3.1 上传图片

    element-ui是比较简单和经典的UI组件库,我们可以看下如何实现。

    1. <el-dialog
    2. class="temp_dialog"
    3. title="上传图片"
    4. :visible.sync="uploadVisible"
    5. >
    6. <el-form
    7. ref="ruleForm"
    8. :model="form"
    9. label-width="100px"
    10. :hide-required-asterisk="true"
    11. >
    12. <el-upload
    13. class="temp_upload"
    14. ref="fileUpload"
    15. drag
    16. action="/api/images/"
    17. :on-change="importPic"
    18. :on-exceed="onFileExceed"
    19. :on-remove="onFileRemove"
    20. :auto-upload="false"
    21. :limit="1"
    22. :file-list="fileList"
    23. multiple
    24. >
    25. <i class="el-icon-upload"/>
    26. <div class="el-upload__text">
    27. 将文件拖到此处,或<em>点击上传</em>
    28. </div>
    29. <div slot="tip" class="el-upload__tip" style="color: red">*注:只能上传1张图片,1分钟内可转换成功,5分钟未转换超时失败!</div>
    30. </el-upload>
    31. </el-form>
    32. <div
    33. slot="footer"
    34. class="dialog-footer"
    35. >
    36. <el-button
    37. type="primary"
    38. @click="uploadImages()"
    39. >
    40. {{ '确认' }}
    41. </el-button>
    42. <el-button @click="uploadVisible = false">
    43. {{ '取消' }}
    44. </el-button>
    45. </div>
    46. </el-dialog>

    这里我们看下上传图片最核心的代码importPic():

    1. //:on-change="importPic"
    2. methods: {
    3. importPic (file, fileList) {
    4. const imgUrl = []
    5. let dtUrl = []
    6. let typeDis = true
    7. let that = this
    8. fileList.forEach(function (value, index) {
    9. const types = value.name.split('.')[1]
    10. const fileType = ['jpg', 'JPG', 'png', 'PNG', 'jpeg', 'JPEG'].some(
    11. item => item === types
    12. )
    13. if (fileType === false) {
    14. typeDis = false
    15. } else {
    16. // imgUrl[index] = URL.createObjectURL(value.raw) // 赋值图片的url,用于图片回显功能
    17. let reader = new FileReader()
    18. reader.readAsDataURL(value.raw)
    19. reader.onload = (e) => {
    20. let result = e.target.result
    21. let img = new Image()
    22. img.src = result.toString()
    23. console.log('*******原图片大小*******')
    24. console.log(result.length / 1024)
    25. // const temp = reader.result
    26. that.compress(img).then((value) => {
    27. dtUrl[index] = value
    28. })
    29. }
    30. }
    31. })
    32. if (typeDis === false) {
    33. this.$message.error('格式错误!请重新选择!')
    34. this.form.data = []
    35. this.$refs['fileUpload'].clearFiles()
    36. this.fileList = []
    37. this.dataUrl = []
    38. } else {
    39. this.fileList = fileList
    40. this.imageUrl = imgUrl
    41. this.dataUrl = dtUrl
    42. }
    43. }

    1.3.1.1 遍历图片

    虽然我们限制只能上传1张图片(为了防止上传太多图片而导致等待时间过长),但是我们的方法是按照上传多张图片来写的。

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

    1.3.1.2 检验文件格式

    我们必须提前筛选格式,避免后端做无谓的操作

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

    1.3.1.3 获取图片链接

    最关键的我们需要获取通过el-upload上传的图片链接,然后传递给后端。这里我们将多张图片链接组合成一个数组dtUrl[]进行接口传递,当然我们也可以组成json格式。

    1. let reader = new FileReader()
    2. reader.readAsDataURL(value.raw)
    3. reader.onload = (e) => {
    4. let result = e.target.result
    5. let img = new Image()
    6. img.src = result.toString()
    7. console.log('*******原图片大小*******')
    8. console.log(result.length / 1024)
    9. // const temp = reader.result
    10. that.compress(img).then((value) => {
    11. dtUrl[index] = value
    12. })
    13. }

    获取图片链接主要有两种方式:a)blob链接;b)base64链接。我们这里采用的是b方式,而that.compress(img)即获取压缩后图片的方法。

    1.3.2 压缩图片

    压缩图片也是重要的一环,一般图片动不动就几mb,这需要耗费后端更长的时间和资源。对于用户来说,也需要等待更长的时间,等待是丢失用户的杀手。

    压缩图片方法compress():

    1. compress (img) {
    2. const canvas = document.createElement('canvas')
    3. const ctx = canvas.getContext('2d')
    4. let that = this
    5. return new Promise(function (resolve, reject) {
    6. img.onload = setTimeout(() => {
    7. // 图片原始尺寸
    8. let originWidth = img.width
    9. let originHeight = img.height
    10. // 最大尺寸限制,可通过设置宽高来实现图片压缩程度
    11. let maxWidth = 1200
    12. let maxHeight = 1200
    13. // 目标尺寸
    14. let targetWidth = originWidth
    15. let targetHeight = originHeight
    16. // 图片尺寸超过限制
    17. if (originWidth > maxWidth || originHeight > maxHeight) {
    18. if (originWidth / originHeight > maxWidth / maxHeight) {
    19. // 更宽,按照宽度限定尺寸
    20. targetWidth = maxWidth
    21. targetHeight = Math.round(maxWidth * (originHeight / originWidth))
    22. } else {
    23. targetHeight = maxHeight
    24. targetWidth = Math.round(maxHeight * (originWidth / originHeight))
    25. }
    26. }
    27. // canvas对图片进行缩放
    28. canvas.width = targetWidth
    29. canvas.height = targetHeight
    30. // 清除画布
    31. ctx.clearRect(0, 0, targetWidth, targetHeight)
    32. // 图片压缩
    33. ctx.drawImage(img, 0, 0, targetWidth, targetHeight)
    34. // 进行最小压缩
    35. that.result = canvas.toDataURL('image/jpeg', 0.7)
    36. resolve(that.result)
    37. console.log('*******压缩后的图片大小*******')
    38. console.log(that.result.length / 1024)
    39. }, 1000)
    40. })
    41. }

    1.3.2.1 promise方法

    网上有很多类似的方法,但是你会发现经常获取不到图片,因为异步的原因,我们最好使用promise方法,通过resolve来保存图片数据。

    1. return new Promise(function (resolve, reject) {
    2. ...
    3. that.result = canvas.toDataURL('image/jpeg', 0.7)
    4. resolve(that.result)
    5. ...
    6. }

    然后,我们可以回顾importPic()中获取resolve保存的压缩图片的方法。

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

    这里提醒一句,数据需要提前定义。

    1. data () {
    2. return {
    3. result: ''
    4. }
    5. },

    1.3.3 动漫化

    当我们获取到压缩后的图片,我们就开始动漫化了。

    1. uploadImages () {
    2. this.uploadVisible = false
    3. this.loading = true
    4. this.$message.warning('图像越大可能需要的时间越长,ofter正在努力动漫化...')
    5. let dtUrl = {}
    6. this.dataUrl.forEach(function (value, index) {
    7. dtUrl[index] = value
    8. })
    9. return getImages(dtUrl).then(
    10. res => {
    11. const {code, data} = res
    12. if (code !== 200) {
    13. this.$message.error('无法从后端获取数据')
    14. this.fileList = []
    15. this.dataUrl = []
    16. this.loading = false
    17. } else {
    18. this.fileList = []
    19. this.imgList1 = data.Hayao
    20. this.imgList2 = data.Paprika
    21. this.imgList3 = data.Shinkai
    22. this.loading = false
    23. }
    24. }
    25. ).catch(() => {
    26. })
    27. },

    通过getImages()方法,我们就进入到了api接口部分,即将连接后端。

    return getImages(dtUrl).then()

    1.4 前端接口

    api:

    1. import request from '../utils/request'
    2. export function getImages (data) {
    3. return request({
    4. url: '/connect/anime',
    5. method: 'post',
    6. data: data
    7. })
    8. }

    request.js:

    1. import axios from 'axios'
    2. import { Message } from 'element-ui'
    3. const service = axios.create({
    4. baseURL: 'http://127.0.0.1:5000/', //连接后端的url
    5. timeout: 300000 // request timeout
    6. })
    7. service.interceptors.response.use(
    8. response => {
    9. const res = response.data
    10. return res
    11. },
    12. error => {
    13. console.log('err' + error) // for debug
    14. Message({
    15. message: '动漫化出现问题,请稍后刷新再试!',
    16. type: 'error',
    17. duration: 300 * 1000
    18. })
    19. return Promise.reject(error)
    20. }
    21. )
    22. export default service

    是的,这就是所有的前端接口代码,够简单吧!

    2、后端Flask-Python

    2.1 后端结构

    1. ├── checkpoint/ #生成器权重
    2. ├── dist/ #前端打包文件
    3. ├── net/ #网络生成器
    4. ├── discriminator/ #图片鉴别器
    5. ├── generator/ #图片生成器
    6. ├── response/ #返回前端接口代码
    7. ├── results/ #图片生成结果保存路径
    8. ├── tools2/ #工具函数
    9. ├── adjust_brightness.py/ #调整图片亮度
    10. ├── base64_code.py/ #base64图像格式转换
    11. ...
    12. ├── app.py #运行程序
    13. ├── README.md #使用说明
    14. ├── requirements.txt #安装库文件
    15. ├── test.py #动漫化代码
    16. ├── start.sh #启动程序脚本
    17. ├── stop.sh #停止程序脚本

    2.2 后端接口

    为了与前端接口对接,flask轻量级后端框架是个不错的选择。代码也很简单,前端通过axios传递了3个参数:

    1. url: '/connect/anime',
    2. method: 'post',
    3. data: data

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

    1. @app.route('/connect/anime', methods=['POST'])
    2. def upload_images():
    3. data = request.get_data()
    4. data = json.loads(data.decode("UTF-8"))
    5. if data is None or data == '':
    6. return response_fail(403, '未接收到任何图片')
    7. images = []
    8. for i in range(len(data)):
    9. images.append(data[str(i)])
    10. Hayao = 'checkpoint/generator_Hayao_weight'
    11. Paprika = 'checkpoint/generator_Paprika_weight'
    12. Shinkai = 'checkpoint/generator_Shinkai_weight'
    13. save_add = 'imgs/'
    14. brightness = False
    15. result_Hayao = test_anime(Hayao, save_add, images, brightness)
    16. result_Paprika = test_anime(Paprika, save_add, images, brightness)
    17. result_Shinkai = test_anime(Shinkai, save_add, images, brightness)
    18. result_arr = {
    19. 'Hayao': result_Hayao,
    20. 'Paprika': result_Paprika,
    21. 'Shinkai': result_Shinkai
    22. }
    23. 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图片,所以该网络架构很简单,我们主要看下生成器网络的实现,我们把网络架构分割成几个部分。

    1. with tf.compat.v1.variable_scope('A'):
    2. inputs = Conv2DNormLReLU(inputs, 32, 7)
    3. inputs = Conv2DNormLReLU(inputs, 64, strides=2)
    4. inputs = Conv2DNormLReLU(inputs, 64)
    5. with tf.compat.v1.variable_scope('B'):
    6. inputs = Conv2DNormLReLU(inputs, 128, strides=2)
    7. inputs = Conv2DNormLReLU(inputs, 128)
    8. with tf.compat.v1.variable_scope('C'):
    9. inputs = Conv2DNormLReLU(inputs, 128)
    10. inputs = self.InvertedRes_block(inputs, 2, 256, 1, 'r1')
    11. inputs = self.InvertedRes_block(inputs, 2, 256, 1, 'r2')
    12. inputs = self.InvertedRes_block(inputs, 2, 256, 1, 'r3')
    13. inputs = self.InvertedRes_block(inputs, 2, 256, 1, 'r4')
    14. inputs = Conv2DNormLReLU(inputs, 128)
    15. with tf.compat.v1.variable_scope('D'):
    16. inputs = Unsample(inputs, 128)
    17. inputs = Conv2DNormLReLU(inputs, 128)
    18. with tf.compat.v1.variable_scope('E'):
    19. inputs = Unsample(inputs, 64)
    20. inputs = Conv2DNormLReLU(inputs, 64)
    21. inputs = Conv2DNormLReLU(inputs, 32, 7)
    22. with tf.compat.v1.variable_scope('out_layer'):
    23. out = Conv2D(inputs, filters =3, kernel_size=1, strides=1)
    24. self.fake = tf.tanh(out)

    代码实现与网络架构一一对上了,当然tensorflow中没有那么便利的方法,而我们只需对每个方法再往下写。

    2.3.2 卷积层

    Conv2DNormLReLU方法:

    1. def Conv2DNormLReLU(inputs, filters, kernel_size=3, strides=1, padding='VALID', Use_bias = None):
    2. x = Conv2D(inputs, filters, kernel_size, strides,padding=padding, Use_bias = Use_bias)
    3. x = layer_norm(x,scope=None)
    4. return lrelu(x)
    5. def Conv2D(inputs, filters, kernel_size=3, strides=1, padding='VALID', Use_bias = None):
    6. if kernel_size == 3 and strides == 1:
    7. inputs = tf.pad(inputs, [[0, 0], [1, 1], [1, 1], [0, 0]], mode="REFLECT")
    8. if kernel_size == 7 and strides == 1:
    9. inputs = tf.pad(inputs, [[0, 0], [3, 3], [3, 3], [0, 0]], mode="REFLECT")
    10. if strides == 2:
    11. inputs = tf.pad(inputs, [[0, 0], [0, 1], [0, 1], [0, 0]], mode="REFLECT")
    12. return tf_layers.conv2d(
    13. inputs,
    14. num_outputs=filters,
    15. kernel_size=kernel_size,
    16. stride=strides,
    17. weights_initializer=tf_layers.variance_scaling_initializer(),
    18. biases_initializer= Use_bias,
    19. normalizer_fn=None,
    20. activation_fn=None,
    21. padding=padding)
    22. def layer_norm(x, scope='layer_norm') :
    23. return tf_layers.layer_norm(x, center=True, scale=True, scope=scope)
    24. def lrelu(x, alpha=0.2):
    25. return tf.nn.leaky_relu(x, alpha)

    Unsample方法:

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

    为了避免丢失图像的信息,我们尽量避免使用池化操作。

    3、深度学习-Gan网络对抗(模型训练)

    对于图像风格迁移来说,最重要的一环是Gan网络对抗。此节与我们的实际应用无关,是用于模型训练的,既然我们今天的应用主题是图像动漫化,那也稍微提一下。以生成器的损失为例:

    1. # gan
    2. c_loss, s_loss = con_sty_loss(self.vgg, self.real, self.anime_gray, self.generated)
    3. tv_loss = self.tv_weight * total_variation_loss(self.generated)
    4. t_loss = self.con_weight * c_loss + self.sty_weight * s_loss + color_loss(self.real,self.generated) * self.color_weight + tv_loss
    5. g_loss = self.g_adv_weight * generator_loss(self.gan_type, generated_logit)
    6. self.Generator_loss = t_loss + g_loss
    7. G_vars = [var for var in t_vars if 'generator' in var.name]
    8. 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获取图片的矩阵数组。

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

    输出结果:

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

    但本案例中ofter获取到的是base64图像,因此采用了另一种读取图像数据的函数:

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

    其中frame才是我们需要的图像数据,我们看下我们加载图像数据,进行预处理的方法:

    1. def load_test_data(image_path, size):
    2. # img = cv2.imread(image_path).astype(np.float32)
    3. cap = cv2.VideoCapture(image_path)
    4. ret, frame = cap.read()
    5. img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
    6. img = preprocessing(img, size)
    7. img = np.expand_dims(img, axis=0)
    8. return img
    9. def preprocessing(img, size):
    10. h, w = img.shape[:2]
    11. if h <= size[0]:
    12. h = size[0]
    13. else:
    14. x = h % 32
    15. h = h - x
    16. if w < size[1]:
    17. w = size[1]
    18. else:
    19. y = w % 32
    20. w = w - y
    21. # the cv2 resize func : dsize format is (W ,H)
    22. img = cv2.resize(img, (w, h))
    23. return img/127.5 - 1.0

    4.2 保存图片数据

    对图像数据进行保存,我们使用cv2.imwrite()方法:

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

    4.3 返回图片数据

    当我们有了图片的路径,我们将其通过接口返回给前端。

    返回base64图片链接的方法:

    1. #获取本地图片
    2. def return_img_stream(img_local_path):
    3. img_stream = ''
    4. with open(img_local_path, 'rb') as img_f:
    5. img_stream = img_f.read()
    6. img_stream = str("data:;base64," + str(base64.b64encode(img_stream).decode('utf-8')))
    7. return img_stream

    return图片链接数组:

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

    5、运行和部署

    5.1 本地后端运行(无需前端)

    如果只需要本地测试,本案例提供了args的方法,在项目路径下,执行如下:

    1. python test.py --checkpoint_dir checkpoint/generator_Hayao_weight --test_dir dataset/pics --save_dir /imgs
    2. # --checkpoint_dir 拉取生成器权重,目前有hayao/paprika/shinkai。
    3. # --test_dir 需要动漫化图片的路径
    4. # --save_dir 动漫化结果图片保存的路径

    即可将图片动漫化结果保存到指定路径。

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

    将电脑当作服务器使用,使用前需先下载Nginx。

    5.2.1 启动flask后端程序

    在项目路径下,执行如下命令:

    1. chmod u+x start.sh #授权脚本运行
    2. ./start.sh > result.log & #运行脚本
    3. #停止脚本:./stop.sh

    5.2.2 Nginx配置和运行

    配置Nginx

    1. #nginx.conf
    2. server
    3. {
    4. listen 80;
    5. server_name localhost;
    6. location / {
    7. index index.html index.htm;
    8. root XX/dist; #dist路径
    9. }
    10. # 接口
    11. location /api {
    12. proxy_pass http://127.0.0.1:5000/;
    13. }
    14. }

    启动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/

  • 相关阅读:
    【如何三行代码下载指定的股票或者基金数据到pandas中】
    Spring Boot集成支付宝电脑网站支付功能
    贝壳村到全球市场:跨境电商助力农村经济
    《工程伦理与学术道德》第二章习题
    漫游计算机系统
    Win11系统下的MindSpore环境搭建
    baichuan2(百川2)本地部署的实战方案
    单播以及多播的书写实验
    解决redis在centos上部署
    PHP8中删除数组中的重复元素-PHP8知识详解
  • 原文地址:https://blog.csdn.net/weixin_42341655/article/details/125436784