【飞桨开发者说】李思佑,昆明理工大学信息与计算科学大四本科生;2018年和2019年两次获得全国大学生数学建模比赛国家二等奖;2020年美国数学建模比赛获M奖。指导老师:昆明理工大学理学院朱志宁想画出独一无二的动漫头像吗?不会画也没关系,深度学习的生成对抗网络(GAN)可以帮你搞定!只需要输入一些随机数,就可以让卷积神经网络为你画出精致并且独一无二的动漫头像!本文将通过趣味解读的方式,基于飞桨深度学习框架以DCGAN为例深入浅出地带您了解GAN的魔法世界!

效果展示

整体效果

下图完全是由机器创造出来的二次元人物头像,细看有些图片足以以假乱真。

横向对比

每次生成一组shape为[1,72]的随机数,更改其中某个数值,依次生成20组随机数,作为生成网络的输入,得到横向对比图片,观察GAN带来的神奇效果,如下所示。改变发色深浅改变头发颜色

知识补充

GAN原理简介

论文地址:https://arxiv.org/abs/1406.2661生成对抗网络(Generative Adversarial Network ,简称GAN)是由一个生成网络与一个判别网络组成。生成网络从潜在空间(latent space)中随机采样作为输入,其输出结果需要尽量模仿训练集中的真实样本。判别网络的输入为真实样本或生成网络的输出,其目的是将生成网络的输出从真实样本中尽可能分辨出来,而生成网络则要尽可能地欺骗判别网络。两个网络相互对抗、不断调整参数,其目的是让判别网络无法判断输入是真实样本还是生成网络的输出内容。生成对抗网络常用于生成以假乱真的图片 。此外,该方法还被用于生成视频、三维物体模型等。以下简单展示了GAN的训练过程:

DCGAN介绍

论文地址:https://arxiv.org/abs/1511.06434DCGAN是深层卷积网络与 GAN 的结合,其基本原理与 GAN 相同,只是将生成网络和判别网络用卷积网络(CNN)替代。为了提高生成样本的质量和网络的收敛速度,论文中的 DCGAN 在网络结构上进行了一些改进:取消pooling 层、加入batch normalization、使用全卷积网络、去掉了FC层。激活函数:在生成网络(G)最后一层使用Tanh函数,其余层采用 ReLu 函数 ; 判别网络(D)中都采用LeakyReLu。但是在实际过程中,很难得到这个完美的平衡点,关于GAN的收敛理论还在持续不断的研究中。

实现过程

本项目由Chainer项目Chainerで顔イラストの自動生成改写为PaddlePaddle项目。

本项目对原项目进行了如下几个方面的改进:

  1. 将Adam优化器beta1参数设置为0.8,具体请参考Adam: A Method for Stochastic Optimization,以进一步缓解梯度消失/爆炸问题。
  2. 将BatchNorm批归一化中momentum参数设置为0.5,调参后网络训练过程加快。
  3. 将判别网络(D)的激活函数由elu改为leakyrelu,并将alpha参数设置为0.2。elu与leakyrelu相比效果并不明显,这里改用计算复杂度更低的leakyrelu
  4. 在判别网络(D)中增加Dropout层,并将dropout_prob设置为0.4,避免过拟合和梯度消失/爆炸问题
  5. 将生成网络(G)中的第一层全连接层改为基本残差模块,加快收敛速度并使网络学习到更丰富的特征。

改进后,网络收敛速度明显加快,原项目训练时间需要300个epoch,训练超过10小时,改进后仅需要90个epoch,训练时间3个小时左右,同时生成的动漫头像在细节上层次更加丰富,生成的动漫头像风格更加多样。

开发环境

PaddlePaddle1.7.1、Python3.7、Scikit-image等以及线上平台AI Studio

数据集

数据集通过参考网络上的爬虫代码结合openCV工具进行头像截取,爬取著名的动漫图库网站的http://safebooru.donmai.us/和http://konachan.net/约6万张图片。项目所需数据集[二次元人物头像]已经上传并公开到AI Studio。

损失函数:

实现过程:(AI Studio用Jupyter实现)

1. 安装缺失库、解压数据集定义数据预处理

!pip install scikit-image!unzip data/data17962/二次元人物头像.zip -d data/!mkdir ./work/Output!mkdir ./work/Generate 

2. 定义数据预处理-DataReader

