
Tony Ng 是 帝國理工學院 MatchLab 的博士生。他的導師是 Dr. Krystian Mikolajczyk,聯合導師是 Dr. Vassileios Balntas。他的研究興趣集中在利用深度學習和經典多檢視幾何來改進視覺定位。
影像檢索是一個長期存在的計算機視覺問題。它被用於影像索引,例如谷歌的影像搜尋。它也是許多視覺定位方法的起點。

我最喜歡的類比如下:假設你有一塊隨機的拼圖,它屬於一百套拼圖板 - 視覺定位的目標是將這塊拼圖 (查詢影像的姿態) 放在正確的位置,而影像檢索是確定這塊拼圖實際屬於哪套一百個拼圖板的第一步。
在這篇博文中,我將首先介紹深度影像檢索的最新進展。此外,我將討論我最近在使用二階資訊和自注意力來改進這些技術的最新工作,以及如何使用 OpenCV 互動式地視覺化結果。希望你會覺得它有用!
兩種主要的深度影像檢索技術型別
為了檢索影像,我們必須計算影像之間的相似性。為了有效地對大量影像之間的相似性進行排名,我們首先必須用單個向量表示每個影像,這被稱為全域性描述符。想想你如何能夠非常容易地識別出兩個人臉是否是同一個人,即使你以前從未見過這些臉?這是你的大腦天生具備的“描述”臉部的能力,而不必分析臉部的每一個細節。全域性描述符在某種程度上與之非常相似,正如其名稱所暗示的,以壓縮的方式描述影像。事實上,是 這篇著名的臉部識別論文 使 三元組損失 聲名大噪,這種損失如今幾乎被所有影像檢索方法使用!
在深度學習流行之前,影像檢索主要是基於區域性特徵的。區域性特徵或區域性描述符與全域性描述符類似,也用向量表示影像。然而,區域性描述符只描述影像的一個小區域,這個區域被稱為塊。由於影像描述的早期工作主要集中在區域性描述符上,例如著名的 SIFT,它們透過聚合這些區域性描述符來生成全域性描述符,例如 詞袋模型。
隨著深度學習越來越流行,學習到的區域性特徵也被聚合形成更穩健的全域性描述符,由 Arandjelović 等人 和 Noh 等人 的作品開創。我們稱這種方法為區域性聚合。
深度學習對影像檢索的另一個影響是另一類全域性描述符,我們稱之為全域性單次透過。這些描述符依賴於從流行的卷積神經網路 (CNN) 中提取的特徵圖,例如 AlexNet、VGG、ResNet 等。這本質上類似於聚合區域性特徵,但使用更簡單的數學運算 (例如最大值、中值和平均值),並且得到 GPU 加速器的更廣泛支援。因此,全域性單次透過方法更具可擴充套件性 (但通常以準確性和精確度為代價),並且是本文的重點。
全域性單次透過描述符的最新進展
隨著大量訓練資料支援的 CNN 的成功,影像檢索界開始認識到 CNN 特徵圖的潛力。在全域性描述符中聚合特徵圖的早期嘗試,即全域性池化,包括 最大池化、平均池化。然而,這些特徵來自在 ImageNet 上預訓練的 CNN,ImageNet 是一個為影像分類而不是影像檢索而設計的資料集。 Radenović 等人 的論文是深度影像檢索的突破,因為它不僅引入了專門用於 排名損失 的資料集來微調 CNN 用於影像檢索,而且還提出了一種更強大的池化操作 - GeM 池化。
這篇論文對我的研究非常重要,因為它不僅是當時的最新方法,而且也是影像檢索中最全面的論文之一,它包含大量的實驗和結果,為隨後對該領域感興趣的研究人員指明瞭非常明確的方向。此外,他們的 程式碼 也寫得非常好 (在 GitHub 上有 800 多顆星),我的程式碼也大量基於他們建立的框架,該框架包括所有模型、資料集訓練和評估指令碼,這些指令碼都是用來重現論文中的結果的。它確實為我節省了數月的寶貴時間!如果你對影像檢索感興趣,我強烈建議你檢視他們的儲存庫。
如果我們有一個輸入影像 I $ \in$ IRH, W, з ,在將其透過 CNN 後,我們得到一個特徵圖 f = $\Theta$ (I) $ \in$ IRh, w, d 。
GeM 池化操作是
其中 N 是特徵的總數,即 N = h x w。
這個公式可能看起來很嚇人,但實際上非常直觀!簡單來說,GeM 取特徵圖 f 中特徵的加權平均值,權重由 CNN 對每個特徵的啟用來確定。因此,CNN 對某個特徵反應越強烈,該特徵對全域性描述符的貢獻就越大。標量 p 然後控制這種加權偏向這些“強”特徵的程度。

