从图像中提取文本可能会让人筋疲力尽,尤其是当您要提取大量内容时。一个众所周知的文本提取库是PyTesseract,一种光学字符识别 (OCR)。该库将为您提供给定图像的文本。

PyTesseract 真的很有帮助,第一次知道 PyTesseract,我直接用它来检测一些短文本,结果很满意。然后,我用它来检测表格中的文本,但算法执行失败。


java opencv文字区域提取 opencv图片文字提取_opencv


图 1. 直接使用 PyTesseract 检测表格中的文本

图 1 描述了文本检测结果,绿色框包围了检测到的单词。您可能意识到算法无法检测到大部分文本,尤其是数字。就我而言,这些数字是数据的基本要素,为我提供了来自家乡当地政府的每日 COVID-19 病例的价值。那么,如何提取这些信息呢?


入门

在编写算法时,我总是试着像人类一样教算法。这样,我可以很容易地将这个想法转化为更详细的算法。

当您阅读表格时,您可能首先注意到的是单元格。可以使用可以是垂直或水平的边框(线)将一个单元格与另一个单元格分开。识别单元格后,您继续阅读其中的信息。将其转换为算法,您可以将过程分为三个过程,即细胞检测感兴趣区域(ROI)选择文本提取

在进行每个任务之前,让我们加载图像,如下所示

import cv2 as cv
import numpy as np
filename = 'source.png'
img = cv.imread(cv.samples.findFile(filename))
cImage = np.copy(img) #image to draw lines
cv.imshow("image", img) #name the window as "image"
cv.waitKey(0)
cv.destroyWindow("image") #close the window

想跳过文章并查看完整代码吗?

这是代码:Text-Extraction-Table-Image

细胞检测

在表格中查找水平线和垂直线可能是最容易开始的。检测线的方法有很多,但对我来说一种有趣的方法是使用 Hough Line Transform,一个 OpenCV 库。有关模式的详细信息,请访问此链接

在应用霍夫线变换之前,需要进行多项预处理。第一个是将图像转换为灰度图像,以防您有 RGB 图像。这个灰度图像对于下一步Canny Edge-Detection很重要。

gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
cv.imshow("gray", gray)
cv.waitKey(0)
cv.destroyWindow("gray")
canny = cv.Canny(gray, 50, 150)
cv.imshow("canny", canny)
cv.waitKey(0)
cv.destroyWindow("canny")

下图左图为灰度图,右图为 Canny 图像。


java opencv文字区域提取 opencv图片文字提取_java opencv文字区域提取_02



java opencv文字区域提取 opencv图片文字提取_python_03


霍夫线变换

在 OpenCV 中,该算法有两种类型,即标准霍夫线变换和概率霍夫线变换。标准的会给你线方程,所以你不知道线的开始和结束。而概率线变换将为您提供线列表,其中线是开始和结束坐标的列表。就我的目的而言,概率的更可取。

java opencv文字区域提取 opencv图片文字提取_OpenCV_04

 


图 3.a。标准霍夫线变换结果示例(来源:OpenCV)



 



java opencv文字区域提取 opencv图片文字提取_OpenCV_05

图 3.b。标准霍夫线变换结果示例(来源:OpenCV)



 


对于 HoughLinesP 函数,有几个输入参数:

  1. image — 8 位、单通道二进制源图像。该功能可以修改图像。
  2. rho - 累加器的距离分辨率(以像素为单位)。
  3. theta - 以弧度为单位的累加器的角度分辨率。
  4. threshold - 累加器阈值参数。仅返回那些获得足够票数的行
  5. line - 线的输出向量。这里设置为None,值保存到linesP
  6. minLineLength — 最小行长度。比这短的线段被拒绝。
  7. maxLineGap - 同一条线上的点之间的最大允许间隙以链接它们。
# cv.HoughLinesP(image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]]) → lines
rho = 1
theta = np.pi/180
threshold = 50
minLinLength = 350
maxLineGap = 6
linesP = cv.HoughLinesP(canny, rho , theta, threshold, None, minLinLength, maxLineGap)

为了区分水平线和垂直线,我定义了一个函数并根据函数返回值添加列表

def is_vertical(line):
    return line[0]==line[2]

def is_horizontal(line):
    return line[1]==line[3]


horizontal_lines = []
vertical_lines = []

if linesP is not None:
    for i in range(0, len(linesP)):
        l = linesP[i][0]
        if (is_vertical(l)):
            vertical_lines.append(l)

        elif (is_horizontal(l)):
            horizontal_lines.append(l)
