おもちゃラボ

Unityで遊びを作ってます

OpenCV超入門 サンプルプログラム集

OpenCVはバージョンが1.0のころに使っていましたが、久しぶりに使おうとするとバージョンが3.0になって、ガラッと様子が変わってしまっていることに気がついた。

  • IplImageはどこへいった?
  • cvのプリヘッダがなくなった!?
  • Matってメモリの割り当てはどうするんだ?
  • ピクセルへのアクセスってどうやるの?

などなど、疑問が山のようにでてきました。そんな疑問に答えるべく、OpenCV3の使い方とサンプルをまとめてみたので、どうぞ活用してやって下さい。ちなみに、サンプル写真にはレナ嬢ではなく京都の風景を使用しています(笑)

画像を表示する

f:id:nn_hokuson:20170529235347j:plain
OpenCV3を使って、画像を表示するだけのプログラムです。

void main()
{
    Mat img = imread("img.png");
    imshow("image",img);
    waitKey(0);
}

OpenCV3からは画像を格納する型としてIplImageではなくMatを使います。画像の読み込みにはimread関数を使います。引数は次のようになります。

■imread(filename, flag)

引数 意味
filename 読み込みファイル名
flag >0: 3チャンネルカラー
=0: グレースケール
<0 読み込み画像に従う


画像の表示はimshow関数で、第1引数にウインドウ名、第2引数に表示したいMatを指定します。

■imshow(name, img)

引数 意味
name ウインドウの名前
img 表示する画像

グレースケール変換

f:id:nn_hokuson:20170531195046j:plain:w300 f:id:nn_hokuson:20170531195217j:plain:w300
画像をグレースケールに変換するプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat dst;
    cvtColor(img, dst, COLOR_RGB2GRAY);
    imshow("image",dst);
    waitKey(0);
}

グレースケール変換にはcvtColor関数を使います。第3引数に指定するコードで変換前と変換後の画像フォーマットを指定します。ここではRGBからグレースケールに変換するため、COLOR_RGB2GRAYを指定しています。第3引数を変更することでHSVなどのフォーマットに変換することも出来ます。

■cvtColor(src, dat, code)

引数 意味
src 入力画像
dst 出力画像
code RGB→GRAY COLOR_RGB2GRAY
RGB→HSV COLOR_RGB2HSV
・・・など

ここで、出力画像のメモリ割り当てを行っていないことに注目して下さい。IplImageとは異なり、Matでは自動的にメモリの割り当て&開放を行ってくれるようになっています。

2値化

f:id:nn_hokuson:20170530065445j:plain f:id:nn_hokuson:20170530065451j:plain
画像を白黒の2値に変換するプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat gray, dst;
    cvtColor(img, gray, COLOR_RGB2GRAY);
    threshold(gray, dst, 80, 200, 0);
    imshow("image",dst);
    waitKey(0);
}

画像を2値化するには、まずグレースケールに変換し、その画像に対してしきい値以上なら該当するピクセルを白、しきい値以下なら黒にする処理を行います。この処理はthreshold関数で行っています。

■threshold(src, dat, thresh, max, type)

引数 意味
src 入力画像
dst 出力画像
thresh 閾値
max 明度の最大値
type THRESH_BINARY
THRESH_BINARY_INV
・・・など

リサイズ

f:id:nn_hokuson:20170531195512j:plain:w300  f:id:nn_hokuson:20170531195512j:plain:w150
画像のサイズを変更するプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat dst;
    resize(img, dst, Size(150, 100));
    imshow("image",dst);
    waitKey(0);
}

画像のサイズを変更するにはresize関数の第3引数に、出力画像のサイズを指定します。あとはこの関数がメモリ割り当てなども自動的に行なってくれます。

■resize(src, dat, size)

引数 意味
src 入力画像
dst 出力画像
size 出力画像のサイズ

トリミング

f:id:nn_hokuson:20170531201225j:plain:w300   f:id:nn_hokuson:20170531201234j:plain
画像の一部をトリミングするためのプログラムです。

void main()
{
    Mat img = imread("img.jpg");
    Mat dst(img, Rect(50, 100, 200, 200));;
    imshow("result", dst);
    waitKey(0);
}

Matクラスのコンストラクタに元画像の変数と、トリミングする長方形をRect型で指定します。

図形を描画する

f:id:nn_hokuson:20170530234921p:plain:w300
円や長方形、直線などを描画するプログラムです。

