序言
以圖搜圖系統指的是從圖像內容提取特徵向量,然後使用向量數據庫進行向量數據的插入、刪除、相似性檢索等操作,進而提供根據圖像內容搜索出具有相似內容的其它圖像的功能。
系統架構
典型的搜圖系統整體架構時序圖如下:
圖像上傳過程:
- 客户端上傳圖像到服務端。
- 服務端存儲圖像至對象存儲、插入結構化數據至關係型數據庫、發送消息至 MQ 消息隊列。
- 服務端對客户端請求返回響應。
- 圖像搜索服務接受 MQ 的消息,下載圖像內容,使用特定模型提取圖像特徵向量,然後將特徵向量插入到向量數據庫。
這裏使用 MQ 的主要原因有:
- 異步快速響應,因為提取圖像特徵比較耗時,如果是同步的過程則會對客户端體驗不友好。
- 解耦服務、服務異構,提取圖像特徵屬於計算機視覺領域,編程語言生態基本是 Python ,而後端服務則常見於 Java、Golang、Node.js 等,這在架構上就要求服務異構和解耦。
- 削峯填谷,由於用户上傳圖像具有波峯波谷的天然特性,使用 MQ 可以使下游圖像計算保持平穩。
圖像搜索過程:
- 客户端上傳圖像到服務端。
- 服務端發起調用並將圖像傳遞到圖像搜索服務,圖像搜索服務提取圖像特徵向量,然後查詢向量數據庫進行相似性搜索,最後返回向量搜索結果。
- 服務端根據向量搜索結果查詢結構化數據,整合數據,最後響應。
我們可以看到以上系統中,比較耗時的有兩部分:
- 圖像傳遞鏈路長:客户端 -> 服務端 -> 對象存儲 -> 圖像搜索服務。
- 圖像特徵計算比較耗時、且比較消耗服務器資源。
使用客户端模型優化架構
為了進一步優化系統架構,我們可以嘗試使用客户端模型進行圖像特徵提取。
圖像上傳過程:
- 客户端向服務端請求對象存儲的直傳地址,然後客户端直接將圖像內容傳遞到對象存儲(需要對象存儲支持直傳操作)。
- 客户端進行本地計算,提取圖像特徵向量,然後傳遞特徵向量和結構化數據給服務端。
- 服務端對結構化數據和向量數據分別插入到不同的數據庫,完成響應。
圖像搜索過程:
- 客户端進行本地計算,提取圖像特徵向量,然後傳遞特徵向量和結構化數據給服務端。
- 服務端分別進行向量檢索和結構化數據查詢,整合數據,完成響應。
優化後的架構:
- 圖像傳遞鏈路短,只有客户端 -> 對象存儲。
- 圖像特徵計算卸載到了客户端完成,服務器不需要再消耗計算資源。
- 減少了 MQ 和圖像搜索服務這兩個構件,架構更加簡單、複雜度降低。
客户端模型的可行性和約束
客户端相比於服務端具有硬件資源有限、且不可擴展的特點,因此這就要求客户端使用的模型要更小、計算消耗更少。
我們根據上圖中的模型對比可以看到 mobilenet 這種模型更符合我們的需求(模型的名字就能看出來)。
示例
以下給出一個前端使用 mobilenet 完成圖像特徵提取的示例:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<script src="https://cdn.jsdelivr.net/npm/@tensorflow/tfjs"></script>
<script src="https://cdn.jsdelivr.net/npm/@tensorflow-models/mobilenet"></script>
</head>
<body>
<input type="file" id="imageInput">
<button onclick="extractFeatures()">Extract Features</button>
<pre id="result"></pre>
<script>
let model;
async function loadModel() {
if (!model) {
// 加載模型時 mobilenet 會去 storage.googleapis.com 下載
model = await mobilenet.load({version: 2, alpha: 1.0});
}
return model;
}
function preprocessImage(image) {
const tensor = tf.browser.fromPixels(image)
.resizeNearestNeighbor([224, 224])
.toFloat()
.expandDims();
return tensor.div(255.0);
}
async function extractFeatures() {
const input = document.getElementById('imageInput');
if (input.files.length === 0) {
alert('Please select an image file first.');
return;
}
const model = await loadModel();
const timeStart = Date.now();
const file = input.files[0];
const reader = new FileReader();
reader.onload = async function (e) {
const image = new Image();
image.src = e.target.result;
image.onload = async function () {
const processedImage = preprocessImage(image);
const features = model.infer(processedImage, false); // 去掉最後的全連接層
const featuresArray = await features.array();
document.getElementById('result').textContent = JSON.stringify(featuresArray, null, 2);
console.log(`Extract feature spend: ${Date.now() - timeStart} ms`);;
}
}
reader.readAsDataURL(file);
}
</script>
</body>
</html>
然後在我的筆記本電腦簡單測試的結果:
從上圖可以看到,在我的客户端處理一張圖像可以在一秒內完成,當然實際耗時取決於硬件資源和圖像大小。
最後,如果你對此類主題感興趣,可以閲讀我的其它相關文章。
參考資料:
- https://keras.io/api/applications/
- https://www.tensorflow.org/js/models
- https://github.com/tensorflow/tfjs-models/tree/master/mobilenet