一。背景
现在非常多的公司和个人,都需要使用到网上的图片,比如用来做实验,用来做展示等等。但是呢,很多网上的图片,其实并不是免费使用的,都是有版权的,你要是使用了别人的、“未公开”的图片,那么别人可能就要起诉你了。
所以,当我们需要使用到网上的图片的时候,一定看搞清楚你使用的图片是否合法。如果是少量图片,那么我们还可以人工观察是否有水印,但是如果是大批量的图片或者不定时的图片(比如开放给用户上传图片的页面,用户就可能上传其他竞品公司的图片进来,就可能造成侵权),那么通过人工就不好检测了。那么就需要技术来识别图片是否带水印了,从而达到高效、实时下架非法数据的作用。
由于单个的深水印(水印小,而且明显),很好实现识别,下面讨论的都是全屏的浅水印。(本文以:中国最大的房产交易平台——房多多的图片为例,给大家讲解水印识别)
二。下面以python代码为例,讲解识别全屏水印的整个流程
0,流程图:(https://www.processon.com/view/link/5dca1ca4e4b0e3a6348a53fa)
1,先准备数据;
1.1 提取水印边缘图的图片 —————— 图片大小相同、水印位置大小相同的图片;(比如20张-30张)
1.2 用于计算阈值的带水印的图片(约50张);
1.3 用于计算阈值的不带水印的图片(约50张);
2,提取水印边缘图;
(1)基于sobel算子,计算x方向的梯度、y方向的梯度;
gradx = map(lambda x: cv2.Sobel(x, cv2.CV_64F, 1, 0, ksize=KERNEL_SIZE), images)
grady = map(lambda x: cv2.Sobel(x, cv2.CV_64F, 0, 1, ksize=KERNEL_SIZE), images)
(2)基于多张模板图片,计算中值梯度;
Wm_x = np.median(np.array(gradx), axis=0)
Wm_y = np.median(np.array(grady), axis=0)
(3)基于上面x、y方向上的中值梯度,得到总梯度;
Wm = (np.average(np.sqrt(np.square(gx) + np.square(gy)), axis=2))
(4)归一化到 [0,255]的整数
_Wm = (255 * PlotImage(Wm)).astype(np.uint8)
(5) ostu进行二值化,阈值为0和255
_, W_2 = cv2.threshold(_Wm, 0, 255, cv2.THRESH_OTSU)
3,提取子水印边缘图
(1)膨胀5次
W_2_dilate = cv2.dilate(bin_img, np.ones((2, 2), np.uint8), 5, iterations=5)
(2) 连通区域标记
label_image = measure.label(W_2_dilate).astype(np.uint8)
(3)连通区域的提取(拿到最多像素值的连通区域即可)
# 循环得到每一个连通区域属性集
for region in measure.regionprops(label_image):
minr, minc, maxr, maxc = region.bbox
_max_value = np.sum((bin_img[minr:maxr, minc:maxc] > 0) * 1.0)
if _max_value > max_value:
max_value = _max_value
best_boundary = minr, minc, maxr, maxc
best_sub_wm = bin_img[minr:maxr, minc:maxc]
=================== 下面是计算待识别图片的最佳重合度 ==============
4,提取“待识别图片”的边缘(使用canny算子提取边缘,用弱边缘减去强边缘来优化)
# 对原图进行canny得到边沿图。
# 使用canny进行非极大值抑制。https://blog.csdn.net/fengye2two/article/details/79190759
img_edgemap = (cv2.Canny(img, 18, 20)) # 弱边缘
img_edgemap_strong = (cv2.Canny(img, 20, 200)) # 强边缘
img_edgemap_weak = img_edgemap * 1.0 - img_edgemap_strong * 1.0
我们可以从上图发现,经过减少强边缘,既能保留原来的水印边缘,又可以去除部分非水印边缘,起到优化的效果。
5,尺寸截取———— 将2的水印边缘图和4的待识别图片边缘图,截取成相同大小的图片
# 模板水印、图片,剪切成尽量大的图片。。并缩放到0和1
m, n, p = np.shape(img) # 这个是图片的大小
m1, n1, p1 = np.shape(gx) # 这个是水印的大小
W_2 = W_2[:min(m, m1), :min(n, n1)] / 255
img_edgemap_weak = img_edgemap_weak[:min(m, m1), :min(n, n1)] / 255
————————这步是为了识别不同大小的图片。(这样截取的前提是水印并没有根据图片大小进行缩放,而且水印都是左上角对齐)
6,计算重合度;
(1)# 计算重合图片【不用卷积】
img_wm = np.multiply(img_edgemap_weak, W_2)
(2)# 计算最佳的子水印 与 待识别图的边缘图 的重合度。【注意:使用0边界填充参数】
chamfer_dist = cv2.filter2D(img_wm.astype(float), -1, sub_wm.astype(float), borderType=cv2.BORDER_CONSTANT)
max_sim = np.max(chamfer_dist) / np.sum(sub_wm) # 卷积重合的效果,跟理想最佳的重合的比例。(值域:[0,1])
7,针对1.2 和1.3 的图片,计算各自的重合度;然后设定阈值;
8,基于7中的阈值,识别待识别的图片。
if max_sim > THRESHOLD:
return True
else:
return False
三。其他的疑问:
1,同样是提取边缘,怎么一会是sobel算子,一会是canny算子?
这个就是实验才知道哪个比较适用。不同场景下使用不同的算子会有不同的效果。
2,这整个流程是怎么来的?
摸索着来的。。比如:1:为什么要提取子水印边缘图?因为全屏的水印边缘图,区分度不够,发现子水印边缘图区分度高。
比如2:使用canny算子提取边缘,阈值为什么是18,20,200?每个参数有它的意义,要先搞清楚参数的意义。然后具体来说,为什么是18,而不是17,那就是要人工调试了,发现哪个的效果好,就用哪个。