【Unity】カメラの映像をAndroid実機に保存する

Android
https://amzn.to/3bd8XOX

 カメラの映像をUnityで表示する手順についてはこちらの記事でまとめましたが、今回はその映像データをJPG形式でAndroidの実機に保存する手順について説明します。ゲーム内で撮影した写真を端末に保存するような場合を想定しています。

Goal

AndroidのUnityアプリでカメラ映像を画像として保存する!




カメラ映像を表示させる準備

 今回は4:3の縦映像を指定したファイル名で保存するサンプルを作成していきます。レイアウトは撮影ボタン、保存ボタン、取り消しボタン、ファイル名指定Textを準備します。取り合えず適当にこんな感じで配置しておきます。

 カメラ起動スクリプトはどこでもいいですが、今回は撮影ボタンのStart()で実行しました。アプリ起動と同時にカメラが起動するようになっています。カメラの縦横サイズも4:3となるように適当に設定しています。使っている環境によってはサイズが合わなかったり、向きが異なる可能性もあります。その時は回転をやめたり、サイズを小さくしてみたりしてみてください。

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

public class ShootButton : MonoBehaviour
{
    public RawImage RawImage;
    public GameObject Text;
    WebCamTexture webCam;

    // Start is called before the first frame update
    void Start()
    {
        // WebCamTextureのインスタンスを生成
        webCam = new WebCamTexture();

        //RawImageのテクスチャにWebCamTextureのインスタンスを設定
        RawImage.texture = webCam;

        //90度回転
        Vector3 angles = RawImage.GetComponent<RectTransform>().eulerAngles;
        angles.z = -90;
        RawImage.GetComponent<RectTransform>().eulerAngles = angles;
        Vector2 size;
        size = RawImage.GetComponent<RectTransform>().sizeDelta;
        size.x = RawImage.GetComponent<RectTransform>().sizeDelta.y;
        size.y = RawImage.GetComponent<RectTransform>().sizeDelta.x;
        RawImage.GetComponent<RectTransform>().sizeDelta = size;


        //縦横のサイズを要求
        webCam.requestedWidth = 3024;
        webCam.requestedHeight = 4032;

        //カメラ表示開始
        webCam.Play();
    }

    // Update is called once per frame
    void Update()
    {
    }

    public void OnClick()
    {

    }
}

 カメラの画像が表示されるか試してみます。無事Androidの実機でカメラ画像が表示されることを確認しました。

