Kinectで背景合成 & 簡易光学迷彩

任意のBitmap画像を、Kinectを使ってリアルタイムに背景として合成するプログラムです。

言語

C#

[デモ]背景合成

f:id:noire722:20110505155005p:image
頭が欠けてる……座って撮影したので下半身が
消えていい感じに(
深度センサでばっかり遊んでましたけど、カメラも直感的で楽しいですね。

[デモ]擬似光学迷彩

f:id:noire722:20110506150337p:image
だいたい消えてる。
でも、頭が…

MainForm.cs

半分くらいおなじみのコード。
肝はMapOutputModeをImageGeneratorから取得することと、ピクセルごとのRGB値をポインタで参照して直にコピーしてくる部分。
RGB値セット時のifの条件を逆にすれば、人物のシルエット上に背景画像が描画されるので光学迷彩っぽいことをして遊べます。

using System;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Imaging;
using System.Text.RegularExpressions;
using System.Threading;
using System.Windows.Forms;
using xn;
using BgComposition.util;

namespace BgComposition
{
    public partial class MainForm : Form
    {

        Thread openNiThread;
        Context context;
        DepthGenerator depthGenerator;
        UserGenerator userGenerator;
        ImageGenerator imageGenerator;
        PoseDetectionCapability poseDetectionCapability;
        SkeletonCapability skeletonCapability;

        Color[] colors = { Color.Red, Color.Blue, Color.ForestGreen, Color.Yellow, Color.Orange, Color.Purple, Color.White };
        Color[] anticolors = { Color.Green, Color.Orange, Color.Red, Color.Purple, Color.Blue, Color.Yellow, Color.Black };
        int ncolors = 6;

        int[] histogram;

        Bitmap bitmap;
        Bitmap bgImg;
        
        public MainForm()
        {
            InitializeComponent();

            // 背景画像読み込み
            this.bgImg = new Bitmap(Settings.IMGPATH_BG);

            StartOpenNI();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            Status.shouldRun = false;
            this.openNiThread.Join();
            base.OnClosing(e);
        }

        protected override void OnPaint(PaintEventArgs e)
        {
            base.OnPaint(e);

            lock (this)
            {
                e.Graphics.DrawImage(this.bitmap,
                    this.panelView.Location.X,
                    this.panelView.Location.Y,
                    this.panelView.Size.Width,
                    this.panelView.Size.Height);
            }
        }

        /// <summary>
        /// OpenNIスレッドの起動
        /// 
        /// </summary>
        private void StartOpenNI()
        {
            try
            {
                context = new Context(Settings.OPENNI_CONFIG);
            }
            catch (GeneralException ge)
            {
                Console.WriteLine(ge.Message);
                this.Close();
            }

            // imageGenerator生成
            imageGenerator = context.FindExistingNode(NodeType.Image) as ImageGenerator;
            imageGenerator.StartGenerating();

            // depthGenerator生成
            depthGenerator = context.FindExistingNode(NodeType.Depth) as DepthGenerator;
            if (this.depthGenerator == null)
            {
                throw new Exception("Viewer must have a depth node!");
            }
            depthGenerator.GetAlternativeViewPointCap().SetViewPoint(imageGenerator);

            // userGenerator生成
            userGenerator = new UserGenerator(context);
            userGenerator.NewUser += new UserGenerator.NewUserHandler(userGenerator_NewUser);
            userGenerator.LostUser += new UserGenerator.LostUserHandler(userGenerator_LostUser);
            userGenerator.StartGenerating();

            poseDetectionCapability = new PoseDetectionCapability(userGenerator);
            poseDetectionCapability.PoseDetected += new PoseDetectionCapability.PoseDetectedHandler(poseDetectionCapability_PoseDetected);

            skeletonCapability = new SkeletonCapability(userGenerator);
            skeletonCapability.CalibrationEnd += new SkeletonCapability.CalibrationEndHandler(skeletonCapability_CalibrationEnd);
            skeletonCapability.SetSkeletonProfile(SkeletonProfile.All);

            histogram = new int[depthGenerator.GetDeviceMaxDepth()];

            // 出力モード
            //MapOutputMode mapMode = depthGenerator.GetMapOutputMode();
            MapOutputMode mapMode = imageGenerator.GetMapOutputMode();

            this.bitmap = new Bitmap((int)mapMode.nXRes, (int)mapMode.nYRes/*, System.Drawing.Imaging.PixelFormat.Format24bppRgb*/);
            
            openNiThread = new Thread(ReaderThread);
            openNiThread.Start();
        }

        /// <summary>
        /// OpenNI 描画スレッド
        /// </summary>
        private unsafe void ReaderThread()
        {
            if (Status.shouldRun == false) return;

            DepthMetaData depthMD = new DepthMetaData();

            // 背景画像のリサイズ
            this.bgImg = ImageUtils.ResizeBmp(this.bgImg, this.bitmap.Width, this.bitmap.Height);

            while (Status.shouldRun)
            {
                try
                {
                    this.context.WaitOneUpdateAll(this.depthGenerator);
                }
                catch (Exception e)
                {
                }
                
                // カメラ画像描画
                var imageMD = this.imageGenerator.GetMetaData();
                if (imageGenerator.IsDataNew())
                {
                    DrawImage(imageMD);
                }
            }

        }

        /// <summary>
        /// カメラ画像の描画
        /// </summary>
        /// <param name="imageMD">ImageMetaData</param>
        private unsafe void DrawImage(ImageMetaData imageMD)
        {
            lock (this)
            {
                Rectangle rect = new Rectangle(0, 0, this.bitmap.Width, this.bitmap.Height);
                BitmapData data = this.bitmap.LockBits(rect, ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
                
                Rectangle bgRect = new Rectangle(0, 0, this.bgImg.Width, this.bgImg.Height);
                BitmapData bgData = this.bgImg.LockBits(bgRect, ImageLockMode.WriteOnly, System.Drawing.Imaging.PixelFormat.Format24bppRgb);

                byte* pSrc = (byte*)imageGenerator.GetImageMapPtr().ToPointer();

                ushort* pLabels = (ushort*)this.userGenerator.GetUserPixels(0).SceneMapPtr.ToPointer();
                int userCount = userGenerator.GetUsers().Length;

                // pixel単位でループ
                for (int y = 0; y < imageMD.YRes; y++)
                {
                    byte* pDest = (byte*)data.Scan0.ToPointer() + y * data.Stride;
                    byte* pBg = (byte*)bgData.Scan0.ToPointer() + y * bgData.Stride;

                    for (int x = 0; x < imageMD.XRes; x++, pSrc += 3, pDest += 3, pBg += 3, pLabels++)
                    {
                        if (*pLabels != 0 || userCount == 0)
                        {
                            // Kinectのカメラ画像からRGB値取得
                            pDest[0] = pSrc[2];
                            pDest[1] = pSrc[1];
                            pDest[2] = pSrc[0];
                        }
                        else
                        {
                            // 背景画像からRGB値取得
                            pDest[0] = *pBg;
                            pDest[1] = *(pBg + 1);
                            pDest[2] = *(pBg + 2);
                        }
                    }
                }
                this.bitmap.UnlockBits(data);
                this.bgImg.UnlockBits(bgData);
            }
            this.Invalidate();
        }

        /// <summary>
        /// textboxにログを表示
        /// </summary>
        /// <param name="msg">ログメッセージ</param>
        public void Log(string msg)
        {
            if (this.consoleTextBox != null)
            {
                try
                {
                    Regex r = new Regex(".*" + msg + @"[.]*");
                    if (r.Match(this.consoleTextBox.Text).Success)
                    {
                        this.consoleTextBox.AppendText(".");
                    }
                    else
                    {
                        this.consoleTextBox.AppendText("\r\n" + msg);
                    }
                    this.consoleTextBox.ScrollToCaret();
                    Console.WriteLine(msg);
                }
                catch (InvalidOperationException e)
                {
                    Console.WriteLine(e.StackTrace);
                }
            }
        }

        #region イベントハンドラ
        void userGenerator_NewUser(ProductionNode node, uint id)
        {
            //新しいユーザを検出した時
            Log(String.Format("New User {0}", id));
            //新しいユーザのポーズ検出を開始します
            poseDetectionCapability.StartPoseDetection(skeletonCapability.GetCalibrationPose(), id);
        }

        void userGenerator_LostUser(ProductionNode node, uint id)
        {
            //ユーザをロストした時
            Log(String.Format("Lost User {0}", id));
        }

        void poseDetectionCapability_PoseDetected(ProductionNode node, string pose, uint id)
        {
            //Ψを検出した時
            Log(String.Format("PoseDetected {1} {0}", id, pose));
            //新しいユーザのポーズ検出を終了し
            poseDetectionCapability.StopPoseDetection(id);
            //キャリブレーションを開始します
            skeletonCapability.RequestCalibration(id, true);
        }

        void skeletonCapability_CalibrationEnd(ProductionNode node, uint id, bool success)
        {
            //キャリブレーション完了した時
            Log(String.Format("CalibrationEnd {1} {0}", id, success));
            if (success)
            {
                //成功したなら、トラッキング開始
                Status.isUserTracking = true;
                skeletonCapability.StartTracking(id);
                Log("Calibration Success" + id);
            }
            else
            {
                //失敗したなら、再度検出開始
                Status.isUserTracking = false;
                poseDetectionCapability.StartPoseDetection(skeletonCapability.GetCalibrationPose(), id);
            }
        }
        #endregion
    }
}

Status.cs

状態管理クラス

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using xn;

namespace BgComposition
{
    class Status
    {
        public static bool shouldRun = true;
        public static bool isUserTracking = false;
        public static int activeUser = 0;

        /// <summary>
        /// 現在有効なユーザを取得
        /// </summary>
        /// <param name="users"></param>
        /// <returns></returns>
        public static uint GetActiveUser(uint[] users)
        {
            Status.activeUser = Status.activeUser > users.Length - 1 ? users.Length - 1 : Status.activeUser;
            return users[Status.activeUser];
        }
    }
}

Settings.cs

config.xmlや画像のファイルパスなどを設定。

using System.Windows.Forms;

namespace BgComposition.util
{
    class Settings
    {
        public const int MAX_USER_COUNT = 10;
        public const int NEUTRAL_MARGIN = 80;

        public static string OPENNI_CONFIG = System.IO.Directory.GetCurrentDirectory() + @"\data\config.xml";
        public static string IMGPATH_BG = System.IO.Directory.GetCurrentDirectory() + @"\data\bg.bmp";

    }
}

ImageUtils.cs

画像処理ユーティリティ

using System.Drawing;

namespace BgComposition.util
{
    class ImageUtils
    {
        /// <summary>
        /// 画像合成Utility
        /// </summary>
        /// <param name="original">元画像</param>
        /// <param name="mix">合成用画像</param>
        /// <param name="x">x座標</param>
        /// <param name="y">y座標</param>
        /// <param name="width"></param>
        /// <param name="height">高さ</param>
        /// <returns></returns>
        public static Bitmap MixImages(Bitmap original, Bitmap mix, int x, int y, int width, int height)
        {
            Bitmap mixed = new Bitmap(original);
            Graphics g = Graphics.FromImage(mixed);
            g.DrawImage(mix, x, y, width, height);
            g.Dispose();
            return mixed;
        }

        /// <summary>
        /// 画像リサイズ
        /// </summary>
        /// <param name="bitmap">元画像</param>
        /// <param name="width"></param>
        /// <param name="height">高さ</param>
        /// <returns>リサイズ後の画像</returns>
        public static Bitmap ResizeBmp(Bitmap bitmap, int width, int height)
        {
            var resized = new Bitmap(width, height, System.Drawing.Imaging.PixelFormat.Format24bppRgb);
            Graphics g = Graphics.FromImage(resized);
            g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
            g.DrawImage(bitmap, 0, 0, width, height);
            g.Dispose();
            return resized;
        }
    }
}

来週の某イベントが終わって落ち着いたら、githubにでもまるっとソース置く予定。