【问题标题】:Bounding box detection for characters / digits字符/数字的边界框检测
【发布时间】:2021-04-20 15:35:41
【问题描述】:

我有如下图片:

我想找到 8 位数字的边界框。我的第一次尝试是使用带有以下代码的 cv2:

import cv2
import matplotlib.pyplot as plt
import cvlib as cv
from cvlib.object_detection import draw_bbox

im = cv2.imread('31197402.png')
bbox, label, conf = cv.detect_common_objects(im)
output_image = draw_bbox(im, bbox, label, conf)
plt.imshow(output_image)
plt.show()

不幸的是,这不起作用。有人有想法吗?

【问题讨论】:

    标签: python opencv ocr


    【解决方案1】:

    您的解决方案中的问题可能是输入图像的质量很差。人物和背景几乎没有对比。 cvlib 的 blob 检测算法可能无法区分字符 blob 和背景,从而产生无用的二进制掩码。让我们尝试使用纯粹的OpenCV 来解决这个问题。

    我提出以下步骤:

    1. 应用自适应阈值以获得相当好的二进制掩码。
    2. 使用区域过滤器清除二进制掩码中的斑点噪声。
    3. 使用形态学提高二值图像的质量。
    4. 获取每个字符的外部轮廓,并为每个字符块设置一个边界矩形
    5. 使用之前计算的边界矩形裁剪每个字符。

    让我们看看代码:

    # importing cv2 & numpy:
    import numpy as np
    import cv2
    
    # Set image path
    path = "C:/opencvImages/"
    fileName = "mrrm9.png"
    
    # Read input image:
    inputImage = cv2.imread(path+fileName)
    inputCopy = inputImage.copy()
    
    # Convert BGR to grayscale:
    grayscaleImage = cv2.cvtColor(inputImage, cv2.COLOR_BGR2GRAY)
    

    从这里没有太多要讨论的,只是阅读BGR图像并将其转换为grayscale。现在,让我们使用gaussian 方法应用adaptive threshold。这是棘手的部分,因为参数是根据输入的质量手动调整的。该方法的工作方式是将图像划分为windowSize 的单元格网格,然后应用局部阈值来找到前景和背景之间的最佳分离。可以在阈值中添加一个由windowConstant 指示的额外常量以微调输出:

    # Set the adaptive thresholding (gasussian) parameters:
    windowSize = 31
    windowConstant = -1
    # Apply the threshold:
    binaryImage = cv2.adaptiveThreshold(grayscaleImage, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, windowSize, windowConstant)
    

    你会得到这个漂亮的二进制图像:

    现在,如您所见,图像有一些斑点噪点。让我们使用area filter 来消除噪音。噪声小于感兴趣的目标斑点,因此我们可以根据区域轻松过滤它们,如下所示:

    # Perform an area filter on the binary blobs:
    componentsNumber, labeledImage, componentStats, componentCentroids = \
    cv2.connectedComponentsWithStats(binaryImage, connectivity=4)
    
    # Set the minimum pixels for the area filter:
    minArea = 20
    
    # Get the indices/labels of the remaining components based on the area stat
    # (skip the background component at index 0)
    remainingComponentLabels = [i for i in range(1, componentsNumber) if componentStats[i][4] >= minArea]
    
    # Filter the labeled pixels based on the remaining labels,
    # assign pixel intensity to 255 (uint8) for the remaining pixels
    filteredImage = np.where(np.isin(labeledImage, remainingComponentLabels) == True, 255, 0).astype('uint8')
    

    这是过滤后的图像:

    我们可以通过一些形态学来提高这张图片的质量。一些字符似乎被破坏了(查看第一个3 - 它被分成两个分开的斑点)。我们可以加入他们应用关闭操作:

    # Set kernel (structuring element) size:
    kernelSize = 3
    
    # Set operation iterations:
    opIterations = 1
    
    # Get the structuring element:
    maxKernel = cv2.getStructuringElement(cv2.MORPH_RECT, (kernelSize, kernelSize))
    
    # Perform closing:
    closingImage = cv2.morphologyEx(filteredImage, cv2.MORPH_CLOSE, maxKernel, None, None, opIterations, cv2.BORDER_REFLECT101)
    

    这是“关闭”的图像:

    现在,您想要获取每个字符的 bounding boxes。让我们检测每个 blob 的外轮廓并在其周围放置一个漂亮的矩形:

    # Get each bounding box
    # Find the big contours/blobs on the filtered image:
    contours, hierarchy = cv2.findContours(closingImage, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
    
    contours_poly = [None] * len(contours)
    # The Bounding Rectangles will be stored here:
    boundRect = []
    
    # Alright, just look for the outer bounding boxes:
    for i, c in enumerate(contours):
    
        if hierarchy[0][i][3] == -1:
            contours_poly[i] = cv2.approxPolyDP(c, 3, True)
            boundRect.append(cv2.boundingRect(contours_poly[i]))
    
    
    # Draw the bounding boxes on the (copied) input image:
    for i in range(len(boundRect)):
        color = (0, 255, 0)
        cv2.rectangle(inputCopy, (int(boundRect[i][0]), int(boundRect[i][1])), \
                  (int(boundRect[i][0] + boundRect[i][2]), int(boundRect[i][1] + boundRect[i][3])), color, 2)
    

    最后一个for 循环几乎是可选的。它从列表中获取每个边界矩形并将其绘制在输入图像上,因此您可以看到每个单独的矩形,如下所示:

    让我们在二进制图像上可视化:

    另外,如果你想使用我们刚刚得到的边界框来裁剪每个字符,你可以这样做:

    # Crop the characters:
    for i in range(len(boundRect)):
        # Get the roi for each bounding rectangle:
        x, y, w, h = boundRect[i]
    
        # Crop the roi:
        croppedImg = closingImage[y:y + h, x:x + w]
        cv2.imshow("Cropped Character: "+str(i), croppedImg)
        cv2.waitKey(0)
    

    这就是获取单个边界框的方法。现在,也许您正试图将这些图像传递给OCR。我尝试将过滤后的二进制图像(在关闭操作之后)传递给pyocr(这是我正在使用的OCR),我将其作为输出字符串:31197402

    我用来获取关闭图像的OCR的代码是这样的:

    # Set the OCR libraries:
    from PIL import Image
    import pyocr
    import pyocr.builders
    
    # Set pyocr tools:
    tools = pyocr.get_available_tools()
    # The tools are returned in the recommended order of usage
    tool = tools[0]
    
    # Set OCR language:
    langs = tool.get_available_languages()
    lang = langs[0]
    
    # Get string from image:
    txt = tool.image_to_string(
        Image.open(path + "closingImage.png"),
        lang=lang,
        builder=pyocr.builders.TextBuilder()
    )
    
    print("Text is:"+txt)
    

    请注意OCR 接收白色背景上的黑色字符,因此您必须先反转图像。

    【讨论】:

    • 非常感谢您的回复(再次):)。您的方法效果很好,但问题是它需要太多手动信息。我需要一些无需任何手动预处理即可自动读取数字的东西。我正在考虑一种算法,至少可以找到正确的边界框,这样我就可以在任务上训练一个神经网络(有点像 MNIST)。你知道这里有什么可能性吗?
    • @spadel 对不起,您的问题中的要求没有提到全自动解决方案。您要求为数字提供边界框,而我只是提供了这一点。也许尝试发布一个新问题并要求一个基于深度学习的唯一答案,看看是否有任何建议出现。祝你好运,我的朋友。
    • 我希望找到一种算法,它可以通过最小的调整被普遍应用。事实上,当迭代你的方法的手动输入数据时,我取得了相当好的结果。但它的计算成本也很高。无论如何,我将您的答案标记为解决方案,因为它是迄今为止我所拥有的最好的:) 再次感谢!
    猜你喜欢
    • 2013-09-01
    • 2016-04-30
    • 2013-04-06
    • 2017-07-15
    • 2015-02-01
    • 2014-02-04
    • 2013-12-09
    • 2014-04-23
    • 1970-01-01
    相关资源
    最近更新 更多