import osimport cv2import numpy as npimport paddle.dataset as datasetfrom skimage import io,color,transformimport matplotlib.pyplot as pltimport mathimport timeimport paddleimport paddle.fluid as fluidimport siximg_dim = 96'''准备数据,定义Reader()'''PATH = 'data/faces/'TEST = 'data/faces/'class DataGenerater:def __init__(self):'''初始化'''        self.datalist = os.listdir(PATH)        self.testlist = os.listdir(TEST)def load(self, image):'''读取图片'''        img = io.imread(image)        img = transform.resize(img,(img_dim,img_dim))        img = img.transpose()        img = img.astype('float32')return imgdef create_train_reader(self):'''给dataset定义reader'''def reader():for img in self.datalist:#print(img)try:                    i = self.load(PATH + img)yield i.astype('float32')except Exception as e:                    print(e)return readerdef create_test_reader(self,):'''给test定义reader'''def reader():for img in self.datalist:#print(img)try:                    i = self.load(PATH + img)yield i.astype('float32')except Exception as e:                    print(e)return readerdef train(batch_sizes = 32):    reader = DataGenerater().create_train_reader()return readerdef test():    reader = DataGenerater().create_test_reader()return reader

3. 定义网络功能模块包括卷积池化组、BatchNorm层、全连接层、反卷积层、BatchNorm卷积层。

