- vừa được xem lúc

Trích xuất thông tin từ chứng minh thư

0 0 244

Người đăng: Bui Quang Manh

Theo Viblo Asia

1. Lời mở đầu

Gần đây chắc hẳn các bạn đã nghe nhiều tới các khái niệm như định danh điện tử, Ekyc,... Nếu như trước đây, khách hàng muốn mở tài khoản ngân hàng, mở thẻ ATM... sẽ phải đến trực tiếp quầy giao dịch để thực hiện các thủ tục đăng kí, xác minh thông tin, thì giờ đây các thao tác này đều có thể thực hiện qua chiếc điện thoại nhờ giải pháp định danh khách hàng điện tử (eKYC). Và bài toán Trích xuất thông tin từ chứng minh thư chính là bài toán nhỏ trong ứng dụng định danh điện từ này. Hôm nay mình xin giới thiệu các bước giúp các bạn thực hiện bài toán này. Tuy cách này chưa thực sự là cách giải quyết tốt nhất nhưng qua bài này các bạn mới học về AI cũng có thể học thêm được kiến thức qua bài toán này. Toàn bộ source code các bạn có thể xem tại link github của mình.

Nguồn TP Bank

2. Các bước xử lý

Trong bài này, mình đề xuất một hướng xử lý như sau :

  1. Detect 4 góc chứng minh thư
  2. Xoay chứng minh thư
  3. Detect vùng chữ có trong chứng minh thư
  4. OCR

2.1. Detect 4 góc chứng minh thư

Do ảnh đầu vào là ảnh chụp từ điện thoại, có thể ảnh bị nghiêng, bị xoay ngược do đó chúng ta cần bước xoay thẳng lại để có thể dễ dàng xử lý. Vì lý do như vậy, nếu chúng ta dùng phương pháp bình thường detect nguyên cả chứng minh thư thì chúng ta khó có thể xoay lại được bằng xử lý ảnh hoặc phải dùng các phương pháp phức tạp hơn. Để đơn giản, ở bài này mình đề xuất một phương pháp đó là chúng ta sẽ coi bốn góc của chứng minh thư như là một object chúng ta cần detect sau đó chúng ta sẽ xoay thẳng bằng tọa độ của bốn góc này. Nếu như các bạn đã làm quen với các bài toán detect face, bike, car,... , trong đó bike hay car là object thì ở đây bốn góc : bottom left, bottom right, top left, top right chính là 4 object chúng ta cần tìm. Các bạn có thể thực hiện nhanh chóng bước này bằng cách sử dụng dữ liệu của bản thân kết hợp với mô hình detect đã được cung cấn sẵn bởi Tensorflow API. Các bạn có thể tìm bài viết hướng dẫn Tensorflow API của mình ở bài viết này. Ta sẽ có kết quả như hình sau:

Step 1: Detect 4 corners

2.2. Xoay chứng minh thư

Ở đây ta cần bốn góc để xoay nhưng mô hình chúng ta lại chỉ detect được có 3 góc ?. Vậy làm thế nào ta có thể tính được ra tọa độ góc còn lại bây giờ ? Đơn giản thôi, chúng ta chỉ cần áp dụng một kiến thức hình học cấp hai, ta dễ dàng nội suy ra tọa độ của 3 góc ra tọa độ góc còn lại. Các bác thử ngâm cứu vẽ vời ra giấy tính toán thử xem. Còn nếu lười thì dùng luôn code của tôi dưới đây nhé :

Đầu tiên chúng ta cần phải tính ra tọa độ trung tâm của mỗi bouding box hay tọa độ của bốn góc bằng hàm get_center_point(). Hàm này nhận dữ liệu đầu vào là dictionary chứa key là tên góc và value: tọa độ của bounding box tương ứng. Đẩu ra là một dictionary chứa key là tên góc và value: tọa độ trung tâm bounding box tương ứng hay chính là tọa độ các góc.

def get_center_point(coordinate_dict): di = dict() for key in coordinate_dict.keys(): xmin, ymin, xmax, ymax = coordinate_dict[key] x_center = (xmin + xmax) / 2 y_center = (ymin + ymax) / 2 di[key] = (x_center, y_center) return di

