前言
本文将整理SA-FAS[1]官方代码实现中的数据预处理部分,以便用于在其他项目当中(如GAC-FAS),按照相同的数据格式进行测试。
预先准备
文件
预处理代码在项目中是./preprocess.py
环境
facenet_pytorch(已经有torch环境了可以直接git clone这个项目)
opencv-python
ffmpeg-python(注意,不是python-ffmpeg)
numpy
math
torchvision
PIL
ylib.scipy_misc(我找不到能用的ylib,另外scipy_misc方法太老了,所以换用了scipy建议的imageio)
必要项目结构
代码主类(__main__)中定义了四种数据集:
OSLU(OULU-NPU)
MSU(MSU-MFSD)
CASIA(CASIA-MFSD)
REPLY(Idiap Replay Attack)
路径分别为:
datasets/FAS/OULU-NPU/
datasets/FAS/MSU-MFSD/
datasets/FAS/CASIA_faceAntisp/
datasets/FAS/Replay/
以处理OULU为例
主类中首先调用了run_oulu 方法
预处理文件夹创建
首先创建了一个用于处理后输出的文件夹:{数据集路径}/prepocess,也就是:datasets/FAS/OULU-NPU/preposess
视频读取
file_list = glob.glob(rootpath + "**/*.avi", recursive=True)
随后读取了数据集下所有以.avi结尾的文件,存入了file_list列表里,格式是:datasets/FAS/OULU-NPU/**/*.avi
随后进入for loop循环,遍历刚刚拿到的文件,每次拿取出视频的文件名,存入video_prefix变量
for i, filepath in enumerate(file_list):
video_prefix = filepath.split("/")[-1].split('.')[0]
例如:“datasets/FAS/OULU-NPU/Dev_files/6_3_23_3.avi”,则存入:“6_3_23_3”
分类
接下来判断视频是真实(live)面部,还是伪造(spoof)面部
if "1.avi" in filepath:
live_or_spoof = 'live'
else:
live_or_spoof = 'spoof'
代码的判断方法是根据视频的尾缀部分,是否为“_1.avi”,是则为真实,不是则为虚假。
这个逻辑可以根据数据集的分布方式来理解,在OULU数据集中,每一组视频的第一个视频,也即”_1.avi”是真实面部的视频,而“_2.avi”开始则是以各种方式予以伪造的面部,诸如纸张或视频录制等。
而另一个分布则是数据集的训练集测试集划分,OULU分为了”train”,”test”和”dev”
if "/Train_files/" in filepath:
split = 'train'
elif "/Test_files/" in filepath:
split = 'test'
elif "/Dev_files/" in filepath:
split = 'dev'
命名规范
代码定义了一个split变量,用于记录数据集类型,然后构建了一个用于辨别数据集内容的命名规范:
name = f"oulu_{split}_{live_or_spoof}_{video_prefix}"
例如:oulu_train_live_1_1_01_1
导出
最后将该文件存入输出路径下,另外很贴心的设计了如果重复保存,则跳过的判断:
savepath = os.path.join(outpath, name)
if os.path.exists(savepath) and len(os.listdir(savepath)) > 10:
continue
else:
os.makedirs(savepath, exist_ok=True)
print(f"make {savepath}")
接下来则开始处理视频文件,每20个汇报一次,另外有一个meta列表用于存储命名:
proposess_video(filepath, savepath)
if i % 20 == 0:
print(f"processed {i} / {len(file_list)}")
meta_info_list.append((name, live_or_spoof, split))
视频处理
在proposess_video方法中,主要进行视频处理,将先前规范好命名的文件传入该方法中。
代码用了两处读取视频文件
v_cap = cv2.VideoCapture(video_path)
rotateCode = check_rotation(video_path)
第一处比较好理解,是opencv库中将视频变为以单帧进行处理的方法[2],第二处是代码在另一处定义的方法,先看该方法。
check_rotation
# this returns meta-data of the video file in form of a dictionary
meta_dict = ffmpeg.probe(path_video_file)
首先代码调用ffmpeg的probe工具,以json格式返回视频的元信息,并存储到字典当中[3]。
如方法名一般,该方法主要是检测视频当中是否存在旋转,而检测的方式是读取元信息中是否存在旋转的信息
if 'tags' in meta_dict['streams'][0] and 'rotate' in meta_dict['streams'][0]['tags']:
if int(meta_dict['streams'][0]['tags']['rotate']) == 90:
rotateCode = cv2.ROTATE_90_CLOCKWISE
elif int(meta_dict['streams'][0]['tags']['rotate']) == 180:
rotateCode = cv2.ROTATE_180
elif int(meta_dict['streams'][0]['tags']['rotate']) == 270:
rotateCode = cv2.ROTATE_90_COUNTERCLOCKWISE
return rotateCode
else:
return -1
meta_dict['streams'][0]中就正是视频流的元信息,当其中的tags下存在rotate属性,则判断视频对旋转做了修改,而下方则是对于旋转角度的判断,rotateCode变量由opencv对旋转角度的Code进行定义:
- 90度时为:0
- 180度时为:1
- 270(逆时针90度)度时为:2
如果没有旋转,则返回-1
跳出控制
看完对视频的旋转检查,我们可以回到先前的处理部分,可以先看看处理的判断式是如何确定视频已经处理完毕的
frame_id = 0
while True:
success, frame = v_cap.read()
if not success:
break
if frame_id > max:
break
if frame_id % sample_ratio == 0:
首先定义了一个frame_id的变量,初始化为0,后续每处理一帧图像都会将其+1
整个代码由一个while循环控制,一旦触发以上两种判断,都会直接退出循环,也即处理完毕了视频。
而方法定义只有当frame_id % sample_ratio == 0时,才对视频进行处理,其他时候仅将frame_id+1
本方法中,定义的sample_ratio为5,也即采样率为5FPS。
接下来看跳出条件
第一个跳出条件是if not success,说明当opencv无法从视频中读取数据时,则跳出
第二个跳出条件则比较图省事,我们已知OULU数据集是以30FPS进行录制的,每个视频固定为5秒钟,也即整个方法基本上最多只会处理150帧左右的图像,而方法中定义的max是1000,一方面可能是考虑到了其他数据集之间视频时长的不同,另一方面,代码命名文件的格式遵从0000开始,总共为四位数,所以1000结束也是合理的,虽然…按理说也能9999?
比较人性化的方法是,直接调用先前拿到的meta_dict下的['nb_frames'],可以直接取得视频的frames数量。不过其实无所谓,因为正常都是直接从if not success这一判断跳出的。
旋转图像
倘若进入了第三条判断式,则开始正式对数据进行处理,首先是作了视频是否被旋转了的判断
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
if rotateCode > -1:
frame = cv2.rotate(frame, rotateCode)
首先作视频格式的初始化,从BGR变为RGB格式,熟悉CV的应该知道这算是比较常见的一步初始化转换,因为imread方法进来的是BGR格式而非RGB格式。
转换格式后开始检测是否存在旋转,在本数据集中,实际不存在视频的meta有“旋转”的属性,所以均为-1。
先前已知,-1为无旋转,0/1/2分别对应旋转角度,因为0/1/2本身就是cv2的定义,所以直接调用cv2.rotate即可完成旋转。
至于为何需要按照元数据进行旋转,主要原因是视频属性标注了该视频旋转了xx度,但是CV处理帧图像时是不会读取该元数据的,处理图像仍然还是按照0度进行处理,所以需要让每一帧图像都对齐视频的属性,按照视频属性定义的旋转度数进行旋转,再对图像进行处理。
面部识别
接下来是对面部信息进行识别
# Detect face
batch_boxes, batch_probs, batch_points = mtcnn.detect(frame, landmarks=True)
if batch_boxes is None:
continue
cropped_tsn = mtcnn.extract(frame, batch_boxes, None)
if cropped_tsn is None:
continue
img_embedding = resnet(cropped_tsn.cuda().unsqueeze(0))
本预处理代码采用的是facenet_pytorch中的MTCNN(Multi-task Cascaded Convolutional Networks),该模型支持人脸识别和人脸对齐。
经过MTCNN的检测后,可以拿到三个变量:batch_boxes, batch_probs, batch_points,分别是:边界框,置信度和关键点。
随后使用mtcnn.extract(frame, batch_boxes, None)将人脸部分进行裁剪,命名为cropped_tsn
而MTCNN遇到Tensor时,batch_mode是False的,永远只会返回第一张图,具体BUG可见:issue[5]
不过本代码也只需要提取一张人脸,考虑到其选择默认为第0张图,而置信度是否越高图片在batch中越靠前?尚不知细,需要进一步的测试
以数据集中某张图为例,面部检测裁剪后为:
之后使用InceptionResnetV1对裁剪后仅有面部的数据进行面部识别,得到一个512长度的人脸嵌入(face embedding)数据
对于人脸嵌入的相关介绍,可参考这篇文章[6]
存储
prob = batch_probs[0]
box = batch_boxes[0].astype(int)
points = batch_points[0].astype(int)
box = np.maximum(box, 0)
points = np.maximum(points, 0)
cropped = frame[int(box[1]):int(box[3]), int(box[0]):int(box[2])]
到此,拿到了所有所需的数据,本处假定了人脸为批次当中第一个识别到的面部的数据,故均为[0],取其相对应的置信度,边界框,关键点。随即对边界框和各点向下取整(286.9798278808594==>286)
然后将box中的值放回frame当中,其为一个形状为(H*W*C),被裁剪好的人脸,最后将其存入一个新的变量cropped,这样不影响原先frame的内容
最后frame就是最终的图像,将其保存进目录当中
imsave(os.path.join(savepath, f'org_{frame_id:04d}.jpg'), frame)
imsave(os.path.join(savepath, f'crop_{frame_id:04d}.jpg'), Image.fromarray(cropped))
info_dict = {
'box': box,
'detect_prob': prob,
'points': points,
'face_embed': img_embedding.data.cpu().numpy(),
'frame_id': frame_id
}
np.save(os.path.join(savepath, f'infov1_{frame_id:04d}.npy'), info_dict, allow_pickle=True)
其分别存储原始图像(frame),裁剪图像(cropped),还有一份构成裁剪图像的原始数据,命名为info_dict,并在导出时存储为npy格式。
尾言
其他三个数据集的处理过程基本一致,仅在目录上面作了不同的判断和命名规范,理解OULU的处理流程后便不难理解其他数据集的处理了,本处不再过多赘述。
Ref
- https://github.com/sunyiyou/SAFAS
- https://docs.opencv.org/3.4/d8/dfe/classcv_1_1VideoCapture.html
- https://kkroening.github.io/ffmpeg-python/
- https://blog.csdn.net/zhang_cherry/article/details/88951259
- https://github.com/timesler/facenet-pytorch/issues/114
- https://uysim.medium.com/face-embedding-and-what-you-need-to-know-a623c7111b5