安全AI挑战者计划第五期:伪造图像的篡改检测 Rank21分享

0 队伍简介

队伍名称: 猛狗哭泣

终榜排名: 21/1470

公榜排名: 31/1470

Code Repo: https://github.com/AshinWang/tianchi-competition-5-manipulation-detection/

1 比赛简介

比赛本质上就是图像分割。

大赛名称:安全AI挑战者计划第五期:伪造图像的篡改检测-赛道2

官方简介: https://tianchi.aliyun.com/competition/entrance/531812/introduction 赛道2(10月12日10:00AM (UTC+8)开启,赛道1报名选手无需重复报名)伪造图像的对抗攻击比赛的赛道1——攻击比赛已经接近尾声,很多高质量的P图不但骗过人眼,还成功骗过我们提供的4个经典检测模型,那是否就真的是“魔高一丈”(反取证技术)呢?我们的对抗攻击比赛开始进入赛道2——检测比赛将在10月12日10:00AM (UTC+8)拉开帷幕!设计出“火眼金睛”(检测算法),把别人的“挖坑”(篡改区域)一一识别出来。区别于以往的图像取证比赛专注于自然内容图像,我们比赛采用的数据为大量伪造的证书文档类图像。任务是通过提供的训练集学习出有效的检测算法,对测试集的伪造图像进行篡改定位。为了更好的评价选手的检测定位效果,我们设计了全面的得分计算准则。

A 数据形式

  • 数据包括训练集和测试集,训练集有1500张JPEG图像及对应mask(分辨率与原图保持一致,像素值0表示该像素标识为未篡改,像素值1表示该像素标识为篡改),JPEG图像的EXIF信息均被擦除,除部分无后处理外,其它可能经过裁边、平滑、下采样、社交工具传输(没有使用组合方式);测试集有1500张JPEG图像,处理过程与训练集一致;允许使用集外数据进行训练学习。
  • 参赛者提交数据时,利用我们提供的python程序生成mask,对1500张mask图像打包上传。
  • 篡改图像可能包括如splicing、copy-move、object removal等任意操作,部分进行后处理(JPEG压缩、重采样、裁剪边缘等)。
  • 不需要考虑图像的元数据(已经被擦除)。
  • 示例

B 评估标准

3 解决方案

A 运行环境

整个实验在 Google Colab 下完成,应主办方提交要求改写了 .py 方式,并通过了本地测试。

主要依赖库

  • tensorflow ==2.1.0
  • keras == 2.3.0
  • segmentation-models==1.0.1

NoteBook

  • Colab Pro

    • GPU Tesla v100 16G

    • RAM 25G

.py

  1. 安装依赖库

    pip3 install -r requirements.txt

  2. 训练

    $ cd code
    $ ./trian.sh

  3. 测试

    $ cd code
    $ ./run.sh

B 数据处理

  • 将图像数据准换为 np 形式的正方形矩阵,其中

    • trian resize(512, 512, 3)

    • train_mask resize(512, 512, 1)

    • test resize(512, 512, 3)

图像的平均长宽在 1200 左右,一开始 resize 到 1024*1024*3,直接爆内存(25 GiB), 512*512*3 的尺寸在 Colab 上基本上达到了训练极限,尽管 256*256 占用小了很多,但是训练模型的效果不好,最终还是将训练集 resize 为 512*512*3

C 图像增强

  • horizontal_flip
  • vertical_flip
1
2
3
4
# Data Generator and augmentation
data_gen_args = dict(horizontal_flip=True,vertical_flip=True)
image_datagen = ImageDataGenerator(**data_gen_args)
mask_datagen = ImageDataGenerator(**data_gen_args)

D Metric

1
2
3
4
5
6
# Dice_Coeff or F1 score
def metric(y_true, y_pred):
y_true_f = K.flatten(y_true)
y_pred_f = K.flatten(y_pred)
intersection = K.sum(y_true_f * y_pred_f)
return (2. * intersection + 1) / (K.sum(y_true_f) + K.sum(y_pred_f) + 1)

E 模型训练

使用公开的分割模型 Repo: https://github.com/qubvel/segmentation_models

Segmentation model = UNet

Backbone = efficientnetb3

Type Names
VGG ‘vgg16’ ‘vgg19’
ResNet ‘resnet18’ ‘resnet34’ ‘resnet50’ ‘resnet101’ ‘resnet152’
SE-ResNet ‘seresnet18’ ‘seresnet34’ ‘seresnet50’ ‘seresnet101’ ‘seresnet152’
ResNeXt ‘resnext50’ ‘resnext101’
SE-ResNeXt ‘seresnext50’ ‘seresnext101’
SENet154 ‘senet154’
DenseNet ‘densenet121’ ‘densenet169’ ‘densenet201’
Inception ‘inceptionv3’ ‘inceptionresnetv2’
MobileNet ‘mobilenet’ ‘mobilenetv2’
EfficientNet ‘efficientnetb0’ ‘efficientnetb1’ ‘efficientnetb2’ ‘efficientnetb3’ ‘efficientnetb4’ ‘efficientnetb5’ efficientnetb6’ efficientnetb7’

All backbones have weights trained on 2012 ILSVRC ImageNet dataset (encoder_weights='imagenet').

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Unet + efficientnetb3
model_name = 'Unet'
backbone_name = 'efficientnetb3'
model = Unet(backbone_name, classes=1, activation='sigmoid')
model.compile(optimizer=Adam(), loss="binary_crossentropy", metrics=metric)

seed = 42
bs = 32 # BacthSize
nb_epochs = 100 # epoch

