C#で作るイラストから線画抽出するコンソールアプリ

コンピュータ

適応型しきい値による2値処理を、境界がしっかりしたイラストなどに施すと、境界部分が線画のように抽出されます。
2値化された画像ですので、線画としてのディティールは情報量が足りない(ギザギザ)ですが、白と黒ではっきりとしていますので範囲選択用のマスクとして使えると思います。
2値化処理後、細かなドット抜け部分を、モルフォロジー変換の膨張と収縮を使い、潰す様な処理を施しています。

ソースコード

ファイル名:ToLine.csproj


<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net9.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="OpenCvSharp4" Version="4.11.0.20250507" />
    <PackageReference Include="OpenCvSharp4.runtime.win" Version="4.11.0.20250507" />
  </ItemGroup>

</Project>

ファイル名:Opt.cs



using System.ComponentModel;
using System.Globalization;

public class Opt
{
    public string Input { get; set; } = "";
    public string? Output { get; set; } = null;
    public double Gamma { get; set; } = 0.75;
    public static Opt ParseArgs(string[] a)
    {
        var o = new Opt();
        string? key = null;
        foreach (var token in a)
        {
            if (token.StartsWith("-"))
            {
                key = token;
            }
            else
            {
                switch (key)
                {
                    case "-i": case "--in": o.Input = token; break;
                    case "-o": case "--out": o.Output = token; break;
                    default:
                        // 位置引数互換
                        if (string.IsNullOrEmpty(o.Input)) o.Input = token;
                        else if (o.Output is null) o.Output = token;
                        break;
                }
                key = null;
            }
        }
        return o;
    }
    public static void PrintHelp()
    {
        Console.Error.WriteLine(
@"Usage:
  toline -i <input> [options]
  toline <input> [output.png]

Options:
  -i, --in <path>          入力ファイル
  -o, --out <path>         出力ファイル(省略時: <input>_line.png)
");
    }

}

ファイル名:Program.cs


// C#で作る適応値型2値化で線画を抽出するコンソールアプリ

// ビルドコマンド
// dotnet publish -r win-x64 -c Release /p:PublishSingleFile=true /p:IncludeAllContentForSelfExtract=true /p:SelfContained=true --output "exeの出力先のディレクトリ"
using OpenCvSharp;

class Program
{
    static void Main(string[] args)
    {
        if (args.Length == 0 || args.Contains("-h") || args.Contains("--help"))
        {
            Opt.PrintHelp();
            Environment.Exit(1);
        }

        var opt = Opt.ParseArgs(args);
        if (string.IsNullOrWhiteSpace(opt.Input))
        {
            Opt.PrintHelp();
            Environment.Exit(1);
        }

        string output = opt.Output ?? Path.Combine(
            Path.GetDirectoryName(opt.Input) ?? "",
            Path.GetFileNameWithoutExtension(opt.Input) + "_line.png");

        imageFilter(opt.Input, output, opt.Gamma);

        Console.WriteLine(output);
    }

    static void imageFilter(string inputPath, string outputPath, double gamma)
    {
        using var src = Cv2.ImRead(inputPath);

        if (src is null) return;
        if (src.Channels() == 3)
            Cv2.CvtColor(src, src, ColorConversionCodes.BGR2GRAY);
        if (src.Channels() == 4)
            Cv2.CvtColor(src, src, ColorConversionCodes.BGRA2GRAY);
        if (src.Channels() != 1)
            return;
        using var element = Cv2.GetStructuringElement(MorphShapes.Ellipse, new OpenCvSharp.Size(3, 3));
        using var work = new Mat();
        using var dst = new Mat();
        Cv2.AdaptiveThreshold(src, work, 255.0,
            AdaptiveThresholdTypes.GaussianC,
            ThresholdTypes.BinaryInv,
            51, 20);
        Cv2.Dilate(work, work, element);
        Cv2.Erode(work, work, element);
        Cv2.BitwiseNot(work, dst);         

        Cv2.ImWrite(outputPath, dst);
    }
}

実行結果

・元画像

・フィルター処理後

一般的な2値化処理ではしきい値より大きいか小さいかで白か黒に置き換わります。
しきい値を調整することは出来ますが、それで特定の物体に合わせて抽出すると、失われる部分があります。
サンプルの画像では自動車のドアやフロントウィンドウなど同色系のでペイントされた境界部が抽出されていることが確認できます。

・しきい値を127に固定した通常の2値化処理画像(参考)

背景と比べて暗い色の車体が黒くなっていて、逆に空に浮かぶ雲が抽出されずに白になっています。

コメント