基於圖像識別的表格數據提取系統


一、前言

1.1 項目需求

  由於公司業務需要,須對從特定網站爬取下來的表格圖片進行識別,將其中的數據提取出來,隨后寫入csv文件。表格圖片形式統一,如下所示。

                            img 待識別圖片

1.2 思路分析

  直接識別整個圖片顯然是不太可能的。很自然地想到,可以將每個單元格從原圖中分割出來后,逐個進行識別。因此整個任務就可以分為圖片分割內容識別兩部分。關於圖片分割,要想分割出每個單元格,就必須獲取表格中每條橫線的縱坐標和每條豎線的橫坐標(圖像學中圖片的坐標原點在圖片的左上角,向右為x軸正方向,向下為y軸正方向,以每個像素點為單位長度)。至於內容識別,經查閱資料后,決定使用Tesseract-OCR(開源的圖像文本識別工具,依賴Java環境)。

1.3 實現環境

  python3.6,所需的python第三方庫有:pillow,opencv,numpy,csv,pytesseract。由於pytesseract依賴Java環境,因此需要安裝JDK。

二、項目流程

2.1 圖像預處理

  要想將圖片分割,就必須從圖片中檢測出組成表格的每條橫線和豎線。通過觀察圖片可以發現,圖片中共有3種顏色:白色的背景和字體,紅色的背景和字體,黑色的字體和分割線。表格的分割線是黑色的連貫線條,要想提取出分割線,就必須同時濾除白色和紅色內容的干擾。通過查閱RGB顏色表可知,黑色RGB三通道的值均為0,白色RGB三通道的值均為255,圖片中深紅色R通道值約為220,G、B通道值分別約為23和13。因此可以將原圖進行通道分離,取其紅色通道進行后續操作。opencv中的split()函數可以實現對圖片的通道分離。

img_R = cv2.split(img)[2] #opencv中三通道排列順序為BGR

                            img_R 紅色通道圖

  分離出紅色通道圖之后,就可以將紅色近似視為白色,選用合適的閾值對紅色通道圖進行二值化。為了方便后續尋線,可以將原來白色、紅色的背景部分轉黑,而黑線轉白。opencv中的threshold()函數可以同時實現圖像二值化和顏色反轉。

ret, img_bin = cv2.threshold(img_R, 100, 255, cv.THRESH_BINARY_INV) #二值化閾值選為100,大於100的置0,小於100的置255

                          img_bin 紅色通道圖二值化后反轉

  使用不同的核對對二值化后的圖像進行開運算(先腐蝕后膨脹),分別檢測出二值圖像中的橫線和豎線。opencv中的morphologyEx()函數可以用自定義的核對圖像進行開、閉運算。根據應用場景不同,可靈活調整核的形狀和大小。

kernel_row = np.ones((1, 9)) # 自定義檢測橫線的核
img_open_row = cv2.morphologyEx(img_bin, cv2.MORPH_OPEN, kernel_row) # 開運算檢測橫線

                          img_open_row 檢測出的橫線

kernel_col = np.ones((9, 1)) # 自定義檢測豎線的核
img_open_col = cv2.morphologyEx(img_bin, cv2.MORPH_OPEN, kernel_col) # 開運算檢測豎線

                          img_open_col 檢測出的豎線

  檢測出橫線和豎線后,可以對兩張圖片分別使用霍夫尋線,獲得每條線兩端點的坐標。但在實際操作過程中,發現尋豎線時效果總是不好,經測試后發現由於圖片高度較低,豎線普遍很短,不易尋找。因此可以通過resize()將img_open_col的高度拔高后,再進行霍夫尋線,效果顯著。

#圖片高度較低,為了方便霍夫尋縱線,將圖片的高度拉高5倍
img_open_col = cv2.resize(img_open_col, (800, 5 * img_h))

2.2 圖片分割

  事實上經過開運算后的img_open_col和img_open_row中已經清晰地呈現出來所有組成表格的橫線和縱線,但要想進一步分割表格,只找到線是不夠的,還必須獲取線在圖片中的位置。霍夫尋線可以幫助我們完成這一操作,將img_open_col和img_open_row作為參數傳遞給從cv2.HoughLinesP(),可返回每條線段兩端點的坐標(x1, y1, x2, y2)。

