Note by Yoshikawa

Love Technology and Art

論文意訳: TableNet: Deep Learning model for end-to-end Table detection and Tabular data extraction from Scanned Document Image

issue: 2020.01 url: https://arxiv.org/pdf/2001.01469.pdf(https://arxiv.org/pdf/2001.01469.pdf)

問題提起

文書の画像データを扱う機会が増えたけど、人力で文字を読んで入力したりするのは大変

テーブル形式の文書データが増えた

テーブルにも色々なレイアウトがあり、そこから文字を機械的に検出するの難題

今ある手法はほとんどが

1) テーブル範囲の検出

2) テーブルの構造の検出

のどちらかを別々にやってるが、それぞれのタスクはそれぞれ補完しうるで、

マルチタスクで解けばいいのではないか

解決案

マルチタスクでEnd2Endにテーブル検出できるモデル「**TableNet** 」を提案

このモデルでは以下を検出

1) テーブルの範囲

2) テーブル内のカラムのセグメンテーション、その後ルールベースで行検出し、セルを取得

エンコーダーとしてImageNet datasetで学習ずみのVGG19を使用

エンコーダーの出力を二種類のデコーダーに渡してテーブル範囲と列を検出

モデル構造
f:id:yoshikawat64m:20200823235637p:plain

データ

学習データ

Marmotデータセットにテーブルのアノテーションをしたもの

1016種類 中国語と英語が半々

評価データ

ICDAR2013 dataset

実装

環境
Intel(R) Xeon(R) Silver CPU having 32 cores
and RAM of 128 GB Tesla V100-PCIE-1 GPU with 6GB
of GPU memory.

ライブラリ tensorflow

学習
結果

既存の手法より少しだけ精度向上

ネットワークの構造

CNN:畳み込みニューラルネットワーク

##LeNet
###概要

1998年、Yann LeCun氏が発案

論文: http://yann.lecun.com/exdb/publis/pdf/lecun-98.pdf

###構造



##AlexNet
###概要
最近のディープラーニングブームの火付け役

2014年

①VGG16

import keras
from keras.layers import Conv2D, MaxPooling2D, Input, Dense, Flatten
from keras.models import Model

inputs = Input(shape=(224, 224, 3))
x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv1')(inputs)
x = Conv2D(64, (3, 3), activation='relu', padding='same', name='block1_conv2')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='valid', name='block1_pool')(x)
x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv1')(x)
x = Conv2D(128, (3, 3), activation='relu', padding='same', name='block2_conv2')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='valid', name='block2_pool')(x)
x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv1')(x)
x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv2')(x)
x = Conv2D(256, (3, 3), activation='relu', padding='same', name='block3_conv3')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='valid', name='block3_pool')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv1')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv2')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block4_conv3')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='valid', name='block4_pool')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv1')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv2')(x)
x = Conv2D(512, (3, 3), activation='relu', padding='same', name='block5_conv3')(x)
x = MaxPooling2D((2, 2), strides=(2, 2), padding='valid', name='block5_pool')(x)
flattened = Flatten(name='flatten')(x)
x = Dense(4096, activation='relu', name='fc1')(flattened)
x = Dense(4096, activation='relu', name='fc2')(x)
predictions = Dense(1000, activation='softmax', name='predictions')(x)

model = Model(inputs=inputs, outputs=predictions)

model.summary()

②AlexNet
f:id:yoshikawat64m:20181102155325p:plain

import keras
from keras.layers import Conv2D, MaxPooling2D, Input, Dense, Flatten, BatchNormalization
from keras.models import Model

inputs = Input(shape=(227, 227, 3))

# Block 1
x = Conv2D(96, (11, 11), padding='valid', strides=(4, 4), activation='relu', name='conv1')(inputs) #out:55*55*96
x = MaxPooling2D((3, 3), padding='valid', strides=(2, 2), name='pool1')(x) #out:27*27*96
x = BatchNormalization(name='norm1')(x)

