图像匹配问题,历久而弥新。从上个世纪六十年代起,人们开始使用灰度匹配进行匹配,目前,图像匹配的研究重点为图像特征的提取与描述,初始匹配以及精准匹配。目前,深度学习在图像领域正如火如荼,传统的图像匹配问题应该更好的拥抱深度学习。
目前,将深度学习应用在图像匹配方面的论文依然较少,针对为数不多的几篇经典论文,我将尽力去复现这些论文的内容。
这篇博客要复现的是MatchNet:Unifying Feature and Metric Learning for Patch-Based Matching。对于图像匹配在深度学习方面的应用,MatchNet这篇文章可以说是鼻祖了,流传的也比较广。作者自身也公开了caffe的代码。传送门(可我不想用caffe,我喜欢keras,= ̄ω ̄=)。下面开始用自己的数据,使用keras复现match代码。
先复习一下MatchNet。
MatchNet的主要创新如下:
1. 提出了一个新的利用深度网络架构基于patch的匹配来明显的改善了效果;
2. 利用更少的描述符,得到了比state-of-the-art更好的结果;
3. 实验研究了该系统的各个成分的有效作用,表明,MatchNet改善了手工设计 和 学习到的描述符加上对比函数;
4. 最后,作者 release 了训练的 MatchNet模型。
MatchNet的框架如下图所示:
主要有如下几个成分:
A:Feature Network.
主要用于提取输入patch的特征,主要根据AlexNet改变而来,有些许变化。主要的卷积和pool层的两段分别有 preprocess layer 和 bottleneck layer,各自起到归一化数据和降维,防止过拟合的作用。激活函数:ReLU.
B:Metric Network.
主要用于feature Comparison,3层fc 加上 softmax,输出得到图像块相似度概率。
C:Two-tower structure with tied parameters
在训练阶段,特征网络用作“双塔”,共享参数。双塔的输出串联在一起作为度量网络的输入。The entire network is trained on labeled patch-pairs generated from the sampler to minimize the cross-entropy loss. 在预测的时候,这两个子网络A 和 B 方便的用在 two-stage pipeline. 如下图所示:
D:The bottleneck layer
用来减少特征表示向量的维度,尽量避免过拟合。在特征提取网络和全连接层之间,控制输入到全连接层的特征向量的维度。
E:The preprocessing layer
输入图像块预处理,归一化到(-1,1)之间。
F:train & predict:
交叉熵损失,SGD优化,由于数据正负样本的不平衡性,会导致实验精度的降低,本文采用采样的训练方法,在一个batchsize中,选择一半正样本,一半负样本进行训练。
总结:
1、MatchNet网络就是 siamese的双分支权重共享网络,与论文Learning to Compare Image Patches via Convolutional Neural Networks有共通之处。CNN提取图像块特征,FC学习度量特征的相似度。
2、本文指出,在测试阶段,可以将特征网络和度量网络分开进行,避免匹配图像时特征提取的重复计算。首先得到图像块的特征编码保存,之后输入度量网络中,计算得到N1*N2的得分矩阵。
接下来直接上干货!( •̀ ω •́ )✧
首先制作样本数据
imgPathA = './map/20181123.tif'
imgPathB = './map/20190315.tif'
首先得到两张位置一样的卫星图,分别是2018年与2019年谷歌卫星地图对同一地区的遥感图像。
容易发现,这两张图在清晰度和景物上,都存在许多变化,可以很好的应用于遥感图像匹配的样本数据构建中。
景物变化:
拍摄角度变化:
光照阴影变化:
论文中模型的输入为64*64,故依序将两张图片裁剪为64*64的小图片,步长为32
按如下代码分割图像,保存在./train/tempA和./train/tempB两个文件夹中,准备构建训练样本
count = 0
# 构建训练样本
for i in range(0, imgAsize[1] - 64, 32):
for j in range(0, imgAsize[0] - 64, 32):
train_tempA = imgA[j:(j+64), i:(i+64)]
train_tempB = imgB[j:(j+64), i:(i+64)]
cv2.imwrite("./img/train/tempA/img" + str(count) + ".jpg", train_tempA)
cv2.imwrite("./img/train/tempB/img" + str(count) + ".jpg", train_tempB)
count += 1
同理为构建测试样本分割图像,保存在./test/tempA和./test/tempB两个文件夹中:
count = 0
# 构建测试样本
for i in range(0, imgAsize[1] - 64, 100):
for j in range(0, imgAsize[0] - 64, 100):
train_tempA = imgA[j:(j+64), i:(i+64)]
train_tempB = imgB[j:(j+64), i:(i+64)]
cv2.imwrite("./img/test/tempA/img" + str(count) + ".jpg", train_tempA)
cv2.imwrite("./img/test/tempB/img" + str(count) + ".jpg", train_tempB)
count += 1
分别保存在对应的文件夹中并编码
由于两张卫星图都是对同一位置的遥感图像,其像素值对应的坐标是大致相同,由于我们的切分和编码方法也是严格按照顺序来的,所以我们可以直接构造正负样本数据。
训练样本:
这里注意:因为我们构建的正负样本的方式比较特殊,所以我们要保证正负样本的命名和数量是一致的
对于正样本来说:
tempA文件夹中的imgX.jpg和tempB文件夹中imgX.jpg文件是匹配的,而imgX.jpg和imgY.jpg是不匹配的 (X!=Y)。
也就是说./tempA/imgX.jpg和./tempB/imgX.jpg是一对匹配图像,label为1,作正样本。
对于负样本来说:
对于tempA文件夹中imgX.jpg,使用numpy.random.choice(pathTempB)从tempB文件夹中随机抽取一个构建负样本,这样从概率上讲,此时的imgX.jpg与imgY.jpg是不匹配的。
也就是说./tempA/imgX.jpg和./tempB/imgY.jpg不是一对匹配图像,label为0,作负样本。
具体看注释,注释很详细。
trainSample = []
trainLabel = []
pathTempA = os.listdir('./img/train/tempB/')
pathTempB = os.listdir('./img/train/tempA/')
# 这里注意:因为我们构建的正负样本的方式比较特殊,所以我们要保证正负样本的命名和数量是一致的
# 就是说tempA文件夹中的imgX.jpg和tempB文件夹中imgX.jpg文件是匹配的,而imgX.jpg和imgY.jpg是不匹配的 (X!=Y)
if pathTempA != pathTempB:
print("Data Error!!!")
return
# 遍历train文件夹中的数据,构建训练样本
for file in pathTempA:
# 分别从tempA文件夹中按顺序抽取imgX.jpg,组成匹配样本
imgLeft = Image.open('./img/train/tempA/' + file)
imgRight = Image.open('./img/train/tempB/' + file)
imgLeft = keras.preprocessing.image.img_to_array(imgLeft)
imgRight = keras.preprocessing.image.img_to_array(imgRight)
trainSample.append([imgLeft, imgRight])
trainLabel.append(1)
# numpy.random.choice(pathTempB)的意思是,对于tempA文件夹中imgX.jpg,从tempB文件夹中随机抽取一个构建负样本
# 从概率上讲,此时的imgX.jpg与imgY.jpg是不匹配的
imgRightFalse = Image.open('./img/train/tempB/' + numpy.random.choice(pathTempB))
imgRightFalse = keras.preprocessing.image.img_to_array(imgRightFalse)
trainSample.append([imgLeft, imgRightFalse])
trainLabel.append(0)
trainSample = numpy.array(trainSample)
trainLabel = numpy.array(trainLabel)
测试样本:
# 遍历test文件夹中的数据,构建训练样本
testSample = []
testLabel = []
pathTempA = os.listdir('./img/test/tempB/')
pathTempB = os.listdir('./img/test/tempA/')
if pathTempA != pathTempB:
print("Data Error!!!")
return
for file in pathTempA:
imgLeft = Image.open('./img/test/tempA/' + file)
imgRight = Image.open('./img/test/tempB/' + file)
imgLeft = keras.preprocessing.image.img_to_array(imgLeft)
imgRight = keras.preprocessing.image.img_to_array(imgRight)
testSample.append([imgLeft, imgRight])
testLabel.append(1)
imgRightFalse = Image.open('./img/test/tempB/' + numpy.random.choice(pathTempB))
imgRightFalse = keras.preprocessing.image.img_to_array(imgRightFalse)
testSample.append([imgLeft, imgRightFalse])
testLabel.append(0)
testSample = numpy.array(testSample)
testLabel = numpy.array(testLabel)
return trainSample, trainLabel, testSample, testLabel
搭建特征提取网络,具体网络结构可见MatchNet论文
def FeatureNetwork():
# add feature Net
inputShape = (64, 64, 1)
models = Sequential()
# conv0
models.add(keras.layers.Conv2D(filters=24, input_shape=inputShape, kernel_size=(7, 7), strides=(1, 1),
padding='same', activation='relu', use_bias=True,
kernel_initializer=keras.initializers.he_normal(seed=None),
bias_initializer='zeros'))
models.add(keras.layers.MaxPooling2D(pool_size=(3, 3), strides=(2, 2), padding='same'))
# conv1
models.add(keras.layers.Conv2D(filters=64, kernel_size=(5, 5), strides=(1, 1),
padding='same', activation='relu', use_bias=True,
kernel_initializer=keras.initializers.he_normal(seed=None),
bias_initializer='zeros'))
models.add(keras.layers.MaxPooling2D(pool_size=(3, 3), strides=(2, 2), padding='same'))
# conv2
models.add(keras.layers.Conv2D(filters=96, kernel_size=(3, 3), strides=(1, 1),
padding='same', activation='relu', use_bias=True,
kernel_initializer=keras.initializers.he_normal(seed=None),
bias_initializer='zeros'))
# conv3
models.add(keras.layers.Conv2D(filters=96, kernel_size=(3, 3), strides=(1, 1),
padding='same', activation='relu', use_bias=True,
kernel_initializer=keras.initializers.he_normal(seed=None),
bias_initializer='zeros'))
# conv4
models.add(keras.layers.Conv2D(filters=64, kernel_size=(3, 3), strides=(1, 1),
padding='same', activation='relu', use_bias=True,
kernel_initializer=keras.initializers.he_normal(seed=None),
bias_initializer='zeros'))
models.add(keras.layers.MaxPooling2D(pool_size=(3, 3), strides=(2, 2), padding='same'))
return models
搭建特征匹配网络,具体网络结构可见MatchNet论文
# 搭建特征匹配网络,具体网络结构可见MatchNet论文
def ClassiFilerNet(): # add classifier Net
input1 = FeatureNetwork()
input2 = FeatureNetwork()
# fci = merge([input1.output, input2.output])
fci = Concatenate()([input1.output, input2.output])
fc0 = Flatten()(fci)
fc1 = Dense(1024, activation='relu')(fc0)
fc2 = Dense(1024, activation='relu')(fc1)
fc3 = Dense(2, activation='softmax')(fc2)
models = Model(inputs=[input1.input, input2.input], outputs=fc3)
return models
按照MatchNet论文中的描述,使用交叉熵损失函数
学习率暂定为0.0001
matchnet.compile(loss='sparse_categorical_crossentropy',
optimizer=keras.optimizers.Adam(learning_rate=0.0001, amsgrad=False),
metrics=['acc'])
训练
matchnet.fit([trainSample[:, 0], trainSample[:, 1]], trainLabel, batch_size=20, epochs=20, verbose=1,
validation_data=([testSample[:, 0], testSample[:, 1]], testLabel), callbacks=[history])
训练的损失和准确度曲线,准确度能达到95%左右。
这篇博客的代码已经上传到CSDN上了,程序可以直接运行,有需要的可以下载,链接如下
https://download.csdn.net/download/weixin_42521239/12104339
如果没有积分的小伙伴也可以在评论中留下你的邮箱,我稍后发给你。