基于OpenCV的表格文本內(nèi)容提取
點擊上方“小白學(xué)視覺”,選擇加"星標(biāo)"或“置頂”
重磅干貨,第一時間送達
小伙伴們可能會覺得從圖像中提取文本是一件很麻煩的事情,尤其是需要提取大量文本時。PyTesseract是一種光學(xué)字符識別(OCR),該庫提了供文本圖像。
PyTesseract確實有一定的效果,用PyTesseract來檢測短文本時,結(jié)果相當(dāng)不錯。但是,當(dāng)我們用它來檢測表格中的文本時,算法執(zhí)行失敗。

圖1.直接使用PyTesseract檢測表中的文本
圖1描繪了文本檢測結(jié)果,綠色框包圍了檢測到的單詞。可以看出算法對于大部分文本都無法檢測,尤其是數(shù)字。而這些數(shù)字卻是展示了每日COVID-19病例的相關(guān)信息。那么,如何提取這些信息?
簡介
在編寫算法時,我們通常應(yīng)該以我們?nèi)祟惱斫鈫栴}的方式來編寫算法。這樣,我們可以輕松地將想法轉(zhuǎn)化為算法。
當(dāng)我們閱讀表格時,首先注意到的就是單元格。一個單元格使用邊框(線)與另一個單元格分開,邊框可以是垂直的也可以是水平的。識別單元格后,我們繼續(xù)閱讀其中的信息。將其轉(zhuǎn)換為算法,您可以將過程分為三個過程,即單元格檢測、區(qū)域(ROI)選擇和文本提取。
在執(zhí)行每個任務(wù)之前,讓我們先導(dǎo)入必要內(nèi)容
import cv2 as cvimport numpy as npfilename = 'filename.png'img = cv.imread(cv.samples.findFile(filename))cImage = np.copy(img) #image to draw linescv.imshow("image", img) #name the window as "image"cv.waitKey(0)cv.destroyWindow("image") #close the window
單元格檢測
查找表格中的水平線和垂直線可能是最容易開始的。有多種檢測線的方法,這里我們采用OpenCV庫中的Hough Line Transform。
在應(yīng)用霍夫線變換之前,需要進行一些預(yù)處理。第一是將存在的RGB圖像轉(zhuǎn)換為灰度圖像。因為灰度圖像對于Canny邊緣檢測而言非常重要。
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圖像。


圖2.灰度和Canny圖像
霍夫線變換
在OpenCV中,此算法有兩種類型,即標(biāo)準(zhǔn)霍夫線變換和概率霍夫線變換。標(biāo)準(zhǔn)變換為我們提供直線方程,因此我們無法得知直線的起點和終點。概率變換將為我們提供線列表,即直線起點與終點的坐標(biāo)值列表。我們優(yōu)先選用的是概率變化。


圖3.霍夫線變換結(jié)果示例(來源:OpenCV)
對于HoughLinesP函數(shù),有如下幾個輸入?yún)?shù):
image?-8位單通道二進制源圖像。該圖像可以通過該功能進行修改。
rho?—累加器的距離分辨率,以像素為單位。
theta?—弧度的累加器角度分辨率。
threshold-累加器閾值參數(shù)。僅返回那些獲得足夠投票的行
line?—?線的輸出向量。這里設(shè)置為無,該值保存到linesP
minLineLength?—最小行長。短于此的線段將被拒絕。
maxLineGap?—同一線上的點之間允許鏈接的最大間隙。
# cv.HoughLinesP(image, rho, theta, threshold[, lines[, minLineLength[, maxLineGap]]]) → linesrho = 1theta = np.pi/180threshold = 50minLinLength = 350maxLineGap = 6linesP = cv.HoughLinesP(canny, rho , theta, threshold, None, minLinLength, maxLineGap)
為了區(qū)分水平線和垂直線,我們定義了一個函數(shù)并根據(jù)該函數(shù)的返回值添加列表。
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

圖4.霍夫線變換結(jié)果—沒有重疊濾波器
重疊濾波器
檢測到的線如上圖所示。但是,霍夫線變換結(jié)果中有一些重疊的線。較粗的線由多個相同位置,長度不同的線組成。為了消除此重疊線,我們定義了一個重疊過濾器。
最初,基于分類索引對線進行分類,水平線的y?和垂直線的x?。如果下一行的間隔小于一定距離,則將其視為與上一行相同的行。
def overlapping_filter(lines, sorting_index):filtered_lines = []lines = sorted(lines, key=lambda lines: lines[sorting_index])separation = 5for 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
實現(xiàn)重疊濾鏡并在圖像上添加文本,現(xiàn)在代碼應(yīng)如下所示:
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

