【机器学习】基于AnimeGAN的漫画人脸生成系统

链接

https://github.com/WanYongyi/machine-learning?tab=readme-ov-file

一、课题描述

1.1 课题背景

1.1.1 论文背景

动画是一种被广泛应用于广告、电影、儿童教育的日常生活中常见的艺术形式。目前,动画的制作主要依靠手工实现。然而,手工制作动画是非常费力的,需要大量的艺术技巧。对于动画艺术家来说,创作高质量的动画作品需要考虑线条、纹理、颜色、阴影等在内的要素,导致创作作品的难度和耗时。因此,能够将真实世界的照片自动转换成高质量的动画风格图像的自动化技术是非常有价值的。

目前,基于深度学习的图像到图像翻译已经取得了很好的效果。近年来,基于学习风格迁移方法[1]-[2]已经成为常见的图像到图像翻译方法。生成对抗网络(GAN)也广泛应用于风格迁移,虽然取得了一定的成功,但也存在着许多明显的问题,主要包括以下几点:1)生成的图像没有明显的动画风格纹理;2)生成的图像失去了原照片的内容;3)大量的网络参数需要较多的内存容量。

1.1.2 论文创新点

Chen等人的工作[3]主要解决了上述背景中存在的三点问题,论文的创新点主要有以下三点:

第一,提出了一种新的轻量级GAN,称为AnimeGAN,它可以将真实世界的照片快速转换为高质量的动画图像。提出的AnimeGAN是一种轻量级的生成对抗模型,具有较少的网络参数,并引入Gram矩阵[4]来获取更生动的风格图像。需要一组照片和一组动画图像进行训练。为了产生高质量的结果,同时使训练数据容易获取,采用了未配对数据进行训练,这意味着训练集中的照片和动画图像在内容上是不相关的。

第二,为了进一步提高生成图像的动画视觉效果,提出了三种新的简单有效的损失函数。提出的损失函数是灰度风格损失、颜色重建损失和灰度对抗损失。在生成网络中,灰度风格损失和颜色重建损失使生成的图像具有更明显的动画风格,并保留了照片的颜色。鉴别器网络中的灰度对抗损失使生成的图像具有鲜艳的色彩。在判别器网络中,则使用了Chen等人文章[5]中提出的促进边缘的对抗损失来保持清晰的边缘。

第三,为了让生成的图像具有原始照片的内容,引入预训练的VGG19[6]作为感知网络,获得生成图像和原始照片的深度感知特征的L1损失。在AnimeGAN开始训练之前对生成器进行初始化训练,使AnimeGAN的训练更稳定。

1.2 目的

机器学习是计算机及其相关学科的一门重要的学科课程,也是人工智能重要学科分支。旨在通过阅读文章,复现模型,研究和应用扩展来完成指定的功能,强化机器学习设计能力,培养独立查找资料能力、自学能力和运用所学知识解决新问题的能力,提高综合素质,并通过课程设计进一步加强对所学知识的理解,进一步提高调用、设计、开发机器学习系统模块以解决实际问题的能力。

1.3 技术要求

主要包括以下几个方面:

  1. 编程语言:需要掌握至少一门编程语言,如Python、MATLAB或Java等,以便实现机器学习算法和数据处理。

  2. 数据处理技能:需要具备数据清洗、数据预处理和数据转换等方面的技能,以便准备训练和测试数据集。

  3. 机器学习算法:需要了解和掌握各种机器学习算法,如分类算法、聚类算法、回归算法等,并能够根据实际需求选择合适的算法。

  4. 模型评估:需要掌握模型评估的方法和技术,如交叉验证、准确率、召回率、F1值等,以便对机器学习模型进行性能评估和优化。

  5. 工具和库:需要了解和使用一些常用的机器学习和数据处理工具和库,如Scikit-learn、TensorFlow、Keras、Pandas等,以便提高数据处理和机器学习算法实现的效率和精度。

  6. 理论知识和数学基础:需要具备一定的理论知识和数学基础,如统计学、线性代数、概率论等,以便更好地理解和应用机器学习算法。

1.4 课题完成要求

1.4.1 主要任务

  1. 阅读文献,了解文献的实验方法、算法、创新点;

  2. 复现代码,掌握代码的核心部分:损失函数L(G,D)的计算方法;

  3. 获取数据集,通过爬虫技术在视觉中国等平台获取数据集用于测试算法;

  4. 设计UI,开发一个前端界面,可以展示我们的训练效果,开放用户上传自行转换。

1.4.2 具体要求

  1. 进行课题的前期准备,要求理解课题内容和要求,并开展资料收集和分析、设计工作;

  2. 在课题总体设计基础上,编写程序的各个子功能模块,调试程序并运行;

  3. 撰写课程设计报告,要求报告结构合理,格式规范。

二、系统总体设计

2.1 包

模型的包

1、argparse:

作用:用于解析命令行参数。

解释:

argparse.ArgumentParser():创建一个参数解析器。

parser.add_argument():定义脚本接受的命令行参数。

parser.parse_args():解析命令行参数并返回一个包含参数的命名空间。

2、torch:

作用:PyTorch深度学习框架。

解释:

torch.backends.cudnn.enabled:禁用cuDNN加速。

torch.backends.cudnn.benchmark:使用cuDNN的性能优化。

torch.backends.cudnn.deterministic:确保cuDNN的结果是确定性的。

3、cv2 (OpenCV):

作用:用于图像处理。

解释:

cv2.imread():读取图像。

cv2.cvtColor():进行颜色空间转换。

cv2.resize():调整图像大小。

cv2.imwrite():保存图像。

4、numpy:

作用:用于处理数值数组。

解释:

np.float32:32位浮点数。

np.uint8:8位无符号整数。

数组操作,如 astype()、clip()。

5、os:

作用:提供与操作系统交互的功能。

解释:

os.makedirs():创建目录。

os.path.join():连接路径。

6、model (custom module):

作用:包含自定义的模型。

解释:

Generator:生成器模型,可能是一个GAN的生成器。

前端的包

Django项目配置:

python==3.9

django==3.2.1

mysqlclient == 2.2.0

mysql: mysql-5.7.41-winx64

模型运行配置:

torch==2.1.1+cu121

numpy==1.26.0

opencv-python==4.8.1.78

2.2 算法、原理

2.2.1 AnimeGAN架构

架构由两个卷积网络组成,一个是生成器G,用于将显示场景的照片转换成动画图像;另一个是鉴别器D,用于区分图像是来自真实目标域还是生成器产出的输出。以下图是论文中对架构的阐释。