池化中的二階資訊
對於位置識別和視覺定位,影像檢索必須在地標影像上表現良好。與其他領域相比,地標影像呈現出一些獨特的挑戰。例如,看看圖 3,(a) 不同的地標可能看起來非常不同或相似,(b) 一張影像中可能有多個地標,(c) 大部分影像都是無關的,或者 (d) 影像可能是極端條件下拍攝的。

GeM 池化的侷限性在於,每個特徵只包含其周圍空間鄰域的資訊,因為它是一階度量。使用圖 3 中的 (b) 作為示例,GeM 如何知道圓頂或尖頂更重要?或者兩者都很重要?因此,我們需要超越一階統計量來獲得對地標更穩健的全域性描述符。
自注意力是自然語言處理中 Transformer 中利用二階資訊的極其成功的機制,在各種計算機視覺任務中也非常流行。圖 5 顯示了它的工作原理。乍一看它可能看起來非常複雜,但實際上很簡單!步驟如下

- 還記得之前的特徵圖 f 嗎?我們首先對 f 進行 3 次投影,稱為查詢 q、鍵 k 和值 v,所有這些都使用 1×1 卷積。
- 每個 q、k 和 v 都具有與 f 完全相同的形狀,即 d x h x w。我們首先在空間維度上將它們全部展平,以獲得形狀為 d x (hw) 的二維張量。
- 然後我們轉置展平後的 q 並與 k 相乘。這裡,矩陣乘積 qTk 的形狀為 hw x hw。
- 現在我們取此乘積 z = softmax ( $\alpha $ $\cdot $ qTk ) 在其第二個維度上的 softmax。有些人可能已經注意到,這是二階資訊發揮作用的部分。這有效地為 f 中的每個空間位置提供了一個所有空間位置的機率分佈!在論文中,我們稱 z 為二階注意力 (SOA)。
- 最後,我們將 z 與 v 相乘,將其重新整形回與 f (d x h x w) 相同的形狀,並將其透過另一個 1×1 卷積。然後我們簡單地將其新增到 f 中以獲得 fso。
你可以將步驟 1-5 視為使用 SOA 重新加權 f 的一種方式。由於 fso 中的每個特徵現在還包含來自 f 中所有其他特徵的資訊,因此對 fso 進行 GeM 池化也應該為我們提供一個對上述地標影像檢索挑戰更穩健的全域性描述符。

讓我們嘗試透過在地標影像上視覺化一些 SOA 來理解它們的效果。在圖 5 中,您可以看到,當我們在粉色星號處選擇一個空間位置時,該位置對應的 SOA 會疊加在原始影像之上。當粉色星號位於地標內時,SOA 會關注影像中非常獨特的部位。當它位於背景/無關物體中時,它會嘗試勾勒出影像中主要地標的輪廓。這表明 SOA 能夠靈活地調整每個特徵從影像其他部位獲取資訊的程度,從而實現上述的重新加權目標。我們發現,在大型影像檢索資料集上,利用 SOA 的二階資訊可以幫助我們獲得顯著更好的結果,如果您對這個領域感興趣,我強烈建議您檢視我們的 ECCV'20 論文,以獲得更深入的分析。
使用 OpenCV 視覺化二階注意力圖
在這個專案進行過程中,我不得不不斷地在大量影像的不同位置視覺化注意力圖。最後我得到了一大堆影像和注意力圖,我無法從中挑選出合適的示例來研究!幸運的是,OpenCV 允許我們只需點選幾下滑鼠就可以互動式地選擇一個位置,並可視化相應的 SOA。
讓我們使用這張埃菲爾鐵塔的精美影像,並將其命名為“eiffel.jpg”。

我們可以使用 OpenCV 載入影像。
import cv2
image = cv2.imread("eiffel.jpg")
cv2.namedWindow("image")
# initialize the list of reference points and boolean indicating
refPt = []
現在我們必須建立一個名為 click_and_draw_rect() 的回撥函式,它由在 OpenCV 影像視窗上繪製矩形觸發。
def click_and_draw_rect(event, x, y, flags, param):
# grab references to the global variables
global refPt
# if the left mouse button was clicked, record the starting
# (x, y) coordinates
if event == cv2.EVENT_LBUTTONDOWN:
refPt = [(x, y)]
# check to see if the left mouse button was released
elif event == cv2.EVENT_LBUTTONUP:
# record the ending (x, y) coordinates and indicate that
refPt.append((x, y))
# draw a rectangle around the region of interest
cv2.rectangle(image, refPt[0], refPt[1], (0, 255, 0), 2)
cv2.imshow("image", image)

