原创

: SPAN: Spatial Pyramid Attention Network for Image Manipulation Localization

SPAN: Spatial Pyramid Attention Network for Image Manipulation Localization

SPAN: Spatial Pyramid Attention Network for Image Manipulation
Localization

ECCV 2020

代码链接

https://github.com/ZhiHanZ/IRIS0-SPAN/blob/d8e4241f151ef2f40eacbb970fe5e3f531c6a4b4/README.md

不过这个作者提供的权重是多gpu的,准确来说是2个GPU,没有这个资源用他这个训练好的模型。受不了。这个作者也是不厚道。使用的tensorflow的框架。
这个代码整了两天没跑起来。生气


这个作者的不厚道有2点


网络框架

在这里插入图片描述
这个 pre-trained feature extracion 使用的是mantra net 的预训练模型,训练时候冻结。

后接5个self attention block.

这个self attention有些玄妙。

self attention block

在这里插入图片描述
不知道有没有理解错。
只需要理解成特殊的卷积层即可。
输入输出的矩阵大小没有变化。

中间层把D通道转为了9D通道(假设了n=1,为8邻域。)
9D为自身以及周围的8个点。为原本的通道数*9.

这个玄妙的地方大概在于 能够把一个像素点为中心的周围(2n+1)*(2n+1)邻域之内的所有像素的信息全部接收到一个对应点。

本来这东西是NLP领域提出的,不使用RNN,就得到时间顺序信息。图像和语言还是有些差别的。语言中的语言顺序和图像的邻域或许是等价的吧。

这是抄的这个作者的tensorflow 代码

基本和此图的结果相同,可能卷积层的数量稍微有点出入。
在这里插入图片描述

self attention 代码

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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
from keras.engine.topology import Layer
import tensorflow as tf
import keras
from matplotlib.image import imread, imsave
from keras import backend as K


class PixelAttention(Layer):
def __init__(self, kernel_range=[3, 3], shift=1, ff_kernel=[3, 3], useBN=False, useRes=False, **kwargs):
self.kernel_range = kernel_range # should be a list
self.shift = shift
self.ff_kernel = ff_kernel
self.useBN = useBN
self.useRes = useRes
super(PixelAttention, self).__init__(**kwargs)

def build(self, input_shape):
D = input_shape[-1]
n_p = self.kernel_range[0] * self.kernel_range[1]
self.K_P = self.add_weight(name='K_P', shape=(1, 1, D, D * n_p),
initializer='glorot_uniform',
trainable=True)
self.V_P = self.add_weight(name='V_P', shape=(1, 1, D, D * n_p),
initializer='glorot_uniform',
trainable=True)
self.Q_P = self.add_weight(name='Q_P', shape=(1, 1, D, D),
initializer='glorot_uniform',
trainable=True)

self.ff1_kernel = self.add_weight(name='ff1_kernel',
shape=(3, 3, D, D),
initializer='glorot_uniform', trainable=True)
self.ff1_bais = self.add_weight(name='ff1_bias',
shape=(D,), initializer='glorot_uniform', trainable=True)
self.ff2_kernel = self.add_weight(name='ff2_kernel',
shape=(3, 3, D, 2 * D),
initializer='glorot_uniform', trainable=True)
self.ff2_bais = self.add_weight(name='ff2_bias',
shape=(2 * D,), initializer='glorot_uniform', trainable=True)

self.ff3_kernel = self.add_weight(name='ff3_kernel',
shape=(3, 3, 2 * D, D),
initializer='glorot_uniform', trainable=True)
self.ff3_bais = self.add_weight(name='ff3_bias',
shape=(D,), initializer='glorot_uniform', trainable=True)

super(PixelAttention, self).build(input_shape)

def call(self, x):
h_half = self.kernel_range[0] // 2
w_half = self.kernel_range[1] // 2
_, _, _, D = x.shape
s = K.shape(x)
x_k = tf.nn.conv2d(input=x, filter=self.K_P, padding="SAME", strides=[1, 1, 1, 1])
x_v = tf.nn.conv2d(input=x, filter=self.V_P, padding="SAME", strides=[1, 1, 1, 1])
x_q = tf.nn.conv2d(input=x, filter=self.Q_P, padding="SAME", strides=[1, 1, 1, 1])
paddings = tf.constant(
[[0, 0], [h_half * self.shift, h_half * self.shift], [w_half * self.shift, w_half * self.shift], [0, 0]])
x_k = tf.pad(x_k, paddings, "CONSTANT")
x_v = tf.pad(x_v, paddings, "CONSTANT")
mask_x = tf.ones(shape=(s[0], s[1], s[2], 1))
mask_pad = tf.pad(mask_x, paddings, "CONSTANT")

k_ls = list()
v_ls = list()
masks = list()