void main()
{
    Mat img(512,512, CV_8UC3, Scalar(100, 100, 100));
    circle(img, Point(256, 256), 100, Scalar(200, 100, 100), 5);
    line(img, Point(0, 0), Point(512, 512), Scalar(0, 200, 0), 3, 4);
    rectangle(img, Point(100, 100), Point(300, 200), Scalar(0, 0, 200), 2, 4);
    imshow("image",img);
    waitKey(0);
}

図形を描画するためのキャンバスを最初に作ります。ここでは大きさを指定してMat型の変数を作成しています。
円を描くにはcircle関数、長方形はrectangle関数、線はline関数を使います。それぞれの関数の引数は次のようになります。図形を塗りつぶすにはticknessの値をマイナスにします。

■circle(img, center, radius, coor, tickness)

引数 意味
img 描画する画像
center 円の中心座標
radius 円の半径
color 円の色
tickness >=0: 線の太さ
<0: 塗りつぶし

■rectangle(img, pt1, pt2, coor, tickness)

引数 意味
img 描画する画像
pt1 左上座標
pt2 右下座標
color 円の色
tickness >=0: 線の太さ
<0: 塗りつぶし

■line(img, pt1, pt2, coor, tickness)

引数 意味
img 描画する画像
pt1 開始座標
pt2 終点座標
color 円の色
tickness >=0: 線の太さ
<0: 塗りつぶし

画像の保存

f:id:nn_hokuson:20170531201717j:plain:w300
画像を保存するプログラムです。

void main()
{
  Mat img = imread("img.png");
  imwrite("img.jpg", img);
}

画像を保存するにはimwriteを使用します。第1引数には保存ファイル名を指定します。拡張子によって自動的に保存形式が選択されます。

■imwrite(filename, img)

引数 意味
filename 保存するファイル名
img 保存する画像

画像の上下左右反転

f:id:nn_hokuson:20170529232958j:plain f:id:nn_hokuson:20170529233006j:plain
画像を上下反転・左右反転(フリップ)するプログラムです。

void main()
{
   Mat img = imread("img.png");
    Mat dst;
    flip(img, dst,0);
    imshow("image",dst);
    waitKey(0);
}

画像を反転するにはflip関数を使います。filp関数の第3引数に反転方向を指定します。反転方向の指定は次のようになります。

■flip(src, dat, code)

引数 意味
src 入力画像
dst 出力画像
code 反転方向の指定
> 0: 左右の反転
= 0 上下の反転
< 0 左右上下の反転

ぼかしフィルタ

f:id:nn_hokuson:20170529233430j:plain f:id:nn_hokuson:20170529233437j:plain
ボックスフィルタを使って画像をぼかすプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat dst;
    blur(img, dst, Size(7,7));
    imshow("image",dst);
    waitKey(0);
}

ボックスフィルタを使って画像をぼかすにはblur関数を使います。ボックスフィルタは周辺のピクセルの平均値を現在のピクセルの値にします。どの範囲の平均値をとるかは、第3引数にぼかしフィルタのカーネルサイズで指定します。

■blur(src, dat, ksize)

引数 意味
src 入力画像
dst 出力画像
ksize ボックスフィルタのカーネルサイズを指定します

ガウシアンフィルタ

f:id:nn_hokuson:20170530065030j:plain f:id:nn_hokuson:20170530065053j:plain
ガウシアンフィルタを使って画像をぼかすプログラムです。

void main()
{
     Mat img = imread("img.png");
     Mat dst;
     GaussianBlur(img, dst, Size(7,7), 10, 10);
     imshow("image",dst);
     waitKey(0);
}

ガウシアンフィルタを使って画像をぼかすにはgaussianBlur関数を使います。ボックスフィルタに比べて綺麗にぼけ、またぼけの量を微調整できるのが特徴です。調整項目は次のようになります。

■gaussianBlur(src, dat, ksize, sigma)

引数 意味
src 入力画像
dst 出力画像
ksize ガウシアンフィルタのカーネルサイズを指定します
sigma ガウシアンフィルタのシグマ値を指定します

Sobelフィルタ

f:id:nn_hokuson:20170531202927j:plain:w300 f:id:nn_hokuson:20170531202936j:plain:w300
Sobelフィルタを使って画像から輪郭線を抽出するプログラムです。

void main()
{
    Mat dst, gray;
    Mat img = imread("img.jpg");
    cvtColor(img, gray, COLOR_RGB2GRAY);
    Sobel(gray, dst, -1, 1, 0);
    imshow("image",dst);
    waitKey(0);
}

Sobelフィルタを使うことで画像中からエッジを検出することが出来ます。第4引数と第5引数に横方向と縦方向の微分係数を指定します。上のサンプルでは(1, 0)を指定しているので、縦方向のエッジが検出できます。逆に(0, 1)を指定すると横方向のエッジを検出できます。

