如何从文档图片中去除水印?

8
我有以下图片: 和另一个完全相同的带有相同标志的变体。
我想要去除标志并保留底层文本。使用以下代码段:
import skimage.filters as filters
import cv2

image = cv2.imread('ingrained.jpeg')

gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
smooth1 = cv2.GaussianBlur(gray, (5,5), 0)
division1 = cv2.divide(gray, smooth1, scale=255)

sharpened = filters.unsharp_mask(division1, radius=3, amount=7, preserve_range=False)
sharpened = (255*sharpened).clip(0,255).astype(np.uint8)

# line segments
components, output, stats, centroids = cv2.connectedComponentsWithStats(sharpened, connectivity=8)
sizes = stats[1:, -1]; components = components - 1
size = 100
result = np.zeros((output.shape))
for i in range(0, components):
    if sizes[i] >= size:
        result[output == i + 1] = 255

cv2.imwrite('image-after.jpeg',result)

我得到了这些结果

但如图所示,由于水印边缘残留和字母消失,导致生成的图像不一致。是否有更好的解决方案?理想的解决方案将是在不影响位于其下面的文本的情况下去除水印边框。

2个回答

8

由于我们知道水印是粉色的,因此可以使用两次HSV颜色阈值方法。第一步是移除大部分水印而保留字母的完整性,第二步是进一步过滤粉色。以下是一个可能的解决方案:

  1. 第一次HSV颜色阈值。 加载图像,转换为HSV格式,然后进行HSV颜色阈值以获取二进制图像。

  2. 膨胀修复轮廓。 由于任何类型的阈值处理都会导致字母变得模糊,因此我们需要通过膨胀来重建一些字符的轮廓。

  3. 第二次HSV颜色阈值。 现在,我们对原始图像进行位与操作,获取第一次HSV掩模的中间结果,但仍然存在粉色伪影。为了去除它们,我们通过生成新的掩模来执行第二次HSV阈值以消除字符周围的粉色。

  4. 将图像转换为灰度图像,然后删除粉色轮廓。 我们将第一次HSV颜色阈值的结果转换为灰度图像,然后将背景从黑色变为白色。最后,我们应用第二次HSV掩模的结果,以获得最终结果。


输入图像 -> 第一次HSV掩码 + 膨胀 -> 位与操作

请注意,背景粉色已经消失,但字母周围仍然存在粉色伪影。因此,现在我们为剩余的粉色生成第二个掩模。

第二个掩模 -> 转换为灰度图像 + 反转 -> 应用第二个掩模以获得结果

放大的结果

这里输入图像描述

代码

import numpy as np
import cv2

# Load image, convert to HSV, then HSV color threshold
image = cv2.imread('1.jpg')
original = image.copy()
hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
lower = np.array([0, 0, 0])
upper = np.array([179, 255, 163])
mask = cv2.inRange(hsv, lower, upper)

# Dilate to repair
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
dilate = cv2.dilate(mask, kernel, iterations=1)

# Second pass of HSV to remove pink
colored = cv2.bitwise_and(original, original, mask=dilate)
colored_hsv = cv2.cvtColor(colored, cv2.COLOR_BGR2HSV)
lower_two = np.array([96, 89, 161])
upper_two = np.array([179, 255, 255])
mask_two = cv2.inRange(colored_hsv, lower_two, upper_two)

# Convert to grayscale then remove pink contours
result = cv2.cvtColor(colored, cv2.COLOR_BGR2GRAY)
result[result <= 10] = 255
cv2.imshow('result before removal', result)
result[mask_two==255] = 255

cv2.imshow('dilate', dilate)
cv2.imshow('colored', colored)
cv2.imshow('mask_two', mask_two)
cv2.imshow('result after removal', result)
cv2.waitKey()

根据图片情况,您可能需要调整HSV范围的下限/上限。要确定HSV下限/上限范围,可以使用带滑块的HSV阈值脚本,这样您就不需要猜测和尝试了。只需更改图像路径即可。

import cv2
import numpy as np