lines_col = cv2.HoughLinesP(img_open_col, 1, np.pi / 180, 100, minLineLength=int(0.52 * 5 * img_h), maxLineGap=5)

  通過打印輸出lines_col的參數信息:

  可以看出,lines_col是shape為30X1X4的numpy.adarray。事實上豎線只有7條,但通過霍夫尋線卻尋出了30條,這是因為處理后的線條較粗,每條線都被當作了多條。就第一條線而言,就被當作了四條線,即上圖中紅色框出的部分。它們的縱坐標都相同,橫坐標相差極小,可以通過后續處理將其歸為一條。在表格分割中,豎線端點坐標信息中,只有橫坐標為有效信息,因此后續處理中只針對其橫坐標即可。橫線亦然,只處理其縱坐標即可。

  就lines_col而言,其處理的思路是:取lines_x = lines_col[: ; : ; 0] ,即取出30條線段的橫坐標,隨后排序並將其轉換為list,對整個list進行遍歷,將差異較小的幾個元素用其中一個元素值代替,如4、5、6、7均替換為4,即4、5、6、7變為4、4、4、4。隨后將整個list轉換為set,即進行去重,4、4、4、4變為一個4。再排序后即可得到7條豎線的橫坐標。

lines_x = np.sort(lines_col[:,:,0], axis=None)
list_x = list(lines_x)

#合並距離相近的點
for i in range(len(list_x) - 1):
    if (list_x[i] - list_x[i + 1]) ** 2 <= (img_w/12)**2:
        list_x[i + 1] = list_x[i]

list_x = list(set(list_x))#去重
list_x.sort()#排序

  同上操作,可得到5條橫線的縱坐標。

  有了這12個關鍵數據,即可定位出每個單元格的位置。圖片分割任務到此圓滿完成,接下來就是內容識別了。

2.3 內容識別

  識別部分采用的是開源的Tesseract-OCR。將需要識別的單元格分離出來后,由於原圖的清晰度不夠,對識別造成了一定的困難。后來將需識別的單元格圖片放大后腐蝕,提高請字體清晰度。處理之后,字體樣式發生了一定程度的變形,為了不影響后續識別,將每個分離出來並經處理后的單元格保存下來,制作了一個較小的數據集,對pytesseract進行訓練,獲得一個新的識別模型,命名為ftnum,並用該模型進行后續的識別工作。

for i in range(2):
    for j in range(5):
        #截取對應的區域
        area = img_gray[(y_val[i+2]+4) :y_val[i+3], (x_val[j+1]+10) :(x_val[j+2]-10)]
        #二值化
        area_ret, area_bin = cv2.threshold(area, 190, 255, cv2.THRESH_BINARY)
        #放大三倍
        area_bin = cv2.resize(area_bin, (0,0), fx=3, fy=3)
        #腐蝕兩次,加粗字體
        area_bin = cv2.erode(area_bin, kernel_small, iterations=2)
        #送入OCR識別
        per_text = pytesseract.image_to_string(Image.fromarray(area_bin), lang="ftnum", config="--psm 7")

  分割處理后的單元格樣式如下(area_bin):

  識別效果:

 

三、后記

  后來在對圖像的批處理過程中,發現對某些圖片的識別效果並不好,之后在圖像剛讀出來后就用一個resize(),將所有要處理的圖像規范到同一個大小,識別效果顯著改善。目前在30張圖片上做過測試,識別准確率為100%。

四、源碼分享及參考文獻

