SKProgramLab

Let's Enjoy Programming! ~画像処理/IoT/機械学習など~

C#/VB.NETで画像処理⑮<顔検出>

こんにちは、SKです。
f:id:SKProgramLab:20200310203357p:plain:w100

C#/VB.NETで画像処理シリーズの第15弾。
今回は、OpenCVSharpを使って顔検出を行う方法を紹介します。

画像の中に存在する「人の顔」を検出する手法であり、誰の顔かを判断する顔認識ではありません。
OpenCVには"Haar-cascade"と呼ばれる分類器によって顔検出を行う機能があります。

Harr-cascadeの仕組みを理解しなくても、簡単なプログラムで顔検出を実装できます。
やはり超絶便利なライブラリ。動かしてみると楽しい機能なので、皆さんも是非試してみてください。

例によって、カメラ画像に顔画像を映し、検出位置に矩形を描画するプログラムを作ります。

参考リンク

qiita.com

OpenCVSharpとは?

skprogramlab.hatenablog.com

(前回)背景差分

skprogramlab.hatenablog.com

動画手順

youtu.be

①特徴分類器ファイルを用意する

まず、OpenCVライブラリ本体を落とします(OpenCVSharpではなく本家のサイトから)。
github.com

2020/5/26現在の最新バージョンは4.3.0です。
opencv-4.3.0-vc14_vc15.exeをクリックし、ダウンロード。 f:id:SKProgramLab:20200526000618p:plain

任意の場所に解凍し、sources\data\haarcascadesフォルダの中から"haarcascade_frontalface_default.xml"を探し、自分のプロジェクトの実行フォルダ(\bin\Debug)にコピーします。
f:id:SKProgramLab:20200526001823p:plain

f:id:SKProgramLab:20200526001131p:plain

②顔検出データを生成する

CascadeClassifier型の変数を定義し、先ほどコピーしたxmlファイルを基に初期化します。

//顔検出データ
private CascadeClassifier faceCascade;

private void Process(Mat img)        
{
    //顔検出データ生成
    if (faceCascade == null)
    {
        faceCascade = new CascadeClassifier(Path.Combine(Application.StartupPath, "haarcascade_frontalface_default.xml"));
    }

}


③顔検出を行い矩形描画

カメラ画像をグレースケール化し、先ほど定義したfaceCascadeの"DetectMultiScale"関数の引数に指定します(その他のパラメータは未指定のデフォルト値でOK)。
顔を検出した座標情報がRect型の配列として返されるので、その数だけループを回し、赤色の矩形を描画します。

private void Process(Mat img)        
{
    //顔検出データ生成
    if (faceCascade == null)
    {
        faceCascade = new CascadeClassifier(Path.Combine(Application.StartupPath, "haarcascade_frontalface_default.xml"));
    }

    //グレースケール化
    using (Mat gray = img.CvtColor(ColorConversionCodes.BGR2GRAY))
    {
        //顔検出
        Rect[] rects = faceCascade.DetectMultiScale(gray);
        foreach(Rect r in rects)
        {
            //赤い四角を描画
            Cv2.Rectangle(img, r, Scalar.Red, 2);
        }
    }
}


実行します。
人の顔で画像検索した結果を映したモニタをWebカメラで撮っています。
画像内の人の顔をある程度検出できています。 f:id:SKProgramLab:20200526003600p:plain
精度はそこそこですが、極々簡単なプログラムで顔検出機能を実装することができました。


コード全文を載せておきます。
C#

using System;
using System.IO;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                using (VideoCapture v = new VideoCapture(0))
                using (Mat img = new Mat())
                {
                    while (true)
                    {
                        v.Read(img);
                        Process(img);
                        pictureBox1.Image = BitmapConverter.ToBitmap(img);
                    }
                }
            });
        }

        //顔検出データ
        private CascadeClassifier faceCascade;

        private void Process(Mat img)        
        {
            //顔検出データ生成
            if (faceCascade == null)
            {
                faceCascade = new CascadeClassifier(Path.Combine(Application.StartupPath, "haarcascade_frontalface_default.xml"));
            }

            //グレースケール化
            using (Mat gray = img.CvtColor(ColorConversionCodes.BGR2GRAY))
            {
                //顔検出
                Rect[] rects = faceCascade.DetectMultiScale(gray);
                foreach(Rect r in rects)
                {
                    //赤い四角を描画
                    Cv2.Rectangle(img, r, Scalar.Red, 2);
                }
            }
        }

    }
}


VB.NET

Imports System.IO
Imports OpenCvSharp
Imports OpenCvSharp.Extensions

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Task.Run(Sub()
                     Using v As New VideoCapture(0)
                         Using img As New Mat
                             Do
                                 v.Read(img)
                                 Process(img)
                                 PictureBox1.Image = img.ToBitmap
                             Loop
                         End Using
                     End Using
                 End Sub)
    End Sub

    '顔検出データ
    Private faceCascade As CascadeClassifier = Nothing

    Private Sub Process(ByVal img As Mat)

        '顔検出データ生成
        If faceCascade Is Nothing Then
            faceCascade = New CascadeClassifier(Path.Combine(Application.StartupPath, "haarcascade_frontalface_default.xml"))
        End If

        'グレースケール化
        Using gray As Mat = img.CvtColor(ColorConversionCodes.BGR2GRAY)
            '顔検出
            Dim rects As Rect() = faceCascade.DetectMultiScale(gray)
            For Each r As Rect In rects
                '赤い四角を描画
                Cv2.Rectangle(img, r, Scalar.Red, 2)
            Next
        End Using

    End Sub

End Class


C#/VB.NETで画像処理シリーズは、この辺で一区切りつけようと思います。

優れたGUI設計環境を持つC#/VB.NET言語、多種多様な画像処理を簡単に実現できるOpenCVライブラリ、そしてそれらを繋ぐラッパーライブラリOpenCVSharp、まだあまり広くは知られていないかもしれませんが、非常に美しい組み合わせだと私は思います。

これまでの記事を通して、画像処理プログラムは決して難しくない、楽しみながら学べる分野であるということが、少しでも伝われば幸いです。

次は、簡単なゲームプログラミングをやろうかと検討中。。

C#/VB.NETで画像処理⑭<背景差分>

