xor

二兎を得るか、一兎をも得ざるか

【Unity】Webカメラ映像取り込み→縦横比整形→正方形切り出しまで

カメラ映像を取り込む

まずWebCamTextureからの映像をRawImage上に表示させます。
手順やスクリプト等は以下参考から。

note.com

MEMO
Texture2D extends Texture
WebCamTexture extends Texture
ピクセルを取り扱うGetPixelsなどのメソッドは共通して使えるみたいです。

f:id:ukkz:20200621144406p:plain

RawImageを追加するとCanvasの子要素になるほか、EventSystemもHierarchyに追加されます。
このRawImage内のコンポーネントとしてスクリプトを追加し、名前をWebCamera.csとしておきます。
実行するとGame画面内でRawImageが表示されないことがあるが、ここではとりあえずCanvasのRender Modeを World Space にしておきます。

kimama-up.net

UnityのUIについては不勉強のためあとで要確認……

表示上の縦横比を変更する

f:id:ukkz:20200621152634p:plain

位置やサイズの変更は、GameObjectはTransformで、UIに関してはRectTransformを用いるっぽいです。
ここではRectTransformのWidthとHeightを変更してみます。とはいえ、映像入力そのままのサイズにしてしまうとScene内のスケールが謎なことになる(Unityの数値表示はメートル単位)ので、Heightを1としたWidth比率を設定することで縦横比をあわせてみることに。

  • WebCamera.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class WebCamera : MonoBehaviour
{
    public int width = 640;
    public int height = 480;
    private static int FPS = 30;
    private WebCamTexture webCamTexture;
    private RawImage rawImage;
    private RectTransform rectTransform;

    void Start()
    {
        // Webカメラの開始
        this.webCamTexture = new WebCamTexture(this.width, this.height, FPS);
        // RawImageにカメラのテクスチャを貼り付ける
        this.rawImage = GetComponent<RawImage>();
        this.rawImage.texture = this.webCamTexture;
        // アスペクト比
        float aspect = (float)this.width / (float)this.height;
        // RectTransformのWidthを変えてアスペクト比をあわせる(heightは常に1.0f)
        this.rectTransform = GetComponent<RectTransform>();
        this.rectTransform.sizeDelta = new Vector2(aspect, 1.0f);
        // カメラ再生開始
        this.webCamTexture.Play();
    }
}

アスペクト比を直接RectTransform.sizeDeltaVector2で設定すればよさそうですね。正しい使い方かどうかはわかりません。

正方形に切り出す

最終的に機械学習系のモデルに入力することを考え、映像を縦横が同一サイズの正方形にトリミングして取り出してみます。
具体的には、RawImageからテクスチャを正方形状態で切り出し、動的生成したメッシュに貼り付けて表示させることを目指します。

MEMO
マテリアル = シェーダー + テクスチャ
テクスチャは画像ファイルなどから生成できるColor型の配列みたいなもの
シェーダーはテクスチャをいかに描画するか規定する関数のようなもの、手を出したら廃人になるってばっちゃが言ってた

切り出したあとに貼り付けて表示するためのメッシュの生成については以下を参考に。

nn-hokuson.hatenablog.com

このメッシュ生成をWebCamera.cs内に組み込みます。

// 以下をStart()内に追加する
//
Mesh mesh = new Mesh();
// 頂点座標
// 第3引数(Z = -0.01f)でRawImageの少し手前側に配置
mesh.vertices = new Vector3[] {
    new Vector3(-0.5f, -0.5f, -0.01f), // 左下
    new Vector3(-0.5f,  0.5f, -0.01f), // 左上
    new Vector3(0.5f , -0.5f, -0.01f), // 右下
    new Vector3(0.5f ,  0.5f, -0.01f), // 右上
};
// UV座標
mesh.uv = new Vector2[] {
    new Vector2(0, 0), // 左下
    new Vector2(0, 1), // 左上
    new Vector2(1, 0), // 右下
    new Vector2(1, 1), // 右上
};
// インデックス配列
mesh.triangles = new int[] {
    0, 1, 2,
    1, 3, 2,
};
// 作成したメッシュを指定
GetComponent<MeshFilter>().sharedMesh = mesh;

前提としてRawImageはPosZ = 0の位置に置いてあり、Main CameraはZ = -2のところからZ+方向を向いているという構成です。
ということで、vertices内のVector3指定において位置を少しだけカメラ寄りの-0.01fにすることで、動的生成されたメッシュがRawImageにオーバーレイするような形にしていきます。

テクスチャはフレームアップデート毎に切り出し → 別の貼り付け用のテクスチャオブジェクトにコピー → メッシュのマテリアルのテクスチャを置き換え、という流れで更新します。

  • WebCamera.cs
void Start()
{
    // Start()内に以下のみ追記
    // メッシュに貼り付ける正方形テクスチャの準備
    // カメラ映像の縦サイズ基準で作成
    this.rectTexture = new Texture2D(this.height, this.height, TextureFormat.RGBA32, false);
}

void Update()
{
    if (this.width >= this.height)
    {
        // カメラ映像が正方形または横長(PCやタブレット):縦サイズに合わせて横はカット
        int margin = (this.width - this.height) / 2;
        // GetPixels (x, y, width, height) で切り出し
        Color[] rectPixels = this.webCamTexture.GetPixels(margin, 0, this.height, this.height);
        this.rectTexture.SetPixels(rectPixels);
    }
    else
    {
        // カメラ映像が縦長(スマホなど):縦サイズに合わせて横は余白
        int margin = (this.height - this.width) / 2;
        // 横方向のみマージン部を除いてコピー
        for (int y = 0; y < this.height; y++)
        {
            for (int x = margin; x < this.width - margin; x++)
            {
                Color c = this.webCamTexture.GetPixel(x, y);
                this.rectTexture.SetPixel(x, y, c);
            }
        }
    }
    // テクスチャに反映 > メッシュのマテリアルに反映
    this.rectTexture.Apply();
    GetComponent<MeshRenderer>().material.mainTexture = this.rectTexture;
}

この時点ではマテリアルが存在していないので、インスペクタから追加します。
特に編集しなくていいのでそのままメッシュレンダラのElement 0に設定します。
シェーダーも指定しないと描画できないので、「Unlit/Texture」を選択します。
これにしておくとスクリプト上での描画がうまいこと反映できるらしいです。

f:id:ukkz:20200621174714p:plain

「Unlit/Texture」は、テクスチャを乗せるだけの超絶シンプルシェーダーだそう。

nakamura001.hatenablog.com

テクスチャ描画関連として以下も参考に。

nn-hokuson.hatenablog.com

ここまで一通りで実行すると以下のようになりました。

f:id:ukkz:20200621175949p:plain

RawImageのColor設定でアルファを入れておくことで、元映像が少し透けて中央の正方形メッシュ部分だけが浮き出たように見せることができました。