C#で画像処理：透明な領域を切り取る

## 欲しいもの

物体のまわりに透明な領域がある画像から
<img width="99" alt="1.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118451/cf8b1baf-c8ce-d9f1-edcb-e1f625cc5e35.jpeg">
物体が描かれている領域だけを
<img width="99" alt="2.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118451/ff45b32a-0eaf-51d1-68bd-aca4bb8c83e1.png">
切り出してファイルに保存したい。
<img width="88" alt="3.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118451/e1aec2c3-ce09-9045-7c4e-e79250111ecd.png">

※画像は[いらすとやの黒はんぺん](https://www.irasutoya.com/2019/12/blog-post_20.html)

## やること

1. 画像ファイルを読み込む
2. 透明でない領域を検出する
3. 画像の一部を切り出す
4. 画像を保存する

### 0. 準備

画像処理には `System.Drawing` 名前空間の API を使うので using しておく。

```csharp
using System.Drawing;
```

.NET Framework の場合、`System.Drawing` アセンブリへの参照を追加する必要あり。

.NET Core の場合、`System.Drawing.Common` NuGet パッケージのインストールが必要。

```bash
dotnet add package System.Drawing.Common
```

Linux の場合、`libgdiplus` も必要になるのでインストールしておく。

```bash
sudo apt install libgdiplus
```

### 1. 画像ファイルを読み込む

画像ファイルを読むには、[System.Drawing.Bitmap](https://docs.microsoft.com/en-us/dotnet/api/system.drawing.bitmap?view=netcore-3.1) クラスを使う。

```csharp
var bmp = new Bitmap(filename);
```

Bitmapクラスは BMP, GIF, EXIF, JPG, PNG, TIFF 形式をサポートしている。

ただし今回は色に透明（アルファ値：不透明度）を含む画像が必要なので、下記のようなチェックを入れておく。

```csharp
if (bmp.PixelFormat != PixelFormat.Format32bppArgb)
{
    Console.WriteLine($"Not Format32bppArgb: {bmp.PixelFormat}");
    return;
}
```

### 2. 透明でない領域を検出する

Bitmapオブジェクトの透明でない領域の矩形を検出する。

<img width="157" alt="4.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118451/6fcf9058-80c3-93af-cce7-0927d16fbff4.png">


ピクセルの色の検出は、画像をバイト列に変換して値を参照することでできる。

`PixelFormat.Format32bppArgb` フォーマットでは1ピクセル4バイト、BGRAの順に1バイトずつ入る。※リトルエンディアンの場合

例えば次のようなサイズ 2x2 の画像があった場合、
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118451/b31761de-c035-d3a0-76ce-b02531b2c97c.png)
こんなバイト列になる。
<img width="434" alt="7.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118451/b460a591-ee76-380a-a6b8-027694450b27.png">

透明でないピクセルの上、下、左、右(y0, y1, x0, x1)の座標を検出し、Rectangleオブジェクトを返すメソッド。

```csharp
static Rectangle GetRect(Bitmap bmp)
{
    // 画像のピクセルを byte[] にコピーする
    var rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
    var bmpData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, bmp.PixelFormat);
    var bytes = Math.Abs(bmpData.Stride) * bmp.Height;
    var rgbValues = new byte[bytes];
    System.Runtime.InteropServices.Marshal.Copy(bmpData.Scan0, rgbValues, 0, bytes);
    bmp.UnlockBits(bmpData);

    int x0 = bmp.Width;
    int y0 = bmp.Height;
    int x1 = 0;
    int y1 = 0;

    // 透明でないピクセルを探す
    for (int i = 3; i < rgbValues.Length; i += 4)
    {
        // Aの値が0なら透明ピクセル
        if (rgbValues[i] != 0)
        {
            int x = i / 4 % bmp.Width;
            int y = i / 4 / bmp.Width;

            if (x0 > x) x0 = x;
            if (y0 > y) y0 = y;
            if (x1 < x) x1 = x;
            if (y1 < y) y1 = y;
        }
    }

    return new Rectangle(x0, y0, x1 - x0, y1 - y0);
}

```

ちなみに、Bitmap クラスにはピクセルの色を取得する `GetPixel(int x, int y)` なんてメソッドも用意されているが、クソ遅いので `LockBits` を使って高速化している。

### 3. 画像の一部を切り出す

Bitmap オブジェクトから Rectangle で指定した領域を切り出し、新しい Bitmap オブジェクトを生成するメソッド。

```csharp
Bitmap Crop(Bitmap bmp, Rectangle rect)
{
    var newbmp = new Bitmap(rect.Width, rect.Height);
    using (var g = Graphics.FromImage(newbmp))
    {
        g.DrawImage(bmp, 0, 0, rect, GraphicsUnit.Pixel);
    }
    return newbmp;
}
```

### 4. 画像を保存する

```csharp
newbmp.Save(filename);
```

ちょっと分かりづらいが、はんぺんの周りの領域が狭くなっている food_kuro_hanpen2.png が生成された。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/118451/59389cc2-ec60-da03-b84d-3e9b3aafbeb0.png)


## ソースコード全体

[GitHub](https://github.com/takutoy/csharp-samples/blob/master/image-trim-transparent/Program.cs)

## 参考

- [.NETによる画像処理の高速化Tips：非unsafe編](https://qiita.com/Nuits/items/4a2fbc0f4e8583bd5531)
- [Bitmap.LockBits メソッド](https://docs.microsoft.com/ja-jp/dotnet/api/system.drawing.bitmap.lockbits)