狠狠撸

狠狠撸Share a Scribd company logo
ARコンテンツ作成勉強会
RGB-Dセンシングと画像処理
#AR_Fukuoka
本日作成するコンテンツ
元ネタ: https://youtu.be/_qXcVtE1jc4
ハンズオンのおおまかな手順
Depth画像取得 手の検出 手より奥を無視
手周辺を切り出し形を認識
Visual Studioを起動
プロジェクトを作成 (1/2)
①ファイル
②新規作成
③プロジェクト
プロジェクトを作成 (2/2)
①VisualC#
②Windows フォームアプリケーション
③OK
动作确认
開始
KinectSDKの導入 (1/3)
①参照を右クリック
②参照の追加
KinectSDKの導入 (2/3)
Kinectで検索
KinectSDKの導入 (3/3)
①チェックを入れる
②OK
今回はカラー画像、Depth画像、スケルトンを活用したコンテンツを作る
Kinect SDKのデータは少々扱いづらいので、こちらで変換プログラムを提供
http://arfukuoka.lolipop.jp/kinect2018/sample.zip
KinectGrabberの導入 (1/4)
KinectGrabber.csをForm1.csと
同じディレクトリにドラッグ&ドロップ
KinectGrabberの導入 (2/4)
プロジェクトを右クリック
KinectGrabberの導入 (3/4)
①追加
②既存の項目
KinectGrabberの導入 (4/4)
①KinectGrabber.cs
②追加
スケールモードを設定
①クリック
②AutoScaleModeをNoneに変更
画像表示領域を作成 (1/6)
①ToolBoxタブ
②PictureBox
画像表示領域を作成 (2/6)
フォーム内をクリック
画像表示領域を作成 (3/6)
①PictureBoxをクリック
②Sizeを640,480
画像表示領域を作成 (4/6)
Formを選択してサイズを広げる
画像表示領域を作成 (5/6)
① PictureBox追加
② サイズを
320,240
画像表示領域を作成 (6/6)
① PictureBox追加
② Sizeを
240,240
③ Background Image LayoutをZoom
各PictureBoxの役割
pictureBox1 : AR表示
pictureBox2: Depth画像
pictureBox3:
手の周辺画像
Timerを用いた更新周期の設定
Timerを使うと任意に設定した間隔で処理(画像の取得?表示)を実行させられる
①ツールボックス
②Timer
Timerを用いた更新周期の設定
①Form上をクリック
②Timerが追加される
?Intervalを30[ms]
に変更(なんでもOK)
一定時間毎に呼び出される関数を作成
①Timerをダブルクリック
一定時間毎に呼び出される関数を作成
timer1_Tick関数が追加される
一定時間おきにtimer1_Tick関数の内部に書いた処理が実行される(予定)
Formが表示されたらTimerをスタートさせる
①Form1[デザイン]
②Form1をクリック
Formが表示されたらTimerをスタートさせる
①?をクリック
②Shownをダブルクリック
Formが表示されたらTimerをスタートさせる
namespace Kinect_ARFukuoka
{
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の機能をインポート
using Microsoft.Kinect; //ついでなのでKinectSDK(本家)もインポート
namespace Kinect_ARFukuoka
{
public partial class Form1 : Form
{
KinectGrabber kinect = new KinectGrabber();
public Form1()
{
InitializeComponent();
}
private void timer1_Tick(object sender, EventArgs e)
{
}
/以下省略/
アプリ終了時にKinectを止める
①?をクリック
②FormClosing
をダブルクリック
アプリ終了時に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)
{
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 threshold = 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(threshold); //Depth画像取得
閾値(奥行きの上限)を変えてみよう
【解説】
? Depth画像を使うとKinectからの距離を
用いて処理すべき領域を絞ることができる。
? 通常はKinectSDKで取得したDepthに
対して処理するが、扱いが面倒なので、
KinectGrabberで処理した結果を使用
Threshold=8 Threshold=4 Threshold=2
このあとやること:手より後方を無視する
Depth画像取得
手周辺を切り出し形を認識
手の検出 手より奥を無視
ユーザーのスケルトンと手の位置を取得
KinectGrabber kinect = new KinectGrabber();
Skeleton skeleton; //ユーザーの関節
SkeletonPoint hand; //ユーザーの手の3次元座標
public Form1()
{
InitializeComponent();
}
private void timer1_Tick(object sender, EventArgs e)
{
float threshold = 8;
skeleton = kinect.GetSkeleton(); //ユーザーの関節を取得
if (skeleton != null)
{
hand = skeleton.Joints[JointType.HandRight].Position;
}
Bitmap color = kinect.GetColorImage(); //カラー画像取得
Bitmap depth = kinect.GetDepthImage(threshold); //Depth画像取得
pictureBox1.Image = color; //カラー画像を表示
pictureBox2.Image = depth; //Depth画像を表示
}
補足
Skeleton skeleton; //ユーザーの関節
SkeletonPoint hand; //ユーザーの手の3次元座標
skeleton = kinect.GetSkeleton(); //ユーザーの関節を取得
if (skeleton != null)
{
hand = skeleton.Joints[JointType.HandRight].Position;
}
【補足】
? Kinect v1では20か所の関節を認識可能
? 全ての関節の情報は配列Jointsに格納
? 各関節の情報はJointsの[]の中で
JointType.XXXを用いて指定
? Positionで関節の3次元座標を取得
? 3次元座標の定義は、Kinectから見て
左右がx, 上下がy、奥行き方向がz
手の位置を描画してみる(1/2)
②?をクリック
③Paintを
ダブルクリック
①pictureBox2
手の位置を描画してみる(2/2)
private void pictureBox2_Paint(object sender, PaintEventArgs e)
{
if (skeleton == null) return;
Graphics g = e.Graphics;
//3次元の関節座標を2次元のDepth画像座標に変換
DepthImagePoint p = kinect.Skeleton2Depth(hand);
//円を描画
g.FillEllipse(Brushes.Red, p.X - 5, p.Y - 5, 10, 10);
}
x
y
スクリーン座標系
手の位置 p.X, p.Y
10
10
5
5
动作确认
手より後ろの画像情報を無視する
private void timer1_Tick(object sender, EventArgs e)
{
float threshold = 8;
skeleton = kinect.GetSkeleton(); //ユーザーの関節を取得
if(skeleton !=null){
hand = skeleton.Joints[JointType.HandRight].Position;
threshold = hand.Z; //手の位置(奥行き方向z)を閾値とする
}
Bitmap color = kinect.GetColorImage(); //カラー画像取得
Bitmap depth = kinect.GetDepthImage(threshold); //Depth画像取得
pictureBox1.Image = color; //カラー画像を表示
pictureBox2.Image = depth; //Depth画像を表示
}
ここから翱辫别苍颁痴による画像処理
Depth画像取得 手の検出 手より奥を無視
手周辺を切り出し形を認識
OpenCvSharpの導入 (1/4)
①ツール
②NuGet パッケージマネージャー
③ソリューションのNuGetパッケージ
OpenCvSharpの導入 (2/4)
①参照 ②nuget.org
OpenCvSharpの導入 (3/4)
①OpenCVsharp3
②OpenCvSharp3 AnyCPU
③パッケージのチェックをON
④Install
OpenCvSharpの導入 (4/4)
NuGetを閉じる
OpenCVの読み込み
using KinectSample; //KinectGrabberの機能をインポート
using Microsoft.Kinect; //ついでにKinectSDK(本家)もインポート
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)
{
}
/以下省略/
手の周辺を切り抜く(概要説明)
? 手より後ろを無視することで、かなり情報を減らすことができた
? 一方、OpenCVはどんな画像に対しても1ピクセルずつ処理
? 無駄が多いので手の周辺画素のみに対して処理すべき
? 注目している領域をROI(Region Of Interest)と呼ぶ
手の周辺を切り抜く(注意点)
? ROIの4隅が元画像の外になるとエラーになる
? 手の位置が元画像の端にある場合はROIを作成しない
roiHalf
roiHalf 搁翱滨を作れる手の位置
手の周辺を切り抜く関数を作成
private void timer1_Tick(object sender, EventArgs e)
{
/*省略*/
}
Mat getROI(Bitmap depth)
{
DepthImagePoint p = kinect.Skeleton2Depth(hand);
int roiSize = 60;
int roiHalf = roiSize / 2;
if(p.X>roiHalf && p.X+roiHalf<depth.Width
&& p.Y>roiHalf && p.Y + roiHalf < depth.Height)
{
Rect rect =
new Rect(p.X - roiHalf, p.Y - roiHalf, roiSize, roiSize);
Mat roi = BitmapConverter.ToMat(depth)[rect];
return roi;
}
return null;
}
roiHalf
roiHalf
搁翱滨を作れる手の位置
切り取り結果を表示
private void timer1_Tick(object sender, EventArgs e)
{
float threshold = 8;
skeleton = kinect.GetSkeleton(); //ユーザーの関節を取得
if(skeleton !=null){
hand = skeleton.Joints[JointType.HandRight].Position;
threshold = hand.Z; //手の位置(奥行き方向z)を閾値とする
}
Bitmap color = kinect.GetColorImage(); //カラー画像取得
Bitmap depth = kinect.GetDepthImage(threshold); //Depth画像取得
pictureBox1.Image = color; //カラー画像を表示
pictureBox2.Image = depth; //Depth画像を表示
if(skeleton == null) return; //ユーザーが見つからなければ終了
Mat roi = getROI(depth); //手の周辺をROIとして切り取る
if(roi == null) return; //ROIが作成できなければ終了
pictureBox3.BackgroundImage=roi.ToBitmap(); //切り抜き画像を貼り付け
roi.Dispose(); //メモリ開放
}
このあとやること
人の目では手の部分が一つの「まとまり」として理解できるが、
コンピュータは各ピクセルが白か黒かしか理解できない。
輪郭認識を用いて一つのまとまりとして扱えるようにする
手の輪郭の取得する関数の作成
OpenCvSharp.Point[] getHandContour(Mat bgr)
{
Mat bin = bgr.CvtColor(ColorConversionCodes.BGR2GRAY);
OpenCvSharp.Point[][] lines;
HierarchyIndex[] h;
bin.FindContours(out lines, out h,
RetrievalModes.External,
ContourApproximationModes.ApproxSimple);
double maxArea = -1;
OpenCvSharp.Point[] contour = null;
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 timer1_Tick(object sender, EventArgs e)
{
/*省略(Kinectからのスケルトンの取得とカラー&Depth画像表示)*/
if(skeleton == null) return; //ユーザーが見つからなければ終了
Mat roi = getROI(depth); //手の周辺をROIとして切り取る
if(roi == null) return; //ROIが作成できなければ終了
OpenCvSharp.Point[] contour = getHandContour(roi); //輪郭抽出
//↓輪郭がない場合または輪郭点数が少ない場合は終了
if (contour == null || contour.Length < 5) return;
DrawPolyLine(roi, contour, Scalar.Cyan); //描画
pictureBox3.BackgroundImage=roi.ToBitmap(); //切り抜き画像を貼り付け
roi.Dispose(); //メモリ開放
}
void DrawPolyLine(Mat bgr, OpenCvSharp.Point[] points, Scalar color)
{
/*次のページ*/
}
輪郭の表示
void DrawPolyLine(Mat bgr, OpenCvSharp.Point[] points, Scalar color)
{
OpenCvSharp.Point p = points.Last();
for(int i = 0; i < points.Length; i++)
{
Cv2.Line(bgr, p, points[i], color);
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 void timer1_Tick(object sender, EventArgs e)
{
/*省略(Kinectからのスケルトンの取得とカラー&Depth画像表示)*/
if(skeleton == null) return; //ユーザーが見つからなければ終了
Mat roi = getROI(depth); //手の周辺をROIとして切り取る
if(roi == null) return; //ROIが作成できなければ終了
OpenCvSharp.Point[] contour = getHandContour(roi); //輪郭抽出
//↓輪郭がない場合または輪郭点数が少ない場合は終了
if (contour == null || contour.Length < 5) return;
OpenCvSharp.Point[] hull = Cv2.ConvexHull(contour);//凸包近似
DrawPolyLine(roi, contour, Scalar.Cyan); //描画
DrawPolyLine(roi, hull, Scalar.Red);
pictureBox3.BackgroundImage=roi.ToBitmap(); //切り抜き画像を貼り付け
roi.Dispose(); //メモリ開放
}
凸包(Convex Hull)を活用した親指認識
? 与えられた点をすべて包含する最小の凸多角形のことを凸包という
? 点の集まりをシンプルな図形として扱うときによく用いられる手法
※手の場合、指の認識の下処理としても用いられる。
? 今回は指の認識はせず、凸包に対する白画素の面積の大小をもとに
親指を立てた時と手を握っている時を判別。
thumbUp: true thumbUp: false
凸包と白画素の比率を表示
①ツールボックス
②Label
③適当に配置
④フォントサイズを20
凸包と白画素の比率を表示
void HandState(OpenCvSharp.Point[] contour, OpenCvSharp.Point[] hull)
{
double handArea = Cv2.ContourArea(contour);
double hullArea = Cv2.ContourArea(hull);
double ratio = handArea / hullArea;
label1.Text = ratio.ToString();
}
private void timer1_Tick(object sender, EventArgs e)
{
/*省略(Kinectのデータ取得 ~ 輪郭抽出)*/
if (contour == null || contour.Length < 5) return;
OpenCvSharp.Point[] hull = Cv2.ConvexHull(contour);//凸包近似
HandState(contour, hull);
DrawPolyLine(roi, contour, Scalar.Cyan); //描画
DrawPolyLine(roi, hull, Scalar.Red);
pictureBox3.BackgroundImage=roi.ToBitmap(); //切り抜き画像を貼り付け
roi.Dispose(); //メモリ開放
}
親指のOn/Offを判別
void HandState(OpenCvSharp.Point[] contour, OpenCvSharp.Point[] hull)
{
double handArea = Cv2.ContourArea(contour);
double hullArea = Cv2.ContourArea(hull);
double ratio = handArea / hullArea;
label1.Text = ratio.ToString();
}
bool thumbUp=false;
private void timer1_Tick(object sender, EventArgs e)
{
thumbUp=false;
/*省略(Kinectのデータ取得 ~ 凸包近似)*/
HandState(contour, hull);
DrawPolyLine(roi, contour, Scalar.Cyan); //描画
DrawPolyLine(roi, hull, Scalar.Red);
pictureBox3.BackgroundImage=roi.ToBitmap(); //切り抜き画像を貼り付け
roi.Dispose(); //メモリ開放
}
bool HandState(OpenCvSharp.Point[] contour, OpenCvSharp.Point[] hull)
{
double handArea = Cv2.ContourArea(contour);
double hullArea = Cv2.ContourArea(hull);
double ratio = handArea / hullArea;
//label1.Text = ratio.ToString();
if (ratio < 0.9) return true;
else return false;
}
thumbUp = HandState(contour, hull);
手の傾きを算出
bool thumbUp = false;
float angle=0;
private void timer1_Tick(object sender, EventArgs e)
{
/*省略(Kinectのデータ取得 ~ 凸包近似)*/
thumbUp = HandState(contour, hull);
if (thumbUp)
{
RotatedRect ell = Cv2.FitEllipse(contour);
Cv2.Ellipse(roi, ell, Scalar.Green);
angle = ell.Angle;
label1.Text = angle.ToString();
}
DrawPolyLine(roi, contour, Scalar.Cyan); //描画
DrawPolyLine(roi, hull, Scalar.Red);
pictureBox3.BackgroundImage=roi.ToBitmap(); //切り抜き画像を貼り付け
roi.Dispose(); //メモリ開放
}
140度
60度
120度
手の傾きを算出
bool thumbUp = false;
float angle=0;
private void timer1_Tick(object sender, EventArgs e)
{
/*省略(Kinectのデータ取得 ~ 凸包近似)*/
thumbUp = HandState(contour, hull);
if (thumbUp)
{
RotatedRect ell = Cv2.FitEllipse(contour);
Cv2.Ellipse(roi, ell, Scalar.LightGreen);
angle = ell.Angle;
if (angle < 90) { angle -= 180; }
label1.Text = angle.ToString();
}
DrawPolyLine(roi, contour, Scalar.Cyan); //描画
DrawPolyLine(roi, hull, Scalar.Red);
pictureBox3.BackgroundImage=roi.ToBitmap(); //切り抜き画像を貼り付け
roi.Dispose(); //メモリ開放
}
プロジェクト名フォルダ&驳迟;プロジェクト名フォルダ&驳迟;产颈苍&驳迟;顿别产耻驳/搁别濒别补蝉别に蝉补产别谤.辫苍驳を入れる
画像の描画
①pictureBox1クリック
③Paintをダブルクリック
②?クリック
画像の描画
①pictureBox1_Paintが追加される
描画しよう
Bitmap saber = new Bitmap("saber.png");
private void pictureBox1_Paint(object sender, PaintEventArgs e)
{
if (thumbUp)
{
ColorImagePoint p = kinect.Skeleton2Color(hand);
Graphics g = e.Graphics;
g.TranslateTransform(p.X, p.Y);
g.RotateTransform(angle);
int h = saber.Height;
int w = saber.Width;
g.DrawImage(saber, 0, 0, w, h);
}
}
g.DrawImage(saber, -w / 2, 0, w, h);
はじめよう搁骋叠-顿センシングと画像処理
こまごました話:データの平滑化
凸包と輪郭ないの画素の比率を計算すると
結構ノイズが多いので平滑化するとより安定
→簡易ローパスフィルターを使う
こまごました話:データの平滑化
double prev=0;
bool HandState(OpenCvSharp.Point[] contour, OpenCvSharp.Point[] hull)
{
double handArea = Cv2.ContourArea(contour);
double hullArea = Cv2.ContourArea(hull);
double ratio = handArea / hullArea;
double c = 0.1;
ratio = ratio * c + prev * (1 - c);
prev = ratio;
if (ratio < 0.9) return true;
else return false;
}
はじめよう搁骋叠-顿センシングと画像処理

More Related Content

はじめよう搁骋叠-顿センシングと画像処理