4.1 源碼

  源碼含圖片爬蟲及寫入csv文件過程,其中爬蟲是公司里一位小哥哥寫的,比心,感謝!

  1 # Created by 秋沐霖 on 2019/3/8.
  2 from PIL import Image
  3 import pytesseract #OCR識別
  4 import cv2 as cv
  5 import numpy as np
  6 import csv
  7 import time
  8 import os
  9 import requests
 10 from bs4 import BeautifulSoup
 11 from openpyxl.compat import range
 12 
 13 # 獲取最新圖片
 14 def getImage():
 15     # 當天是否發布報告的標值
 16     flag = 0
 17     headers = {
 18         'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/57.0.2987.98 Safari/537.36 LBBROWSER',
 19     }
 20 
 21     # 收益率曲線主頁
 22     YieldCurveUrl='https://www.chinaratings.com.cn/AbsPrice/YieldCurve/'
 23 
 24     # 請求並解析網頁
 25     html = requests.get(YieldCurveUrl, headers=headers)
 26     html=html.content.decode('UTF-8')
 27     soup = BeautifulSoup(html, 'lxml')
 28     #  獲取今天日期
 29     today=time.strftime('%Y-%m-%d', time.localtime(time.time()))
 30 
 31     # 獲取當前日期,作為圖片的名字保存到本地
 32     img_title=soup.select('body > div.main > div > div.ctr > div.recruit > ul > li > span')[0].text.split('')[-1]
 33 
 34     if img_title==today:
 35         flag = 1
 36         # print(img_title)
 37 
 38         # 獲取最新的曲線所在頁面的鏈接
 39         YieldCurveUrl='https://www.chinaratings.com.cn'+soup.select('body > div.main > div > div.ctr > div.recruit > ul > li > a')[0].get('href')
 40 
 41         # 請求該鏈接,解析出該圖片的下載鏈接img_url
 42         html = requests.get(YieldCurveUrl, headers=headers)
 43         soup = BeautifulSoup(html.text, 'lxml')
 44         img_url ='https://www.chinaratings.com.cn'+ soup.select('body > div.main > div.ctr > div > div.newsmcont > p > img')[1].get('src')
 45 
 46         # print(img_url)
 47         rep = requests.get(img_url, headers=headers)
 48 
 49         #將圖片寫到本地
 50         with open(r'./img/'+img_title+'.png','wb')as f:
 51             f.write(rep.content)
 52 
 53     return img_title, flag
 54 
 55 
 56 #圖像預處理
 57 def picProcess():
 58     img = cv.imread(file)
 59 
 60     #為了方便后續操作,將圖像統一大小
 61     img = cv.resize(img, (800, 165))
 62 
 63     img_h = img.shape[0]
 64     img_w = img.shape[1]
 65     # 轉為灰度圖
 66     img_gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
 67 
 68     #分離處紅色通道
 69     img_R = cv.split(img)[2]
 70     # 紅色通道圖二值化,同時反轉,即將原圖中紅色、白色變黑,黑色變白,便於后續操作
 71     thr = 100
 72     ret, img_bin = cv.threshold(img_R, thr, 255, cv.THRESH_BINARY_INV)
 73 
 74     # 濾波器的長度設為9,是為了避免較粗線條的干擾
 75     kernel_col = np.ones((9, 1))
 76     kernel_row = np.ones((1, 9))
 77 
 78     #開運算求橫線和縱線
 79     img_open_col = cv.morphologyEx(img_bin, cv.MORPH_OPEN, kernel_col)
 80     img_open_row = cv.morphologyEx(img_bin, cv.MORPH_OPEN, kernel_row)
 81     #圖片高度較低,為了方便霍夫尋縱線,將圖片的高度拉高5倍
 82     img_open_col = cv.resize(img_open_col, (800, 5 * img_h))
 83 
 84     #霍夫尋線
 85     lines_col = cv.HoughLinesP(img_open_col, 1, np.pi / 180, 100, minLineLength=int(0.52 * 5 * img_h),
 86                                maxLineGap=5)
 87     lines_row = cv.HoughLinesP(img_open_row, 1, np.pi / 180, 100, minLineLength=int(0.75 * img_w),
 88                                maxLineGap=5)
 89 
 90     return img_w,img_h, img_gray, lines_col, lines_row
 91 
 92 #求交點坐標
 93 def getCoord(lines, flag):
 94     #求豎線的橫坐標
 95     if flag == "col":
 96         lines_x = np.sort(lines[:,:,0], axis=None)
 97         list_x = list(lines_x)
 98 
 99         #合並距離相近的點