こんにちは、SKです。
f:id:SKProgramLab:20200310203357p:plain:w100

C#/VB.NETで画像処理シリーズの第14弾。
今回は、画像処理の分野で良く使われる背景差分を行う方法を紹介します。

背景画像と現在画像を重ね合わせ、画像の中で変化のあった領域を抽出できます。
動体検知に使われたりします。

ボタン押下時に背景画像を登録し、背景差分をリアルタイムで行った結果の二値化画像を表示してみます。

OpenCVSharpとは?

skprogramlab.hatenablog.com

(前回)テンプレートマッチング

skprogramlab.hatenablog.com

動画手順

youtu.be

①背景画像登録ボタンを配置する

フォーム上にボタンを配置します。
f:id:SKProgramLab:20200507222624p:plain

前回同様、メインのProcess関数内でカメラ画像を常に変数picImageに格納しつつ、このボタンのクリックイベントで背景画像の変数backImageに格納します。

private Mat picImage;
private Mat backImage;

private void Process(Mat img)        
{
    //カメラ画像格納
    if(picImage == null)
    {
        picImage = img.Clone();
    }
    else
    {
        img.CopyTo(picImage);
    }
}

private void button1_Click(object sender, EventArgs e)
{
    //背景画像格納
    backImage = picImage.Clone();
}


②背景差分を行う

AbsDiff関数を使用し、背景画像とカメラ画像の差分画像を作成します。
出力画像もBGRの3チャンネルですが、結果が分かりやすいように二値化して表示します。

AbsDiff関数

Cv2.Absdiff Method
・第1引数:入力画像1
・第2引数:入力画像2
・第3引数:出力画像

//背景差分
if(backImage != null)
{               
    using(Mat dst = new Mat())
    {
        Cv2.Absdiff(img, backImage, dst);
        using(Mat gray = dst.CvtColor(ColorConversionCodes.BGR2GRAY))
        {
            Cv2.Threshold(gray, gray, 120, 255, ThresholdTypes.Binary);
            Cv2.ImShow("diff", gray);
            Cv2.WaitKey(1);
        }
    }
}



実行します。
背景画像を登録すると、最初は真っ黒 f:id:SKProgramLab:20200507225256p:plain

カップを入れるとこんな感じ f:id:SKProgramLab:20200507225342p:plain

最後に、コード全文を載せておきます。
C#

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                using (VideoCapture v = new VideoCapture(0))
                using (Mat img = new Mat())
                {
                    while (true)
                    {
                        v.Read(img);
                        Process(img);
                        pictureBox1.Image = BitmapConverter.ToBitmap(img);
                    }
                }
            });
        }

        private Mat picImage;
        private Mat backImage;

        private void Process(Mat img)        
        {
            //カメラ画像格納
            if(picImage == null)
            {
                picImage = img.Clone();
            }
            else
            {
                img.CopyTo(picImage);
            }

            //背景差分
            if(backImage != null)
            {               
                using(Mat dst = new Mat())
                {
                    Cv2.Absdiff(img, backImage, dst);
                    using(Mat gray = dst.CvtColor(ColorConversionCodes.BGR2GRAY))
                    {
                        Cv2.Threshold(gray, gray, 120, 255, ThresholdTypes.Binary);
                        Cv2.ImShow("diff", gray);
                        Cv2.WaitKey(1);
                    }
                }
            }

        }

        private void button1_Click(object sender, EventArgs e)
        {
            //背景画像格納
            backImage = picImage.Clone();
        }
    }
}


VB.NET

Imports OpenCvSharp
Imports OpenCvSharp.Extensions

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Task.Run(Sub()
                     Using v As New VideoCapture(0)
                         Using img As New Mat
                             Do
                                 v.Read(img)
                                 Process(img)
                                 PictureBox1.Image = img.ToBitmap
                             Loop
                         End Using
                     End Using
                 End Sub)
    End Sub

    Private picImage As Mat
    Private backImage As Mat

    Private Sub Process(ByVal img As Mat)

        'カメラ画像格納
        If picImage Is Nothing Then
            picImage = img.Clone
        Else
            img.CopyTo(picImage)
        End If

        '背景差分
        If backImage IsNot Nothing Then
            Using dst As New Mat
                Cv2.Absdiff(img, backImage, dst)
                Using gray As Mat = dst.CvtColor(ColorConversionCodes.BGR2GRAY)
                    Cv2.Threshold(gray, gray, 120, 255, ThresholdTypes.Binary)
                    Cv2.ImShow("diff", gray)
                    Cv2.WaitKey(1)
                End Using
            End Using
        End If

    End Sub

    '背景画像格納
    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        backImage = picImage.Clone
    End Sub

End Class


C#/VB.NETで画像処理⑬<テンプレートマッチング>

こんにちは、SKです。
f:id:SKProgramLab:20200310203357p:plain:w100

C#/VB.NETで画像処理シリーズの第13弾。
画像照合の代表的な手法であるテンプレートマッチングを行う方法を紹介します。

PictureBox上のマウスドラッグ操作によりテンプレート画像を作成し、
カメラ画像内でテンプレート画像を捜索し、マッチングした位置に矩形と照合値を描画します。

テンプレートマッチングについて

labs.eecs.tottori-u.ac.jp

OpenCVSharpとは?

skprogramlab.hatenablog.com

(前回)画像の指定色抽出

skprogramlab.hatenablog.com

動画手順

youtu.be

①PictureBoxのMouseDown/MouseMove/MouseUpイベントを作成する

PictureBoxのMouse関連の3つのイベントを使い、マウスドラッグによって囲われた矩形からテンプレート画像を作成します。

まずは、ドラッグの開始/終了座標を格納する変数dragStartPoint/dragEndPointと、カメラ画像とテンプレート画像を格納する変数picImagetemplateを定義します。

private Point dragStartPoint;
private Point dragEndPoint;
private Mat picImage;
private Mat template;


前回までと同様の手順で、MouseDown、MouseMove、MouseUpイベントを作成します。
・MouseDown:ドラッグ開始点を格納

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
    dragStartPoint = new Point(e.X, e.Y);
}

・MouseMove:ドラッグ終了点を格納

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    dragEndPoint = new Point(e.X, e.Y);
}

・MouseUp:ドラッグ矩形からテンプレート画像を作成