圖5.霍夫線變換結(jié)果—帶重疊濾波器
有了這個代碼,就不會提取出重疊的行了。此外,我們還將在圖像中寫入水平和垂直線的索引,這將有利于ROI的選擇。
ROI選擇
首先,我們需要定義列數(shù)和行數(shù)。這里我們只對第二行第十四行以及所有列中的數(shù)據(jù)感興趣。對于列,我們定義了一個名為關(guān)鍵字的列表,將其用于字典關(guān)鍵字。
## set keywordskeywords = ['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 indexingcounter = 0## set line indexfirst_line_index = 1last_line_index = 14
然后,要選擇ROI,我們定義了一個函數(shù),該函數(shù)將圖像(水平線和垂直線都作為輸入)以及線索引作為邊框。此函數(shù)返回裁剪的圖像及其在圖像全局坐標(biāo)中的位置和大小
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] + offsety1 = horizontal[top_line_index][3] + offsetx2 = vertical[right_line_index][2] - offsety2 = horizontal[bottom_line_index][3] - offsetw = x2 - x1h = y2 - y1cropped_image = get_cropped_image(image, x1, y1, w, h)return cropped_image, (x1, y1, w, h)
裁剪的圖像將用于下一個任務(wù),即文本提取。返回的第二個參數(shù)將用于繪制ROI的邊界框
文字提取
現(xiàn)在,我們定義了ROI功能。我們可以繼續(xù)提取結(jié)果。我們可以通過遍歷單元格來讀取列中的所有數(shù)據(jù)。列數(shù)由關(guān)鍵字的長度指定,而行數(shù)則由定義。
首先,讓我們定義一個函數(shù)來繪制文本和周圍的框,并定義另一個函數(shù)來提取文本。
import pytesseractpytesseract.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
將圖像轉(zhuǎn)換為黑白以獲得更好的效果,讓我們開始迭代!
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 += 1left_line_index = jright_line_index = j+1top_line_index = ibottom_line_index = i+1cropped_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)
問題解決
這是文本提取的結(jié)果!我們只選擇了最后三列,因為它對某些文本給出了奇怪的結(jié)果,其余的很好,所以我不顯示它。

圖6.檢測到的文本—版本1
一些數(shù)字被檢測為隨機文本,即39個數(shù)據(jù)中的5個。這是由于最后三列與其余列不同。文本為白色時背景為黑色,會以某種方式影響文本提取的性能。

圖7.二進制圖像
為了解決這個問題,讓我們倒數(shù)最后三列。
def invert_area(image, x, y, w, h, display=False):ones = np.copy(image)ones = 1image[ 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 = 17right_line_index = 20top_line_index = 0bottom_line_index = -1cropped_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)
結(jié)果如下所示。

圖8.處理后的二進制圖像
結(jié)果
反轉(zhuǎn)圖像后,重新執(zhí)行步驟,這是最終結(jié)果!
算法成功檢測到文本后,現(xiàn)在可以將其保存到Python對象(例如Dictionary或List)中。由于Tesseract訓(xùn)練數(shù)據(jù)中未包含某些地區(qū)名稱(“ Kabupaten / Kota”中的名稱),因此無法準(zhǔn)確檢測到。但是,由于可以精確檢測到地區(qū)的索引,因此這不會成為問題。文本提取可能無法檢測到其他字體的文本,具體取決于所使用的字體,如果出現(xiàn)誤解,例如將“ 5”檢測為“ 8”,則可以進行諸如腐蝕膨脹之類的圖像處理。
源代碼:https://github.com/fazlurnu/Text-Extraction-Table-Image
交流群
歡迎加入公眾號讀者群一起和同行交流,目前有SLAM、三維視覺、傳感器、自動駕駛、計算攝影、檢測、分割、識別、醫(yī)學(xué)影像、GAN、算法競賽等微信群(以后會逐漸細(xì)分),請掃描下面微信號加群,備注:”昵稱+學(xué)校/公司+研究方向“,例如:”張三?+?上海交大?+?視覺SLAM“。請按照格式備注,否則不予通過。添加成功后會根據(jù)研究方向邀請進入相關(guān)微信群。請勿在群內(nèi)發(fā)送廣告,否則會請出群,謝謝理解~
