ひきぷろのプログラミング日記

プログラミングの日記です。

(C#) 移動平均フィルタを実装してみた

画像に移動平均フィルタをかける処理を実装してみました。今のところ 3x3 サイズのフィルタになっています。
詳しい処理内容については、次のページを見てみてください。

平滑化(移動平均、ガウシアン)フィルタ 画像処理ソリューション

ピクセル操作クラスを作る

1 つ前の記事でグレースケールフィルタを作りましたが、 byte 配列をそのまま扱っていたので、ピクセル操作が若干複雑でした。

hikipuro.hatenadiary.jp

なので、今回はまず、ピクセル操作が簡単になるようなクラスを準備しました。

using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Runtime.InteropServices;

// ピクセル操作クラス (今のところ 24 bit RGB 固定)
class PixelManipulator {
	// 画像の幅
	public int width;

	// 画像の高さ
	public int height;

	// スキャンラインの幅
	public int stride;

	// ピクセルのバイトサイズ (24 bit RGB の場合は 3)
	public int pixelSize;

	// 画像データを展開したバイト配列
	public byte[] bytes;

	// ピクセルフォーマット
	public PixelFormat pixelFormat;

	// コンストラクタ
	public PixelManipulator() {
		pixelFormat = PixelFormat.Format24bppRgb;
	}

	// ピクセルをセットする
	public void SetPixel(int x, int y, byte r, byte g, byte b) {
		if (_IsValidPosition(x, y) == false) {
			return;
		}
		int i = _GetIndex(x, y);
		bytes[i + 2] = r;
		bytes[i + 1] = g;
		bytes[i + 0] = b;
	}

	// R の値を取得する
	public byte R(int x, int y, byte defaultValue = 0) {
		if (_IsValidPosition(x, y) == false) {
			return defaultValue;
		}
		int i = _GetIndex(x, y);
		return bytes[i + 2];
	}

	// R の値を設定する
	public void SetR(int x, int y, byte value) {
		if (_IsValidPosition(x, y) == false) {
			return;
		}
		int i = _GetIndex(x, y);
		bytes[i + 2] = value;
	}

	// G の値を取得する
	public byte G(int x, int y, byte defaultValue = 0) {
		if (_IsValidPosition(x, y) == false) {
			return defaultValue;
		}
		int i = _GetIndex(x, y);
		return bytes[i + 1];
	}

	// G の値を設定する
	public void SetG(int x, int y, byte value) {
		if (_IsValidPosition(x, y) == false) {
			return;
		}
		int i = _GetIndex(x, y);
		bytes[i + 1] = value;
	}

	// B の値を取得する
	public byte B(int x, int y, byte defaultValue = 0) {
		if (_IsValidPosition(x, y) == false) {
			return defaultValue;
		}
		int i = _GetIndex(x, y);
		return bytes[i];
	}

	// B の値を設定する
	public void SetB(int x, int y, byte value) {
		if (_IsValidPosition(x, y) == false) {
			return;
		}
		int i = _GetIndex(x, y);
		bytes[i] = value;
	}

	// 全てのピクセルを巡回する
	public void EachPixel(Action<int, int> action) {
		for (int y = 0; y < height; y++) {
			for (int x = 0; x < width; x++) {
				action(x, y);
			}
		}
	}

	// (x, y) を中心に、周囲 size ピクセル分の R 値の配列を取得する
	public byte[,] RangeR(int x, int y, int size) {
		return _Range(R, x, y, size);
	}

	// (x, y) を中心に、周囲 size ピクセル分の G 値の配列を取得する
	public byte[,] RangeG(int x, int y, int size) {
		return _Range(G, x, y, size);
	}

	// (x, y) を中心に、周囲 size ピクセル分の B 値の配列を取得する
	public byte[,] RangeB(int x, int y, int size) {
		return _Range(B, x, y, size);
	}

	// PixelManipulator をコピーする
	public PixelManipulator Clone(bool copyBytes = false) {
		PixelManipulator result = new PixelManipulator();
		result.width = width;
		result.height = height;
		result.stride = stride;
		result.pixelSize = pixelSize;
		result.bytes = new byte[bytes.Length];
		if (copyBytes) {
			Array.Copy(bytes, result.bytes, bytes.Length);
		}
		return result;
	}

	// ビットマップ画像を作成する
	public Bitmap CreateBitmap() {
		Bitmap bitmap = new Bitmap(width, height, pixelFormat);
		Rectangle rect = new Rectangle(0, 0, width, height);
		BitmapData data = bitmap.LockBits(rect, ImageLockMode.WriteOnly, pixelFormat);
		Marshal.Copy(bytes, 0, data.Scan0, bytes.Length);
		bitmap.UnlockBits(data);
		return bitmap;
	}

	// 指定された座標が正常な範囲に収まっているかチェックする
	private bool _IsValidPosition(int x, int y) {
		if (x < 0 || x >= width || y < 0 || y >= height) {
			return false;
		}
		return true;
	}

	// bytes 変数の中の、指定された座標のインデックス値を取得する
	private int _GetIndex(int x, int y) {
		return (x + y * stride) * pixelSize;
	}

	// (x, y) を中心に、周囲 size ピクセル分のピクセルを取得する
	private byte[,] _Range(Func<int, int, byte, byte> func, int x, int y, int size) {
		int count = size * 2 + 1;
		byte[,] pixels = new byte[count, count];

		byte center = func(x, y, 0);
		for (int y2 = -size; y2 <= size; y2++) {
			for (int x2 = -size; x2 <= size; x2++) {
				pixels[x2 + size, y2 + size] = func(x + x2, y + y2, center);
			}
		}
		return pixels;
	}

	// Bitmap オブジェクトから PixelManipulator を作成する
	public static PixelManipulator LoadBitmap(Bitmap bitmap) {
		if (bitmap == null) {
			return null;
		}

		PixelManipulator result = new PixelManipulator();
		result.width = bitmap.Width;
		result.height = bitmap.Height;
		result.stride = _GetScanLineSize(bitmap, result.pixelFormat);
		result.pixelSize = Image.GetPixelFormatSize(result.pixelFormat) / 8;
		result.bytes = _GetPixels(bitmap, result.pixelFormat);
		return result;
	}

	// 横の長さを計る (width よりも大きいサイズになることがある)
	private static int _GetScanLineSize(Bitmap bitmap, PixelFormat pixelFormat) {
		int pixelSize = Image.GetPixelFormatSize(pixelFormat) / 8;
		Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
		BitmapData data = bitmap.LockBits(rect, ImageLockMode.ReadOnly, pixelFormat);
		int stride = data.Stride;
		bitmap.UnlockBits(data);
		return stride / pixelSize;
	}

	// ビットマップから全てのピクセルをコピーする
	private static byte[] _GetPixels(Bitmap bitmap, PixelFormat pixelFormat) {
		Rectangle rect = new Rectangle(0, 0, bitmap.Width, bitmap.Height);
		BitmapData data = bitmap.LockBits(rect, ImageLockMode.ReadOnly, pixelFormat);
		byte[] bytes = new byte[data.Stride * bitmap.Height];
		Marshal.Copy(data.Scan0, bytes, 0, bytes.Length);
		bitmap.UnlockBits(data);
		return bytes;
	}
}
ピクセル操作クラスの使い方

次のような使い方になります。

// Bitmap を準備しておく
Bitmap bitmap;

// Bitmap の全てのピクセルを抜き出して、 PixelManipulator オブジェクトを作成する
PixelManipulator manipulator = PixelManipulator.LoadBitmap(bitmap);

// 全てのピクセルを巡回するメソッド
manipulator.EachPixel((x, y) => {
	// ここでピクセル操作を行う
	// x: X 座標
	// y: Y 座標

	// 1 ピクセルずつ取得する
	byte r = manipulator.R(x, y);
	byte g = manipulator.G(x, y);
	byte b = manipulator.B(x, y);

	// 範囲でピクセル列を取得する (3x3)
	byte[,] rangeR1 = manipulator.RangeR(x, y, 1);
	byte[,] rangeG1 = manipulator.RangeG(x, y, 1);
	byte[,] rangeB1 = manipulator.RangeB(x, y, 1);

	// 範囲でピクセル列を取得する (5x5)
	byte[,] rangeR2 = manipulator.RangeR(x, y, 2);

	// 範囲でピクセル列を取得する (7x7)
	byte[,] rangeR3 = manipulator.RangeR(x, y, 3);

	// ピクセルをセットする
	manipulator.SetR(x, y, r);
	manipulator.SetG(x, y, g);
	manipulator.SetB(x, y, b);

	// まとめてピクセルをセットする
	manipulator.SetPixel(x, y, r, g, b);
});

// 加工したピクセル列を使って Bitmap 画像を作成する
Bitmap result = manipulator.CreateBitmap();
移動平均フィルタの実装

PixelManipulator::EachPixel() メソッドを使って、全てのピクセルを巡回しています。
処理対象のピクセルの、周囲のピクセルの値を足し合わせて平均を出すような感じです。

using System.Drawing;

// 移動平均フィルタ
class SMAFilter {
	// 引数で渡されたビットマップ画像に移動平均フィルタを適用します
	public static Bitmap Apply(Bitmap source, int size = 3) {
		// ビットマップ画像から全てのピクセルを抜き出す
		PixelManipulator s = PixelManipulator.LoadBitmap(source);
		PixelManipulator d = s.Clone();

		// ピクセル数の範囲チェック
		if (size < 3) {
			size = 3;
		}
		if (size > 9) {
			size = 9;
		}
		size--;
		size /= 2;

		// 全てのピクセルを巡回する
		s.EachPixel((x, y) => {
			byte r = _SMA(s.RangeR(x, y, size));
			byte g = _SMA(s.RangeG(x, y, size));
			byte b = _SMA(s.RangeB(x, y, size));
			d.SetPixel(x, y, r, g, b);
		});

		// 新しいビットマップ画像を作成して、ピクセルをセットする
		return d.CreateBitmap();
	}

	// 周囲のピクセルの平均値を出す処理
	private static byte _SMA(byte[,] pixels) {
		int color = 0;
		int size = pixels.GetLength(0);
		for (int y = 0; y < size; y++) {
			for (int x = 0; x < size; x++) {
				color += pixels[x, y];
			}
		}
		color /= size * size;
		return (byte)color;
	}
}

Apply() メソッドの、 size 引数でピクセル数を変更するようにしています。
size = 3 だと、 3x3 の平均値を出します。
範囲が広がると処理が重くなるため、最大 9 までになるようにしています。

使い方
// PictureBox に設定された画像へ移動平均フィルタを適用する場合
Bitmap bitmap = new Bitmap(pictureBox1.Image);
bitmap = SMAFilter.Apply(bitmap);
pictureBox1.Image = bitmap;