WinFormsで画像にガウスぼかしをかける(C#)——3つの実装と最小サンプル
TL;DR
すぐ動かすなら OpenCvSharp、純 .NET なら ImageSharp、依存を増やしたくない/仕組みを学びたいなら LockBits + 分離畳み込み(自前実装)。WinForms では UI フリーズを避けるために必ず Task.Run 等でワーカースレッド実行に。
目次
この記事で得られるもの
- WinForms でガウスぼかしをかける 3 通りの方法
- 最小 UI(PictureBox + TrackBar + ボタン)で動く サンプルコード
- 自前実装(LockBits + 分離畳み込み)の 高速・高品質な基礎実装
- 画質と速度の チューニング指針 と 落とし穴
注: .NET 6 以降の System.Drawing.Common は Windows 専用です(WinForms は問題ありません)。
アプローチ早見表
方法 | 依存 | 速度/品質 | 学習コスト | こんな人に |
---|---|---|---|---|
OpenCvSharp | OpenCV | とても速い/高品質 | 低 | まず動かしたい、速度重視 |
ImageSharp | SixLabors.ImageSharp | 速い/高品質 | 低 | 純 .NET で完結したい |
自前実装 | なし(unsafe 推奨) | 速い(最適化次第)/高品質 | 中〜高 | 仕組みを理解・制御したい |
1) OpenCvSharp で最短実装(おすすめ)
セットアップ
- NuGet: OpenCvSharp4, OpenCvSharp4.runtime.win, OpenCvSharp4.Extensions
プラットフォーム変更
- プロジェクトの [プラットフォーム ターゲット] を x64 にして [32 ビット優先] をオフ
- いったん Clean → Rebuild(bin/obj を消すと確実)

