【论文复现,含代码】MatchNet: Unifying Feature and Metric Learning for Patch-Based Matching

图像匹配问题,历久而弥新。从上个世纪六十年代起,人们开始使用灰度匹配进行匹配,目前,图像匹配的研究重点为图像特征的提取与描述,初始匹配以及精准匹配。目前,深度学习在图像领域正如火如荼,传统的图像匹配问题应该更好的拥抱深度学习。

目前,将深度学习应用在图像匹配方面的论文依然较少,针对为数不多的几篇经典论文,我将尽力去复现这些论文的内容。

这篇博客要复现的是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的框架如下图所示:

【论文复现,含代码】MatchNet: Unifying Feature and Metric Learning for Patch-Based Matching_第1张图片

主要有如下几个成分:

  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. 如下图所示:

【论文复现,含代码】MatchNet: Unifying Feature and Metric Learning for Patch-Based Matching_第2张图片

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%左右。

【论文复现,含代码】MatchNet: Unifying Feature and Metric Learning for Patch-Based Matching_第3张图片

这篇博客的代码已经上传到CSDN上了,程序可以直接运行,有需要的可以下载,链接如下

https://download.csdn.net/download/weixin_42521239/12104339

如果没有积分的小伙伴也可以在评论中留下你的邮箱,我稍后发给你。

你可能感兴趣的:(TensorFlow,图像匹配,计算机视觉与航空航天)