本文文件仓库

本文所用到的所有内容(运行前)均保存在该仓库中:https://github.com/chmoe/Real_Fake_Face

由于代码内容较多,正文部分不会展示全部代码,有需要的小可爱可以去github上clone。

另外本文内容为试验(Test)而非实验(Experiment)。

环境

开发环境

项目版本
DeviceMacMini(2020) M1
OSmacOS Big [email protected]
RAM16G
SSD1T
Python3.8.6 based conda
IDEPyCharm 2021.2.2(Community Edition)

运行环境

项目版本
CPUAMD Ryzen 7 3800X @ 3.90GHz
GPUNvidia GeForce RTX2080 SUPER (8GB)
OSCentOS
RAM64GB (DDR4-3200)
SSDSSD 1TB
Python3.8.8 based conda

整体思想

步骤

  1. 使用MTCNN对于人脸有一个初步的识别,并且提取人脸部分的图像。
  2. 对于提取出的人脸部分的图像使用SLIC算法进行superpixel处理。
  3. 根据superpixel处理后的区块对于图像进行分割。
  4. 将分割后的图像送入神经网络进行学习。
  5. 构建一个自动化程序完成上述步骤的2、3、4三个部分

预计达成的试验目标

  1. 在使用SLIC算法对于图像分割时,找到较优的分割块数(K): 20~64 块
  2. 神经网络训练时,找到较优的神经网络: 经过Fine-tuning化的VGG16、Resnet50、Xception

文件树

本程序运行在一个名为AllInOne的文件夹中,为方便调试程序,因此生成的所有文件夹都将会位于AllInOne的同级目录中,可以在config.py文件中进行修改相关目录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.
├── Debug.py // 模拟C#的debug类
├── config.py // 项目的配置目录及静态类
├── main.py // 项目入口
├── pypeline // 处理管线(拼写错了但是懒得改了)
│   ├── Net.py // 三种网络的公共部分
│   ├── Resnet.py // 生成Resnet50网络的模型
│   ├── SLIC.py // 使用SLIC算法对图片进行标注
│   ├── Split.py // 根据SLIC算法标注部分对图片进行分割
│   ├── VGG.py // 生成VGG16网络的模型
│   ├── Validation.py // 自定义回调函数计算验证集的精确度
│   ├── XCeption.py // 生成XCeption网络的模型
│   └── __init__.py // 自定义包的入口
├── result // 存放使用MTCNN处理好的图片
│   ├── fake // MTCNN处理后的生成图片
│   └── real // MTCNN处理后的真实图片
└── s.sh // Debug时使用的文件,可以忽略。

开发

关于数据集合

数据集

本文的数据集采用了Kaggle的Real and Fake Face Detection数据集的real_and_fake_face部分,并对数据集进行预处理。

数据集预处理

针对本文中的MTCNN部分可以参见上一篇文章中的MTCNN部分,这里不再过多赘述。

但是本来觉得会如此轻松的过程却遇到了问题。不知道是不是M1晶片引起的问题,在使用MTCNN的时候发现该函数会造成内存泄漏,同样在检索的时候发现这条Issue一直是开放的状态,所以应该不是我自己的问题。

为了解决这个问题,只好对于代码进行一定的改造,既然内存泄漏会导致程序被操作系统杀死,那么只好让程序分段进行。

