0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

WPF × Basler pylon SDKで背景差分(動体検出)を実装する

Posted at

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, 照明固定(ちらつき低減)


実装の流れ

以下の流れで行います。

  1. 背景画像(動きがないときの1枚)を保存
  2. 前処理:ぼかし
  3. 現在フレームと背景の絶対差分Absdiff) → 二値化Threshold)→ オープニングErodeDilate
  4. 輪郭抽出FindContours)→ 外接矩形を描画
  5. 処理結果を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は下図のようになります。

BackgroundSubtraction_View.png


🔧 コード(差分抽出のコア)

差分抽出を実装していきます。明らかにModelで実装すべき内容ですが、これまでの記事との差分を減らすためViewModelに実装しています。

また、筆者はMono8のカメラで検証しているため、カラーカメラをお使いの方は必要に応じてグレー変換処理を入れてください。

ViewModelMatのリソースを開放するために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; }
            });
        }
    }
}

コードビハインド

ViewModelDisposeします。

/// <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ボタンで背景を取得します。

BackgroundSubtraction_BG2.png

背景に対して変化のある部分が検出されました。
スライダーで検出感度を調整可能です。

閾値=30、MinArea=30000、カーネル=3×3、open×1


🎛 チューニングのコツ

  • 背景の取り直し:照明が変わったら「背景キャプチャ」を押して再取得
  • 閾値(Threshold):暗騒音が多いときは閾値↑、微小動作を拾いたい時は↓
  • モルフォロジーの回数:形がギザギザならiterationsを1→2へ、余計に太るなら1へ戻す
  • 最小面積:誤検出を減らすフィルタ
  • 高速化
    • ROIで処理範囲を絞る
    • Cv2.Resizeで小さくしてから検出→座標を拡大換算
    • BackgroundFrameだけでなく、ぼかしを入れた背景も保持すれば、毎フレーム背景をぼかす必要がない

🧭 よくあるハマりどころ

症状 原因 対策
何も検出されない 背景に近すぎる/閾値が高すぎ 閾値を下げる/ガウシアンのカーネルを大きく
全面が白くなる 露出・ゲインがオートで揺れている ExposureAuto/GainAuto=Off、照明固定
点ノイズが多い センサノイズ・微小揺らぎ 膨張の前に erodedilate(open)
UIが固まる 画像処理が重い 処理を別Taskで行いDispatcherでUI更新/ROI採用
メモリ増加 MatのDispose漏れ usingで都度破棄、再利用可能なMatはクラス変数化

📝 まとめ

  • 背景差分で動いた領域だけを強調できる
  • 露出・照明を固定すると安定(ExposureAuto/GainAuto=Off
  • 閾値・最小面積・膨張回数で精度とノイズのバランスを取る
  • ROIや縮小処理で軽くできる

👨‍💻 筆者について

@MilleVision
産業用カメラ・画像処理システムの開発に関する情報を発信中。
pylon SDK × C# の活用シリーズを連載しています。


🛠 サンプルコード完全版のご案内

Qiita記事のサンプルをまとめた C#プロジェクト(単体テスト付き) を BOOTH で配布しています。

  • 撮影・露光・ゲイン・フレームレート・ROI・イベント駆動撮影など主要機能を網羅
  • WPFへの実装もフォロー
  • 単体テスト同梱で動作確認や学習がスムーズ
  • 記事更新に合わせてアップデート予定

👉 商品ページはこちら

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?