您現在應該在“image”視窗中看到您繪製的(綠色)矩形。現在發生的是, click_and_draw_rect() 記錄了三角形對角頂點的座標,並將它們附加到 refPt 中。
現在,我們想提取這個位置的 SOA。讓我們看看這個名為 draw_soa_map(img, model, refPt) 的函式。
import torch
import torch.nn.functional as F
from torchvision import transforms
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.patches as patches
from matplotlib.patches import Arrow, Circle
from mpl_toolkits.axes_grid1 import make_axes_locatable
def draw_soa_map(img, model, refPt):
# imagenet statistics for normalization
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
normalize = transforms.Normalize(mean=mean, std=std)
transform = transforms.Compose([
transforms.ToTensor(),
normalize,
])
img_tensor = transform(img).unsqueeze(0).cuda()
fig = plt.figure(dpi=200)
ax = fig.add_subplot(111)
ax.set_xticks([])
ax.set_yticks([])
# h and w are the height and width of our input image
h, w = img_tensor.shape[-2:]
with torch.no_grad():
######################################################
# Here, model (one of the function's arguments) is a torch.nn.Module class, which is our deep CNN. In this case, we are using ResNet-101
# let's say calling model.features(img_tensor) returns two tensors, the first of which is the final feature layer f with size 2048 * h//32 * w//32
# the second of which is the second-order attention map from the last layer with size (h//32 * w//32)**2
#######################################################
f, soa_last = model.features(img_tensor)
# h_last and w_last are the height and width of final feature map f
h_last, w_last = f.shape[-2:]
# now we try to find the location of the centre of the rectangle we drew, i.e. refPt, projected onto f
pos_h_last, pos_w_last = int(((refPt[0][1] + refPt[1][1]) / 2) // 32), int(((refPt[0][0] + refPt[1][0]) / 2) // 32)
# and the location of the original image
pos_h, pos_w = ((refPt[0][1] + refPt[1][1]) / 2), ((refPt[0][0] + refPt[1][0]) / 2)
# then we retrieve the SOA at that location
soa_last = soa_last.view(1, h_last, w_last, -1)
self_soa_last = soa_last[:, pos_h_last, pos_w_last, ...].view(-1, h_m1, w_m1)
self_soa_last = F.interpolate(self_soa_last.unsqueeze(1), size=(h, w), mode='bilinear').squeeze()
# overlay the attention on the original image with alpha mask
ax.imshow(img)
ax.imshow(self_soa_m1.cpu().numpy(), cmap='jet', alpha=.65)
# add a circle at the centre of the rectangle we draw to indicate where the soa is selected from
ax.add_patch(Circle((pos_w1, pos_h1), radius=5, color='white', edgecolor='white', linewidth=5))
plt.tight_layout()
# redraw the canvas
fig.canvas.draw()
# convert canvas to image
img_cv2 = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='')
img_cv2 = img_cv2.reshape(fig.canvas.get_width_height()[::-1] + (3,))
# img_cv2 is rgb, convert to opencv's default bgr
img_cv2 = cv2.cvtColor(img_cv2,cv2.COLOR_RGB2BGR)
return img_cv2
我知道,這個函式很長很複雜!但是,它所做的只是找到我們在特徵圖 f 上應該選擇的正確位置。由於大多數 CNN(在本例中為 ResNet-101)的最後一個特徵圖比原始影像具有更低的空間解析度,因此我們必須除以尺度差並四捨五入原始影像中矩形中心的座標到特徵圖的大小(在 ResNet-101 的情況下,f 是輸入影像大小的 1/32)。請記住,我們之前提到過 soa 的形狀是 hw x hw?在上面的函式中,我們將其重塑為 h x w x h x w ,並使用我們從參考點計算出的 pos_h_last, pos_w_last ,我們可以獲得該位置的 SOA,其形狀為 h x w。非常簡單!
最後,我們在前面的滑鼠回撥函式之後呼叫此函式,它仍然在 while 迴圈內。
from PIL import Image
# if there are two reference points, then crop the region of interest
# from the image and display it
if len(refPt) == 2:
# display soa
soa = draw_soa_map(Image.open("eiffel.jpg"), model, refPt)
cv2.imshow("Second order attention", soa)
cv2.waitKey(20)
# close all open windows
cv2.destroyAllWindows()
(注意:model 是我們使用的 CNN 的 torch.nn.Module 類。有關更多詳細資訊,請參見 我們的 GitHub 庫。)
您應該會看到 SOA 在一個新視窗中彈出。

現在嘗試在原始視窗的天空上繪製矩形。

SOA 更加分散,並且勾勒出影像中的地標,這證實了我們之前的觀察結果!
藉助 OpenCV 提供的此功能,我可以毫不費力地建立 GIF,這非常適合在社交媒體上宣傳您的作品!

結論
儘管深度學習對影像檢索貢獻巨大,但最先進的地標檢索技術仍然遠非完美。區域性聚合方法的準確性和全域性單次遍歷方法的效率之間存在著不斷的拉鋸戰。我希望我的工作能夠略微彌合這一差距,我們能夠看到未來出現一些真正令人驚歎的作品,這些作品既快速又準確,能夠在極大規模上執行——也許可以在眨眼之間檢索數十億張影像!








