一、平均哈希算法
第一步:
缩小尺寸为8×8,以去除图片的细节,只保留结构、明暗等基本信息,摒弃不同尺寸、比例带来的图片差异。
第二步:
简化色彩。将缩小后的图片转为灰度图像 (取每个像素rgb三色的平均值,[228, 233, 253, 255]-> [238, 238, 238, 255] )。
第三步:
计算平均值。计算所有像素的灰度平均值。(根据每个像素平均后的值, 计算整张图片的平均颜色值)
第四步:
比较像素的灰度。将64个像素的灰度,与平均值进行比较。大于或等于平均值,记为1;小于平均值,记为 0。
第五步:
计算哈希值。将上一步的比较结果,组合在一起,就构成了一个64位的整数,这就是这张图片的指纹。
第六步:
计算指纹的相似度,得出两张图片是否相似 。
// 将图片压缩成 8*8大小
function compressImg (imgSrc, imgWidth = 8) {
return new Promise((resolve, reject) => {
if (!imgSrc) {
reject('imgSrc can not be empty!')
}
const canvas = document.getElementById('canvas1')
const ctx = canvas.getContext('2d')
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = function () {
canvas.width = imgWidth
canvas.height = imgWidth
ctx?.drawImage(img, 0, 0, imgWidth, imgWidth)
const data = ctx?.getImageData(0, 0, imgWidth, imgWidth)
resolve(data)
}
img.src = imgSrc
})
}
// 色彩简化、计算RGB的平均值,分别设置给RGB
function createGrayscale (imgData) {
const newData = 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)
}
// 因为每个像素是 RGBA 组成的、 A 固定255, 所以每4个数里取一个就行
function getHashFingerprint (imgData) {
const grayList = imgData.data.reduce((pre, cur, index) => {
if ((index + 1) % 4 === 0) {
pre.push(imgData.data[index - 1])
}
return pre
}, [])
console.log("64个颜色的平均rgb:",grayList)
const length = grayList.length
const grayAverage = Math.round(grayList.reduce((pre, next) => (pre + next), 0) / length)
console.log("简化后的图片所有像素平均色彩值:",grayAverage)
return grayList.map(gray => (gray >= grayAverage ? 1 : 0)).join('')
}
二、颜色分布法
借用一篇文章的解释:
1、 每张图片都可以生成颜色分布的直方图、如果两张图的直方图很接近,就可以认为他们相似
2、 任何一种颜色都是由红绿蓝三原色组成的、我们平时用的每种原色有256个色值,整个颜色空间有1600万种颜色(256的三次方)
3、 如果用1600万种颜色去比较直方图、计算量太大了、因此需要采用简化方法,可以将0~256分城4个区、 0-63为0区,64-127为1区,128-191为2区,192-256为3区;
也就以为这简化后的图片总共会有64种颜色(4的3次方)、
4、统计64种颜色组合包含的像素数量
5、我们这里想展示每一步操作后的图片变化,取 0,1,2,3的话整张图片都是黑色、所以 0-63取的是32,64-127 取 96,128-191取 160, 192-256取 224;
6、因为颜色的组合都是固定的,所以把表格中的最后一栏(像素数量)提出来,组成一个64维向量, 便是这张图片的 “指纹”, 寻找相似的图片就是寻找相似的“指纹”
let keyList = []
function compressImg (imgSrc, imgWidth = 120) {
return new Promise((resolve, reject) => {
if (!imgSrc) {
reject('imgSrc can not be empty!')
}
const canvas = document.getElementById('canvas1')
const ctx = canvas.getContext('2d')
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = function () {
canvas.width = imgWidth
canvas.height = imgWidth
ctx?.drawImage(img, 0, 0, imgWidth, imgWidth)
const data = ctx?.getImageData(0, 0, imgWidth, imgWidth)
resolve(data)
}
img.src = imgSrc
})
}
// 根据 RGBA 数组生成 ImageData
function createImgData (dataDetail) {
const canvas = document.getElementById('canvas2')
const ctx = canvas.getContext('2d')
const imgWidth = Math.sqrt(dataDetail.length / 4)
const newImageData = ctx?.createImageData(imgWidth, imgWidth)
for (let i = 0; i < dataDetail.length; i += 4) {
let R = dataDetail[i]
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
}
ctx.putImageData(newImageData,0,0);
return newImageData
}
// 划分颜色区间,默认区间数目为4个
// 把256种颜色取值简化为4种
function simplifyColorData (imgData, zoneAmount=4) {
const colorZoneDataList = []
const zoneStep = 256 / zoneAmount
const zoneBorder = [0] // 区间边界
for (let i = 1; i <= zoneAmount; i++) {
zoneBorder.push(zoneStep * i - 1)
}
imgData.data.forEach((rgb, index) => {
if ((index + 1) % 4 !== 0) {
// for (let i = 0; i < zoneBorder.length; i++) {
// if (rgb > zoneBorder[i] && rgb <= zoneBorder[i + 1]) {
// rgb = i
// }
// }
if(rgb<64){
rgb= 32
}else if(rgb<128){
rgb= 96
}else if(rgb<192){
rgb= 160
}else{
rgb= 224
}
}
colorZoneDataList.push(rgb)
})
return createImgData(colorZoneDataList)
// return createImgData(imgData.data)
}
// 把色值归类到不同的分组里
function seperateListToColorZone (simplifiedDataList) {
const zonedList = []
let tempZone = []
simplifiedDataList.forEach((data, index) => {
if ((index + 1) % 4 !== 0) {
tempZone.push(data)
} else {
zonedList.push(JSON.stringify(tempZone))
tempZone = []
}
})
return zonedList
}
// 统计相同的分组数量
function getFingerprint (zonedList, zoneAmount=256) {
const colorSeperateMap = {}
const list = []
for (let i = 32; i < zoneAmount; i+=64) {
for (let j = 32; j < zoneAmount; j+=64) {
for (let k = 32; k < zoneAmount; k+=64) {
list.push(JSON.stringify([i, j, k]))
colorSeperateMap[JSON.stringify([i, j, k])] = 0
}
}
}
keyList=list
zonedList.forEach(zone => {
colorSeperateMap[zone]++
})
return Object.values(colorSeperateMap)
}
三、内容特征法
”内容特征法“是指把图片转化为灰度图后再转化为”二值图“,然后根据像素的取值(黑或白)形成指纹后进行比对的方法。这种算法的核心是找到一个“阈值”去生成二值图。
1、首先,将原图转化成一张灰度图片 、(可以压缩可不压缩、 最好是压缩成固定大小:可以相似图片因为图片大小不一样带来的差异, 减少一定的计算,减小最终指纹的长度)
2、确定一个阈值,将灰度图片转化为黑白图片
// 根据 RGBA 数组生成 ImageData
function createImgData (dataDetail,canvasId) {
const canvas = document.getElementById(canvasId)
const ctx = canvas.getContext('2d')
const imgWidth = Math.sqrt(dataDetail.length / 4)
const newImageData = ctx?.createImageData(imgWidth, imgWidth)
for (let i = 0; i < dataDetail.length; i += 4) {
let R = dataDetail[i]
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
}
ctx.putImageData(newImageData,0,0);
return newImageData
}
const GrayscaleWeight = {
R : .299,
G : .587,
B : .114
}
function compressImg (imgSrc, imgWidth = 240) {
return new Promise((resolve, reject) => {
if (!imgSrc) {
reject('imgSrc can not be empty!')
}
const canvas = document.getElementById('canvas1')
const ctx = canvas.getContext('2d')
const img = new Image()
img.crossOrigin = 'Anonymous'
img.onload = function () {
canvas.width = imgWidth
canvas.height = imgWidth
ctx?.drawImage(img, 0, 0, imgWidth, imgWidth)
const data = ctx?.getImageData(0, 0, imgWidth, imgWidth)
resolve(data)
}
img.src = imgSrc
})
}
function toGray (imgData) {
const grayData = []
const data = imgData.data
for (let i = 0; i < data.length; i += 4) {
// ~~两次按位取反,就是保持原值 不过会把布尔值转换成 0 或 1
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
}
// 大律法 u=w0*u0+w1*u1
// w0:前景点数占图像比例
// u0:前景点数平均灰度
// w1:背景点数占图像比例
// u1:背景点平均灰度为
// u = 62
// 100 个 80个
// 120 4
// 黑色部分的
// g=w0*(u0-u)**2 + w1*(u1-u)**2
// 直接应用大津法计算量较大,因此我们在实现时采用了等价的公式 g=w0*w1*(u0-u1)**2。
// 通过toGray() 方法获取到灰度值列表,根据“大津法”算出最佳阈值
function OTSUAlgorithm (imgData) {
const grayData = toGray(imgData)
console.log("灰度值列表!!!!", grayData)
let ptr = 0
let histData = Array(256).fill(0)
let total = grayData.length
// 统计图片灰度值的引用点数
while (ptr < total) {
// grayData[ptr++] % 256 其实不用,灰度值最大也就256
let h = 0xFF & grayData[ptr++]
histData[h]++
}
let sum = 0
for (let i = 0; i < 256; i++) {
sum += i * histData[i] // 灰度值 * 灰度值数量
}
// sum 图片所有灰度值总和 * 灰度值
let w0 = 0 // 灰度值数量总和
let w1 = 0 // 剩余灰度数量
let sumB = 0 // 灰度值 * 灰度值数量 总和 前景点灰度总数
let varMax = 0 // 最大阈值
let threshold = 0
console.log("histData histData",histData)
for (let t = 0; t < 256; t++) {
w0 += histData[t]
if (w0 === 0) continue
w1 = total - w0
if (w1 === 0) break
// t 灰度值 * 灰度值数量
sumB += t * histData[t]
// console.log("sumB sumB sumB",sumB)
// 前景点平均灰度 mB 平均灰度
let u0 = sumB / w0
// 剩余灰度总值 / 剩余灰度key数量
// 后景点平均灰度
let u1 = (sum - sumB) / w1
// 最大类间方差 x **2 表示 x的二次方
// 灰度值数量 * 平均灰度 -
let varBetween = w0 * w1 * (u0 - u1) ** 2
// 每个灰度值的方差、取最大的
if (varBetween > varMax) {
varMax = varBetween
threshold = t
}
}
return threshold
}
function binaryzation (imgData, threshold) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
const imgWidth = Math.sqrt(imgData.data.length / 4)
const newImageData = ctx?.createImageData(imgWidth, imgWidth)
const fingerprint =[]
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
fingerprint.push(sum > threshold ? 1 : 0)
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
}
createImgData(newImageData.data,'canvas4')
return fingerprint
}