[AIOCR]手書き日本語OCRデータセットを自動生成する[etlcdb]

2021年11月18日木曜日

Artificial Intelligence

本記事では、ETL文字データベースからAIOCRのデータセットを自動生成する方法を紹介します。

アイキャッチ

AIOCRとは

本記事では、機械学習を利用したOCRであるAI-OCRのデータセットを生成する方法を紹介します。

こちらの記事「[機械学習]AIOCRの仕組みと実装」でも紹介していますが、AI-OCRは主に文字検出文字認識の2つの技術要素で構成されています。

そして文字検出のトレーニング、文字認識のトレーニングのそれぞれで学習データが必要となります。本記事では、etlcdbを利用してそれぞれの学習データを生成していきます。

コードの紹介の前にデータセットの概要を紹介します。

文字検出のデータセット

文字検出は画像中の文字の場所を検出します。
学習データは文字を含む画像と、文字の座標が入力となります。

下記は学習データのサンプルです。

文字検出データセットイメージ
[
 [x1, y1, width1, height1], # 文
 [x2, y2, width2, height2], # 字
 [x3, y3, width3, height3], # 検
 [x4, y4, width4, height4], # 出
]

文字認識のデータセット

文字認識は、文字検出によって抽出された文字を認識します。
正確には抽出された文字が、トレーニングによって学習された文字候補のどれに該当するかを分類しています。
このため、学習していない文字は認識できません。

学習データは、文字画像とその画像を示すテキストデータとなります。

下記は文字認識の学習データのサンプルです。

文字認識データセット

学習データ生成

学習データを生成していきます。
目標は下図左のetlcdb画像から外接矩形を取得するところまで行います。
なお、下記に紹介するコードはこちらのGithubにて公開しています。

目標データセット

なお、このデモはPythonで実装しています。
Pythonの実装に不安がある方、Pythonを使った機械学習について詳しく勉強したい方は、以下の書籍やオンライン講座などがおすすめです。

ETLCDBのパース

ETL文字データベースはバイナリファイルで提供されているためパースしていきます。

def convert_m_type(target_dir, JIS0201_char_dict):
    """
    ETL1, 6, 7のバイナリデータ(M-Type)をPNGに変換します
    M-Type : http://etlcdb.db.aist.go.jp/etlcdb/etln/form_m.htm 

    Parameters
    ----------
    target_dir : string
        バイナリデータがある親ディレクトリ. (E.g. ~/ETL7/)
    JIS0201_char_dict: dict
        JIS0201とCharのdict E.g. '0xb1': 'あ'

    Returns
    -------
    img_path_list : list
        saveしたimgのフルパスのリスト. E.g. [[img_full_path, unicode char e.g.'あ']...]
    
    """

    allFiles = glob.glob(target_dir + '*')
    targetFiles = [path for path in allFiles if os.path.isfile(path)]

    img_path_list = []

    # 変換. binary -> png.
    for etlfile in targetFiles:
        if 'INFO' in etlfile: # infoにはpngが含まれていないため.
            continue

        # 出力先ディレクトリ作成.
        dir_name = os.path.basename(etlfile)
        img_dir = target_dir + dir_name + '_IMG'
        os.makedirs(img_dir, exist_ok=True)
        print('create output image folder:', img_dir)

        # convert m-type data.
        RECORD_SIZE = 2052
        X_SIZE = 64
        Y_SIZE = 63
        DATA_NUM_POSITION = 0
        SERIAL_SHEETS_NUM_POSTION = 2
        JIS_CODE_POSITION = 3 # JIS Code (JIS X 0201)
        IMAGE_POSITION = 18
        Y_POSITION = 14
        X_POSITION = 15

        f = open(etlfile, 'rb')
        f.seek(0)

        while True:
            s = f.read(RECORD_SIZE)

            # stream完了でbreak.
            if not s:
                break

            # byte unpack
            r = struct.unpack('>H2sH6BI4H4B4x2016s4x', s)
            # read code jis.
            code_jis = r[JIS_CODE_POSITION]

            # JIS0201からunicode文字を取得.
            unicode_char = JIS0201_char_dict[ hex(code_jis) ]

            # JIS_CODE毎の出力ディレクトリ作成.
            jis_path = img_dir + '/' + str(code_jis)
            os.makedirs(jis_path, exist_ok=True)
            # imgのファイル名作成.
            fn = "{0:02x}_{1:02x}_{2:04x}.png".format(code_jis, r[DATA_NUM_POSITION], r[SERIAL_SHEETS_NUM_POSTION])
            image_full_path = jis_path + "/" + fn

            # when already exist image, skip save. 
            if os.path.exists(image_full_path) == False:
                # read image.
                iF = Image.frombytes('F', (X_SIZE, Y_SIZE), r[IMAGE_POSITION], 'bit', 4)
                iP = iF.convert('L')

                # save image.
                enhancer = ImageEnhance.Brightness(iP)
                iE = enhancer.enhance(16)
                iE.save(image_full_path)

            img_path_list.append([image_full_path, unicode_char])

    return img_path_list

文字画像から外接矩形を取得

OpenCVを使ってパースしたETL文字データベースの画像の文字から外接矩形を抽出します。

