Figure 1. UML Diagram of the OCR systems
OCR 파이프라인을 제작하면서 처음에 참고했던 파이프라인은 Automatic Number Plate Recognition(이하 ANPR) 의 파이프라인입니다.
여기서 1 번을 처리할 때, object detection 을 사용하지 않고 semantic segmentation 을 사용하였습니다. Object Detection 기술은 기본적으로 rectangular shape 을 검출합니다. 하지만 고객이 촬영한 신분증은 어떤 형태일지 알 수 없습니다. 일부가 가려진 형태일 수 있고, 촬영각도가 틀어져서 이미지가 정방향으로 촬영되지 않았을 수 있습니다. 이러한 문제는 semantic segmentation 을 사용하면 해결됩니다.
Semantic segmentation 문제는 여러 최신 모델들을 통해 해결할 수 있습니다. 하지만 문제가 되는 것은 state-of-the-art 성능을 내는 모델들은 ImageNet pretrained model 을 이용한 fine tuning 을 하여야 claimed accuracy 를 낼 수 있다는 것이었습니다. [1] self training 을 통해 fine tuning 보다 더 나은 성능을 확보할 수도 있었지만, 해당 논문이 publish 되기 전 프로젝트의 전체 파이프라인을 설계하고 있어서 해당 기술은 고려대상이 아니었습니다. 그러므로 [2] DeepLab V3+, [3] HRNet V2 와 같은 모델을 고려하였습니다.
데이터의 전처리는 [4] CoCo datasets 를 이용하였습니다. Python API 를 통해 정형화된 전처리를 할 수 있는 장점이 있었기 때문입니다.
import os
import cv2
import numpy as np
from detail import Detail, maskUtils
from matplotlib import pyplot as plt
%matplotlib inline
image = cv2.imread("../data/rimages/loan-117304-신분증앞면-1.jpeg")
annotation = cv2.imread("../data/annotations/loan-117304-신분증앞면-1.png")
plt.imshow(cv2.cvtColor(annotation[:, :, 2], cv2.COLOR_BGR2RGB))
예시 annotation 이미지입니다. 촬영 각도가 틀어져있는 것을 확인할 수 있습니다.
image_extensions = dict()
annotation_extensions = dict()
IMAGE_DIR = "../data/rimages"
ANNOTATION_DIR = "../data/annotations"
# List all the image in the dataset
for _, _, files in os.walk(IMAGE_DIR):
for filename in files:
key, ext = filename.split(".")
image_extensions[key] = ext
for _, _, files in os.walk(ANNOTATION_DIR):
for filename in files:
key, ext = filename.split(".")
annotation_extensions[key] = ext
이미지의 extension 이 제각각이었기 때문에 dictionary 로 관리해주었습니다.
def mask2rle(image):
"""
Convert mask to rle
image: numpy array
1 - mask,
0 - background
Returns run length as string formated
"""
if not isinstance(image, np.ndarray):
raise TypeError("image should be in np.ndarray type.")
if np.max(image) == 255:
print("Warning: Image should be converted to binary format.")
image = image // 255
pixels = image.T.flatten()
pixels = np.concatenate([[0], pixels, [0]])
runs = np.where(pixels[1:] != pixels[:-1])[0] + 1
runs[1::2] -= runs[::2]
return ' '.join(str(x) for x in runs)
# test if the implementation is correct
example = np.array(
[
[1, 1, 0, 0, 0, 0, 0],
[0, 0, 0, 0, 0, 1, 1]
]
).T
assert mask2rle(example) == '1 2 13 2'
Segmentation mask 의 encoding method 로 [6] run length encoding 기법을 사용하였습니다. CoCo api 가 지원하는 segmentation mask 인코딩 기법은 2 가지로, polygonal encoding 과 run length encoding 방법이 있습니다. 기존의 annotation 이 binary image 이었기 때문에 run length encoding 이 더 적합하다 판단했습니다.
def generate_image_description(key, extension, phase, height, width):
return {
"file_name": ".".join([key, extension]),
"phiase": phase,
"height": height,
"width": width,
"image_id": key
}
def generate_segmentation_description(key, extension, annotation, idx):
encoded = maskUtils.encode(np.asfortranarray(annotation))
return {
"segmentation": {
"size": encoded["size"],
"counts": encoded["counts"].decode("ascii")
},
"area": maskUtils.area(encoded),
"iscrowd": 1,
"image_id": key,
"category_id": 1,
"id": idx
}
def generate_categories(supercategory, category_id, name):
return {
"supercategory": supercategory,
"category_id": category_id,
"name": name,
"onlysemantic": 0,
"parts": []
}
def image_resize(image, width=None, height=None, inter=cv2.INTER_LANCZOS4):
dim = None
(h, w) = image.shape[:2]
if width is None and height is None:
return image
try:
if width is None:
r = height / float(h)
dim = (int(w * r), height)
resized = cv2.resize(image, dim, interpolation=inter)
resized = cv2.copyMakeBorder(resized, 0, 0, 0, height - dim[0], cv2.BORDER_CONSTANT, 0)
else:
r = width / float(w)
dim = (width, int(h * r))
resized = cv2.resize(image, dim, interpolation=inter)
resized = cv2.copyMakeBorder(resized, 0, width - dim[1], 0, 0, cv2.BORDER_CONSTANT, 0)
except Exception as e:
print(width - dim[1], dim[1])
raise e
return resized
데이터셋을 만드는데 필요한 miscellaneous 함수들입니다.
from sklearn.model_selection import train_test_split
valid_keys = list()
for key in image_extensions.keys():
if annotation_extensions.get(key, None) is not None:
# This is the valid example
valid_keys.append(key)
train, test = train_test_split(valid_keys, test_size=0.1)