合并MSER检测到的对象(OpenCV,Python)

3

我正在使用这张图片作为来源:

src

应用下面的代码...

import cv2
import numpy as np

mser = cv2.MSER_create()
img = cv2.imread('C:\\Users\\Link\\Desktop\\test2.png')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
vis = img.copy()
regions, _ = mser.detectRegions(gray)
hulls = [cv2.convexHull(p.reshape(-1, 1, 2)) for p in regions]
cv2.polylines(vis, hulls, 1, (0, 255, 0))

mask = np.zeros((img.shape[0], img.shape[1], 1), dtype=np.uint8)
for contour in hulls:
    cv2.drawContours(mask, [contour], -1, (255, 255, 255), -1)

    text_only = cv2.bitwise_and(img, img, mask=mask)

cv2.imshow('img', vis)
cv2.waitKey(0)
cv2.imshow('img', mask)
cv2.waitKey(0)
cv2.imshow('img', text_only)
cv2.waitKey(0)

cv2.imwrite('C:\\Users\\Link\\Desktop\\test_o\\1.png', text_only)

我获得了以下结果(掩码):

o1

问题如下:

如何将数字系列(157661546)中的数字5合并为单个对象,只要它在掩码图像中被划分?

谢谢


请注意,每个帖子只能提出一个问题。您应该知道这一点。 - Hille
1个回答

4

请看这里,它似乎是确切的答案。

这里有我对上述代码进行了微调以进行文本提取(还带有掩码)的版本。

下面是先前文章中的原始代码,“移植”到Python 3、OpenCV 3中,并添加了MSER和边界框。我的版本与下面的版本主要区别在于如何定义分组距离:我的版本是以文本为导向的,而下面的版本是自由几何距离。

import sys
import cv2
import numpy as np

def find_if_close(cnt1,cnt2):
    row1,row2 = cnt1.shape[0],cnt2.shape[0]
    for i in range(row1):
        for j in range(row2):
            dist = np.linalg.norm(cnt1[i]-cnt2[j])
            if abs(dist) < 25:      # <-- threshold
                return True
            elif i==row1-1 and j==row2-1:
                return False

img = cv2.imread(sys.argv[1])
gray = cv2.cvtColor(img,cv2.COLOR_BGR2GRAY)

cv2.imshow('input', img)

ret,thresh = cv2.threshold(gray,127,255,0)

mser=False
if mser:
    mser = cv2.MSER_create()
    regions = mser.detectRegions(thresh)
    hulls = [cv2.convexHull(p.reshape(-1, 1, 2)) for p in regions[0]]
    contours = hulls
else:
    thresh = cv2.bitwise_not(thresh) # wants black bg
    im2,contours,hier = cv2.findContours(thresh,cv2.RETR_EXTERNAL,2)

cv2.drawContours(img, contours, -1, (0,0,255), 1)
cv2.imshow('base contours', img)


LENGTH = len(contours)
status = np.zeros((LENGTH,1))

print("Elements:", len(contours))
for i,cnt1 in enumerate(contours):
    x = i    
    if i != LENGTH-1:
        for j,cnt2 in enumerate(contours[i+1:]):
            x = x+1
            dist = find_if_close(cnt1,cnt2)
            if dist == True:
                val = min(status[i],status[x])
                status[x] = status[i] = val
            else:
                if status[x]==status[i]:
                    status[x] = i+1

unified = []
maximum = int(status.max())+1
for i in range(maximum):
    pos = np.where(status==i)[0]
    if pos.size != 0:
        cont = np.vstack(contours[i] for i in pos)
        hull = cv2.convexHull(cont)
        unified.append(hull)

cv2.drawContours(img,contours,-1,(0,0,255),1)
cv2.drawContours(img,unified,-1,(0,255,0),2)
#cv2.drawContours(thresh,unified,-1,255,-1)

for c in unified:
    (x,y,w,h) = cv2.boundingRect(c)
    cv2.rectangle(img, (x,y), (x+w,y+h), (255, 0, 0), 2)

cv2.imshow('result', img)
cv2.waitKey(0)
cv2.destroyAllWindows()

示例输出(黄色斑块在二进制阈值转换下被忽略)。红色:原始轮廓,绿色:统一轮廓,蓝色:边界框。

Sample output

也许没有必要使用MSER,简单的findContours可能会很好地工作。

------------------------