private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
    //テンプレート画像作成
    if (dragStartPoint.X > 0 && dragStartPoint.Y > 0 && dragEndPoint.X > 0 && dragEndPoint.Y > 0)
    {
        Rect r = Cv2.BoundingRect(new Point[] { dragStartPoint, dragEndPoint });
        if(picImage != null)
        {
            template = picImage.SubMat(r).Clone();
            Cv2.ImShow("template", template);
            Cv2.WaitKey(1);
        }
    }
    dragStartPoint = new Point(0,0);
    dragEndPoint = new Point(0, 0);
}

Cv2.BoundingRect関数により、ドラッグ開始点/終了点から矩形を作成しています。

②カメラ画像を変数に格納する

カメラ画像をMouseUpイベントから参照できるようにするため、クラス変数picImageに格納します。

//画像格納
if(picImage == null)
{
    picImage = img.Clone();
}
else
{
    img.CopyTo(picImage);
}

初回のみpicImage==nullのため、画像をClone関数で複製したものを代入します。
ただし、このClone関数は別のメモリ上に画像データを複製するため、都度メモリを解放しないとメモリを消費し続けエラーで落ちてしまうことがあります。

そのため、2回目以降はCopyTo関数を使っています。
CopyTo関数は同じメモリ内に画像データを上書きする処理であるため、メモリを無駄に消費せずに済むので、可能な限りこちらを使うべきです。

③テンプレートマッチング処理を行う

MatchTemplate関数を使用し、カメラ画像からテンプレート画像をサーチします。
結果は、座標を1pxずつずらしながら照合値を計算した結果が格納されたMat型の行列として得られます。

MinMaxLoc関数によって、最も合致した照合値と座標を抜き出し、閾値(0.7)以上であれば、その位置に矩形と値を青色で描画します。

//テンプレートマッチング
if (template != null)
{
    using(Mat result = new Mat())
    {
        Cv2.MatchTemplate(img, template, result, TemplateMatchModes.CCoeffNormed); //マッチング処理
        double minVal, maxVal;
        Point minLoc, maxLoc;
        Cv2.MinMaxLoc(result, out minVal, out maxVal, out minLoc, out maxLoc); //最大値と座標を取得
        if (maxVal >= 0.7)
        {
            //矩形と値を描画
            img.Rectangle(new Rect(maxLoc, template.Size()), Scalar.Blue, 2);
            img.PutText(maxVal.ToString(), maxLoc, HersheyFonts.HersheyDuplex, 1, Scalar.Blue);
        }
    }
}


④ドラッグ矩形描画

最後に、マウスドラッグ中の矩形を緑色(Lime)で描画します。

//ドラッグ矩形描画
if (dragStartPoint.X > 0 && dragStartPoint.Y > 0 && dragEndPoint.X > 0 && dragEndPoint.Y > 0)
{
    Rect r = Cv2.BoundingRect(new Point[] { dragStartPoint, dragEndPoint });
    img.Rectangle(r, Scalar.Lime, 2);
}



実行します。
マウスドラッグでテンプレート画像を登録
f:id:SKProgramLab:20200419231136p:plain

検知中の表示
f:id:SKProgramLab:20200419231304p:plain

最後に、コード全文を載せておきます。
C#

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                using (VideoCapture v = new VideoCapture(0))
                using (Mat img = new Mat())
                {
                    while (true)
                    {
                        v.Read(img);
                        Process(img);
                        pictureBox1.Image = BitmapConverter.ToBitmap(img);
                    }
                }
            });
        }

        private Point dragStartPoint;
        private Point dragEndPoint;
        private Mat picImage;
        private Mat template;

        private void Process(Mat img)        
        {
            //画像格納
            if(picImage == null)
            {
                picImage = img.Clone();
            }
            else
            {
                img.CopyTo(picImage);
            }

            //テンプレートマッチング
            if (template != null)
            {
                using(Mat result = new Mat())
                {
                    Cv2.MatchTemplate(img, template, result, TemplateMatchModes.CCoeffNormed); //マッチング処理
                    double minVal, maxVal;
                    Point minLoc, maxLoc;
                    Cv2.MinMaxLoc(result, out minVal, out maxVal, out minLoc, out maxLoc); //最大値と座標を取得
                    if (maxVal >= 0.7)
                    {
                        //矩形と値を描画
                        img.Rectangle(new Rect(maxLoc, template.Size()), Scalar.Blue, 2);
                        img.PutText(maxVal.ToString(), maxLoc, HersheyFonts.HersheyDuplex, 1, Scalar.Blue);
                    }
                }
            }

            //ドラッグ矩形描画
            if (dragStartPoint.X > 0 && dragStartPoint.Y > 0 && dragEndPoint.X > 0 && dragEndPoint.Y > 0)
            {
                Rect r = Cv2.BoundingRect(new Point[] { dragStartPoint, dragEndPoint });
                img.Rectangle(r, Scalar.Lime, 2);
            }

        }

        private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
        {
            dragStartPoint = new Point(e.X, e.Y);
        }

        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            dragEndPoint = new Point(e.X, e.Y);
        }

        private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
        {
            //テンプレート画像作成
            if (dragStartPoint.X > 0 && dragStartPoint.Y > 0 && dragEndPoint.X > 0 && dragEndPoint.Y > 0)
            {
                Rect r = Cv2.BoundingRect(new Point[] { dragStartPoint, dragEndPoint });
                if(picImage != null)
                {
                    template = picImage.SubMat(r).Clone();
                    Cv2.ImShow("template", template);
                    Cv2.WaitKey(1);
                }
            }
            dragStartPoint = new Point(0,0);
            dragEndPoint = new Point(0, 0);
        }
    }
}


VB.NET

