2017年10月22日日曜日

OpenCV で星座検出(1) - 星座検出ソフトstardust

どうもwebアプリ制作をやる気が起きないので(多分飽きた)、また新しいことを始めました。
近々学校の方で OpenCV を触るかもしれないし、画像処理でやってみたいこともあったので、練習しておくことにしました。
windows10 上で python3.6 を使って OpenCV (ver 3.3)を動かします。
星空の写真をよく撮っているというのもあり、手元の写真から星座を検出してみたいと思います。



1.準備

まずは OpenCV を動かせる環境を作らないといけませんが、調べたり人に聞いたり人にやってもらったりしてなんとかなりました(自分ではほとんどなにもやっていない)。
なので早速プログラムを書く準備をします。「python openCV (やりたいこと)」で検索すればだいたい出てくるので触りながら勉強しました。
使う画像も準備します。星の写真をよく撮るとはいったもののそんなに数はないし、この星座を撮ろうと決めて撮ったりもしていないためこれが難航しました。天の川を撮った写真にいて座が映り込んでいる写真が多くあったため、まずはこのいて座の中の南斗六星を検出することにします。
形も特徴的だし数も六個とちょうどいいのではないかと思います。
映り込むいて座をなぞる(下の方は星が映ってないのでへにょへにょ線です)
南斗六星はいて座中央辺りにあります。一発変換できないのが玉に瑕

2.星を検出する

星座を検出する前に、まずは画像のどの部分が星なのか検出しないといけません。僕の場合手持ち画像に明かりと星が写っているものはなかったので、これは明るさに閾値を設けることでなんとかなると考えました。
しかし、星も点ではなく円のような形で写っていたり、30秒も露光していれば流れて曲線になっているため、これを座標として入力するには一工夫必要です。色々調べた結果、今回は「明るさから星の輪郭検出→輪郭の重心を星の座標とする」という方針でいくことにしました(楽そうなので)。
import cv2
import numpy as np

#画像を読み込む
img = cv2.imread("test.jpg")
#グレースケール画像にする
img_gray = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
#明るさに閾値を設ける(ここでは適当に100)
ret, new = cv2.threshold(img_gray, 100, 255, cv2.THRESH_BINARY)
#画像は黒背景に白点になっているため、白点の輪郭を検出
det_img, contours, hierarchy = cv2.findContours(new, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE)
#各輪郭について、重心を求めて配列に入れる
stars = []
for cnt in contours:
    M = cv2.moments(cnt)
    if M['m00'] != 0:
        cx = int(M['m10']/M['m00'])
        cy = int(M['m01']/M['m00'])
        stars.append(np.array([[cx,cy]], dtype='int32'))
    else:
        stars.append(np.array([cnt[0][0]], dtype='int32'))

return stars

なんやこのコードはと思われる部分もあるかもしれませんが僕は頑張っています。
輪郭の重心を求める部分ですが、今回、入力画像をある程度縮小してから輪郭をとっていて、輪郭がつぶれたせいか、重心を求めるための要素が0であることがあり、ゼロ除算エラーが出ました。それを避けるためめんどくさくなっています。openCV ではこのようにモーメントを計算し、その要素からいろいろと計算できるみたいですね。
実際のコードでは星の数を調整するためスレッショルドを変えるループがあり、その中にこれが入る感じです。
星を検出した後輪郭を描画したようす

3. 星座を検出する

星の座標が求まったので、あとは計算でなんとかできそうな気がしてきました。
とはいえ、どのように星座を探そうか考えると少し悩んでしまいます。僕は星座を探すのがかなり苦手なのでなおさらです。
うまいやり方などは思いつかず、コンピュータは人間と比べると大量の計算を素早くするのが得意なので、総当たりさせようと思いました。