全てのエッジを検出するには、縦と横の両方向に処理が必要になります。Sobelフィルタの計算は一次微分なので、ノイズの影響を受けにくく比較的処理速度も速いです。

■sobel(src, dat, depth, dx, dy)

引数 意味
src 入力画像
dst 出力画像
depth 出力画像のビット深度
dx 横方向の微分係数
dy 縦方向の微分係数

Cannyフィルタ

f:id:nn_hokuson:20170531202927j:plain:w300 f:id:nn_hokuson:20170531204417j:plain:w300
Cannyフィルタを使って画像から輪郭線を抽出するプログラムです。

void main()
{
    Mat img = imread("img.jpg");
    Mat dst, gray;
    cvtColor(img, gray, COLOR_RGB2GRAY);
    Canny(gray, dst, 40, 150);
    imshow("image",dst);
    waitKey(0);
}

CannyフィルタはSobelフィルタやLaplacianフィルタと比べて非常に綺麗にエッジを検出することが出来ますが・・・それに比例して処理負荷と処理時間も大きくなります。とにかく綺麗にエッジを抽出したいときはCannyフィルタを使うと良いでしょう。

■ Canny(src, dat, thresold1, thresold2)

引数 意味
src 入力画像
dst 出力画像
thresold1 閾値1
thresold2 閾値2

透視変換

f:id:nn_hokuson:20170531205709j:plain:w300 f:id:nn_hokuson:20170531205722j:plain:w270
画像を透視変換によって変形するプログラムです。

void main()
{
    Mat img = imread("img.png");
    Mat dst;
    Point2f srcPoint[] =
    {
        Point(0,        0),
        Point(0,        img.cols),
        Point(img.rows, img.cols),
        Point(img.rows, 0),
    };
    Point2f dstPoint[] =
    {
        Point(100, 100),
        Point(0,   300),
        Point(400, 450),
        Point(500, 50),
    };

    Mat H = getPerspectiveTransform(srcPoint, dstPoint);
    warpPerspective( img, dst, H, Size(512, 512));
    imshow("image", dst);
    waitKey(0);
}

OpenCVで透視変換を行うためには、まず変換前の4点と変換後の4点の座標を指定します。これらの座標をgetPerspectiveTransform関数に渡すことで、透視変換行列が得られます。この透視変換行列をwarpPerspectiveに渡すことで、画像を変形することができます。

透視変換をよく使う場面としては、QRコードの解析などX軸やY軸に沿って処理を進めたいときに使います。オリジナル画像で処理をすすめると、数学的に大変になるので、一度透視変換で正方形に変形してから、処理をすることでプログラムが簡単になります。

■ getPerspectiveTransform(src, dat)

引数 意味
src 入力座標の配列
dst 出力座標の配列
戻り値 透視変換行列

■ warpPerspective(src, dat, M, size)

引数 意味
src 入力画像
dst 出力画像
M 透視変換行列
size 出力画像のサイズ

ヒストグラムの平滑化

f:id:nn_hokuson:20170530235904j:plain:w300 f:id:nn_hokuson:20170530235917j:plain:w300
グレースケール画像のヒストグラムを均一化するプログラムです。

void main
    Mat img = imread("img.png");
    Mat gray, dst;
    cvtColor(img, gray, COLOR_RGB2GRAY);
    equalizeHist(gray, dst);
    imshow("image",dst);
    waitKey(0);
}

ヒストグラムを均一化することで、とコントラストを向上させたり、画像の全体的な明るさのバランスを改善することが出来ます。変換前のヒストグラム(左)と変換後のヒストグラム(右)は次のようになります。

f:id:nn_hokuson:20170531210247p:plain:w200 f:id:nn_hokuson:20170531210253p:plain:w200

均一化したことで、ヒストグラムが左右に広がって平らになったことがわかると思います。OpneCV3でヒストグラムの均一化を行うためにはequalizeHist関数を使います。

■ equalizeHist(src, dat)

引数 意味
src 入力画像
dst 出力画像

参考図書

今回は次の本を参考にさせていただきました!

【Unity】シーンが遷移したことを検知する

あるシーンから別のシーンへ遷移するタイミングで何か処理を実行したい場合、シーンが遷移したことを検知する必要があります。Unity5.4からはシーン遷移にSceneManagerを使います。そして、このSceneMangerには「activeSceneChanged」「sceneLoaded」「sceneUnloaded」という3つのデリゲートが用意されています。

デリゲートとは特定のイベントが起こった場合に呼び出すメソッドを指定できる仕組みです。Unityではオブジェクト同士が衝突したときにはOnColliderEnterメソッドなどが自動的に呼ばれますが、このように既に決まったメソッドを呼び出すのではなく、プログラマが自由に呼ばれるメソッドを自由に決められる仕組みがデリゲートになります。