for i, line in enumerate(horizontal_lines):
    cv.line(cImage, (line[0], line[1]), (line[2], line[3]), (0, 255, 0), 3, cv.LINE_AA)

for i, line in enumerate(vertical_lines):
    cv.line(cImage, (line[0], line[1]), (line[2], line[3]), (0, 0, 255), 3, cv.LINE_AA)

cv.imshow("with_line", cImage)
cv.waitKey(0)
cv.destroyWindow("with_line")  # close the window

java opencv文字区域提取 opencv图片文字提取_opencv_06

图 4. 霍夫线变换结果 - 没有重叠滤波器

重叠过滤器

检测到的线如上图所示。但是,霍夫线变换结果中存在一些重叠线。较粗的线条在同一位置由多条线组成,长度不同。为了消除这条重叠线,我定义了一个重叠过滤器。

最初,根据排序索引对线进行排序,y₁ 用于水平线,x₁ 用于垂直线。如果下一行相隔小于一定距离,那么我们认为它与前一行是同一行。这可能有点像“肮脏的工作”,但它确实有效。

def overlapping_filter(lines, sorting_index):
    filtered_lines = []
    
    lines = sorted(lines, key=lambda lines: lines[sorting_index])
    separation = 5    for i in range(len(lines)):
            l_curr = lines[i]
            if(i>0):
                l_prev = lines[i-1]
                if ( (l_curr[sorting_index] - l_prev[sorting_index]) > separation):
                    filtered_lines.append(l_curr)
            else:
                filtered_lines.append(l_curr)
                
    return filtered_lines

实现重叠过滤器并在图像上添加文本,现在代码应如下所示:

horizontal_lines = []
vertical_lines = []
    
if linesP is not None:
    for i in range(0, len(linesP)):
        l = linesP[i][0]        if (is_vertical(l)): 
            vertical_lines.append(l)
                
        elif (is_horizontal(l)):
            horizontal_lines.append(l)    horizontal_lines = overlapping_filter(horizontal_lines, 1)
    vertical_lines = overlapping_filter(vertical_lines, 0)for i, line in enumerate(horizontal_lines):
    cv.line(cImage, (line[0], line[1]), (line[2], line[3]), (0,255,0), 3, cv.LINE_AA)
    cv.putText(cImage, str(i) + "h", (line[0] + 5, line[1]), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv.LINE_AA)                      for i, line in enumerate(vertical_lines):
    cv.line(cImage, (line[0], line[1]), (line[2], line[3]), (0,0,255), 3, cv.LINE_AA)
    cv.putText(cImage, str(i) + "v", (line[0], line[1] + 5), cv.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 0), 1, cv.LINE_AA)            
cv.imshow("with_line", cImage)
cv.waitKey(0)
cv.destroyWindow("with_line") #close the window

java opencv文字区域提取 opencv图片文字提取_计算机视觉_07

图 5. 霍夫线变换结果 - 使用重叠滤波器

使用此精炼代码,您将不会有重叠的行。此外,您将在图像中写入水平和垂直线的索引。该索引将用于下一个任务,ROI 选择

投资回报率选择

首先,我们需要定义列数和行数。就我而言,我只对第二十四行和所有列中的数据感兴趣。对于列,我定义了一个名为关键字的列表,以将其用于字典关键字。

## set keywords
keywords = ['no', 'kabupaten', 'kb_otg', 'kl_otg', 'sm_otg', 'ks_otg', 'not_cvd_otg',
            'kb_odp', 'kl_odp', 'sm_odp', 'ks_odp', 'not_cvd_odp', 'death_odp',
            'kb_pdp', 'kl_pdp', 'sm_pdp', 'ks_pdp', 'not_cvd_pdp', 'death_pdp',
            'positif', 'sembuh', 'meninggal']
    
dict_kabupaten = {}
    for keyword in keywords:
        dict_kabupaten[keyword] = []
        
## set counter for image indexing
counter = 0
    
## set line index
first_line_index = 1
last_line_index = 14

然后,为了选择 ROI,我定义了一个函数,它以图像为输入,水平线和垂直线作为输入,线索引作为边界。该函数返回裁剪后的图像,以及它在图像全局坐标中的位置和大小

