【C#】他のアプリから GIMP に画像を出力させる

# はじめに

画像の輪郭線を抽出したいというニッチな需要がありまして、色々と模索していました。  
GIMP <https://www.gimp.org/> にいい具合に輪郭線を抽出するフィルターがあるようです。

![a](https://drive.google.com/thumbnail?id=1szFtVbIRyhCzqwrrxi4-eTnZ0Gt-8d5B)

メニュー > フィルター > 輪郭抽出 > 輪郭

GIMP は画像編集ソフトで、通常 GUI からマウス等で操作しますが、これを**コードで制御する**というのが今回の本題です。

## サンプルコード

※ **Windows x64 用**です。他の環境ではおそらくエラーになります

1. GIMP をインストール
1. `outline.scm` を `"%Program Files%\GIMP 2\share\gimp\2.0\scripts"` にコピー

<details>
<summary>outline.scm</summary>

指定した入力パスの画像の輪郭を抽出して指定した出力パスに PNG 出力する GIMP のスクリプト

```scm
(define (outline input output)(
    let* 
        (   
            (img (car (gimp-file-load 1 input input)))
            (drw (car (gimp-image-active-drawable img)))
            (car (plug-in-edge 1 img drw 2 1 0))
        )
	    (file-png-save 1 img drw output output 0 5 0 0 0 0 0)
        (gimp-quit 0)
    )
)
```

</details>

<details>
<summary>C# からの GIMP 呼び出しコード</summary>

```cs
using System.Diagnostics;

file static class GIMPCaller
{
    internal static async Task CreateOutlineAsync(string input, string output)
    {
        // GIMP のインストールディレクトリを取得
        // x64 のほうの ProgramFiles を参照
        var gimpDir = Path.Join(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "GIMP 2", "bin");
        if (!Directory.Exists(gimpDir))
            throw new InvalidOperationException("GIMP がインストールされていません");

        // GIMP のインストールディレクトリから gimp-console のパスを取得
        var files = Directory.GetFiles(gimpDir, "gimp-console*");
        if (files.Length is 0)
            throw new InvalidOperationException("gimp-console が見つかりませんでした");

        var gimpPath = files[^1];

        if (!File.Exists(input))
            throw new InvalidOperationException("入力ファイルが見つかりません");
        var inputFullPath = Path.GetFullPath(input);
        var outputFullPath = Path.GetFullPath(output);

        // -i: ユーザーインターフェースなしで起動
        // -d: パターン、 グラデーション、 パレット、 各種ブラシなどのデータを読み込まずに起動
        // -f: フォントを読み込まずに起動
        // -b: バッチ処理（コマンドの複合技を一気に応答なく実行）
        var arg = $"""
            -i -d -f -b "(outline \"{inputFullPath}\" \"{outputFullPath}\")"
            """;
        var info = new ProcessStartInfo(gimpPath, arg)
        {
            UseShellExecute = false,
            CreateNoWindow = true,
        };
        var process = Process.Start(info)!;
        await process.WaitForExitAsync();

        if (process.ExitCode != 0)
            throw new InvalidOperationException("GIMP でエラーが発生しました");
    }

    static async void HouToUse()
    {
        await GIMPCaller.CreateOutlineAsync("input.png", "output.png");
    }
}
```

</details>

## 使い方

```cs
// input.png は入力画像のパス、output.png は出力画像のパス
// C# から使用する分には gimp-console のパスを通さなくて良い
await GIMPCaller.CreateOutlineAsync("input.png", "output.png");
```

コマンドプロンプトから（WindowsPowerShell だとエラーになるため注意）

```text
gimp-console-2.10 -i -d -f -b "(outline \"input.png\" \"output.png\")"
```

- `gimp-console-2.10.exe` のパスを通しておく必要があります
- **コマンドプロンプトを使用する**必要があります。WindowsPowerShell だと引数指定の都合でうまくいかないようです。最近の Windows の統合型のターミナルだと標準で WindowsPowerShell が使われる気がするので注意が必要です
- 画像のファイルパスには入力・出力それぞれ**フルパス**を指定します

## 所見

- GIMP 形式のスクリプト（Script-Fu）は書き慣れない言語のためよくわかっていません。エディタのサポートもないため難易度が高めに感じました
- GIMP の関数一覧は メニュー > フィルター >Script-Fu > Script-Fu コンソール > 参照ボタン から検索でき、引数の説明等を見ることができます
- GIMP は Python による画像編集に対応しているようですが、これもバッチ処理として起動する必要があるようです
- 中間データとして扱うならファイルを経由せずにできないかと思い、クリップボード経由でやってみましたがうまくいかなかったです
- 画像のサイズにも依存しますが、一連の処理に**数秒**かかるためパフォーマンスを気にする必要がありそうです。GIMP の起動はコストが高いようなので、複数の画像を処理するときはディレクトリ内の画像をすべて処理する、みたいなバッチ処理にするといいかもしれません

## 参考 URL

- GIMPでバッチ処理を行う <https://qiita.com/data-dokata/items/72c7d449d171710007ad>
- Script-Fu <https://wiki.gz-labs.net/index.php/Script-Fu>

## おわりに

今回は全体的に資料が少ない印象で、先駆者の資料が大変参考になりました。  
C# コードで直接画像の輪郭を抽出するコードを書こうかと考えましたが、流石に再発明する車輪が大きすぎたため見送りました。  
今回の内容はサーバーに画像を処理させるみたいなことに応用できそうなので、備忘録として残しておこうと思います。