JavaScriptを使って二つの画像の類似度を計算します.


最近、阮一峰先生の似たような写真検索の原理(二)を見ました.その中で、内容特徴法によって二つの写真の類似性を比較することを紹介しました.
大体の手順:
  • 写真を50 x 50サイズに縮小します.
  • 階調画像に変換する
  • は、「大津法」を用いてしきい値
  • を決定する.
  • は、閾値を介して画像を二値化する
  • .
  • は、2つのピクチャの対応する位置の画素を比較し、結果
  • を導出する.
    次に、JSで上のステップをどのように実現するかを見てみます.理論部分は多く紹介されていません.やはり似たような画像検索の原理を見ます.
    まず、画像データを操作するにはもちろんcanvasを使いますので、まずキャンバスとその描画コンテキストを作成します.
    const canvas = document.createElement('canvas')
    const context = canvas.getContext('2d')
    画像を拡大縮小してキャンバスに描画し、画像のピクセルデータを取得します.
    function toZoom() {
      canvas.width = 50
      canvas.height = 50
    
      const img = new Image
      img.onload = function () {
        context.drawImage(this, 0, 0, this.width, this.height, 0, 0, 50, 50)
        const imageData = context.getImageData(0, 0, 50, 50)
      }
      img.src = 'test.jpg'
    }
    画像の階調化は、各画素のrgbに対して同じ値を設定し、各画素の値を計算する方法が多く、ここでは重み付けアルゴリズムを使用する.
    function toGray() {
      const grayData = []
      const data = imageData.data
      canvas.width = 50
      canvas.height = 50
    
      for (let i = 0; i < data.length; i += 4) {
        const gray = data[i] * .299 + data[i + 1] * .587 + data[i + 2] * .114 | 0
        data[i] = data[i + 1] = data[i + 2] = gray
        grayData.push(gray)
      }
    
      context.putImageData(imageData, 0, 0)
    
      return grayData
    }
    画像を二値化し、白黒画像にする前に、まず一つの閾値を決定し、この閾値に基づいて画像を二値化することで、画像の輪郭を最も鮮明にすることができる.記事では、「大津法」(Ots's method)によってこの閾値を求めていると述べ、実例的なウェブサイトにJava版のアルゴリズムを提供し、JSで書き換える:
    function toOtsu() {
      let ptr = 0
      let histData = Array(256).fill(0) //   0-256        ,    0
      let total = grayData.length
    
      while (ptr < total) {
        let h = 0xFF & grayData[ptr++]
        histData[h]++
      }
    
      let sum = 0        //   (   x  )
      for (let i = 0; i < 256; i++) {
        sum += i * histData[i]
      }
    
    
      let wB = 0         //   (    )   
      let wF = 0         //   (    )   
      let sumB = 0       //     (  x  )  
      let varMax = 0     //          
      let threshold = 0  //   
    
      for (let t = 0; t < 256; t++) {
        wB += histData[t]       //   (    )     
        if (wB === 0) continue
        wF = total - wB         //   (    )     
        if (wF === 0) break
    
        sumB += t * histData[t] //   (  x  )  
    
        let mB = sumB / wB          //   (    )     
        let mF = (sum - sumB) / wF  //   (    )     
    
        let varBetween = wB * wF * (mB - mF) ** 2  //     
    
        if (varBetween > varMax) {
          varMax = varBetween
          threshold = t
        }
      }
    
      return threshold
    }
    上記で求めたしきい値に基づいて二値化し、しきい値より小さい階調値は0であり、しきい値より大きい階調値は255である.
    function toBinary() {
      const threshold = toOtsu(grayData, index)
      const imageData = context.createImageData(50, 50)
      const data = imageData.data
      const temp = []
    
      grayData.forEach((v, i) => {
        let gray = v > threshold ? 255 : 0
        data[i * 4] = data[i * 4 + 1] = data[i * 4 + 2] = gray
        data[i * 4 + 3] = 255
        temp.push(gray > 0 ? 0 : 1)
      })
    
      canvas.width = 50
      canvas.height = 50
      context.putImageData(imageData, 0, 0)
    }
    最後に、2つの画像の各ピクセルの値の合計のパーセンテージを計算します.
    function toCompare() {
      let sameCount = 0
      // img1_data
      // img2_data
    
      const total = img1_data.length
      for (let i = 0; i < total; i++) {
        sameCount += img1_data[i] === img2_data[i]
      }
    
      console.log((sameCount / total * 100).toLocaleString() + '%')
    }
    
    どこで間違えたのか分かりませんが、この方法で計算した結果は理想的ではないと思います.実例デモ