Imports OpenCvSharp
Imports OpenCvSharp.Extensions

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Task.Run(Sub()
                     Using v As New VideoCapture(0)
                         Using img As New Mat
                             Do
                                 v.Read(img)
                                 Process(img)
                                 PictureBox1.Image = img.ToBitmap
                             Loop
                         End Using
                     End Using
                 End Sub)
    End Sub

    Private dragStartPoint As Point
    Private dragEndPoint As Point
    Private picImage As Mat
    Private template As Mat

    Private Sub Process(ByVal img As Mat)

        '画像格納
        If picImage Is Nothing Then
            picImage = img.Clone
        Else
            img.CopyTo(picImage)
        End If

        'テンプレートマッチング
        If template IsNot Nothing Then
            Using result As New Mat
                Cv2.MatchTemplate(img, template, result, TemplateMatchModes.CCoeffNormed)
                Dim maxVal As Double
                Dim maxLoc As Point
                Cv2.MinMaxLoc(result, Nothing, maxVal, Nothing, maxLoc)
                If maxVal >= 0.7 Then
                    img.Rectangle(New Rect(maxLoc, template.Size), Scalar.Blue, 2)
                    img.PutText(maxVal.ToString, maxLoc, HersheyFonts.HersheyDuplex, 1, Scalar.Blue)
                End If
            End Using
        End If

        'ドラッグ矩形描画
        If dragStartPoint.X > 0 AndAlso dragStartPoint.Y > 0 AndAlso dragEndPoint.X > 0 AndAlso dragEndPoint.Y > 0 Then
            Dim r As Rect = Cv2.BoundingRect(New Point() {dragStartPoint, dragEndPoint})
            img.Rectangle(r, Scalar.Lime, 2)
        End If

    End Sub

    Private Sub PictureBox1_MouseDown(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseDown
        dragStartPoint = New Point(e.X, e.Y)
    End Sub

    Private Sub PictureBox1_MouseMove(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseMove
        dragEndPoint = New Point(e.X, e.Y)
    End Sub

    Private Sub PictureBox1_MouseUp(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseUp
        'テンプレート画像作成
        If dragStartPoint.X > 0 AndAlso dragStartPoint.Y > 0 AndAlso dragEndPoint.X > 0 AndAlso dragEndPoint.Y > 0 Then
            Dim r As Rect = Cv2.BoundingRect(New Point() {dragStartPoint, dragEndPoint})
            If picImage IsNot Nothing Then
                template = picImage.SubMat(r).Clone
                Cv2.ImShow("template", template)
                Cv2.WaitKey(1)
            End If
        End If
        dragStartPoint = New Point(0, 0)
        dragEndPoint = New Point(0, 0)
    End Sub

End Class


C#/VB.NETで画像処理⑫<画像の指定色抽出>

こんにちは、SKです。
f:id:SKProgramLab:20200310203357p:plain:w100

C#/VB.NETで画像処理シリーズの第12弾。
カメラ画像のピクセルにアクセスし、指定した色のピクセルのみ抽出する方法を紹介します。

PictureBox上でマウスをクリックすると、その座標のR,G,B情報を取得し、
カメラ画像内で指定した色に近いピクセルを抜き出し、別のMat画像として表示します。

OpenCVSharpとは?

skprogramlab.hatenablog.com

(前回)画像のピクセルアクセス

skprogramlab.hatenablog.com

動画手順

youtu.be

①色表示Labelを配置する

フォーム上にLabelを配置します。
前回の記事で作成したラベルをそのまま使ってOKです。
f:id:SKProgramLab:20200416214115p:plain

②PictureBoxのMouseDownイベントを作成する

PictureBox上のマウスクリック座標取得には、MouseDownイベントを使用します。
PictureBoxのプロパティ→"イベント"(⚡アイコン)を選択→MouseDownの欄をダブルクリックし、コード上にMouseDownイベントを作成します。
f:id:SKProgramLab:20200417232909p:plain

f:id:SKProgramLab:20200417233029p:plain

指定色を格納するScalar型の変数cursorColorを定義し、MouseDownイベント内で下記のようにマウス座標の色取得とラベル表示を行います。

PictureBox画像をBitmapに変換 ➡ GetPixelで指定座標の色取得 ➡ 変数に格納+ラベルにテキスト表示

private Scalar cursorColor;

private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
    Bitmap bmp = (Bitmap)pictureBox1.Image;
    Color c = bmp.GetPixel(e.X, e.Y);
    cursorColor = new Scalar(c.B, c.G, c.R);
    label1.Text = "R:" + c.R + ",G:" + c.G + ",B:" + c.B;
    label1.BackColor = c;
}


③指定色ピクセルを抽出する

前回同様、Mat画像のピクセルアクセスにはMatIndexerを使用します。
今回は元画像と抽出画像で2種類のIndexerを定義します。

二重Forループで各ピクセルの色情報を取得し、②の指定色のR,G,Bの値が全て近い(差が20以下の)ピクセルのみ抜き出し、その色を抽出画像のMatに書き込んでいきます。

private void Process(Mat img)        
{
    //元画像アクセス用
    var mat3_src = new Mat<Vec3b>(img);
    var indexer_src = mat3_src.GetIndexer();

    //抽出画像アクセス用
    Mat pixelImage = new Mat(img.Size(), MatType.CV_8UC3, Scalar.Black);
    var mat3_dst = new Mat<Vec3b>(pixelImage);
    var indexer_dst = mat3_dst.GetIndexer();

    //ピクセルアクセス
    for (int y = 0; y < img.Height; y++)
    {
        for (int x = 0; x < img.Width; x++)
        {
            Vec3b pixel = indexer_src[y, x];
            if(Math.Abs(pixel.Item0 - cursorColor.Val0) <= 20 && 
               Math.Abs(pixel.Item1 - cursorColor.Val1) <= 20 && 
               Math.Abs(pixel.Item2 - cursorColor.Val2) <= 20)
            {
                indexer_dst[y, x] = pixel;
            }
        }
    }
    Cv2.ImShow("pixel", pixelImage);
    Cv2.WaitKey(1);
}

Math.Abs関数は、差の絶対値を計算する関数です。

実行します。
白っぽいところ
f:id:SKProgramLab:20200417235020p:plain

灰色っぽいところ
f:id:SKProgramLab:20200417235152p:plain

赤っぽいところ
f:id:SKProgramLab:20200417235348p:plain

最後に、コード全文を載せておきます。
C#

using System;
using System.Drawing;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                using (VideoCapture v = new VideoCapture(0))
                using (Mat img = new Mat())
                {
                    while (true)
                    {
                        v.Read(img);
                        Process(img);
                        pictureBox1.Image = BitmapConverter.ToBitmap(img);
                    }
                }
            });
        }

        private Scalar cursorColor;

        private void Process(Mat img)        
        {
            //元画像アクセス用
            var mat3_src = new Mat<Vec3b>(img);
            var indexer_src = mat3_src.GetIndexer();

            //抽出画像アクセス用
            Mat pixelImage = new Mat(img.Size(), MatType.CV_8UC3, Scalar.Black);
            var mat3_dst = new Mat<Vec3b>(pixelImage);
            var indexer_dst = mat3_dst.GetIndexer();

            //ピクセルアクセス
            for (int y = 0; y < img.Height; y++)
            {
                for (int x = 0; x < img.Width; x++)
                {
                    Vec3b pixel = indexer_src[y, x];
                    if(Math.Abs(pixel.Item0 - cursorColor.Val0) <= 20 && 
                       Math.Abs(pixel.Item1 - cursorColor.Val1) <= 20 && 
                       Math.Abs(pixel.Item2 - cursorColor.Val2) <= 20)
                    {
                        indexer_dst[y, x] = pixel;
                    }
                }
            }
            Cv2.ImShow("pixel", pixelImage);
            Cv2.WaitKey(1);
        }

        private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
        {
            Bitmap bmp = (Bitmap)pictureBox1.Image;
            Color c = bmp.GetPixel(e.X, e.Y);
            cursorColor = new Scalar(c.B, c.G, c.R);
            label1.Text = "R:" + c.R + ",G:" + c.G + ",B:" + c.B;
            label1.BackColor = c;
        }
    }
}