まず南斗六星のひしゃくの先っちょの部分を基準点ζ1とし、先ほど計算した星の座標ひとつひとつについて、「これはζか?」と確認していきます。
ではどうやってζか否かを判定しましょう?これは、ζに対し正しい方向に正しい距離で次の星が存在しているかを確認していけばいいと思います。ζ星から次のτ星2までの距離はわからないので、片っ端から探します。ζからτの距離が定まれば、以降はζ-τ間の距離との比率がわかっているので(というか、画像から調べる)判定していけばよさそうです。

実はこのζ-τの距離がわからない問題が未だ解決できていません。あまり大きい範囲まで探すとわりと簡単に別の星から並びを発見されてしまうし…(もうちょっと探す星座の星を増やせばそういうことはないのかも?)

それはおいといて、この方針でコードを書きました。
import cv2
import numpy as np

def search_near_star(x, y, i, stars):
    """starのうちから、(x, y)にi番目に近い星を返す""" 
    p = np.array([x, y])
    L = np.array([])
    for star in stars:
        L = np.append(L, np.linalg.norm(star-p))
    index = np.array(L)
    index = np.argsort(index)
    return stars[index[i]]

ある座標にi番目に近い座標を取得できる関数です。シンプルながらかなり使いでがあり、これをうまく使ってバシバシ書き進めていきました。

4. ベクトルと内積に関するちょっとした問題

(2018/04/06追記:この内容は別の手法によって置き換えられました OpenCV で星座検出(5)
 
search_near_star を用いて見つけた星が星座の条件に合っているかを確認していきます。
使う条件は方向(角度)と距離でした。
探したい範囲
例えば2番目のτ星からσ星3を探すことを考えます。上図のように、ζ-τベクトルとτ-σベクトルにより角度を求めて、だいたい85°あたりにあって、かつ距離がζ-τベクトルの1.25倍であればよいわけです。
しかし、内積で求められるのは2ベクトルのなす角であるから、上図のようにはいきません。いらないものまで条件に合致してしまいます。

実際に探される範囲
ある程度進んでから、ある画像でやたら丸まった南斗六星が検出されてしまってからこのことに気づきました。
これを避けるためには角度の正負もわからなければいけないんですが、ベクトルの知識がないためそういうのがあるのかもわかりません。あったら教えてください。
とりあえず力技を2手法思いつきました。
  1.  2ベクトルの和を求め、基準ベクトル(ζ-τ)との角度を見る。
  2.  基準点(ζ)との距離を見る。
どちらの手法でも上図のσ星は判別できませんが、その後の3つの星の検出のときじわじわ効いてくるんだと思います(希望)。実装が楽そうな 2. でやってみたら、実際丸まった南斗六星は出なくなりました。このあたりのコードはゴチャゴチャなので割愛します。

5. できぐあい




以上4枚で正しく検出することができました。1秒かからないくらいで発見してくれます。特に最後の画像は上3枚とは違う場所、違う時間で撮影しており、塔が白いのもあり検出が不安でしたがうまくいったので満足しています。手持ちに南斗六星が写っているものがあと1枚ありますが、それがうまくいかないので調整したいです。
課題としては、
  1. 入力画像がどんなサイズでも臨機応変に対応できるようにしたい
  2. 3でも述べたζ-τの距離の範囲を決めたい
  3. 別のレンズで撮影した写真でもうまくいくか?
  4. いて座全体への拡張
  5. 他の星座への拡張
があります。
いかんせん素材画像が少ないですが結構面白いプログラミングだったので続きも作っていってもっと大きなプロジェクトにできたらいいなと思います。



1:ゼータ。ひしゃくの先っちょはアスケラ(いて座ζ星)と呼ばれているらしい。こういうζとかをバイエル符号というらしい。
2:タウ。いて座タウ星はWow!シグナルという宇宙からの未確認信号が送られてきた場所だと考えられているとか。
3:シグマ。太陽の7倍の質量がある2等星。地球より外側の惑星によって見えなくなる星のうち最も明るいらしい。

0 件のコメント:

コメントを投稿