# Block 2
x = Conv2D(256, (5, 5), padding='same',  strides=(1, 1), activation='relu', name='conv2')(x) #out:27*27*256 padding:2
x = MaxPooling2D((3, 3), padding='valid',  strides=(2, 2), name='pool2')(x) #out:13*13*256
x = BatchNormalization(name='norm2')(x)

# Block 3
x = Conv2D(384, (3, 3), padding='same', strides=(1, 1), activation='relu', name='conv3')(x) #out:13*13*384 padding:1
x = Conv2D(384, (3, 3), padding='same', strides=(1, 1), activation='relu', name='conv4')(x) #out:13*13*384 padding:1
x = Conv2D(256, (3, 3), padding='same',  strides=(1, 1), activation='relu', name='conv5')(x) #out:13*13*256 padding:1
x = MaxPooling2D((3, 3), padding='valid',  strides=(2, 2), name='pool3')(x) #out:6*6*256

# Classification block
flattened = Flatten(name='flatten')(x) # out:9216
x = Dense(4096, activation='relu', name='fc1')(flattened)  # out:4096
x = Dense(4096, activation='relu', name='fc2')(x) # out:4096

predictions = Dense(1000, activation='softmax', name='predictions')(x)

model = Model(inputs=inputs, outputs=predictions)

model.summary()

③GoogLeNet

④ResNet

openCVで台形補正

opencvを使って台形補正する方法を紹介

台形補正とは?

もともと四角形だったものを斜めから撮影すると遠近によるゆがみで台形に見える。
この遠近による歪みをなくし、真上から見た元の四角形の形に補正する。

やり方

①getPerspectiveTransformで透視変換行列を計算

getPerspectiveTransformでは元画像の変換したい台形の4つの頂点座標と変換後の画像サイズを入力して、変換行列を出力する。

変換行列の定義は以下
f:id:yoshikawat64m:20190121115333p:plain
引用:
opencv.jp


[変換前のx座標,変換前のy座標,1]
を追加したベクトルの変換行列を掛けると、
[変換後のx座標*t,変換後のy座標*t,t]になる

②warpPerspectiveで補正

warpPerspectiveは、元画像、変換行列、変換後の画像サイズを入力して、変換後の画像を出力する
①で求めた透視変換行列を使う

コード

# ---------------------------------------
#              透視変換行列取得
# ---------------------------------------
def get_transformMatrix(transformArea, transformSize):
    """
    Args:
        transformArea: 4点座標[top-left,top-right,down-right,down-left]
        transformSize: 変換後の画像サイズ(横,縦)
    Return:
        transformMatrix: 透視変換行列
    """
    pts1 = np.float32(transformArea)
    pts2 = np.float32([[0, 0], [transformSize[0], 0], [transformSize[0], transformSize[1]], [0, transformSize[1]]])
    transformMatrix = cv2.getPerspectiveTransform(pts1, pts2)
    return transformMatrix


# --------------------------------------------
#                   補正画像出力
# --------------------------------------------
def perspectiveTransform(img, transformMatrix, transformSize):
    """
    Args:
        img:元画像
        transformMatrix: 透視変換行列
        transformSize: 変換後の画像サイズ(横,縦)
    Return:
        transformImage: 変換後の画像
    """
    transformImage = cv2.warpPerspective(img, transformMatrix, transformSize)
    return transformImage

ニューラルネットの活性化関数

STEP


f(x) = \begin{cases}
    1~~~~  (x > α) \newline
    0~~~~  (x \leq α )
\end{cases}

特徴

コード

def step(x):
  if x > 0:
    return 1
  else:
    return 0

プロット

f:id:yoshikawat64m:20190114202352p:plain

relu

{
f(x) =
 \begin{cases}
x~~~~  (x > 0) \newline
0~~~~  (x \leq 0 )
\end{cases}
}

特徴

  • よく使われる
  • シンプル
  • 勾配消失が防げる
  • 計算コストが低い
  • 性能がいい

 コード

