因爲公司業務須要,須對從特定網站爬取下來的表格圖片進行識別,將其中的數據提取出來,隨後寫入csv文件。表格圖片形式統一,以下所示。html
img 待識別圖片python
直接識別整個圖片顯然是不太可能的。很天然地想到,能夠將每一個單元格從原圖中分割出來後,逐個進行識別。所以整個任務就能夠分爲圖片分割和內容識別兩部分。關於圖片分割,要想分割出每一個單元格,就必須獲取表格中每條橫線的縱座標和每條豎線的橫座標(圖像學中圖片的座標原點在圖片的左上角,向右爲x軸正方向,向下爲y軸正方向,以每一個像素點爲單位長度)。至於內容識別,經查閱資料後,決定使用Tesseract-OCR(開源的圖像文本識別工具,依賴Java環境)。app
python3.6,所需的python第三方庫有:pillow,opencv,numpy,csv,pytesseract。因爲pytesseract依賴Java環境,所以須要安裝JDK。ide
要想將圖片分割,就必須從圖片中檢測出組成表格的每條橫線和豎線。經過觀察圖片能夠發現,圖片中共有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))
事實上通過開運算後的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個關鍵數據,便可定位出每一個單元格的位置。圖片分割任務到此圓滿完成,接下來就是內容識別了。
識別部分採用的是開源的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%。
源碼含圖片爬蟲及寫入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("今天未發佈報告")
思路啓蒙: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