# 特征提取算法

## 平均哈希算法

#### 图片压缩：

``````export function compressImg (imgSrc: string, imgWidth: number = 8): Promise<ImageData> {
return new Promise((resolve, reject) => {
if (!imgSrc) {
reject('imgSrc can not be empty!')
}
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const img = new Image()
img.crossOrigin = 'Anonymous'
canvas.width = imgWidth
canvas.height = imgWidth
ctx?.drawImage(img, 0, 0, imgWidth, imgWidth)
const data = ctx?.getImageData(0, 0, imgWidth, imgWidth) as ImageData
resolve(data)
}
img.src = imgSrc
})
}

#### 图片灰度化

``````// 根据 RGBA 数组生成 ImageData
export function createImgData (dataDetail: number[]) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const imgWidth = Math.sqrt(dataDetail.length / 4)
const newImageData = ctx?.createImageData(imgWidth, imgWidth) as ImageData
for (let i = 0; i < dataDetail.length; i += 4) {
let G = dataDetail[i + 1]
let B = dataDetail[i + 2]
let Alpha = dataDetail[i + 3]

newImageData.data[i] = R
newImageData.data[i + 1] = G
newImageData.data[i + 2] = B
newImageData.data[i + 3] = Alpha
}
return newImageData
}

export function createGrayscale (imgData: ImageData) {
const newData: number[] = Array(imgData.data.length)
newData.fill(0)
imgData.data.forEach((_data, index) => {
if ((index + 1) % 4 === 0) {
const R = imgData.data[index - 3]
const G = imgData.data[index - 2]
const B = imgData.data[index - 1]

const gray = ~~((R + G + B) / 3)
newData[index - 3] = gray
newData[index - 2] = gray
newData[index - 1] = gray
newData[index] = 255 // Alpha 值固定为255
}
})
return createImgData(newData)
}

`ImageData.data` 是一个 Uint8ClampedArray 数组，可以理解为“RGBA数组”，数组中的每个数字取值为0~255，每4个数字为一组，表示一个像素的 RGBA 值。由于`ImageData` 为只读对象，所以要另外写一个 `creaetImageData()` 方法，利用 `context.createImageData()` 来创建新的 `ImageData` 对象。

#### 指纹提取

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

## 感知哈希算法

• 缩小尺寸：pHash以小图片开始，但图片大于88，3232是最好的。这样做的目的是简化了DCT的计算，而不是减小频率。
• 简化色彩：将图片转化成灰度图像，进一步简化计算量。
• 计算DCT：计算图片的DCT变换，得到32*32的DCT系数矩阵。
• 缩小DCT：虽然DCT的结果是3232大小的矩阵，但我们只要保留左上角的88的矩阵，这部分呈现了图片中的最低频率。
• 计算平均值：如同均值哈希一样，计算DCT的均值。
• 计算hash值：这是最主要的一步，根据8*8的DCT矩阵，设置0或1的64位的hash值，大于等于DCT均值的设为”1”，小于DCT均值的设为“0”。组合在一起，就构成了一个64位的整数，这就是这张图片的指纹。

``````function memoizeCosines (N: number, cosMap: any) {
cosMap = cosMap || {}
cosMap[N] = new Array(N * N)

let PI_N = Math.PI / N

for (let k = 0; k < N; k++) {
for (let n = 0; n < N; n++) {
cosMap[N][n + (k * N)] = Math.cos(PI_N * (n + 0.5) * k)
}
}
return cosMap
}

function dct (signal: number[], scale: number = 2) {
let L = signal.length
let cosMap: any = null

if (!cosMap || !cosMap[L]) {
cosMap = memoizeCosines(L, cosMap)
}

let coefficients = signal.map(function () { return 0 })

return coefficients.map(function (_, ix) {
return scale * signal.reduce(function (prev, cur, index) {
return prev + (cur * cosMap[L][index + (ix * L)])
}, 0)
})
}

``````// 一维数组升维
function createMatrix (arr: number[]) {
const length = arr.length
const matrixWidth = Math.sqrt(length)
const matrix = []
for (let i = 0; i < matrixWidth; i++) {
const _temp = arr.slice(i * matrixWidth, i * matrixWidth + matrixWidth)
matrix.push(_temp)
}
return matrix
}

// 从矩阵中获取其“左上角”大小为 range × range 的内容
function getMatrixRange (matrix: number[][], range: number = 1) {
const rangeMatrix = []
for (let i = 0; i < range; i++) {
for (let j = 0; j < range; j++) {
rangeMatrix.push(matrix[i][j])
}
}
return rangeMatrix
}

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

## 颜色分布法

``````// 划分颜色区间，默认区间数目为4个
// 把256种颜色取值简化为4种
export function simplifyColorData (imgData: ImageData, zoneAmount: number = 4) {
const colorZoneDataList: number[] = []
const zoneStep = 256 / zoneAmount
const zoneBorder = [0] // 区间边界
for (let i = 1; i <= zoneAmount; i++) {
zoneBorder.push(zoneStep * i - 1)
}
imgData.data.forEach((data, index) => {
if ((index + 1) % 4 !== 0) {
for (let i = 0; i < zoneBorder.length; i++) {
if (data > zoneBorder[i] && data <= zoneBorder[i + 1]) {
data = i
}
}
}
colorZoneDataList.push(data)
})
return colorZoneDataList
}