def relu(x):
  return max(0,x)

プロット

f:id:yoshikawat64m:20190114202414p:plain

Leaky Relu

{
f(x) =
 \begin{cases}
x~~~~  (x > 0) \newline
αx~~~~  (x \leq 0 )
\end{cases}
}

特徴

  • Reluの0以下の場合に0にせず、傾斜をつけて出力

コード

def leaky_relu(x, α):
  if x>0:
    return x
  else:
    return α*x

プロット

f:id:yoshikawat64m:20190114202434p:plain

Sigmoid

 {
f(x) = \cfrac{1}{1+e^{-x}}
}

特徴

  • 0から1に正規化する

コード

def sigmoid(x):
  return 1/(1 + e(-x))

プロット

f:id:yoshikawat64m:20190114202447p:plain

Tahn

 {
f(x) = \cfrac{1- e^{-2x}}{1+ e^{-2x}}
}

特徴

  • -1から1で正規化

コード

def tanh(x):
  return (1-exp(-2*x))/(1+exp(-2*x))

プロット

f:id:yoshikawat64m:20190114202500p:plain

Hard Tahn

{
f(x) =
 \begin{cases}
1~~~~  (x > 1) \\
x~~~~  (-1 \leq x  \leq 1) \\
-1~~~~ (x \leq -1)
\end{cases}
}

 f(x) = max(-1, min(1,x))

特徴

  • 0から1の範囲はそのまま
  • 1以上は1,0以下は0、

コード

def hard_tanh(x):
  if x > 1:
    return 1
  elif x < -1: 
    return -1
  else:
    return x

プロット

f:id:yoshikawat64m:20190114202620p:plain

Softplus

{
f(x) = log(1+e^{-x})
}

特徴

  • あまり使われない

コード

def softplus(x):
  return log(1+exp(x))

プロット

f:id:yoshikawat64m:20190114202648p:plain

Softplus

式(例:ガウス関数)

{
f(x) = e^{-α(β-x)^2}
}

特徴

  • あまり使われない
  • ある値を中心に左右対称
  • 放射基底関数ともいう
  • ガウス関数が代表的

コード

def rbf_gaussian(x, α, β):
  return exp(-α*(β - x)**2)

プロット

f:id:yoshikawat64m:20190114202715p:plain

プロッまとめ

f:id:yoshikawat64m:20190115211931p:plain

tkinter使ってみた

プログラムの実行をguiにしたいときに手軽に組める。

以下サンプル

よく使う入力枠、ドロップダウンリスト、ラジオボタンの例

import tkinter  as tk
from tkinter import messagebox as tkm
from tkinter import ttk

def show(line):
    tkm.showinfo('sample', line)

root = tk.Tk()
root.title(u'Sample')
root.geometry('300x300')

# 入力ボックス
label1 = ttk.Label(text=u'入力')
label1.pack()

entry = ttk.Entry(width=20)
entry.pack(pady=(0,5))

# ドロップダウンリスト
label2 = ttk.Label(text=u'ドロップダウンリスト')
label2.pack()

combo = ttk.Combobox(width=15, state='readonly')
combo["values"] = ['ちょえーーーー!!!','くわっ!!!','はーーーーーっ!!!','ふひふふっひいいい!!!']
combo.current(0)
combo.pack(pady=(0,5))

# ラジオボタン
label3 = ttk.Label(text=u'ラジオボタン')
label3.pack()

var = tk.StringVar()
radio1 = ttk.Radiobutton(text=u"1", variable=var, value='キタ━─━─━ヾ(o✪‿✪o)シ━─━─━ッ♪')
radio1.pack()
radio2 = ttk.Radiobutton(text=u"2", variable=var, value='( `)3′)▃▃▃▅▆▇▉ブブブーブォォオ')
radio2.pack(pady=(0,5))

#ボタンを押すと関数呼び出し
button1 = tk.Button(text=u'入力ボックス表示', width=20, height=2, command=lambda: show(entry.get()))
button1.pack(pady=(0,5))