c_x, c_y = h_half * self.shift, w_half * self.shift
layer = 0
for i in range(-h_half, h_half + 1):
# 每一个点的 8邻域 的8个点 根据w_half 决定邻域大小 共 (2n+1)^2 大小的块 每隔像素的邻域点 分到不同的通道中 最后 达到了 h*w *c*(2n+1)^2大小
# 对于每个点 都是 ((2n+1)^2 *d,1) ->1的一个映射 每个特定位置中所有通道的所有邻域 到 单个点
for j in range(-w_half, w_half + 1):
k_t = x_k[:, c_x + i * self.shift:c_x + i * self.shift + s[1],
c_y + j * self.shift:c_y + j * self.shift + s[2], layer * D:(layer + 1) * D]
k_ls.append(k_t)

v_t = x_v[:, c_x + i * self.shift:c_x + i * self.shift + s[1],
c_y + j * self.shift:c_y + j * self.shift + s[2], layer * D:(layer + 1) * D]
v_ls.append(v_t)

_m = mask_pad[:, c_x + i * self.shift:c_x + i * self.shift + s[1],
c_y + j * self.shift:c_y + j * self.shift + s[2], :]
masks.append(_m)
layer += 1
m_stack = tf.stack(masks, axis=3, name="mask")
m_vec = tf.reshape(m_stack, shape=[s[0] * s[1] * s[2], self.kernel_range[0] * self.kernel_range[1], 1])
k_stack = tf.stack(k_ls, axis=3, name="k_stack")
v_stack = tf.stack(v_ls, axis=3, name="v_stack")
k = tf.reshape(k_stack, shape=[s[0] * s[1] * s[2], self.kernel_range[0] * self.kernel_range[1], D])
v = tf.reshape(v_stack, shape=[s[0] * s[1] * s[2], self.kernel_range[0] * self.kernel_range[1], D])
q = tf.reshape(x_q, shape=[s[0] * s[1] * s[2], 1, D])

alpha = tf.nn.softmax(tf.matmul(k, q, transpose_b=True) * m_vec / 8, axis=1) # s[0]*s[1]*s[2]*9 #softmax
__res = tf.matmul(alpha, v, transpose_a=True) # a*v
_res = tf.reshape(__res, shape=[s[0], s[1], s[2], D])
if self.useRes:
t = x + _res
else:
t = _res
if self.useBN:
t = keras.layers.BatchNormalization(axis=-1)(t)
_t = t
t = tf.nn.relu(
tf.nn.conv2d(input=t, filter=self.ff1_kernel, padding='SAME', strides=[1, 1, 1, 1]) + self.ff1_bais)
t = tf.nn.relu(
tf.nn.conv2d(input=t, filter=self.ff2_kernel, padding='SAME', strides=[1, 1, 1, 1]) + self.ff2_bais)
t = tf.nn.relu(
tf.nn.conv2d(input=t, filter=self.ff3_kernel, padding='SAME', strides=[1, 1, 1, 1]) + self.ff3_bais)
if self.useRes:
t = _t + t
if self.useBN:
res = keras.layers.BatchNormalization(axis=-1)(t)
else:
res = t
# self attention 层之后的特征图和 输入图像的大小和通道数 相同
return res

def compute_output_shape(self, input_shape):
return input_shape


主体代码 从此处可以看出信息流

虽然不熟tensorflow,但是基本看得懂

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def get_model_1010_resize(self,layers_steps=[1,3,9,27,81]):
# todo 把这个函数改为pytorch model
# get_model_1010_resize 实际使用的model 使用自注意力机制
img_in = Input(shape=(None,None,3), name='img_in')
Featex=self.get_Featex()
rf=Featex(img_in)
resize = Lambda( lambda t : tf.image.resize(t, (224, 224)), name='resize')( rf )
rf = Conv2D( 32, (1,1), activation=None, use_bias=False, kernel_constraint = unit_norm( axis=-2 ),
name='outlierTrans_new', padding = 'same' )(resize)
t=rf
for step in layers_steps:
# 循环5次
t=pa.PixelAttention(shift=step,useBN=False, useRes=True)(t)

pred_out=self.Last_Layer_0725(t)
model=Model( inputs=img_in, outputs=pred_out, name='sigNet')
model.load_weights(self.weight_file,by_name=True)
return model

Last_Layer_0725

1
2
3
4
5
6
7
8
9
# Last_Layer_0725
def Last_Layer_0725(self,x):
t=Conv2D( 32, (5,5), activation='relu', name='final_1', padding = 'same' )(x)
t=Conv2D( 16, (5,5), activation='relu', name='final_2', padding = 'same' )(t)
t=Conv2D( 8, (5,5), activation='relu', name='final_3', padding = 'same' )(t)
t=Conv2D( 4, (5,5), activation='relu', name='final_4', padding = 'same' )(t)
t=Conv2D( 1, (5,5), activation='sigmoid', name='final_5', padding = 'same' )(t)
return t

get_Featex

1
2
3
4
5
6
7
        
def get_Featex(self,trainable=False):
type_idx = self.IMC_model_idx if self.IMC_model_idx < 4 else 2
Featex = modelCore.create_featex_vgg16_base(type_idx)
Featex.trainable=trainable
return Featex

结果