SA-FAS数据集预处理代码整理

前言

本文将整理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帧左右的图像,而方法中定义的max1000,一方面可能是考虑到了其他数据集之间视频时长的不同,另一方面,代码命名文件的格式遵从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格式。

可视化来源[4]

转换格式后开始检测是否存在旋转,在本数据集中,实际不存在视频的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_modeFalse的,永远只会返回第一张图,具体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

最后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

  1. https://github.com/sunyiyou/SAFAS
  2. https://docs.opencv.org/3.4/d8/dfe/classcv_1_1VideoCapture.html
  3. https://kkroening.github.io/ffmpeg-python/
  4. https://blog.csdn.net/zhang_cherry/article/details/88951259
  5. https://github.com/timesler/facenet-pytorch/issues/114
  6. https://uysim.medium.com/face-embedding-and-what-you-need-to-know-a623c7111b5
No Comments

Send Comment Edit Comment


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
Previous