Sau đó chúng ta đưa dictionary thu được bên trên vào hàm calculate_missed_coord_corner để nội suy ra góc còn thiếu và trả dữ liệu dạng dictionary chứa tên và tọa độ bốn góc tương ứng. Ở đây hàm này chỉ khắc phục trong trường hợp thiếu một góc, những trường hợp thiếu hai góc không khắc phục được. Cách khắc phục nhược điểm thiếu nhiều góc là cải thiện model detect trở nên tốt hơn.

def calculate_missed_coord_corner(coordinate_dict): thresh = 0 index = find_miss_corner(coordinate_dict) # calculate missed corner coordinate # case 1: missed corner is "top_left" if index == 0: midpoint = np.add(coordinate_dict['top_right'], coordinate_dict['bottom_left']) / 2 y = 2 * midpoint[1] - coordinate_dict['bottom_right'][1] - thresh x = 2 * midpoint[0] - coordinate_dict['bottom_right'][0] - thresh coordinate_dict['top_left'] = (x, y) elif index == 1: # "top_right" midpoint = np.add(coordinate_dict['top_left'], coordinate_dict['bottom_right']) / 2 y = 2 * midpoint[1] - coordinate_dict['bottom_left'][1] - thresh x = 2 * midpoint[0] - coordinate_dict['bottom_left'][0] - thresh coordinate_dict['top_right'] = (x, y) elif index == 2: # "bottom_left" midpoint = np.add(coordinate_dict['top_left'], coordinate_dict['bottom_right']) / 2 y = 2 * midpoint[1] - coordinate_dict['top_right'][1] - thresh x = 2 * midpoint[0] - coordinate_dict['top_right'][0] - thresh coordinate_dict['bottom_left'] = (x, y) elif index == 3: # "bottom_right" midpoint = np.add(coordinate_dict['bottom_left'], coordinate_dict['top_right']) / 2 y = 2 * midpoint[1] - coordinate_dict['top_left'][1] - thresh x = 2 * midpoint[0] - coordinate_dict['top_left'][0] - thresh coordinate_dict['bottom_right'] = (x, y) return coordinate_dict

Sau khi có được tọa độ 4 góc của chứng minh thư, ta xoay thẳng ảnh lại dựa vào kích thước thực tế có chiều dài 500, chiều rộng 300. Ở đây, mình dùng PerspectiveTransform của OpenCV:

def perspective_transform(image, source_points): dest_points = np.float32([[0, 0], [500, 0], [500, 300], [0, 300]]) M = cv2.getPerspectiveTransform(source_points, dest_points) dst = cv2.warpPerspective(image, M, (500, 300)) return dst

Và kết quả cuối cùng, mình sẽ thu được về như sau:

Step 2: Xoay ảnh

2.3. Detect vùng chữ có trong chứng minh thư

Tương tự như ý tưởng detect các góc, ở task này chúng ta sẽ sử dụng các mô hình detect có sẵn trong thư viện Tensorflow API như ở bước 1 đã làm để huấn luyện detect ra các chữ có trong ảnh chứng minh thư đã được crop ở bước trên. Các bạn có thể xem hướng dẫn ở đây. Mình chia các chữ ra thành 5 class tương ứng với 5 trường thông tin cần thu: id, name, birth, home và add. Chúng ta sẽ thu được ảnh kết quả trả về như sau:

Ảnh 1: Trước khi dùng NMS

Trong ảnh thu được, để tránh hiện tượng cùng một object nhưng có nhiều bouding box đè lên nhau (overlap), chúng ta sử dụng một thuật toán có tên là NMS (Non Maximum Suppression) giúp loại bỏ đi box thừa giữ lại box tốt nhất cho object.

 def non_max_suppression_fast(boxes, labels, overlapThresh): # if there are no boxes, return an empty list if len(boxes) == 0: return [] # if the bounding boxes integers, convert them to floats -- # this is important since we'll be doing a bunch of divisions if boxes.dtype.kind == "i": boxes = boxes.astype("float") # # initialize the list of picked indexes pick = [] # grab the coordinates of the bounding boxes x1 = boxes[:, 1] y1 = boxes[:, 0] x2 = boxes[:, 3] y2 = boxes[:, 2] # compute the area of the bounding boxes and sort the bounding # boxes by the bottom-right y-coordinate of the bounding box area = (x2 - x1 + 1) * (y2 - y1 + 1) idxs = np.argsort(y2) # keep looping while some indexes still remain in the indexes # list while len(idxs) > 0: # grab the last index in the indexes list and add the # index value to the list of picked indexes last = len(idxs) - 1 i = idxs[last] pick.append(i) # find the largest (x, y) coordinates for the start of # the bounding box and the smallest (x, y) coordinates # for the end of the bounding box xx1 = np.maximum(x1[i], x1[idxs[:last]]) yy1 = np.maximum(y1[i], y1[idxs[:last]]) xx2 = np.minimum(x2[i], x2[idxs[:last]]) yy2 = np.minimum(y2[i], y2[idxs[:last]]) # compute the width and height of the bounding box w = np.maximum(0, xx2 - xx1 + 1) h = np.maximum(0, yy2 - yy1 + 1) # compute the ratio of overlap overlap = (w * h) / area[idxs[:last]] # delete all indexes from the index list that have idxs = np.delete(idxs, np.concatenate(([last], np.where(overlap > overlapThresh)[0]))) # return only the bounding boxes that were picked using the # integer data type final_labels = [labels[idx] for idx in pick] final_boxes = boxes[pick].astype("int") return final_boxes, final_labels