100         for i in range(len(list_x) - 1):
101             if (list_x[i] - list_x[i + 1]) ** 2 <= (img_w/12)**2:
102                 list_x[i + 1] = list_x[i]
103 
104         list_x = list(set(list_x))#去重
105         list_x.sort()#排序
106         return list_x
107 
108     #求橫線的縱坐標
109     elif flag == "row":
110         lines_y = np.sort(lines[:,:,1], axis=None)
111         list_y = list(lines_y)
112 
113         # 合並距離相近的點
114         for i in range(len(list_y) - 1):
115             if (list_y[i] - list_y[i + 1]) ** 2 <= (img_h/8)**2:
116                 list_y[i + 1] = list_y[i]
117 
118         list_y = list(set(list_y))  # 去重
119         list_y.sort()  # 排序
120         return list_y
121 
122 #識別日期及數值
123 def recognize():
124     kernel_small = np.ones((3, 3))
125     text = ['關鍵期限點曲線值']
126 
127     #日期,為報告發布日期
128     per_text = png_name
129     text.append(per_text)
130 
131     add_list = ['360','1080','1800','3600','10800','ABS','RMBS']
132     text = text + add_list
133 
134     #數值,放大三倍,腐蝕兩次,效果較好
135     for i in range(2):
136         for j in range(5):
137             #截取對應的區域
138             area = img_gray[(y_val[i+2]+4) :y_val[i+3], (x_val[j+1]+10) :(x_val[j+2]-10)]
139             #二值化
140             area_ret, area_bin = cv.threshold(area, 190, 255, cv.THRESH_BINARY)
141             #放大三倍
142             area_bin = cv.resize(area_bin, (0,0), fx=3, fy=3)
143             # 腐蝕兩次,加粗字體
144             area_bin = cv.erode(area_bin, kernel_small, iterations=2)
145 
146             #送入OCR識別
147             per_text = pytesseract.image_to_string(Image.fromarray(area_bin), lang="ftnum", config="--psm 7")
148 
149             #易錯修正
150             if ' ' in per_text:
151                 per_text = ''.join(per_text.split()) #去多余空格
152             if '..' in per_text:
153                 per_text.replace('..', '.')
154 
155             text.append(per_text)
156 
157     #整理順序,方便寫入表格
158     index = text[8]
159     text[8:13] = text[9:14]
160     text[13] = index
161 
162     return text
163 
164 #寫入csv
165 def writeCsv(path):
166     with open(path,"w", newline='') as file:
167         writer = csv.writer(file, dialect='excel')
168 
169         #寫表頭
170         header = ["CurveName", "RateType", "ReportingDate", "TermBase", "Term", "Rate"]
171         writer.writerows([header])
172 
173         #寫ABS數據
174         for i in range(2,7):
175             writer.writerows([["ABS", "SpotRate", text[1], "D", text[i], text[i+6] ]])
176         #寫RMBS數據
177         for j in range(2,7):
178             writer.writerows([["RMBS", "SpotRate", text[1], "D", text[j], text[j+12] ]])
179 
180 
181 if __name__ == "__main__":
182     current_dir = os.getcwd()  # 返回當前工作目錄
183     files_dir = os.listdir(current_dir)  # 返回指定的文件夾包含的文件或文件夾的名字的列表,
184 
185     png_name, flag = getImage()
186 
187     if flag == 1:
188         if "CSV存放文件夾" not in files_dir:
189             os.mkdir(current_dir + "\\CSV存放文件夾")
190         if "img" not in files_dir:
191             os.mkdir(current_dir + "\\img")
192 
193         os.chdir(".\\img")  # 跳進img文件夾
194         files = os.listdir(".")  # 返回該文件夾下所有文件
195         for file in files:
196             if (os.path.splitext(file)[0] == png_name)and(os.path.splitext(file)[1] == ".png"):
197 
198                 #獲取交點坐標
199                 img_w, img_h, img_gray, lines_col, lines_row = picProcess()
200                 x_val = getCoord(lines_col, flag="col")
201                 y_val = getCoord(lines_row, flag="row")
202 
203                 #分割識別
204                 text= recognize()
205 
206                 #寫入csv文件
207                 csv_path = current_dir+"\\CSV存放文件夾\\"+os.path.splitext(file)[0]+"_data.csv"
208                 writeCsv(csv_path)
209         os.chdir(current_dir)
210     elif flag == 0:
211         print("今天未發布報告")
View Code

 

4.2 參考文獻

思路啟蒙:https://blog.csdn.net/huangwumanyan/article/details/82526873

霍夫尋線:https://blog.csdn.net/dcrmg/article/details/78880046

Tesseract-OCR的安裝、訓練及簡單使用:https://www.cnblogs.com/cnlian/p/5765871.html

                     http://www.cnblogs.com/lizm166/p/8343872.html

                     https://www.cnblogs.com/wzben/p/5930538.html

csv文件操作:https://blog.csdn.net/lwgkzl/article/details/82147474

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM