おもちゃラボ

Unityで遊びを作っていきます

【Unity】Webカメラの画像を加工して表示する

Unityを使えばUSBカメラやスマートフォンのカメラからの画像を簡単に取得したり、加工したりすることが出来ます。ここではUnityでWebカメラを使う方法と、取得した画像のピクセルにアクセスして画像処理する方法を紹介します。

f:id:nn_hokuson:20170809192551j:plain

今回の記事の内容は次のとおりです。

カメラ画像を画面に表示する

まずは画像を表示するためのPlaneを作成します。ヒエラルキーウインドウからCreate -> 3D Object -> Planeを選択してください。カメラに対して垂直になるように回転し、サイズを調整してください。

f:id:nn_hokuson:20170809191014j:plain:w500

続いてWebカメラの画像をいま作成したPlaneに表示するためのスクリプトを作りましょう。プロジェクトウィンドウで右クリックし、Create -> Script -> C# Scriptを選択して「WebCamController」を作成してください。

スクリプトが作成できたら、次のスクリプトを入力してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WebCamController : MonoBehaviour
 {
    int width = 1920;
    int height = 1080;
    int fps = 30;
    WebCamTexture webcamTexture;

    void Start () {
        WebCamDevice[] devices = WebCamTexture.devices;
        webcamTexture = new WebCamTexture(devices[0].name, this.width, this.height, this.fps);
        GetComponent<Renderer> ().material.mainTexture = webcamTexture;
        webcamTexture.Play();
    }
}

このスクリプトでは、カメラデバイスのリストを取得し、先頭のデバイス(スマートフォンの内側カメラならdevices[1]とかになります)から得られるWebCamTextureをPlaneにセットしています。

f:id:nn_hokuson:20170809191548p:plain:w400

WebCamTextureをnewするときに画像の幅と高さを渡していますが、指定どおりの大きさのテクスチャが得られるわけではなく、カメラがサポートしている解像度から最も近いものが選択されます。

WebCamControllerをPlaneにアタッチします。プロジェクトウィンドウのWebCamControllerをヒエラルキーウインドウのPlaneにドラッグ&ドロップしてください。

f:id:nn_hokuson:20170808155425j:plain

実行すると、カメラで撮影された動画がPlaneに表示されると思います。

映像が上下逆さまになっている場合

PlaneのScaleのy値をマイナスにしてみてください。左右方向も逆さまになっている場合はxの値もマイナスにしてください。

f:id:nn_hokuson:20170809191219p:plain:w300

映像が淡い色合いになっている場合

マテリアルを新規で作成してPlaneにアタッチし、ShaderをUnlit/Textureに設定してください。

f:id:nn_hokuson:20170809191228p:plain:w300

Webカメラからの画像に対して画像処理をする

続いて、カメラから得られた画像をグレースケールに変換する画像処理をしてみましょう。画像処理する場合はテクスチャをもう一枚用意する必要があります。

先程はWebCamTextureを直接Planeのマテリアルに設定していましたが、今回はWebCamTextureで得られたテクスチャに対して画像処理したものをTexture2D型の変数に格納します。この画像をPlaneに設定することで、画像処理後の画像が表示されます。

f:id:nn_hokuson:20170809191914p:plain:w550

次のプログラムを入力して実行してください。

using System.Collections;
using System.Collections.Generic;
using UnityEngiusing System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class WebCamController : MonoBehaviour {

    int width = 1920;
    int height = 1080;
    int fps = 30;
    Texture2D texture;
    WebCamTexture webcamTexture;
    Color32[] colors = null;

    IEnumerator Init()
    {
        while (true)
        {
            if (webcamTexture.width > 16 && webcamTexture.height > 16)
            {
                colors = new Color32[webcamTexture.width * webcamTexture.height];
                texture = new Texture2D (webcamTexture.width, webcamTexture.height, TextureFormat.RGBA32, false);
                GetComponent<Renderer> ().material.mainTexture = texture;
                break;
            }
            yield return null;
        }
    }

    // Use this for initialization
    void Start () {
        WebCamDevice[] devices = WebCamTexture.devices;
        webcamTexture = new WebCamTexture(devices[0].name, this.width, this.height, this.fps);
        webcamTexture.Play();

        StartCoroutine (Init ());
    }

    // Update is called once per frame
    void Update () {
        if (colors != null)
        {
            webcamTexture.GetPixels32 (colors);

            int width = webcamTexture.width;
            int height = webcamTexture.height;
            Color32 rc = new Color32();

            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    Color c = colors [x + y * webcamTexture.width];
                    colors [x + y * webcamTexture.width] = new Color (c.grayscale, c.grayscale, c.grayscale);
                }
            }

            texture.SetPixels32 (colors);
            texture.Apply ();
        }
    }
}

このプログラムでは画像処理後の画像を格納するためのピクセルデータ配列(colors)とバッファになるテクスチャ(texture)を用意しています。Initメソッドの中で、ピクセル配列とテクスチャ用にカメラから得られる画像の大きさぶんのメモリを確保しています。

WebCamTextureをPlayしてから、画像が表示されるまで少しタイムラグがあります。カメラからの映像が得られてから、メモリを確保するようにInitメソッド内では待ちのプログラムを挟んでいます。

画像処理はUpdateメソッドの中で行っています。GetPixels32メソッドを使ってWebCamTextureからピクセルデータを取得し、得られた画像の全ピクセルを走査して、1ピクセルごとにグレースケール化する処理をしています。最後にSetPixels32メソッドでtextureに画像処理したピクセルをセットしています。

プログラムができたら実行してみてください。グレースケールに変換された映像が表示されたと思います。ただ・・・重たいですね・・・。Statusで確認すると3FPSぐらいしか出ていませんでした。

高速化する

ということで、何処の処理が重たいのかprofilerで確認してみましょう。Window -> Profilerをクリックしてプロファイラウインドウを開いてください。「Deep Profile」をクリックしてからゲームを実行してください。

f:id:nn_hokuson:20170808155721j:plain:w550

プロファイル結果を見ると、Color32.op_implictとTexture.get_width、Texture.get_heightでCPUを65%ぐらい食ってしまっていますね。WebCamTextureの幅と高さを取得する処理とColor->Color32の型変換が遅いようです。

Updateメソッドを次のように書き換えてみてください。

void Update () 
{
    if (colors != null)
    {
        webcamTexture.GetPixels32 (colors);

        int width = webcamTexture.width;
        int height = webcamTexture.height;
        Color32 rc = new Color32();

        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                Color32 c = colors [x + y * width];
                byte gray = (byte)(0.1f * c.r + 0.7f * c.g + 0.2f * c.b);
                rc.r = rc.g = rc.b = gray;
                colors [x + y * width] = rc;
            }
        }

        texture.SetPixels32 (colors);
        texture.Apply ();
    }
}

ここでは、テクスチャの幅と高さを取得する部分を画像処理ループの外側に移動しています。また、Color型ではなくColor32型を使うように変更し、newするのをループの外側に持っていっています。

もう一度実行してみてください。今度は20FPS程度は出るはずです。

f:id:nn_hokuson:20170809192551j:plain

iOSで実行するとクラッシュする場合

iOSで実行しようとすると、iOSのバージョンによってはアプリがクラッシュする場合があります。この場合次のようなエラーが表示されます。

This app has crashed because it attempted to access privacy-sensitive data without a usage description. The app's Info.plist must contain an NSCameraUsageDescription key with a string value explaining to the user how the app uses this data.

iOS10からはカメラへアクセスする場合のセキュリティが厳しくなり、info.plistにDescriptionを記述しなければいけなくなりました。

なにもクラッシュすることはないと思うのですが・・・(笑)

Xcodeのinfo.plistを開いて、info.plistに「Privacy - Camera Usage Description」を追加してから、再度Xcodeでビルドしてください。

f:id:nn_hokuson:20170809191304p:plain:w300

実行するとiPhone上でカメラアクセスを許可する画面がでて、OKするとカメラからの映像が表示されます。