def nothing(x):
    pass

# Load image
image = cv2.imread('1.jpg')

# Create a window
cv2.namedWindow('image')

# Create trackbars for color change
# Hue is from 0-179 for Opencv
cv2.createTrackbar('HMin', 'image', 0, 179, nothing)
cv2.createTrackbar('SMin', 'image', 0, 255, nothing)
cv2.createTrackbar('VMin', 'image', 0, 255, nothing)
cv2.createTrackbar('HMax', 'image', 0, 179, nothing)
cv2.createTrackbar('SMax', 'image', 0, 255, nothing)
cv2.createTrackbar('VMax', 'image', 0, 255, nothing)

# Set default value for Max HSV trackbars
cv2.setTrackbarPos('HMax', 'image', 179)
cv2.setTrackbarPos('SMax', 'image', 255)
cv2.setTrackbarPos('VMax', 'image', 255)

# Initialize HSV min/max values
hMin = sMin = vMin = hMax = sMax = vMax = 0
phMin = psMin = pvMin = phMax = psMax = pvMax = 0

while(1):
    # Get current positions of all trackbars
    hMin = cv2.getTrackbarPos('HMin', 'image')
    sMin = cv2.getTrackbarPos('SMin', 'image')
    vMin = cv2.getTrackbarPos('VMin', 'image')
    hMax = cv2.getTrackbarPos('HMax', 'image')
    sMax = cv2.getTrackbarPos('SMax', 'image')
    vMax = cv2.getTrackbarPos('VMax', 'image')

    # Set minimum and maximum HSV values to display
    lower = np.array([hMin, sMin, vMin])
    upper = np.array([hMax, sMax, vMax])

    # Convert to HSV format and color threshold
    hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
    mask = cv2.inRange(hsv, lower, upper)
    result = cv2.bitwise_and(image, image, mask=mask)

    # Print if there is a change in HSV value
    if((phMin != hMin) | (psMin != sMin) | (pvMin != vMin) | (phMax != hMax) | (psMax != sMax) | (pvMax != vMax) ):
        print("(hMin = %d , sMin = %d, vMin = %d), (hMax = %d , sMax = %d, vMax = %d)" % (hMin , sMin , vMin, hMax, sMax , vMax))
        phMin = hMin
        psMin = sMin
        pvMin = vMin
        phMax = hMax
        psMax = sMax
        pvMax = vMax

    # Display result image
    cv2.imshow('image', result)
    if cv2.waitKey(10) & 0xFF == ord('q'):
        break

cv2.destroyAllWindows()

水印颜色范围有时会变化,因为我有另一张相同图像的实例,带有另一种(也可能不同的)黑色标题文本的水印。是否有更动态的方法来处理这样的不一致性?到目前为止,图像锐化更加一致,底层文本的结果更易读,但剩余的轮廓是唯一的缺点。如果剩余的连接线可以无视颜色调色板而交替起始点,则更好。 - kak
尝试在锐化后去除轮廓是相当困难的,因为没有独特的特征来隔离不需要的轮廓。如果您坚持采用这种方法,我脑海中另一种方法是尝试通过膨胀将字符按行分开,但我仍然看不到任何简单的方法来去除额外的轮廓。 - nathancy
我已经回到了之前的版本,因为当前的版本产生了类似褪色字母的结果,并且在添加其他颜色时得到了极其混乱的结果,例如标题是黑色而不是粉色。最终目标只是简单的改进,而不是最佳清晰结果,所以我想去除轮廓或长度超过普通字母长度的不连续扭曲线条就足够了。我添加的当前版本,如第一张图片所示,似乎并不总是能够完成工作。 - kak
1
披萨馅肠已被撕成碎片 - nathancy
@yusif 你可以看到,链接的帖子检测灰色像素,而你在这里试图保留的文档中的文本也是灰色的。 - Ann Zen
显示剩余10条评论

6

概念

为此,我使用了两个简单的HSV掩模;一个用于淡化标志(使用简单公式),另一个则通过完全删除标志来完成掩模处理。