use_cudnn = Trueuse_gpu = Truen = 0def bn(x, name=None, act=None,momentum=0.5):return fluid.layers.batch_norm(        x,        param_attr=name + '1',# 指定权重参数属性的对象        bias_attr=name + '2',# 指定偏置的属性的对象        moving_mean_name=name + '3',# moving_mean的名称        moving_variance_name=name + '4',# moving_variance的名称        name=name,        act=act,        momentum=momentum,    )###卷积池化组def conv(x, num_filters,name=None, act=None):return fluid.nets.simple_img_conv_pool(        input=x,        filter_size=5,        num_filters=num_filters,        pool_size=2,# 池化窗口大小        pool_stride=2,# 池化滑动步长        param_attr=name + 'w',        bias_attr=name + 'b',        use_cudnn=use_cudnn,        act=act    )###全连接层def fc(x, num_filters, name=None, act=None):return fluid.layers.fc(        input=x,        size=num_filters,        act=act,        param_attr=name + 'w',        bias_attr=name + 'b'    )###反卷积层def deconv(x, num_filters, name=None, filter_size=5, stride=2, dilation=1, padding=2, output_size=None, act=None):return fluid.layers.conv2d_transpose(        input=x,        param_attr=name + 'w',        bias_attr=name + 'b',        num_filters=num_filters,# 滤波器数量        output_size=output_size,# 输出图片大小        filter_size=filter_size,# 滤波器大小        stride=stride,# 步长        dilation=dilation,# 膨胀比例大小        padding=padding,        use_cudnn=use_cudnn,# 是否使用cudnn内核        act=act# 激活函数    )###BatchNorm卷积层def conv_bn_layer(input,                  ch_out,                  filter_size,                  stride,                  padding,                  act=None,                  groups=64,                  name=None):    tmp = fluid.layers.conv2d(        input=input,        filter_size=filter_size,        num_filters=ch_out,        stride=stride,        padding=padding,        act=None,        bias_attr=name + '_conv_b',        param_attr=name + '_conv_w',    )return fluid.layers.batch_norm(        input=tmp,        act=act,        param_attr=name + '_bn_1',# 指定权重参数属性的对象        bias_attr=name + '_bn_2',# 指定偏置的属性的对象        moving_mean_name=name + '_bn_3',# moving_mean的名称        moving_variance_name=name + '_bn_4',# moving_variance的名称        name=name + '_bn_',        momentum=0.5,    )

4. 定义基本残差模块本文采用的残差单元如上图所示,由两个输出通道数相同的3x3卷积组成。

def shortcut(input, ch_in, ch_out, stride,name):if ch_in != ch_out:return conv_bn_layer(input, ch_out, 1, stride, 0, None,name=name)else:return inputdef basicblock(input, ch_in, ch_out, stride,name,act):    tmp = conv_bn_layer(input, ch_out, 3, stride, 1, name=name + '_1_',act=act)    tmp = conv_bn_layer(tmp, ch_out, 3, 1, 1, act=None, name=name + '_2_')    short = shortcut(input, ch_in, ch_out, stride,name=name)return fluid.layers.elementwise_add(x=tmp, y=short, act='relu')def layer_warp(block_func, input, ch_in, ch_out, count, stride,name,act='relu'):    tmp = block_func(input, ch_in, ch_out, stride,name=name + '1',act=act)for i in range(1, count):        tmp = block_func(tmp, ch_out, ch_out, 1,name=name + str(i + 1),act=act)return tmp

5. 判别网络

  • 将BatchNorm批归一化中momentum参数设置为0.5
  • 将判别网络(D)激活函数由elu改为leaky_relu,并将alpha参数设置为0.2
  • 在判别器(D)中增加Dropout层,并将dropout_prob设置为0.4

输入为大小96x96的RGB三通道图片。输出结果经过一层全连接层最后输出shape为[batch_size,2]的Tensor。

###判别器def D(x):# (96 + 2 * 1 - 4) / 2 + 1 = 48    x = conv_bn_layer(x, 64, 4, 2, 1, act=None, name='conv_bn_1')    x = fluid.layers.leaky_relu(x,alpha=0.2,name='leaky_relu_1')    x = fluid.layers.dropout(x,0.4,name='dropout1')# (48 + 2 * 1 - 4) / 2 + 1 = 24    x = conv_bn_layer(x, 128, 4, 2, 1, act=None, name='conv_bn_2')    x = fluid.layers.leaky_relu(x,alpha=0.2,name='leaky_relu_2')    x = fluid.layers.dropout(x,0.4,name='dropout2')# (24 + 2 * 1 - 4) / 2 + 1 = 12    x = conv_bn_layer(x, 256, 4, 2, 1, act=None, name='conv_bn_3')    x = fluid.layers.leaky_relu(x,alpha=0.2,name='leaky_relu_3')    x = fluid.layers.dropout(x,0.4,name='dropout3')# (12 + 2 * 1 - 4) / 2 + 1 = 6    x = conv_bn_layer(x, 512, 4, 2, 1, act=None, name='conv_bn_4')    x = fluid.layers.leaky_relu(x,alpha=0.2,name='leaky_relu_4')    x = fluid.layers.dropout(x,0.4,name='dropout4')    x = fluid.layers.reshape(x,shape=[-1, 512 * 6 * 6])    x = fc(x, 2, name='fc1')return x

6. 生成网络将BatchNorm批归一化中momentum参数设置为0.5。将生成器(G)中的第一层全连接层改为基本残差模块。输入Tensor的Shape为[batch_size,72],其中每个数值大小都是0~1之间的float32随机数。输出为大小96x96RGB三通道图片。

###生成器def G(x):    #x = fc(x,6 * 6 * 2,name='g_fc1',act='relu')    #x = bn(x, name='g_bn_1', act='relu',momentum=0.5)    x = fluid.layers.reshape(x, shape=[-1, 2, 6, 6])    x = layer_warp(basicblock, x, 2, 256, 1, 1, name='g_res1', act='relu')    # 2 * (6 - 1) - 2 * 1  + 4 = 12    x = deconv(x, num_filters=256, filter_size=4, stride=2, padding=1, name='g_deconv_1')    x = bn(x, name='g_bn_2', act='relu',momentum=0.5)    # 2 * (12 - 1) - 2 * 1  + 4 = 24    x = deconv(x, num_filters=128, filter_size=4, stride=2, padding=1, name='g_deconv_2')    x = bn(x, name='g_bn_3', act='relu',momentum=0.5)    # 2 * (24 - 1) - 2 * 1  + 4 = 48    x = deconv(x, num_filters=64, filter_size=4, stride=2, padding=1, name='g_deconv_3')    x = bn(x, name='g_bn_4', act='relu',momentum=0.5)    # 2 * (48 - 1) - 2 * 1  + 4 = 96    x = deconv(x, num_filters=3, filter_size=4, stride=2, padding=1, name='g_deconv_4',act='relu')    return x

损失函数选用softmax_with_cross_entropy,公式如下:7. 训练网络设置的超参数为:

  • 学习率:2e-4
  • Epoch: 90
  • Mini-Batch:100
  • 单个随机张量大小:72
import IPython.display as displayimport warningswarnings.filterwarnings('ignore')img_dim = 96LEARENING_RATE = 2e-4SHOWNUM = 12epoch = 90output = "work/Output/"batch_size = 100G_DIMENSION = 72d_program = fluid.Program()dg_program = fluid.Program()###定义判别网络program# program_guard()接口配合with语句将with block中的算子和变量添加指定的全局主程序(main_program)和启动程序(start_progrom)with fluid.program_guard(d_program):# 输入图片大小为28*28    img = fluid.layers.data(name='img', shape=[None,3,img_dim,img_dim], dtype='float32')# 标签shape=1    label = fluid.layers.data(name='label', shape=[None,1], dtype='int64')    d_logit = D(img)    d_loss = loss(x=d_logit, label=label)###定义生成网络programwith fluid.program_guard(dg_program):    noise = fluid.layers.data(name='noise', shape=[None,G_DIMENSION], dtype='float32')#label = np.ones(shape=[batch_size, G_DIMENSION], dtype='int64')# 噪声数据作为输入得到生成照片    g_img = G(x=noise)    g_program = dg_program.clone()    g_program_test = dg_program.clone(for_test=True)# 判断生成图片为真实样本的概率    dg_logit = D(g_img)# 计算生成图片被判别为真实样本的loss    dg_loss = loss(        x=dg_logit,        label=fluid.layers.fill_constant_batch_size_like(input=noise, dtype='int64', shape=[-1,1], value=1)    )###优化函数opt = fluid.optimizer.Adam(learning_rate=LEARENING_RATE,beta1=0.5)opt.minimize(loss=d_loss)parameters = [p.name for p in g_program.global_block().all_parameters()]opt.minimize(loss=dg_loss, parameter_list=parameters)train_reader = paddle.batch(    paddle.reader.shuffle(        reader=train(), buf_size=50000    ),    batch_size=batch_size)test_reader = paddle.batch(    paddle.reader.shuffle(        reader=test(), buf_size=10000    ),    batch_size=10)###执行器if use_gpu:    exe = fluid.Executor(fluid.CUDAPlace(0))else:    exe = fluid.Executor(fluid.CPUPlace())start_program = fluid.default_startup_program()exe.run(start_program)#加载模型#fluid.io.load_persistables(exe,'work/Model/D/',d_program)#fluid.io.load_persistables(exe,'work/Model/G/',dg_program)###训练过程t_time = 0losses = [[], []]# 判别器迭代次数NUM_TRAIN_TIME_OF_DG = 2# 最终生成的噪声数据const_n = np.random.uniform(    low=0.0, high=1.0,    size=[batch_size, G_DIMENSION]).astype('float32')test_const_n = np.random.uniform(    low=0.0, high=1.0,    size=[100, G_DIMENSION]).astype('float32')#plt.ion()now = 0for pass_id in range(epoch):    fluid.io.save_persistables(exe, 'work/Model/G', dg_program)    fluid.io.save_persistables(exe, 'work/Model/D', d_program)for batch_id, data in enumerate(train_reader()):  # enumerate()函数将一个可遍历的数据对象组合成一个序列列表if len(data) != batch_size:continue# 生成训练过程的噪声数据        noise_data = np.random.uniform(            low=0.0, high=1.0,            size=[batch_size, G_DIMENSION]).astype('float32')# 真实图片        real_image = np.array(data)# 真实标签        real_labels = np.ones(shape=[batch_size,1], dtype='int64')# real_labels = real_labels * 10# 虚假标签        fake_labels = np.zeros(shape=[batch_size,1], dtype='int64')        s_time = time.time()#print(np.max(noise_data))# 虚假图片        generated_image = exe.run(g_program,                                  feed={'noise': noise_data},                                  fetch_list=[g_img])[0]###训练判别器# D函数判断虚假图片为假的loss        d_loss_1 = exe.run(d_program,                           feed={'img': generated_image,'label': fake_labels,                           },                           fetch_list=[d_loss])[0][0]# D函数判断真实图片为真的loss        d_loss_2 = exe.run(d_program,                           feed={'img': real_image,'label': real_labels,                           },                           fetch_list=[d_loss])[0][0]        d_loss_n = d_loss_1 + d_loss_2        losses[0].append(d_loss_n)###训练生成器for _ in six.moves.xrange(NUM_TRAIN_TIME_OF_DG):            noise_data = np.random.uniform(  # uniform()方法从一个均匀分布[low,high)中随机采样                low=0.0, high=1.0,                size=[batch_size, G_DIMENSION]).astype('float32')            dg_loss_n = exe.run(dg_program,                                feed={'noise': noise_data},                                fetch_list=[dg_loss])[0][0]        losses[1].append(dg_loss_n)        t_time += (time.time() - s_time)if batch_id % 500 == 0:if not os.path.exists(output):                os.makedirs(output)# 每轮的生成结果            generated_image = exe.run(g_program_test, feed={'noise': test_const_n}, fetch_list=[g_img])[0]#print(generated_image[1])            imgs = []            plt.figure(figsize=(15,15))try:for i in range(100):                    image = generated_image[i].transpose()                    plt.subplot(10, 10, i + 1)                    plt.imshow(image)                    plt.axis('off')                    plt.xticks([])                    plt.yticks([])                    plt.subplots_adjust(wspace=0.1, hspace=0.1)# plt.subplots_adjust(wspace=0.1,hspace=0.1)                msg = 'Epoch ID={0} Batch ID={1} \n D-Loss={2} G-Loss={3}'.format(pass_id + 92, batch_id, d_loss_n, dg_loss_n)#print(msg)                plt.suptitle(msg,fontsize=20)                plt.draw()#if batch_id % 10000 == 0:                plt.savefig('{}/{:04d}_{:04d}.png'.format(output, pass_id + 92, batch_id),bbox_inches='tight')                plt.pause(0.01)                display.clear_output(wait=True)#plt.pause(0.01)except IOError:                print(IOError)#plt.ioff()plt.close()plt.figure(figsize=(15, 6))x = np.arange(len(losses[0]))plt.title('Loss')plt.xlabel('Number of Batch')plt.plot(x,np.array(losses[0]),'r-',label='D Loss')plt.plot(x,np.array(losses[1]),'b-',label='G Loss')plt.legend()plt.savefig('work/Train Process')plt.show()

得到的损失变化曲线为:

项目总结

简单介绍了一下DCGAN的原理,通过对原项目的改进和优化,一步一步依次对生成网络和判别网络以及训练过程进行介绍。通过横向对比某个输入元素对生成图片的影响。平均更改其中某个数值,依次生成20组随机数,输入生成器,得到横向对比图片,得到GAN神奇的过渡。DCGAN生成的二次元头像仔细看有些图片确实是足以以假乱真的,通过DCGAN了解到GAN的强大“魔力”。不足之处是生成的图片分辨率比较低(96X96),在以后的项目我会通过改进网络使得生成的二次元头像有更高的分辨率和更丰富的细节。个人AI Studio主页:https://aistudio.baidu.com/aistudio/personalcenter/thirdview/56447如在使用过程中有问题,可加入飞桨官方QQ群进行交流:703252161。如果您想详细了解更多飞桨的相关内容,请参阅以下文档。飞桨生成对抗网络项目地址:GitHub: https://github.com/PaddlePaddle/models/tree/release/1.8/PaddleCV/ganGitee:https://gitee.com/paddlepaddle/models/tree/develop/PaddleCV/gan官网地址:https://www.paddlepaddle.org.cn飞桨开源框架项目地址:GitHub:https://github.com/PaddlePaddle/PaddleGitee: https://gitee.com/paddlepaddle/Paddle

END

zip直链生成网站_手把手教你如何用飞桨自动生成二次元人物头像相关推荐

  1. 手把手教你如何用飞桨自动生成二次元人物头像

    [飞桨开发者说]李思佑,昆明理工大学信息与计算科学大四本科生:2018年和2019年两次获得全国大学生数学建模比赛国家二等奖:2020年美国数学建模比赛获M奖. 指导老师:昆明理工大学理学院朱志宁 想 ...

  2. python远程桌面控制_手把手教你如何用Pycharm2020.1.1配置远程连接的详细步骤

    配置说明 使用Pycharm 2020.1.1 professional 专业版.(据说只有专业版可以远程连接)如果不是专业的伙伴,可以用校园邮箱注册一个专业版,免费的哦! 步骤 1. 设置Conne ...

  3. 手把手教你在百度飞桨云平台下运行PPYOLO-E,训练COCO数据集

    百度ai云平台:飞桨AI Studio - 人工智能学习实训社区 (baidu.com) 首先感谢百度提供这样一个云平台 .ps 每天会送8个算力也就是每天可以使用8个小时V100-32G 完成任务还 ...

  4. python开发一个自己的技术网站_手把手教你写网站:Python WEB开发技术实战

    摘要:本文详细介绍了Python WEB开发的基础入门.以一个博客站点的开发为例讲解了基于Django框架开发WEB站点的全过程.通过本文的学习可以快速掌握基于Django的Python WEB的开发 ...

  5. 提取点位属性文本_手把手教你如何用Python爬取网站文本信息

    提取网页源代码--Requests 工具包 在我们提取网络信息之前,我们必须将网页的源代码进行提取,Requests工具包现在可以说是最好用和最普及的静态网页爬虫工具,它是由大神Kenneth Rei ...

  6. python爬取网页文本_手把手教你如何用Python爬取网站文本信息

    提取网页源代码--Requests 工具包 在我们提取网络信息之前,我们必须将网页的源代码进行提取,Requests工具包现在可以说是最好用和最普及的静态网页爬虫工具,它是由大神Kenneth Rei ...

  7. 如何关闭苹果手机自动扣费_手把手教你如何取消包月自动扣费服务

    昨天看某视频网站一个电影发现时候VIP会员才可以看,就看了看如何成为会员,有一下几个选项 选项一:包一个月25元 选项二:连续包月15元 选项三:三个月68元 选项四:全年148元 明眼人一看就知道那 ...

  8. 手把手教你如何用python制作自动翻译程序

    思路:"输入文本"  →  "翻译"  →  "得到译文" Ps:这个思路好像有点那啥哈哈哈哈(是个正常人都知道的思路) 好的,现在让我们直 ...

  9. 手把手教你如何用selenium实现自动抽奖工具(穿越火线王者夺宝活动为例)

    需要的环境: chromedriver驱动器来驱动chrome, 模拟浏览器行为 python3.x以上的解释器 chromedriver这个到博客上搜索一下,会有详细教程 分析: 穿越火线王者夺宝网 ...

最新文章

  1. python版mapreduce题目实现寻找共同好友
  2. 关于ZendOptimizer和wamp的phpmyadmin冲突问题
  3. mysql驱动不支持批处理_ADO.NET 中可以发送包含多个SQL语句的批处理脚本到SQL Server,但是用MySQL的ODBC驱动不行...
  4. 大神TP_英雄联盟:男枪瞬秒大龙,佐伊遍地TP,新版本BUG谁来监管?
  5. 唐人街探案3观后感:大四学生的看法
  6. 每日一题(43)—— 数组越界
  7. Atitit 理解参数 目录 1. 参数 1 1.1. 意义 1 1.2. 形式参数 实际参数 1 1.3. 默认参数 vs 必须参数 2 1.4. 位置参数,那么这个命名关键字参数 2 1.5. I
  8. LaTeX数学符号大全
  9. Python应用开发——爬取网页图片
  10. 通过谷歌Google轻松拥有自己的站内搜索代码
  11. openwrt-mt7628 wds配置
  12. 如何使用CK-S610-A01擦写RI-TRP-DR2B-40玻璃管标签的数据信息
  13. 计算机改名字sql2008不能登录,Win7电脑修改计算机名称后SQL2008数据库无法登录提示无法连接到load怎么处理...
  14. 求一个数的最小素因子外加快速分解质因子
  15. Python安装libsvm
  16. JAVA程序的逻辑控制及输入输出
  17. 我的世界服务器修改速度,我的世界速度提升指令是什么_我的世界速度提升指令介绍_玩游戏网...
  18. java dya01 HelloWorld与环境变量
  19. arduino 下16进制转2进制
  20. 走向单体地狱(一):Maven详解

热门文章

  1. HbuildX中使用类似VSCode配色
  2. python字母频率统计
  3. C++ Crow web框架使用;升级cmake ;pthread、boost、asio 报错
  4. 九度oj-1163-素数
  5. 神奇的sqrt函数以及高精度记时函数
  6. Win10出现0x8000000b错误如何解决?
  7. Excel VBA自动化办公:选择Excel文件合并订单数据生成订单汇总表、生成发货单并导出pdf文件、自动统计业绩生成业绩表
  8. 深度linux触摸板失灵,deepin下触摸板无法使用
  9. systemctl enable与systemctl start的区别
  10. OpenCV的本地相机抓图和本地视频取帧