Các bạn để ý các box ở trường home và add đã ít hơn hẳn so với ảnh trước.

Ảnh 2: Sau khi dùng NMS

2.4. OCR

OCR hay nhận dạng kí tự là kĩ thuật nhận dạng chữ có trong ảnh đầu vào. Với các box chữ chúng ta thu được từ bước trên, ta crop và đưa qua mô hình OCR để đọc. Để giải quyết bài toán nhanh gọn, không mất thời gian các bạn có thể sử dụng thư viện VietOcr của tác gỉa Phạm Quốc, lý thuyết về Transformer OCR các bạn có thể theo dõi tại bài viết của mình nhé. Ở đây do tác giả chưa hỗ trợ dự đoán theo batch_size nên các bạn có thể custom lại file Predictor.py như sau để dự đoán theo theo batch_size nhanh hơn nhé:

from vietocr.tool.translate import build_model, translate, translate_beam_search, batch_translate_beam_search
import cv2
import numpy as np
import math
import time
import torch
from collections import defaultdict class Predictor(object): def __init__(self, config): device = config['device'] model, vocab = build_model(config) weights = config['weights'] model.load_state_dict(torch.load(weights, map_location=torch.device(device))) self.config = config self.model = model self.vocab = vocab def predict(self, img): img = img / 255.0 img = self.preprocess_input(img) img = np.expand_dims(img, axis=0) img = torch.FloatTensor(img) img = img.to(self.config['device']) if self.config['predictor']['beamsearch']: sent = translate_beam_search(img, self.model) s = sent else: s = translate(img, self.model)[0].tolist() s = self.vocab.decode(s) return s def batch_predict(self, images): """ param: images : list of ndarray """ batch_dict, indices = self.batch_process(images) list_keys = [i for i in batch_dict if batch_dict[i] != batch_dict.default_factory()] result = list([]) for width in list_keys: batch = batch_dict[width] batch = np.asarray(batch) batch = torch.FloatTensor(batch) batch = batch.to(self.config['device']) if self.config['predictor']['beamsearch']: sent = batch_translate_beam_search(batch, model=self.model) else: sent = translate(batch, self.model).tolist() batch_text = self.vocab.batch_decode(sent) result.extend(batch_text) # sort text result to original coordinate def get_index(element): return element[1] z = zip(result, indices) sorted_result = sorted(z, key=get_index) result, _ = zip(*sorted_result) return result def preprocess_input(self, image): """ param: image: ndarray of image """ h, w, _ = image.shape new_w, image_height = self.resize_v1(w, h, self.config['dataset']['image_height'], self.config['dataset']['image_min_width'], self.config['dataset']['image_max_width']) img = cv2.resize(image, (new_w, image_height)) img = np.transpose(img, (2, 0, 1)) return img def batch_process(self, images): batch_img_dict = defaultdict(list) image_height = self.config['dataset']['image_height'] images = images / 255.0 batch_img_li = [self.preprocess_input(img) for img in images] batch_imgs, width_list, indices = self.sort_width(batch_img_li, reverse=False) min_bucket_width = min(width_list) max_width = max(width_list) thresh = 30 max_bucket_width = np.minimum(min_bucket_width + thresh, max_width) for i, image in enumerate(batch_imgs): c, h, w = image.shape # reset min_bucket_width, max_bucket_width if w > max_bucket_width: min_bucket_width = w max_bucket_width = np.minimum(min_bucket_width + thresh, max_width) avg_bucket_width = int((max_bucket_width + min_bucket_width) / 2) new_img = self.resize_v2(image, avg_bucket_width, height=image_height) batch_img_dict[avg_bucket_width].append(new_img) return batch_img_dict, indices @staticmethod def sort_width(batch_img, reverse=False): def get_img_width(element): img = element[0] c, h, w = img.shape return w batch = list(zip(batch_img, range(len(batch_img)))) sorted_batch = sorted(batch, key=get_img_width, reverse=reverse) sorted_batch_img, indices = list(zip(*sorted_batch)) return sorted_batch_img, list(map(get_img_width, batch)), indices @staticmethod def resize_v1(w, h, expected_height, image_min_width, image_max_width): new_w = int(expected_height * float(w) / float(h)) round_to = 10 new_w = math.ceil(new_w / round_to) * round_to new_w = max(new_w, image_min_width) new_w = min(new_w, image_max_width) return new_w, expected_height @staticmethod def resize_v2(img, width, height): new_img = np.transpose(img, (1, 2, 0)) new_img = cv2.resize(new_img, (width, height), cv2.INTER_AREA) new_img = np.transpose(new_img, (2, 0, 1)) return new_img