button2 = tk.Button(text=u'ドロップダウンリスト表示', width=20, height=2, command=lambda: show(combo.get()))
button2.pack(pady=(0,5))

button3 = tk.Button(text=u'ラジオボタン選択表示', width=20, height=2, command=lambda: show(var.get()))
button3.pack(pady=(0,5))

root.mainloop()


タブを使う場合

import tkinter as tk
from tkinter import ttk
from tkinter import messagebox as tkm


def show(line):
    tkm.showinfo('test', line)


class set_tab1():
    def __init__(self, tab):
        self.var = tk.StringVar()
        self.tab = tab
        self.set_entry()
        self.set_button()

    def set_entry(self):
        frame = tk.Frame(self.tab)
        frame.pack(pady=(10, 20))

        label = ttk.Label(frame, text=u'エントリー', width=10)
        label.pack(side='left', anchor=tk.W, padx=(0, 10))

        entry = ttk.Entry(frame, textvariable=self.var, width=25)
        entry.pack(side='left', anchor=tk.W)

    def set_button(self):
        Button = tk.Button(self.tab, text=u'表示', width=20, height=3, command=lambda: show(self.var.get()))
        Button.pack()

class set_tab2():
    def __init__(self, tab):
        self.var = tk.StringVar()
        self.tab = tab
        self.set_dropdown()
        self.set_button()

    def set_dropdown(self):
        frame = tk.Frame(self.tab)
        frame.pack(pady=(10, 20))

        label = ttk.Label(frame, text=u'ドロップダウン', width=10)
        label.pack(side='left', anchor=tk.W, padx=(0, 10))

        combo = ttk.Combobox(frame, width=20, state='readonly')
        combo["values"] = ['ちょえーーーー!!!', 'くわっ!!!', 'はーーーーーっ!!!', 'ふひふふっひいいい!!!']
        combo.current(0)
        combo.pack(side='left', anchor=tk.W)

        self.var = combo

    def set_button(self):
        Button = tk.Button(self.tab, text=u'表示', width=20, height=3, command=lambda:show(self.var.get()))
        Button.pack()


class set_tab3():
    def __init__(self, tab):
        self.var = tk.StringVar()
        self.tab = tab
        self.set_radio()
        self.set_button()

    def set_radio(self):
        frame = tk.Frame(self.tab)
        frame.pack(pady=(10, 20))

        label = ttk.Label(frame, text=u'ラジオボタン', width=10)
        label.pack(side='left', anchor=tk.W, padx=(0, 10))

        radio1 = ttk.Radiobutton(frame, text=u"1", variable=self.var, value='キタ━─━─━ヾ(o✪‿✪o)シ━─━─━ッ♪')
        radio1.pack(side='left', padx=(0, 10))

        radio2 = ttk.Radiobutton(frame, text=u"2", variable=self.var, value='( `)3′)▃▃▃▅▆▇▉ブブブーブォォオ')
        radio2.pack(side='left')

    def set_button(self):
        Button = tk.Button(self.tab, text=u'表示', width=20, height=3, command=lambda:show(self.var.get()))
        Button.pack()


if __name__ == '__main__':
    root = tk.Tk()
    root.title(u'サンプル')
    root.geometry('400x250')
    root.resizable(False, False)

    # ノートブック
    nb = ttk.Notebook(width=100, height=300)

    # タブ作成
    tab1 = ttk.Frame(nb)
    tab2 = ttk.Frame(nb)
    tab3 = ttk.Frame(nb)
    nb.add(tab1, text='エントリー', padding=3)
    nb.add(tab2, text='ドロップダウン', padding=3)
    nb.add(tab3, text='ラジオボタン', padding=3)
    nb.pack(expand=1, fill='both')

    # タブ配置
    set_tab1(tab1)
    set_tab2(tab2)
    set_tab3(tab3)

    root.mainloop()


以下のサイトがめちゃわかりやすい
Introduction · tkinter