image_generator = image_datagen.flow(X_train, seed=seed, batch_size=bs, shuffle=True)
mask_generator = mask_datagen.flow(y_train, seed=seed, batch_size=bs, shuffle=True)

# Just zip the two generators to get a generator that provides augmented images and masks at the same time
train_generator = zip(image_generator, mask_generator)

results = model.fit_generator(train_generator, steps_per_epoch=(spe), epochs=nb_epochs,validation_data=(X_valid, y_valid),callbacks=[save,lr_schedule,reduce_lr])

F 模型预测

预测测试集的图像时,将 test 转化为 512*512*3 的图像,再经过模型篡改区域预测后得到 512*512*1的 test_mask,再将 mask resize 到图像本身的长宽。这样 resize 来回应该会影响预测效果吧!!!但不 resize 会报错。_

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
36
37
38
39
40
41
TEST_MASK_PATH = '../prediction_result/images/'
predicted_test = model.predict(X_test)

# Save test mask
print('Get img name and path')
each_test_name = []
each_test_path = []

for i in tqdm(list({i.split('.')[0] for i in os.listdir(TEST_PATH)})):
i = i.split('.')[0]
each_test_path.append(TEST_PATH + '{}.jpg'.format(i))
each_test_name.append(i)

each_test_path.remove(each_test_path[0])
each_test_name.remove(each_test_name[0])
print(each_test_path[0])
print(each_test_name[0])

# Resize mask
print('Get img height and width')
h = []
w = []
for filename in tqdm(each_test_path):
temp = Image.open(filename)
h.append(temp.height)
w.append(temp.width)

print('Resize and save img')
for index in tqdm(range(0,1500)):
pred = np.squeeze(predicted_test[index])
plt.imsave('pred_mask.png',pred)
im_gray = cv2.imread('pred_mask.png', cv2.IMREAD_GRAYSCALE)
(thresh, im_bw) = cv2.threshold(im_gray, 220, 255, cv2.THRESH_BINARY | cv2.THRESH_OTSU)

im_bw_t = Image.fromarray(im_bw)
im_bw_t.save(TEST_MASK_PATH+'{}.png'.format(each_test_name[index]))

im_bw_t_n = Image.open(TEST_MASK_PATH+'{}.png'.format(each_test_name[index]))
im_bw_t_nn = im_bw_t_n.resize(( w[index], h[index]),Image.ANTIALIAS)
im_bw_t_nn.save(TEST_MASK_PATH+'{}.png'.format(each_test_name[index]))
print('DONE! Save mask img!')

4 总结

本次比赛,由于时间和硬件资源不够,仅在 Colab 上采用了 UNet + efficientnetb3 进行了实验。还有许多可以提升的地方,例如

  • Backbone(inceptionresnetv2, efficientnetb7)

  • 损失函数

  • 图像扩增

  • 打伪标签

    在训练集中,未篡改的图像可通过 mask 来判断。把测试集中的一些明显未篡改的图像,打上全黑的 mask,加入到训练集。一是 Colab 内存不够,二是害怕判定作弊,没加。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    for i,img in tqdm(zip(list({i.split('.')[0] for i in os.listdir(TRAIN_PATH)}), range(0,len(os.listdir(TRAIN_PATH))))):

    image= cv2.imread(train_data[img])
    mask = cv2.imread(mask_data[img],0)

    if cv2.countNonZero(mask) == 0:
    cv2.imwrite(path+'train_ok/{}.jpg'.format(i), image)
    cv2.imwrite(path+'train_ok_mask/{}.png'.format(i), mask)
    else: pass
    print(len(path+ 'train_ok/'), len(path+ 'train_ok_mask/'))
  • 训练策略

    • epoch 和 学习率
    • 低分辨率 –> 高分辨率
  • 测试集不 Resize

Top 选手经验

Rank 3

2021-01-07 18:00 看了 Rank 3 的思路,数据增强的思路真棒!深度学习,数据为王!!!

Post: https://tianchi.aliyun.com/forum/postDetail?postId=161067

repo: https://github.com/HighwayWu/Tianchi-FFT2?spm=5176.12282029.0.0.59d727c1TpgQpA

我们在比赛开始阶段,首先使用了一个简单的模型(U-Net)在原始训练集上进行训练,结果发现在测试集上的效果很差(约400分)。
于是接下来先从模型架构入手,尝试了多种不同的模型,如ResNet、VGG、Xception、FCN、HRNet、Nested-UNet、VGG-UNet等等。
最终发现效果最好的是使用Se-Resnext50为编码器、以及融合了SCSE感知模块的U-Net(详见训练流程3.1)(约550分)。
值得一提的是,SCSE感知模块效果显著。主要是因为其进行的”感知”操作能够很好的让模型对篡改区域的特征进行re-weighting。
当然,我们也尝试过自行设计Attention Module并嵌入其中,但发现效果微乎其微。就没有继续尝试修改模型架构,接下来从训练数据入手。
首先观察到训练集中大量证书类数据是由同一张图片裁剪而得,所以直接使用其进行训练极容易导致过拟合。故我们制作了一批新的证书类数据并加入训练(约提升100150分)。
其次观察到训练集中书籍封面类图片全部是真实的,而测试集中大量书籍封面类图片为篡改的。故我们也制作了一批新的书籍封面类书籍并加入训练(约提升50
80分)。
最后,将训练集进行不同的划分(如fold 1~4),然后分别训练模型;最终将4个模型的结果融合(约提升80分)。

5 更多参考技巧