成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

利用JS實(shí)現(xiàn)多種圖片相似度算法

開(kāi)發(fā) 前端 算法
為了便于理解,每種算法都會(huì)經(jīng)過(guò)“特征提取”和“特征比對(duì)”兩個(gè)步驟進(jìn)行。接下來(lái)將著重對(duì)每種算法的“特征提取”步驟進(jìn)行詳細(xì)解讀,而“特征比對(duì)”則單獨(dú)進(jìn)行闡述。

 

在搜索領(lǐng)域,早已出現(xiàn)了“查找相似圖片/相似商品”的相關(guān)功能,如 Google 搜圖,百度搜圖,淘寶的拍照搜商品等。要實(shí)現(xiàn)類似的計(jì)算圖片相似度的功能,除了使用聽(tīng)起來(lái)高大上的“人工智能”以外,其實(shí)通過(guò) js 和幾種簡(jiǎn)單的算法,也能八九不離十地實(shí)現(xiàn)類似的效果。

在閱讀本文之前,強(qiáng)烈建議先閱讀完阮一峰于多年所撰寫(xiě)的《相似圖片搜索的原理》相關(guān)文章,本文所涉及的算法也來(lái)源于其中。

體驗(yàn)地址:https://img-compare.netlify.com/

特征提取算法

為了便于理解,每種算法都會(huì)經(jīng)過(guò)“特征提取”和“特征比對(duì)”兩個(gè)步驟進(jìn)行。接下來(lái)將著重對(duì)每種算法的“特征提取”步驟進(jìn)行詳細(xì)解讀,而“特征比對(duì)”則單獨(dú)進(jìn)行闡述。

平均哈希算法

參考阮大的文章,“平均哈希算法”主要由以下幾步組成:

第一步,縮小尺寸為8×8,以去除圖片的細(xì)節(jié),只保留結(jié)構(gòu)、明暗等基本信息,摒棄不同尺寸、比例帶來(lái)的圖片差異。

第二步,簡(jiǎn)化色彩。將縮小后的圖片轉(zhuǎn)為灰度圖像。

第三步,計(jì)算平均值。計(jì)算所有像素的灰度平均值。

第四步,比較像素的灰度。將64個(gè)像素的灰度,與平均值進(jìn)行比較。大于或等于平均值,記為1;小于平均值,記為0。

第五步,計(jì)算哈希值。將上一步的比較結(jié)果,組合在一起,就構(gòu)成了一個(gè)64位的整數(shù),這就是這張圖片的指紋。

第六步,計(jì)算哈希值的差異,得出相似度(漢明距離或者余弦值)。

明白了“平均哈希算法”的原理及步驟以后,就可以開(kāi)始編碼工作了。為了讓代碼可讀性更高,本文的所有例子我都將使用 typescript 來(lái)實(shí)現(xiàn)。

圖片壓縮:

我們采用 canvas 的 drawImage() 方法實(shí)現(xiàn)圖片壓縮,后使用 getImageData() 方法獲取 ImageData 對(duì)象。 

  1. export function compressImg (imgSrc: string, imgWidth: number = 8): Promise<ImageData> {  
  2.   return new Promise((resolve, reject) => {  
  3.     if (!imgSrc) {  
  4.       reject('imgSrc can not be empty!')  
  5.     }  
  6.     const canvas = document.createElement('canvas')  
  7.     const ctx = canvas.getContext('2d')  
  8.     const img = new Image()  
  9.     img.crossOrigin = 'Anonymous'  
  10.     img.onload = function () {  
  11.       canvas.width = imgWidth  
  12.       canvas.height = imgWidth  
  13.       ctx?.drawImage(img, 0, 0, imgWidth, imgWidth)  
  14.       const data = ctx?.getImageData(0, 0, imgWidth, imgWidth) as ImageData  
  15.       resolve(data)  
  16.     }  
  17.     img.src = imgSrc  
  18.   })  

可能有讀者會(huì)問(wèn),為什么使用 canvas 可以實(shí)現(xiàn)圖片壓縮呢?簡(jiǎn)單來(lái)說(shuō),為了把“大圖片”繪制到“小畫(huà)布”上,一些相鄰且顏色相近的像素往往會(huì)被刪減掉,從而有效減少了圖片的信息量,因此能夠?qū)崿F(xiàn)壓縮的效果:

