【C#】OpenCVを使って画像から線画を抽出する

Qiitaの以下投稿でPythonを使用して画像から線画を抽出するという処理を見たのですが、この処理をC#で書き直したらどうなるかどういうコードになるか確認してきたいと思います。

qiita.com

元のコードの確認

まず対象のコードの確認です。

以下Qiitaからの引用となります。

# -*- coding: utf-8 -*-
"""
Created on Fri Jan 27 11:30:12 2017

@author: khsk
"""

import numpy as np
import cv2 as c
import glob
import os

# 8近傍の定義
neiborhood8 = np.array([[1, 1, 1],
                            [1, 1, 1],
                            [1, 1, 1]],
                            np.uint8)

for path in glob.glob('./images/eupho/*'):
    if (os.path.basename(path) == 'Thumbs.db'):
        continue
    img = c.imread(path, 0) # 0なしでカラー
    img_dilate = c.dilate(img, neiborhood8, iterations=1)
    img_diff = c.absdiff(img, img_dilate)
    img_diff_not = c.bitwise_not(img_diff)
    #gray = c.cvtColor(img_diff_not, c.COLOR_RGB2GRAY)

    #at = c.adaptiveThreshold(img_diff_not, 255, \
        c.ADAPTIVE_THRESH_GAUSSIAN_C, c.THRESH_BINARY, 7, 8) # intをいい感じに調整する
    c.imwrite(os.path.dirname(path) + '_clean_senga_color_gray/' + \
        os.path.basename(path), img_diff_not)

c.imshow('test',img)
c.imshow('test2',img_dilate)
c.imshow('test3',img_diff)
c.imshow('test4',img_diff_not)
c.waitKey(10000)

c.destroyAllWindows()

あるフォルダパスを受け取って中身を列挙し、同階層に "XXXX_clean_senga_color_gray"というフォルダに結果を出力を行っています。

C#で書き直してみる

で、次にC#でのコードです。(言い出しておいてなんですが、当方Pythonはある程度読める程度です。

実装&確認環境ですが、以下の通りです。

  • VisualStudio2017 + .NET Framework4.6.2 + C# 7.0
  • Windows10
  • OpenCVShapr3(3.4.1.20180319)をNuGet経由で導入

想像通り、かなり長くなってしまいました。(割と余計なコードも入っていますが…何より中カッコが邪魔ですね。

using OpenCvSharp;
using System;
using System.Collections.Generic;
using System.IO;

namespace LineDrawing
{
    internal class AppMain
    {
        public static void Main(string[] args)
        {
            // # 8近傍の定義
            // neiborhood8 = np.array([[1, 1, 1],
            //                         [1, 1, 1],
            //                         [1, 1, 1]],
            //                         np.uint8)
            var neiborhood8 = new Mat(new Size(3, 3), MatType.CV_8U);
            for (int y = 0; y < 3; y++)
            {
                for (int x = 0; x < 3; x++)
                {
                    neiborhood8.Set<byte>(y, x, 1);
                }
            }

            // ちょっと違うけど
            // for path in glob.glob('./images/eupho/*'):
            //     if (os.path.basename(path) == 'Thumbs.db'):
            //         continue
            foreach (string path in GetFileList(args[0]))
            {
                try
                {
                    // img = c.imread(path, 0) # 0なしでカラー
                    Mat img = Cv2.ImRead(path, ImreadModes.GrayScale);

                    // img_dilate = c.dilate(img, neiborhood8, iterations=1)
                    var dilateMat = new Mat();
                    Cv2.Dilate(img, dilateMat, neiborhood8, null, 1);

                    // img_diff = c.absdiff(img, img_dilate)
                    var diffMat = new Mat();
                    Cv2.Absdiff(img, dilateMat, diffMat);

                    //img_diff_not = c.bitwise_not(img_diff)
                    var diffNotMat = new Mat();
                    Cv2.BitwiseNot(diffMat, diffNotMat);

                    // ちょっと違うけど
                    // c.imwrite(os.path.dirname(path) + \
                    // '_clean_senga_color_gray/' + os.path.basename(path), img_diff_not)
                    string savePath = 
                        Path.Combine(Path.GetDirectoryName(path), "conv_" + Path.GetFileName(path));
                    Cv2.ImWrite(savePath, diffNotMat);
                }
                catch (IOException ex)
                {
                    Console.WriteLine($"[Error] {path}, {ex.Message}");
                }
            }
        }

        // 指定したパスからファイルリストを取得する
        public static IEnumerable<string> GetFileList(string path)
        {
            if (Directory.Exists(path))
            {
                // ディレクトリなら内容をのファイルリストを返す
                foreach (string tempPath in Directory.GetFiles(path, "*.*"))
                {
                    string extenstion = Path.GetExtension(tempPath).ToLower();
                    if (extenstion == ".jpg" || extenstion == ".jpeg" || 
                        extenstion == ".bmp" || extenstion == ".png")
                    {
                        yield return tempPath;
                    }
                }
            }
            else
            {
                yield return path; // ファイルならそのまま1件返す
            }
        }
    }
}

全体的にC#だと、必要な型をあらかじめ宣言してつつ、結果はAPIにOUTパラメータとして(OutputArrayという形で)渡すというスタイルのAPIになっているので使い勝手がかなり違う感じです。

解説ですが、使い方はコマンドラインから以下のようにディレクトリかファイルを指定します。ディレクトリの場合、内容の画像ファイルを列挙して一括で処理を行います。結果は、同じソースと同じ場所に"conv_"と接頭辞のついた画像が出力されます。

> program.exe [$filepath | $directory]

次に、3x3の1バイト配列を"var neiborhood8"~の行で作成しています。(多分これが一番違うと思います。

var neiborhood8 = new Mat(new Size(3, 3), MatType.CV_8U);
for (int y = 0; y < 3; y++)
{
    for (int x = 0; x < 3; x++)
    {
        neiborhood8.Set<byte>(y, x, 1);
    }
}

また、以下のサムネイル以外をフォルダから列挙する処理ですが、

for path in glob.glob('./images/eupho/*'):
    if (os.path.basename(path) == 'Thumbs.db'):
        continue

以下の関数に置き換えています。(これが長くなる原因でしたね/(^o^)\

public static IEnumerable<string> GetFileList(string path)

元記事の膨張回数ですが以下の第4引数を指定することで変更できます。(なんとなく1回でかなりいい感じなのでコードにリテラルとして書いています。

Cv2.Dilate(img, dilateMat, neiborhood8, null, 1/* ← これ*/);

あとは、Qiitaに説明のある通りなので割愛します。

結果

結果ですが、以下の元画像が

f:id:Takachan:20180828001545j:plain

このように変換されました。大体同じですね。

f:id:Takachan:20180828001727j:plain

また、ディレクトリを指定した場合の変換結果は以下の通りになります。

f:id:Takachan:20180828001832p:plain

少し苦手な画像があるみたいですね。輪郭が取りにくんでしょうか。この画像を基に着色するAIに放り込んで遊ぶこともできそうです。