3. Kết quả

Trong bài viết lần này, mình có xây dựng một API bằng thư viện FastApi để các bạn dễ hình dung hơn, các bạn có thể chạy demo bằng cách sau đây:

git clone https://github.com/buiquangmanhhp1999/extract-information-from-identity-card.git
pip install -r requirement.txt
cd complete
python server.py

Và kết quả API trả về như sau:

{ "id": "174873017", "name": "NGUYỄN TÀI ĐỨC", "birth": "10-10-1998", "home": "TT Lang Chánh Huyện Lang Chánh Thanh Hóa", "add": "Xã Đông Hòa Huyện Đông Sơn Thanh Hoá"
}

4. Lời kết

Cảm ơn các bạn đã theo dõi bài viết của mình. Bài toán trích xuất từ chứng minh thư đã không còn xa lạ trong cộng đồng học machine learning nhưng mình vẫn hy vọng bài viết của mình sẽ giúp cho nhiều bạn có thêm được nhiều kiến thức mới mẻ. ???

Bình luận

Bài viết tương tự

- vừa được xem lúc

Information Extraction trong OCR là gì? Phương pháp nào để giải quyết bài toán?

Giới thiệu bài toán. Nhắc đến Key Information Extraction là gì chắc nhiều bạn đang chưa có câu trả lời cho mình, vì lâu nay đối với bài toán Optical Character Recognition (OCR) thường thì chúng ta sẽ quan tâm đến các bài toán như Text Detection và Text Recognition.

0 0 78

- vừa được xem lúc

Bài toán trích xuất thông tin từ hóa đơn

Mở đầu. Đợt vừa rồi thì mình có tham gia một cuộc thi về trích xuất thông tin từ hóa đơn có tên gọi là The Mobile capture receipts Optical Character Recognition (MC-OCR) .

0 0 353

- vừa được xem lúc

Tìm hiểu về STN-OCR, mạng neural vừa có thể phát hiện và nhận dạng văn bản

1. Giới thiệu.

0 0 42

- vừa được xem lúc

[OCR] Nhận dạng biểu thức toán học viết tay với Dense + MSA

I. Giới thiệu.

0 0 45

- vừa được xem lúc

Cơ chế mã hóa vị trị 2 chiều giải quyết bài toán nhận dạng nhiều dòng

Giới thiệu. Hầu hết các mô hình nhận dạng văn bản hiện nay đều xử lí trên dữ liệu 1 dòng .

0 0 34

- vừa được xem lúc

XÂY DỰNG MÔ HÌNH 1 PHA PHÁT HIỆN VÀ NHẬN DẠNG VĂN BẢN NHIỀU DÒNG

Tổng quan. Tổng quan bài toán: Trong lĩnh vực xử lí ảnh trong Học sâu, đặc biệt là liên quan đến bài toán nhận dạng kí tự quan học, các bài toán phát hiện và nhận dạng văn bản vẫn đang là một bài toán

0 0 44