画像解析をUnityでやってみよう。6回目です。
前回までで、OpenCVforUnityを使用して、カメラ画像の変換を行いました。
今回は、楽譜読み取りの第一歩、五線譜を読み取ってみたいと思います。

線分を検出する

五線譜を読み取るために、どうするか。
五線譜は線の集まりですので、線分検出がいいかなと思います。
私は線分検出を選択しましたが、もっと他のアプローチもあるかもしれません。でもここは単純に考えます。

HoughLinesPを使う

線分検出を行うためには、HoughLinesP関数を使用します。
OpenCVforUnity HoughLinesP API
OpenCVforUnityのAPIをみてわかる通り、まったくこれだけの情報ではなんなのかよくわかりません。。
大体名前同じなので、どういう関数なのか見るときは、OpenCV側のリファレンスを参照した方が良いです。
OpenCV HoughLinesP関数

OpenCVとOpenCVforUnityで異なるところは、OpenCVforUnityだと第2引数がMatになってます。
すべてMatなのです。OpenCVforUnityちゃん。
ちょっとここがめんどくさいのですが、どのようにデータ入っているのか、いちいち検証しないといけません。。
APIに書いてないし。。そこ書いてよね。。

では実際にコードを書くと、こんな感じです。

Imgproc.HoughLinesP(
            srcMat,            
            lines,
            Imgproc.CV_HOUGH_PROBABILISTIC,
            Mathf.PI / 180,
            threshold,
            minLineLength,
            maxLineGap
        );


第1引数に線分検出対象の画像をMatで指定します。渡す画像は2値画像である必要があります。
(これもコードみただけではわかりません。実際にグレースケールとかカラー画像渡してもエラーなく動きます。。)
関数の引数の詳細は、OpenCVのAPIを見て下さい。
OpenCVforUnityとOpenCVで違うところは、第2引数がMatになっていることくらいです。
正直第3、第4引数はなんなのかよくわかってません。
詳しく知りたい人は、ここなどみるとわかるかも?
僕は見てません(キッパリ)
完全にサンプルからパクリました。
第5から第7引数が実際に線分検出する時に結構影響がありそうなので、これらをパラメータとして外から渡せるような作りにしました。
まあものは試しってことで、いろいろ閾値変えて試してみます。

検出する前に

これは個人的にこうした方が良いという事なのですが、
APIをみてわかるとおり、OpenCVforUnityは正直そのままコードに埋め込むと後からみてわけわからなくなります。
線分もMat形式なので、自分で線分クラスを作成して、Matを渡して線分クラスを作るような作りにした方が後々読みやすくなると思います。
僕はこんな感じで作りました。
ほんとはMatのチャンネルとかみてエラー処理した方がいいんだろうけど。。それは特にしてません。

public class Line
{
    public Vector2 Start { get; }
    public Vector2 End { get; }

    public Line(float startX, float starty, float endX, float endY)
    {
        Start = new Vector2(startX, starty);
        End = new Vector2(endX, endY);
    }

    public Line(Vector2 start, Vector2 end)
    {
        Start = start;
        End = end;
    }

    public static List<Line> FromArray(int[] pointsArray)
    {
        List<Line> lines = new List<Line>();
        for (int i = 0; i < pointsArray.Length; i = i + 4)
        {
            Vector2 start = new Vector2(pointsArray[i + 0], pointsArray[i + 1]);
            Vector2 end = new Vector2(pointsArray[i + 2], pointsArray[i + 3]);            

            lines.Add(new Line(start, end));
        }

        return lines;
    }

    public static List<Line> FromMat(Mat pointsMat)
    {
        if (pointsMat == null || pointsMat.empty())
        {
            return new List<Line>();
        }
        int[] linesArray = new int[pointsMat.cols() * pointsMat.rows() * pointsMat.channels()];        
        pointsMat.get(0, 0, linesArray);
        return FromArray(linesArray);
    }
}

この実装からわかる通り、検出した線分のMatの中には、始点、終点のx座標、y座標の順番に線分情報が格納されています。
なんて雑なツッコミ方なんだ。。
なのでその順番に取り出しています。

Canny変換する

線分検出に渡す画像は、いろいろ参考にしたところ、Canny変換すると良いらしい。
これもOpenCVに用意されているので、実際に試してみます。

変換前 変換後
変換前 Canny変換


輪郭検出みたいなやつかな?輪郭だけ白になってる。。
とりあえずこれで線分を検出してみます。
おそらく白と黒の領域で、白の点をみて線分かどうかを判断するっぽいですね。なるほど。

検出してみた

早速、楽譜から線分検出してみます。
使用する楽譜はきらきら星。 きらきら星 とりあえず、閾値を38,最小線分長10,同一線分とみなす最大距離を100にします。
適当。 検出結果 うわあ。。
線がいっぱい。。

閾値を変えてみる

閾値変えてみます。

150 200 100
検出結果 検出結果 検出結果


150くらいが良さそう??150でいきます。

最小線分長を変えてみる

まずは大きめから.

2000 1000
検出結果 検出結果


全然検出できないので、もっと短いんですね。線分の長さ画面に出しておけばよかた。。
てことで

200 400 800 500 300
検出結果 検出結果 検出結果 検出結果 検出結果

200くらいが一番いいのかな??
あとやってて思ったけど、本だから線が歪む。。
まあこれはもういいや。本から読み取るんじゃなくて、ペラ一枚から読み取る方向性で。。

線分とみなす2点の最大距離を変えてみる

20 100 200 50
検出結果 検出結果 検出結果 検出結果

これは大体50くらいで良さそう。。かな?

検出前の画像をみてみる

しかし線分なんでこんなに多いんだろ。。とおもってCanny画像をみてみると。。
検出結果 あ、五線譜が2重になっている!!!
これだと一本の線に対して、2本線が検出されます。
で、いろいろ調べると、細線化という手法が使えそうです。
文字の検出などに使われる手法のようですが、これを試してみよう。

でも今回はちょっと長くなったのでまた次回。。

まとめ

今回は五線譜を実際に読み取るという処理をやってみました。
やっぱり細かいパラメータ調整などが正直めんどいですが、避けようがないですね。

実装をする際に気をつけた方がいいこと

  • OpenCVforUnityはそのまま使うと使いづらいというか、後からコードみてもわけわからなくなるので、ラップしながら使った方が良い。
  • パラメータ調整は必須の作業になる(はず)ので、パラメータを外だしし、画面からいじれるようにした方が良い(クライアントとの確認にも使えるよ)
  • 何かの検出処理を行う際は、前処理段階の画像を確認できるパラメータを用意した方が良い。検出のパラメータよりも、前処理に問題がある事がある

てことで、また次回。
次は本から楽譜切り取って平にして読み取ります。
歪みとかまで考えたくないし。。