从这里开始是我在找到上面的代码之前的旧答案。我仍然保留它,因为它描述了几种不同的方法,可能更容易/更适合某些情况。

一个快速而简单的技巧是在MSER(或某些稀疏/腐蚀)之前添加小的高斯模糊和高阈值。实际上,您只需使文本更加粗体,以便填补小间隙。显然,您可以稍后放弃此版本并从原始版本裁剪。

否则,如果您的文本是按行排列的,则可以尝试检测平均行中心(例如制作Y坐标的直方图并查找峰值)。然后,对于每一行,查找具有接近平均X的片段。如果文本嘈杂/复杂,则相当脆弱。

如果您不需要拆分每个字母,则获取整个单词的边界框可能更容易:只需基于片段之间的最大水平距离(使用轮廓的左/右最点)将其分组。然后在每个组内使用最左和最右的框来找到整个边界框。对于多行文本,首先按质心Y坐标分组。

实现说明:

Opencv允许您创建直方图,但您可能可以使用类似以下内容的东西(对于我来说在类似任务上起作用):
def histogram(vals, th=4, bins=400):
    hist = np.zeros(bins)
    for y_center in vals:
        bucket = int(round(y_center / 2.)) <-- change this "2."
        hist[bucket-1] += 1
    print("hist: ", hist)

    hist = np.where(hist > th, hist, 0)
    return hist

这里我的直方图只是一个有400个桶的数组(因为我的图像高度为800像素,所以每个桶捕捉两个像素,这就是“2.”来的地方)。Vals是每个片段质心的Y坐标(在构建此列表时,您可能希望忽略非常小的元素)。Th阈值只是为了去除一些噪音。您应该得到类似于以下内容的东西:
0,0,0,5,22,0,0,0,0,43,7,0,0,0

这个列表从上到下描述了每个位置有多少个片段。

现在我进行了另一次合并峰值的操作,将它们合并为一个单独的值(只需扫描数组并在其非零时求和,在第一个零时重置计数),得到类似于这样的结果 {y:count}:

{9:27, 20:50}

现在我知道我的文本分别位于y=9和y=20两行。现在或之前,您将每个片段分配到一行(在我的情况下再次使用8px阈值)。现在,您可以单独处理每一行,找到“单词”。顺便说一句,我也遇到了与断字相同的问题,这就是为什么我来这里寻找MSER的原因 :)。请注意,如果您找到单词的整个边界框,则此问题仅会发生在第一个/最后一个字母上:其他断开的字母仍然会落在单词框中。
这里是关于腐蚀/膨胀的参考Here,但高斯模糊/th对我有用。
更新:我注意到这一行有些问题:
regions = mser.detectRegions(thresh)

我传入了已经进行阈值处理的图像(!?)。这对于聚合部分来说并不重要,但请记住,MSER部分没有按预期使用。

检查一下什么是直方图方法。你在回答中提到了许多关键词,我需要研究一下。顺便说一句,谢谢。 - lucians
感谢您详尽而清晰的回答。您提到的大部分内容我还需要学习。这是我的第一个结果。如您所见,与我的第一次尝试相比,它看起来有些“奇怪”。正如您所说,我现在正在处理以下问题:对原始图像进行模糊处理并尝试查找单个数字的轮廓。之后,对于每个轮廓,绘制一个MSER线并将其保存到ROI中(不是框,因为框不够准确)。顺便说一下,那就是答案。 - lucians
你知道如何提取单个图像形状(不是边界框,而是形状),就像我在这里所问的那样:https://stackoverflow.com/questions/47632535/save-shape-of-mser-python-opencv?noredirect=1#comment82226600_47632535 - lucians
1
默认情况下,上述代码不使用mser,可能是这个原因吗?我认为上述代码也存在一些问题,因为我发现远离的形状随机合并。我会在发现更多问题后更新答案。 - lorenzo
我的目标是提取字符的形状而不是边界框,因为这样可以得到更清晰的图像。你上面发布的代码还可以,但在提取方面只有一点帮助。无论如何,我找到了一个简单(原始)的解决方案来提取我的数字,并且可以说是MNIST准备好了;提取后再进行单个ROI的优化。目前看起来似乎有效...此外,高斯和阈值帮助我理解了关于opencv的其他事情,所以谢谢。我将在我的博客上撰写一篇完整的教程,介绍如何进行良好的ROI提取(希望如此)。 - lucians
显示剩余4条评论

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