PG日誌

各記事はブラウザの横幅を1410px以上にすると2カラムの見出しが表示されます。なるべく横に広げてみてください。

C#でOlliの手書き風描画のフィルタを再現する

動画と写真を手書き風に加工する「Olli」というアプリがあるのですが、結構きれいに手書き風のフィルタがかけられると話題になっていました。

ねとらぼさんのOlli紹介記事

そこで、C#を使って手書き風のフィルタを再現してみようと思います。

申し訳ないですが、iOS系の端末が無いのでアプリが確認できません。そこで申し訳ないですがサイトにあった画像を使って検証していきたいと思います。

実装結果

元の画像がコレです。

f:id:Takachan:20181108221729j:plain

Olliでフィルターした見本画像がコレ。

f:id:Takachan:20181108221750j:plain

今回実装した結果がコレ。

f:id:Takachan:20181108221803p:plain

★もう一枚の元画像

f:id:Takachan:20181108225259j:plain

Olliでフィルターをかけた結果

f:id:Takachan:20181108225451j:plain

今回実装した結果

f:id:Takachan:20181108225435p:plain

ノイズ除去と線を太くして平準化しないといけない感じでまだまだ調整が必要そうですが概ね考え方の方向性はあってそうですね。アプリはさすがで、風味の味付けがとっても素晴らしいです。

実装環境

実装環境は以下の通り

  • Windows10
  • VisualStudoo2017
  • C# 7.3
  • OpenCvShapr3(3.4.1.20180830)

考え方

処理の流れはこんな感じになります。

  • (1) グレースケール化
  • (2) エッジ検出画像を作成
  • (3) 2値化して塗る部分を取得
  • (4) (2) + (3) を合成して着色

全部OpenCVの関数呼ぶだけなので難しいことはありません。

実装コード

実装コードは以下の通り。

Mat.DataPointer からポインタをとってデータ操作しているので unsafe を使用しています。

public static void Main(string[] args)
{
    //string path = @"D:\Sample.jpg";
    string path = args[0];    // 変換対象のファイルパス
    string outDir = args[1]; // 結果を出力するフォルダパス

    Mat m1 = Cv2.ImRead(path);

    // (1) グレースケール化
    var m2 = new Mat();
    Cv2.CvtColor(m1, m2, ColorConversionCodes.BGR2GRAY);

    // (2) エッジ検出
    var m3 = new Mat();
    Cv2.Canny(m2, m3, 70, 130);

    // (3) 色の反転
    var m4 = new Mat();
    Cv2.BitwiseNot(m3, m4);

    // (4) グレースケール画像を2値化
    var m6 = new Mat();
    Cv2.Threshold(m2, m6, 30, 255, ThresholdTypes.Binary);

    // (3) と (4)を合成して着色
    var m5 = new Mat(new Size(m4.Cols, m4.Rows), MatType.CV_8UC3);
    Convert_And_Marge(m4, m6, m5);

    // 中間画像も含めて全部保存
    Cv2.ImWrite(outDir + @"\result_2.png", m2);
    Cv2.ImWrite(outDir + @"\result_3.png", m3);
    Cv2.ImWrite(outDir + @"\result_4.png", m4);
    Cv2.ImWrite(outDir + @"\result_5.png", m5);
    Cv2.ImWrite(outDir + @"\result_6.png", m6);
}

// エッジ検出結果と2値化した画像を合成して着色するメソッド
public unsafe static void Convert_And_Marge(Mat mat1, Mat mat2, Mat result)
{
    long len = mat1.DataEnd.ToInt64() - mat1.DataStart.ToInt64();

    byte* ba = mat1.DataPointer;
    byte* bb = mat2.DataPointer;
    byte* br = result.DataPointer;

    for (int i = 0; i < len; i++)
    {
        if (ba[i] == 0 || bb[i] == 0)
        {
            byte* _p = br + i * 3; // 線の色
            *_p = 0x39;
            *(_p + 1) = 0x33;
            *(_p + 2) = 0x2E;
        }
        else
        {
            byte* _p = br + i * 3; // 背景色
            *_p = 0xDE;
            *(_p + 1) = 0xEA;
            *(_p + 2) = 0xEC;
        }
    }
}

各工程ごとの画像は以下の通り。

result_1.png:グレースケール化した画像

f:id:Takachan:20181108223302p:plain

result_2.png:エッジ検出した画像

f:id:Takachan:20181108223705p:plain

result_3.png:エッジ検出結果をネガ反転した画像

f:id:Takachan:20181108223729p:plain

result_6.png:(着色用に)2値化した画像

f:id:Takachan:20181108223755p:plain

result_4.png:合成 + 色調変換した結果の画像

f:id:Takachan:20181108223835p:plain

Webカメラに組み込んでみた

上記処理をWebカメラの画像に組み込んでリアルタイムに処理してみました。

どうも環境に応じて閾値を自動調整しないといけないっぽい。

]

作った感想

難しい事はOpenCVが全部やってくれるので、コードはすごくシンプルでした。

むしろ、各関数をC#で正しく呼び出せるかどうかがキモだと思います。

アプリのように手書き感を出すのにさらに一工夫必要だと思います。また、エッジ検出と2値化の閾値次第で結果がかなり変わるのでちゃんとフィルタするなら味付けに研究が必要そうです。

OpenCvSharpのMat.Dataはポインタだったのでunsafeを使用しました。実行速度はさすがに早く利用個所は色々ありそうです。

以前、線画の抽出(C#のOpenCVSharp3を使って画像から線画を抽出する - PG日誌)をしたことがありましたが、そっちはアニメ調の画像にしか効果が無かったのですがこっちはリアルの画像でもそれなりに線画取り出せるみたいです。