def get_cropped_image(image, x, y, w, h):
    cropped_image = image[ y:y+h , x:x+w ]
    return cropped_imagedef get_ROI(image, horizontal, vertical, left_line_index, right_line_index, top_line_index, bottom_line_index, offset=4):
    x1 = vertical[left_line_index][2] + offset
    y1 = horizontal[top_line_index][3] + offset
    x2 = vertical[right_line_index][2] - offset
    y2 = horizontal[bottom_line_index][3] - offset
    
    w = x2 - x1
    h = y2 - y1
    
    cropped_image = get_cropped_image(image, x1, y1, w, h)
    
    return cropped_image, (x1, y1, w, h)

裁剪后的图像将用于下一个任务,文本提取。第二个返回的参数将用于绘制 ROI 的边界框

文本提取

现在,我们定义了一个 ROI 函数。我们可以继续提取结果。我们可以通过遍历单元格来读取列中的所有数据。列数由关键字的长度给出,而行数是定义的。

首先,让我们定义一个函数来绘制文本和周围的框,以及另一个函数来提取文本。

import pytesseract
pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files (x86)\Tesseract-OCR\tesseract.exe'def draw_text(src, x, y, w, h, text):
    cFrame = np.copy(src)
    cv.rectangle(cFrame, (x, y), (x+w, y+h), (255, 0, 0), 2)
    cv.putText(cFrame, "text: " + text, (50, 50), cv.FONT_HERSHEY_SIMPLEX, 2, (0, 0, 0), 5, cv.LINE_AA)
    
    return cFramedef detect(cropped_frame, is_number = False):
    if (is_number):
        text = pytesseract.image_to_string(cropped_frame,
                                           config ='-c tessedit_char_whitelist=0123456789 --psm 10 --oem 2')
    else:
        text = pytesseract.image_to_string(cropped_frame, config='--psm 10')        
        
    return text

将图像转换为黑白以获得更好的效果,让我们开始迭代吧!

counter = 0print("Start detecting text...")
(thresh, bw) = cv.threshold(gray, 100, 255, cv.THRESH_BINARY)for i in range(first_line_index, last_line_index):
    for j, keyword in enumerate(keywords):
        counter += 1
            
        left_line_index = j
        right_line_index = j+1
        top_line_index = i
        bottom_line_index = i+1
            
        cropped_image, (x,y,w,h) = get_ROI(bw, horizontal, vertical, left_line_index, right_line_index, top_line_index, bottom_line_index)
            
        if (keywords[j]=='kabupaten'):
           text = detect(cropped_image)
           dict_kabupaten[keyword].append(text)
         
        else:
            text = detect(cropped_image, is_number=True)
            dict_kabupaten[keyword].append(text)        image_with_text = draw_text(img, x, y, w, h, text)

疑难解答

这是文本提取的结果!我只选择了最后三列,因为它对某些文本给出了奇怪的结果,其余的都很好,所以我不显示它。

图 6. 检测到的文本 - 版本 1

您可能会意识到某些数字被检测为随机文本,39 个数据中有 5 个。这是由于最后三列与其余列不同。背景为黑色,文本为白色。不知何故,它会影响文本提取的性能。

java opencv文字区域提取 opencv图片文字提取_OpenCV_08

图 7. 二值图像

为了解决这个问题,让我们反转最后三列。

def invert_area(image, x, y, w, h, display=False):
    ones = np.copy(image)
    ones = 1
    
    image[ y:y+h , x:x+w ] = ones*255 - image[ y:y+h , x:x+w ] 
    
    if (display): 
        cv.imshow("inverted", image)
        cv.waitKey(0)
        cv.destroyAllWindows()
    return imageleft_line_index = 17
right_line_index = 20
top_line_index = 0
bottom_line_index = -1
    
cropped_image, (x, y, w, h) = get_ROI(img, horizontal, vertical, left_line_index, right_line_index, top_line_index, bottom_line_index)gray = get_grayscale(img)
bw = get_binary(gray)
bw = invert_area(bw, x, y, w, h, display=True)

结果如下所示。

java opencv文字区域提取 opencv图片文字提取_java opencv文字区域提取_09

图 8. 处理后的二值图像

瞧!结果

反转图像后,重做这一步,这是最终的结果!

在您的算法成功检测到文本后,现在您可以将其保存到 Python 对象中,例如 Dictionary 或 List。一些区域名称(在“Kabupaten/Kota”中没有被精确检测到,因为它没有包含在 Tesseract 训练数据中。但是,这应该不是问题,因为可以精确检测到区域的索引。另外,这个文本提取可能无法检测到其他字体的文本,这取决于使用的字体。如果出现误解,例如“5”被检测为“8”,您可以进行腐蚀和扩张等图像处理。