WPF × Basler pylon SDKで背景差分(動体検出)を実装する
ライブビューが動いたら、次は 「動いたところだけ強調表示」 をやってみましょう。
本記事では、簡単な 背景差分(Background Subtraction) で動体領域を抽出し、WPFの画面に矩形表示する方法を紹介します。
✅ 環境
BitmapSource <=> Mat変換を行うので、nugetでOpenCvSharp4.WpfExtensions
を導入します。
項目 | 内容 |
---|---|
カメラ | Basler acA2500-14gm |
SDK | pylon Camera Software Suite |
言語 / GUI | C# / .NET 8 / WPF |
ライブラリ | OpenCvSharp4(OpenCvSharp4.Windows , OpenCvSharp4.WpfExtensions ) |
推奨カメラ設定(背景差分の安定化のため)
ExposureAuto=Off
,GainAuto=Off
, 照明固定(ちらつき低減)
- 前回記事(TransformGroupを活用したWPFライブビューのズーム/パン対応)までの内容>
- #8: GrabResult → OpenCvSharp
Mat
変換(Qiita記事リンク) - #13:
ImageGrabbed
→WriteableBitmap
でライブ表示(Qiita記事リンク)
- #8: GrabResult → OpenCvSharp
実装の流れ
以下の流れで行います。
- 背景画像(動きがないときの1枚)を保存
- 前処理:ぼかし
- 現在フレームと背景の絶対差分(
Absdiff
) → 二値化(Threshold
)→ オープニング(Erode
→Dilate
) -
輪郭抽出(
FindContours
)→ 外接矩形を描画 - 処理結果をWPFの
Image
に表示
二値化の後にオープニングを入れるのは小粒なノイズを除去するためです。
🧩 XAML(最小UI)
Grid
の列を1個追加して、背景取得ボタンをパラメータ調整スライダを追加します。
<Grid Grid.Row="2" Grid.Column="0">
<Button Content="Set BG" Width="80" Margin="0,5" ToolTip="背景キャプチャ" Command="{Binding SetBackgroundCommand}"/>
</Grid>
<Grid Grid.Row="2" Grid.Column="1" ColumnSpan="2" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<TextBlock Text="二値化閾値:" Margin="10,0"/>
<Slider x:Name="ThresholdSlider" Minimum="0" Maximum="255" Value="{Binding DetectionThreshold}" Width="100" SmallChange="1"/>
</StackPanel>
</Grid>
<Grid Grid.Row="2" Grid.Column="3" ColumnSpan="2" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<TextBlock Text="最小矩形面積:" Margin="10,0"/>
<Slider x:Name="MinAreaSlider" Minimum="200" Maximum="100000" Value="{Binding MinArea}" Width="100" SmallChange="100"/>
</StackPanel>
</Grid>
View
は下図のようになります。
🔧 コード(差分抽出のコア)
差分抽出を実装していきます。明らかにModelで実装すべき内容ですが、これまでの記事との差分を減らすためViewModelに実装しています。
また、筆者はMono8のカメラで検証しているため、カラーカメラをお使いの方は必要に応じてグレー変換処理を入れてください。
ViewModel
はMat
のリソースを開放するためにIDisposable
としておきます。
using Basler.Pylon;
using BaslerSamples;
using Common;
using OpenCvSharp;
using OpenCvSharp.WpfExtensions; // 追加
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Imaging;
using Application = System.Windows.Application;
using Size = OpenCvSharp.Size;
namespace BaslerGUISample.ViewModels
{
public partial class MainViewModel : BindableBase, IDisposable
{
BaslerCameraSample _cameraService = new BaslerCameraSample();
// 表示に必要なフィールドを追加
// 背景キャプチャのためのフラグ
private volatile bool _captureBgNext = false;
public MainViewModel()
{
// その他のコマンドは省略
SetBackgroundCommand = new DelegateCommand(SetBackground, () => IsGrabbing);
}
/// <summary>
/// カメラが画像を連続取得中かどうかを示します。
/// </summary>
private bool _isGrabbing;
public bool IsGrabbing
{
get => _isGrabbing;
set => SetProperty(ref _isGrabbing, value);
}
private Mat? _backgroundFrame;
/// <summary>
/// 背景画像を表します。
/// </summary>
/// <value></value>
public Mat? BackgroundFrame
{
get => _backgroundFrame;
set
{
var old = _backgroundFrame;
if (SetProperty(ref _backgroundFrame, value))
{
old?.Dispose();
BgBlurCached = null; // 背景差し替え時はぼかしキャッシュも破棄
}
}
}
private Mat? _bgBlurCached;
/// <summary>
/// ぼかし済み背景を保持します
/// </summary>
/// <value></value>
public Mat? BgBlurCached
{
get => _bgBlurCached;
set
{
var old = _bgBlurCached;
if (SetProperty(ref _bgBlurCached, value))
{
old?.Dispose();
}
}
}
/// <summary>
/// 背景画像が設定されているかどうかを示します。
/// </summary>
public bool HasBackground => BackgroundFrame != null;
private double _detectionThreshold = 30.0;
/// <summary>
/// 動体検知のしきい値を示します。
/// </summary>
/// <value></value>
public double DetectionThreshold
{
get => _detectionThreshold;
set => SetProperty(ref _detectionThreshold, value);
}
private double _minArea = 800.0;
/// <summary>
/// 検出する最小矩形面積を示します。
/// </summary>
/// <value></value>
public double MinArea
{
get => _minArea;
set => SetProperty(ref _minArea, value);
}
/// <summary>
/// 背景キャプチャを設定するコマンドを示します。
/// </summary>
/// <value></value>
public DelegateCommand SetBackgroundCommand { get; }
public void Dispose()
{
if (IsGrabbing)
{
Stop();
}
Disconnect();
BackgroundFrame = null;
BgBlurCached = null;
}
/// <summary>
/// 背景画像を設定します。
/// </summary>
public void SetBackground()
{
// 次に到着するフレームを背景として確保する方式
_captureBgNext = true;
BgBlurCached = null;
}
private int _updating; // UI過負荷対策(1フレーム処理中は次を捨てる)
/// <summary>
/// フレーム受信時の処理を行います。
/// </summary>
/// <param name="sender"></param>
/// <param name="frame"></param>
private async void OnImageGrabbed(object? sender, ImageGrabbedEventArgs e)
{
IsGrabbing = _cameraService.IsGrabbing;
// UI過負荷対策(フレーム処理中は次を捨てる)
if (Interlocked.Exchange(ref _updating, 1) == 1) return;
try
{
await UpdateCurrentFrame(e.GrabResult);
// フレーム数加算、HUD更新(~10Hz)は省略
}
catch (InvalidOperationException ex)
{
MessageBox.Show($"Failed to grab. Error: {ex.Message}");
}
catch (TaskCanceledException)
{
// ライブ中にアプリが終了すると例外がでる。
}
finally
{
Interlocked.Exchange(ref _updating, 0);
}
}
/// <summary>
/// フレーム更新処理の本体
/// 背景差分法による動体検知を行い、CurrentFrameプロパティを更新します。
/// </summary>
/// <param name="result"></param>
private async Task UpdateCurrentFrame(IGrabResult result)
{
if (!result.GrabSucceeded) return;
// 1) GrabResult → Mat
using Mat raw = result.ToMat(); // #8の実装を使用
// 入力を単一チャネル化(カラー機でも安全)
// ※ カラー機は未検証です。
using Mat frame = new Mat();
if (raw.Channels() == 1)
{
raw.CopyTo(frame);
}
else
{
Cv2.CvtColor(raw, frame, ColorConversionCodes.BGR2GRAY);
}
if (_captureBgNext)
{
// 背景キャプチャ
BackgroundFrame = frame.Clone();
_captureBgNext = false;
}
// 背景が未設定ならそのまま表示
if (HasBackground == false)
{
await SetCurrentFrame(frame);
return;
}
// 2) 前処理:ぼかし
using Mat blur = new();
Cv2.GaussianBlur(frame, blur, new Size(5, 5), 0);
// 背景ぼかしキャッシュが無ければ作る
if (BgBlurCached == null || BgBlurCached.Empty())
{
BgBlurCached = new Mat();
Cv2.GaussianBlur(BackgroundFrame!, BgBlurCached, new Size(5,5), 0);
}
// 3) 差分 → 二値化 → オープニング
using var diff = new Mat();
Cv2.Absdiff(blur, BgBlurCached, diff);
using var bw = new Mat();
Cv2.Threshold(diff, bw, DetectionThreshold, 255, ThresholdTypes.Binary);
// オープニング(ノイズ除去)
using var k = Cv2.GetStructuringElement(MorphShapes.Rect, new Size(3,3));
Cv2.MorphologyEx(bw, bw, MorphTypes.Open, k, iterations: 1);
// 4) 輪郭 → 外接矩形描画
Cv2.FindContours(bw, out var contours, out _, RetrievalModes.External, ContourApproximationModes.ApproxSimple);
using var colorFrame = new Mat();
Cv2.CvtColor(frame, colorFrame, ColorConversionCodes.GRAY2BGR);
int thick = Math.Max(1, colorFrame.Width / 400);
foreach (var c in contours)
{
var rect = Cv2.BoundingRect(c);
if (rect.Width * rect.Height < MinArea) continue;
Cv2.Rectangle(colorFrame, rect, Scalar.Red, thick);
}
// 5) 表示
await SetCurrentFrame(colorFrame);
}
private readonly object _lock = new(); // UI更新の同期
private async Task SetCurrentFrame(Mat mat)
{
// Mat → BitmapSource
BitmapSource src = mat.ToBitmapSource(); // OpenCvSharp4.WpfExtensions
src.Freeze(); // 別スレッドで生成したのでFreeze必須
await Application.Current.Dispatcher.BeginInvoke(() =>
{
lock (_lock) { CurrentFrame = src; }
});
}
}
}
コードビハインド
ViewModel
をDispose
します。
/// <summary>
/// ウィンドウが閉じられる前にViewModelを破棄する
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void Window_Closing(object sender, System.ComponentModel.CancelEventArgs e)
{
_viewModel.Dispose();
}
実行例
Set BG
ボタンで背景を取得します。
背景に対して変化のある部分が検出されました。
スライダーで検出感度を調整可能です。
🎛 チューニングのコツ
- 背景の取り直し:照明が変わったら「背景キャプチャ」を押して再取得
- 閾値(Threshold):暗騒音が多いときは閾値↑、微小動作を拾いたい時は↓
-
モルフォロジーの回数:形がギザギザなら
iterations
を1→2へ、余計に太るなら1へ戻す - 最小面積:誤検出を減らすフィルタ
-
高速化:
- ROIで処理範囲を絞る
-
Cv2.Resize
で小さくしてから検出→座標を拡大換算 -
BackgroundFrame
だけでなく、ぼかしを入れた背景も保持すれば、毎フレーム背景をぼかす必要がない
🧭 よくあるハマりどころ
症状 | 原因 | 対策 |
---|---|---|
何も検出されない | 背景に近すぎる/閾値が高すぎ | 閾値を下げる/ガウシアンのカーネルを大きく |
全面が白くなる | 露出・ゲインがオートで揺れている |
ExposureAuto/GainAuto=Off 、照明固定 |
点ノイズが多い | センサノイズ・微小揺らぎ | 膨張の前に erode → dilate (open) |
UIが固まる | 画像処理が重い | 処理を別Taskで行いDispatcher でUI更新/ROI採用 |
メモリ増加 | MatのDispose漏れ |
using で都度破棄、再利用可能なMatはクラス変数化 |
📝 まとめ
- 背景差分で動いた領域だけを強調できる
- 露出・照明を固定すると安定(
ExposureAuto/GainAuto=Off
) - 閾値・最小面積・膨張回数で精度とノイズのバランスを取る
- ROIや縮小処理で軽くできる
👨💻 筆者について
@MilleVision
産業用カメラ・画像処理システムの開発に関する情報を発信中。
pylon SDK × C# の活用シリーズを連載しています。
🛠 サンプルコード完全版のご案内
Qiita記事のサンプルをまとめた C#プロジェクト(単体テスト付き) を BOOTH で配布しています。
- 撮影・露光・ゲイン・フレームレート・ROI・イベント駆動撮影など主要機能を網羅
- WPFへの実装もフォロー
- 単体テスト同梱で動作確認や学習がスムーズ
- 記事更新に合わせてアップデート予定