def create_contour(img_path):
    """
    opencvを利用し、外接矩形を抽出します.
    この関数は、ETL文字データベースの黒画像に白文字で1文字のみ文字が存在することを前提としています.

    Parameters
    ----------
    img_path : string
        画像データのフルパス E.g.[[img_full_path, unicode char e.g.'あ']...]

    Returns
    -------
    output : list
        img_pathに外接矩形の座標を付与したリスト E.g. [[img_path, [leftTop_x, leftTop_y, width, height], unicode char e.g. 'あ']...]
    
    """
    # load image.
    img_cv = cv2.imread(img_path[0], cv2.IMREAD_COLOR)
    # get image size.
    height, width, channels = img_cv.shape
    image_size = height * width

    # 入力画像の中には、画像の端まで文字がかかれているものがある
    # 余白がないとうまく文字の外接矩形を切り出せないため黒画素の画像を追加し余白を生成する.
    BLANK_SIZE = 1
    height_blank_array = np.zeros((height, BLANK_SIZE, 3), np.uint8)
    width_blank_array = np.zeros((1, width+BLANK_SIZE*2, 3), np.uint8) # width+BLANK_SIZE*2は先に幅1の画像を両端に連結するため.
    width_blank_pil = Image.fromarray(np.uint8(width_blank_array))
    height_blank_pil = Image.fromarray(np.uint8(height_blank_array))
    # 左右に連結.
    concat_img_cv = cv2.hconcat([convertPILtoCV(height_blank_pil), img_cv])
    concat_img_cv = cv2.hconcat([concat_img_cv, convertPILtoCV(height_blank_pil)])
    # 上下に連結.
    concat_img_cv = cv2.vconcat([convertPILtoCV(width_blank_pil), concat_img_cv])
    concat_img_cv = cv2.vconcat([concat_img_cv, convertPILtoCV(width_blank_pil)])

    # to gray scale.
    img_gray = cv2.cvtColor(concat_img_cv, cv2.COLOR_RGB2GRAY)

    # 2値化.
    # ref http://labs.eecs.tottori-u.ac.jp/sd/Member/oyamada/OpenCV/html/py_tutorials/py_imgproc/py_thresholding/py_thresholding.html
    ret,img_thresh = cv2.threshold(img_gray,155,255,cv2.THRESH_BINARY_INV)

    # 輪郭の取得.
    contours, hierarchy = cv2.findContours(img_thresh,cv2.RETR_TREE,cv2.CHAIN_APPROX_SIMPLE)

    for index, contour in enumerate(contours):
        area = cv2.contourArea(contour)
        # 画像全体を占めるareaを除去.
        if image_size * 0.99 < area:
            contours.pop(index)
            continue
        # 画像サイズの0.3%以下はノイズとして除去.
        # 64*63の画像中に12以下areaでノイズとして判定
        if area < image_size * 0.003:
            contours.pop(index)

    # 文字の外接矩形を取得.
    # inputの画像に文字は1文字のみという前提.
    # そのため、最小のx,y座標, 最大のx,y座標を抽出し、矩形を作り文字の外接矩形とする.
    x_list = []
    y_list = []

    for index, contour in enumerate(contours):
        for coordinate in contour:
            x_list.append(coordinate[0][0])
            y_list.append(coordinate[0][1])

    # 追加したBLANK_SIZE分座標を調整.
    if len(x_list) != 0 and len(y_list): 
        min_x = min(x_list) - BLANK_SIZE
        if min_x < 0:
            min_x = 0
        min_y = min(y_list) - BLANK_SIZE
        if min_y < 0:
            min_y = 0
        max_x = max(x_list) - BLANK_SIZE
        max_y = max(y_list) - BLANK_SIZE
        rec_width = max_x - min_x
        if width < rec_width:
            rec_width = width
        rec_height = max_y - min_y
        if height < rec_height:
            rec_height = height
    else:
        print('this file was not get contours:', img_path[0])
        min_x, min_y, rec_width, rec_height = 0, 0, 0, 0

    return [img_path[0], [min_x,min_y,rec_width,rec_height], img_path[1]]

これによって、画像中の文字の外接矩形を生成します。

出力結果

出力結果は以下のデータ形式でpickle化しています。

output data: ['./output/ETL7/ETL7LC_1_IMG/177/b1_01_0001.png', [14, 10, 35, 43], 'あ']

まとめ

本記事では、etl文字データベースからAIOCRのデータセットを生成する方法を紹介しました。
機械学習を使った機能の実現において、課題になりやすいことの一つとしてデータセットの確保が挙げられます。

データセットを一つずつ手作業で作成すると、時間と費用がかかるため
極力自動化できる方法をお勧めします。

また、もう少し学術的に体系立てて学びたいという方には以下の書籍などがお勧めです。ぜひご一読下さい。


また動かせるだけから理解して応用できるエンジニアの足掛かりに下記のUdemyなどもお勧めです。

AIで副業ならココから!

まずは無料会員登録

プロフィール

メーカーで研究開発を行う現役エンジニア
組み込み機器開発や機会学習モデル開発に従事しています

本ブログでは最新AI技術を中心にソースコード付きでご紹介します


Twitter

カテゴリ

このブログを検索

ブログ アーカイブ

TeDokology