VB.NET

Imports OpenCvSharp
Imports OpenCvSharp.Extensions

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Task.Run(Sub()
                     Using v As New VideoCapture(0)
                         Using img As New Mat
                             Do
                                 v.Read(img)
                                 Process(img)
                                 PictureBox1.Image = img.ToBitmap
                             Loop
                         End Using
                     End Using
                 End Sub)
    End Sub

    Private cursorColor As Scalar = Nothing

    Private Sub Process(ByVal img As Mat)

        '元画像アクセス用
        Dim indexer_src As MatIndexer(Of Vec3b) = img.GetGenericIndexer(Of Vec3b)()

        '抽出画像アクセス用
        Dim pixelImage As New Mat(img.Size, MatType.CV_8UC3, Scalar.Black)
        Dim indexer_dst As MatIndexer(Of Vec3b) = pixelImage.GetGenericIndexer(Of Vec3b)()

        'ピクセルアクセス
        For y = 0 To img.Height
            For x = 0 To img.Width
                Dim pixel As Vec3b = indexer_src(y, x)
                If Math.Abs(pixel.Item0 - cursorColor.Val0) <= 20 AndAlso
                   Math.Abs(pixel.Item1 - cursorColor.Val1) <= 20 AndAlso
                   Math.Abs(pixel.Item2 - cursorColor.Val2) <= 20 Then
                    indexer_dst(y, x) = pixel
                End If
            Next
        Next
        Cv2.ImShow("pixel", pixelImage)
        Cv2.WaitKey(1)
    End Sub

    Private Sub PictureBox1_MouseDown(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseDown
        Dim bmp As Bitmap = PictureBox1.Image
        Dim c As Color = bmp.GetPixel(e.X, e.Y)
        cursorColor = New Scalar(c.B, c.G, c.R)
        Label1.Text = "R:" & c.R & ",G:" & c.G & ",B:" & c.B
        Label1.BackColor = c
    End Sub

End Class


C#/VB.NETで画像処理⑪<画像のピクセルアクセス>

こんにちは、SKです。
f:id:SKProgramLab:20200310203357p:plain:w100

C#/VB.NETで画像処理シリーズの第11弾。
カメラ画像のピクセルにアクセスし、色情報を取り出す方法を紹介します。

PictureBox上にマウスカーソルを合わせると、その座標のR,G,B情報を取得し、
画面上のLabelに表示するプログラムを作っていきます。

OpenCVSharpとは?

skprogramlab.hatenablog.com

(前回)画像のヒストグラム作成

skprogramlab.hatenablog.com

動画手順

youtu.be

①色表示Labelを配置する

フォーム上にLabelを配置します。
見やすいようフォントサイズを大きくしておきます(20pt)。
f:id:SKProgramLab:20200416214115p:plain
また、今回はMat画像とPictureBoxのサイズを合わせるため、PictureBoxのDockプロパティをNoneにし、サイズを640×480にします。
f:id:SKProgramLab:20200416221004p:plain

②PictureBoxのMouseMoveイベントを作成する

PictureBox上のマウスカーソルの座標取得には、MouseMoveイベントを使用します。
Formのデザイナー画面→プロパティ→"イベント"(⚡アイコン)を選択→MouseMoveの欄をダブルクリックすると、コード上にMouseMoveイベントが生成されます。
f:id:SKProgramLab:20200416214335p:plain

f:id:SKProgramLab:20200416214533p:plain

マウス座標を格納するPoint型の変数cursorPointを定義し、MouseMoveイベントの第2引数eに格納されている座標X,Yを代入します。

private Point cursorPoint;

private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
    cursorPoint = new Point(e.X, e.Y);
}


ピクセルの色情報を取得する

Mat画像のピクセルアクセス方法にはいくつか種類があり、OpenCVSharpのWikiに記載があります。
Accessing Pixel · shimat/opencvsharp Wiki · GitHub
↑のWikiには他にも役立つサンプルコードがあります。要チェック!

今回は、最も高速にアクセスできるらしいTypeSpecificMatという方法を採用します。
f:id:SKProgramLab:20200416212541p:plain

Indexerの引数はY, Xの順に指定することに注意。

private void Process(Mat img)
{
    //ピクセル情報取得
    var mat3 = new Mat<Vec3b>(img);
    var indexer = mat3.GetIndexer();
    Vec3b color = indexer[cursorPoint.Y, cursorPoint.X];
}

Vec3bデータからR,G,Bの値を取り出し、Labelに表示します。
また、その取得した色をLabelの背景色に指定しています。

