AndroidThings & Raspberry Pi3 で猫用扇風機を作る（4）

2017年10月28日に実施された「[東北Tech道場 郡山道場 第7回](https://medium.com/@koriyamadojo/%E6%9D%B1%E5%8C%97tech%E9%81%93%E5%A0%B4-%E9%83%A1%E5%B1%B1%E9%81%93%E5%A0%B4-%E7%AC%AC%EF%BC%93%E6%9C%9F-%E7%AC%AC%EF%BC%97%E5%9B%9E%E9%96%8B%E5%82%AC-15f7a2af5b35)」の作業内容をまとめます。

前回までの作業で、公式サンプルアプリのDoorbellをほどんど流用して、5秒おきに写真を撮る「CatFans」を作成しましたが、いくつか問題が出てきました。

- スマホでFirebaseに保存した画像と注釈を確認すると、画像は表示されるが注釈は「no annotations yet」になっている。
- 画像サイズが2Mバイトを超えるため、スマホで確認するときに通信量が多くなる。

今回はこれらの問題を解決することにしました。

#1. "no annotations yet" になる問題

##(1) HandlerThreadがうまく動作していない
元々Doorbellでは、LooperとHandlerを使って、Cloud Vision APIを呼んでいます。調べたところによると、Androidでは時間のかかる処理はHandlerThreadを使ってバッググラウンドで処理をするように作るようです。LooperとHandlerはそのための仕組みです。Doorbellでは、カメラから取得したイメージとタイムスパンは即時にFirebaseに書き込みに行くのですが、Cloud Vision APIで画像解析する部分は時間がかかるためだと思いますが、別スレッドで動くようになっています。ところが、ブレークポイントを設置してもこのスレッドに一向に止まらないのです。そのため、画像はFirebeseに保存するが、注釈は付かない、という状況になっています。

###(i) HandlerThreadを外してみる

試しに、スレッドは使わず、画像のFirebeseへのアップロードと同時にCloud Vision APIを呼ぶようにコメントアウトしてみました。けれども、Cloud Vision APIを呼んでいる
`Map<String, Float> annotations = CloudVisionUtils.annotateImage(imageBytes);`
の部分で戻ってこなくなってしまいました。と、思ったら戻ってきましたがものすごく時間がかかります。
これは、ファイルサイズが大きいためではないかと思い、ファイルサイズを小さくすることにしました。

###(ii) ファイルサイズを小さくする
カメラで撮った画像のサイズはおよそ2Mバイトもあるので、「[Android: 画像周りの処理を楽にするUtilクラス。Bitmap生成、拡大縮小リサイズ、バイト配列＜=＞Bitmapなどなど](https://qiita.com/ogawatachi/items/3000f0b8232e96bd762e)」から「createScaleBitmap」メソッドを使わせていただき、320×240の大きさにしてからCloud Vision APIに渡すようにしました。すると、すぐに戻ってくるようになりました。また、画像サイズもおよそ25kバイトと飛躍的に少なくなりました。

###(iii) コメントアウトを戻す
そこで、(i)でコメントアウトしていた部分を戻してみました。すると、スレッドも正常に動くようになりました。結局、カメラの性能が良すぎるため、画像サイズが大きすぎて正常に処理できなかった、というのが "no annotations yet" になった理由のようです。

#2. 修正ソース

以下、今回修正した部分のソースです。なお、BitmapUtilsクラスは、「[Android: 画像周りの処理を楽にするUtilクラス。Bitmap生成、拡大縮小リサイズ、バイト配列＜=＞Bitmapなどなど](https://qiita.com/ogawatachi/items/3000f0b8232e96bd762e)」を一部変更しています。

```java:BitmapUtils.java
package xxxxxx.catfans;

/**
 * https://qiita.com/ogawatachi/items/3000f0b8232e96bd762e
 */

import java.io.ByteArrayOutputStream;

import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;

/**
 * 画像変換クラス
 *
 */
public class BitmapUtils {

    /**
     * バイト配列からBitpmap生成 表示サイズ合わせて拡大縮小します。
     *
     * @param bytes
     *            バイト配列の画像
     * @param wPercent
     *            表示幅拡大縮小率
     * @param hPercent
     *            表示高さ拡大縮小率
     * @return 生成Bitmap
     */
    public static Bitmap createScaleBitmap(byte[] bytes, float wPercent, float hPercent) {

        Bitmap bm = byte2bmp(bytes);
        Matrix matrix = new Matrix();

        float xScale = wPercent * 0.8f;
        float yScale = hPercent * 0.8f;

        matrix.postScale(xScale, yScale);

        return Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
    }

    /**
     * バイト配列をbitmapに変換します。
     *
     * @param bytes
     * @return
     */
    public static Bitmap byte2bmp(byte[] bytes) {
        Bitmap bmp = null;
        if (bytes != null) {
            bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
        }

        return bmp;
    }

    /**
     * bitmapをバイト配列に変換します。
     *
     * @param bitmap
     *            ビットマップ
     * @param format
     *            圧縮フォーマット
     * @param compressVal
     *            圧縮率
     * @return
     */
    public static byte[] bmp2byteArray(Bitmap bitmap, CompressFormat format, int compressVal) {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        bitmap.compress(format, compressVal, baos);
        return baos.toByteArray();

    }
}
```

```java:CatFansActivity.java
package xxxxxx.catfans;

import android.Manifest;
import android.app.Activity;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.media.Image;
import android.media.ImageReader;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Base64;
import android.util.Log;

import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ServerValue;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Map;

import java.util.TimerTask;
import java.util.Timer;

import static kitcut.catfans.BitmapUtils.bmp2byteArray;
import static kitcut.catfans.BitmapUtils.createScaleBitmap;

/**
 * ラスベリーパイ3カメラの画像をタイマーでキャプチャし、
 * FirebaseとGoogle Cloud Vision APIに投稿するCatFansアクティビティ
 */
public class CatFansActivity extends Activity {
    private static final String TAG = CatFansActivity.class.getSimpleName();

    private FirebaseDatabase mDatabase;
    private CatFansCamera mCamera;
    private Timer timer;

    /**
     * A {@link Handler} カメラタスクをバックグラウンドで実行するために必要
     */
    private Handler mCameraHandler;

    /**
     * UIをブロックすべきでないCameraタスクを実行するための追加のスレッド
     */
    private HandlerThread mCameraThread;

    /**
     * A {@link Handler} バックグラウンドでCloudタスクを実行
     */
    private Handler mCloudHandler;

    /**
     * UIをブロックすべきでないCloudタスクを実行するための追加のスレッド。
     */
    private HandlerThread mCloudThread;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "CatFans Activity created.");

        // カメラにアクセスするための許可が必要
        if (checkSelfPermission(Manifest.permission.CAMERA)
                != PackageManager.PERMISSION_GRANTED) {
            // 権限を自動付与する際に問題が発生した
            Log.d(TAG, "No permission");
            return;
        }

        mDatabase = FirebaseDatabase.getInstance();

        // カメラおよびネットワーク操作用の新しいハンドラと関連するスレッドを作成
        mCameraThread = new HandlerThread("CameraBackground");
        mCameraThread.start();
        mCameraHandler = new Handler(mCameraThread.getLooper());

        mCloudThread = new HandlerThread("CloudThread");
        mCloudThread.start();
        mCloudHandler = new Handler(mCloudThread.getLooper());

        mCamera = CatFansCamera.getInstance();
        mCamera.initializeCamera(this, mCameraHandler, mOnImageAvailableListener);

        //タイマーの作成と設定
        timer = new Timer();
        TimerTask timerTask = new CatFansTimerTask(this);
        timer.scheduleAtFixedRate(timerTask, 0, 5000);      //5秒ごとにタイマーを起動
    }

    //カメラで写真を撮る
    public void takePicture() {
        mCamera.takePicture();
        Log.d(TAG, "Take a picture.");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mCamera.shutDown();

        mCameraThread.quitSafely();
        mCloudThread.quitSafely();
    }

    /**
     * 新しいカメラ画像のリスナー
     */
    private ImageReader.OnImageAvailableListener mOnImageAvailableListener =
            new ImageReader.OnImageAvailableListener() {
                @Override
                public void onImageAvailable(ImageReader reader) {
                    Image image = reader.acquireLatestImage();
                    // イメージバイトを取得する
                    ByteBuffer imageBuf = image.getPlanes()[0].getBuffer();
                    final byte[] imageBytes = new byte[imageBuf.remaining()];
                    imageBuf.get(imageBytes);

                    onPictureTaken(imageBytes);

                    image.close();
                }
            };


    /**
     * Cloud Visionで画像にコメントを付けてからFirebaseにアップロードする
     */
    private void onPictureTaken(final byte[] imageBytes) {
        if (imageBytes != null) {
            final DatabaseReference log = mDatabase.getReference("logs").push();

            //画像を画面サイズに合わせて縮小する
            Bitmap b = createScaleBitmap(imageBytes, 1.0F, 1.0F);
            byte[] temp = bmp2byteArray(b, Bitmap.CompressFormat.JPEG, 100);
            final byte[] imageB = temp;

            String imageStr = Base64.encodeToString(imageB, Base64.NO_WRAP | Base64.URL_SAFE);

            // イメージをfirebaseにアップロードする
            log.child("timestamp").setValue(ServerValue.TIMESTAMP);
            log.child("image").setValue(imageStr);

            mCloudHandler.post(new Runnable() {
                @Override
                public void run() {
                    Log.d(TAG, "sending image to cloud vision");
                    // Cloud Vision APIにアップロードして画像に注釈を付ける
                    try {
                        Map<String, Float> annotations = CloudVisionUtils.annotateImage(imageB);
                        Log.d(TAG, "cloud vision annotations:" + annotations);
                        if (annotations != null) {
                            log.child("annotations").setValue(annotations);
                        }
                    } catch (IOException e) {
                        Log.e(TAG, "Cloud Vison API error: ", e);
                    }
                }
            });
        }
    }
}
```

第8回に続きます。