Upgrade to Pro — share decks privately, control downloads, hide ads and more …

AR Fukuoka: OpenCVとRGB-Dセンサで始めるハンドトラッキング

AR Fukuoka: OpenCVとRGB-Dセンサで始めるハンドトラッキング

3/2に開催した勉強会資料
https://eventon.jp/16146/

TakashiYoshinaga

March 02, 2019
Tweet

More Decks by TakashiYoshinaga

Other Decks in Technology

Transcript

  1. 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が追加される ②自分で追加
  2. 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) { } /*****以下省略*****/
  3. アプリ終了時に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(); }
  4. 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(); }
  5. 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画像取得
  6. 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) { } /*****以下省略*****/
  7. 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();
  8. 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(); }
  9. 色の抽出をしよう 【やりたいこと】  特定の色の領域を白、それ以外を黒で二値化 【問題点】  色情報は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
  10. カラー/二値化画像用の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;
  11. 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
  12. 演習:閾値を変更して実行 Scalar lower = new OpenCvSharp.Scalar(50, 0, 0); Scalar upper

    = new OpenCvSharp.Scalar(80, 255, 255); 緑の領域だけ白くなる
  13. 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; }
  14. 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; }
  15. 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(); }
  16. 平滑化と二値化 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(); }
  17. 手の輪郭取得の準備 //輪郭抽出 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; /*以下省略*/ }
  18. 手の輪郭を取得(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;
  19. 輪郭を描画 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;
  20. 輪郭の表示 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
  21. 凸包近似 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) { /*省略*/ }
  22. 凸包近似 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(); /*以下省略*/ }
  23. 凸包の頂点を表示 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点にまとめた方が楽
  24. クラスタリング 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; }
  25. クラスタリング 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; } }
  26. 矩形の中心算出 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; }
  27. 代表点の選択 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の内容をこの辺りに張り付ける
  28. 指を検出 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); } /*省略*/ }
  29. 指を検出 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
  30. 指を検出 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
  31. 指の検出 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++; } }
  32. 認識した指の本数を表示 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(); /*省略*/ }