ほそぼそとやっていくブログ

機械系高専から東大に編入した学生のブログです.時空を旅しています

Python OpenCV AKAZEで文字検出したい話

最近はGoogle Cloud Vision APIとかいう便利なツールのおかげで個人の環境でOCRする必要はないのかなと思いつつ,AKAZEで文字認識に挑戦してみました.

今回はこちらのサンプル画像から

f:id:m12watanabe1a:20180928192332j:plain:w300
サンプル画像

こちらの文字を見つけたいと思います.

f:id:m12watanabe1a:20180928192048j:plain
文字画像

なんでこんなことをするかというと,たまにこんな風に線が被ってる画像があってCloud Vision APIではうまく文字を検出できないからです.

f:id:m12watanabe1a:20181012203348j:plain:w300
あかんヤツ

なので自分でこれを特定の文字であると認識できるようにテンプレートマッチングをしてみようかなと思い,AKAZEの利用に至りました.

なんも考えずにとりあえずやってみる

とりあえずテキトーにコードを書いてみた結果はこちら.左側に文字画像,右側に間取り図画像が表示されていて,対応する特徴量は線で結ばれます.

f:id:m12watanabe1a:20180928195515j:plain
失敗

全くマッチングされていませんね. よくみてみると文字画像の方から特徴量が検出されていません.

ちなみにその時のコードはこんな感じです.

import cv2
import numpy as np

# 画像読み込み先のパス,結果保存用のパスの設定
template_path = "assets/img/template/"
template_filename = "you_shitsu.jpg"

sample_path = "assets/img/sample/"
sample_filename = "floor_plan_001.jpg"

result_path = "assets/img/result_AKAZE/"
result_name = "no_change.jpg"

akaze = cv2.AKAZE_create() 

# 文字画像を読み込んで特徴量計算
template_img = cv2.imread(template_path + template_filename, 0)
kp_temp, des_temp = akaze.detectAndCompute(template_img, None)

# 間取り図を読み込んで特徴量計算
sample_img = cv2.imread(sample_path + sample_filename, 0)
kp_samp, des_samp = akaze.detectAndCompute(sample_img, None)

# 特徴量マッチング実行
bf = cv2.BFMatcher()
matches = bf.knnMatch(des_temp, des_samp, k=2)

# マッチング精度が高いもののみ抽出
ratio = 0.5
good = []
for m, n in matches:
    if m.distance < ratio * n.distance:
        good.append([m])

# マッチング結果を描画して保存
cv2.namedWindow("Result", cv2.WINDOW_KEEPRATIO | cv2.WINDOW_NORMAL)
result_img = cv2.drawMatchesKnn(template_img, kp_temp, sample_img, kp_samp, good, None, flags=0)
cv2.imshow("Result", result_img)
cv2.imwrite(result_path + result_name, result_img)
cv2.waitKey(0) 

何も考えずにライブラリに頼るのはよくないですね.しっかり文献を読んでいきましょう.

AKAZEとは?

AKAZEはSIFTをベースに開発され,ロバスト性や処理速度を向上させた特徴量抽出アルゴリズムで,検出対象のオブジェクトのスケールの異なる画像回転している画像同士からのマッチングが可能です.

今回はAKAZEの元になっているSIFTの論文を読みました.

ガウシアンフィルタで特徴量を検出している

SIFTではガウシアンフィルタを利用して特徴量を検出しています.順を追ってみていきましょう.

ガウシアンフィルタとは?

ガウシアンフィルタとは,ガウス関数 G(x, y, \sigma)=\frac{1}{2 \pi \sigma ^{2}} e^{\frac{-(x^{2}+y^{2})}{2\sigma^{2}}}によって生成されるカーネル Kの畳み込み演算をする処理です.標準偏差 \sigma = 1.3 3\times 3カーネル Kは以下のようになります.


K = \frac{1}{16}\left(
    \begin{array}{ccc}
      1 & 2 & 1 \\
      2 & 4 & 2 \\
      1 & 2 & 1
    \end{array}
  \right)

ざっくりいうと周囲のピクセルガウス関数に基づいて重み付けして足し合わせる処理を行なっています.実際にこのフィルタをLennaさんに適応してみるとこんな感じになります.

f:id:m12watanabe1a:20181014182300p:plainf:id:m12watanabe1a:20181014182303p:plain
左:デフォ 右:σ=3のガウシアンフィルタ適応後

ガウシアンフィルタをかけると画像の高周波成分が綺麗に取り除かれる,つまり急に色が変わっているところがボヤァっとなるんですね.

ガウシアンフィルタの差分を取ってみる

ガウシアンフィルタは先ほど説明した通り,画像の高周波成分を取り除く処理です.つまりガウシアンフィルタによって,色が急に変化しているピクセルは大きく値が変動します.ここで異なる標準偏差 \sigmaのガウシアンフィルタどうしの差分を取ってみると,

f:id:m12watanabe1a:20181014182303p:plain -f:id:m12watanabe1a:20181014183310p:plain =f:id:m12watanabe1a:20181014182307p:plain
左:σ=3 中:σ=3.1 右:それらの差分
こんな感じに輪郭が浮かび上がっています.ここで \sigma標準偏差ですので,その値が大きいほどぼかしが効くようになります.

SIFTの特徴量検出

SIFTの特徴量検出ではこのように逐次的にガウシアンフィルタの差分を取って,その差分のもっとも大きかった点(極値)を特徴点として採用しています.

f:id:m12watanabe1a:20181014184242j:plain
DoG -D. Lowe氏の論文より