映像を保存する(撮影)

 次にShootボタンを押されたタイミングで表示している画像を一時停止してその後、saveボタンで保存します。カメラを停止するのはPause()を実行します。

    public void OnClick()
    {
        // カメラを停止
        webCam.Pause();
    }

 Pasuseでカメラを止めた後、Saveボタンで表示されている映像を保存します。保存は以下のサンプルの通り、Texture2Dを使用します。File.WriteAllBytesでファイルを指定したフォルダに書き出すのですが、PCはApplication.dataPathを指定することで、Assetsフォルダのパス、AndroidはApplication.persistentDataPathを指定することで、Android\data\アプリファイル名\filesのパスを指定することができます。

    public void OnClick()
    {
        // インスタンス取得
        webCam = ShootButton.GetComponent<ShootButton>().webCam;
        // Texture2Dを新規作成
        Texture2D texture = new Texture2D(webCam.width, webCam.height, TextureFormat.ARGB32, false);
        // カメラのピクセルデータを設定
        texture.SetPixels(webCam.GetPixels());
        // TextureをApply
        texture.Apply();

        // Encode
        byte[] bin = texture.EncodeToJPG();
        // Encodeが終わったら削除
        Object.Destroy(texture);

        // ファイルを保存
#if UNITY_ANDROID
        File.WriteAllBytes(Application.persistentDataPath + "/test.jpg", bin);
#else
        File.WriteAllBytes(Application.dataPath + "/test.jpg", bin);
#endif
    }

 ここでさらなる問題が発生します。保存したデータが-90度回転しています。そもそもここではWebCamTextureのインスタンスからデータを引っ張ってきているので、回転させた表示領域は関係ないです。回転して保存されるのは当たり前です。
 Texture2Dを回転させる方法やWebcamTextureを回転させる方法は見つからなかったので、強引にPixelデータを回転させる処理を作って対応しました。

    private Color[] _rotateImg(Color[] coler, int width, int height, int rotate)
    {
        Color[] rotatepix = new Color[width * height];
        int startposi = width* height-width;
        int posi = 0;

        if (rotate == 0) {
            for (int i = 0; i < rotatepix.Length; i++) {
                rotatepix[i] = coler[i];
            }
        } else if (rotate == 90) {
            startposi = width -1;
            for (int j = 0; j < width; j++) {
                for (int i = startposi; i < rotatepix.Length ; i += width) {
                    rotatepix[posi] = coler[i];
                    posi++;
                }
                startposi--;
            }
        } else if (rotate == 180) {
            for (int i = (rotatepix.Length-1); i >= 0; i--) {
                rotatepix[rotatepix.Length-1-i] = coler[i];
            }
        } else if (rotate == 270) {
            startposi = width * height - width;
            for (int j = 0; j < width; j++) {
                for (int i = startposi; i >= 0; i -= width) {
                    rotatepix[posi] = coler[i];
                    posi++;
                }
                startposi++;
            }
        }

        return rotatepix;
    }

 いまだに何でこれで回転できているのか、わからないですし、汚いコードですが、予定通りに回ったので、良しとします。

まとめ

 Unityでカメラから取り込んだ映像を写真として扱うにあたって、回転に特にハマりました。画像として保存するとなると、UIを回転する要領では扱えないので、ピクセルデータを直接配置変更する必要がありました。(私がやり方を知らないだけかもしれませんが。。。)
 あと、サイズや回転など決め打ちでやっていますが、アプリを色々な機器で動かすとなると、Native コードできちんと情報を取得しておく必要がありそうです。

https://amzn.to/3bd8XOX

コメント

  1. 天茶 より:

    hirokuma様 はじめまして。
    Unityに関して、いつもブログを参考にさせていただいております。

    そこで、一つ質問がございます。
    4つ目と5つ目のコードを、どこにどのようにして記述すれば宜しいのですか?
    新しく、SaveButton.cs を作って、
    そこにコピーしたのですが、いろいろとコンパイルエラーが出て、困っております。
    (例えば、4つ目の4行目、
      webCam = ShootButton.GetComponent().webCam;
     が、
       静的でないフィールド、メソッド、またはプロパティ’Component.GetComponent()’で、オブジェクト参照が必要です。
     と出ます)

    私の勉強不足を十分に痛感しております…古い記事なので、覚えていらっしゃるか不安ですが、どうか記述の場所・方法を教えてください。

    ご多忙とは存じますが、宜しくお願いします。

  2. 天茶 より:

    hirokuma様 ご回答、ありがとうございました。
    あれから独学で色々試した結果、以下のように作り変えました。

    保存ボタン(SaveButton.cs)に、アタッチした関数内で、

    WebCamTexture webCam;
    GameObject UnittButton; // ボタンが入る入る変数
    ShootButton script; //ボタン内のScriptが入る変数

    UnittButton = GameObject.Find(“Canvas”).transform.Find(“Button (Legacy)”).gameObject;
    script = UnittButton.GetComponent();
    webCam = script.webCam;

    という風に、ちょっと強引ですが、外部から無理やりShhotButtonにあるカメラを参照するようにしました。

    この使い方を応用して、色んな所にあるオブジェクトを参照できるようになり、
    結構便利になりました。

    ここまでできたのも、ベースを作っていただいた、hirokuma様のおかげかと思っております。
    ご回答も含めて、ご多忙の中、色々とありがとうございました。
    また、ご質問させていただくかもしれませんが、その際は宜しくです。