image

生成器(G)中,所有框上的数字表示通道数量,SUM表示元素总和;鉴别器(D)中,K为核的大小,C为特征映射个数,S为卷积层的步幅,Inst Norm表示实例归一化层。

关于上图的一些解释:生成器可以看做一个对称的编码器-解码器网络,由标准卷积、深度可分离卷积、倒残差块(IRBs)、上下采样模块组成。在生成器中,具有1×1卷积核的最后一个卷积层不用归一化,后面是tanh非线性激活函数。

论文中还提到了其小模块,如下图所示:

image

2.2.2 损失函数

生成器损失函数主要分为4个部分,不同的损失有不同的权重系数,公式有六个,如下图:
image
image
image
image
image

公式(1)中,对抗损失(adv)是生成器G中影响动画转换过程的对抗性损失,内容损失(con)是帮助生成的图像保留输入照片内容的内容丢失,灰度风格损失(gra)是使生成的图像在纹理和线条上具有清晰的动漫风格,颜色重建损失(col)是使生成的图像具有原照片的颜色。[7]

如上图公式(2)(3),对于内容丢失和灰度风格丢失,使用预先训练好的VGG作为感知网络,提取图像的高级语义特征;公式(4),关于颜色的提取和转换,将RGB通道转换为YUV通道,然后对不同通道使用不同的损失计算方法;公式(5),是最终生成器的损失函数;公式(6)则是鉴别器使用的损失函数,除了引入CartoonGAN提出的促进边缘的对抗损失,还使用了一种新的灰度对抗损失,防止生成的图像以灰度图像形式显示。

2.3 流程图

2.3.1 算法流程图

image

2.3.2 爬虫流程图

image

2.3.3 系统用例图

image

2.3.4 Django框架图

image

2.4 各模块属性及申明

2.4.1 爬虫

1.from urllib import request:导入Python内置库urllib的request模块,用于发送http请求和获取响应数据;

2.import os:导入Python内置库os,用于提供与操作系统交互的功能;

3.import re:导入Python内置库re,用于支持正则表达式的操作;

4.def get_path(classname, subclassname, filename):定义一个名为get_path的函数,用于生成图像文件的保存路径,接受类别名称、子类名称和文件名作为参数,返回一个表示图像文件路径的字符串;

5.url:通过拼接字符串的方式构建表示要访问的网址;

6.header:定义一个HTTP请求头,模拟浏览器User-Agent信息,以便发送请求时能够得到正确的响应;

2.4.2 模型

1.设置PyTorch的cuDNN行为:


torch.backends.cudnn.enabled = False

torch.backends.cudnn.benchmark = False

torch.backends.cudnn.deterministic = True

torch.backends.cudnn.enabled:禁用cuDNN加速。

torch.backends.cudnn.benchmark:使用cuDNN的性能优化。

torch.backends.cudnn.deterministic:确保cuDNN的结果是确定性的。

2.加载图像的函数 load_image:


def load_image(image_path, x32=False):

​    # ... (函数的实现)

load_image 函数加载图像,进行颜色空间转换,并可选择是否调整图像大小。

参数:

image_path:图像文件路径。

x32:布尔值,表示是否将图像大小调整为32的倍数。

3.测试函数 test


def test(args):

​    # ... (函数的实现)

test 函数进行漫画风格转换的测试。

参数:

args:包含命令行参数的命名空间。

主要步骤:

加载生成器模型。

遍历输入目录中的图像。

对每个图像进行处理并保存结果。

4.命令行参数的解析:


parser = argparse.ArgumentParser()

parser.add_argument('--checkpoint', type=str, default='path/to/your/model.pth')

parser.add_argument('--input_dir', type=str, default='path/to/your/input/images')

parser.add_argument('--output_dir', type=str, default='path/to/your/output/directory')

parser.add_argument('--device', type=str, default='cuda:0')

parser.add_argument('--upsample_align', type=bool, default=False)

parser.add_argument('--x32', action="store_true")

args = parser.parse_args()

argparse.ArgumentParser():创建参数解析器。

parser.add_argument():定义命令行参数。

args:解析后的命令行参数。

5.模型加载和推理:


net = Generator()

net.load_state_dict(torch.load(args.checkpoint, map_location="cpu"))

net.to(device).eval()

Generator():创建生成器模型的实例。

torch.load(args.checkpoint, map_location="cpu"):加载预训练模型的权重。

net.to(device).eval():将模型移动到指定的设备并设置为评估模式。

6.目录的创建:


os.makedirs(args.output_dir, exist_ok=True)

创建输出目录,如果目录已存在则不报错。

7.图像处理和保存:


for image_name in sorted(os.listdir(args.input_dir)):

​    # ... (图像处理和保存的逻辑)

遍历输入目录中的图像文件。

跳过不是 .jpg, .png, .bmp, .tiff 格式的文件。

调用 load_image 函数加载图像。

使用生成器模型进行推理,得到漫画风格图像。

将生成的图像保存到输出目录。

三、 系统详细设计

3.1 爬虫

我们设计爬虫是为了获取模型测试集,以方便调整模型参数得到更好的训练效果。按照聚焦网络爬虫的简要流程图,我们开发设计了一个可以根据关键词分类进行检索的爬虫。

3.1.1 对爬取目标的定义和描述

依据爬取需求定义好该聚焦网络爬虫爬取的目标及进行相关描述。

classnames = ['super_star', 'cartoon']  # 明星和动漫人物
keypoints = ['%E6%98%8E%E6%98%9F', '%E5%8A%A8%E6%BC%AB%E4%BA%BA%E7%89%A9']  # 关键字对应
gender_file_path = ['male', 'female']  # 对于人的分类检索可以按性别筛选

因为模型需要的是一组真实人物的图片和一组动漫人物的图片,因此我们用classname类定义两个字符串,分别是super_star(明星)和cartoon(动漫人物),假设我们对这两类进行分类检索。

每个字符串都是一个URL编码后的关键字,分别表示“明星”和“动漫人物”,在实际应用中,这些关键字通常是来自于对应网站检索功能的参数用于进行检索。

3.1.2 获取URL

以关键词“明星”为例,搜索后的网址为https://www.vcg.com/creative-image/mingxing/,
按F12查看源码,根据初始的URL爬取页面,并获得新的URL。如下图所示。
image
检索词为“明星”部分网页源码
image
检索词为“明星”部分网络参数
image
其中一个网页图片的完整URL

