此文為本人原創,轉載請注明:http://www.cnblogs.com/ygh1229/p/7227660.html
接上文: 深度學習實踐系列之--身份證上漢字及數字識別系統的實現(上)
訓練完成后,就要對模型進行測試:
在實驗中,我取得一張數據測試的圖片,在word里輸入三行數據並截一張圖,如圖所示:
圖 測試圖片示意圖
然后將圖片數據讀到image_color和灰度image里面,讓其生成灰度圖,代碼如下:
image_color = cv2.imread(path_test_image) new_shape = (image_color.shape[1] * 2, image_color.shape[0] * 2) image_color = cv2.resize(image_color, new_shape) image = cv2.cvtColor(image_color, cv2.COLOR_BGR2GRAY)
讀取到圖片后,然后對圖像進行二值化處理,生成的二值化后的灰度圖如圖所示:
圖 數據圖片灰度圖
然后使用水平投影的方法提取每一行的文本,使用垂直投影的方法切分每個字符,為了展示切分的效果,我將水平方向的圖像求和,然后利用Opencv的庫函數畫出結果,部分代碼如下:
plt.plot(horizontal_sum, range(horizontal_sum.shape[0]))
plt.gca().invert_yaxis()
plt.show()
最后畫出的結果圖如圖所示,可以見到文本行被明顯分隔出來:
圖 水平投影圖像求和波形圖
在輸入的數據圖片中有三行文本,在圖中的縱軸上的波谷就代表行間的空白區域,同理也可以垂直投影將每個字符切割出來,最后的切割效果圖如圖所示:
圖 字符切分后的數據圖
在圖中可以看出有部分字符沒有切分出來,有“深”、“如”和“對”字,這是因為因為它們的組成都是左右結構,由於組合時不緊密,切分中還沒有解決這類問題,所以沒有切分到。
然后為了將數據圖片放入模型識別,根據切分結果獲取到數據圖片中的每一個字符圖片,如圖所示:
圖 部分數據圖中截取的字符圖
然后將這些取出來的字符圖片依次放入模型中識別,
caffe_cls = CaffeCls(model_def, model_weights, y_tag_json_path)
output_tag_to_max_proba = caffe_cls.predict_cv2_imgs(np_char_imgs)
在終端下運行這個python的腳本,然后經過圖片處理和模型的計算,最終在終端下輸出結果,輸出的結果如圖所示:
圖 圖片中字符識別結果圖
在圖中可以看出,在圖片處理時個別字符沒有切分出來,但是定位切分成功的字符都識別出來了,並且除了一個“級”識別有小錯誤,其他字符全部識別正確。
這里貼出測試時python腳本的核心代碼:
cv2_color_img = cv2.imread(test_image) ##放大圖片 resize_keep_ratio = PreprocessResizeKeepRatio(1024, 1024) cv2_color_img = resize_keep_ratio.do(cv2_color_img) ##轉換成灰度圖 cv2_img = cv2.cvtColor(cv2_color_img, cv2.COLOR_RGB2GRAY) height, width = cv2_img.shape ##二值化 調整自適應閾值 使得圖像的像素值更單一、圖像更簡單 adaptive_threshold = cv2.adaptiveThreshold( cv2_img, ##原始圖像 255, ##像素值上限 cv2.ADAPTIVE_THRESH_GAUSSIAN_C, ##指定自適應方法Adaptive Method,這里表示領域內像素點加權和 cv2.THRESH_BINARY, ##賦值方法(二值化) 11, ## 規定領域大小(一個正方形的領域) 2) ## 常數C,閾值等於均值或者加權值減去這個常數 adaptive_threshold = 255 - adaptive_threshold ## 水平方向求和,找到行間隙和字符所在行(numpy) horizontal_sum = np.sum(adaptive_threshold, axis=1) ## 根據求和結果獲取字符行范圍 peek_ranges = extract_peek_ranges_from_array(horizontal_sum) vertical_peek_ranges2d = [] for peek_range in peek_ranges: start_y = peek_range[0] ##起始位置 end_y = peek_range[1] ##結束位置 line_img = adaptive_threshold[start_y:end_y, :] ## 垂直方向求和,分割每一行的每個字符 vertical_sum = np.sum(line_img, axis=0) ## 根據求和結果獲取字符行范圍 vertical_peek_ranges = extract_peek_ranges_from_array( vertical_sum, minimun_val=40, ## 設最小和為40 minimun_range=1) ## 字符最小范圍為1 ## 開始切割字符 vertical_peek_ranges = median_split_ranges(vertical_peek_ranges) ## 存放入數組中 vertical_peek_ranges2d.append(vertical_peek_ranges) ## 去除噪音,主要排除雜質,小的曝光點不是字符的部分 filtered_vertical_peek_ranges2d = [] for i, peek_range in enumerate(peek_ranges): new_peek_range = [] median_w = compute_median_w_from_ranges(vertical_peek_ranges2d[i]) for vertical_range in vertical_peek_ranges2d[i]: ## 選取水平區域內的字符,當字符與字符間的間距大於0.7倍的median_w,說明是字符 if vertical_range[1] - vertical_range[0] > median_w*0.7: new_peek_range.append(vertical_range) filtered_vertical_peek_ranges2d.append(new_peek_range) vertical_peek_ranges2d = filtered_vertical_peek_ranges2d char_imgs = [] crop_zeros = PreprocessCropZeros() resize_keep_ratio = PreprocessResizeKeepRatioFillBG( norm_width, norm_height, fill_bg=False, margin=4) for i, peek_range in enumerate(peek_ranges): for vertical_range in vertical_peek_ranges2d[i]: ## 划定字符的上下左右邊界區域 x = vertical_range[0] y = peek_range[0] w = vertical_range[1] - x h = peek_range[1] - y ## 生成二值化圖 char_img = adaptive_threshold[y:y+h+1, x:x+w+1] ## 輸出二值化圖 char_img = crop_zeros.do(char_img) char_img = resize_keep_ratio.do(char_img) ## 加入字符圖片列表中 char_imgs.append(char_img) ## 將列表轉換為數組 np_char_imgs = np.asarray(char_imgs) ## 放入模型中識別並返回結果 output_tag_to_max_proba = caffe_cls.predict_cv2_imgs(np_char_imgs) ocr_res = "" ## 讀取結果並展示 for item in output_tag_to_max_proba: ocr_res += item[0][0] print(ocr_res.encode("utf-8")) ## 生成一些Debug過程產生的圖片 if debug_dir is not None: path_adaptive_threshold = os.path.join(debug_dir, "adaptive_threshold.jpg") cv2.imwrite(path_adaptive_threshold, adaptive_threshold) seg_adaptive_threshold = cv2_color_img # color = (255, 0, 0) # for rect in rects: # x, y, w, h = rect # pt1 = (x, y) # pt2 = (x + w, y + h) # cv2.rectangle(seg_adaptive_threshold, pt1, pt2, color) color = (0, 255, 0) for i, peek_range in enumerate(peek_ranges): for vertical_range in vertical_peek_ranges2d[i]: x = vertical_range[0] y = peek_range[0] w = vertical_range[1] - x h = peek_range[1] - y pt1 = (x, y) pt2 = (x + w, y + h) cv2.rectangle(seg_adaptive_threshold, pt1, pt2, color) path_seg_adaptive_threshold = os.path.join(debug_dir, "seg_adaptive_threshold.jpg") cv2.imwrite(path_seg_adaptive_threshold, seg_adaptive_threshold) debug_dir_chars = os.path.join(debug_dir, "chars") os.makedirs(debug_dir_chars) for i, char_img in enumerate(char_imgs): path_char = os.path.join(debug_dir_chars, "%d.jpg" % i) cv2.imwrite(path_char, char_img)
完整代碼詳見: 鏈接
四、系統設計及實現
本文系統共分為兩部分:移動(Android)端和服務器端。移動端共分為兩個模塊:輸入模塊和輸出模塊;服務器端共分為三個模塊:模型加載模塊、模型處理模塊和結果映射模塊。如圖所示:
圖 系統模型框架
其中,移動端的輸入模塊負責獲取手機上身份證圖片信息,包括從手機相冊選擇和手機拍照兩種方式,輸入模塊獲取到圖片后再進行裁剪,去除其他背景使得正好裁剪出身份證圖像,然后向服務器發送圖片信息;輸出模塊負責接收服務器端返回的身份證上的個人信息,並將信息顯示在手機移動端。
服務器端的模型加載模塊負責接收移動端輸入模塊發送過來的身份證圖片信息,經過圖片預處理,進行文字定位,提取圖片文字信息,將數據傳入模型處理模塊;模型處理模塊接收到模型加載模塊里的符合模型要求的數據格式,調用模型進行數據分類與識別,處理完成后依次輸出每個處理后的數據到結果映射模塊;結果映射模塊接收到處理模塊輸出的數據后,將數據封裝成json格式,然后返回給移動端進行下一步處理。
4.1 模型服務器端設計
(1)Flask服務器搭建
Flask是一個輕量級的 Web 應用框架,它使用Python語言編寫的,優點是部署快,可移植性強。由於Flask的輕量級和易於移植,本文的服務器采用Flask架構開發,在Ubuntu系統中配置下載Flask包即可使用。
首先,本文通過pip下載Flask包,然后在程序中直接import了Flask 類就可以使用Flask的功能,非常簡潔。為了讓Flask服務器在本地服務器上運行,需要在main函數中執行 run()函數 ,核心代碼如下:
if __name__ == '__main__': app.run(host='0.0.0.0',port=8888)
其中上述代碼的第一句表示這個函數時Python程序中的主函數,也就是執行程序時首先訪問此函數,這樣確保服務器的Flask腳本在終端下直接執行的時候會首先執行,而不能將其作為一個依賴導入其他程序中。本文搭建的Flask要在手機端訪問,所以要設置外部可訪問服務器:app.run(host='0.0.0.0',port=8888),這會讓服務器監聽所有在局域網內的 IP地址並且訪問端口是8888。
編寫好Python的WSGI程序,在終端下直接運行即可。服務器啟動后會實時監聽網絡端口,當有數據請求發送到這一端口,Flask服務器獲取數據請求體,然后將其中的身份證圖片保存到服務器指定目錄下。然后對身份證圖像進行二值化處理,進行字符的分割,然后依次將字符放入訓練好的模型中進行字符的識別,待模型識別完成后將結果返回,Flask服務器將結果封裝成Json格式並返回到客戶端。本文搭建的Flask服務器處理身份證圖像的過程如圖所示。
圖 Flask系統流程圖
(2)身份證圖片預處理
在Android移動端攝像頭拍攝的圖片是彩色圖像,上傳到服務器后為了讀取到身份證上的主要信息,就要去除其他無關的元素,因此對身份證圖像取得它的灰度圖並得到二值化圖。
對身份證圖像的的二值化有利於對圖像內的信息的進一步處理,可以將待識別的信息更加突出。在OpenCV中,提供了讀入圖像接口函數imread, 首先通過imread將身份證圖像讀入內存中:
id_card_img = cv2.imread(path_img)
之后再調用轉化為灰度圖的接口函數cvtColor並給它傳入參數COLOR_BGR2GRAY,它就可以實現彩色圖到灰度圖的轉換,代碼如下
gray_id_card_img = cv2.cvtColor(color_img, cv2.COLOR_BGR2GRAY)
preprocess_bg_mask = PreprocessBackgroundMask(boundary)
轉化為二值化的灰度圖后圖像如圖所示:
圖 身份證圖像灰度圖
轉換成灰度圖之后要進行字符定位,通過每一行進行垂直投影,就可以找到所有字段的位置,具體結果如圖4.4所示:
圖 垂直投影后的灰度圖
然后根據像素點起始位置,確定字符區域,然后將字符區域一一對應放入存放字符的列表中:
vertical_peek_ranges = extract_peek_ranges_from_array( vertical_sum, minimun_val=40, minimun_range=1) vertical_peek_ranges2d.append(vertical_peek_ranges)
最后的效果圖如圖所示:
圖 字符切割后的效果圖
(3) 身份證信息識別
在對身份證上字符截取后,開始調用模型進行識別,首先要初始化第三部分模型訓練中生成的deploy網絡模型,因為輸入的數據會放入網絡模型中進行參數匹配,核心代碼如下:
base_dir = "/workspace/data/deepocr" model_def = os.path.join(base_dir, "deploy_train_test.prototxt")
之后還要初始化經過訓練得到的模型文件:
model_weights = os.path.join(base_dir, "lenet_iter_50000.caffemodel"
然后初始化訓練的文字庫清單文件列表:
y_tag_json_path = os.path.join(base_dir, "y_tag.json")
最后放入模型里開始識別,會對單個漢字進行分類,然后將識別結果與文字庫文件一一映射,取出最終結果,並將身份證上的所有信息進行拼接,存入數組中:
ocr_res = reco_text_line.do(boundary2binimgs, segment, caffe_cls)
Flask向移動端返回數據采用json的格式,如下所示:
data = [{"result":"sucess","response": {"name":key_ocr_res["name"], "address":key_ocr_res["address"] …… "day":key_ocr_res["day"]} }]
然后移動端解析json格式即可。
由於篇幅,附上我截取的代碼,其余詳見github: 鏈接
@app.route('/upload', methods=['GET', 'POST']) def upload_file(): if request.method == 'POST': f = request.files['file'] f.save('/home/ygh/flask/id_card_img.jpg') ## path_img = os.path.expanduser("/home/ygh/deep_ocr/data/id_card_img.jpg") path_img = os.path.expanduser("/home/ygh/flask/id_card_img.jpg") debug_path = os.path.expanduser("/home/ygh/deep_ocr_workspace/debug") if debug_path is not None: if os.path.isdir(debug_path): shutil.rmtree(debug_path) os.makedirs(debug_path) cls_dir_sim = os.path.expanduser("/home/ygh/deep_ocr_workspace/data/chongdata_caffe_cn_sim_digits_64_64") cls_dir_ua = os.path.expanduser("/home/ygh/deep_ocr_workspace/data/chongdata_train_ualpha_digits_64_64") caffe_cls_builder = CaffeClsBuilder() cls_sim = caffe_cls_builder.build(cls_dir=cls_dir_sim,) cls_ua = caffe_cls_builder.build(cls_dir=cls_dir_ua,) caffe_classifiers = {"sim": cls_sim, "ua": cls_ua} seg_norm_width = 600 seg_norm_height = 600 preprocess_resize = PreprocessResizeKeepRatio( seg_norm_width, seg_norm_height) id_card_img = cv2.imread(path_img) id_card_img = preprocess_resize.do(id_card_img) segmentation = Segmentation(debug_path) key_to_segmentation = segmentation.do(id_card_img) boundaries = [ ((0, 0, 0), (100, 100, 100)), ] boundary2binimgs = [] for boundary in boundaries: preprocess_bg_mask = PreprocessBackgroundMask(boundary) id_card_img_mask = preprocess_bg_mask.do(id_card_img) boundary2binimgs.append((boundary, id_card_img_mask)) char_set = CharSet() char_set_data = char_set.get() rect_img_clf = RectImageClassifier( None, None, char_set, caffe_cls_width=64, caffe_cls_height=64) reco_text_line = RecoTextLine(rect_img_clf) key_ocr_res = {} for key in key_to_segmentation: key_ocr_res[key] = [] print("="*64) print(key) for i, segment in enumerate(key_to_segmentation[key]): if debug_path is not None: line_debug_path = "key_%s_%i" % (key, i) line_debug_path = os.path.join(debug_path, line_debug_path) reco_text_line.debug_path = line_debug_path reco_text_line.char_set = char_set_data[key] ## 初始化模型 caffe_cls = caffe_classifiers[ char_set_data[key]["caffe_cls"]] ## 輸入到模型中進行識別 ocr_res = reco_text_line.do(boundary2binimgs, segment, caffe_cls) ## 將結果輸出到列表中 key_ocr_res[key].append(ocr_res) print("ocr res:") for key in key_ocr_res: print("="*60) print(key) for res_i in key_ocr_res[key]: print(res_i.encode("utf-8")) if debug_path is not None: path_debug_image_mask = os.path.join( debug_path, "reco_debug_01_image_mask.jpg") cv2.imwrite(path_debug_image_mask, id_card_img_mask) ## 返回結果 將其封裝成json的鍵值對的格式 data = [{"result":"sucess","response":{"name":key_ocr_res["name"],"address":key_ocr_res["address"],"month":key_ocr_res["month"],
"minzu":key_ocr_res["minzu"],"year":key_ocr_res["year"],"sex":key_ocr_res["sex"],"id":key_ocr_res["id"],"day":key_ocr_res["day"]}}] ## data = '{"result":"sucess"} ## result = json.loads(data) return json.dumps(data,skipkeys=True,ensure_ascii=False,encoding="utf-8") else: data2 = [{"result":"error"}] ## result2 = json.loads(data2) return json.dumps(data2) ## return "error" if __name__ == '__main__': app.run(host='0.0.0.0',port=8880)
4.3 Android移動端設計
(1) 圖片處理
Android端獲取用戶輸入的身份證圖像信息有兩種方式,一種是拍照,一種是從手機相冊里選擇。首頁點擊“+”可以選擇兩種不同的方式,移動端首頁如圖所示:
圖 移動端首頁圖
拍照獲取圖像
在主活動首頁選擇“拍照”按鈕,按鈕的點擊事件是通過Intent機制調用系統相機功能開始拍照,首先通過getOutputMediaFileUri方法指定拍照后圖片的保存位置,之后利用Intent對象帶有MediaStore.ACTION_IMAGE_CAPTURE的參數開始拍照。
當獲取到系統相機拍的照片后,在主活動的onActivityResult回調方法中調用startPhotoZoom(fileUri)方法進行身份證圖片的裁剪,通過Intent機制調用系統相冊的裁剪功能,首先利用getOutputMediaFileUri方法指定裁剪后圖片的保存位置,然后指定Intent的動作為:action.CROP,然后開始裁剪當前圖片。
調用系統相機的裁剪事件后,進行圖像的剪切工作,之后通過Intent的putExtra方法將裁剪后的圖片路徑返回,
之后繼續執行onActivityResult回調方法,進行訪問接口,將獲取到的圖片上傳到服務器端。
選擇相冊獲取圖像
在主活動首頁選擇“相冊”按鈕,按鈕的點擊事件是通過Intent啟動系統相機的拍照功能,指定Intent的動作為ACTION_PICK,然后指定保存路徑,之后繼續執行onActivityResult回調方法完成選擇圖片的功能。
獲取到相冊里的圖片后,繼續執行onActivityResult回調方法里裁剪圖片的事件,完成后就訪問接口,方法是PostFile(String imgPath),將獲取到的圖片上傳到服務器端。
(2) 數據傳輸
本文圖片文件上傳與json格式數據獲取采用okhttp網絡請求框架,是一個效率非常高的 網絡請求框架,它采用連接池降低了請求延遲,下載文件時采用GZIP 縮減了下載的大小,並且對於請求不成功的狀態還支持請求的重傳,支持異步的調用。
在方法PostFile(String imgPath)中調用okhttp請求框架,在請求體中封裝要上傳的圖片內容,核心代碼如下:
RequestBody body = new MultipartBuilder().addFormDataPart("file",imgPath , RequestBody.create(MediaType.parse("media/type"), new File(imgPath))) .type(MultipartBuilder.FORM) .build();
設置傳輸格式為"media/type",然后將File傳入請求體完成封裝。然后通過execute()方法完成請求:
Response response = client.newCall(request).execute();
按照上文所描述,服務器會處理移動端的okhttp發送的網絡請求,接收身份證圖片進行下一步處理,等到處理后封裝成json數據並返回結果體,然后okhttp通過接收體Response接收json數據:
String tempResponse = response.body().string(); JSONArray arr = new JSONArray(tempResponse); String responsew = arr.getString(0); JSONObject obj = new JSONObject(responsew);
通過方法將數據格式解析成格式,之后直接解析JSONObject里的數據,利用handler機制將數據更新到首頁指定位置,具體實現效果如圖所示:
圖 返回數據並更新界面效果圖
Android端源碼已經上傳至github: 鏈接
綜上,完成了移動端獲取身份證圖像並上傳圖片,接收返回請求並更新界面等一系列操作。
五、總結
本文解決了在設計中遇到的諸多困難,比如:1)在模型的選擇上,經過大量實驗,最后選擇了符合本系統的改進Lenet網絡模型;2)在模型的訓練上,由於考慮到獲取大量身份證圖片數據比較困難,所以訓練模型采取對單個字符進行訓練識別,通過Python將字體文件庫轉換成單個字符灰度圖,共分6492類,放入11層改進Lenet模型進行訓練3)對於身份證圖片中的字符提取,首先利用水平和垂直投影法將字符定位,並切割,之后再將單獨的每一個字符放入模型中進行識別,實驗表明得到了較好的結果。
本文主要做的工作如下:1)首先提出了本文基於深度學習對身份證識別的研究意義,並介紹了深度學習發展的現狀以及文字識別的研究進展;2)然后介紹了卷積神經網絡、Lenet和Alexnet兩種模型,以及字符切割等關鍵的技術3)然后獲取數據訓練集,在實驗中選擇並設計模型,配置Solver文件,把深層卷積神經網絡模型進行訓練,得出精度超過96%的模型4)之后進行系統設計開發,對身份證圖像進行定位和切割,並放入模型中測試實驗,得到較好的識別率;5)最后完成了android端程序的設計以及Flask服務器的搭建,並連通系統完成身份證圖片信息識別的工作。
(完)
by still、