【Unity】機種依存することなく正しくカメラ映像を出力する方法

Android
https://amzn.to/3bd8XOX

 これまで、UnityでAndroid実機のカメラを使用する方法について以下のように画像表示や画像保存を目的とした記事を書いてきました。しかし、機種依存のコードばかりで、機種が変わると正しく表示されなかったり、保存されないのかなぁと思っていました。

 今回はどんなAndroid端末でも正しく期待したアスペクト比で映像を表示させるためのノウハウをまとめようと思います。そのためにこれまで、理解することを放置してきた回転、幅、高さ、解像度といった内容をしっかり理解し、コントロールできるようになることを目指していきます。

Goal

どんな機種のAndroidカメラでも正しく表示できること!




様々な向き幅高さ

基準の向き

 向きと一言で言ってもどこを基準にするかで変わってきます。おおむね、スマホであれば縦向きが基準となり、タブレットであれば横向きが基準となるようです。ただ、全てがそうなっているかはわかりません。正しい判断方法は

getWindowManager().getDefaultDisplay().getRotation()で取得される値がSurface.ROTATION_0となる向き = 基準の向き

 となるようですが、これだけではそれが縦向きなのか、横向きなのかわかりません。この向きと、縦横の画面サイズを合わせて、基準が縦長なのか横長なのかの判断ができます。今回は縦長、横長は問いませんので、ここでは触れません。
 端末毎に基準の向きがあるのだなということだけ押さえておけば大丈夫です。

カメラの向き

 カメラは基準の向き通りについていると思ってしまいます。スマホだとカメラは撮影方向についているだろうと。しかし、そうとは限りません。実際、スマホのカメラは基準に対して90度もしくは270度で取り付けられています。

 首を90度右に傾けてみてください。この景色がスマホには取り込まれてきているのです。

画像の幅、高さ

 こちらも、カメラ基準で取得できます。カメラは右90度に傾いているのであれば、カメラの幅は縦方向、カメラの高さは横方向の幅になります。

Unityに取り込まれる向き

 カメラの取り付けられている向きでカメラが取り込んだままの画像がやってきます。90度の位置で取り付けられているカメラであれば、首を右に90度傾けた景色

 がUnityのWebCamTextureでは取り込まれます。カメラが見たまま、カメラの上方向を基準にして取り込まれるのです。
 ただし、Unityも鬼ではありません。この画像を見るとき、何度回転させてみてくださいというのをvideoRotationAngleというパラメータで教えてくれます。

無理やり整理してみると

 無理やり絵にして整理してみました。カメラマネージャーのCharacteristicsからとれる情報と、WindowManagerからとれる情報でその端末の基準の向きカメラの向き対応している解像度が取れます。

 アプリに取り込まれる画像はUnityから提供される情報を用いて回転したりリサイズしたりします。

Androidライブラリの実装

 AndroidのNative Codeでカメラの情報を取得するライブラリを作成していきます。ライブラリの作成方法についてはこちらにもまとめてあります。

マニフェストの追記

 カメラの情報を取得するためにManifest(AndroidManifest.xml)ファイルに以下カメラのpermissionとfeatureを追加します。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.getcam">

    <uses-permission android:name="android.permission.CAMERA" />
    <uses-feature android:name="android.hardware.camera" />
</manifest>

