【C#】画像にピクセル単位でアクセスする【備忘録】

# 概要
画像を読み込んでヒストグラムを作りたかったのでメモ

# 画像の読み込み
読み込めるフォーマット

- gif
- jpeg
- bmp
- exif
- png

```C#
System.Drawing.Bitmap bitmap;
// ローカルファイルの場合
bitmap = new Bitmap("local.png");
// Webの場合
var wc = new System.Net.WebClient();
var stream = wc.OpenRead("http://hogehoge/hoge.png");
bitmap = new Bitmap(stream);
wc.Dispose();
stream.Close();
```

# ピクセルにアクセス
```C#
int w = bitmap.Width, h = bitmap.Height;
for (int x = 0; x < w; x++)
{
  for (int y = 0; y < h; y++)
  {
    Color pixel = bitmap.GetPixel(x,y);
    // ARGB
    byte R = pixel.R; // A,G,Bも同様
    // HSB
    float h = pixel.GetHue(),
          s = pixel.GetSaturation(),
          b = pixel.GetBrightness();
    // SetPixelで書き込み
  }
}
```
ただし写真の様に幅、高さが数千ピクセルある画像だと全部走査するのはかなり時間がかかるので、画像を縮小するか数ピクセル置きに取得するなどしたほうが良さそう？
あとは`Task.Run`で別スレッドで動作させるか

## LockBitsでメモリに展開する
メモリに展開してポインタでアクセスすることで、上の方法よりは速く実行できます。

```C#
Bitmap bitmap = ...
PixelFormat pixelFormat = PixelFormat.Format32bppArgb;
int pixelSize = 4;
BitmapData bmpData = bitmap.LockBits(
  new Rectangle(0, 0, bitmap.Width, bitmap.Height),
  ImageLockMode.ReadOnly,  // 書き込むときはReadAndWriteで
  pixelFormat
);
// strideは１次元配列が何個ずつで折り返されているのか　その数
// strideがマイナス＝ボトムアップ　らしい
if (bmpData.Stride < 0) // height * strideで計算するのでマイナスだと困る
{
  bitmap.UnlockBits(bmpData);
  throw new Exception();
}

IntPtr ptr = bmpData.Scan0;
byte[] pixels = new byte[bmpData.Stride * bitmap.Height];
System.Runtime.InteropServices.Marshal.Copy(ptr, pixels, 0, pixels.Length);

for (int y = 0; y < BmpData.Height; y++)
{
  for (int x = 0; x < BmpData.Width; x++)
  {
    //(x,y)のデータ位置
    int pos = y * bmpData.Stride + x * pixelSize;
    // RGB
    byte b = pixels[pos] , g = pixels[pos + 1], r = pixels[pos + 2];
    // 色
    Color col = Color.FromArgb(255, r, g, b);
  }
}
// 変更を（した場合）反映
System.Runtime.InteropServices.Marshal.Copy(pixels,0,ptr,pixels.Length);
bitmap.UnlockBits(bmpData);
```
300*300の画像を１ピクセルずつ読み込んでみると、大体20~90秒くらいにまで実行時間を短縮できました。なお、unsafeを使えば、ポインタを直接操作することも可能です。（参考文献の方にかかれています）

# 画像の拡大・縮小
```C#
// 300x300に縮小
Bitmap old; // 縮小する元の画像
Bitmap b = new Bitmap(300,300);
Graphics g = Graphics.FromImage(b);
// 縮小・拡大のモード（補間方法）
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
g.DrawImage(old,0,0,300,300);
// b.Dispose(); g.Dispose();
```
## 補完方法
|列挙体の名前|補間方法|説明|
|-----------|-------|----|
|Bicubic|双三次補間（バイキュービック法）|25%以下の縮小には向いていない|
|Bilinear|双一次補間（バイリニア法）|50%以下の縮小には向いていない|
|Default|||
|High|高品質||
|Low|低品質||
|HighQualityBicubic|高品質双三次補間||
|HighQualityBilinear|高品質双一次補間||
|NearestNeighbor|最近傍補間（ニアレストネイバー法）||
|Invalid|無効||

# 画像の保存
```C#
Bitmap bitmap;
... // bitmapに対する処理
bitmap.Save("save.png");
// フォーマット指定
bitmap.Save("save.png", System.Drawing.Imaging.ImageFormat.Png);
```

# おまけ　図形・線描画
ご指摘があったとおり、Dispose()の呼び忘れなどを考えて、usingで宣言するようにしました。
こうすると自動的にメモリ解放されます。（Disposableなもののみ使えます）

```C#
Bitmap bmp;
using(Graphics g = Graphics.FromImage(bmp)){
  // 画像塗りつぶし
  g.FillRectangle(Brushes.White,g.VisibleClipBounds);
  // 幅1pxのペン
  using( Pen p = new Pen(Color.Black,1)){
    // または
    Pens.Black;
    // 線を引く
    g.DrawLine(p,x,y,x2,y2);
    // 長方形
    g.DrawRectangle(p,x,y,w,h);
  }
}
```

usingはまとめて書けます

``` C#
using(Graphics g = Graphics.FromImage(bmp))
using(Pen p = new Pen(Color.Black, 1))
{
  // ...
}
```

# 参考文献
[プログラムで画像を動的に作成する: .NET Tips: C#, VB.NET]
(http://dobon.net/vb/dotnet/graphics/createimage.html)
[＠IT：.NET TIPS 画像を読み込むには？ - C# Windowsフォーム](http://www.atmarkit.co.jp/fdotnet/dotnettips/018loadbmp/loadbmp.html)
[補間方法を指定して画像を拡大、縮小（スケーリング）表示する: .NET Tips: C#, VB.NET]
(http://dobon.net/vb/dotnet/graphics/interpolationmode.html)
LockBitsなどの使い方↓
[色を反転させた画像（ネガティブイメージ）を表示する: .NET Tips: C#, VB.NET]
(http://dobon.net/vb/dotnet/graphics/drawnegativeimage.html#lockbits)