在做文档图像的OCR时,经常会遇到水印的问题,会导致文字检测与识别很容易出错,因此,去水印的功能非常有必要。我们在实现去水印的过程中,经历了几个版本,今天做一个回顾:
1. V1版本:根据颜色值范围进行过滤
因为我们看到的水印大多是比较淡的背景色,很自然的想法,我们根据颜色值的范围是不是就可以直接过滤掉了呢。先转成灰度图,将颜色值大于某个阈值的,直接设置为255(纯白色)。代码实现比较简单,就不写了。但是这样有比较大的问题,因为对于一些扫描档,字体上的颜色可能并不是那么均匀,如果直接根据像素的颜色值进行过滤,容易导致字体坑坑洼洼的感觉。
2. V2版本:根据周围是否有黑点进行过滤
第一种方案直接根据单个像素值进行过滤容易导致一些字体本身的颜色值也被过滤的缺陷,很直接的一个改进方向就是,根据该像素周围是否有黑点来判断该像素是否为字体上的像素。
实现上也比较简单,对每个像素进行循环,然后判断该像素及其周围(相当于一个3*3的卷积核)是否有黑点(判断的依据就是像素值是否小于某个阈值),如果没有黑点,则判断该像素为应该为白色点,直接设置为255。
开始的时候,看起来该算法效果还是可以的。但是,一个大问题是,这个算法跑的真的很慢。。。其实稍微分析就知道,对每个像素进行循环,能不慢吗?
3. V3版本:使用numpy和opencv来优化时间效率
说到优化执行速度,很自然的想法就是使用numpy和opencv的内置函数来替代循环,那自然效率就能起来。但是要怎么做呢?这是这个文章重点要讲的,不妨先来分析一下V2版本算法。
V2算法的核心思想是对每个像素的周围的像素判断是否有黑点的存在,从而来判断该点是否应该过滤掉。np和opencv并没有单独这样的函数,我们该怎么实现呢?
在神经网络里,卷积运算就能实现类似的功能,而且opencv也可以进行相应的卷积计算,这是大方向。因此,我们可以将V2版本的算法分拆成三个步骤:
- 计算每个像素点是否为黑点;
- 使用卷积核计算每个像素点周围黑点的数量;
- 将原图中黑点数量为0的像素点的像素值设置为255.
不过这样注意一个问题,边界点怎么处理?做过深度学习的其实也很容易想到,只要增加padding就可以了。下面直接上代码:
代码语言:javascript复制def rm_watermark(image, thr=200, convol=3):
"""
简单粗暴去水印,可将将pdf或者扫描件中水印去除
使用卷积来优化计算
:param image: 输入图片,cv格式灰度图像
:param thr: 去除图片中像素阈值
:param convol: 卷积窗口的大小
:return: 返回np.array格式图片
"""
border = int((convol - 1) / 2) # 为了执行卷积,对图像连缘进行像素扩充
# 使用白色来进行边缘像素扩充
# 4个border: top, bottom, left, right
image = cv2.copyMakeBorder(image, border, border, border, border,
cv2.BORDER_CONSTANT, value=255)
# 生成模板: 黑点为1,白点为0
mask = (image < thr).astype(int)
# 卷积滤波: 求和
# -1表示通道数保持不变
# normalize: 表示是否进行归一化处理
mask = cv2.boxFilter(mask, -1, (convol, convol), normalize=False)
mask = (mask >= 1).astype(int) # 掩膜构建完成,>=1表示窗口内有黑点
image[np.where(mask == 0)] = 255 # 掩膜中为0的位置赋值为255,白色,达到去水印效果
h, w = image.shape[:2]
image = image[border:h-border, border:w-border]
return image
算法思路看起来比前一个版本复杂,但是这里没有使用循环,实际运行比直接使用循环快1到2个数量级,一页图像在百毫秒的级别。
4. 后续
后来发现其实这样百毫秒还是不够快,于是就想把这个算法直接移植到GPU上去运行,不过考虑到这个算法其实还是不够好的,后来还是直接使用深度学习训练模型来解决。有机会可以讲讲这个。
5. 小结
python中循环效率是比较低的,怎么将循环改变为不用循环的形式往往是性能提升的关键,可以充分利用numpy的内置函数,或者其他工具包的内置函数。