private void Process(Mat img)
{
    //ピクセル情報取得
    var mat3 = new Mat<Vec3b>(img);
    var indexer = mat3.GetIndexer();
    Vec3b pixel = indexer[cursorPoint.Y, cursorPoint.X];

    //Labelに表示
    this.BeginInvoke((Action)(() =>
    {
        label1.Text = "R:" + pixel.Item2 + ",G:" + pixel.Item1 + ",B:" + pixel.Item0;
        label1.BackColor = System.Drawing.Color.FromArgb(pixel.Item2, pixel.Item1, pixel.Item0);
    }));
}

ここでのポイントは、上記コードのようにBeginInvoke経由で文字列を指定すること。

このようにする理由は、Process関数はTask.Runによってサブスレッド処理になっているのに対し、LabelなどのUIに関わる操作はメインスレッドで行わなければならない、というのが.NETの仕様であるからです。
InvokeせずそのままLabelを操作しようとすると、例外が発生します。

C#/VB.NETでは、InvokeやBeginInvokeを使うと、一部の処理をメインスレッド側に戻すことができます。

実行します。
白っぽいところ=151,157,157
f:id:SKProgramLab:20200416220515p:plain

赤っぽいところ=107,20,22
f:id:SKProgramLab:20200416220714p:plain


最後に、コード全文を載せておきます。
C#

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                using (VideoCapture v = new VideoCapture(0))
                using (Mat img = new Mat())
                {
                    while (true)
                    {
                        v.Read(img);
                        Process(img);
                        pictureBox1.Image = BitmapConverter.ToBitmap(img);
                    }
                }
            });
        }

        private Point cursorPoint;

        private void Process(Mat img)
        {
            //ピクセル情報取得
            var mat3 = new Mat<Vec3b>(img);
            var indexer = mat3.GetIndexer();
            Vec3b pixel = indexer[cursorPoint.Y, cursorPoint.X];

            //Labelに表示
            this.BeginInvoke((Action)(() =>
            {
                label1.Text = "R:" + pixel.Item2 + ",G:" + pixel.Item1 + ",B:" + pixel.Item0;
                label1.BackColor = System.Drawing.Color.FromArgb(pixel.Item2, pixel.Item1, pixel.Item0);
            }));
        }

        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            cursorPoint = new Point(e.X, e.Y);
        }

    }
}


VB.NET
何故かTypeSpecificMat (faster)は構文エラーで動かず。
下記はGenericIndexer (reasonably fast)のサンプルです。

Imports OpenCvSharp
Imports OpenCvSharp.Extensions

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Task.Run(Sub()
                     Using v As New VideoCapture(0)
                         Using img As New Mat
                             Do
                                 v.Read(img)
                                 Process(img)
                                 PictureBox1.Image = img.ToBitmap
                             Loop
                         End Using
                     End Using
                 End Sub)
    End Sub

    Private cursorPoint As Point = Nothing

    Private Sub Process(ByVal img As Mat)
        'Dim mat3 As New Mat(Of Vec3b)(img) '何故かエラー
        'Dim indexer As MatIndexer(Of Vec3b) = mat3.GetIndexer
        Dim indexer = img.GetGenericIndexer(Of Vec3b)()
        Dim pixel As Vec3b = indexer(cursorPoint.Y, cursorPoint.X)
        Me.BeginInvoke(Sub()
                           Label1.Text = "R:" & pixel.Item2 & ",G:" & pixel.Item1 & ",B:" & pixel.Item0
                           Label1.BackColor = Color.FromArgb(pixel.Item2, pixel.Item1, pixel.Item0)
                       End Sub)
    End Sub

    Private Sub PictureBox1_MouseMove(sender As Object, e As MouseEventArgs) Handles PictureBox1.MouseMove
        cursorPoint = New Point(e.X, e.Y)
    End Sub

End Class


C#/VB.NETで画像処理⑩<画像のヒストグラム作成>

こんにちは、SKです。
f:id:SKProgramLab:20200310203357p:plain:w100

C#/VB.NETで画像処理シリーズの第10弾。
カメラ画像のヒストグラムを作成します。

画像のヒストグラムとは、各ピクセルの輝度値の分布を表すグラフのこと。
画像同士の照合等に使われるこのヒストグラムOpenCVでは簡単に計算できます。

OpenCVSharpとは?

skprogramlab.hatenablog.com

(前回)画像のラベリング処理

skprogramlab.hatenablog.com

動画手順

youtu.be

ヒストグラムを作成する

OpenCVSharpのCalcHist関数でヒストグラムを作成します。

CalcHist関数

ヒストグラム — opencv 2.2 documentation
ヒストグラム その1: 計算して,プロットして,解析する !!! — OpenCV-Python Tutorials 1 documentation
・第1引数:入力画像の配列:Mat配列
・第2引数:画像チャンネルのインデックス:int配列
・第3引数:マスク画像:Mat
・第4引数:出力ヒストグラム:Mat
・第5引数:ヒストグラム次元数:int
・第6引数:ヒストグラムサイズ:int配列
・第7引数:計測する画素値の範囲:Rangef配列

引数が多く、入力画像を配列で指定する必要があるなど、やや複雑です。
B,G,Rそれぞれについて、下記のようにヒストグラムを計算します。

//ヒストグラム計算
Mat b_hist = new Mat();
Mat g_hist = new Mat();
Mat r_hist = new Mat();
Cv2.CalcHist(new Mat[] { img }, new int[] { 0 }, null, b_hist, 1, new int[] { 256 }, new Rangef[] { new Rangef(0, 256) });
Cv2.CalcHist(new Mat[] { img }, new int[] { 1 }, null, g_hist, 1, new int[] { 256 }, new Rangef[] { new Rangef(0, 256) });
Cv2.CalcHist(new Mat[] { img }, new int[] { 2 }, null, r_hist, 1, new int[] { 256 }, new Rangef[] { new Rangef(0, 256) });

f:id:SKProgramLab:20200412223516p:plain

得られたヒストグラムはMat型ですが、中身は32bitの小数値が格納された1次元配列(サイズ=256)です。
f:id:SKProgramLab:20200412224120p:plain

ヒストグラムを正規化する

ヒストグラムを線グラフで描画する用にMat画像histImage(512×400)を定義します。
そして、OpenCVSharpのNormalize関数でヒストグラムを正規化します。
今回は、0~400(histImageの高さ)の範囲で正規化することにします。
配列操作 — opencv 2.2 documentation
・第1引数:入力画像:Mat
・第2引数:出力画像:Mat
・第3引数:正規化範囲の下界:Mat
・第4引数:正規化範囲の上界:Mat
・第5引数:正規化の種類:NormTypes

