コーナー検出器について
皆さんは、画像を見るとき、イラストを描くとき、隅っこを注意してみていますでしょうか。隅っこと言っても、一番端の4つの隅のことではないですよ。写っているもの、又は描かれているもののうち、角ばっている部分のことです。
天才的な絵師以外は、普段そのようなこと気にも留めていない人がほとんどであることと存じます。しかしながら、世の中には、この「隅っこ」を探求し続け、「隅っこ」に情熱を燃やし続けた人たちがいます。そのうちの一人こそ、かの有名なS.T.コーナーさんです。(うそです。)
前置きはともかく、いくつかそのような物体の角を見つけ出す「コーナー検出器」があることを知りましたので、今回はそのうちの一つ、「セグメントテスト」を自己流で実装してみました。他にも、「コーナー検出器」には「ヘシアンコーナー検出器」、「モラベックコーナー検出器」、「ハリスコーナー検出器」など、様々な種類があるようなので、気になる方は是非調べてみてください!!
セグメントテスト
セグメントテストについて簡単に触れると、ある画素について、周辺の画素との輝度を比較して、大きく異なる部分とあまり差がない部分に分けます。そして、周辺画素がすべてあまり差がない部分なら、その部分はほぼ一色に塗りつぶされた場所になりますし、差がある部分とない部分が同じくらいの割合なら、その部分はエッジ(辺)になります。大きく異なる部分があまり差がない部分よりも多くなる、又はその逆の場合は、その部分はコーナー(角)ということになります。
これを手順化すると、大体以下のようになります。
注目画素をp, 周辺の画素を調べる円の半径をr, どのくらい異なるかのしきい値をtとする。
1.pよりr離れていて真上、真下の位置にある画素の画素値がpとt以上差異があるか調べる。どちらもt未満であれば、pはコーナーではないとして棄却する。
2.1でpが棄却されなかった場合、pよりr離れていて真横の関係にある左右二つの画素についても同様に調べる。「これまで調べた4つの画素のうち、3つ以上がpよりt以上明るい、又は暗い」という条件を満たさない場合は、pはコーナーではないとして棄却する。
3.2でもpが棄却されなかった場合、周辺画素すべてについて画素値の関係を調べる。
今回はこれを、実装の手間や処理時間を考えて(「実装できなかっただけじゃないの?」って声が聞こえてきた気がしますが、空耳か幻聴でしょう。)、もっと簡潔にしたやり方でPythonで実装してみました。
実行環境
・Windows10
・GoogleColaboratory(ランタイムタイプ:CPU)
・Python3
実装例
#必要なライブラリのimport
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
from PIL import Image
from tqdm import tqdm # 進捗状況可視化に使う
まずはいつも通り使うライブラリをインポートします。今回は画像を表示したり編集するくらいしかしないので、実質PIL, numpy, matplotlibくらいしか使いません。tqdmは解像度の高い画像を処理するときなど、進捗状況を見たかったので使いました。
from google.colab import drive
drive.mount('/content/drive')
まぁこれはGoogleColaboratoryを使いたい人がやってください(蹴
# image = Image.open("/content/drive/MyDrive/MyWorks/img/カラフルランターン_ペインティング塗り2.png")
# image = Image.open("/content/drive/MyDrive/MyWorks/img/アルピノニンフィア12.png")
# image = Image.open("/content/drive/MyDrive/MyWorks/img/kyuukon.png")
# image = Image.open("/content/drive/MyDrive/MyWorks/img/kuwa.jpg")
# image = Image.open("/content/drive/MyDrive/MyWorks/img/yosshi3.jpg")
# image = Image.open("/content/drive/MyDrive/MyWorks/img/ピカチュウのアイアンテール_リニューアル_色変更17.png")
# image = Image.open("/content/drive/MyDrive/MyWorks/img/トゲキッス アンミカ風 色変更2.png")
# image = Image.open("/content/drive/MyDrive/MyWorks/img/pizza.png")
# image = Image.open("/content/drive/MyDrive/MyWorks/img/snowman.png")
image = Image.open("/content/drive/MyDrive/MyWorks/img/usagi.png")
image_array = np.array(image)
print(image_array.shape)
image
なんか色々読み込んでますが、要するに好きな画像を自由に読み込んでくださいってことです。そのあと画素値を編集するためにnumpyのndarray型に変換しています。
image_gray = image.convert("L")
image_gray_array = np.array(image_gray)
print(image_gray_array.shape)
カラー画像の場合は、モノクロに変換しておいてください。今回はコーナーを検出するだけなので、あえてカラーのままやる必要もあまりないかと思われます。
さて、ここからが本番です。コーナー検出を行うための関数、segment_testを作成します。(関数名をキャメルケースにするかスネークケースにするか地味に迷いました。今回あまり関係ないですが。)
def segment_test(image, r, threshold):
"""
モノクロ画像の配列を受け取り、コーナーを検出した形の画像を返す関数
image: モノクロ画像の配列
r: 探索したい円の半径
threshold: どのくらいの差で画素値が大きく異なるとみなすかのしきい値
"""
width = image.shape[0] # 画像の幅を取得
height = image.shape[1] # 画像の高さを取得
image = np.pad(image, r) # チェックする半径分、周囲に余白を追加。(チェックするときに範囲外に出ないようにするため)
for x in tqdm(np.arange(width)): # このループで進捗状況の可視化も行う
for y in np.arange(height):
top = image[x, y-r] # pとr離れている真上の画素
bottom = image[x, y+r] # pとr離れている真下の画素
left = image[x-r, y] # pとr離れている真左の画素
right = image[x+r, y] # pとr離れている真右の画素
check_points = [top, bottom, left, right]
check_points_results = np.array([]) # それぞれの画素がpとt以上異なるかの結果を格納するための配列
# print(check_points)
for check_point in check_points:
# print(f'check_point: {check_point}')
if check_point <= image[x, y] - threshold: # 注目画素値よりしきい値以上暗い場合
check_points_results = np.append(check_points_results, 'd') # 'dark'のdを格納
elif (check_point > image[x, y] - threshold) and (check_point < image[x, y] + threshold): # 注目画素値と同程度の場合
check_points_results = np.append(check_points_results, 's') # 'same'のsを格納
else: # 注目画素値よりしきい値以上明るい場合
check_points_results = np.append(check_points_results, 'b') # 'bright'のbを格納
# print(f'check_points_results: {check_points_results}')
if np.count_nonzero(check_points_results == 'd') >= 3 or np.count_nonzero(check_points_results == 's') >= 3: # 閾値以上暗い又は明るい部分が3つ以上あったら
image[x, y] = 255 # 注目画素値を白くする
else:
image[x, y] = 0 # 注目画素値を黒くする
return image
今回は手順3の「周辺の全ての画素について調べる」という工程はせず、上下左右4つの画素についてのみ調べました。(というか斜めの画素とかどう調べるねん!)
また、チェックするときに注目画素pが端の方だと、そもそも真上に画素ありませんよ~とかなりそうなので、numpyのpadという関数を使って余白を追加しています。(普通DeepLearningの畳み込みとかで使うもんな気がするけど)
出力する画像は、コーナーと判定された画素については白く、逆の場合は黒くなるように画素値を変更しています。普通コーナーを黒くしたいと思うのですが、逆の方が仕上がりがきれいでしたのでこうしてます。また、先程の追加した余白は出力するときもそのままにしています。これも削除とかしていると、計算時間が増えそうなので。
また、内包表記ではなくfor文ばかり使っていますし、もっと処理効率の良い書き方があるかもしれませんが、今回はこれでもそこまで時間かからないのと、分かりやすい方が良いのでこのままとします。
new_image = segment_test(image_gray_array, 3, 30)
plt.imshow(new_image, cmap='binary')
作成した関数を先程読み込んだ画像に適用してみます。この場合、半径3で周辺画素について調べて、画素値が30以上異なるかどうか見ています。
出力例
1.ピッツァ
r = 3, t = 10
元々の輪郭線がそのまま抽出されたような感じになっています。ただ、やはり線と線の交点で角になっているようなところは、気持ち強めに強調されていることが分かります。
r = 3, t = 30
今回は白黒はっきりした元画像だったので、しきい値を変えてもほとんど変化がありませんね。
r = 10, t = 30
解像度の低い小さめの画像だと、半径をあまり大きくしすぎると出力結果が崩れます。
r = 1, t = 30
試しに半径を小さくして見ると、きめ細やかな出力結果になりますが、この画像の場合は元画像の輪郭線を抽出しただけのような出力になります。
2.ゆきダルマン
r = 3, t = 30
元の画像と似たような感じですが、なんとなく角っぽい部分や、円形の右下あたりが黒っぽくなっています。
r = 10, t = 30
より角が強調された仕上がりになりました。今回の場合はこのくらいの半径の方が分かりやすいかもしれないですね。
r = 10, t = 30 ※白黒反転
当初やろうとしていた、白黒を反転したものも出力してみました。見にくいですが、これはこれで特徴がよく出ているのでアリなのかもしれません。ただちょっと、右下あたりに輪郭線が繰り返されてしまっているのがあまりよくないかもしれないです。
3.ウサギン
元画像
どこかの誰かが作ったとされている、ウサギのオリジナルキャラクターです。グッズ販売中と作者本人から聞いております。
r = 3 t = 30
いや怖い(笑)確かに周囲との差が大きい輪郭線周辺が狙い通り白く出ていますね。
ちなみに、今回はセグメントテストによりコーナーを検出したいのでこれはただのコーディングミスなのですが、
elif (check_point > image[x, y] - threshold) and (check_point < image[x, y] + threshold): # 注目画素値と同程度の場合
の部分のandをorにした方が、割とすっきりとしたきれいな画像が出力されます。というかむしろこれで良くないか。
r =3, t = 30
r = 10, t = 30
r = 10, t = 30 ※白黒反転
4.クワガ〇ン
元画像
さあこっから段々と複雑な画像になっていきますよ~。まずはキレイなニジイロクワガタのフリー素材です。私ニジイロクワガタ好きで子供の頃から憧れてたんですよね~ムシキ〇グでも強いのよりもこっちが出た時のほうが嬉しかったです。
r = 3, t = 10
うーん、粗い!画像が小さいのに画面が複雑だからですかね~。。。まあでも一応輪郭線とか角っぽいところは検出できているみたいですね。
r = 3, t = 30
意外にも、半径を大きくすると落ち着きます。木の幹の質感がちゃんと出てるのが面白いですね~。
r = 3, t = 100
しきい値を大きくすると、やりすぎた感がハンパなくなります。しきい値の上げすぎには気を付けましょう。でもこっちの方がむしろ角が分かりやすいか??
ちなみに先程のor条件で出力したものはこちらになります。いや、もうこっち使えよ!!
5.ニンフィア
r = 3, t = 30
この世の終わりみたいな形相をしていますが、コーナーを検出しようという強い意志がうかがえます。
r = 1, t = 10
半径を小さくした方がオワッてますね。でもはっきりと角が白色で示されています。
6.でっていう
元画像
ヨッシーストーリーと毛糸のカービィの世界観を混ぜたような(いや、ヨッシーのウールワールドか。)、なんともほほえましいフリー素材。このクオリティでフリーなのは驚愕!でも「これは無理だろー」臭がプンプンします。
r = 3, t = 10
それが上手くいくんですよー。むしろ今までで一番きれいじゃね?くらいのレベル。毛糸の質感が見事にでているもはや芸術です。めちゃくちゃかっこいい!!案外細かい質感表現の方が得意なんですかね。左上あたりは元画像では白の中でも比較的あかるい領域だったので、グラデーションになっている部分がコーナーとして検出されたのかなー。
r = 10, t = 10
いやはや見事見事。さながらPhotoShopのエンボス効果のよう。ヨッシーとポチどちらの質感も平等に表現できています。左上の領域も健在。
r = 3, t = 30
しきい値を変えると、雰囲気が変わりました。ヨッシーの足の付け根やポチの目、口、首輪がまるごとコーナーとして検出されていますね。背景の領域は少し変わりました。これも同じように白っぽい部分と茶色っぽくなる部分を分けているのでしょうね。それにしても、ちゃんとヨッシーの形を認識してる風なのがウレシイ!
7.piyo
元画像
色んな意味でラスボス感あふれる画像です。さすがにこれはキツイか??
r = 3, t = 30
あーやっぱり厳しいかー。でも一応輪郭とか毛の質感ででる?!?目がトンボみたいになってます。くちばしもちゃんと検出できてますね。卵の接地面がちゃんと角として検出されているのがポイント高いですねぇ~。
r = 1, t = 30
調子に乗って半径を小さくしたら、黒い部分ばかりになってしまいました。でも本当に重要な、目の周りの細かいボサボサやくちばし、タマゴの接地面や足の輪郭が捉えられているので、むしろこっちの方が優秀??!
r = 3, t = 100
しきい値をあげると、なんだか幽霊みたいになってしまいました。足や、タマゴの影、目の中などは色の変化が少ないということなのでしょうね。くちばしの影も黒くなっていて、全体的に陰の部分が黒くなっている印象です。
r = 3, t = 10
やりました!ついにやりましたよ僕。今回はしきい値が低い方が良かったのですね(毛は色の微妙な変化が多いから?)。毛の質感、タマゴの輪郭、足の質感などすべてが表現されていてほぼ完ぺきです。
r = 1, t = 10
半径を小さくすると、今度はデジタルアートのような趣になりました。全体的にスッキリとした印象です。足の質感は一番的確に表していそうです。
r = 3, t = 10 ※白黒反転
白黒反転させてみたものです。いかにもやらかした感が漂っていますが、コーナーを検出したいという当初の目的は一応達成しているように見えます。
r = 3, t = 10
例のor条件にしてしまったバージョンです。いや貫禄あるやん!!魔王降臨してるやん。
という感じで、皆さんも是非色々な画像で試してみてくださいねー・
まとめ
・深層学習使え
・繊細な質感の表現が意外に得意??
・画像なので、セオリー通りや正解の方が必ずしもよい訳ではないという自由さ、柔軟さがオモシロイ!!
・あれか、角が黒い方で良かったのかも
おわりに
本を読んでいて、「案外簡単に実装できるかも??」と思い立ちやってみました。かなり粗削りな部分が多いと思いますが、深層学習まで使わなくても、結構気軽に検出できるもんなんだなと思いました。
少し前に、ライブラリを使わずにnumpyだけで画像を極座標変換するのにもチャレンジしていたのですが、カラー画像を変換する場合、数学的な変形が複雑すぎて私には手に負えなくて断念しました。いつか再チャレンジして成功させたいです。
あと、最近AIによる画像生成がかなり問題視されていますが、これは機械学習でも深層学習でもなく、古典的な画像処理で、しかもフリー素材や自作のイラストを使っているのでセーフってことで!!
それではごきげんよう
参考
・『画像認識』著:原田達也
・numpy padについて
https://qiita.com/kuroitu/items/51f4c867c8a44de739ec
・matplotlibのcmap一覧(真面目にやれ!!)
https://beiznotes.org/matplot-cmap-list/
・numpy形式の画像の保存
https://note.nkmk.me/python-numpy-image-processing/