具体做法就是看着程序的处理进度,在程序被杀死之后立刻手动调整运行参数,让程序继续处理。由于本次试验使用的数据集不大,因此相比于用代码实现自动化,手动处理会更加节省时间,因此决定使用这种方式,代码部分如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import cv2  # conda install opencv
from mtcnn import MTCNN # conda install mtcnn
import copy
train_file_path = "./archive/real_and_fake_face/training_"
def is_image_file(filename):
return any(filename.endswith(extension) for extension in ['.png', '.jpg', '.jpeg', '.PNG', '.JPG', '.JPEG'])
def get_image_file_list(path):
image_filenames = [os.path.join(path, x) for x in os.listdir(path) if is_image_file(x)]
return image_filenames
def path_exist(path):
if not os.path.exists(path):
os.makedirs(path)
return path
fake_image_filenames = get_image_file_list(train_file_path + "fake")
real_image_filenames = get_image_file_list(train_file_path + "real")
# detector
detector = MTCNN()
def get_face(list_name, name):
path = path_exist('./result/' + name + '/')
for i in range(851, len(list_name)):
img = cv2.imread(list_name[i])
face_frame = copy.deepcopy(img)
# BGR2RGB
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
# 检测出人脸的四个点
dets = detector.detect_faces(img_rgb)
for face in dets:
# 座標と高幅を取得
box_x, box_y, box_w, box_h = face['box']
# 顔のトリミング
face_image = face_frame[box_y:box_y + box_h, box_x:box_x + box_w]
cv2.imwrite(path + str(i) + '.jpg', face_image)
print("Processing: {}/{}".format(i, len(list_name)))
get_face(fake_image_filenames, 'fake')
get_face(real_image_filenames, 'real')

上述代码中第20行的851是需在程序终止运行时手动提供的运行参数,程序最开始运行时应为0.

代码运行结束后可获得文件夹result,其内部结构如本文中文件树所示。fake中放置生成的人脸,real中放置真实的人脸,这两个文件夹中都是经过预处理后单独提取脸部内容。

pipeline

本栏内部介绍的每一个py文件均为一个同名的类,会介绍类的部分作用和参数含义。

Net

描述

Net类的作用是三种网络的调用类,需要传入SLIC的分割数,fine-tuning的冷冻层数以及网络名称。

注:本类中的init部分的图像宽高以及epoch和batch数需要在本类中进行控制。

保存训练过程

本类中的save_history会读取model.history的训练结果,然后根据每一个网络和分割数将下述内容存入对应的txt文件

  1. accuracy
  2. loss
  3. val_accuracy
  4. val_loss(由于回调函数Validation中未计算损失值,因此该项不可用)

生成网络

本类中的create_model方法用于生成网络,根据传入的网络名称来决定调用哪一个网络,其中可选网络如下

调用名称对应类对应网络
Config.name_vgg16VGGVGG16
Config.name_resnetResnetResnet50
Config.name_xceptionXCeptionXCeption

生成训练用数据

本类中的generate_train方法用于生成训练用数据集,使用的是keras。preprocessing.image下的ImageDataGenerator类对图片进行的处理。

经过该类的处理,训练用图片集合被转换为300*300大小的图片生成器。

生成验证用数据

由于任务处理的特殊性,训练集合和验证集合对于图片的处理方式应该有所不同。

训练数据输入时,应该按照每一个分割对应一个标签进行输入,而验证数据需要将一张图片的所有分割部分统一进行输入,然后对于这些分割部分的评价采用多数表决的方式决定整张图片的判定结果。然后判定验证集中的每一图片进而计算验证集的精确度。