//ヒストグラム画像
Mat histImage = new Mat(400, 512, MatType.CV_8UC3, Scalar.Black);

//正規化
Cv2.Normalize(b_hist, b_hist, 0, histImage.Height, NormTypes.MinMax);
Cv2.Normalize(g_hist, g_hist, 0, histImage.Height, NormTypes.MinMax);
Cv2.Normalize(r_hist, r_hist, 0, histImage.Height, NormTypes.MinMax);

f:id:SKProgramLab:20200412225209p:plain

ヒストグラム描画関数を定義する

B,G,Rそれぞれのヒストグラムを線グラフとして描画するための関数DrawHistを定義します。
forループでヒストグラム内の値とその次の値を取り出し、画像上の座標を計算し、Cv2.Line関数を使い線で繋いでいます。

private void DrawHist(Mat histImage, Mat hist, Scalar color)
{
    int bin = histImage.Width / 256;
    for(int i = 0; i < 255; i++)
    {
        float value1 = hist.At<float>(i, 0);
        float value2 = hist.At<float>(i+1, 0);
        Point pt1 = new Point(i * bin, histImage.Height - value1);
        Point pt2 = new Point((i + 1) * bin, histImage.Height - value2);
        Cv2.Line(histImage, pt1, pt2, color, 2);
    }
}

f:id:SKProgramLab:20200412225804p:plain

ヒストグラム描画関数を呼び出す

③で定義したDrawHist関数をB、G、Rそれぞれのヒストグラムについて呼び出して描画した後、Cv2.ImShow関数で表示します。

//ヒストグラム表示
DrawHist(histImage, b_hist, Scalar.Blue);
DrawHist(histImage, g_hist, Scalar.Lime);
DrawHist(histImage, r_hist, Scalar.Red);
Cv2.ImShow("Histogram", histImage);
Cv2.WaitKey(1);

f:id:SKProgramLab:20200412230321p:plain

実行します。
f:id:SKProgramLab:20200412230444p:plain
カメラ画像と、ヒストグラムのグラフが表示されました。


最後に、コード全文を載せておきます。
C#

using System;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                using (VideoCapture v = new VideoCapture(0))
                using (Mat img = new Mat())
                {
                    while (true)
                    {
                        v.Read(img);
                        Process(img);
                        pictureBox1.Image = BitmapConverter.ToBitmap(img);
                    }
                }
            });
        }

        private void Process(Mat img)
        {
            //ヒストグラム計算
            Mat b_hist = new Mat();
            Mat g_hist = new Mat();
            Mat r_hist = new Mat();
            Cv2.CalcHist(new Mat[] { img }, new int[] { 0 }, null, b_hist, 1, new int[] { 256 }, new Rangef[] { new Rangef(0, 256) });
            Cv2.CalcHist(new Mat[] { img }, new int[] { 1 }, null, g_hist, 1, new int[] { 256 }, new Rangef[] { new Rangef(0, 256) });
            Cv2.CalcHist(new Mat[] { img }, new int[] { 2 }, null, r_hist, 1, new int[] { 256 }, new Rangef[] { new Rangef(0, 256) });

            //ヒストグラム画像
            Mat histImage = new Mat(400, 512, MatType.CV_8UC3, Scalar.Black);

            //正規化
            Cv2.Normalize(b_hist, b_hist, 0, histImage.Height, NormTypes.MinMax);
            Cv2.Normalize(g_hist, g_hist, 0, histImage.Height, NormTypes.MinMax);
            Cv2.Normalize(r_hist, r_hist, 0, histImage.Height, NormTypes.MinMax);

            //ヒストグラム表示
            DrawHist(histImage, b_hist, Scalar.Blue);
            DrawHist(histImage, g_hist, Scalar.Lime);
            DrawHist(histImage, r_hist, Scalar.Red);
            Cv2.ImShow("Histogram", histImage);
            Cv2.WaitKey(1);
        }

        private void DrawHist(Mat histImage, Mat hist, Scalar color)
        {
            int bin = histImage.Width / 256;
            for(int i = 0; i < 255; i++)
            {
                float value1 = hist.At<float>(i, 0);
                float value2 = hist.At<float>(i+1, 0);
                Point pt1 = new Point(i * bin, histImage.Height - value1);
                Point pt2 = new Point((i + 1) * bin, histImage.Height - value2);
                Cv2.Line(histImage, pt1, pt2, color, 2);
            }
        }

    }
}


VB.NET

Imports OpenCvSharp
Imports OpenCvSharp.Extensions

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Task.Run(Sub()
                     Using v As New VideoCapture(0)
                         Using img As New Mat
                             Do
                                 v.Read(img)
                                 Process(img)
                                 PictureBox1.Image = img.ToBitmap
                             Loop
                         End Using
                     End Using
                 End Sub)
    End Sub

    Private Sub Process(ByVal img As Mat)

        'ヒストグラム計算
        Dim b_hist As New Mat()
        Dim g_hist As New Mat()
        Dim r_hist As New Mat()
        Cv2.CalcHist(New Mat() {img}, New Integer() {0}, Nothing, b_hist, 1, New Integer() {256}, New Rangef() {New Rangef(0, 256)})
        Cv2.CalcHist(New Mat() {img}, New Integer() {1}, Nothing, g_hist, 1, New Integer() {256}, New Rangef() {New Rangef(0, 256)})
        Cv2.CalcHist(New Mat() {img}, New Integer() {2}, Nothing, r_hist, 1, New Integer() {256}, New Rangef() {New Rangef(0, 256)})

        'ヒストグラム画像
        Dim histImage As Mat = New Mat(400, 512, MatType.CV_8UC3, Scalar.Black)

        '正規化
        Cv2.Normalize(b_hist, b_hist, 0, histImage.Height, NormTypes.MinMax)
        Cv2.Normalize(g_hist, g_hist, 0, histImage.Height, NormTypes.MinMax)
        Cv2.Normalize(r_hist, r_hist, 0, histImage.Height, NormTypes.MinMax)

        'ヒストグラム表示
        DrawHist(histImage, b_hist, Scalar.Blue)
        DrawHist(histImage, g_hist, Scalar.Lime)
        DrawHist(histImage, r_hist, Scalar.Red)
        Cv2.ImShow("Histogram", histImage)
        Cv2.WaitKey(1)
    End Sub

    Private Sub DrawHist(ByVal histImage As Mat, ByVal hist As Mat, ByVal color As Scalar)
        Dim bin As Integer = histImage.Width / 256
        For i = 0 To 254
            Dim value1 As Single = hist.At(Of Single)(i, 0)
            Dim value2 As Single = hist.At(Of Single)(i + 1, 0)
            Dim pt1 As New Point(i * bin, histImage.Height - value1)
            Dim pt2 As New Point((i + 1) * bin, histImage.Height - value2)
            Cv2.Line(histImage, pt1, pt2, color, 2)
        Next
    End Sub