カメラのプレビューサイズの取得

 ライブラリからどうやってSize情報を返すか、非常にハマりました。色々迷った挙句、Xml形式のStringで返すことにしました。もっといい方法があるかもしれませんが、初心者にはこれが限界でした。ライブラリのAPIサンプルコードは以下になります。

    public String GetPreviewSize() throws CameraAccessException {
        String[] id_list = null;
        String xml_txt = "<PreviewSize>";
        List<Size> prev_sizes = new ArrayList<>();
        StreamConfigurationMap map;
        CameraCharacteristics characteristics;
        Size size_tmp;

        CameraManager manager = (CameraManager)_context.getSystemService(Context.CAMERA_SERVICE);
        try {
            id_list = manager.getCameraIdList();
            for(int i=0;i<id_list.length;i++) {
                xml_txt += "<device id=\"" + id_list[i]+"\">";
                // 指定したカメラの情報を取得
                characteristics = manager.getCameraCharacteristics(id_list[i]);
                map = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
                prev_sizes = Arrays.asList(map.getOutputSizes(SurfaceTexture.class));
                for (int j = 0 ; j < prev_sizes.size() ; j++) {
                    size_tmp = prev_sizes.get(j);
                    xml_txt += "<Size Width=\"";
                    xml_txt += String.valueOf(size_tmp.getWidth());
                    xml_txt += "\" Height=\"";
                    xml_txt += String.valueOf(size_tmp.getHeight());
                    xml_txt += "\" />";
                }
                xml_txt += "</device>";
            }
        } catch (CameraAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        xml_txt += "</PreviewSize>";

        return xml_txt;
    }

 このAPIは以下のようなxml形式でカメラ情報を返却します。

<PreviewSize>
<device id="0">
<Size Height="3024" Width="4032"/>
<Size Height="2268" Width="4032"/>
<Size Height="3024" Width="3024"/>
<Size Height="2160" Width="3840"/>
<Size Height="1728" Width="4032"/>
<Size Height="1644" Width="3840"/>
<Size Height="1080" Width="2520"/>
<Size Height="1080" Width="1920"/>
<Size Height="720" Width="1680"/>
・
・
・
</device>
</PreviewSize>

カメラの取り付け方向の取得

 カメラがどの角度で取り付けれているかを取得します。WidthやHeightといったパラメータもこの取り付け角度が基準になります。縦に持っているからWidthは横ではないのです。

    public int[] GetOrientation() throws CameraAccessException {
        String[] id_list = null;
        int[] orientation = null;
        CameraCharacteristics characteristics;

        CameraManager manager = (CameraManager)_context.getSystemService(Context.CAMERA_SERVICE);

        try {
            id_list = manager.getCameraIdList();
            orientation = new int[id_list.length];
            for(int i=0;i<id_list.length;i++) {
                characteristics = manager.getCameraCharacteristics(id_list[i]);
                orientation[i] = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION);
            }
        } catch (CameraAccessException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return orientation;
    }

Unityアプリの実装

Androidライブラリからの情報取得

 まずは先ほど作成したライブラリを使ってNative Codeからカメラの情報を取得します。Unity側は戻り値のxml文字列からカメラ情報をParseしています。いったんなじみのあるVector2のリストに値を格納しています。

    private AndroidJavaObject _javaClass = null;
    private List<Vector2> _prevSizeList = new List<Vector2>();
    private List<List<Vector2>> _deviceList = new List<List<Vector2>>();

    // Start is called before the first frame update
    void Start()
    {
        _javaClass = new AndroidJavaObject("com.example.getcam.GetCamParameter");
        string sizelist;

        sizelist = _javaClass.Call<string>("GetPreviewSize");

        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load(new StringReader(sizelist));

        XmlNode root = xmlDoc.FirstChild;

        XmlNodeList talkList = xmlDoc.GetElementsByTagName("device");

        Vector2 tmpsize;
        foreach (XmlNode devtmp in talkList) {
            _prevSizeList.Clear();
            XmlNodeList nodelist = devtmp.ChildNodes;
            foreach (XmlNode s in nodelist) {
                tmpsize.x = float.Parse(s.Attributes["Width"].Value);
                tmpsize.y = float.Parse(s.Attributes["Height"].Value);
                _prevSizeList.Add(tmpsize);
            }
            _deviceList.Add(_prevSizeList);
        }
    }

UnityのWebCamTextureからの情報取得

 続いて、UnityのWebCamTextureから提供される情報を取得します。今回確認したパラメータはvideoRotationAnglevideoVerticallyMirroredです。ともに、Play()を実行した後でないと値が反映されていませんでした。また、スマホの縦持ちの時、videoRotationAngleは90が設定されていました。これはカメラ画像を表示する時は90度回転させて表示させてね!という意味があります。

正しい向きに表示させる

 以上を踏まえて、正しい向きに表示させます。カメラ番号0のカメラの情報をもとに考察していきます。まず、Androidライブラリから取得できるプレビューサイズの情報です。

<PreviewSize>
<device id="0">
<Size Height="3024" Width="4032"/>
<Size Height="2268" Width="4032"/>
<Size Height="3024" Width="3024"/>
<Size Height="2160" Width="3840"/>
<Size Height="1728" Width="4032"/>
<Size Height="1644" Width="3840"/>
<Size Height="1080" Width="2520"/>
<Size Height="1080" Width="1920"/>
<Size Height="720" Width="1680"/>
・
・
・
</device>
</PreviewSize>

 よく見ると、Heightが3024、Widthが4032と横長のサイズになっています。そしてUnityから取得できるvideoRotationAngleは90となっています。
 これらから、videoRotationAngleが0または180の時は取得できるプレビューサイズそのまま、videoRotationAngleが90または270の時はプレビューサイズの縦横を入れ替えればいいことがわかります。

 以上を元に正しい向きに正しいサイズで表示させるコードサンプルは以下になります。(カメラ0のみ、サイズも抜粋した一つのみ)

    void Start()
    {
        string sizelist;
        int[] orientation;
        float scale;

        //ライブラリの初期化
        _javaClass = new AndroidJavaObject("com.example.getcam.GetCamParameter");

        // プレビューサイズリストの取得
        sizelist = _javaClass.Call<string>("GetPreviewSize");

        // XMLをリストに変換
        XmlDocument xmlDoc = new XmlDocument();
        xmlDoc.Load(new StringReader(sizelist));
        XmlNode root = xmlDoc.FirstChild;
        XmlNodeList talkList = xmlDoc.GetElementsByTagName("device");
        Vector2 tmpsize;
        foreach (XmlNode devtmp in talkList) {
            _prevSizeList.Clear();
            XmlNodeList nodelist = devtmp.ChildNodes;
            foreach (XmlNode s in nodelist) {
                tmpsize.x = float.Parse(s.Attributes["Width"].Value);
                tmpsize.y = float.Parse(s.Attributes["Height"].Value);
                _prevSizeList.Add(tmpsize);
            }
            _deviceList.Add(_prevSizeList);
        }

        // カメラの取り付け向き取得
        orientation = _javaClass.Call<int[]>("GetOrientation");

        // UnityのWebCamTextureでカメラリスト取得
        WebCamDevice[] devices = WebCamTexture.devices;
        // カメラ0を起動させる
        webCam = new WebCamTexture(devices[0].name);
        // RawImageのテクスチャにWebCamTextureのインスタンスを設定
        RawImage.texture = webCam;
        // 縦横のサイズを要求(_deviceListからorientationを考慮して決定する。ここでは直値)
        webCam.requestedWidth = 4032;
        webCam.requestedHeight = 3024;
        // カメラ起動
        webCam.Play();
        // 起動させて初めてvideoRotationAngle、width、heightに値が入り、
        // アスペクト比、何度回転させれば正しく表示されるかがわかる
        if ((webCam.videoRotationAngle == 90) || (webCam.videoRotationAngle == 270)) {
            // 表示するRawImageを回転させる
            Vector3 angles = RawImage.GetComponent<RectTransform>().eulerAngles;
            angles.z = -webCam.videoRotationAngle;
            RawImage.GetComponent<RectTransform>().eulerAngles = angles;
        }
        // 回転済みなので、widthはx、heightはyでそのままサイズ調整
        // 全体を表示させるように、大きい方のサイズを表示枠に合わせる
        Vector2 sizetmp = RawImage.GetComponent<RectTransform>().sizeDelta;
        if (webCam.width > webCam.height) {
            scale = sizetmp.x / webCam.width;
        } else {
            scale = sizetmp.y / webCam.height;
        }
        sizetmp.x = webCam.width * scale;
        sizetmp.y = webCam.height * scale;

        RawImage.GetComponent<RectTransform>().sizeDelta = sizetmp;
    }

まとめ

 カメラを理解するには

  • 端末の基準の向き
  • 端末基準向きに対するカメラの取り付け向き
  • アプリで指定できるプレビューサイズ
  • アプリに取り込まれる画像の向き

 と、かなり向きとサイズが出てきて、非常に辛かったです。ただ、今回の検証で、どういうパラメータを指定すべきか、どのように向きをかえるべきか、アスペクト比を維持してリサイズするにはどうすべきかがかなり理解できました。

https://amzn.to/3bd8XOX

  

コメント