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

1、前言

1.1 項目需求

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

                            img 待識別圖片python

1.2 思路分析

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

1.3 實現環境

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

2、項目流程

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 檢測出的橫線ui

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進行遍歷,將差別較小的幾個元素用其中一個元素值代替,如四、五、六、7均替換爲4,即四、五、六、7變爲四、四、四、4。隨後將整個list轉換爲set,即進行去重,四、四、四、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):

  識別效果:

 

3、後記

  後來在對圖像的批處理過程當中,發現對某些圖片的識別效果並很差,以後在圖像剛讀出來後就用一個resize(),將全部要處理的圖像規範到同一個大小,識別效果顯著改善。目前在30張圖片上作過測試,識別準確率爲100%。

4、源碼分享及參考文獻

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的安裝、訓練及簡單使用:http://www.javashuo.com/article/p-vqltyuob-hq.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

相關文章
相關標籤/搜索