因为聚焦网络爬虫对网页的爬取是有目的性的,所以与目标无关的网页将会被过滤掉。同时,也需要将已爬取的URL地址存放到一个URL列表中,用于去重和判断爬取的进程。将过滤后的链接放到URL队列中。对聚焦网络爬虫来说,不同的爬取顺序可能导致爬虫的执行效率不同,因此需要依据搜索策略来确定下一步需要爬取哪些URL地址。

                for page in range(1, all_page + 1):
                    num_in_page = 1
                    # 获得url链接,这里额外增加了筛选条件:图片中仅有一个人
                    url = 'https://www.vcg.com/creative/search?phrase=' + keypoints[
                        class_index] + '&creativePeopleNum=2&creativeGender=' + str(gender_index + 1) + '&page=' + str(
                        page)
                    header = {
                        'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36',
                        #                'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'
                    }
                    req = request.Request(url=url, headers=header)
                    openhtml = request.urlopen(req).read().decode('utf8')

                    # 正则表达式
                    com = re.compile('"url800":.*?/creative/.*?.jpg"')

                    # 匹配URl地址
                    urladds = com.findall(openhtml)

以上代码是截取了一部分URL的代码,通过循环遍历每一页的内容,要求构建向服务器发送请求以获取特定条件下图片资源的URL,随后构建一个HTTP请求对象,请求对象包含了要请求的URL和自定义的请求头部信息,随后发送该请求并读取响应数据,并进行解码,将其转换为utf-8编码的字符串。

                    for urladd in urladds:
                        # try ... except防止匹配出错后程序停止
                        try:
                            add = 'http:' + urladd.strip('"url800":')
                            # 获取文件名称,格式:vcg+性别+获取方式+page+page中的第几张图片,vcg_raw代表原vcg网站对性别分类
                            filename = classnames[class_index] + '_' + gender_file_path[
                                gender_index] + '_vcg_raw_page' + str(page) + '_' + str(num_in_page) + '.jpg'
                            path = get_path(classnames[class_index], gender_file_path[gender_index], filename)
                            print('当前下载...', filename)

                            dom = request.urlopen(add).read()
                            with open(path, 'wb') as f:
                                f.write(dom)
                                sum_all_num += 1
                                num_in_page += 1
                        except:
                            print('当前该任务总共总共下载:', sum_all_num)  # 监控进度
                        if sum_all_num % 50 == 0:  # 监控进度
                            print('当前该任务总共下载:', sum_all_num)

随后构建一个完整的图片URL,并确定生成的图片文件名,包括类别、性别、来源、页面和图片在当前页中的序号。我们使用urllib.request.urlopen方法下载图片,并将其保存到本地指定的路径中,在下载过程中通过打印信息来监控下载进度,每下载50张输出一次当前下载总量。

com = re.compile('"url800":.*?/creative/.*?.jpg"')

下面详细解释一下正则表达式匹配HTML页面中图片的URL地址。”url800”:表示匹配以此开头的字符串; .*?表示匹配了任意数量的字符,这里的.表示匹配除换行符之外的任意字符,表示匹配前面的字符零次或多次,?表示尽可能少地匹配,这里的作用是匹配任意数量的字符; /creative/表示表达式匹配了/creative/这部分固定的字符串; .*?.jpg再次使用了.*?匹配任意数量的字符,直到.jpg*结尾。

3.1.3 保存

def get_path(classname,subclassname,filename):
    #获取当前工作路径
    cwd = os.getcwd()
    #获取图像的保存目录
    dir_path = cwd+'/vcg_test/' + classname +'/'+ subclassname
    #目录是否存在,不存在则创建目录
    if os.path.exists(dir_path):
        pass
    else:
        os.makedirs(dir_path)
    #获取图像的绝对路径
    file_path = dir_path +'/'+ filename
    return file_path

使用os模块的getcwd函数获取当前工作路径,即运行该代码的当前目录。根据类别名称和子类别名称拼接出图像的保存目录。这里假设图像保存的根目录是vcg_test,在根目录下再按照类别和子类别进行保存。通过os.path.exists判断图像保存的目录是否存在,如果不存在则通过os.makedirs创建。随后将图像的保存路径和文件名拼接起来,生成图像的绝对路径。返回图像文件的绝对路径。

3.2 前端

3.2.1 数据模型设计

根据前期设想并制定的目标,我们需要实现用户可以通过浏览器页面上传图片,并进行相应的动漫图像生成。理论上在用户点击上传文件后,利用Django框架后台可以直接调用模型对图像文件进行处理,然后将风格转换后的图片效果反馈给用户,不涉及对数据库的相关操作,但是这样会大大降低系统后续的可开发性,导致一些个针对用户个性化的内容和其他高级功能无法设计实现。为避免上述情况的发生,我们希望可以尽可能完整的实现系统,提前为系统将来的拓展开发做准备,因此将对数据库的使用考虑在设计过程中。

image
image

3.2.2 系统功能设计

本人在系统功能设计部分主要负责登录、注册页面设计,故在此处仅详细写了这两部分,其他由队友完成的部分仅贴了相关功能示意图和一些必要的说明。

3.2.2.1 图像文件的上传与收集

image

3.2.2.2 动漫图像的生成与反馈

image

3.2.2.3 动漫人像的展示

image

3.2.2.4 动漫风景的展示

模型根据不同的权重分为风景动漫迁移和人脸动漫迁移两类,目前已有的包括celeba_distill.pt和paprika.pt两个风景动漫迁移模型和face_paint_512_v1.pt和face_paint_512_v2.pt两个人脸动漫迁移模型。有必要说明的是,本系统暂时只支持使用face_paint_512_v2.pt模型进行动漫图像生成,一方面是因为系统初始目标是动漫人脸的生成,另一方面在使用大量图片进行测试比较之后,我们发现face_paint_512_v2.pt对风景图像进行动漫风格迁移的效果尚可。但本系统为了展现模型原始的风格迁移效果,在动漫风景展示页面所展示的图像都准备使用风景动漫迁移模型进行生成。

动漫风景展示页面会提供正式的图像下载接口,人脸图像展示页没有提供下载接口,是考虑到可能设计隐私方面的问题,因此暂时作罢。页面的展示效果需要以大图的方式展现,以丰富系统的功能。

3.2.2.5 注册与登录

在登录功能实现后,就可以对系统的一些功能页面设置访问权限了,这可以通过设置中间件的方法实现,设置的中间件将对用户从前端页面发起所有的请求进行处理,当系统后台无法从数据库的session中获取请求体相应的用户信息时认为用户为进行登录,于是拒绝请求继续访问后台,具体实现过程将在系统实现中说明。

image

3.2.3 系统架构设计

3.2.3.1 功能模块化

目前系统要实现的功能主要分为两大板块,分别是登录注册功能板块和动漫图像生成展示板块,考虑短时间内动漫图像的生成与展示部分功能较少,所以合并在同一文件内进行设计。登录注册部分预期设计实现的功能模块有登录模块、登出模块、注册模块和密码修改模块。动漫图像生成与展示部分预期预计实现的功能模块有首页模块、动漫图像上传与生成模块、动漫图像展示模块。

3.2.3.2 模板复用

项目开发过程中大部分视图函数会通过html模板文件向浏览器返回显示相关的内容,这些模板文件使用相同的编写规则,这导致很多时候模板设计过程中会重复编写一些模板代码,为了避免出现这种代码冗余的情况,需要充分利用模板可复用的特性。目前主要实现模板复用的方式有三种,分别是宏、继承和包含。宏类似函数,可以传入参数,需要定义、调用;继承本质是代码替换,一般用来实现多个页面中重复不变的区域;包含是直接将目标模板文件整个渲染出来。模板复用能很大程度上提高开发效率,减轻开发负担。

3.2.3.3 模型独立化

系统开发的目的是向用户开放模型的使用和向用户展示模型训练的效果,使模型独立化有利于系统的迁移使用和模型的更换。在系统开发过程中,将尽可能的避免模型的嵌入,主要以导入或调用的方式将模型代入系统。

3.2.4 前端交互界面设计

目前系统前端交互界面主要包括用户进行动漫图像生成页面、登录注册页面和图像展示页面,其中动漫图像生成页面的预设如下图所示。

image

四、 系统实现

4.1 爬虫

我们运行爬虫代码,可以自动生成如下的文件夹框架,用于分别保存明星和动漫人物的男性、女性照片,代码运行示例图如下图所示:

image
文件夹框架图
image

代码运行示例图

image

动漫图像女性示例图

image

明星图像男性示例图

4.2 前端

4.2.1 Django项目环境搭建

4.2.1.1 虚拟环境

系统应用程序的开发在Conda虚拟环境中进行,基于模型以及现有数据库MySQL的版本进行相关软件包的组合搭配,具体见上文系统软件包的说明。使用Pycharm在该虚拟环境中完成Django项目的初步创建和配置。

4.2.1.2 数据库

数据库使用5.7.41版本的MySQL,版本较老,需要降低Django的版本。数据库的创建可以通过管理员CMD命令实现,本人电脑中配备了MySQL可视化管理工具Navicat,所以最终选择利用该工具完成数据库的创建,数据库命名为animegandb,具体如下图所示。另外在Django项目文件settings.py中,默认连接的是sqlite3,所以要手动配置连接建立的MySQL。

image

4.2.1.3 项目准备

项目根目录中需要另外新建media目录用于存放用户上传的媒体文件,由于Django中存在相关的保护机制,用户无法直接访问获取media目录中需要的内容,有必要要在settings.py和urls.py中完成路径参数配置。

image

4.2.2 登录与注册

4.2.2.1 实现逻辑

登录和注册功能的实现分别对应login和signup视图函数,两个视图函数分别向对应的模板传递参数,用户点击页面上的交互按钮后前端页面将数据表单送到对应的视图函数处理。

登录视图函数login分别对GET和POST两种请求方式做出不同的处理,当请求方式为GET时,将创建的LoginForm对象传给模板文件进行前端渲染,效果为用户通过连接访问登录页时页面显示出对应的样式和登录框组。当请求方式为POST时,即用户点击了登录按钮后页面模板将包裹在

标签中的内容打包发送到后台,后台使用函数login根据LoginForm进行表单的验证,这里不用ModelForm是因为不涉及对数据库的改动。验证通过后,根据表单中的信息在数据库进行filter()查询后根据结果做出不同的返回。如果查询内容非空则表示用户已注册且密码正确,在数据库用户表留有记录,则通过网站生成随机字符串,并写到用户浏览器的cookie中,再写入到数据库的session表中,进行登录标记,再将页面通过redirect()转向动漫图像生成页面。若查询结果为空则返回“用户名或密码错误”信息。

注册视图函数signup同样对两种请求方式做不同的处理,分别是显示页面和处理用户提交的数据表单。和登录有所区别的是,注册使用SignUpModelFrom创建form对象,因为涉及到数据库数据的更新,在SignUpModelFrom中使用钩子方法和MD5方式进行密码的加密处理。在前端页面中,用户需要在输入密码后确认密码,当前两次输入一致时signup函数才会允许is_valid验证通过然后保存到数据库。当注册流程结束后,后台将使页面转向登录页引导用户进行登录。

4.2.2.2 中间件

在系统应用程序开发过程中,于先前建立的中间件目录存放自建的auth.py中间件文件,为了使中间件生效,还需要在配置文件settings.py中的MIDDLEWARE处进行配置,这个过程和app的注册有些相似,在配置文件中,系统会根据配置按顺序执行所有的中间件。在auth.py中,通过选择部分不需要登录也能访问的页面开放,其他页面在系统无法从数据库的session中获取请求体对应的信息时拒绝请求继续向后台发送。

4.2.2.3 登录模板

登录模板login.html以继承的方式,作为导航条模板layout_account.html的子模版进行开发,父模板中主要对页面的整体布局和背景进行了设置。模板利用css将登录框设计成半透明状摆在页面中心,对输入框和登录按钮进行了一些调整和美化。模板中表单部分用标签包裹,设置请求类型为POST实现隐蔽的请求发送。另外登陆页面设置了注册和忘记密码两个链接按钮,利用JS监听忘记密码按钮,当按钮被点击时显示出修改密码的对话框。对话框的模板样式取自bootstrap官网,框内嵌入包裹输入的用户名和密码,将请求发送路由通过action属性设置为忘记密码对应的视图函数。点击注册则页面跳转至对应的页面。

image
image

4.2.2.4 注册模板

登录模板signup.html同样以继承的方式,作为导航条模板layout_account.html的子模版进行开发。该模板的样式设计于登录模板基本一致,区别在于模板中表单部分的input由视图函数传递的参数给出,表单发送的请求类型为POST。在注册框底部设置登录按钮方便用户在登录注册功能之间转换。

image

4.2.3 其他

4.2.6.1 注销

注销功能用于登录用户的登出,其原理是清除request请求发起者的session记录,登出后对应的logout视图函数会将页面转向动漫人像展示页。

4.2.6.2 忘记密码

忘记密码功能对应视图函数forget,函数接收处理从登录页面传来的POST请求,通过get()获取指定input框的字段内容在数据库中查询,查询结果非空则使用MD5进行密码加密处理后update()方式更新数据,否则返回用户不存在页面信息。

4.2.6.3 自定义工具函数

在utils目录中存放开发过程中自定义的bootstrap函数和encrypt函数,前者用于帮助注册用的SignUpModelFrom添加widget即对应input框的属性,后者定义了MD5加密方法。

4.2.6.4 导航条模板

导航条模板layout_nav.html作为父模板供其他模板继承使用,导航条设计页面左侧悬停,使用纯css为导航条添加动画。

五、 项目总结

一方面,在阅读论文时,我在学习算法过程中有所参考学习,文中作者提出了NST算法都是基于GRAM矩阵的匹配统计,从预训练的卷积神经网络中个提取深度特征。[8]我给出以下看法:使用Gram矩阵就是对于一个向量,去计算与其转置向量的内积,从而得到该向量的Gram矩阵,而这个Gram矩阵由一个特点:对称。一个n维向量可以得到n*n维的Gram矩阵,其中每一个元素都可以表示为特征i与特征j的相关性,而对角线上的元素可以表示为某个特征i在整个图像中的强度。在NST中,n是filter的数量,Gram(i,j)测量了filter i的激活量与j激活量的相似度,Gram(i,i)测量filter i的活跃度,即如果filter i检测垂直纹理,那么Gram(i,i)就可以测量垂直纹理在一幅画中的频繁程度。以上看法是我自己的思考,并不一定准确,如有问题,还请老师批评指正。

另一方面,我负责爬虫代码的编写和部分前端代码的编写。我在Python语言程序设计课程设计、项目综合实践课程设计等课设中,都曾经写过爬虫代码,因此对这方面比较熟悉,很快地就把这一部分成功解决了。在其中遇到的问题就是对正则表达式匹配上还不是很熟悉,翻了很多网上了资料和Python的教科书,最终是顺利解决了。在编写前端代码过程中,也遇到了一些问题,如如何进行用户的权限和权限管理并设计安全的用户登录系统呢?其实,Django提供了内置的用户认证和权限管理系统,可以使用内置的User模型或者自定义用户模型来实现用户的注册、登录和权限管理。可以使用Django内制动认证视图函数如login、logout等及装饰器如@login_required来限制用户的访问。在保存用户的密码方面,Django内置了密码加密功能,使用set_password()方法来加密用户密码,然后用check_password()方法来验证密码。

通过本次课程设计,我学习到了很多东西。通过该项目,深入理解了GAN模型以及AnimeGAN在图像风格转换中的应用。通过实际开发系统的过程,掌握了模型训练、前后端开发、性能优化等方面的技能和经验。为漫画风格生成技术的应用提供了一个具体的实践案例,为相关领域的研究和应用提供了借鉴和参考。

后续展望:进一步优化模型的生成效果和速度,提高系统的实时性。探索其他图像风格转换技术,并丰富系统的功能和体验。拓展系统的应用范围,如与社交平台集成、移动端应用等,增强系统的影响力和推广度。

参考文献

[1] Gatys, L.A., Ecker, A.S., Bethge, M.: A neural algorithm of artistic style. CoRRabs/1508.06576 (2015). http://arxiv.org/abs/1508.06576.

[2] Gatys, L.A., Ecker, A.S., Bethge, M., Hertzmann, A., Shechtman, E.: Controlling perceptual factors in neural style transfer. In: Proceedings 30th IEEE Conference on Computer Vision and Pattern Recognition, CVPR 2017, Honolulu, HI, United States, pp. 3730–3738 (2017).

[3] Chen, Jie et al. “AnimeGAN: A Novel Lightweight GAN for Photo Animation.” International Symposium on Intelligence Computation and Applications (2019).

[4] Li, Y., Fang, C., Yang, J., Wang, Z., Lu, X., Yang, M.H.: Diversified texture syn- thesis with feed-forward networks. In: Proceedings 30th IEEE Conference on Com- puter Vision and Pattern Recognition, CVPR 2017, Honolulu, HI, United States, pp. 266–274 (2017).

[5] Chen, Yang, Yu-Kun Lai and Yong-Jin Liu. “CartoonGAN: Generative Adversarial Networks for Photo Cartoonization.” 2018 IEEE/CVF Conference on Computer Vision and Pattern Recognition (2018): 9465-9474.

[6] Simonyan, K., Zisserman, A.: Very deep convolutional networks for large-scale image recognition. CoRR abs/1409.1556 (2014). http://arxiv.org/abs/1409.1556.

[7] 昇思MindSpore. GAN系列之动漫风格迁移AnimeGAN2. [OL]. (2022-12-21). https://blog.csdn.net/Kenji_Shinji/article/details/128392989.

[8] Xerrors. AnimeGAN论文阅读笔记. [OL]. (2021-04-20). https://juejin.cn/post/6953172570245955615.

**
**

附录代码

1 test.py

\1. import argparse

\2.  

\3. import torch

\4. import cv2

\5. import numpy as np

\6. import os

\7.  

\8. from model import Generator

\9.  

\10. torch.backends.cudnn.enabled = False

\11. torch.backends.cudnn.benchmark = False

\12. torch.backends.cudnn.deterministic = True

\13.  

\14.  

\15. def load_image(image_path, x32=False):

\16.   img = cv2.imread(image_path).astype(np.float32)

\17.   img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)

\18.   h, w = img.shape[:2]

\19.  

\20.   if x32: *# resize image to multiple of 32s*

\21.     def to_32s(x):

\22.       return 256 if x < 256 else x - x % 32

\23.  

\24.     img = cv2.resize(img, (to_32s(w), to_32s(h)))

\25.  

\26.   img = torch.from_numpy(img)

\27.   img = img / 127.5 - 1.0

\28.   return img

\29.  

\30.  

\31. def test(args):

\32.   device = args.device

\33.  

\34.   net = Generator()

\35.   net.load_state_dict(torch.load(args.checkpoint, map_location="cpu"))