并且因为ImageDataGenerator.flow_from_directory是根据文件夹对于图片进行分类,因此需要自行使用yield关键词构建生成器方法。(不了解生成器方法的小可爱可以看这里的生成器部分

根据下文的Split部分分割的验证集文件结构如SLIC_result所示,验证集的fake|real文件夹中的每一个子文件夹对应一个面容图片文件,子文件夹中每一个图像文件均为SLIC分割后的图像。

因此在构建生成器的时候,需要将x值的形状设置为(batch_size, K, width, heigh, RGB)=(32, 40, 300, 300, 3)(这里的K指的是SLIC算法的分割数),y值的形状应该为(batch_size, result)=(32, 1)。

在函数内部构建一个while True的循环,每一次使用yield关键词返回batch_size个numpy数组。

同时为方便训练时计算验证集何时完成一个循环,需要在每次返回时同时返回一个bool值finish_flag,在每次完成一个循环时返回为真,否则为假。在回调函数Validation中计算验证集Accuracy时每一次检测该bool值是否为真,如若为真则结束本次计算。

开始训练

经过上文生成的网络需要送入到类中fit方法进行训练,而本方法在训练之前会执行几种操作。

首先是使用keras.callbacks下的ModelCheckpoint方法创建检查点,并且分别调用generate_train方法与Validation进行生成训练生成器和验证用回调函数。

在执行正式训练之前,先检查对应的checkpoint保存目录,获取上次的训练情况,加载上次训练的情况后继续进行训练,同时设置ModelCheckpointValidation回调函数。

代码中还有一个keras.callbacks下的ReduceLROnPlateau类的调用,但是由于本程序中没有验证集的损失值,因此这个类即使被调用也不会产生任何效果,在程序运行时候会Warning提示该回调函数会被忽略,因此可以删除此回调函数也可继续保留。,已经修正为根据准确率调整。

另外:本程序使用了model.fit_generator方法进行训练,但是其内部也同样是调用的model.fit方法进行训练,并且会产生警告提示该方法即将被抛弃不可用。

保存

该类中的save方法将会保存两种内容,首先是将训练好的模型进行保存。另外会调用该类中的save_history方法对于训练过程进行保存。

Resnet

该类的目标为创造一个可以用于训练的模型,需要输入的值为图像的shape=(width, height, 3),以及需要冻结的层数,默认为前100层。

类内部调用了keras.applications.resnet50类的ResNet50方法,并且根据resnet的Fine-tuning方法构建相应模型,最后返回构建好的模型。

SLIC

本类的作用是根据输入的slic处理分割数和训练集验证集比例,调用skimage.segmentation类下的slic方法进行slic算法计算分隔结果,将计算好的分割的结果送入Split中进行图像分割。

本类中的重点是由于训练集合和验证集合的数据保存方式不同,因此需要分别对于两种情况分别处理。

Split

根据试验可知,slic算法的输出结果是一个由[0,x][0, x]区间组成的二维数组(当且仅当输入也为二维时),每一个数字标记的像素对应为一种色块,每个色块之间数值不会有重复。

因此在分割图像时,可以根据SLIC算法的这一特点入手进行编写。

根据要求,图片需要处理成当前色块显示原图像的部分,其他不属于当前色块的地方设置为0,因此遍历SLIC类传入的数组,一次对于数组中每一个值所对应的色块进行处理并且保存为图像。

此时,由于train和validation的数据需求不同,因此保存时候需要分别进行处理,训练集合保存为{}_{}.jpg的形式,而验证集保存为{}/{}.jpg的形式。(形式描述采用python的string.format形式)

Validation

Validation类是继承回调函数keras.callbacks.Callback的子类,其功能是在每一次epoch结束后,使用Net类中的generate_validation方法生成的验证集生成器计算val_accuracy,同时将每一次的epoch结果保存在数组中,在每一次训练结束后将该数组写入model.history.history

注:本类中初始化和最终保存时涉及到了loss值,但是在计算的时候没有计算loss值,因此写入到model.history.history中的val_loss值为空,调用可能会出错。

VGG

该类的目标为创造一个可以用于训练的模型,需要输入的值为图像的shape=(width, height, 3),以及需要冻结的层数,默认为前15层。

类内部调用了keras.applications.vgg16类的VGG16方法,并且根据resnet的Fine-tuning方法构建相应模型,最后返回构建好的模型。

XCeption

该类的目标为创造一个可以用于训练的模型,需要输入的值为图像的shape=(width, height, 3),以及需要冻结的层数,默认为101层。

类内部调用了keras.applications.xception类的Xception方法,并且根据resnet的Fine-tuning方法构建相应模型,最后返回构建好的模型。

SLIC_result

该文件夹将由程序自动生成,位于main.py文件的上一级目录中。

由于该程序设置的分割值为20~64,因此该SLIC_result文件夹的结构如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

├── 20
│   ├── train
│   │   ├── fake
│   │   │   ├── 0_0.jpg
│   │   │   ├── 0_1.jpg
│   │   │   └── …
│   │   └── real
│   │      ├── 0_0.jpg
│   │      ├── 0_1.jpg
│   │      └── …
│   └── validation
│   ├── fake
│      │   ├── 0
│      │   │  ├── 1.jpg
│      │   │  ├── 2.jpg
│      │   │  ├── ….jpg
│      │   ├── 1
│      │   └── …
│   └── real
│         ├── 0
│         │  ├── 1.jpg
│         │  ├── 2.jpg
│         │  ├── ….jpg
│         ├── 1
│         └── …
├── 21
├── 22
├── 23
└── …

由于文件树太过庞大,因此只使用分割数位20的地方进行部分展示,其他分割数以此类推。

config

config文件中的Config类承载了整个程序运行的所有可变常量,具体内容如下

常量名称常量值功能|含义
name_fakefake非真实图片的名称,会作用于文件夹命名等
name_realreal真实图片的名称,会作用于文件夹命名等
name_traintrain用于存放训练用图片的文件夹名称,会作用于文件夹命名等
name_validationvalidation用于存放验证用图片的文件夹名称,会作用于文件夹命名等
name_vgg16vgg使用VGG16网络时调用的名称
name_resnetresnet使用resnet网络时调用的名称
name_xceptionxception使用xception网络时调用的名称
frozen_layer_vgg1615使用vgg16网络时冻结的层数
frozen_layer_resnet100使用resnet网络时冻结的层数
frozen_layer_xception101使用xception网络时冻结的层数
original_face_pathresult/人脸区域原始图像
slic_result_path…/SLIC_result/超像素处理后的位置
checkpoint_path…/checkpoint/检查点的公共文件夹
model_path…/models/存放模型的公共文件夹
history_path…/history/存放历史记录的公共文件夹

另外,config文件中还保存了一些全局会使用到的静态方法

方法输入输出功能
path_existpath: 一个路径返回输入的路径判断路径是否存在,不存在创建这样一个路径,返回原来的路径目的是可以直接将函数放在路径中
is_image_file文件名bool值: 表示是否为图片判断一个文件是否为图片文件
get_image_file_listpath: 一个路径, is_train_data: bool值是否为训练文件返回一个所有图片路径的list根据给定路径获取路径下所有图片文件
path若干路径一个拼接好的路径将给定的内容拼接成为路径,并且替换掉//…/
get_child_folder一个路径路径下所有子文件夹的list根据给定的路径返回所有子文件夹的路径,不包含内部文件

Debug

本类中模仿C#的Debug类,重点是每次调用时,在输出的内容之前使用time.localtime自动加上系统时间。

main

Main类中调用pipeline.SLICpipeline.Net类进行调用,并且使用循环控制SLIC分割数,对于每个分割数分别调用三个网络进行训练。

总结

本文在发布之时,程序运行还没有结束,因此后续待运行结束后会进行更新。

参考及素材来源

利用screen和nohup让Linux服务器后台运行程序:https://blog.csdn.net/Cowry5/article/details/80630324

Python不定长参数(*args、**kwargs含义): https://zhuanlan.zhihu.com/p/70649428

Python replace()方法: https://www.runoob.com/python/att-string-replace.html

Keras自定义评估函数: https://www.cnblogs.com/shixiangwan/p/9041707.html

在Keras中使用数据生成器的详细示例: https://blog.csdn.net/qq_36763031/article/details/108278105

Python 正则表达式: https://www.runoob.com/python/python-reg-expressions.html

图片输入大小问题-keras/PIL.Image: https://blog.csdn.net/qq_23590921/article/details/80912350

改变输入形状尺寸的微调与Keras: https://www.kismetceyiz.com/2019/06/24/change-input-shape-dimensions-for-fine-tuning-with-keras/

Python将多个list合并为1个list: https://blog.csdn.net/roytao2/article/details/54180182

编写自己的回调函数: https://tensorflow.google.cn/guide/keras/custom_callback?hl=zh-cn

创建一个回调: https://keras-zh.readthedocs.io/callbacks/

判断python字典中key是否存在的: https://blog.csdn.net/tao546377318/article/details/52160942

生成器: https://www.liaoxuefeng.com/wiki/1016959663602400/1017318207388128

TensorFlow-Keras 2.自定义Loss与metrics: https://blog.csdn.net/BIT_666/article/details/108796486

Python之Lambda表达式和if not…else用法: https://blog.csdn.net/liudinglong1989/article/details/78728683

python中os.listdir( )函数读取文件夹下文件的乱序和排序问题: https://blog.csdn.net/qq_22227123/article/details/79903116