C#で画像の透明なピクセルを検知してみる

## はじめに

イラスト作成が趣味の筆者。

わたし「イラストの塗り残しが多すぎる！」
わたし「逆に透明にしておきたいところの消し忘れも多すぎる！」
わたし「そういうのって検知できないのかなあ……」

わたし「……わたしにはC#って武器が、あったな？」

## 作るもの

**「画像の透明部分を検知して可視化できるアプリ」**

MVPとしては、以下を満たせればよいと考えました。
- 画像を読み込める
- 読み込んだ画像から透明なピクセルを見つけられる
- 見つけた透明ピクセルに指定した色をつけられる
- 画面上で透明ピクセルに色を付けた状態の画像を表示できる

超原始的ですが、やりたいことは満たせそうなので良しとします。

## 参考にした記事

とはいえ画像処理なんかやったことない！というわけで、先人のお知恵を借りました。
https://qiita.com/takutoy/items/b123dde5a699f65917b4
https://qiita.com/Nuits/items/4a2fbc0f4e8583bd5531

## 書いたコード

UI周りは割愛します。
引数に対象の画像のパスと、検知したピクセルを表示する色を渡しています

画像のピクセルを配列にコピーすると、1ピクセルにつき4バイト、B(青)、G(緑)、R(赤)、A(透明度)の順に格納されます。(参考にした記事の1本目に詳しいです)
そこで、Aの値が0のピクセルを「透明なピクセル」と判定して、ピクセルを選択した色に変更したBitMapを作成し、戻り値で返すようにしています。

```c#

// [System.Drawing.Common]Nugetパッケージのインストールが別途必要です。
using System.Drawing

//...

/// <summary>
///  画像の透明ピクセルを検知する
/// </summary>
/// <param name="filename">画像のファイルパス</param>
/// <param name="selectedColor">検知した透明ピクセルの表示色</param>
/// <returns></returns>
public Bitmap FindTransparentPixel(string filename, Color selectedColor)
{
    // 画像のBitMapデータを読み込む
    var sourceBmp = new Bitmap(filename);
    var sourceBmpData = sourceBmp.LockBits(new System.Drawing.Rectangle(0, 0, sourceBmp.Width, sourceBmp.Height), ImageLockMode.ReadOnly, sourceBmp.PixelFormat);

    // 読み込んだ画像のピクセルを配列にコピーする
    var bytes = Math.Abs(sourceBmpData.Stride) * sourceBmp.Height;
    byte[] sourcePixels = new byte[bytes];
    System.Runtime.InteropServices.Marshal.Copy(sourceBmpData.Scan0, sourcePixels, 0, bytes);
   
    // 結果のBitMapデータを作成する            
    Bitmap resultBmp = 
        new Bitmap(
            sourceBmp.Width, sourceBmp.Height, PixelFormat.Format32bppArgb);
    var resultBmpData = resultBmp.LockBits(new System.Drawing.Rectangle(0, 0, resultBmp.Width, resultBmp.Height), ImageLockMode.WriteOnly, resultBmp.PixelFormat);
    byte[] resultPixels = new byte[bytes];

    // 透明なピクセルを探す
    for (int i = 3; i < sourcePixels.Length; i += 4)
    {
        if (sourcePixels[i] == 0)
        {
            // 透明ピクセルを指定した色に変える
            var b = selectedColor.B;
            var g = selectedColor.G;
            var r = selectedColor.R;
            var a = selectedColor.A;
            resultPixels.SetValue(b, i - 3);
            resultPixels.SetValue(g, i - 2);
            resultPixels.SetValue(r, i - 1);
            resultPixels.SetValue(a, i);
        }
        else
        {
            // 透明でないピクセルはそのままコピーする
            var srcB = sourcePixels[i - 3];
            var srcG = sourcePixels[i - 2];
            var srcR = sourcePixels[i - 1];
            var srcA = sourcePixels[i];
            resultPixels.SetValue(srcB, i - 3);
            resultPixels.SetValue(srcG, i - 2);
            resultPixels.SetValue(srcR, i - 1);
            resultPixels.SetValue(srcA, i);
        }
    }

    // 結果のBitMapデータにピクセルをコピーする
    System.Runtime.InteropServices.Marshal.Copy(resultPixels,0, resultBmpData.Scan0, resultPixels.Length);
    sourceBmp.UnlockBits(sourceBmpData);
    resultBmp.UnlockBits(resultBmpData);

    return resultBmp;
}

```

戻り値のBitMapは以下のようにBitMapImage型にしてからUIに表示しています。
(ImageプロパティをxamlのImageコントロールとバインディングしています)

```C#

// 透明なピクセルを指定した色で表示したBitMapを作成する
 var bmp = service.FindTransparentPixel(ofd.FileName, System.Drawing.Color.Red);

using (Stream stream = new MemoryStream())
{
    bmp.Save(stream, ImageFormat.Png);
    stream.Seek(0, SeekOrigin.Begin);
    BitmapImage img = new BitmapImage();
    img.BeginInit();
    img.CacheOption = BitmapCacheOption.OnLoad;
    img.StreamSource = stream;
    img.EndInit();
    Image = img;
}

```
## 結果
例えばこんな感じの、どこが透明かわからない画像を読み込ませると……
![2023-10-31_20h29_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1062427/06281630-9f60-eddb-0301-28bbd9ae132d.png)

こんな感じになります。背景と目が塗り忘れられていることが一目でわかりますね。
![2023-10-31_20h30_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1062427/210478af-46d8-4aed-6143-9d036e9d30f9.png)

(ちょっとこわい)

## 改善点

Aの値が0でないと検知できないので、半透明は検知できません。
スライダー等でUIから検知する透明度のしきい値を変えられたら良かったかもと思います。

## さいごに

実は初の個人開発でした。
画像処理、勝手に難しそうなイメージを持っていたのですが、(題材が難しくなかったとはいえ)サクッと実装できて驚きました。
今回は超原始的なアプリですし、UIも残念極まりない状態なので、今後はもっとみんなが使いたくなるようなアプリを作りたい！と思いますが、まずは第一歩ということで。