SKProgramLab

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

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