はじめに
少し前に肌色領域を検出する記事を書きました。
これの続きですが、この領域をラベリング(塊ごとに番号を付け)して最大面積の領域のみ抽出してみたいと思います。
これをすることで手を検出するために一歩前進できるはずです。
今のところ考えているのは顔を検出してそこを除外し、一番大きい領域は手では?みたいな方針でいこうと思っていますが、これについてはもっと色々と考えた方が良さそうな気がします。
では早速いきましょう。
ラベリングをする
ラベリングの原理は以下の記事がとてもわかりやすく解説してくれていました。
ただしOpenCV
にはこれをやってくれるメソッドが既に備わっているのでそちらを使わせていただきましょう。
それはこちら。
public static int ConnectedComponentsWithStats( InputArray image, // 入力画像 OutputArray labels, // ラベリング結果画像(ラベリングされた数値の画像 ちなみに0は背景) OutputArray stats, // 統計データ OutputArray centroids, // 重心 int connectivity = 8, // ラベリングするときに4連結か8連結か(原理を参照) int ltype = CV_32S // ラベリング結果画像の型 )
OpenCV: Structural Analysis and Shape Descriptors
返り値はラベルの数です。
この統計データstats
には以下のような情報が入っています。
列挙子 | 意味 |
---|---|
CC_STAT_LEFT(=0) | 水平方向の境界ボックスの両端を含む左端(x)座標 |
CC_STAT_TOP(=1) | 垂直方向の境界ボックスの包括的な開始点である最上位(y)座標 |
CC_STAT_WIDTH(=2) | 境界ボックスの水平サイズ |
CC_STAT_HEIGHT(=3) | 境界ボックスの垂直サイズ |
CC_STAT_AREA(=4) | 接続されたコンポーネントの総面積(ピクセル単位) |
OpenCV: Structural Analysis and Shape Descriptors
ただこのあたりは初見では結構使い方がわかりづらい気がします。
ラベリングのコード
var label = new MatOfInt(); // ラベリング結果画像 var stats = new MatOfInt(); // 統計データ var centroids = new MatOfDouble(); // 重心 var nLabels = Cv2.ConnectedComponentsWithStats(binary, label, stats, centroids, PixelConnectivity.Connectivity8, MatType.CV_32SC1); var statsIndexer = stats.GetGenericIndexer<int>(); for (int i = 1; i<nLabels; i++) { var x = statsIndexer[i, 0]; // 左端の座標 var y = statsIndexer[i, 1]; // 最上位の座標 var width = statsIndexer[i, 2]; // 水平サイズ var height = statsIndexer[i, 3]; // 垂直サイズ var area = statsIndexer[i, 4]; // 総面積(ピクセルの数) }
特に統計データを取り出すところが結構わかりづらくて初見殺しな気もしますが、とりあえずこれで動きました。
最大面積を検出する
いままでの知識を使って実際に動くコードを書いてみました。
using OpenCvSharp; using UnityEngine; using UnityEngine.UI; public class FormFinder : MonoBehaviour { [SerializeField] private RawImage _renderer; private readonly static Scalar SKIN_LOWER = new Scalar(0, 60, 80); private readonly static Scalar SKIN_UPPER = new Scalar(10, 160, 240); private int _width = 1920; private int _height = 1080; private int _fps = 30; private WebCamTexture _webcamTexture; private void Start() { WebCamDevice[] devices = WebCamTexture.devices; _webcamTexture = new WebCamTexture(devices[0].name, this._width, this._height, this._fps); _webcamTexture.Play(); } void OnDestroy() { if (_webcamTexture != null) { if (_webcamTexture.isPlaying) _webcamTexture.Stop(); _webcamTexture = null; } } private void Update() => ExtractSkinColor(_webcamTexture); private void ExtractSkinColor(WebCamTexture tex) { // Texture2DからOpenCVで使われる形式Matに変換 Mat mat = OpenCvSharp.Unity.TextureToMat(tex); Mat hsvMat = new Mat(); // BGRからHSVに変換 Cv2.CvtColor(mat, hsvMat, ColorConversionCodes.BGR2HSV); // 肌色で抽出して2値化 Mat binary = hsvMat.InRange(SKIN_LOWER, SKIN_UPPER); // ラベリング var label = new MatOfInt(); var stats = new MatOfInt(); var centroids = new MatOfDouble(); var nLabels = Cv2.ConnectedComponentsWithStats(binary, label, stats, centroids, PixelConnectivity.Connectivity8, MatType.CV_32SC1); var statsIndexer = stats.GetGenericIndexer<int>(); // 最大面積のラベルを調べる var maxArea = 0; var maxIndex = 0; for (int i = 1; i < nLabels; i++) { var area = statsIndexer[i, 4]; if (maxArea < area) { maxArea = area; maxIndex = i; } } // 最大面積のラベルのものだけ取り出し var handMat = label.InRange(maxIndex, maxIndex + 1); Cv2.CvtColor(handMat, handMat, ColorConversionCodes.GRAY2BGR); _renderer.texture = OpenCvSharp.Unity.MatToTexture(handMat); } }
さいごに
結構いろんな処理をしていたのですが、macbook pro2019で40FPSぐらいは出ていました。
ただネットに上がっている記事の中にはピクセルデータを2重ループを使って処理しているものがありますが、あれをやるとFPSがガタ落ちするのでOpenCVのメソッドをうまく活用してみてください。
今回はこれくらいで。
ではまた。