\36.   net.to(device).eval()

\37.   print(f"model loaded: {args.checkpoint}")

\38.  

\39.   os.makedirs(args.output_dir, exist_ok=True)

\40.   for image_name in sorted(os.listdir(args.input_dir)):

\41.     if os.path.splitext(image_name)[-1].lower() not in [".jpg", ".png", ".bmp", ".tiff"]:

\42.       break

\43.     *#* *当为目标**img**时使用模型进行风格转换*

\44.     if image_name == args.origin_img:

\45.       image = load_image(os.path.join(args.input_dir, image_name), args.x32)

\46.       with torch.no_grad():

\47.         input = image.permute(2, 0, 1).unsqueeze(0).to(device)

\48.         out = net(input, args.upsample_align).squeeze(0).permute(1, 2, 0).cpu().numpy()

\49.         out = (out + 1) * 127.5

\50.         out = np.clip(out, 0, 255).astype(np.uint8)

\51.  

\52.       cv2.imwrite(os.path.join(args.output_dir, image_name), cv2.cvtColor(out, cv2.COLOR_BGR2RGB))

\53.       print(f"image saved: {image_name}")

\54.       return

\55.  

\56.  

\57. if __name__ == '__main__':

\58.   parser = argparse.ArgumentParser()

\59.   parser.add_argument(

\60.     '--checkpoint',

\61.     type=str,

\62.     default='weights/face_paint_512_v2.pt',

\63.   )

\64.   parser.add_argument(

\65.     '--input_dir',

\66.     type=str,

\67.     default='media/input',

\68.   )

\69.   parser.add_argument(

\70.     '--output_dir',

\71.     type=str,

\72.     default='media/output',

\73.   )

\74.   parser.add_argument(

\75.     '--device',

\76.     type=str,

\77.     default='cpu',

\78.   )

\79.   parser.add_argument(

\80.     '--upsample_align',

\81.     type=bool,

\82.     default=False,

\83.   )

\84.   parser.add_argument(

\85.     '--x32',

\86.     action="store_true",

\87.   )

\88.   parser.add_argument(

\89.     '--origin_img',

\90.     type=str,

\91.     default='example.jpg',

\92.   )

\93.   args = parser.parse_args()

\94.  

\95.   test(args)

2 pachong.py

\1. from urllib import request

\2. import os

\3. import re *#**正则表达式库*

\4.  

\5. *#**输入类别名称**,**子类别名称,文件名,输出图片路径*

\6. def get_path(classname,subclassname,filename):

\7.   *#**获取当前工作路径*

\8.   cwd = os.getcwd()

\9.   *#**获取图像的保存目录*

\10.   dir_path = cwd+'/vcg_test/' + classname +'/'+ subclassname

\11.   *#**目录是否存在**,**不存在则创建目录*

\12.   if os.path.exists(dir_path):

\13.     pass

\14.   else:

\15.     os.makedirs(dir_path)

\16.   *#**获取图像的绝对路径*

\17.   file_path = dir_path +'/'+ filename

\18.   return file_path

\19.  

\20.  

\21. classnames = ['super_star', 'cartoon'] *#* *假设我们分类是明星和动漫人物*

\22. keypoints = ['%E6%98%8E%E6%98%9F', '%E5%8A%A8%E6%BC%AB%E4%BA%BA%E7%89%A9'] *#* *关键字对应*

\23. gender_file_path = ['male', 'female'] *#* *对于人的分类检索可以按性别筛选*

\24. all_page = 1 *#* *想要下载的总页数**,**其中每页与检索相同**,**为**100**张*

\25. for class_index, phrase in enumerate(classnames):

\26.   sum_all_num = 0

\27.   if class_index >= 0: *#* *从某一类断开则选择该类为起始点*

\28.     *# if class_index == 1 : #**例如下载动漫人物时被服务器**Kill**掉可以改为该行继续下载*

\29.     for gender_index, gender in enumerate(gender_file_path):

\30.       if gender_index >= 0: *#* *从性别某类断开则选择该类为起始点*

\31.         for page in range(1, all_page + 1):

\32.           num_in_page = 1

\33.           *#* *获得**url**链接**,**这里额外增加了筛选条件**:**图片中仅有一个人*

\34.           url = 'https://www.vcg.com/creative/search?phrase=' + keypoints[

\35.             class_index] + '&creativePeopleNum=2&creativeGender=' + str(gender_index + 1) + '&page=' + str(

\36.             page)

\37.           header = {

\38.             'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36',

\39.             *#        'User-Agent':'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US; rv:1.9.1.6) Gecko/20091201 Firefox/3.5.6'*

\40.           }

\41.           req = request.Request(url=url, headers=header)

\42.           openhtml = request.urlopen(req).read().decode('utf8')

\43.  

\44.           *#* *正则表达式*

\45.           com = re.compile('"url800":.*?/creative/.*?.jpg"')

\46.  

\47.           *#* *匹配**URl**地址*

\48.           urladds = com.findall(openhtml)

\49.           for urladd in urladds:

\50.             *# try ... except**防止匹配出错后程序停止*

\51.             try:

\52.               add = 'http:' + urladd.strip('"url800":')

\53.               *#* *获取文件名称**,**格式**:vcg+**性别**+**获取方式**+page+page**中的第几张图片,**vcg_raw**代表原**vcg**网站对性别分类*

\54.               filename = classnames[class_index] + '_' + gender_file_path[

\55.                 gender_index] + '_vcg_raw_page' + str(page) + '_' + str(num_in_page) + '.jpg'

\56.               path = get_path(classnames[class_index], gender_file_path[gender_index], filename)

\57.               print('当前下载...', filename)

\58.  

\59.               dom = request.urlopen(add).read()

\60.               with open(path, 'wb') as f:

\61.                 f.write(dom)

\62.                 sum_all_num += 1

\63.                 num_in_page += 1

\64.             except:

\65.               print('当前该任务总共总共下载:', sum_all_num) *#* *监控进度*

\66.             if sum_all_num % 50 == 0: *#* *监控进度*

\67.               print('当前该任务总共下载:', sum_all_num)

3 model.py

\1. import torch

\2. from torch import nn

\3. import torch.nn.functional as F

\4.  

\5.  

\6. class ConvNormLReLU(nn.Sequential):

\7.   def __init__(self, in_ch, out_ch, kernel_size=3, stride=1, padding=1, pad_mode="reflect", groups=1, bias=False):

\8.     

