金環日食とOpenCVの○○な関係


先日の金環日食を見るために会社を休んで東京にまで行った東です。
(大阪より東京の方が日食中心線に近いため、より完璧な金環が見られるという目論見です。)

日食グラスの他に、三脚、デジカメ、減光フィルターにレリーズケーブルと、準備万端で当日に臨んだのですが、06:00の時点ではあいにくの空模様、ほぼ一面の雲、雲、雲。
諦めずにじっと空を見上げていると、雲の切れ間から時折太陽が現れました。
そのたびに周りの知らない人と一緒に歓声を上げながら、夢中でシャッターを切りました。

さて。
家に帰って一段落して、大量の写真を前に途方に暮れました。
三脚を固定して撮影したため、写った太陽の位置がバラバラなんです。しかも太陽が小さく写ってる。

太陽を中心にしてトリミングしたいのですが、手で一枚一枚処理するのは面倒ですし、時間がかかりすぎて現実的ではありません。きっちり太陽が中心に来ないとイライラするし。
どうしようか、写真このままで諦めようか。

「画像処理で太陽を円として検出すればいいんじゃないの?」
神の声が聞こえました。早速やってみます。

画像処理と言えばOpenCVです。円の検出も簡単にできます。
今回はUbuntu 12.04 LTSでOpenCV 2.3.1を使用します。apt-getで簡単にインストールできます。

> sudo apt-get install libcv-dev

円を検出してそれを中心にトリミングするプログラムのソースコードはこんな感じ。
コマンドライン引数でファイルを指定すると、トリミングしたファイルを書き出すようになっています。

#include <iostream>
#include <math.h>
#include <opencv2/core/core.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace std;
using namespace cv;

int main(int argc, char* argv[]) {
    if (argc != 2) {
        cerr << "usage: " << argv[0] << " in" << endl;
        return 1;
    }
    // 入出力ファイル名の設定
    // 入力ファイルが XX.jpg ならば、出力ファイルは XX_edited.jpg
    string inFileName = argv[1];
    int rindex = inFileName.rfind(".");
    string outFileName = inFileName.substr(0, rindex);
    outFileName += "_edited";
    outFileName += inFileName.substr(rindex);
    // 入力ファイルの読み込み
    Mat image = imread(inFileName, 1);
    if (!image.data) {
        cerr << "error in inread(" << inFileName << ")" << endl;
        return 1;
    }
    // 対象画像のグレースケール化
    Mat gray;
    cvtColor(image, gray, CV_BGR2GRAY);
    // 検出率向上のための平滑化
    GaussianBlur(gray, gray, Size(9, 9), 2, 2);
    // 円の検出
    vector circles;
    double dp        = 2;
    double minDist   = gray.rows / 4;
    double param1    = 100;
    double param2    = 100;
    HoughCircles(gray, circles, CV_HOUGH_GRADIENT, dp, minDist, param1, param2);
    if (circles.size() == 0) {
        cerr << "failed. " << inFileName << endl;
        return 1;
    }
    // 検出した円を中心に画像を切り出す
    int h = 150;
    int w = 200;
    double x = circles[0][0];
    double y = circles[0][1];
    imwrite(outFileName, image.colRange(x - w / 2, x + w / 2).rowRange(y - h / 2, y + h / 2));

    return 0;
}

39行目のHoughCirclesが処理の中心です。各引数の詳細はこちらが詳しいです。
第7引数のparam2は検出された円を結果に含めるかどうかの閾値です。この値が大きすぎると検出に失敗しますし、小さすぎると複数の円が検出されてしまいます。どのくらいの値を指定すれば良いかは画像によって異なります。

今回のソースコードではparam2に適当な値を指定しましたが、画像中に円(太陽)が存在するのはあらかじめわかっていますし、検出したい円は1つだけです。また、結果を格納するcirclesは最も円らしいものから順に並んでいますので、先頭のものを答えとすることができます。
param2は低い値に固定しておいて、無条件にcirclesの先頭を使うのでも良いと思います。

上記のソースコードを次のようにコンパイル&実行すると、検出した円(太陽)を中心にトリミングしたファイルが01_edited.jpgに出力されます。

> g++ -o trimTheSun trimTheSun.cpp -lopencv_core -lopencv_highgui -lopencv_imgproc
> ./trimTheSun 01.jpg

以下の画像のうち、上段が検出対象の画像、下段が自動でトリミングした画像です。


短くて簡単なソースコードですが、なかなかうまく切り出せました。OpenCV素敵!


This entry was posted in 技術. Bookmark the permalink.