在上面的 compressImg() 函數(shù)中,我們利用 new Image() 加載圖片,然后設(shè)定一個(gè)預(yù)設(shè)的圖片寬高值讓圖片壓縮到指定的大小,最后獲取到壓縮后的圖片的 ImageData 數(shù)據(jù)——這也意味著我們能獲取到圖片的每一個(gè)像素的信息。

關(guān)于 ImageData,可以參考 MDN 的文檔介紹

圖片灰度化

為了把彩色的圖片轉(zhuǎn)化成灰度圖,我們首先要明白“灰度圖”的概念。在維基百科里是這么描述灰度圖像的:

在計(jì)算機(jī)領(lǐng)域中,灰度(Gray scale)數(shù)字圖像是每個(gè)像素只有一個(gè)采樣顏色的圖像。

大部分情況下,任何的顏色都可以通過(guò)三種顏色通道(R, G, B)的亮度以及一個(gè)色彩空間(A)來(lái)組成,而一個(gè)像素只顯示一種顏色,因此可以得到“像素 => RGBA”的對(duì)應(yīng)關(guān)系。而“每個(gè)像素只有一個(gè)采樣顏色”,則意味著組成這個(gè)像素的三原色通道亮度相等,因此只需要算出 RGB 的平均值即可: 

  1. // 根據(jù) RGBA 數(shù)組生成 ImageData  
  2. export function createImgData (dataDetail: number[]) {  
  3.   const canvas = document.createElement('canvas')  
  4.   const ctx = canvas.getContext('2d')  
  5.   const imgWidth = Math.sqrt(dataDetail.length / 4)  
  6.   const newImageData = ctx?.createImageData(imgWidth, imgWidth) as ImageData  
  7.   for (let i = 0; i < dataDetail.length; i += 4) {  
  8.     let R = dataDetail[i]  
  9.     let G = dataDetail[i + 1]  
  10.     let B = dataDetail[i + 2]  
  11.     let Alpha = dataDetail[i + 3]  
  12.     newImageData.data[i] = R  
  13.     newImageData.data[i + 1] = G  
  14.     newImageData.data[i + 2] = B  
  15.     newImageData.data[i + 3] = Alpha  
  16.   }  
  17.   return newImageData  
  18.  
  19. export function createGrayscale (imgData: ImageData) {  
  20.   const newData: number[] = Array(imgData.data.length)  
  21.   newData.fill(0)  
  22.   imgData.data.forEach((_data, index) => {  
  23.     if ((index + 1) % 4 === 0) {  
  24.       const R = imgData.data[index - 3]  
  25.       const G = imgData.data[index - 2]  
  26.       const B = imgData.data[index - 1] 
  27.        const gray = ~~((R + G + B) / 3)  
  28.       newData[index - 3] = gray  
  29.       newData[index - 2] = gray  
  30.       newData[index - 1] = gray 
  31.        newData[index] = 255 // Alpha 值固定為255  
  32.     }  
  33.   })  
  34.   return createImgData(newData)  

ImageData.data 是一個(gè) Uint8ClampedArray 數(shù)組,可以理解為“RGBA數(shù)組”,數(shù)組中的每個(gè)數(shù)字取值為0~255,每4個(gè)數(shù)字為一組,表示一個(gè)像素的 RGBA 值。由于ImageData 為只讀對(duì)象,所以要另外寫(xiě)一個(gè) creaetImageData() 方法,利用 context.createImageData() 來(lái)創(chuàng)建新的 ImageData 對(duì)象。

拿到灰度圖像以后,就可以進(jìn)行指紋提取的操作了。

指紋提取

在“平均哈希算法”中,若灰度圖的某個(gè)像素的灰度值大于平均值,則視為1,否則為0。把這部分信息組合起來(lái)就是圖片的指紋。由于我們已經(jīng)拿到了灰度圖的 ImageData 對(duì)象,要提取指紋也就變得很容易了: 

  1. export function getHashFingerprint (imgData: ImageData) {  
  2.   const grayList = imgData.data.reduce((pre: number[], cur, index) => {  
  3.     if ((index + 1) % 4 === 0) {  
  4.       pre.push(imgData.data[index - 1])  
  5.     }  
  6.     return pre  
  7.   }, [])  
  8.   const length = grayList.length  
  9.   const grayAverage = grayList.reduce((pre, next) => (pre + next), 0) / length  
  10.   return grayList.map(gray => (gray >= grayAverage ? 1 : 0)).join('')  

通過(guò)上述一連串的步驟,我們便可以通過(guò)“平均哈希算法”獲取到一張圖片的指紋信息(示例是大小為8×8的灰度圖):

感知哈希算法

關(guān)于“感知哈希算法”的詳細(xì)介紹,可以參考這篇文章:《基于感知哈希算法的視覺(jué)目標(biāo)跟蹤》。

簡(jiǎn)單來(lái)說(shuō),該算法經(jīng)過(guò)離散余弦變換以后,把圖像從像素域轉(zhuǎn)化到了頻率域,而攜帶了有效信息的低頻成分會(huì)集中在 DCT 矩陣的左上角,因此我們可以利用這個(gè)特性提取圖片的特征。

該算法的步驟如下:

  •   縮小尺寸:pHash以小圖片開(kāi)始,但圖片大于88,3232是最好的。這樣做的目的是簡(jiǎn)化了DCT的計(jì)算,而不是減小頻率。
  •   簡(jiǎn)化色彩:將圖片轉(zhuǎn)化成灰度圖像,進(jìn)一步簡(jiǎn)化計(jì)算量。
  •   計(jì)算DCT:計(jì)算圖片的DCT變換,得到32*32的DCT系數(shù)矩陣。
  •   縮小DCT:雖然DCT的結(jié)果是3232大小的矩陣,但我們只要保留左上角的88的矩陣,這部分呈現(xiàn)了圖片中的最低頻率。
  •   計(jì)算平均值:如同均值哈希一樣,計(jì)算DCT的均值。
  •   計(jì)算hash值:這是最主要的一步,根據(jù)8*8的DCT矩陣,設(shè)置0或1的64位的hash值,大于等于DCT均值的設(shè)為”1”,小于DCT均值的設(shè)為“0”。組合在一起,就構(gòu)成了一個(gè)64位的整數(shù),這就是這張圖片的指紋。

回到代碼中,首先添加一個(gè) DCT 方法: 

  1. function memoizeCosines (N: number, cosMap: any) {  
  2.   cosMapcosMap = cosMap || {}  
  3.   cosMap[N] = new Array(N * N)  
  4.   let PI_N = Math.PI / N  
  5.   for (let k = 0; k < N; k++) {  
  6.     for (let n = 0; n < N; n++) {  
  7.       cosMap[N][n + (k * N)] = Math.cos(PI_N * (n + 0.5) * k)  
  8.     }  
  9.   }  
  10.   return cosMap  
  11.  
  12. function dct (signal: number[], scale: number = 2) {  
  13.   let L = signal.length  
  14.   let cosMap: any = null  
  15.   if (!cosMap || !cosMap[L]) {  
  16.     cosMap = memoizeCosines(L, cosMap)  
  17.   }  
  18.   let coefficients = signal.map(function () { return 0 })  
  19.   return coefficients.map(function (_, ix) {  
  20.     return scale * signal.reduce(function (prev, cur, index) {  
  21.       return prev + (cur * cosMap[L][index + (ix * L)])  
  22.     }, 0)  
  23.   })  

然后添加兩個(gè)矩陣處理方法,分別是把經(jīng)過(guò) DCT 方法生成的一維數(shù)組升維成二維數(shù)組(矩陣),以及從矩陣中獲取其“左上角”內(nèi)容。 

  1. // 一維數(shù)組升維  
  2. function createMatrix (arr: number[]) {  
  3.   const length = arr.length  
  4.   const matrixWidth = Math.sqrt(length)  
  5.   const matrix = []  
  6.   for (let i = 0; i < matrixWidth; i++) {  
  7.     const _temp = arr.slice(i * matrixWidth, i * matrixWidth + matrixWidth)  
  8.     matrix.push(_temp)  
  9.   }  
  10.   return matrix  
  11.  
  12. // 從矩陣中獲取其“左上角”大小為 range × range 的內(nèi)容  
  13. function getMatrixRange (matrix: number[][], range: number = 1) {  
  14.   const rangeMatrix = []  
  15.   for (let i = 0; i < range; i++) {  
  16.     for (let j = 0; j < range; j++) {  
  17.       rangeMatrix.push(matrix[i][j]) 
  18.      }  
  19.   }  
  20.   return rangeMatrix  

復(fù)用之前在“平均哈希算法”中所寫(xiě)的灰度圖轉(zhuǎn)化函數(shù)createGrayscale(),我們可以獲取“感知哈希算法”的特征值: 

  1. export function getPHashFingerprint (imgData: ImageData) {  
  2.   const dctdctData = dct(imgData.data as any)  
  3.   const dctMatrix = createMatrix(dctData)  
  4.   const rangeMatrix = getMatrixRange(dctMatrix, dctMatrix.length / 8)  
  5.   const rangeAve = rangeMatrix.reduce((pre, cur) => pre + cur, 0) / rangeMatrix.length  
  6.   return rangeMatrix.map(val => (val >= rangeAve ? 1 : 0)).join('')  

顏色分布法

首先摘抄一段阮大關(guān)于“顏色分布法“的描述:

阮大把256種顏色取值簡(jiǎn)化成了4種。基于這個(gè)原理,我們?cè)谶M(jìn)行顏色分布法的算法設(shè)計(jì)時(shí),可以把這個(gè)區(qū)間的劃分設(shè)置為可修改的,唯一的要求就是區(qū)間的數(shù)量必須能夠被256整除。算法如下:

 

  1. // 劃分顏色區(qū)間,默認(rèn)區(qū)間數(shù)目為4個(gè)  
  2. // 把256種顏色取值簡(jiǎn)化為4種  
  3. export function simplifyColorData (imgData: ImageData, zoneAmount: number = 4) {  
  4.   const colorZoneDataList: number[] = []  
  5.   const zoneStep = 256 / zoneAmount  
  6.   const zoneBorder = [0] // 區(qū)間邊界 
  7.    for (let i = 1; i <= zoneAmount; i++) {  
  8.     zoneBorder.push(zoneStep * i - 1)  
  9.   }  
  10.   imgData.data.forEach((data, index) => {  
  11.     if ((index + 1) % 4 !== 0) {  
  12.       for (let i = 0; i < zoneBorder.length; i++) {  
  13.         if (data > zoneBorder[i] && data <= zoneBorder[i + 1]) {  
  14.           data = i  
  15.         }  
  16.       }  
  17.     }  
  18.     colorZoneDataList.push(data)  
  19.   })  
  20.   return colorZoneDataList  

把顏色取值進(jìn)行簡(jiǎn)化以后,就可以把它們歸類到不同的分組里面去: 

  1. export function seperateListToColorZone (simplifiedDataList: number[]) {  
  2.   const zonedList: string[] = []  
  3.   let tempZone: number[] = []  
  4.   simplifiedDataList.forEach((data, index) => {  
  5.     if ((index + 1) % 4 !== 0) {  
  6.       tempZone.push(data)  
  7.     } else { 
  8.        zonedList.push(JSON.stringify(tempZone))  
  9.       tempZone = []  
  10.     }  
  11.   })  
  12.   return zonedList  

最后只需要統(tǒng)計(jì)每個(gè)相同的分組的總數(shù)即可: 

  1. export function getFingerprint (zonedList: string[], zoneAmount: number = 16) {  
  2.   const colorSeperateMap: {  
  3.     [key: string]: number  
  4.   } = {}  
  5.   for (let i = 0; i < zoneAmount; i++) {  
  6.     for (let j = 0; j < zoneAmount; j++) {  
  7.       for (let k = 0; k < zoneAmount; k++) {  
  8.         colorSeperateMap[JSON.stringify([i, j, k])] = 0  
  9.       }  
  10.     }  
  11.   }  
  12.   zonedList.forEach(zone => {  
  13.     colorSeperateMap[zone]++  
  14.   })  
  15.   return Object.values(colorSeperateMap)  

內(nèi)容特征法

”內(nèi)容特征法“是指把圖片轉(zhuǎn)化為灰度圖后再轉(zhuǎn)化為”二值圖“,然后根據(jù)像素的取值(黑或白)形成指紋后進(jìn)行比對(duì)的方法。這種算法的核心是找到一個(gè)“閾值”去生成二值圖。

對(duì)于生成灰度圖,有別于在“平均哈希算法”中提到的取 RGB 均值的辦法,在這里我們使用加權(quán)的方式去實(shí)現(xiàn)。為什么要這么做呢?這里涉及到顏色學(xué)的一些概念。

具體可以參考這篇《Grayscale to RGB Conversion》,下面簡(jiǎn)單梳理一下。

采用 RGB 均值的灰度圖是最簡(jiǎn)單的一種辦法,但是它忽略了紅、綠、藍(lán)三種顏色的波長(zhǎng)以及對(duì)整體圖像的影響。以下面圖為示例,如果直接取得 RGB 的均值作為灰度,那么處理后的灰度圖整體來(lái)說(shuō)會(huì)偏暗,對(duì)后續(xù)生成二值圖會(huì)產(chǎn)生較大的干擾。

那么怎么改善這種情況呢?答案就是為 RGB 三種顏色添加不同的權(quán)重。鑒于紅光有著更長(zhǎng)的波長(zhǎng),而綠光波長(zhǎng)更短且對(duì)視覺(jué)的刺激相對(duì)更小,所以我們要有意地減小紅光的權(quán)重而提升綠光的權(quán)重。經(jīng)過(guò)統(tǒng)計(jì),比較好的權(quán)重配比是 R:G:B = 0.299:0.587:0.114。

于是我們可以得到灰度處理函數(shù): 

  1. enum GrayscaleWeight {  
  2.   R = .299,  
  3.   G = .587,  
  4.   B = .114  
  5.  
  6. function toGray (imgData: ImageData) {  
  7.   const grayData = []  
  8.   const data = imgData.data  
  9.   for (let i = 0; i < data.length; i += 4) {  
  10.     const gray = ~~(data[i] * GrayscaleWeight.R + data[i + 1] * GrayscaleWeight.G + data[i + 2] * GrayscaleWeight.B)  
  11.     data[i] = data[i + 1] = data[i + 2] = gray  
  12.     grayData.push(gray)  
  13.   }  
  14.   return grayData  

上述函數(shù)返回一個(gè) grayData 數(shù)組,里面每個(gè)元素代表一個(gè)像素的灰度值(因?yàn)?RBG 取值相同,所以只需要一個(gè)值即可)。接下來(lái)則使用“大津法”(Otsu's method)去計(jì)算二值圖的閾值。關(guān)于“大津法”,阮大的文章已經(jīng)說(shuō)得很詳細(xì),在這里就不展開(kāi)了。我在這個(gè)地方找到了“大津法”的 Java 實(shí)現(xiàn),后來(lái)稍作修改,把它改為了 js 版本: 

  1. / OTSU algorithm  
  2. // rewrite from http://www.labbookpages.co.uk/software/imgProc/otsuThreshold.html  
  3. export function OTSUAlgorithm (imgData: ImageData) {  
  4.   const grayData = toGray(imgData)  
  5.   let ptr = 0  
  6.   let histData = Array(256).fill(0)  
  7.   let total = grayData.length  
  8.   while (ptr < total) {  
  9.     let h = 0xFF & grayData[ptr++]  
  10.     histData[h]++  
  11.   }  
  12.   let sum = 0  
  13.   for (let i = 0; i < 256; i++) {  
  14.     sum += i * histData[i]  
  15.   }  
  16.   let wB = 0  
  17.   let wF = 0  
  18.   let sumB = 0  
  19.   let varMax = 0  
  20.   let threshold = 0  
  21.   for (let t = 0; t < 256; t++) {  
  22.     wB += histData[t]  
  23.     if (wB === 0) continue  
  24.     wF = total - wB  
  25.     if (wF === 0) break  
  26.     sumB += t * histData[t]  
  27.     let mB = sumB / wB  
  28.     let mF = (sum - sumB) / wF  
  29.     let varBetween = wB * wF * (mB - mF) ** 2  
  30.     if (varBetween > varMax) {  
  31.       varMax = varBetween  
  32.       tthreshold = t  
  33.     }  
  34.   }  
  35.   return threshold  

OTSUAlgorithm() 函數(shù)接收一個(gè) ImageData 對(duì)象,經(jīng)過(guò)上一步的 toGray() 方法獲取到灰度值列表以后,根據(jù)“大津法”算出最佳閾值然后返回。接下來(lái)使用這個(gè)閾值對(duì)原圖進(jìn)行處理,即可獲取二值圖。 

  1. export function binaryzation (imgData: ImageData, threshold: number) {  
  2.   const canvas = document.createElement('canvas')  
  3.   const ctx = canvas.getContext('2d') 
  4.    const imgWidth = Math.sqrt(imgData.data.length / 4)  
  5.   const newImageData = ctx?.createImageData(imgWidth, imgWidth) as ImageData  
  6.   for (let i = 0; i < imgData.data.length; i += 4) {  
  7.     let R = imgData.data[i]  
  8.     let G = imgData.data[i + 1]  
  9.     let B = imgData.data[i + 2]  
  10.     let Alpha = imgData.data[i + 3]  
  11.     let sum = (R + G + B) / 3  
  12.     newImageData.data[i] = sum > threshold ? 255 : 0  
  13.     newImageData.data[i + 1] = sum > threshold ? 255 : 0  
  14.     newImageData.data[i + 2] = sum > threshold ? 255 : 0  
  15.     newImageData.data[i + 3] = Alpha  
  16.   }  
  17.   return newImageData  

若圖片大小為 N×N,根據(jù)二值圖“非黑即白”的特性,我們便可以得到一個(gè) N×N 的 0-1 矩陣,也就是指紋:

特征比對(duì)算法

經(jīng)過(guò)不同的方式取得不同類型的圖片指紋(特征)以后,應(yīng)該怎么去比對(duì)呢?這里將介紹三種比對(duì)算法,然后分析這幾種算法都適用于哪些情況。

漢明距離

摘一段維基百科關(guān)于“漢明距離”的描述:

在信息論中,兩個(gè)等長(zhǎng)字符串之間的漢明距離(英語(yǔ):Hamming distance)是兩個(gè)字符串對(duì)應(yīng)位置的不同字符的個(gè)數(shù)。換句話說(shuō),它就是將一個(gè)字符串變換成另外一個(gè)字符串所需要替換的字符個(gè)數(shù)。

例如:

  • 1011101與1001001之間的漢明距離是2。
  • 2143896與2233796之間的漢明距離是3。
  • "toned"與"roses"之間的漢明距離是3。

明白了含義以后,我們可以寫(xiě)出計(jì)算漢明距離的方法: 

  1. export function hammingDistance (str1: string, str2: string) {  
  2.   let distance = 0  
  3.   const str1str1Arr = str1.split('')  
  4.   const str2str2Arr = str2.split('')  
  5.   str1Arr.forEach((letter, index) => {  
  6.     if (letter !== str2Arr[index]) {  
  7.       distance++  
  8.     }  
  9.   })  
  10.   return distance  

使用這個(gè) hammingDistance() 方法,來(lái)驗(yàn)證下維基百科上的例子:

驗(yàn)證結(jié)果符合預(yù)期。

知道了漢明距離,也就可以知道兩個(gè)等長(zhǎng)字符串之間的相似度了(漢明距離越小,相似度越大):

相似度 = (字符串長(zhǎng)度 - 漢明距離) / 字符串長(zhǎng)度

余弦相似度

從維基百科中我們可以了解到關(guān)于余弦相似度的定義:

余弦相似性通過(guò)測(cè)量?jī)蓚€(gè)向量的夾角的余弦值來(lái)度量它們之間的相似性。0度角的余弦值是1,而其他任何角度的余弦值都不大于1;并且其最小值是-1。從而兩個(gè)向量之間的角度的余弦值確定兩個(gè)向量是否大致指向相同的方向。兩個(gè)向量有相同的指向時(shí),余弦相似度的值為1;兩個(gè)向量夾角為90°時(shí),余弦相似度的值為0;兩個(gè)向量指向完全相反的方向時(shí),余弦相似度的值為-1。這結(jié)果是與向量的長(zhǎng)度無(wú)關(guān)的,僅僅與向量的指向方向相關(guān)。余弦相似度通常用于正空間,因此給出的值為0到1之間。

注意這上下界對(duì)任何維度的向量空間中都適用,而且余弦相似性最常用于高維正空間。

余弦相似度可以計(jì)算出兩個(gè)向量之間的夾角,從而很直觀地表示兩個(gè)向量在方向上是否相似,這對(duì)于計(jì)算兩個(gè) N×N 的 0-1 矩陣的相似度來(lái)說(shuō)非常有用。根據(jù)余弦相似度的公式,我們可以把它的 js 實(shí)現(xiàn)寫(xiě)出來(lái): 

  1. export function cosineSimilarity (sampleFingerprint: number[], targetFingerprint: number[]) {  
  2.   // cosθ = ∑n, i=1(Ai × Bi) / (√∑n, i=1(Ai)^2) × (√∑n, i=1(Bi)^2) = A · B / |A| × |B|  
  3.   const length = sampleFingerprint.length  
  4.   let innerProduct = 0  
  5.   for (let i = 0; i < length; i++) {  
  6.     innerProduct += sampleFingerprint[i] * targetFingerprint[i]  
  7.   }  
  8.   let vecA = 0  
  9.   let vecB = 0  
  10.   for (let i = 0; i < length; i++) {  
  11.     vecA += sampleFingerprint[i] ** 2  
  12.     vecB += targetFingerprint[i] ** 2  
  13.   }  
  14.   const outerProduct = Math.sqrt(vecA) * Math.sqrt(vecB)  
  15.   return innerProduct / outerProduct  

種比對(duì)算法的適用場(chǎng)景

明白了“漢明距離”和“余弦相似度”這兩種特征比對(duì)算法以后,我們就要去看看它們分別適用于哪些特征提取算法的場(chǎng)景。

首先來(lái)看“顏色分布法”。在“顏色分布法”里面,我們把一張圖的顏色進(jìn)行區(qū)間劃分,通過(guò)統(tǒng)計(jì)不同顏色區(qū)間的數(shù)量來(lái)獲取特征,那么這里的特征值就和“數(shù)量”有關(guān),也就是非 0-1 矩陣。

顯然,要比較兩個(gè)“顏色分布法”特征的相似度,“漢明距離”是不適用的,只能通過(guò)“余弦相似度”來(lái)進(jìn)行計(jì)算。

接下來(lái)看“平均哈希算法”和“內(nèi)容特征法”。從結(jié)果來(lái)說(shuō),這兩種特征提取算法都能獲得一個(gè) N×N 的 0-1 矩陣,且矩陣內(nèi)元素的值和“數(shù)量”無(wú)關(guān),只有 0-1 之分。所以它們同時(shí)適用于通過(guò)“漢明距離”和“余弦相似度”來(lái)計(jì)算相似度。

計(jì)算精度

明白了如何提取圖片的特征以及如何進(jìn)行比對(duì)以后,最重要的就是要了解它們對(duì)于相似度的計(jì)算精度。

本文所講的相似度僅僅是通過(guò)客觀的算法來(lái)實(shí)現(xiàn),而判斷兩張圖片“像不像”卻是一個(gè)很主觀的問(wèn)題。于是我寫(xiě)了一個(gè)簡(jiǎn)單的服務(wù),可以自行把兩張圖按照不同的算法和精度去計(jì)算相似度:

https://img-compare.netlify.com/

經(jīng)過(guò)對(duì)不同素材的多方比對(duì),我得出了下列幾個(gè)非常主觀的結(jié)論。

  •  對(duì)于兩張顏色較為豐富,細(xì)節(jié)較多的圖片來(lái)說(shuō),“顏色分布法”的計(jì)算結(jié)果是最符合直覺(jué)的。  

  •  對(duì)于兩張內(nèi)容相近但顏色差異較大的圖片來(lái)說(shuō),“內(nèi)容特征法”和“平均/感知哈希算法”都能得到符合直覺(jué)的結(jié)果。 

  •  針對(duì)“顏色分布法“,區(qū)間的劃分?jǐn)?shù)量對(duì)計(jì)算結(jié)果影響較大,選擇合適的區(qū)間很重要。   

總結(jié)一下,三種特征提取算法和兩種特征比對(duì)算法各有優(yōu)劣,在實(shí)際應(yīng)用中應(yīng)該針對(duì)不同的情況靈活選用。

總結(jié)

本文是在拜讀阮一峰的兩篇《相似圖片搜索的原理》之后,經(jīng)過(guò)自己的實(shí)踐總結(jié)以后而成。由于對(duì)色彩、數(shù)學(xué)等領(lǐng)域的了解只停留在淺顯的層面,文章難免有謬誤之處,如果有發(fā)現(xiàn)表述得不正確的地方,歡迎留言指出,我會(huì)及時(shí)予以更正。

 

 

責(zé)任編輯:龐桂玉 來(lái)源: segmentfault
相關(guān)推薦

2022-02-18 08:26:12

TopK數(shù)組面試題

2017-02-09 16:16:24

Java負(fù)載均衡算法

2015-10-15 10:27:12

文本相似度判定

2024-09-23 14:36:20

2025-01-14 13:51:44

2020-05-07 09:45:16

前端JS圖片壓縮

2022-02-15 13:55:08

圖片濾鏡glfx.jslena.js

2020-06-15 18:00:36

transformbannerJavascript

2022-08-05 19:27:22

通用API鴻蒙

2023-11-21 16:06:04

計(jì)算機(jī)視覺(jué)人工智能

2018-05-28 15:33:09

無(wú)監(jiān)督學(xué)習(xí)算法Python

2021-03-08 15:39:58

人工智能科技數(shù)據(jù)

2010-07-22 12:19:07

2023-10-10 15:33:55

機(jī)器學(xué)習(xí)相似性度量

2010-03-09 16:26:08

Python列表

2013-08-28 13:44:42

數(shù)據(jù)算法

2021-12-03 07:27:30

全景瀏覽Three.js

2010-08-16 16:39:48

DIV內(nèi)容居中

2013-08-29 14:28:58

海量數(shù)據(jù)simhash

2012-05-08 16:29:32

K-meansJava算法
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)

主站蜘蛛池模板: 日韩欧美精品 | 国产一区免费 | 中文字幕视频三区 | 国产激情视频网 | 91麻豆精品国产91久久久久久久久 | 欧美精品一区二区三区在线播放 | 免费观看一级特黄欧美大片 | 99爱视频 | 国产精品不卡 | 国产ts人妖系列高潮 | 国产偷录视频叫床高潮对白 | 国产欧美精品一区二区色综合朱莉 | 亚洲精品高清视频 | 91av视频在线观看 | 色一级| 午夜精品视频一区 | 黄网站涩免费蜜桃网站 | 国产成人午夜电影网 | 99在线观看| 日韩精品一区二区三区中文字幕 | 亚洲视频精品 | 日韩免费网站 | 国产资源在线视频 | 精品亚洲一区二区 | 欧美午夜剧场 | 欧美成人精品一区二区男人看 | 日日夜夜操天天干 | 特级丰满少妇一级aaaa爱毛片 | 国产一区二区在线免费播放 | 一级a爱片性色毛片免费 | 成人网址在线观看 | 免费成人午夜 | 国产精品99久久久久久动医院 | 天堂在线1 | 久久久69| 成人免费激情视频 | 欧美精品一区二区三区在线四季 | 欧美一级特黄aaa大片在线观看 | 欧美日韩不卡 | 国产中文 | 亚洲午夜精品 |