Share
Embed
AR Fukuoka: OpenCVとRGB-Dセンサで始めるハンドトラッキング
3/2に開催した勉強会資料
https://eventon.jp/16146/
TakashiYoshinaga
March 02, 2019
Transcript
156 slides
-
Formが表示されたらTimerをスタートさせる namespace ARFukuokaCV { public partial class Form1 : Form
{ public Form1() { InitializeComponent(); } private void timer1_Tick(object sender, EventArgs e) { } private void Form1_Shown(object sender, EventArgs e) { timer1.Start(); } } } ①Form1_Shownが追加される ②自分で追加 -
Kinectと接続 using KinectSample; //KinectGrabberの機能をインポート namespace Kinect_ARFukuoka { public partial class
Form1 : Form { KinectGrabber kinect = new KinectGrabber(); public Form1() { InitializeComponent(); } private void timer1_Tick(object sender, EventArgs e) { } /*****以下省略*****/ -
アプリ終了時にKinectを止める KinectGrabber kinect = new KinectGrabber(); public Form1() { InitializeComponent();
} private void timer1_Tick(object sender, EventArgs e) { } private void Form1_Shown(object sender, EventArgs e) { timer1.Start(); } private void Form1_FormClosing(object sender, FormClosingEventArgs e) { timer1.Stop(); kinect.Close(); } -
Kinectの画像を表示 KinectGrabber kinect = new KinectGrabber(); public Form1() { InitializeComponent();
} private void timer1_Tick(object sender, EventArgs e) { Bitmap color = kinect.GetColorImage(); //カラー画像取得 Bitmap depth = kinect.GetDepthImage(); //Depth画像取得 pictureBox1.Image = color; //カラー画像を表示 pictureBox2.Image = depth; //Depth画像を表示 } private void Form1_Shown(object sender, EventArgs e) { timer1.Start(); } -
Depth画像で使用する距離の範囲を指定 KinectGrabber kinect = new KinectGrabber(); public Form1() { InitializeComponent();
} private void timer1_Tick(object sender, EventArgs e) { float maxDepth = 8; Bitmap color = kinect.GetColorImage(); //カラー画像取得 Bitmap depth = kinect.GetDepthImage(); //Depth画像取得 pictureBox1.Image = color; //カラー画像を表示 pictureBox2.Image = depth; //Depth画像を表示 } private void Form1_Shown(object sender, EventArgs e) { timer1.Start(); } Bitmap depth = kinect.GetDepthImage(maxDepth); //Depth画像取得 -
OpenCVの読み込み using KinectSample; //KinectGrabberの機能をインポート using OpenCvSharp; using OpenCvSharp.Extensions; namespace Kinect_ARFukuoka
{ public partial class Form1 : Form { KinectGrabber kinect = new KinectGrabber(); public Form1() { InitializeComponent(); } private void timer1_Tick(object sender, EventArgs e) { } /*****以下省略*****/ -
Bitmap画像をMatに変換 //Depth画像を格納する変数 Mat depthMat; public Form1() { InitializeComponent(); } private
void timer1_Tick(object sender, EventArgs e) { float maxDepth = 0.75f; Bitmap color = kinect.GetColorImage(); Bitmap depth = kinect.GetDepthImage(maxDepth); //BitmapをOpenCV用の画像フォーマットMatに変換 depthMat = BitmapConverter.ToMat(depth); pictureBox1.Image = color; pictureBox2.Image = depth; //メモリを解放 depthMat.Release(); } pictureBox2.Image = depth; pictureBox2.Image = depthMat.ToBitmap(); -
Depth画像に対する二値化処理 //二値化を行うための関数 private void Binarization() { //BGR→GrayScale depthMat = depthMat.CvtColor(ColorConversionCodes.BGR2GRAY);
//しきい値=20で二値化 depthMat = depthMat.Threshold(20, 255, ThresholdTypes.Binary); } private void timer1_Tick(object sender, EventArgs e) { float maxDepth = 0.75f; Bitmap color = kinect.GetColorImage(); Bitmap depth = kinect.GetDepthImage(maxDepth); depthMat = BitmapConverter.ToMat(depth); //二値化処理を呼び出す Binarization(); pictureBox1.Image = color; pictureBox2.Image = depthMat.ToBitmap(); depthMat.Release(); } -
色の抽出をしよう 【やりたいこと】 特定の色の領域を白、それ以外を黒で二値化 【問題点】 色情報はRGBの組み合わせで表現されている RGBでは色の境目を直線的に引くことが困難 【解決策】
色空間としてHSVを使用する H(Hue)は色相で0~360°で表現される ※OpenCVの場合0~180 S(Saturation)は彩度、0~100%で表現。 値が大きいほうがその色の鮮やかさが強い。 ※OpenCVの場合0~255 V(Value)は明度、 0~100%で表現。 値が大きいほど明るい ※OpenCVの場合0~255 RGB HSV -
カラー/二値化画像用のMatの準備 //カラー画像を格納する変数を追加 Mat depthMat; public Form1() { InitializeComponent(); } private
void timer1_Tick(object sender, EventArgs e) { float maxDepth = 0.75f; Bitmap color = kinect.GetColorImage(); Bitmap depth = kinect.GetDepthImage(maxDepth); depthMat = BitmapConverter.ToMat(depth); bgrMat = BitmapConverter.ToMat(color); Binarization(); pictureBox1.Image = color; pictureBox2.Image = depthMat.ToBitmap(); depthMat.Release(); bgrMat.Release(); binMat.Release(); } Mat depthMat, bgrMat, binMat; -
HSV画像に対する二値化 //HSVの閾値の最小値・最大値 Scalar lower = new Scalar(0, 0, 0); Scalar
upper = new Scalar(180, 255, 255); public Form1() { InitializeComponent(); } private void Binarization() { depthMat = depthMat.CvtColor(ColorConversionCodes.BGR2GRAY); depthMat = depthMat.Threshold(20, 255, ThresholdTypes.Binary); //HSVに変換 Mat hsv = bgrMat.CvtColor(ColorConversionCodes.BGR2HSV); //HSVに対する閾値処理 binMat = hsv.InRange(lower, upper); //二値化結果を可視化 pictureBox3.Image = binMat.ToBitmap(); hsv.Release(); } lower upper -
演習:閾値を変更して実行 Scalar lower = new OpenCvSharp.Scalar(50, 0, 0); Scalar upper
= new OpenCvSharp.Scalar(80, 255, 255); 緑の領域だけ白くなる -
演習:閾値をもとに戻す Scalar lower = new OpenCvSharp.Scalar(0, 0, 0); Scalar upper
= new OpenCvSharp.Scalar(180, 255, 255); -
UIを使ってHの閾値を変更 //minHを動かすと呼び出される private void minH_Scroll(object sender, EventArgs e) { //閾値の下限のHの値を変更
lower.Val0 = minH.Value; } 開始 -
UIを使ってHの閾値を変更 //maxHを動かすと呼び出される private void maxH_Scroll(object sender, EventArgs e) { //閾値の上限のHの値を変更
upper.Val0 = maxH.Value; } 開始 -
UIを使ってSの閾値を変更 //minSを動かすと呼び出される private void minS_Scroll(object sender, EventArgs e) { //閾値の下限のSの値を変更
lower.Val1 = minS.Value; } //maxSを動かすと呼び出される private void maxS_Scroll(object sender, EventArgs e) { //閾値の上限のSの値を変更 upper.Val1 = maxS.Value; } -
UIを使ってVの閾値を変更 //minVを動かすと呼び出される private void minV_Scroll(object sender, EventArgs e) { //閾値の下限のVの値を変更
lower.Val2 = minV.Value; } //maxVを動かすと呼び出される private void maxV_Scroll(object sender, EventArgs e) { //閾値の上限のVの値を変更 upper.Val2 = maxV.Value; } -
2種類の二値化画像に対するAND演算 private void Binarization() { //Depth画像を二値化 depthMat = depthMat.CvtColor(ColorConversionCodes.BGR2GRAY); //しきい値=20で二値化
depthMat = depthMat.Threshold(20, 255, ThresholdTypes.Binary); //HSVに変換 Mat hsv = bgrMat.CvtColor(ColorConversionCodes.BGR2HSV); //HSVに対する閾値処理 binMat = hsv.InRange(lower, upper); //二値化結果を可視化 pictureBox3.Image = binMat.ToBitmap(); //HSVおよびDepthに対する二値化結果のANDをbinMatに代入 Cv2.BitwiseAnd(binMat, depthMat, binMat); //AND演算の結果を可視化 pictureBox4.Image = binMat.ToBitmap(); hsv.Release(); } -
平滑化と二値化 private void Binarization() { //Depth画像を二値化 depthMat = depthMat.CvtColor(ColorConversionCodes.BGR2GRAY); depthMat
= depthMat.Threshold(20, 255, ThresholdTypes.Binary); //HSVに変換 Mat hsv = bgrMat.CvtColor(ColorConversionCodes.BGR2HSV); //HSVに対する閾値処理 binMat = hsv.InRange(lower, upper); //二値化結果を可視化 pictureBox3.Image = binMat.ToBitmap(); Cv2.BitwiseAnd(binMat, depthMat, binMat); //周囲9×9pixelの画素を用いて平滑化 binMat = binMat.Blur(new OpenCvSharp.Size(9, 9)); //二値化 binMat = binMat.Threshold(60, 256, ThresholdTypes.Binary); pictureBox4.Image = binMat.ToBitmap(); hsv.Release(); } -
手の輪郭取得の準備 //輪郭抽出 private Point[] ContourExtraction() { Point[] contour = null;
return contour; } private void timer1_Tick(object sender, EventArgs e) { float maxDepth = 0.75f; Bitmap color = kinect.GetColorImage(); Bitmap depth = kinect.GetDepthImage(maxDepth); depthMat = BitmapConverter.ToMat(depth); bgrMat = BitmapConverter.ToMat(color); Binarization(); //抽出した輪郭の点をcontourに格納 Point[] contour = ContourExtraction(); pictureBox1.Image = color; /*以下省略*/ } -
手の輪郭を取得(ContorExtraction関数内) Point[] contour = null; Point[][] lines; HierarchyIndex[] h; //複数の輪郭抽出
binMat.FindContours(out lines, out h, RetrievalModes.External, ContourApproximationModes.ApproxSimple); double maxArea = -1; for (int i = 0; i < lines.Length; i++) { double area = Cv2.ContourArea(lines[i]); //面積最大の輪郭を手と仮定する if (area > maxArea) { maxArea = area; contour = lines[i]; } } return contour; -
輪郭を描画 private void DrawPolyLine(Point[] points, Scalar color) { /*内容は次のページにて*/ }
private void timer1_Tick(object sender, EventArgs e) { /*省略*/ bgrMat = BitmapConverter.ToMat(color); Binarization(); Point[] contour = ContourExtraction(); if (contour != null) { DrawPolyLine( contour, Scalar.Green); } pictureBox1.Image = color; pictureBox1.Image = bgrMat.ToBitmap(); /*省略*/ } pictureBox1.Image = color; -
輪郭の表示 private void DrawPolyLine(Point[] points, Scalar color) { Point p
= points.Last(); for (int i = 0; i < points.Length; i++) { Cv2.Line(bgrMat, p, points[i], color, 2); p = points[i]; } } 0 1 2 3 4 points[0] 1 2 3 p p points[1] 2 3 4 0 1 2 p points[4] ・・・ i = 0 i = 1 i = 4 -
凸包近似 private int[] Polygonization(Point[] contour) { //凸包近似。戻り値は使っている点のインデックス int[] hullIdx =
Cv2.ConvexHullIndices(contour); //凸包近似。戻り値は実際の点の座標 Point[] hullPt = Cv2.ConvexHull(contour); DrawPolyLine(hullPt, Scalar.Blue); return hullIdx; } private void timer1_Tick(object sender, EventArgs e) { /*省略*/ } -
凸包近似 private void timer1_Tick(object sender, EventArgs e) { float maxDepth
= 0.75f; Bitmap color = kinect.GetColorImage(); Bitmap depth = kinect.GetDepthImage(maxDepth); depthMat = BitmapConverter.ToMat(depth); bgrMat = BitmapConverter.ToMat(color); Binarization(); Point[] contour = ContourExtraction(); if (contour != null) { DrawPolyLine( contour, Scalar.Green); int[] hull = Polygonization(contour); } pictureBox1.Image = bgrMat.ToBitmap(); pictureBox2.Image = depthMat.ToBitmap(); /*以下省略*/ } -
凸包の頂点を表示 private int[] Polygonization(Point[] contour) { int[] hullIdx = Cv2.ConvexHullIndices(contour);
Point[] hullPt = Cv2.ConvexHull(contour); for(int i=0;i<hullIdx.Length;i++) { int index = hullIdx[i]; bgrMat.Circle(contour[index], 10, Scalar.Red, 2); } DrawPolyLine(bgr, hullPt, Scalar.Blue); return hullIdx; } 凸包の頂点付近に多数の 点が存在する。 指の先端の位置を求める際 には1点にまとめた方が楽 -
クラスタリング private int[] Polygonization(Point[] contour) { int[] hullIdx = Cv2.ConvexHullIndices(contour);
Point[] hullPt = Cv2.ConvexHull(contour); //凸包を形成する点のラベル番号を格納 int[] labels; //Predicateで設定した条件(後述)に従って分類 Cv2.Partition(hullPt, out labels, Predicate); for (int i = 0; i < hullIdx.Length; i++) { int index = hullIdx[i]; bgrMat.Circle(contour[index], 10, Scalar.Red, 2); //各点のラベル番号を表示 bgrMat.PutText(labels[i].ToString(), contour[index], HersheyFonts.HersheyDuplex, 1, Scalar.White); } DrawPolyLine(hullPt, Scalar.Blue); return hullIdx; } -
クラスタリング private int[] Polygonization(Point[] contour) { /*省略*/ Cv2.Partition(hullPt, out labels,
Predicate); /*省略*/ } private bool Predicate(Point t1, Point t2) { //2点の距離が20pixelより小さければ同ラベル if (t1.DistanceTo(t2) < 20) { return true; } else { return false; } } -
ラベルごとの代表点を取得 Center Point 【処理手順】 Step1: 手を内包する最小の矩形を求める Step2: 矩形の中心座標を算出 Step3: 中心点と凸包頂点の距離を算出
Step4: 最も遠い点を各ラベルの代表点とする Min Area Rectangle -
矩形の中心算出 private int[] Polygonization(Point[] contour) { int[] hullIdx = Cv2.ConvexHullIndices(contour);
Point[] hullPt = Cv2.ConvexHull(contour); int[] labels = new int[hullIdx.Length]; Cv2.Partition(hullPt, out labels, Predicate); RotatedRect rect = Cv2.MinAreaRect(hullPt); bgrMat.Circle((int)rect.Center.X, (int)rect.Center.Y, 10, Scalar.Cyan, 3); for (int i=0;i<hullIdx.Length;i++) { int index = hullIdx[i]; bgrMat.Circle(contour[index], 10, Scalar.Red, 2); bgrMat.PutText(labels[i].ToString(), contour[index], HersheyFonts.HersheyDuplex, 1, Scalar.White); } DrawPolyLine(bgr, hullPt, Scalar.Blue); return hullIdx; } -
ラベルごとの代表点を取得 Center Point 【処理手順】 Step1: 手を内包する最小の矩形を求める Step2: 矩形の中心座標を算出 Step3: 中心点と凸包頂点の距離を算出
Step4: 最も遠い点を各ラベルの代表点とする Min Area Rectangle -
代表点の選択 private int[] Polygonization(Point[] contour) { /*コード省略*/ RotatedRect rect =
Cv2.MinAreaRect(hullPt); bgrMat.Circle((int)rect.Center.X, (int)rect.Center.Y, 10, Scalar.Cyan, 3); hullIdx = HullSimplification(contour, hullIdx, labels, rect.Center); for (int i=0;i<hullIdx.Length;i++) { int index = hullIdx[i]; bgrMat.Circle(contour[index], 10, Scalar.Red, 2); } /*コード省略*/ } copy_and_paste.textの内容をこの辺りに張り付ける -
指を検出 private int FingerDetection(Point[] contour, int[] hull) { int num
= 0;//指の本数 return num; } private void timer1_Tick(object sender, EventArgs e) { int fingerNum = 0; /*省略*/ Binarization(); Point[] contour = ContourExtraction(); if (contour != null) { DrawPolyLine( contour, Scalar.Green); int[] hull = Polygonization(contour); fingerNum = FingerDetection(contour, hull); } /*省略*/ } -
指を検出 private int FingerDetection(Point[] contour, int[] hull) { int num
= 0; //最も凹んでいる点を計算。引数は全輪郭点と凸包の頂点 Vec4i[] defects = Cv2.ConvexityDefects(contour, hull); //Item2の座標を可視化 foreach (Vec4i v in defects) { bgrMat.Circle(contour[v.Item2], 5, Scalar.White, 2); } return num; } 【解説】 ConvexityDefetsは凸包の欠陥性、 つまり内側への凹みを計算。 戻り値のVec4iは下記の順にデータを格納。 Item0: 始点のインデックス Item1: 終点のインデックス Item2: 最も遠い点のインデックス Item3: 最も遠い点までの距離 Item0 Item2 Item1 -
指とみなす条件 p1 tip p2 θ l1 l2 θ<70[deg] && l
1 +l 2 >60[pixel] -
指を検出 private int FingerDetection(Point[] contour, int[] hull) { Vec4i[] defects
= Cv2.ConvexityDefects(contour, hull); foreach (Vec4i v in defects) { bgr.Circle(contour[v.Item2], 5, Scalar.White, 2); } //指かどうかを判定 for (int i = 0; i < defects.Length; i++) { Vec4i d1 = defects[i]; Vec4i d2 = defects[(i + 1) % defects.Length]; //指先と指間腔の点を取得 Point p1 = contour[d1.Item2]; Point p2 = contour[d2.Item2]; Point tip = contour[d1.Item1]; //指先から指間腔へのベクトルを計算 p1 = p1 - tip; p2 = p2 - tip; } } p1 tip p2 -
指の検出 for (int i = 0; i < defects.Length; i++)
{ Vec4i d1 = defects[i]; Vec4i d2 = defects[(i + 1) % defects.Length]; Point p1 = contour[d1.Item2]; Point p2 = contour[d2.Item2]; Point tip = contour[d1.Item1]; p1 = p1 - tip; p2 = p2 - tip; //p1,p2の長さを計算 double l1 = Math.Sqrt(p1.X * p1.X + p1.Y * p1.Y); double l2 = Math.Sqrt(p2.X * p2.X + p2.Y * p2.Y); //内積の計算と角度の計算 double dot = Point.DotProduct(p1, p2) / (l1 * l2); double angle = Math.Acos(dot) * 180.0 / Math.PI; if (angle < 70 && (l1+l2) > 60) { bgrMat.Circle(tip, 5, Scalar.Orange, 3); num++; } } -
認識した指の本数を表示 private void timer1_Tick(object sender, EventArgs e) { float maxDepth
= 0.75f; Bitmap color = kinect.GetColorImage(); Bitmap depth = kinect.GetDepthImage(maxDepth); depthMat = BitmapConverter.ToMat(depth); bgrMat = BitmapConverter.ToMat(color); Binarization(); Point[] contour = ContourExtraction(); int fingerNum = 0; if (contour != null) { DrawPolyLine( contour, Scalar.Green); int[] hull = Polygonization(contour); fingerNum = FingerDetection(contour, hull); } label7.Text = fingerNum.ToString(); pictureBox1.Image = bgrMat.ToBitmap(); pictureBox2.Image = depthMat.ToBitmap(); /*省略*/ }