如何在OpenCV中去除图像中的空白?

24

我有以下这张图像,上面有文字并且文字下面有大量的空白。我想裁剪掉空白部分,使其看起来像第二个图像。

在此输入图片描述

裁剪后的图像

在此输入图片描述

这是我所做的

>>> img = cv2.imread("pg13_gau.jpg.png")
>>> gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)
>>> edged = cv2.Canny(gray, 30,300)
>>> (img,cnts, _) = cv2.findContours(edged.copy(), cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
>>> cnts = sorted(cnts, key = cv2.contourArea, reverse = True)[:10]

2
只需在黑色像素的"boundingRect"上进行裁剪即可。 - Miki
boundingRect接受轮廓,我需要使用findContour来找到轮廓吗?如何获取左上角和右下角的黑色像素? - Anthony
1
boundingRect函数需要一个点向量作为输入,不需要找到轮廓。C++代码如下:std::vector<cv::Point> pts; cv::findNonZero(~gray); cv::Rect roi = cv::boundingRect(pts); cv::Mat1b crop = img(roi); Python代码也差不多。 - Miki
请查看此处的第7节cv2.boundingRect() - Jeru Luke
我做了以下操作,但结果图像不是裁剪后的图像:img = cv2.imread("ws.png"); gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY); coords = cv2.findNonZero(gray); rekt = cv2.boundingRect(coords) cv2.imwrite("rekt.png",rekt) - Anthony
cv2.boundingRect 返回的是边界框坐标,而不是裁剪后的图像。 - rayryeng
3个回答

28

正如评论中许多人所暗示的那样,最好的方法是反转图像,使黑色文本变为白色,找到图像中所有非零点,然后确定最小跨度边界框将是什么。您可以使用此边界框最终裁剪图像。在这里找轮廓非常昂贵,并且不需要 - 特别是因为您的文本是轴对齐的。您可以使用cv2.findNonZerocv2.boundingRect的组合来完成所需操作。

因此,类似以下内容会起作用:

import numpy as np
import cv2

img = cv2.imread('ws.png') # Read in the image and convert to grayscale
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = 255*(gray < 128).astype(np.uint8) # To invert the text to white
coords = cv2.findNonZero(gray) # Find all non-zero points (text)
x, y, w, h = cv2.boundingRect(coords) # Find minimum spanning bounding box
rect = img[y:y+h, x:x+w] # Crop the image - note we do this on the original image
cv2.imshow("Cropped", rect) # Show it
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("rect.png", rect) # Save the image

上述代码恰好阐述了我在开头谈论的内容。我们读取图像,但由于某种原因图像是彩色的,因此我们也将其转换为灰度。其中棘手的部分是代码的第三行,在此我们将低于128强度的像素阈值化,以使暗文本变为白色。然而,这会产生二进制图像,因此我转换为uint8,然后缩放255。这实质上翻转了文本。

接下来,给定此图像,我们使用cv2.findNonZero找到所有非零坐标,最后将其输入到cv2.boundingRect中,它将为您提供边界框的左上角以及宽度和高度。我们最终可以使用这个来裁剪图像。请注意,我们是在原始图像上而不是翻转后的图像上进行操作。我们使用简单的NumPy数组索引来完成裁剪。

最后,我们展示图像以表明其有效性,并将其保存到磁盘。


现在我得到了这张图片:

enter image description here


对于第二张图片,一个好的方法是先裁剪掉右边框和底部边框。我们可以通过将图像裁剪到该位置来实现。接下来,该图像包含一些非常小的噪点像素,建议使用非常小的内核进行形态学开操作,然后重新执行上述逻辑。

因此:

import numpy as np
import cv2

img = cv2.imread('pg13_gau_preview.png') # Read in the image and convert to grayscale
img = img[:-20,:-20] # Perform pre-cropping
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
gray = 255*(gray < 128).astype(np.uint8) # To invert the text to white
gray = cv2.morphologyEx(gray, cv2.MORPH_OPEN, np.ones((2, 2), dtype=np.uint8)) # Perform noise filtering
coords = cv2.findNonZero(gray) # Find all non-zero points (text)
x, y, w, h = cv2.boundingRect(coords) # Find minimum spanning bounding box
rect = img[y:y+h, x:x+w] # Crop the image - note we do this on the original image
cv2.imshow("Cropped", rect) # Show it
cv2.waitKey(0)
cv2.destroyAllWindows()
cv2.imwrite("rect.png", rect) # Save the image

注意:由于隐私原因,输出图像已被移除


@Anthony,你需要先移除右边的一些边框以及底部的一些像素,然后这个代码才能正常工作。如果图像中有像右侧所见的杂点,则该代码无法正常工作。在进行边界框选择之前,请尝试对图像执行以下操作:img = img[:-20,:-20]。这将删除行和列的最后20个像素。 - rayryeng
啊,我明白了,因为这样图像中会有两个黑色区域。有没有办法通过编程来移除它们,以便裁剪可以按预期工作? - Anthony
@Anthony 是的,你绝对可以去除边框像素。在MATLAB中,有一个名为imclearborder的函数,可以删除与边框相连的任何像素。OpenCV没有这个函数,但我在之前提供的答案中编写了一个:https://dev59.com/DWAf5IYBdhLWcg3wXhsJ。请注意,这可能会变得计算密集,因为该过程是查找图像中的所有轮廓,然后检查是否有任何轮廓接触图像中的任何边框像素。然后我将这些轮廓的区域涂成黑色。 - rayryeng
1
@DanMašek 我知道。现在我无能为力,但第二张图片底部有一个非常小的黑点。如果我们在反转后找到了所有的非零像素,这个黑点将被包括在内,因此包围这组点的边界框将会扩展以包含这个嘈杂的黑点。只有当我下载并在GIMP上查看它时,我才能看到它。在进行裁剪之前,我使用形态学方法去除了它。 - rayryeng
2
你可以标记它以便管理员删除,或直接向Imgur发送删除请求 - Dan Mašek
显示剩余5条评论

3

Opencv 将图像读取为numpy数组,直接使用numpy更简单 (scikit-image也是如此)。可能的一种方法是将图像读取为灰度或转换为灰度,并按行和列进行操作,如下面代码片段所示。这将删除所有像素都是pixel_value(在这种情况下是白色)的列和行。

def crop_image(filename, pixel_value=255):
    gray = cv2.imread(filename, cv2.IMREAD_GRAYSCALE)
    crop_rows = gray[~np.all(gray == pixel_value, axis=1), :]
    cropped_image = crop_rows[:, ~np.all(crop_rows == pixel_value, axis=0)]
    return cropped_image

并输出:

输入图像描述


如果图像不是灰度图呢? - Naourass Derouichi

-1

这也可以工作:

from PIL import Image, ImageChops

img = Image.open("pUq4x.png")
pixels = img.load()

print (f"original: {img.size[0]} x {img.size[1]}")
xlist = []
ylist = []
for y in range(0, img.size[1]):
    for x in range(0, img.size[0]):
        if pixels[x, y] != (255, 255, 255, 255):
            xlist.append(x)
            ylist.append(y)
left = min(xlist)
right = max(xlist)
top = min(ylist)
bottom = max(ylist)

img = img.crop((left-10, top-10, right+10, bottom+10))
img.show()


enter image description here


1
不是最优解,你可以在for循环内部保存最大值和最小值的记录。 - Amir Fo
你能把数组转换成NumPy并使用np.minnp.max吗?在Python中,双重for循环进行图像处理并不是首选方法。 - rayryeng

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