シーン遷移を検知するデリゲートと、登録したメソッドの呼ばれるタイミングは次のようになります。

デリゲート タイミング
activeSceneChanged アクティブなシーンが変更されたとき
sceneLoaded シーンが読み込まれたとき
sceneUnloaded シーンが破棄されたとき


これらのデリゲートの登録方法と、デリゲートの書き方は次のようになります。

ng System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.SceneManagement;

public class Test : MonoBehaviour {

    void Start()
    {
        SceneManager.activeSceneChanged += OnActiveSceneChanged;
        SceneManager.sceneLoaded              += OnSceneLoaded;
        SceneManager.sceneUnloaded          += OnSceneUnloaded;    
    }

    void OnActiveSceneChanged( Scene prevScene, Scene nextScene )
    {
        Debug.Log ( prevScene.name + "->"  + nextScene.name );
    }

    void OnSceneLoaded( Scene scene, LoadSceneMode mode )
    {
        Debug.Log ( scene.name + " scene loaded");
    }

    void OnSceneUnloaded( Scene scene )
    {
        Debug.Log ( scene.name + " scene unloaded");
    }
}

Startメソッドの中で デリゲートに呼び出すメソッドを登録しています。Startメソッドに続く3つのメソッドがデリゲートになります。デリゲートの名前は自由に決めてOKですが、戻り値と引数の型は決まっているので注意して下さい。

【Unity】落下地点の座標から、放物線の方程式を求める

落下地点が決まっていて、そこに向かって弾をとばしたり、ミサイルを撃ち込みたいことがあります。落下地点から放物線の方程式を求めるのって、結構難しいんですよね・・・。

ということで、ここでは、打ち出し角度と落下地点の座標を指定することで放物線の係数をもとめる方法を紹介します。

f:id:nn_hokuson:20170526213726p:plain:w600

これにより、指定した落下地点に確実に落下するミサイルなどを作ることが出来ますよ!記事の内容は次のとおりです。

放物線の求め方

放物線を求めるのに必要な情報として次の2つを与えます。

  • 落下地点の座標
  • 射出角度

ただし、打ち出す場所は原点とします。原点以外から撃つ場合は、オフセットを足してあげればOKです。この情報をもとに図を書くとこんな感じになります。

f:id:nn_hokuson:20170526214134p:plain:w500

求める放物線はf(x)=ax^2+bx+cと書けます。コレに上の条件をあたえると・・・

f(0)=0より、c = 0
f(target.x)=target.yより、target.y = a*target.x^2+b*target.x
f'(0)=tan(deg)より、b = tan(deg)

この連立方程式を解くと

a = (target.y - b * target.x) / (target.x * target.x)
b = tan(deg)

と、比較的簡単に放物線の方程式を求めることが出来ます。

放物線を求めるプログラム

落下地点の座標から放物線をもとめるスクリプトは次のようになります(ここでは、実際に放物線の方程式を求めているボールのスクリプトを示しています)

public class BallController : MonoBehaviour {

    public GameObject block;
    Vector3 offset;
    Vector3 target;
    float deg;
 
    IEnumerator ThrowBall()
    {
        float b = Mathf.Tan (deg * Mathf.Deg2Rad);
        float a = (target.y - b * target.x) / (target.x * target.x);
      
        for (float x = 0; x <= this.target.x; x+= 0.3f)
        {
            float y = a * x * x + b * x;
            transform.position = new Vector3 (x, y, 0) + offset;
            yield return null;
        }
    }

    public void SetTarget(Vector3 target, float deg)
    {
        this.offset = transform.position;
        this.target = target - this.offset;
        this.deg = deg;

        StartCoroutine ("ThrowBall");
    }

    void Start()
    {
        // ブロックに向かって60度の角度で射出
        SetTarget ( block.transform.position, 60 );
    }
}

SetTargetメソッドで、ボールの落下地点の座標(target)と、ボールの射出角度(deg)を指定しています。この例では、落下地点はブロックの位置、射出角度は60度を指定しています。射出地点を原点にするためoffsetを保存してから、放物線の係数を計算しています。ThrowBallコルーチンの中で、現在のx座標と放物線の方程式からy座標を求めています。最後にoffsetを戻してボールの位置に反映しています。

実行結果

クリックした点にボールが飛んで行くように修正したプログラムの実行結果がこちらです。

f:id:nn_hokuson:20170526215539g:plain

ここでは射出角度を、射出座標と落下座標のなす角度から求めています。