nreal lightでQRコードを読んでみた

# はじめに
ARグラス活用の施策として、ARグラスでQRコードを見るとQRコードの内容を目の前に表示するアプリを作ってみました。今回ARグラスはnreal lightを使用しています。
本記事で紹介するのは、シンプルにQRコードに記録されている文字を表示するというものですが、例えば展示物の説明表示や、施設内のナビゲーションなどへの応用もできそうです。

# nreal lightについて
nreal lightは、[nreal社](https://www.nreal.ai/)が開発したARグラスです。昨年から[Developer kit](https://developer.nreal.ai/dev-kit)が発売され、国内では[KDDI](https://news.kddi.com/kddi/corporate/newsrelease/2020/11/10/4778.html)が12月から販売予定です。
AR/MRグラスというと、Microsoft社の[Hololens](https://www.microsoft.com/ja-jp/hololens)が有名ですが、nreal lightはHololensに比べて軽量、普通のメガネ／サングラスに近い形状であることが特徴です。しかも、軽量ながらマイク、スピーカー、カメラなどの機器を備え、6Dofトラッキングなどに対応する多機能なARグラスです。
またOSにはAndroidを採用しており、開発ツールにはUnityをサポートしています。そのため、Unityを使ったAndroid向けゲームの開発経験のある方でしたら、若干の学習コストですぐにアプリを開発することができます。

# アプリの出来上がりイメージ
前述で述べたとおり、nreal lightにはカメラが搭載されています。今回のアプリはnreal lightのカメラからQRコードを読み込み、その結果をレンズ上に表示する、というものです。
作り方の説明に行く前に、作成したアプリのイメージと実際に動作している映像を紹介します。

## 出来上がりの外観
ARグラスアプリ（以下、アプリ）は、下図のようにレンズ左上にQRコードに記録されている文字を表示します。仕組みとしては右のレンズ端に搭載されたカメラでQRコードを読み込み、QRコードの内容を左レンズ上部に、表示します。
![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43330/abedca99-1be2-5109-aa96-13ff20b17245.png)

## 実際の動作
実際に動作しているところの映像がこちらです。
※ARグラス内側からスマホのカメラで撮影しているため、お見苦しいところはご容赦ください。
![nreal_qr_read.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43330/557003da-827e-1952-453a-77b3fc725db3.gif)


# アプリの開発方法
## nrealアプリはUnityで開発する
nreal lightのアプリ開発にはUnityを使用します。（2020/12/1現在）
そしてUnityでnreal lightの各機能を使うには、nreal社が提供しているSDK（[NRSDK](https://developer.nreal.ai/download)）を使用します。
UnityとNRSDKによる開発環境構築は[公式ドキュメント](https://developer.nreal.ai/develop/unity/android-quickstart)を参考に構築してください。

## QRコード操作ライブラリ”Zxing”
nrealアプリはUnityで開発するため、QRコードを読み込む機能もUnityで開発する必要があります。
UnityでQRコードを読み込むライブラリを調べてみると「[Zxing](https://github.com/micjahn/ZXing.Net)」を知りました。ドキュメント、サンプルコード、使用した記事といった情報もかなり多かったことから、今回はZxingを使用しました。

## 開発環境
整理すると、今回の開発環境は下表の通りになります。
※UnityとNRSDKのバージョンは組み合わせによっては正常に動作しない場合があります。
　下表の組み合わせ、または[公式ドキュメント](https://developer.nreal.ai/develop/unity/android-quickstart)に記載の組み合わせで環境を構築することをおすすめします。

|ソフトウェア|バージョン|
|---|----|
|Unity|2018.4.24f1|
|NRSDK|1.2.1|
|Zxing.NET|v0.16.5.0|


## 実装方法
### プロジェクトの作成とZxing.NETのインポート
nrealアプリを作成するにはUnityの3Dプロジェクトを作成し、NRSDKをインポートします。NRSDKのインポート方法については、[nrealのQuickStart](https://developer.nreal.ai/develop/unity/android-quickstart)をご覧ください。
Zxing.NETは[Githubのリポジトリ](https://github.com/micjahn/ZXing.Net)からライブラリファイル”zxing.unity.dll”取得します。
※リポジトリの”Clients\UnityDemo\Assets”に格納されています。

取得したライブラリファイルは、先に作成したUnityプロジェクトにドラッグ＆ドロップするとインポートできます。
![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43330/5fbbfe04-0e68-3c89-2c37-1b83eef1ab9e.png)

### UIを作成する
UIと言っても画面上に文字列を表示するだけなので、UIパーツのTextを画面に貼り付けるだけです。Sceneビューの位置と実際のnreal lightのレンズ上の位置はやや異なるので、Gameビューを見ながら調整してください。
![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43330/85ee87a6-c176-bd84-a59c-aabfbe73285d.png)

### QRコード読み込みのベースプログラムをgithubから取得する
QRコード読み込み／解析処理の実装は、UnityDemoのサンプルをベースにすると簡単に実装できます。こちらの[BarcodeCam.cs](https://github.com/micjahn/ZXing.Net/blob/master/Clients/UnityDemo/Assets/BarcodeCam.cs)をダウンロードして、Unityプロジェクトに追加してください。
※今回の例では、Assets/OrgAssets/Scripts配下に追加しています。

### プログラムをnreal light用にカスタマイズする
BarcodeCam.csの実装はモバイルアプリ用に実装されているため、nreal light用にカスタマイズをする必要があります。カスタマイズのポイントは2点です。

**1.カメラ映像取得クラスの変更**
Zxing.NETのサンプルコードでは、カメラから映像を取得するために`WebCamTextureクラス`を使用しています。
`WebCamTextureクラス`は、Unity標準提供のクラスで、スマホのカメラや、Webブラウザが接続しているカメラから映像を取得するためのクラスです。
ですが、nreal lightのカメラは`WebCamTextureクラス`に対応していないため、nreal専用の`NRRGBCamTextureクラス`を使用します。そのため、サンプルコード中のWebCamTextureクラスの定義を`NRRGBCamTextureクラス`に変更します。

```c#:Webカメラから映像を取得するクラスをNRRGBCamTextureクラスに変更する
private WebCamTexture camTexture;
```
　　　　　　　↓

```c#

private NRRGBCamTexture camTexture;
```

**2.映像取得方法の変更**
カメラ映像の取得クラスを`WebCamTextureクラス`から`NRRGBCamTextureクラス`に変更したことにより映像情報を取得する処理も若干変更があります。
WebCamTextureクラスは、`Textureクラス（画像情報クラス）`のサブクラスとして実装しているため、WebCamTextureクラスをインスタンス化後、カメラを通じて取得した映像は、WebCamTextureオブジェクトそのものに返却されます。
NRRGBCamTextureクラスは、Textureクラスのサブクラスではなく独自クラスとして実装されており、NRRGBCamTextureクラスのメンバーとしてTextureオブジェクトを持ちます。そのため、映像の取得にはNRRGBCamTexture#GetTextureメソッドを使用します。

```c#:Webカメラから映像を取得するコードを変更する
GUI.DrawTexture(screenRect, camTexture, ScaleMode.ScaleToFit);
```
　　　　　　　              　　　↓

```c#
GUI.DrawTexture(screenRect, camTexture.GetTexture(), ScaleMode.ScaleToFit);
```

## ソースコード
nreal light用にカスタマイズしたBarcodeCamクラス（BarcodeCam.cs）、BarcodeCamクラス用いてQRコードから読み込んだ情報をnreal lightのレンズ上に表するクラス（SceneDirector.cs）の全体像は下記のとおりです。

```C#:BarcodeCam.cs（QRコード読み込みの実装）
using NRKernal;
using System.Threading;
using UnityEngine;
using ZXing;
using ZXing.QrCode;

public class BarcodeCam : MonoBehaviour
{
    // Texture for encoding test
    public Texture2D encoded;
    private NRRGBCamTexture camTexture;
    private Thread qrThread;
    private Color32[] c;
    private int W, H;
    private Rect screenRect;
    private bool isQuit;
    public string LastResult;
    private bool shouldEncodeNow;
    private string qrValue = "Not Found.";

    public string GetQRValue() {
        return this.qrValue;
    }

    void OnGUI()
    {
        GUI.DrawTexture(screenRect, camTexture.GetTexture(), ScaleMode.ScaleToFit);
    }

    void OnEnable()
    {
        if (camTexture != null)
        {
            camTexture.Play();
            W = camTexture.Width;
            H = camTexture.Height;
        }
    }

    void OnDisable()
    {
        if (camTexture != null)
        {
            camTexture.Pause();
        }
    }

    void OnDestroy()                                                                                                      
    {
        qrThread.Abort();
        camTexture.Stop();
    }

    // It's better to stop the thread by itself rather than abort it.
    void OnApplicationQuit()
    {
        isQuit = true;
    }

    void Start()
    {
        encoded = new Texture2D(256, 256);
        LastResult = "http://www.google.com";
        shouldEncodeNow = true;
        screenRect = new Rect(0, 0, Screen.width, Screen.height);
        camTexture = new NRRGBCamTexture();
        OnEnable();

        qrThread = new Thread(DecodeQR);
        qrThread.Start();
    }

    void Update()
    {
        if (c == null)
        {
            c = camTexture.GetTexture().GetPixels32();
        }

        // encode the last found
        var textForEncoding = LastResult;
        if (shouldEncodeNow &&
            textForEncoding != null)
        {
            var color32 = Encode(textForEncoding, encoded.width, encoded.height);
            encoded.SetPixels32(color32);
            encoded.Apply();
            shouldEncodeNow = false;
        }
    }

    void DecodeQR()
    {
        Debug.Log("DecodeQR");

        // create a reader with a custom luminance source
        var barcodeReader = new BarcodeReader { AutoRotate = false, TryHarder = false };

        while (true)
        {
            if (isQuit)
                break;

            try
            {
                // decode the current frame
                var result = barcodeReader.Decode(c, W, H);
                if (result != null)
                {
                    LastResult = result.Text;
                    shouldEncodeNow = true;
                    print(result.Text);
                    this.qrValue = result.Text.ToString();
                }

                // Sleep a little bit and set the signal to get the next frame
                Thread.Sleep(200);
                c = null;
            }
            catch
            {
            }
        }
    }

    private static Color32[] Encode(string textForEncoding, int width, int height)
    {
        var writer = new BarcodeWriter
        {
            Format = BarcodeFormat.QR_CODE,
            Options = new QrCodeEncodingOptions
            {
                Height = height,
                Width = width
            }
        };
        return writer.Write(textForEncoding);
    }
}
```
```C#:SceneDirector.cs（BarcodeCamで読み込んだQRコードの内容を画面のText欄に表示する）
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class SceneDirector : MonoBehaviour
{
    private GameObject qrText;
    private GameObject cam;


    // Start is called before the first frame update
    void Start()
    {
        this.qrText = GameObject.Find("QRText");
        this.cam = GameObject.Find("BarcodeCam");
    }

    // Update is called once per frame
    void Update()
    {
        this.qrText.GetComponent<Text>().text = this.cam.GetComponent<BarcodeCam>().GetQRValue();
    }
}
```

# おわりに
今回の試みで、Zxing.NETと提供されているサンプルコードを用いれば、nreal lightによるQRコードリーダーは比較的簡単に実現できたのではないかと思います。

一方で使用感の面では、QRコードにカメラのピントを合わせる際に工夫が必要だと感じました。グラス型デバイスはスマホのカメラと違い、「カメラから見えている映像」を利用者が確認できないためにうまく認識できないことがありました。。レンズの端にカメラが写している映像をワイプ表示する、などの対応で改善することができるでしょう。

グラス型デバイスでQRコードを読み込むというアプローチはなかなか斬新な手段ではないかと思います。興味のある方は、ぜひnreal lightと本記事で紹介したソースコードを利用して体験してみてください。

最後に、nrealアプリ開発に関するTipsなどを[記事](https://qiita.com/mininobu/items/9fe23a21e628e0d347ed)にまとめています。nrealアプリの開発に興味のある方は、ぜひそちらも覗いてみてください。