End Class


C#/VB.NETで画像処理⑨<画像のラベリング処理>

こんにちは、SKです。
f:id:SKProgramLab:20200310203357p:plain:w100

C#/VB.NETで画像処理シリーズの第9弾。
カメラ画像のラベリング処理を実装します。

ラベリングとは、二値化画像の白領域の連結領域を抽出する処理のことです。前回の輪郭抽出処理と似ていますが、ラベリングは各領域の面積が簡単に取り出せたり、使い勝手が良い関数です。

OpenCVSharpの作者さんも好きだと公言しているのがこのラベリング。下記の記事は本当に参考になります。

★OpenCVSharpのラベリングについて

schima.hatenablog.com

OpenCVSharpとは?

skprogramlab.hatenablog.com

(前回)画像の輪郭抽出

skprogramlab.hatenablog.com

動画手順

youtu.be

①ラベリングボタンを配置する

フォーム上にボタンを配置します。
このボタンの押下イベントで、ラベリングフラグのTrue/Falseを切り替えます。フラグがTrueの時に、Process関数内で画像の二値化→ラベリング処理を行い、各ラベリング領域に矩形を描画してPictureBoxに表示します。 表示テキストは"Labeling"。 f:id:SKProgramLab:20200407220902p:plain

ラベリングフラグのbool変数isLabelingを追加します。
f:id:SKProgramLab:20200407220951p:plain

②画像を二値化する

まずは前回同様、画像を二値化し、Mat変数grayに格納します。 f:id:SKProgramLab:20200407221125p:plain
Usingで定義することを忘れずに。

(参考)Usingについて

C# Tips −usingを使え、使えったら使え(^^)−

③ラベリング処理

ラベリングには、OpenCVSharpのConnectedComponentsEx関数を使用します。これは、OpenCVのConnectedComponents関数をより使いやすくShimat先生が改良した独自の関数です。

ConnectedComponentsEx関数

OpenCvSharpをつかう その24 (OpenCV 3.0のラベリング) - schima.hatenablog.com
引数には二値化画像を渡すのみ。そして戻り値のラベリングデータBlobsをForループで回し1つずつアクセスできます。超簡単!

Cv2.Rectangle関数で各領域の矩形を描画します。

private void Process(Mat img)
{
    if (isLabeling)
    {
        using (Mat gray = new Mat(img.Size(), MatType.CV_8UC1))
        {
            Cv2.CvtColor(img, gray, ColorConversionCodes.BGR2GRAY);
            Cv2.Threshold(gray, gray, 0, 255, ThresholdTypes.Otsu);
            ConnectedComponents cc = Cv2.ConnectedComponentsEx(gray);
            foreach (var blob in cc.Blobs.Skip(1))
            {
                img.Rectangle(blob.Rect, Scalar.Lime, 2);
            }
        }
    }
}

f:id:SKProgramLab:20200407223625p:plain

実行します。
起動時 f:id:SKProgramLab:20200406220559p:plain
Contours押下時
f:id:SKProgramLab:20200407223716p:plain

最後に、コード全文を載せておきます。
C#

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using OpenCvSharp;
using OpenCvSharp.Extensions;

namespace WindowsFormsApp1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            Task.Run(() =>
            {
                using (VideoCapture v = new VideoCapture(0))
                using (Mat img = new Mat())
                {
                    while (true)
                    {
                        v.Read(img);
                        Process(img);
                        pictureBox1.Image = BitmapConverter.ToBitmap(img);
                    }
                }
            });
        }

        private bool isLabeling = false;

        private void Process(Mat img)
        {
            if (isLabeling)
            {
                using (Mat gray = new Mat(img.Size(), MatType.CV_8UC1))
                {
                    Cv2.CvtColor(img, gray, ColorConversionCodes.BGR2GRAY);
                    Cv2.Threshold(gray, gray, 0, 255, ThresholdTypes.Otsu);
                    ConnectedComponents cc = Cv2.ConnectedComponentsEx(gray);
                    foreach (var blob in cc.Blobs.Skip(1))
                    {
                        img.Rectangle(blob.Rect, Scalar.Lime, 2);
                    }
                }
            }
        }

        private void button1_Click(object sender, EventArgs e)
        {
            isLabeling = !isLabeling;
        }
    }
}


VB.NET

Imports OpenCvSharp
Imports OpenCvSharp.Extensions

Public Class Form1

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        Task.Run(Sub()
                     Using v As New VideoCapture(0)
                         Using img As New Mat
                             Do
                                 v.Read(img)
                                 Process(img)
                                 PictureBox1.Image = img.ToBitmap
                             Loop
                         End Using
                     End Using
                 End Sub)
    End Sub

    Private isLabeling As Boolean = False

    Private Sub Process(ByVal img As Mat)
        If isLabeling Then
            Using gray As New Mat(img.Size, MatType.CV_8UC1)
                Cv2.CvtColor(img, gray, ColorConversionCodes.BGR2GRAY)
                Cv2.Threshold(gray, gray, 0, 255, ThresholdTypes.Otsu)
                Dim cc As ConnectedComponents = Cv2.ConnectedComponentsEx(gray)
                For Each blob As ConnectedComponents.Blob In cc.Blobs.Skip(1)
                    img.Rectangle(blob.Rect, Scalar.Lime, 2)
                Next
            End Using
        End If
    End Sub

    Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
        isLabeling = Not isLabeling
    End Sub

End Class