LoginSignup
8
15

More than 5 years have passed since last update.

QRコードカメラプレビュー

Last updated at Posted at 2017-05-23

 本記事は以下の環境で動作を確認しています。

バージョン
NAOqi 2.5.5.5
Choregraphe 2.5.5.5

PepperにQRコードをスムーズに読み込ませる

 PepperはQRコードを簡単に認識できる仕組みが備わっています。QRコードを認識するとイベントBarcodeReader/BarcodeDetectedが発火し、認識した結果を利用できます。

fig1.png
メモリイベントを選択し、受信したいボックスへ結線すればよい

 QRコードの認識は簡単に実装できますが、かざしたQRコードがPepperの目にどのように見えているのかをリアルタイムに確認できないため、利用者はうまくかざせているのか不安になります。
 カメラの映像をディスプレイに表示させPepper側でどのように映像が写っているのかをリアルタイムに確認できると使い勝手がよくなるはずです。
(ソフトバンクロボティクスが作成しているロボアプリはこの手法がよく用いられています。)
 しかし、QRコードの認識と違い、カメラプレビューは簡単には実現できません。ソフトバンクロボティクス村山 龍太郎、谷沢 智史、西村 一彦著『Pepperプログラミング 基本動作からアプリの企画・演出まで』から該当部分を抽出し、簡略化したサンプルを作成しましたので、その仕組みを解説したいと思います。

サンプルプログラム

 サンプルプログラムはGitHubよりダウンロードしてください。
 https://github.com/Atelier-Akihabara/pepper-qrpreview-example

 サンプルを実行すると以下のように表示されます。
fig2.png

  • QRコードを認識すると、アプリが終了します。

プログラムの構造

 プログラムは以下の流れになっております。

1. Pepper側

a. ディスプレイを利用するため、[Show App]を利用しHTMLを表示できるようにします。
b. Pepperのカメラにアクセスを行います。
c. データを受信するためのsubscriberIdを取得します。
d. 受け取ったsubscriberIdをALMemory経由で、ディスプレイ側に渡します。

2. ディスプレイ側

a. Pepper側から送られてくるsubscriberIdを受け取れるようにハンドラを作成します。
b. 作成したハンドラで受けたsubscriberId使いカメラ画像データを受け取ります。
c. 受け取ったカメラ画像データをCanvasを使い描画します。
d. b. c. を必要な回数実行します。

Pepper側

 Pepper側から処理を追っていきます。Pepper側では画像取得に必要なsubscriberIdをディスプレイ側に渡すことがポイントです。

1-a.

 ディスプレイを利用するために、[Show App]を利用し以下のHTML(index.html)を表示させます。

index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="initial-scale = 1, minimum-scale = 1, maximum-scale = 1" />
    <link rel="stylesheet" type="text/css" href="main.css">
    <script src="/libs/qimessaging/2/qimessaging.js"></script>
    <script src="scripts/jquery-2.2.2.min.js"></script>
    <script src="scripts/adjust23.js"></script>
    <script src="scripts/main.js"></script>
      </head>
  <body onLoad="startSession();">
    <div class="container">
        <div id="camera" class="panel inactive">
            <div class="canvas-container">
                <div class="canvas-background">
                    <canvas id="canvas" width="80" height="60"></canvas>
                </div>
            </div>
        </div>
    </div>
    <div class="abort-container inactive" id="common">
        <div class="abort-button" id="abort">中断</div>
    </div>
  </body>
</html>

 ここで注目してもらいたいのは、<canvas id="canvas" width="80" height="60"></canvas>です。この領域にカメラ画像が描画されます。
 <script src="scripts/main.js"></script>は後続する描画処理が含まれます。startSession()は描画処理の起動関数です。表示させるための箱(キャンバス)が必要だということがポイントです。

1-b, c.

 [root/QR Preview/Subscribe Camera]をご覧ください。

self.subscriberId = self.videoDevice.subscribeCamera("QRPreview",
                    0, # TopCamera
                    7, # AL:kQQQVGA
                    0, # AL::kYuvColorSpace
                    fps)

 ALVideoDevice::subscribeCameraを呼び出し、subscriberIdを取得しています。引数は以下の順番で構成されています。

名前 内容
Name 名前
CameraIndex カメラのインデックス(0つまり額のカメラ)
Resolution カメラの解像度を示す定数(定数AL:kQQQVGAに対応する7、QQQVGAはつまり80x60ピクセルです)
ColorSpace カメラから得られる画像フォーマット(定数AL::kYuvColorSpaceに対応する0、YuvColorSpaceとはYUVカラースペースの輝度信号のみになります)
Fps 要求するフレームレート

 各種パラメーターで設定している定数については以下を参照してください。
 http://doc.aldebaran.com/2-5/family/pepper_technical/video_pep.html

 subscribeCameraの戻り値がself.subscriberIdに入ります。これでカメラデータにアクセスできるようなりました。

補足:AL::kから始まっている定数はC++言語での表現方法のため、Python側では具体的な数字にする必要があります。

1-d.

 引き続き[root/QR Preview/Subscribe Camera]をご覧ください。