\9.     pad_layer = {

\10.       "zero":  nn.ZeroPad2d,

\11.       "same":  nn.ReplicationPad2d,

\12.       "reflect": nn.ReflectionPad2d,

\13.     }

\14.     if pad_mode not in pad_layer:

\15.       raise NotImplementedError

\16.       

\17.     super(ConvNormLReLU, self).__init__(

\18.       pad_layer[pad_mode](padding),

\19.       nn.Conv2d(in_ch, out_ch, kernel_size=kernel_size, stride=stride, padding=0, groups=groups, bias=bias),

\20.       nn.GroupNorm(num_groups=1, num_channels=out_ch, affine=True),

\21.       nn.LeakyReLU(0.2, inplace=True)

\22.     )

\23.  

\24.  

\25. class InvertedResBlock(nn.Module):

\26.   def __init__(self, in_ch, out_ch, expansion_ratio=2):

\27.     super(InvertedResBlock, self).__init__()

\28.  

\29.     self.use_res_connect = in_ch == out_ch

\30.     bottleneck = int(round(in_ch*expansion_ratio))

\31.     layers = []

\32.     if expansion_ratio != 1:

\33.       layers.append(ConvNormLReLU(in_ch, bottleneck, kernel_size=1, padding=0))

\34.     

\35.     # dw

\36.     layers.append(ConvNormLReLU(bottleneck, bottleneck, groups=bottleneck, bias=True))

\37.     # pw

\38.     layers.append(nn.Conv2d(bottleneck, out_ch, kernel_size=1, padding=0, bias=False))

\39.     layers.append(nn.GroupNorm(num_groups=1, num_channels=out_ch, affine=True))

\40.  

\41.     self.layers = nn.Sequential(*layers)

\42.     

\43.   def forward(self, input):

\44.     out = self.layers(input)

\45.     if self.use_res_connect:

\46.       out = input + out

\47.     return out

\48.  

\49.   

\50. class Generator(nn.Module):

\51.   def __init__(self, ):

\52.     super().__init__()

\53.     

\54.     self.block_a = nn.Sequential(

\55.       ConvNormLReLU(3, 32, kernel_size=7, padding=3),

\56.       ConvNormLReLU(32, 64, stride=2, padding=(0,1,0,1)),

\57.       ConvNormLReLU(64, 64)

\58.     )

\59.     

\60.     self.block_b = nn.Sequential(

\61.       ConvNormLReLU(64, 128, stride=2, padding=(0,1,0,1)),      

\62.       ConvNormLReLU(128, 128)

\63.     )

\64.     

\65.     self.block_c = nn.Sequential(

\66.       ConvNormLReLU(128, 128),

\67.       InvertedResBlock(128, 256, 2),

\68.       InvertedResBlock(256, 256, 2),

\69.       InvertedResBlock(256, 256, 2),

\70.       InvertedResBlock(256, 256, 2),

\71.       ConvNormLReLU(256, 128),

\72.     )  

\73.     

\74.     self.block_d = nn.Sequential(

\75.       ConvNormLReLU(128, 128),

\76.       ConvNormLReLU(128, 128)

\77.     )

\78.  

\79.     self.block_e = nn.Sequential(

\80.       ConvNormLReLU(128, 64),

\81.       ConvNormLReLU(64, 64),

\82.       ConvNormLReLU(64, 32, kernel_size=7, padding=3)

\83.     )

\84.  

\85.     self.out_layer = nn.Sequential(

\86.       nn.Conv2d(32, 3, kernel_size=1, stride=1, padding=0, bias=False),

\87.       nn.Tanh()

\88.     )

\89.     

\90.   def forward(self, input, align_corners=True):

\91.     out = self.block_a(input)

\92.     half_size = out.size()[-2:]

\93.     out = self.block_b(out)

\94.     out = self.block_c(out)

\95.     

\96.     if align_corners:

\97.       out = F.interpolate(out, half_size, mode="bilinear", align_corners=True)

\98.     else:

\99.       out = F.interpolate(out, scale_factor=2, mode="bilinear", align_corners=False)

\100.      out = self.block_d(out)

\101.  

\102.      if align_corners:

\103.        out = F.interpolate(out, input.size()[-2:], mode="bilinear", align_corners=True)

\104.      else:

\105.        out = F.interpolate(out, scale_factor=2, mode="bilinear", align_corners=False)

\106.      out = self.block_e(out)

\107.  

\108.      out = self.out_layer(out)

\109.      return out

\110.      

4 style_conversion.py

\1. from django.shortcuts import render, HttpResponse

\2. from django import forms

\3. from app01 import models

\4. import os

\5. from datetime import datetime

\6. import time

\7.  

\8.  

\9. class UploadModelForm(forms.ModelForm):

\10.   class Meta:

\11.     model = models.UserInfo

\12.     fields = ['origin_img']

\13.  

\14.  

\15. def homepage(request):

\16.   form = UploadModelForm()

\17.   origin_img = "media/input/example.jpg"

\18.   anime_img = "media/output/example.jpg"

\19.   return render(request, 'homepage.html', {"form": form, "origin_img": origin_img, "anime_img": anime_img})

\20.  

\21.  

\22. def upload(request):

\23.   if request.method == "POST":

\24.     *#* *指定操作的数据库对象*

\25.     user = request.session["user_info"]["id"]

\26.     print(request.session["user_info"]["id"])

\27.     row_object = models.UserInfo.objects.filter(id=user).first()

\28.  

\29.     form = UploadModelForm(data=request.POST, files=request.FILES, instance=row_object)

\30.     if form.is_valid():

\31.       *#* *检查文件是否符合要求*

\32.       image_file = form.cleaned_data['origin_img']

\33.       ext = os.path.splitext(image_file.name)[1] *#* *获取文件扩展名*

\34.       if ext.lower() not in ['.jpg', '.png']:

\35.         return HttpResponse("文件不满足要求")

\36.  

\37.       *#* *修改文件名(用户**id +* *时间* *+* *拓展名)*

\38.       uid = row_object.id

\39.       filename = str(uid) + datetime.now().strftime("%Y%m%d%H%M%S") + ext

\40.       image_file.name = filename

\41.  

\42.       *#* *保存路径和原图片*

\43.       form.instance.origin_img = image_file

\44.       form.instance.anime_img = 'output/' + filename

\45.       form.save()

\46.  

\47.       *#* *指定图片调用模型进行图片转换*

\48.       cmd = "python test.py --origin_img " + filename