最小コード(pictureBox1 の画像に σ を適用)
using OpenCvSharp;
using OpenCvSharp.Extensions;
double sigma = 2.0; // ぼかし強度
using var mat = BitmapConverter.ToMat((Bitmap)pictureBox1.Image);
using var dst = new Mat();
Cv2.GaussianBlur(mat, dst, new Size(0, 0), sigma);
pictureBox1.Image?.Dispose();
pictureBox1.Image = BitmapConverter.ToBitmap(dst);
- Size(0, 0) を渡すと カーネルサイズは σ から自動決定されます。
- 高速・高品質。色ずれやエッジのアーティファクトが少ないです。
2) ImageSharp(純 .NET)
セットアップ
- NuGet: SixLabors.ImageSharp, SixLabors.ImageSharp.Drawing(必要に応じて)
最小コード
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Processing;
float sigma = 2.0f;
using var msIn = new MemoryStream();
((Bitmap)pictureBox1.Image).Save(msIn, System.Drawing.Imaging.ImageFormat.Png);
msIn.Position = 0;
using var img = Image.Load(msIn);
img.Mutate(x => x.GaussianBlur(sigma));
using var msOut = new MemoryStream();
img.SaveAsPng(msOut);
msOut.Position = 0;
pictureBox1.Image?.Dispose();
pictureBox1.Image = new Bitmap(msOut);
- 依存は .NET のみで完結。クロスプラットフォーム志向のコードベースにも流用可能です。
3) 依存なし・自前実装(LockBits + 分離畳み込み)
ポイント
- ガウスカーネルは 水平→垂直 の 2 回に分けて畳み込み(分離畳み込み)すると高速。
- カーネル半径は経験則で radius = ceil(3 * σ) が目安。
- GetPixel/SetPixel は非常に遅いので LockBits + unsafe を推奨。
- 端の画素は クランプ(端値の繰り返し) で処理。
実装(シングルファイル)
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading.Tasks;
public static class GaussianBlur
{
public static Bitmap Apply(Bitmap source, double sigma, int radius = -1)
{
if (source == null) throw new ArgumentNullException(nameof(source));
if (sigma <= 0) throw new ArgumentOutOfRangeException(nameof(sigma));
int r = radius > 0 ? radius : Math.Max(1, (int)Math.Ceiling(sigma * 3));
double[] kernel = BuildKernel(sigma, r);
// 32bpp ARGB に正規化
Bitmap src32 = source.PixelFormat == PixelFormat.Format32bppArgb
? (Bitmap)source.Clone()
: source.Clone(new Rectangle(0, 0, source.Width, source.Height), PixelFormat.Format32bppArgb);
Bitmap tmp = new(src32.Width, src32.Height, PixelFormat.Format32bppArgb);
Bitmap dst = new(src32.Width, src32.Height, PixelFormat.Format32bppArgb);
var srcData = src32.LockBits(new Rectangle(0, 0, src32.Width, src32.Height),
ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
var tmpData = tmp.LockBits(new Rectangle(0, 0, tmp.Width, tmp.Height),
ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
var dstData = dst.LockBits(new Rectangle(0, 0, dst.Width, dst.Height),
ImageLockMode.WriteOnly, PixelFormat.Format32bppArgb);
try
{
unsafe
{
int w = src32.Width;
int h = src32.Height;
// 横方向
byte* s0 = (byte*)srcData.Scan0;
byte* t0 = (byte*)tmpData.Scan0;
int sStride = srcData.Stride;
int tStride = tmpData.Stride;
Parallel.For(0, h, y =>
{
byte* sRow = s0 + y * sStride;
byte* tRow = t0 + y * tStride;
for (int x = 0; x < w; x++)
{
double b = 0, g = 0, rCh = 0, a = 0;
for (int k = -r; k <= r; k++)
{
int xi = x + k;
if (xi < 0) xi = 0;
else if (xi >= w) xi = w - 1;
byte* p = sRow + xi * 4;
double kk = kernel[k + r];
b += p[0] * kk;
g += p[1] * kk;
rCh += p[2] * kk;
a += p[3] * kk;
}
byte* d = tRow + x * 4;
d[0] = ToByte(b);
d[1] = ToByte(g);
d[2] = ToByte(rCh);
d[3] = ToByte(a);
}
});
// 縦方向
byte* tt0 = (byte*)tmpData.Scan0;
byte* d0 = (byte*)dstData.Scan0;
int dStride = dstData.Stride;
Parallel.For(0, w, x =>
{
for (int y = 0; y < h; y++)
{
double b = 0, g = 0, rCh = 0, a = 0;
for (int k = -r; k <= r; k++)
{
int yi = y + k;
if (yi < 0) yi = 0;
else if (yi >= h) yi = h - 1;
byte* p = tt0 + yi * tStride + x * 4;
double kk = kernel[k + r];
b += p[0] * kk;
g += p[1] * kk;
rCh += p[2] * kk;
a += p[3] * kk;
}
byte* d = d0 + y * dStride + x * 4;
d[0] = ToByte(b);
d[1] = ToByte(g);
d[2] = ToByte(rCh);
d[3] = ToByte(a);
}
});
}
}
finally
{
src32.UnlockBits(srcData);
tmp.UnlockBits(tmpData);
dst.UnlockBits(dstData);
src32.Dispose();
tmp.Dispose();
}
return dst;
}
private static double[] BuildKernel(double sigma, int r)
{
double[] k = new double[2 * r + 1];
double sigma2 = sigma * sigma;
double sum = 0;
for (int i = -r; i <= r; i++)
{
double v = Math.Exp(-(i * i) / (2 * sigma2));
k[i + r] = v;
sum += v;
}
// 正規化
for (int i = 0; i < k.Length; i++) k[i] /= sum;
return k;
}
private static byte ToByte(double v)
{
if (v <= 0) return 0;
if (v >= 255) return 255;
return (byte)Math.Round(v);
}
}
使用例(UIスレッドをブロックしない)
private async void btnBlur_Click(object sender, EventArgs e)
{
if (pictureBox1.Image is not Bitmap src) return;
double sigma = trackBarSigma.Value / 10.0; // 例: 10〜50 → 1.0〜5.0
var blurred = await Task.Run(() => GaussianBlur.Apply(src, sigma));
var old = pictureBox1.Image;
pictureBox1.Image = blurred;
old?.Dispose();
}
最小フォーム例(そのまま貼って動く)
- コントロール: PictureBox pictureBox1, TrackBar trackBarSigma (Min=10, Max=50, Value=20), Button btnOpen, Button btnBlur
- 画像読込→スライダーで σ 調整→「ぼかす」で適用
using System;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
trackBarSigma.Minimum = 10; // 1.0
trackBarSigma.Maximum = 50; // 5.0
trackBarSigma.Value = 20; // 2.0
trackBarSigma.TickFrequency = 5;
trackBarSigma.ValueChanged += (_, __) => this.Text = $"σ={trackBarSigma.Value / 10.0:F1}";
this.Text = $"σ={trackBarSigma.Value / 10.0:F1}";
}
private void btnOpen_Click(object sender, EventArgs e)
{
using var ofd = new OpenFileDialog
{
Filter = "画像ファイル|*.png;*.jpg;*.jpeg;*.bmp",
RestoreDirectory = true
};
if (ofd.ShowDialog() == DialogResult.OK)
{
pictureBox1.Image?.Dispose();
pictureBox1.Image = new Bitmap(ofd.FileName);
}
}
private async void btnBlur_Click(object sender, EventArgs e)
{
if (pictureBox1.Image is not Bitmap src) return;
double sigma = trackBarSigma.Value / 10.0;
// 重い処理は別スレッドで
var blurred = await Task.Run(() => GaussianBlur.Apply(src, sigma));
var old = pictureBox1.Image;
pictureBox1.Image = blurred;
old?.Dispose();
}
}
画質・速度のコツ
- カーネル半径: radius = ceil(3 * σ) を基準に。大きくしすぎると計算量が増えるだけで効果は飽和します。
- 分離畳み込み: 2D 畳み込み((2r+1)²)を 横+縦(2×(2r+1)) に分解して高速化。
- 並列化: Parallel.For は大きい画像で有効。小さい画像ではオーバーヘッドに注意。
- UI フリーズ対策: Task.Run + await で非同期実行。
- 透明画素: 透過 PNG などは アルファも一緒にぼかすとエッジが柔らかくなります。ハローが気になる場合は プリマルチプライド(premultiplied alpha) で計算→復元が最良。
- ダウンサンプル・アップサンプル: ふわっとした背景用途なら、縮小→ぼかし→拡大で高速化。
- GetPixel/SetPixel 禁止: デモ用途以外では使用しない(極端に遅い)。
- ディスポーズ: Bitmap.Dispose() を忘れない(メモリリーク対策)。
よくある落とし穴
- System.Drawing のプラットフォーム制限: .NET 6+ で Windows 以外は非推奨。WinForms なら Windows 前提なので OK。
- Stride の扱い: 画像幅×4 が必ずしも Stride と一致しません。LockBits の Stride を必ず使う。
- 巨大画像: Task.Run でも処理が長い場合は 進捗表示 や キャンセル(CancellationToken)を検討。
- 端処理: クランプ以外(ミラー、ラップ)にすると見え方が変わります。要件に合わせて実装を差し替え。
演習
- σリアルタイムプレビュートラックバーが動いたら 300ms デバウンスして自動ぼかし。System.Windows.Forms.Timer を使い、連続操作でも滑らかに。
- ボックスぼかし×3でガウス近似3 回のボックスフィルタでガウスを近似。自前実装で速度比較し、画質差を観察。
- プリマルチプライド対応アルファ付き PNG を用意し、premultiplied alpha でのぼかし→非プリマルチ復元を追加して輪郭の差を確認。
まとめ
- まずは OpenCvSharp で最短・高品質を確保。
- 依存を避けたい/学習目的なら LockBits + 分離畳み込み が王道。
- σ と半径、非同期化、アルファ処理 を押さえれば、実践用のガウスぼかしは完成です。
訪問数 7 回, 今日の訪問数 7回
ディスカッション
コメント一覧
まだ、コメントがありません