``````export function seperateListToColorZone (simplifiedDataList: number[]) {
const zonedList: string[] = []
let tempZone: number[] = []
simplifiedDataList.forEach((data, index) => {
if ((index + 1) % 4 !== 0) {
tempZone.push(data)
} else {
zonedList.push(JSON.stringify(tempZone))
tempZone = []
}
})
return zonedList
}

``````export function getFingerprint (zonedList: string[], zoneAmount: number = 16) {
const colorSeperateMap: {
[key: string]: number
} = {}
for (let i = 0; i < zoneAmount; i++) {
for (let j = 0; j < zoneAmount; j++) {
for (let k = 0; k < zoneAmount; k++) {
colorSeperateMap[JSON.stringify([i, j, k])] = 0
}
}
}
zonedList.forEach(zone => {
colorSeperateMap[zone]++
})
return Object.values(colorSeperateMap)
}

## 内容特征法

”内容特征法“是指把图片转化为灰度图后再转化为”二值图“，然后根据像素的取值（黑或白）形成指纹后进行比对的方法。这种算法的核心是找到一个“阈值”去生成二值图。

``````enum GrayscaleWeight {
R = .299,
G = .587,
B = .114
}

function toGray (imgData: ImageData) {
const grayData = []
const data = imgData.data

for (let i = 0; i < data.length; i += 4) {
const gray = ~~(data[i] * GrayscaleWeight.R + data[i + 1] * GrayscaleWeight.G + data[i + 2] * GrayscaleWeight.B)
data[i] = data[i + 1] = data[i + 2] = gray
grayData.push(gray)
}

return grayData
}

``````/ OTSU algorithm
// rewrite from http://www.labbookpages.co.uk/software/imgProc/otsuThreshold.html
export function OTSUAlgorithm (imgData: ImageData) {
const grayData = toGray(imgData)
let ptr = 0
let histData = Array(256).fill(0)
let total = grayData.length

while (ptr < total) {
let h = 0xFF & grayData[ptr++]
histData[h]++
}

let sum = 0
for (let i = 0; i < 256; i++) {
sum += i * histData[i]
}

let wB = 0
let wF = 0
let sumB = 0
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]

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
}

`OTSUAlgorithm()` 函数接收一个 `ImageData` 对象，经过上一步的 `toGray()` 方法获取到灰度值列表以后，根据“大津法”算出最佳阈值然后返回。接下来使用这个阈值对原图进行处理，即可获取二值图。

``````export function binaryzation (imgData: ImageData, threshold: number) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const imgWidth = Math.sqrt(imgData.data.length / 4)
const newImageData = ctx?.createImageData(imgWidth, imgWidth) as ImageData
for (let i = 0; i < imgData.data.length; i += 4) {
let R = imgData.data[i]
let G = imgData.data[i + 1]
let B = imgData.data[i + 2]
let Alpha = imgData.data[i + 3]
let sum = (R + G + B) / 3

newImageData.data[i] = sum > threshold ? 255 : 0
newImageData.data[i + 1] = sum > threshold ? 255 : 0
newImageData.data[i + 2] = sum > threshold ? 255 : 0
newImageData.data[i + 3] = Alpha
}
return newImageData
}

# 特征比对算法

## 汉明距离

• 1011101与1001001之间的汉明距离是2。

• 2143896与2233796之间的汉明距离是3。

• "toned"与"roses"之间的汉明距离是3。

``````export function hammingDistance (str1: string, str2: string) {
let distance = 0
const str1Arr = str1.split('')
const str2Arr = str2.split('')
str1Arr.forEach((letter, index) => {
if (letter !== str2Arr[index]) {
distance++
}
})
return distance
}

``````相似度 = (字符串长度 - 汉明距离) / 字符串长度

## 余弦相似度

``````export function cosineSimilarity (sampleFingerprint: number[], targetFingerprint: number[]) {
// cosθ = ∑n, i=1(Ai × Bi) / (√∑n, i=1(Ai)^2) × (√∑n, i=1(Bi)^2) = A · B / |A| × |B|
const length = sampleFingerprint.length
let innerProduct = 0
for (let i = 0; i < length; i++) {
innerProduct += sampleFingerprint[i] * targetFingerprint[i]
}
let vecA = 0
let vecB = 0
for (let i = 0; i < length; i++) {
vecA += sampleFingerprint[i] ** 2
vecB += targetFingerprint[i] ** 2
}
const outerProduct = Math.sqrt(vecA) * Math.sqrt(vecB)
return innerProduct / outerProduct
}

# 计算精度

img-compare.netlify.com/

• 对于两张颜色较为丰富，细节较多的图片来说，“颜色分布法”的计算结果是最符合直觉的。
• 对于两张内容相近但颜色差异较大的图片来说，“内容特征法”和“平均/感知哈希算法”都能得到符合直觉的结果。
• 针对“颜色分布法“，区间的划分数量对计算结果影响较大，选择合适的区间很重要。

• ssssyoki
5年前
• CUGGZ
2年前