\49.       print(cmd)

\50.       os.system(cmd) *#* *设置参数的同时运行了**test.py*

\51.  

\52.       data_object = models.UserInfo.objects.filter(id=user).first()

\53.       origin_img = "media/" + str(data_object.origin_img)

\54.       anime_img = "media/" + str(data_object.anime_img)

\55.       print(origin_img)

\56.  

\57.       time.sleep(5)

\58.       return render(request, 'homepage.html', {"form": form, "origin_img": origin_img, "anime_img": anime_img})

\59.  

\60.  

\61. def exhibition1(request):

\62.   image_folder = "app01/static/img/origin/face/"

\63.   image_files = [f for f in os.listdir(image_folder)]

\64.   *# print(image_files)*

\65.   return render(request, 'exhibition1.html', {"image_files": image_files})

\66.  

\67.  

\68. def exhibition2(request):

\69.   image_folder = "app01/static/img/origin/scenery/"

\70.   image_files = [f for f in os.listdir(image_folder)]

\71.  

\72.   return render(request, 'exhibition2.html', {"image_files": image_files})

5 account.py

\1. from django.shortcuts import render, redirect, HttpResponse

\2. from django import forms

\3. from app01 import models

\4. from django.views.decorators.csrf import csrf_exempt

\5. from django.http import JsonResponse

\6.  

\7. from app01.utils.bootstrap import BootStrapFrom

\8. from app01.utils.bootstrap import BootStrapModelFrom

\9. from app01.utils.encrypt import md5

\10. from django.core.exceptions import ValidationError

\11.  

\12.  

\13. class LoginForm(BootStrapFrom):

\14.   *#* *因为不用**modelform**所以要自己写字段**(**字段名应尽量与数据库表中属性一致,方便使用)*

\15.   username = forms.CharField(

\16.     label="用户名",

\17.     *# widget=forms.TextInput(attrs={}),*

\18.     required=True, *#* *不能为空*

\19.   )

\20.   password = forms.CharField(

\21.     label="密码",

\22.     *# widget=forms.PasswordInput(attrs={}),*

\23.     required=True,

\24.   )

\25.  

\26.   *#* *钩子返回加密后密码*

\27.   def clean_password(self):

\28.     pwd = self.cleaned_data.get("password")

\29.     return md5(pwd)

\30.  

\31.  

\32. class SignUpModelFrom(BootStrapModelFrom):

\33.   """定义用户信息ModelForm类"""

\34.   confirm_password = forms.CharField(

\35.     label="确认密码",

\36.   )

\37.  

\38.   class Meta:

\39.     model = models.UserInfo

\40.     fields = ["username", "password", "confirm_password"]

\41.  

\42.   *# MD5**加密*

\43.   def clean_password(self):

\44.     pwd = self.cleaned_data.get("password")

\45.     return md5(pwd)

\46.  

\47.   *#* *钩子方法确认密码*

\48.   def clean_confirm_password(self):

\49.     pwd = self.cleaned_data.get("password")

\50.     confirm = md5(self.cleaned_data.get("confirm_password")) *# pwd**在上面加密过,需要加密确认内容以进行比较*

\51.     if confirm != pwd:

\52.       raise ValidationError("密码不一致")

\53.     *#* *返回的内容会被作为保存到数据库的对象(若数据库无此属性则不管)*

\54.     return confirm

\55.  

\56.  

\57. def login(request):

\58.   if request.method == "GET":

\59.     form_login = LoginForm()

\60.     return render(request, 'login.html', {"form_login": form_login})

\61.  

\62.   form_login = LoginForm(data=request.POST)

\63.   if form_login.is_valid():

\64.     *# form.cleaned_data**已经是字典了*

\65.     user_object = models.UserInfo.objects.filter(**form_login.cleaned_data).first()

\66.     *#* *当**admin_object**用户对象获取结果为**None*

\67.     if not user_object:

\68.       *#* *主动添加错误提示,在**"password"**下显示**"**用户名或密码错误**"*

\69.       form_login.add_error("password", "用户名或密码错误")

\70.       return render(request, 'login.html', {"form_login": form_login})

\71.  

\72.     *#* *用户名和密码正确*

\73.     *#* *网站生成随机字符串,并写到用户浏览器的**cookie**中,再写入到**session**中(将**“info”**及其对应的内容保存)*

\74.     request.session["user_info"] = {'id': user_object.id, 'user': user_object.username}

\75.     return redirect('/homepage/')

\76.  

\77.   return render(request, 'login.html', {"form_login": form_login})

\78.  

\79.  

\80. def signup(request):

\81.   if request.method == "GET":

\82.     form_signup = SignUpModelFrom()

\83.     return render(request, 'signup.html', {"form_signup": form_signup})

\84.  

\85.   form_signup = SignUpModelFrom(data=request.POST)

\86.   if form_signup.is_valid():

\87.     form_signup.save()

\88.     return redirect('/login/')

\89.   return render(request, 'signup.html', {"form_signup": form_signup})

\90.  

\91.  

\92. def logout(request):

\93.   """用户登出"""

\94.   *# del request.session['user_info']*

\95.   request.session.clear()

\96.   return redirect('/exhibition1/')

\97.  

\98.  

\99. def forget(request):

\100.    if request.method == "POST":

\101.      username = request.POST.get("username")

\102.      user_object = models.UserInfo.objects.filter(username=username).first()

\103.      if not user_object:

\104.        return render(request, 'error.html', {"msg": "用户不存在"})

\105.  

\106.      password = request.POST.get("password")

\107.      password = md5(password)

\108.      models.UserInfo.objects.filter(username=username).update(password=password)

\109.      return redirect('/login/')

\110.  

6 models.py

\1. from django.db import models

\2.  

\3.  

\4. *# Create your models here.*

\5. class UserInfo(models.Model):

\6.   """用户信息表"""

\7.   username = models.CharField(verbose_name="用户名", max_length=16)

\8.   password = models.CharField(verbose_name="密码", max_length=32)

\9.   *# upload_to**可指向**media**中的目录*

\10.   origin_img = models.FileField(verbose_name="原始图片", max_length=128, upload_to="input/", blank=True, null=True)

\11.   anime_img = models.CharField(verbose_name="动漫图片", max_length=128, blank=True, null=True)

\12.  

\13.   def __str__(self):

\14.     return self.username
posted @ 2024-01-21 19:21  诩言Wan  阅读(364)  评论(0编辑  收藏  举报