以下依次是原始图像预掩蔽图像完全掩蔽图像

以下是两个掩模的外观:

输出结果

enter image description here

代码

import cv2
import numpy as np

def HSV_mask(img_hsv, lower):
    lower = np.array(lower)
    upper = np.array([255, 255, 255])
    return cv2.inRange(img_hsv, lower, upper)
    
img = cv2.imread("image.jpg")
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img_gray[img_gray >= 235] = 255
mask1 = HSV_mask(img_hsv, [0, 0, 155])[..., None].astype(np.float32)
mask2 = HSV_mask(img_hsv, [0, 20, 0])
masked = np.uint8((img + mask1) / (1 + mask1 / 255))
gray = cv2.cvtColor(masked, cv2.COLOR_BGR2GRAY)
gray[gray >= 180] = 255
gray[mask2 == 0] = img_gray[mask2 == 0]

cv2.imshow("result", gray)
cv2.waitKey(0)

解释

  1. 导入所需的库:
import cv2
import numpy as np

定义一个函数HSV_mask,接受一个图像(已转换为HSV颜色空间),以及HSV掩码的下限范围(上限范围将是255, 255, 255),并返回HSV掩码。
def HSV_mask(img_hsv, lower):
    lower = np.array(lower)
    upper = np.array([255, 255, 255])
    return cv2.inRange(img_hsv, lower, upper)
  1. 导入图像image.jpg,并定义另外两个变量来分别保存将图像转换为HSV和灰度格式后的结果。对于灰度图像,将所有大于或等于235的像素替换成255,以消除图像中白色部分的一些噪声:
img = cv2.imread("image.jpg")
img_hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
img_gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
img_gray[img_gray >= 235] = 255
  1. 定义两个变量mask1mask2,使用之前定义的HSV_mask函数。 mask1将屏蔽除文本外的一切内容,而mask2将屏蔽除标志以外的一切内容:
mask1 = HSV_mask(img_hsv, [0, 0, 155])[..., None].astype(np.float32)
mask2 = HSV_mask(img_hsv, [0, 20, 0])

使用mask1和一个淡化(但不是清除)标志的公式来掩盖原始图像。 这只是预处理步骤,以便稍后可以干净地删除标志。
masked = np.uint8((img + mask1) / (1 + mask1 / 255))

将带有褪色标志的图像转换为灰度,并应用mask2,以便所有被该掩码遮盖的像素都将转换回原始图像:
gray = cv2.cvtColor(masked, cv2.COLOR_BGR2GRAY)
gray[gray >= 180] = 255
gray[mask2 == 0] = img_gray[mask2 == 0]

最后,展示结果:
cv2.imshow("result", gray)
cv2.waitKey(0)

不幸的是,它遭受了与我的第二张图像相同的不一致性问题,并且也没有接受第一个答案。水印悬停的标题颜色相当多样化,因此会出现混乱的结果,例如12,以及最初的3 - kak
啊,又是一种HSV颜色阈值方法,看到@yusif,没有使用颜色作为唯一标识符真的很难去除粉色。找到一个动态解决方案将会非常困难,通常在图像处理任务中,它们是基于特定情况的,尤其是当颜色变化时。 - nathancy
我现在明白了,但即使采用逐个处理的方法,我认为HSV颜色模式也无法处理这些混合颜色,例如黑色标题附加到水印上。 - kak
@yusif “但即使采用逐个案例的方法,我认为HSV颜色也无法处理这些诱导出的颜色。” 这并不完全正确;您只需要微调参数。 无论如何,您处理图像的用途是否必须删除水印? 我不知道,因为您没有说明,但如果是这样,我们可能会找到一种解决方案,既不会删除水印,又能完成相同的任务。 - Ann Zen
@AnnZen 最初只是有关纯粹的文本提取,但由于加入了水印,OCR 通常难以处理文本。您只需要微调参数即可。 您能否更新一下代码,并为这种不同的黑色标题情况添加注释? - kak
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接