self.memory.raiseEvent("ProgrammingPepperSample/PreviewMode", self.subscriberId)

 このコードでALMemoryを経由して先ほど取得したsubscriberIdをディスプレイ側に渡します。ディスプレイ側ではこのsubscriberIdを使いカメラから描画するためのデータを取り出します。

ディスプレイ側

 いよいよディスプレイ側です。Pepper側から描画処理ができないため、ディスプレイ側ではJavaScript上でピクセルプロセッシングが必要となります。

2-a.

 main.js をご覧ください。

main.js
ALMemory.subscriber("ProgrammingPepperSample/PreviewMode").then(function(subscriber) {
    subscriber.signal.connect(function(subscriberId) {
        if(subscriberId.length > 0) {
            console.log("Subscribing...: " + subscriberId)
            previewRunning = true
            activatePanel("camera")
            activatePanel("common")
            getImage(ALVideoDevice, subscriberId)
        }else{
            previewRunning = false
            inactivatePanel("camera")
            inactivatePanel("common")
        }
    })
});

 ここではgetImage(ALVideoDevice, subscriberId)に処理をさせるための部分がポイントです。"ProgrammingPepperSample/PreviewMode"が飛んできたら、getImage(ALVideoDevice, subscriberId)が呼び出されます。
 内部で呼び出されているそのほかの関数はHTML上の要素を切り替えているにすぎません。

2-b, c.

 いよいよ描画処理です。関数getImage()で加工されます。

main.js
function getImage(ALVideoDevice, subscriberId) {
    ALVideoDevice.getImageRemote(subscriberId).then(function (image) {
        if(image) {
            var imageWidth = image[0];
            var imageHeight = image[1];
            var imageBuf = image[6];
            console.log("Get image: " + imageWidth + ", " + imageHeight);

            if (!context) {
                context = document.getElementById("canvas").getContext("2d");
            }
            if (!imgData || imageWidth != imgData.width || imageHeight != imgData.height) {
                imgData = context.createImageData(imageWidth, imageHeight);
            }
            var data = imgData.data;

            for (var i = 0, len = imageHeight * imageWidth; i < len; i++) {
                var v = imageBuf[i];
                data[i * 4 + 0] = v;
                data[i * 4 + 1] = v;
                data[i * 4 + 2] = v;
                data[i * 4 + 3] = 255;
            }

            context.putImageData(imgData, 0, 0);
        }

        if(previewRunning) {
            setTimeout(function() { getImage(ALVideoDevice, subscriberId) }, 100)
        }
    })
}

 ALVideoDevice.getImageRemoteによりPepperから画像データを受け取ります。画像データ(image)は以下の構造で渡されます。

内容
[0] 横サイズ
[1] 縦サイズ
[2] レイヤーの数
[3] カラースペース
[4] タイムスタンプ (seconds).
[5] タイムスタンプ (microseconds).
[6] 画像データのバイナリ配列
[7] カメラID
[8] カメラ左アングル情報 (radian).
[9] カメラ上アングル情報 (radian).
[10] カメラ右アングル情報 (radian).
[11] カメラ下アングル情報 (radian).

 必要な情報は[6]の情報つまり画像データになります。

 ここでキャンバスに描くための初期化(ブランクイメージの作成)を行います。

if (!context) {
    context = document.getElementById("canvas").getContext("2d");
}
if (!imgData || imageWidth != imgData.width || imageHeight != imgData.height) {
    imgData = context.createImageData(imageWidth, imageHeight);
}

 そして、putImageDataを通してデータを描画します。

context.putImageData(imgData,x,y);

 x, y は描画を開始する座標が入ります。始点は0, 0ですので、x及びyには0を指定します。imageDataには描画するデータの配列をRGBAの順番で配列に入れることになります。Rは赤、Gは緑、Bは青、Aはアルファチャンネルになります。Pepperから得たカメラデータは輝度情報ですので、それぞれのRGBに同じ値を入れれば、モノクロの画像を得ることができます。Aはアルファチャンネルですので、透過しないように255を入れます。

for (var i = 0, len = imageHeight * imageWidth; i < len; i++) {
    var v = imageBuf[i];
    data[i * 4 + 0] = v;
    data[i * 4 + 1] = v;
    data[i * 4 + 2] = v;
    data[i * 4 + 3] = 255;
}

 上記で静止画を描画できるようなりました。

2-d.

 画像は絶えず更新される必要があります。

setTimeout(function() { getImage(ALVideoDevice, subscriberId) }, 100)

 再帰的に動作させて描画します。

最後に

 Pepperのディスプレイにカメラプレビューを表示させる流れが掴めたでしょうか。ポイントは、subscriberIdを取得しディスプレイ側で描画処理を行うことです。
 サンプルではQQQVGA(80x60)という非常に低い解像度なっていますが、より高い解像度での画像も取得できます。また、サンプルはモノクロでしたが、フォーマットと描画ルーチンを変えることで、カラー画像も取得できます。
 ですが、描画処理を行うのはHTML上のJavaScriptになります。高解像度で描画を行なったり、カラー処理を行なったりすると処理が追いつかなくなる場合があります。作成したアプリ上で快適に動作する設定を探してみてください。

8
15
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
15