極値は8つの周囲のピクセル \sigmaの値を変化させた上下の9つのピクセルを比較して決定されます.

f:id:m12watanabe1a:20181014191622j:plain
極値の決定 -D. Lowe氏の論文より

ちなみにAKAZEでは

このフィルタ部分が少し改良されています.ガウシアンフィルタだと,フィルタをかける時に画像の特徴量としての情報をもつ輪郭をもぼかしてしまうので,これら をぼかさないようにする非線形フィルタを使用しているそうです.

たくさん特徴点を見つける.

ここで,特徴点を決定するに当たって,SIFTでは周囲の8ピクセルとの値を比較をして極値を求めているので,検出される特徴点の数が入力される画像の拡大縮小に影響されることがわかります.論文にも

Therefore, to make full use of the input, the image can be expanded to create more sample points than were present in the original.

とあるように画像をあらかじめ拡大してから入力すると検出できる特徴点が増やせるようです.

実際に使っているライブラリはAKAZEですが,SIFTのこの考え方自体はAKAZE実際にも適用できるなと考えて,以下のように線形補間をして画像のサイズを2倍にして特徴量検出を行なってみると

expand_template=2
template_img = cv2.imread(template_path + template_filename, 0)
template_img = cv2.resize(template_img, None, interpolation=cv2.INTER_LINEAR, fx = expand_template, fy = expand_template) #拡大!!
kp_temp, des_temp = akaze.detectAndCompute(template_img, None)

ご覧のように特徴量が前よりも多く見つかりました.(特徴点はカラフルな丸で囲まれます)

f:id:m12watanabe1a:20180928192048j:plainf:id:m12watanabe1a:20181014194136j:plainf:id:m12watanabe1a:20181014194511j:plain
左:1倍 中:2倍 右:5倍

ただし,「じゃあめちゃめちゃ拡大すればいいじゃん!」というわけでもなくて,ある程度からはただ計算コストが膨れるだけなので出力をみながらいい具合に拡大しましょう.

しかし,拡大された画像をよく見てみると

f:id:m12watanabe1a:20181014194136j:plain
拡大された画像

特徴点が画像中央に集中していて文字の端から特徴量が検出されていません. おそらく平滑化フィルタがうまくかかっていないのが原因だと思われます. 余白を付け足すと,

whitespace = 20
template_temp = cv2.imread(template_path + template_filename, 0)
height, width = template_temp.shape[:2]
template_img=np.ones((height+whitespace*2, width+whitespace*2),np.uint8)*255 #余白を付け足す処理!
template_img[whitespace:whitespace + height, whitespace:whitespace+width] = template_temp #余白を付け足す処理!

template_img = cv2.resize(template_img, None, fx = expand_template, fy = expand_template) #拡大!!
kp_temp, des_temp = akaze.detectAndCompute(template_img, None)
f:id:m12watanabe1a:20181014195406j:plain
余白付き

いい感じですね!

これで再びマッチングをしてみましょう!

再挑戦

結果はこちら.

f:id:m12watanabe1a:20181014200207j:plain:w400
結果

いい感じですね!

コードはこんな感じです.

import cv2
import numpy as np

# 画像読み込み先のパス,結果保存用のパスの設定
template_path = "assets/img/template/"
template_filename = "you_shitsu.jpg"

sample_path = "assets/img/sample/"
sample_filename = "floor_plan_001.jpg"

result_path = "assets/img/result_AKAZE/"
result_name = "perfect.jpg"

akaze = cv2.AKAZE_create() 

# 文字画像を読み込んで特徴量計算
expand_template=2
whitespace = 20
template_temp = cv2.imread(template_path + template_filename, 0)
height, width = template_temp.shape[:2]
template_img=np.ones((height+whitespace*2, width+whitespace*2),np.uint8)*255
template_img[whitespace:whitespace + height, whitespace:whitespace+width] = template_temp
template_img = cv2.resize(template_img, None, fx = expand_template, fy = expand_template)
kp_temp, des_temp = akaze.detectAndCompute(template_img, None)

# 間取り図を読み込んで特徴量計算
expand_sample = 2
sample_img = cv2.imread(sample_path + sample_filename, 0)
sample_img = cv2.resize(sample_img, None, fx = expand_sample, fy = expand_sample)
kp_samp, des_samp = akaze.detectAndCompute(sample_img, None)

# 特徴量マッチング実行
bf = cv2.BFMatcher()
matches = bf.knnMatch(des_temp, des_samp, k=2)

# マッチング精度が高いもののみ抽出
ratio = 0.5
good = []
for m, n in matches:
    if m.distance < ratio * n.distance:
        good.append([m])

# マッチング結果を描画して保存
cv2.namedWindow("Result", cv2.WINDOW_KEEPRATIO | cv2.WINDOW_NORMAL)
result_img = cv2.drawMatchesKnn(template_img, kp_temp, sample_img, kp_samp, good, None, flags=0)
cv2.imshow("Result", result_img)
# cv2.imwrite(result_path + result_name, result_img)
cv2.waitKey(0) 

特徴量を増やす為に一応間取り図の画像も少し拡大しました.

今後

しかし,色々比較してみると,特徴量検出によるマッチングではフォントの影響を受けてしまうようですね.間取り図中の右下の部屋が洋室なのに全く検出してないです.

f:id:m12watanabe1a:20181014200952j:plain
フォント違う間取り図から検出されないの図

なので今後はR-CNNで文字検出しようかなと検討中です.

あとがき

なんか訂正とかおかしいところとかあったら教えてもらえると嬉しいです.