原理分析
为了便于分析原理,我写了一个简单的Demo,如下图所示,在Demo页面中有三张单色图片,第一张原始图是颜色为#999的灰色图片,第二张水印图是颜色为#fff透明度为0.7的图片,第三张结果图是前两张图片合成以后得到的图片。
在右侧的控制台打印出了这三张图片的数据(数据是通过canvas画布中的getImageData方法获取到的),从数据中可以看到,三张图片的宽高均为100像素,数据长度均为40000。可以看出,每张图片有100*100个像素点,每个像素点用4个长度来表示,分别是R(红)、G(绿)、B(蓝)、A(透明度)。
接下来,对比下三张图片数据有何不同,原图片和水印图的数据是怎么得到结果图的数据的?
稍微想一下,应该可以得出一个思路:水印图的透明度为0.7,意味着它的色值在结果图中占的比重是70%,原始图因为被水印图遮挡,所以占的比重是剩余的30%。于是,153 × 0.3 + 255 × 0.7 = 224.4,向上取整后为225,正好与结果图中数据相等。
功能实现
实践是检验理论的唯一手段。根据上述猜测的计算方式,如果已知结果图和水印图,那么应该就可以推算出来原始图。不过需要准备好与待处理图片相对应的水印图片,确保它们的尺寸大小和水印位置是一致的,否则靠程序自己去猜测水印位置的话,需要额外复杂的算法和很大的计算量支撑。
这里我设计的程序界面如下:
程序上方是参数配置,可以设置水印图片和下载目录,左侧区域是待处理图片列表区,可以将待处理的图片拖拽进来,右侧区域是图片预览区域,点选左侧某一条数据可以对其进行预览。
程序去水印的关键代码如下:
// 获取imgData
getImgData (imgObj) {
let canvasObj = document.createElement('canvas')
canvasObj.width = imgObj.width
canvasObj.height = imgObj.height
let ctx = canvasObj.getContext('2d')
ctx.drawImage(imgObj, 0, 0, canvasObj.width, canvasObj.height)
let imgData = ctx.getImageData(0, 0, canvasObj.width, canvasObj.height)
return imgData.data
}
let imgData = this.imagesList[index].imgData
let imgData_sy = this.watermarkInfo.imgData
if (imgData.length !== imgData_sy.length) {
this.$toast('待处理图片与水印图片大小不一致')
return reject('待处理图片与水印图片大小不一致')
}
let bakColor = null
for (let i = imgData.length - 4; i > -1; i -= 4) {
if (!imgData_sy[i + 3]) {
bakColor = [imgData[i + 0], imgData[i + 1], imgData[i + 2], imgData[i + 3]]
continue
} else if (imgData_sy[i + 3] === 255 && bakColor) {
imgData[i + 0] = bakColor[0]
imgData[i + 1] = bakColor[1]
imgData[i + 2] = bakColor[2]
imgData[i + 3] = bakColor[3]
continue
}
let opacity_sy = imgData_sy[i + 3] / 255
let opacity_raw = (255 - imgData_sy[i + 3]) / 255
imgData[i + 0] = (imgData[i + 0] - imgData_sy[i + 0] * opacity_sy) / opacity_raw
imgData[i + 1] = (imgData[i + 1] - imgData_sy[i + 1] * opacity_sy) / opacity_raw
imgData[i + 2] = (imgData[i + 2] - imgData_sy[i + 2] * opacity_sy) / opacity_raw
if (imgData[i + 3] > 0 && imgData[i + 3] < 255) {
imgData[i + 3] = (imgData[i + 3] - imgData_sy[i + 3]) / opacity_raw
}
}
this.imagesList[index].imgData = imgData
// imgData2dataUrl
let canvasObj = document.createElement('canvas')
canvasObj.width = item.imgObj.width
canvasObj.height = item.imgObj.height
let ctx = canvasObj.getContext('2d')
let imageData = new ImageData(imgData, canvasObj.width, canvasObj.height)
ctx.putImageData(imageData, 0, 0)
经测,常见的水印可以正常去除;对于不透明的水印,这里采用的是使用附近的图片色值来替代;假设原始图片本身也是半透明,这种情况下,透明度好计算,但色值计算存在些许误差,不过好在这种情况在现实中相对较少。