Architecture

Figure 1. UML Diagram of the OCR systems

Figure 1. UML Diagram of the OCR systems

OCR 파이프라인을 제작하면서 처음에 참고했던 파이프라인은 Automatic Number Plate Recognition(이하 ANPR) 의 파이프라인입니다.

  1. 번호판 영역을 찾아냅니다.
  2. 영역 내에서 정해진 필드를 Template 을 통해 찾아냅니다.
  3. 필드의 글자를 식별합니다.

여기서 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 와 같은 모델을 고려하였습니다.

Preprocessing

데이터의 전처리는 [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 encodingrun 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)